diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..ee18842f61e --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.checkstyle +.classpath +.pmd +.project +.ruleset +.settings/ +target/ +*.iml +*.iws +*.ipr +velocity.log +maven-eclipse.xml +.externalToolBuilders +.idea/ +*~ +dependency-reduced-pom.xml diff --git a/README.md b/README.md new file mode 100644 index 00000000000..b330dffa7c6 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Apache Felix + +The **Apache Felix** project is a collection of semi-related **OSGi** sub-projects that build and release individually. + +## Felix Framework + +The flagship project is the **Apache Felix Framework** which implements the [**OSGi Core R7**](https://osgi.org/specification/osgi.core/7.0.0/) specification. The `/framework` directory contains the source and build tree for the **OSGi**-compliant +framework implementation. + +Directly related projects: + +- **main** `/main*` - provides an executable jar that launches the Felix framework. + +## OSGi Compendium + +Several sub-projects cover various **OSGi Compendium** specifications such as: + +- [**Configuration Admin**](https://osgi.org/specification/osgi.cmpn/7.0.0/service.cm.html) `/configadmin` +- [**Configurator**](https://osgi.org/specification/osgi.cmpn/7.0.0/service.configurator.html) `/configurator` +- [**Converter**](https://osgi.org/specification/osgi.cmpn/7.0.0/util.converter.html) `/converter` +- [**Coordinator**](https://osgi.org/specification/osgi.cmpn/7.0.0/service.coordinator.html) `/coordinator` +- [**Deployment Admin**](https://osgi.org/specification/osgi.cmpn/7.0.0/service.deploymentadmin.html) `/deploymentadmin` +- [**Device Access**](https://osgi.org/specification/osgi.cmpn/7.0.0/service.device.html) `/deviceaccess` +- [**Declarative Services**](https://osgi.org/specification/osgi.cmpn/7.0.0/service.component.html) `/scr*` +- [**Event Admin**](https://osgi.org/specification/osgi.cmpn/7.0.0/service.event.html) `/eventadmin` +- [**Http Service**](https://osgi.org/specification/osgi.cmpn/7.0.0/service.http.html) `/http` +- [**Http Whiteboard**](https://osgi.org/specification/osgi.cmpn/7.0.0/service.http.whiteboard.html) `/http` +- [**IO**](https://osgi.org/specification/osgi.cmpn/7.0.0/service.io.html) `/io` +- [**Log Service**](https://osgi.org/specification/osgi.cmpn/7.0.0/service.log.html) `/log*` +- [**Metatype**](https://osgi.org/specification/osgi.cmpn/7.0.0/service.metatype.html) `/metatype` +- [**Preferences**](https://osgi.org/specification/osgi.cmpn/7.0.0/service.prefs.html) `/prefs` +- [**Resolver**](https://osgi.org/specification/osgi.core/7.0.0/service.resolver.html) `/resolver` +- [**UPnP**](https://osgi.org/specification/osgi.cmpn/7.0.0/service.upnp.html) `/upnp` +- [**User Admin**](https://osgi.org/specification/osgi.cmpn/7.0.0/service.useradmin.html) `/useradmin` +- [**Wire Admin**](https://osgi.org/specification/osgi.cmpn/7.0.0/service.wireadmin.html) `/wireadmin` + +## Extra Features + +Several projects provide extra features to an OSGi runtime. + +- **bundle repository** `/bundlerepository` - Bundle repository service. +- **connect** `/connect` - A service registry that enables OSGi style service registry programs without using an OSGi framework. +- **dependency manager** `/dependencymanager` - A versatile java API, allowing to declaratively + register, acquire, and manage dynamic OSGi services. +- **fileinstall** `/fileinstall*` - A utility to automatically install bundles from a directory. +- **gogo** `/gogo` - A command line shell, runtime and set of base commands for interacting with and introspecting an OSGi framework. +- **inventory** `/inventory` - Provides some mechanisms to get the current state of the system and therefore provides an inventory of the system. +- **ipojo** `/ipojo` - A *service component runtime* aiming to simplify OSGi application development. +- **jaas support** `/jaas` - Bundle to simplify JAAS usage within OSGi environment. +- **logback** `/logback` - A simple integration of the OSGi R7 Log (1.4) service to Logback backend. +- **rootcause** `/rootcause` - Finding the root cause of problems with OSGi declarative services components. +- **systemready** `/systemready` - Provides a framework to configure and create so called system checks, and signal the ready state of an OSGi based system. +- **utils** `/utils` - Utility classes for OSGi (intended for embedding within other bundles.) +- **webconsole** `/webconsole*` - Web Based Management Console for OSGi Frameworks. +- and many other **OSGi** things + +## Build tools + +The `/tools` directory contains various build tools. + +- **maven-bundle-plugin** `/tools/maven-bundle-plugin` - A maven plugin for building **OSGi** bundles. +- **osgicheck-maven-plugin** `/tools/osgicheck-maven-plugin` - Maven plugin for checking several OSGi aspects of your project. + diff --git a/build_run.sh b/build_run.sh deleted file mode 100755 index 7623c5ca1c8..00000000000 --- a/build_run.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -mvn clean install; -cd main/ ; -java -jar bin/felix.jar ; -cd ../ diff --git a/bundlerepository.osgi-ct/pom.xml b/bundlerepository.osgi-ct/pom.xml new file mode 100644 index 00000000000..320ab2f2309 --- /dev/null +++ b/bundlerepository.osgi-ct/pom.xml @@ -0,0 +1,101 @@ + + + + org.apache.felix + felix-parent + 2.1 + ../../pom/pom.xml + + 4.0.0 + bundle + Apache Felix Bundle Repository - OSGi CT integration + + Bundle repository service OSGi CT integration. To run a Repository implementation in the + OSGi CT, a small integration layer needs to be provided by the implementation that knows + how to prime the repository with the provided repository xml file. + + org.apache.felix.bundlerepository.osgi-ct + 2.0.3-SNAPSHOT + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/bundlerepository.osgi-ct + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/bundlerepository.osgi-ct + http://svn.apache.org/repos/asf/felix/trunk/bundlerepository.osgi-ct + + + + org.osgi + org.osgi.core + 5.0.0 + + + org.osgi + org.osgi.compendium + 5.0.0 + + + ${project.groupId} + org.apache.felix.bundlerepository + ${project.version} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + + org.apache.felix + maven-bundle-plugin + 2.3.4 + true + + + + org.apache.felix.bundlerepository.osgict + org.apache.felix.bundlerepository.osgict.Activator + ${project.artifactId} + The Apache Software Foundation + + + + + org.apache.rat + apache-rat-plugin + + false + true + true + + doc/* + maven-eclipse.xml + .checkstyle + .externalToolBuilders/* + + + + + + diff --git a/bundlerepository.osgi-ct/src/main/java/org/apache/felix/bundlerepository/osgict/Activator.java b/bundlerepository.osgi-ct/src/main/java/org/apache/felix/bundlerepository/osgict/Activator.java new file mode 100644 index 00000000000..d1d65b56815 --- /dev/null +++ b/bundlerepository.osgi-ct/src/main/java/org/apache/felix/bundlerepository/osgict/Activator.java @@ -0,0 +1,109 @@ +/* + * 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.bundlerepository.osgict; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.apache.felix.bundlerepository.RepositoryAdmin; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Filter; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; + +/** + * This Activator implements the required glue between an OSGi Repository implementation and the + * OSGi CT. It is needed to prime the repository with the data needed by the CT and works as + * follows: + * + */ +public class Activator implements BundleActivator +{ + private BundleContext bundleContext; + private ServiceTracker repoXMLTracker; + private ServiceTracker repoTracker; + + public void start(BundleContext context) throws Exception + { + bundleContext = context; + Filter f = context.createFilter("(&(objectClass=java.lang.String)(repository-xml=*))"); + repoXMLTracker = new ServiceTracker(context, f, null) { + @Override + public String addingService(ServiceReference reference) + { + try + { + String xml = super.addingService(reference); + handleRepositoryXML(reference, xml); + return xml; + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + }; + repoXMLTracker.open(); + } + + public void stop(BundleContext context) throws Exception + { + repoXMLTracker.close(); + if (repoTracker != null) + repoTracker.close(); + } + + private void handleRepositoryXML(ServiceReference reference, String xml) throws Exception + { + File tempXMLFile = bundleContext.getDataFile("repo-" + reference.getProperty("repository-xml") + ".xml"); + writeXMLToFile(tempXMLFile, xml); + + repoTracker = new ServiceTracker(bundleContext, RepositoryAdmin.class, null); + repoTracker.open(); + RepositoryAdmin repo = repoTracker.waitForService(30000); + repo.addRepository(tempXMLFile.toURI().toURL()); + tempXMLFile.delete(); + + Dictionary props = new Hashtable(); + props.put("repository-populated", reference.getProperty("repository-xml")); + bundleContext.registerService(String.class, "", props); + } + + private void writeXMLToFile(File tempXMLFile, String xml) throws IOException + { + FileOutputStream fos = new FileOutputStream(tempXMLFile); + try + { + fos.write(xml.getBytes()); + } + finally + { + fos.close(); + } + } +} diff --git a/bundlerepository/DEPENDENCIES b/bundlerepository/DEPENDENCIES new file mode 100644 index 00000000000..f91489810a9 --- /dev/null +++ b/bundlerepository/DEPENDENCIES @@ -0,0 +1,32 @@ +Apache Felix OSGi Bundle Repository +Copyright 2014 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +This product includes software from http://kxml.sourceforge.net. +Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany. +Licensed under BSD License. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2012). +Licensed under the Apache License 2.0. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2012). +Licensed under the Apache License 2.0. + +This product uses software developed at +The Codehaus (http://www.codehaus.org) +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 +- BSD License diff --git a/org.osgi.compendium/src/main/resources/LICENSE b/bundlerepository/LICENSE similarity index 100% rename from org.osgi.compendium/src/main/resources/LICENSE rename to bundlerepository/LICENSE diff --git a/bundlerepository/LICENSE.kxml2 b/bundlerepository/LICENSE.kxml2 new file mode 100644 index 00000000000..1fe595b0392 --- /dev/null +++ b/bundlerepository/LICENSE.kxml2 @@ -0,0 +1,19 @@ +Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/bundlerepository/NOTICE b/bundlerepository/NOTICE new file mode 100644 index 00000000000..515c8d58fe6 --- /dev/null +++ b/bundlerepository/NOTICE @@ -0,0 +1,16 @@ +Apache Felix OSGi Bundle Repository +Copyright 2014 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software from http://kxml.sourceforge.net. +Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany. +Licensed under BSD License. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2012). +Licensed under the Apache License 2.0. + diff --git a/bundlerepository/doc/apache-felix-osgi-bundle-repository.html b/bundlerepository/doc/apache-felix-osgi-bundle-repository.html new file mode 100644 index 00000000000..825a9ff4517 --- /dev/null +++ b/bundlerepository/doc/apache-felix-osgi-bundle-repository.html @@ -0,0 +1,524 @@ + + + + + + Apache Felix - Apache Felix OSGi Bundle Repository + + + +
Apache
+ +
+

Apache Felix OSGi Bundle Repository (OBR)

+ + + +

+ +

Motivation

+ +

The goal of the Apache Felix OSGi Bundle Repository (OBR) is two-fold:

+ +
    +
  1. To simplify deploying and using available bundles with Felix.
  2. +
  3. To encourage independent bundle development so that communities of interest can grow.
  4. +
+ + +

OBR achieves the first goal by providing a service that can +automatically install a bundle, with its deployment dependencies, from +a bundle repository. This makes it easier for people to experiment with +existing bundles. The second goal is achieved by raising the visibility +of the available bundles and providing access to both the executable +bundle and its source code. Hopefully, by making OBR and the bundles +themselves more visible, community members will be encouraged to +provide or improve service implementations.

+ +

Note: OBR provides access to the Felix' default bundle repository, +but you can also use it to deploy your own bundles by creating a bundle +repository meta-data file for your local bundles; see the obr list-url, obr add-url, and obr remove-url commands for more details.

+ +

+ +

Overview

+ +

For the most part, OBR is quite simple. An OBR "repository server" +is not necessary, since all functionality may reside on the client +side. OBR is able to provide its functionality by reading an XML-based +meta-data file that describes the bundles available to it. The +meta-data file essentially contains an XML encoding of the bundles' +manifest information. From the meta-data, OBR is able to construct +dependency information for deploying (i.e., installing and updating) +bundles.

+ +

OBR defines the following entities:

+ +
    +
  • Repository Admin - a service to access a federation of repositories.
  • +
  • Repository - provides access to a set of resources.
  • +
  • Resource - a description of an artifact to be installed on a device.
  • +
  • Capability - a named set of properties.
  • +
  • Requirement - an assertion on a capability.
  • +
  • Resolver - an object to resolve resource dependencies and to deploy them.
  • +
  • Repository file - XML file containing resource meta-data.
  • +
+ + +

The following diagram illustrates the relationships among these entities:

+ +

+ +

The client has access to a federated set of repositories via the Repository Admin service; such as depicted in this view:

+ +

+ +

+ +

OBR Repository File

+ +

The OBR repository file is an XML-based representation of bundle +meta-data. The goal is provide a generic model for describing +dependencies among resources; as such, the term resource is used instead of bundle in the OBR repository syntax; a detailed description of the OBR meta-data format is available in the OSGi RFC 112 +document; this document is not completely in sync with the +implementation, but the concepts are still correct. The following XML +snippet depicts the overall structure of a repository file:

+ +
+
<repository presentationname="..." symbolicname="..." ... >
+    <resource>
+        <description>...</description>
+        <size>...</size>
+        <documentation>...</documentation>
+        <source>...</source>
+        <category id="..."/>
+        <capability>...</capability>
+        ...
+        <requirement>...</requirement>
+        ...
+    </resource>
+    ...
+</repository>
+
+
+ +

The above repository defines a set of available resources, each +described by a set of meta-data. Some resource meta-data is purely +intended for human consumption; the most important aspects relate to +the generic capability/requirement model.

+ +

A resource can provide any number of capabilities. A capability is a +typed set of properties. For example, the following is an exported +package capability:

+ +
+
<capability name='package'>
+    <p n='package' v='org.foo.bar'/>
+    <p n='version' t='version' v='1.0.0'/>
+</capability>
+
+
+ +

This capability is of type 'package' and exports 'org.foo.bar' at +version '1.0.0'. Conversely, a requirement is a typed LDAP query over a +set of capability properties. For example, the following is an imported +package requirement:

+ +
+
<require name='package' extend='false'
+    multiple='false' optional='false'
+    filter='(&amp;(package=org.foo.bar)(version&gt;=1.0.0))'>
+    Import package org.foo.bar
+</require>
+
+
+ +

This requirement is of type 'package' and imports 'org.foo.bar' at +versions greater than '1.0.0'. Although this syntax looks rather +complicated with the '\&' and '\>=' syntax, it is simply the +standard OSGi LDAP query syntax in XML form (additionally, Peter Kriens +has created a tool called bindex to generate this meta-data from a bundle's manifest).

+ +

With this generic dependency model, OBR is able to provide mappings +for the various OSGi bundle dependencies; e.g., import/export package, +provide/require bundle, host/fragment, import/export service, execution +environment, and native code. In addition, it is possible for bundles +to introduce arbitrary dependencies for custom purposes.

+ +

Two other important pieces of meta-data are Bundle-SymbolicName and Bundle-Version; +these are standard OSGi bundle manifest attributes that OBR uses to +uniquely identify a bundle. For example, if you want to use OBR to +update a locally installed bundle, OBR gets its symbolic name and +version and searches the repository metadata for a matching symbolic +name. If the matching symbolic name is found, then OBR checks if there +is a newer version than the local copy using the bundle version number. +Thus, the symbolic name plus bundle version forms a unique key to match +locally installed bundles to remotely available bundles.

+ +

+ +

OBR Service API

+ +

Typically, OBR service clients only need to interact with the +Repository Admin service, which provides the mechanisms necessary to +discover available resources. The Repository Admin interface is defined +as follows:

+ +
+
public interface RepositoryAdmin
+{
+    public Resource[] discoverResources(String filterExpr);
+    public Resolver resolver();
+    public Repository addRepository(URL repository)?
+        throws Exception;
+    public boolean removeRepository(URL repository);
+    public Repository[] listRepositories();
+    public Resource getResource(String respositoryId);
+}
+
+
+ +

In order to resolve and deploy available resources, the Repository +Admin provides Resolver instances, which are defined as follows:

+ +
+
public interface Resolver
+{
+    public void add(Resource resource);
+    public Requirement[] getUnsatisfiedRequirements();
+    public Resource[] getOptionalResources();
+    public Requirement[] getReason(Resource resource);
+    public Resource[] getResources(Requirement requirement);
+    public Resource[] getRequiredResources();
+    public Resource[] getAddedResources();
+    public boolean resolve();
+    public void deploy(boolean start);
+}
+
+
+ +

When desired resources are discovered via the query mechanisms of +the Repository Admin, they are added to a Resolver instance which will +can be used to resolve all transitive dependencies and to reflect on +any resolution result. The following code snippet depicts a typical +usage scenario:

+ +
+
RepositoryAdmin repoAdmin = ... // Get repo admin service
+Resolver resolver = repoAdmin.resolver();
+Resource resource = repoAdmin.discoverResources(filterStr);
+resolver.add(resource);
+if (resolver.resolve())
+{
+    resolver.deploy(true);
+}
+else
+{
+    Requirement[] reqs = resolver.getUnsatisfiedRequirements();
+    for (int i = 0; i < reqs.length; i++)
+    {
+        System.out.println("Unable to resolve: " + reqs[i]);
+    }
+}
+
+
+ +

This code gets the Repository Admin service and then gets a Resolver +instance from it. It then discovers an available resource and adds it +to the resolver. Then it tries to resolve the resources dependencies. +If successful it deploys the resource to the local framework instance; +if not successful it prints the unsatisfied requirements.

+ +

OBR's deployment algorithm appears simple at first glance, but it is +actually somewhat complex due to the nature of deploying independently +developed bundles. For example, in an ideal world, if an update for a +bundle is made available, then updates for all of the bundles +satisfying its dependencies are also made available. Unfortunately, +this may not be the case, thus the deployment algorithm might have to +install new bundles during an update to satisfy either new dependencies +or updated dependencies that can no longer be satisfied by existing +local bundles. In response to this type of scenario, the OBR deployment +algorithm tries to favor updating existing bundles, if possible, as +opposed to installing new bundles to satisfy dependencies.

+ +

In the general case, OBR user's will not use the OBR API directly, +but will use its functionality indirectly from another tool or user +interface. For example, interactive access to OBR is available via a +command for Felix' shell service. The OBR shell command is discussed in the next section.

+ +

+ +

OBR Shell Command

+ +

Besides providing a service API, OBR implements a Felix shell +command for accessing its functionality. For the end user, the OBR +shell command is accessed using the text-based or GUI-based user +interfaces for Felix' shell service. This section describes the syntax +for the OBR shell command.

+ +

+ +

obr help

+ +

Syntax:

+
+
obr help [add-url | remove-url | list-url | list | info | deploy | start | source | javadoc]
+
+
+

This command is used to display additional information about the other OBR commands.

+ +

+ +

obr list-url

+ +

Syntax:

+
+
obr list-url
+
+
+

This command gets the URLs to the repository files used by the Repository Admin.

+ +

+ +

obr add-url

+ +

Syntax:

+
+
obr add-url [<repository-file-url> ...]
+
+
+

This command adds a repository file to the set of repository files +for which the Repository Admin service provides access. The repository +file is represented as a URL. If the repository file URL is already in +the Repository Admin's set of repository files, the request is treated +like a reload operation.

+ +

+ +

obr remove-url

+ +

Syntax:

+
+
obr remove-url [<repository-file-url> ...]
+
+
+

This command removes a repository file to the set of repository +files for which the Repository Admin service provides access. The +repository file is represented as a URL.

+ +

+ +

obr list

+ +

Syntax:

+
+
obr list [<string> ...]
+
+
+

This command lists bundles available in the bundle repository. If no +arguments are specified, then all available bundles are listed, +otherwise any arguments are concatenated with spaces and used as a +substring filter on the bundle names.

+ +

+ +

obr info

+ +

Syntax:

+
+
obr info <bundle-name>[;<version>] ...
+
+
+

This command displays the meta-data for the specified bundles. If a +bundle's name contains spaces, then it must be surrounded by quotes. It +is also possible to specify a precise version if more than one version +exists, such as:

+
+
obr info "Bundle Repository";1.0.0
+
+
+

The above example retrieves the meta-data for version "1.0.0" of the bundle named "Bundle Repository".

+ +

+ +

obr deploy

+ +

Syntax:

+
+
obr deploy <bundle-name>[;<version>] ... | <bundle-id> ...
+
+
+

This command tries to install or update the specified bundles and +all of their dependencies by default. You can specify either the bundle +name or the bundle identifier. If a bundle's name contains spaces, then +it must be surrounded by quotes. It is also possible to specify a +precise version if more than one version exists, such as:

+
+
obr deploy "Bundle Repository";1.0.0
+
+
+

For the above example, if version "1.0.0" of "Bundle Repository" is +already installed locally, then the command will attempt to update it +and all of its dependencies; otherwise, the command will install it and +all of its dependencies.

+ +

+ +

obr start

+ +

Syntax:

+
+
obr start [-nodeps] <bundle-name>[;<version>] ...
+
+
+

This command installs and starts the specified bundles and all of +their dependencies by default; use the "-nodeps" switch to ignore +dependencies. If a bundle's name contains spaces, then it must be +surrounded by quotes. If a specified bundle is already installed, then +this command has no effect. It is also possible to specify a precise +version if more than one version exists, such as:

+
+
obr start "Bundle Repository";1.0.0
+
+
+

The above example installs and starts the "1.0.0" version of the bundle named "Bundle Repository" and its dependencies.

+ +

+ +

obr source

+ +

Syntax:

+
+
obr source [-x] <local-dir> <bundle-name>[;<version>] ...
+
+
+

This command retrieves the source archives of the specified bundles +and saves them to the specified local directory; use the "-x" switch to +automatically extract the source archives. If a bundle name contains +spaces, then it must be surrounded by quotes. It is also possible to +specify a precise version if more than one version exists, such as:

+
+
obr source /home/rickhall/tmp "Bundle Repository";1.0.0
+
+
+

The above example retrieves the source archive of version "1.0.0" of +the bundle named "Bundle Repository" and saves it to the specified +local directory.

+ +

obr javadoc

+ +

Syntax:

+
+
obr javadoc [-x] <local-dir> <bundle-name>[;<version>] ...
+
+
+

This command retrieves the javadoc archives of the specified bundles +and saves them to the specified local directory; use the "-x" switch to +automatically extract the javadoc archives. If a bundle name contains +spaces, then it must be surrounded by quotes. It is also possible to +specify a precise version if more than one version exists, such as:

+
+
obr javadoc /home/rickhall/tmp "Bundle Repository";1.0.0
+
+
+

The above example retrieves the javadoc archive of version "1.0.0" +of the bundle named "Bundle Repository" and saves it to the specified +local directory.

+ +

+ +

Using OBR with a Proxy

+ +

If you use a proxy for Web access, then OBR will not work for you in +its default configuration; certain system properties must be set to +enable OBR to work with a proxy. These properties are:

+ +
    +
  • http.proxyHost - the name of the proxy host.
  • +
  • http.proxyPort - the port of the proxy host.
  • +
  • http.proxyAuth +- the user name and password to use when connecting to the proxy; this +string should be the user name and password separated by a colon (e.g., +rickhall:mypassword).
  • +
+ + +

These system properties can be set directly on the command line when +starting the JVM using the standard "-D<prop>=<value>" +syntax or you can put them in the lib/system.properties file of your +Felix installation; see documentation on configuring Felix for more +information.

+ +

+ +

Bundle Source Packaging

+ +

Coming soon...

+ +

+ +

Note on OSGi R3 Bundles

+ +

In contrast to OSGi R4 the previous specifications, most notably R3, allowed bundles without the Bundle-SymbolicName +header. The Felix OSGi Bundle Repository implementation heavily relies +on the symbolic name being defined in bundles. As a consequence bundles +without a symbolic name are not fully supported by the Bundle +Repository:

+ +
    +
  • Bundles installed in the framework are used by the Bundle +Repository implementation to resolve dependencies regardless of whether +they have a Bundle-SymbolicName header or not. Resolution of dependencies against the installed bundles takes place based on the Export-Package headers.
  • +
  • Bundles installed in the framework without a Bundle-SymbolicName +header cannot be updated by the Bundle Repository implementation +because updates from the bundle repository cannot be correlated to such +"anonymous" bundles.
  • +
+ + + +

+ +

Feedback

+ +

Subscribe to the Felix users mailing list by sending a message to users-subscribe@felix.apache.org; after subscribing, email questions or feedback to users@felix.apache.org.

+
+ diff --git a/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/apache-felix-small.png b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/apache-felix-small.png new file mode 100644 index 00000000000..95bfa5ef925 Binary files /dev/null and b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/apache-felix-small.png differ diff --git a/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/apache.png b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/apache.png new file mode 100644 index 00000000000..5132f65ec15 Binary files /dev/null and b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/apache.png differ diff --git a/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/button.html b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/button.html new file mode 100644 index 00000000000..dc64b6cd107 --- /dev/null +++ b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/button.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/button_data/2009-usa-125x125.png b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/button_data/2009-usa-125x125.png new file mode 100644 index 00000000000..117c6954ddf Binary files /dev/null and b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/button_data/2009-usa-125x125.png differ diff --git a/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/linkext7.gif b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/linkext7.gif new file mode 100644 index 00000000000..f2dd2dcfa9b Binary files /dev/null and b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/linkext7.gif differ diff --git a/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/logo.png b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/logo.png new file mode 100644 index 00000000000..dccbddcc35c Binary files /dev/null and b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/logo.png differ diff --git a/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/mail_small.gif b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/mail_small.gif new file mode 100644 index 00000000000..a3b7d9f06f5 Binary files /dev/null and b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/mail_small.gif differ diff --git a/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/obr-entities.png b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/obr-entities.png new file mode 100644 index 00000000000..81d37608e06 Binary files /dev/null and b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/obr-entities.png differ diff --git a/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/obr-high-level.png b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/obr-high-level.png new file mode 100644 index 00000000000..566f171fd93 Binary files /dev/null and b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/obr-high-level.png differ diff --git a/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/site.css b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/site.css new file mode 100644 index 00000000000..959ab0a592d --- /dev/null +++ b/bundlerepository/doc/apache-felix-osgi-bundle-repository_files/site.css @@ -0,0 +1,25 @@ +/* @override http://felix.apache.org/site/media.data/site.css */ + +body { background-color: #ffffff; color: #3b3b3b; font-family: Tahoma, Arial, sans-serif; font-size: 10pt; line-height: 140% } +h1, h2, h3, h4, h5, h6 { font-weight: normal; color: #000000; line-height: 100%; margin-top: 0px} +h1 { font-size: 200% } +h2 { font-size: 175% } +h3 { font-size: 150% } +h4 { font-size: 140% } +h5 { font-size: 130% } +h6 { font-size: 120% } +a { color: #1980af } +a:visited { color: #1980af } +a:hover { color: #1faae9 } +.title { position: absolute; left: 1px; right: 1px; top:25px; height: 81px; background: url(http://felix.apache.org/site/media.data/gradient.png) repeat-x; background-position: bottom; } +.logo { position: absolute; width: 15em; height: 81px; text-align: center; } +.header { text-align: right; margin-right: 20pt; margin-top: 30pt;} +.menu { border-top: 10px solid #f9bb00; position: absolute; top: 107px; left: 1px; width: 15em; bottom: 0px; padding: 0px; background-color: #fcfcfc } +.menu ul { background-color: #fdf5d9; list-style: none; padding-left: 4em; margin-top: 0px; padding-top: 2em; padding-bottom: 2em; margin-left: 0px; color: #4a4a43} +.menu a { text-decoration: none; color: #4a4a43 } +.main { position: absolute; border-top: 10px solid #cde0ea; top: 107px; left: 15em; right: 1px; margin-left: 2px; padding-right: 4em; padding-left: 1em; padding-top: 1em;} +.code { background-color: #eeeeee; border: solid 1px black; padding: 0.5em } +.code-keyword { color: #880000 } +.code-quote { color: #008800 } +.code-object { color: #0000dd } +.code-java { margin: 0em } \ No newline at end of file diff --git a/bundlerepository/doc/changelog.txt b/bundlerepository/doc/changelog.txt new file mode 100644 index 00000000000..d2aa1eb2552 --- /dev/null +++ b/bundlerepository/doc/changelog.txt @@ -0,0 +1,193 @@ + +Changes from 2.0.4 to 2.0.6 +--------------------------- + +** Improvement + * [FELIX-4764] - Add support to GZIP based compact index files + +Changes from 2.0.2 to 2.0.4 +--------------------------- + +** Bug + * [FELIX-3097] - LocalRepository is not updated when bundles are + * [FELIX-4571] - NullPointerException when using Repository impl with Aries subsystem impl + * [FELIX-4616] - BundleRepository ResourceComparator violates comparison contract + * [FELIX-4640] - missing (&(osgi.ee=JavaSE)(version=1.8)) when embedding in org.apache.felix.framework + +** Improvement + * [FELIX-4812] - BundleRepository can be quite CPU intensive when starting a lot of bundles + +Changes from 1.6.6 to 2.0.2 +--------------------------- + +** New Feature + * [FELIX-4368] - Support OSGi Repository 1.0 Specification + ** [FELIX-4369] - Support repository.xml as defined by OSGi Repository spec + ** [FELIX-4370] - Support Repository service as defined by OSGi spec + ** [FELIX-4371] - Pass the Repository 1.0 OSGi CT + +** Bug + * [FELIX-3257] - OBR resolver unable to pick up the highest bundle version when selecting the best candidate + * [FELIX-2465] - system.bundle should be automatically wired to the relevant bundle + * [FELIX-3842] - NPE in LocalRepositoryImpl + +Changes from 1.6.4 to 1.6.6 +--------------------------- + +** Bug + * [FELIX-2612] - [OBR] Doesn't work on Java 1.4 due to use of Boolean.parseBoolean() + * [FELIX-2884] - The multiplicity isn't taken into account by the maven bundle plugin and bundlerepository when generating the repository xml + * [FELIX-2912] - Host name is lost in exceptions when dealing with Windows shared drives + * [FELIX-2958] - Unable to remove previously added repository from OBR + +Changes from 1.6.2 to 1.6.4 +--------------------------- + +** Bug + * [FELIX-2306] - ClassCastException in Wrapper.unwrap() when calling Resolver.add(x implements Resource) + * [FELIX-2385] - Execution environment property is not correctly exposed + +Changes from 1.6.0 to 1.6.2 +--------------------------- + +** Bug + * [FELIX-2269] - Only the higher version of a given bundle is kept in a repository + * [FELIX-2276] - Authentication credentials for proxies are not set when retrieving resources + * [FELIX-2304] - Single quotes need to be escaped in xml attribute values + +Changes from 1.4.3 to 1.6.0 +--------------------------- + +** Bug + * [FELIX-1007] - OBR search doesn't take 'categories' into account + * [FELIX-1531] - Mandatory directive is ignored on the Export-Package when it comes to resolve the bundles + * [FELIX-1621] - OBR fails to take bundles into account that are already available in the framework + * [FELIX-1809] - OBR issue when using parameters with exported packages + * [FELIX-2081] - Attribtues and directives and not used on local resources + * [FELIX-2082] - Local resources should really be preferred over remote resources + * [FELIX-2083] - bundlerepository should mark dependencies it includes as optional + * [FELIX-2102] - Bad exception thrown when an obr url can not be resolved + * [FELIX-2114] - The reasons for adding a resource may contain the same requirement several times + * [FELIX-2126] - Dependencies of optional resources should be optional + * [FELIX-2136] - Improve OBR speed + * [FELIX-2138] - The resolver should prefer required resources over optional resources to minimize the set of required resources + * [FELIX-2139] - Move extensions to a new api into the bundlerepository module as to not pollute the org.osgi.* package + * [FELIX-2221] - DataModelHelper.filter() throws wrong Exception + +** Improvement + * [FELIX-280] - OBR should be able to confirm satisfaction of a filter, including availability of local resources + * [FELIX-483] - Log detailed information on invalid syntax in parsed repository xml requirements + * [FELIX-692] - OBR should provide an API for resolving bundles dependencies regardless of locally installed bundles + * [FELIX-1492] - Add option to exclude optional dependencies during OBR deploy + * [FELIX-2106] - Resolver scoped Repository + * [FELIX-2115] - The api offers no way to have a timeout or cancel the resolution if it takes too long + * [FELIX-2127] - The explanation given why a resource is include is insufficient + * [FELIX-2134] - Change the filter implementation + * [FELIX-2140] - The Requirement#isSatisfied() method should actually check the capability/requirement namespace + * [FELIX-2151] - Use Strings instead of URLs in the API + +** New Feature + * [FELIX-178] - OBR should expose some way to convert a locally installed bundle to a Resource + * [FELIX-2103] - Improve the OBR url handler to be able to access external bundles + * [FELIX-2144] - Add global requirements and capabilities + +** Task + * [FELIX-2104] - Add an optional faster stax based parser + * [FELIX-2211] - Simplify the repository parser based on KXml2 + * [FELIX-2215] - Refactor bundlerepository and maven bundle plugin obr data model + +Changes from 1.4.2 to 1.4.3 +--------------------------- + +** Bug + * [FELIX-1792] - Felix OBR seems to just randomly choose one of the satisifed bundles if more than one bundle meets the requirement + + +Changes from 1.4.1 to 1.4.2 +--------------------------- + +** Task + * [FELIX-1617] - Modify framework, main, shell, shell.tui, and obr to depend on official OSGi JAR files + +Changes from 1.4.0 to 1.4.1 +--------------------------- + +** Bug + * [FELIX-1000] - Updating an bundle which was installed via OBR fails + * [FELIX-1157] - NPE results in OBR if a resource does not have a presentation name + * [FELIX-1433] - java.lang.NumberFormatException in Bundle-Version (org.osgi.framework.Version) due to trailing whitespace + +Changes from 1.2.1 to 1.4.0 +--------------------------- +** Bug + * [FELIX-973] - FilterImpl from Felix Framework does not support none LDAP operators + * [FELIX-977] - Bundle resolving runs extreme long + * [FELIX-999] - The OBR ResolverImpl shouldn't try to start fragment bundles + +** Improvement + * [FELIX-884] - OBR should expose registered services as capabilities of local repository + * [FELIX-887] - Ensure BundleListeners are not forgotten about + * [FELIX-940] - Add support for execution environment + * [FELIX-986] - Include the symbolicname in the output of obr list -v + +** New Feature + * [FELIX-976] - OBR update-url shell command + +Changes from 1.2.0 to 1.2.1 +--------------------------- +* [2008-10-24] Fixed potential NPE when comparing resources. (FELIX-789) +* [2008-10-24] Removed the default repository URL from OBR, so now it must + be configured to have a repository. (FELIX-481) +* [2008-10-24] Print message if there are no matching bundles. (FELIX-785) +* [2008-10-23] Modified the OBR shell command to hide multiple versions of + available artifacts to cut down on noise. It is still possible to list + all versions by using a new "-v" switch. +* [2008-09-29] Adapt Bundle-DocURL header to modified URL + + +Changes from 1.0.3 to 1.2.0 +--------------------------- + +* [2008-08-30] Prevent issues when updating running bundles. (FELIX-701) +* [2008-08-28] Prevent NullPointerException if a locally installed bundle + does not have a Bundle-SymbolicName or version. (FELIX-108) +* [2008-08-12] Added OBR descriptor and updated to the newest bundle plugin. (FELIX-672) +* [2008-07-31] Use LogService instead of System.err. (FELIX-482) +* [2008-07-21] Modified OBR to correctly consider the namespace attribute + when matching capabilities to requirements. (FELIX-638) +* [2008-06-26] Implement referral with hop count support. (FELIX-399) +* [2008-05-09] Return an empty resource array when querying with a filter + with invalid syntax. (FELIX-480) +* [2008-05-09] Fixed improper synchronization with respect to visibility rules. +* [2008-05-09] Ignore resources with invalid filters. (FELIX-484) +* [2008-05-09] Move repository URL list initialization to a later time to + avoid the default repository URL if it is not desired. (FELIX-485) + +Changes from 1.0.2 to 1.0.3 +--------------------------- + +* [2008-04-21] Re-release to make bytecode executable on jre 1.3. + +Changes from 1.0.0 to 1.0.2 +--------------------------- + +* [2008-01-27] Change the default url from sf.net to sourceforge.net. +* [2007-10-25] Add support for zipped repository files. (FELIX-410) +* [2007-10-03] Updated OBR's VersionRange to match the Framework's VersionRange + and now accept whitespace in its version range. (FELIX-389) +* [2007-09-24] Extract OSGi OBR service API to a non-bundle jar to avoid + circular build problems. + +Changes from 0.8.0-incubator to 1.0.0 +------------------------------------- + +* [2007-03-16] Correctly initialized member fields to avoid incorrectly + assigning the source and license URLs. (FELIX-242) +* [2007-03-19] Parent POM extends Apache POM for Apache-wide policies. + (FELIX-260) +* [2007-05-18] Improved OBR dependency resolution by searching resolving + bundles before local bundles and to search through all available + candidates to find one that can resolve instead of picking one and failing + if it cannot be resolved. (FELIX-285) +* [2007-07-13] Fixed LDAP filter syntax bug when using inclusive version + ranges. (FELIX-327) diff --git a/bundlerepository/obr.xml b/bundlerepository/obr.xml new file mode 100644 index 00000000000..9056eda6be4 --- /dev/null +++ b/bundlerepository/obr.xml @@ -0,0 +1,29 @@ + + + +

+ + +

+ + +

+ + diff --git a/bundlerepository/pom.xml b/bundlerepository/pom.xml index f8187a52131..c453804c5d9 100644 --- a/bundlerepository/pom.xml +++ b/bundlerepository/pom.xml @@ -1,55 +1,190 @@ - + + org.apache.felix - felix - 0.8.0-SNAPSHOT + felix-parent + 2.1 + ../pom/pom.xml 4.0.0 - osgi-bundle - Apache Felix Bundle Repository Service + bundle + Apache Felix Bundle Repository + Bundle repository service. org.apache.felix.bundlerepository + 2.0.11-SNAPSHOT + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/bundlerepository + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/bundlerepository + http://svn.apache.org/repos/asf/felix/trunk/bundlerepository + - ${pom.groupId} - org.osgi.core - ${pom.version} - provided + ${project.groupId} + org.apache.felix.utils + 1.11.0-SNAPSHOT + true - ${pom.groupId} + ${project.groupId} + org.osgi.service.obr + 1.0.2 + true + + + org.apache.felix + org.osgi.core + + + + + ${project.groupId} org.apache.felix.shell - ${pom.version} - provided + 1.4.1 + true + + + ${project.groupId} + org.apache.felix.gogo.runtime + 1.0.0 + true - kxml2 + net.sf.kxml kxml2 - 2.2.2 + 2.3.0 + true + + + xmlpull + xmlpull + + + + + org.osgi + org.osgi.compendium + 5.0.0 + true + + + org.osgi + org.osgi.core + 5.0.0 + + + org.codehaus.woodstox + woodstox-core-asl + 4.0.7 + true + + + org.easymock + easymock + 3.4 + test - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + + org.apache.felix + maven-bundle-plugin + 2.4.0 true - javax.xml.parsers,org.xml.sax - - BundleRepository - auto-detect - Bundle repository service. - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/ - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}.jar - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}-src.jar - ${pom.artifactId} - org.apache.felix.shell - org.osgi.service.obr - org.osgi.service.obr.RepositoryAdmin - + true + + + org.osgi.service.repository, + org.apache.felix.bundlerepository;version="2.1" + + + org.kxml2.io, + org.xmlpull.v1, + org.apache.felix.bundlerepository.impl.*, + org.apache.felix.utils.* + + + + + !javax.xml.parsers, + !org.xml.sax, + org.osgi.service.repository;resolution:=mandatory;version="[1.0,1.1)", + org.osgi.service.log;resolution:=optional, + org.osgi.service.obr;resolution:=optional, + org.apache.felix.shell;resolution:=optional, + org.apache.felix.service.command;resolution:=optional, + javax.xml.stream;resolution:=optional, + * + + org.apache.felix.shell,org.apache.felix.service.command + ${project.artifactId}.impl.Activator + http://felix.apache.org/site/apache-felix-osgi-bundle-repository.html + http://felix.apache.org/site/downloads.cgi + http://felix.apache.org/site/downloads.cgi + ${project.artifactId} + The Apache Software Foundation + org.apache.felix.bundlerepository.RepositoryAdmin,org.osgi.service.obr.RepositoryAdmin + <_versionpolicy>[$(version;==;$(@)),$(version;+;$(@))) + META-INF/LICENSE=LICENSE,META-INF/LICENSE.kxml2=LICENSE.kxml2,META-INF/NOTICE=NOTICE,META-INF/DEPENDENCIES=DEPENDENCIES + osgi.implementation;osgi.implementation="osgi.repository";uses:="org.osgi.service.repository";version:Version="1.1",osgi.service;objectClass:List<String>="org.osgi.service.repository.Repository";uses:="org.osgi.service.repository" + + + org.apache.rat + apache-rat-plugin + + false + true + true + + doc/* + maven-eclipse.xml + .checkstyle + .externalToolBuilders/* + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Activator.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Activator.java deleted file mode 100644 index 1f604f5e0d4..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Activator.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; -import org.osgi.service.obr.RepositoryAdmin; - -public class Activator implements BundleActivator -{ - private transient BundleContext m_context = null; - private transient RepositoryAdminImpl m_repoAdmin = null; - - public void start(BundleContext context) - { - m_context = context; - - // Register bundle repository service. - m_repoAdmin = new RepositoryAdminImpl(m_context); - context.registerService( - RepositoryAdmin.class.getName(), - m_repoAdmin, null); - - // We dynamically import the impl service API, so it - // might not actually be available, so be ready to catch - // the exception when we try to register the command service. - try - { - // Register "obr" impl command service as a - // wrapper for the bundle repository service. - context.registerService( - org.apache.felix.shell.Command.class.getName(), - new ObrCommandImpl(m_context, m_repoAdmin), null); - } - catch (Throwable th) - { - // Ignore. - } - } - - public void stop(BundleContext context) - { - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Capability.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Capability.java new file mode 100644 index 00000000000..c2ec5efbe1a --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Capability.java @@ -0,0 +1,85 @@ +/* + * 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. + */ +/* + * $Header: /cvshome/build/org.osgi.service.obr/src/org/osgi/service/obr/Capability.java,v 1.3 2006/03/16 14:56:17 hargrave Exp $ + * + * Copyright (c) OSGi Alliance (2006). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This document is an experimental draft to enable interoperability +// between bundle repositories. There is currently no commitment to +// turn this draft into an official specification. +package org.apache.felix.bundlerepository; + +import java.util.Map; + +/** + * A named set of properties representing some capability that is provided by + * its owner. + * + * @version $Revision: 1.3 $ + */ +public interface Capability +{ + String BUNDLE = "bundle"; + String FRAGMENT = "fragment"; + String PACKAGE = "package"; + String SERVICE = "service"; + String EXECUTIONENVIRONMENT = "ee"; + + /** + * Return the name of the capability. + * + */ + String getName(); + + /** + * Return the properties of this capability + * + * @return + */ + Property[] getProperties(); + + /** + * Return the map of properties. + * + * @return a Map + */ + Map getPropertiesAsMap(); + + /** + * Return the directives of this capability. The returned map + * can not be modified. + * + * @return a Map of directives or an empty map there are no directives. + */ + Map getDirectives(); +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/CapabilityImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/CapabilityImpl.java deleted file mode 100644 index 2f45aad0610..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/CapabilityImpl.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -import java.util.*; - -import org.osgi.service.obr.Capability; - -public class CapabilityImpl implements Capability -{ - private String m_name = null; - private Map m_map = null; - - public CapabilityImpl() - { - m_map = new TreeMap(new Comparator() { - public int compare(Object o1, Object o2) - { - return o1.toString().compareToIgnoreCase(o2.toString()); - } - }); - } - - public String getName() - { - return m_name; - } - - public void setName(String name) - { - m_name = name; - } - - public Map getProperties() - { - return m_map; - } - - protected void addP(PropertyImpl prop) - { - m_map.put(prop.getN(), prop.getV()); - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/CategoryImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/CategoryImpl.java deleted file mode 100644 index 45c35895e61..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/CategoryImpl.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -public class CategoryImpl -{ - String m_id = null; - - public void setId(String id) - { - m_id = id; - } - - public String getId() - { - return m_id; - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/DataModelHelper.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/DataModelHelper.java new file mode 100644 index 00000000000..36a37e8f900 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/DataModelHelper.java @@ -0,0 +1,173 @@ +/* + * 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. + */ +/* + * $Header: /cvshome/build/org.osgi.service.obr/src/org/osgi/service/obr/Requirement.java,v 1.4 2006/03/16 14:56:17 hargrave Exp $ + * + * Copyright (c) OSGi Alliance (2006). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This document is an experimental draft to enable interoperability +// between bundle repositories. There is currently no commitment to +// turn this draft into an official specification. +package org.apache.felix.bundlerepository; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.net.URL; +import java.util.Map; +import java.util.jar.Attributes; + +import org.osgi.framework.Bundle; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; + +public interface DataModelHelper { + + /** + * Create a simple requirement to be used for selection + * @param name + * @param filter + * @return + * @throws org.osgi.framework.InvalidSyntaxException + */ + Requirement requirement(String name, String filter); + + /** + * Create an extender filter supporting the SUBSET, SUPERSET and other extensions + * + * @param filter the string filter + * @return + * @throws org.osgi.framework.InvalidSyntaxException + */ + Filter filter(String filter) throws InvalidSyntaxException; + + /** + * Create a repository from the specified URL. + * + * @param repository + * @return + * @throws Exception + */ + Repository repository(URL repository) throws Exception; + + /** + * Create a repository for the given set of resources. + * Such repositories can be used to create a resolver + * that would resolve on a subset of available resources + * instead of all of them. + * + * @param resources an array of resources + * @return a repository containing the given resources + */ + Repository repository(Resource[] resources); + + /** + * Create a capability + * + * @param name name of this capability + * @param properties the properties + * @return a new capability with the specified name and properties + */ + Capability capability(String name, Map properties); + + /** + * Create a resource corresponding to the given bundle. + * + * @param bundle the bundle + * @return the corresponding resource + */ + Resource createResource(Bundle bundle); + + /** + * Create a resource for the bundle located at the + * given location. + * + * @param bundleUrl the location of the bundle + * @return the corresponding resource + * @throws IOException + */ + Resource createResource(URL bundleUrl) throws IOException; + + /** + * Create a resource corresponding to the given manifest + * entries. + * + * @param attributes the manifest headers + * @return the corresponding resource + */ + Resource createResource(Attributes attributes); + + //=========================== + //== XML serialization == + //=========================== + + Repository readRepository(String xml) throws Exception; + + Repository readRepository(Reader reader) throws Exception; + + Resource readResource(String xml) throws Exception; + + Resource readResource(Reader reader) throws Exception; + + Capability readCapability(String xml) throws Exception; + + Capability readCapability(Reader reader) throws Exception; + + Requirement readRequirement(String xml) throws Exception; + + Requirement readRequirement(Reader reader) throws Exception; + + Property readProperty(String xml) throws Exception; + + Property readProperty(Reader reader) throws Exception; + + String writeRepository(Repository repository); + + void writeRepository(Repository repository, Writer writer) throws IOException; + + String writeResource(Resource resource); + + void writeResource(Resource resource, Writer writer) throws IOException; + + String writeCapability(Capability capability); + + void writeCapability(Capability capability, Writer writer) throws IOException; + + String writeRequirement(Requirement requirement); + + void writeRequirement(Requirement requirement, Writer writer) throws IOException; + + String writeProperty(Property property); + + void writeProperty(Property property, Writer writer) throws IOException; + +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/FileUtil.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/FileUtil.java deleted file mode 100644 index f7dd1d238a9..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/FileUtil.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -import java.io.*; -import java.net.URL; -import java.net.URLConnection; -import java.util.jar.JarEntry; -import java.util.jar.JarInputStream; - -public class FileUtil -{ - public static void downloadSource( - PrintStream out, PrintStream err, - URL srcURL, String dirStr, boolean extract) - { - // Get the file name from the URL. - String fileName = (srcURL.getFile().lastIndexOf('/') > 0) - ? srcURL.getFile().substring(srcURL.getFile().lastIndexOf('/') + 1) - : srcURL.getFile(); - - try - { - out.println("Connecting..."); - - File dir = new File(dirStr); - if (!dir.exists()) - { - err.println("Destination directory does not exist."); - } - File file = new File(dir, fileName); - - OutputStream os = new FileOutputStream(file); - URLConnection conn = srcURL.openConnection(); - int total = conn.getContentLength(); - InputStream is = conn.getInputStream(); - - if (total > 0) - { - out.println("Downloading " + fileName - + " ( " + total + " bytes )."); - } - else - { - out.println("Downloading " + fileName + "."); - } - byte[] buffer = new byte[4096]; - int count = 0; - for (int len = is.read(buffer); len > 0; len = is.read(buffer)) - { - count += len; - os.write(buffer, 0, len); - } - - os.close(); - is.close(); - - if (extract) - { - is = new FileInputStream(file); - JarInputStream jis = new JarInputStream(is); - out.println("Extracting..."); - unjar(jis, dir); - jis.close(); - file.delete(); - } - } - catch (Exception ex) - { - err.println(ex); - } - } - - public static void unjar(JarInputStream jis, File dir) - throws IOException - { - // Reusable buffer. - byte[] buffer = new byte[4096]; - - // Loop through JAR entries. - for (JarEntry je = jis.getNextJarEntry(); - je != null; - je = jis.getNextJarEntry()) - { - if (je.getName().startsWith("/")) - { - throw new IOException("JAR resource cannot contain absolute paths."); - } - - File target = new File(dir, je.getName()); - - // Check to see if the JAR entry is a directory. - if (je.isDirectory()) - { - if (!target.exists()) - { - if (!target.mkdirs()) - { - throw new IOException("Unable to create target directory: " - + target); - } - } - // Just continue since directories do not have content to copy. - continue; - } - - int lastIndex = je.getName().lastIndexOf('/'); - String name = (lastIndex >= 0) ? - je.getName().substring(lastIndex + 1) : je.getName(); - String destination = (lastIndex >= 0) ? - je.getName().substring(0, lastIndex) : ""; - - // JAR files use '/', so convert it to platform separator. - destination = destination.replace('/', File.separatorChar); - copy(jis, dir, name, destination, buffer); - } - } - - public static void copy( - InputStream is, File dir, String destName, String destDir, byte[] buffer) - throws IOException - { - if (destDir == null) - { - destDir = ""; - } - - // Make sure the target directory exists and - // that is actually a directory. - File targetDir = new File(dir, destDir); - if (!targetDir.exists()) - { - if (!targetDir.mkdirs()) - { - throw new IOException("Unable to create target directory: " - + targetDir); - } - } - else if (!targetDir.isDirectory()) - { - throw new IOException("Target is not a directory: " - + targetDir); - } - - BufferedOutputStream bos = new BufferedOutputStream( - new FileOutputStream(new File(targetDir, destName))); - int count = 0; - while ((count = is.read(buffer)) > 0) - { - bos.write(buffer, 0, count); - } - bos.close(); - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/InterruptedResolutionException.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/InterruptedResolutionException.java new file mode 100644 index 00000000000..d7b73399209 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/InterruptedResolutionException.java @@ -0,0 +1,45 @@ +/* + * 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.bundlerepository; + +/** + * + * Exception thrown by the resolver if the resolution has been interrupted. + */ +public class InterruptedResolutionException extends RuntimeException +{ + public InterruptedResolutionException() + { + } + + public InterruptedResolutionException(String message) + { + super(message); + } + + public InterruptedResolutionException(String message, Throwable cause) + { + super(message, cause); + } + + public InterruptedResolutionException(Throwable cause) + { + super(cause); + } +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/IteratorToEnumeration.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/IteratorToEnumeration.java deleted file mode 100644 index 5b6ba3b2acc..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/IteratorToEnumeration.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -import java.util.Enumeration; -import java.util.Iterator; - -public class IteratorToEnumeration implements Enumeration -{ - private Iterator m_iter = null; - - public IteratorToEnumeration(Iterator iter) - { - m_iter = iter; - } - - public boolean hasMoreElements() - { - if (m_iter == null) - return false; - return m_iter.hasNext(); - } - - public Object nextElement() - { - if (m_iter == null) - return null; - return m_iter.next(); - } -} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/LocalRepositoryImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/LocalRepositoryImpl.java deleted file mode 100644 index 9dd5dc2a4ee..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/LocalRepositoryImpl.java +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -import java.net.URL; -import java.util.*; - -import org.osgi.framework.*; -import org.osgi.service.obr.Repository; -import org.osgi.service.obr.Resource; - -public class LocalRepositoryImpl implements Repository -{ - private BundleContext m_context = null; - private long m_currentTimeStamp = 0; - private long m_snapshotTimeStamp = 0; - private List m_localResourceList = new ArrayList(); - private BundleListener m_bundleListener = null; - - public LocalRepositoryImpl(BundleContext context) - { - m_context = context; - initialize(); - } - - public void dispose() - { - m_context.removeBundleListener(m_bundleListener); - } - - public URL getURL() - { - return null; - } - - public String getName() - { - return "Locally Installed Repository"; - } - - public synchronized long getLastModified() - { - return m_snapshotTimeStamp; - } - - public synchronized long getCurrentTimeStamp() - { - return m_currentTimeStamp; - } - - public synchronized Resource[] getResources() - { - return (Resource[]) m_localResourceList.toArray(new Resource[m_localResourceList.size()]); - } - - private void initialize() - { - // Create a bundle listener to list for events that - // change the state of the framework. - m_bundleListener = new SynchronousBundleListener() { - public void bundleChanged(BundleEvent event) - { - synchronized (LocalRepositoryImpl.this) - { - m_currentTimeStamp = new Date().getTime(); - } - } - }; - m_context.addBundleListener(m_bundleListener); - - // Generate the resource list from the set of installed bundles. - // Lock so we can ensure that no bundle events arrive before we - // are done getting our state snapshot. - Bundle[] bundles = null; - synchronized (this) - { - m_snapshotTimeStamp = m_currentTimeStamp = new Date().getTime(); - bundles = m_context.getBundles(); - } - - // Create a local resource object for each bundle, which will - // convert the bundle headers to the appropriate resource metadata. - for (int i = 0; (bundles != null) && (i < bundles.length); i++) - { - m_localResourceList.add(new LocalResourceImpl(bundles[i])); - } - } - - public static class LocalResourceImpl extends ResourceImpl - { - private Bundle m_bundle = null; - - LocalResourceImpl(Bundle bundle) - { - this(null, bundle); - } - - LocalResourceImpl(ResourceImpl resource, Bundle bundle) - { - super(resource); - m_bundle = bundle; - initialize(); - } - - public Bundle getBundle() - { - return m_bundle; - } - - private void initialize() - { - Dictionary dict = m_bundle.getHeaders(); - - // Convert bundle manifest header attributes to resource properties. - convertAttributesToProperties(dict); - - // Convert import package declarations into requirements. - convertImportPackageToRequirement(dict); - - // Convert import service declarations into requirements. - convertImportServiceToRequirement(dict); - - // Convert export package declarations into capabilities. - convertExportPackageToCapability(dict); - - // Convert export service declarations into capabilities. - convertExportServiceToCapability(dict); - - // For the system bundle, add a special platform capability. - if (m_bundle.getBundleId() == 0) - { -/* TODO: OBR - Fix system capabilities. - // Create a case-insensitive map. - Map map = new TreeMap(new Comparator() { - public int compare(Object o1, Object o2) - { - return o1.toString().compareToIgnoreCase(o2.toString()); - } - }); - map.put( - Constants.FRAMEWORK_VERSION, - m_context.getProperty(Constants.FRAMEWORK_VERSION)); - map.put( - Constants.FRAMEWORK_VENDOR, - m_context.getProperty(Constants.FRAMEWORK_VENDOR)); - map.put( - Constants.FRAMEWORK_LANGUAGE, - m_context.getProperty(Constants.FRAMEWORK_LANGUAGE)); - map.put( - Constants.FRAMEWORK_OS_NAME, - m_context.getProperty(Constants.FRAMEWORK_OS_NAME)); - map.put( - Constants.FRAMEWORK_OS_VERSION, - m_context.getProperty(Constants.FRAMEWORK_OS_VERSION)); - map.put( - Constants.FRAMEWORK_PROCESSOR, - m_context.getProperty(Constants.FRAMEWORK_PROCESSOR)); -// map.put( -// FelixConstants.FELIX_VERSION_PROPERTY, -// m_context.getProperty(FelixConstants.FELIX_VERSION_PROPERTY)); - Map[] capMaps = (Map[]) bundleMap.get("capability"); - if (capMaps == null) - { - capMaps = new Map[] { map }; - } - else - { - Map[] newCaps = new Map[capMaps.length + 1]; - newCaps[0] = map; - System.arraycopy(capMaps, 0, newCaps, 1, capMaps.length); - capMaps = newCaps; - } - bundleMap.put("capability", capMaps); -*/ - } - } - - private void convertAttributesToProperties(Dictionary dict) - { - for (Enumeration keys = dict.keys(); keys.hasMoreElements(); ) - { - String key = (String) keys.nextElement(); - if (key.equalsIgnoreCase(Constants.BUNDLE_SYMBOLICNAME)) - { - put(Resource.SYMBOLIC_NAME, (String) dict.get(key)); - } - else if (key.equalsIgnoreCase(Constants.BUNDLE_NAME)) - { - put(Resource.PRESENTATION_NAME, (String) dict.get(key)); - } - else if (key.equalsIgnoreCase(Constants.BUNDLE_VERSION)) - { - put(Resource.VERSION, (String) dict.get(key)); - } - else if (key.equalsIgnoreCase("Bundle-Source")) - { - put(Resource.SOURCE_URL, (String) dict.get(key)); - } - else if (key.equalsIgnoreCase(Constants.BUNDLE_DESCRIPTION)) - { - put(Resource.DESCRIPTION, (String) dict.get(key)); - } - else if (key.equalsIgnoreCase(Constants.BUNDLE_DOCURL)) - { - put(Resource.DOCUMENTATION_URL, (String) dict.get(key)); - } - else if (key.equalsIgnoreCase(Constants.BUNDLE_COPYRIGHT)) - { - put(Resource.COPYRIGHT, (String) dict.get(key)); - } - else if (key.equalsIgnoreCase("Bundle-License")) - { - put(Resource.LICENSE_URL, (String) dict.get(key)); - } - } - } - - private void convertImportPackageToRequirement(Dictionary dict) - { - String target = (String) dict.get(Constants.IMPORT_PACKAGE); - if (target != null) - { - R4Package[] pkgs = R4Package.parseImportOrExportHeader(target); - R4Import[] imports = new R4Import[pkgs.length]; - for (int i = 0; i < pkgs.length; i++) - { - imports[i] = new R4Import(pkgs[i]); - } - - for (int impIdx = 0; impIdx < imports.length; impIdx++) - { - String low = imports[impIdx].isLowInclusive() - ? "(version>=" + imports[impIdx].getVersion() + ")" - : "(!(version<=" + imports[impIdx].getVersion() + ")"; - - if (imports[impIdx].getVersionHigh() != null) - { - String high = imports[impIdx].isHighInclusive() - ? "(version<=" + imports[impIdx].getVersionHigh() + ")" - : "(!(version>=" + imports[impIdx].getVersionHigh() + ")"; - RequirementImpl req = new RequirementImpl(); - req.setMultiple("false"); - req.setName("package"); - req.addText("Import package " + imports[impIdx].toString()); - req.setFilter("(&(package=" - + imports[impIdx].getName() + ")" - + low + high + ")"); - addRequire(req); - } - else - { - RequirementImpl req = new RequirementImpl(); - req.setMultiple("false"); - req.setName("package"); - req.addText("Import package " + imports[impIdx].toString()); - req.setFilter( - "(&(package=" - + imports[impIdx].getName() + ")" - + low + ")"); - addRequire(req); - } - } - } - } - - private void convertImportServiceToRequirement(Dictionary dict) - { - String target = (String) dict.get(Constants.IMPORT_SERVICE); - if (target != null) - { - R4Package[] pkgs = R4Package.parseImportOrExportHeader(target); - for (int pkgIdx = 0; (pkgs != null) && (pkgIdx < pkgs.length); pkgIdx++) - { - RequirementImpl req = new RequirementImpl(); - req.setMultiple("false"); - req.setName("service"); - req.addText("Import service " + pkgs[pkgIdx].toString()); - req.setFilter("(service=" - + pkgs[pkgIdx].getName() + ")"); - addRequire(req); - } - } - } - - private void convertExportPackageToCapability(Dictionary dict) - { - String target = (String) dict.get(Constants.EXPORT_PACKAGE); - if (target != null) - { - R4Package[] pkgs = R4Package.parseImportOrExportHeader(target); - for (int pkgIdx = 0; (pkgs != null) && (pkgIdx < pkgs.length); pkgIdx++) - { - CapabilityImpl cap = new CapabilityImpl(); - cap.setName("package"); - cap.addP(new PropertyImpl("package", null, pkgs[pkgIdx].getName())); - cap.addP(new PropertyImpl("version", "version", pkgs[pkgIdx].getVersion().toString())); - addCapability(cap); - } - } - } - - private void convertExportServiceToCapability(Dictionary dict) - { - String target = (String) dict.get(Constants.EXPORT_SERVICE); - if (target != null) - { - R4Package[] pkgs = R4Package.parseImportOrExportHeader(target); - for (int pkgIdx = 0; (pkgs != null) && (pkgIdx < pkgs.length); pkgIdx++) - { - CapabilityImpl cap = new CapabilityImpl(); - cap.setName("service"); - cap.addP(new PropertyImpl("service", null, pkgs[pkgIdx].getName())); - addCapability(cap); - } - } - } - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/LocalResource.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/LocalResource.java new file mode 100644 index 00000000000..c4718d43321 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/LocalResource.java @@ -0,0 +1,24 @@ +/* + * 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.bundlerepository; + +import org.osgi.framework.BundleReference; + +public interface LocalResource extends BundleReference, Resource { +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/MapToDictionary.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/MapToDictionary.java deleted file mode 100644 index f50fec2deda..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/MapToDictionary.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -import java.util.*; - - -/** - * This is a simple class that implements a Dictionary - * from a Map. The resulting dictionary is immutatable. -**/ -public class MapToDictionary extends Dictionary -{ - /** - * Map source. - **/ - private Map m_map = null; - - public MapToDictionary(Map map) - { - m_map = map; - } - - public void setSourceMap(Map map) - { - m_map = map; - } - - public Enumeration elements() - { - if (m_map == null) - { - return null; - } - return new IteratorToEnumeration(m_map.values().iterator()); - } - - public Object get(Object key) - { - if (m_map == null) - { - return null; - } - return m_map.get(key); - } - - public boolean isEmpty() - { - if (m_map == null) - { - return true; - } - return m_map.isEmpty(); - } - - public Enumeration keys() - { - if (m_map == null) - { - return null; - } - return new IteratorToEnumeration(m_map.keySet().iterator()); - } - - public Object put(Object key, Object value) - { - throw new UnsupportedOperationException(); - } - - public Object remove(Object key) - { - throw new UnsupportedOperationException(); - } - - public int size() - { - if (m_map == null) - { - return 0; - } - return m_map.size(); - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/ObrCommandImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/ObrCommandImpl.java deleted file mode 100644 index 14bcb198693..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/ObrCommandImpl.java +++ /dev/null @@ -1,1087 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -import java.io.*; -import java.lang.reflect.Array; -import java.net.URL; -import java.util.*; - -import org.apache.felix.shell.Command; -import org.osgi.framework.*; -import org.osgi.service.obr.*; - -public class ObrCommandImpl implements Command -{ - private static final String HELP_CMD = "help"; - private static final String ADDURL_CMD = "add-url"; - private static final String REMOVEURL_CMD = "remove-url"; - private static final String LISTURL_CMD = "list-url"; - private static final String LIST_CMD = "list"; - private static final String INFO_CMD = "info"; - private static final String DEPLOY_CMD = "deploy"; - private static final String START_CMD = "start"; - private static final String SOURCE_CMD = "source"; - - private static final String EXTRACT_SWITCH = "-x"; - - private BundleContext m_context = null; - private RepositoryAdmin m_repoAdmin = null; - - public ObrCommandImpl(BundleContext context, RepositoryAdmin repoAdmin) - { - m_context = context; - m_repoAdmin = repoAdmin; - } - - public String getName() - { - return "obr"; - } - - public String getUsage() - { - return "obr help"; - } - - public String getShortDescription() - { - return "OSGi bundle repository."; - } - - public synchronized void execute(String commandLine, PrintStream out, PrintStream err) - { - try - { - // Parse the commandLine to get the OBR command. - StringTokenizer st = new StringTokenizer(commandLine); - // Ignore the invoking command. - st.nextToken(); - // Try to get the OBR command, default is HELP command. - String command = HELP_CMD; - try - { - command = st.nextToken(); - } - catch (Exception ex) - { - // Ignore. - } - - // Perform the specified command. - if ((command == null) || (command.equals(HELP_CMD))) - { - help(out, st); - } - else - { - if (command.equals(ADDURL_CMD) || - command.equals(REMOVEURL_CMD) || - command.equals(LISTURL_CMD)) - { - urls(commandLine, command, out, err); - } - else if (command.equals(LIST_CMD)) - { - list(commandLine, command, out, err); - } - else if (command.equals(INFO_CMD)) - { - info(commandLine, command, out, err); - } - else if (command.equals(DEPLOY_CMD) || command.equals(START_CMD)) - { - deploy(commandLine, command, out, err); - } - else if (command.equals(SOURCE_CMD)) - { - source(commandLine, command, out, err); - } - else - { - err.println("Unknown command: " + command); - } - } - } - catch (InvalidSyntaxException ex) - { - err.println("Syntax error: " + ex.getMessage()); - } - catch (IOException ex) - { - err.println("Error: " + ex); - } - } - - private void urls( - String commandLine, String command, PrintStream out, PrintStream err) - throws IOException - { - // Parse the commandLine. - StringTokenizer st = new StringTokenizer(commandLine); - // Ignore the "obr" command. - st.nextToken(); - // Ignore the "url" command. - st.nextToken(); - - int count = st.countTokens(); - if (count > 0) - { - while (st.hasMoreTokens()) - { - if (command.equals(ADDURL_CMD)) - { - try - { - m_repoAdmin.addRepository(new URL(st.nextToken())); - } - catch (Exception ex) - { - ex.printStackTrace(err); - } - } - else - { - m_repoAdmin.removeRepository(new URL(st.nextToken())); - } - } - } - else - { - Repository[] repos = m_repoAdmin.listRepositories(); - if ((repos != null) && (repos.length > 0)) - { - for (int i = 0; i < repos.length; i++) - { - out.println(repos[i].getURL()); - } - } - else - { - out.println("No repository URLs are set."); - } - } - } - - private void list( - String commandLine, String command, PrintStream out, PrintStream err) - throws IOException - { - // Create a stream tokenizer for the command line string, - // since the syntax for install/start is more sophisticated. - StringReader sr = new StringReader(commandLine); - StreamTokenizer tokenizer = new StreamTokenizer(sr); - tokenizer.resetSyntax(); - tokenizer.quoteChar('\''); - tokenizer.quoteChar('\"'); - tokenizer.whitespaceChars('\u0000', '\u0020'); - tokenizer.wordChars('A', 'Z'); - tokenizer.wordChars('a', 'z'); - tokenizer.wordChars('0', '9'); - tokenizer.wordChars('\u00A0', '\u00FF'); - tokenizer.wordChars('.', '.'); - tokenizer.wordChars('-', '-'); - tokenizer.wordChars('_', '_'); - - // Ignore the invoking command name and the OBR command. - int type = tokenizer.nextToken(); - type = tokenizer.nextToken(); - - String substr = null; - - for (type = tokenizer.nextToken(); - type != StreamTokenizer.TT_EOF; - type = tokenizer.nextToken()) - { - // Add a space in between tokens. - if (substr == null) - { - substr = ""; - } - else - { - substr += " "; - } - - if ((type == StreamTokenizer.TT_WORD) || - (type == '\'') || (type == '"')) - { - substr += tokenizer.sval; - } - } - - StringBuffer sb = new StringBuffer(); - if ((substr == null) || (substr.length() == 0)) - { - sb.append("(|(presentationname=*)(symbolicname=*))"); - } - else - { - sb.append("(|(presentationname=*"); - sb.append(substr); - sb.append("*)(symbolicname=*"); - sb.append(substr); - sb.append("*))"); - } - Resource[] resources = m_repoAdmin.discoverResources(sb.toString()); - for (int resIdx = 0; (resources != null) && (resIdx < resources.length); resIdx++) - { - String name = resources[resIdx].getPresentationName(); - Version version = resources[resIdx].getVersion(); - if (version != null) - { - out.println(name + " (" + version + ")"); - } - else - { - out.println(name); - } - } - - if (resources == null) - { - out.println("No matching bundles."); - } - } - - private void info( - String commandLine, String command, PrintStream out, PrintStream err) - throws IOException, InvalidSyntaxException - { - ParsedCommand pc = parseInfo(commandLine); - for (int cmdIdx = 0; (pc != null) && (cmdIdx < pc.getTargetCount()); cmdIdx++) - { - // Find the target's bundle resource. - Resource[] resources = searchRepository(pc.getTargetId(cmdIdx), pc.getTargetVersion(cmdIdx)); - if (resources == null) - { - err.println("Unknown bundle and/or version: " - + pc.getTargetId(cmdIdx)); - } - else - { - for (int resIdx = 0; resIdx < resources.length; resIdx++) - { - if (resIdx > 0) - { - out.println(""); - } - printResource(out, resources[resIdx]); - } - } - } - } - - private void deploy( - String commandLine, String command, PrintStream out, PrintStream err) - throws IOException, InvalidSyntaxException - { - ParsedCommand pc = parseInstallStart(commandLine); - _deploy(pc, command, out, err); - } - - private void _deploy( - ParsedCommand pc, String command, PrintStream out, PrintStream err) - throws IOException, InvalidSyntaxException - { - Resolver resolver = m_repoAdmin.resolver(); - for (int i = 0; (pc != null) && (i < pc.getTargetCount()); i++) - { - // Find the target's bundle resource. - Resource resource = selectNewestVersion( - searchRepository(pc.getTargetId(i), pc.getTargetVersion(i))); - if (resource != null) - { - resolver.add(resource); - } - else - { - err.println("Unknown bundle - " + pc.getTargetId(i)); - } - } - - if ((resolver.getAddedResources() != null) && - (resolver.getAddedResources().length > 0)) - { - if (resolver.resolve()) - { - out.println("Target resource(s):"); - printUnderline(out, 19); - Resource[] resources = resolver.getAddedResources(); - for (int resIdx = 0; (resources != null) && (resIdx < resources.length); resIdx++) - { - out.println(" " + resources[resIdx].getPresentationName() - + " (" + resources[resIdx].getVersion() + ")"); - } - resources = resolver.getRequiredResources(); - if ((resources != null) && (resources.length > 0)) - { - out.println("\nRequired resource(s):"); - printUnderline(out, 21); - for (int resIdx = 0; resIdx < resources.length; resIdx++) - { - out.println(" " + resources[resIdx].getPresentationName() - + " (" + resources[resIdx].getVersion() + ")"); - } - } - resources = resolver.getOptionalResources(); - if ((resources != null) && (resources.length > 0)) - { - out.println("\nOptional resource(s):"); - printUnderline(out, 21); - for (int resIdx = 0; resIdx < resources.length; resIdx++) - { - out.println(" " + resources[resIdx].getPresentationName() - + " (" + resources[resIdx].getVersion() + ")"); - } - } - - try - { - out.print("\nDeploying..."); - resolver.deploy(command.equals(START_CMD)); - out.println("done."); - } - catch (IllegalStateException ex) - { - err.println(ex); - } - } - else - { - Requirement[] reqs = resolver.getUnsatisfiedRequirements(); - if ((reqs != null) && (reqs.length > 0)) - { - out.println("Unsatisfied requirement(s):"); - printUnderline(out, 27); - for (int reqIdx = 0; reqIdx < reqs.length; reqIdx++) - { - out.println(" " + reqs[reqIdx].getFilter()); - Resource[] resources = resolver.getResources(reqs[reqIdx]); - for (int resIdx = 0; resIdx < resources.length; resIdx++) - { - out.println(" " + resources[resIdx].getPresentationName()); - } - } - } - else - { - out.println("Could not resolve targets."); - } - } - } - } - - private void source( - String commandLine, String command, PrintStream out, PrintStream err) - throws IOException, InvalidSyntaxException - { - // Parse the command line to get all local targets to update. - ParsedCommand pc = parseSource(commandLine); - for (int i = 0; i < pc.getTargetCount(); i++) - { - Resource resource = selectNewestVersion( - searchRepository(pc.getTargetId(i), pc.getTargetVersion(i))); - if (resource == null) - { - err.println("Unknown bundle and/or version: " - + pc.getTargetId(i)); - } - else - { - URL srcURL = (URL) resource.getProperties().get(Resource.SOURCE_URL); - if (srcURL != null) - { - FileUtil.downloadSource( - out, err, srcURL, pc.getDirectory(), pc.isExtract()); - } - else - { - err.println("Missing source URL: " + pc.getTargetId(i)); - } - } - } - } - - private Resource[] searchRepository(String targetId, String targetVersion) - { - // Try to see if the targetId is a bundle ID. - try - { - Bundle bundle = m_context.getBundle(Long.parseLong(targetId)); - targetId = bundle.getSymbolicName(); - } - catch (NumberFormatException ex) - { - // It was not a number, so ignore. - } - - // The targetId may be a bundle name or a bundle symbolic name, - // so create the appropriate LDAP query. - StringBuffer sb = new StringBuffer("(|(presentationname="); - sb.append(targetId); - sb.append(")(symbolicname="); - sb.append(targetId); - sb.append("))"); - if (targetVersion != null) - { - sb.insert(0, "(&"); - sb.append("(version="); - sb.append(targetVersion); - sb.append("))"); - } - return m_repoAdmin.discoverResources(sb.toString()); - } - - public Resource selectNewestVersion(Resource[] resources) - { - int idx = -1; - Version v = null; - for (int i = 0; (resources != null) && (i < resources.length); i++) - { - if (i == 0) - { - idx = 0; - v = resources[i].getVersion(); - } - else - { - Version vtmp = resources[i].getVersion(); - if (vtmp.compareTo(v) > 0) - { - idx = i; - v = vtmp; - } - } - } - - return (idx < 0) ? null : resources[idx]; - } - - private void printResource(PrintStream out, Resource resource) - { - printUnderline(out, resource.getPresentationName().length()); - out.println(resource.getPresentationName()); - printUnderline(out, resource.getPresentationName().length()); - - Map map = resource.getProperties(); - for (Iterator iter = map.entrySet().iterator(); iter.hasNext(); ) - { - Map.Entry entry = (Map.Entry) iter.next(); - if (entry.getValue().getClass().isArray()) - { - out.println(entry.getKey() + ":"); - for (int j = 0; j < Array.getLength(entry.getValue()); j++) - { - out.println(" " + Array.get(entry.getValue(), j)); - } - } - else - { - out.println(entry.getKey() + ": " + entry.getValue()); - } - } - - Requirement[] reqs = resource.getRequirements(); - if ((reqs != null) && (reqs.length > 0)) - { - out.println("Requires:"); - for (int i = 0; i < reqs.length; i++) - { - out.println(" " + reqs[i].getFilter()); - } - } - - Capability[] caps = resource.getCapabilities(); - if ((caps != null) && (caps.length > 0)) - { - out.println("Capabilities:"); - for (int i = 0; i < caps.length; i++) - { - out.println(" " + caps[i].getProperties()); - } - } - } - - private static void printUnderline(PrintStream out, int length) - { - for (int i = 0; i < length; i++) - { - out.print('-'); - } - out.println(""); - } - - private ParsedCommand parseInfo(String commandLine) - throws IOException, InvalidSyntaxException - { - // Create a stream tokenizer for the command line string, - // since the syntax for install/start is more sophisticated. - StringReader sr = new StringReader(commandLine); - StreamTokenizer tokenizer = new StreamTokenizer(sr); - tokenizer.resetSyntax(); - tokenizer.quoteChar('\''); - tokenizer.quoteChar('\"'); - tokenizer.whitespaceChars('\u0000', '\u0020'); - tokenizer.wordChars('A', 'Z'); - tokenizer.wordChars('a', 'z'); - tokenizer.wordChars('0', '9'); - tokenizer.wordChars('\u00A0', '\u00FF'); - tokenizer.wordChars('.', '.'); - tokenizer.wordChars('-', '-'); - tokenizer.wordChars('_', '_'); - - // Ignore the invoking command name and the OBR command. - int type = tokenizer.nextToken(); - type = tokenizer.nextToken(); - - int EOF = 1; - int SWITCH = 2; - int TARGET = 4; - int VERSION = 8; - int VERSION_VALUE = 16; - - // Construct an install record. - ParsedCommand pc = new ParsedCommand(); - String currentTargetName = null; - - // The state machine starts by expecting either a - // SWITCH or a TARGET. - int expecting = (TARGET); - while (true) - { - // Get the next token type. - type = tokenizer.nextToken(); - switch (type) - { - // EOF received. - case StreamTokenizer.TT_EOF: - // Error if we weren't expecting EOF. - if ((expecting & EOF) == 0) - { - throw new InvalidSyntaxException( - "Expecting more arguments.", null); - } - // Add current target if there is one. - if (currentTargetName != null) - { - pc.addTarget(currentTargetName, null); - } - // Return cleanly. - return pc; - - // WORD or quoted WORD received. - case StreamTokenizer.TT_WORD: - case '\'': - case '\"': - // If we are expecting a target, the record it. - if ((expecting & TARGET) > 0) - { - // Add current target if there is one. - if (currentTargetName != null) - { - pc.addTarget(currentTargetName, null); - } - // Set the new target as the current target. - currentTargetName = tokenizer.sval; - expecting = (EOF | TARGET | VERSION); - } - else if ((expecting & VERSION_VALUE) > 0) - { - pc.addTarget(currentTargetName, tokenizer.sval); - currentTargetName = null; - expecting = (EOF | TARGET); - } - else - { - throw new InvalidSyntaxException( - "Not expecting '" + tokenizer.sval + "'.", null); - } - break; - - // Version separator character received. - case ';': - // Error if we weren't expecting the version separator. - if ((expecting & VERSION) == 0) - { - throw new InvalidSyntaxException( - "Not expecting version.", null); - } - // Otherwise, we will only expect a version value next. - expecting = (VERSION_VALUE); - break; - } - } - } - - private ParsedCommand parseInstallStart(String commandLine) - throws IOException, InvalidSyntaxException - { - // Create a stream tokenizer for the command line string, - // since the syntax for install/start is more sophisticated. - StringReader sr = new StringReader(commandLine); - StreamTokenizer tokenizer = new StreamTokenizer(sr); - tokenizer.resetSyntax(); - tokenizer.quoteChar('\''); - tokenizer.quoteChar('\"'); - tokenizer.whitespaceChars('\u0000', '\u0020'); - tokenizer.wordChars('A', 'Z'); - tokenizer.wordChars('a', 'z'); - tokenizer.wordChars('0', '9'); - tokenizer.wordChars('\u00A0', '\u00FF'); - tokenizer.wordChars('.', '.'); - tokenizer.wordChars('-', '-'); - tokenizer.wordChars('_', '_'); - - // Ignore the invoking command name and the OBR command. - int type = tokenizer.nextToken(); - type = tokenizer.nextToken(); - - int EOF = 1; - int SWITCH = 2; - int TARGET = 4; - int VERSION = 8; - int VERSION_VALUE = 16; - - // Construct an install record. - ParsedCommand pc = new ParsedCommand(); - String currentTargetName = null; - - // The state machine starts by expecting either a - // SWITCH or a TARGET. - int expecting = (SWITCH | TARGET); - while (true) - { - // Get the next token type. - type = tokenizer.nextToken(); - switch (type) - { - // EOF received. - case StreamTokenizer.TT_EOF: - // Error if we weren't expecting EOF. - if ((expecting & EOF) == 0) - { - throw new InvalidSyntaxException( - "Expecting more arguments.", null); - } - // Add current target if there is one. - if (currentTargetName != null) - { - pc.addTarget(currentTargetName, null); - } - // Return cleanly. - return pc; - - // WORD or quoted WORD received. - case StreamTokenizer.TT_WORD: - case '\'': - case '\"': - // If we are expecting a target, the record it. - if ((expecting & TARGET) > 0) - { - // Add current target if there is one. - if (currentTargetName != null) - { - pc.addTarget(currentTargetName, null); - } - // Set the new target as the current target. - currentTargetName = tokenizer.sval; - expecting = (EOF | TARGET | VERSION); - } - else if ((expecting & VERSION_VALUE) > 0) - { - pc.addTarget(currentTargetName, tokenizer.sval); - currentTargetName = null; - expecting = (EOF | TARGET); - } - else - { - throw new InvalidSyntaxException( - "Not expecting '" + tokenizer.sval + "'.", null); - } - break; - - // Version separator character received. - case ';': - // Error if we weren't expecting the version separator. - if ((expecting & VERSION) == 0) - { - throw new InvalidSyntaxException( - "Not expecting version.", null); - } - // Otherwise, we will only expect a version value next. - expecting = (VERSION_VALUE); - break; - } - } - } - - private ParsedCommand parseSource(String commandLine) - throws IOException, InvalidSyntaxException - { - // Create a stream tokenizer for the command line string, - // since the syntax for install/start is more sophisticated. - StringReader sr = new StringReader(commandLine); - StreamTokenizer tokenizer = new StreamTokenizer(sr); - tokenizer.resetSyntax(); - tokenizer.quoteChar('\''); - tokenizer.quoteChar('\"'); - tokenizer.whitespaceChars('\u0000', '\u0020'); - tokenizer.wordChars('A', 'Z'); - tokenizer.wordChars('a', 'z'); - tokenizer.wordChars('0', '9'); - tokenizer.wordChars('\u00A0', '\u00FF'); - tokenizer.wordChars('.', '.'); - tokenizer.wordChars('-', '-'); - tokenizer.wordChars('_', '_'); - tokenizer.wordChars('/', '/'); - tokenizer.wordChars('\\', '\\'); - tokenizer.wordChars(':', ':'); - - // Ignore the invoking command name and the OBR command. - int type = tokenizer.nextToken(); - type = tokenizer.nextToken(); - - int EOF = 1; - int SWITCH = 2; - int DIRECTORY = 4; - int TARGET = 8; - int VERSION = 16; - int VERSION_VALUE = 32; - - // Construct an install record. - ParsedCommand pc = new ParsedCommand(); - String currentTargetName = null; - - // The state machine starts by expecting either a - // SWITCH or a DIRECTORY. - int expecting = (SWITCH | DIRECTORY); - while (true) - { - // Get the next token type. - type = tokenizer.nextToken(); - switch (type) - { - // EOF received. - case StreamTokenizer.TT_EOF: - // Error if we weren't expecting EOF. - if ((expecting & EOF) == 0) - { - throw new InvalidSyntaxException( - "Expecting more arguments.", null); - } - // Add current target if there is one. - if (currentTargetName != null) - { - pc.addTarget(currentTargetName, null); - } - // Return cleanly. - return pc; - - // WORD or quoted WORD received. - case StreamTokenizer.TT_WORD: - case '\'': - case '\"': - // If we are expecting a command SWITCH and the token - // equals a command SWITCH, then record it. - if (((expecting & SWITCH) > 0) && tokenizer.sval.equals(EXTRACT_SWITCH)) - { - pc.setExtract(true); - expecting = (DIRECTORY); - } - // If we are expecting a directory, the record it. - else if ((expecting & DIRECTORY) > 0) - { - // Set the directory for the command. - pc.setDirectory(tokenizer.sval); - expecting = (TARGET); - } - // If we are expecting a target, the record it. - else if ((expecting & TARGET) > 0) - { - // Add current target if there is one. - if (currentTargetName != null) - { - pc.addTarget(currentTargetName, null); - } - // Set the new target as the current target. - currentTargetName = tokenizer.sval; - expecting = (EOF | TARGET | VERSION); - } - else if ((expecting & VERSION_VALUE) > 0) - { - pc.addTarget(currentTargetName, tokenizer.sval); - currentTargetName = null; - expecting = (EOF | TARGET); - } - else - { - throw new InvalidSyntaxException( - "Not expecting '" + tokenizer.sval + "'.", null); - } - break; - - // Version separator character received. - case ';': - // Error if we weren't expecting the version separator. - if ((expecting & VERSION) == 0) - { - throw new InvalidSyntaxException( - "Not expecting version.", null); - } - // Otherwise, we will only expect a version value next. - expecting = (VERSION_VALUE); - break; - } - } - } - - private void help(PrintStream out, StringTokenizer st) - { - String command = HELP_CMD; - if (st.hasMoreTokens()) - { - command = st.nextToken(); - } - if (command.equals(ADDURL_CMD)) - { - out.println(""); - out.println("obr " + ADDURL_CMD + " [ ...]"); - out.println(""); - out.println( - "This command adds the space-delimited list of repository URLs to\n" + - "the repository service."); - out.println(""); - } - else if (command.equals(REMOVEURL_CMD)) - { - out.println(""); - out.println("obr " + REMOVEURL_CMD + " [ ...]"); - out.println(""); - out.println( - "This command removes the space-delimited list of repository URLs\n" + - "from the repository service."); - out.println(""); - } - else if (command.equals(LISTURL_CMD)) - { - out.println(""); - out.println("obr " + LISTURL_CMD); - out.println(""); - out.println( - "This command displays the repository URLs currently associated\n" + - "with the repository service."); - out.println(""); - } - else if (command.equals(LIST_CMD)) - { - out.println(""); - out.println("obr " + LIST_CMD + " [ ...]"); - out.println(""); - out.println( - "This command lists bundles available in the bundle repository.\n" + - "If no arguments are specified, then all available bundles are\n" + - "listed, otherwise any arguments are concatenated with spaces\n" + - "and used as a substring filter on the bundle names."); - out.println(""); - } - else if (command.equals(INFO_CMD)) - { - out.println(""); - out.println("obr " + INFO_CMD - + " ||[;] ..."); - out.println(""); - out.println( - "This command displays the meta-data for the specified bundles.\n" + - "If a bundle's name contains spaces, then it must be surrounded\n" + - "by quotes. It is also possible to specify a precise version\n" + - "if more than one version exists, such as:\n" + - "\n" + - " obr info \"Bundle Repository\";1.0.0\n" + - "\n" + - "The above example retrieves the meta-data for version \"1.0.0\"\n" + - "of the bundle named \"Bundle Repository\"."); - out.println(""); - } - else if (command.equals(DEPLOY_CMD)) - { - out.println(""); - out.println("obr " + DEPLOY_CMD - + " ||[;] ... "); - out.println(""); - out.println( - "This command tries to install or update the specified bundles\n" + - "and all of their dependencies. You can specify either the bundle\n" + - "name or the bundle identifier. If a bundle's name contains spaces,\n" + - "then it must be surrounded by quotes. It is also possible to\n" + - "specify a precise version if more than one version exists, such as:\n" + - "\n" + - " obr deploy \"Bundle Repository\";1.0.0\n" + - "\n" + - "For the above example, if version \"1.0.0\" of \"Bundle Repository\" is\n" + - "already installed locally, then the command will attempt to update it\n" + - "and all of its dependencies; otherwise, the command will install it\n" + - "and all of its dependencies."); - out.println(""); - } - else if (command.equals(START_CMD)) - { - out.println(""); - out.println("obr " + START_CMD - + " ||[;] ..."); - out.println(""); - out.println( - "This command installs and starts the specified bundles and all\n" + - "of their dependencies. If a bundle's name contains spaces, then\n" + - "it must be surrounded by quotes. If a specified bundle is already\n" + "installed, then this command has no effect. It is also possible\n" + "to specify a precise version if more than one version exists,\n" + "such as:\n" + - "\n" + - " obr start \"Bundle Repository\";1.0.0\n" + - "\n" + - "The above example installs and starts version \"1.0.0\" of the\n" + - "bundle named \"Bundle Repository\" and its dependencies."); - out.println(""); - } - else if (command.equals(SOURCE_CMD)) - { - out.println(""); - out.println("obr " + SOURCE_CMD - + " [" + EXTRACT_SWITCH - + "] [;] ..."); - out.println(""); - out.println( - "This command retrieves the source archives of the specified\n" + - "bundles and saves them to the specified local directory; use\n" + - "the \"" + EXTRACT_SWITCH + "\" switch to automatically extract the source archives.\n" + - "If a bundle name contains spaces, then it must be surrounded\n" + - "by quotes. It is also possible to specify a precise version if\n" + "more than one version exists, such as:\n" + - "\n" + - " obr source /home/rickhall/tmp \"Bundle Repository\";1.0.0\n" + - "\n" + - "The above example retrieves the source archive of version \"1.0.0\"\n" + - "of the bundle named \"Bundle Repository\" and saves it to the\n" + - "specified local directory."); - out.println(""); - } - else - { - out.println("obr " + HELP_CMD - + " [" + ADDURL_CMD - + " | " + REMOVEURL_CMD - + " | " + LISTURL_CMD - + " | " + LIST_CMD - + " | " + INFO_CMD - + " | " + DEPLOY_CMD + " | " + START_CMD - + " | " + SOURCE_CMD + "]"); - out.println("obr " + ADDURL_CMD + " [ ...]"); - out.println("obr " + REMOVEURL_CMD + " [ ...]"); - out.println("obr " + LISTURL_CMD); - out.println("obr " + LIST_CMD + " [ ...]"); - out.println("obr " + INFO_CMD - + " ||[;] ..."); - out.println("obr " + DEPLOY_CMD - + " ||[;] ..."); - out.println("obr " + START_CMD - + " ||[;] ..."); - out.println("obr " + SOURCE_CMD - + " [" + EXTRACT_SWITCH - + "] [;] ..."); - } - } - - private static class ParsedCommand - { - private static final int NAME_IDX = 0; - private static final int VERSION_IDX = 1; - - private boolean m_isResolve = true; - private boolean m_isCheck = false; - private boolean m_isExtract = false; - private String m_dir = null; - private String[][] m_targets = new String[0][]; - - public boolean isResolve() - { - return m_isResolve; - } - - public void setResolve(boolean b) - { - m_isResolve = b; - } - - public boolean isCheck() - { - return m_isCheck; - } - - public void setCheck(boolean b) - { - m_isCheck = b; - } - - public boolean isExtract() - { - return m_isExtract; - } - - public void setExtract(boolean b) - { - m_isExtract = b; - } - - public String getDirectory() - { - return m_dir; - } - - public void setDirectory(String s) - { - m_dir = s; - } - - public int getTargetCount() - { - return m_targets.length; - } - - public String getTargetId(int i) - { - if ((i < 0) || (i >= getTargetCount())) - { - return null; - } - return m_targets[i][NAME_IDX]; - } - - public String getTargetVersion(int i) - { - if ((i < 0) || (i >= getTargetCount())) - { - return null; - } - return m_targets[i][VERSION_IDX]; - } - - public void addTarget(String name, String version) - { - String[][] newTargets = new String[m_targets.length + 1][]; - System.arraycopy(m_targets, 0, newTargets, 0, m_targets.length); - newTargets[m_targets.length] = new String[] { name, version }; - m_targets = newTargets; - } - } -} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Property.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Property.java new file mode 100644 index 00000000000..469beac3868 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Property.java @@ -0,0 +1,39 @@ +/* + * 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.bundlerepository; + +public interface Property +{ + + String VERSION = "version"; + String URL = "url"; + String URI = "uri"; + String LONG = "long"; + String DOUBLE = "double"; + String SET = "set"; + + String getName(); + + String getType(); + + String getValue(); + + Object getConvertedValue(); + +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/PropertyImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/PropertyImpl.java deleted file mode 100644 index e1280267833..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/PropertyImpl.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -import java.net.MalformedURLException; -import java.net.URL; - -import org.osgi.framework.Version; -import org.osgi.service.obr.Resource; - -public class PropertyImpl -{ - private String m_name = null; - private String m_type = null; - private Object m_value = null; - - public PropertyImpl() - { - } - - public PropertyImpl(String name, String type, String value) - { - setN(name); - setT(type); - setV(value); - } - - public void setN(String name) - { - m_name = name; - } - - public String getN() - { - return m_name; - } - - public void setT(String type) - { - m_type = type; - - // If there is an existing value, then convert - // it based on the new type. - if (m_value != null) - { - m_value = convertType(m_value.toString()); - } - } - - public String getT() - { - return m_type; - } - - public void setV(String value) - { - m_value = convertType(value); - } - - public Object getV() - { - return m_value; - } - - private Object convertType(String value) - { - if ((m_type != null) && (m_type.equalsIgnoreCase(Resource.VERSION))) - { - return new Version(value); - } - else if ((m_type != null) && (m_type.equalsIgnoreCase(Resource.URL))) - { - try - { - return new URL(value); - } - catch (MalformedURLException ex) - { - ex.printStackTrace(); - } - } - return value; - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/R4Attribute.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/R4Attribute.java deleted file mode 100644 index d46b5e3f468..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/R4Attribute.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -public class R4Attribute -{ - private String m_name = ""; - private String m_value = ""; - private boolean m_isMandatory = false; - - public R4Attribute(String name, String value, boolean isMandatory) - { - m_name = name; - m_value = value; - m_isMandatory = isMandatory; - } - - public String getName() - { - return m_name; - } - - public String getValue() - { - return m_value; - } - - public boolean isMandatory() - { - return m_isMandatory; - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/R4Directive.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/R4Directive.java deleted file mode 100644 index d1ac972a875..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/R4Directive.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -public class R4Directive -{ - private String m_name = ""; - private String m_value = ""; - - public R4Directive(String name, String value) - { - m_name = name; - m_value = value; - } - - public String getName() - { - return m_name; - } - - public String getValue() - { - return m_value; - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/R4Export.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/R4Export.java deleted file mode 100644 index 5340286c93a..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/R4Export.java +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -import java.util.*; - -import org.osgi.framework.Constants; - -public class R4Export extends R4Package -{ - private String[] m_uses = null; - private String[][] m_includeFilter = null; - private String[][] m_excludeFilter = null; - - public R4Export(R4Package pkg) - { - this(pkg.getName(), pkg.getDirectives(), pkg.getAttributes()); - } - - public R4Export(String name, R4Directive[] directives, R4Attribute[] attrs) - { - super(name, directives, attrs); - - // Find all export directives: uses, mandatory, include, and exclude. - String mandatory = "", uses = ""; - for (int i = 0; i < m_directives.length; i++) - { - if (m_directives[i].getName().equals(Constants.USES_DIRECTIVE)) - { - uses = m_directives[i].getValue(); - } - else if (m_directives[i].getName().equals(Constants.MANDATORY_DIRECTIVE)) - { - mandatory = m_directives[i].getValue(); - } - else if (m_directives[i].getName().equals(Constants.INCLUDE_DIRECTIVE)) - { - String[] ss = Util.parseDelimitedString(m_directives[i].getValue(), ","); - m_includeFilter = new String[ss.length][]; - for (int filterIdx = 0; filterIdx < ss.length; filterIdx++) - { - m_includeFilter[filterIdx] = parseSubstring(ss[filterIdx]); - } - } - else if (m_directives[i].getName().equals(Constants.EXCLUDE_DIRECTIVE)) - { - String[] ss = Util.parseDelimitedString(m_directives[i].getValue(), ","); - m_excludeFilter = new String[ss.length][]; - for (int filterIdx = 0; filterIdx < ss.length; filterIdx++) - { - m_excludeFilter[filterIdx] = parseSubstring(ss[filterIdx]); - } - } - } - - // Parse these uses directive. - StringTokenizer tok = new StringTokenizer(uses, ","); - m_uses = new String[tok.countTokens()]; - for (int i = 0; i < m_uses.length; i++) - { - m_uses[i] = tok.nextToken().trim(); - } - - // Parse mandatory directive and mark specified - // attributes as mandatory. - tok = new StringTokenizer(mandatory, ","); - while (tok.hasMoreTokens()) - { - // Get attribute name. - String attrName = tok.nextToken().trim(); - // Find attribute and mark it as mandatory. - boolean found = false; - for (int i = 0; (!found) && (i < m_attrs.length); i++) - { - if (m_attrs[i].getName().equals(attrName)) - { - m_attrs[i] = new R4Attribute( - m_attrs[i].getName(), - m_attrs[i].getValue(), true); - found = true; - } - } - // If a specified mandatory attribute was not found, - // then error. - if (!found) - { - throw new IllegalArgumentException( - "Mandatory attribute '" + attrName + "' does not exist."); - } - } - } - - public String[] getUses() - { - return m_uses; - } - - public boolean isIncluded(String name) - { - if ((m_includeFilter == null) && (m_excludeFilter == null)) - { - return true; - } - - // Get the class name portion of the target class. - String className = Util.getClassName(name); - - // If there are no include filters then all classes are included - // by default, otherwise try to find one match. - boolean included = (m_includeFilter == null); - for (int i = 0; - (!included) && (m_includeFilter != null) && (i < m_includeFilter.length); - i++) - { - included = checkSubstring(m_includeFilter[i], className); - } - - // If there are no exclude filters then no classes are excluded - // by default, otherwise try to find one match. - boolean excluded = false; - for (int i = 0; - (!excluded) && (m_excludeFilter != null) && (i < m_excludeFilter.length); - i++) - { - excluded = checkSubstring(m_excludeFilter[i], className); - } - return included && !excluded; - } - - // - // The following substring-related code was lifted and modified - // from the LDAP parser code. - // - - private static String[] parseSubstring(String target) - { - List pieces = new ArrayList(); - StringBuffer ss = new StringBuffer(); - // int kind = SIMPLE; // assume until proven otherwise - boolean wasStar = false; // indicates last piece was a star - boolean leftstar = false; // track if the initial piece is a star - boolean rightstar = false; // track if the final piece is a star - - int idx = 0; - - // We assume (sub)strings can contain leading and trailing blanks -loop: for (;;) - { - if (idx >= target.length()) - { - if (wasStar) - { - // insert last piece as "" to handle trailing star - rightstar = true; - } - else - { - pieces.add(ss.toString()); - // accumulate the last piece - // note that in the case of - // (cn=); this might be - // the string "" (!=null) - } - ss.setLength(0); - break loop; - } - - char c = target.charAt(idx++); - if (c == '*') - { - if (wasStar) - { - // encountered two successive stars; - // I assume this is illegal - throw new IllegalArgumentException("Invalid filter string: " + target); - } - if (ss.length() > 0) - { - pieces.add(ss.toString()); // accumulate the pieces - // between '*' occurrences - } - ss.setLength(0); - // if this is a leading star, then track it - if (pieces.size() == 0) - { - leftstar = true; - } - ss.setLength(0); - wasStar = true; - } - else - { - wasStar = false; - ss.append(c); - } - } - if (leftstar || rightstar || pieces.size() > 1) - { - // insert leading and/or trailing "" to anchor ends - if (rightstar) - { - pieces.add(""); - } - if (leftstar) - { - pieces.add(0, ""); - } - } - return (String[]) pieces.toArray(new String[pieces.size()]); - } - - private static boolean checkSubstring(String[] pieces, String s) - { - // Walk the pieces to match the string - // There are implicit stars between each piece, - // and the first and last pieces might be "" to anchor the match. - // assert (pieces.length > 1) - // minimal case is * - - boolean result = false; - int len = pieces.length; - -loop: for (int i = 0; i < len; i++) - { - String piece = (String) pieces[i]; - int index = 0; - if (i == len - 1) - { - // this is the last piece - if (s.endsWith(piece)) - { - result = true; - } - else - { - result = false; - } - break loop; - } - // initial non-star; assert index == 0 - else if (i == 0) - { - if (!s.startsWith(piece)) - { - result = false; - break loop; - } - } - // assert i > 0 && i < len-1 - else - { - // Sure wish stringbuffer supported e.g. indexOf - index = s.indexOf(piece, index); - if (index < 0) - { - result = false; - break loop; - } - } - // start beyond the matching piece - index += piece.length(); - } - - return result; - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/R4Import.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/R4Import.java deleted file mode 100644 index 589b9d289eb..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/R4Import.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -import org.osgi.framework.Constants; -import org.osgi.framework.Version; - -public class R4Import extends R4Package -{ - private VersionRange m_versionRange = null; - private boolean m_isOptional = false; - - public R4Import(R4Package pkg) - { - this(pkg.getName(), pkg.getDirectives(), pkg.getAttributes()); - } - - public R4Import(String name, R4Directive[] directives, R4Attribute[] attrs) - { - super(name, directives, attrs); - - // Find all import directives: resolution. - for (int i = 0; i < m_directives.length; i++) - { - if (m_directives[i].getName().equals(Constants.RESOLUTION_DIRECTIVE)) - { - m_isOptional = m_directives[i].getValue().equals(Constants.RESOLUTION_OPTIONAL); - } - } - - // Find and parse version attribute, if present. - String rangeStr = "0.0.0"; - for (int i = 0; i < m_attrs.length; i++) - { - if (m_attrs[i].getName().equals(Constants.VERSION_ATTRIBUTE) || - m_attrs[i].getName().equals(Constants.PACKAGE_SPECIFICATION_VERSION)) - { - // Normalize version attribute name. - m_attrs[i] = new R4Attribute( - Constants.VERSION_ATTRIBUTE, m_attrs[i].getValue(), - m_attrs[i].isMandatory()); - rangeStr = m_attrs[i].getValue(); - break; - } - } - - m_versionRange = VersionRange.parse(rangeStr); - m_version = m_versionRange.getLow(); - } - - public Version getVersionHigh() - { - return m_versionRange.getHigh(); - } - - public boolean isLowInclusive() - { - return m_versionRange.isLowInclusive(); - } - - public boolean isHighInclusive() - { - return m_versionRange.isHighInclusive(); - } - - public boolean isOptional() - { - return m_isOptional; - } - - public boolean isSatisfied(R4Export export) - { - // For packages to be compatible, they must have the - // same name. - if (!getName().equals(export.getName())) - { - return false; - } - - return m_versionRange.isInRange(export.getVersion()) - && doAttributesMatch(export); - } - - private boolean doAttributesMatch(R4Export export) - { - // Cycle through all attributes of this import package - // and make sure its values match the attribute values - // of the specified export package. - for (int impAttrIdx = 0; impAttrIdx < getAttributes().length; impAttrIdx++) - { - // Get current attribute from this import package. - R4Attribute impAttr = getAttributes()[impAttrIdx]; - - // Ignore version attribute, since it is a special case that - // has already been compared using isVersionInRange() before - // the call to this method was made. - if (impAttr.getName().equals(Constants.VERSION_ATTRIBUTE)) - { - continue; - } - - // Check if the export package has the same attribute. - boolean found = false; - for (int expAttrIdx = 0; - (!found) && (expAttrIdx < export.getAttributes().length); - expAttrIdx++) - { - // Get current attribute for the export package. - R4Attribute expAttr = export.getAttributes()[expAttrIdx]; - // Check if the attribute names are equal. - if (impAttr.getName().equals(expAttr.getName())) - { - // If the values are not equal, then return false immediately. - // We should not compare version values here, since they are - // a special case and have already been compared by a call to - // isVersionInRange() before getting here; however, it is - // possible for version to be mandatory, so make sure it is - // present below. - if (!impAttr.getValue().equals(expAttr.getValue())) - { - return false; - } - found = true; - } - } - // If the attribute was not found, then return false. - if (!found) - { - return false; - } - } - - // Now, cycle through all attributes of the export package and verify that - // all mandatory attributes are present in this import package. - for (int expAttrIdx = 0; expAttrIdx < export.getAttributes().length; expAttrIdx++) - { - // Get current attribute for this package. - R4Attribute expAttr = export.getAttributes()[expAttrIdx]; - - // If the export attribute is mandatory, then make sure - // this import package has the attribute. - if (expAttr.isMandatory()) - { - boolean found = false; - for (int impAttrIdx = 0; - (!found) && (impAttrIdx < getAttributes().length); - impAttrIdx++) - { - // Get current attribute from specified package. - R4Attribute impAttr = getAttributes()[impAttrIdx]; - - // Check if the attribute names are equal - // and set found flag. - if (expAttr.getName().equals(impAttr.getName())) - { - found = true; - } - } - // If not found, then return false. - if (!found) - { - return false; - } - } - } - - return true; - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/R4Package.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/R4Package.java deleted file mode 100644 index 2d254681670..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/R4Package.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -import java.util.ArrayList; -import java.util.List; - -import org.osgi.framework.Constants; -import org.osgi.framework.Version; - -public class R4Package -{ - private String m_name = ""; - protected R4Directive[] m_directives = null; - protected R4Attribute[] m_attrs = null; - protected Version m_version = null; - - public R4Package(String name, R4Directive[] directives, R4Attribute[] attrs) - { - m_name = name; - m_directives = (directives == null) ? new R4Directive[0] : directives; - m_attrs = (attrs == null) ? new R4Attribute[0] : attrs; - - // Find and parse version attribute, if present. - String rangeStr = "0.0.0"; - for (int i = 0; i < m_attrs.length; i++) - { - if (m_attrs[i].getName().equals(Constants.VERSION_ATTRIBUTE) || - m_attrs[i].getName().equals(Constants.PACKAGE_SPECIFICATION_VERSION)) - { - // Normalize version attribute name. - m_attrs[i] = new R4Attribute( - Constants.VERSION_ATTRIBUTE, m_attrs[i].getValue(), - m_attrs[i].isMandatory()); - rangeStr = m_attrs[i].getValue(); - break; - } - } - - VersionRange range = VersionRange.parse(rangeStr); - // For now, ignore if we have a version range. - m_version = range.getLow(); - } - - public String getName() - { - return m_name; - } - - public R4Directive[] getDirectives() - { - return m_directives; - } - - public R4Attribute[] getAttributes() - { - return m_attrs; - } - - public Version getVersion() - { - return m_version; - } - - public String toString() - { - String msg = getName(); - for (int i = 0; (m_directives != null) && (i < m_directives.length); i++) - { - msg = msg + " [" + m_directives[i].getName() + ":="+ m_directives[i].getValue() + "]"; - } - for (int i = 0; (m_attrs != null) && (i < m_attrs.length); i++) - { - msg = msg + " [" + m_attrs[i].getName() + "="+ m_attrs[i].getValue() + "]"; - } - return msg; - } - - // Like this: pkg1; pkg2; dir1:=dirval1; dir2:=dirval2; attr1=attrval1; attr2=attrval2, - // pkg1; pkg2; dir1:=dirval1; dir2:=dirval2; attr1=attrval1; attr2=attrval2 - public static R4Package[] parseImportOrExportHeader(String s) - { - R4Package[] pkgs = null; - if (s != null) - { - if (s.length() == 0) - { - throw new IllegalArgumentException( - "The import and export headers cannot be an empty string."); - } - String[] ss = Util.parseDelimitedString(s, ","); - pkgs = parsePackageStrings(ss); - } - return (pkgs == null) ? new R4Package[0] : pkgs; - } - - // Like this: pkg1; pkg2; dir1:=dirval1; dir2:=dirval2; attr1=attrval1; attr2=attrval2 - public static R4Package[] parsePackageStrings(String[] ss) - throws IllegalArgumentException - { - if (ss == null) - { - return null; - } - - List completeList = new ArrayList(); - for (int ssIdx = 0; ssIdx < ss.length; ssIdx++) - { - // Break string into semi-colon delimited pieces. - String[] pieces = Util.parseDelimitedString(ss[ssIdx], ";"); - - // Count the number of different packages; packages - // will not have an '=' in their string. This assumes - // that packages come first, before directives and - // attributes. - int pkgCount = 0; - for (int pieceIdx = 0; pieceIdx < pieces.length; pieceIdx++) - { - if (pieces[pieceIdx].indexOf('=') >= 0) - { - break; - } - pkgCount++; - } - - // Error if no packages were specified. - if (pkgCount == 0) - { - throw new IllegalArgumentException( - "No packages specified on import: " + ss[ssIdx]); - } - - // Parse the directives/attributes. - R4Directive[] dirs = new R4Directive[pieces.length - pkgCount]; - R4Attribute[] attrs = new R4Attribute[pieces.length - pkgCount]; - int dirCount = 0, attrCount = 0; - int idx = -1; - String sep = null; - for (int pieceIdx = pkgCount; pieceIdx < pieces.length; pieceIdx++) - { - // Check if it is a directive. - if ((idx = pieces[pieceIdx].indexOf(":=")) >= 0) - { - sep = ":="; - } - // Check if it is an attribute. - else if ((idx = pieces[pieceIdx].indexOf("=")) >= 0) - { - sep = "="; - } - // It is an error. - else - { - throw new IllegalArgumentException( - "Not a directive/attribute: " + ss[ssIdx]); - } - - String key = pieces[pieceIdx].substring(0, idx).trim(); - String value = pieces[pieceIdx].substring(idx + sep.length()).trim(); - - // Remove quotes, if value is quoted. - if (value.startsWith("\"") && value.endsWith("\"")) - { - value = value.substring(1, value.length() - 1); - } - - // Save the directive/attribute in the appropriate array. - if (sep.equals(":=")) - { - dirs[dirCount++] = new R4Directive(key, value); - } - else - { - attrs[attrCount++] = new R4Attribute(key, value, false); - } - } - - // Shrink directive array. - R4Directive[] dirsFinal = new R4Directive[dirCount]; - System.arraycopy(dirs, 0, dirsFinal, 0, dirCount); - // Shrink attribute array. - R4Attribute[] attrsFinal = new R4Attribute[attrCount]; - System.arraycopy(attrs, 0, attrsFinal, 0, attrCount); - - // Create package attributes for each package and - // set directives/attributes. Add each package to - // completel list of packages. - R4Package[] pkgs = new R4Package[pkgCount]; - for (int pkgIdx = 0; pkgIdx < pkgCount; pkgIdx++) - { - pkgs[pkgIdx] = new R4Package(pieces[pkgIdx], dirsFinal, attrsFinal); - completeList.add(pkgs[pkgIdx]); - } - } - - R4Package[] pkgs = (R4Package[]) - completeList.toArray(new R4Package[completeList.size()]); - return pkgs; - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Reason.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Reason.java new file mode 100644 index 00000000000..b74daabbc2a --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Reason.java @@ -0,0 +1,33 @@ +/* + * 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.bundlerepository; + +/** + * A pair of requirement and resource indicating a reason + * why a resource has been chosen. + * The reason indicates which resource and which requirement + * has been satisfied by the selected resource. + */ +public interface Reason { + + Resource getResource(); + + Requirement getRequirement(); + +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Repository.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Repository.java new file mode 100644 index 00000000000..6794222e63c --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Repository.java @@ -0,0 +1,85 @@ +/* + * 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. + */ +/* + * $Header: /cvshome/build/org.osgi.service.obr/src/org/osgi/service/obr/Repository.java,v 1.3 2006/03/16 14:56:17 hargrave Exp $ + * + * Copyright (c) OSGi Alliance (2006). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This document is an experimental draft to enable interoperability +// between bundle repositories. There is currently no commitment to +// turn this draft into an official specification. +package org.apache.felix.bundlerepository; + +/** + * Represents a repository. + * + * @version $Revision: 1.3 $ + */ +public interface Repository +{ + + /** + * URI identifying the system repository + */ + String SYSTEM = "system"; + + /** + * URI identiying the local repository + */ + String LOCAL = "local"; + + /** + * Return the associated URL for the repository. + * + */ + String getURI(); + + /** + * Return the resources for this repository. + */ + Resource[] getResources(); + + /** + * Return the name of this repository. + * + * @return a non-null name + */ + String getName(); + + /** + * Return the last modification date of this repository + * + * @return the last modification date + */ + long getLastModified(); + +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/RepositoryAdmin.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/RepositoryAdmin.java new file mode 100644 index 00000000000..cfa43de1714 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/RepositoryAdmin.java @@ -0,0 +1,178 @@ +/* + * 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. + */ +/* + * $Header: /cvshome/build/org.osgi.service.obr/src/org/osgi/service/obr/RepositoryAdmin.java,v 1.3 2006/03/16 14:56:17 hargrave Exp $ + * + * Copyright (c) OSGi Alliance (2006). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This document is an experimental draft to enable interoperability +// between bundle repositories. There is currently no commitment to +// turn this draft into an official specification. +package org.apache.felix.bundlerepository; + +import java.net.URL; + +import org.osgi.framework.InvalidSyntaxException; + +/** + * Provides centralized access to the distributed repository. + * + * A repository contains a set of resources. A resource contains a + * number of fixed attributes (name, version, etc) and sets of: + *

    + *
  1. Capabilities - Capabilities provide a named aspect: a bundle, a display, + * memory, etc.
  2. + *
  3. Requirements - A named filter expression. The filter must be satisfied + * by one or more Capabilities with the given name. These capabilities can come + * from other resources or from the platform. If multiple resources provide the + * requested capability, one is selected. (### what algorithm? ###)
  4. + *
  5. Requests - Requests are like requirements, except that a request can be + * fulfilled by 0..n resources. This feature can be used to link to resources + * that are compatible with the given resource and provide extra functionality. + * For example, a bundle could request all its known fragments. The UI + * associated with the repository could list these as optional downloads.
  6. + * + * @version $Revision: 1.3 $ + */ +public interface RepositoryAdmin +{ + /** + * Discover any resources that match the given filter. + * + * This is not a detailed search, but a first scan of applicable resources. + * + * ### Checking the capabilities of the filters is not possible because that + * requires a new construct in the filter. + * + * The filter expression can assert any of the main headers of the resource. + * The attributes that can be checked are: + * + *
      + *
    1. name
    2. + *
    3. version (uses filter matching rules)
    4. + *
    5. description
    6. + *
    7. category
    8. + *
    9. copyright
    10. + *
    11. license
    12. + *
    13. source
    14. + *
    + * + * @param filterExpr + * A standard OSGi filter + * @return List of resources matching the filters. + */ + Resource[] discoverResources(String filterExpr) throws InvalidSyntaxException; + + /** + * Discover any resources that match the given requirements. + * + * @param requirements + * @return List of resources matching the filter + */ + Resource[] discoverResources(Requirement[] requirements); + + /** + * Create a resolver. + * + * @return + */ + Resolver resolver(); + + /** + * Create a resolver on the given repositories. + * + * @param repositories the list of repositories to use for the resolution + * @return + */ + Resolver resolver(Repository[] repositories); + + /** + * Add a new repository to the federation. + * + * The url must point to a repository XML file. + * + * @param repository + * @return + * @throws Exception + */ + Repository addRepository(String repository) throws Exception; + + /** + * Add a new repository to the federation. + * + * The url must point to a repository XML file. + * + * @param repository + * @return + * @throws Exception + */ + Repository addRepository(URL repository) throws Exception; + + /** + * Remove a repository from the federation + * + * The url must point to a repository XML file. + * + * @param repository + * @return + */ + boolean removeRepository(String repository); + + /** + * List all the repositories. + * + * @return + */ + Repository[] listRepositories(); + + /** + * Return the repository containing the system bundle + * + * @return + */ + Repository getSystemRepository(); + + /** + * Return the repository containing locally installed resources + * + * @return + */ + Repository getLocalRepository(); + + /** + * Return a helper to perform various operations on the data model + * + * @return + */ + DataModelHelper getHelper(); + +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/RepositoryAdminImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/RepositoryAdminImpl.java deleted file mode 100644 index 2042c5fe037..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/RepositoryAdminImpl.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -import java.net.MalformedURLException; -import java.net.URL; -import java.util.*; - -import org.osgi.framework.*; -import org.osgi.service.obr.*; - -public class RepositoryAdminImpl implements RepositoryAdmin -{ - static BundleContext m_context = null; - private List m_urlList = new ArrayList(); - private Map m_repoMap = new HashMap(); - private boolean m_initialized = false; - - // Reusable comparator for sorting resources by name. - private Comparator m_nameComparator = new ResourceComparator(); - - private static final String DEFAULT_REPOSITORY_URL = - "http://oscar-osgi.sf.net/obr2/repository.xml"; - public static final String REPOSITORY_URL_PROP = "obr.repository.url"; - public static final String EXTERN_REPOSITORY_TAG = "extern-repositories"; - - public RepositoryAdminImpl(BundleContext context) - { - m_context = context; - - // Get repository URLs. - String urlStr = m_context.getProperty(REPOSITORY_URL_PROP); - if (urlStr != null) - { - StringTokenizer st = new StringTokenizer(urlStr); - if (st.countTokens() > 0) - { - while (st.hasMoreTokens()) - { - try - { - m_urlList.add(new URL(st.nextToken())); - } - catch (MalformedURLException ex) - { - System.err.println("RepositoryAdminImpl: " + ex); - } - } - } - } - - // Use the default URL if none were specified. - if (m_urlList.size() == 0) - { - try - { - m_urlList.add(new URL(DEFAULT_REPOSITORY_URL)); - } - catch (MalformedURLException ex) - { - System.err.println("RepositoryAdminImpl: " + ex); - } - } - } - - public synchronized Repository addRepository(URL url) throws Exception - { - if (!m_urlList.contains(url)) - { - m_urlList.add(url); - } - - // If the repository URL is a duplicate, then we will just - // replace the existing repository object with a new one, - // which is effectively the same as refreshing the repository. - Repository repo = new RepositoryImpl(url); - m_repoMap.put(url, repo); - return repo; - } - - public synchronized boolean removeRepository(URL url) - { - m_repoMap.remove(url); - return (m_urlList.remove(url)) ? true : false; - } - - public synchronized Repository[] listRepositories() - { - if (!m_initialized) - { - initialize(); - } - return (Repository[]) m_repoMap.values().toArray(new Repository[m_repoMap.size()]); - } - - public synchronized Resource getResource(String respositoryId) - { - // TODO: OBR - Auto-generated method stub - return null; - } - - public synchronized Resolver resolver() - { - if (!m_initialized) - { - initialize(); - } - - return new ResolverImpl(m_context, this); - } - - public synchronized Resource[] discoverResources(String filterExpr) - { - if (!m_initialized) - { - initialize(); - } - - Filter filter = null; - try - { - filter = m_context.createFilter(filterExpr); - } - catch (InvalidSyntaxException ex) - { - System.err.println(ex); - } - - Resource[] resources = null; - MapToDictionary dict = new MapToDictionary(null); - Repository[] repos = listRepositories(); - List matchList = new ArrayList(); - for (int repoIdx = 0; (repos != null) && (repoIdx < repos.length); repoIdx++) - { - resources = repos[repoIdx].getResources(); - for (int resIdx = 0; (resources != null) && (resIdx < resources.length); resIdx++) - { - dict.setSourceMap(resources[resIdx].getProperties()); - if (filter.match(dict)) - { - matchList.add(resources[resIdx]); - } - } - } - - // Convert matching resources to an array an sort them by name. - resources = (Resource[]) matchList.toArray(new Resource[matchList.size()]); - Arrays.sort(resources, m_nameComparator); - return resources; - } - - private void initialize() - { - m_initialized = true; - m_repoMap.clear(); - - for (int i = 0; i < m_urlList.size(); i++) - { - URL url = (URL) m_urlList.get(i); - try - { - Repository repo = new RepositoryImpl(url); - if (repo != null) - { - m_repoMap.put(url, repo); - } - } - catch (Exception ex) - { - System.err.println( - "RepositoryAdminImpl: Exception creating repository - " + ex); - System.err.println( - "RepositoryAdminImpl: Ignoring repository " + url); - } - } - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/RepositoryImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/RepositoryImpl.java deleted file mode 100644 index 2a24caf6154..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/RepositoryImpl.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -import java.io.*; -import java.lang.reflect.Method; -import java.net.*; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Arrays; - -import org.apache.felix.bundlerepository.metadataparser.XmlCommonHandler; -import org.apache.felix.bundlerepository.metadataparser.kxmlsax.KXml2SAXParser; -import org.osgi.service.obr.*; - -public class RepositoryImpl implements Repository -{ - private String m_name = null; - private long m_lastmodified = 0; - private URL m_url = null; - private Resource[] m_resources = null; - private int m_hopCount = 1; - - // Reusable comparator for sorting resources by name. - private ResourceComparator m_nameComparator = new ResourceComparator(); - - public RepositoryImpl(URL url) throws Exception - { - m_url = url; - parseRepositoryFile(m_hopCount); - } - - public URL getURL() - { - return m_url; - } - - protected void setURL(URL url) - { - m_url = url; - } - - public Resource[] getResources() - { - return m_resources; - } - - public void addResource(Resource resource) - { - // Set resource's repository. - ((ResourceImpl) resource).setRepository(this); - - // Add to resource array. - if (m_resources == null) - { - m_resources = new Resource[] { resource }; - } - else - { - Resource[] newResources = new Resource[m_resources.length + 1]; - System.arraycopy(m_resources, 0, newResources, 0, m_resources.length); - newResources[m_resources.length] = resource; - m_resources = newResources; - } - - Arrays.sort(m_resources, m_nameComparator); - } - - public String getName() - { - return m_name; - } - - public void setName(String name) - { - m_name = name; - } - - public long getLastModified() - { - return m_lastmodified; - } - - public void setLastmodified(String s) - { - SimpleDateFormat format = new SimpleDateFormat("yyyyMMddhhmmss.SSS"); - try - { - m_lastmodified = format.parse(s).getTime(); - } - catch (ParseException ex) - { - } - } - - /** - * Default setter method when setting parsed data from the XML file, - * which currently ignores everything. - **/ - protected Object put(Object key, Object value) - { - // Ignore everything for now. - return null; - } - - private void parseRepositoryFile(int hopCount) throws Exception - { -// TODO: OBR - Implement hop count. - InputStream is = null; - BufferedReader br = null; - - try - { - // Do it the manual way to have a chance to - // set request properties as proxy auth (EW). - URLConnection conn = m_url.openConnection(); - - // Support for http proxy authentication - String auth = System.getProperty("http.proxyAuth"); - if ((auth != null) && (auth.length() > 0)) - { - if ("http".equals(m_url.getProtocol()) || - "https".equals(m_url.getProtocol())) - { - String base64 = Util.base64Encode(auth); - conn.setRequestProperty( - "Proxy-Authorization", "Basic " + base64); - } - } - is = conn.getInputStream(); - - // Create the parser Kxml - XmlCommonHandler handler = new XmlCommonHandler(); - Object factory = new Object() { - public RepositoryImpl newInstance() - { - return RepositoryImpl.this; - } - }; - - // Get default setter method for Repository. - Method repoSetter = RepositoryImpl.class.getDeclaredMethod( - "put", new Class[] { Object.class, Object.class }); - - // Get default setter method for Resource. - Method resSetter = ResourceImpl.class.getDeclaredMethod( - "put", new Class[] { Object.class, Object.class }); - - // Map XML tags to types. - handler.addType("repository", factory, Repository.class, repoSetter); - handler.addType("resource", ResourceImpl.class, Resource.class, resSetter); - handler.addType("category", CategoryImpl.class, null, null); - handler.addType("require", RequirementImpl.class, Requirement.class, null); - handler.addType("capability", CapabilityImpl.class, Capability.class, null); - handler.addType("p", PropertyImpl.class, null, null); - handler.setDefaultType(String.class, null, null); - - br = new BufferedReader(new InputStreamReader(is)); - KXml2SAXParser parser; - parser = new KXml2SAXParser(br); - parser.parseXML(handler); - } - finally - { - try - { - if (is != null) is.close(); - } - catch (IOException ex) - { - // Not much we can do. - } - } - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Requirement.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Requirement.java new file mode 100644 index 00000000000..06b2bd7be11 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Requirement.java @@ -0,0 +1,100 @@ +/* + * 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. + */ +/* + * $Header: /cvshome/build/org.osgi.service.obr/src/org/osgi/service/obr/Requirement.java,v 1.4 2006/03/16 14:56:17 hargrave Exp $ + * + * Copyright (c) OSGi Alliance (2006). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This document is an experimental draft to enable interoperability +// between bundle repositories. There is currently no commitment to +// turn this draft into an official specification. +package org.apache.felix.bundlerepository; + +import java.util.Map; + +/** + * A named requirement specifies the need for certain capabilities with the same + * name. + * + * A requirement is said to be satisfied by a capability if and only if: + *
      + *
    • they have the same nsame
    • + *
    • the filter matches the capability properties
    • + *
    + * + * @version $Revision: 1.4 $ + */ +public interface Requirement +{ + /** + * Return a map of attributes. Requirements can have attributes, but these are not + * used for matching. They are for informational purposes only. + * + * @return The map of attributes. + */ + Map getAttributes(); + + /** + * Return the map of directives for this requirement. This requirements map does *not* + * contain requirements that are modeled via direct APIs on this interface, such as the + * filter, cardinality and resolution. + * @return + */ + Map getDirectives(); + + /** + * Return the name of the requirement. + */ + String getName(); + + /** + * Return the filter. + */ + String getFilter(); + + boolean isMultiple(); + + boolean isOptional(); + + boolean isExtend(); + + String getComment(); + + /** + * Check if the given capability satisfied this requirement. + * + * @param capability the capability to check + * @return true is the capability satisfies this requirement, false otherwise + */ + boolean isSatisfied(Capability capability); + +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/RequirementImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/RequirementImpl.java deleted file mode 100644 index b959f74b600..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/RequirementImpl.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -import org.osgi.framework.Filter; -import org.osgi.framework.InvalidSyntaxException; -import org.osgi.service.obr.Capability; -import org.osgi.service.obr.Requirement; - -public class RequirementImpl implements Requirement -{ - private String m_name = null; - private boolean m_extend = false; - private boolean m_multiple = false; - private boolean m_optional = false; - private Filter m_filter = null; - private String m_comment = null; - private MapToDictionary m_dict = new MapToDictionary(null); - - public RequirementImpl() - { - } - - public String getName() - { - return m_name; - } - - public synchronized void setName(String name) - { - m_name = name; - } - - public String getFilter() - { - return m_filter.toString(); - } - - public synchronized void setFilter(String filter) - { - try - { - m_filter = RepositoryAdminImpl.m_context.createFilter(filter); - } - catch (InvalidSyntaxException ex) - { - m_filter = null; - System.err.println(ex); - } - } - - public synchronized boolean isSatisfied(Capability capability) - { - m_dict.setSourceMap(capability.getProperties()); - return m_filter.match(m_dict); - } - - public boolean isExtend() - { - return m_extend; - } - - public synchronized void setExtend(String s) - { - m_extend = Boolean.valueOf(s).booleanValue(); - } - - public boolean isMultiple() - { - return m_multiple; - } - - public synchronized void setMultiple(String s) - { - m_multiple = Boolean.valueOf(s).booleanValue(); - } - - public boolean isOptional() - { - return m_optional; - } - - public synchronized void setOptional(String s) - { - m_optional = Boolean.valueOf(s).booleanValue(); - } - - public String getComment() - { - return m_comment; - } - - public synchronized void addText(String s) - { - m_comment = s; - } - - public synchronized boolean equals(Object o) - { - if (o instanceof Requirement) - { - Requirement r = (Requirement) o; - return m_name.equals(r.getName()) && - (m_optional == r.isOptional()) && - (m_multiple == r.isMultiple()) && - m_filter.toString().equals(r.getFilter()) && - ((m_comment == r.getComment()) || - ((m_comment != null) && (m_comment.equals(r.getComment())))); - } - return false; - } - - public int hashCode() - { - return m_filter.toString().hashCode(); - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Resolver.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Resolver.java new file mode 100644 index 00000000000..42e9dab5601 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Resolver.java @@ -0,0 +1,150 @@ +/* + * 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. + */ +/* + * $Header: /cvshome/build/org.osgi.service.obr/src/org/osgi/service/obr/Resolver.java,v 1.3 2006/03/16 14:56:17 hargrave Exp $ + * + * Copyright (c) OSGi Alliance (2006). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This document is an experimental draft to enable interoperability +// between bundle repositories. There is currently no commitment to +// turn this draft into an official specification. +package org.apache.felix.bundlerepository; + +public interface Resolver +{ + + int NO_OPTIONAL_RESOURCES = 0x0001; + int NO_LOCAL_RESOURCES = 0x0002; + int NO_SYSTEM_BUNDLE = 0x0004; + int DO_NOT_PREFER_LOCAL = 0x0008; + int START = 0x0010; + + /** + * Add the following resource to the resolution. + * + * The resource will be part of the output and all its requirements + * will be satisfied. + * + * It has the same effect has adding a requirement that will match + * this resource by symbolicname and version. + * + * The current resolution will be lost after adding a resource. + * + * @param resource the resource to add + */ + void add(Resource resource); + + /** + * Returns the list of resources that have been added to the resolution + * @return + */ + Resource[] getAddedResources(); + + /** + * Add the following requirement to the resolution + * + * The current resolution will be lost after adding a requirement. + * + * @param requirement the requirement to add + */ + void add(Requirement requirement); + + /** + * Returns the list of requirements that have been added to the resolution + * @return + */ + Requirement[] getAddedRequirements(); + + /** + * Add a global capability. + * + * A global capability is one capability provided by the environment + * but not reflected in local resources. + * + * @param capability the new global capability + */ + void addGlobalCapability(Capability capability); + + /** + * Returns the list of global capabilities + * @return + */ + Capability[] getGlobalCapabilities(); + + /** + * Start the resolution process and return whether the constraints have + * been successfully met or not. + * The resolution can be interrupted by a call to Thread.interrupt() at any + * time. The result will be to stop the resolver and throw an InterruptedException. + * + * @return true if the resolution has succeeded else false + * @throws InterruptedResolutionException if the resolution has been interrupted + */ + boolean resolve() throws InterruptedResolutionException; + + /** + * Start the resolution process with the following flags. + * @param flags resolution flags + * @return true if the resolution has succeeded else false + * @throws InterruptedResolutionException if the resolution has been interrupted + */ + boolean resolve(int flags) throws InterruptedResolutionException; + + /** + * List of mandatory resources that need to be installed + * @return + */ + Resource[] getRequiredResources(); + + /** + * List of optional resources that may be installed + * @return + */ + Resource[] getOptionalResources(); + + /** + * List of reasons why a resource has been included either as a mandatory or + * optional resource during the resolution. + * + * @param resource + * @return an array of Reason + */ + Reason[] getReason(Resource resource); + + /** + * List of requirements that could not be satisfied during the resolution + * @return + */ + Reason[] getUnsatisfiedRequirements(); + + void deploy(int flags); +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/ResolverImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/ResolverImpl.java deleted file mode 100644 index b98be57eb50..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/ResolverImpl.java +++ /dev/null @@ -1,624 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -import java.net.URL; -import java.util.*; - -import org.apache.felix.bundlerepository.LocalRepositoryImpl.LocalResourceImpl; -import org.osgi.framework.*; -import org.osgi.service.obr.*; - -public class ResolverImpl implements Resolver -{ - private BundleContext m_context = null; - private RepositoryAdmin m_admin = null; - private LocalRepositoryImpl m_local = null; - private Set m_addedSet = new HashSet(); - private Set m_resolveSet = new HashSet(); - private Set m_requiredSet = new HashSet(); - private Set m_optionalSet = new HashSet(); - private Map m_reasonMap = new HashMap(); - private Map m_unsatisfiedMap = new HashMap(); - private boolean m_resolved = false; - - public ResolverImpl(BundleContext context, RepositoryAdmin admin) - { - m_context = context; - m_admin = admin; - } - - public synchronized void add(Resource resource) - { - m_resolved = false; - m_addedSet.add(resource); - } - - public synchronized Requirement[] getUnsatisfiedRequirements() - { - if (m_resolved) - { - return (Requirement[]) - m_unsatisfiedMap.keySet().toArray( - new Requirement[m_unsatisfiedMap.size()]); - } - throw new IllegalStateException("The resources have not been resolved."); - } - - public synchronized Resource[] getOptionalResources() - { - if (m_resolved) - { - return (Resource[]) - m_optionalSet.toArray( - new Resource[m_optionalSet.size()]); - } - throw new IllegalStateException("The resources have not been resolved."); - } - - public synchronized Requirement[] getReason(Resource resource) - { - if (m_resolved) - { - return (Requirement[]) m_reasonMap.get(resource); - } - throw new IllegalStateException("The resources have not been resolved."); - } - - public synchronized Resource[] getResources(Requirement requirement) - { - if (m_resolved) - { - return (Resource[]) m_unsatisfiedMap.get(requirement); - } - throw new IllegalStateException("The resources have not been resolved."); - } - - public synchronized Resource[] getRequiredResources() - { - if (m_resolved) - { - return (Resource[]) - m_requiredSet.toArray( - new Resource[m_requiredSet.size()]); - } - throw new IllegalStateException("The resources have not been resolved."); - } - - public synchronized Resource[] getAddedResources() - { - return (Resource[]) m_addedSet.toArray(new Resource[m_addedSet.size()]); - } - - public synchronized boolean resolve() - { - // Get a current local repository. - // TODO: OBR - We might want to make a smarter local repository - // that caches installed bundles rather than re-parsing them - // each time, since this could be costly. - if (m_local != null) - { - m_local.dispose(); - } - m_local = new LocalRepositoryImpl(m_context); - - // Reset instance values. - m_resolveSet.clear(); - m_requiredSet.clear(); - m_optionalSet.clear(); - m_reasonMap.clear(); - m_unsatisfiedMap.clear(); - m_resolved = true; - - boolean result = true; - - // Loop through each resource in added list and resolve. - for (Iterator iter = m_addedSet.iterator(); iter.hasNext(); ) - { - if (!resolve((Resource) iter.next())) - { - // If any resource does not resolve, then the - // entire result will be false. - result = false; - } - } - - // Clean up the resulting data structures. - List locals = Arrays.asList(m_local.getResources()); - m_requiredSet.removeAll(m_addedSet); - m_requiredSet.removeAll(locals); - m_optionalSet.removeAll(m_addedSet); - m_optionalSet.removeAll(m_requiredSet); - m_optionalSet.removeAll(locals); - - // Return final result. - return result; - } - - private boolean resolve(Resource resource) - { - boolean result = true; - - // Check for a cycle. - if (m_resolveSet.contains(resource)) - { - return result; - } - - // Add to resolve map to avoid cycles. - m_resolveSet.add(resource); - - // Resolve the requirements for the resource according to the - // search order of: added, local, resolving, and remote resources. - Requirement[] reqs = resource.getRequirements(); - if (reqs != null) - { - Resource candidate = null; - for (int reqIdx = 0; reqIdx < reqs.length; reqIdx++) - { - candidate = searchAddedResources(reqs[reqIdx]); - if (candidate == null) - { - candidate = searchLocalResources(reqs[reqIdx]); - if (candidate == null) - { - candidate = searchResolvingResources(reqs[reqIdx]); - if (candidate == null) - { - candidate = searchRemoteResources(reqs[reqIdx]); - } - } - } - - if ((candidate == null) && !reqs[reqIdx].isOptional()) - { - // The resolve failed. - result = false; - // Associated the current resource to the requirement - // in the unsatisfied requirement map. - Resource[] resources = (Resource[]) m_unsatisfiedMap.get(reqs[reqIdx]); - if (resources == null) - { - resources = new Resource[] { resource }; - } - else - { - Resource[] tmp = new Resource[resources.length + 1]; - System.arraycopy(resources, 0, tmp, 0, resources.length); - tmp[resources.length] = resource; - resources = tmp; - } - m_unsatisfiedMap.put(reqs[reqIdx], resources); - } - else if (candidate != null) - { - // The resolved succeeded; record the candidate - // as either optional or required. - if (reqs[reqIdx].isOptional()) - { - m_optionalSet.add(candidate); - } - else - { - m_requiredSet.add(candidate); - } - - // Add the reason why the candidate was selected. - addReason(candidate, reqs[reqIdx]); - - // Try to resolve the candidate. - if (!resolve(candidate)) - { - result = false; - } - } - } - } - - return result; - } - - private Resource searchAddedResources(Requirement req) - { - for (Iterator iter = m_addedSet.iterator(); iter.hasNext(); ) - { - Resource resource = (Resource) iter.next(); - Capability[] caps = resource.getCapabilities(); - for (int capIdx = 0; (caps != null) && (capIdx < caps.length); capIdx++) - { - if (req.isSatisfied(caps[capIdx])) - { - // The requirement is already satisfied an existing - // resource, return the resource. - return resource; - } - } - } - - return null; - } - - private Resource searchLocalResources(Requirement req) - { - Resource[] resources = m_local.getResources(); - for (int resIdx = 0; (resources != null) && (resIdx < resources.length); resIdx++) - { - Capability[] caps = resources[resIdx].getCapabilities(); - for (int capIdx = 0; (caps != null) && (capIdx < caps.length); capIdx++) - { - if (req.isSatisfied(caps[capIdx])) - { - return resources[resIdx]; - } - } - } - - return null; - } - - private Resource searchResolvingResources(Requirement req) - { - Resource[] resources = (Resource[]) - m_resolveSet.toArray(new Resource[m_resolveSet.size()]); - for (int resIdx = 0; (resources != null) && (resIdx < resources.length); resIdx++) - { - Capability[] caps = resources[resIdx].getCapabilities(); - for (int capIdx = 0; (caps != null) && (capIdx < caps.length); capIdx++) - { - if (req.isSatisfied(caps[capIdx])) - { - return resources[resIdx]; - } - } - } - - return null; - } - - private Resource searchRemoteResources(Requirement req) - { - // For now, guess that if there is a version associated with - // the candidate capability that we should choose the highest - // version; otherwise, choose the resource with the greatest - // number of capabilities. - // TODO: OBR - This could probably be improved. - Resource best = null; - Version bestVersion = null; - Repository[] repos = m_admin.listRepositories(); - for (int repoIdx = 0; (repos != null) && (repoIdx < repos.length); repoIdx++) - { - Resource[] resources = repos[repoIdx].getResources(); - for (int resIdx = 0; (resources != null) && (resIdx < resources.length); resIdx++) - { - Capability[] caps = resources[resIdx].getCapabilities(); - for (int capIdx = 0; (caps != null) && (capIdx < caps.length); capIdx++) - { - if (req.isSatisfied(caps[capIdx])) - { - if (best == null) - { - best = resources[resIdx]; - Object v = caps[capIdx].getProperties().get(Resource.VERSION); - if ((v != null) && (v instanceof Version)) - { - bestVersion = (Version) v; - } - } - else - { - Object v = caps[capIdx].getProperties().get(Resource.VERSION); - - // If there is no version, then select the resource - // with the greatest number of capabilities. - if ((v == null) && (bestVersion == null) - && (best.getCapabilities().length < caps.length)) - { - best = resources[resIdx]; - bestVersion = (Version) v; - } - else if ((v != null) && (v instanceof Version)) - { - // If there is no best version or if the current - // resource's version is lower, then select it. - if ((bestVersion == null) || (bestVersion.compareTo(v) < 0)) - { - best = resources[resIdx]; - bestVersion = (Version) v; - } - // If the current resource version is equal to the - // best, then select the one with the greatest - // number of capabilities. - else if ((bestVersion != null) && (bestVersion.compareTo(v) == 0) - && (best.getCapabilities().length < caps.length)) - { - best = resources[resIdx]; - bestVersion = (Version) v; - } - } - } - } - } - } - } - - return best; - } - - public synchronized void deploy(boolean start) - { - // Must resolve if not already resolved. - if (!m_resolved && !resolve()) - { - // TODO: OBR - Use logger if possible. - System.err.println("Resolver: Cannot resolve target resources."); - return; - } - - // Check to make sure that our local state cache is up-to-date - // and error if it is not. This is not completely safe, because - // the state can still change during the operation, but we will - // be optimistic. This could also be made smarter so that it checks - // to see if the local state changes overlap with the resolver. - if (m_local.getLastModified() != m_local.getCurrentTimeStamp()) - { - throw new IllegalStateException("Framework state has changed, must resolve again."); - } - - // Eliminate duplicates from target, required, optional resources. - Map deployMap = new HashMap(); - Resource[] resources = getAddedResources(); - for (int i = 0; (resources != null) && (i < resources.length); i++) - { - deployMap.put(resources[i], resources[i]); - } - resources = getRequiredResources(); - for (int i = 0; (resources != null) && (i < resources.length); i++) - { - deployMap.put(resources[i], resources[i]); - } - resources = getOptionalResources(); - for (int i = 0; (resources != null) && (i < resources.length); i++) - { - deployMap.put(resources[i], resources[i]); - } - Resource[] deployResources = (Resource[]) - deployMap.keySet().toArray(new Resource[deployMap.size()]); - - // List to hold all resources to be started. - List startList = new ArrayList(); - - // Deploy each resource, which will involve either finding a locally - // installed resource to update or the installation of a new version - // of the resource to be deployed. - for (int i = 0; i < deployResources.length; i++) - { - // For the resource being deployed, see if there is an older - // version of the resource already installed that can potentially - // be updated. - LocalRepositoryImpl.LocalResourceImpl localResource = - findUpdatableLocalResource(deployResources[i]); - // If a potentially updatable older version was found, - // then verify that updating the local resource will not - // break any of the requirements of any of the other - // resources being deployed. - if ((localResource != null) && - isResourceUpdatable(localResource, deployResources[i], deployResources)) - { - // Only update if it is a different version. - if (!localResource.equals(deployResources[i])) - { - // Update the installed bundle. - try - { - localResource.getBundle().update(deployResources[i].getURL().openStream()); - - // If necessary, save the updated bundle to be - // started later. - if (start) - { - startList.add(localResource.getBundle()); - } - } - catch (Exception ex) - { - // TODO: OBR - Use logger if possible. - System.err.println("Resolver: Update error - " + Util.getBundleName(localResource.getBundle())); - ex.printStackTrace(System.err); - return; - } - } - } - else - { - // Install the bundle. - try - { - // Perform the install, but do not use the actual - // bundle JAR URL for the bundle location, since this will - // limit OBR's ability to manipulate bundle versions. Instead, - // use a unique timestamp as the bundle location. - URL url = deployResources[i].getURL(); - if (url != null) - { - Bundle bundle = m_context.installBundle( - "obr://" - + deployResources[i].getSymbolicName() - + "/" + System.currentTimeMillis(), - url.openStream()); - - // If necessary, save the installed bundle to be - // started later. - if (start) - { - startList.add(bundle); - } - } - } - catch (Exception ex) - { - // TODO: OBR - Use logger if possible. - System.err.println("Resolver: Install error - " - + deployResources[i].getSymbolicName()); - ex.printStackTrace(System.err); - return; - } - } - } - - for (int i = 0; i < startList.size(); i++) - { - try - { - ((Bundle) startList.get(i)).start(); - } - catch (BundleException ex) - { - // TODO: OBR - Use logger if possible. - System.err.println("Resolver: Start error - " + ex); - } - } - } - - private void addReason(Resource resource, Requirement req) - { - Requirement[] reasons = (Requirement[]) m_reasonMap.get(resource); - if (reasons == null) - { - reasons = new Requirement[] { req }; - } - else - { - Requirement[] tmp = new Requirement[reasons.length + 1]; - System.arraycopy(reasons, 0, tmp, 0, reasons.length); - tmp[reasons.length] = req; - reasons = tmp; - } - m_reasonMap.put(resource, reasons); - } - - // TODO: OBR - Think about this again and make sure that deployment ordering - // won't impact it...we need to update the local state too. - private LocalResourceImpl findUpdatableLocalResource(Resource resource) - { - // Determine if any other versions of the specified resource - // already installed. - Resource[] localResources = findLocalResources(resource.getSymbolicName()); - if (localResources != null) - { - // Since there are local resources with the same symbolic - // name installed, then we must determine if we can - // update an existing resource or if we must install - // another one. Loop through all local resources with same - // symbolic name and find the first one that can be updated - // without breaking constraints of existing local resources. - for (int i = 0; i < localResources.length; i++) - { - if (isResourceUpdatable(localResources[i], resource, m_local.getResources())) - { - return (LocalResourceImpl) localResources[i]; - } - } - } - return null; - } - - private Resource[] findLocalResources(String symName) - { - Resource[] localResources = m_local.getResources(); - - List matchList = new ArrayList(); - for (int i = 0; i < localResources.length; i++) - { - String localSymName = localResources[i].getSymbolicName(); - if ((localSymName != null) && localSymName.equals(symName)) - { - matchList.add(localResources[i]); - } - } - return (Resource[]) matchList.toArray(new Resource[matchList.size()]); - } - - private boolean isResourceUpdatable( - Resource oldVersion, Resource newVersion, Resource[] resources) - { - // Get all of the local resolvable requirements for the old - // version of the resource from the specified resource array. - Requirement[] reqs = getResolvableRequirements(oldVersion, resources); - - // Now make sure that all of the requirements resolved by the - // old version of the resource can also be resolved by the new - // version of the resource. - Capability[] caps = newVersion.getCapabilities(); - if (caps == null) - { - return false; - } - for (int reqIdx = 0; reqIdx < reqs.length; reqIdx++) - { - boolean satisfied = false; - for (int capIdx = 0; !satisfied && (capIdx < caps.length); capIdx++) - { - if (reqs[reqIdx].isSatisfied(caps[capIdx])) - { - satisfied = true; - } - } - - // If any of the previously resolved requirements cannot - // be resolved, then the resource is not updatable. - if (!satisfied) - { - return false; - } - } - - return true; - } - - private Requirement[] getResolvableRequirements(Resource resource, Resource[] resources) - { - // For the specified resource, find all requirements that are - // satisfied by any of its capabilities in the specified resource - // array. - Capability[] caps = resource.getCapabilities(); - if ((caps != null) && (caps.length > 0)) - { - List reqList = new ArrayList(); - for (int capIdx = 0; capIdx < caps.length; capIdx++) - { - boolean added = false; - for (int resIdx = 0; !added && (resIdx < resources.length); resIdx++) - { - Requirement[] reqs = resources[resIdx].getRequirements(); - for (int reqIdx = 0; - (reqs != null) && (reqIdx < reqs.length); - reqIdx++) - { - if (reqs[reqIdx].isSatisfied(caps[capIdx])) - { - added = true; - reqList.add(reqs[reqIdx]); - } - } - } - } - return (Requirement[]) - reqList.toArray(new Requirement[reqList.size()]); - } - return null; - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Resource.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Resource.java new file mode 100644 index 00000000000..7246873fa02 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Resource.java @@ -0,0 +1,151 @@ +/* + * 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. + */ +/* + * $Header: /cvshome/build/org.osgi.service.obr/src/org/osgi/service/obr/Resource.java,v 1.5 2006/03/16 14:56:17 hargrave Exp $ + * + * Copyright (c) OSGi Alliance (2006). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This document is an experimental draft to enable interoperability +// between bundle repositories. There is currently no commitment to +// turn this draft into an official specification. +package org.apache.felix.bundlerepository; + +import java.util.Map; + +import org.osgi.framework.Version; + +/** + * A resource is an abstraction of a downloadable thing, like a bundle. + * + * Resources have capabilities and requirements. All a resource's requirements + * must be satisfied before it can be installed. + */ +public interface Resource +{ + final String LICENSE_URI = "license"; + + final String DESCRIPTION = "description"; + + final String DOCUMENTATION_URI = "documentation"; + + final String COPYRIGHT = "copyright"; + + final String SOURCE_URI = "source"; + + final String JAVADOC_URI = "javadoc"; + + final String SYMBOLIC_NAME = "symbolicname"; + + final String PRESENTATION_NAME = "presentationname"; + + final String ID = "id"; + + final String VERSION = "version"; + + final String URI = "uri"; + + final String SIZE = "size"; + + final String CATEGORY = "category"; + + final String MANIFEST_VERSION = "manifestversion"; + + /** + * Get all resource properties + * @return + */ + Map getProperties(); + + /** + * Shortcut for {{getProperties().get(ID)}} + * @return + */ + String getId(); + + /** + * Shortcut for {{getProperties().get(SYMBOLIC_NAME)}} + * @return + */ + String getSymbolicName(); + + /** + * Shortcut for {{getProperties().get(VERSION)}} + * @return + */ + Version getVersion(); + + /** + * Shortcut for {{getProperties().get(PRESENTATION_NAME)}} + * @return + */ + String getPresentationName(); + + /** + * Shortcut for {{getProperties().get(URI)}} + * @return + */ + String getURI(); + + /** + * Shortcut for {{getProperties().get(SIZE)}} + * @return + */ + Long getSize(); + + /** + * Retrieve this resource categories + * @return + */ + String[] getCategories(); + + /** + * Retrieve the capabilities + * @return + */ + Capability[] getCapabilities(); + + /** + * Retrieve the requirements + * + * @return + */ + Requirement[] getRequirements(); + + /** + * Returns whether this resource is a local one or not. + * + * Local resources are already available in the OSGi framework and thus will be + * preferred over other resources. + */ + boolean isLocal(); + +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/ResourceComparator.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/ResourceComparator.java deleted file mode 100644 index cb84c33ba36..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/ResourceComparator.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -import java.util.Comparator; - -import org.osgi.service.obr.Resource; - -class ResourceComparator implements Comparator -{ - public int compare(Object o1, Object o2) - { - Resource r1 = (Resource) o1; - Resource r2 = (Resource) o2; - String name1 = (String) r1.getPresentationName(); - String name2 = (String) r2.getPresentationName(); - return name1.compareToIgnoreCase(name2); - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/ResourceImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/ResourceImpl.java deleted file mode 100644 index 55fa4222d89..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/ResourceImpl.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -import java.net.MalformedURLException; -import java.net.URL; -import java.util.*; - -import org.osgi.framework.Version; -import org.osgi.service.obr.*; - -public class ResourceImpl implements Resource -{ - private final String URI = "uri"; - - private Repository m_repo = null; - private Map m_map = null; - private List m_catList = new ArrayList(); - private List m_capList = new ArrayList(); - private List m_reqList = new ArrayList(); - - private String m_resourceURI = ""; - private String m_docURI = ""; - private String m_licenseURI = ""; - private String m_sourceURI = ""; - private boolean m_converted = false; - - public ResourceImpl() - { - this(null); - } - - public ResourceImpl(ResourceImpl resource) - { - m_map = new TreeMap(new Comparator() { - public int compare(Object o1, Object o2) - { - return o1.toString().compareToIgnoreCase(o2.toString()); - } - }); - - if (resource != null) - { - m_map.putAll(resource.getProperties()); - m_catList.addAll(resource.m_catList); - m_capList.addAll(resource.m_capList); - m_reqList.addAll(resource.m_reqList); - } - } - - public boolean equals(Object o) - { - if (o instanceof Resource) - { - return ((Resource) o).getSymbolicName().equals(getSymbolicName()) - && ((Resource) o).getVersion().equals(getVersion()); - } - return false; - } - - public int hashCode() - { - return getSymbolicName().hashCode() ^ getVersion().hashCode(); - } - - public Map getProperties() - { - if (!m_converted) - { - convertURItoURL(); - } - return m_map; - } - - public String getPresentationName() - { - return (String) m_map.get(PRESENTATION_NAME); - } - - public String getSymbolicName() - { - return (String) m_map.get(SYMBOLIC_NAME); - } - - public String getId() - { - return (String) m_map.get(ID); - } - - public Version getVersion() - { - return (Version) m_map.get(VERSION); - } - - public URL getURL() - { - if (!m_converted) - { - convertURItoURL(); - } - return (URL) m_map.get(URL); - } - - public Requirement[] getRequirements() - { - return (Requirement[]) m_reqList.toArray(new Requirement[m_reqList.size()]); - } - - protected void addRequire(Requirement req) - { - m_reqList.add(req); - } - - public Capability[] getCapabilities() - { - return (Capability[]) m_capList.toArray(new Capability[m_capList.size()]); - } - - protected void addCapability(Capability cap) - { - m_capList.add(cap); - } - - public String[] getCategories() - { - return (String[]) m_catList.toArray(new String[m_catList.size()]); - } - - protected void addCategory(CategoryImpl cat) - { - m_catList.add(cat.getId()); - } - - public Repository getRepository() - { - return m_repo; - } - - protected void setRepository(Repository repo) - { - m_repo = repo; - } - - /** - * Default setter method when setting parsed data from the XML file. - **/ - protected Object put(Object key, Object value) - { - // Capture the URIs since they might be relative, so we - // need to defer setting the actual URL value until they - // are used so that we will know our repository and its - // base URL. - if (key.equals(LICENSE_URL)) - { - m_licenseURI = (String) value; - } - else if (key.equals(DOCUMENTATION_URL)) - { - m_docURI = (String) value; - } - else if (key.equals(SOURCE_URL)) - { - m_sourceURI = (String) value; - } - else if (key.equals(URI)) - { - m_resourceURI = (String) value; - } - else - { - if (key.equals(VERSION)) - { - value = new Version(value.toString()); - } - else if (key.equals(SIZE)) - { - value = Long.valueOf(value.toString()); - } - - return m_map.put(key, value); - } - - return null; - } - - private void convertURItoURL() - { - if (m_repo != null) - { - try - { - URL base = m_repo.getURL(); - if (m_resourceURI != null) - { - m_map.put(URL, new URL(base, m_resourceURI)); - } - if (m_docURI != null) - { - m_map.put(DOCUMENTATION_URL, new URL(base, m_docURI)); - } - if (m_licenseURI != null) - { - m_map.put(LICENSE_URL, new URL(base, m_licenseURI)); - } - if (m_sourceURI != null) - { - m_map.put(SOURCE_URL, new URL(base, m_sourceURI)); - } - m_converted = true; - } - catch (MalformedURLException ex) - { - ex.printStackTrace(System.err); - } - } - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Util.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Util.java deleted file mode 100644 index 8b0c0be4ae0..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/Util.java +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -import java.io.*; -import java.util.ArrayList; -import java.util.List; - -import org.osgi.framework.Bundle; -import org.osgi.framework.Constants; - -public class Util -{ - public static String getClassName(String className) - { - if (className == null) - { - className = ""; - } - return (className.lastIndexOf('.') < 0) - ? "" : className.substring(className.lastIndexOf('.') + 1); - } - - public static String getBundleName(Bundle bundle) - { - String name = (String) bundle.getHeaders().get(Constants.BUNDLE_NAME); - return (name == null) - ? "Bundle " + Long.toString(bundle.getBundleId()) - : name; - } - - /** - * Parses delimited string and returns an array containing the tokens. This - * parser obeys quotes, so the delimiter character will be ignored if it is - * inside of a quote. This method assumes that the quote character is not - * included in the set of delimiter characters. - * @param value the delimited string to parse. - * @param delim the characters delimiting the tokens. - * @return an array of string tokens or null if there were no tokens. - **/ - public static String[] parseDelimitedString(String value, String delim) - { - if (value == null) - { - value = ""; - } - - List list = new ArrayList(); - - int CHAR = 1; - int DELIMITER = 2; - int STARTQUOTE = 4; - int ENDQUOTE = 8; - - StringBuffer sb = new StringBuffer(); - - int expecting = (CHAR | DELIMITER | STARTQUOTE); - - for (int i = 0; i < value.length(); i++) - { - char c = value.charAt(i); - - boolean isDelimiter = (delim.indexOf(c) >= 0); - boolean isQuote = (c == '"'); - - if (isDelimiter && ((expecting & DELIMITER) > 0)) - { - list.add(sb.toString().trim()); - sb.delete(0, sb.length()); - expecting = (CHAR | DELIMITER | STARTQUOTE); - } - else if (isQuote && ((expecting & STARTQUOTE) > 0)) - { - sb.append(c); - expecting = CHAR | ENDQUOTE; - } - else if (isQuote && ((expecting & ENDQUOTE) > 0)) - { - sb.append(c); - expecting = (CHAR | STARTQUOTE | DELIMITER); - } - else if ((expecting & CHAR) > 0) - { - sb.append(c); - } - else - { - throw new IllegalArgumentException("Invalid delimited string: " + value); - } - } - - if (sb.length() > 0) - { - list.add(sb.toString().trim()); - } - - return (String[]) list.toArray(new String[list.size()]); - } - - public static int compareVersion(int[] v1, int[] v2) - { - if (v1[0] > v2[0]) - { - return 1; - } - else if (v1[0] < v2[0]) - { - return -1; - } - else if (v1[1] > v2[1]) - { - return 1; - } - else if (v1[1] < v2[1]) - { - return -1; - } - else if (v1[2] > v2[2]) - { - return 1; - } - else if (v1[2] < v2[2]) - { - return -1; - } - return 0; - } - - private static final byte encTab[] = { 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, - 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, - 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x61, 0x62, 0x63, 0x64, - 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, - 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x30, 0x31, - 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x2b, 0x2f }; - - private static final byte decTab[] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, - -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, - -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, - 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, - 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, - 48, 49, 50, 51, -1, -1, -1, -1, -1 }; - - public static String base64Encode(String s) throws IOException - { - return encode(s.getBytes(), 0); - } - - /** - * Encode a raw byte array to a Base64 String. - * - * @param in Byte array to encode. - * @param len Length of Base64 lines. 0 means no line breaks. - **/ - public static String encode(byte[] in, int len) throws IOException - { - ByteArrayOutputStream baos = null; - ByteArrayInputStream bais = null; - try - { - baos = new ByteArrayOutputStream(); - bais = new ByteArrayInputStream(in); - encode(bais, baos, len); - // ASCII byte array to String - return (new String(baos.toByteArray())); - } - finally - { - if (baos != null) - { - baos.close(); - } - if (bais != null) - { - bais.close(); - } - } - } - - public static void encode(InputStream in, OutputStream out, int len) - throws IOException - { - - // Check that length is a multiple of 4 bytes - if (len % 4 != 0) - { - throw new IllegalArgumentException("Length must be a multiple of 4"); - } - - // Read input stream until end of file - int bits = 0; - int nbits = 0; - int nbytes = 0; - int b; - - while ((b = in.read()) != -1) - { - bits = (bits << 8) | b; - nbits += 8; - while (nbits >= 6) - { - nbits -= 6; - out.write(encTab[0x3f & (bits >> nbits)]); - nbytes++; - // New line - if (len != 0 && nbytes >= len) - { - out.write(0x0d); - out.write(0x0a); - nbytes -= len; - } - } - } - - switch (nbits) - { - case 2: - out.write(encTab[0x3f & (bits << 4)]); - out.write(0x3d); // 0x3d = '=' - out.write(0x3d); - break; - case 4: - out.write(encTab[0x3f & (bits << 2)]); - out.write(0x3d); - break; - } - - if (len != 0) - { - if (nbytes != 0) - { - out.write(0x0d); - out.write(0x0a); - } - out.write(0x0d); - out.write(0x0a); - } - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/VersionRange.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/VersionRange.java deleted file mode 100644 index 381d93b9716..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/VersionRange.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository; - -import org.osgi.framework.Version; - -public class VersionRange -{ - private Version m_low = null; - private boolean m_isLowInclusive = false; - private Version m_high = null; - private boolean m_isHighInclusive = false; - - public VersionRange(Version low, boolean isLowInclusive, - Version high, boolean isHighInclusive) - { - m_low = low; - m_isLowInclusive = isLowInclusive; - m_high = high; - m_isHighInclusive = isHighInclusive; - } - - public Version getLow() - { - return m_low; - } - - public boolean isLowInclusive() - { - return m_isLowInclusive; - } - - public Version getHigh() - { - return m_high; - } - - public boolean isHighInclusive() - { - return m_isHighInclusive; - } - - public boolean isInRange(Version version) - { - // We might not have an upper end to the range. - if (m_high == null) - { - return (version.compareTo(m_low) >= 0); - } - else if (isLowInclusive() && isHighInclusive()) - { - return (version.compareTo(m_low) >= 0) && (version.compareTo(m_high) <= 0); - } - else if (isHighInclusive()) - { - return (version.compareTo(m_low) > 0) && (version.compareTo(m_high) <= 0); - } - else if (isLowInclusive()) - { - return (version.compareTo(m_low) >= 0) && (version.compareTo(m_high) < 0); - } - return (version.compareTo(m_low) > 0) && (version.compareTo(m_high) < 0); - } - - public static VersionRange parse(String range) - { - // Check if the version is an interval. - if (range.indexOf(',') >= 0) - { - String s = range.substring(1, range.length() - 1); - String vlo = s.substring(0, s.indexOf(',')); - String vhi = s.substring(s.indexOf(',') + 1, s.length()); - return new VersionRange ( - new Version(vlo), (range.charAt(0) == '['), - new Version(vhi), (range.charAt(range.length() - 1) == ']')); - } - else - { - return new VersionRange(new Version(range), true, null, false); - } - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/Activator.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/Activator.java new file mode 100644 index 00000000000..a202173a4ea --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/Activator.java @@ -0,0 +1,140 @@ +/* + * 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.bundlerepository.impl; + +import java.util.Hashtable; + +import org.apache.felix.bundlerepository.RepositoryAdmin; +import org.apache.felix.bundlerepository.impl.wrapper.Wrapper; +import org.apache.felix.utils.log.Logger; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.service.repository.Repository; +import org.osgi.service.url.URLConstants; +import org.osgi.service.url.URLStreamHandlerService; + +public class Activator implements BundleActivator +{ + private static BundleContext context = null; + private static Logger logger = new Logger(null); + private transient RepositoryAdminImpl m_repoAdmin = null; + + + public static BundleContext getContext() + { + return context; + } + + static void setContext(BundleContext context) + { + Activator.context = context; + } + + public static void log(int level, String message) + { + if (logger != null) + { + logger.log(level, message); + } + } + + public static void log(int level, String message, Throwable exception) + { + if (logger != null) + { + logger.log(level, message, exception); + } + } + + public void start(BundleContext context) + { + Activator.context = context; + Activator.logger = new Logger(context); + + // Register bundle repository service. + m_repoAdmin = new RepositoryAdminImpl(context, logger); + context.registerService( + RepositoryAdmin.class.getName(), + m_repoAdmin, null); + + // Register the OSGi Repository-spec compliant facade + context.registerService( + Repository.class.getName(), + new OSGiRepositoryImpl(m_repoAdmin), null); + + try + { + context.registerService( + org.osgi.service.obr.RepositoryAdmin.class.getName(), + Wrapper.wrap(m_repoAdmin), null); + } + catch (Throwable th) + { + // Ignore + } + + // We dynamically import the impl service API, so it + // might not actually be available, so be ready to catch + // the exception when we try to register the command service. + try + { + // Register "obr" impl command service as a + // wrapper for the bundle repository service. + context.registerService( + org.apache.felix.shell.Command.class.getName(), + new ObrCommandImpl(Activator.context, m_repoAdmin), null); + } + catch (Throwable th) + { + // Ignore. + } + + try + { + Hashtable dict = new Hashtable(); + dict.put("osgi.command.scope", "obr"); + dict.put("osgi.command.function", new String[] { + "deploy", "info", "javadoc", "list", "repos", "source" }); + context.registerService(ObrGogoCommand.class.getName(), + new ObrGogoCommand(Activator.context, m_repoAdmin), dict); + } + catch (Throwable th) + { + // Ignore + } + + try + { + Hashtable dict = new Hashtable(); + dict.put(URLConstants.URL_HANDLER_PROTOCOL, "obr"); + context.registerService(URLStreamHandlerService.class.getName(), + new ObrURLStreamHandlerService(Activator.context, m_repoAdmin), dict); + } + catch (Exception e) + { + throw new RuntimeException("could not register obr url handler"); + } + + } + + public void stop(BundleContext context) + { + m_repoAdmin.dispose(); + } +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/Base64Encoder.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/Base64Encoder.java new file mode 100644 index 00000000000..93b89085e21 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/Base64Encoder.java @@ -0,0 +1,131 @@ +/* + * 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.bundlerepository.impl; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class Base64Encoder +{ + private static final byte encTab[] = { 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, + 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, + 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x61, 0x62, 0x63, 0x64, + 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, + 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x30, 0x31, + 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x2b, 0x2f }; + + public static String base64Encode(String s) throws IOException + { + return encode(s.getBytes(), 0); + } + + /** + * Encode a raw byte array to a Base64 String. + * + * @param in Byte array to encode. + * @param len Length of Base64 lines. 0 means no line breaks. + **/ + public static String encode(byte[] in, int len) throws IOException + { + ByteArrayOutputStream baos = null; + ByteArrayInputStream bais = null; + try + { + baos = new ByteArrayOutputStream(); + bais = new ByteArrayInputStream(in); + encode(bais, baos, len); + // ASCII byte array to String + return (new String(baos.toByteArray())); + } + finally + { + if (baos != null) + { + baos.close(); + } + if (bais != null) + { + bais.close(); + } + } + } + + public static void encode(InputStream in, OutputStream out, int len) + throws IOException + { + + // Check that length is a multiple of 4 bytes + if (len % 4 != 0) + { + throw new IllegalArgumentException("Length must be a multiple of 4"); + } + + // Read input stream until end of file + int bits = 0; + int nbits = 0; + int nbytes = 0; + int b; + + while ((b = in.read()) != -1) + { + bits = (bits << 8) | b; + nbits += 8; + while (nbits >= 6) + { + nbits -= 6; + out.write(encTab[0x3f & (bits >> nbits)]); + nbytes++; + // New line + if (len != 0 && nbytes >= len) + { + out.write(0x0d); + out.write(0x0a); + nbytes -= len; + } + } + } + + switch (nbits) + { + case 2: + out.write(encTab[0x3f & (bits << 4)]); + out.write(0x3d); // 0x3d = '=' + out.write(0x3d); + break; + case 4: + out.write(encTab[0x3f & (bits << 2)]); + out.write(0x3d); + break; + } + + if (len != 0) + { + if (nbytes != 0) + { + out.write(0x0d); + out.write(0x0a); + } + out.write(0x0d); + out.write(0x0a); + } + } +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/CapabilityImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/CapabilityImpl.java new file mode 100644 index 00000000000..50e0f2fa6bb --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/CapabilityImpl.java @@ -0,0 +1,106 @@ +/* + * 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.bundlerepository.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.felix.bundlerepository.Capability; +import org.apache.felix.bundlerepository.Property; + +public class CapabilityImpl implements Capability +{ + private String m_name = null; + private final Map m_attributes = new HashMap(); + private final Map m_directives = new HashMap(); + private final List m_propList = new ArrayList(); + + public CapabilityImpl() + { + } + + public CapabilityImpl(String name) + { + setName(name); + } + + public CapabilityImpl(String name, PropertyImpl[] properties) + { + setName(name); + for (int i = 0; properties != null && i < properties.length; i++) + { + addProperty(properties[i]); + } + } + + public String getName() + { + return m_name; + } + + public void setName(String name) + { + m_name = name.intern(); + } + + public Map getPropertiesAsMap() + { + return m_attributes; + } + + public Property[] getProperties() + { + return m_propList.toArray(new Property[m_propList.size()]); + } + + public void addProperty(Property prop) + { + // m_map.put(prop.getName().toLowerCase(), prop.getConvertedValue()); // TODO is toLowerCase() on the key the right thing to do? + // However if we definitely need to re-enable the to-lowercasing, the Felix Util FilterImpl supports treating filters + // case-insensitively + m_attributes.put(prop.getName(), prop.getConvertedValue()); + m_propList.add(prop); + } + + public void addProperty(String name, String value) + { + addProperty(name, null, value); + } + + public void addProperty(String name, String type, String value) + { + addProperty(new PropertyImpl(name, type, value)); + } + + public String toString() + { + return m_name + ":" + m_attributes.toString(); + } + + public void addDirective(String key, String value) { + m_directives.put(key, value); + } + + public Map getDirectives() { + return Collections.unmodifiableMap(m_directives); + } +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/DataModelHelperImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/DataModelHelperImpl.java new file mode 100644 index 00000000000..5d845b75cf8 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/DataModelHelperImpl.java @@ -0,0 +1,1080 @@ +/* + * 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.bundlerepository.impl; + +import java.io.*; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Dictionary; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.zip.GZIPInputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.apache.felix.bundlerepository.Capability; +import org.apache.felix.bundlerepository.DataModelHelper; +import org.apache.felix.bundlerepository.Property; +import org.apache.felix.bundlerepository.Repository; +import org.apache.felix.bundlerepository.Requirement; +import org.apache.felix.bundlerepository.Resource; +import org.apache.felix.utils.filter.FilterImpl; +import org.apache.felix.utils.manifest.Attribute; +import org.apache.felix.utils.manifest.Clause; +import org.apache.felix.utils.manifest.Directive; +import org.apache.felix.utils.manifest.Parser; +import org.apache.felix.utils.version.VersionCleaner; +import org.apache.felix.utils.version.VersionRange; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.Version; + +public class DataModelHelperImpl implements DataModelHelper +{ + + public static final String BUNDLE_LICENSE = "Bundle-License"; + public static final String BUNDLE_SOURCE = "Bundle-Source"; + + public Requirement requirement(String name, String filter) + { + RequirementImpl req = new RequirementImpl(); + req.setName(name); + if (filter != null) + { + req.setFilter(filter); + } + return req; + } + + public Filter filter(String filter) + { + try + { + return FilterImpl.newInstance(filter); + } + catch (InvalidSyntaxException e) + { + IllegalArgumentException ex = new IllegalArgumentException(); + ex.initCause(e); + throw ex; + } + } + + public Repository repository(final URL url) throws Exception + { + InputStream is = null; + + try + { + if (url.getPath().endsWith(".zip")) + { + ZipInputStream zin = new ZipInputStream(FileUtil.openURL(url)); + ZipEntry entry = zin.getNextEntry(); + while (entry != null) + { + if (entry.getName().equals("repository.xml") || entry.getName().equals("index.xml")) + { + is = zin; + break; + } + entry = zin.getNextEntry(); + } + // as the ZipInputStream is not used further it would not be closed. + if (is == null) + { + try + { + zin.close(); + } + catch (IOException ex) + { + // Not much we can do. + } + } + } + else if (url.getPath().endsWith(".gz")) + { + is = new GZIPInputStream(FileUtil.openURL(url)); + } + else + { + is = FileUtil.openURL(url); + } + + if (is != null) + { + String repositoryUri = url.toExternalForm(); + String baseUri; + if (repositoryUri.endsWith(".zip")) { + baseUri = new StringBuilder("jar:").append(repositoryUri).append("!/").toString(); + } else if (repositoryUri.endsWith(".xml")) { + baseUri = repositoryUri.substring(0, repositoryUri.lastIndexOf('/') + 1); + } else { + baseUri = repositoryUri; + } + RepositoryImpl repository = repository(is, URI.create(baseUri)); + repository.setURI(repositoryUri); + + return repository; + } + else + { + // This should not happen. + throw new Exception("Unable to get input stream for repository."); + } + } + finally + { + try + { + if (is != null) + { + is.close(); + } + } + catch (IOException ex) + { + // Not much we can do. + } + } + } + + public RepositoryImpl repository(InputStream is, URI baseURI) throws Exception + { + RepositoryParser parser = RepositoryParser.getParser(); + RepositoryImpl repository = parser.parseRepository(is, baseURI); + + return repository; + } + + public Repository repository(Resource[] resources) + { + return new RepositoryImpl(resources); + } + + public Capability capability(String name, Map properties) + { + CapabilityImpl cap = new CapabilityImpl(name); + for (Iterator it = properties.entrySet().iterator(); it.hasNext();) + { + Map.Entry e = (Map.Entry) it.next(); + cap.addProperty((String) e.getKey(), (String) e.getValue()); + } + return cap; + } + + public String writeRepository(Repository repository) + { + try + { + StringWriter sw = new StringWriter(); + writeRepository(repository, sw); + return sw.toString(); + } + catch (IOException e) + { + IllegalStateException ex = new IllegalStateException(e); + ex.initCause(e); + throw ex; + } + } + + public void writeRepository(Repository repository, Writer writer) throws IOException + { + XmlWriter w = new XmlWriter(writer); + toXml(w, repository); + } + + public String writeResource(Resource resource) + { + try + { + StringWriter sw = new StringWriter(); + writeResource(resource, sw); + return sw.toString(); + } + catch (IOException e) + { + IllegalStateException ex = new IllegalStateException(e); + ex.initCause(e); + throw ex; + } + } + + public void writeResource(Resource resource, Writer writer) throws IOException + { + XmlWriter w = new XmlWriter(writer); + toXml(w, resource); + } + + public String writeCapability(Capability capability) + { + try + { + StringWriter sw = new StringWriter(); + writeCapability(capability, sw); + return sw.toString(); + } + catch (IOException e) + { + IllegalStateException ex = new IllegalStateException(e); + ex.initCause(e); + throw ex; + } + } + + public void writeCapability(Capability capability, Writer writer) throws IOException + { + XmlWriter w = new XmlWriter(writer); + toXml(w, capability); + } + + public String writeRequirement(Requirement requirement) + { + try + { + StringWriter sw = new StringWriter(); + writeRequirement(requirement, sw); + return sw.toString(); + } + catch (IOException e) + { + IllegalStateException ex = new IllegalStateException(e); + ex.initCause(e); + throw ex; + } + } + + public void writeRequirement(Requirement requirement, Writer writer) throws IOException + { + XmlWriter w = new XmlWriter(writer); + toXml(w, requirement); + } + + public String writeProperty(Property property) + { + try + { + StringWriter sw = new StringWriter(); + writeProperty(property, sw); + return sw.toString(); + } + catch (IOException e) + { + IllegalStateException ex = new IllegalStateException(e); + ex.initCause(e); + throw ex; + } + } + + public void writeProperty(Property property, Writer writer) throws IOException + { + XmlWriter w = new XmlWriter(writer); + toXml(w, property); + } + + private static void toXml(XmlWriter w, Repository repository) throws IOException + { + SimpleDateFormat format = new SimpleDateFormat("yyyyMMddhhmmss.SSS"); + w.element(RepositoryParser.REPOSITORY) + .attribute(RepositoryParser.NAME, repository.getName()) + .attribute(RepositoryParser.LASTMODIFIED, format.format(new Date(repository.getLastModified()))); + + if (repository instanceof RepositoryImpl) + { + Referral[] referrals = ((RepositoryImpl) repository).getReferrals(); + for (int i = 0; referrals != null && i < referrals.length; i++) + { + w.element(RepositoryParser.REFERRAL) + .attribute(RepositoryParser.DEPTH, new Integer(referrals[i].getDepth())) + .attribute(RepositoryParser.URL, referrals[i].getUrl()) + .end(); + } + } + + Resource[] resources = repository.getResources(); + for (int i = 0; resources != null && i < resources.length; i++) + { + toXml(w, resources[i]); + } + + w.end(); + } + + private static void toXml(XmlWriter w, Resource resource) throws IOException + { + w.element(RepositoryParser.RESOURCE) + .attribute(Resource.ID, resource.getId()) + .attribute(Resource.SYMBOLIC_NAME, resource.getSymbolicName()) + .attribute(Resource.PRESENTATION_NAME, resource.getPresentationName()) + .attribute(Resource.URI, getRelativeUri(resource, Resource.URI)) + .attribute(Resource.VERSION, resource.getVersion().toString()); + + w.textElement(Resource.DESCRIPTION, resource.getProperties().get(Resource.DESCRIPTION)) + .textElement(Resource.SIZE, resource.getProperties().get(Resource.SIZE)) + .textElement(Resource.DOCUMENTATION_URI, getRelativeUri(resource, Resource.DOCUMENTATION_URI)) + .textElement(Resource.SOURCE_URI, getRelativeUri(resource, Resource.SOURCE_URI)) + .textElement(Resource.JAVADOC_URI, getRelativeUri(resource, Resource.JAVADOC_URI)) + .textElement(Resource.LICENSE_URI, getRelativeUri(resource, Resource.LICENSE_URI)); + + String[] categories = resource.getCategories(); + for (int i = 0; categories != null && i < categories.length; i++) + { + w.element(RepositoryParser.CATEGORY) + .attribute(RepositoryParser.ID, categories[i]) + .end(); + } + Capability[] capabilities = resource.getCapabilities(); + for (int i = 0; capabilities != null && i < capabilities.length; i++) + { + toXml(w, capabilities[i]); + } + Requirement[] requirements = resource.getRequirements(); + for (int i = 0; requirements != null && i < requirements.length; i++) + { + toXml(w, requirements[i]); + } + w.end(); + } + + private static String getRelativeUri(Resource resource, String name) + { + String uri = (String) resource.getProperties().get(name); + if (resource instanceof ResourceImpl) + { + try + { + uri = URI.create(((ResourceImpl) resource).getRepository().getURI()).relativize(URI.create(uri)).toASCIIString(); + } + catch (Throwable t) + { + } + } + return uri; + } + + private static void toXml(XmlWriter w, Capability capability) throws IOException + { + w.element(RepositoryParser.CAPABILITY) + .attribute(RepositoryParser.NAME, capability.getName()); + Property[] props = capability.getProperties(); + for (int j = 0; props != null && j < props.length; j++) + { + toXml(w, props[j]); + } + w.end(); + } + + private static void toXml(XmlWriter w, Property property) throws IOException + { + w.element(RepositoryParser.P) + .attribute(RepositoryParser.N, property.getName()) + .attribute(RepositoryParser.T, property.getType()) + .attribute(RepositoryParser.V, property.getValue()) + .end(); + } + + private static void toXml(XmlWriter w, Requirement requirement) throws IOException + { + w.element(RepositoryParser.REQUIRE) + .attribute(RepositoryParser.NAME, requirement.getName()) + .attribute(RepositoryParser.FILTER, requirement.getFilter()) + .attribute(RepositoryParser.EXTEND, Boolean.toString(requirement.isExtend())) + .attribute(RepositoryParser.MULTIPLE, Boolean.toString(requirement.isMultiple())) + .attribute(RepositoryParser.OPTIONAL, Boolean.toString(requirement.isOptional())) + .text(requirement.getComment().trim()) + .end(); + } + + public Resource createResource(final Bundle bundle) + { + final Dictionary dict = bundle.getHeaders(); + return createResource(new Headers() + { + public String getHeader(String name) + { + return (String) dict.get(name); + } + }); + } + + public Resource createResource(final URL bundleUrl) throws IOException + { + ResourceImpl resource = createResource(new Headers() + { + private final Manifest manifest; + private Properties localization; + { + // Do not use a JarInputStream so that we can read the manifest even if it's not + // the first entry in the JAR. + byte[] man = loadEntry(JarFile.MANIFEST_NAME); + if (man == null) + { + throw new IllegalArgumentException("The specified url is not a valid jar (can't read manifest): " + bundleUrl); + } + manifest = new Manifest(new ByteArrayInputStream(man)); + } + public String getHeader(String name) + { + String value = manifest.getMainAttributes().getValue(name); + if (value != null && value.startsWith("%")) + { + if (localization == null) + { + try + { + localization = new Properties(); + String path = manifest.getMainAttributes().getValue(Constants.BUNDLE_LOCALIZATION); + if (path == null) + { + path = Constants.BUNDLE_LOCALIZATION_DEFAULT_BASENAME; + } + path += ".properties"; + byte[] loc = loadEntry(path); + if (loc != null) + { + localization.load(new ByteArrayInputStream(loc)); + } + } + catch (IOException e) + { + // TODO: ? + } + } + value = value.substring(1); + value = localization.getProperty(value, value); + } + return value; + } + private byte[] loadEntry(String name) throws IOException + { + ZipInputStream zis = new ZipInputStream(FileUtil.openURL(bundleUrl)); + try + { + for (ZipEntry e = zis.getNextEntry(); e != null; e = zis.getNextEntry()) + { + if (name.equalsIgnoreCase(e.getName())) + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buf = new byte[1024]; + int n; + while ((n = zis.read(buf, 0, buf.length)) > 0) + { + baos.write(buf, 0, n); + } + return baos.toByteArray(); + } + } + } + finally + { + zis.close(); + } + return null; + } + }); + if (resource != null) + { + if ("file".equals(bundleUrl.getProtocol())) + { + try { + File f = new File(bundleUrl.toURI()); + resource.put(Resource.SIZE, Long.toString(f.length()), null); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + resource.put(Resource.URI, bundleUrl.toExternalForm(), null); + } + return resource; + } + + public Resource createResource(final Attributes attributes) + { + return createResource(new Headers() + { + public String getHeader(String name) + { + return attributes.getValue(name); + } + }); + } + + public ResourceImpl createResource(Headers headers) + { + String bsn = headers.getHeader(Constants.BUNDLE_SYMBOLICNAME); + if (bsn == null) + { + return null; + } + ResourceImpl resource = new ResourceImpl(); + populate(headers, resource); + return resource; + } + + static void populate(Headers headers, ResourceImpl resource) + { + String bsn = getSymbolicName(headers); + String v = getVersion(headers); + + resource.put(Resource.ID, bsn + "/" + v); + resource.put(Resource.SYMBOLIC_NAME, bsn); + resource.put(Resource.VERSION, v); + if (headers.getHeader(Constants.BUNDLE_NAME) != null) + { + resource.put(Resource.PRESENTATION_NAME, headers.getHeader(Constants.BUNDLE_NAME)); + } + if (headers.getHeader(Constants.BUNDLE_DESCRIPTION) != null) + { + resource.put(Resource.DESCRIPTION, headers.getHeader(Constants.BUNDLE_DESCRIPTION)); + } + if (headers.getHeader(BUNDLE_LICENSE) != null) + { + resource.put(Resource.LICENSE_URI, headers.getHeader(BUNDLE_LICENSE)); + } + if (headers.getHeader(Constants.BUNDLE_COPYRIGHT) != null) + { + resource.put(Resource.COPYRIGHT, headers.getHeader(Constants.BUNDLE_COPYRIGHT)); + } + if (headers.getHeader(Constants.BUNDLE_DOCURL) != null) + { + resource.put(Resource.DOCUMENTATION_URI, headers.getHeader(Constants.BUNDLE_DOCURL)); + } + if (headers.getHeader(BUNDLE_SOURCE) != null) + { + resource.put(Resource.SOURCE_URI, headers.getHeader(BUNDLE_SOURCE)); + } + + doCategories(resource, headers); + doBundle(resource, headers); + doImportExportServices(resource, headers); + doFragment(resource, headers); + doRequires(resource, headers); + doExports(resource, headers); + doImports(resource, headers); + doExecutionEnvironment(resource, headers); + doProvides(resource, headers); + } + + private static void doCategories(ResourceImpl resource, Headers headers) + { + Clause[] clauses = Parser.parseHeader(headers.getHeader(Constants.BUNDLE_CATEGORY)); + for (int i = 0; clauses != null && i < clauses.length; i++) + { + resource.addCategory(clauses[i].getName()); + } + } + + private static void doImportExportServices(ResourceImpl resource, Headers headers) + { + Clause[] imports = Parser.parseHeader(headers.getHeader(Constants.IMPORT_SERVICE)); + for (int i = 0; imports != null && i < imports.length; i++) { + RequirementImpl ri = new RequirementImpl(Capability.SERVICE); + ri.setFilter(createServiceFilter(imports[i])); + ri.addText("Import Service " + imports[i].getName()); + + String avail = imports[i].getDirective("availability"); + String mult = imports[i].getDirective("multiple"); + ri.setOptional("optional".equalsIgnoreCase(avail)); + ri.setMultiple(!"false".equalsIgnoreCase(mult)); + resource.addRequire(ri); + } + + Clause[] exports = Parser.parseHeader(headers.getHeader(Constants.EXPORT_SERVICE)); + for (int i = 0; exports != null && i < exports.length; i++) { + CapabilityImpl cap = createServiceCapability(exports[i]); + resource.addCapability(cap); + } + } + + private static String createServiceFilter(Clause clause) { + String f = clause.getAttribute("filter"); + StringBuffer filter = new StringBuffer(); + if (f != null) { + filter.append("(&"); + } + filter.append("("); + filter.append(Capability.SERVICE); + filter.append("="); + filter.append(clause.getName()); + filter.append(")"); + if (f != null) { + if (!f.startsWith("(")) + { + filter.append("(").append(f).append(")"); + } + else + { + filter.append(f); + } + filter.append(")"); + } + return filter.toString(); + } + + private static CapabilityImpl createServiceCapability(Clause clause) { + CapabilityImpl capability = new CapabilityImpl(Capability.SERVICE); + capability.addProperty(Capability.SERVICE, clause.getName()); + Attribute[] attributes = clause.getAttributes(); + for (int i = 0; attributes != null && i < attributes.length; i++) + { + capability.addProperty(attributes[i].getName(), attributes[i].getValue()); + } + return capability; + } + + private static void doFragment(ResourceImpl resource, Headers headers) + { + // Check if we are a fragment + Clause[] clauses = Parser.parseHeader(headers.getHeader(Constants.FRAGMENT_HOST)); + if (clauses != null && clauses.length == 1) + { + // We are a fragment, create a requirement + // to our host. + RequirementImpl r = new RequirementImpl(Capability.BUNDLE); + StringBuffer sb = new StringBuffer(); + sb.append("(&(symbolicname="); + sb.append(clauses[0].getName()); + sb.append(")"); + appendVersion(sb, VersionRange.parseVersionRange(clauses[0].getAttribute(Constants.BUNDLE_VERSION_ATTRIBUTE))); + sb.append(")"); + r.setFilter(sb.toString()); + r.addText("Required Host " + clauses[0].getName()); + r.setExtend(true); + r.setOptional(false); + r.setMultiple(false); + resource.addRequire(r); + + // And insert a capability that we are available + // as a fragment. ### Do we need that with extend? + CapabilityImpl capability = new CapabilityImpl(Capability.FRAGMENT); + capability.addProperty("host", clauses[0].getName()); + capability.addProperty("version", Property.VERSION, getVersion(clauses[0])); + resource.addCapability(capability); + } + } + + private static void doRequires(ResourceImpl resource, Headers headers) + { + Clause[] clauses = Parser.parseHeader(headers.getHeader(Constants.REQUIRE_BUNDLE)); + for (int i = 0; clauses != null && i < clauses.length; i++) { + RequirementImpl r = new RequirementImpl(Capability.BUNDLE); + + VersionRange v = VersionRange.parseVersionRange(clauses[i].getAttribute(Constants.BUNDLE_VERSION_ATTRIBUTE)); + + StringBuffer sb = new StringBuffer(); + sb.append("(&(symbolicname="); + sb.append(clauses[i].getName()); + sb.append(")"); + appendVersion(sb, v); + sb.append(")"); + r.setFilter(sb.toString()); + + r.addText("Require Bundle " + clauses[i].getName() + "; " + v); + r.setOptional(Constants.RESOLUTION_OPTIONAL.equalsIgnoreCase(clauses[i].getDirective(Constants.RESOLUTION_DIRECTIVE))); + resource.addRequire(r); + } + } + + private static void doBundle(ResourceImpl resource, Headers headers) { + CapabilityImpl capability = new CapabilityImpl(Capability.BUNDLE); + capability.addProperty(Resource.SYMBOLIC_NAME, getSymbolicName(headers)); + if (headers.getHeader(Constants.BUNDLE_NAME) != null) + { + capability.addProperty(Resource.PRESENTATION_NAME, headers.getHeader(Constants.BUNDLE_NAME)); + } + capability.addProperty(Resource.VERSION, Property.VERSION, getVersion(headers)); + capability.addProperty(Resource.MANIFEST_VERSION, getManifestVersion(headers)); + resource.addCapability(capability); + } + + private static void doExports(ResourceImpl resource, Headers headers) + { + Clause[] clauses = Parser.parseHeader(headers.getHeader(Constants.EXPORT_PACKAGE)); + for (int i = 0; clauses != null && i < clauses.length; i++) + { + CapabilityImpl capability = createCapability(Capability.PACKAGE, clauses[i]); + resource.addCapability(capability); + } + } + + private static void doProvides(ResourceImpl resource, Headers headers) { + Clause[] clauses = Parser.parseHeader(headers.getHeader(Constants.PROVIDE_CAPABILITY)); + + if (clauses != null) { + for (Clause clause : clauses) { + CapabilityImpl capability = createCapability(clause.getName(), clause); + resource.addCapability(capability); + } + } + } + + private static CapabilityImpl createCapability(String name, Clause clause) + { + CapabilityImpl capability = new CapabilityImpl(NamespaceTranslator.getFelixNamespace(name)); + capability.addProperty(name, clause.getName()); + capability.addProperty(Resource.VERSION, Property.VERSION, getVersion(clause)); + Attribute[] attributes = clause.getAttributes(); + for (int i = 0; attributes != null && i < attributes.length; i++) + { + String key = attributes[i].getName(); + if (key.equalsIgnoreCase(Constants.PACKAGE_SPECIFICATION_VERSION) || key.equalsIgnoreCase(Constants.VERSION_ATTRIBUTE) || key.equalsIgnoreCase("version:Version")) + { + continue; + } + else + { + String value = attributes[i].getValue(); + capability.addProperty(key, value); + } + } + Directive[] directives = clause.getDirectives(); + for (int i = 0; directives != null && i < directives.length; i++) + { + String key = directives[i].getName(); + String value = directives[i].getValue(); + capability.addProperty(key + ":", value); + } + return capability; + } + + private static void doImports(ResourceImpl resource, Headers headers) + { + Clause[] clauses = Parser.parseHeader(headers.getHeader(Constants.IMPORT_PACKAGE)); + for (int i = 0; clauses != null && i < clauses.length; i++) + { + RequirementImpl requirement = new RequirementImpl(Capability.PACKAGE); + + createImportFilter(requirement, Capability.PACKAGE, clauses[i]); + requirement.addText("Import package " + clauses[i]); + requirement.setOptional(Constants.RESOLUTION_OPTIONAL.equalsIgnoreCase(clauses[i].getDirective(Constants.RESOLUTION_DIRECTIVE))); + resource.addRequire(requirement); + } + } + + private static void createImportFilter(RequirementImpl requirement, String name, Clause clause) + { + StringBuffer filter = new StringBuffer(); + filter.append("(&("); + filter.append(name); + filter.append("="); + filter.append(clause.getName()); + filter.append(")"); + appendVersion(filter, getVersionRange(clause)); + Attribute[] attributes = clause.getAttributes(); + Set attrs = doImportPackageAttributes(requirement, filter, attributes); + + // The next code is using the subset operator + // to check mandatory attributes, it seems to be + // impossible to rewrite. It must assert that whateber + // is in mandatory: must be in any of the attributes. + // This is a fundamental shortcoming of the filter language. + if (attrs.size() > 0) + { + String del = ""; + filter.append("(mandatory:<*"); + for (Iterator i = attrs.iterator(); i.hasNext();) + { + filter.append(del); + filter.append(i.next()); + del = ", "; + } + filter.append(")"); + } + filter.append(")"); + requirement.setFilter(filter.toString()); + } + + private static Set doImportPackageAttributes(RequirementImpl requirement, StringBuffer filter, Attribute[] attributes) + { + HashSet set = new HashSet(); + for (int i = 0; attributes != null && i < attributes.length; i++) + { + String name = attributes[i].getName(); + String value = attributes[i].getValue(); + if (name.equalsIgnoreCase(Constants.PACKAGE_SPECIFICATION_VERSION) || name.equalsIgnoreCase(Constants.VERSION_ATTRIBUTE)) + { + continue; + } + else if (name.equalsIgnoreCase(Constants.RESOLUTION_DIRECTIVE + ":")) + { + requirement.setOptional(Constants.RESOLUTION_OPTIONAL.equalsIgnoreCase(value)); + } + if (name.endsWith(":")) + { + // Ignore + } + else + { + filter.append("("); + filter.append(name); + filter.append("="); + filter.append(value); + filter.append(")"); + set.add(name); + } + } + return set; + } + + private static void doExecutionEnvironment(ResourceImpl resource, Headers headers) + { + Clause[] clauses = Parser.parseHeader(headers.getHeader(Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT)); + if (clauses != null && clauses.length > 0) + { + StringBuffer sb = new StringBuffer(); + sb.append("(|"); + for (int i = 0; i < clauses.length; i++) + { + sb.append("("); + sb.append(Capability.EXECUTIONENVIRONMENT); + sb.append("="); + sb.append(clauses[i].getName()); + sb.append(")"); + } + sb.append(")"); + RequirementImpl req = new RequirementImpl(Capability.EXECUTIONENVIRONMENT); + req.setFilter(sb.toString()); + req.addText("Execution Environment " + sb.toString()); + resource.addRequire(req); + } + } + + private static String getVersion(Clause clause) + { + String v = clause.getAttribute(Constants.VERSION_ATTRIBUTE); + if (v == null) + { + v = clause.getAttribute(Constants.PACKAGE_SPECIFICATION_VERSION); + } + if (v == null) + { + v = clause.getAttribute(Constants.BUNDLE_VERSION_ATTRIBUTE); + } + if (v == null) + { + v = clause.getAttribute("version:Version"); + } + + return VersionCleaner.clean(v); + } + + private static VersionRange getVersionRange(Clause clause) + { + String v = clause.getAttribute(Constants.VERSION_ATTRIBUTE); + if (v == null) + { + v = clause.getAttribute(Constants.PACKAGE_SPECIFICATION_VERSION); + } + if (v == null) + { + v = clause.getAttribute(Constants.BUNDLE_VERSION_ATTRIBUTE); + } + return VersionRange.parseVersionRange(v); + } + + private static String getSymbolicName(Headers headers) + { + String bsn = headers.getHeader(Constants.BUNDLE_SYMBOLICNAME); + if (bsn == null) + { + bsn = headers.getHeader(Constants.BUNDLE_NAME); + if (bsn == null) + { + bsn = "Untitled-" + headers.hashCode(); + } + } + Clause[] clauses = Parser.parseHeader(bsn); + return clauses[0].getName(); + } + + private static String getVersion(Headers headers) + { + String v = headers.getHeader(Constants.BUNDLE_VERSION); + return VersionCleaner.clean(v); + } + + private static String getManifestVersion(Headers headers) + { + String v = headers.getHeader(Constants.BUNDLE_MANIFESTVERSION); + if (v == null) + { + v = "1"; + } + return v; + } + + private static void appendVersion(StringBuffer filter, VersionRange version) + { + if (version != null) + { + if ( !version.isOpenFloor() ) + { + if ( !Version.emptyVersion.equals(version.getFloor()) ) + { + filter.append("("); + filter.append(Constants.VERSION_ATTRIBUTE); + filter.append(">="); + filter.append(version.getFloor()); + filter.append(")"); + } + } + else + { + filter.append("(!("); + filter.append(Constants.VERSION_ATTRIBUTE); + filter.append("<="); + filter.append(version.getFloor()); + filter.append("))"); + } + + if (!VersionRange.INFINITE_VERSION.equals(version.getCeiling())) + { + if ( !version.isOpenCeiling() ) + { + filter.append("("); + filter.append(Constants.VERSION_ATTRIBUTE); + filter.append("<="); + filter.append(version.getCeiling()); + filter.append(")"); + } + else + { + filter.append("(!("); + filter.append(Constants.VERSION_ATTRIBUTE); + filter.append(">="); + filter.append(version.getCeiling()); + filter.append("))"); + } + } + } + } + + interface Headers + { + String getHeader(String name); + } + + public Repository readRepository(String xml) throws Exception + { + try + { + return readRepository(new StringReader(xml)); + } + catch (IOException e) + { + IllegalStateException ex = new IllegalStateException(e); + ex.initCause(e); + throw ex; + } + } + + public Repository readRepository(Reader reader) throws Exception + { + return RepositoryParser.getParser().parseRepository(reader); + } + + public Resource readResource(String xml) throws Exception + { + try + { + return readResource(new StringReader(xml)); + } + catch (IOException e) + { + IllegalStateException ex = new IllegalStateException(e); + ex.initCause(e); + throw ex; + } + } + + public Resource readResource(Reader reader) throws Exception + { + return RepositoryParser.getParser().parseResource(reader); + } + + public Capability readCapability(String xml) throws Exception + { + try + { + return readCapability(new StringReader(xml)); + } + catch (IOException e) + { + IllegalStateException ex = new IllegalStateException(e); + ex.initCause(e); + throw ex; + } + } + + public Capability readCapability(Reader reader) throws Exception + { + return RepositoryParser.getParser().parseCapability(reader); + } + + public Requirement readRequirement(String xml) throws Exception + { + try + { + return readRequirement(new StringReader(xml)); + } + catch (IOException e) + { + IllegalStateException ex = new IllegalStateException(e); + ex.initCause(e); + throw ex; + } + } + + public Requirement readRequirement(Reader reader) throws Exception + { + return RepositoryParser.getParser().parseRequirement(reader); + } + + public Property readProperty(String xml) throws Exception + { + try + { + return readProperty(new StringReader(xml)); + } + catch (IOException e) + { + IllegalStateException ex = new IllegalStateException(e); + ex.initCause(e); + throw ex; + } + } + + public Property readProperty(Reader reader) throws Exception + { + return RepositoryParser.getParser().parseProperty(reader); + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/FelixCapabilityAdapter.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/FelixCapabilityAdapter.java new file mode 100644 index 00000000000..8d93af42e1c --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/FelixCapabilityAdapter.java @@ -0,0 +1,134 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.bundlerepository.impl; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.osgi.framework.namespace.BundleNamespace; +import org.osgi.framework.namespace.PackageNamespace; +import org.osgi.resource.Capability; +import org.osgi.resource.Resource; + +public class FelixCapabilityAdapter implements Capability +{ + private final org.apache.felix.bundlerepository.Capability capability; + private final Resource resource; + private volatile Map convertedAttributes; + + public FelixCapabilityAdapter(org.apache.felix.bundlerepository.Capability capability, Resource resource) + { + if (capability == null) + throw new NullPointerException("Missing required parameter: capability"); + this.capability = capability; + this.resource = resource; + } + + public Map getAttributes() + { + if (convertedAttributes == null) + { + Map orgMap = capability.getPropertiesAsMap(); + HashMap converted = new HashMap(orgMap.size() + 2); + + for (Map.Entry entry : orgMap.entrySet()) + { + converted.put(NamespaceTranslator.getOSGiNamespace(entry.getKey()), entry.getValue()); + } + + if (BundleNamespace.BUNDLE_NAMESPACE.equals(getNamespace())) + { + defaultAttribute(orgMap, converted, BundleNamespace.BUNDLE_NAMESPACE, + orgMap.get(org.apache.felix.bundlerepository.Resource.SYMBOLIC_NAME)); + defaultAttribute(orgMap, converted, BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE, + orgMap.get(org.apache.felix.bundlerepository.Resource.VERSION)); + } + else if (PackageNamespace.PACKAGE_NAMESPACE.equals(getNamespace())) + { + Capability bundleCap = getBundleCapability(); + if (bundleCap != null) + { + defaultAttribute(orgMap, converted, PackageNamespace.CAPABILITY_BUNDLE_SYMBOLICNAME_ATTRIBUTE, + bundleCap.getAttributes().get(BundleNamespace.BUNDLE_NAMESPACE)); + defaultAttribute(orgMap, converted, PackageNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE, + bundleCap.getAttributes().get(BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE)); + } + } + convertedAttributes = converted; + } + return convertedAttributes; + } + + private void defaultAttribute(Map orgMap, Map converted, String newAttr, Object defVal) + { + if (converted.get(newAttr) == null) + converted.put(newAttr, defVal); + } + + public Map getDirectives() + { + return capability.getDirectives(); + } + + public String getNamespace() + { + return NamespaceTranslator.getOSGiNamespace(capability.getName()); + } + + public Resource getResource() + { + return resource; + } + + private Capability getBundleCapability() + { + if (resource == null) + return null; + + List caps = resource.getCapabilities(BundleNamespace.BUNDLE_NAMESPACE); + if (caps.size() > 0) + return caps.get(0); + else + return null; + } + + @Override + public boolean equals(Object o) + { + if (o == this) + return true; + if (!(o instanceof Capability)) + return false; + Capability c = (Capability) o; + return c.getNamespace().equals(getNamespace()) && c.getAttributes().equals(getAttributes()) + && c.getDirectives().equals(getDirectives()) && c.getResource().equals(getResource()); + } + + @Override + public int hashCode() + { + int result = 17; + result = 31 * result + getNamespace().hashCode(); + result = 31 * result + getAttributes().hashCode(); + result = 31 * result + getDirectives().hashCode(); + result = 31 * result + getResource().hashCode(); + return result; + } + + public String toString() + { + return resource + ":" + capability; + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/FelixPropertyAdapter.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/FelixPropertyAdapter.java new file mode 100644 index 00000000000..5080ff00f87 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/FelixPropertyAdapter.java @@ -0,0 +1,69 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.bundlerepository.impl; + +import java.util.List; +import java.util.Map; + +import org.apache.felix.bundlerepository.Property; +import org.osgi.framework.Version; + +class FelixPropertyAdapter implements Property +{ + private final String name; + private final Object value; + + public FelixPropertyAdapter(String name, Object value) + { + if (name == null) + throw new NullPointerException("Missing required parameter: name"); + if (value == null) + throw new NullPointerException("Missing required parameter: value"); + this.name = name; + this.value = value; + } + + public FelixPropertyAdapter(Map.Entry entry) + { + this(entry.getKey(), entry.getValue()); + } + + public Object getConvertedValue() + { + return value; + } + + public String getName() + { + return name; + } + + public String getType() + { + if (value instanceof Version) + return Property.VERSION; + if (value instanceof Long) + return Property.LONG; + if (value instanceof Double) + return Property.DOUBLE; + if (value instanceof List) + return Property.SET; + return null; + } + + public String getValue() + { + return String.valueOf(value); + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/FelixRequirementAdapter.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/FelixRequirementAdapter.java new file mode 100644 index 00000000000..dcf74aad00a --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/FelixRequirementAdapter.java @@ -0,0 +1,129 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.bundlerepository.impl; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.osgi.resource.Capability; +import org.osgi.resource.Namespace; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; + +public class FelixRequirementAdapter implements Requirement +{ + private final Map directives; + private final org.apache.felix.bundlerepository.Requirement requirement; + private final Resource resource; + + public FelixRequirementAdapter(org.apache.felix.bundlerepository.Requirement requirement, Resource resource) + { + if (requirement == null) + throw new NullPointerException("Missing required parameter: requirement"); + if (resource == null) + throw new NullPointerException("Missing required parameter: resource"); + this.requirement = requirement; + this.resource = resource; + this.directives = computeDirectives(); + } + + public Map getAttributes() + { + return requirement.getAttributes(); + } + + public Map getDirectives() + { + return directives; + } + + public String getNamespace() + { + return NamespaceTranslator.getOSGiNamespace(requirement.getName()); + } + + public Resource getResource() + { + return resource; + } + + public boolean matches(Capability capability) + { + return requirement.isSatisfied(new OSGiCapabilityAdapter(capability)); + } + + private Map computeDirectives() + { + Map result; + if (requirement.getDirectives() == null) + result = new HashMap(); + else + result = new HashMap(requirement.getDirectives()); + + /* + * (1) The Felix OBR specific "mandatory:<*" syntax must be stripped out + * of the filter. + * (2) service references removed + * (3) objectClass capitalised + * (4) The namespaces must be translated. + */ + String filter = requirement.getFilter().replaceAll("\\(mandatory\\:\\<\\*[^\\)]*\\)", ""). + replaceAll("objectclass", "objectClass"); + + for (String ns : NamespaceTranslator.getTranslatedFelixNamespaces()) + { + filter = filter.replaceAll("[(][ ]*" + ns + "[ ]*=", + "(" + NamespaceTranslator.getOSGiNamespace(ns) + "="); + } + result.put(Namespace.REQUIREMENT_FILTER_DIRECTIVE, filter); + + if (requirement.isOptional()) + result.put(Namespace.REQUIREMENT_RESOLUTION_DIRECTIVE, Namespace.RESOLUTION_OPTIONAL); + + if (requirement.isMultiple()) + result.put(Namespace.REQUIREMENT_CARDINALITY_DIRECTIVE, Namespace.CARDINALITY_MULTIPLE); + + return Collections.unmodifiableMap(result); + } + + @Override + public boolean equals(Object o) + { + if (o == this) + return true; + if (!(o instanceof Requirement)) + return false; + Requirement c = (Requirement) o; + return c.getNamespace().equals(getNamespace()) && c.getAttributes().equals(getAttributes()) + && c.getDirectives().equals(getDirectives()) && c.getResource() != null ? c.getResource().equals(getResource()) + : getResource() == null; + } + + @Override + public int hashCode() + { + int result = 17; + result = 31 * result + getNamespace().hashCode(); + result = 31 * result + getAttributes().hashCode(); + result = 31 * result + getDirectives().hashCode(); + result = 31 * result + (getResource() == null ? 0 : getResource().hashCode()); + return result; + } + + public String toString() + { + return resource + ":" + requirement; + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/FelixResourceAdapter.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/FelixResourceAdapter.java new file mode 100644 index 00000000000..6047c67bd4b --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/FelixResourceAdapter.java @@ -0,0 +1,128 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.bundlerepository.impl; + +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.felix.utils.resource.CapabilityImpl; +import org.osgi.framework.namespace.IdentityNamespace; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; +import org.osgi.service.repository.ContentNamespace; +import org.osgi.service.repository.RepositoryContent; + +public class FelixResourceAdapter implements Resource, RepositoryContent +{ + private final org.apache.felix.bundlerepository.Resource resource; + + public FelixResourceAdapter(final org.apache.felix.bundlerepository.Resource resource) + { + this.resource = resource; + } + + public List getCapabilities(String namespace) + { + ArrayList result = new ArrayList(); + + if (namespace == null || namespace.equals(IdentityNamespace.IDENTITY_NAMESPACE)) + { + CapabilityImpl c = OSGiRepositoryImpl.newOSGiIdentityCapability(this, resource); + result.add(c); + } + if (namespace == null || namespace.equals(ContentNamespace.CONTENT_NAMESPACE)) + { + CapabilityImpl c = OSGiRepositoryImpl.newOSGiContentCapability(this, resource); + result.add(c); + } + + namespace = NamespaceTranslator.getFelixNamespace(namespace); + org.apache.felix.bundlerepository.Capability[] capabilities = resource.getCapabilities(); + for (org.apache.felix.bundlerepository.Capability capability : capabilities) + { + if (namespace != null && !capability.getName().equals(namespace)) + continue; + result.add(new FelixCapabilityAdapter(capability, this)); + } + result.trimToSize(); + return result; + } + + public InputStream getContent() + { + try + { + return new URL(resource.getURI()).openStream(); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + public List getRequirements(String namespace) + { + namespace = NamespaceTranslator.getFelixNamespace(namespace); + org.apache.felix.bundlerepository.Requirement[] requirements = resource.getRequirements(); + ArrayList result = new ArrayList(requirements.length); + for (final org.apache.felix.bundlerepository.Requirement requirement : requirements) + { + if (namespace == null || requirement.getName().equals(namespace)) + result.add(new FelixRequirementAdapter(requirement, this)); + } + result.trimToSize(); + return result; + } + + @Override + public boolean equals(Object o) + { + if (o == this) + return true; + if (!(o instanceof Resource)) + return false; + Resource that = (Resource) o; + if (!OSGiResourceHelper.getTypeAttribute(that).equals(OSGiResourceHelper.getTypeAttribute(this))) + return false; + if (!OSGiResourceHelper.getSymbolicNameAttribute(that).equals(OSGiResourceHelper.getSymbolicNameAttribute(this))) + return false; + if (!OSGiResourceHelper.getVersionAttribute(that).equals(OSGiResourceHelper.getVersionAttribute(this))) + return false; + return true; + } + + @Override + public int hashCode() + { + int result = 17; + result = 31 * result + OSGiResourceHelper.getTypeAttribute(this).hashCode(); + result = 31 * result + OSGiResourceHelper.getSymbolicNameAttribute(this).hashCode(); + result = 31 * result + OSGiResourceHelper.getVersionAttribute(this).hashCode(); + return result; + } + + @Override + public String toString() + { + Capability c = getCapabilities(IdentityNamespace.IDENTITY_NAMESPACE).iterator().next(); + Map atts = c.getAttributes(); + return new StringBuilder().append(atts.get(IdentityNamespace.IDENTITY_NAMESPACE)).append(';') + .append(atts.get(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE)).append(';') + .append(atts.get(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE)).toString(); + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/FileUtil.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/FileUtil.java new file mode 100644 index 00000000000..9a27e3ff49b --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/FileUtil.java @@ -0,0 +1,222 @@ +/* + * 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.bundlerepository.impl; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; + +public class FileUtil +{ + public static void downloadSource( + PrintStream out, PrintStream err, + URL srcURL, String dirStr, boolean extract) + { + // Get the file name from the URL. + String fileName = (srcURL.getFile().lastIndexOf('/') > 0) + ? srcURL.getFile().substring(srcURL.getFile().lastIndexOf('/') + 1) + : srcURL.getFile(); + + try + { + out.println("Connecting..."); + + File dir = new File(dirStr); + if (!dir.exists()) + { + err.println("Destination directory does not exist."); + } + File file = new File(dir, fileName); + + OutputStream os = new FileOutputStream(file); + URLConnection conn = srcURL.openConnection(); + FileUtil.setProxyAuth(conn); + int total = conn.getContentLength(); + InputStream is = conn.getInputStream(); + + if (total > 0) + { + out.println("Downloading " + fileName + + " ( " + total + " bytes )."); + } + else + { + out.println("Downloading " + fileName + "."); + } + byte[] buffer = new byte[4096]; + int count = 0; + for (int len = is.read(buffer); len > 0; len = is.read(buffer)) + { + count += len; + os.write(buffer, 0, len); + } + + os.close(); + is.close(); + + if (extract) + { + is = new FileInputStream(file); + JarInputStream jis = new JarInputStream(is); + out.println("Extracting..."); + unjar(jis, dir); + jis.close(); + file.delete(); + } + } + catch (Exception ex) + { + err.println(ex); + } + } + + public static void unjar(JarInputStream jis, File dir) + throws IOException + { + // Reusable buffer. + byte[] buffer = new byte[4096]; + + // Loop through JAR entries. + for (JarEntry je = jis.getNextJarEntry(); + je != null; + je = jis.getNextJarEntry()) + { + if (je.getName().startsWith("/")) + { + throw new IOException("JAR resource cannot contain absolute paths."); + } + + File target = new File(dir, je.getName()); + + // Check to see if the JAR entry is a directory. + if (je.isDirectory()) + { + if (!target.exists()) + { + if (!target.mkdirs()) + { + throw new IOException("Unable to create target directory: " + + target); + } + } + // Just continue since directories do not have content to copy. + continue; + } + + int lastIndex = je.getName().lastIndexOf('/'); + String name = (lastIndex >= 0) ? + je.getName().substring(lastIndex + 1) : je.getName(); + String destination = (lastIndex >= 0) ? + je.getName().substring(0, lastIndex) : ""; + + // JAR files use '/', so convert it to platform separator. + destination = destination.replace('/', File.separatorChar); + copy(jis, dir, name, destination, buffer); + } + } + + public static void copy( + InputStream is, File dir, String destName, String destDir, byte[] buffer) + throws IOException + { + if (destDir == null) + { + destDir = ""; + } + + // Make sure the target directory exists and + // that is actually a directory. + File targetDir = new File(dir, destDir); + if (!targetDir.exists()) + { + if (!targetDir.mkdirs()) + { + throw new IOException("Unable to create target directory: " + + targetDir); + } + } + else if (!targetDir.isDirectory()) + { + throw new IOException("Target is not a directory: " + + targetDir); + } + + BufferedOutputStream bos = new BufferedOutputStream( + new FileOutputStream(new File(targetDir, destName))); + int count = 0; + while ((count = is.read(buffer)) > 0) + { + bos.write(buffer, 0, count); + } + bos.close(); + } + + public static void setProxyAuth(URLConnection conn) throws IOException + { + // Support for http proxy authentication + String auth = System.getProperty("http.proxyAuth"); + if ((auth != null) && (auth.length() > 0)) + { + if ("http".equals(conn.getURL().getProtocol()) + || "https".equals(conn.getURL().getProtocol())) + { + String base64 = Base64Encoder.base64Encode(auth); + conn.setRequestProperty("Proxy-Authorization", "Basic " + base64); + } + } + + } + + public static InputStream openURL(final URL url) throws IOException + { + // Do it the manual way to have a chance to + // set request properties as proxy auth (EW). + return openURL(url.openConnection()); + } + + public static InputStream openURL(final URLConnection conn) throws IOException + { + // Do it the manual way to have a chance to + // set request properties as proxy auth (EW). + setProxyAuth(conn); + try + { + return conn.getInputStream(); + } + catch (IOException e) + { + // Rather than just throwing the original exception, we wrap it + // because in some cases the original exception doesn't include + // the full URL (see FELIX-2912). + URL url = conn.getURL(); + IOException newException = new IOException("Error accessing " + url); + newException.initCause(e); + throw newException; + } + } +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/LazyLocalResourceImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/LazyLocalResourceImpl.java new file mode 100644 index 00000000000..12fca9a8589 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/LazyLocalResourceImpl.java @@ -0,0 +1,114 @@ +/* + * 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.bundlerepository.impl; + +import org.apache.felix.bundlerepository.Capability; +import org.apache.felix.bundlerepository.LocalResource; +import org.apache.felix.bundlerepository.Requirement; +import org.apache.felix.bundlerepository.Resource; +import org.apache.felix.utils.log.Logger; +import org.osgi.framework.Bundle; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.Version; + +import java.util.Map; + +public class LazyLocalResourceImpl implements LocalResource +{ + private final Bundle m_bundle; + private final Logger m_logger; + private volatile Resource m_resource = null; + + LazyLocalResourceImpl(Bundle bundle, Logger logger) + { + m_bundle = bundle; + m_logger = logger; + } + + public boolean isLocal() + { + return true; + } + + public Bundle getBundle() + { + return m_bundle; + } + + public String toString() + { + return m_bundle.toString(); + } + + private final Resource getResource() { + if (m_resource == null) { + synchronized (this) { + try { + m_resource = new LocalResourceImpl(m_bundle); + } catch (InvalidSyntaxException ex) { + // This should never happen since we are generating filters, + // but ignore the resource if it does occur. + m_logger.log(Logger.LOG_WARNING, ex.getMessage(), ex); + m_resource = new ResourceImpl(); + } + } + } + return m_resource; + } + + public Map getProperties() { + return getResource().getProperties(); + } + + public String getId() { + return getResource().getId(); + } + + public String getSymbolicName() { + return getResource().getSymbolicName(); + } + + public Version getVersion() { + return getResource().getVersion(); + } + + public String getPresentationName() { + return getResource().getPresentationName(); + } + + public String getURI() { + return getResource().getURI(); + } + + public Long getSize() { + return getResource().getSize(); + } + + public String[] getCategories() { + return getResource().getCategories(); + } + + public Capability[] getCapabilities() { + return getResource().getCapabilities(); + } + + public Requirement[] getRequirements() { + return getResource().getRequirements(); + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/LazyStringMap.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/LazyStringMap.java new file mode 100644 index 00000000000..57bdd4db827 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/LazyStringMap.java @@ -0,0 +1,67 @@ +/* + * 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.bundlerepository.impl; + +import java.util.Map; + +import org.apache.felix.utils.collections.StringArrayMap; + +/** + * A map that can delay the computation of certain values up until the moment that they + * are actually needed. Useful for expensive to compute values such as the SHA-256. + * This map does not support {@code null} values. + */ +@SuppressWarnings("serial") +public class LazyStringMap extends StringArrayMap +{ + public LazyStringMap(Map map) { + super(map); + } + + public LazyStringMap() { + } + + public LazyStringMap(int capacity) { + super(capacity); + } + + @Override + @SuppressWarnings("unchecked") + public V get(Object key) + { + V val = super.get(key); + if (val instanceof LazyValue) { + val = ((LazyValue) val).compute(); + if (val == null) { + throw new NullPointerException("Lazy computed values may not be null"); + } + put((String) key, val); + } + return val; + } + + public void putLazy(String key, LazyValue lazy) { + super.doPut(key, lazy); + } + + public interface LazyValue + { + V compute(); + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/LocalRepositoryImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/LocalRepositoryImpl.java new file mode 100644 index 00000000000..862ffb08c2c --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/LocalRepositoryImpl.java @@ -0,0 +1,158 @@ +/* + * 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.bundlerepository.impl; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.utils.log.Logger; +import org.osgi.framework.AllServiceListener; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.SynchronousBundleListener; +import org.apache.felix.bundlerepository.*; + +public class LocalRepositoryImpl implements Repository, SynchronousBundleListener, AllServiceListener +{ + private final BundleContext m_context; + private final Logger m_logger; + private long m_snapshotTimeStamp = 0; + private Map m_localResourceList = new HashMap(); + + public LocalRepositoryImpl(BundleContext context, Logger logger) + { + m_context = context; + m_logger = logger; + initialize(); + } + + public void bundleChanged(BundleEvent event) + { + if (event.getType() == BundleEvent.INSTALLED || event.getType() == BundleEvent.UPDATED) + { + synchronized (this) + { + addBundle(event.getBundle()); + m_snapshotTimeStamp = System.currentTimeMillis(); + } + } + else if (event.getType() == BundleEvent.UNINSTALLED) + { + synchronized (this) + { + removeBundle(event.getBundle()); + m_snapshotTimeStamp = System.currentTimeMillis(); + } + } + } + + public void serviceChanged(ServiceEvent event) + { + Bundle bundle = event.getServiceReference().getBundle(); + if ((bundle != null) + && (bundle.getState() == Bundle.ACTIVE && event.getType() != ServiceEvent.MODIFIED)) + { + synchronized (this) + { + removeBundle(bundle); + addBundle(bundle); + m_snapshotTimeStamp = System.currentTimeMillis(); + } + } + } + + private void addBundle(Bundle bundle) + { + /* + * Concurrency note: This method MUST be called in a context which + * is synchronized on this instance to prevent data structure + * corruption. + */ + + // Ignore system bundle + if (bundle.getBundleId() == 0) + { + return; + } + m_localResourceList.put(bundle.getBundleId(), new LazyLocalResourceImpl(bundle, m_logger)); + } + + private void removeBundle(Bundle bundle) + { + /* + * Concurrency note: This method MUST be called in a context which + * is synchronized on this instance to prevent data structure + * corruption. + */ + + m_localResourceList.remove(bundle.getBundleId()); + } + + public void dispose() + { + m_context.removeBundleListener(this); + m_context.removeServiceListener(this); + } + + public String getURI() + { + return LOCAL; + } + + public String getName() + { + return "Locally Installed Repository"; + } + + public synchronized long getLastModified() + { + return m_snapshotTimeStamp; + } + + public synchronized Resource[] getResources() + { + return m_localResourceList.values().toArray(new Resource[m_localResourceList.size()]); + } + + private void initialize() + { + // register for bundle and service events now + m_context.addBundleListener(this); + m_context.addServiceListener(this); + + // Generate the resource list from the set of installed bundles. + // Lock so we can ensure that no bundle events arrive before we + // are done getting our state snapshot. + Bundle[] bundles; + synchronized (this) + { + // Create a local resource object for each bundle, which will + // convert the bundle headers to the appropriate resource metadata. + bundles = m_context.getBundles(); + for (int i = 0; (bundles != null) && (i < bundles.length); i++) + { + addBundle(bundles[i]); + } + + m_snapshotTimeStamp = System.currentTimeMillis(); + } + } +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/LocalResourceImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/LocalResourceImpl.java new file mode 100644 index 00000000000..b0bbb216021 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/LocalResourceImpl.java @@ -0,0 +1,136 @@ +/* + * 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.bundlerepository.impl; + +import java.util.Dictionary; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; + +import org.apache.felix.bundlerepository.Capability; +import org.apache.felix.bundlerepository.LocalResource; +import org.apache.felix.bundlerepository.Resource; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.wiring.BundleRevision; + +public class LocalResourceImpl extends ResourceImpl implements LocalResource +{ + private Bundle m_bundle = null; + + LocalResourceImpl(Bundle bundle) throws InvalidSyntaxException + { + m_bundle = bundle; + initialize(); + } + + public boolean isLocal() + { + return true; + } + + public Bundle getBundle() + { + return m_bundle; + } + + private void initialize() throws InvalidSyntaxException + { + final Dictionary dict = m_bundle.getHeaders(); + + DataModelHelperImpl.populate(new DataModelHelperImpl.Headers() + { + public String getHeader(String name) + { + return (String) dict.get(name); + } + public void close() { } + }, this); + + // Convert export service declarations and services into capabilities. + convertExportServiceToCapability(dict, m_bundle); + + // For the system bundle, add a special platform capability. + if (m_bundle.getBundleId() == 0) + { + // add the alias bundle symbolic name "system.bundle" + CapabilityImpl sysBundleCap = new CapabilityImpl(Capability.BUNDLE); + sysBundleCap.addProperty(Resource.SYMBOLIC_NAME, Constants.SYSTEM_BUNDLE_SYMBOLICNAME); + addCapability(sysBundleCap); + + // set the execution environment(s) as Capability ee of the + // system bundle to resolve bundles with specific requirements + String ee = m_bundle.getBundleContext().getProperty(Constants.FRAMEWORK_EXECUTIONENVIRONMENT); + if (ee != null) + { + StringTokenizer tokens = new StringTokenizer(ee, ","); + while (tokens.hasMoreTokens()) { + CapabilityImpl cap = new CapabilityImpl(Capability.EXECUTIONENVIRONMENT); + cap.addProperty(Capability.EXECUTIONENVIRONMENT, tokens.nextToken().trim()); + addCapability(cap); + } + } + + // Add all the OSGi capabilities from the system bundle as repo capabilities + BundleRevision br = m_bundle.adapt(BundleRevision.class); + for (org.osgi.resource.Capability cap : br.getCapabilities(null)) + { + CapabilityImpl bcap = new CapabilityImpl(cap.getNamespace()); + for (Map.Entry entry : cap.getAttributes().entrySet()) + { + bcap.addProperty(new FelixPropertyAdapter(entry)); + } + for (Map.Entry entry : cap.getDirectives().entrySet()) + { + bcap.addDirective(entry.getKey(), entry.getValue()); + } + addCapability(bcap); + } + } + } + + private void convertExportServiceToCapability(Dictionary dict, Bundle bundle) + { + Set services = new HashSet(); + + // add actual registered services + ServiceReference[] refs = bundle.getRegisteredServices(); + for (int i = 0; refs != null && i < refs.length; i++) + { + String[] cls = (String[]) refs[i].getProperty(Constants.OBJECTCLASS); + for (int j = 0; cls != null && j < cls.length; j++) + { + CapabilityImpl cap = new CapabilityImpl(); + cap.setName(Capability.SERVICE); + cap.addProperty(new PropertyImpl(Capability.SERVICE, null, cls[j])); + // TODO: add service properties + addCapability(cap); + } + } + // TODO: check duplicates with service-export properties + } + + public String toString() + { + return m_bundle.toString(); + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/NamespaceTranslator.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/NamespaceTranslator.java new file mode 100644 index 00000000000..d554878f5af --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/NamespaceTranslator.java @@ -0,0 +1,78 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.bundlerepository.impl; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.osgi.framework.namespace.BundleNamespace; +import org.osgi.framework.namespace.HostNamespace; +import org.osgi.framework.namespace.PackageNamespace; +import org.osgi.namespace.service.ServiceNamespace; + +class NamespaceTranslator +{ + private static final Map osgiToFelixMap = fillOSGiToFelixMap(); + private static final Map felixToOSGiMap = fillFelixToOSGiMap(); + + private static Map fillOSGiToFelixMap() + { + Map result = new HashMap(4); + result.put(PackageNamespace.PACKAGE_NAMESPACE, org.apache.felix.bundlerepository.Capability.PACKAGE); + result.put(ServiceNamespace.SERVICE_NAMESPACE, org.apache.felix.bundlerepository.Capability.SERVICE); + result.put(BundleNamespace.BUNDLE_NAMESPACE, org.apache.felix.bundlerepository.Capability.BUNDLE); + result.put(HostNamespace.HOST_NAMESPACE, org.apache.felix.bundlerepository.Capability.FRAGMENT); + return Collections.unmodifiableMap(result); + } + + private static Map fillFelixToOSGiMap() + { + Map result = new HashMap(4); + result.put(org.apache.felix.bundlerepository.Capability.PACKAGE, PackageNamespace.PACKAGE_NAMESPACE); + result.put(org.apache.felix.bundlerepository.Capability.SERVICE, ServiceNamespace.SERVICE_NAMESPACE); + result.put(org.apache.felix.bundlerepository.Capability.BUNDLE, BundleNamespace.BUNDLE_NAMESPACE); + result.put(org.apache.felix.bundlerepository.Capability.FRAGMENT, HostNamespace.HOST_NAMESPACE); + return Collections.unmodifiableMap(result); + } + + public static String getFelixNamespace(String osgiNamespace) + { + String result = osgiToFelixMap.get(osgiNamespace); + if (result == null) + return osgiNamespace; + else + return result; + } + + public static Collection getTranslatedFelixNamespaces() + { + return felixToOSGiMap.keySet(); + } + + public static String getOSGiNamespace(String felixNamespace) + { + String result = felixToOSGiMap.get(felixNamespace); + if (result == null) + return felixNamespace; + else + return result; + } + + public static Collection getTranslatedOSGiNamespaces() + { + return osgiToFelixMap.keySet(); + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/OSGiCapabilityAdapter.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/OSGiCapabilityAdapter.java new file mode 100644 index 00000000000..5546d777bef --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/OSGiCapabilityAdapter.java @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.bundlerepository.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.bundlerepository.Capability; +import org.apache.felix.bundlerepository.Property; + +public class OSGiCapabilityAdapter implements Capability +{ + private final org.osgi.resource.Capability capability; + + public OSGiCapabilityAdapter(org.osgi.resource.Capability capability) + { + this.capability = capability; + } + + @Override + public boolean equals(Object o) + { + return capability.equals(o); + } + + public String getName() + { + return NamespaceTranslator.getFelixNamespace(capability.getNamespace()); + } + + public Property[] getProperties() + { + Map attributes = capability.getAttributes(); + Collection result = new ArrayList(attributes.size()); + for (final Map.Entry entry : capability.getAttributes().entrySet()) + { + if (entry.getKey().equals(capability.getNamespace())) + { + result.add(new FelixPropertyAdapter(getName(), entry.getValue())); + continue; + } + result.add(new FelixPropertyAdapter(entry)); + } + return result.toArray(new Property[result.size()]); + } + + public Map getPropertiesAsMap() + { + Map result = new HashMap(capability.getAttributes()); + result.put(getName(), result.get(capability.getNamespace())); + return result; + } + + public Map getDirectives() { + return Collections.unmodifiableMap(capability.getDirectives()); + } + + @Override + public int hashCode() + { + return capability.hashCode(); + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/OSGiRepositoryImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/OSGiRepositoryImpl.java new file mode 100644 index 00000000000..ea992f9b8cb --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/OSGiRepositoryImpl.java @@ -0,0 +1,185 @@ +/* + * 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.bundlerepository.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.felix.bundlerepository.RepositoryAdmin; +import org.apache.felix.bundlerepository.Resource; +import org.apache.felix.utils.resource.CapabilityImpl; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.namespace.IdentityNamespace; +import org.osgi.resource.Capability; +import org.osgi.resource.Namespace; +import org.osgi.resource.Requirement; +import org.osgi.service.repository.ContentNamespace; +import org.osgi.service.repository.Repository; + +class OSGiRepositoryImpl implements Repository +{ + private final RepositoryAdmin repository; + + OSGiRepositoryImpl(RepositoryAdmin repository) + { + this.repository = repository; + } + + public Map> findProviders(Collection requirements) + { + Map> m = new HashMap>(); + for (Requirement r : requirements) + { + m.put(r, findProviders(r)); + } + return m; + } + + private Collection findProviders(Requirement req) + { + List caps = new ArrayList(); + if (IdentityNamespace.IDENTITY_NAMESPACE.equals(req.getNamespace())) + { + for(org.apache.felix.bundlerepository.Repository repo : repository.listRepositories()) + { + for (org.apache.felix.bundlerepository.Resource res : repo.getResources()) + { + String f = req.getDirectives().get(Namespace.REQUIREMENT_FILTER_DIRECTIVE); + try + { + addResourceForIdentity(res, + f == null ? null : FrameworkUtil.createFilter(f), caps); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + } + } + else + { + org.apache.felix.bundlerepository.Resource[] resources = repository.discoverResources( + new org.apache.felix.bundlerepository.Requirement[] {new OSGiRequirementAdapter(req)}); + OSGiRequirementAdapter adapter = new OSGiRequirementAdapter(req); + for (org.apache.felix.bundlerepository.Resource resource : resources) + { + for (org.apache.felix.bundlerepository.Capability cap : resource.getCapabilities()) + { + if (adapter.isSatisfied(cap)) + caps.add(new FelixCapabilityAdapter(cap, new FelixResourceAdapter(resource))); + } + } + } + + return caps; + } + + private void addResourceForIdentity(final org.apache.felix.bundlerepository.Resource res, Filter filter, List caps) + throws Exception + { + List idCaps = new FelixResourceAdapter(res).getCapabilities(IdentityNamespace.IDENTITY_NAMESPACE); + if (idCaps.size() == 0) + return; + + Capability idCap = idCaps.get(0); // there should only be one osgi.identity anyway + if (filter != null) + { + if (!filter.matches(idCap.getAttributes())) + return; + } + caps.add(idCap); + } + + static CapabilityImpl newOSGiIdentityCapability(org.osgi.resource.Resource or, org.apache.felix.bundlerepository.Resource res) + { + @SuppressWarnings("unchecked") + Map idAttrs = new HashMap(res.getProperties()); + + // Set a number of specific properties that need to be translated + idAttrs.put(IdentityNamespace.IDENTITY_NAMESPACE, res.getSymbolicName()); + + if (idAttrs.get(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE) == null) + idAttrs.put(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE, IdentityNamespace.TYPE_BUNDLE); + + return new CapabilityImpl(or, IdentityNamespace.IDENTITY_NAMESPACE, Collections. emptyMap(), idAttrs); + } + + static CapabilityImpl newOSGiContentCapability(org.osgi.resource.Resource or, Resource resource) + { + final String uri = resource.getURI(); + LazyStringMap.LazyValue content = new LazyStringMap.LazyValue() { + public String compute() { + // This is expensive to do, so only compute it when actually obtained... + try { + return OSGiRepositoryImpl.getSHA256(uri); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + }; + Object mime = resource.getProperties().get("mime"); + if (mime == null) + mime = "application/vnd.osgi.bundle"; + + Map contentAttrs = new LazyStringMap(4); + contentAttrs.put(ContentNamespace.CAPABILITY_MIME_ATTRIBUTE, mime); + contentAttrs.put(ContentNamespace.CAPABILITY_SIZE_ATTRIBUTE, resource.getSize()); + contentAttrs.put(ContentNamespace.CAPABILITY_URL_ATTRIBUTE, uri); + contentAttrs.put(ContentNamespace.CONTENT_NAMESPACE, content); + return new CapabilityImpl(or, ContentNamespace.CONTENT_NAMESPACE, Collections. emptyMap(), contentAttrs); + } + + static String getSHA256(String uri) throws IOException, NoSuchAlgorithmException // TODO find a good place for this + { + InputStream is = new URL(uri).openStream(); + MessageDigest md = MessageDigest.getInstance("SHA-256"); + + // Use a digest inputstream as using byte arrays directly to compute the SHA-256 can + // have big effects on memory consumption. I.e. you don't want to have to read the + // entire resource in memory. We rather stream it through... + DigestInputStream dis = new DigestInputStream(is, md); + + byte[] buffer = new byte[16384]; + while (dis.read(buffer) != -1) { + // we just drain the stream here to compute the Message Digest + } + + StringBuilder sb = new StringBuilder(64); // SHA-256 is always 64 hex characters + for (byte b : md.digest()) + { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/OSGiRequirementAdapter.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/OSGiRequirementAdapter.java new file mode 100644 index 00000000000..dde9b279cd1 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/OSGiRequirementAdapter.java @@ -0,0 +1,100 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.bundlerepository.impl; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.bundlerepository.Capability; +import org.apache.felix.bundlerepository.Requirement; +import org.osgi.framework.Constants; +import org.osgi.resource.Namespace; + +class OSGiRequirementAdapter implements Requirement +{ + private final org.osgi.resource.Requirement requirement; + private final HashMap cleanedDirectives; + private final String filter; + + public OSGiRequirementAdapter(org.osgi.resource.Requirement requirement) + { + this.requirement = requirement; + + String f = requirement.getDirectives().get(Constants.FILTER_DIRECTIVE); + if (f != null) + { + for (String ns : NamespaceTranslator.getTranslatedOSGiNamespaces()) + { + f = f.replaceAll("[(][ ]*" + ns + "[ ]*=", + "(" + NamespaceTranslator.getFelixNamespace(ns) + "="); + } + } + filter = f; + + cleanedDirectives = new HashMap(requirement.getDirectives()); + // Remove directives that are represented as APIs on this class. + cleanedDirectives.remove(Constants.FILTER_DIRECTIVE); + cleanedDirectives.remove(Namespace.REQUIREMENT_CARDINALITY_DIRECTIVE); + cleanedDirectives.remove(Constants.RESOLUTION_DIRECTIVE); + } + + public Map getAttributes() + { + return requirement.getAttributes(); + } + + public Map getDirectives() + { + + return cleanedDirectives; + } + + public String getComment() + { + return null; + } + + public String getFilter() + { + return filter; + } + + public String getName() + { + return NamespaceTranslator.getFelixNamespace(requirement.getNamespace()); + } + + public boolean isExtend() + { + return false; + } + + public boolean isMultiple() + { + String multiple = requirement.getDirectives().get(Namespace.REQUIREMENT_CARDINALITY_DIRECTIVE); + return Namespace.CARDINALITY_MULTIPLE.equals(multiple); + } + + public boolean isOptional() + { + String resolution = requirement.getDirectives().get(Constants.RESOLUTION_DIRECTIVE); + return Constants.RESOLUTION_OPTIONAL.equals(resolution); + } + + public boolean isSatisfied(Capability capability) + { + boolean result = OSGiResourceHelper.matches(requirement, new FelixCapabilityAdapter(capability, null)); + return result; + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/OSGiResourceHelper.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/OSGiResourceHelper.java new file mode 100644 index 00000000000..6d560d6a352 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/OSGiResourceHelper.java @@ -0,0 +1,111 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.bundlerepository.impl; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.apache.felix.utils.filter.FilterImpl; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.Version; +import org.osgi.framework.namespace.IdentityNamespace; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; +import org.osgi.service.repository.ContentNamespace; +import org.osgi.service.repository.Repository; + +public class OSGiResourceHelper +{ + public static String getContentAttribute(Resource resource) + { + return (String) getContentAttribute(resource, ContentNamespace.CONTENT_NAMESPACE); + } + + public static Object getContentAttribute(Resource resource, String name) + { + List capabilities = resource.getCapabilities(ContentNamespace.CONTENT_NAMESPACE); + Capability capability = capabilities.get(0); + return capability.getAttributes().get(name); + } + + public static Object getIdentityAttribute(Resource resource, String name) + { + List capabilities = resource.getCapabilities(IdentityNamespace.IDENTITY_NAMESPACE); + Capability capability = capabilities.get(0); + return capability.getAttributes().get(name); + } + + public static Resource getResource(Requirement requirement, Repository repository) + { + Map> map = repository.findProviders(Arrays.asList(requirement)); + Collection capabilities = map.get(requirement); + return capabilities == null ? null : capabilities.size() == 0 ? null : capabilities.iterator().next().getResource(); + } + + public static String getSymbolicNameAttribute(Resource resource) + { + return (String) getIdentityAttribute(resource, IdentityNamespace.IDENTITY_NAMESPACE); + } + + public static String getTypeAttribute(Resource resource) + { + String result = (String) getIdentityAttribute(resource, IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE); + if (result == null) + result = IdentityNamespace.TYPE_BUNDLE; + return result; + } + + public static Version getVersionAttribute(Resource resource) + { + Version result = (Version) getIdentityAttribute(resource, IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE); + if (result == null) + result = Version.emptyVersion; + return result; + } + + public static boolean matches(Requirement requirement, Capability capability) + { + boolean result = false; + if (requirement == null && capability == null) + result = true; + else if (requirement == null || capability == null) + result = false; + else if (!capability.getNamespace().equals(requirement.getNamespace())) + result = false; + else + { + String filterStr = requirement.getDirectives().get(Constants.FILTER_DIRECTIVE); + if (filterStr == null) + result = true; + else + { + try + { + if (FilterImpl.newInstance(filterStr).matchCase(capability.getAttributes())) + result = true; + } + catch (InvalidSyntaxException e) + { + result = false; + } + } + } + // TODO Check directives. + return result; + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/OSGiResourceImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/OSGiResourceImpl.java new file mode 100644 index 00000000000..2b4de898df4 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/OSGiResourceImpl.java @@ -0,0 +1,73 @@ +/* + * 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.bundlerepository.impl; + +import java.util.ArrayList; +import java.util.List; + +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; + +public class OSGiResourceImpl implements Resource +{ + private final List capabilities; + private final List requirements; + + @SuppressWarnings("unchecked") + public OSGiResourceImpl(List caps, List reqs) + { + capabilities = (List) caps; + requirements = (List) reqs; + } + + public List getCapabilities(String namespace) + { + if (namespace == null) + return capabilities; + + List caps = new ArrayList(); + for(Capability cap : capabilities) + { + if (namespace.equals(cap.getNamespace())) + { + caps.add(cap); + } + } + return caps; + } + + public List getRequirements(String namespace) + { + if (namespace == null) + return requirements; + + List reqs = new ArrayList(); + for(Requirement req : requirements) + { + if (namespace.equals(req.getNamespace())) + { + reqs.add(req); + } + } + return reqs; + } + + // TODO implement equals and hashcode +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ObrCommandImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ObrCommandImpl.java new file mode 100644 index 00000000000..e784c97c911 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ObrCommandImpl.java @@ -0,0 +1,1352 @@ +/* + * 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.bundlerepository.impl; + +import java.io.*; +import java.lang.reflect.Array; +import java.net.URL; +import java.util.*; + +import org.apache.felix.bundlerepository.Capability; +import org.apache.felix.bundlerepository.Reason; +import org.apache.felix.bundlerepository.Requirement; +import org.apache.felix.bundlerepository.Resolver; +import org.apache.felix.bundlerepository.Resource; +import org.apache.felix.bundlerepository.impl.FileUtil; +import org.apache.felix.shell.Command; +import org.osgi.framework.*; + +public class ObrCommandImpl implements Command +{ + private static final String HELP_CMD = "help"; + private static final String ADDURL_CMD = "add-url"; + private static final String REMOVEURL_CMD = "remove-url"; + private static final String LISTURL_CMD = "list-url"; + private static final String REFRESHURL_CMD = "refresh-url"; + private static final String LIST_CMD = "list"; + private static final String INFO_CMD = "info"; + private static final String DEPLOY_CMD = "deploy"; + private static final String START_CMD = "start"; + private static final String SOURCE_CMD = "source"; + private static final String JAVADOC_CMD = "javadoc"; + + private static final String EXTRACT_SWITCH = "-x"; + private static final String VERBOSE_SWITCH = "-v"; + + private BundleContext m_context = null; + private org.apache.felix.bundlerepository.RepositoryAdmin m_repoAdmin = null; + + public ObrCommandImpl(BundleContext context, org.apache.felix.bundlerepository.RepositoryAdmin repoAdmin) + { + m_context = context; + m_repoAdmin = repoAdmin; + } + + public String getName() + { + return "obr"; + } + + public String getUsage() + { + return "obr help"; + } + + public String getShortDescription() + { + return "OSGi bundle repository."; + } + + public synchronized void execute(String commandLine, PrintStream out, PrintStream err) + { + try + { + // Parse the commandLine to get the OBR command. + StringTokenizer st = new StringTokenizer(commandLine); + // Ignore the invoking command. + st.nextToken(); + // Try to get the OBR command, default is HELP command. + String command = HELP_CMD; + try + { + command = st.nextToken(); + } + catch (Exception ex) + { + // Ignore. + } + + // Perform the specified command. + if ((command == null) || (command.equals(HELP_CMD))) + { + help(out, st); + } + else + { + if (command.equals(ADDURL_CMD) || + command.equals(REFRESHURL_CMD) || + command.equals(REMOVEURL_CMD) || + command.equals(LISTURL_CMD)) + { + urls(commandLine, command, out, err); + } + else if (command.equals(LIST_CMD)) + { + list(commandLine, command, out, err); + } + else if (command.equals(INFO_CMD)) + { + info(commandLine, command, out, err); + } + else if (command.equals(DEPLOY_CMD) || command.equals(START_CMD)) + { + deploy(commandLine, command, out, err); + } + else if (command.equals(SOURCE_CMD)) + { + source(commandLine, command, out, err); + } + else if (command.equals(JAVADOC_CMD)) + { + javadoc(commandLine, command, out, err); + } + else + { + err.println("Unknown command: " + command); + } + } + } + catch (InvalidSyntaxException ex) + { + err.println("Syntax error: " + ex.getMessage()); + } + catch (IOException ex) + { + err.println("Error: " + ex); + } + } + + private void urls( + String commandLine, String command, PrintStream out, PrintStream err) + throws IOException + { + // Parse the commandLine. + StringTokenizer st = new StringTokenizer(commandLine); + // Ignore the "obr" command. + st.nextToken(); + // Ignore the "url" command. + st.nextToken(); + + int count = st.countTokens(); + if (count > 0) + { + while (st.hasMoreTokens()) + { + try + { + String uri = st.nextToken(); + if (command.equals(ADDURL_CMD)) + { + m_repoAdmin.addRepository(uri); + } + else if (command.equals(REFRESHURL_CMD)) + { + m_repoAdmin.removeRepository(uri); + m_repoAdmin.addRepository(uri); + } + else + { + m_repoAdmin.removeRepository(uri); + } + } + catch (Exception ex) + { + ex.printStackTrace(err); + } + } + } + else + { + org.apache.felix.bundlerepository.Repository[] repos = m_repoAdmin.listRepositories(); + if ((repos != null) && (repos.length > 0)) + { + for (int i = 0; i < repos.length; i++) + { + out.println(repos[i].getURI()); + } + } + else + { + out.println("No repository URLs are set."); + } + } + } + + private void list( + String commandLine, String command, PrintStream out, PrintStream err) + throws IOException, InvalidSyntaxException + { + // Parse the command for an option switch and tokens. + ParsedCommand pc = parseList(commandLine); + + // Create a filter that will match presentation name or symbolic name. + StringBuffer sb = new StringBuffer(); + if ((pc.getTokens() == null) || (pc.getTokens().length() == 0)) + { + sb.append("(|(presentationname=*)(symbolicname=*))"); + } + else + { + sb.append("(|(presentationname=*"); + sb.append(pc.getTokens()); + sb.append("*)(symbolicname=*"); + sb.append(pc.getTokens()); + sb.append("*))"); + } + // Use filter to get matching resources. + Resource[] resources = m_repoAdmin.discoverResources(sb.toString()); + + // Group the resources by symbolic name in descending version order, + // but keep them in overall sorted order by presentation name. + Map revisionMap = new TreeMap(new Comparator() { + public int compare(Object o1, Object o2) + { + Resource r1 = (Resource) o1; + Resource r2 = (Resource) o2; + // Assume if the symbolic name is equal, then the two are equal, + // since we are trying to aggregate by symbolic name. + int symCompare = r1.getSymbolicName().compareTo(r2.getSymbolicName()); + if (symCompare == 0) + { + return 0; + } + // Otherwise, compare the presentation name to keep them sorted + // by presentation name. If the presentation names are equal, then + // use the symbolic name to differentiate. + int compare = (r1.getPresentationName() == null) + ? -1 + : (r2.getPresentationName() == null) + ? 1 + : r1.getPresentationName().compareToIgnoreCase( + r2.getPresentationName()); + if (compare == 0) + { + return symCompare; + } + return compare; + } + }); + for (int resIdx = 0; (resources != null) && (resIdx < resources.length); resIdx++) + { + Resource[] revisions = (Resource[]) revisionMap.get(resources[resIdx]); + revisionMap.put(resources[resIdx], addResourceByVersion(revisions, resources[resIdx])); + } + + // Print any matching resources. + for (Iterator i = revisionMap.entrySet().iterator(); i.hasNext(); ) + { + Map.Entry entry = (Map.Entry) i.next(); + Resource[] revisions = (Resource[]) entry.getValue(); + String name = revisions[0].getPresentationName(); + name = (name == null) ? revisions[0].getSymbolicName() : name; + out.print(name); + + if (pc.isVerbose() && revisions[0].getPresentationName() != null) + { + out.print(" [" + revisions[0].getSymbolicName() + "]"); + } + + out.print(" ("); + int revIdx = 0; + do + { + if (revIdx > 0) + { + out.print(", "); + } + out.print(revisions[revIdx].getVersion()); + revIdx++; + } + while (pc.isVerbose() && (revIdx < revisions.length)); + if (!pc.isVerbose() && (revisions.length > 1)) + { + out.print(", ..."); + } + out.println(")"); + } + + if ((resources == null) || (resources.length == 0)) + { + out.println("No matching bundles."); + } + } + + private void info( + String commandLine, String command, PrintStream out, PrintStream err) + throws IOException, InvalidSyntaxException + { + ParsedCommand pc = parseInfo(commandLine); + for (int cmdIdx = 0; (pc != null) && (cmdIdx < pc.getTargetCount()); cmdIdx++) + { + // Find the target's bundle resource. + Resource[] resources = searchRepository(pc.getTargetId(cmdIdx), pc.getTargetVersion(cmdIdx)); + if (resources == null) + { + err.println("Unknown bundle and/or version: " + + pc.getTargetId(cmdIdx)); + } + else + { + for (int resIdx = 0; resIdx < resources.length; resIdx++) + { + if (resIdx > 0) + { + out.println(""); + } + printResource(out, resources[resIdx]); + } + } + } + } + + private void deploy( + String commandLine, String command, PrintStream out, PrintStream err) + throws IOException, InvalidSyntaxException + { + ParsedCommand pc = parseInstallStart(commandLine); + _deploy(pc, command, out, err); + } + + private void _deploy( + ParsedCommand pc, String command, PrintStream out, PrintStream err) + throws IOException, InvalidSyntaxException + { + org.apache.felix.bundlerepository.Resolver resolver = m_repoAdmin.resolver(); + for (int i = 0; (pc != null) && (i < pc.getTargetCount()); i++) + { + // Find the target's bundle resource. + Resource resource = selectNewestVersion( + searchRepository(pc.getTargetId(i), pc.getTargetVersion(i))); + if (resource != null) + { + resolver.add(resource); + } + else + { + err.println("Unknown bundle - " + pc.getTargetId(i)); + } + } + + if ((resolver.getAddedResources() != null) && + (resolver.getAddedResources().length > 0)) + { + if (resolver.resolve()) + { + out.println("Target resource(s):"); + printUnderline(out, 19); + Resource[] resources = resolver.getAddedResources(); + for (int resIdx = 0; (resources != null) && (resIdx < resources.length); resIdx++) + { + out.println(" " + resources[resIdx].getPresentationName() + + " (" + resources[resIdx].getVersion() + ")"); + } + resources = resolver.getRequiredResources(); + if ((resources != null) && (resources.length > 0)) + { + out.println("\nRequired resource(s):"); + printUnderline(out, 21); + for (int resIdx = 0; resIdx < resources.length; resIdx++) + { + out.println(" " + resources[resIdx].getPresentationName() + + " (" + resources[resIdx].getVersion() + ")"); + } + } + resources = resolver.getOptionalResources(); + if ((resources != null) && (resources.length > 0)) + { + out.println("\nOptional resource(s):"); + printUnderline(out, 21); + for (int resIdx = 0; resIdx < resources.length; resIdx++) + { + out.println(" " + resources[resIdx].getPresentationName() + + " (" + resources[resIdx].getVersion() + ")"); + } + } + + try + { + out.print("\nDeploying..."); + resolver.deploy(command.equals(START_CMD) ? Resolver.START : 0); + out.println("done."); + } + catch (IllegalStateException ex) + { + err.println(ex); + } + } + else + { + Reason[] reqs = resolver.getUnsatisfiedRequirements(); + if ((reqs != null) && (reqs.length > 0)) + { + out.println("Unsatisfied requirement(s):"); + printUnderline(out, 27); + for (int reqIdx = 0; reqIdx < reqs.length; reqIdx++) + { + out.println(" " + reqs[reqIdx].getRequirement().getFilter()); + out.println(" " + reqs[reqIdx].getResource().getPresentationName()); + } + } + else + { + out.println("Could not resolve targets."); + } + } + } + } + + private void source( + String commandLine, String command, PrintStream out, PrintStream err) + throws IOException, InvalidSyntaxException + { + // Parse the command line to get all local targets to update. + ParsedCommand pc = parseSource(commandLine); + for (int i = 0; i < pc.getTargetCount(); i++) + { + Resource resource = selectNewestVersion( + searchRepository(pc.getTargetId(i), pc.getTargetVersion(i))); + if (resource == null) + { + err.println("Unknown bundle and/or version: " + + pc.getTargetId(i)); + } + else + { + String srcURI = (String) resource.getProperties().get(Resource.SOURCE_URI); + if (srcURI != null) + { + FileUtil.downloadSource( + out, err, new URL(srcURI), pc.getDirectory(), pc.isExtract()); + } + else + { + err.println("Missing source URL: " + pc.getTargetId(i)); + } + } + } + } + + private void javadoc( + String commandLine, String command, PrintStream out, PrintStream err) + throws IOException, InvalidSyntaxException + { + // Parse the command line to get all local targets to update. + ParsedCommand pc = parseSource(commandLine); + for (int i = 0; i < pc.getTargetCount(); i++) + { + Resource resource = selectNewestVersion( + searchRepository(pc.getTargetId(i), pc.getTargetVersion(i))); + if (resource == null) + { + err.println("Unknown bundle and/or version: " + + pc.getTargetId(i)); + } + else + { + URL docURL = (URL) resource.getProperties().get("javadoc"); + if (docURL != null) + { + FileUtil.downloadSource( + out, err, docURL, pc.getDirectory(), pc.isExtract()); + } + else + { + err.println("Missing javadoc URL: " + pc.getTargetId(i)); + } + } + } + } + + private Resource[] searchRepository(String targetId, String targetVersion) throws InvalidSyntaxException + { + // Try to see if the targetId is a bundle ID. + try + { + Bundle bundle = m_context.getBundle(Long.parseLong(targetId)); + targetId = bundle.getSymbolicName(); + } + catch (NumberFormatException ex) + { + // It was not a number, so ignore. + } + + // The targetId may be a bundle name or a bundle symbolic name, + // so create the appropriate LDAP query. + StringBuffer sb = new StringBuffer("(|(presentationname="); + sb.append(targetId); + sb.append(")(symbolicname="); + sb.append(targetId); + sb.append("))"); + if (targetVersion != null) + { + sb.insert(0, "(&"); + sb.append("(version="); + sb.append(targetVersion); + sb.append("))"); + } + return m_repoAdmin.discoverResources(sb.toString()); + } + + public Resource selectNewestVersion(Resource[] resources) + { + int idx = -1; + Version v = null; + for (int i = 0; (resources != null) && (i < resources.length); i++) + { + if (i == 0) + { + idx = 0; + v = resources[i].getVersion(); + } + else + { + Version vtmp = resources[i].getVersion(); + if (vtmp.compareTo(v) > 0) + { + idx = i; + v = vtmp; + } + } + } + + return (idx < 0) ? null : resources[idx]; + } + + private void printResource(PrintStream out, Resource resource) + { + printUnderline(out, resource.getPresentationName().length()); + out.println(resource.getPresentationName()); + printUnderline(out, resource.getPresentationName().length()); + + Map map = resource.getProperties(); + for (Iterator iter = map.entrySet().iterator(); iter.hasNext(); ) + { + Map.Entry entry = (Map.Entry) iter.next(); + if (entry.getValue().getClass().isArray()) + { + out.println(entry.getKey() + ":"); + for (int j = 0; j < Array.getLength(entry.getValue()); j++) + { + out.println(" " + Array.get(entry.getValue(), j)); + } + } + else + { + out.println(entry.getKey() + ": " + entry.getValue()); + } + } + + Requirement[] reqs = resource.getRequirements(); + if ((reqs != null) && (reqs.length > 0)) + { + out.println("Requires:"); + for (int i = 0; i < reqs.length; i++) + { + out.println(" " + reqs[i].getFilter()); + } + } + + Capability[] caps = resource.getCapabilities(); + if ((caps != null) && (caps.length > 0)) + { + out.println("Capabilities:"); + for (int i = 0; i < caps.length; i++) + { + out.println(" " + caps[i].getPropertiesAsMap()); + } + } + } + + private static void printUnderline(PrintStream out, int length) + { + for (int i = 0; i < length; i++) + { + out.print('-'); + } + out.println(""); + } + + private ParsedCommand parseList(String commandLine) + throws IOException, InvalidSyntaxException + { + // The command line for list will be something like: + // obr list -v token token + + // Create a stream tokenizer for the command line string, + StringReader sr = new StringReader(commandLine); + StreamTokenizer tokenizer = new StreamTokenizer(sr); + tokenizer.resetSyntax(); + tokenizer.quoteChar('\''); + tokenizer.quoteChar('\"'); + tokenizer.whitespaceChars('\u0000', '\u0020'); + tokenizer.wordChars('A', 'Z'); + tokenizer.wordChars('a', 'z'); + tokenizer.wordChars('0', '9'); + tokenizer.wordChars('\u00A0', '\u00FF'); + tokenizer.wordChars('.', '.'); + tokenizer.wordChars('-', '-'); + tokenizer.wordChars('_', '_'); + + // Ignore the invoking command name and the OBR command. + int type = tokenizer.nextToken(); + type = tokenizer.nextToken(); + + int EOF = 1; + int SWITCH = 2; + int TOKEN = 4; + + // Construct an install record. + ParsedCommand pc = new ParsedCommand(); + String tokens = null; + + // The state machine starts by expecting either a + // SWITCH or a DIRECTORY. + int expecting = (SWITCH | TOKEN | EOF); + while (true) + { + // Get the next token type. + type = tokenizer.nextToken(); + switch (type) + { + // EOF received. + case StreamTokenizer.TT_EOF: + // Error if we weren't expecting EOF. + if ((expecting & EOF) == 0) + { + throw new InvalidSyntaxException( + "Expecting more arguments.", null); + } + // Add current target if there is one. + if (tokens != null) + { + pc.setTokens(tokens); + } + // Return cleanly. + return pc; + + // WORD or quoted WORD received. + case StreamTokenizer.TT_WORD: + case '\'': + case '\"': + // If we are expecting a command SWITCH and the token + // equals a command SWITCH, then record it. + if (((expecting & SWITCH) > 0) && tokenizer.sval.equals(VERBOSE_SWITCH)) + { + pc.setVerbose(true); + expecting = (TOKEN | EOF); + } + // If we are expecting a target, the record it. + else if ((expecting & TOKEN) > 0) + { + // Add a space in between tokens. + if (tokens == null) + { + tokens = ""; + } + else + { + tokens += " "; + } + // Append to the current token. + tokens += tokenizer.sval; + expecting = (EOF | TOKEN); + } + else + { + throw new InvalidSyntaxException( + "Not expecting '" + tokenizer.sval + "'.", null); + } + break; + } + } + } + + private ParsedCommand parseInfo(String commandLine) + throws IOException, InvalidSyntaxException + { + // Create a stream tokenizer for the command line string, + // since the syntax for install/start is more sophisticated. + StringReader sr = new StringReader(commandLine); + StreamTokenizer tokenizer = new StreamTokenizer(sr); + tokenizer.resetSyntax(); + tokenizer.quoteChar('\''); + tokenizer.quoteChar('\"'); + tokenizer.whitespaceChars('\u0000', '\u0020'); + tokenizer.wordChars('A', 'Z'); + tokenizer.wordChars('a', 'z'); + tokenizer.wordChars('0', '9'); + tokenizer.wordChars('\u00A0', '\u00FF'); + tokenizer.wordChars('.', '.'); + tokenizer.wordChars('-', '-'); + tokenizer.wordChars('_', '_'); + + // Ignore the invoking command name and the OBR command. + int type = tokenizer.nextToken(); + type = tokenizer.nextToken(); + + int EOF = 1; + int SWITCH = 2; + int TARGET = 4; + int VERSION = 8; + int VERSION_VALUE = 16; + + // Construct an install record. + ParsedCommand pc = new ParsedCommand(); + String currentTargetName = null; + + // The state machine starts by expecting either a + // SWITCH or a TARGET. + int expecting = (TARGET); + while (true) + { + // Get the next token type. + type = tokenizer.nextToken(); + switch (type) + { + // EOF received. + case StreamTokenizer.TT_EOF: + // Error if we weren't expecting EOF. + if ((expecting & EOF) == 0) + { + throw new InvalidSyntaxException( + "Expecting more arguments.", null); + } + // Add current target if there is one. + if (currentTargetName != null) + { + pc.addTarget(currentTargetName, null); + } + // Return cleanly. + return pc; + + // WORD or quoted WORD received. + case StreamTokenizer.TT_WORD: + case '\'': + case '\"': + // If we are expecting a target, the record it. + if ((expecting & TARGET) > 0) + { + // Add current target if there is one. + if (currentTargetName != null) + { + pc.addTarget(currentTargetName, null); + } + // Set the new target as the current target. + currentTargetName = tokenizer.sval; + expecting = (EOF | TARGET | VERSION); + } + else if ((expecting & VERSION_VALUE) > 0) + { + pc.addTarget(currentTargetName, tokenizer.sval); + currentTargetName = null; + expecting = (EOF | TARGET); + } + else + { + throw new InvalidSyntaxException( + "Not expecting '" + tokenizer.sval + "'.", null); + } + break; + + // Version separator character received. + case ';': + // Error if we weren't expecting the version separator. + if ((expecting & VERSION) == 0) + { + throw new InvalidSyntaxException( + "Not expecting version.", null); + } + // Otherwise, we will only expect a version value next. + expecting = (VERSION_VALUE); + break; + } + } + } + + private ParsedCommand parseInstallStart(String commandLine) + throws IOException, InvalidSyntaxException + { + // Create a stream tokenizer for the command line string, + // since the syntax for install/start is more sophisticated. + StringReader sr = new StringReader(commandLine); + StreamTokenizer tokenizer = new StreamTokenizer(sr); + tokenizer.resetSyntax(); + tokenizer.quoteChar('\''); + tokenizer.quoteChar('\"'); + tokenizer.whitespaceChars('\u0000', '\u0020'); + tokenizer.wordChars('A', 'Z'); + tokenizer.wordChars('a', 'z'); + tokenizer.wordChars('0', '9'); + tokenizer.wordChars('\u00A0', '\u00FF'); + tokenizer.wordChars('.', '.'); + tokenizer.wordChars('-', '-'); + tokenizer.wordChars('_', '_'); + + // Ignore the invoking command name and the OBR command. + int type = tokenizer.nextToken(); + type = tokenizer.nextToken(); + + int EOF = 1; + int SWITCH = 2; + int TARGET = 4; + int VERSION = 8; + int VERSION_VALUE = 16; + + // Construct an install record. + ParsedCommand pc = new ParsedCommand(); + String currentTargetName = null; + + // The state machine starts by expecting either a + // SWITCH or a TARGET. + int expecting = (SWITCH | TARGET); + while (true) + { + // Get the next token type. + type = tokenizer.nextToken(); + switch (type) + { + // EOF received. + case StreamTokenizer.TT_EOF: + // Error if we weren't expecting EOF. + if ((expecting & EOF) == 0) + { + throw new InvalidSyntaxException( + "Expecting more arguments.", null); + } + // Add current target if there is one. + if (currentTargetName != null) + { + pc.addTarget(currentTargetName, null); + } + // Return cleanly. + return pc; + + // WORD or quoted WORD received. + case StreamTokenizer.TT_WORD: + case '\'': + case '\"': + // If we are expecting a target, the record it. + if ((expecting & TARGET) > 0) + { + // Add current target if there is one. + if (currentTargetName != null) + { + pc.addTarget(currentTargetName, null); + } + // Set the new target as the current target. + currentTargetName = tokenizer.sval; + expecting = (EOF | TARGET | VERSION); + } + else if ((expecting & VERSION_VALUE) > 0) + { + pc.addTarget(currentTargetName, tokenizer.sval); + currentTargetName = null; + expecting = (EOF | TARGET); + } + else + { + throw new InvalidSyntaxException( + "Not expecting '" + tokenizer.sval + "'.", null); + } + break; + + // Version separator character received. + case ';': + // Error if we weren't expecting the version separator. + if ((expecting & VERSION) == 0) + { + throw new InvalidSyntaxException( + "Not expecting version.", null); + } + // Otherwise, we will only expect a version value next. + expecting = (VERSION_VALUE); + break; + } + } + } + + private ParsedCommand parseSource(String commandLine) + throws IOException, InvalidSyntaxException + { + // Create a stream tokenizer for the command line string, + // since the syntax for install/start is more sophisticated. + StringReader sr = new StringReader(commandLine); + StreamTokenizer tokenizer = new StreamTokenizer(sr); + tokenizer.resetSyntax(); + tokenizer.quoteChar('\''); + tokenizer.quoteChar('\"'); + tokenizer.whitespaceChars('\u0000', '\u0020'); + tokenizer.wordChars('A', 'Z'); + tokenizer.wordChars('a', 'z'); + tokenizer.wordChars('0', '9'); + tokenizer.wordChars('\u00A0', '\u00FF'); + tokenizer.wordChars('.', '.'); + tokenizer.wordChars('-', '-'); + tokenizer.wordChars('_', '_'); + tokenizer.wordChars('/', '/'); + tokenizer.wordChars('\\', '\\'); + tokenizer.wordChars(':', ':'); + + // Ignore the invoking command name and the OBR command. + int type = tokenizer.nextToken(); + type = tokenizer.nextToken(); + + int EOF = 1; + int SWITCH = 2; + int DIRECTORY = 4; + int TARGET = 8; + int VERSION = 16; + int VERSION_VALUE = 32; + + // Construct an install record. + ParsedCommand pc = new ParsedCommand(); + String currentTargetName = null; + + // The state machine starts by expecting either a + // SWITCH or a DIRECTORY. + int expecting = (SWITCH | DIRECTORY); + while (true) + { + // Get the next token type. + type = tokenizer.nextToken(); + switch (type) + { + // EOF received. + case StreamTokenizer.TT_EOF: + // Error if we weren't expecting EOF. + if ((expecting & EOF) == 0) + { + throw new InvalidSyntaxException( + "Expecting more arguments.", null); + } + // Add current target if there is one. + if (currentTargetName != null) + { + pc.addTarget(currentTargetName, null); + } + // Return cleanly. + return pc; + + // WORD or quoted WORD received. + case StreamTokenizer.TT_WORD: + case '\'': + case '\"': + // If we are expecting a command SWITCH and the token + // equals a command SWITCH, then record it. + if (((expecting & SWITCH) > 0) && tokenizer.sval.equals(EXTRACT_SWITCH)) + { + pc.setExtract(true); + expecting = (DIRECTORY); + } + // If we are expecting a directory, the record it. + else if ((expecting & DIRECTORY) > 0) + { + // Set the directory for the command. + pc.setDirectory(tokenizer.sval); + expecting = (TARGET); + } + // If we are expecting a target, the record it. + else if ((expecting & TARGET) > 0) + { + // Add current target if there is one. + if (currentTargetName != null) + { + pc.addTarget(currentTargetName, null); + } + // Set the new target as the current target. + currentTargetName = tokenizer.sval; + expecting = (EOF | TARGET | VERSION); + } + else if ((expecting & VERSION_VALUE) > 0) + { + pc.addTarget(currentTargetName, tokenizer.sval); + currentTargetName = null; + expecting = (EOF | TARGET); + } + else + { + throw new InvalidSyntaxException( + "Not expecting '" + tokenizer.sval + "'.", null); + } + break; + + // Version separator character received. + case ';': + // Error if we weren't expecting the version separator. + if ((expecting & VERSION) == 0) + { + throw new InvalidSyntaxException( + "Not expecting version.", null); + } + // Otherwise, we will only expect a version value next. + expecting = (VERSION_VALUE); + break; + } + } + } + + private void help(PrintStream out, StringTokenizer st) + { + String command = HELP_CMD; + if (st.hasMoreTokens()) + { + command = st.nextToken(); + } + if (command.equals(ADDURL_CMD)) + { + out.println(""); + out.println("obr " + ADDURL_CMD + " ..."); + out.println(""); + out.println( + "This command adds the space-delimited list of repository URLs to\n" + + "the repository service."); + out.println(""); + } + else if (command.equals(REFRESHURL_CMD)) + { + out.println(""); + out.println("obr " + REFRESHURL_CMD + " ..."); + out.println(""); + out.println( + "This command refreshes the space-delimited list of repository URLs\n" + + "within the repository service.\n" + + "(The command internally removes and adds the specified URLs from the\n" + + "repository service.)"); + out.println(""); + } + else if (command.equals(REMOVEURL_CMD)) + { + out.println(""); + out.println("obr " + REMOVEURL_CMD + " ..."); + out.println(""); + out.println( + "This command removes the space-delimited list of repository URLs\n" + + "from the repository service."); + out.println(""); + } + else if (command.equals(LISTURL_CMD)) + { + out.println(""); + out.println("obr " + LISTURL_CMD); + out.println(""); + out.println( + "This command displays the repository URLs currently associated\n" + + "with the repository service."); + out.println(""); + } + else if (command.equals(LIST_CMD)) + { + out.println(""); + out.println("obr " + LIST_CMD + + " [" + VERBOSE_SWITCH + "] [ ...]"); + out.println(""); + out.println( + "This command lists bundles available in the bundle repository.\n" + + "If no arguments are specified, then all available bundles are\n" + + "listed, otherwise any arguments are concatenated with spaces\n" + + "and used as a substring filter on the bundle names. By default,\n" + + "only the most recent version of each artifact is shown. To list\n" + + "all available versions use the \"" + VERBOSE_SWITCH + "\" switch."); + out.println(""); + } + else if (command.equals(INFO_CMD)) + { + out.println(""); + out.println("obr " + INFO_CMD + + " ||[;] ..."); + out.println(""); + out.println( + "This command displays the meta-data for the specified bundles.\n" + + "If a bundle's name contains spaces, then it must be surrounded\n" + + "by quotes. It is also possible to specify a precise version\n" + + "if more than one version exists, such as:\n" + + "\n" + + " obr info \"Bundle Repository\";1.0.0\n" + + "\n" + + "The above example retrieves the meta-data for version \"1.0.0\"\n" + + "of the bundle named \"Bundle Repository\"."); + out.println(""); + } + else if (command.equals(DEPLOY_CMD)) + { + out.println(""); + out.println("obr " + DEPLOY_CMD + + " ||[;] ... "); + out.println(""); + out.println( + "This command tries to install or update the specified bundles\n" + + "and all of their dependencies. You can specify either the bundle\n" + + "name or the bundle identifier. If a bundle's name contains spaces,\n" + + "then it must be surrounded by quotes. It is also possible to\n" + + "specify a precise version if more than one version exists, such as:\n" + + "\n" + + " obr deploy \"Bundle Repository\";1.0.0\n" + + "\n" + + "For the above example, if version \"1.0.0\" of \"Bundle Repository\" is\n" + + "already installed locally, then the command will attempt to update it\n" + + "and all of its dependencies; otherwise, the command will install it\n" + + "and all of its dependencies."); + out.println(""); + } + else if (command.equals(START_CMD)) + { + out.println(""); + out.println("obr " + START_CMD + + " ||[;] ..."); + out.println(""); + out.println( + "This command installs and starts the specified bundles and all\n" + + "of their dependencies. If a bundle's name contains spaces, then\n" + + "it must be surrounded by quotes. If a specified bundle is already\n" + "installed, then this command has no effect. It is also possible\n" + "to specify a precise version if more than one version exists,\n" + "such as:\n" + + "\n" + + " obr start \"Bundle Repository\";1.0.0\n" + + "\n" + + "The above example installs and starts version \"1.0.0\" of the\n" + + "bundle named \"Bundle Repository\" and its dependencies."); + out.println(""); + } + else if (command.equals(SOURCE_CMD)) + { + out.println(""); + out.println("obr " + SOURCE_CMD + + " [" + EXTRACT_SWITCH + + "] [;] ..."); + out.println(""); + out.println( + "This command retrieves the source archives of the specified\n" + + "bundles and saves them to the specified local directory; use\n" + + "the \"" + EXTRACT_SWITCH + "\" switch to automatically extract the source archives.\n" + + "If a bundle name contains spaces, then it must be surrounded\n" + + "by quotes. It is also possible to specify a precise version if\n" + "more than one version exists, such as:\n" + + "\n" + + " obr source /home/rickhall/tmp \"Bundle Repository\";1.0.0\n" + + "\n" + + "The above example retrieves the source archive of version \"1.0.0\"\n" + + "of the bundle named \"Bundle Repository\" and saves it to the\n" + + "specified local directory."); + out.println(""); + } + else if (command.equals(JAVADOC_CMD)) + { + out.println(""); + out.println("obr " + JAVADOC_CMD + + " [" + EXTRACT_SWITCH + + "] [;] ..."); + out.println(""); + out.println( + "This command retrieves the javadoc archives of the specified\n" + + "bundles and saves them to the specified local directory; use\n" + + "the \"" + EXTRACT_SWITCH + "\" switch to automatically extract the javadoc archives.\n" + + "If a bundle name contains spaces, then it must be surrounded\n" + + "by quotes. It is also possible to specify a precise version if\n" + "more than one version exists, such as:\n" + + "\n" + + " obr javadoc /home/rickhall/tmp \"Bundle Repository\";1.0.0\n" + + "\n" + + "The above example retrieves the javadoc archive of version \"1.0.0\"\n" + + "of the bundle named \"Bundle Repository\" and saves it to the\n" + + "specified local directory."); + out.println(""); + } + else + { + out.println("obr " + HELP_CMD + + " [" + ADDURL_CMD + + " | " + REMOVEURL_CMD + + " | " + LISTURL_CMD + + " | " + LIST_CMD + + " | " + INFO_CMD + + " | " + DEPLOY_CMD + " | " + START_CMD + + " | " + SOURCE_CMD + " | " + JAVADOC_CMD + "]"); + out.println("obr " + ADDURL_CMD + " [ ...]"); + out.println("obr " + REFRESHURL_CMD + " [ ...]"); + out.println("obr " + REMOVEURL_CMD + " [ ...]"); + out.println("obr " + LISTURL_CMD); + out.println("obr " + LIST_CMD + " [" + VERBOSE_SWITCH + "] [ ...]"); + out.println("obr " + INFO_CMD + + " ||[;] ..."); + out.println("obr " + DEPLOY_CMD + + " ||[;] ..."); + out.println("obr " + START_CMD + + " ||[;] ..."); + out.println("obr " + SOURCE_CMD + + " [" + EXTRACT_SWITCH + + "] [;] ..."); + out.println("obr " + JAVADOC_CMD + + " [" + EXTRACT_SWITCH + + "] [;] ..."); + } + } + + private static Resource[] addResourceByVersion(Resource[] revisions, Resource resource) + { + // We want to add the resource into the array of revisions + // in descending version sorted order (i.e., newest first) + Resource[] sorted = null; + if (revisions == null) + { + sorted = new Resource[] { resource }; + } + else + { + Version version = resource.getVersion(); + Version middleVersion = null; + int top = 0, bottom = revisions.length - 1, middle = 0; + while (top <= bottom) + { + middle = (bottom - top) / 2 + top; + middleVersion = revisions[middle].getVersion(); + // Sort in reverse version order. + int cmp = middleVersion.compareTo(version); + if (cmp < 0) + { + bottom = middle - 1; + } + else + { + top = middle + 1; + } + } + + // Ignore duplicates. + if ((top >= revisions.length) || (revisions[top] != resource)) + { + sorted = new Resource[revisions.length + 1]; + System.arraycopy(revisions, 0, sorted, 0, top); + System.arraycopy(revisions, top, sorted, top + 1, revisions.length - top); + sorted[top] = resource; + } + } + return sorted; + } + + private static class ParsedCommand + { + private static final int NAME_IDX = 0; + private static final int VERSION_IDX = 1; + + private boolean m_isResolve = true; + private boolean m_isCheck = false; + private boolean m_isExtract = false; + private boolean m_isVerbose = false; + private String m_tokens = null; + private String m_dir = null; + private String[][] m_targets = new String[0][]; + + public boolean isResolve() + { + return m_isResolve; + } + + public void setResolve(boolean b) + { + m_isResolve = b; + } + + public boolean isCheck() + { + return m_isCheck; + } + + public void setCheck(boolean b) + { + m_isCheck = b; + } + + public boolean isExtract() + { + return m_isExtract; + } + + public void setExtract(boolean b) + { + m_isExtract = b; + } + + public boolean isVerbose() + { + return m_isVerbose; + } + + public void setVerbose(boolean b) + { + m_isVerbose = b; + } + + public String getTokens() + { + return m_tokens; + } + + public void setTokens(String s) + { + m_tokens = s; + } + + public String getDirectory() + { + return m_dir; + } + + public void setDirectory(String s) + { + m_dir = s; + } + + public int getTargetCount() + { + return m_targets.length; + } + + public String getTargetId(int i) + { + if ((i < 0) || (i >= getTargetCount())) + { + return null; + } + return m_targets[i][NAME_IDX]; + } + + public String getTargetVersion(int i) + { + if ((i < 0) || (i >= getTargetCount())) + { + return null; + } + return m_targets[i][VERSION_IDX]; + } + + public void addTarget(String name, String version) + { + String[][] newTargets = new String[m_targets.length + 1][]; + System.arraycopy(m_targets, 0, newTargets, 0, m_targets.length); + newTargets[m_targets.length] = new String[] { name, version }; + m_targets = newTargets; + } + } +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ObrGogoCommand.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ObrGogoCommand.java new file mode 100644 index 00000000000..4d3f7a6ebef --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ObrGogoCommand.java @@ -0,0 +1,831 @@ +/* + * 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.bundlerepository.impl; + +import org.apache.felix.bundlerepository.*; +import org.apache.felix.service.command.Descriptor; +import org.apache.felix.service.command.Parameter; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.Version; + +import java.io.*; +import java.lang.reflect.Array; +import java.net.URL; +import java.net.URLConnection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.Map; +import java.util.TreeMap; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; + +public class ObrGogoCommand +{ + private static final String REPO_ADD = "add"; + private static final String REPO_REMOVE = "remove"; + private static final String REPO_LIST = "list"; + private static final String REPO_REFRESH = "refresh"; + + private static final char VERSION_SEPARATOR = '@'; + + private final BundleContext m_bc; + private final RepositoryAdmin m_repositoryAdmin; + + public ObrGogoCommand(BundleContext bc, RepositoryAdmin repositoryAdmin) + { + m_bc = bc; + m_repositoryAdmin = repositoryAdmin; + } + + private RepositoryAdmin getRepositoryAdmin() + { + return m_repositoryAdmin; + } + + @Descriptor("manage repositories") + public void repos( + @Descriptor("( add | list | refresh | remove )") String action, + @Descriptor("space-delimited list of repository URLs") String[] args) + throws IOException + { + Object svcObj = getRepositoryAdmin(); + if (svcObj == null) + { + return; + } + RepositoryAdmin ra = (RepositoryAdmin) svcObj; + + if (args.length > 0) + { + for (int i = 0; i < args.length; i++) + { + try + { + if (action.equals(REPO_ADD)) + { + ra.addRepository(args[i]); + } + else if (action.equals(REPO_REFRESH)) + { + ra.removeRepository(args[i]); + ra.addRepository(args[i]); + } + else if (action.equals(REPO_REMOVE)) + { + ra.removeRepository(args[i]); + } + else + { + System.out.println("Unknown repository operation: " + action); + } + } + catch (Exception ex) + { + ex.printStackTrace(System.err); + } + } + } + else + { + org.apache.felix.bundlerepository.Repository[] repos = + ra.listRepositories(); + if ((repos != null) && (repos.length > 0)) + { + for (int i = 0; i < repos.length; i++) + { + System.out.println(repos[i].getURI()); + } + } + else + { + System.out.println("No repository URLs are set."); + } + } + } + + @Descriptor("list repository resources") + public void list( + @Descriptor("display all versions") + @Parameter(names={ "-v", "--verbose" }, presentValue="true", + absentValue="false") boolean verbose, + @Descriptor("optional strings used for name matching") String[] args) + throws IOException, InvalidSyntaxException + { + Object svcObj = getRepositoryAdmin(); + if (svcObj == null) + { + return; + } + RepositoryAdmin ra = (RepositoryAdmin) svcObj; + + // Create a filter that will match presentation name or symbolic name. + StringBuffer sb = new StringBuffer(); + if ((args == null) || (args.length == 0)) + { + sb.append("(|(presentationname=*)(symbolicname=*))"); + } + else + { + StringBuffer value = new StringBuffer(); + for (int i = 0; i < args.length; i++) + { + if (i > 0) + { + value.append(" "); + } + value.append(args[i]); + } + sb.append("(|(presentationname=*"); + sb.append(value); + sb.append("*)(symbolicname=*"); + sb.append(value); + sb.append("*))"); + } + // Use filter to get matching resources. + Resource[] resources = ra.discoverResources(sb.toString()); + + // Group the resources by symbolic name in descending version order, + // but keep them in overall sorted order by presentation name. + Map revisionMap = new TreeMap(new Comparator() { + public int compare(Object o1, Object o2) + { + Resource r1 = (Resource) o1; + Resource r2 = (Resource) o2; + // Assume if the symbolic name is equal, then the two are equal, + // since we are trying to aggregate by symbolic name. + int symCompare = r1.getSymbolicName().compareTo(r2.getSymbolicName()); + if (symCompare == 0) + { + return 0; + } + // Otherwise, compare the presentation name to keep them sorted + // by presentation name. If the presentation names are equal, then + // use the symbolic name to differentiate. + int compare = (r1.getPresentationName() == null) + ? -1 + : (r2.getPresentationName() == null) + ? 1 + : r1.getPresentationName().compareToIgnoreCase( + r2.getPresentationName()); + if (compare == 0) + { + return symCompare; + } + return compare; + } + }); + for (int resIdx = 0; (resources != null) && (resIdx < resources.length); resIdx++) + { + Resource[] revisions = (Resource[]) revisionMap.get(resources[resIdx]); + revisionMap.put(resources[resIdx], addResourceByVersion(revisions, resources[resIdx])); + } + + // Print any matching resources. + for (Iterator i = revisionMap.entrySet().iterator(); i.hasNext(); ) + { + Map.Entry entry = (Map.Entry) i.next(); + Resource[] revisions = (Resource[]) entry.getValue(); + String name = revisions[0].getPresentationName(); + name = (name == null) ? revisions[0].getSymbolicName() : name; + System.out.print(name); + + if (verbose && revisions[0].getPresentationName() != null) + { + System.out.print(" [" + revisions[0].getSymbolicName() + "]"); + } + + System.out.print(" ("); + int revIdx = 0; + do + { + if (revIdx > 0) + { + System.out.print(", "); + } + System.out.print(revisions[revIdx].getVersion()); + revIdx++; + } + while (verbose && (revIdx < revisions.length)); + if (!verbose && (revisions.length > 1)) + { + System.out.print(", ..."); + } + System.out.println(")"); + } + + if ((resources == null) || (resources.length == 0)) + { + System.out.println("No matching bundles."); + } + } + + @Descriptor("retrieve resource description from repository") + public void info( + @Descriptor("( | | )[@] ...") + String[] args) + throws IOException, InvalidSyntaxException + { + Object svcObj = getRepositoryAdmin(); + if (svcObj == null) + { + return; + } + RepositoryAdmin ra = (RepositoryAdmin) svcObj; + + for (int argIdx = 0; (args != null) && (argIdx < args.length); argIdx++) + { + // Find the target's bundle resource. + String targetName = args[argIdx]; + String targetVersion = null; + int idx = args[argIdx].indexOf(VERSION_SEPARATOR); + if (idx > 0) + { + targetName = args[argIdx].substring(0, idx); + targetVersion = args[argIdx].substring(idx + 1); + } + Resource[] resources = searchRepository(ra, targetName, targetVersion); + if ((resources == null) || (resources.length == 0)) + { + System.err.println("Unknown bundle and/or version: " + args[argIdx]); + } + else + { + for (int resIdx = 0; resIdx < resources.length; resIdx++) + { + if (resIdx > 0) + { + System.out.println(""); + } + printResource(System.out, resources[resIdx]); + } + } + } + } + + @Descriptor("deploy resource from repository") + public void deploy( + @Descriptor("start deployed bundles") + @Parameter(names={ "-s", "--start" }, presentValue="true", + absentValue="false") boolean start, + @Descriptor("deploy required bundles only") + @Parameter(names={ "-ro", "--required-only" }, presentValue="true", + absentValue="false") boolean requiredOnly, + @Descriptor("( | | )[@] ...") + String[] args) + throws IOException, InvalidSyntaxException + { + Object svcObj = getRepositoryAdmin(); + if (svcObj == null) + { + return; + } + RepositoryAdmin ra = (RepositoryAdmin) svcObj; + + Resolver resolver = ra.resolver(); + for (int argIdx = 0; (args != null) && (argIdx < args.length); argIdx++) + { + // Find the target's bundle resource. + String targetName = args[argIdx]; + String targetVersion = null; + int idx = args[argIdx].indexOf(VERSION_SEPARATOR); + if (idx > 0) + { + targetName = args[argIdx].substring(0, idx); + targetVersion = args[argIdx].substring(idx + 1); + } + Resource resource = selectNewestVersion( + searchRepository(ra, targetName, targetVersion)); + if (resource != null) + { + resolver.add(resource); + } + else + { + System.err.println("Unknown bundle - " + args[argIdx]); + } + } + + if ((resolver.getAddedResources() != null) && + (resolver.getAddedResources().length > 0)) + { + if (resolver.resolve()) + { + System.out.println("Target resource(s):"); + System.out.println(getUnderlineString(19)); + Resource[] resources = resolver.getAddedResources(); + for (int resIdx = 0; (resources != null) && (resIdx < resources.length); resIdx++) + { + System.out.println(" " + resources[resIdx].getPresentationName() + + " (" + resources[resIdx].getVersion() + ")"); + } + resources = resolver.getRequiredResources(); + if ((resources != null) && (resources.length > 0)) + { + System.out.println("\nRequired resource(s):"); + System.out.println(getUnderlineString(21)); + for (int resIdx = 0; resIdx < resources.length; resIdx++) + { + System.out.println(" " + resources[resIdx].getPresentationName() + + " (" + resources[resIdx].getVersion() + ")"); + } + } + if (!requiredOnly) + { + resources = resolver.getOptionalResources(); + if ((resources != null) && (resources.length > 0)) + { + System.out.println("\nOptional resource(s):"); + System.out.println(getUnderlineString(21)); + for (int resIdx = 0; resIdx < resources.length; resIdx++) + { + System.out.println(" " + resources[resIdx].getPresentationName() + + " (" + resources[resIdx].getVersion() + ")"); + } + } + } + + try + { + System.out.print("\nDeploying...\n"); + int options = 0; + if (start) + { + options |= Resolver.START; + } + if (requiredOnly) + { + options |= Resolver.NO_OPTIONAL_RESOURCES; + } + resolver.deploy(options); + System.out.println("done."); + } + catch (IllegalStateException ex) + { + System.err.println(ex); + } + } + else + { + Reason[] reqs = resolver.getUnsatisfiedRequirements(); + if ((reqs != null) && (reqs.length > 0)) + { + System.out.println("Unsatisfied requirement(s):"); + System.out.println(getUnderlineString(27)); + for (int reqIdx = 0; reqIdx < reqs.length; reqIdx++) + { + System.out.println(" " + reqs[reqIdx].getRequirement().getFilter()); + System.out.println(" " + reqs[reqIdx].getResource().getPresentationName()); + } + } + else + { + System.out.println("Could not resolve targets."); + } + } + } + } + + @Descriptor("retrieve resource source code from repository") + public void source( + @Descriptor("extract source code") + @Parameter(names={ "-x", "--extract" }, presentValue="true", + absentValue="false") boolean extract, + @Descriptor("local target directory") File localDir, + @Descriptor("( | | )[@] ...") + String[] args) + throws IOException, InvalidSyntaxException + { + Object svcObj = getRepositoryAdmin(); + if (svcObj == null) + { + return; + } + RepositoryAdmin ra = (RepositoryAdmin) svcObj; + + for (int argIdx = 0; argIdx < args.length; argIdx++) + { + // Find the target's bundle resource. + String targetName = args[argIdx]; + String targetVersion = null; + int idx = args[argIdx].indexOf(VERSION_SEPARATOR); + if (idx > 0) + { + targetName = args[argIdx].substring(0, idx); + targetVersion = args[argIdx].substring(idx + 1); + } + Resource resource = selectNewestVersion( + searchRepository(ra, targetName, targetVersion)); + if (resource == null) + { + System.err.println("Unknown bundle and/or version: " + args[argIdx]); + } + else + { + String srcURI = (String) resource.getProperties().get(Resource.SOURCE_URI); + if (srcURI != null) + { + downloadSource( + System.out, System.err, new URL(srcURI), + localDir, extract); + } + else + { + System.err.println("Missing source URL: " + args[argIdx]); + } + } + } + } + + @Descriptor("retrieve resource JavaDoc from repository") + public void javadoc( + @Descriptor("extract documentation") + @Parameter(names={"-x", "--extract" }, presentValue="true", + absentValue="false") boolean extract, + @Descriptor("local target directory") File localDir, + @Descriptor("( | | )[@] ...") + String[] args) + throws IOException, InvalidSyntaxException + { + Object svcObj = getRepositoryAdmin(); + if (svcObj == null) + { + return; + } + RepositoryAdmin ra = (RepositoryAdmin) svcObj; + + for (int argIdx = 0; argIdx < args.length; argIdx++) + { + // Find the target's bundle resource. + String targetName = args[argIdx]; + String targetVersion = null; + int idx = args[argIdx].indexOf(VERSION_SEPARATOR); + if (idx > 0) + { + targetName = args[argIdx].substring(0, idx); + targetVersion = args[argIdx].substring(idx + 1); + } + Resource resource = selectNewestVersion( + searchRepository(ra, targetName, targetVersion)); + if (resource == null) + { + System.err.println("Unknown bundle and/or version: " + args[argIdx]); + } + else + { + URL docURL = (URL) resource.getProperties().get("javadoc"); + if (docURL != null) + { + downloadSource( + System.out, System.err, docURL, localDir, extract); + } + else + { + System.err.println("Missing javadoc URL: " + args[argIdx]); + } + } + } + } + + private Resource[] searchRepository( + RepositoryAdmin ra, String targetId, String targetVersion) + throws InvalidSyntaxException + { + // Try to see if the targetId is a bundle ID. + try + { + Bundle bundle = m_bc.getBundle(Long.parseLong(targetId)); + if (bundle != null) + { + targetId = bundle.getSymbolicName(); + } + else + { + return null; + } + } + catch (NumberFormatException ex) + { + // It was not a number, so ignore. + } + + // The targetId may be a bundle name or a bundle symbolic name, + // so create the appropriate LDAP query. + StringBuffer sb = new StringBuffer("(|(presentationname="); + sb.append(targetId); + sb.append(")(symbolicname="); + sb.append(targetId); + sb.append("))"); + if (targetVersion != null) + { + sb.insert(0, "(&"); + sb.append("(version="); + sb.append(targetVersion); + sb.append("))"); + } + return ra.discoverResources(sb.toString()); + } + + private Resource selectNewestVersion(Resource[] resources) + { + int idx = -1; + Version v = null; + for (int i = 0; (resources != null) && (i < resources.length); i++) + { + if (i == 0) + { + idx = 0; + v = resources[i].getVersion(); + } + else + { + Version vtmp = resources[i].getVersion(); + if (vtmp.compareTo(v) > 0) + { + idx = i; + v = vtmp; + } + } + } + + return (idx < 0) ? null : resources[idx]; + } + + private void printResource(PrintStream out, Resource resource) + { + String presentationName = resource.getPresentationName(); + if (presentationName == null) + presentationName = resource.getSymbolicName(); + + System.out.println(getUnderlineString(presentationName.length())); + out.println(presentationName); + System.out.println(getUnderlineString(presentationName.length())); + + Map map = resource.getProperties(); + for (Iterator iter = map.entrySet().iterator(); iter.hasNext(); ) + { + Map.Entry entry = (Map.Entry) iter.next(); + if (entry.getValue().getClass().isArray()) + { + out.println(entry.getKey() + ":"); + for (int j = 0; j < Array.getLength(entry.getValue()); j++) + { + out.println(" " + Array.get(entry.getValue(), j)); + } + } + else + { + out.println(entry.getKey() + ": " + entry.getValue()); + } + } + + Requirement[] reqs = resource.getRequirements(); + if ((reqs != null) && (reqs.length > 0)) + { + out.println("Requires:"); + for (int i = 0; i < reqs.length; i++) + { + out.println(" " + reqs[i].getFilter()); + } + } + + Capability[] caps = resource.getCapabilities(); + if ((caps != null) && (caps.length > 0)) + { + out.println("Capabilities:"); + for (int i = 0; i < caps.length; i++) + { + out.println(" " + caps[i].getPropertiesAsMap()); + } + } + } + + private static Resource[] addResourceByVersion(Resource[] revisions, Resource resource) + { + // We want to add the resource into the array of revisions + // in descending version sorted order (i.e., newest first) + Resource[] sorted = null; + if (revisions == null) + { + sorted = new Resource[] { resource }; + } + else + { + Version version = resource.getVersion(); + Version middleVersion = null; + int top = 0, bottom = revisions.length - 1, middle = 0; + while (top <= bottom) + { + middle = (bottom - top) / 2 + top; + middleVersion = revisions[middle].getVersion(); + // Sort in reverse version order. + int cmp = middleVersion.compareTo(version); + if (cmp < 0) + { + bottom = middle - 1; + } + else + { + top = middle + 1; + } + } + + // Ignore duplicates. + if ((top >= revisions.length) || (revisions[top] != resource)) + { + sorted = new Resource[revisions.length + 1]; + System.arraycopy(revisions, 0, sorted, 0, top); + System.arraycopy(revisions, top, sorted, top + 1, revisions.length - top); + sorted[top] = resource; + } + } + return sorted; + } + + private final static StringBuffer m_sb = new StringBuffer(); + + public static String getUnderlineString(int len) + { + synchronized (m_sb) + { + m_sb.delete(0, m_sb.length()); + for (int i = 0; i < len; i++) + { + m_sb.append('-'); + } + return m_sb.toString(); + } + } + + public static void downloadSource( + PrintStream out, PrintStream err, + URL srcURL, File localDir, boolean extract) + { + // Get the file name from the URL. + String fileName = (srcURL.getFile().lastIndexOf('/') > 0) + ? srcURL.getFile().substring(srcURL.getFile().lastIndexOf('/') + 1) + : srcURL.getFile(); + + try + { + out.println("Connecting..."); + + if (!localDir.exists()) + { + err.println("Destination directory does not exist."); + } + File file = new File(localDir, fileName); + + OutputStream os = new FileOutputStream(file); + URLConnection conn = srcURL.openConnection(); + setProxyAuth(conn); + int total = conn.getContentLength(); + InputStream is = conn.getInputStream(); + + if (total > 0) + { + out.println("Downloading " + fileName + + " ( " + total + " bytes )."); + } + else + { + out.println("Downloading " + fileName + "."); + } + byte[] buffer = new byte[4096]; + for (int len = is.read(buffer); len > 0; len = is.read(buffer)) + { + os.write(buffer, 0, len); + } + + os.close(); + is.close(); + + if (extract) + { + is = new FileInputStream(file); + JarInputStream jis = new JarInputStream(is); + out.println("Extracting..."); + unjar(jis, localDir); + jis.close(); + file.delete(); + } + } + catch (Exception ex) + { + err.println(ex); + } + } + + public static void setProxyAuth(URLConnection conn) throws IOException + { + // Support for http proxy authentication + String auth = System.getProperty("http.proxyAuth"); + if ((auth != null) && (auth.length() > 0)) + { + if ("http".equals(conn.getURL().getProtocol()) + || "https".equals(conn.getURL().getProtocol())) + { + String base64 = Base64Encoder.base64Encode(auth); + conn.setRequestProperty("Proxy-Authorization", "Basic " + base64); + } + } + } + + public static void unjar(JarInputStream jis, File dir) + throws IOException + { + // Reusable buffer. + byte[] buffer = new byte[4096]; + + // Loop through JAR entries. + for (JarEntry je = jis.getNextJarEntry(); + je != null; + je = jis.getNextJarEntry()) + { + if (je.getName().startsWith("/")) + { + throw new IOException("JAR resource cannot contain absolute paths."); + } + + File target = new File(dir, je.getName()); + + // Check to see if the JAR entry is a directory. + if (je.isDirectory()) + { + if (!target.exists()) + { + if (!target.mkdirs()) + { + throw new IOException("Unable to create target directory: " + + target); + } + } + // Just continue since directories do not have content to copy. + continue; + } + + int lastIndex = je.getName().lastIndexOf('/'); + String name = (lastIndex >= 0) ? + je.getName().substring(lastIndex + 1) : je.getName(); + String destination = (lastIndex >= 0) ? + je.getName().substring(0, lastIndex) : ""; + + // JAR files use '/', so convert it to platform separator. + destination = destination.replace('/', File.separatorChar); + copy(jis, dir, name, destination, buffer); + } + } + + public static void copy( + InputStream is, File dir, String destName, String destDir, byte[] buffer) + throws IOException + { + if (destDir == null) + { + destDir = ""; + } + + // Make sure the target directory exists and + // that is actually a directory. + File targetDir = new File(dir, destDir); + if (!targetDir.exists()) + { + if (!targetDir.mkdirs()) + { + throw new IOException("Unable to create target directory: " + + targetDir); + } + } + else if (!targetDir.isDirectory()) + { + throw new IOException("Target is not a directory: " + + targetDir); + } + + BufferedOutputStream bos = new BufferedOutputStream( + new FileOutputStream(new File(targetDir, destName))); + int count = 0; + while ((count = is.read(buffer)) > 0) + { + bos.write(buffer, 0, count); + } + bos.close(); + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ObrURLStreamHandlerService.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ObrURLStreamHandlerService.java new file mode 100644 index 00000000000..1e38bd1e87b --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ObrURLStreamHandlerService.java @@ -0,0 +1,324 @@ +/* + * 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.bundlerepository.impl; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.apache.felix.utils.log.Logger; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.Version; +import org.apache.felix.bundlerepository.*; +import org.osgi.service.url.AbstractURLStreamHandlerService; + +/** + * Simple {@link URLStreamHandler} which is able to handle + * obr urls. The urls must be conform the following schema: + * + * obr:/// + * + * Example: + * + * obr://org.apache.felix.javax.servlet/1240305961998 + * + * + * Update to the bundle is done + * + */ +public class ObrURLStreamHandlerService extends AbstractURLStreamHandlerService +{ + /** + * Syntax for the url; to be shown on exception messages. + */ + private static final String SYNTAX = "obr:['/']"; + /** + * Property defining the obr update strategy + */ + public static final String OBR_UPDATE_STRATEGY = "obr.update.strategy"; + /** + * The BundleContext to search for the bundles. + */ + private final BundleContext m_bundleContext; + /** + * The RepositoryAdmin to query for the actual url + * for a bundle. + */ + private final RepositoryAdmin m_reRepositoryAdmin; + /** + * Logger to use. + */ + private final Logger m_logger; + /** + * The update strategy to use. + * Default: newest + */ + private String m_updateStrategy = "newest"; + + /** + * Constructor + * + * @param context context to use + * @param admin admin to use + */ + public ObrURLStreamHandlerService(BundleContext context, org.apache.felix.bundlerepository.RepositoryAdmin admin) + { + m_bundleContext = context; + m_reRepositoryAdmin = admin; + m_logger = new Logger(context); + if (m_bundleContext.getProperty(OBR_UPDATE_STRATEGY) != null) + { + this.m_updateStrategy = m_bundleContext.getProperty(OBR_UPDATE_STRATEGY); + } + } + + /** + * {@inheritDoc} + * + * This implementation looks up the bundle with the given + * url set as location String within the current {@link BundleContext}. + * The real url for this bundle is determined afterwards via the + * {@link RepositoryAdmin}. + */ + public URLConnection openConnection(URL u) throws IOException + { + String url = u.toExternalForm(); + + URL remoteURL = null; + + try + { + Bundle[] bundles = m_bundleContext.getBundles(); + + int i = 0; + while ((remoteURL == null) && (i < bundles.length)) + { + if (url.equals(bundles[i].getLocation())) + { + remoteURL = getRemoteUrlForBundle(bundles[i]); + } + i++; + } + + if (remoteURL == null) + { + String path = u.getPath(); + remoteURL = getRemoteObrInstallUrl(path); + } + } + catch (InvalidSyntaxException e) + { + throw (IOException) new IOException().initCause(e); + } + + return remoteURL.openConnection(); + + } + + /** + * Assume the URL is a query URL and try to find a matching resource. + * + * Note: the code from the below method comes from OPS4j Pax URL handler + * + * @param path the OBR url path + * @return the remote URL of the resolved bundle + * @throws IOException if an error occurs + */ + private URL getRemoteObrInstallUrl(String path) throws IOException, InvalidSyntaxException + { + if( path == null || path.trim().length() == 0 ) + { + throw new MalformedURLException( "Path cannot be null or empty. Syntax " + SYNTAX ); + } + final String[] segments = path.split( "/" ); + if( segments.length > 2 ) + { + throw new MalformedURLException( "Path cannot contain more then one '/'. Syntax " + SYNTAX ); + } + final StringBuffer buffer = new StringBuffer(); + // add bundle symbolic name filter + buffer.append( "(symbolicname=" ).append( segments[ 0 ] ).append( ")" ); + if( !validateFilter( buffer.toString() ) ) + { + throw new MalformedURLException( "Invalid symbolic name value." ); + } + // add bundle version filter + if( segments.length > 1 ) + { + buffer.insert( 0, "(&" ).append( "(version=" ).append( segments[ 1 ] ).append( "))" ); + if( !validateFilter( buffer.toString() ) ) + { + throw new MalformedURLException( "Invalid version value." ); + } + } + Resource[] discoverResources = + m_reRepositoryAdmin.discoverResources(buffer.toString()); + if (discoverResources == null || discoverResources.length == 0) + { + throw new IOException( "No resource found for filter [" + buffer.toString() + "]" ); + } + ResourceSelectionStrategy strategy = new NewestSelectionStrategy(m_logger); + Resource selected = strategy.selectOne(Version.emptyVersion, discoverResources); + + return new URL(selected.getURI()); + } + + private boolean validateFilter(String filter) { + try + { + FrameworkUtil.createFilter(filter); + return true; + } + catch (InvalidSyntaxException e) + { + return false; + } + } + + /** + * Determines the remote url for the given bundle according to + * the configured {@link ResourceSelectionStrategy}. + * + * @param bundle bundle + * @return remote url + * @throws IOException if something went wrong + */ + private URL getRemoteUrlForBundle(Bundle bundle) throws IOException, InvalidSyntaxException + { + String symbolicName = bundle.getSymbolicName(); + String version = (String) bundle.getHeaders().get(Constants.BUNDLE_VERSION); + + StringBuffer buffer = new StringBuffer(); + buffer.append("(symbolicname="); + buffer.append(symbolicName); + buffer.append(")"); + + Resource[] discoverResources = + m_reRepositoryAdmin.discoverResources(buffer.toString()); + if (discoverResources == null || discoverResources.length == 0) + { + throw new IOException( "No resource found for filter [" + buffer.toString() + "]" ); + } + + ResourceSelectionStrategy strategy = getStrategy(m_updateStrategy); + Resource selected = strategy.selectOne( + Version.parseVersion(version), discoverResources); + + return new URL(selected.getURI()); + } + + private ResourceSelectionStrategy getStrategy(String strategy) + { + m_logger.log(Logger.LOG_DEBUG, "Using ResourceSelectionStrategy: " + strategy); + + if ("same".equals(strategy)) + { + return new SameSelectionStrategy(m_logger); + } + else if ("newest".equals(strategy)) + { + return new NewestSelectionStrategy(m_logger); + } + + throw new RuntimeException("Could not determine obr update strategy : " + strategy); + } + + /** + * Abstract class for Resource Selection Strategies + */ + private static abstract class ResourceSelectionStrategy + { + private final Logger m_logger; + + ResourceSelectionStrategy(Logger logger) + { + m_logger = logger; + } + + Logger getLogger() + { + return m_logger; + } + + final Resource selectOne(Version currentVersion, Resource[] resources) + { + SortedMap sortedResources = new TreeMap(); + for (int i = 0; i < resources.length; i++) + { + sortedResources.put(resources[i].getVersion(), resources[i]); + } + + Version versionToUse = determineVersion(currentVersion, sortedResources); + + m_logger.log(Logger.LOG_DEBUG, + "Using Version " + versionToUse + " for bundle " + + resources[0].getSymbolicName()); + + return (Resource) sortedResources.get(versionToUse); + } + + abstract Version determineVersion(Version currentVersion, SortedMap sortedResources); + } + + /** + * Strategy returning the current version. + */ + static class SameSelectionStrategy extends ResourceSelectionStrategy + { + SameSelectionStrategy(Logger logger) + { + super(logger); + } + + /** + * {@inheritDoc} + */ + Version determineVersion(Version currentVersion, SortedMap sortedResources) + { + return currentVersion; + } + } + + /** + * Strategy returning the newest entry. + */ + static class NewestSelectionStrategy extends ResourceSelectionStrategy + { + NewestSelectionStrategy(Logger logger) + { + super(logger); + } + + /** + * {@inheritDoc} + */ + Version determineVersion(Version currentVersion, SortedMap sortedResources) + { + return (Version) sortedResources.lastKey(); + } + } +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/PropertyImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/PropertyImpl.java new file mode 100644 index 00000000000..3c4a25b5f5f --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/PropertyImpl.java @@ -0,0 +1,110 @@ +/* + * 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.bundlerepository.impl; + +import java.net.URI; +import java.net.URL; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.StringTokenizer; + +import org.apache.felix.bundlerepository.Property; +import org.apache.felix.utils.version.VersionTable; + +public class PropertyImpl implements Property +{ + private final String name; + private final String type; + private final String value; + + public PropertyImpl(String name, String type, String value) + { + this.name = name; + this.type = type; + this.value = value; + } + + public String getName() + { + return name; + } + + public String getType() + { + return type; + } + + public String getValue() + { + return value; + } + + public Object getConvertedValue() + { + return convert(value, type); + } + + private static Object convert(String value, String type) + { + try + { + if (value != null && type != null) + { + if (VERSION.equalsIgnoreCase(type)) + { + return VersionTable.getVersion(value); + } + else if (URI.equalsIgnoreCase(type)) + { + return new URI(value); + } + else if (URL.equalsIgnoreCase(type)) + { + return new URL(value); + } + else if (LONG.equalsIgnoreCase(type)) + { + return new Long(value); + } + else if (DOUBLE.equalsIgnoreCase(type)) + { + return new Double(value); + } + else if (SET.equalsIgnoreCase(type)) + { + StringTokenizer st = new StringTokenizer(value, ","); + Set s = new HashSet(); + while (st.hasMoreTokens()) + { + s.add(st.nextToken().trim()); + } + return s; + } + } + return value; + } + catch (Exception e) + { + IllegalArgumentException ex = new IllegalArgumentException(); + ex.initCause(e); + throw ex; + } + } +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/PullParser.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/PullParser.java new file mode 100644 index 00000000000..d91b2ec0dd3 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/PullParser.java @@ -0,0 +1,410 @@ +/* + * 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.bundlerepository.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.net.URI; + +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** + * Repository XML xml based on StaX + */ +public class PullParser extends RepositoryParser +{ + + public PullParser() + { + } + + public RepositoryImpl parseRepository(InputStream is, URI baseUri) throws Exception + { + XmlPullParser reader = new KXmlParser(); + + // The spec-based Repository XML uses namespaces, so switch this on... + reader.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + + reader.setInput(is, null); + int event = reader.nextTag(); + if (event != XmlPullParser.START_TAG || !REPOSITORY.equals(reader.getName())) + { + throw new Exception("Expected element 'repository' at the root of the document"); + } + + RepositoryImpl repo; + if ("http://www.osgi.org/xmlns/repository/v1.0.0".equals(reader.getNamespace())) { + // TODO there are a bunch of other methods here that create a parser, should they be updated too? + // at the very least they should be made namespace-aware too, so that parsing is the same no matter + // how its initiated. + return SpecXMLPullParser.parse(reader, baseUri); + } else { + // We're parsing the old + return parse(reader); + } + } + + public RepositoryImpl parseRepository(Reader r) throws Exception + { + XmlPullParser reader = new KXmlParser(); + reader.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + + reader.setInput(r); + int event = reader.nextTag(); + if (event != XmlPullParser.START_TAG || !REPOSITORY.equals(reader.getName())) + { + throw new Exception("Expected element 'repository' at the root of the document"); + } + return parse(reader); + } + + public ResourceImpl parseResource(Reader r) throws Exception + { + XmlPullParser reader = new KXmlParser(); + reader.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + + reader.setInput(r); + int event = reader.nextTag(); + if (event != XmlPullParser.START_TAG || !RESOURCE.equals(reader.getName())) + { + throw new Exception("Expected element 'resource'"); + } + return parseResource(reader); + } + + public CapabilityImpl parseCapability(Reader r) throws Exception + { + XmlPullParser reader = new KXmlParser(); + reader.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + + reader.setInput(r); + int event = reader.nextTag(); + if (event != XmlPullParser.START_TAG || !CAPABILITY.equals(reader.getName())) + { + throw new Exception("Expected element 'capability'"); + } + return parseCapability(reader); + } + + public PropertyImpl parseProperty(Reader r) throws Exception + { + XmlPullParser reader = new KXmlParser(); + reader.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + + reader.setInput(r); + int event = reader.nextTag(); + if (event != XmlPullParser.START_TAG || !P.equals(reader.getName())) + { + throw new Exception("Expected element 'p'"); + } + return parseProperty(reader); + } + + public RequirementImpl parseRequirement(Reader r) throws Exception + { + XmlPullParser reader = new KXmlParser(); + reader.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + + reader.setInput(r); + int event = reader.nextTag(); + if (event != XmlPullParser.START_TAG || !REQUIRE.equals(reader.getName())) + { + throw new Exception("Expected element 'require'"); + } + return parseRequire(reader); + } + + public RepositoryImpl parse(XmlPullParser reader) throws Exception + { + RepositoryImpl repository = new RepositoryImpl(); + for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) + { + String name = reader.getAttributeName(i); + String value = reader.getAttributeValue(i); + if (NAME.equals(name)) + { + repository.setName(value); + } + else if (LASTMODIFIED.equals(name)) + { + repository.setLastModified(value); + } + } + int event; + while ((event = reader.nextTag()) == XmlPullParser.START_TAG) + { + String element = reader.getName(); + if (REFERRAL.equals(element)) + { + Referral referral = parseReferral(reader); + repository.addReferral(referral); + } + else if (RESOURCE.equals(element)) + { + ResourceImpl resource = parseResource(reader); + repository.addResource(resource); + } + else + { + ignoreTag(reader); + } + } + // Sanity check + sanityCheckEndElement(reader, event, REPOSITORY); + return repository; + } + + static void sanityCheckEndElement(XmlPullParser reader, int event, String element) + { + if (event != XmlPullParser.END_TAG || !element.equals(reader.getName())) + { + throw new IllegalStateException("Unexpected state while finishing element " + element); + } + } + + public Referral parseReferral(XmlPullParser reader) throws Exception + { + Referral referral = new Referral(); + for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) + { + String name = reader.getAttributeName(i); + String value = reader.getAttributeValue(i); + if (DEPTH.equals(name)) + { + referral.setDepth(value); + } + else if (URL.equals(name)) + { + referral.setUrl(value); + } + } + sanityCheckEndElement(reader, reader.nextTag(), REFERRAL); + return referral; + } + + public ResourceImpl parseResource(XmlPullParser reader) throws Exception + { + ResourceImpl resource = new ResourceImpl(); + try + { + for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) + { + resource.put(reader.getAttributeName(i), reader.getAttributeValue(i)); + } + int event; + while ((event = reader.nextTag()) == XmlPullParser.START_TAG) + { + String element = reader.getName(); + if (CATEGORY.equals(element)) + { + String category = parseCategory(reader); + resource.addCategory(category); + } + else if (CAPABILITY.equals(element)) + { + CapabilityImpl capability = parseCapability(reader); + resource.addCapability(capability); + } + else if (REQUIRE.equals(element)) + { + RequirementImpl requirement = parseRequire(reader); + resource.addRequire(requirement); + } + else + { + StringBuffer sb = null; + String type = reader.getAttributeValue(null, "type"); + while ((event = reader.next()) != XmlPullParser.END_TAG) + { + switch (event) + { + case XmlPullParser.START_TAG: + throw new Exception("Unexpected element inside element"); + case XmlPullParser.TEXT: + if (sb == null) + { + sb = new StringBuffer(); + } + sb.append(reader.getText()); + break; + } + } + if (sb != null) + { + resource.put(element, sb.toString().trim(), type); + } + } + } + // Sanity check + if (event != XmlPullParser.END_TAG || !RESOURCE.equals(reader.getName())) + { + throw new Exception("Unexpected state"); + } + return resource; + } + catch (Exception e) + { + throw new Exception("Error while parsing resource " + resource.getId() + " at line " + reader.getLineNumber() + " and column " + reader.getColumnNumber(), e); + } + } + + public String parseCategory(XmlPullParser reader) throws IOException, XmlPullParserException + { + String id = null; + for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) + { + if (ID.equals(reader.getAttributeName(i))) + { + id = reader.getAttributeValue(i); + } + } + sanityCheckEndElement(reader, reader.nextTag(), CATEGORY); + return id; + } + + public CapabilityImpl parseCapability(XmlPullParser reader) throws Exception + { + CapabilityImpl capability = new CapabilityImpl(); + for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) + { + String name = reader.getAttributeName(i); + String value = reader.getAttributeValue(i); + if (NAME.equals(name)) + { + capability.setName(value); + } + } + int event; + while ((event = reader.nextTag()) == XmlPullParser.START_TAG) + { + String element = reader.getName(); + if (P.equals(element)) + { + PropertyImpl prop = parseProperty(reader); + capability.addProperty(prop); + } + else + { + ignoreTag(reader); + } + } + // Sanity check + sanityCheckEndElement(reader, event, CAPABILITY); + return capability; + } + + public PropertyImpl parseProperty(XmlPullParser reader) throws Exception + { + String n = null, t = null, v = null; + for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) + { + String name = reader.getAttributeName(i); + String value = reader.getAttributeValue(i); + if (N.equals(name)) + { + n = value; + } + else if (T.equals(name)) + { + t = value; + } + else if (V.equals(name)) + { + v = value; + } + } + PropertyImpl prop = new PropertyImpl(n, t, v); + // Sanity check + sanityCheckEndElement(reader, reader.nextTag(), P); + return prop; + } + + public RequirementImpl parseRequire(XmlPullParser reader) throws Exception + { + RequirementImpl requirement = new RequirementImpl(); + for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) + { + String name = reader.getAttributeName(i); + String value = reader.getAttributeValue(i); + if (NAME.equals(name)) + { + requirement.setName(value); + } + else if (FILTER.equals(name)) + { + requirement.setFilter(value); + } + else if (EXTEND.equals(name)) + { + requirement.setExtend(Boolean.valueOf(value).booleanValue()); + } + else if (MULTIPLE.equals(name)) + { + requirement.setMultiple(Boolean.valueOf(value).booleanValue()); + } + else if (OPTIONAL.equals(name)) + { + requirement.setOptional(Boolean.valueOf(value).booleanValue()); + } + } + int event; + StringBuffer sb = null; + while ((event = reader.next()) != XmlPullParser.END_TAG) + { + switch (event) + { + case XmlPullParser.START_TAG: + throw new Exception("Unexpected element inside element"); + case XmlPullParser.TEXT: + if (sb == null) + { + sb = new StringBuffer(); + } + sb.append(reader.getText()); + break; + } + } + if (sb != null) + { + requirement.addText(sb.toString()); + } + // Sanity check + sanityCheckEndElement(reader, event, REQUIRE); + return requirement; + } + + static void ignoreTag(XmlPullParser reader) throws IOException, XmlPullParserException { + int level = 1; + while (level > 0) + { + int eventType = reader.next(); + if (eventType == XmlPullParser.START_TAG) + { + level++; + } + else if (eventType == XmlPullParser.END_TAG) + { + level--; + } + } + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ReasonImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ReasonImpl.java new file mode 100644 index 00000000000..d3bc3ca9753 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ReasonImpl.java @@ -0,0 +1,46 @@ +/* + * 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.bundlerepository.impl; + +import org.apache.felix.bundlerepository.Reason; +import org.apache.felix.bundlerepository.Requirement; +import org.apache.felix.bundlerepository.Resource; + +public class ReasonImpl implements Reason +{ + + private final Resource resource; + private final Requirement requirement; + + public ReasonImpl(Resource resource, Requirement requirement) + { + this.resource = resource; + this.requirement = requirement; + } + + public Resource getResource() + { + return resource; + } + + public Requirement getRequirement() + { + return requirement; + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/Referral.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/Referral.java new file mode 100644 index 00000000000..8d6317b2231 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/Referral.java @@ -0,0 +1,52 @@ +/* + * 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.bundlerepository.impl; + +public class Referral +{ + private int m_depth = 1; + private String m_url; + + public int getDepth() + { + return m_depth; + } + + public String getUrl() + { + return m_url; + } + + public void setUrl(String url) + { + m_url = url; + } + + public void setDepth(String depth) + { + try + { + m_depth = Integer.parseInt(depth); + } + catch (NumberFormatException nfe) + { + // don't care, and don't change current value + } + } +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/RepositoryAdminImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/RepositoryAdminImpl.java new file mode 100644 index 00000000000..13d2e04acfa --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/RepositoryAdminImpl.java @@ -0,0 +1,294 @@ +/* + * 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.bundlerepository.impl; + +import java.net.MalformedURLException; +import java.net.URL; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import org.apache.felix.bundlerepository.Capability; +import org.apache.felix.bundlerepository.DataModelHelper; +import org.apache.felix.bundlerepository.Repository; +import org.apache.felix.bundlerepository.RepositoryAdmin; +import org.apache.felix.bundlerepository.Requirement; +import org.apache.felix.bundlerepository.Resolver; +import org.apache.felix.bundlerepository.Resource; +import org.apache.felix.utils.collections.MapToDictionary; +import org.apache.felix.utils.log.Logger; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; + +public class RepositoryAdminImpl implements RepositoryAdmin +{ + private final BundleContext m_context; + private final Logger m_logger; + private final SystemRepositoryImpl m_system; + private final LocalRepositoryImpl m_local; + private final DataModelHelper m_helper = new DataModelHelperImpl(); + private Map m_repoMap = new LinkedHashMap(); + private boolean m_initialized = false; + + // Reusable comparator for sorting resources by name. + private Comparator m_nameComparator = new ResourceComparator(); + + public static final String REPOSITORY_URL_PROP = "obr.repository.url"; + public static final String EXTERN_REPOSITORY_TAG = "extern-repositories"; + + public RepositoryAdminImpl(BundleContext context, Logger logger) + { + m_context = context; + m_logger = logger; + m_system = new SystemRepositoryImpl(context, logger); + m_local = new LocalRepositoryImpl(context, logger); + } + + public DataModelHelper getHelper() + { + return m_helper; + } + + public Repository getLocalRepository() + { + return m_local; + } + + public Repository getSystemRepository() + { + return m_system; + } + + public void dispose() + { + m_local.dispose(); + } + + public Repository addRepository(String uri) throws Exception + { + return addRepository(new URL(uri)); + } + + public Repository addRepository(URL url) throws Exception + { + return addRepository(url, Integer.MAX_VALUE); + } + + public synchronized RepositoryImpl addRepository(final URL url, int hopCount) throws Exception + { + initialize(); + + // If the repository URL is a duplicate, then we will just + // replace the existing repository object with a new one, + // which is effectively the same as refreshing the repository. + try + { + RepositoryImpl repository = (RepositoryImpl) AccessController.doPrivileged(new PrivilegedExceptionAction() + { + public Object run() throws Exception + { + return m_helper.repository(url); + } + }); + m_repoMap.put(url.toExternalForm(), repository); + + // resolve referrals + hopCount--; + if (hopCount > 0 && repository.getReferrals() != null) + { + for (int i = 0; i < repository.getReferrals().length; i++) + { + Referral referral = repository.getReferrals()[i]; + + URL referralUrl = new URL(url, referral.getUrl()); + hopCount = (referral.getDepth() > hopCount) ? hopCount : referral.getDepth(); + + addRepository(referralUrl, hopCount); + } + } + + return repository; + } + catch (PrivilegedActionException ex) + { + throw (Exception) ex.getCause(); + } + + } + + public synchronized boolean removeRepository(String uri) + { + initialize(); + + try + { + URL url = new URL(uri); + return m_repoMap.remove(url.toExternalForm()) != null; + } + catch (MalformedURLException e) + { + return m_repoMap.remove(uri) != null; + } + } + + public synchronized Repository[] listRepositories() + { + initialize(); + + return (Repository[]) m_repoMap.values().toArray(new Repository[m_repoMap.size()]); + } + + public synchronized Resolver resolver() + { + initialize(); + + List repositories = new ArrayList(); + repositories.add(m_system); + repositories.add(m_local); + repositories.addAll(m_repoMap.values()); + return resolver((Repository[]) repositories.toArray(new Repository[repositories.size()])); + } + + public synchronized Resolver resolver(Repository[] repositories) + { + initialize(); + + if (repositories == null) + { + return resolver(); + } + return new ResolverImpl(m_context, repositories, m_logger); + } + + public synchronized Resource[] discoverResources(String filterExpr) throws InvalidSyntaxException + { + initialize(); + + Filter filter = filterExpr != null ? m_helper.filter(filterExpr) : null; + Resource[] resources; + MapToDictionary dict = new MapToDictionary(null); + Repository[] repos = listRepositories(); + List matchList = new ArrayList(); + for (int repoIdx = 0; (repos != null) && (repoIdx < repos.length); repoIdx++) + { + resources = repos[repoIdx].getResources(); + for (int resIdx = 0; (resources != null) && (resIdx < resources.length); resIdx++) + { + dict.setSourceMap(resources[resIdx].getProperties()); + if (filter == null || filter.match(dict)) + { + matchList.add(resources[resIdx]); + } + } + } + + // Convert matching resources to an array an sort them by name. + resources = (Resource[]) matchList.toArray(new Resource[matchList.size()]); + Arrays.sort(resources, m_nameComparator); + return resources; + } + + public synchronized Resource[] discoverResources(Requirement[] requirements) + { + initialize(); + + Resource[] resources = null; + Repository[] repos = listRepositories(); + List matchList = new ArrayList(); + for (int repoIdx = 0; (repos != null) && (repoIdx < repos.length); repoIdx++) + { + resources = repos[repoIdx].getResources(); + for (int resIdx = 0; (resources != null) && (resIdx < resources.length); resIdx++) + { + boolean match = true; + for (int reqIdx = 0; (requirements != null) && (reqIdx < requirements.length); reqIdx++) + { + boolean reqMatch = false; + Capability[] caps = resources[resIdx].getCapabilities(); + for (int capIdx = 0; (caps != null) && (capIdx < caps.length); capIdx++) + { + if (requirements[reqIdx].isSatisfied(caps[capIdx])) + { + reqMatch = true; + break; + } + } + match &= reqMatch; + if (!match) + { + break; + } + } + if (match) + { + matchList.add(resources[resIdx]); + } + } + } + + // Convert matching resources to an array an sort them by name. + resources = (Resource[]) matchList.toArray(new Resource[matchList.size()]); + Arrays.sort(resources, m_nameComparator); + return resources; + } + + private void initialize() + { + if (m_initialized) + { + return; + } + m_initialized = true; + + // First check the repository URL config property. + String urlStr = m_context.getProperty(REPOSITORY_URL_PROP); + if (urlStr != null) + { + StringTokenizer st = new StringTokenizer(urlStr); + if (st.countTokens() > 0) + { + while (st.hasMoreTokens()) + { + final String token = st.nextToken(); + try + { + addRepository(token); + } + catch (Exception ex) + { + m_logger.log( + Logger.LOG_WARNING, + "Repository url " + token + " cannot be used. Skipped.", + ex); + } + } + } + } + + } + +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/RepositoryImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/RepositoryImpl.java new file mode 100644 index 00000000000..f2a7d5a520c --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/RepositoryImpl.java @@ -0,0 +1,146 @@ +/* + * 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.bundlerepository.impl; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.apache.felix.bundlerepository.Resource; +import org.apache.felix.bundlerepository.Repository; + +public class RepositoryImpl implements Repository +{ + private String m_name = null; + private long m_lastmodified = System.currentTimeMillis(); + private String m_uri = null; + private Resource[] m_resources = null; + private Referral[] m_referrals = null; + private Set m_resourceSet = new HashSet(); + + public RepositoryImpl() + { + } + + public RepositoryImpl(Resource[] resources) + { + m_resources = resources; + } + + public String getURI() + { + return m_uri; + } + + protected void setURI(String uri) + { + m_uri = uri; + } + + public Resource[] getResources() + { + if (m_resources == null) + { + m_resources = (Resource[]) m_resourceSet.toArray(new Resource[m_resourceSet.size()]); + Arrays.sort(m_resources, new ResourceComparator()); + + } + return m_resources; + } + + public void addResource(Resource resource) + { + // Set resource's repository. + if (resource instanceof ResourceImpl) + { + ((ResourceImpl) resource).setRepository(this); + } + + // Add to resource array. + m_resourceSet.remove(resource); + m_resourceSet.add(resource); + m_resources = null; + } + + public Referral[] getReferrals() + { + return m_referrals; + } + + public void addReferral(Referral referral) throws Exception + { + // Add to resource array. + if (m_referrals == null) + { + m_referrals = new Referral[] { referral }; + } + else + { + Referral[] newResources = new Referral[m_referrals.length + 1]; + System.arraycopy(m_referrals, 0, newResources, 0, m_referrals.length); + newResources[m_referrals.length] = referral; + m_referrals = newResources; + } + } + + public String getName() + { + return m_name; + } + + public void setName(String name) + { + m_name = name; + } + + public long getLastModified() + { + return m_lastmodified; + } + + public void setLastModified(long lastModified) + { + m_lastmodified = lastModified; + } + + public void setLastModified(String s) + { + SimpleDateFormat format = new SimpleDateFormat("yyyyMMddhhmmss.SSS"); + try + { + m_lastmodified = format.parse(s).getTime(); + } + catch (ParseException ex) + { + } + } + + /** + * Default setter method when setting parsed data from the XML file, + * which currently ignores everything. + **/ + protected Object put(Object key, Object value) + { + // Ignore everything for now. + return null; + } + +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/RepositoryParser.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/RepositoryParser.java new file mode 100644 index 00000000000..3fb28cc6898 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/RepositoryParser.java @@ -0,0 +1,87 @@ +/* + * 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.bundlerepository.impl; + +import java.io.InputStream; +import java.io.Reader; +import java.net.URI; + +public abstract class RepositoryParser +{ + public static final String REPOSITORY = "repository"; + public static final String NAME = "name"; + public static final String LASTMODIFIED = "lastmodified"; + public static final String REFERRAL = "referral"; + public static final String RESOURCE = "resource"; + public static final String DEPTH = "depth"; + public static final String URL = "url"; + public static final String CATEGORY = "category"; + public static final String ID = "id"; + public static final String CAPABILITY = "capability"; + public static final String REQUIRE = "require"; + public static final String P = "p"; + public static final String N = "n"; + public static final String T = "t"; + public static final String V = "v"; + public static final String FILTER = "filter"; + public static final String EXTEND = "extend"; + public static final String MULTIPLE = "multiple"; + public static final String OPTIONAL = "optional"; + + public static final String OBR_PARSER_CLASS = "obr.xml.class"; + + public static RepositoryParser getParser() + { + RepositoryParser parser = null; + try + { + String className = Activator.getContext() != null + ? Activator.getContext().getProperty(OBR_PARSER_CLASS) + : System.getProperty(OBR_PARSER_CLASS); + if (className != null && className.length() > 0) + { + parser = (RepositoryParser) Class.forName(className).newInstance(); + } + } + catch (Throwable t) + { + // Ignore + } + if (parser == null) + { + parser = new PullParser(); + + } + return parser; + } + + + public abstract RepositoryImpl parseRepository(InputStream is, URI baseUri) throws Exception; + + public abstract RepositoryImpl parseRepository(Reader r) throws Exception; + + public abstract ResourceImpl parseResource(Reader reader) throws Exception; + + public abstract CapabilityImpl parseCapability(Reader reader) throws Exception; + + public abstract PropertyImpl parseProperty(Reader reader) throws Exception; + + public abstract RequirementImpl parseRequirement(Reader reader) throws Exception; + +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/RequirementImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/RequirementImpl.java new file mode 100644 index 00000000000..81798e03461 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/RequirementImpl.java @@ -0,0 +1,184 @@ +/* + * 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.bundlerepository.impl; + +import java.util.*; +import java.util.regex.Pattern; + +import org.apache.felix.bundlerepository.Capability; +import org.apache.felix.bundlerepository.Requirement; +import org.apache.felix.utils.collections.MapToDictionary; +import org.apache.felix.utils.filter.FilterImpl; +import org.osgi.framework.InvalidSyntaxException; + +public class RequirementImpl implements Requirement +{ + private static final Pattern REMOVE_LT = Pattern.compile("\\(([^<>=~()]*)<([^*=]([^\\\\\\*\\(\\)]|\\\\|\\*|\\(|\\))*)\\)"); + private static final Pattern REMOVE_GT = Pattern.compile("\\(([^<>=~()]*)>([^*=]([^\\\\\\*\\(\\)]|\\\\|\\*|\\(|\\))*)\\)"); + private static final Pattern REMOVE_NV = Pattern.compile("\\(version>=0.0.0\\)"); + + private String m_name = null; + private boolean m_extend = false; + private boolean m_multiple = false; + private boolean m_optional = false; + private FilterImpl m_filter = null; + private String m_comment = null; + private Map m_attributes = Collections.emptyMap(); + private Map m_directives = Collections.emptyMap(); + + public RequirementImpl() + { + } + + public RequirementImpl(String name) + { + setName(name); + } + + public Map getAttributes() + { + return m_attributes; + } + + public void setAttributes(Map attributes) { + m_attributes = Collections.unmodifiableMap(attributes); + } + + public Map getDirectives() + { + return m_directives; + } + + public void setDirectives(Map directives) + { + m_directives = Collections.unmodifiableMap(directives); + } + + public String getName() + { + return m_name; + } + + public void setName(String name) + { + // Name of capabilities and requirements are interned for performances + // (with a very low memory consumption as there are only a handful of values) + m_name = name.intern(); + } + + public String getFilter() + { + return m_filter.toString(); + } + + public void setFilter(String filter) + { + try + { + String nf = REMOVE_LT.matcher(filter).replaceAll("(!($1>=$2))"); + nf = REMOVE_GT.matcher(nf).replaceAll("(!($1<=$2))"); + nf = REMOVE_NV.matcher(nf).replaceAll(""); + m_filter = FilterImpl.newInstance(nf, true); + } + catch (InvalidSyntaxException e) + { + IllegalArgumentException ex = new IllegalArgumentException(); + ex.initCause(e); + throw ex; + } + } + + public boolean isSatisfied(Capability capability) + { + Dictionary propertyDict = new MapToDictionary(capability.getPropertiesAsMap()); + + return m_name.equals(capability.getName()) && + m_filter.match(propertyDict) && + (m_filter.toString().contains("(mandatory:<*") || propertyDict.get("mandatory:") == null); + } + + public boolean isExtend() + { + return m_extend; + } + + public void setExtend(boolean extend) + { + m_extend = extend; + } + + public boolean isMultiple() + { + return m_multiple; + } + + public void setMultiple(boolean multiple) + { + m_multiple = multiple; + } + + public boolean isOptional() + { + return m_optional; + } + + public void setOptional(boolean optional) + { + m_optional = optional; + } + + public String getComment() + { + return m_comment; + } + + public void addText(String s) + { + m_comment = s; + } + + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (o instanceof Requirement) + { + Requirement r = (Requirement) o; + return m_name.equals(r.getName()) && + (m_optional == r.isOptional()) && + (m_multiple == r.isMultiple()) && + m_filter.toString().equals(r.getFilter()) && + ((m_comment == r.getComment()) || + ((m_comment != null) && (m_comment.equals(r.getComment())))); + } + return false; + } + + public int hashCode() + { + return m_filter.toString().hashCode(); + } + + public String toString() + { + return m_name + ":" + getFilter(); + } +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ResolverImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ResolverImpl.java new file mode 100644 index 00000000000..071c13d5d3c --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ResolverImpl.java @@ -0,0 +1,738 @@ +/* + * 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.bundlerepository.impl; + +import java.net.URL; +import java.util.*; + +import org.apache.felix.bundlerepository.*; +import org.apache.felix.utils.log.Logger; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.Version; + +public class ResolverImpl implements Resolver +{ + private final BundleContext m_context; + private final Logger m_logger; + private final Repository[] m_repositories; + private final Set m_addedSet = new HashSet(); + private final Set m_addedRequirementSet = new HashSet(); + private final Set m_globalCapabilities = new HashSet(); + private final Set m_failedSet = new HashSet(); + private final Set m_resolveSet = new HashSet(); + private final Set m_requiredSet = new HashSet(); + private final Set m_optionalSet = new HashSet(); + private final Map> m_reasonMap = new HashMap>(); + private final Set m_unsatisfiedSet = new HashSet(); + private boolean m_resolved = false; + private long m_resolveTimeStamp; + private int m_resolutionFlags; + + public ResolverImpl(BundleContext context, Repository[] repositories, Logger logger) + { + m_context = context; + m_logger = logger; + m_repositories = repositories; + } + + public synchronized void add(Resource resource) + { + m_resolved = false; + m_addedSet.add(resource); + } + + public synchronized Resource[] getAddedResources() + { + return m_addedSet.toArray(new Resource[m_addedSet.size()]); + } + + public synchronized void add(Requirement requirement) + { + m_resolved = false; + m_addedRequirementSet.add(requirement); + } + + public synchronized Requirement[] getAddedRequirements() + { + return m_addedRequirementSet.toArray(new Requirement[m_addedRequirementSet.size()]); + } + + public void addGlobalCapability(Capability capability) + { + m_globalCapabilities.add(capability); + } + + public Capability[] getGlobalCapabilities() + { + return m_globalCapabilities.toArray(new Capability[m_globalCapabilities.size()]); + } + + public synchronized Resource[] getRequiredResources() + { + if (m_resolved) + { + return m_requiredSet.toArray(new Resource[m_requiredSet.size()]); + } + throw new IllegalStateException("The resources have not been resolved."); + } + + public synchronized Resource[] getOptionalResources() + { + if (m_resolved) + { + return m_optionalSet.toArray(new Resource[m_optionalSet.size()]); + } + throw new IllegalStateException("The resources have not been resolved."); + } + + public synchronized Reason[] getReason(Resource resource) + { + if (m_resolved) + { + List l = m_reasonMap.get(resource); + return l != null ? l.toArray(new Reason[l.size()]) : null; + } + throw new IllegalStateException("The resources have not been resolved."); + } + + public synchronized Reason[] getUnsatisfiedRequirements() + { + if (m_resolved) + { + return m_unsatisfiedSet.toArray(new Reason[m_unsatisfiedSet.size()]); + } + throw new IllegalStateException("The resources have not been resolved."); + } + + protected LocalResource[] getLocalResources() + { + List resources = new ArrayList(); + for (Resource resource : getResources()) + { + if (resource != null && resource.isLocal()) + { + resources.add((LocalResource) resource); + } + } + return resources.toArray(new LocalResource[resources.size()]); + } + + private Resource[] getRemoteResources() + { + List resources = new ArrayList(); + for (Resource resource : getResources()) + { + if (resource != null && !resource.isLocal()) + { + resources.add(resource); + } + } + return resources.toArray(new Resource[resources.size()]); + } + + private Resource[] getResources() + { + List resources = new ArrayList(); + for (int repoIdx = 0; (m_repositories != null) && (repoIdx < m_repositories.length); repoIdx++) + { + boolean isLocal = m_repositories[repoIdx].getURI().equals(Repository.LOCAL); + boolean isSystem = m_repositories[repoIdx].getURI().equals(Repository.SYSTEM); + if (isLocal && (m_resolutionFlags & NO_LOCAL_RESOURCES) != 0) { + continue; + } + if (isSystem && (m_resolutionFlags & NO_SYSTEM_BUNDLE) != 0) { + continue; + } + Collections.addAll(resources, m_repositories[repoIdx].getResources()); + } + return resources.toArray(new Resource[resources.size()]); + } + + public synchronized boolean resolve() + { + return resolve(0); + } + + public synchronized boolean resolve(int flags) + { + // Find resources + Resource[] locals = getLocalResources(); + Resource[] remotes = getRemoteResources(); + + // time of the resolution process start + m_resolveTimeStamp = 0; + for (int repoIdx = 0; (m_repositories != null) && (repoIdx < m_repositories.length); repoIdx++) + { + m_resolveTimeStamp = Math.max(m_resolveTimeStamp, m_repositories[repoIdx].getLastModified()); + } + + // Reset instance values. + m_failedSet.clear(); + m_resolveSet.clear(); + m_requiredSet.clear(); + m_optionalSet.clear(); + m_reasonMap.clear(); + m_unsatisfiedSet.clear(); + m_resolved = true; + m_resolutionFlags = flags; + + boolean result = true; + + // Add a fake resource if needed + if (!m_addedRequirementSet.isEmpty() || !m_globalCapabilities.isEmpty()) + { + ResourceImpl fake = new ResourceImpl(); + for (Capability cap : m_globalCapabilities) { + fake.addCapability(cap); + } + for (Requirement req : m_addedRequirementSet) { + fake.addRequire(req); + } + if (!resolve(fake, locals, remotes, false)) + { + result = false; + } + } + + // Loop through each resource in added list and resolve. + for (Resource aM_addedSet : m_addedSet) { + if (!resolve(aM_addedSet, locals, remotes, false)) { + // If any resource does not resolve, then the + // entire result will be false. + result = false; + } + } + + // Clean up the resulting data structures. + m_requiredSet.removeAll(m_addedSet); + if ((flags & NO_LOCAL_RESOURCES) == 0) + { + m_requiredSet.removeAll(Arrays.asList(locals)); + } + m_optionalSet.removeAll(m_addedSet); + m_optionalSet.removeAll(m_requiredSet); + if ((flags & NO_LOCAL_RESOURCES) == 0) + { + m_optionalSet.removeAll(Arrays.asList(locals)); + } + + // Return final result. + return result; + } + + private boolean resolve(Resource resource, Resource[] locals, Resource[] remotes, boolean optional) + { + boolean result = true; + + // Check for cycles. + if (m_resolveSet.contains(resource) || m_requiredSet.contains(resource) || m_optionalSet.contains(resource)) + { + return true; + } + else if (m_failedSet.contains(resource)) + { + return false; + } + + // Add to resolve map to avoid cycles. + m_resolveSet.add(resource); + + // Resolve the requirements for the resource according to the + // search order of: added, resolving, local and finally remote + // resources. + Requirement[] reqs = resource.getRequirements(); + if (reqs != null) + { + Resource candidate; + for (Requirement req : reqs) { + // Do not resolve optional requirements + if ((m_resolutionFlags & NO_OPTIONAL_RESOURCES) != 0 && req.isOptional()) { + continue; + } + candidate = searchResources(req, m_addedSet); + if (candidate == null) { + candidate = searchResources(req, m_requiredSet); + } + if (candidate == null) { + candidate = searchResources(req, m_optionalSet); + } + if (candidate == null) { + candidate = searchResources(req, m_resolveSet); + } + if (candidate == null) { + List candidateCapabilities = searchResources(req, locals); + candidateCapabilities.addAll(searchResources(req, remotes)); + + // Determine the best candidate available that + // can resolve. + while ((candidate == null) && !candidateCapabilities.isEmpty()) { + ResourceCapability bestCapability = getBestCandidate(candidateCapabilities); + + // Try to resolve the best resource. + if (resolve(bestCapability.getResource(), locals, remotes, optional || req.isOptional())) { + candidate = bestCapability.getResource(); + } else { + candidateCapabilities.remove(bestCapability); + } + } + } + + if ((candidate == null) && !req.isOptional()) { + // The resolve failed. + result = false; + // Associated the current resource to the requirement + // in the unsatisfied requirement set. + m_unsatisfiedSet.add(new ReasonImpl(resource, req)); + } else if (candidate != null) { + + // Try to resolve the candidate. + if (resolve(candidate, locals, remotes, optional || req.isOptional())) { + // The resolved succeeded; record the candidate + // as either optional or required. + if (optional || req.isOptional()) { + m_optionalSet.add(candidate); + m_resolveSet.remove(candidate); + } else { + m_requiredSet.add(candidate); + m_optionalSet.remove(candidate); + m_resolveSet.remove(candidate); + } + + // Add the reason why the candidate was selected. + List reasons = m_reasonMap.get(candidate); + if (reasons == null) { + reasons = new ArrayList(); + m_reasonMap.put(candidate, reasons); + } + reasons.add(new ReasonImpl(resource, req)); + } else { + result = false; + } + } + } + } + + // If the resolve failed, remove the resource from the resolve set and + // add it to the failed set to avoid trying to resolve it again. + if (!result) + { + m_resolveSet.remove(resource); + m_failedSet.add(resource); + } + + return result; + } + + private Resource searchResources(Requirement req, Set resourceSet) + { + for (Resource aResourceSet : resourceSet) { + checkInterrupt(); + Capability[] caps = aResourceSet.getCapabilities(); + if (caps != null) { + for (Capability cap : caps) { + if (req.isSatisfied(cap)) { + // The requirement is already satisfied an existing + // resource, return the resource. + return aResourceSet; + } + } + } + } + + return null; + } + + /** + * Searches for resources that do meet the given requirement + * @param req the the requirement that must be satisfied by resources + * @param resources list of resources to look at + * @return all resources meeting the given requirement + */ + private List searchResources(Requirement req, Resource[] resources) + { + List matchingCapabilities = new ArrayList(); + + if (resources != null) { + for (Resource resource : resources) { + checkInterrupt(); + // We don't need to look at resources we've already looked at. + if (!m_failedSet.contains(resource)) { + Capability[] caps = resource.getCapabilities(); + if (caps != null) { + for (Capability cap : caps) { + if (req.isSatisfied(cap)) + matchingCapabilities.add(new ResourceCapabilityImpl(resource, cap)); + } + } + } + } + } + + return matchingCapabilities; + } + + /** + * Determines which resource is preferred to deliver the required capability. + * This method selects the resource providing the highest version of the capability. + * If two resources provide the same version of the capability, the resource with + * the largest number of cabailities be preferred + * @param caps + * @return + */ + private ResourceCapability getBestCandidate(List caps) + { + Version bestVersion = null; + ResourceCapability best = null; + boolean bestLocal = false; + + for (ResourceCapability cap : caps) { + boolean isCurrentLocal = cap.getResource().isLocal(); + + if (best == null) { + best = cap; + bestLocal = isCurrentLocal; + Object v = cap.getCapability().getPropertiesAsMap().get(Resource.VERSION); + if ((v != null) && (v instanceof Version)) { + bestVersion = (Version) v; + } + } else if ((m_resolutionFlags & DO_NOT_PREFER_LOCAL) != 0 || !bestLocal || isCurrentLocal) { + Object v = cap.getCapability().getPropertiesAsMap().get(Resource.VERSION); + + // If there is no version, then select the resource + // with the greatest number of capabilities. + if ((v == null) && (bestVersion == null) + && (best.getResource().getCapabilities().length + < cap.getResource().getCapabilities().length)) { + best = cap; + bestLocal = isCurrentLocal; + bestVersion = null; + } else if ((v != null) && (v instanceof Version)) { + // If there is no best version or if the current + // resource's version is lower, then select it. + if ((bestVersion == null) || (bestVersion.compareTo((Version) v) < 0)) { + best = cap; + bestLocal = isCurrentLocal; + bestVersion = (Version) v; + } + // If the current resource version is equal to the + // best + else if ((bestVersion.compareTo((Version) v) == 0)) { + // If the symbolic name is the same, use the highest + // bundle version. + if ((best.getResource().getSymbolicName() != null) + && best.getResource().getSymbolicName().equals( + cap.getResource().getSymbolicName())) { + if (best.getResource().getVersion().compareTo( + cap.getResource().getVersion()) < 0) { + best = cap; + bestLocal = isCurrentLocal; + bestVersion = (Version) v; + } + } + // Otherwise select the one with the greatest + // number of capabilities. + else if (best.getResource().getCapabilities().length + < cap.getResource().getCapabilities().length) { + best = cap; + bestLocal = isCurrentLocal; + bestVersion = (Version) v; + } + } + } + } + } + + return (best == null) ? null : best; + } + + private void checkInterrupt() + { + if (Thread.interrupted()) + { + throw new org.apache.felix.bundlerepository.InterruptedResolutionException(); + } + } + + public synchronized void deploy(int flags) + { + // Must resolve if not already resolved. + if (!m_resolved && !resolve(flags)) + { + m_logger.log(Logger.LOG_ERROR, "Resolver: Cannot resolve target resources."); + return; + } + + // Check to make sure that our local state cache is up-to-date + // and error if it is not. This is not completely safe, because + // the state can still change during the operation, but we will + // be optimistic. This could also be made smarter so that it checks + // to see if the local state changes overlap with the resolver. + for (int repoIdx = 0; (m_repositories != null) && (repoIdx < m_repositories.length); repoIdx++) + { + if (m_repositories[repoIdx].getLastModified() > m_resolveTimeStamp) + { + throw new IllegalStateException("Framework state has changed, must resolve again."); + } + } + + // Eliminate duplicates from target, required, optional resources. + Set resourceSet = new HashSet(); + Resource[] resources = getAddedResources(); + for (int i = 0; (resources != null) && (i < resources.length); i++) + { + resourceSet.add(resources[i]); + } + resources = getRequiredResources(); + for (int i = 0; (resources != null) && (i < resources.length); i++) + { + resourceSet.add(resources[i]); + } + if ((flags & NO_OPTIONAL_RESOURCES) == 0) + { + resources = getOptionalResources(); + for (int i = 0; (resources != null) && (i < resources.length); i++) + { + resourceSet.add(resources[i]); + } + } + Resource[] deployResources = resourceSet.toArray(new Resource[resourceSet.size()]); + + // List to hold all resources to be started. + List startList = new ArrayList(); + + // Deploy each resource, which will involve either finding a locally + // installed resource to update or the installation of a new version + // of the resource to be deployed. + for (Resource deployResource : deployResources) { + // For the resource being deployed, see if there is an older + // version of the resource already installed that can potentially + // be updated. + LocalResource localResource = findUpdatableLocalResource(deployResource); + // If a potentially updatable older version was found, + // then verify that updating the local resource will not + // break any of the requirements of any of the other + // resources being deployed. + if ((localResource != null) && + isResourceUpdatable(localResource, deployResource, deployResources)) { + // Only update if it is a different version. + if (!localResource.equals(deployResource)) { + // Update the installed bundle. + try { + // stop the bundle before updating to prevent + // the bundle update from throwing due to not yet + // resolved dependencies + boolean doStartBundle = (flags & START) != 0; + if (localResource.getBundle().getState() == Bundle.ACTIVE) { + doStartBundle = true; + localResource.getBundle().stop(); + } + + localResource.getBundle().update(FileUtil.openURL(new URL(deployResource.getURI()))); + + // If necessary, save the updated bundle to be + // started later. + if (doStartBundle) { + Bundle bundle = localResource.getBundle(); + if (!isFragmentBundle(bundle)) { + startList.add(bundle); + } + } + } catch (Exception ex) { + m_logger.log( + Logger.LOG_ERROR, + "Resolver: Update error - " + getBundleName(localResource.getBundle()), + ex); + return; + } + } + } else { + // Install the bundle. + try { + // Perform the install, but do not use the actual + // bundle JAR URL for the bundle location, since this will + // limit OBR's ability to manipulate bundle versions. Instead, + // use a unique timestamp as the bundle location. + URL url = new URL(deployResource.getURI()); + Bundle bundle = m_context.installBundle( + "obr://" + + deployResource.getSymbolicName() + + "/-" + System.currentTimeMillis(), + FileUtil.openURL(url)); + + // If necessary, save the installed bundle to be + // started later. + if ((flags & START) != 0) { + if (!isFragmentBundle(bundle)) { + startList.add(bundle); + } + } + } catch (Exception ex) { + m_logger.log( + Logger.LOG_ERROR, + "Resolver: Install error - " + deployResource.getSymbolicName(), + ex); + return; + } + } + } + + for (Bundle aStartList : startList) { + try { + aStartList.start(); + } catch (BundleException ex) { + m_logger.log( + Logger.LOG_ERROR, + "Resolver: Start error - " + aStartList.getSymbolicName(), + ex); + } + } + } + + /** + * Determines if the given bundle is a fragement bundle. + * + * @param bundle bundle to check + * @return flag indicating if the given bundle is a fragement + */ + private boolean isFragmentBundle(Bundle bundle) + { + return bundle.getHeaders().get(Constants.FRAGMENT_HOST) != null; + } + + // TODO: OBR - Think about this again and make sure that deployment ordering + // won't impact it...we need to update the local state too. + private LocalResource findUpdatableLocalResource(Resource resource) + { + // Determine if any other versions of the specified resource + // already installed. + LocalResource[] localResources = findLocalResources(resource.getSymbolicName()); + // Since there are local resources with the same symbolic + // name installed, then we must determine if we can + // update an existing resource or if we must install + // another one. Loop through all local resources with same + // symbolic name and find the first one that can be updated + // without breaking constraints of existing local resources. + for (LocalResource localResource : localResources) { + if (isResourceUpdatable(localResource, resource, localResources)) { + return localResource; + } + } + return null; + } + + /** + * Returns all local resources with the given symbolic name. + * @param symName The symbolic name of the wanted local resources. + * @return The local resources with the specified symbolic name. + */ + private LocalResource[] findLocalResources(String symName) + { + LocalResource[] localResources = getLocalResources(); + + List matchList = new ArrayList(); + for (LocalResource localResource : localResources) { + String localSymName = localResource.getSymbolicName(); + if ((localSymName != null) && localSymName.equals(symName)) { + matchList.add(localResource); + } + } + return matchList.toArray(new LocalResource[matchList.size()]); + } + + private boolean isResourceUpdatable( + Resource oldVersion, Resource newVersion, Resource[] resources) + { + // Get all of the local resolvable requirements for the old + // version of the resource from the specified resource array. + Requirement[] reqs = getResolvableRequirements(oldVersion, resources); + if (reqs == null) + { + return true; + } + + // Now make sure that all of the requirements resolved by the + // old version of the resource can also be resolved by the new + // version of the resource. + Capability[] caps = newVersion.getCapabilities(); + if (caps == null) + { + return false; + } + for (Requirement req : reqs) { + boolean satisfied = false; + for (int capIdx = 0; !satisfied && (capIdx < caps.length); capIdx++) { + if (req.isSatisfied(caps[capIdx])) { + satisfied = true; + } + } + + // If any of the previously resolved requirements cannot + // be resolved, then the resource is not updatable. + if (!satisfied) { + return false; + } + } + + return true; + } + + private Requirement[] getResolvableRequirements(Resource resource, Resource[] resources) + { + // For the specified resource, find all requirements that are + // satisfied by any of its capabilities in the specified resource + // array. + Capability[] caps = resource.getCapabilities(); + if ((caps != null) && (caps.length > 0)) + { + List reqList = new ArrayList(); + for (Capability cap : caps) { + boolean added = false; + + for (Resource aResource : resources) { + Requirement[] reqs = aResource.getRequirements(); + + if (reqs != null) { + for (Requirement req : reqs) { + if (req.isSatisfied(cap)) { + added = true; + reqList.add(req); + } + } + } + + if (added) break; + } + } + return reqList.toArray(new Requirement[reqList.size()]); + } + return null; + } + + public static String getBundleName(Bundle bundle) + { + String name = bundle.getHeaders().get(Constants.BUNDLE_NAME); + return (name == null) + ? "Bundle " + Long.toString(bundle.getBundleId()) + : name; + } + +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ResourceCapability.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ResourceCapability.java new file mode 100644 index 00000000000..64ea01f7813 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ResourceCapability.java @@ -0,0 +1,31 @@ +/* + * 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.bundlerepository.impl; + +import org.apache.felix.bundlerepository.Capability; +import org.apache.felix.bundlerepository.Resource; + +public interface ResourceCapability +{ + + Resource getResource(); + + Capability getCapability(); + +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ResourceCapabilityImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ResourceCapabilityImpl.java new file mode 100644 index 00000000000..45cd72e5fba --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ResourceCapabilityImpl.java @@ -0,0 +1,46 @@ +/* + * 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.bundlerepository.impl; + +import org.apache.felix.bundlerepository.Capability; +import org.apache.felix.bundlerepository.Resource; + + +public class ResourceCapabilityImpl implements ResourceCapability +{ + private final Resource resource; + private final Capability capability; + + public ResourceCapabilityImpl(Resource resource, Capability capability) + { + this.resource = resource; + this.capability = capability; + } + + public Resource getResource() + { + return resource; + } + + public Capability getCapability() + { + return capability; + } + +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ResourceComparator.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ResourceComparator.java new file mode 100644 index 00000000000..2dfc892e90c --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ResourceComparator.java @@ -0,0 +1,47 @@ +/* + * 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.bundlerepository.impl; + +import java.util.Comparator; + +import org.apache.felix.bundlerepository.Resource; + +class ResourceComparator implements Comparator +{ + public int compare(Object o1, Object o2) + { + Resource r1 = (Resource) o1; + Resource r2 = (Resource) o2; + String name1 = r1.getPresentationName(); + String name2 = r2.getPresentationName(); + if (name1 == null) + { + if (name2 == null) + { + return 0; + } + return -1; + } + else if (name2 == null) + { + return 1; + } + return name1.compareToIgnoreCase(name2); + } +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ResourceImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ResourceImpl.java new file mode 100644 index 00000000000..2d4fbc683d2 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/ResourceImpl.java @@ -0,0 +1,316 @@ +/* + * 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.bundlerepository.impl; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; + +import org.apache.felix.bundlerepository.Capability; +import org.apache.felix.bundlerepository.Property; +import org.apache.felix.bundlerepository.Repository; +import org.apache.felix.bundlerepository.Requirement; +import org.apache.felix.bundlerepository.Resource; +import org.apache.felix.utils.version.VersionTable; +import org.osgi.framework.Version; + +public class ResourceImpl implements Resource +{ + + private final Map m_map = new HashMap(); + private final List m_capList = new ArrayList(); + private final List m_reqList = new ArrayList(); + private Repository m_repo; + private Map m_uris; + private transient int m_hash; + + public ResourceImpl() + { + } + + public boolean equals(Object o) + { + if (o instanceof Resource) + { + if (getSymbolicName() == null || getVersion() == null) + { + return this == o; + } + return getSymbolicName().equals(((Resource) o).getSymbolicName()) + && getVersion().equals(((Resource) o).getVersion()); + } + return false; + } + + public int hashCode() + { + if (m_hash == 0) + { + if (getSymbolicName() == null || getVersion() == null) + { + m_hash = super.hashCode(); + } + else + { + m_hash = getSymbolicName().hashCode() ^ getVersion().hashCode(); + } + } + return m_hash; + } + + public Repository getRepository() + { + return m_repo; + } + + public void setRepository(Repository repository) + { + this.m_repo = repository; + } + + public Map getProperties() + { + convertURIs(); + return m_map; + } + + public String getPresentationName() + { + String pres = (String) m_map.get(PRESENTATION_NAME); + return (pres!=null && !pres.isEmpty())? pres : toString(); + } + + public String getSymbolicName() + { + return (String) m_map.get(SYMBOLIC_NAME); + } + + public String getId() + { + return (String) m_map.get(ID); + } + + public Version getVersion() + { + Version v = (Version) m_map.get(VERSION); + v = (v == null) ? Version.emptyVersion : v; + return v; + } + + public String getURI() + { + convertURIs(); + return (String) m_map.get(Resource.URI); + } + + public Long getSize() + { + Object sz = m_map.get(Resource.SIZE); + if (sz instanceof Long) + return ((Long) sz); + + long size = findResourceSize(); + m_map.put(Resource.SIZE, size); + return size; + } + + private long findResourceSize() + { + String uri = getURI(); + if (uri != null) { + try + { + URL url = new URL(uri); + if ("file".equals(url.getProtocol())) + return new File(url.getFile()).length(); + else + return findResourceSize(url); + } + catch (Exception e) + { + // TODO should really log this... + } + } + return -1L; + } + + private long findResourceSize(URL url) throws IOException + { + byte[] bytes = new byte[8192]; + + // Not a File URL, stream the whole thing through to find out the size + InputStream is = null; + long fileSize = 0; + try + { + is = url.openStream(); + + int length = 0; + while ((length = is.read(bytes)) != -1) + { + fileSize += length; + } + } + catch (Exception ex) + { + // should really log this... + } + finally + { + if (is != null) + is.close(); + } + return fileSize; + } + + public Requirement[] getRequirements() + { + return (Requirement[]) m_reqList.toArray(new Requirement[m_reqList.size()]); + } + + public void addRequire(Requirement req) + { + m_reqList.add(req); + } + + public Capability[] getCapabilities() + { + return (Capability[]) m_capList.toArray(new Capability[m_capList.size()]); + } + + public void addCapability(Capability cap) + { + m_capList.add(cap); + } + + public String[] getCategories() + { + List catList = (List) m_map.get(CATEGORY); + if (catList == null) + { + return new String[0]; + } + return (String[]) catList.toArray(new String[catList.size()]); + } + + public void addCategory(String category) + { + List catList = (List) m_map.get(CATEGORY); + if (catList == null) + { + catList = new ArrayList(); + m_map.put(CATEGORY, catList); + } + catList.add(category); + } + + public boolean isLocal() + { + return false; + } + + /** + * Default setter method when setting parsed data from the XML file. + **/ + public Object put(Object key, Object value) + { + put(key.toString(), value.toString(), null); + return null; + } + + public void put(String key, String value, String type) + { + key = key.toLowerCase(); + m_hash = 0; + if (Property.URI.equals(type) || URI.equals(key)) + { + if (m_uris == null) + { + m_uris = new HashMap(); + } + m_uris.put(key, value); + } + else if (Property.VERSION.equals(type) || VERSION.equals(key)) + { + m_map.put(key, VersionTable.getVersion(value)); + } + else if (Property.LONG.equals(type) || SIZE.equals(key)) + { + m_map.put(key, Long.valueOf(value)); + } + else if (Property.SET.equals(type) || CATEGORY.equals(key)) + { + StringTokenizer st = new StringTokenizer(value, ","); + Set s = new HashSet(); + while (st.hasMoreTokens()) + { + s.add(st.nextToken().trim()); + } + m_map.put(key, s); + } + else + { + m_map.put(key, value); + } + } + + private void convertURIs() + { + if (m_uris != null) + { + for (Iterator it = m_uris.keySet().iterator(); it.hasNext();) + { + String key = (String) it.next(); + String val = (String) m_uris.get(key); + m_map.put(key, resolveUri(val)); + } + m_uris = null; + } + } + + private String resolveUri(String uri) + { + try + { + if (m_repo != null && m_repo.getURI() != null) + { + return new URI(m_repo.getURI()).resolve(uri).toString(); + } + } + catch (Throwable t) + { + } + return uri; + } + + public String toString() + { + return (getId() == null || getId().isEmpty())?getSymbolicName():getId(); + } +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/SpecXMLPullParser.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/SpecXMLPullParser.java new file mode 100644 index 00000000000..0cfa78f5189 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/SpecXMLPullParser.java @@ -0,0 +1,378 @@ +/* + * 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.bundlerepository.impl; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.felix.bundlerepository.Capability; +import org.apache.felix.bundlerepository.Requirement; +import org.apache.felix.bundlerepository.Resource; +import org.osgi.framework.Version; +import org.osgi.framework.namespace.BundleNamespace; +import org.osgi.framework.namespace.IdentityNamespace; +import org.osgi.resource.Namespace; +import org.osgi.service.repository.ContentNamespace; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +public class SpecXMLPullParser +{ + private static final String ATTRIBUTE = "attribute"; + private static final String CAPABILITY = "capability"; + private static final String DIRECTIVE = "directive"; + private static final String INCREMENT = "increment"; + private static final String NAME = "name"; + private static final String NAMESPACE = "namespace"; + private static final String REFERRAL = "referral"; + private static final String REPOSITORY = "repository"; + private static final String REQUIREMENT = "requirement"; + private static final String RESOURCE = "resource"; + + public static RepositoryImpl parse(XmlPullParser reader, URI baseUri) throws Exception + { + RepositoryImpl repository = new RepositoryImpl(); + for (int i = 0, ac = reader.getAttributeCount(); i < ac; i++) + { + String name = reader.getAttributeName(i); + String value = reader.getAttributeValue(i); + if (NAME.equals(name)) + repository.setName(value); + else if (INCREMENT.equals(name)) + repository.setLastModified(value); // TODO increment is not necessarily a timestamp + } + + int event; + while ((event = reader.nextTag()) == XmlPullParser.START_TAG) + { + String element = reader.getName(); + if (REFERRAL.equals(element)) + { + // TODO + } + else if (RESOURCE.equals(element)) + { + Resource resource = parseResource(reader, baseUri); + repository.addResource(resource); + } + else + { + PullParser.ignoreTag(reader); + } + } + + PullParser.sanityCheckEndElement(reader, event, REPOSITORY); + return repository; + } + + private static Resource parseResource(XmlPullParser reader, URI baseUri) throws Exception + { + ResourceImpl resource = new ResourceImpl(); + try + { + int event; + while ((event = reader.nextTag()) == XmlPullParser.START_TAG) + { + String element = reader.getName(); + if (CAPABILITY.equals(element)) + { + Capability capability = parseCapability(reader, resource, baseUri); + if (capability != null) + resource.addCapability(capability); + } + else if (REQUIREMENT.equals(element)) + { + Requirement requirement = parseRequirement(reader); + if (requirement != null) { + resource.addRequire(requirement); + } + } + else + { + PullParser.ignoreTag(reader); + } + } + + PullParser.sanityCheckEndElement(reader, event, RESOURCE); + return resource; + } + catch (Exception e) + { + throw new Exception("Error while parsing resource " + resource.getId() + " at line " + reader.getLineNumber() + " and column " + reader.getColumnNumber(), e); + } + } + + private static Capability parseCapability(XmlPullParser reader, ResourceImpl resource, URI baseUri) throws Exception + { + String namespace = reader.getAttributeValue(null, NAMESPACE); + if (IdentityNamespace.IDENTITY_NAMESPACE.equals(namespace)) + { + parseIdentityNamespace(reader, resource); + return null; + } + if (ContentNamespace.CONTENT_NAMESPACE.equals(namespace)) + { + if (resource.getURI() == null) + { + parseContentNamespace(reader, resource, baseUri); + return null; + } + // if the URI is already set, this is a second osgi.content capability. + // The first content capability, which is the main one, is stored in the Resource. + // Subsequent content capabilities are stored are ordinary capabilities. + } + + CapabilityImpl capability = new CapabilityImpl(); + if (!namespace.equals(NamespaceTranslator.getOSGiNamespace(namespace))) + throw new Exception("Namespace conflict. Namespace not allowed: " + namespace); + + capability.setName(NamespaceTranslator.getFelixNamespace(namespace)); + Map attributes = new HashMap(); + Map directives = new HashMap(); + parseAttributesDirectives(reader, attributes, directives, CAPABILITY); + + for (Map.Entry entry : attributes.entrySet()) + { + if (BundleNamespace.BUNDLE_NAMESPACE.equals(namespace) && BundleNamespace.BUNDLE_NAMESPACE.equals(entry.getKey())) + { + capability.addProperty(new FelixPropertyAdapter(Resource.SYMBOLIC_NAME, entry.getValue())); + continue; + } + if (BundleNamespace.BUNDLE_NAMESPACE.equals(namespace) && BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE.equals(entry.getKey())) + { + capability.addProperty(new FelixPropertyAdapter(Resource.VERSION, entry.getValue())); + continue; + } + capability.addProperty(new FelixPropertyAdapter(NamespaceTranslator.getFelixNamespace(entry.getKey()), entry.getValue())); + } + for (Map.Entry entry : directives.entrySet()) + { + capability.addDirective(entry.getKey(), entry.getValue()); + } + + return capability; + } + + private static void parseIdentityNamespace(XmlPullParser reader, ResourceImpl resource) throws Exception + { + Map attributes = new HashMap(); + parseAttributesDirectives(reader, attributes, new HashMap(), CAPABILITY); + // TODO need to cater for the singleton directive... + + for (Map.Entry entry : attributes.entrySet()) + { + if (IdentityNamespace.IDENTITY_NAMESPACE.equals(entry.getKey())) + resource.put(Resource.SYMBOLIC_NAME, entry.getValue()); + else + resource.put(entry.getKey(), entry.getValue()); + } + } + + private static void parseContentNamespace(XmlPullParser reader, ResourceImpl resource, URI baseUri) throws Exception + { + Map attributes = new HashMap(); + parseAttributesDirectives(reader, attributes, new HashMap(), CAPABILITY); + + for (Map.Entry entry : attributes.entrySet()) + { + if (ContentNamespace.CONTENT_NAMESPACE.equals(entry.getKey())) + // TODO we should really check the SHA + continue; + else if (ContentNamespace.CAPABILITY_URL_ATTRIBUTE.equals(entry.getKey())) { + String value = (String) entry.getValue(); + URI resourceUri = URI.create(value); + if (!resourceUri.isAbsolute()) { + resourceUri = URI.create(new StringBuilder(baseUri.toString()).append(value).toString()); + } + resource.put(Resource.URI, resourceUri); + } + else + resource.put(entry.getKey(), entry.getValue()); + } + } + + private static void parseAttributesDirectives(XmlPullParser reader, Map attributes, Map directives, String parentTag) throws XmlPullParserException, IOException + { + int event; + while ((event = reader.nextTag()) == XmlPullParser.START_TAG) + { + String element = reader.getName(); + if (ATTRIBUTE.equals(element)) + { + String name = reader.getAttributeValue(null, "name"); + String type = reader.getAttributeValue(null, "type"); + String value = reader.getAttributeValue(null, "value"); + attributes.put(name, getTypedValue(type, value)); + PullParser.sanityCheckEndElement(reader, reader.nextTag(), ATTRIBUTE); + } + else if (DIRECTIVE.equals(element)) + { + String name = reader.getAttributeValue(null, "name"); + String value = reader.getAttributeValue(null, "value"); + directives.put(name, value); + PullParser.sanityCheckEndElement(reader, reader.nextTag(), DIRECTIVE); + } + else + { + PullParser.ignoreTag(reader); + } + } + PullParser.sanityCheckEndElement(reader, event, parentTag); + } + + private static Object getTypedValue(String type, String value) + { + if (type == null) + return value; + + type = type.trim(); + if ("Version".equals(type)) + return Version.parseVersion(value); + else if ("Long".equals(type)) + return Long.parseLong(value); + else if ("Double".equals(type)) + return Double.parseDouble(value); + else if ("List".equals(type)) + return parseStringList(value); + else if ("List".equals(type)) + return parseVersionList(value); + else if ("List".equals(type)) + return parseLongList(value); + else if ("List".equals(type)) + return parseDoubleList(value); + return value; + } + + private static List parseStringList(String value) + { + List l = new ArrayList(); + StringBuilder sb = new StringBuilder(); + + boolean escaped = false; + for (char c : value.toCharArray()) + { + if (escaped) + { + sb.append(c); + escaped = false; + } + else + { + switch (c) + { + case '\\': + escaped = true; + break; + case ',': + l.add(sb.toString().trim()); + sb.setLength(0); + break; + default: + sb.append(c); + } + } + } + + if (sb.length() > 0) + l.add(sb.toString().trim()); + + return l; + } + + private static List parseVersionList(String value) + { + List l = new ArrayList(); + + // Version strings cannot contain a comma, as it's not an allowed character in it anywhere + for (String v : value.split(",")) + { + l.add(Version.parseVersion(v.trim())); + } + return l; + } + + private static List parseLongList(String value) + { + List l = new ArrayList(); + + for (String x : value.split(",")) + { + l.add(Long.parseLong(x.trim())); + } + return l; + } + + private static List parseDoubleList(String value) + { + List l = new ArrayList(); + + for (String d : value.split(",")) + { + l.add(Double.parseDouble(d.trim())); + } + return l; + } + + private static Requirement parseRequirement(XmlPullParser reader) throws Exception + { + RequirementImpl requirement = new RequirementImpl(); + String namespace = reader.getAttributeValue(null, NAMESPACE); + if (!namespace.equals(NamespaceTranslator.getOSGiNamespace(namespace))) + throw new Exception("Namespace conflict. Namespace not allowed: " + namespace); + + requirement.setName(NamespaceTranslator.getFelixNamespace(namespace)); + + Map attributes = new HashMap(); + Map directives = new HashMap(); + parseAttributesDirectives(reader, attributes, directives, REQUIREMENT); + requirement.setAttributes(attributes); + + String effective = directives.get("effective"); + if (effective != null && !effective.equals("resolve")) { + return null; + } + + String filter = directives.remove(Namespace.REQUIREMENT_FILTER_DIRECTIVE); + for (String ns : NamespaceTranslator.getTranslatedOSGiNamespaces()) + { + if (BundleNamespace.BUNDLE_NAMESPACE.equals(namespace) && BundleNamespace.BUNDLE_NAMESPACE.equals(ns)) + { + filter = filter.replaceAll("[(][ ]*" + ns + "[ ]*=", + "(" + Resource.SYMBOLIC_NAME + "="); + } + else + filter = filter.replaceAll("[(][ ]*" + ns + "[ ]*=", + "(" + NamespaceTranslator.getFelixNamespace(ns) + "="); + } + requirement.setFilter(filter); + requirement.setMultiple(Namespace.CARDINALITY_MULTIPLE.equals( + directives.remove(Namespace.REQUIREMENT_CARDINALITY_DIRECTIVE))); + requirement.setOptional(Namespace.RESOLUTION_OPTIONAL.equals( + directives.remove(Namespace.REQUIREMENT_RESOLUTION_DIRECTIVE))); + requirement.setDirectives(directives); + + requirement.setExtend(false); + + return requirement; + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/StaxParser.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/StaxParser.java new file mode 100644 index 00000000000..7f4a5f912e8 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/StaxParser.java @@ -0,0 +1,423 @@ +/* + * 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.bundlerepository.impl; + +import java.io.InputStream; +import java.io.Reader; +import java.net.URI; + +import javax.xml.stream.Location; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +/** + * Repository XML xml based on StaX + */ +public class StaxParser extends RepositoryParser +{ + + static XMLInputFactory factory; + + public static synchronized void setFactory(XMLInputFactory factory) + { + StaxParser.factory = factory; + } + + public static synchronized XMLInputFactory getFactory() + { + if (factory == null) + { + XMLInputFactory factory = XMLInputFactory.newInstance(); + setProperty(factory, XMLInputFactory.IS_NAMESPACE_AWARE, false); + setProperty(factory, XMLInputFactory.IS_VALIDATING, false); + setProperty(factory, XMLInputFactory.IS_COALESCING, false); + StaxParser.factory = factory; + } + return factory; + } + + public StaxParser() + { + } + + protected static boolean setProperty(XMLInputFactory factory, String name, boolean value) + { + try + { + factory.setProperty(name, Boolean.valueOf(value)); + return true; + } + catch (Throwable t) + { + } + return false; + } + + public RepositoryImpl parseRepository(InputStream is, URI baseUri) throws Exception + { + XMLStreamReader reader = getFactory().createXMLStreamReader(is); + int event = reader.nextTag(); + if (event != XMLStreamConstants.START_ELEMENT || !REPOSITORY.equals(reader.getLocalName())) + { + throw new Exception("Expected element 'repository' at the root of the document"); + } + return parseRepository(reader); + } + + public RepositoryImpl parseRepository(Reader r) throws Exception + { + XMLStreamReader reader = getFactory().createXMLStreamReader(r); + int event = reader.nextTag(); + if (event != XMLStreamConstants.START_ELEMENT || !REPOSITORY.equals(reader.getLocalName())) + { + throw new Exception("Expected element 'repository' at the root of the document"); + } + return parseRepository(reader); + } + + public ResourceImpl parseResource(Reader r) throws Exception + { + XMLStreamReader reader = getFactory().createXMLStreamReader(r); + int event = reader.nextTag(); + if (event != XMLStreamConstants.START_ELEMENT || !RESOURCE.equals(reader.getLocalName())) + { + throw new Exception("Expected element 'resource'"); + } + return parseResource(reader); + } + + public CapabilityImpl parseCapability(Reader r) throws Exception + { + XMLStreamReader reader = getFactory().createXMLStreamReader(r); + int event = reader.nextTag(); + if (event != XMLStreamConstants.START_ELEMENT || !CAPABILITY.equals(reader.getLocalName())) + { + throw new Exception("Expected element 'capability'"); + } + return parseCapability(reader); + } + + public PropertyImpl parseProperty(Reader r) throws Exception + { + XMLStreamReader reader = getFactory().createXMLStreamReader(r); + int event = reader.nextTag(); + if (event != XMLStreamConstants.START_ELEMENT || !P.equals(reader.getLocalName())) + { + throw new Exception("Expected element 'p'"); + } + return parseProperty(reader); + } + + public RequirementImpl parseRequirement(Reader r) throws Exception + { + XMLStreamReader reader = getFactory().createXMLStreamReader(r); + int event = reader.nextTag(); + if (event != XMLStreamConstants.START_ELEMENT || !REQUIRE.equals(reader.getLocalName())) + { + throw new Exception("Expected element 'require'"); + } + return parseRequire(reader); + } + + public RepositoryImpl parseRepository(XMLStreamReader reader) throws Exception + { + RepositoryImpl repository = new RepositoryImpl(); + for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) + { + String name = reader.getAttributeLocalName(i); + String value = reader.getAttributeValue(i); + if (NAME.equals(name)) + { + repository.setName(value); + } + else if (LASTMODIFIED.equals(name)) + { + repository.setLastModified(value); + } + } + int event; + while ((event = reader.nextTag()) == XMLStreamConstants.START_ELEMENT) + { + String element = reader.getLocalName(); + if (REFERRAL.equals(element)) + { + Referral referral = parseReferral(reader); + repository.addReferral(referral); + } + else if (RESOURCE.equals(element)) + { + ResourceImpl resource = parseResource(reader); + repository.addResource(resource); + } + else + { + ignoreTag(reader); + } + } + // Sanity check + sanityCheckEndElement(reader, event, REPOSITORY); + return repository; + } + + private void sanityCheckEndElement(XMLStreamReader reader, int event, String element) + { + if (event != XMLStreamConstants.END_ELEMENT || !element.equals(reader.getLocalName())) + { + throw new IllegalStateException("Unexpected state while finishing element " + element); + } + } + + private Referral parseReferral(XMLStreamReader reader) throws Exception + { + Referral referral = new Referral(); + for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) + { + String name = reader.getAttributeLocalName(i); + String value = reader.getAttributeValue(i); + if (DEPTH.equals(name)) + { + referral.setDepth(value); + } + else if (URL.equals(name)) + { + referral.setUrl(value); + } + } + sanityCheckEndElement(reader, reader.nextTag(), REFERRAL); + return referral; + } + + private ResourceImpl parseResource(XMLStreamReader reader) throws Exception + { + ResourceImpl resource = new ResourceImpl(); + try + { + for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) + { + resource.put(reader.getAttributeLocalName(i), reader.getAttributeValue(i)); + } + int event; + while ((event = reader.nextTag()) == XMLStreamConstants.START_ELEMENT) + { + String element = reader.getLocalName(); + if (CATEGORY.equals(element)) + { + String category = parseCategory(reader); + resource.addCategory(category); + } + else if (CAPABILITY.equals(element)) + { + CapabilityImpl capability = parseCapability(reader); + resource.addCapability(capability); + } + else if (REQUIRE.equals(element)) + { + RequirementImpl requirement = parseRequire(reader); + resource.addRequire(requirement); + } + else + { + StringBuffer sb = null; + String type = reader.getAttributeValue(null, "type"); + while ((event = reader.next()) != XMLStreamConstants.END_ELEMENT) + { + switch (event) + { + case XMLStreamConstants.START_ELEMENT: + throw new Exception("Unexpected element inside element"); + case XMLStreamConstants.CHARACTERS: + if (sb == null) + { + sb = new StringBuffer(); + } + sb.append(reader.getText()); + break; + } + } + if (sb != null) + { + resource.put(element, sb.toString().trim(), type); + } + } + } + // Sanity check + if (event != XMLStreamConstants.END_ELEMENT || !RESOURCE.equals(reader.getLocalName())) + { + throw new Exception("Unexpected state"); + } + return resource; + } + catch (Exception e) + { + Location loc = reader.getLocation(); + if (loc != null) { + throw new Exception("Error while parsing resource " + resource.getId() + " at line " + loc.getLineNumber() + " and column " + loc.getColumnNumber(), e); + } + else + { + throw new Exception("Error while parsing resource " + resource.getId(), e); + } + } + } + + private String parseCategory(XMLStreamReader reader) throws XMLStreamException + { + String id = null; + for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) + { + if (ID.equals(reader.getAttributeLocalName(i))) + { + id = reader.getAttributeValue(i); + } + } + sanityCheckEndElement(reader, reader.nextTag(), CATEGORY); + return id; + } + + private CapabilityImpl parseCapability(XMLStreamReader reader) throws Exception + { + CapabilityImpl capability = new CapabilityImpl(); + for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) + { + String name = reader.getAttributeLocalName(i); + String value = reader.getAttributeValue(i); + if (NAME.equals(name)) + { + capability.setName(value); + } + } + int event; + while ((event = reader.nextTag()) == XMLStreamConstants.START_ELEMENT) + { + String element = reader.getLocalName(); + if (P.equals(element)) + { + PropertyImpl prop = parseProperty(reader); + capability.addProperty(prop); + } + else + { + ignoreTag(reader); + } + } + // Sanity check + sanityCheckEndElement(reader, event, CAPABILITY); + return capability; + } + + private PropertyImpl parseProperty(XMLStreamReader reader) throws Exception + { + String n = null, t = null, v = null; + for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) + { + String name = reader.getAttributeLocalName(i); + String value = reader.getAttributeValue(i); + if (N.equals(name)) + { + n = value; + } + else if (T.equals(name)) + { + t = value; + } + else if (V.equals(name)) + { + v = value; + } + } + PropertyImpl prop = new PropertyImpl(n, t, v); + // Sanity check + sanityCheckEndElement(reader, reader.nextTag(), P); + return prop; + } + + private RequirementImpl parseRequire(XMLStreamReader reader) throws Exception + { + RequirementImpl requirement = new RequirementImpl(); + for (int i = 0, nb = reader.getAttributeCount(); i < nb; i++) + { + String name = reader.getAttributeLocalName(i); + String value = reader.getAttributeValue(i); + if (NAME.equals(name)) + { + requirement.setName(value); + } + else if (FILTER.equals(name)) + { + requirement.setFilter(value); + } + else if (EXTEND.equals(name)) + { + requirement.setExtend(Boolean.parseBoolean(value)); + } + else if (MULTIPLE.equals(name)) + { + requirement.setMultiple(Boolean.parseBoolean(value)); + } + else if (OPTIONAL.equals(name)) + { + requirement.setOptional(Boolean.parseBoolean(value)); + } + } + int event; + StringBuffer sb = null; + while ((event = reader.next()) != XMLStreamConstants.END_ELEMENT) + { + switch (event) + { + case XMLStreamConstants.START_ELEMENT: + throw new Exception("Unexpected element inside element"); + case XMLStreamConstants.CHARACTERS: + if (sb == null) + { + sb = new StringBuffer(); + } + sb.append(reader.getText()); + break; + } + } + if (sb != null) + { + requirement.addText(sb.toString()); + } + // Sanity check + sanityCheckEndElement(reader, event, REQUIRE); + return requirement; + } + + private void ignoreTag(XMLStreamReader reader) throws XMLStreamException + { + int level = 1; + int event = 0; + while (level > 0) + { + event = reader.next(); + if (event == XMLStreamConstants.START_ELEMENT) + { + level++; + } + else if (event == XMLStreamConstants.END_ELEMENT) + { + level--; + } + } + } +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/SystemRepositoryImpl.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/SystemRepositoryImpl.java new file mode 100644 index 00000000000..502dd8a9f6a --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/SystemRepositoryImpl.java @@ -0,0 +1,71 @@ +/* + * 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.bundlerepository.impl; + +import org.apache.felix.bundlerepository.Resource; +import org.apache.felix.utils.log.Logger; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.apache.felix.bundlerepository.Repository; + +public class SystemRepositoryImpl implements Repository +{ + + private final Logger m_logger; + private final long lastModified; + private final LocalResourceImpl systemBundleResource; + + public SystemRepositoryImpl(BundleContext context, Logger logger) + { + m_logger = logger; + lastModified = System.currentTimeMillis(); + try + { + systemBundleResource = new LocalResourceImpl(context.getBundle(0)); + } + catch (InvalidSyntaxException ex) + { + // This should never happen since we are generating filters, + // but ignore the resource if it does occur. + m_logger.log(Logger.LOG_WARNING, ex.getMessage(), ex); + throw new IllegalStateException("Unexpected error", ex); + } + } + + public String getURI() + { + return SYSTEM; + } + + public Resource[] getResources() + { + return new Resource[] { systemBundleResource }; + } + + public String getName() + { + return "System Repository"; + } + + public long getLastModified() + { + return lastModified; + } + +} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/XmlWriter.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/XmlWriter.java new file mode 100644 index 00000000000..71a0136ae39 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/XmlWriter.java @@ -0,0 +1,145 @@ +/* + * 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.bundlerepository.impl; + +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; + +public class XmlWriter +{ + private final Writer w; + private final List elements = new ArrayList(); + private boolean empty; + private boolean endAttr = true; + private boolean indent; + + public XmlWriter(Writer w) + { + this(w, true); + } + + public XmlWriter(Writer w, boolean indent) + { + this.w = w; + this.indent = indent; + } + + public XmlWriter indent(int nb) throws IOException + { + if (indent) + { + while (nb-- > 0) + { + w.append(" "); + } + } + return this; + } + + public XmlWriter newLine() throws IOException + { + if (indent) + { + w.append("\n"); + } + return this; + } + + public XmlWriter element(String name) throws IOException + { + if (!endAttr) + { + endAttr = true; + w.append(">"); + } + if (!elements.isEmpty()) + { + newLine(); + indent(elements.size()); + } + w.append("<").append(name); + elements.add(name); + empty = true; + endAttr = false; + return this; + } + + public XmlWriter attribute(String name, Object value) throws IOException + { + if (value != null) + { + w.append(" ").append(name).append("='").append(encode(value.toString())).append("'"); + } + return this; + } + + public XmlWriter end() throws IOException + { + return end(true); + } + + public XmlWriter end(boolean indent) throws IOException + { + String name = (String) elements.remove(elements.size() - 1); + if (!endAttr) + { + endAttr = true; + w.append("/>"); + } + else + { + if (indent && !empty) + { + newLine(); + indent(elements.size()); + } + w.append(""); + } + empty = false; + return this; + } + + public XmlWriter text(Object value) throws IOException + { + if (!endAttr) + { + endAttr = true; + w.append(">"); + } + w.append(encode(value.toString())); + return this; + } + + public XmlWriter textElement(String name, Object value) throws IOException + { + if (value != null) + { + element(name).text(value).end(false); + } + return this; + } + + private static String encode(Object o) { + String s = o != null ? o.toString() : ""; + return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("'", "'"); + } + +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/CapabilityWrapper.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/CapabilityWrapper.java new file mode 100644 index 00000000000..fd3d779948c --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/CapabilityWrapper.java @@ -0,0 +1,41 @@ +/* + * 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.bundlerepository.impl.wrapper; + +import java.util.Map; + +import org.apache.felix.bundlerepository.Capability; + +public class CapabilityWrapper implements org.osgi.service.obr.Capability { + + final Capability capability; + + public CapabilityWrapper(Capability capability) + { + this.capability = capability; + } + + public String getName() { + return capability.getName(); + } + + public Map getProperties() { + return capability.getPropertiesAsMap(); + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/ConvertedResource.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/ConvertedResource.java new file mode 100644 index 00000000000..505530549e8 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/ConvertedResource.java @@ -0,0 +1,118 @@ +/* + * 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.bundlerepository.impl.wrapper; + +import java.util.Iterator; +import java.util.Map; + +import org.apache.felix.bundlerepository.Capability; +import org.apache.felix.bundlerepository.Requirement; +import org.apache.felix.bundlerepository.Resource; +import org.apache.felix.bundlerepository.impl.CapabilityImpl; +import org.apache.felix.bundlerepository.impl.RequirementImpl; +import org.osgi.framework.Version; + +public class ConvertedResource implements Resource { + + private final org.osgi.service.obr.Resource resource; + + private Capability[] capabilities; + private Requirement[] requirements; + + public ConvertedResource(org.osgi.service.obr.Resource resource) { + this.resource = resource; + + // convert capabilities + org.osgi.service.obr.Capability[] c = resource.getCapabilities(); + if (c != null) { + capabilities = new Capability[c.length]; + for (int i = 0; i < c.length; i++) { + CapabilityImpl cap = new CapabilityImpl(c[i].getName()); + Iterator iter = c[i].getProperties().entrySet().iterator(); + int j = 0; + while (iter.hasNext()) { + Map.Entry entry = (Map.Entry) iter.next(); + cap.addProperty((String) entry.getKey(), null, (String) entry.getValue()); + } + + capabilities[i] = cap; + } + } + + // convert requirements + org.osgi.service.obr.Requirement[] r = resource.getRequirements(); + if (r != null) { + requirements = new Requirement[r.length]; + for (int i = 0; i < r.length; i++) { + RequirementImpl req = new RequirementImpl(r[i].getName()); + req.setFilter(r[i].getFilter()); + req.setOptional(r[i].isOptional()); + req.setExtend(r[i].isExtend()); + req.setMultiple(r[i].isMultiple()); + + requirements[i] = req; + } + } + } + + public Capability[] getCapabilities() { + return capabilities; + } + + public Requirement[] getRequirements() { + return requirements; + } + + public String[] getCategories() { + return resource.getCategories(); + } + + public String getId() { + return resource.getId(); + } + + public String getPresentationName() { + return resource.getPresentationName(); + } + + public Map getProperties() { + return resource.getProperties(); + } + + public Long getSize() { + return null; + } + + public String getSymbolicName() { + return resource.getSymbolicName(); + } + + public String getURI() { + return resource.getURL().toString(); + } + + public Version getVersion() { + return resource.getVersion(); + } + + public boolean isLocal() { + return false; + } + +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/RepositoryAdminWrapper.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/RepositoryAdminWrapper.java new file mode 100644 index 00000000000..79655bc1ebf --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/RepositoryAdminWrapper.java @@ -0,0 +1,64 @@ +/* + * 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.bundlerepository.impl.wrapper; + +import java.net.URL; + +import org.apache.felix.bundlerepository.RepositoryAdmin; +import org.osgi.framework.InvalidSyntaxException; + +public class RepositoryAdminWrapper implements org.osgi.service.obr.RepositoryAdmin +{ + + private final RepositoryAdmin admin; + + public RepositoryAdminWrapper(RepositoryAdmin admin) + { + this.admin = admin; + } + + public org.osgi.service.obr.Resource[] discoverResources(String filterExpr) { + try { + return Wrapper.wrap(admin.discoverResources(filterExpr)); + } catch (InvalidSyntaxException e) { + throw new RuntimeException(e); + } + } + + public org.osgi.service.obr.Resolver resolver() { + return Wrapper.wrap(admin.resolver()); + } + + public org.osgi.service.obr.Repository addRepository(URL repository) throws Exception { + return Wrapper.wrap(admin.addRepository(repository)); + } + + public boolean removeRepository(URL repository) { + return admin.removeRepository(repository.toExternalForm()); + } + + public org.osgi.service.obr.Repository[] listRepositories() { + return Wrapper.wrap(admin.listRepositories()); + } + + public org.osgi.service.obr.Resource getResource(String s) { + throw new UnsupportedOperationException(); + } + +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/RepositoryWrapper.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/RepositoryWrapper.java new file mode 100644 index 00000000000..8b1f342eb2e --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/RepositoryWrapper.java @@ -0,0 +1,55 @@ +/* + * 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.bundlerepository.impl.wrapper; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.apache.felix.bundlerepository.Repository; + +public class RepositoryWrapper implements org.osgi.service.obr.Repository { + + private final Repository repository; + + public RepositoryWrapper(Repository repository) + { + this.repository = repository; + } + + public URL getURL() { + try { + return new URL(repository.getURI()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + public org.osgi.service.obr.Resource[] getResources() { + return Wrapper.wrap(repository.getResources()); + } + + public String getName() { + return repository.getName(); + } + + public long getLastModified() { + return repository.getLastModified(); + } + +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/RequirementWrapper.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/RequirementWrapper.java new file mode 100644 index 00000000000..c9008b49f07 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/RequirementWrapper.java @@ -0,0 +1,73 @@ +/* + * 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.bundlerepository.impl.wrapper; + +import org.apache.felix.bundlerepository.Requirement; + +public class RequirementWrapper implements org.osgi.service.obr.Requirement { + + final Requirement requirement; + + public RequirementWrapper(Requirement requirement) { + this.requirement = requirement; + } + + public String getName() { + return requirement.getName(); + } + + public String getFilter() { + return requirement.getFilter(); + } + + public boolean isMultiple() { + return requirement.isMultiple(); + } + + public boolean isOptional() { + return requirement.isOptional(); + } + + public boolean isExtend() { + return requirement.isExtend(); + } + + public String getComment() { + return requirement.getComment(); + } + + public boolean isSatisfied(org.osgi.service.obr.Capability capability) { + return requirement.isSatisfied(Wrapper.unwrap(capability)); + } + + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RequirementWrapper that = (RequirementWrapper) o; + + if (requirement != null ? !requirement.equals(that.requirement) : that.requirement != null) return false; + + return true; + } + + public int hashCode() { + return requirement != null ? requirement.hashCode() : 0; + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/ResolverWrapper.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/ResolverWrapper.java new file mode 100644 index 00000000000..6fef6417bd4 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/ResolverWrapper.java @@ -0,0 +1,109 @@ +/* + * 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.bundlerepository.impl.wrapper; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.felix.bundlerepository.Reason; +import org.apache.felix.bundlerepository.Requirement; +import org.apache.felix.bundlerepository.Resolver; + +public class ResolverWrapper implements org.osgi.service.obr.Resolver { + + private final Resolver resolver; + + public ResolverWrapper(Resolver resolver) + { + this.resolver = resolver; + } + + public void add(org.osgi.service.obr.Resource resource) { + resolver.add(Wrapper.unwrap(resource)); + } + + public org.osgi.service.obr.Resource[] getAddedResources() { + return Wrapper.wrap(resolver.getAddedResources()); + } + + public org.osgi.service.obr.Resource[] getRequiredResources() { + return Wrapper.wrap(resolver.getRequiredResources()); + } + + public org.osgi.service.obr.Resource[] getOptionalResources() { + return Wrapper.wrap(resolver.getOptionalResources()); + } + + public org.osgi.service.obr.Requirement[] getReason(org.osgi.service.obr.Resource resource) { + Reason[] r = resolver.getReason(Wrapper.unwrap(resource)); + if (r == null) + { + return new org.osgi.service.obr.Requirement[0]; + } + Requirement[] r2 = new Requirement[r.length]; + for (int reaIdx = 0; reaIdx < r.length; reaIdx++) + { + r2[reaIdx] = r[reaIdx].getRequirement(); + } + return Wrapper.wrap(r2); + } + + public org.osgi.service.obr.Requirement[] getUnsatisfiedRequirements() { + Map map = getUnsatisfiedRequirementsMap(); + return (org.osgi.service.obr.Requirement[]) map.keySet().toArray(new org.osgi.service.obr.Requirement[map.size()]); + } + + public org.osgi.service.obr.Resource[] getResources(org.osgi.service.obr.Requirement requirement) { + Map map = getUnsatisfiedRequirementsMap(); + List l = (List) map.get(requirement); + if (l == null) + { + return new org.osgi.service.obr.Resource[0]; + } + return (org.osgi.service.obr.Resource[]) l.toArray(new org.osgi.service.obr.Resource[l.size()]); + } + + public boolean resolve() { + return resolver.resolve(); + } + + public void deploy(boolean start) { + resolver.deploy(start ? Resolver.START : 0); + } + + private Map getUnsatisfiedRequirementsMap() { + Reason[] reasons = resolver.getUnsatisfiedRequirements(); + Map map = new HashMap(); + for (int i = 0; i < reasons.length; i++) + { + org.osgi.service.obr.Requirement req = Wrapper.wrap(reasons[i].getRequirement()); + org.osgi.service.obr.Resource res = Wrapper.wrap(reasons[i].getResource()); + List l = (List) map.get(req); + if (l == null) + { + l = new ArrayList(); + map.put(req, l); + } + l.add(res); + } + return map; + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/ResourceWrapper.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/ResourceWrapper.java new file mode 100644 index 00000000000..6ef61b855ef --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/ResourceWrapper.java @@ -0,0 +1,82 @@ +/* + * 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.bundlerepository.impl.wrapper; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; + +import org.apache.felix.bundlerepository.Resource; +import org.osgi.framework.Version; +import org.osgi.service.obr.Capability; +import org.osgi.service.obr.Repository; +import org.osgi.service.obr.Requirement; + +public class ResourceWrapper implements org.osgi.service.obr.Resource { + + final Resource resource; + + public ResourceWrapper(Resource resource) { + this.resource = resource; + } + + public Map getProperties() { + return resource.getProperties(); + } + + public String getSymbolicName() { + return resource.getSymbolicName(); + } + + public String getPresentationName() { + return resource.getPresentationName(); + } + + public Version getVersion() { + return resource.getVersion(); + } + + public String getId() { + return resource.getId(); + } + + public URL getURL() { + try { + return new URL(resource.getURI()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + public Requirement[] getRequirements() { + return Wrapper.wrap(resource.getRequirements()); + } + + public Capability[] getCapabilities() { + return Wrapper.wrap(resource.getCapabilities()); + } + + public String[] getCategories() { + return resource.getCategories(); + } + + public Repository getRepository() { + throw new UnsupportedOperationException(); + } +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/Wrapper.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/Wrapper.java new file mode 100644 index 00000000000..56a59793500 --- /dev/null +++ b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/impl/wrapper/Wrapper.java @@ -0,0 +1,110 @@ +/* + * 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.bundlerepository.impl.wrapper; + +import org.apache.felix.bundlerepository.Capability; +import org.apache.felix.bundlerepository.Repository; +import org.apache.felix.bundlerepository.RepositoryAdmin; +import org.apache.felix.bundlerepository.Requirement; +import org.apache.felix.bundlerepository.Resolver; +import org.apache.felix.bundlerepository.Resource; + +public class Wrapper { + + public static org.osgi.service.obr.RepositoryAdmin wrap(RepositoryAdmin admin) { + return new RepositoryAdminWrapper(admin); + } + + public static org.osgi.service.obr.Resource wrap(Resource resource) { + return new ResourceWrapper(resource); + } + + public static org.osgi.service.obr.Repository wrap(Repository repository) { + return new RepositoryWrapper(repository); + } + + public static org.osgi.service.obr.Resolver wrap(Resolver resolver) { + return new ResolverWrapper(resolver); + } + + public static org.osgi.service.obr.Requirement wrap(Requirement resolver) { + return new RequirementWrapper(resolver); + } + + public static org.osgi.service.obr.Capability wrap(Capability capability) { + return new CapabilityWrapper(capability); + } + + public static Capability unwrap(org.osgi.service.obr.Capability capability) { + return ((CapabilityWrapper) capability).capability; + } + + public static Resource unwrap(org.osgi.service.obr.Resource resource) { + if (resource instanceof ResourceWrapper) { + return ((ResourceWrapper) resource).resource; + } else { + return new ConvertedResource(resource); + } + } + + public static Requirement unwrap(org.osgi.service.obr.Requirement requirement) { + return ((RequirementWrapper) requirement).requirement; + } + + public static org.osgi.service.obr.Resource[] wrap(Resource[] resources) + { + org.osgi.service.obr.Resource[] res = new org.osgi.service.obr.Resource[resources.length]; + for (int i = 0; i < resources.length; i++) + { + res[i] = wrap(resources[i]); + } + return res; + } + + public static org.osgi.service.obr.Repository[] wrap(Repository[] repositories) + { + org.osgi.service.obr.Repository[] rep = new org.osgi.service.obr.Repository[repositories.length]; + for (int i = 0; i < repositories.length; i++) + { + rep[i] = wrap(repositories[i]); + } + return rep; + } + + public static org.osgi.service.obr.Requirement[] wrap(Requirement[] requirements) + { + org.osgi.service.obr.Requirement[] req = new org.osgi.service.obr.Requirement[requirements.length]; + for (int i = 0; i < requirements.length; i++) + { + req[i] = wrap(requirements[i]); + } + return req; + } + + public static org.osgi.service.obr.Capability[] wrap(Capability[] capabilities) + { + org.osgi.service.obr.Capability[] cap = new org.osgi.service.obr.Capability[capabilities.length]; + for (int i = 0; i < capabilities.length; i++) + { + cap[i] = wrap(capabilities[i]); + } + return cap; + } + +} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/ClassUtility.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/ClassUtility.java deleted file mode 100644 index f83b497ef3f..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/ClassUtility.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository.metadataparser; - -/** - * This class provides methods to process class name - */ - -public class ClassUtility { - - /** - * This method capitalizes the first character in the provided string. - * @return resulted string - */ - public static String capitalize(String name) { - - int len=name.length(); - StringBuffer sb=new StringBuffer(len); - boolean setCap=true; - for(int i=0; i0) { - return fullclassname.substring(0,index); - } else { - return ""; - } - } - - /** - * This method returns the package name in a full class name - * @return resulted string - */ - public static String classOf(String fullclassname) { - int index=fullclassname.lastIndexOf("."); - if(index>0) { - return fullclassname.substring(index+1); - } else { - return fullclassname; - } - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/KXml2MetadataHandler.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/KXml2MetadataHandler.java deleted file mode 100644 index 16b909d47dc..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/KXml2MetadataHandler.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository.metadataparser; - -import java.io.*; - -import org.apache.felix.bundlerepository.metadataparser.kxmlsax.KXml2SAXParser; - -/** - * handles the metadata in XML format - * (use kXML (http://kxml.enhydra.org/) a open-source very light weight XML parser - */ -public class KXml2MetadataHandler extends MetadataHandler { - - public KXml2MetadataHandler() {} - - /** - * Called to parse the InputStream and set bundle list and package hash map - */ - public void parse(InputStream is) throws Exception { - BufferedReader br = new BufferedReader(new InputStreamReader(is)); - KXml2SAXParser parser; - parser = new KXml2SAXParser(br); - parser.parseXML(handler); - } -} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/MappingProcessingInstructionHandler.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/MappingProcessingInstructionHandler.java deleted file mode 100644 index 7f0963e2f43..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/MappingProcessingInstructionHandler.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository.metadataparser; - - -/** - * this class adds type of elements to the parser - */ -public class MappingProcessingInstructionHandler { - - private XmlCommonHandler handler; - private String name; - private String classname; - - public MappingProcessingInstructionHandler(XmlCommonHandler handler) { - this.handler = handler; - } - - public void process() throws Exception { - if(name==null) { - throw new Exception("element is missing"); - } - if(classname==null) { - throw new Exception("class is missing"); - } - handler.addType(name,this.getClass().getClassLoader().loadClass(classname),null,null); - } - - public void setElement(String element) { - this.name=element; - } - - public void setClass(String classname) { - this.classname=classname; - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/MetadataHandler.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/MetadataHandler.java deleted file mode 100644 index 53fccb15616..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/MetadataHandler.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository.metadataparser; - -import java.io.InputStream; -import java.lang.reflect.Method; - -public abstract class MetadataHandler { - - protected XmlCommonHandler handler; - - /** - * constructor - * - */ - public MetadataHandler() { - handler = new XmlCommonHandler(); - } - - /** - * Called to parse the InputStream and set bundle list and package hash map - */ - public abstract void parse(InputStream is) throws Exception; - - /** - * return the metadata after the parsing - * @return a Object. Its class is the returned type of instanceFactory newInstance method for the root element of the XML document. - */ - public final Object getMetadata() { - return handler.getRoot(); - } - /** - * Add a type for a element - * @param qname the name of the element to process - * @param instanceFactory the factory of objects representing an element. Must have a newInstance method. could be a class. - * @throws Exception - */ - public final void addType(String qname, Object instanceFactory) throws Exception { - handler.addType(qname, instanceFactory, null, null); - } - - /** - * Add a type for a element - * @param qname the name of the element to process - * @param instanceFactory the factory of objects representing an element. Must have a newInstance method. could be a class. - * @param castClass the class used to introspect the adder/setter and parameters in parent adder/setter. if null the castClass is by default the class returned by the newInstance method of the instanceFactory. - * @throws Exception - */ - public final void addType(String qname, Object instanceFactory, Class castClass) throws Exception { - handler.addType(qname, instanceFactory, castClass, null); - } - - /** - * Add a type for a element - * @param qname the name of the element to process - * @param instanceFactory the factory of objects representing an element. Must have a newInstance method. could be a class. - * @param castClass the class used to introspect the adder/setter and parameters in parent adder/setter. if null the castClass is by default the class returned by the newInstance method of the instanceFactory. - * @param defaultAddMethod the method used to add the sub-elements and attributes if no adder/setter is founded. could be omitted. - * @throws Exception - */ - public final void addType(String qname, Object instanceFactory, Class castClass, Method defaultAddMethod) throws Exception { - handler.addType(qname, instanceFactory, castClass, defaultAddMethod); - } - - /** - * Add a type for the default element - * @param instanceFactory the factory of objects representing an element. Must have a newInstance method. could be a class. - * @throws Exception - */ - public final void setDefaultType(Object instanceFactory) throws Exception { - handler.setDefaultType(instanceFactory,null,null); - } - - /** - * Add a type for the default element - * @param instanceFactory the factory of objects representing an element. Must have a newInstance method. could be a class. - * @param castClass the class used to introspect the adder/setter and parameters in parent adder/setter. if null the castClass is by default the class returned by the newInstance method of the instanceFactory. - * @throws Exception - */ - public final void setDefaultType(Object instanceFactory, Class castClass) throws Exception { - handler.setDefaultType(instanceFactory, castClass,null); - } - - /** - * Add a type for the default element - * @param instanceFactory the factory of objects representing an element. Must have a newInstance method. could be a class. - * @param castClass the class used to introspect the adder/setter and parameters in parent adder/setter. if null the castClass is by default the class returned by the newInstance method of the instanceFactory. - * @param defaultAddMethod the method used to add the sub-elements and attributes if no adder/setter is founded. could be omitted. - * @throws Exception - */ - public final void setDefaultType(Object instanceFactory, Class castClass, Method defaultAddMethod) throws Exception { - handler.setDefaultType(instanceFactory,castClass,defaultAddMethod); - } - - /** - * Add a type to process the processing instruction - * @param piname - * @param clazz - */ - public final void addPI(String piname, Class clazz) { - handler.addPI(piname, clazz); - } - - /** - * set the missing PI exception flag. If during parsing, the flag is true and the processing instruction is unknown, then the parser throws a exception - * @param flag - */ - public final void setMissingPIExceptionFlag(boolean flag) { - handler.setMissingPIExceptionFlag(flag); - } - - /** - * - * @param trace - * @since 0.9.1 - */ - public final void setTrace(boolean trace) { - handler.setTrace(trace); - } -} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/ReplaceUtility.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/ReplaceUtility.java deleted file mode 100644 index 5f5e396ba62..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/ReplaceUtility.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository.metadataparser; - -import java.util.Map; - -/** - * This class provides methods to replace ${var} substring by values stored in a map - */ - -public class ReplaceUtility { - - /** - * This method replaces ${var} substring by values stored in a map. - * @return resulted string - */ - public static String replace(String str, Map values) { - - int len = str.length(); - StringBuffer sb = new StringBuffer(len); - - int prev = 0; - int start = str.indexOf("${"); - int end = str.indexOf("}", start); - while (start != -1 && end != -1) { - String key = str.substring(start + 2, end); - Object value = values.get(key); - if (value != null) { - sb.append(str.substring(prev, start)); - sb.append(value); - } else { - sb.append(str.substring(prev, end + 1)); - } - prev = end + 1; - if (prev >= str.length()) - break; - - start = str.indexOf("${", prev); - if (start != -1) - end = str.indexOf("}", start); - } - - sb.append(str.substring(prev)); - - return sb.toString(); - } - - // public static void main(String[] args){ - // Map map=new HashMap(); - // map.put("foo","FOO"); - // map.put("bar","BAR"); - // map.put("map",map); - // - // String str; - // if(args.length==0) str=""; else str=args[0]; - // - // System.out.println(replace(str,map)); - // - // } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/XmlCommonHandler.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/XmlCommonHandler.java deleted file mode 100644 index 639eac3b83d..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/XmlCommonHandler.java +++ /dev/null @@ -1,865 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository.metadataparser; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.*; - -import org.apache.felix.bundlerepository.metadataparser.kxmlsax.KXml2SAXHandler; -import org.xml.sax.SAXException; - - -/** - * SAX handler for the XML file - */ -public class XmlCommonHandler implements KXml2SAXHandler { - - private static final String PI_MAPPING = "mapping"; - - public static final String METADATAPARSER_PIS = "METADATAPARSER_PIS"; - - public static final String METADATAPARSER_TYPES = "METADATAPARSER_TYPES"; - - private int columnNumber; - - private int lineNumber; - - private boolean traceFlag = false; - - private static String VALUE = "value"; - - // - // Data - // - - private XmlStackElement root; - - private Stack elementStack; - - private Map pis; - - private boolean missingPIExceptionFlag; - - private Map types; - - private TypeEntry defaultType; - - private StringBuffer currentText; - - private Map context; - - private class XmlStackElement { - - public final String qname; - public Object object; - - public XmlStackElement(String qname, Object object) { - super(); - this.qname = qname; - this.object = object; - }; - } - - public class TypeEntry { - public final Object instanceFactory; - public final Class instanceClass; - public final Method newInstanceMethod; - public final Class castClass; - public final Method defaultAddMethod; - - public TypeEntry(Object instanceFactory, Class castClass, Method defaultAddMethod) throws Exception { - super(); - this.instanceFactory = instanceFactory; - - try { - if (instanceFactory instanceof Class) { - newInstanceMethod = instanceFactory.getClass() - .getDeclaredMethod("newInstance", null); - if (castClass == null) { - this.castClass = (Class) instanceFactory; - } else { - if (!castClass.isAssignableFrom((Class) instanceFactory)) { - throw new Exception( - "instanceFactory " - + instanceFactory.getClass().getName() - + " could not instanciate objects assignable to " - + castClass.getName()); - } - this.castClass=castClass; - } - instanceClass = (Class) instanceFactory; - } else { - newInstanceMethod = instanceFactory.getClass() - .getDeclaredMethod("newInstance", null); - Class returnType = newInstanceMethod.getReturnType(); - if (castClass == null) { - this.castClass = returnType; - } else if (!castClass.isAssignableFrom(returnType)) { - throw new Exception( - "instanceFactory " - + instanceFactory.getClass().getName() - + " could not instanciate objects assignable to " - + castClass.getName()); - } else - this.castClass=castClass; - instanceClass = returnType; - } - } catch (NoSuchMethodException e) { - throw new Exception( - "instanceFactory " + instanceFactory.getClass().getName() - + " should have a newInstance method"); - } - - // TODO check method - this.defaultAddMethod = defaultAddMethod; - if (this.defaultAddMethod != null) - { - this.defaultAddMethod.setAccessible(true); - } - } - - public String toString(){ - StringBuffer sb=new StringBuffer(); - sb.append("["); - if(instanceFactory instanceof Class) - sb.append("instanceFactory=").append(((Class)instanceFactory).getName()); - else - sb.append("instanceFactory=").append(instanceFactory.getClass().getName()); - sb.append(",instanceClass=").append(instanceClass.getName()); - sb.append(",castClass=").append(castClass.getName()); - sb.append(",defaultAddMethod="); - if(defaultAddMethod==null) sb.append(""); else sb.append(defaultAddMethod.getName()); - sb.append("]"); - return sb.toString(); - } - } - - public XmlCommonHandler() { - elementStack = new Stack(); - pis = new HashMap(); - missingPIExceptionFlag = false; - types = new HashMap(); - context = new HashMap(); - context.put(METADATAPARSER_PIS, pis); - context.put(METADATAPARSER_TYPES, types); - } - - public void addPI(String piname, Class clazz) { - pis.put(piname, clazz); - } - - /** - * set the missing PI exception flag. If during parsing, the flag is true - * and the processing instruction is unknown, then the parser throws a - * exception - * - * @param flag - */ - public void setMissingPIExceptionFlag(boolean flag) { - missingPIExceptionFlag = flag; - } - - public void addType(String qname, Object instanceFactory, Class castClass, Method defaultAddMethod) - throws Exception { - - TypeEntry typeEntry; - try { - typeEntry = new TypeEntry( - instanceFactory, - castClass, - defaultAddMethod - ); - } catch (Exception e) { - throw new Exception(lineNumber + "," + columnNumber + ":" + qname + " : " + e.getMessage()); - } - types.put(qname,typeEntry); - trace("element " - + qname - + " : " + typeEntry.toString()); - } - - public void setDefaultType(Object instanceFactory, Class castClass, Method defaultAddMethod) - throws Exception { - TypeEntry typeEntry; - try { - typeEntry = new TypeEntry( - instanceFactory, - castClass, - defaultAddMethod - ); - } catch (Exception e) { - throw new Exception(lineNumber + "," + columnNumber + ": default element : " + e.getMessage()); - } - defaultType=typeEntry; - trace("default element " - + " : " + typeEntry.toString()); - } - - public void setContext(Map context) { - this.context = context; - } - - public Map getContext() { - return context; - } - - public Object getRoot() { - return root.object; - } - - /* for PCDATA */ - public void characters(char[] ch, int offset, int length) throws Exception { - if (currentText != null) - currentText.append(ch, offset, length); - } - - private String adderOf(Class clazz) { - return "add" - + ClassUtility - .capitalize(ClassUtility.classOf(clazz.getName())); - } - - private String adderOf(String key) { - return "add" + ClassUtility.capitalize(key); - } - - private String setterOf(Class clazz) { - return "set" - + ClassUtility - .capitalize(ClassUtility.classOf(clazz.getName())); - } - - private String setterOf(String key) { - return "set" + ClassUtility.capitalize(key); - } - - /** - * set the parser context in a object - */ - private void setObjectContext(Object object) - throws IllegalArgumentException, IllegalAccessException, - InvocationTargetException { - Method method = null; - try { - // TODO setContext from castClass or object.getClass() ? - method = object.getClass().getDeclaredMethod("setContext", - new Class[] { Map.class }); - } catch (NoSuchMethodException e) { - // do nothing - } - if (method != null) { - trace(method.getName()); - try { - method.invoke(object, new Object[] { context }); - } catch (InvocationTargetException e) { - e.getTargetException().printStackTrace(System.err); - throw e; - } - } - } - - /** - * set the parser context in a object - * - * @throws Throwable - */ - private void invokeProcess(Object object) throws Throwable { - Method method = null; - try { - // TODO process from castClass or object.getClass() ? - method = object.getClass().getDeclaredMethod("process", null); - } catch (NoSuchMethodException e) { - // do nothing - } - if (method != null) { - trace(method.getName()); - try { - method.invoke(object, null); - } catch (InvocationTargetException e) { - // e.getTargetException().printStackTrace(System.err); - throw e.getTargetException(); - } - - } - } - - /** - * set the parent in a object - */ - private void setObjectParent(Object object, Object parent) - throws InvocationTargetException, IllegalArgumentException, - IllegalAccessException { - Method method = null; - try { - // TODO setParent from castClass or object.getClass() ? - method = object.getClass().getDeclaredMethod("setParent", - new Class[] { parent.getClass() }); - } catch (NoSuchMethodException e) { - // do nothing - } - if (method != null) { - trace(method.getName()); - try { - method.invoke(object, new Object[] { parent }); - } catch (InvocationTargetException e) { - e.getTargetException().printStackTrace(System.err); - throw e; - } - } - } - - /** - * Method called when a tag opens - * - * @param uri - * @param localName - * @param qName - * @param attrib - * @exception SAXException - */ - public void startElement(String uri, String localName, String qName, - Properties attrib) throws Exception { - - trace("START (" + lineNumber + "," + columnNumber + "):" + uri + ":" - + qName); - - // TODO: should add uri in the qname in the future - TypeEntry type=(TypeEntry) types.get(qName); - if(type==null) { - type=defaultType; - } - - Object obj = null; - if (type != null) { - - try { - // enables to access to "unmuttable" method - type.newInstanceMethod.setAccessible(true); - obj = type.newInstanceMethod.invoke(type.instanceFactory, null); - } catch (Exception e) { - // do nothing - } - - // set parent - if (!elementStack.isEmpty()) { - XmlStackElement parent = (XmlStackElement) elementStack.peek(); - setObjectParent(obj, parent.object); - } - - // set the parser context - setObjectContext(obj); - - // set the attributes - Set keyset = attrib.keySet(); - Iterator iter = keyset.iterator(); - while (iter.hasNext()) { - String key = (String) iter.next(); - - // substitute ${property} sbustrings by context' properties - // values - String value = ReplaceUtility.replace((String) attrib.get(key), - context); - - // Firstly, test if the getter or the adder exists - - Method method = null; - if (!(obj instanceof String)) { - try { - // method = castClass.getDeclaredMethod(setterOf(key),new - // Class[] { String.class }); - method = type.instanceClass.getDeclaredMethod(setterOf(key), - new Class[] { String.class }); - } catch (NoSuchMethodException e) { - // do nothing - } - if (method == null) - try { - method = type.instanceClass.getDeclaredMethod(adderOf(key), - new Class[] { String.class }); - - } catch (NoSuchMethodException e) { - /* - * throw new Exception(lineNumber + "," + - * columnNumber + ":" + "element " + qName + " does - * not support the attribute " + key); - */ - } - - } - - if (method != null) { - trace(method.getName()); - try { - method.invoke(obj, new String[] { value }); - } catch (InvocationTargetException e) { - e.getTargetException().printStackTrace(System.err); - throw e; - } - } else { - - if (obj instanceof String) { - if (key.equals(VALUE)) { - obj = value; - } else { - throw new Exception(lineNumber + "," + columnNumber - + ":" + "String element " + qName - + " cannot have other attribute than value"); - } - } else { - if (type.defaultAddMethod != null) { - Class[] parameterTypes=type.defaultAddMethod.getParameterTypes(); - if(parameterTypes.length==2 - && parameterTypes[0].isAssignableFrom(String.class) - && parameterTypes[1].isAssignableFrom(String.class) - ){ - type.defaultAddMethod.invoke(obj,new String[]{key, value}); - } else if(parameterTypes.length==1 - && parameterTypes[0].isAssignableFrom(String.class) - ){ - type.defaultAddMethod.invoke(obj,new String[]{value}); - } else - throw new Exception(lineNumber + "," + columnNumber - + ":" + "class " - + type.instanceFactory.getClass().getName() - + " for element " + qName - + " does not support the attribute " + key - ); - } else { - throw new Exception(lineNumber + "," + columnNumber - + ":" + "class " - + type.instanceFactory.getClass().getName() - + " for element " + qName - + " does not support the attribute " + key - ); - } - - } - } - - } - - } else { - throw new Exception(lineNumber + "," + columnNumber + ":" - + "this element " + qName + " has not corresponding class"); - } - XmlStackElement element=new XmlStackElement(qName,obj); - if (root == null) - root = element; - elementStack.push(element); - currentText = new StringBuffer(); - - trace("START/ (" + lineNumber + "," + columnNumber + "):" + uri + ":" - + qName); - } - - /** - * Method called when a tag closes - * - * @param uri - * @param localName - * @param qName - * @exception SAXException - */ - public void endElement(java.lang.String uri, java.lang.String localName, - java.lang.String qName) throws Exception { - - trace("END (" + lineNumber + "," + columnNumber + "):" + uri + ":" - + qName); - - XmlStackElement element = (XmlStackElement) elementStack.pop(); - TypeEntry elementType=(TypeEntry) types.get(element.qname); - if(elementType==null) { - elementType=defaultType; - } - - if (currentText != null && currentText.length() != 0) { - - String currentStr = ReplaceUtility.replace(currentText.toString(), - context).trim(); - // TODO: trim may be not the right choice - trace("current text:" + currentStr); - - Method method = null; - try { - method = elementType.castClass.getDeclaredMethod("addText", - new Class[] { String.class }); - } catch (NoSuchMethodException e) { - try { - method = elementType.castClass.getDeclaredMethod("setText", - new Class[] { String.class }); - } catch (NoSuchMethodException e2) { - // do nothing - } - } - if (method != null) { - trace(method.getName()); - try { - method.invoke(element.object, new String[] { currentStr }); - } catch (InvocationTargetException e) { - e.getTargetException().printStackTrace(System.err); - throw e; - } - } else { - if (String.class.isAssignableFrom(elementType.castClass)) { - String str = (String) element.object; - if (str.length() != 0) { - throw new Exception( - lineNumber - + "," - + columnNumber - + ":" - + "String element " - + qName - + " cannot have both PCDATA and an attribute value"); - } else { - element.object = currentStr; - } - } - } - - } - - currentText = null; - - if (!elementStack.isEmpty()) { - - XmlStackElement parent = (XmlStackElement) elementStack.peek(); - TypeEntry parentType = (TypeEntry) types.get(parent.qname); - if(parentType==null) { - parentType=defaultType; - } - - String capqName=ClassUtility.capitalize(qName); - Method method = null; - try { -// TODO: OBR PARSER: We should also check for instance class as a parameter. - method = parentType.instanceClass.getDeclaredMethod( - adderOf(capqName), - new Class[] { elementType.castClass }); // instanceClass - } catch (NoSuchMethodException e) { - trace("NoSuchMethodException: " - + adderOf(capqName) + "("+elementType.castClass.getName()+")"); - // do nothing - } - if (method == null) - try { - method = parentType.instanceClass.getDeclaredMethod( - setterOf(capqName), - new Class[] { elementType.castClass }); - } catch (NoSuchMethodException e) { - trace("NoSuchMethodException: " - + setterOf(capqName) + "("+elementType.castClass.getName()+")"); - // do nothing - } - /*if (method == null) - try { - method = parentType.castClass.getDeclaredMethod( - adderOf(type.castClass), - new Class[] { type.castClass }); - } catch (NoSuchMethodException e) { - trace("NoSuchMethodException: " + adderOf(type.castClass)+ "("+type.castClass.getName()+")"); - // do nothing - } - if (method == null) - try { - method = parentType.castClass.getDeclaredMethod( - setterOf(type.castClass), - new Class[] { type.castClass }); - } catch (NoSuchMethodException e) { - trace("NoSuchMethodException: " + setterOf(type.castClass)+ "("+type.castClass.getName()+")"); - // do nothing - } - */ - if (method != null) { - trace(method.getName()); - try { - method.setAccessible(true); - method.invoke(parent.object, new Object[] { element.object }); - } catch (InvocationTargetException e) { - e.getTargetException().printStackTrace(System.err); - throw e; - } - } else { - if (parentType.defaultAddMethod != null) { - Class[] parameterTypes=parentType.defaultAddMethod.getParameterTypes(); - if(parameterTypes.length==2 - && parameterTypes[0].isAssignableFrom(String.class) - && parameterTypes[1].isAssignableFrom(elementType.castClass) - ){ - parentType.defaultAddMethod.invoke(parent.object,new Object[]{qName, element.object}); - } else if(parameterTypes.length==1 - && parameterTypes[0].isAssignableFrom(elementType.castClass) - ){ - parentType.defaultAddMethod.invoke(parent.object,new Object[]{element.object}); - } else { - throw new Exception(lineNumber + "," + columnNumber + ":" - + " element " + parent.qname - + " cannot have an attribute " + qName - + " of type " + elementType.castClass); - } - } else { - throw new Exception(lineNumber + "," + columnNumber + ":" - + " element " + parent.qname - + " cannot have an attribute " + qName - + " of type " + elementType.castClass); - } - } - - } - - // invoke the process method - try { - invokeProcess(element); - } catch (Throwable e) { - e.printStackTrace(); - throw new Exception(e); - } - - trace("END/ (" + lineNumber + "," + columnNumber + "):" + uri + ":" - + qName); - - } - - public void setTrace(boolean trace) { - this.traceFlag = trace; - } - - private void trace(String msg) { - if (traceFlag) - System.err.println(msg); - } - - /** - * @see kxml.sax.KXmlSAXHandler#setLineNumber(int) - */ - public void setLineNumber(int lineNumber) { - this.lineNumber = lineNumber; - } - - /** - * @see kxml.sax.KXmlSAXHandler#setColumnNumber(int) - */ - public void setColumnNumber(int columnNumber) { - this.columnNumber = columnNumber; - - } - - /** - * @see kxml.sax.KXmlSAXHandler#processingInstruction(java.lang.String, - * java.lang.String) - */ - - public void processingInstruction(String target, String data) - throws Exception { - trace("PI:" + target + ";" + data); - trace("ignore PI : "+data); -/* // reuse the kXML parser methods to parser the PI data - Reader reader = new StringReader(data); - XmlParser parser = new XmlParser(reader); - parser.parsePIData(); - - target = parser.getTarget(); - Map attributes = parser.getAttributes(); - - // get the class - Class clazz = (Class) pis.get(target); - if (clazz == null) { - if (missingPIExceptionFlag) - throw new Exception(lineNumber + "," + columnNumber + ":" - + "Unknown processing instruction"); - else { - trace(lineNumber + "," + columnNumber + ":" - + "No class for PI " + target); - return; - } - } - - // instanciate a object - Object object; - Constructor ctor = null; - try { - ctor = clazz.getConstructor(new Class[] { XmlCommonHandler.class }); - } catch (NoSuchMethodException e) { - // do nothing - trace("no constructor with XmlCommonHandler parameter"); - } - try { - if (ctor == null) { - object = clazz.newInstance(); - } else { - object = ctor.newInstance(new Object[] { this }); - } - } catch (InstantiationException e) { - throw new Exception( - lineNumber - + "," - + columnNumber - + ":" - + "class " - + clazz.getName() - + " for PI " - + target - + " should have an empty constructor or a constructor with XmlCommonHandler parameter"); - } catch (IllegalAccessException e) { - throw new Exception(lineNumber + "," + columnNumber + ":" - + "illegal access on the constructor " + clazz.getName() - + " for PI " + target); - } - - // set the context - setObjectContext(object); - - // TODO: set the parent - - // invoke setter - Iterator iter = attributes.keySet().iterator(); - while (iter.hasNext()) { - String key = (String) iter.next(); - String value = ReplaceUtility.replace((String) attributes.get(key), - context); - Method method = null; - try { - method = clazz.getDeclaredMethod(setterOf(key), - new Class[] { String.class }); - } catch (NoSuchMethodException e) { - // do nothing - } - if (method != null) { - trace(method.getName()); - try { - method.invoke(object, new String[] { value }); - } catch (InvocationTargetException e) { - e.getTargetException().printStackTrace(System.err); - throw e; - } - } - - } - - // invoke process - try { - invokeProcess(object); - } catch (Throwable e) { - e.printStackTrace(); - throw new Exception(e); - } -*/ } - - public void processingInstructionForMapping(String target, String data) - throws Exception { - - - if (target == null) { // TODO kXML - if (!data.startsWith(PI_MAPPING)) - return; - } else if (!target.equals(PI_MAPPING)) - return; - - // defaultclass attribute - String datt = "defaultclass=\""; - int dstart = data.indexOf(datt); - if (dstart != -1) { - int dend = data.indexOf("\"", dstart + datt.length()); - if (dend == -1) - throw new Exception( - lineNumber - + "," - + columnNumber - + ":" - + " \"defaultclass\" attribute in \"mapping\" PI is not quoted"); - - String classname = data.substring(dstart + datt.length(), dend); - Class clazz = null; - try { - clazz = getClass().getClassLoader().loadClass(classname); - } catch (ClassNotFoundException e) { - throw new Exception(lineNumber + "," + columnNumber + ":" - + " cannot found class " + classname - + " for \"mapping\" PI"); - } - - // TODO Add method - Method defaultdefaultAddMethod=null; - setDefaultType(clazz, null,defaultdefaultAddMethod); - return; - } - - // element attribute - String eatt = "element=\""; - int estart = data.indexOf(eatt); - if (estart == -1) - throw new Exception(lineNumber + "," + columnNumber + ":" - + " missing \"element\" attribute in \"mapping\" PI"); - int eend = data.indexOf("\"", estart + eatt.length()); - if (eend == -1) - throw new Exception(lineNumber + "," + columnNumber + ":" - + " \"element\" attribute in \"mapping\" PI is not quoted"); - - String element = data.substring(estart + eatt.length(), eend); - - // element class - String catt = "class=\""; - int cstart = data.indexOf(catt); - if (cstart == -1) - throw new Exception(lineNumber + "," + columnNumber + ":" - + " missing \"class\" attribute in \"mapping\" PI"); - int cend = data.indexOf("\"", cstart + catt.length()); - if (cend == -1) - throw new Exception(lineNumber + "," + columnNumber + ":" - + " \"class\" attribute in \"mapping\" PI is not quoted"); - - String classname = data.substring(cstart + catt.length(), cend); - - // element cast (optional) - String castname = null; - String castatt = "cast=\""; - int caststart = data.indexOf(castatt); - if (caststart != -1) { - int castend = data.indexOf("\"", cstart + castatt.length()); - if (castend == -1) - throw new Exception(lineNumber + "," + columnNumber + ":" - + " \"cast\" attribute in \"mapping\" PI is not quoted"); - - castname = data.substring(caststart + castatt.length(), castend); - } - - Class clazz = null; - try { - clazz = getClass().getClassLoader().loadClass(classname); - } catch (ClassNotFoundException e) { - throw new Exception(lineNumber + "," + columnNumber + ":" - + " cannot found class " + classname - + " for \"mapping\" PI"); - } - - Class castClazz = null; - if (castname != null) - try { - clazz = getClass().getClassLoader().loadClass(castname); - } catch (ClassNotFoundException e) { - throw new Exception(lineNumber + "," + columnNumber + ":" - + " cannot found cast class " + classname - + " for \"mapping\" PI"); - } - - // TODO Add method - Method defaultAddMethod=null; - - addType(element, clazz, castClazz, defaultAddMethod); - } -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/XmlMetadataHandler.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/XmlMetadataHandler.java deleted file mode 100644 index bab5b948d76..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/XmlMetadataHandler.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository.metadataparser; - -import java.io.IOException; -import java.io.InputStream; - -import javax.xml.parsers.*; - -import org.xml.sax.*; - -/** - * handles the metadata in XML format - */ -public class XmlMetadataHandler extends MetadataHandler { - - public XmlMetadataHandler() { - } - - /** - * Called to parse the InputStream and set bundle list and package hash map - */ - public void parse(InputStream istream) throws ParserConfigurationException, IOException, SAXException { - // Parse the Meta-Data - - ContentHandler contenthandler = (ContentHandler) handler; - - InputSource is = new InputSource(istream); - - SAXParserFactory spf = SAXParserFactory.newInstance(); - spf.setValidating(false); - - SAXParser saxParser = spf.newSAXParser(); - - XMLReader xmlReader = null; - xmlReader = saxParser.getXMLReader(); - xmlReader.setContentHandler(contenthandler); - xmlReader.parse(is); - } -} diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/kxmlsax/KXml2SAXHandler.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/kxmlsax/KXml2SAXHandler.java deleted file mode 100644 index 2ba350c25c1..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/kxmlsax/KXml2SAXHandler.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository.metadataparser.kxmlsax; - -import java.util.Properties; - -/** - * Interface for SAX handler with kXML - */ -public interface KXml2SAXHandler { - - /** - * Method called when parsing text - * - * @param ch - * @param offset - * @param length - * @exception SAXException - */ - public void characters(char[] ch, int offset, int length) throws Exception; - - /** - * Method called when a tag opens - * - * @param uri - * @param localName - * @param qName - * @param attrib - * @exception SAXException - **/ - public void startElement( - String uri, - String localName, - String qName, - Properties attrib) - throws Exception; - /** - * Method called when a tag closes - * - * @param uri - * @param localName - * @param qName - * @exception SAXException - */ - public void endElement( - java.lang.String uri, - java.lang.String localName, - java.lang.String qName) - throws Exception; - - public void processingInstruction(String target, - String data) - throws Exception; - - public void setLineNumber(int lineNumber); - - public void setColumnNumber(int columnNumber); -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/kxmlsax/KXml2SAXParser.java b/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/kxmlsax/KXml2SAXParser.java deleted file mode 100644 index 907a65e3845..00000000000 --- a/bundlerepository/src/main/java/org/apache/felix/bundlerepository/metadataparser/kxmlsax/KXml2SAXParser.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.bundlerepository.metadataparser.kxmlsax; - -import java.io.Reader; -import java.util.Properties; - -import org.kxml2.io.KXmlParser; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -/** - * The KXml2SAXParser extends the XmlParser from kxml2 (which does not take into account the DTD). - */ -public class KXml2SAXParser extends KXmlParser { - - public String uri="uri"; - - /** - * The constructor for a parser, it receives a java.io.Reader. - * - * @param reader The reader - * @throws XmlPullParserException - */ - public KXml2SAXParser(Reader reader) throws XmlPullParserException { - super(); - setInput(reader); - } - - /** - * parse from the reader provided in the constructor, and call - * the startElement and endElement in the handler - * - * @param handler The handler - * @exception Exception thrown by the superclass - */ - public void parseXML(KXml2SAXHandler handler) throws Exception { - - while (next() != XmlPullParser.END_DOCUMENT) { - handler.setLineNumber(getLineNumber()); - handler.setColumnNumber(getColumnNumber()); - if (getEventType() == XmlPullParser.START_TAG) { - Properties props = new Properties(); - for (int i = 0; i < getAttributeCount(); i++) { - props.put(getAttributeName(i), getAttributeValue(i)); - } - handler.startElement( - getNamespace(), - getName(), - getName(), - props); - } else if (getEventType() == XmlPullParser.END_TAG) { - handler.endElement(getNamespace(), getName(), getName()); - } else if (getEventType() == XmlPullParser.TEXT) { - String text = getText(); - handler.characters(text.toCharArray(),0,text.length()); - } else if (getEventType() == XmlPullParser.PROCESSING_INSTRUCTION) { - // TODO extract the target from the evt.getText() - handler.processingInstruction(null,getText()); - } else { - // do nothing - } - } - } -} diff --git a/bundlerepository/src/main/java/org/osgi/service/obr/Repository.java b/bundlerepository/src/main/java/org/osgi/service/obr/Repository.java deleted file mode 100644 index 30adeb9d228..00000000000 --- a/bundlerepository/src/main/java/org/osgi/service/obr/Repository.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * $Header: /cvshome/build/org.osgi.service.obr/src/org/osgi/service/obr/Repository.java,v 1.3 2006/03/16 14:56:17 hargrave Exp $ - * - * Copyright (c) OSGi Alliance (2006). All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// This document is an experimental draft to enable interoperability -// between bundle repositories. There is currently no commitment to -// turn this draft into an official specification. -package org.osgi.service.obr; - -import java.net.URL; - -/** - * Represents a repository. - * - * @version $Revision: 1.3 $ - */ -public interface Repository -{ - /** - * Return the associated URL for the repository. - * - */ - URL getURL(); - - /** - * Return the resources for this repository. - */ - Resource[] getResources(); - - /** - * Return the name of this reposotory. - * - * @return a non-null name - */ - String getName(); - - long getLastModified(); - -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/osgi/service/obr/RepositoryAdmin.java b/bundlerepository/src/main/java/org/osgi/service/obr/RepositoryAdmin.java deleted file mode 100644 index 74688712842..00000000000 --- a/bundlerepository/src/main/java/org/osgi/service/obr/RepositoryAdmin.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * $Header: /cvshome/build/org.osgi.service.obr/src/org/osgi/service/obr/RepositoryAdmin.java,v 1.3 2006/03/16 14:56:17 hargrave Exp $ - * - * Copyright (c) OSGi Alliance (2006). All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// This document is an experimental draft to enable interoperability -// between bundle repositories. There is currently no commitment to -// turn this draft into an official specification. -package org.osgi.service.obr; - -import java.net.URL; - -/** - * Provides centralized access to the distributed repository. - * - * A repository contains a set of resources. A resource contains a - * number of fixed attributes (name, version, etc) and sets of: - *
      - *
    1. Capabilities - Capabilities provide a named aspect: a bundle, a display, - * memory, etc.
    2. - *
    3. Requirements - A named filter expression. The filter must be satisfied - * by one or more Capabilties with the given name. These capabilities can come - * from other resources or from the platform. If multiple resources provide the - * requested capability, one is selected. (### what algorithm? ###)
    4. - *
    5. Requests - Requests are like requirements, except that a request can be - * fullfilled by 0..n resources. This feature can be used to link to resources - * that are compatible with the given resource and provide extra functionality. - * For example, a bundle could request all its known fragments. The UI - * associated with the repository could list these as optional downloads.
    6. - * - * @version $Revision: 1.3 $ - */ -public interface RepositoryAdmin -{ - /** - * Discover any resources that match the given filter. - * - * This is not a detailed search, but a first scan of applicable resources. - * - * ### Checking the capabilities of the filters is not possible because that - * requires a new construct in the filter. - * - * The filter expression can assert any of the main headers of the resource. - * The attributes that can be checked are: - * - *
        - *
      1. name
      2. - *
      3. version (uses filter matching rules)
      4. - *
      5. description
      6. - *
      7. category
      8. - *
      9. copyright
      10. - *
      11. license
      12. - *
      13. source
      14. - *
      - * - * @param filterExpr - * A standard OSGi filter - * @return List of resources matching the filters. - */ - Resource[] discoverResources(String filterExpr); - - /** - * Create a resolver. - * - * @param resource - * @return - */ - Resolver resolver(); - - /** - * Add a new repository to the federation. - * - * The url must point to a repository XML file. - * - * @param repository - * @return - * @throws Exception - */ - Repository addRepository(URL repository) throws Exception; - - boolean removeRepository(URL repository); - - /** - * List all the repositories. - * - * @return - */ - Repository[] listRepositories(); - - Resource getResource(String respositoryId); -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/osgi/service/obr/Resolver.java b/bundlerepository/src/main/java/org/osgi/service/obr/Resolver.java deleted file mode 100644 index 629159bf567..00000000000 --- a/bundlerepository/src/main/java/org/osgi/service/obr/Resolver.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * $Header: /cvshome/build/org.osgi.service.obr/src/org/osgi/service/obr/Resolver.java,v 1.3 2006/03/16 14:56:17 hargrave Exp $ - * - * Copyright (c) OSGi Alliance (2006). All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// This document is an experimental draft to enable interoperability -// between bundle repositories. There is currently no commitment to -// turn this draft into an official specification. -package org.osgi.service.obr; - -public interface Resolver -{ - - void add(Resource resource); - - Requirement[] getUnsatisfiedRequirements(); - - Resource[] getOptionalResources(); - - Requirement[] getReason(Resource resource); - - Resource[] getResources(Requirement requirement); - - Resource[] getRequiredResources(); - - Resource[] getAddedResources(); - - boolean resolve(); - - void deploy(boolean start); -} \ No newline at end of file diff --git a/bundlerepository/src/main/java/org/osgi/service/obr/Resource.java b/bundlerepository/src/main/java/org/osgi/service/obr/Resource.java deleted file mode 100644 index e6b050b82a9..00000000000 --- a/bundlerepository/src/main/java/org/osgi/service/obr/Resource.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * $Header: /cvshome/build/org.osgi.service.obr/src/org/osgi/service/obr/Resource.java,v 1.5 2006/03/16 14:56:17 hargrave Exp $ - * - * Copyright (c) OSGi Alliance (2006). All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// This document is an experimental draft to enable interoperability -// between bundle repositories. There is currently no commitment to -// turn this draft into an official specification. -package org.osgi.service.obr; - -import java.net.URL; -import java.util.Map; - -import org.osgi.framework.Version; - -/** - * A resource is an abstraction of a downloadable thing, like a bundle. - * - * Resources have capabilities and requirements. All a resource's requirements - * must be satisfied before it can be installed. - * - * @version $Revision: 1.5 $ - */ -public interface Resource -{ - final String LICENSE_URL = "license"; - - final String DESCRIPTION = "description"; - - final String DOCUMENTATION_URL = "documentation"; - - final String COPYRIGHT = "copyright"; - - final String SOURCE_URL = "source"; - - final String SYMBOLIC_NAME = "symbolicname"; - - final String PRESENTATION_NAME = "presentationname"; - - final String ID = "id"; - - final String VERSION = "version"; - - final String URL = "url"; - - final String SIZE = "size"; - - final static String[] KEYS = { DESCRIPTION, SIZE, ID, LICENSE_URL, - DOCUMENTATION_URL, COPYRIGHT, SOURCE_URL, PRESENTATION_NAME, - SYMBOLIC_NAME, VERSION, URL }; - - // get readable name - - Map getProperties(); - - String getSymbolicName(); - - String getPresentationName(); - - Version getVersion(); - - String getId(); - - URL getURL(); - - Requirement[] getRequirements(); - - Capability[] getCapabilities(); - - String[] getCategories(); - - Repository getRepository(); -} \ No newline at end of file diff --git a/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/CapabilityImplTest.java b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/CapabilityImplTest.java new file mode 100644 index 00000000000..36435ecff5e --- /dev/null +++ b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/CapabilityImplTest.java @@ -0,0 +1,48 @@ +/* + * 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.bundlerepository.impl; + +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +public class CapabilityImplTest extends TestCase +{ + public void testDirectives() + { + CapabilityImpl c = new CapabilityImpl(); + + assertEquals(0, c.getDirectives().size()); + c.addDirective("x", "y"); + assertEquals(1, c.getDirectives().size()); + assertEquals("y", c.getDirectives().get("x")); + + c.addDirective("x", "z"); + assertEquals(1, c.getDirectives().size()); + assertEquals("z", c.getDirectives().get("x")); + + c.addDirective("Y", "A b C"); + + Map expected = new HashMap(); + expected.put("x", "z"); + expected.put("Y", "A b C"); + assertEquals(expected, c.getDirectives()); + } +} diff --git a/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/DataModelHelperTest.java b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/DataModelHelperTest.java new file mode 100644 index 00000000000..5f13df6c8ae --- /dev/null +++ b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/DataModelHelperTest.java @@ -0,0 +1,140 @@ +/* + * 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.bundlerepository.impl; + +import java.net.URL; +import java.util.Map; +import java.util.jar.Attributes; + +import junit.framework.TestCase; +import org.apache.felix.bundlerepository.Capability; +import org.apache.felix.bundlerepository.DataModelHelper; +import org.apache.felix.bundlerepository.Repository; +import org.apache.felix.bundlerepository.Resource; +import org.apache.felix.utils.manifest.Clause; +import org.osgi.framework.Constants; + +import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class DataModelHelperTest extends TestCase +{ + + private DataModelHelper dmh = new DataModelHelperImpl(); + + public void testResource() throws Exception + { + Attributes attr = new Attributes(); + attr.putValue("Manifest-Version", "1.0"); + attr.putValue("Bundle-Name", "Apache Felix Utils"); + attr.putValue("Bundle-Version", "0.1.0.SNAPSHOT"); + attr.putValue("Bundle-ManifestVersion", "2"); + attr.putValue("Bundle-License", "http://www.apache.org/licenses/LICENSE-2.0.txt"); + attr.putValue("Bundle-Description", "Utility classes for OSGi."); + attr.putValue("Import-Package", "org.osgi.framework;version=\"[1.4,2)\""); + attr.putValue("Bundle-SymbolicName", "org.apache.felix.utils"); + + Resource resource = dmh.createResource(attr); + + String xml = dmh.writeResource(resource); + System.out.println(xml); + + Resource resource2 = dmh.readResource(xml); + String xml2 = dmh.writeResource(resource2); + System.out.println(xml2); + + assertEquals(xml, xml2); + } + + public void testRequirementFilter() throws Exception + { + RequirementImpl r = new RequirementImpl(); + r.setFilter("(&(package=foo.bar)(version>=0.0.0)(version<3.0.0))"); + assertEquals("(&(package=foo.bar)(!(version>=3.0.0)))", r.getFilter()); + + r.setFilter("(&(package=javax.transaction)(partial=true)(mandatory:<*partial))"); + assertEquals("(&(package=javax.transaction)(partial=true)(mandatory:<*partial))", r.getFilter()); + } + + public void testCapabilities() throws Exception { + Attributes attr = new Attributes(); + attr.putValue("Manifest-Version", "1.0"); + attr.putValue("Bundle-Name", "Apache Felix Utils"); + attr.putValue("Bundle-Version", "0.1.0.SNAPSHOT"); + attr.putValue("Bundle-ManifestVersion", "2"); + attr.putValue("Bundle-License", "http://www.apache.org/licenses/LICENSE-2.0.txt"); + attr.putValue("Bundle-Description", "Utility classes for OSGi."); + attr.putValue("Import-Package", "org.osgi.framework;version=\"[1.4,2)\""); + attr.putValue("Bundle-SymbolicName", "org.apache.felix.utils"); + attr.putValue("Provide-Capability", "osgi.extender;osgi.extender=\"osgi.component\";uses:=\"\n" + + " org.osgi.service.component\";version:Version=\"1.3\",osgi.service;objectCl\n" + + " ass:List=\"org.osgi.service.component.runtime.ServiceComponentRu\n" + + " ntime\";uses:=\"org.osgi.service.component.runtime\""); + attr.putValue("Export-Package", "test.package;version=\"1.0.0\""); + + Resource resource = dmh.createResource(attr); + + assertEquals(4, resource.getCapabilities().length); + + Capability bundleCap = null; + Capability osgiExtenderCap = null; + Capability osgiServiceCap = null; + Capability osgiPackageCap = null; + + for (Capability capability : resource.getCapabilities()) { + if (capability.getName().equals("bundle")) { + bundleCap = capability; + } else if (capability.getName().equals("osgi.extender")) { + osgiExtenderCap = capability; + } else if (capability.getName().equals("service")) { + osgiServiceCap = capability; + } else if (capability.getName().equals("package")) { + osgiPackageCap = capability; + } else { + osgiServiceCap = capability; + } + } + + assertNotNull(bundleCap); + assertNotNull(osgiExtenderCap); + assertNotNull(osgiServiceCap); + assertNotNull(osgiPackageCap); + + assertEquals("osgi.extender", osgiExtenderCap.getName()); + assertEquals("osgi.component", osgiExtenderCap.getPropertiesAsMap().get("osgi.extender")); + assertEquals("1.3.0", osgiExtenderCap.getPropertiesAsMap().get(Constants.VERSION_ATTRIBUTE).toString()); + + assertEquals("service", osgiServiceCap.getName()); + + assertEquals("package", osgiPackageCap.getName()); + } + + public void testGzipResource() throws Exception { + URL urlArchive = getClass().getResource("/spec_repository.gz"); + assertNotNull("GZ archive was not found", urlArchive); + Repository repository1 = dmh.repository(urlArchive); + + URL urlRepo = getClass().getResource("/spec_repository.xml"); + assertNotNull("Repository file was not found", urlRepo); + Repository repository2 = dmh.repository(urlRepo); + assertEquals(repository1.getName(), repository2.getName()); + assertEquals(repository1.getResources().length, repository2.getResources().length); + } +} diff --git a/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/FelixRequirementAdapterTest.java b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/FelixRequirementAdapterTest.java new file mode 100644 index 00000000000..ca909e15611 --- /dev/null +++ b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/FelixRequirementAdapterTest.java @@ -0,0 +1,74 @@ +/* + * 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.bundlerepository.impl; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; + +public class FelixRequirementAdapterTest extends TestCase +{ + public void testDirectiveTranslation() + { + assertFilter("(foo=bar)", "(foo=bar)"); + assertFilter("(package=x.y.z)", "(osgi.wiring.package=x.y.z)"); + // TODO should this be symbolicname? + assertFilter("( bundle = abc )", "(osgi.wiring.bundle= abc )"); + assertFilter("(service=xyz)", "(osgi.service=xyz)"); + assertFilter("(|(bundle=x)(&(bundle=y)(fragment=z)))", + "(|(osgi.wiring.bundle=x)(&(osgi.wiring.bundle=y)(osgi.wiring.host=z)))"); + } + + private void assertFilter(String obr, String osgi) + { + Resource resource = new OSGiResourceImpl( + Collections.emptyList(), + Collections.emptyList()); + + RequirementImpl requirement = new RequirementImpl(); + requirement.setFilter(obr); + assertEquals(osgi, new FelixRequirementAdapter(requirement, resource).getDirectives().get("filter")); + } + + public void testOtherDirectives() + { + Resource resource = new OSGiResourceImpl( + Collections.emptyList(), + Collections.emptyList()); + + RequirementImpl requirement = new RequirementImpl(); + requirement.setFilter("(a=b)"); + Map other = new HashMap(); + other.put("xyz", "abc"); + requirement.setDirectives(other); + + FelixRequirementAdapter adapter = new FelixRequirementAdapter(requirement, resource); + + Map expected = new HashMap(); + expected.put("filter", "(a=b)"); + expected.put("xyz", "abc"); + assertEquals(expected, adapter.getDirectives()); + } +} diff --git a/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/LazyStringMapTest.java b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/LazyStringMapTest.java new file mode 100644 index 00000000000..0290b15121f --- /dev/null +++ b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/LazyStringMapTest.java @@ -0,0 +1,72 @@ +/* + * 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.bundlerepository.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicInteger; + +import junit.framework.TestCase; +import org.apache.felix.bundlerepository.impl.LazyStringMap.LazyValue; + +public class LazyStringMapTest extends TestCase +{ + public void testLazyHashMap() { + final AtomicInteger lv1Computed = new AtomicInteger(0); + LazyValue lv1 = new LazyValue() { + public Long compute() { + lv1Computed.incrementAndGet(); + return 24L; + } + }; + + final AtomicInteger lv2Computed = new AtomicInteger(0); + LazyValue lv2 = new LazyValue() { + public Long compute() { + lv2Computed.incrementAndGet(); + return 0L; + } + }; + + Collection> lazyValues = new ArrayList>(); + lazyValues.add(lv1); + lazyValues.add(lv2); + LazyStringMap lhm = new LazyStringMap(); + lhm.put("1", 2L); + lhm.putLazy("42", lv1); + lhm.putLazy("zero", lv2); + + assertEquals(new Long(2L), lhm.get("1")); + assertEquals("No computation should have happened yet", 0, lv1Computed.get()); + assertEquals("No computation should have happened yet", 0, lv2Computed.get()); + + assertEquals(new Long(24L), lhm.get("42")); + assertEquals("lv1 should have been computed", 1, lv1Computed.get()); + assertEquals("No computation should have happened yet for lv2", 0, lv2Computed.get()); + + lhm.put("zero", -1L); + assertEquals(new Long(-1L), lhm.get("zero")); + assertEquals("lv1 should have been computed", 1, lv1Computed.get()); + assertEquals("No computation should have happened for lv2, as we put a value in for it", + 0, lv2Computed.get()); + } +} diff --git a/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/NamespaceTranslatorTest.java b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/NamespaceTranslatorTest.java new file mode 100644 index 00000000000..94f50f4513c --- /dev/null +++ b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/NamespaceTranslatorTest.java @@ -0,0 +1,53 @@ +/* + * 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.bundlerepository.impl; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import junit.framework.TestCase; + +public class NamespaceTranslatorTest extends TestCase +{ + public void testNamespaceTranslator() + { + Map expected = new HashMap(); + expected.put("osgi.wiring.bundle", "bundle"); + expected.put("osgi.wiring.package", "package"); + expected.put("osgi.wiring.host", "fragment"); + expected.put("osgi.service", "service"); + + assertEquals(new HashSet(expected.keySet()), + new HashSet(NamespaceTranslator.getTranslatedOSGiNamespaces())); + assertEquals(new HashSet(expected.values()), + new HashSet(NamespaceTranslator.getTranslatedFelixNamespaces())); + + for (Map.Entry entry : expected.entrySet()) + { + assertEquals(entry.getValue(), + NamespaceTranslator.getFelixNamespace(entry.getKey())); + assertEquals(entry.getKey(), + NamespaceTranslator.getOSGiNamespace(entry.getValue())); + } + + assertEquals("bheuaark", NamespaceTranslator.getFelixNamespace("bheuaark")); + assertEquals("bheuaark", NamespaceTranslator.getOSGiNamespace("bheuaark")); + } +} diff --git a/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/OSGiRepositoryImplTest.java b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/OSGiRepositoryImplTest.java new file mode 100644 index 00000000000..902b856c57b --- /dev/null +++ b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/OSGiRepositoryImplTest.java @@ -0,0 +1,247 @@ +/* + * 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.bundlerepository.impl; + +import junit.framework.TestCase; + +import org.apache.felix.bundlerepository.Reason; +import org.apache.felix.bundlerepository.Resolver; +import org.apache.felix.utils.log.Logger; +import org.apache.felix.utils.resource.CapabilityImpl; +import org.apache.felix.utils.resource.RequirementImpl; +import org.mockito.Mockito; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Version; +import org.osgi.framework.namespace.BundleNamespace; +import org.osgi.framework.namespace.IdentityNamespace; +import org.osgi.framework.namespace.PackageNamespace; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; +import org.osgi.service.repository.ContentNamespace; +import org.osgi.service.repository.Repository; +import org.osgi.service.repository.RepositoryContent; + +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; + +public class OSGiRepositoryImplTest extends TestCase +{ + public void testCapabilities() throws Exception + { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + URL url = getClass().getResource("/another_repository.xml"); + repoAdmin.addRepository(url); + + Repository repo = new OSGiRepositoryImpl(repoAdmin); + Requirement req = new RequirementImpl(Mockito.mock(Resource.class), "osgi.identity", null); + + Map> result = repo.findProviders(Collections.singleton(req)); + assertEquals(1, result.size()); + Collection caps = result.values().iterator().next(); + assertEquals(2, caps.size()); + + Capability tf1Cap = null; + for (Capability cap : caps) + { + if ("test_file_1".equals(cap.getAttributes().get(IdentityNamespace.IDENTITY_NAMESPACE))) { + tf1Cap = cap; + break; + } + } + + assertEquals(Version.parseVersion("1.0.0.SNAPSHOT"), tf1Cap.getAttributes().get(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE)); + assertEquals(IdentityNamespace.TYPE_BUNDLE, tf1Cap.getAttributes().get(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE)); + + Resource res = tf1Cap.getResource(); + assertEquals(0, res.getRequirements(null).size()); + assertEquals(1, res.getCapabilities(IdentityNamespace.IDENTITY_NAMESPACE).size()); + assertEquals(1, res.getCapabilities(ContentNamespace.CONTENT_NAMESPACE).size()); + assertEquals(1, res.getCapabilities(BundleNamespace.BUNDLE_NAMESPACE).size()); + assertEquals(8, res.getCapabilities(PackageNamespace.PACKAGE_NAMESPACE).size()); + assertEquals(1, res.getCapabilities("foo").size()); + assertEquals(12, res.getCapabilities(null).size()); + + Capability contentCap = res.getCapabilities(ContentNamespace.CONTENT_NAMESPACE).iterator().next(); + assertEquals("4b68ab3847feda7d6c62c1fbcbeebfa35eab7351ed5e78f4ddadea5df64b8015", + contentCap.getAttributes().get(ContentNamespace.CONTENT_NAMESPACE)); + assertEquals(getClass().getResource("/repo_files/test_file_1.jar").toExternalForm(), + contentCap.getAttributes().get(ContentNamespace.CAPABILITY_URL_ATTRIBUTE)); + assertEquals(1L, contentCap.getAttributes().get(ContentNamespace.CAPABILITY_SIZE_ATTRIBUTE)); + assertEquals("application/vnd.osgi.bundle", contentCap.getAttributes().get(ContentNamespace.CAPABILITY_MIME_ATTRIBUTE)); + + Capability bundleCap = res.getCapabilities(BundleNamespace.BUNDLE_NAMESPACE).iterator().next(); + assertEquals("2", bundleCap.getAttributes().get("manifestversion")); + assertEquals("dummy", bundleCap.getAttributes().get(BundleNamespace.BUNDLE_NAMESPACE)); + assertEquals(Version.parseVersion("1.0.0.SNAPSHOT"), bundleCap.getAttributes().get(BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE)); + assertEquals("Unnamed - dummy", bundleCap.getAttributes().get("presentationname")); + + Capability packageCap = res.getCapabilities(PackageNamespace.PACKAGE_NAMESPACE).get(7); + assertEquals("org.apache.commons.logging", packageCap.getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE)); + assertEquals(Version.parseVersion("1.0.4"), packageCap.getAttributes().get(PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE)); + assertEquals("dummy", packageCap.getAttributes().get(PackageNamespace.CAPABILITY_BUNDLE_SYMBOLICNAME_ATTRIBUTE)); + assertEquals(Version.parseVersion("1.0.0.SNAPSHOT"), packageCap.getAttributes().get(PackageNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE)); + + Capability fooCap = res.getCapabilities("foo").iterator().next(); + assertEquals("someVal", fooCap.getAttributes().get("someKey")); + } + + public void testIdentityCapabilityFilter() throws Exception + { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + URL url = getClass().getResource("/another_repository.xml"); + repoAdmin.addRepository(url); + + Repository repo = new OSGiRepositoryImpl(repoAdmin); + Requirement req = new RequirementImpl(Mockito.mock(Resource.class), "osgi.identity", "(osgi.identity=test_file_2)"); + + Map> result = repo.findProviders(Collections.singleton(req)); + assertEquals(1, result.size()); + Collection caps = result.values().iterator().next(); + assertEquals(1, caps.size()); + Capability cap = caps.iterator().next(); + + assertEquals("test_file_2", cap.getAttributes().get(IdentityNamespace.IDENTITY_NAMESPACE)); + assertEquals(Version.parseVersion("1.0.0"), cap.getAttributes().get(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE)); + assertEquals(IdentityNamespace.TYPE_BUNDLE, cap.getAttributes().get(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE)); + } + + public void testFilterOnCapability() throws Exception + { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + URL url = getClass().getResource("/another_repository.xml"); + repoAdmin.addRepository(url); + + Repository repo = new OSGiRepositoryImpl(repoAdmin); + Requirement req = new RequirementImpl(Mockito.mock(Resource.class), "foo", "(someKey=someOtherVal)"); + + Map> result = repo.findProviders(Collections.singleton(req)); + assertEquals(1, result.size()); + Collection caps = result.values().iterator().next(); + assertEquals(1, caps.size()); + + Resource res = caps.iterator().next().getResource(); + assertEquals("test_file_2", + res.getCapabilities(IdentityNamespace.IDENTITY_NAMESPACE).iterator().next(). + getAttributes().get(IdentityNamespace.IDENTITY_NAMESPACE)); + } + + public void testFilterOnCapabilityExistence() throws Exception + { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + URL url = getClass().getResource("/another_repository.xml"); + repoAdmin.addRepository(url); + + Repository repo = new OSGiRepositoryImpl(repoAdmin); + Requirement req = new RequirementImpl(Mockito.mock(Resource.class), "foo", "(someKey=*)"); + + Map> result = repo.findProviders(Collections.singleton(req)); + assertEquals(1, result.size()); + Collection caps = result.values().iterator().next(); + assertEquals(2, caps.size()); + + Set identities = new HashSet(); + for (Capability cap : caps) + { + identities.add(cap.getResource().getCapabilities(IdentityNamespace.IDENTITY_NAMESPACE). + iterator().next().getAttributes().get(IdentityNamespace.IDENTITY_NAMESPACE)); + } + + Set expected = new HashSet(Arrays.asList("test_file_1", "test_file_2")); + assertEquals(expected, identities); + } + + public void testRepositoryContent() throws Exception { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + URL url = getClass().getResource("/another_repository.xml"); + repoAdmin.addRepository(url); + + Repository repo = new OSGiRepositoryImpl(repoAdmin); + Requirement req = new RequirementImpl(Mockito.mock(Resource.class), "osgi.wiring.package", + "(&(osgi.wiring.package=org.apache.commons.logging)(version>=1.0.1)(!(version>=2)))"); + + Map> result = repo.findProviders(Collections.singleton(req)); + assertEquals(1, result.size()); + Collection caps = result.values().iterator().next(); + assertEquals(1, caps.size()); + Capability cap = caps.iterator().next(); + assertEquals("osgi.wiring.package", cap.getNamespace()); + assertEquals("org.apache.commons.logging", cap.getAttributes().get("osgi.wiring.package")); + assertEquals(Version.parseVersion("1.0.4"), cap.getAttributes().get("version")); + + Resource resource = cap.getResource(); + RepositoryContent rc = (RepositoryContent) resource; // Repository Resources must implement this interface + byte[] actualBytes = Streams.suck(rc.getContent()); + + URL actualURL = getClass().getResource("/repo_files/test_file_1.jar"); + byte[] expectedBytes = Streams.suck(actualURL.openStream()); + + assertTrue(Arrays.equals(expectedBytes, actualBytes)); + } + + public void testSystemBundleCapabilities() throws Exception { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + Resolver resolver = repoAdmin.resolver(); + org.apache.felix.bundlerepository.impl.RequirementImpl req = + new org.apache.felix.bundlerepository.impl.RequirementImpl("some.system.cap"); + req.setFilter("(sys.cap=something)"); + resolver.add(req); + ResourceImpl res = new ResourceImpl(); + res.addRequire(req); + + resolver.add(res); + assertTrue(resolver.resolve()); + + // This should add the system bundle repo to the resolved set. + org.apache.felix.bundlerepository.Resource sysBundleRes = repoAdmin.getSystemRepository().getResources()[0]; + Reason[] reason = resolver.getReason(sysBundleRes); + assertTrue(reason.length >= 1); + assertEquals(req, reason[0].getRequirement()); + } + + private RepositoryAdminImpl createRepositoryAdmin() throws Exception + { + Bundle sysBundle = Mockito.mock(Bundle.class); + Mockito.when(sysBundle.getHeaders()).thenReturn(new Hashtable()); + + BundleRevision br = Mockito.mock(BundleRevision.class); + Mockito.when(sysBundle.adapt(BundleRevision.class)).thenReturn(br); + Capability cap1 = new CapabilityImpl(Mockito.mock(Resource.class), "some.system.cap", + Collections.singletonMap("x", "y"), + Collections.singletonMap("sys.cap", "something")); + Capability cap2 = new CapabilityImpl(Mockito.mock(Resource.class), "some.system.cap", + Collections.emptyMap(), + Collections.singletonMap("sys.cap", "somethingelse")); + Mockito.when(br.getCapabilities(null)).thenReturn(Arrays.asList(cap1, cap2)); + + BundleContext bc = Mockito.mock(BundleContext.class); + Mockito.when(bc.getBundle(0)).thenReturn(sysBundle); + Mockito.when(sysBundle.getBundleContext()).thenReturn(bc); + + return new RepositoryAdminImpl(bc, new Logger(bc)); + } +} diff --git a/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/OSGiRepositoryXMLTest.java b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/OSGiRepositoryXMLTest.java new file mode 100644 index 00000000000..93a43104010 --- /dev/null +++ b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/OSGiRepositoryXMLTest.java @@ -0,0 +1,219 @@ +/* + * 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.bundlerepository.impl; + +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import junit.framework.TestCase; + +import org.apache.felix.bundlerepository.Resolver; +import org.apache.felix.utils.log.Logger; +import org.apache.felix.utils.resource.RequirementImpl; +import org.mockito.Mockito; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Version; +import org.osgi.framework.namespace.IdentityNamespace; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; +import org.osgi.service.repository.ContentNamespace; +import org.osgi.service.repository.Repository; + +public class OSGiRepositoryXMLTest extends TestCase { + public void testIdentityCapability() throws Exception { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + URL url = getClass().getResource("/spec_repository.xml"); + repoAdmin.addRepository(url); + + Repository repo = new OSGiRepositoryImpl(repoAdmin); + Requirement req = new RequirementImpl(Mockito.mock(Resource.class), + "osgi.identity", + "(osgi.identity=cdi-subsystem)"); + + Map> result = repo + .findProviders(Collections.singleton(req)); + assertEquals(1, result.size()); + Collection caps = result.values().iterator().next(); + assertEquals(1, caps.size()); + Capability cap = caps.iterator().next(); + + assertEquals("cdi-subsystem", + cap.getAttributes().get(IdentityNamespace.IDENTITY_NAMESPACE)); + assertEquals(Version.parseVersion("0.5.0"), cap.getAttributes() + .get(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE)); + assertEquals("osgi.subsystem.feature", cap.getAttributes() + .get(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE)); + } + + public void testIdentityCapabilityWithRelativePath() throws Exception { + URL url = getClass().getResource("/spec_repository.xml"); + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + RepositoryImpl repository = (RepositoryImpl) repoAdmin.addRepository(url); + + Resolver resolver = repoAdmin.resolver(); + + org.apache.felix.bundlerepository.Resource[] discoverResources = repoAdmin + .discoverResources( + "(symbolicname=org.apache.felix.bundlerepository.test_file_6*)"); + assertNotNull(discoverResources); + assertEquals(1, discoverResources.length); + + resolver.add(discoverResources[0]); + assertTrue(resolver.resolve()); + + org.apache.felix.bundlerepository.Resource[] resources = resolver.getAddedResources(); + assertNotNull(resources[0]); + + String repositoryUri = repository.getURI(); + String baseUri = repositoryUri.substring(0, repositoryUri.lastIndexOf('/') + 1); + String resourceUri = new StringBuilder(baseUri).append("repo_files/test_file_6.jar").toString(); + assertEquals(resourceUri, resources[0].getURI()); + } + + public void testIdentityCapabilityForZipWithRelativePath() throws Exception { + URL url = getClass().getResource("/spec_repository.zip"); + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + RepositoryImpl repository = (RepositoryImpl) repoAdmin.addRepository(url); + + Resolver resolver = repoAdmin.resolver(); + + org.apache.felix.bundlerepository.Resource[] discoverResources = repoAdmin + .discoverResources( + "(symbolicname=org.apache.felix.bundlerepository.test_file_6*)"); + assertNotNull(discoverResources); + assertEquals(1, discoverResources.length); + + resolver.add(discoverResources[0]); + assertTrue(resolver.resolve()); + + org.apache.felix.bundlerepository.Resource[] resources = resolver.getAddedResources(); + assertNotNull(resources[0]); + + String repositoryUri = repository.getURI(); + String baseUri = new StringBuilder("jar:").append(repositoryUri).append("!/").toString(); + String resourceUri = new StringBuilder(baseUri).append("repo_files/test_file_6.jar").toString(); + assertEquals(resourceUri, resources[0].getURI()); + } + + + public void testOtherIdentityAttribute() throws Exception { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + URL url = getClass().getResource("/spec_repository.xml"); + repoAdmin.addRepository(url); + + Repository repo = new OSGiRepositoryImpl(repoAdmin); + Requirement req = new RequirementImpl(Mockito.mock(Resource.class), + "osgi.identity", + "(license=http://www.opensource.org/licenses/mytestlicense)"); + + Map> result = repo + .findProviders(Collections.singleton(req)); + assertEquals(1, result.size()); + Collection caps = result.values().iterator().next(); + assertEquals(1, caps.size()); + Capability cap = caps.iterator().next(); + assertEquals("org.apache.felix.bundlerepository.test_file_3", + cap.getAttributes().get(IdentityNamespace.IDENTITY_NAMESPACE)); + } + + public void testContentCapability() throws Exception { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + URL url = getClass().getResource("/spec_repository.xml"); + repoAdmin.addRepository(url); + + Repository repo = new OSGiRepositoryImpl(repoAdmin); + Requirement req = new RequirementImpl(Mockito.mock(Resource.class),"foo", "(bar=toast)"); + + Map> result = repo + .findProviders(Collections.singleton(req)); + assertEquals(1, result.size()); + Collection caps = result.values().iterator().next(); + assertEquals(1, caps.size()); + Capability cap = caps.iterator().next(); + + assertEquals("foo", cap.getNamespace()); + assertEquals(0, cap.getDirectives().size()); + assertEquals(1, cap.getAttributes().size()); + Entry fooCap = cap.getAttributes().entrySet().iterator() + .next(); + assertEquals("bar", fooCap.getKey()); + assertEquals("toast", fooCap.getValue()); + + Resource res = cap.getResource(); + List idCaps = res + .getCapabilities(IdentityNamespace.IDENTITY_NAMESPACE); + assertEquals(1, idCaps.size()); + Capability idCap = idCaps.iterator().next(); + + assertEquals("org.apache.felix.bundlerepository.test_file_3", idCap + .getAttributes().get(IdentityNamespace.IDENTITY_NAMESPACE)); + assertEquals(Version.parseVersion("1.2.3.something"), + idCap.getAttributes() + .get(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE)); + assertEquals("osgi.bundle", idCap.getAttributes() + .get(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE)); + + List contentCaps = res + .getCapabilities(ContentNamespace.CONTENT_NAMESPACE); + assertEquals(1, contentCaps.size()); + Capability contentCap = contentCaps.iterator().next(); + + assertEquals( + "b5d4045c3f466fa91fe2cc6abe79232a1a57cdf104f7a26e716e0a1e2789df78", + contentCap.getAttributes() + .get(ContentNamespace.CONTENT_NAMESPACE)); + assertEquals(new Long(3), contentCap.getAttributes() + .get(ContentNamespace.CAPABILITY_SIZE_ATTRIBUTE)); + assertEquals("application/vnd.osgi.bundle", contentCap.getAttributes() + .get(ContentNamespace.CAPABILITY_MIME_ATTRIBUTE)); + + URL fileURL = getClass().getResource("/repo_files/test_file_3.jar"); + byte[] expectedBytes = Streams.suck(fileURL.openStream()); + + String resourceURL = (String) contentCap.getAttributes() + .get(ContentNamespace.CAPABILITY_URL_ATTRIBUTE); + byte[] actualBytes = Streams.suck(new URL(resourceURL).openStream()); + assertEquals(3L, actualBytes.length); + assertTrue(Arrays.equals(expectedBytes, actualBytes)); + } + + private RepositoryAdminImpl createRepositoryAdmin() throws Exception { + Bundle sysBundle = Mockito.mock(Bundle.class); + Mockito.when(sysBundle.getHeaders()) + .thenReturn(new Hashtable()); + BundleRevision br = Mockito.mock(BundleRevision.class); + Mockito.when(sysBundle.adapt(BundleRevision.class)).thenReturn(br); + + BundleContext bc = Mockito.mock(BundleContext.class); + Mockito.when(bc.getBundle(0)).thenReturn(sysBundle); + Mockito.when(sysBundle.getBundleContext()).thenReturn(bc); + + return new RepositoryAdminImpl(bc, new Logger(bc)); + } + +} diff --git a/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/OSGiRequirementAdapterTest.java b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/OSGiRequirementAdapterTest.java new file mode 100644 index 00000000000..28e87339c9e --- /dev/null +++ b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/OSGiRequirementAdapterTest.java @@ -0,0 +1,56 @@ +/* + * 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.bundlerepository.impl; + +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +import org.apache.felix.utils.resource.RequirementImpl; +import org.mockito.Mockito; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; + +public class OSGiRequirementAdapterTest extends TestCase +{ + public void testDirectives() + { + Map attrs = new HashMap(); + Map dirs = new HashMap(); + dirs.put("cardinality", "multiple"); + dirs.put("filter", "(osgi.wiring.package=y)"); + dirs.put("foo", "bar"); + dirs.put("resolution", "optional"); + dirs.put("test", "test"); + + Requirement req = new RequirementImpl(Mockito.mock(Resource.class), "osgi.wiring.package", dirs, attrs); + OSGiRequirementAdapter adapter = new OSGiRequirementAdapter(req); + + assertEquals("(package=y)", adapter.getFilter()); + assertTrue(adapter.isMultiple()); + assertTrue(adapter.isOptional()); + assertEquals("package", adapter.getName()); + + Map expected = new HashMap(); + expected.put("foo", "bar"); + expected.put("test", "test"); + assertEquals(expected, adapter.getDirectives()); + } +} diff --git a/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/RepositoryAdminTest.java b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/RepositoryAdminTest.java new file mode 100644 index 00000000000..68133a092df --- /dev/null +++ b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/RepositoryAdminTest.java @@ -0,0 +1,119 @@ +/* + * 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.bundlerepository.impl; + +import java.net.URL; +import java.util.Collections; +import java.util.Hashtable; + +import junit.framework.TestCase; + +import org.apache.felix.bundlerepository.Repository; +import org.apache.felix.bundlerepository.Resource; +import org.apache.felix.utils.filter.FilterImpl; +import org.apache.felix.utils.log.Logger; +import org.easymock.Capture; +import org.easymock.EasyMock; +import org.easymock.IAnswer; +import org.easymock.internal.matchers.Captures; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleListener; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.resource.Capability; + +public class RepositoryAdminTest extends TestCase +{ + public void testResourceFilterOnCapabilities() throws Exception + { + URL url = getClass().getResource("/repo_for_resolvertest.xml"); + + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + RepositoryImpl repo = (RepositoryImpl) repoAdmin.addRepository(url); + + Resource[] resources = repoAdmin.discoverResources("(category<*dummy)"); + assertNotNull(resources); + assertEquals(1, resources.length); + + resources = repoAdmin.discoverResources("(category*>dummy)"); + assertNotNull(resources); + assertEquals(1, resources.length); + } + + public void testRemoveRepository() throws Exception { + URL url = getClass().getResource("/repo_for_resolvertest.xml"); + + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + Repository repository = repoAdmin.addRepository(url); + assertNotNull(repository); + + String repositoryUri = repository.getURI(); + assertNotNull(repositoryUri); + + assertTrue(repoAdmin.removeRepository(repositoryUri)); + for (Repository repo : repoAdmin.listRepositories()) { + assertNotSame(repositoryUri, repo.getURI()); + } + } + + private RepositoryAdminImpl createRepositoryAdmin() throws Exception + { + BundleContext bundleContext = EasyMock.createMock(BundleContext.class); + Bundle systemBundle = EasyMock.createMock(Bundle.class); + BundleRevision systemBundleRevision = EasyMock.createMock(BundleRevision.class); + + Activator.setContext(bundleContext); + EasyMock.expect(bundleContext.getProperty((String) EasyMock.anyObject())).andReturn(null).anyTimes(); + EasyMock.expect(bundleContext.getBundle(0)).andReturn(systemBundle); + EasyMock.expect(systemBundle.getHeaders()).andReturn(new Hashtable()); + EasyMock.expect(systemBundle.getRegisteredServices()).andReturn(null); + EasyMock.expect(new Long(systemBundle.getBundleId())).andReturn(new Long(0)).anyTimes(); + EasyMock.expect(systemBundle.getBundleContext()).andReturn(bundleContext); + EasyMock.expect(systemBundleRevision.getCapabilities(null)).andReturn(Collections.emptyList()); + EasyMock.expect(systemBundle.adapt(BundleRevision.class)).andReturn(systemBundleRevision); + bundleContext.addBundleListener((BundleListener) EasyMock.anyObject()); + bundleContext.addServiceListener((ServiceListener) EasyMock.anyObject()); + EasyMock.expect(bundleContext.getBundles()).andReturn(new Bundle[] { systemBundle }); + final Capture c = new Capture(); + EasyMock.expect(bundleContext.createFilter((String) capture(c))).andAnswer(new IAnswer() { + public Object answer() throws Throwable { + return FilterImpl.newInstance((String) c.getValue()); + } + }).anyTimes(); + EasyMock.replay(new Object[] { bundleContext, systemBundle, systemBundleRevision }); + + RepositoryAdminImpl repoAdmin = new RepositoryAdminImpl(bundleContext, new Logger(bundleContext)); + + // force initialization && remove all initial repositories + org.apache.felix.bundlerepository.Repository[] repos = repoAdmin.listRepositories(); + for (int i = 0; repos != null && i < repos.length; i++) + { + repoAdmin.removeRepository(repos[i].getURI()); + } + + return repoAdmin; + } + + static Object capture(Capture capture) { + EasyMock.reportMatcher(new Captures(capture)); + return null; + } + +} diff --git a/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/RepositoryImplTest.java b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/RepositoryImplTest.java new file mode 100644 index 00000000000..d46d8cbe44a --- /dev/null +++ b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/RepositoryImplTest.java @@ -0,0 +1,144 @@ +/* + * 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.bundlerepository.impl; + +import java.net.URL; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; + +import junit.framework.TestCase; + +import org.apache.felix.bundlerepository.Repository; +import org.apache.felix.bundlerepository.Resource; +import org.apache.felix.utils.log.Logger; +import org.easymock.EasyMock; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleListener; +import org.osgi.framework.Filter; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.resource.Capability; + +public class RepositoryImplTest extends TestCase +{ + public void testReferral1() throws Exception + { + URL url = getClass().getResource("/referral1_repository.xml"); + + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + RepositoryImpl repo = (RepositoryImpl) repoAdmin.addRepository(url); + Referral[] refs = repo.getReferrals(); + + assertNotNull("Expect referrals", refs); + assertTrue("Expect one referral", refs.length == 1); + + // + assertEquals(1, refs[0].getDepth()); + assertEquals("referred.xml", refs[0].getUrl()); + + // expect two resources + Resource[] res = repoAdmin.discoverResources((String) null); + assertNotNull("Expect Resource", res); + assertEquals("Expect two resources", 2, res.length); + + // first resource is from the referral1_repository.xml + assertEquals("6", res[0].getId()); +// assertEquals("referral1_repository", res[0].getRepository().getName()); + + // second resource is from the referred.xml + assertEquals("99", res[1].getId()); +// assertEquals("referred", res[1].getRepository().getName()); + } + + public void testReferral2() throws Exception + { + URL url = getClass().getResource("/referral1_repository.xml"); + + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + RepositoryImpl repo = repoAdmin.addRepository(url, 1); + Referral[] refs = repo.getReferrals(); + + assertNotNull("Expect referrals", refs); + assertTrue("Expect one referral", refs.length == 1); + + // + assertEquals(1, refs[0].getDepth()); + assertEquals("referred.xml", refs[0].getUrl()); + + // expect one resource (referral is not followed + Resource[] res = repoAdmin.discoverResources((String) null); + assertNotNull("Expect Resource", res); + assertEquals("Expect one resource", 1, res.length); + + // first resource is from the referral1_repository.xml + assertEquals("6", res[0].getId()); +// assertEquals("referral1_repository", res[0].getRepository().getName()); + } + + private RepositoryAdminImpl createRepositoryAdmin() throws Exception + { + BundleContext bundleContext = EasyMock.createMock(BundleContext.class); + Bundle systemBundle = EasyMock.createMock(Bundle.class); + BundleRevision systemBundleRevision = EasyMock.createMock(BundleRevision.class); + + Activator.setContext(bundleContext); + EasyMock.expect(bundleContext.getProperty((String) EasyMock.anyObject())).andReturn(null).anyTimes(); + EasyMock.expect(bundleContext.getBundle(0)).andReturn(systemBundle); + EasyMock.expect(systemBundle.getHeaders()).andReturn(new Hashtable()); + EasyMock.expect(systemBundle.getRegisteredServices()).andReturn(null); + EasyMock.expect(new Long(systemBundle.getBundleId())).andReturn(new Long(0)).anyTimes(); + EasyMock.expect(systemBundle.getBundleContext()).andReturn(bundleContext); + EasyMock.expect(systemBundleRevision.getCapabilities(null)).andReturn(Collections.emptyList()); + EasyMock.expect(systemBundle.adapt(BundleRevision.class)).andReturn(systemBundleRevision); + bundleContext.addBundleListener((BundleListener) EasyMock.anyObject()); + bundleContext.addServiceListener((ServiceListener) EasyMock.anyObject()); + EasyMock.expect(bundleContext.getBundles()).andReturn(new Bundle[] { systemBundle }); + EasyMock.expect(bundleContext.createFilter(null)).andReturn(new Filter() { + public boolean match(ServiceReference reference) { + return true; + } + public boolean match(Dictionary dictionary) { + return true; + } + public boolean matchCase(Dictionary dictionary) { + return true; + } + public boolean matches(Map map) { + return true; + } + }).anyTimes(); + EasyMock.replay(new Object[] { bundleContext, systemBundle, systemBundleRevision }); + + RepositoryAdminImpl repoAdmin = new RepositoryAdminImpl(bundleContext, new Logger(bundleContext)); + + // force initialization && remove all initial repositories + Repository[] repos = repoAdmin.listRepositories(); + for (int i = 0; repos != null && i < repos.length; i++) + { + repoAdmin.removeRepository(repos[i].getURI()); + } + + return repoAdmin; + } + +} \ No newline at end of file diff --git a/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/ResolverImplTest.java b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/ResolverImplTest.java new file mode 100644 index 00000000000..867010768d5 --- /dev/null +++ b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/ResolverImplTest.java @@ -0,0 +1,320 @@ +/* + * 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.bundlerepository.impl; + +import java.io.InputStream; +import java.net.URL; +import java.util.Collections; +import java.util.Hashtable; + +import junit.framework.TestCase; + +import org.apache.felix.bundlerepository.*; +import org.apache.felix.utils.filter.FilterImpl; +import org.apache.felix.utils.log.Logger; + +import org.easymock.Capture; +import org.easymock.EasyMock; +import org.easymock.IAnswer; +import org.easymock.internal.matchers.Captures; +import org.osgi.framework.*; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.resource.Capability; + +public class ResolverImplTest extends TestCase +{ + public void testReferral1() throws Exception + { + + URL url = getClass().getResource("/repo_for_resolvertest.xml"); + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + RepositoryImpl repo = (RepositoryImpl) repoAdmin.addRepository(url); + + Resolver resolver = repoAdmin.resolver(); + + Resource[] discoverResources = repoAdmin.discoverResources("(symbolicname=org.apache.felix.test*)"); + assertNotNull(discoverResources); + assertEquals(1, discoverResources.length); + + resolver.add(discoverResources[0]); + assertTrue(resolver.resolve()); + } + + public void testSpec() throws Exception + { + URL url = getClass().getResource("/spec_repository.xml"); + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + repoAdmin.addRepository(url); + + Resolver resolver = repoAdmin.resolver(); + + RequirementImpl requirement = new RequirementImpl("foo"); + requirement.setFilter("(bar=toast)"); + + Requirement[] requirements = { requirement }; + + Resource[] discoverResources = repoAdmin.discoverResources(requirements); + assertNotNull(discoverResources); + assertEquals(1, discoverResources.length); + + resolver.add(discoverResources[0]); + assertTrue("Resolver could not resolve", resolver.resolve()); + } + + public void testSpec2() throws Exception + { + URL url = getClass().getResource("/spec_repository.xml"); + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + repoAdmin.addRepository(url); + + Resolver resolver = repoAdmin.resolver(); + + // Create a Local Resource with an extender capability + CapabilityImpl capability = new CapabilityImpl("osgi.extender"); + capability.addProperty("osgi.extender", "osgi.component"); + capability.addProperty("version", "Version", "1.3"); + + org.apache.felix.bundlerepository.Capability[] capabilities = { capability }; + + Resource resource = EasyMock.createMock(Resource.class); + EasyMock.expect(resource.getSymbolicName()).andReturn("com.test.bundleA").anyTimes(); + EasyMock.expect(resource.getRequirements()).andReturn(null).anyTimes(); + EasyMock.expect(resource.getCapabilities()).andReturn(capabilities).anyTimes(); + EasyMock.expect(resource.getURI()).andReturn("http://test.com").anyTimes(); + EasyMock.expect(resource.isLocal()).andReturn(true).anyTimes(); + + // Create a Local Resource with a service capability + CapabilityImpl capability2 = new CapabilityImpl("service"); + capability2.addProperty("objectClass", "org.some.other.interface"); + capability2.addProperty("effective", "active"); + + org.apache.felix.bundlerepository.Capability[] capabilities2 = { capability2 }; + + Resource resource2 = EasyMock.createMock(Resource.class); + EasyMock.expect(resource2.getSymbolicName()).andReturn("com.test.bundleB").anyTimes(); + EasyMock.expect(resource2.getRequirements()).andReturn(null).anyTimes(); + EasyMock.expect(resource2.getCapabilities()).andReturn(capabilities2).anyTimes(); + EasyMock.expect(resource2.getURI()).andReturn("http://test2.com").anyTimes(); + EasyMock.expect(resource2.isLocal()).andReturn(true).anyTimes(); + + EasyMock.replay(resource, resource2); + + resolver.add(resource); + resolver.add(resource2); + + // Set the requirements to get the bundle + RequirementImpl requirement = new RequirementImpl("foo"); + requirement.setFilter("(bar=bread)"); + + Requirement[] requirements = { requirement }; + + Resource[] discoverResources = repoAdmin.discoverResources(requirements); + assertNotNull(discoverResources); + assertEquals(1, discoverResources.length); + + resolver.add(discoverResources[0]); + assertTrue("Resolver could not resolve", resolver.resolve()); + } + + public void testSpecBundleNamespace() throws Exception + { + URL url = getClass().getResource("/spec_repository.xml"); + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + RepositoryImpl repo = (RepositoryImpl) repoAdmin.addRepository(url); + + Resolver resolver = repoAdmin.resolver(); + + Resource[] discoverResources = repoAdmin.discoverResources("(symbolicname=org.apache.felix.bundlerepository.test_file_6*)"); + assertNotNull(discoverResources); + assertEquals(1, discoverResources.length); + + resolver.add(discoverResources[0]); + assertTrue(resolver.resolve()); + + } + + public void testMatchingReq() throws Exception + { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + repoAdmin.addRepository(getClass().getResource("/repo_for_resolvertest.xml")); + + Resource[] res = repoAdmin.discoverResources( + new Requirement[] { repoAdmin.getHelper().requirement( + "package", "(package=org.apache.felix.test.osgi)") }); + assertNotNull(res); + assertEquals(1, res.length); + } + + public void testResolveReq() throws Exception + { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + repoAdmin.addRepository(getClass().getResource("/repo_for_resolvertest.xml")); + + Resolver resolver = repoAdmin.resolver(); + resolver.add(repoAdmin.getHelper().requirement("package", "(package=org.apache.felix.test.osgi)")); + assertTrue(resolver.resolve()); + } + + public void testResolveInterrupt() throws Exception + { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + repoAdmin.addRepository(getClass().getResource("/repo_for_resolvertest.xml")); + + Resolver resolver = repoAdmin.resolver(); + resolver.add(repoAdmin.getHelper().requirement("package", "(package=org.apache.felix.test.osgi)")); + + Thread.currentThread().interrupt(); + try + { + resolver.resolve(); + fail("An excepiton should have been thrown"); + } + catch (org.apache.felix.bundlerepository.InterruptedResolutionException e) + { + // ok + } + } + + public void testOptionalResolution() throws Exception + { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + repoAdmin.addRepository(getClass().getResource("/repo_for_optional_resources.xml")); + + Resolver resolver = repoAdmin.resolver(); + resolver.add(repoAdmin.getHelper().requirement("bundle", "(symbolicname=res1)")); + + assertTrue(resolver.resolve()); + assertEquals(1, resolver.getRequiredResources().length); + assertEquals(2, resolver.getOptionalResources().length); + } + + public void testMandatoryPackages() throws Exception + { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(); + repoAdmin.addRepository(getClass().getResource("/repo_for_mandatory.xml")); + + Resolver resolver = repoAdmin.resolver(); + resolver.add(repoAdmin.getHelper().requirement("bundle", "(symbolicname=res2)")); + assertFalse(resolver.resolve()); + + resolver = repoAdmin.resolver(); + resolver.add(repoAdmin.getHelper().requirement("bundle", "(symbolicname=res3)")); + assertTrue(resolver.resolve()); + + resolver = repoAdmin.resolver(); + resolver.add(repoAdmin.getHelper().requirement("bundle", "(symbolicname=res4)")); + assertFalse(resolver.resolve()); + + } + + public void testFindUpdatableLocalResource() throws Exception { + LocalResource resource = EasyMock.createMock(LocalResource.class); + EasyMock.expect(resource.getSymbolicName()).andReturn("com.test.bundleA").anyTimes(); + EasyMock.expect(resource.getRequirements()).andReturn(null).anyTimes(); + EasyMock.expect(resource.getCapabilities()).andReturn(null).anyTimes(); + EasyMock.expect(resource.getURI()).andReturn("http://test.com").anyTimes(); + EasyMock.expect(resource.isLocal()).andReturn(true).anyTimes(); + + Repository localRepo = EasyMock.createMock(Repository.class); + + Repository[] localRepos = { localRepo }; + final LocalResource[] localResources = { resource }; + + EasyMock.expect(localRepo.getResources()).andReturn(localResources).anyTimes(); + EasyMock.expect(localRepo.getURI()).andReturn(Repository.LOCAL).anyTimes(); + EasyMock.expect(localRepo.getLastModified()).andReturn(System.currentTimeMillis()).anyTimes(); + + BundleContext bundleContext = EasyMock.createMock(BundleContext.class); + + EasyMock.replay(resource, localRepo); + + ResolverImpl resolver = new ResolverImpl(bundleContext, localRepos, new Logger(bundleContext)) { + @Override + public LocalResource[] getLocalResources() { + return localResources; + } + }; + + resolver.add(resource); + + boolean exceptionThrown = false; + try { + resolver.resolve(); + resolver.deploy(Resolver.START); + } catch (Exception e) { + e.printStackTrace(); + exceptionThrown = true; + } + assertFalse(exceptionThrown); + } + + public static void main(String[] args) throws Exception + { + new ResolverImplTest().testReferral1(); + } + + private RepositoryAdminImpl createRepositoryAdmin() throws Exception + { + BundleContext bundleContext = EasyMock.createMock(BundleContext.class); + Bundle systemBundle = EasyMock.createMock(Bundle.class); + BundleRevision systemBundleRevision = EasyMock.createMock(BundleRevision.class); + + Activator.setContext(bundleContext); + EasyMock.expect(bundleContext.getProperty(RepositoryAdminImpl.REPOSITORY_URL_PROP)) + .andReturn(getClass().getResource("/referred.xml").toExternalForm()); + EasyMock.expect(bundleContext.getProperty((String) EasyMock.anyObject())).andReturn(null).anyTimes(); + EasyMock.expect(bundleContext.getBundle(0)).andReturn(systemBundle); + EasyMock.expect(bundleContext.installBundle((String) EasyMock.anyObject(), (InputStream) EasyMock.anyObject())).andReturn(systemBundle); + EasyMock.expect(systemBundle.getHeaders()).andReturn(new Hashtable()).anyTimes(); + systemBundle.start(); + EasyMock.expectLastCall().anyTimes(); + EasyMock.expect(systemBundle.getRegisteredServices()).andReturn(null); + EasyMock.expect(new Long(systemBundle.getBundleId())).andReturn(new Long(0)).anyTimes(); + EasyMock.expect(systemBundle.getBundleContext()).andReturn(bundleContext); + EasyMock.expect(systemBundleRevision.getCapabilities(null)).andReturn(Collections.emptyList()); + EasyMock.expect(systemBundle.adapt(BundleRevision.class)).andReturn(systemBundleRevision); + bundleContext.addBundleListener((BundleListener) EasyMock.anyObject()); + bundleContext.addServiceListener((ServiceListener) EasyMock.anyObject()); + EasyMock.expect(bundleContext.getBundles()).andReturn(new Bundle[] { systemBundle }); + final Capture c = new Capture(); + EasyMock.expect(bundleContext.createFilter((String) capture(c))).andAnswer(new IAnswer() { + public Object answer() throws Throwable { + return FilterImpl.newInstance((String) c.getValue()); + } + }).anyTimes(); + EasyMock.replay(new Object[] { bundleContext, systemBundle, systemBundleRevision }); + + RepositoryAdminImpl repoAdmin = new RepositoryAdminImpl(bundleContext, new Logger(bundleContext)); + + // force initialization && remove all initial repositories + Repository[] repos = repoAdmin.listRepositories(); + for (int i = 0; repos != null && i < repos.length; i++) + { + repoAdmin.removeRepository(repos[i].getURI()); + } + + return repoAdmin; + } + + static Object capture(Capture capture) { + EasyMock.reportMatcher(new Captures(capture)); + return null; + } + +} \ No newline at end of file diff --git a/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/ResourceImplTest.java b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/ResourceImplTest.java new file mode 100644 index 00000000000..8f9dd3a1166 --- /dev/null +++ b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/ResourceImplTest.java @@ -0,0 +1,85 @@ +/* + * 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.bundlerepository.impl; + +import java.net.URL; + +import junit.framework.TestCase; + +import org.apache.felix.bundlerepository.Property; +import org.apache.felix.bundlerepository.Repository; + +public class ResourceImplTest extends TestCase +{ + public void testGetSizeFileResource() { + ResourceImpl res = new ResourceImpl(); + res.put(Property.URI, "repo_files/test_file_3.jar"); + + final URL dir = getClass().getResource("/repo_files"); + Repository repo = new RepositoryImpl() { + { setURI(dir.toExternalForm()); } + }; + res.setRepository(repo); + + assertEquals("Should have obtained the file size", 3, (long) res.getSize()); + } + + public void testGetSizeNonExistentFileResource() { + ResourceImpl res = new ResourceImpl(); + res.put(Property.URI, "repo_files/test_file_3_garbage.jar"); + + final URL dir = getClass().getResource("/repo_files"); + Repository repo = new RepositoryImpl() { + { setURI(dir.toExternalForm()); } + }; + res.setRepository(repo); + + assertEquals("File size should be reported as 0", 0, (long) res.getSize()); + } + + public void testGetSizeNonFileResource() { + final URL testFile4 = getClass().getResource("/repo_files/test_file_4.jar"); + + ResourceImpl res = new ResourceImpl(); + res.put(Property.URI, "jar:" + testFile4.toExternalForm() + "!/blah.txt"); + + final URL dir = getClass().getResource("/repo_files"); + Repository repo = new RepositoryImpl() { + { setURI(dir.toExternalForm()); } + }; + res.setRepository(repo); + + assertEquals("Should have obtained the file size", 5, (long) res.getSize()); + } + + public void testGetSizeNonExistentResource() { + final URL testFile4 = getClass().getResource("/repo_files/test_file_4.jar"); + + ResourceImpl res = new ResourceImpl(); + res.put(Property.URI, "jar:" + testFile4.toExternalForm() + "!/blah_xyz.txt"); + + final URL dir = getClass().getResource("/repo_files"); + Repository repo = new RepositoryImpl() { + { setURI(dir.toExternalForm()); } + }; + res.setRepository(repo); + + assertEquals("File size should be reported as 0", 0, (long) res.getSize()); + } +} diff --git a/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/StaxParserTest.java b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/StaxParserTest.java new file mode 100644 index 00000000000..54a3a918b89 --- /dev/null +++ b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/StaxParserTest.java @@ -0,0 +1,188 @@ +/* + * 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.bundlerepository.impl; + +import java.net.URL; +import java.util.Collections; +import java.util.Hashtable; + +import junit.framework.TestCase; + +import org.apache.felix.bundlerepository.Repository; +import org.apache.felix.bundlerepository.Resolver; +import org.apache.felix.bundlerepository.Resource; +import org.apache.felix.utils.filter.FilterImpl; +import org.apache.felix.utils.log.Logger; +import org.easymock.Capture; +import org.easymock.EasyMock; +import org.easymock.IAnswer; +import org.easymock.internal.matchers.Captures; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleListener; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.resource.Capability; + +public class StaxParserTest extends TestCase +{ + public void testStaxParser() throws Exception + { + URL url = getClass().getResource("/repo_for_resolvertest.xml"); + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(StaxParser.class); + RepositoryImpl repo = (RepositoryImpl) repoAdmin.addRepository(url); + + Resolver resolver = repoAdmin.resolver(); + + Resource[] discoverResources = repoAdmin.discoverResources("(symbolicname=org.apache.felix.test*)"); + assertNotNull(discoverResources); + assertEquals(1, discoverResources.length); + + resolver.add(discoverResources[0]); + assertTrue(resolver.resolve()); + } + + public void testPullParser() throws Exception + { + URL url = getClass().getResource("/repo_for_resolvertest.xml"); + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(PullParser.class); + RepositoryImpl repo = (RepositoryImpl) repoAdmin.addRepository(url); + + Resolver resolver = repoAdmin.resolver(); + + Resource[] discoverResources = repoAdmin.discoverResources("(symbolicname=org.apache.felix.test*)"); + assertNotNull(discoverResources); + assertEquals(1, discoverResources.length); + + resolver.add(discoverResources[0]); + assertTrue(resolver.resolve()); + } + + public void testPerfs() throws Exception + { + for (int i = 0; i < 10; i++) { +// testPerfs(new File(System.getProperty("user.home"), ".m2/repository/repository.xml").toURI().toURL(), 0, 100); + } + } + + protected void testPerfs(URL url, int nbWarm, int nbTest) throws Exception + { + long t0, t1; + + StaxParser.setFactory(null); + System.setProperty("javax.xml.stream.XMLInputFactory", "com.ctc.wstx.stax.WstxInputFactory"); + for (int i = 0; i < nbWarm; i++) + { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(StaxParser.class); + RepositoryImpl repo = (RepositoryImpl) repoAdmin.addRepository(url); + } + t0 = System.currentTimeMillis(); + for (int i = 0; i < nbTest; i++) + { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(StaxParser.class); + RepositoryImpl repo = (RepositoryImpl) repoAdmin.addRepository(url); + } + t1 = System.currentTimeMillis(); + System.err.println("Woodstox: " + (t1 - t0) + " ms"); + + + StaxParser.setFactory(null); + System.setProperty("javax.xml.stream.XMLInputFactory", "com.sun.xml.internal.stream.XMLInputFactoryImpl"); + for (int i = 0; i < nbWarm; i++) + { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(StaxParser.class); + RepositoryImpl repo = (RepositoryImpl) repoAdmin.addRepository(url); + } + t0 = System.currentTimeMillis(); + for (int i = 0; i < nbTest; i++) + { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(StaxParser.class); + RepositoryImpl repo = (RepositoryImpl) repoAdmin.addRepository(url); + } + t1 = System.currentTimeMillis(); + System.err.println("DefStax: " + (t1 - t0) + " ms"); + + for (int i = 0; i < nbWarm; i++) + { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(PullParser.class); + RepositoryImpl repo = (RepositoryImpl) repoAdmin.addRepository(url); + } + t0 = System.currentTimeMillis(); + for (int i = 0; i < nbTest; i++) + { + RepositoryAdminImpl repoAdmin = createRepositoryAdmin(PullParser.class); + RepositoryImpl repo = (RepositoryImpl) repoAdmin.addRepository(url); + } + t1 = System.currentTimeMillis(); + System.err.println("PullParser: " + (t1 - t0) + " ms"); + } + + public static void main(String[] args) throws Exception + { + new StaxParserTest().testStaxParser(); + } + + private RepositoryAdminImpl createRepositoryAdmin(Class repositoryParser) throws Exception + { + BundleContext bundleContext = EasyMock.createMock(BundleContext.class); + Bundle systemBundle = EasyMock.createMock(Bundle.class); + BundleRevision systemBundleRevision = EasyMock.createMock(BundleRevision.class); + + Activator.setContext(bundleContext); + EasyMock.expect(bundleContext.getProperty(RepositoryAdminImpl.REPOSITORY_URL_PROP)) + .andReturn(getClass().getResource("/referral1_repository.xml").toExternalForm()); + EasyMock.expect(bundleContext.getProperty(RepositoryParser.OBR_PARSER_CLASS)) + .andReturn(repositoryParser.getName()); + EasyMock.expect(bundleContext.getProperty((String) EasyMock.anyObject())).andReturn(null).anyTimes(); + EasyMock.expect(bundleContext.getBundle(0)).andReturn(systemBundle); + EasyMock.expect(systemBundle.getHeaders()).andReturn(new Hashtable()); + EasyMock.expect(systemBundle.getRegisteredServices()).andReturn(null); + EasyMock.expect(new Long(systemBundle.getBundleId())).andReturn(new Long(0)).anyTimes(); + EasyMock.expect(systemBundle.getBundleContext()).andReturn(bundleContext); + EasyMock.expect(systemBundleRevision.getCapabilities(null)).andReturn(Collections.emptyList()); + EasyMock.expect(systemBundle.adapt(BundleRevision.class)).andReturn(systemBundleRevision); + bundleContext.addBundleListener((BundleListener) EasyMock.anyObject()); + bundleContext.addServiceListener((ServiceListener) EasyMock.anyObject()); + EasyMock.expect(bundleContext.getBundles()).andReturn(new Bundle[] { systemBundle }); + final Capture c = new Capture(); + EasyMock.expect(bundleContext.createFilter((String) capture(c))).andAnswer(new IAnswer() { + public Object answer() throws Throwable { + return FilterImpl.newInstance((String) c.getValue()); + } + }).anyTimes(); + EasyMock.replay(new Object[] { bundleContext, systemBundle, systemBundleRevision }); + + RepositoryAdminImpl repoAdmin = new RepositoryAdminImpl(bundleContext, new Logger(bundleContext)); + + // force initialization && remove all initial repositories + Repository[] repos = repoAdmin.listRepositories(); + for (int i = 0; repos != null && i < repos.length; i++) + { + repoAdmin.removeRepository(repos[i].getURI()); + } + + return repoAdmin; + } + + static Object capture(Capture capture) { + EasyMock.reportMatcher(new Captures(capture)); + return null; + } + +} \ No newline at end of file diff --git a/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/Streams.java b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/Streams.java new file mode 100644 index 00000000000..77881e590f4 --- /dev/null +++ b/bundlerepository/src/test/java/org/apache/felix/bundlerepository/impl/Streams.java @@ -0,0 +1,57 @@ +/* + * 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.bundlerepository.impl; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class Streams { + private Streams() {} + + public static void pump(InputStream is, OutputStream os) throws IOException { + byte[] bytes = new byte[8192]; + + int length = 0; + int offset = 0; + + while ((length = is.read(bytes, offset, bytes.length - offset)) != -1) { + offset += length; + + if (offset == bytes.length) { + os.write(bytes, 0, bytes.length); + offset = 0; + } + } + if (offset != 0) { + os.write(bytes, 0, offset); + } + } + + public static byte [] suck(InputStream is) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + pump(is, baos); + return baos.toByteArray(); + } finally { + is.close(); + } + } +} \ No newline at end of file diff --git a/bundlerepository/src/test/resources/another_repository.xml b/bundlerepository/src/test/resources/another_repository.xml new file mode 100644 index 00000000000..749d9b52e3b --- /dev/null +++ b/bundlerepository/src/test/resources/another_repository.xml @@ -0,0 +1,81 @@ + + + + + + 1 + +

      +

      +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      + + +

      + + + + + 2 + + +

      + + + diff --git a/bundlerepository/src/test/resources/referral1_repository.xml b/bundlerepository/src/test/resources/referral1_repository.xml new file mode 100644 index 00000000000..741f469f5b1 --- /dev/null +++ b/bundlerepository/src/test/resources/referral1_repository.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/bundlerepository/src/test/resources/referred.xml b/bundlerepository/src/test/resources/referred.xml new file mode 100644 index 00000000000..157be81601e --- /dev/null +++ b/bundlerepository/src/test/resources/referred.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/bundlerepository/src/test/resources/repo_files/test_file_1.jar b/bundlerepository/src/test/resources/repo_files/test_file_1.jar new file mode 100644 index 00000000000..500c0709ca2 --- /dev/null +++ b/bundlerepository/src/test/resources/repo_files/test_file_1.jar @@ -0,0 +1 @@ +X \ No newline at end of file diff --git a/bundlerepository/src/test/resources/repo_files/test_file_2.jar b/bundlerepository/src/test/resources/repo_files/test_file_2.jar new file mode 100644 index 00000000000..dfc91791b27 --- /dev/null +++ b/bundlerepository/src/test/resources/repo_files/test_file_2.jar @@ -0,0 +1 @@ +AB \ No newline at end of file diff --git a/bundlerepository/src/test/resources/repo_files/test_file_3.jar b/bundlerepository/src/test/resources/repo_files/test_file_3.jar new file mode 100644 index 00000000000..48b83b862eb --- /dev/null +++ b/bundlerepository/src/test/resources/repo_files/test_file_3.jar @@ -0,0 +1 @@ +ABC \ No newline at end of file diff --git a/bundlerepository/src/test/resources/repo_files/test_file_4.jar b/bundlerepository/src/test/resources/repo_files/test_file_4.jar new file mode 100644 index 00000000000..5816bf40621 Binary files /dev/null and b/bundlerepository/src/test/resources/repo_files/test_file_4.jar differ diff --git a/bundlerepository/src/test/resources/repo_for_mandatory.xml b/bundlerepository/src/test/resources/repo_for_mandatory.xml new file mode 100644 index 00000000000..5bb3787030c --- /dev/null +++ b/bundlerepository/src/test/resources/repo_for_mandatory.xml @@ -0,0 +1,67 @@ + + + + + + +

      + + +

      +

      +

      + + + + + +

      + + + + + + +

      + + + + + + +

      + + + + + + +

      + + +

      +

      +

      +

      +

      + + + + diff --git a/bundlerepository/src/test/resources/repo_for_optional_resources.xml b/bundlerepository/src/test/resources/repo_for_optional_resources.xml new file mode 100644 index 00000000000..2b5faca2a69 --- /dev/null +++ b/bundlerepository/src/test/resources/repo_for_optional_resources.xml @@ -0,0 +1,42 @@ + + + + + + +

      + + + + + + +

      + + + + + + +

      + + + + diff --git a/bundlerepository/src/test/resources/repo_for_resolvertest.xml b/bundlerepository/src/test/resources/repo_for_resolvertest.xml new file mode 100644 index 00000000000..10686adaaca --- /dev/null +++ b/bundlerepository/src/test/resources/repo_for_resolvertest.xml @@ -0,0 +1,1051 @@ + + + + + + + 42 + + +

      +

      +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      + + + + + + 2975 + + +

      +

      +

      +

      + + +

      +

      +

      + + Import package + org.apache.felix.test.osgi + Import package + org.apache.commons.dbcp ;version=1.2.0 + Import package + org.springframework.context ;version=2.5.0 + + + + 122704 + + +

      +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + Import package javax.naming + Import package + javax.naming.spi + Import package javax.sql + Import package + org.apache.commons.pool ;version=[1.3.0,2.0.0) + Import package + org.apache.commons.pool.impl ;version=[1.3.0,2.0.0) + Import package org.xml.sax + Import package + org.xml.sax.helpers + + + + 62200 + + +

      +

      +

      +

      + + +

      +

      + + +

      +

      +

      + + + + + 488282 + + +

      +

      +

      +

      + + +

      +

      +

      + + +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + Import package + javax.el ;version=[2.1.0,3.0.0) + Import package + javax.xml.parsers + Import package + net.sf.cglib.proxy ;version=[2.1.3,2.2.0) + Import package + org.apache.commons.logging ;version=[1.0.4,2.0.0) + Import package + org.springframework.core ;version=[2.5.6,2.5.6] + Import package + org.springframework.core.annotation ;version=[2.5.6,2.5.6] + Import package + org.springframework.core.io ;version=[2.5.6,2.5.6] + Import package + org.springframework.core.io.support ;version=[2.5.6,2.5.6] + Import package + org.springframework.core.type ;version=[2.5.6,2.5.6] + Import package + org.springframework.util ;version=[2.5.6,2.5.6] + Import package + org.springframework.util.xml ;version=[2.5.6,2.5.6] + Import package org.w3c.dom + Import package org.xml.sax + + + + 285491 + + +

      +

      +

      +

      + + +

      +

      + + +

      +

      +

      + + +

      +

      + + +

      +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      +

      + + +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      +

      + + +

      +

      + + +

      +

      +

      + + Import package + edu.emory.mathcs.backport.java.util.concurrent ;version=[3.0.0,4.0.0) + + Import package + javax.xml.transform + Import package + org.apache.commons.attributes ;version=[2.2.0,3.0.0) + Import package + org.apache.commons.collections ;version=[3.2.0,4.0.0) + Import package + org.apache.commons.collections.map ;version=[3.2.0,4.0.0) + Import package + org.apache.commons.logging ;version=[1.0.4,2.0.0) + Import package + org.apache.log4j ;version=[1.2.15,2.0.0) + Import package + org.apache.log4j.xml ;version=[1.2.15,2.0.0) + Import package + org.aspectj.bridge ;version=[1.5.4,2.0.0) + Import package + org.aspectj.weaver ;version=[1.5.4,2.0.0) + Import package + org.aspectj.weaver.bcel ;version=[1.5.4,2.0.0) + Import package + org.aspectj.weaver.patterns ;version=[1.5.4,2.0.0) + Import package + org.eclipse.core.runtime + Import package org.w3c.dom + Import package org.xml.sax + + + + 476940 + + +

      +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      + + +

      +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      + + +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      +

      + + +

      +

      + + Import package bsh + ;version=[2.0.0.b4,3.0.0) + Import package + com.ibm.websphere.management + Import package + com.sun.enterprise.loader ;version=[1.0.0,2.0.0) + Import package + com.sun.net.httpserver + Import package + edu.emory.mathcs.backport.java.util.concurrent ;version=[3.0.0,4.0.0) + + Import package + groovy.lang ;version=[1.5.1,2.0.0) + Import package + javax.annotation ;version=[1.0.0,2.0.0) + Import package + javax.ejb ;version=[3.0.0,4.0.0) + Import package + javax.interceptor ;version=[3.0.0,4.0.0) + Import package + javax.jms ;version=[1.1.0,2.0.0) + Import package + javax.management + Import package + javax.management.modelmbean + Import package + javax.management.openmbean + Import package + javax.management.remote + Import package javax.naming + Import package + javax.persistence ;version=[1.0.0,2.0.0) + Import package + javax.persistence.spi ;version=[1.0.0,2.0.0) + Import package javax.rmi + Import package + javax.rmi.CORBA + Import package + javax.xml.namespace + Import package + javax.xml.ws ;version=[2.1.1,3.0.0) + Import package + net.sf.cglib.asm ;version=[2.1.3,2.2.0) + Import package + net.sf.cglib.core ;version=[2.1.3,2.2.0) + Import package + net.sf.cglib.proxy ;version=[2.1.3,2.2.0) + Import package + oracle.classloader.util ;version=[10.1.3.1,10.2.0.0) + Import package + org.aopalliance.aop ;version=[1.0.0,2.0.0) + Import package + org.aopalliance.intercept ;version=[1.0.0,2.0.0) + Import package + org.apache.commons.logging ;version=[1.0.4,2.0.0) + Import package + org.aspectj.weaver.loadtime ;version=[1.5.4,2.0.0) + Import package + org.codehaus.groovy.control ;version=[1.5.1,2.0.0) + Import package + org.jruby ;version=[1.1.0,2.0.0) + Import package + org.jruby.ast ;version=[1.1.0,2.0.0) + Import package + org.jruby.exceptions ;version=[1.1.0,2.0.0) + Import package + org.jruby.javasupport ;version=[1.1.0,2.0.0) + Import package + org.jruby.runtime ;version=[1.1.0,2.0.0) + Import package + org.jruby.runtime.builtin ;version=[1.1.0,2.0.0) + Import package org.omg.CORBA + Import package + org.omg.CORBA.portable + Import package + org.omg.CORBA_2_3.portable + Import package + org.springframework.aop ;version=[2.5.6,2.5.6] + Import package + org.springframework.aop.framework ;version=[2.5.6,2.5.6] + Import package + org.springframework.aop.framework.adapter ;version=[2.5.6,2.5.6] + + Import package + org.springframework.aop.scope ;version=[2.5.6,2.5.6] + Import package + org.springframework.aop.support ;version=[2.5.6,2.5.6] + Import package + org.springframework.aop.target ;version=[2.5.6,2.5.6] + Import package + org.springframework.aop.target.dynamic ;version=[2.5.6,2.5.6] + + Import package + org.springframework.beans ;version=[2.5.6,2.5.6] + Import package + org.springframework.beans.annotation ;version=[2.5.6,2.5.6] + Import package + org.springframework.beans.factory ;version=[2.5.6,2.5.6] + Import package + org.springframework.beans.factory.access ;version=[2.5.6,2.5.6] + + Import package + org.springframework.beans.factory.annotation ;version=[2.5.6,2.5.6] + + Import package + org.springframework.beans.factory.config ;version=[2.5.6,2.5.6] + + Import package + org.springframework.beans.factory.parsing ;version=[2.5.6,2.5.6] + + Import package + org.springframework.beans.factory.support ;version=[2.5.6,2.5.6] + + Import package + org.springframework.beans.factory.xml ;version=[2.5.6,2.5.6] + + Import package + org.springframework.beans.propertyeditors ;version=[2.5.6,2.5.6] + + Import package + org.springframework.beans.support ;version=[2.5.6,2.5.6] + Import package + org.springframework.core ;version=[2.5.6,2.5.6] + Import package + org.springframework.core.annotation ;version=[2.5.6,2.5.6] + Import package + org.springframework.core.io ;version=[2.5.6,2.5.6] + Import package + org.springframework.core.io.support ;version=[2.5.6,2.5.6] + Import package + org.springframework.core.task ;version=[2.5.6,2.5.6] + Import package + org.springframework.core.task.support ;version=[2.5.6,2.5.6] + + Import package + org.springframework.core.type ;version=[2.5.6,2.5.6] + Import package + org.springframework.core.type.classreading ;version=[2.5.6,2.5.6] + + Import package + org.springframework.core.type.filter ;version=[2.5.6,2.5.6] + Import package + org.springframework.instrument ;version=[2.5.6,2.5.6] + Import package + org.springframework.metadata ;version=[2.5.6,2.5.6] + Import package + org.springframework.orm.jpa.support ;version=[2.5.6,2.5.6] + Import package + org.springframework.util ;version=[2.5.6,2.5.6] + Import package + org.springframework.util.xml ;version=[2.5.6,2.5.6] + Import package org.w3c.dom + Import package org.xml.sax + + diff --git a/bundlerepository/src/test/resources/spec_repository.gz b/bundlerepository/src/test/resources/spec_repository.gz new file mode 100644 index 00000000000..0a6cc487168 Binary files /dev/null and b/bundlerepository/src/test/resources/spec_repository.gz differ diff --git a/bundlerepository/src/test/resources/spec_repository.xml b/bundlerepository/src/test/resources/spec_repository.xml new file mode 100644 index 00000000000..cf049dd5ecb --- /dev/null +++ b/bundlerepository/src/test/resources/spec_repository.xml @@ -0,0 +1,998 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundlerepository/src/test/resources/spec_repository.zip b/bundlerepository/src/test/resources/spec_repository.zip new file mode 100644 index 00000000000..862f489403a Binary files /dev/null and b/bundlerepository/src/test/resources/spec_repository.zip differ diff --git a/check_staged_release.sh b/check_staged_release.sh new file mode 100755 index 00000000000..3427ccd33e0 --- /dev/null +++ b/check_staged_release.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +STAGING=${1} +DOWNLOAD=${2:-/tmp/felix-staging} +mkdir ${DOWNLOAD} 2>/dev/null + +# The following code automatically imports the signing KEYS, but it may actually be +# better to download them from a key server and/or let the user choose what keys +# he wants to import. +#wget --no-check-certificate -P "${DOWNLOAD}" http://www.apache.org/dist/felix/KEYS +#gpg --import "${DOWNLOAD}/KEYS" + +if [ -z "${STAGING}" -o ! -d "${DOWNLOAD}" ] +then + echo "Usage: check_staged_release.sh [temp-directory]" + exit +fi + +if [ ! -e "${DOWNLOAD}/${STAGING}" ] +then + echo "################################################################################" + echo " DOWNLOAD STAGED REPOSITORY " + echo "################################################################################" + + wget \ + -e "robots=off" --wait 1 -nv -r -np "--reject=html,txt" "--follow-tags=" \ + -P "${DOWNLOAD}/${STAGING}" -nH "--cut-dirs=3" \ + "https://repository.apache.org/content/repositories/orgapachefelix-${STAGING}/org/apache/felix/" + +else + echo "################################################################################" + echo " USING EXISTING STAGED REPOSITORY " + echo "################################################################################" + echo "${DOWNLOAD}/${STAGING}" +fi + +echo "################################################################################" +echo " CHECK SIGNATURES AND DIGESTS " +echo "################################################################################" + +for i in `find "${DOWNLOAD}/${STAGING}" -type f | grep -v '\.\(asc\|sha1\|md5\)$'` +do + f=`echo $i | sed 's/\.asc$//'` + echo "$f" + gpg --verify $f.asc 2>/dev/null + if [ "$?" = "0" ]; then CHKSUM="GOOD"; else CHKSUM="BAD!!!!!!!!"; fi + if [ ! -f "$f.asc" ]; then CHKSUM="----"; fi + echo "gpg: ${CHKSUM}" + if [ "`cat $f.md5 2>/dev/null`" = "`openssl md5 < $f 2>/dev/null | sed 's/.*= *//'`" ]; then CHKSUM="GOOD"; else CHKSUM="BAD!!!!!!!!"; fi + if [ ! -f "$f.md5" ]; then CHKSUM="----"; fi + echo "md5: ${CHKSUM}" + if [ "`cat $f.sha1 2>/dev/null`" = "`openssl sha1 < $f 2>/dev/null | sed 's/.*= *//'`" ]; then CHKSUM="GOOD"; else CHKSUM="BAD!!!!!!!!"; fi + if [ ! -f "$f.sha1" ]; then CHKSUM="----"; fi + echo "sha1: ${CHKSUM}" +done + +if [ -z "${CHKSUM}" ]; then echo "WARNING: no files found!"; fi + +echo "################################################################################" + diff --git a/configadmin/changelog.txt b/configadmin/changelog.txt new file mode 100644 index 00000000000..55d0584e940 --- /dev/null +++ b/configadmin/changelog.txt @@ -0,0 +1,314 @@ +Changes in 1.9.12 +----------------- +** Bug + * [FELIX-6062] : Coordination is not checked if its terminated + * [FELIX-6061] : Avoid using getDeclaredMethods which needs a permission check + + +Changes in 1.9.10 +----------------- +** Bug + * [FELIX-5962] : Exception during event processing leaves CM threads alive and holding the JVM running during framework shutdown + +** New Feature + * [FELIX-5951] : Automatically add ConfigurationAdmin service as a shell command + + +Changes in 1.9.8 +---------------- +** Bug + * [FELIX-5949] : Configuration _updates_ are ignored when using a NotCachablePersistenceManager + + +Changes in 1.9.6 +---------------- +** Bug + * [FELIX-5908] : NoClassDefFoundError for the CM Security Domain combiner + * [FELIX-5918] : AccessControlExceptions in ConfigurationAdmin methods + + +Changes in 1.9.4 +---------------- +** Bug + * [FELIX-5892] : Repeated calls to getFactoryConfiguration return different configuration instances + + +Changes in 1.9.2 +---------------- +** Bug + * [FELIX-5845] : org.apache.felix.cm.impl.persistence.PersistenceManagerProxy broken + + +Changes in 1.9.0 +---------------- +** Improvement + * [FELIX-5288] : Implement RFC 227 (OSGi R7 Update) + * [FELIX-5351] : Implement coordinations (OSGi R7) + * [FELIX-5289] : Alias PID Handling of Factory Configurations (OSGi R7) + * [FELIX-5290] : Locking Configuration Records (OSGi R7) + * [FELIX-5291] : Improve Configuration Updates (OSGi R7) + * [FELIX-5292] : Capabilities (OSGi R7) + * [FELIX-5293] : Improved ConfigurationPlugin Support (OSGi R7) + * [FELIX-5468] : Refactor persistence handling + * [FELIX-5693] : Improve persistence manager handling + * [FELIX-5695] : Use Java 7 as base version + * [FELIX-5778] : Refactor factory configuration handling +** Bug + * [FELIX-5745] : Empty collections should be allowed as property value + + +Changes from 1.8.14 to 1.8.16 +----------------------------- +** Bug + * [FELIX-5669] : Registering a PersistenceManager causes duplicate caches. + + +Changes from 1.8.12 to 1.8.14 +----------------------------- +** Bug + * [FELIX-5443] : Frequent Changes cause UpdateThread to ConcurrentModificationException + * [FELIX-5435] : Service does not get loaded with updated properties that have been modified on file system after felix framework restart + + +Changes from 1.8.10 to 1.8.12 +----------------------------- + +** Improvement + * [FELIX-5380] : Reduce quite useless log verbosity + * [FELIX-5366] : Additional Unit Tests for ConfigurationHandler + +** Bug + * [FELIX-5368] : ConfigurationManager ignores NotCachablePersistenceManager marker interface + * [FELIX-5385] : ConfigAdmin uses wrong security when calling ManagedServices + + +Changes from 1.8.8 to 1.8.10 +---------------------------- + +** Improvement + * [FELIX-5088] : CaseSensitiveDictionary should implement equals() + * [FELIX-5211] : Use provide/require capabilities instead of obsolete and meaningless import-export service headers + +** Bug + * [FELIX-5301] : ConfigurationPlugin support is not spec compliant + + +Changes from 1.8.6 to 1.8.8 +--------------------------- + +** Improvement + * [FELIX-4917] : FilePersistenceManager doesn't support comments + +** Bug + * [FELIX-4962] : Configadmin leaks caller's security context downstream + * [FELIX-4945] : Escaped folder names makes ConfigAdmin incompatible and factory configs not always work + + +Changes from 1.8.4 to 1.8.6 +--------------------------- + +** Improvement + * [FELIX-4844] : Store configuration data in a diff-tool friendly way + * [FELIX-4884] : Avoid unnecessary array creation if listConfigurations does not find anything + + +Changes from 1.8.2 to 1.8.4 +--------------------------- + +** Bug + * [FELIX-4846] : Wrong exception type in list operation + * [FELIX-4851] : ConfigAdmin only forwards ConfigurationEvents to ConfigurationListeners which are provided by bundles that are in state ACTIVE + * [FELIX-4855] : ConfigAdmin does not update ManagedServiceFactory on framework restart + + +Changes from 1.8.0 to 1.8.2 +---------------------------- + +** Bug + * [FELIX-4302] : Config Admin can create illegal names on windows + * [FELIX-4360] : Ensure ordered property values in configurations + * [FELIX-4362] : Security ConfigAdmin permissions are inherited on the stack + * [FELIX-4385] : NPE in Configuration Admin Service when deleting configuration + * [FELIX-4408] : CaseInsensitiveDictionary is platform/locale dependant + * [FELIX-4566] : Consistency in PersistenceManager and Cache is not guaranteed + +** Improvement + * [FELIX-4316] : Packages imported dynamically should also be imported statically with an optional flag + * [FELIX-4811] : Optimize ConfigurationManager#listConfigurations + + +Changes from 1.6.0 to 1.8.0 +---------------------------- + +** Bug + * [FELIX-3360] : Bundle location is statically set for dynamically bound bundles + * [FELIX-3762] : NPE in UpdateConfiguration#run + * [FELIX-3820] : Possible NPE in ConfigAdmin after shutting down when accessed by bad behaving bundles + * [FELIX-3823] : ConfigAdmin should explicitely unregister services + * [FELIX-4165] : FilePersistenceManager fails to rename configuration file + * [FELIX-4197] : [CM] Always check permission on Configuration.get/setBundleLocation + * [FELIX-4238] : Unnecessary re-initialization of PersistenceManagers in configadmin + +** Improvement + * [FELIX-4039] : Add Permissions file in ConfigAdmin bundle + +** Task + * [FELIX-3808] : Upgrade ConfigAdmin to pax-exam 2 + + +Changes from 1.4.0 to 1.6.0 +---------------------------- + +** Bug + * [FELIX-3532] : Source artifact is not being generated + * [FELIX-3596] : Config Admin should track SynchronousConfigurationListeners + * [FELIX-3721] : Configuration not always provided upon initial service registration + +** Improvement + * [FELIX-3577] : Refactor helpers and service trackers + * [FELIX-3622] : ConfigurationManager.listConfigurations may not always properly log the configuration PID + +** Task + * [FELIX-3479] : [Config Admin 1.5] Implement Configuration.getChangeCount() + * [FELIX-3480] : [Config Admin 1.5] Implement support for SynchronousConfigurationListener + * [FELIX-3481] : [Config Admin 1.5] Implement support for Targeted PIDs + * [FELIX-3483] : [Config Admin 1.5] Export cm API at version 1.5 + * [FELIX-3554] : Prevent same configuration to be delivered multiple times + * [FELIX-3562] : Use OSGi Configuration Admin 1.5 API classes + * [FELIX-3578] : ConfigAdmin Maven build does not have Maven convention *-sources.jar artifacts + + +Changes from 1.2.8 to 1.4.0 +---------------------------- + +** Bug + * [FELIX-2766] : Calling update() on a newly created factory configuration causes FileNotFoundException + * [FELIX-2771] : Configuration Admin does not work on Foundation 1.2 and Mika + * [FELIX-2813] : NPE in UpdateThread when updating a configuration right after ConfigurationAdmin service starts + * [FELIX-2847] : NPE in ConfigurationManager.java:1003 (static String toString( ServiceReference ref )) + * [FELIX-2885] : The config admin bundle does not indicate its provided and required services dependencies + * [FELIX-2888] : if you create a factory configuration and anybody takes a peek before you've had a chance to update, your pudding is trapped forever + * [FELIX-3046] : Empty array configuration value is read as null after restart + * [FELIX-3175] : RankingComparator results in wrong results + * [FELIX-3227] : ManagedService.update must be called with null if configuration exists but is not visilbe + * [FELIX-3228] : Configuration.getBundleLocation to generous + * [FELIX-3229] : ConfigurationAdmin.getConfiguration(String, String) and .createConfiguration(String) to generous + * [FELIX-3230] : ConfiguartionAdapter.setBundleLocation checks configuration permission incorrectly + * [FELIX-3231] : Disable update counter + * [FELIX-3233] : ConfigurationManager.canReceive may throw NullPointerException + * [FELIX-3390] : Intermittent NPE in ConfigurationManager + +** Improvement + * [FELIX-3180] : Provide MessageFormat based logging method + * [FELIX-3327] : Gracefully handle Configuration access after Configuration Admin Service has terminated + +** Task + * [FELIX-3176] : Implement Configuration Admin 1.4 changes + * [FELIX-3177] : Remove temporary inclusion of OSGi classes + * [FELIX-3200] : Track PID changes of ManagedService[Factory] services + * [FELIX-3301] : Enforce only using Java 1.3 API use + +** Wish + * [FELIX-1747] : Use Remote Resources Plugin to generate the legal files + + +Changes from 1.2.4 to 1.2.8 +--------------------------- + +** Bug + * [FELIX-1545] : Configurations may still be delivered more than once (or not at all) + * [FELIX-1727] : Properties with leading dots not allowed + * [FELIX-2179] : junit does not need to be a compile scope dependency of configadmin + * [FELIX-2557] : ConfigurationEvent delivery not building the listener list correctly + +** Improvement + * [FELIX-1907] : Improve documentation on PersistenceManager API + * [FELIX-2552] : Add caching PersistenceManager proxy + * [FELIX-2554] : Improve unit test setup to allow for easier use of integration tests from within IDEs + +** Task + * [FELIX-1543] : Remove org.osgi.service.cm from configadmin project as soon as R4.2 compendium library is available from the Maven Repository + * [FELIX-2559] : Adapt Configuration Admin LICENSE and NOTICE files + + +Changes from 1.2.0 to 1.2.4 +--------------------------- + +** Bug + * [FELIX-1535] : Permission is checked against the using bundle instead of the access control context (call stack) + * [FELIX-1542] : Configuration may be supplied twice in certain situations + +** Improvement + * [FELIX-1541] : Include latest CM 1.3 (Compendium R 4.2) package export + + +Changes from 1.0.10 to 1.2.0 +---------------------------- + +** Bug + * [FELIX-979] : Config Admin throwing NPE + * [FELIX-1146] : ConfigAdmin can deliver updates to a managed service factory more than once + * [FELIX-1165] : When restarting a bundle, the config admin reports "Configuration ... has already been delivered", and the bundle receives no configuration. + * [FELIX-1477] : ConfigAdmin implementation is not thread-safe + * [FELIX-1479] : Security errors accessing configurations in the file system + * [FELIX-1484] : Dynamically bound configuration must be reset to null after target bundle is uninstalled. + * [FELIX-1486] : Multiple PIDs must be supported + * [FELIX-1488] : Configuration binding is broken + * [FELIX-1489] : New Configurations must still be delivered to ManagedService + * [FELIX-1508] : Configuration.update() must not send CM_UPDATED event + +** Improvement + * [FELIX-1219] : ConfigAdmin package version has been bumped + * [FELIX-1507] : Prevent update failure in case of multiple updates/deletes + +** New Feature + * [FELIX-1231] : Support multi-value service.pid service properties + * [FELIX-1234] : Configuration Plugins should be called for all configuration instances of factory targets + + +Changes from 1.0.8 to 1.0.10 +---------------------------- + +** Bug + * [FELIX-889] : Arrays of primitives not supported by Configuration Admin + * [FELIX-890] : Configuration.getProperty returns a Dictionary which is not entirely private + +** Improvement + * [FELIX-903] : Add functionality to limit log output in the absence of a LogService + + +Changes from 1.0.4 to 1.0.8 +--------------------------- + +** Bug + * [FELIX-740] : ConfigurationManager throws NPE when bundle that registered service is uninstalled + * [FELIX-880] : ServiceReference of ConfigurationEvent is null + * [FELIX-881] : Stopping the Configuration Admin bundle causes a NullPointerException + +** Improvement + * [FELIX-665] : Configuration Admin OBR description + * [FELIX-865] : OBR: Do not declare ManagedService[Factory], ConfigurationListener and PersistenceManager as required services + * [FELIX-879] : Use Collection interface internally instead of Vector, also be lenient and accept any Colleciton value and do not require Vector + + +Changes from 1.0.1 to 1.0.4 +--------------------------- + +** Bug + * [FELIX-611] : ConfigurationAdmin.listConfigurations returns empty configurations + * [FELIX-612] : ConfigurationAdmin.createFactoryConfiguration should not persist Configuration + +** Improvement + * [FELIX-605] : Include ServiceTracker as private package + + +Changes from 1.0.0 to 1.0.1 +--------------------------- + +** Bug + * [FELIX-516] : ManagedService[Factory] may be updated twice with the same Configuration + * [FELIX-522] : Configuration Admin allows configuration keys with illegal characters + + +Initial Release 1.0.0 +--------------------- diff --git a/configadmin/pom.xml b/configadmin/pom.xml new file mode 100644 index 00000000000..2ed2e4b8c79 --- /dev/null +++ b/configadmin/pom.xml @@ -0,0 +1,312 @@ + + + + 4.0.0 + + org.apache.felix + felix-parent + 6 + + + + org.apache.felix.configadmin + 1.9.15-SNAPSHOT + bundle + + Apache Felix Configuration Admin Service + + Implementation of the OSGi Configuration Admin Service Specification 1.6 + + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/configadmin + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/configadmin + http://svn.apache.org/repos/asf/felix/configadmin + + + + + + ${basedir}/target + + + ${bundle.build.name}/${project.build.finalName}.jar + + + + + + org.osgi + osgi.annotation + 6.0.1 + provided + + + org.osgi + org.osgi.core + 6.0.0 + provided + + + org.osgi + org.osgi.service.cm + 1.6.0 + provided + + + org.osgi + org.osgi.service.log + 1.3.0 + provided + + + org.osgi + org.osgi.service.coordinator + 1.0.2 + provided + + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-core + 2.17.0 + test + + + org.ops4j.pax.exam + pax-exam-junit4 + 2.6.0 + test + + + org.ops4j.pax.exam + pax-exam-container-native + 2.6.0 + test + + + + org.ops4j.pax.exam + pax-exam-container-forked + 2.6.0 + test + + + org.ops4j.pax.exam + pax-exam-link-mvn + 2.6.0 + test + + + org.ops4j.pax.url + pax-url-aether + 1.5.0 + test + + + org.ops4j.pax.tinybundles + tinybundles + 1.0.0 + test + + + org.apache.geronimo.specs + geronimo-atinject_1.0_spec + 1.0 + test + + + org.slf4j + slf4j-simple + 1.7.1 + test + + + org.apache.felix + org.apache.felix.framework + 5.6.1 + test + + + org.apache.felix + org.apache.felix.framework.security + 2.6.1 + test + + + + + + + org.apache.felix + maven-bundle-plugin + 3.5.0 + true + + + osgi + + ${project.artifactId} + + The Apache Software Foundation + + http://felix.apache.org/site/apache-felix-config-admin.html + + + org.apache.felix.cm.impl.Activator + + + + + org.apache.felix.cm; + org.apache.felix.cm.file, + org.osgi.service.cm;provide:=true + + + org.osgi.service.cm, + org.osgi.service.coordinator;resolution:=optional, + org.osgi.service.log;resolution:=optional, + * + + + org.osgi.service.coordinator;version="[1.0,2)", + org.osgi.service.log;version="[1.3,2)" + + ="org.osgi.service.cm.ConfigurationAdmin";uses:="org.osgi.service.cm,org.apache.felix.cm", + osgi.service;objectClass:List="org.apache.felix.cm.PersistenceManager";uses:="org.osgi.service.cm,org.apache.felix.cm", + osgi.implementation;osgi.implementation="osgi.cm";uses:="org.osgi.service.cm,org.apache.felix.cm";version:Version="1.6" + ]]> + + + + + + baseline + + baseline + + + + + + + maven-surefire-plugin + + + surefire-it + integration-test + + test + + + + + project.bundle.file + ${bundle.file.name} + + + + **/cm/* + **/cm/file/* + **/cm/impl/** + + + **/integration/* + + + + + + + **/integration/** + + + + + + + + + + ide + + + + maven-antrun-plugin + 1.3 + + + cm-file-create + package + + run + + + + + + + + + + + + + + diff --git a/configadmin/src/main/appended-resources/META-INF/DEPENDENCIES b/configadmin/src/main/appended-resources/META-INF/DEPENDENCIES new file mode 100644 index 00000000000..114cc1ee685 --- /dev/null +++ b/configadmin/src/main/appended-resources/META-INF/DEPENDENCIES @@ -0,0 +1,25 @@ +I. Included Third-Party Software + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2012). +Licensed under the Apache License 2.0. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2012). +Licensed under the Apache License 2.0. + +This product uses software developed at +The Codehaus (http://www.codehaus.org) +Licensed under the Apache License 2.0. + +This product uses software developed at +Open Participation Software for Java (http://www.ops4j.org) +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 + diff --git a/configadmin/src/main/appended-resources/META-INF/NOTICE b/configadmin/src/main/appended-resources/META-INF/NOTICE new file mode 100644 index 00000000000..c87a1877c2d --- /dev/null +++ b/configadmin/src/main/appended-resources/META-INF/NOTICE @@ -0,0 +1,4 @@ +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2012). +Licensed under the Apache License 2.0. \ No newline at end of file diff --git a/configadmin/src/main/java/org/apache/felix/cm/NotCachablePersistenceManager.java b/configadmin/src/main/java/org/apache/felix/cm/NotCachablePersistenceManager.java new file mode 100644 index 00000000000..2c5921f9e5f --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/NotCachablePersistenceManager.java @@ -0,0 +1,40 @@ +/* + * 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.cm; + +import org.osgi.annotation.versioning.ConsumerType; + +/** + * NotCachablePersistenceManager is a marker interface which + * extends {@link PersistenceManager} to inform that no cache should be applied + * around this persistence manager. This gives the opportunity for the + * persistence manager to implement it's own caching heuristics. + *

      + * To make implementations of this interface available to the Configuration + * Admin Service they must be registered as service for interface + * {@link PersistenceManager}. + *

      + * See also {@link PersistenceManager} + * + * @since 1.1 + */ +@ConsumerType +public interface NotCachablePersistenceManager extends PersistenceManager +{ +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/PersistenceManager.java b/configadmin/src/main/java/org/apache/felix/cm/PersistenceManager.java new file mode 100644 index 00000000000..40660ed75e9 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/PersistenceManager.java @@ -0,0 +1,146 @@ +/* + * 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.cm; + + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Enumeration; + +import org.osgi.annotation.versioning.ConsumerType; + + +/** + * The PersistenceManager interface defines the API to be + * implemented to support persisting configuration data. This interface may + * be implemented by bundles, which support storing configuration data in + * different locations. + *

      + * The Apache Felix Configuration Admin Service bundles provides an + * implementation of this interface using the platform file system to store + * configuration data. + *

      + * Implementations of this interface must support loading and storing + * java.util.Dictionary objects as defined in section 104.4.2, + * Configuration Properties, of the Configuration Admin Service Specification + * Version 1.2. + *

      + * To make implementations of this interface available to the Configuration + * Admin Service they must be registered as service for this interface. The + * Configuration Admin Service will consider all registered services plus the + * default platform file system based implementation to load configuration data. + * To store new configuration data, the persistence manager service with the + * highest rank value - the service.ranking service property - is + * used. If no persistence manager service has been registered, the platform + * file system based implementation is used. + */ +@ConsumerType +public interface PersistenceManager +{ + /** + * Service registration property to define a unique name for the persistence manager. + * @since 1.2 + */ + String PROPERTY_NAME = "name"; + + /** + * Returns true if a persisted Dictionary exists for + * the given pid. + * + * @param pid The identifier for the dictionary to test. + * @return {@code true} if a persisted dictionary exists for the pid. + */ + boolean exists( String pid ); + + + /** + * Returns the Dictionary for the given pid. + *

      + * Implementations are expected to return dictionary instances which may be + * modified by the caller without affecting any underlying data or affecting + * future calls to this method with the same PID. In other words the + * reference equation load(pid) != load(pid) must hold + * true. + * + * @param pid The identifier for the dictionary to load. + * + * @return The dictionary for the identifier. This must not be + * null but may be empty. + * + * @throws IOException If an error occurs loading the dictionary. An + * IOException must also be thrown if no dictionary + * exists for the given identifier. + */ + Dictionary load( String pid ) throws IOException; + + + /** + * Returns an enumeration of all Dictionary objects known to + * this persistence manager. + *

      + * Implementations of this method are allowed to return lazy enumerations. + * That is, it is allowable for the enumeration to not return a dictionary + * if loading it results in an error. + *

      + * Implementations are expected to return dictionary instances which may be + * modified by the caller without affecting any underlying data or affecting + * future calls to this method. + *

      + * The Enumeration returned from this method must be stable + * against concurrent calls to either of the {@link #load(String)}, + * {@link #store(String, Dictionary)}, and {@link #delete(String)} methods. + * + * @return A possibly empty Enumeration of all dictionaries. + * + * @throws IOException If an error occurs getting the dictionaries. + */ + Enumeration getDictionaries() throws IOException; + + + /** + * Stores the Dictionary under the given pid. + *

      + * The dictionary provided to this method must be considered private to the + * caller. Any modification by the caller after this method finishes must + * not influence any internal storage of the provided data. Implementations + * must not modify the dictionary. + * + * @param pid The identifier of the dictionary. + * @param properties The Dictionary to store. + * + * @throws IOException If an error occurs storing the dictionary. If this + * exception is thrown, it is expected, that + * {@link #exists(String) exists(pid} returns false. + */ + void store( String pid, Dictionary properties ) throws IOException; + + + /** + * Removes the Dictionary for the given pid. If + * such a dictionary does not exist, this method has no effect. + * + * @param pid The identifier of the dictionary to delete. + * + * @throws IOException If an error occurs deleting the dictionary. This + * exception must not be thrown if no dictionary with the given + * identifier exists. + */ + void delete( String pid ) throws IOException; + +} \ No newline at end of file diff --git a/configadmin/src/main/java/org/apache/felix/cm/file/ConfigurationHandler.java b/configadmin/src/main/java/org/apache/felix/cm/file/ConfigurationHandler.java new file mode 100644 index 00000000000..62a40c7a9f6 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/file/ConfigurationHandler.java @@ -0,0 +1,859 @@ +/* + * 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.cm.file; + + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PushbackReader; +import java.io.Writer; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + + +/** + * The ConfigurationHandler class implements configuration reading + * form a java.io.InputStream and writing to a + * java.io.OutputStream on behalf of the + * {@link FilePersistenceManager} class. + * + *

      + * cfg = prop "=" value .
      + *  prop = symbolic-name . // 1.4.2 of OSGi Core Specification
      + *  symbolic-name = token { "." token } .
      + *  token = { [ 0..9 ] | [ a..z ] | [ A..Z ] | '_' | '-' } .
      + *  value = [ type ] ( "[" values "]" | "(" values ")" | simple ) .
      + *  values = simple { "," simple } .
      + *  simple = """ stringsimple """ .
      + *  type = // 1-char type code .
      + *  stringsimple = // quoted string representation of the value .
      + * 
      + */ +public class ConfigurationHandler +{ + protected static final String ENCODING = "UTF-8"; + + protected static final int TOKEN_NAME = 'N'; + protected static final int TOKEN_EQ = '='; + protected static final int TOKEN_ARR_OPEN = '['; + protected static final int TOKEN_ARR_CLOS = ']'; + protected static final int TOKEN_VEC_OPEN = '('; + protected static final int TOKEN_VEC_CLOS = ')'; + protected static final int TOKEN_COMMA = ','; + protected static final int TOKEN_VAL_OPEN = '"'; // '{'; + protected static final int TOKEN_VAL_CLOS = '"'; // '}'; + + protected static final int TOKEN_COMMENT = '#'; + + // simple types (string & primitive wrappers) + protected static final int TOKEN_SIMPLE_STRING = 'T'; + protected static final int TOKEN_SIMPLE_INTEGER = 'I'; + protected static final int TOKEN_SIMPLE_LONG = 'L'; + protected static final int TOKEN_SIMPLE_FLOAT = 'F'; + protected static final int TOKEN_SIMPLE_DOUBLE = 'D'; + protected static final int TOKEN_SIMPLE_BYTE = 'X'; + protected static final int TOKEN_SIMPLE_SHORT = 'S'; + protected static final int TOKEN_SIMPLE_CHARACTER = 'C'; + protected static final int TOKEN_SIMPLE_BOOLEAN = 'B'; + + // primitives + protected static final int TOKEN_PRIMITIVE_INT = 'i'; + protected static final int TOKEN_PRIMITIVE_LONG = 'l'; + protected static final int TOKEN_PRIMITIVE_FLOAT = 'f'; + protected static final int TOKEN_PRIMITIVE_DOUBLE = 'd'; + protected static final int TOKEN_PRIMITIVE_BYTE = 'x'; + protected static final int TOKEN_PRIMITIVE_SHORT = 's'; + protected static final int TOKEN_PRIMITIVE_CHAR = 'c'; + protected static final int TOKEN_PRIMITIVE_BOOLEAN = 'b'; + + protected static final String CRLF = "\r\n"; + protected static final String INDENT = " "; + protected static final String COLLECTION_LINE_BREAK = " \\\r\n"; + + protected static final Map> code2Type; + protected static final Map, Integer> type2Code; + + // set of valid characters for "symblic-name" + private static final BitSet NAME_CHARS; + private static final BitSet TOKEN_CHARS; + + static + { + type2Code = new HashMap, Integer>(); + + // simple (exclusive String whose type code is not written) + type2Code.put( Integer.class, new Integer( TOKEN_SIMPLE_INTEGER ) ); + type2Code.put( Long.class, new Integer( TOKEN_SIMPLE_LONG ) ); + type2Code.put( Float.class, new Integer( TOKEN_SIMPLE_FLOAT ) ); + type2Code.put( Double.class, new Integer( TOKEN_SIMPLE_DOUBLE ) ); + type2Code.put( Byte.class, new Integer( TOKEN_SIMPLE_BYTE ) ); + type2Code.put( Short.class, new Integer( TOKEN_SIMPLE_SHORT ) ); + type2Code.put( Character.class, new Integer( TOKEN_SIMPLE_CHARACTER ) ); + type2Code.put( Boolean.class, new Integer( TOKEN_SIMPLE_BOOLEAN ) ); + + // primitives + type2Code.put( Integer.TYPE, new Integer( TOKEN_PRIMITIVE_INT ) ); + type2Code.put( Long.TYPE, new Integer( TOKEN_PRIMITIVE_LONG ) ); + type2Code.put( Float.TYPE, new Integer( TOKEN_PRIMITIVE_FLOAT ) ); + type2Code.put( Double.TYPE, new Integer( TOKEN_PRIMITIVE_DOUBLE ) ); + type2Code.put( Byte.TYPE, new Integer( TOKEN_PRIMITIVE_BYTE ) ); + type2Code.put( Short.TYPE, new Integer( TOKEN_PRIMITIVE_SHORT ) ); + type2Code.put( Character.TYPE, new Integer( TOKEN_PRIMITIVE_CHAR ) ); + type2Code.put( Boolean.TYPE, new Integer( TOKEN_PRIMITIVE_BOOLEAN ) ); + + // reverse map to map type codes to classes, string class mapping + // to be added manually, as the string type code is not written and + // hence not included in the type2Code map + code2Type = new HashMap>(); + for(final Map.Entry, Integer> entry : type2Code.entrySet()) + { + code2Type.put( entry.getValue(), entry.getKey() ); + } + code2Type.put( new Integer( TOKEN_SIMPLE_STRING ), String.class ); + + NAME_CHARS = new BitSet(); + for ( int i = '0'; i <= '9'; i++ ) + NAME_CHARS.set( i ); + for ( int i = 'a'; i <= 'z'; i++ ) + NAME_CHARS.set( i ); + for ( int i = 'A'; i <= 'Z'; i++ ) + NAME_CHARS.set( i ); + NAME_CHARS.set( '_' ); + NAME_CHARS.set( '-' ); + NAME_CHARS.set( '.' ); + NAME_CHARS.set( '\\' ); + + TOKEN_CHARS = new BitSet(); + TOKEN_CHARS.set( TOKEN_EQ ); + TOKEN_CHARS.set( TOKEN_ARR_OPEN ); + TOKEN_CHARS.set( TOKEN_ARR_CLOS ); + TOKEN_CHARS.set( TOKEN_VEC_OPEN ); + TOKEN_CHARS.set( TOKEN_VEC_CLOS ); + TOKEN_CHARS.set( TOKEN_COMMA ); + TOKEN_CHARS.set( TOKEN_VAL_OPEN ); + TOKEN_CHARS.set( TOKEN_VAL_CLOS ); + TOKEN_CHARS.set( TOKEN_SIMPLE_STRING ); + TOKEN_CHARS.set( TOKEN_SIMPLE_INTEGER ); + TOKEN_CHARS.set( TOKEN_SIMPLE_LONG ); + TOKEN_CHARS.set( TOKEN_SIMPLE_FLOAT ); + TOKEN_CHARS.set( TOKEN_SIMPLE_DOUBLE ); + TOKEN_CHARS.set( TOKEN_SIMPLE_BYTE ); + TOKEN_CHARS.set( TOKEN_SIMPLE_SHORT ); + TOKEN_CHARS.set( TOKEN_SIMPLE_CHARACTER ); + TOKEN_CHARS.set( TOKEN_SIMPLE_BOOLEAN ); + + // primitives + TOKEN_CHARS.set( TOKEN_PRIMITIVE_INT ); + TOKEN_CHARS.set( TOKEN_PRIMITIVE_LONG ); + TOKEN_CHARS.set( TOKEN_PRIMITIVE_FLOAT ); + TOKEN_CHARS.set( TOKEN_PRIMITIVE_DOUBLE ); + TOKEN_CHARS.set( TOKEN_PRIMITIVE_BYTE ); + TOKEN_CHARS.set( TOKEN_PRIMITIVE_SHORT ); + TOKEN_CHARS.set( TOKEN_PRIMITIVE_CHAR ); + TOKEN_CHARS.set( TOKEN_PRIMITIVE_BOOLEAN ); + } + + + /** + * Writes the configuration data from the Dictionary to the + * given OutputStream. + *

      + * This method writes at the current location in the stream and does not + * close the output stream. + * + * @param out + * The OutputStream to write the configuration data + * to. + * @param properties + * The Dictionary to write. + * @throws IOException + * If an error occurs writing to the output stream. + */ + @SuppressWarnings("rawtypes") + public static void write( OutputStream out, Dictionary properties ) throws IOException + { + BufferedWriter bw = new BufferedWriter( new OutputStreamWriter( out, ENCODING ) ); + + for ( Enumeration ce = orderedKeys(properties); ce.hasMoreElements(); ) + { + String key = ( String ) ce.nextElement(); + + // cfg = prop "=" value "." . + writeQuoted( bw, key ); + bw.write( TOKEN_EQ ); + writeValue( bw, properties.get( key ) ); + bw.write( CRLF ); + } + + bw.flush(); + } + + /** + * Generates an Enumeration for the given + * Dictionary where the keys of the Dictionary + * are provided in sorted order. + * + * @param properties + * The Dictionary that keys are sorted. + * @return An Enumeration that provides the keys of + * properties in an ordered manner. + */ + @SuppressWarnings("rawtypes") + private static Enumeration orderedKeys(Dictionary properties) { + String[] keyArray = new String[properties.size()]; + int i = 0; + for ( Enumeration ce = properties.keys(); ce.hasMoreElements(); ) + { + keyArray[i] = ( String ) ce.nextElement(); + i++; + } + Arrays.sort(keyArray); + return Collections.enumeration( Arrays.asList( keyArray ) ); + } + + + /** + * Reads configuration data from the given InputStream and + * returns a new Dictionary object containing the data. + *

      + * This method reads from the current location in the stream up to the end of + * the stream but does not close the stream at the end. + * + * @param ins + * The InputStream from which to read the + * configuration data. + * @return A Dictionary object containing the configuration + * data. This object may be empty if the stream contains no + * configuration data. + * @throws IOException + * If an error occurs reading from the stream. This exception + * is also thrown if a syntax error is encountered. + */ + @SuppressWarnings("rawtypes") + public static Dictionary read( InputStream ins ) throws IOException + { + return new ConfigurationHandler().readInternal( ins ); + } + + + // private constructor, this class is not to be instantiated from the + // outside + private ConfigurationHandler() + { + } + + // ---------- Configuration Input Implementation --------------------------- + + private int token; + private String tokenValue; + private int line; + private int pos; + + + private Dictionary readInternal( InputStream ins ) throws IOException + { + BufferedReader br = new BufferedReader( new InputStreamReader( ins, ENCODING ) ); + PushbackReader pr = new PushbackReader( br, 1 ); + + token = 0; + tokenValue = null; + line = 0; + pos = 0; + + Dictionary configuration = new Hashtable(); + token = 0; + while ( nextToken( pr, true ) == TOKEN_NAME ) + { + String key = tokenValue; + + // expect equal sign + if ( nextToken( pr, false ) != TOKEN_EQ ) + { + throw readFailure( token, TOKEN_EQ ); + } + + // expect the token value + Object value = readValue( pr ); + if ( value != null ) + { + configuration.put( key, value ); + } + } + + return configuration; + } + + + /** + * value = type ( "[" values "]" | "(" values ")" | simple ) . values = + * value { "," value } . simple = "{" stringsimple "}" . type = // 1-char + * type code . stringsimple = // quoted string representation of the value . + * + * @param pr + * @return + * @throws IOException + */ + private Object readValue( PushbackReader pr ) throws IOException + { + // read (optional) type code + int type = read( pr ); + + // read value kind code if type code is not a value kinde code + int code; + if ( code2Type.containsKey( new Integer( type ) ) ) + { + code = read( pr ); + } + else + { + code = type; + type = TOKEN_SIMPLE_STRING; + } + + switch ( code ) + { + case TOKEN_ARR_OPEN: + return readArray( type, pr ); + + case TOKEN_VEC_OPEN: + return readCollection( type, pr ); + + case TOKEN_VAL_OPEN: + Object value = readSimple( type, pr ); + ensureNext( pr, TOKEN_VAL_CLOS ); + return value; + + default: + return null; + } + } + + + private Object readArray( int typeCode, PushbackReader pr ) throws IOException + { + List list = new ArrayList(); + for ( ;; ) + { + int c = ignorablePageBreakAndWhiteSpace( pr ); + if ( c == TOKEN_VAL_OPEN ) + { + Object value = readSimple( typeCode, pr ); + if ( value == null ) + { + // abort due to error + return null; + } + + ensureNext( pr, TOKEN_VAL_CLOS ); + + list.add( value ); + + c = ignorablePageBreakAndWhiteSpace( pr ); + } + + if ( c == TOKEN_ARR_CLOS ) + { + Class type = code2Type.get( new Integer( typeCode ) ); + Object array = Array.newInstance( type, list.size() ); + for ( int i = 0; i < list.size(); i++ ) + { + Array.set( array, i, list.get( i ) ); + } + return array; + } + else if ( c < 0 ) + { + return null; + } + else if ( c != TOKEN_COMMA ) + { + return null; + } + } + } + + + private Collection readCollection( int typeCode, PushbackReader pr ) throws IOException + { + Collection collection = new ArrayList(); + for ( ;; ) + { + int c = ignorablePageBreakAndWhiteSpace( pr ); + if ( c == TOKEN_VAL_OPEN ) + { + Object value = readSimple( typeCode, pr ); + if ( value == null ) + { + // abort due to error + return null; + } + + ensureNext( pr, TOKEN_VAL_CLOS ); + + collection.add( value ); + + c = ignorablePageBreakAndWhiteSpace( pr ); + } + + if ( c == TOKEN_VEC_CLOS ) + { + return collection; + } + else if ( c < 0 ) + { + return null; + } + else if ( c != TOKEN_COMMA ) + { + return null; + } + } + } + + + private Object readSimple( int code, PushbackReader pr ) throws IOException + { + switch ( code ) + { + case -1: + return null; + + case TOKEN_SIMPLE_STRING: + return readQuoted( pr ); + + // Simple/Primitive, only use wrapper classes + case TOKEN_SIMPLE_INTEGER: + case TOKEN_PRIMITIVE_INT: + return Integer.valueOf( readQuoted( pr ) ); + + case TOKEN_SIMPLE_LONG: + case TOKEN_PRIMITIVE_LONG: + return Long.valueOf( readQuoted( pr ) ); + + case TOKEN_SIMPLE_FLOAT: + case TOKEN_PRIMITIVE_FLOAT: + int fBits = Integer.parseInt( readQuoted( pr ) ); + return new Float( Float.intBitsToFloat( fBits ) ); + + case TOKEN_SIMPLE_DOUBLE: + case TOKEN_PRIMITIVE_DOUBLE: + long dBits = Long.parseLong( readQuoted( pr ) ); + return new Double( Double.longBitsToDouble( dBits ) ); + + case TOKEN_SIMPLE_BYTE: + case TOKEN_PRIMITIVE_BYTE: + return Byte.valueOf( readQuoted( pr ) ); + + case TOKEN_SIMPLE_SHORT: + case TOKEN_PRIMITIVE_SHORT: + return Short.valueOf( readQuoted( pr ) ); + + case TOKEN_SIMPLE_CHARACTER: + case TOKEN_PRIMITIVE_CHAR: + String cString = readQuoted( pr ); + if ( cString != null && cString.length() > 0 ) + { + return new Character( cString.charAt( 0 ) ); + } + return null; + + case TOKEN_SIMPLE_BOOLEAN: + case TOKEN_PRIMITIVE_BOOLEAN: + return Boolean.valueOf( readQuoted( pr ) ); + + // unknown type code + default: + return null; + } + } + + + private void ensureNext( PushbackReader pr, int expected ) throws IOException + { + int next = read( pr ); + if ( next != expected ) + { + readFailure( next, expected ); + } + } + + + private String readQuoted( PushbackReader pr ) throws IOException + { + StringBuilder buf = new StringBuilder(); + for ( ;; ) + { + int c = read( pr ); + switch ( c ) + { + // escaped character + case '\\': + c = read( pr ); + switch ( c ) + { + // well known escapes + case 'b': + buf.append( '\b' ); + break; + case 't': + buf.append( '\t' ); + break; + case 'n': + buf.append( '\n' ); + break; + case 'f': + buf.append( '\f' ); + break; + case 'r': + buf.append( '\r' ); + break; + case 'u':// need 4 characters ! + char[] cbuf = new char[4]; + if ( read( pr, cbuf ) == 4 ) + { + c = Integer.parseInt( new String( cbuf ), 16 ); + buf.append( ( char ) c ); + } + break; + + // just an escaped character, unescape + default: + buf.append( ( char ) c ); + } + break; + + // eof + case -1: // fall through + + // separator token + case TOKEN_EQ: + case TOKEN_VAL_CLOS: + pr.unread( c ); + return buf.toString(); + + // no escaping + default: + buf.append( ( char ) c ); + } + } + } + + private int nextToken( PushbackReader pr, final boolean newLine ) throws IOException + { + int c = ignorableWhiteSpace( pr ); + + // immediately return EOF + if ( c < 0 ) + { + return ( token = c ); + } + + // check for comment + if ( newLine && c == TOKEN_COMMENT ) + { + // skip everything until end of line + do + { + c = read( pr ); + } while ( c != -1 && c != '\n' ); + if ( c == -1 ) + { + return ( token = c); + } + // and start over + return nextToken( pr, true ); + } + + // check whether there is a name + if ( NAME_CHARS.get( c ) || !TOKEN_CHARS.get( c ) ) + { + // read the property name + pr.unread( c ); + tokenValue = readQuoted( pr ); + return ( token = TOKEN_NAME ); + } + + // check another token + if ( TOKEN_CHARS.get( c ) ) + { + return ( token = c ); + } + + // unexpected character -> so what ?? + return ( token = -1 ); + } + + + private int ignorableWhiteSpace( PushbackReader pr ) throws IOException + { + int c = read( pr ); + while ( c >= 0 && Character.isWhitespace( ( char ) c ) ) + { + c = read( pr ); + } + return c; + } + + + private int ignorablePageBreakAndWhiteSpace( PushbackReader pr ) throws IOException + { + int c = ignorableWhiteSpace( pr ); + for ( ;; ) + { + if ( c != '\\' ) + { + break; + } + int c1 = pr.read(); + if ( c1 == '\r' || c1 == '\n' ) + { + c = ignorableWhiteSpace( pr ); + } else { + pr.unread(c1); + break; + } + } + return c; + } + + + private int read( PushbackReader pr ) throws IOException + { + int c = pr.read(); + if ( c == '\r' ) + { + int c1 = pr.read(); + if ( c1 != '\n' ) + { + pr.unread( c1 ); + } + c = '\n'; + } + + if ( c == '\n' ) + { + line++; + pos = 0; + } + else + { + pos++; + } + + return c; + } + + + private int read( PushbackReader pr, char[] buf ) throws IOException + { + for ( int i = 0; i < buf.length; i++ ) + { + int c = read( pr ); + if ( c >= 0 ) + { + buf[i] = ( char ) c; + } + else + { + return i; + } + } + + return buf.length; + } + + + private IOException readFailure( int current, int expected ) + { + return new IOException( "Unexpected token " + current + "; expected: " + expected + " (line=" + line + ", pos=" + + pos + ")" ); + } + + + // ---------- Configuration Output Implementation -------------------------- + + private static void writeValue( Writer out, Object value ) throws IOException + { + Class clazz = value.getClass(); + if ( clazz.isArray() ) + { + writeArray( out, value ); + } + else if ( value instanceof Collection ) + { + writeCollection( out, ( Collection ) value ); + } + else + { + writeType( out, clazz ); + writeSimple( out, value ); + } + } + + + private static void writeArray( Writer out, Object arrayValue ) throws IOException + { + int size = Array.getLength( arrayValue ); + writeType( out, arrayValue.getClass().getComponentType() ); + out.write( TOKEN_ARR_OPEN ); + out.write( COLLECTION_LINE_BREAK ); + for ( int i = 0; i < size; i++ ) + { + writeCollectionElement(out, Array.get( arrayValue, i )); + } + out.write( INDENT ); + out.write( TOKEN_ARR_CLOS ); + } + + + private static void writeCollection( Writer out, Collection collection ) throws IOException + { + if ( collection.isEmpty() ) + { + out.write( TOKEN_VEC_OPEN ); + out.write( COLLECTION_LINE_BREAK ); + out.write( TOKEN_VEC_CLOS ); + } + else + { + Iterator ci = collection.iterator(); + Object firstElement = ci.next(); + + writeType( out, firstElement.getClass() ); + out.write( TOKEN_VEC_OPEN ); + out.write( COLLECTION_LINE_BREAK ); + + writeCollectionElement( out, firstElement ); + + while ( ci.hasNext() ) + { + writeCollectionElement( out, ci.next() ); + } + out.write( TOKEN_VEC_CLOS ); + } + } + + + private static void writeCollectionElement(Writer out, Object element) throws IOException { + out.write( INDENT ); + writeSimple( out, element ); + out.write( TOKEN_COMMA ); + out.write(COLLECTION_LINE_BREAK); + } + + + private static void writeType( Writer out, Class valueType ) throws IOException + { + Integer code = type2Code.get( valueType ); + if ( code != null ) + { + out.write( ( char ) code.intValue() ); + } + } + + + private static void writeSimple( Writer out, Object value ) throws IOException + { + if ( value instanceof Double ) + { + double dVal = ( ( Double ) value ).doubleValue(); + value = new Long( Double.doubleToRawLongBits( dVal ) ); + } + else if ( value instanceof Float ) + { + float fVal = ( ( Float ) value ).floatValue(); + value = new Integer( Float.floatToRawIntBits( fVal ) ); + } + + out.write( TOKEN_VAL_OPEN ); + writeQuoted( out, String.valueOf( value ) ); + out.write( TOKEN_VAL_CLOS ); + } + + + private static void writeQuoted( Writer out, String simple ) throws IOException + { + if ( simple == null || simple.length() == 0 ) + { + return; + } + + char c = 0; + int len = simple.length(); + for ( int i = 0; i < len; i++ ) + { + c = simple.charAt( i ); + switch ( c ) + { + case '\\': + case TOKEN_VAL_CLOS: + case ' ': + case TOKEN_EQ: + out.write( '\\' ); + out.write( c ); + break; + + // well known escapes + case '\b': + out.write( "\\b" ); + break; + case '\t': + out.write( "\\t" ); + break; + case '\n': + out.write( "\\n" ); + break; + case '\f': + out.write( "\\f" ); + break; + case '\r': + out.write( "\\r" ); + break; + + // other escaping + default: + if ( c < ' ' ) + { + String t = "000" + Integer.toHexString( c ); + out.write( "\\u" + t.substring( t.length() - 4 ) ); + } + else + { + out.write( c ); + } + } + } + } +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/file/FilePersistenceManager.java b/configadmin/src/main/java/org/apache/felix/cm/file/FilePersistenceManager.java new file mode 100644 index 00000000000..91c8419e46e --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/file/FilePersistenceManager.java @@ -0,0 +1,927 @@ +/* + * 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.cm.file; + + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.BitSet; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.NoSuchElementException; +import java.util.Stack; +import java.util.StringTokenizer; + +import org.apache.felix.cm.PersistenceManager; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; + + +/** + * The FilePersistenceManager class stores configuration data in + * properties-like files inside a given directory. All configuration files are + * located in the same directory. + *

      + * The configuration directory is set by either the + * {@link #FilePersistenceManager(String)} constructor or the + * {@link #FilePersistenceManager(BundleContext, String)} constructor. Refer to + * the respective JavaDocs for more information. + *

      + * When this persistence manager is used by the Configuration Admin Service, the + * location may be configured using the + * {@link org.apache.felix.cm.impl.ConfigurationManager} bundle context + * property. + *

      + * If the location is not set, the config directory in the current + * working directory (as set in the user.dir system property) is + * used. If the the location is set but, no such directory exists, the directory + * and any missing parent directories are created. If a file exists at the given + * location, the constructor fails. + *

      + * Configuration files are created in the configuration directory by appending + * the extension .config to the PID of the configuration. The PID + * is converted into a relative path name by replacing enclosed dots to slashes. + * Non-symbolic-name characters in the PID are encoded with their + * Unicode character code in hexadecimal. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
      Examples of PID to name conversion:
      PIDConfiguration File Name
      sample + * sample.config + *
      org.apache.felix.log.LogService + * org/apache/felix/log/LogService.config + *
      sample.fläche + * sample/fl%00e8che.config + *
      + *

      + * Mulithreading Issues + *

      + * In a multithreaded environment the {@link #store(String, Dictionary)} and + * {@link #load(String)} methods may be called at the the quasi-same time for + * the same configuration PID. It may no happen, that the store method starts + * writing the file and the load method might at the same time read from the + * file currently being written and thus loading corrupt data (if data is + * available at all). + *

      + * To prevent this situation from happening, the methods use synchronization and + * temporary files as follows: + *

        + *
      • The {@link #store(String, Dictionary)} method writes a temporary file + * with file extension .tmp. When done, the file is renamed to + * actual configuration file name as implied by the PID. This last step of + * renaming the file is synchronized on the FilePersistenceManager + * instance.
      • + *
      • The {@link #load(String)} method is completeley synchronized on the + * FilePersistenceManager instance such that the {@link #store} method might + * inadvertantly try to replace the file while it is being read.
      • + *
      • Finally the Iterator returned by {@link #getDictionaries()} + * is implemented such that any temporary configuration file is just + * ignored.
      • + *
      + */ +public class FilePersistenceManager implements PersistenceManager +{ + + /** + * The default configuration data directory if no location is configured + * (value is "config"). + */ + public static final String DEFAULT_CONFIG_DIR = "config"; + + /** + * The name of this persistence manager when registered in the service registry. + * (value is "file"). + */ + public static final String DEFAULT_PERSISTENCE_MANAGER_NAME = "file"; + + /** + * The extension of the configuration files. + */ + private static final String FILE_EXT = ".config"; + + /** + * The extension of the configuration files, while they are being written + * (value is ".tmp"). + * + * @see #store(String, Dictionary) + */ + private static final String TMP_EXT = ".tmp"; + + private static final BitSet VALID_PATH_CHARS; + + /** + * The access control context we use in the presence of a security manager. + */ + private final AccessControlContext acc; + + /** + * The abstract path name of the configuration files. + */ + private final File location; + + /** + * Flag indicating whether this instance is running on a Windows + * platform or not. + */ + private final boolean isWin; + + // sets up this class defining the set of valid characters in path + // set getFile(String) for details. + static + { + VALID_PATH_CHARS = new BitSet(); + + for ( int i = 'a'; i <= 'z'; i++ ) + { + VALID_PATH_CHARS.set( i ); + } + for ( int i = 'A'; i <= 'Z'; i++ ) + { + VALID_PATH_CHARS.set( i ); + } + for ( int i = '0'; i <= '9'; i++ ) + { + VALID_PATH_CHARS.set( i ); + } + VALID_PATH_CHARS.set( File.separatorChar ); + VALID_PATH_CHARS.set( ' ' ); + VALID_PATH_CHARS.set( '-' ); + VALID_PATH_CHARS.set( '_' ); + } + + + { + // Windows Specific Preparation (FELIX-4302) + // According to the GetJavaProperties method in + // jdk/src/windows/native/java/lang/java_props_md.c the os.name + // system property on all Windows platforms start with the string + // "Windows" hence we assume a Windows platform in thus case. + final String osName = System.getProperty( "os.name" ); + isWin = osName != null && osName.startsWith( "Windows" ); + } + + private static boolean equalsNameWithPrefixPlusOneDigit( String name, String prefix) { + if ( name.length() != prefix.length() + 1 ) { + return false; + } + if ( !name.startsWith(prefix) ) { + return false; + } + char charAfterPrefix = name.charAt( prefix.length() ); + return charAfterPrefix > '0' && charAfterPrefix < '9'; + } + + private static boolean isWinReservedName(String name) { + String upperCaseName = name.toUpperCase(); + if ( "CON".equals( upperCaseName ) ) { + return true; + } else if ( "PRN".equals( upperCaseName ) ){ + return true; + } else if ( "AUX".equals( upperCaseName ) ){ + return true; + } else if ( "CLOCK$".equals( upperCaseName ) ){ + return true; + } else if ( "NUL".equals( upperCaseName ) ){ + return true; + } else if ( equalsNameWithPrefixPlusOneDigit( upperCaseName, "COM") ) { + return true; + } else if ( equalsNameWithPrefixPlusOneDigit( upperCaseName, "LPT") ){ + return true; + } + return false; + } + + + /** + * Creates an instance of this persistence manager using the given location + * as the directory to store and retrieve the configuration files. + *

      + * This constructor resolves the configuration file location as follows: + *

        + *
      • If location is null, the config + * directory in the current working directory as specified in the + * user.dir system property is assumed.
      • + *
      • Otherwise the named directory is used.
      • + *
      • If the directory name resolved in the first or second step is not an + * absolute path, it is resolved to an absolute path calling the + * File.getAbsoluteFile() method.
      • + *
      • If a non-directory file exists as the location found in the previous + * step or the named directory (including any parent directories) cannot be + * created, an IllegalArgumentException is thrown.
      • + *
      + *

      + * This constructor is equivalent to calling + * {@link #FilePersistenceManager(BundleContext, String)} with a + * null BundleContext. + * + * @param location The configuration file location. If this is + * null the config directory below the current + * working directory is used. + * + * @throws IllegalArgumentException If the location exists but + * is not a directory or does not exist and cannot be created. + */ + public FilePersistenceManager( String location ) + { + this( null, location ); + } + + + /** + * Creates an instance of this persistence manager using the given location + * as the directory to store and retrieve the configuration files. + *

      + * This constructor resolves the configuration file location as follows: + *

        + *
      • If location is null, the config + * directory in the persistent storage area of the bundle identified by + * bundleContext is used.
      • + *
      • If the framework does not support persistent storage area for bundles + * in the filesystem or if bundleContext is null, + * the config directory in the current working directory as + * specified in the user.dir system property is assumed.
      • + *
      • Otherwise the named directory is used.
      • + *
      • If the directory name resolved in the first, second or third step is + * not an absolute path and a bundleContext is provided which + * provides access to persistent storage area, the directory name is + * resolved as being inside the persistent storage area. Otherwise the + * directory name is resolved to an absolute path calling the + * File.getAbsoluteFile() method.
      • + *
      • If a non-directory file exists as the location found in the previous + * step or the named directory (including any parent directories) cannot be + * created, an IllegalArgumentException is thrown.
      • + *
      + * + * @param bundleContext The BundleContext to optionally get + * the data location for the configuration files. This may be + * null, in which case this constructor acts exactly the + * same as calling {@link #FilePersistenceManager(String)}. + * @param location The configuration file location. If this is + * null the config directory below the current + * working directory is used. + * + * @throws IllegalArgumentException If the location exists but is not a + * directory or does not exist and cannot be created. + * @throws IllegalStateException If the bundleContext is not + * valid. + */ + public FilePersistenceManager( BundleContext bundleContext, String location ) + { + // setup the access control context from the calling setup + if ( System.getSecurityManager() != null ) + { + acc = AccessController.getContext(); + } + else + { + acc = null; + } + + // no configured location, use the config dir in the bundle persistent + // area + if ( location == null && bundleContext != null ) + { + File locationFile = bundleContext.getDataFile( DEFAULT_CONFIG_DIR ); + if ( locationFile != null ) + { + location = locationFile.getAbsolutePath(); + } + } + + // fall back to the current working directory if the platform does + // not support filesystem based data area + if ( location == null ) + { + location = System.getProperty( "user.dir" ) + "/config"; + } + + // ensure the file is absolute + File locationFile = new File( location ); + if ( !locationFile.isAbsolute() ) + { + if ( bundleContext != null ) + { + File bundleLocationFile = bundleContext.getDataFile( locationFile.getPath() ); + if ( bundleLocationFile != null ) + { + locationFile = bundleLocationFile; + } + } + + // ensure the file object is an absolute file object + locationFile = locationFile.getAbsoluteFile(); + } + + // check the location + if ( !locationFile.isDirectory() ) + { + if ( locationFile.exists() ) + { + throw new IllegalArgumentException( location + " is not a directory" ); + } + + if ( !locationFile.mkdirs() ) + { + throw new IllegalArgumentException( "Cannot create directory " + location ); + } + } + + this.location = locationFile; + } + + + /** + * Encodes a Service PID to a filesystem path as described in the class + * JavaDoc above. + *

      + * This method is not part of the API of this class and is declared package + * private to enable JUnit testing on it. This method may be removed or + * modified at any time without notice. + * + * @param pid The Service PID to encode into a relative path name. + * + * @return The relative path name corresponding to the Service PID. + */ + String encodePid( String pid ) + { + + // replace dots by File.separatorChar + pid = pid.replace( '.', File.separatorChar ); + + // replace slash by File.separatorChar if different + if ( File.separatorChar != '/' ) + { + pid = pid.replace( '/', File.separatorChar ); + } + + // scan for first non-valid character (if any) + int first = 0; + while ( first < pid.length() && VALID_PATH_CHARS.get( pid.charAt( first ) ) ) + { + first++; + } + + // check whether we exhausted + if ( first < pid.length() ) + { + StringBuilder buf = new StringBuilder( pid.substring( 0, first ) ); + + for ( int i = first; i < pid.length(); i++ ) + { + char c = pid.charAt( i ); + if ( VALID_PATH_CHARS.get( c ) ) + { + buf.append( c ); + } + else + { + appendEncoded( buf, c ); + } + } + + pid = buf.toString(); + } + + // Prefix special device names on Windows (FELIX-4302) + if ( isWin ) + { + final StringTokenizer segments = new StringTokenizer( pid, File.separator, true ); + final StringBuilder pidBuffer = new StringBuilder( pid.length() ); + while ( segments.hasMoreTokens() ) + { + final String segment = segments.nextToken(); + if ( isWinReservedName(segment) ) + { + appendEncoded( pidBuffer, segment.charAt( 0 ) ); + pidBuffer.append( segment.substring( 1 ) ); + } + else + { + pidBuffer.append( segment ); + } + } + pid = pidBuffer.toString(); + } + + return pid; + } + + + private void appendEncoded( StringBuilder buf, final char c ) + { + String val = "000" + Integer.toHexString( c ); + buf.append( '%' ); + buf.append( val.substring( val.length() - 4 ) ); + } + + + /** + * Returns the directory in which the configuration files are written as + * a File object. + * + * @return The configuration file location. + */ + public File getLocation() + { + return location; + } + + + /** + * Loads configuration data from the configuration location and returns + * it as Dictionary objects. + *

      + * This method is a lazy implementation, which is just one configuration + * file ahead of the current enumeration location. + * + * @return an enumeration of configuration data returned as instances of + * the Dictionary class. + */ + @SuppressWarnings("rawtypes") + @Override + public Enumeration getDictionaries() + { + return new DictionaryEnumeration(); + } + + + /** + * Deletes the file for the given identifier. + * + * @param pid The identifier of the configuration file to delete. + */ + @Override + public void delete( final String pid ) + { + if ( System.getSecurityManager() != null ) + { + _privilegedDelete( pid ); + } + else + { + _delete( pid ); + } + } + + + private void _privilegedDelete( final String pid ) + { + AccessController.doPrivileged( new PrivilegedAction() + { + @Override + public Object run() + { + _delete( pid ); + return null; + } + }, acc ); + } + + + private void _delete( final String pid ) + { + synchronized ( this ) + { + getFile( pid ).delete(); + } + } + + + /** + * Returns true if a (configuration) file exists for the given + * identifier. + * + * @param pid The identifier of the configuration file to check. + * + * @return true if the file exists + */ + @Override + public boolean exists( final String pid ) + { + if ( System.getSecurityManager() != null ) + { + return _privilegedExists( pid ); + } + + return _exists( pid ); + } + + + private boolean _privilegedExists( final String pid ) + { + final Object result = AccessController.doPrivileged( new PrivilegedAction() + { + @Override + public Boolean run() + { + // FELIX-2771: Boolean.valueOf(boolean) is not in Foundation + return _exists( pid ) ? Boolean.TRUE : Boolean.FALSE; + } + } ); + return ( ( Boolean ) result ).booleanValue(); + } + + + private boolean _exists( final String pid ) + { + synchronized ( this ) + { + return getFile( pid ).isFile(); + } + } + + + /** + * Reads the (configuration) for the given identifier into a + * Dictionary object. + * + * @param pid The identifier of the configuration file to delete. + * + * @return The configuration read from the file. This Dictionary + * may be empty if the file contains no configuration information + * or is not properly formatted. + */ + @SuppressWarnings("rawtypes") + @Override + public Dictionary load( String pid ) throws IOException + { + final File cfgFile = getFile( pid ); + + if ( System.getSecurityManager() != null ) + { + return _privilegedLoad( cfgFile ); + } + + return _load( cfgFile ); + } + + + @SuppressWarnings("rawtypes") + private Dictionary _privilegedLoad( final File cfgFile ) throws IOException + { + try + { + Dictionary result = AccessController.doPrivileged( new PrivilegedExceptionAction() + { + @Override + public Dictionary run() throws IOException + { + return _load( cfgFile ); + } + } ); + + return result; + } + catch ( PrivilegedActionException pae ) + { + // FELIX-2771: getCause() is not available in Foundation + throw ( IOException ) pae.getException(); + } + } + + + /** + * Loads the contents of the cfgFile into a new + * Dictionary object. + * + * @param cfgFile + * The file from which to load the data. + * @return A new Dictionary object providing the file contents. + * @throws java.io.FileNotFoundException + * If the given file does not exist. + * @throws IOException + * If an error occurrs reading the configuration file. + */ + @SuppressWarnings("rawtypes") + Dictionary _load( File cfgFile ) throws IOException + { + // this method is not part of the API of this class but is made + // package private to prevent the creation of a synthetic method + // for use by the DictionaryEnumeration._seek method + + // synchronize this instance to make at least sure, the file is + // not at the same time accessed by another thread (see store()) + // we have to synchronize the complete load time as the store + // method might want to replace the file while we are reading and + // still have the file open. This might be a problem e.g. in Windows + // environments, where files may not be removed which are still open + synchronized ( this ) + { + InputStream ins = null; + try + { + ins = new FileInputStream( cfgFile ); + return ConfigurationHandler.read( ins ); + } + finally + { + if ( ins != null ) + { + try + { + ins.close(); + } + catch ( IOException ioe ) + { + // ignore + } + } + } + } + } + + + /** + * Stores the contents of the Dictionary in a file denoted + * by the given identifier. + * + * @param pid The identifier of the configuration file to which to write + * the configuration contents. + * @param props The configuration data to write. + * + * @throws IOException If an error occurrs writing the configuration data. + */ + @SuppressWarnings("rawtypes") + @Override + public void store( final String pid, final Dictionary props ) throws IOException + { + if ( System.getSecurityManager() != null ) + { + _privilegedStore( pid, props ); + } + else + { + _store( pid, props ); + } + } + + + @SuppressWarnings("rawtypes") + private void _privilegedStore( final String pid, final Dictionary props ) throws IOException + { + try + { + AccessController.doPrivileged( new PrivilegedExceptionAction() + { + @Override + public Object run() throws IOException + { + _store( pid, props ); + return null; + } + } ); + } + catch ( PrivilegedActionException pae ) + { + // FELIX-2771: getCause() is not available in Foundation + throw ( IOException ) pae.getException(); + } + } + + + @SuppressWarnings("rawtypes") + private void _store( final String pid, final Dictionary props ) throws IOException + { + OutputStream out = null; + File tmpFile = null; + try + { + File cfgFile = getFile( pid ); + + // ensure parent path + File cfgDir = cfgFile.getParentFile(); + cfgDir.mkdirs(); + + // write the configuration to a temporary file + tmpFile = File.createTempFile( cfgFile.getName(), TMP_EXT, cfgDir ); + out = new FileOutputStream( tmpFile ); + ConfigurationHandler.write( out, props ); + out.close(); + + // after writing the file, rename it but ensure, that no other + // might at the same time open the new file + // see load(File) + synchronized ( this ) + { + // make sure the cfg file does not exists (just for sanity) + if ( cfgFile.exists() ) + { + // FELIX-4165: detect failure to delete old file + if ( !cfgFile.delete() ) + { + throw new IOException( "Cannot remove old file '" + cfgFile + "'; changes in '" + tmpFile + + "' cannot be persisted at this time" ); + } + } + + // rename the temporary file to the new file + if ( !tmpFile.renameTo( cfgFile ) ) + { + throw new IOException( "Failed to rename configuration file from '" + tmpFile + "' to '" + cfgFile ); + } + } + } + finally + { + if ( out != null ) + { + try + { + out.close(); + } + catch ( IOException ioe ) + { + // ignore + } + } + + if (tmpFile != null && tmpFile.exists()) + { + tmpFile.delete(); + } + } + } + + + /** + * Creates an abstract path name for the pid encoding it as + * follows: + *
        + *
      • Dots (.) are replaced by File.separatorChar + *
      • Characters not matching [a-zA-Z0-9 _-] are encoded with a percent + * character (%) and a 4-place hexadecimal unicode value. + *
      + * Before returning the path name, the parent directory and any ancestors + * are created. + * + * @param pid The identifier for which to create the abstract file name. + * + * @return The abstract path name, which the parent directory path created. + */ + File getFile( String pid ) + { + // this method is not part of the API of this class but is made + // package private to prevent the creation of a synthetic method + // for use by the DictionaryEnumeration._seek method + + return new File( location, encodePid( pid ) + FILE_EXT ); + } + + /** + * The DictionaryEnumeration class implements the + * Enumeration returning configuration Dictionary + * objects on behalf of the {@link FilePersistenceManager#getDictionaries()} + * method. + *

      + * This enumeration loads configuration lazily with a look ahead of one + * dictionary. + */ + @SuppressWarnings("rawtypes") + class DictionaryEnumeration implements Enumeration + { + private Stack dirStack; + private File[] fileList; + private int idx; + private Dictionary next; + + + DictionaryEnumeration() + { + dirStack = new Stack<>(); + fileList = null; + idx = 0; + + dirStack.push( getLocation() ); + next = seek(); + } + + + @Override + public boolean hasMoreElements() + { + return next != null; + } + + + @Override + public Object nextElement() + { + if ( next == null ) + { + throw new NoSuchElementException(); + } + + Dictionary toReturn = next; + next = seek(); + return toReturn; + } + + + private Dictionary seek() + { + if ( System.getSecurityManager() != null ) + { + return _privilegedSeek(); + } + + return _seek(); + } + + + protected Dictionary _privilegedSeek() + { + Dictionary result = AccessController.doPrivileged( new PrivilegedAction() + { + @Override + public Dictionary run() + { + return _seek(); + } + } ); + return result; + } + + + protected Dictionary _seek() + { + while ( ( fileList != null && idx < fileList.length ) || !dirStack.isEmpty() ) + { + if ( fileList == null || idx >= fileList.length ) + { + File dir = dirStack.pop(); + fileList = dir.listFiles(); + idx = 0; + } + else + { + + File cfgFile = fileList[idx++]; + if ( cfgFile.isFile() && !cfgFile.getName().endsWith( TMP_EXT )) + { + try + { + Dictionary dict = _load( cfgFile ); + + // use the dictionary if it has no PID or the PID + // derived file name matches the source file name + if ( dict.get( Constants.SERVICE_PID ) == null + || cfgFile.equals( getFile( ( String ) dict.get( Constants.SERVICE_PID ) ) ) ) + { + return dict; + } + } + catch ( IOException ioe ) + { + // ignore, check next file + } + } + else if ( cfgFile.isDirectory() ) + { + dirStack.push( cfgFile ); + } + } + } + + // exhausted + return null; + } + } + +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/file/package-info.java b/configadmin/src/main/java/org/apache/felix/cm/file/package-info.java new file mode 100644 index 00000000000..ca361f87899 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/file/package-info.java @@ -0,0 +1,25 @@ +/* + * 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. + */ + +@org.osgi.annotation.versioning.Version("1.1.0") +package org.apache.felix.cm.file; + + + + diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/Activator.java b/configadmin/src/main/java/org/apache/felix/cm/impl/Activator.java new file mode 100644 index 00000000000..060ea37ff0d --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/Activator.java @@ -0,0 +1,179 @@ +/* + * 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.cm.impl; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.apache.felix.cm.PersistenceManager; +import org.apache.felix.cm.file.FilePersistenceManager; +import org.apache.felix.cm.impl.persistence.PersistenceManagerTracker; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.log.LogService; + +/** + * Activator for the configuration admin implementation. + * When the bundle is started this activator: + *

        + *
      • Sets up the logger {@link Log}. + *
      • A {@link FilePersistenceManager} instance is registered as a default + * {@link PersistenceManager}. + *
      • Creates and sets up the {@link ConfigurationManager}. + *
      + *

      + * The default {@link FilePersistenceManager} is configured with a configuration + * location taken from the felix.cm.dir framework property. If + * this property is not set the config directory in the current + * working directory as specified in the user.dir system property + * is used. + */ +public class Activator implements BundleActivator +{ + + /** + * The name of the framework context property defining the location for the + * configuration files (value is "felix.cm.dir"). + * + * @see #start(BundleContext) + */ + private static final String CM_CONFIG_DIR = "felix.cm.dir"; + + /** + * The name of the framework context property defining the persistence + * manager to be used. If this property is not set or empty, the built-in + * persistence manager (named file) is used. If it is specified it refers + * to the name property of a persistence manager and that persistence manager + * needs to be registered. + * + * @see #start(BundleContext) + */ + private static final String CM_CONFIG_PM = "felix.cm.pm"; + + private volatile PersistenceManagerTracker tracker; + + // the service registration of the default file persistence manager + private volatile ServiceRegistration filepmRegistration; + + @Override + public void start( final BundleContext bundleContext ) throws BundleException + { + // setup log + Log.logger.start(bundleContext); + + // register default file persistence manager + final PersistenceManager defaultPM = this.registerFilePersistenceManager(bundleContext); + if ( defaultPM == null ) + { + throw new BundleException("Unable to register default persistence manager."); + } + + String configuredPM = bundleContext.getProperty(CM_CONFIG_PM); + if ( configuredPM != null && configuredPM.isEmpty() ) + { + configuredPM = null; + } + try + { + this.tracker = new PersistenceManagerTracker(bundleContext, defaultPM, configuredPM); + } + catch ( InvalidSyntaxException iae ) + { + Log.logger.log( LogService.LOG_ERROR, "Cannot create the persistence manager tracker", iae ); + throw new BundleException(iae.getMessage(), iae); + } + } + + + @Override + public void stop( final BundleContext bundleContext ) + { + // stop logger + Log.logger.stop(); + + // stop tracker and configuration manager implementation + if ( this.tracker != null ) + { + this.tracker.stop(); + this.tracker = null; + } + + // shutdown the file persistence manager and unregister + this.unregisterFilePersistenceManager(); + } + + private PersistenceManager registerFilePersistenceManager(final BundleContext bundleContext) + { + try + { + final FilePersistenceManager fpm = new FilePersistenceManager( bundleContext, + bundleContext.getProperty( CM_CONFIG_DIR ) ); + final Dictionary props = new Hashtable<>(); + props.put( Constants.SERVICE_DESCRIPTION, "Platform Filesystem Persistence Manager" ); + props.put( Constants.SERVICE_VENDOR, "The Apache Software Foundation" ); + props.put( Constants.SERVICE_RANKING, new Integer( Integer.MIN_VALUE ) ); + props.put( PersistenceManager.PROPERTY_NAME, FilePersistenceManager.DEFAULT_PERSISTENCE_MANAGER_NAME); + filepmRegistration = bundleContext.registerService( PersistenceManager.class, fpm, props ); + + return fpm; + + } + catch ( final IllegalArgumentException iae ) + { + Log.logger.log( LogService.LOG_ERROR, "Cannot create the FilePersistenceManager", iae ); + } + return null; + } + + private void unregisterFilePersistenceManager() + { + if ( this.filepmRegistration != null ) + { + this.filepmRegistration.unregister(); + this.filepmRegistration = null; + } + } + + public static String getLocation(final Bundle bundle) + { + if (System.getSecurityManager() != null) + { + return AccessController.doPrivileged(new PrivilegedAction() + { + @Override + public String run() + { + return bundle.getLocation(); + } + }); + } + else + { + return bundle.getLocation(); + } + } +} + diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/CaseInsensitiveDictionary.java b/configadmin/src/main/java/org/apache/felix/cm/impl/CaseInsensitiveDictionary.java new file mode 100644 index 00000000000..c6c07f3eea7 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/CaseInsensitiveDictionary.java @@ -0,0 +1,548 @@ +/* + * 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.cm.impl; + + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.Vector; + + +/** + * The CaseInsensitiveDictionary is a + * java.util.Dictionary which conforms to the requirements laid + * out by the Configuration Admin Service Specification requiring the property + * names to keep case but to ignore case when accessing the properties. + */ +public class CaseInsensitiveDictionary extends Dictionary +{ + + /** + * The backend dictionary with lower case keys. + */ + private SortedMap internalMap; + + public CaseInsensitiveDictionary() + { + internalMap = new TreeMap<>( CASE_INSENSITIVE_ORDER ); + } + + + public CaseInsensitiveDictionary( Dictionary props ) + { + if ( props instanceof CaseInsensitiveDictionary) + { + internalMap = new TreeMap<>( ((CaseInsensitiveDictionary) props).internalMap ); + } + else if ( props != null ) + { + internalMap = new TreeMap<>( CASE_INSENSITIVE_ORDER ); + Enumeration keys = props.keys(); + while ( keys.hasMoreElements() ) + { + Object key = keys.nextElement(); + + // check the correct syntax of the key + String k = checkKey( key ); + + // check uniqueness of key + if ( internalMap.containsKey( k ) ) + { + throw new IllegalArgumentException( "Key [" + key + "] already present in different case" ); + } + + // check the value + Object value = props.get( key ); + value = checkValue( value ); + + // add the key/value pair + internalMap.put( k, value ); + } + } + else + { + internalMap = new TreeMap<>( CASE_INSENSITIVE_ORDER ); + } + } + + + CaseInsensitiveDictionary( CaseInsensitiveDictionary props, boolean deepCopy ) + { + if ( deepCopy ) + { + internalMap = new TreeMap<>( CASE_INSENSITIVE_ORDER ); + for( Map.Entry entry : props.internalMap.entrySet() ) + { + Object value = entry.getValue(); + if ( value.getClass().isArray() ) + { + // copy array + int length = Array.getLength( value ); + Object newValue = Array.newInstance( value.getClass().getComponentType(), length ); + System.arraycopy( value, 0, newValue, 0, length ); + value = newValue; + } + else if ( value instanceof Collection ) + { + // copy collection, create Vector + // a Vector is created because the R4 and R4.1 specs + // state that the values must be simple, array or + // Vector. And even though we accept Collection nowadays + // there might be clients out there still written against + // R4 and R4.1 spec expecting Vector + value = new Vector<>( ( Collection ) value ); + } + internalMap.put( entry.getKey(), value ); + } + } + else + { + internalMap = new TreeMap<>( props.internalMap ); + } + } + + + /* + * (non-Javadoc) + * + * @see java.util.Dictionary#elements() + */ + @Override + public Enumeration elements() + { + return Collections.enumeration( internalMap.values() ); + } + + + /* + * (non-Javadoc) + * + * @see java.util.Dictionary#get(java.lang.Object) + */ + @Override + public Object get( Object key ) + { + if ( key == null ) + { + throw new NullPointerException( "key" ); + } + + return internalMap.get( key ); + } + + + /* + * (non-Javadoc) + * + * @see java.util.Dictionary#isEmpty() + */ + @Override + public boolean isEmpty() + { + return internalMap.isEmpty(); + } + + + /* + * (non-Javadoc) + * + * @see java.util.Dictionary#keys() + */ + @Override + public Enumeration keys() + { + return Collections.enumeration( internalMap.keySet() ); + } + + + /* + * (non-Javadoc) + * + * @see java.util.Dictionary#put(java.lang.String, java.lang.Object) + */ + @Override + public Object put( String key, Object value ) + { + if ( key == null || value == null ) + { + throw new NullPointerException( "key or value" ); + } + + checkKey( key ); + value = checkValue( value ); + + return internalMap.put( key, value ); + } + + + /* + * (non-Javadoc) + * + * @see java.util.Dictionary#remove(java.lang.Object) + */ + @Override + public Object remove( Object key ) + { + if ( key == null ) + { + throw new NullPointerException( "key" ); + } + + return internalMap.remove( key ); + } + + + /* + * (non-Javadoc) + * + * @see java.util.Dictionary#size() + */ + @Override + public int size() + { + return internalMap.size(); + } + + + // ---------- internal ----------------------------------------------------- + + /** + * Ensures the key complies with the symbolic-name + * production of the OSGi core specification (1.3.2): + * + *
      +     * symbolic-name :: = token('.'token)*
      +     * digit    ::= [0..9]
      +     * alpha    ::= [a..zA..Z]
      +     * alphanum ::= alpha | digit
      +     * token    ::= ( alphanum | ’_’ | ’-’ )+
      +     * 
      + * + * If the key does not comply an IllegalArgumentException is + * thrown. + * + * @param keyObject + * The configuration property key to check. + * @throws IllegalArgumentException + * if the key does not comply with the symbolic-name production. + */ + static String checkKey( Object keyObject ) + { + // check for wrong type or null key + if ( !( keyObject instanceof String ) ) + { + throw new IllegalArgumentException( "Key [" + keyObject + "] must be a String" ); + } + + String key = ( String ) keyObject; + + // check for empty string + if ( key.length() == 0 ) + { + throw new IllegalArgumentException( "Key [" + key + "] must not be an empty string" ); + } + + return key; + } + + + private static final Set KNOWN = new HashSet<>(Arrays.asList( + String.class, Integer.class, Long.class, Float.class, + Double.class, Byte.class, Short.class, Character.class, + Boolean.class)); + + static Object checkValue( Object value ) + { + if ( value == null ) + { + // null is illegal + throw new IllegalArgumentException( "Value must not be null" ); + + } + + Class type = value.getClass(); + // Fast check for simple types + if ( KNOWN.contains( type ) ) + { + return value; + } + else if ( type.isArray() ) + { + // check simple or primitive + type = value.getClass().getComponentType(); + + // check for primitive type (simple types are checked below) + // note: void[] cannot be created, so we ignore this here + if ( type.isPrimitive() ) + { + return value; + } + + } + else if ( value instanceof Collection ) + { + // check simple + Collection collection = ( Collection ) value; + if ( collection.isEmpty() ) + { + return Collections.EMPTY_LIST; + } + else + { + // ensure all elements have the same type and to internal list + Collection internalValue = new ArrayList<>( collection.size() ); + type = null; + for ( Object el : collection ) + { + if ( el == null ) + { + throw new IllegalArgumentException( "Collection must not contain null elements" ); + } + if ( type == null ) + { + type = el.getClass(); + } + else if ( type != el.getClass() ) + { + throw new IllegalArgumentException( "Collection element types must not be mixed" ); + } + internalValue.add( el ); + } + value = internalValue; + } + } + else + { + // get the type to check (must be simple) + type = value.getClass(); + + } + + // check for simple type + if ( KNOWN.contains( type ) ) + { + return value; + } + + // not a valid type + throw new IllegalArgumentException( "Value [" + value + "] has unsupported (base-) type " + type ); + } + + + // ---------- Object Overwrites -------------------------------------------- + + @Override + public String toString() + { + return internalMap.toString(); + } + + @Override + public int hashCode() + { + return internalMap.hashCode(); + } + + @Override + public synchronized boolean equals(final Object o) + { + if (o == this) + { + return true; + } + + if (!(o instanceof Dictionary)) + { + return false; + } + + @SuppressWarnings("unchecked") + final Dictionary t = (Dictionary) o; + if (t.size() != size()) + { + return false; + } + + try + { + final Enumeration keys = keys(); + while ( keys.hasMoreElements() ) + { + final String key = keys.nextElement(); + final Object value = get(key); + + if (!value.equals(t.get(key))) + { + return false; + } + } + } + catch (ClassCastException unused) + { + return false; + } + catch (NullPointerException unused) + { + return false; + } + + return true; + } + + public static Dictionary unmodifiable(Dictionary dict) { + return new UnmodifiableDictionary(dict); + } + + public static final class UnmodifiableDictionary extends Dictionary + { + private final Dictionary delegatee; + + public UnmodifiableDictionary(final Dictionary delegatee) + { + this.delegatee = delegatee; + } + + @Override + public Object put(String key, Object value) + { + // prevent put + return null; + } + + @Override + public Object remove(Object key) + { + // prevent remove + return null; + } + + @Override + public int hashCode() + { + return delegatee.hashCode(); + } + + @Override + public boolean equals(Object obj) + { + return delegatee.equals(obj); + } + + @Override + public String toString() + { + return delegatee.toString(); + } + + @Override + public int size() + { + return delegatee.size(); + } + + @Override + public boolean isEmpty() + { + return delegatee.isEmpty(); + } + + @Override + public Enumeration keys() + { + return delegatee.keys(); + } + + @Override + public Enumeration elements() + { + return delegatee.elements(); + } + + @Override + public Object get(Object key) + { + return delegatee.get(key); + } + } + + public static final Comparator CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator(); + + private static class CaseInsensitiveComparator implements Comparator + { + + @Override + public int compare(String s1, String s2) + { + int n1 = s1.length(); + int n2 = s2.length(); + int min = n1 < n2 ? n1 : n2; + for ( int i = 0; i < min; i++ ) + { + char c1 = s1.charAt( i ); + char c2 = s2.charAt( i ); + if ( c1 != c2 ) + { + // Fast check for simple ascii codes + if ( c1 <= 128 && c2 <= 128 ) + { + c1 = toLowerCaseFast(c1); + c2 = toLowerCaseFast(c2); + if ( c1 != c2 ) + { + return c1 - c2; + } + } + else + { + c1 = Character.toUpperCase( c1 ); + c2 = Character.toUpperCase( c2 ); + if ( c1 != c2 ) + { + c1 = Character.toLowerCase( c1 ); + c2 = Character.toLowerCase( c2 ); + if ( c1 != c2 ) + { + // No overflow because of numeric promotion + return c1 - c2; + } + } + } + } + } + return n1 - n2; + } + } + + private static char toLowerCaseFast( char ch ) + { + return ( ch >= 'A' && ch <= 'Z' ) ? ( char ) ( ch + 'a' - 'A' ) : ch; + } + +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationAdapter.java b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationAdapter.java new file mode 100644 index 00000000000..7f974852a29 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationAdapter.java @@ -0,0 +1,372 @@ +/* + * 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.cm.impl; + + +import java.io.IOException; +import java.util.Dictionary; +import java.util.EnumSet; +import java.util.Set; + +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.ConfigurationPermission; +import org.osgi.service.cm.ReadOnlyConfigurationException; +import org.osgi.service.log.LogService; + + +/** + * The ConfigurationAdapter is just an adapter to the internal + * configuration object. Instances of this class are returned as Configuration + * objects to the client, where each caller gets a fresh instance of this + * class while internal Configuration objects are shared. + */ +public class ConfigurationAdapter implements Configuration +{ + + private final ConfigurationAdminImpl configurationAdmin; + private final ConfigurationImpl delegatee; + + + ConfigurationAdapter( ConfigurationAdminImpl configurationAdmin, ConfigurationImpl delegatee ) + { + this.configurationAdmin = configurationAdmin; + this.delegatee = delegatee; + } + + + /** + * @see org.apache.felix.cm.impl.ConfigurationImpl#getPid() + */ + @Override + public String getPid() + { + checkDeleted(); + return delegatee.getPidString(); + } + + + /** + * @see org.apache.felix.cm.impl.ConfigurationImpl#getFactoryPid() + */ + @Override + public String getFactoryPid() + { + checkDeleted(); + return delegatee.getFactoryPidString(); + } + + + /** + * @see org.apache.felix.cm.impl.ConfigurationImpl#getBundleLocation() + */ + @Override + public String getBundleLocation() + { + // CM 1.4 / 104.13.2.4 + final String bundleLocation = delegatee.getBundleLocation(); + //delegatee.getConfigurationManager().log( LogService.LOG_DEBUG, "getBundleLocation() ==> {0}", new Object[] + // { bundleLocation } ); + checkActive(); + configurationAdmin.checkPermission( delegatee.getConfigurationManager(), ( bundleLocation == null ) ? "*" : bundleLocation, true ); + checkDeleted(); + return bundleLocation; + } + + + /** + * @param bundleLocation + * @see org.apache.felix.cm.impl.ConfigurationImpl#setStaticBundleLocation(String) + */ + @Override + public void setBundleLocation( String bundleLocation ) + { + Log.logger.log( LogService.LOG_DEBUG, "setBundleLocation(bundleLocation={0})", + new Object[] + { bundleLocation } ); + + // CM 1.4 / 104.13.2.4 + checkActive(); + final String configLocation = delegatee.getBundleLocation(); + configurationAdmin.checkPermission( delegatee.getConfigurationManager(), ( configLocation == null ) ? "*" : configLocation, true ); + configurationAdmin.checkPermission( delegatee.getConfigurationManager(), ( bundleLocation == null ) ? "*" : bundleLocation, true ); + checkDeleted(); + delegatee.setStaticBundleLocation( bundleLocation ); + } + + + /** + * @throws IOException + * @see org.apache.felix.cm.impl.ConfigurationImpl#update() + */ + @Override + public void update() throws IOException + { + Log.logger.log( LogService.LOG_DEBUG, "update()", ( Throwable ) null ); + + checkActive(); + checkDeleted(); + delegatee.update(); + } + + + /** + * @param properties + * @throws IOException + * @see org.apache.felix.cm.impl.ConfigurationImpl#update(java.util.Dictionary) + */ + @Override + public void update( Dictionary properties ) throws IOException + { + Log.logger.log( LogService.LOG_DEBUG, "update(properties={0})", new Object[] + { properties } ); + + checkActive(); + checkDeleted(); + checkLocked(); + delegatee.update( properties ); + } + + + @Override + public Dictionary getProperties() + { + //delegatee.getConfigurationManager().log( LogService.LOG_DEBUG, "getProperties()", ( Throwable ) null ); + + checkDeleted(); + + // return a deep copy since the spec says, that modification of + // any value should not modify the internal, stored value + return delegatee.getProperties( true ); + } + + + @Override + public long getChangeCount() + { + //delegatee.getConfigurationManager().log( LogService.LOG_DEBUG, "getChangeCount()", ( Throwable ) null ); + + checkDeleted(); + + return delegatee.getRevision(); + } + + + /** + * @throws IOException + * @see org.apache.felix.cm.impl.ConfigurationImpl#delete() + */ + @Override + public void delete() throws IOException + { + Log.logger.log( LogService.LOG_DEBUG, "delete()", ( Throwable ) null ); + + checkActive(); + checkDeleted(); + delegatee.delete(); + } + + + /** + * @see org.osgi.service.cm.Configuration#updateIfDifferent(java.util.Dictionary) + */ + @SuppressWarnings("unchecked") + @Override + public boolean updateIfDifferent(final Dictionary properties) throws IOException + { + Log.logger.log( LogService.LOG_DEBUG, "updateIfDifferent(properties={0})", new Object[] + { properties } ); + + checkActive(); + checkDeleted(); + checkLocked(); + + if ( !ConfigurationImpl.equals((Dictionary)properties, delegatee.getProperties(false)) ) + { + delegatee.update( properties ); + return true; + } + return false; + } + + + /** + * @see org.osgi.service.cm.Configuration#addAttributes(org.osgi.service.cm.Configuration.ConfigurationAttribute[]) + */ + @Override + public void addAttributes(final ConfigurationAttribute... attrs) throws IOException + { + checkDeleted(); + final String bundleLocation = delegatee.getBundleLocation(); + this.configurationAdmin.checkPermission(this.delegatee.getConfigurationManager(), + ( bundleLocation == null ) ? "*" : bundleLocation, + ConfigurationPermission.ATTRIBUTE, + false); + + Log.logger.log( LogService.LOG_DEBUG, "addAttributes({0})", attrs ); + + if ( attrs != null ) + { + + for(ConfigurationAttribute ca : attrs) + { + // locked is the only attribute at the moment + if ( ca == ConfigurationAttribute.READ_ONLY ) { + + delegatee.setLocked( true ); + } + } + } + } + + + /** + * @see org.osgi.service.cm.Configuration#getAttributes() + */ + @Override + public Set getAttributes() + { + checkDeleted(); + if ( delegatee.isLocked() ) + { + return EnumSet.of(ConfigurationAttribute.READ_ONLY); + } + return EnumSet.noneOf(ConfigurationAttribute.class); + } + + + /** + * @see org.osgi.service.cm.Configuration#removeAttributes(org.osgi.service.cm.Configuration.ConfigurationAttribute[]) + */ + @Override + public void removeAttributes(final ConfigurationAttribute... attrs) throws IOException + { + checkDeleted(); + final String bundleLocation = delegatee.getBundleLocation(); + this.configurationAdmin.checkPermission(this.delegatee.getConfigurationManager(), + ( bundleLocation == null ) ? "*" : bundleLocation, + ConfigurationPermission.ATTRIBUTE, + false); + + Log.logger.log( LogService.LOG_DEBUG, "removeAttributes({0})", attrs ); + + if ( attrs != null ) + { + for(ConfigurationAttribute ca : attrs) + { + // locked is the only attribute at the moment + if ( ca == ConfigurationAttribute.READ_ONLY ) { + + delegatee.setLocked( false ); + } + } + } + } + + /** + * @see org.osgi.service.cm.Configuration#getProcessedProperties(ServiceReference) + */ + @Override + public Dictionary getProcessedProperties(ServiceReference sr) + { + final Dictionary props = this.getProperties(); + + this.delegatee.getConfigurationManager().callPlugins(props, sr, + (String)props.get(Constants.SERVICE_PID), + (String)props.get(ConfigurationAdmin.SERVICE_FACTORYPID)); + + return props; + } + + /** + * @see org.apache.felix.cm.impl.ConfigurationImpl#hashCode() + */ + @Override + public int hashCode() + { + return delegatee.hashCode(); + } + + + /** + * @param obj + * @see org.apache.felix.cm.impl.ConfigurationImpl#equals(java.lang.Object) + */ + @Override + public boolean equals( Object obj ) + { + return delegatee.equals( obj ); + } + + + /** + * @see org.apache.felix.cm.impl.ConfigurationImpl#toString() + */ + @Override + public String toString() + { + return delegatee.toString(); + } + + /** + * Checks whether this configuration object is backed by an active + * Configuration Admin Service (ConfigurationManager here). + * + * @throws IllegalStateException If this configuration object is not + * backed by an active ConfigurationManager + */ + private void checkActive() + { + if ( !delegatee.isActive() ) + { + throw new IllegalStateException( "Configuration " + delegatee.getPid() + + " not backed by an active Configuration Admin Service" ); + } + } + + + /** + * Checks whether this configuration object has already been deleted. + * + * @throws IllegalStateException If this configuration object has been + * deleted. + */ + private void checkDeleted() + { + if ( delegatee.isDeleted() ) + { + throw new IllegalStateException( "Configuration " + delegatee.getPid() + " deleted" ); + } + } + + /** + * Checks whether this configuration object is locked. + * + * @throws ReadOnlyConfigurationException If this configuration object is locked. + */ + private void checkLocked() throws IOException + { + if ( delegatee.isLocked() ) + { + throw new ReadOnlyConfigurationException( "Configuration " + delegatee.getPid() + " is read-only" ); + } + } +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationAdminFactory.java b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationAdminFactory.java new file mode 100644 index 00000000000..679499d18ca --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationAdminFactory.java @@ -0,0 +1,69 @@ +/* + * 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.cm.impl; + + +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.ConfigurationAdmin; + + +/** + * The ConfigurationAdminFactory is the ServiceFactory + * registered as the ConfigurationAdmin service responsible to + * create the real ConfiguratAdmin instances returend to client + * bundles. Each bundle gets a separate instance. + */ +class ConfigurationAdminFactory implements ServiceFactory +{ + + // The configuration manager to which the configuration admin instances + // delegate most of their work + private final ConfigurationManager configurationManager; + + + ConfigurationAdminFactory( final ConfigurationManager configurationManager ) + { + this.configurationManager = configurationManager; + } + + + /** + * Returns a new instance of the {@link ConfigurationAdminImpl} class for + * the given bundle. + */ + @Override + public ConfigurationAdmin getService( final Bundle bundle, final ServiceRegistration registration ) + { + return new ConfigurationAdminImpl( configurationManager, bundle ); + } + + + /** + * Disposes off the given {@link ConfigurationAdminImpl} instance as the + * given bundle has no use of it any more. + */ + @Override + public void ungetService( final Bundle bundle, final ServiceRegistration registration, final ConfigurationAdmin service ) + { + ( ( ConfigurationAdminImpl ) service ).dispose(); + } + +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationAdminImpl.java b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationAdminImpl.java new file mode 100644 index 00000000000..caca06bde4c --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationAdminImpl.java @@ -0,0 +1,405 @@ +/* + * 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.cm.impl; + + +import java.io.IOException; + +import org.osgi.framework.Bundle; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.ConfigurationPermission; +import org.osgi.service.log.LogService; + + +/** + * The ConfigurationAdminImpl is the per-bundle frontend to the + * configuration manager. Instances of this class are created on-demand for + * each bundle trying to get hold of the ConfigurationAdmin + * service. + */ +public class ConfigurationAdminImpl implements ConfigurationAdmin +{ + + // The configuration manager to which most of the tasks are delegated + private volatile ConfigurationManager configurationManager; + + // The bundle for which this instance has been created + private volatile Bundle bundle; + + + ConfigurationAdminImpl( ConfigurationManager configurationManager, Bundle bundle ) + { + this.configurationManager = configurationManager; + this.bundle = bundle; + } + + + void dispose() + { + bundle = null; + configurationManager = null; + } + + + Bundle getBundle() + { + return bundle; + } + + + //---------- ConfigurationAdmin interface --------------------------------- + + /* (non-Javadoc) + * @see org.osgi.service.cm.ConfigurationAdmin#createFactoryConfiguration(java.lang.String) + */ + @Override + public Configuration createFactoryConfiguration( String factoryPid ) throws IOException + { + final ConfigurationManager configurationManager = getConfigurationManager(); + + Log.logger.log( LogService.LOG_DEBUG, "createFactoryConfiguration(factoryPid={0})", new Object[] + { factoryPid } ); + + // FELIX-3360: new factory configuration with implicit binding is dynamic + ConfigurationImpl config = configurationManager.createFactoryConfiguration( factoryPid, null ); + config.setDynamicBundleLocation( Activator.getLocation(this.getBundle()), false ); + return this.wrap( config ); + } + + + /* (non-Javadoc) + * @see org.osgi.service.cm.ConfigurationAdmin#createFactoryConfiguration(java.lang.String, java.lang.String) + */ + @Override + public Configuration createFactoryConfiguration( String factoryPid, String location ) throws IOException + { + final ConfigurationManager configurationManager = getConfigurationManager(); + + Log.logger.log( LogService.LOG_DEBUG, "createFactoryConfiguration(factoryPid={0}, location={1})", + new Object[] + { factoryPid, location } ); + + // CM 1.4 / 104.13.2.3 + this.checkPermission( configurationManager, ( location == null ) ? "*" : location, false ); + + ConfigurationImpl config = configurationManager.createFactoryConfiguration( factoryPid, location ); + return this.wrap( config ); + } + + + /* (non-Javadoc) + * @see org.osgi.service.cm.ConfigurationAdmin#getConfiguration(java.lang.String) + */ + @Override + public Configuration getConfiguration( String pid ) throws IOException + { + final ConfigurationManager configurationManager = getConfigurationManager(); + + Log.logger.log( LogService.LOG_DEBUG, "getConfiguration(pid={0})", new Object[] + { pid } ); + + ConfigurationImpl config = configurationManager.getConfiguration( pid ); + if ( config == null ) + { + config = configurationManager.createConfiguration( pid, null ); + + // FELIX-3360: configuration creation with implicit binding is dynamic + config.setDynamicBundleLocation( Activator.getLocation(getBundle()), false ); + } + else + { + if ( config.getBundleLocation() == null ) + { + Log.logger.log( LogService.LOG_DEBUG, "Binding configuration {0} (isNew: {1}) to bundle {2}", + new Object[] + { config.getPid(), config.isNew() ? Boolean.TRUE : Boolean.FALSE, + Activator.getLocation(this.getBundle()) } ); + + // FELIX-3360: first implicit binding is dynamic + config.setDynamicBundleLocation( Activator.getLocation(getBundle()), true ); + } + else + { + // CM 1.4 / 104.13.2.3 + this.checkPermission( configurationManager, config.getBundleLocation(), false ); + } + } + + return this.wrap( config ); + } + + + /* (non-Javadoc) + * @see org.osgi.service.cm.ConfigurationAdmin#getConfiguration(java.lang.String, java.lang.String) + */ + @Override + public Configuration getConfiguration( String pid, String location ) throws IOException + { + final ConfigurationManager configurationManager = getConfigurationManager(); + + Log.logger.log( LogService.LOG_DEBUG, "getConfiguration(pid={0}, location={1})", new Object[] + { pid, location } ); + + // CM 1.4 / 104.13.2.3 + this.checkPermission( configurationManager, ( location == null ) ? "*" : location, false ); + + ConfigurationImpl config = configurationManager.getConfiguration( pid ); + if ( config == null ) + { + config = configurationManager.createConfiguration( pid, location ); + } + else + { + final String configLocation = config.getBundleLocation(); + this.checkPermission( configurationManager, ( configLocation == null ) ? "*" : configLocation, false ); + } + + return this.wrap( config ); + } + + + /* (non-Javadoc) + * @see org.osgi.service.cm.ConfigurationAdmin#listConfigurations(java.lang.String) + */ + @Override + public Configuration[] listConfigurations( String filter ) throws IOException, InvalidSyntaxException + { + final ConfigurationManager configurationManager = getConfigurationManager(); + + Log.logger.log( LogService.LOG_DEBUG, "listConfigurations(filter={0})", new Object[] + { filter } ); + + ConfigurationImpl ci[] = configurationManager.listConfigurations( this, filter ); + if ( ci == null || ci.length == 0 ) + { + return null; + } + + Configuration[] cfgs = new Configuration[ci.length]; + for ( int i = 0; i < cfgs.length; i++ ) + { + cfgs[i] = this.wrap( ci[i] ); + } + + return cfgs; + } + + + //---------- Security checks ---------------------------------------------- + + private Configuration wrap( ConfigurationImpl configuration ) + { + return new ConfigurationAdapter( this, configuration ); + } + + + /** + * Returns true if the current access control context (call + * stack) has the CONFIGURE permission. + */ + boolean hasPermission( final ConfigurationManager configurationManager, String name ) + { + try + { + checkPermission(configurationManager, name, false); + return true; + } + catch ( SecurityException se ) + { + return false; + } + } + + + /** + * Checks whether the current access control context (call stack) has + * the given permission for the given bundle location and throws a + * SecurityException if this is not the case. + * + * @param name The bundle location to check for permission. If this + * is null permission is always granted. + * @param checkOwn If {@code false} permission is always granted if + * {@code name} is the same the using bundle's location. + * + * @throws SecurityException if the access control context does not + * have the appropriate permission + */ + void checkPermission( final ConfigurationManager configurationManager, String name, boolean checkOwn ) + { + checkPermission(configurationManager, name, ConfigurationPermission.CONFIGURE, checkOwn); + } + + /** + * Checks whether the current access control context (call stack) has + * the given permission for the given bundle location and throws a + * SecurityException if this is not the case. + * + * @param name The bundle location to check for permission. If this + * is null permission is always granted. + * @param action The action to check. + * @param checkOwn If {@code false} permission is always granted if + * {@code name} is the same as the using bundle's location. + * + * @throws SecurityException if the access control context does not + * have the appropriate permission + */ + void checkPermission( final ConfigurationManager configurationManager, String name, String action, boolean checkOwn ) + { + // the caller's permission must be checked + final SecurityManager sm = System.getSecurityManager(); + if ( sm != null ) + { + // CM 1.4 / 104.11.1 Implicit permission + if ( name != null && ( checkOwn || !name.equals( Activator.getLocation(getBundle()) ) ) ) + { + try + { + sm.checkPermission( new ConfigurationPermission( name, action ) ); + + Log.logger.log( LogService.LOG_DEBUG, + "Explicit Permission; grant {0} permission on configuration bound to {1} to bundle {2}", + new Object[] + { action, name, Activator.getLocation(getBundle()) } ); + } + catch ( SecurityException se ) + { + Log.logger + .log( + LogService.LOG_DEBUG, + "No Permission; denied {0} permission on configuration bound to {1} to bundle {2}; reason: {3}", + new Object[] + { action, name, Activator.getLocation(getBundle()), se.getMessage() } ); + throw se; + } + } + else if ( Log.logger.isLogEnabled( LogService.LOG_DEBUG ) ) + { + Log.logger.log( LogService.LOG_DEBUG, + "Implicit Permission; grant {0} permission on configuration bound to {1} to bundle {2}", + new Object[] + { action, name, Activator.getLocation(getBundle()) } ); + + } + } + else if ( Log.logger.isLogEnabled( LogService.LOG_DEBUG ) ) + { + Log.logger.log( LogService.LOG_DEBUG, + "No SecurityManager installed; grant {0} permission on configuration bound to {1} to bundle {2}", + new Object[] + { action, name, Activator.getLocation(getBundle()) } ); + } + } + + + /** + * Returns the {@link ConfigurationManager} backing this configuration + * admin instance or throws {@code IllegalStateException} if already + * disposed off. + * + * @return The {@link ConfigurationManager} instance if still active + * @throws IllegalStateException if this instance has been + * {@linkplain #dispose() disposed off} already. + */ + private ConfigurationManager getConfigurationManager() + { + if ( this.configurationManager == null ) + { + throw new IllegalStateException( "Configuration Admin service has been unregistered" ); + } + + return this.configurationManager; + } + + + /** + * @see org.osgi.service.cm.ConfigurationAdmin#getFactoryConfiguration(java.lang.String, java.lang.String, java.lang.String) + */ + @Override + public Configuration getFactoryConfiguration(String factoryPid, String name, String location) throws IOException + { + final ConfigurationManager configurationManager = getConfigurationManager(); + + Log.logger.log( LogService.LOG_DEBUG, "getFactoryConfiguration(factoryPid={0}, name={1}, location={2})", new Object[] + { factoryPid, name, location } ); + + final String pid = factoryPid + '~' + name; + + // CM 1.4 / 104.13.2.3 + this.checkPermission( configurationManager, ( location == null ) ? "*" : location, false ); + + ConfigurationImpl config = configurationManager.getConfiguration( pid ); + if ( config == null ) + { + config = configurationManager.createFactoryConfiguration( pid, factoryPid, location ); + } + else + { + final String configLocation = config.getBundleLocation(); + this.checkPermission( configurationManager, ( configLocation == null ) ? "*" : configLocation, false ); + } + + return this.wrap( config ); + } + + + /** + * @see org.osgi.service.cm.ConfigurationAdmin#getFactoryConfiguration(java.lang.String, java.lang.String) + */ + @Override + public Configuration getFactoryConfiguration(String factoryPid, String name) throws IOException { + final ConfigurationManager configurationManager = getConfigurationManager(); + + Log.logger.log( LogService.LOG_DEBUG, "getFactoryConfiguration(factoryPid={0}, name={1})", new Object[] + { factoryPid, name } ); + + final String pid = factoryPid + '~' + name; + + ConfigurationImpl config = configurationManager.getConfiguration( pid ); + if ( config == null ) + { + config = configurationManager.createFactoryConfiguration( pid, factoryPid, null ); + + // FELIX-3360: configuration creation with implicit binding is dynamic + config.setDynamicBundleLocation( Activator.getLocation(getBundle()), false ); + } + else + { + if ( config.getBundleLocation() == null ) + { + Log.logger.log( LogService.LOG_DEBUG, "Binding configuration {0} (isNew: {1}) to bundle {2}", + new Object[] + { config.getPid(), config.isNew() ? Boolean.TRUE : Boolean.FALSE, + Activator.getLocation(this.getBundle()) } ); + + // FELIX-3360: first implicit binding is dynamic + config.setDynamicBundleLocation( Activator.getLocation(getBundle()), true ); + } + else + { + // CM 1.4 / 104.13.2.3 + this.checkPermission( configurationManager, config.getBundleLocation(), false ); + } + } + + return this.wrap( config ); + } + +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationImpl.java b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationImpl.java new file mode 100644 index 00000000000..4d97ce70b42 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationImpl.java @@ -0,0 +1,910 @@ +/* + * 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.cm.impl; + + +import java.io.IOException; +import java.util.Arrays; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; + +import org.apache.felix.cm.PersistenceManager; +import org.apache.felix.cm.impl.helper.TargetedPID; +import org.osgi.framework.Constants; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.log.LogService; + + +/** + * The ConfigurationImpl is the backend implementation of the + * Configuration Admin Service Specification Configuration object + * (section 104.4). Instances of this class are shared by multiple instances of + * the {@link ConfigurationAdapter} class, whose instances are actually returned + * to clients. + */ +public class ConfigurationImpl +{ + + /* + * Concurrency note: There is a slight (but real) chance of a race condition + * between a configuration update and a ManagedService[Factory] registration. + * Per the specification a ManagedService must be called with configuration + * or null when registered and a ManagedService must be called with currently + * existing configuration when registered. Also the ManagedService[Factory] + * must be updated when the configuration is updated. + * + * Consider now this situation of two threads T1 and T2: + * + * T1. create and update configuration + * ConfigurationImpl.update persists configuration and sets field + * Thread preempted + * + * T2. ManagedServiceUpdate constructor reads configuration + * Uses configuration already persisted by T1 for update + * Schedules task to update service with the configuration + * + * T1. Runs again creating the UpdateConfiguration task with the + * configuration persisted before being preempted + * Schedules task to update service + * + * Update Thread: + * Updates ManagedService with configuration prepared by T2 + * Updates ManagedService with configuration prepared by T1 + * + * The correct behaviour would be here, that the second call to update + * would not take place. We cannot at this point in time easily fix + * this issue. Also, it seems that changes for this to happen are + * small. + * + * This class provides modification counter (lastModificationTime) + * which is incremented on each change of the configuration. This + * helps the update tasks in the ConfigurationManager to log the + * revision of the configuration supplied. + */ + + /** + * The name of a synthetic property stored in the persisted configuration + * data to indicate that the configuration data is new, that is created but + * never updated (value is "_felix_.cm.newConfiguration"). + *

      + * This special property is stored by the + * {@link #ConfigurationImpl(ConfigurationManager, PersistenceManager, String, String, String)} + * constructor, when the configuration is first created and persisted and is + * interpreted by the + * {@link #ConfigurationImpl(ConfigurationManager, PersistenceManager, Dictionary)} + * method when the configuration data is loaded in a new object. + *

      + * The goal of this property is to keep the information on whether + * configuration data is new (but persisted as per the spec) or has already + * been assigned with possible no data. + */ + private static final String CONFIGURATION_NEW = "_felix_.cm.newConfiguration"; + + private static final String PROPERTY_LOCKED = ":org.apache.felix.configadmin.locked:"; + + private static final String PROPERTY_REVISION = ":org.apache.felix.configadmin.revision:"; + + /** + * The factory PID of this configuration or null if this + * is not a factory configuration. + */ + private final TargetedPID factoryPID; + + /** + * The statically bound bundle location, which is set explicitly by calling + * the Configuration.setBundleLocation(String) method or when the + * configuration was created with the two-argument method. + */ + private volatile String staticBundleLocation; + + /** + * The bundle location from dynamic binding. This value is set as the + * configuration or factory is assigned to a ManagedService[Factory]. + */ + private volatile String dynamicBundleLocation; + + /** + * The configuration data of this configuration instance. This is a private + * copy of the properties of which a copy is made when the + * {@link #getProperties()} method is called. This field is + * null if the configuration has been created and never been + * updated with acutal configuration properties. + */ + private volatile CaseInsensitiveDictionary properties; + + /** + * Flag indicating that this configuration has been deleted. + * + * @see #isDeleted() + */ + private volatile boolean isDeleted; + + /** + * Configuration revision counter incremented each time the + * {@link #properties} is set (in the constructor or the + * {@link #configure(Dictionary)} method. This counter is + * persisted transparently so that {@link NotCachablePersistenceManager} + * can provide a proper change count. The persistence is forward + * compatible such that previously persisted configurations are + * handled gracefully. + */ + private volatile long revision; + + private volatile boolean locked; + + + /** + * The {@link ConfigurationManager configuration manager} instance which + * caused this configuration object to be created. + */ + private final ConfigurationManager configurationManager; + + // the persistence manager storing this factory mapping + private final PersistenceManager persistenceManager; + + // the basic ID of this instance + private final TargetedPID baseId; + + + + public ConfigurationImpl( ConfigurationManager configurationManager, PersistenceManager persistenceManager, + Dictionary properties ) + { + if ( configurationManager == null ) + { + throw new IllegalArgumentException( "ConfigurationManager must not be null" ); + } + + if ( persistenceManager == null ) + { + throw new IllegalArgumentException( "PersistenceManager must not be null" ); + } + + this.configurationManager = configurationManager; + this.persistenceManager = persistenceManager; + this.baseId = new TargetedPID( ( String ) properties.remove( Constants.SERVICE_PID ) ); + + final String factoryPid = ( String ) properties.remove( ConfigurationAdmin.SERVICE_FACTORYPID ); + this.factoryPID = ( factoryPid == null ) ? null : new TargetedPID( factoryPid ); + this.isDeleted = false; + + // set bundle location from persistence and/or check for dynamic binding + this.staticBundleLocation = ( String ) properties.remove( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ; + this.dynamicBundleLocation = configurationManager.getDynamicBundleLocation( this.baseId.toString() ); + + // set the properties internally + configureFromPersistence( properties ); + } + + + ConfigurationImpl( ConfigurationManager configurationManager, PersistenceManager persistenceManager, String pid, + String factoryPid, String bundleLocation ) throws IOException + { + if ( configurationManager == null ) + { + throw new IllegalArgumentException( "ConfigurationManager must not be null" ); + } + + if ( persistenceManager == null ) + { + throw new IllegalArgumentException( "PersistenceManager must not be null" ); + } + + this.configurationManager = configurationManager; + this.persistenceManager = persistenceManager; + this.baseId = new TargetedPID( pid ); + + this.factoryPID = ( factoryPid == null ) ? null : new TargetedPID( factoryPid ); + this.isDeleted = false; + + // set bundle location from persistence and/or check for dynamic binding + this.staticBundleLocation = bundleLocation; + this.dynamicBundleLocation = configurationManager.getDynamicBundleLocation( this.baseId.toString() ); + + // first "update" + this.properties = null; + this.revision = 1; + + // this is a new configuration object, store immediately unless + // the new configuration object is created from a factory, in which + // case the configuration is only stored when first updated + if ( factoryPid == null ) + { + storeNewConfiguration(); + } + } + + /** + * Returns true if the ConfigurationManager of this + * configuration is still active. + */ + boolean isActive() + { + return configurationManager.isActive(); + } + + + void storeSilently() + { + try + { + this.store(); + } + catch ( IOException ioe ) + { + Log.logger.log( LogService.LOG_ERROR, "Persisting ID {0} failed", new Object[] + { this.baseId, ioe } ); + } + } + + + static protected void replaceProperty( Dictionary properties, String key, String value ) + { + if ( value == null ) + { + properties.remove( key ); + } + else + { + properties.put( key, value ); + } + } + + public void delete() throws IOException + { + this.isDeleted = true; + this.persistenceManager.delete( this.getPidString() ); + configurationManager.setDynamicBundleLocation( this.getPidString(), null ); + configurationManager.deleted( this ); + } + + + public String getPidString() + { + return this.baseId.toString(); + } + + + public TargetedPID getPid() + { + return this.baseId; + } + + + public String getFactoryPidString() + { + return (factoryPID == null) ? null : factoryPID.toString(); + } + + + public TargetedPID getFactoryPid() + { + return factoryPID; + } + + + /** + * Returns the "official" bundle location as visible from the outside + * world of code calling into the Configuration.getBundleLocation() method. + *

      + * In other words: The {@link #getStaticBundleLocation()} is returned if + * not null. Otherwise the {@link #getDynamicBundleLocation()} + * is returned (which may also be null). + */ + String getBundleLocation() + { + if ( staticBundleLocation != null ) + { + return staticBundleLocation; + } + + return dynamicBundleLocation; + } + + + String getDynamicBundleLocation() + { + return dynamicBundleLocation; + } + + + String getStaticBundleLocation() + { + return staticBundleLocation; + } + + + void setStaticBundleLocation( final String bundleLocation ) + { + // CM 1.4; needed for bundle location change at the end + final String oldBundleLocation = getBundleLocation(); + + // 104.15.2.8 The bundle location will be set persistently + this.staticBundleLocation = bundleLocation; + storeSilently(); + + // FELIX-3360: Always clear dynamic binding if a new static + // location is set. The static location is the relevant binding + // for a configuration unless it is not explicitly set. + setDynamicBundleLocation( null, false ); + + // CM 1.4 + this.configurationManager.locationChanged( this, oldBundleLocation ); + } + + + void setDynamicBundleLocation( final String bundleLocation, final boolean dispatchConfiguration ) + { + // CM 1.4; needed for bundle location change at the end + final String oldBundleLocation = getBundleLocation(); + + this.dynamicBundleLocation = bundleLocation; + this.configurationManager.setDynamicBundleLocation( this.getPidString(), bundleLocation ); + + // CM 1.4 + if ( dispatchConfiguration ) + { + this.configurationManager.locationChanged( this, oldBundleLocation ); + + } + } + + + /** + * Dynamically binds this configuration to the given location unless + * the configuration is already bound (statically or dynamically). In + * the case of this configuration to be dynamically bound a + * CM_LOCATION_CHANGED event is dispatched. + */ + void tryBindLocation( final String bundleLocation ) + { + if ( this.getBundleLocation() == null ) + { + Log.logger.log( LogService.LOG_DEBUG, "Dynamically binding config {0} to {1}", new Object[] + { getPidString(), bundleLocation } ); + setDynamicBundleLocation( bundleLocation, true ); + } + } + + + /** + * Returns an optionally deep copy of the properties of this configuration + * instance. + *

      + * This method returns a copy of the internal dictionary. If the + * deepCopy parameter is true array and collection values are + * copied into new arrays or collections. Otherwise just a new dictionary + * referring to the same objects is returned. + * + * @param deepCopy + * true if a deep copy is to be returned. + * @return the configuration properties + */ + public Dictionary getProperties( boolean deepCopy ) + { + // no properties yet + if ( properties == null ) + { + return null; + } + + CaseInsensitiveDictionary props = new CaseInsensitiveDictionary( properties, deepCopy ); + + // fix special properties (pid, factory PID, bundle location) + setAutoProperties( props, false ); + + return props; + } + + + /* (non-Javadoc) + * @see org.osgi.service.cm.Configuration#update() + */ + public void update() throws IOException + { + // read configuration from persistence (again) + if ( persistenceManager.exists( getPidString() ) ) + { + @SuppressWarnings("unchecked") + Dictionary properties = persistenceManager.load( getPidString() ); + + // ensure serviceReference pid + String servicePid = ( String ) properties.get( Constants.SERVICE_PID ); + if ( servicePid != null && !getPidString().equals( servicePid ) ) + { + throw new IOException( "PID of configuration file does match requested PID; expected " + getPidString() + + ", got " + servicePid ); + } + + // we're doing a local update, so override the properties revision + properties.put( PROPERTY_REVISION, Long.valueOf(getRevision()) ); + configureFromPersistence( properties ); + } + + // update the service but do not fire an CM_UPDATED event + configurationManager.updated( this, false ); + } + + + /** + * @see org.osgi.service.cm.Configuration#update(java.util.Dictionary) + */ + public void update( Dictionary properties ) throws IOException + { + CaseInsensitiveDictionary newProperties = new CaseInsensitiveDictionary( properties ); + + Log.logger.log( LogService.LOG_DEBUG, "Updating config {0} with {1}", new Object[] + { getPidString(), newProperties } ); + + setAutoProperties( newProperties, true ); + + // persist new configuration + newProperties.put( PROPERTY_REVISION, Long.valueOf(getRevision()) ); + persistenceManager.store( getPidString(), newProperties ); + + // finally assign the configuration for use + configure( newProperties ); + + // update the service and fire an CM_UPDATED event + configurationManager.updated( this, true ); + } + + + //---------- Object overwrites -------------------------------------------- + + @Override + public boolean equals( Object obj ) + { + if ( obj == this ) + { + return true; + } + + if ( obj instanceof Configuration ) + { + return getPidString().equals( ( ( Configuration ) obj ).getPid() ); + } + + return false; + } + + + @Override + public int hashCode() + { + return getPidString().hashCode(); + } + + + @Override + public String toString() + { + return "Configuration PID=" + getPidString() + ", factoryPID=" + factoryPID + ", bundleLocation=" + getBundleLocation(); + } + + + // ---------- private helper ----------------------------------------------- + + /** + * Stores the configuration if it is a newly factory configuration + * which has not been persisted yet. + *

      + * This is used to ensure a configuration c as in + *

      +     * Configuration cf = cm.createFactoryConfiguration(factoryPid);
      +     * Configuration c = cm.getConfiguration(cf.getPid());
      +     * 
      + * is persisted after getConfiguration while + * createConfiguration alone does not persist yet. + */ + void ensureFactoryConfigPersisted() throws IOException + { + if ( this.factoryPID != null && isNew() && !persistenceManager.exists( getPidString() ) ) + { + storeNewConfiguration(); + } + } + + + /** + * Persists a new (freshly created) configuration with a marker for + * it to be a new configuration. + * + * @throws IOException If an error occurrs storing the configuraiton + */ + private void storeNewConfiguration() throws IOException + { + Dictionary props = new Hashtable<>(); + setAutoProperties( props, true ); + props.put( CONFIGURATION_NEW, Boolean.TRUE ); + props.put( PROPERTY_REVISION, Long.valueOf(getRevision()) ); + persistenceManager.store( getPidString(), props ); + } + + + void store() throws IOException + { + // we don't need a deep copy, since we are not modifying + // any value in the dictionary itself. we are just adding + // properties to it, which are required for storing + Dictionary props = getProperties( false ); + + // if this is a new configuration, we just use an empty Dictionary + if ( props == null ) + { + props = new Hashtable<>(); + + // add automatic properties including the bundle location (if + // statically bound) + setAutoProperties( props, true ); + } + else + { + replaceProperty( props, ConfigurationAdmin.SERVICE_BUNDLELOCATION, getStaticBundleLocation() ); + } + + if ( this.locked ) + { + props.put(PROPERTY_LOCKED, this.locked); + } + else + { + props.remove(PROPERTY_LOCKED); + } + // only store now, if this is not a new configuration + props.put( PROPERTY_REVISION, Long.valueOf(getRevision()) ); + persistenceManager.store( getPidString(), props ); + } + + + /** + * Returns the revision of this configuration object. + *

      + * When getting both the configuration properties and this revision + * counter, the two calls should be synchronized on this instance to + * ensure configuration values and revision counter match. + */ + public long getRevision() + { + return revision; + } + + + /** + * Returns false if this configuration contains configuration + * properties. Otherwise true is returned and this is a + * newly creted configuration object whose {@link #update(Dictionary)} + * method has never been called. + */ + boolean isNew() + { + return properties == null; + } + + + /** + * Returns true if this configuration has already been deleted + * on the persistence. + */ + boolean isDeleted() + { + return isDeleted; + } + + + private void configureFromPersistence( Dictionary properties ) + { + // if the this is not an empty/new configuration, accept the properties + // otherwise just set the properties field to null + if ( properties.get( CONFIGURATION_NEW ) == null ) + { + configure( properties ); + } + else + { + configure( null ); + } + } + + private void configure( final Dictionary properties ) + { + final Object revisionValue = properties == null ? null : properties.get(PROPERTY_REVISION); + final Object lockedValue = properties == null ? null : properties.get(PROPERTY_LOCKED); + if ( lockedValue != null ) + { + this.locked = true; + } + final CaseInsensitiveDictionary newProperties; + if ( properties == null ) + { + newProperties = null; + } + else + { + // remove predefined properties + clearAutoProperties( properties ); + + // ensure CaseInsensitiveDictionary + if ( properties instanceof CaseInsensitiveDictionary ) + { + newProperties = ( CaseInsensitiveDictionary ) properties; + } + else + { + newProperties = new CaseInsensitiveDictionary( properties ); + } + } + + synchronized ( this ) + { + this.properties = newProperties; + this.revision = (revisionValue != null) ? 1 + ((Long)revisionValue).longValue() : ++revision ; + } + } + + + void setAutoProperties( Dictionary properties, boolean withBundleLocation ) + { + // set pid and factory pid in the properties + replaceProperty( properties, Constants.SERVICE_PID, getPidString() ); + replaceProperty( properties, ConfigurationAdmin.SERVICE_FACTORYPID, getFactoryPidString() ); + + // bundle location is not set here + if ( withBundleLocation ) + { + replaceProperty( properties, ConfigurationAdmin.SERVICE_BUNDLELOCATION, getStaticBundleLocation() ); + } + else + { + properties.remove( ConfigurationAdmin.SERVICE_BUNDLELOCATION ); + } + properties.remove( PROPERTY_LOCKED ); + properties.remove( PROPERTY_REVISION ); + } + + + static void setAutoProperties( Dictionary properties, String pid, String factoryPid ) + { + replaceProperty( properties, Constants.SERVICE_PID, pid ); + replaceProperty( properties, ConfigurationAdmin.SERVICE_FACTORYPID, factoryPid ); + properties.remove( ConfigurationAdmin.SERVICE_BUNDLELOCATION ); + properties.remove( PROPERTY_LOCKED ); + properties.remove( PROPERTY_REVISION ); + } + + + private static final String[] AUTO_PROPS = new String[] { + Constants.SERVICE_PID, + ConfigurationAdmin.SERVICE_FACTORYPID, + ConfigurationAdmin.SERVICE_BUNDLELOCATION, + PROPERTY_LOCKED, PROPERTY_REVISION + }; + + static void clearAutoProperties( Dictionary properties ) + { + for(final String p : AUTO_PROPS) + { + properties.remove( p ); + } + } + + + public void setLocked(final boolean flag) throws IOException + { + this.locked = flag; + store(); + } + + /** + * Compare the two properties, ignoring auto properties + * @param props1 Set of properties + * @param props2 Set of properties + * @return {@code true} if the set of properties is equal + */ + static boolean equals( Dictionary props1, Dictionary props2) + { + if (props1 == null) { + if (props2 == null) { + return true; + } else { + return false; + } + } else if (props2 == null) { + return false; + } + + final int count1 = getCount(props1); + final int count2 = getCount(props2); + if ( count1 != count2 ) + { + return false; + } + + final Enumeration keys = props1.keys(); + while ( keys.hasMoreElements() ) + { + final String key = keys.nextElement(); + if ( !isAutoProp(key) ) + { + final Object val1 = props1.get(key); + final Object val2 = props2.get(key); + if ( val1 == null ) + { + if ( val2 != null ) + { + return false; + } + } + else + { + if ( val2 == null ) + { + return false; + } + // arrays are compared using Arrays.equals + if ( val1.getClass().isArray() ) + { + if ( !val2.getClass().isArray() ) + { + return false; + } + final Object[] a1 = convertToObjectArray(val1); + final Object[] a2 = convertToObjectArray(val2); + if ( ! Arrays.equals(a1, a2) ) + { + return false; + } + } + else if ( !val1.equals(val2) ) + { + return false; + } + } + } + } + + return true; + } + + /** + * Convert the object to an array + * @param value The array + * @return an object array + */ + private static Object[] convertToObjectArray(final Object value) + { + final Object[] values; + if (value instanceof long[]) + { + final long[] a = (long[])value; + values = new Object[a.length]; + for(int i=0;i props ) + { + int count = (props == null ? 0 : props.size()); + if ( props != null ) + { + for(final String p : AUTO_PROPS) + { + if ( props.get(p) != null ) + { + count--; + } + } + } + return count; + } + + public boolean isLocked() + { + return this.locked; + } + + + final ConfigurationManager getConfigurationManager() + { + return this.configurationManager; + } +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationManager.java b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationManager.java new file mode 100644 index 00000000000..89db2b505d1 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationManager.java @@ -0,0 +1,1725 @@ +/* + * 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.cm.impl; + + +import java.io.IOException; +import java.lang.reflect.Method; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.Set; + +import org.apache.felix.cm.PersistenceManager; +import org.apache.felix.cm.impl.helper.BaseTracker; +import org.apache.felix.cm.impl.helper.ConfigurationMap; +import org.apache.felix.cm.impl.helper.ManagedServiceFactoryTracker; +import org.apache.felix.cm.impl.helper.ManagedServiceTracker; +import org.apache.felix.cm.impl.helper.TargetedPID; +import org.apache.felix.cm.impl.persistence.CachingPersistenceManagerProxy; +import org.apache.felix.cm.impl.persistence.ExtPersistenceManager; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.BundleListener; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.ConfigurationEvent; +import org.osgi.service.cm.ConfigurationListener; +import org.osgi.service.cm.ConfigurationPermission; +import org.osgi.service.cm.ConfigurationPlugin; +import org.osgi.service.cm.ManagedService; +import org.osgi.service.cm.ManagedServiceFactory; +import org.osgi.service.cm.SynchronousConfigurationListener; +import org.osgi.service.log.LogService; +import org.osgi.util.tracker.ServiceTracker; + + +/** + * The {@code ConfigurationManager} is the central class in this + * implementation of the Configuration Admin Service Specification. As such it + * has the following tasks: + *

        + *
      • It is a BundleListener which gets informed when the + * states of bundles change. Mostly this is needed to unbind any bound + * configuration in case a bundle is uninstalled. + *
      • It is a ServiceListener which gets informed when + * ManagedService and ManagedServiceFactory + * services are registered and unregistered. This is used to provide + * configuration to these services. As a service listener it also listens for + * {@link PersistenceManager} instances being registered to support different + * configuration persistence layers. + *
      • A {@link ConfigurationAdminFactory} instance is registered as the + * ConfigurationAdmin service. + *
      • Last but not least this instance manages all tasks laid out in the + * specification such as maintaining configuration, taking care of configuration + * events, etc. + *
      + */ +public class ConfigurationManager implements BundleListener +{ + // random number generator to create configuration PIDs for factory + // configurations + private static Random numberGenerator; + + // the BundleContext of the Configuration Admin Service bundle + private final BundleContext bundleContext; + + // the service registration of the configuration admin + private volatile ServiceRegistration configurationAdminRegistration; + + // the ConfigurationEvent listeners + private ServiceTracker configurationListenerTracker; + + // the synchronous ConfigurationEvent listeners + private ServiceTracker syncConfigurationListenerTracker; + + // service tracker for managed services + private ManagedServiceTracker managedServiceTracker; + + // service tracker for managed service factories + private ManagedServiceFactoryTracker managedServiceFactoryTracker; + + // the thread used to schedule tasks required to run asynchronously + private UpdateThread updateThread; + + // the thread used to schedule events to be dispatched asynchronously + private UpdateThread eventThread; + + /** + * The persistence manager + */ + private final ExtPersistenceManager persistenceManager; + + // the cache of Configuration instances mapped by their PID + // have this always set to prevent NPE on bundle shutdown + private final HashMap configurations = new HashMap<>(); + + /** + * The map of dynamic configuration bindings. This maps the + * PID of the dynamically bound configuration or factory to its bundle + * location. + *

      + * On bundle startup this map is loaded from persistence and validated + * against the locations of installed bundles: Entries pointing to bundle + * locations not currently installed are removed. + *

      + * The map is written to persistence on each change. + */ + private final DynamicBindings dynamicBindings; + + // flag indicating whether BundleChange events should be consumed (FELIX-979) + private volatile boolean handleBundleEvents; + + // flag indicating whether the manager is considered alive + private volatile boolean isActive; + + // Coordinator service if available + private volatile Object coordinator; + + public ConfigurationManager(final ExtPersistenceManager persistenceManager, + final BundleContext bundleContext) + throws IOException + { + // set up some fields + this.bundleContext = bundleContext; + this.dynamicBindings = new DynamicBindings( bundleContext, persistenceManager.getDelegatee() ); + this.persistenceManager = persistenceManager; + } + + public ServiceReference start() + { + // configurationlistener support + configurationListenerTracker = new ServiceTracker<>( bundleContext, ConfigurationListener.class, null ); + configurationListenerTracker.open(); + syncConfigurationListenerTracker = new ServiceTracker<>( bundleContext, + SynchronousConfigurationListener.class, null ); + syncConfigurationListenerTracker.open(); + + // initialize the asynchonous updater thread + ThreadGroup tg = new ThreadGroup( "Configuration Admin Service" ); + tg.setDaemon( true ); + this.updateThread = new UpdateThread( tg, "CM Configuration Updater" ); + this.eventThread = new UpdateThread( tg, "CM Event Dispatcher" ); + + // register as bundle and service listener + handleBundleEvents = true; + bundleContext.addBundleListener( this ); + + // consider alive now (before clients use Configuration Admin + // service registered in the next step) + isActive = true; + + // create and register configuration admin - start after PM tracker ... + ConfigurationAdminFactory caf = new ConfigurationAdminFactory( this ); + Dictionary props = new Hashtable<>(); + props.put( Constants.SERVICE_PID, "org.apache.felix.cm.ConfigurationAdmin" ); + props.put( Constants.SERVICE_DESCRIPTION, "Configuration Admin Service Specification 1.6 Implementation" ); + props.put( Constants.SERVICE_VENDOR, "The Apache Software Foundation" ); + props.put( "osgi.command.scope", "cm" ); + Set functions = new HashSet<>(); + for ( Method method : ConfigurationAdmin.class.getMethods() ) + { + functions.add(method.getName()); + } + props.put( "osgi.command.function", functions.toArray(new String[0]) ); + configurationAdminRegistration = bundleContext.registerService( ConfigurationAdmin.class, caf, props ); + + // start handling ManagedService[Factory] services + managedServiceTracker = new ManagedServiceTracker(this); + managedServiceFactoryTracker = new ManagedServiceFactoryTracker(this); + + // start processing the event queues only after registering the service + // see FELIX-2813 for details + this.updateThread.start(); + this.eventThread.start(); + + return configurationAdminRegistration.getReference(); + } + + + public void stop( ) + { + + // stop handling bundle events immediately + handleBundleEvents = false; + + // stop handling ManagedService[Factory] services + managedServiceFactoryTracker.close(); + managedServiceTracker.close(); + + // stop queue processing before unregistering the service + // see FELIX-2813 for details + if ( updateThread != null ) + { + updateThread.terminate(); + } + if ( eventThread != null ) + { + eventThread.terminate(); + } + + // immediately unregister the Configuration Admin before cleaning up + // clearing the field before actually unregistering the service + // prevents IllegalStateException in getServiceReference() if + // the field is not null but the service already unregistered + final ServiceRegistration caReg = configurationAdminRegistration; + configurationAdminRegistration = null; + if ( caReg != null ) + { + caReg.unregister(); + } + + // consider inactive after unregistering such that during + // unregistration the manager is still alive and can react + isActive = false; + + // stop listening for events + bundleContext.removeBundleListener( this ); + + if ( configurationListenerTracker != null ) + { + configurationListenerTracker.close(); + } + + if ( syncConfigurationListenerTracker != null ) + { + syncConfigurationListenerTracker.close(); + } + + // just ensure the configuration cache is empty + synchronized ( configurations ) + { + configurations.clear(); + } + } + + + /** + * Returns true if this manager is considered active. + */ + boolean isActive() + { + return isActive; + } + + public BundleContext getBundleContext() + { + return bundleContext; + } + + // ---------- Configuration caching support -------------------------------- + + ConfigurationImpl getCachedConfiguration( String pid ) + { + synchronized ( configurations ) + { + return configurations.get( pid ); + } + } + + + ConfigurationImpl[] getCachedConfigurations() + { + synchronized ( configurations ) + { + return configurations.values().toArray( + new ConfigurationImpl[configurations.size()] ); + } + } + + + ConfigurationImpl cacheConfiguration( ConfigurationImpl configuration ) + { + synchronized ( configurations ) + { + final String pid = configuration.getPidString(); + final Object existing = configurations.get( pid ); + if ( existing != null ) + { + return ( ConfigurationImpl ) existing; + } + + configurations.put( pid, configuration ); + return configuration; + } + } + + + void removeConfiguration( ConfigurationImpl configuration ) + { + synchronized ( configurations ) + { + configurations.remove( configuration.getPidString() ); + } + } + + + // ---------- ConfigurationAdminImpl support + + void setDynamicBundleLocation( final String pid, final String location ) + { + if ( dynamicBindings != null ) + { + try + { + dynamicBindings.putLocation( pid, location ); + } + catch ( IOException ioe ) + { + Log.logger.log( LogService.LOG_ERROR, "Failed storing dynamic configuration binding for {0} to {1}", new Object[] + { pid, location, ioe } ); + } + } + } + + + String getDynamicBundleLocation( final String pid ) + { + if ( dynamicBindings != null ) + { + return dynamicBindings.getLocation( pid ); + } + + return null; + } + + + ConfigurationImpl createFactoryConfiguration( String factoryPid, String location ) throws IOException + { + return cacheConfiguration( internalCreateConfiguration( createPid( factoryPid ), factoryPid, location ) ); + } + + ConfigurationImpl createFactoryConfiguration(String pid, String factoryPid, String location ) throws IOException + { + return cacheConfiguration( internalCreateConfiguration( pid, factoryPid, location ) ); + } + + /** + * Returns a targeted configuration for the given service PID and + * the reference target service. + *

      + * A configuration returned has already been checked for visibility + * by the bundle registering the referenced service. Additionally, + * the configuration is also dynamically bound if needed. + * + * @param rawPid The raw service PID to get targeted configuration for. + * @param target The target ServiceReference to get + * configuration for. + * @return The best matching targeted configuration or null + * if there is no configuration at all. + * @throwss IOException if an error occurrs reading configurations + * from persistence. + */ + ConfigurationImpl getTargetedConfiguration( final String rawPid, final ServiceReference target ) throws IOException + { + final Bundle serviceBundle = target.getBundle(); + if ( serviceBundle != null ) + { + // list of targeted PIDs to check + final StringBuilder targetedPid = new StringBuilder( rawPid ); + int i = 3; + String[] names = new String[4]; + names[i--] = targetedPid.toString(); + targetedPid.append( '|' ).append( serviceBundle.getSymbolicName() ); + names[i--] = targetedPid.toString(); + targetedPid.append( '|' ).append( serviceBundle.getVersion().toString() ); + names[i--] = targetedPid.toString(); + targetedPid.append( '|' ).append( Activator.getLocation(serviceBundle) ); + names[i--] = targetedPid.toString(); + + for ( String candidate : names ) + { + ConfigurationImpl config = getConfiguration( candidate ); + if ( config != null && !config.isDeleted() ) + { + // check visibility to use and dynamically bind + if ( canReceive( serviceBundle, config.getBundleLocation() ) ) + { + config.tryBindLocation( Activator.getLocation(serviceBundle) ); + return config; + } + + // CM 1.4 / 104.13.2.2 / 104.5.3 + // act as if there is no configuration + Log.logger.log( + LogService.LOG_DEBUG, + "Cannot use configuration {0} for {1}: No visibility to configuration bound to {2}; calling with null", + new Object[] + { config.getPid(), target , config.getBundleLocation() } ); + } + } + } + else + { + Log.logger.log( LogService.LOG_INFO, + "Service for PID {0} seems to already have been unregistered, not updating with configuration", + new Object[] + { rawPid } ); + } + + // service already unregistered, nothing to do really + return null; + } + + + /** + * Returns the {@link ConfigurationImpl} with the given PID if + * available in the internal cache or from any persistence manager. + * Otherwise null is returned. + * + * @param pid The PID for which to return the configuration + * @return The configuration or null if non exists + * @throws IOException If an error occurs reading from a persistence + * manager. + */ + ConfigurationImpl getConfiguration( String pid ) throws IOException + { + ConfigurationImpl config = getCachedConfiguration( pid ); + if ( config != null ) + { + Log.logger.log( LogService.LOG_DEBUG, "Found cached configuration {0} bound to {1}", new Object[] + { pid, config.getBundleLocation() } ); + + config.ensureFactoryConfigPersisted(); + + return config; + } + + if ( this.persistenceManager.exists( pid ) ) + { + final Dictionary props = this.persistenceManager.load( pid ); + config = new ConfigurationImpl( this, this.persistenceManager, props ); + Log.logger.log( LogService.LOG_DEBUG, "Found existing configuration {0} bound to {1}", new Object[] + { pid, config.getBundleLocation() } ); + return cacheConfiguration( config ); + } + + // neither the cache nor the persistence manager has configuration + return null; + } + + + /** + * Creates a regular (non-factory) configuration for the given PID + * setting the bundle location accordingly. + *

      + * This method assumes the configuration to not exist yet and will + * create it without further checking. + * + * @param pid The PID of the new configuration + * @param bundleLocation The location to set on the new configuration. + * This may be null to not bind the configuration + * yet. + * @return The new configuration persisted in the first persistence + * manager. + * @throws IOException If an error occurrs writing the configuration + * to the persistence. + */ + ConfigurationImpl createConfiguration( String pid, String bundleLocation ) throws IOException + { + // check for existing (cached or persistent) configuration + ConfigurationImpl config = getConfiguration( pid ); + if ( config != null ) + { + return config; + } + + // else create new configuration also setting the bundle location + // and cache the new configuration + config = internalCreateConfiguration( pid, null, bundleLocation ); + return cacheConfiguration( config ); + } + + + ConfigurationImpl[] listConfigurations( ConfigurationAdminImpl configurationAdmin, String filterString ) + throws IOException, InvalidSyntaxException + { + SimpleFilter filter = null; + if ( filterString != null ) + { + filter = SimpleFilter.parse( filterString ); + } + + Log.logger.log( LogService.LOG_DEBUG, "Listing configurations matching {0}", new Object[] + { filterString } ); + + List configList = new ArrayList<>(); + + Collection configs = this.persistenceManager.getDictionaries(filter ); + for(final Dictionary config : configs) + { + // ignore non-Configuration dictionaries + final String pid = ( String ) config.get( Constants.SERVICE_PID ); + if ( pid == null ) + { + continue; + } + + // CM 1.4 / 104.13.2.3 Permission required + if ( !configurationAdmin.hasPermission( this, + ( String ) config.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ) ) + { + Log.logger.log( + LogService.LOG_DEBUG, + "Omitting configuration {0}: No permission for bundle {1} on configuration bound to {2}", + new Object[] + { pid, Activator.getLocation(configurationAdmin.getBundle()), + config.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) } ); + continue; + } + + // ensure the service.pid and returned a cached config if available + ConfigurationImpl cfg = null; + if ( this.persistenceManager instanceof CachingPersistenceManagerProxy) + { + cfg = getCachedConfiguration( pid ); + if (cfg == null) { + cfg = new ConfigurationImpl(this, this.persistenceManager, config); + // add the to configurations cache if it wasn't in the cache + cacheConfiguration(cfg); + } + } else { + cfg = new ConfigurationImpl( this, this.persistenceManager, config ); + } + + // FELIX-611: Ignore configuration objects without props + if ( !cfg.isNew() ) + { + Log.logger.log( LogService.LOG_DEBUG, "Adding configuration {0}", new Object[] + { pid } ); + configList.add( cfg ); + } + else + { + Log.logger.log( LogService.LOG_DEBUG, "Omitting configuration {0}: Is new", new Object[] + { pid } ); + } + } + + if ( configList.size() == 0 ) + { + return null; + } + return configList.toArray( new ConfigurationImpl[configList + .size()] ); + } + + + void deleted( ConfigurationImpl config ) + { + // remove the configuration from the cache + removeConfiguration( config ); + fireConfigurationEvent( ConfigurationEvent.CM_DELETED, config.getPidString(), config.getFactoryPidString() ); + final Runnable task = new DeleteConfiguration( config ); + if ( this.coordinator == null || !CoordinatorUtil.addToCoordination(this.coordinator, updateThread, task) ) + { + updateThread.schedule( task ); + } + Log.logger.log( LogService.LOG_DEBUG, "DeleteConfiguration({0}) scheduled", new Object[] + { config.getPid() } ); + } + + + void updated( ConfigurationImpl config, boolean fireEvent ) + { + if ( fireEvent ) + { + fireConfigurationEvent( ConfigurationEvent.CM_UPDATED, config.getPidString(), config.getFactoryPidString() ); + } + final Runnable task = new UpdateConfiguration( config ); + if ( this.coordinator == null || !CoordinatorUtil.addToCoordination(this.coordinator, updateThread, task) ) + { + updateThread.schedule( task ); + } + Log.logger.log( LogService.LOG_DEBUG, "UpdateConfiguration({0}) scheduled", new Object[] + { config.getPid() } ); + } + + + void locationChanged( ConfigurationImpl config, String oldLocation ) + { + fireConfigurationEvent( ConfigurationEvent.CM_LOCATION_CHANGED, config.getPidString(), config.getFactoryPidString() ); + if ( oldLocation != null && !config.isNew() ) + { + final Runnable task = new LocationChanged( config, oldLocation ); + if ( this.coordinator == null || !CoordinatorUtil.addToCoordination(this.coordinator, updateThread, task) ) + { + updateThread.schedule( task ); + } + Log.logger.log( LogService.LOG_DEBUG, "LocationChanged({0}, {1}=>{2}) scheduled", new Object[] + { config.getPid(), oldLocation, config.getBundleLocation() } ); + } + else + { + Log.logger.log( LogService.LOG_DEBUG, + "LocationChanged not scheduled for {0} (old location is null or configuration is new)", new Object[] + { config.getPid() } ); + } + } + + + void fireConfigurationEvent( int type, String pid, String factoryPid ) + { + // prevent event senders + FireConfigurationEvent asyncSender = new FireConfigurationEvent( this.configurationListenerTracker, type, pid, + factoryPid ); + FireConfigurationEvent syncSender = new FireConfigurationEvent( this.syncConfigurationListenerTracker, type, + pid, factoryPid ); + + // send synchronous events + if ( syncSender.hasConfigurationEventListeners() ) + { + syncSender.run(); + } + else + { + Log.logger.log( LogService.LOG_DEBUG, "No SynchronousConfigurationListeners to send {0} event to.", new Object[] + { syncSender.getTypeName() } ); + } + + // schedule asynchronous events + if ( asyncSender.hasConfigurationEventListeners() ) + { + if ( this.coordinator == null || !CoordinatorUtil.addToCoordination(this.coordinator, eventThread, asyncSender) ) + { + eventThread.schedule( asyncSender ); + } + } + else + { + Log.logger.log( LogService.LOG_DEBUG, "No ConfigurationListeners to send {0} event to.", new Object[] + { asyncSender.getTypeName() } ); + } + } + + + // ---------- BundleListener ----------------------------------------------- + + @Override + public void bundleChanged( BundleEvent event ) + { + if ( event.getType() == BundleEvent.UNINSTALLED && handleBundleEvents ) + { + final String location = Activator.getLocation(event.getBundle()); + + // we only reset dynamic bindings, which are only present in + // cached configurations, hence only consider cached configs here + final ConfigurationImpl[] configs = getCachedConfigurations(); + for ( int i = 0; i < configs.length; i++ ) + { + final ConfigurationImpl cfg = configs[i]; + if ( location.equals( cfg.getDynamicBundleLocation() ) ) + { + cfg.setDynamicBundleLocation( null, true ); + } + } + } + } + + + // ---------- internal ----------------------------------------------------- + + private ServiceReference getServiceReference() + { + ServiceRegistration reg = configurationAdminRegistration; + if (reg != null) { + return reg.getReference(); + } + + // probably called for firing an event during service registration + // since we didn't get the service registration yet we use the + // service registry to get our service reference + BundleContext context = bundleContext; + if ( context != null ) + { + try + { + Collection> refs = context.getServiceReferences( ConfigurationAdmin.class, null ); + if ( refs != null && !refs.isEmpty()) + { + for(final ServiceReference ref : refs) + { + if ( ref.getBundle().getBundleId() == context.getBundle().getBundleId() ) + { + return ref; + } + } + } + } + catch ( InvalidSyntaxException e ) + { + // unexpected since there is no filter + } + } + + // service references + return null; + } + + + /** + * Configures the ManagedService and returns the service.pid + * service property as a String[], which may be null if + * the ManagedService does not have such a property. + */ + /** + * Configures the ManagedServiceFactory and returns the service.pid + * service property as a String[], which may be null if + * the ManagedServiceFactory does not have such a property. + */ + /** + * Schedules the configuration of the referenced service with + * configuration for the given PID. + * + * @param pid The list of service PID of the configurations to be + * provided to the referenced service. + * @param sr The ServiceReference to the service + * to be configured. + * @param factory true If the service is considered to + * be a ManagedServiceFactory. Otherwise the service + * is considered to be a ManagedService. + */ + public void configure( String[] pid, ServiceReference sr, final boolean factory, final ConfigurationMap configs ) + { + if ( Log.logger.isLogEnabled( LogService.LOG_DEBUG ) ) + { + Log.logger.log( LogService.LOG_DEBUG, "configure(ManagedService {0})", new Object[] + { sr } ); + } + + Runnable r; + if ( factory ) + { + r = new ManagedServiceFactoryUpdate( pid, sr, configs ); + } + else + { + r = new ManagedServiceUpdate( pid, sr, configs ); + } + if ( this.coordinator == null || !CoordinatorUtil.addToCoordination(this.coordinator, updateThread, r) ) + { + updateThread.schedule( r ); + } + Log.logger.log( LogService.LOG_DEBUG, "[{0}] scheduled", new Object[] + { r } ); + } + + + /** + * Factory method to create a new configuration object. The configuration + * object returned is not stored in configuration cache and only persisted + * if the factoryPid parameter is null. + * + * @param pid + * The PID of the new configuration object. Must not be + * null. + * @param factoryPid + * The factory PID of the new configuration. Not + * null if the new configuration object belongs to a + * factory. The configuration object will not be persisted if + * this parameter is not null. + * @param bundleLocation + * The bundle location of the bundle to which the configuration + * belongs or null if the configuration is not bound + * yet. + * @return The new configuration object + * @throws IOException + * May be thrown if an error occurrs persisting the new + * configuration object. + */ + private ConfigurationImpl internalCreateConfiguration( String pid, String factoryPid, String bundleLocation ) throws IOException + { + Log.logger.log( LogService.LOG_DEBUG, "createConfiguration({0}, {1}, {2})", new Object[] + { pid, factoryPid, bundleLocation } ); + return new ConfigurationImpl( this, this.persistenceManager, pid, factoryPid, bundleLocation ); + } + + + /** + * Returns a list of {@link Factory} instances according to the + * Configuration Admin 1.5 specification for targeted PIDs (Section + * 104.3.2) + * + * @param rawFactoryPid The raw factory PID without any targettng. + * @param target The ServiceReference of the service to + * be supplied with targeted configuration. + * @return A list of {@link Factory} instances as listed above. This + * list will always at least include an instance for the + * rawFactoryPid. Other instances are only included + * if existing. + * @throws IOException If an error occurs reading any of the + * {@link Factory} instances from persistence + */ + List getTargetedFactories( final String rawFactoryPid, final ServiceReference target ) throws IOException + { + List factories = new LinkedList<>(); + + final Bundle serviceBundle = target.getBundle(); + if ( serviceBundle != null ) + { + final StringBuilder targetedPid = new StringBuilder( rawFactoryPid ); + factories.add( targetedPid.toString() ); + + targetedPid.append( '|' ).append( serviceBundle.getSymbolicName() ); + factories.add( 0, targetedPid.toString() ); + + targetedPid.append( '|' ).append( serviceBundle.getVersion().toString() ); + factories.add( 0, targetedPid.toString() ); + + targetedPid.append( '|' ).append( Activator.getLocation(serviceBundle) ); + factories.add( 0, targetedPid.toString() ); + } + + return factories; + } + + /** + * Calls the registered configuration plugins on the given configuration + * properties from the given configuration object. + *

      + * The plugins to be called are selected as ConfigurationPlugin + * services registered with a cm.target property set to + * * or the factory PID of the configuration (for factory + * configurations) or the PID of the configuration (for non-factory + * configurations). + * + * @param props The configuration properties run through the registered + * ConfigurationPlugin services. This must not be + * null. + * @param sr The service reference of the managed service (factory) which + * is to be updated with configuration + * @param configPid The PID of the configuration object whose properties + * are to be augmented + * @param factoryPid the factory PID of the configuration object whose + * properties are to be augmented. This is non-null + * only for a factory configuration. + */ + public void callPlugins( final Dictionary props, final ServiceReference sr, final String configPid, + final String factoryPid ) + { + ServiceReference[] plugins = null; + try + { + final String targetPid = (factoryPid == null) ? configPid : factoryPid; + String filter = "(|(!(cm.target=*))(cm.target=" + targetPid + "))"; + plugins = bundleContext.getServiceReferences( ConfigurationPlugin.class.getName(), filter ); + } + catch ( InvalidSyntaxException ise ) + { + // no filter, no exception ... + } + + // abort early if there are no plugins + if ( plugins == null || plugins.length == 0 ) + { + return; + } + + // sort the plugins by their service.cmRanking + if ( plugins.length > 1 ) + { + Arrays.sort( plugins, RankingComparator.CM_RANKING ); + } + + // call the plugins in order + for ( int i = 0; i < plugins.length; i++ ) + { + ServiceReference pluginRef = plugins[i]; + ConfigurationPlugin plugin = ( ConfigurationPlugin ) bundleContext.getService( pluginRef ); + if ( plugin != null ) + { + // if cmRanking is below 0 or above 1000, ignore modifications from the plugin + boolean ignore = false; + Object rankObj = pluginRef.getProperty( ConfigurationPlugin.CM_RANKING ); + if ( rankObj instanceof Integer ) + { + final int ranking = ( ( Integer ) rankObj ).intValue(); + ignore = (ranking < 0 ) || (ranking > 1000); + } + + try + { + plugin.modifyConfiguration( sr, ignore ? CaseInsensitiveDictionary.unmodifiable(props) : props ); + } + catch ( Throwable t ) + { + Log.logger.log( LogService.LOG_ERROR, "Unexpected problem calling configuration plugin {0}", new Object[] + { pluginRef , t } ); + } + finally + { + // ensure ungetting the plugin + bundleContext.ungetService( pluginRef ); + } + ConfigurationImpl.setAutoProperties( props, configPid, factoryPid ); + } + } + } + + + /** + * Creates a PID for the given factoryPid + * + * @param factoryPid + * @return + */ + private static String createPid( String factoryPid ) + { + Random ng = numberGenerator; + if ( ng == null ) + { + // FELIX-2771 Secure Random not available on Mika + try + { + ng = new SecureRandom(); + } + catch ( Throwable t ) + { + // fall back to Random + ng = new Random(); + } + } + + byte[] randomBytes = new byte[16]; + ng.nextBytes( randomBytes ); + randomBytes[6] &= 0x0f; /* clear version */ + randomBytes[6] |= 0x40; /* set to version 4 */ + randomBytes[8] &= 0x3f; /* clear variant */ + randomBytes[8] |= 0x80; /* set to IETF variant */ + + StringBuilder buf = new StringBuilder( factoryPid.length() + 1 + 36 ); + + // prefix the new pid with the factory pid + buf.append( factoryPid ).append( "." ); + + // serialize the UUID into the buffer + for ( int i = 0; i < randomBytes.length; i++ ) + { + + if ( i == 4 || i == 6 || i == 8 || i == 10 ) + { + buf.append( '-' ); + } + + int val = randomBytes[i] & 0xff; + buf.append( Integer.toHexString( val >> 4 ) ); + buf.append( Integer.toHexString( val & 0xf ) ); + } + + return buf.toString(); + } + + + /** + * Checks whether the bundle is allowed to receive the configuration + * with the given location binding. + *

      + * This method implements the logic defined CM 1.4 / 104.4.1: + *

        + *
      • If the location is null (the configuration is not + * bound yet), assume the bundle is allowed
      • + *
      • If the location is a single location (no leading "?"), require + * the bundle's location to match
      • + *
      • If the location is a multi-location (leading "?"), assume the + * bundle is allowed if there is no security manager. If there is a + * security manager, check whether the bundle has "target" permission + * on this location.
      • + *
      + */ + boolean canReceive( final Bundle bundle, final String location ) + { + if ( location == null ) + { + Log.logger.log( LogService.LOG_DEBUG, "canReceive=true; bundle={0}; configuration=(unbound)", new Object[] + { Activator.getLocation(bundle) } ); + return true; + } + else if ( location.startsWith( "?" ) ) + { + // multi-location + if ( System.getSecurityManager() != null ) + { + final boolean hasPermission = bundle.hasPermission( new ConfigurationPermission( location, + ConfigurationPermission.TARGET ) ); + Log.logger.log( LogService.LOG_DEBUG, "canReceive={0}: bundle={1}; configuration={2} (SecurityManager check)", + new Object[] + { new Boolean( hasPermission ), Activator.getLocation(bundle), location } ); + return hasPermission; + } + + Log.logger.log( LogService.LOG_DEBUG, "canReceive=true; bundle={0}; configuration={1} (no SecurityManager)", + new Object[] + { Activator.getLocation(bundle), location } ); + return true; + } + else + { + // single location, must match + final boolean hasPermission = location.equals( Activator.getLocation(bundle) ); + Log.logger.log( LogService.LOG_DEBUG, "canReceive={0}: bundle={1}; configuration={2}", new Object[] + { new Boolean( hasPermission ), Activator.getLocation(bundle), location } ); + return hasPermission; + } + } + + + // ---------- inner classes + + /** + * The ManagedServiceUpdate updates a freshly registered + * ManagedService with a specific configuration. If a + * ManagedService is registered with multiple PIDs an instance of this + * class is used for each registered PID. + */ + private class ManagedServiceUpdate implements Runnable + { + private final String[] pids; + + private final ServiceReference sr; + + private final ConfigurationMap configs; + + + ManagedServiceUpdate( String[] pids, ServiceReference sr, ConfigurationMap configs ) + { + this.pids = pids; + this.sr = sr; + this.configs = configs; + } + + + @Override + public void run() + { + for ( String pid : this.pids ) + { + try + { + final ConfigurationImpl config = getTargetedConfiguration( pid, this.sr ); + provide( pid, config ); + } + catch ( IOException ioe ) + { + Log.logger.log( LogService.LOG_ERROR, "Error loading configuration for {0}", new Object[] + { pid, ioe } ); + } + catch ( Exception e ) + { + Log.logger.log( LogService.LOG_ERROR, "Unexpected problem providing configuration {0} to service {1}", + new Object[] + { pid, this.sr, e } ); + } + } + } + + + private void provide(final String servicePid, final ConfigurationImpl config) + { + // check configuration + final TargetedPID configPid; + final Dictionary properties; + final long revision; + if ( config != null ) + { + synchronized ( config ) + { + configPid = config.getPid(); + properties = config.getProperties( true ); + revision = config.getRevision(); + } + } + else + { + // 104.5.3 ManagedService.updated must be called with null + // if no configuration is available + configPid = new TargetedPID( servicePid ); + properties = null; + revision = -1; + } + + Log.logger.log( LogService.LOG_DEBUG, "Updating service {0} with configuration {1}@{2}", new Object[] + { servicePid, configPid, new Long( revision ) } ); + + managedServiceTracker.provideConfiguration( sr, configPid, null, properties, revision, this.configs ); + } + + @Override + public String toString() + { + return "ManagedService Update: pid=" + Arrays.asList( pids ); + } + } + + /** + * The ManagedServiceFactoryUpdate updates a freshly + * registered ManagedServiceFactory with a specific + * configuration. If a ManagedServiceFactory is registered with + * multiple PIDs an instance of this class is used for each registered + * PID. + */ + private class ManagedServiceFactoryUpdate implements Runnable + { + private final String[] factoryPids; + + private final ServiceReference sr; + + private final ConfigurationMap configs; + + + ManagedServiceFactoryUpdate( String[] factoryPids, ServiceReference sr, final ConfigurationMap configs ) + { + this.factoryPids = factoryPids; + this.sr = sr; + this.configs = configs; + } + + + @Override + public void run() + { + for ( String factoryPid : this.factoryPids ) + { + + try + { + final List targetedFactoryPids = getTargetedFactories( factoryPid, sr ); + final Set pids = persistenceManager.getFactoryConfigurationPids(targetedFactoryPids); + for ( final String pid : pids ) + { + ConfigurationImpl cfg; + try + { + cfg = getConfiguration( pid ); + } + catch ( IOException ioe ) + { + Log.logger.log( LogService.LOG_ERROR, "Error loading configuration for {0}", new Object[] + { pid, ioe } ); + continue; + } + + // sanity check on the configuration + if ( cfg == null ) + { + Log.logger.log( LogService.LOG_ERROR, + "Configuration {0} referred to by factory {1} does not exist", new Object[] + { pid, factoryPid } ); + continue; + } + else if ( cfg.isNew() ) + { + // Configuration has just been created but not yet updated + // we currently just ignore it and have the update mechanism + // provide the configuration to the ManagedServiceFactory + // As of FELIX-612 (not storing new factory configurations) + // this should not happen. We keep this for added stability + // but raise the logging level to error. + Log.logger.log( LogService.LOG_ERROR, "Ignoring new configuration pid={0}", new Object[] + { pid } ); + continue; + } + + provide( factoryPid, cfg ); + } + } + catch ( IOException ioe ) + { + Log.logger.log( LogService.LOG_ERROR, "Cannot get factory mapping for factory PID {0}", new Object[] + { factoryPid, ioe } ); + } + } + } + + + private void provide(final String factoryPid, final ConfigurationImpl config) { + + final Dictionary rawProperties; + final long revision; + synchronized ( config ) + { + rawProperties = config.getProperties( true ); + revision = config.getRevision(); + } + + Log.logger.log( LogService.LOG_DEBUG, "Updating service {0} with configuration {1}/{2}@{3}", new Object[] + { factoryPid, config.getFactoryPid(), config.getPid(), new Long( revision ) } ); + + // CM 1.4 / 104.13.2.1 + final Bundle serviceBundle = this.sr.getBundle(); + if ( serviceBundle == null ) + { + Log.logger.log( + LogService.LOG_INFO, + "ManagedServiceFactory for factory PID {0} seems to already have been unregistered, not updating with factory", + new Object[] + { factoryPid } ); + return; + } + + if ( !canReceive( serviceBundle, config.getBundleLocation() ) ) + { + Log.logger.log( LogService.LOG_ERROR, + "Cannot use configuration {0} for {1}: No visibility to configuration bound to {2}", + new Object[] + { config.getPid(), sr , config.getBundleLocation() } ); + + // no service, really, bail out + return; + } + + // 104.4.2 Dynamic Binding + config.tryBindLocation( Activator.getLocation(serviceBundle) ); + + // update the service with the configuration (if non-null) + if ( rawProperties != null ) + { + Log.logger.log( LogService.LOG_DEBUG, "{0}: Updating configuration pid={1}", new Object[] + { sr, config.getPid() } ); + managedServiceFactoryTracker.provideConfiguration( sr, config.getPid(), config.getFactoryPid(), + rawProperties, revision, this.configs ); + } + } + + + @Override + public String toString() + { + return "ManagedServiceFactory Update: factoryPid=" + Arrays.asList( this.factoryPids ); + } + } + + private abstract class ConfigurationProvider implements Runnable + { + + protected final ConfigurationImpl config; + protected final long revision; + protected final Dictionary properties; + private BaseTracker helper; + + + protected ConfigurationProvider( final ConfigurationImpl config ) + { + synchronized ( config ) + { + this.config = config; + this.revision = config.getRevision(); + this.properties = config.getProperties( true ); + } + } + + + protected TargetedPID getTargetedServicePid() + { + final TargetedPID factoryPid = this.config.getFactoryPid(); + if ( factoryPid != null ) + { + return factoryPid; + } + return this.config.getPid(); + } + + + protected BaseTracker getHelper() + { + if ( this.helper == null ) + { + this.helper = ( BaseTracker ) ( ( this.config.getFactoryPid() == null ) ? ConfigurationManager.this.managedServiceTracker + : ConfigurationManager.this.managedServiceFactoryTracker ); + } + return this.helper; + } + + + protected boolean provideReplacement( ServiceReference sr ) + { + if ( this.config.getFactoryPid() == null ) + { + try + { + final String configPidString = this.getHelper().getServicePid( sr, this.config.getPid() ); + if (configPidString == null) { + return false; // The managed service is not registered anymore in the OSGi service registry. + } + final ConfigurationImpl rc = getTargetedConfiguration( configPidString, sr ); + if ( rc != null ) + { + final TargetedPID configPid; + final Dictionary properties; + final long revision; + synchronized ( rc ) + { + configPid = rc.getPid(); + properties = rc.getProperties( true ); + revision = rc.getRevision(); + } + + this.getHelper().provideConfiguration( sr, configPid, null, properties, -revision, null ); + + return true; + } + } + catch ( IOException ioe ) + { + Log.logger.log( LogService.LOG_ERROR, "Error loading configuration for {0}", new Object[] + { this.config.getPid(), ioe } ); + } + catch ( Exception e ) + { + Log.logger.log( LogService.LOG_ERROR, "Unexpected problem providing configuration {0} to service {1}", + new Object[] + { this.config.getPid(), sr, e } ); + } + } + + // factory or no replacement available + return false; + } + } + + /** + * The UpdateConfiguration is used to update + * ManagedService[Factory] services with the configuration + * they are subscribed to. This may cause the configuration to be + * supplied to multiple services. + */ + private class UpdateConfiguration extends ConfigurationProvider + { + + UpdateConfiguration( final ConfigurationImpl config ) + { + super( config ); + } + + + @Override + public void run() + { + Log.logger.log( LogService.LOG_DEBUG, "Updating configuration {0} to revision #{1}", new Object[] + { config.getPid(), new Long( revision ) } ); + + final List> srList = this.getHelper().getServices( getTargetedServicePid() ); + if ( !srList.isEmpty() ) + { + // optionally bind dynamically to the first service + Bundle bundle = srList.get(0).getBundle(); + if (bundle == null) { + Log.logger.log( LogService.LOG_DEBUG, + "Service {0} seems to be unregistered concurrently (not providing configuration)", + new Object[] + { srList.get(0) } ); + return; + } + config.tryBindLocation( Activator.getLocation(bundle) ); + + final String configBundleLocation = config.getBundleLocation(); + + // provide configuration to all services from the + // correct bundle + for (ServiceReference ref : srList) + { + final Bundle refBundle = ref.getBundle(); + if ( refBundle == null ) + { + Log.logger.log( LogService.LOG_DEBUG, + "Service {0} seems to be unregistered concurrently (not providing configuration)", + new Object[] + { ref } ); + } + else if ( canReceive( refBundle, configBundleLocation ) ) + { + this.getHelper().provideConfiguration( ref, this.config.getPid(), this.config.getFactoryPid(), + this.properties, this.revision, null ); + } + else + { + // CM 1.4 / 104.13.2.2 + Log.logger.log( LogService.LOG_ERROR, + "Cannot use configuration {0} for {1}: No visibility to configuration bound to {2}", + new Object[] + { config.getPid(), ref, configBundleLocation } ); + } + + } + } + else if ( Log.logger.isLogEnabled( LogService.LOG_DEBUG ) ) + { + Log.logger.log( LogService.LOG_DEBUG, "No ManagedService[Factory] registered for updates to configuration {0}", + new Object[] + { config.getPid() } ); + } + } + + + @Override + public String toString() + { + return "Update: pid=" + config.getPid(); + } + } + + + /** + * The DeleteConfiguration class is used to inform + * ManagedService[Factory] services of a configuration + * being deleted. + */ + private class DeleteConfiguration extends ConfigurationProvider + { + + private final String configLocation; + + + DeleteConfiguration( ConfigurationImpl config ) + { + /* + * NOTE: We keep the configuration because it might be cleared just + * after calling this method. The pid and factoryPid fields are + * final and cannot be reset. + */ + super(config); + this.configLocation = config.getBundleLocation(); + } + + + @Override + public void run() + { + List> srList = this.getHelper().getServices( getTargetedServicePid() ); + if ( !srList.isEmpty() ) + { + for (ServiceReference sr : srList) + { + final Bundle srBundle = sr.getBundle(); + if ( srBundle == null ) + { + Log.logger.log( LogService.LOG_DEBUG, + "Service {0} seems to be unregistered concurrently (not removing configuration)", + new Object[] + { sr } ); + } + else if ( canReceive( srBundle, configLocation ) ) + { + // revoke configuration unless a replacement + // configuration can be provided + if ( !this.provideReplacement( sr ) ) + { + this.getHelper().removeConfiguration( sr, this.config.getPid(), this.config.getFactoryPid() ); + } + } + else + { + // CM 1.4 / 104.13.2.2 + Log.logger.log( LogService.LOG_ERROR, + "Cannot remove configuration {0} for {1}: No visibility to configuration bound to {2}", + new Object[] + { config.getPid(), sr, configLocation } ); + } + } + } + } + + @Override + public String toString() + { + return "Delete: pid=" + config.getPid(); + } + } + + private class LocationChanged extends ConfigurationProvider + { + private final String oldLocation; + + + LocationChanged( ConfigurationImpl config, String oldLocation ) + { + super( config ); + this.oldLocation = oldLocation; + } + + + @Override + public void run() + { + List> srList = this.getHelper().getServices( getTargetedServicePid() ); + if ( !srList.isEmpty() ) + { + for (final ServiceReference sr : srList) + { + final Bundle srBundle = sr.getBundle(); + if ( srBundle == null ) + { + Log.logger.log( LogService.LOG_DEBUG, + "Service {0} seems to be unregistered concurrently (not processing)", new Object[] + { sr } ); + continue; + } + + final boolean wasVisible = canReceive( srBundle, oldLocation ); + final boolean isVisible = canReceive( srBundle, config.getBundleLocation() ); + + // make sure the config is dynamically bound to the first + // service if the config has been unbound causing this update + if ( isVisible ) + { + config.tryBindLocation( Activator.getLocation(srBundle) ); + } + + if ( wasVisible && !isVisible ) + { + // revoke configuration unless a replacement + // configuration can be provided + if ( !this.provideReplacement( sr ) ) + { + this.getHelper().removeConfiguration( sr, this.config.getPid(), this.config.getFactoryPid() ); + Log.logger.log( LogService.LOG_DEBUG, "Configuration {0} revoked from {1} (no more visibility)", + new Object[] + { config.getPid(), sr } ); + } + } + else if ( !wasVisible && isVisible ) + { + // call updated method + this.getHelper().provideConfiguration( sr, this.config.getPid(), this.config.getFactoryPid(), + this.properties, this.revision, null ); + Log.logger.log( LogService.LOG_DEBUG, "Configuration {0} provided to {1} (new visibility)", new Object[] + { config.getPid(), sr } ); + } + else + { + // same visibility as before + Log.logger.log( LogService.LOG_DEBUG, "Unmodified visibility to configuration {0} for {1}", new Object[] + { config.getPid(), sr } ); + } + } + } + } + + + @Override + public String toString() + { + return "Location Changed (pid=" + config.getPid() + "): " + oldLocation + " ==> " + + config.getBundleLocation(); + } + } + + private class FireConfigurationEvent implements Runnable + { + private final int type; + + private final String pid; + + private final String factoryPid; + + private final ServiceReference[] listenerReferences; + + private final ConfigurationListener[] listeners; + + private final Bundle[] listenerProvider; + + private ConfigurationEvent event; + + private FireConfigurationEvent( final ServiceTracker listenerTracker, final int type, final String pid, final String factoryPid) + { + this.type = type; + this.pid = pid; + this.factoryPid = factoryPid; + + final ServiceReference[] srs = listenerTracker.getServiceReferences(); + if ( srs == null || srs.length == 0 ) + { + this.listenerReferences = null; + this.listeners = null; + this.listenerProvider = null; + } + else + { + this.listenerReferences = srs; + this.listeners = new ConfigurationListener[srs.length]; + this.listenerProvider = new Bundle[srs.length]; + for ( int i = 0; i < srs.length; i++ ) + { + this.listeners[i] = ( ConfigurationListener ) listenerTracker.getService( srs[i] ); + this.listenerProvider[i] = srs[i].getBundle(); + } + } + } + + + boolean hasConfigurationEventListeners() + { + return this.listenerReferences != null; + } + + + String getTypeName() + { + switch ( type ) + { + case ConfigurationEvent.CM_DELETED: + return "CM_DELETED"; + case ConfigurationEvent.CM_UPDATED: + return "CM_UPDATED"; + case ConfigurationEvent.CM_LOCATION_CHANGED: + return "CM_LOCATION_CHANGED"; + default: + return ""; + } + } + + + @Override + public void run() + { + for ( int i = 0; i < listeners.length; i++ ) + { + sendEvent( i ); + } + } + + + @Override + public String toString() + { + return "Fire ConfigurationEvent: pid=" + pid; + } + + + private ConfigurationEvent getConfigurationEvent(ServiceReference serviceReference) + { + if ( event == null ) + { + this.event = new ConfigurationEvent( serviceReference, type, factoryPid, pid ); + } + return event; + } + + + private void sendEvent( final int serviceIndex ) + { + if ( (listenerProvider[serviceIndex].getState() & (Bundle.ACTIVE | Bundle.STARTING)) > 0 + && this.listeners[serviceIndex] != null ) + { + Log.logger.log( LogService.LOG_DEBUG, "Sending {0} event for {1} to {2}", new Object[] + { getTypeName(), pid, listenerReferences[serviceIndex]} ); + + final ServiceReference serviceReference = getServiceReference(); + + if (serviceReference == null) + { + Log.logger.log( LogService.LOG_WARNING, "No ConfigurationAdmin for delivering configuration event to {0}", new Object[] + { listenerReferences[serviceIndex] } ); + + return; + } + + try + { + if ( System.getSecurityManager() != null ) + { + AccessController.doPrivileged( + new PrivilegedAction() + { + @Override + public Void run() + { + listeners[serviceIndex].configurationEvent(getConfigurationEvent(serviceReference)); + return null; + } + }, BaseTracker.getAccessControlContext(listenerProvider[serviceIndex]) + ); + } + else + { + listeners[serviceIndex].configurationEvent(getConfigurationEvent(serviceReference)); + } + } + catch ( Throwable t ) + { + Log.logger.log( LogService.LOG_ERROR, "Unexpected problem delivering configuration event to {0}", new Object[] + { listenerReferences[serviceIndex], t } ); + } + finally + { + this.listeners[serviceIndex] = null; + } + } + } + } + + public void setCoordinator(final Object service) + { + this.coordinator = service; + } +} + diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/CoordinatorUtil.java b/configadmin/src/main/java/org/apache/felix/cm/impl/CoordinatorUtil.java new file mode 100644 index 00000000000..a79b6ffe4c2 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/CoordinatorUtil.java @@ -0,0 +1,97 @@ +/* + * 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.cm.impl; + +import java.util.ArrayList; +import java.util.List; + +import org.osgi.service.coordinator.Coordination; +import org.osgi.service.coordinator.Coordinator; +import org.osgi.service.coordinator.Participant; + +/** + * Utility class for coordinations + */ +public class CoordinatorUtil +{ + + public static final class Notifier implements Participant + { + private final List runnables = new ArrayList(); + + private final UpdateThread thread; + + public Notifier(final UpdateThread t) + { + this.thread = t; + } + + private void execute() + { + for(final Runnable r : runnables) + { + this.thread.schedule(r); + } + runnables.clear(); + } + + @Override + public void ended(Coordination coordination) throws Exception + { + execute(); + } + + @Override + public void failed(Coordination coordination) throws Exception + { + execute(); + } + + public void add(final Runnable t) + { + runnables.add(t); + } + } + + public static boolean addToCoordination(final Object srv, final UpdateThread thread, final Runnable task) + { + final Coordinator coordinator = (Coordinator) srv; + Coordination c = coordinator.peek(); + if ( c != null && !c.isTerminated() ) + { + Notifier n = null; + for(final Participant p : c.getParticipants()) + { + if ( p instanceof Notifier ) + { + n = (Notifier) p; + break; + } + } + if ( n == null ) + { + n = new Notifier(thread); + c.addParticipant(n); + } + n.add(task); + return true; + } + return false; + } +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/DynamicBindings.java b/configadmin/src/main/java/org/apache/felix/cm/impl/DynamicBindings.java new file mode 100644 index 00000000000..7cd8a72fa35 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/DynamicBindings.java @@ -0,0 +1,118 @@ +/* + * 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.cm.impl; + + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; + +import org.apache.felix.cm.PersistenceManager; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; + + +class DynamicBindings +{ + + static final String BINDINGS_FILE_NAME = "org_apache_felix_cm_impl_DynamicBindings"; + + private final PersistenceManager persistenceManager; + + private final Dictionary bindings; + + DynamicBindings( BundleContext bundleContext, PersistenceManager persistenceManager ) throws IOException + { + this.persistenceManager = persistenceManager; + + if ( persistenceManager.exists( BINDINGS_FILE_NAME ) ) + { + this.bindings = persistenceManager.load( BINDINGS_FILE_NAME ); + + // get locations of installed bundles to validate the bindings + final HashSet locations = new HashSet(); + final Bundle[] bundles = bundleContext.getBundles(); + for ( int i = 0; i < bundles.length; i++ ) + { + locations.add( Activator.getLocation(bundles[i]) ); + } + + // collect pids whose location is not installed any more + ArrayList removedKeys = new ArrayList(); + for ( Enumeration ke = bindings.keys(); ke.hasMoreElements(); ) + { + final String pid = ( String ) ke.nextElement(); + final String location = bindings.get( pid ); + if ( !locations.contains( location ) ) + { + removedKeys.add( pid ); + } + } + + // if some bindings had to be removed, store the mapping again + if ( removedKeys.size() > 0 ) + { + // remove invalid mappings + for ( Iterator rki = removedKeys.iterator(); rki.hasNext(); ) + { + bindings.remove( rki.next() ); + } + + // store the modified map + persistenceManager.store( BINDINGS_FILE_NAME, bindings ); + } + } + else + { + this.bindings = new Hashtable<>(); + } + + } + + + String getLocation( final String pid ) + { + synchronized ( this ) + { + return this.bindings.get( pid ); + } + } + + + void putLocation( final String pid, final String location ) throws IOException + { + synchronized ( this ) + { + if ( location == null ) + { + this.bindings.remove( pid ); + } + else + { + this.bindings.put( pid, location ); + } + + this.persistenceManager.store( BINDINGS_FILE_NAME, bindings ); + } + } +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/Log.java b/configadmin/src/main/java/org/apache/felix/cm/impl/Log.java new file mode 100644 index 00000000000..3655868c685 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/Log.java @@ -0,0 +1,254 @@ +/* + * 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.cm.impl; + +import java.text.MessageFormat; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.log.LogService; +import org.osgi.util.tracker.ServiceTracker; + +/** + * Log implementation either logging to a {@code LogService} or to {@code System.err}. + * The logger can be get using the static {@link #logger} field. + * + * The logger is initialized through {@link #start(BundleContext)} and {@link #set(ServiceReference)}. + * It gets cleaned up through {@link #stop()}. + */ +public class Log +{ + /** The shared logger instance. */ + public static final Log logger = new Log(); + + /** + * The name of the bundle context property defining the maximum log level + * (value is "felix.cm.loglevel"). The log level setting is only used if + * there is no OSGi LogService available. Otherwise this setting is ignored. + *

      + * This value of this property is expected to be an integer number + * corresponding to the log level values of the OSGi LogService. That is 1 + * for errors, 2 for warnings, 3 for informational messages and 4 for debug + * messages. The default value is 2, such that only warnings and errors are + * logged in the absence of a LogService. + */ + private static final String CM_LOG_LEVEL = "felix.cm.loglevel"; + + // The name of the LogService (not using the class, which might be missing) + private static final String LOG_SERVICE_NAME = "org.osgi.service.log.LogService"; + + private static final int CM_LOG_LEVEL_DEFAULT = 2; + + // the ServiceTracker to emit log services (see log(int, String, Throwable)) + @SuppressWarnings("rawtypes") + private volatile ServiceTracker logTracker; + + // the maximum log level when no LogService is available + private volatile int logLevel = CM_LOG_LEVEL_DEFAULT; + + private volatile ServiceReference serviceReference; + + /** + * Start the tracker for the logger and set the log level according to the configuration. + * @param bundleContext The bundle context + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void start( final BundleContext bundleContext) + { + // track the log service using a ServiceTracker + logTracker = new ServiceTracker( bundleContext, LOG_SERVICE_NAME , null ); + logTracker.open(); + + // assign the log level + String logLevelProp = bundleContext.getProperty( CM_LOG_LEVEL ); + if ( logLevelProp == null ) + { + logLevel = CM_LOG_LEVEL_DEFAULT; + } + else + { + try + { + logLevel = Integer.parseInt( logLevelProp ); + } + catch ( NumberFormatException nfe ) + { + logLevel = CM_LOG_LEVEL_DEFAULT; + } + } + } + + /** + * Set the service reference to the configuration admin in order to include this + * in every log message. + * @param ref The service reference + */ + public void set(final ServiceReference ref) + { + this.serviceReference = ref; + } + + /** + * Stop the log service tracker and clear the service reference + */ + public void stop() + { + if ( logTracker != null ) + { + logTracker.close(); + logTracker = null; + } + serviceReference = null; + } + + /** + * Is the log level enabled? + * @param level The level + * @return {@code true} if enabled + */ + public boolean isLogEnabled( final int level ) + { + return level <= logLevel; + } + + /** + * Log a message in the given level. + * If arguments are provided and contain a {@code ServiceReference} then + * the argument is replaced with the result of {@link #toString(ServiceReference)}. + * + * @param level The log level + * @param format The message text + * @param args The optional arguments + */ + public void log( final int level, final String format, final Object[] args ) + { + @SuppressWarnings("rawtypes") + final ServiceTracker tracker = this.logTracker; + final Object log = tracker == null ? null : tracker.getService(); + if ( log != null || isLogEnabled( level ) ) + { + Throwable throwable = null; + String message = format; + + if ( args != null && args.length > 0 ) + { + for(int i=0; i)args[i]); + } + } + if ( args[args.length - 1] instanceof Throwable ) + { + throwable = ( Throwable ) args[args.length - 1]; + } + message = MessageFormat.format( format, args ); + } + + log( level, message, throwable ); + } + } + + /** + * Log the message with the given level and throwable. + * @param level The log level + * @param message The message + * @param t The exception + */ + public void log( final int level, final String message, final Throwable t ) + { + // log using the LogService if available + @SuppressWarnings("rawtypes") + final ServiceTracker tracker = this.logTracker; + final Object log = tracker == null ? null : tracker.getService(); + if ( log != null ) + { + ( ( LogService ) log ).log( serviceReference, level, message, t ); + return; + } + + // Otherwise only log if more serious than the configured level + if ( isLogEnabled( level ) ) + { + String code; + switch ( level ) + { + case LogService.LOG_INFO: + code = "*INFO *"; + break; + + case LogService.LOG_WARNING: + code = "*WARN *"; + break; + + case LogService.LOG_ERROR: + code = "*ERROR*"; + break; + + case LogService.LOG_DEBUG: + default: + code = "*DEBUG*"; + } + + System.err.println( code + " " + message ); + if ( t != null ) + { + t.printStackTrace( System.err ); + } + } + } + + /** + * Create a string representation of the service reference + * @param ref The service reference + * @return The string representation + */ + private static String toString( final ServiceReference ref ) + { + String[] ocs = ( String[] ) ref.getProperty( "objectClass" ); + StringBuilder buf = new StringBuilder( "[" ); + for ( int i = 0; i < ocs.length; i++ ) + { + buf.append( ocs[i] ); + if ( i < ocs.length - 1 ) + buf.append( ", " ); + } + + buf.append( ", id=" ).append( ref.getProperty( Constants.SERVICE_ID ) ); + + Bundle provider = ref.getBundle(); + if ( provider != null ) + { + buf.append( ", bundle=" ).append( provider.getBundleId() ); + buf.append( '/' ).append( Activator.getLocation(provider) ); + } + else + { + buf.append( ", unregistered" ); + } + + buf.append( "]" ); + return buf.toString(); + } +} + diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/RankingComparator.java b/configadmin/src/main/java/org/apache/felix/cm/impl/RankingComparator.java new file mode 100644 index 00000000000..2f546ff707e --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/RankingComparator.java @@ -0,0 +1,140 @@ +/* + * 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.cm.impl; + + +import java.util.Comparator; + +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.ConfigurationPlugin; + + +/** + * The RankingComparator may be used to maintain sorted + * sets or to sort arrays such that the first element in the set or + * array is the one to use first and the last elements the one to + * use last. + */ +public abstract class RankingComparator implements Comparator> +{ + + /** + * Implements a comparator to sort arrays and sets according to the + * specification of the service.ranking property. This + * results in collections whose first element has the highest ranking + * and the last element has the lowest ranking. Thus the results of + * this comparator are as follows: + *

        + *
      • < 0 if obj1 has higher ranking than obj2
      • + *
      • == 0 if obj1 and obj2 reference the same service
      • + *
      • > 0 if obj1 has lower ranking than obj2
      • + *
      + */ + public static Comparator> SRV_RANKING = new RankingComparator() + { + public int compare( ServiceReference obj1, ServiceReference obj2 ) + { + final long id1 = this.getLong( obj1, Constants.SERVICE_ID ); + final long id2 = this.getLong( obj2, Constants.SERVICE_ID ); + + if ( id1 == id2 ) + { + return 0; + } + + final int rank1 = this.getInteger( obj1, Constants.SERVICE_RANKING ); + final int rank2 = this.getInteger( obj2, Constants.SERVICE_RANKING ); + + if ( rank1 == rank2 ) + { + return ( id1 < id2 ) ? -1 : 1; + } + + return ( rank1 > rank2 ) ? -1 : 1; + } + + }; + + + /** + * Implements a comparator to sort arrays and sets according to the + * specification of the service.cmRanking property in + * the Configuration Admin specification. This results in collections + * where the first element has the lowest ranking value and the last + * element has the highest ranking value. Order amongst elements with + * the same ranking value is left undefined, however we order it + * by service id, lowest last. Thus the results of this + * comparator are as follows: + *
        + *
      • < 0 if obj1 has lower ranking than obj2
      • + *
      • == 0 if obj1 and obj2 have the same ranking
      • + *
      • > 0 if obj1 has higher ranking than obj2
      • + *
      + */ + public static Comparator> CM_RANKING = new RankingComparator() + { + public int compare( ServiceReference obj1, ServiceReference obj2 ) + { + final long id1 = this.getLong( obj1, Constants.SERVICE_ID ); + final long id2 = this.getLong( obj2, Constants.SERVICE_ID ); + + if ( id1 == id2 ) + { + return 0; + } + + final int rank1 = this.getInteger( obj1, ConfigurationPlugin.CM_RANKING ); + final int rank2 = this.getInteger( obj2, ConfigurationPlugin.CM_RANKING ); + + if ( rank1 == rank2 ) + { + return ( id1 > id2 ) ? -1 : 1; + } + + return ( rank1 < rank2 ) ? -1 : 1; + } + + }; + + + protected int getInteger( ServiceReference sr, String property ) + { + Object rankObj = sr.getProperty( property ); + if ( rankObj instanceof Integer ) + { + return ( ( Integer ) rankObj ).intValue(); + } + + // null or not an integer + return 0; + } + + protected long getLong( ServiceReference sr, String property ) + { + Object rankObj = sr.getProperty( property ); + if ( rankObj instanceof Long ) + { + return ( ( Long ) rankObj ).longValue(); + } + + // null or not a long + return 0; + } +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/SimpleFilter.java b/configadmin/src/main/java/org/apache/felix/cm/impl/SimpleFilter.java new file mode 100644 index 00000000000..a10b30eef17 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/SimpleFilter.java @@ -0,0 +1,866 @@ +/* + * 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.cm.impl; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Dictionary; +import java.util.Iterator; +import java.util.List; + +import org.osgi.framework.InvalidSyntaxException; + +public class SimpleFilter +{ + public static final int MATCH_ALL = 0; + public static final int AND = 1; + public static final int OR = 2; + public static final int NOT = 3; + public static final int EQ = 4; + public static final int LTE = 5; + public static final int GTE = 6; + public static final int SUBSTRING = 7; + public static final int PRESENT = 8; + public static final int APPROX = 9; + + private final String m_name; + private final Object m_value; + private final int m_op; + + public SimpleFilter(String attr, Object value, int op) + { + m_name = attr; + m_value = value; + m_op = op; + } + + public String getName() + { + return m_name; + } + + public Object getValue() + { + return m_value; + } + + public int getOperation() + { + return m_op; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + toString(sb); + return sb.toString(); + } + + private void toString(StringBuilder sb) + { + switch (m_op) + { + case AND: + sb.append("(&"); + toString(sb, (List) m_value); + sb.append(")"); + break; + case OR: + sb.append("(|"); + toString(sb, (List) m_value); + sb.append(")"); + break; + case NOT: + sb.append("(!"); + toString(sb, (List) m_value); + sb.append(")"); + break; + case EQ: + sb.append("(") + .append(m_name) + .append("="); + toEncodedString(sb, m_value); + sb.append(")"); + break; + case LTE: + sb.append("(") + .append(m_name) + .append("<="); + toEncodedString(sb, m_value); + sb.append(")"); + break; + case GTE: + sb.append("(") + .append(m_name) + .append(">="); + toEncodedString(sb, m_value); + sb.append(")"); + break; + case SUBSTRING: + sb.append("(").append(m_name).append("="); + unparseSubstring(sb, (List) m_value); + sb.append(")"); + break; + case PRESENT: + sb.append("(").append(m_name).append("=*)"); + break; + case APPROX: + sb.append("(").append(m_name).append("~="); + toEncodedString(sb, m_value); + sb.append(")"); + break; + case MATCH_ALL: + sb.append("(*)"); + break; + } + } + + private static void toString(StringBuilder sb, List list) + { + for (Object o : list) + { + SimpleFilter sf = (SimpleFilter) o; + sf.toString(sb); + } + } + + private static String toDecodedString(String s, int startIdx, int endIdx) + { + StringBuilder sb = new StringBuilder(endIdx - startIdx); + boolean escaped = false; + for (int i = 0; i < (endIdx - startIdx); i++) + { + char c = s.charAt(startIdx + i); + if (!escaped && (c == '\\')) + { + escaped = true; + } + else + { + escaped = false; + sb.append(c); + } + } + + return sb.toString(); + } + + private static void toEncodedString(StringBuilder sb, Object o) + { + if (o instanceof String) + { + String s = (String) o; + for (int i = 0; i < s.length(); i++) + { + char c = s.charAt(i); + if ((c == '\\') || (c == '(') || (c == ')') || (c == '*')) + { + sb.append('\\'); + } + sb.append(c); + } + } + else + { + sb.append(o); + } + } + + public static SimpleFilter parse(final String filter) throws InvalidSyntaxException + { + int idx = skipWhitespace(filter, 0); + + if ((filter == null) || (filter.length() == 0) || (idx >= filter.length())) + { + throw new InvalidSyntaxException("Null or empty filter.", filter); + } + else if (filter.charAt(idx) != '(') + { + throw new InvalidSyntaxException("Missing opening parenthesis", filter); + } + + SimpleFilter sf = null; + List stack = new ArrayList(); + boolean isEscaped = false; + while (idx < filter.length()) + { + if (sf != null) + { + throw new InvalidSyntaxException( + "Only one top-level operation allowed", filter); + } + + if (!isEscaped && (filter.charAt(idx) == '(')) + { + // Skip paren and following whitespace. + idx = skipWhitespace(filter, idx + 1); + + if (filter.charAt(idx) == '&') + { + int peek = skipWhitespace(filter, idx + 1); + if (filter.charAt(peek) == '(') + { + idx = peek - 1; + stack.add(0, new SimpleFilter(null, new ArrayList(), SimpleFilter.AND)); + } + else + { + stack.add(0, idx); + } + } + else if (filter.charAt(idx) == '|') + { + int peek = skipWhitespace(filter, idx + 1); + if (filter.charAt(peek) == '(') + { + idx = peek - 1; + stack.add(0, new SimpleFilter(null, new ArrayList(), SimpleFilter.OR)); + } + else + { + stack.add(0, idx); + } + } + else if (filter.charAt(idx) == '!') + { + int peek = skipWhitespace(filter, idx + 1); + if (filter.charAt(peek) == '(') + { + idx = peek - 1; + stack.add(0, new SimpleFilter(null, new ArrayList(), SimpleFilter.NOT)); + } + else + { + stack.add(0, idx); + } + } + else + { + stack.add(0, idx); + } + } + else if (!isEscaped && (filter.charAt(idx) == ')')) + { + Object top = stack.remove(0); + if (top instanceof SimpleFilter) + { + if (!stack.isEmpty() && (stack.get(0) instanceof SimpleFilter)) + { + ((List) ((SimpleFilter) stack.get(0)).m_value).add(top); + } + else + { + sf = (SimpleFilter) top; + } + } + else if (!stack.isEmpty() && (stack.get(0) instanceof SimpleFilter)) + { + ((List) ((SimpleFilter) stack.get(0)).m_value).add( + SimpleFilter.subfilter(filter, (Integer) top, idx)); + } + else + { + sf = SimpleFilter.subfilter(filter, (Integer) top, idx); + } + } + else if (!isEscaped && (filter.charAt(idx) == '\\')) + { + isEscaped = true; + } + else + { + isEscaped = false; + } + + idx = skipWhitespace(filter, idx + 1); + } + + if (sf == null) + { + throw new InvalidSyntaxException("Missing closing parenthesis", filter); + } + + return sf; + } + + private static SimpleFilter subfilter(String filter, int startIdx, int endIdx) throws InvalidSyntaxException + { + final String opChars = "=<>~"; + + // Determine the ending index of the attribute name. + int attrEndIdx = startIdx; + for (int i = 0; i < (endIdx - startIdx); i++) + { + char c = filter.charAt(startIdx + i); + if (opChars.indexOf(c) >= 0) + { + break; + } + else if (!Character.isWhitespace(c)) + { + attrEndIdx = startIdx + i + 1; + } + } + if (attrEndIdx == startIdx) + { + throw new InvalidSyntaxException( + "Missing attribute name: " + filter.substring(startIdx, endIdx), filter); + } + String attr = filter.substring(startIdx, attrEndIdx); + + // Skip the attribute name and any following whitespace. + startIdx = skipWhitespace(filter, attrEndIdx); + + // Determine the operator type. + int op; + switch (filter.charAt(startIdx)) + { + case '=': + op = EQ; + startIdx++; + break; + case '<': + if (filter.charAt(startIdx + 1) != '=') + { + throw new InvalidSyntaxException( + "Unknown operator: " + filter.substring(startIdx, endIdx), filter); + } + op = LTE; + startIdx += 2; + break; + case '>': + if (filter.charAt(startIdx + 1) != '=') + { + throw new InvalidSyntaxException( + "Unknown operator: " + filter.substring(startIdx, endIdx), filter); + } + op = GTE; + startIdx += 2; + break; + case '~': + if (filter.charAt(startIdx + 1) != '=') + { + throw new InvalidSyntaxException( + "Unknown operator: " + filter.substring(startIdx, endIdx), filter); + } + op = APPROX; + startIdx += 2; + break; + default: + throw new InvalidSyntaxException( + "Unknown operator: " + filter.substring(startIdx, endIdx), filter); + } + + // Parse value. + Object value = toDecodedString(filter, startIdx, endIdx); + + // Check if the equality comparison is actually a substring + // or present operation. + if (op == EQ) + { + String valueStr = filter.substring(startIdx, endIdx); + List values = parseSubstring(valueStr); + if ((values.size() == 2) + && (values.get(0).length() == 0) + && (values.get(1).length() == 0)) + { + op = PRESENT; + } + else if (values.size() > 1) + { + op = SUBSTRING; + value = values; + } + } + + return new SimpleFilter(attr, value, op); + } + + public static List parseSubstring(String value) + { + List pieces = new ArrayList(); + StringBuilder ss = new StringBuilder(); + // int kind = SIMPLE; // assume until proven otherwise + boolean wasStar = false; // indicates last piece was a star + boolean leftstar = false; // track if the initial piece is a star + boolean rightstar = false; // track if the final piece is a star + + int idx = 0; + + // We assume (sub)strings can contain leading and trailing blanks + boolean escaped = false; + loop: for (;;) + { + if (idx >= value.length()) + { + if (wasStar) + { + // insert last piece as "" to handle trailing star + rightstar = true; + } + else + { + pieces.add(ss.toString()); + // accumulate the last piece + // note that in the case of + // (cn=); this might be + // the string "" (!=null) + } + ss.setLength(0); + break loop; + } + + // Read the next character and account for escapes. + char c = value.charAt(idx++); + if (!escaped && (c == '*')) + { + // If we have successive '*' characters, then we can + // effectively collapse them by ignoring succeeding ones. + if (!wasStar) + { + if (ss.length() > 0) + { + pieces.add(ss.toString()); // accumulate the pieces + // between '*' occurrences + } + ss.setLength(0); + // if this is a leading star, then track it + if (pieces.isEmpty()) + { + leftstar = true; + } + wasStar = true; + } + } + else if (!escaped && (c == '\\')) + { + escaped = true; + } + else + { + escaped = false; + wasStar = false; + ss.append(c); + } + } + if (leftstar || rightstar || pieces.size() > 1) + { + // insert leading and/or trailing "" to anchor ends + if (rightstar) + { + pieces.add(""); + } + if (leftstar) + { + pieces.add(0, ""); + } + } + return pieces; + } + + public static void unparseSubstring(StringBuilder sb, List pieces) + { + for (int i = 0; i < pieces.size(); i++) + { + if (i > 0) + { + sb.append("*"); + } + toEncodedString(sb, pieces.get(i)); + } + } + + public static boolean compareSubstring(List pieces, String s) + { + // Walk the pieces to match the string + // There are implicit stars between each piece, + // and the first and last pieces might be "" to anchor the match. + // assert (pieces.length > 1) + // minimal case is * + + boolean result = true; + int len = pieces.size(); + + // Special case, if there is only one piece, then + // we must perform an equality test. + if (len == 1) + { + return s.equals(pieces.get(0)); + } + + // Otherwise, check whether the pieces match + // the specified string. + + int index = 0; + + loop: for (int i = 0; i < len; i++) + { + String piece = pieces.get(i); + + // If this is the first piece, then make sure the + // string starts with it. + if (i == 0) + { + if (!s.startsWith(piece)) + { + result = false; + break loop; + } + } + + // If this is the last piece, then make sure the + // string ends with it. + if (i == (len - 1)) + { + if (s.endsWith(piece) && (s.length() >= (index + piece.length()))) + { + result = true; + } + else + { + result = false; + } + break loop; + } + + // If this is neither the first or last piece, then + // make sure the string contains it. + if ((i > 0) && (i < (len - 1))) + { + index = s.indexOf(piece, index); + if (index < 0) + { + result = false; + break loop; + } + } + + // Move string index beyond the matching piece. + index += piece.length(); + } + + return result; + } + + private static int skipWhitespace(String s, int startIdx) + { + int len = s.length(); + while ((startIdx < len) && Character.isWhitespace(s.charAt(startIdx))) + { + startIdx++; + } + return startIdx; + } + + public boolean matches(Dictionary dict) + { + boolean matched = true; + + if (getOperation() == SimpleFilter.MATCH_ALL) + { + matched = true; + } + else if (getOperation() == SimpleFilter.AND) + { + // Evaluate each subfilter against the remaining capabilities. + // For AND we calculate the intersection of each subfilter. + // We can short-circuit the AND operation if there are no + // remaining capabilities. + List sfs = (List) getValue(); + for (int i = 0; matched && (i < sfs.size()); i++) + { + matched = sfs.get(i).matches(dict); + } + } + else if (getOperation() == SimpleFilter.OR) + { + // Evaluate each subfilter against the remaining capabilities. + // For OR we calculate the union of each subfilter. + matched = false; + List sfs = (List) getValue(); + for (int i = 0; !matched && (i < sfs.size()); i++) + { + matched = sfs.get(i).matches(dict); + } + } + else if (getOperation() == SimpleFilter.NOT) + { + // Evaluate each subfilter against the remaining capabilities. + // For OR we calculate the union of each subfilter. + List sfs = (List) getValue(); + for (int i = 0; i < sfs.size(); i++) + { + matched = !sfs.get(i).matches(dict); + } + } + else + { + matched = false; + Object lhs = dict.get( getName() ); + if (lhs != null) + { + matched = compare(lhs, getValue(), getOperation()); + } + } + + return matched; + } + + private static final Class[] STRING_CLASS = new Class[] { String.class }; + + private static boolean compare(Object lhs, Object rhsUnknown, int op) + { + if (lhs == null) + { + return false; + } + + // If this is a PRESENT operation, then just return true immediately + // since we wouldn't be here if the attribute wasn't present. + if (op == SimpleFilter.PRESENT) + { + return true; + } + + // If the type is comparable, then we can just return the + // result immediately. + if (lhs instanceof Comparable) + { + // Spec says SUBSTRING is false for all types other than string. + if ((op == SimpleFilter.SUBSTRING) && !(lhs instanceof String)) + { + return false; + } + + Object rhs; + if (op == SimpleFilter.SUBSTRING) + { + rhs = rhsUnknown; + } + else + { + try + { + rhs = coerceType(lhs, (String) rhsUnknown); + } + catch (Exception ex) + { + return false; + } + } + + switch (op) + { + case SimpleFilter.EQ : + try + { + return (((Comparable) lhs).compareTo(rhs) == 0); + } + catch (Exception ex) + { + return false; + } + case SimpleFilter.GTE : + try + { + return (((Comparable) lhs).compareTo(rhs) >= 0); + } + catch (Exception ex) + { + return false; + } + case SimpleFilter.LTE : + try + { + return (((Comparable) lhs).compareTo(rhs) <= 0); + } + catch (Exception ex) + { + return false; + } + case SimpleFilter.APPROX : + return compareApproximate((lhs), rhs); + case SimpleFilter.SUBSTRING : + return SimpleFilter.compareSubstring((List) rhs, (String) lhs); + default: + throw new RuntimeException( + "Unknown comparison operator: " + op); + } + } + // Booleans do not implement comparable, so special case them. + else if (lhs instanceof Boolean) + { + Object rhs; + try + { + rhs = coerceType(lhs, (String) rhsUnknown); + } + catch (Exception ex) + { + return false; + } + + switch (op) + { + case SimpleFilter.EQ : + case SimpleFilter.GTE : + case SimpleFilter.LTE : + case SimpleFilter.APPROX : + return (lhs.equals(rhs)); + default: + throw new RuntimeException( + "Unknown comparison operator: " + op); + } + } + + // If the LHS is not a comparable or boolean, check if it is an + // array. If so, convert it to a list so we can treat it as a + // collection. + if (lhs.getClass().isArray()) + { + lhs = convertArrayToList(lhs); + } + + // If LHS is a collection, then call compare() on each element + // of the collection until a match is found. + if (lhs instanceof Collection) + { + for (Iterator iter = ((Collection) lhs).iterator(); iter.hasNext(); ) + { + if (compare(iter.next(), rhsUnknown, op)) + { + return true; + } + } + + return false; + } + + // Spec says SUBSTRING is false for all types other than string. + if ((op == SimpleFilter.SUBSTRING) && !(lhs instanceof String)) + { + return false; + } + + // Since we cannot identify the LHS type, then we can only perform + // equality comparison. + try + { + return lhs.equals(coerceType(lhs, (String) rhsUnknown)); + } + catch (Exception ex) + { + return false; + } + } + + private static boolean compareApproximate(Object lhs, Object rhs) + { + if (rhs instanceof String) + { + return removeWhitespace((String) lhs) + .equalsIgnoreCase(removeWhitespace((String) rhs)); + } + else if (rhs instanceof Character) + { + return Character.toLowerCase(((Character) lhs)) + == Character.toLowerCase(((Character) rhs)); + } + return lhs.equals(rhs); + } + + private static String removeWhitespace(String s) + { + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) + { + if (!Character.isWhitespace(s.charAt(i))) + { + sb.append(s.charAt(i)); + } + } + return sb.toString(); + } + + private static Object coerceType(Object lhs, String rhsString) throws Exception + { + // If the LHS expects a string, then we can just return + // the RHS since it is a string. + if (lhs.getClass() == rhsString.getClass()) + { + return rhsString; + } + + // Try to convert the RHS type to the LHS type by using + // the string constructor of the LHS class, if it has one. + Object rhs = null; + try + { + // The Character class is a special case, since its constructor + // does not take a string, so handle it separately. + if (lhs instanceof Character) + { + rhs = rhsString.charAt(0); + } + else + { + // Spec says we should trim number types. + if ((lhs instanceof Number) || (lhs instanceof Boolean)) + { + rhsString = rhsString.trim(); + } + Constructor ctor = lhs.getClass().getConstructor(STRING_CLASS); + ctor.setAccessible(true); + rhs = ctor.newInstance(rhsString); + } + } + catch (Exception ex) + { + throw new Exception( + "Could not instantiate class " + + lhs.getClass().getName() + + " from string constructor with argument '" + + rhsString + "' because " + ex); + } + + return rhs; + } + + /** + * This is an ugly utility method to convert an array of primitives + * to an array of primitive wrapper objects. This method simplifies + * processing LDAP filters since the special case of primitive arrays + * can be ignored. + * @param array An array of primitive types. + * @return An corresponding array using pritive wrapper objects. + **/ + private static List convertArrayToList(Object array) + { + int len = Array.getLength(array); + List list = new ArrayList(len); + for (int i = 0; i < len; i++) + { + list.add(Array.get(array, i)); + } + return list; + } +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/UpdateThread.java b/configadmin/src/main/java/org/apache/felix/cm/impl/UpdateThread.java new file mode 100644 index 00000000000..12842a2577e --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/UpdateThread.java @@ -0,0 +1,218 @@ +/* + * 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.cm.impl; + +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.LinkedList; + +import org.osgi.service.log.LogService; + + +/** + * The UpdateThread is the thread used to update managed services + * and managed service factories as well as to send configuration events. + */ +public class UpdateThread implements Runnable +{ + + // the thread group into which the worker thread will be placed + private final ThreadGroup workerThreadGroup; + + // the thread's base name + private final String workerBaseName; + + // the queue of Runnable instances to be run + private final LinkedList updateTasks; + + // the actual thread + private Thread worker; + + // the access control context + private final AccessControlContext acc; + + public UpdateThread( final ThreadGroup tg, final String name ) + { + this.workerThreadGroup = tg; + this.workerBaseName = name; + this.acc = AccessController.getContext(); + + this.updateTasks = new LinkedList(); + } + + + // waits on Runnable instances coming into the queue. As instances come + // in, this method calls the Runnable.run method, logs any exception + // happening and keeps on waiting for the next Runnable. If the Runnable + // taken from the queue is this thread instance itself, the thread + // terminates. + @Override + public void run() + { + for ( ;; ) + { + Runnable task; + synchronized ( updateTasks ) + { + while ( updateTasks.isEmpty() ) + { + try + { + updateTasks.wait(); + } + catch ( InterruptedException ie ) + { + // don't care + } + } + + task = ( Runnable ) updateTasks.removeFirst(); + } + + // return if the task is this thread itself + if ( task == this ) + { + return; + } + + // otherwise execute the task, log any issues + try + { + // set the thread name indicating the current task + Thread.currentThread().setName( workerBaseName + " (" + task + ")" ); + + Log.logger.log( LogService.LOG_DEBUG, "Running task {0}", new Object[] + { task } ); + + run0(task); + } + catch ( Throwable t ) + { + Log.logger.log( LogService.LOG_ERROR, "Unexpected problem executing task", t ); + } + finally + { + // reset the thread name to "idle" + Thread.currentThread().setName( workerBaseName ); + } + } + } + + void run0(final Runnable task) throws Throwable { + if (System.getSecurityManager() != null) { + try { + AccessController.doPrivileged( + new PrivilegedExceptionAction() { + @Override + public Void run() throws Exception { + task.run(); + return null; + } + }, + acc + ); + } + catch (PrivilegedActionException pae) { + throw pae.getException(); + } + } + else { + task.run(); + } + } + + /** + * Starts processing the queued tasks. This method does nothing if the + * worker has already been started. + */ + synchronized void start() + { + if ( this.worker == null ) + { + Thread workerThread = new Thread( workerThreadGroup, this, workerBaseName ); + workerThread.setDaemon( true ); + workerThread.start(); + this.worker = workerThread; + } + } + + + /** + * Terminates the worker thread and waits for the thread to have processed + * all outstanding events up to and including the termination job. All + * jobs {@link #schedule(Runnable) scheduled} after termination has been + * initiated will not be processed any more. This method does nothing if + * the worker thread is not currently active. + *

      + * If the worker thread does not terminate within 5 seconds it is killed + * by calling the (deprecated) Thread.stop() method. It may + * be that the worker thread may be blocked by a deadlock (it should not, + * though). In this case hope is that Thread.stop() will be + * able to released that deadlock at the expense of one or more tasks to + * not be executed any longer.... In any case an ERROR message is logged + * with the LogService in this situation. + */ + synchronized void terminate() + { + if ( this.worker != null ) + { + Thread workerThread = this.worker; + this.worker = null; + + schedule( this ); + + // wait for all updates to terminate (<= 10 seconds !) + try + { + workerThread.join( 5000 ); + } + catch ( InterruptedException ie ) + { + // don't really care + } + + if ( workerThread.isAlive() ) + { + Log.logger.log( LogService.LOG_ERROR, + "Worker thread {0} did not terminate within 5 seconds; trying to kill", new Object[] + { workerBaseName } ); + workerThread.stop(); + } + } + } + + + // queue the given runnable to be run as soon as possible + void schedule( Runnable update ) + { + synchronized ( updateTasks ) + { + Log.logger.log( LogService.LOG_DEBUG, "Scheduling task {0}", new Object[] + { update } ); + + // append to the task queue + updateTasks.add( update ); + + // notify the waiting thread + updateTasks.notifyAll(); + } + } +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/helper/BaseTracker.java b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/BaseTracker.java new file mode 100644 index 00000000000..238f820dc82 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/BaseTracker.java @@ -0,0 +1,337 @@ +/* + * 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.cm.impl.helper; + + +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.DomainCombiner; +import java.security.Permission; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.List; + +import org.apache.felix.cm.impl.CaseInsensitiveDictionary; +import org.apache.felix.cm.impl.ConfigurationManager; +import org.apache.felix.cm.impl.Log; +import org.apache.felix.cm.impl.RankingComparator; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedService; +import org.osgi.service.cm.ManagedServiceFactory; +import org.osgi.service.log.LogService; +import org.osgi.util.tracker.ServiceTracker; + + +/** + * The BaseTracker is the base class for tracking + * ManagedService and ManagedServiceFactory + * services. It maps their ServiceRegistration to the + * {@link ConfigurationMap} mapping their service PIDs to provided + * configuration. + */ +public abstract class BaseTracker extends ServiceTracker> +{ + protected final ConfigurationManager cm; + + private final boolean managedServiceFactory; + + protected BaseTracker( final ConfigurationManager cm, final boolean managedServiceFactory ) + { + super( cm.getBundleContext(), ( managedServiceFactory ? ManagedServiceFactory.class.getName() + : ManagedService.class.getName() ), null ); + this.cm = cm; + this.managedServiceFactory = managedServiceFactory; + open(); + } + + + @Override + public ConfigurationMap addingService( ServiceReference reference ) + { + Log.logger.log( LogService.LOG_DEBUG, "Registering service {0}", new Object[] + { reference } ); + + final String[] pids = getServicePid( reference ); + final ConfigurationMap configurations = createConfigurationMap( pids ); + configure( reference, pids, configurations ); + return configurations; + } + + + @Override + public void modifiedService( ServiceReference reference, ConfigurationMap service ) + { + Log.logger.log( LogService.LOG_DEBUG, "Modified service {0}", new Object[] + { reference} ); + + String[] pids = getServicePid( reference ); + if ( service.isDifferentPids( pids ) ) + { + service.setConfiguredPids( pids ); + configure( reference, pids, service ); + } + } + + + @Override + public void removedService( ServiceReference reference, ConfigurationMap service ) + { + // just log + Log.logger.log( LogService.LOG_DEBUG, "Unregistering service {0}", new Object[] + { reference } ); + } + + + private void configure( ServiceReference reference, String[] pids, ConfigurationMap configurations ) + { + if ( pids != null ) + { + this.cm.configure( pids, reference, managedServiceFactory, configurations ); + } + } + + + public final List> getServices( final TargetedPID pid ) + { + ServiceReference[] refs = this.getServiceReferences(); + if ( refs != null ) + { + ArrayList> result = new ArrayList>( refs.length ); + for ( ServiceReference ref : refs ) + { + ConfigurationMap map = this.getService( ref ); + if ( map != null + && ( map.accepts( pid.getRawPid() ) || ( map.accepts( pid.getServicePid() ) && pid + .matchesTarget( ref ) ) ) ) + { + result.add( ref ); + } + } + + if ( result.size() > 1 ) + { + Collections.sort( result, RankingComparator.SRV_RANKING ); + } + + return result; + } + + return Collections.emptyList(); + } + + + protected abstract ConfigurationMap createConfigurationMap( String[] pids ); + + /** + * Returns the String to be used as the PID of the service PID for the + * {@link TargetedPID pid} retrieved from the configuration. + *

      + * This method will return {@link TargetedPID#getServicePid()} most of + * the time except if the service PID used for the consumer's service + * registration contains one or more pipe symbols (|). In this case + * {@link TargetedPID#getRawPid()} might be returned. + * + * @param service The reference ot the service for which the service + * PID is to be returned. + * @param pid The {@link TargetedPID} for which to return the service + * PID. + * @return The service PID or null if the service does not + * respond to the targeted PID at all. + */ + public abstract String getServicePid( ServiceReference service, TargetedPID pid ); + + + /** + * Updates the given service with the provided configuration. + *

      + * See the implementations of this method for more information. + * + * @param service The reference to the service to update + * @param configPid The targeted configuration PID + * @param factoryPid The targeted factory PID or null for + * a non-factory configuration + * @param properties The configuration properties, which may be + * null if this is the provisioning call upon + * service registration of a ManagedService + * @param revision The configuration revision or -1 if there is no + * configuration actually to provide. + * @param configurationMap The PID to configuration map for PIDs + * used by the service to update + * + * @see ManagedServiceTracker#provideConfiguration(ServiceReference, TargetedPID, TargetedPID, Dictionary, long, ConfigurationMap) + * @see ManagedServiceFactoryTracker#provideConfiguration(ServiceReference, TargetedPID, TargetedPID, Dictionary, long, ConfigurationMap) + */ + public abstract void provideConfiguration( ServiceReference service, TargetedPID configPid, + TargetedPID factoryPid, Dictionary properties, long revision, + ConfigurationMap configurationMap); + + + /** + * Remove the configuration indicated by the {@code configPid} from + * the service. + * + * @param service The reference to the service from which the + * configuration is to be removed. + * @param configPid The {@link TargetedPID} of the configuration + * @param factoryPid The {@link TargetedPID factory PID} of the + * configuration. This may be {@code null} for a non-factory + * configuration. + */ + public abstract void removeConfiguration( ServiceReference service, TargetedPID configPid, TargetedPID factoryPid); + + + protected final S getRealService( ServiceReference reference ) + { + return this.context.getService( reference ); + } + + + protected final void ungetRealService( ServiceReference reference ) + { + this.context.ungetService( reference ); + } + + + protected final Dictionary getProperties( Dictionary rawProperties, ServiceReference service, + String configPid, String factoryPid ) + { + Dictionary props = new CaseInsensitiveDictionary( rawProperties ); + this.cm.callPlugins( props, service, configPid, factoryPid ); + return props; + } + + + protected final void handleCallBackError( final Throwable error, final ServiceReference target, final TargetedPID pid ) + { + if ( error instanceof ConfigurationException ) + { + final ConfigurationException ce = ( ConfigurationException ) error; + if ( ce.getProperty() != null ) + { + Log.logger.log( LogService.LOG_ERROR, + "{0}: Updating property {1} of configuration {2} caused a problem: {3}", new Object[] + { target , ce.getProperty(), pid, ce.getReason(), ce } ); + } + else + { + Log.logger.log( LogService.LOG_ERROR, "{0}: Updating configuration {1} caused a problem: {2}", + new Object[] + { target, pid, ce.getReason(), ce } ); + } + } + else + { + { + Log.logger.log( LogService.LOG_ERROR, "{0}: Unexpected problem updating configuration {1}", new Object[] + { target, pid, error } ); + } + + } + } + + + /** + * Returns the service.pid property of the service reference as + * an array of strings or null if the service reference does + * not have a service PID property. + *

      + * The service.pid property may be a single string, in which case a single + * element array is returned. If the property is an array of string, this + * array is returned. If the property is a collection it is assumed to be a + * collection of strings and the collection is converted to an array to be + * returned. Otherwise (also if the property is not set) null + * is returned. + * + * @throws NullPointerException + * if reference is null + * @throws ArrayStoreException + * if the service pid is a collection and not all elements are + * strings. + */ + private static String[] getServicePid( ServiceReference reference ) + { + Object pidObj = reference.getProperty( Constants.SERVICE_PID ); + if ( pidObj instanceof String ) + { + return new String[] + { ( String ) pidObj }; + } + else if ( pidObj instanceof String[] ) + { + return ( String[] ) pidObj; + } + else if ( pidObj instanceof Collection ) + { + Collection pidCollection = ( Collection ) pidObj; + return ( String[] ) pidCollection.toArray( new String[pidCollection.size()] ); + } + + return null; + } + + + public static AccessControlContext getAccessControlContext( final Bundle bundle ) + { + return new AccessControlContext(AccessController.getContext(), new CMDomainCombiner(bundle)); + } + + private static class CMDomainCombiner implements DomainCombiner { + private final CMProtectionDomain domain; + + CMDomainCombiner(Bundle bundle) { + + // FELIX-5908 - Eagerly instantiate this class + // to avoid a potential NoClassDefFoundError + this.domain = new CMProtectionDomain(bundle); + } + + @Override + public ProtectionDomain[] combine(ProtectionDomain[] arg0, + ProtectionDomain[] arg1) { + return new ProtectionDomain[] { domain }; + } + + } + + private static class CMProtectionDomain extends ProtectionDomain { + + private final Bundle bundle; + + CMProtectionDomain(Bundle bundle) { + super(null, null); + this.bundle = bundle; + } + + @Override + public boolean implies(Permission permission) { + try { + return bundle.hasPermission(permission); + } catch (IllegalStateException e) { + return false; + } + } + } + +} \ No newline at end of file diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ConfigurationMap.java b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ConfigurationMap.java new file mode 100644 index 00000000000..998a3fb9df4 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ConfigurationMap.java @@ -0,0 +1,167 @@ +/* + * 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.cm.impl.helper; + + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + + +public abstract class ConfigurationMap +{ + private Map configurations; + + + protected ConfigurationMap( final String[] configuredPids ) + { + this.configurations = Collections.emptyMap(); + setConfiguredPids( configuredPids ); + } + + + protected abstract Map createMap( int size ); + + + protected abstract boolean shallTake( TargetedPID configPid, TargetedPID factoryPid, long revision ); + + + protected abstract void record( TargetedPID configPid, TargetedPID factoryPid, long revision ); + + + protected abstract boolean removeConfiguration( TargetedPID configPid, TargetedPID factoryPid ); + + + protected T get( final TargetedPID key ) + { + final String servicePid = getKeyPid( key ); + if ( servicePid != null ) + { + return this.configurations.get( servicePid ); + } + + // the targeted PID does not match here + return null; + } + + + protected void put( final TargetedPID key, final T value ) + { + final String servicePid = getKeyPid( key ); + if ( servicePid != null ) + { + this.configurations.put( servicePid, value ); + } + } + + + protected String getKeyPid( final TargetedPID targetedPid ) + { + // regular use case: service PID is the key + if ( this.accepts( targetedPid.getServicePid() ) ) + { + return targetedPid.getServicePid(); + } + + // the raw PID is the key (if the service PID contains pipes) + if ( this.accepts( targetedPid.getRawPid() ) ) + { + return targetedPid.getRawPid(); + } + + // this is not really expected here + return null; + } + + + /** + * Returns true if this map is foreseen to take a + * configuration with the given service PID. + * + * @param servicePid The service PID of the configuration which is + * the part of the targeted PID without the bundle's symbolic + * name, version, and location; i.e. {@link TargetedPID#getServicePid()} + * + * @return true if this map is configured to take + * configurations for the service PID. + */ + public boolean accepts( final String servicePid ) + { + return configurations.containsKey( servicePid ); + } + + + public void setConfiguredPids( String[] configuredPids ) + { + final Map newConfigs; + if ( configuredPids != null ) + { + newConfigs = this.createMap( configuredPids.length ); + for ( String pid : configuredPids ) + { + newConfigs.put( pid, this.configurations.get( pid ) ); + } + } + else + { + newConfigs = Collections.emptyMap(); + } + this.configurations = newConfigs; + } + + + /** + * Returns true if the set of service PIDs given is + * different from the current set of service PIDs. + *

      + * For comparison a null argument is considered to + * be an empty set of service PIDs. + * + * @param pids The new set of service PIDs to be compared to the + * current set of service PIDs. + * @return true if the set is different + */ + boolean isDifferentPids( final String[] pids ) + { + if ( this.configurations.isEmpty() && pids == null ) + { + return false; + } + else if ( this.configurations.isEmpty() ) + { + return true; + } + else if ( pids == null ) + { + return true; + } + else if ( this.configurations.size() != pids.length ) + { + return true; + } + else + { + Set thisPids = this.configurations.keySet(); + HashSet otherPids = new HashSet( Arrays.asList( pids ) ); + return !thisPids.equals( otherPids ); + } + } +} \ No newline at end of file diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ManagedServiceConfigurationMap.java b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ManagedServiceConfigurationMap.java new file mode 100644 index 00000000000..c5b6f1243c6 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ManagedServiceConfigurationMap.java @@ -0,0 +1,112 @@ +/* + * 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.cm.impl.helper; + + +import java.util.HashMap; +import java.util.Map; + + +class ManagedServiceConfigurationMap extends ConfigurationMap +{ + + protected ManagedServiceConfigurationMap( String[] configuredPids ) + { + super( configuredPids ); + } + + + @Override + protected Map createMap( int size ) + { + return new HashMap( size ); + } + + + @Override + protected boolean shallTake( TargetedPID configPid, TargetedPID factoryPid, long revision ) + { + Entry entry = this.get( configPid ); + + // no configuration assigned yet, take it + if ( entry == null ) + { + return true; + } + + // compare revision numbers if raw PID is the same + if ( configPid.equals( entry.targetedPid ) ) + { + return revision > entry.revision; + } + + // otherwise only take if targeted PID is more binding + return configPid.bindsStronger( entry.targetedPid ); + } + + + @Override + protected boolean removeConfiguration( TargetedPID configPid, TargetedPID factoryPid ) + { + Entry entry = this.get( configPid ); + + // nothing to remove because the service does not know it anyway + if ( entry == null ) + { + return false; + } + + // update if the used targeted PID matches + if ( configPid.equals( entry.targetedPid ) ) + { + return true; + } + + // the config is not assigned and so there must not be a removal + return false; + } + + + @Override + protected void record( TargetedPID configPid, TargetedPID factoryPid, long revision ) + { + final Entry entry = ( revision < 0 ) ? null : new Entry( configPid, revision ); + this.put( configPid, entry ); + } + + static class Entry + { + final TargetedPID targetedPid; + final long revision; + + + Entry( final TargetedPID targetedPid, final long revision ) + { + this.targetedPid = targetedPid; + this.revision = revision; + } + + + @Override + public String toString() + { + return "Entry(pid=" + targetedPid + ",rev=" + revision + ")"; + } + } +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ManagedServiceFactoryConfigurationMap.java b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ManagedServiceFactoryConfigurationMap.java new file mode 100644 index 00000000000..04cce60cd32 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ManagedServiceFactoryConfigurationMap.java @@ -0,0 +1,93 @@ +/* + * 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.cm.impl.helper; + + +import java.util.HashMap; +import java.util.Map; + + +public class ManagedServiceFactoryConfigurationMap extends ConfigurationMap> +{ + + protected ManagedServiceFactoryConfigurationMap( String[] configuredPids ) + { + super( configuredPids ); + } + + + @Override + protected Map> createMap( int size ) + { + return new HashMap>( size ); + } + + + @Override + protected boolean shallTake( TargetedPID configPid, TargetedPID factoryPid, long revision ) + { + Map configs = this.get( factoryPid ); + + // no configuration yet, yes we can + if (configs == null) { + return true; + } + + Long rev = configs.get( configPid ); + + // this config is missing, yes we can + if (rev == null) { + return true; + } + + // finally take if newer + return rev < revision; + } + + + @Override + protected boolean removeConfiguration( TargetedPID configPid, TargetedPID factoryPid ) + { + Map configs = this.get( factoryPid ); + return configs != null && configs.containsKey( configPid ); + } + + + @Override + protected void record( TargetedPID configPid, TargetedPID factoryPid, long revision ) + { + Map configs = this.get( factoryPid ); + + if (configs == null) { + configs = new HashMap( 4 ); + } + + if (revision < 0) { + configs.remove( configPid ); + } else { + configs.put(configPid, revision); + } + + if (configs.size() == 0) { + configs = null; + } + + this.put( factoryPid, configs ); + } +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ManagedServiceFactoryTracker.java b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ManagedServiceFactoryTracker.java new file mode 100644 index 00000000000..135773ee926 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ManagedServiceFactoryTracker.java @@ -0,0 +1,182 @@ +/* + * 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.cm.impl.helper; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Dictionary; + +import org.apache.felix.cm.impl.ConfigurationManager; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedServiceFactory; + +public class ManagedServiceFactoryTracker extends BaseTracker +{ + + public ManagedServiceFactoryTracker( ConfigurationManager cm ) + { + super( cm, true ); + } + + + @Override + protected ConfigurationMap createConfigurationMap( String[] pids ) + { + return new ManagedServiceFactoryConfigurationMap( pids ); + } + + + /** + * Always returns the raw PID because for a ManagedServiceFactory + * the configuration's PID is automatically generated and is not a + * real targeted PID. + */ + @Override + public String getServicePid( ServiceReference service, TargetedPID pid ) + { + return pid.getRawPid(); + } + + + @Override + public void provideConfiguration( ServiceReference reference, TargetedPID configPid, + TargetedPID factoryPid, Dictionary properties, long revision, ConfigurationMap configs ) + { + // Get the ManagedServiceFactory and terminate here if already + // unregistered from the framework concurrently + ManagedServiceFactory service = getRealService( reference ); + if (service == null) { + return; + } + + // Get the Configuration-to-PID map from the parameter or from + // the service tracker. If not available, the service tracker + // already unregistered this service concurrently + if ( configs == null ) + { + configs = this.getService( reference ); + if ( configs == null ) + { + return; + } + } + + // Both the ManagedService to update and the Configuration-to-PID + // are available, so the service can be updated with the + // configuration (which may be null) + + if ( configs.shallTake( configPid, factoryPid, revision ) ) + { + try + { + Dictionary props = getProperties( properties, reference, configPid.toString(), + factoryPid.toString() ); + updated( reference, service, configPid.toString(), props ); + configs.record( configPid, factoryPid, revision ); + } + catch ( Throwable t ) + { + this.handleCallBackError( t, reference, configPid ); + } + finally + { + this.ungetRealService( reference ); + } + } + } + + + @Override + public void removeConfiguration( ServiceReference reference, TargetedPID configPid, + TargetedPID factoryPid ) + { + final ManagedServiceFactory service = this.getRealService( reference ); + final ConfigurationMap configs = this.getService( reference ); + if ( service != null && configs != null) + { + if ( configs.removeConfiguration( configPid, factoryPid ) ) + { + try + { + deleted( reference, service, configPid.toString() ); + configs.record( configPid, factoryPid, -1 ); + } + catch ( Throwable t ) + { + this.handleCallBackError( t, reference, configPid ); + } + finally + { + this.ungetRealService( reference ); + } + } + } + } + + + private void updated( final ServiceReference reference, final ManagedServiceFactory service, final String pid, final Dictionary properties ) + throws ConfigurationException + { + if ( System.getSecurityManager() != null ) + { + try + { + AccessController.doPrivileged( new PrivilegedExceptionAction() + { + public Object run() throws ConfigurationException + { + service.updated( pid, properties ); + return null; + } + }, getAccessControlContext( reference.getBundle() ) ); + } + catch ( PrivilegedActionException e ) + { + throw ( ConfigurationException ) e.getException(); + } + } + else + { + service.updated( pid, properties ); + } + } + + + private void deleted( final ServiceReference reference, final ManagedServiceFactory service, final String pid ) + { + if ( System.getSecurityManager() != null ) + { + AccessController.doPrivileged( new PrivilegedAction() + { + public Object run() + { + service.deleted( pid ); + return null; + } + }, getAccessControlContext( reference.getBundle() ) ); + } + else + { + service.deleted( pid ); + } + } +} \ No newline at end of file diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ManagedServiceTracker.java b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ManagedServiceTracker.java new file mode 100644 index 00000000000..f05b61d980d --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/ManagedServiceTracker.java @@ -0,0 +1,192 @@ +/* + * 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.cm.impl.helper; + + +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.apache.felix.cm.impl.ConfigurationManager; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedService; + + +public class ManagedServiceTracker extends BaseTracker +{ + + private static final Dictionary INITIAL_MARKER = new Hashtable( 0 ); + + + public ManagedServiceTracker( ConfigurationManager cm ) + { + super( cm, false ); + } + + + @Override + protected ConfigurationMap createConfigurationMap( String[] pids ) + { + return new ManagedServiceConfigurationMap( pids ); + } + + + @Override + public String getServicePid( ServiceReference service, TargetedPID pid ) + { + final ConfigurationMap configs = this.getService( service ); + if ( configs != null ) + { + return configs.getKeyPid( pid ); + } + + // this service is not handled... + return null; + } + + + /** + * Provides the given configuration to the managed service. + *

      + * Depending on targeted PIDs this configuration may not actually be + * provided if the service already has more strictly binding + * configuration from a targeted configuration bound. + *

      + * If the revision is a negative value, the provided configuration + * is assigned to the ManagedService in any case without further + * checks. This allows a replacement configuration for a deleted + * or invisible configuration to be assigned without first removing + * the deleted or invisible configuration. + */ + @Override + public void provideConfiguration( ServiceReference service, TargetedPID configPid, + TargetedPID factoryPid, Dictionary properties, long revision, ConfigurationMap configs ) + { + Dictionary supplied = ( properties == null ) ? INITIAL_MARKER : properties; + updateService( service, configPid, supplied, revision, configs ); + } + + + @Override + public void removeConfiguration( ServiceReference service, TargetedPID configPid, + TargetedPID factoryPid ) + { + updateService( service, configPid, null, -1, null ); + } + + + private void updateService( ServiceReference service, final TargetedPID configPid, + Dictionary properties, long revision, ConfigurationMap configs) + { + // Get the ManagedService and terminate here if already + // unregistered from the framework concurrently + final ManagedService srv = this.getRealService( service ); + if (srv == null) { + return; + } + + // Get the Configuration-to-PID map from the parameter or from + // the service tracker. If not available, the service tracker + // already unregistered this service concurrently + if ( configs == null ) + { + configs = this.getService( service ); + if ( configs == null ) + { + return; + } + } + + // Both the ManagedService to update and the Configuration-to-PID + // are available, so the service can be updated with the + // configuration (which may be null) + + boolean doUpdate = false; + if ( properties == null ) + { + doUpdate = configs.removeConfiguration( configPid, null ); + } + else if ( properties == INITIAL_MARKER ) + { + // initial call to ManagedService may supply null properties + properties = null; + revision = -1; + doUpdate = true; + } + else if ( revision < 0 || configs.shallTake( configPid, null, revision ) ) + { + // run the plugins and cause the update + properties = getProperties( properties, service, configPid.toString(), null ); + doUpdate = true; + revision = Math.abs( revision ); + } + else + { + // new configuration is not a better match, don't update + doUpdate = false; + } + + if ( doUpdate ) + { + try + { + updated( service, srv, properties ); + configs.record( configPid, null, revision ); + } + catch ( Throwable t ) + { + this.handleCallBackError( t, service, configPid ); + } + finally + { + this.ungetRealService( service ); + } + } + } + + + private void updated( final ServiceReference reference, final ManagedService service, final Dictionary properties) throws ConfigurationException + { + if ( System.getSecurityManager() != null ) + { + try + { + AccessController.doPrivileged( new PrivilegedExceptionAction() + { + public Object run() throws ConfigurationException + { + service.updated( properties ); + return null; + } + }, getAccessControlContext( reference.getBundle() ) ); + } + catch ( PrivilegedActionException e ) + { + throw ( ConfigurationException ) e.getException(); + } + } + else + { + service.updated( properties ); + } + } +} \ No newline at end of file diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/helper/TargetedPID.java b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/TargetedPID.java new file mode 100644 index 00000000000..e49cc5889fa --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/helper/TargetedPID.java @@ -0,0 +1,241 @@ +/* + * 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.cm.impl.helper; + + +import org.apache.felix.cm.impl.Activator; +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceReference; + + +/** + * The TargetedPID class represents a targeted PID as read + * from a configuration object. + *

      + * For a factory configuration the TargetedPID represents + * the factory PID of the configuration. Otherwise it represents the + * PID itself of the configuration. + */ +public class TargetedPID +{ + + private final String rawPid; + + private final String servicePid; + + private final String symbolicName; + private final String version; + private final String location; + + /** + * The level of binding of this targeted PID: + *

        + *
      • 0 -- this PID is not targeted at all
      • + *
      • 1 -- this PID is targeted by the symbolic name
      • + *
      • 2 -- this PID is targeted by the symbolic name and version
      • + *
      • 3 -- this PID is targeted by the symoblic name, version, and location
      • + *
      + */ + private final short bindingLevel; + + public TargetedPID( final String rawPid ) + { + this.rawPid = rawPid; + + if ( rawPid.indexOf( '|' ) < 0 ) + { + this.servicePid = rawPid; + this.symbolicName = null; + this.version = null; + this.location = null; + this.bindingLevel = 0; + } + else + { + int start = 0; + int end = rawPid.indexOf( '|' ); + this.servicePid = rawPid.substring( start, end ); + + start = end + 1; + end = rawPid.indexOf( '|', start ); + if ( end >= 0 ) + { + this.symbolicName = rawPid.substring( start, end ); + start = end + 1; + end = rawPid.indexOf( '|', start ); + if ( end >= 0 ) + { + this.version = rawPid.substring( start, end ); + this.location = rawPid.substring( end + 1 ); + this.bindingLevel = 3; + } + else + { + this.version = rawPid.substring( start ); + this.location = null; + this.bindingLevel = 2; + } + } + else + { + this.symbolicName = rawPid.substring( start ); + this.version = null; + this.location = null; + this.bindingLevel = 1; + } + } + } + + + /** + * Returns true if the target of this PID (bundle symbolic name, + * version, and location) match the bundle registering the referenced + * service. + *

      + * This method just checks the target not the PID value itself, so + * this method returning true does not indicate whether + * the service actually is registered with a service PID equal to the + * raw PID of this targeted PID. + *

      + * This method also returns false if the service has + * concurrently been unregistered and the registering bundle is now + * null. + * + * @param reference ServiceReference to the registered + * service + * @return true if the referenced service matches the + * target of this PID. + */ + public boolean matchesTarget( ServiceReference reference ) + { + // already unregistered + final Bundle serviceBundle = reference.getBundle(); + if ( serviceBundle == null ) + { + return false; + } + + // This is not really targeted + if ( this.symbolicName == null ) + { + return true; + } + + // bundle symbolic names don't match + if ( !this.symbolicName.equals( serviceBundle.getSymbolicName() ) ) + { + return false; + } + + // no more specific target + if ( this.version == null ) + { + return true; + } + + // bundle version does not match + + if ( !this.version.equals( serviceBundle.getVersion().toString() ) ) + { + return false; + } + + // assert bundle location match + return this.location == null || this.location.equals( Activator.getLocation(serviceBundle) ); + } + + + /** + * Gets the raw PID with which this instance has been created. + *

      + * If an actual service PID contains pipe symbols that PID might be + * considered being targeted PID without it actually being one. This + * method provides access to the raw PID to allow for such services to + * be configured. + */ + public String getRawPid() + { + return rawPid; + } + + + /** + * Returns the service PID of this targeted PID which basically is + * the targeted PID without the targeting information. + */ + public String getServicePid() + { + return servicePid; + } + + + /** + * Returns true if this targeted PID binds stronger than + * the other {@link TargetedPID}. + *

      + * This method assumes both targeted PIDs have already been checked for + * suitability for the bundle encoded in the targetting. + * + * @param other The targeted PID to check whether it is binding stronger + * or not. + * @return true if the other targeted PID + * is binding strong. + */ + boolean bindsStronger( final TargetedPID other ) + { + return this.bindingLevel > other.bindingLevel; + } + + + @Override + public int hashCode() + { + return this.rawPid.hashCode(); + } + + + @Override + public boolean equals( Object obj ) + { + if ( obj == null ) + { + return false; + } + else if ( obj == this ) + { + return true; + } + + // assume equality if same class and raw PID equals + if ( this.getClass() == obj.getClass() ) + { + return this.rawPid.equals( ( ( TargetedPID ) obj ).rawPid ); + } + + // not the same class or different raw PID + return false; + } + + + @Override + public String toString() + { + return this.rawPid; + } +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/persistence/CachingPersistenceManagerProxy.java b/configadmin/src/main/java/org/apache/felix/cm/impl/persistence/CachingPersistenceManagerProxy.java new file mode 100644 index 00000000000..2202593f9eb --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/persistence/CachingPersistenceManagerProxy.java @@ -0,0 +1,344 @@ +/* + * 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.cm.impl.persistence; + + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.apache.felix.cm.PersistenceManager; +import org.apache.felix.cm.impl.CaseInsensitiveDictionary; +import org.apache.felix.cm.impl.SimpleFilter; +import org.osgi.framework.Constants; +import org.osgi.service.cm.ConfigurationAdmin; + + +/** + * The CachingPersistenceManagerProxy adds a caching layer to the + * underlying actual {@link PersistenceManager} implementation. All API calls + * are also (or primarily) routed through a local cache of dictionaries indexed + * by the service.pid. + */ +public class CachingPersistenceManagerProxy implements ExtPersistenceManager +{ + + /** The actual PersistenceManager */ + private final PersistenceManager pm; + + /** Cached dictionaries */ + private final Map cache = new HashMap<>(); + + /** Protecting lock */ + private final ReadWriteLock globalLock = new ReentrantReadWriteLock(); + + /** + * Indicates whether the getDictionaries method has already been called + * and the cache is complete with respect to the contents of the underlying + * persistence manager. + */ + private volatile boolean fullyLoaded; + + /** Factory configuration cache. */ + private final Map> factoryConfigCache = new HashMap<>(); + + /** + * Creates a new caching layer for the given actual {@link PersistenceManager}. + * @param pm The actual {@link PersistenceManager} + */ + public CachingPersistenceManagerProxy( final PersistenceManager pm ) + { + this.pm = pm; + } + + @Override + public PersistenceManager getDelegatee() + { + return pm; + } + + /** + * Remove the configuration with the given PID. This implementation removes + * the entry from the cache before calling the underlying persistence + * manager. + */ + @Override + public void delete( final String pid ) throws IOException + { + Lock lock = globalLock.writeLock(); + try + { + lock.lock(); + final Dictionary props = cache.remove( pid ); + if ( props != null ) + { + final String factoryPid = (String)props.get(ConfigurationAdmin.SERVICE_FACTORYPID); + if ( factoryPid != null ) + { + final Set factoryPids = this.factoryConfigCache.get(factoryPid); + if ( factoryPids != null ) + { + factoryPids.remove(pid); + if ( factoryPids.isEmpty() ) + { + this.factoryConfigCache.remove(factoryPid); + } + } + } + } + pm.delete(pid); + } + finally + { + lock.unlock(); + } + } + + + /** + * Checks whether a dictionary with the given pid exists. First checks for + * the existence in the cache. If not in the cache the underlying + * persistence manager is asked. + */ + @Override + public boolean exists( final String pid ) + { + Lock lock = globalLock.readLock(); + try + { + lock.lock(); + return cache.containsKey( pid ) || ( !fullyLoaded && pm.exists( pid ) ); + } + finally + { + lock.unlock(); + } + } + + + /** + * Returns an Enumeration of Dictionary objects + * representing the configurations stored in the underlying persistence + * managers. The dictionaries returned are guaranteed to contain the + * service.pid property. + *

      + * Note, that each call to this method will return new dictionary objects. + * That is modifying the contents of a dictionary returned from this method + * has no influence on the dictionaries stored in the cache. + */ + @Override + public Enumeration getDictionaries() throws IOException + { + return Collections.enumeration(getDictionaries( null )); + } + + private final CaseInsensitiveDictionary cache(final Dictionary props) + { + final String pid = (String) props.get( Constants.SERVICE_PID ); + CaseInsensitiveDictionary dict = null; + if ( pid != null ) + { + dict = cache.get(pid); + if ( dict == null ) + { + dict = new CaseInsensitiveDictionary(props); + cache.put( pid, dict ); + final String factoryPid = (String)props.get(ConfigurationAdmin.SERVICE_FACTORYPID); + if ( factoryPid != null ) + { + Set factoryPids = this.factoryConfigCache.get(factoryPid); + if ( factoryPids == null ) + { + factoryPids = new HashSet<>(); + this.factoryConfigCache.put(factoryPid, factoryPids); + } + factoryPids.add(pid); + } + } + } + return dict; + } + + @Override + public Collection getDictionaries( final SimpleFilter filter ) throws IOException + { + Lock lock = globalLock.readLock(); + try + { + lock.lock(); + // if not fully loaded, call back to the underlying persistence + // manager and cache all dictionaries whose service.pid is set + if ( !fullyLoaded ) + { + lock.unlock(); + lock = globalLock.writeLock(); + lock.lock(); + if ( !fullyLoaded ) + { + Enumeration fromPm = pm.getDictionaries(); + while ( fromPm.hasMoreElements() ) + { + Dictionary next = (Dictionary) fromPm.nextElement(); + this.cache(next); + } + this.fullyLoaded = true; + } + } + + // Deep copy the configuration to avoid any threading issue + final List configs = new ArrayList<>(); + for (final Dictionary d : cache.values()) + { + if ( d.get( Constants.SERVICE_PID ) != null && ( filter == null || filter.matches( d ) ) ) + { + configs.add( new CaseInsensitiveDictionary( d ) ); + } + } + return configs; + } + finally + { + lock.unlock(); + } + } + + + /** + * Returns the dictionary for the given PID or null if no + * such dictionary is stored by the underlying persistence manager. This + * method caches the returned dictionary for future use after retrieving + * if from the persistence manager. + *

      + * Note, that each call to this method will return new dictionary instance. + * That is modifying the contents of a dictionary returned from this method + * has no influence on the dictionaries stored in the cache. + */ + @Override + public Dictionary load( final String pid ) throws IOException + { + Lock lock = globalLock.readLock(); + try + { + lock.lock(); + CaseInsensitiveDictionary loaded = cache.get( pid ); + if ( loaded == null && !fullyLoaded ) + { + lock.unlock(); + lock = globalLock.writeLock(); + lock.lock(); + loaded = cache.get( pid ); + if ( loaded == null ) + { + final Dictionary props = pm.load( pid ); + if ( props != null ) + { + loaded = this.cache(props); + } + } + } + return loaded == null ? null : new CaseInsensitiveDictionary(loaded); + } + finally + { + lock.unlock(); + } + } + + + /** + * Stores the dictionary in the cache and in the underlying persistence + * manager. This method first calls the underlying persistence manager + * before updating the dictionary in the cache. + *

      + * Note, that actually a copy of the dictionary is stored in the cache. That + * is subsequent modification to the given dictionary has no influence on + * the cached data. + */ + @Override + public void store( final String pid, final Dictionary properties ) throws IOException + { + final Lock lock = globalLock.writeLock(); + try + { + lock.lock(); + pm.store( pid, properties ); + this.cache.remove(pid); + this.cache(properties); + } + finally + { + lock.unlock(); + } + } + + @Override + public Set getFactoryConfigurationPids(final List targetedFactoryPids ) + throws IOException + { + final Set pids = new HashSet<>(); + Lock lock = globalLock.readLock(); + try + { + lock.lock(); + if ( !this.fullyLoaded ) + { + lock.unlock(); + lock = globalLock.writeLock(); + lock.lock(); + if ( !this.fullyLoaded ) + { + final Enumeration fromPm = pm.getDictionaries(); + while ( fromPm.hasMoreElements() ) + { + Dictionary next = (Dictionary) fromPm.nextElement(); + this.cache(next); + } + this.fullyLoaded = true; + } + lock.unlock(); + lock = globalLock.readLock(); + lock.lock(); + } + for(final String targetFactoryPid : targetedFactoryPids) + { + final Set cachedPids = this.factoryConfigCache.get(targetFactoryPid); + if ( cachedPids != null ) + { + pids.addAll(cachedPids); + } + } + } + finally + { + lock.unlock(); + } + return pids; + } +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/persistence/ExtPersistenceManager.java b/configadmin/src/main/java/org/apache/felix/cm/impl/persistence/ExtPersistenceManager.java new file mode 100644 index 00000000000..b872eeb9b26 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/persistence/ExtPersistenceManager.java @@ -0,0 +1,41 @@ +/* + * 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.cm.impl.persistence; + +import java.io.IOException; +import java.util.Collection; +import java.util.Dictionary; +import java.util.List; +import java.util.Set; + +import org.apache.felix.cm.PersistenceManager; +import org.apache.felix.cm.impl.SimpleFilter; + +/** + * Extension of the {@link PersistenceManager}. + */ +public interface ExtPersistenceManager extends PersistenceManager +{ + Collection getDictionaries( SimpleFilter filter ) throws IOException; + + Set getFactoryConfigurationPids( List targetedFactoryPids ) + throws IOException; + + PersistenceManager getDelegatee(); +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/persistence/PersistenceManagerProxy.java b/configadmin/src/main/java/org/apache/felix/cm/impl/persistence/PersistenceManagerProxy.java new file mode 100644 index 00000000000..0fdc1d70b22 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/persistence/PersistenceManagerProxy.java @@ -0,0 +1,247 @@ +/* + * 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.cm.impl.persistence; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.apache.felix.cm.PersistenceManager; +import org.apache.felix.cm.impl.CaseInsensitiveDictionary; +import org.apache.felix.cm.impl.SimpleFilter; +import org.osgi.framework.Constants; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * The PersistenceManagerProxy proxies a persistence + * manager and adds a read/write lock. + */ +public class PersistenceManagerProxy implements ExtPersistenceManager +{ + /** the actual PersistenceManager */ + private final PersistenceManager pm; + + /** protecting lock */ + private final ReadWriteLock globalLock = new ReentrantReadWriteLock(); + + /** + * Creates a new proxy for the given actual {@link PersistenceManager}. + * @param pm The actual {@link PersistenceManager} + */ + public PersistenceManagerProxy( final PersistenceManager pm ) + { + this.pm = pm; + } + + @Override + public PersistenceManager getDelegatee() + { + return pm; + } + + /** + * Remove the configuration with the given PID. This implementation removes + * the entry from the cache before calling the underlying persistence + * manager. + */ + @Override + public void delete( final String pid ) throws IOException + { + Lock lock = globalLock.writeLock(); + try + { + lock.lock(); + pm.delete(pid); + } + finally + { + lock.unlock(); + } + } + + + /** + * Checks whether a dictionary with the given pid exists. First checks for + * the existence in the cache. If not in the cache the underlying + * persistence manager is asked. + */ + @Override + public boolean exists( String pid ) + { + Lock lock = globalLock.readLock(); + try + { + lock.lock(); + return pm.exists( pid ); + } + finally + { + lock.unlock(); + } + } + + + /** + * Returns an Enumeration of Dictionary objects + * representing the configurations stored in the underlying persistence + * managers. The dictionaries returned are garanteed to contain the + * service.pid property. + *

      + * Note, that each call to this method will return new dictionary objects. + * That is modifying the contents of a dictionary returned from this method + * has no influence on the dictionaries stored in the cache. + */ + @Override + public Enumeration getDictionaries() throws IOException + { + return Collections.enumeration(getDictionaries( null )); + } + + @Override + public Collection getDictionaries( final SimpleFilter filter ) throws IOException + { + Lock lock = globalLock.readLock(); + try + { + final Set pids = new HashSet<>(); + final List result = new ArrayList<>(); + + lock.lock(); + Enumeration fromPm = pm.getDictionaries(); + while ( fromPm.hasMoreElements() ) + { + Dictionary next = (Dictionary) fromPm.nextElement(); + String pid = (String) next.get( Constants.SERVICE_PID ); + if ( pid != null && !pids.contains(pid) && ( filter == null || filter.matches( next ) ) ) + { + pids.add(pid); + result.add( new CaseInsensitiveDictionary( next ) ); + } + } + + return result; + } + finally + { + lock.unlock(); + } + } + + + /** + * Returns the dictionary for the given PID or null if no + * such dictionary is stored by the underyling persistence manager. This + * method caches the returned dictionary for future use after retrieving + * if from the persistence manager. + *

      + * Note, that each call to this method will return new dictionary instance. + * That is modifying the contents of a dictionary returned from this method + * has no influence on the dictionaries stored in the cache. + */ + @Override + public Dictionary load( String pid ) throws IOException + { + Lock lock = globalLock.readLock(); + try + { + lock.lock(); + Dictionary loaded = pm.load( pid ); + if ( loaded != null ) + { + return new CaseInsensitiveDictionary( loaded ); + } + return null; + } + finally + { + lock.unlock(); + } + } + + + /** + * Stores the dictionary in the cache and in the underlying persistence + * manager. This method first calls the underlying persistence manager + * before updating the dictionary in the cache. + *

      + * Note, that actually a copy of the dictionary is stored in the cache. That + * is subsequent modification to the given dictionary has no influence on + * the cached data. + */ + @Override + public void store( String pid, Dictionary properties ) throws IOException + { + Lock lock = globalLock.writeLock(); + try + { + lock.lock(); + pm.store( pid, properties ); + } + finally + { + lock.unlock(); + } + } + + @Override + public Set getFactoryConfigurationPids(List targetedFactoryPids) throws IOException { + final Set pids = new HashSet<>(); + Lock lock = globalLock.readLock(); + try + { + lock.lock(); + final Enumeration fromPm = pm.getDictionaries(); + while ( fromPm.hasMoreElements() ) + { + final Dictionary next = (Dictionary) fromPm.nextElement(); + final String pid = (String)next.get(Constants.SERVICE_PID); + if ( pid != null ) + { + final String factoryPid = (String)next.get(ConfigurationAdmin.SERVICE_FACTORYPID); + if ( factoryPid != null ) + { + for(final String targetedFactoryPid : targetedFactoryPids) + { + if ( targetedFactoryPid.equals(factoryPid) ) + { + pids.add(pid); + break; + } + } + } + } + } + } + finally + { + lock.unlock(); + } + return pids; + } + +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/persistence/PersistenceManagerTracker.java b/configadmin/src/main/java/org/apache/felix/cm/impl/persistence/PersistenceManagerTracker.java new file mode 100644 index 00000000000..c4c536166df --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/persistence/PersistenceManagerTracker.java @@ -0,0 +1,374 @@ +/* + * 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.cm.impl.persistence; + + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.apache.felix.cm.NotCachablePersistenceManager; +import org.apache.felix.cm.PersistenceManager; +import org.apache.felix.cm.impl.ConfigurationManager; +import org.apache.felix.cm.impl.Log; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.log.LogService; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +/** + * This tracker tracks registered persistence managers and + * if the required PM becomes available, configuration admin + * is registered. + * Service ranking of registered persistence managers + * is respected. + */ +public class PersistenceManagerTracker + implements ServiceTrackerCustomizer +{ + /** Tracker for the persistence manager. */ + private final ServiceTracker persistenceManagerTracker; + + private final List holders = new ArrayList<>(); + + private final WorkerQueue workerQueue; + + private final BundleContext bundleContext; + + private volatile ConfigurationManager configurationManager; + + // service tracker for optional coordinator + private volatile ServiceTracker coordinatorTracker; + + public PersistenceManagerTracker(final BundleContext bundleContext, + final PersistenceManager defaultPM, + final String pmName ) + throws InvalidSyntaxException + { + this.bundleContext = bundleContext; + if ( pmName != null ) + { + Log.logger.log(LogService.LOG_DEBUG, "Using persistence manager {0}", new Object[] {pmName}); + this.workerQueue = new WorkerQueue(); + this.persistenceManagerTracker = new ServiceTracker<>( bundleContext, + bundleContext.createFilter("(&(" + Constants.OBJECTCLASS + "=" + PersistenceManager.class.getName() + ")(name=" + pmName + "))"), + this ); + this.persistenceManagerTracker.open(); + } + else + { + Log.logger.log(LogService.LOG_DEBUG, "Using default persistence manager", (Object[])null); + this.workerQueue = null; + this.persistenceManagerTracker = null; + this.activate(this.createPersistenceManagerProxy(defaultPM)); + } + } + + /** + * Stop the tracker, stop configuration admin + */ + public void stop( ) + { + if ( this.persistenceManagerTracker != null ) + { + this.workerQueue.stop(); + this.deactivate(); + this.persistenceManagerTracker.close(); + } + else + { + this.deactivate(); + } + } + + private void activate(final ExtPersistenceManager pm) + { + try + { + configurationManager = new ConfigurationManager(pm, bundleContext); + // start coordinator tracker + this.startCoordinatorTracker(); + + final ServiceReference ref = configurationManager.start(); + // update log + Log.logger.set(ref); + + } + catch (final IOException ioe ) + { + Log.logger.log( LogService.LOG_ERROR, "Failure setting up dynamic configuration bindings", ioe ); + } + } + + private void deactivate() + { + this.stopCoordinatorTracker(); + if ( this.configurationManager != null ) + { + this.configurationManager.stop(); + this.configurationManager = null; + } + // update log + Log.logger.set(null); + } + + private ExtPersistenceManager createPersistenceManagerProxy(final PersistenceManager pm) + { + final ExtPersistenceManager extPM; + if ( pm instanceof NotCachablePersistenceManager ) + { + extPM = new PersistenceManagerProxy( pm ); + } + else + { + extPM = new CachingPersistenceManagerProxy( pm ); + } + return extPM; + } + + @Override + public Holder addingService(final ServiceReference reference) + { + final PersistenceManager pm = this.bundleContext.getService(reference); + if ( pm != null ) + { + final ExtPersistenceManager extPM = createPersistenceManagerProxy(pm); + final Holder holder = new Holder(reference, extPM); + + synchronized ( this.holders ) + { + final Holder oldHolder = this.holders.isEmpty() ? null : this.holders.get(0); + this.holders.add(holder); + Collections.sort(holders); + if ( holders.get(0) == holder ) + { + this.workerQueue.enqueue(new Runnable() + { + + @Override + public void run() + { + if ( oldHolder != null ) + { + deactivate(); + } + if (!holder.isActivated()) { + activate(holder.getPersistenceManager()); + holder.activate(); + } + } + }); + } + } + return holder; + } + return null; + } + + + @Override + public void modifiedService(final ServiceReference reference, final Holder holder) + { + // find the old holder, remove, add new holder, sort + synchronized ( this.holders ) + { + final Holder oldHolder = this.holders.isEmpty() ? null : this.holders.get(0); + + this.holders.remove(holder); + this.holders.add(new Holder(reference, holder.getPersistenceManager())); + Collections.sort(this.holders); + if ( holders.get(0) == holder && oldHolder != null && oldHolder.compareTo(holder) != 0 ) + { + this.workerQueue.enqueue(new Runnable() + { + + @Override + public void run() + { + deactivate(); + if (!holder.isActivated()) { + activate(holder.getPersistenceManager()); + holder.activate(); + } + } + }); + } + } + + } + + + @Override + public void removedService(final ServiceReference reference, + final Holder holder) + { + synchronized ( this.holders ) + { + final boolean deactivate = holders.get(0) == holder; + this.holders.remove(holder); + if ( deactivate ) + { + this.workerQueue.enqueue(new Runnable() + { + + @Override + public void run() + { + deactivate(); + if ( !holders.isEmpty() ) + { + Holder h = holders.get(0); + if (!h.isActivated()) { + activate(h.getPersistenceManager()); + h.activate(); + } + } + } + }); + } + } + } + + public static final class Holder implements Comparable + { + private final ServiceReference reference; + + private final ExtPersistenceManager manager; + + // no need to synchronize, as it's changed only in WorkQueue tasks + private boolean activated; + + public Holder(final ServiceReference ref, final ExtPersistenceManager epm) + { + this.reference = ref; + this.manager = epm; + } + + public ExtPersistenceManager getPersistenceManager() + { + return this.manager; + } + + @Override + public int compareTo(final Holder o) + { + // sort, highest first + return -reference.compareTo(o.reference); + } + + public boolean isActivated() { + return activated; + } + + public void activate() { + this.activated = true; + } + + @Override + public int hashCode() + { + return this.reference.hashCode(); + } + + @Override + public boolean equals(final Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null || getClass() != obj.getClass()) + { + return false; + } + final Holder other = (Holder) obj; + return this.reference.equals(other.reference); + } + } + + private void startCoordinatorTracker() + { + this.coordinatorTracker = new ServiceTracker<>(bundleContext, "org.osgi.service.coordinator.Coordinator", + new ServiceTrackerCustomizer() + { + private final SortedMap, Object> sortedServices = new TreeMap<>(); + + @Override + public Object addingService(final ServiceReference reference) + { + final Object srv = bundleContext.getService(reference); + if ( srv != null ) + { + synchronized ( this.sortedServices ) + { + sortedServices.put(reference, srv); + configurationManager.setCoordinator(sortedServices.get(sortedServices.lastKey())); + } + } + return srv; + } + + @Override + public void modifiedService(final ServiceReference reference, final Object srv) { + synchronized ( this.sortedServices ) + { + // update the map, service ranking might have changed + sortedServices.remove(reference); + sortedServices.put(reference, srv); + configurationManager.setCoordinator(sortedServices.get(sortedServices.lastKey())); + } + } + + @Override + public void removedService(final ServiceReference reference, final Object service) { + synchronized ( this.sortedServices ) + { + sortedServices.remove(reference); + if ( sortedServices.isEmpty() ) + { + configurationManager.setCoordinator(null); + } + else + { + configurationManager.setCoordinator(sortedServices.get(sortedServices.lastKey())); + } + } + bundleContext.ungetService(reference); + } + }); + coordinatorTracker.open(); + } + + private void stopCoordinatorTracker() + { + if ( this.coordinatorTracker != null ) + { + this.coordinatorTracker.close(); + this.coordinatorTracker = null; + } + } +} + diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/persistence/WorkerQueue.java b/configadmin/src/main/java/org/apache/felix/cm/impl/persistence/WorkerQueue.java new file mode 100644 index 00000000000..7054757bffb --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/impl/persistence/WorkerQueue.java @@ -0,0 +1,85 @@ +/* + * 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.cm.impl.persistence; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +import org.apache.felix.cm.impl.Log; +import org.osgi.service.log.LogService; + +public class WorkerQueue implements Runnable { + + private final ThreadFactory threadFactory; + + private final List tasks = new ArrayList<>(); + + private volatile Thread backgroundThread; + + private volatile boolean stopped = false; + + public WorkerQueue() { + this.threadFactory = Executors.defaultThreadFactory(); + } + + public void stop() { + synchronized ( this.tasks ) { + this.stopped = true; + } + } + + public void enqueue(final Runnable r) { + synchronized ( this.tasks ) { + if ( !this.stopped ) { + this.tasks.add(r); + if ( this.backgroundThread == null ) { + this.backgroundThread = this.threadFactory.newThread(this); + this.backgroundThread.setDaemon(true); + this.backgroundThread.setName("Apache Felix Configuration Admin Activator Thread"); + this.backgroundThread.start(); + } + } + } + } + + @Override + public void run() { + Runnable r; + do { + r = null; + synchronized ( this.tasks ) { + if ( !this.stopped && !this.tasks.isEmpty() ) { + r = this.tasks.remove(0); + } else { + this.backgroundThread = null; + } + } + if ( r != null ) { + try { + r.run(); + } catch ( final Throwable t) { + // just to be sure our loop never dies + Log.logger.log(LogService.LOG_ERROR, "Error processing task", t); + } + } + } while ( r != null ); + } +} diff --git a/configadmin/src/main/java/org/apache/felix/cm/package-info.java b/configadmin/src/main/java/org/apache/felix/cm/package-info.java new file mode 100644 index 00000000000..297503962e5 --- /dev/null +++ b/configadmin/src/main/java/org/apache/felix/cm/package-info.java @@ -0,0 +1,25 @@ +/* + * 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. + */ + +@org.osgi.annotation.versioning.Version("1.2.0") +package org.apache.felix.cm; + + + + diff --git a/configadmin/src/main/resources/OSGI-INF/permissions.perm b/configadmin/src/main/resources/OSGI-INF/permissions.perm new file mode 100644 index 00000000000..3c9219fdaf0 --- /dev/null +++ b/configadmin/src/main/resources/OSGI-INF/permissions.perm @@ -0,0 +1,56 @@ +# 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. + +# Apache Felix Configuration Admin Service +# Bundle permissions +# see FELIX-4039 +# + +# Imported/Exported packages +# -> MANIFEST.MF +(org.osgi.framework.PackagePermission "org.osgi.service.log" "import") +(org.osgi.framework.PackagePermission "org.osgi.framework" "import") +(org.osgi.framework.PackagePermission "org.osgi.util.tracker" "import") +(org.osgi.framework.PackagePermission "org.osgi.service.cm" "import,exportonly") +(org.osgi.framework.PackagePermission "org.apache.felix.cm" "import,exportonly") +(org.osgi.framework.PackagePermission "org.apache.felix.cm.file" "import,exportonly") + +# General bundle permissions +(java.util.PropertyPermission "felix.cm.*" "read") +(org.osgi.framework.ServicePermission "org.apache.felix.cm.*" "get,register") +(org.osgi.framework.ServicePermission "org.osgi.service.cm.*" "get,register") +(org.osgi.framework.ServicePermission "org.osgi.service.log.LogService" "get") + +# Manage configurations +# -> ConfigurationAdminImpl +(org.osgi.framework.AdminPermission "*" "metadata") +(org.osgi.service.cm.ConfigurationPermission "*" "configure,target") + +# Handle persistent configuration files +# -> FilePersistenceManager +(java.util.PropertyPermission "os.name" "read") +(java.util.PropertyPermission "user.dir" "read") +(java.io.FilePermission "-" "read,write,execute,delete") + +# -> ConfigurationManager +(org.osgi.framework.ServicePermission "org.apache.felix.cm.PersistenceManager" "register") + +# -> BaseTracker.getAccessControlContext +(java.security.SecurityPermission "createAccessControlContext") + +# Coordinator Support +(org.osgi.framework.PackagePermission "org.osgi.service.coordinator" "import") +(org.osgi.framework.ServicePermission "org.osgi.service.coordinator.*" "get") +(org.osgi.service.coordinator.CoordinationPermission "*" "initiate,participate") diff --git a/configadmin/src/test/java/org/apache/felix/cm/MockBundle.java b/configadmin/src/test/java/org/apache/felix/cm/MockBundle.java new file mode 100644 index 00000000000..673589c0168 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/MockBundle.java @@ -0,0 +1,263 @@ +/* + * 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.cm; + + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.security.cert.X509Certificate; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.Version; + + +public class MockBundle implements Bundle +{ + + private final BundleContext context; + private final String location; + + + public MockBundle( BundleContext context, String location ) + { + this.context = context; + this.location = location; + } + + + @Override + public Enumeration findEntries( String arg0, String arg1, boolean arg2 ) + { + return null; + } + + + @Override + public BundleContext getBundleContext() + { + return context; + } + + + @Override + public long getBundleId() + { + return 0; + } + + + @Override + public URL getEntry( String arg0 ) + { + return null; + } + + + @Override + public Enumeration getEntryPaths( String arg0 ) + { + return null; + } + + + @Override + public Dictionary getHeaders() + { + return null; + } + + + @Override + public Dictionary getHeaders( String arg0 ) + { + return null; + } + + + @Override + public long getLastModified() + { + return 0; + } + + + @Override + public String getLocation() + { + return location; + } + + + @Override + public ServiceReference[] getRegisteredServices() + { + return null; + } + + + @Override + public URL getResource( String arg0 ) + { + return null; + } + + + @Override + public Enumeration getResources( String arg0 ) + { + return null; + } + + + @Override + public ServiceReference[] getServicesInUse() + { + return null; + } + + + @Override + public int getState() + { + return 0; + } + + + @Override + public String getSymbolicName() + { + return null; + } + + + @Override + public boolean hasPermission( Object arg0 ) + { + return false; + } + + + @Override + public Class loadClass( String arg0 ) throws ClassNotFoundException + { + throw new ClassNotFoundException( arg0 ); + } + + + @Override + public void start() + { + } + + + @Override + public void stop() + { + } + + + @Override + public void uninstall() + { + } + + + @Override + public void update() + { + } + + + @Override + public void update( InputStream arg0 ) throws BundleException + { + if ( arg0 != null ) + { + try + { + arg0.close(); + } + catch ( IOException ioe ) + { + throw new BundleException( ioe.getMessage(), ioe ); + } + } + } + + + @Override + public void start( int options ) + { + } + + + @Override + public void stop( int options ) + { + } + + + @Override + public int compareTo( Bundle o ) + { + return 0; + } + + + // Framework 1.5 additions + + @Override + public Map> getSignerCertificates( int signersType ) + { + throw new AbstractMethodError( "Not supported on Framework API 1.4; added in Framework API 1.5" ); + } + + + @Override + public Version getVersion() + { + return Version.emptyVersion; + } + + + // Framework 1.6 additions + + @Override + public A adapt( Class type ) + { + throw new AbstractMethodError( "Not supported on Framework API 1.4; added in Framework API 1.6" ); + } + + + @Override + public File getDataFile( String filename ) + { + throw new AbstractMethodError( "Not supported on Framework API 1.4; added in Framework API 1.6" ); + } + +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/MockBundleContext.java b/configadmin/src/test/java/org/apache/felix/cm/MockBundleContext.java new file mode 100644 index 00000000000..981acd5960d --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/MockBundleContext.java @@ -0,0 +1,367 @@ +/* + * 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.cm; + + +import java.io.File; +import java.io.InputStream; +import java.util.Collection; +import java.util.Dictionary; +import java.util.Properties; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleListener; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceObjects; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; + + +/** + * The MockBundleContext is a dummy implementation of the + * BundleContext interface. No methods are implemented here, that + * is all methods have no effect and return null if a return value + * is specified. + *

      + * Extensions may overwrite methods as see fit. + */ +public class MockBundleContext implements BundleContext +{ + + private final Properties properties = new Properties(); + + + public void setProperty( String name, String value ) + { + if ( value == null ) + { + properties.remove( name ); + } + else + { + properties.setProperty( name, value ); + } + } + + + /* + * (non-Javadoc) + * @see + * org.osgi.framework.BundleContext#addBundleListener(org.osgi.framework + * .BundleListener) + */ + @Override + public void addBundleListener( BundleListener arg0 ) + { + } + + + /* + * (non-Javadoc) + * @see + * org.osgi.framework.BundleContext#addFrameworkListener(org.osgi.framework + * .FrameworkListener) + */ + @Override + public void addFrameworkListener( FrameworkListener arg0 ) + { + } + + + /* + * (non-Javadoc) + * @see + * org.osgi.framework.BundleContext#addServiceListener(org.osgi.framework + * .ServiceListener) + */ + @Override + public void addServiceListener( ServiceListener arg0 ) + { + } + + + /* + * (non-Javadoc) + * @see + * org.osgi.framework.BundleContext#addServiceListener(org.osgi.framework + * .ServiceListener, java.lang.String) + */ + @Override + public void addServiceListener( ServiceListener arg0, String arg1 ) + { + } + + + /* + * (non-Javadoc) + * @see org.osgi.framework.BundleContext#createFilter(java.lang.String) + */ + @Override + public Filter createFilter( String arg0 ) + { + return null; + } + + + /* + * (non-Javadoc) + * @see + * org.osgi.framework.BundleContext#getAllServiceReferences(java.lang.String + * , java.lang.String) + */ + @Override + public ServiceReference[] getAllServiceReferences( String arg0, String arg1 ) + { + return null; + } + + + /* + * (non-Javadoc) + * @see org.osgi.framework.BundleContext#getBundle() + */ + @Override + public Bundle getBundle() + { + return null; + } + + + /* + * (non-Javadoc) + * @see org.osgi.framework.BundleContext#getBundle(long) + */ + @Override + public Bundle getBundle( long arg0 ) + { + return null; + } + + + /* + * (non-Javadoc) + * @see org.osgi.framework.BundleContext#getBundles() + */ + @Override + public Bundle[] getBundles() + { + return new Bundle[0]; + } + + + /* + * (non-Javadoc) + * @see org.osgi.framework.BundleContext#getDataFile(java.lang.String) + */ + @Override + public File getDataFile( String arg0 ) + { + return null; + } + + + /* + * (non-Javadoc) + * @see org.osgi.framework.BundleContext#getProperty(java.lang.String) + */ + @Override + public String getProperty( String name ) + { + return properties.getProperty( name ); + } + + + /* + * (non-Javadoc) + * @seeorg.osgi.framework.BundleContext#getService(org.osgi.framework. + * ServiceReference) + */ + @Override + public S getService( ServiceReference reference ) + { + return null; + } + + + /* + * (non-Javadoc) + * @see + * org.osgi.framework.BundleContext#getServiceReference(java.lang.String) + */ + @Override + public ServiceReference getServiceReference( String arg0 ) + { + return null; + } + + + /* + * (non-Javadoc) + * @see + * org.osgi.framework.BundleContext#getServiceReferences(java.lang.String, + * java.lang.String) + */ + @Override + public ServiceReference[] getServiceReferences( String arg0, String arg1 ) + { + return null; + } + + + /* + * (non-Javadoc) + * @see org.osgi.framework.BundleContext#installBundle(java.lang.String) + */ + @Override + public Bundle installBundle( String arg0 ) + { + return null; + } + + + /* + * (non-Javadoc) + * @see org.osgi.framework.BundleContext#installBundle(java.lang.String, + * java.io.InputStream) + */ + @Override + public Bundle installBundle( String arg0, InputStream arg1 ) + { + return null; + } + + + /* + * (non-Javadoc) + * @see org.osgi.framework.BundleContext#registerService(java.lang.String[], + * java.lang.Object, java.util.Dictionary) + */ + @Override + public ServiceRegistration registerService( String[] clazzes, Object service, Dictionary properties ) + { + return null; + } + + + /* + * (non-Javadoc) + * @see org.osgi.framework.BundleContext#registerService(java.lang.String, + * java.lang.Object, java.util.Dictionary) + */ + @Override + public ServiceRegistration registerService( String clazz, Object service, Dictionary properties ) + { + return null; + } + + + /* + * (non-Javadoc) + * @see + * org.osgi.framework.BundleContext#removeBundleListener(org.osgi.framework + * .BundleListener) + */ + @Override + public void removeBundleListener( BundleListener arg0 ) + { + } + + + /* + * (non-Javadoc) + * @see + * org.osgi.framework.BundleContext#removeFrameworkListener(org.osgi.framework + * .FrameworkListener) + */ + @Override + public void removeFrameworkListener( FrameworkListener arg0 ) + { + } + + + /* + * (non-Javadoc) + * @see + * org.osgi.framework.BundleContext#removeServiceListener(org.osgi.framework + * .ServiceListener) + */ + @Override + public void removeServiceListener( ServiceListener arg0 ) + { + } + + + /* + * (non-Javadoc) + * @seeorg.osgi.framework.BundleContext#ungetService(org.osgi.framework. + * ServiceReference) + */ + @Override + public boolean ungetService( ServiceReference reference ) + { + return false; + } + + + @Override + public ServiceRegistration registerService( Class clazz, S service, Dictionary properties ) + { + return null; + } + + + @Override + public ServiceReference getServiceReference( Class clazz ) + { + return null; + } + + + @Override + public Collection> getServiceReferences( Class clazz, String filter ) + { + return null; + } + + + @Override + public Bundle getBundle( String location ) + { + return null; + } + + + @Override + public ServiceRegistration registerService(Class clazz, ServiceFactory factory, + Dictionary properties) + { + return null; + } + + + @Override + public ServiceObjects getServiceObjects(ServiceReference reference) + { + return null; + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/MockLogService.java b/configadmin/src/test/java/org/apache/felix/cm/MockLogService.java new file mode 100644 index 00000000000..bd68fd0f070 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/MockLogService.java @@ -0,0 +1,88 @@ +/* + * 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.cm; + + +import org.osgi.framework.ServiceReference; +import org.osgi.service.log.LogService; + + +/** + * The MockLogService is a very simple log service, which just + * prints the loglevel and message to StdErr. + */ +public class MockLogService implements LogService +{ + + @Override + public void log( int logLevel, String message ) + { + System.err.print( toMessageLine( logLevel, message ) ); + } + + + @Override + public void log( int logLevel, String message, Throwable t ) + { + log( logLevel, message ); + } + + + @Override + public void log( @SuppressWarnings("rawtypes") ServiceReference ref, int logLevel, String message ) + { + log( logLevel, message ); + } + + + @Override + public void log( @SuppressWarnings("rawtypes") ServiceReference ref, int logLevel, String message, Throwable t ) + { + log( logLevel, message ); + } + + + /** + * Helper method to format log level and log message exactly the same as the + * ConfigurationManager.log() does. + */ + public static String toMessageLine( int level, String message ) + { + String messageLine; + switch ( level ) + { + case LogService.LOG_INFO: + messageLine = "*INFO *"; + break; + + case LogService.LOG_WARNING: + messageLine = "*WARN *"; + break; + + case LogService.LOG_ERROR: + messageLine = "*ERROR*"; + break; + + case LogService.LOG_DEBUG: + default: + messageLine = "*DEBUG*"; + } + return messageLine + " " + message + System.getProperty( "line.separator" ); + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/MockNotCachablePersistenceManager.java b/configadmin/src/test/java/org/apache/felix/cm/MockNotCachablePersistenceManager.java new file mode 100644 index 00000000000..4f0cf6a0539 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/MockNotCachablePersistenceManager.java @@ -0,0 +1,85 @@ +/* + * 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.cm; + + +import java.io.IOException; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.cm.impl.CaseInsensitiveDictionary; + + +public class MockNotCachablePersistenceManager implements NotCachablePersistenceManager +{ + + private final Map> configs = new HashMap<>(); + + public Map> getStored() + { + return configs; + } + + @Override + public void delete( String pid ) + { + configs.remove( pid ); + } + + + @Override + public boolean exists( String pid ) + { + return configs.containsKey( pid ); + } + + + @SuppressWarnings("rawtypes") + @Override + public Enumeration getDictionaries() + { + return Collections.enumeration( configs.values() ); + } + + + @SuppressWarnings("rawtypes") + @Override + public Dictionary load( String pid ) throws IOException + { + Dictionary config = configs.get( pid ); + if ( config != null ) + { + return config; + } + + throw new IOException( "No such configuration: " + pid ); + } + + + @SuppressWarnings("unchecked") + @Override + public void store( String pid, @SuppressWarnings("rawtypes") Dictionary properties ) + { + configs.put( pid, new CaseInsensitiveDictionary( properties ) ); + } + +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/MockPersistenceManager.java b/configadmin/src/test/java/org/apache/felix/cm/MockPersistenceManager.java new file mode 100644 index 00000000000..bce61774a07 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/MockPersistenceManager.java @@ -0,0 +1,70 @@ +/* + * 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.cm; + +import java.io.IOException; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +public class MockPersistenceManager implements PersistenceManager +{ + private final Map> configs = new HashMap<>(); + + @Override + public void delete( final String pid ) + { + configs.remove( pid ); + } + + @Override + public boolean exists( final String pid ) + { + return configs.containsKey( pid ); + } + + @SuppressWarnings("rawtypes") + @Override + public Enumeration getDictionaries() + { + return Collections.enumeration( configs.values() ); + } + + @SuppressWarnings("rawtypes") + @Override + public Dictionary load( final String pid ) throws IOException + { + Dictionary config = configs.get( pid ); + if ( config != null ) + { + return config; + } + + throw new IOException( "No such configuration: " + pid ); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public void store( String pid, Dictionary properties ) + { + configs.put( pid, properties ); + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/MockServiceReference.java b/configadmin/src/test/java/org/apache/felix/cm/MockServiceReference.java new file mode 100644 index 00000000000..230d71309b3 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/MockServiceReference.java @@ -0,0 +1,70 @@ +/* + * 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.cm; + + +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceReference; + + +public class MockServiceReference implements ServiceReference +{ + + @Override + public Object getProperty( String key ) + { + return null; + } + + + @Override + public String[] getPropertyKeys() + { + return null; + } + + + @Override + public Bundle getBundle() + { + return null; + } + + + @Override + public Bundle[] getUsingBundles() + { + return null; + } + + + @Override + public boolean isAssignableTo( Bundle bundle, String className ) + { + return false; + } + + + @Override + public int compareTo( Object reference ) + { + return 0; + } + +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/file/ConfigurationHandlerTest.java b/configadmin/src/test/java/org/apache/felix/cm/file/ConfigurationHandlerTest.java new file mode 100644 index 00000000000..4ae76beb214 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/file/ConfigurationHandlerTest.java @@ -0,0 +1,348 @@ +/* + * 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.cm.file; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +public class ConfigurationHandlerTest { + + private static final String SERVICE_PID = "service.pid"; + + private static final String PAR_1 = "mongouri"; + private static final String VAL_1 = "127.0.0.1:27017"; + private static final String PAR_2 = "customBlobStore"; + private static final String VAL_2 = "true"; + + private static final String CONFIG = + "#mongodb URI\n" + + PAR_1 + "=\"" + VAL_1 + "\"\n" + + "\n" + + " # custom datastore\n" + + PAR_2 + "=B\"" + VAL_2 + "\"\n"; + + @Test + public void testComments() throws IOException + { + @SuppressWarnings("unchecked") + final Dictionary dict = ConfigurationHandler.read(new ByteArrayInputStream(CONFIG.getBytes("UTF-8"))); + Assert.assertEquals(2, dict.size()); + Assert.assertEquals(VAL_1, dict.get(PAR_1)); + Assert.assertEquals(VAL_2, dict.get(PAR_2).toString()); + } + + + @Test + public void test_writeArray() throws IOException { + OutputStream out = new ByteArrayOutputStream(); + Dictionary< String, Object> properties = new Hashtable<>(); + properties.put(SERVICE_PID , new String [] {"foo", "bar"}); + ConfigurationHandler.write(out, properties); + String entry = new String(((ByteArrayOutputStream)out).toByteArray(),"UTF-8"); + Assert.assertEquals("service.pid=[ \\\r\n \"foo\", \\\r\n \"bar\", \\\r\n ]\r\n", entry); + } + + @Test + public void test_writeEmptyCollection() throws IOException { + OutputStream out = new ByteArrayOutputStream(); + Dictionary< String, Object> properties = new Hashtable<>(); + properties.put(SERVICE_PID , new ArrayList()); + ConfigurationHandler.write(out, properties); + String entry = new String(((ByteArrayOutputStream)out).toByteArray(),"UTF-8"); + Assert.assertEquals("service.pid=( \\\r\n)\r\n", entry); + } + + @Test + public void test_writeCollection() throws IOException { + OutputStream out = new ByteArrayOutputStream(); + Dictionary< String, Object> properties = new Hashtable<>(); + List list = new ArrayList<>(); + list.add("foo"); + list.add("bar"); + + properties.put(SERVICE_PID , list); + ConfigurationHandler.write(out, properties); + String entry = new String(((ByteArrayOutputStream)out).toByteArray(),"UTF-8"); + Assert.assertEquals("service.pid=( \\\r\n \"foo\", \\\r\n \"bar\", \\\r\n)\r\n", entry); + } + + @Test + public void test_writeSimpleString() throws IOException { + OutputStream out = new ByteArrayOutputStream(); + Dictionary< String, String> properties = new Hashtable<>(); + properties.put(SERVICE_PID, "com.adobe.granite.foo.Bar"); + ConfigurationHandler.write(out, properties); + String entry = new String(((ByteArrayOutputStream)out).toByteArray(),"UTF-8"); + Assert.assertEquals("service.pid=\"com.adobe.granite.foo.Bar\"\r\n", entry); + } + + @Test + public void test_writeInteger() throws IOException { + OutputStream out = new ByteArrayOutputStream(); + Dictionary< String, Integer> properties = new Hashtable<>(); + properties.put(SERVICE_PID, 1000); + ConfigurationHandler.write(out, properties); + String entry = new String(((ByteArrayOutputStream)out).toByteArray(),"UTF-8"); + Assert.assertEquals("service.pid=I\"1000\"\r\n", entry); + } + + @Test + public void test_writeLong() throws IOException { + OutputStream out = new ByteArrayOutputStream(); + Dictionary< String, Long> properties = new Hashtable<>(); + properties.put(SERVICE_PID, 1000L); + ConfigurationHandler.write(out, properties); + String entry = new String(((ByteArrayOutputStream)out).toByteArray(),"UTF-8"); + Assert.assertEquals("service.pid=L\"1000\"\r\n", entry); + } + + @Test + public void test_writeFloat() throws IOException { + OutputStream out = new ByteArrayOutputStream(); + Dictionary< String, Float> properties = new Hashtable<>(); + properties.put(SERVICE_PID, 3.6f); + ConfigurationHandler.write(out, properties); + String entry = new String(((ByteArrayOutputStream)out).toByteArray(),"UTF-8"); + Assert.assertEquals("service.pid=F\"1080452710\"\r\n", entry); + } + + @Test + public void test_writeDouble() throws IOException { + OutputStream out = new ByteArrayOutputStream(); + Dictionary< String, Double> properties = new Hashtable<>(); + properties.put(SERVICE_PID, 3.6d); + ConfigurationHandler.write(out, properties); + String entry = new String(((ByteArrayOutputStream)out).toByteArray(),"UTF-8"); + Assert.assertEquals("service.pid=D\"4615288898129284301\"\r\n", entry); + } + + @Test + public void test_writeByte() throws IOException { + OutputStream out = new ByteArrayOutputStream(); + Dictionary< String, Byte> properties = new Hashtable<>(); + properties.put(SERVICE_PID, new Byte("10")); + ConfigurationHandler.write(out, properties); + String entry = new String(((ByteArrayOutputStream)out).toByteArray(),"UTF-8"); + Assert.assertEquals("service.pid=X\"10\"\r\n", entry); + } + + @Test + public void test_writeShort() throws IOException { + OutputStream out = new ByteArrayOutputStream(); + Dictionary< String, Short> properties = new Hashtable<>(); + properties.put(SERVICE_PID, (short)10); + ConfigurationHandler.write(out, properties); + String entry = new String(((ByteArrayOutputStream)out).toByteArray(),"UTF-8"); + Assert.assertEquals("service.pid=S\"10\"\r\n", entry); + } + + @Test + public void test_writeChar() throws IOException { + OutputStream out = new ByteArrayOutputStream(); + Dictionary< String, Character> properties = new Hashtable<>(); + properties.put(SERVICE_PID, 'c'); + ConfigurationHandler.write(out, properties); + String entry = new String(((ByteArrayOutputStream)out).toByteArray(),"UTF-8"); + Assert.assertEquals("service.pid=C\"c\"\r\n", entry); + } + + @Test + public void test_writeBoolean() throws IOException { + OutputStream out = new ByteArrayOutputStream(); + Dictionary< String, Boolean> properties = new Hashtable<>(); + properties.put(SERVICE_PID, true); + ConfigurationHandler.write(out, properties); + String entry = new String(((ByteArrayOutputStream)out).toByteArray(),"UTF-8"); + Assert.assertEquals("service.pid=B\"true\"\r\n", entry); + } + + @Test + public void test_writeSimpleStringWithError() throws IOException { + OutputStream out = new ByteArrayOutputStream(); + Dictionary< String, String> properties = new Hashtable<>(); + properties.put("foo.bar", "com.adobe.granite.foo.Bar"); + ConfigurationHandler.write(out, properties); + String entry = new String(((ByteArrayOutputStream)out).toByteArray(),"UTF-8"); + Assert.assertEquals("foo.bar=\"com.adobe.granite.foo.Bar\"\r\n", entry); + } + + @Test + public void test_readArray() throws IOException { + String entry = "service.pid=[ \\\r\n \"foo\", \\\r\n \"bar\", \\\r\n ]\r\n"; + InputStream stream = new ByteArrayInputStream(entry.getBytes(StandardCharsets.UTF_8)); + @SuppressWarnings("unchecked") + Dictionary dictionary = ConfigurationHandler.read(stream); + Assert.assertEquals(1, dictionary.size()); + Assert.assertArrayEquals(new String [] {"foo", "bar"}, (String [])dictionary.get(SERVICE_PID)); + } + + @Test + public void test_readEmptyCollection() throws IOException { + String entry = "service.pid=( \\\r\n)\r\n"; + InputStream stream = new ByteArrayInputStream(entry.getBytes(StandardCharsets.UTF_8)); + @SuppressWarnings("unchecked") + Dictionary dictionary = ConfigurationHandler.read(stream); + Assert.assertEquals(1, dictionary.size()); + Assert.assertEquals(new ArrayList(), dictionary.get(SERVICE_PID)); + } + + @Test + public void test_readCollection() throws IOException { + String entry = "service.pid=( \\\r\n \"foo\", \\\r\n \"bar\", \\\r\n)\r\n"; + InputStream stream = new ByteArrayInputStream(entry.getBytes(StandardCharsets.UTF_8)); + @SuppressWarnings("unchecked") + Dictionary dictionary = ConfigurationHandler.read(stream); + Assert.assertEquals(1, dictionary.size()); + List list = new ArrayList<>(); + list.add("foo"); + list.add("bar"); + Assert.assertEquals(list, dictionary.get(SERVICE_PID)); + } + + @Test + public void test_readSimpleString() throws IOException { + String entry = "service.pid=\"com.adobe.granite.foo.Bar\"\r\n"; + InputStream stream = new ByteArrayInputStream(entry.getBytes(StandardCharsets.UTF_8)); + @SuppressWarnings("unchecked") + Dictionary dictionary = ConfigurationHandler.read(stream); + Assert.assertEquals(1, dictionary.size()); + Assert.assertEquals( "com.adobe.granite.foo.Bar", dictionary.get(SERVICE_PID)); + } + + @Test + public void test_readSimpleStrings() throws IOException { + String entry = "service.pid=\"com.adobe.granite.foo.Bar\"\r\nfoo.bar=\"com.adobe.granite.foo.Baz\"\r\n"; + InputStream stream = new ByteArrayInputStream(entry.getBytes(StandardCharsets.UTF_8)); + @SuppressWarnings("unchecked") + Dictionary dictionary = ConfigurationHandler.read(stream); + Assert.assertEquals(2, dictionary.size()); + Assert.assertEquals( "com.adobe.granite.foo.Bar", dictionary.get(SERVICE_PID)); + Assert.assertNotNull(dictionary.get("foo.bar")); + } + + @Test + public void test_readInteger() throws IOException { + String entry = "service.pid=I\"1000\"\r\n"; + InputStream stream = new ByteArrayInputStream(entry.getBytes(StandardCharsets.UTF_8)); + @SuppressWarnings("unchecked") + Dictionary dictionary = ConfigurationHandler.read(stream); + Assert.assertEquals(1, dictionary.size()); + Assert.assertEquals( 1000, dictionary.get(SERVICE_PID)); + } + + @Test + public void test_readLong() throws IOException { + String entry = "service.pid=L\"1000\"\r\n"; + InputStream stream = new ByteArrayInputStream(entry.getBytes(StandardCharsets.UTF_8)); + @SuppressWarnings("unchecked") + Dictionary dictionary = ConfigurationHandler.read(stream); + Assert.assertEquals(1, dictionary.size()); + Assert.assertEquals( 1000L, dictionary.get(SERVICE_PID)); + } + + @Test + public void test_readFloat() throws IOException { + String entry = "service.pid=F\"1080452710\"\r\n"; + InputStream stream = new ByteArrayInputStream(entry.getBytes(StandardCharsets.UTF_8)); + @SuppressWarnings("unchecked") + Dictionary dictionary = ConfigurationHandler.read(stream); + Assert.assertEquals(1, dictionary.size()); + Assert.assertEquals( 3.6f, dictionary.get(SERVICE_PID)); + } + + @Test + public void test_readDouble() throws IOException { + String entry = "service.pid=D\"4615288898129284301\"\r\n"; + InputStream stream = new ByteArrayInputStream(entry.getBytes(StandardCharsets.UTF_8)); + @SuppressWarnings("unchecked") + Dictionary dictionary = ConfigurationHandler.read(stream); + Assert.assertEquals(1, dictionary.size()); + Assert.assertEquals( 3.6d, dictionary.get(SERVICE_PID)); + } + + @Test + public void test_readByte() throws IOException { + String entry = "service.pid=X\"10\"\r\n"; + InputStream stream = new ByteArrayInputStream(entry.getBytes(StandardCharsets.UTF_8)); + @SuppressWarnings("unchecked") + Dictionary dictionary = ConfigurationHandler.read(stream); + Assert.assertEquals(1, dictionary.size()); + Assert.assertEquals((byte)10 , dictionary.get(SERVICE_PID)); + } + + @Test + public void test_readShort() throws IOException { + String entry = "service.pid=S\"10\"\r\n"; + InputStream stream = new ByteArrayInputStream(entry.getBytes(StandardCharsets.UTF_8)); + @SuppressWarnings("unchecked") + Dictionary dictionary = ConfigurationHandler.read(stream); + Assert.assertEquals(1, dictionary.size()); + Assert.assertEquals((short)10 , dictionary.get(SERVICE_PID)); + } + + @Test + public void test_readChar() throws IOException { + String entry = "service.pid=C\"c\"\r\n"; + InputStream stream = new ByteArrayInputStream(entry.getBytes(StandardCharsets.UTF_8)); + @SuppressWarnings("unchecked") + Dictionary dictionary = ConfigurationHandler.read(stream); + Assert.assertEquals(1, dictionary.size()); + Assert.assertEquals('c' , dictionary.get(SERVICE_PID)); + } + + @Test + public void test_readBoolean() throws IOException { + String entry = "service.pid=B\"true\"\r\n"; + InputStream stream = new ByteArrayInputStream(entry.getBytes(StandardCharsets.UTF_8)); + @SuppressWarnings("unchecked") + Dictionary dictionary = ConfigurationHandler.read(stream); + Assert.assertEquals(1, dictionary.size()); + Assert.assertEquals(true , dictionary.get(SERVICE_PID)); + } + + @Test + public void test_backslash() throws IOException { + final String VALUE = "val\\ue\\\\"; + final Dictionary dict = new Hashtable<>(); + dict.put("key", VALUE); + try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) { + ConfigurationHandler.write(out, dict); + + try (final ByteArrayInputStream ins = new ByteArrayInputStream(out.toByteArray())) { + @SuppressWarnings("unchecked") + final Dictionary read = ConfigurationHandler.read(ins); + + Assert.assertNotNull(read.get("key")); + Assert.assertEquals(VALUE, read.get("key")); + } + } + } +} + diff --git a/configadmin/src/test/java/org/apache/felix/cm/file/FilePersistenceManagerConstructorTest.java b/configadmin/src/test/java/org/apache/felix/cm/file/FilePersistenceManagerConstructorTest.java new file mode 100644 index 00000000000..1ba625d1813 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/file/FilePersistenceManagerConstructorTest.java @@ -0,0 +1,193 @@ +/* + * 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.cm.file; + +import static org.junit.Assert.assertEquals; + +import java.io.File; + +import org.apache.felix.cm.MockBundleContext; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.osgi.framework.BundleContext; + +public class FilePersistenceManagerConstructorTest +{ + + /** The current working directory for the tests */ + private static final String TEST_WORKING_DIRECTORY = "target"; + + /** The Bundle Data Area directory for testing */ + private static final String BUNDLE_DATA = "bundleData"; + + /** The Configuration location path for testing */ + private static final String LOCATION_TEST = "test"; + + /** the previous working directory to return to on tearDown */ + private String oldWorkingDirectory; + + @Before + public void setUp() throws Exception + { + String testDir = new File(TEST_WORKING_DIRECTORY).getAbsolutePath(); + + oldWorkingDirectory = System.getProperty( "user.dir" ); + System.setProperty( "user.dir", testDir ); + } + + @After + public void tearDown() throws Exception + { + System.setProperty( "user.dir", oldWorkingDirectory ); + } + + /** + * Test method for {@link org.apache.felix.cm.file.FilePersistenceManager#FilePersistenceManager(java.lang.String)}. + */ + @Test + public void testFilePersistenceManagerString() + { + // variables used in these tests + FilePersistenceManager fpm; + String relPath; + String absPath; + + // with null + fpm = new FilePersistenceManager(null); + assertFpm(fpm, new File(FilePersistenceManager.DEFAULT_CONFIG_DIR) ); + + // with a relative path + relPath = LOCATION_TEST; + fpm = new FilePersistenceManager(relPath); + assertFpm(fpm, new File(relPath) ); + + // with an absolute path + absPath = new File(LOCATION_TEST).getAbsolutePath(); + fpm = new FilePersistenceManager(absPath); + assertFpm(fpm, new File(absPath) ); + } + + + /** + * Test method for {@link org.apache.felix.cm.file.FilePersistenceManager#FilePersistenceManager(org.osgi.framework.BundleContext, java.lang.String)}. + */ + @Test + public void testFilePersistenceManagerBundleContextString() + { + // variables used in these tests + BundleContext bundleContext; + FilePersistenceManager fpm; + String relPath; + String absPath; + File dataArea; + + // first suite: no BundleContext at all + + // with null + fpm = new FilePersistenceManager(null); + assertFpm(fpm, new File(FilePersistenceManager.DEFAULT_CONFIG_DIR) ); + + // with a relative path + relPath = LOCATION_TEST; + fpm = new FilePersistenceManager(relPath); + assertFpm(fpm, new File(relPath) ); + + // with an absolute path + absPath = new File(LOCATION_TEST).getAbsolutePath(); + fpm = new FilePersistenceManager(absPath); + assertFpm(fpm, new File(absPath) ); + + + // second suite: BundleContext without data file + bundleContext = new FilePersistenceManagerBundleContext(null); + + // with null + fpm = new FilePersistenceManager(bundleContext, null); + assertFpm(fpm, new File(FilePersistenceManager.DEFAULT_CONFIG_DIR) ); + + // with a relative path + relPath = LOCATION_TEST; + fpm = new FilePersistenceManager(bundleContext, relPath); + assertFpm(fpm, new File(relPath) ); + + // with an absolute path + absPath = new File(LOCATION_TEST).getAbsolutePath(); + fpm = new FilePersistenceManager(bundleContext, absPath); + assertFpm(fpm, new File(absPath) ); + + + // third suite: BundleContext with relative data file + dataArea = new File(BUNDLE_DATA); + bundleContext = new FilePersistenceManagerBundleContext(dataArea); + + // with null + fpm = new FilePersistenceManager(bundleContext, null); + assertFpm(fpm, new File(dataArea, FilePersistenceManager.DEFAULT_CONFIG_DIR) ); + + // with a relative path + relPath = LOCATION_TEST; + fpm = new FilePersistenceManager(bundleContext, relPath); + assertFpm(fpm, new File(dataArea, relPath) ); + + // with an absolute path + absPath = new File(LOCATION_TEST).getAbsolutePath(); + fpm = new FilePersistenceManager(bundleContext, absPath); + assertFpm(fpm, new File(absPath) ); + + // fourth suite: BundleContext with absolute data file + dataArea = new File(BUNDLE_DATA).getAbsoluteFile(); + bundleContext = new FilePersistenceManagerBundleContext(dataArea); + + // with null + fpm = new FilePersistenceManager(bundleContext, null); + assertFpm(fpm, new File(dataArea, FilePersistenceManager.DEFAULT_CONFIG_DIR) ); + + // with a relative path + relPath = LOCATION_TEST; + fpm = new FilePersistenceManager(bundleContext, relPath); + assertFpm(fpm, new File(dataArea, relPath) ); + + // with an absolute path + absPath = new File(LOCATION_TEST).getAbsolutePath(); + fpm = new FilePersistenceManager(bundleContext, absPath); + assertFpm(fpm, new File(absPath) ); + } + + + private void assertFpm(FilePersistenceManager fpm, File expected) { + assertEquals( expected.getAbsoluteFile(), fpm.getLocation() ); + } + + private static final class FilePersistenceManagerBundleContext extends MockBundleContext { + + private File dataArea; + + private FilePersistenceManagerBundleContext( File dataArea ) + { + this.dataArea = dataArea; + } + + @Override + public File getDataFile( String path ) + { + return (dataArea != null) ? new File(dataArea, path) : null; + } + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/file/FilePersistenceManagerTest.java b/configadmin/src/test/java/org/apache/felix/cm/file/FilePersistenceManagerTest.java new file mode 100644 index 00000000000..411743c7c35 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/file/FilePersistenceManagerTest.java @@ -0,0 +1,354 @@ +/* + * 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.cm.file; + + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Vector; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class FilePersistenceManagerTest +{ + private File file = new File( System.getProperty( "java.io.tmpdir" ), "config" ); + + private FilePersistenceManager fpm; + + @Before + public void setUp() throws Exception + { + fpm = new FilePersistenceManager( file.getAbsolutePath() ); + } + + @After + public void tearDown() throws Exception + { + File[] children = file.listFiles(); + for ( int i = 0; children != null && i < children.length; i++ ) + { + children[i].delete(); + } + file.delete(); + } + + @Test + public void testPidPlain() + { + assertEquals( "plain", fpm.encodePid( "plain" ) ); + assertEquals( "plain" + File.separatorChar + "path", fpm.encodePid( "plain.path" ) ); + assertEquals( "encod%00e8", fpm.encodePid( "encod\u00E8" ) ); + assertEquals( "encod%00e8" + File.separatorChar + "path", fpm.encodePid( "encod\u00E8/path" ) ); + assertEquals( "encode" + File.separatorChar + "%1234" + File.separatorChar + "path", + fpm.encodePid( "encode/\u1234/path" ) ); + assertEquals( "encode" + File.separatorChar + " %0025 " + File.separatorChar + "path", + fpm.encodePid( "encode/ % /path" ) ); + } + + @Test + public void testPidEncodingCollision() { + // assert a == encode(a) ==> encode(a) == encode(encode(a)) + final String plain = "plain"; + assertEquals( plain, fpm.encodePid( plain ) ); + assertEquals( fpm.encodePid( plain ), fpm.encodePid( fpm.encodePid( plain ) ) ); + assertEquals( plain, fpm.encodePid( fpm.encodePid( plain ) ) ); + + // assert a != encode(a) ==> encode(a) != encode(encode(a)) + final String encode = "encod\u00E8"; + final String encoded = "encod%00e8"; + assertEquals( encoded, fpm.encodePid( encode ) ); + assertFalse( encode.equals( fpm.encodePid( encode ) ) ); + assertFalse( fpm.encodePid( encode ).equals( fpm.encodePid( fpm.encodePid( encode ) ) ) ); + assertFalse( encode.equals( fpm.encodePid( fpm.encodePid( encode ) ) ) ); + } + + @Test + public void testPidDeviceNameEncodingWindows() { + // assert proper encoding of windows device file names (FELIX-4302) + String oldOsName = System.getProperty( "os.name" ); + try { + System.setProperty("os.name", "Windows for testing"); + FilePersistenceManager winFpm = new FilePersistenceManager( file.getAbsolutePath() ); + assertEquals("%004cPT1", winFpm.encodePid( "LPT1" )); + assertEquals("%006cpt1", winFpm.encodePid( "lpt1" )); + assertEquals("%0043ON", winFpm.encodePid( "CON" )); + assertEquals("%0050RN", winFpm.encodePid( "PRN" )); + assertEquals("%0041UX", winFpm.encodePid( "AUX" )); + assertEquals("CLOCK%0024", winFpm.encodePid( "CLOCK$" )); + assertEquals("%004eUL", winFpm.encodePid( "NUL" )); + assertEquals("%0043OM6", winFpm.encodePid( "COM6" )); + } finally { + System.setProperty( "os.name", oldOsName ); + } + } + + @Test + public void testPidDeviceNameEncodingNonWindows() { + // assert no encoding of windows device file names (FELIX-4302) + String oldOsName = System.getProperty( "os.name" ); + try { + System.setProperty("os.name", "Unix for testing"); + FilePersistenceManager winFpm = new FilePersistenceManager( file.getAbsolutePath() ); + assertEquals("LPT1", winFpm.encodePid( "LPT1" )); + assertEquals("lpt1", winFpm.encodePid( "lpt1" )); + assertEquals("CON", winFpm.encodePid( "CON" )); + assertEquals("PRN", winFpm.encodePid( "PRN" )); + assertEquals("AUX", winFpm.encodePid( "AUX" )); + assertEquals("CLOCK%0024", winFpm.encodePid( "CLOCK$" )); + assertEquals("NUL", winFpm.encodePid( "NUL" )); + assertEquals("COM6", winFpm.encodePid( "COM6" )); + } finally { + System.setProperty( "os.name", oldOsName ); + } + } + + @Test + public void testCreateDir() + { + assertTrue( file.isDirectory() ); + } + + + @Test + public void testSimple() throws IOException + { + check( "String", "String Value" ); + check( "Integer", new Integer( 2 ) ); + check( "Long", new Long( 2 ) ); + check( "Float", new Float( 2 ) ); + check( "Double", new Double( 2 ) ); + check( "Byte", new Byte( ( byte ) 2 ) ); + check( "Short", new Short( ( short ) 2 ) ); + check( "Character", new Character( 'a' ) ); + check( "Boolean", Boolean.TRUE ); + } + + + @Test + public void testQuoting() throws IOException + { + check( "QuotingSeparators", "\\()[]{}.,=\"\"''" ); + check( "QuotingWellKnown", "BSP:\b, TAB:\t, LF:\n, FF:\f, CR:\r" ); + check( "QuotingControl", new String( new char[] + { 5, 10, 32, 64 } ) ); + } + + + @Test + public void testArray() throws IOException + { + check( "StringArray", new String[] + { "one", "two", "three" } ); + check( "IntArray", new int[] + { 0, 1, 2 } ); + check( "IntegerArray", new Integer[] + { new Integer( 0 ), new Integer( 1 ), new Integer( 2 ) } ); + } + + + @Test + public void testEmptyArray() throws IOException + { + check( "StringArray", new String[0] ); + check( "IntArray", new int[0] ); + check( "CharArray", new char[0] ); + check( "ShortArray", new short[0] ); + } + + + @Test + public void testVector() throws IOException + { + check( "StringVector", new Vector<>( Arrays.asList( new String[] + { "one", "two", "three" } ) ) ); + check( "IntegerVector", new Vector<>( Arrays.asList( new Integer[] + { new Integer( 0 ), new Integer( 1 ), new Integer( 2 ) } ) ) ); + } + + + @Test + public void testEmptyVector() throws IOException + { + check( "StringArray", new Vector() ); + check( "IntArray", new Vector() ); + check( "CharArray", new Vector() ); + check( "ShortArray", new Vector() ); + } + + + @Test + public void testList() throws IOException + { + check( "StringList", Arrays.asList( new String[] + { "one", "two", "three" } ) ); + check( "IntegerList", Arrays.asList( new Integer[] + { new Integer( 0 ), new Integer( 1 ), new Integer( 2 ) } ) ); + } + + + @Test + public void testEmptyList() throws IOException + { + check( "StringArray", new ArrayList(0) ); + check( "IntArray", new ArrayList(0) ); + check( "CharArray", new ArrayList(0) ); + check( "ShortArray", new ArrayList(0) ); + } + + + @Test + public void testMultiValue() throws IOException + { + Dictionary props = new Hashtable<>(); + props.put( "String", "String Value" ); + props.put( "Integer", new Integer( 2 ) ); + props.put( "Long", new Long( 2 ) ); + props.put( "Float", new Float( 2 ) ); + props.put( "Double", new Double( 2 ) ); + props.put( "Byte", new Byte( ( byte ) 2 ) ); + props.put( "Short", new Short( ( short ) 2 ) ); + props.put( "Character", new Character( 'a' ) ); + props.put( "Boolean", Boolean.TRUE ); + props.put( "Array", new boolean[] + { true, false } ); + + check( "MultiValue", props ); + } + + + // test configuration keys not conforming to the recommended specification + // for configuration keys in OSGi CM 1.3, 104.4.2, Configuration Properties + @Test + public void testNonSpecKeys() throws IOException { + check( "with\ttab", "the value" ); + check( "with blank", "the value" ); + check( "\\()[]{}.,=\"\"''", "quoted key" ); + check( "\"with quotes\"", "key with quotes" ); + check( "=leading equals", "leading equals" ); + } + + + // Test expected to always succeed on non-Windows platforms. It may + // break if FilePersistenceManager.encode does not cope properly + // with Windows device names (see FELIX-4302) + @Test + public void testWindowsSpecialNames() throws IOException + { + check( "prefixLPT1", "lpt1" ); + check( "prefix.prefix2.LPT1.suffix", "lpt1" ); + check( "prefix.LPT1.suffix", "lpt1" ); + check( "prefix.LPT1", "lpt1" ); + check( "LPT1", "lpt1" ); + } + + @Test + public void testKeyOrderInFile() throws IOException + { + Dictionary props = new Hashtable<>(); + // The following keys are stored as "c, a, b" in HashTable based + // due to their hash code + props.put( "a_first", "a" ); + props.put( "b_second", "b" ); + props.put( "c_third", "c" ); + + String pid = "keyOrderInFile"; + fpm.store( pid, props ); + File configFile = new File( file, fpm.encodePid( pid ) + ".config" ); + FileReader reader = new FileReader( configFile ); + BufferedReader breader = new BufferedReader(reader); + try + { + String previousLine = breader.readLine(); + while ( previousLine != null) + { + String line = breader.readLine(); + if (line != null) { + assertTrue( previousLine.compareTo( line ) < 0 ); + } + previousLine = line; + } + } + finally + { + breader.close(); + } + } + + private void check( String name, Object value ) throws IOException + { + Dictionary props = new Hashtable<>(); + props.put( name, value ); + + check( name, props ); + } + + + private void check( String pid, Dictionary props ) throws IOException + { + fpm.store( pid, props ); + + assertTrue( new File( file, fpm.encodePid( pid ) + ".config" ).exists() ); + + @SuppressWarnings("unchecked") + Dictionary loaded = fpm.load( pid ); + assertNotNull( loaded ); + assertEquals( props.size(), loaded.size() ); + + for ( Enumeration pe = props.keys(); pe.hasMoreElements(); ) + { + String key = pe.nextElement(); + checkValues( props.get( key ), loaded.get( key ) ); + } + } + + + private void checkValues( Object value1, Object value2 ) + { + assertNotNull( value2 ); + if ( value1.getClass().isArray() ) + { + assertTrue( value2.getClass().isArray() ); + assertEquals( value1.getClass().getComponentType(), value2.getClass().getComponentType() ); + assertEquals( Array.getLength( value1 ), Array.getLength( value2 ) ); + for ( int i = 0; i < Array.getLength( value1 ); i++ ) + { + assertEquals( Array.get( value1, i ), Array.get( value2, i ) ); + } + } + else + { + assertEquals( value1, value2 ); + } + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/impl/CaseInsensitiveDictionaryTest.java b/configadmin/src/test/java/org/apache/felix/cm/impl/CaseInsensitiveDictionaryTest.java new file mode 100644 index 00000000000..2d6e31eea7f --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/impl/CaseInsensitiveDictionaryTest.java @@ -0,0 +1,293 @@ +/* + * 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.cm.impl; + + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Vector; + +import org.junit.Test; + + +public class CaseInsensitiveDictionaryTest +{ + + @Test + public void testLocaleIndependence() { + Locale defaultLocal = Locale.getDefault(); + CaseInsensitiveDictionary dict = new CaseInsensitiveDictionary(); + dict.put("illegal", "value1"); + dict.put("ILLEGAL", "value2"); + assertEquals(dict.get("illegal"), "value2"); + assertEquals(dict.get("ILLEGAL"), "value2"); + + // validate "i" conversion with Turkish default locale + Locale.setDefault(new Locale("tr", "" ,"")); + try { + dict = new CaseInsensitiveDictionary(); + dict.put("illegal", "value1"); + dict.put("ILLEGAL", "value2"); + assertEquals(dict.get("illegal"), "value2"); + assertEquals(dict.get("ILLEGAL"), "value2"); + } finally { + Locale.setDefault(defaultLocal); + } + } + + + @Test + public void testCheckValueNull() + { + // null which must throw IllegalArgumentException + try + { + CaseInsensitiveDictionary.checkValue( null ); + fail( "Expected IllegalArgumentException for null value" ); + } + catch ( IllegalArgumentException iae ) + { + + } + + } + + + @Test + public void testCheckValueSimple() + { + internalTestCheckValue( "String" ); + internalTestCheckValue( new Integer( 1 ) ); + internalTestCheckValue( new Long( 1 ) ); + internalTestCheckValue( new Float( 1 ) ); + internalTestCheckValue( new Double( 1 ) ); + internalTestCheckValue( new Byte( ( byte ) 1 ) ); + internalTestCheckValue( new Short( ( short ) 1 ) ); + internalTestCheckValue( new Character( 'a' ) ); + internalTestCheckValue( Boolean.TRUE ); + } + + + @Test + public void testCheckValueSimpleArray() + { + internalTestCheckValue( new String[] + { "String" } ); + internalTestCheckValue( new Integer[] + { new Integer( 1 ) } ); + internalTestCheckValue( new Long[] + { new Long( 1 ) } ); + internalTestCheckValue( new Float[] + { new Float( 1 ) } ); + internalTestCheckValue( new Double[] + { new Double( 1 ) } ); + internalTestCheckValue( new Byte[] + { new Byte( ( byte ) 1 ) } ); + internalTestCheckValue( new Short[] + { new Short( ( short ) 1 ) } ); + internalTestCheckValue( new Character[] + { new Character( 'a' ) } ); + internalTestCheckValue( new Boolean[] + { Boolean.TRUE } ); + } + + + @Test + public void testCheckValuePrimitiveArray() + { + internalTestCheckValue( new long[] + { 1 } ); + internalTestCheckValue( new int[] + { 1 } ); + internalTestCheckValue( new short[] + { 1 } ); + internalTestCheckValue( new char[] + { 1 } ); + internalTestCheckValue( new byte[] + { 1 } ); + internalTestCheckValue( new double[] + { 1 } ); + internalTestCheckValue( new float[] + { 1 } ); + internalTestCheckValue( new boolean[] + { true } ); + } + + + @Test + public void testCheckValueSimpleVector() + { + internalTestCheckValueVector( "String", String.class ); + internalTestCheckValueVector( new Integer( 1 ), Integer.class ); + internalTestCheckValueVector( new Long( 1 ), Long.class ); + internalTestCheckValueVector( new Float( 1 ), Float.class ); + internalTestCheckValueVector( new Double( 1 ), Double.class ); + internalTestCheckValueVector( new Byte( ( byte ) 1 ), Byte.class ); + internalTestCheckValueVector( new Short( ( short ) 1 ), Short.class ); + internalTestCheckValueVector( new Character( 'a' ), Character.class ); + internalTestCheckValueVector( Boolean.TRUE, Boolean.class ); + } + + @Test + public void testCheckValueSimpleSet() + { + internalTestCheckValueSet( "String", String.class ); + internalTestCheckValueSet( new Integer( 1 ), Integer.class ); + internalTestCheckValueSet( new Long( 1 ), Long.class ); + internalTestCheckValueSet( new Float( 1 ), Float.class ); + internalTestCheckValueSet( new Double( 1 ), Double.class ); + internalTestCheckValueSet( new Byte( ( byte ) 1 ), Byte.class ); + internalTestCheckValueSet( new Short( ( short ) 1 ), Short.class ); + internalTestCheckValueSet( new Character( 'a' ), Character.class ); + internalTestCheckValueSet( Boolean.TRUE, Boolean.class ); + } + + + @Test + public void testCheckValueSimpleArrayList() + { + internalTestCheckValueList( "String", String.class ); + internalTestCheckValueList( new Integer( 1 ), Integer.class ); + internalTestCheckValueList( new Long( 1 ), Long.class ); + internalTestCheckValueList( new Float( 1 ), Float.class ); + internalTestCheckValueList( new Double( 1 ), Double.class ); + internalTestCheckValueList( new Byte( ( byte ) 1 ), Byte.class ); + internalTestCheckValueList( new Short( ( short ) 1 ), Short.class ); + internalTestCheckValueList( new Character( 'a' ), Character.class ); + internalTestCheckValueList( Boolean.TRUE, Boolean.class ); + } + + + private void internalTestCheckValueList( T value, Class collectionType ) + { + Collection coll = new ArrayList<>(); + + coll.add( value ); + internalTestCheckValue( coll ); + } + + private void internalTestCheckValueVector( T value, Class collectionType ) + { + Collection coll = new Vector<>(); + + coll.add( value ); + internalTestCheckValue( coll ); + } + + private void internalTestCheckValueSet( T value, Class collectionType ) + { + Collection coll = new HashSet<>(); + + coll.add( value ); + internalTestCheckValue( coll ); + } + + private void internalTestCheckValue( Object value ) + { + assertEqualValue( value, CaseInsensitiveDictionary.checkValue( value ) ); + } + + + private void assertEqualValue( Object expected, Object actual ) + { + if ( ( expected instanceof Collection ) && ( actual instanceof Collection ) ) + { + Collection eColl = ( Collection ) expected; + Collection aColl = ( Collection ) actual; + if ( eColl.size() != aColl.size() ) + { + fail( "Unexpected size. expected:" + eColl.size() + ", actual: " + aColl.size() ); + } + + // create a list from the expected collection and remove + // all values from the actual collection, this should get + // an empty collection + List eList = new ArrayList<>( eColl ); + eList.removeAll( aColl ); + assertTrue( "Collections do not match. expected:" + eColl + ", actual: " + aColl, eList.isEmpty() ); + } + else + { + assertEquals( expected, actual ); + } + } + + + @Test + public void testValidKeys() + { + CaseInsensitiveDictionary.checkKey( "a" ); + CaseInsensitiveDictionary.checkKey( "1" ); + CaseInsensitiveDictionary.checkKey( "-" ); + CaseInsensitiveDictionary.checkKey( "_" ); + CaseInsensitiveDictionary.checkKey( "A" ); + CaseInsensitiveDictionary.checkKey( "a.b.c" ); + CaseInsensitiveDictionary.checkKey( "a.1.c" ); + CaseInsensitiveDictionary.checkKey( "a-sample.dotted_key.end" ); + } + + + @Test + public void testKeyDots() + { + // FELIX-2184 these keys are all valid (but not recommended) + CaseInsensitiveDictionary.checkKey( "." ); + CaseInsensitiveDictionary.checkKey( "a.b.c." ); + CaseInsensitiveDictionary.checkKey( ".a.b.c." ); + CaseInsensitiveDictionary.checkKey( "a..b" ); + + // valid key as of OSGi Compendium R4.2 (CM 1.3) + CaseInsensitiveDictionary.checkKey( ".a.b.c" ); + } + + @Test + public void testKeyIllegalCharacters() + { + testFailingKey( null ); + testFailingKey( "" ); + + // FELIX-2184 these keys are all valid (but not recommended) + CaseInsensitiveDictionary.checkKey( " " ); + CaseInsensitiveDictionary.checkKey( "§" ); + CaseInsensitiveDictionary.checkKey( "${yikes}" ); + CaseInsensitiveDictionary.checkKey( "a key with spaces" ); + CaseInsensitiveDictionary.checkKey( "fail:key" ); + } + + + private void testFailingKey( String key ) + { + try + { + CaseInsensitiveDictionary.checkKey( key ); + fail( "Expected IllegalArgumentException for key [" + key + "]" ); + } + catch ( IllegalArgumentException iae ) + { + // expected + } + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/impl/ConfigurationAdapterTest.java b/configadmin/src/test/java/org/apache/felix/cm/impl/ConfigurationAdapterTest.java new file mode 100644 index 00000000000..7c9436fb98a --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/impl/ConfigurationAdapterTest.java @@ -0,0 +1,168 @@ +/* + * 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.cm.impl; + + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.apache.felix.cm.MockPersistenceManager; +import org.apache.felix.cm.PersistenceManager; +import org.junit.Test; +import org.mockito.Mockito; +import org.osgi.framework.Constants; +import org.osgi.service.cm.Configuration; + + +public class ConfigurationAdapterTest +{ + + private static final String SCALAR = "scalar"; + private static final String STRING_VALUE = "String Value"; + private static final String STRING_VALUE2 = "Another String Value"; + + private static final String ARRAY = "array"; + private final String[] ARRAY_VALUE; + + private static final String COLLECTION = "collection"; + private final Collection COLLECTION_VALUE; + + private static final String TEST_PID = "test.pid"; + private static final String TEST_LOCATION = "test:location"; + + private final PersistenceManager pm = new MockPersistenceManager(); + + { + ARRAY_VALUE = new String[] + { STRING_VALUE }; + COLLECTION_VALUE = new ArrayList<>(); + COLLECTION_VALUE.add( STRING_VALUE ); + } + + + private Configuration getConfiguration() throws IOException + { + final ConfigurationManager configMgr = Mockito.mock(ConfigurationManager.class); + Mockito.when(configMgr.isActive()).thenReturn(true); + + ConfigurationImpl cimpl = new ConfigurationImpl( configMgr, pm, TEST_PID, null, TEST_LOCATION ); + return new ConfigurationAdapter( null, cimpl ); + } + + + @Test public void testScalar() throws IOException + { + Configuration cimpl = getConfiguration(); + Dictionary props = cimpl.getProperties(); + assertNull( "Configuration is fresh", props ); + + props = new Hashtable<>(); + props.put( SCALAR, STRING_VALUE ); + cimpl.update( props ); + + Dictionary newProps = cimpl.getProperties(); + assertNotNull( "Configuration is not fresh", newProps ); + assertEquals( "Expect 2 elements", 2, newProps.size() ); + assertEquals( "Service.pid must match", TEST_PID, newProps.get( Constants.SERVICE_PID ) ); + assertEquals( "Scalar value must match", STRING_VALUE, newProps.get( SCALAR ) ); + } + + + @Test public void testArray() throws IOException + { + Configuration cimpl = getConfiguration(); + + Dictionary props = cimpl.getProperties(); + assertNull( "Configuration is fresh", props ); + + props = new Hashtable<>(); + props.put( ARRAY, ARRAY_VALUE ); + cimpl.update( props ); + + Dictionary newProps = cimpl.getProperties(); + assertNotNull( "Configuration is not fresh", newProps ); + assertEquals( "Expect 2 elements", 2, newProps.size() ); + assertEquals( "Service.pid must match", TEST_PID, newProps.get( Constants.SERVICE_PID ) ); + + Object testProp = newProps.get( ARRAY ); + assertNotNull( testProp ); + assertTrue( testProp.getClass().isArray() ); + assertEquals( 1, Array.getLength( testProp ) ); + assertEquals( STRING_VALUE, Array.get( testProp, 0 ) ); + + // modify the array property + Array.set( testProp, 0, STRING_VALUE2 ); + + // the array element change must not be reflected in the configuration + Dictionary newProps2 = cimpl.getProperties(); + Object testProp2 = newProps2.get( ARRAY ); + assertNotNull( testProp2 ); + assertTrue( testProp2.getClass().isArray() ); + assertEquals( 1, Array.getLength( testProp2 ) ); + assertEquals( STRING_VALUE, Array.get( testProp2, 0 ) ); + } + + + @SuppressWarnings("unchecked") + @Test public void testCollection() throws IOException + { + Configuration cimpl = getConfiguration(); + + Dictionary props = cimpl.getProperties(); + assertNull( "Configuration is fresh", props ); + + props = new Hashtable<>(); + props.put( COLLECTION, COLLECTION_VALUE ); + cimpl.update( props ); + + Dictionary newProps = cimpl.getProperties(); + assertNotNull( "Configuration is not fresh", newProps ); + assertEquals( "Expect 2 elements", 2, newProps.size() ); + assertEquals( "Service.pid must match", TEST_PID, newProps.get( Constants.SERVICE_PID ) ); + + Object testProp = newProps.get( COLLECTION ); + assertNotNull( testProp ); + assertTrue( testProp instanceof Collection ); + Collection coll = ( Collection ) testProp; + assertEquals( 1, coll.size() ); + assertEquals( STRING_VALUE, coll.iterator().next() ); + + // modify the array property + coll.clear(); + coll.add( STRING_VALUE2 ); + + // the array element change must not be reflected in the configuration + Dictionary newProps2 = cimpl.getProperties(); + Object testProp2 = newProps2.get( COLLECTION ); + assertNotNull( testProp2 ); + assertTrue( testProp2 instanceof Collection ); + Collection coll2 = ( Collection ) testProp2; + assertEquals( 1, coll2.size() ); + assertEquals( STRING_VALUE, coll2.iterator().next() ); + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/impl/ConfigurationImplTest.java b/configadmin/src/test/java/org/apache/felix/cm/impl/ConfigurationImplTest.java new file mode 100644 index 00000000000..0219679d54e --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/impl/ConfigurationImplTest.java @@ -0,0 +1,59 @@ +/* + * 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.cm.impl; + +import java.util.Dictionary; +import java.util.Hashtable; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ConfigurationImplTest { + + @Test public void testEqualsWithArrays() { + final Dictionary props1 = new Hashtable(); + props1.put("array", new long[] {1,2}); + + final Dictionary props2 = new Hashtable(); + props2.put("array", new long[] {1,2}); + + assertTrue(ConfigurationImpl.equals(props1, props2)); + + props2.put("array", new Long[] {1L,2L}); + assertTrue(ConfigurationImpl.equals(props1, props2)); + + final Dictionary props3 = new Hashtable(); + props3.put("array", new long[] {1,2,3}); + assertFalse(ConfigurationImpl.equals(props1, props3)); + + final Dictionary props4 = new Hashtable(); + props3.put("array", new long[] {1}); + assertFalse(ConfigurationImpl.equals(props1, props4)); + } + + @Test public void testEqualsForNull() { + final Dictionary props = new Hashtable(); + assertFalse(ConfigurationImpl.equals(props, null)); + assertFalse(ConfigurationImpl.equals(null, props)); + assertTrue(ConfigurationImpl.equals(null, null)); + + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/impl/ConfigurationManagerTest.java b/configadmin/src/test/java/org/apache/felix/cm/impl/ConfigurationManagerTest.java new file mode 100644 index 00000000000..821ad23cced --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/impl/ConfigurationManagerTest.java @@ -0,0 +1,583 @@ +/* + * 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.cm.impl; + + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.reflect.Field; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; + +import org.apache.felix.cm.MockBundleContext; +import org.apache.felix.cm.MockLogService; +import org.apache.felix.cm.MockNotCachablePersistenceManager; +import org.apache.felix.cm.MockPersistenceManager; +import org.apache.felix.cm.PersistenceManager; +import org.apache.felix.cm.impl.helper.ManagedServiceFactoryTracker; +import org.apache.felix.cm.impl.persistence.CachingPersistenceManagerProxy; +import org.apache.felix.cm.impl.persistence.PersistenceManagerProxy; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.ConfigurationEvent; +import org.osgi.service.cm.SynchronousConfigurationListener; +import org.osgi.service.log.LogService; +import org.osgi.util.tracker.ServiceTracker; + +public class ConfigurationManagerTest +{ + + private PrintStream replacedStdErr; + + private ByteArrayOutputStream output; + + + @Before + public void setUp() throws Exception + { + replacedStdErr = System.err; + + output = new ByteArrayOutputStream(); + System.setErr( new PrintStream( output ) ); + setLogLevel(LogService.LOG_WARNING); + } + + + @After + public void tearDown() throws Exception + { + System.setErr( replacedStdErr ); + } + + @Test public void test_listConfigurations_cached() throws Exception + { + String pid = "testDefaultPersistenceManager"; + + PersistenceManager pm =new MockPersistenceManager(); + Dictionary dictionary = new Hashtable<>(); + dictionary.put( "property1", "value1" ); + dictionary.put( Constants.SERVICE_PID, pid ); + pm.store( pid, dictionary ); + + ConfigurationManager configMgr = new ConfigurationManager(new CachingPersistenceManagerProxy(pm), null); + + ConfigurationImpl[] conf = configMgr.listConfigurations(new ConfigurationAdminImpl(configMgr, null), null); + + assertEquals(1, conf.length); + assertEquals(2, conf[0].getProperties(true).size()); + + dictionary = new Hashtable<>(); + dictionary.put( "property1", "value2" ); + pid = "testDefaultPersistenceManager"; + dictionary.put( Constants.SERVICE_PID, pid ); + pm.store( pid, dictionary ); + + conf = configMgr.listConfigurations(new ConfigurationAdminImpl(configMgr, null), null); + assertEquals(1, conf.length); + assertEquals(2, conf[0].getProperties(true).size()); + + // verify that the property in the configurations cache was used + assertEquals("value1", conf[0].getProperties(true).get("property1")); + } + + @Test public void test_listConfigurations_notcached() throws Exception + { + String pid = "testDefaultPersistenceManager"; + PersistenceManager pm = new MockNotCachablePersistenceManager(); + Dictionary dictionary = new Hashtable<>(); + dictionary.put( "property1", "value1" ); + dictionary.put( Constants.SERVICE_PID, pid ); + pm.store( pid, dictionary ); + + ConfigurationManager configMgr = new ConfigurationManager(new PersistenceManagerProxy(pm), null); + + ConfigurationImpl[] conf = configMgr.listConfigurations(new ConfigurationAdminImpl(configMgr, null), null); + + assertEquals(1, conf.length); + assertEquals(2, conf[0].getProperties(true).size()); + + dictionary = new Hashtable<>(); + dictionary.put("property1", "valueNotCached"); + pid = "testDefaultPersistenceManager"; + dictionary.put( Constants.SERVICE_PID, pid ); + pm.store( pid, dictionary ); + + conf = configMgr.listConfigurations(new ConfigurationAdminImpl(configMgr, null), null); + assertEquals(1, conf.length); + assertEquals(2, conf[0].getProperties(true).size()); + + // verify that the value returned was not the one from the cache + assertEquals("valueNotCached", conf[0].getProperties(true).get("property1")); + } + + @Test public void test_listConfigurations_notcached_handlesUpdates() throws Exception + { + String pid = "testDefaultPersistenceManager"; + PersistenceManager pm = new MockNotCachablePersistenceManager(); + Dictionary dictionary = new Hashtable<>(); + dictionary.put( "property1", "value1" ); + dictionary.put( Constants.SERVICE_PID, pid ); + pm.store( pid, dictionary ); + + ConfigurationManager configMgr = new ConfigurationManager(new PersistenceManagerProxy(pm), null) { + @Override + void updated(ConfigurationImpl config, boolean fireEvent) { + } + }; + + ConfigurationImpl[] conf1 = configMgr.listConfigurations(new ConfigurationAdminImpl(configMgr, null), null); + + assertEquals(1, conf1.length); + assertEquals(2, conf1[0].getProperties(true).size()); + + // internal changecount + long revision = conf1[0].getRevision(); + + dictionary = new Hashtable<>(); + dictionary.put("property1", "valueNotCached"); + dictionary.put( Constants.SERVICE_PID, pid ); + conf1[0].update(dictionary); + + assertEquals(revision + 1, conf1[0].getRevision()); + + ConfigurationImpl[] conf2 = configMgr.listConfigurations(new ConfigurationAdminImpl(configMgr, null), null); + assertEquals(1, conf2.length); + assertEquals(2, conf2[0].getProperties(true).size()); + + assertEquals(revision + 1, conf2[0].getRevision()); + + dictionary = new Hashtable<>(); + dictionary.put("property1", "secondUpdate"); + dictionary.put( Constants.SERVICE_PID, pid ); + conf2[0].update(dictionary); + + assertEquals(revision + 2, conf2[0].getRevision()); + + ConfigurationImpl[] conf3 = configMgr.listConfigurations(new ConfigurationAdminImpl(configMgr, null), null); + assertEquals(1, conf3.length); + assertEquals(2, conf3[0].getProperties(true).size()); + + assertEquals(revision + 2, conf3[0].getRevision()); + } + + @Test public void testLogNoLogService() throws IOException + { + ConfigurationManager configMgr = createConfigurationManagerAndLog( null ); + + setLogLevel( LogService.LOG_WARNING ); + assertNoLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null ); + assertNoLog( configMgr, LogService.LOG_INFO, "Info Test Message", null ); + assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null ); + assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null ); + + setLogLevel( LogService.LOG_ERROR ); + assertNoLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null ); + assertNoLog( configMgr, LogService.LOG_INFO, "Info Test Message", null ); + assertNoLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null ); + assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null ); + + // lower than error -- no output + setLogLevel( LogService.LOG_ERROR - 1 ); + assertNoLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null ); + assertNoLog( configMgr, LogService.LOG_INFO, "Info Test Message", null ); + assertNoLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null ); + assertNoLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null ); + + // minimal log level -- no output + setLogLevel( Integer.MIN_VALUE ); + assertNoLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null ); + assertNoLog( configMgr, LogService.LOG_INFO, "Info Test Message", null ); + assertNoLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null ); + assertNoLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null ); + + setLogLevel( LogService.LOG_INFO ); + assertNoLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null ); + assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null ); + assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null ); + assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null ); + + setLogLevel( LogService.LOG_DEBUG ); + assertLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null ); + assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null ); + assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null ); + assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null ); + + // maximal log level -- all output + setLogLevel( Integer.MAX_VALUE ); + assertLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null ); + assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null ); + assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null ); + assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null ); + } + + // this test always expects output since when using a LogService, the log + // level property is ignored + @Test public void testLogWithLogService() throws IOException + { + LogService logService = new MockLogService(); + ConfigurationManager configMgr = createConfigurationManagerAndLog( logService ); + + setLogLevel( LogService.LOG_WARNING ); + assertLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null ); + assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null ); + assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null ); + assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null ); + + setLogLevel( LogService.LOG_ERROR ); + assertLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null ); + assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null ); + assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null ); + assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null ); + + setLogLevel( LogService.LOG_ERROR - 1 ); + assertLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null ); + assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null ); + assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null ); + assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null ); + + setLogLevel( Integer.MIN_VALUE ); + assertLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null ); + assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null ); + assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null ); + assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null ); + + setLogLevel( LogService.LOG_INFO ); + assertLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null ); + assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null ); + assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null ); + assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null ); + + setLogLevel( LogService.LOG_DEBUG ); + assertLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null ); + assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null ); + assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null ); + assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null ); + + setLogLevel( Integer.MAX_VALUE ); + assertLog( configMgr, LogService.LOG_DEBUG, "Debug Test Message", null ); + assertLog( configMgr, LogService.LOG_INFO, "Info Test Message", null ); + assertLog( configMgr, LogService.LOG_WARNING, "Warning Test Message", null ); + assertLog( configMgr, LogService.LOG_ERROR, "Error Test Message", null ); + } + + + @Test public void testLogSetup() throws IOException + { + final MockBundleContext bundleContext = new MockBundleContext(); + createConfigurationManagerAndLog( null ); + + // ensure the configuration data goes to target + bundleContext.setProperty( "felix.cm.dir", "target/config" ); + + // default value is 2 + bundleContext.setProperty( "felix.cm.loglevel", null ); + Log.logger.start( bundleContext ); + assertEquals( 2, getLogLevel( ) ); + Log.logger.stop( ); + + // illegal number yields default value + bundleContext.setProperty( "felix.cm.loglevel", "not-a-number" ); + Log.logger.start( bundleContext ); + assertEquals( 2, getLogLevel( ) ); + Log.logger.stop( ); + + bundleContext.setProperty( "felix.cm.loglevel", "-100" ); + Log.logger.start( bundleContext ); + assertEquals( -100, getLogLevel( ) ); + Log.logger.stop( ); + + bundleContext.setProperty( "felix.cm.loglevel", "4" ); + Log.logger.start( bundleContext ); + assertEquals( 4, getLogLevel( ) ); + Log.logger.stop( ); + } + + + @Test public void testEventsStartingBundle() throws Exception + { + final Set result = new HashSet<>(); + + SynchronousConfigurationListener syncListener1 = new SynchronousConfigurationListener() + { + @Override + public void configurationEvent(ConfigurationEvent event) + { + result.add("L1"); + } + }; + SynchronousConfigurationListener syncListener2 = new SynchronousConfigurationListener() + { + @Override + public void configurationEvent(ConfigurationEvent event) + { + result.add("L2"); + } + }; + SynchronousConfigurationListener syncListener3 = new SynchronousConfigurationListener() + { + @Override + public void configurationEvent(ConfigurationEvent event) + { + result.add("L3"); + } + }; + + ServiceReference mockRef = Mockito.mock( ServiceReference.class ); + ServiceRegistration mockReg = Mockito.mock( ServiceRegistration.class ); + Mockito.when( mockReg.getReference() ).thenReturn( mockRef ); + + ConfigurationManager configMgr = new ConfigurationManager(new PersistenceManagerProxy(new MockPersistenceManager()), null); + + setServiceTrackerField( configMgr, "configurationListenerTracker" ); + ServiceReference[] refs = + setServiceTrackerField( configMgr, "syncConfigurationListenerTracker", + syncListener1, syncListener2, syncListener3 ); + for ( int i=0; i < refs.length; i++) + { + Bundle mockBundle = Mockito.mock( Bundle.class ); + + switch (i) + { + case 0: + Mockito.when( mockBundle.getState() ).thenReturn( Bundle.ACTIVE ); + break; + case 1: + Mockito.when( mockBundle.getState() ).thenReturn( Bundle.STARTING ); + break; + case 2: + Mockito.when( mockBundle.getState() ).thenReturn( Bundle.STOPPING ); + break; + } + + Mockito.when( refs[i].getBundle() ).thenReturn( mockBundle ); + } + + Field srField = configMgr.getClass().getDeclaredField( "configurationAdminRegistration" ); + srField.setAccessible( true ); + srField.set( configMgr, mockReg ); + Field utField = configMgr.getClass().getDeclaredField( "updateThread" ); + utField.setAccessible( true ); + utField.set( configMgr, new UpdateThread( null, "Test updater" )); + + Dictionary props = new Hashtable<>(); + props.put( Constants.SERVICE_PID, "org.acme.testpid" ); + ConfigurationImpl config = new ConfigurationImpl( configMgr, new MockPersistenceManager(), props ); + configMgr.updated( config, true ); + + assertEquals("Both listeners should have been called, both in the STARTING and ACTIVE state, but not in the STOPPING state", + 2, result.size()); + } + + public void test_factoryConfigurationCleanup() throws Exception + { + MockNotCachablePersistenceManager pm = new MockNotCachablePersistenceManager(); + ConfigurationManager configMgr = new ConfigurationManager(new CachingPersistenceManagerProxy(pm), null); + + final Field bcField = configMgr.getClass().getDeclaredField("bundleContext"); + bcField.setAccessible(true); + bcField.set(configMgr, new MockBundleContext()); + setServiceTrackerField( configMgr, "persistenceManagerTracker" ); + setServiceTrackerField( configMgr, "logTracker" ); + setServiceTrackerField( configMgr, "configurationListenerTracker" ); + setServiceTrackerField( configMgr, "syncConfigurationListenerTracker" ); + + final Field mstField = configMgr.getClass().getDeclaredField("managedServiceFactoryTracker"); + mstField.setAccessible(true); + mstField.set( configMgr, new ManagedServiceFactoryTracker(configMgr) { + + @Override + public void open() { + } + }); + final Field utField = configMgr.getClass().getDeclaredField( "updateThread" ); + utField.setAccessible( true ); + utField.set( configMgr, new UpdateThread( null, "Test updater" ) { + + @Override + void schedule(Runnable update) { + update.run(); + } + }); + + final String factoryPid = "my.factory"; + final Dictionary props = new Hashtable<>(); + props.put("hello", "world"); + + final ConfigurationImpl c1 = configMgr.createFactoryConfiguration(factoryPid, null); + c1.update(props); + final ConfigurationImpl c2 = configMgr.createFactoryConfiguration(factoryPid, null); + c2.update(props); + final ConfigurationImpl c3 = configMgr.createFactoryConfiguration(factoryPid, null); + c3.update(props); + + assertEquals(4, pm.getStored().size()); + + c1.delete(); + assertEquals(3, pm.getStored().size()); + + c2.delete(); + assertEquals(2, pm.getStored().size()); + + c3.delete(); + assertEquals(0, pm.getStored().size()); + } + + private void assertNoLog( ConfigurationManager configMgr, int level, String message, Throwable t ) + { + try + { + Log.logger.log( level, message, t ); + assertTrue( "Expecting no log output", output.size() == 0 ); + } + finally + { + // clear the output for future data + output.reset(); + } + } + + + private void assertLog( ConfigurationManager configMgr, int level, String message, Throwable t ) + { + try + { + Log.logger.log( level, message, t ); + assertTrue( "Expecting log output", output.size() > 0 ); + + final String expectedLog = MockLogService.toMessageLine( level, message ); + final String actualLog = new String( output.toByteArray() ); + assertEquals( "Log Message not correct", expectedLog, actualLog ); + + } + finally + { + // clear the output for future data + output.reset(); + } + } + + + private static void setLogLevel( int level ) + { + final String fieldName = "logLevel"; + try + { + Field field = Log.class.getDeclaredField( fieldName ); + field.setAccessible( true ); + field.setInt( Log.logger, level ); + } + catch ( Throwable ignore ) + { + throw ( IllegalArgumentException ) new IllegalArgumentException( "Cannot set logLevel field value" ) + .initCause( ignore ); + } + } + + + private static int getLogLevel( ) + { + final String fieldName = "logLevel"; + try + { + Field field = Log.class.getDeclaredField( fieldName ); + field.setAccessible( true ); + return field.getInt( Log.logger ); + } + catch ( Throwable ignore ) + { + throw ( IllegalArgumentException ) new IllegalArgumentException( "Cannot get logLevel field value" ) + .initCause( ignore ); + } + } + + + private static ServiceReference[] setServiceTrackerField( ConfigurationManager configMgr, + String fieldName, Object ... services ) throws Exception + { + final Map refMap = new HashMap<>(); + for ( Object svc : services ) + { + ServiceReference sref = Mockito.mock( ServiceReference.class ); + Mockito.when( sref.getProperty( "objectClass" ) ).thenReturn(new String[] { "TestService" }); + refMap.put( sref, svc ); + } + + + Field field = configMgr.getClass().getDeclaredField( fieldName ); + field.setAccessible( true ); + field.set( configMgr, new ServiceTracker( new MockBundleContext(), "", null ) + { + @Override + public ServiceReference[] getServiceReferences() + { + return refMap.keySet().toArray( new ServiceReference[0] ); + } + + @Override + public Object getService(ServiceReference reference) + { + return refMap.get( reference ); + } + } ); + + return refMap.keySet().toArray(new ServiceReference[0]); + } + + private static ConfigurationManager createConfigurationManagerAndLog( final LogService logService ) + throws IOException + { + final PersistenceManager pm = Mockito.mock(PersistenceManager.class); + ConfigurationManager configMgr = new ConfigurationManager(new CachingPersistenceManagerProxy(pm), null); + + try + { + Field field = Log.class.getDeclaredField( "logTracker" ); + field.setAccessible( true ); + field.set( Log.logger, new ServiceTracker( new MockBundleContext(), "", null ) + { + @Override + public Object getService() + { + return logService; + } + } ); + } + catch ( Throwable ignore ) + { + throw ( IllegalArgumentException ) new IllegalArgumentException( "Cannot set logTracker field value" ) + .initCause( ignore ); + } + + return configMgr; + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/impl/DynamicBindingsTest.java b/configadmin/src/test/java/org/apache/felix/cm/impl/DynamicBindingsTest.java new file mode 100644 index 00000000000..a04cf46e984 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/impl/DynamicBindingsTest.java @@ -0,0 +1,180 @@ +/* + * 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.cm.impl; + + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.Dictionary; + +import org.apache.felix.cm.MockBundle; +import org.apache.felix.cm.MockBundleContext; +import org.apache.felix.cm.file.FilePersistenceManager; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; + + +public class DynamicBindingsTest +{ + + private File configLocation; + + private File bindingsFile; + + private FilePersistenceManager persistenceManager; + + private static final String PID1 = "test.pid.1"; + + private static final String LOCATION1 = "test://location.1"; + + @Before + public void setUp() throws Exception + { + configLocation = new File( "target/config." + System.currentTimeMillis() ); + persistenceManager = new FilePersistenceManager( configLocation.getAbsolutePath() ); + + bindingsFile = new File( configLocation, DynamicBindings.BINDINGS_FILE_NAME + ".config" ); + } + + @After + public void tearDown() throws Exception + { + bindingsFile.delete(); + configLocation.delete(); + } + + + @Test public void test_no_bindings() throws IOException + { + + // ensure there is no file + bindingsFile.delete(); + + final BundleContext ctx = new MockBundleContext(); + final DynamicBindings dm = new DynamicBindings( ctx, persistenceManager ); + final Dictionary bindings = getBindings( dm ); + + assertNotNull( bindings ); + assertTrue( bindings.isEmpty() ); + } + + + @Test public void test_store_bindings() throws IOException + { + // ensure there is no file + bindingsFile.delete(); + + final BundleContext ctx = new MockBundleContext(); + final DynamicBindings dm = new DynamicBindings( ctx, persistenceManager ); + + dm.putLocation( PID1, LOCATION1 ); + assertEquals( LOCATION1, dm.getLocation( PID1 ) ); + + assertTrue( bindingsFile.exists() ); + + @SuppressWarnings("unchecked") + final Dictionary bindings = persistenceManager.load( DynamicBindings.BINDINGS_FILE_NAME ); + assertNotNull( bindings ); + assertEquals( 1, bindings.size() ); + assertEquals( LOCATION1, bindings.get( PID1 ) ); + } + + + @Test public void test_store_and_load_bindings() throws IOException + { + // ensure there is no file + bindingsFile.delete(); + + // preset bindings + final DynamicBindings dm0 = new DynamicBindings( new MockBundleContext(), persistenceManager ); + dm0.putLocation( PID1, LOCATION1 ); + + // check bindings + final BundleContext ctx = new DMTestMockBundleContext(); + final DynamicBindings dm = new DynamicBindings( ctx, persistenceManager ); + + // API check + assertEquals( LOCATION1, dm.getLocation( PID1 ) ); + + // low level check + final Dictionary bindings = getBindings( dm ); + assertNotNull( bindings ); + assertEquals( 1, bindings.size() ); + assertEquals( LOCATION1, bindings.get( PID1 ) ); + } + + + @Test public void test_store_and_load_bindings_with_cleanup() throws IOException + { + // ensure there is no file + bindingsFile.delete(); + + // preset bindings + final DynamicBindings dm0 = new DynamicBindings( new MockBundleContext(), persistenceManager ); + dm0.putLocation( PID1, LOCATION1 ); + + // check bindings + final DynamicBindings dm = new DynamicBindings( new MockBundleContext(), persistenceManager ); + + // API check + assertNull( dm.getLocation( PID1 ) ); + + // low level check + final Dictionary bindings = getBindings( dm ); + assertNotNull( bindings ); + assertTrue( bindings.isEmpty() ); + } + + + @SuppressWarnings("unchecked") + private static Dictionary getBindings( DynamicBindings dm ) + { + try + { + final Field bindings = dm.getClass().getDeclaredField( "bindings" ); + bindings.setAccessible( true ); + return ( Dictionary ) bindings.get( dm ); + } + catch ( Throwable t ) + { + fail( "Cannot get bindings field: " + t ); + return null; + } + } + + private static class DMTestMockBundleContext extends MockBundleContext + { + @Override + public Bundle[] getBundles() + { + return new Bundle[] + { new MockBundle( this, LOCATION1 ) }; + } + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/impl/RankingComparatorTest.java b/configadmin/src/test/java/org/apache/felix/cm/impl/RankingComparatorTest.java new file mode 100644 index 00000000000..3be2b222c33 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/impl/RankingComparatorTest.java @@ -0,0 +1,276 @@ +/* + * 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.cm.impl; + + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import org.junit.Test; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.ConfigurationPlugin; + + +public class RankingComparatorTest +{ + + private final Comparator> srvRank = RankingComparator.SRV_RANKING; + private final Comparator> cmRank = RankingComparator.CM_RANKING; + + + @Test public void test_service_ranking_no_property() + { + ServiceReference r1 = new MockServiceReference().setProperty( Constants.SERVICE_RANKING, null ); + ServiceReference r2 = new MockServiceReference().setProperty( Constants.SERVICE_RANKING, null ); + ServiceReference r3 = new MockServiceReference().setProperty( Constants.SERVICE_RANKING, null ); + + assertTrue( srvRank.compare( r1, r1 ) == 0 ); + assertTrue( srvRank.compare( r1, r2 ) < 0 ); + assertTrue( srvRank.compare( r1, r3 ) < 0 ); + + assertTrue( srvRank.compare( r2, r1 ) > 0 ); + assertTrue( srvRank.compare( r2, r2 ) == 0 ); + assertTrue( srvRank.compare( r2, r3 ) < 0 ); + + assertTrue( srvRank.compare( r3, r1 ) > 0 ); + assertTrue( srvRank.compare( r3, r2 ) > 0 ); + assertTrue( srvRank.compare( r3, r3 ) == 0 ); + + assertTrue( cmRank.compare( r1, r1 ) == 0 ); + assertTrue( cmRank.compare( r1, r2 ) > 0 ); + assertTrue( cmRank.compare( r1, r3 ) > 0 ); + + assertTrue( cmRank.compare( r2, r1 ) < 0 ); + assertTrue( cmRank.compare( r2, r2 ) == 0 ); + assertTrue( cmRank.compare( r2, r3 ) > 0 ); + + assertTrue( cmRank.compare( r3, r1 ) < 0 ); + assertTrue( cmRank.compare( r3, r2 ) < 0 ); + assertTrue( cmRank.compare( r3, r3 ) == 0 ); + } + + + @Test public void test_service_ranking_property() + { + ServiceReference r1 = new MockServiceReference().setProperty( Constants.SERVICE_RANKING, new Integer( 100 ) ); + ServiceReference r2 = new MockServiceReference().setProperty( Constants.SERVICE_RANKING, new Integer( -100 ) ); + ServiceReference r3 = new MockServiceReference().setProperty( Constants.SERVICE_RANKING, null ); + + assertTrue( srvRank.compare( r1, r1 ) == 0 ); + assertTrue( srvRank.compare( r1, r2 ) < 0 ); + assertTrue( srvRank.compare( r1, r3 ) < 0 ); + + assertTrue( srvRank.compare( r2, r1 ) > 0 ); + assertTrue( srvRank.compare( r2, r2 ) == 0 ); + assertTrue( srvRank.compare( r2, r3 ) > 0 ); + + assertTrue( srvRank.compare( r3, r1 ) > 0 ); + assertTrue( srvRank.compare( r3, r2 ) < 0 ); + assertTrue( srvRank.compare( r3, r3 ) == 0 ); + } + + + @Test public void test_service_cm_ranking_property() + { + ServiceReference r1 = new MockServiceReference() + .setProperty( ConfigurationPlugin.CM_RANKING, new Integer( 100 ) ); + ServiceReference r2 = new MockServiceReference().setProperty( ConfigurationPlugin.CM_RANKING, + new Integer( -100 ) ); + ServiceReference r3 = new MockServiceReference().setProperty( ConfigurationPlugin.CM_RANKING, null ); + + assertTrue( cmRank.compare( r1, r1 ) == 0 ); + assertTrue( cmRank.compare( r1, r2 ) > 0 ); + assertTrue( cmRank.compare( r1, r3 ) > 0 ); + + assertTrue( cmRank.compare( r2, r1 ) < 0 ); + assertTrue( cmRank.compare( r2, r2 ) == 0 ); + assertTrue( cmRank.compare( r2, r3 ) < 0 ); + + assertTrue( cmRank.compare( r3, r1 ) < 0 ); + assertTrue( cmRank.compare( r3, r2 ) > 0 ); + assertTrue( cmRank.compare( r3, r3 ) == 0 ); + } + + + @Test public void test_service_ranking_sort() + { + ServiceReference r1 = new MockServiceReference().setProperty( Constants.SERVICE_RANKING, new Integer( 100 ) ); + ServiceReference r2 = new MockServiceReference().setProperty( Constants.SERVICE_RANKING, new Integer( -100 ) ); + ServiceReference r3 = new MockServiceReference().setProperty( Constants.SERVICE_RANKING, null ); + ServiceReference[] refs = + { r1, r2, r3 }; + + assertSame( r1, refs[0] ); + assertSame( r2, refs[1] ); + assertSame( r3, refs[2] ); + + Arrays.sort( refs, srvRank ); + + assertSame( r1, refs[0] ); + assertSame( r2, refs[2] ); + assertSame( r3, refs[1] ); + } + + + @Test public void test_service_ranking_set() + { + ServiceReference r1 = new MockServiceReference().setProperty( Constants.SERVICE_RANKING, new Integer( 100 ) ); + ServiceReference r2 = new MockServiceReference().setProperty( Constants.SERVICE_RANKING, new Integer( -100 ) ); + ServiceReference r3 = new MockServiceReference().setProperty( Constants.SERVICE_RANKING, null ); + + Set> refSet = new TreeSet<>( srvRank ); + refSet.add( r1 ); + refSet.add( r2 ); + refSet.add( r3 ); + + Iterator> refIter = refSet.iterator(); + assertSame( r1, refIter.next() ); + assertSame( r3, refIter.next() ); + assertSame( r2, refIter.next() ); + } + + + @Test public void test_service_cm_ranking_sort() + { + ServiceReference r1 = new MockServiceReference() + .setProperty( ConfigurationPlugin.CM_RANKING, new Integer( 100 ) ); + ServiceReference r2 = new MockServiceReference().setProperty( ConfigurationPlugin.CM_RANKING, + new Integer( -100 ) ); + ServiceReference r3 = new MockServiceReference().setProperty( ConfigurationPlugin.CM_RANKING, null ); + ServiceReference[] refs = + { r1, r2, r3 }; + + assertSame( r1, refs[0] ); + assertSame( r2, refs[1] ); + assertSame( r3, refs[2] ); + + Arrays.sort( refs, cmRank ); + + assertSame( r1, refs[2] ); + assertSame( r2, refs[0] ); + assertSame( r3, refs[1] ); + } + + + @Test public void test_service_cm_ranking_set() + { + ServiceReference r1 = new MockServiceReference() + .setProperty( ConfigurationPlugin.CM_RANKING, new Integer( 100 ) ); + ServiceReference r2 = new MockServiceReference().setProperty( ConfigurationPlugin.CM_RANKING, + new Integer( -100 ) ); + ServiceReference r3 = new MockServiceReference().setProperty( ConfigurationPlugin.CM_RANKING, null ); + + Set> refSet = new TreeSet<>( cmRank ); + refSet.add( r1 ); + refSet.add( r2 ); + refSet.add( r3 ); + + Iterator> refIter = refSet.iterator(); + assertSame( r2, refIter.next() ); + assertSame( r3, refIter.next() ); + assertSame( r1, refIter.next() ); + } + + private static class MockServiceReference implements ServiceReference + { + + static long id = 0; + + private final Map props = new HashMap<>(); + + { + props.put( Constants.SERVICE_ID, new Long( id ) ); + id++; + } + + + MockServiceReference setProperty( final String key, final Object value ) + { + if ( value == null ) + { + props.remove( key ); + } + else + { + props.put( key, value ); + } + return this; + } + + + @Override + public Object getProperty( String key ) + { + return props.get( key ); + } + + + @Override + public String[] getPropertyKeys() + { + return props.keySet().toArray( new String[props.size()] ); + } + + + @Override + public Bundle getBundle() + { + return null; + } + + + @Override + public Bundle[] getUsingBundles() + { + return null; + } + + + @Override + public boolean isAssignableTo( Bundle bundle, String className ) + { + return false; + } + + + @Override + public int compareTo( Object reference ) + { + return 0; + } + + + @Override + public String toString() + { + return "ServiceReference " + getProperty( Constants.SERVICE_ID ); + } + } + +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/impl/helper/ConfigurationMapTest.java b/configadmin/src/test/java/org/apache/felix/cm/impl/helper/ConfigurationMapTest.java new file mode 100644 index 00000000000..6cddf3dee5c --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/impl/helper/ConfigurationMapTest.java @@ -0,0 +1,140 @@ +/* + * 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.cm.impl.helper; + + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +import junit.framework.TestCase; + + +public class ConfigurationMapTest +{ + + @Test + public void test_accepts() + { + ConfigurationMap holder = new TestConfigurationMap( new String[] + { "a", "b", "c" } ); + + TestCase.assertTrue( holder.accepts( "a" ) ); + TestCase.assertTrue( holder.accepts( "b" ) ); + TestCase.assertTrue( holder.accepts( "c" ) ); + + TestCase.assertFalse( holder.accepts( "x" ) ); + } + + @Test + public void test_isDifferentPids_null_null() + { + ConfigurationMap holder = new TestConfigurationMap( null ); + TestCase.assertFalse( "Expect both pids null to be the same", holder.isDifferentPids( null ) ); + } + + + @Test + public void test_isDifferentPids_null_notNull() + { + ConfigurationMap holder = new TestConfigurationMap( null ); + TestCase.assertTrue( "Expect not same for one pid not null", holder.isDifferentPids( new String[] + { "entry" } ) ); + } + + + @Test + public void test_isDifferentPids_notNull_null() + { + ConfigurationMap holder = new TestConfigurationMap( new String[] + { "entry" } ); + TestCase.assertTrue( "Expect not same for one pid not null", holder.isDifferentPids( null ) ); + } + + + @Test + public void test_isDifferentPids_notNull_notNull() + { + final String[] pids10 = + { "a", "b" }; + final String[] pids11 = + { "b", "a" }; + final String[] pids20 = + { "a", "c" }; + final String[] pids30 = + { "a", "b", "c" }; + + final ConfigurationMap holder10 = new TestConfigurationMap( pids10 ); + TestCase.assertFalse( holder10.isDifferentPids( pids10 ) ); + TestCase.assertFalse( holder10.isDifferentPids( pids11 ) ); + TestCase.assertTrue( holder10.isDifferentPids( pids20 ) ); + TestCase.assertTrue( holder10.isDifferentPids( pids30 ) ); + + final ConfigurationMap holder20 = new TestConfigurationMap( pids20 ); + TestCase.assertTrue( holder20.isDifferentPids( pids10 ) ); + TestCase.assertTrue( holder20.isDifferentPids( pids11 ) ); + TestCase.assertFalse( holder20.isDifferentPids( pids20 ) ); + TestCase.assertTrue( holder20.isDifferentPids( pids30 ) ); + } + + /* + * Simple ConfigurationMap implementation sufficing for these tests + * which only test the methods in the abstract base class. + */ + static class TestConfigurationMap extends ConfigurationMap + { + + protected TestConfigurationMap( String[] configuredPids ) + { + super( configuredPids ); + } + + + @Override + protected Map createMap( int size ) + { + return new HashMap<>( size ); + } + + + @Override + protected void record( TargetedPID configPid, TargetedPID factoryPid, long revision ) + { + TestCase.fail( " is not implemented" ); + } + + + @Override + protected boolean shallTake( TargetedPID configPid, TargetedPID factoryPid, long revision ) + { + TestCase.fail( " is not implemented" ); + return false; + } + + + @Override + protected boolean removeConfiguration( TargetedPID configPid, TargetedPID factoryPid ) + { + TestCase.fail( " is not implemented" ); + return false; + } + + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/impl/helper/TargetedPidTest.java b/configadmin/src/test/java/org/apache/felix/cm/impl/helper/TargetedPidTest.java new file mode 100644 index 00000000000..15662476e42 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/impl/helper/TargetedPidTest.java @@ -0,0 +1,203 @@ +/* + * 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.cm.impl.helper; + + +import org.apache.felix.cm.MockBundle; +import org.apache.felix.cm.MockBundleContext; +import org.apache.felix.cm.MockServiceReference; +import org.junit.Test; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.Version; + +import junit.framework.TestCase; + + +public class TargetedPidTest +{ + + @Test + public void test_matchLevel() + { + // TestCase.fail( "not implemented" ); + } + + + @Test + public void test_equals() + { + // TestCase.fail( "not implemented" ); + } + + + @Test + public void test_matchesTarget_no_target() + { + final String pid = "a.b.c"; + final String symbolicName = "b1"; + final Version version = new Version( "1.0.0" ); + final String location = "loc:" + symbolicName; + + final Bundle b1 = createBundle( symbolicName, version, location ); + final ServiceReference r1 = createServiceReference( b1, pid ); + + final ServiceReference rn = createServiceReference( createBundle( symbolicName + "_", version, location ), pid ); + final ServiceReference rv = createServiceReference( + createBundle( symbolicName, new Version( "0.2.0" ), location ), pid ); + final ServiceReference rl = createServiceReference( createBundle( symbolicName, version, location + "_" ), pid ); + final ServiceReference rnone = createServiceReference( null, pid ); + + final TargetedPID p1 = new TargetedPID( String.format( "%s", pid ) ); + + TestCase.assertTrue( p1.matchesTarget( r1 ) ); + TestCase.assertTrue( p1.matchesTarget( rn ) ); + TestCase.assertTrue( p1.matchesTarget( rv ) ); + TestCase.assertTrue( p1.matchesTarget( rl ) ); + TestCase.assertFalse( "Unregistered service must not match targeted PID", p1.matchesTarget( rnone ) ); + } + + + @Test + public void test_matchesTarget_name() + { + final String pid = "a.b.c"; + final String symbolicName = "b1"; + final Version version = new Version( "1.0.0" ); + final String location = "loc:" + symbolicName; + + final Bundle b1 = createBundle( symbolicName, version, location ); + final ServiceReference r1 = createServiceReference( b1, pid ); + + final ServiceReference rn = createServiceReference( createBundle( symbolicName + "_", version, location ), pid ); + final ServiceReference rv = createServiceReference( + createBundle( symbolicName, new Version( "0.2.0" ), location ), pid ); + final ServiceReference rl = createServiceReference( createBundle( symbolicName, version, location + "_" ), pid ); + final ServiceReference rnone = createServiceReference( null, pid ); + + final TargetedPID p1 = new TargetedPID( String.format( "%s|%s", pid, symbolicName ) ); + + TestCase.assertTrue( "Reference from same bundle must match targeted PID", p1.matchesTarget( r1 ) ); + TestCase.assertFalse( "Different symbolic name must not match targeted PID", p1.matchesTarget( rn ) ); + TestCase.assertTrue( p1.matchesTarget( rv ) ); + TestCase.assertTrue( p1.matchesTarget( rl ) ); + TestCase.assertFalse( "Unregistered service must not match targeted PID", p1.matchesTarget( rnone ) ); + } + + + @Test + public void test_matchesTarget_name_version() + { + final String pid = "a.b.c"; + final String symbolicName = "b1"; + final Version version = new Version( "1.0.0" ); + final String location = "loc:" + symbolicName; + + final Bundle b1 = createBundle( symbolicName, version, location ); + final ServiceReference r1 = createServiceReference( b1, pid ); + + final ServiceReference rn = createServiceReference( createBundle( symbolicName + "_", version, location ), pid ); + final ServiceReference rv = createServiceReference( + createBundle( symbolicName, new Version( "0.2.0" ), location ), pid ); + final ServiceReference rl = createServiceReference( createBundle( symbolicName, version, location + "_" ), pid ); + final ServiceReference rnone = createServiceReference( null, pid ); + + final TargetedPID p1 = new TargetedPID( String.format( "%s|%s|%s", pid, symbolicName, version ) ); + + TestCase.assertTrue( "Reference from same bundle must match targeted PID", p1.matchesTarget( r1 ) ); + TestCase.assertFalse( "Different symbolic name must not match targeted PID", p1.matchesTarget( rn ) ); + TestCase.assertFalse( "Different version must not match targeted PID", p1.matchesTarget( rv ) ); + TestCase.assertTrue( p1.matchesTarget( rl ) ); + TestCase.assertFalse( "Unregistered service must not match targeted PID", p1.matchesTarget( rnone ) ); + } + + + + @Test + public void test_matchesTarget_name_version_location() + { + final String pid = "a.b.c"; + final String symbolicName = "b1"; + final Version version = new Version( "1.0.0" ); + final String location = "loc:" + symbolicName; + + final Bundle b1 = createBundle( symbolicName, version, location ); + final ServiceReference r1 = createServiceReference( b1, pid ); + + final ServiceReference rn = createServiceReference( createBundle( symbolicName + "_", version, location ), pid ); + final ServiceReference rv = createServiceReference( + createBundle( symbolicName, new Version( "0.2.0" ), location ), pid ); + final ServiceReference rl = createServiceReference( createBundle( symbolicName, version, location + "_" ), pid ); + final ServiceReference rnone = createServiceReference( null, pid ); + + final TargetedPID p1 = new TargetedPID( String.format( "%s|%s|%s|%s", pid, symbolicName, version, location ) ); + + TestCase.assertTrue( "Reference from same bundle must match targeted PID", p1.matchesTarget( r1 ) ); + TestCase.assertFalse( "Different symbolic name must not match targeted PID", p1.matchesTarget( rn ) ); + TestCase.assertFalse( "Different version must not match targeted PID", p1.matchesTarget( rv ) ); + TestCase.assertFalse( "Different location must not match targeted PID", p1.matchesTarget( rl ) ); + TestCase.assertFalse( "Unregistered service must not match targeted PID", p1.matchesTarget( rnone ) ); + } + + + Bundle createBundle( final String symbolicName, final Version version, final String location ) + { + BundleContext ctx = new MockBundleContext(); + return new MockBundle( ctx, location ) + { + @Override + public String getSymbolicName() + { + return symbolicName; + } + + + @Override + public Version getVersion() { + return version; + } + }; + } + + + ServiceReference createServiceReference( final Bundle bundle, final Object pids ) + { + return new MockServiceReference() + { + @Override + public Bundle getBundle() + { + return bundle; + } + + + @Override + public Object getProperty( String key ) + { + if ( Constants.SERVICE_PID.equals( key ) ) + { + return pids; + } + return super.getProperty( key ); + } + }; + } +} \ No newline at end of file diff --git a/configadmin/src/test/java/org/apache/felix/cm/impl/persistence/CachingPersistenceManagerProxyTest.java b/configadmin/src/test/java/org/apache/felix/cm/impl/persistence/CachingPersistenceManagerProxyTest.java new file mode 100644 index 00000000000..6990ac532de --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/impl/persistence/CachingPersistenceManagerProxyTest.java @@ -0,0 +1,181 @@ +/* + * 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.cm.impl.persistence; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Set; + +import org.apache.felix.cm.MockPersistenceManager; +import org.apache.felix.cm.PersistenceManager; +import org.apache.felix.cm.impl.SimpleFilter; +import org.junit.Test; +import org.osgi.framework.Constants; +import org.osgi.service.cm.ConfigurationAdmin; + + +/** + * The CachingPersistenceManagerProxyTest class tests the issues + * related to caching of configurations. + *

      + * @see FELIX-4930 + */ +public class CachingPersistenceManagerProxyTest +{ + private static final String PID_A = "foo.a"; + private static final String PID_B = "foo.b"; + private static final String PID_C = "foo.c"; + private static final String FACTORY_PID_A = "bla.a"; + private static final String FACTORY_PID_B = "bla.b"; + private static final String FA_PID_A = "728206-a"; + private static final String FA_PID_B = "728206-b"; + private static final String FA_PID_C = "728206-c"; + private static final String FB_PID_A = "992101-a"; + private static final String FB_PID_B = "992101-b"; + + private static final String PREFIX = "this-is-"; + + private Dictionary createConfiguration(final String pid, final String factoryPid) + { + final Dictionary dict = new Hashtable<>(); + + dict.put(Constants.SERVICE_PID, pid); + if ( factoryPid != null ) + { + dict.put(ConfigurationAdmin.SERVICE_FACTORYPID, factoryPid); + } + dict.put("value", PREFIX + pid); + + return dict; + } + + private PersistenceManager createAndPopulatePersistenceManager() + throws IOException + { + final PersistenceManager pm = new MockPersistenceManager(); + + pm.store(PID_A, createConfiguration(PID_A, null)); + pm.store(PID_B, createConfiguration(PID_B, null)); + pm.store(PID_C, createConfiguration(PID_C, null)); + + pm.store(FA_PID_A, createConfiguration(FA_PID_A, FACTORY_PID_A)); + pm.store(FA_PID_B, createConfiguration(FA_PID_B, FACTORY_PID_A)); + pm.store(FA_PID_C, createConfiguration(FA_PID_C, FACTORY_PID_A)); + + pm.store(FB_PID_A, createConfiguration(FB_PID_A, FACTORY_PID_B)); + pm.store(FB_PID_B, createConfiguration(FB_PID_B, FACTORY_PID_B)); + return pm; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test public void test_caching_is_applied() throws Exception + { + String pid = "testDefaultPersistenceManager"; + SimpleFilter filter = SimpleFilter.parse("(&(service.pid=" + pid + ")(property1=value1))"); + + PersistenceManager pm = new MockPersistenceManager(); + CachingPersistenceManagerProxy cpm = new CachingPersistenceManagerProxy( pm ); + + Dictionary dictionary = new Hashtable(); + dictionary.put( "property1", "value1" ); + dictionary.put( Constants.SERVICE_PID, pid ); + pm.store( pid, dictionary ); + + Collection list = cpm.getDictionaries( filter ); + assertEquals(1, list.size()); + + dictionary = new Hashtable(); + dictionary.put( "property1", "value2" ); + pid = "testDefaultPersistenceManager"; + dictionary.put( Constants.SERVICE_PID, pid ); + pm.store( pid, dictionary ); + + list = cpm.getDictionaries( filter ); + assertEquals(1, list.size()); + } + + @Test public void testPopulation() throws Exception + { + final CachingPersistenceManagerProxy cpm = new CachingPersistenceManagerProxy(this.createAndPopulatePersistenceManager()); + + assertTrue(cpm.exists(PID_A)); + assertTrue(cpm.exists(PID_B)); + assertTrue(cpm.exists(PID_C)); + assertTrue(cpm.exists(FA_PID_A)); + assertTrue(cpm.exists(FA_PID_B)); + assertTrue(cpm.exists(FA_PID_C)); + assertTrue(cpm.exists(FB_PID_A)); + assertTrue(cpm.exists(FB_PID_B)); + + assertFalse(cpm.exists("foo")); + } + + @Test public void testGetPopulatedFactoryConfigurationPids() throws Exception + { + final CachingPersistenceManagerProxy cpm = new CachingPersistenceManagerProxy(this.createAndPopulatePersistenceManager()); + + final Set pidsOfFA = cpm.getFactoryConfigurationPids(Collections.singletonList(FACTORY_PID_A)); + assertEquals(3, pidsOfFA.size()); + assertTrue(pidsOfFA.contains(FA_PID_A)); + assertTrue(pidsOfFA.contains(FA_PID_B)); + assertTrue(pidsOfFA.contains(FA_PID_C)); + + final Set pidsOfFB = cpm.getFactoryConfigurationPids(Collections.singletonList(FACTORY_PID_B)); + assertEquals(2, pidsOfFB.size()); + assertTrue(pidsOfFB.contains(FB_PID_A)); + assertTrue(pidsOfFB.contains(FB_PID_B)); + + assertTrue(cpm.getFactoryConfigurationPids(Collections.singletonList(PID_A)).isEmpty()); + } + + @Test public void testGetFactoryConfigurationPids() throws Exception + { + final CachingPersistenceManagerProxy cpm = new CachingPersistenceManagerProxy(this.createAndPopulatePersistenceManager()); + + // modify populated + cpm.store("new_pid_for_fa", createConfiguration("new_pid_for_fa", FACTORY_PID_A)); + cpm.delete(FB_PID_B); + + final Set pidsOfFA = cpm.getFactoryConfigurationPids(Collections.singletonList(FACTORY_PID_A)); + assertEquals(4, pidsOfFA.size()); + assertTrue(pidsOfFA.contains(FA_PID_A)); + assertTrue(pidsOfFA.contains(FA_PID_B)); + assertTrue(pidsOfFA.contains(FA_PID_C)); + assertTrue(pidsOfFA.contains("new_pid_for_fa")); + + final Set pidsOfFB = cpm.getFactoryConfigurationPids(Collections.singletonList(FACTORY_PID_B)); + assertEquals(1, pidsOfFB.size()); + assertTrue(pidsOfFB.contains(FB_PID_A)); + + // use new factory pid + cpm.store("new_pid_for_newf1", createConfiguration("new_pid_for_newf1", "factory.pid")); + cpm.store("new_pid_for_newf2", createConfiguration("new_pid_for_newf2", "factory.pid")); + final Set pids = cpm.getFactoryConfigurationPids(Collections.singletonList("factory.pid")); + assertEquals(2, pids.size()); + assertTrue(pids.contains("new_pid_for_newf1")); + assertTrue(pids.contains("new_pid_for_newf2")); + } +} \ No newline at end of file diff --git a/configadmin/src/test/java/org/apache/felix/cm/impl/persistence/PersistenceManagerProxyTest.java b/configadmin/src/test/java/org/apache/felix/cm/impl/persistence/PersistenceManagerProxyTest.java new file mode 100644 index 00000000000..b08fcbee8d8 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/impl/persistence/PersistenceManagerProxyTest.java @@ -0,0 +1,68 @@ +/* + * 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.cm.impl.persistence; + +import static org.junit.Assert.assertEquals; + +import java.util.Collection; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.apache.felix.cm.MockNotCachablePersistenceManager; +import org.apache.felix.cm.PersistenceManager; +import org.apache.felix.cm.impl.SimpleFilter; +import org.junit.Test; +import org.osgi.framework.Constants; + + +/** + * The PersistenceManagerProxyTest class tests the issues + * related to caching of configurations. + *

      + * @see FELIX-4930 + */ +public class PersistenceManagerProxyTest +{ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test public void test_caching_is_avoided() throws Exception { + String pid = "testDefaultPersistenceManager"; + SimpleFilter filter = SimpleFilter.parse("(&(service.pid=" + pid + ")(property1=value1))"); + + PersistenceManager pm = new MockNotCachablePersistenceManager(); + PersistenceManagerProxy cpm = new PersistenceManagerProxy( pm ); + + Dictionary dictionary = new Hashtable(); + dictionary.put( "property1", "value1" ); + dictionary.put( Constants.SERVICE_PID, pid ); + pm.store( pid, dictionary ); + + Collection list = cpm.getDictionaries( filter ); + assertEquals(1, list.size()); + + dictionary = new Hashtable(); + dictionary.put( "property1", "value2" ); + pid = "testDefaultPersistenceManager"; + dictionary.put( Constants.SERVICE_PID, pid ); + pm.store( pid, dictionary ); + + list = cpm.getDictionaries( filter ); + assertEquals(0, list.size()); + } + +} \ No newline at end of file diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigAdminSecurityTest.java b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigAdminSecurityTest.java new file mode 100644 index 00000000000..0932bde619c --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigAdminSecurityTest.java @@ -0,0 +1,244 @@ +/* + * 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.cm.integration; + + +import static org.ops4j.pax.exam.CoreOptions.frameworkProperty; +import static org.ops4j.pax.exam.CoreOptions.mavenBundle; +import static org.ops4j.pax.exam.CoreOptions.options; +import static org.ops4j.pax.exam.CoreOptions.systemProperty; +import static org.osgi.framework.Constants.FRAMEWORK_SECURITY; +import static org.osgi.framework.Constants.FRAMEWORK_SECURITY_OSGI; +import static org.osgi.framework.Constants.FRAMEWORK_STORAGE_CLEAN; +import static org.osgi.framework.Constants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT; +import static org.osgi.service.url.URLConstants.URL_HANDLER_PROTOCOL; + +import java.io.File; +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.apache.felix.cm.integration.helper.ConfigurationListenerTestActivator; +import org.apache.felix.cm.integration.helper.ManagedServiceFactoryTestActivator3; +import org.apache.felix.cm.integration.helper.ManagedServiceFactoryTestActivator4; +import org.apache.felix.cm.integration.helper.NestedURLStreamHandler; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.forked.ForkedTestContainerFactory; +import org.ops4j.pax.exam.junit.ExamFactory; +import org.ops4j.pax.exam.junit.ExamReactorStrategy; +import org.ops4j.pax.exam.junit.JUnit4TestRunner; +import org.ops4j.pax.exam.spi.reactors.AllConfinedStagedReactorFactory; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.url.URLStreamHandlerService; + +import junit.framework.TestCase; + + +/** + * This test case runs the main Configuration tests with security on to check that + * nothing breaks. + * + * Note that it must run as a {@link ForkedTestContainerFactory} because otherwise + * we can't enable Java Security in the Framework + */ +@RunWith( JUnit4TestRunner.class ) +@ExamFactory( ForkedTestContainerFactory.class ) +@ExamReactorStrategy( AllConfinedStagedReactorFactory.class ) +public class ConfigAdminSecurityTest extends ConfigurationBaseTest +{ + + @Override + protected Option[] additionalConfiguration() { + File policyFile = new File( "src/test/resources/all.policy" ); + return options( + frameworkProperty( FRAMEWORK_STORAGE_CLEAN ).value( FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT ), + frameworkProperty( FRAMEWORK_SECURITY ).value( FRAMEWORK_SECURITY_OSGI ), + systemProperty( "java.security.policy" ).value( policyFile.getAbsolutePath() ), + mavenBundle( "org.apache.felix", "org.apache.felix.framework.security", "2.6.1" ) + ); + } + + @Test + public void test_secure_configuration() throws BundleException, IOException + { + final String factoryPid = "test_secure_configuration"; + bundle = installBundle( factoryPid, ManagedServiceFactoryTestActivator3.class ); + bundle.start(); + delay(); + + final Configuration config = createFactoryConfiguration( factoryPid, null, true ); + final String pid = config.getPid(); + delay(); + + // ==> configuration supplied to the service ms1 + final ManagedServiceFactoryTestActivator3 tester = ManagedServiceFactoryTestActivator3.INSTANCE; + Dictionary props = tester.configs.get( pid ); + TestCase.assertNotNull( props ); + TestCase.assertEquals( pid, props.get( Constants.SERVICE_PID ) ); + TestCase.assertEquals( factoryPid, props.get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( props.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, props.get( PROP_NAME ) ); + TestCase.assertEquals( File.separator, props.get( "foo" ) ); + TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls ); + TestCase.assertEquals( 1, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 0, tester.numManagedServiceFactoryDeleteCalls ); + + // delete + config.delete(); + delay(); + + // ==> update with null + TestCase.assertNull( tester.configs.get( pid ) ); + TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls ); + TestCase.assertEquals( 1, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 1, tester.numManagedServiceFactoryDeleteCalls ); + } + + @Test + public void test_secure_configuration_non_standard_install_url() throws Exception + { + // Override the file URL handler + + @SuppressWarnings({ "serial", "unused" }) + ServiceRegistration reg = bundleContext + .registerService( URLStreamHandlerService.class, new NestedURLStreamHandler(), + new Hashtable() { { + put( URL_HANDLER_PROTOCOL, new String[] { "file" } ); + } } ); + + + // Run the actual test + + final String factoryPid = "test_secure_configuration_non_standard_install_url"; + bundle = installBundle( factoryPid, ManagedServiceFactoryTestActivator3.class ); + bundle.start(); + delay(); + + final Configuration config = createFactoryConfiguration( factoryPid, null, true ); + final String pid = config.getPid(); + delay(); + + // ==> configuration supplied to the service ms1 + final ManagedServiceFactoryTestActivator3 tester = ManagedServiceFactoryTestActivator3.INSTANCE; + Dictionary props = tester.configs.get( pid ); + TestCase.assertNotNull( props ); + TestCase.assertEquals( pid, props.get( Constants.SERVICE_PID ) ); + TestCase.assertEquals( factoryPid, props.get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( props.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, props.get( PROP_NAME ) ); + TestCase.assertEquals( File.separator, props.get( "foo" ) ); + TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls ); + TestCase.assertEquals( 1, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 0, tester.numManagedServiceFactoryDeleteCalls ); + + // delete + config.delete(); + delay(); + + // ==> update with null + TestCase.assertNull( tester.configs.get( pid ) ); + TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls ); + TestCase.assertEquals( 1, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 1, tester.numManagedServiceFactoryDeleteCalls ); + } + + @Test + public void test_secure_configuration_client_has_perm_we_do_not() throws BundleException, IOException + { + final String factoryPid = "test_secure_configuration_client_has_perm_we_do_not"; + bundle = installBundle( factoryPid, ManagedServiceFactoryTestActivator4.class ); + bundle.start(); + delay(); + + final Configuration config = createFactoryConfiguration( factoryPid, null, true ); + final String pid = config.getPid(); + delay(); + + // ==> configuration supplied to the service ms1 + final ManagedServiceFactoryTestActivator4 tester = ManagedServiceFactoryTestActivator4.INSTANCE; + Dictionary props = tester.configs.get( pid ); + TestCase.assertNotNull( props ); + TestCase.assertEquals( pid, props.get( Constants.SERVICE_PID ) ); + TestCase.assertEquals( factoryPid, props.get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( props.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, props.get( PROP_NAME ) ); + TestCase.assertTrue( props.get( "port" ) != null ); + TestCase.assertTrue( ( (Integer) props.get( "port" ) ) > 0 ); + TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls ); + TestCase.assertEquals( 1, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 0, tester.numManagedServiceFactoryDeleteCalls ); + + // delete + config.delete(); + delay(); + + // ==> update with null + TestCase.assertNull( tester.configs.get( pid ) ); + TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls ); + TestCase.assertEquals( 1, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 1, tester.numManagedServiceFactoryDeleteCalls ); + } + + @Test + public void test_secure_configuration_listener_has_perm_we_do_not() throws BundleException, IOException + { + final String pid = "test_secure_configuration_listener_has_perm_we_do_not"; + bundle = installBundle( pid, ConfigurationListenerTestActivator.class ); + bundle.start(); + delay(); + + final Configuration config = configure( pid, null, true ); + delay(); + + // ==> configuration supplied to the service ms1 + final ConfigurationListenerTestActivator tester = ConfigurationListenerTestActivator.INSTANCE; + Dictionary props = tester.configs.get( pid ); + TestCase.assertNotNull( props ); + TestCase.assertEquals( pid, props.get( Constants.SERVICE_PID ) ); + TestCase.assertNull( props.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, props.get( PROP_NAME ) ); + TestCase.assertTrue( props.get( "port" ) != null ); + TestCase.assertTrue( ( (Integer) props.get( "port" ) ) > 0 ); + TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls ); + TestCase.assertEquals( 0, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 0, tester.numManagedServiceFactoryDeleteCalls ); + TestCase.assertEquals( 1, tester.numListenerUpdatedCalls ); + TestCase.assertEquals( 0, tester.numListenerDeleteCalls ); + TestCase.assertEquals( 1, tester.numListenerLocationChangedCalls ); + + // delete + config.delete(); + delay(); + + // ==> update with null + TestCase.assertNull( tester.configs.get( pid ) ); + TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls ); + TestCase.assertEquals( 0, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 0, tester.numManagedServiceFactoryDeleteCalls ); + TestCase.assertEquals( 1, tester.numListenerUpdatedCalls ); + TestCase.assertEquals( 1, tester.numListenerDeleteCalls ); + TestCase.assertEquals( 1, tester.numListenerLocationChangedCalls ); + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigUpdateStressTest.java b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigUpdateStressTest.java new file mode 100644 index 00000000000..81ac693233e --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigUpdateStressTest.java @@ -0,0 +1,233 @@ +/* + * 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.cm.integration; + + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Dictionary; + +import junit.framework.TestCase; + +import org.apache.felix.cm.integration.helper.ConfigureThread; +import org.apache.felix.cm.integration.helper.ManagedServiceFactoryThread; +import org.apache.felix.cm.integration.helper.ManagedServiceThread; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.JUnit4TestRunner; + + +/** + * The ConfigUpdateStressTest class tests the issues related to + * concurrency between configuration update (Configuration.update(Dictionary)) + * and ManagedService[Factory] registration. + *

      + * @see FELIX-1545 + */ +@RunWith(JUnit4TestRunner.class) +public class ConfigUpdateStressTest extends ConfigurationTestBase +{ + + @Test + public void test_ManagedService_race_condition_test() + { + int counterMax = 30; + int failures = 0; + + for ( int counter = 0; counter < counterMax; counter++ ) + { + try + { + single_test_ManagedService_race_condition_test( counter ); + } + catch ( Throwable ae ) + { + System.out.println( "single_test_ManagedService_race_condition_test#" + counter + " failed: " + ae ); + ae.printStackTrace( System.out ); + failures++; + } + } + + // fail the test if there is at least one failure + if ( failures != 0 ) + { + TestCase.fail( failures + "/" + counterMax + " iterations failed" ); + } + } + + + @Test + public void test_ManagedServiceFactory_race_condition_test() + { + int counterMax = 30; + int failures = 0; + + for ( int counter = 0; counter < counterMax; counter++ ) + { + try + { + single_test_ManagedServiceFactory_race_condition_test( counter ); + } + catch ( Throwable ae ) + { + System.out.println( "single_test_ManagedServiceFactory_race_condition_test#" + counter + " failed: " + + ae ); + ae.printStackTrace( System.out ); + failures++; + } + } + + // fail the test if there is at least one failure + if ( failures != 0 ) + { + TestCase.fail( failures + "/" + counterMax + " iterations failed" ); + } + } + + + // runs a single test to encounter the race condition between ManagedService + // registration and Configuration.update(Dictionary) + // This test creates/updates configuration and registers a ManagedService + // almost at the same time. The ManagedService must receive the + // configuration + // properties exactly once. + private void single_test_ManagedService_race_condition_test( final int counter ) throws IOException, + InterruptedException + { + + final String pid = "single_test_ManagedService_race_condition_test." + counter; + + final ConfigureThread ct = new ConfigureThread( getConfigurationAdmin(), pid, false ); + final ManagedServiceThread mt = new ManagedServiceThread( bundleContext, pid ); + + try + { + // start threads -- both are waiting to be triggered + ct.start(); + mt.start(); + + // trigger for action + ct.trigger(); + mt.trigger(); + + // wait for threads to terminate + ct.join(); + mt.join(); + + // wait for all tasks to terminate + delay(); + + final boolean isConfigured = mt.isConfigured(); + final ArrayList configs = mt.getConfigs(); + + // terminate mt to ensure no further config updates + mt.cleanup(); + + TestCase.assertTrue( "Last update call must have been with configuration", isConfigured); + + if ( configs.size() == 0 ) + { + TestCase.fail( "No configuration provided to ManagedService at all" ); + } + else if ( configs.size() == 2 ) + { + final Dictionary props0 = configs.get( 0 ); + final Dictionary props1 = configs.get( 1 ); + + TestCase.assertNull( "Expected first (of two) updates without configuration", props0 ); + TestCase.assertNotNull( "Expected second (of two) updates with configuration", props1 ); + } + else if ( configs.size() == 1 ) + { + final Dictionary props = configs.get( 0 ); + TestCase.assertNotNull( "Expected non-null configuration: " + props, props ); + } + else + { + TestCase.fail( "Unexpectedly got " + configs.size() + " updated" ); + } + } + finally + { + mt.cleanup(); + ct.cleanup(); + } + } + + + // runs a single test to encounter the race condition between + // ManagedServiceFactory registration and Configuration.update(Dictionary) + // This test creates/updates configuration and registers a + // ManagedServiceFactory almost at the same time. The ManagedServiceFactory + // must receive the configuration properties exactly once. + private void single_test_ManagedServiceFactory_race_condition_test( final int counter ) throws IOException, + InterruptedException + { + + final String factoryPid = "single_test_ManagedServiceFactory_race_condition_test." + counter; + + final ConfigureThread ct = new ConfigureThread( getConfigurationAdmin(), factoryPid, true ); + final ManagedServiceFactoryThread mt = new ManagedServiceFactoryThread( bundleContext, factoryPid ); + + try + { + // start threads -- both are waiting to be triggered + ct.start(); + mt.start(); + + // trigger for action + ct.trigger(); + mt.trigger(); + + // wait for threads to terminate + ct.join(); + mt.join(); + + // wait for all tasks to terminate + delay(); + + final boolean isConfigured = mt.isConfigured(); + final ArrayList configs = mt.getConfigs(); + + // terminate mt to ensure no further config updates + mt.cleanup(); + + TestCase.assertTrue( "Last update call must have been with configuration", isConfigured); + + if ( configs.size() == 0 ) + { + TestCase.fail( "No configuration provided to ManagedServiceFactory at all" ); + } + else if ( configs.size() == 1 ) + { + final Dictionary props = configs.get( 0 ); + TestCase.assertNotNull( "Expected non-null configuration: " + props, props ); + } + else + { + TestCase.fail( "Unexpectedly got " + configs.size() + " updated" ); + } + } + finally + { + mt.cleanup(); + ct.cleanup(); + } + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationAdminUpdateStressTest.java b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationAdminUpdateStressTest.java new file mode 100644 index 00000000000..d6b4eddceac --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationAdminUpdateStressTest.java @@ -0,0 +1,385 @@ +/* + * 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.cm.integration; + + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Dictionary; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.Assert; + +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.JUnit4TestRunner; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedServiceFactory; +import org.osgi.service.log.LogService; +import org.osgi.util.tracker.ServiceTracker; + + +/** + * The ConfigurationAdminUpdateStressTest repeatedly updates + * a ManagedFactoryService with configuration to verify configuration is + * exactly delivered once and no update is lost. + * + * @see FELIX-1545 + */ +@RunWith(JUnit4TestRunner.class) +public class ConfigurationAdminUpdateStressTest extends ConfigurationTestBase implements LogService +{ + public static final int TEST_LOOP = 10; + public static final int UPDATE_LOOP = 100; + + private String _FACTORYPID = "MyPID"; + private String _FACTORYNAME = "MyName"; + + private volatile CountDownLatch _factoryConfigCreateLatch; + private volatile CountDownLatch _factoryConfigUpdateLatch; + private volatile CountDownLatch _factoryConfigDeleteLatch; + private volatile CountDownLatch _testLatch; + private volatile ServiceTracker _tracker; + + + // ----------------------- Initialization ------------------------------------------- + + @Before + public void startup() + { + bundleContext.registerService( LogService.class.getName(), this, null ); + _tracker = new ServiceTracker<>( bundleContext, ConfigurationAdmin.class, null ); + _tracker.open(); + } + + + /** + * Always cleanup our bundle location file (because pax seems to forget to cleanup it) + * @param context + */ + + @After + public void tearDown() + { + _tracker.close(); + } + + + // ----------------------- LogService ----------------------------------------------- + + public void log( int level, String message ) + { + System.out.println( "[LogService/" + level + "] " + message ); + } + + + public void log( int level, String message, Throwable exception ) + { + StringBuilder sb = new StringBuilder(); + sb.append( "[LogService/" + level + "] " ); + sb.append( message ); + parse( sb, exception ); + System.out.println( sb.toString() ); + } + + + public void log( @SuppressWarnings("rawtypes") ServiceReference sr, int level, String message ) + { + StringBuilder sb = new StringBuilder(); + sb.append( "[LogService/" + level + "] " ); + sb.append( message ); + System.out.println( sb.toString() ); + } + + + public void log( @SuppressWarnings("rawtypes") ServiceReference sr, int level, String message, Throwable exception ) + { + StringBuilder sb = new StringBuilder(); + sb.append( "[LogService/" + level + "] " ); + sb.append( message ); + parse( sb, exception ); + System.out.println( sb.toString() ); + } + + + private void parse( StringBuilder sb, Throwable t ) + { + if ( t != null ) + { + sb.append( " - " ); + StringWriter buffer = new StringWriter(); + PrintWriter pw = new PrintWriter( buffer ); + t.printStackTrace( pw ); + sb.append( buffer.toString() ); + } + } + + + // --------------------------- CM Update stress test ------------------------------------- + + @Test + public void testCMUpdateStress() + { + _testLatch = new CountDownLatch( 1 ); + try + { + CreateStress stress = new CreateUpdateStress( bundleContext ); + stress.start(); + + if ( !_testLatch.await( 15, TimeUnit.SECONDS ) ) + { + + log( LogService.LOG_DEBUG, "create latch: " + _factoryConfigCreateLatch.getCount() ); + log( LogService.LOG_DEBUG, "update latch: " + _factoryConfigUpdateLatch.getCount() ); + log( LogService.LOG_DEBUG, "delete latch: " + _factoryConfigDeleteLatch.getCount() ); + + Assert.fail( "Test did not completed timely" ); + } + } + catch ( InterruptedException e ) + { + Assert.fail( "Test interrupted" ); + } + } + + @Test + public void testCMUpdateNamedStress() + { + _testLatch = new CountDownLatch( 1 ); + try + { + CreateStress stress = new CreateUpdateNamedStress( bundleContext ); + stress.start(); + + if ( !_testLatch.await( 15, TimeUnit.SECONDS ) ) + { + + log( LogService.LOG_DEBUG, "create latch: " + _factoryConfigCreateLatch.getCount() ); + log( LogService.LOG_DEBUG, "update latch: " + _factoryConfigUpdateLatch.getCount() ); + log( LogService.LOG_DEBUG, "delete latch: " + _factoryConfigDeleteLatch.getCount() ); + + Assert.fail( "Test did not completed timely" ); + } + } + catch ( InterruptedException e ) + { + Assert.fail( "Test interrupted" ); + } + } + + + /** + * Setup the latches used throughout this test + */ + private void setupLatches() + { + _factoryConfigCreateLatch = new CountDownLatch( 1 ); + _factoryConfigUpdateLatch = new CountDownLatch( UPDATE_LOOP ); + _factoryConfigDeleteLatch = new CountDownLatch( 1 ); + } + + /** + * This is our Factory class which will react up CM factory configuration objects. + * Each time a factory configuration object is created, the _factoryConfigCreatedLatch is counted down. + * Each time a factory configuration object is updated, the _factoryConfigUpdatedLatch is counted down. + * Each time a factory configuration object is deleted, the _factoryConfigDeletedLatch is counted down. + */ + @Ignore + class Factory implements ManagedServiceFactory + { + Set _pids = new HashSet(); + + + public synchronized void updated( String pid, Dictionary properties ) throws ConfigurationException + { + if ( _pids.add( pid ) ) + { + // pid created + _factoryConfigCreateLatch.countDown(); + log( LogService.LOG_DEBUG, "Config created; create latch= " + _factoryConfigCreateLatch.getCount() ); + } + else + { + // pid updated + try + { + Long number = ( Long ) properties.get( "number" ); + long currentNumber = _factoryConfigUpdateLatch.getCount(); + if ( number.longValue() != currentNumber ) + { + throw new ConfigurationException( "number", "Expected number=" + currentNumber + ", actual=" + + number ); + } + _factoryConfigUpdateLatch.countDown(); + log( LogService.LOG_DEBUG, "Config updated; update latch= " + _factoryConfigUpdateLatch.getCount() + + " (number=" + number + ")" ); + } + catch ( ClassCastException e ) + { + throw new ConfigurationException( "number", e.getMessage(), e ); + } + } + } + + + public void deleted( String pid ) + { + // We need to remove this as the same PID can re-occur after deletion + _pids.remove(pid); + _factoryConfigDeleteLatch.countDown(); + log( LogService.LOG_DEBUG, "Config deleted; delete latch= " + _factoryConfigDeleteLatch.getCount() ); + } + + + public String getName() + { + return "MyPID"; + } + } + + /** + * This class creates/update/delete some factory configuration instances, using a separate thread. + */ + @Ignore + abstract class CreateStress extends Thread + { + BundleContext _bc; + + + CreateStress( BundleContext bctx ) + { + _bc = bctx; + } + + + public void run() + { + try + { + System.out.println( "Starting CM stress test ..." ); + ConfigurationAdmin cm = ( ConfigurationAdmin ) _tracker.waitForService( 2000 ); + setupLatches(); + Factory factory = new Factory(); + Hashtable serviceProps = new Hashtable(); + serviceProps.put( "service.pid", _FACTORYPID ); + _bc.registerService( ManagedServiceFactory.class.getName(), factory, serviceProps ); + + for ( int l = 0; l < TEST_LOOP; l++ ) + { + // Create factory configuration + Hashtable props = new Hashtable(); + props.put( "foo", "bar" ); + obtainConfiguration(cm).update( props ); + + // Check if our Factory has seen the factory configuration creation + if ( !_factoryConfigCreateLatch.await( 10, TimeUnit.SECONDS ) ) + { + throw new RuntimeException( "_factoryConfigCreateLatch did not reach zero timely" ); + } + + // Update factory configuration many times + for ( int i = 0; i < UPDATE_LOOP; i++ ) + { + props = new Hashtable(); + props.put( "foo", "bar" + i ); + props.put( "number", Long.valueOf( UPDATE_LOOP - i ) ); + obtainConfiguration(cm).update( props ); + } + + // Check if all configuration updates have been caught by our Factory + if ( !_factoryConfigUpdateLatch.await( 10, TimeUnit.SECONDS ) ) + { + throw new RuntimeException( "_factoryConfigUpdateLatch did not reach zero timely" ); + } + + // Remove factory configuration + obtainConfiguration(cm).delete(); + + // Check if our Factory has seen the configration removal + if ( !_factoryConfigDeleteLatch.await( 10, TimeUnit.SECONDS ) ) + { + throw new RuntimeException( "_factoryConfigDeleteLatch did not reach zero timely" ); + } + + // Reset latches + setupLatches(); + } + } + catch ( Exception e ) + { + e.printStackTrace( System.err ); + return; + } + _testLatch.countDown(); // Notify that our test is done + } + + + protected abstract org.osgi.service.cm.Configuration obtainConfiguration(ConfigurationAdmin cm) + throws Exception; + + } + + @Ignore + class CreateUpdateStress extends CreateStress { + CreateUpdateStress(BundleContext bctx) { + super(bctx); + } + + protected org.osgi.service.cm.Configuration obtainConfiguration(ConfigurationAdmin cm) + throws Exception { + Configuration[] cfgs = cm.listConfigurations("(service.factoryPid=" + _FACTORYPID + ")" ); + + org.osgi.service.cm.Configuration conf; + if(cfgs == null) { + conf = cm.createFactoryConfiguration( _FACTORYPID, null ); + } else if (cfgs.length == 1) { + conf = cfgs[0]; + } else { + throw new IllegalArgumentException("Only one configuration expected"); + } + + return conf; + } + } + + @Ignore + class CreateUpdateNamedStress extends CreateStress { + CreateUpdateNamedStress(BundleContext bctx) { + super(bctx); + } + + protected org.osgi.service.cm.Configuration obtainConfiguration(ConfigurationAdmin cm) + throws IOException { + org.osgi.service.cm.Configuration conf = cm.getFactoryConfiguration( _FACTORYPID, _FACTORYNAME, null ); + return conf; + } + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationBaseTest.java b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationBaseTest.java new file mode 100644 index 00000000000..9d1025e92b9 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationBaseTest.java @@ -0,0 +1,1326 @@ +/* + * 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.cm.integration; + + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; + +import junit.framework.TestCase; + +import org.apache.felix.cm.integration.helper.ManagedServiceFactoryTestActivator; +import org.apache.felix.cm.integration.helper.ManagedServiceTestActivator; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.JUnit4TestRunner; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + + +@RunWith(JUnit4TestRunner.class) +public class ConfigurationBaseTest extends ConfigurationTestBase +{ + + static + { + // uncomment to enable debugging of this test class + // paxRunnerVmOption = DEBUG_VM_OPTION; + } + + + @Test + public void test_configuration_getFacotryPid_after_config_admin_stop() throws BundleException + { + final String pid = "test_configuration_after_config_admin_stop"; + final Configuration config = configure( pid, null, true ); + + final Bundle cfgAdminBundle = configAdminTracker.getServiceReference().getBundle(); + cfgAdminBundle.stop(); + try + { + config.getFactoryPid(); + } + finally + { + try + { + cfgAdminBundle.start(); + } + catch ( BundleException be ) + { + // tooo bad + } + } + } + + + @Test + public void test_configuration_equals_after_config_admin_stop() throws BundleException + { + final String pid = "test_configuration_after_config_admin_stop"; + final Configuration config = configure( pid, null, true ); + + final Bundle cfgAdminBundle = configAdminTracker.getServiceReference().getBundle(); + cfgAdminBundle.stop(); + try + { + config.equals( config ); + } + finally + { + try + { + cfgAdminBundle.start(); + } + catch ( BundleException be ) + { + // tooo bad + } + } + } + + + @Test + public void test_configuration_hashCode_after_config_admin_stop() throws BundleException + { + final String pid = "test_configuration_after_config_admin_stop"; + final Configuration config = configure( pid, null, true ); + + final Bundle cfgAdminBundle = configAdminTracker.getServiceReference().getBundle(); + cfgAdminBundle.stop(); + try + { + config.hashCode(); + } + finally + { + try + { + cfgAdminBundle.start(); + } + catch ( BundleException be ) + { + // tooo bad + } + } + } + + + @Test + public void test_configuration_toString_after_config_admin_stop() throws BundleException + { + final String pid = "test_configuration_after_config_admin_stop"; + final Configuration config = configure( pid, null, true ); + + final Bundle cfgAdminBundle = configAdminTracker.getServiceReference().getBundle(); + cfgAdminBundle.stop(); + try + { + config.toString(); + } + finally + { + try + { + cfgAdminBundle.start(); + } + catch ( BundleException be ) + { + // tooo bad + } + } + } + + + public void test_configuration_getPid_after_config_admin_stop() throws BundleException + { + final String pid = "test_configuration_after_config_admin_stop"; + final Configuration config = configure( pid, null, true ); + + final Bundle cfgAdminBundle = configAdminTracker.getServiceReference().getBundle(); + cfgAdminBundle.stop(); + try + { + config.getPid(); + } + finally + { + try + { + cfgAdminBundle.start(); + } + catch ( BundleException be ) + { + // tooo bad + } + } + } + + + @Test + public void test_configuration_getProperties_after_config_admin_stop() throws BundleException + { + final String pid = "test_configuration_after_config_admin_stop"; + final Configuration config = configure( pid, null, true ); + + final Bundle cfgAdminBundle = configAdminTracker.getServiceReference().getBundle(); + cfgAdminBundle.stop(); + try + { + config.getProperties(); + } + finally + { + try + { + cfgAdminBundle.start(); + } + catch ( BundleException be ) + { + // tooo bad + } + } + } + + + @Test + public void test_configuration_delete_after_config_admin_stop() throws BundleException + { + final String pid = "test_configuration_after_config_admin_stop"; + final Configuration config = configure( pid, null, true ); + + final Bundle cfgAdminBundle = configAdminTracker.getServiceReference().getBundle(); + cfgAdminBundle.stop(); + try + { + config.delete(); + TestCase.fail( "Expected IllegalStateException for config.delete" ); + } + catch ( IllegalStateException ise ) + { + // expected + } + catch ( Exception e ) + { + TestCase.fail( "Expected IllegalStateException for config.delete" ); + } + finally + { + try + { + cfgAdminBundle.start(); + } + catch ( BundleException be ) + { + // tooo bad + } + } + } + + + @Test + public void test_configuration_getBundleLocation_after_config_admin_stop() throws BundleException + { + final String pid = "test_configuration_after_config_admin_stop"; + final Configuration config = configure( pid, null, true ); + + final Bundle cfgAdminBundle = configAdminTracker.getServiceReference().getBundle(); + cfgAdminBundle.stop(); + try + { + config.getBundleLocation(); + TestCase.fail( "Expected IllegalStateException for config.getBundleLocation" ); + } + catch ( IllegalStateException ise ) + { + // expected + } + catch ( Exception e ) + { + TestCase.fail( "Expected IllegalStateException for config.getBundleLocation" ); + } + finally + { + try + { + cfgAdminBundle.start(); + } + catch ( BundleException be ) + { + // tooo bad + } + } + } + + + @Test + public void test_configuration_setBundleLocation_after_config_admin_stop() throws BundleException + { + final String pid = "test_configuration_after_config_admin_stop"; + final Configuration config = configure( pid, null, true ); + + final Bundle cfgAdminBundle = configAdminTracker.getServiceReference().getBundle(); + cfgAdminBundle.stop(); + try + { + config.setBundleLocation( "?*" ); + TestCase.fail( "Expected IllegalStateException for config.setBundleLocation" ); + } + catch ( IllegalStateException ise ) + { + // expected + } + catch ( Exception e ) + { + TestCase.fail( "Expected IllegalStateException for config.setBundleLocation" ); + } + finally + { + try + { + cfgAdminBundle.start(); + } + catch ( BundleException be ) + { + // tooo bad + } + } + } + + + @Test + public void test_configuration_update_after_config_admin_stop() throws BundleException + { + final String pid = "test_configuration_after_config_admin_stop"; + final Configuration config = configure( pid, null, true ); + + final Bundle cfgAdminBundle = configAdminTracker.getServiceReference().getBundle(); + cfgAdminBundle.stop(); + try + { + config.update(); + TestCase.fail( "Expected IllegalStateException for config.update" ); + } + catch ( IllegalStateException ise ) + { + // expected + } + catch ( Exception e ) + { + TestCase.fail( "Expected IllegalStateException for config.update" ); + } + finally + { + try + { + cfgAdminBundle.start(); + } + catch ( BundleException be ) + { + // tooo bad + } + } + } + + + @SuppressWarnings("serial") + @Test + public void test_configuration_update_with_Dictionary_after_config_admin_stop() throws BundleException + { + final String pid = "test_configuration_after_config_admin_stop"; + final Configuration config = configure( pid, null, true ); + + final Bundle cfgAdminBundle = configAdminTracker.getServiceReference().getBundle(); + cfgAdminBundle.stop(); + try + { + config.update( new Hashtable() + { + { + put( "sample", "sample" ); + } + } ); + TestCase.fail( "Expected IllegalStateException for config.update" ); + } + catch ( IllegalStateException ise ) + { + // expected + } + catch ( Exception e ) + { + TestCase.fail( "Expected IllegalStateException for config.update" ); + } + finally + { + try + { + cfgAdminBundle.start(); + } + catch ( BundleException be ) + { + // tooo bad + } + } + } + + + @Test + public void test_configuration_admin_createFactoryConfiguration_1_after_config_admin_stop() throws BundleException + { + final ConfigurationAdmin ca = getConfigurationAdmin(); + TestCase.assertNotNull( "ConfigurationAdmin service is required", ca ); + + final Bundle cfgAdminBundle = configAdminTracker.getServiceReference().getBundle(); + cfgAdminBundle.stop(); + try + { + ca.createFactoryConfiguration( "sample" ); + TestCase.fail( "Expected IllegalStateException for ConfigurationAdmin.createFactoryConfiguration" ); + } + catch ( IllegalStateException ise ) + { + // expected + } + catch ( Exception e ) + { + TestCase.fail( "Expected IllegalStateException for ConfigurationAdmin.createFactoryConfiguration, got: " + e ); + } + finally + { + try + { + cfgAdminBundle.start(); + } + catch ( BundleException be ) + { + // tooo bad + } + } + } + + + @Test + public void test_configuration_admin_createFactoryConfiguration_2_after_config_admin_stop() throws BundleException + { + final ConfigurationAdmin ca = getConfigurationAdmin(); + TestCase.assertNotNull( "ConfigurationAdmin service is required", ca ); + + final Bundle cfgAdminBundle = configAdminTracker.getServiceReference().getBundle(); + cfgAdminBundle.stop(); + try + { + ca.createFactoryConfiguration( "sample", "location" ); + TestCase.fail( "Expected IllegalStateException for ConfigurationAdmin.createFactoryConfiguration" ); + } + catch ( IllegalStateException ise ) + { + // expected + } + catch ( Exception e ) + { + TestCase.fail( "Expected IllegalStateException for ConfigurationAdmin.createFactoryConfiguration, got: " + e ); + } + finally + { + try + { + cfgAdminBundle.start(); + } + catch ( BundleException be ) + { + // tooo bad + } + } + } + + + @Test + public void test_configuration_admin_getConfiguration_1_after_config_admin_stop() throws BundleException + { + final ConfigurationAdmin ca = getConfigurationAdmin(); + TestCase.assertNotNull( "ConfigurationAdmin service is required", ca ); + + final Bundle cfgAdminBundle = configAdminTracker.getServiceReference().getBundle(); + cfgAdminBundle.stop(); + try + { + ca.getConfiguration( "sample" ); + TestCase.fail( "Expected IllegalStateException for ConfigurationAdmin.getConfiguration" ); + } + catch ( IllegalStateException ise ) + { + // expected + } + catch ( Exception e ) + { + TestCase.fail( "Expected IllegalStateException for ConfigurationAdmin.getConfiguration, got: " + e ); + } + finally + { + try + { + cfgAdminBundle.start(); + } + catch ( BundleException be ) + { + // tooo bad + } + } + } + + + @Test + public void test_configuration_admin_getConfiguration_2_after_config_admin_stop() throws BundleException + { + final ConfigurationAdmin ca = getConfigurationAdmin(); + TestCase.assertNotNull( "ConfigurationAdmin service is required", ca ); + + final Bundle cfgAdminBundle = configAdminTracker.getServiceReference().getBundle(); + cfgAdminBundle.stop(); + try + { + ca.getConfiguration( "sample", "location" ); + TestCase.fail( "Expected IllegalStateException for ConfigurationAdmin.getConfiguration" ); + } + catch ( IllegalStateException ise ) + { + // expected + } + catch ( Exception e ) + { + TestCase.fail( "Expected IllegalStateException for ConfigurationAdmin.getConfiguration, got: " + e ); + } + finally + { + try + { + cfgAdminBundle.start(); + } + catch ( BundleException be ) + { + // tooo bad + } + } + } + + + @Test + public void test_configuration_admin_listConfigurations_after_config_admin_stop() throws BundleException + { + final ConfigurationAdmin ca = getConfigurationAdmin(); + TestCase.assertNotNull( "ConfigurationAdmin service is required", ca ); + + final Bundle cfgAdminBundle = configAdminTracker.getServiceReference().getBundle(); + cfgAdminBundle.stop(); + try + { + ca.listConfigurations( "(service.pid=sample)" ); + TestCase.fail( "Expected IllegalStateException for ConfigurationAdmin.listConfigurations" ); + } + catch ( IllegalStateException ise ) + { + // expected + } + catch ( Exception e ) + { + TestCase.fail( "Expected IllegalStateException for ConfigurationAdmin.listConfigurations, got: " + e ); + } + finally + { + try + { + cfgAdminBundle.start(); + } + catch ( BundleException be ) + { + // tooo bad + } + } + } + + + @Test + public void test_configuration_change_counter() throws IOException + { + // 1. create config with pid and locationA + // 2. update config with properties + final String pid = "test_configuration_change_counter"; + final Configuration config = configure( pid, null, false ); + + TestCase.assertEquals("Expect first version to be 1", 1, config.getChangeCount()); + + config.update(new Hashtable(){{put("x", "x");}}); + TestCase.assertEquals("Expect second version to be 2", 2, config.getChangeCount()); + + // delete + config.delete(); + } + + + @Test + public void test_basic_configuration_configure_then_start() throws BundleException, IOException + { + // 1. create config with pid and locationA + // 2. update config with properties + final String pid = "test_basic_configuration_configure_then_start"; + final Configuration config = configure( pid, null, true ); + + // 3. register ManagedService ms1 with pid from said locationA + bundle = installBundle( pid, ManagedServiceTestActivator.class ); + bundle.start(); + delay(); + + // ==> configuration supplied to the service ms1 + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( tester.props ); + TestCase.assertEquals( pid, tester.props.get( Constants.SERVICE_PID ) ); + TestCase.assertNull( tester.props.get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( tester.props.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, tester.props.get( PROP_NAME ) ); + TestCase.assertEquals( 1, tester.numManagedServiceUpdatedCalls ); + + // delete + config.delete(); + delay(); + + // ==> update with null + TestCase.assertNull( tester.props ); + TestCase.assertEquals( 2, tester.numManagedServiceUpdatedCalls ); + } + + + @Test + public void test_basic_configuration_strange_pid() throws BundleException, IOException + { + // 1. create config with pid and locationA + // 2. update config with properties + final String pid = "pid with blanks and stuff %\"'"; + theConfig.put( pid, pid ); + final Configuration config = configure( pid, null, true ); + theConfig.remove( pid ); + + // 3. register ManagedService ms1 with pid from said locationA + bundle = installBundle( pid, ManagedServiceTestActivator.class ); + bundle.start(); + delay(); + + // ==> configuration supplied to the service ms1 + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( tester.props ); + TestCase.assertEquals( pid, tester.props.get( Constants.SERVICE_PID ) ); + TestCase.assertNull( tester.props.get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( tester.props.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, tester.props.get( PROP_NAME ) ); + TestCase.assertEquals( pid, tester.props.get( pid ) ); + TestCase.assertEquals( 1, tester.numManagedServiceUpdatedCalls ); + + // delete + config.delete(); + delay(); + + // ==> update with null + TestCase.assertNull( tester.props ); + TestCase.assertEquals( 2, tester.numManagedServiceUpdatedCalls ); + } + + + @Test + public void test_basic_configuration_start_then_configure() throws BundleException, IOException + { + final String pid = "test_basic_configuration_start_then_configure"; + + // 1. register ManagedService ms1 with pid from said locationA + bundle = installBundle( pid, ManagedServiceTestActivator.class ); + bundle.start(); + delay(); + + // 1. create config with pid and locationA + // 2. update config with properties + final Configuration config = configure( pid, null, true ); + delay(); + + // ==> configuration supplied to the service ms1 + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( tester.props ); + TestCase.assertEquals( pid, tester.props.get( Constants.SERVICE_PID ) ); + TestCase.assertNull( tester.props.get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( tester.props.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, tester.props.get( PROP_NAME ) ); + TestCase.assertEquals( 2, tester.numManagedServiceUpdatedCalls ); + + // delete + config.delete(); + delay(); + + // ==> update with null + TestCase.assertNull( tester.props ); + TestCase.assertEquals( 3, tester.numManagedServiceUpdatedCalls ); + } + + + @Test + public void test_basic_configuration_factory_start_then_configure() throws BundleException, IOException + { + final String factoryPid = "test_basic_configuration_factory_configure_then_start"; + bundle = installBundle( factoryPid, ManagedServiceFactoryTestActivator.class ); + bundle.start(); + delay(); + + final Configuration config = createFactoryConfiguration( factoryPid, null, true ); + final String pid = config.getPid(); + delay(); + + // ==> configuration supplied to the service ms1 + final ManagedServiceFactoryTestActivator tester = ManagedServiceFactoryTestActivator.INSTANCE; + Dictionary props = tester.configs.get( pid ); + TestCase.assertNotNull( props ); + TestCase.assertEquals( pid, props.get( Constants.SERVICE_PID ) ); + TestCase.assertEquals( factoryPid, props.get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( props.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, props.get( PROP_NAME ) ); + TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls ); + TestCase.assertEquals( 1, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 0, tester.numManagedServiceFactoryDeleteCalls ); + + // delete + config.delete(); + delay(); + + // ==> update with null + TestCase.assertNull( tester.configs.get( pid ) ); + TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls ); + TestCase.assertEquals( 1, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 1, tester.numManagedServiceFactoryDeleteCalls ); + } + + + @Test + public void test_basic_configuration_factory_configure_then_start() throws BundleException, IOException + { + // 1. create config with pid and locationA + // 2. update config with properties + final String factoryPid = "test_basic_configuration_factory_start_then_configure"; + final Configuration config = createFactoryConfiguration( factoryPid, null, true ); + final String pid = config.getPid(); + delay(); + + // 3. register ManagedService ms1 with pid from said locationA + bundle = installBundle( factoryPid, ManagedServiceFactoryTestActivator.class ); + bundle.start(); + delay(); + + // ==> configuration supplied to the service ms1 + final ManagedServiceFactoryTestActivator tester = ManagedServiceFactoryTestActivator.INSTANCE; + Dictionary props = tester.configs.get( pid ); + TestCase.assertNotNull( props ); + TestCase.assertEquals( pid, props.get( Constants.SERVICE_PID ) ); + TestCase.assertEquals( factoryPid, props.get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( props.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, props.get( PROP_NAME ) ); + TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls ); + TestCase.assertEquals( 1, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 0, tester.numManagedServiceFactoryDeleteCalls ); + + // delete + config.delete(); + delay(); + + // ==> update with null + TestCase.assertNull( tester.configs.get( pid ) ); + TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls ); + TestCase.assertEquals( 1, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 1, tester.numManagedServiceFactoryDeleteCalls ); + } + + + @Test + public void test_start_bundle_configure_stop_start_bundle() throws BundleException + { + String pid = "test_start_bundle_configure_stop_start_bundle"; + + // start the bundle and assert this + bundle = installBundle( pid ); + bundle.start(); + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + + // give cm time for distribution + delay(); + + // assert activater has no configuration + TestCase.assertNull( "Expect no Properties after Service Registration", tester.props ); + TestCase.assertEquals( "Expect update call", 1, tester.numManagedServiceUpdatedCalls ); + + // configure after ManagedServiceRegistration --> configure via update + configure( pid ); + delay(); + + // assert activater has configuration + TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props ); + TestCase.assertEquals( "Expect a second update call", 2, tester.numManagedServiceUpdatedCalls ); + + // stop the bundle now + bundle.stop(); + + // assert INSTANCE is null + TestCase.assertNull( ManagedServiceTestActivator.INSTANCE ); + + delay(); + + // start the bundle again (and check) + bundle.start(); + final ManagedServiceTestActivator tester2 = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started the second time!!", tester2 ); + TestCase.assertNotSame( "Instances must not be the same", tester, tester2 ); + + // give cm time for distribution + delay(); + + // assert activater has configuration + TestCase.assertNotNull( "Expect Properties after Service Registration", tester2.props ); + TestCase.assertEquals( "Expect a second update call", 1, tester2.numManagedServiceUpdatedCalls ); + + // cleanup + bundle.uninstall(); + bundle = null; + + // remove the configuration for good + deleteConfig( pid ); + } + + + @Test + public void test_configure_start_bundle_stop_start_bundle() throws BundleException + { + String pid = "test_configure_start_bundle_stop_start_bundle"; + configure( pid ); + + // start the bundle and assert this + bundle = installBundle( pid ); + bundle.start(); + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + + // give cm time for distribution + delay(); + + // assert activater has configuration + TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props ); + TestCase.assertEquals( "Expect no update call", 1, tester.numManagedServiceUpdatedCalls ); + + // stop the bundle now + bundle.stop(); + + // assert INSTANCE is null + TestCase.assertNull( ManagedServiceTestActivator.INSTANCE ); + + delay(); + + // start the bundle again (and check) + bundle.start(); + final ManagedServiceTestActivator tester2 = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started the second time!!", tester2 ); + TestCase.assertNotSame( "Instances must not be the same", tester, tester2 ); + + // give cm time for distribution + delay(); + + // assert activater has configuration + TestCase.assertNotNull( "Expect Properties after Service Registration", tester2.props ); + TestCase.assertEquals( "Expect a second update call", 1, tester2.numManagedServiceUpdatedCalls ); + + // cleanup + bundle.uninstall(); + bundle = null; + + // remove the configuration for good + deleteConfig( pid ); + } + + + @Test + public void test_listConfiguration() throws BundleException, IOException + { + // 1. create a new Conf1 with pid1 and null location. + // 2. Conf1#update(props) is called. + final String pid = "test_listConfiguration"; + final Configuration config = configure( pid, null, true ); + + // 3. bundleA will locationA registers ManagedServiceA with pid1. + bundle = installBundle( pid ); + bundle.start(); + delay(); + + // ==> ManagedServiceA is called back. + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( tester ); + TestCase.assertNotNull( tester.props ); + TestCase.assertEquals( 1, tester.numManagedServiceUpdatedCalls ); + + // 4. bundleA is stopped but *NOT uninstalled*. + bundle.stop(); + delay(); + + // 5. test bundle calls cm.listConfigurations(null). + final Configuration listed = getConfiguration( pid ); + + // ==> Conf1 is included in the returned list and + // it has locationA. + // (In debug mode, dynamicBundleLocation==locationA + // and staticBundleLocation==null) + TestCase.assertNotNull( listed ); + TestCase.assertEquals( bundle.getLocation(), listed.getBundleLocation() ); + + // 6. test bundle calls cm.getConfiguration(pid1) + final Configuration get = getConfigurationAdmin().getConfiguration( pid ); + TestCase.assertEquals( bundle.getLocation(), get.getBundleLocation() ); + + final Bundle cmBundle = getCmBundle(); + cmBundle.stop(); + delay(); + cmBundle.start(); + delay(); + + // 5. test bundle calls cm.listConfigurations(null). + final Configuration listed2 = getConfiguration( pid ); + + // ==> Conf1 is included in the returned list and + // it has locationA. + // (In debug mode, dynamicBundleLocation==locationA + // and staticBundleLocation==null) + TestCase.assertNotNull( listed2 ); + TestCase.assertEquals( bundle.getLocation(), listed2.getBundleLocation() ); + + // 6. test bundle calls cm.getConfiguration(pid1) + final Configuration get2 = getConfigurationAdmin().getConfiguration( pid ); + TestCase.assertEquals( bundle.getLocation(), get2.getBundleLocation() ); + } + + + @Test + public void test_ManagedService_change_pid() throws BundleException, IOException + { + final String pid0 = "test_ManagedService_change_pid_0"; + final String pid1 = "test_ManagedService_change_pid_1"; + + final Configuration config0 = configure( pid0, null, true ); + final Configuration config1 = configure( pid1, null, true ); + delay(); + + // register ManagedService ms1 with pid from said locationA + bundle = installBundle( pid0, ManagedServiceTestActivator.class ); + bundle.start(); + delay(); + + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( tester.props ); + TestCase.assertEquals( pid0, tester.props.get( Constants.SERVICE_PID ) ); + TestCase.assertNull( tester.props.get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( tester.props.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, tester.props.get( PROP_NAME ) ); + TestCase.assertEquals( 1, tester.numManagedServiceUpdatedCalls ); + + // change ManagedService PID + tester.changePid( pid1 ); + delay(); + + TestCase.assertNotNull( tester.props ); + TestCase.assertEquals( pid1, tester.props.get( Constants.SERVICE_PID ) ); + TestCase.assertNull( tester.props.get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( tester.props.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, tester.props.get( PROP_NAME ) ); + TestCase.assertEquals( 2, tester.numManagedServiceUpdatedCalls ); + + // delete + config0.delete(); + config1.delete(); + delay(); + + // ==> update with null + TestCase.assertNull( tester.props ); + TestCase.assertEquals( 3, tester.numManagedServiceUpdatedCalls ); + } + + + @Test + public void test_ManagedService_change_pid_overlap() throws BundleException, IOException + { + final String pid0 = "test_ManagedService_change_pid_0"; + final String pid1 = "test_ManagedService_change_pid_1"; + final String pid2 = "test_ManagedService_change_pid_2"; + + final Configuration config0 = configure( pid0, null, true ); + final Configuration config1 = configure( pid1, null, true ); + final Configuration config2 = configure( pid2, null, true ); + delay(); + + // register ManagedService ms1 with pid from said locationA + bundle = installBundle( pid0 + "," + pid1, ManagedServiceTestActivator.class ); + bundle.start(); + delay(); + + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( tester.props ); + + TestCase.assertEquals( pid0, tester.configs.get( pid0 ).get( Constants.SERVICE_PID ) ); + TestCase.assertNull( tester.configs.get( pid0 ).get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( tester.configs.get( pid0 ).get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, tester.configs.get( pid0 ).get( PROP_NAME ) ); + + TestCase.assertEquals( pid1, tester.configs.get( pid1 ).get( Constants.SERVICE_PID ) ); + TestCase.assertNull( tester.configs.get( pid1 ).get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( tester.configs.get( pid1 ).get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, tester.configs.get( pid1 ).get( PROP_NAME ) ); + + // two pids, two calls + TestCase.assertEquals( 2, tester.numManagedServiceUpdatedCalls ); + + // change ManagedService PID + tester.changePid( pid1 + "," + pid2 ); + delay(); + + TestCase.assertNotNull( tester.props ); + + // config pid0 is not "removed" + TestCase.assertEquals( pid0, tester.configs.get( pid0 ).get( Constants.SERVICE_PID ) ); + TestCase.assertNull( tester.configs.get( pid0 ).get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( tester.configs.get( pid0 ).get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, tester.configs.get( pid0 ).get( PROP_NAME ) ); + + // config pid1 is retained + TestCase.assertEquals( pid1, tester.configs.get( pid1 ).get( Constants.SERVICE_PID ) ); + TestCase.assertNull( tester.configs.get( pid1 ).get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( tester.configs.get( pid1 ).get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, tester.configs.get( pid1 ).get( PROP_NAME ) ); + + // config pid2 is added + TestCase.assertEquals( pid2, tester.configs.get( pid2 ).get( Constants.SERVICE_PID ) ); + TestCase.assertNull( tester.configs.get( pid2 ).get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( tester.configs.get( pid2 ).get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, tester.configs.get( pid2 ).get( PROP_NAME ) ); + + // one "additional" pid, one additional call + TestCase.assertEquals( 3, tester.numManagedServiceUpdatedCalls ); + + // delete + config0.delete(); // ignored by MS + config1.delete(); + config2.delete(); + delay(); + + // ==> update with null + TestCase.assertNull( tester.props ); + + // two pids removed, two calls + TestCase.assertEquals( 5, tester.numManagedServiceUpdatedCalls ); + } + + + @Test + public void test_ManagedServiceFactory_change_pid() throws BundleException, IOException + { + + final String factoryPid0 = "test_ManagedServiceFactory_change_pid_0"; + final String factoryPid1 = "test_ManagedServiceFactory_change_pid_1"; + + final Configuration config0 = createFactoryConfiguration( factoryPid0, null, true ); + final String pid0 = config0.getPid(); + final Configuration config1 = createFactoryConfiguration( factoryPid1, null, true ); + final String pid1 = config1.getPid(); + delay(); + + bundle = installBundle( factoryPid0, ManagedServiceFactoryTestActivator.class ); + bundle.start(); + delay(); + + // pid0 properties provided on registration + final ManagedServiceFactoryTestActivator tester = ManagedServiceFactoryTestActivator.INSTANCE; + Dictionary props0 = tester.configs.get( pid0 ); + TestCase.assertNotNull( props0 ); + TestCase.assertEquals( pid0, props0.get( Constants.SERVICE_PID ) ); + TestCase.assertEquals( factoryPid0, props0.get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( props0.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, props0.get( PROP_NAME ) ); + TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls ); + TestCase.assertEquals( 1, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 0, tester.numManagedServiceFactoryDeleteCalls ); + + // change ManagedService PID + tester.changePid( factoryPid1 ); + delay(); + + // pid1 properties must have been added + Dictionary props1 = tester.configs.get( pid1 ); + TestCase.assertNotNull( props1 ); + TestCase.assertEquals( pid1, props1.get( Constants.SERVICE_PID ) ); + TestCase.assertEquals( factoryPid1, props1.get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( props1.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, props1.get( PROP_NAME ) ); + TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls ); + TestCase.assertEquals( 2, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 0, tester.numManagedServiceFactoryDeleteCalls ); + + // pid0 properties must still exist ! + Dictionary props01 = tester.configs.get( pid0 ); + TestCase.assertNotNull( props01 ); + TestCase.assertEquals( pid0, props01.get( Constants.SERVICE_PID ) ); + TestCase.assertEquals( factoryPid0, props01.get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( props01.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, props01.get( PROP_NAME ) ); + + + // delete + config0.delete(); + config1.delete(); + delay(); + + // only pid1 properties removed because pid0 is not registered any longer + TestCase.assertNotNull( tester.configs.get( pid0 ) ); + TestCase.assertNull( tester.configs.get( pid1 ) ); + TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls ); + TestCase.assertEquals( 2, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 1, tester.numManagedServiceFactoryDeleteCalls ); + } + + + @Test + public void test_ManagedServiceFactory_change_pid_overlap() throws BundleException, IOException + { + + final String factoryPid0 = "test_ManagedServiceFactory_change_pid_0"; + final String factoryPid1 = "test_ManagedServiceFactory_change_pid_1"; + final String factoryPid2 = "test_ManagedServiceFactory_change_pid_2"; + + final Configuration config0 = createFactoryConfiguration( factoryPid0, null, true ); + final String pid0 = config0.getPid(); + final Configuration config1 = createFactoryConfiguration( factoryPid1, null, true ); + final String pid1 = config1.getPid(); + final Configuration config2 = createFactoryConfiguration( factoryPid2, null, true ); + final String pid2 = config2.getPid(); + delay(); + + bundle = installBundle( factoryPid0 + "," + factoryPid1, ManagedServiceFactoryTestActivator.class ); + bundle.start(); + delay(); + + // pid0 properties provided on registration + final ManagedServiceFactoryTestActivator tester = ManagedServiceFactoryTestActivator.INSTANCE; + Dictionary props0 = tester.configs.get( pid0 ); + TestCase.assertNotNull( props0 ); + TestCase.assertEquals( pid0, props0.get( Constants.SERVICE_PID ) ); + TestCase.assertEquals( factoryPid0, props0.get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( props0.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, props0.get( PROP_NAME ) ); + + Dictionary props1 = tester.configs.get( pid1 ); + TestCase.assertNotNull( props1 ); + TestCase.assertEquals( pid1, props1.get( Constants.SERVICE_PID ) ); + TestCase.assertEquals( factoryPid1, props1.get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( props1.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, props1.get( PROP_NAME ) ); + + TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls ); + TestCase.assertEquals( 2, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 0, tester.numManagedServiceFactoryDeleteCalls ); + + // change ManagedService PID + tester.changePid( factoryPid1 + "," + factoryPid2 ); + delay(); + + // pid2 properties must have been added + Dictionary props2 = tester.configs.get( pid2 ); + TestCase.assertNotNull( props2 ); + TestCase.assertEquals( pid2, props2.get( Constants.SERVICE_PID ) ); + TestCase.assertEquals( factoryPid2, props2.get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( props2.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, props2.get( PROP_NAME ) ); + + // pid0 properties must still exist ! + Dictionary props01 = tester.configs.get( pid0 ); + TestCase.assertNotNull( props01 ); + TestCase.assertEquals( pid0, props01.get( Constants.SERVICE_PID ) ); + TestCase.assertEquals( factoryPid0, props01.get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( props01.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, props01.get( PROP_NAME ) ); + + // pid1 properties must still exist ! + Dictionary props11 = tester.configs.get( pid1 ); + TestCase.assertNotNull( props11 ); + TestCase.assertEquals( pid1, props11.get( Constants.SERVICE_PID ) ); + TestCase.assertEquals( factoryPid1, props11.get( ConfigurationAdmin.SERVICE_FACTORYPID ) ); + TestCase.assertNull( props11.get( ConfigurationAdmin.SERVICE_BUNDLELOCATION ) ); + TestCase.assertEquals( PROP_NAME, props11.get( PROP_NAME ) ); + + TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls ); + TestCase.assertEquals( 3, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 0, tester.numManagedServiceFactoryDeleteCalls ); + + // delete + config0.delete(); + config1.delete(); + config2.delete(); + delay(); + + // only pid1 and pid2 properties removed because pid0 is not registered any longer + TestCase.assertNotNull( tester.configs.get( pid0 ) ); + TestCase.assertNull( tester.configs.get( pid1 ) ); + TestCase.assertNull( tester.configs.get( pid2 ) ); + + TestCase.assertEquals( 0, tester.numManagedServiceUpdatedCalls ); + TestCase.assertEquals( 3, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 2, tester.numManagedServiceFactoryDeleteCalls ); + } + + + @Test + public void test_factory_configuration_collision() throws IOException, InvalidSyntaxException, BundleException { + final String factoryPid = "test_factory_configuration_collision"; + + final Configuration cf = getConfigurationAdmin().createFactoryConfiguration( factoryPid, null ); + TestCase.assertNotNull( cf ); + final String pid = cf.getPid(); + + // check factory configuration setup + TestCase.assertNotNull( "Configuration must have PID", pid ); + TestCase.assertEquals( "Factory configuration must have requested factory PID", factoryPid, cf.getFactoryPid() ); + + try + { + bundle = installBundle( factoryPid, ManagedServiceFactoryTestActivator.class ); + bundle.start(); + delay(); + + final ManagedServiceFactoryTestActivator tester = ManagedServiceFactoryTestActivator.INSTANCE; + TestCase.assertEquals( "MSF must not be updated with new configuration", 0, tester.numManagedServiceFactoryUpdatedCalls ); + + // assert getConfiguration returns the same configurtion + final Configuration c1 = getConfigurationAdmin().getConfiguration( pid, null ); + TestCase.assertEquals( "getConfiguration must retrieve required PID", pid, c1.getPid() ); + TestCase.assertEquals( "getConfiguration must retrieve new factory configuration", factoryPid, c1.getFactoryPid() ); + TestCase.assertNull( "Configuration must not have properties", c1.getProperties() ); + + TestCase.assertEquals( "MSF must not be updated with new configuration", 0, tester.numManagedServiceFactoryUpdatedCalls ); + + // restart config admin and verify getConfiguration persisted + // the new factory configuration as such + final Bundle cmBundle = getCmBundle(); + TestCase.assertNotNull( "Config Admin Bundle missing", cmBundle ); + cmBundle.stop(); + delay(); + cmBundle.start(); + delay(); + + TestCase.assertEquals( "MSF must not be updated with new configuration even after CM restart", 0, tester.numManagedServiceFactoryUpdatedCalls ); + + final Configuration c2 = getConfigurationAdmin().getConfiguration( pid, null ); + TestCase.assertEquals( "getConfiguration must retrieve required PID", pid, c2.getPid() ); + TestCase.assertEquals( "getConfiguration must retrieve new factory configuration from persistence", factoryPid, c2.getFactoryPid() ); + TestCase.assertNull( "Configuration must not have properties", c2.getProperties() ); + + c2.update( theConfig ); + delay(); + + TestCase.assertEquals( 1, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( theConfig.get( PROP_NAME ), tester.configs.get( cf.getPid() ).get( PROP_NAME ) ); + + final Configuration[] cfs = getConfigurationAdmin().listConfigurations( "(" + ConfigurationAdmin.SERVICE_FACTORYPID + "=" + + factoryPid + ")" ); + TestCase.assertNotNull( "Expect at least one configuration", cfs ); + TestCase.assertEquals( "Expect exactly one configuration", 1, cfs.length ); + TestCase.assertEquals( cf.getPid(), cfs[0].getPid() ); + TestCase.assertEquals( cf.getFactoryPid(), cfs[0].getFactoryPid() ); + } + finally + { + // make sure no configuration survives ... + getConfigurationAdmin().getConfiguration( pid, null ).delete(); + } + } + + @Test + public void test_collection_property_order() throws IOException, BundleException + { + final String pid = "test_collection_property_order"; + final String[] value = new String[] + { "a", "b", "c" }; + final Bundle cmBundle = getCmBundle(); + try + { + final Vector v = new Vector( Arrays.asList( value ) ); + getConfigurationAdmin().getConfiguration( pid ).update( new Hashtable() + { + { + put( "v", v ); + } + } ); + assertOrder( value, getConfigurationAdmin().getConfiguration( pid ).getProperties().get( "v" ) ); + + cmBundle.stop(); + cmBundle.start(); + + assertOrder( value, getConfigurationAdmin().getConfiguration( pid ).getProperties().get( "v" ) ); + getConfigurationAdmin().getConfiguration( pid, null ).delete(); + + final List l = Arrays.asList( value ); + getConfigurationAdmin().getConfiguration( pid ).update( new Hashtable() + { + { + put( "v", l ); + } + } ); + assertOrder( value, getConfigurationAdmin().getConfiguration( pid ).getProperties().get( "v" ) ); + + cmBundle.stop(); + cmBundle.start(); + + assertOrder( value, getConfigurationAdmin().getConfiguration( pid ).getProperties().get( "v" ) ); + getConfigurationAdmin().getConfiguration( pid, null ).delete(); + } + finally + { + // make sure no configuration survives ... + getConfigurationAdmin().getConfiguration( pid, null ).delete(); + } + } + + + private void assertOrder( final String[] expected, final Object actual ) + { + TestCase.assertTrue( "Actual value must be a collection", actual instanceof Collection ); + TestCase.assertEquals( "Collection must have " + expected.length + " entries", expected.length, + ( ( Collection ) actual ).size() ); + + final Iterator actualI = ( ( Collection ) actual ).iterator(); + for ( int i = 0; i < expected.length; i++ ) + { + String string = expected[i]; + TestCase.assertEquals( i + "th element must be " + string, string, actualI.next() ); + } + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationBindingTest.java b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationBindingTest.java new file mode 100644 index 00000000000..eb398440d06 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationBindingTest.java @@ -0,0 +1,1154 @@ +/* + * 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.cm.integration; + + +import java.io.IOException; +import java.util.Hashtable; +import junit.framework.TestCase; + +import org.apache.felix.cm.integration.helper.ManagedServiceFactoryTestActivator; +import org.apache.felix.cm.integration.helper.ManagedServiceFactoryTestActivator2; +import org.apache.felix.cm.integration.helper.ManagedServiceTestActivator; +import org.apache.felix.cm.integration.helper.ManagedServiceTestActivator2; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.JUnit4TestRunner; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.ConfigurationEvent; +import org.osgi.service.cm.ConfigurationListener; + + +@RunWith(JUnit4TestRunner.class) +public class ConfigurationBindingTest extends ConfigurationTestBase +{ + + static + { + // uncomment to enable debugging of this test class + // paxRunnerVmOption = DEBUG_VM_OPTION; + } + + + private ConfigListener configListener; + private ServiceRegistration configListenerReg; + + + @Override + public void setUp() + { + super.setUp(); + + configListener = new ConfigListener(); + configListenerReg = bundleContext.registerService( ConfigurationListener.class.getName(), configListener, null ); + } + + + @Override + public void tearDown() throws BundleException + { + if ( configListenerReg != null ) + { + configListenerReg.unregister(); + configListenerReg = null; + } + configListener = null; + + super.tearDown(); + } + + + @Test + public void test_configuration_unbound_on_uninstall() throws BundleException + { + String pid = "test_configuration_unbound_on_uninstall"; + configure( pid ); + + delay(); // for the event to be distributed + configListener.assertEvents( ConfigurationEvent.CM_UPDATED, 1 ); + + // ensure configuration is unbound + final Configuration beforeInstall = getConfiguration( pid ); + TestCase.assertNull( beforeInstall.getBundleLocation() ); + + bundle = installBundle( pid ); + + // ensure no configuration bound before start + final Configuration beforeStart = getConfiguration( pid ); + TestCase.assertNull( beforeInstall.getBundleLocation() ); + TestCase.assertNull( beforeStart.getBundleLocation() ); + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 0 ); + + bundle.start(); + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + + // give cm time for distribution + delay(); + + // assert activater has configuration + TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props ); + TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceUpdatedCalls ); + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 1 ); + + // ensure a freshly retrieved object also has the location + final Configuration beforeStop = getConfiguration( pid ); + TestCase.assertEquals( beforeStop.getBundleLocation(), bundle.getLocation() ); + + // check whether bundle context is set on first configuration + TestCase.assertEquals( beforeInstall.getBundleLocation(), bundle.getLocation() ); + TestCase.assertEquals( beforeStart.getBundleLocation(), bundle.getLocation() ); + + bundle.stop(); + + delay(); + + // ensure configuration still bound + TestCase.assertEquals( beforeInstall.getBundleLocation(), bundle.getLocation() ); + TestCase.assertEquals( beforeStart.getBundleLocation(), bundle.getLocation() ); + TestCase.assertEquals( beforeStop.getBundleLocation(), bundle.getLocation() ); + + // ensure a freshly retrieved object also has the location + final Configuration beforeUninstall = getConfiguration( pid ); + TestCase.assertEquals( beforeUninstall.getBundleLocation(), bundle.getLocation() ); + + bundle.uninstall(); + bundle = null; + + delay(); + + // ensure configuration is not bound any more + TestCase.assertNull( beforeInstall.getBundleLocation() ); + TestCase.assertNull( beforeStart.getBundleLocation() ); + TestCase.assertNull( beforeStop.getBundleLocation() ); + TestCase.assertNull( beforeUninstall.getBundleLocation() ); + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 1 ); + + // ensure a freshly retrieved object also does not have the location + final Configuration atEnd = getConfiguration( pid ); + TestCase.assertNull( atEnd.getBundleLocation() ); + + // remove the configuration for good + deleteConfig( pid ); + delay(); + configListener.assertEvents( ConfigurationEvent.CM_DELETED, 1 ); + } + + + @Test + public void test_configuration_unbound_on_uninstall_with_cm_restart() throws BundleException + { + final String pid = "test_configuration_unbound_on_uninstall_with_cm_restart"; + configure( pid ); + final Bundle cmBundle = getCmBundle(); + + // ensure configuration is unbound + final Configuration beforeInstall = getConfiguration( pid ); + TestCase.assertNull( beforeInstall.getBundleLocation() ); + + bundle = installBundle( pid ); + + // ensure no configuration bound before start + final Configuration beforeStart = getConfiguration( pid ); + TestCase.assertNull( beforeInstall.getBundleLocation() ); + TestCase.assertNull( beforeStart.getBundleLocation() ); + + bundle.start(); + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "IOActivator not started !!", tester ); + + // give cm time for distribution + delay(); + + // assert activater has configuration + TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props ); + TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceUpdatedCalls ); + + // ensure a freshly retrieved object also has the location + final Configuration beforeStop = getConfiguration( pid ); + TestCase.assertEquals( beforeStop.getBundleLocation(), bundle.getLocation() ); + + // check whether bundle context is set on first configuration + TestCase.assertEquals( beforeInstall.getBundleLocation(), bundle.getLocation() ); + TestCase.assertEquals( beforeStart.getBundleLocation(), bundle.getLocation() ); + + bundle.stop(); + + // ensure configuration still bound + TestCase.assertEquals( beforeInstall.getBundleLocation(), bundle.getLocation() ); + TestCase.assertEquals( beforeStart.getBundleLocation(), bundle.getLocation() ); + TestCase.assertEquals( beforeStop.getBundleLocation(), bundle.getLocation() ); + + // ensure a freshly retrieved object also has the location + final Configuration beforeUninstall = getConfiguration( pid ); + TestCase.assertEquals( beforeUninstall.getBundleLocation(), bundle.getLocation() ); + + // stop cm bundle now before uninstalling configured bundle + cmBundle.stop(); + delay(); + + // assert configuration admin service is gone + TestCase.assertNull( configAdminTracker.getService() ); + + // uninstall bundle while configuration admin is stopped + bundle.uninstall(); + bundle = null; + + // start cm bundle again after uninstallation + cmBundle.start(); + delay(); + + // ensure a freshly retrieved object also does not have the location + // FELIX-1484: this test fails due to bundle location not verified + // at first configuration access + final Configuration atEnd = getConfiguration( pid ); + TestCase.assertNull( atEnd.getBundleLocation() ); + + // remove the configuration for good + deleteConfig( pid ); + } + + + @Test + public void test_not_updated_new_configuration_not_bound_after_bundle_uninstall() throws IOException, + BundleException + { + final String pid = "test_not_updated_new_configuration_not_bound_after_bundle_uninstall"; + + // create a configuration but do not update with properties + final Configuration newConfig = configure( pid, null, false ); + TestCase.assertNull( newConfig.getProperties() ); + TestCase.assertNull( newConfig.getBundleLocation() ); + + // start and settle bundle + bundle = installBundle( pid ); + bundle.start(); + delay(); + + // ensure no properties provided to bundle + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + TestCase.assertNull( "Expect no properties after Service Registration", tester.props ); + TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceUpdatedCalls ); + + // assert configuration is still unset but bound + TestCase.assertNull( newConfig.getProperties() ); + TestCase.assertEquals( bundle.getLocation(), newConfig.getBundleLocation() ); + + // uninstall bundle, should unbind configuration + bundle.uninstall(); + bundle = null; + + delay(); + + // assert configuration is still unset and unbound + TestCase.assertNull( newConfig.getProperties() ); + TestCase.assertNull( newConfig.getBundleLocation() ); + + // remove the configuration for good + deleteConfig( pid ); + } + + + @Test + public void test_create_with_location_unbind_before_service_supply() throws BundleException, IOException + { + + final String pid = "test_create_with_location_unbind_before_service_supply"; + final String dummyLocation = "http://some/dummy/location"; + + // 1. create and statically bind the configuration + final Configuration config = configure( pid, dummyLocation, false ); + TestCase.assertEquals( pid, config.getPid() ); + TestCase.assertEquals( dummyLocation, config.getBundleLocation() ); + + // 2. update configuration + Hashtable props = new Hashtable(); + props.put( PROP_NAME, PROP_NAME ); + config.update( props ); + TestCase.assertEquals( PROP_NAME, config.getProperties().get( PROP_NAME ) ); + TestCase.assertEquals( pid, config.getPid() ); + TestCase.assertEquals( dummyLocation, config.getBundleLocation() ); + + // 3. (statically) set location to null + config.setBundleLocation( null ); + TestCase.assertNull( config.getBundleLocation() ); + + // 4. install bundle with service + bundle = installBundle( pid ); + bundle.start(); + delay(); + + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + + // assert activater has configuration (two calls, one per pid) + TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props ); + TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceUpdatedCalls ); + + TestCase.assertEquals( bundle.getLocation(), config.getBundleLocation() ); + + bundle.uninstall(); + bundle = null; + + delay(); + + // statically bound configurations must remain bound after bundle + // uninstall + TestCase.assertNull( config.getBundleLocation() ); + + // remove the configuration for good + deleteConfig( pid ); + } + + + @Test + public void test_statically_bound() throws BundleException + { + final String pid = "test_statically_bound"; + + // install the bundle (we need the location) + bundle = installBundle( pid ); + final String location = bundle.getLocation(); + + // create and statically bind the configuration + configure( pid ); + final Configuration config = getConfiguration( pid ); + TestCase.assertEquals( pid, config.getPid() ); + TestCase.assertNull( config.getBundleLocation() ); + config.setBundleLocation( location ); + TestCase.assertEquals( location, config.getBundleLocation() ); + + // ensure configuration is settled before starting the bundle + delay(); + + // expect single config update and location change + configListener.assertEvents( ConfigurationEvent.CM_UPDATED, 1 ); + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 1 ); + + bundle.start(); + + // give cm time for distribution + delay(); + + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + + // assert activater has configuration (two calls, one per pid) + TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props ); + TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceUpdatedCalls ); + + TestCase.assertEquals( location, config.getBundleLocation() ); + + // config already statically bound, no change event + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 0 ); + + bundle.uninstall(); + bundle = null; + + delay(); + + // statically bound configurations must remain bound after bundle + // uninstall + TestCase.assertEquals( location, config.getBundleLocation() ); + + // configuration statically bound, no change event + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 0 ); + + // remove the configuration for good + deleteConfig( pid ); + + delay(); + configListener.assertEvents( ConfigurationEvent.CM_DELETED, 1 ); + configListener.assertEvents( ConfigurationEvent.CM_UPDATED, 0 ); + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 0 ); + } + + + @Test + public void test_static_binding_and_unbinding() throws BundleException + { + final String pid = "test_static_binding_and_unbinding"; + final String location = bundleContext.getBundle().getLocation(); + + // create and statically bind the configuration + configure( pid ); + final Configuration config = getConfiguration( pid ); + TestCase.assertEquals( pid, config.getPid() ); + TestCase.assertNull( config.getBundleLocation() ); + + // first configuration updated event + delay(); + configListener.assertEvents( ConfigurationEvent.CM_UPDATED, 1 ); + + // bind the configuration + config.setBundleLocation( location ); + TestCase.assertEquals( location, config.getBundleLocation() ); + delay(); + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 1 ); + + // restart CM bundle + final Bundle cmBundle = getCmBundle(); + cmBundle.stop(); + delay(); + cmBundle.start(); + + // assert configuration still bound + final Configuration configAfterRestart = getConfiguration( pid ); + TestCase.assertEquals( pid, configAfterRestart.getPid() ); + TestCase.assertEquals( location, configAfterRestart.getBundleLocation() ); + + // unbind the configuration + configAfterRestart.setBundleLocation( null ); + TestCase.assertNull( configAfterRestart.getBundleLocation() ); + delay(); + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 1 ); + + // restart CM bundle + cmBundle.stop(); + delay(); + cmBundle.start(); + + // assert configuration unbound + final Configuration configUnboundAfterRestart = getConfiguration( pid ); + TestCase.assertEquals( pid, configUnboundAfterRestart.getPid() ); + TestCase.assertNull( configUnboundAfterRestart.getBundleLocation() ); + + configListener.assertEvents( ConfigurationEvent.CM_DELETED, 0 ); + configListener.assertEvents( ConfigurationEvent.CM_UPDATED, 0 ); + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 0 ); + } + + + @Test + public void test_dynamic_binding_and_unbinding() throws BundleException + { + final String pid = "test_dynamic_binding_and_unbinding"; + + // create and statically bind the configuration + configure( pid ); + final Configuration config = getConfiguration( pid ); + TestCase.assertEquals( pid, config.getPid() ); + TestCase.assertNull( config.getBundleLocation() ); + + // dynamically bind the configuration + bundle = installBundle( pid ); + final String location = bundle.getLocation(); + bundle.start(); + delay(); + TestCase.assertEquals( location, config.getBundleLocation() ); + + // restart CM bundle + final Bundle cmBundle = getCmBundle(); + cmBundle.stop(); + delay(); + cmBundle.start(); + + // assert configuration still bound + final Configuration configAfterRestart = getConfiguration( pid ); + TestCase.assertEquals( pid, configAfterRestart.getPid() ); + TestCase.assertEquals( location, configAfterRestart.getBundleLocation() ); + + // stop bundle (configuration remains bound !!) + bundle.stop(); + delay(); + TestCase.assertEquals( location, configAfterRestart.getBundleLocation() ); + + // restart CM bundle + cmBundle.stop(); + delay(); + cmBundle.start(); + + // assert configuration still bound + final Configuration configBoundAfterRestart = getConfiguration( pid ); + TestCase.assertEquals( pid, configBoundAfterRestart.getPid() ); + TestCase.assertEquals( location, configBoundAfterRestart.getBundleLocation() ); + } + + + @Test + public void test_static_binding() throws BundleException + { + final String pid = "test_static_binding"; + + // install a bundle to get a location for binding + bundle = installBundle( pid ); + final String location = bundle.getLocation(); + + // create and statically bind the configuration + configure( pid ); + final Configuration config = getConfiguration( pid ); + TestCase.assertEquals( pid, config.getPid() ); + TestCase.assertNull( config.getBundleLocation() ); + config.setBundleLocation( location ); + TestCase.assertEquals( location, config.getBundleLocation() ); + + // ensure configuration is settled before starting the bundle + delay(); + configListener.assertEvents( ConfigurationEvent.CM_UPDATED, 1 ); + + // start the bundle + bundle.start(); + delay(); + TestCase.assertEquals( location, config.getBundleLocation() ); + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 1 ); + + // assert the configuration is supplied + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props ); + TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceUpdatedCalls ); + + // remove the static binding and assert bound (again) + config.setBundleLocation( null ); + delay(); + TestCase.assertEquals( location, config.getBundleLocation() ); + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 2 ); + + // uninstall bundle and assert configuration unbound + bundle.uninstall(); + bundle = null; + delay(); + TestCase.assertNull( config.getBundleLocation() ); + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 1 ); + } + + + @Test + public void test_two_bundles_one_pid() throws BundleException, IOException + { + // 1. Bundle registers service with pid1 + final String pid = "test_two_bundles_one_pid"; + final Bundle bundleA = installBundle( pid, ManagedServiceTestActivator.class ); + final String locationA = bundleA.getLocation(); + bundleA.start(); + delay(); + + // call back with null + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNull( tester.props ); + TestCase.assertEquals( 1, tester.numManagedServiceUpdatedCalls ); + + // 2. create new Conf with pid1 and locationA. + final Configuration config = configure( pid, locationA, false ); + delay(); + + // ==> No call back. + TestCase.assertNull( tester.props ); + TestCase.assertEquals( 1, tester.numManagedServiceUpdatedCalls ); + + // 3. Configuration#update(prop) is called. + config.update( theConfig ); + delay(); + + // ==> call back with the prop. + TestCase.assertNotNull( tester.props ); + TestCase.assertEquals( 2, tester.numManagedServiceUpdatedCalls ); + + // 4. Stop BundleA + bundleA.stop(); + delay(); + + // 5. Start BundleA + bundleA.start(); + delay(); + + // ==> call back with the prop. + final ManagedServiceTestActivator tester2 = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( tester2.props ); + TestCase.assertEquals( 1, tester2.numManagedServiceUpdatedCalls ); + + // 6. Configuration#deleted() is called. + config.delete(); + delay(); + + // ==> call back with null. + TestCase.assertNull( tester2.props ); + TestCase.assertEquals( 2, tester2.numManagedServiceUpdatedCalls ); + + // 7. uninstall Bundle A for cleanup. + bundleA.uninstall(); + delay(); + + // Test 2 + + // 8. BundleA registers ManagedService with pid1. + final Bundle bundleA2 = installBundle( pid, ManagedServiceTestActivator.class ); + final String locationA2 = bundleA.getLocation(); + bundleA2.start(); + delay(); + + // call back with null + final ManagedServiceTestActivator tester21 = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNull( tester21.props ); + TestCase.assertEquals( 1, tester21.numManagedServiceUpdatedCalls ); + + // 9. create new Conf with pid1 and locationB. + final String locationB = "test:locationB/" + pid; + final Configuration configB = configure( pid, locationB, false ); + delay(); + + // ==> No call back. + TestCase.assertNull( tester21.props ); + TestCase.assertEquals( 1, tester21.numManagedServiceUpdatedCalls ); + + // 10. Configuration#update(prop) is called. + configB.update( theConfig ); + delay(); + + // ==> No call back because the Conf is not bound to locationA. + TestCase.assertNull( tester21.props ); + TestCase.assertEquals( 1, tester21.numManagedServiceUpdatedCalls ); + } + + + @Test + public void test_switch_static_binding() throws BundleException + { + // 1. create config with pid and locationA + // 2. update config with properties + final String pid = "test_switch_static_binding"; + final String locationA = "test:location/A/" + pid; + final Configuration config = configure( pid, locationA, true ); + + // 3. register ManagedService ms1 with pid from said locationA + final Bundle bundleA = installBundle( pid, ManagedServiceTestActivator.class, locationA ); + bundleA.start(); + delay(); + + // ==> configuration supplied to the service ms1 + final ManagedServiceTestActivator testerA1 = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( testerA1.props ); + TestCase.assertEquals( 1, testerA1.numManagedServiceUpdatedCalls ); + + // 4. register ManagedService ms2 with pid from locationB + final String locationB = "test:location/B/" + pid; + final Bundle bundleB = installBundle( pid, ManagedServiceTestActivator2.class, locationB ); + bundleB.start(); + delay(); + + // ==> invisible configuration supplied as null to service ms2 + final ManagedServiceTestActivator2 testerB1 = ManagedServiceTestActivator2.INSTANCE; + TestCase.assertNull( testerB1.props ); + TestCase.assertEquals( 1, testerB1.numManagedServiceUpdatedCalls ); + + // 5. Call Configuration.setBundleLocation( "locationB" ) + config.setBundleLocation( locationB ); + delay(); + + // ==> configuration is bound to locationB + TestCase.assertEquals( locationB, config.getBundleLocation() ); + + // ==> configuration removed from service ms1 + TestCase.assertNull( testerA1.props ); + TestCase.assertEquals( 2, testerA1.numManagedServiceUpdatedCalls ); + + // ==> configuration supplied to the service ms2 + TestCase.assertNotNull( testerB1.props ); + TestCase.assertEquals( 2, testerB1.numManagedServiceUpdatedCalls ); + } + + + @Test + public void test_switch_dynamic_binding() throws BundleException, IOException + { + // 1. create config with pid with null location + // 2. update config with properties + final String pid = "test_switch_dynamic_binding"; + final String locationA = "test:location/A/" + pid; + final Configuration config = configure( pid, null, true ); + + // 3. register ManagedService ms1 with pid from locationA + final Bundle bundleA = installBundle( pid, ManagedServiceTestActivator.class, locationA ); + bundleA.start(); + delay(); + + // ==> configuration supplied to the service ms1 + final ManagedServiceTestActivator testerA1 = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( testerA1.props ); + TestCase.assertEquals( 1, testerA1.numManagedServiceUpdatedCalls ); + + // ==> configuration is dynamically bound to locationA + TestCase.assertEquals( locationA, config.getBundleLocation() ); + + // 4. register ManagedService ms2 with pid from locationB + final String locationB = "test:location/B/" + pid; + final Bundle bundleB = installBundle( pid, ManagedServiceTestActivator2.class, locationB ); + bundleB.start(); + delay(); + + // ==> invisible configuration supplied as null to service ms2 + final ManagedServiceTestActivator2 testerB1 = ManagedServiceTestActivator2.INSTANCE; + TestCase.assertNull( testerB1.props ); + TestCase.assertEquals( 1, testerB1.numManagedServiceUpdatedCalls ); + + // 5. Call Configuration.setBundleLocation( "locationB" ) + config.setBundleLocation( locationB ); + delay(); + + // ==> configuration is bound to locationB + TestCase.assertEquals( locationB, config.getBundleLocation() ); + + // ==> configuration removed from service ms1 + TestCase.assertNull( testerA1.props ); + TestCase.assertEquals( 2, testerA1.numManagedServiceUpdatedCalls ); + + // ==> configuration supplied to the service ms2 + TestCase.assertNotNull( testerB1.props ); + TestCase.assertEquals( 2, testerB1.numManagedServiceUpdatedCalls ); + + // 6. Update configuration now + config.update(); + delay(); + + // ==> configuration supplied to the service ms2 + TestCase.assertNotNull( testerB1.props ); + TestCase.assertEquals( 3, testerB1.numManagedServiceUpdatedCalls ); + } + + + @Test + public void test_switch_static_binding_factory() throws BundleException, IOException + { + // 1. create config with pid and locationA + // 2. update config with properties + final String factoryPid = "test_switch_static_binding_factory"; + final String locationA = "test:location/A/" + factoryPid; + final Configuration config = createFactoryConfiguration( factoryPid, locationA, true ); + final String pid = config.getPid(); + + // 3. register ManagedService ms1 with pid from said locationA + final Bundle bundleA = installBundle( factoryPid, ManagedServiceFactoryTestActivator.class, locationA ); + bundleA.start(); + delay(); + + // ==> configuration supplied to the service ms1 + final ManagedServiceFactoryTestActivator testerA1 = ManagedServiceFactoryTestActivator.INSTANCE; + TestCase.assertNotNull( testerA1.configs.get( pid ) ); + TestCase.assertEquals( 1, testerA1.numManagedServiceFactoryUpdatedCalls ); + + // 4. register ManagedService ms2 with pid from locationB + final String locationB = "test:location/B/" + factoryPid; + final Bundle bundleB = installBundle( factoryPid, ManagedServiceFactoryTestActivator2.class, locationB ); + bundleB.start(); + delay(); + + // ==> configuration not supplied to service ms2 + final ManagedServiceFactoryTestActivator2 testerB1 = ManagedServiceFactoryTestActivator2.INSTANCE; + TestCase.assertNull( testerB1.configs.get( pid )); + TestCase.assertEquals( 0, testerB1.numManagedServiceFactoryUpdatedCalls ); + + // 5. Call Configuration.setBundleLocation( "locationB" ) + config.setBundleLocation( locationB ); + delay(); + + // ==> configuration is bound to locationB + TestCase.assertEquals( locationB, config.getBundleLocation() ); + + // ==> configuration removed from service ms1 + TestCase.assertNull( testerA1.configs.get( pid )); + TestCase.assertEquals( 1, testerA1.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 1, testerA1.numManagedServiceFactoryDeleteCalls ); + + // ==> configuration supplied to the service ms2 + TestCase.assertNotNull( testerB1.configs.get( pid ) ); + TestCase.assertEquals( 1, testerB1.numManagedServiceFactoryUpdatedCalls ); + + // 6. Update configuration now + config.update(); + delay(); + + // ==> configuration supplied to the service ms2 + TestCase.assertNotNull( testerB1.configs.get( pid ) ); + TestCase.assertEquals( 2, testerB1.numManagedServiceFactoryUpdatedCalls ); + } + + + @Test + public void test_switch_dynamic_binding_factory() throws BundleException, IOException + { + // 1. create config with pid and locationA + // 2. update config with properties + final String factoryPid = "test_switch_static_binding_factory"; + final String locationA = "test:location/A/" + factoryPid; + final Configuration config = createFactoryConfiguration( factoryPid, null, true ); + final String pid = config.getPid(); + + TestCase.assertNull( config.getBundleLocation() ); + + // 3. register ManagedService ms1 with pid from said locationA + final Bundle bundleA = installBundle( factoryPid, ManagedServiceFactoryTestActivator.class, locationA ); + bundleA.start(); + delay(); + + // ==> configuration supplied to the service ms1 + final ManagedServiceFactoryTestActivator testerA1 = ManagedServiceFactoryTestActivator.INSTANCE; + TestCase.assertNotNull( testerA1.configs.get( pid ) ); + TestCase.assertEquals( 1, testerA1.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( locationA, config.getBundleLocation() ); + + // 4. register ManagedService ms2 with pid from locationB + final String locationB = "test:location/B/" + factoryPid; + final Bundle bundleB = installBundle( factoryPid, ManagedServiceFactoryTestActivator2.class, locationB ); + bundleB.start(); + delay(); + + // ==> configuration not supplied to service ms2 + final ManagedServiceFactoryTestActivator2 testerB1 = ManagedServiceFactoryTestActivator2.INSTANCE; + TestCase.assertNull( testerB1.configs.get( pid )); + TestCase.assertEquals( 0, testerB1.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( locationA, config.getBundleLocation() ); + + // 5. Call Configuration.setBundleLocation( "locationB" ) + config.setBundleLocation( locationB ); + delay(); + + // ==> configuration is bound to locationB + TestCase.assertEquals( locationB, config.getBundleLocation() ); + + // ==> configuration removed from service ms1 + TestCase.assertNull( testerA1.configs.get( pid )); + TestCase.assertEquals( 1, testerA1.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( 1, testerA1.numManagedServiceFactoryDeleteCalls ); + + // ==> configuration supplied to the service ms2 + TestCase.assertNotNull( testerB1.configs.get( pid ) ); + TestCase.assertEquals( 1, testerB1.numManagedServiceFactoryUpdatedCalls ); + + // 6. Update configuration now + config.update(); + delay(); + + // ==> configuration supplied to the service ms2 + TestCase.assertNotNull( testerB1.configs.get( pid ) ); + TestCase.assertEquals( 2, testerB1.numManagedServiceFactoryUpdatedCalls ); + } + + + @Test + public void test_switch_dynamic_binding_after_uninstall() throws BundleException, IOException + { + // 1. create config with pid with null location + // 2. update config with properties + final String pid = "test_switch_dynamic_binding"; + final String locationA = "test:location/A/" + pid; + final Configuration config = configure( pid, null, true ); + + TestCase.assertNull( config.getBundleLocation() ); + + // 3. register ManagedService ms1 with pid from locationA + final Bundle bundleA = installBundle( pid, ManagedServiceTestActivator.class, locationA ); + bundleA.start(); + delay(); + + // ==> configuration supplied to the service ms1 + final ManagedServiceTestActivator testerA1 = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( testerA1.props ); + TestCase.assertEquals( 1, testerA1.numManagedServiceUpdatedCalls ); + + // ==> configuration is dynamically bound to locationA + TestCase.assertEquals( locationA, config.getBundleLocation() ); + + // 4. register ManagedService ms2 with pid from locationB + final String locationB = "test:location/B/" + pid; + final Bundle bundleB = installBundle( pid, ManagedServiceTestActivator2.class, locationB ); + bundleB.start(); + delay(); + + // ==> invisible configuration supplied as null to service ms2 + final ManagedServiceTestActivator2 testerB1 = ManagedServiceTestActivator2.INSTANCE; + TestCase.assertNull( testerB1.props ); + TestCase.assertEquals( 1, testerB1.numManagedServiceUpdatedCalls ); + + // 5. Uninstall bundle A + bundleA.uninstall(); + delay(); + + // ==> configuration is bound to locationB + TestCase.assertEquals( locationB, config.getBundleLocation() ); + + // ==> configuration supplied to the service ms2 + TestCase.assertNotNull( testerB1.props ); + TestCase.assertEquals( 2, testerB1.numManagedServiceUpdatedCalls ); + + // 6. Update configuration now + config.update(); + delay(); + + // ==> configuration supplied to the service ms2 + TestCase.assertNotNull( testerB1.props ); + TestCase.assertEquals( 3, testerB1.numManagedServiceUpdatedCalls ); + } + + + @Test + public void test_switch_dynamic_binding_factory_after_uninstall() throws BundleException, IOException + { + // 1. create config with pid and locationA + // 2. update config with properties + final String factoryPid = "test_switch_static_binding_factory"; + final String locationA = "test:location/A/" + factoryPid; + final Configuration config = createFactoryConfiguration( factoryPid, null, true ); + final String pid = config.getPid(); + + TestCase.assertNull( config.getBundleLocation() ); + + // 3. register ManagedService ms1 with pid from said locationA + final Bundle bundleA = installBundle( factoryPid, ManagedServiceFactoryTestActivator.class, locationA ); + bundleA.start(); + delay(); + + // ==> configuration supplied to the service ms1 + final ManagedServiceFactoryTestActivator testerA1 = ManagedServiceFactoryTestActivator.INSTANCE; + TestCase.assertNotNull( testerA1.configs.get( pid ) ); + TestCase.assertEquals( 1, testerA1.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( locationA, config.getBundleLocation() ); + + // 4. register ManagedService ms2 with pid from locationB + final String locationB = "test:location/B/" + factoryPid; + final Bundle bundleB = installBundle( factoryPid, ManagedServiceFactoryTestActivator2.class, locationB ); + bundleB.start(); + delay(); + + // ==> configuration not supplied to service ms2 + final ManagedServiceFactoryTestActivator2 testerB1 = ManagedServiceFactoryTestActivator2.INSTANCE; + TestCase.assertNull( testerB1.configs.get( pid )); + TestCase.assertEquals( 0, testerB1.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertEquals( locationA, config.getBundleLocation() ); + + // 5. Uninstall bundle A + bundleA.uninstall(); + delay(); + + // ==> configuration is bound to locationB + TestCase.assertEquals( locationB, config.getBundleLocation() ); + + // ==> configuration supplied to the service ms2 + TestCase.assertNotNull( testerB1.configs.get( pid ) ); + TestCase.assertEquals( 1, testerB1.numManagedServiceFactoryUpdatedCalls ); + + // 6. Update configuration now + config.update(); + delay(); + + // ==> configuration supplied to the service ms2 + TestCase.assertNotNull( testerB1.configs.get( pid ) ); + TestCase.assertEquals( 2, testerB1.numManagedServiceFactoryUpdatedCalls ); + } + + + @Test + public void test_location_changed_events() throws BundleException, IOException + { + String pid = "test_location_changed_events"; + configure( pid ); + delay(); + configListener.assertEvents( ConfigurationEvent.CM_UPDATED, 1 ); + + // ensure configuration is unbound + final Configuration config = getConfiguration( pid ); + TestCase.assertNull( config.getBundleLocation() ); + + bundle = installBundle( pid ); + bundle.start(); + delay(); + + // ensure no configuration bound before start + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 1 ); + + // uninstall the bundle, dynamic location changed + bundle.uninstall(); + bundle = null; + delay(); + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 1 ); + + // change the location + config.setBundleLocation( "some_location_1" ); + delay(); + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 1 ); + + // change the location + config.setBundleLocation( "some_location_2" ); + delay(); + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 1 ); + + // remove configuration, delete event + config.delete(); + delay(); + configListener.assertEvents( ConfigurationEvent.CM_DELETED, 1 ); + + // no more events + delay(); + configListener.assertEvents( ConfigurationEvent.CM_DELETED, 0 ); + configListener.assertEvents( ConfigurationEvent.CM_UPDATED, 0 ); + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 0 ); + } + + /** + * Tests configuration dynamic binding. See FELIX-3360. + */ + @SuppressWarnings({ "serial", "javadoc" }) + @Test + public void test_dynamic_binding_getConfiguration_pid() throws BundleException, IOException { + String ignoredPid = "test_dynamic_binding_getConfiguration_pid_ignored"; + String pid1 = "test_dynamic_binding_getConfiguration_pid_1"; + String pid2 = "test_dynamic_binding_getConfiguration_pid_2"; + + // ensure configuration is unbound + configure( pid1 ); + delay(); + configListener.assertEvents( ConfigurationEvent.CM_UPDATED, 1 ); + + bundle = installBundle( ignoredPid ); + bundle.start(); + delay(); + + // ensure config1 unbound + Configuration config1 = getConfiguration( pid1 ); + TestCase.assertNull( config1.getBundleLocation() ); + + ServiceReference sr = bundle.getBundleContext().getServiceReference( ConfigurationAdmin.class ); + ConfigurationAdmin bundleCa = bundle.getBundleContext().getService( sr ); + + // ensure dynamic binding + Configuration bundleConfig1 = bundleCa.getConfiguration( pid1 ); + TestCase.assertEquals( bundle.getLocation(), bundleConfig1.getBundleLocation() ); + delay(); + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 1 ); + + // create config2; ensure dynamic binding + Configuration bundleConfig2 = bundleCa.getConfiguration( pid2 ); + TestCase.assertNull(bundleConfig2.getProperties()); + TestCase.assertEquals( bundle.getLocation(), bundleConfig2.getBundleLocation() ); + bundleConfig2.update( new Hashtable() + { + { + put( "key", "value" ); + } + } ); + + // uninstall the bundle, 2 dynamic locations changed + bundle.uninstall(); + bundle = null; + delay(); + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 2 ); + + bundleConfig1 = getConfiguration( pid1 ); + TestCase.assertNull( bundleConfig1.getBundleLocation() ); + + bundleConfig2 = getConfiguration( pid2 ); + TestCase.assertNull(bundleConfig2.getBundleLocation()); + + bundleConfig1.delete(); + bundleConfig2.delete(); + } + + /** + * Tests factory configuration dynamic binding. See FELIX-3360. + */ + @SuppressWarnings({ "javadoc", "serial" }) + @Test + public void test_dynamic_binding_createFactoryConfiguration_pid() throws BundleException, IOException { + String ignoredPid = "test_dynamic_binding_createFactoryConfiguration_pid_ignored"; + String pid1 = null; + String pid2 = null; + String factoryPid1 = "test_dynamic_binding_createFactoryConfiguration_pid_1"; + String factoryPid2 = "test_dynamic_binding_createFactoryConfiguration_pid_2"; + + // ensure configuration is unbound + pid1 = createFactoryConfiguration( factoryPid1 ).getPid(); + delay(); + configListener.assertEvents( ConfigurationEvent.CM_UPDATED, 1 ); + + bundle = installBundle( ignoredPid ); + bundle.start(); + delay(); + + // ensure config1 unbound + Configuration config1 = getConfiguration( pid1 ); + TestCase.assertNull( config1.getBundleLocation() ); + + ServiceReference sr = bundle.getBundleContext().getServiceReference( ConfigurationAdmin.class ); + ConfigurationAdmin bundleCa = bundle.getBundleContext().getService( sr ); + + // ensure dynamic binding + Configuration bundleConfig1 = bundleCa.getConfiguration( pid1 ); + TestCase.assertEquals( bundle.getLocation(), bundleConfig1.getBundleLocation() ); + delay(); + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 1 ); + + // create config2; ensure dynamic binding + Configuration bundleConfig2 = bundleCa.createFactoryConfiguration( factoryPid2 ); + pid2 = bundleConfig2.getPid(); + TestCase.assertNull(bundleConfig2.getProperties()); + TestCase.assertEquals( bundle.getLocation(), bundleConfig2.getBundleLocation() ); + bundleConfig2.update( new Hashtable() + { + { + put( "key", "value" ); + } + } ); + + // uninstall the bundle, 2 dynamic locations changed + bundle.uninstall(); + bundle = null; + delay(); + configListener.assertEvents( ConfigurationEvent.CM_LOCATION_CHANGED, 2 ); + + bundleConfig1 = getConfiguration( pid1 ); + TestCase.assertNull( bundleConfig1.getBundleLocation() ); + + bundleConfig2 = getConfiguration( pid2 ); + TestCase.assertNull(bundleConfig2.getBundleLocation()); + + bundleConfig1.delete(); + bundleConfig2.delete(); + } + + private static class ConfigListener implements ConfigurationListener { + + private int[] events = new int[3]; + + public void configurationEvent( ConfigurationEvent event ) + { + events[event.getType()-1]++; + } + + + void assertEvents( final int type, final int numEvents ) + { + TestCase.assertEquals( "Events of type " + type, numEvents, events[type - 1] ); + events[type - 1] = 0; + } + + + void reset() + { + for ( int i = 0; i < events.length; i++ ) + { + events[i] = 0; + } + } + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationListenerTest.java b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationListenerTest.java new file mode 100644 index 00000000000..4961d008bd6 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationListenerTest.java @@ -0,0 +1,192 @@ +/* + * 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.cm.integration; + + +import java.io.IOException; +import java.util.Hashtable; + +import org.apache.felix.cm.integration.helper.SynchronousTestListener; +import org.apache.felix.cm.integration.helper.TestListener; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.JUnit4TestRunner; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationEvent; +import org.osgi.service.cm.ConfigurationListener; +import org.osgi.service.cm.SynchronousConfigurationListener; + + +@RunWith(JUnit4TestRunner.class) +public class ConfigurationListenerTest extends ConfigurationTestBase +{ + + static + { + // uncomment to enable debugging of this test class + // paxRunnerVmOption = DEBUG_VM_OPTION; + } + + + @Test + public void test_async_listener() throws IOException + { + final String pid = "test_listener"; + final TestListener testListener = new TestListener(); + final ServiceRegistration listener = this.bundleContext.registerService( ConfigurationListener.class.getName(), + testListener, null ); + int eventCount = 0; + + Configuration config = configure( pid, null, false ); + try + { + delay(); + testListener.assertNoEvent(); + + config.update( new Hashtable() + { + { + put( "x", "x" ); + } + } ); + delay(); + testListener.assertEvent( ConfigurationEvent.CM_UPDATED, pid, null, true, ++eventCount ); + + config.update( new Hashtable() + { + { + put( "x", "x" ); + } + } ); + delay(); + testListener.assertEvent( ConfigurationEvent.CM_UPDATED, pid, null, true, ++eventCount ); + + config.setBundleLocation( "new_Location" ); + delay(); + testListener.assertEvent( ConfigurationEvent.CM_LOCATION_CHANGED, pid, null, true, ++eventCount ); + + config.update(); + testListener.assertNoEvent(); + + config.delete(); + config = null; + delay(); + testListener.assertEvent( ConfigurationEvent.CM_DELETED, pid, null, true, ++eventCount ); + } + finally + { + if ( config != null ) + { + try + { + config.delete(); + } + catch ( IOException ioe ) + { + // ignore + } + } + + listener.unregister(); + } + } + + + @Test + public void test_sync_listener() throws IOException + { + final String pid = "test_listener"; + Configuration config = configure( pid, null, false ); + + // Synchronous listener expecting synchronous events being + // registered as a SynchronousConfigurationListener + final TestListener testListener = new SynchronousTestListener(); + final ServiceRegistration listener = this.bundleContext.registerService( + SynchronousConfigurationListener.class.getName(), testListener, null ); + + // Synchronous listener expecting asynchronous events being + // registered as a regular ConfigurationListener + final TestListener testListenerAsync = new SynchronousTestListener(); + final ServiceRegistration listenerAsync = this.bundleContext.registerService( + ConfigurationListener.class.getName(), testListenerAsync, null ); + + int eventCount = 0; + int eventCountAsync = 0; + + try + { + delay(); + testListener.assertNoEvent(); + testListenerAsync.assertNoEvent(); + + config.update( new Hashtable() + { + { + put( "x", "x" ); + } + } ); + delay(); + testListener.assertEvent( ConfigurationEvent.CM_UPDATED, pid, null, false, ++eventCount ); + testListenerAsync.assertEvent( ConfigurationEvent.CM_UPDATED, pid, null, true, ++eventCountAsync ); + + config.update( new Hashtable() + { + { + put( "x", "x" ); + } + } ); + delay(); + testListener.assertEvent( ConfigurationEvent.CM_UPDATED, pid, null, false, ++eventCount ); + testListenerAsync.assertEvent( ConfigurationEvent.CM_UPDATED, pid, null, true, ++eventCountAsync ); + + config.setBundleLocation( "new_Location" ); + delay(); + testListener.assertEvent( ConfigurationEvent.CM_LOCATION_CHANGED, pid, null, false, ++eventCount ); + testListenerAsync.assertEvent( ConfigurationEvent.CM_LOCATION_CHANGED, pid, null, true, ++eventCountAsync ); + + config.update(); + testListener.assertNoEvent(); + testListenerAsync.assertNoEvent(); + + config.delete(); + config = null; + delay(); + testListener.assertEvent( ConfigurationEvent.CM_DELETED, pid, null, false, ++eventCount ); + testListenerAsync.assertEvent( ConfigurationEvent.CM_DELETED, pid, null, true, ++eventCountAsync ); + } + finally + { + if ( config != null ) + { + try + { + config.delete(); + } + catch ( IOException ioe ) + { + // ignore + } + } + + listener.unregister(); + listenerAsync.unregister(); + } + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationTestBase.java b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationTestBase.java new file mode 100644 index 00000000000..d248d9a6196 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationTestBase.java @@ -0,0 +1,403 @@ +/* + * 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.cm.integration; + + +import static org.ops4j.pax.exam.CoreOptions.bundle; +import static org.ops4j.pax.exam.CoreOptions.cleanCaches; +import static org.ops4j.pax.exam.CoreOptions.junitBundles; +import static org.ops4j.pax.exam.CoreOptions.mavenBundle; +import static org.ops4j.pax.exam.CoreOptions.options; +import static org.ops4j.pax.exam.CoreOptions.vmOption; +import static org.ops4j.pax.exam.CoreOptions.workingDirectory; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Dictionary; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Set; + +import javax.inject.Inject; + +import org.apache.felix.cm.integration.helper.BaseTestActivator; +import org.apache.felix.cm.integration.helper.ManagedServiceTestActivator; +import org.apache.felix.cm.integration.helper.UpdateThreadSignalTask; +import org.junit.After; +import org.junit.Before; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.OptionUtils; +import org.ops4j.pax.exam.TestProbeBuilder; +import org.ops4j.pax.exam.forked.ForkedTestContainer; +import org.ops4j.pax.exam.junit.ExamFactory; +import org.ops4j.pax.exam.junit.ProbeBuilder; +import org.ops4j.pax.exam.nat.internal.NativeTestContainer; +import org.ops4j.pax.exam.nat.internal.NativeTestContainerFactory; +import org.ops4j.pax.tinybundles.core.TinyBundles; +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.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.util.tracker.ServiceTracker; + +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + + +/** + * The common integration test support class + * + * The default is always to use the {@link NativeTestContainer} as it is much + * faster. Tests that need more isolation should use the {@link ForkedTestContainer}. + */ +@ExamFactory(NativeTestContainerFactory.class) +public abstract class ConfigurationTestBase +{ + + // the name of the system property providing the bundle file to be installed and tested + protected static final String BUNDLE_JAR_SYS_PROP = "project.bundle.file"; + + // the default bundle jar file name + protected static final String BUNDLE_JAR_DEFAULT = "target/configadmin.jar"; + + // the JVM option to set to enable remote debugging + protected static final String DEBUG_VM_OPTION = "-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=30303"; + + // the actual JVM option set, extensions may implement a static + // initializer overwriting this value to have the configuration() + // method include it when starting the OSGi framework JVM + protected static String paxRunnerVmOption = null; + + @Inject + protected BundleContext bundleContext; + + protected Bundle bundle; + + protected ServiceTracker configAdminTracker; + + private Set configurations = new HashSet<>(); + + protected static final String PROP_NAME = "theValue"; + protected static final Dictionary theConfig; + + static + { + theConfig = new Hashtable<>(); + theConfig.put( PROP_NAME, PROP_NAME ); + } + + + @org.ops4j.pax.exam.junit.Configuration + public Option[] configuration() + { + final String bundleFileName = System.getProperty( BUNDLE_JAR_SYS_PROP, BUNDLE_JAR_DEFAULT ); + final File bundleFile = new File( bundleFileName ); + if ( !bundleFile.canRead() ) + { + throw new IllegalArgumentException( "Cannot read from bundle file " + bundleFileName + " specified in the " + + BUNDLE_JAR_SYS_PROP + " system property" ); + } + + final Option[] base = options( + workingDirectory("target/paxexam/"), + cleanCaches(true), + junitBundles(), + mavenBundle("org.ops4j.pax.tinybundles", "tinybundles", "1.0.0"), + bundle(bundleFile.toURI().toString()) + ); + final Option option = ( paxRunnerVmOption != null ) ? vmOption( paxRunnerVmOption ) : null; + return OptionUtils.combine(OptionUtils.combine( base, option ), additionalConfiguration()); + } + + protected Option[] additionalConfiguration() { + return null; + } + + + @Before + public void setUp() + { + configAdminTracker = new ServiceTracker<>( bundleContext, ConfigurationAdmin.class, null ); + configAdminTracker.open(); + } + + + @After + public void tearDown() throws BundleException + { + if ( bundle != null ) + { + bundle.uninstall(); + } + + for ( String pid : configurations ) + { + deleteConfig( pid ); + } + + configAdminTracker.close(); + configAdminTracker = null; + } + + + protected Bundle installBundle( final String pid ) throws BundleException + { + return installBundle( pid, ManagedServiceTestActivator.class ); + } + + + protected Bundle installBundle( final String pid, final Class activatorClass ) throws BundleException + { + return installBundle( pid, activatorClass, activatorClass.getName() ); + } + + + @ProbeBuilder + public TestProbeBuilder buildProbe( TestProbeBuilder builder ) { + return builder.setHeader(Constants.EXPORT_PACKAGE, "org.apache.felix.cm.integration.helper"); + } + + protected Bundle installBundle( final String pid, final Class activatorClass, final String location ) + throws BundleException + { + final String activatorClassName = activatorClass.getName(); + final InputStream bundleStream = TinyBundles.bundle() + .set(Constants.BUNDLE_SYMBOLICNAME, activatorClassName) + .set( Constants.BUNDLE_VERSION, "0.0.11" ) + .set( Constants.IMPORT_PACKAGE, "org.apache.felix.cm.integration.helper" ) + .set( Constants.BUNDLE_ACTIVATOR, activatorClassName ) + .set( BaseTestActivator.HEADER_PID, pid ) + .build( TinyBundles.withBnd() ); + + try + { + return bundleContext.installBundle( location, bundleStream ); + } + finally + { + try + { + bundleStream.close(); + } + catch ( IOException ioe ) + { + } + } + } + + + protected void delay() + { + Object ca = configAdminTracker.getService(); + if ( ca != null ) + { + try + { + + Field caf = ca.getClass().getDeclaredField( "configurationManager" ); + caf.setAccessible( true ); + Object cm = caf.get( ca ); + + Field cmf = cm.getClass().getDeclaredField( "updateThread" ); + cmf.setAccessible( true ); + Object ut = cmf.get( cm ); + + Method utm = ut.getClass().getDeclaredMethod( "schedule" ); + utm.setAccessible( true ); + + UpdateThreadSignalTask signalTask = new UpdateThreadSignalTask(); + utm.invoke( ut, signalTask ); + signalTask.waitSignal(); + + return; + } + catch ( AssertionFailedError afe ) + { + throw afe; + } + catch ( Throwable t ) + { + // ignore any problem and revert to timed delay (might log this) + } + } + + // no configadmin or failure while setting up task + try + { + Thread.sleep( 300 ); + } + catch ( InterruptedException ie ) + { + // dont care + } + } + + + protected Bundle getCmBundle() + { + final ServiceReference caref = configAdminTracker.getServiceReference(); + return ( caref == null ) ? null : caref.getBundle(); + } + + + protected ConfigurationAdmin getConfigurationAdmin() + { + ConfigurationAdmin ca = null; + try { + ca = configAdminTracker.waitForService(5000L); + } catch (InterruptedException e) { + // ignore + } + if ( ca == null ) + { + TestCase.fail( "Missing ConfigurationAdmin service" ); + } + return ca; + } + + + protected Configuration configure( final String pid ) + { + return configure( pid, null, true ); + } + + + protected Configuration configure( final String pid, final String location, final boolean withProps ) + { + final ConfigurationAdmin ca = getConfigurationAdmin(); + try + { + final Configuration config = ca.getConfiguration( pid, location ); + if ( withProps ) + { + config.update( theConfig ); + } + return config; + } + catch ( IOException ioe ) + { + TestCase.fail( "Failed updating configuration " + pid + ": " + ioe.toString() ); + return null; // keep the compiler quiet + } + } + + + protected Configuration createFactoryConfiguration( final String factoryPid ) + { + return createFactoryConfiguration( factoryPid, null, true ); + } + + + protected Configuration createFactoryConfiguration( final String factoryPid, final String location, + final boolean withProps ) + { + final ConfigurationAdmin ca = getConfigurationAdmin(); + try + { + final Configuration config = ca.createFactoryConfiguration( factoryPid, location ); + if ( withProps ) + { + config.update( theConfig ); + } + return config; + } + catch ( IOException ioe ) + { + TestCase.fail( "Failed updating factory configuration " + factoryPid + ": " + ioe.toString() ); + return null; // keep the compiler quiet + } + } + + + protected Configuration getConfiguration( final String pid ) + { + final ConfigurationAdmin ca = getConfigurationAdmin(); + try + { + final String filter = "(" + Constants.SERVICE_PID + "=" + pid + ")"; + final Configuration[] configs = ca.listConfigurations( filter ); + if ( configs != null && configs.length > 0 ) + { + return configs[0]; + } + } + catch ( InvalidSyntaxException ise ) + { + // unexpected + } + catch ( IOException ioe ) + { + TestCase.fail( "Failed listing configurations " + pid + ": " + ioe.toString() ); + } + + TestCase.fail( "No Configuration " + pid + " found" ); + return null; + } + + + protected void deleteConfig( final String pid ) + { + final ConfigurationAdmin ca = getConfigurationAdmin(); + try + { + configurations.remove( pid ); + final Configuration config = ca.getConfiguration( pid ); + config.delete(); + } + catch ( IOException ioe ) + { + TestCase.fail( "Failed deleting configuration " + pid + ": " + ioe.toString() ); + } + } + + + protected void deleteFactoryConfigurations( String factoryPid ) + { + ConfigurationAdmin ca = getConfigurationAdmin(); + try + { + final String filter = "(service.factoryPid=" + factoryPid + ")"; + Configuration[] configs = ca.listConfigurations( filter ); + if ( configs != null ) + { + for ( Configuration configuration : configs ) + { + configurations.remove( configuration.getPid() ); + configuration.delete(); + } + } + } + catch ( InvalidSyntaxException ise ) + { + // unexpected + } + catch ( IOException ioe ) + { + TestCase.fail( "Failed deleting configurations " + factoryPid + ": " + ioe.toString() ); + } + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/FELIX2813_ConfigurationAdminStartupTest.java b/configadmin/src/test/java/org/apache/felix/cm/integration/FELIX2813_ConfigurationAdminStartupTest.java new file mode 100644 index 00000000000..c69e47c7b5e --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/FELIX2813_ConfigurationAdminStartupTest.java @@ -0,0 +1,123 @@ +/* + * 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.cm.integration; + + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import org.apache.felix.cm.integration.helper.SynchronousTestListener; +import org.apache.felix.cm.integration.helper.TestListener; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.JUnit4TestRunner; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.ConfigurationEvent; +import org.osgi.service.cm.ConfigurationListener; +import org.osgi.service.cm.SynchronousConfigurationListener; + + +@RunWith(JUnit4TestRunner.class) +public class FELIX2813_ConfigurationAdminStartupTest extends ConfigurationTestBase implements ServiceListener +{ + + @Test + public void testAddConfigurationWhenConfigurationAdminStarts() throws InvalidSyntaxException, BundleException + { + + List bundles = new ArrayList(); + ServiceReference[] refs = configAdminTracker.getServiceReferences(); + if ( refs != null ) + { + for ( ServiceReference ref : refs ) + { + bundles.add( ref.getBundle() ); + ref.getBundle().stop(); + } + } + + final TestListener listener = new TestListener(); + bundleContext.registerService( ConfigurationListener.class.getName(), listener, null ); + final TestListener syncListener = new SynchronousTestListener(); + bundleContext.registerService( SynchronousConfigurationListener.class.getName(), syncListener, null ); + final TestListener syncListenerAsync = new SynchronousTestListener(); + bundleContext.registerService( ConfigurationListener.class.getName(), syncListenerAsync, null ); + bundleContext.addServiceListener( this, "(" + Constants.OBJECTCLASS + "=" + ConfigurationAdmin.class.getName() + + ")" ); + + for ( Bundle bundle : bundles ) + { + bundle.start(); + } + + /* + * Look at the console output for the following exception: + * + * *ERROR* Unexpected problem executing task + * java.lang.NullPointerException: reference and pid must not be null + * at org.osgi.service.cm.ConfigurationEvent.(ConfigurationEvent.java:120) + * at org.apache.felix.cm.impl.ConfigurationManager$FireConfigurationEvent.run(ConfigurationManager.java:1818) + * at org.apache.felix.cm.impl.UpdateThread.run(UpdateThread.java:104) + * at java.lang.Thread.run(Thread.java:680) + * + * It is in fact the service reference that is still null, because the service registration + * has not been 'set' yet. + * + * This following code will ensure the situation did not occurr and the + * event has effectively been sent. The eventSeen flag is set by the + * configurationEvent method when the event for the test PID has been + * received. If the flag is not set, we wait at most 2 seconds for the + * event to arrive. If the event does not arrive by then, the test is + * assumed to have failed. This will rather generate false negatives + * (on slow machines) than false positives. + */ + delay(); + listener.assertEvent( ConfigurationEvent.CM_UPDATED, "test", null, true, 1 ); + syncListener.assertEvent( ConfigurationEvent.CM_UPDATED, "test", null, false, 1 ); + syncListenerAsync.assertEvent( ConfigurationEvent.CM_UPDATED, "test", null, true, 1 ); + } + + + public void serviceChanged( ServiceEvent event ) + { + if ( event.getType() == ServiceEvent.REGISTERED ) + { + ServiceReference ref = event.getServiceReference(); + ConfigurationAdmin ca = ( ConfigurationAdmin ) bundleContext.getService( ref ); + try + { + org.osgi.service.cm.Configuration config = ca.getConfiguration( "test" ); + Hashtable props = new Hashtable(); + props.put( "abc", "123" ); + config.update( props ); + } + catch ( IOException e ) + { + } + } + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/FELIX4385_StressTest.java b/configadmin/src/test/java/org/apache/felix/cm/integration/FELIX4385_StressTest.java new file mode 100644 index 00000000000..659ce88cd21 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/FELIX4385_StressTest.java @@ -0,0 +1,227 @@ +/* + * 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.cm.integration; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import junit.framework.Assert; +import junit.framework.TestCase; + +import org.apache.felix.cm.integration.helper.Log; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.JUnit4TestRunner; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedService; + +/** + * The FELIX4385_StressTest class tests the issue related to concurrency between configuration + * creation/update/removal and ManagedService registration/unregistration. + * The test performs some loops, each one is then executing the following scenario: + * Some ManagedServices are concurrently registered in the OSGi registry using an Executor, and for each + * managed service, we create a Configuration. + * We then wait until every managed services have been updated with a non null configuration. Care is taken when a + * ManagedService is called with an initial update(null) callback, because when a configuration is created the very first + * time, an empty configuration is delivered to the corresponding managed service until the configuration is really updated. + * Once all managed services have been updated, we then concurrently unregister the managed services, and we also + * delete every created configurations. We don't use an executor when deleting configuration because the configuration + * removal is already asynchronous. + * + *

      + * @see FELIX-4385 + */ +@RunWith(JUnit4TestRunner.class) +public class FELIX4385_StressTest extends ConfigurationTestBase +{ + final static int MAXWAIT = 10000; + final static int MANAGED_SERVICES = 3; + volatile ExecutorService executor; + + @Test + public void test_ConcurrentManagedServicesWithConcurrentConfigurations() + { + final Log log = new Log(bundleContext); + log.info("starting test_ConcurrentManagedServicesWithConcurrentConfigurations"); + // Use at least 10 parallel threads, or take all available processors if the running host contains more than 10 processors. + int parallelism = Math.max(10, Runtime.getRuntime().availableProcessors()); + final ConfigurationAdmin ca = getConfigurationAdmin(); + final ExecutorService executor = Executors.newFixedThreadPool(parallelism); + try + { + int pidCounter = 0; + + long timeStamp = System.currentTimeMillis(); + for (int loop = 0; loop < 1000; loop++) + { + log.debug("loop#%d -------------------------", (loop + 1)); + + final CountDownLatch managedServiceUpdated = new CountDownLatch(MANAGED_SERVICES); + final CountDownLatch managedServiceUnregistered = new CountDownLatch(MANAGED_SERVICES); + + // Create some ManagedServices concurrently + log.info("registering aspects concurrently"); + final CopyOnWriteArrayList managedServices = new CopyOnWriteArrayList(); + final CopyOnWriteArrayList confs = new CopyOnWriteArrayList(); + + for (int i = 0; i < MANAGED_SERVICES; i++) + { + final String pid = "pid." + i + "-" + (pidCounter++); + executor.execute(new Runnable() + { + public void run() + { + Hashtable props = new Hashtable(); + props.put(Constants.SERVICE_PID, pid); + + ServiceRegistration sr = bundleContext.registerService( + ManagedService.class.getName(), + new TestManagedService(managedServiceUpdated), props); + managedServices.add(sr); + try + { + Configuration c = ca.getConfiguration(pid, null); + c.update(new Hashtable() + { + { + put("foo", "bar"); + } + }); + confs.add(c); + } + catch (IOException e) + { + log.error("could not create pid %s", e, pid); + return; + } + } + }); + } + + if (!managedServiceUpdated.await(MAXWAIT, TimeUnit.MILLISECONDS)) + { + TestCase.fail("Detected errors logged during concurrent test"); + break; + } + log.info("all managed services updated"); + + // Unregister managed services concurrently + log.info("unregistering services concurrently"); + for (final ServiceRegistration sr : managedServices) + { + executor.execute(new Runnable() + { + public void run() + { + sr.unregister(); + managedServiceUnregistered.countDown(); + } + }); + } + + // Unregister configuration concurrently + log.info("unregistering configuration concurrently"); + for (final Configuration c : confs) + { + c.delete(); + } + + // Wait until managed services have been unregistered + if (!managedServiceUnregistered.await(MAXWAIT, TimeUnit.MILLISECONDS)) + { + TestCase.fail("Managed Servives could not be unregistered timely"); + break; + } + + if (log.errorsLogged()) + { + TestCase.fail("Detected errors logged during concurrent test"); + break; + } + + log.info("finished one test loop"); + if ((loop + 1) % 100 == 0) + { + long duration = System.currentTimeMillis() - timeStamp; + System.out.println(String.format("Performed %d tests in %d ms.", (loop + 1), duration)); + timeStamp = System.currentTimeMillis(); + } + } + } + + catch (Throwable t) + { + Assert.fail("Test failed: " + t.getMessage()); + } + + finally + { + shutdown(executor); + log.close(); + } + } + + void shutdown(ExecutorService exec) + { + exec.shutdown(); + try + { + exec.awaitTermination(5, TimeUnit.SECONDS); + } + catch (InterruptedException e) + { + } + } + + /** + * One ManagedService concurrently registered in the OSGI registry. + * We count down a latch once we have been updated with our configuration. + */ + public class TestManagedService implements ManagedService + { + private final CountDownLatch latch; + private Dictionary props; + + TestManagedService(CountDownLatch latch) + { + this.latch = latch; + } + + public synchronized void updated(Dictionary properties) throws ConfigurationException + { + if (this.props == null && properties == null) + { + // GetConfiguration has been called, but configuration have not yet been delivered. + return; + } + this.props = properties; + latch.countDown(); + } + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/MultiServiceFactoryPIDTest.java b/configadmin/src/test/java/org/apache/felix/cm/integration/MultiServiceFactoryPIDTest.java new file mode 100644 index 00000000000..e25d8276028 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/MultiServiceFactoryPIDTest.java @@ -0,0 +1,259 @@ +/* + * 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.cm.integration; + + +import junit.framework.TestCase; + +import org.apache.felix.cm.integration.helper.ManagedServiceFactoryTestActivator; +import org.apache.felix.cm.integration.helper.ManagedServiceFactoryTestActivator2; +import org.apache.felix.cm.integration.helper.MultiManagedServiceFactoryTestActivator; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.JUnit4TestRunner; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; +import org.osgi.service.cm.Configuration; + + +/** + * The MultiServicePIDTest tests the case of multiple services + * bound with the same PID + */ +@RunWith(JUnit4TestRunner.class) +public class MultiServiceFactoryPIDTest extends ConfigurationTestBase +{ + static + { + // uncomment to enable debugging of this test class + // paxRunnerVmOption = DEBUG_VM_OPTION; + } + + @Test + public void test_two_services_same_pid_in_same_bundle_configure_before_registration() throws BundleException + { + final String factoryPid = "test.pid"; + + final Configuration config = createFactoryConfiguration( factoryPid ); + final String pid = config.getPid(); + TestCase.assertEquals( factoryPid, config.getFactoryPid() ); + TestCase.assertNull( config.getBundleLocation() ); + + bundle = installBundle( factoryPid, MultiManagedServiceFactoryTestActivator.class ); + bundle.start(); + + // give cm time for distribution + delay(); + + final MultiManagedServiceFactoryTestActivator tester = MultiManagedServiceFactoryTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + + // assert activater has configuration (two calls, one per pid) + TestCase.assertNotNull( "Expect Properties after Service Registration", tester.configs.get( pid ) ); + TestCase.assertEquals( "Expect two update calls", 2, tester.numManagedServiceFactoryUpdatedCalls ); + + TestCase.assertEquals( bundle.getLocation(), config.getBundleLocation() ); + + bundle.uninstall(); + bundle = null; + + delay(); + + TestCase.assertNull( config.getBundleLocation() ); + + // remove the configuration for good + deleteConfig( pid ); + } + + + @Test + public void test_two_services_same_pid_in_same_bundle_configure_after_registration() throws BundleException + { + final String factoryPid = "test.pid"; + + bundle = installBundle( factoryPid, MultiManagedServiceFactoryTestActivator.class ); + bundle.start(); + + // give cm time for distribution + delay(); + + final MultiManagedServiceFactoryTestActivator tester = MultiManagedServiceFactoryTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + + // no configuration yet + TestCase.assertTrue( "Expect Properties after Service Registration", tester.configs.isEmpty() ); + TestCase.assertEquals( "Expect two update calls", 0, tester.numManagedServiceFactoryUpdatedCalls ); + + final Configuration config = createFactoryConfiguration( factoryPid ); + final String pid = config.getPid(); + + delay(); + + TestCase.assertEquals( factoryPid, config.getFactoryPid() ); + TestCase.assertEquals( bundle.getLocation(), config.getBundleLocation() ); + + // assert activater has configuration (two calls, one per pid) + TestCase.assertNotNull( "Expect Properties after Service Registration", tester.configs.get( pid ) ); + TestCase.assertEquals( "Expect another two single update call", 2, tester.numManagedServiceFactoryUpdatedCalls ); + + bundle.uninstall(); + bundle = null; + + delay(); + + TestCase.assertNull( config.getBundleLocation() ); + + // remove the configuration for good + deleteConfig( pid ); + } + + + @Test + public void test_two_services_same_pid_in_two_bundle_configure_before_registration() throws BundleException + { + Bundle bundle2 = null; + try + { + final String factoryPid = "test.pid"; + final Configuration config = createFactoryConfiguration( factoryPid ); + final String pid = config.getPid(); + + TestCase.assertEquals( factoryPid, config.getFactoryPid() ); + TestCase.assertNull( config.getBundleLocation() ); + + bundle = installBundle( factoryPid, ManagedServiceFactoryTestActivator.class ); + bundle.start(); + + bundle2 = installBundle( factoryPid, ManagedServiceFactoryTestActivator2.class ); + bundle2.start(); + + // give cm time for distribution + delay(); + + final ManagedServiceFactoryTestActivator tester = ManagedServiceFactoryTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + + final ManagedServiceFactoryTestActivator2 tester2 = ManagedServiceFactoryTestActivator2.INSTANCE; + TestCase.assertNotNull( "Activator 2 not started !!", tester2 ); + + // expect first activator to have received properties + + // assert first bundle has configuration (two calls, one per srv) + TestCase.assertNotNull( "Expect Properties after Service Registration", tester.configs.get( pid ) ); + TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceFactoryUpdatedCalls ); + + // assert second bundle has no configuration + TestCase.assertTrue( tester2.configs.isEmpty() ); + TestCase.assertEquals( 0, tester2.numManagedServiceFactoryUpdatedCalls ); + + // expect configuration bound to first bundle + TestCase.assertEquals( bundle.getLocation(), config.getBundleLocation() ); + + bundle.uninstall(); + bundle = null; + + delay(); + + // expect configuration reassigned + TestCase.assertEquals( bundle2.getLocation(), config.getBundleLocation() ); + TestCase.assertNotNull( "Expect Properties after Configuration Redispatch", tester2.configs.get( pid ) ); + TestCase.assertEquals( "Expect update call after Configuration Redispatch", 1, tester2.numManagedServiceFactoryUpdatedCalls ); + + // remove the configuration for good + deleteConfig( pid ); + } + finally + { + if ( bundle2 != null ) + { + bundle2.uninstall(); + } + } + } + + + @Test + public void test_two_services_same_pid_in_two_bundle_configure_after_registration() throws BundleException + { + Bundle bundle2 = null; + try + { + final String factoryPid = "test.pid"; + + bundle = installBundle( factoryPid, ManagedServiceFactoryTestActivator.class ); + bundle.start(); + + bundle2 = installBundle( factoryPid, ManagedServiceFactoryTestActivator2.class ); + bundle2.start(); + + final ManagedServiceFactoryTestActivator tester = ManagedServiceFactoryTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + + final ManagedServiceFactoryTestActivator2 tester2 = ManagedServiceFactoryTestActivator2.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester2 ); + + delay(); + + // expect no configuration but a call in each service + TestCase.assertTrue( "Expect Properties after Service Registration", tester.configs.isEmpty() ); + TestCase.assertEquals( "Expect a single update call", 0, tester.numManagedServiceFactoryUpdatedCalls ); + TestCase.assertTrue( "Expect Properties after Service Registration", tester2.configs.isEmpty() ); + TestCase.assertEquals( "Expect a single update call", 0, tester2.numManagedServiceFactoryUpdatedCalls ); + + final Configuration config = createFactoryConfiguration( factoryPid ); + final String pid = config.getPid(); + + delay(); + + TestCase.assertEquals( factoryPid, config.getFactoryPid() ); + + TestCase.assertEquals( + "Configuration must be bound to second bundle because the service has higher ranking", + bundle.getLocation(), config.getBundleLocation() ); + + // configuration assigned to the first bundle + TestCase.assertNotNull( "Expect Properties after Service Registration", tester.configs.get( pid ) ); + TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceFactoryUpdatedCalls ); + + TestCase.assertTrue( "Expect Properties after Service Registration", tester2.configs.isEmpty() ); + TestCase.assertEquals( "Expect a single update call", 0, tester2.numManagedServiceFactoryUpdatedCalls ); + + bundle.uninstall(); + bundle = null; + + delay(); + + // expect configuration reassigned + TestCase.assertEquals( bundle2.getLocation(), config.getBundleLocation() ); + TestCase.assertNotNull( "Expect Properties after Configuration Redispatch", tester2.configs.get( pid ) ); + TestCase.assertEquals( "Expect a single update call after Configuration Redispatch", 1, tester2.numManagedServiceFactoryUpdatedCalls ); + + // remove the configuration for good + deleteConfig( pid ); + } + finally + { + if ( bundle2 != null ) + { + bundle2.uninstall(); + } + } + } + +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/MultiServicePIDTest.java b/configadmin/src/test/java/org/apache/felix/cm/integration/MultiServicePIDTest.java new file mode 100644 index 00000000000..c0f547bfc4f --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/MultiServicePIDTest.java @@ -0,0 +1,272 @@ +/* + * 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.cm.integration; + + +import junit.framework.TestCase; + +import org.apache.felix.cm.integration.helper.ManagedServiceTestActivator; +import org.apache.felix.cm.integration.helper.ManagedServiceTestActivator2; +import org.apache.felix.cm.integration.helper.MultiManagedServiceTestActivator; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.JUnit4TestRunner; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; +import org.osgi.service.cm.Configuration; + + +/** + * The MultiServicePIDTest tests the case of multiple services + * bound with the same PID + */ +@RunWith(JUnit4TestRunner.class) +public class MultiServicePIDTest extends ConfigurationTestBase +{ + static + { + // uncomment to enable debugging of this test class + // paxRunnerVmOption = DEBUG_VM_OPTION; + } + + @Test + public void test_two_services_same_pid_in_same_bundle_configure_before_registration() throws BundleException + { + final String pid = "test.pid"; + + configure( pid ); + + final Configuration config = getConfiguration( pid ); + TestCase.assertEquals( pid, config.getPid() ); + TestCase.assertNull( config.getBundleLocation() ); + + bundle = installBundle( pid, MultiManagedServiceTestActivator.class ); + bundle.start(); + + // give cm time for distribution + delay(); + + final MultiManagedServiceTestActivator tester = MultiManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + + // assert activater has configuration (two calls, one per pid) + TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props ); + TestCase.assertEquals( "Expect two update calls", 2, tester.numManagedServiceUpdatedCalls ); + + TestCase.assertEquals( bundle.getLocation(), config.getBundleLocation() ); + + bundle.uninstall(); + bundle = null; + + delay(); + + TestCase.assertNull( config.getBundleLocation() ); + + // remove the configuration for good + deleteConfig( pid ); + } + + + @Test + public void test_two_services_same_pid_in_same_bundle_configure_after_registration() throws BundleException + { + final String pid = "test.pid"; + + bundle = installBundle( pid, MultiManagedServiceTestActivator.class ); + bundle.start(); + + // give cm time for distribution + delay(); + + final MultiManagedServiceTestActivator tester = MultiManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + + // no configuration yet + TestCase.assertNull( "Expect Properties after Service Registration", tester.props ); + TestCase.assertEquals( "Expect two update calls", 2, tester.numManagedServiceUpdatedCalls ); + + configure( pid ); + delay(); + + final Configuration config = getConfiguration( pid ); + TestCase.assertEquals( pid, config.getPid() ); + TestCase.assertEquals( bundle.getLocation(), config.getBundleLocation() ); + + // assert activater has configuration (two calls, one per pid) + TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props ); + TestCase.assertEquals( "Expect another two single update call", 4, tester.numManagedServiceUpdatedCalls ); + + bundle.uninstall(); + bundle = null; + + delay(); + + TestCase.assertNull( config.getBundleLocation() ); + + // remove the configuration for good + deleteConfig( pid ); + } + + + @Test + public void test_two_services_same_pid_in_two_bundle_configure_before_registration() throws BundleException + { + Bundle bundle2 = null; + try + { + final String pid = "test.pid"; + + configure( pid ); + + final Configuration config = getConfiguration( pid ); + TestCase.assertEquals( pid, config.getPid() ); + TestCase.assertNull( config.getBundleLocation() ); + + bundle = installBundle( pid, ManagedServiceTestActivator.class ); + bundle.start(); + + bundle2 = installBundle( pid, ManagedServiceTestActivator2.class ); + bundle2.start(); + + // give cm time for distribution + delay(); + + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + + final ManagedServiceTestActivator2 tester2 = ManagedServiceTestActivator2.INSTANCE; + TestCase.assertNotNull( "Activator 2 not started !!", tester2 ); + + // expect first activator to have received properties + + // assert first bundle has configuration (one calls, one per srv) + TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props ); + TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceUpdatedCalls ); + + // assert second bundle has no configuration (but called with null) + TestCase.assertNull( tester2.props ); + TestCase.assertEquals( 1, tester2.numManagedServiceUpdatedCalls ); + + // expect configuration bound to first bundle + TestCase.assertEquals( bundle.getLocation(), config.getBundleLocation() ); + + bundle.uninstall(); + bundle = null; + + delay(); + + // after uninstallation, the configuration is redispatched + // due to the dynamic binding being removed + + // expect configuration reassigned + TestCase.assertEquals( bundle2.getLocation(), config.getBundleLocation() ); + + // assert second bundle now has the configuration + TestCase.assertNotNull( "Expect Properties after Configuration redispatch", tester2.props ); + TestCase.assertEquals( "Expect a single update call after Configuration redispatch", 2, + tester2.numManagedServiceUpdatedCalls ); + + // remove the configuration for good + deleteConfig( pid ); + } + finally + { + if ( bundle2 != null ) + { + bundle2.uninstall(); + } + } + } + + + @Test + public void test_two_services_same_pid_in_two_bundle_configure_after_registration() throws BundleException + { + Bundle bundle2 = null; + try + { + final String pid = "test.pid"; + + bundle = installBundle( pid, ManagedServiceTestActivator.class ); + bundle.start(); + + bundle2 = installBundle( pid, ManagedServiceTestActivator2.class ); + bundle2.start(); + + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + + final ManagedServiceTestActivator2 tester2 = ManagedServiceTestActivator2.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester2 ); + + delay(); + + // expect no configuration but a call in each service + TestCase.assertNull( "Expect Properties after Service Registration", tester.props ); + TestCase.assertEquals( "Expect a single update call", 1, tester.numManagedServiceUpdatedCalls ); + TestCase.assertNull( "Expect Properties after Service Registration", tester2.props ); + TestCase.assertEquals( "Expect a single update call", 1, tester2.numManagedServiceUpdatedCalls ); + + configure( pid ); + + delay(); + + final Configuration config = getConfiguration( pid ); + TestCase.assertEquals( pid, config.getPid() ); + + TestCase.assertEquals( + "Configuration must be bound to first bundle because the service has higher ranking", + bundle.getLocation(), config.getBundleLocation() ); + + // configuration assigned to the first bundle + TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props ); + TestCase.assertEquals( "Expect a single update call", 2, tester.numManagedServiceUpdatedCalls ); + + TestCase.assertNull( "Expect Properties after Service Registration", tester2.props ); + TestCase.assertEquals( "Expect a single update call", 1, tester2.numManagedServiceUpdatedCalls ); + + bundle.uninstall(); + bundle = null; + + delay(); + + // after uninstallation, the configuration is redispatched + // due to the dynamic binding being removed + + // expect configuration reassigned + TestCase.assertEquals( bundle2.getLocation(), config.getBundleLocation() ); + + // assert second bundle now has the configuration + TestCase.assertNotNull( "Expect Properties after Configuration redispatch", tester2.props ); + TestCase.assertEquals( "Expect a single update call after Configuration redispatch", 2, + tester2.numManagedServiceUpdatedCalls ); + + // remove the configuration for good + deleteConfig( pid ); + } + finally + { + if ( bundle2 != null ) + { + bundle2.uninstall(); + } + } + } + +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/MultiValuePIDTest.java b/configadmin/src/test/java/org/apache/felix/cm/integration/MultiValuePIDTest.java new file mode 100644 index 00000000000..e0ed8da2d45 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/MultiValuePIDTest.java @@ -0,0 +1,130 @@ +/* + * 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.cm.integration; + + +import junit.framework.TestCase; + +import org.apache.felix.cm.integration.helper.ManagedServiceTestActivator; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.JUnit4TestRunner; +import org.osgi.framework.BundleException; +import org.osgi.service.cm.Configuration; + + +@RunWith(JUnit4TestRunner.class) +public class MultiValuePIDTest extends ConfigurationTestBase +{ + + @Test + public void test_multi_value_pid_array() throws BundleException + { + final String pid1 = "test.pid.1"; + final String pid2 = "test.pid.2"; + + configure( pid1 ); + configure( pid2 ); + + final Configuration config1 = getConfiguration( pid1 ); + TestCase.assertEquals( pid1, config1.getPid() ); + TestCase.assertNull( config1.getBundleLocation() ); + + final Configuration config2 = getConfiguration( pid2 ); + TestCase.assertEquals( pid2, config2.getPid() ); + TestCase.assertNull( config2.getBundleLocation() ); + + // multi-pid with array + bundle = installBundle( pid1 + "," + pid2 ); + bundle.start(); + + // give cm time for distribution + delay(); + + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + + // assert activater has configuration (two calls, one per pid) + TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props ); + TestCase.assertEquals( "Expect a single update call", 2, tester.numManagedServiceUpdatedCalls ); + + TestCase.assertEquals( bundle.getLocation(), config1.getBundleLocation() ); + TestCase.assertEquals( bundle.getLocation(), config2.getBundleLocation() ); + + bundle.uninstall(); + bundle = null; + + delay(); + + TestCase.assertNull( config1.getBundleLocation() ); + TestCase.assertNull( config2.getBundleLocation() ); + + // remove the configuration for good + deleteConfig( pid1 ); + deleteConfig( pid2 ); + } + + + @Test + public void test_multi_value_pid_collection() throws BundleException + { + String pid1 = "test.pid.1"; + String pid2 = "test.pid.2"; + + configure( pid1 ); + configure( pid2 ); + + final Configuration config1 = getConfiguration( pid1 ); + TestCase.assertEquals( pid1, config1.getPid() ); + TestCase.assertNull( config1.getBundleLocation() ); + + final Configuration config2 = getConfiguration( pid2 ); + TestCase.assertEquals( pid2, config2.getPid() ); + TestCase.assertNull( config2.getBundleLocation() ); + + // multi-pid with collection + bundle = installBundle( pid1 + ";" + pid2 ); + bundle.start(); + + // give cm time for distribution + delay(); + + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + + // assert activater has configuration (two calls, one per pid) + TestCase.assertNotNull( "Expect Properties after Service Registration", tester.props ); + TestCase.assertEquals( "Expect a single update call", 2, tester.numManagedServiceUpdatedCalls ); + + TestCase.assertEquals( bundle.getLocation(), config1.getBundleLocation() ); + TestCase.assertEquals( bundle.getLocation(), config2.getBundleLocation() ); + + bundle.uninstall(); + bundle = null; + + delay(); + + TestCase.assertNull( config1.getBundleLocation() ); + TestCase.assertNull( config2.getBundleLocation() ); + + // remove the configuration for good + deleteConfig( pid1 ); + deleteConfig( pid2 ); + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/TargetedPidTest.java b/configadmin/src/test/java/org/apache/felix/cm/integration/TargetedPidTest.java new file mode 100644 index 00000000000..b35d08e8199 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/TargetedPidTest.java @@ -0,0 +1,273 @@ +/* + * 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.cm.integration; + + +import junit.framework.TestCase; + +import org.apache.felix.cm.integration.helper.ManagedServiceTestActivator; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.JUnit4TestRunner; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; + + +@RunWith(JUnit4TestRunner.class) +public class TargetedPidTest extends ConfigurationTestBase +{ + + static + { + // uncomment to enable debugging of this test class + // paxRunnerVmOption = DEBUG_VM_OPTION; + } + + + @Test + public void test_targetet_pid_no_replace() throws BundleException + { + String basePid = "test_targeted"; + String[] pids = null; + try + { + + // start the bundle and assert this + bundle = installBundle( basePid ); + bundle.start(); + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + + // give cm time for distribution + delay(); + + // assert activater has configuration + int callCount = 0; + TestCase.assertNull( "Expect no Properties after Service Registration", tester.props ); + TestCase.assertEquals( "Expect calls", ++callCount, tester.numManagedServiceUpdatedCalls ); + + pids = new String[] + { + basePid, + String.format( "%s|%s", basePid, bundle.getSymbolicName() ), + String.format( "%s|%s|%s", basePid, bundle.getSymbolicName(), + bundle.getHeaders().get( Constants.BUNDLE_VERSION ) ), + String.format( "%s|%s|%s|%s", basePid, bundle.getSymbolicName(), + bundle.getHeaders().get( Constants.BUNDLE_VERSION ), bundle.getLocation() ) }; + + for (String pid : pids) { + configure( pid ); + delay(); + TestCase.assertNotNull( "Expect Properties after update " + pid, tester.props ); + TestCase.assertEquals( "Expect PID", pid, tester.props.get( Constants.SERVICE_PID ) ); + TestCase.assertEquals( "Expect calls", ++callCount, tester.numManagedServiceUpdatedCalls ); + deleteConfig( pid ); + delay(); + TestCase.assertNull( "Expect no Properties after delete " + pid, tester.props ); + TestCase.assertEquals( "Expect calls", ++callCount, tester.numManagedServiceUpdatedCalls ); + } + + // cleanup + bundle.uninstall(); + bundle = null; + + } + finally + { + // remove the configuration for good + if ( pids != null ) + { + for ( String p : pids ) + { + deleteConfig( p ); + } + } + } + } + + @Test + public void test_targetet_pid_replace() throws BundleException + { + String basePid = "test_targeted"; + String[] pids = null; + try + { + + // start the bundle and assert this + bundle = installBundle( basePid ); + bundle.start(); + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + + // give cm time for distribution + delay(); + + // assert activater has configuration + int callCount = 0; + TestCase.assertNull( "Expect no Properties after Service Registration", tester.props ); + TestCase.assertEquals( "Expect calls", ++callCount, tester.numManagedServiceUpdatedCalls ); + + pids = new String[] + { + basePid, + String.format( "%s|%s", basePid, bundle.getSymbolicName() ), + String.format( "%s|%s|%s", basePid, bundle.getSymbolicName(), + bundle.getHeaders().get( Constants.BUNDLE_VERSION ) ), + String.format( "%s|%s|%s|%s", basePid, bundle.getSymbolicName(), + bundle.getHeaders().get( Constants.BUNDLE_VERSION ), bundle.getLocation() ) }; + + for (String pid : pids) { + configure( pid ); + delay(); + TestCase.assertNotNull( "Expect Properties after update " + pid, tester.props ); + TestCase.assertEquals( "Expect PID", pid, tester.props.get( Constants.SERVICE_PID ) ); + TestCase.assertEquals( "Expect calls", ++callCount, tester.numManagedServiceUpdatedCalls ); + } + + // cleanup + bundle.uninstall(); + bundle = null; + + } + finally + { + // remove the configuration for good + if ( pids != null ) + { + for ( String p : pids ) + { + deleteConfig( p ); + } + } + } + } + + @Test + public void test_targetet_pid_delete_fallback() throws BundleException + { + String basePid = "test_targeted"; + String[] pids = null; + try + { + + // start the bundle and assert this + bundle = installBundle( basePid ); + + pids = new String[] + { + String.format( "%s|%s|%s|%s", basePid, bundle.getSymbolicName(), + bundle.getHeaders().get( Constants.BUNDLE_VERSION ), bundle.getLocation() ), + String.format( "%s|%s|%s", basePid, bundle.getSymbolicName(), + bundle.getHeaders().get( Constants.BUNDLE_VERSION ) ), + String.format( "%s|%s", basePid, bundle.getSymbolicName() ), basePid }; + + for (String pid : pids) { + configure( pid ); + } + + + bundle.start(); + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + + // give cm time for distribution + delay(); + + // assert activater has configuration + int callCount = 0; + for (String pid : pids) { + TestCase.assertNotNull( "Expect Properties after update " + pid, tester.props ); + TestCase.assertEquals( "Expect PID", pid, tester.props.get( Constants.SERVICE_PID ) ); + TestCase.assertEquals( "Expect calls", ++callCount, tester.numManagedServiceUpdatedCalls ); + + deleteConfig( pid ); + delay(); + } + + // final delete + TestCase.assertNull( "Expect Properties after delete", tester.props ); + TestCase.assertEquals( "Expect calls", ++callCount, tester.numManagedServiceUpdatedCalls ); + + // cleanup + bundle.uninstall(); + bundle = null; + + } + finally + { + // remove the configuration for good + if ( pids != null ) + { + for ( String p : pids ) + { + deleteConfig( p ); + } + } + } + } + + @Test + public void test_pid_with_pipe() throws BundleException + { + final String pid0 = "test_targeted"; + final String pid1 = String.format( "%s|%s", pid0, ManagedServiceTestActivator.class.getName() ); + try + { + + // start the bundle and assert this + bundle = installBundle( pid1 ); + + configure( pid0 ); + configure( pid1 ); + + bundle.start(); + final ManagedServiceTestActivator tester = ManagedServiceTestActivator.INSTANCE; + TestCase.assertNotNull( "Activator not started !!", tester ); + + // give cm time for distribution + delay(); + + // assert activater has configuration + int callCount = 0; + TestCase.assertNotNull( "Expect Properties after update", tester.props ); + TestCase.assertEquals( "Expect PID", pid1, tester.props.get( Constants.SERVICE_PID ) ); + TestCase.assertEquals( "Expect calls", ++callCount, tester.numManagedServiceUpdatedCalls ); + + // delete pid1 - don't expect pid0 is assigned + deleteConfig( pid1 ); + delay(); + + // final delete + TestCase.assertNull( "Expect no Properties after delete", tester.props ); + TestCase.assertEquals( "Expect calls", ++callCount, tester.numManagedServiceUpdatedCalls ); + + // cleanup + bundle.uninstall(); + bundle = null; + + } + finally + { + // remove the configuration for good + deleteConfig( pid0 ); + deleteConfig( pid1 ); + } + } + +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/BaseTestActivator.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/BaseTestActivator.java new file mode 100644 index 00000000000..9919960b75e --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/BaseTestActivator.java @@ -0,0 +1,121 @@ +/* + * 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.cm.integration.helper; + + +import java.util.Arrays; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.service.cm.ManagedService; +import org.osgi.service.cm.ManagedServiceFactory; + + +public abstract class BaseTestActivator implements BundleActivator, ManagedService, ManagedServiceFactory +{ + + // the bundle manifest header naming a pid of configurations we require + public static final String HEADER_PID = "The-Test-PID"; + + public int numManagedServiceUpdatedCalls = 0; + public int numManagedServiceFactoryUpdatedCalls = 0; + public int numManagedServiceFactoryDeleteCalls = 0; + + public Dictionary props = null; + + public Map configs = new HashMap(); + + + // ---------- ManagedService + + public void updated( Dictionary props ) + { + numManagedServiceUpdatedCalls++; + this.props = props; + + if ( props != null ) + { + this.configs.put( ( String ) props.get( Constants.SERVICE_PID ), props ); + } + } + + + // ---------- ManagedServiceFactory + + public String getName() + { + return getClass().getName(); + } + + + public void deleted( String pid ) + { + numManagedServiceFactoryDeleteCalls++; + this.configs.remove( pid ); + } + + + public void updated( String pid, Dictionary props ) + { + numManagedServiceFactoryUpdatedCalls++; + this.configs.put( pid, props ); + } + + + protected Dictionary getServiceProperties( BundleContext bundleContext ) throws Exception + { + final Object prop = bundleContext.getBundle().getHeaders().get( HEADER_PID ); + if ( prop instanceof String ) + { + final Hashtable props = new Hashtable(); + + // multi-value PID support + props.put( Constants.SERVICE_PID, toServicePidObject( ( String ) prop ) ); + + return props; + } + + // missing pid, fail + throw new Exception( "Missing " + HEADER_PID + " manifest header, cannot start" ); + } + + + protected Object toServicePidObject( final String pid ) + { + if ( pid.indexOf( ',' ) > 0 ) + { + final String[] pids = pid.split( "," ); + return pids; + } + else if ( pid.indexOf( ';' ) > 0 ) + { + final String[] pids = pid.split( ";" ); + return Arrays.asList( pids ); + } + else + { + return pid; + } + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ConfigurationListenerTestActivator.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ConfigurationListenerTestActivator.java new file mode 100644 index 00000000000..989d598b220 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ConfigurationListenerTestActivator.java @@ -0,0 +1,115 @@ +/* + * 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.cm.integration.helper; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.Dictionary; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.service.cm.ConfigurationEvent; +import org.osgi.service.cm.ConfigurationListener; + +public class ConfigurationListenerTestActivator extends BaseTestActivator implements ConfigurationListener +{ + public static ConfigurationListenerTestActivator INSTANCE; + + private BundleContext context; + + public int numListenerUpdatedCalls = 0; + public int numListenerDeleteCalls = 0; + public int numListenerLocationChangedCalls = 0; + + public void start( BundleContext context ) throws Exception + { + this.context = context; + context.registerService( ConfigurationListener.class.getName(), this, null ); + INSTANCE = this; + } + + + public void stop( BundleContext arg0 ) throws Exception + { + INSTANCE = null; + } + + @Override + public void configurationEvent(ConfigurationEvent event) { + + String pid = event.getPid(); + + switch( event.getType() ) { + case ConfigurationEvent.CM_DELETED : + numListenerDeleteCalls++; + this.configs.remove( pid ); + return; + case ConfigurationEvent.CM_LOCATION_CHANGED : + numListenerLocationChangedCalls++; + return; + case ConfigurationEvent.CM_UPDATED : + numListenerUpdatedCalls++; + // Deliberate fall through + } + + try { + if( !pid.equals(getServiceProperties( context ).get( Constants.SERVICE_PID ))) { + return; + } + } catch ( Exception e1 ) { + e1.printStackTrace(); + return; + } + + + Dictionary props; + + try { + props = context.getService( event.getReference() ) + .getConfiguration( pid ).getProperties(); + } catch ( IOException ioe ) { + ioe.printStackTrace(); + return; + } + + this.configs.put( pid, props ); + + // Opening a socket is a secure action which Config Admin doesn't have permission to do + + int port = 0; + ServerSocket ss = null; + try { + ss = new ServerSocket( 0 ); + port = ss.getLocalPort(); + } catch ( Exception e ) { + e.printStackTrace(); + return; + } finally { + if (ss != null) { + try { + ss.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + props.put( "port", port ); + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ConfigureThread.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ConfigureThread.java new file mode 100644 index 00000000000..ae849ba23e0 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ConfigureThread.java @@ -0,0 +1,91 @@ +/* + * 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.cm.integration.helper; + + +import java.io.IOException; +import java.util.Hashtable; + +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + + +/** + * The ConfigureThread class is extends the {@link TestThread} for + * use as the configuration creator and updater in the + * {@link org.apache.felix.cm.integration.ConfigUpdateStressTest}. + */ +public class ConfigureThread extends TestThread +{ + private final Configuration config; + + private final Hashtable props; + + + public ConfigureThread( final ConfigurationAdmin configAdmin, final String pid, final boolean isFactory ) + throws IOException + { + // ensure configuration and disown it + final Configuration config; + if ( isFactory ) + { + config = configAdmin.createFactoryConfiguration( pid ); + } + else + { + config = configAdmin.getConfiguration( pid ); + } + config.setBundleLocation( null ); + + Hashtable props = new Hashtable(); + props.put( "prop1", "aValue" ); + props.put( "prop2", 4711 ); + + this.config = config; + this.props = props; + } + + + @Override + public void doRun() + { + try + { + config.update( props ); + } + catch ( IOException ioe ) + { + // ignore + } + } + + + @Override + public void cleanup() + { + try + { + config.delete(); + } + catch ( IOException ioe ) + { + // ignore + } + } +} \ No newline at end of file diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/Log.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/Log.java new file mode 100644 index 00000000000..d30da00dbe2 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/Log.java @@ -0,0 +1,239 @@ +/* + * 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.cm.integration.helper; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.log.LogService; + +/** + * OSGi log service which logs messages to standard output. + * This class can also be used to detect if the ConfigurationAdmin service has logged + * some warnings during a stress integration test. + */ +public class Log implements LogService, FrameworkListener +{ + // Default OSGI log service level logged to standard output. + private final static int LOG_LEVEL = LogService.LOG_WARNING; + + // Flag used to check if some errors have been logged during the execution of a given test. + private volatile boolean m_errorsLogged; + + // We implement OSGI log service. + protected ServiceRegistration logService; + + // Bundle context used to register our log listener + private BundleContext ctx; + + /** + * Default constructor. + * @Param ctx the Bundle Context used to register this log service. The {@link #close} must + * be called when the logger is not used anymore. + */ + public Log(BundleContext ctx) + { + this.ctx = ctx; + logService = ctx.registerService(LogService.class.getName(), this, null); + ctx.addFrameworkListener(this); + } + + /** + * Unregister our log listener + */ + public void close() + { + logService.unregister(); + ctx.removeFrameworkListener(this); + } + + public void log(int level, String message) + { + checkError(level, null); + if (LOG_LEVEL >= level) + { + System.out.println(getLevel(level) + " - " + Thread.currentThread().getName() + " : " + message); + } + } + + public void log(int level, String message, Throwable exception) + { + checkError(level, exception); + if (LOG_LEVEL >= level) + { + StringBuilder sb = new StringBuilder(); + sb.append(getLevel(level) + " - " + Thread.currentThread().getName() + " : "); + sb.append(message); + parse(sb, exception); + System.out.println(sb.toString()); + } + } + + public void log(ServiceReference sr, int level, String message) + { + checkError(level, null); + if (LOG_LEVEL >= level) + { + StringBuilder sb = new StringBuilder(); + sb.append(getLevel(level) + " - " + Thread.currentThread().getName() + " : "); + sb.append(message); + System.out.println(sb.toString()); + } + } + + public void log(ServiceReference sr, int level, String message, Throwable exception) + { + checkError(level, exception); + if (LOG_LEVEL >= level) + { + StringBuilder sb = new StringBuilder(); + sb.append(getLevel(level) + " - " + Thread.currentThread().getName() + " : "); + sb.append(message); + parse(sb, exception); + System.out.println(sb.toString()); + } + } + + public boolean errorsLogged() + { + return m_errorsLogged; + } + + private void parse(StringBuilder sb, Throwable t) + { + if (t != null) + { + sb.append(" - "); + StringWriter buffer = new StringWriter(); + PrintWriter pw = new PrintWriter(buffer); + t.printStackTrace(pw); + sb.append(buffer.toString()); + m_errorsLogged = true; + } + } + + private String getLevel(int level) + { + switch (level) + { + case LogService.LOG_DEBUG: + return "DEBUG"; + case LogService.LOG_ERROR: + return "ERROR"; + case LogService.LOG_INFO: + return "INFO"; + case LogService.LOG_WARNING: + return "WARN"; + default: + return ""; + } + } + + private void checkError(int level, Throwable exception) + { + if (level <= LOG_ERROR) + { + m_errorsLogged = true; + } + if (exception != null) + { + m_errorsLogged = true; + } + } + + public void frameworkEvent(FrameworkEvent event) + { + int eventType = event.getType(); + String msg = getFrameworkEventMessage(eventType); + int level = (eventType == FrameworkEvent.ERROR) ? LOG_ERROR : LOG_WARNING; + if (msg != null) + { + log(level, msg, event.getThrowable()); + } + else + { + log(level, "Unknown fwk event: " + event); + } + } + + private String getFrameworkEventMessage(int event) + { + switch (event) + { + case FrameworkEvent.ERROR: + return "FrameworkEvent: ERROR"; + case FrameworkEvent.INFO: + return "FrameworkEvent INFO"; + case FrameworkEvent.PACKAGES_REFRESHED: + return "FrameworkEvent: PACKAGE REFRESHED"; + case FrameworkEvent.STARTED: + return "FrameworkEvent: STARTED"; + case FrameworkEvent.STARTLEVEL_CHANGED: + return "FrameworkEvent: STARTLEVEL CHANGED"; + case FrameworkEvent.WARNING: + return "FrameworkEvent: WARNING"; + default: + return null; + } + } + + public void warn(String msg, Object... params) + { + if (LOG_LEVEL >= LogService.LOG_WARNING) + { + log(LogService.LOG_WARNING, params.length > 0 ? String.format(msg, params) : msg); + } + } + + public void info(String msg, Object... params) + { + if (LOG_LEVEL >= LogService.LOG_INFO) + { + log(LogService.LOG_INFO, params.length > 0 ? String.format(msg, params) : msg); + } + } + + public void debug(String msg, Object... params) + { + if (LOG_LEVEL >= LogService.LOG_DEBUG) + { + log(LogService.LOG_DEBUG, params.length > 0 ? String.format(msg, params) : msg); + } + } + + public void error(String msg, Object... params) + { + log(LogService.LOG_ERROR, params.length > 0 ? String.format(msg, params) : msg); + } + + public void error(String msg, Throwable err, Object... params) + { + log(LogService.LOG_ERROR, params.length > 0 ? String.format(msg, params) : msg, err); + } + + public void error(Throwable err) + { + log(LogService.LOG_ERROR, "error", err); + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceFactoryTestActivator.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceFactoryTestActivator.java new file mode 100644 index 00000000000..204d446e27d --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceFactoryTestActivator.java @@ -0,0 +1,59 @@ +/* + * 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.cm.integration.helper; + + +import java.util.Dictionary; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.ManagedServiceFactory; + + +public class ManagedServiceFactoryTestActivator extends BaseTestActivator +{ + + public static ManagedServiceFactoryTestActivator INSTANCE; + + private Dictionary registrationProps; + private ServiceRegistration registration; + + + public void start( BundleContext context ) throws Exception + { + this.registrationProps = getServiceProperties( context ); + this.registration = context.registerService( ManagedServiceFactory.class.getName(), this, + this.registrationProps ); + INSTANCE = this; + } + + + public void stop( BundleContext arg0 ) throws Exception + { + INSTANCE = null; + } + + + public void changePid( final String newPid ) + { + this.registrationProps.put( Constants.SERVICE_PID, toServicePidObject( newPid ) ); + this.registration.setProperties( this.registrationProps ); + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceFactoryTestActivator2.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceFactoryTestActivator2.java new file mode 100644 index 00000000000..398cbe94666 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceFactoryTestActivator2.java @@ -0,0 +1,42 @@ +/* + * 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.cm.integration.helper; + + +import org.osgi.framework.BundleContext; +import org.osgi.service.cm.ManagedServiceFactory; + + +public class ManagedServiceFactoryTestActivator2 extends BaseTestActivator +{ + public static ManagedServiceFactoryTestActivator2 INSTANCE; + + + public void start( BundleContext context ) throws Exception + { + context.registerService( ManagedServiceFactory.class.getName(), this, getServiceProperties( context ) ); + INSTANCE = this; + } + + + public void stop( BundleContext arg0 ) throws Exception + { + INSTANCE = null; + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceFactoryTestActivator3.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceFactoryTestActivator3.java new file mode 100644 index 00000000000..6c0aab2bf39 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceFactoryTestActivator3.java @@ -0,0 +1,56 @@ +/* + * 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.cm.integration.helper; + + +import java.util.Dictionary; + +import org.osgi.framework.BundleContext; +import org.osgi.service.cm.ManagedServiceFactory; + + +public class ManagedServiceFactoryTestActivator3 extends BaseTestActivator +{ + public static ManagedServiceFactoryTestActivator3 INSTANCE; + + + public void start( BundleContext context ) throws Exception + { + context.registerService( ManagedServiceFactory.class.getName(), this, getServiceProperties( context ) ); + INSTANCE = this; + } + + + public void stop( BundleContext arg0 ) throws Exception + { + INSTANCE = null; + } + + public void updated( String pid, Dictionary props ) + { + // Getting a property is a secure action + String property = System.getProperty("file.separator"); + + if(property != null) { + props.put("foo", property); + } + + super.updated(pid, props); + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceFactoryTestActivator4.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceFactoryTestActivator4.java new file mode 100644 index 00000000000..9df4922ff6b --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceFactoryTestActivator4.java @@ -0,0 +1,72 @@ +/* + * 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.cm.integration.helper; + + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.Dictionary; + +import org.osgi.framework.BundleContext; +import org.osgi.service.cm.ManagedServiceFactory; + + +public class ManagedServiceFactoryTestActivator4 extends BaseTestActivator +{ + public static ManagedServiceFactoryTestActivator4 INSTANCE; + + + public void start( BundleContext context ) throws Exception + { + context.registerService( ManagedServiceFactory.class.getName(), this, getServiceProperties( context ) ); + INSTANCE = this; + } + + + public void stop( BundleContext arg0 ) throws Exception + { + INSTANCE = null; + } + + public void updated( String pid, Dictionary props ) + { + // Opening a socket is a secure action which Config Admin doesn't have permission to do + + int port = 0; + ServerSocket ss = null; + try { + ss = new ServerSocket( 0 ); + port = ss.getLocalPort(); + } catch ( Exception e ) { + e.printStackTrace(); + } finally { + if (ss != null) { + try { + ss.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + props.put( "port", port ); + + super.updated( pid, props ); + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceFactoryThread.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceFactoryThread.java new file mode 100644 index 00000000000..54ab2015665 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceFactoryThread.java @@ -0,0 +1,112 @@ +/* + * 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.cm.integration.helper; + + +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.ManagedServiceFactory; + + +/** + * The ManagedServiceFactoryThread class is a ManagedServiceFactory + * and extends the {@link TestThread} for use in the + * {@link org.apache.felix.cm.integration.ConfigUpdateStressTest}. + */ +public class ManagedServiceFactoryThread extends TestThread implements ManagedServiceFactory +{ + + private final BundleContext bundleContext; + + private final Hashtable serviceProps; + + private ServiceRegistration service; + + private final ArrayList configs; + + private boolean configured; + + + public ManagedServiceFactoryThread( final BundleContext bundleContext, final String pid ) + { + Hashtable serviceProps = new Hashtable(); + serviceProps.put( Constants.SERVICE_PID, pid ); + + this.bundleContext = bundleContext; + this.serviceProps = serviceProps; + this.configs = new ArrayList(); + } + + + public ArrayList getConfigs() + { + synchronized ( configs ) + { + return new ArrayList( configs ); + } + } + + + public boolean isConfigured() + { + return configured; + } + + + @Override + public void doRun() + { + service = bundleContext.registerService( ManagedServiceFactory.class.getName(), this, serviceProps ); + } + + + @Override + public void cleanup() + { + if ( service != null ) + { + service.unregister(); + service = null; + } + } + + + public void deleted( String pid ) + { + synchronized ( configs ) + { + configs.add( null ); + } + } + + + public void updated( String pid, Dictionary properties ) + { + synchronized ( configs ) + { + configs.add( properties ); + configured = properties != null; + } + } +} \ No newline at end of file diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceTestActivator.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceTestActivator.java new file mode 100644 index 00000000000..962a43571aa --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceTestActivator.java @@ -0,0 +1,57 @@ +/* + * 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.cm.integration.helper; + + +import java.util.Dictionary; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.ManagedService; + + +public class ManagedServiceTestActivator extends BaseTestActivator +{ + + public static ManagedServiceTestActivator INSTANCE; + + private Dictionary registrationProps; + private ServiceRegistration registration; + + public void start( BundleContext context ) throws Exception + { + this.registrationProps = getServiceProperties( context ); + this.registration = context.registerService( ManagedService.class.getName(), this, this.registrationProps ); + INSTANCE = this; + } + + + public void stop( BundleContext arg0 ) throws Exception + { + INSTANCE = null; + } + + + public void changePid( final String newPid ) + { + this.registrationProps.put( Constants.SERVICE_PID, toServicePidObject( newPid ) ); + this.registration.setProperties( this.registrationProps ); + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceTestActivator2.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceTestActivator2.java new file mode 100644 index 00000000000..1e95d40e3ae --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceTestActivator2.java @@ -0,0 +1,43 @@ +/* + * 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.cm.integration.helper; + + +import org.osgi.framework.BundleContext; +import org.osgi.service.cm.ManagedService; + + +public class ManagedServiceTestActivator2 extends BaseTestActivator +{ + + public static ManagedServiceTestActivator2 INSTANCE; + + + public void start( BundleContext context ) throws Exception + { + context.registerService( ManagedService.class.getName(), this, getServiceProperties( context ) ); + INSTANCE = this; + } + + + public void stop( BundleContext arg0 ) throws Exception + { + INSTANCE = null; + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceThread.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceThread.java new file mode 100644 index 00000000000..dc143175b9b --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/ManagedServiceThread.java @@ -0,0 +1,103 @@ +/* + * 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.cm.integration.helper; + + +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.ManagedService; + + +/** + * The ManagedServiceThread class is a ManagedService and extends + * the {@link TestThread} for use in the + * {@link org.apache.felix.cm.integration.ConfigUpdateStressTest}. + */ +public class ManagedServiceThread extends TestThread implements ManagedService +{ + + private final BundleContext bundleContext; + + private final Hashtable serviceProps; + + private ServiceRegistration service; + + private final ArrayList configs; + + private boolean configured; + + + public ManagedServiceThread( final BundleContext bundleContext, final String pid ) + { + Hashtable serviceProps = new Hashtable(); + serviceProps.put( Constants.SERVICE_PID, pid ); + + this.bundleContext = bundleContext; + this.serviceProps = serviceProps; + this.configs = new ArrayList(); + } + + + public ArrayList getConfigs() + { + synchronized ( configs ) + { + return new ArrayList( configs ); + } + } + + + public boolean isConfigured() + { + return configured; + } + + + @Override + public void doRun() + { + service = bundleContext.registerService( ManagedService.class.getName(), this, serviceProps ); + } + + + @Override + public void cleanup() + { + if ( service != null ) + { + service.unregister(); + service = null; + } + } + + + public void updated( Dictionary properties ) + { + synchronized ( configs ) + { + configs.add( properties ); + configured = properties != null; + } + } +} \ No newline at end of file diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/MultiManagedServiceFactoryTestActivator.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/MultiManagedServiceFactoryTestActivator.java new file mode 100644 index 00000000000..3b2dd3c951d --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/MultiManagedServiceFactoryTestActivator.java @@ -0,0 +1,46 @@ +/* + * 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.cm.integration.helper; + + +import org.osgi.framework.BundleContext; +import org.osgi.service.cm.ManagedServiceFactory; + + +public class MultiManagedServiceFactoryTestActivator extends ManagedServiceFactoryTestActivator +{ + + public static MultiManagedServiceFactoryTestActivator INSTANCE; + + + public void start( BundleContext context ) throws Exception + { + super.start( context ); + context.registerService( ManagedServiceFactory.class.getName(), this, getServiceProperties( context ) ); + INSTANCE = this; + } + + + @Override + public void stop( BundleContext context ) throws Exception + { + INSTANCE = null; + super.stop( context ); + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/MultiManagedServiceTestActivator.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/MultiManagedServiceTestActivator.java new file mode 100644 index 00000000000..ff11dca7759 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/MultiManagedServiceTestActivator.java @@ -0,0 +1,47 @@ +/* + * 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.cm.integration.helper; + + +import org.osgi.framework.BundleContext; +import org.osgi.service.cm.ManagedService; + + +public class MultiManagedServiceTestActivator extends ManagedServiceTestActivator +{ + + public static MultiManagedServiceTestActivator INSTANCE; + + + @Override + public void start( BundleContext context ) throws Exception + { + super.start( context ); + context.registerService( ManagedService.class.getName(), this, getServiceProperties( context ) ); + INSTANCE = this; + } + + + @Override + public void stop( BundleContext context ) throws Exception + { + INSTANCE = null; + super.stop( context ); + } +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/NestedURLStreamHandler.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/NestedURLStreamHandler.java new file mode 100644 index 00000000000..c421874655f --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/NestedURLStreamHandler.java @@ -0,0 +1,71 @@ +/* + * 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.cm.integration.helper; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; + +import org.osgi.service.url.AbstractURLStreamHandlerService; +import org.osgi.service.url.URLStreamHandlerService; + +public class NestedURLStreamHandler extends AbstractURLStreamHandlerService implements URLStreamHandlerService { + + @Override + public URLConnection openConnection(URL u) throws IOException { + return new NestedURLConnection( u ); + } + + public static class NestedURLConnection extends URLConnection { + + protected NestedURLConnection( URL url ) { + super( url ); + } + + @Override + public void connect() throws IOException { + + } + + @Override + public InputStream getInputStream() throws IOException { + return new FileInputStream( getURL().getFile() ); + } + } + + @Override + public String toExternalForm( final URL u ) { + // This is necessary, because we want to force a permission check + + try { + String property = System.getProperty("file.separator"); + + if(property != null) { + System.out.println( "File Separator is: " + property ); + } + } catch (SecurityException se) { + System.out.println( "Forbidden to check the File Separator." ); + } + + return super.toExternalForm( u ); + } + +} diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/SynchronousTestListener.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/SynchronousTestListener.java new file mode 100644 index 00000000000..baec499a543 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/SynchronousTestListener.java @@ -0,0 +1,27 @@ +/* + * 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.cm.integration.helper; + + +import org.osgi.service.cm.SynchronousConfigurationListener; + + +public class SynchronousTestListener extends TestListener implements SynchronousConfigurationListener +{ +} \ No newline at end of file diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/TestListener.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/TestListener.java new file mode 100644 index 00000000000..663a58445ca --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/TestListener.java @@ -0,0 +1,125 @@ +/* + * 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.cm.integration.helper; + + +import org.osgi.service.cm.ConfigurationEvent; +import org.osgi.service.cm.ConfigurationListener; + +import junit.framework.TestCase; + + +public class TestListener implements ConfigurationListener +{ + + private final Thread mainThread; + + private ConfigurationEvent event; + + private Thread eventThread; + + private int numberOfEvents; + + { + this.mainThread = Thread.currentThread(); + this.numberOfEvents = 0; + } + + + @Override + public void configurationEvent( final ConfigurationEvent event ) + { + this.numberOfEvents++; + + if ( this.event != null ) + { + throw new IllegalStateException( "Untested event to be replaced: " + this.event.getType() + "/" + + this.event.getPid() ); + } + + this.event = event; + this.eventThread = Thread.currentThread(); + } + + + void resetNumberOfEvents() + { + this.numberOfEvents = 0; + } + + + /** + * Asserts an expected event has arrived since the last call to + * {@link #assertEvent(int, String, String, boolean, int)} and + * {@link #assertNoEvent()}. + * + * @param type The expected event type + * @param pid The expected PID of the event + * @param factoryPid The expected factory PID of the event or + * null if no factory PID is expected + * @param expectAsync Whether the event is expected to have been + * provided asynchronously + * @param numberOfEvents The number of events to have arrived in total + */ + public void assertEvent( final int type, final String pid, final String factoryPid, final boolean expectAsync, + final int numberOfEvents ) + { + try + { + TestCase.assertNotNull( "Expecting an event", this.event ); + TestCase.assertEquals( "Expecting event type " + type, type, this.event.getType() ); + TestCase.assertEquals( "Expecting pid " + pid, pid, this.event.getPid() ); + if ( factoryPid == null ) + { + TestCase.assertNull( "Expecting no factoryPid", this.event.getFactoryPid() ); + } + else + { + TestCase.assertEquals( "Expecting factory pid " + factoryPid, factoryPid, this.event.getFactoryPid() ); + } + + TestCase.assertEquals( "Expecting " + numberOfEvents + " events", numberOfEvents, this.numberOfEvents ); + + if ( expectAsync ) + { + TestCase.assertNotSame( "Expecting asynchronous event", this.mainThread, this.eventThread ); + } + else + { + TestCase.assertSame( "Expecting synchronous event", this.mainThread, this.eventThread ); + } + } + finally + { + this.event = null; + this.eventThread = null; + } + } + + + /** + * Fails if an event has been received since the last call to + * {@link #assertEvent(int, String, String, boolean, int)} or + * {@link #assertNoEvent()}. + */ + public void assertNoEvent() + { + TestCase.assertNull( this.event ); + } +} \ No newline at end of file diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/TestThread.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/TestThread.java new file mode 100644 index 00000000000..19f950cfec1 --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/TestThread.java @@ -0,0 +1,74 @@ +/* + * 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.cm.integration.helper; + + +/** + * The TestThread class is a base helper class for the + * {@link org.apache.felix.cm.integration.ConfigUpdateStressTest}. It implements + * basic mechanics to be able to run two task at quasi the same time. + *

      + * It is not important to have exact timings because running the tests multiple + * times and based on low-level Java VM timings thread execution will in the end + * be more or less random. + */ +abstract class TestThread extends Thread +{ + private final Object flag = new Object(); + + private volatile boolean notified; + + + @Override + public void run() + { + synchronized ( flag ) + { + while ( !notified ) + { + try + { + flag.wait( 500L ); + } + catch ( InterruptedException ie ) + { + // ignore + } + } + } + + doRun(); + } + + + protected abstract void doRun(); + + + public abstract void cleanup(); + + + public void trigger() + { + synchronized ( flag ) + { + notified = true; + flag.notifyAll(); + } + } +} \ No newline at end of file diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/UpdateThreadSignalTask.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/UpdateThreadSignalTask.java new file mode 100644 index 00000000000..dd55aede2ea --- /dev/null +++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/UpdateThreadSignalTask.java @@ -0,0 +1,80 @@ +/* + * 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.cm.integration.helper; + + +import junit.framework.TestCase; + + +/** + * The UpdateThreadSignalTask class is a special task used by the + * {@link org.apache.felix.cm.integration.ConfigurationTestBase#delay} method. + *

      + * This task is intended to be added to the update thread schedule and signals + * to the tests that all current tasks on the queue have terminated and tests + * may continue checking results. + */ +public class UpdateThreadSignalTask implements Runnable +{ + + private final Object trigger = new Object(); + + private volatile boolean signal; + + + public void run() + { + synchronized ( trigger ) + { + signal = true; + trigger.notifyAll(); + } + } + + + public void waitSignal() + { + synchronized ( trigger ) + { + if ( !signal ) + { + try + { + trigger.wait( 10 * 1000L ); // seconds + } + catch ( InterruptedException ie ) + { + // sowhat ?? + } + } + + if ( !signal ) + { + TestCase.fail( "Timed out waiting for the queue to keep up" ); + } + } + } + + + @Override + public String toString() + { + return "Update Thread Signal Task"; + } +} diff --git a/configadmin/src/test/resources/all.policy b/configadmin/src/test/resources/all.policy new file mode 100644 index 00000000000..043d1a64acd --- /dev/null +++ b/configadmin/src/test/resources/all.policy @@ -0,0 +1,21 @@ +// +// 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. +// +grant { + permission java.security.AllPermission; +}; \ No newline at end of file diff --git a/configurator/pom.xml b/configurator/pom.xml new file mode 100644 index 00000000000..ec55a41bbdf --- /dev/null +++ b/configurator/pom.xml @@ -0,0 +1,157 @@ + + + + 4.0.0 + + org.apache.felix + felix-parent + 6 + + + + org.apache.felix.configurator + bundle + + Apache Felix Configurator Service + + Implementation of the OSGi Configurator Service Specification 1.0 + + 1.0.9-SNAPSHOT + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/configurator + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/configurator + http://svn.apache.org/viewvc/felix/trunk/configurator + + + + + + org.apache.felix + maven-bundle-plugin + 3.5.0 + true + + + org.apache.felix.configurator.impl.Activator + osgi + + ${project.artifactId} + + + org.osgi.service.coordinator;resolution:=optional, + org.osgi.service.log;resolution:=optional, + * + + + org.osgi.service.configurator + + + org.osgi.service.coordinator;version="[1.0,2)", + org.osgi.service.log;version="[1.3,2)" + + + osgi.extender;osgi.extender="osgi.configurator";version:Version="1.0" + + geronimo-json_1.0_spec,johnzon-core,org.apache.felix.converter + + + + + org.apache.rat + apache-rat-plugin + + + + **/*.json + + + + + + + + org.osgi + osgi.annotation + 6.0.1 + provided + + + org.osgi + org.osgi.core + 6.0.0 + provided + + + org.apache.felix + org.apache.felix.converter + 1.0.0 + provided + + + org.apache.geronimo.specs + geronimo-json_1.0_spec + 1.0-alpha-1 + provided + + + org.apache.johnzon + johnzon-core + 1.0.0 + provided + + + org.osgi + org.osgi.service.cm + 1.6.0 + provided + + + org.osgi + org.osgi.service.configurator + 1.0.0 + provided + + + org.osgi + org.osgi.service.log + 1.3.0 + provided + + + org.osgi + org.osgi.service.coordinator + 1.0.2 + provided + + + junit + junit + 4.12 + test + + + org.mockito + mockito-core + 2.17.0 + test + + + diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/Activator.java b/configurator/src/main/java/org/apache/felix/configurator/impl/Activator.java new file mode 100644 index 00000000000..ceddf134a95 --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/Activator.java @@ -0,0 +1,48 @@ +/* + * 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.configurator.impl; + +import org.apache.felix.configurator.impl.logger.SystemLogger; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +/** + * Bundle activator to start the configurator once + * configuration admin is ready. + */ +public class Activator implements BundleActivator { + + private volatile ServicesListener listener; + + @Override + public final void start(final BundleContext context) + throws Exception { + SystemLogger.init(context); + + listener = new ServicesListener(context); + } + + @Override + public final void stop(BundleContext context) + throws Exception { + if ( listener != null ) { + listener.deactivate(); + listener = null; + } + SystemLogger.destroy(); + } +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/ConfigUtil.java b/configurator/src/main/java/org/apache/felix/configurator/impl/ConfigUtil.java new file mode 100644 index 00000000000..e6fa9a6e395 --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/ConfigUtil.java @@ -0,0 +1,75 @@ +/* + * 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.configurator.impl; + +import java.io.IOException; + +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * Utilities for configuration handling + */ +public abstract class ConfigUtil { + + /** + * Encode the value for the LDAP filter: \, *, (, and ) should be escaped. + */ + private static String encode(final String value) { + return value.replace("\\", "\\\\") + .replace("*", "\\*") + .replace("(", "\\(") + .replace(")", "\\)"); + } + + /** + * Get or create a configuration + * @param ca The configuration admin + * @param pid The pid + * @param createIfNeeded If {@code true} the configuration is created if it doesn't exists + * @return The configuration or {@code null}. + * @throws IOException If anything goes wrong + * @throws InvalidSyntaxException If the filter syntax is invalid (very unlikely) + */ + public static Configuration getOrCreateConfiguration(final ConfigurationAdmin ca, + final String pid, + final boolean createIfNeeded) + throws IOException, InvalidSyntaxException { + final String filter = "(" + Constants.SERVICE_PID + "=" + encode(pid) + ")"; + final Configuration[] configs = ca.listConfigurations(filter); + if (configs != null && configs.length > 0) { + return configs[0]; + } + if ( !createIfNeeded ) { + return null; + } + + final int pos = pid.indexOf('~'); + if ( pos != -1 ) { + final String factoryPid = pid.substring(0, pos); + final String alias = pid.substring(pos + 1); + + return ca.getFactoryConfiguration(factoryPid, alias, "?"); + } else { + return ca.getConfiguration(pid, "?"); + } + } +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/Configurator.java b/configurator/src/main/java/org/apache/felix/configurator/impl/Configurator.java new file mode 100644 index 00000000000..c7bdc5afb97 --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/Configurator.java @@ -0,0 +1,654 @@ +/* + * 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.configurator.impl; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import org.apache.felix.configurator.impl.json.BinUtil; +import org.apache.felix.configurator.impl.json.JSONUtil; +import org.apache.felix.configurator.impl.json.TypeConverter; +import org.apache.felix.configurator.impl.logger.SystemLogger; +import org.apache.felix.configurator.impl.model.BundleState; +import org.apache.felix.configurator.impl.model.Config; +import org.apache.felix.configurator.impl.model.ConfigList; +import org.apache.felix.configurator.impl.model.ConfigPolicy; +import org.apache.felix.configurator.impl.model.ConfigState; +import org.apache.felix.configurator.impl.model.ConfigurationFile; +import org.apache.felix.configurator.impl.model.State; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServicePermission; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.configurator.ConfiguratorConstants; +import org.osgi.util.tracker.BundleTrackerCustomizer; + +/** + * The main class of the configurator. + * + */ +public class Configurator { + + private final BundleContext bundleContext; + + private final State state; + + private final org.osgi.util.tracker.BundleTracker tracker; + + private volatile boolean active = true; + + private volatile Object coordinator; + + private final WorkerQueue queue; + + private final List> configAdminReferences; + + /** + * Create a new configurator and start it + * + * @param bc The bundle context + * @param configAdminReferences Dynamic list of references to the configuration admin service visible to the configurator + */ + public Configurator(final BundleContext bc, final List> configAdminReferences) { + this.queue = new WorkerQueue(); + this.bundleContext = bc; + this.configAdminReferences = configAdminReferences; + State s = null; + try { + s = State.createOrReadState(bundleContext.getDataFile(State.FILE_NAME)); + } catch ( final ClassNotFoundException | IOException e ) { + SystemLogger.error("Unable to read persisted state from " + State.FILE_NAME, e); + s = new State(); + } + this.state = s; + this.tracker = new org.osgi.util.tracker.BundleTracker<>(this.bundleContext, + Bundle.ACTIVE|Bundle.STARTING|Bundle.STOPPING|Bundle.RESOLVED|Bundle.INSTALLED, + + new BundleTrackerCustomizer() { + + @Override + public Bundle addingBundle(final Bundle bundle, final BundleEvent event) { + final int state = bundle.getState(); + if ( active && + (state == Bundle.ACTIVE || state == Bundle.STARTING) ) { + SystemLogger.debug("Adding bundle " + getBundleIdentity(bundle) + " : " + getBundleState(state)); + queue.enqueue(new Runnable() { + + @Override + public void run() { + if ( processAddBundle(bundle) ) { + process(); + } + } + }); + } + return bundle; + } + + @Override + public void modifiedBundle(final Bundle bundle, final BundleEvent event, final Bundle object) { + this.addingBundle(bundle, event); + } + + @Override + public void removedBundle(final Bundle bundle, final BundleEvent event, final Bundle object) { + final int state = bundle.getState(); + if ( active && state == Bundle.UNINSTALLED ) { + SystemLogger.debug("Removing bundle " + getBundleIdentity(bundle) + " : " + getBundleState(state)); + queue.enqueue(new Runnable() { + + @Override + public void run() { + try { + if ( processRemoveBundle(bundle.getBundleId()) ) { + process(); + } + } catch ( final IllegalStateException ise) { + SystemLogger.error("Error processing bundle " + getBundleIdentity(bundle), ise); + } + } + }); + } + } + + }); + } + + public void configAdminAdded() { + queue.enqueue(new Runnable() { + + @Override + public void run() { + process(); + } + }); + } + + private String getBundleIdentity(final Bundle bundle) { + if ( bundle.getSymbolicName() == null ) { + return bundle.getBundleId() + " (" + bundle.getLocation() + ")"; + } else { + return bundle.getSymbolicName() + ":" + bundle.getVersion() + " (" + bundle.getBundleId() + ")"; + } + } + + private String getBundleState(int state) { + switch ( state ) { + case Bundle.ACTIVE : return "active"; + case Bundle.INSTALLED : return "installed"; + case Bundle.RESOLVED : return "resolved"; + case Bundle.STARTING : return "starting"; + case Bundle.STOPPING : return "stopping"; + case Bundle.UNINSTALLED : return "uninstalled"; + } + return String.valueOf(state); + } + + /** + * Shut down the configurator + */ + public void shutdown() { + this.active = false; + this.queue.stop(); + this.tracker.close(); + } + + /** + * Start the configurator. + */ + public void start() { + // get the directory for storing binaries + String dirPath = this.bundleContext.getProperty(ConfiguratorConstants.CONFIGURATOR_BINARIES); + if ( dirPath != null ) { + final File dir = new File(dirPath); + if ( dir.exists() && dir.isDirectory() ) { + BinUtil.binDirectory = dir; + } else if ( dir.exists() ) { + SystemLogger.error("Directory property is pointing at a file not a dir: " + dirPath + ". Using default path."); + } else { + try { + if ( dir.mkdirs() ) { + BinUtil.binDirectory = dir; + } + } catch ( final SecurityException se ) { + // ignore + } + if ( BinUtil.binDirectory == null ) { + SystemLogger.error("Unable to create a directory at: " + dirPath + ". Using default path."); + } + } + } + if ( BinUtil.binDirectory == null ) { + BinUtil.binDirectory = this.bundleContext.getDataFile("binaries" + File.separatorChar + ".check"); + BinUtil.binDirectory = BinUtil.binDirectory.getParentFile(); + BinUtil.binDirectory.mkdirs(); + } + + // before we start the tracker we process all available bundles and initial configuration + final String initial = this.bundleContext.getProperty(ConfiguratorConstants.CONFIGURATOR_INITIAL); + if ( initial == null ) { + this.processRemoveBundle(-1); + } else { + // JSON or URLs ? + final Set hashes = new HashSet<>(); + final Map files = new TreeMap<>(); + + if ( !initial.trim().startsWith("{") ) { + // URLs + final String[] urls = initial.trim().split(","); + for(final String urlString : urls) { + URL url = null; + try { + url = new URL(urlString); + } catch (final MalformedURLException e) { + } + if ( url != null ) { + try { + final String contents = JSONUtil.getResource(urlString, url); + files.put(urlString, contents); + hashes.add(Util.getSHA256(contents.trim())); + } catch ( final IOException ioe ) { + SystemLogger.error("Unable to read " + urlString, ioe); + } + } + } + } else { + // JSON + hashes.add(Util.getSHA256(initial.trim())); + files.put(ConfiguratorConstants.CONFIGURATOR_INITIAL, initial); + } + if ( state.getInitialHashes() == null || !state.getInitialHashes().equals(hashes)) { + if ( state.getInitialHashes() != null ) { + processRemoveBundle(-1); + } + final TypeConverter converter = new TypeConverter(null); + final JSONUtil.Report report = new JSONUtil.Report(); + final List allFiles = new ArrayList<>(); + for(final Map.Entry entry : files.entrySet()) { + final ConfigurationFile file = org.apache.felix.configurator.impl.json.JSONUtil.readJSON(converter, entry.getKey(), null, -1, entry.getValue(), report); + if ( file != null ) { + allFiles.add(file); + } + } + for(final String w : report.warnings) { + SystemLogger.warning(w); + } + for(final String e : report.errors) { + SystemLogger.error(e); + } + final BundleState bState = new BundleState(); + bState.addFiles(allFiles); + for(final String pid : bState.getPids()) { + state.addAll(pid, bState.getConfigurations(pid)); + } + state.setInitialHashes(hashes); + } + + } + + final Bundle[] bundles = this.bundleContext.getBundles(); + final Set ids = new HashSet<>(); + for(final Bundle b : bundles) { + ids.add(b.getBundleId()); + final int state = b.getState(); + if ( state == Bundle.ACTIVE || state == Bundle.STARTING ) { + processAddBundle(b); + } + } + for(final long id : state.getKnownBundleIds()) { + if ( !ids.contains(id) ) { + processRemoveBundle(id); + } + } + this.process(); + this.tracker.open(); + } + + public boolean processAddBundle(final Bundle bundle) { + final long bundleId = bundle.getBundleId(); + final long bundleLastModified = bundle.getLastModified(); + + final Long lastModified = state.getLastModified(bundleId); + if ( lastModified != null && lastModified.longValue() == bundleLastModified ) { + // no changes, nothing to do + return false; + } + + BundleState config = null; + try { + final Set paths = Util.isConfigurerBundle(bundle, this.bundleContext.getBundle().getBundleId()); + if ( paths != null ) { + final JSONUtil.Report report = new JSONUtil.Report(); + config = JSONUtil.readConfigurationsFromBundle(new BinUtil.ResourceProvider() { + + @Override + public String getIdentifier() { + return bundle.toString(); + } + + @Override + public URL getEntry(String path) { + return bundle.getEntry(path); + } + + @Override + public long getBundleId() { + return bundle.getBundleId(); + } + + @Override + public Enumeration findEntries(String path, String filePattern) { + return bundle.findEntries(path, filePattern, false); + } + }, paths, report); + for(final String w : report.warnings) { + SystemLogger.warning(w); + } + for(final String e : report.errors) { + SystemLogger.error(e); + } + } + } catch ( final IllegalStateException ise) { + SystemLogger.error("Error processing bundle " + getBundleIdentity(bundle), ise); + } + if ( lastModified != null ) { + processRemoveBundle(bundleId); + } + if ( config != null ) { + for(final String pid : config.getPids()) { + state.addAll(pid, config.getConfigurations(pid)); + } + state.setLastModified(bundleId, bundleLastModified); + return true; + } + return lastModified != null; + } + + public boolean processRemoveBundle(final long bundleId) { + if ( state.getLastModified(bundleId) != null ) { + state.removeLastModified(bundleId); + for(final String pid : state.getPids()) { + final ConfigList configList = state.getConfigurations(pid); + configList.uninstall(bundleId); + } + return true; + } + return false; + } + + /** + * Set or unset the coordinator service + * @param coordinator The coordinator service or {@code null} + */ + public void setCoordinator(final Object coordinator) { + this.coordinator = coordinator; + } + + /** + * Process the state to activate/deactivate configurations + */ + public void process() { + final Object localCoordinator = this.coordinator; + Object coordination = null; + if ( localCoordinator != null ) { + coordination = CoordinatorUtil.getCoordination(localCoordinator); + } + + boolean retry = false; + try { + for(final String pid : state.getPids()) { + final ConfigList configList = state.getConfigurations(pid); + + if ( configList.hasChanges() ) { + if ( process(configList) ) { + try { + State.writeState(this.bundleContext.getDataFile(State.FILE_NAME), state); + } catch ( final IOException ioe) { + SystemLogger.error("Unable to persist state to " + State.FILE_NAME, ioe); + } + } else { + retry = true; + } + } + } + + } finally { + if ( coordination != null ) { + CoordinatorUtil.endCoordination(coordination); + } + } + if ( !retry ) { + // check whether there is a stale config admin bundle id + boolean changed = false; + for(final Long bundleId : this.state.getBundleIdsUsingConfigAdmin()) { + if ( this.state.getLastModified(bundleId) == null ) { + this.state.removeConfigAdminBundleId(bundleId); + changed = true; + } + } + if ( changed ) { + try { + State.writeState(this.bundleContext.getDataFile(State.FILE_NAME), state); + } catch ( final IOException ioe) { + SystemLogger.error("Unable to persist state to " + State.FILE_NAME, ioe); + } + } + } + } + + /** + * Process changes to a pid. + * @param configList The config list + * @return {@code true} if the change has been processed, {@code false} if a retry is required + */ + public boolean process(final ConfigList configList) { + Config toActivate = null; + Config toDeactivate = null; + + for(final Config cfg : configList) { + switch ( cfg.getState() ) { + case INSTALL : // activate if first found + if ( toActivate == null ) { + toActivate = cfg; + } + break; + + case IGNORED : // same as installed + case INSTALLED : // check if we have to uninstall + if ( toActivate == null ) { + toActivate = cfg; + } else { + cfg.setState(ConfigState.INSTALL); + } + break; + + case UNINSTALL : // deactivate if first found (we should only find one anyway) + if ( toDeactivate == null ) { + toDeactivate = cfg; + } + break; + + case UNINSTALLED : // nothing to do + break; + } + + } + // if there is a configuration to activate, we can directly activate it + // without deactivating (reducing the changes of the configuration from two + // to one) + boolean noRetryNeeded = true; + if ( toActivate != null && toActivate.getState() == ConfigState.INSTALL ) { + noRetryNeeded = activate(configList, toActivate); + } + if ( toActivate == null && toDeactivate != null ) { + noRetryNeeded = deactivate(configList, toDeactivate); + } + + if ( noRetryNeeded ) { + // remove all uninstall(ed) configurations + final Iterator iter = configList.iterator(); + boolean foundInstalled = false; + while ( iter.hasNext() ) { + final Config cfg = iter.next(); + if ( cfg.getState() == ConfigState.UNINSTALL || cfg.getState() == ConfigState.UNINSTALLED ) { + if ( cfg.getFiles() != null ) { + for(final File f : cfg.getFiles()) { + f.delete(); + } + } + iter.remove(); + } else if ( cfg.getState() == ConfigState.INSTALLED ) { + if ( foundInstalled ) { + cfg.setState(ConfigState.INSTALL); + } else { + foundInstalled = true; + } + } + } + + // mark as processed + configList.setHasChanges(false); + } + return noRetryNeeded; + } + + private ConfigurationAdmin getConfigurationAdmin(final long configAdminServiceBundleId) { + ServiceReference ref = null; + synchronized ( this.configAdminReferences ) { + for(final ServiceReference r : this.configAdminReferences ) { + final Bundle bundle = r.getBundle(); + if ( bundle != null && bundle.getBundleId() == configAdminServiceBundleId) { + ref = r; + break; + } + } + } + if ( ref != null ) { + return this.bundleContext.getService(ref); + } + return null; + } + + /** + * Try to activate a configuration + * Check policy and change count + * @param configList The configuration list + * @param cfg The configuration to activate + * @return {@code true} if activation was successful + */ + public boolean activate(final ConfigList configList, final Config cfg) { + // check for configuration admin + Long configAdminServiceBundleId = this.state.getConfigAdminBundleId(cfg.getBundleId()); + if ( configAdminServiceBundleId == null ) { + final Bundle configBundle = cfg.getBundleId() == -1 ? this.bundleContext.getBundle() : this.bundleContext.getBundle(Constants.SYSTEM_BUNDLE_LOCATION).getBundleContext().getBundle(cfg.getBundleId()); + // we check the state again, just to be sure (to avoid race conditions) + if ( configBundle != null + && (configBundle.getState() == Bundle.STARTING || configBundle.getState() == Bundle.ACTIVE)) { + if ( System.getSecurityManager() == null + || configBundle.hasPermission( new ServicePermission(ConfigurationAdmin.class.getName(), ServicePermission.GET)) ) { + try { + final BundleContext ctx = configBundle.getBundleContext(); + if ( ctx != null ) { + final Collection> refs = ctx.getServiceReferences(ConfigurationAdmin.class, null); + final List> sortedRefs = new ArrayList<>(refs); + Collections.sort(sortedRefs); + for(int i=sortedRefs.size();i>0;i--) { + final ServiceReference r = sortedRefs.get(i-1); + synchronized ( this.configAdminReferences ) { + if ( this.configAdminReferences.contains(r) ) { + configAdminServiceBundleId = r.getBundle().getBundleId(); + break; + } + } + } + } + } catch ( final IllegalStateException e) { + // this might happen if the config admin bundle gets deactivated while we use it + // we can ignore this and retry later on + } catch (final InvalidSyntaxException e) { + // this can never happen as we pass {@code null} as the filter + } + } + } + } + if ( configAdminServiceBundleId == null ) { + // no configuration admin found, we have to retry + return false; + } + final ConfigurationAdmin configAdmin = this.getConfigurationAdmin(configAdminServiceBundleId); + if ( configAdmin == null ) { + // getting configuration admin failed, we have to retry + return false; + } + this.state.setConfigAdminBundleId(cfg.getBundleId(), configAdminServiceBundleId); + + boolean ignore = false; + try { + // get existing configuration - if any + boolean update = false; + Configuration configuration = ConfigUtil.getOrCreateConfiguration(configAdmin, cfg.getPid(), false); + if ( configuration == null ) { + // new configuration + configuration = ConfigUtil.getOrCreateConfiguration(configAdmin, cfg.getPid(), true); + update = true; + } else { + if ( cfg.getPolicy() == ConfigPolicy.FORCE ) { + update = true; + } else { + if ( configList.getLastInstalled() == null + || configList.getChangeCount() != configuration.getChangeCount() ) { + ignore = true; + } else { + update = true; + } + } + } + + if ( update ) { + configuration.updateIfDifferent(cfg.getProperties()); + cfg.setState(ConfigState.INSTALLED); + configList.setChangeCount(configuration.getChangeCount()); + configList.setLastInstalled(cfg); + } + } catch (final InvalidSyntaxException | IOException e) { + SystemLogger.error("Unable to update configuration " + cfg.getPid() + " : " + e.getMessage(), e); + ignore = true; + } + if ( ignore ) { + cfg.setState(ConfigState.IGNORED); + configList.setChangeCount(-1); + configList.setLastInstalled(null); + } + + return true; + } + + /** + * Try to deactivate a configuration + * Check policy and change count + * @param cfg The configuration + */ + public boolean deactivate(final ConfigList configList, final Config cfg) { + final Long configAdminServiceBundleId = this.state.getConfigAdminBundleId(cfg.getBundleId()); + // check if configuration admin bundle is still available + // if not or if we didn't record anything, we consider the configuration uninstalled + final Bundle configBundle = configAdminServiceBundleId == null ? null : this.bundleContext.getBundle(Constants.SYSTEM_BUNDLE_LOCATION).getBundleContext().getBundle(configAdminServiceBundleId); + if ( configBundle != null ) { + final ConfigurationAdmin configAdmin = this.getConfigurationAdmin(configAdminServiceBundleId); + if ( configAdmin == null ) { + // getting configuration admin failed, we have to retry + return false; + } + + try { + final Configuration c = ConfigUtil.getOrCreateConfiguration(configAdmin, cfg.getPid(), false); + if ( c != null ) { + if ( cfg.getPolicy() == ConfigPolicy.FORCE + || configList.getChangeCount() == c.getChangeCount() ) { + c.delete(); + } + } + } catch (final InvalidSyntaxException | IOException e) { + SystemLogger.error("Unable to remove configuration " + cfg.getPid() + " : " + e.getMessage(), e); + } + } + cfg.setState(ConfigState.UNINSTALLED); + configList.setChangeCount(-1); + configList.setLastInstalled(null); + + return true; + } +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/CoordinatorUtil.java b/configurator/src/main/java/org/apache/felix/configurator/impl/CoordinatorUtil.java new file mode 100644 index 00000000000..bd9218afadb --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/CoordinatorUtil.java @@ -0,0 +1,51 @@ +/* + * 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.configurator.impl; + +import org.apache.felix.configurator.impl.logger.SystemLogger; +import org.osgi.service.coordinator.Coordination; +import org.osgi.service.coordinator.Coordinator; + +/** + * Utility class for coordinations + */ +public class CoordinatorUtil { + + public static Object getCoordination(final Object object) { + final Coordinator coordinator = (Coordinator) object; + final Coordination threadCoordination = coordinator.peek(); + if ( threadCoordination == null ) { + try { + return coordinator.create("org.apache.felix.configurator", 0L); + } catch (final Exception e) { + SystemLogger.error("Unable to create new coordination with coordinator " + coordinator, e); + } + } + return null; + } + + public static void endCoordination(final Object object) { + final Coordination coordination = (Coordination) object; + try { + coordination.end(); + } catch (final Exception e) { + SystemLogger.error("Error ending coordination " + coordination, e); + } + } +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/ServicesListener.java b/configurator/src/main/java/org/apache/felix/configurator/impl/ServicesListener.java new file mode 100644 index 00000000000..1b82f4fa920 --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/ServicesListener.java @@ -0,0 +1,235 @@ +/* + * 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.configurator.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.felix.configurator.impl.logger.SystemLogger; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +/** + * The {@code ServicesListener} listens for the required services + * and starts the configurator when all services are available. + * It also handles optional services + */ +public class ServicesListener { + + /** The bundle context. */ + private final BundleContext bundleContext; + + /** The service tracker for configuration admin */ + private final ServiceTracker> caTracker; + + /** The listener for the coordinator. */ + private final Listener coordinatorListener; + + /** The current configurator. */ + private volatile Configurator configurator; + + private final List> configAdminReferences; + + /** + * Start listeners + */ + public ServicesListener(final BundleContext bundleContext) { + this.bundleContext = bundleContext; + this.configAdminReferences = new ArrayList<>(); + this.caTracker = new ServiceTracker<>(bundleContext, ConfigurationAdmin.class, + + new ServiceTrackerCustomizer>() { + + @Override + public ServiceReference addingService(final ServiceReference reference) { + synchronized ( configAdminReferences ) { + configAdminReferences.add(reference); + Collections.sort(configAdminReferences); + } + notifyChange(); + return reference; + } + + @Override + public void modifiedService(final ServiceReference reference, + final ServiceReference service) { + // nothing to do + } + + @Override + public void removedService(final ServiceReference reference, + final ServiceReference service) { + synchronized ( configAdminReferences ) { + configAdminReferences.remove(reference); + } + notifyChange(); + } + }); + this.coordinatorListener = new Listener("org.osgi.service.coordinator.Coordinator"); + this.caTracker.open(); + this.coordinatorListener.start(); + SystemLogger.debug("Started services listener for configurator."); + } + + /** + * Notify of service changes from the listeners. + * If all services are available, start + */ + public void notifyChange() { + synchronized ( configAdminReferences ) { + // check if there is at least a single configuration admin + final boolean hasConfigAdmin = !this.configAdminReferences.isEmpty(); + final Object coordinator = this.coordinatorListener.getService(); + SystemLogger.debug("Services updated for configurator: " + configAdminReferences + " - " + coordinator); + + if ( hasConfigAdmin ) { + boolean isNew = configurator == null; + if ( isNew ) { + SystemLogger.debug("Starting new configurator"); + configurator = new Configurator(this.bundleContext, this.configAdminReferences); + } + configurator.setCoordinator(coordinator); + if ( isNew ) { + configurator.start(); + } else { + configurator.configAdminAdded(); + } + } else { + if ( configurator != null ) { + SystemLogger.debug("Stopping configurator"); + configurator.shutdown(); + configurator = null; + } + } + } + } + + /** + * Deactivate this listener. + */ + public void deactivate() { + this.caTracker.close(); + this.coordinatorListener.deactivate(); + if ( configurator != null ) { + configurator.shutdown(); + configurator = null; + } + } + + /** + * Helper class listening for service events for a defined service. + */ + protected final class Listener implements ServiceListener { + + /** The name of the service. */ + private final String serviceName; + + /** The service reference. */ + private volatile ServiceReference reference; + + /** The service. */ + private volatile Object service; + + /** + * Constructor + */ + public Listener(final String serviceName) { + this.serviceName = serviceName; + } + + /** + * Start the listener. + * First register a service listener and then check for the service. + */ + public void start() { + try { + bundleContext.addServiceListener(this, "(" + + Constants.OBJECTCLASS + "=" + serviceName + ")"); + } catch (final InvalidSyntaxException ise) { + // this should really never happen + throw new RuntimeException("Unexpected exception occured.", ise); + } + this.retainService(); + } + + /** + * Unregister the listener. + */ + public void deactivate() { + bundleContext.removeServiceListener(this); + } + + /** + * Return the service (if available) + */ + public synchronized Object getService() { + return this.service; + } + + /** + * Try to get the service and notify the change. + */ + private synchronized void retainService() { + if ( this.reference == null ) { + this.reference = bundleContext.getServiceReference(this.serviceName); + if ( this.reference != null ) { + this.service = bundleContext.getService(this.reference); + if ( this.service == null ) { + this.reference = null; + } else { + notifyChange(); + } + } + } + } + + /** + * Try to release the service and notify the change. + */ + private synchronized void releaseService(final ServiceReference ref) { + if ( this.reference != null && this.reference.compareTo(ref) == 0 ) { + this.service = null; + bundleContext.ungetService(this.reference); + this.reference = null; + notifyChange(); + } + } + + /** + * @see org.osgi.framework.ServiceListener#serviceChanged(org.osgi.framework.ServiceEvent) + */ + @Override + public void serviceChanged(ServiceEvent event) { + if (event.getType() == ServiceEvent.REGISTERED) { + this.retainService(); + } else if ( event.getType() == ServiceEvent.UNREGISTERING ) { + this.releaseService(event.getServiceReference()); + this.retainService(); + } + } + } +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/Util.java b/configurator/src/main/java/org/apache/felix/configurator/impl/Util.java new file mode 100644 index 00000000000..ba2a585601e --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/Util.java @@ -0,0 +1,104 @@ +/* + * 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.configurator.impl; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.felix.configurator.impl.logger.SystemLogger; +import org.osgi.framework.Bundle; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; + +public class Util { + + public static final String NS_OSGI_EXTENDER = "osgi.extender"; + + private static final String PROP_CONFIGURATIONS = "configurations"; + + private static final String DEFAULT_PATH = "OSGI-INF/configurator"; + + /** + * Check if the bundle contains configurations for the configurator + * @param bundle The bundle + * @param configuratorBundleId The bundle id of the configurator bundle to check the wiring + * @return Set of locations or {@code null} + */ + @SuppressWarnings("unchecked") + public static Set isConfigurerBundle(final Bundle bundle, final long configuratorBundleId) { + // check for bundle wiring + final BundleWiring bundleWiring = bundle.adapt(BundleWiring.class); + if ( bundleWiring == null ) { + return null; + } + + // check for bundle requirement to implementation namespace + final List requirements = bundleWiring.getRequirements(NS_OSGI_EXTENDER); + if ( requirements == null || requirements.isEmpty() ) { + return null; + } + // get all wires for the implementation namespace + final List wires = bundleWiring.getRequiredWires(NS_OSGI_EXTENDER); + for(final BundleWire wire : wires) { + // if the wire is to this bundle (configurator), it must be the correct + // requirement (no need to do additional checks like version etc.) + if ( wire.getProviderWiring() != null + && wire.getProviderWiring().getBundle().getBundleId() == configuratorBundleId ) { + final Object val = wire.getRequirement().getAttributes().get(PROP_CONFIGURATIONS); + if ( val != null ) { + if ( val instanceof String ) { + return Collections.singleton((String)val); + } + if ( val instanceof List ) { + final List paths = (List)val; + final Set result = new HashSet<>(); + for(final String p : paths) { + result.add(p); + } + return result; + } + SystemLogger.error("Attribute " + PROP_CONFIGURATIONS + " for configurator requirement has an invalid type: " + val + + ". Using default configuration."); + } + return Collections.singleton(DEFAULT_PATH); + } + } + + return null; + } + + public static String getSHA256(final String value) { + try { + final StringBuilder builder = new StringBuilder(); + final MessageDigest md = MessageDigest.getInstance("SHA-256"); + for (final byte b : md.digest(value.getBytes("UTF-8")) ) { + builder.append(String.format("%02x", b)); + } + return builder.toString(); + } catch ( final NoSuchAlgorithmException | UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/WorkerQueue.java b/configurator/src/main/java/org/apache/felix/configurator/impl/WorkerQueue.java new file mode 100644 index 00000000000..fb1117bdb5f --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/WorkerQueue.java @@ -0,0 +1,84 @@ +/* + * 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.configurator.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +import org.apache.felix.configurator.impl.logger.SystemLogger; + +public class WorkerQueue implements Runnable { + + private final ThreadFactory threadFactory; + + private final List tasks = new ArrayList<>(); + + private volatile Thread backgroundThread; + + private volatile boolean stopped = false; + + public WorkerQueue() { + this.threadFactory = Executors.defaultThreadFactory(); + } + + public void stop() { + synchronized ( this.tasks ) { + this.stopped = true; + } + } + + public void enqueue(final Runnable r) { + synchronized ( this.tasks ) { + if ( !this.stopped ) { + this.tasks.add(r); + if ( this.backgroundThread == null ) { + this.backgroundThread = this.threadFactory.newThread(this); + this.backgroundThread.setDaemon(true); + this.backgroundThread.setName("Apache Felix Configurator Worker Thread"); + this.backgroundThread.start(); + } + } + } + } + + @Override + public void run() { + Runnable r; + do { + r = null; + synchronized ( this.tasks ) { + if ( !this.stopped && !this.tasks.isEmpty() ) { + r = this.tasks.remove(0); + } else { + this.backgroundThread = null; + } + } + if ( r != null ) { + try { + r.run(); + } catch ( final Throwable t) { + // just to be sure our loop never dies + SystemLogger.error("Error processing task" + t.getMessage(), t); + } + } + } while ( r != null ); + } +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/json/BinUtil.java b/configurator/src/main/java/org/apache/felix/configurator/impl/json/BinUtil.java new file mode 100644 index 00000000000..a9c590328e4 --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/json/BinUtil.java @@ -0,0 +1,71 @@ +/* + * 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.configurator.impl.json; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.util.Enumeration; +import java.util.UUID; + +public class BinUtil { + + public static volatile File binDirectory; + + public interface ResourceProvider { + + long getBundleId(); + + URL getEntry(String path); + + String getIdentifier(); + + Enumeration findEntries(String path, String filePattern); + } + + public static File extractFile(final ResourceProvider provider, final String pid, final String path) + throws IOException { + final URL url = provider.getEntry(path); + if ( url == null ) { + return null; + } + final URLConnection connection = url.openConnection(); + + final File dir = new File(binDirectory, URLEncoder.encode(pid, "UTF-8")); + dir.mkdir(); + final File newFile = new File(dir, UUID.randomUUID().toString()); + + try(final BufferedInputStream in = new BufferedInputStream(connection.getInputStream()); + final FileOutputStream fos = new FileOutputStream(newFile)) { + + int len = 0; + final byte[] buffer = new byte[16384]; + + while ( (len = in.read(buffer)) > 0 ) { + fos.write(buffer, 0, len); + } + } + + return newFile; + } +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/json/JSMin.java b/configurator/src/main/java/org/apache/felix/configurator/impl/json/JSMin.java new file mode 100644 index 00000000000..606a7718aa4 --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/json/JSMin.java @@ -0,0 +1,290 @@ +/* + * 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. + * + *

      + * Copyright (c) 2006 John Reilly (www.inconspicuous.org) This work is a + * translation from C to Java of jsmin.c published by Douglas Crockford. + * Permission is hereby granted to use the Java version under the same + * conditions as the jsmin.c on which it is based. + *

      + * http://www.crockford.com/javascript/jsmin.html + */ +package org.apache.felix.configurator.impl.json; + +import java.io.IOException; +import java.io.PushbackReader; +import java.io.Reader; +import java.io.Writer; + +public class JSMin { + + private static final int EOF = -1; + + private final PushbackReader in; + + private final Writer out; + + private int theA; + + private int theB; + + private int theLookahead = EOF; + + private int theX = EOF; + + private int theY = EOF; + + public JSMin(final Reader in, final Writer out) { + this.in = new PushbackReader(in); + this.out = out; + } + + /** + * isAlphanum -- return true if the character is a letter, digit, underscore, + * dollar sign, or non-ASCII character. + */ + private boolean isAlphanum(final int c) { + return ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') + || (c >= 'A' && c <= 'Z') || c == '_' || c == '$' || c == '\\' || c > 126); + } + + /** + * get -- return the next character from stdin. Watch out for lookahead. If + * the character is a control character, translate it to a space or + * linefeed. + */ + private int get() throws IOException { + int c = theLookahead; + theLookahead = EOF; + if ( c == EOF ) { + c = in.read(); + } + + if (c >= ' ' || c == '\n' || c == EOF) { + return c; + } + + if (c == '\r') { + return '\n'; + } + + return ' '; + } + + /** + * peek -- get the next character without getting it. + */ + private int peek() throws IOException { + theLookahead = get(); + return theLookahead; + } + + /** + * next -- get the next character, excluding comments. peek() is used to see + * if a '/' is followed by a '/' or '*'. + */ + private int next() throws IOException { + int c = get(); + if (c == '/') { + switch (peek()) { + case '/': + for (;;) { + c = get(); + if (c <= '\n') { + break; + } + } + break; + case '*': + get(); + while (c != ' ') { + switch (get()) { + case '*': + if (peek() == '/') { + get(); + c = ' '; + } + break; + case EOF: + throw new IOException("Unterminated comment."); + } + } + break; + } + + } + theY = theX; + theX = c; + return c; + } + + /** + * action -- do something! What you do is determined by the argument: + *

        + *
      • 1 Output A. Copy B to A. Get the next B.
      • + *
      • 2 Copy B to A. Get the next B. (Delete A).
      • + *
      • 3 Get the next B. (Delete B).
      • + *
      + * action treats a string as a single character. Wow!
      + * action recognizes a regular expression if it is preceded by ( or , or =. + */ + void action(final int d) throws IOException { + switch (d) { + case 1: + out.write(theA); + if ((theY == '\n' || theY == ' ') && + (theA == '+' || theA == '-' || theA == '*' || theA == '/') && + (theB == '+' || theB == '-' || theB == '*' || theB == '/')) { + out.write(theY); + } + case 2: + theA = theB; + + if (theA == '\'' || theA == '"' || theA == '`') { + for (;;) { + out.write(theA); + theA = get(); + if (theA == theB) { + break; + } + if (theA == '\\') { + out.write(theA); + theA = get(); + } + if ( theA == EOF) { + throw new IOException("Unterminated string literal."); + } + } + } + + case 3: + theB = next(); + if (theB == '/' + && (theA == '(' || theA == ',' || theA == '=' || theA == ':' + || theA == '[' || theA == '!' || theA == '&' || theA == '|' + || theA == '?' || theA == '+' || theA == '-' || theA == '~' + || theA == '*' || theA == '/' || theA == '{' || theA == '\n')) { + out.write(theA); + if (theA == '/' || theA == '*') { + out.write(' '); + } + out.write(theB); + for (;;) { + theA = get(); + if (theA == '[') { + for (;;) { + out.write(theA); + theA = get(); + if (theA == ']') { + break; + } + if (theA == '\\') { + out.write(theA); + theA = get(); + } + if (theA == EOF) { + throw new IOException("Unterminated set in Regular Expression literal."); + } + } + } else if (theA == '/') { + switch (peek()) { + case '/': + case '*': + throw new IOException("Unterminated set in Regular Expression literal."); + } + break; + } else if (theA == '\\') { + out.write(theA); + theA = get(); + } else if (theA == EOF) { + throw new IOException("Unterminated Regular Expression literal."); + } + out.write(theA); + } + theB = next(); + } + } + } + + /** + * jsmin -- Copy the input to the output, deleting the characters which are + * insignificant to JavaScript. Comments will be removed. Tabs will be + * replaced with spaces. Carriage returns will be replaced with linefeeds. + * Most spaces and linefeeds will be removed. + */ + public void jsmin() throws IOException { + if (peek() == 0xEF) { + get(); + get(); + get(); + } + theA = '\n'; + action(3); + while (theA != EOF) { + switch (theA) { + case ' ': + action(isAlphanum(theB) ? 1: 2); + break; + case '\n': + switch (theB) { + case '{': + case '[': + case '(': + case '+': + case '-': + case '!': + case '~': + action(1); + break; + case ' ': + action(3); + break; + default: + action(isAlphanum(theB) ? 1: 2); + } + break; + default: + switch (theB) { + case ' ': + action(isAlphanum(theB) ? 1: 3); + break; + case '\n': + switch (theA) { + case '}': + case ']': + case ')': + case '+': + case '-': + case '"': + case '\'': + case '`': + action(1); + break; + default: + action(isAlphanum(theB) ? 1: 3); + } + break; + default: + action(1); + break; + } + } + } + out.flush(); + } +} \ No newline at end of file diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/json/JSONUtil.java b/configurator/src/main/java/org/apache/felix/configurator/impl/json/JSONUtil.java new file mode 100644 index 00000000000..a2f63e87ba6 --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/json/JSONUtil.java @@ -0,0 +1,496 @@ +/* + * 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.configurator.impl.json; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.json.JsonArray; +import javax.json.JsonArrayBuilder; +import javax.json.JsonNumber; +import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; +import javax.json.JsonReader; +import javax.json.JsonString; +import javax.json.JsonStructure; +import javax.json.JsonValue; +import javax.json.JsonValue.ValueType; + +import org.apache.felix.configurator.impl.model.BundleState; +import org.apache.felix.configurator.impl.model.Config; +import org.apache.felix.configurator.impl.model.ConfigPolicy; +import org.apache.felix.configurator.impl.model.ConfigurationFile; +import org.apache.johnzon.core.JsonProviderImpl; +import org.osgi.service.configurator.ConfiguratorConstants; + +public class JSONUtil { + + private static final String INTERNAL_PREFIX = ":configurator:"; + + private static final String PROP_RANKING = "ranking"; + + private static final String PROP_POLICY = "policy"; + + public static final class Report { + + public final List warnings = new ArrayList<>(); + + public final List errors = new ArrayList<>(); + } + + /** + * Read all configurations from a bundle + * @param provider The bundle provider + * @param paths The paths to read from + * @param report The report for errors and warnings + * @return The bundle state. + */ + public static BundleState readConfigurationsFromBundle(final BinUtil.ResourceProvider provider, + final Set paths, + final Report report) { + final BundleState config = new BundleState(); + + final List allFiles = new ArrayList<>(); + for(final String path : paths) { + final List files = readJSON(provider, path, report); + allFiles.addAll(files); + } + Collections.sort(allFiles); + + config.addFiles(allFiles); + + return config; + } + + /** + * Read all json files from a given path in the bundle + * + * @param provider The bundle provider + * @param path The path + * @param report The report for errors and warnings + * @return A list of configuration files - sorted by url, might be empty. + */ + public static List readJSON(final BinUtil.ResourceProvider provider, + final String path, + final Report report) { + final List result = new ArrayList<>(); + final Enumeration urls = provider.findEntries(path, "*.json"); + if ( urls != null ) { + while ( urls.hasMoreElements() ) { + final URL url = urls.nextElement(); + + final String filePath = url.getPath(); + final int pos = filePath.lastIndexOf('/'); + final String name = path + filePath.substring(pos); + + try { + final String contents = getResource(name, url); + boolean done = false; + final TypeConverter converter = new TypeConverter(provider); + try { + final ConfigurationFile file = readJSON(converter, name, url, provider.getBundleId(), contents, report); + if ( file != null ) { + result.add(file); + done = true; + } + } finally { + if ( !done ) { + converter.cleanupFiles(); + } + } + } catch ( final IOException ioe ) { + report.errors.add("Unable to read " + name + " : " + ioe.getMessage()); + } + } + Collections.sort(result); + } else { + report.errors.add("No configurations found at path " + path); + } + return result; + } + + /** + * Read a single JSON file + * @param converter type converter + * @param name The name of the file + * @param url The url to that file or {@code null} + * @param bundleId The bundle id of the bundle containing the file + * @param contents The contents of the file + * @param report The report for errors and warnings + * @return The configuration file or {@code null}. + */ + public static ConfigurationFile readJSON( + final TypeConverter converter, + final String name, + final URL url, + final long bundleId, + final String contents, + final Report report) { + final String identifier = (url == null ? name : url.toString()); + final JsonObject json = parseJSON(name, contents, report); + final Map configs = verifyJSON(name, json, url != null, report); + if ( configs != null ) { + final List list = readConfigurationsJSON(converter, bundleId, identifier, configs, report); + if ( !list.isEmpty() ) { + final ConfigurationFile file = new ConfigurationFile(url, list); + + return file; + } + } + return null; + } + + /** + * Read the configurations JSON + * @param converter The converter to use + * @param bundleId The bundle id + * @param identifier The identifier + * @param configs The map containing the configurations + * @param report The report for errors and warnings + * @return The list of {@code Config}s or {@code null} + */ + public static List readConfigurationsJSON(final TypeConverter converter, + final long bundleId, + final String identifier, + final Map configs, + final Report report) { + final List configurations = new ArrayList<>(); + for(final Map.Entry entry : configs.entrySet()) { + if ( ! (entry.getValue() instanceof Map) ) { + if ( !entry.getKey().startsWith(INTERNAL_PREFIX) ) { + report.errors.add("Ignoring configuration in '" + identifier + "' (not a configuration) : " + entry.getKey()); + } + } else { + @SuppressWarnings("unchecked") + final Map mainMap = (Map)entry.getValue(); + final String pid = entry.getKey(); + + int ranking = 0; + ConfigPolicy policy = ConfigPolicy.DEFAULT; + + final Dictionary properties = new OrderedDictionary(); + boolean valid = true; + for(final String mapKey : mainMap.keySet()) { + final Object value = mainMap.get(mapKey); + + final boolean internalKey = mapKey.startsWith(INTERNAL_PREFIX); + String key = mapKey; + if ( internalKey ) { + key = key.substring(INTERNAL_PREFIX.length()); + } + final int pos = key.indexOf(':'); + String typeInfo = null; + if ( pos != -1 ) { + typeInfo = key.substring(pos + 1); + key = key.substring(0, pos); + } + + if ( internalKey ) { + // no need to do type conversion based on typeInfo for internal props, type conversion is done directly below + if ( key.equals(PROP_RANKING) ) { + final Integer intObj = TypeConverter.getConverter().convert(value).defaultValue(null).to(Integer.class); + if ( intObj == null ) { + report.warnings.add("Invalid ranking for configuration in '" + identifier + "' : " + pid + " - " + value); + } else { + ranking = intObj.intValue(); + } + } else if ( key.equals(PROP_POLICY) ) { + final String stringVal = TypeConverter.getConverter().convert(value).defaultValue(null).to(String.class); + if ( stringVal == null ) { + report.errors.add("Invalid policy for configuration in '" + identifier + "' : " + pid + " - " + value); + } else { + if ( value.equals("default") || value.equals("force") ) { + policy = ConfigPolicy.valueOf(stringVal.toUpperCase()); + } else { + report.errors.add("Invalid policy for configuration in '" + identifier + "' : " + pid + " - " + value); + } + } + } + } else { + try { + + final Object convertedVal = getTypedValue(converter, pid, value, typeInfo); + properties.put(key, convertedVal); + } catch ( final IOException io ) { + report.errors.add("Invalid value/type for configuration in '" + identifier + "' : " + pid + " - " + mapKey + " : " + io.getMessage()); + valid = false; + break; + } + } + } + + if ( valid ) { + final Config c = new Config(pid, properties, bundleId, ranking, policy); + c.setFiles(converter.flushFiles()); + configurations.add(c); + } + } + } + return configurations; + } + + public static JsonStructure build(final Object value) { + if ( value instanceof List ) { + @SuppressWarnings("unchecked") + final List list = (List)value; + final JsonArrayBuilder builder = new JsonProviderImpl().createArrayBuilder(); + for(final Object obj : list) { + if ( obj instanceof String ) { + builder.add(obj.toString()); + } else if ( obj instanceof Long ) { + builder.add((Long)obj); + } else if ( obj instanceof Double ) { + builder.add((Double)obj); + } else if (obj instanceof Boolean ) { + builder.add((Boolean)obj); + } else if ( obj instanceof Map ) { + builder.add(build(obj)); + } else if ( obj instanceof List ) { + builder.add(build(obj)); + } + + } + return builder.build(); + } else if ( value instanceof Map ) { + @SuppressWarnings("unchecked") + final Map map = (Map)value; + final JsonObjectBuilder builder = new JsonProviderImpl().createObjectBuilder(); + for(final Map.Entry entry : map.entrySet()) { + if ( entry.getValue() instanceof String ) { + builder.add(entry.getKey(), entry.getValue().toString()); + } else if ( entry.getValue() instanceof Long ) { + builder.add(entry.getKey(), (Long)entry.getValue()); + } else if ( entry.getValue() instanceof Double ) { + builder.add(entry.getKey(), (Double)entry.getValue()); + } else if ( entry.getValue() instanceof Boolean ) { + builder.add(entry.getKey(), (Boolean)entry.getValue()); + } else if ( entry.getValue() instanceof Map ) { + builder.add(entry.getKey(), build(entry.getValue())); + } else if ( entry.getValue() instanceof List ) { + builder.add(entry.getKey(), build(entry.getValue())); + } + } + return builder.build(); + } + return null; + } + + /** + * Parse a JSON content + * @param name The name of the file + * @param contents The contents + * @param report The report for errors and warnings + * @return The parsed JSON object or {@code null} on failure, + */ + public static JsonObject parseJSON(final String name, + String contents, + final Report report) { + // minify JSON first (remove comments) + try (final Reader in = new StringReader(contents); + final Writer out = new StringWriter()) { + final JSMin min = new JSMin(in, out); + min.jsmin(); + contents = out.toString(); + } catch ( final IOException ioe) { + report.errors.add("Invalid JSON from " + name); + return null; + } + // Jonhzon is packaged in, so we can just use the impl type to avoid ClassLoader mess + try (final JsonReader reader = new JsonProviderImpl().createReader(new StringReader(contents)) ) { + final JsonStructure obj = reader.read(); + if ( obj != null && obj.getValueType() == ValueType.OBJECT ) { + return (JsonObject)obj; + } + report.errors.add("Invalid JSON from " + name); + } + return null; + } + + /** + * Get the value of a JSON property + * @param root The JSON Object + * @param key The key in the JSON Obejct + * @return The value or {@code null} + */ + public static Object getValue(final JsonObject root, final String key) { + if ( !root.containsKey(key) ) { + return null; + } + final JsonValue value = root.get(key); + return getValue(value); + } + + public static Object getValue(final JsonValue value) { + switch ( value.getValueType() ) { + // type NULL -> return null + case NULL : return null; + // type TRUE or FALSE -> return boolean + case FALSE : return false; + case TRUE : return true; + // type String -> return String + case STRING : return ((JsonString)value).getString(); + // type Number -> return long or double + case NUMBER : final JsonNumber num = (JsonNumber)value; + if (num.isIntegral()) { + return num.longValue(); + } + return num.doubleValue(); + // type ARRAY -> return list and call this method for each value + case ARRAY : final List array = new ArrayList<>(); + for(final JsonValue x : ((JsonArray)value)) { + array.add(getValue(x)); + } + return array; + // type OBJECT -> return map + case OBJECT : final Map map = new LinkedHashMap<>(); + final JsonObject obj = (JsonObject)value; + for(final Map.Entry entry : obj.entrySet()) { + map.put(entry.getKey(), getValue(entry.getValue())); + } + return map; + } + return null; + } + + public static Object getTypedValue(final TypeConverter converter, + final String pid, + final Object value, + final String typeInfo) throws IOException { + Object convertedVal = converter.convert(pid, value, typeInfo); + if ( convertedVal == null ) { + if ( typeInfo != null ) { + throw new IOException("Unable to convert to type " + typeInfo); + } + JsonStructure json = build(value); + if ( json == null ) { + convertedVal = value.toString(); + } else { + // JSON Structure, this will result in a String or in an array of Strings + if ( json.getValueType() == ValueType.ARRAY ) { + final JsonArray arr = (JsonArray)json; + final String[] val = new String[arr.size()]; + for(int i=0;i verifyJSON(final String name, + final JsonObject root, + final boolean bundleResource, + final Report report) { + if ( root == null ) { + return null; + } + final Object version = getValue(root, ConfiguratorConstants.PROPERTY_RESOURCE_VERSION); + if ( version != null ) { + + final int v = TypeConverter.getConverter().convert(version).defaultValue(-1).to(Integer.class); + if ( v == -1 ) { + report.errors.add("Invalid resource version information in " + name + " : " + version); + return null; + } + // we only support version 1 + if ( v != 1 ) { + report.errors.add("Invalid resource version number in " + name + " : " + version); + return null; + } + } + if ( !bundleResource) { + // if this is not a bundle resource + // then version and symbolic name must be set + final Object rsrcVersion = getValue(root, ConfiguratorConstants.PROPERTY_VERSION); + if ( rsrcVersion == null ) { + report.errors.add("Missing version information in " + name); + return null; + } + if ( !(rsrcVersion instanceof String) ) { + report.errors.add("Invalid version information in " + name + " : " + rsrcVersion); + return null; + } + final Object rsrcName = getValue(root, ConfiguratorConstants.PROPERTY_SYMBOLIC_NAME); + if ( rsrcName == null ) { + report.errors.add("Missing symbolic name information in " + name); + return null; + } + if ( !(rsrcName instanceof String) ) { + report.errors.add("Invalid symbolic name information in " + name + " : " + rsrcName); + return null; + } + } + return (Map) getValue(root); + } + + /** + * Read the contents of a resource, encoded as UTF-8 + * @param name The resource name + * @param url The resource URL + * @return The contents + * @throws IOException If anything goes wrong + */ + public static String getResource(final String name, final URL url) + throws IOException { + final URLConnection connection = url.openConnection(); + + try(final BufferedReader in = new BufferedReader( + new InputStreamReader( + connection.getInputStream(), "UTF-8"))) { + + final StringBuilder sb = new StringBuilder(); + String line; + + while ((line = in.readLine()) != null) { + sb.append(line); + sb.append('\n'); + } + + return sb.toString(); + } + } +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/json/OrderedDictionary.java b/configurator/src/main/java/org/apache/felix/configurator/impl/json/OrderedDictionary.java new file mode 100644 index 00000000000..d7e0463b099 --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/json/OrderedDictionary.java @@ -0,0 +1,145 @@ +/* + * 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.configurator.impl.json; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * A dictionary implementation with predictable iteration order. + * + * Actually this class is a simple adapter from the Dictionary interface + * to a synchronized LinkedHashMap + */ +public class OrderedDictionary extends Dictionary implements Map, Serializable { + + private static final long serialVersionUID = -525111601546803041L; + + private static class EnumarationImpl implements Enumeration { + private final Iterator iterator; + + public EnumarationImpl(Iterator iterator) { + this.iterator = iterator; + } + + @Override + public boolean hasMoreElements() { + return iterator.hasNext(); + } + + @Override + public E nextElement() { + return iterator.next(); + } + } + + private Map map = Collections.synchronizedMap(new LinkedHashMap()); + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return map.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return map.containsValue(value); + } + + @Override + public Enumeration keys() { + return new EnumarationImpl<>(map.keySet().iterator()); + } + + @Override + public Enumeration elements() { + return new EnumarationImpl<>(map.values().iterator()); + } + + @Override + public Object get(Object key) { + return map.get(key); + } + + @Override + public Object put(String key, Object value) { + // Make sure the value is not null + if (value == null) { + throw new NullPointerException(); + } + + return map.put(key, value); + } + + @Override + public Object remove(Object key) { + return map.remove(key); + } + + @Override + public void putAll(Map m) { + map.putAll(m); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public Set keySet() { + return map.keySet(); + } + + @Override + public Collection values() { + return map.values(); + } + + @Override + public Set> entrySet() { + return map.entrySet(); + } + + @Override + public boolean equals(Object o) { + return map.equals(o); + } + + @Override + public int hashCode() { + return map.hashCode(); + } +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/json/TypeConverter.java b/configurator/src/main/java/org/apache/felix/configurator/impl/json/TypeConverter.java new file mode 100644 index 00000000000..e57a5451fb4 --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/json/TypeConverter.java @@ -0,0 +1,320 @@ +/* + * 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.configurator.impl.json; + +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.json.JsonStructure; + +import org.apache.johnzon.core.JsonProviderImpl; +import org.osgi.util.converter.Converter; +import org.osgi.util.converter.ConverterFunction; +import org.osgi.util.converter.Converters; +import org.osgi.util.converter.TargetRule; +import org.osgi.util.converter.TypeReference; + +public class TypeConverter { + + public static Converter getConverter() { + return Converters.standardConverter().newConverterBuilder().rule(new TargetRule() { + + @Override + public Type getTargetType() { + return String.class; + } + + @Override + public ConverterFunction getFunction() { + return new ConverterFunction() { + + @Override + public Object apply(final Object obj, final Type targetType) throws Exception { + if ( obj instanceof Map || obj instanceof List ) { + final JsonStructure json = JSONUtil.build(obj); + final StringWriter w = new StringWriter(); + new JsonProviderImpl().createWriter(w).write(json); + return w.toString(); + } + return CANNOT_HANDLE; + } + }; + } + }).build(); + } + + private static final Map> TYPE_MAP = new HashMap<>(); + static { + // scalar types and primitive types + TYPE_MAP.put("String", String.class); + TYPE_MAP.put("Integer", Integer.class); + TYPE_MAP.put("int", Integer.class); + TYPE_MAP.put("Long", Long.class); + TYPE_MAP.put("long", Long.class); + TYPE_MAP.put("Float", Float.class); + TYPE_MAP.put("float", Float.class); + TYPE_MAP.put("Double", Double.class); + TYPE_MAP.put("double", Double.class); + TYPE_MAP.put("Byte", Byte.class); + TYPE_MAP.put("byte", Byte.class); + TYPE_MAP.put("Short", Short.class); + TYPE_MAP.put("short", Short.class); + TYPE_MAP.put("Character", Character.class); + TYPE_MAP.put("char", Character.class); + TYPE_MAP.put("Boolean", Boolean.class); + TYPE_MAP.put("boolean", Boolean.class); + // array of scalar types and primitive types + TYPE_MAP.put("String[]", String[].class); + TYPE_MAP.put("Integer[]", Integer[].class); + TYPE_MAP.put("int[]", int[].class); + TYPE_MAP.put("Long[]", Long[].class); + TYPE_MAP.put("long[]", long[].class); + TYPE_MAP.put("Float[]", Float[].class); + TYPE_MAP.put("float[]", float[].class); + TYPE_MAP.put("Double[]", Double[].class); + TYPE_MAP.put("double[]", double[].class); + TYPE_MAP.put("Byte[]", Byte[].class); + TYPE_MAP.put("byte[]", byte[].class); + TYPE_MAP.put("Short[]", Short[].class); + TYPE_MAP.put("short[]", short[].class); + TYPE_MAP.put("Boolean[]", Boolean[].class); + TYPE_MAP.put("boolean[]", boolean[].class); + TYPE_MAP.put("Character[]", Character[].class); + TYPE_MAP.put("char[]", char[].class); + } + + private final List allFiles = new ArrayList<>(); + + private final List files = new ArrayList<>(); + + private final BinUtil.ResourceProvider provider; + + /** + * Create a new instance + * @param provider The bundle provider, might be {@code null}. + */ + public TypeConverter(final BinUtil.ResourceProvider provider) { + this.provider = provider; + } + + /** + * Convert a value to the given type + * @param value The value + * @param typeInfo Optional type info, might be {@code null} + * @return The converted value or {@code null} if the conversion failed. + * @throws IOException If an error happens + */ + public Object convert( + final String pid, + final Object value, + final String typeInfo) throws IOException { + if ( typeInfo == null ) { + if ( value instanceof String || value instanceof Boolean ) { + return value; + } else if ( value instanceof Long || value instanceof Double ) { + return value; + } else if ( value instanceof Integer ) { + return ((Integer)value).longValue(); + } else if ( value instanceof Short ) { + return ((Short)value).longValue(); + } else if ( value instanceof Byte ) { + return ((Byte)value).longValue(); + } else if ( value instanceof Float ) { + return ((Float)value).doubleValue(); + } + if ( value instanceof List ) { + @SuppressWarnings("unchecked") + final List list = (List)value; + if ( list.isEmpty() ) { + return new String[0]; + } + final Object firstObject = list.get(0); + boolean hasListOrMap = false; + for(final Object v : list) { + if ( v instanceof List || v instanceof Map ) { + hasListOrMap = true; + break; + } + } + Object convertedValue = null; + if ( !hasListOrMap ) { + if ( firstObject instanceof Boolean ) { + convertedValue = getConverter().convert(list).defaultValue(null).to(Boolean[].class); + } else if ( firstObject instanceof Long || firstObject instanceof Integer || firstObject instanceof Byte || firstObject instanceof Short ) { + convertedValue = getConverter().convert(list).defaultValue(null).to(Long[].class); + } else if ( firstObject instanceof Double || firstObject instanceof Float ) { + convertedValue = getConverter().convert(list).defaultValue(null).to(Double[].class); + } + } + if ( convertedValue == null ) { + convertedValue = getConverter().convert(list).defaultValue(null).to(String[].class); + } + return convertedValue; + } + return null; + } + + // binary + if ( "binary".equals(typeInfo) ) { + if ( provider == null ) { + throw new IOException("Binary files only allowed within a bundle"); + } + final String path = getConverter().convert(value).defaultValue(null).to(String.class); + if ( path == null ) { + throw new IOException("Invalid path for binary property: " + value); + } + final File filePath; + try { + filePath = BinUtil.extractFile(provider, pid, path); + } catch ( final IOException ioe ) { + throw new IOException("Unable to read " + path + + " in bundle " + provider.getIdentifier() + + " for pid " + pid + + " and write to " + BinUtil.binDirectory + " : " + ioe.getMessage(), ioe); + } + if ( filePath == null ) { + throw new IOException("Entry " + path + " not found in bundle " + provider.getIdentifier()); + } + files.add(filePath); + allFiles.add(filePath); + return filePath.getAbsolutePath(); + + } else if ( "binary[]".equals(typeInfo) ) { + if ( provider == null ) { + throw new IOException("Binary files only allowed within a bundle"); + } + final String[] paths = getConverter().convert(value).defaultValue(null).to(String[].class); + if ( paths == null ) { + throw new IOException("Invalid paths for binary[] property: " + value); + } + final String[] filePaths = new String[paths.length]; + int i = 0; + while ( i < paths.length ) { + final File filePath; + try { + filePath = BinUtil.extractFile(provider, pid, paths[i]); + } catch ( final IOException ioe ) { + throw new IOException("Unable to read " + paths[i] + + " in bundle " + provider.getIdentifier() + + " for pid " + pid + + " and write to " + BinUtil.binDirectory + " : " + ioe.getMessage(), ioe); + } + if ( filePath == null ) { + throw new IOException("Entry " + paths[i] + " not found in bundle " + provider.getIdentifier()); + } + files.add(filePath); + allFiles.add(filePath); + filePaths[i] = filePath.getAbsolutePath(); + i++; + } + return filePaths; + } + + final Class typeClass = TYPE_MAP.get(typeInfo); + if ( typeClass != null ) { + return getConverter().convert(value).defaultValue(null).to(typeClass); + } + + // Collections of scalar types + if ( "Collection".equals(typeInfo) ) { + return getConverter().convert(value).defaultValue(null).to(new TypeReference>() {}); + + } else if ( "Collection".equals(typeInfo) ) { + return getConverter().convert(value).defaultValue(null).to(new TypeReference>() {}); + + } else if ( "Collection".equals(typeInfo) ) { + return getConverter().convert(value).defaultValue(null).to(new TypeReference>() {}); + + } else if ( "Collection".equals(typeInfo) ) { + return getConverter().convert(value).defaultValue(null).to(new TypeReference>() {}); + + } else if ( "Collection".equals(typeInfo) ) { + return getConverter().convert(value).defaultValue(null).to(new TypeReference>() {}); + + } else if ( "Collection".equals(typeInfo) ) { + return getConverter().convert(value).defaultValue(null).to(new TypeReference>() {}); + + } else if ( "Collection".equals(typeInfo) ) { + return getConverter().convert(value).defaultValue(null).to(new TypeReference>() {}); + + } else if ( "Collection".equals(typeInfo) ) { + return getConverter().convert(value).defaultValue(null).to(new TypeReference>() {}); + + } else if ( "Collection".equals(typeInfo) ) { + return getConverter().convert(value).defaultValue(null).to(new TypeReference>() {}); + } else if ( "Collection".equals(typeInfo) ) { + if ( value instanceof List ) { + @SuppressWarnings("unchecked") + final List list = (List)value; + if ( list.isEmpty() ) { + return Collections.EMPTY_LIST; + } + final Object firstObject = list.get(0); + boolean hasListOrMap = false; + for(final Object v : list) { + if ( v instanceof List || v instanceof Map ) { + hasListOrMap = true; + break; + } + } + Object convertedValue = null; + if ( !hasListOrMap ) { + if ( firstObject instanceof Boolean ) { + convertedValue = getConverter().convert(list).defaultValue(null).to(new TypeReference>() {}); + } else if ( firstObject instanceof Long || firstObject instanceof Integer || firstObject instanceof Byte || firstObject instanceof Short) { + convertedValue = getConverter().convert(list).defaultValue(null).to(new TypeReference>() {}); + } else if ( firstObject instanceof Double || firstObject instanceof Float ) { + convertedValue = getConverter().convert(list).defaultValue(null).to(new TypeReference>() {}); + } + } + if ( convertedValue == null ) { + convertedValue = getConverter().convert(list).defaultValue(null).to(new TypeReference>() {}); + } + return convertedValue; + } + return getConverter().convert(value).defaultValue(null).to(ArrayList.class); + } + + // unknown type - ignore configuration + throw new IOException("Invalid type information: " + typeInfo); + } + + public void cleanupFiles() { + for(final File f : allFiles) { + f.delete(); + } + } + + public List flushFiles() { + if ( this.files.isEmpty() ) { + return null; + } else { + final List result = new ArrayList<>(this.files); + this.files.clear(); + return result; + } + } +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/logger/InternalLogger.java b/configurator/src/main/java/org/apache/felix/configurator/impl/logger/InternalLogger.java new file mode 100644 index 00000000000..2dcae023ffc --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/logger/InternalLogger.java @@ -0,0 +1,24 @@ +/* + * 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.configurator.impl.logger; + +interface InternalLogger { + + void log(int level, String message, Throwable exception); +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/logger/LogServiceEnabledLogger.java b/configurator/src/main/java/org/apache/felix/configurator/impl/logger/LogServiceEnabledLogger.java new file mode 100644 index 00000000000..a9ca6c2077e --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/logger/LogServiceEnabledLogger.java @@ -0,0 +1,111 @@ +/* + * 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.configurator.impl.logger; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +/** + * This class adds support for using a LogService + */ +public class LogServiceEnabledLogger { + // name of the LogService class (this is a string to not create a reference to the class) + private static final String LOGSERVICE_CLASS = "org.osgi.service.log.LogService"; + + // the log service to log messages to + protected final ServiceTracker logServiceTracker; + + private volatile InternalLogger currentLogger; + + protected volatile int trackingCount = -2; + + public LogServiceEnabledLogger(final BundleContext bundleContext) { + // Start a tracker for the log service + // we only track a single log service which in reality should be enough + logServiceTracker = new ServiceTracker<>( bundleContext, LOGSERVICE_CLASS, new ServiceTrackerCustomizer() { + private volatile boolean hasService = false; + + @Override + public Object addingService(final ServiceReference reference) { + if ( !hasService ) { + final Object logService = bundleContext.getService(reference); + if ( logService != null ) { + hasService = true; + final LogServiceSupport lsl = new LogServiceSupport(logService); + return lsl; + } + } + return null; + } + + @Override + public void modifiedService(final ServiceReference reference, final Object service) { + // nothing to do + } + + @Override + public void removedService(final ServiceReference reference, final Object service) { + hasService = false; + bundleContext.ungetService(reference); + } + } ); + logServiceTracker.open(); + } + + /** + * Close the logger + */ + public void close() { + // stop the tracker + logServiceTracker.close(); + } + + /** + * Method to actually emit the log message. If the LogService is available, + * the message will be logged through the LogService. Otherwise the message + * is logged to stdout (or stderr in case of LOG_ERROR level messages), + * + * @param level The log level of the messages. This corresponds to the log + * levels defined by the OSGi LogService. + * @param message The message to print + * @param ex The Throwable causing the message to be logged. + */ + public void log(final int level, final String message, final Throwable ex) { + getLogger().log(level, message, ex); + } + + private InternalLogger getLogger() { + if ( this.trackingCount < this.logServiceTracker.getTrackingCount() ) { + final Object logServiceSupport = this.logServiceTracker.getService(); + if ( logServiceSupport == null ) { + this.currentLogger = this.getDefaultLogger(); + } else { + this.currentLogger = ((LogServiceSupport)logServiceSupport).getLogger(); + } + this.trackingCount = this.logServiceTracker.getTrackingCount(); + } + return currentLogger; + } + + private InternalLogger getDefaultLogger() { + return new StdOutLogger(); + } +} \ No newline at end of file diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/logger/LogServiceSupport.java b/configurator/src/main/java/org/apache/felix/configurator/impl/logger/LogServiceSupport.java new file mode 100644 index 00000000000..f2da7b74d66 --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/logger/LogServiceSupport.java @@ -0,0 +1,37 @@ +/* + * 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.configurator.impl.logger; + +import org.osgi.service.log.LogService; + +/** + * This is a logger based on the LogService. + */ +class LogServiceSupport { + + private final LogService logService; + + public LogServiceSupport(final Object logService) { + this.logService = (LogService) logService; + } + + InternalLogger getLogger() { + return new R6LogServiceLogger(this.logService); + } +} \ No newline at end of file diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/logger/R6LogServiceLogger.java b/configurator/src/main/java/org/apache/felix/configurator/impl/logger/R6LogServiceLogger.java new file mode 100644 index 00000000000..542c66e17fc --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/logger/R6LogServiceLogger.java @@ -0,0 +1,37 @@ +/* + * 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.configurator.impl.logger; + +import org.osgi.service.log.LogService; + +/** + * This is a logger based on the R6 LogService. + */ +class R6LogServiceLogger implements InternalLogger { + private final LogService logService; + + public R6LogServiceLogger(final LogService logService) { + this.logService = logService; + } + + @Override + public void log(final int level, final String message, final Throwable ex) { + this.logService.log(level, message, ex); + } +} \ No newline at end of file diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/logger/StdOutLogger.java b/configurator/src/main/java/org/apache/felix/configurator/impl/logger/StdOutLogger.java new file mode 100644 index 00000000000..36a76803e53 --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/logger/StdOutLogger.java @@ -0,0 +1,69 @@ +/* + * 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.configurator.impl.logger; + +import java.io.PrintStream; + +import org.osgi.service.log.LogService; + +/** + * This logger logs to std out / err + */ +class StdOutLogger implements InternalLogger { + + @Override + public void log(final int level, final String message, final Throwable ex) { + // output depending on level + final PrintStream out = ( level == LogService.LOG_ERROR )? System.err: System.out; + + // level as a string + final StringBuilder buf = new StringBuilder(); + switch (level) { + case ( LogService.LOG_DEBUG ): + buf.append( "[DEBUG] " ); + break; + case ( LogService.LOG_INFO ): + buf.append( "[INFO] " ); + break; + case ( LogService.LOG_WARNING ): + buf.append( "[WARN] " ); + break; + case ( LogService.LOG_ERROR ): + buf.append( "[ERROR] " ); + break; + default: + buf.append( "[UNK] " ); + break; + } + + buf.append(message); + + final String msg = buf.toString(); + + if ( ex == null ) { + out.println(msg); + } else { + // keep the message and the stacktrace together + synchronized ( out ) { + out.println( msg ); + ex.printStackTrace( out ); + } + } + } +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/logger/SystemLogger.java b/configurator/src/main/java/org/apache/felix/configurator/impl/logger/SystemLogger.java new file mode 100644 index 00000000000..23ed447a82c --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/logger/SystemLogger.java @@ -0,0 +1,73 @@ +/* + * 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.configurator.impl.logger; + +import org.osgi.framework.BundleContext; +import org.osgi.service.log.LogService; + +public final class SystemLogger { + + private static volatile LogServiceEnabledLogger LOGGER; + + public static void init(final BundleContext bundleContext) { + LOGGER = new LogServiceEnabledLogger(bundleContext); + } + + public static void destroy() { + if ( LOGGER != null ) { + LOGGER.close(); + LOGGER = null; + } + } + + private static void log(final int level, final String message, final Throwable cause) { + final LogServiceEnabledLogger l = LOGGER; + if ( l != null ) { + l.log(level, message, cause); + } + } + + public static void debug(final String message) { + log(LogService.LOG_DEBUG, message, null); + } + + public static void debug(final String message, final Throwable cause) { + log(LogService.LOG_DEBUG, message, cause); + } + + public static void info(final String message) { + log(LogService.LOG_INFO, message, null); + } + + public static void warning(final String message) { + log(LogService.LOG_WARNING, message, null); + } + + public static void warning(final String message, final Throwable cause) { + log(LogService.LOG_WARNING, message, cause); + } + + public static void error(final String message) { + log(LogService.LOG_ERROR, message, null); + } + + public static void error(final String message, final Throwable cause) { + log(LogService.LOG_ERROR, message, cause); + } +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/model/AbstractState.java b/configurator/src/main/java/org/apache/felix/configurator/impl/model/AbstractState.java new file mode 100644 index 00000000000..fb3a37239b0 --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/model/AbstractState.java @@ -0,0 +1,88 @@ +/* + * 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.configurator.impl.model; + +import java.io.IOException; +import java.io.Serializable; +import java.util.Collection; +import java.util.Map; +import java.util.TreeMap; + +/** + * This object holds a sorted map of configurations + */ +public class AbstractState implements Serializable { + + private static final long serialVersionUID = 1L; + + /** Serialization version. */ + private static final int VERSION = 1; + + private Map configurationsByPid = new TreeMap<>(); + + /** + * Serialize the object + * - write version id + * - serialize fields + * @param out Object output stream + * @throws IOException + */ + private void writeObject(final java.io.ObjectOutputStream out) + throws IOException { + out.writeInt(VERSION); + out.writeObject(configurationsByPid); + } + + /** + * Deserialize the object + * - read version id + * - deserialize fields + */ + @SuppressWarnings("unchecked") + private void readObject(final java.io.ObjectInputStream in) + throws IOException, ClassNotFoundException { + final int version = in.readInt(); + if ( version < 1 || version > VERSION ) { + throw new ClassNotFoundException(this.getClass().getName()); + } + this.configurationsByPid = (Map) in.readObject(); + } + + public void add(final Config c) { + ConfigList configs = this.configurationsByPid.get(c.getPid()); + if ( configs == null ) { + configs = new ConfigList(); + this.configurationsByPid.put(c.getPid(), configs); + } + + configs.add(c); + } + + public Map getConfigurations() { + return this.configurationsByPid; + } + + public ConfigList getConfigurations(final String pid) { + return this.getConfigurations().get(pid); + } + + public Collection getPids() { + return this.getConfigurations().keySet(); + } +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/model/BundleState.java b/configurator/src/main/java/org/apache/felix/configurator/impl/model/BundleState.java new file mode 100644 index 00000000000..7f77f73928b --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/model/BundleState.java @@ -0,0 +1,46 @@ +/* + * 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.configurator.impl.model; + +import java.io.Serializable; +import java.util.List; + +/** + * This object holds all configurations provided by a single bundle + * when the configurations are read. + * Later on it just holds the last modified information. The configurations + * are merged into the {@code State} object. + */ +public class BundleState extends AbstractState implements Serializable { + + private static final long serialVersionUID = 1L; + + public void addFiles(final List allFiles) { + for(final ConfigurationFile f : allFiles) { + for(final Config c : f.getConfigurations()) { + // set index + final ConfigList list = this.getConfigurations(c.getPid()); + if ( list != null ) { + c.setIndex(list.size()); + } + this.add(c); + } + } + } +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/model/Config.java b/configurator/src/main/java/org/apache/felix/configurator/impl/model/Config.java new file mode 100644 index 00000000000..30fa956198f --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/model/Config.java @@ -0,0 +1,219 @@ +/* + * 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.configurator.impl.model; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.util.Dictionary; +import java.util.List; + +public class Config implements Serializable, Comparable { + + private static final long serialVersionUID = 1L; + + /** Serialization version. */ + private static final int VERSION = 1; + + /** The configuration pid */ + private String pid; + + /** The configuration ranking */ + private int ranking; + + /** The bundle id. */ + private long bundleId; + + /** The configuration policy. */ + private ConfigPolicy policy; + + /** The configuration properties. */ + private Dictionary properties; + + /** The index within the list of configurations if several. */ + private volatile int index = 0; + + /** The configuration state. */ + private volatile ConfigState state = ConfigState.INSTALL; + + private volatile List files; + + public Config(final String pid, + final Dictionary properties, + final long bundleId, + final int ranking, + final ConfigPolicy policy) { + this.pid = pid; + this.ranking = ranking; + this.bundleId = bundleId; + this.properties = properties; + this.policy = policy; + } + + /** + * Serialize the object + * - write version id + * - serialize fields + * @param out Object output stream + * @throws IOException + */ + private void writeObject(final java.io.ObjectOutputStream out) + throws IOException { + out.writeInt(VERSION); + out.writeObject(pid); + out.writeObject(properties); + out.writeObject(policy.name()); + out.writeLong(bundleId); + out.writeInt(ranking); + out.writeInt(index); + out.writeObject(state.name()); + out.writeObject(files); + } + + /** + * Deserialize the object + * - read version id + * - deserialize fields + */ + @SuppressWarnings("unchecked") + private void readObject(final java.io.ObjectInputStream in) + throws IOException, ClassNotFoundException { + final int version = in.readInt(); + if ( version < 1 || version > VERSION ) { + throw new ClassNotFoundException(this.getClass().getName()); + } + this.pid = (String) in.readObject(); + this.properties = (Dictionary) in.readObject(); + this.policy = ConfigPolicy.valueOf((String)in.readObject()); + this.bundleId = in.readLong(); + this.ranking = in.readInt(); + this.index = in.readInt(); + this.state = ConfigState.valueOf((String)in.readObject()); + this.files = (List) in.readObject(); + } + + /** + * Get the PID + * @return The pid. + */ + public String getPid() { + return this.pid; + } + + /** + * The configuration ranking + * @return The configuration ranking + */ + public int getRanking() { + return this.ranking; + } + + /** + * The bundle id + * @return The bundle id + */ + public long getBundleId() { + return this.bundleId; + } + + /** + * The index of the configuration. This value is only + * relevant if there are several configurations for the + * same pid with same ranking and bundle id. + * @return The index within the configuration set + */ + public int getIndex() { + return this.index; + } + + /** + * Set the index + */ + public void setIndex(final int value) { + this.index = value; + } + + /** + * Get the configuration state + * @return The state + */ + public ConfigState getState() { + return this.state; + } + + /** + * Set the configuration state + * @param value The new state + */ + public void setState(final ConfigState value) { + this.state = value; + } + + /** + * Get the policy + * @return The policy + */ + public ConfigPolicy getPolicy() { + return this.policy; + } + + /** + * Get all properties + * @return The configuration properties + */ + public Dictionary getProperties() { + return this.properties; + } + + public void setFiles(final List f) { + this.files = f; + } + + public List getFiles() { + return this.files; + } + + @Override + public int compareTo(final Config o) { + // sort by ranking, highest first + // if same ranking, sort by bundle id, lowest first + // if same bundle id, sort by index + if ( this.getRanking() > o.getRanking() ) { + return -1; + } else if ( this.getRanking() == o.getRanking() ) { + if ( this.getBundleId() < o.getBundleId() ) { + return -1; + } else if ( this.getBundleId() == o.getBundleId() ) { + return this.getIndex() - o.getIndex(); + } + } + return 1; + } + + @Override + public String toString() { + return "Config [pid=" + pid + + ", ranking=" + ranking + + ", bundleId=" + bundleId + + ", index=" + index + + ", properties=" + properties + + ", policy=" + policy + + ", state=" + state + "]"; + } +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/model/ConfigList.java b/configurator/src/main/java/org/apache/felix/configurator/impl/model/ConfigList.java new file mode 100644 index 00000000000..7cd0c574b9a --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/model/ConfigList.java @@ -0,0 +1,193 @@ +/* + * 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.configurator.impl.model; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * The config list holds all configurations for a single PID + */ +public class ConfigList implements Serializable, Iterable { + + private static final long serialVersionUID = 1L; + + /** Serialization version. */ + private static final int VERSION = 1; + + private List configurations = new ArrayList<>(); + + /** The change count. */ + private volatile long changeCount = -1; + + /** Flag to indicate whether this list needs to be processed. */ + private volatile boolean hasChanges; + + /** Last installed configuration. */ + private volatile Config lastInstalled; + + /** + * Serialize the object + * - write version id + * - serialize fields + * @param out Object output stream + * @throws IOException + */ + private void writeObject(final java.io.ObjectOutputStream out) + throws IOException { + out.writeInt(VERSION); + out.writeObject(configurations); + out.writeObject(lastInstalled); + out.writeLong(changeCount); + out.writeBoolean(hasChanges); + } + + /** + * Deserialize the object + * - read version id + * - deserialize fields + */ + @SuppressWarnings("unchecked") + private void readObject(final java.io.ObjectInputStream in) + throws IOException, ClassNotFoundException { + final int version = in.readInt(); + if ( version < 1 || version > VERSION ) { + throw new ClassNotFoundException(this.getClass().getName()); + } + this.configurations = (List) in.readObject(); + lastInstalled = (Config) in.readObject(); + this.changeCount = in.readLong(); + this.hasChanges = in.readBoolean(); + } + + /** + * Does this list need to be processed + * @return {@code true} if it needs processing. + */ + public boolean hasChanges() { + return hasChanges; + } + + /** + * Set the has changes flag. + * @param value New value. + */ + public void setHasChanges(final boolean value) { + this.hasChanges = hasChanges; + } + + /** + * Add a configuration to the list. + * @param c The configuration. + */ + public void add(final Config c) { + this.hasChanges = true; + this.configurations.add(c); + Collections.sort(this.configurations); + } + + /** + * Add all configurations from another list + * @param configs The config list + */ + public void addAll(final ConfigList configs) { + for(final Config cfg : configs) { + // search if we already have this configuration + + for(final Config current : this.configurations) { + if ( current.getBundleId() == cfg.getBundleId() + && current.getProperties().equals(cfg.getProperties()) ) { + if ( current.getState() == ConfigState.UNINSTALL ) { + cfg.setState(ConfigState.INSTALLED); + current.setState(ConfigState.UNINSTALLED); + } + break; + } + } + + this.hasChanges = true; + this.configurations.add(cfg); + } + Collections.sort(this.configurations); + } + + /** + * Get the size of the list of configurations + * @return + */ + public int size() { + return this.configurations.size(); + } + + @Override + public Iterator iterator() { + return this.configurations.iterator(); + } + + /** + * Get the change count. + * @return The change count + */ + public long getChangeCount() { + return this.changeCount; + } + + /** + * Set the change count + * @param value The new change count + */ + public void setChangeCount(final long value) { + this.changeCount = value; + } + + public Config getLastInstalled() { + return lastInstalled; + } + + public void setLastInstalled(Config lastInstalled) { + this.lastInstalled = lastInstalled; + } + + /** + * Mark configurations for a bundle uninstall + * @param bundleId The bundle id of the uninstalled bundle + */ + public void uninstall(final long bundleId) { + for(final Config cfg : this.configurations) { + if ( cfg.getBundleId() == bundleId ) { + this.hasChanges = true; + if ( cfg.getState() == ConfigState.INSTALLED ) { + cfg.setState(ConfigState.UNINSTALL); + } else { + cfg.setState(ConfigState.UNINSTALLED); + } + } + } + } + + @Override + public String toString() { + return "ConfigList [configurations=" + configurations + ", changeCount=" + changeCount + ", hasChanges=" + + hasChanges + ", lastInstalled=" + lastInstalled + "]"; + } +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/model/ConfigPolicy.java b/configurator/src/main/java/org/apache/felix/configurator/impl/model/ConfigPolicy.java new file mode 100644 index 00000000000..7461f4bbc81 --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/model/ConfigPolicy.java @@ -0,0 +1,25 @@ +/* + * 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.configurator.impl.model; + +public enum ConfigPolicy { + + DEFAULT, + FORCE +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/model/ConfigState.java b/configurator/src/main/java/org/apache/felix/configurator/impl/model/ConfigState.java new file mode 100644 index 00000000000..de347c1706c --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/model/ConfigState.java @@ -0,0 +1,39 @@ +/* + * 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.configurator.impl.model; + +/** + * The state of a configuration. + * + * The state represents the configurator's view. It might not + * reflect the current state of the system. For example if a + * configuration is installed through the configurator, it gets + * the state "INSTALLED". However if an administrator now deletes + * the configuration through any other way like e.g. the web console, + * the configuration still has the state "INSTALLED". + * + */ +public enum ConfigState { + + INSTALL, // the configuration should be installed + UNINSTALL, // the configuration should be uninstalled + INSTALLED, // the configuration is installed + UNINSTALLED, // the configuration is uninstalled + IGNORED // the configuration is ignored +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/model/ConfigurationFile.java b/configurator/src/main/java/org/apache/felix/configurator/impl/model/ConfigurationFile.java new file mode 100644 index 00000000000..177094ef146 --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/model/ConfigurationFile.java @@ -0,0 +1,52 @@ +/* + * 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.configurator.impl.model; + +import java.net.URL; +import java.util.List; + +/** + * This object holds all configurations from a single file. + * This is only an intermediate object. + */ +public class ConfigurationFile implements Comparable { + + private final URL url; + + private final List configurations; + + public ConfigurationFile(final URL url, final List configs) { + this.url = url; + this.configurations = configs; + } + + @Override + public int compareTo(final ConfigurationFile o) { + return url.getPath().compareTo(o.url.getPath()); + } + + @Override + public String toString() { + return "ConfigurationFile [url=" + url + ", configurations=" + configurations + "]"; + } + + public List getConfigurations() { + return this.configurations; + } +} diff --git a/configurator/src/main/java/org/apache/felix/configurator/impl/model/State.java b/configurator/src/main/java/org/apache/felix/configurator/impl/model/State.java new file mode 100644 index 00000000000..c42b4e90824 --- /dev/null +++ b/configurator/src/main/java/org/apache/felix/configurator/impl/model/State.java @@ -0,0 +1,181 @@ +/* + * 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.configurator.impl.model; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class State extends AbstractState implements Serializable { + + private static final long serialVersionUID = 1L; + + /** Serialization version. */ + private static final int VERSION = 1; + + public static final String FILE_NAME = "state.ser"; + + private Map bundlesLastModified = new HashMap<>(); + + private Map bundlesConfigAdminBundleId = new HashMap<>(); + + private volatile Set initialHashes; + + /** + * Serialize the object + * - write version id + * - serialize fields + * @param out Object output stream + * @throws IOException + */ + private void writeObject(final java.io.ObjectOutputStream out) + throws IOException { + out.writeInt(VERSION); + out.writeObject(bundlesLastModified); + out.writeObject(bundlesConfigAdminBundleId); + out.writeObject(initialHashes); + } + + /** + * Deserialize the object + * - read version id + * - deserialize fields + */ + @SuppressWarnings("unchecked") + private void readObject(final java.io.ObjectInputStream in) + throws IOException, ClassNotFoundException { + final int version = in.readInt(); + if ( version < 1 || version > VERSION ) { + throw new ClassNotFoundException(this.getClass().getName()); + } + this.bundlesLastModified =(Map) in.readObject(); + this.bundlesConfigAdminBundleId = (Map) in.readObject(); + initialHashes = (Set) in.readObject(); + } + + public static State createOrReadState(final File f) + throws ClassNotFoundException, IOException { + if ( f == null || !f.exists() ) { + return new State(); + } + try ( final ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f)) ) { + + return (State) ois.readObject(); + } + } + + public static void writeState(final File f, final State state) + throws IOException { + if ( f == null ) { + // do nothing, no file system support + return; + } + try ( final ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f)) ) { + oos.writeObject(state); + } + } + + public Long getLastModified(final long bundleId) { + return this.bundlesLastModified.get(bundleId); + } + + public void setLastModified(final long bundleId, final long lastModified) { + this.bundlesLastModified.put(bundleId, lastModified); + } + + public void removeLastModified(final long bundleId) { + this.bundlesLastModified.remove(bundleId); + } + + public Long getConfigAdminBundleId(final long bundleId) { + return this.bundlesConfigAdminBundleId.get(bundleId); + } + + public void setConfigAdminBundleId(final long bundleId, final long lastModified) { + this.bundlesConfigAdminBundleId.put(bundleId, lastModified); + } + + public void removeConfigAdminBundleId(final long bundleId) { + this.bundlesConfigAdminBundleId.remove(bundleId); + } + + public Set getKnownBundleIds() { + return this.bundlesLastModified.keySet(); + } + + public Set getInitialHashes() { + return this.initialHashes; + } + + public void setInitialHashes(final Set value) { + this.initialHashes = value; + } + + /** + * Add all configurations for a pid + * @param pid The pid + * @param configs The list of configurations + */ + public void addAll(final String pid, final ConfigList configs) { + if ( configs != null ) { + ConfigList list = this.getConfigurations().get(pid); + if ( list == null ) { + list = new ConfigList(); + this.getConfigurations().put(pid, list); + } + + list.addAll(configs); + } + } + + /** + * Mark all configurations from that bundle as changed to reprocess them + * @param bundleId The bundle id + */ + public void checkEnvironments(final long bundleId) { + for(final String pid : this.getPids()) { + final ConfigList configList = this.getConfigurations(pid); + for(final Config cfg : configList) { + if ( cfg.getBundleId() == bundleId ) { + configList.setHasChanges(true); + break; + } + } + } + } + + @Override + public String toString() { + return "State [bundlesLastModified=" + bundlesLastModified + + ", initialHashes=" + initialHashes + + ", bundlesConfigAdminBundleId=" + bundlesConfigAdminBundleId + "]"; + } + + public Set getBundleIdsUsingConfigAdmin() { + return new HashSet<>(this.bundlesConfigAdminBundleId.keySet()); + } +} diff --git a/configurator/src/test/java/org/apache/felix/configurator/impl/ConfigUtilTest.java b/configurator/src/test/java/org/apache/felix/configurator/impl/ConfigUtilTest.java new file mode 100644 index 00000000000..dbfeddaf5b7 --- /dev/null +++ b/configurator/src/test/java/org/apache/felix/configurator/impl/ConfigUtilTest.java @@ -0,0 +1,108 @@ +/* + * 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.configurator.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import org.junit.Test; +import org.osgi.framework.Constants; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +public class ConfigUtilTest { + + private String getFilterString(final String pid) { + return "(" + Constants.SERVICE_PID + "=" + pid + ")"; + } + + @Test public void testGetNoCreate() throws Exception { + final String pid = "a.b"; + final ConfigurationAdmin ca = mock(ConfigurationAdmin.class); + when(ca.listConfigurations(getFilterString(pid))).thenReturn(null); + assertNull(ConfigUtil.getOrCreateConfiguration(ca, pid, false)); + verify(ca).listConfigurations(getFilterString(pid)); + verifyNoMoreInteractions(ca); + } + + @Test public void testGetCreate() throws Exception { + final String pid = "a.b"; + final Configuration cfg = mock(Configuration.class); + when(cfg.getPid()).thenReturn(pid); + + final ConfigurationAdmin ca = mock(ConfigurationAdmin.class); + when(ca.listConfigurations(getFilterString(pid))).thenReturn(null); + when(ca.getConfiguration(pid, "?")).thenReturn(cfg); + assertEquals(cfg, ConfigUtil.getOrCreateConfiguration(ca, pid, true)); + verify(ca).listConfigurations(getFilterString(pid)); + verify(ca).getConfiguration(pid, "?"); + verifyNoMoreInteractions(ca); + } + + @Test public void testGetAvailable() throws Exception { + final String pid = "a.b"; + final Configuration cfg = mock(Configuration.class); + when(cfg.getPid()).thenReturn(pid); + + final ConfigurationAdmin ca = mock(ConfigurationAdmin.class); + when(ca.listConfigurations(getFilterString(pid))).thenReturn(new Configuration[] {cfg}); + assertEquals(cfg, ConfigUtil.getOrCreateConfiguration(ca, pid, true)); + verify(ca).listConfigurations(getFilterString(pid)); + verifyNoMoreInteractions(ca); + } + + @Test public void testGetFactoryNoCreate() throws Exception { + final String pid = "a.b~name"; + final ConfigurationAdmin ca = mock(ConfigurationAdmin.class); + when(ca.listConfigurations(getFilterString(pid))).thenReturn(null); + assertNull(ConfigUtil.getOrCreateConfiguration(ca, pid, false)); + verify(ca).listConfigurations(getFilterString(pid)); + verifyNoMoreInteractions(ca); + } + + @Test public void testGetFactoryCreate() throws Exception { + final String pid = "a.b~name"; + final Configuration cfg = mock(Configuration.class); + when(cfg.getPid()).thenReturn(pid); + + final ConfigurationAdmin ca = mock(ConfigurationAdmin.class); + when(ca.listConfigurations(getFilterString(pid))).thenReturn(null); + when(ca.getFactoryConfiguration("a.b", "name", "?")).thenReturn(cfg); + assertEquals(cfg, ConfigUtil.getOrCreateConfiguration(ca, pid, true)); + verify(ca).listConfigurations(getFilterString(pid)); + verify(ca).getFactoryConfiguration("a.b", "name", "?"); + verifyNoMoreInteractions(ca); + } + + @Test public void testGetFactoryAvailable() throws Exception { + final String pid = "a.b~name"; + final Configuration cfg = mock(Configuration.class); + when(cfg.getPid()).thenReturn(pid); + + final ConfigurationAdmin ca = mock(ConfigurationAdmin.class); + when(ca.listConfigurations(getFilterString(pid))).thenReturn(new Configuration[] {cfg}); + assertEquals(cfg, ConfigUtil.getOrCreateConfiguration(ca, pid, true)); + verify(ca).listConfigurations(getFilterString(pid)); + verifyNoMoreInteractions(ca); + } +} diff --git a/configurator/src/test/java/org/apache/felix/configurator/impl/ConfiguratorTest.java b/configurator/src/test/java/org/apache/felix/configurator/impl/ConfiguratorTest.java new file mode 100644 index 00000000000..f67628dfded --- /dev/null +++ b/configurator/src/test/java/org/apache/felix/configurator/impl/ConfiguratorTest.java @@ -0,0 +1,253 @@ +/* + * 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.configurator.impl; + +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Vector; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +public class ConfiguratorTest { + + private Configurator configurator; + + private BundleContext bundleContext; + + private Bundle bundle; + + private ConfigurationAdmin configurationAdmin; + + private ServiceReference caRef; + + @SuppressWarnings("unchecked") + @Before public void setup() throws IOException { + bundle = mock(Bundle.class); + when(bundle.getBundleId()).thenReturn(42L); + when(bundle.getState()).thenReturn(Bundle.ACTIVE); + bundleContext = mock(BundleContext.class); + when(bundle.getBundleContext()).thenReturn(bundleContext); + when(bundleContext.getBundle()).thenReturn(bundle); + when(bundleContext.getBundle(Constants.SYSTEM_BUNDLE_LOCATION)).thenReturn(bundle); + when(bundleContext.getBundle(42)).thenReturn(bundle); + when(bundleContext.getBundles()).thenReturn(new Bundle[0]); + when(bundleContext.getDataFile("binaries" + File.separatorChar + ".check")).thenReturn(Files.createTempDirectory("test").toFile()); + caRef = mock(ServiceReference.class); + when(caRef.getBundle()).thenReturn(bundle); + + configurationAdmin = mock(ConfigurationAdmin.class); + when(bundleContext.getService(caRef)).thenReturn(configurationAdmin); + + configurator = new Configurator(bundleContext, Collections.singletonList(caRef)); + } + + private Bundle setupBundle(final long id) throws Exception { + return setupBundle(id, String.valueOf(id)); + } + + private long lastModified = 1; + + private Bundle setupBundle(final long id, final String jsonName) throws Exception { + final Bundle b = mock(Bundle.class); + when(b.getBundleId()).thenReturn(id); + when(b.getLastModified()).thenReturn(lastModified); + lastModified++; + when(b.getState()).thenReturn(Bundle.ACTIVE); + final BundleWiring wiring = mock(BundleWiring.class); + when(b.adapt(BundleWiring.class)).thenReturn(wiring); + final BundleRequirement req = mock(BundleRequirement.class); + when(wiring.getRequirements(Util.NS_OSGI_EXTENDER)).thenReturn(Collections.singletonList(req)); + final BundleWire wire = mock(BundleWire.class); + when(wire.getProviderWiring()).thenReturn(wiring); + when(wire.getRequirement()).thenReturn(req); + when(wiring.getBundle()).thenReturn(bundle); + when(wiring.getRequiredWires(Util.NS_OSGI_EXTENDER)).thenReturn(Collections.singletonList(wire)); + final Vector urls = new Vector<>(); + urls.add(this.getClass().getResource("/bundles/" + jsonName + ".json")); + when(b.findEntries("OSGI-INF/configurator", "*.json", false)).thenReturn(urls.elements()); + + final BundleContext bContext = mock(BundleContext.class); + when(b.getBundleContext()).thenReturn(bContext); + when(bContext.getServiceReferences(ConfigurationAdmin.class, null)).thenReturn(Collections.singleton(caRef)); + when(bundleContext.getBundle(id)).thenReturn(b); + return b; + } + + @Test public void testSimpleAddRemove() throws Exception { + final Bundle b = setupBundle(1); + + Configuration c1 = mock(Configuration.class); + Configuration c2 = mock(Configuration.class); + when(configurationAdmin.getConfiguration("a", "?")).thenReturn(c1); + when(configurationAdmin.getConfiguration("b", "?")).thenReturn(c2); + + when(c1.getChangeCount()).thenReturn(1L); + when(c2.getChangeCount()).thenReturn(1L); + configurator.processAddBundle(b); + + configurator.process(); + + when(configurationAdmin.listConfigurations("(" + Constants.SERVICE_PID + "=a)")).thenReturn(new Configuration[] {c1}); + when(configurationAdmin.listConfigurations("(" + Constants.SERVICE_PID + "=b)")).thenReturn(new Configuration[] {c2}); + + final Dictionary props1 = new Hashtable<>(); + props1.put("foo", "bar"); + verify(c1).updateIfDifferent(props1); + final Dictionary props2 = new Hashtable<>(); + props2.put("x", "y"); + verify(c2).updateIfDifferent(props2); + + configurator.processRemoveBundle(1); + configurator.process(); + + verify(c1).delete(); + verify(c2).delete(); + } + + @Test public void testSimpleAddUpdate() throws Exception { + final Bundle bV1 = setupBundle(2); + final Bundle bV2 = setupBundle(2, "2a"); + + // initial - no configuration + Configuration c1 = mock(Configuration.class); + Configuration c2 = mock(Configuration.class); + Configuration c3 = mock(Configuration.class); + when(c1.getChangeCount()).thenReturn(1L); + when(c2.getChangeCount()).thenReturn(1L); + when(c3.getChangeCount()).thenReturn(1L); + when(configurationAdmin.getConfiguration("a", "?")).thenReturn(c1); + when(configurationAdmin.getConfiguration("b", "?")).thenReturn(c2); + when(configurationAdmin.getConfiguration("c", "?")).thenReturn(c3); + + configurator.processAddBundle(bV1); + configurator.process(); + + final Dictionary props1a = new Hashtable<>(); + props1a.put("foo", "bar2"); + final Dictionary props1b = new Hashtable<>(); + props1b.put("x", "y2"); + final Dictionary props1c = new Hashtable<>(); + props1c.put("c", "1"); + + InOrder inorder = inOrder(c1,c2,c3); + inorder.verify(c1).updateIfDifferent(props1a); + inorder.verify(c1).getChangeCount(); + inorder.verify(c2).updateIfDifferent(props1b); + inorder.verify(c2).getChangeCount(); + inorder.verify(c3).updateIfDifferent(props1c); + inorder.verify(c3).getChangeCount(); + inorder.verifyNoMoreInteractions(); + + // c1 and c2 are changed manually -> increase change count, c3 is not changed manually + when(c1.getChangeCount()).thenReturn(2L); + when(c2.getChangeCount()).thenReturn(2L); + when(configurationAdmin.listConfigurations("(service.pid=a)")).thenReturn(new Configuration[] {c1}); + when(configurationAdmin.listConfigurations("(service.pid=b)")).thenReturn(new Configuration[] {c2}); + when(configurationAdmin.listConfigurations("(service.pid=c)")).thenReturn(new Configuration[] {c3}); + + configurator.processAddBundle(bV2); + configurator.process(); + + final Dictionary props2a = new Hashtable<>(); + props2a.put("foo", "bar3"); + final Dictionary props2c = new Hashtable<>(); + props2c.put("c", "2"); + + inorder = inOrder(c1, c2, c3); + inorder.verify(c1).updateIfDifferent(props2a); + inorder.verify(c1).getChangeCount(); + inorder.verify(c2).getChangeCount(); + inorder.verify(c3).updateIfDifferent(props2c); + inorder.verify(c3).getChangeCount(); + inorder.verifyNoMoreInteractions(); + } + + @Test public void testSimpleRankingRemove() throws Exception { + final Bundle b1 = setupBundle(1); + final Bundle b2 = setupBundle(2); + + Configuration c1 = mock(Configuration.class); + Configuration c2 = mock(Configuration.class); + Configuration c3 = mock(Configuration.class); + when(configurationAdmin.getConfiguration("a", "?")).thenReturn(c1); + when(configurationAdmin.getConfiguration("b", "?")).thenReturn(c2); + when(configurationAdmin.getConfiguration("c", "?")).thenReturn(c3); + + when(c1.getChangeCount()).thenReturn(1L); + when(c2.getChangeCount()).thenReturn(1L); + when(c3.getChangeCount()).thenReturn(1L); + configurator.processAddBundle(b2); + configurator.process(); + + when(configurationAdmin.listConfigurations("(" + Constants.SERVICE_PID + "=a)")).thenReturn(new Configuration[] {c1}); + when(configurationAdmin.listConfigurations("(" + Constants.SERVICE_PID + "=b)")).thenReturn(new Configuration[] {c2}); + when(configurationAdmin.listConfigurations("(" + Constants.SERVICE_PID + "=c)")).thenReturn(new Configuration[] {c3}); + + final Dictionary props1 = new Hashtable<>(); + props1.put("foo", "bar2"); + final Dictionary props2 = new Hashtable<>(); + props2.put("x", "y2"); + + configurator.processAddBundle(b1); + configurator.process(); + + final Dictionary props3 = new Hashtable<>(); + props3.put("foo", "bar"); + final Dictionary props4 = new Hashtable<>(); + props4.put("x", "y"); + + configurator.processRemoveBundle(1); + configurator.process(); + + configurator.processRemoveBundle(2); + configurator.process(); + + InOrder inorder = inOrder(c1, c2); + inorder.verify(c1).updateIfDifferent(props1); + inorder.verify(c2).updateIfDifferent(props2); + inorder.verify(c1).updateIfDifferent(props3); + inorder.verify(c2).updateIfDifferent(props4); + inorder.verify(c1).updateIfDifferent(props1); + inorder.verify(c2).updateIfDifferent(props2); + inorder.verify(c1).delete(); + inorder.verify(c2).delete(); + inorder.verifyNoMoreInteractions(); + } +} diff --git a/configurator/src/test/java/org/apache/felix/configurator/impl/json/JSMinTest.java b/configurator/src/test/java/org/apache/felix/configurator/impl/json/JSMinTest.java new file mode 100644 index 00000000000..e04e6c8fb65 --- /dev/null +++ b/configurator/src/test/java/org/apache/felix/configurator/impl/json/JSMinTest.java @@ -0,0 +1,46 @@ +/* + * 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.configurator.impl.json; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; + +import org.junit.Test; + +public class JSMinTest { + + @Test public void simpleTest() throws IOException { + final String input = "// Some comment\n" + + "{\n" + + " \"a\" : 1,\n" + + " // another comment\n" + + " /** And more\n" + + " * comments\n" + + " */\n" + + " \"b\" : 2\n" + + "}\n"; + final StringWriter w = new StringWriter(); + final JSMin min = new JSMin(new StringReader(input), w); + min.jsmin(); + assertEquals("\n{\"a\":1,\"b\":2}", w.toString()); + } +} diff --git a/configurator/src/test/java/org/apache/felix/configurator/impl/json/JSONUtilTest.java b/configurator/src/test/java/org/apache/felix/configurator/impl/json/JSONUtilTest.java new file mode 100644 index 00000000000..1e25d1db5f1 --- /dev/null +++ b/configurator/src/test/java/org/apache/felix/configurator/impl/json/JSONUtilTest.java @@ -0,0 +1,92 @@ +/* + * 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.configurator.impl.json; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.net.URL; +import java.util.List; + +import javax.json.JsonObject; + +import org.apache.felix.configurator.impl.model.ConfigurationFile; +import org.junit.Test; + +public class JSONUtilTest { + + /** Read the data from that name */ + public static String readJSON(final String name) throws Exception { + + try ( final Reader reader = new InputStreamReader(JSONUtilTest.class.getResourceAsStream("/" + name), "UTF-8"); + final Writer writer = new StringWriter()) { + + final char[] buf = new char[2048]; + int len = 0; + while ((len = reader.read(buf)) > 0) { + writer.write(buf, 0, len); + } + + return writer.toString(); + } + } + + @Test public void testReadJSON() throws Exception { + final ConfigurationFile cg = JSONUtil.readJSON(new TypeConverter(null), + "a", new URL("http://a"), 1, readJSON("json/valid.json"), + new JSONUtil.Report()); + assertNotNull(cg); + assertEquals(2, cg.getConfigurations().size()); + } + + @SuppressWarnings("unchecked") + @Test public void testTypes() throws Exception { + final JsonObject config = JSONUtil.parseJSON("a", + JSONUtilTest.readJSON("json/simple-types.json"), + new JSONUtil.Report()); + final JsonObject properties = (JsonObject)config.get("config"); + + assertTrue(JSONUtil.getValue(properties, "string") instanceof String); + assertTrue(JSONUtil.getValue(properties, "boolean") instanceof Boolean); + assertTrue(JSONUtil.getValue(properties, "number") instanceof Long); + assertTrue(JSONUtil.getValue(properties, "float") instanceof Double); + + // arrays + assertTrue(JSONUtil.getValue(properties, "string.array") instanceof List); + assertTrue(((List)JSONUtil.getValue(properties, "string.array")).get(0) instanceof String); + assertTrue(((List)JSONUtil.getValue(properties, "string.array")).get(1) instanceof String); + + assertTrue((List)JSONUtil.getValue(properties, "boolean.array") instanceof List); + assertTrue(((List)JSONUtil.getValue(properties, "boolean.array")).get(0) instanceof Boolean); + assertTrue(((List)JSONUtil.getValue(properties, "boolean.array")).get(1) instanceof Boolean); + + assertTrue((List)JSONUtil.getValue(properties, "number.array") instanceof List); + assertTrue(((List)JSONUtil.getValue(properties, "number.array")).get(0) instanceof Long); + assertTrue(((List)JSONUtil.getValue(properties, "number.array")).get(1) instanceof Long); + + assertTrue((List)JSONUtil.getValue(properties, "float.array") instanceof List); + assertTrue(((List)JSONUtil.getValue(properties, "float.array")).get(0) instanceof Double); + assertTrue(((List)JSONUtil.getValue(properties, "float.array")).get(1) instanceof Double); + } +} \ No newline at end of file diff --git a/configurator/src/test/java/org/apache/felix/configurator/impl/json/TypeConverterTest.java b/configurator/src/test/java/org/apache/felix/configurator/impl/json/TypeConverterTest.java new file mode 100644 index 00000000000..aa3d3171672 --- /dev/null +++ b/configurator/src/test/java/org/apache/felix/configurator/impl/json/TypeConverterTest.java @@ -0,0 +1,332 @@ +/* + * 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.configurator.impl.json; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Iterator; + +import javax.json.JsonObject; + +import org.junit.Test; + +public class TypeConverterTest { + + @Test public void testStringConversionNoTypeInfo() throws IOException { + final String v_String = "world"; + final TypeConverter converter = new TypeConverter(null); + final Object result = converter.convert(null, v_String, null); + assertTrue(result instanceof String); + assertEquals(v_String, result); + } + + @Test public void testLongConversionNoTypeInfo() throws IOException { + final long v_long = 3; + final TypeConverter converter = new TypeConverter(null); + final Object result = converter.convert(null, v_long, null); + assertTrue(result instanceof Long); + assertEquals(v_long, result); + } + + @Test public void testIntegerConversionNoTypeInfo() throws IOException { + final int v_int = 3; + final TypeConverter converter = new TypeConverter(null); + final Object result = converter.convert(null, v_int, null); + assertTrue(result instanceof Long); + assertEquals(3L, result); + } + + @Test public void testShortConversionNoTypeInfo() throws IOException { + final short v_short = 3; + final TypeConverter converter = new TypeConverter(null); + final Object result = converter.convert(null, v_short, null); + assertTrue(result instanceof Long); + assertEquals(3L, result); + } + + @Test public void testByteConversionNoTypeInfo() throws IOException { + final byte v_byte = 3; + final TypeConverter converter = new TypeConverter(null); + final Object result = converter.convert(null, v_byte, null); + assertTrue(result instanceof Long); + assertEquals(3L, result); + } + + @Test public void testCharConversionNoTypeInfo() throws IOException { + final char v_char = 'a'; + final TypeConverter converter = new TypeConverter(null); + assertNull(converter.convert(null, v_char, null)); + } + + @Test public void testCharacterConversionNoTypeInfo() throws IOException { + final Character v_Character = new Character('a'); + final TypeConverter converter = new TypeConverter(null); + assertNull(converter.convert(null, v_Character, null)); + } + + @Test public void testFloatConversionNoTypeInfo() throws IOException { + final float v_float = 3.1f; + final TypeConverter converter = new TypeConverter(null); + final Object result = converter.convert(null, v_float, null); + assertTrue(result instanceof Double); + } + + @Test public void testDoubleConversionNoTypeInfo() throws IOException { + final double v_double = 3.0; + final TypeConverter converter = new TypeConverter(null); + final Object result = converter.convert(null, v_double, null); + assertTrue(result instanceof Double); + assertEquals(v_double, result); + } + + @Test public void testSimpleTypeConversions() throws Exception { + final TypeConverter converter = new TypeConverter(null); + + final JsonObject config = JSONUtil.parseJSON("a", + JSONUtilTest.readJSON("json/simple-types.json"), + new JSONUtil.Report()); + final JsonObject properties = (JsonObject)config.get("config"); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "string"), null) instanceof String); + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "boolean"), null) instanceof Boolean); + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "number"), null) instanceof Long); + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "float"), null) instanceof Double); + + // arrays + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "string.array"), null).getClass().isArray()); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "string.array"), null), 0) instanceof String); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "string.array"), null), 1) instanceof String); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "boolean.array"), null).getClass().isArray()); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "boolean.array"), null), 0) instanceof Boolean); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "boolean.array"), null), 1) instanceof Boolean); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "number.array"), null).getClass().isArray()); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "number.array"), null), 0) instanceof Long); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "number.array"), null), 1) instanceof Long); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "float.array"), null).getClass().isArray()); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "float.array"), null), 0) instanceof Double); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "float.array"), null), 1) instanceof Double); + } + + @Test public void testSimpleTypeConversionsWithTypeHint() throws Exception { + final TypeConverter converter = new TypeConverter(null); + + final JsonObject config = JSONUtil.parseJSON("a", + JSONUtilTest.readJSON("json/simple-types.json"), + new JSONUtil.Report()); + final JsonObject properties = (JsonObject)config.get("config"); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "string"), "String") instanceof String); + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "boolean"), "Boolean") instanceof Boolean); + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "boolean"), "boolean") instanceof Boolean); + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "number"), "Integer") instanceof Integer); + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "number"), "int") instanceof Integer); + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "number"), "Long") instanceof Long); + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "number"), "long") instanceof Long); + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "float"), "Double") instanceof Double); + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "float"), "double") instanceof Double); + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "float"), "Float") instanceof Float); + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "float"), "float") instanceof Float); + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "number"), "Byte") instanceof Byte); + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "number"), "byte") instanceof Byte); + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "number"), "Short") instanceof Short); + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "number"), "short") instanceof Short); + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "string"), "Character") instanceof Character); + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "string"), "char") instanceof Character); + + // arrays + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "string.array"), "String[]").getClass().isArray()); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "string.array"), "String[]"), 0) instanceof String); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "string.array"), "String[]"), 1) instanceof String); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "boolean.array"), "Boolean[]").getClass().isArray()); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "boolean.array"), "Boolean[]"), 0) instanceof Boolean); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "boolean.array"), "Boolean[]"), 1) instanceof Boolean); + + // the following would throw class cast exceptions + boolean[] a0 = (boolean[])converter.convert(null, JSONUtil.getValue(properties, "boolean.array"), "boolean[]"); + assertNotNull(a0); + int[] a1 = (int[])converter.convert(null, JSONUtil.getValue(properties, "number.array"), "int[]"); + assertNotNull(a1); + long[] a2 = (long[])converter.convert(null, JSONUtil.getValue(properties, "number.array"), "long[]"); + assertNotNull(a2); + double[] a3 = (double[])converter.convert(null, JSONUtil.getValue(properties, "float.array"), "double[]"); + assertNotNull(a3); + float[] a4 = (float[])converter.convert(null, JSONUtil.getValue(properties, "float.array"), "float[]"); + assertNotNull(a4); + byte[] a5 = (byte[])converter.convert(null, JSONUtil.getValue(properties, "number.array"), "byte[]"); + assertNotNull(a5); + short[] a6 = (short[])converter.convert(null, JSONUtil.getValue(properties, "number.array"), "short[]"); + assertNotNull(a6); + char[] a7 = (char[])converter.convert(null, JSONUtil.getValue(properties, "string.array"), "char[]"); + assertNotNull(a7); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Integer[]").getClass().isArray()); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Integer[]"), 0) instanceof Integer); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Integer[]"), 1) instanceof Integer); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Long[]").getClass().isArray()); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Long[]"), 0) instanceof Long); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Long[]"), 1) instanceof Long); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Byte[]").getClass().isArray()); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Byte[]"), 0) instanceof Byte); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Byte[]"), 1) instanceof Byte); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Short[]").getClass().isArray()); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Short[]"), 0) instanceof Short); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Short[]"), 1) instanceof Short); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "float.array"), "Float[]").getClass().isArray()); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "float.array"), "Float[]"), 0) instanceof Float); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "float.array"), "Float[]"), 1) instanceof Float); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "float.array"), "Double[]").getClass().isArray()); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "float.array"), "Double[]"), 0) instanceof Double); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "float.array"), "Double[]"), 1) instanceof Double); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "string.array"), "Character[]").getClass().isArray()); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "string.array"), "Character[]"), 0) instanceof Character); + assertTrue(Array.get(converter.convert(null, JSONUtil.getValue(properties, "string.array"), "Character[]"), 1) instanceof Character); + } + + @SuppressWarnings("unchecked") + @Test public void testCollectionTypeConversion() throws Exception { + final TypeConverter converter = new TypeConverter(null); + final JsonObject config = JSONUtil.parseJSON("a", + JSONUtilTest.readJSON("json/simple-types.json"), + new JSONUtil.Report()); + final JsonObject properties = (JsonObject)config.get("config"); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "string.array"), "Collection") instanceof Collection); + assertTrue(((Collection)converter.convert(null, JSONUtil.getValue(properties, "string.array"), "Collection")).iterator().next() instanceof String); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Collection") instanceof Collection); + assertTrue(((Collection)converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Collection")).iterator().next() instanceof Integer); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Collection") instanceof Collection); + assertTrue(((Collection)converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Collection")).iterator().next() instanceof Long); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "float.array"), "Collection") instanceof Collection); + assertTrue(((Collection)converter.convert(null, JSONUtil.getValue(properties, "float.array"), "Collection")).iterator().next() instanceof Float); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "float.array"), "Collection") instanceof Collection); + assertTrue(((Collection)converter.convert(null, JSONUtil.getValue(properties, "float.array"), "Collection")).iterator().next() instanceof Double); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Collection") instanceof Collection); + assertTrue(((Collection)converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Collection")).iterator().next() instanceof Short); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Collection") instanceof Collection); + assertTrue(((Collection)converter.convert(null, JSONUtil.getValue(properties, "number.array"), "Collection")).iterator().next() instanceof Byte); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "string.array"), "Collection") instanceof Collection); + assertTrue(((Collection)converter.convert(null, JSONUtil.getValue(properties, "string.array"), "Collection")).iterator().next() instanceof Character); + + assertTrue(converter.convert(null, JSONUtil.getValue(properties, "boolean.array"), "Collection") instanceof Collection); + assertTrue(((Collection)converter.convert(null, JSONUtil.getValue(properties, "boolean.array"), "Collection")).iterator().next() instanceof Boolean); + } + + private Object getConverted(final String propName, final String typeInfo) throws Exception { + final TypeConverter converter = new TypeConverter(null); + final JsonObject config = JSONUtil.parseJSON("a", + JSONUtilTest.readJSON("json/complex-types.json"), + new JSONUtil.Report()); + final JsonObject properties = (JsonObject)config.get("config"); + + final Object value = JSONUtil.getValue(properties, propName); + assertNotNull(value); + final Object converted = JSONUtil.getTypedValue(converter, null, value, typeInfo); + assertNotNull(converted); + + return converted; + } + + @SuppressWarnings("unchecked") + @Test public void testUntypedLongCollection() throws Exception { + final Object converted = getConverted("untyped", "Collection"); + assertTrue(converted instanceof Collection); + final Iterator iter = ((Collection)converted).iterator(); + assertEquals(1L, iter.next()); + assertEquals(2L, iter.next()); + assertEquals(3L, iter.next()); + assertFalse(iter.hasNext()); + } + + @SuppressWarnings("unchecked") + @Test public void testUntypedMixedCollection() throws Exception { + // an untyped collection is tried to be converted to the type + // of the first item in the list. If that fails, String is used. + final Object converted = getConverted("untyped_mixed", "Collection"); + assertTrue(converted instanceof Collection); + final Iterator iter = ((Collection)converted).iterator(); + assertEquals("1", iter.next()); + assertEquals("two", iter.next()); + assertEquals("3", iter.next()); + assertFalse(iter.hasNext()); + } + + @SuppressWarnings("unchecked") + @Test public void testEmptyTypedCollection() throws Exception { + final Object converted = getConverted("empty", "Collection"); + assertTrue(converted instanceof Collection); + final Iterator iter = ((Collection)converted).iterator(); + assertFalse(iter.hasNext()); + } + + @SuppressWarnings("unchecked") + @Test public void testEmptyUntypedCollection() throws Exception { + final Object converted = getConverted("empty", "Collection"); + assertTrue(converted instanceof Collection); + final Iterator iter = ((Collection)converted).iterator(); + assertFalse(iter.hasNext()); + } + + @Test public void testObjectArray() throws Exception { + final Object converted = getConverted("objects_array", null); + assertTrue(converted.getClass().isArray()); + final String[] vals = (String[])converted; + assertEquals(2, vals.length); + assertTrue(vals[0] instanceof String); + assertTrue(vals[1] instanceof String); + assertEquals("{\"foo\":1}", vals[0]); + assertEquals("{\"foo\":2}", vals[1]); + } + + @Test public void testMixedObjectArray() throws Exception { + final Object converted = getConverted("objects_array_mixed", null); + assertTrue(converted.getClass().isArray()); + final String[] vals = (String[])converted; + assertEquals(3, vals.length); + assertTrue(vals[0] instanceof String); + assertTrue(vals[1] instanceof String); + assertTrue(vals[2] instanceof String); + assertEquals("2", vals[0]); + assertEquals("{\"foo\":1}", vals[1]); + assertEquals("{\"foo\":2}", vals[2]); + } +} diff --git a/configurator/src/test/java/org/apache/felix/configurator/impl/model/BundleStateTest.java b/configurator/src/test/java/org/apache/felix/configurator/impl/model/BundleStateTest.java new file mode 100644 index 00000000000..aeed67bb8f8 --- /dev/null +++ b/configurator/src/test/java/org/apache/felix/configurator/impl/model/BundleStateTest.java @@ -0,0 +1,65 @@ +/* + * 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.configurator.impl.model; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.junit.Test; + +public class BundleStateTest { + + @Test public void testReadWrite() throws Exception { + final BundleState state = new BundleState(); + + final Config c1 = new Config("a", null, 1, 0, ConfigPolicy.DEFAULT); + final Config c2 = new Config("b", null, 1, 10, ConfigPolicy.DEFAULT); + + state.add(c1); + state.add(c2); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try ( final ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(state); + } + + try ( final ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) { + final BundleState s = (BundleState) ois.readObject(); + + assertEquals(1, s.getConfigurations("a").size()); + assertEquals(1, s.getConfigurations("b").size()); + } + } + + @Test public void testDifferentPids() { + final BundleState state = new BundleState(); + final Config c1 = new Config("a", null, 1, 0, ConfigPolicy.DEFAULT); + final Config c2 = new Config("b", null, 1, 10, ConfigPolicy.DEFAULT); + + state.add(c1); + state.add(c2); + + assertEquals(1, state.getConfigurations("a").size()); + assertEquals(1, state.getConfigurations("b").size()); + } +} diff --git a/configurator/src/test/java/org/apache/felix/configurator/impl/model/ConfigListTest.java b/configurator/src/test/java/org/apache/felix/configurator/impl/model/ConfigListTest.java new file mode 100644 index 00000000000..af21b90f0b5 --- /dev/null +++ b/configurator/src/test/java/org/apache/felix/configurator/impl/model/ConfigListTest.java @@ -0,0 +1,94 @@ +/* + * 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.configurator.impl.model; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Iterator; + +import org.junit.Test; + +public class ConfigListTest { + + @Test public void testReadWrite() throws Exception { + final ConfigList list = new ConfigList(); + + final Config c1 = new Config("a", + null, 10, 0, ConfigPolicy.DEFAULT); + final Config c2 = new Config("a", + null, 10, 50, ConfigPolicy.DEFAULT); + list.add(c1); + list.add(c2); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try ( final ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(list); + } + + try ( final ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) { + final ConfigList l = (ConfigList) ois.readObject(); + + assertEquals(2, l.size()); + } + } + + @Test public void testRanking() { + final ConfigList list = new ConfigList(); + final Config c1 = new Config("a", null, 1, 0, ConfigPolicy.DEFAULT); + final Config c2 = new Config("a", null, 1, 10, ConfigPolicy.DEFAULT); + final Config c3 = new Config("a", null, 1, 0, ConfigPolicy.DEFAULT); + final Config c4 = new Config("a", null, 1, 50, ConfigPolicy.DEFAULT); + final Config c5 = new Config("a", null, 1, 20, ConfigPolicy.DEFAULT); + final Config c6 = new Config("a", null, 1, 10, ConfigPolicy.DEFAULT); + + list.add(c1); + list.add(c2); + list.add(c3); + list.add(c4); + list.add(c5); + list.add(c6); + + assertEquals(6, list.size()); + final Iterator iter = list.iterator(); + assertEquals(c4, iter.next()); + assertEquals(c5, iter.next()); + assertEquals(c2, iter.next()); + assertEquals(c6, iter.next()); + assertEquals(c1, iter.next()); + assertEquals(c3, iter.next()); + } + + @Test public void testDifferentBundleIds() { + final ConfigList list = new ConfigList(); + final Config c1 = new Config("a", null, 2, 10, ConfigPolicy.DEFAULT); + final Config c2 = new Config("a", null, 1, 10, ConfigPolicy.DEFAULT); + + list.add(c1); + list.add(c2); + + assertEquals(2, list.size()); + final Iterator iter = list.iterator(); + assertEquals(c2, iter.next()); + assertEquals(c1, iter.next()); + } +} diff --git a/configurator/src/test/java/org/apache/felix/configurator/impl/model/ConfigTest.java b/configurator/src/test/java/org/apache/felix/configurator/impl/model/ConfigTest.java new file mode 100644 index 00000000000..a974c0866a5 --- /dev/null +++ b/configurator/src/test/java/org/apache/felix/configurator/impl/model/ConfigTest.java @@ -0,0 +1,65 @@ +/* + * 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.configurator.impl.model; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.junit.Test; + +public class ConfigTest { + + @Test public void testReadWrite() throws Exception { + final Dictionary props = new Hashtable<>(); + props.put("x", "1"); + props.put("y", 1L); + + final Config cfg = new Config("a", + props, 10, 50, ConfigPolicy.DEFAULT); + cfg.setIndex(70); + cfg.setState(ConfigState.UNINSTALL); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try ( final ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(cfg); + } + + try ( final ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) { + final Config c = (Config) ois.readObject(); + + assertEquals("a", c.getPid()); + + assertEquals(10, c.getBundleId()); + assertEquals(50, c.getRanking()); + assertEquals(70, c.getIndex()); + assertEquals(ConfigState.UNINSTALL, c.getState()); + assertEquals(ConfigPolicy.DEFAULT, c.getPolicy()); + + assertEquals(2, c.getProperties().size()); + assertEquals("1", c.getProperties().get("x")); + assertEquals(1L, c.getProperties().get("y")); + } + } +} diff --git a/configurator/src/test/java/org/apache/felix/configurator/impl/model/StateTest.java b/configurator/src/test/java/org/apache/felix/configurator/impl/model/StateTest.java new file mode 100644 index 00000000000..a943d942496 --- /dev/null +++ b/configurator/src/test/java/org/apache/felix/configurator/impl/model/StateTest.java @@ -0,0 +1,71 @@ +/* + * 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.configurator.impl.model; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.junit.Test; + +public class StateTest { + + @Test public void testReadWrite() throws Exception { + final State state = new State(); + + final Config c1 = new Config("a", null, 1, 0, ConfigPolicy.DEFAULT); + final Config c2 = new Config("b", null, 1, 10, ConfigPolicy.DEFAULT); + + state.add(c1); + state.add(c2); + + state.setLastModified(1, 5); + state.setLastModified(2, 15); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try ( final ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(state); + } + + try ( final ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) { + final State s = (State) ois.readObject(); + + assertEquals(1, s.getConfigurations("a").size()); + assertEquals(1, s.getConfigurations("b").size()); + + assertEquals(5L, (Object)s.getLastModified(1)); + assertEquals(15L, (Object)s.getLastModified(2)); + } + } + + @Test public void testDifferentPids() { + final State state = new State(); + final Config c1 = new Config("a", null, 1, 0, ConfigPolicy.DEFAULT); + final Config c2 = new Config("b", null, 1, 10, ConfigPolicy.DEFAULT); + + state.add(c1); + state.add(c2); + + assertEquals(1, state.getConfigurations("a").size()); + assertEquals(1, state.getConfigurations("b").size()); + } +} diff --git a/configurator/src/test/resources/bundles/1.json b/configurator/src/test/resources/bundles/1.json new file mode 100644 index 00000000000..6f56293a07b --- /dev/null +++ b/configurator/src/test/resources/bundles/1.json @@ -0,0 +1,8 @@ +{ + "a" : { + "foo" : "bar" + }, + "b" : { + "x" : "y" + } +} diff --git a/configurator/src/test/resources/bundles/2.json b/configurator/src/test/resources/bundles/2.json new file mode 100644 index 00000000000..869b0040ae1 --- /dev/null +++ b/configurator/src/test/resources/bundles/2.json @@ -0,0 +1,11 @@ +{ + "a" : { + "foo" : "bar2" + }, + "b" : { + "x" : "y2" + }, + "c" : { + "c" : "1" + } +} \ No newline at end of file diff --git a/configurator/src/test/resources/bundles/2a.json b/configurator/src/test/resources/bundles/2a.json new file mode 100644 index 00000000000..462556444c5 --- /dev/null +++ b/configurator/src/test/resources/bundles/2a.json @@ -0,0 +1,13 @@ +{ + "a" : { + ":configurator:policy" : "force", + "foo" : "bar3" + }, + "b" : { + "x" : "y3" + }, + "c" : { + "c" : "2" + } + +} \ No newline at end of file diff --git a/configurator/src/test/resources/json/complex-types.json b/configurator/src/test/resources/json/complex-types.json new file mode 100644 index 00000000000..c5ab133b48d --- /dev/null +++ b/configurator/src/test/resources/json/complex-types.json @@ -0,0 +1,16 @@ +{ + "config" : { + "untyped" : [1,2,3], + "untyped_mixed" : [1,"two",3], + "empty" : [], + "objects_array" : [ + {"foo":1}, + {"foo":2} + ], + "objects_array_mixed" : [ + 2, + {"foo":1}, + {"foo":2} + ] + } +} diff --git a/configurator/src/test/resources/json/simple-types.json b/configurator/src/test/resources/json/simple-types.json new file mode 100644 index 00000000000..4b0cdee5554 --- /dev/null +++ b/configurator/src/test/resources/json/simple-types.json @@ -0,0 +1,12 @@ +{ + "config" : { + "string" : "bar", + "boolean" : true, + "number": 17, + "float": 5.0, + "string.array" : ["a", "b"], + "boolean.array" : [true, false], + "number.array" : [3,4], + "float.array" : [1.0,2.0] + } +} diff --git a/configurator/src/test/resources/json/valid.json b/configurator/src/test/resources/json/valid.json new file mode 100644 index 00000000000..6f56293a07b --- /dev/null +++ b/configurator/src/test/resources/json/valid.json @@ -0,0 +1,8 @@ +{ + "a" : { + "foo" : "bar" + }, + "b" : { + "x" : "y" + } +} diff --git a/connect/DEPENDENCIES b/connect/DEPENDENCIES new file mode 100644 index 00000000000..7aa354c4ba9 --- /dev/null +++ b/connect/DEPENDENCIES @@ -0,0 +1,28 @@ +Apache Felix Connect + +I. Included Third-Party Software + +This product includes software developed at +The Apache Software Foundation +(http://www.apache.org) +Copyright (c) Apache Software Foundation +Licensded under the Apache License 2.0 + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +This product uses software developed at +The Codehaus (http://www.codehaus.org) +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 diff --git a/org.osgi.core/src/main/resources/LICENSE b/connect/LICENSE similarity index 100% rename from org.osgi.core/src/main/resources/LICENSE rename to connect/LICENSE diff --git a/connect/NOTICE b/connect/NOTICE new file mode 100644 index 00000000000..1973983a1b2 --- /dev/null +++ b/connect/NOTICE @@ -0,0 +1,28 @@ +Apache Felix Connect +Copyright 2017-2018 The Apache Software Foundation + + +I. Included Software + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +II. Used Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +This product uses software developed at +The Codehaus (http://www.codehaus.org) +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 diff --git a/connect/pom.xml b/connect/pom.xml new file mode 100644 index 00000000000..ec03220d3f0 --- /dev/null +++ b/connect/pom.xml @@ -0,0 +1,206 @@ + + + + org.apache.felix + felix-parent + 5 + ../pom/pom.xml + + 4.0.0 + bundle + Apache Felix Connect + org.apache.felix.connect + 0.2.1-SNAPSHOT + A service registry that enables OSGi style service registry programs without using an OSGi framework. + + http://felix.apache.org/ + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/connect + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/connect + http://svn.apache.org/repos/asf/felix/connect + + + + karlpauls + Karl Pauls + karlpauls@gmail.com + + + gnodet + Guillaume Nodet + gnodet@gmail.com + + + + + org.osgi + osgi.core + 6.0.0 + provided + + + org.osgi + org.osgi.compendium + 5.0.0 + provided + + + org.jboss + jboss-vfs + 3.2.11.Final + provided + + + + + + + + org.apache.rat + apache-rat-plugin + + + verify + + check + + + + + + src/** + + + src/main/resources/META-INF/services/org.apache.felix.connect.launch.PojoServiceRegistryFactory + src/main/resources/META-INF/services/org.osgi.framework.launch.FrameworkFactory + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + org.codehaus.mojo + animal-sniffer-maven-plugin + 1.5 + + + check-java-version + verify + + check + + + + org.codehaus.mojo.signature + java15 + 1.0 + + + + + + + org.apache.maven.plugins + maven-source-plugin + 2.1.2 + + + attach-sources + verify + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.8 + + false + + + + attach-javadoc + verify + + jar + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.4 + true + + + org.apache.felix.connect + Pojo Service Registry + Apache Software Foundation + + org.osgi.framework.*, + org.osgi.resource, + org.osgi.service.url, + org.osgi.service.packageadmin, + org.osgi.service.startlevel, + org.osgi.util.tracker, + org.apache.felix.connect.* + + !* + + META-INF/LICENSE=LICENSE,META-INF/NOTICE=NOTICE,META-INF/DEPENDENCIES=DEPENDENCIES,{src/main/resources/} + + org.apache.felix.connect.PojoSR + + + + + + + src/main/resources + true + + + . + META-INF + + LICENSE* + NOTICE* + DEPENDENCIES* + + + + + diff --git a/connect/src/main/java/org/apache/felix/connect/BundleAware.java b/connect/src/main/java/org/apache/felix/connect/BundleAware.java new file mode 100644 index 00000000000..98c1ccb91c6 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/BundleAware.java @@ -0,0 +1,28 @@ +/* + * 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.connect; + +import org.osgi.framework.Bundle; + +public interface BundleAware +{ + + void setBundle(Bundle bundle); + +} diff --git a/connect/src/main/java/org/apache/felix/connect/DirRevision.java b/connect/src/main/java/org/apache/felix/connect/DirRevision.java new file mode 100644 index 00000000000..5b49c7de5a5 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/DirRevision.java @@ -0,0 +1,69 @@ +/* + * 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.connect; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Enumeration; + +class DirRevision implements Revision +{ + private final File m_file; + + public DirRevision(File file) + { + m_file = file; + } + + @Override + public long getLastModified() + { + return m_file.lastModified(); + } + + @Override + public Enumeration getEntries() + { + return new FileEntriesEnumeration(m_file); + } + + @Override + public URL getEntry(String entryName) + { + try + { + if (entryName != null) + { + File file = (new File(m_file, (entryName.startsWith("/")) ? entryName.substring(1) : entryName)); + if (file.exists()) + { + return file.toURI().toURL(); + } + } + } + catch (MalformedURLException e) + { + e.printStackTrace(); + } + return null; + + } + +} diff --git a/connect/src/main/java/org/apache/felix/connect/EntriesEnumeration.java b/connect/src/main/java/org/apache/felix/connect/EntriesEnumeration.java new file mode 100644 index 00000000000..d6b753ac404 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/EntriesEnumeration.java @@ -0,0 +1,80 @@ +/* + * 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.connect; + +import java.util.Enumeration; +import java.util.NoSuchElementException; +import java.util.zip.ZipEntry; + +class EntriesEnumeration implements Enumeration +{ + private final Enumeration m_enumeration; + private final String m_prefix; + private volatile String current; + + public EntriesEnumeration(Enumeration enumeration) + { + this(enumeration, null); + } + + public EntriesEnumeration(Enumeration enumeration, String prefix) + { + m_enumeration = enumeration; + m_prefix = prefix; + } + + public boolean hasMoreElements() + { + while ((current == null) && m_enumeration.hasMoreElements()) + { + String result = m_enumeration.nextElement().getName(); + if (m_prefix != null) + { + if (result.startsWith(m_prefix)) + { + current = result.substring(m_prefix.length()); + } + } + else + { + current = result; + } + } + return (current != null); + } + + public String nextElement() + { + try + { + if (hasMoreElements()) + { + return current; + } + else + { + throw new NoSuchElementException(); + } + } + finally + { + current = null; + } + } +} \ No newline at end of file diff --git a/connect/src/main/java/org/apache/felix/connect/EntryFilterEnumeration.java b/connect/src/main/java/org/apache/felix/connect/EntryFilterEnumeration.java new file mode 100644 index 00000000000..01c9d4daea6 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/EntryFilterEnumeration.java @@ -0,0 +1,237 @@ +/* + * 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.connect; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; + +import org.apache.felix.connect.felix.framework.capabilityset.SimpleFilter; + +class EntryFilterEnumeration implements Enumeration +{ + private final Enumeration m_enumeration; + private final Revision m_revision; + private final String m_path; + private final List m_filePattern; + private final boolean m_recurse; + private final boolean m_isURLValues; + private final Set m_dirEntries = new HashSet(); + private final List m_nextEntries = new ArrayList(2); + + public EntryFilterEnumeration( + Revision rev, + boolean includeFragments, + String path, + String filePattern, + boolean recurse, + boolean isURLValues) + { + m_revision = rev; + m_enumeration = rev.getEntries(); + m_recurse = recurse; + m_isURLValues = isURLValues; + + // Sanity check the parameters. + if (path == null) + { + throw new IllegalArgumentException("The path for findEntries() cannot be null."); + } + // Strip leading '/' if present. + if ((path.length() > 0) && (path.charAt(0) == '/')) + { + path = path.substring(1); + } + // Add a '/' to the end if not present. + if ((path.length() > 0) && (path.charAt(path.length() - 1) != '/')) + { + path = path + "/"; + } + m_path = path; + + // File pattern defaults to "*" if not specified. + filePattern = (filePattern == null) ? "*" : filePattern; + + m_filePattern = SimpleFilter.parseSubstring(filePattern); + + findNext(); + } + + public synchronized boolean hasMoreElements() + { + return (m_nextEntries.size() != 0); + } + + public synchronized T nextElement() + { + if (m_nextEntries.size() == 0) + { + throw new NoSuchElementException("No more entries."); + } + T last = m_nextEntries.remove(0); + findNext(); + return last; + } + + private void findNext() + { + // This method filters the content entry enumeration, such that + // it only displays the contents of the directory specified by + // the path argument either recursively or not; much like using + // "ls -R" or "ls" to list the contents of a directory, respectively. + if (m_enumeration == null) + { + return; + } + if (m_nextEntries.size() == 0) + { + while (m_enumeration.hasMoreElements() && m_nextEntries.size() == 0) + { + // Get the current entry to determine if it should be filtered + // or not. + String entryName = (String) m_enumeration.nextElement(); + // Check to see if the current entry is a descendent of the + // specified path. + if (!entryName.equals(m_path) && entryName.startsWith(m_path)) + { + // Cached entry URL. If we are returning URLs, we use this + // cached URL to avoid doing multiple URL lookups from a + // module + // when synthesizing directory URLs. + URL entryURL = null; + + // If the current entry is in a subdirectory of the + // specified path, + // get the index of the slash character. + int dirSlashIdx = entryName.indexOf('/', m_path.length()); + + // JAR files are supposed to contain entries for + // directories, + // but not all do. So calculate the directory for this entry + // and see if we've already seen an entry for the directory. + // If not, synthesize an entry for the directory. If we are + // doing a recursive match, we need to synthesize each + // matching + // subdirectory of the entry. + if (dirSlashIdx >= 0) + { + // Start synthesizing directories for the current entry + // at the subdirectory after the initial path. + int subDirSlashIdx = dirSlashIdx; + String dir; + do + { + // Calculate the subdirectory name. + dir = entryName.substring(0, subDirSlashIdx + 1); + // If we have not seen this directory before, then + // record + // it and potentially synthesize an entry for it. + if (!m_dirEntries.contains(dir)) + { + // Record it. + m_dirEntries.add(dir); + // If the entry is actually a directory entry + // (i.e., + // it ends with a slash), then we don't need to + // synthesize an entry since it exists; + // otherwise, + // synthesize an entry if it matches the file + // pattern. + if (entryName.length() != (subDirSlashIdx + 1)) + { + // See if the file pattern matches the last + // element of the path. + if (SimpleFilter.compareSubstring( + m_filePattern, + getLastPathElement(dir))) + { + if (m_isURLValues) + { + entryURL = (entryURL == null) ? m_revision + .getEntry(entryName) + : entryURL; + try + { + m_nextEntries.add((T) new URL(entryURL, "/" + dir)); + } + catch (MalformedURLException ex) + { + } + } + else + { + m_nextEntries.add((T) dir); + } + } + } + } + // Now prepare to synthesize the next subdirectory + // if we are matching recursively. + subDirSlashIdx = entryName.indexOf('/', + dir.length()); + } + while (m_recurse && (subDirSlashIdx >= 0)); + } + + // Now we actually need to check if the current entry itself + // should + // be filtered or not. If we are recursive or the current + // entry + // is a child (not a grandchild) of the initial path, then + // we need + // to check if it matches the file pattern. + if (m_recurse || (dirSlashIdx < 0) || (dirSlashIdx == entryName.length() - 1)) + { + // See if the file pattern matches the last element of + // the path. + if (SimpleFilter.compareSubstring(m_filePattern, + getLastPathElement(entryName))) + { + if (m_isURLValues) + { + entryURL = (entryURL == null) ? m_revision + .getEntry(entryName) : entryURL; + m_nextEntries.add((T) entryURL); + } + else + { + m_nextEntries.add((T) entryName); + } + } + } + } + } + } + } + + private static String getLastPathElement(String entryName) + { + int endIdx = (entryName.charAt(entryName.length() - 1) == '/') ? entryName + .length() - 1 : entryName.length(); + int startIdx = (entryName.charAt(entryName.length() - 1) == '/') ? entryName + .lastIndexOf('/', endIdx - 1) + 1 : entryName.lastIndexOf('/', + endIdx) + 1; + return entryName.substring(startIdx, endIdx); + } +} \ No newline at end of file diff --git a/connect/src/main/java/org/apache/felix/connect/ExportedPackageImpl.java b/connect/src/main/java/org/apache/felix/connect/ExportedPackageImpl.java new file mode 100644 index 00000000000..f7698494478 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/ExportedPackageImpl.java @@ -0,0 +1,90 @@ +/* + * 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.connect; + +import java.util.HashSet; +import java.util.Set; + +import org.osgi.framework.Bundle; +import org.osgi.framework.Version; +import org.osgi.framework.namespace.BundleNamespace; +import org.osgi.framework.namespace.PackageNamespace; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.service.packageadmin.ExportedPackage; + +class ExportedPackageImpl implements ExportedPackage +{ + private final BundleCapability m_export; + + public ExportedPackageImpl(BundleCapability export) + { + m_export = export; + } + + public Bundle getExportingBundle() + { + return m_export.getRevision().getBundle(); + } + + public Bundle[] getImportingBundles() + { + // Create set for storing importing bundles. + Set result = new HashSet(); + // Get all importers and requirers for all revisions of the bundle. + // The spec says that require-bundle should be returned with importers. + for (BundleWire wire : m_export.getRevision().getWiring().getProvidedWires(null)) + { + if (wire.getCapability() == m_export + || BundleNamespace.BUNDLE_NAMESPACE.equals(wire.getCapability().getNamespace())) + { + result.add( wire.getRequirer().getBundle() ); + } + } + // Return the results. + return result.toArray(new Bundle[result.size()]); + } + + public String getName() + { + return (String) m_export.getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE); + } + + public String getSpecificationVersion() + { + return getVersion().toString(); + } + + public Version getVersion() + { + return m_export.getAttributes().containsKey(PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE) + ? (Version) m_export.getAttributes().get(PackageNamespace.CAPABILITY_VERSION_ATTRIBUTE) + : Version.emptyVersion; + } + + public boolean isRemovalPending() + { + return false; + } + + public String toString() + { + return getName() + "; version=" + getVersion(); + } +} \ No newline at end of file diff --git a/connect/src/main/java/org/apache/felix/connect/FileEntriesEnumeration.java b/connect/src/main/java/org/apache/felix/connect/FileEntriesEnumeration.java new file mode 100644 index 00000000000..0208a37d788 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/FileEntriesEnumeration.java @@ -0,0 +1,87 @@ +/* + * 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.connect; + +import java.io.File; +import java.util.Enumeration; +import java.util.NoSuchElementException; + +class FileEntriesEnumeration implements Enumeration +{ + private final File m_dir; + private final File[] m_children; + private int m_counter = 0; + + public FileEntriesEnumeration(File dir) + { + m_dir = dir; + m_children = listFilesRecursive(m_dir); + } + + public synchronized boolean hasMoreElements() + { + return (m_children != null) && (m_counter < m_children.length); + } + + public synchronized String nextElement() + { + if ((m_children == null) || (m_counter >= m_children.length)) + { + throw new NoSuchElementException("No more entry paths."); + } + + // Convert the file separator character to slashes. + String abs = m_children[m_counter].getAbsolutePath().replace( + File.separatorChar, '/'); + + // Remove the leading path of the reference directory, since the + // entry paths are supposed to be relative to the root. + StringBuilder sb = new StringBuilder(abs); + sb.delete(0, m_dir.getAbsolutePath().length() + 1); + // Add a '/' to the end of directory entries. + if (m_children[m_counter].isDirectory()) + { + sb.append('/'); + } + m_counter++; + return sb.toString(); + } + + private File[] listFilesRecursive(File dir) + { + File[] children = dir.listFiles(); + File[] combined = children; + for (File aChildren : children) + { + if (aChildren.isDirectory()) + { + File[] grandchildren = listFilesRecursive(aChildren); + if (grandchildren.length > 0) + { + File[] tmp = new File[combined.length + grandchildren.length]; + System.arraycopy(combined, 0, tmp, 0, combined.length); + System.arraycopy(grandchildren, 0, tmp, combined.length, + grandchildren.length); + combined = tmp; + } + } + } + return combined; + } +} \ No newline at end of file diff --git a/connect/src/main/java/org/apache/felix/connect/JarRevision.java b/connect/src/main/java/org/apache/felix/connect/JarRevision.java new file mode 100644 index 00000000000..61b645582cd --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/JarRevision.java @@ -0,0 +1,136 @@ +/* + * 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.connect; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +class JarRevision implements Revision +{ + private final long m_lastModified; + private final JarFile m_jar; + private final URL m_url; + private final String m_urlString; + private final String m_prefix; + + public JarRevision(JarFile jar, URL url, String prefix, long lastModified) + { + m_jar = jar; + m_url = url; + m_urlString = m_url.toExternalForm(); + m_prefix = prefix; + if (lastModified > 0) + { + m_lastModified = lastModified; + } + else + { + m_lastModified = System.currentTimeMillis(); + } + } + + @Override + public long getLastModified() + { + return m_lastModified; + } + + public Enumeration getEntries() + { + return new EntriesEnumeration(m_jar.entries(), m_prefix); + } + + @Override + public URL getEntry(String entryName) + { + try + { + if ("/".equals(entryName) || "".equals(entryName) || " ".equals(entryName)) + { + return new URL("jar:" + m_urlString + "!/" + ((m_prefix == null) ? "" : m_prefix)); + } + if (entryName != null) + { + final String target = ((entryName.startsWith("/")) ? entryName.substring(1) : entryName); + final JarEntry entry = m_jar.getJarEntry(((m_prefix == null) ? "" : m_prefix) + target); + if (entry != null) + { + URL result = new URL(null, "jar:" + m_urlString + "!/" + ((m_prefix == null) ? "" : m_prefix) + target, new URLStreamHandler() + { + protected URLConnection openConnection(final URL u) throws IOException + { + return new java.net.JarURLConnection(u) + { + + public JarFile getJarFile() + { + return m_jar; + } + + public void connect() throws IOException + { + } + + public InputStream getInputStream() throws IOException + { + String extF = u.toExternalForm(); + JarEntry targetEntry = entry; + if (!extF.endsWith(target)) + { + extF = extF.substring(extF.indexOf('!') + 2); + if (m_prefix != null) + { + if (!extF.startsWith(m_prefix)) + { + extF = m_prefix + extF; + } + } + targetEntry = m_jar.getJarEntry(extF); + } + return m_jar.getInputStream(targetEntry); + } + }; + } + }); + return result; + } + else + { + if (entryName.endsWith("/")) + { + return new URL("jar:" + m_urlString + "!/" + ((m_prefix == null) ? "" : m_prefix) + target); + } + } + } + } + catch (IOException e) + { + e.printStackTrace(); + } + return null; + + } + +} diff --git a/connect/src/main/java/org/apache/felix/connect/PojoSR.java b/connect/src/main/java/org/apache/felix/connect/PojoSR.java new file mode 100644 index 00000000000..aabf4480af4 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/PojoSR.java @@ -0,0 +1,714 @@ +/* + * 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.connect; + +import java.io.File; +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.Version; +import org.osgi.framework.VersionRange; +import org.osgi.framework.startlevel.FrameworkStartLevel; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.service.packageadmin.ExportedPackage; +import org.osgi.service.packageadmin.PackageAdmin; +import org.osgi.service.packageadmin.RequiredBundle; +import org.osgi.service.startlevel.StartLevel; + +import org.apache.felix.connect.felix.framework.ServiceRegistry; +import org.apache.felix.connect.felix.framework.util.EventDispatcher; +import org.apache.felix.connect.launch.BundleDescriptor; +import org.apache.felix.connect.launch.ClasspathScanner; +import org.apache.felix.connect.launch.PojoServiceRegistry; +import org.apache.felix.connect.launch.PojoServiceRegistryFactory; + +public class PojoSR implements PojoServiceRegistry +{ + private final BundleContext m_context; + private final ServiceRegistry m_registry = new ServiceRegistry( + new ServiceRegistry.ServiceRegistryCallbacks() + { + public void serviceChanged(ServiceEvent event, Dictionary oldProps) + { + m_dispatcher.fireServiceEvent(event, oldProps, m_bundles.get(0l)); + } + }); + + private final EventDispatcher m_dispatcher = new EventDispatcher(m_registry); + private final Map m_bundles = new HashMap(); + private final Map bundleConfig; + private final boolean m_hasVFS; + + public static BundleDescriptor createSystemBundle() { + final Map headers = new HashMap(); + headers.put(Constants.BUNDLE_SYMBOLICNAME, "org.apache.felix.connect"); + headers.put(Constants.BUNDLE_VERSION, "0.0.0"); + headers.put(Constants.BUNDLE_NAME, "System Bundle"); + headers.put(Constants.BUNDLE_MANIFESTVERSION, "2"); + headers.put(Constants.BUNDLE_VENDOR, "Apache Software Foundation"); + + + Revision revision = new Revision() + { + final long lastModified = System.currentTimeMillis(); + @Override + public long getLastModified() + { + return lastModified; + } + + @Override + public Enumeration getEntries() + { + return Collections.enumeration(Collections.EMPTY_LIST); + } + + @Override + public URL getEntry(String entryName) + { + return getClass().getClassLoader().getResource(entryName); + } + }; + Map services = new HashMap(); + services.put(FrameworkStartLevel.class, new FrameworkStartLevelImpl()); + return new BundleDescriptor( + PojoSR.class.getClassLoader(), + "System Bundle", + headers, + revision, + services + ); + } + + public PojoSR(Map config) throws Exception + { + this(config, null); + } + + public PojoSR(Map config, BundleDescriptor systemBundle) throws Exception + { + if (systemBundle == null) { + systemBundle = createSystemBundle(); + } + bundleConfig = new HashMap(config); + final Bundle b = new PojoSRBundle( + m_registry, + m_dispatcher, + m_bundles, + systemBundle.getUrl(), + 0, + "org.apache.felix.connect", + new Version(0, 0, 1), + systemBundle.getRevision(), + systemBundle.getClassLoader(), + systemBundle.getHeaders(), + systemBundle.getServices(), + bundleConfig) + { + @Override + public synchronized void start() throws BundleException + { + if (m_state != Bundle.RESOLVED) + { + return; + } + m_dispatcher.startDispatching(); + m_state = Bundle.STARTING; + + m_dispatcher.fireBundleEvent(new BundleEvent(BundleEvent.STARTING, this)); + m_context = new PojoSRBundleContext(this, m_registry, m_dispatcher, m_bundles, bundleConfig); + int i = 0; + for (Bundle b : m_bundles.values()) + { + i++; + try + { + if (b != this) + { + b.start(); + } + } + catch (Throwable t) + { + System.out.println("Unable to start bundle: " + i); + t.printStackTrace(); + } + } + m_state = Bundle.ACTIVE; + m_dispatcher.fireBundleEvent(new BundleEvent(BundleEvent.STARTED, this)); + + m_dispatcher.fireFrameworkEvent(new FrameworkEvent(FrameworkEvent.STARTED, this, null)); + super.start(); + } + + @Override + public synchronized void stop() throws BundleException + { + if ((m_state == Bundle.STOPPING) || m_state == Bundle.RESOLVED) + { + return; + + } + else if (m_state != Bundle.ACTIVE) + { + throw new BundleException("Can't stop pojosr because it is not ACTIVE"); + } + final Bundle systemBundle = this; + Runnable r = new Runnable() + { + + public void run() + { + m_dispatcher.fireBundleEvent(new BundleEvent(BundleEvent.STOPPING, systemBundle)); + for (Bundle b : m_bundles.values()) + { + try + { + if (b != systemBundle) + { + b.stop(); + } + } + catch (Throwable t) + { + t.printStackTrace(); + } + } + m_dispatcher.fireBundleEvent(new BundleEvent(BundleEvent.STOPPED, systemBundle)); + m_state = Bundle.RESOLVED; + m_dispatcher.stopDispatching(); + } + }; + m_state = Bundle.STOPPING; + if ("true".equalsIgnoreCase(System.getProperty("org.apache.felix.connect.events.sync"))) + { + r.run(); + } + else + { + new Thread(r).start(); + } + } + }; + m_bundles.put(0l, b); + b.start(); + b.getBundleContext().registerService(StartLevel.class.getName(), new StartLevelImpl(), null); + + b.getBundleContext().registerService(PackageAdmin.class.getName(), new PackageAdminImpl(), null); + m_context = b.getBundleContext(); + + boolean hasVFS; + try + { + hasVFS = org.jboss.vfs.VFS.class != null; + } catch (Throwable t) { + hasVFS = false; + } + m_hasVFS = hasVFS; + + Collection scan = (Collection) config.get(PojoServiceRegistryFactory.BUNDLE_DESCRIPTORS); + + if (scan != null) + { + Object autoStart = config.get(PojoServiceRegistryFactory.BUNDLES_AUTOSTART); + if(autoStart == null || Boolean.TRUE.equals(autoStart)) + { + startBundles(scan); + } + else + { + for (BundleDescriptor desc : scan) { + registerBundle(desc); + } + } + } + } + + public void startBundles(Collection scan) throws Exception { + List bundles = new LinkedList(); + + for (BundleDescriptor desc : scan) + { + Bundle bundle = registerBundle(desc); + bundles.add(bundle); + } + + for (Bundle bundle : bundles) + { + try + { + bundle.start(); + } + catch (Throwable e) + { + System.out.println("Unable to start bundle: " + bundle); + e.printStackTrace(); + } + } + } + + public Bundle registerBundle(BundleDescriptor desc) throws Exception + { + Revision revision = desc.getRevision(); + if (revision == null) + { + revision = buildRevision(desc); + } + Map bundleHeaders = desc.getHeaders(); + Version osgiVersion; + try + { + osgiVersion = Version.parseVersion(bundleHeaders.get(Constants.BUNDLE_VERSION)); + } + catch (Exception ex) + { + ex.printStackTrace(); + osgiVersion = Version.emptyVersion; + } + String sym = bundleHeaders.get(Constants.BUNDLE_SYMBOLICNAME); + if (sym != null) + { + int idx = sym.indexOf(';'); + if (idx > 0) + { + sym = sym.substring(0, idx); + } + sym = sym.trim(); + } + + Bundle bundle = new PojoSRBundle( + m_registry, + m_dispatcher, + m_bundles, + desc.getUrl(), + m_bundles.size(), + sym, + osgiVersion, + revision, + desc.getClassLoader(), + bundleHeaders, + desc.getServices(), + bundleConfig); + + m_bundles.put(bundle.getBundleId(), bundle); + + return bundle; + } + + private Revision buildRevision(BundleDescriptor desc) throws IOException + { + Revision r; + URL url = new URL(desc.getUrl()); + URL u = new URL(desc.getUrl() + "META-INF/MANIFEST.MF"); + String extF = u.toExternalForm(); + if (extF.startsWith("file:")) + { + File root = new File(URLDecoder.decode(url.getFile(), "UTF-8")); + r = new DirRevision(root); + } + else + { + URLConnection uc = u.openConnection(); + if (uc instanceof JarURLConnection) + { + String target = ((JarURLConnection) uc).getJarFileURL().toExternalForm(); + String prefix = null; + if (!("jar:" + target + "!/").equals(desc.getUrl()) && desc.getUrl().startsWith("jar:" + target + "!/")) + { + System.out.println(desc.getUrl() + " " + target); + prefix = desc.getUrl().substring(("jar:" + target + "!/").length()); + } + r = new JarRevision( + ((JarURLConnection) uc).getJarFile(), + ((JarURLConnection) uc).getJarFileURL(), + prefix, + uc.getLastModified()); + } + else if (m_hasVFS && extF.startsWith("vfs")) + { + r = new VFSRevision(url, url.openConnection().getLastModified()); + } + else + { + r = new URLRevision(url, url.openConnection().getLastModified()); + } + } + return r; + } + + public static void main(String[] args) throws Exception + { + Filter filter = null; + Class main = null; + for (int i = 0; (args != null) && (i < args.length) && (i < 2); i++) + { + try + { + filter = FrameworkUtil.createFilter(args[i]); + } + catch (InvalidSyntaxException ie) + { + try + { + main = PojoSR.class.getClassLoader().loadClass(args[i]); + } + catch (Exception ex) + { + throw new IllegalArgumentException("Argument is neither a filter nor a class: " + args[i]); + } + } + } + Map config = new HashMap(); + config.put( + PojoServiceRegistryFactory.BUNDLE_DESCRIPTORS, + (filter != null) ? new ClasspathScanner() + .scanForBundles(filter.toString()) : new ClasspathScanner() + .scanForBundles()); + new PojoServiceRegistryFactoryImpl().newPojoServiceRegistry(config); + if (main != null) + { + int count = 0; + if (filter != null) + { + count++; + } + count++; + String[] newArgs = args; + if (count > 0) + { + newArgs = new String[args.length - count]; + System.arraycopy(args, count, newArgs, 0, newArgs.length); + } + main.getMethod("main", String[].class).invoke(null, newArgs); + } + } + + public BundleContext getBundleContext() + { + return m_context; + } + + @Override + public void addServiceListener(ServiceListener listener, String filter) throws InvalidSyntaxException + { + m_context.addServiceListener(listener, filter); + } + + @Override + public void addServiceListener(ServiceListener listener) + { + m_context.addServiceListener(listener); + } + + @Override + public void removeServiceListener(ServiceListener listener) + { + m_context.removeServiceListener(listener); + } + + @Override + public ServiceRegistration registerService(String[] clazzes, Object service, Dictionary properties) + { + return m_context.registerService(clazzes, service, properties); + } + + @Override + public ServiceRegistration registerService(String clazz, Object service, Dictionary properties) + { + return m_context.registerService(clazz, service, properties); + } + + @Override + public ServiceReference[] getServiceReferences(String clazz, String filter) throws InvalidSyntaxException + { + return m_context.getServiceReferences(clazz, filter); + } + + @Override + public ServiceReference getServiceReference(String clazz) + { + return m_context.getServiceReference(clazz); + } + + @Override + public S getService(ServiceReference reference) + { + return m_context.getService(reference); + } + + @Override + public boolean ungetService(ServiceReference reference) + { + return m_context.ungetService(reference); + } + + private static class FrameworkStartLevelImpl implements FrameworkStartLevel, BundleAware + { + + private Bundle bundle; + + @Override + public void setBundle(Bundle bundle) + { + this.bundle = bundle; + } + + @Override + public int getStartLevel() + { + return 0; + } + + @Override + public void setStartLevel(int startlevel, FrameworkListener... listeners) + { + } + + @Override + public int getInitialBundleStartLevel() + { + return 0; + } + + @Override + public void setInitialBundleStartLevel(int startlevel) + { + } + + @Override + public Bundle getBundle() + { + return bundle; + } + } + + private static class StartLevelImpl implements StartLevel + { + @Override + public void setStartLevel(int startlevel) + { + // TODO Auto-generated method stub + } + + @Override + public void setInitialBundleStartLevel(int startlevel) + { + // TODO Auto-generated method stub + } + + @Override + public void setBundleStartLevel(Bundle bundle, int startlevel) + { + // TODO Auto-generated method stub + } + + @Override + public boolean isBundlePersistentlyStarted(Bundle bundle) + { + // TODO Auto-generated method stub + return true; + } + + @Override + public boolean isBundleActivationPolicyUsed(Bundle bundle) + { + // TODO Auto-generated method stub + return false; + } + + @Override + public int getStartLevel() + { + // TODO Auto-generated method stub + return 1; + } + + @Override + public int getInitialBundleStartLevel() + { + // TODO Auto-generated method stub + return 1; + } + + @Override + public int getBundleStartLevel(Bundle bundle) + { + // TODO Auto-generated method stub + return 1; + } + } + + private class PackageAdminImpl implements PackageAdmin + { + + @Override + public boolean resolveBundles(Bundle[] bundles) + { + return true; + } + + @Override + public void refreshPackages(Bundle[] bundles) + { + FrameworkEvent event = new FrameworkEvent(FrameworkEvent.PACKAGES_REFRESHED, m_bundles.get(0l), null); + m_dispatcher.fireFrameworkEvent(event); + } + + @Override + public RequiredBundle[] getRequiredBundles(String symbolicName) + { + List list = new ArrayList(); + for (Bundle bundle : PojoSR.this.m_bundles.values()) + { + if ((symbolicName == null) || (symbolicName.equals(bundle.getSymbolicName()))) + { + list.add(new RequiredBundleImpl(bundle)); + } + } + return (list.isEmpty()) + ? null + : (RequiredBundle[]) list.toArray(new RequiredBundle[list.size()]); + } + + @Override + public Bundle[] getHosts(Bundle bundle) + { + // TODO Auto-generated method stub + return null; + } + + @Override + public Bundle[] getFragments(Bundle bundle) + { + // TODO Auto-generated method stub + return null; + } + + @Override + public ExportedPackage[] getExportedPackages(String name) + { + // TODO Auto-generated method stub + return null; + } + + @Override + public ExportedPackage[] getExportedPackages(Bundle bundle) + { + List list = new ArrayList(); + // If a bundle is specified, then return its + // exported packages. + if (bundle != null) + { + getExportedPackages(bundle, list); + } + // Otherwise return all exported packages. + else + { + for (Bundle b : m_bundles.values()) + { + getExportedPackages(b, list); + } + } + return list.isEmpty() ? null : list.toArray(new ExportedPackage[list.size()]); + } + + private void getExportedPackages(Bundle bundle, List list) + { + // Since a bundle may have many revisions associated with it, + // one for each revision in the cache, search each revision + // to get all exports. + for (BundleCapability cap : bundle.adapt(BundleWiring.class).getCapabilities(null)) + { + if (cap.getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE)) + { + list.add(new ExportedPackageImpl(cap)); + } + } + } + + @Override + public ExportedPackage getExportedPackage(String name) + { + // TODO Auto-generated method stub + return null; + } + + @Override + public Bundle[] getBundles(String symbolicName, String versionRange) + { + Set result = new HashSet(); + VersionRange range = versionRange != null ? new VersionRange(versionRange) : null; + for (Bundle bundle : m_bundles.values()) + { + if (symbolicName != null && !bundle.getSymbolicName().equals(symbolicName)) + { + continue; + } + if (range != null && !range.includes(bundle.getVersion())) + { + continue; + } + result.add(bundle); + } + return result.isEmpty() ? null : result.toArray(new Bundle[result.size()]); + } + + @Override + public int getBundleType(Bundle bundle) + { + return bundle.adapt(BundleRevision.class).getTypes(); + } + + @Override + public Bundle getBundle(Class clazz) + { + return m_context.getBundle(); + } + } +} diff --git a/connect/src/main/java/org/apache/felix/connect/PojoSRBundle.java b/connect/src/main/java/org/apache/felix/connect/PojoSRBundle.java new file mode 100644 index 00000000000..df2632973a2 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/PojoSRBundle.java @@ -0,0 +1,805 @@ +/* + * 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.connect; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.StringTokenizer; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.Version; +import org.osgi.framework.startlevel.BundleStartLevel; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleRevisions; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Wire; + +import org.apache.felix.connect.felix.framework.ServiceRegistry; +import org.apache.felix.connect.felix.framework.util.EventDispatcher; +import org.apache.felix.connect.felix.framework.util.MapToDictionary; +import org.apache.felix.connect.felix.framework.util.StringMap; + +class PojoSRBundle implements Bundle, BundleRevisions +{ + private final Revision m_revision; + private final Map m_headers; + private final Version m_version; + private final String m_location; + private final Map m_bundles; + private final ServiceRegistry m_registry; + private final String m_activatorClass; + private final long m_id; + private final String m_symbolicName; + private volatile BundleActivator m_activator = null; + volatile int m_state = Bundle.RESOLVED; + volatile BundleContext m_context = null; + private final EventDispatcher m_dispatcher; + private final ClassLoader m_classLoader; + private final Map m_services; + private final Map m_config; + + public PojoSRBundle(ServiceRegistry registry, + EventDispatcher dispatcher, + Map bundles, + String location, + long id, + String symbolicName, + Version version, + Revision revision, + ClassLoader classLoader, + Map headers, + Map services, + Map config) + { + m_revision = revision; + m_headers = headers; + m_version = version; + m_location = location; + m_registry = registry; + m_dispatcher = dispatcher; + m_activatorClass = headers.get(Constants.BUNDLE_ACTIVATOR); + m_id = id; + m_symbolicName = symbolicName; + m_bundles = bundles; + m_classLoader = classLoader; + m_services = services; + m_config = config; + if (classLoader instanceof BundleAware) { + ((BundleAware) classLoader).setBundle(this); + } + if (services != null) { + for (Object o : services.values()) { + if (o instanceof BundleAware) { + ((BundleAware) o).setBundle(this); + } + } + } + } + + @Override + public int getState() + { + return m_state; + } + + @Override + public void start(int options) throws BundleException + { + // TODO: lifecycle - fix this + start(); + } + + @Override + public synchronized void start() throws BundleException + { + if (m_state != Bundle.RESOLVED) + { + if (m_state == Bundle.ACTIVE) + { + return; + } + throw new BundleException("Bundle is in wrong state for start"); + } + try + { + m_state = Bundle.STARTING; + + m_context = new PojoSRBundleContext(this, m_registry, m_dispatcher, m_bundles, m_config); + m_dispatcher.fireBundleEvent(new BundleEvent(BundleEvent.STARTING, this)); + if (m_activatorClass != null) + { + m_activator = (BundleActivator) m_classLoader.loadClass(m_activatorClass).newInstance(); + m_activator.start(m_context); + } + m_state = Bundle.ACTIVE; + m_dispatcher.fireBundleEvent(new BundleEvent(BundleEvent.STARTED, this)); + } + catch (Throwable ex) + { + m_state = Bundle.RESOLVED; + m_activator = null; + m_dispatcher.fireBundleEvent(new BundleEvent(BundleEvent.STOPPED, this)); + throw new BundleException("Unable to start bundle", ex); + } + } + + @Override + public void stop(int options) throws BundleException + { + // TODO: lifecycle - fix this + stop(); + } + + @Override + public synchronized void stop() throws BundleException + { + if (m_state != Bundle.ACTIVE) + { + if (m_state == Bundle.RESOLVED) + { + return; + } + throw new BundleException("Bundle is in wrong state for stop"); + } + try + { + m_state = Bundle.STOPPING; + m_dispatcher.fireBundleEvent(new BundleEvent(BundleEvent.STOPPING, + this)); + if (m_activator != null) + { + m_activator.stop(m_context); + } + } + catch (Throwable ex) + { + throw new BundleException("Error while stopping bundle", ex); + } + finally + { + m_registry.unregisterServices(this); + m_dispatcher.removeListeners(m_context); + m_activator = null; + m_context = null; + m_state = Bundle.RESOLVED; + m_dispatcher.fireBundleEvent(new BundleEvent(BundleEvent.STOPPED, + this)); + } + } + + @Override + public void update(InputStream input) throws BundleException + { + throw new BundleException("pojosr bundles can't be updated"); + } + + @Override + public void update() throws BundleException + { + throw new BundleException("pojosr bundles can't be updated"); + } + + @Override + public void uninstall() throws BundleException + { + throw new BundleException("pojosr bundles can't be uninstalled"); + } + + @Override + public Dictionary getHeaders() + { + return getHeaders(Locale.getDefault().toString()); + } + + @Override + public long getBundleId() + { + return m_id; + } + + @Override + public String getLocation() + { + return m_location; + } + + @Override + public ServiceReference[] getRegisteredServices() + { + return m_registry.getRegisteredServices(this); + } + + @Override + public ServiceReference[] getServicesInUse() + { + return m_registry.getServicesInUse(this); + } + + @Override + public boolean hasPermission(Object permission) + { + // TODO: security - fix this + return true; + } + + @Override + public URL getResource(String name) + { + // TODO: module - implement this based on the revision + URL result = m_classLoader.getResource(name); + return result; + } + + @Override + public Dictionary getHeaders(String locale) + { + return new MapToDictionary(getCurrentLocalizedHeader(locale)); + } + + Map getCurrentLocalizedHeader(String locale) + { + Map result = null; + + // Spec says empty local returns raw headers. + if ((locale == null) || (locale.length() == 0)) + { + result = new StringMap(m_headers, false); + } + + // If we have no result, try to get it from the cached headers. + if (result == null) + { + synchronized (m_cachedHeaders) + { + // If the bundle is uninstalled, then the cached headers should + // only contain the localized headers for the default locale at + // the time of uninstall, so just return that. + if (getState() == Bundle.UNINSTALLED) + { + result = m_cachedHeaders.values().iterator().next(); + } + // If the bundle has been updated, clear the cached headers. + else if (getLastModified() > m_cachedHeadersTimestamp) + { + m_cachedHeaders.clear(); + } + // Otherwise, returned the cached headers if they exist. + else + { + // Check if headers for this locale have already been resolved + result = m_cachedHeaders.get(locale); + } + } + } + + // If the requested locale is not cached, then try to create it. + if (result == null) + { + // Get a modifiable copy of the raw headers. + Map headers = new StringMap(m_headers, false); + // Assume for now that this will be the result. + result = headers; + + // Check to see if we actually need to localize anything + boolean localize = false; + for (String s : headers.values()) + { + if ((s).startsWith("%")) + { + localize = true; + break; + } + } + + if (!localize) + { + // If localization is not needed, just cache the headers and + // return + // them as-is. Not sure if this is useful + updateHeaderCache(locale, headers); + } + else + { + // Do localization here and return the localized headers + String basename = headers.get(Constants.BUNDLE_LOCALIZATION); + if (basename == null) + { + basename = Constants.BUNDLE_LOCALIZATION_DEFAULT_BASENAME; + } + + // Create ordered list of files to load properties from + List resourceList = createLocalizationResourceList(basename, locale); + + // Create a merged props file with all available props for this + // locale + boolean found = false; + Properties mergedProperties = new Properties(); + for (String aResourceList : resourceList) + { + URL temp = m_revision.getEntry(aResourceList + ".properties"); + if (temp != null) + { + found = true; + try + { + mergedProperties.load(temp.openConnection().getInputStream()); + } + catch (IOException ex) + { + // File doesn't exist, just continue loop + } + } + } + + // If the specified locale was not found, then the spec says we + // should + // return the default localization. + if (!found && !locale.equals(Locale.getDefault().toString())) + { + result = getCurrentLocalizedHeader(Locale.getDefault().toString()); + } + // Otherwise, perform the localization based on the discovered + // properties and cache the result. + else + { + // Resolve all localized header entries + for (Map.Entry entry : headers.entrySet()) + { + String value = entry.getValue(); + if (value.startsWith("%")) + { + String newvalue; + String key = value.substring(value.indexOf("%") + 1); + newvalue = mergedProperties.getProperty(key); + if (newvalue == null) + { + newvalue = key; + } + entry.setValue(newvalue); + } + } + + updateHeaderCache(locale, headers); + } + } + } + + return result; + } + + private void updateHeaderCache(String locale, Map localizedHeaders) + { + synchronized (m_cachedHeaders) + { + m_cachedHeaders.put(locale, localizedHeaders); + m_cachedHeadersTimestamp = System.currentTimeMillis(); + } + } + + private final Map> m_cachedHeaders = new HashMap>(); + private long m_cachedHeadersTimestamp; + + private static List createLocalizationResourceList(String basename, String locale) + { + List result = new ArrayList(4); + + StringTokenizer tokens; + StringBuilder tempLocale = new StringBuilder(basename); + + result.add(tempLocale.toString()); + + if (locale.length() > 0) + { + tokens = new StringTokenizer(locale, "_"); + while (tokens.hasMoreTokens()) + { + tempLocale.append("_").append(tokens.nextToken()); + result.add(tempLocale.toString()); + } + } + return result; + } + + public String getSymbolicName() + { + return m_symbolicName; + } + + public Class loadClass(String name) throws ClassNotFoundException + { + return m_classLoader.loadClass(name); + } + + @Override + public Enumeration getResources(String name) throws IOException + { + // TODO: module - implement this based on the revision + return m_classLoader.getResources(name); + } + + @Override + public Enumeration getEntryPaths(String path) + { + return new EntryFilterEnumeration(m_revision, false, path, null, false, + false); + } + + @Override + public URL getEntry(String path) + { + URL result = m_revision.getEntry(path); + return result; + } + + @Override + public long getLastModified() + { + return m_revision.getLastModified(); + } + + @Override + public Enumeration findEntries(String path, String filePattern, boolean recurse) + { + // TODO: module - implement this based on the revision + return new EntryFilterEnumeration(m_revision, true, path, filePattern, recurse, true); + } + + @Override + public BundleContext getBundleContext() + { + return m_context; + } + + @Override + public Map> getSignerCertificates(int signersType) + { + // TODO: security - fix this + return new HashMap>(); + } + + @Override + public Version getVersion() + { + return m_version; + } + + @Override + public boolean equals(Object o) + { + if (o instanceof PojoSRBundle) + { + return ((PojoSRBundle) o).m_id == m_id; + } + return false; + } + + @Override + public int compareTo(Bundle o) + { + long thisBundleId = this.getBundleId(); + long thatBundleId = o.getBundleId(); + return (thisBundleId < thatBundleId ? -1 : (thisBundleId == thatBundleId ? 0 : 1)); + } + + @SuppressWarnings("unchecked") + public A adapt(Class type) + { + if (m_services != null && m_services.containsKey(type)) + { + return (A) m_services.get(type); + } + if (type.isInstance(this)) + { + return (A) this; + } + if (type == BundleWiring.class) + { + return (A) new BundleWiringImpl(this, m_classLoader); + } + if (type == BundleRevision.class) + { + return (A) new BundleRevisionImpl(this); + } + if (type == BundleStartLevel.class) + { + return (A) new BundleStartLevelImpl(this); + } + return null; + } + + public File getDataFile(String filename) + { + return m_context.getDataFile(filename); + } + + public String toString() + { + String sym = getSymbolicName(); + if (sym != null) + { + return sym + " [" + getBundleId() + "]"; + } + return "[" + getBundleId() + "]"; + } + + @Override + public List getRevisions() + { + return Arrays.asList(adapt(BundleRevision.class)); + } + + @Override + public Bundle getBundle() + { + return this; + } + + + public static class BundleStartLevelImpl implements BundleStartLevel + { + private final Bundle bundle; + + public BundleStartLevelImpl(Bundle bundle) + { + this.bundle = bundle; + } + + public int getStartLevel() + { + // TODO Implement this? + return 1; + } + + public void setStartLevel(int startlevel) + { + // TODO Implement this? + } + + public boolean isPersistentlyStarted() + { + return true; + } + + public boolean isActivationPolicyUsed() + { + return false; + } + + @Override + public Bundle getBundle() + { + return bundle; + } + } + + public static class BundleRevisionImpl implements BundleRevision + { + private final Bundle bundle; + + public BundleRevisionImpl(Bundle bundle) + { + this.bundle = bundle; + } + + @Override + public String getSymbolicName() + { + return bundle.getSymbolicName(); + } + + @Override + public Version getVersion() + { + return bundle.getVersion(); + } + + @Override + public List getDeclaredCapabilities(String namespace) + { + return Collections.emptyList(); + } + + @Override + public List getDeclaredRequirements(String namespace) + { + return Collections.emptyList(); + } + + @Override + public int getTypes() + { + if (bundle.getHeaders().get(Constants.FRAGMENT_HOST) != null) + { + return BundleRevision.TYPE_FRAGMENT; + } + return 0; + } + + @Override + public BundleWiring getWiring() + { + return bundle.adapt(BundleWiring.class); + } + + @Override + public List getCapabilities(String namespace) + { + return Collections.emptyList(); + } + + @Override + public List getRequirements(String namespace) + { + return Collections.emptyList(); + } + + @Override + public Bundle getBundle() + { + return bundle; + } + } + + public static class BundleWiringImpl implements BundleWiring + { + + private final Bundle bundle; + private final ClassLoader classLoader; + + public BundleWiringImpl(Bundle bundle, ClassLoader classLoader) + { + this.bundle = bundle; + this.classLoader = classLoader; + } + + @Override + public boolean isInUse() + { + return true; + } + + @Override + public boolean isCurrent() + { + return true; + } + + @Override + public BundleRevision getRevision() + { + return bundle.adapt(BundleRevision.class); + } + + @Override + public List getRequirements(String namespace) + { + return Collections.emptyList(); + } + + @Override + public List getRequiredWires(String namespace) + { + return Collections.emptyList(); + } + + @Override + public List getProvidedWires(String namespace) + { + return Collections.emptyList(); + } + + @Override + public ClassLoader getClassLoader() + { + return classLoader; + } + + @Override + public List getCapabilities(String namespace) + { + return Collections.emptyList(); + } + + @Override + public List getResourceCapabilities(String namespace) + { + return Collections.emptyList(); + } + + @Override + public List getResourceRequirements(String namespace) + { + return Collections.emptyList(); + } + + @Override + public List getProvidedResourceWires(String namespace) + { + return Collections.emptyList(); + } + + @Override + public List getRequiredResourceWires(String namespace) + { + return Collections.emptyList(); + } + + @Override + public BundleRevision getResource() + { + return getRevision(); + } + + @Override + public Bundle getBundle() + { + return bundle; + } + + @Override + public List findEntries(String path, String filePattern, int options) + { + List result = new ArrayList(); + for (Enumeration e = bundle.findEntries(path, filePattern, options == BundleWiring.FINDENTRIES_RECURSE); e.hasMoreElements(); ) + { + result.add(e.nextElement()); + } + return result; + } + + @Override + public Collection listResources(String path, String filePattern, int options) + { + // TODO: this is wrong, we should return the resource names + Collection result = new ArrayList(); + for (URL u : findEntries(path, filePattern, options)) + { + result.add(u.toString()); + } + return result; + } + + } + +} diff --git a/connect/src/main/java/org/apache/felix/connect/PojoSRBundleContext.java b/connect/src/main/java/org/apache/felix/connect/PojoSRBundleContext.java new file mode 100644 index 00000000000..9e0ab96bc23 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/PojoSRBundleContext.java @@ -0,0 +1,421 @@ +/* + * 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.connect; + +import java.io.File; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Dictionary; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.BundleListener; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceObjects; +import org.osgi.framework.ServicePermission; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.hooks.service.FindHook; + +import org.apache.felix.connect.felix.framework.ServiceRegistry; +import org.apache.felix.connect.felix.framework.capabilityset.SimpleFilter; +import org.apache.felix.connect.felix.framework.util.EventDispatcher; +import org.apache.felix.connect.felix.framework.util.ShrinkableCollection; +import org.apache.felix.connect.felix.framework.util.Util; + +class PojoSRBundleContext implements BundleContext +{ + private final Bundle m_bundle; + private final ServiceRegistry m_reg; + private final EventDispatcher m_dispatcher; + private final Map m_bundles; + private final Map m_config; + + public PojoSRBundleContext(Bundle bundle, ServiceRegistry reg, + EventDispatcher dispatcher, Map bundles, Map config) + { + m_bundle = bundle; + m_reg = reg; + m_dispatcher = dispatcher; + m_bundles = bundles; + m_config = config; + } + + public boolean ungetService(ServiceReference reference) + { + return m_reg.ungetService(m_bundle, reference); + } + + public void removeServiceListener(ServiceListener listener) + { + m_dispatcher.removeListener(this, ServiceListener.class, + listener); + } + + public void removeFrameworkListener(FrameworkListener listener) + { + m_dispatcher + .removeListener(this, FrameworkListener.class, listener); + } + + public void removeBundleListener(BundleListener listener) + { + m_dispatcher.removeListener(this, BundleListener.class, listener); + } + + public ServiceRegistration registerService(String clazz, Object service, + Dictionary properties) + { + return m_reg.registerService(m_bundle, new String[]{clazz}, service, + properties); + } + + public ServiceRegistration registerService(String[] clazzes, + Object service, Dictionary properties) + { + return m_reg.registerService(m_bundle, clazzes, service, properties); + } + + public Bundle installBundle(String location) throws BundleException + { + throw new BundleException("pojosr can't do that"); + } + + public Bundle installBundle(String location, InputStream input) + throws BundleException + { + + throw new BundleException("pojosr can't do that"); + } + + public ServiceReference[] getServiceReferences(String clazz, String filter) + throws InvalidSyntaxException + { + return getServiceReferences(clazz, filter, true); + } + + public ServiceReference getServiceReference(String clazz) + { + try + { + return getBestServiceReference(getServiceReferences(clazz, null)); + } + catch (InvalidSyntaxException e) + { + throw new IllegalStateException(e); + } + } + + private ServiceReference getBestServiceReference(ServiceReference[] refs) + { + if (refs == null) + { + return null; + } + + if (refs.length == 1) + { + return refs[0]; + } + + // Loop through all service references and return + // the "best" one according to its rank and ID. + ServiceReference bestRef = refs[0]; + for (int i = 1; i < refs.length; i++) + { + if (bestRef.compareTo(refs[i]) < 0) + { + bestRef = refs[i]; + } + } + + return bestRef; + } + + public S getService(ServiceReference reference) + { + return m_reg.getService(m_bundle, reference); + } + + @Override + public ServiceObjects getServiceObjects(ServiceReference reference) { + return new ServiceObjectsImpl(reference); + } + + + // + // ServiceObjects implementation + // + class ServiceObjectsImpl implements ServiceObjects + { + private final ServiceReference m_ref; + + public ServiceObjectsImpl(final ServiceReference ref) + { + this.m_ref = ref; + } + + public S getService() { + + // CONCURRENCY NOTE: This is a check-then-act situation, + // but we ignore it since the time window is small and + // the result is the same as if the calling thread had + // won the race condition. + + final Object sm = System.getSecurityManager(); + + if (sm != null) + { + ((SecurityManager) sm).checkPermission(new ServicePermission(m_ref, ServicePermission.GET)); + } + + return PojoSRBundleContext.this.getService(m_ref); + } + + public void ungetService(final S srvObj) + { + PojoSRBundleContext.this.ungetService(m_ref); + } + + public ServiceReference getServiceReference() + { + return m_ref; + } + } + public String getProperty(String key) + { + Object result = m_config.get(key); + + return result == null ? System.getProperty(key) : result.toString(); + } + + public File getDataFile(String filename) + { + File root = new File("bundle" + m_bundle.getBundleId()); + String storage = getProperty("org.osgi.framework.storage"); + if (storage != null) + { + root = new File(new File(storage), root.getName()); + } + root.mkdirs(); + return filename.trim().length() > 0 ? new File(root, filename) : root; + } + + public Bundle[] getBundles() + { + Bundle[] result = m_bundles.values().toArray( + new Bundle[m_bundles.size()]); + Arrays.sort(result, new Comparator() + { + + public int compare(Bundle o1, Bundle o2) + { + return (int) (o1.getBundleId() - o2.getBundleId()); + } + }); + return result; + } + + public Bundle getBundle(long id) + { + return m_bundles.get(id); + } + + public Bundle getBundle() + { + return m_bundle; + } + + public ServiceReference[] getAllServiceReferences(String clazz, + String filter) throws InvalidSyntaxException + { + return getServiceReferences(clazz, filter, false); + } + + /** + * Retrieves an array of {@link ServiceReference} objects based on calling bundle, + * service class name, and filter expression. Optionally checks for isAssignable to + * make sure that the service can be cast to the + * @param className Service Classname or null for all + * @param expr Filter Criteria or null + * @return Array of ServiceReference objects that meet the criteria + * @throws InvalidSyntaxException + */ + ServiceReference[] getServiceReferences( + final String className, + final String expr, final boolean checkAssignable) + throws InvalidSyntaxException + { + // Define filter if expression is not null. + SimpleFilter filter = null; + if (expr != null) + { + try + { + filter = SimpleFilter.parse(expr); + } + catch (Exception ex) + { + throw new InvalidSyntaxException(ex.getMessage(), expr); + } + } + + // Ask the service registry for all matching service references. + final Collection> refList = m_reg.getServiceReferences(className, filter); + + // Filter on assignable references + if (checkAssignable) + { + for (Iterator> it = refList.iterator(); it.hasNext();) + { + // Get the current service reference. + ServiceReference ref = it.next(); + // Now check for castability. + if (!Util.isServiceAssignable(m_bundle, ref)) + { + it.remove(); + } + } + } + + // activate findhooks + Set> findHooks = m_reg.getHooks(org.osgi.framework.hooks.service.FindHook.class); + for (ServiceReference sr : findHooks) + { + org.osgi.framework.hooks.service.FindHook fh = m_reg.getService(getBundle(0), sr); + if (fh != null) + { + try + { + fh.find(this, + className, + expr, + !checkAssignable, + new ShrinkableCollection>(refList)); + } + catch (Throwable th) + { + System.err.println("Problem invoking service registry hook"); + th.printStackTrace(); + } + finally + { + m_reg.ungetService(getBundle(0), sr); + } + } + } + + if (refList.size() > 0) + { + return refList.toArray(new ServiceReference[refList.size()]); + } + + return null; + } + + public Filter createFilter(String filter) throws InvalidSyntaxException + { + return FrameworkUtil.createFilter(filter); + } + + public void addServiceListener(ServiceListener listener) + { + try + { + addServiceListener(listener, null); + } + catch (InvalidSyntaxException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + public void addServiceListener(final ServiceListener listener, String filter) + throws InvalidSyntaxException + { + m_dispatcher.addListener(this, ServiceListener.class, listener, + filter == null ? null : FrameworkUtil.createFilter(filter)); + } + + public void addFrameworkListener(FrameworkListener listener) + { + m_dispatcher.addListener(this, FrameworkListener.class, listener, + null); + } + + public void addBundleListener(BundleListener listener) + { + m_dispatcher + .addListener(this, BundleListener.class, listener, null); + } + + @SuppressWarnings("unchecked") + public ServiceRegistration registerService(Class clazz, S service, Dictionary properties) + { + return (ServiceRegistration) registerService(clazz.getName(), service, properties); + } + + @Override + public ServiceRegistration registerService(Class clazz, ServiceFactory factory, Dictionary properties) { + return (ServiceRegistration) registerService(clazz.getName(), factory, properties); + } + + @SuppressWarnings("unchecked") + public ServiceReference getServiceReference(Class clazz) + { + return (ServiceReference) getServiceReference(clazz.getName()); + } + + @SuppressWarnings("unchecked") + public Collection> getServiceReferences(Class clazz, String filter) + throws InvalidSyntaxException + { + ServiceReference[] refs = (ServiceReference[]) getServiceReferences(clazz.getName(), filter); + if (refs == null) + { + return Collections.emptyList(); + } + return Arrays.asList(refs); + } + + public Bundle getBundle(String location) + { + for (Bundle bundle : m_bundles.values()) + { + if (location.equals(bundle.getLocation())) + { + return bundle; + } + } + return null; + } +} diff --git a/connect/src/main/java/org/apache/felix/connect/PojoServiceRegistryFactoryImpl.java b/connect/src/main/java/org/apache/felix/connect/PojoServiceRegistryFactoryImpl.java new file mode 100644 index 00000000000..1402f62c1d7 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/PojoServiceRegistryFactoryImpl.java @@ -0,0 +1,289 @@ +/* + * 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.connect; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.security.cert.X509Certificate; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.BundleException; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.SynchronousBundleListener; +import org.osgi.framework.Version; +import org.osgi.framework.launch.Framework; +import org.osgi.framework.launch.FrameworkFactory; + +import org.apache.felix.connect.launch.ClasspathScanner; +import org.apache.felix.connect.launch.PojoServiceRegistry; +import org.apache.felix.connect.launch.PojoServiceRegistryFactory; + +public class PojoServiceRegistryFactoryImpl implements PojoServiceRegistryFactory, FrameworkFactory +{ + + public PojoServiceRegistry newPojoServiceRegistry(Map configuration) throws Exception + { + return new PojoSR(configuration); + } + + public Framework newFramework(Map configuration) + { + return new FrameworkImpl(configuration.get("pojosr.filter")); + } + + private static final class FrameworkImpl implements Framework + { + private final String m_filter; + private volatile Bundle m_bundle = null; + private volatile PojoServiceRegistry m_reg = null; + + public FrameworkImpl(String filter) + { + m_filter = filter; + } + + public void init() throws BundleException + { + try + { + m_reg = new PojoServiceRegistryFactoryImpl() + .newPojoServiceRegistry(new HashMap()); + m_bundle = m_reg.getBundleContext().getBundle(); + } + catch (Exception ex) + { + throw new BundleException("Unable to scan classpath", ex); + } + } + + @Override + public void init(FrameworkListener... listeners) throws BundleException { + init(); + for (FrameworkListener listener : listeners) { + m_bundle.getBundleContext().addFrameworkListener(listener); + } + } + + public int getState() + { + return (m_bundle == null) ? Bundle.INSTALLED : m_bundle.getState(); + } + + public void start(int options) throws BundleException + { + start(); + } + + public void start() throws BundleException + { + try + { + m_reg.startBundles((m_filter != null) ? new ClasspathScanner() + .scanForBundles(m_filter) + : new ClasspathScanner().scanForBundles()); + } + catch (Exception e) + { + throw new BundleException("Error starting framework", e); + } + } + + public void stop(int options) throws BundleException + { + m_bundle.stop(options); + } + + public void stop() throws BundleException + { + m_bundle.stop(); + } + + public void update(InputStream input) throws BundleException + { + m_bundle.update(input); + } + + public void update() throws BundleException + { + m_bundle.update(); + } + + public void uninstall() throws BundleException + { + m_bundle.uninstall(); + } + + public Dictionary getHeaders() + { + return m_bundle.getHeaders(); + } + + public long getBundleId() + { + return m_bundle.getBundleId(); + } + + public String getLocation() + { + return m_bundle.getLocation(); + } + + public ServiceReference[] getRegisteredServices() + { + return m_bundle.getRegisteredServices(); + } + + public ServiceReference[] getServicesInUse() + { + return m_bundle.getServicesInUse(); + } + + public boolean hasPermission(Object permission) + { + return m_bundle.hasPermission(permission); + } + + public URL getResource(String name) + { + return m_bundle.getResource(name); + } + + public Dictionary getHeaders(String locale) + { + return m_bundle.getHeaders(locale); + } + + public String getSymbolicName() + { + return m_bundle.getSymbolicName(); + } + + public Class loadClass(String name) throws ClassNotFoundException + { + return m_bundle.loadClass(name); + } + + public Enumeration getResources(String name) throws IOException + { + return m_bundle.getResources(name); + } + + public Enumeration getEntryPaths(String path) + { + return m_bundle.getEntryPaths(path); + } + + public URL getEntry(String path) + { + return m_bundle.getEntry(path); + } + + public long getLastModified() + { + return m_bundle.getLastModified(); + } + + public Enumeration findEntries(String path, String filePattern, boolean recurse) + { + return m_bundle.findEntries(path, filePattern, recurse); + } + + public BundleContext getBundleContext() + { + return m_bundle.getBundleContext(); + } + + public Map> getSignerCertificates(int signersType) + { + return m_bundle.getSignerCertificates(signersType); + } + + public Version getVersion() + { + return m_bundle.getVersion(); + } + + public FrameworkEvent waitForStop(long timeout) + throws InterruptedException + { + final Object lock = new Object(); + + m_bundle.getBundleContext().addBundleListener(new SynchronousBundleListener() + { + + public void bundleChanged(BundleEvent event) + { + if ((event.getBundle() == m_bundle) && (event.getType() == BundleEvent.STOPPED)) + { + synchronized (lock) + { + lock.notifyAll(); + } + } + } + }); + synchronized (lock) + { + while (m_bundle.getState() != Bundle.RESOLVED) + { + if (m_bundle.getState() == Bundle.STOPPING) + { + lock.wait(100); + } + else + { + lock.wait(); + } + } + } + return new FrameworkEvent(FrameworkEvent.STOPPED, m_bundle, null); + } + + public File getDataFile(String filename) + { + return m_bundle.getDataFile(filename); + } + + public int compareTo(Bundle o) + { + if (o == this) + { + return 0; + } + return m_bundle.compareTo(o); + } + + public A adapt(Class type) + { + return m_bundle.adapt(type); + } + + } +} diff --git a/connect/src/main/java/org/apache/felix/connect/RequiredBundleImpl.java b/connect/src/main/java/org/apache/felix/connect/RequiredBundleImpl.java new file mode 100644 index 00000000000..50993b7fbc0 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/RequiredBundleImpl.java @@ -0,0 +1,79 @@ +/* + * 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.connect; + +import java.util.HashSet; +import java.util.Set; + +import org.osgi.framework.Bundle; +import org.osgi.framework.Version; +import org.osgi.framework.namespace.BundleNamespace; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.service.packageadmin.RequiredBundle; + +public class RequiredBundleImpl implements RequiredBundle +{ + + private final Bundle m_bundle; + + public RequiredBundleImpl(Bundle bundle) + { + m_bundle = bundle; + } + + public String getSymbolicName() + { + return m_bundle.getSymbolicName(); + } + + public Bundle getBundle() + { + return m_bundle; + } + + public Bundle[] getRequiringBundles() + { + Set set = new HashSet(); + for (BundleWire wire : m_bundle.adapt(BundleWiring.class).getProvidedWires(null)) + { + if (BundleNamespace.BUNDLE_NAMESPACE.equals(wire.getCapability().getNamespace())) + { + set.add(wire.getRequirer().getBundle()); + } + } + return set.toArray(new Bundle[set.size()]); + } + + public Version getVersion() + { + return m_bundle.getVersion(); + } + + public boolean isRemovalPending() + { + return false; + } + + public String toString() + { + return m_bundle.getSymbolicName() + "; version=" + m_bundle.getVersion(); + } + +} diff --git a/connect/src/main/java/org/apache/felix/connect/Revision.java b/connect/src/main/java/org/apache/felix/connect/Revision.java new file mode 100644 index 00000000000..77b23bdbd3b --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/Revision.java @@ -0,0 +1,31 @@ +/* + * 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.connect; + +import java.net.URL; +import java.util.Enumeration; + +public interface Revision +{ + public long getLastModified(); + + public URL getEntry(String entryName); + + public Enumeration getEntries(); +} \ No newline at end of file diff --git a/connect/src/main/java/org/apache/felix/connect/URLRevision.java b/connect/src/main/java/org/apache/felix/connect/URLRevision.java new file mode 100644 index 00000000000..721b897ed58 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/URLRevision.java @@ -0,0 +1,167 @@ +/* + * 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.connect; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; +import java.lang.ref.WeakReference; + +class URLRevision implements Revision +{ + private final URL m_url; + private final long m_lastModified; + private WeakReference m_urlContent; + + public URLRevision(URL url, long lastModified) + { + m_url = url; + if (lastModified > 0) + { + m_lastModified = lastModified; + } + else + { + m_lastModified = System.currentTimeMillis(); + } + } + + @Override + public long getLastModified() + { + return m_lastModified; + } + + @Override + public Enumeration getEntries() + { + InputStream content = null; + JarInputStream jarInput = null; + + try + { + content = getUrlContent(); + jarInput = new JarInputStream(content); + List entries = new ArrayList(); + JarEntry jarEntry; + + while ((jarEntry = jarInput.getNextJarEntry()) != null) + { + entries.add(jarEntry.getName()); + } + return Collections.enumeration(entries); + } + + catch (IOException e) + { + e.printStackTrace(); + return Collections.enumeration(Collections.EMPTY_LIST); + } + + finally + { + close(content); + close(jarInput); + } + } + + @Override + public URL getEntry(String entryName) + { + try + { + return new URL(m_url, entryName); + } + catch (MalformedURLException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + return null; + } + } + + /** + * Loads the URL content, and cache it using a weak reference. + * + * @return the URL content + * @throws IOException on any io errors + */ + private synchronized InputStream getUrlContent() throws IOException { + BufferedInputStream in = null; + ByteArrayOutputStream out = null; + byte[] content = null; + + try + { + if (m_urlContent == null || (content = m_urlContent.get()) == null) + { + out = new ByteArrayOutputStream(4096); + in = new BufferedInputStream(m_url.openStream(), 4096); + int c; + while ((c = in.read()) != -1) + { + out.write(c); + } + content = out.toByteArray(); + m_urlContent = new WeakReference(content); + } + + return new ByteArrayInputStream(content); + } + + finally + { + close(out); + close(in); + } + } + + /** + * Helper method used to simply close a stream. + * + * @param closeable the stream to close + */ + private void close(Closeable closeable) + { + try + { + if (closeable != null) + { + closeable.close(); + } + } + catch (IOException e) + { + } + } +} diff --git a/connect/src/main/java/org/apache/felix/connect/VFSRevision.java b/connect/src/main/java/org/apache/felix/connect/VFSRevision.java new file mode 100644 index 00000000000..fda0045be45 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/VFSRevision.java @@ -0,0 +1,121 @@ +/* + * 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.connect; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import org.jboss.vfs.VFS; +import org.jboss.vfs.VirtualFile; +import org.jboss.vfs.VirtualFileVisitor; +import org.jboss.vfs.VisitorAttributes; + +/** + * Loads the content of a bundle using JBoss VFS protocol. + */ +public class VFSRevision implements Revision +{ + private final URL m_url; + private final long m_lastModified; + private final Map m_entries = new HashMap(); + + public VFSRevision(URL url, long lastModified) + { + m_url = url; + m_lastModified = lastModified; + } + + public long getLastModified() + { + return m_lastModified; + } + + public Enumeration getEntries() + { + try + { + loadEntries(); // lazily load entries + return Collections.enumeration(m_entries.keySet()); + } + catch (URISyntaxException e) + { + e.printStackTrace(); + return null; + } + catch (IOException e) + { + e.printStackTrace(); + return null; + } + } + + public URL getEntry(String entryName) + { + try + { + loadEntries(); + VirtualFile vfile = m_entries.get(entryName); + return vfile != null ? vfile.toURL() : null; + } + catch (MalformedURLException e) + { + e.printStackTrace(); + return null; + } + catch (URISyntaxException e) + { + e.printStackTrace(); + return null; + } + catch (IOException e) + { + e.printStackTrace(); + return null; + } + } + + private synchronized void loadEntries() throws URISyntaxException, IOException + { + if (m_entries.size() == 0) + { + final VirtualFile root = VFS.getChild(m_url.toURI()); + final String uriPath = m_url.toURI().getPath(); + + root.visit(new VirtualFileVisitor() + { + public void visit(VirtualFile vfile) + { + String entryPath = vfile.getPathName().substring(uriPath.length()); + m_entries.put(entryPath, vfile); + } + + public VisitorAttributes getAttributes() + { + return VisitorAttributes.RECURSE_LEAVES_ONLY; + } + }); + } + } +} diff --git a/connect/src/main/java/org/apache/felix/connect/felix/framework/ServiceRegistrationImpl.java b/connect/src/main/java/org/apache/felix/connect/felix/framework/ServiceRegistrationImpl.java new file mode 100644 index 00000000000..2c401b07cf7 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/felix/framework/ServiceRegistrationImpl.java @@ -0,0 +1,585 @@ +/* + * 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.connect.felix.framework; + +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Map; +import java.util.Set; + +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceException; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRevision; + +import org.apache.felix.connect.felix.framework.util.MapToDictionary; +import org.apache.felix.connect.felix.framework.util.StringMap; +import org.apache.felix.connect.felix.framework.util.Util; + +class ServiceRegistrationImpl implements ServiceRegistration +{ + // Service registry. + private final ServiceRegistry m_registry; + // Bundle providing the service. + private final Bundle m_bundle; + // Interfaces associated with the service object. + private final String[] m_classes; + // Service Id associated with the service object. + private final Long m_serviceId; + // Service object. + private volatile Object m_svcObj; + // Service factory interface. + private volatile ServiceFactory m_factory; + // Associated property dictionary. + private volatile Map m_propMap = new StringMap(false); + // Re-usable service reference. + private final ServiceReferenceImpl m_ref; + // Flag indicating that we are unregistering. + private volatile boolean m_isUnregistering = false; + + public ServiceRegistrationImpl(ServiceRegistry registry, Bundle bundle, + String[] classes, Long serviceId, Object svcObj, Dictionary dict) + { + m_registry = registry; + m_bundle = bundle; + m_classes = classes; + m_serviceId = serviceId; + m_svcObj = svcObj; + m_factory = (m_svcObj instanceof ServiceFactory) ? (ServiceFactory) m_svcObj + : null; + + initializeProperties(dict); + + // This reference is the "standard" reference for this + // service and will always be returned by getReference(). + m_ref = new ServiceReferenceImpl(); + } + + protected synchronized boolean isValid() + { + return (m_svcObj != null); + } + + protected synchronized void invalidate() + { + m_svcObj = null; + } + + public synchronized ServiceReferenceImpl getReference() + { + // Make sure registration is valid. + if (!isValid()) + { + throw new IllegalStateException( + "The service registration is no longer valid."); + } + return m_ref; + } + + public void setProperties(Dictionary dict) + { + Map oldProps; + synchronized (this) + { + // Make sure registration is valid. + if (!isValid()) + { + throw new IllegalStateException( + "The service registration is no longer valid."); + } + // Remember old properties. + oldProps = m_propMap; + // Set the properties. + initializeProperties(dict); + } + // Tell registry about it. + m_registry.servicePropertiesModified(this, + new MapToDictionary(oldProps)); + } + + public void unregister() + { + synchronized (this) + { + if (!isValid() || m_isUnregistering) + { + throw new IllegalStateException("Service already unregistered."); + } + m_isUnregistering = true; + } + m_registry.unregisterService(m_bundle, this); + synchronized (this) + { + m_svcObj = null; + m_factory = null; + } + } + + // + // Utility methods. + // + Object getProperty(String key) + { + return m_propMap.get(key); + } + + private String[] getPropertyKeys() + { + Set s = m_propMap.keySet(); + return (String[]) s.toArray(new String[s.size()]); + } + + private Bundle[] getUsingBundles() + { + return m_registry.getUsingBundles(m_ref); + } + + /** + * This method provides direct access to the associated service object; it + * generally should not be used by anyone other than the service registry + * itself. + * + * @return The service object associated with the registration. + */ + Object getService() + { + return m_svcObj; + } + + Object getService(Bundle acqBundle) + { + // If the service object is a service factory, then + // let it create the service object. + if (m_factory != null) + { + Object svcObj = null; + svcObj = getFactoryUnchecked(acqBundle); + + return svcObj; + } + else + { + return m_svcObj; + } + } + + void ungetService(Bundle relBundle, T svcObj) + { + // If the service object is a service factory, then + // let it release the service object. + if (m_factory != null) + { + try + { + ungetFactoryUnchecked(relBundle, svcObj); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + } + + private void initializeProperties(Dictionary dict) + { + // Create a case-insensitive map for the properties. + Map props = new StringMap(false); + + if (dict != null) + { + // Make sure there are no duplicate keys. + Enumeration keys = dict.keys(); + while (keys.hasMoreElements()) + { + Object key = keys.nextElement(); + if (props.get(key) == null) + { + props.put(key, dict.get(key)); + } + else + { + throw new IllegalArgumentException( + "Duplicate service property: " + key); + } + } + } + + // Add the framework assigned properties. + props.put(Constants.OBJECTCLASS, m_classes); + props.put(Constants.SERVICE_ID, m_serviceId); + + // Update the service property map. + m_propMap = props; + } + + private Object getFactoryUnchecked(Bundle bundle) + { + Object svcObj = null; + try + { + svcObj = m_factory.getService(bundle, this); + } + catch (Throwable th) + { + throw new ServiceException("Service factory exception: " + + th.getMessage(), ServiceException.FACTORY_EXCEPTION, th); + } + if (svcObj != null) + { + for (String className : m_classes) + { + Class clazz = Util.loadClassUsingClass(svcObj.getClass(), className); + if ((clazz == null) || !clazz.isAssignableFrom(svcObj.getClass())) + { + if (clazz == null) + { + throw new ServiceException( + "Service cannot be cast due to missing class: " + className, + ServiceException.FACTORY_ERROR); + } + else + { + throw new ServiceException( + "Service cannot be cast: " + className, + ServiceException.FACTORY_ERROR); + } + } + } + } + else + { + throw new ServiceException("Service factory returned null.", + ServiceException.FACTORY_ERROR); + } + return svcObj; + } + + private void ungetFactoryUnchecked(Bundle bundle, T svcObj) + { + m_factory.ungetService(bundle, this, svcObj); + } + + + // + // ServiceReference implementation + // + + class ServiceReferenceImpl implements ServiceReference, BundleCapability + { + private final ServiceReferenceMap m_map; + + private ServiceReferenceImpl() + { + m_map = new ServiceReferenceMap(); + } + + ServiceRegistrationImpl getRegistration() + { + return ServiceRegistrationImpl.this; + } + + // + // Capability methods. + // + + + @Override + public BundleRevision getResource() + { + return getRevision(); + } + + @Override + public BundleRevision getRevision() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String getNamespace() + { + return "service-reference"; + } + + @Override + public Map getDirectives() + { + return Collections.emptyMap(); + } + + @Override + public Map getAttributes() + { + return m_map; + } + + @Override + public Object getProperty(String s) + { + return ServiceRegistrationImpl.this.getProperty(s); + } + + @Override + public String[] getPropertyKeys() + { + return ServiceRegistrationImpl.this.getPropertyKeys(); + } + + @Override + public Bundle getBundle() + { + // The spec says that this should return null if + // the service is unregistered. + return (isValid()) ? m_bundle : null; + } + + @Override + public Bundle[] getUsingBundles() + { + return ServiceRegistrationImpl.this.getUsingBundles(); + } + + public String toString() + { + String[] ocs = (String[]) getProperty("objectClass"); + String oc = "["; + for (int i = 0; i < ocs.length; i++) + { + oc = oc + ocs[i]; + if (i < ocs.length - 1) + { + oc = oc + ", "; + } + } + oc = oc + "]"; + return oc; + } + + @Override + public boolean isAssignableTo(Bundle requester, String className) + { + // Always return true if the requester is the same as the provider. + if (requester == m_bundle) + { + return true; + } + + // Boolean flag. + boolean allow = true; + /* // Get the package. + * String pkgName = Util.getClassPackage(className); + * Module requesterModule = ((BundleImpl) + * requester).getCurrentModule(); // Get package wiring from service + * requester. Wire requesterWire = Util.getWire(requesterModule, + * pkgName); // Get package wiring from service provider. Module + * providerModule = ((BundleImpl) m_bundle).getCurrentModule(); Wire + * providerWire = Util.getWire(providerModule, pkgName); + * + * // There are four situations that may occur here: // 1. Neither + * the requester, nor provider have wires for the package. // 2. The + * requester does not have a wire for the package. // 3. The + * provider does not have a wire for the package. // 4. Both the + * requester and provider have a wire for the package. // For case + * 1, if the requester does not have access to the class at // all, + * we assume it is using reflection and do not filter. If the // + * requester does have access to the class, then we make sure it is + * // the same class as the service. For case 2, we do not filter if + * the // requester is the exporter of the package to which the + * provider of // the service is wired. Otherwise, as in case 1, if + * the requester // does not have access to the class at all, we do + * not filter, but if // it does have access we check if it is the + * same class accessible to // the providing module. For case 3, the + * provider will not have a wire // if it is exporting the package, + * so we determine if the requester // is wired to it or somehow + * using the same class. For case 4, we // simply compare the + * exporting modules from the package wiring to // determine if we + * need to filter the service reference. + * + * // Case 1: Both requester and provider have no wire. if + * ((requesterWire == null) && (providerWire == null)) { // If + * requester has no access then true, otherwise service // + * registration must have same class as requester. try { Class + * requestClass = requesterModule.getClassByDelegation(className); + * allow = getRegistration().isClassAccessible(requestClass); } + * catch (Exception ex) { // Requester has no access to the class, + * so allow it, since // we assume the requester is using + * reflection. allow = true; } } // Case 2: Requester has no wire, + * but provider does. else if ((requesterWire == null) && + * (providerWire != null)) { // Allow if the requester is the + * exporter of the provider's wire. if + * (providerWire.getExporter().equals(requesterModule)) { allow = + * true; } // Otherwise, check if the requester has access to the + * class and, // if so, if it is the same class as the provider. + * else { try { // Try to load class from requester. Class + * requestClass = requesterModule.getClassByDelegation(className); + * try { // If requester has access to the class, verify it is the + * // same class as the provider. allow = + * (providerWire.getClass(className) == requestClass); } catch + * (Exception ex) { allow = false; } } catch (Exception ex) { // + * Requester has no access to the class, so allow it, since // we + * assume the requester is using reflection. allow = true; } } } // + * Case 3: Requester has a wire, but provider does not. else if + * ((requesterWire != null) && (providerWire == null)) { // If the + * provider is the exporter of the requester's package, then check + * // if the requester is wired to the latest version of the + * provider, if so // then allow else don't (the provider has been + * updated but not refreshed). if (((BundleImpl) + * m_bundle).hasModule(requesterWire.getExporter())) { allow = + * providerModule.equals(requesterWire.getExporter()); } // If the + * provider is not the exporter of the requester's package, // then + * try to use the service registration to see if the requester's // + * class is accessible. else { try { // Load the class from the + * requesting bundle. Class requestClass = + * requesterModule.getClassByDelegation(className); // Get the + * service registration and ask it to check // if the service object + * is assignable to the requesting // bundle's class. allow = + * getRegistration().isClassAccessible(requestClass); } catch + * (Exception ex) { // Filter to be safe. allow = false; } } } // + * Case 4: Both requester and provider have a wire. else { // + * Include service reference if the wires have the // same source + * module. allow = + * providerWire.getExporter().equals(requesterWire.getExporter()); } + */ + + return allow; + } + + @Override + public int compareTo(Object reference) + { + ServiceReference other = (ServiceReference) reference; + + Long id = (Long) getProperty(Constants.SERVICE_ID); + Long otherId = (Long) other.getProperty(Constants.SERVICE_ID); + + if (id.equals(otherId)) + { + return 0; // same service + } + + Object rankObj = getProperty(Constants.SERVICE_RANKING); + Object otherRankObj = other.getProperty(Constants.SERVICE_RANKING); + + // If no rank, then spec says it defaults to zero. + rankObj = (rankObj == null) ? new Integer(0) : rankObj; + otherRankObj = (otherRankObj == null) ? new Integer(0) + : otherRankObj; + + // If rank is not Integer, then spec says it defaults to zero. + Integer rank = (rankObj instanceof Integer) ? (Integer) rankObj + : new Integer(0); + Integer otherRank = (otherRankObj instanceof Integer) ? (Integer) otherRankObj + : new Integer(0); + + // Sort by rank in ascending order. + if (rank.compareTo(otherRank) < 0) + { + return -1; // lower rank + } + else if (rank.compareTo(otherRank) > 0) + { + return 1; // higher rank + } + + // If ranks are equal, then sort by service id in descending order. + return (id.compareTo(otherId) < 0) ? 1 : -1; + } + } + + private class ServiceReferenceMap implements Map + { + @Override + public int size() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean isEmpty() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean containsKey(Object o) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean containsValue(Object o) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Object get(Object o) + { + return ServiceRegistrationImpl.this.getProperty((String) o); + } + + @Override + public Object put(String k, Object v) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Object remove(Object o) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void putAll(Map map) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void clear() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Set keySet() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Collection values() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Set> entrySet() + { + return Collections.emptySet(); + } + } +} \ No newline at end of file diff --git a/connect/src/main/java/org/apache/felix/connect/felix/framework/ServiceRegistry.java b/connect/src/main/java/org/apache/felix/connect/felix/framework/ServiceRegistry.java new file mode 100644 index 00000000000..a076b7f9351 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/felix/framework/ServiceRegistry.java @@ -0,0 +1,809 @@ +/* + * 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.connect.felix.framework; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.WeakHashMap; + +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceException; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.wiring.BundleCapability; + +import org.apache.felix.connect.felix.framework.capabilityset.CapabilitySet; +import org.apache.felix.connect.felix.framework.capabilityset.SimpleFilter; + +public class ServiceRegistry +{ + private long m_currentServiceId = 1L; + // Maps bundle to an array of service registrations. + private final Map m_regsMap = Collections.synchronizedMap(new HashMap()); + // Capability set for all service registrations. + private final CapabilitySet m_regCapSet; + // Maps registration to thread to keep track when a + // registration is in use, which will cause other + // threads to wait. + private final Map m_lockedRegsMap = new HashMap(); + // Maps bundle to an array of usage counts. + private final Map m_inUseMap = new HashMap(); + private final ServiceRegistryCallbacks m_callbacks; + private final WeakHashMap m_blackList = + new WeakHashMap(); + private final static Class[] m_hookClasses = + { + org.osgi.framework.hooks.bundle.FindHook.class, + org.osgi.framework.hooks.bundle.EventHook.class, + org.osgi.framework.hooks.service.EventHook.class, + org.osgi.framework.hooks.service.EventListenerHook.class, + org.osgi.framework.hooks.service.FindHook.class, + org.osgi.framework.hooks.service.ListenerHook.class, + org.osgi.framework.hooks.weaving.WeavingHook.class, + org.osgi.framework.hooks.resolver.ResolverHookFactory.class, + org.osgi.service.url.URLStreamHandlerService.class, + java.net.ContentHandler.class + }; + private final Map, Set>> m_allHooks = + new HashMap, Set>>(); + + public ServiceRegistry(ServiceRegistryCallbacks callbacks) + { + m_callbacks = callbacks; + + m_regCapSet = new CapabilitySet(Collections.singletonList(Constants.OBJECTCLASS), false); + } + + public ServiceReference[] getRegisteredServices(Bundle bundle) + { + ServiceRegistration[] regs = m_regsMap.get(bundle); + if (regs != null) + { + List refs = new ArrayList(regs.length); + for (ServiceRegistration reg : regs) + { + try + { + refs.add(reg.getReference()); + } + catch (IllegalStateException ex) + { + // Don't include the reference as it is not valid anymore + } + } + return refs.toArray(new ServiceReference[refs.size()]); + } + return null; + } + + // Caller is expected to fire REGISTERED event. + public ServiceRegistration registerService( + Bundle bundle, String[] classNames, Object svcObj, Dictionary dict) + { + ServiceRegistrationImpl reg = null; + + synchronized (this) + { + // Create the service registration. + reg = new ServiceRegistrationImpl( + this, bundle, classNames, m_currentServiceId++, svcObj, dict); + + // Keep track of registered hooks. + addHooks(classNames, svcObj, reg.getReference()); + + // Get the bundles current registered services. + ServiceRegistration[] regs = (ServiceRegistration[]) m_regsMap.get(bundle); + m_regsMap.put(bundle, addServiceRegistration(regs, reg)); + m_regCapSet.addCapability(reg.getReference()); + } + + // Notify callback objects about registered service. + if (m_callbacks != null) + { + m_callbacks.serviceChanged(new ServiceEvent( + ServiceEvent.REGISTERED, reg.getReference()), null); + } + return reg; + } + + public void unregisterService(Bundle bundle, ServiceRegistrationImpl reg) + { + // If this is a hook, it should be removed. + removeHook(reg.getReference()); + + synchronized (this) + { + // Note that we don't lock the service registration here using + // the m_lockedRegsMap because we want to allow bundles to get + // the service during the unregistration process. However, since + // we do remove the registration from the service registry, no + // new bundles will be able to look up the service. + + // Now remove the registered service. + ServiceRegistration[] regs = m_regsMap.get(bundle); + m_regsMap.put(bundle, removeServiceRegistration(regs, reg)); + m_regCapSet.removeCapability(reg.getReference()); + } + + // Notify callback objects about unregistering service. + if (m_callbacks != null) + { + m_callbacks.serviceChanged( + new ServiceEvent(ServiceEvent.UNREGISTERING, reg.getReference()), null); + } + + // Now forcibly unget the service object for all stubborn clients. + synchronized (this) + { + Bundle[] clients = getUsingBundles(reg.getReference()); + for (int i = 0; (clients != null) && (i < clients.length); i++) + { + while (ungetService(clients[i], reg.getReference())) + { + ; // Keep removing until it is no longer possible + } + } + ((ServiceRegistrationImpl) reg).invalidate(); + } + } + + /** + * This method retrieves all services registrations for the specified bundle + * and invokes ServiceRegistration.unregister() on each one. This + * method is only called be the framework to clean up after a stopped + * bundle. + * + * @param bundle the bundle whose services should be unregistered. + */ + public void unregisterServices(Bundle bundle) + { + // Simply remove all service registrations for the bundle. + ServiceRegistration[] regs = null; + synchronized (this) + { + regs = m_regsMap.get(bundle); + } + + // Note, there is no race condition here with respect to the + // bundle registering more services, because its bundle context + // has already been invalidated by this point, so it would not + // be able to register more services. + + // Unregister each service. + for (int i = 0; (regs != null) && (i < regs.length); i++) + { + if (((ServiceRegistrationImpl) regs[i]).isValid()) + { + regs[i].unregister(); + } + } + + // Now remove the bundle itself. + synchronized (this) + { + m_regsMap.remove(bundle); + } + } + + public synchronized Collection> getServiceReferences(String className, SimpleFilter filter) + { + if ((className == null) && (filter == null)) + { + // Return all services. + filter = new SimpleFilter(Constants.OBJECTCLASS, "*", SimpleFilter.PRESENT); + } + else if ((className != null) && (filter == null)) + { + // Return services matching the class name. + filter = new SimpleFilter(Constants.OBJECTCLASS, className, SimpleFilter.EQ); + } + else if ((className != null) && (filter != null)) + { + // Return services matching the class name and filter. + List filters = new ArrayList(2); + filters.add(new SimpleFilter(Constants.OBJECTCLASS, className, SimpleFilter.EQ)); + filters.add(filter); + filter = new SimpleFilter(null, filters, SimpleFilter.AND); + } + // else just use the specified filter. + + Set matches = m_regCapSet.match(filter, false); + + return (Collection) matches; + } + + public synchronized ServiceReference[] getServicesInUse(Bundle bundle) + { + UsageCount[] usages = (UsageCount[]) m_inUseMap.get(bundle); + if (usages != null) + { + ServiceReference[] refs = new ServiceReference[usages.length]; + for (int i = 0; i < refs.length; i++) + { + refs[i] = usages[i].m_ref; + } + return refs; + } + return null; + } + + public S getService(Bundle bundle, ServiceReference ref) + { + UsageCount usage = null; + Object svcObj = null; + + // Get the service registration. + ServiceRegistrationImpl reg = + ((ServiceRegistrationImpl.ServiceReferenceImpl) ref).getRegistration(); + + synchronized (this) + { + // First make sure that no existing operation is currently + // being performed by another thread on the service registration. + for (Object o = m_lockedRegsMap.get(reg); (o != null); o = m_lockedRegsMap.get(reg)) + { + // We don't allow cycles when we call out to the service factory. + if (o.equals(Thread.currentThread())) + { + throw new ServiceException( + "ServiceFactory.getService() resulted in a cycle.", + ServiceException.FACTORY_ERROR, + null); + } + + // Otherwise, wait for it to be freed. + try + { + wait(); + } + catch (InterruptedException ex) + { + } + } + + // Lock the service registration. + m_lockedRegsMap.put(reg, Thread.currentThread()); + + // Make sure the service registration is still valid. + if (reg.isValid()) + { + // Get the usage count, if any. + usage = getUsageCount(bundle, ref); + + // If we don't have a usage count, then create one and + // since the spec says we increment usage count before + // actually getting the service object. + if (usage == null) + { + usage = addUsageCount(bundle, ref); + } + + // Increment the usage count and grab the already retrieved + // service object, if one exists. + usage.m_count++; + svcObj = usage.m_svcObj; + } + } + + // If we have a usage count, but no service object, then we haven't + // cached the service object yet, so we need to create one now without + // holding the lock, since we will potentially call out to a service + // factory. + try + { + if ((usage != null) && (svcObj == null)) + { + svcObj = reg.getService(bundle); + } + } + finally + { + // If we successfully retrieved a service object, then we should + // cache it in the usage count. If not, we should flush the usage + // count. Either way, we need to unlock the service registration + // so that any threads waiting for it can continue. + synchronized (this) + { + // Before caching the service object, double check to see if + // the registration is still valid, since it may have been + // unregistered while we didn't hold the lock. + if (!reg.isValid() || (svcObj == null)) + { + flushUsageCount(bundle, ref); + } + else + { + usage.m_svcObj = svcObj; + } + m_lockedRegsMap.remove(reg); + notifyAll(); + } + } + + return (S) svcObj; + } + + public boolean ungetService(Bundle bundle, ServiceReference ref) + { + UsageCount usage = null; + ServiceRegistrationImpl reg = + ((ServiceRegistrationImpl.ServiceReferenceImpl) ref).getRegistration(); + + synchronized (this) + { + // First make sure that no existing operation is currently + // being performed by another thread on the service registration. + for (Object o = m_lockedRegsMap.get(reg); (o != null); o = m_lockedRegsMap.get(reg)) + { + // We don't allow cycles when we call out to the service factory. + if (o.equals(Thread.currentThread())) + { + throw new IllegalStateException( + "ServiceFactory.ungetService() resulted in a cycle."); + } + + // Otherwise, wait for it to be freed. + try + { + wait(); + } + catch (InterruptedException ex) + { + } + } + + // Get the usage count. + usage = getUsageCount(bundle, ref); + // If there is no cached services, then just return immediately. + if (usage == null) + { + return false; + } + + // Lock the service registration. + m_lockedRegsMap.put(reg, Thread.currentThread()); + } + + // If usage count will go to zero, then unget the service + // from the registration; we do this outside the lock + // since this might call out to the service factory. + try + { + if (usage.m_count == 1) + { + // Remove reference from usages array. + ((ServiceRegistrationImpl.ServiceReferenceImpl) ref) + .getRegistration().ungetService(bundle, usage.m_svcObj); + } + } + finally + { + // Finally, decrement usage count and flush if it goes to zero or + // the registration became invalid while we were not holding the + // lock. Either way, unlock the service registration so that any + // threads waiting for it can continue. + synchronized (this) + { + // Decrement usage count, which spec says should happen after + // ungetting the service object. + usage.m_count--; + + // If the registration is invalid or the usage count has reached + // zero, then flush it. + if (!reg.isValid() || (usage.m_count <= 0)) + { + usage.m_svcObj = null; + flushUsageCount(bundle, ref); + } + + // Release the registration lock so any waiting threads can + // continue. + m_lockedRegsMap.remove(reg); + notifyAll(); + } + } + + return true; + } + + /** + * This is a utility method to release all services being used by the + * specified bundle. + * + * @param bundle the bundle whose services are to be released. + */ + public void ungetServices(Bundle bundle) + { + UsageCount[] usages; + synchronized (this) + { + usages = m_inUseMap.get(bundle); + } + + if (usages == null) + { + return; + } + + // Note, there is no race condition here with respect to the + // bundle using more services, because its bundle context + // has already been invalidated by this point, so it would not + // be able to look up more services. + + // Remove each service object from the + // service cache. + for (UsageCount usage : usages) + { + // Keep ungetting until all usage count is zero. + while (ungetService(bundle, usage.m_ref)) + { + // Empty loop body. + } + } + } + + public synchronized Bundle[] getUsingBundles(ServiceReference ref) + { + Bundle[] bundles = null; + for (Map.Entry entry : m_inUseMap.entrySet()) + { + Bundle bundle = entry.getKey(); + UsageCount[] usages = entry.getValue(); + for (UsageCount usage : usages) + { + if (usage.m_ref.equals(ref)) + { + // Add the bundle to the array to be returned. + if (bundles == null) + { + bundles = new Bundle[]{bundle}; + } + else + { + Bundle[] nbs = new Bundle[bundles.length + 1]; + System.arraycopy(bundles, 0, nbs, 0, bundles.length); + nbs[bundles.length] = bundle; + bundles = nbs; + } + } + } + } + return bundles; + } + + void servicePropertiesModified(ServiceRegistration reg, Dictionary oldProps) + { + updateHook(reg.getReference()); + if (m_callbacks != null) + { + m_callbacks.serviceChanged( + new ServiceEvent(ServiceEvent.MODIFIED, reg.getReference()), oldProps); + } + } + + private static ServiceRegistration[] addServiceRegistration( + ServiceRegistration[] regs, ServiceRegistration reg) + { + if (regs == null) + { + regs = new ServiceRegistration[] + { + reg + }; + } + else + { + ServiceRegistration[] newRegs = new ServiceRegistration[regs.length + 1]; + System.arraycopy(regs, 0, newRegs, 0, regs.length); + newRegs[regs.length] = reg; + regs = newRegs; + } + return regs; + } + + private static ServiceRegistration[] removeServiceRegistration( + ServiceRegistration[] regs, ServiceRegistration reg) + { + for (int i = 0; (regs != null) && (i < regs.length); i++) + { + if (regs[i].equals(reg)) + { + // If this is the only usage, then point to empty list. + if ((regs.length - 1) == 0) + { + regs = new ServiceRegistration[0]; + } + // Otherwise, we need to do some array copying. + else + { + ServiceRegistration[] newRegs = new ServiceRegistration[regs.length - 1]; + System.arraycopy(regs, 0, newRegs, 0, i); + if (i < newRegs.length) + { + System.arraycopy( + regs, i + 1, newRegs, i, newRegs.length - i); + } + regs = newRegs; + } + } + } + return regs; + } + + /** + * Utility method to retrieve the specified bundle's usage count for the + * specified service reference. + * + * @param bundle The bundle whose usage counts are being searched. + * @param ref The service reference to find in the bundle's usage counts. + * @return The associated usage count or null if not found. + */ + private UsageCount getUsageCount(Bundle bundle, ServiceReference ref) + { + UsageCount[] usages = (UsageCount[]) m_inUseMap.get(bundle); + for (int i = 0; (usages != null) && (i < usages.length); i++) + { + if (usages[i].m_ref.equals(ref)) + { + return usages[i]; + } + } + return null; + } + + /** + * Utility method to update the specified bundle's usage count array to + * include the specified service. This method should only be called to add a + * usage count for a previously unreferenced service. If the service already + * has a usage count, then the existing usage count counter simply needs to + * be incremented. + * + * @param bundle The bundle acquiring the service. + * @param ref The service reference of the acquired service. + */ + private UsageCount addUsageCount(Bundle bundle, ServiceReference ref) + { + UsageCount[] usages = m_inUseMap.get(bundle); + + UsageCount usage = new UsageCount(); + usage.m_ref = ref; + + if (usages == null) + { + usages = new UsageCount[] + { + usage + }; + } + else + { + UsageCount[] newUsages = new UsageCount[usages.length + 1]; + System.arraycopy(usages, 0, newUsages, 0, usages.length); + newUsages[usages.length] = usage; + usages = newUsages; + } + + m_inUseMap.put(bundle, usages); + + return usage; + } + + /** + * Utility method to flush the specified bundle's usage count for the + * specified service reference. This should be called to completely remove + * the associated usage count object for the specified service reference. If + * the goal is to simply decrement the usage, then get the usage count and + * decrement its counter. This method will also remove the specified bundle + * from the "in use" map if it has no more usage counts after removing the + * usage count for the specified service reference. + * + * @param bundle The bundle whose usage count should be removed. + * @param ref The service reference whose usage count should be removed. + */ + private void flushUsageCount(Bundle bundle, ServiceReference ref) + { + UsageCount[] usages = (UsageCount[]) m_inUseMap.get(bundle); + for (int i = 0; (usages != null) && (i < usages.length); i++) + { + if (usages[i].m_ref.equals(ref)) + { + // If this is the only usage, then point to empty list. + if ((usages.length - 1) == 0) + { + usages = null; + } + // Otherwise, we need to do some array copying. + else + { + UsageCount[] newUsages = new UsageCount[usages.length - 1]; + System.arraycopy(usages, 0, newUsages, 0, i); + if (i < newUsages.length) + { + System.arraycopy( + usages, i + 1, newUsages, i, newUsages.length - i); + } + usages = newUsages; + } + } + } + + if (usages != null) + { + m_inUseMap.put(bundle, usages); + } + else + { + m_inUseMap.remove(bundle); + } + } + + // + // Hook-related methods. + // + boolean isHookBlackListed(ServiceReference sr) + { + return m_blackList.containsKey(sr); + } + + void blackListHook(ServiceReference sr) + { + m_blackList.put(sr, sr); + } + + static boolean isHook(String[] classNames, Class hookClass, Object svcObj) + { + // For a service factory, we can only match names. + if (svcObj instanceof ServiceFactory) + { + for (String className : classNames) + { + if (className.equals(hookClass.getName())) + { + return true; + } + } + } + + // For a service object, check if its class matches. + if (hookClass.isAssignableFrom(svcObj.getClass())) + { + // But still only if it is registered under that interface. + String hookName = hookClass.getName(); + for (String className : classNames) + { + if (className.equals(hookName)) + { + return true; + } + } + } + return false; + } + + private void addHooks(String[] classNames, Object svcObj, ServiceReference ref) + { + for (Class hookClass : m_hookClasses) + { + if (isHook(classNames, hookClass, svcObj)) + { + synchronized (m_allHooks) + { + Set> hooks = m_allHooks.get(hookClass); + if (hooks == null) + { + hooks = new TreeSet>(Collections.reverseOrder()); + m_allHooks.put(hookClass, hooks); + } + hooks.add(ref); + } + } + } + } + + private void updateHook(ServiceReference ref) + { + // We maintain the hooks sorted, so if ranking has changed for example, + // we need to ensure the order remains correct by resorting the hooks. + Object svcObj = ((ServiceRegistrationImpl.ServiceReferenceImpl) ref) + .getRegistration().getService(); + String[] classNames = (String[]) ref.getProperty(Constants.OBJECTCLASS); + + for (Class hookClass : m_hookClasses) + { + if (isHook(classNames, hookClass, svcObj)) + { + synchronized (m_allHooks) + { + Set> hooks = m_allHooks.get(hookClass); + if (hooks != null) + { + List> refs = new ArrayList>(hooks); + hooks.clear(); + hooks.addAll(refs); + } + } + } + } + } + + private void removeHook(ServiceReference ref) + { + Object svcObj = ((ServiceRegistrationImpl.ServiceReferenceImpl) ref) + .getRegistration().getService(); + String[] classNames = (String[]) ref.getProperty(Constants.OBJECTCLASS); + + for (Class hookClass : m_hookClasses) + { + if (isHook(classNames, hookClass, svcObj)) + { + synchronized (m_allHooks) + { + Set> hooks = m_allHooks.get(hookClass); + if (hooks != null) + { + hooks.remove(ref); + if (hooks.isEmpty()) + { + m_allHooks.remove(hookClass); + } + } + } + } + } + } + + public Set> getHooks(Class hookClass) + { + synchronized (m_allHooks) + { + @SuppressWarnings("unchecked") + Set> hooks = (Set) m_allHooks.get(hookClass); + if (hooks != null) + { + SortedSet> sorted = new TreeSet>(Collections.reverseOrder()); + sorted.addAll(hooks); + return sorted; + } + return Collections.emptySet(); + } + } + + private static class UsageCount + { + public int m_count = 0; + public ServiceReference m_ref = null; + public Object m_svcObj = null; + } + + public interface ServiceRegistryCallbacks + { + void serviceChanged(ServiceEvent event, Dictionary oldProps); + } +} \ No newline at end of file diff --git a/connect/src/main/java/org/apache/felix/connect/felix/framework/capabilityset/CapabilitySet.java b/connect/src/main/java/org/apache/felix/connect/felix/framework/capabilityset/CapabilitySet.java new file mode 100644 index 00000000000..cfeccefa0f6 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/felix/framework/capabilityset/CapabilitySet.java @@ -0,0 +1,556 @@ +/* + * 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.connect.felix.framework.capabilityset; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; + +import org.osgi.resource.Capability; + +import org.apache.felix.connect.felix.framework.util.StringComparator; + +public class CapabilitySet +{ + private final Map>> m_indices; + private final Set m_capSet = new HashSet(); + + public CapabilitySet(List indexProps, boolean caseSensitive) + { + m_indices = (caseSensitive) + ? new TreeMap>>() + : new TreeMap>>( + new StringComparator(false)); + for (int i = 0; (indexProps != null) && (i < indexProps.size()); i++) + { + m_indices.put( + indexProps.get(i), new HashMap>()); + } + } + + public void addCapability(T cap) + { + m_capSet.add(cap); + + // Index capability. + for (Entry>> entry : m_indices.entrySet()) + { + Object value = cap.getAttributes().get(entry.getKey()); + if (value != null) + { + if (value.getClass().isArray()) + { + value = convertArrayToList(value); + } + + Map> index = entry.getValue(); + + if (value instanceof Collection) + { + Collection c = (Collection) value; + for (Object o : c) + { + indexCapability(index, cap, o); + } + } + else + { + indexCapability(index, cap, value); + } + } + } + } + + private void indexCapability( + Map> index, T cap, Object capValue) + { + Set caps = index.get(capValue); + if (caps == null) + { + caps = new HashSet(); + index.put(capValue, caps); + } + caps.add(cap); + } + + public void removeCapability(T cap) + { + if (m_capSet.remove(cap)) + { + for (Entry>> entry : m_indices.entrySet()) + { + Object value = cap.getAttributes().get(entry.getKey()); + if (value != null) + { + if (value.getClass().isArray()) + { + value = convertArrayToList(value); + } + + Map> index = entry.getValue(); + + if (value instanceof Collection) + { + Collection c = (Collection) value; + for (Object o : c) + { + deindexCapability(index, cap, o); + } + } + else + { + deindexCapability(index, cap, value); + } + } + } + } + } + + private void deindexCapability( + Map> index, T cap, Object value) + { + Set caps = index.get(value); + if (caps != null) + { + caps.remove(cap); + if (caps.isEmpty()) + { + index.remove(value); + } + } + } + + public Set match(SimpleFilter sf, boolean obeyMandatory) + { + Set matches = match(m_capSet, sf); + return /* (obeyMandatory) + ? matchMandatory(matches, sf) + : */ matches; + } + + @SuppressWarnings("unchecked") + private Set match(Set caps, SimpleFilter sf) + { + Set matches = new HashSet(); + + if (sf.getOperation() == SimpleFilter.MATCH_ALL) + { + matches.addAll(caps); + } + else if (sf.getOperation() == SimpleFilter.AND) + { + // Evaluate each subfilter against the remaining capabilities. + // For AND we calculate the intersection of each subfilter. + // We can short-circuit the AND operation if there are no + // remaining capabilities. + List sfs = (List) sf.getValue(); + for (int i = 0; (caps.size() > 0) && (i < sfs.size()); i++) + { + matches = match(caps, sfs.get(i)); + caps = matches; + } + } + else if (sf.getOperation() == SimpleFilter.OR) + { + // Evaluate each subfilter against the remaining capabilities. + // For OR we calculate the union of each subfilter. + List sfs = (List) sf.getValue(); + for (SimpleFilter sf1 : sfs) + { + matches.addAll(match(caps, sf1)); + } + } + else if (sf.getOperation() == SimpleFilter.NOT) + { + // Evaluate each subfilter against the remaining capabilities. + // For OR we calculate the union of each subfilter. + matches.addAll(caps); + List sfs = (List) sf.getValue(); + for (SimpleFilter sf1 : sfs) + { + matches.removeAll(match(caps, sf1)); + } + } + else + { + Map> index = m_indices.get(sf.getName()); + if ((sf.getOperation() == SimpleFilter.EQ) && (index != null)) + { + Set existingCaps = index.get(sf.getValue()); + if (existingCaps != null) + { + matches.addAll(existingCaps); + matches.retainAll(caps); + } + } + else + { + for (T cap : caps) + { + Object lhs = cap.getAttributes().get(sf.getName()); + if (lhs != null) + { + if (compare(lhs, sf.getValue(), sf.getOperation())) + { + matches.add(cap); + } + } + } + } + } + + return matches; + } + + /* public static boolean matches(BundleCapability cap, SimpleFilter sf) + { + return matchesInternal(cap, sf) && matchMandatory(cap, sf); + } + */ + @SuppressWarnings("unchecked") + private boolean matchesInternal(T cap, SimpleFilter sf) + { + boolean matched = true; + + if (sf.getOperation() == SimpleFilter.MATCH_ALL) + { + matched = true; + } + else if (sf.getOperation() == SimpleFilter.AND) + { + // Evaluate each subfilter against the remaining capabilities. + // For AND we calculate the intersection of each subfilter. + // We can short-circuit the AND operation if there are no + // remaining capabilities. + List sfs = (List) sf.getValue(); + for (int i = 0; matched && (i < sfs.size()); i++) + { + matched = matchesInternal(cap, sfs.get(i)); + } + } + else if (sf.getOperation() == SimpleFilter.OR) + { + // Evaluate each subfilter against the remaining capabilities. + // For OR we calculate the union of each subfilter. + matched = false; + List sfs = (List) sf.getValue(); + for (int i = 0; !matched && (i < sfs.size()); i++) + { + matched = matchesInternal(cap, sfs.get(i)); + } + } + else if (sf.getOperation() == SimpleFilter.NOT) + { + // Evaluate each subfilter against the remaining capabilities. + // For OR we calculate the union of each subfilter. + List sfs = (List) sf.getValue(); + for (SimpleFilter sf1 : sfs) + { + matched = !(matchesInternal(cap, sf1)); + } + } + else + { + matched = false; + Object lhs = cap.getAttributes().get(sf.getName()); + if (lhs != null) + { + matched = compare(lhs, sf.getValue(), sf.getOperation()); + } + } + + return matched; + } + + /* + private Set matchMandatory( + Set caps, SimpleFilter sf) + { + for (Iterator it = caps.iterator(); it.hasNext(); ) + { + T cap = it.next(); + if (!matchMandatory(cap, sf)) + { + it.remove(); + } + } + return caps; + } + + private boolean matchMandatory(T cap, SimpleFilter sf) + { + Map attrs = cap.getAttributes(); + for (Entry entry : attrs.entrySet()) + { + if (((T) cap).isAttributeMandatory(entry.getKey()) + && !matchMandatoryAttrbute(entry.getKey(), sf)) + { + return false; + } + } + return true; + } + + private boolean matchMandatoryAttrbute(String attrName, SimpleFilter sf) + { + if ((sf.getName() != null) && sf.getName().equals(attrName)) + { + return true; + } + else if (sf.getOperation() == SimpleFilter.AND) + { + List list = (List) sf.getValue(); + for (int i = 0; i < list.size(); i++) + { + SimpleFilter sf2 = (SimpleFilter) list.get(i); + if ((sf2.getName() != null) + && sf2.getName().equals(attrName)) + { + return true; + } + } + } + return false; + }*/ + + private static final Class[] STRING_CLASS = new Class[]{String.class}; + + @SuppressWarnings("unchecked") + private static boolean compare(Object lhs, Object rhsUnknown, int op) + { + if (lhs == null) + { + return false; + } + + // If this is a PRESENT operation, then just return true immediately + // since we wouldn't be here if the attribute wasn't present. + if (op == SimpleFilter.PRESENT) + { + return true; + } + + // If the type is comparable, then we can just return the + // result immediately. + if (lhs instanceof Comparable) + { + // Spec says SUBSTRING is false for all types other than string. + if ((op == SimpleFilter.SUBSTRING) && !(lhs instanceof String)) + { + return false; + } + + Object rhs; + if (op == SimpleFilter.SUBSTRING) + { + rhs = rhsUnknown; + } + else + { + try + { + rhs = coerceType(lhs, (String) rhsUnknown); + } + catch (Exception ex) + { + return false; + } + } + + switch (op) + { + case SimpleFilter.EQ: + try + { + return (((Comparable) lhs).compareTo(rhs) == 0); + } + catch (Exception ex) + { + return false; + } + case SimpleFilter.GTE: + try + { + return (((Comparable) lhs).compareTo(rhs) >= 0); + } + catch (Exception ex) + { + return false; + } + case SimpleFilter.LTE: + try + { + return (((Comparable) lhs).compareTo(rhs) <= 0); + } + catch (Exception ex) + { + return false; + } + case SimpleFilter.APPROX: + return compareApproximate(lhs, rhs); + case SimpleFilter.SUBSTRING: + return SimpleFilter.compareSubstring((List) rhs, (String) lhs); + default: + throw new RuntimeException( + "Unknown comparison operator: " + op); + } + } + + + // If the LHS is not a comparable or boolean, check if it is an + // array. If so, convert it to a list so we can treat it as a + // collection. + if (lhs.getClass().isArray()) + { + lhs = convertArrayToList(lhs); + } + + // If LHS is a collection, then call compare() on each element + // of the collection until a match is found. + if (lhs instanceof Collection) + { + for (Object o : ((Collection) lhs)) + { + if (compare(o, rhsUnknown, op)) + { + return true; + } + } + + return false; + } + + // Spec says SUBSTRING is false for all types other than string. + if (op == SimpleFilter.SUBSTRING) + { + return false; + } + + // Since we cannot identify the LHS type, then we can only perform + // equality comparison. + try + { + return lhs.equals(coerceType(lhs, (String) rhsUnknown)); + } + catch (Exception ex) + { + return false; + } + } + + private static boolean compareApproximate(Object lhs, Object rhs) + { + if (rhs instanceof String) + { + return removeWhitespace((String) lhs) + .equalsIgnoreCase(removeWhitespace((String) rhs)); + } + else if (rhs instanceof Character) + { + return Character.toLowerCase(((Character) lhs)) + == Character.toLowerCase(((Character) rhs)); + } + return lhs.equals(rhs); + } + + private static String removeWhitespace(String s) + { + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) + { + if (!Character.isWhitespace(s.charAt(i))) + { + sb.append(s.charAt(i)); + } + } + return sb.toString(); + } + + private static Object coerceType(Object lhs, String rhsString) throws Exception + { + // If the LHS expects a string, then we can just return + // the RHS since it is a string. + if (lhs.getClass() == rhsString.getClass()) + { + return rhsString; + } + + // Try to convert the RHS type to the LHS type by using + // the string constructor of the LHS class, if it has one. + Object rhs; + try + { + // The Character class is a special case, since its constructor + // does not take a string, so handle it separately. + if (lhs instanceof Character) + { + rhs = rhsString.charAt(0); + } + else + { + // Spec says we should trim number types. + if ((lhs instanceof Number) || (lhs instanceof Boolean)) + { + rhsString = rhsString.trim(); + } + Constructor ctor = lhs.getClass().getConstructor(STRING_CLASS); + ctor.setAccessible(true); + rhs = ctor.newInstance(rhsString); + } + } + catch (Exception ex) + { + throw new Exception( + "Could not instantiate class " + + lhs.getClass().getName() + + " from string constructor with argument '" + + rhsString + "' because " + ex); + } + + return rhs; + } + + /** + * This is an ugly utility method to convert an array of primitives to an + * array of primitive wrapper objects. This method simplifies processing + * LDAP filters since the special case of primitive arrays can be ignored. + * + * @param array An array of primitive types. + * @return An corresponding array using pritive wrapper objects. + */ + private static List convertArrayToList(Object array) + { + int len = Array.getLength(array); + List list = new ArrayList(len); + for (int i = 0; i < len; i++) + { + list.add(Array.get(array, i)); + } + return list; + } +} \ No newline at end of file diff --git a/connect/src/main/java/org/apache/felix/connect/felix/framework/capabilityset/SimpleFilter.java b/connect/src/main/java/org/apache/felix/connect/felix/framework/capabilityset/SimpleFilter.java new file mode 100644 index 00000000000..c8f031000b4 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/felix/framework/capabilityset/SimpleFilter.java @@ -0,0 +1,541 @@ +/* + * 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.connect.felix.framework.capabilityset; + +import java.util.ArrayList; +import java.util.List; + +public class SimpleFilter +{ + public static final int MATCH_ALL = 0; + public static final int AND = 1; + public static final int OR = 2; + public static final int NOT = 3; + public static final int EQ = 4; + public static final int LTE = 5; + public static final int GTE = 6; + public static final int SUBSTRING = 7; + public static final int PRESENT = 8; + public static final int APPROX = 9; + + private final String m_name; + private final Object m_value; + private final int m_op; + + public SimpleFilter(String attr, Object value, int op) + { + m_name = attr; + m_value = value; + m_op = op; + } + + public String getName() + { + return m_name; + } + + public Object getValue() + { + return m_value; + } + + public int getOperation() + { + return m_op; + } + + @SuppressWarnings("unchecked") + public String toString() + { + String s = null; + switch (m_op) + { + case AND: + s = "(&" + toString((List) m_value) + ")"; + break; + case OR: + s = "(|" + toString((List) m_value) + ")"; + break; + case NOT: + s = "(!" + toString((List) m_value) + ")"; + break; + case EQ: + s = "(" + m_name + "=" + toEncodedString(m_value) + ")"; + break; + case LTE: + s = "(" + m_name + "<=" + toEncodedString(m_value) + ")"; + break; + case GTE: + s = "(" + m_name + ">=" + toEncodedString(m_value) + ")"; + break; + case SUBSTRING: + s = "(" + m_name + "=" + unparseSubstring((List) m_value) + ")"; + break; + case PRESENT: + s = "(" + m_name + "=*)"; + break; + case APPROX: + s = "(" + m_name + "~=" + toEncodedString(m_value) + ")"; + break; + } + return s; + } + + private static String toString(List list) + { + StringBuilder sb = new StringBuilder(); + for (Object aList : list) + { + sb.append(aList.toString()); + } + return sb.toString(); + } + + private static String toDecodedString(String s, int startIdx, int endIdx) + { + StringBuilder sb = new StringBuilder(endIdx - startIdx); + boolean escaped = false; + for (int i = 0; i < (endIdx - startIdx); i++) + { + char c = s.charAt(startIdx + i); + if (!escaped && (c == '\\')) + { + escaped = true; + } + else + { + escaped = false; + sb.append(c); + } + } + + return sb.toString(); + } + + private static String toEncodedString(Object o) + { + if (o instanceof String) + { + String s = (String) o; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) + { + char c = s.charAt(i); + if ((c == '\\') || (c == '(') || (c == ')') || (c == '*')) + { + sb.append('\\'); + } + sb.append(c); + } + + o = sb.toString(); + } + + return o.toString(); + } + + @SuppressWarnings("unchecked") + public static SimpleFilter parse(String filter) + { + int idx = skipWhitespace(filter, 0); + + if ((filter == null) || (filter.length() == 0) + || (idx >= filter.length())) + { + throw new IllegalArgumentException("Null or empty filter."); + } + else if (filter.charAt(idx) != '(') + { + throw new IllegalArgumentException("Missing opening parenthesis: " + + filter); + } + + SimpleFilter sf = null; + List stack = new ArrayList(); + boolean isEscaped = false; + while (idx < filter.length()) + { + if (sf != null) + { + throw new IllegalArgumentException( + "Only one top-level operation allowed: " + filter); + } + + if (!isEscaped && (filter.charAt(idx) == '(')) + { + // Skip paren and following whitespace. + idx = skipWhitespace(filter, idx + 1); + + if (filter.charAt(idx) == '&') + { + int peek = skipWhitespace(filter, idx + 1); + if (filter.charAt(peek) == '(') + { + idx = peek - 1; + stack.add(0, new SimpleFilter(null, new ArrayList(), + SimpleFilter.AND)); + } + else + { + stack.add(0, idx); + } + } + else if (filter.charAt(idx) == '|') + { + int peek = skipWhitespace(filter, idx + 1); + if (filter.charAt(peek) == '(') + { + idx = peek - 1; + stack.add(0, new SimpleFilter(null, new ArrayList(), + SimpleFilter.OR)); + } + else + { + stack.add(0, idx); + } + } + else if (filter.charAt(idx) == '!') + { + int peek = skipWhitespace(filter, idx + 1); + if (filter.charAt(peek) == '(') + { + idx = peek - 1; + stack.add(0, new SimpleFilter(null, new ArrayList(), + SimpleFilter.NOT)); + } + else + { + stack.add(0, idx); + } + } + else + { + stack.add(0, idx); + } + } + else if (!isEscaped && (filter.charAt(idx) == ')')) + { + Object top = stack.remove(0); + if (top instanceof SimpleFilter) + { + if (!stack.isEmpty() && (stack.get(0) instanceof SimpleFilter)) + { + ((List) ((SimpleFilter) stack.get(0)).m_value).add(top); + } + else + { + sf = (SimpleFilter) top; + } + } + else if (!stack.isEmpty() && (stack.get(0) instanceof SimpleFilter)) + { + ((List) ((SimpleFilter) stack.get(0)).m_value) + .add(SimpleFilter.subfilter(filter, (Integer) top, idx)); + } + else + { + sf = SimpleFilter.subfilter(filter, (Integer) top, idx); + } + } + else + { + isEscaped = !isEscaped && (filter.charAt(idx) == '\\'); + } + + idx = skipWhitespace(filter, idx + 1); + } + + if (sf == null) + { + throw new IllegalArgumentException("Missing closing parenthesis: " + filter); + } + + return sf; + } + + private static SimpleFilter subfilter(String filter, int startIdx, + int endIdx) + { + final String opChars = "=<>~"; + + // Determine the ending index of the attribute name. + int attrEndIdx = startIdx; + for (int i = 0; i < (endIdx - startIdx); i++) + { + char c = filter.charAt(startIdx + i); + if (opChars.indexOf(c) >= 0) + { + break; + } + else if (!Character.isWhitespace(c)) + { + attrEndIdx = startIdx + i + 1; + } + } + if (attrEndIdx == startIdx) + { + throw new IllegalArgumentException("Missing attribute name: " + + filter.substring(startIdx, endIdx)); + } + String attr = filter.substring(startIdx, attrEndIdx); + + // Skip the attribute name and any following whitespace. + startIdx = skipWhitespace(filter, attrEndIdx); + + // Determine the operator type. + int op; + switch (filter.charAt(startIdx)) + { + case '=': + op = EQ; + startIdx++; + break; + case '<': + if (filter.charAt(startIdx + 1) != '=') + { + throw new IllegalArgumentException("Unknown operator: " + + filter.substring(startIdx, endIdx)); + } + op = LTE; + startIdx += 2; + break; + case '>': + if (filter.charAt(startIdx + 1) != '=') + { + throw new IllegalArgumentException("Unknown operator: " + + filter.substring(startIdx, endIdx)); + } + op = GTE; + startIdx += 2; + break; + case '~': + if (filter.charAt(startIdx + 1) != '=') + { + throw new IllegalArgumentException("Unknown operator: " + + filter.substring(startIdx, endIdx)); + } + op = APPROX; + startIdx += 2; + break; + default: + throw new IllegalArgumentException("Unknown operator: " + + filter.substring(startIdx, endIdx)); + } + + // Parse value. + Object value = toDecodedString(filter, startIdx, endIdx); + + // Check if the equality comparison is actually a substring + // or present operation. + if (op == EQ) + { + String valueStr = filter.substring(startIdx, endIdx); + List values = parseSubstring(valueStr); + if ((values.size() == 2) && (values.get(0).length() == 0) + && (values.get(1).length() == 0)) + { + op = PRESENT; + } + else if (values.size() > 1) + { + op = SUBSTRING; + value = values; + } + } + + return new SimpleFilter(attr, value, op); + } + + public static List parseSubstring(String value) + { + List pieces = new ArrayList(); + StringBuilder ss = new StringBuilder(); + // int kind = SIMPLE; // assume until proven otherwise + boolean wasStar = false; // indicates last piece was a star + boolean leftstar = false; // track if the initial piece is a star + boolean rightstar = false; // track if the final piece is a star + + int idx = 0; + + // We assume (sub)strings can contain leading and trailing blanks + boolean escaped = false; + for (; ; ) + { + if (idx >= value.length()) + { + if (wasStar) + { + // insert last piece as "" to handle trailing star + rightstar = true; + } + else + { + pieces.add(ss.toString()); + // accumulate the last piece + // note that in the case of + // (cn=); this might be + // the string "" (!=null) + } + ss.setLength(0); + break; + } + + // Read the next character and account for escapes. + char c = value.charAt(idx++); + if (!escaped && ((c == '(') || (c == ')'))) + { + throw new IllegalArgumentException("Illegal value: " + value); + } + else if (!escaped && (c == '*')) + { + if (wasStar) + { + // encountered two successive stars; + // I assume this is illegal + throw new IllegalArgumentException( + "Invalid filter string: " + value); + } + if (ss.length() > 0) + { + pieces.add(ss.toString()); // accumulate the pieces + // between '*' occurrences + } + ss.setLength(0); + // if this is a leading star, then track it + if (pieces.size() == 0) + { + leftstar = true; + } + wasStar = true; + } + else if (!escaped && (c == '\\')) + { + escaped = true; + } + else + { + escaped = false; + wasStar = false; + ss.append(c); + } + } + if (leftstar || rightstar || pieces.size() > 1) + { + // insert leading and/or trailing "" to anchor ends + if (rightstar) + { + pieces.add(""); + } + if (leftstar) + { + pieces.add(0, ""); + } + } + return pieces; + } + + public static String unparseSubstring(List pieces) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < pieces.size(); i++) + { + if (i > 0) + { + sb.append("*"); + } + sb.append(toEncodedString(pieces.get(i))); + } + return sb.toString(); + } + + public static boolean compareSubstring(List pieces, String s) + { + // Walk the pieces to match the string + // There are implicit stars between each piece, + // and the first and last pieces might be "" to anchor the match. + // assert (pieces.length > 1) + // minimal case is * + + boolean result = true; + int len = pieces.size(); + + // Special case, if there is only one piece, then + // we must perform an equality test. + if (len == 1) + { + return s.equals(pieces.get(0)); + } + + // Otherwise, check whether the pieces match + // the specified string. + + int index = 0; + + for (int i = 0; i < len; i++) + { + String piece = pieces.get(i); + + // If this is the first piece, then make sure the + // string starts with it. + if (i == 0) + { + if (!s.startsWith(piece)) + { + result = false; + break; + } + } + + // If this is the last piece, then make sure the + // string ends with it. + if (i == len - 1) + { + result = s.endsWith(piece); + break; + } + + // If this is neither the first or last piece, then + // make sure the string contains it. + if ((i > 0) && (i < (len - 1))) + { + index = s.indexOf(piece, index); + if (index < 0) + { + result = false; + break; + } + } + + // Move string index beyond the matching piece. + index += piece.length(); + } + + return result; + } + + private static int skipWhitespace(String s, int startIdx) + { + int len = s.length(); + while ((startIdx < len) && Character.isWhitespace(s.charAt(startIdx))) + { + startIdx++; + } + return startIdx; + } +} \ No newline at end of file diff --git a/connect/src/main/java/org/apache/felix/connect/felix/framework/util/EventDispatcher.java b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/EventDispatcher.java new file mode 100644 index 00000000000..1e72b703584 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/EventDispatcher.java @@ -0,0 +1,1019 @@ +/* + * 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.connect.felix.framework.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.EventListener; +import java.util.EventObject; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.BundleListener; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.SynchronousBundleListener; +import org.osgi.framework.hooks.bundle.EventHook; +import org.osgi.framework.hooks.service.EventListenerHook; +import org.osgi.framework.hooks.service.ListenerHook; +import org.osgi.framework.launch.Framework; + +import org.apache.felix.connect.felix.framework.ServiceRegistry; + +public class EventDispatcher +{ + private final ServiceRegistry m_registry; + private Map> m_fwkListeners = Collections.emptyMap(); + private Map> m_bndlListeners = Collections.emptyMap(); + private Map> m_syncBndlListeners = Collections.emptyMap(); + private Map> m_svcListeners = Collections.emptyMap(); + // A single thread is used to deliver events for all dispatchers. + private static Thread m_thread = null; + private final static String m_threadLock = "thread lock"; + private static int m_references = 0; + private static volatile boolean m_stopping = false; + // List of requests. + private static final List m_requestList = new ArrayList(); + // Pooled requests to avoid memory allocation. + private static final List m_requestPool = new ArrayList(); + + private static final boolean m_sync = "true".equalsIgnoreCase(System + .getProperty("org.apache.felix.connect.events.sync")); + + public EventDispatcher(ServiceRegistry registry) + { + m_registry = registry; + } + + public void startDispatching() + { + synchronized (m_threadLock) + { + // Start event dispatching thread if necessary. + if (m_thread == null || !m_thread.isAlive()) + { + m_stopping = false; + + if (!m_sync) + { + m_thread = new Thread(new Runnable() + { + public void run() + { + try + { + EventDispatcher.run(); + } + finally + { + // Ensure we update state even if stopped by + // external cause + // e.g. an Applet VM forceably killing threads + synchronized (m_threadLock) + { + m_thread = null; + m_stopping = false; + m_references = 0; + m_threadLock.notifyAll(); + } + } + } + }, "FelixDispatchQueue"); + m_thread.start(); + } + } + + // reference counting and flags + m_references++; + } + } + + public void stopDispatching() + { + synchronized (m_threadLock) + { + // Return if already dead or stopping. + if (m_thread == null || m_stopping) + { + return; + } + + // decrement use counter, don't continue if there are users + m_references--; + if (m_references > 0) + { + return; + } + + m_stopping = true; + } + + // Signal dispatch thread. + synchronized (m_requestList) + { + m_requestList.notify(); + } + + // Use separate lock for shutdown to prevent any chance of nested lock + // deadlock + synchronized (m_threadLock) + { + while (m_thread != null) + { + try + { + m_threadLock.wait(); + } + catch (InterruptedException ex) + { + // Ignore + } + } + } + } + + public Filter addListener(BundleContext bc, Class clazz, EventListener l, Filter filter) + { + // Verify the listener. + if (l == null) + { + throw new IllegalArgumentException("Listener is null"); + } + else if (!clazz.isInstance(l)) + { + throw new IllegalArgumentException("Listener not of type " + clazz.getName()); + } + + // See if we can simply update the listener, if so then + // return immediately. + Filter oldFilter = updateListener(bc, clazz, l, filter); + if (oldFilter != null) + { + return oldFilter; + } + + // Lock the object to add the listener. + synchronized (this) + { + // Verify that the bundle context is still valid. + try + { + bc.getBundle(); + } + catch (IllegalStateException ex) + { + // Bundle context is no longer valid, so just return. + } + + Map> listeners; + Object acc = null; + + if (clazz == FrameworkListener.class) + { + listeners = m_fwkListeners; + } + else if (clazz == BundleListener.class) + { + if (SynchronousBundleListener.class.isInstance(l)) + { + listeners = m_syncBndlListeners; + } + else + { + listeners = m_bndlListeners; + } + } + else if (clazz == ServiceListener.class) + { + // Remember security context for filtering service events. + /* Object sm = System.getSecurityManager(); + if (sm != null) + { + acc = ((SecurityManager) sm).getSecurityContext(); + }*/ + // We need to create a Set for keeping track of matching service + // registrations so we can fire ServiceEvent.MODIFIED_ENDMATCH + // events. We need a Set even if filter is null, since the + // listener can be updated and have a filter added later. + listeners = m_svcListeners; + } + else + { + throw new IllegalArgumentException("Unknown listener: " + l.getClass()); + } + + // Add listener. + ListenerInfo info = + new ListenerInfo(bc.getBundle(), bc, clazz, l, filter, acc, false); + listeners = addListenerInfo(listeners, info); + + if (clazz == FrameworkListener.class) + { + m_fwkListeners = listeners; + } + else if (clazz == BundleListener.class) + { + if (SynchronousBundleListener.class.isInstance(l)) + { + m_syncBndlListeners = listeners; + } + else + { + m_bndlListeners = listeners; + } + } + else if (clazz == ServiceListener.class) + { + m_svcListeners = listeners; + } + } + return null; + } + + public ListenerHook.ListenerInfo removeListener( + BundleContext bc, Class clazz, EventListener l) + { + ListenerHook.ListenerInfo returnInfo = null; + + // Verify listener. + if (l == null) + { + throw new IllegalArgumentException("Listener is null"); + } + else if (!clazz.isInstance(l)) + { + throw new IllegalArgumentException( + "Listener not of type " + clazz.getName()); + } + + // Lock the object to remove the listener. + synchronized (this) + { + Map> listeners; + + if (clazz == FrameworkListener.class) + { + listeners = m_fwkListeners; + } + else if (clazz == BundleListener.class) + { + if (SynchronousBundleListener.class.isInstance(l)) + { + listeners = m_syncBndlListeners; + } + else + { + listeners = m_bndlListeners; + } + } + else if (clazz == ServiceListener.class) + { + listeners = m_svcListeners; + } + else + { + throw new IllegalArgumentException("Unknown listener: " + l.getClass()); + } + + // Try to find the instance in our list. + int idx = -1; + for (Entry> entry : listeners.entrySet()) + { + List infos = entry.getValue(); + for (int i = 0; i < infos.size(); i++) + { + ListenerInfo info = infos.get(i); + if (info.getBundleContext().equals(bc) + && (info.getListenerClass() == clazz) + && (info.getListener() == l)) + { + // For service listeners, we must return some info about + // the listener for the ListenerHook callback. + if (ServiceListener.class == clazz) + { + returnInfo = new ListenerInfo(infos.get(i), true); + } + idx = i; + break; + } + } + } + + // If we have the instance, then remove it. + if (idx >= 0) + { + listeners = removeListenerInfo(listeners, bc, idx); + } + + if (clazz == FrameworkListener.class) + { + m_fwkListeners = listeners; + } + else if (clazz == BundleListener.class) + { + if (SynchronousBundleListener.class.isInstance(l)) + { + m_syncBndlListeners = listeners; + } + else + { + m_bndlListeners = listeners; + } + } + else if (clazz == ServiceListener.class) + { + m_svcListeners = listeners; + } + } + + // Return information about the listener; this is null + // for everything but service listeners. + return returnInfo; + } + + public void removeListeners(BundleContext bc) + { + if (bc == null) + { + return; + } + + synchronized (this) + { + // Remove all framework listeners associated with the specified bundle. + m_fwkListeners = removeListenerInfos(m_fwkListeners, bc); + + // Remove all bundle listeners associated with the specified bundle. + m_bndlListeners = removeListenerInfos(m_bndlListeners, bc); + + // Remove all synchronous bundle listeners associated with + // the specified bundle. + m_syncBndlListeners = removeListenerInfos(m_syncBndlListeners, bc); + + // Remove all service listeners associated with the specified bundle. + m_svcListeners = removeListenerInfos(m_svcListeners, bc); + } + } + + public Filter updateListener(BundleContext bc, Class clazz, EventListener l, Filter filter) + { + if (clazz == ServiceListener.class) + { + synchronized (this) + { + // Verify that the bundle context is still valid. + try + { + bc.getBundle(); + } + catch (IllegalStateException ex) + { + // Bundle context is no longer valid, so just return. + } + + // See if the service listener is already registered; if so then + // update its filter per the spec. + List infos = m_svcListeners.get(bc); + for (int i = 0; (infos != null) && (i < infos.size()); i++) + { + ListenerInfo info = infos.get(i); + if (info.getBundleContext().equals(bc) + && (info.getListenerClass() == clazz) + && (info.getListener() == l)) + { + // The spec says to update the filter in this case. + Filter oldFilter = info.getParsedFilter(); + ListenerInfo newInfo = new ListenerInfo( + info.getBundle(), + info.getBundleContext(), + info.getListenerClass(), + info.getListener(), + filter, + info.getSecurityContext(), + info.isRemoved()); + m_svcListeners = updateListenerInfo(m_svcListeners, i, newInfo); + return oldFilter; + } + } + } + } + + return null; + } + + /** + * Returns all existing service listener information into a List of + * ListenerHook.ListenerInfo objects. This is used the first time a listener + * hook is registered to synchronize it with the existing set of listeners. + * + * @return Returns all existing service listener information into a + * List of ListenerHook.ListenerInfo objects + */ + public List getAllServiceListeners() + { + List listeners = new ArrayList(); + synchronized (this) + { + for (Entry> entry : m_svcListeners.entrySet()) + { + listeners.addAll(entry.getValue()); + } + } + return listeners; + } + + public void fireFrameworkEvent(FrameworkEvent event) + { + // Take a snapshot of the listener array. + Map> listeners; + synchronized (this) + { + listeners = m_fwkListeners; + } + + // Fire all framework listeners on a separate thread. + fireEventAsynchronously(this, Request.FRAMEWORK_EVENT, listeners, event); + } + + public void fireBundleEvent(BundleEvent event) + { + // Take a snapshot of the listener array. + Map> listeners; + Map> syncListeners; + synchronized (this) + { + listeners = m_bndlListeners; + syncListeners = m_syncBndlListeners; + } + + // Create a whitelist of bundle context for bundle listeners, + // if we have hooks. + Set whitelist = createWhitelistFromHooks(event, event.getBundle(), + listeners.keySet(), syncListeners.keySet()); + + // If we have a whitelist, then create copies of only the whitelisted + // listeners. + if (whitelist != null) + { + Map> copy = new HashMap>(); + for (BundleContext bc : whitelist) + { + List infos = listeners.get(bc); + if (infos != null) + { + copy.put(bc, infos); + } + } + listeners = copy; + copy = new HashMap>(); + for (BundleContext bc : whitelist) + { + List infos = syncListeners.get(bc); + if (infos != null) + { + copy.put(bc, infos); + } + } + syncListeners = copy; + } + + // Fire synchronous bundle listeners immediately on the calling thread. + fireEventImmediately(this, Request.BUNDLE_EVENT, syncListeners, event, null); + + // The spec says that asynchronous bundle listeners do not get events + // of types STARTING, STOPPING, or LAZY_ACTIVATION. + if ((event.getType() != BundleEvent.STARTING) + && (event.getType() != BundleEvent.STOPPING) + && (event.getType() != BundleEvent.LAZY_ACTIVATION)) + { + // Fire asynchronous bundle listeners on a separate thread. + fireEventAsynchronously(this, Request.BUNDLE_EVENT, listeners, event); + } + } + + private Set createWhitelistFromHooks( + BundleEvent event, + Bundle bundle, + Set listeners1, + Set listeners2) + { + if (bundle == null) + { + return null; + } + // Create a whitelist of bundle context, if we have hooks. + Set whitelist = null; + Set> hooks = m_registry.getHooks(EventHook.class); + if ((hooks != null) && !hooks.isEmpty()) + { + whitelist = new HashSet(); + whitelist.addAll(listeners1); + whitelist.addAll(listeners2); + + int originalSize = whitelist.size(); + ShrinkableCollection shrinkable = new ShrinkableCollection(whitelist); + for (ServiceReference sr : hooks) + { + try + { + EventHook eh = m_registry.getService(bundle, sr); + if (eh != null) + { + try + { + eh.event(event, shrinkable); + } + catch (Throwable th) + { + System.out.println("Problem invoking bundle hook"); + th.printStackTrace(); + } + finally + { + m_registry.ungetService(bundle, sr); + } + } + } + catch (Throwable th) + { + // If we can't get the hook, then ignore it. + } + } + // If the whitelist hasn't changed, then null it to avoid having + // to do whitelist lookups during event delivery. + if (originalSize == whitelist.size()) + { + whitelist = null; + } + } + return whitelist; + } + + public void fireServiceEvent(final ServiceEvent event, final Dictionary oldProps, final Bundle framework) + { + // Take a snapshot of the listener array. + Map> listeners; + synchronized (this) + { + listeners = m_svcListeners; + } + + // Use service registry hooks to filter target listeners. + listeners = filterListenersUsingHooks(event, framework, listeners); + + // Fire all service events immediately on the calling thread. + fireEventImmediately(this, Request.SERVICE_EVENT, listeners, event, oldProps); + } + + private Map> filterListenersUsingHooks( + ServiceEvent event, Bundle framework, Map> listeners) + { + if (framework == null) + { + return listeners; + } + + Set> ehs = + m_registry.getHooks(org.osgi.framework.hooks.service.EventHook.class); + Set> elhs = + m_registry.getHooks(EventListenerHook.class); + + if ((ehs == null || ehs.isEmpty()) && (elhs == null || elhs.isEmpty())) + { + return listeners; + } + + // Create a shrinkable copy of the map + Map> shrinkableMap = new HashMap>(); + for (Entry> entry : listeners.entrySet()) + { + List shrinkableList = + new ShrinkableList( + new ArrayList(entry.getValue())); + shrinkableMap.put(entry.getKey(), shrinkableList); + } + shrinkableMap = new ShrinkableMap>(shrinkableMap); + + // Go through service EventHook + if (ehs != null && !ehs.isEmpty()) + { + Set shrink = shrinkableMap.keySet(); + for (ServiceReference sr : ehs) + { + try + { + org.osgi.framework.hooks.service.EventHook eh = m_registry.getService(framework, sr); + if (eh != null) + { + try + { + eh.event(event, shrink); + } + catch (Throwable th) + { + System.out.println("Problem invoking event hook"); + th.printStackTrace(); + } + finally + { + m_registry.ungetService(framework, sr); + } + } + } + catch (Throwable th) + { + // Ignore + } + } + } + + // Go through EventListenerHook + if (elhs != null && !elhs.isEmpty()) + { + @SuppressWarnings("unchecked") + Map> shrink = + (Map>) (Map) shrinkableMap; + for (ServiceReference sr : elhs) + { + try + { + EventListenerHook elh = m_registry.getService(framework, sr); + if (elh != null) + { + try + { + elh.event(event, shrink); + } + catch (Throwable th) + { + System.out.println("Problem invoking event hook"); + th.printStackTrace(); + } + finally + { + m_registry.ungetService(framework, sr); + } + } + } + catch (Throwable th) + { + // Ignore + } + } + } + + return shrinkableMap; + } + + private static void fireEventAsynchronously( + EventDispatcher dispatcher, int type, + Map> listeners, + EventObject event) + { + if (!m_sync) + { + // TODO: should possibly check this within thread lock, seems to be ok though without + // If dispatch thread is stopped, then ignore dispatch request. + if (m_stopping || m_thread == null) + { + return; + } + + // First get a request from the pool or create one if necessary. + Request req; + synchronized (m_requestPool) + { + if (m_requestPool.size() > 0) + { + req = m_requestPool.remove(0); + } + else + { + req = new Request(); + } + } + + // Initialize dispatch request. + req.m_dispatcher = dispatcher; + req.m_type = type; + req.m_listeners = listeners; + req.m_event = event; + + // Lock the request list. + synchronized (m_requestList) + { + // Add our request to the list. + m_requestList.add(req); + // Notify the dispatch thread that there is work to do. + m_requestList.notify(); + } + } + else + { + fireEventImmediately(dispatcher, type, listeners, event, null); + } + } + + private static void fireEventImmediately( + EventDispatcher dispatcher, int type, + Map> listeners, + EventObject event, Dictionary oldProps) + { + if (!listeners.isEmpty()) + { + // Notify appropriate listeners. + for (Entry> entry : listeners.entrySet()) + { + for (ListenerInfo info : entry.getValue()) + { + Bundle bundle = info.getBundle(); + EventListener l = info.getListener(); + Filter filter = info.getParsedFilter(); + Object acc = info.getSecurityContext(); + + try + { + if (type == Request.FRAMEWORK_EVENT) + { + invokeFrameworkListenerCallback(bundle, l, event); + } + else if (type == Request.BUNDLE_EVENT) + { + invokeBundleListenerCallback(bundle, l, event); + } + else if (type == Request.SERVICE_EVENT) + { + invokeServiceListenerCallback(bundle, l, filter, acc, event, oldProps); + } + } + catch (Throwable th) + { + if ((type != Request.FRAMEWORK_EVENT) + || (((FrameworkEvent) event).getType() != FrameworkEvent.ERROR)) + { + System.out.println("EventDispatcher: Error during dispatch."); + th.printStackTrace(); + dispatcher.fireFrameworkEvent(new FrameworkEvent(FrameworkEvent.ERROR, bundle, th)); + } + } + } + } + } + } + + private static void invokeFrameworkListenerCallback( + Bundle bundle, final EventListener l, final EventObject event) + { + // The spec says only active bundles receive asynchronous events, + // but we will include starting bundles too otherwise + // it is impossible to see everything. + if ((bundle.getState() == Bundle.STARTING) || (bundle.getState() == Bundle.ACTIVE)) + { + ((FrameworkListener) l).frameworkEvent((FrameworkEvent) event); + + } + } + + private static void invokeBundleListenerCallback( + Bundle bundle, final EventListener l, final EventObject event) + { + // A bundle listener is either synchronous or asynchronous. + // If the bundle listener is synchronous, then deliver the + // event to bundles with a state of STARTING, STOPPING, or + // ACTIVE. If the listener is asynchronous, then deliver the + // event only to bundles that are STARTING or ACTIVE. + if (((SynchronousBundleListener.class.isAssignableFrom(l.getClass())) + && ((bundle.getState() == Bundle.STARTING) + || (bundle.getState() == Bundle.STOPPING) + || (bundle.getState() == Bundle.ACTIVE))) + || ((bundle.getState() == Bundle.STARTING) + || (bundle.getState() == Bundle.ACTIVE))) + { + ((BundleListener) l).bundleChanged((BundleEvent) event); + } + } + + private static void invokeServiceListenerCallback(Bundle bundle, + final EventListener l, Filter filter, Object acc, + final EventObject event, final Dictionary oldProps) + { + // Service events should be delivered to STARTING, + // STOPPING, and ACTIVE bundles. + if ((bundle.getState() != Bundle.STARTING) + && (bundle.getState() != Bundle.STOPPING) + && (bundle.getState() != Bundle.ACTIVE)) + { + return; + } + + // Check that the bundle has permission to get at least + // one of the service interfaces; the objectClass property + // of the service stores its service interfaces. + ServiceReference ref = ((ServiceEvent) event).getServiceReference(); + + boolean hasPermission = true; + if (hasPermission) + { + // Dispatch according to the filter. + boolean matched = (filter == null) + || filter.match(((ServiceEvent) event).getServiceReference()); + + if (matched) + { + ((ServiceListener) l).serviceChanged((ServiceEvent) event); + } + // We need to send an MODIFIED_ENDMATCH event if the listener + // matched previously. + else if (((ServiceEvent) event).getType() == ServiceEvent.MODIFIED) + { + if (filter.match(oldProps)) + { + final ServiceEvent se = new ServiceEvent( + ServiceEvent.MODIFIED_ENDMATCH, + ((ServiceEvent) event).getServiceReference()); + ((ServiceListener) l).serviceChanged(se); + + } + } + } + } + + private static Map> addListenerInfo( + Map> listeners, ListenerInfo info) + { + // Make a copy of the map, since we will be mutating it. + Map> copy = + new HashMap>(listeners); + // Remove the affected entry and make a copy so we can modify it. + List infos = copy.remove(info.getBundleContext()); + if (infos == null) + { + infos = new ArrayList(); + } + else + { + infos = new ArrayList(infos); + } + // Add the new listener info. + infos.add(info); + // Put the listeners back into the copy of the map and return it. + copy.put(info.getBundleContext(), infos); + return copy; + } + + private static Map> updateListenerInfo( + Map> listeners, int idx, + ListenerInfo info) + { + // Make a copy of the map, since we will be mutating it. + Map> copy = + new HashMap>(listeners); + // Remove the affected entry and make a copy so we can modify it. + List infos = copy.remove(info.getBundleContext()); + if (infos != null) + { + List copylist = new ArrayList(infos); + // Update the new listener info. + copylist.set(idx, info); + // Put the listeners back into the copy of the map and return it. + copy.put(info.getBundleContext(), copylist); + return copy; + } + return listeners; + } + + private static Map> removeListenerInfo( + Map> listeners, BundleContext bc, int idx) + { + // Make a copy of the map, since we will be mutating it. + Map> copy = + new HashMap>(listeners); + // Remove the affected entry and make a copy so we can modify it. + List infos = copy.remove(bc); + if (infos != null) + { + infos = new ArrayList(infos); + // Remove the listener info. + infos.remove(idx); + if (!infos.isEmpty()) + { + // Put the listeners back into the copy of the map and return it. + copy.put(bc, infos); + } + return copy; + } + return listeners; + } + + private static Map> removeListenerInfos( + Map> listeners, BundleContext bc) + { + // Make a copy of the map, since we will be mutating it. + Map> copy = + new HashMap>(listeners); + // Remove the affected entry and return the copy. + copy.remove(bc); + return copy; + } + + /** + * This is the dispatching thread's main loop. + */ + private static void run() + { + Request req; + while (true) + { + // Lock the request list so we can try to get a + // dispatch request from it. + synchronized (m_requestList) + { + // Wait while there are no requests to dispatch. If the + // dispatcher thread is supposed to stop, then let the + // dispatcher thread exit the loop and stop. + while (m_requestList.isEmpty() && !m_stopping) + { + // Wait until some signals us for work. + try + { + m_requestList.wait(); + } + catch (InterruptedException ex) + { + // Not much we can do here except for keep waiting. + } + } + + // If there are no events to dispatch and shutdown + // has been called then exit, otherwise dispatch event. + if (m_requestList.isEmpty() && m_stopping) + { + return; + } + + // Get the dispatch request. + req = m_requestList.remove(0); + } + + // Deliver event outside of synchronized block + // so that we don't block other requests from being + // queued during event processing. + // NOTE: We don't catch any exceptions here, because + // the invoked method shields us from exceptions by + // catching Throwables when it invokes callbacks. + fireEventImmediately( + req.m_dispatcher, req.m_type, req.m_listeners, + req.m_event, null); + + // Put dispatch request in cache. + synchronized (m_requestPool) + { + req.m_dispatcher = null; + req.m_type = -1; + req.m_listeners = null; + req.m_event = null; + m_requestPool.add(req); + } + } + } + + private static class Request + { + public static final int FRAMEWORK_EVENT = 0; + public static final int BUNDLE_EVENT = 1; + public static final int SERVICE_EVENT = 2; + public EventDispatcher m_dispatcher = null; + public int m_type = -1; + public Map> m_listeners = null; + public EventObject m_event = null; + } +} diff --git a/connect/src/main/java/org/apache/felix/connect/felix/framework/util/ListenerInfo.java b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/ListenerInfo.java new file mode 100644 index 00000000000..e0aa9336706 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/ListenerInfo.java @@ -0,0 +1,142 @@ +/* + * 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.connect.felix.framework.util; + +import java.util.EventListener; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Filter; +import org.osgi.framework.hooks.service.ListenerHook; + + +public class ListenerInfo implements ListenerHook.ListenerInfo +{ + private final Bundle m_bundle; + private final BundleContext m_context; + private final Class m_listenerClass; + private final EventListener m_listener; + private final Filter m_filter; + private final Object m_acc; + private final boolean m_removed; + + public ListenerInfo( + Bundle bundle, BundleContext context, Class listenerClass, EventListener listener, + Filter filter, Object acc, boolean removed) + { + // Technically, we could get the bundle from the bundle context, but + // there are some corner cases where the bundle context might become + // invalid and we still need the bundle. + m_bundle = bundle; + m_context = context; + m_listenerClass = listenerClass; + m_listener = listener; + m_filter = filter; + m_acc = acc; + m_removed = removed; + } + + public ListenerInfo(ListenerInfo info, boolean removed) + { + m_bundle = info.m_bundle; + m_context = info.m_context; + m_listenerClass = info.m_listenerClass; + m_listener = info.m_listener; + m_filter = info.m_filter; + m_acc = info.m_acc; + m_removed = removed; + } + + public Bundle getBundle() + { + return m_bundle; + } + + public BundleContext getBundleContext() + { + return m_context; + } + + public Class getListenerClass() + { + return m_listenerClass; + } + + public EventListener getListener() + { + return m_listener; + } + + public Filter getParsedFilter() + { + return m_filter; + } + + public String getFilter() + { + if (m_filter != null) + { + return m_filter.toString(); + } + return null; + } + + public Object getSecurityContext() + { + return m_acc; + } + + public boolean isRemoved() + { + return m_removed; + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) + { + return true; + } + + if (!(obj instanceof ListenerInfo)) + { + return false; + } + + ListenerInfo other = (ListenerInfo) obj; + return (other.m_bundle == m_bundle) + && (other.m_context == m_context) + && (other.m_listenerClass == m_listenerClass) + && (other.m_listener == m_listener) + && (m_filter == null ? other.m_filter == null : m_filter.equals(other.m_filter)); + } + + @Override + public int hashCode() + { + int hash = 7; + hash = 59 * hash + (this.m_bundle != null ? this.m_bundle.hashCode() : 0); + hash = 59 * hash + (this.m_context != null ? this.m_context.hashCode() : 0); + hash = 59 * hash + (this.m_listenerClass != null ? this.m_listenerClass.hashCode() : 0); + hash = 59 * hash + (this.m_listener != null ? this.m_listener.hashCode() : 0); + hash = 59 * hash + (this.m_filter != null ? this.m_filter.hashCode() : 0); + return hash; + } +} \ No newline at end of file diff --git a/connect/src/main/java/org/apache/felix/connect/felix/framework/util/MapToDictionary.java b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/MapToDictionary.java new file mode 100644 index 00000000000..bd33969b24a --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/MapToDictionary.java @@ -0,0 +1,85 @@ +/* + * 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.connect.felix.framework.util; + +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Map; + +/** + * This is a simple class that implements a Dictionary from a + * Map. The resulting dictionary is immutable. + */ +public class MapToDictionary extends Dictionary +{ + /** + * Map source. + */ + private Map m_map = null; + + public MapToDictionary(Map map) + { + if (map == null) + { + throw new IllegalArgumentException("Source map cannot be null."); + } + m_map = map; + } + + public Enumeration elements() + { + return Collections.enumeration(m_map.values()); + } + + public V get(Object key) + { + return m_map.get(key); + } + + public boolean isEmpty() + { + return m_map.isEmpty(); + } + + public Enumeration keys() + { + return Collections.enumeration(m_map.keySet()); + } + + public V put(K key, V value) + { + throw new UnsupportedOperationException(); + } + + public V remove(Object key) + { + throw new UnsupportedOperationException(); + } + + public int size() + { + return m_map.size(); + } + + public String toString() + { + return m_map.toString(); + } +} \ No newline at end of file diff --git a/connect/src/main/java/org/apache/felix/connect/felix/framework/util/ShrinkableCollection.java b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/ShrinkableCollection.java new file mode 100644 index 00000000000..04ae94c1721 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/ShrinkableCollection.java @@ -0,0 +1,49 @@ +/* + * 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.connect.felix.framework.util; + +import java.util.AbstractCollection; +import java.util.Collection; +import java.util.Iterator; + +/** + * A collection wrapper that only permits clients to shrink the collection. + */ +public class ShrinkableCollection extends AbstractCollection +{ + private final Collection m_delegate; + + public ShrinkableCollection(Collection delegate) + { + m_delegate = delegate; + } + + @Override + public Iterator iterator() + { + return m_delegate.iterator(); + } + + @Override + public int size() + { + return m_delegate.size(); + } + +} diff --git a/connect/src/main/java/org/apache/felix/connect/felix/framework/util/ShrinkableList.java b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/ShrinkableList.java new file mode 100644 index 00000000000..6f4aed70291 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/ShrinkableList.java @@ -0,0 +1,54 @@ +/* + * 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.connect.felix.framework.util; + +import java.util.AbstractList; +import java.util.List; + +/** + * A collection wrapper that only permits clients to shrink the collection. + */ +public class ShrinkableList extends AbstractList +{ + private final List m_delegate; + + public ShrinkableList(List delegate) + { + m_delegate = delegate; + } + + @Override + public T get(int index) + { + return m_delegate.get(index); + } + + @Override + public int size() + { + return m_delegate.size(); + } + + @Override + public T remove(int index) + { + return m_delegate.remove(index); + } + +} diff --git a/connect/src/main/java/org/apache/felix/connect/felix/framework/util/ShrinkableMap.java b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/ShrinkableMap.java new file mode 100644 index 00000000000..dbaac0bdcbb --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/ShrinkableMap.java @@ -0,0 +1,40 @@ +/* + * 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.connect.felix.framework.util; + +import java.util.AbstractMap; +import java.util.Map; +import java.util.Set; + +public class ShrinkableMap extends AbstractMap +{ + private final Map m_delegate; + + public ShrinkableMap(Map delegate) + { + m_delegate = delegate; + } + + @Override + public Set> entrySet() + { + return m_delegate.entrySet(); + } + +} diff --git a/connect/src/main/java/org/apache/felix/connect/felix/framework/util/StringComparator.java b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/StringComparator.java new file mode 100644 index 00000000000..2a34edabd81 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/StringComparator.java @@ -0,0 +1,49 @@ +/* + * 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.connect.felix.framework.util; + +import java.util.Comparator; + +public class StringComparator implements Comparator +{ + private final boolean m_isCaseSensitive; + + public StringComparator(boolean b) + { + m_isCaseSensitive = b; + } + + @Override + public int compare(String o1, String o2) + { + if (m_isCaseSensitive) + { + return o1.compareTo(o2); + } + else + { + return o1.compareToIgnoreCase(o2); + } + } + + public boolean isCaseSensitive() + { + return m_isCaseSensitive; + } +} \ No newline at end of file diff --git a/connect/src/main/java/org/apache/felix/connect/felix/framework/util/StringMap.java b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/StringMap.java new file mode 100644 index 00000000000..f4727828418 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/StringMap.java @@ -0,0 +1,147 @@ +/* + * 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.connect.felix.framework.util; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * Simple utility class that creates a map for string-based keys. This map can + * be set to use case-sensitive or case-insensitive comparison when searching + * for the key. Any keys put into this map will be converted to a + * String using the toString() method, since it is only + * intended to compare strings. + */ +public class StringMap implements Map +{ + private TreeMap m_map; + + public StringMap() + { + this(true); + } + + public StringMap(boolean caseSensitive) + { + m_map = new TreeMap(new StringComparator(caseSensitive)); + } + + public StringMap(Map map, boolean caseSensitive) + { + this(caseSensitive); + putAll(map); + } + + public boolean isCaseSensitive() + { + return ((StringComparator) m_map.comparator()).isCaseSensitive(); + } + + public void setCaseSensitive(boolean b) + { + if (isCaseSensitive() != b) + { + TreeMap map = new TreeMap(new StringComparator(b)); + map.putAll(m_map); + m_map = map; + } + } + + @Override + public int size() + { + return m_map.size(); + } + + @Override + public boolean isEmpty() + { + return m_map.isEmpty(); + } + + @Override + public boolean containsKey(Object arg0) + { + return m_map.containsKey(arg0); + } + + @Override + public boolean containsValue(Object arg0) + { + return m_map.containsValue(arg0); + } + + @Override + public T get(Object arg0) + { + return m_map.get(arg0); + } + + @Override + public T put(String key, T value) + { + return m_map.put(key, value); + } + + @Override + public void putAll(Map map) + { + for (Entry entry : map.entrySet()) + { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public T remove(Object arg0) + { + return m_map.remove(arg0); + } + + @Override + public void clear() + { + m_map.clear(); + } + + @Override + public Set keySet() + { + return m_map.keySet(); + } + + @Override + public Collection values() + { + return m_map.values(); + } + + @Override + public Set> entrySet() + { + return m_map.entrySet(); + } + + public String toString() + { + return m_map.toString(); + } +} \ No newline at end of file diff --git a/connect/src/main/java/org/apache/felix/connect/felix/framework/util/Util.java b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/Util.java new file mode 100644 index 00000000000..565287e29df --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/Util.java @@ -0,0 +1,518 @@ +/* + * 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.connect.felix.framework.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + +public class Util +{ + /** + * The default name used for the default configuration properties file. + */ + private static final String DEFAULT_PROPERTIES_FILE = "default.properties"; + + public static String getDefaultProperty(String name) + { + String value = null; + + URL propURL = Util.class.getClassLoader().getResource( + DEFAULT_PROPERTIES_FILE); + if (propURL != null) + { + InputStream is = null; + try + { + // Load properties from URL. + is = propURL.openConnection().getInputStream(); + Properties props = new Properties(); + props.load(is); + is.close(); + // Perform variable substitution for property. + value = props.getProperty(name); + value = (value != null) ? Util.substVars(value, name, null, + props) : null; + } + catch (Exception ex) + { + // Try to close input stream if we have one. + try + { + if (is != null) + { + is.close(); + } + } + catch (IOException ex2) + { + // Nothing we can do. + } + + ex.printStackTrace(); + } + } + return value; + } + + /** + * Converts a module identifier to a bundle identifier. Module IDs are + * typically <bundle-id>.<revision>; this method + * returns only the portion corresponding to the bundle ID. + */ + public static long getBundleIdFromModuleId(String id) + { + try + { + String bundleId = (id.indexOf('.') >= 0) ? id.substring(0, + id.indexOf('.')) : id; + return Long.parseLong(bundleId); + } + catch (NumberFormatException ex) + { + return -1; + } + } + + /** + * Converts a module identifier to a bundle identifier. Module IDs are + * typically <bundle-id>.<revision>; this method + * returns only the portion corresponding to the revision. + */ + public static int getModuleRevisionFromModuleId(String id) + { + try + { + int index = id.indexOf('.'); + if (index >= 0) + { + return Integer.parseInt(id.substring(index + 1)); + } + } + catch (NumberFormatException ex) + { + } + return -1; + } + + public static String getClassName(String className) + { + if (className == null) + { + className = ""; + } + return (className.lastIndexOf('.') < 0) ? "" : className + .substring(className.lastIndexOf('.') + 1); + } + + public static String getClassPackage(String className) + { + if (className == null) + { + className = ""; + } + return (className.lastIndexOf('.') < 0) ? "" : className.substring(0, + className.lastIndexOf('.')); + } + + public static String getResourcePackage(String resource) + { + if (resource == null) + { + resource = ""; + } + // NOTE: The package of a resource is tricky to determine since + // resources do not follow the same naming conventions as classes. + // This code is pessimistic and assumes that the package of a + // resource is everything up to the last '/' character. By making + // this choice, it will not be possible to load resources from + // imports using relative resource names. For example, if a + // bundle exports "foo" and an importer of "foo" tries to load + // "/foo/bar/myresource.txt", this will not be found in the exporter + // because the following algorithm assumes the package name is + // "foo.bar", not just "foo". This only affects imported resources, + // local resources will work as expected. + String pkgName = (resource.startsWith("/")) ? resource.substring(1) + : resource; + pkgName = (pkgName.lastIndexOf('/') < 0) ? "" : pkgName.substring(0, + pkgName.lastIndexOf('/')); + pkgName = pkgName.replace('/', '.'); + return pkgName; + } + + /** + *

      + * This is a simple utility class that attempts to load the named class + * using the class loader of the supplied class or the class loader of one + * of its super classes or their implemented interfaces. This is necessary + * during service registration to test whether a given service object + * implements its declared service interfaces. + *

      + *

      + * To perform this test, the framework must try to load the classes + * associated with the declared service interfaces, so it must choose a + * class loader. The class loader of the registering bundle cannot be used, + * since this disallows third parties to register service on behalf of + * another bundle. Consequently, the class loader of the service object must + * be used. However, this is also not sufficient since the class loader of + * the service object may not have direct access to the class in question. + *

      + *

      + * The service object's class loader may not have direct access to its + * service interface if it extends a super class from another bundle which + * implements the service interface from an imported bundle or if it + * implements an extension of the service interface from another bundle + * which imports the base interface from another bundle. In these cases, the + * service object's class loader only has access to the super class's class + * or the extended service interface, respectively, but not to the actual + * service interface. + *

      + *

      + * Thus, it is necessary to not only try to load the service interface class + * from the service object's class loader, but from the class loaders of any + * interfaces it implements and the class loaders of all super classes. + *

      + * + * @param svcObj the class that is the root of the search. + * @param name the name of the class to load. + * @return the loaded class or null if it could not be loaded. + */ + public static Class loadClassUsingClass(Class clazz, String name) + { + Class loadedClass = null; + + while (clazz != null) + { + // Get the class loader of the current class object. + ClassLoader loader = clazz.getClassLoader(); + // A null class loader represents the system class loader. + loader = (loader == null) ? ClassLoader.getSystemClassLoader() + : loader; + try + { + return loader.loadClass(name); + } + catch (ClassNotFoundException ex) + { + // Ignore and try interface class loaders. + } + + // Try to see if we can load the class from + // one of the class's implemented interface + // class loaders. + Class[] ifcs = clazz.getInterfaces(); + for (int i = 0; i < ifcs.length; i++) + { + loadedClass = loadClassUsingClass(ifcs[i], name); + if (loadedClass != null) + { + return loadedClass; + } + } + + // Try to see if we can load the class from + // the super class class loader. + clazz = clazz.getSuperclass(); + } + + return null; + } + + /** + * This method determines if the requesting bundle is able to cast the + * specified service reference based on class visibility rules of the + * underlying modules. + * + * @param requester The bundle requesting the service. + * @param ref The service in question. + * @return true if the requesting bundle is able to case the + * service object to a known type. + */ + public static boolean isServiceAssignable(Bundle requester, + ServiceReference ref) + { + // Boolean flag. + boolean allow = true; + // Get the service's objectClass property. + String[] objectClass = (String[]) ref + .getProperty(Constants.OBJECTCLASS); + + // The the service reference is not assignable when the requesting + // bundle is wired to a different version of the service object. + // NOTE: We are pessimistic here, if any class in the service's + // objectClass is not usable by the requesting bundle, then we + // disallow the service reference. + for (int classIdx = 0; (allow) && (classIdx < objectClass.length); classIdx++) + { + if (!ref.isAssignableTo(requester, objectClass[classIdx])) + { + allow = false; + } + } + return allow; + } + + private static final byte encTab[] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, + 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, + 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x61, 0x62, + 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, + 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, + 0x79, 0x7a, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, + 0x39, 0x2b, 0x2f}; + + private static final byte decTab[] = {-1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, + -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, + 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1}; + + public static String base64Encode(String s) throws IOException + { + return encode(s.getBytes(), 0); + } + + /** + * Encode a raw byte array to a Base64 String. + * + * @param in Byte array to encode. + * @param len Length of Base64 lines. 0 means no line breaks. + */ + public static String encode(byte[] in, int len) throws IOException + { + ByteArrayOutputStream baos = null; + ByteArrayInputStream bais = null; + try + { + baos = new ByteArrayOutputStream(); + bais = new ByteArrayInputStream(in); + encode(bais, baos, len); + // ASCII byte array to String + return (new String(baos.toByteArray())); + } + finally + { + if (baos != null) + { + baos.close(); + } + if (bais != null) + { + bais.close(); + } + } + } + + public static void encode(InputStream in, OutputStream out, int len) + throws IOException + { + + // Check that length is a multiple of 4 bytes + if (len % 4 != 0) + { + throw new IllegalArgumentException("Length must be a multiple of 4"); + } + + // Read input stream until end of file + int bits = 0; + int nbits = 0; + int nbytes = 0; + int b; + + while ((b = in.read()) != -1) + { + bits = (bits << 8) | b; + nbits += 8; + while (nbits >= 6) + { + nbits -= 6; + out.write(encTab[0x3f & (bits >> nbits)]); + nbytes++; + // New line + if (len != 0 && nbytes >= len) + { + out.write(0x0d); + out.write(0x0a); + nbytes -= len; + } + } + } + + switch (nbits) + { + case 2: + out.write(encTab[0x3f & (bits << 4)]); + out.write(0x3d); // 0x3d = '=' + out.write(0x3d); + break; + case 4: + out.write(encTab[0x3f & (bits << 2)]); + out.write(0x3d); + break; + } + + if (len != 0) + { + if (nbytes != 0) + { + out.write(0x0d); + out.write(0x0a); + } + out.write(0x0d); + out.write(0x0a); + } + } + + private static final String DELIM_START = "${"; + private static final String DELIM_STOP = "}"; + + /** + *

      + * This method performs property variable substitution on the specified + * value. If the specified value contains the syntax + * ${<prop-name>}, where <prop-name> refers to + * either a configuration property or a system property, then the + * corresponding property value is substituted for the variable placeholder. + * Multiple variable placeholders may exist in the specified value as well + * as nested variable placeholders, which are substituted from inner most to + * outer most. Configuration properties override system properties. + *

      + * + * @param val The string on which to perform property substitution. + * @param currentKey The key of the property being evaluated used to detect cycles. + * @param cycleMap Map of variable references used to detect nested cycles. + * @param configProps Set of configuration properties. + * @return The value of the specified string after system property + * substitution. + * @throws IllegalArgumentException If there was a syntax error in the property placeholder + * syntax or a recursive variable reference. + */ + public static String substVars(String val, String currentKey, Map cycleMap, + Properties configProps) throws IllegalArgumentException + { + // If there is currently no cycle map, then create + // one for detecting cycles for this invocation. + if (cycleMap == null) + { + cycleMap = new HashMap(); + } + + // Put the current key in the cycle map. + cycleMap.put(currentKey, currentKey); + + // Assume we have a value that is something like: + // "leading ${foo.${bar}} middle ${baz} trailing" + + // Find the first ending '}' variable delimiter, which + // will correspond to the first deepest nested variable + // placeholder. + int stopDelim = -1; + int startDelim = -1; + + do + { + stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1); + // If there is no stopping delimiter, then just return + // the value since there is no variable declared. + if (stopDelim < 0) + { + return val; + } + // Try to find the matching start delimiter by + // looping until we find a start delimiter that is + // greater than the stop delimiter we have found. + startDelim = val.indexOf(DELIM_START); + // If there is no starting delimiter, then just return + // the value since there is no variable declared. + if (startDelim < 0) + { + return val; + } + while (stopDelim >= 0) + { + int idx = val.indexOf(DELIM_START, + startDelim + DELIM_START.length()); + if ((idx < 0) || (idx > stopDelim)) + { + break; + } + else if (idx < stopDelim) + { + startDelim = idx; + } + } + } + while ((startDelim > stopDelim) && (stopDelim >= 0)); + + // At this point, we have found a variable placeholder so + // we must perform a variable substitution on it. + // Using the start and stop delimiter indices, extract + // the first, deepest nested variable placeholder. + String variable = val.substring(startDelim + DELIM_START.length(), + stopDelim); + + // Verify that this is not a recursive variable reference. + if (cycleMap.get(variable) != null) + { + throw new IllegalArgumentException("recursive variable reference: " + + variable); + } + + // Get the value of the deepest nested variable placeholder. + // Try to configuration properties first. + String substValue = (configProps != null) ? configProps.getProperty( + variable, null) : null; + if (substValue == null) + { + // Ignore unknown property values. + substValue = System.getProperty(variable, ""); + } + + // Remove the found variable from the cycle map, since + // it may appear more than once in the value and we don't + // want such situations to appear as a recursive reference. + cycleMap.remove(variable); + + // Append the leading characters, the substituted value of + // the variable, and the trailing characters to get the new + // value. + val = val.substring(0, startDelim) + substValue + + val.substring(stopDelim + DELIM_STOP.length(), val.length()); + + // Now perform substitution again, since there could still + // be substitutions to make. + val = substVars(val, currentKey, cycleMap, configProps); + + // Return the value. + return val; + } + +} \ No newline at end of file diff --git a/connect/src/main/java/org/apache/felix/connect/felix/framework/util/VersionRange.java b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/VersionRange.java new file mode 100644 index 00000000000..7971840ccac --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/felix/framework/util/VersionRange.java @@ -0,0 +1,163 @@ +/* + * 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.connect.felix.framework.util; + +import org.osgi.framework.Version; + +public class VersionRange +{ + private final Version m_floor; + private final boolean m_isFloorInclusive; + private final Version m_ceiling; + private final boolean m_isCeilingInclusive; + public static final VersionRange infiniteRange = new VersionRange( + Version.emptyVersion, true, null, true); + + public VersionRange(Version low, boolean isLowInclusive, Version high, + boolean isHighInclusive) + { + m_floor = low; + m_isFloorInclusive = isLowInclusive; + m_ceiling = high; + m_isCeilingInclusive = isHighInclusive; + } + + public Version getFloor() + { + return m_floor; + } + + public boolean isFloorInclusive() + { + return m_isFloorInclusive; + } + + public Version getCeiling() + { + return m_ceiling; + } + + public boolean isCeilingInclusive() + { + return m_isCeilingInclusive; + } + + public boolean isInRange(Version version) + { + // We might not have an upper end to the range. + if (m_ceiling == null) + { + return (version.compareTo(m_floor) >= 0); + } + else if (isFloorInclusive() && isCeilingInclusive()) + { + return (version.compareTo(m_floor) >= 0) + && (version.compareTo(m_ceiling) <= 0); + } + else if (isCeilingInclusive()) + { + return (version.compareTo(m_floor) > 0) + && (version.compareTo(m_ceiling) <= 0); + } + else if (isFloorInclusive()) + { + return (version.compareTo(m_floor) >= 0) + && (version.compareTo(m_ceiling) < 0); + } + return (version.compareTo(m_floor) > 0) + && (version.compareTo(m_ceiling) < 0); + } + + public static VersionRange parse(String range) + { + // Check if the version is an interval. + if (range.indexOf(',') >= 0) + { + String s = range.substring(1, range.length() - 1); + String vlo = s.substring(0, s.indexOf(',')).trim(); + String vhi = s.substring(s.indexOf(',') + 1, s.length()).trim(); + return new VersionRange(new Version(vlo), (range.charAt(0) == '['), + new Version(vhi), (range.charAt(range.length() - 1) == ']')); + } + else + { + return new VersionRange(new Version(range), true, null, false); + } + } + + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + if (getClass() != obj.getClass()) + { + return false; + } + final VersionRange other = (VersionRange) obj; + if (m_floor != other.m_floor + && (m_floor == null || !m_floor.equals(other.m_floor))) + { + return false; + } + if (m_isFloorInclusive != other.m_isFloorInclusive) + { + return false; + } + if (m_ceiling != other.m_ceiling + && (m_ceiling == null || !m_ceiling.equals(other.m_ceiling))) + { + return false; + } + if (m_isCeilingInclusive != other.m_isCeilingInclusive) + { + return false; + } + return true; + } + + public int hashCode() + { + int hash = 5; + hash = 97 * hash + (m_floor != null ? m_floor.hashCode() : 0); + hash = 97 * hash + (m_isFloorInclusive ? 1 : 0); + hash = 97 * hash + (m_ceiling != null ? m_ceiling.hashCode() : 0); + hash = 97 * hash + (m_isCeilingInclusive ? 1 : 0); + return hash; + } + + public String toString() + { + if (m_ceiling != null) + { + StringBuffer sb = new StringBuffer(); + sb.append(m_isFloorInclusive ? '[' : '('); + sb.append(m_floor.toString()); + sb.append(','); + sb.append(m_ceiling.toString()); + sb.append(m_isCeilingInclusive ? ']' : ')'); + return sb.toString(); + } + else + { + return m_floor.toString(); + } + } +} \ No newline at end of file diff --git a/connect/src/main/java/org/apache/felix/connect/launch/BundleDescriptor.java b/connect/src/main/java/org/apache/felix/connect/launch/BundleDescriptor.java new file mode 100644 index 00000000000..0a2b1cc01c2 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/launch/BundleDescriptor.java @@ -0,0 +1,80 @@ +/* + * 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.connect.launch; + +import java.util.Map; + +import org.apache.felix.connect.Revision; + +public class BundleDescriptor +{ + private final ClassLoader m_loader; + private final String m_url; + private final Map m_headers; + private final Revision m_revision; + private final Map m_services; + + public BundleDescriptor(ClassLoader loader, String url, + Map headers) + { + this(loader, url, headers, null, null); + } + + public BundleDescriptor(ClassLoader loader, String url, + Map headers, + Revision revision, + Map services) + { + m_loader = loader; + m_url = url; + m_headers = headers; + m_revision = revision; + m_services = services; + } + + public ClassLoader getClassLoader() + { + return m_loader; + } + + public String getUrl() + { + return m_url; + } + + public String toString() + { + return m_url; + } + + public Map getHeaders() + { + return m_headers; + } + + public Revision getRevision() + { + return m_revision; + } + + public Map getServices() + { + return m_services; + } +} diff --git a/connect/src/main/java/org/apache/felix/connect/launch/ClasspathScanner.java b/connect/src/main/java/org/apache/felix/connect/launch/ClasspathScanner.java new file mode 100644 index 00000000000..da19f9f3d17 --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/launch/ClasspathScanner.java @@ -0,0 +1,180 @@ +/* + * 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.connect.launch; + +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; + +import org.apache.felix.connect.felix.framework.util.MapToDictionary; + +public class ClasspathScanner +{ + public List scanForBundles() throws Exception + { + return scanForBundles(null, null); + } + + public List scanForBundles(ClassLoader loader) throws Exception + { + return scanForBundles(null, loader); + } + + public List scanForBundles(String filterString) + throws Exception + { + return scanForBundles(filterString, null); + } + + public List scanForBundles(String filterString, ClassLoader loader) + throws Exception + { + Filter filter = (filterString != null) ? FrameworkUtil + .createFilter(filterString) : null; + + loader = (loader != null) ? loader : getClass().getClassLoader(); + + List bundles = new ArrayList(); + byte[] bytes = new byte[1024 * 1024 * 2]; + for (Enumeration e = loader.getResources( + "META-INF/MANIFEST.MF"); e.hasMoreElements(); ) + { + URL manifestURL = e.nextElement(); + InputStream input = null; + try + { + input = manifestURL.openStream(); + int size = 0; + for (int i = input.read(bytes); i != -1; i = input.read(bytes, size, bytes.length - size)) + { + size += i; + if (size == bytes.length) + { + byte[] tmp = new byte[size * 2]; + System.arraycopy(bytes, 0, tmp, 0, bytes.length); + bytes = tmp; + } + } + + // Now parse the main attributes. The idea is to do that + // without creating new byte arrays. Therefore, we read through + // the manifest bytes inside the bytes array and write them back into + // the same array unless we don't need them (e.g., \r\n and \n are skipped). + // That allows us to create the strings from the bytes array without the skipped + // chars. We stopp as soon as we see a blankline as that denotes that the main + //attributes part is finished. + String key = null; + int last = 0; + int current = 0; + + Map headers = new HashMap(); + for (int i = 0; i < size; i++) + { + // skip \r and \n if it is follows by another \n + // (we catch the blank line case in the next iteration) + if (bytes[i] == '\r') + { + if ((i + 1 < size) && (bytes[i + 1] == '\n')) + { + continue; + } + } + if (bytes[i] == '\n') + { + if ((i + 1 < size) && (bytes[i + 1] == ' ')) + { + i++; + continue; + } + } + // If we don't have a key yet and see the first : we parse it as the key + // and skip the : that follows it. + if ((key == null) && (bytes[i] == ':')) + { + key = new String(bytes, last, (current - last), "UTF-8"); + if ((i + 1 < size) && (bytes[i + 1] == ' ')) + { + last = current + 1; + continue; + } + else + { + throw new Exception( + "Manifest error: Missing space separator - " + key); + } + } + // if we are at the end of a line + if (bytes[i] == '\n') + { + // and it is a blank line stop parsing (main attributes are done) + if ((last == current) && (key == null)) + { + break; + } + // Otherwise, parse the value and add it to the map (we throw an + // exception if we don't have a key or the key already exist. + String value = new String(bytes, last, (current - last), "UTF-8"); + if (key == null) + { + throw new Exception("Manifst error: Missing attribute name - " + value); + } + else if (headers.put(key, value) != null) + { + throw new Exception("Manifst error: Duplicate attribute name - " + key); + } + last = current; + key = null; + } + else + { + // write back the byte if it needs to be included in the key or the value. + bytes[current++] = bytes[i]; + } + } + if ((filter == null) + || filter.match(new MapToDictionary(headers))) + { + bundles.add(new BundleDescriptor(loader, getParentURL(manifestURL).toExternalForm(), headers)); + } + } + finally + { + if (input != null) + { + input.close(); + } + } + } + return bundles; + } + + private URL getParentURL(URL url) throws Exception + { + String externalForm = url.toExternalForm(); + return new URL(externalForm.substring(0, externalForm.length() + - "META-INF/MANIFEST.MF".length())); + } +} diff --git a/connect/src/main/java/org/apache/felix/connect/launch/PojoServiceRegistry.java b/connect/src/main/java/org/apache/felix/connect/launch/PojoServiceRegistry.java new file mode 100644 index 00000000000..f8a9ad9795d --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/launch/PojoServiceRegistry.java @@ -0,0 +1,57 @@ +/* + * 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.connect.launch; + +import java.util.Collection; +import java.util.Dictionary; +import java.util.List; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; + +public interface PojoServiceRegistry +{ + public BundleContext getBundleContext(); + + public void startBundles(Collection bundles) throws Exception; + + public Bundle registerBundle(BundleDescriptor bundle) throws Exception; + + public void addServiceListener(ServiceListener listener, String filter) throws InvalidSyntaxException; + + public void addServiceListener(ServiceListener listener); + + public void removeServiceListener(ServiceListener listener); + + public ServiceRegistration registerService(String[] clazzes, Object service, Dictionary properties); + + public ServiceRegistration registerService(String clazz, Object service, Dictionary properties); + + public ServiceReference[] getServiceReferences(String clazz, String filter) throws InvalidSyntaxException; + + public ServiceReference getServiceReference(String clazz); + + public S getService(ServiceReference reference); + + public boolean ungetService(ServiceReference reference); +} diff --git a/connect/src/main/java/org/apache/felix/connect/launch/PojoServiceRegistryFactory.java b/connect/src/main/java/org/apache/felix/connect/launch/PojoServiceRegistryFactory.java new file mode 100644 index 00000000000..b49148c43fc --- /dev/null +++ b/connect/src/main/java/org/apache/felix/connect/launch/PojoServiceRegistryFactory.java @@ -0,0 +1,32 @@ +/* + * 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.connect.launch; + +import java.util.Map; + +public interface PojoServiceRegistryFactory +{ + public static final String BUNDLE_DESCRIPTORS = + PojoServiceRegistry.class.getName().toLowerCase() + ".bundles"; + + public static final String BUNDLES_AUTOSTART = + PojoServiceRegistry.class.getName().toLowerCase() + ".bundles.autostart"; + + public PojoServiceRegistry newPojoServiceRegistry(Map configuration) throws Exception; +} diff --git a/connect/src/main/resources/META-INF/services/org.apache.felix.connect.launch.PojoServiceRegistryFactory b/connect/src/main/resources/META-INF/services/org.apache.felix.connect.launch.PojoServiceRegistryFactory new file mode 100644 index 00000000000..84bd8a04545 --- /dev/null +++ b/connect/src/main/resources/META-INF/services/org.apache.felix.connect.launch.PojoServiceRegistryFactory @@ -0,0 +1 @@ +org.apache.felix.connect.PojoServiceRegistryFactoryImpl \ No newline at end of file diff --git a/connect/src/main/resources/META-INF/services/org.osgi.framework.launch.FrameworkFactory b/connect/src/main/resources/META-INF/services/org.osgi.framework.launch.FrameworkFactory new file mode 100644 index 00000000000..84bd8a04545 --- /dev/null +++ b/connect/src/main/resources/META-INF/services/org.osgi.framework.launch.FrameworkFactory @@ -0,0 +1 @@ +org.apache.felix.connect.PojoServiceRegistryFactoryImpl \ No newline at end of file diff --git a/converter/converter/pom.xml b/converter/converter/pom.xml new file mode 100644 index 00000000000..21f6b906cd4 --- /dev/null +++ b/converter/converter/pom.xml @@ -0,0 +1,140 @@ + + + + 4.0.0 + + org.apache.felix + felix-parent + 6 + ../pom/pom.xml + + + Apache Felix Converter + org.apache.felix.converter + 1.0.5-SNAPSHOT + jar + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/converter/converter + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/converter/converter + http://svn.apache.org/viewvc/felix/trunk/converter/converter/ + + + + 8 + java17 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.7 + 1.7 + 1.8 + 1.8 + + + + org.apache.felix + maven-bundle-plugin + 3.2.0 + + + bundle + package + + bundle + + + + baseline + + baseline + + + + + + + org.osgi.util.function, + org.osgi.util.converter;-split-package:=merge-first + + + org.osgi.util.function, + org.osgi.util.converter, + * + + + + + + org.apache.rat + apache-rat-plugin + + + verify + + check + + + + + + + + + + org.osgi + org.osgi.util.function + 1.0.0 + + + + org.osgi + osgi.annotation + 6.0.1 + provided + + + + org.osgi + osgi.core + 6.0.0 + provided + + + + junit + junit + 4.12 + test + + + + org.apache.sling + org.apache.sling.commons.json + 2.0.16 + test + + + diff --git a/converter/converter/src/main/java/org/osgi/util/converter/AbstractCollectionDelegate.java b/converter/converter/src/main/java/org/osgi/util/converter/AbstractCollectionDelegate.java new file mode 100644 index 00000000000..e57933f36b1 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/AbstractCollectionDelegate.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.converter; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +/** + * @author $Id$ + */ +abstract class AbstractCollectionDelegate implements List { + @Override + public boolean add(T e) { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public boolean contains(Object o) { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public boolean containsAll(Collection< ? > c) { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public boolean addAll(Collection< ? extends T> c) { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public boolean addAll(int index, Collection< ? extends T> c) { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public boolean removeAll(Collection< ? > c) { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public boolean retainAll(Collection< ? > c) { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public Iterator iterator() { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public T set(int index, T element) { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public void add(int index, T element) { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public T remove(int index) { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public int indexOf(Object o) { + Object[] arr = toArray(); + for (int i = 0; i < arr.length; i++) { + if (o != null) { + if (o.equals(arr[i])) + return i; + } else { + if (arr[i] == null) + return i; + } + } + return -1; + } + + @Override + public int lastIndexOf(Object o) { + Object[] arr = toArray(); + for (int i = arr.length - 1; i >= 0; i--) { + if (o != null) { + if (o.equals(arr[i])) + return i; + } else { + if (arr[i] == null) + return i; + } + } + return -1; + } + + @Override + public X[] toArray(X[] a) { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public ListIterator listIterator() { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public ListIterator listIterator(int index) { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public List subList(int fromIndex, int toIndex) { + throw new UnsupportedOperationException(); // Never called + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/AbstractSpecifying.java b/converter/converter/src/main/java/org/osgi/util/converter/AbstractSpecifying.java new file mode 100644 index 00000000000..c3fba1154ca --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/AbstractSpecifying.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.converter; + +/** + * @author $Id$ + */ +abstract class AbstractSpecifying> + implements Specifying { + protected volatile Object defaultValue; + protected volatile boolean hasDefault = false; + protected volatile boolean liveView = false; + protected volatile boolean keysIgnoreCase = false; + protected volatile Class< ? > sourceAsClass; + protected volatile boolean sourceAsDTO = false; + protected volatile boolean sourceAsJavaBean = false; + protected volatile Class< ? > targetAsClass; + protected volatile boolean targetAsDTO = false; + protected volatile boolean targetAsJavaBean = false; + + @SuppressWarnings("unchecked") + private T castThis() { + return (T) this; + } + + @Override + public T defaultValue(Object defVal) { + defaultValue = defVal; + hasDefault = true; + return castThis(); + } + + @Override + public T keysIgnoreCase() { + keysIgnoreCase = true; + return castThis(); + } + + @Override + public T sourceAs(Class< ? > cls) { + sourceAsClass = cls; + return castThis(); + } + + @Override + public T sourceAsBean() { + // To avoid ambiguity, reset any instruction to sourceAsDTO + sourceAsDTO = false; + sourceAsJavaBean = true; + return castThis(); + } + + @Override + public T sourceAsDTO() { + // To avoid ambiguity, reset any instruction to sourceAsJavaBean + sourceAsJavaBean = false; + sourceAsDTO = true; + return castThis(); + } + + @Override + public T targetAs(Class< ? > cls) { + targetAsClass = cls; + return castThis(); + } + + @Override + public T targetAsBean() { + // To avoid ambiguity, reset any instruction to targetAsDTO + targetAsDTO = false; + targetAsJavaBean = true; + return castThis(); + } + + @Override + public T targetAsDTO() { + // To avoid ambiguity, reset any instruction to targetAsJavaBean + targetAsJavaBean = false; + targetAsDTO = true; + return castThis(); + } + + @Override + public T view() { + liveView = true; + return castThis(); + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/ArrayDelegate.java b/converter/converter/src/main/java/org/osgi/util/converter/ArrayDelegate.java new file mode 100644 index 00000000000..26a7065077a --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/ArrayDelegate.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.converter; + +import java.lang.reflect.Array; +import java.util.List; + +/** + * @author $Id$ + */ +class ArrayDelegate extends AbstractCollectionDelegate + implements List { + // An array, either scalar or primitive + private final Object backingArray; + + ArrayDelegate(Object arr) { + backingArray = arr; + } + + @Override + public int size() { + return Array.getLength(backingArray); + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public Object[] toArray() { + Object[] arr = (Object[]) Array.newInstance(Object.class, size()); + for (int i = 0; i < size(); i++) { + arr[i] = Array.get(backingArray, i); + } + return arr; + } + + @SuppressWarnings("unchecked") + @Override + public T get(int index) { + return (T) Array.get(backingArray, index); + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/CollectionDelegate.java b/converter/converter/src/main/java/org/osgi/util/converter/CollectionDelegate.java new file mode 100644 index 00000000000..e6e3022c0cd --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/CollectionDelegate.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.converter; + +import java.util.Collection; +import java.util.List; + +/** + * @author $Id$ + */ +class CollectionDelegate extends AbstractCollectionDelegate + implements List { + private final Collection delegate; + + CollectionDelegate(Collection coll) { + delegate = coll; + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public Object[] toArray() { + return delegate.toArray(); + } + + @SuppressWarnings("unchecked") + @Override + public T get(int index) { + Object[] arr = toArray(); + if (index > arr.length) + throw new IndexOutOfBoundsException("" + index); + return (T) arr[index]; + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/CollectionSetDelegate.java b/converter/converter/src/main/java/org/osgi/util/converter/CollectionSetDelegate.java new file mode 100644 index 00000000000..c1261c67b0a --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/CollectionSetDelegate.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.converter; + +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * @author $Id$ + */ +class CollectionSetDelegate implements Set { + private final Collection delegate; + + CollectionSetDelegate(Collection coll) { + delegate = coll; + } + + private Set setSnapshot() { + Set s = new LinkedHashSet<>(); + for (T o : delegate) { + s.add(o); + } + return s; + } + + @Override + public int size() { + return toArray().length; + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return delegate.contains(o); + } + + @Override + public Iterator iterator() { + return setSnapshot().iterator(); + } + + @Override + public Object[] toArray() { + return toArray(new Object[] {}); + } + + @Override + public X[] toArray(X[] a) { + Set s = setSnapshot(); + return s.toArray(a); + } + + @Override + public boolean add(Object e) { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public boolean containsAll(Collection< ? > c) { + return delegate.containsAll(c); + } + + @Override + public boolean addAll(Collection< ? extends T> c) { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public boolean retainAll(Collection< ? > c) { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public boolean removeAll(Collection< ? > c) { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + @Override + public boolean equals(Object obj) { + throw new UnsupportedOperationException(); // Never called + } + + @Override + public String toString() { + return delegate.toString(); + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/ConversionException.java b/converter/converter/src/main/java/org/osgi/util/converter/ConversionException.java new file mode 100644 index 00000000000..962595f4269 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/ConversionException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.osgi.util.converter; + +/** + * This Runtime Exception is thrown when an object is requested to be converted + * but the conversion cannot be done. For example when the String "test" is to + * be converted into a Long. + * + * @author $Id$ + */ +public class ConversionException extends RuntimeException { + private static final long serialVersionUID = 1L; + + /** + * Create a Conversion Exception with a message. + * + * @param message The message for this exception. + */ + public ConversionException(String message) { + super(message); + } + + /** + * Create a Conversion Exception with a message and a nested cause. + * + * @param message The message for this exception. + * @param cause The causing exception. + */ + public ConversionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/Converter.java b/converter/converter/src/main/java/org/osgi/util/converter/Converter.java new file mode 100644 index 00000000000..812da9b6f55 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/Converter.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.osgi.util.converter; + +import org.osgi.annotation.versioning.ProviderType; + +/** + * The Converter service is used to start a conversion. The service is obtained + * from the service registry. The conversion is then completed via the + * Converting interface that has methods to specify the target type. + * + * @author $Id$ + */ +@ProviderType +public interface Converter { + /** + * Start a conversion for the given object. + * + * @param obj The object that should be converted. + * @return A {@link Converting} object to complete the conversion. + */ + Converting convert(Object obj); + + /** + * Start defining a function that can perform given conversions. + * + * @return A {@link Functioning} object to complete the definition. + */ + Functioning function(); + + /** + * Obtain a builder to create a modified converter based on this converter. + * For more details see the {@link ConverterBuilder} interface. + * + * @return A new Converter Builder. + */ + ConverterBuilder newConverterBuilder(); +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/ConverterBuilder.java b/converter/converter/src/main/java/org/osgi/util/converter/ConverterBuilder.java new file mode 100644 index 00000000000..24c778fadf3 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/ConverterBuilder.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) OSGi Alliance (2017, 2018). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.osgi.util.converter; + +import java.lang.reflect.Type; + +import org.osgi.annotation.versioning.ProviderType; + +/** + * A builder to create a new converter with modified behavior based on an + * existing converter. The modified behavior is specified by providing rules + * and/or conversion functions. If multiple rules match they will be visited in + * sequence of registration. If a rule's function returns {@code null} the next + * rule found will be visited. If none of the rules can handle the conversion, + * the original converter will be used to perform the conversion. + * + * @author $Id$ + */ +@ProviderType +public interface ConverterBuilder { + /** + * Build the specified converter. Each time this method is called a new + * custom converter is produced based on the rules registered with the + * builder. + * + * @return A new converter with the rules provided to the builder. + */ + Converter build(); + + /** + * Register a custom error handler. The custom error handler will be called + * when the conversion would otherwise throw an exception. The error handler + * can either throw a different exception or return a value to be used for + * the failed conversion. + * + * @param func The function to be used to handle errors. + * @return This converter builder for further building. + */ + ConverterBuilder errorHandler(ConverterFunction func); + + /** + * Register a conversion rule for this converter. Note that only the target + * type is specified, so the rule will be visited for every conversion to + * the target type. + * + * @param type The type that this rule will produce. + * @param func The function that will handle the conversion. + * @return This converter builder for further building. + */ + ConverterBuilder rule(Type type, ConverterFunction func); + + /** + * Register a conversion rule for this converter. + * + * @param rule A rule implementation. + * @return This converter builder for further building. + */ + ConverterBuilder rule(TargetRule rule); + + /** + * Register a catch-all rule, will be called of no other rule matches. + * + * @param func The function that will handle the conversion. + * @return This converter builder for further building. + */ + ConverterBuilder rule(ConverterFunction func); +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/ConverterBuilderImpl.java b/converter/converter/src/main/java/org/osgi/util/converter/ConverterBuilderImpl.java new file mode 100644 index 00000000000..30edb206ea5 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/ConverterBuilderImpl.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.converter; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author $Id$ + */ +class ConverterBuilderImpl implements ConverterBuilder { + private final InternalConverter converter; + private final Map> rules = new HashMap<>(); + private final List catchAllRules = new ArrayList<>(); + private final List errorHandlers = new ArrayList<>(); + + public ConverterBuilderImpl(InternalConverter c) { + this.converter = c; + } + + @Override + public InternalConverter build() { + return new CustomConverterImpl(converter, rules, catchAllRules, + errorHandlers); + } + + @Override + public ConverterBuilder errorHandler(ConverterFunction func) { + errorHandlers.add(func); + return this; + } + + @Override + public ConverterBuilder rule(ConverterFunction func) { + catchAllRules.add(func); + return this; + } + + @Override + public ConverterBuilder rule(Type t, ConverterFunction func) { + getRulesList(t).add(func); + return this; + } + + @Override + public ConverterBuilder rule(TargetRule rule) { + Type type = rule.getTargetType(); + getRulesList(type).add(rule.getFunction()); + + if (type instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType) type; + + boolean containsWildCard = false; + for (Type t : pt.getActualTypeArguments()) { + if (t instanceof WildcardType) { + containsWildCard = true; + break; + } + } + + // If the parameterized type is a wildcard (e.g. '?') then register + // also the raw + // type for the rule. I.e Class will also be registered under + // bare Class. + if (containsWildCard) + getRulesList(pt.getRawType()).add(rule.getFunction()); + } + + return this; + } + + private List getRulesList(Type type) { + List l = rules.get(type); + if (l == null) { + l = new ArrayList<>(); + rules.put(type, l); + } + return l; + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/ConverterFunction.java b/converter/converter/src/main/java/org/osgi/util/converter/ConverterFunction.java new file mode 100644 index 00000000000..98afbf4376a --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/ConverterFunction.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.osgi.util.converter; + +import java.lang.reflect.Type; + +import org.osgi.annotation.versioning.ConsumerType; + +/** + * An functional interface with a convert method that is passed the original + * object and the target type to perform a custom conversion. + *

      + * This interface can also be used to register a custom error handler. + * + * @author $Id$ + */ +@ConsumerType +public interface ConverterFunction { + /** + * Special object to indicate that a custom converter rule or error handler + * cannot handle the conversion. + */ + static final Object CANNOT_HANDLE = new Object(); + + /** + * Convert the object into the target type. + * + * @param obj The object to be converted. This object will never be + * {@code null} as the convert function will not be invoked for + * null values. + * @param targetType The target type. + * @return The conversion result or {@link #CANNOT_HANDLE} to indicate that + * the convert function cannot handle this conversion. In this case + * the next matching rule or parent converter will be given a + * opportunity to convert. + * @throws Exception the operation can throw an exception if the conversion + * can not be performed due to incompatible types. + */ + Object apply(Object obj, Type targetType) throws Exception; +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/ConverterImpl.java b/converter/converter/src/main/java/org/osgi/util/converter/ConverterImpl.java new file mode 100644 index 00000000000..84b52d6cfb5 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/ConverterImpl.java @@ -0,0 +1,360 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.converter; + +import java.lang.reflect.Method; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; +import java.util.UUID; +import java.util.regex.Pattern; + +import org.osgi.util.function.Function; + +/** + * Top-level implementation of the Converter. This class contains a number of + * rules that cover 'special cases'. + *

      + * Note that this class avoids lambda's and hard dependencies on Java-8 (or + * later) types to also work under Java 7. + * + * @author $Id$ + */ +class ConverterImpl implements InternalConverter { + static final SimpleDateFormat ISO8601_DATE_FORMAT = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ssXXX"); + static { + ISO8601_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + @Override + public InternalConverting convert(Object obj) { + return new ConvertingImpl(this, obj); + } + + @Override + public Functioning function() { + return new FunctioningImpl(this); + } + + void addStandardRules(ConverterBuilder cb) { + // Not written using lambda's because this code needs to run with Java 7 + cb.rule(new Rule(new Function() { + @Override + public String apply(Calendar f) { + synchronized (ISO8601_DATE_FORMAT) { + return ISO8601_DATE_FORMAT.format(f.getTime()); + } + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule(new Function() { + @Override + public Calendar apply(String f) { + try { + synchronized (ISO8601_DATE_FORMAT) { + Calendar cc = Calendar.getInstance(); + cc.setTime(ISO8601_DATE_FORMAT.parse(f)); + return cc; + } + } catch (ParseException e) { + throw new ConversionException( + "Cannot convert " + f + " to Date", e); + } + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule(new Function() { + @Override + public Long apply(Calendar f) { + return Long.valueOf(f.getTime().getTime()); + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule(new Function() { + @Override + public Calendar apply(Long f) { + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(f.longValue()); + return c; + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule(new Function() { + @Override + public Boolean apply(Character c) { + return Boolean.valueOf(c.charValue() != 0); + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule(new Function() { + @Override + public Character apply(Boolean b) { + return Character + .valueOf(b.booleanValue() ? (char) 1 : (char) 0); + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule(new Function() { + @Override + public Integer apply(Character c) { + return Integer.valueOf(c.charValue()); + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule(new Function() { + @Override + public Long apply(Character c) { + return Long.valueOf(c.charValue()); + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule(new Function() { + @Override + public Character apply(String f) { + return Character + .valueOf(f.length() > 0 ? f.charAt(0) : (char) 0); + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule>(new Function>() { + @Override + public Class< ? > apply(String cn) { + return loadClassUnchecked(cn); + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule(new Function() { + @Override + public Long apply(Date d) { + return Long.valueOf(d.getTime()); + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule(new Function() { + @Override + public Date apply(Long f) { + return new Date(f.longValue()); + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule(new Function() { + @Override + public String apply(Date d) { + synchronized (ISO8601_DATE_FORMAT) { + return ISO8601_DATE_FORMAT.format(d); + } + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule(new Function() { + @Override + public Date apply(String f) { + try { + synchronized (ISO8601_DATE_FORMAT) { + return ISO8601_DATE_FORMAT.parse(f); + } + } catch (ParseException e) { + throw new ConversionException( + "Cannot convert " + f + " to Date", e); + } + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule(new Function() { + @Override + public Pattern apply(String ps) { + return Pattern.compile(ps); + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule(new Function() { + @Override + public UUID apply(String uuid) { + return UUID.fromString(uuid); + } + }) { + // empty subclass to capture generics + }); + + // Special conversions between character arrays and String + cb.rule(new Rule(new Function() { + @Override + public String apply(char[] ca) { + return charArrayToString(ca); + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule( + new Function() { + @Override + public String apply(Character[] ca) { + return characterArrayToString(ca); + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule(new Function() { + @Override + public char[] apply(String s) { + return stringToCharArray(s); + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule( + new Function() { + @Override + public Character[] apply(String s) { + return stringToCharacterArray(s); + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule(new Function() { + @Override + public Boolean apply(Number obj) { + return Boolean.valueOf(obj.longValue() != 0); + } + }) { + // empty subclass to capture generics + }); + + cb.rule(new Rule(new Function() { + @Override + public Character apply(Number obj) { + return Character.valueOf((char) obj.intValue()); + } + }) { + // empty subclass to capture generics + }); + + reflectiveAddJavaTimeRule(cb, "java.time.LocalDateTime"); + reflectiveAddJavaTimeRule(cb, "java.time.LocalDate"); + reflectiveAddJavaTimeRule(cb, "java.time.LocalTime"); + reflectiveAddJavaTimeRule(cb, "java.time.OffsetDateTime"); + reflectiveAddJavaTimeRule(cb, "java.time.OffsetTime"); + reflectiveAddJavaTimeRule(cb, "java.time.ZonedDateTime"); + } + + private void reflectiveAddJavaTimeRule(ConverterBuilder cb, + String timeClsName) { + try { + final Class< ? > toCls = getClass().getClassLoader() + .loadClass(timeClsName); + final Method toMethod = toCls.getMethod("parse", + CharSequence.class); + + cb.rule(new TypeRule(String.class, toCls, + new Function() { + @Override + public Object apply(String f) { + try { + return toMethod.invoke(null, f); + } catch (Exception e) { + throw new ConversionException( + "Problem converting to " + toCls, e); + } + } + })); + cb.rule(new TypeRule(toCls, String.class, + new Function() { + @Override + public String apply(Object t) { + return t.toString(); + } + })); + } catch (Exception ex) { + // Class not available, do not add rule for it + } + } + + String charArrayToString(char[] ca) { + StringBuilder sb = new StringBuilder(ca.length); + for (char c : ca) { + sb.append(c); + } + return sb.toString(); + } + + String characterArrayToString(Character[] ca) { + return charArrayToString(convert(ca).to(char[].class)); + } + + char[] stringToCharArray(String s) { + char[] ca = new char[s.length()]; + + for (int i = 0; i < s.length(); i++) { + ca[i] = s.charAt(i); + } + return ca; + } + + Character[] stringToCharacterArray(String s) { + return convert(stringToCharArray(s)).to(Character[].class); + } + + Class< ? > loadClassUnchecked(String className) { + try { + return getClass().getClassLoader().loadClass(className); + } catch (ClassNotFoundException e) { + throw new NoClassDefFoundError(className); + } + } + + @Override + public ConverterBuilderImpl newConverterBuilder() { + return new ConverterBuilderImpl(this); + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/Converters.java b/converter/converter/src/main/java/org/osgi/util/converter/Converters.java new file mode 100644 index 00000000000..7a76d4cb44c --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/Converters.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.converter; + +/** + * Factory class to obtain the standard converter or a new converter builder. + * + * @author $Id$ + */ +public class Converters { + private static final Converter CONVERTER; + + static { + ConverterImpl impl = new ConverterImpl(); + ConverterBuilder cb = impl.newConverterBuilder(); + impl.addStandardRules(cb); + CONVERTER = cb.build(); + } + + private Converters() { + // Do not instantiate this factory class + } + + /** + * Obtain the standard converter. + * + * @return The standard converter. + */ + public static Converter standardConverter() { + return CONVERTER; + } + + /** + * Obtain a converter builder based on the standard converter. + * + * @return A new converter builder. + */ + public static ConverterBuilder newConverterBuilder() { + return CONVERTER.newConverterBuilder(); + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/Converting.java b/converter/converter/src/main/java/org/osgi/util/converter/Converting.java new file mode 100644 index 00000000000..afcab503ee6 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/Converting.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.osgi.util.converter; + +import java.lang.reflect.Type; + +import org.osgi.annotation.versioning.ProviderType; + +/** + * This interface is used to specify the target that an object should be + * converted to. A {@link Converting} instance can be obtained via the + * {@link Converter}. + * + * @author $Id$ + */ +@ProviderType +public interface Converting extends Specifying { + /** + * Specify the target object type for the conversion as a class object. + * + * @param cls The class to convert to. + * @param The type to convert to. + * @return The converted object. + */ + T to(Class cls); + + /** + * Specify the target object type as a Java Reflection Type object. + * + * @param type A Type object to represent the target type to be converted + * to. + * @param The type to convert to. + * @return The converted object. + */ + T to(Type type); + + /** + * Specify the target object type as a {@link TypeReference}. If the target + * class carries generics information a TypeReference should be used as this + * preserves the generic information whereas a Class object has this + * information erased. Example use: + * + *

      +	 * List<String> result = converter.convert(Arrays.asList(1, 2, 3))
      +	 * 		.to(new TypeReference<List<String>>() {});
      +	 * 
      + * + * @param ref A type reference to the object being converted to. + * @param The type to convert to. + * @return The converted object. + */ + T to(TypeReference ref); +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/ConvertingImpl.java b/converter/converter/src/main/java/org/osgi/util/converter/ConvertingImpl.java new file mode 100644 index 00000000000..8f965492e93 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/ConvertingImpl.java @@ -0,0 +1,1285 @@ +/* + * 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.osgi.util.converter; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.Queue; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentNavigableMap; +import java.util.concurrent.ConcurrentSkipListMap; + +/** + * @author $Id$ + */ +class ConvertingImpl extends AbstractSpecifying + implements Converting, InternalConverting { + private static final Map,Class< ? >> INTERFACE_IMPLS; + // Interfaces with no methods are also not considered + private static final Collection> NO_MAP_VIEW_TYPES; + static { + + Map,Class< ? >> cim = new HashMap<>(); + cim.put(Collection.class, ArrayList.class); + // Lists + cim.put(List.class, ArrayList.class); + // Sets + cim.put(Set.class, LinkedHashSet.class); // preserves insertion order + cim.put(NavigableSet.class, TreeSet.class); + cim.put(SortedSet.class, TreeSet.class); + // Queues + cim.put(Queue.class, LinkedList.class); + cim.put(Deque.class, LinkedList.class); + + Map,Class< ? >> iim = new HashMap<>(cim); + // Maps + iim.put(Map.class, LinkedHashMap.class); // preserves insertion order + iim.put(ConcurrentMap.class, ConcurrentHashMap.class); + iim.put(ConcurrentNavigableMap.class, ConcurrentSkipListMap.class); + iim.put(NavigableMap.class, TreeMap.class); + iim.put(SortedMap.class, TreeMap.class); + + Set> nmv = new HashSet<>(cim.keySet()); + nmv.addAll(Arrays.> asList(String.class, Class.class, + Comparable.class, CharSequence.class, Map.Entry.class)); + + INTERFACE_IMPLS = Collections.unmodifiableMap(iim); + NO_MAP_VIEW_TYPES = Collections.unmodifiableSet(nmv); + } + + private final InternalConverter converter; + private volatile Object object; + private volatile Class< ? > sourceClass; + private volatile Class< ? > targetClass; + private volatile Type[] typeArguments; + private volatile Type targetType; + + ConvertingImpl(InternalConverter c, Object obj) { + converter = c; + object = obj; + } + + @SuppressWarnings("unchecked") + @Override + public T to(Class cls) { + Type type = cls; + return (T) to(type); + } + + @SuppressWarnings("unchecked") + @Override + public T to(TypeReference ref) { + return (T) to(ref.getType()); + } + + @SuppressWarnings("unchecked") + @Override + public Object to(Type type) { + return to(type, converter); + } + + @SuppressWarnings("unchecked") + @Override + public Object to(Type type, InternalConverter c) { + // Wildcard types are strange - we immediately resolve them to something + // that we can actually use. + if (type instanceof WildcardType) { + WildcardType wt = (WildcardType) type; + Type[] lowerBounds = wt.getLowerBounds(); + if(lowerBounds.length != 0) { + // This is a ? super X generic, why on earth would you do this? + throw new ConversionException("The authors of this implementation have no idea what to do with the type variable " + + wt.getTypeName() + ". The use of is highly ambiguous for the converter"); + } else { + type = wt.getUpperBounds()[0]; + } + } + + Class< ? > cls = null; + if (type instanceof Class) { + cls = (Class< ? >) type; + } else if (type instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType) type; + Type rt = pt.getRawType(); + typeArguments = pt.getActualTypeArguments(); + if (rt instanceof Class) + cls = (Class< ? >) rt; + } else if (type instanceof GenericArrayType) { + GenericArrayType pt = (GenericArrayType) type; + Type rt = pt.getGenericComponentType(); + if (rt instanceof Class) + cls = (Class< ? >) rt; + else if (rt instanceof ParameterizedType) { + Type rt2 = ((ParameterizedType) rt).getRawType(); + if (rt2 instanceof Class) { + cls = (Class< ? >) rt2; + } + } + + } + targetType = type; + if (cls == null) + return null; + + if (object == null) + return handleNull(cls, c); + + targetClass = Util.primitiveToBoxed(cls); + if (targetAsClass == null) + targetAsClass = targetClass; + + sourceClass = sourceAsClass != null ? sourceAsClass : object.getClass(); + + if (!isCopyRequiredType(targetAsClass) + && targetAsClass.isAssignableFrom(sourceClass)) { + return object; + } + + Object res = trySpecialCases(c); + if (res != null) + return res; + + if (targetAsClass.isArray()) { + return convertToArray(targetAsClass.getComponentType(), + targetAsClass.getComponentType(), c); + } else if (type instanceof GenericArrayType) { + return convertToArray(targetAsClass, + ((GenericArrayType) type).getGenericComponentType(), c); + } else if (Collection.class.isAssignableFrom(targetAsClass)) { + return convertToCollectionType(c); + } else if (isMapType(targetAsClass, targetAsJavaBean, targetAsDTO)) { + return convertToMapType(c); + } + + // At this point we know that the target is a 'singular' type: not a + // map, collection or array + if (Collection.class.isAssignableFrom(sourceClass)) { + return convertCollectionToSingleValue(targetAsClass, c); + } else if (isMapType(sourceClass, sourceAsJavaBean, sourceAsDTO)) { + return convertMapToSingleValue(targetAsClass, c); + } else if (object instanceof Map.Entry) { + return convertMapEntryToSingleValue(targetAsClass, c); + } else if ((object = asBoxedArray(object)) instanceof Object[]) { + return convertArrayToSingleValue(targetAsClass, c); + } + + Object res2 = tryStandardMethods(); + if (res2 != null) { + return res2; + } else { + if (hasDefault) + return c.convert(defaultValue) + .sourceAs(sourceAsClass) + .targetAs(targetAsClass) + .to(targetClass); + else + throw new ConversionException( + "Cannot convert " + object + " to " + targetAsClass); + } + } + + private Object convertArrayToSingleValue(Class< ? > cls, InternalConverter c) { + Object[] arr = (Object[]) object; + if (arr.length == 0) + return null; + else + return c.convert(arr[0]).to(cls); + } + + private Object convertCollectionToSingleValue(Class< ? > cls, InternalConverter c) { + Collection< ? > coll = (Collection< ? >) object; + if (coll.size() == 0) + return null; + else + return c.convert(coll.iterator().next()).to(cls); + } + + private Object convertMapToSingleValue(Class< ? > cls, InternalConverter c) { + Map< ? , ? > m = mapView(object, sourceClass, c); + if (m.size() > 0) { + return c.convert(m.entrySet().iterator().next()).to(cls); + } else { + return null; + } + } + + @SuppressWarnings("rawtypes") + private Object convertMapEntryToSingleValue(Class< ? > cls, InternalConverter c) { + Map.Entry entry = (Map.Entry) object; + + Class keyCls = entry.getKey() != null ? entry.getKey().getClass() + : null; + Class valueCls = entry.getValue() != null ? entry.getValue().getClass() + : null; + + if (cls.equals(keyCls)) { + return c.convert(entry.getKey()).to(cls); + } else if (cls.equals(valueCls)) { + return c.convert(entry.getValue()).to(cls); + } else if (cls.isAssignableFrom(keyCls)) { + return c.convert(entry.getKey()).to(cls); + } else if (cls.isAssignableFrom(valueCls)) { + return c.convert(entry.getValue()).to(cls); + } else if (entry.getKey() instanceof String) { + return c.convert(entry.getKey()).to(cls); + } else if (entry.getValue() instanceof String) { + return c.convert(entry.getValue()).to(cls); + } + + return c.convert(c.convert(entry.getKey()).to(String.class)) + .to(cls); + } + + @SuppressWarnings("unchecked") + private T convertToArray(Class< ? > componentClz, Type componentType, InternalConverter c) { + Collection< ? > collectionView = collectionView(c); + Iterator< ? > itertor = collectionView.iterator(); + try { + Object array = Array.newInstance(componentClz, + collectionView.size()); + for (int i = 0; i < collectionView.size() + && itertor.hasNext(); i++) { + Object next = itertor.next(); + Object converted = c.convert(next) + .to(componentType); + Array.set(array, i, converted); + } + return (T) array; + } catch (Exception e) { + return null; + } + } + + @SuppressWarnings("unchecked") + private T convertToCollectionType(InternalConverter c) { + Collection< ? > res = convertToCollectionDelegate(c); + if (res != null) + return (T) res; + + return convertToCollection(c); + } + + private Collection< ? > convertToCollectionDelegate(InternalConverter c) { + if (!liveView) + return null; + + if (List.class.equals(targetClass) + || Collection.class.equals(targetClass)) { + if (sourceClass.isArray()) { + return ListDelegate.forArray(object, this, c); + } else if (Collection.class.isAssignableFrom(sourceClass)) { + return ListDelegate.forCollection((Collection< ? >) object, + this, c); + } + } else if (Set.class.equals(targetClass)) { + if (sourceClass.isArray()) { + return SetDelegate.forCollection( + ListDelegate.forArray(object, this, c), this, c); + } else if (Collection.class.isAssignableFrom(sourceClass)) { + return SetDelegate.forCollection((Collection< ? >) object, + this, c); + } + } + return null; + } + + @SuppressWarnings({ + "rawtypes", "unchecked" + }) + private T convertToCollection(InternalConverter c) { + Collection< ? > cv = collectionView(c); + Class< ? > targetElementType = null; + if (typeArguments != null && typeArguments.length > 0 + && typeArguments[0] instanceof Class) { + targetElementType = (Class< ? >) typeArguments[0]; + } + + Class< ? > ctrCls = INTERFACE_IMPLS.get(targetAsClass); + Class< ? > targetCls; + if (ctrCls != null) + targetCls = ctrCls; + else + targetCls = targetAsClass; + + Collection instance = (Collection) createMapOrCollection(targetCls, + cv.size()); + if (instance == null) + return null; + + for (Object o : cv) { + if (targetElementType != null) { + try { + o = c.convert(o).to(targetElementType); + } catch (ConversionException ce) { + if (hasDefault) { + return (T) defaultValue; + } + } + } + + instance.add(o); + } + + return (T) instance; + } + + @SuppressWarnings({ + "rawtypes", "unchecked" + }) + private T convertToDTO(Class< ? > sourceCls, Class< ? > targetAsCls, InternalConverter c) { + Map m = mapView(object, sourceCls, c); + + try { + String prefix = Util.getPrefix(targetAsCls); + + T dto = (T) targetClass.newInstance(); + + List names = getNames(targetAsClass); + for (Map.Entry entry : (Set) m.entrySet()) { + Object key = entry.getKey(); + if (key == null) + continue; + + String fieldName = Util.mangleName(prefix, key.toString(), names); + if (fieldName == null) + continue; + + Field f = null; + try { + f = targetAsCls.getField(fieldName); + } catch (NoSuchFieldException e) { + // There is no field with this name + if (keysIgnoreCase) { + // If enabled, try again but now ignore case + for (Field fs : targetAsCls.getFields()) { + if (fs.getName().equalsIgnoreCase(fieldName)) { + f = fs; + break; + } + } + + if (f == null) { + for (Field fs : targetAsCls.getFields()) { + if (fs.getName() + .equalsIgnoreCase(fieldName)) { + f = fs; + break; + } + } + } + } + } + + if (f != null) { + Object val = entry.getValue(); + if (sourceAsDTO && DTOUtil.isDTOType(f.getType(), false)) + val = c.convert(val).sourceAsDTO().to( + f.getType()); + else { + Type genericType = reifyType(f.getGenericType(), + targetAsClass, typeArguments); + val = c.convert(val).to(genericType); + } + f.set(dto, val); + } + } + + return dto; + } catch (Exception e) { + throw new ConversionException("Cannot create DTO " + targetClass, + e); + } + } + + static Type reifyType(Type typeToReify, Class< ? > ownerClass, + Type[] typeArgs) { + + if (typeToReify instanceof TypeVariable) { + String name = ((TypeVariable< ? >) typeToReify).getName(); + for (int i = 0; i < ownerClass.getTypeParameters().length; i++) { + TypeVariable< ? > typeVariable = ownerClass + .getTypeParameters()[i]; + if (typeVariable.getName().equals(name)) { + return typeArgs[i]; + } + } + + // The direct type variable wasn't found, maybe it was already + // bound in this class. + + Type currentType = ownerClass; + while (currentType != null) { + if (currentType instanceof Class) { + currentType = ((Class< ? >) currentType) + .getGenericSuperclass(); + } else if (currentType instanceof ParameterizedType) { + currentType = ((ParameterizedType) currentType) + .getRawType(); + } + + if (currentType instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType) currentType; + Type rawType = pt.getRawType(); + if (rawType instanceof Class) { + return reifyType(typeToReify, (Class< ? >) rawType, + pt.getActualTypeArguments()); + } + } + } + } else if (typeToReify instanceof ParameterizedType) { + final ParameterizedType parameterizedType = (ParameterizedType) typeToReify; + Type[] parameters = parameterizedType.getActualTypeArguments(); + boolean useCopy = false; + final Type[] copiedParameters = new Type[parameters.length]; + + for (int i = 0; i < parameters.length; i++) { + copiedParameters[i] = reifyType(parameters[i], ownerClass, + typeArgs); + useCopy |= copiedParameters[i] != parameters[i]; + } + + if (useCopy) { + return new ParameterizedType() { + + @Override + public Type getRawType() { + return parameterizedType.getRawType(); + } + + @Override + public Type getOwnerType() { + return parameterizedType.getOwnerType(); + } + + @Override + public Type[] getActualTypeArguments() { + return Arrays.copyOf(copiedParameters, + copiedParameters.length); + } + }; + } + } else if (typeToReify instanceof GenericArrayType) { + GenericArrayType type = (GenericArrayType) typeToReify; + Type genericComponentType = type.getGenericComponentType(); + final Type reifiedType = reifyType(genericComponentType, ownerClass, + typeArgs); + + if (reifiedType != genericComponentType) { + return new GenericArrayType() { + + @Override + public Type getGenericComponentType() { + return reifiedType; + } + }; + } + } + + return typeToReify; + } + + private List getNames(Class< ? > cls) { + List names = new ArrayList<>(); + for (Field field : cls.getFields()) { + int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers)) + continue; + + String name = field.getName(); + if (!names.contains(name)) + names.add(name); + + } + return names; + } + + @SuppressWarnings({ + "rawtypes", "unchecked" + }) + private Map convertToMap(InternalConverter c) { + Map m = mapView(object, sourceClass, c); + if (m == null) + return null; + + Class< ? > ctrCls = INTERFACE_IMPLS.get(targetClass); + if (ctrCls == null) + ctrCls = targetClass; + + Map instance = (Map) createMapOrCollection(ctrCls, m.size()); + if (instance == null) + return null; + + for (Map.Entry entry : (Set) m.entrySet()) { + Object key = entry.getKey(); + Object value = entry.getValue(); + key = convertMapKey(key, c); + value = convertMapValue(value, c); + instance.put(key, value); + } + + return instance; + } + + Object convertCollectionValue(Object element, InternalConverter c) { + Type type = null; + if (typeArguments != null && typeArguments.length > 0) { + type = typeArguments[0]; + } + + if (element != null) { + if (type != null) { + element = c.convert(element).to(type); + } else { + Class< ? > cls = element.getClass(); + if (isCopyRequiredType(cls)) { + cls = getConstructableType(cls); + } + // Either force source as DTO, or lenient DTO type + if (sourceAsDTO || DTOUtil.isDTOType(cls, true)) + element = c.convert(element).sourceAsDTO().to(cls); + else + element = c.convert(element).to(cls); + } + } + return element; + } + + Object convertMapKey(Object key, InternalConverter c) { + return convertMapElement(key, 0, c); + } + + Object convertMapValue(Object value, InternalConverter c) { + return convertMapElement(value, 1, c); + } + + private Object convertMapElement(Object element, int typeIdx, InternalConverter c) { + Type type = null; + if (typeArguments != null && typeArguments.length > typeIdx) { + type = typeArguments[typeIdx]; + } + + if (element != null) { + if (type != null) { + element = c.convert(element).to(type); + } else { + Class< ? > cls = element.getClass(); + if (isCopyRequiredType(cls)) { + cls = getConstructableType(cls); + } + // Either force source as DTO, or DTO type + if (sourceAsDTO || DTOUtil.isDTOType(cls, false)) + element = c.convert(element).sourceAsDTO().to(cls); + else + element = c.convert(element).to(cls); + } + } + return element; + } + + @SuppressWarnings({ + "unchecked", "rawtypes" + }) + private Map convertToMapDelegate(InternalConverter c) { + if (Map.class.isAssignableFrom(sourceClass)) { + return MapDelegate.forMap((Map) object, this, c); + } else if (Dictionary.class.isAssignableFrom(sourceClass)) { + return MapDelegate.forDictionary((Dictionary) object, this, c); + } else if (DTOUtil.isDTOType(sourceClass, true) || sourceAsDTO) { + return MapDelegate.forDTO(object, sourceClass, this, c); + } else if (sourceAsJavaBean) { + return MapDelegate.forBean(object, sourceClass, this, c); + } else if (hasGetProperties(sourceClass)) { + return null; // Handled in convertToMap() + } + + // Assume it's an interface + Set> interfaces = getInterfaces(sourceClass); + if (interfaces.size() > 0) { + return MapDelegate.forInterface(object, + interfaces.iterator().next(), this, c); + } + return null; + } + + @SuppressWarnings("rawtypes") + private Object convertToMapType(InternalConverter c) { + if (!isMapType(sourceClass, sourceAsJavaBean, sourceAsDTO)) { + throw new ConversionException( + "Cannot convert " + object + " to " + targetAsClass); + } + + if (Map.class.equals(targetClass) && liveView) { + Map res = convertToMapDelegate(c); + if (res != null) + return res; + } + + if (Map.class.isAssignableFrom(targetAsClass)) + return convertToMap(c); + else if (Dictionary.class.isAssignableFrom(targetAsClass)) + return convertToDictionary(c); + else if (targetAsDTO || DTOUtil.isDTOType(targetAsClass, false)) + return convertToDTO(sourceClass, targetAsClass, c); + else if (targetAsClass.isInterface()) + return convertToInterface(sourceClass, targetAsClass, c); + else if (targetAsJavaBean) + return convertToJavaBean(sourceClass, targetAsClass, c); + throw new ConversionException( + "Cannot convert " + object + " to " + targetAsClass); + } + + @SuppressWarnings({ + "unchecked", "rawtypes" + }) + private Object convertToDictionary(InternalConverter c) { + return new Hashtable( + (Map) c.convert(object).to(new ParameterizedType() { + @Override + public Type getRawType() { + return HashMap.class; + } + + @Override + public Type getOwnerType() { + return null; + } + + @SuppressWarnings("synthetic-access") + @Override + public Type[] getActualTypeArguments() { + return typeArguments; + } + })); + } + + private Object convertToJavaBean(Class< ? > sourceCls, + Class< ? > targetCls, InternalConverter c) { + String prefix = Util.getPrefix(targetCls); + + @SuppressWarnings("rawtypes") + Map m = mapView(object, sourceCls, c); + try { + Object res = targetClass.newInstance(); + for (Method setter : getSetters(targetCls)) { + String setterName = setter.getName(); + StringBuilder propName = new StringBuilder(Character + .valueOf(Character.toLowerCase(setterName.charAt(3))) + .toString()); + if (setterName.length() > 4) + propName.append(setterName.substring(4)); + + Class< ? > setterType = setter.getParameterTypes()[0]; + String key = propName.toString(); + Object val = m.get(Util.unMangleName(prefix, key)); + setter.invoke(res, c.convert(val).to(setterType)); + } + return res; + } catch (Exception e) { + throw new ConversionException( + "Cannot convert to class: " + targetCls.getName() + + ". Not a JavaBean with a Zero-arg Constructor.", + e); + } + } + + @SuppressWarnings("rawtypes") + private Object convertToInterface(Class< ? > sourceCls, + final Class< ? > targetCls, InternalConverter c) { + InternalConverting ic = c.convert(object); + ic.sourceAs(sourceAsClass).view(); + if (sourceAsDTO) + ic.sourceAsDTO(); + if (sourceAsJavaBean) + ic.sourceAsBean(); + final Map m = ic.to(Map.class); + + return createProxy(targetCls, m, c); + } + + private Object createProxy(final Class< ? > cls, final Map< ? , ? > data, final InternalConverter c) { + return Proxy.newProxyInstance(cls.getClassLoader(), new Class[] { + cls + }, new InvocationHandler() { + @SuppressWarnings("boxing") + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + Class< ? > mdDecl = method.getDeclaringClass(); + if (mdDecl.equals(Object.class)) + switch (method.getName()) { + case "equals" : + return proxy == args[0]; + case "hashCode" : + return System.identityHashCode(proxy); + case "toString" : + return "Proxy for " + cls; + default : + throw new UnsupportedOperationException("Method " + + method + " not supported on proxy for " + + cls); + } + if (mdDecl.equals(Annotation.class)) { + if ("annotationType".equals(method.getName()) + && method.getParameterTypes().length == 0) { + return cls; + } + } + + String propName = Util.getInterfacePropertyName(method, + Util.getSingleElementAnnotationKey(cls, proxy), + proxy); + if (propName == null) + return null; + + Object val = data.get(propName); + if (val == null && keysIgnoreCase) { + // try in a case-insensitive way + for (Iterator< ? > it = data.keySet().iterator(); it + .hasNext() + && val == null;) { + String k = it.next().toString(); + if (propName.equalsIgnoreCase(k)) { + val = data.get(k); + } + } + } + + // If no value is available take the default if specified + if (val == null) { + if (cls.isAnnotation()) { + val = method.getDefaultValue(); + } + + if (val == null) { + if (args != null && args.length == 1) { + val = args[0]; + } else { + throw new ConversionException( + "No value for property: " + propName); + } + } + } + + @SuppressWarnings("synthetic-access") + Type genericType = reifyType(method.getGenericReturnType(), + targetAsClass, typeArguments); + return c.convert(val).to(genericType); + } + }); + } + + @SuppressWarnings("boxing") + private Object handleNull(Class< ? > cls, InternalConverter c) { + if (hasDefault) + return c.convert(defaultValue).to(cls); + + Class< ? > boxed = Util.primitiveToBoxed(cls); + if (boxed.equals(cls)) { + if (cls.isArray()) { + return new Object[] {}; + } else if (Collection.class.isAssignableFrom(cls)) { + return c.convert(Collections.emptyList()).to(cls); + } + // This is not a primitive, just return null + return null; + } + + return c.convert(0).to(cls); + } + + private static boolean isMapType(Class< ? > cls, boolean asJavaBean, + boolean asDTO) { + if (asDTO) + return true; + + // All interface types that are not Collections are treated as maps + if (Map.class.isAssignableFrom(cls)) + return true; + else if (getInterfaces(cls).size() > 0) + return true; + else if (DTOUtil.isDTOType(cls, true)) + return true; + else if (asJavaBean && isWriteableJavaBean(cls)) + return true; + else + return Dictionary.class.isAssignableFrom(cls); + } + + @SuppressWarnings("boxing") + private Object trySpecialCases(InternalConverter c) { + if (Boolean.class.equals(targetAsClass)) { + if (object instanceof Collection + && ((Collection< ? >) object).size() == 0) { + return Boolean.FALSE; + } + } else if (Number.class.isAssignableFrom(targetAsClass)) { + if (object instanceof Boolean) { + return ((Boolean) object).booleanValue() ? 1 : 0; + } else if (object instanceof Number) { + if (Byte.class.isAssignableFrom(targetAsClass)) { + return ((Number) object).byteValue(); + } else if (Short.class.isAssignableFrom(targetAsClass)) { + return ((Number) object).shortValue(); + } else if (Integer.class.isAssignableFrom(targetAsClass)) { + return ((Number) object).intValue(); + } else if (Long.class.isAssignableFrom(targetAsClass)) { + return ((Number) object).longValue(); + } else if (Float.class.isAssignableFrom(targetAsClass)) { + return ((Number) object).floatValue(); + } else if (Double.class.isAssignableFrom(targetAsClass)) { + return ((Number) object).doubleValue(); + } + } + } else if (Enum.class.isAssignableFrom(targetAsClass)) { + if (object instanceof Number) { + try { + Method m = targetAsClass.getMethod("values"); + Object[] values = (Object[]) m.invoke(null); + return values[((Number) object).intValue()]; + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + try { + Method m = targetAsClass.getMethod("valueOf", String.class); + return m.invoke(null, object.toString()); + } catch (Exception e) { + try { + // Case insensitive fallback + Method m = targetAsClass.getMethod("values"); + for (Object v : (Object[]) m.invoke(null)) { + if (v.toString() + .equalsIgnoreCase(object.toString())) { + return v; + } + } + } catch (Exception e1) { + throw new RuntimeException(e1); + } + } + } + } else if (Annotation.class.isAssignableFrom(sourceClass) + && isMarkerAnnotation(sourceClass)) { + // Special treatment for marker annotations + String key = Util.getMarkerAnnotationKey(sourceClass, object); + return c.convert(Collections.singletonMap(key, Boolean.TRUE)) + .targetAs(targetAsClass) + .to(targetType); + } else if (Annotation.class.isAssignableFrom(targetAsClass) + && isMarkerAnnotation(targetAsClass)) { + Map representation = Converters.standardConverter() + .convert(object) + .to(new TypeReference>() { + /* empty subclass */ + }); + if (Boolean.TRUE.equals( + representation.get(Util.toSingleElementAnnotationKey( + targetAsClass.getSimpleName())))) { + return createProxy(targetClass, Collections.emptyMap(), c); + } else { + throw new ConversionException("Cannot convert " + object + + " to marker annotation " + targetAsClass); + } + } + return null; + } + + private static boolean isMarkerAnnotation(Class< ? > annClass) { + for (Method m : annClass.getMethods()) { + if (m.getDeclaringClass() != annClass) { + // this is a base annotation or object method + continue; + } + return false; + } + return true; + } + + @SuppressWarnings("unchecked") + private T tryStandardMethods() { + try { + // Section 707.4.2.3 and 707.4.2.5 require valueOf to be public and static + Method m = targetAsClass.getMethod("valueOf", String.class); + if (m != null && Modifier.isStatic(m.getModifiers())) { + return (T) m.invoke(null, object.toString()); + } + } catch (Exception e) { + try { + Constructor< ? > ctr = targetAsClass + .getConstructor(String.class); + return (T) ctr.newInstance(object.toString()); + } catch (Exception e2) { + // Ignore + } + } + return null; + } + + private Collection< ? > collectionView(InternalConverter conv) { + if (object == null) + return null; + + Collection< ? > c = asCollection(conv); + if (c == null) + return Collections.singleton(object); + else + return c; + } + + private Collection< ? > asCollection(InternalConverter c) { + if (object instanceof Collection) + return (Collection< ? >) object; + else if ((object = asBoxedArray(object)) instanceof Object[]) + return Arrays.asList((Object[]) object); + else if (isMapType(sourceClass, sourceAsJavaBean, sourceAsDTO)) + return mapView(object, sourceClass, c).entrySet(); + else + return null; + } + + private static Object asBoxedArray(Object obj) { + Class< ? > objClass = obj.getClass(); + if (!objClass.isArray()) + return obj; + + int len = Array.getLength(obj); + Object arr = Array.newInstance( + Util.primitiveToBoxed(objClass.getComponentType()), len); + for (int i = 0; i < len; i++) { + Object val = Array.get(obj, i); + Array.set(arr, i, val); + } + return arr; + } + + @SuppressWarnings("rawtypes") + private static Map createMapFromBeanAccessors(Object obj, + Class< ? > sourceCls) { + Set invokedMethods = new HashSet<>(); + + Map result = new HashMap(); + // Bean accessors must be public + for (Method md : sourceCls.getMethods()) { + handleBeanMethod(obj, md, invokedMethods, result); + } + + return result; + } + + @SuppressWarnings("rawtypes") + private Map createMapFromDTO(Object obj, InternalConverter ic) { + Set handledFields = new HashSet<>(); + + Map result = new HashMap(); + // We only use public fields for mapping a DTO + for (Field f : obj.getClass().getFields()) { + handleDTOField(obj, f, handledFields, result, ic); + } + return result; + } + + @SuppressWarnings({"unchecked","rawtypes"}) + private static Map createMapFromInterface(Object obj, Class< ? > srcCls) { + Map result = new HashMap(); + + if(Annotation.class.isAssignableFrom(srcCls) && isMarkerAnnotation(((Annotation)obj).annotationType())) { + // We special case this if the source is a marker annotation because we will end up with no + // interface methods otherwise + result.put(Util.getMarkerAnnotationKey(((Annotation)obj).annotationType(), obj), Boolean.TRUE); + return result; + } else { + for (Class i : getInterfaces(srcCls)) { + for (Method md : i.getMethods()) { + handleInterfaceMethod(obj, i, md, new HashSet(), + result); + } + if (result.size() > 0) + return result; + } + } + throw new ConversionException("Cannot be converted to map: " + obj); + } + + @SuppressWarnings("boxing") + private static Object createMapOrCollection(Class< ? > cls, + int initialSize) { + try { + Constructor< ? > ctor = cls.getConstructor(int.class); + return ctor.newInstance(initialSize); + } catch (Exception e1) { + try { + Constructor< ? > ctor2 = cls.getConstructor(); + return ctor2.newInstance(); + } catch (Exception e2) { + // ignore + } + } + return null; + } + + private static Class< ? > getConstructableType(Class< ? > targetCls) { + if (targetCls.isArray()) + return targetCls; + + Class< ? > cls = targetCls; + do { + try { + cls.getConstructor(int.class); + return cls; // If no exception the constructor is there + } catch (NoSuchMethodException e) { + try { + cls.getConstructor(); + return cls; // If no exception the constructor is there + } catch (NoSuchMethodException e1) { + // There is no constructor with this name + } + } + for (Class< ? > intf : cls.getInterfaces()) { + Class< ? > impl = INTERFACE_IMPLS.get(intf); + if (impl != null) + return impl; + } + + cls = cls.getSuperclass(); + } while (!Object.class.equals(cls)); + + return null; + } + + // Returns an ordered set + private static Set> getInterfaces(Class< ? > cls) { + if (NO_MAP_VIEW_TYPES.contains(cls)) + return Collections.emptySet(); + + Set> interfaces = getInterfaces0(cls); + outer: for (Iterator> it = interfaces.iterator(); it.hasNext();) { + Class< ? > intf = it.next(); + for (Method method : intf.getMethods()) { + if(method.getDeclaringClass() == intf) { + continue outer; + } + } + it.remove(); + } + + interfaces.removeAll(NO_MAP_VIEW_TYPES); + + return interfaces; + } + + // Returns an ordered set + private static Set> getInterfaces0(Class< ? > cls) { + if (cls == null) + return Collections.emptySet(); + + Set> classes = new LinkedHashSet<>(); + if (cls.isInterface()) { + classes.add(cls); + } + for (Class< ? > intf : cls.getInterfaces()) { + classes.addAll(getInterfaces(intf)); + } + + classes.addAll(getInterfaces(cls.getSuperclass())); + + return classes; + } + + @SuppressWarnings({ + "rawtypes", "unchecked" + }) + private void handleDTOField(Object obj, Field field, + Set handledFields, Map result, InternalConverter ic) { + String fn = Util.getDTOKey(field); + if (fn == null) + return; + + if (handledFields.contains(fn)) + return; // Field with this name was already handled + + try { + Object fVal = field.get(obj); + result.put(fn, fVal); + handledFields.add(fn); + } catch (Exception e) { + // Ignore + } + } + + @SuppressWarnings({ + "rawtypes", "unchecked" + }) + private static void handleBeanMethod(Object obj, Method md, + Set invokedMethods, Map res) { + String bp = Util.getBeanKey(md); + if (bp == null) + return; + + if (invokedMethods.contains(bp)) + return; // method with this name already invoked + + try { + res.put(bp, md.invoke(obj)); + invokedMethods.add(bp); + } catch (Exception e) { + // Ignore + } + } + + @SuppressWarnings({ + "rawtypes", "unchecked" + }) + private static void handleInterfaceMethod(Object obj, Class< ? > intf, + Method md, Set invokedMethods, Map res) { + String mn = md.getName(); + if (invokedMethods.contains(mn)) + return; // method with this name already invoked + + String propName = Util.getInterfacePropertyName(md, + Util.getSingleElementAnnotationKey(intf, obj), obj); + if (propName == null) + return; + + try { + Object r = Util.getInterfaceProperty(obj, md); + if (r == null) + return; + + res.put(propName, r); + invokedMethods.add(mn); + } catch (Exception e) { + // Ignore + } + } + + private Map< ? , ? > mapView(Object obj, Class< ? > sourceCls, + InternalConverter ic) { + if (Map.class.isAssignableFrom(sourceCls) + || (DTOUtil.isDTOType(sourceCls, true) && obj instanceof Map)) + return (Map< ? , ? >) obj; + else if (Dictionary.class.isAssignableFrom(sourceCls)) + return MapDelegate.forDictionary((Dictionary< ? , ? >) object, + this, ic); + else if (DTOUtil.isDTOType(sourceCls, true) || sourceAsDTO) + return createMapFromDTO(obj, ic); + else if (sourceAsJavaBean) { + Map< ? , ? > m = createMapFromBeanAccessors(obj, sourceCls); + if (m.size() > 0) + return m; + } else if (hasGetProperties(sourceCls)) { + return getPropertiesDelegate(obj, sourceCls, ic); + } + return createMapFromInterface(obj, sourceClass); + } + + private boolean hasGetProperties(Class< ? > cls) { + try { + // Section 707.4.4.4.8 says getProperties must be public + Method m = cls.getMethod("getProperties"); + return m != null; + } catch (Exception e) { + return false; + } + } + + private Map< ? , ? > getPropertiesDelegate(Object obj, Class< ? > cls, InternalConverter c) { + try { + // Section 707.4.4.4.8 says getProperties must be public + Method m = cls.getMethod("getProperties"); + + return c.convert(m.invoke(obj)).to(Map.class); + } catch (Exception e) { + return Collections.emptyMap(); + } + } + + private static boolean isCopyRequiredType(Class< ? > cls) { + if (cls.isEnum()) + return false; + return Map.class.isAssignableFrom(cls) + || Collection.class.isAssignableFrom(cls) + || DTOUtil.isDTOType(cls, true) || cls.isArray(); + } + + private static boolean isWriteableJavaBean(Class< ? > cls) { + boolean hasNoArgCtor = false; + for (Constructor< ? > ctor : cls.getConstructors()) { + if (ctor.getParameterTypes().length == 0) + hasNoArgCtor = true; + } + if (!hasNoArgCtor) + return false; // A JavaBean must have a public no-arg constructor + + return getSetters(cls).size() > 0; + } + + private static Set getSetters(Class< ? > cls) { + Set setters = new HashSet<>(); + while (!Object.class.equals(cls)) { + Set methods = new HashSet<>(); + // Only public methods can be Java Bean setters + methods.addAll(Arrays.asList(cls.getMethods())); + for (Method md : methods) { + if (md.getParameterTypes().length != 1) + continue; // Only setters with a single argument + String name = md.getName(); + if (name.length() < 4) + continue; + if (name.startsWith("set") + && Character.isUpperCase(name.charAt(3))) + setters.add(md); + } + cls = cls.getSuperclass(); + } + return setters; + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/CustomConverterImpl.java b/converter/converter/src/main/java/org/osgi/util/converter/CustomConverterImpl.java new file mode 100644 index 00000000000..e2dfbff97eb --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/CustomConverterImpl.java @@ -0,0 +1,269 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.converter; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * A custom converter wraps another converter by adding rules and/or error + * handlers. + * + * @author $Id$ + */ +class CustomConverterImpl implements InternalConverter { + private final InternalConverter delegate; + final Map> typeRules; + final List allRules; + final List errorHandlers; + + CustomConverterImpl(InternalConverter converter, + Map> rules, + List catchAllRules, + List errHandlers) { + delegate = converter; + typeRules = rules; + allRules = catchAllRules; + errorHandlers = errHandlers; + } + + @Override + public InternalConverting convert(Object obj) { + InternalConverting converting = delegate.convert(obj); + return new ConvertingWrapper(obj, converting, this); + } + + @Override + public Functioning function() { + return new FunctioningImpl(this); + } + + @Override + public ConverterBuilder newConverterBuilder() { + return new ConverterBuilderImpl(this); + } + + private class ConvertingWrapper implements InternalConverting { + private final InternalConverter converter; + private final InternalConverting del; + private final Object object; + private volatile Object defaultValue; + private volatile boolean hasDefault; + + ConvertingWrapper(Object obj, InternalConverting c, InternalConverter conv) { + converter = conv; + object = obj; + del = c; + } + + @Override + public Converting view() { + del.view(); + return this; + } + + @Override + public Converting defaultValue(Object defVal) { + del.defaultValue(defVal); + defaultValue = defVal; + hasDefault = true; + return this; + } + + @Override + public Converting keysIgnoreCase() { + del.keysIgnoreCase(); + return this; + } + + @Override + public Converting sourceAs(Class< ? > type) { + del.sourceAs(type); + return this; + } + + @Override + public Converting sourceAsBean() { + del.sourceAsBean(); + return this; + } + + @Override + public Converting sourceAsDTO() { + del.sourceAsDTO(); + return this; + } + + @Override + public Converting targetAs(Class< ? > cls) { + del.targetAs(cls); + return this; + } + + @Override + public Converting targetAsBean() { + del.targetAsBean(); + return this; + } + + @Override + public Converting targetAsDTO() { + del.targetAsDTO(); + return this; + } + + @SuppressWarnings("unchecked") + @Override + public T to(Class cls) { + Type type = cls; + return (T) to(type); + } + + @SuppressWarnings("unchecked") + @Override + public T to(TypeReference ref) { + return (T) to(ref.getType()); + } + + @SuppressWarnings("unchecked") + @Override + public Object to(Type type) { + return to(type, converter); + } + + @SuppressWarnings("unchecked") + @Override + public Object to(Type type, InternalConverter c) { + List tr = typeRules.get(Util.baseType(type)); + if (tr == null) + tr = Collections.emptyList(); + List converters = new ArrayList<>( + tr.size() + allRules.size()); + converters.addAll(tr); + converters.addAll(allRules); + + try { + if (object != null) { + for (ConverterFunction cf : converters) { + try { + Object res = cf.apply(object, type); + if (res != ConverterFunction.CANNOT_HANDLE) { + return res; + } + } catch (Exception ex) { + if (hasDefault) + return defaultValue; + else + throw new ConversionException("Cannot convert " + + object + " to " + type, ex); + } + } + } + + Object result = del.to(type, c); + if (result != null && Proxy.isProxyClass(result.getClass()) && getErrorHandlers(c).size() > 0) { + return wrapErrorHandling(result, c); + } else { + return result; + } + } catch (Exception ex) { + for (ConverterFunction eh : getErrorHandlers(c)) { + try { + Object handled = eh.apply(object, type); + if (handled != ConverterFunction.CANNOT_HANDLE) + return handled; + } catch (RuntimeException re) { + throw re; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + // No error handler, throw the original exception + throw ex; + } + } + + private List getErrorHandlers(Converter converter) { + List handlers = new ArrayList<>(); + + if (converter instanceof CustomConverterImpl) { + CustomConverterImpl cconverter = (CustomConverterImpl) converter; + handlers.addAll(cconverter.errorHandlers); + + Converter nextDel = cconverter.delegate; + handlers.addAll(getErrorHandlers(nextDel)); + } + + handlers.addAll(errorHandlers); + + return handlers; + } + + private Object wrapErrorHandling(final Object wrapped, final InternalConverter c) { + final Class cls = wrapped.getClass(); + return Proxy.newProxyInstance(cls.getClassLoader(), cls.getInterfaces(), new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Class< ? > mdDecl = method.getDeclaringClass(); + if (mdDecl.equals(Object.class)) { + switch (method.getName()) { + case "equals" : + return proxy == args[0]; + case "hashCode" : + return System.identityHashCode(proxy); + case "toString" : + return "Proxy for " + cls; + default : + throw new UnsupportedOperationException("Method " + + method + " not supported on proxy for " + + cls); + } + } + + try { + return method.invoke(wrapped, args); + } catch (Exception ex) { + for (ConverterFunction eh : getErrorHandlers(c)) { + try { + Object handled = eh.apply(wrapped, method.getGenericReturnType()); + if (handled != ConverterFunction.CANNOT_HANDLE) + return handled; + } catch (RuntimeException re) { + throw re; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + return null; + } + }); + } + + @Override + public String toString() { + return to(String.class); + } + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/DTOUtil.java b/converter/converter/src/main/java/org/osgi/util/converter/DTOUtil.java new file mode 100644 index 00000000000..c018713718a --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/DTOUtil.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.converter; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +/** + * @author $Id$ + */ +class DTOUtil { + private DTOUtil() { + // Do not instantiate. This is a utility class. + } + + static boolean isDTOType(Class< ? > cls, boolean ignorePublicNoArgsCtor) { + if(!ignorePublicNoArgsCtor) { + try { + cls.getConstructor(); + } catch (NoSuchMethodException | SecurityException e) { + // No public zero-arg constructor, not a DTO + return false; + } + } + + for (Method m : cls.getMethods()) { + try { + Object.class.getMethod(m.getName(), m.getParameterTypes()); + } catch (NoSuchMethodException snme) { + // Not a method defined by Object.class (or override of such + // method) + return false; + } + } + + /* + * for (Field f : cls.getDeclaredFields()) { int modifiers = + * f.getModifiers(); if (Modifier.isStatic(modifiers)) { // ignore + * static fields continue; } if (!Modifier.isPublic(modifiers)) { return + * false; } } + */ + + boolean foundField = false; + for (Field f : cls.getFields()) { + int modifiers = f.getModifiers(); + if (Modifier.isStatic(modifiers)) { + // ignore static fields + continue; + } + + if (!Modifier.isPublic(modifiers)) { + return false; + } + foundField = true; + } + return foundField; + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/DynamicMapLikeFacade.java b/converter/converter/src/main/java/org/osgi/util/converter/DynamicMapLikeFacade.java new file mode 100644 index 00000000000..088ac05140f --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/DynamicMapLikeFacade.java @@ -0,0 +1,301 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.converter; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author $Id$ + */ +abstract class DynamicMapLikeFacade implements Map { + protected final ConvertingImpl convertingImpl; + + protected DynamicMapLikeFacade(ConvertingImpl convertingImpl) { + this.convertingImpl = convertingImpl; + } + + @Override + public int size() { + return keySet().size(); + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public boolean containsKey(Object key) { + return keySet().contains(key); + } + + @Override + public boolean containsValue(Object value) { + for (Entry entry : entrySet()) { + if (value == null) { + if (entry.getValue() == null) { + return true; + } + } else if (value.equals(entry.getValue())) { + return true; + } + } + return false; + } + + @Override + public V put(K key, V value) { + // Should never be called; the delegate should swap to a copy in this + // case + throw new UnsupportedOperationException(); + } + + @Override + public V remove(Object key) { + // Should never be called; the delegate should swap to a copy in this + // case + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(Map< ? extends K, ? extends V> m) { + // Should never be called; the delegate should swap to a copy in this + // case + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + // Should never be called; the delegate should swap to a copy in this + // case + throw new UnsupportedOperationException(); + } + + @Override + public Collection values() { + List res = new ArrayList<>(); + + for (Map.Entry entry : entrySet()) { + res.add(entry.getValue()); + } + return res; + } + + @Override + public Set> entrySet() { + Set ks = keySet(); + + Set> res = new LinkedHashSet<>(ks.size()); + + for (K k : ks) { + V v = get(k); + res.add(new MapDelegate.MapEntry(k, v)); + } + return res; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append('{'); + boolean first = true; + for (Map.Entry entry : entrySet()) { + if (first) + first = false; + else + sb.append(", "); + + sb.append(entry.getKey()); + sb.append('='); + sb.append(entry.getValue()); + } + sb.append('}'); + + return sb.toString(); + } +} + +class DynamicBeanFacade extends DynamicMapLikeFacade { + private Map keys = null; + private final Object backingObject; + private final Class< ? > beanClass; + + DynamicBeanFacade(Object backingObject, Class< ? > beanClass, + ConvertingImpl convertingImpl) { + super(convertingImpl); + this.backingObject = backingObject; + this.beanClass = beanClass; + } + + @Override + public Object get(Object key) { + Method m = getKeys().get(key); + try { + return m.invoke(backingObject); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public Set keySet() { + return getKeys().keySet(); + } + + private Map getKeys() { + if (keys == null) + keys = Util.getBeanKeys(beanClass); + + return keys; + } +} + +class DynamicDictionaryFacade extends DynamicMapLikeFacade { + private final Dictionary backingObject; + + DynamicDictionaryFacade(Dictionary backingObject, + ConvertingImpl convertingImpl) { + super(convertingImpl); + this.backingObject = backingObject; + } + + @Override + public V get(Object key) { + return backingObject.get(key); + } + + @Override + public Set keySet() { + return new HashSet<>(Collections.list(backingObject.keys())); + } +} + +class DynamicMapFacade extends DynamicMapLikeFacade { + private final Map backingObject; + + DynamicMapFacade(Map backingObject, ConvertingImpl convertingImpl) { + super(convertingImpl); + this.backingObject = backingObject; + } + + @Override + public V get(Object key) { + return backingObject.get(key); + } + + @Override + public Set keySet() { + Map m = backingObject; + return m.keySet(); + } +} + +class DynamicDTOFacade extends DynamicMapLikeFacade { + private Map keys = null; + private final Object backingObject; + private final Class< ? > dtoClass; + + DynamicDTOFacade(Object backingObject, Class< ? > dtoClass, + ConvertingImpl converting) { + super(converting); + this.backingObject = backingObject; + this.dtoClass = dtoClass; + } + + @Override + public Object get(Object key) { + Field f = getKeys().get(key); + if (f == null) + return null; + + try { + return f.get(backingObject); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public Set keySet() { + return getKeys().keySet(); + } + + private Map getKeys() { + if (keys == null) + keys = Util.getDTOKeys(dtoClass); + + return keys; + } +} + +class DynamicInterfaceFacade extends DynamicMapLikeFacade { + private Map> keys = null; + private final Object backingObject; + private final Class< ? > theInterface; + + DynamicInterfaceFacade(Object backingObject, Class< ? > intf, + ConvertingImpl convertingImpl) { + super(convertingImpl); + this.backingObject = backingObject; + this.theInterface = intf; + } + + @Override + public Object get(Object key) { + Set set = getKeys().get(key); + if (set == null) + return null; + for (Iterator iterator = set.iterator(); iterator.hasNext();) { + Method m = iterator.next(); + if (m.getParameterTypes().length > 0) + continue; + try { + return m.invoke(backingObject); + } catch (Exception e) { + if (RuntimeException.class + .isAssignableFrom(e.getCause().getClass())) + throw ((RuntimeException) e.getCause()); + throw new RuntimeException(e); + } + } + throw new ConversionException("Missing no-arg method for key: " + key); + } + + @Override + public Set keySet() { + return getKeys().keySet(); + } + + private Map> getKeys() { + if (keys == null) + keys = Util.getInterfaceKeys(theInterface, backingObject); + + return keys; + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/Functioning.java b/converter/converter/src/main/java/org/osgi/util/converter/Functioning.java new file mode 100644 index 00000000000..e024fc559a2 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/Functioning.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.converter; + +import java.lang.reflect.Type; + +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.util.function.Function; + +/** + * This interface is used to specify the target function to perform conversions. + * This function can be used multiple times. A {@link Functioning} instance can + * be obtained via the {@link Converter}. + * + * @author $Id$ + */ +@ProviderType +public interface Functioning extends Specifying { + /** + * Specify the target object type for the conversion as a class object. + * + * @param cls The class to convert to. + * @param The type to convert to. + * @return A function that can perform the conversion. + */ + Function to(Class cls); + + /** + * Specify the target object type as a Java Reflection Type object. + * + * @param type A Type object to represent the target type to be converted + * to. + * @param The type to convert to. + * @return A function that can perform the conversion. + */ + Function to(Type type); + + /** + * Specify the target object type as a {@link TypeReference}. If the target + * class carries generics information a TypeReference should be used as this + * preserves the generic information whereas a Class object has this + * information erased. Example use: + * + *
      +	 * List<String> result = converter.function()
      +	 * 		.to(new TypeReference<List<String>>() {});
      +	 * 
      + * + * @param ref A type reference to the object being converted to. + * @param The type to convert to. + * @return A function that can perform the conversion. + */ + Function to(TypeReference ref); +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/FunctioningImpl.java b/converter/converter/src/main/java/org/osgi/util/converter/FunctioningImpl.java new file mode 100644 index 00000000000..fe932c35acb --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/FunctioningImpl.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.converter; + +import java.lang.reflect.Type; + +import org.osgi.util.function.Function; + +/** + * @author $Id$ + */ +class FunctioningImpl extends AbstractSpecifying + implements Functioning { + InternalConverter converter; + + FunctioningImpl(InternalConverter converterImpl) { + converter = converterImpl; + } + + @Override + public Function to(Class cls) { + Type type = cls; + return to(type); + } + + @Override + public Function to(TypeReference ref) { + return to(ref.getType()); + } + + @Override + public Function to(final Type type) { + return new Function() { + @Override + public T apply(Object t) { + InternalConverting ic = converter.convert(t); + return applyModifiers(ic).to(type); + } + }; + } + + InternalConverting applyModifiers(InternalConverting ic) { + if (hasDefault) + ic.defaultValue(defaultValue); + if (liveView) + ic.view(); + if (keysIgnoreCase) + ic.keysIgnoreCase(); + if (sourceAsClass != null) + ic.sourceAs(sourceAsClass); + if (sourceAsDTO) + ic.sourceAsDTO(); + if (sourceAsJavaBean) + ic.sourceAsBean(); + if (targetAsClass != null) + ic.targetAs(targetAsClass); + if (targetAsDTO) + ic.targetAsBean(); + if (targetAsJavaBean) + ic.targetAsBean(); + + return ic; + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/InternalConverter.java b/converter/converter/src/main/java/org/osgi/util/converter/InternalConverter.java new file mode 100644 index 00000000000..209729158cb --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/InternalConverter.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.converter; + +/** + * @author $Id$ + */ +interface InternalConverter extends Converter { + // This interface specifies a convert(Object) method that returns an + // InternalConverting rather than a normal Converting instance. + @Override + InternalConverting convert(Object obj); +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/InternalConverting.java b/converter/converter/src/main/java/org/osgi/util/converter/InternalConverting.java new file mode 100644 index 00000000000..1af5ce07dbf --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/InternalConverting.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.converter; + +import java.lang.reflect.Type; + +/** + * This interface is the same as the {@link Converting} interface with the + * addition that the current converter (which may include custom rules) can be + * set on it. This allows the converter to be re-entrant and use itself for + * sub-conversions if applicable. + * + * @author $Id$ + */ +interface InternalConverting extends Converting { + /** + * Invoke the conversion while passing the top-level converter. The + * top-level converter is needed when performing embedded conversions such + * as in map elements. When using a custom converter, the top converter + * must be used for this. + * + * @param type A Type object to represent the target type to be converted + * to. + * @param The type to convert to. + * @return The converted object. + */ + T to(Type type, InternalConverter c); +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/ListDelegate.java b/converter/converter/src/main/java/org/osgi/util/converter/ListDelegate.java new file mode 100644 index 00000000000..ca6b2cdfd3a --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/ListDelegate.java @@ -0,0 +1,249 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.converter; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +/** + * @author $Id$ + */ +class ListDelegate implements List { + private volatile List delegate; + private volatile boolean cloned; + private final ConvertingImpl convertingImpl; + private final InternalConverter converter; + + @SuppressWarnings({ + "unchecked", "rawtypes" + }) + static List forArray(Object arr, ConvertingImpl converting, InternalConverter c) { + return new ListDelegate(new ArrayDelegate(arr), converting, c); + } + + static List forCollection(Collection object, + ConvertingImpl converting, InternalConverter c) { + if (object instanceof List) { + return new ListDelegate((List) object, converting, c); + } + return new ListDelegate(new CollectionDelegate<>(object), + converting, c); + } + + private ListDelegate(List del, ConvertingImpl conv, InternalConverter c) { + delegate = del; + convertingImpl = conv; + converter = c; + } + + // Whenever a modification is made, the delegate is cloned and detached. + @SuppressWarnings("unchecked") + private void cloneDelegate() { + if (cloned) { + return; + } else { + cloned = true; + delegate = new ArrayList((List) Arrays.asList(toArray())); + } + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return containsAll(Collections.singletonList(o)); + } + + @Override + public Iterator iterator() { + return listIterator(); + } + + @Override + public Object[] toArray() { + return toArray(new Object[size()]); + } + + @SuppressWarnings("unchecked") + @Override + public X[] toArray(X[] a) { + int mySize = size(); + if (Array.getLength(a) < size()) { + a = (X[]) Array.newInstance(a.getClass().getComponentType(), + mySize); + } + + for (int i = 0; i < a.length; i++) { + if (mySize > i) { + a[i] = (X) get(i); + } else { + a[i] = null; + } + } + return a; + } + + @Override + public boolean add(T e) { + cloneDelegate(); + + return delegate.add(e); + } + + @Override + public boolean remove(Object o) { + cloneDelegate(); + + return delegate.remove(o); + } + + @Override + public boolean containsAll(Collection< ? > c) { + List l = Arrays.asList(toArray()); + for (Object o : c) { + if (!l.contains(o)) + return false; + } + + return true; + } + + @Override + public boolean addAll(Collection< ? extends T> c) { + cloneDelegate(); + + return delegate.addAll(c); + } + + @Override + public boolean addAll(int index, Collection< ? extends T> c) { + cloneDelegate(); + + return delegate.addAll(index, c); + } + + @Override + public boolean removeAll(Collection< ? > c) { + cloneDelegate(); + + return delegate.removeAll(c); + } + + @Override + public boolean retainAll(Collection< ? > c) { + cloneDelegate(); + + return delegate.retainAll(c); + } + + @Override + public void clear() { + cloned = true; + delegate = new ArrayList<>(); + } + + @SuppressWarnings("unchecked") + @Override + public T get(int index) { + return (T) convertingImpl.convertCollectionValue(delegate.get(index), converter); + } + + @Override + public T set(int index, T element) { + cloneDelegate(); + + return delegate.set(index, element); + } + + @Override + public void add(int index, T element) { + cloneDelegate(); + + delegate.add(index, element); + } + + @Override + public T remove(int index) { + cloneDelegate(); + + return delegate.remove(index); + } + + @Override + public int indexOf(Object o) { + return delegate.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return delegate.lastIndexOf(o); + } + + @SuppressWarnings("unchecked") + @Override + public ListIterator listIterator() { + return (ListIterator) Arrays.asList(toArray()).listIterator(); + } + + @SuppressWarnings("unchecked") + @Override + public ListIterator listIterator(int index) { + return (ListIterator) Arrays.asList(toArray()).listIterator(index); + } + + @SuppressWarnings("unchecked") + @Override + public List subList(int fromIndex, int toIndex) { + return (List) Arrays.asList(toArray()).subList(fromIndex, toIndex); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (!(obj instanceof List)) + return false; + + List< ? > l1 = new ArrayList<>(this); + List< ? > l2 = new ArrayList<>((List< ? >) obj); + return l1.equals(l2); + } + + @Override + public String toString() { + return delegate.toString(); + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/MapDelegate.java b/converter/converter/src/main/java/org/osgi/util/converter/MapDelegate.java new file mode 100644 index 00000000000..7ff9344ba20 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/MapDelegate.java @@ -0,0 +1,253 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.converter; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author $Id$ + */ +class MapDelegate implements Map { + // not synchronized. Worst that can happen is that cloning is done more than + // once, which is harmless. + private volatile boolean cloned = false; + private final ConvertingImpl convertingImpl; + private final InternalConverter converter; + Map delegate; + + private MapDelegate(ConvertingImpl converting, InternalConverter c, Map del) { + convertingImpl = converting; + converter = c; + delegate = del; + } + + static MapDelegate forBean(Object b, Class< ? > beanClass, + ConvertingImpl converting, InternalConverter c) { + return new MapDelegate<>(converting, c, + new DynamicBeanFacade(b, beanClass, converting)); + } + + static Map forMap(Map m, ConvertingImpl converting, InternalConverter c) { + return new MapDelegate<>(converting, c, + new DynamicMapFacade<>(m, converting)); + } + + static MapDelegate forDictionary(Dictionary d, + ConvertingImpl converting, InternalConverter c) { + return new MapDelegate<>(converting, c, + new DynamicDictionaryFacade<>(d, converting)); + } + + static MapDelegate forDTO(Object obj, Class< ? > dtoClass, + ConvertingImpl converting, InternalConverter c) { + return new MapDelegate<>(converting, c, + new DynamicDTOFacade(obj, dtoClass, converting)); + } + + static MapDelegate forInterface(Object obj, Class< ? > intf, + ConvertingImpl converting, InternalConverter c) { + return new MapDelegate<>(converting, c, + new DynamicInterfaceFacade(obj, intf, converting)); + } + + @Override + public int size() { + // Need to convert the entire map to get the size + Set keys = new HashSet<>(); + + Set ks = delegate.keySet(); + for (K key : ks) { + keys.add(getConvertedKey(key)); + } + + return keys.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return keySet().contains(key); + } + + @Override + public boolean containsValue(Object value) { + return values().contains(value); + } + + @Override + @SuppressWarnings("unchecked") + public V get(Object key) { + V val = null; + if (internalKeySet().contains(key)) { + val = delegate.get(key); + } + + if (val == null) { + key = findConvertedKey(internalKeySet(), key); + val = delegate.get(key); + } + + if (val == null) + return null; + else + return (V) getConvertedValue(val); + } + + private Object getConvertedKey(Object key) { + return convertingImpl.convertMapKey(key, converter); + } + + private Object getConvertedValue(Object val) { + return convertingImpl.convertMapValue(val, converter); + } + + private Object findConvertedKey(Set< ? > keySet, Object key) { + for (Object k : keySet) { + if (key.equals(k)) + return k; + } + + for (Object k : keySet) { + Object c = converter.convert(k).to(key.getClass()); + if (c != null && c.equals(key)) + return k; + } + return key; + } + + @Override + public V put(K key, V value) { + cloneDelegate(); + + return delegate.put(key, value); + } + + @Override + public V remove(Object key) { + cloneDelegate(); + + return delegate.remove(key); + } + + @Override + public void putAll(Map< ? extends K, ? extends V> m) { + cloneDelegate(); + + delegate.putAll(m); + } + + @Override + public void clear() { + cloned = true; + delegate = new HashMap<>(); + } + + private Set internalKeySet() { + return delegate.keySet(); + } + + @SuppressWarnings("unchecked") + @Override + public Set keySet() { + Set keys = new HashSet<>(); + for (Object key : internalKeySet()) { + keys.add((K) getConvertedKey(key)); + } + return keys; + } + + @Override + public Collection values() { + List values = new ArrayList<>(); + for (Map.Entry entry : entrySet()) { + values.add(entry.getValue()); + } + return values; + } + + @Override + @SuppressWarnings("unchecked") + public Set> entrySet() { + Set> result = new HashSet<>(); + for (Map.Entry< ? , ? > entry : delegate.entrySet()) { + K key = (K) findConvertedKey(internalKeySet(), entry.getKey()); + V val = (V) getConvertedValue(entry.getValue()); + result.add(new MapEntry(key, val)); + } + return result; + } + + @Override + public boolean equals(Object o) { + return delegate.equals(o); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + private void cloneDelegate() { + if (cloned) { + return; + } else { + cloned = true; + delegate = new HashMap<>(delegate); + } + } + + @Override + public String toString() { + return delegate.toString(); + } + + static class MapEntry implements Map.Entry { + private final K key; + private final V value; + + MapEntry(K k, V v) { + key = k; + value = v; + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return value; + } + + @Override + public V setValue(V value) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/Rule.java b/converter/converter/src/main/java/org/osgi/util/converter/Rule.java new file mode 100644 index 00000000000..6aea34f33cf --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/Rule.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.osgi.util.converter; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import org.osgi.util.function.Function; + +/** + * A rule implementation that works by capturing the type arguments via + * subclassing. The rule supports specifying both from and to + * types. Filtering on the from by the {@code Rule} implementation. + * Filtering on the to is done by the converter customization + * mechanism. + * + * @author $Id$ + * @param The type to convert from. + * @param The type to convert to. + */ +public abstract class Rule implements TargetRule { + private final ConverterFunction function; + + /** + * Create an instance with a conversion function. + * + * @param func The conversion function to use. + */ + public Rule(Function func) { + function = getGenericFunction(func); + } + + private ConverterFunction getGenericFunction(final Function func) { + return new ConverterFunction() { + @Override + @SuppressWarnings("unchecked") + public Object apply(Object obj, Type targetType) throws Exception { + Rule< ? , ? > r = Rule.this; + Type type = ((ParameterizedType) r.getClass() + .getGenericSuperclass()).getActualTypeArguments()[0]; + + if (type instanceof ParameterizedType) { + type = ((ParameterizedType) type).getRawType(); + } + + Class< ? > cls = null; + if (type instanceof Class) { + cls = ((Class< ? >) type); + } else { + return ConverterFunction.CANNOT_HANDLE; + } + + if (cls.isInstance(obj)) { + return func.apply((F) obj); + } + return ConverterFunction.CANNOT_HANDLE; + } + }; + } + + @Override + public ConverterFunction getFunction() { + return function; + } + + @Override + public Type getTargetType() { + Type type = ((ParameterizedType) getClass().getGenericSuperclass()) + .getActualTypeArguments()[1]; + return type; + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/SetDelegate.java b/converter/converter/src/main/java/org/osgi/util/converter/SetDelegate.java new file mode 100644 index 00000000000..d32834af649 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/SetDelegate.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.converter; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * @author $Id$ + */ +class SetDelegate implements Set { + private volatile Set delegate; + private volatile boolean cloned; + private final ConvertingImpl convertingImpl; + private final InternalConverter converter; + + static Set forCollection(Collection collection, + ConvertingImpl converting, InternalConverter c) { + if (collection instanceof Set) { + return new SetDelegate((Set) collection, converting, c); + } + return new SetDelegate(new CollectionSetDelegate<>(collection), + converting, c); + } + + SetDelegate(Set collection, ConvertingImpl converting, InternalConverter c) { + delegate = collection; + convertingImpl = converting; + converter = c; + } + + // Whenever a modification is made, the delegate is cloned and detached. + private void cloneDelegate() { + if (cloned) { + return; + } else { + cloned = true; + delegate = new HashSet<>(delegate); + } + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return containsAll(Collections.singletonList(o)); + } + + @Override + public Iterator iterator() { + return new SetDelegateIterator(); + } + + @Override + public Object[] toArray() { + return toArray(new Object[size()]); + } + + @SuppressWarnings("unchecked") + @Override + public X[] toArray(X[] a) { + int mySize = size(); + if (Array.getLength(a) < size()) { + a = (X[]) Array.newInstance(a.getClass().getComponentType(), + mySize); + } + + Iterator it = iterator(); + for (int i = 0; i < a.length; i++) { + if (mySize > i && it.hasNext()) { + a[i] = (X) it.next(); + } else { + a[i] = null; + } + } + return a; + } + + @Override + public boolean add(T e) { + cloneDelegate(); + + return delegate.add(e); + } + + @Override + public boolean remove(Object o) { + cloneDelegate(); + + return delegate.remove(o); + } + + @Override + public boolean containsAll(Collection< ? > c) { + List l = Arrays.asList(toArray()); + for (Object o : c) { + if (!l.contains(o)) + return false; + } + + return true; + } + + @Override + public boolean addAll(Collection< ? extends T> c) { + cloneDelegate(); + + return delegate.addAll(c); + } + + @Override + public boolean retainAll(Collection< ? > c) { + cloneDelegate(); + + return delegate.retainAll(c); + } + + @Override + public boolean removeAll(Collection< ? > c) { + cloneDelegate(); + + return delegate.removeAll(c); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (!(obj instanceof Set)) + return false; + + Set< ? > s1 = new HashSet<>(this); + Set< ? > s2 = new HashSet<>((Set< ? >) obj); + return s1.equals(s2); + // cannot call delegate.equals() because they are of different types + } + + @Override + public String toString() { + return delegate.toString(); + } + + @Override + public void clear() { + cloned = true; + delegate = new HashSet<>(); + } + + private class SetDelegateIterator implements Iterator { + final Iterator< ? > delegateIterator; + + @SuppressWarnings("synthetic-access") + SetDelegateIterator() { + delegateIterator = delegate.iterator(); + } + + @Override + public boolean hasNext() { + return delegateIterator.hasNext(); + } + + @SuppressWarnings({ + "unchecked", "synthetic-access" + }) + @Override + public T next() { + Object obj = delegateIterator.next(); + return (T) convertingImpl.convertCollectionValue(obj, converter); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove"); + } + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/Specifying.java b/converter/converter/src/main/java/org/osgi/util/converter/Specifying.java new file mode 100644 index 00000000000..a8356b61156 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/Specifying.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) OSGi Alliance (2017, 2018). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.osgi.util.converter; + +import org.osgi.annotation.versioning.ProviderType; + +/** + * This is the base interface for the {@link Converting} and {@link Functioning} + * interfaces and defines the common modifiers that can be applied to these. + * + * @param Either {@link Converting} or {@link Specifying}. + * @author $Id$ + */ +@ProviderType +public interface Specifying> { + /** + * The default value to use when the object cannot be converted or in case + * of conversion from a {@code null} value. + * + * @param defVal The default value. + * @return The current {@code Converting} object so that additional calls + * can be chained. + */ + T defaultValue(Object defVal); + + /** + * When converting between map-like types use case-insensitive mapping of + * keys. + * + * @return The current {@code Converting} object so that additional calls + * can be chained. + */ + T keysIgnoreCase(); + + /** + * Treat the source object as the specified class. This can be used to + * disambiguate a type if it implements multiple interfaces or extends + * multiple classes. + * + * @param cls The class to treat the object as. + * @return The current {@code Converting} object so that additional calls + * can be chained. + */ + T sourceAs(Class< ? > cls); + + /** + * Treat the source object as a JavaBean. By default objects will not be + * treated as JavaBeans, this has to be specified using this method. + * + * @return The current {@code Converting} object so that additional calls + * can be chained. + */ + T sourceAsBean(); + + /** + * Treat the source object as a DTO even if the source object has methods or + * is otherwise not recognized as a DTO. + * + * @return The current {@code Converting} object so that additional calls + * can be chained. + */ + T sourceAsDTO(); + + /** + * Treat the target object as the specified class. This can be used to + * disambiguate a type if it implements multiple interfaces or extends + * multiple classes. + * + * @param cls The class to treat the object as. + * @return The current {@code Converting} object so that additional calls + * can be chained. + */ + T targetAs(Class< ? > cls); + + /** + * Treat the target object as a JavaBean. By default objects will not be + * treated as JavaBeans, this has to be specified using this method. + * + * @return The current {@code Converting} object so that additional calls + * can be chained. + */ + T targetAsBean(); + + /** + * Treat the target object as a DTO even if it has methods or is otherwise + * not recognized as a DTO. + * + * @return The current {@code Converting} object so that additional calls + * can be chained. + */ + T targetAsDTO(); + + /** + * Return a live view over the backing object that reflects any changes to + * the original object. This is only possible with conversions to + * {@link java.util.Map}, {@link java.util.Collection}, + * {@link java.util.List} and {@link java.util.Set}. The live view object + * will cease to be live as soon as modifications are made to it. Note that + * conversions to an interface or annotation will always produce a live view + * that cannot be modified. This modifier has no effect with conversions to + * other types. + * + * @return The current {@code Converting} object so that additional calls + * can be chained. + */ + T view(); +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/TargetRule.java b/converter/converter/src/main/java/org/osgi/util/converter/TargetRule.java new file mode 100644 index 00000000000..c56237d7a7e --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/TargetRule.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.osgi.util.converter; + +import java.lang.reflect.Type; + +/** + * Interface for custom conversion rules. + * + * @author $Id$ + */ +public interface TargetRule { + /** + * The function to perform the conversion. + * + * @return The function. + */ + ConverterFunction getFunction(); + + /** + * The target type of this rule. The conversion function is invoked for each + * conversion to the target type. + * + * @return The target type. + */ + Type getTargetType(); +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/TypeReference.java b/converter/converter/src/main/java/org/osgi/util/converter/TypeReference.java new file mode 100644 index 00000000000..f7c668ba43f --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/TypeReference.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) OSGi Alliance (2015, 2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.osgi.util.converter; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import org.osgi.annotation.versioning.ConsumerType; + +/** + * An object does not carry any runtime information about its generic type. + * However sometimes it is necessary to specify a generic type, that is the + * purpose of this class. It allows you to specify an generic type by defining a + * type T, then subclassing it. The subclass will have a reference to the super + * class that contains this generic information. Through reflection, we pick + * this reference up and return it with the getType() call. + * + *
      + * List<String> result = converter.convert(Arrays.asList(1, 2, 3))
      + * 		.to(new TypeReference<List<String>>() {});
      + * 
      + * + * @param The target type for the conversion. + * @author $Id$ + */ +@ConsumerType +public class TypeReference { + /** + * A {@link TypeReference} cannot be directly instantiated. To use it, it + * has to be extended, typically as an anonymous inner class. + */ + protected TypeReference() {} + + /** + * Return the actual type of this Type Reference + * + * @return the type of this reference. + */ + public Type getType() { + return ((ParameterizedType) getClass().getGenericSuperclass()) + .getActualTypeArguments()[0]; + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/TypeRule.java b/converter/converter/src/main/java/org/osgi/util/converter/TypeRule.java new file mode 100644 index 00000000000..f4dddce75f9 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/TypeRule.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.osgi.util.converter; + +import java.lang.reflect.Type; + +import org.osgi.util.function.Function; + +/** + * Rule implementation that works by passing in type arguments rather than + * subclassing. The rule supports specifying both from and to + * types. Filtering on the from by the {@code Rule} implementation. + * Filtering on the to is done by the converter customization + * mechanism. + * + * @author $Id$ + * @param The type to convert from. + * @param The type to convert to. + */ +public class TypeRule implements TargetRule { + private final ConverterFunction function; + private final Type toType; + + /** + * Create an instance based on source, target types and a conversion + * function. + * + * @param from The type to convert from. + * @param to The type to convert to. + * @param func The conversion function to use. + */ + public TypeRule(Type from, Type to, Function func) { + function = getFunction(from, func); + toType = to; + } + + private static ConverterFunction getFunction(final Type from, + final Function func) { + return new ConverterFunction() { + @Override + @SuppressWarnings("unchecked") + public Object apply(Object obj, Type targetType) throws Exception { + if (from instanceof Class) { + Class< ? > cls = (Class< ? >) from; + if (cls.isInstance(obj)) { + T res = func.apply((F) obj); + if (res != null) + return res; + else + return ConverterFunction.CANNOT_HANDLE; + } + } + return ConverterFunction.CANNOT_HANDLE; + } + }; + } + + @Override + public ConverterFunction getFunction() { + return function; + } + + @Override + public Type getTargetType() { + return toType; + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/Util.java b/converter/converter/src/main/java/org/osgi/util/converter/Util.java new file mode 100644 index 00000000000..4fa91c4fa86 --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/Util.java @@ -0,0 +1,425 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.converter; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * @author $Id$ + */ +class Util { + private static final Map,Class< ? >> boxedClasses; + static { + Map,Class< ? >> m = new HashMap<>(); + m.put(int.class, Integer.class); + m.put(long.class, Long.class); + m.put(double.class, Double.class); + m.put(float.class, Float.class); + m.put(boolean.class, Boolean.class); + m.put(char.class, Character.class); + m.put(byte.class, Byte.class); + m.put(void.class, Void.class); + m.put(short.class, Short.class); + boxedClasses = Collections.unmodifiableMap(m); + } + + private Util() {} // prevent instantiation + + static Type primitiveToBoxed(Type type) { + if (type instanceof Class) + return primitiveToBoxed((Class< ? >) type); + else + return null; + } + + static Type baseType(Type type) { + if (type instanceof Class) + return primitiveToBoxed((Class< ? >) type); + else if (type instanceof ParameterizedType) + return type; + else + return null; + } + + static Class< ? > primitiveToBoxed(Class< ? > cls) { + Class< ? > boxed = boxedClasses.get(cls); + if (boxed != null) + return boxed; + else + return cls; + } + + static Map getBeanKeys(Class< ? > beanClass) { + Map keys = new LinkedHashMap<>(); + // Bean methods must be public and can be on parent classes + for (Method md : beanClass.getMethods()) { + String key = getBeanKey(md); + if (key != null && !keys.containsKey(key)) { + keys.put(key, md); + } + } + return keys; + } + + static String getBeanKey(Method md) { + if (Modifier.isStatic(md.getModifiers())) + return null; + + if (!Modifier.isPublic(md.getModifiers())) + return null; + + return getBeanAccessorPropertyName(md); + } + + private static String getBeanAccessorPropertyName(Method md) { + if (md.getReturnType().equals(Void.class)) + return null; // not an accessor + + if (md.getParameterTypes().length > 0) + return null; // not an accessor + + if (Object.class.equals(md.getDeclaringClass())) + return null; // do not use any methods on the Object class as a + // accessor + + String mn = md.getName(); + int prefix; + if (mn.startsWith("get")) + prefix = 3; + else if (mn.startsWith("is")) + prefix = 2; + else + return null; // not an accessor prefix + + if (mn.length() <= prefix) + return null; // just 'get' or 'is': not an accessor + String propStr = mn.substring(prefix); + StringBuilder propName = new StringBuilder(propStr.length()); + char firstChar = propStr.charAt(0); + if (!Character.isUpperCase(firstChar)) + return null; // no acccessor as no camel casing + propName.append(Character.toLowerCase(firstChar)); + if (propStr.length() > 1) + propName.append(propStr.substring(1)); + + return unMangleName(getPrefix(md.getDeclaringClass()), + propName.toString()); + } + + static Map getDTOKeys(Class< ? > dto) { + Map keys = new LinkedHashMap<>(); + + for (Field f : dto.getFields()) { + String key = getDTOKey(f); + if (key != null && !keys.containsKey(key)) + keys.put(key, f); + } + return keys; + } + + static String getDTOKey(Field f) { + if (Modifier.isStatic(f.getModifiers())) + return null; + + if (!Modifier.isPublic(f.getModifiers())) + return null; + + return unMangleName(getPrefix(f.getDeclaringClass()), f.getName()); + } + + static Map> getInterfaceKeys(Class< ? > intf, + Object object) { + Map> keys = new LinkedHashMap<>(); + + String seank = getSingleElementAnnotationKey(intf, object); + for (Method md : intf.getMethods()) { + String name = getInterfacePropertyName(md, seank, object); + if (name != null) { + Set set = keys.get(name); + if (set == null) { + set = new LinkedHashSet<>(); + keys.put(name, set); + } + md.setAccessible(true); + set.add(md); + } + } + + for (Iterator>> it = keys.entrySet() + .iterator(); it.hasNext();) { + Entry> entry = it.next(); + boolean zeroArgFound = false; + for (Method md : entry.getValue()) { + if (md.getParameterTypes().length == 0) { + // OK found the zero-arg param + zeroArgFound = true; + break; + } + } + if (!zeroArgFound) + it.remove(); + } + return keys; + } + + static String getMarkerAnnotationKey(Class< ? > intf, Object obj) { + Class< ? > ann = getAnnotationType(intf, obj); + return getPrefix(intf) + toSingleElementAnnotationKey(ann.getSimpleName()); + } + + static String getSingleElementAnnotationKey(Class< ? > intf, Object obj) { + Class< ? > ann = getAnnotationType(intf, obj); + if (ann == null) + return null; + + boolean valueFound = false; + // All annotation methods must be public + for (Method md : ann.getMethods()) { + if(md.getDeclaringClass() != ann) { + // Ignore Object methods and Annotation methods + continue; + } + + if ("value".equals(md.getName())) { + valueFound = true; + continue; + } + + if (md.getDefaultValue() == null) { + // All elements bar value must have a default + return null; + } + } + + if (!valueFound) { + // Single Element Annotation must have a value element. + return null; + } + + return getPrefix(ann) + toSingleElementAnnotationKey(ann.getSimpleName()); + } + + static Class< ? > getAnnotationType(Class< ? > intf, Object obj) { + try { + Method md = intf.getMethod("annotationType"); + Object res = md.invoke(obj); + if (res instanceof Class) + return (Class< ? >) res; + } catch (Exception e) { + // Ignore exception + } + return null; + } + + static String toSingleElementAnnotationKey(String simpleName) { + StringBuilder sb = new StringBuilder(); + + boolean capitalSeen = true; + for (char c : simpleName.toCharArray()) { + if (!capitalSeen) { + if (Character.isUpperCase(c)) { + capitalSeen = true; + sb.append('.'); + } + } else { + if (Character.isLowerCase(c)) { + capitalSeen = false; + } + } + sb.append(Character.toLowerCase(c)); + } + + return sb.toString(); + } + + static String getInterfacePropertyName(Method md, + String singleElementAnnotationKey, Object object) { + if (md.getReturnType().equals(Void.class)) + return null; // not an accessor + + if (md.getParameterTypes().length > 1) + return null; // not an accessor + + if ("value".equals(md.getName()) && md.getParameterTypes().length == 0 + && singleElementAnnotationKey != null) + return singleElementAnnotationKey; + + if (Object.class.equals(md.getDeclaringClass()) + || Annotation.class.equals(md.getDeclaringClass())) + return null; // do not use any methods on the Object or Annotation + // class as a accessor + + if ("annotationType".equals(md.getName()) + && md.getParameterTypes().length == 0) { + try { + Object cls = md.invoke(object); + if (cls instanceof Class && ((Class< ? >) cls).isAnnotation()) + return null; + } catch (Exception e) { + // Ignore exception + } + } + + if (md.getDeclaringClass().getSimpleName().startsWith("$Proxy")) { + // TODO is there a better way to do this? + if (isInheritedMethodInProxy(md, Object.class) + || isInheritedMethodInProxy(md, Annotation.class)) + return null; + } + + return unMangleName(getPrefix(md.getDeclaringClass()), md.getName()); + } + + private static boolean isInheritedMethodInProxy(Method md, Class< ? > cls) { + for (Method om : cls.getMethods()) { + if (om.getName().equals(md.getName()) && Arrays + .equals(om.getParameterTypes(), md.getParameterTypes())) { + return true; + } + } + return false; + } + + static Object getInterfaceProperty(Object obj, Method md) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException { + if (Modifier.isStatic(md.getModifiers())) + return null; + + if (md.getParameterTypes().length > 0) + return null; + + return md.invoke(obj); + } + + static String getPrefix(Class< ? > cls) { + try { + // We can use getField as the PREFIX must be public (see spec erratum) + Field prefixField = cls.getField("PREFIX_"); + if (prefixField.getDeclaringClass() == cls && + prefixField.getType().equals(String.class)) { + int modifiers = prefixField.getModifiers(); + // We need to be final *and* static + if (Modifier.isFinal(modifiers) && + Modifier.isStatic(modifiers)) { + + if(!prefixField.isAccessible()) { + // Should we log that we have to do this? + prefixField.setAccessible(true); + } + + return (String) prefixField.get(null); + } + } + } catch (Exception ex) { + // LOG no prefix field + } + + if (!cls.isInterface()) { + for (Class< ? > intf : cls.getInterfaces()) { + String prefix = getPrefix(intf); + if (prefix.length() > 0) + return prefix; + } + } + + return ""; + } + + static String mangleName(String prefix, String key, List names) { + if (!key.startsWith(prefix)) + return null; + + key = key.substring(prefix.length()); + + // Do a reverse search because some characters get removed as part of + // the mangling + for (String name : names) { + if (key.equals(unMangleName(name))) + return name; + } + + // Fallback if not found in the list - TODO maybe this can be removed. + String res = key.replace("_", "__"); + res = res.replace("$", "$$"); + res = res.replace("-", "$_$"); + res = res.replaceAll("[.]([._])", "_\\$$1"); + res = res.replace('.', '_'); + return res; + } + + static String unMangleName(String prefix, String key) { + return prefix + unMangleName(key); + } + + static String unMangleName(String id) { + char[] array = id.toCharArray(); + int out = 0; + + boolean changed = false; + for (int i = 0; i < array.length; i++) { + if (match("$$", array, i) || match("__", array, i)) { + array[out++] = array[i++]; + changed = true; + } else if (match("$_$", array, i)) { + array[out++] = '-'; + i += 2; + } else { + char c = array[i]; + if (c == '_') { + array[out++] = '.'; + changed = true; + } else if (c == '$') { + changed = true; + } else { + array[out++] = c; + } + } + } + if (id.length() != out || changed) + return new String(array, 0, out); + + return id; + } + + private static boolean match(String pattern, char[] array, int i) { + for (int j = 0; j < pattern.length(); j++, i++) { + if (i >= array.length) + return false; + + if (pattern.charAt(j) != array[i]) + return false; + } + return true; + } +} diff --git a/converter/converter/src/main/java/org/osgi/util/converter/package-info.java b/converter/converter/src/main/java/org/osgi/util/converter/package-info.java new file mode 100644 index 00000000000..0609cebb6ba --- /dev/null +++ b/converter/converter/src/main/java/org/osgi/util/converter/package-info.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) OSGi Alliance (2016, 2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Converter Package Version 1.0. + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. This package has two types of + * users: the consumers that use the API in this package and the providers that + * implement the API in this package. + *

      + * Example import for consumers using the API in this package: + *

      + * {@code Import-Package: org.osgi.util.converter; version="[1.0,2.0)"} + *

      + * Example import for providers implementing the API in this package: + *

      + * {@code Import-Package: org.osgi.util.converter; version="[1.0,1.1)"} + * + * @author $Id$ + */ +@Version("1.0.2") +package org.osgi.util.converter; + +import org.osgi.annotation.versioning.Version; diff --git a/converter/converter/src/test/java/org/osgi/util/converter/ConverterBuilderTest.java b/converter/converter/src/test/java/org/osgi/util/converter/ConverterBuilderTest.java new file mode 100644 index 00000000000..ac6b34e5b1c --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/ConverterBuilderTest.java @@ -0,0 +1,250 @@ +/* + * 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.osgi.util.converter; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.osgi.util.function.Function; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class ConverterBuilderTest { + private Converter converter; + + @Before + public void setUp() { + converter = Converters.standardConverter(); + } + + @After + public void tearDown() { + converter = null; + } + + @Test + public void testStringArrayToStringAdapter() { + ConverterBuilder cb = converter.newConverterBuilder(); + Converter ca = cb. + rule(new TypeRule(String[].class, String.class, + v -> Stream.of(v).collect(Collectors.joining(",")))). + rule(new TypeRule(String.class, String[].class, + v -> v.split(","))). + build(); + + assertEquals("A", converter.convert(new String[] {"A", "B"}).to(String.class)); + assertEquals("A,B", ca.convert(new String[] {"A", "B"}).to(String.class)); + + assertArrayEquals(new String [] {"A,B"}, + converter.convert("A,B").to(String[].class)); + assertArrayEquals(new String [] {"A","B"}, + ca.convert("A,B").to(String[].class)); + } + + static String convertToString(char[] a) { + StringBuilder sb = new StringBuilder(); + for (char c : a) { + sb.append(c); + } + return sb.toString(); + } + + @Test + public void testSecondLevelAdapter() { + ConverterBuilder cb = converter.newConverterBuilder(); + cb.rule(new TypeRule<>(char[].class, String.class, ConverterBuilderTest::convertToString)); + cb.rule(Integer.class, (f,t) -> -1); + cb.rule(Long.class, (f,t) -> -1L); + Converter ca = cb.build(); + + assertEquals("hi", ca.convert(new char[] {'h', 'i'}).to(String.class)); + assertEquals(Integer.valueOf(-1), ca.convert("Hello").to(Integer.class)); + assertEquals(Long.valueOf(-1), ca.convert("Hello").to(Long.class)); + + // Shadow the Integer variant but keep Long going to the Number variant. + Converter ca2 = ca.newConverterBuilder().rule( + new TypeRule(String.class, Integer.class, s -> s.length())).build(); + assertEquals(5, (int) ca2.convert("Hello").to(Integer.class)); + assertEquals(Long.valueOf(-1), ca2.convert("Hello").to(Long.class)); + } + + @Test @Ignore + public void testConvertToBaseArray() { + // TODO + } + + @Test @Ignore + public void testThrowExceptionInCustomConverter() { + // TODO + } + + @Test @Ignore + public void testMixedListToNumberCase() { + // TODO + } + + @Test + public void testCannotHandleSpecific() { + Converter ca = converter.newConverterBuilder().rule( + new TypeRule<>(Integer.class, Long.class, new Function() { + @Override + public Long apply(Integer obj) { + if (obj.intValue() != 1) + return new Long(-obj.intValue()); + return null; + } + })).build(); + + + assertEquals(Long.valueOf(-2), ca.convert(Integer.valueOf(2)).to(Long.class)); + + // This is the exception that the rule cannot handle + assertEquals(Long.valueOf(1), ca.convert(Integer.valueOf(1)).to(Long.class)); + } + + @Test + public void testWildcardAdapter() { + ConverterFunction foo = new ConverterFunction() { + @Override + public Object apply(Object obj, Type type) throws Exception { + if (!(obj instanceof List)) + return ConverterFunction.CANNOT_HANDLE; + + + List t = (List) obj; + if (t.size() == 0) + // Empty list is converted to null + return null; + + if (type instanceof Class) { + if (Number.class.isAssignableFrom((Class) type)) + return converter.convert(t.size()).to(type); + } + return ConverterFunction.CANNOT_HANDLE; + } + }; + + ConverterBuilder cb = converter.newConverterBuilder(); + cb.rule(foo); + cb.rule((v,t) -> v.toString()); + Converter ca = cb.build(); + + assertEquals(3L, (long) ca.convert(Arrays.asList("a", "b", "c")).to(Long.class)); + assertEquals(3, (long) ca.convert(Arrays.asList("a", "b", "c")).to(Integer.class)); + assertEquals("[a, b, c]", ca.convert(Arrays.asList("a", "b", "c")).to(String.class)); + assertNull(ca.convert(Arrays.asList()).to(String.class)); + } + + @Test + public void testWildcardAdapter1() { + ConverterFunction foo = new ConverterFunction() { + @Override + public Object apply(Object obj, Type type) throws Exception { + if (!(obj instanceof List)) + return ConverterFunction.CANNOT_HANDLE; + + List t = (List) obj; + if (type instanceof Class) { + if (Number.class.isAssignableFrom((Class) type)) + return converter.convert(t.size()).to(type); + } + return ConverterFunction.CANNOT_HANDLE; + } + }; + + ConverterBuilder cb = converter.newConverterBuilder(); + cb.rule((v,t) -> converter.convert(1).to(t)); + cb.rule(foo); + Converter ca = cb.build(); + + // The catchall converter should be called always because it can handle all and was registered first + assertEquals(1L, (long) ca.convert(Arrays.asList("a", "b", "c")).to(Long.class)); + assertEquals(1, (int) ca.convert(Arrays.asList("a", "b", "c")).to(Integer.class)); + assertEquals("1", ca.convert(Arrays.asList("a", "b", "c")).to(String.class)); + } + + @Test + public void testWildcardAdapter2() { + Map snooped = new HashMap<>(); + ConverterBuilder cb = converter.newConverterBuilder(); + cb.rule(new Rule>(v -> { + Arrays.sort(v, Collections.reverseOrder()); + return new ArrayList<>(Arrays.asList(v)); + }) {}); + cb.rule(new Rule>(v -> { + Arrays.sort(v, Collections.reverseOrder()); + return new CopyOnWriteArrayList<>(Arrays.asList(v)); + }) {}); + cb.rule((v,t) -> { snooped.put(v,t); return ConverterFunction.CANNOT_HANDLE;}); + Converter ca = cb.build(); + + assertEquals(new ArrayList<>(Arrays.asList("c", "b", "a")), ca.convert( + new String [] {"a", "b", "c"}).to(new TypeReference>() {})); + assertEquals("Precondition", 0, snooped.size()); + String[] sa0 = new String [] {"a", "b", "c"}; + assertEquals(new LinkedList<>(Arrays.asList("a", "b", "c")), ca.convert( + sa0).to(LinkedList.class)); + assertEquals(1, snooped.size()); + assertEquals(LinkedList.class, snooped.get(sa0)); + assertEquals(new CopyOnWriteArrayList<>(Arrays.asList("c", "b", "a")), ca.convert( + new String [] {"a", "b", "c"}).to(new TypeReference>() {})); + + snooped.clear(); + String[] sa = new String [] {"a", "b", "c"}; + assertEquals(new CopyOnWriteArrayList<>(Arrays.asList("a", "b", "c")), ca.convert( + sa).to(CopyOnWriteArrayList.class)); + assertEquals(1, snooped.size()); + assertEquals(CopyOnWriteArrayList.class, snooped.get(sa)); + } + + static interface MyIntf { + int value(); + } + + static class MyBean implements MyIntf { + int intfVal; + String beanVal; + + @Override + public int value() { + return intfVal; + } + + public String getValue() { + return beanVal; + } + } + + static class MyCustomDTO { + public String field; + } +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/ConverterCollectionsTest.java b/converter/converter/src/test/java/org/osgi/util/converter/ConverterCollectionsTest.java new file mode 100644 index 00000000000..c4f05c0020b --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/ConverterCollectionsTest.java @@ -0,0 +1,514 @@ +/* + * 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.osgi.util.converter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class ConverterCollectionsTest { + @Test + public void testLiveBackingList() { + List l = Arrays.asList(9, 8, 7); + Converter converter = Converters.standardConverter(); + List sl = converter.convert(l).view() + .to(new TypeReference>() {}); + + assertEquals(Short.valueOf((short) 9), sl.get(0)); + assertEquals(Short.valueOf((short) 8), sl.get(1)); + assertEquals(Short.valueOf((short) 7), sl.get(2)); + assertEquals(3, sl.size()); + + l.set(1, 11); + assertEquals(Short.valueOf((short) 9), sl.get(0)); + assertEquals(Short.valueOf((short) 11), sl.get(1)); + assertEquals(Short.valueOf((short) 7), sl.get(2)); + assertEquals(3, sl.size()); + + List sl2 = converter.convert(l).view() + .to(new TypeReference>() {}); + List sl3 = converter.convert(l).view() + .to(new TypeReference>() {}); + sl3.add(Short.valueOf((short) 6)); + + assertEquals(sl.hashCode(), sl2.hashCode()); + assertTrue(sl.hashCode() != sl3.hashCode()); + + assertEquals(sl, sl2); + assertFalse(sl.equals(sl3)); + } + + @Test + public void testLiveBackingList1() { + long[] a = new long[] { + 9l, 8l + }; + + List l = Converters.standardConverter().convert(a).view().to( + new TypeReference>() {}); + a[0] = 7l; + l.addAll(Arrays.asList(7, 6)); + a[0] = 1l; + assertEquals(Arrays.asList(7, 8, 7, 6), l); + } + + @Test + public void testLiveBackingList2() { + long[] a = new long[] { + 9l, 8l + }; + + List l = Converters.standardConverter().convert(a).view().to( + new TypeReference>() {}); + l.addAll(1, Arrays.asList(7, 6)); + a[0] = 1l; + assertEquals(Arrays.asList(9, 7, 6, 8), l); + } + + @Test + public void testLiveBackingList3() { + long[] a = new long[] { + 9l, 8l + }; + + List l = Converters.standardConverter().convert(a).view().to( + new TypeReference>() {}); + l.removeAll(Collections.singleton(8)); + a[0] = 1l; + assertEquals(Collections.singletonList(9), l); + } + + @Test + public void testLiveBackingList4() { + long[] a = new long[] { + 9l, 8l + }; + + List l = Converters.standardConverter().convert(a).view().to( + new TypeReference>() {}); + l.retainAll(Collections.singleton(8)); + a[1] = 1l; + assertEquals(Collections.singletonList(8), l); + } + + @Test + public void testLiveBackingList5() { + long[] a = new long[] { + 9l, 8l + }; + + List l = Converters.standardConverter().convert(a).view().to( + new TypeReference>() {}); + l.clear(); + l.add(10); + a[0] = 1l; + assertEquals(Collections.singletonList(10), l); + } + + @Test + public void testLiveBackingList6() { + long[] a = new long[] { + 9l, 8l + }; + + List l = Converters.standardConverter().convert(a).view().to( + new TypeReference>() {}); + l.add(10); + a[0] = 1l; + assertEquals(Arrays.asList(9, 8, 10), l); + } + + @Test + public void testLiveBackingList7() { + long[] a = new long[] { + 9l, 8l + }; + + List l = Converters.standardConverter().convert(a).view().to( + new TypeReference>() {}); + l.add(0, 10); + a[0] = 1l; + assertEquals(Arrays.asList(10, 9, 8), l); + } + + @Test + public void testLiveBackingList8() { + long[] a = new long[] { + 9l, 8l + }; + + List l = Converters.standardConverter().convert(a).view().to( + new TypeReference>() {}); + assertEquals(Integer.valueOf(8), l.remove(1)); + a[0] = 1l; + assertEquals(Arrays.asList(9), l); + } + + @Test + public void testLiveBackingCollection() { + Set s = new LinkedHashSet<>(Arrays.asList("yo", "yo", "ma")); + Converter converter = Converters.standardConverter(); + List sl = converter.convert(s).view() + .to(new TypeReference>() {}); + + assertEquals("yo", sl.get(0)); + assertEquals("ma", sl.get(1)); + assertEquals(2, sl.size()); + + s.add("ha"); + s.add("yo"); + assertEquals("yo", sl.get(0)); + assertEquals("ma", sl.get(1)); + assertEquals("ha", sl.get(2)); + assertEquals(3, sl.size()); + assertFalse(sl.isEmpty()); + + assertTrue(sl.contains("ma")); + assertFalse(sl.contains("na")); + + String[] sa = sl.toArray(new String[] {}); + assertEquals(3, sa.length); + assertEquals("yo", sa[0]); + assertEquals("ma", sa[1]); + assertEquals("ha", sa[2]); + + assertTrue(sl.containsAll(Arrays.asList("ma", "yo"))); + assertFalse(sl.containsAll(Arrays.asList("xxx"))); + } + + @Test + public void testLiveBackingEmptyCollection() { + Set s = Collections.emptySet(); + Collection< ? > l = Converters.standardConverter().convert(s).view().to( + Collection.class); + assertTrue(l.isEmpty()); + assertEquals(0, l.size()); + } + + @Test + public void testLiveBackingArray() { + Converter converter = Converters.standardConverter(); + int[] arr = new int[] { + 1, 2 + }; + + @SuppressWarnings("rawtypes") + List l = converter.convert(arr).view().to(List.class); + assertEquals(2, l.size()); + assertFalse(l.isEmpty()); + assertEquals(1, l.get(0)); + assertEquals(2, l.get(1)); + + assertTrue(l.contains(1)); + assertTrue(l.contains(2)); + assertFalse(l.contains(3)); + assertFalse(l.contains(0)); + + arr[0] = -3; + arr[1] = 3; + assertEquals(-3, l.get(0)); + assertEquals(3, l.get(1)); + } + + @Test + public void testLiveBackingMixedArrayWithNulls() { + Object[] oa = new Object[] { + "hi", null, 'x' + }; + List< ? > l = Converters.standardConverter().convert(oa).view().to(List.class); + assertTrue(l.contains("hi")); + assertTrue(l.contains(null)); + assertTrue(l.contains('x')); + assertFalse(l.containsAll(Arrays.asList('x', 7))); + assertTrue(l.containsAll(Arrays.asList('x', null, null, "hi", "hi"))); + assertEquals(0, l.indexOf("hi")); + assertEquals(1, l.indexOf(null)); + assertEquals(2, l.indexOf('x')); + assertEquals(-1, l.indexOf("test")); + + List< ? > l0 = l.subList(1, 1); + assertEquals(0, l0.size()); + List< ? > l1 = l.subList(1, 2); + assertEquals(Arrays.asList((Object) null), l1); + List< ? > l2 = l.subList(1, 3); + assertEquals(Arrays.asList(null, 'x'), l2); + List< ? > l3 = l.subList(0, 2); + assertEquals(Arrays.asList("hi", null), l3); + } + + @Test + public void testLiveStringArray() { + String[] sa = new String[] { + "yo", "ho", "yo", null, "yo" + }; + + List l = Converters.standardConverter().convert(sa).view().to( + new TypeReference>() {}); + Object[] oa1 = l.toArray(); + String[] sa1 = l.toArray(new String[] {}); + assertEquals("yo", sa1[0]); + assertEquals("ho", sa1[1]); + assertEquals("yo", sa1[2]); + assertNull(sa1[3]); + assertEquals("yo", sa1[4]); + assertEquals(oa1[0], sa1[0]); + assertEquals(oa1[1], sa1[1]); + assertEquals(oa1[2], sa1[2]); + assertEquals(oa1[3], sa1[3]); + assertEquals(oa1[4], sa1[4]); + assertEquals(5, oa1.length); + assertEquals(5, sa1.length); + + String[] sa2 = l.toArray(new String[6]); + assertEquals(oa1[0], sa2[0]); + assertEquals(oa1[1], sa2[1]); + assertEquals(oa1[2], sa2[2]); + assertEquals(oa1[3], sa2[3]); + assertEquals(oa1[4], sa2[4]); + assertNull(sa2[5]); + assertEquals(6, sa2.length); + + assertEquals(4, l.lastIndexOf("yo")); + assertEquals(1, l.lastIndexOf("ho")); + assertEquals(3, l.lastIndexOf(null)); + assertEquals(-1, l.lastIndexOf(123)); + } + + @Test + public void testLiveBackingArray0() { + Converter converter = Converters.standardConverter(); + List< ? > l = converter.convert(new double[] {}).view().to(List.class); + assertTrue(l.isEmpty()); + assertEquals(0, l.size()); + } + + @Test + public void testLiveBackingArray1() { + Converter converter = Converters.standardConverter(); + Integer[] arr = new Integer[] {1, 2}; + + @SuppressWarnings("rawtypes") + List l = converter.convert(arr).view().to(List.class); + assertEquals(1, l.get(0)); + assertEquals(2, l.get(1)); + + arr[0] = -3; + arr[1] = 3; + assertEquals(-3, l.get(0)); + assertEquals(3, l.get(1)); + } + + @Test + public void testLiveBackingArray2() { + Converter converter = Converters.standardConverter(); + Integer[] arr = new Integer[] {1, 2}; + + List l = converter.convert(arr).view().to(new TypeReference>() {}); + assertTrue(l.contains(Long.valueOf(2))); + assertTrue( + l.containsAll(Arrays.asList(Long.valueOf(2), Long.valueOf(1)))); + assertFalse(l.contains(Long.valueOf(3))); + assertFalse( + l.containsAll(Arrays.asList(Long.valueOf(2), Long.valueOf(3)))); + + arr[0] = Integer.valueOf(3); + assertTrue(l.contains(Long.valueOf(2))); + assertFalse( + l.containsAll(Arrays.asList(Long.valueOf(2), Long.valueOf(1)))); + assertTrue(l.contains(Long.valueOf(3))); + assertTrue( + l.containsAll(Arrays.asList(Long.valueOf(2), Long.valueOf(3)))); + + l.add(Long.valueOf(4)); + l.add(Long.valueOf(5)); + arr[0] = Integer.valueOf(1); + assertTrue(l.containsAll(Arrays.asList(Long.valueOf(2), Long.valueOf(3), + Long.valueOf(4), Long.valueOf(5)))); + } + + @Test + public void testLiveBackingArray3() { + Converter converter = Converters.standardConverter(); + Integer[] arr = new Integer[] { + 1, 2 + }; + + List l = converter.convert(arr).view() + .to(new TypeReference>() {}); + assertTrue(l.remove(Long.valueOf(1))); + arr[1] = Integer.valueOf(3); + assertEquals(Collections.singletonList(Long.valueOf(2)), l); + } + + @Test + public void testLiveArrayBackingSet() { + char[] ca = new char[] { + 'a', 'b', 'c' + }; + + Set s = Converters.standardConverter().convert(ca).view().to( + new TypeReference>() {}); + assertTrue(s.containsAll(Arrays.asList(Character.valueOf('a'), + Character.valueOf('b'), Character.valueOf('c')))); + + ca[0] = 'd'; + assertTrue(s.containsAll(Arrays.asList(Character.valueOf('b'), + Character.valueOf('c'), Character.valueOf('d')))); + } + + @Test + public void testLiveBackingSet() { + List l = new ArrayList<>(); + l.add(3.1415); + + Set s = Converters.standardConverter().convert(l).view().to( + new TypeReference>() {}); + Float f1 = Float.valueOf(3.1415f); + Float f2 = Float.valueOf(1.0f); + assertEquals(1, s.size()); + assertFalse(s.isEmpty()); + assertTrue(s.contains(f1)); + assertFalse(s.contains(f2)); + assertEquals(f1, s.iterator().next()); + + l.set(0, null); + assertEquals(1, s.size()); + assertFalse(s.isEmpty()); + assertTrue(s.contains(null)); + assertFalse(s.contains(f2)); + assertFalse(s.contains(f1)); + assertNull(s.iterator().next()); + + Float f3 = Float.valueOf(2.7182f); + s.add(f3); + assertEquals("Original should not be modified", 1, l.size()); + l.set(0, -1.0); + assertEquals(2, s.size()); + assertTrue(s.contains(null)); + assertTrue(s.contains(f3)); + assertFalse(s.contains(f2)); + assertFalse(s.contains(f1)); + } + + @Test + public void testLiveBackingSet0() { + List l = new ArrayList<>(); + l.addAll(Arrays.asList("hi", "there")); + + Set s = Converters.standardConverter().convert(l).view().to( + new TypeReference>() {}); + l.set(0, "ho"); + + String[] sa = s.toArray(new String[1]); + assertEquals(Arrays.asList("ho", "there"), Arrays.asList(sa)); + + String[] sa2 = s.toArray(new String[4]); + assertEquals(Arrays.asList("ho", "there", null, null), + Arrays.asList(sa2)); + + Set s2 = Converters.standardConverter().convert(l).view().to( + new TypeReference>() {}); + Set s3 = Converters.standardConverter().convert(l).view().to( + new TypeReference>() {}); + s3.add("!!"); + assertEquals(s.hashCode(), s2.hashCode()); + assertFalse(s.hashCode() == s3.hashCode()); + + assertTrue(s.equals(s2)); + assertFalse(s.equals(s3)); + } + + @Test + public void testLiveBackingSet1() { + List l = new ArrayList<>(); + l.addAll(Arrays.asList("hi", "there")); + + Set s = Converters.standardConverter().convert(l).view().to( + new TypeReference>() {}); + assertTrue(s.containsAll(Arrays.asList("there", "hi"))); + s.clear(); + assertEquals("Original should not be modified", Arrays.asList("hi", "there"), l); + assertEquals(0, s.size()); + assertTrue(s.isEmpty()); + } + + @Test + public void testLiveBackingSet2() { + List l = new ArrayList<>(); + l.addAll(Arrays.asList("hi", "there")); + + Set s = Converters.standardConverter().convert(l).view().to( + new TypeReference>() {}); + s.remove("yo"); + l.set(0, "xxx"); // Should not have an effect since 'remove' was called + assertTrue(s.containsAll(Arrays.asList("there", "hi"))); + + s.remove("hi"); + assertEquals(Collections.singleton("there"), s); + } + + @Test + public void testLiveBackingSet3() { + List l = new ArrayList<>(); + l.addAll(Arrays.asList("hi", "there")); + + Set s = Converters.standardConverter().convert(l).view().to( + new TypeReference>() {}); + assertFalse(s.addAll(Collections.singleton("there"))); + assertTrue(s.addAll(Arrays.asList("there", "!!"))); + l.remove("hi"); + assertTrue(s.containsAll(Arrays.asList("there", "hi", "!!"))); + } + + @Test + public void testLiveBackingSet4() { + List l = new ArrayList<>(); + l.addAll(Arrays.asList("hi", "there")); + + Set s = Converters.standardConverter().convert(l).view().to( + new TypeReference>() {}); + assertFalse(s.removeAll(Collections.singleton("yo"))); + l.remove("hi"); + assertTrue(s.containsAll(Arrays.asList("there", "hi"))); + assertTrue(s.removeAll(Arrays.asList("there", "hi"))); + assertEquals(0, s.size()); + } + + @Test + public void testLiveBackingSet5() { + List l = new ArrayList<>(); + l.addAll(Arrays.asList("hi", "there")); + + Set s = Converters.standardConverter().convert(l).view().to( + new TypeReference>() {}); + + assertTrue(s.retainAll(Arrays.asList("hi", "!!"))); + assertEquals(new HashSet<>(Collections.singleton("hi")), s); + } +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/ConverterFunctionTest.java b/converter/converter/src/test/java/org/osgi/util/converter/ConverterFunctionTest.java new file mode 100644 index 00000000000..f88d76afe6f --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/ConverterFunctionTest.java @@ -0,0 +1,67 @@ +/* + * 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.osgi.util.converter; + +import org.junit.Test; +import org.osgi.util.function.Function; + +import static org.junit.Assert.assertEquals; + +public class ConverterFunctionTest { + @Test + public void testConverterFunction() { + Converter c = Converters.standardConverter(); + assertEquals(12.5, c.convert("12.5").to(double.class), 0.001); + + Function f = c.function().to(double.class); + assertEquals(12.5, f.apply("12.5"), 0.001); + assertEquals(50.505, f.apply("50.505"), 0.001); + } + + @Test + public void testConverterFunctionWithModifier() { + Converter c = Converters.standardConverter(); + + Function cf = c.function().defaultValue(999).to(Integer.class); + + assertEquals(Integer.valueOf(999), + cf.apply("")); + assertEquals(Integer.valueOf(999), + c.convert("").defaultValue(999).to(Integer.class)); + + assertEquals(Integer.valueOf(123), + cf.apply("123")); + assertEquals(Integer.valueOf(123), + c.convert("123").defaultValue(999).to(Integer.class)); + } + + @Test + public void testConverterFunctionWithRule() { + Converter c = Converters.standardConverter(); + Function cf = c.function().to(String.class); + + String[] sa = new String [] {"h", "i"}; + assertEquals("h", cf.apply(sa)); + + Converter ac = c.newConverterBuilder(). + rule(new Rule(v -> String.join("", v)) {}). + build(); + + Function af = ac.function().to(String.class); + assertEquals("hi", af.apply(sa)); + } +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/ConverterMapTest.java b/converter/converter/src/test/java/org/osgi/util/converter/ConverterMapTest.java new file mode 100644 index 00000000000..803372aed9c --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/ConverterMapTest.java @@ -0,0 +1,678 @@ +/* + * 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.osgi.util.converter; + +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.osgi.util.converter.ConverterMapTest.TestValue; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; +import java.math.BigInteger; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class ConverterMapTest { + private Converter converter; + + @Before + public void setUp() { + converter = Converters.standardConverter(); + } + + @After + public void tearDown() { + converter = null; + } + + @Test + public void testGenericMapConversion() { + Map m1 = Collections.singletonMap(42, "987654321"); + Map m2 = converter.convert(m1).to(new TypeReference>(){}); + assertEquals(1, m2.size()); + assertEquals(987654321L, (long) m2.get("42")); + } + + @Test + public void testConvertMapToDictionary() throws Exception { + Map m = new HashMap<>(); + BigInteger bi = new BigInteger("123"); + URL url = new URL("http://0.0.0.0:123"); + m.put(bi, url); + + @SuppressWarnings("unchecked") + Dictionary d = converter.convert(m).to(Dictionary.class); + assertEquals(1, d.size()); + assertSame(bi, d.keys().nextElement()); + assertSame(url, d.get(bi)); + } + + @Test + public void testJavaBeanToMap() { + MyBean mb = new MyBean(); + mb.setMe("You"); + mb.setF(true); + mb.setNumbers(new int[] {3,2,1}); + + @SuppressWarnings("rawtypes") + Map m = converter.convert(mb).sourceAsBean().to(Map.class); + assertEquals(5, m.size()); + assertEquals("You", m.get("me")); + assertTrue((boolean) m.get("f")); + assertFalse((boolean) m.get("enabled")); + assertArrayEquals(new int [] {3,2,1}, (int[]) m.get("numbers")); + } + + @Test + public void testJavaBeanToMapCustom() { + SimpleDateFormat sdf = new SimpleDateFormat("yyMMddHHmmssZ"); + Date d = new Date(); + String expectedDate = sdf.format(d); + + MyBean mb = new MyBean(); + mb.setStartDate(d); + mb.setEnabled(true); + + ConverterBuilder cb = Converters.newConverterBuilder(); + cb.rule(new Rule(v -> sdf.format(v)) {}); + cb.rule(new Rule(v -> { + try { + return sdf.parse(v); + } catch (Exception ex) { + return null; + } + }) {}); + Converter ca = cb.build(); + Map m = ca.convert(mb).sourceAsBean().to(new TypeReference>(){}); + assertEquals("true", m.get("enabled")); + assertEquals(expectedDate, m.get("startDate")); + } + + @Test + public void testMapToJavaBean() { + Map m = new HashMap<>(); + + m.put("me", "Joe"); + m.put("enabled", "true"); + m.put("numbers", "42"); + m.put("s", "will disappear"); + MyBean mb = converter.convert(m).targetAsBean().to(MyBean.class); + assertEquals("Joe", mb.getMe()); + assertTrue(mb.isEnabled()); + assertNull(mb.getF()); + assertArrayEquals(new int[] {42}, mb.getNumbers()); + } + + public void testMapToJavaBean2() { + Map m = new HashMap<>(); + + m.put("blah", "blahblah"); + m.put("f", "true"); + MyBean mb = converter.convert(m).to(MyBean.class); + assertNull(mb.getMe()); + assertTrue(mb.getF()); + assertFalse(mb.isEnabled()); + assertNull(mb.getNumbers()); + } + + @Test + public void testInterfaceToMap() { + TestInterface impl = new TestInterface() { + @Override + public String foo() { + return "Chocolate!"; + } + + @Override + public int bar() { + return 76543; + } + + @Override + public int bar(String def) { + return 0; + } + + @Override + public Boolean za_za() { + return true; + } + }; + + @SuppressWarnings("rawtypes") + Map m = converter.convert(impl).to(Map.class); + assertEquals(3, m.size()); + assertEquals("Chocolate!", m.get("foo")); + assertEquals(76543, (int) m.get("bar")); + assertEquals(true, (boolean) m.get("za.za")); + } + + @Test + public void testInterfaceToMapEmptySub() { + TestInterfaceSub impl = new TestInterfaceSub() { + @Override + public String foo() { + return "Chocolate!"; + } + + @Override + public int bar() { + return 76543; + } + + @Override + public int bar(String def) { + return 0; + } + + @Override + public Boolean za_za() { + return true; + } + }; + + @SuppressWarnings("rawtypes") + Map m = converter.convert(impl).to(Map.class); + assertEquals(3, m.size()); + assertEquals("Chocolate!", m.get("foo")); + assertEquals(76543, (int) m.get("bar")); + assertEquals(true, (boolean) m.get("za.za")); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void testMapToInterface1() { + Map m = new HashMap<>(); + m.put("foo", 12345); + m.put("bar", "999"); + m.put("alt", "someval"); + m.put("za.za", true); + + TestInterface ti = converter.convert(m).to(TestInterface.class); + assertEquals("12345", ti.foo()); + assertEquals(999, ti.bar()); + assertEquals(Boolean.TRUE, ti.za_za()); + } + + @SuppressWarnings("rawtypes") + @Test + public void testMapToInterface2() { + Map m = new HashMap<>(); + + TestInterface ti = converter.convert(m).to(TestInterface.class); + try { + ti.foo(); + fail("Should have thrown a conversion exception"); + } catch (ConversionException ce) { + // good + } + assertEquals(999, ti.bar("999")); + try { + assertNull(ti.za_za()); + fail("Should have thrown a conversion exception"); + } catch (ConversionException ce) { + // good + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void testMapToAnnotation1() { + Map m = new HashMap<>(); + m.put("foo", 12345); + m.put("bar", "999"); + m.put("alt", "someval"); + m.put("za.za", true); + + TestAnnotation ta = converter.convert(m).to(TestAnnotation.class); + assertEquals("12345", ta.foo()); + assertEquals(999, ta.bar()); + assertTrue(ta.za_za()); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void testMapToAnnotationDefaults() { + Map m = new HashMap<>(); + m.put("alt", "someval"); + + TestAnnotation ta = converter.convert(m).to(TestAnnotation.class); + assertEquals("fooo!", ta.foo()); + assertEquals(42, ta.bar()); + } + + @Test + public void testAnnotationMethods() { + TestAnnotation ta = converter.convert(new HashMap<>()).to(TestAnnotation.class); + Map m = converter.convert(ta).view().to(new TypeReference>(){}); + assertEquals(3, m.size()); + assertEquals("fooo!", m.get("foo")); + assertEquals(42, m.get("bar")); + try { + assertEquals(false, m.get("za.za")); + fail("Should have thrown a conversion exception as there is no default for 'za.za'"); + } catch (ConversionException ce) { + // good + } + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void testSingleElementAnnotation() { + class MySingleElementAnnotation implements SingleElementAnnotation { + @Override + public Class annotationType() { + return SingleElementAnnotation.class; + } + + @Override + public String[] value() { + return new String[] {"hi", "there"}; + } + + @Override + public long somethingElse() { + return 42; + } + }; + MySingleElementAnnotation sea = new MySingleElementAnnotation(); + Map m = converter.convert(sea).to(Map.class); + assertEquals(2, m.size()); + assertArrayEquals(new String[] {"hi", "there"}, (String []) m.get("single.element.annotation")); + assertEquals(42L, m.get("somethingElse")); + + m.put("somethingElse", 51.0); + SingleElementAnnotation sea2 = converter.convert(m).to(SingleElementAnnotation.class); + assertArrayEquals(new String[] {"hi", "there"}, sea2.value()); + assertEquals(51L, sea2.somethingElse()); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void testCopyMap() { + Object obj = new Object(); + Map m = new HashMap<>(); + m.put("key", obj); + Map cm = converter.convert(m).to(Map.class); + assertNotSame(m, cm); + assertSame(m.get("key"), cm.get("key")); + } + + @Test + public void testProxyObjectMethodsInterface() { + Map m = new HashMap<>(); + TestInterface ti = converter.convert(m).to(TestInterface.class); + assertTrue(ti.equals(ti)); + assertFalse(ti.equals(new Object())); + assertFalse(ti.equals(null)); + + assertNotNull(ti.toString()); + assertTrue(ti.hashCode() != 0); + } + + @Test + public void testProxyObjectMethodsAnnotation() { + Map m = new HashMap<>(); + TestAnnotation ta = converter.convert(m).to(TestAnnotation.class); + assertTrue(ta.equals(ta)); + } + + @Test + public void testCaseInsensitiveKeysAnnotation() { + Map m = new HashMap<>(); + m.put("FOO", "Bleh"); + m.put("baR", 21); + m.put("za.za", true); + + TestInterface ti = converter.convert(m).keysIgnoreCase().to(TestInterface.class); + assertEquals("Bleh", ti.foo()); + assertEquals(21, ti.bar("42")); + assertTrue(ti.za_za()); + } + + @Test + public void testCaseSensitiveKeysAnnotation() { + Map m = new HashMap<>(); + m.put("FOO", "Bleh"); + m.put("baR", 21); + m.put("za.za", true); + + TestInterface ti = converter.convert(m).to(TestInterface.class); + try { + ti.foo(); + fail("Should have thrown a conversion exception as 'foo' was not set"); + } catch (ConversionException ce) { + // good + } + assertEquals(42, ti.bar("42")); + assertTrue(ti.za_za()); + } + + @Test + public void testCaseInsensitiveDTO() { + Dictionary d = new Hashtable<>(); + d.put("COUNT", "one"); + d.put("PinG", "Piiiiiiing!"); + d.put("pong", "999"); + + MyDTO dto = converter.convert(d).keysIgnoreCase().to(MyDTO.class); + assertEquals(MyDTO.Count.ONE, dto.count); + assertEquals("Piiiiiiing!", dto.ping); + assertEquals(999L, dto.pong); + } + + @Test + public void testCaseSensitiveDTO() { + Dictionary d = new Hashtable<>(); + d.put("COUNT", "one"); + d.put("PinG", "Piiiiiiing!"); + d.put("pong", "999"); + + MyDTO dto = converter.convert(d).to(MyDTO.class); + assertNull(dto.count); + assertNull(dto.ping); + assertEquals(999L, dto.pong); + } + + @Test + public void testRemovePasswords() { + Map m = new LinkedHashMap<>(); + m.put("foo", "bar"); + m.put("password", "secret"); + + Converter c = converter.newConverterBuilder(). + rule(new Rule,String>(v -> { + Map cm = new LinkedHashMap<>(v); + + for (Map.Entry entry : cm.entrySet()) { + if (entry.getKey().contains("password")) + entry.setValue("xxx"); + } + return cm.toString(); + }) {}). + build(); + assertEquals("{foo=bar, password=xxx}", c.convert(m).to(String.class)); + assertEquals("Original should not be modified", + "{foo=bar, password=secret}", m.toString()); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test @Ignore + public void testAnnotationDefaultMaterializer() throws Exception { + Map vals = new HashMap<>(); + vals.put("bar", 99L); + vals.put("tar", true); + vals.put("za.za", false); + + Class ta1cls = getClass().getClassLoader().loadClass(getClass().getPackage().getName() + ".sub1.TestAnn1"); + Object ta = converter.convert(vals).to(ta1cls); + Map vals2 = converter.convert(ta).to(Map.class); + vals2.putAll(vals); + Class ta2cls = getClass().getClassLoader().loadClass(getClass().getPackage().getName() + ".sub2.TestAnn2"); + Object ta2 = converter.convert(vals2).to(ta2cls); + + Method m1 = ta2cls.getDeclaredMethod("foo"); + m1.setAccessible(true); + assertEquals("fooo!", m1.invoke(ta2)); + + Method m2 = ta2cls.getDeclaredMethod("bar"); + m2.setAccessible(true); + assertEquals(99, m2.invoke(ta2)); + + Method m3 = ta2cls.getDeclaredMethod("tar"); + m3.setAccessible(true); + assertEquals(true, m3.invoke(ta2)); + } + + @Test + public void testMapEntry() { + Map m1 = Collections.singletonMap("Hi", Boolean.TRUE); + Map.Entry e1 = getMapEntry(m1); + + assertTrue(converter.convert(e1).to(Boolean.class)); + assertTrue(converter.convert(e1).to(boolean.class)); + assertEquals("Hi", converter.convert(e1).to(String.class)); + + } + + @Test + public void testMapEntry1() { + Map m1 = Collections.singletonMap(17L, "18"); + Map.Entry e1 = getMapEntry(m1); + + assertEquals(17L, converter.convert(e1).to(Number.class)); + assertEquals("18", converter.convert(e1).to(String.class)); + assertEquals("18", converter.convert(e1).to(Bar.class).value); + } + + @Test + public void testMapEntry2() { + Map m1 = Collections.singletonMap("123", Short.valueOf((short) 567)); + Map.Entry e1 = getMapEntry(m1); + + assertEquals(Integer.valueOf(123), converter.convert(e1).to(Integer.class)); + } + + @Test + public void testMapEntry3() { + Map l1 = Collections.singletonMap(9L, 10L); + Map.Entry e1 = getMapEntry(l1); + + assertEquals("Should take the key if key and value are equally suitable", + 9L, (long) converter.convert(e1).to(long.class)); + } + + @Test + public void testMapEntry4() { + Map m1 = Collections.singletonMap(new Foo(111), new Foo(999)); + Map.Entry e1 = getMapEntry(m1); + + assertEquals("111", converter.convert(e1).to(Bar.class).value); + } + + @Test + public void testDictionaryToAnnotation() { + Dictionary dict = new TestDictionary<>(); + dict.put("foo", "hello"); + TestAnnotation ta = converter.convert(dict).to(TestAnnotation.class); + assertEquals("hello", ta.foo()); + } + + @Test + public void testDictionaryToMap() { + Dictionary dict = new TestDictionary<>(); + dict.put("foo", "hello"); + @SuppressWarnings("rawtypes") + Map m = converter.convert(dict).to(Map.class); + assertEquals("hello", m.get("foo")); + } + + @Test + public void testInterfaceWithGetProperties() { + TestInterfaceWithGetProperties tiwgp = new TestInterfaceWithGetProperties() { + @Override + public int blah() { + return 99; + } + + @Override + public Dictionary getProperties() { + Dictionary d = new TestDictionary<>(); + d.put("hi", "ha"); + d.put("ho", "ho"); + return d; + } + }; + + @SuppressWarnings("rawtypes") + Map m = converter.convert(tiwgp).to(Map.class); + assertEquals(2, m.size()); + assertEquals("ha", m.get("hi")); + assertEquals("ho", m.get("ho")); + } + + @Test + public void testInterfaceWithGetPropertiesCopied() { + TestInterfaceWithGetProperties tiwgp = new TestInterfaceWithGetProperties() { + @Override + public int blah() { + return 99; + } + + @Override + public Dictionary getProperties() { + Dictionary d = new TestDictionary<>(); + d.put("hi", "ha"); + d.put("ho", "ho"); + return d; + } + }; + + @SuppressWarnings("rawtypes") + Map m = converter.convert(tiwgp).to(Map.class); + assertEquals(2, m.size()); + assertEquals("ha", m.get("hi")); + assertEquals("ho", m.get("ho")); + } + + @Test + public void testMapWithKeywords() { + Map m = new HashMap<>(); + m.put("new", "123"); + m.put("continue", 987l); + + MyDTOWithKeyWords dto = converter.convert(m).to(MyDTOWithKeyWords.class); + assertEquals(123l, dto.$new); + assertEquals("987", dto.$continue); + + Map m2 = converter.convert(dto).to(new TypeReference>() {}); + assertEquals(2, m2.size()); + assertEquals(123l, m2.get("new")); + assertEquals("987", m2.get("continue")); + } + + @Test + public void testSingleElementAnnotationPrefix() { + final Converter converter = Converters.standardConverter(); + final TestValue testValue = converter.convert(Collections.singletonMap("my.prefix.test.value", true)).to(TestValue.class); + assertTrue(testValue.value()); + } + + @Test + @TestValue(true) + public void testSingleElementAnnotationPrefixToMap() throws Exception { + final Converter converter = Converters.standardConverter(); + Method method = getClass().getMethod("testSingleElementAnnotationPrefixToMap"); + TestValue annotation = method.getDeclaredAnnotation(TestValue.class); + Map map = converter.convert(annotation).to(new TypeReference>() {}); + assertTrue((Boolean)map.get("my.prefix.test.value")); + } + + @Test + @PrefixMarkerAnnotation + public void testMarkerAnnotationPrefixToMap() throws Exception { + final Converter converter = Converters.standardConverter(); + Method method = getClass().getMethod("testMarkerAnnotationPrefixToMap"); + PrefixMarkerAnnotation annotation = method.getDeclaredAnnotation(PrefixMarkerAnnotation.class); + Map map = converter.convert(annotation).to(new TypeReference>() {}); + assertTrue(map.containsKey("org.foo.bar.prefix.marker.annotation")); + assertTrue((Boolean)map.get("org.foo.bar.prefix.marker.annotation")); + } + + private Map.Entry getMapEntry(Map map) { + assertEquals("This method assumes a map of size 1", 1, map.size()); + return map.entrySet().iterator().next(); + } + + interface TestInterface { + String foo(); + int bar(); + int bar(String def); + Boolean za_za(); + } + + interface TestInterfaceSub extends TestInterface {} + + interface TestInterfaceWithGetProperties { + int blah(); + Dictionary getProperties(); + } + + @interface TestAnnotation { + String foo() default "fooo!"; + int bar() default 42; + boolean za_za(); + } + + @interface SingleElementAnnotation { + String[] value(); + long somethingElse() default -87; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface TestValue { + static final String PREFIX_ = "my.prefix."; + + boolean value(); + } + + private static class Foo { + private final int value; + + Foo(int v) { + value = v; + } + + @Override + public String toString() { + return "" + value; + } + } + + public static class Bar { + final String value; + public Bar(String v) { + value = v; + } + } + + public static class MyDTOWithKeyWords { + public long $new; + public String $continue; + } +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/ConverterTest.java b/converter/converter/src/test/java/org/osgi/util/converter/ConverterTest.java new file mode 100644 index 00000000000..638feeb7bc7 --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/ConverterTest.java @@ -0,0 +1,1409 @@ +/* + * 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.osgi.util.converter; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.osgi.framework.Version; +import org.osgi.util.converter.MyDTO.Count; +import org.osgi.util.converter.MyEmbeddedDTO.Alpha; + +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Deque; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.NavigableSet; +import java.util.Queue; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TimeZone; +import java.util.UUID; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentNavigableMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class ConverterTest { + private Converter converter; + + @Before + public void setUp() { + converter = Converters.standardConverter(); + } + + @After + public void tearDown() { + converter = null; + } + + @Test + public void testVersion() { + Version v =new Version(1,2,3,"qualifier"); + Converter c = Converters.standardConverter(); + String s = c.convert(v).to(String.class); + Version v2 = c.convert(s).to(Version.class); + assertEquals(v, v2); + } + + @Test + public void testSimpleConversions() { + // Conversions to String + assertEquals("abc", converter.convert("abc").to(String.class)); + assertEquals("true", converter.convert(Boolean.TRUE).to(String.class)); + assertEquals("c", converter.convert('c').to(String.class)); + assertEquals("123", converter.convert(123).to(String.class)); + assertEquals("" + Long.MAX_VALUE, converter.convert(Long.MAX_VALUE).to(String.class)); + assertEquals("12.3", converter.convert(12.3f).to(String.class)); + assertEquals("12.345", converter.convert(12.345d).to(String.class)); + assertNull(converter.convert(null).to(String.class)); + assertNull(converter.convert(Collections.emptyList()).to(String.class)); + + String bistr = "999999999999999999999"; // more than Long.MAX_VALUE + assertEquals(bistr, converter.convert(new BigInteger(bistr)).to(String.class)); + + // Conversions to boolean + assertTrue(converter.convert("true").to(boolean.class)); + assertTrue(converter.convert("TRUE").to(boolean.class)); + assertTrue(converter.convert('x').to(boolean.class)); + assertTrue(converter.convert(Long.MIN_VALUE).to(boolean.class)); + assertTrue(converter.convert(72).to(boolean.class)); + assertFalse(converter.convert("false").to(boolean.class)); + assertFalse(converter.convert("bleh").to(boolean.class)); + assertFalse(converter.convert((char) 0).to(boolean.class)); + assertFalse(converter.convert(null).to(boolean.class)); + assertFalse(converter.convert(Collections.emptyList()).to(boolean.class)); + + // Conversions to integer + assertEquals(Integer.valueOf(123), converter.convert("123").to(int.class)); + assertEquals(1, (int) converter.convert(true).to(int.class)); + assertEquals(0, (int) converter.convert(false).to(int.class)); + assertEquals(65, (int) converter.convert('A').to(int.class)); + + // Conversions to long + assertEquals(Long.valueOf(65), converter.convert('A').to(Long.class)); + + // Conversions to Class + assertEquals(BigDecimal.class, converter.convert("java.math.BigDecimal").to(Class.class)); + assertEquals(BigDecimal.class, converter.convert("java.math.BigDecimal").to(new TypeReference>() {})); + assertNull(converter.convert(null).to(Class.class)); + assertNull(converter.convert(Collections.emptyList()).to(Class.class)); + + assertEquals(Integer.valueOf(123), converter.convert("123").to(Integer.class)); + assertEquals(Long.valueOf(123), converter.convert("123").to(Long.class)); + assertEquals('1', (char) converter.convert("123").to(Character.class)); + assertEquals('Q', (char) converter.convert(null).defaultValue('Q').to(Character.class)); + assertEquals((char) 123, (char) converter.convert(123L).to(Character.class)); + assertEquals((char) 123, (char) converter.convert(123).to(Character.class)); + assertEquals(Byte.valueOf((byte) 123), converter.convert("123").to(Byte.class)); + assertEquals(Float.valueOf("12.3"), converter.convert("12.3").to(Float.class)); + assertEquals(Double.valueOf("12.3"), converter.convert("12.3").to(Double.class)); + } + + @Test + public void testCharAggregateToString() { + Converter c = Converters.newConverterBuilder(). + rule(new Rule, String>(ConverterTest::characterListToString) {}). + rule(new Rule>(ConverterTest::stringToCharacterList) {}). + build(); + + char[] ca = new char[] {'h', 'e', 'l', 'l', 'o'}; + assertEquals("hello", c.convert(ca).to(String.class)); + + Character[] ca2 = c.convert(ca).to(Character[].class); + assertEquals("hello", c.convert(ca2).to(String.class)); + + List cl = c.convert(ca).to(new TypeReference>() {}); + assertEquals("hello", c.convert(cl).to(String.class)); + + // And back + assertArrayEquals(ca, c.convert("hello").to(char[].class)); + assertArrayEquals(ca2, c.convert("hello").to(Character[].class)); + assertEquals(cl, c.convert("hello").to(new TypeReference>() {})); + } + + private static String characterListToString(List cl) { + StringBuilder sb = new StringBuilder(cl.size()); + for (char c : cl) { + sb.append(c); + } + return sb.toString(); + } + + private static List stringToCharacterList(String s) { + List lc = new ArrayList<>(); + + for (int i=0; i l = new ArrayList<>(Arrays.asList("A", 'B', 333)); + + Set s = converter.convert(l).to(Set.class); + assertEquals(3, s.size()); + + for (Object o : s) { + Object expected = l.remove(0); + assertEquals(expected, o); + } + } + + @Test + public void testFromGenericSetToLinkedList() { + Set s = new LinkedHashSet<>(); + s.add(123); + s.add(456); + + LinkedList ll = converter.convert(s).to(new TypeReference>() {}); + assertEquals(Arrays.asList("123", "456"), ll); + } + + @Test + public void testFromArrayToGenericOrderPreservingSet() { + String[] sa = {"567", "-765", "0", "-900"}; + + // Returned set should be order preserving + Set s = converter.convert(sa).to(new TypeReference>() {}); + + List sl = new ArrayList<>(Arrays.asList(sa)); + for (long l : s) { + long expected = Long.parseLong(sl.remove(0)); + assertEquals(expected, l); + } + } + + @Test + public void testFromSetToArray() { + Set s = new LinkedHashSet<>(); + s.add(Integer.MIN_VALUE); + + long[] la = converter.convert(s).to(long[].class); + assertEquals(1, la.length); + assertEquals(Integer.MIN_VALUE, la[0]); + } + + @Test + public void testStringArrayToIntegerArray() { + String[] sa = {"999", "111", "-909"}; + Integer[] ia = converter.convert(sa).to(Integer[].class); + assertEquals(3, ia.length); + assertArrayEquals(new Integer[] {999, 111, -909}, ia); + } + + @Test + public void testCharArrayConversion() { + char[] ca = converter.convert(new int[] {9,8,7}).to(char[].class); + assertArrayEquals(new char[] {9,8,7}, ca); + Character[] ca2 = converter.convert((long) 17).to(Character[].class); + assertArrayEquals(new Character[] {(char)17}, ca2); + char[] ca3 = converter.convert(new short[] {257}).to(char[].class); + assertArrayEquals(new char[] {257}, ca3); + char c = converter.convert(new char[] {'x', 'y'}).to(char.class); + assertEquals('x', c); + char[] ca4a = {'x', 'y'}; + char[] ca4b = converter.convert(ca4a).to(char[].class); + assertArrayEquals(new char [] {'x', 'y'}, ca4b); + assertNotSame("Should have created a new instance", ca4a, ca4b); + } + + @Test + public void testLongCollectionConversion() { + long[] l = converter.convert(Long.MAX_VALUE).to(long[].class); + assertArrayEquals(new long[] {Long.MAX_VALUE}, l); + Long[] l2 = converter.convert(Long.MAX_VALUE).to(Long[].class); + assertArrayEquals(new Long[] {Long.MAX_VALUE}, l2); + List ll = converter.convert(new long[] {Long.MIN_VALUE, Long.MAX_VALUE}).to(new TypeReference>() {}); + assertEquals(Arrays.asList(Long.MIN_VALUE, Long.MAX_VALUE), ll); + List ll2 = converter.convert(Arrays.asList(123, 345)).to(new TypeReference>() {}); + assertEquals(Arrays.asList(123L, 345L), ll2); + + } + + @Test + public void testExceptionDefaultValue() { + assertEquals(42, (int) converter.convert("haha").defaultValue(42).to(int.class)); + assertEquals(999, (int) converter.convert("haha").defaultValue(999).to(int.class)); + try { + converter.convert("haha").to(int.class); + fail("Should have thrown an exception"); + } catch (ConversionException ex) { + // good + } + } + + @Test + public void testStandardStringArrayConversion() { + String[] sa = {"A", "B"}; + assertEquals("A", converter.convert(sa).toString()); + assertEquals("A", converter.convert(sa).to(String.class)); + + String[] sa2 = {"A"}; + assertArrayEquals(sa2, converter.convert("A").to(String[].class)); + } + + @Test + public void testCustomStringArrayConversion() { + ConverterBuilder cb = converter.newConverterBuilder(); + cb.rule(new Rule(v -> Stream.of(v).collect(Collectors.joining(","))){}); + cb.rule(new Rule(v -> v.split(",")){}); + + Converter adapted = cb.build(); + + String[] sa = {"A", "B"}; + assertEquals("A,B", adapted.convert(sa).to(String.class)); + assertArrayEquals(sa, adapted.convert("A,B").to(String[].class)); + } + + @Test + public void testCustomIntArrayConversion() { + ConverterBuilder cb = converter.newConverterBuilder(); + cb.rule(String.class, (f,t) -> f instanceof int[] ? Arrays.stream((int []) f).mapToObj(Integer::toString).collect(Collectors.joining(",")) : null); + cb.rule(int[].class, (f,t) -> f instanceof String ? Arrays.stream(((String) f).split(",")).mapToInt(Integer::parseInt).toArray() : null); + Converter adapted = cb.build(); + + int[] ia = {1, 2}; + assertEquals("1,2", adapted.convert(ia).to(String.class)); + assertArrayEquals(ia, adapted.convert("1,2").to(int[].class)); + } + + @Test + public void testCustomConverterChaining() { + ConverterBuilder cb = converter.newConverterBuilder(); + cb.rule(Date.class, + (f,t) -> f instanceof String ? new Date(0) : ConverterFunction.CANNOT_HANDLE); + Converter c1 = cb.build(); + assertEquals(new Date(0), c1.convert("something").to(Date.class)); + assertEquals(new Date(0), c1.convert("foo").to(Date.class)); + + ConverterBuilder cb2 = c1.newConverterBuilder(); + cb2.rule(Date.class, + (f,t) -> f.equals("foo") ? new Date(100000) : ConverterFunction.CANNOT_HANDLE); + Converter c2 = cb2.build(); + assertEquals(new Date(0), c2.convert("something").to(Date.class)); + assertEquals(new Date(100000), c2.convert("foo").to(Date.class)); + } + + + @Test + public void testCustomErrorHandling() { + ConverterFunction func = new ConverterFunction() { + @Override + public Object apply(Object obj, Type targetType) { + if ("hello".equals(obj)) { + return -1; + } + if ("goodbye".equals(obj)) { + return null; + } + return ConverterFunction.CANNOT_HANDLE; + } + }; + + ConverterBuilder cb = converter.newConverterBuilder(); + Converter adapted = cb.errorHandler(func).build(); + + assertEquals(new Integer(12), adapted.convert("12").to(Integer.class)); + assertEquals(new Integer(-1), adapted.convert("hello").to(Integer.class)); + assertNull(adapted.convert("goodbye").to(Integer.class)); + + try { + adapted.convert("nothing").to(Integer.class); + fail("Should have thrown a Conversion Exception when converting 'hello' to a number"); + } catch (ConversionException ce) { + // good + } + + // This is with the non-adapted converter + try { + converter.convert("hello").to(Integer.class); + fail("Should have thrown a Conversion Exception when converting 'hello' to a number"); + } catch (ConversionException ce) { + // good + } + } + + @Test + public void testCustomErrorHandlingProxy() { + ConverterFunction errHandler = new ConverterFunction() { + @Override + public Object apply(Object obj, Type targetType) throws Exception { + return 123; + } + }; + ConverterBuilder cb = converter.newConverterBuilder(); + Converter c = cb.errorHandler(errHandler).build(); + + Map m = new HashMap<>(); + + MyIntf i = c.convert(m).to(MyIntf.class); + assertEquals(123, i.value()); + } + + @Test + public void testMultipleCustomErrorHandling() { + ConverterBuilder cb1 = converter.newConverterBuilder(); + ConverterFunction func1 = new ConverterFunction() { + @Override + public Object apply(Object obj, Type targetType) { + return -1; + } + }; + cb1.errorHandler(func1); + Converter c1 = cb1.build(); + + ConverterBuilder cb2 = c1.newConverterBuilder(); + ConverterFunction func2 = new ConverterFunction() { + @Override + public Object apply(Object obj, Type targetType) { + if ("hello".equals(obj)) { + return 0; + } + return ConverterFunction.CANNOT_HANDLE; + } + }; + cb2.errorHandler(func2); + Converter adapted = cb2.build(); + + assertEquals(Integer.valueOf(0), adapted.convert("hello").to(Integer.class)); + assertEquals(Integer.valueOf(-1), adapted.convert("bye").to(Integer.class)); + } + + static class MyConverterFunction implements ConverterFunction { + @Override + public Object apply(Object obj, Type targetType) throws Exception { + if ("hello".equals(obj)) { + return 0; + } + return ConverterFunction.CANNOT_HANDLE; + } + } + + @Test + public void testUUIDConversion() { + UUID uuid = UUID.randomUUID(); + String s = converter.convert(uuid).to(String.class); + assertTrue("UUID should be something", s.length() > 0); + UUID uuid2 = converter.convert(s).to(UUID.class); + assertEquals(uuid, uuid2); + } + + @Test + public void testPatternConversion() { + String p = "\\S*"; + Pattern pattern = converter.convert(p).to(Pattern.class); + Matcher matcher = pattern.matcher("hi"); + assertTrue(matcher.matches()); + String p2 = converter.convert(pattern).to(String.class); + assertEquals(p, p2); + } + + @Test + public void testLocalDateTime() { + LocalDateTime ldt = LocalDateTime.now(); + String s = converter.convert(ldt).to(String.class); + assertTrue(s.length() > 0); + LocalDateTime ldt2 = converter.convert(s).to(LocalDateTime.class); + assertEquals(ldt, ldt2); + } + + @Test + public void testLocalDate() { + LocalDate ld = LocalDate.now(); + String s = converter.convert(ld).to(String.class); + assertTrue(s.length() > 0); + LocalDate ld2 = converter.convert(s).to(LocalDate.class); + assertEquals(ld, ld2); + } + + @Test + public void testLocalTime() { + LocalTime lt = LocalTime.now(); + String s = converter.convert(lt).to(String.class); + assertTrue(s.length() > 0); + LocalTime lt2 = converter.convert(s).to(LocalTime.class); + assertEquals(lt, lt2); + } + + @Test + public void testOffsetDateTime() { + OffsetDateTime ot = OffsetDateTime.now(); + String s = converter.convert(ot).to(String.class); + assertTrue(s.length() > 0); + OffsetDateTime ot2 = converter.convert(s).to(OffsetDateTime.class); + assertEquals(ot, ot2); + } + + @Test + public void testOffsetTime() { + OffsetTime ot = OffsetTime.now(); + String s = converter.convert(ot).to(String.class); + assertTrue(s.length() > 0); + OffsetTime ot2 = converter.convert(s).to(OffsetTime.class); + assertEquals(ot, ot2); + } + + @Test + public void testZonedDateTime() { + ZonedDateTime zdt = ZonedDateTime.now(); + String s = converter.convert(zdt).to(String.class); + assertTrue(s.length() > 0); + ZonedDateTime zdt2 = converter.convert(s).to(ZonedDateTime.class); + assertEquals(zdt, zdt2); + } + + @Test + public void testCalendarDate() { + Calendar cal = new GregorianCalendar(1971, 1, 13, 12, 37, 41); + TimeZone tz =TimeZone.getTimeZone("CET"); + cal.setTimeZone(tz); + Date d = cal.getTime(); + + Converter c = converter; + + String s = c.convert(d).toString(); + assertEquals("1971-02-13T11:37:41Z", s); + assertEquals(d, c.convert(s).to(Date.class)); + + String s2 = c.convert(cal).toString(); + assertEquals("1971-02-13T11:37:41Z", s2); + Calendar cal2 = c.convert(s2).to(Calendar.class); + assertEquals(cal.getTime(), cal2.getTime()); + } + + @Test + public void testCalendarLong() { + Calendar cal = new GregorianCalendar(1971, 1, 13, 12, 37, 41); + TimeZone tz =TimeZone.getTimeZone("UTC"); + cal.setTimeZone(tz); + + long l = converter.convert(cal).to(Long.class); + assertEquals(l, cal.getTimeInMillis()); + + Calendar cal2 = converter.convert(l).to(Calendar.class); + assertEquals(cal.getTime(), cal2.getTime()); + } + + @Test + public void testDefaultValue() { + long l = converter.convert(null).defaultValue("12").to(Long.class); + assertEquals(12L, l); + assertNull(converter.convert("haha").defaultValue(null).to(Integer.class)); + assertNull(converter.convert("test").defaultValue(null).to(new TypeReference>() {})); + } + + @Test + public void testDTO2Map() { + MyEmbeddedDTO embedded = new MyEmbeddedDTO(); + embedded.marco = "hohoho"; + embedded.polo = Long.MAX_VALUE; + embedded.alpha = Alpha.A; + + MyDTO dto = new MyDTO(); + dto.ping = "lalala"; + dto.pong = Long.MIN_VALUE; + dto.count = Count.ONE; + dto.embedded = embedded; + + @SuppressWarnings("rawtypes") + Map m = converter.convert(dto).to(Map.class); + assertEquals(4, m.size()); + assertEquals("lalala", m.get("ping")); + assertEquals(Long.MIN_VALUE, m.get("pong")); + assertEquals(Count.ONE, m.get("count")); + assertNotNull(m.get("embedded")); + + MyEmbeddedDTO e = (MyEmbeddedDTO) m.get("embedded"); + assertEquals("hohoho", e.marco); + assertEquals(Long.MAX_VALUE, e.polo); + assertEquals(Alpha.A, e.alpha); + } + + @Test + public void testDTO2Map2() { + MyEmbeddedDTO embedded = new MyEmbeddedDTO(); + embedded.marco = "hohoho"; + embedded.polo = Long.MAX_VALUE; + embedded.alpha = Alpha.A; + + MyDTO dto = new MyDTO(); + dto.ping = "lalala"; + dto.pong = Long.MIN_VALUE; + dto.count = Count.ONE; + dto.embedded = embedded; + + @SuppressWarnings("rawtypes") + Map m = converter.convert(dto).sourceAsDTO().to(Map.class); + assertEquals(4, m.size()); + assertEquals("lalala", m.get("ping")); + assertEquals(Long.MIN_VALUE, m.get("pong")); + assertEquals(Count.ONE, m.get("count")); + assertNotNull(m.get("embedded")); + + MyEmbeddedDTO e = (MyEmbeddedDTO) m.get("embedded"); + assertEquals("hohoho", e.marco); + assertEquals(Long.MAX_VALUE, e.polo); + assertEquals(Alpha.A, e.alpha); + + /* TODO this is the way it was, but it does not seem right + Map e = (Map)m.get("embedded"); + assertEquals("hohoho", e.get("marco")); + assertEquals(Long.MAX_VALUE, e.get("polo")); + assertEquals(Alpha.A, e.get("alpha")); + */ + } + + @Test + public void testDTO2Map3() { + MyEmbeddedDTO embedded2 = new MyEmbeddedDTO(); + embedded2.marco = "hohoho"; + embedded2.polo = Long.MAX_VALUE; + embedded2.alpha = Alpha.A; + + MyDTOWithMethods embedded = new MyDTOWithMethods(); + embedded.ping = "lalala"; + embedded.pong = Long.MIN_VALUE; + embedded.count = Count.ONE; + embedded.embedded = embedded2; + + MyDTO8 dto = new MyDTO8(); + dto.ping = "lalala"; + dto.pong = Long.MIN_VALUE; + dto.count = MyDTO8.Count.ONE; + dto.embedded = embedded; + + @SuppressWarnings("rawtypes") + Map m = converter.convert(dto).sourceAsDTO().to(Map.class); + assertEquals(4, m.size()); + assertEquals("lalala", m.get("ping")); + assertEquals(Long.MIN_VALUE, m.get("pong")); + assertEquals(MyDTO8.Count.ONE, m.get("count")); + assertNotNull(m.get("embedded")); + assertTrue(m.get( "embedded" ) instanceof MyDTOWithMethods); + MyDTOWithMethods e = (MyDTOWithMethods)m.get("embedded"); + assertEquals("lalala", e.ping); + assertEquals(Long.MIN_VALUE, e.pong); + assertEquals(Count.ONE, e.count); + assertNotNull(e.embedded); + assertTrue(e.embedded instanceof MyEmbeddedDTO); + MyEmbeddedDTO e2 = e.embedded; + assertEquals("hohoho", e2.marco); + assertEquals(Long.MAX_VALUE, e2.polo); + assertEquals(Alpha.A, e2.alpha); + } + + @Test + public void testDTO2Map4() { + MyDefaultCtorDTOAlike dto = new MyDefaultCtorDTOAlike(); + dto.myProp = "myValue"; + + @SuppressWarnings("rawtypes") + Map m = converter.convert(dto).to(Map.class); + assertEquals(1, m.size()); + assertEquals("myValue", m.get("myProp")); + } + + @Test + public void testDTO2Map5() { + MyDTO3 dto = new MyDTO3(); + dto.charSet = new HashSet<>(Arrays.asList('f', 'o', 'o')); + + @SuppressWarnings("rawtypes") + Map m = converter.convert(dto).to(new TypeReference>() {}); + assertEquals(1, m.size()); + assertEquals(dto.charSet, m.get("charSet")); + + m = converter.convert(dto).to(new TypeReference>>() {}); + assertEquals(1, m.size()); + + List list = new ArrayList<>(); + for (Character character : dto.charSet) { + list.add(String.valueOf(character)); + } + + assertEquals(list, m.get("charSet")); + } + + @Test @SuppressWarnings({ "rawtypes", "unchecked" }) + public void testDTOFieldShadowing() { + MySubDTO dto = new MySubDTO(); + dto.ping = "test"; + dto.count = Count.THREE; + + Map m = converter.convert(dto).to(new TypeReference>() {}); + + Map expected = new HashMap<>(); + expected.put("ping", "test"); + expected.put("count", "THREE"); + expected.put("pong", "0"); + expected.put("embedded", null); + assertEquals(expected, new HashMap(m)); + + MySubDTO dto2 = converter.convert(m).to(MySubDTO.class); + assertEquals("test", dto2.ping); + assertEquals(Count.THREE, dto2.count); + assertEquals(0L, dto2.pong); + assertNull(dto2.embedded); + } + + @Test + public void testMap2DTO() { + Map m = new HashMap<>(); + m.put("ping", "abc xyz"); + m.put("pong", 42L); + m.put("count", Count.ONE); + Map e = new HashMap<>(); + e.put("marco", "ichi ni san"); + e.put("polo", 64L); + e.put("alpha", Alpha.A); + m.put("embedded", e); + + MyDTO dto = converter.convert(m).to(MyDTO.class); + assertEquals("abc xyz", dto.ping); + assertEquals(42L, dto.pong); + assertEquals(Count.ONE, dto.count); + assertNotNull(dto.embedded); + assertEquals(dto.embedded.marco, "ichi ni san"); + assertEquals(dto.embedded.polo, 64L); + assertEquals(dto.embedded.alpha, Alpha.A); + } + + @Test + public void testMap2DTOView() { + Map src = Collections.singletonMap("pong", 42); + MyDTOWithMethods dto = converter.convert(src).targetAs(MyDTO.class).to(MyDTOWithMethods.class); + assertEquals(42, dto.pong); + } + + @Test @SuppressWarnings({ "rawtypes", "unchecked" }) + public void testDTOWithGenerics() { + MyDTO2 dto = new MyDTO2(); + dto.longList = Arrays.asList(999L, 1000L); + dto.dtoMap = new LinkedHashMap<>(); + + MyDTO3 subDTO1 = new MyDTO3(); + subDTO1.charSet = new HashSet<>(Arrays.asList('f', 'o', 'o')); + dto.dtoMap.put("zzz", subDTO1); + + MyDTO3 subDTO2 = new MyDTO3(); + subDTO2.charSet = new HashSet<>(Arrays.asList('b', 'a', 'r')); + dto.dtoMap.put("aaa", subDTO2); + + Map m = converter.convert(dto).to(Map.class); + assertEquals(2, m.size()); + + assertEquals(Arrays.asList(999L, 1000L), m.get("longList")); + Map nestedMap = (Map) m.get("dtoMap"); + + // Check iteration order is preserved by iterating + int i=0; + for (Iterator it = nestedMap.entrySet().iterator(); it.hasNext(); i++) { + Map.Entry entry = it.next(); + switch (i) { + case 0: + assertEquals("zzz", entry.getKey()); + MyDTO3 dto1 = (MyDTO3) entry.getValue(); + assertNotSame("Should have created a copy", subDTO1, dto1); + assertEquals(new HashSet(Arrays.asList('f', 'o')), dto1.charSet); + break; + case 1: + assertEquals("aaa", entry.getKey()); + MyDTO3 dto2 = (MyDTO3) entry.getValue(); + assertNotSame("Should have created a copy", subDTO2, dto2); + assertEquals(new HashSet(Arrays.asList('b', 'a', 'r')), dto2.charSet); + break; + default: + fail("Unexpected number of elements on map"); + } + } + + // convert back + MyDTO2 dto2 = converter.convert(m).to(MyDTO2.class); + assertEquals(dto.longList, dto2.longList); + + // Cannot simply do dto.equals() as the DTOs don't implement that + assertEquals(dto.dtoMap.size(), dto2.dtoMap.size()); + MyDTO3 dto2SubZZZ = dto2.dtoMap.get("zzz"); + assertEquals(dto2SubZZZ.charSet, new HashSet(Arrays.asList('f', 'o'))); + MyDTO3 dto2SubAAA = dto2.dtoMap.get("aaa"); + assertEquals(dto2SubAAA.charSet, new HashSet(Arrays.asList('b', 'a', 'r'))); + } + + @Test + public void testMapToDTOWithGenerics() { + Map dto = new HashMap<>(); + + dto.put("longList", Arrays.asList((short)999, "1000")); + + Map dtoMap = new LinkedHashMap<>(); + dto.put("dtoMap", dtoMap); + + Map subDTO1 = new HashMap<>(); + subDTO1.put("charSet", new HashSet<>(Arrays.asList("foo", (int) 'o', 'o'))); + dtoMap.put("zzz", subDTO1); + + Map subDTO2 = new HashMap<>(); + subDTO2.put("charSet", new HashSet<>(Arrays.asList('b', 'a', 'r'))); + dtoMap.put("aaa", subDTO2); + + MyDTO2 converted = converter.convert(dto).to(MyDTO2.class); + + assertEquals(Arrays.asList(999L, 1000L), converted.longList); + Map nestedMap = converted.dtoMap; + + // Check iteration order is preserved by iterating + int i=0; + for (Iterator> it = nestedMap.entrySet().iterator(); it.hasNext(); i++) { + Map.Entry entry = it.next(); + switch (i) { + case 0: + assertEquals("zzz", entry.getKey()); + MyDTO3 dto1 = entry.getValue(); + assertEquals(new HashSet(Arrays.asList('f', 'o')), dto1.charSet); + break; + case 1: + assertEquals("aaa", entry.getKey()); + MyDTO3 dto2 = entry.getValue(); + assertEquals(new HashSet(Arrays.asList('b', 'a', 'r')), dto2.charSet); + break; + default: + fail("Unexpected number of elements on map"); + } + } + } + + @Test + public void testMapToDTOWithGenericVariables() { + Map dto = new HashMap<>(); + dto.put("set", new HashSet<>(Arrays.asList("foo", (int) 'o', 'o'))); + dto.put("raw", "1234"); + dto.put("array", Arrays.asList("foo", (int) 'o', 'o')); + + MyGenericDTOWithVariables converted = + converter.convert(dto).to(new TypeReference>() {}); + assertEquals(Character.valueOf('1'), converted.raw); + assertArrayEquals(new Character[] {'f', 'o', 'o'}, converted.array); + assertEquals(new HashSet(Arrays.asList('f', 'o')), converted.set); + } + + @Test + public void testMapToDTOWithSurplusMapFiels() { + Map m = new HashMap<>(); + m.put("foo", "bar"); + MyDTO3 dtoDoesNotMap = converter.convert(m).to(MyDTO3.class); + assertNull(dtoDoesNotMap.charSet); + } + + @Test @SuppressWarnings("rawtypes") + public void testCopyMap() { + Map m = new HashMap(); + Map m2 = converter.convert(m).to(Map.class); + assertEquals(m, m2); + assertNotSame(m, m2); + } + + @Test @SuppressWarnings({ "rawtypes", "unchecked" }) + public void testCopyMap2() { + Map m = new HashMap(); + m.put("key", Arrays.asList("a", "b", "c")); + Map m2 = converter.convert(m).to(Map.class); + assertEquals(m, m2); + assertNotSame(m, m2); + } + + @Test + public void testConversionPriority() { + MyBean mb = new MyBean(); + mb.intfVal = 17; + mb.beanVal = "Hello"; + + assertEquals(Collections.singletonMap("value", "Hello"), + converter.convert(mb).sourceAsBean().to(Map.class)); + } + + @Test + public void testConvertAsInterface() { + MyBean mb = new MyBean(); + mb.intfVal = 17; + mb.beanVal = "Hello"; + + assertEquals(17, + converter.convert(mb).sourceAs(MyIntf.class).to(Map.class).get("value")); + } + + @Test + public void testConvertAsBean() { + MyBean mb = new MyBean(); + mb.intfVal = 17; + mb.beanVal = "Hello"; + + assertEquals(Collections.singletonMap("value", "Hello"), + converter.convert(mb).sourceAsBean().to(Map.class)); + } + + @Test + public void testConvertAsDTO() { + MyClass3 mc3 = new MyClass3(17); + + assertEquals(17, + converter.convert(mc3).sourceAsDTO().to(Map.class).get("value")); + } + + @Test + public void testDTONameMangling() { + Map m = new HashMap<>(); + m.put("org.osgi.framework.uuid", "test123"); + m.put("myProperty143", "true"); + m.put("my$prop", "42"); + m.put("dot.prop", "456"); + m.put(".secret", " "); + m.put("another_prop", "lalala"); + m.put("three_.prop", "hi ha ho"); + m.put("four._prop", ""); + m.put("five..prop", "test"); + m.put("six-prop", "987"); + m.put("seven$.prop", "3.141"); + + MyDTO7 dto = converter.convert(m).to(MyDTO7.class); + assertEquals("test123", dto.org_osgi_framework_uuid); + assertTrue(dto.myProperty143); + assertEquals(42, dto.my$$prop); + assertEquals(Long.valueOf(456L), dto.dot_prop); + assertEquals(' ', dto._secret); + assertEquals("lalala", dto.another__prop); + assertEquals("hi ha ho", dto.three___prop); + assertEquals("", dto.four_$__prop); + assertEquals("test", dto.five_$_prop); + assertEquals((short) 987, dto.six$_$prop); + dto.seven$$_$prop = 3.141; + + // And convert back + Map m2 = converter.convert(dto).to(new TypeReference>() {}); + assertEquals(new HashMap(m), new HashMap(m2)); + } + + @Test + public void testCollectionInterfaceMapping() { + Collection coll = converter.convert("test").to(Collection.class); + assertEquals("test", coll.iterator().next()); + + List list = converter.convert("test").to(List.class); + assertEquals("test", list.iterator().next()); + + Set set = converter.convert("test").to(Set.class); + assertEquals("test", set.iterator().next()); + + NavigableSet ns = converter.convert("test").to(NavigableSet.class); + assertEquals("test", ns.iterator().next()); + + SortedSet ss = converter.convert("test").to(SortedSet.class); + assertEquals("test", ss.iterator().next()); + + Queue q = converter.convert("test").to(Queue.class); + assertEquals("test", q.iterator().next()); + + Deque dq = converter.convert("test").to(Deque.class); + assertEquals("test", dq.iterator().next()); + + Map m = converter.convert(Collections.singletonMap("x", "y")).to(Map.class); + assertEquals("y", m.get("x")); + + ConcurrentMap cm = converter.convert(Collections.singletonMap("x", "y")).to(ConcurrentMap.class); + assertEquals("y", cm.get("x")); + + ConcurrentNavigableMap cnm = converter.convert(Collections.singletonMap("x", "y")).to(ConcurrentNavigableMap.class); + assertEquals("y", cnm.get("x")); + + NavigableMap nm = converter.convert(Collections.singletonMap("x", "y")).to(NavigableMap.class); + assertEquals("y", nm.get("x")); + + SortedMap sm = converter.convert(Collections.singletonMap("x", "y")).to(SortedMap.class); + assertEquals("y", sm.get("x")); + } + + @SuppressWarnings("unchecked") + @Test + public void testLiveMapFromInterface() { + int[] val = new int[1]; + val[0] = 51; + + MyIntf intf = new MyIntf() { + @Override + public int value() { + return val[0]; + } + }; + + @SuppressWarnings("rawtypes") + Map m = converter.convert(intf).view().to(Map.class); + assertEquals(51, m.get("value")); + + val[0] = 52; + assertEquals("Changes to the backing map should be reflected", + 52, m.get("value")); + + m.put("value", 53); + assertEquals(53, m.get("value")); + + val[0] = 54; + assertEquals("Changes to the backing map should not be reflected any more", + 53, m.get("value")); + } + + @SuppressWarnings("unchecked") + @Test + public void testLiveMapFromDTO() { + MyDTO8 myDTO = new MyDTO8(); + + myDTO.count = MyDTO8.Count.TWO; + myDTO.pong = 42L; + + @SuppressWarnings("rawtypes") + Map m = converter.convert(myDTO).view().to(Map.class); + assertEquals(42L, m.get("pong")); + + myDTO.ping = "Ping!"; + assertEquals("Ping!", m.get("ping")); + myDTO.pong = 52L; + assertEquals(52L, m.get("pong")); + myDTO.ping = "Pong!"; + assertEquals("Pong!", m.get("ping")); + assertNull(m.get("nonexistant")); + + m.put("pong", 62L); + myDTO.ping = "Poing!"; + myDTO.pong = 72L; + assertEquals("Pong!", m.get("ping")); + assertEquals(62L, m.get("pong")); + assertNull(m.get("nonexistant")); + } + + @Test + public void testMapFromDTO() { + MyDTO9 dto = new MyDTO9(); + dto.key1 = "value1"; + dto.key2 = "value2"; + + Map m = converter.convert(dto).to(new TypeReference>() {}); + assertEquals(1, m.size()); + assertEquals('v', (char) m.get('k')); + + assertTrue(m.containsKey('k')); + assertFalse(m.containsKey("key1")); + assertTrue(m.containsValue('v')); + assertFalse(m.containsValue("value1")); + } + + @Test + public void testLiveMapFromDictionary() throws URISyntaxException { + URI testURI = new URI("http://foo"); + Hashtable d = new Hashtable<>(); + d.put("test", testURI); + + Map m = converter.convert(d).view().to(new TypeReference>(){}); + assertEquals(testURI, m.get("test")); + + URI testURI2 = new URI("http://bar"); + d.put("test2", testURI2); + assertEquals(testURI2, m.get("test2")); + assertEquals(testURI, m.get("test")); + } + + @Test + public void testLiveMapFromMap() { + Map s = new HashMap<>(); + + s.put("true", "123"); + s.put("false", "456"); + + Map m = converter.convert(s).view().to(new TypeReference>(){}); + assertEquals(Short.valueOf("123"), m.get(Boolean.TRUE)); + assertEquals(Short.valueOf("456"), m.get(Boolean.FALSE)); + + s.remove("true"); + assertNull(m.get(Boolean.TRUE)); + + s.put("TRUE", "999"); + assertEquals(Short.valueOf("999"), m.get(Boolean.TRUE)); + } + + @Test + public void testLiveMapFromBean() { + MyBean mb = new MyBean(); + mb.beanVal = "" + Long.MAX_VALUE; + + Map m = converter.convert(mb).sourceAsBean().view().to(new TypeReference>(){}); + assertEquals(1, m.size()); + assertEquals(Long.valueOf(Long.MAX_VALUE), m.get(SomeEnum.VALUE)); + + mb.beanVal = "" + Long.MIN_VALUE; + assertEquals(Long.valueOf(Long.MIN_VALUE), m.get(SomeEnum.VALUE)); + + m.put(SomeEnum.GETVALUE, 123L); + mb.beanVal = "12"; + assertEquals(Long.valueOf(Long.MIN_VALUE), m.get(SomeEnum.VALUE)); + } + + @Test + public void testPrefixDTO() { + Map m = new HashMap<>(); + m.put("org.foo.bar.width", "327"); + m.put("org.foo.bar.warp", "eeej"); + m.put("length", "12"); + + PrefixDTO dto = converter.convert(m).to(PrefixDTO.class); + assertEquals(327L, dto.width); + assertEquals("This one should not be set", 0, dto.length); + + Map m2 = converter.convert(dto).to(new TypeReference>() {}); + Map expected = new HashMap<>(); + expected.put("org.foo.bar.width", "327"); + expected.put("org.foo.bar.length", "0"); + assertEquals(expected, m2); + } + + @Test + public void testPrefixInterface() { + Map m = new HashMap<>(); + m.put("org.foo.bar.width", "327"); + m.put("org.foo.bar.warp", "eeej"); + m.put("length", "12"); + + PrefixInterface i = converter.convert(m).to(PrefixInterface.class); + assertEquals(327L, i.width()); + try { + i.length(); + fail("Should have thrown an exception"); + } catch (ConversionException ce) { + // good + } + + PrefixInterface i2 = new PrefixInterface() { + @Override + public long width() { + return Long.MAX_VALUE; + } + + @Override + public int length() { + return Integer.MIN_VALUE; + } + }; + + Map m2 = converter.convert(i2).to(new TypeReference>() {}); + Map expected = new HashMap<>(); + expected.put("org.foo.bar.width", "" + Long.MAX_VALUE); + expected.put("org.foo.bar.length", "" + Integer.MIN_VALUE); + assertEquals(expected, m2); + } + + @Test + public void testAnnotationInterface() { + Map m = new HashMap<>(); + m.put("org.foo.bar.width", "327"); + m.put("org.foo.bar.warp", "eeej"); + m.put("length", "12"); + + PrefixAnnotation pa = converter.convert(m).to(PrefixAnnotation.class); + assertEquals(327L, pa.width()); + assertEquals(51, pa.length()); + + Map m2 = converter.convert(pa).to(new TypeReference>() {}); + Map expected = new HashMap<>(); + expected.put("org.foo.bar.width", "327"); + expected.put("org.foo.bar.length", "51"); + assertEquals(expected, m2); + } + + @Test + public void testPrefixEnumAnnotation() { + PrefixEnumAnnotation pea = converter.convert(Collections.emptyMap()).to(PrefixEnumAnnotation.class); + + assertEquals(1000, pea.timeout()); + assertEquals(PrefixEnumAnnotation.Type.SINGLE, pea.type()); + + @SuppressWarnings("rawtypes") + Map m = converter.convert(pea).to(Map.class); + assertEquals(1000L, m.get("com.acme.config.timeout")); + assertEquals(PrefixEnumAnnotation.Type.SINGLE, m.get("com.acme.config.type")); + } + + @Test + public void testTargetAsString() { + Map m = new HashMap<>(); + CharSequence cs = converter.convert(m).targetAs(String.class).to(CharSequence.class); + assertNull(cs); + + Map m2 = new HashMap<>(); + m2.put("Hi", "there"); + CharSequence cs2 = converter.convert(m2).targetAs(String.class).to(CharSequence.class); + assertEquals("Hi", cs2); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void testTargetAsDTO() { + MyDTOWithMethods expected = new MyDTOWithMethods(); + expected.count = Count.ONE; + expected.ping = "pong"; + expected.pong = 42; + Map m = new HashMap<>(); + m.put("count", Count.ONE); + m.put("ping", "pong"); + m.put("pong", 42); + MyDTOWithMethods actual = converter.convert(m).targetAsDTO().to(MyDTOWithMethods.class); + assertEquals(expected.count, actual.count); + assertEquals(expected.ping, actual.ping); + assertEquals(expected.pong, actual.pong); + } + + @SuppressWarnings("rawtypes") + @Test + public void testLongArrayToLongCollection() { + Long[] la = new Long[] {Long.MIN_VALUE, Long.MAX_VALUE}; + + List lc = converter.convert(la).to(List.class); + + assertEquals(la.length, lc.size()); + + int i=0; + for (Iterator it = lc.iterator(); it.hasNext(); i++) { + assertEquals(la[i], it.next()); + } + } + + @Test + public void testMapToInterfaceWithGenerics() { + Map dto = new HashMap<>(); + dto.put("charSet", new HashSet<>(Arrays.asList("foo", (int) 'o', 'o'))); + + MyGenericInterface converted = converter.convert(dto).to(MyGenericInterface.class); + assertEquals(new HashSet(Arrays.asList('f', 'o')), converted.charSet()); + } + + @Test + public void testMapToInterfaceWithGenericVariables() { + Map dto = new HashMap<>(); + dto.put("set", new HashSet<>(Arrays.asList("foo", (int) 'o', 'o'))); + dto.put("raw", "1234"); + dto.put("array", Arrays.asList("foo", (int) 'o', 'o')); + + MyGenericInterfaceWithVariables converted = + converter.convert(dto).to(new TypeReference>() {}); + assertEquals(Character.valueOf('1'), converted.raw()); + assertArrayEquals(new Character[] {'f', 'o', 'o'}, converted.array()); + assertEquals(new HashSet(Arrays.asList('f', 'o')), converted.set()); + } + + @Test + public void testMapToInterfaceWithOptionalValue() throws Exception { + ConverterBuilder cb = Converters.newConverterBuilder(); + cb.errorHandler(new ConverterFunction() { + @Override + public Object apply(Object pObj, Type pTargetType) throws Exception { + if ("java.lang.Integer".equals(pTargetType.getTypeName())) + { + return 0; + } + return ConverterFunction.CANNOT_HANDLE; + } + }); + Converter convWithErrorHandler = cb.build(); + + Map map = new HashMap(); + map.put("code", "harley"); + MyIntf2 inter = convWithErrorHandler.convert(map).to(MyIntf2.class); + assertEquals("harley", inter.code()); + assertEquals(Integer.valueOf(0), inter.value()); + } + + static interface MyIntf2 { + String code(); + Integer value(); + } + + static class MyClass2 { + private final String value; + public MyClass2(String v) { + value = v; + } + + @Override + public String toString() { + return value; + } + } + + static interface MyIntf { + int value(); + } + + static class MyBean implements MyIntf { + int intfVal; + String beanVal; + + @Override + public int value() { + return intfVal; + } + + public String getValue() { + return beanVal; + } + } + + static class MyClass3 { + public int value; + public String string = "String"; + + public MyClass3( int value ) { + this.value = value; + } + + public int value() { + return value; + } + } + + static @interface MyAnnotation { + int value() default 17; + } + + enum SomeEnum { VALUE, GETVALUE }; +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/MyBean.java b/converter/converter/src/test/java/org/osgi/util/converter/MyBean.java new file mode 100644 index 00000000000..dd3660570a8 --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/MyBean.java @@ -0,0 +1,70 @@ +/* + * 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.osgi.util.converter; + +import java.util.Date; + +public class MyBean { + String me; + boolean enabled; + Boolean f; + int[] numbers; + Date startDate; + + public String get() { + return "Not a bean accessor because no camel casing"; + } + public String gettisburgh() { + return "Not a bean accessor because no camel casing"; + } + public int issue() { + return -1; // not a bean accessor as no camel casing + } + public void sets(String s) { + throw new RuntimeException("Not a bean accessor because no camel casing"); + } + public String getMe() { + return me; + } + public void setMe(String me) { + this.me = me; + } + public boolean isEnabled() { + return enabled; + } + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + public Boolean getF() { + return f; + } + public void setF(Boolean f) { + this.f = f; + } + public int[] getNumbers() { + return numbers; + } + public void setNumbers(int[] numbers) { + this.numbers = numbers; + } + public Date getStartDate() { + return startDate; + } + public void setStartDate(Date date) { + this.startDate = date; + } +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/MyDTO.java b/converter/converter/src/test/java/org/osgi/util/converter/MyDTO.java new file mode 100644 index 00000000000..912385cf112 --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/MyDTO.java @@ -0,0 +1,32 @@ +/* + * 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.osgi.util.converter; + +import org.osgi.dto.DTO; + +public class MyDTO extends DTO { + public enum Count { ONE, TWO, THREE } + + public Count count; + + public String ping; + + public long pong; + + public MyEmbeddedDTO embedded; +} + diff --git a/converter/converter/src/test/java/org/osgi/util/converter/MyDTO2.java b/converter/converter/src/test/java/org/osgi/util/converter/MyDTO2.java new file mode 100644 index 00000000000..9fac4391417 --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/MyDTO2.java @@ -0,0 +1,30 @@ +/* + * 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.osgi.util.converter; + +import java.util.List; +import java.util.Map; + +import org.osgi.dto.DTO; + +public class MyDTO2 extends DTO { + public static String shouldBeIgnored = "ignoreme"; + + public List longList; + + public Map dtoMap; +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/MyDTO3.java b/converter/converter/src/test/java/org/osgi/util/converter/MyDTO3.java new file mode 100644 index 00000000000..c17f858634f --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/MyDTO3.java @@ -0,0 +1,25 @@ +/* + * 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.osgi.util.converter; + +import java.util.Set; + +import org.osgi.dto.DTO; + +public class MyDTO3 extends DTO { + public Set charSet; +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/MyDTO4.java b/converter/converter/src/test/java/org/osgi/util/converter/MyDTO4.java new file mode 100644 index 00000000000..731b394adcd --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/MyDTO4.java @@ -0,0 +1,22 @@ +/* + * 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.osgi.util.converter; + +public class MyDTO4 { + public MyDTO5 sub1; + public MyDTO5 sub2; +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/MyDTO5.java b/converter/converter/src/test/java/org/osgi/util/converter/MyDTO5.java new file mode 100644 index 00000000000..859ac8a2049 --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/MyDTO5.java @@ -0,0 +1,22 @@ +/* + * 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.osgi.util.converter; + +public class MyDTO5 { + public MyDTO6 subsub1; + public MyDTO6 subsub2; +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/MyDTO6.java b/converter/converter/src/test/java/org/osgi/util/converter/MyDTO6.java new file mode 100644 index 00000000000..cb572bea15c --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/MyDTO6.java @@ -0,0 +1,23 @@ +/* + * 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.osgi.util.converter; + +import java.util.Collection; + +public class MyDTO6 { + public Collection chars; +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/MyDTO7.java b/converter/converter/src/test/java/org/osgi/util/converter/MyDTO7.java new file mode 100644 index 00000000000..17d5300dc2b --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/MyDTO7.java @@ -0,0 +1,31 @@ +/* + * 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.osgi.util.converter; + +public class MyDTO7 { + public String org_osgi_framework_uuid; + public boolean myProperty143; + public int my$$prop; + public Long dot_prop; + public char _secret; + public String another__prop; + public String three___prop; + public String four_$__prop; + public String five_$_prop; + public short six$_$prop; + public double seven$$_$prop; +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/MyDTO8.java b/converter/converter/src/test/java/org/osgi/util/converter/MyDTO8.java new file mode 100644 index 00000000000..80c9597af0f --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/MyDTO8.java @@ -0,0 +1,32 @@ +/* + * 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.osgi.util.converter; + +import org.osgi.dto.DTO; + +public class MyDTO8 extends DTO { + public enum Count { ONE, TWO, THREE } + + public Count count; + + public String ping; + + public long pong; + + public MyDTOWithMethods embedded; +} + diff --git a/converter/converter/src/test/java/org/osgi/util/converter/MyDTO9.java b/converter/converter/src/test/java/org/osgi/util/converter/MyDTO9.java new file mode 100644 index 00000000000..df9c8780f28 --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/MyDTO9.java @@ -0,0 +1,22 @@ +/* + * 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.osgi.util.converter; + +public class MyDTO9 { + public String key1; + public String key2; +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/MyDTOWithMethods.java b/converter/converter/src/test/java/org/osgi/util/converter/MyDTOWithMethods.java new file mode 100644 index 00000000000..03b498fecb8 --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/MyDTOWithMethods.java @@ -0,0 +1,23 @@ +/* + * 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.osgi.util.converter; + +public class MyDTOWithMethods extends MyDTO { + public int someMethod() { + return 123; + } +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/MyDefaultCtorDTOAlike.java b/converter/converter/src/test/java/org/osgi/util/converter/MyDefaultCtorDTOAlike.java new file mode 100644 index 00000000000..a1b46edff6c --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/MyDefaultCtorDTOAlike.java @@ -0,0 +1,27 @@ +/* + * 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.osgi.util.converter; + +public class MyDefaultCtorDTOAlike { + + MyDefaultCtorDTOAlike() { + // Deliberately default visibiilty + } + + public String myProp; +} + diff --git a/converter/converter/src/test/java/org/osgi/util/converter/MyEmbeddedDTO.java b/converter/converter/src/test/java/org/osgi/util/converter/MyEmbeddedDTO.java new file mode 100644 index 00000000000..d5108c4e283 --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/MyEmbeddedDTO.java @@ -0,0 +1,29 @@ +/* + * 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.osgi.util.converter; + +import org.osgi.dto.DTO; + +public class MyEmbeddedDTO extends DTO { + public enum Alpha { A, B, C } + + public Alpha alpha; + + public String marco; + + public long polo; +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/MyGenericDTOWithVariables.java b/converter/converter/src/test/java/org/osgi/util/converter/MyGenericDTOWithVariables.java new file mode 100644 index 00000000000..52ade1a36f0 --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/MyGenericDTOWithVariables.java @@ -0,0 +1,29 @@ +/* + * 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.osgi.util.converter; + +import java.util.Set; + +import org.osgi.dto.DTO; + +public class MyGenericDTOWithVariables extends DTO { + public Set set; + + public T raw; + + public T[] array; +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/MyGenericInterface.java b/converter/converter/src/test/java/org/osgi/util/converter/MyGenericInterface.java new file mode 100644 index 00000000000..0aa4fee7e27 --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/MyGenericInterface.java @@ -0,0 +1,23 @@ +/* + * 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.osgi.util.converter; + +import java.util.Set; + +public interface MyGenericInterface { + public Set charSet(); +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/MyGenericInterfaceWithVariables.java b/converter/converter/src/test/java/org/osgi/util/converter/MyGenericInterfaceWithVariables.java new file mode 100644 index 00000000000..cbbd8e1163c --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/MyGenericInterfaceWithVariables.java @@ -0,0 +1,27 @@ +/* + * 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.osgi.util.converter; + +import java.util.Set; + +public interface MyGenericInterfaceWithVariables { + public Set set(); + + public T raw(); + + public T[] array(); +} \ No newline at end of file diff --git a/converter/converter/src/test/java/org/osgi/util/converter/MySubDTO.java b/converter/converter/src/test/java/org/osgi/util/converter/MySubDTO.java new file mode 100644 index 00000000000..8628a5ccce4 --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/MySubDTO.java @@ -0,0 +1,21 @@ +/* + * 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.osgi.util.converter; + +public class MySubDTO extends MyDTO { + public String ping; +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/PrefixAnnotation.java b/converter/converter/src/test/java/org/osgi/util/converter/PrefixAnnotation.java new file mode 100644 index 00000000000..9fe18f55389 --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/PrefixAnnotation.java @@ -0,0 +1,25 @@ +/* + * 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.osgi.util.converter; + +public @interface PrefixAnnotation { + static final String PREFIX_ = "org.foo.bar."; + + long width() default 42L; + + int length() default 51; +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/PrefixDTO.java b/converter/converter/src/test/java/org/osgi/util/converter/PrefixDTO.java new file mode 100644 index 00000000000..f1aab793f11 --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/PrefixDTO.java @@ -0,0 +1,25 @@ +/* + * 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.osgi.util.converter; + +public class PrefixDTO { + public static final String PREFIX_ = "org.foo.bar."; + + public long width; + + public int length; +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/PrefixEnumAnnotation.java b/converter/converter/src/test/java/org/osgi/util/converter/PrefixEnumAnnotation.java new file mode 100644 index 00000000000..74fa0421b78 --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/PrefixEnumAnnotation.java @@ -0,0 +1,26 @@ +/* + * 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.osgi.util.converter; + +public @interface PrefixEnumAnnotation { + static final String PREFIX_ = "com.acme.config."; + + enum Type { SINGLE, MULTI }; + + long timeout() default 1000L; + Type type() default Type.SINGLE; +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/PrefixInterface.java b/converter/converter/src/test/java/org/osgi/util/converter/PrefixInterface.java new file mode 100644 index 00000000000..d4a0c6a612c --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/PrefixInterface.java @@ -0,0 +1,25 @@ +/* + * 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.osgi.util.converter; + +public interface PrefixInterface { + static final String PREFIX_ = "org.foo.bar."; + + long width(); + + int length(); +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/PrefixMarkerAnnotation.java b/converter/converter/src/test/java/org/osgi/util/converter/PrefixMarkerAnnotation.java new file mode 100644 index 00000000000..28cf46b867f --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/PrefixMarkerAnnotation.java @@ -0,0 +1,25 @@ +/* + * 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.osgi.util.converter; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface PrefixMarkerAnnotation { + static final String PREFIX_ = "org.foo.bar."; +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/TestDictionary.java b/converter/converter/src/test/java/org/osgi/util/converter/TestDictionary.java new file mode 100644 index 00000000000..f26675d5d3b --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/TestDictionary.java @@ -0,0 +1,64 @@ +/* + * 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.osgi.util.converter; + +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; + +/** This test Dictionary does not implement Map. It is used to test cases + * where a Dictionary is needed and one that does not implement map needs + * to be tested. + */ +public class TestDictionary extends Dictionary { + private Hashtable delegate = new Hashtable<>(); + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public Enumeration keys() { + return delegate.keys(); + } + + @Override + public Enumeration elements() { + return delegate.elements(); + } + + @Override + public V get(Object key) { + return delegate.get(key); + } + + @Override + public V put(K key, V value) { + return delegate.put(key, value); + } + + @Override + public V remove(Object key) { + return delegate.remove(key); + } +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/UtilTest.java b/converter/converter/src/test/java/org/osgi/util/converter/UtilTest.java new file mode 100644 index 00000000000..b4650480991 --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/UtilTest.java @@ -0,0 +1,62 @@ +/* + * 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.osgi.util.converter; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class UtilTest { + @Test + public void testMangling() { + assertMangle("", ""); + assertMangle("a", "a"); + assertMangle("ab", "ab"); + assertMangle("abc", "abc"); + assertMangle("a\u0008bc", "a\bbc"); + + assertMangle("$_$", "-"); + assertMangle("$_", "."); + assertMangle("_$", "."); + assertMangle("x$_$", "x-"); + assertMangle("$_$x", "-x"); + assertMangle("abc$_$abc", "abc-abc"); + assertMangle("$$_$x", "$.x"); + assertMangle("$_$$", "-"); + assertMangle("$_$$$", "-$"); + assertMangle("$", ""); + assertMangle("$$", "$"); + assertMangle("_", "."); + assertMangle("$_", "."); + + assertMangle("myProperty143", "myProperty143"); + assertMangle("$new", "new"); + assertMangle("n$ew", "new"); + assertMangle("new$", "new"); + assertMangle("my$$prop", "my$prop"); + assertMangle("dot_prop", "dot.prop"); + assertMangle("_secret", ".secret"); + assertMangle("another__prop", "another_prop"); + assertMangle("three___prop", "three_.prop"); + assertMangle("four_$__prop", "four._prop"); + assertMangle("five_$_prop", "five..prop"); + } + + private void assertMangle(String methodName, String key) { + assertEquals(Util.unMangleName(methodName), key); + } +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/sub1/TestAnn1.java b/converter/converter/src/test/java/org/osgi/util/converter/sub1/TestAnn1.java new file mode 100644 index 00000000000..67c80cfa8c2 --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/sub1/TestAnn1.java @@ -0,0 +1,23 @@ +/* + * 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.osgi.util.converter.sub1; + +@interface TestAnn1 { + String foo() default "fooo!"; + int bar() default 42; + boolean za_za(); +} diff --git a/converter/converter/src/test/java/org/osgi/util/converter/sub2/TestAnn2.java b/converter/converter/src/test/java/org/osgi/util/converter/sub2/TestAnn2.java new file mode 100644 index 00000000000..3eafb84264e --- /dev/null +++ b/converter/converter/src/test/java/org/osgi/util/converter/sub2/TestAnn2.java @@ -0,0 +1,23 @@ +/* + * 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.osgi.util.converter.sub2; + +@interface TestAnn2 { + String foo(); + int bar() default 42; + boolean tar(); +} diff --git a/converter/pom.xml b/converter/pom.xml new file mode 100644 index 00000000000..584accc7128 --- /dev/null +++ b/converter/pom.xml @@ -0,0 +1,45 @@ + + + + 4.0.0 + + org.apache.felix + felix-parent + 4 + ../pom/pom.xml + + + Apache Felix Converter Reactor + org.apache.felix.converter.reactor + 0.1-SNAPSHOT + pom + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/converter + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/converter + http://svn.apache.org/viewvc/felix/trunk/converter/ + + + + converter + serializer + schematizer + + diff --git a/converter/readme.md b/converter/readme.md new file mode 100644 index 00000000000..7b00f7727a1 --- /dev/null +++ b/converter/readme.md @@ -0,0 +1,28 @@ +# Apache Converter + +## Overview + +This is the home for the OSGi R7-compliant Converter. It is an implementation +of the Converter Specification. For more details, see Chapter 707 of the +OSGi Compendium. + +There are two other sister projects: Serializer and Schematizer. + +## Serializer +The Serializer, based heavily on the Converter, is useful for transforming a +serialized string of text to an object, and vice-versa. Please refer to the +project for more details. + + +## Schematizer +Once data is serialized, until you can identity the type of object associated +with the serialized data, it is difficult to guess which object the data should +be serialized to. + +The Schematizer, based on the Converter and the Serializer, is useful in cases +where serialized data needs to contain meta data about the type of object +serialized. Using the Schematizer, it is possible to serialize different data +types to the same stream, then upon deserialization, by reading the meta data +determine the object type to be used. + +Please refer to the project for more details. diff --git a/converter/schematizer/pom.xml b/converter/schematizer/pom.xml new file mode 100644 index 00000000000..d6b9ea2a27a --- /dev/null +++ b/converter/schematizer/pom.xml @@ -0,0 +1,163 @@ + + + + 4.0.0 + + org.apache.felix + felix-parent + 4 + ../pom/pom.xml + + + Apache Felix Schematizer Service + org.apache.felix.schematizer + 0.3.0-SNAPSHOT + jar + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/converter/schematizer + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/converter/schematizer + http://svn.apache.org/viewvc/felix/trunk/converter/schematizer/ + + + + 8 + java18 + + + + + + org.apache.felix + maven-bundle-plugin + 3.2.0 + + + bundle + package + + bundle + + + + baseline + + baseline + + + + + + org.apache.felix.schematizer.impl.Activator + + org.apache.felix.schematizer.*, + org.apache.felix.serializer.impl.*, + org.yaml.snakeyaml.*, + org.apache.felix.utils.* + + + org.apache.felix.schematizer; + uses:="org.osgi.util.converter,org.osgi.util.function,org.osgi.dto,org.osgi.framework" + + + org.osgi.util.converter, + org.apache.felix.serializer, + * + + + osgi.service;objectClass:List<String>="org.apache.felix.schematizer.Schematizer,org.apache.felix.serializer.Serializer"; + uses:="org.apache.felix.schematizer,org.apache.serializer,org.osgi.util.converter,org.osgi.util.function" + + <_sources>true + + + + + org.apache.rat + apache-rat-plugin + + + verify + + check + + + + + + src/** + + + src/main/resources/META-INF/services/org.apache.felix.schematizer.Schematizer + + + + + + + + + + org.apache.felix + org.apache.felix.converter + ${project.version} + + + + org.apache.felix + org.apache.felix.serializer + ${project.version} + + + + org.osgi + osgi.annotation + 6.0.1 + provided + + + + org.osgi + osgi.core + 6.0.0 + provided + + + + org.apache.felix + org.apache.felix.utils + 1.9.1-SNAPSHOT + provided + + + + junit + junit + test + + + + org.apache.sling + org.apache.sling.commons.json + 2.0.16 + test + + + diff --git a/converter/schematizer/readme.txt b/converter/schematizer/readme.txt new file mode 100644 index 00000000000..052a507088c --- /dev/null +++ b/converter/schematizer/readme.txt @@ -0,0 +1,71 @@ +# Apache Felix Converter - Schematizer module + +## Overview + +The Schematizer follows the concept of "DTO-as-Schema", meaning the idea that +the DTO describes the data schema, and using this idea to make the schema a +first-class citizen in the design and implementation of a domain model. + +## DTO-as-Schema + +DTO-as-Schema (DaS) takes a step away from common Object Oriented (OO) design principles. +When learning OO programming, common convention was to "hide away" the data in +order to "protect" it from the wild. Instead of accessing a field directly, the +idea was to make a field private, and provide "getters" and "setters". The getters +and setters were supposed to ensure the invariants of the object. Often, however, +we would end up with code like this: + +```java +public class SomeClass { + private String value; + + public String getValue() { + return value; + } + + public void setValue( String aValue ) { + value = aValue; + } +} +``` + +The above is really just a complicated and misleading way of doing this: + +```java +public class SomeClass { + public String value; +} +``` + +Even when OO-style classes are well written, it can be argued that the idea of data-hiding +is a farce anyway when dealing with distributed systems. The reason is because the classes +need to be serialized before they are put on the wire, and deserialzed again by the remote system. +This requires exposing the system in the form of an "API", these days usually as a REST API. +So, when the system is seen as a whole, we recognize that it is simply not possible to have +a working complex system while "hiding" the core data. + +DaS is based on this admission. We admit that there are really *two* interfaces: +a *programmatic API* and a *data API*. + +In the [WHICH?] OSGi specification, DTOs were introduced as a convention for describing objects +and transferring their state between system sub-parts. It so happens that the rules for DTOs +describe a schema, in Java code, for the data objects being transferred. By taking advantage of this +schema and elevating it as a first-class citizen during the design and implementation of domain +objects, we can elegantly expose both the programmatic API and the data API in code, and reap a few +other benefits as well, as described below. + +Building also on other ideas, notably some of the ideas emerging from functional programming, +it is possible to develop domain models with a leaner--and thus more productive--code base. + +# STATUS + +This module is highly experimental and is *not* recommended for production. + +# Coding conventions + +[TODO] + +# Topics to Explore + +## Schema transforms +## Lenses \ No newline at end of file diff --git a/converter/schematizer/src/main/java/org/apache/felix/schematizer/AsDTO.java b/converter/schematizer/src/main/java/org/apache/felix/schematizer/AsDTO.java new file mode 100644 index 00000000000..611294eedbf --- /dev/null +++ b/converter/schematizer/src/main/java/org/apache/felix/schematizer/AsDTO.java @@ -0,0 +1,27 @@ +/* + * 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.schematizer; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface AsDTO { +} diff --git a/converter/schematizer/src/main/java/org/apache/felix/schematizer/Node.java b/converter/schematizer/src/main/java/org/apache/felix/schematizer/Node.java new file mode 100644 index 00000000000..41d95965204 --- /dev/null +++ b/converter/schematizer/src/main/java/org/apache/felix/schematizer/Node.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) OSGi Alliance (2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.schematizer; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.osgi.util.converter.TypeReference; + +public interface Node { + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public static @interface CollectionType { + Class value() default Object.class; + } + + static class DTO extends org.osgi.dto.DTO { + public String name; + public String path; + public String type; + public String collectionType; + public boolean isCollection; + public Map children = new HashMap<>(); + } + + String name(); + /** + * Return the absolute path of this Node relative to the root Node. + */ + String absolutePath(); + Type type(); + Field field(); + Optional> typeReference(); + boolean isCollection(); + Map children(); + Class> collectionType(); + + static Node ERROR = new Node() { + @Override public String name() { return "ERROR"; } + @Override public String absolutePath() { return "ERROR"; } + @Override public Type type() { return Object.class; } + @Override public Field field() { return null; } + @Override public Optional> typeReference() { return Optional.empty(); } + @Override public boolean isCollection() { return false; } + @Override public Map children() { return Collections.emptyMap(); } + @Override public Class> collectionType() { return null; } + }; +} diff --git a/converter/schematizer/src/main/java/org/apache/felix/schematizer/NodeVisitor.java b/converter/schematizer/src/main/java/org/apache/felix/schematizer/NodeVisitor.java new file mode 100644 index 00000000000..add0b1ee232 --- /dev/null +++ b/converter/schematizer/src/main/java/org/apache/felix/schematizer/NodeVisitor.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) OSGi Alliance (2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.schematizer; + +@FunctionalInterface +public interface NodeVisitor{ + void apply(Node node); +} diff --git a/converter/schematizer/src/main/java/org/apache/felix/schematizer/Schema.java b/converter/schematizer/src/main/java/org/apache/felix/schematizer/Schema.java new file mode 100644 index 00000000000..0e0f35b321f --- /dev/null +++ b/converter/schematizer/src/main/java/org/apache/felix/schematizer/Schema.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) OSGi Alliance (2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.schematizer; + +import java.util.Collection; +import java.util.Map; + +public interface Schema { + String name(); + Node rootNode(); + boolean hasNodeAtPath(String absolutePath); + Node nodeAtPath(String absolutePath); + Node parentOf(Node aNode); + Map toMap(); + + /** + * Recursively visits all nodes in the {@code Schema} for processing. + */ + void visit(NodeVisitor visitor); + + Collection valuesAt(String path, Object object); +} diff --git a/converter/schematizer/src/main/java/org/apache/felix/schematizer/Schematizer.java b/converter/schematizer/src/main/java/org/apache/felix/schematizer/Schematizer.java new file mode 100644 index 00000000000..b5efee838c4 --- /dev/null +++ b/converter/schematizer/src/main/java/org/apache/felix/schematizer/Schematizer.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) OSGi Alliance (2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.schematizer; + +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.util.converter.Converter; +import org.osgi.util.converter.TypeReference; + +@ProviderType +public interface Schematizer { + /** + * Can be a class or a TypeReference. + * + * TODO: Consider splitting into two separate methods. + */ + Schematizer schematize(String schemaName, Object type); + + Schema get(String schemaName); + + /** + * Associates a type rule. Useful for classes that have type parameters, which + * get lost at runtime. + * + * @param name the name of the Schema to which this rule is to be associated + * @param path the path in the object graph where the rule gets applied + * @param type the type + */ + Schematizer type(String name, String path, TypeReference type); + + /** + * Associates a type rule. Useful for classes that have type parameters, which + * get lost at runtime. + * + * @param name the name of the Schema to which this rule is to be associated + * @param path the path in the object graph where the rule gets applied + * @param type the type + */ + Schematizer type(String name, String path, Class type); + + /** + * Returns a Converter for the Schema corresponding to the given name. + * The Schema must already have been schematized using the given name. + */ + Converter converterFor(String schemaName); + +// /** +// * Returns a Converter for the provided Schema. +// */ +// Converter converterFor(Schema s); +} diff --git a/converter/schematizer/src/main/java/org/apache/felix/schematizer/StandardSchematizer.java b/converter/schematizer/src/main/java/org/apache/felix/schematizer/StandardSchematizer.java new file mode 100644 index 00000000000..e4cf8bffdd2 --- /dev/null +++ b/converter/schematizer/src/main/java/org/apache/felix/schematizer/StandardSchematizer.java @@ -0,0 +1,54 @@ +/* + * 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.schematizer; + +import org.apache.felix.schematizer.impl.SchematizerImpl; +import org.osgi.util.converter.Converter; +import org.osgi.util.converter.TypeReference; + +public class StandardSchematizer implements Schematizer { + private final Schematizer schematizer; + + public StandardSchematizer() { + schematizer = new SchematizerImpl(); + } + + @Override + public Schematizer schematize(String schemaName, Object type) { + return schematizer.schematize(schemaName, type); + } + + @Override + public Schema get(String schemaName) { + return schematizer.get(schemaName); + } + + @Override + public Schematizer type(String name, String path, TypeReference type) { + return schematizer.type(name, path, type); + } + + @Override + public Schematizer type(String name, String path, Class type) { + return schematizer.type(name, path, type); + } + + @Override + public Converter converterFor(String schemaName) { + return schematizer.converterFor(schemaName); + } +} diff --git a/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/Activator.java b/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/Activator.java new file mode 100644 index 00000000000..9e09ca33421 --- /dev/null +++ b/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/Activator.java @@ -0,0 +1,45 @@ +/* + * 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.schematizer.impl; + +import java.util.Dictionary; +import java.util.Hashtable; + +import org.apache.felix.schematizer.Schematizer; +import org.apache.felix.serializer.Serializer; +import org.apache.felix.serializer.impl.json.JsonSerializerImpl; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceFactory; + +public class Activator implements BundleActivator { + @Override + public void start(BundleContext context) throws Exception { + Dictionary jsonProps = new Hashtable<>(); + jsonProps.put("mimetype", new String[] { + "application/json", "application/x-javascript", "text/javascript", + "text/x-javascript", "text/x-json" }); + jsonProps.put("provider", "felix"); + context.registerService(Serializer.class, new JsonSerializerImpl(), jsonProps); + + context.registerService(Schematizer.class, (ServiceFactory)new SchematizerImpl(), null); + } + + @Override + public void stop(BundleContext context) throws Exception { + } +} diff --git a/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/CollectionNode.java b/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/CollectionNode.java new file mode 100644 index 00000000000..78f2d0fdef4 --- /dev/null +++ b/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/CollectionNode.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) OSGi Alliance (2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.schematizer.impl; + +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Map; +import java.util.function.Function; + +import org.apache.felix.schematizer.Node; + +public class CollectionNode + extends NodeImpl +{ + private final Class> collectionType; + + public CollectionNode( + String aName, + Object aType, + String anAbsolutePath, + Class> aCollectionType ) { + super( aName, aType, true, anAbsolutePath ); + collectionType = aCollectionType; + } + + public CollectionNode( + Node.DTO dto, + String contextPath, + Function f, + Map nodes, + Class> aCollectionType ) { + super(dto, contextPath, f, nodes); + collectionType = aCollectionType; + } + + @Override + public Class> collectionType() { + return collectionType; + } + + @Override + public DTO toDTO() { + DTO dto = super.toDTO(); + dto.collectionType = collectionType.getName(); + return dto; + } +} diff --git a/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/NodeImpl.java b/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/NodeImpl.java new file mode 100644 index 00000000000..311a9036cd4 --- /dev/null +++ b/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/NodeImpl.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) OSGi Alliance (2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.schematizer.impl; + +import java.lang.reflect.Field; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import org.apache.felix.schematizer.Node; +import org.osgi.util.converter.TypeReference; + +public class NodeImpl implements Node { + + private final String name; + private final Object type; + private final boolean isCollection; + private final String absolutePath; + + private NodeImpl parent; + private HashMap children = new HashMap<>(); + private Field field; + + public NodeImpl( + String aName, + Object aType, + boolean isACollection, + String anAbsolutePath ) { + name = aName; + type = aType; + isCollection = isACollection; + absolutePath = anAbsolutePath; + } + + @SuppressWarnings( { "unchecked", "rawtypes" } ) + public NodeImpl(Node.DTO dto, String contextPath, Function f, Map nodes) { + name = dto.name; + type = f.apply(dto.type); + isCollection = dto.isCollection; + absolutePath = contextPath + dto.path; + for (Node.DTO child : dto.children.values()) { + NodeImpl node; + if (child.isCollection) + try { + node = new CollectionNode(child, contextPath, f, nodes, (Class)getClass().getClassLoader().loadClass(child.collectionType)); + } catch ( ClassNotFoundException e ) { + node = new CollectionNode(child, contextPath, f, nodes, (Class)Collection.class); + } + else + node = new NodeImpl(child, contextPath, f, nodes); + children.put("/" + child.name, node); + nodes.put(child.path, node); + } + } + + @Override + public String name() { + return name; + } + + @Override + public Type type() { + if (type instanceof TypeReference) + return ((TypeReference)type).getType(); + return (Type)type; + } + + @Override + public Optional> typeReference() { + if (type instanceof TypeReference) + return Optional.of((TypeReference)type); + return Optional.empty(); + } + + @Override + public boolean isCollection() { + return isCollection; + } + + @Override + public String absolutePath() { + return absolutePath; + } + + @Override + public Field field() { + return field; + } + + public void field(Field aField) { + field = aField; + } + + @SuppressWarnings( { "unchecked", "rawtypes" } ) + @Override + public Map children() { + return (Map)childrenInternal(); + } + + @Override + public Class> collectionType() { + return null; + } + + @SuppressWarnings( { "unchecked", "rawtypes" } ) + Map childrenInternal() { + return (Map)children.clone(); + } + + NodeImpl parent() { + return parent; + } + + void parent(NodeImpl aParent) { + parent = aParent; + } + + void add(NodeImpl child) { + children.put(child.absolutePath, child); + } + + void add(Map moreChildren) { + children.putAll(moreChildren); + } + + public Node.DTO toDTO() { + Node.DTO dto = new Node.DTO(); + dto.name = name(); + dto.path = absolutePath(); + dto.type = type().getTypeName(); + dto.isCollection = isCollection(); + childrenInternal().values().stream().forEach(v -> dto.children.put(v.name, v.toDTO())); + return dto; + } + + @Override + public String toString() { + return absolutePath; + } +} diff --git a/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/SchemaBasedConverter.java b/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/SchemaBasedConverter.java new file mode 100644 index 00000000000..65bcdf15a2e --- /dev/null +++ b/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/SchemaBasedConverter.java @@ -0,0 +1,206 @@ +/* + * 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.schematizer.impl; + +import java.lang.reflect.Field; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.apache.felix.schematizer.Node; +import org.apache.felix.schematizer.Schema; +import org.osgi.dto.DTO; +import org.osgi.util.converter.ConversionException; +import org.osgi.util.converter.Converter; +import org.osgi.util.converter.ConverterFunction; +import org.osgi.util.converter.Converters; +import org.osgi.util.converter.TargetRule; +import org.osgi.util.converter.TypeReference; + +import static org.apache.felix.schematizer.impl.Util.asDTO; +import static org.apache.felix.schematizer.impl.Util.rawClassOf; + +public class SchemaBasedConverter implements TargetRule { + private final SchemaImpl schema; + private final Converter converter; + + public SchemaBasedConverter(SchemaImpl aSchema) { + schema = aSchema; + converter = Converters.standardConverter(); + // TODO: how can we add the error handler?? +// .newConverterBuilder() +// .errorHandler( (obj,type) -> new Exception( "Could not convert object " + obj.toString() + " of type " + type.getTypeName() ) ) +// .build(); + } + + @Override + public ConverterFunction getFunction() { + return (obj,t) -> { + if (!(obj instanceof Map) || schema == null) + return handleInvalid(); + return convertMap((Map)obj, schema, "/"); + }; + } + + @Override + public Type getTargetType() { + return schema.rootNode().type(); + } + + @SuppressWarnings( "unchecked" ) + private T convertMap(Map map, Schema s, String contextPath) { + Node node = s.nodeAtPath(contextPath); + Class cls = Util.rawClassOf(node.type()); + + if (!asDTO(cls)) + return handleInvalid(); + + if (!contextPath.endsWith("/")) + contextPath = contextPath + "/"; + + return (T)convertToDTO((Class)cls, map, s, contextPath); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private U convertToDTO(Class targetCls, Map m, Schema schema, String contextPath) { + try { + U dto; + + try { + dto = targetCls.newInstance(); + } catch (Throwable t) { + throw new ConversionException("Cannot create instance of DTO " + targetCls + ". Bad constructor?", t); + } + + for (Map.Entry entry : m.entrySet()) { + try { + Field f = targetCls.getField(entry.getKey().toString()); + Object val = entry.getValue(); + if (val == null) + continue; + String path = contextPath + f.getName(); + Node node = schema.nodeAtPath(path); + Object obj; + if (node.typeReference().isPresent()) { + TypeReference tr = Util.typeReferenceOf(node.typeReference().get()); + if (node.isCollection()) + if (!Collection.class.isAssignableFrom(val.getClass())) + // TODO: PANIC! Something is wrong... what should we do?? + obj = null; + else + obj = convertToCollection( (Class)Util.rawClassOf(tr), (Class)node.collectionType(), (Collection)val, schema, path); + else + obj = convertToDTO((Class)rawClassOf(tr), (Map)val, schema, path + "/"); + } else { + if (node.isCollection()) { + Collection c = instantiateCollection(node.collectionType()); + Type type = node.type(); + for (Object o : (Collection)val) { + if (o == null) + c.add(null); + else if (asDTO(rawClassOf(type))) + c.add(convertToDTO((Class)Util.rawClassOf(type), (Map)o, schema, path + "/")); + else + c.add(converter.convert(o).to(type)); + } + obj = c; + } else { + Class rawClass = rawClassOf(node.type()); + if (asDTO(rawClass)) + obj = convertToDTO((Class)rawClass, (Map)val, schema, path + "/"); + else + obj = converter.convert(val).to(node.type()); + } + } + + f.set(dto, obj); + } catch (NoSuchFieldException e) { + } + } + + return dto; + } catch (Exception e) { + throw new ConversionException("Cannot create DTO " + targetCls, e); + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private >V convertToCollection(Class targetCls, Class collectionClass, Collection sourceCollection, Schema schema, String path) { + try { + V targetCollection = instantiateCollection(collectionClass); + sourceCollection.stream() + .map(obj -> convertCollectionItemToObject(obj, targetCls, schema, path)) + .forEach(u -> targetCollection.add((U)u)); + + return targetCollection; + } catch (Exception e) { + throw new ConversionException("Cannot create DTO " + targetCls, e); + } + } + + @SuppressWarnings( "unchecked" ) + private >V instantiateCollection(Class collectionClass) { + if (collectionClass == null) + return (V)new ArrayList(); + if (Collection.class.equals(collectionClass) || List.class.isAssignableFrom(collectionClass)) + return (V)new ArrayList(); + else + // TODO: incomplete + return null; + } + + private U convertCollectionItemToObject(Object obj, Class targetCls, Schema schema, String path) { + try + { + if (asDTO(targetCls)) + return convertCollectionItemToDTO(obj, targetCls, schema, path); + + U newItem = targetCls.newInstance(); + return newItem; + } + catch ( Exception e ) + { + e.printStackTrace(); + return null; + } + } + + @SuppressWarnings( "unchecked" ) + private U convertCollectionItemToDTO(Object obj, Class targetCls, Schema schema, String path) { + Node node = schema.nodeAtPath(path); + if (node.typeReference().isPresent()) { + TypeReference tr = (TypeReference)Util.typeReferenceOf(node.typeReference().get()); + return converter.convert(obj).to(tr); + } else { + Type type = node.type(); + type.toString(); + // TODO +// if (DTO.class.isAssignableFrom(Util.rawClassOf(type))) +// obj = convertToDTO((Class)Util.rawClassOf(type), (Map)val, schema, path + "/" ); +// else +// obj = converter.convert(val).to(type); + return null; + } + } + + // TODO + private T handleInvalid() { + return null; + } +} diff --git a/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/SchemaImpl.java b/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/SchemaImpl.java new file mode 100644 index 00000000000..31e6f15037c --- /dev/null +++ b/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/SchemaImpl.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) OSGi Alliance (2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.schematizer.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.apache.felix.schematizer.Node; +import org.apache.felix.schematizer.NodeVisitor; +import org.apache.felix.schematizer.Schema; +import org.osgi.util.converter.Converter; +import org.osgi.util.converter.Converters; + +public class SchemaImpl implements Schema { + private final String name; + private final HashMap nodes = new LinkedHashMap<>(); + + public SchemaImpl(String aName) { + name = aName; + } + + @Override + public String name() { + return name; + } + + @Override + public Node rootNode() { + return rootNodeInternal(); + } + + public NodeImpl rootNodeInternal() { + return nodes.get("/"); + } + + @Override + public boolean hasNodeAtPath(String absolutePath) { + return nodes.containsKey(absolutePath); + } + + @Override + public Node nodeAtPath( String absolutePath ) { + return nodes.get(absolutePath); + } + + @Override + public Node parentOf( Node aNode ) { + if (aNode == null || aNode.absolutePath() == null) + return Node.ERROR; + + NodeImpl node = nodes.get(aNode.absolutePath()); + if (node == null) + return Node.ERROR; + + return node.parent(); + } + + void add(NodeImpl node) { + nodes.put(node.absolutePath(), node); + } + + void add(Map moreNodes) { + nodes.putAll(moreNodes); + } + + @Override + public Map toMap() { + NodeImpl root = nodes.get("/"); + Map m = new HashMap<>(); + m.put("/",root.toDTO()); + return m; + } + + @SuppressWarnings( { "unchecked", "rawtypes" } ) + Map toMapInternal() { + return (Map)nodes.clone(); + } + + @Override + public void visit(NodeVisitor visitor) { + nodes.values().stream().forEach(n -> visitor.apply(n)); + } + + @Override + public Collection valuesAt(String path, Object object) { + final Converter converter = Converters.standardConverter(); + @SuppressWarnings( "unchecked" ) + final Map map = (Map)converter.convert(object).sourceAsDTO().to( Map.class ); + if (map == null || map.isEmpty()) + return Collections.emptyList(); + + if (path.startsWith("/")) + path = path.substring(1); + String[] pathParts = path.split("/"); + if (pathParts.length <= 0) + return Collections.emptyList(); + + List contexts = Arrays.asList(pathParts); + + return valuesAt("", map, contexts, 0); + } + + @SuppressWarnings( { "rawtypes", "unchecked" } ) + private Collection valuesAt(String context, Map objectMap, List contexts, int currentIndex) { + List result = new ArrayList<>(); + String currentContext = contexts.get(currentIndex); + if (objectMap == null) + return result; + Object o = objectMap.get(currentContext); + if (o instanceof List) { + List l = (List)o; + if (currentIndex == contexts.size() - 1) { + // We are at the end, so just add the collection + result.add(convertToType(pathFrom(contexts, 0), l)); + return result; + } + + currentContext = pathFrom(contexts, ++currentIndex); + for (Object o2 : l) + { + final Converter converter = Converters.standardConverter(); + final Map m = (Map)converter.convert(o2).sourceAsDTO().to( Map.class ); + result.addAll( valuesAt( currentContext, m, contexts, currentIndex ) ); + } + } else if (o instanceof Map){ + if (currentIndex == contexts.size() - 1) { + // We are at the end, so just add the result + result.add(convertToType(pathFrom(contexts, 0), (Map)o)); + return result; + } + + result.addAll(valuesAt( currentContext, (Map)o, contexts, ++currentIndex)); + } else if (currentIndex < contexts.size() - 1) { + final Converter converter = Converters.standardConverter(); + final Map m = (Map)converter.convert(o).sourceAsDTO().to(Map.class); + currentContext = pathFrom(contexts, ++currentIndex); + result.addAll(valuesAt( currentContext, m, contexts, currentIndex )); + } else { + result.add(o); + } + + return result; + } + + @SuppressWarnings( "rawtypes" ) + private Object convertToType( String path, Map map ) { + if (!hasNodeAtPath(path)) + return map; + + Node node = nodeAtPath(path); + Object result = Converters.standardConverter().convert(map).targetAsDTO().to(node.type()); + return result; + } + + private List convertToType( String path, List list ) { + if (!hasNodeAtPath(path)) + return list; + + Node node = nodeAtPath(path); + return list.stream() + .map( v -> Converters.standardConverter().convert(v).sourceAsDTO().to(node.type())) + .collect( Collectors.toList() ); + } + + private String pathFrom(List contexts, int index) { + return IntStream.range(0, contexts.size()) + .filter( i -> i >= index ) + .mapToObj( i -> contexts.get(i) ) + .reduce( "", (s1,s2) -> s1 + "/" + s2 ); + + } +} \ No newline at end of file diff --git a/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/SchematizerImpl.java b/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/SchematizerImpl.java new file mode 100644 index 00000000000..1cb336aaf6b --- /dev/null +++ b/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/SchematizerImpl.java @@ -0,0 +1,428 @@ +/* + * 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.schematizer.impl; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +import org.apache.felix.schematizer.Schematizer; +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceRegistration; +import org.osgi.util.converter.Converter; +import org.osgi.util.converter.Converters; +import org.osgi.util.converter.TypeReference; + +import static org.apache.felix.schematizer.impl.Util.*; + +public class SchematizerImpl implements Schematizer, ServiceFactory { + private final Map schemas = new HashMap<>(); + private final Map> typeRules = new HashMap<>(); + + @Override + public Schematizer getService( Bundle bundle, ServiceRegistration registration ) { + return this; + } + + @Override + public void ungetService(Bundle bundle, ServiceRegistration registration, Schematizer service) { + // For now, a brutish, simplistic version. If there is any change to the environment, just + // wipe the state and start over. + // + // TODO: something more precise, which will remove only the classes that are no longer valid (if that is possible). + schemas.clear(); + } + + @Override + public Schematizer type(String schemaName, String path, TypeReference type) { + Map rules = rulesFor(schemaName); + // The internal implementation uses "" as the path for the root, + // but the API accepts "/". + path = "/".equals( path ) ? "" : path; + rules.put(path, type); + return this; + } + + @Override + public Schematizer type(String schemaName, String path, Class cls) { + Map rules = rulesFor(schemaName); + // The internal implementation uses "" as the path for the root, + // but the API accepts "/". + path = "/".equals( path ) ? "" : path; + rules.put(path, cls); + return this; + } + + private Map rulesFor(String schemaName) { + if (!typeRules.containsKey(schemaName)) + typeRules.put(schemaName, new HashMap<>()); + + return typeRules.get(schemaName); + } + + @Override + public SchematizerImpl schematize(String schemaName, Object type) { + return schematize(schemaName, type, ""); + } + + public SchematizerImpl schematize(String schemaName, Object type, String context) { + // TODO: test to ensure that the schema is not already in the cache + Map rules = typeRules.get(schemaName); + rules = ( rules != null ) ? rules : Collections.emptyMap(); + SchemaImpl schema = internalSchematize(schemaName, type, context, rules, false); + schemas.put(schemaName, schema); + return this; + } + + private static SchemaImpl internalSchematize( + String schemaName, + Object unknownType, + String contextPath, + Map rules, + boolean isCollection) { + + TypeRefOrClass type = new TypeRefOrClass(unknownType, rules.get(contextPath)); + + if (asDTO(type.cls)) { + return schematizeDTO(schemaName, type, contextPath, rules, isCollection); + } + + return schematizeObject(schemaName, type.cls, contextPath, isCollection); + } + + private static SchemaImpl schematizeDTO( + String schemaName, + TypeRefOrClass type, + String contextPath, + Map rules, + boolean isCollection ) { + + SchemaImpl schema = new SchemaImpl(schemaName); + NodeImpl rootNode = new NodeImpl(contextPath, type.isTypeRef() ? type.typeRef : type.cls, false, contextPath + "/"); + schema.add(rootNode); + Map m = createMapFromDTO(schemaName, type, rules, contextPath); + m.values().stream() + .filter(v -> v.absolutePath().equals(rootNode.absolutePath() + v.name())) + .forEach(v -> rootNode.add(v)); + associateChildNodes( rootNode ); + schema.add(m); + return schema; + } + + private static SchemaImpl schematizeObject( + String schemaName, + Class targetCls, + String contextPath, + boolean isCollection) { + + SchemaImpl schema = new SchemaImpl(schemaName); + NodeImpl node = new NodeImpl(contextPath, targetCls, isCollection, contextPath + "/"); + schema.add(node); + return schema; + } + + private static final Comparator> byPath = (e1, e2) -> e1.getValue().absolutePath().compareTo(e2.getValue().absolutePath()); + private static Map createMapFromDTO( + String schemaName, + TypeRefOrClass type, + Map rules, + String contextPath) { + Set handledFields = new HashSet<>(); + + Map result = new HashMap<>(); + for (Field f : type.cls.getDeclaredFields()) { + handleField(schemaName, f, rules, handledFields, result, contextPath); + } + for (Field f : type.cls.getFields()) { + handleField(schemaName, f, rules, handledFields, result, contextPath); + } + + return result.entrySet().stream() + .sorted(byPath) + .collect(Collectors.toMap( + Entry::getKey, + Entry::getValue, + (e1, e2) -> e1, + LinkedHashMap::new)); + } + + @SuppressWarnings( { "unchecked", "rawtypes" } ) + private static void handleField( + String schemaName, + Field field, + Map rules, + Set handledFields, + Map result, + String contextPath) { + if (Modifier.isStatic(field.getModifiers())) + return; + + String fieldName = field.getName(); + if (handledFields.contains(fieldName)) + return; // Field with this name was already handled + + try { + String path = contextPath + "/" + fieldName; + NodeImpl node; + if (rules.containsKey(path)) { + // The actual field. Since the type for this node is provided as a rule, we + // only need it to test whether or not it is a collection. + Class actualFieldType = field.getType(); + boolean isCollection = Collection.class.isAssignableFrom(actualFieldType); + Class ruleBasedClass = rawClassOf(rules.get(path)); + // This is the type we will persist in the Schema (as provided by the rules), NOT the "actual" field type. + SchemaImpl embedded = SchematizerImpl.internalSchematize(schemaName, ruleBasedClass, path, rules, isCollection); + Class fieldClass = Util.primitiveToBoxed(ruleBasedClass); + TypeRefOrClass fieldType = new TypeRefOrClass(fieldClass,rules.get(path)); + if (isCollection) + node = new CollectionNode( + field.getName(), + fieldType.get(), + path, + (Class)actualFieldType); + else + node = new NodeImpl(fieldName, fieldType.get(), false, path); + Map allNodes = embedded.toMapInternal(); + allNodes.remove(path + "/"); + result.putAll(allNodes); + Map childNodes = extractChildren(path, allNodes); + node.add(childNodes); + } else { + Type fieldType = field.getType(); + Class rawClass = rawClassOf(fieldType); + Class fieldClass = primitiveToBoxed(rawClass); + + if (isCollectionType(fieldClass)) { + Class collectionType = getCollectionTypeOf(field); + node = new CollectionNode( + field.getName(), + collectionType, + path, + (Class)fieldClass); + + if (asDTO(collectionType)) { +// newSchematizer.typeRules.put(path, rules); +// if (!rules.containsKey(path)) +// newSchematizer.rule(path, path, collectionType); + SchemaImpl embedded = new SchematizerImpl().schematize(path, collectionType, path).get(path); + Map allNodes = embedded.toMapInternal(); + allNodes.remove(path + "/"); + result.putAll(allNodes); + Map childNodes = extractChildren(path, allNodes); + node.add(childNodes); + } + } + else if (asDTO(fieldClass) || Util.isDTOType(fieldClass)) { +// newSchematizer.typeRules.put(path, rules); +// if (!rules.containsKey(path)) +// newSchematizer.rule(path, path, fieldClass); + SchemaImpl embedded = new SchematizerImpl().schematize(path, fieldClass, path).get(path); + node = new NodeImpl( + field.getName(), + fieldClass, + false, + path); + Map allNodes = embedded.toMapInternal(); + allNodes.remove(path + "/"); + result.putAll(allNodes); + Map childNodes = extractChildren(path, allNodes); + node.add(childNodes); + } else { + node = new NodeImpl( + field.getName(), + fieldClass, + false, + path); + } + } + + result.put(node.absolutePath(), node); + handledFields.add(fieldName); + } catch (Exception e) { + // Ignore this field + // TODO print warning?? + return; + } + } + + static private void associateChildNodes(NodeImpl rootNode) { + for (NodeImpl child: rootNode.childrenInternal().values()) { + child.parent(rootNode); + String fieldName = child.name(); + Class parentClass = rawClassOf(rootNode.type()); + try { + Field field = parentClass.getField(fieldName); + child.field(field); + } catch ( NoSuchFieldException e ) { + e.printStackTrace(); + } + + associateChildNodes(child); + } + } + + @Override + public SchemaImpl get(String schemaName) { + return schemas.get(schemaName); + } + + @Override + public Converter converterFor(String schemaName) { +// ConverterBuilder b = new StandardConverter().newConverterBuilder(); +// Schema s = schemas.get(schemaName); +// RuleExtractor ex = new RuleExtractor(); +// s.visit( ex ); +// ex.rules().stream().forEach( rule -> b.rule(rule) ); +// return b.build(); + return Converters + .newConverterBuilder() + .rule(new SchemaBasedConverter(schemas.get(schemaName))) + .build(); + } + +// private static class RuleExtractor implements NodeVisitor { +// private final List> rules = new ArrayList<>(); +// +// @Override +// public void apply(Node node) { +// rules.add(new DTOTargetRule(node)); +// } +// +// List> rules() { +// return rules; +// } +// } +// private static class DTOTargetRule implements TargetRule { +// private final Type type; +// +// public DTOTargetRule(Node node) { +// if (node.isCollection()) +// type = new CollectionType(node.collectionType(), new TypeRefOrClass(node.type())); +// else +// type = node.type(); +// } +// +// @Override +// public ConverterFunction getFunction() { +// return (obj,t) -> { +// TypeRefOrClass type = null; +// if(t instanceof CollectionType) { +// return convertCollection((Collection)obj, (CollectionType)t); +// } else { +// type = new TypeRefOrClass(t); +// return convertObject(obj,type); +// } +// }; +// } +// +// @SuppressWarnings( "unchecked" ) +// private T convertCollection(Collection c, CollectionType type) { +// Collection copy = newCollection(type); +// for(Object obj : c) +// copy.add((Object)convertObject(obj, type.itemType)); +// return (T)copy; +// } +// +// private Collection newCollection(CollectionType type) { +// // TODO what else? +// return new ArrayList<>(); +// } +// +// @SuppressWarnings( "unchecked" ) +// private T convertObject(Object obj, TypeRefOrClass type) { +// Converter c = new StandardConverter(); +// if (asDTO(type.getClassType())) +// if(type.isTypeRef()) +// return c.convert(obj).targetAsDTO().to((TypeReference)type.getTypeRef()); +// else +// return c.convert(obj).targetAsDTO().to(type.getType()); +// return c.convert(obj).targetAsDTO().to(type.getType()); +// } +// +// @Override +// public Type getTargetType() { +// if (type instanceof CollectionType) +// return ((CollectionType)type).collectionType; +// return type; +// } +// }; + + static class TypeRefOrClass { + TypeReference typeRef; + Class cls; + + public TypeRefOrClass(Object type, Object ruleBasedType) { + this(ruleBasedType != null ? ruleBasedType : type); + } + + public TypeRefOrClass(Object type) { + typeRef = (TypeReference)(typeReferenceOf(type)); + if (typeRef != null ) + cls = rawClassOf(typeRef); + else + cls = rawClassOf(type); + } + + boolean isTypeRef() { + return typeRef != null; + } + + TypeReference getTypeRef() { + return typeRef; + } + + Object get() { + if (typeRef != null ) + return typeRef; + return cls; + } + + Type getType() { + if (typeRef != null) + return typeRef.getType(); + return cls; + } + + Class getClassType() { + Type t = getType(); + if (t instanceof Class) + return (Class)t; + return t.getClass(); + } + } + + static class CollectionType implements Type { + Class> collectionType; + TypeRefOrClass itemType; + + public CollectionType(Class> aCollectionType, TypeRefOrClass anItemType) { + collectionType = aCollectionType; + itemType = anItemType; + } + } +} \ No newline at end of file diff --git a/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/SchematizerImplOLD.java b/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/SchematizerImplOLD.java new file mode 100644 index 00000000000..f0906b12a7a --- /dev/null +++ b/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/SchematizerImplOLD.java @@ -0,0 +1,477 @@ +/* + * 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.schematizer.impl; + +//import java.lang.annotation.Annotation; +//import java.lang.reflect.Field; +//import java.lang.reflect.Method; +//import java.lang.reflect.Modifier; +//import java.lang.reflect.ParameterizedType; +//import java.lang.reflect.Type; +//import java.util.ArrayList; +//import java.util.Arrays; +//import java.util.Collection; +//import java.util.Collections; +//import java.util.Comparator; +//import java.util.HashMap; +//import java.util.HashSet; +//import java.util.LinkedHashMap; +//import java.util.List; +//import java.util.Map; +//import java.util.Map.Entry; +//import java.util.Optional; +//import java.util.Set; +//import java.util.function.Function; +//import java.util.stream.Collectors; +// +//import org.apache.felix.schematizer.Node; +//import org.apache.felix.schematizer.Node.CollectionType; +//import org.apache.felix.schematizer.Schema; +//import org.apache.felix.schematizer.Schematizer; +//import org.apache.felix.schematizer.TypeRule; +//import org.osgi.dto.DTO; +//import org.osgi.framework.Bundle; +//import org.osgi.framework.ServiceFactory; +//import org.osgi.framework.ServiceRegistration; +//import org.osgi.util.converter.StandardConverter; +//import org.osgi.util.converter.TypeReference; +// +//public class SchematizerImplOLD implements Schematizer, ServiceFactory { +// +// private final Map schemas = new HashMap<>(); +// private volatile Map> typeRules = new HashMap<>(); +// private final List classloaders = new ArrayList<>(); +// +// @Override +// public Schematizer getService( Bundle bundle, ServiceRegistration registration ) { +// return this; +// } +// +// @Override +// public void ungetService(Bundle bundle, ServiceRegistration registration, Schematizer service) { +// // For now, a brutish, simplistic version. If there is any change to the environment, just +// // wipe the state and start over. +// // +// // TODO: something more precise, which will remove only the classes that are no longer valid (if that is possible). +// schemas.clear(); +// typeRules.clear(); +// classloaders.clear(); +// } +// +// @Override +// public Optional get(String name) { +// if (!schemas.containsKey(name)) { +// SchemaImpl schema = schematize(name, ""); +// schemas.put(name, schema); +// } +// +// return Optional.ofNullable(schemas.get(name)); +// } +// +// @Override +// public Optional from(String name, Map map) { +// try { +// // TODO: some validation of the Map here would be good +// SchemaImpl schema = new SchemaImpl(name); +// Object rootMap = map.get("/"); +// Node.DTO rootDTO = new StandardConverter().convert(rootMap).to(Node.DTO.class); +// Map allNodes = new HashMap<>(); +// NodeImpl root = new NodeImpl(rootDTO, "", new Instantiator(classloaders), allNodes); +// associateChildNodes(root); +// schema.add(root); +// schema.add(allNodes); +// return Optional.of(schema); +// } catch (Throwable t) { +// return Optional.empty(); +// } +// } +// +// @Override +// public Schematizer rule(String name, TypeRule rule) { +// Map rules = rulesFor(name); +// rules.put(rule.getPath(), rule.getType()); +// return this; +// } +// +// @Override +// public Schematizer rule(String name, String path, TypeReference type) { +// Map rules = rulesFor(name); +// rules.put(path, type); +// return this; +// } +// +// @Override +// public Schematizer rule(String name, TypeReference type) { +// Map rules = rulesFor(name); +// rules.put("/", type); +// return this; +// } +// +// @Override +// public Schematizer rule(String name, String path, Class cls) { +// Map rules = rulesFor(name); +// rules.put(path, cls); +// return this; +// } +// +// private Map rulesFor(String name) { +// if (!typeRules.containsKey(name)) +// typeRules.put(name, new HashMap<>()); +// +// return typeRules.get(name); +// } +// +// @Override +// public Schematizer usingLookup( ClassLoader classloader ) { +// if (classloader != null) +// classloaders.add(classloader); +// return this; +// } +// +// /** +// * Top-level entry point for schematizing a DTO. This is the starting point to set up the +// * parsing. All other methods make recursive calls. +// */ +// private SchemaImpl schematize(String name, String contextPath) { +// Map rules = typeRules.get(name); +// rules = ( rules != null ) ? rules : Collections.emptyMap(); +// return SchematizerImplOLD.internalSchematize(name, contextPath, rules, false, this); +// } +// +// @SuppressWarnings( { "unchecked", "rawtypes" } ) +// /** +// * Schematize any node, without knowing in advance its type. +// */ +// private static SchemaImpl internalSchematize( +// String name, +// String contextPath, +// Map rules, +// boolean isCollection, +// SchematizerImplOLD schematizer) { +// Class cls = null; +// TypeReference ref = null; +// if (contextPath.isEmpty() && rules.containsKey("/")) { +// ref = (TypeReference)typeReferenceOf(rules.get("/")); +// if (ref == null ) +// cls = rawClassOf(rules.get("/")); +// } +// +// if (rules.containsKey(contextPath)) { +// ref = (TypeReference)(typeReferenceOf(rules.get(contextPath))); +// if (ref == null ) +// cls = rawClassOf(rules.get(contextPath)); +// } +// +// if (ref != null ) +// cls = rawClassOf(ref); +// +// if (cls == null) +// return handleInvalid(); +// +// if (DTO.class.isAssignableFrom(cls)) { +// Class targetCls = (Class)cls; +// return schematizeDTO(name, targetCls, ref, contextPath, rules, isCollection, schematizer); +// } +// +// return schematizeObject( name, cls, contextPath, rules, isCollection, schematizer); +// } +// +// private static SchemaImpl schematizeDTO( +// String name, +// Class targetCls, +// TypeReference ref, +// String contextPath, +// Map rules, +// boolean isCollection, +// SchematizerImplOLD schematizer) { +// +// SchemaImpl schema = new SchemaImpl(name); +// NodeImpl rootNode; +// if (ref != null) +// rootNode = new NodeImpl(contextPath, ref, false, contextPath + "/"); +// else +// rootNode = new NodeImpl(contextPath, targetCls, false, contextPath + "/"); +// schema.add(rootNode); +// Map m = createMapFromDTO(name, targetCls, ref, contextPath, rules, schematizer); +// m.values().stream().filter(v -> v.absolutePath().equals(rootNode.absolutePath() + v.name())).forEach(v -> rootNode.add(v)); +// associateChildNodes( rootNode ); +// schema.add(m); +// return schema; +// } +// +// private static SchemaImpl schematizeObject( +// String name, +// Class targetCls, +// String contextPath, +// Map rules, +// boolean isCollection, +// SchematizerImplOLD schematizer) { +// +// SchemaImpl schema = new SchemaImpl(name); +// NodeImpl node = new NodeImpl(contextPath, targetCls, isCollection, contextPath + "/"); +// schema.add(node); +// return schema; +// } +// +// private static final Comparator> byPath = (e1, e2) -> e1.getValue().absolutePath().compareTo(e2.getValue().absolutePath()); +// private static Map createMapFromDTO( +// String name, +// Class targetCls, +// TypeReference ref, +// String contextPath, +// Map typeRules, +// SchematizerImplOLD schematizer) { +// Set handledFields = new HashSet<>(); +// +// Map result = new HashMap<>(); +// for (Field f : targetCls.getDeclaredFields()) { +// handleField(name, f, handledFields, result, targetCls, ref, contextPath, typeRules, schematizer); +// } +// for (Field f : targetCls.getFields()) { +// handleField(name, f, handledFields, result, targetCls, ref, contextPath, typeRules, schematizer); +// } +// +// return result.entrySet().stream().sorted(byPath).collect(Collectors.toMap(Entry::getKey, Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); +// } +// +// @SuppressWarnings( { "rawtypes", "unchecked" } ) +// private static void handleField( +// String name, +// Field field, +// Set handledFields, +// Map result, +// Class targetCls, +// TypeReference ref, +// String contextPath, +// Map rules, +// SchematizerImplOLD schematizer) { +// if (Modifier.isStatic(field.getModifiers())) +// return; +// +// String fieldName = field.getName(); +// if (handledFields.contains(fieldName)) +// return; // Field with this name was already handled +// +// try { +// String path = contextPath + "/" + fieldName; +// NodeImpl node; +// if (rules.containsKey(path)) { +// // The actual field. Since the type for this node is provided as a rule, we +// // only need it to test whether or not it is a collection. +// Class actualFieldType = field.getType(); +// boolean isCollection = Collection.class.isAssignableFrom(actualFieldType); +// Class rawClass = rawClassOf(rules.get(path)); +// // This is the type we will persist in the Schema (as provided by the rules), NOT the "actual" field type. +// SchemaImpl embedded = SchematizerImplOLD.internalSchematize(name, path, rules, isCollection, schematizer); +// Class fieldClass = Util.primitiveToBoxed(rawClass); +// TypeReference fieldRef = typeReferenceOf(rules.get(path)); +// if (isCollection) +// node = new CollectionNode( +// field.getName(), +// fieldRef, +// path, +// (Class)actualFieldType); +// else if (fieldRef != null ) +// node = new NodeImpl(fieldName, fieldRef, false, path); +// else +// node = new NodeImpl(fieldName, fieldClass, false, path); +// Map allNodes = embedded.toMapInternal(); +// allNodes.remove(path + "/"); +// result.putAll(allNodes); +// Map childNodes = extractChildren(path, allNodes); +// node.add(childNodes); +// } else { +// Type fieldType = field.getType(); +// Class rawClass = rawClassOf(fieldType); +// Class fieldClass = Util.primitiveToBoxed(rawClass); +// +// if (Collection.class.isAssignableFrom(fieldClass)) { +// CollectionType collectionTypeAnnotation = field.getAnnotation( CollectionType.class ); +// Class collectionType; +// if (collectionTypeAnnotation != null) +// collectionType = collectionTypeAnnotation.value(); +// else if (hasCollectionTypeAnnotation(field)) +// collectionType = collectionTypeOf(field); +// else +// collectionType = Object.class; +// node = new CollectionNode( +// field.getName(), +// collectionType, +// path, +// (Class)fieldClass); +// +// if (DTO.class.isAssignableFrom(collectionType)) { +// SchematizerImplOLD newSchematizer = new SchematizerImplOLD(); +// newSchematizer.typeRules.put(path, rules); +// if (!rules.containsKey(path)) +// newSchematizer.rule(path, path, collectionType); +// SchemaImpl embedded = newSchematizer.schematize(path, path); +// Map allNodes = embedded.toMapInternal(); +// allNodes.remove(path + "/"); +// result.putAll(allNodes); +// Map childNodes = extractChildren(path, allNodes); +// node.add(childNodes); +// } +// } +// else if (DTO.class.isAssignableFrom(fieldClass)) { +// SchematizerImplOLD newSchematizer = new SchematizerImplOLD(); +// newSchematizer.typeRules.put(path, rules); +// if (!rules.containsKey(path)) +// newSchematizer.rule(path, path, fieldClass); +// SchemaImpl embedded = newSchematizer.schematize(path, path); +// node = new NodeImpl( +// field.getName(), +// fieldClass, +// false, +// path); +// Map allNodes = embedded.toMapInternal(); +// allNodes.remove(path + "/"); +// result.putAll(allNodes); +// Map childNodes = extractChildren(path, allNodes); +// node.add(childNodes); +// } else { +// node = new NodeImpl( +// field.getName(), +// fieldClass, +// false, +// path); +// } +// } +// +// result.put(node.absolutePath(), node); +// handledFields.add(fieldName); +// } catch (Exception e) { +// // Ignore this field +// // TODO print warning?? +// return; +// } +// } +// +// private static Map extractChildren( String path, Map allNodes ) { +// final Map children = new HashMap<>(); +// for (String key : allNodes.keySet()) { +// String newKey = key.replace(path, ""); +// if (!newKey.substring(1).contains("/")) +// children.put( newKey, allNodes.get(key)); +// } +// +// return children; +// } +// +// private static SchemaImpl handleInvalid() { +// // TODO +// return null; +// } +// +// private static Class rawClassOf(Object type) { +// Class rawClass = null; +// if (type instanceof Class) { +// rawClass = (Class)type; +// } else if (type instanceof ParameterizedType) { +// ParameterizedType paramType = (ParameterizedType) type; +// Type rawType = paramType.getRawType(); +// if (rawType instanceof Class) +// rawClass = (Class)rawType; +// } else if (type instanceof TypeReference) { +// return rawClassOf(((TypeReference)type).getType()); +// } +// +// return rawClass; +// } +// +// private static TypeReference typeReferenceOf(Object type) { +// TypeReference typeRef = null; +// if (type instanceof TypeReference) +// typeRef = (TypeReference)type; +// return typeRef; +// } +// +// public static class Instantiator implements Function { +// private final List classloaders = new ArrayList<>(); +// +// public Instantiator(List aClassLoadersList) { +// classloaders.addAll( aClassLoadersList ); +// } +// +// @Override +// public Type apply(String className) { +// for (ClassLoader cl : classloaders) { +// try { +// return cl.loadClass(className); +// } catch (ClassNotFoundException e) { +// // Try next +// } +// } +// +// // Could not find the class. Try "this" ClassLoader +// try { +// return getClass().getClassLoader().loadClass(className); +// } catch (ClassNotFoundException e) { +// // Too bad +// } +// +// // Nothing to do. Return Object.class as the fallback +// return Object.class; +// } +// } +// +// static private void associateChildNodes(NodeImpl rootNode) { +// for (NodeImpl child: rootNode.childrenInternal().values()) { +// child.parent(rootNode); +// String fieldName = child.name(); +// Class parentClass = rawClassOf(rootNode.type()); +// try { +// Field field = parentClass.getField(fieldName); +// child.field(field); +// } catch ( NoSuchFieldException e ) { +// e.printStackTrace(); +// } +// +// associateChildNodes(child); +// } +// } +// +// static private boolean hasCollectionTypeAnnotation(Field field) { +// if (field == null) +// return false; +// +// Annotation[] annotations = field.getAnnotations(); +// if (annotations.length == 0) +// return false; +// +// return Arrays.stream(annotations) +// .map(a -> a.annotationType().getName()) +// .anyMatch(a -> "CollectionType".equals(a.substring(a.lastIndexOf(".") + 1) )); +// } +// +// static private Class collectionTypeOf(Field field) { +// Annotation[] annotations = field.getAnnotations(); +// +// Annotation annotation = Arrays.stream(annotations) +// .filter(a -> "CollectionType".equals(a.annotationType().getName().substring(a.annotationType().getName().lastIndexOf(".") + 1) )) +// .findFirst() +// .get(); +// +// try { +// Method m = annotation.annotationType().getMethod("value"); +// Class value = (Class)m.invoke(annotation, (Object[])null); +// return value; +// } catch ( Exception e ) { +// return null; +// } +// } +//} diff --git a/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/Util.java b/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/Util.java new file mode 100644 index 00000000000..c6101e65758 --- /dev/null +++ b/converter/schematizer/src/main/java/org/apache/felix/schematizer/impl/Util.java @@ -0,0 +1,235 @@ +/* + * 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.schematizer.impl; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.schematizer.AsDTO; +import org.apache.felix.schematizer.Node.CollectionType; +import org.osgi.util.converter.TypeReference; + +public class Util { + private static final Map, Class> boxedClasses; + static { + Map, Class> m = new HashMap<>(); + m.put(int.class, Integer.class); + m.put(long.class, Long.class); + m.put(double.class, Double.class); + m.put(float.class, Float.class); + m.put(boolean.class, Boolean.class); + m.put(char.class, Character.class); + m.put(byte.class, Byte.class); + m.put(void.class, Void.class); + m.put(short.class, Short.class); + boxedClasses = Collections.unmodifiableMap(m); + } + + public static Type primitiveToBoxed(Type type) { + if (type instanceof Class) + return primitiveToBoxed((Class) type); + else + return null; + } + + public static Class primitiveToBoxed(Class cls) { + Class boxed = boxedClasses.get(cls); + if (boxed != null) + return boxed; + else + return cls; + } + + public static byte [] readStream(InputStream is) throws IOException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] bytes = new byte[8192]; + + int length = 0; + int offset = 0; + + while ((length = is.read(bytes, offset, bytes.length - offset)) != -1) { + offset += length; + + if (offset == bytes.length) { + baos.write(bytes, 0, bytes.length); + offset = 0; + } + } + if (offset != 0) { + baos.write(bytes, 0, offset); + } + return baos.toByteArray(); + } finally { + is.close(); + } + } + + public static Class rawClassOf(Object type) { + Class rawClass = null; + if (type instanceof Class) { + rawClass = (Class)type; + } else if (type instanceof ParameterizedType) { + ParameterizedType paramType = (ParameterizedType) type; + Type rawType = paramType.getRawType(); + if (rawType instanceof Class) + rawClass = (Class)rawType; + } else if (type instanceof TypeReference) { + return rawClassOf(((TypeReference)type).getType()); + } + + return rawClass; + } + + public static TypeReference typeReferenceOf(Object type) { + TypeReference typeRef = null; + if (type instanceof TypeReference) + typeRef = (TypeReference)type; + return typeRef; + } + + public static boolean isDTOType(Class cls) { + try { + cls.getDeclaredConstructor(); + } catch (NoSuchMethodException | SecurityException e) { + // No zero-arg constructor, not a DTO + return false; + } + + // ATTENTION!! (Note from David Leangen) + // This may not be according to spec, but without this, it is not possible + // to use streams in the constructor, which I think is not intended. + if (cls.getDeclaredMethods().length > 0) { + return Arrays.stream(cls.getDeclaredMethods()) + .map(m -> m.getName()) + .allMatch(n -> n.startsWith( "lambda$")); + } + + for (Method m : cls.getMethods()) { + try { + Object.class.getMethod(m.getName(), m.getParameterTypes()); + } catch (NoSuchMethodException snme) { + // Not a method defined by Object.class (or override of such method) + return false; + } + } + + for (Field f : cls.getDeclaredFields()) { + int modifiers = f.getModifiers(); + if (Modifier.isStatic(modifiers)) { + // ignore static fields + continue; + } + + if (!Modifier.isPublic(modifiers)) { + return false; + } + } + + for (Field f : cls.getFields()) { + int modifiers = f.getModifiers(); + if (Modifier.isStatic(modifiers)) { + // ignore static fields + continue; + } + + if (!Modifier.isPublic(modifiers)) { + return false; + } + } + return true; + } + + public static boolean hasDTOAnnotation(Class clazz) { + AsDTO asDTOAnnotation = clazz.getAnnotation(AsDTO.class); + return asDTOAnnotation != null; + } + + public static boolean asDTO(Class clazz) { + return hasDTOAnnotation(clazz) || isDTOType(clazz); + } + + public static boolean hasCollectionTypeAnnotation(Field field) { + if (field == null) + return false; + + Annotation[] annotations = field.getAnnotations(); + if (annotations.length == 0) + return false; + + return Arrays.stream(annotations) + .map(a -> a.annotationType().getName()) + .anyMatch(a -> "CollectionType".equals(a.substring(a.lastIndexOf(".") + 1) )); + } + + public static Class collectionTypeOf(Field field) { + Annotation[] annotations = field.getAnnotations(); + + Annotation annotation = Arrays.stream(annotations) + .filter(a -> "CollectionType".equals(a.annotationType().getName().substring(a.annotationType().getName().lastIndexOf(".") + 1) )) + .findFirst() + .get(); + + try { + Method m = annotation.annotationType().getMethod("value"); + Class value = (Class)m.invoke(annotation, (Object[])null); + return value; + } catch ( Exception e ) { + return null; + } + } + + public static Class getCollectionTypeOf(Field field) { + Class collectionType; + CollectionType collectionTypeAnnotation = field.getAnnotation(CollectionType.class); + if (collectionTypeAnnotation != null) + collectionType = collectionTypeAnnotation.value(); + else if (hasCollectionTypeAnnotation(field)) + collectionType = collectionTypeOf(field); + else + collectionType = Object.class; + + return collectionType; + } + + public static boolean isCollectionType(Class clazz) { + return Collection.class.isAssignableFrom(clazz); + } + + public static Map extractChildren(String path, Map allNodes) { + final Map children = new HashMap<>(); + for (String key : allNodes.keySet()) { + String newKey = key.replaceFirst(path, ""); + if (!newKey.substring(1).contains("/")) + children.put( newKey, allNodes.get(key)); + } + + return children; + } +} diff --git a/converter/schematizer/src/main/java/org/apache/felix/schematizer/package-info.java b/converter/schematizer/src/main/java/org/apache/felix/schematizer/package-info.java new file mode 100644 index 00000000000..b0c787ed662 --- /dev/null +++ b/converter/schematizer/src/main/java/org/apache/felix/schematizer/package-info.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) OSGi Alliance (2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Schematizer Package Version 1.0. + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. This package has two types of + * users: the consumers that use the API in this package and the providers that + * implement the API in this package. + *

      + * Example import for consumers using the API in this package: + *

      + * {@code Import-Package: org.osgi.service.converter; version="[1.0,2.0)"} + *

      + * Example import for providers implementing the API in this package: + *

      + * {@code Import-Package: org.osgi.service.converter; version="[1.0,1.1)"} + * + * @author $Id$ + */ +@Version("1.0") +package org.apache.felix.schematizer; + +import org.osgi.annotation.versioning.Version; diff --git a/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/CollectionType.java b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/CollectionType.java new file mode 100644 index 00000000000..f1e12db7f65 --- /dev/null +++ b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/CollectionType.java @@ -0,0 +1,28 @@ +/* + * 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.schematizer.impl; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface CollectionType { + Class value() default Object.class; +} diff --git a/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyBean.java b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyBean.java new file mode 100644 index 00000000000..defadf6c095 --- /dev/null +++ b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyBean.java @@ -0,0 +1,61 @@ +/* + * 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.schematizer.impl; + +public class MyBean { + String me; + boolean enabled; + Boolean f; + int[] numbers; + + public String get() { + return "Not a bean accessor because no camel casing"; + } + public String gettisburgh() { + return "Not a bean accessor because no camel casing"; + } + public int issue() { + return -1; // not a bean accessor as no camel casing + } + public void sets(String s) { + throw new RuntimeException("Not a bean accessor because no camel casing"); + } + public String getMe() { + return me; + } + public void setMe(String me) { + this.me = me; + } + public boolean isEnabled() { + return enabled; + } + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + public Boolean getF() { + return f; + } + public void setF(Boolean f) { + this.f = f; + } + public int[] getNumbers() { + return numbers; + } + public void setNumbers(int[] numbers) { + this.numbers = numbers; + } +} diff --git a/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyDTO.java b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyDTO.java new file mode 100644 index 00000000000..ffe7d24a7f5 --- /dev/null +++ b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyDTO.java @@ -0,0 +1,34 @@ +/* + * 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.schematizer.impl; + +import org.apache.felix.schematizer.AsDTO; +import org.osgi.dto.DTO; + +@AsDTO +public class MyDTO extends DTO { + public enum Count { ONE, TWO, THREE } + + public Count count; + + public String ping; + + public long pong; + + public MyEmbeddedDTO embedded; +} + diff --git a/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyDTO2.java b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyDTO2.java new file mode 100644 index 00000000000..ab6ba6e8dd7 --- /dev/null +++ b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyDTO2.java @@ -0,0 +1,26 @@ +/* + * 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.schematizer.impl; + +import org.osgi.dto.DTO; + +public class MyDTO2 extends DTO { + public T1 value; + + public T2 embedded; +} + diff --git a/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyDTO3.java b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyDTO3.java new file mode 100644 index 00000000000..42d653780c3 --- /dev/null +++ b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyDTO3.java @@ -0,0 +1,34 @@ +/* + * 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.schematizer.impl; + +import java.util.List; + +import org.osgi.dto.DTO; + +public class MyDTO3 extends DTO { + public enum Count { ONE, TWO, THREE } + + public Count count; + + public String ping; + + public long pong; + + public List embedded; +} + diff --git a/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyDTO4.java b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyDTO4.java new file mode 100644 index 00000000000..69ee8519e9f --- /dev/null +++ b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyDTO4.java @@ -0,0 +1,35 @@ +/* + * 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.schematizer.impl; + +import java.util.List; + +import org.osgi.dto.DTO; + +public class MyDTO4 extends DTO { + public enum Count { ONE, TWO, THREE } + + public Count count; + + public String ping; + + public long pong; + + @org.apache.felix.schematizer.impl.CollectionType(MyEmbeddedDTO.class) + public List embedded; +} + diff --git a/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyEmbeddedDTO.java b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyEmbeddedDTO.java new file mode 100644 index 00000000000..ad82e98bf27 --- /dev/null +++ b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyEmbeddedDTO.java @@ -0,0 +1,31 @@ +/* + * 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.schematizer.impl; + +import org.apache.felix.schematizer.AsDTO; +import org.osgi.dto.DTO; + +@AsDTO +public class MyEmbeddedDTO extends DTO { + public enum Alpha { A, B, C } + + public Alpha alpha; + + public String marco; + + public long polo; +} diff --git a/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyEmbeddedDTO2.java b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyEmbeddedDTO2.java new file mode 100644 index 00000000000..f60d98fdd2a --- /dev/null +++ b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MyEmbeddedDTO2.java @@ -0,0 +1,26 @@ +/* + * 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.schematizer.impl; + +import org.apache.felix.schematizer.AsDTO; +import org.osgi.dto.DTO; + +@AsDTO +public class MyEmbeddedDTO2 extends DTO { + public T value; +} + diff --git a/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MySubDTO.java b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MySubDTO.java new file mode 100644 index 00000000000..76b017e8ec8 --- /dev/null +++ b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/MySubDTO.java @@ -0,0 +1,21 @@ +/* + * 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.schematizer.impl; + +public class MySubDTO extends MyDTO { + public String ping; +} diff --git a/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/SchemaTest.java b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/SchemaTest.java new file mode 100644 index 00000000000..e785e604a0f --- /dev/null +++ b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/SchemaTest.java @@ -0,0 +1,159 @@ +/* + * 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.schematizer.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.felix.schematizer.Schema; +import org.apache.felix.schematizer.impl.MyEmbeddedDTO.Alpha; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.osgi.util.converter.TypeReference; + +import junit.framework.AssertionFailedError; + +import static org.junit.Assert.*; + +public class SchemaTest { + private SchematizerImpl schematizer; + + @Before + public void setUp() { + schematizer = new SchematizerImpl(); + } + + @After + public void tearDown() { + schematizer = null; + } + + @Test + public void testValues() { + final Schema s = schematizer + .schematize("MyDTO", new TypeReference>>(){}) + .get("MyDTO"); + assertNotNull(s); + + MyEmbeddedDTO2 embedded1 = new MyEmbeddedDTO2<>(); + embedded1.value = "value1"; + MyEmbeddedDTO2 embedded2 = new MyEmbeddedDTO2<>(); + embedded2.value = "value2"; + MyEmbeddedDTO2 embedded3 = new MyEmbeddedDTO2<>(); + embedded3.value = "value3"; + + MyDTO3> dto = new MyDTO3<>(); + dto.ping = "lalala"; + dto.pong = Long.MIN_VALUE; + dto.count = MyDTO3.Count.ONE; + dto.embedded = new ArrayList<>(); + dto.embedded.add(embedded1); + dto.embedded.add(embedded2); + dto.embedded.add(embedded3); + + assertEquals("lalala", s.valuesAt("/ping", dto).iterator().next()); + assertEquals(Long.MIN_VALUE, s.valuesAt("/pong", dto).iterator().next()); + assertEquals(MyDTO3.Count.ONE, s.valuesAt("/count", dto).iterator().next()); + assertNotNull(s.valuesAt("/embedded", dto)); + Object embeddedList = s.valuesAt("/embedded", dto).iterator().next(); + assertNotNull(embeddedList); + assertTrue(embeddedList instanceof List); + assertFalse(((List)embeddedList).isEmpty()); + Object embeddedObject = ((List)embeddedList).get(0); + assertTrue(embeddedObject instanceof MyEmbeddedDTO2); + assertListEquals(Arrays.asList(new String[]{"value1", "value2", "value3"}), s.valuesAt("/embedded/value", dto)); + } + + @Test + public void testEmbeddedValues() { + Schema s = schematizer + .schematize("MyDTO", new TypeReference(){}) + .get("MyDTO"); + assertNotNull(s); + + MyEmbeddedDTO embedded = new MyEmbeddedDTO(); + embedded.alpha = Alpha.A; + embedded.marco = "mmmm"; + embedded.polo = 66; + + MyDTO dto = new MyDTO(); + dto.ping = "lalala"; + dto.pong = Long.MIN_VALUE; + dto.count = MyDTO.Count.ONE; + dto.embedded = embedded; + + assertEquals("lalala", s.valuesAt("/ping", dto).iterator().next()); + assertEquals(Long.MIN_VALUE, s.valuesAt("/pong", dto).iterator().next()); + assertEquals(MyDTO.Count.ONE, s.valuesAt("/count", dto).iterator().next()); + assertNotNull(s.valuesAt("/embedded", dto)); + Object embeddedObject = s.valuesAt("/embedded", dto).iterator().next(); + assertTrue(embeddedObject instanceof MyEmbeddedDTO); + assertEquals(Alpha.A, s.valuesAt("/embedded/alpha", dto).iterator().next()); + assertEquals("mmmm", s.valuesAt("/embedded/marco", dto).iterator().next()); + assertEquals(66L, s.valuesAt("/embedded/polo", dto).iterator().next()); + } + + @Test + public void testNullValues() { + Schema s = schematizer + .schematize("MyDTO", new TypeReference>>(){}) + .get("MyDTO"); + assertNotNull(s); + + MyEmbeddedDTO2 embedded1 = new MyEmbeddedDTO2<>(); + MyEmbeddedDTO2 embedded2 = new MyEmbeddedDTO2<>(); + MyEmbeddedDTO2 embedded3 = new MyEmbeddedDTO2<>(); + + MyDTO3> dto = new MyDTO3<>(); + dto.ping = "lalala"; + dto.pong = Long.MIN_VALUE; + dto.count = MyDTO3.Count.ONE; + dto.embedded = new ArrayList<>(); + dto.embedded.add(embedded1); + dto.embedded.add(embedded2); + dto.embedded.add(embedded3); + + assertEquals("lalala", s.valuesAt("/ping", dto).iterator().next()); + assertEquals(Long.MIN_VALUE, s.valuesAt("/pong", dto).iterator().next()); + assertEquals(MyDTO3.Count.ONE, s.valuesAt("/count", dto).iterator().next()); + assertNotNull(s.valuesAt("/embedded", dto)); + assertListEquals(Arrays.asList(new String[]{null, null, null}), s.valuesAt("/embedded/value", dto)); + } + + @SuppressWarnings( { "rawtypes", "unchecked" } ) + private boolean assertListEquals(List expected, Collection actual) { + if (expected == null || actual == null) + throw new AssertionFailedError("The collection is null"); + + if (expected.size() != actual.size()) + throw new AssertionFailedError("Expected list size of " + expected.size() + ", but was: " + actual.size()); + + List actualList = new ArrayList<>(); + if (actual instanceof List) + actualList = (List)actual; + else + actualList.addAll(actual); + + for (int i = 0; i < actual.size(); i++) + assertEquals(expected.get(i), actualList.get(i)); + + return true; + } +} diff --git a/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/SchematizerServiceTest.java b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/SchematizerServiceTest.java new file mode 100644 index 00000000000..08dd1d84b98 --- /dev/null +++ b/converter/schematizer/src/test/java/org/apache/felix/schematizer/impl/SchematizerServiceTest.java @@ -0,0 +1,316 @@ +/* + * 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.schematizer.impl; + +import java.util.Map; + +import org.apache.felix.schematizer.Node; +import org.apache.felix.schematizer.Schema; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.osgi.util.converter.TypeReference; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class SchematizerServiceTest { + private SchematizerImpl schematizer; + + @Before + public void setUp() { + schematizer = new SchematizerImpl(); + } + + @After + public void tearDown() { + schematizer = null; + } + + @Test + public void testSchematizeDTO() { + Schema s = schematizer + .schematize("MyDTO", new TypeReference(){}) + .get("MyDTO"); + assertNotNull(s); + Node root = s.rootNode(); + assertNodeEquals("", "/", false, MyDTO.class, false, root); + assertEquals(4, root.children().size()); + Node pingNode = root.children().get("/ping"); + assertNodeEquals("ping", "/ping", false, String.class, true, pingNode); + Node pongNode = root.children().get("/pong"); + assertNodeEquals("pong", "/pong", false, Long.class, true, pongNode); + Node countNode = root.children().get("/count"); + assertNodeEquals("count", "/count", false, MyDTO.Count.class, true, countNode); + Node embeddedNode = root.children().get("/embedded"); + assertEquals(3, embeddedNode.children().size()); + assertNodeEquals("embedded", "/embedded", false, MyEmbeddedDTO.class, true, embeddedNode); + Node marcoNode = embeddedNode.children().get("/marco"); + assertNodeEquals("marco", "/embedded/marco", false, String.class, true, marcoNode); + Node poloNode = embeddedNode.children().get("/polo"); + assertNodeEquals("polo", "/embedded/polo", false, Long.class, true, poloNode); + Node alphaNode = embeddedNode.children().get("/alpha"); + assertNodeEquals("alpha", "/embedded/alpha", false, MyEmbeddedDTO.Alpha.class, true, alphaNode); + + Node sRoot = s.nodeAtPath("/"); + assertNodeEquals("", "/", false, MyDTO.class, false, sRoot); + Node sPingNode = s.nodeAtPath("/ping"); + assertNodeEquals("ping", "/ping", false, String.class, true, sPingNode); + Node sPongNode = s.nodeAtPath("/pong"); + assertNodeEquals("pong", "/pong", false, Long.class, true, sPongNode); + Node sCountNode = s.nodeAtPath("/count"); + assertNodeEquals("count", "/count", false, MyDTO.Count.class, true, sCountNode); + Node sEmbeddedNode = s.nodeAtPath("/embedded"); + assertNodeEquals("embedded", "/embedded", false, MyEmbeddedDTO.class, true, sEmbeddedNode); + Node sMarcoNode = s.nodeAtPath("/embedded/marco"); + assertNodeEquals("marco", "/embedded/marco", false, String.class, true, sMarcoNode); + Node sPoloNode = s.nodeAtPath("/embedded/polo"); + assertNodeEquals("polo", "/embedded/polo", false, Long.class, true, sPoloNode); + Node sAlphaNode = s.nodeAtPath("/embedded/alpha"); + assertNodeEquals("alpha", "/embedded/alpha", false, MyEmbeddedDTO.Alpha.class, true, sAlphaNode); + } + + @Test + public void testSchematizeDTOWithColletion() { + Schema s = schematizer + .type("MyDTO", "/embedded", new TypeReference>(){}) + .type("MyDTO", "/embedded/value", String.class) + .schematize("MyDTO", new TypeReference>>(){}) + .get("MyDTO"); + assertNotNull(s); + Node root = s.rootNode(); + assertNodeEquals("", "/", false, new TypeReference>>(){}.getType(), false, root); + assertEquals(4, root.children().size()); + Node pingNode = root.children().get("/ping"); + assertNodeEquals("ping", "/ping", false, String.class, true, pingNode); + Node pongNode = root.children().get("/pong"); + assertNodeEquals("pong", "/pong", false, Long.class, true, pongNode); + Node countNode = root.children().get("/count"); + assertNodeEquals("count", "/count", false, MyDTO3.Count.class, true, countNode); + Node embeddedNode = root.children().get("/embedded"); + assertEquals(1, embeddedNode.children().size()); + assertNodeEquals("embedded", "/embedded", true, new TypeReference>(){}.getType(), true, embeddedNode); + Node valueNode = embeddedNode.children().get("/value"); + assertNodeEquals("value", "/embedded/value", false, String.class, true, valueNode); + + Node sRoot = s.nodeAtPath("/"); + assertNodeEquals("", "/", false, new TypeReference>>(){}.getType(), false, sRoot); + Node sPingNode = s.nodeAtPath("/ping"); + assertNodeEquals("ping", "/ping", false, String.class, true, sPingNode); + Node sPongNode = s.nodeAtPath("/pong"); + assertNodeEquals("pong", "/pong", false, Long.class, true, sPongNode); + Node sCountNode = s.nodeAtPath("/count"); + assertNodeEquals("count", "/count", false, MyDTO3.Count.class, true, sCountNode); + Node sEmbeddedNode = s.nodeAtPath("/embedded"); + assertNodeEquals("embedded", "/embedded", true, new TypeReference>(){}.getType(), true, sEmbeddedNode); + Node sValueNode = s.nodeAtPath("/embedded/value"); + assertNodeEquals("value", "/embedded/value", false, String.class, true, sValueNode); + } + + @Test + public void testSchematizeDTOWithAnnotatedColletion() { + Schema s = schematizer + .schematize("MyDTO4", new TypeReference(){}) + .get("MyDTO4"); + assertNotNull(s); + Node root = s.rootNode(); + assertNodeEquals("", "/", false, MyDTO4.class, false, root); + assertEquals(4, root.children().size()); + Node pingNode = root.children().get("/ping"); + assertNodeEquals("ping", "/ping", false, String.class, true, pingNode); + Node pongNode = root.children().get("/pong"); + assertNodeEquals("pong", "/pong", false, Long.class, true, pongNode); + Node countNode = root.children().get("/count"); + assertNodeEquals("count", "/count", false, MyDTO4.Count.class, true, countNode); + Node embeddedNode = root.children().get("/embedded"); + assertEquals(3, embeddedNode.children().size()); + assertNodeEquals("embedded", "/embedded", true, MyEmbeddedDTO.class, true, embeddedNode); + Node marcoNode = embeddedNode.children().get("/marco"); + assertNodeEquals("marco", "/embedded/marco", false, String.class, true, marcoNode); + Node poloNode = embeddedNode.children().get("/polo"); + assertNodeEquals("polo", "/embedded/polo", false, Long.class, true, poloNode); + Node alphaNode = embeddedNode.children().get("/alpha"); + assertNodeEquals("alpha", "/embedded/alpha", false, MyEmbeddedDTO.Alpha.class, true, alphaNode); + + Node sRoot = s.nodeAtPath("/"); + assertNodeEquals("", "/", false, MyDTO4.class, false, sRoot); + Node sPingNode = s.nodeAtPath("/ping"); + assertNodeEquals("ping", "/ping", false, String.class, true, sPingNode); + Node sPongNode = s.nodeAtPath("/pong"); + assertNodeEquals("pong", "/pong", false, Long.class, true, sPongNode); + Node sCountNode = s.nodeAtPath("/count"); + assertNodeEquals("count", "/count", false, MyDTO4.Count.class, true, sCountNode); + Node sEmbeddedNode = s.nodeAtPath("/embedded"); + assertNodeEquals("embedded", "/embedded", true, MyEmbeddedDTO.class, true, sEmbeddedNode); + Node sMarcoNode = s.nodeAtPath("/embedded/marco"); + assertNodeEquals("marco", "/embedded/marco", false, String.class, true, sMarcoNode); + Node sPoloNode = s.nodeAtPath("/embedded/polo"); + assertNodeEquals("polo", "/embedded/polo", false, Long.class, true, sPoloNode); + Node sAlphaNode = s.nodeAtPath("/embedded/alpha"); + assertNodeEquals("alpha", "/embedded/alpha", false, MyEmbeddedDTO.Alpha.class, true, sAlphaNode); + } + + @Test + public void testSchematizeToMap() { + Schema s = schematizer + .schematize("MyDTO", new TypeReference(){}) + .get("MyDTO"); + Map map = s.toMap(); + testMapValues(map); + } + + private void testMapValues(Map map) { + assertNotNull(map); + assertEquals(1, map.size()); + Node.DTO root = map.get("/"); + assertEquals(4, root.children.size()); + assertNodeDTOEquals("", "/", false, MyDTO.class, root); + Node.DTO pingNode = root.children.get("ping"); + assertNodeDTOEquals("ping", "/ping", false, String.class, pingNode); + Node.DTO pongNode = root.children.get("pong"); + assertNodeDTOEquals("pong", "/pong", false, Long.class, pongNode); + Node.DTO countNode = root.children.get("count"); + assertNodeDTOEquals("count", "/count", false, MyDTO.Count.class, countNode); + Node.DTO embeddedNode = root.children.get("embedded"); + assertEquals(3, embeddedNode.children.size()); + assertNodeDTOEquals("embedded", "/embedded", false, MyEmbeddedDTO.class, embeddedNode); + Node.DTO marcoNode = embeddedNode.children.get("marco"); + assertNodeDTOEquals("marco", "/embedded/marco", false, String.class, marcoNode); + Node.DTO poloNode = embeddedNode.children.get("polo"); + assertNodeDTOEquals("polo", "/embedded/polo", false, Long.class, poloNode); + Node.DTO alphaNode = embeddedNode.children.get("alpha"); + assertNodeDTOEquals("alpha", "/embedded/alpha", false, MyEmbeddedDTO.Alpha.class, alphaNode); + } + + @SuppressWarnings( "unused" ) + @Test + @Ignore("Probably no longer necessary...") + public void testSchemaFromMap() { + Schema s1 = schematizer + .schematize("MyDTO", new TypeReference(){}) + .get("MyDTO"); + Map map = s1.toMap(); + +// Schema s2 = schematizer.from("MyDTO", map); +// testSchema(s2); + } + + @SuppressWarnings( "unused" ) + private void testSchema(Schema s) { + // Assume that the map is serialized, then deserialized "as is". + assertNotNull(s); + Node root = s.rootNode(); + assertEquals(4, root.children().size()); + assertNodeEquals("", "/", false, MyDTO.class, false, root); + Node pingNode = root.children().get("/ping"); + assertNodeEquals("ping", "/ping", false, String.class, true, pingNode); + Node pongNode = root.children().get("/pong"); + assertNodeEquals("pong", "/pong", false, Long.class, true, pongNode); + Node countNode = root.children().get("/count"); + assertNodeEquals("count", "/count", false, MyDTO.Count.class, true, countNode); + Node embeddedNode = root.children().get("/embedded"); + assertEquals(3, embeddedNode.children().size()); + assertNodeEquals("embedded", "/embedded", false, MyEmbeddedDTO.class, true, embeddedNode); + Node marcoNode = embeddedNode.children().get("/marco"); + assertNodeEquals("marco", "/embedded/marco", false, String.class, true, marcoNode); + Node poloNode = embeddedNode.children().get("/polo"); + assertNodeEquals("polo", "/embedded/polo", false, Long.class, true, poloNode); + Node alphaNode = embeddedNode.children().get("/alpha"); + assertNodeEquals("alpha", "/embedded/alpha", false, MyEmbeddedDTO.Alpha.class, true, alphaNode); + + Node sRoot = s.nodeAtPath("/"); + assertNodeEquals("", "/", false, MyDTO.class, false, sRoot); + Node sPingNode = s.nodeAtPath("/ping"); + assertNodeEquals("ping", "/ping", false, String.class, true, sPingNode); + Node sPongNode = s.nodeAtPath("/pong"); + assertNodeEquals("pong", "/pong", false, Long.class, true, sPongNode); + Node sCountNode = s.nodeAtPath("/count"); + assertNodeEquals("count", "/count", false, MyDTO.Count.class, true, sCountNode); + Node sEmbeddedNode = s.nodeAtPath("/embedded"); + assertNodeEquals("embedded", "/embedded", false, MyEmbeddedDTO.class, true, sEmbeddedNode); + Node sMarcoNode = s.nodeAtPath("/embedded/marco"); + assertNodeEquals("marco", "/embedded/marco", false, String.class, true, sMarcoNode); + Node sPoloNode = s.nodeAtPath("/embedded/polo"); + assertNodeEquals("polo", "/embedded/polo", false, Long.class, true, sPoloNode); + Node sAlphaNode = s.nodeAtPath("/embedded/alpha"); + assertNodeEquals("alpha", "/embedded/alpha", false, MyEmbeddedDTO.Alpha.class, true, sAlphaNode); + } + + @Test + public void testVisitor() { + Schema s = schematizer + .schematize("MyDTO", new TypeReference(){}) + .get("MyDTO"); + StringBuilder sb = new StringBuilder(); + s.visit( n -> sb.append("::").append(n.name())); + assertEquals("::::count::embedded::alpha::marco::polo::ping::pong", sb.toString()); + } + + @Test + public void testGetParentNode() { + Schema s = schematizer + .schematize("MyDTO", new TypeReference(){}) + .get("MyDTO"); + assertNotNull(s); + Node embeddedNode = s.nodeAtPath("/embedded/marco"); + assertTrue(!"ERROR".equals(embeddedNode.name())); + Node parentNode = s.parentOf(embeddedNode); + assertTrue(!"ERROR".equals(parentNode.name())); + Node grandparentNode = s.parentOf(parentNode); + assertTrue(!"ERROR".equals(grandparentNode.name())); + assertEquals("/", grandparentNode.absolutePath()); + } + + @Test + public void testTypeRules() { + Schema s = schematizer + .type("MyDTO", "/embedded", new TypeReference>(){}) + .type("MyDTO", "/embedded/value", String.class) + .schematize("MyDTO", new TypeReference>>(){}) + .get("MyDTO"); + assertNotNull(s); + Node embeddedNode = s.nodeAtPath("/embedded/value"); + assertTrue(!"ERROR".equals(embeddedNode.name())); + Node parentNode = s.parentOf(embeddedNode); + assertTrue(!"ERROR".equals(parentNode.name())); + Node grandparentNode = s.parentOf(parentNode); + assertTrue(!"ERROR".equals(grandparentNode.name())); + assertEquals("/", grandparentNode.absolutePath()); + } + + private void assertNodeEquals(String name, String path, boolean isCollection, Object type, boolean fieldNotNull, Node node) { + assertNotNull(node); + assertEquals(name, node.name()); + assertEquals(path, node.absolutePath()); + assertEquals(isCollection, node.isCollection()); + assertEquals(type, node.type()); + if (fieldNotNull) + assertNotNull(node.field()); + else + assertTrue(node.field() == null); + } + + private void assertNodeDTOEquals(String name, String path, boolean isCollection, Class type, Node.DTO node) { + assertNotNull(node); + assertEquals(name, node.name); + assertEquals(path, node.path); + assertEquals(isCollection, node.isCollection); + assertEquals(type.getName(), node.type); + } +} diff --git a/converter/schematizer/src/test/java/org/apache/felix/serializer/impl/json/JsonDeserializationTest.java b/converter/schematizer/src/test/java/org/apache/felix/serializer/impl/json/JsonDeserializationTest.java new file mode 100644 index 00000000000..ec91e688a3a --- /dev/null +++ b/converter/schematizer/src/test/java/org/apache/felix/serializer/impl/json/JsonDeserializationTest.java @@ -0,0 +1,98 @@ +/* + * 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.serializer.impl.json; + +import java.util.ArrayList; + +import org.apache.felix.schematizer.impl.SchematizerImpl; +import org.apache.felix.serializer.impl.json.MyDTO.Count; +import org.apache.felix.serializer.impl.json.MyEmbeddedDTO.Alpha; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.osgi.util.converter.Converter; +import org.osgi.util.converter.TypeReference; + +import static org.junit.Assert.*; + +public class JsonDeserializationTest { + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + @Test + public void testDeserialization() { + MyEmbeddedDTO embedded = new MyEmbeddedDTO(); + embedded.marco = "mmmm"; + embedded.polo = 24; + embedded.alpha = Alpha.B; + + MyDTO dto = new MyDTO(); + dto.ping = "pppping"; + dto.pong = 42; + dto.count = Count.TWO; + dto.embedded = embedded; + + Converter c = new SchematizerImpl() + .schematize("MyDTO", new TypeReference(){}) + .converterFor("MyDTO"); + String serialized = new JsonSerializerImpl().serialize(dto).convertWith(c).toString(); + MyDTO result = new JsonSerializerImpl() + .deserialize(MyDTO.class) + .convertWith(c) + .from(serialized); + + assertEquals(dto.ping, result.ping); + assertEquals(dto.pong, result.pong); + assertEquals(dto.count, result.count); + assertEquals(dto.embedded.marco, result.embedded.marco); + assertEquals(dto.embedded.polo, result.embedded.polo); + assertEquals(dto.embedded.alpha, result.embedded.alpha); + } + + @Test + public void testDeserializationWithCollection() { + MyEmbeddedDTO2 embedded = new MyEmbeddedDTO2<>(); + embedded.value = "one million dollars"; + + MyDTO2> dto = new MyDTO2<>(); + dto.ping = "pppping"; + dto.pong = 42; + dto.embedded = new ArrayList<>(); + dto.embedded.add( embedded ); + + String serialized = new JsonSerializerImpl().serialize(dto).toString(); + + Converter c = new SchematizerImpl() + .type("MyDTO", "/embedded", new TypeReference>(){}) + .schematize("MyDTO", new TypeReference>>(){}) + .converterFor("MyDTO"); + MyDTO2> result = new JsonSerializerImpl() + .deserialize(new TypeReference>>(){}) + .convertWith(c) + .from(serialized); + + assertEquals(dto.ping, result.ping); + assertEquals(dto.pong, result.pong); + assertEquals(dto.embedded.get(0).value, result.embedded.get(0).value); + } +} diff --git a/converter/schematizer/src/test/java/org/apache/felix/serializer/impl/json/MyDTO.java b/converter/schematizer/src/test/java/org/apache/felix/serializer/impl/json/MyDTO.java new file mode 100644 index 00000000000..6a293730a1d --- /dev/null +++ b/converter/schematizer/src/test/java/org/apache/felix/serializer/impl/json/MyDTO.java @@ -0,0 +1,32 @@ +/* + * 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.serializer.impl.json; + +import org.osgi.dto.DTO; + +public class MyDTO extends DTO { + public enum Count { ONE, TWO, THREE } + + public Count count; + + public String ping; + + public long pong; + + public MyEmbeddedDTO embedded; +} + diff --git a/converter/schematizer/src/test/java/org/apache/felix/serializer/impl/json/MyDTO2.java b/converter/schematizer/src/test/java/org/apache/felix/serializer/impl/json/MyDTO2.java new file mode 100644 index 00000000000..6234e0ecab1 --- /dev/null +++ b/converter/schematizer/src/test/java/org/apache/felix/serializer/impl/json/MyDTO2.java @@ -0,0 +1,32 @@ +/* + * 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.serializer.impl.json; + +import java.util.List; + +import org.osgi.dto.DTO; + +public class MyDTO2 extends DTO { + public enum Count { ONE, TWO, THREE } + + public String ping; + + public long pong; + + public List embedded; +} + diff --git a/converter/schematizer/src/test/java/org/apache/felix/serializer/impl/json/MyEmbeddedDTO.java b/converter/schematizer/src/test/java/org/apache/felix/serializer/impl/json/MyEmbeddedDTO.java new file mode 100644 index 00000000000..1906c30d182 --- /dev/null +++ b/converter/schematizer/src/test/java/org/apache/felix/serializer/impl/json/MyEmbeddedDTO.java @@ -0,0 +1,29 @@ +/* + * 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.serializer.impl.json; + +import org.osgi.dto.DTO; + +public class MyEmbeddedDTO extends DTO { + public enum Alpha { A, B, C } + + public Alpha alpha; + + public String marco; + + public long polo; +} diff --git a/converter/schematizer/src/test/java/org/apache/felix/serializer/impl/json/MyEmbeddedDTO2.java b/converter/schematizer/src/test/java/org/apache/felix/serializer/impl/json/MyEmbeddedDTO2.java new file mode 100644 index 00000000000..923edaf5e62 --- /dev/null +++ b/converter/schematizer/src/test/java/org/apache/felix/serializer/impl/json/MyEmbeddedDTO2.java @@ -0,0 +1,26 @@ +/* + * 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.serializer.impl.json; + +import org.apache.felix.schematizer.AsDTO; +import org.osgi.dto.DTO; + +@AsDTO +public class MyEmbeddedDTO2 extends DTO { + public T value; +} + diff --git a/converter/serializer/pom.xml b/converter/serializer/pom.xml new file mode 100644 index 00000000000..2b5014badb6 --- /dev/null +++ b/converter/serializer/pom.xml @@ -0,0 +1,141 @@ + + + + 4.0.0 + + org.apache.felix + felix-parent + 4 + ../pom/pom.xml + + + Apache Felix Serializer Services + org.apache.felix.serializer + 0.3.0-SNAPSHOT + jar + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/converter/serializer + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/converter/serializer + http://svn.apache.org/viewvc/felix/trunk/converter/serializer/ + + + + 8 + java18 + + + + + + org.apache.felix + maven-bundle-plugin + 3.2.0 + + + bundle + package + + bundle + + + + baseline + + baseline + + + + + + org.apache.felix.serializer.impl.Activator + + org.apache.felix.serializer.*, + org.yaml.snakeyaml.*, + org.apache.felix.utils.* + + + org.apache.felix.serializer;-split-package:=merge-first + + * + + osgi.service;objectClass:List<String>="org.apache.felix.serializer.Serializer, + org.apache.felix.serializer.Serializer$JsonSerializer, + org.apache.felix.serializer.Serializer$YamlSerializer, + org.apache.felix.serializer.WriterFactory, + org.apache.felix.serializer.WriterFactory$JsonWriterFactory, + org.apache.felix.serializer.WriterFactory$YamlWriterFactory"; + uses:="org.apache.felix.serializer,org.osgi.util.converter,org.osgi.util.function" + + <_sources>true + + + + + + + + + org.apache.felix + org.apache.felix.converter + ${project.version} + + + + org.osgi + osgi.annotation + 6.0.1 + provided + + + + org.osgi + osgi.core + 6.0.0 + provided + + + + org.yaml + snakeyaml + 1.17 + + + + org.apache.felix + org.apache.felix.utils + 1.10.1-SNAPSHOT + provided + + + + junit + junit + test + + + + org.apache.sling + org.apache.sling.commons.json + 2.0.16 + test + + + diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/Deserializing.java b/converter/serializer/src/main/java/org/apache/felix/serializer/Deserializing.java new file mode 100644 index 00000000000..eec38f09f54 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/Deserializing.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) OSGi Alliance (2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.serializer; + +import java.io.InputStream; +import java.nio.charset.Charset; + +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.util.converter.Converter; + +/** + * Interface to specify the source of the decoding operation + * + * @param The target type for the decoding operation. + * @author $Id$ + * @ThreadSafe + */ +@ProviderType +public interface Deserializing { + /** + * Use an input stream as the source of the decoding operation. As encoding + * UTF-8 is used. + * + * @param in The stream to use. + * @return the decoded object. + */ + T from(InputStream in); + + /** + * Use an input stream as the source of the decoding operation. + * + * @param in The stream to use. + * @param charset The character set to use. + * @return the decoded object. + */ + T from(InputStream in, Charset charset); + + /** + * Use a Readable as the source of the decoding operation. + * + * @param in The readable to use. + * @return the decoded object. + */ + T from(Readable in); + + /** + * Use a Char Sequence as the source of the decoding operation. + * + * @param in The char sequence to use. + * @return the decoded object. + */ + T from(CharSequence in); + + /** + * Specify the converter to be used by the code, if an alternative, adapted, + * converter is to be used. + * + * @param converter The converter to use. + * @return This Deserializing object to allow further invocations on it. + */ + Deserializing convertWith(Converter converter); + + /** + * Specify the parser to be used, if an alternative to the default internal + * parser is required. + * + * @param parser the parser to use. + * @return This Deserializing object to allow further invocations on it. + */ + Deserializing parseWith(Parser parser); +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/Parser.java b/converter/serializer/src/main/java/org/apache/felix/serializer/Parser.java new file mode 100644 index 00000000000..db86a28db01 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/Parser.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) OSGi Alliance (2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.serializer; + +import java.io.InputStream; +import java.util.Map; + +/** + * Common interface for a parser, which can be provided by the client. + */ +public interface Parser { + Map parse(InputStream in); + Map parse(CharSequence in); +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/Serializer.java b/converter/serializer/src/main/java/org/apache/felix/serializer/Serializer.java new file mode 100644 index 00000000000..27dc18f76f5 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/Serializer.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) OSGi Alliance (2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.serializer; + +import java.lang.reflect.Type; + +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.util.converter.TypeReference; + +/** + * The Codec service can be used to encode a given object in a certain + * representation, for example JSON, YAML or XML. The Codec service can also + * decode the representation it produced. A single Codec service can + * encode/decode only a single format. To support multiple encoding formats + * register multiple services. + * + * @author $Id$ + * @ThreadSafe + */ +@ProviderType +public interface Serializer { + /** + * Start specifying a decode operation. + * + * @param The type to decode to. + * @param cls The class to decode to. + * @return A {@link Deserializing} object to specify the source for the + * decode operation. + */ + Deserializing deserialize(Class cls); + + /** + * Start specifying a decode operation. + * + * @param The type to decode to. + * @param ref A type reference for the target type. + * @return A {@link Deserializing} object to specify the source for the + * decode operation. + */ + Deserializing deserialize(TypeReference ref); + + /** + * Start specifying a decode operation. + * + * @param type The type to convert to. + * @return A {@link Deserializing} object to specify the source for the + * decode operation. + */ + Deserializing< ? > deserialize(Type type); + + /** + * Start specifying an encode operation. + * + * @param obj The object to encode. + * @return an Encoding object to specify the target for the decode + * operation. + */ + Serializing serialize(Object obj); + + /** + * A convenience means of obtaining a JsonSerializer without having to + * configure service settings. + */ + static interface JsonSerializer extends Serializer {} + + /** + * A convenience means of obtaining a YamlWriterFactory without having to + * configure service settings. + */ + static interface YamlSerializer extends Serializer {} +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/Serializing.java b/converter/serializer/src/main/java/org/apache/felix/serializer/Serializing.java new file mode 100644 index 00000000000..2b19e708135 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/Serializing.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) OSGi Alliance (2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.serializer; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; + +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.util.converter.Converter; +import org.osgi.util.converter.Specifying; + +/** + * Interface to specify the target of the encoding operation. + * + * @author $Id$ + * @ThreadSafe + */ +@ProviderType +public interface Serializing extends Specifying { + /** + * Use an output stream as the target of the encoding operation. UTF-8 will + * be used if applicable, the character set may not apply to binary + * encodings. + * + * @param out The output stream to use. + * @throws IOException If an I/O error occurred. + */ + void to(OutputStream out) throws IOException; + + /** + * Use an output stream as the target of the encoding operation. + * + * @param out The output stream to use. + * @param charset The character set to use, if applicable, the character set + * may not apply to binary encodings. + * @throws IOException If an I/O error occurred. + */ + void to(OutputStream out, Charset charset) throws IOException; + + /** + * Encode the object and append the result to an appendable. + * + * @param out The appendable object to use. + * @return The appendable object provided in, which allows further appends + * to it be done in a fluent programming style. + */ + Appendable to(Appendable out); + + /** + * Encode the object and return the result as a string. + * + * @return The encoded object. + */ + @Override + String toString(); + + /** + * Specify the converter to be used by the code, if an alternative, adapted, + * converter is to be used. + * + * @param converter The converter to use. + * @return This Serializing object to allow further invocations on it. + */ + Serializing convertWith(Converter converter); + + /** + * Specify the writer to be used, if an alternative to the default internal + * writer is required. A selection of Writers are available via the WriterFactory, + * or a completely different Writer can be provided directly. + * + * @param parser the writer to use. + * @return This Serializing object to allow further invocations on it. + */ + Serializing writeWith(Writer writer); +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/Writer.java b/converter/serializer/src/main/java/org/apache/felix/serializer/Writer.java new file mode 100644 index 00000000000..2c7e1f60c26 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/Writer.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) OSGi Alliance (2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.serializer; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +/** + * Common interface for a writer, which can be provided by the client. + */ +public interface Writer { + String write(Object obj); + Map> mapOrderingRules(); + Map> arrayOrderingRules(); +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/WriterFactory.java b/converter/serializer/src/main/java/org/apache/felix/serializer/WriterFactory.java new file mode 100644 index 00000000000..5be2d095e20 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/WriterFactory.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.serializer; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.util.converter.Converter; + +/** + * A factory to create a writer with the desired behaviour. + * + * @author $Id:$ + */ +@ProviderType +public interface WriterFactory { + + /** + * A default Writer, but which will use a custom Converter. + * + * @param c the custom Converter that will be used by the Writer + * @return a default Writer, but one which will use a custom Converter + */ + Writer newDefaultWriter(Converter c); + + /** + * A writer that is useful for debugging. This write outputs "pretty" + * format. + * + * @param c the custom Converter that will be used by the Writer + * @return A new writer useful for debugging + */ + Writer newDebugWriter(Converter c); + + /** + * Register an ordering rule for this writer. + * + * An ordering rule causes the written json to be output in the order + * specified. This can be useful, for example, for debugging or when + * the data otherwise needs to be human consumable. + * + * This rule only affects map-type objects located at the given path. + * + * @param path the path where the key is located in the object graph. + * @param keyOrder A list with the desired key order. + * @return This factory object to allow further invocations on it. + */ + WriterFactory orderMap(String path, List keyOrder); + + /** + * Register multiple ordering rules for this writer. + * + * An ordering rule causes the written json to be output in the order + * specified. This can be useful, for example, for debugging or when + * the data otherwise needs to be human consumable. + * + * This rule only affects map-type objects located at the given path. + * + * Each map entry is a path/keyOrder pair, the same as if calling + * WriterFactory.orderMap(String,List). + * + * @param orderingRules the rules to be added + * @return This factory object to allow further invocations on it. + */ + WriterFactory orderMap( Map> orderingRules ); + + /** + * Register an ordering rule for this writer. + * + * An ordering rule causes the written json to be output in the order + * specified. This can be useful, for example, for debugging or when + * the data otherwise needs to be human consumable. + * + * This rule only affects array-type objects located at the given path. + * + * @param path the path where the key is located in the object graph. + * @return This factory object to allow further invocations on it. + */ + WriterFactory orderArray(String path); + + /** + * Register an ordering rule for this writer. + * + * An ordering rule causes the written json to be output in the order + * specified. This can be useful, for example, for debugging or when + * the data otherwise needs to be human consumable. + * + * This rule only affects array-type objects located at the given path. + * + * @param path the path where the key is located in the object graph. + * @param comparator A comparator that will be used to sort the items in the array. + * @return This factory object to allow further invocations on it. + */ + WriterFactory orderArray(String path, Comparator comparator); + + /** + * A convenience means of obtaining a JsonWriterFactory without having to + * configure service settings. + */ + static interface JsonWriterFactory extends WriterFactory {} + + /** + * A convenience means of obtaining a YamlWriterFactory without having to + * configure service settings. + */ + static interface YamlWriterFactory extends WriterFactory {} +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/impl/AbstractSpecifying.java b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/AbstractSpecifying.java new file mode 100644 index 00000000000..26cbde3fcd2 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/AbstractSpecifying.java @@ -0,0 +1,100 @@ +/* + * 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.serializer.impl; + +import org.osgi.util.converter.Specifying; + +public abstract class AbstractSpecifying> implements Specifying { + protected volatile Object defaultValue; + protected volatile boolean hasDefault = false; + protected volatile boolean forceCopy = false; + protected volatile boolean keysIgnoreCase = false; + protected volatile Class sourceAsClass; + protected volatile boolean sourceAsDTO = false; + protected volatile boolean sourceAsJavaBean = false; + protected volatile Class targetAsClass; + protected volatile boolean targetAsDTO = false; + protected volatile boolean targetAsJavaBean = false; + + @SuppressWarnings("unchecked") + private T castThis() { + return (T) this; + } + +// @Override +// public T copy() { +// forceCopy = true; +// return castThis(); +// } + + @Override + public T defaultValue(Object defVal) { + defaultValue = defVal; + hasDefault = true; + return castThis(); + } + + @Override + public T keysIgnoreCase() { + keysIgnoreCase = true; + return castThis(); + } + + @Override + public T sourceAs(Class cls) { + sourceAsClass = cls; + return castThis(); + } + + @Override + public T sourceAsBean() { + // To avoid ambiguity, reset any instruction to sourceAsDTO + sourceAsDTO = false; + sourceAsJavaBean = true; + return castThis(); + } + + @Override + public T sourceAsDTO() { + // To avoid ambiguity, reset any instruction to sourceAsJavaBean + sourceAsJavaBean = false; + sourceAsDTO = true; + return castThis(); + } + + @Override + public T targetAs(Class cls) { + targetAsClass = cls; + return castThis(); + } + + @Override + public T targetAsBean() { + // To avoid ambiguity, reset any instruction to targetAsDTO + targetAsDTO = false; + targetAsJavaBean = true; + return castThis(); + } + + @Override + public T targetAsDTO() { + // To avoid ambiguity, reset any instruction to targetAsJavaBean + targetAsJavaBean = false; + targetAsDTO = true; + return castThis(); + } +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/impl/Activator.java b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/Activator.java new file mode 100644 index 00000000000..6ab0e2289e9 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/Activator.java @@ -0,0 +1,80 @@ +/* + * 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.serializer.impl; + +import java.util.Arrays; +import java.util.Dictionary; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Set; + +import org.apache.felix.serializer.Serializer; +import org.apache.felix.serializer.WriterFactory; +import org.apache.felix.serializer.impl.json.JsonSerializerImpl; +import org.apache.felix.serializer.impl.yaml.YamlSerializerImpl; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +public class Activator implements BundleActivator { + public static final String[] jsonArray = new String[] { + "application/json", "application/x-javascript", "text/javascript", + "text/x-javascript", "text/x-json" }; + public static final Set jsonSet = new HashSet<>(Arrays.asList(jsonArray)); + + public static final String[] yamlArray = new String[] { + "text/yaml", "text/x-yaml", "application/yaml", + "application/x-yaml" }; + public static final Set yamlSet = new HashSet<>(Arrays.asList(yamlArray)); + + @SuppressWarnings( { "unchecked", "rawtypes" } ) + @Override + public void start(BundleContext context) throws Exception { + // JSON + Dictionary jsonProps = new Hashtable<>(); + jsonProps.put("mimetype", jsonArray); + context.registerService( + new String[]{Serializer.class.getName(), Serializer.JsonSerializer.class.getName()}, + new JsonSerializerImpl(), + jsonProps); + context.registerService( + WriterFactory.JsonWriterFactory.class, + new PrototypeWriterFactory(), + jsonProps); + + // YAML + Dictionary yamlProps = new Hashtable<>(); + yamlProps.put("mimetype", yamlArray); + context.registerService( + new String[]{Serializer.class.getName(), Serializer.YamlSerializer.class.getName()}, + new YamlSerializerImpl(), + yamlProps); + context.registerService( + WriterFactory.YamlWriterFactory.class, + new PrototypeWriterFactory(), + yamlProps); + + // Not-specified (default will be JSON) + context.registerService( + WriterFactory.class, + new PrototypeWriterFactory(), + null); + } + + @Override + public void stop(BundleContext context) throws Exception { + } +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/impl/PrototypeWriterFactory.java b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/PrototypeWriterFactory.java new file mode 100644 index 00000000000..0c432572ab1 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/PrototypeWriterFactory.java @@ -0,0 +1,51 @@ +/* + * 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.serializer.impl; + +import org.apache.felix.serializer.WriterFactory; +import org.apache.felix.serializer.impl.json.JsonWriterFactory; +import org.apache.felix.serializer.impl.yaml.YamlWriterFactory; +import org.osgi.framework.Bundle; +import org.osgi.framework.PrototypeServiceFactory; +import org.osgi.framework.ServiceRegistration; + +public class PrototypeWriterFactory implements PrototypeServiceFactory { + + @SuppressWarnings( "unchecked" ) + @Override + public T getService(Bundle bundle, ServiceRegistration registration) { + String[] mimetype = (String[])registration.getReference().getProperty("mimetype"); + if (isYaml(mimetype)) + return (T)new YamlWriterFactory(); + else + return (T)new JsonWriterFactory(); + } + + private boolean isYaml(String[] mimetype) { + if (mimetype == null || mimetype.length == 0) + return false; + for (String entry : mimetype) { + if ("application/yaml".equals(entry)) + return true; + } + + return false; + } + @Override + public void ungetService(Bundle bundle, ServiceRegistration registration, T service ) { + } +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/impl/Util.java b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/Util.java new file mode 100644 index 00000000000..c1cdf9ff8cf --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/Util.java @@ -0,0 +1,50 @@ +/* + * 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.serializer.impl; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +public class Util { + private Util() {} // prevent instantiation + + public static byte [] readStream(InputStream is) throws IOException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] bytes = new byte[8192]; + + int length = 0; + int offset = 0; + + while ((length = is.read(bytes, offset, bytes.length - offset)) != -1) { + offset += length; + + if (offset == bytes.length) { + baos.write(bytes, 0, bytes.length); + offset = 0; + } + } + if (offset != 0) { + baos.write(bytes, 0, offset); + } + return baos.toByteArray(); + } finally { + is.close(); + } + } +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/DebugJsonWriter.java b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/DebugJsonWriter.java new file mode 100644 index 00000000000..b3f0f0332d5 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/DebugJsonWriter.java @@ -0,0 +1,194 @@ +/* + * 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.serializer.impl.json; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.Map.Entry; + +import org.apache.felix.serializer.Writer; +import org.osgi.dto.DTO; +import org.osgi.util.converter.Converter; + +public class DebugJsonWriter implements Writer { + + private Converter converter; + private final Map> mapOrderingRules; + private final Map> arrayOrderingRules; + private final boolean ignoreNull = false; + private final int indentation = 2; + + public DebugJsonWriter(Converter c, Map> mapRules, Map> arrayRules) { + converter = c; + mapOrderingRules = mapRules; + arrayOrderingRules = arrayRules; + } + + @Override + public String write(Object obj) { + return encode(obj, "/", 0).trim(); + } + + @Override + public Map> mapOrderingRules() { + return mapOrderingRules; + } + + @Override + public Map> arrayOrderingRules() { + return arrayOrderingRules; + } + + @SuppressWarnings("rawtypes") + private String encode(Object obj, String path, int level) { + if (obj == null) { + return ignoreNull ? "" : "null"; + } + + if (obj instanceof String) { + return "\"" + (String)obj + "\""; + } else if (obj instanceof Map) { + return encodeMap(orderMap((Map)obj, path), path, level); + } else if (obj instanceof Collection) { + return encodeCollection((Collection) obj, path, level); + } else if (obj instanceof DTO) { + Map converted = converter.convert(obj).sourceAsDTO().to(Map.class); + return encodeMap(orderMap(converted, path), path, level); + } else if (obj.getClass().isArray()) { + return encodeCollection(asCollection(obj), path, level); + } else if (obj instanceof Number) { + return obj.toString(); + } else if (obj instanceof Boolean) { + return obj.toString(); + } + + return "\"" + converter.convert(obj).to(String.class) + "\""; + } + + @SuppressWarnings( { "unchecked", "rawtypes" } ) + private Map orderMap(Map unordered, String path) { + Map ordered = (mapOrderingRules.containsKey(path)) ? new LinkedHashMap<>() : new TreeMap<>(); + List keys = (mapOrderingRules.containsKey(path)) ? mapOrderingRules.get(path) : new ArrayList<>(unordered.keySet()); + for (String key : keys) { + String itemPath = (path.endsWith("/")) ? path + key : path + "/" + key; + Object value = unordered.get(key); + if (value instanceof Map) + ordered.put(key, orderMap((Map)value, itemPath)); + else if(value instanceof Collection) + ordered.put(key, orderCollectionItems((Collection)value, itemPath)); + else + ordered.put(key, value); + } + + return ordered; + } + + @SuppressWarnings( { "unchecked", "rawtypes" } ) + private List orderCollectionItems(Collection unordered, String path) { + List ordered = new ArrayList<>(); + for (Object obj: unordered) { + if (obj instanceof Map) + ordered.add(orderMap((Map)obj, path)); + else if(obj instanceof Collection) + ordered.add(orderCollectionItems((Collection)obj, path)); + else + ordered.add(obj); + } + + if (arrayOrderingRules.containsKey(path)) { + Comparator c = arrayOrderingRules.get(path); + if (c == null) + Collections.sort(ordered); + else + Collections.sort(ordered,c); + } + + return ordered; + } + + private Collection asCollection(Object arr) { + // Arrays.asList() doesn't work for primitive arrays + int len = Array.getLength(arr); + List l = new ArrayList<>(len); + for (int i=0; i collection, String path, int level) { + level++; + StringBuilder sb = new StringBuilder("[\n"); + + boolean first = true; + for (Object o : collection) { + if (first) + first = false; + else + sb.append(",\n"); + + sb.append(getIdentPrefix(level)); + sb.append(encode(o, path, level)); + } + + sb.append("\n"); + sb.append( getIdentPrefix(--level)); + sb.append("]"); + return sb.toString(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private String encodeMap(Map m, String path, int level) { + level++; + StringBuilder sb = new StringBuilder("{\n"); + for (Entry entry : (Set) m.entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) + if (ignoreNull) + continue; + + String itemPath = (path.endsWith("/")) ? path + entry.getKey() : path + "/" + entry.getKey(); + if (sb.length() > 2) + sb.append(",\n"); + sb.append(getIdentPrefix(level)); + sb.append('"'); + sb.append(entry.getKey().toString()); + sb.append("\":"); + sb.append(encode(entry.getValue(), itemPath, level)); + } + sb.append("\n"); + sb.append(getIdentPrefix(--level)); + sb.append("}"); + + return sb.toString(); + } + + private String getIdentPrefix(int level) { + int numSpaces = indentation * level; + StringBuilder sb = new StringBuilder(numSpaces); + for (int i=0; i < numSpaces; i++) + sb.append(' '); + return sb.toString(); + } +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/DefaultJsonParser.java b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/DefaultJsonParser.java new file mode 100644 index 00000000000..82c0d4b67c8 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/DefaultJsonParser.java @@ -0,0 +1,47 @@ +/* + * 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.serializer.impl.json; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.serializer.Parser; +import org.apache.felix.utils.json.JSONParser; + +public class DefaultJsonParser implements Parser { + + @Override + public Map parse(InputStream in) + { + try { + JSONParser parser = new JSONParser(in); + return parser.getParsed(); + } catch (IOException e) { + Map report = new HashMap<>(); + report.put("error", e.getMessage()); + return report; + } + } + + @Override + public Map parse(CharSequence in) { + JSONParser parser = new JSONParser(in); + return parser.getParsed(); + } +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/DefaultJsonWriter.java b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/DefaultJsonWriter.java new file mode 100644 index 00000000000..81cdc743b13 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/DefaultJsonWriter.java @@ -0,0 +1,128 @@ +/* + * 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.serializer.impl.json; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +import org.apache.felix.serializer.Writer; +import org.osgi.dto.DTO; +import org.osgi.util.converter.Converter; + +public class DefaultJsonWriter implements Writer { + + private final Converter converter; + private final boolean ignoreNull = false; + + public DefaultJsonWriter(Converter c) { + converter = c; + } + + @Override + public String write(Object obj) { + return encode(obj); + } + + @Override + public Map> mapOrderingRules() { + return Collections.emptyMap(); + } + + @Override + public Map> arrayOrderingRules() { + return Collections.emptyMap(); + } + + @SuppressWarnings("rawtypes") + private String encode(Object obj) { + if (obj == null) { + return ignoreNull ? "" : "null"; + } + + if (obj instanceof String) { + return "\"" + (String)obj + "\""; + } else if (obj instanceof Map) { + return encodeMap((Map) obj); + } else if (obj instanceof Collection) { + return encodeCollection((Collection) obj); + } else if (obj instanceof DTO) { + return encodeMap(converter.convert(obj).sourceAsDTO().to(Map.class)); + } else if (obj.getClass().isArray()) { + return encodeCollection(asCollection(obj)); + } else if (obj instanceof Number) { + return obj.toString(); + } else if (obj instanceof Boolean) { + return obj.toString(); + } + + return "\"" + converter.convert(obj).to(String.class) + "\""; + } + + private Collection asCollection(Object arr) { + // Arrays.asList() doesn't work for primitive arrays + int len = Array.getLength(arr); + List l = new ArrayList<>(len); + for (int i=0; i collection) { + StringBuilder sb = new StringBuilder("["); + + boolean first = true; + for (Object o : collection) { + if (first) + first = false; + else + sb.append(','); + + sb.append(encode(o)); + } + + sb.append("]"); + return sb.toString(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private String encodeMap(Map m) { + StringBuilder sb = new StringBuilder("{"); + for (Entry entry : (Set) m.entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) + if (ignoreNull) + continue; + + if (sb.length() > 1) + sb.append(','); + sb.append('"'); + sb.append(entry.getKey().toString()); + sb.append("\":"); + sb.append(encode(entry.getValue())); + } + sb.append("}"); + + return sb.toString(); + } +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/JsonDeserializingImpl.java b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/JsonDeserializingImpl.java new file mode 100644 index 00000000000..e6307797f50 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/JsonDeserializingImpl.java @@ -0,0 +1,90 @@ +/* + * 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.serializer.impl.json; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Scanner; + +import org.apache.felix.serializer.Deserializing; +import org.apache.felix.serializer.Parser; +import org.apache.felix.serializer.impl.Util; +import org.osgi.util.converter.ConversionException; +import org.osgi.util.converter.Converter; + +public class JsonDeserializingImpl implements Deserializing { + private final Type type; + private volatile Converter converter; + private volatile Parser parser; + + public JsonDeserializingImpl(Converter c, Parser p, Type t) { + converter = c; + parser = p; + type = t; + } + + @Override + @SuppressWarnings("unchecked") + public T from(CharSequence in) { + Map m = parser.parse(in); + if (type instanceof Class) + if (m.getClass().isAssignableFrom((Class) type)) + return (T) m; + + return (T) converter.convert(m).to(type); + } + + @Override + public T from(InputStream in) { + return from(in, StandardCharsets.UTF_8); + } + + @Override + public T from(InputStream in, Charset charset) { + try { + byte[] bytes = Util.readStream(in); + String s = new String(bytes, charset); + return from(s); + } catch (IOException e) { + throw new ConversionException("Error reading inputstream", e); + } + } + + @Override + public T from(Readable in) { + try (Scanner s = new Scanner(in)) { + s.useDelimiter("\\Z"); + return from(s.next()); + } + } + + @Override + public Deserializing convertWith(Converter c) { + converter = c; + return this; + } + + @Override + public Deserializing parseWith(Parser p) { + parser = p; + return this; + } +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/JsonSerializerImpl.java b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/JsonSerializerImpl.java new file mode 100644 index 00000000000..733675a979a --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/JsonSerializerImpl.java @@ -0,0 +1,54 @@ +/* + * 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.serializer.impl.json; + +import java.lang.reflect.Type; + +import org.apache.felix.serializer.Deserializing; +import org.apache.felix.serializer.Parser; +import org.apache.felix.serializer.Serializer; +import org.apache.felix.serializer.Serializing; +import org.apache.felix.serializer.Writer; +import org.osgi.util.converter.Converter; +import org.osgi.util.converter.Converters; +import org.osgi.util.converter.TypeReference; + +public class JsonSerializerImpl implements Serializer, Serializer.JsonSerializer { + private final Converter converter = Converters.standardConverter(); + private final Parser parser = new DefaultJsonParser(); + private final Writer writer = new DefaultJsonWriter(converter); + + @Override + public Deserializing deserialize(Class cls) { + return new JsonDeserializingImpl(converter, parser, cls); + } + + @Override + public Serializing serialize(Object obj) { + return new JsonSerializingImpl(converter, writer, obj); + } + + @Override + public Deserializing deserialize(TypeReference ref) { + return new JsonDeserializingImpl(converter, parser, ref.getType()); + } + + @Override @SuppressWarnings("rawtypes") + public Deserializing deserialize(Type type) { + return new JsonDeserializingImpl(converter, parser, type); + } +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/JsonSerializingImpl.java b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/JsonSerializingImpl.java new file mode 100644 index 00000000000..1a4577295f9 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/JsonSerializingImpl.java @@ -0,0 +1,91 @@ +/* + * 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.serializer.impl.json; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.apache.felix.serializer.Serializing; +import org.apache.felix.serializer.Writer; +import org.apache.felix.serializer.impl.AbstractSpecifying; +import org.osgi.util.converter.ConversionException; +import org.osgi.util.converter.Converter; + +public class JsonSerializingImpl extends AbstractSpecifying implements Serializing { + private volatile Converter converter; + private volatile boolean useCustomWriter; + private volatile Writer writer; + private final Object object; + + JsonSerializingImpl(Converter c, Writer w, Object obj) { + converter = c; + writer = w; + object = obj; + } + + @Override + public Appendable to(Appendable out) { + try { + out.append(writer.write(object)); + return out; + } catch (IOException e) { + throw new ConversionException("Problem converting to JSON", e); + } + } + + @Override + public void to(OutputStream os, Charset charset) { + try { + os.write(writer.write(object).getBytes(charset)); + } catch (IOException e) { + throw new ConversionException("Problem converting to JSON", e); + } + } + + @Override + public void to(OutputStream out) throws IOException { + to(out, StandardCharsets.UTF_8); + } + + @Override + public String toString() { + return writer.write(object); + } + + @Override + public Serializing convertWith(Converter c) { + converter = c; + if (!useCustomWriter) + writer = new DefaultJsonWriter(converter); + return this; + } + + @Override + public Serializing writeWith(Writer w) { + writer = w; + return this; + } + + // TODO: what is intended here?? + @Override + public Serializing view() + { + return null; + } +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/JsonWriterFactory.java b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/JsonWriterFactory.java new file mode 100644 index 00000000000..48edaa7e371 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/json/JsonWriterFactory.java @@ -0,0 +1,65 @@ +/* + * 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.serializer.impl.json; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.felix.serializer.Writer; +import org.apache.felix.serializer.WriterFactory; +import org.osgi.util.converter.Converter; + +public class JsonWriterFactory implements WriterFactory, WriterFactory.JsonWriterFactory { + private final Map> mapOrderingRules = new HashMap<>(); + private final Map> arrayOrderingRules = new HashMap<>(); + + @Override + public JsonWriterFactory orderMap(String path, List keyOrder) { + mapOrderingRules.put(path, keyOrder); + return this; + } + + @Override + public WriterFactory orderMap(Map> orderingRules) { + mapOrderingRules.putAll(orderingRules); + return this; + } + + @Override + public WriterFactory orderArray(String path) { + arrayOrderingRules.put(path, null); + return this; + } + + @Override + public WriterFactory orderArray(String path, Comparator comparator) { + arrayOrderingRules.put(path, comparator); + return this; + } + + @Override + public Writer newDefaultWriter(Converter c) { + return new DefaultJsonWriter(c); + } + + @Override + public Writer newDebugWriter(Converter c) { + return new DebugJsonWriter(c, mapOrderingRules, arrayOrderingRules); + } +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/impl/yaml/DefaultYamlParser.java b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/yaml/DefaultYamlParser.java new file mode 100644 index 00000000000..426f176db20 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/yaml/DefaultYamlParser.java @@ -0,0 +1,50 @@ +/* + * 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.serializer.impl.yaml; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.serializer.Parser; +import org.yaml.snakeyaml.Yaml; + +public class DefaultYamlParser implements Parser { + + @Override + public Map parse(InputStream in) + { + Yaml yaml = new Yaml(); + return toMap(yaml.load(in)); + } + + @Override + public Map parse(CharSequence in) { + Yaml yaml = new Yaml(); + return toMap(yaml.load(in.toString())); + } + + @SuppressWarnings( { "unchecked", "rawtypes" } ) + private Map toMap(Object obj) { + if (obj instanceof Map) + return (Map)obj; + + Map map = new HashMap<>(); + map.put("parsed", obj); + return map; + } +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/impl/yaml/DefaultYamlWriter.java b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/yaml/DefaultYamlWriter.java new file mode 100644 index 00000000000..b567e1fd2ec --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/yaml/DefaultYamlWriter.java @@ -0,0 +1,118 @@ +/* + * 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.serializer.impl.yaml; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +import org.apache.felix.serializer.Writer; +import org.osgi.util.converter.Converter; + +public class DefaultYamlWriter implements Writer { + + private final Converter converter; + private final int indentation = 2; + + public DefaultYamlWriter(Converter c) { + converter = c; + } + + @Override + public String write(Object obj) { + return encode(obj, 0).trim(); + } + + @Override + public Map> mapOrderingRules() { + return Collections.emptyMap(); + } + + @Override + public Map> arrayOrderingRules() { + return Collections.emptyMap(); + } + + @SuppressWarnings("rawtypes") + private String encode(Object obj, int level) { + if (obj == null) + return ""; + + if (obj instanceof Map) { + return encodeMap((Map) obj, level); + } else if (obj instanceof Collection) { + return encodeCollection((Collection) obj, level); + } else if (obj.getClass().isArray()) { + return encodeCollection(asCollection(obj), level); + } else if (obj instanceof Number) { + return obj.toString(); + } else if (obj instanceof Boolean) { + return obj.toString(); + } + + return "'" + converter.convert(obj).to(String.class) + "'"; + } + + private Collection asCollection(Object arr) { + // Arrays.asList() doesn't work for primitive arrays + int len = Array.getLength(arr); + List l = new ArrayList<>(len); + for (int i=0; i collection, int level) { + StringBuilder sb = new StringBuilder(); + for (Object o : collection) { + sb.append("\n"); + sb.append(getIdentPrefix(level)); + sb.append("- "); + sb.append(encode(o, level + 1)); + } + return sb.toString(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private String encodeMap(Map m, int level) { + StringBuilder sb = new StringBuilder(); + for (Entry entry : (Set) m.entrySet()) { + sb.append("\n"); + sb.append(getIdentPrefix(level)); + sb.append(entry.getKey().toString()); + sb.append(": "); + sb.append(encode(entry.getValue(), level + 1)); + } + + return sb.toString(); + } + + private String getIdentPrefix(int level) { + int numSpaces = indentation * level; + StringBuilder sb = new StringBuilder(numSpaces); + for (int i=0; i < numSpaces; i++) + sb.append(' '); + return sb.toString(); + } +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/impl/yaml/YamlDeserializingImpl.java b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/yaml/YamlDeserializingImpl.java new file mode 100644 index 00000000000..0bdeb1d9668 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/yaml/YamlDeserializingImpl.java @@ -0,0 +1,90 @@ +/* + * 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.serializer.impl.yaml; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Scanner; + +import org.apache.felix.serializer.Deserializing; +import org.apache.felix.serializer.Parser; +import org.apache.felix.serializer.impl.Util; +import org.osgi.util.converter.ConversionException; +import org.osgi.util.converter.Converter; + +public class YamlDeserializingImpl implements Deserializing { + private volatile Converter converter; + private volatile Parser parser; + private final Type type; + + YamlDeserializingImpl(Converter c, Parser p, Type cls) { + converter = c; + parser = p; + type = cls; + } + + @Override + public T from(InputStream in) { + return from(in, StandardCharsets.UTF_8); + } + + @Override + public T from(InputStream in, Charset charset) { + try { + byte[] bytes = Util.readStream(in); + String s = new String(bytes, charset); + return from(s); + } catch (IOException e) { + throw new ConversionException("Error reading inputstream", e); + } + } + + @Override + public T from(Readable in) { + try (Scanner s = new Scanner(in)) { + s.useDelimiter("\\Z"); + return from(s.next()); + } + } + + @Override + @SuppressWarnings("unchecked") + public T from(CharSequence in) { + Map m = parser.parse(in); + if (type instanceof Class) + if (m.getClass().isAssignableFrom((Class) type)) + return (T) m; + + return (T) converter.convert(m).to(type); + } + + @Override + public Deserializing convertWith(Converter c) { + converter = c; + return this; + } + + @Override + public Deserializing parseWith(Parser p) { + parser = p; + return this; + } +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/impl/yaml/YamlSerializerImpl.java b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/yaml/YamlSerializerImpl.java new file mode 100644 index 00000000000..b902d812b13 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/yaml/YamlSerializerImpl.java @@ -0,0 +1,54 @@ +/* + * 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.serializer.impl.yaml; + +import java.lang.reflect.Type; + +import org.apache.felix.serializer.Deserializing; +import org.apache.felix.serializer.Parser; +import org.apache.felix.serializer.Serializer; +import org.apache.felix.serializer.Serializing; +import org.apache.felix.serializer.Writer; +import org.osgi.util.converter.Converter; +import org.osgi.util.converter.Converters; +import org.osgi.util.converter.TypeReference; + +public class YamlSerializerImpl implements Serializer, Serializer.YamlSerializer { + private final Converter converter = Converters.standardConverter(); + private final Parser parser = new DefaultYamlParser(); + private final Writer writer = new DefaultYamlWriter(converter); + + @Override + public Deserializing deserialize(Class cls) { + return new YamlDeserializingImpl(converter, parser, cls); + } + + @Override + public Deserializing deserialize(TypeReference ref) { + return new YamlDeserializingImpl(converter, parser, ref.getType()); + } + + @Override @SuppressWarnings("rawtypes") + public Deserializing deserialize(Type type) { + return new YamlDeserializingImpl(converter, parser, type); + } + + @Override + public Serializing serialize(Object obj) { + return new YamlSerializingImpl(converter, writer, obj); + } +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/impl/yaml/YamlSerializingImpl.java b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/yaml/YamlSerializingImpl.java new file mode 100644 index 00000000000..b03f03b1f78 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/yaml/YamlSerializingImpl.java @@ -0,0 +1,94 @@ +/* + * 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.serializer.impl.yaml; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.apache.felix.serializer.Serializing; +import org.apache.felix.serializer.Writer; +import org.apache.felix.serializer.impl.AbstractSpecifying; +import org.osgi.util.converter.ConversionException; +import org.osgi.util.converter.Converter; + +public class YamlSerializingImpl extends AbstractSpecifying implements Serializing { + private volatile Converter converter; + private volatile boolean useCustomWriter; + private volatile Writer writer; + private final Object object; + + public YamlSerializingImpl(Converter c, Writer w, Object obj) { + converter = c; + writer = w; + object = obj; + } + + @Override + public Appendable to(Appendable out) { + try { + out.append(writer.write(object)); + return out; + } catch (IOException e) { + throw new ConversionException("Problem converting to YAML", e); + } + } + + + @Override + public void to(OutputStream os) throws IOException { + to(os, StandardCharsets.UTF_8); + } + + @Override + public void to(OutputStream os, Charset charset) { + try { + os.write(writer.write(object).getBytes(charset)); + } catch (IOException e) { + throw new ConversionException("Problem converting to YAML", e); + } + } + + @Override + public String toString() { + return writer.write(object); + } + + @Override + public Serializing convertWith(Converter c) { + converter = c; + if (!useCustomWriter) + writer = new DefaultYamlWriter(converter); + return this; + } + + @Override + public Serializing writeWith(Writer w) { + writer = w; + useCustomWriter = true; + return this; + } + + + // TODO: what is intended here?? + @Override + public Serializing view() + { + return null; + } +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/impl/yaml/YamlWriterFactory.java b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/yaml/YamlWriterFactory.java new file mode 100644 index 00000000000..7833003f949 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/impl/yaml/YamlWriterFactory.java @@ -0,0 +1,62 @@ +/* + * 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.serializer.impl.yaml; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.felix.serializer.Writer; +import org.apache.felix.serializer.WriterFactory; +import org.osgi.util.converter.Converter; + +public class YamlWriterFactory implements WriterFactory, WriterFactory.YamlWriterFactory { + private final Map> orderingRules = new HashMap<>(); + + @Override + public YamlWriterFactory orderMap(String path, List keyOrder) { + orderingRules.put(path,keyOrder); + return this; + } + + @Override + public WriterFactory orderMap(Map> rules) { + orderingRules.putAll(rules); + return this; + } + + @Override + public WriterFactory orderArray(String path) { + return this; + } + + @Override + public WriterFactory orderArray(String path, Comparator comparator) { + return this; + } + + @Override + public Writer newDefaultWriter(Converter c) { + return new DefaultYamlWriter(c); + } + + @Override + public Writer newDebugWriter(Converter c) { + return new DefaultYamlWriter(c); + } +} diff --git a/converter/serializer/src/main/java/org/apache/felix/serializer/package-info.java b/converter/serializer/src/main/java/org/apache/felix/serializer/package-info.java new file mode 100644 index 00000000000..909cea5a839 --- /dev/null +++ b/converter/serializer/src/main/java/org/apache/felix/serializer/package-info.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) OSGi Alliance (2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Converter Package Version 1.0. + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. This package has two types of + * users: the consumers that use the API in this package and the providers that + * implement the API in this package. + *

      + * Example import for consumers using the API in this package: + *

      + * {@code Import-Package: org.osgi.service.serializer; version="[1.0,2.0)"} + *

      + * Example import for providers implementing the API in this package: + *

      + * {@code Import-Package: org.osgi.service.serializer; version="[1.0,1.1)"} + * + * @author $Id: 1b82a2a1db1431c5e4398f368662b5b6fb5f8547 $ + */ +@Version("1.0") +package org.apache.felix.serializer; + +import org.osgi.annotation.versioning.Version; diff --git a/converter/serializer/src/test/java/org/apache/felix/serializer/impl/json/JsonBackingObjectSerializationTest.java b/converter/serializer/src/test/java/org/apache/felix/serializer/impl/json/JsonBackingObjectSerializationTest.java new file mode 100644 index 00000000000..350d1d440f3 --- /dev/null +++ b/converter/serializer/src/test/java/org/apache/felix/serializer/impl/json/JsonBackingObjectSerializationTest.java @@ -0,0 +1,202 @@ +/* + * 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.serializer.impl.json; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.Test; +import org.osgi.dto.DTO; +import org.osgi.util.converter.Converter; +import org.osgi.util.converter.ConverterFunction; +import org.osgi.util.converter.Converters; +import org.osgi.util.converter.TargetRule; + +import static org.junit.Assert.assertEquals; + +public class JsonBackingObjectSerializationTest { + @Test + @SuppressWarnings( "rawtypes" ) + public void testComplexMapSerializationFirstUsingConversion() { + final MyDTOishObject obj = MyDTOishObject.factory( "A", "B" ); + final Map m = Converters + .standardConverter() + .convert(obj) + .sourceAsDTO() + .to(Map.class); + + final String actual = new JsonSerializerImpl() + .serialize(m) + .writeWith(new JsonWriterFactory().newDebugWriter(Converters.standardConverter())) + .toString(); + + assertEquals(EXPECTED, actual); + } + + @Test + public void testComplexMapSerializationWithoutUsingPreConversion() { + final JsonWriterFactory factory = new JsonWriterFactory(); + final String actual = new JsonSerializerImpl() + .serialize(MyDTOishObject.factory("A", "B")) + .writeWith(factory.newDebugWriter(Converters.standardConverter())) + .toString(); + + assertEquals(EXPECTED, actual); + } + + @Test + public void testComplexMapSerializationUsingRule() { + final Converter converter = Converters.newConverterBuilder().rule(new MapTargetRule()).build(); + final JsonWriterFactory factory = new JsonWriterFactory(); + final String actual = new JsonSerializerImpl() + .serialize(MyDTOishObject.factory("A", "B")) + .convertWith(converter) + .writeWith(factory.newDebugWriter(converter)) + .toString(); + + assertEquals(EXPECTED, actual); + } + + @Test + public void testOrderedSerialization() { + final String actual = new JsonSerializerImpl() + .serialize(MyDTOishObject.factory("A", "B")) + .writeWith(new JsonWriterFactory() + .orderMap("/", Arrays.asList("b", "a", "o", "l2", "l1")) + .orderMap("/l2", Arrays.asList("b", "a")) + .orderArray("/l1") + .newDebugWriter(Converters.standardConverter())) + .toString(); + + assertEquals(ORDERED, actual); + } + + public static class MyDTOishObject extends DTO { + public String a; + public String b; + public OtherObject o; + public List l1; + public List l2; + + public MyDTOishObject( String a, String b ) { + this.a = a; + this.b = b; + o = OtherObject.factory(a + a, b + b); + l1 = Stream.of("one", "two", "three", "four").collect(Collectors.toList()); + l2 = Stream.of(OtherObject.factory(a, b), OtherObject.factory(a + a, b + b)).collect(Collectors.toList()); + } + + public static MyDTOishObject factory( String a, String b ) { + return new MyDTOishObject(a, b); + } + } + + public static class OtherObject extends DTO { + public String a; + public String b; + + public OtherObject(String a, String b) { + this.a = a; + this.b = b; + } + + public static OtherObject factory(String a, String b) { + return new OtherObject(a, b); + } + } + + static class MapTargetRule implements TargetRule { + + @Override + public ConverterFunction getFunction() { + return new MapConverterFunction(); + } + + @Override + public Type getTargetType() { + return Map.class; + } + } + + static class MapConverterFunction implements ConverterFunction { + + @Override + public Object apply( Object obj, Type targetType ) throws Exception { + return Converters + .standardConverter() + .convert(obj) + .sourceAsDTO() + .to(targetType); + } + } + + private static final String EXPECTED = + "{\n" + + " \"a\":\"A\",\n" + + " \"b\":\"B\",\n" + + " \"l1\":[\n" + + " \"one\",\n" + + " \"two\",\n" + + " \"three\",\n" + + " \"four\"\n" + + " ],\n" + + " \"l2\":[\n" + + " {\n" + + " \"a\":\"A\",\n" + + " \"b\":\"B\"\n" + + " },\n" + + " {\n" + + " \"a\":\"AA\",\n" + + " \"b\":\"BB\"\n" + + " }\n" + + " ],\n" + + " \"o\":{\n" + + " \"a\":\"AA\",\n" + + " \"b\":\"BB\"\n" + + " }\n" + + "}"; + + private static final String ORDERED = + "{\n" + + " \"b\":\"B\",\n" + + " \"a\":\"A\",\n" + + " \"o\":{\n" + + " \"a\":\"AA\",\n" + + " \"b\":\"BB\"\n" + + " },\n" + + " \"l2\":[\n" + + " {\n" + + " \"b\":\"B\",\n" + + " \"a\":\"A\"\n" + + " },\n" + + " {\n" + + " \"b\":\"BB\",\n" + + " \"a\":\"AA\"\n" + + " }\n" + + " ],\n" + + " \"l1\":[\n" + + " \"four\",\n" + + " \"one\",\n" + + " \"three\",\n" + + " \"two\"\n" + + " ]\n" + + "}"; +} diff --git a/converter/serializer/src/test/java/org/apache/felix/serializer/impl/json/JsonParserTest.java b/converter/serializer/src/test/java/org/apache/felix/serializer/impl/json/JsonParserTest.java new file mode 100644 index 00000000000..5aa41bcdb14 --- /dev/null +++ b/converter/serializer/src/test/java/org/apache/felix/serializer/impl/json/JsonParserTest.java @@ -0,0 +1,119 @@ +/* + * 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.serializer.impl.json; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.felix.serializer.Parser; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class JsonParserTest { + private Parser parser; + + @Before + public void setup() { + parser = new DefaultJsonParser(); + } + + @Test + public void testJsonSimple() { + String json = "{\"hi\": \"ho\", \"ha\": true}"; + Map m = parser.parse(json); + assertEquals(2, m.size()); + assertEquals("ho", m.get("hi")); + assertTrue((Boolean) m.get("ha")); + } + + @Test + public void testJsonWithNewline() { + String json = "" + + "{\n" + + " \"hi\": \"ho\",\n" + + " \"ha\": [\n" + + " \"one\",\n" + + " \"two\",\n" + + " \"three\"\n" + + " ]\n" + + "}\n" + + "\n"; + Map m = parser.parse(json); + assertEquals(2, m.size()); + assertEquals("ho", m.get("hi")); + assertEquals(3, ((List)m.get("ha")).size()); + } + + @Test + public void testJsonWithCRLF() { + String json = "" + + "{\r\n" + + " \"hi\": \"ho\",\r\n" + + " \"ha\": [\r\n" + + " \"one\",\r\n" + + " \"two\",\r\n" + + " \"three\"\r\n" + + " ]\r\n" + + "}\r\n" + + "\r\n"; + Map m = parser.parse(json); + assertEquals(2, m.size()); + assertEquals("ho", m.get("hi")); + assertEquals(3, ((List)m.get("ha")).size()); + } + + @Test + @SuppressWarnings("unchecked") + public void testJsonComplex() { + String json = "{\"a\": [1,2,3,4,5], \"b\": {\"x\": 12, \"y\": 42, \"z\": {\"test test\": \"hello hello\"}}, \"ddd\": 12.34}"; + Map m = parser.parse(json); + assertEquals(3, m.size()); + assertEquals(Arrays.asList(1L, 2L, 3L, 4L, 5L), m.get("a")); + Map mb = (Map) m.get("b"); + assertEquals(3, mb.size()); + assertEquals(12L, mb.get("x")); + assertEquals(42L, mb.get("y")); + Map mz = (Map) mb.get("z"); + assertEquals(1, mz.size()); + assertEquals("hello hello", mz.get("test test")); + assertEquals(12.34d, ((Double) m.get("ddd")).doubleValue(), 0.0001d); + } + + @Test + public void testJsonArray() { + String json = "{\"abc\": [\"x\", \"y\", \"z\"]}"; + Map m = parser.parse(json); + assertEquals(1, m.size()); + assertEquals(Arrays.asList("x", "y", "z"), m.get("abc")); + } + + @Test + public void testEmptyJsonArray() { + String json = "{\"abc\": {\"def\": []}}"; + Map m = parser.parse(json); + assertEquals(1, m.size()); + Map result = new HashMap<>(); + result.put("def", Collections.emptyList()); + assertEquals(result, m.get("abc")); + } +} diff --git a/converter/serializer/src/test/java/org/apache/felix/serializer/impl/json/JsonSerializationTest.java b/converter/serializer/src/test/java/org/apache/felix/serializer/impl/json/JsonSerializationTest.java new file mode 100644 index 00000000000..a3cc1b370a7 --- /dev/null +++ b/converter/serializer/src/test/java/org/apache/felix/serializer/impl/json/JsonSerializationTest.java @@ -0,0 +1,89 @@ +/* + * 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.serializer.impl.json; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class JsonSerializationTest { + @Test + @SuppressWarnings("unchecked") + public void testComplexMapSerialization() { + Map m = new LinkedHashMap<>(); + m.put("sKey", "a string"); + m.put("iKey", 42); + m.put("bKey", true); + m.put("noKey", null); + m.put("simpleArray", new int[] {1,2,3}); + + Map m1 = new LinkedHashMap<>(); + m1.put("a", 1L); + m1.put("b", "hello"); + m.put("simpleObject", m1); + + String expected = "{\"sKey\":\"a string\"," + + "\"iKey\":42," + + "\"bKey\":true," + + "\"noKey\":null," + + "\"simpleArray\":[1,2,3]," + + "\"simpleObject\":{\"a\":1,\"b\":\"hello\"}}"; + assertEquals(expected, new JsonSerializerImpl().serialize(m).toString()); + + Map dm = new JsonSerializerImpl().deserialize(Map.class).from(expected); + Map expected2 = new LinkedHashMap<>(); + expected2.put("sKey", "a string"); + expected2.put("iKey", 42L); + expected2.put("bKey", true); + expected2.put("noKey", null); + expected2.put("simpleArray", Arrays.asList(1L,2L,3L)); + expected2.put("simpleObject", m1); + assertEquals(expected2, dm); + } + + @Test + public void testComplexMapSerialization2() { + Map m2 = new LinkedHashMap<>(); + m2.put("yes", Boolean.TRUE); + m2.put("no", Collections.singletonMap("maybe", false)); + + Map cm = new LinkedHashMap<>(); + cm.put("list", Arrays.asList( + Collections.singletonMap("x", "y"), + Collections.singletonMap("x", "b"))); + cm.put("embedded", m2); + + String expected = "{\"list\":[{\"x\":\"y\"},{\"x\":\"b\"}]," + + "\"embedded\":" + + "{\"yes\":true,\"no\":{\"maybe\":false}}}"; + assertEquals(expected, new JsonSerializerImpl().serialize(cm).toString()); + } + + @Test + public void testEmptyMapSerialization() { + Map m = new LinkedHashMap<>(); + String expected = "{}"; + assertEquals(expected, new JsonSerializerImpl().serialize(m).toString()); + Map m2 = new JsonSerializerImpl().deserialize(Map.class).from(expected); + assertEquals(m, m2); + } +} diff --git a/converter/serializer/src/test/java/org/apache/felix/serializer/impl/json/JsonSerializerTest.java b/converter/serializer/src/test/java/org/apache/felix/serializer/impl/json/JsonSerializerTest.java new file mode 100644 index 00000000000..c662f70c9dc --- /dev/null +++ b/converter/serializer/src/test/java/org/apache/felix/serializer/impl/json/JsonSerializerTest.java @@ -0,0 +1,167 @@ +/* + * 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.serializer.impl.json; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.serializer.impl.json.MyDTO.Count; +import org.apache.felix.serializer.impl.json.MyEmbeddedDTO.Alpha; +import org.apache.sling.commons.json.JSONException; +import org.apache.sling.commons.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.osgi.util.converter.Converter; +import org.osgi.util.converter.Converters; +import org.osgi.util.converter.TypeReference; +import org.osgi.util.converter.TypeRule; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class JsonSerializerTest { + private Converter converter; + + @Before + public void setUp() { + converter = Converters.standardConverter(); + } + + @After + public void tearDown() { + converter = null; + } + + @Test + public void testJSONCodec() throws Exception { + Map m1 = new HashMap<>(); + m1.put("x", true); + m1.put("y", null); + Map m = new HashMap<>(); + m.put(1, 11L); + m.put("ab", "cd"); + m.put(true, m1); + + JsonSerializerImpl jsonCodec = new JsonSerializerImpl(); + String json = jsonCodec.serialize(m).toString(); + + JSONObject jo = new JSONObject(json); + assertEquals(11, jo.getInt("1")); + assertEquals("cd", jo.getString("ab")); + JSONObject jo2 = jo.getJSONObject("true"); + assertEquals(true, jo2.getBoolean("x")); + assertTrue(jo2.isNull("y")); + + @SuppressWarnings("rawtypes") + Map m2 = jsonCodec.deserialize(Map.class).from(json); + // m2 is not exactly equal to m, as the keys are all strings now, this is unavoidable with JSON + assertEquals(m.size(), m2.size()); + assertEquals(m.get(1), m2.get("1")); + assertEquals(m.get("ab"), m2.get("ab")); + assertEquals(m.get(true), m2.get("true")); + } + + @Test + public void testCodecWithAdapter() throws JSONException { + Map m1 = new HashMap<>(); + m1.put("f", new Foo("fofofo")); + Map> m = new HashMap<>(); + m.put("submap", m1); + + Converter ca = converter.newConverterBuilder(). + rule(new TypeRule(Foo.class, String.class, Foo::tsFun)). + rule(new TypeRule(String.class, Foo.class, v -> Foo.fsFun(v))).build(); + + JsonSerializerImpl jsonCodec = new JsonSerializerImpl(); + String json = jsonCodec.serialize(m).convertWith(ca).toString(); + + JSONObject jo = new JSONObject(json); + assertEquals(1, jo.length()); + JSONObject jo1 = jo.getJSONObject("submap"); + assertEquals("", jo1.getString("f")); + + // And convert back + Map> m2 = jsonCodec.deserialize(new TypeReference>>(){}). + convertWith(ca).from(json); + assertEquals(m, m2); + } + + @Test + public void testDTO() { + MyDTO dto = new MyDTO(); + dto.count = Count.ONE; + dto.ping = "'"; + dto.pong = Long.MIN_VALUE; + + MyEmbeddedDTO embedded = new MyEmbeddedDTO(); + embedded.alpha = Alpha.B; + embedded.marco = "jo !"; + embedded.polo = 327; + dto.embedded = embedded; + + JsonSerializerImpl jsonCodec = new JsonSerializerImpl(); + String json = jsonCodec.serialize(dto).toString(); + // NOTE: cannot predict ordering, so test fails intermittently +// assertEquals( +// "{\"count\":\"ONE\",\"ping\":\"'\"," +// + "\"embedded\":{\"polo\":327,\"alpha\":\"B\",\"marco\":\"jo !\"}," +// + "\"pong\":-9223372036854775808}", +// json); + + MyDTO dto2 = jsonCodec.deserialize(MyDTO.class).from(json); + assertEquals(Count.ONE, dto2.count); + assertEquals("'", dto2.ping); + assertEquals(Long.MIN_VALUE, dto2.pong); + MyEmbeddedDTO embedded2 = dto2.embedded; + assertEquals(Alpha.B, embedded2.alpha); + assertEquals("jo !", embedded2.marco); + assertEquals(327, embedded2.polo); + } + + static class Foo { + private final String val; + + public Foo(String s) { + val = s; + } + + public String tsFun() { + return "<" + val + ">"; + } + + public static Foo fsFun(String s) { + return new Foo(s.substring(1, s.length() - 1)); + } + + @Override + public int hashCode() { + return val.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (!(obj instanceof Foo)) + return false; + + Foo f = (Foo) obj; + return f.val.equals(val); + } + } +} diff --git a/converter/serializer/src/test/java/org/apache/felix/serializer/impl/json/MyDTO.java b/converter/serializer/src/test/java/org/apache/felix/serializer/impl/json/MyDTO.java new file mode 100644 index 00000000000..6a293730a1d --- /dev/null +++ b/converter/serializer/src/test/java/org/apache/felix/serializer/impl/json/MyDTO.java @@ -0,0 +1,32 @@ +/* + * 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.serializer.impl.json; + +import org.osgi.dto.DTO; + +public class MyDTO extends DTO { + public enum Count { ONE, TWO, THREE } + + public Count count; + + public String ping; + + public long pong; + + public MyEmbeddedDTO embedded; +} + diff --git a/converter/serializer/src/test/java/org/apache/felix/serializer/impl/json/MyEmbeddedDTO.java b/converter/serializer/src/test/java/org/apache/felix/serializer/impl/json/MyEmbeddedDTO.java new file mode 100644 index 00000000000..1906c30d182 --- /dev/null +++ b/converter/serializer/src/test/java/org/apache/felix/serializer/impl/json/MyEmbeddedDTO.java @@ -0,0 +1,29 @@ +/* + * 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.serializer.impl.json; + +import org.osgi.dto.DTO; + +public class MyEmbeddedDTO extends DTO { + public enum Alpha { A, B, C } + + public Alpha alpha; + + public String marco; + + public long polo; +} diff --git a/converter/serializer/src/test/java/org/apache/felix/serializer/impl/yaml/YamlSerializationTest.java b/converter/serializer/src/test/java/org/apache/felix/serializer/impl/yaml/YamlSerializationTest.java new file mode 100644 index 00000000000..056d28ee5a5 --- /dev/null +++ b/converter/serializer/src/test/java/org/apache/felix/serializer/impl/yaml/YamlSerializationTest.java @@ -0,0 +1,105 @@ +/* + * 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.serializer.impl.yaml; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.felix.serializer.impl.json.JsonSerializerImpl; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class YamlSerializationTest { + @Test + @SuppressWarnings("unchecked") + public void testComplexMapSerialization() { + Map m = new LinkedHashMap<>(); + m.put("sKey", "a string"); + m.put("iKey", 42); + m.put("bKey", true); + m.put("noKey", null); + m.put("simpleArray", new int[] {1,2,3}); + + Map m1 = new LinkedHashMap<>(); + m1.put("a", 1L); + m1.put("b", "hello"); + m.put("simpleObject", m1); + + String expected = "sKey: 'a string'\n" + + "iKey: 42\n" + + "bKey: true\n" + + "noKey: \n" + + "simpleArray: \n" + + " - 1\n" + + " - 2\n" + + " - 3\n" + + "simpleObject: \n" + + " a: 1\n" + + " b: 'hello'"; + assertEquals(expected, new YamlSerializerImpl().serialize(m).toString().trim()); + + Map dm = new YamlSerializerImpl().deserialize(Map.class).from(expected); + Map expected2 = new LinkedHashMap<>(); + expected2.put("sKey", "a string"); + expected2.put("iKey", 42); + expected2.put("bKey", true); + expected2.put("noKey", null); + expected2.put("simpleArray", Arrays.asList(1,2,3)); + + Map m2 = new LinkedHashMap<>(); + m2.put("a", 1); + m2.put("b", "hello"); + expected2.put("simpleObject", m2); + assertEquals(expected2, dm); + } + + @Test + public void testComplexMapSerialization2() { + Map m2 = new LinkedHashMap<>(); + m2.put("yes", Boolean.TRUE); + m2.put("no", Collections.singletonMap("maybe", false)); + + Map cm = new LinkedHashMap<>(); + cm.put("list", Arrays.asList( + Collections.singletonMap("x", "y"), + Collections.singletonMap("x", "b"))); + cm.put("embedded", m2); + + String expected = "list: \n" + + " - \n" + + " x: 'y'\n" + + " - \n" + + " x: 'b'\n" + + "embedded: \n" + + " yes: true\n" + + " no: \n" + + " maybe: false"; + assertEquals(expected, new YamlSerializerImpl().serialize(cm).toString().trim()); + } + + @Test + public void testEmptyMapSerialization() { + Map m = new LinkedHashMap<>(); + String expected = "{}"; + assertEquals(expected, new JsonSerializerImpl().serialize(m).toString()); + Map m2 = new JsonSerializerImpl().deserialize(Map.class).from(expected); + assertEquals(m, m2); + } +} diff --git a/converter/serializer/src/test/java/org/apache/felix/serializer/impl/yaml/YamlSerializerTest.java b/converter/serializer/src/test/java/org/apache/felix/serializer/impl/yaml/YamlSerializerTest.java new file mode 100644 index 00000000000..86403e58234 --- /dev/null +++ b/converter/serializer/src/test/java/org/apache/felix/serializer/impl/yaml/YamlSerializerTest.java @@ -0,0 +1,128 @@ +/* + * 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.serializer.impl.yaml; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.osgi.util.converter.Converter; +import org.osgi.util.converter.Converters; +import org.osgi.util.converter.TypeReference; +import org.osgi.util.converter.TypeRule; + +import static org.junit.Assert.assertEquals; + +public class YamlSerializerTest { + private Converter converter; + + @Before + public void setUp() { + converter = Converters.standardConverter(); + } + + @After + public void tearDown() { + converter = null; + } + + @Test + public void testYAMLCodec() throws Exception { + Map m1 = new HashMap<>(); + m1.put("x", true); + m1.put("y", null); + Map m = new HashMap<>(); + m.put(1, 11L); + m.put("ab", "cd"); + m.put(true, m1); + + YamlSerializerImpl yamlCodec = new YamlSerializerImpl(); + String yaml = yamlCodec.serialize(m).toString(); + + assertEquals("1: 11\n" + + "ab: 'cd'\n" + + "true: \n" + + " x: true\n" + + " y:", yaml); + + @SuppressWarnings("rawtypes") + Map m2 = yamlCodec.deserialize(Map.class).from(yaml); + // m2 is not exactly equal to m, as the keys are all strings now, this is unavoidable with YAML + assertEquals(m.size(), m2.size()); + assertEquals(converter.convert(m.get(1)).to(int.class), + converter.convert(m2.get(1)).to(int.class)); + assertEquals(m.get("ab"), m2.get("ab")); + assertEquals(m.get(true), m2.get(true)); + } + + @Test + public void testCodecWithAdapter() { + Map m1 = new HashMap<>(); + m1.put("f", new Foo("fofofo")); + Map> m = new HashMap<>(); + m.put("submap", m1); + + Converter ca = converter.newConverterBuilder(). + rule(new TypeRule(Foo.class, String.class, Foo::tsFun)). + rule(new TypeRule(String.class, Foo.class, v -> Foo.fsFun(v))).build(); + + YamlSerializerImpl yamlCodec = new YamlSerializerImpl(); + String yaml = yamlCodec.serialize(m).convertWith(ca).toString(); + + assertEquals("submap: \n" + + " f: ''", yaml); + + // And convert back + Map> m2 = yamlCodec.deserialize(new TypeReference>>(){}). + convertWith(ca).from(yaml); + assertEquals(m, m2); + } + + static class Foo { + private final String val; + + public Foo(String s) { + val = s; + } + + public String tsFun() { + return "<" + val + ">"; + } + + public static Foo fsFun(String s) { + return new Foo(s.substring(1, s.length() - 1)); + } + + @Override + public int hashCode() { + return val.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (!(obj instanceof Foo)) + return false; + + Foo f = (Foo) obj; + return f.val.equals(val); + } + } +} diff --git a/coordinator/pom.xml b/coordinator/pom.xml new file mode 100644 index 00000000000..0ce28c444d6 --- /dev/null +++ b/coordinator/pom.xml @@ -0,0 +1,110 @@ + + + + 4.0.0 + + org.apache.felix + felix-parent + 6 + ../pom/pom.xml + + + org.apache.felix.coordinator + bundle + + Apache Felix Coordinator Service + + Implementation of the OSGi Coordinator Service Specification 1.0 + + 1.0.3-SNAPSHOT + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/coordinator + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/coordinator + http://svn.apache.org/viewvc/felix/trunk/coordinator + + + + + + org.apache.felix + maven-bundle-plugin + 3.2.0 + true + + + osgi + + ${project.artifactId} + + + http://felix.apache.org/site/apache-felix-coordination-service.html + + + org.osgi.service.log;version="[1.3,2)" + + + + org.osgi.service.log;version="[1.3,2)";resolution:=optional, + * + + + org.osgi.service.coordinator + + + org.apache.felix.coordinator.impl.* + + + org.apache.felix.coordinator.impl.Activator + + + osgi.service;objectClass:List<String>="org.osgi.service.coordinator.Coordinator"; + uses:="org.osgi.service.coordinator" + + + + + + + + + + org.osgi + org.osgi.core + 4.3.0 + provided + + + org.osgi + org.osgi.compendium + 5.0.0 + provided + + + + junit + junit + 4.12 + test + + + diff --git a/coordinator/src/main/appended-resources/META-INF/DEPENDENCIES b/coordinator/src/main/appended-resources/META-INF/DEPENDENCIES new file mode 100644 index 00000000000..500831da0ae --- /dev/null +++ b/coordinator/src/main/appended-resources/META-INF/DEPENDENCIES @@ -0,0 +1,21 @@ +I. Included Software + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2012). +Licensed under the Apache License 2.0. + +II. Used Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2012). +Licensed under the Apache License 2.0. + + +III. License Summary +- Apache License 2.0 diff --git a/coordinator/src/main/appended-resources/META-INF/NOTICE b/coordinator/src/main/appended-resources/META-INF/NOTICE new file mode 100755 index 00000000000..e3e5e7c961b --- /dev/null +++ b/coordinator/src/main/appended-resources/META-INF/NOTICE @@ -0,0 +1,4 @@ +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2012). +Licensed under the Apache License 2.0. diff --git a/coordinator/src/main/java/org/apache/felix/coordinator/impl/Activator.java b/coordinator/src/main/java/org/apache/felix/coordinator/impl/Activator.java new file mode 100644 index 00000000000..d8b4581652f --- /dev/null +++ b/coordinator/src/main/java/org/apache/felix/coordinator/impl/Activator.java @@ -0,0 +1,85 @@ +/* + * 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.coordinator.impl; + +import java.util.Hashtable; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.coordinator.Coordinator; + +public class Activator implements BundleActivator +{ + + private CoordinationMgr mgr; + + private ServiceRegistration coordinatorService; + + public void start(final BundleContext context) + { + LogWrapper.setContext(context); + + mgr = new CoordinationMgr(); + + final ServiceFactory factory = new CoordinatorFactory(mgr); + final Hashtable props = new Hashtable(); + props.put(Constants.SERVICE_DESCRIPTION, "Coordinator Service Implementation"); + props.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation"); + coordinatorService = context.registerService(Coordinator.class.getName(), factory, props); + } + + public void stop(final BundleContext context) + { + if (coordinatorService != null) + { + coordinatorService.unregister(); + coordinatorService = null; + } + + mgr.cleanUp(); + + LogWrapper.setContext(null); + } + + static final class CoordinatorFactory implements ServiceFactory + { + + private final CoordinationMgr mgr; + + CoordinatorFactory(final CoordinationMgr mgr) + { + this.mgr = mgr; + } + + public Object getService(final Bundle bundle, final ServiceRegistration registration) + { + return new CoordinatorImpl(bundle, mgr); + } + + public void ungetService(final Bundle bundle, final ServiceRegistration registration, final Object service) + { + ((CoordinatorImpl) service).dispose(); + } + + } +} diff --git a/coordinator/src/main/java/org/apache/felix/coordinator/impl/CoordinationHolder.java b/coordinator/src/main/java/org/apache/felix/coordinator/impl/CoordinationHolder.java new file mode 100644 index 00000000000..65d55ba46c6 --- /dev/null +++ b/coordinator/src/main/java/org/apache/felix/coordinator/impl/CoordinationHolder.java @@ -0,0 +1,186 @@ +/* + * 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.coordinator.impl; + +import java.util.List; +import java.util.Map; + +import org.osgi.framework.Bundle; +import org.osgi.service.coordinator.Coordination; +import org.osgi.service.coordinator.Participant; + +/** + * This is a simple wrapper around a {@link CoordinationImpl} to handle + * orphaned coordinations. + * While clients of the coordinator service get a holder object, + * all internal classes hold the real coordination impl instead. + * Therefore if no clients holds a reference to a holder anymore + * we can assume that the coordination is orphaned and remove it. + */ +public class CoordinationHolder implements Coordination { + + private CoordinationImpl coordination; + + public void setCoordination(final CoordinationImpl coordination) + { + this.coordination = coordination; + } + + /** + * @see org.osgi.service.coordinator.Coordination#addParticipant(org.osgi.service.coordinator.Participant) + */ + public void addParticipant(final Participant participant) { + coordination.addParticipant(participant); + } + + /** + * @see org.osgi.service.coordinator.Coordination#end() + */ + public void end() { + coordination.end(); + } + + /** + * @see org.osgi.service.coordinator.Coordination#extendTimeout(long) + */ + public long extendTimeout(final long timeMillis) { + return coordination.extendTimeout(timeMillis); + } + + /** + * @see org.osgi.service.coordinator.Coordination#fail(java.lang.Throwable) + */ + public boolean fail(final Throwable cause) { + return coordination.fail(cause); + } + + /** + * @see org.osgi.service.coordinator.Coordination#getBundle() + */ + public Bundle getBundle() { + return coordination.getBundle(); + } + + /** + * @see org.osgi.service.coordinator.Coordination#getEnclosingCoordination() + */ + public Coordination getEnclosingCoordination() { + return coordination.getEnclosingCoordination(); + } + + /** + * @see org.osgi.service.coordinator.Coordination#getFailure() + */ + public Throwable getFailure() { + return coordination.getFailure(); + } + + /** + * @see org.osgi.service.coordinator.Coordination#getId() + */ + public long getId() { + return coordination.getId(); + } + + /** + * @see org.osgi.service.coordinator.Coordination#getName() + */ + public String getName() { + return coordination.getName(); + } + + /** + * @see org.osgi.service.coordinator.Coordination#getParticipants() + */ + public List getParticipants() { + return coordination.getParticipants(); + } + + /** + * @see org.osgi.service.coordinator.Coordination#getThread() + */ + public Thread getThread() { + return coordination.getThread(); + } + + /** + * @see org.osgi.service.coordinator.Coordination#getVariables() + */ + public Map, Object> getVariables() { + return coordination.getVariables(); + } + + /** + * @see org.osgi.service.coordinator.Coordination#isTerminated() + */ + public boolean isTerminated() { + return coordination.isTerminated(); + } + + /** + * @see org.osgi.service.coordinator.Coordination#join(long) + */ + public void join(final long timeMillis) throws InterruptedException { + coordination.join(timeMillis); + } + + /** + * @see org.osgi.service.coordinator.Coordination#push() + */ + public Coordination push() { + coordination.push(); + return this; + } + + @Override + public boolean equals(final Object object) { + if ( object instanceof CoordinationImpl ) + { + return coordination.equals(object); + } + if (!(object instanceof CoordinationHolder)) + { + return false; + } + return coordination.equals(((CoordinationHolder)object).coordination); + } + + @Override + public int hashCode() { + return coordination.hashCode(); + } + + @Override + public String toString() { + return coordination.toString(); + } + + @Override + protected void finalize() throws Throwable { + if ( !this.coordination.isTerminated() ) + { + this.coordination.fail(Coordination.ORPHANED); + } + super.finalize(); + } + + public Coordination getCoordination() { + return this.coordination; + } +} diff --git a/coordinator/src/main/java/org/apache/felix/coordinator/impl/CoordinationImpl.java b/coordinator/src/main/java/org/apache/felix/coordinator/impl/CoordinationImpl.java new file mode 100644 index 00000000000..59994603ecc --- /dev/null +++ b/coordinator/src/main/java/org/apache/felix/coordinator/impl/CoordinationImpl.java @@ -0,0 +1,559 @@ +/* + * 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.coordinator.impl; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TimerTask; + +import org.osgi.framework.Bundle; +import org.osgi.service.coordinator.Coordination; +import org.osgi.service.coordinator.CoordinationException; +import org.osgi.service.coordinator.CoordinationPermission; +import org.osgi.service.coordinator.Participant; + +public class CoordinationImpl implements Coordination +{ + + private enum State { + /** Active */ + ACTIVE, + + /** failed() called */ + FAILED, + + /** Coordination termination started */ + TERMINATING, + + /** Coordination completed */ + TERMINATED + } + + private final WeakReference holderRef; + + private final CoordinatorImpl owner; + + private final long id; + + private final String name; + + private long deadLine; + + /** + * Access to this field must be synchronized as long as the expected state + * is {@link State#ACTIVE}. Once the state has changed, further updates to this + * instance will not take place any more and the state will only be modified + * by the thread successfully setting the state to {@link State#TERMINATING}. + */ + private volatile State state; + + private Throwable failReason; + + private final ArrayList participants; + + private final Map, Object> variables; + + private TimerTask timeoutTask; + + private Thread associatedThread; + + private final Object waitLock = new Object(); + + public static CoordinationMgr.CreationResult create(final CoordinatorImpl owner, final long id, final String name, final long timeOutInMs) + { + final CoordinationMgr.CreationResult result = new CoordinationMgr.CreationResult(); + result.holder = new CoordinationHolder(); + result.coordination = new CoordinationImpl(owner, id, name, timeOutInMs, result.holder); + return result; + } + + private CoordinationImpl(final CoordinatorImpl owner, + final long id, + final String name, + final long timeOutInMs, + final CoordinationHolder holder) + { + this.owner = owner; + this.id = id; + this.name = name; + this.state = State.ACTIVE; + this.participants = new ArrayList(); + this.variables = new HashMap, Object>(); + this.deadLine = (timeOutInMs > 0) ? System.currentTimeMillis() + timeOutInMs : 0; + holder.setCoordination(this); + this.holderRef = new WeakReference(holder); + + scheduleTimeout(deadLine); + } + + /** + * @see org.osgi.service.coordinator.Coordination#getId() + */ + public long getId() + { + return this.id; + } + + /** + * @see org.osgi.service.coordinator.Coordination#getName() + */ + public String getName() + { + return name; + } + + /** + * @see org.osgi.service.coordinator.Coordination#fail(java.lang.Throwable) + */ + public boolean fail(final Throwable reason) + { + this.owner.checkPermission(name, CoordinationPermission.PARTICIPATE); + if ( reason == null) + { + throw new IllegalArgumentException("Reason must not be null"); + } + if (startTermination()) + { + this.failReason = reason; + + final List releaseList = new ArrayList(); + synchronized ( this.participants ) + { + releaseList.addAll(this.participants); + this.participants.clear(); + } + // consider failure reason (if not null) + for (int i=releaseList.size()-1;i>=0;i--) + { + final Participant part = releaseList.get(i); + try + { + part.failed(this); + } + catch (final Exception e) + { + LogWrapper.getLogger() + .log(LogWrapper.LOG_ERROR, "Participant threw exception during call to fail()", e); + } + + // release the participant for other coordinations + owner.releaseParticipant(part); + } + + this.owner.unregister(this, false); + state = State.FAILED; + + synchronized (this.waitLock) + { + this.waitLock.notifyAll(); + } + + return true; + } + return false; + } + + /** + * @see org.osgi.service.coordinator.Coordination#end() + */ + public void end() + { + this.owner.checkPermission(name, CoordinationPermission.INITIATE); + if ( !this.isTerminated() && this.associatedThread != null && Thread.currentThread() != this.associatedThread ) + { + throw new CoordinationException("Coordination is associated with different thread", this, CoordinationException.WRONG_THREAD); + } + + if (startTermination()) + { + + final CoordinationException nestedFailed = this.owner.endNestedCoordinations(this); + if ( nestedFailed != null ) + { + this.failReason = nestedFailed; + } + boolean partialFailure = false; + this.owner.unregister(this, true); + + final List releaseList = new ArrayList(); + synchronized ( this.participants ) + { + releaseList.addAll(this.participants); + this.participants.clear(); + } + // consider failure reason (if not null) + for (int i=releaseList.size()-1;i>=0;i--) + { + final Participant part = releaseList.get(i); + try + { + if ( this.failReason != null ) + { + part.failed(this); + } + else + { + part.ended(this); + } + } + catch (final Exception e) + { + LogWrapper.getLogger() + .log(LogWrapper.LOG_ERROR, "Participant threw exception during call to fail()", e); + partialFailure = true; + } + + // release the participant for other coordinations + owner.releaseParticipant(part); + } + + state = State.TERMINATED; + + synchronized (this.waitLock) + { + this.waitLock.notifyAll(); + } + + this.associatedThread = null; + + if ( this.failReason != null ) + { + throw new CoordinationException("Nested coordination failed", this, + CoordinationException.FAILED, this.failReason); + } + if (partialFailure) + { + throw new CoordinationException("One or more participants threw while ending the coordination", this, + CoordinationException.PARTIALLY_ENDED); + } + } + else if ( state == State.FAILED ) + { + this.owner.unregister(this, true); + state = State.TERMINATED; + throw new CoordinationException("Coordination failed", this, CoordinationException.FAILED, failReason); + } + else + { + // already terminated + throw new CoordinationException("Coordination " + id + "/" + name + " has already terminated", this, + CoordinationException.ALREADY_ENDED); + } + } + + + /** + * @see org.osgi.service.coordinator.Coordination#getParticipants() + */ + public List getParticipants() + { + this.owner.checkPermission(name, CoordinationPermission.INITIATE); + // synchronize access to the state to prevent it from being changed + // while we create a copy of the participant list + synchronized (this) + { + if (state == State.ACTIVE) + { + synchronized ( this.participants ) + { + return new ArrayList(participants); + } + } + } + + return Collections. emptyList(); + } + + /** + * @see org.osgi.service.coordinator.Coordination#getFailure() + */ + public Throwable getFailure() + { + this.owner.checkPermission(name, CoordinationPermission.INITIATE); + return failReason; + } + + + /** + * @see org.osgi.service.coordinator.Coordination#addParticipant(org.osgi.service.coordinator.Participant) + */ + public void addParticipant(final Participant p) + { + this.owner.checkPermission(name, CoordinationPermission.PARTICIPATE); + if ( p == null ) { + throw new IllegalArgumentException("Participant must not be null"); + } + // ensure participant only participates on a single coordination + // this blocks until the participant can participate or until + // a timeout occurs (or a deadlock is detected) + owner.lockParticipant(p, this); + + // synchronize access to the state to prevent it from being changed + // while adding the participant + synchronized (this) + { + if (isTerminated()) + { + owner.releaseParticipant(p); + + throw new CoordinationException("Cannot add Participant " + p + " to terminated Coordination", this, + (getFailure() != null) ? CoordinationException.FAILED : CoordinationException.ALREADY_ENDED, getFailure()); + } + + synchronized ( this.participants ) + { + boolean found = false; + final Iterator iter = this.participants.iterator(); + while ( !found && iter.hasNext() ) + { + if ( iter.next() == p ) + { + found = true; + } + } + if (!found) + { + participants.add(p); + } + + } + } + } + + /** + * @see org.osgi.service.coordinator.Coordination#getVariables() + */ + public Map, Object> getVariables() + { + this.owner.checkPermission(name, CoordinationPermission.PARTICIPATE); + return variables; + } + + /** + * @see org.osgi.service.coordinator.Coordination#extendTimeout(long) + */ + public long extendTimeout(final long timeOutInMs) + { + this.owner.checkPermission(name, CoordinationPermission.PARTICIPATE); + if ( timeOutInMs < 0 ) + { + throw new IllegalArgumentException("Timeout must not be negative"); + } + if ( this.deadLine > 0 ) + { + synchronized (this) + { + if (isTerminated()) + { + throw new CoordinationException("Cannot extend timeout on terminated Coordination", this, + (getFailure() != null) ? CoordinationException.FAILED : CoordinationException.ALREADY_ENDED, getFailure()); + } + + if (timeOutInMs > 0) + { + this.deadLine += timeOutInMs; + scheduleTimeout(this.deadLine); + } + + } + } + return this.deadLine; + } + + /** + * @see org.osgi.service.coordinator.Coordination#isTerminated() + */ + public boolean isTerminated() + { + return state != State.ACTIVE; + } + + /** + * @see org.osgi.service.coordinator.Coordination#getThread() + */ + public Thread getThread() + { + this.owner.checkPermission(name, CoordinationPermission.ADMIN); + return associatedThread; + } + + /** + * @see org.osgi.service.coordinator.Coordination#join(long) + */ + public void join(final long timeOutInMs) throws InterruptedException + { + this.owner.checkPermission(name, CoordinationPermission.PARTICIPATE); + if ( timeOutInMs < 0 ) + { + throw new IllegalArgumentException("Timeout must not be negative"); + } + + if ( !isTerminated() ) + { + synchronized ( this.waitLock ) + { + this.waitLock.wait(timeOutInMs); + } + } + } + + /** + * @see org.osgi.service.coordinator.Coordination#push() + */ + public Coordination push() + { + this.owner.checkPermission(name, CoordinationPermission.INITIATE); + if ( isTerminated() ) + { + throw new CoordinationException("Coordination already ended", this, CoordinationException.ALREADY_ENDED); + } + + owner.push(this); + return this; + } + + /** + * @see org.osgi.service.coordinator.Coordination#getBundle() + */ + public Bundle getBundle() + { + this.owner.checkPermission(name, CoordinationPermission.ADMIN); + return this.owner.getBundle(); + } + + /** + * @see org.osgi.service.coordinator.Coordination#getEnclosingCoordination() + */ + public Coordination getEnclosingCoordination() + { + this.owner.checkPermission(name, CoordinationPermission.ADMIN); + Coordination c = this.owner.getEnclosingCoordination(this); + if ( c != null ) + { + c = ((CoordinationImpl)c).holderRef.get(); + } + return c; + } + + //------- + + /** + * Initiates a coordination timeout. Called from the timer task scheduled by + * the {@link #scheduleTimeout(long)} method. + *

      + * This method is intended to only be called from the scheduled timer task. + */ + private void timeout() + { + // Fail the Coordination upon timeout + fail(TIMEOUT); + } + + /** + * If this coordination is still active, this method initiates the + * termination of the coordination by setting the state to + * {@value State#TERMINATING}, unregistering from the + * {@link CoordinationMgr} and ensuring there is no timeout task active any + * longer to timeout this coordination. + * + * @return true If the coordination was active and termination + * can continue. If false is returned, the coordination + * must be considered terminated (or terminating) in the current + * thread and no further termination processing must take place. + */ + private synchronized boolean startTermination() + { + if (state == State.ACTIVE) + { + state = State.TERMINATING; + scheduleTimeout(-1); + return true; + } + + // this coordination is not active any longer, nothing to do + return false; + } + + /** + * Helper method for timeout scheduling. If a timer is currently scheduled + * it is canceled. If the new timeout value is a positive value a new timer + * is scheduled to fire at the desired time (in the future) + * + * @param deadline The at which to schedule the timer + */ + private void scheduleTimeout(final long deadLine) + { + if (timeoutTask != null) + { + owner.schedule(timeoutTask, -1); + timeoutTask = null; + } + + if (deadLine > System.currentTimeMillis()) + { + timeoutTask = new TimerTask() + { + @Override + public void run() + { + CoordinationImpl.this.timeout(); + } + }; + + owner.schedule(timeoutTask, deadLine); + } + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + (int) (id ^ (id >>> 32)); + return result; + } + + @Override + public boolean equals(final Object obj) + { + if (obj instanceof CoordinationHolder ) + { + return obj.equals(this); + } + if ( !(obj instanceof CoordinationImpl) ) + { + return false; + } + final CoordinationImpl other = (CoordinationImpl) obj; + return id == other.id; + } + + void setAssociatedThread(final Thread t) { + this.associatedThread = t; + } + + public Coordination getHolder() { + return this.holderRef.get(); + } +} diff --git a/coordinator/src/main/java/org/apache/felix/coordinator/impl/CoordinationMgr.java b/coordinator/src/main/java/org/apache/felix/coordinator/impl/CoordinationMgr.java new file mode 100644 index 00000000000..ffd7eaaa453 --- /dev/null +++ b/coordinator/src/main/java/org/apache/felix/coordinator/impl/CoordinationMgr.java @@ -0,0 +1,364 @@ +/* + * 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.coordinator.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicLong; + +import org.osgi.framework.Bundle; +import org.osgi.service.coordinator.Coordination; +import org.osgi.service.coordinator.CoordinationException; +import org.osgi.service.coordinator.Participant; + +/** + * The CoordinationMgr is the actual back-end manager of all + * Coordinations created by the Coordinator implementation. + */ +public class CoordinationMgr +{ + public static final class CreationResult + { + public CoordinationImpl coordination; + public CoordinationHolder holder; + } + + private ThreadLocal> perThreadStack; + + private final AtomicLong ctr; + + private final Map coordinations; + + private final Map participants; + + private final Timer coordinationTimer; + + /** + * Wait at most 60 seconds for participant to be eligible for participation + * in a coordination. + * + * @see #singularizeParticipant(Participant, CoordinationImpl) + */ + private long participationTimeOut = 60 * 1000L; + + CoordinationMgr() + { + perThreadStack = new ThreadLocal>(); + ctr = new AtomicLong(-1); + coordinations = new HashMap(); + participants = new IdentityHashMap(); + coordinationTimer = new Timer("Coordination Timer", true); + } + + void cleanUp() + { + // terminate coordination timeout timer + coordinationTimer.purge(); + coordinationTimer.cancel(); + + // terminate all active coordinations + final List coords = new ArrayList(); + synchronized ( this.coordinations ) { + coords.addAll(this.coordinations.values()); + this.coordinations.clear(); + } + for(final CoordinationImpl c : coords) + { + if ( !c.isTerminated() ) + { + c.fail(Coordination.RELEASED); + } + } + + // release all participants + synchronized ( this.participants ) + { + participants.clear(); + } + + // cannot really clear out the thread local but we can let it go + perThreadStack = null; + } + + private Stack getThreadStack(final boolean create) + { + final ThreadLocal> tl = this.perThreadStack; + Stack stack = null; + if ( tl != null ) + { + stack = tl.get(); + if ( stack == null && create ) { + stack = new Stack(); + tl.set(stack); + } + } + return stack; + } + + void configure(final long participationTimeout) + { + this.participationTimeOut = participationTimeout; + } + + void schedule(final TimerTask task, final long deadLine) + { + if (deadLine < 0) + { + task.cancel(); + } + else + { + coordinationTimer.schedule(task, new Date(deadLine)); + } + } + + void lockParticipant(final Participant p, final CoordinationImpl c) + { + synchronized (participants) + { + // wait for participant to be released + long completeWaitTime = participationTimeOut; + long cutOff = System.currentTimeMillis() + completeWaitTime; + + CoordinationImpl current = participants.get(p); + while (current != null && current != c) + { + final long waitTime = (completeWaitTime > 500) ? 500 : completeWaitTime; + completeWaitTime = completeWaitTime - waitTime; + if (current.getThread() != null && current.getThread() == c.getThread()) + { + throw new CoordinationException("Participant " + p + " already participating in Coordination " + + current.getId() + "/" + current.getName() + " in this thread", c, + CoordinationException.DEADLOCK_DETECTED); + } + + try + { + participants.wait(waitTime); + } + catch (InterruptedException ie) + { + throw new CoordinationException("Interrupted waiting to add Participant " + p + + " currently participating in Coordination " + current.getId() + "/" + current.getName() + + " in this thread", c, CoordinationException.LOCK_INTERRUPTED); + } + + // timeout waiting for participation + if (System.currentTimeMillis() >= cutOff) + { + throw new CoordinationException("Timed out waiting to join coordinaton", c, + CoordinationException.FAILED, Coordination.TIMEOUT); + } + + // check again + current = participants.get(p); + } + + // lock participant into coordination + participants.put(p, c); + } + } + + void releaseParticipant(final Participant p) + { + synchronized (participants) + { + participants.remove(p); + participants.notifyAll(); + } + } + + // ---------- Coordinator back end implementation + + CreationResult create(final CoordinatorImpl owner, final String name, final long timeout) + { + final long id = ctr.incrementAndGet(); + final CreationResult result = CoordinationImpl.create(owner, id, name, timeout); + synchronized ( this.coordinations ) + { + coordinations.put(id, result.coordination); + } + return result; + } + + void unregister(final CoordinationImpl c, final boolean removeFromThread) + { + synchronized ( this.coordinations ) + { + coordinations.remove(c.getId()); + } + if ( removeFromThread ) + { + final Stack stack = this.getThreadStack(false); + if (stack != null) + { + stack.remove(c); + } + } + } + + void push(final CoordinationImpl c) + { + Stack stack = this.getThreadStack(true); + if ( stack != null) + { + if ( stack.contains(c) ) + { + throw new CoordinationException("Coordination already pushed", c, CoordinationException.ALREADY_PUSHED); + } + c.setAssociatedThread(Thread.currentThread()); + stack.push(c); + } + } + + Coordination pop() + { + final Stack stack = this.getThreadStack(false); + if (stack != null && !stack.isEmpty()) + { + final CoordinationImpl c = stack.pop(); + if ( c != null ) { + c.setAssociatedThread(null); + } + return c; + } + return null; + } + + Coordination peek() + { + final Stack stack = this.getThreadStack(false); + if (stack != null && !stack.isEmpty()) + { + return stack.peek(); + } + return null; + } + + Collection getCoordinations() + { + final ArrayList result = new ArrayList(); + synchronized ( this.coordinations ) + { + for(final CoordinationImpl c : this.coordinations.values() ) + { + result.add(c.getHolder()); + } + } + return result; + } + + Coordination getCoordinationById(final long id) + { + synchronized ( this.coordinations ) + { + final CoordinationImpl c = coordinations.get(id); + return (c == null || c.isTerminated()) ? null : c; + } + } + + public Coordination getEnclosingCoordination(final CoordinationImpl c) + { + final Stack stack = this.getThreadStack(false); + if ( stack != null ) + { + final int index = stack.indexOf(c); + if ( index > 0 ) + { + return stack.elementAt(index - 1); + } + } + return null; + } + + public CoordinationException endNestedCoordinations(final CoordinationImpl c) + { + CoordinationException partiallyFailed = null; + final Stack stack = this.getThreadStack(false); + if ( stack != null ) + { + final int index = stack.indexOf(c) + 1; + if ( index > 0 && stack.size() > index ) + { + final int count = stack.size()-index; + for(int i=0;i candidates = new ArrayList(); + synchronized ( this.coordinations ) + { + final Iterator> iter = this.coordinations.entrySet().iterator(); + while ( iter.hasNext() ) + { + final Map.Entry entry = iter.next(); + final CoordinationImpl c = entry.getValue(); + if ( c.getBundle().getBundleId() == owner.getBundleId() ) + { + candidates.add(c); + } + } + } + if ( candidates.size() > 0 ) + { + for(final CoordinationImpl c : candidates) + { + if ( !c.isTerminated() ) + { + c.fail(Coordination.RELEASED); + } + else + { + this.unregister(c, true); + } + } + } + } +} diff --git a/coordinator/src/main/java/org/apache/felix/coordinator/impl/CoordinatorImpl.java b/coordinator/src/main/java/org/apache/felix/coordinator/impl/CoordinatorImpl.java new file mode 100644 index 00000000000..9ec9b0ea5b3 --- /dev/null +++ b/coordinator/src/main/java/org/apache/felix/coordinator/impl/CoordinatorImpl.java @@ -0,0 +1,313 @@ +/* + * 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.coordinator.impl; + +import java.security.Permission; +import java.util.Collection; +import java.util.Iterator; +import java.util.TimerTask; + +import org.osgi.framework.Bundle; +import org.osgi.service.coordinator.Coordination; +import org.osgi.service.coordinator.CoordinationException; +import org.osgi.service.coordinator.CoordinationPermission; +import org.osgi.service.coordinator.Coordinator; +import org.osgi.service.coordinator.Participant; + +/** + * The coordinator implementation is a per bundle wrapper for the + * coordination manager. + */ +public class CoordinatorImpl implements Coordinator +{ + + /** The bundle that requested this service. */ + private final Bundle owner; + + /** The coordination mgr. */ + private final CoordinationMgr mgr; + + CoordinatorImpl(final Bundle owner, final CoordinationMgr mgr) + { + this.owner = owner; + this.mgr = mgr; + } + + /** + * Ensure all active Coordinations started by this CoordinatorImpl instance + * are terminated before the service is ungotten by the bundle. + *

      + * Called by the Coordinator ServiceFactory when this CoordinatorImpl + * instance is not used any longer by the owner bundle. + * + * @see FELIX-2671/OSGi Bug 104 + */ + void dispose() + { + this.mgr.dispose(this.owner); + } + + /** + * Ensures the name complies with the symbolic-name + * production of the OSGi core specification (1.3.2): + * + *

      +     * symbolic-name :: = token('.'token)*
      +     * digit    ::= [0..9]
      +     * alpha    ::= [a..zA..Z]
      +     * alphanum ::= alpha | digit
      +     * token    ::= ( alphanum | ’_’ | ’-’ )+
      +     * 
      + * + * If the key does not comply an IllegalArgumentException is + * thrown. + * + * @param key + * The configuration property key to check. + * @throws IllegalArgumentException + * if the key does not comply with the symbolic-name production. + */ + private void checkName( final String name ) + { + // check for empty string + if ( name.length() == 0 ) + { + throw new IllegalArgumentException( "Name must not be an empty string" ); + } + final String[] parts = name.split("\\."); + for(final String p : parts) + { + boolean valid = true; + if ( p.length() == 0 ) + { + valid = false; + } + else + { + for(int i=0; i= '0' && c <= '9') { + continue; + } + if ( c >= 'a' && c <= 'z') { + continue; + } + if ( c >= 'A' && c <= 'Z') { + continue; + } + if ( c == '_' || c == '-') { + continue; + } + valid = false; + break; + } + } + if ( !valid ) + { + throw new IllegalArgumentException( "Name [" + name + "] does not comply with the symbolic-name definition." ); + } + } + } + + public void checkPermission(final String coordinationName, final String actions ) + { + final SecurityManager securityManager = System.getSecurityManager(); + if (securityManager != null) + { + final Permission permission = new CoordinationPermission(coordinationName, this.owner, actions); + securityManager.checkPermission(permission); + } + } + + /** + * @see org.osgi.service.coordinator.Coordinator#create(java.lang.String, long) + */ + public Coordination create(final String name, final long timeout) + { + this.checkPermission(name, CoordinationPermission.INITIATE); + + // check arguments + checkName(name); + if ( timeout < 0 ) + { + throw new IllegalArgumentException("Timeout must not be negative"); + } + + // create coordination + final CoordinationMgr.CreationResult result = mgr.create(this, name, timeout); + + return result.holder; + } + + /** + * @see org.osgi.service.coordinator.Coordinator#getCoordinations() + */ + public Collection getCoordinations() + { + final Collection result = mgr.getCoordinations(); + final Iterator i = result.iterator(); + while ( i.hasNext() ) + { + final Coordination c = i.next(); + try { + this.checkPermission(c.getName(), CoordinationPermission.ADMIN); + } + catch (final SecurityException se) + { + i.remove(); + } + } + return result; + } + + /** + * @see org.osgi.service.coordinator.Coordinator#fail(java.lang.Throwable) + */ + public boolean fail(final Throwable reason) + { + CoordinationImpl current = (CoordinationImpl)mgr.peek(); + if (current != null) + { + return current.fail(reason); + } + return false; + } + + /** + * @see org.osgi.service.coordinator.Coordinator#peek() + */ + public Coordination peek() + { + Coordination c = mgr.peek(); + if ( c != null ) + { + c = ((CoordinationImpl)c).getHolder(); + } + return c; + } + + /** + * @see org.osgi.service.coordinator.Coordinator#begin(java.lang.String, long) + */ + public Coordination begin(final String name, final long timeout) + { + this.checkPermission(name, CoordinationPermission.INITIATE); + + // check arguments + checkName(name); + if ( timeout < 0 ) + { + throw new IllegalArgumentException("Timeout must not be negative"); + } + + // create coordination + final CoordinationMgr.CreationResult result = mgr.create(this, name, timeout); + this.mgr.push(result.coordination); + return result.holder; + } + + /** + * @see org.osgi.service.coordinator.Coordinator#pop() + */ + public Coordination pop() + { + Coordination c = mgr.pop(); + if ( c != null ) + { + checkPermission(c.getName(), CoordinationPermission.INITIATE); + c = ((CoordinationImpl)c).getHolder(); + } + return c; + } + + /** + * @see org.osgi.service.coordinator.Coordinator#addParticipant(org.osgi.service.coordinator.Participant) + */ + public boolean addParticipant(final Participant participant) + { + Coordination current = peek(); + if (current != null) + { + current.addParticipant(participant); + return true; + } + return false; + } + + /** + * @see org.osgi.service.coordinator.Coordinator#getCoordination(long) + */ + public Coordination getCoordination(final long id) + { + Coordination c = mgr.getCoordinationById(id); + if ( c != null ) + { + try { + checkPermission(c.getName(), CoordinationPermission.ADMIN); + c = ((CoordinationImpl)c).getHolder(); + } catch (final SecurityException e) { + c = null; + } + } + return c; + } + + //---------- + + void push(final CoordinationImpl c) + { + mgr.push(c); + } + + void unregister(final CoordinationImpl c, final boolean removeFromStack) + { + mgr.unregister(c, removeFromStack); + } + + void schedule(final TimerTask task, final long deadLine) + { + mgr.schedule(task, deadLine); + } + + void lockParticipant(final Participant p, final CoordinationImpl c) + { + mgr.lockParticipant(p, c); + } + + void releaseParticipant(final Participant p) + { + mgr.releaseParticipant(p); + } + + Bundle getBundle() + { + return this.owner; + } + + Coordination getEnclosingCoordination(final CoordinationImpl c) + { + return mgr.getEnclosingCoordination(c); + } + + CoordinationException endNestedCoordinations(final CoordinationImpl c) + { + return this.mgr.endNestedCoordinations(c); + } +} diff --git a/coordinator/src/main/java/org/apache/felix/coordinator/impl/LogWrapper.java b/coordinator/src/main/java/org/apache/felix/coordinator/impl/LogWrapper.java new file mode 100644 index 00000000000..a47af96f237 --- /dev/null +++ b/coordinator/src/main/java/org/apache/felix/coordinator/impl/LogWrapper.java @@ -0,0 +1,391 @@ +/* + * 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.coordinator.impl; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.Set; +import java.util.TreeSet; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; + +/** + * This class mimics the standard OSGi LogService interface. It logs to an + * available log service with the highest service ranking. + * + * @see org.osgi.service.log.LogService +**/ +public class LogWrapper +{ + /** + * ERROR LEVEL + * + * @see org.osgi.service.log.LogService#LOG_ERROR + */ + public static final int LOG_ERROR = 1; + + /** + * WARNING LEVEL + * + * @see org.osgi.service.log.LogService#LOG_WARNING + */ + public static final int LOG_WARNING = 2; + + /** + * INFO LEVEL + * + * @see org.osgi.service.log.LogService#LOG_INFO + */ + public static final int LOG_INFO = 3; + + /** + * DEBUG LEVEL + * + * @see org.osgi.service.log.LogService#LOG_DEBUG + */ + public static final int LOG_DEBUG = 4; + + /** A sorted set containing the currently available LogServices. + * Furthermore used as lock + */ + private final Set loggerRefs = new TreeSet( + new Comparator() { + + public int compare(ServiceReference o1, ServiceReference o2) { + return o2.compareTo(o1); + } + + }); + + /** + * Only null while not set and loggerRefs is empty hence, only needs to be + * checked in case m_loggerRefs is empty otherwise it will not be null. + */ + private BundleContext context; + + private ServiceListener logServiceListener; + + /** + * Current log level. Message with log level less than or equal to + * current log level will be logged. + * The default value is {@link #LOG_WARNING} + * + * @see #setLogLevel(int) + */ + private int logLevel = LOG_WARNING; + + /** + * Create the singleton + */ + private static class LogWrapperLoader + { + static final LogWrapper SINGLETON = new LogWrapper(); + } + + /** + * Returns the singleton instance of this LogWrapper that can be used to send + * log messages to all currently available LogServices or to standard output, + * respectively. + * + * @return the singleton instance of this LogWrapper. + */ + public static LogWrapper getLogger() + { + return LogWrapperLoader.SINGLETON; + } + + /** + * Set the BundleContext of the bundle. This method registers a service + * listener for LogServices with the framework that are subsequently used to + * log messages. + *

      + * If the bundle context is null, the service listener is + * unregistered and all remaining references to LogServices dropped before + * internally clearing the bundle context field. + * + * @param context The context of the bundle. + */ + public static void setContext( final BundleContext context ) + { + final LogWrapper logWrapper = LogWrapperLoader.SINGLETON; + + // context is removed, unregister and drop references + if ( context == null ) + { + if ( logWrapper.logServiceListener != null ) + { + logWrapper.context.removeServiceListener( logWrapper.logServiceListener ); + logWrapper.logServiceListener = null; + } + logWrapper.removeLoggerRefs(); + } + + // set field + logWrapper.setBundleContext( context ); + + // context is set, register and get existing services + if ( context != null ) + { + try + { + final ServiceListener listener = new ServiceListener() + { + // Add a newly available LogService reference to the singleton. + public void serviceChanged( final ServiceEvent event ) + { + if ( ServiceEvent.REGISTERED == event.getType() ) + { + LogWrapperLoader.SINGLETON.addLoggerRef( event.getServiceReference() ); + } + else if ( ServiceEvent.UNREGISTERING == event.getType() ) + { + LogWrapperLoader.SINGLETON.removeLoggerRef( event.getServiceReference() ); + } + } + + }; + context.addServiceListener( listener, "(" + Constants.OBJECTCLASS + "=org.osgi.service.log.LogService)" ); + logWrapper.logServiceListener = listener; + + // Add all available LogService references to the singleton. + final ServiceReference[] refs = context.getServiceReferences( "org.osgi.service.log.LogService", null ); + + if ( null != refs ) + { + for ( int i = 0; i < refs.length; i++ ) + { + logWrapper.addLoggerRef( refs[i] ); + } + } + } + catch ( InvalidSyntaxException e ) + { + // this never happens + } + } + } + + + /** + * The private singleton constructor. + */ + LogWrapper() + { + // Singleton + } + + /** + * Removes all references to LogServices still kept + */ + void removeLoggerRefs() + { + synchronized ( loggerRefs ) + { + loggerRefs.clear(); + } + } + + /** + * Add a reference to a newly available LogService + */ + void addLoggerRef( final ServiceReference ref ) + { + synchronized (loggerRefs) + { + loggerRefs.add(ref); + } + } + + /** + * Remove a reference to a LogService + */ + void removeLoggerRef( final ServiceReference ref ) + { + synchronized (loggerRefs) + { + loggerRefs.remove(ref); + } + } + + /** + * Set the context of the bundle in the singleton implementation. + */ + private void setBundleContext(final BundleContext context) + { + synchronized(loggerRefs) + { + this.context = context; + } + } + + public void log(final int level, final String msg) + { + log(null, level, msg, null); + } + + public void log(final int level, final String msg, final Throwable ex) + { + log(null, level, msg, null); + } + + public void log(final ServiceReference sr, final int level, final String msg) + { + log(sr, level, msg, null); + } + + public void log(final ServiceReference sr, final int level, final String msg, + final Throwable ex) + { + // The method will remove any unregistered service reference as well. + synchronized (loggerRefs) + { + if (level > logLevel) + { + return; // don't log + } + + boolean logged = false; + + if (!loggerRefs.isEmpty()) + { + // There is at least one LogService available hence, we can use the + // class as well. + for (Iterator iter = loggerRefs.iterator(); iter.hasNext();) + { + final ServiceReference next = iter.next(); + + org.osgi.service.log.LogService logger = + (org.osgi.service.log.LogService) context.getService(next); + + if (null != logger) + { + if ( sr == null ) + { + if ( ex == null ) + { + logger.log(level, msg); + } + else + { + logger.log(level, msg, ex); + } + } + else + { + if ( ex == null ) + { + logger.log(sr, level, msg); + } + else + { + logger.log(sr, level, msg, ex); + } + } + context.ungetService(next); + // we logged, so we can finish + logged = true; + break; + } + else + { + // The context returned null for the reference - it follows + // that the service is unregistered and we can remove it + iter.remove(); + } + } + } + if ( !logged) + { + _log(sr, level, msg, ex); + } + } + } + + /* + * Log the message to standard output. This appends the level to the message. + * null values are handled appropriate. + */ + private void _log(final ServiceReference sr, final int level, final String msg, + Throwable ex) + { + String s = (sr == null) ? null : "SvcRef " + sr; + s = (s == null) ? msg : s + " " + msg; + s = (ex == null) ? s : s + " (" + ex + ")"; + + switch (level) + { + case LOG_DEBUG: + System.out.println("DEBUG: " + s); + break; + case LOG_ERROR: + System.out.println("ERROR: " + s); + if (ex != null) + { + if ((ex instanceof BundleException) + && (((BundleException) ex).getNestedException() != null)) + { + ex = ((BundleException) ex).getNestedException(); + } + + ex.printStackTrace(); + } + break; + case LOG_INFO: + System.out.println("INFO: " + s); + break; + case LOG_WARNING: + System.out.println("WARNING: " + s); + break; + default: + System.out.println("UNKNOWN[" + level + "]: " + s); + } + } + + /** + * Change the current log level. Log level decides what messages gets + * logged. Any message with a log level higher than the currently set + * log level is not logged. + * + * @param logLevel new log level + */ + public void setLogLevel(int logLevel) + { + synchronized (loggerRefs) + { + logLevel = logLevel; + } + } + + /** + * @return current log level. + */ + public int getLogLevel() + { + synchronized (loggerRefs) + { + return logLevel; + } + } +} diff --git a/coordinator/src/test/java/org/apache/felix/coordinator/impl/CoordinatorImplTest.java b/coordinator/src/test/java/org/apache/felix/coordinator/impl/CoordinatorImplTest.java new file mode 100644 index 00000000000..de8d524df74 --- /dev/null +++ b/coordinator/src/test/java/org/apache/felix/coordinator/impl/CoordinatorImplTest.java @@ -0,0 +1,404 @@ +/* + * 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.coordinator.impl; + +import org.osgi.service.coordinator.Coordination; +import org.osgi.service.coordinator.CoordinationException; +import org.osgi.service.coordinator.Participant; + +import junit.framework.TestCase; + +public class CoordinatorImplTest extends TestCase +{ + + private CoordinationMgr mgr; + private CoordinatorImpl coordinator; + + @Override + protected void setUp() throws Exception + { + super.setUp(); + + mgr = new CoordinationMgr(); + coordinator = new CoordinatorImpl(null, mgr); + } + + public void test_createCoordination() + { + final String name = "test"; + final Coordination c1 = coordinator.create(name, 0); + assertNotNull(c1); + assertEquals(name, c1.getName()); + assertNull(coordinator.peek()); + assertNull(c1.getFailure()); + assertFalse(c1.isTerminated()); + assertTrue(c1.getParticipants().isEmpty()); + + Exception cause = new Exception(); + assertTrue(c1.fail(cause)); + assertSame(cause, c1.getFailure()); + assertTrue(c1.isTerminated()); + assertNull(coordinator.peek()); + + assertFalse(c1.fail(new Exception())); + try + { + c1.end(); + fail("Expected CoordinationException.FAILED on end() after fail()"); + } + catch (CoordinationException ce) + { + // expected failed + assertEquals(CoordinationException.FAILED, ce.getType()); + } + + final Coordination c2 = coordinator.create(name, 0); + assertNotNull(c2); + assertEquals(name, c2.getName()); + assertNull(coordinator.peek()); + assertNull(c2.getFailure()); + assertFalse(c2.isTerminated()); + assertTrue(c2.getParticipants().isEmpty()); + + c2.end(); + assertNull(c2.getFailure()); + assertTrue(c2.isTerminated()); + assertNull(coordinator.peek()); + + assertFalse(c2.fail(new Exception())); + try + { + c2.end(); + fail("Expected CoordinationException.ALREADY_ENDED on second end()"); + } + catch (CoordinationException ce) + { + // expected already terminated + assertEquals(CoordinationException.ALREADY_ENDED, ce.getType()); + } + } + + public void test_beginCoordination() + { + final String name = "test"; + final Coordination c1 = coordinator.begin(name, 0); + assertNotNull(c1); + assertEquals(name, c1.getName()); + + assertEquals(c1, coordinator.peek()); + assertEquals(c1, coordinator.pop()); + + assertNull(coordinator.peek()); + c1.push(); + assertEquals(c1, coordinator.peek()); + + c1.end(); + assertNull(coordinator.peek()); + + final Coordination c2 = coordinator.begin(name, 0); + assertNotNull(c2); + assertEquals(name, c2.getName()); + assertEquals(c2, coordinator.peek()); + c2.fail(new Exception()); + assertNotNull(coordinator.peek()); + try { + c2.end(); + fail("Exception should be thrown"); + } catch (CoordinationException ce) { + // ignore + } + assertNull(coordinator.peek()); + } + + public void test_beginCoordination_stack() + { + final String name = "test"; + + final Coordination c1 = coordinator.begin(name, 0); + assertNotNull(c1); + assertEquals(name, c1.getName()); + assertEquals(c1, coordinator.peek()); + + final Coordination c2 = coordinator.begin(name, 0); + assertNotNull(c2); + assertEquals(name, c2.getName()); + assertEquals(c2, coordinator.peek()); + + c2.end(); + assertEquals(c1, coordinator.peek()); + + c1.end(); + assertNull(coordinator.peek()); + } + + public void test_beginCoordination_stack2() + { + final String name = "test"; + + final Coordination c1 = coordinator.begin(name, 0); + assertNotNull(c1); + assertEquals(name, c1.getName()); + assertEquals(c1, coordinator.peek()); + + final Coordination c2 = coordinator.begin(name, 0); + assertNotNull(c2); + assertEquals(name, c2.getName()); + assertEquals(c2, coordinator.peek()); + + c1.end(); + assertNull(coordinator.peek()); + + try + { + c2.end(); + fail("c2 is already terminated"); + } + catch (CoordinationException ce) + { + assertEquals(CoordinationException.ALREADY_ENDED, ce.getType()); + } + assertNull(coordinator.peek()); + } + + public void test_addParticipant_with_ended() + { + final String name = "test"; + final Coordination c1 = coordinator.create(name, 0); + + final MockParticipant p1 = new MockParticipant(); + c1.addParticipant(p1); + assertTrue(c1.getParticipants().contains(p1)); + assertEquals(1, c1.getParticipants().size()); + + c1.end(); + assertTrue(p1.ended); + assertFalse(p1.failed); + assertEquals(c1, p1.c); + + // assert order of call + final Coordination c2 = coordinator.create(name, 0); + final MockParticipant p21 = new MockParticipant(); + final MockParticipant p22 = new MockParticipant(); + c2.addParticipant(p21); + c2.addParticipant(p22); + assertTrue(c2.getParticipants().contains(p21)); + assertTrue(c2.getParticipants().contains(p22)); + assertEquals(2, c2.getParticipants().size()); + + c2.end(); + assertTrue(p21.ended); + assertEquals(c2, p21.c); + assertTrue(p22.ended); + assertEquals(c2, p22.c); + assertTrue("p22 must be called before p21", p22.time < p21.time); + + // assert order of call with two registrations + final Coordination c3 = coordinator.create(name, 0); + final MockParticipant p31 = new MockParticipant(); + final MockParticipant p32 = new MockParticipant(); + c3.addParticipant(p31); + c3.addParticipant(p32); + c3.addParticipant(p31); // should be "ignored" + assertTrue(c3.getParticipants().contains(p31)); + assertTrue(c3.getParticipants().contains(p32)); + assertEquals(2, c3.getParticipants().size()); + + c3.end(); + assertTrue(p31.ended); + assertEquals(c3, p31.c); + assertTrue(p32.ended); + assertEquals(c3, p32.c); + assertTrue("p32 must be called before p31", p32.time < p31.time); + } + + public void test_addParticipant_with_failed() + { + final String name = "test"; + final Coordination c1 = coordinator.create(name, 0); + + final MockParticipant p1 = new MockParticipant(); + c1.addParticipant(p1); + assertTrue(c1.getParticipants().contains(p1)); + assertEquals(1, c1.getParticipants().size()); + + c1.fail(new Exception()); + assertFalse(p1.ended); + assertTrue(p1.failed); + assertEquals(c1, p1.c); + + // assert order of call + final Coordination c2 = coordinator.create(name, 0); + final MockParticipant p21 = new MockParticipant(); + final MockParticipant p22 = new MockParticipant(); + c2.addParticipant(p21); + c2.addParticipant(p22); + assertTrue(c2.getParticipants().contains(p21)); + assertTrue(c2.getParticipants().contains(p22)); + assertEquals(2, c2.getParticipants().size()); + + c2.fail(new Exception()); + assertTrue(p21.failed); + assertEquals(c2, p21.c); + assertTrue(p22.failed); + assertEquals(c2, p22.c); + assertTrue("p22 must be called before p21", p22.time < p21.time); + + // assert order of call with two registrations + final Coordination c3 = coordinator.create(name, 0); + final MockParticipant p31 = new MockParticipant(); + final MockParticipant p32 = new MockParticipant(); + c3.addParticipant(p31); + c3.addParticipant(p32); + c3.addParticipant(p31); // should be "ignored" + assertTrue(c3.getParticipants().contains(p31)); + assertTrue(c3.getParticipants().contains(p32)); + assertEquals(2, c3.getParticipants().size()); + + c3.fail(new Exception()); + assertTrue(p31.failed); + assertEquals(c3, p31.c); + assertTrue(p32.failed); + assertEquals(c3, p32.c); + assertTrue("p31 must be called before p32", p32.time < p31.time); + } + + public void test_Coordination_timeout() throws InterruptedException + { + final String name = "test"; + final Coordination c1 = coordinator.create(name, 200); + final MockParticipant p1 = new MockParticipant(); + c1.addParticipant(p1); + assertTrue(c1.getParticipants().contains(p1)); + assertEquals(1, c1.getParticipants().size()); + + // wait for the coordination to time out + Thread.sleep(250); + + // expect coordination to have terminated + assertTrue(c1.isTerminated()); + assertSame(Coordination.TIMEOUT, c1.getFailure()); + + // expect Participant.failed() being called + assertTrue(p1.failed); + assertEquals(c1, p1.c); + } + + public void test_Coordination_addParticipant_timeout() throws InterruptedException + { + final String name1 = "test1"; + final String name2 = "test2"; + final MockParticipant p1 = new MockParticipant(); + + // ensure short timeout for participation + mgr.configure(200); + + final Coordination c1 = coordinator.create(name1, 0); + c1.addParticipant(p1); + assertTrue(c1.getParticipants().contains(p1)); + assertEquals(1, c1.getParticipants().size()); + + // preset p1PartFailure to be be sure the participation actually starts + p1.addParticipantFailure(new Exception("Not Started yet")); + + Thread c2Thread = new Thread() + { + @Override + public void run() + { + final Coordination c2 = coordinator.create(name2, 0); + try + { + p1.addParticipantFailure(null); + c2.addParticipant(p1); + } + catch (Throwable t) + { + p1.addParticipantFailure(t); + } + finally + { + c2.end(); + } + } + }; + c2Thread.start(); + + // wait at most 2 seconds for the second thread to terminate + // we expect this if the participation properly times out + c2Thread.join(2000); + assertFalse("Thread for second Coordination did not terminate....", c2Thread.isAlive()); + + Throwable p1PartFailure = p1.addParticipantFailure; + if (p1PartFailure == null) + { + fail("Expecting CoordinationException/FAILED for second participation"); + } + else if (p1PartFailure instanceof CoordinationException) + { + assertEquals(CoordinationException.FAILED, ((CoordinationException) p1PartFailure).getType()); + } + else + { + fail("Unexpected Throwable while trying to addParticipant: " + p1PartFailure); + } + + c1.end(); + + // make sure c2Thread has terminated + if (c2Thread.isAlive()) + { + c2Thread.interrupt(); + c2Thread.join(1000); + assertFalse("Thread for second Coordination did still not terminate....", c2Thread.isAlive()); + } + } + + static final class MockParticipant implements Participant + { + + long time; + + Coordination c; + + boolean failed; + + boolean ended; + + Throwable addParticipantFailure; + + public void failed(Coordination c) throws Exception + { + this.failed = true; + this.c = c; + this.time = System.nanoTime(); + } + + public void ended(Coordination c) throws Exception + { + this.ended = true; + this.c = c; + this.time = System.nanoTime(); + } + + void addParticipantFailure(Throwable t) + { + this.addParticipantFailure = t; + } + } +} diff --git a/dependencymanager/.gitignore b/dependencymanager/.gitignore new file mode 100644 index 00000000000..de001d36b8e --- /dev/null +++ b/dependencymanager/.gitignore @@ -0,0 +1,8 @@ +/.gradle/ +/reports/ +/generated/ +/.metadata/ +/.recommenders/ +/RemoteSystemsTempFiles/ +/rat-report.xml + diff --git a/dependencymanager/.gradle-wrapper/gradle-wrapper.properties b/dependencymanager/.gradle-wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..1610e0fdbc7 --- /dev/null +++ b/dependencymanager/.gradle-wrapper/gradle-wrapper.properties @@ -0,0 +1,23 @@ +/* + * 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. + */ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip diff --git a/dependencymanager/README b/dependencymanager/README new file mode 100644 index 00000000000..6cd3557ebab --- /dev/null +++ b/dependencymanager/README @@ -0,0 +1,44 @@ +/* + * 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. + */ + +Welcome to Apache Felix Dependency Manager +========================================== + +Apache Felix Dependency Manager is a versatile java API, allowing to declaratively +register, acquire, and manage dynamic OSGi services. + +Please refer to release/resources/src/README + +Building and testing Apache Felix Dependency Manager +==================================================== + +The build instructions can be found from release/resources/src/README.src + +Getting Started +=============== + +To start using Apache Felix Dependency Manager, please go to our website and read the +getting started guide for users: + + http://felix.apache.org/documentation/subprojects/apache-felix-dependency-manager.html + +Many examples are also available from the dependency manager examples, in the org.apache.felix.dependencymanager.samples module +See org.apache.felix.dependencymanager.samples/README.samples + +Many thanks for using Apache Felix Dependency Manager. + +The Felix Team diff --git a/dependencymanager/build.gradle b/dependencymanager/build.gradle new file mode 100644 index 00000000000..80e7d4f20f9 --- /dev/null +++ b/dependencymanager/build.gradle @@ -0,0 +1,22 @@ +/* + * 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. + */ + +task wrapper(type: Wrapper) { + jarFile = rootProject.file('.gradle-wrapper/gradle-wrapper.jar') +} diff --git a/dependencymanager/changelog.txt b/dependencymanager/changelog.txt new file mode 100644 index 00000000000..6bd35503741 --- /dev/null +++ b/dependencymanager/changelog.txt @@ -0,0 +1,287 @@ +Release Notes - Felix - Version org.apache.felix.dependencymanager-r15 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.6.0 + * org.apache.felix.dependencymanager.shell; version=4.0.8 + * org.apache.felix.dependencymanager.runtime; version=4.0.7 + * org.apache.felix.dependencymanager.annotation; version=5.0.1 + * org.apache.felix.dependencymanager.lambda; version=1.2.1 + +** Task + * [FELIX-5996] - Remove generic parameter in DM Component interface + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r14 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.5.1 + * org.apache.felix.dependencymanager.shell; version=4.0.7 + * org.apache.felix.dependencymanager.runtime; version=4.0.6 + * org.apache.felix.dependencymanager.annotation; version=5.0.1 + * org.apache.felix.dependencymanager.lambda; version=1.2.0 + +** Bug + * [FELIX-5990] - DM ServiceTracker memory leak + +** Wish + * [FELIX-5991] - DM annotation scanner debug messages are logged in warn + +** Task + * [FELIX-5992 - Do not use a global changelog for all dm modules + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r13 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.5.0 + * org.apache.felix.dependencymanager.shell; version=4.0.7 + * org.apache.felix.dependencymanager.runtime; version=4.0.6 + * org.apache.felix.dependencymanager.annotation; version=5.0.0 + * org.apache.felix.dependencymanager.lambda; version=1.2.0 + + +** Bug + * [FELIX-5683] - getServiceProperties returns null instead of empty dictionary + * [FELIX-5716] - Dead Lock in DM + * [FELIX-5768] - DM Lambda stop callback not being called + * [FELIX-5955] - Move changelog.txt to toplevel project dir + * [FELIX-5956] - NPE when invoking a lifecycle runnable method from init method + +** New Feature + * [FELIX-5336] - Add support for prototype scope services in DM4 + + +** Improvement + * [FELIX-5967] - DM does not support java9+ + * [FELIX-5937] - Refactor DM bndtools/gradle project + * [FELIX-5939] - DM annotations enhancements + * [FELIX-5941] - DM APi enhancements + * [FELIX-5957] - Check if a default implementation is used only on optional dependencies + + +** Task + * [FELIX-5960] - Do not supply MD5 checksum + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r11 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.4.1 + * org.apache.felix.dependencymanager.shell; version=4.0.6 + * org.apache.felix.dependencymanager.runtime; version=4.0.5 + * org.apache.felix.dependencymanager.annotation; version=4.2.1 + * org.apache.felix.dependencymanager.lambda; version=1.1.1 + +** Bug + * [FELIX-5630] - NullObject is created for a required dependency if the component is removed and added again to the dependency manager + * [FELIX-5636] - Component of aspect service does not have any service properties anymore + * [FELIX-5657] - DM released sources can't be rebuilt + +** Improvement + * [FELIX-5619] - MultiProperyFilterIndex memory consumption + * [FELIX-5623] - Improve performance of ComponentImpl.getName method + * [FELIX-5650] - Support latest version of Gogo + * [FELIX-5653] - Simplify DM-Lambda samples + * [FELIX-5658] - Include poms in dm artifacts + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r9 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.4.0 + * org.apache.felix.dependencymanager.shell; version=4.0.5 + * org.apache.felix.dependencymanager.runtime; version=4.0.4 + * org.apache.felix.dependencymanager.annotation; version=4.2.0 + * org.apache.felix.dependencymanager.lambda; version=1.1.0 + +** Bug + * [FELIX-5236] - Single @Property annotation on a type doesn't work + * [FELIX-5242] - Configuration updates may be missed when the component is restarting + * [FELIX-5244] - Can't inject service using a method ref on a parent class method. + * [FELIX-5245] - Typo in error logged when a component callback is not found. + * [FELIX-5268] - Service not unregistered while bundle is starting + * [FELIX-5273] - Wrong log when a callback is not found from component instance(s) + * [FELIX-5274] - remove callback fails after manually removing dynamic dependencies + * [FELIX-5399] - Unable to define default map or list config types + * [FELIX-5400] - Can't override default configuration type list value using an empty list + * [FELIX-5401] - Can't override default configuration type map value using an empty map + * [FELIX-5402] - Factory configuration adapter ignores factory method + * [FELIX-5411] - When you stop a component, the service references are not ungotten. + * [FELIX-5426] - Remove callbacks aren't called for optional dependencies in a "circular" dependency scenario + * [FELIX-5428] - Dependency events set not cleared when component is removed + * [FELIX-5429] - Aspect swap callback sometimes not called on optional dependencies + * [FELIX-5469] - Methodcache system size property is not used + * [FELIX-5471] - Ensure that unbound services are always handled synchronously + * [FELIX-5517] - @Inject annotation ignored when applied on ServiceRegistration + * [FELIX-5519] - services are not ungotten when swapped by an aspect + * [FELIX-5523] - required dependencies added to a started adapter (or aspect) are not injected + + + + +** Improvement + * [FELIX-5228] - Upgrade DM With latest release of BndTools + * [FELIX-5237] - Configurable invocation handler should use default method values + * [FELIX-5346] - Start annotation not propagated to sub classes + * [FELIX-5355] - Allow to use properties having dots with configuration proxies + * [FELIX-5403] - Improve the Javadoc for org.apache.felix.dm.ComponentStateListener + * [FELIX-5405] - Do not have org.apache.felix.dm.Logger invoke toString() of message parameters when enabled log level is not high enough + * [FELIX-5406] - DM lambda fluent service properties don't support dots + * [FELIX-5407] - DM annotation plugin generates temp log files even if logging is disabled + * [FELIX-5408] - Parallel DM should not stop components asynchronously + * [FELIX-5467] - MultiPropertyFilterIndex is unusable when a service reference contains a lot of values for one key + * [FELIX-5499] - Remove usage of json.org from dependency manager + * [FELIX-5515] - Upgrade DM to OSGi R6 API + * [FELIX-5516] - Allow to not dereference services internally + * [FELIX-5518] - Remove all eclipse warnings in DM code + * [FELIX-5520] - ComponentStateListener not supported in DM lambda + * [FELIX-5521] - add more callback method signature in DM lambda service dependency callbacks + * [FELIX-5522] - Refactor aspect service implementation + * [FELIX-5524] - add more signatures for aspect swap callbacks + * [FELIX-5526] - Allow to use generic custom DM dependencies when using dm lambda. + * [FELIX-5531] - Document dependency callback signatures + * [FELIX-5532] - Swap callback is missing in @ServiceDependency annotation + + + +** Task + * [FELIX-5533] - Fix a semantic versioning issue when releasing dependency manager + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r8 +====================================================================== + +** Bug + * [FELIX-5146] - Service adapters turn on autoconf even if callbacks are used + * [FELIX-5147] - Bundle Adapter auto configures class fields even if callbacks are used + * [FELIX-5153] - DM4 calls stop before ungetService() on ServiceFactory components + * [FELIX-5155] - Adapter/Aspect extra service dependencies injected twice if using callback instance + * [FELIX-5178] - Make some component parameters as volatile + * [FELIX-5181] - Only log info/debug if dm annotation log parameter is enabled + * [FELIX-5187] - No errog log when configuration dependency callback is not found + * [FELIX-5188] - No error log when a factory pid adapter update callback is not found + * [FELIX-5192] - ConfigurationDependency race condition when component is stopped + * [FELIX-5193] - Factory Pid Adapter race condition when component is stopped + * [FELIX-5200] - Factory configuration adapter not restarted + + +** New Feature + * [FELIX-4689] - Create a more fluent syntax for the dependency manager builder + + +** Improvement + * [FELIX-5126] - Build DM using Java 8 + * [FELIX-5164] - Add support for callback instance in Aspects + * [FELIX-5177] - Support injecting configuration proxies + * [FELIX-5180] - Support for Java8 Repeatable Properties in DM annotations. + * [FELIX-5182] - Cleanup DM samples + * [FELIX-5201] - Improve how components are displayed with gogo shell + + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r6 +====================================================================== + +** Bug + * [FELIX-4974] - DM filter indices not enabled if the dependencymanager bundle is started first + * [FELIX-5045] - DM Optional callbacks may sometimes be invoked before start callback + * [FELIX-5046] - Gradle wrapper is not included in DM source release + + + + +** Improvement + * [FELIX-4921] - Ensure binary equality of the same bundle between successive DM releases + * [FELIX-4922] - Simplify DM changelog management + * [FELIX-5054] - Clean-up instance bound dependencies when component is destroyed + * [FELIX-5055] - Upgrade DM to BndTools 3.0.0 + * [FELIX-5104] - Call a conf dependency callback Instance with an instantiated component + * [FELIX-5113] - Remove useless wrong test in ConfigurationDependencyImpl + * [FELIX-5114] - Schedule configuration update in Component executor synchronously + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r5: +====================================================================== + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r5 + + + +** Bug + * [FELIX-4907] - ConfigurationDependency calls updated(null) when component is stopped. + * [FELIX-4910] - ComponentExecutorFactory does not allow to return null from getExecutorFor method. + * [FELIX-4913] - DM Optional callbacks may sometimes be invoked twice + + + + +** Improvement + * [FELIX-4876] - DM Annotations bnd plugin compatibility with Bndtools 2.4.1 / 3.0.0 versions + * [FELIX-4877] - DM Annotations should detect service type using more method signatures. + * [FELIX-4915] - Skip unecessary manifest headers in DM bnd file + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r3: +===================================================================== + +** Bug + * [FELIX-4858] - DependencyManager: missing createCopy method in timed service dependency + * [FELIX-4869] - Callbacks not invoked for dependencies that are added after the component is initialized + + + + +** Improvement + * [FELIX-4614] - Factory create() method should have access to the component definition + * [FELIX-4873] - Enhance DM API to get missing and circular dependencies + * [FELIX-4878] - Support more signatures for Dependency callbacks + * [FELIX-4880] - Missing callback instance support for some adapters + * [FELIX-4889] - Refactor dm shell command to use the org.apache.dm.diagnostics api + + +** Wish + * [FELIX-4875] - Update DM integration test with latest ConfigAdmin + + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r2: +===================================================================== + +** Bug + * [FELIX-4832] - ClassCastException with autoconfig Iterable fields + * [FELIX-4833] - Revisit some javadocs in the DM annotations. + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r1: +====================================================================== + +** Bug + * [FELIX-4304] - DependencyManager ComponentImpl should not assume all service properties are stored in a Hashtable + * [FELIX-4394] - Race problems in DependencyManager Configuration Dependency + * [FELIX-4588] - createCopy method ConfigurationDependency produces a malfunctioning clone + * [FELIX-4594] - Propagation from dependencies overwrites service properties + * [FELIX-4598] - BundleDependency can effectively track only one bundle + * [FELIX-4602] - TemporalServiceDependency does not properly propagate RuntimeExceptions + * [FELIX-4709] - Incorrect Named Dependencies are binded to the Service Instance + + +** New Feature + * [FELIX-4426] - Allow DM to manage collections of services + * [FELIX-4807] - New thread model for Dependency Manager + + +** Improvement + * [FELIX-3914] - Log unsuccessful field injections + * [FELIX-4158] - ComponentDeclaration should give access to component information + * [FELIX-4667] - "top" command for the Dependency Manager Shell + * [FELIX-4672] - Allow callbacks to third party instance for adapters + * [FELIX-4673] - Log any error thrown when trying to create a null object. + * [FELIX-4777] - Dynamic initialization time configuration of @ConfigurationDependency + * [FELIX-4805] - Deprecate DM annotation metatypes + + +** Wish + * [FELIX-2706] - Support callback delegation for Configuration Dependecies + * [FELIX-4600] - Cherrypicking of propagated properties + * [FELIX-4676] - Add Provide-Capability for DependencyManager Runtime bundle + * [FELIX-4680] - Add more DM ServiceDependency callback signatures + * [FELIX-4683] - Allow to configure the DependencyManager shell scope + * [FELIX-4684] - Replace DependencyManager Runtime "factorySet" by a cleaner API + * [FELIX-4816] - bndtools-ify Dependency Manager + * [FELIX-4818] - New release process for Dependency Manager + diff --git a/dependencymanager/cnf/.classpath b/dependencymanager/cnf/.classpath new file mode 100644 index 00000000000..fb5011632c0 --- /dev/null +++ b/dependencymanager/cnf/.classpath @@ -0,0 +1,6 @@ + + + + + + diff --git a/dependencymanager/cnf/.gitignore b/dependencymanager/cnf/.gitignore new file mode 100644 index 00000000000..120f0c45758 --- /dev/null +++ b/dependencymanager/cnf/.gitignore @@ -0,0 +1,3 @@ +/bin/ +/generated/ +/cache/ diff --git a/dependencymanager/cnf/.project b/dependencymanager/cnf/.project new file mode 100644 index 00000000000..1397bbe6205 --- /dev/null +++ b/dependencymanager/cnf/.project @@ -0,0 +1,11 @@ + + + cnf + + + + + + + + diff --git a/dependencymanager/cnf/build.bnd b/dependencymanager/cnf/build.bnd new file mode 100644 index 00000000000..c9832cec6ec --- /dev/null +++ b/dependencymanager/cnf/build.bnd @@ -0,0 +1,62 @@ +# 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. +# + +# +# Set here bnd repositories +# +-plugin.1.Release: aQute.bnd.deployer.repository.LocalIndexedRepo; \ + name=Release; \ + local=${workspace}/cnf/releaserepo; \ + pretty=true + +-plugin.2.Local: aQute.bnd.deployer.repository.LocalIndexedRepo; \ + name=Local; \ + local=${workspace}/cnf/localrepo; \ + pretty=true + +-plugin.3.Repository: aQute.bnd.repository.maven.provider.MavenBndRepository; \ + releaseUrl = 'https://repo1.maven.org/maven2'; \ + name = Repository; \ + index = ${.}/repository.mvn; \ + noupdateOnRelease=true + +# +# configure release and baseline repos +# +-releaserepo: Release +-baselinerepo: Repository +-baseline: * + +# +# Set here the maven group id for the workspace bundles +# +-pom: groupid = org.apache.felix + +# +# Don't include sources in generated bundles +# +-sources: true + +# +# Java parameters +# +javac.source: 1.8 +javac.target: 1.8 + +# +# Removes some headers in order to reduce binary diff between same bundles that are not changed between subsequent releases. +# +-removeheaders: Bnd-LastModified,Tool,Created-By,Include-Resource diff --git a/dependencymanager/cnf/ext/junit.bnd b/dependencymanager/cnf/ext/junit.bnd new file mode 100644 index 00000000000..129c585689f --- /dev/null +++ b/dependencymanager/cnf/ext/junit.bnd @@ -0,0 +1,19 @@ +# +# 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. +# + +junit: org.apache.servicemix.bundles.junit; version=4.12 +test-reports: test-results diff --git a/dependencymanager/cnf/ext/libraries.bnd b/dependencymanager/cnf/ext/libraries.bnd new file mode 100644 index 00000000000..ef9b3a3e0e2 --- /dev/null +++ b/dependencymanager/cnf/ext/libraries.bnd @@ -0,0 +1,77 @@ +# +# 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. +# + +## +# metatype service +metatype=org.apache.felix.metatype;version=1.0.4 + +## +# log service +log=org.apache.felix.log;version=1.0.1 + +## +# Gogo bundles +gogo=\ + org.apache.felix.gogo.command;version=1.0.2,\ + org.apache.felix.gogo.runtime;version=1.0.10,\ + org.apache.felix.gogo.shell;version=1.0.0,\ + org.jline;version=3.0.4,\ + org.fusesource.jansi;version=1.16.0 + + +## +# Configuration Admin +configadmin=org.apache.felix.configadmin;version=1.8.8 + +## +# Event Admin +eventadmin=org.apache.felix.eventadmin;version=1.4.4 + +## +# Web Console +webconsole=\ + org.apache.felix.http.api;version=3.0.0,\ + org.apache.felix.http.servlet-api;version=1.1.2,\ + org.apache.felix.http.jetty;version="[3.4.8,3.4.9]",\ + org.apache.felix.webconsole;version=4.3.4,\ + org.apache.commons.fileupload;version=1.2.1,\ + org.apache.commons.io;version=2.4.0 + +## +# bndlib +bndlib=biz.aQute.bndlib;version=3.5.0 + +## +# mockito +mockito=org.mockito.mockito-core;version=1.10.19 + +## +# objenesis +objenesis=org.objenesis;version=2.2 + +## +# runtime dependencies used by integration tests +# +itest.run=\ + org.apache.servicemix.bundles.junit;version=4.12,\ + org.mockito.mockito-core;version='[1.10.19,1.10.20)',\ + org.objenesis;version='[2.2.0,2.2.1)' + +## +# Felix Framework +# +felix.framework=org.apache.felix.framework;version='[5.6.10,5.6.10]' diff --git a/dependencymanager/cnf/localrepo/index.xml b/dependencymanager/cnf/localrepo/index.xml new file mode 100644 index 00000000000..0bfa6dcab5e --- /dev/null +++ b/dependencymanager/cnf/localrepo/index.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/dependencymanager/cnf/localrepo/index.xml.sha b/dependencymanager/cnf/localrepo/index.xml.sha new file mode 100644 index 00000000000..001203e7461 --- /dev/null +++ b/dependencymanager/cnf/localrepo/index.xml.sha @@ -0,0 +1 @@ +8de8e8866152096f0c4ea5ddd6ed1d633b4d1a6edfbd823245cb57f6639732fd \ No newline at end of file diff --git a/dependencymanager/cnf/releaserepo/index.xml b/dependencymanager/cnf/releaserepo/index.xml new file mode 100644 index 00000000000..c89dffca14d --- /dev/null +++ b/dependencymanager/cnf/releaserepo/index.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/dependencymanager/cnf/releaserepo/index.xml.sha b/dependencymanager/cnf/releaserepo/index.xml.sha new file mode 100644 index 00000000000..f2b26bf5d27 --- /dev/null +++ b/dependencymanager/cnf/releaserepo/index.xml.sha @@ -0,0 +1 @@ +262a924c4164db96f2409bfbe8d20793eb0c91a51c3dcd7c2160cde2bac8a492 \ No newline at end of file diff --git a/dependencymanager/cnf/repository.mvn b/dependencymanager/cnf/repository.mvn new file mode 100644 index 00000000000..a8bdecee96f --- /dev/null +++ b/dependencymanager/cnf/repository.mvn @@ -0,0 +1,53 @@ +# +# 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. +# + +ant:ant:1.5 +biz.aQute.bnd:biz.aQute.bnd.gradle:3.5.0 +biz.aQute.bnd:biz.aQute.bndlib:3.5.0 +org.apache.felix:org.apache.felix.framework:5.6.10 +org.apache.felix:org.apache.felix.eventadmin:1.4.4 +org.apache.felix:org.apache.felix.gogo.command:1.0.2 +org.apache.felix:org.apache.felix.gogo.shell:1.0.0 +org.apache.felix:org.apache.felix.gogo.runtime:1.0.10 +org.jline:jline:3.0.4 +org.apache.felix:org.apache.felix.http.api:3.0.0 +org.apache.felix:org.apache.felix.http.servlet-api:1.1.2 +org.apache.felix:org.apache.felix.configadmin:1.8.8 +org.apache.felix:org.apache.felix.log:1.0.1 +org.apache.felix:org.apache.felix.metatype:1.1.2 +org.apache.felix:org.apache.felix.http.jetty:3.4.8 +org.apache.felix:org.apache.felix.webconsole:4.3.4 +org.apache.felix:org.apache.felix.dependencymanager:4.6.0 +org.apache.felix:org.apache.felix.dependencymanager.annotation:5.0.1 +org.apache.felix:org.apache.felix.dependencymanager.lambda:1.2.1 +org.apache.felix:org.apache.felix.dependencymanager.runtime:4.0.7 +org.apache.felix:org.apache.felix.dependencymanager.shell:4.0.8 +org.jline:jline-builtins:3.3.0 +org.apache.servicemix.bundles:org.apache.servicemix.bundles.junit:4.12_1 +org.mockito:mockito-core:1.10.19 +org.objenesis:objenesis:2.2 +org.osgi:osgi.annotation:6.0.1 +org.osgi:osgi.cmpn:6.0.0 +org.osgi:osgi.core:6.0.0 +biz.aQute.bnd:biz.aQute.bnd.annotation:3.5.0 +org.slf4j:slf4j-simple:1.7.7 +org.slf4j:slf4j-api:1.7.7 +commons-fileupload:commons-fileupload:1.2.1 +org.fusesource.jansi:jansi:1.16 +commons-io:commons-io:2.4 + + diff --git a/dependencymanager/docs/.project b/dependencymanager/docs/.project new file mode 100644 index 00000000000..e248a4fa709 --- /dev/null +++ b/dependencymanager/docs/.project @@ -0,0 +1,11 @@ + + + docs + + + + + + + + diff --git a/dependencymanager/docs/A_Glance_At_DependencyManager.odp b/dependencymanager/docs/A_Glance_At_DependencyManager.odp new file mode 100644 index 00000000000..18f6736dabc Binary files /dev/null and b/dependencymanager/docs/A_Glance_At_DependencyManager.odp differ diff --git a/dependencymanager/docs/migrating.mdtext b/dependencymanager/docs/migrating.mdtext new file mode 100644 index 00000000000..306f91e65f4 --- /dev/null +++ b/dependencymanager/docs/migrating.mdtext @@ -0,0 +1,14 @@ +# Migrating from earlier versions + +DependencyManager 4.0 has some API changes that need to be taken into account when migrating from DependencyManager 3. + +* A dependency can no longer be shared accross components. +* You no longer have to call setInstanceBound() when adding a dependency from within the init() method of a component. Therefore the setInstanceBound() method has been removed from all Dependency interfaces. +* in the Dependency interface, the following method have been removed: isInstanceBound, invokeAdded, invokeRemoved, createCopy. +* In the Component interface, the "Object Component.getService()" method has been replaced by the " T getInstance()" method. +* In the Component interface, the "void addStateListener(ComponentStateListener listener) method" has been replaced by the "add(ComponentStateListener listener)" method. +* In the Component interface, the "start", "stop", "getDependencies" methods have been removed. +* In the Component interface and in the DependencyManager class, the createTemporalServiceDependency() method is now taking a timeout parameter: createTemporalServiceDependency(long timeout). +* The ComponentStateListener interface has changed: it is now providing a single "changed(Component c, ComponentState state)" method. +* The DependencyManager 4 Shell commands are no longer available for framework specific shell implementations, and support the gogo shell only. +* The TemporalServiceDependency interface has been removed. diff --git a/dependencymanager/docs/shell.mdtext b/dependencymanager/docs/shell.mdtext new file mode 100644 index 00000000000..f2bf6c7cf39 --- /dev/null +++ b/dependencymanager/docs/shell.mdtext @@ -0,0 +1,102 @@ +# Introduction + +The shell bundle for the dependency manager extends the gogo shell with one new command called "dm". This command can be used to get insight in the actual components and services in a running OSGi framework. + +Typing help ```help dm``` in the gogo shell gives an overview of the available command options. + +``` +dm - List dependency manager components + scope: dependencymanager + flags: + compact, cp Displays components using a compact form + nodeps, nd Hides component dependencies + notavail, na Only displays unavailable components + stats, stat, st Displays components statistics + wtf Detects where are the root failures + options: + bundleIds, bid, bi, b [optional] + componentIds, cid, ci [optional] + components, c [optional] + services, s [optional] + top This command displays components callbacks (init/start) times> [optional] + parameters: + CommandSession +``` + + +# Usage examples +Below are some examples for typical usage of the dependency manager shell commands. The examples are based on a simple component model with a dashboard which has a required dependency on four probes (temperature, humidity, radiation, pressure). The radiation probe requires a Sensor service but this sensor is not available. + +__List all dependency manager components__ + +```dm``` + +Sample output + +``` +[9] dm.demo + [6] dm.demo.Probe(type=radiation) unregistered + dm.demo.Sensor service required unavailable + [7] dm.demo.Probe(type=humidity) registered + [9] dm.demo.impl.Dashboard unregistered + dm.demo.Probe (type=temperature) service required available + dm.demo.Probe (type=radiation) service required unavailable + dm.demo.Probe (type=humidity) service required available + dm.demo.Probe (type=pressure) service required available + [5] dm.demo.Probe(type=temperature) registered + [8] dm.demo.Probe(type=pressure) registered +``` +All components are listed including the dependencies and the availability of these dependencies. The top level element is the bundle and below are the components registered with that bundle's bundle context. The lowest level is that of the component's dependencies. + +``` +[bundleid] bundle + [component id] component interfaces (service properties) + dependency +``` + +The following flags can be used to tailor the output. + +```compact, cp``` shortens package names and dependencies and therefore gives a more compressed output. + +```nodeps, nd``` omits the dependencies from the output. + +```notavail, na``` filters out all components that are registered wich results in the output only containing those components that are in the unregistered state due to one or more unsatisfied required dependencies. This is the command option most used when using the dependency manager shell commands. + +Sample output for ```dm na```: + +``` +[9] dm.demo + [14] dm.demo.impl.Dashboard unregistered + dm.demo.Probe (type=radiation) service required unavailable + [11] dm.demo.Probe(type=radiation) unregistered + dm.demo.Sensor service required unavailable +``` + +The flags can be used in conjunction with the other command options. + +__Find all components for a given classname__ + +```dm c .*ProbeImpl``` + +dm c or components finds all components for which the classname of the implementation matches the regular expression. + +__Find all services matching a service filter__ + +```dm s "(type=temperature)"``` + +dm s allows finding components based on the service properties of their registered services in the service registry using a standard OSGi service filter. + +__Find out why components are not registered__ + +```dm wtf``` + +Sample output + +``` +2 missing dependencies found. +----------------------------- +The following service(s) are missing: + * dm.demo.Sensor is not found in the service registry +``` + +wtf gives the root cause for components not being registered and therefore their services not being available. In a typical application components have dependencies on services implemented by components that have dependencies on services etcetera. This transitivity means that an entire chain of components could be unregistered due to a (few) root dependencies not being satisified. wtf is about discovering those dependencies. diff --git a/dependencymanager/docs/whatsnew.mdtext b/dependencymanager/docs/whatsnew.mdtext new file mode 100644 index 00000000000..ed682ae34f6 --- /dev/null +++ b/dependencymanager/docs/whatsnew.mdtext @@ -0,0 +1,58 @@ +# What's new in DependencyManager 4.0 + +DependencyManager 4.0 has been significantly reworked to improve support for concurrency. The following principles form the basis of the new concurrency model in DM4. + + * All external events that influence the state of dependencies are recorded and given to the serial executor of the component. We record whatever data comes in, so when the actual job is run by the serial executor, we still have access to the original data without having to access other sources whose state might have changed since. + * The serial executor of a component will execute a job immediately if it is being called by the thread that is already executing jobs. + * If the serial executor of a component had not yet started a job, it will queue and start it on the current thread. + * If the serial executor gets invoked from a different thread than the one currently executing jobs, the job will be put at the end of the queue. As mentioned before, any data associated with the event will also be recorded so it is available when the job executes. + * State in the component and dependency can only be modified via the serial executor thread. This means we don't need explicit synchronization anywhere. + +DependencyManager 4 now also supports parallel execution of component wiring. + +Added support for parallelism: To allow components to be started and handled in parallel, you can now register in the OSGi service registry a ComponentExecutorFactory service that is used to get an Executor for the management of all components dependencies/lifecycle callbacks. See javadoc from the org.apache.felix.dm.ComponentExecutorFactory interface for more information. + +You can also take a look at the the org.apache.felix.dependencymanager.samples project, which is registering a ComponentExecutorFactory from org.apache.felix.dependencymanager.samples.tpool bundle. + +See also the following property in the org.apache.felix.dependencymanager.samples/bnd.bnd + + org.apache.felix.dependencymanager.parallel=\ + '!org.apache.felix.dependencymanager.samples.tpool, *',\ + +Here, all components will be handled by Executors provided by the ComponentExecutorFactory, except those having a package starting with "org.apache.felix.dependencymanager.samples.tpool" (because the threadpool is itself defined using the Dependency Manager API). + +In addition, some new features have been implemented in dependency manager: + +* Auto Config Iterable fields: AutoConfig dependencies can be applied on Iterable fields in order to be able to traverse currently injected services safely. The Iterable must be parameterized with the Service type. See org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/Spellcheck.java for an example. + +* AutoConfig Map field: AutoConfig dependencies can be applied on a field with a Map type, allowing to traverse currently injected services safely, including service properties. The Map must be traversed using the Map.Entry iterator. See javadoc for DependencyManager.setAutoConfig(). + +* Inject Configuration on separate callback instance: Configuration can be injected on a separate callback instance, like a CompositionManager for example. See an example in the samples, in org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/Activator.java. +See FELIX-2706 + +* Added propagate flag for Service Adapters: you can now choose to propagate or not adaptee service properties. See FELIX-4600 + +* "Top" command in the shell: a "top" command is now available from the shell and can be used to display all top components sorted by their init/start elapsed time. + +* The Annotations plugin can now automatically generate a Require-Capability header on the Dependency Manager Runtime bundle. +Use "add-require-capability=true" option in the plugin declaration property to enable this new feature (see FELIX-4676): +** -plugin: org.apache.felix.dm.annotation.plugin.bnd.AnnotationPlugin; add-require-capability=true + +* The Configuration Dependency Configuration dependency now supports a "name" attribute, allowing to dynamically configure configuration pids from the @Init method. see FELIX-4777 + +* Added a benchmark tool for dependency manager (not released, only available from the trunk, see dependencymanager/org.apache.felix.dependencymanager.benchmark/README + +* The Annotations "Factory Sets" are deprecated and have been replaced by a nice api exported by the runtime bundle. See FELIX-4684 + +# What's changed in DependencyManager 4.0 + +* The Annotations processor is not generating anymore the Import-Service/Export Service by default (no need to specify the "build-import-export-service=false" option in the +annotations plugin if you don't need to generate automatically the deprecated headers. + +* The Dependency Manager metatype Annotations are now deprecated and it is encouraged to use standard bndtools metatypes. +See org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/DictionaryConfiguration.java for an example. +You can also check http://www.aqute.biz/Bnd/MetaType for more information about the bnd metatypes annotations. + + + + diff --git a/dependencymanager/gradle.properties b/dependencymanager/gradle.properties new file mode 100644 index 00000000000..07c177e8d0e --- /dev/null +++ b/dependencymanager/gradle.properties @@ -0,0 +1,25 @@ +/* + * 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. + */ + +bnd_version=3.5.0 + +# bnd_plugin is the dependency declaration for the bnd gradle plugin +bnd_plugin=biz.aQute.bnd:biz.aQute.bnd.gradle:3.5.0 + + diff --git a/dependencymanager/gradlew b/dependencymanager/gradlew new file mode 100755 index 00000000000..6a4bf153fca --- /dev/null +++ b/dependencymanager/gradlew @@ -0,0 +1,178 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +if [ ! -f $APP_HOME/.gradle-wrapper/gradle-wrapper.jar ] +then + mkdir -p $APP_HOME/.gradle-wrapper + curl https://raw.githubusercontent.com/gradle/gradle/master/gradle/wrapper/gradle-wrapper.jar -o $APP_HOME/.gradle-wrapper/gradle-wrapper.jar +fi + +CLASSPATH=$APP_HOME/.gradle-wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/.classpath b/dependencymanager/org.apache.felix.dependencymanager.annotation/.classpath new file mode 100644 index 00000000000..7a6fc254361 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.annotation/.gitignore new file mode 100644 index 00000000000..90dde36e4ac --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/.gitignore @@ -0,0 +1,3 @@ +/bin/ +/bin_test/ +/generated/ diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/.project b/dependencymanager/org.apache.felix.dependencymanager.annotation/.project new file mode 100644 index 00000000000..35b34cacf36 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/.project @@ -0,0 +1,23 @@ + + + org.apache.felix.dependencymanager.annotation + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/bnd.bnd b/dependencymanager/org.apache.felix.dependencymanager.annotation/bnd.bnd new file mode 100644 index 00000000000..9fae67c4116 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/bnd.bnd @@ -0,0 +1,30 @@ +# +# 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. +# +Bundle-Version: 5.0.1 +-buildpath: \ + osgi.core;version=6.0,\ + osgi.cmpn;version=6.0,\ + biz.aQute.bndlib;version=3.5 +Private-Package: org.apache.felix.dm.annotation.plugin.bnd +Export-Package: org.apache.felix.dm.annotation.api +Include-Resource: META-INF/=resources/,META-INF/changelog.txt=changelog.txt +Bundle-Name: Apache Felix Dependency Manager Annotations +Bundle-Description: Annotations for Apache Felix Dependency Manager +Bundle-Category: osgi +Bundle-License: http://www.apache.org/licenses/LICENSE-2.0.txt +Bundle-Vendor: The Apache Software Foundation +Bundle-DocURL: http://felix.apache.org/documentation/subprojects/apache-felix-dependency-manager/apache-felix-dependency-manager-using-annotations.html diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/changelog.txt b/dependencymanager/org.apache.felix.dependencymanager.annotation/changelog.txt new file mode 100644 index 00000000000..0857a4075a1 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/changelog.txt @@ -0,0 +1,268 @@ +Release Notes - Felix - Version org.apache.felix.dependencymanager-r14 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.5.1 + * org.apache.felix.dependencymanager.shell; version=4.0.7 + * org.apache.felix.dependencymanager.runtime; version=4.0.6 + * org.apache.felix.dependencymanager.annotation; version=5.0.1 + * org.apache.felix.dependencymanager.lambda; version=1.2.0 + +** Wish + * [FELIX-5991] - DM annotation scanner debug messages are logged in warn + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r13 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.5.0 + * org.apache.felix.dependencymanager.shell; version=4.0.7 + * org.apache.felix.dependencymanager.runtime; version=4.0.6 + * org.apache.felix.dependencymanager.annotation; version=5.0.0 + * org.apache.felix.dependencymanager.lambda; version=1.2.0 + + +** Bug + * [FELIX-5683] - getServiceProperties returns null instead of empty dictionary + * [FELIX-5716] - Dead Lock in DM + * [FELIX-5768] - DM Lambda stop callback not being called + * [FELIX-5955] - Move changelog.txt to toplevel project dir + * [FELIX-5956] - NPE when invoking a lifecycle runnable method from init method + +** New Feature + * [FELIX-5336] - Add support for prototype scope services in DM4 + + +** Improvement + * [FELIX-5967] - DM does not support java9+ + * [FELIX-5937] - Refactor DM bndtools/gradle project + * [FELIX-5939] - DM annotations enhancements + * [FELIX-5941] - DM APi enhancements + * [FELIX-5957] - Check if a default implementation is used only on optional dependencies + + +** Task + * [FELIX-5960] - Do not supply MD5 checksum + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r11 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.4.1 + * org.apache.felix.dependencymanager.shell; version=4.0.6 + * org.apache.felix.dependencymanager.runtime; version=4.0.5 + * org.apache.felix.dependencymanager.annotation; version=4.2.1 + * org.apache.felix.dependencymanager.lambda; version=1.1.1 + +** Bug + * [FELIX-5630] - NullObject is created for a required dependency if the component is removed and added again to the dependency manager + * [FELIX-5636] - Component of aspect service does not have any service properties anymore + * [FELIX-5657] - DM released sources can't be rebuilt + +** Improvement + * [FELIX-5619] - MultiProperyFilterIndex memory consumption + * [FELIX-5623] - Improve performance of ComponentImpl.getName method + * [FELIX-5650] - Support latest version of Gogo + * [FELIX-5653] - Simplify DM-Lambda samples + * [FELIX-5658] - Include poms in dm artifacts + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r9 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.4.0 + * org.apache.felix.dependencymanager.shell; version=4.0.5 + * org.apache.felix.dependencymanager.runtime; version=4.0.4 + * org.apache.felix.dependencymanager.annotation; version=4.2.0 + * org.apache.felix.dependencymanager.lambda; version=1.1.0 + +** Bug + * [FELIX-5236] - Single @Property annotation on a type doesn't work + * [FELIX-5242] - Configuration updates may be missed when the component is restarting + * [FELIX-5244] - Can't inject service using a method ref on a parent class method. + * [FELIX-5245] - Typo in error logged when a component callback is not found. + * [FELIX-5268] - Service not unregistered while bundle is starting + * [FELIX-5273] - Wrong log when a callback is not found from component instance(s) + * [FELIX-5274] - remove callback fails after manually removing dynamic dependencies + * [FELIX-5399] - Unable to define default map or list config types + * [FELIX-5400] - Can't override default configuration type list value using an empty list + * [FELIX-5401] - Can't override default configuration type map value using an empty map + * [FELIX-5402] - Factory configuration adapter ignores factory method + * [FELIX-5411] - When you stop a component, the service references are not ungotten. + * [FELIX-5426] - Remove callbacks aren't called for optional dependencies in a "circular" dependency scenario + * [FELIX-5428] - Dependency events set not cleared when component is removed + * [FELIX-5429] - Aspect swap callback sometimes not called on optional dependencies + * [FELIX-5469] - Methodcache system size property is not used + * [FELIX-5471] - Ensure that unbound services are always handled synchronously + * [FELIX-5517] - @Inject annotation ignored when applied on ServiceRegistration + * [FELIX-5519] - services are not ungotten when swapped by an aspect + * [FELIX-5523] - required dependencies added to a started adapter (or aspect) are not injected + + + + +** Improvement + * [FELIX-5228] - Upgrade DM With latest release of BndTools + * [FELIX-5237] - Configurable invocation handler should use default method values + * [FELIX-5346] - Start annotation not propagated to sub classes + * [FELIX-5355] - Allow to use properties having dots with configuration proxies + * [FELIX-5403] - Improve the Javadoc for org.apache.felix.dm.ComponentStateListener + * [FELIX-5405] - Do not have org.apache.felix.dm.Logger invoke toString() of message parameters when enabled log level is not high enough + * [FELIX-5406] - DM lambda fluent service properties don't support dots + * [FELIX-5407] - DM annotation plugin generates temp log files even if logging is disabled + * [FELIX-5408] - Parallel DM should not stop components asynchronously + * [FELIX-5467] - MultiPropertyFilterIndex is unusable when a service reference contains a lot of values for one key + * [FELIX-5499] - Remove usage of json.org from dependency manager + * [FELIX-5515] - Upgrade DM to OSGi R6 API + * [FELIX-5516] - Allow to not dereference services internally + * [FELIX-5518] - Remove all eclipse warnings in DM code + * [FELIX-5520] - ComponentStateListener not supported in DM lambda + * [FELIX-5521] - add more callback method signature in DM lambda service dependency callbacks + * [FELIX-5522] - Refactor aspect service implementation + * [FELIX-5524] - add more signatures for aspect swap callbacks + * [FELIX-5526] - Allow to use generic custom DM dependencies when using dm lambda. + * [FELIX-5531] - Document dependency callback signatures + * [FELIX-5532] - Swap callback is missing in @ServiceDependency annotation + + + +** Task + * [FELIX-5533] - Fix a semantic versioning issue when releasing dependency manager + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r8 +====================================================================== + +** Bug + * [FELIX-5146] - Service adapters turn on autoconf even if callbacks are used + * [FELIX-5147] - Bundle Adapter auto configures class fields even if callbacks are used + * [FELIX-5153] - DM4 calls stop before ungetService() on ServiceFactory components + * [FELIX-5155] - Adapter/Aspect extra service dependencies injected twice if using callback instance + * [FELIX-5178] - Make some component parameters as volatile + * [FELIX-5181] - Only log info/debug if dm annotation log parameter is enabled + * [FELIX-5187] - No errog log when configuration dependency callback is not found + * [FELIX-5188] - No error log when a factory pid adapter update callback is not found + * [FELIX-5192] - ConfigurationDependency race condition when component is stopped + * [FELIX-5193] - Factory Pid Adapter race condition when component is stopped + * [FELIX-5200] - Factory configuration adapter not restarted + + +** New Feature + * [FELIX-4689] - Create a more fluent syntax for the dependency manager builder + + +** Improvement + * [FELIX-5126] - Build DM using Java 8 + * [FELIX-5164] - Add support for callback instance in Aspects + * [FELIX-5177] - Support injecting configuration proxies + * [FELIX-5180] - Support for Java8 Repeatable Properties in DM annotations. + * [FELIX-5182] - Cleanup DM samples + * [FELIX-5201] - Improve how components are displayed with gogo shell + + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r6 +====================================================================== + +** Bug + * [FELIX-4974] - DM filter indices not enabled if the dependencymanager bundle is started first + * [FELIX-5045] - DM Optional callbacks may sometimes be invoked before start callback + * [FELIX-5046] - Gradle wrapper is not included in DM source release + + + + +** Improvement + * [FELIX-4921] - Ensure binary equality of the same bundle between successive DM releases + * [FELIX-4922] - Simplify DM changelog management + * [FELIX-5054] - Clean-up instance bound dependencies when component is destroyed + * [FELIX-5055] - Upgrade DM to BndTools 3.0.0 + * [FELIX-5104] - Call a conf dependency callback Instance with an instantiated component + * [FELIX-5113] - Remove useless wrong test in ConfigurationDependencyImpl + * [FELIX-5114] - Schedule configuration update in Component executor synchronously + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r5: +====================================================================== + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r5 + + + +** Bug + * [FELIX-4907] - ConfigurationDependency calls updated(null) when component is stopped. + * [FELIX-4910] - ComponentExecutorFactory does not allow to return null from getExecutorFor method. + * [FELIX-4913] - DM Optional callbacks may sometimes be invoked twice + + + + +** Improvement + * [FELIX-4876] - DM Annotations bnd plugin compatibility with Bndtools 2.4.1 / 3.0.0 versions + * [FELIX-4877] - DM Annotations should detect service type using more method signatures. + * [FELIX-4915] - Skip unecessary manifest headers in DM bnd file + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r3: +===================================================================== + +** Bug + * [FELIX-4858] - DependencyManager: missing createCopy method in timed service dependency + * [FELIX-4869] - Callbacks not invoked for dependencies that are added after the component is initialized + + + + +** Improvement + * [FELIX-4614] - Factory create() method should have access to the component definition + * [FELIX-4873] - Enhance DM API to get missing and circular dependencies + * [FELIX-4878] - Support more signatures for Dependency callbacks + * [FELIX-4880] - Missing callback instance support for some adapters + * [FELIX-4889] - Refactor dm shell command to use the org.apache.dm.diagnostics api + + +** Wish + * [FELIX-4875] - Update DM integration test with latest ConfigAdmin + + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r2: +===================================================================== + +** Bug + * [FELIX-4832] - ClassCastException with autoconfig Iterable fields + * [FELIX-4833] - Revisit some javadocs in the DM annotations. + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r1: +====================================================================== + +** Bug + * [FELIX-4304] - DependencyManager ComponentImpl should not assume all service properties are stored in a Hashtable + * [FELIX-4394] - Race problems in DependencyManager Configuration Dependency + * [FELIX-4588] - createCopy method ConfigurationDependency produces a malfunctioning clone + * [FELIX-4594] - Propagation from dependencies overwrites service properties + * [FELIX-4598] - BundleDependency can effectively track only one bundle + * [FELIX-4602] - TemporalServiceDependency does not properly propagate RuntimeExceptions + * [FELIX-4709] - Incorrect Named Dependencies are binded to the Service Instance + + +** New Feature + * [FELIX-4426] - Allow DM to manage collections of services + * [FELIX-4807] - New thread model for Dependency Manager + + +** Improvement + * [FELIX-3914] - Log unsuccessful field injections + * [FELIX-4158] - ComponentDeclaration should give access to component information + * [FELIX-4667] - "top" command for the Dependency Manager Shell + * [FELIX-4672] - Allow callbacks to third party instance for adapters + * [FELIX-4673] - Log any error thrown when trying to create a null object. + * [FELIX-4777] - Dynamic initialization time configuration of @ConfigurationDependency + * [FELIX-4805] - Deprecate DM annotation metatypes + + +** Wish + * [FELIX-2706] - Support callback delegation for Configuration Dependecies + * [FELIX-4600] - Cherrypicking of propagated properties + * [FELIX-4676] - Add Provide-Capability for DependencyManager Runtime bundle + * [FELIX-4680] - Add more DM ServiceDependency callback signatures + * [FELIX-4683] - Allow to configure the DependencyManager shell scope + * [FELIX-4684] - Replace DependencyManager Runtime "factorySet" by a cleaner API + * [FELIX-4816] - bndtools-ify Dependency Manager + * [FELIX-4818] - New release process for Dependency Manager + diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/resources/DEPENDENCIES b/dependencymanager/org.apache.felix.dependencymanager.annotation/resources/DEPENDENCIES new file mode 100644 index 00000000000..4a8833fb519 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/resources/DEPENDENCIES @@ -0,0 +1,24 @@ +Apache Felix Dependency Manager Annotation +Copyright 2011-2017 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2016). +Licensed under the Apache License 2.0. + +This product uses software developed by Peter Kriens +(http://www.aqute.biz/Code/Bnd) +Copyright 2006-2017 aQute, All rights reserved +Licensed under the Apache License 2.0. + +III. Overall License Summary + +- Apache License 2.0 diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/resources/LICENSE b/dependencymanager/org.apache.felix.dependencymanager.annotation/resources/LICENSE new file mode 100644 index 00000000000..6b0b1270ff0 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/resources/LICENSE @@ -0,0 +1,203 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/resources/NOTICE b/dependencymanager/org.apache.felix.dependencymanager.annotation/resources/NOTICE new file mode 100644 index 00000000000..3bbc09a835e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/resources/NOTICE @@ -0,0 +1,6 @@ +Apache Felix Dependency Manager Annotation +Copyright 2011-2017 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. diff --git a/ipojo/src/main/java/org/apache/felix/ipojo/DummyActivator.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/.gitignore similarity index 100% rename from ipojo/src/main/java/org/apache/felix/ipojo/DummyActivator.java rename to dependencymanager/org.apache.felix.dependencymanager.annotation/src/.gitignore diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/AdapterService.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/AdapterService.java new file mode 100644 index 00000000000..3cfc5c2360a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/AdapterService.java @@ -0,0 +1,197 @@ +/* + * 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.dm.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates an Adapater service component. Adapters, like {@link AspectService}, are used to "extend" + * existing services, and can publish different services based on the existing one. + * An example would be implementing a management interface for an existing service, etc .... + *

      When you annotate an adapter class with the @AdapterService annotation, it will be applied + * to any service that matches the implemented interface and filter. The adapter will be registered + * with the specified interface and existing properties from the original service plus any extra + * properties you supply here. If you declare the original service as a member it will be injected. + * + *

      For "add", "change", "remove" callbacks, the following method signatures are supported: + * + *

      {@code
      + * (Component comp, ServiceReference ref, Service service)
      + * (Component comp, ServiceReference ref, Object service)
      + * (Component comp, ServiceReference ref)
      + * (Component comp, Service service)
      + * (Component comp, Object service)
      + * (Component comp)
      + * (Component comp, Map properties, Service service)
      + * (ServiceReference ref, Service service)
      + * (ServiceReference ref, Object service)
      + * (ServiceReference ref)
      + * (Service service)
      + * (Service service, Map propeerties)
      + * (Map properties, Service, service)
      + * (Service service, Dictionary properties)
      + * (Dictionary properties, Service service)
      + * (Object service)
      + * }
      + * + *

      For "swap" callbacks, the following method signatures are supported: + * + *

      {@code
      + * (Service old, Service replace)
      + * (Object old, Object replace)
      + * (ServiceReference old, Service old, ServiceReference replace, Service replace)
      + * (ServiceReference old, Object old, ServiceReference replace, Object replace)
      + * (Component comp, Service old, Service replace)
      + * (Component comp, Object old, Object replace)
      + * (Component comp, ServiceReference old, Service old, ServiceReference replace, Service replace)
      + * (Component comp, ServiceReference old, Object old, ServiceReference replace, Object replace)
      + * (ServiceReference old, ServiceReference replace)
      + * (Component comp, ServiceReference old, ServiceReference replace)
      + * }
      + * + *

      Usage Examples

      + * + *

      Here, the AdapterService is registered into the OSGI registry each time an AdapteeService + * is found from the registry. The AdapterImpl class adapts the AdapteeService to the AdapterService. + * The AdapterService will also have the following service property: p1=v1, p2=v2 : + *

      + *
      + * 
      + * interface AdapteeService {
      + *     void method1();
      + *     void method2();
      + * }
      + * 
      + * @Component
      + * @Property(name="p1", value="v1")
      + * class Adaptee implements AdapteeService {
      + *     ...
      + * } 
      + * 
      + * interface AdapterService {
      + *     void doWork();
      + * }
      + * 
      + * @AdapterService(adapteeService = AdapteeService.class)
      + * @Property(name="p2", value="v2")
      + * class AdapterImpl implements AdapterService {
      + *     // The service we are adapting (injected by reflection)
      + *     volatile AdapteeService adaptee;
      + *   
      + *     public void doWork() {
      + *        adaptee.method1();
      + *        adaptee.method2();
      + *     }
      + * }
      + * 
      + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface AdapterService +{ + /** + * Sets the adapter service interface(s). By default, the directly implemented interface(s) is (are) used. + * @return the adapter service interface(s) + * @deprecated you can apply {@link Property} annotation directly on the component class. + */ + Class[] provides() default {}; + + /** + * Sets some additional properties to use with the adapter service registration. By default, + * the adapter will inherit all adaptee service properties. + * @return some additional properties + */ + Property[] properties() default {}; + + /** + * Sets the adaptee service interface this adapter is applying to. + * @return the adaptee service interface this adapter is applying to. + */ + Class adapteeService(); + + /** + * Sets the filter condition to use with the adapted service interface. + * @return the adaptee filter + */ + String adapteeFilter() default ""; + + /** + * Sets the static method used to create the adapter service implementation instance. + * By default, the default constructor of the annotated class is used. + * @return the factory method + */ + String factoryMethod() default ""; + + /** + * Sets the field name where to inject the original service. By default, the original service is injected + * in any attributes of the adapter implementation that are of the same type as the adaptee interface. + * @return the field used to inject the original service + */ + String field() default ""; + + /** + * The callback method to be invoked when the original service is available. This attribute can't be mixed with + * the field attribute. + * @return the add callback + */ + String added() default ""; + + /** + * The callback method to be invoked when the original service properties have changed. When this attribute is used, + * then the added attribute must also be used. + * @return the changed callback + */ + String changed() default ""; + + /** + * name of the callback method to invoke on swap. + * @return the swap callback + */ + String swap() default ""; + + /** + * The callback method to invoke when the service is lost. When this attribute is used, then the added attribute + * must also be used. + * @return the remove callback + */ + String removed() default ""; + + /** + * Specifies if adaptee service properties should be propagated to the adapter service. + * The component provided service properties take precedence over the propagated adaptee service properties, meaning + * that a give component service property overrides the same property present in the propagated adaptee service properties. + * @return the service propagation flag + */ + boolean propagate() default true; + + /** + * The service scope for the service of this Component. + * + *

      + * If not specified, the {@link ServiceScope#SINGLETON singleton} service + * scope is used. + */ + ServiceScope scope() default ServiceScope.SINGLETON; +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/AspectService.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/AspectService.java new file mode 100644 index 00000000000..b0731ad4435 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/AspectService.java @@ -0,0 +1,174 @@ +/* + * 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.dm.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates an Aspect service. Aspects allow you to define an interceptor, or chain of interceptors + * for a service (to add features like caching or logging, etc ...). The dependency manager intercepts + * the original service, and allows you to execute some code before invoking the original service ... + * The aspect will be applied to any service that matches the specified interface and filter and + * will be registered with the same interface and properties as the original service, plus any + * extra properties you supply here. If you declare the original service as a member it will be injected. + * + *

      For "add", "change", "remove" callbacks, the following method signatures are supported: + * + *

      {@code
      + * (Component comp, ServiceReference ref, Service service)
      + * (Component comp, ServiceReference ref, Object service)
      + * (Component comp, ServiceReference ref)
      + * (Component comp, Service service)
      + * (Component comp, Object service)
      + * (Component comp)
      + * (Component comp, Map properties, Service service)
      + * (ServiceReference ref, Service service)
      + * (ServiceReference ref, Object service)
      + * (ServiceReference ref)
      + * (Service service)
      + * (Service service, Map propeerties)
      + * (Map properties, Service, service)
      + * (Service service, Dictionary properties)
      + * (Dictionary properties, Service service)
      + * (Object service)
      + * }
      + * + *

      For "swap" callbacks, the following method signatures are supported: + * + *

      {@code
      + * (Service old, Service replace)
      + * (Object old, Object replace)
      + * (ServiceReference old, Service old, ServiceReference replace, Service replace)
      + * (ServiceReference old, Object old, ServiceReference replace, Object replace)
      + * (Component comp, Service old, Service replace)
      + * (Component comp, Object old, Object replace)
      + * (Component comp, ServiceReference old, Service old, ServiceReference replace, Service replace)
      + * (Component comp, ServiceReference old, Object old, ServiceReference replace, Object replace)
      + * (ServiceReference old, ServiceReference replace)
      + * (Component comp, ServiceReference old, ServiceReference replace)
      + * }
      + * + *

      Usage Examples

      + * + *

      Here, the AspectService is registered into the OSGI registry each time an InterceptedService + * is found from the registry. The AspectService class intercepts the InterceptedService, and decorates + * its "doWork()" method. This aspect uses a rank with value "10", meaning that it will intercept some + * other eventual aspects with lower ranks. The Aspect also uses a service property (param=value), and + * include eventual service properties found from the InterceptedService: + *

      + *
      + * 
      + * @AspectService(ranking=10))
      + * class AspectService implements InterceptedService {
      + *     // The service we are intercepting (injected by reflection)
      + *     volatile InterceptedService intercepted;
      + *   
      + *     public void doWork() {
      + *        intercepted.doWork();
      + *     }
      + * }
      + * 
      + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface AspectService +{ + /** + * Sets the service interface to apply the aspect to. By default, the directly implemented interface is used. + * @return the service aspect + */ + Class service() default Object.class; + + /** + * Sets the filter condition to use with the service interface this aspect is applying to. + * @return the service aspect filter + */ + String filter() default ""; + + /** + * Sets Additional properties to use with the aspect service registration + * @return the aspect service properties. + * @deprecated you can apply {@link Property} annotation directly on the component class. + */ + Property[] properties() default {}; + + /** + * Sets the ranking of this aspect. Since aspects are chained, the ranking defines the order in which they are chained. + * Chain ranking is implemented as a service ranking so service lookups automatically retrieve the top of the chain. + * @return the aspect service rank + */ + int ranking(); + + /** + * Sets the field name where to inject the original service. By default, the original service is injected + * in any attributes of the aspect implementation that are of the same type as the aspect interface. + * @return the field used to inject the original service + */ + String field() default ""; + + /** + * The callback method to be invoked when the original service is available. This attribute can't be mixed with + * the field attribute. + * @return the add callback + */ + String added() default ""; + + /** + * The callback method to be invoked when the original service properties have changed. When this attribute is used, + * then the added attribute must also be used. + * @return the changed callback + */ + String changed() default ""; + + /** + * The callback method to invoke when the service is lost. When this attribute is used, then the added attribute + * must also be used. + * @return the remove callback + */ + String removed() default ""; + + /** + * name of the callback method to invoke on swap. + * @return the swap callback + */ + String swap() default ""; + + /** + * Sets the static method used to create the AspectService implementation instance. The + * default constructor of the annotated class is used. The factoryMethod can be used to provide a specific + * aspect implements, like a DynamicProxy. + * @return the aspect service factory method + */ + String factoryMethod() default ""; + + /** + * The service scope for the service of this Component. + * + *

      + * If not specified, the {@link ServiceScope#SINGLETON singleton} service + * scope is used. + */ + ServiceScope scope() default ServiceScope.SINGLETON; +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/BundleAdapterService.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/BundleAdapterService.java new file mode 100644 index 00000000000..5230499bb62 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/BundleAdapterService.java @@ -0,0 +1,112 @@ +/* + * 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.dm.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.osgi.framework.Bundle; + +/** + * Annotates a bundle adapter service class. Bundle adapters are similar to {@link AdapterService}, + * but instead of adapting a service, they adapt a bundle with a certain set of states (STARTED|INSTALLED|...), + * and provide a service on top of it.

      + * The bundle adapter will be applied to any bundle that matches the specified bundle state mask and + * filter conditions, which may match some of the bundle OSGi manifest headers. For each matching + * bundle an adapter will be created based on the adapter implementation class. The adapter will be + * registered with the specified interface and with service properties found from the original bundle + * OSGi manifest headers plus any extra properties you supply here. + * If you declare the original bundle as a member it will be injected. + * + *

      Usage Examples

      + * + *

      In the following example, a "VideoPlayer" Service is registered into the OSGi registry each time + * an active bundle containing a "Video-Path" manifest header is detected: + * + *

      + *
      + * @BundleAdapterService(filter = "(Video-Path=*)", stateMask = Bundle.ACTIVE, propagate=true)
      + * public class VideoPlayerImpl implements VideoPlayer {
      + *     volatile Bundle bundle; // Injected by reflection
      + *     
      + *     void play() {
      + *         URL mpegFile = bundle.getEntry(bundle.getHeaders().get("Video-Path"));
      + *         // play the video provided by the bundle ...
      + *     }     
      + * }
      + * 
      + *
      + * + * @author Felix Project Team + */ +public @Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +@interface BundleAdapterService +{ + /** + * The interface(s) to use when registering adapters. By default, the interface(s) directly implemented + * by the annotated class is (are) used. + * @return the interface(s) to use when registering adapters + */ + Class[] provides() default {}; + + /** + * Additional properties to use with the service registration + * @return the bundle adapter properties + * @deprecated you can apply {@link Property} annotation directly on the component class. + */ + Property[] properties() default {}; + + /** + * The ldap filter used to match some manifest headers of the adapted bundle. + * @return the bundle adapter filter + */ + String filter(); + + /** + * the bundle state mask to apply + * @return the bundle state mask to apply + */ + int stateMask() default Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE; + + /** + * Specifies if manifest headers from the bundle should be propagated to the service properties. + * The component service properties take precedence over the propagated bundle manifest headers. + * + * @return the propagation flag + */ + boolean propagate() default true; + + /** + * Sets the static method used to create the BundleAdapterService implementation instance. + * @return the factory method + */ + String factoryMethod() default ""; + + /** + * The service scope for the service of this Component. + * + *

      + * If not specified, the {@link ServiceScope#SINGLETON singleton} service + * scope is used. + */ + ServiceScope scope() default ServiceScope.SINGLETON; +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/BundleDependency.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/BundleDependency.java new file mode 100644 index 00000000000..62eed9b5f73 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/BundleDependency.java @@ -0,0 +1,115 @@ +/* + * 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.dm.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.osgi.framework.Bundle; + +/** + * Annotates a class or method for a bundle dependency. A bundle dependency allows you to + * depend on a bundle in a certain set of states (INSTALLED|RESOLVED|STARTED|...), as + * indicated by a state mask. You can also use a filter condition that is matched against + * all manifest entries. When applied on a class field, optional unavailable dependencies + * are injected with a NullObject. + * + *

      Usage Examples

      + * + *

      In the following example, the "SCR" Component allows to track + * all bundles containing a specific "Service-Component" OSGi header, in order to load + * and manage all Declarative Service components specified in the SCR xml documents referenced by the header: + * + *

      + *
      + * @Component
      + * public class SCR {
      + *     @BundleDependency(required = false, removed = "unloadServiceComponents", filter = "(Service-Component=*)", stateMask = Bundle.ACTIVE)
      + *     void loadServiceComponents(Bundle b) {
      + *         String descriptorPaths = (String) b.getHeaders().get("Service-Component");
      + *         // load all service component specified in the XML descriptorPaths files ...
      + *     }
      + *
      + *     void unloadServiceComponents(Bundle b) {
      + *         // unload all service component we loaded from our "loadServiceComponents" method.
      + *     }
      + * }
      + * 
      + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.METHOD) +public @interface BundleDependency +{ + /** + * Returns the callback method to be invoked when the bundle have been updated + * @return the change callback + */ + String changed() default ""; + + /** + * Returns the callback method to invoke when the bundle is lost. + * @return the remove callback + */ + String removed() default ""; + + /** + * Returns whether the dependency is required or not. + * @return the required flag + */ + boolean required() default true; + + /** + * Returns the ldap filter dependency matching some bundle manifest headers + * @return the filter + */ + String filter() default ""; + + /** + * Returns the bundle state mask + * @return the state mask + */ + int stateMask() default Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE; + + /** + * Specifies if the manifest headers from the bundle should be propagated to the service properties. + * The provided service properties take precedence over the propagated bundle manifest headers. + * It means a bundle manifest header won't override a component service property having the same name. + * + * @return the propagation flag + */ + boolean propagate() default false; + + /** + * The name used when dynamically configuring this dependency from the init method. + * Specifying this attribute allows to dynamically configure the dependency + * filter and required flag from the Service's init method. + * All unnamed dependencies will be injected before the init() method; so from the init() method, you can + * then pick up whatever information needed from already injected (unnamed) dependencies, and configure dynamically + * your named dependencies, which will then be calculated once the init() method returns. + * + *

      See {@link Init} annotation for an example usage of a dependency dynamically configured from the init method. + * @return the dependency name used to dynamically configure the dependency from the init callback + */ + String name() default ""; +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Component.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Component.java new file mode 100644 index 00000000000..05fede3a62a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Component.java @@ -0,0 +1,177 @@ +/* + * 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.dm.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates an OSGi Component class with its dependencies. Components are the main building + * blocks for OSGi applications. They can publish themselves as a service, and/or they can have + * dependencies. These dependencies will influence their life cycle as component will only be + * activated when all required dependencies are available. + * By default, all directly implemented interfaces are registered into the OSGi registry, + * and the component is instantiated automatically, when the component bundle is started and + * when the component dependencies are available. If you need to take control of when and how + * much component instances must be created, then you can use the factoryName + * annotation attribute.

      + * If a factoryPid attribute is set, the component is not started automatically + * during bundle startup, and the component can then be instantiated multiple times using + * Configuration Admin "Factory Configurations". + * + *

      Usage Examples

      + * + * Here is a sample showing a Hello component, which depends on a configuration dependency: + *
      + * + *
      + * /**
      + *   * This component will be activated once the bundle is started and when all required dependencies
      + *   * are available.
      + *   */
      + * @Component
      + * class Hello implements HelloService {
      + *     @ConfigurationDependency(pid="my.pid")
      + *     void configure(Dictionary conf) {
      + *          // Configure or reconfigure our component.
      + *     }
      + *   
      + *     @Start
      + *     void start() {
      + *         // Our component is starting and is about to be registered in the OSGi registry as a HelloService service.
      + *     }   
      + * }
      + * 
      + *
      + * + * Here is a sample showing how a HelloFactory component may dynamically instantiate several Hello component instances, + * using Configuration Admin "Factory Configurations": + *
      + * + *
      + *  /**
      + *    * All component instances will be created/updated/removed by the "HelloFactory" component
      + *    */
      + *  @Component(factoryPid="my.factory.pid")
      + *  class Hello implements HelloService {                 
      + *      void updated(Dictionary conf) {
      + *          // Configure or reconfigure our component. The conf is provided by the factory,
      + *      }
      + *       
      + *      @Start
      + *      void start() {
      + *          // Our component is starting and is about to be registered in the OSGi registry as a Hello service.
      + *      }       
      + *  } 
      + *
      + *  /**
      + *    * This class will instantiate some Hello component instances
      + *    */
      + *  @Component 
      + *  class HelloFactory {
      + *     @ServiceDependency
      + *     void bind(ConfigurationAdmin cm) {
      + *          // instantiate a first instance of Hello component
      + *          Configuration c1 = cm.createFactoryConfiguration("my.factory.pid", "?");
      + *          Hashtable props = new Hashtable();
      + *          newprops.put("key", "value1");
      + *          c1.update(props);
      + *          
      + *          // instantiate another instance of Hello component
      + *          Configuration c2 = cm.createFactoryConfiguration("my.factory.pid", "?");
      + *          props = new Hashtable();
      + *          newprops.put("key", "value2");
      + *          c2.update(props);
      + *          
      + *          // destroy the two instances of X component
      + *          c1.delete();
      + *          c2.delete();
      + *     }
      + *  }
      + * 
      + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface Component +{ + /** + * Sets list of provided interfaces. By default, the directly implemented interfaces are provided. + * @return the provided interfaces + */ + Class[] provides() default {}; + + /** + * Sets the static method used to create the components implementation instance. + * @return the factory method used to instantiate the component + */ + String factoryMethod() default ""; + + /** + * Returns the factory pid whose configurations will instantiate the annotated service class. Leaving this attribute + * unset means your component is a singleton. If you specify a factory pid, then this component will be instantiated + * each time a corresponding factory configuration is created. + * + * @return the factory pid + */ + String factoryPid() default ""; + + /** + * The Update method to invoke (defaulting to "updated"), when a factory configuration is created or updated. + * Only used if the factoryPid attribute is set. + * The updated callback supported signatures are the following:

      + *

      • callback(Dictionary) + *
      • callback(Component, Dictionary) + *
      • callback(Component, Configuration ... configTypes) // type safe configuration interface(s) + *
      • callback(Configuration ... configTypes) // type safe configuration interface(s) + *
      • callback(Dictionary, Configuration ... configTypes) // type safe configuration interfaces(s) + *
      • callback(Component, Dictionary, Configuration ... configTypes) // type safe configuration interfaces(s) + *
      + * @return the updated callback + */ + String updated() default "updated"; + + /** + * Returns true if the factory configuration properties must be published to the service properties. + * Only used if the factoryPid attribute is set. + * @return true if configuration must be published along with the service, false if not. + */ + boolean propagate() default false; + + /** + * The service scope for the service of this Component. + * + *

      + * If not specified, the {@link ServiceScope#SINGLETON singleton} service + * scope is used. + */ + ServiceScope scope() default ServiceScope.SINGLETON; + + /** + * Sets list of provided service properties. Since R7 version, Property annotation is repeatable and you can directly + * apply it on top of the component class multiple times, instead of using the Component properties attribute. + * @return the component properties. + * @deprecated you can apply {@link Property} annotation directly on the component class. + */ + Property[] properties() default {}; +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Composition.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Composition.java new file mode 100644 index 00000000000..a6bc8dfe7e2 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Composition.java @@ -0,0 +1,81 @@ +/* + * 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.dm.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates a method returning the list of objects which are part of a Component implementation. + * When implementing complex Components, you often need to use more than one object instances. + * Moreover, several of these instances might want to have dependencies injected, as well as lifecycle + * callbacks invoked, like the methods annotated with {@link Init}, {@link Start}, {@link Stop}, + * {@link Destroy} annotations. In such cases you can tell the dependency manager which instances to + * consider, by annotating a method in your Component, returning a list of objects which are part + * of the implementation. + *

      + * This annotation may be applied on a method which is part of class annotated with either a {@link Component}, + * {@link AspectService}, or {@link AdapterService}. + * + *

      Usage Examples

      + * + *

      Here, the "MyComponent" component is composed of the Helper class, which is also injected with + * service dependencies. The lifecycle callbacks are also invoked in the Helper (if the Helper defines + * them): + *

      + *
      + *
      + * class Helper {
      + *     volatile LogService logService; // Injected
      + *     
      + *     void start() {} // lifecycle callback
      + *     void bind(OtherService otherService) {} // injected
      + * }
      + * 
      + * @Component
      + * class MyComponent {
      + *     // Helper which will also be injected with our service dependencies
      + *     private Helper helper = new Helper();
      + *      
      + *     @Composition
      + *     Object[] getComposition() {
      + *         return new Object[] { this, helper }; 
      + *     }
      + *
      + *     @ServiceDependency
      + *     private LogService logService; // Helper.logService will be also be injected, if defined.
      + *     
      + *     @Start
      + *     void start() {} // the Helper.start() method will also be called, if defined
      + *     
      + *     @ServiceDependency
      + *     void bind(OtherService otherService) {} // the Helper.bind() method will also be called, if defined     
      + * }
      + * 
      + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.METHOD) +public @interface Composition +{ +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/ConfigurationDependency.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/ConfigurationDependency.java new file mode 100644 index 00000000000..80b1e9475ab --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/ConfigurationDependency.java @@ -0,0 +1,256 @@ +/* + * 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.dm.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Collection; +import java.util.Dictionary; +import java.util.Map; + +/** + * Annotates a method for injecting a Configuration Dependency. + * + *

      A configuration dependency + * is required by default, and allows you to depend on the availability of a valid configuration + * for your component. This dependency requires the OSGi Configuration Admin Service. + * + * Configuration Dependency callback is always invoked before any service dependency callbacks, and before init/start callbacks. + * + * The annotation can be applied on a callback method which accepts the following parameters: + * + *

        + *
      • callback(Dictionary) + *
      • callback(Component, Dictionary) + *
      • callback(Component, Configuration ... configTypes) // type safe configuration interface(s) + *
      • callback(Configuration ... configTypes) // type safe configuration interface(s) + *
      • callback(Dictionary, Configuration ... configTypes) // type safe configuration interfaces(s) + *
      • callback(Component, Dictionary, Configuration ... configTypes) // type safe configuration interfaces(s) + *
      + * + *

      Usage Examples

      + * + *

      In the following example, the Printer components depends on a configuration + * whose PID name is "sample.Printer". This service will initialize + * its ip/port number from the provided configuration. + * + *

      + *
      + *
      + * package sample;
      + * 
      + * @Component
      + * public class Printer {
      + *     @ConfigurationDependency(propagate=true) // Will use the fqdn of the  Printer interface as the pid.
      + *     void updated(Dictionary cnf) {
      + *         if (cnf != null) {
      + *             String ip = cnf.get("address");
      + *             int port = Integer.parseInt(cnf.get("port"));
      + *         }
      + *     }
      + * }
      + * 
      + *
      + * + * You can also define your own component properties using a custom type-safe interface: + * + *
      + *
      + * package sample;
      + * 
      + * interface PrinterConfig {
      + *     String getAddress();    	
      + *     int getPort();
      + * }
      + * 
      + *
      + * + * Next, we define our Printer service which depends on the PrinterConfig: + * + *
      + *
      + * package sample;
      + *
      + * @Component
      + * public class Printer {
      + *     @ConfigurationDependency // Will use the fqdn of the  PrinterConfig interface as the pid.
      + *     void updated(PrinterConfig cnf) {
      + *         if (cnf != null) {
      + *             String ip = cnf.getAddress();
      + *             int port = cnf.getPort();
      + *         }
      + *     }
      + * }
      + * 
      + *
      + * + * In the above example, the updated callback accepts a type-safe configuration type (and its fqdn is used as the pid). + *

      Configuration type is a new feature that allows you to specify an interface that is implemented + * by DM and such interface is then injected to your callback instead of the actual Dictionary. + * Using such configuration interface provides a way for creating type-safe configurations from a actual {@link Dictionary} that is + * normally injected by Dependency Manager. + * The callback accepts in argument an interface that you have to provide, and DM will inject a proxy that converts + * method calls from your configuration-type to lookups in the actual map or dictionary. The results of these lookups are then + * converted to the expected return type of the invoked configuration method.
      + * As proxies are injected, no implementations of the desired configuration-type are necessary! + *

      + *

      + * The lookups performed are based on the name of the method called on the configuration type. The method names are + * "mangled" to the following form: [lower case letter] [any valid character]*. Method names starting with + * get or is (JavaBean convention) are stripped from these prefixes. For example: given a dictionary + * with the key "foo" can be accessed from a configuration-type using the following method names: + * foo(), getFoo() and isFoo().

      + * If the property contains a dot (which is invalid in java method names), then dots (".") can be converted using the following conventions: + *

        + * + *
      • if the method name follows the javabean convention and/or kamel casing convention, then each capital letter is assumed to map to a "dot", + * followed by the same letter in lower case. This means only lower case properties are + * supported in this case. Example: getFooBar() or fooBar() will map to "foo.bar" property. + * + *
      • else, if the method name follows the standard OSGi metatype specification, then dots + * are encoded as "_"; and "_" is encoded as "__". (see OSGi r6 compendium, chapter 105.9.2). + * Example: "foo_BAR()" is mapped to "foo.BAR" property; "foo__BAR_zoo()" is mapped to "foo_BAR.zoo" property. + *
      + *

      + * The return values supported are: primitive types (or their object wrappers), strings, enums, arrays of + * primitives/strings, {@link Collection} types, {@link Map} types, {@link Class}es and interfaces. When an interface is + * returned, it is treated equally to a configuration type, that is, it is returned as a proxy. + *

      + *

      + * Arrays can be represented either as comma-separated values, optionally enclosed in square brackets. For example: + * [ a, b, c ] and a, b,c are both considered an array of length 3 with the values "a", "b" and "c". + * Alternatively, you can append the array index to the key in the dictionary to obtain the same: a dictionary with + * "arr.0" => "a", "arr.1" => "b", "arr.2" => "c" would result in the same array as the earlier examples. + *

      + *

      + * Maps can be represented as single string values similarly as arrays, each value consisting of both the key and value + * separated by a dot. Optionally, the value can be enclosed in curly brackets. Similar to array, you can use the same + * dot notation using the keys. For example, a dictionary with + * + *

      {@code "map" => "{key1.value1, key2.value2}"}
      + * + * and a dictionary with + * + *
      {@code "map.key1" => "value1", "map2.key2" => "value2"}
      + * + * result in the same map being returned. + * Instead of a map, you could also define an interface with the methods getKey1() and getKey2 and use + * that interface as return type instead of a {@link Map}. + * + *

      + * In case a lookup does not yield a value from the underlying map or dictionary, the following rules are applied: + *

        + *
      1. primitive types yield their default value, as defined by the Java Specification; + *
      2. string, {@link Class}es and enum values yield null; + *
      3. for arrays, collections and maps, an empty array/collection/map is returned; + *
      4. for other interface types that are treated as configuration type a null-object is returned. + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.METHOD) +public @interface ConfigurationDependency +{ + /** + * Returns the pid for a given service (by default, the pid is the service class name, of the FQDN of + * the configuration type found in the updated callback signature. + * @return the pid for a given service (default = Service class name) + */ + String pid() default ""; + + /** + * Returns the pid from a class name. The full class name will be used as the configuration PID. + * @return the pid class whose FQDN name is used as the configuration PID. + */ + Class pidClass() default Object.class; + + /** + * Returns true if the configuration properties must be published along with the service. + * Any additional service properties specified directly are merged with these. The configuration + * dependency properties take precedence over the component service properties, meaning that a given configuration + * property will override the same property that is already present in the component service properties. + * + * @return true if configuration must be published along with the service, false if not. + */ + boolean propagate() default false; + + /** + * Sets the required flag which determines if this configuration dependency is required or not. + * A configuration dependency is required by default. + * + * @return this service dependency + */ + boolean required() default true; + + /** + * The name for this configuration dependency. When you give a name a dependency, it won't be evaluated + * immediately, but after the component's init method has been called, and from the init method, you can then return + * a map in order to dynamically configure the configuration dependency (the map has to contain a "pid" and/or "propagate" + * flag, prefixed with the dependency name). Then the dependency will be evaluated after the component init method, and will + * be injected before the start method. + * + *

      Usage example of a Configuration dependency whose pid and propagate flag is configured dynamically from init method: + * + *

      +     *  /**
      +     *    * A Service that dynamically defines an extra dynamic configuration dependency from its init method. 
      +     *    */
      +     *  @Component
      +     *  class X {
      +     *      private Dictionary m_config;
      +     *      
      +     *      // Inject initial Configuration (injected before any other required dependencies)
      +     *      @ConfigurationDependency
      +     *      void componentConfiguration(Dictionary config) {
      +     *           // you must throw an exception if the configuration is not valid
      +     *           m_config = config;
      +     *      }
      +     *      
      +     *      /**
      +     *       * All unnamed dependencies are injected: we can now configure our dynamic configuration whose dependency name is "global".
      +     *       */
      +     *      @Init
      +     *      Map init() {
      +     *          return new HashMap() {{
      +     *              put("global.pid", m_config.get("globalConfig.pid"));
      +     *              put("global.propagate", m_config.get("globalConfig.propagate"));
      +     *          }};
      +     *      } 
      +     * 
      +     *      // Injected after init, and dynamically configured by the init method.
      +     *      @ConfigurationDependency(name="global")
      +     *      void globalConfiguration(Dictionary globalConfig) {
      +     *           // you must throw an exception if the configuration is not valid
      +     *      }
      +     * 
      +     *      /**
      +     *       * All dependencies are injected and our service is now ready to be published.
      +     *       */
      +     *      @Start
      +     *      void start() {
      +     *      }
      +     *  }
      +     *  
      + * @return the dependency name used to configure the dependency dynamically from init callback + */ + String name() default ""; +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Destroy.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Destroy.java new file mode 100644 index 00000000000..f81088ab58e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Destroy.java @@ -0,0 +1,53 @@ +/* + * 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.dm.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates a method which is invoked when the component is destroyed. + * The method is called when the component's bundle is stopped, or when one of its + * required dependency is lost (unless the dependency has been defined as an "instance bound" + * dependency using the Dependency Manager API). + * + * + *

      Usage Examples

      + *
      + *
      + * @Component
      + * class MyComponent {
      + *     @ServiceDependency
      + *     private LogService logService; // Required dependency over the log service.
      + *     
      + *     @Destroy
      + *     void destroyed() {} // called if bundle is stopped or if we have lost some required dependencies.     
      + * }
      + * 
      + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.METHOD) +public @interface Destroy +{ +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Init.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Init.java new file mode 100644 index 00000000000..82b4fac32b5 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Init.java @@ -0,0 +1,188 @@ +/* + * 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.dm.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates a method used to configure dynamic dependencies. + * When this method is invoked, all required dependencies (except the ones declared with a name + * attribute) are already injected, and optional dependencies on class fields + * are also already injected (possibly with NullObjects). + * + * The purpose of the @Init method is to either declare more dynamic dependencies using the DM API, or to + * return a Map used to dynamically configure dependencies that are annotated using a name attribute. + * + * After the init method returns, the added or configured dependencies are then tracked, and when all + * dependencies are injected, then the start method (annotated with @Start) is invoked. + * + * The method annotated with @Init may have the following signatures, + *
        + *
      1. void init(Component component)
      2. + *
      3. void init()
      4. + *
      5. Map init(Component component)
      6. + *
      7. Map init()
      8. + *
      + * + * When the init method defines a Component argument, it can then be used by the method to add more dependencie using the Dependency Manager API. + * + *

      Usage Examples

      + * In this sample, the "PersistenceImpl" component dynamically configures the "storage" dependency from the "init" method. + * The dependency "required" flag and filter string are derived from an xml configuration that is already injected before the init + * method. + * + *
      + *
      + * @Component
      + * public class PersistenceImpl implements Persistence {
      + *     // Injected before init.
      + *     @ConfigurationDependency
      + *     void updated(Dictionary conf) {
      + *        if (conf != null) {
      + *           _xmlConfiguration = parseXmlConfiguration(conf.get("xmlConfiguration"));
      + *        }
      + *     }
      + *     
      + *     // Parsed xml configuration, where we'll get our storage service filter and required dependency flag.
      + *     XmlConfiguration _xmlConfiguration;
      + *  
      + *     // Dynamically configure the dependency declared with a "storage" name.
      + *     @Init
      + *     Map<String, String> init() {
      + *        Map<String, String> props = new HashMap<>();
      + *        props.put("storage.required", Boolean.toString(_xmlConfiguration.isStorageRequired()))
      + *        props.put("storage.filter", "(type=" + _xmlConfiguration.getStorageType() + ")");
      + *        return props;       
      + *     }
      + *  
      + *     // Injected after init (dependency filter is defined dynamically from our init method).
      + *     @ServiceDependency(name="storage")
      + *     Storage storage;
      + * 
      + *     // All dependencies injected, including dynamic dependencies defined from init method.
      + *     @Start
      + *     void start() {
      + *        log.log(LogService.LOG_WARNING, "start");
      + *     }
      + * 
      + *     @Override
      + *     void store(String key, String value) {
      + *        storage.store(key, value);
      + *     }
      + * }
      + * 
      + *
      + * + * Same example as above, but this time the dependency is added from the init method using the Dependency Manager API: + * + *
      + *
      + * @Component
      + * public class PersistenceImpl implements Persistence {
      + *     // Injected before init.
      + *     @ConfigurationDependency
      + *     void updated(Dictionary conf) {
      + *        if (conf != null) {
      + *           _xmlConfiguration = parseXmlConfiguration(conf.get("xmlConfiguration"));
      + *        }
      + *     }
      + *     
      + *     // Parsed xml configuration, where we'll get our storage service filter and required dependency flag.
      + *     XmlConfiguration _xmlConfiguration;
      + *  
      + *     // Dynamically configure the dependency declared with a "storage" name.
      + *     @Init
      + *     void init(org.apache.felix.dm.Comppnent myComponent) {
      + *        boolean required = _xmlConfiguration.isStorageRequired();
      + *        String filter =  _xmlConfiguration.getStorageType();
      + *        DependencyManager dm = myComponent.getDependencyManager();
      + *        myComponent.add(dm.createServiceDependency().setService(Storage.class, filter).setRequired(required));
      + *     }
      + *  
      + *     // Injected after init, later, when the dependency added from the init() method is satisfied
      + *     volatile Storage storage;
      + * 
      + *     // All dependencies injected, including dynamic dependencies defined from init method.
      + *     @Start
      + *     void start() {
      + *        log.log(LogService.LOG_WARNING, "start");
      + *     }
      + * 
      + *     @Override
      + *     void store(String key, String value) {
      + *        storage.store(key, value);
      + *     }
      + * }
      + * 
      + *
      + * + * Same example as above, but this time the dependency is added from the init method using the Dependency Manager Lambda: + * + *
      + *
      + * import static org.apache.felix.dm.lambda.DependencyManagerActivator.component;
      + * 
      + * @Component
      + * public class PersistenceImpl implements Persistence {
      + *     // Injected before init.
      + *     @ConfigurationDependency
      + *     void updated(Dictionary conf) {
      + *        if (conf != null) {
      + *           _xmlConfiguration = parseXmlConfiguration(conf.get("xmlConfiguration"));
      + *        }
      + *     }
      + *     
      + *     // Parsed xml configuration, where we'll get our storage service filter and required dependency flag.
      + *     XmlConfiguration _xmlConfiguration;
      + *  
      + *     // Dynamically configure the dependency declared with a "storage" name.
      + *     @Init
      + *     void init(org.apache.felix.dm.Comppnent myComponent) {
      + *        boolean required = _xmlConfiguration.isStorageRequired();
      + *        String filter =  _xmlConfiguration.getStorageType();
      + *        component(myComponent, comp -> comp.withSvc(Storage.class, filter, required));
      + *     }
      + *  
      + *     // Injected after init, later, when the dependency added from the init() method is satisfied
      + *     volatile Storage storage;
      + * 
      + *     // All dependencies injected, including dynamic dependencies defined from init method.
      + *     @Start
      + *     void start() {
      + *        log.log(LogService.LOG_WARNING, "start");
      + *     }
      + * 
      + *     @Override
      + *     void store(String key, String value) {
      + *        storage.store(key, value);
      + *     }
      + * }
      + * 
      + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.METHOD) +public @interface Init +{ +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Inject.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Inject.java new file mode 100644 index 00000000000..42e97390b70 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Inject.java @@ -0,0 +1,79 @@ +/* + * 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.dm.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Inject classes in a component instance field. + * The following injections are currently performed, depending on the type of the + * field this annotation is applied on: + *
        + *
      • BundleContext: the bundle context of the bundle + *
      • DependencyManager: the dependency manager instance + *
      • Component: the component instance of the dependency manager + *
      + * + *

      Usage Examples

      + *
      + * + *
      + * @Component
      + * class X implements Z {
      + *     @Inject
      + *     BundleContext bundleContext;
      + *   
      + *     @Inject
      + *     Component component;
      + *     
      + *     @Inject
      + *     DependencyManager manager;
      + *   
      + *     OtherService otherService;
      + *   
      + *     @Init
      + *     void init() {
      + *         System.out.println("Bundle Context: " + bundleContext);
      + *         System.out.println("Manager: " + manager);
      + *         
      + *         // Use DM API for defining an extra service dependency
      + *         componnent.add(manager.createServiceDependency()
      + *                               .setService(OtherService.class)
      + *                               .setRequired(true)
      + *                               .setInstanceBound(true));
      + *     }
      + *     
      + *     @Start
      + *     void start() {
      + *         System.out.println("OtherService: " + otherService);
      + *     }
      + * }
      + * 
      + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.FIELD) +public @interface Inject +{ +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/LifecycleController.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/LifecycleController.java new file mode 100644 index 00000000000..0f42364012f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/LifecycleController.java @@ -0,0 +1,97 @@ +/* + * 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.dm.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Injects a Runnable object in a Service for starting/stopping it programatically. + * By default, a Service is implicitly started when the service's bundle is started and when + * all required dependencies are satisfied. However, it is sometimes required to programatically + * take control of when the service is started or stopped. In this case, the injected Runnable + * can be invoked in order to start/register (or stop/unregister) a Service at any time. When this annotation + * is used, then the Service on which this annotation is applied is not activated by default, and you have to + * call the injected Runnable yourself. + * + *

      Usage Examples

      + *
      + * + *
      + * /**
      + *   * This Service will be registered programmatically into the OSGi registry, using the LifecycleController annotation.
      + *   */
      + * @Component
      + * class X implements Z {
      + *     @LifecycleController
      + *     Runnable starter
      + *     
      + *     @LifecycleController(start=false)
      + *     Runnable stopper
      + *   
      + *     @Init
      + *     void init() {
      + *         // At this point, all required dependencies are there, but we'll activate our service in 2 seconds ...
      + *         Thread t = new Thread() {
      + *            public void run() {
      + *              sleep(2000);
      + *              // start our "Z" service (our "start" method will be called, juste before service registration
      + *              starter.run();
      + *              
      + *              sleep(2000);
      + *              // now, stop/unregister the "Z" service (we'll then be called in our stop() method
      + *              stopper.run();
      + *            }
      + *          };
      + *          t.start();
      + *     }
      + *     
      + *     @Start
      + *     public void start() {
      + *         // This method will be called after we invoke our starter Runnable, and our service will be
      + *         // published after our method returns, as in normal case.
      + *     }
      + *
      + *     @Stop
      + *     public void stop() {
      + *         // This method will be called after we invoke our "stop" Runnable, and our service will be
      + *         // unregistered before our method is invoked, as in normal case. Notice that the service won't
      + *         // be destroyed here, and the "starter" runnable can be re-invoked later.
      + *     }
      + * }
      + * 
      + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.FIELD) +public @interface LifecycleController +{ + /** + * Specifies the action to be performed when the injected runnable is invoked. By default, the + * Runnable will fire a Service Component activation, when invoked. If you specify this attribute + * to false, then the Service Component will be stopped, when the runnable is invoked. + * @return true if the component must be started when you invoke the injected runnable, or false if + * the component must stopped when invoking the runnable. + */ + public boolean start() default true; +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Property.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Property.java new file mode 100644 index 00000000000..5aa527d6b3d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Property.java @@ -0,0 +1,154 @@ +/* + * 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.dm.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used to describe a property key-value(s) pair. Such annotation can be applied on components.

      + * + * Property value(s) type is String by default, and the type is scalar if the value is single-valued, + * or an array if the value is multi-valued. You can apply this annotation on a component class multiple times + * (it's a java8 repeatable property). + * + * Eight primitive types are supported: + *

        + *
      • String (default type) + *
      • Long + *
      • Double + *
      • Float + *
      • Integer + *
      • Byte + *
      • Boolean + *
      • Short + *
      + * + * You can specify the type of a property either using a combination of value and type attributes, + * or using one of the longValue/doubleValue/floatValue/intValue/byteValue/charValue/booleanValue/shortValue attributes. + * + * Notice that you can also specify service properties dynamically by returning a Map from a method + * annotated with {@link Start}. + * + *

      Usage Examples

      + *
      + *
      + * @Component
      + * @Property(name="p1", value="v")                      // String value type (scalar)
      + * @Property(name="p2", value={"s1", "s2"})             // Array of Strings
      + * @Property(name="service.ranking", intValue=10)       // Integer value type (scalar)
      + * @Property(name="p3", intValue={1,2})                 // Array of Integers
      + * @Property(name="p3", value="1", type=Long.class)     // Long value (scalar)
      + * class ServiceImpl implements Service {
      + * }
      + * 
      + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target( { ElementType.TYPE, ElementType.ANNOTATION_TYPE }) +@Repeatable(RepeatableProperty.class) +public @interface Property +{ + /** + * Returns the property name. + * @return this property name + */ + String name(); + + /** + * Returns the property value(s). The property value(s) is (are) + * parsed using the valueOf method of the class specified in the #type attribute + * (which is String by default). When the property value is single-value, then + * the value type is scalar (not an array). If the property value is multi-valued, then the value type + * is an array of the type specified in the {@link #type()} attribute (String by default). + * + * @return this property value(s). + */ + String[] value() default {}; + + /** + * Specifies how the {@link #value()} or {@link #values()} attributes are parsed. + * @return the property value type (String by default) used to parse {@link #value()} or {@link #values()} + * attribtues + */ + Class type() default String.class; + + /** + * A Long value or an array of Long values. + * @return the long value(s). + */ + long[] longValue() default {}; + + /** + * A Double value or an array of Double values. + * @return the double value(s). + */ + double[] doubleValue() default {}; + + /** + * A Float value or an array of Float values. + * @return the float value(s). + */ + float[] floatValue() default {}; + + /** + * An Integer value or an array of Integer values. + * @return the int value(s). + */ + int[] intValue() default {}; + + /** + * A Byte value or an array of Byte values. + * @return the byte value(s). + */ + byte[] byteValue() default {}; + + /** + * A Character value or an array of Character values. + * @return the char value(s). + */ + char[] charValue() default {}; + + /** + * A Boolean value or an array of Boolean values. + * @return the boolean value(s). + */ + boolean[] booleanValue() default {}; + + /** + * A Short value or an array of Short values. + * @return the short value(s). + */ + short[] shortValue() default {}; + + /** + * Returns an array of property values. + * The property value are parsed using the valueOf method of the class specified in the #type attribute + * (which is String by default). + * + * @return an array of property values. + * @deprecated use {@link #value()} attribute. + */ + String[] values() default {}; +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/PropertyType.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/PropertyType.java new file mode 100644 index 00000000000..5bbecff88b0 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/PropertyType.java @@ -0,0 +1,150 @@ +/* + * 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.dm.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * When defining component service properties, one way to achieve this is to apply the {@link Property} annotation on your component class name. + * Now, you can also define your own component property type interfaces and apply them directly on your components (instead of the + * {@link Property} annotation). The PropertyType annotation is closely similar to standard OSGi r7 declarative service @ComponentPropertyType + * (which is also supported by dependency manager annotations). + * + *

      Usage Examples

      + * + * Let’s assume your write an OSGi r7 jax rs servlet context which needs the two following service properties: + * + *

        + *
      • osgi.http.whiteboard.context.name + *
      • osgi.http.whiteboard.context.path + *
      + * + *

      Then you can first define your own annotation (but you could also reuse the default annotations provided by the jaxrs whiteboard r7 api): + * (notice that in the annotation, you can define default service property values): + * + *

      + *
      + * @PropertyType
      + * @interface ServletContext {
      + *     String osgi_http_whiteboard_context_name() default AppServletContext.NAME;
      + *     String osgi_http_whiteboard_context_path();
      + * }
      + * 
      + *
      + * + * In the above, the underscore is mapped to ".". + * Then you can apply the above annotation on top of your component like this: + * + *
      + *
      + * @Component
      + * @ServletContext(osgi_http_whiteboard_context_path="/game")
      + * public class AppServletContext extends ServletContextHelper {
      + * }
      + * 
      + *
      + * + * You can also use configuration admin service in order to override the default service properties: + * + *
      + *
      + * @Component
      + * @ServletContext(osgi_http_whiteboard_context_path="/game")
      + * public class AppServletContext extends ServletContextHelper {
      + *     @ConfigurationDependency(propagate=true, pid="my.pid")
      + *     void updated(ServletContext cnf) {
      + *        // if some properties are not present in the configuration, then the ones used in the annotation will be used.
      + *        // The configuration admin properties, if defined, will override the default configurations defined in the annotations
      + *     }
      + * }
      + * 
      + *
      + * + * You can also define multiple property type annotations, and possibly single valued annotation. In this case, you can use + * the standard R7 PREFIX_ constants in order to specify the property prefix, and the property name will be derived from the + * single valued annotation (using camel case convention): + * + *
      + *
      + * @PropertyType
      + * @interface ContextName { // will map to "osgi.http.whiteboard.context.name" property name
      + *     String PREFIX="osgi.http.whiteboard.";
      + *     String value();
      + * }
      + * 
      + * @PropertyType
      + * @interface ContextPath { // will map to "osgi.http.whiteboard.context.path" property name
      + *     String PREFIX="osgi.http.whiteboard.";
      + *     String value();
      + * }
      + * 
      + * @Component
      + * @ContextName(AppServletContext.NAME)
      + * @ContextPath("/game")
      + * public class AppServletContext extends ServletContextHelper {
      + * }
      + * 
      + *
      + * + * Same example as above, but also using configuration admin service in order to override default service properties: Here, as in OSGi r7 declarative service, + * you can define a callback method which accepts as arguments all (or some of) the defined property types: + * + *
      + *
      + * @Component
      + * @ContextName(AppServletContext.NAME)
      + * @ContextPath("/game")
      + * public class AppServletContext extends ServletContextHelper {
      + *     @ConfigurationDependency(propagate=true, pid="my.pid")
      + *     void updated(ContextName ctxName, ContextPath ctxPath) {
      + *        // if some properties are not present in the configuration, then the ones used in the annotation will be used.
      + *        // The configuration admin properties, if defined, will override the default configurations defined in the annotations
      + *     }
      + * }
      + * 
      + *
      + * + * The following is the same example as above, but this time the configuration callback can also define a Dictionary in the first argument + * (in case you want to also get the raw configuration dictionary: + * + *
      + *
      + * @Component
      + * @ContextName(AppServletContext.NAME)
      + * @ContextPath("/game")
      + * public class AppServletContext extends ServletContextHelper {
      + *     @ConfigurationDependency(propagate=true, pid="my.pid")
      + *     void updated(Dictionary<String, Object> rawConfig, ContextName ctxName) {
      + *        // if some properties are not present in the configuration, then the ones used in the annotation will be used.
      + *        // The configuration admin properties, if defined, will override the default configurations defined in the annotations
      + *     }
      + * }
      + * 
      + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.ANNOTATION_TYPE) +public @interface PropertyType { + // meta-annotation +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Registered.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Registered.java new file mode 100644 index 00000000000..40d0c533137 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Registered.java @@ -0,0 +1,57 @@ +/* + * 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.dm.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation can be used to be notified when a component is registered. At this point, the + * component has been registered into the OSGI registry (if it provides some services). + * When a service is registered, the ServiceRegistration used to register the service is + * also passed to the method (if it takes a ServiceRegistration as parameter). + * + *

      Usage Examples

      + *
      + * + *
      + * @Component
      + * class X implements Z {
      + *     @Start
      + *     void start() {
      + *         // Our Z Service is about to be registered into the OSGi registry. 
      + *     }
      + *     
      + *     @Registered
      + *     void registered(ServiceRegistration sr) {
      + *        // At this point, our service has been registered into the registry.
      + *     }
      + * }
      + * 
      + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.METHOD) +public @interface Registered +{ +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/RepeatableProperty.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/RepeatableProperty.java new file mode 100644 index 00000000000..940c34c06f5 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/RepeatableProperty.java @@ -0,0 +1,41 @@ +/* + * 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.dm.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used to describe repeated Property annotation. You actually don't have to use directly this annotation, + * which is used to allow repeating several times the {@link Property} annotation on a given component class. + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target( { ElementType.TYPE, ElementType.ANNOTATION_TYPE }) +public @interface RepeatableProperty +{ + /** + * Returns the set of repeated {@link Property} applied on a given component class. + * @return the set of repeated {@link Property} applied on a given component class. + */ + Property[] value(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/ServiceDependency.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/ServiceDependency.java new file mode 100644 index 00000000000..a5f79d9c467 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/ServiceDependency.java @@ -0,0 +1,264 @@ +/* + * 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.dm.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates a method or a field for injecting a Service Dependency. When applied on a class + * field, optional unavailable dependencies are injected with a NullObject. + * + *

      For "add", "change", "remove" callbacks, the following method signatures are supported: + * + *

      {@code
      + * (Component comp, ServiceReference ref, Service service)
      + * (Component comp, ServiceReference ref, Object service)
      + * (Component comp, ServiceReference ref)
      + * (Component comp, Service service)
      + * (Component comp, Object service)
      + * (Component comp)
      + * (Component comp, Map properties, Service service)
      + * (ServiceReference ref, Service service)
      + * (ServiceReference ref, Object service)
      + * (ServiceReference ref)
      + * (Service service)
      + * (Service service, Map properties)
      + * (Map properties, Service, service)
      + * (Service service, Dictionary properties)
      + * (Dictionary properties, Service service)
      + * (Object service)
      + * (ServiceReference service)
      + * (ServiceObjects service)
      + * }
      + * + *

      For "swap" callbacks, the following method signatures are supported: + * + *

      {@code
      + * (Service old, Service replace)
      + * (Object old, Object replace)
      + * (ServiceReference old, Service old, ServiceReference replace, Service replace)
      + * (ServiceReference old, Object old, ServiceReference replace, Object replace)
      + * (Component comp, Service old, Service replace)
      + * (Component comp, Object old, Object replace)
      + * (Component comp, ServiceReference old, Service old, ServiceReference replace, Service replace)
      + * (Component comp, ServiceReference old, Object old, ServiceReference replace, Object replace)
      + * (ServiceReference old, ServiceReference replace)
      + * (Component comp, ServiceReference old, ServiceReference replace)
      + * (ServiceObjects old, ServiceObjects replace)
      + * (Component comp, ServiceObjects old, ServiceObjects replace)
      + * }
      + * + *

      When the dependency is injected on a class field, the following field types are supported: + * + *

        + *
      • a field having the same type as the dependency. If the field may be accessed by any thread, then the field should be declared volatile, in order to ensure visibility + * when the field is auto injected concurrently. + *
      • a field which is assignable to an {@literal Iterable} where T must match the dependency type. In this case, an Iterable will be injected by DependencyManager before the start + * callback is called. The Iterable field may then be traversed to inspect the currently available dependency services. The Iterable can possibly be set to a final value + * so you can choose the Iterable implementation of your choice (for example, a CopyOnWrite ArrayList, or a ConcurrentLinkedQueue). + *
      • a {@literal Map} where K must match the dependency type and V must exactly equals Dictionary class. In this case, a ConcurrentHashMap will be injected by DependencyManager + * before the start callback is called. The Map may then be consulted to lookup current available dependency services, including the dependency service properties + * (the map key holds the dependency services, and the map value holds the dependency service properties). The Map field may be set to a final value so you can choose a Map of your choice (Typically a ConcurrentHashMap). A ConcurrentHashMap is "weakly consistent", meaning that when traversing the elements, you may or may not see any concurrent updates made on the map. So, take care to traverse the map using an iterator on the map entry set, which allows to atomically lookup pairs of Dependency service/Service properties. + *
      + * + *

      Usage Examples

      + * Here, the MyComponent component is injected with a dependency over a "MyDependency" service + * + *
      + * @Component
      + * class MyComponent {
      + *     @ServiceDependency
      + *     volatile MyDependency dependency;
      + * }
      + * 
      + * + * The dependency can also be injected using a method callback: + * + *
      + * @Component
      + * class MyComponent {
      + *     @ServiceDependency
      + *     void bind(MyDependency dependency) {}
      + * }
      + * 
      + * + * Same example as before, but the callback signatures includes service properties: + * + *
      + * @Component
      + * class MyComponent {
      + *     @ServiceDependency
      + *     void bind(MyDependency dependency, Map<String, Object> serviceProperties) {}
      + * }
      + * 
      + * + * Another example, were we inject multiple dependencies to an Iterable field + * + *
      + * @Component
      + * class MyComponent {
      + *     @ServiceDependency
      + *     final Iterable<Dependency> dependencies = new CopyOnWriteArrayList<>();
      + * }
      + * 
      + * + * Another example, were we inject multiple dependencies to a Map field, allowing to inspect service dependency properties + * (the keys hold the services, and the values hold the associated service properties): + * + *
      + * @Component
      + * class MyComponent {
      + *     @ServiceDependency
      + *     final Map<MyDependency, Dictionary<String, Object>> dependencies = new ConcurrentHashMap<>;
      + * }
      + * 
      + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.METHOD, ElementType.FIELD}) +public @interface ServiceDependency +{ + /** + * Marker interface used to match any service types. When you set the {@link ServiceDependency#service} attribute to this class, + * it means that the dependency will return any services (matching the {@link ServiceDependency#filter()} attribute if it is specified). + */ + public interface Any { } + + /** + * The type if the service this dependency is applying on. By default, the method parameter + * (or the class field) is used as the type. If you want to match all available services, you can set + * this attribute to the {@link Any} class. In this case, all services (matching the {@link ServiceDependency#filter()} attribute if it is specified) will + * be returned. + * @return the service dependency + */ + Class service() default Object.class; + + /** + * The Service dependency OSGi filter. + * @return the service filter + */ + String filter() default ""; + + /** + * Sets the default implementation for an optional service dependency. You can use this to supply + * your own implementation that will be used instead of a Null Object when the dependency is + * not available. This is also convenient if the service dependency is not an interface + * (which would cause the Null Object creation to fail) but a class. + * Only use this attribute on an optional service dependency applied on a class field. + * + * @return the default implementation class + */ + Class defaultImpl() default Object.class; + + /** + * Whether the Service dependency is required or not. When the dependency is optional and if the annotation is declared on a class field, then + * a Null Object will be injected in case the dependency is unavailable. + * @return the required flag + */ + boolean required() default true; + + /** + * The callback method to be invoked when the service is available. This attribute is only meaningful when + * the annotation is applied on a class field. + * + * @return the add callback + */ + String added() default ""; + + /** + * The callback method to be invoked when the service properties have changed. + * @return the change callback + */ + String changed() default ""; + + /** + * The callback method to invoke when the service is lost. + * @return the remove callback + */ + String removed() default ""; + + /** + * the method to call when the service was swapped due to addition or removal of an aspect + * @return the swap callback + */ + String swap() default ""; + + /** + * The max time in millis to wait for the dependency availability. + * Specifying a positive number allow to block the caller thread between service updates. Only + * useful for required stateless dependencies that can be replaced transparently. + * A Dynamic Proxy is used to wrap the actual service dependency (which must be an interface). + * When the dependency goes away, an attempt is made to replace it with another one which satisfies + * the service dependency criteria. If no service replacement is available, then any method invocation + * (through the dynamic proxy) will block during a configurable timeout. On timeout, an unchecked + * IllegalStateException exception is raised (but the service is not deactivated).

      + * Notice that the changed/removed callbacks are not used when the timeout parameter is greater than -1. + * + * -1 means no timeout at all (default). 0 means that invocation on a missing service will fail + * immediately. A positive number represents the max timeout in millis to wait for the service availability. + * + * Sample Code: + *

      +     * @Component
      +     * class MyServer implements Runnable {
      +     *   @ServiceDependency(timeout=15000)
      +     *   MyDependency dependency;.
      +     *   
      +     *   @Start
      +     *   void start() {
      +     *     (new Thread(this)).start();
      +     *   }
      +     *   
      +     *   public void run() {
      +     *     try {
      +     *       dependency.doWork();
      +     *     } catch (IllegalStateException e) {
      +     *       t.printStackTrace();
      +     *     }
      +     *   }   
      +     * 
      + * @return the wait time when the dependency is unavailable + */ + long timeout() default -1; + + /** + * The name used when dynamically configuring this dependency from the init method. + * Specifying this attribute allows to dynamically configure the dependency + * filter and required flag from the Service's init method. + * All unnamed dependencies will be injected before the init() method; so from the init() method, you can + * then pick up whatever information needed from already injected (unnamed) dependencies, and configure dynamically + * your named dependencies, which will then be calculated once the init() method returns. + * + *

      See {@link Init} annotation for an example usage of a dependency dynamically configured from the init method. + * @return the dependency name used to dynamically configure the filter and required flag from the init callback. + */ + String name() default ""; + + /** + * Returns true if the dependency service properties must be published along with the service. + * Any additional service properties specified directly are merged with these. + * The component provided service properties take precedence over the propagated service dependency properties, meaning + * that a give component service property overrides the same property included in the propagated service dependency properties. + * @return true if dependency service properties must be published along with the service, false if not. + */ + boolean propagate() default false; +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/ServiceScope.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/ServiceScope.java new file mode 100644 index 00000000000..9b3c2d7f6d6 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/ServiceScope.java @@ -0,0 +1,47 @@ +/* + * 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.dm.annotation.api; + +import org.osgi.service.component.annotations.Component; + +/** + * Service scope for the {@link Component}/{@link AdapterService}/{@link AspectService}/{@link BundleAdapterService} annotations. + */ +public enum ServiceScope { + /** + * When the component is registered as a service, it must be registered as a + * bundle scope service but only a single instance of the component must be + * used for all bundles using the service. + */ + SINGLETON, + + /** + * When the component is registered as a service, it must be registered as a + * bundle scope service and an instance of the component must be created for + * each bundle using the service. + */ + BUNDLE, + + /** + * When the component is registered as a service, it must be registered as a + * prototype scope service and an instance of the component must be created + * for each distinct request for the service. + */ + PROTOTYPE +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Start.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Start.java new file mode 100644 index 00000000000..ec9e31a3e15 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Start.java @@ -0,0 +1,62 @@ +/* + * 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.dm.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates a method which will be invoked when the component is started. + * The annotated method is invoked juste before registering the service into the OSGi registry + * (if the service provides an interface). Notice that the start method may optionally return + * a Map which will be propagated to the provided service properties.

      + * Service activation/deactivation can be programatically controlled using {@link LifecycleController}. + * + *

      Usage Examples

      + *
      + * + *
      + * @Component(properties={@Property(name="foo", value="bar")})
      + * class X implements Z {
      + *     @ServiceDependency
      + *     OtherService m_dependency;
      + *   
      + *     @Start
      + *     Map start() {
      + *         // Our Z Service is ready (all required dependencies have been satisfied), and is about to be 
      + *         // registered into the OSGi registry. We return here an optional Map containing some extra-properties
      + *         // which will be appended to the properties supplied in the Component annotation.
      + *         return new HashMap() {{
      + *            put("foo2", "bar2");
      + *            put(Constants.SERVICE_RANKING, Integer.valueOf(10));
      + *         }};
      + *     }
      + * }
      + * 
      + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.METHOD) +public @interface Start +{ +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Stop.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Stop.java new file mode 100644 index 00000000000..f963db06bb9 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Stop.java @@ -0,0 +1,53 @@ +/* + * 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.dm.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates a method which is invoked when the Service is being unregistered from the + * OSGi registry. + * The method is called when the component's bundle is stopped, or when one of its + * required dependency is lost, or when a {@link LifecycleController} is programatically used to + * stop a service. + * + *

      Usage Examples

      + *
      + *
      + * @Component
      + * class MyComponent implements MyService {
      + *     @ServiceDependency
      + *     private LogService logService; // Required dependency over the log service.
      + *     
      + *     @Stop
      + *     void stop() {} // We are unregistering from the OSGi registry.     
      + * }
      + * 
      + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.METHOD) +public @interface Stop +{ +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Unregistered.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Unregistered.java new file mode 100644 index 00000000000..26ac430654c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/Unregistered.java @@ -0,0 +1,56 @@ +/* + * 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.dm.annotation.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation can be used to be notified when a component is unregistered from the registry. + * At this point, the component has been unregistered from the OSGI registry (if it provides some services). + * The method must not take any parameters. + * + *

      Usage Examples

      + *
      + * + *
      + * @Component
      + * class X implements Z {     
      + *     @Stop
      + *     void stop(ServiceRegistration sr) {
      + *        // Our service must stop because it is about to be unregistered from the registry.
      + *     }
      + *     
      + *     @Unregistered
      + *     void unregistered() {
      + *        // At this point, our service has been unregistered from the OSGi registry
      + *     }
      + * }
      + * 
      + *
      + * + * @author Felix Project Team + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.METHOD) +public @interface Unregistered +{ +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/packageinfo b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/packageinfo new file mode 100644 index 00000000000..084a0d4e363 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/api/packageinfo @@ -0,0 +1 @@ +version 2.0.0 diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/AnnotationCollector.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/AnnotationCollector.java new file mode 100644 index 00000000000..78f7130e71a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/AnnotationCollector.java @@ -0,0 +1,1891 @@ +/* + * 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.dm.annotation.plugin.bnd; + +import static aQute.bnd.osgi.Clazz.QUERY.ANNOTATED; + +import java.io.PrintWriter; +import java.lang.reflect.Array; +import java.util.AbstractMap; +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.felix.dm.annotation.api.AdapterService; +import org.apache.felix.dm.annotation.api.AspectService; +import org.apache.felix.dm.annotation.api.BundleAdapterService; +import org.apache.felix.dm.annotation.api.BundleDependency; +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Composition; +import org.apache.felix.dm.annotation.api.ConfigurationDependency; +import org.apache.felix.dm.annotation.api.Destroy; +import org.apache.felix.dm.annotation.api.Init; +import org.apache.felix.dm.annotation.api.Inject; +import org.apache.felix.dm.annotation.api.LifecycleController; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.PropertyType; +import org.apache.felix.dm.annotation.api.Registered; +import org.apache.felix.dm.annotation.api.RepeatableProperty; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.ServiceScope; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.annotation.api.Unregistered; +import org.osgi.framework.Bundle; + +import aQute.bnd.osgi.Analyzer; +import aQute.bnd.osgi.Annotation; +import aQute.bnd.osgi.ClassDataCollector; +import aQute.bnd.osgi.Clazz; +import aQute.bnd.osgi.Clazz.FieldDef; +import aQute.bnd.osgi.Clazz.MethodDef; +import aQute.bnd.osgi.Descriptors; +import aQute.bnd.osgi.Descriptors.TypeRef; +import aQute.bnd.osgi.Instruction; +import aQute.bnd.osgi.Verifier; +import aQute.lib.collections.MultiMap; + +/** + * This is the scanner which does all the annotation parsing on a given class. + * To start the parsing, just invoke the parseClassFileWithCollector and finish methods. + * Once parsed, the corresponding component descriptors can be built using the "writeTo" method. + * + * @author Felix Project Team + */ +public class AnnotationCollector extends ClassDataCollector +{ + private final static String A_INIT = Init.class.getName(); + private final static String A_START = Start.class.getName(); + private final static String A_STOP = Stop.class.getName(); + private final static String A_DESTROY = Destroy.class.getName(); + private final static String A_COMPOSITION = Composition.class.getName(); + private final static String A_LIFCLE_CTRL = LifecycleController.class.getName(); + + private final static String A_COMPONENT = Component.class.getName(); + private final static String A_PROPERTY = Property.class.getName(); + private final static String A_REPEATABLE_PROPERTY = RepeatableProperty.class.getName(); + private final static String A_SERVICE_DEP = ServiceDependency.class.getName(); + private final static String A_CONFIGURATION_DEPENDENCY = ConfigurationDependency.class.getName(); + private final static String A_BUNDLE_DEPENDENCY = BundleDependency.class.getName(); + private final static String A_ASPECT_SERVICE = AspectService.class.getName(); + private final static String A_ADAPTER_SERVICE = AdapterService.class.getName(); + private final static String A_BUNDLE_ADAPTER_SERVICE = BundleAdapterService.class.getName(); + private final static String A_INJECT = Inject.class.getName(); + private final static String A_REGISTERED = Registered.class.getName(); + private final static String A_UNREGISTERED = Unregistered.class.getName(); + + private final Logger m_logger; + private String[] m_interfaces; + private boolean m_isField; + private String m_field; + private FieldDef m_fieldDef; + private String m_method; + private MethodDef m_methodDef; + private String m_descriptor; + private final Set m_dependencyNames = new HashSet(); + private final List m_writers = new ArrayList(); + private String m_startMethod; + private String m_stopMethod; + private String m_initMethod; + private String m_destroyMethod; + private String m_compositionMethod; + private String m_starter; + private String m_stopper; + private final Set m_importService = new HashSet(); + private final Set m_exportService = new HashSet(); + private String m_bundleContextField; + private String m_dependencyManagerField; + private String m_registrationField; + private String m_bundleField; + private String m_componentField; + private String m_registeredMethod; + private String m_unregisteredMethod; + private TypeRef m_superClass; + private boolean m_baseClass = true; + private final static String[] EMPTY_STRING_ARRAY = new String[0]; + + /** + * Name of the class annotated with @Component (or other kind of components, like aspect, adapters). + */ + private String m_componentClassName; + + /* + * Name of class currently being parsed (the component class name at first, then the inherited classes). + * See DescriptorGenerator class, which first calls parseClassFileWithCollector method with the component class, then it calls + * again the parseClassFileWithCollector method with all inherited component super classes. + */ + private String m_currentClassName; + + /** + * Contains all bind methods annotated with a dependency. + * Each entry has the format: "methodName/method signature". + */ + private final Set m_bindMethods = new HashSet<>(); + + /** + * When more than one @Property annotation are declared on a component type (outside of the @Component annotation), then a @Repeatable + * annotation is used as the container for the @Property annotations. When such annotation is found, it is stored in this attribute, which + * will be parsed in our finish() method. + */ + private Annotation m_repeatableProperty; + + /** + * If a Single @Property is declared on the component type (outside of the @Component annotation), then there is no @Repeatable annotation. + * When such single @Property annotation is found, it is stored in this attribute, which will be parsed in our finish() method. + */ + private Annotation m_singleProperty; + + /** + * List of all possible DM components. + */ + private final List m_componentTypes = Arrays.asList(EntryType.Component, EntryType.AspectService, EntryType.AdapterService, + EntryType.BundleAdapterService, EntryType.ResourceAdapterService, EntryType.FactoryConfigurationAdapterService); + + + /** + * Class Analyzer used to scan component property type annotation. + */ + private final Analyzer m_analyzer; + + /** + * List of component property type annotation applied on component class. + */ + private final List m_componentPropertyTypes = new ArrayList<>(); + + /** + * List of all parsed methods. Key = method name, value is list of signatures. + * Example: if a component has two methods update(Dictionary) and updated(MyConfig), then the map will contain the following: + * "updated" -> ["(Ljava/util/Dictionary;)V", "(Lfoo.bar.MyConfig;)V"] + */ + final Map> m_methods = new HashMap<>(); + + /** + * Object used to see if an annotation is matching a declarative service ComponentPropertyType annotation. + */ + private static final Instruction DS_PROPERTY_TYPE = new Instruction("org.osgi.service.component.annotations.ComponentPropertyType"); + + /** + * Object used to see if an annotation is matching a DM PropertyType annotation. + */ + private static final Instruction DM_PROPERTY_TYPE = new Instruction(PropertyType.class.getName()); + + /** + * Mapping between type strings -> type class + */ + private final static Map>m_types = Collections.unmodifiableMap(Stream.of( + new SimpleEntry<>("boolean", Boolean.class), + new SimpleEntry<>("byte", Byte.class), + new SimpleEntry<>("short", Short.class), + new SimpleEntry<>("char", Character.class), + new SimpleEntry<>("int", Integer.class), + new SimpleEntry<>("long", Long.class), + new SimpleEntry<>("float", Float.class), + new SimpleEntry<>("double", Double.class)) + .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue))); + + /** + * Marker used when parsing component property types annotations. + * (To make sure the output is an array, we must make sure there is more than one entry) + */ + private final static String MARKER = new String("|marker"); + + /** + * Pattern used when parsing component property type annotations. + */ + private static final Pattern IDENTIFIERTOPROPERTY = Pattern.compile("(__)|(_)|(\\$_\\$)|(\\$\\$)|(\\$)"); + + /** + * Makes a new Collector for parsing a given class. + * @param reporter the object used to report logs. + * @param m_analyzer + */ + public AnnotationCollector(Logger reporter, Analyzer analyzer) + { + m_logger = reporter; + m_analyzer = analyzer; + } + + /** + * Indicates that we are parsing a superclass of a given component class. + */ + public void baseClass(boolean baseClass) { + m_logger.debug("baseClass:%b", baseClass); + m_baseClass = baseClass; + } + + /** + * Parses the name of the class. + * @param access the class access + * @param name the class name (package are "/" separated). + */ + @Override + public void classBegin(int access, TypeRef name) + { + if (m_baseClass) + { + m_componentClassName = name.getFQN(); + m_logger.debug("Parsing class: %s", m_componentClassName); + } + m_currentClassName = name.getFQN(); + } + + /** + * Parses the implemented interfaces ("/" separated). + */ + @Override + public void implementsInterfaces(TypeRef[] interfaces) + { + if (m_baseClass) + { + List result = new ArrayList(); + for (int i = 0; i < interfaces.length; i++) + { + if (!interfaces[i].getBinary().equals("scala/ScalaObject")) + { + result.add(interfaces[i].getFQN()); + } + } + + m_interfaces = result.toArray(new String[result.size()]); + m_logger.debug("implements: %s", Arrays.toString(m_interfaces)); + } + } + + /** + * Parses a method. Always invoked BEFORE eventual method annotation. + */ + @Override + public void method(Clazz.MethodDef method) + { + m_logger.debug("Parsed method %s descriptor=%s signature=%s", method.getName(), method.getDescriptor(), method.getSignature()); + m_isField = false; + m_method = method.getName(); + m_methodDef = method; + m_descriptor = method.getDescriptor().toString(); + m_methods.computeIfAbsent(method.getName(), k -> new ArrayList()).add(method.getDescriptor()); + } + + /** + * Parses a field. Always invoked BEFORE eventual field annotation + */ + @Override + public void field(Clazz.FieldDef field) + { + m_logger.debug("Parsed field %s descriptor=%s", field.getName(), field.getDescriptor()); + m_isField = true; + m_field = field.getName(); + m_fieldDef = field; + m_descriptor = field.getDescriptor().toString(); + } + + @Override + public void extendsClass(TypeRef name) { + m_superClass = name; + } + + public TypeRef getSuperClass() { + return m_superClass; + } + + /** + * An annotation has been parsed. Always invoked AFTER the "method"/"field"/"classBegin" callbacks. + */ + @Override + public void annotation(Annotation annotation) + { + m_logger.debug("Parsing annotation: %s", annotation.getName()); + + // if we are parsing a superclass of a given component, then ignore any component annotations. + String name = annotation.getName().getFQN(); + if (! m_baseClass) { + String simpleName = name.indexOf(".") != -1 ? name.substring(name.lastIndexOf(".")+1) : name; + Optional type = m_componentTypes.stream().filter(writer -> writer.name().equals(simpleName)).findFirst(); + if (type.isPresent()) { + m_logger.debug("Ignoring annotation %s from super class %s of component class %s", name, m_currentClassName, m_componentClassName); + return; + } + } + + if (name.equals(A_COMPONENT)) + { + parseComponentAnnotation(annotation); + } + else if (name.equals(A_ASPECT_SERVICE)) + { + parseAspectService(annotation); + } + else if (name.equals(A_ADAPTER_SERVICE)) + { + parseAdapterService(annotation); + } + else if (name.equals(A_BUNDLE_ADAPTER_SERVICE)) + { + parseBundleAdapterService(annotation); + } + else if (name.equals(A_INIT)) + { + checkAlreadyDeclaredSingleAnnot(() -> m_initMethod, "@Init"); + m_initMethod = m_method; + } + else if (name.equals(A_START)) + { + checkAlreadyDeclaredSingleAnnot(() -> m_startMethod, "@Start"); + m_startMethod = m_method; + } + else if (name.equals(A_REGISTERED)) + { + checkAlreadyDeclaredSingleAnnot(() -> m_registeredMethod, "@Registered"); + m_registeredMethod = m_method; + } + else if (name.equals(A_STOP)) + { + checkAlreadyDeclaredSingleAnnot(() -> m_stopMethod, "@Stop"); + m_stopMethod = m_method; + } + else if (name.equals(A_UNREGISTERED)) + { + checkAlreadyDeclaredSingleAnnot(() -> m_unregisteredMethod, "@Unregistered"); + m_unregisteredMethod = m_method; + } + else if (name.equals(A_DESTROY)) + { + checkAlreadyDeclaredSingleAnnot(() -> m_destroyMethod, "@Destroy"); + m_destroyMethod = m_method; + } + else if (name.equals(A_COMPOSITION)) + { + checkAlreadyDeclaredSingleAnnot(() -> m_compositionMethod, "@Composition"); + Patterns.parseMethod(m_method, m_descriptor, Patterns.COMPOSITION); + m_compositionMethod = m_method; + } + else if (name.equals(A_LIFCLE_CTRL)) + { + parseLifecycleAnnotation(annotation); + } + else if (name.equals(A_SERVICE_DEP)) + { + parseServiceDependencyAnnotation(annotation); + } + else if (name.equals(A_CONFIGURATION_DEPENDENCY)) + { + parseConfigurationDependencyAnnotation(annotation); + } + else if (name.equals(A_BUNDLE_DEPENDENCY)) + { + parseBundleDependencyAnnotation(annotation); + } + else if (name.equals(A_INJECT)) + { + parseInject(annotation); + } + else if (name.equals(A_REPEATABLE_PROPERTY)) + { + parseRepeatableProperties(annotation); + } + else if (annotation.getName().getFQN().equals(A_PROPERTY)) + { + m_singleProperty = annotation; + } + else { + handlePossibleComponentPropertyAnnotation(annotation); + } + } + + /** + * Finishes up the class parsing. This method must be called once the parseClassFileWithCollector method has returned. + * @return true if some annotations have been parsed, false if not. + */ + public boolean finish() + { + m_logger.info("finish %s", m_componentClassName); + + // check if we have a component (or adapter) annotation. + Optional componentWriter = m_writers.stream() + .filter(writer -> m_componentTypes.indexOf(writer.getEntryType()) != -1) + .findFirst(); + + if (! componentWriter.isPresent() || m_writers.size() == 0) { + m_logger.info("No components found for class %s", m_componentClassName); + return false; + } + + finishComponentAnnotation(componentWriter.get()); + + // log all meta data for component annotations, dependencies, etc ... + StringBuilder sb = new StringBuilder(); + sb.append("Parsed annotation for class "); + sb.append(m_componentClassName); + for (int i = m_writers.size() - 1; i >= 0; i--) + { + sb.append("\n\t").append(m_writers.get(i).toString()); + } + m_logger.info(sb.toString()); + return true; + } + + private void finishComponentAnnotation(EntryWriter componentWriter) { + // Register previously parsed Init/Start/Stop/Destroy/Composition annotations + addCommonServiceParams(componentWriter); + + // Add any repeated @Property annotations to the component (or to the aspect, or adapter). + if (m_repeatableProperty != null) + { + Object[] properties = m_repeatableProperty.get("value"); + for (Object property : properties) + { + // property is actually a @Property annotation. + parseProperty((Annotation) property, componentWriter); + } + } + + // handle component property types + parseComponentPropertyAnnotation(componentWriter); + + // Handle a single Property declared on the component type (in this case, there is no @Repeatable annotation). + if (m_singleProperty != null) { + parseProperty(m_singleProperty, componentWriter); + } + + if (componentWriter.getEntryType() == EntryType.FactoryConfigurationAdapterService) { + m_logger.debug("finishFactoryComponent ..."); + finishFactoryComponent(componentWriter); + } + } + + /** + * Finish to parse a Factory Pid Component annotation. + * @param componentWriter the datastructure which holds the parsed annotation attributes. + */ + private void finishFactoryComponent(EntryWriter componentWriter) { + + if (componentWriter.getParameter(EntryParam.configType) == null) { + // No configtype defined in the annotation, then try to detect if the method signature contains some config types. + // First, see if there is an updated callback without a config type: + String method = componentWriter.getParameter(EntryParam.updated).toString(); // can't be null + List methodSigs = m_methods.get(method); + if (methodSigs == null) { + throw new IllegalStateException("can't find callback " + method + " on class" + m_componentClassName); + } + boolean parseConfigTypesFromArgs = true; + for (Descriptors.Descriptor desc : methodSigs) { + Matcher m = Patterns.UPDATED_NO_CONFIG_TYPES.matcher(desc.toString()); + if (m.matches()) { + // Found an updated callback which does not contain any config type + m_logger.debug("updated callback %s does not contain any config type in accepted arguments", method); + parseConfigTypesFromArgs = false; + break; + } + } + // Now, try to find an updated callback having some config types accepted as arguments + if (parseConfigTypesFromArgs) { + for (Descriptors.Descriptor desc : methodSigs) { + Matcher m = Patterns.UPDATED_CONFIG_TYPES.matcher(desc.toString()); + String[] configTypes = parsePossibleConfigTypesFromUpdatedCallback(componentWriter, + desc.toString()); + if (configTypes.length > 0) { + // If factory pid not specified, derive the factory pid from the config type + // (and if only one config type is specified) + if (componentWriter.getParameter(EntryParam.factoryPid) == null) { + if (configTypes.length == 1) { + componentWriter.put(EntryParam.factoryPid, configTypes[0]); + } + } + break; + } + } + } + } + + // set factory pid to component class name if it has not been derived from any annotation attributes or from updated callback parameters. + if (componentWriter.getParameter(EntryParam.factoryPid) == null) { + componentWriter.put(EntryParam.factoryPid, m_componentClassName); + } + } + + /** + * Auto Detect if an updated callback method contains some configuration types. + * @param writer the writer where the detected configration types are written in case some are detected + * @param callbackDescriptor the descriptor of the udpated callback method. + * @param propagateConfigTypeDefVal true if any found config type default values should be parsed and propagated to service properties + * @return an empty string array in case no config types has been detected, or the array of parsed config types. + */ + private String[] parsePossibleConfigTypesFromUpdatedCallback(EntryWriter writer, String callbackDescriptor) { + Matcher m = Patterns.UPDATED_CONFIG_TYPES.matcher(callbackDescriptor); + List types = new ArrayList<>(); + while (m.find()) { + String type = m.group(5); + if (type != null) { + m_logger.debug("checking if type %s is an interface or an annotation", type); + TypeRef typeRef = m_analyzer.getTypeRef(type); + try { + Clazz clazz = m_analyzer.findClass(typeRef); + if (! clazz.isAnnotation() && ! clazz.isInterface()) { + m_logger.debug("ignoring updated callback signature %s (argument type not an interface or an annotation", callbackDescriptor); + continue; + } + types.add(type.replace("/", ".")); + + /* + if (propagateConfigTypeDefVal) { + // set component service properties with config type default values + if (clazz.isAnnotation()) { + clazz.parseClassFileWithCollector (new ComponentPropertyTypeDataCollector(writer)); + } + } + */ + } catch (Exception e) { + throw new IllegalStateException("could not find config type class " + type); + } + } + } + if (types.size() > 0) { + m_logger.debug("detected config types for updated callback %s", types); + if (types.size() == 1) { + writer.put(EntryParam.configType, types.get(0)); + } else { + writer.put(EntryParam.configTypes, types); + } + return types.stream().toArray(String[]::new); + } + return EMPTY_STRING_ARRAY; + } + + /** + * Writes the generated component descriptor in the given print writer. + * The first line must be the component descriptor (@Component or AspectService, etc ..). + * @param pw the writer where the component descriptor will be written. + */ + public void writeTo(PrintWriter pw) + { + // write first the component descriptor (@Component, @AspectService, ...) + EntryWriter componentWriter = m_writers.stream() + .filter(writer -> m_componentTypes.indexOf(writer.getEntryType()) != -1) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Component type not found while scanning class " + m_componentClassName)); + pw.println(componentWriter); + + // and write other component descriptors (dependencies, and other annotations) + m_writers.stream() + .filter(writer -> m_componentTypes.indexOf(writer.getEntryType()) == -1) + .forEach(dependency -> pw.println(dependency.toString())); + } + + /** + * Returns list of all imported services. Imported services are deduced from every + * @ServiceDependency annotations. + * @return the list of imported services, or null + */ + public Set getImportService() + { + return m_importService; + } + + /** + * Returns list of all exported services. Imported services are deduced from every + * annotations which provides a service (@Component, etc ...) + * @return the list of exported services, or null + */ + public Set getExportService() + { + return m_exportService; + } + + /** + * Parses a Property annotation that is applied on the component class. + * @param property the Property annotation. + */ + private void parseRepeatableProperties(Annotation repeatedProperties) + { + m_repeatableProperty = repeatedProperties; + } + + /** + * Checks double declaration of an annotation, which normally must be declared only one time. + * @param field the field supplier (if not null, means the annotation is already declared) + * @param annot the annotation name. + * @throws IllegalStateException if annotation is already declared. + */ + private void checkAlreadyDeclaredSingleAnnot(Supplier field, String annot) { + if (field.get() != null) { + throw new IllegalStateException("detected multiple " + annot + " annotation from class " + m_currentClassName + " (on from child classes)"); + } + } + + private void parseComponentAnnotation(Annotation annotation) + { + String factoryPid = annotation.get(EntryParam.factoryPid.toString()); + if (factoryPid != null) { + parseFactoryComponent(annotation); + return; + } + + EntryWriter writer = new EntryWriter(EntryType.Component); + m_writers.add(writer); + + // impl attribute + writer.put(EntryParam.impl, m_componentClassName); + + // Parse Adapter properties, and other params common to all kind of component + parseCommonComponentAttributes(annotation, writer); + + // provides attribute. + if (writer.putClassArray(annotation, EntryParam.provides, m_interfaces, m_exportService) == 0) + { + // no service provided: check if @Registered/@Unregistered annotation are used + // and raise an error if true. + checkRegisteredUnregisteredNotPresent(); + } + + // factoryMethod attribute + writer.putString(annotation, EntryParam.factoryMethod, null); + } + + private void addCommonServiceParams(EntryWriter writer) + { + if (m_initMethod != null) + { + writer.put(EntryParam.init, m_initMethod); + } + + if (m_startMethod != null) + { + writer.put(EntryParam.start, m_startMethod); + } + + if (m_registeredMethod != null) + { + writer.put(EntryParam.registered, m_registeredMethod); + } + + if (m_stopMethod != null) + { + writer.put(EntryParam.stop, m_stopMethod); + } + + if (m_unregisteredMethod != null) + { + writer.put(EntryParam.unregistered, m_unregisteredMethod); + } + + if (m_destroyMethod != null) + { + writer.put(EntryParam.destroy, m_destroyMethod); + } + + if (m_compositionMethod != null) + { + writer.put(EntryParam.composition, m_compositionMethod); + } + + if (m_starter != null) + { + writer.put(EntryParam.starter, m_starter); + } + + if (m_stopper != null) + { + writer.put(EntryParam.stopper, m_stopper); + if (m_starter == null) + { + throw new IllegalArgumentException("Can't use a @LifecycleController annotation for stopping a service without declaring a " + + "@LifecycleController that starts the component in class " + m_currentClassName); + } + } + + if (m_bundleContextField != null) + { + writer.put(EntryParam.bundleContextField, m_bundleContextField); + } + + if (m_dependencyManagerField != null) + { + writer.put(EntryParam.dependencyManagerField, m_dependencyManagerField); + } + + if (m_componentField != null) + { + writer.put(EntryParam.componentField, m_componentField); + } + + if (m_registrationField != null) + { + writer.put(EntryParam.registrationField, m_registrationField); + } + + if (m_bundleField != null) + { + Object scope = writer.getParameter(EntryParam.scope); + if (scope == null || scope == ServiceScope.SINGLETON.name()) { + throw new IllegalStateException("can't inject a bundle field on a component without prototype or bundle scope"); + } + writer.put(EntryParam.bundle, m_bundleField); + } + } + + /** + * Check if a dependency is already declared in another same bindMethod (or class field) on another child class. + */ + private void checkDependencyAlreadyDeclaredInChild(Annotation annotation, String methodOrField, boolean method) { + if (! m_baseClass && m_bindMethods.contains(methodOrField + "/" + m_descriptor)) { + throw new IllegalStateException("Annotation " + annotation.getName().getShortName() + + " declared on " + m_currentClassName + "." + methodOrField + (method ? " method" : " field") + " is already declared in child classe(s)"); + } + m_bindMethods.add(methodOrField + "/" + m_descriptor); + } + + /** + * Parses a ServiceDependency Annotation. + * @param annotation the ServiceDependency Annotation. + */ + private void parseServiceDependencyAnnotation(Annotation annotation) + { + EntryWriter writer = new EntryWriter(EntryType.ServiceDependency); + boolean doDereference = true; // false means dependency manager must not internally dereference the service dependency + + m_writers.add(writer); + + // service attribute + String service = parseClassAttrValue(annotation.get(EntryParam.service.toString())); + if (service == null) + { + if (m_isField) + { + checkDependencyAlreadyDeclaredInChild(annotation, m_field, false); + //service = Patterns.parseClass(m_descriptor, Patterns.CLASS, 1); + m_logger.debug("parsing field service type %s", m_field); + service = FieldTypeGetter.determineFieldType(m_logger, m_fieldDef); + m_logger.debug("field service type=%s", service); + } + else + { + // if we are parsing some inherited classes, detect if the bind method is already declared in child classes + checkDependencyAlreadyDeclaredInChild(annotation, m_method, true); + + // parse "bind(Component, ServiceReference, Service)" signature + service = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS1, 3, false); + + if (service == null) { + // parse "bind(Component, Service)" signature + service = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS2, 2, false); + } + + if (service == null) { + // parse "bind(ServiceReference)" or "bind(ServiceObjects)" signatures + service = Patterns.inferTypeFromGenericType(m_methodDef.getDescriptor().toString(), + m_methodDef.getSignature(), + m_logger); + // dm must not internally dereference the service, since it is injected as a ServiceRef or a ServiceObjects + doDereference = false; + } + + if (service == null) { + // parse "bind(Component, Map, Service)" signature + service = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS3, 3, false); + } + + if (service == null) { + // parse "bind(ServiceReference, Service)" signature + service = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS4, 2, false); + } + + if (service == null) { + // parse "bind(Service)" signature + service = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS5, 1, false); + } + + if (service == null) { + // parse "bind(Service, Map)" signature + service = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS6, 1, false); + } + + if (service == null) { + // parse "bind(Map, Service)" signature + service = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS7, 2, false); + } + + if (service == null) { + // parse "bind(Service, Dictionary)" signature + service = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS8, 1, false); + } + + if (service == null) { + // parse "bind(Dictionary, Service)" signature + service = Patterns.parseClass(m_descriptor, Patterns.BIND_CLASS9, 2, true); + } + } + } + + boolean matchAnyService = service != null && service.equals(ServiceDependency.Any.class.getName()); + if (matchAnyService) { + service = null; + } + writer.put(EntryParam.service, service); + + // Store this service in list of imported services. + m_importService.add(service); + + // autoConfig attribute + if (m_isField) + { + writer.put(EntryParam.autoConfig, m_field); + } + + // filter attribute + String filter = annotation.get(EntryParam.filter.toString()); + if (filter != null) + { + Verifier.verifyFilter(filter, 0); + if (matchAnyService) { + filter = "(&(objectClass=*)" + filter + ")"; + } + writer.put(EntryParam.filter, filter); + } else if (matchAnyService) { + filter = "(objectClass=*)"; + writer.put(EntryParam.filter, filter); + } + + // defaultImpl attribute + writer.putClass(annotation, EntryParam.defaultImpl); + + // added callback + writer.putString(annotation, EntryParam.added, (!m_isField) ? m_method : null); + + // timeout parameter + writer.putString(annotation, EntryParam.timeout, null); + Long t = (Long) annotation.get(EntryParam.timeout.toString()); + if (t != null && t.longValue() < -1) + { + throw new IllegalArgumentException("Invalid timeout value " + t + " in ServiceDependency annotation from class " + m_currentClassName); + } + + // required attribute (not valid if parsing a temporal service dependency) + writer.putString(annotation, EntryParam.required, null); + + // changed callback + writer.putString(annotation, EntryParam.changed, null); + + // removed callback + writer.putString(annotation, EntryParam.removed, null); + + // swap callback + writer.putString(annotation, EntryParam.swap, null); + + // name attribute + parseDependencyName(writer, annotation); + + // propagate attribute + writer.putString(annotation, EntryParam.propagate, null); + + // dereference flag + writer.putString(annotation, EntryParam.dereference, String.valueOf(doDereference)); + + if (writer.getParameter(EntryParam.defaultImpl) != null) { + // If the defaultImpl attribute is set, then check if the required flag is set to false. + if ("true".equals(writer.getParameter(EntryParam.required))) { + throw new IllegalArgumentException("ServiceDependency defaultImpl attribute can't be used on a required dependency from class " + m_currentClassName); + } + // If the defaultImpl attribute is set, then check if dependency is applied on a class field + if (! m_isField) { + throw new IllegalArgumentException("ServiceDependency defaultImpl attribute can only be used when the ServiceDependency is applied on a class field from class " + m_currentClassName); + } + } + } + + /** + * Parse the value of a given annotation attribute (which is of type 'class'). + * This method is compatible with bndtools 2.4.1 (where the annotation.get() method returns a String of the form "Lfull/class/name;"), + * and with bndtools 3.x.x (where the annotation.get() method returns a TypeRef). + * + * @param annot the annotation which contains the given attribute + * @param attr the attribute name (of 'class' type). + * @return the annotation class attribute value + */ + public static String parseClassAttrValue(Object value) { + if (value instanceof String) + { + return Patterns.parseClass((String) value, Patterns.CLASS, 1); + } + else if (value instanceof TypeRef) + { + return ((TypeRef) value).getFQN(); + } + else if (value == null) { + return null; + } + else { + throw new IllegalStateException("can't parse class attribute value from " + value); + } + } + + /** + * Parses a ConfigurationDependency annotation. + * @param annotation the ConfigurationDependency annotation. + */ + private void parseConfigurationDependencyAnnotation(Annotation annotation) { + checkDependencyAlreadyDeclaredInChild(annotation, m_method, true); + + EntryWriter writer = new EntryWriter(EntryType.ConfigurationDependency); + m_writers.add(writer); + + // propagate attribute + writer.putString(annotation, EntryParam.propagate, null); + + // required flag (true by default) + writer.putString(annotation, EntryParam.required, Boolean.TRUE.toString()); + + // Parse possible config types specified in the updated callback. + // First, check if the update callback does not contain any config type parameters + String[] configTypes = EMPTY_STRING_ARRAY; + Matcher m = Patterns.UPDATED_NO_CONFIG_TYPES.matcher(m_descriptor.toString()); + if (! m.matches()) { + // the updated callback may contain some configuration types. + configTypes = parsePossibleConfigTypesFromUpdatedCallback(writer, m_descriptor); + } + + // Calculate the the pid, which value is either: + // + // - the value of the pid attribute, if specified + // - or the fqdn of the class specified by the pidFromClass attribute, if specified + // - or the fqdn of one configuration proxy type found from the updated callback method (if multiple config types are specified in the callback, + // then we can't derive the pid) + // - or by default the fdqn of the class where the annotation is found + String pid = get(annotation, EntryParam.pid.toString(), null); + if (pid == null) { + pid = parseClassAttrValue(annotation.get(EntryParam.pidClass.toString())); + } + if (pid == null) { + if (configTypes.length == 1) { + pid = configTypes[0]; + } + } + if (pid == null) { + pid = m_componentClassName; + } + + writer.put(EntryParam.pid, pid); + + // the method on which the annotation is applied + writer.put(EntryParam.updated, m_method); + + // name attribute + parseDependencyName(writer, annotation); + } + + /** + * Parses an AspectService annotation. + * @param annotation + */ + private void parseAspectService(Annotation annotation) + { + EntryWriter writer = new EntryWriter(EntryType.AspectService); + m_writers.add(writer); + + // Parse service filter + String filter = annotation.get(EntryParam.filter.toString()); + if (filter != null) + { + Verifier.verifyFilter(filter, 0); + writer.put(EntryParam.filter, filter); + } + + // Parse service aspect ranking + Integer ranking = annotation.get(EntryParam.ranking.toString()); + writer.put(EntryParam.ranking, ranking.toString()); + + // Generate Aspect Implementation + writer.put(EntryParam.impl, m_componentClassName); + + // Parse Adapter properties, and other params common to all kind of component + parseCommonComponentAttributes(annotation, writer); + + // Parse field/added/changed/removed attributes + parseAspectOrAdapterCallbackMethods(annotation, writer); + + // Parse service interface this aspect is applying to + Object service = annotation.get(EntryParam.service.toString()); + if (service == null) + { + if (m_interfaces == null) + { + throw new IllegalStateException("Invalid AspectService annotation: " + + "the service attribute has not been set and the class " + m_componentClassName + + " does not implement any interfaces"); + } + if (m_interfaces.length != 1) + { + throw new IllegalStateException("Invalid AspectService annotation: " + + "the service attribute has not been set and the class " + m_componentClassName + + " implements more than one interface"); + } + + writer.put(EntryParam.service, m_interfaces[0]); + } + else + { + writer.putClass(annotation, EntryParam.service); + } + + // Parse factoryMethod attribute + writer.putString(annotation, EntryParam.factoryMethod, null); + } + + private void parseAspectOrAdapterCallbackMethods(Annotation annotation, EntryWriter writer) + { + String field = annotation.get(EntryParam.field.toString()); + String added = annotation.get(EntryParam.added.toString()); + String changed = annotation.get(EntryParam.changed.toString()); + String removed = annotation.get(EntryParam.removed.toString()); + String swap = annotation.get(EntryParam.swap.toString()); + + // "field" and "added/changed/removed/swap" attributes can't be mixed + if (field != null && (added != null || changed != null || removed != null || swap != null)) + { + throw new IllegalStateException("Annotation " + annotation + "can't applied on " + m_componentClassName + + " can't mix \"field\" attribute with \"added/changed/removed\" attributes"); + } + + // Parse aspect impl field where to inject the original service. + writer.putString(annotation, EntryParam.field, null); + + // Parse aspect impl callback methods. + writer.putString(annotation, EntryParam.added, null); + writer.putString(annotation, EntryParam.changed, null); + writer.putString(annotation, EntryParam.removed, null); + writer.putString(annotation, EntryParam.swap, null); + } + + /** + * Parses an AspectService annotation. + * @param annotation + */ + private void parseAdapterService(Annotation annotation) + { + EntryWriter writer = new EntryWriter(EntryType.AdapterService); + m_writers.add(writer); + + // Generate Adapter Implementation + writer.put(EntryParam.impl, m_componentClassName); + + // Parse adaptee filter + String adapteeFilter = annotation.get(EntryParam.adapteeFilter.toString()); + if (adapteeFilter != null) + { + Verifier.verifyFilter(adapteeFilter, 0); + writer.put(EntryParam.adapteeFilter, adapteeFilter); + } + + // Parse the mandatory adapted service interface. + writer.putClass(annotation, EntryParam.adapteeService); + + // Parse Adapter properties, and other params common to all kind of component + parseCommonComponentAttributes(annotation, writer); + + // Parse the provided adapter service (use directly implemented interface by default). + if (writer.putClassArray(annotation, EntryParam.provides, m_interfaces, m_exportService) == 0) + { + checkRegisteredUnregisteredNotPresent(); + } + + // Parse factoryMethod attribute + writer.putString(annotation, EntryParam.factoryMethod, null); + + // Parse propagate flag. + // Parse factoryMethod attribute + writer.putString(annotation, EntryParam.propagate, null); + + // Parse field/added/changed/removed attributes + parseAspectOrAdapterCallbackMethods(annotation, writer); + } + + /** + * Parses a BundleAdapterService annotation. + * @param annotation + */ + private void parseBundleAdapterService(Annotation annotation) + { + EntryWriter writer = new EntryWriter(EntryType.BundleAdapterService); + m_writers.add(writer); + + // Generate Adapter Implementation + writer.put(EntryParam.impl, m_componentClassName); + + // Parse bundle filter + String filter = annotation.get(EntryParam.filter.toString()); + if (filter != null) + { + Verifier.verifyFilter(filter, 0); + writer.put(EntryParam.filter, filter); + } + + // Parse stateMask attribute + writer.putString(annotation, EntryParam.stateMask, Integer.valueOf( + Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE).toString()); + + // Parse Adapter properties, and other params common to all kind of component + parseCommonComponentAttributes(annotation, writer); + + // Parse the optional adapter service (use directly implemented interface by default). + if (writer.putClassArray(annotation, EntryParam.provides, m_interfaces, m_exportService) == 0) + { + checkRegisteredUnregisteredNotPresent(); + } + + // Parse propagate attribute + writer.putString(annotation, EntryParam.propagate, Boolean.FALSE.toString()); + + // Parse factoryMethod attribute + writer.putString(annotation, EntryParam.factoryMethod, null); + } + + /** + * Parses a Factory Pid Component + * @param annotation + */ + private void parseFactoryComponent(Annotation annotation) + { + EntryWriter writer = new EntryWriter(EntryType.FactoryConfigurationAdapterService); + m_writers.add(writer); + + // Generate Adapter Implementation + writer.put(EntryParam.impl, m_componentClassName); + + String factoryPid = get(annotation, EntryParam.factoryPid.toString(), null); + + if (factoryPid != null) { + writer.put(EntryParam.factoryPid, factoryPid); + } else { + // The finishFactoryComponent will set later the factoryPid (either to a config type specified in the updated callback, + // or to the component class name). See finishFactoryComponent() method. + } + + // Parse updated callback + writer.putString(annotation, EntryParam.updated, "updated"); + + // propagate attribute + writer.putString(annotation, EntryParam.propagate, Boolean.FALSE.toString()); + + // Parse the provided adapter service (use directly implemented interface by default). + if (writer.putClassArray(annotation, EntryParam.provides, m_interfaces, m_exportService) == 0) + { + checkRegisteredUnregisteredNotPresent(); + } + + // Parse Adapter properties, and other params common to all kind of component + parseCommonComponentAttributes(annotation, writer); + + // Parse factoryMethod attribute + writer.putString(annotation, EntryParam.factoryMethod, null); + m_logger.debug("Parsed factory configuration adapter annotations, methods=%s", m_methods); + } + + private void parseBundleDependencyAnnotation(Annotation annotation) + { + checkDependencyAlreadyDeclaredInChild(annotation, m_method, true); + + EntryWriter writer = new EntryWriter(EntryType.BundleDependency); + m_writers.add(writer); + + String filter = annotation.get(EntryParam.filter.toString()); + if (filter != null) + { + Verifier.verifyFilter(filter, 0); + writer.put(EntryParam.filter, filter); + } + + writer.putString(annotation, EntryParam.added, m_method); + writer.putString(annotation, EntryParam.changed, null); + writer.putString(annotation, EntryParam.removed, null); + writer.putString(annotation, EntryParam.required, null); + writer.putString(annotation, EntryParam.stateMask, null); + writer.putString(annotation, EntryParam.propagate, null); + parseDependencyName(writer, annotation); + } + + /** + * Parse the name of a given dependency. + * @param writer The writer where to write the dependency name + * @param annotation the dependency to be parsed + */ + private void parseDependencyName(EntryWriter writer, Annotation annotation) + { + String name = annotation.get(EntryParam.name.toString()); + if (name != null) + { + if(! m_dependencyNames.add(name)) + { + throw new IllegalArgumentException("Duplicate dependency name " + name + " in Dependency " + annotation + " from class " + m_currentClassName); + } + writer.put(EntryParam.name, name); + } + } + + private void parseLifecycleAnnotation(Annotation annotation) + { + Patterns.parseField(m_field, m_descriptor, Patterns.RUNNABLE); + if ("true".equals(get(annotation,EntryParam.start.name(), "true"))) + { + if (m_starter != null) { + throw new IllegalStateException("Lifecycle annotation already defined on field " + + m_starter + " in class (or super class of) " + m_componentClassName); + } + m_starter = m_field; + } else { + if (m_stopper != null) { + throw new IllegalStateException("Lifecycle annotation already defined on field " + + m_stopper + " in class (or super class of) " + m_componentClassName); + } + m_stopper = m_field; + } + } + + /** + * Parses attributes common to all kind of components. + * First, Property annotation is parsed which represents a list of key-value pair. + * The properties are encoded using the following json form: + * + * properties ::= key-value-pair* + * key-value-pair ::= key value + * value ::= String | String[] | value-type + * value-type ::= jsonObject with value-type-info + * value-type-info ::= "type"=primitive java type + * "value"=String|String[] + * + * Exemple: + * + * "properties" : { + * "string-param" : "string-value", + * "string-array-param" : ["str1", "str2], + * "long-param" : {"type":"java.lang.Long", "value":"1"}} + * "long-array-param" : {"type":"java.lang.Long", "value":["1"]}} + * } + * } + * + * @param component the component annotation which contains a "properties" attribute. The component can be either a @Component, or an aspect, or an adapter. + * @param writer the object where the parsed attributes are written. + */ + private void parseCommonComponentAttributes(Annotation component, EntryWriter writer) + { + // Parse properties attribute. + Object[] properties = component.get(EntryParam.properties.toString()); + if (properties != null) + { + for (Object property : properties) + { + Annotation propertyAnnotation = (Annotation) property; + parseProperty(propertyAnnotation, writer); + } + } + + // scope attribute + String scope = component.get("scope"); + if (scope == null) { + scope = ServiceScope.SINGLETON.name(); + } + writer.put(EntryParam.scope, scope); + } + + /** + * Parses a Property annotation. The result is added to the associated writer object + * @param annotation the @Property annotation. + * @param writer the writer object where the parsed property will be added to. + */ + private void parseProperty(Annotation property, EntryWriter writer) + { + String name = (String) property.get("name"); + String type = parseClassAttrValue(property.get("type")); + Class classType; + try + { + classType = (type == null) ? String.class : Class.forName(type); + } + catch (ClassNotFoundException e) + { + // Theorically impossible + throw new IllegalArgumentException("Invalid Property type " + type + + " from annotation " + property + " in class " + m_componentClassName); + } + + Object[] values; + + if ((values = property.get("value")) != null) + { + values = checkPropertyType(name, classType, values); + writer.addProperty(name, values, classType); + } + else if ((values = property.get("values")) != null) + { // deprecated + values = checkPropertyType(name, classType, values); + writer.addProperty(name, values, classType); + } + else if ((values = property.get("longValue")) != null) + { + writer.addProperty(name, values, Long.class); + } + else if ((values = property.get("doubleValue")) != null) + { + writer.addProperty(name, values, Double.class); + } + else if ((values = property.get("floatValue")) != null) + { + writer.addProperty(name, values, Float.class); + } + else if ((values = property.get("intValue")) != null) + { + writer.addProperty(name, values, Integer.class); + } + else if ((values = property.get("byteValue")) != null) + { + writer.addProperty(name, values, Byte.class); + } + else if ((values = property.get("charValue")) != null) + { + writer.addProperty(name, values, Character.class); + } + else if ((values = property.get("booleanValue")) != null) + { + writer.addProperty(name, values, Boolean.class); + } + else if ((values = property.get("shortValue")) != null) + { + writer.addProperty(name, values, Short.class); + } + else + { + throw new IllegalArgumentException( + "Missing Property value from annotation " + property + " in class " + m_componentClassName); + } + } + + /** + * Checks if a property contains values that are compatible with a give primitive type. + * + * @param name the property name + * @param values the values for the property name + * @param type the primitive type. + * @return the same property values, possibly modified if the type is 'Character' (the strings are converted to their character integer values). + */ + private Object[] checkPropertyType(String name, Class type, Object ... values) + { + if (type.equals(String.class)) + { + return values; + } else if (type.equals(Long.class)) { + for (Object value : values) { + try { + Long.valueOf(value.toString()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_componentClassName + + " does not contain a valid Long value (" + value.toString() + ")"); + } + } + } else if (type.equals(Double.class)) { + for (Object value : values) { + try { + Double.valueOf(value.toString()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_componentClassName + + " does not contain a valid Double value (" + value.toString() + ")"); + } + } + } else if (type.equals(Float.class)) { + for (Object value : values) { + try { + Float.valueOf(value.toString()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_componentClassName + + " does not contain a valid Float value (" + value.toString() + ")"); + } + } + } else if (type.equals(Integer.class)) { + for (Object value : values) { + try { + Integer.valueOf(value.toString()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_componentClassName + + " does not contain a valid Integer value (" + value.toString() + ")"); + } + } + } else if (type.equals(Byte.class)) { + for (Object value : values) { + try { + Byte.valueOf(value.toString()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_componentClassName + + " does not contain a valid Byte value (" + value.toString() + ")"); + } + } + } else if (type.equals(Character.class)) { + for (int i = 0; i < values.length; i++) + { + try + { + // If the string is already an integer, don't modify it + Integer.valueOf(values[i].toString()); + } + catch (NumberFormatException e) + { + // Converter the character string to its corresponding integer code. + if (values[i].toString().length() != 1) + { + throw new IllegalArgumentException("Property \"" + name + "\" in class " + + m_componentClassName + " does not contain a valid Character value (" + values[i] + ")"); + } + try + { + values[i] = Integer.valueOf(values[i].toString().charAt(0)); + } + catch (NumberFormatException e2) + { + throw new IllegalArgumentException("Property \"" + name + "\" in class " + + m_componentClassName + " does not contain a valid Character value (" + values[i].toString() + + ")"); + } + } + } + } else if (type.equals(Boolean.class)) { + for (Object value : values) { + if (! value.toString().equalsIgnoreCase("false") && ! value.toString().equalsIgnoreCase("true")) { + throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_componentClassName + + " does not contain a valid Boolean value (" + value.toString() + ")"); + } + } + } else if (type.equals(Short.class)) { + for (Object value : values) { + try { + Short.valueOf(value.toString()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_componentClassName + + " does not contain a valid Short value (" + value.toString() + ")"); + } + } + } + + return values; + } + + /** + * Parse Inject annotation, used to inject some special classes in some fields + * (BundleContext/DependencyManager etc ...) + * @param annotation the Inject annotation + */ + private void parseInject(Annotation annotation) + { + if (Patterns.BUNDLE_CONTEXT.matcher(m_descriptor).matches()) + { + if (m_bundleContextField != null) { + throw new IllegalStateException("detected multiple @Inject annotation from class " + m_currentClassName + " (on from child classes)"); + } + m_bundleContextField = m_field; + } + else if (Patterns.DEPENDENCY_MANAGER.matcher(m_descriptor).matches()) + { + if (m_dependencyManagerField != null) { + throw new IllegalStateException("detected multiple @Inject annotation from class " + m_currentClassName + " (on from child classes)"); + } + m_dependencyManagerField = m_field; + } + else if (Patterns.COMPONENT.matcher(m_descriptor).matches()) + { + if (m_componentField != null) { + throw new IllegalStateException("detected multiple @Inject annotation from class " + m_currentClassName + " (on from child classes)"); + } + m_componentField = m_field; + } + else if (Patterns.SERVICE_REGISTRATION.matcher(m_descriptor).matches()) + { + if (m_registrationField != null) { + throw new IllegalStateException("detected multiple @Inject annotation from class " + m_currentClassName + " (on from child classes)"); + } + m_registrationField = m_field; + } + else if (Patterns.BUNDLE.matcher(m_descriptor).matches()) + { + if (m_bundleField != null) { + throw new IllegalStateException("detected multiple @Inject annotation from class " + m_currentClassName + " (on from child classes)"); + } + m_bundleField = m_field; + } + else + { + throw new IllegalArgumentException("@Inject annotation can't be applied on the field \"" + m_field + + "\" in class " + m_currentClassName); + } + } + + /** + * This method checks if the @Registered and/or @Unregistered annotations have been defined + * while they should not, because the component does not provide a service. + */ + private void checkRegisteredUnregisteredNotPresent() + { + if (m_registeredMethod != null) + { + throw new IllegalStateException("@Registered annotation can't be used on a Component " + + " which does not provide a service (class=" + m_currentClassName + ")"); + + } + + if (m_unregisteredMethod != null) + { + throw new IllegalStateException("@Unregistered annotation can't be used on a Component " + + " which does not provide a service (class=" + m_currentClassName + ")"); + + } + } + + /** + * Get an annotation attribute, and return a default value if its not present. + * @param the type of the variable which is assigned to the return value of this method. + * @param annotation The annotation we are parsing + * @param name the attribute name to get from the annotation + * @param defaultValue the default value to return if the attribute is not found in the annotation + * @return the annotation attribute value, or the defaultValue if not found + */ + @SuppressWarnings("unchecked") + private T get(Annotation annotation, String name, T defaultValue) + { + T value = (T) annotation.get(name); + return value != null ? value : defaultValue; + } + + /** + * Check if an annotation applied on the component class is a component property type. + * (the following is adapted from the original bnd DS AnnotationReader class) + */ + private void handlePossibleComponentPropertyAnnotation(Annotation annotation) { + m_logger.debug("Checking possible component property annotations %s", annotation); + + try { + Clazz clazz = m_analyzer.findClass(annotation.getName()); + + if (clazz == null) { + m_logger.debug( + "Unable to find the annotation %s applied to type %s from project build path (ignoring it).", + annotation.getName().getFQN(), m_currentClassName); + return; + } + + if (clazz.is(ANNOTATED, DS_PROPERTY_TYPE, m_analyzer) || clazz.is(ANNOTATED, DM_PROPERTY_TYPE, m_analyzer)) { + m_logger.debug("Found component property type %s", annotation); + m_componentPropertyTypes.add(annotation); + } else { + m_logger.debug("The annotation %s on component type %s will not be used for properties as the annotation is not annotated with @ComponentPropertyType", + clazz.getFQN(), m_currentClassName); + return; + } + } catch (Exception e) { + m_logger.error("An error occurred when attempting to process annotation %s, applied to component %s", e, + annotation.getName().getFQN(), m_currentClassName); + } + } + + /** + * Parse a component property type annotation applied on the component class. + * (the following is adapted from the original bnd DS AnnotationReader class) + */ + private void parseComponentPropertyAnnotation(EntryWriter componentWriter) { + for (Annotation annotation : m_componentPropertyTypes) { + try { + Clazz clazz = m_analyzer.findClass(annotation.getName()); + if (clazz == null) { + m_logger.warn( + "Unable to determine whether the annotation %s applied to type %s is a component property type as it is not on the project build path. If this annotation is a component property type then it must be present on the build path in order to be processed", + annotation.getName().getFQN(), m_currentClassName); + continue; + } + + m_logger.debug("Parsing component property type %s", annotation); + clazz.parseClassFileWithCollector(new ComponentPropertyTypeDataCollector(annotation, componentWriter)); + } catch (Exception e) { + m_logger.error("An error occurred when attempting to process annotation %s, applied to component %s", e, + annotation.getName().getFQN(), m_currentClassName); + } + } + } + + private final class ComponentPropertyTypeDataCollector extends ClassDataCollector { + private final MultiMap props = new MultiMap(); + private final Map propertyTypes = new HashMap<>(); + private int hasNoDefault = 0; + private boolean hasValue = false; + private boolean hasMethods = false; + private FieldDef prefixField = null; + private TypeRef typeRef = null; + private final EntryWriter m_componentWriter; + + private ComponentPropertyTypeDataCollector(EntryWriter componentWriter) { + m_componentWriter = componentWriter; + } + + private ComponentPropertyTypeDataCollector(Annotation componentPropertyAnnotation, EntryWriter componentWriter) { + m_componentWriter = componentWriter; + // Add in the defined attributes + for (String key : componentPropertyAnnotation.keySet()) { + Object value = componentPropertyAnnotation.get(key); + m_logger.debug("ComponentPropertyTypeDataCollector: handle value %s %s", key, value); + handleValue(key, value, value instanceof TypeRef, null); + } + } + + @Override + public void classBegin(int access, TypeRef name) { + m_logger.debug("PropertyType: class begin %s", name); + typeRef = name; + } + + @Override + public void field(FieldDef defined) { + m_logger.debug("PropertyType: field %s", defined); + if (defined.isStatic() && defined.getName().equals("PREFIX_")) { + prefixField = defined; + } + } + + @Override + public void method(MethodDef defined) { + m_logger.debug("PropertyType: method %s", defined); + + if (defined.isStatic()) { + return; + } + hasMethods = true; + if (defined.getName().equals("value")) { + hasValue = true; + } else { + hasNoDefault++; + } + } + + @Override + public void annotationDefault(MethodDef defined, Object value) { + m_logger.debug("PropertyType: annotationDefault method %s value %s", defined, value); + + if (!defined.getName().equals("value")) { + hasNoDefault--; + } + // check type, exit with warning if annotation + // or annotation array + boolean isClass = false; + Class< ? > typeClass = null; + TypeRef type = defined.getType().getClassRef(); + if (!type.isPrimitive()) { + if (type == m_analyzer.getTypeRef("java/lang/Class")) { + isClass = true; + } else { + try { + Clazz r = m_analyzer.findClass(type); + if (r.isAnnotation()) { + m_logger.warn("Nested annotation type found in member %s, %s", + defined.getName(), type.getFQN()); + return; + } + } catch (Exception e) { + m_logger.error("Exception looking at annotation type to lifecycle method with type %s", e, type); + } + } + } else { + m_logger.debug("PropertyType: type.getFQN()=%s", type.getFQN()); + typeClass = m_types.get(type.getFQN()); + } + if (value != null) { + String name = defined.getName(); + // only add the default value if the user has not specified the property name + if (! props.containsKey(name)) { + handleValue(name, value, isClass, typeClass); + } + } + } + + private void handleValue(String name, Object value, boolean isClass, Class< ? > typeClass) { + if (value.getClass().isArray()) { + // add element individually + int len = Array.getLength(value); + for (int i = 0; i < len; i++) { + Object element = Array.get(value, i); + valueToProperty(name, element, isClass, typeClass); + } +// if (len == 1) { +// // To make sure the output is an array, we must make +// // sure there is more than one entry +// props.add(name, MARKER); +// } + } else { + valueToProperty(name, value, isClass, typeClass); + } + } + + @Override + public void classEnd() throws Exception { + m_logger.debug("PropertyType: classEnd"); + + String prefix = null; + if (prefixField != null) { + Object c = prefixField.getConstant(); + if (prefixField.isFinal() && (prefixField.getType() == m_analyzer.getTypeRef("java/lang/String")) + && (c instanceof String)) { + prefix = (String) c; + } else { + m_logger.warn( + "Field PREFIX_ in %s is not a static final String field with a compile-time constant value: %s", + typeRef.getFQN(), c); + } + m_logger.debug("PropertyType: classEnd prefix = %s", prefix); + } + + if (!hasMethods) { + // This is a marker annotation so treat it like it is a single + // element annotation with a value of Boolean.TRUE + hasValue = true; + handleValue("value", Boolean.TRUE, false, Boolean.class); + } + + String singleElementAnnotation = null; + if (hasValue && (hasNoDefault == 0)) { + m_logger.debug("PropertyType: classEnd hasValue && hasNoDefault == 0"); + + StringBuilder sb = new StringBuilder(typeRef.getShorterName()); + boolean lastLowerCase = false; + for (int i = 0; i < sb.length(); i++) { + char c = sb.charAt(i); + if (Character.isUpperCase(c)) { + sb.setCharAt(i, Character.toLowerCase(c)); + if (lastLowerCase) { + sb.insert(i++, '.'); + } + lastLowerCase = false; + } else { + lastLowerCase = Character.isLowerCase(c); + } + } + singleElementAnnotation = sb.toString(); + m_logger.debug("PropertyType: classEnd singleElementAnnotation=%s", singleElementAnnotation); + } + m_logger.debug("PropertyType: classEnd props=" + props); + for (Entry> entry : props.entrySet()) { + String key = entry.getKey(); + List value = entry.getValue(); + Class type = propertyTypes.get(key); + if ((singleElementAnnotation != null) && key.equals("value")) { + key = singleElementAnnotation; + } else { + key = identifierToPropertyName(key); + } + if (prefix != null) { + key = prefix + key; + } + + m_componentWriter.addProperty(key, value.toArray(), type); + m_logger.info("PropertyType: parsed property key:%s, value:%s, type:%s", key, value, type); + } + } + + private void valueToProperty(String name, Object value, boolean isClass, Class typeClass) { + if (isClass) + value = ((TypeRef) value).getFQN(); + if (typeClass == null) + typeClass = value.getClass(); + // enums already come out as the enum name, no + // processing needed. + //String type = typeClass.getSimpleName(); + String type = typeClass.getName(); + propertyTypes.put(name, typeClass); + props.add(name, value.toString()); + } + + private String identifierToPropertyName(String name) { + name = derivePropertyNameUsingJavaBeanConvention(name); + name = derivePropertyNameUsingCamelCaseConvention(name); + name = derivePropertyNameUsingMetatypeConvention(name); + return name; + } + + // getFoo -> foo; isFoo -> foo + private String derivePropertyNameUsingJavaBeanConvention(String methodName) { + StringBuilder sb = new StringBuilder(methodName); + + if (methodName.startsWith("get")) { + sb.delete(0, 3); + } else if (methodName.startsWith("is")) { + sb.delete(0, 2); + } + + char c = sb.charAt(0); + if (Character.isUpperCase(c)) { + sb.setCharAt(0, Character.toLowerCase(c)); + } + + return (sb.toString()); + } + + // fooBarZoo -> foo.bar.zoo + private String derivePropertyNameUsingCamelCaseConvention(String methodName) { + StringBuilder sb = new StringBuilder(methodName); + for (int i = 0; i < sb.length(); i++) { + char c = sb.charAt(i); + if (Character.isUpperCase(c)) { + // camel casing: replace fooBar -> foo.bar + sb.setCharAt(i, Character.toLowerCase(c)); + sb.insert(i, "."); + } + } + return sb.toString(); + } + + // foo__bar -> foo_bar; foo_bar -> foo.bar + private String derivePropertyNameUsingMetatypeConvention(String name) { + Matcher m = IDENTIFIERTOPROPERTY.matcher(name); + StringBuffer b = new StringBuffer(); + while (m.find()) { + String replace; + if (m.group(1) != null) // __ to _ + replace = "_"; + else if (m.group(2) != null) // _ to . + replace = "."; + else if (m.group(3) != null) { // $_$ to - + replace = "-"; + } + else if (m.group(4) != null) // $$ to $ + replace = "\\$"; + else // $ removed. + replace = ""; + m.appendReplacement(b, replace); + } + m.appendTail(b); + return b.toString(); + } + } + + public static Map.Entry entry(K key, V value) { + return new AbstractMap.SimpleEntry<>(key, value); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/AnnotationPlugin.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/AnnotationPlugin.java new file mode 100644 index 00000000000..b2613763a64 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/AnnotationPlugin.java @@ -0,0 +1,164 @@ +/* + * 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.dm.annotation.plugin.bnd; + +import java.util.Map; +import java.util.Set; + +import aQute.bnd.osgi.Analyzer; +import aQute.bnd.osgi.Resource; +import aQute.bnd.service.AnalyzerPlugin; +import aQute.bnd.service.Plugin; +import aQute.service.reporter.Reporter; + +/** + * This class is a BND plugin. It scans the target bundle and look for DependencyManager annotations. + * It can be directly used when using ant and can be referenced inside the ".bnd" descriptor, using + * the "-plugin" parameter. + * + * @author Felix Project Team + */ +public class AnnotationPlugin implements AnalyzerPlugin, Plugin { + private static final String IMPORT_SERVICE = "Import-Service"; + private static final String EXPORT_SERVICE = "Export-Service"; + private static final String REQUIRE_CAPABILITY = "Require-Capability"; + + private static final String LOGLEVEL = "log"; + private static final String BUILD_IMPEXT = "build-import-export-service"; + private static final String ADD_REQUIRE_CAPABILITY = "add-require-capability"; + private static final String DM_RUNTIME_CAPABILITY = "osgi.extender; filter:=\"(&(osgi.extender=org.apache.felix.dependencymanager.runtime)(version>=4.0.0))\""; + private BndLogger m_logger; + private Reporter m_reporter; + private boolean m_buildImportExportService; + private boolean m_addRequireCapability; + private Map m_properties; + + public void setReporter(Reporter reporter) { + m_reporter = reporter; + } + + public void setProperties(Map map) { + m_properties = map; + } + + /** + * This plugin is called after analysis of the JAR but before manifest + * generation. When some DM annotations are found, the plugin will add the corresponding + * DM component descriptors under META-INF/ directory. It will also set the + * "DependencyManager-Component" manifest header (which references the descriptor paths). + * + * @param analyzer the object that is used to retrieve classes containing DM annotations. + * @return true if the classpath has been modified so that the bundle classpath must be reanalyzed + * @throws Exception on any errors. + */ + public boolean analyzeJar(Analyzer analyzer) throws Exception { + try { + init(analyzer); + + // We'll do the actual parsing using a DescriptorGenerator object. + DescriptorGenerator generator = new DescriptorGenerator(analyzer, m_logger); + + if (generator.execute()) { + // We have parsed some annotations: set the OSGi "DependencyManager-Component" header in the target bundle. + analyzer.setProperty("DependencyManager-Component", generator.getDescriptorPaths()); + + if (m_addRequireCapability) { + // Add our Require-Capability header + buildRequireCapability(analyzer); + } + + // Possibly set the Import-Service/Export-Service header + if (m_buildImportExportService) { + // Don't override Import-Service header, if it is found from the bnd directives. + if (analyzer.getProperty(IMPORT_SERVICE) == null) { + buildImportExportService(analyzer, IMPORT_SERVICE, generator.getImportService()); + } + + // Don't override Export-Service header, if already defined + if (analyzer.getProperty(EXPORT_SERVICE) == null) { + buildImportExportService(analyzer, EXPORT_SERVICE, generator.getExportService()); + } + } + + // And insert the generated descriptors into the target bundle. + Map resources = generator.getDescriptors(); + for (Map.Entry entry : resources.entrySet()) { + analyzer.getJar().putResource(entry.getKey(), entry.getValue()); + } + + // Insert the metatype resource, if any. + Resource metaType = generator.getMetaTypeResource(); + if (metaType != null) { + analyzer.getJar().putResource("OSGI-INF/metatype/metatype.xml", metaType); + } + } + } + + catch (Throwable t) { + m_logger.error("Dependency Manager Annotation Errors: %s", t, t); + } + + finally { + m_logger.close(); + } + + return false; // do not reanalyze bundle classpath because our plugin has not changed it. + } + + private void init(Analyzer analyzer) { + m_logger = new BndLogger(m_reporter, analyzer.getBsn(), parseOption(m_properties, LOGLEVEL, null)); + m_buildImportExportService = parseOption(m_properties, BUILD_IMPEXT, false); + m_addRequireCapability = parseOption(m_properties, ADD_REQUIRE_CAPABILITY, false); + analyzer.setExceptions(true); + m_logger.info("Initialized Bnd DependencyManager plugin: buildImportExport=%b", m_buildImportExportService); + } + + private String parseOption(Map opts, String name, String def) { + String value = opts.get(name); + return value == null ? def : value; + } + + private boolean parseOption(Map opts, String name, boolean def) { + String value = opts.get(name); + return value == null ? def : Boolean.valueOf(value); + } + + private void buildImportExportService(Analyzer analyzer, String header, Set services) { + m_logger.info("building %s header with the following services: %s", header, services); + if (services.size() > 0) { + StringBuilder sb = new StringBuilder(); + for (String service : services) { + sb.append(service); + sb.append(","); + } + sb.setLength(sb.length() - 1); // skip last comma + analyzer.setProperty(header, sb.toString()); + } + } + + private void buildRequireCapability(Analyzer analyzer) { + String requireCapability = analyzer.getProperty(REQUIRE_CAPABILITY); + if (requireCapability == null) { + analyzer.setProperty(REQUIRE_CAPABILITY, DM_RUNTIME_CAPABILITY); + } else { + StringBuilder sb = new StringBuilder(requireCapability).append(",").append(DM_RUNTIME_CAPABILITY); + analyzer.setProperty(REQUIRE_CAPABILITY, sb.toString()); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/BndLogger.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/BndLogger.java new file mode 100644 index 00000000000..5caf2063950 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/BndLogger.java @@ -0,0 +1,203 @@ +/* + * 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.dm.annotation.plugin.bnd; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.text.SimpleDateFormat; +import java.util.Date; + +import aQute.service.reporter.Reporter; + +/** + * Clas used to log messages into the bnd logger. + * + * @author Felix Project Team + */ +public class BndLogger extends Logger { + private final Reporter m_reporter; + + /** + * Writer to log file, in tmp dir/dmplugin-BSN.log + */ + private final PrintWriter logWriter; + + /** + * DateFormat used when logging. + */ + private final static SimpleDateFormat dateFormat = new SimpleDateFormat("E yyyy.MM.dd hh:mm:ss.S"); + + /** + * Enabled log level, which can be configured in bnd plugin declaration. + */ + private Level logEnabled = Level.Warn; + + /** + * Creates a new bnd Log implementaiton + * + * @param reporter + * the bnd logger + * @param logLevel + * @param bsn + */ + public BndLogger(Reporter reporter, String bsn, String level) { + m_reporter = reporter; + if (level != null) { + setLevel(level); + File logFilePath = new File(System.getProperty("java.io.tmpdir") + File.separator + "dmplugin" + + File.separator + bsn + ".log"); + new File(logFilePath.getParent()).mkdirs(); + + PrintWriter writer = null; + try { + writer = new PrintWriter(new FileWriter(logFilePath, false)); + } catch (IOException e) { + reporter.exception(e, "Could not create scrplugin log file: %s", logFilePath); + writer = null; + } + this.logWriter = writer; + } else { + this.logWriter = null; // no logging enabled, we'll only report warn/errs to bnd reporter. + } + } + + /** + * Close log file. + */ + public void close() { + if (logWriter != null) { + logWriter.close(); + } + } + + // Reporter + + public boolean isDebugEnabled() { + return logEnabled.ordinal() >= Level.Debug.ordinal(); + } + + public void debug(String content, Object ... args) { + if (isDebugEnabled()) { + m_reporter.trace(content, args); + logDebug(String.format(content, args), null); + } + } + + public boolean isInfoEnabled() { + return logEnabled.ordinal() >= Level.Info.ordinal(); + } + + public void info(String content, Object ... args) { + if (isInfoEnabled()) { + m_reporter.trace(content, args); + logInfo(String.format(content, args), null); + } + } + + public boolean isWarnEnabled() { + return logEnabled.ordinal() >= Level.Warn.ordinal(); + } + + public void warn(String content, Object ... args) { + if (isWarnEnabled()) { + m_reporter.warning(content, args); + logWarn(String.format(content, args), null); + } + } + + public void warn(String content, Throwable err, Object ... args) { + if (isWarnEnabled()) { + m_reporter.warning(content, args); + logWarn(String.format(content, args), err); + } + } + + public boolean isErrorEnabled() { + return logEnabled.ordinal() >= Level.Error.ordinal(); + } + + public void error(String content, Throwable err, Object ... args) { + m_reporter.error(content, args); + logErr(String.format(content, args), err); + } + + public void error(String content, Object ... args) { + m_reporter.error(content, args); + logErr(String.format(content, args), null); + } + + /** + * Sets the enabled log level. + * + * @param level + * the enabled level ("Error", "Warn", "Info", or "Debug") + */ + private void setLevel(String level) { + try { + level = Character.toUpperCase(level.charAt(0)) + + level.substring(1).toLowerCase(); + this.logEnabled = Level.valueOf(level); + } catch (IllegalArgumentException e) { + this.logEnabled = Level.Warn; + warn("Bnd scrplugin logger initialized with invalid log level: " + + level); + } + } + + private void logErr(String msg, Throwable t) { + log(Level.Error, msg, t); + } + + private void logWarn(String msg, Throwable t) { + log(Level.Warn, msg, t); + } + + private void logInfo(String msg, Throwable t) { + log(Level.Info, msg, t); + } + + private void logDebug(String msg, Throwable t) { + log(Level.Debug, msg, t); + } + + private void log(Level level, String msg, Throwable t) { + if (logWriter != null) { + StringBuilder sb = new StringBuilder(); + sb.append(dateFormat.format(new Date())); + sb.append(" - "); + sb.append(level); + sb.append(": "); + sb.append(msg); + if (t != null) { + sb.append(" - ").append(toString(t)); + } + logWriter.println(sb.toString()); + } + } + + private static String toString(Throwable e) { + StringWriter buffer = new StringWriter(); + PrintWriter pw = new PrintWriter(buffer); + e.printStackTrace(pw); + return (buffer.toString()); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/DescriptorGenerator.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/DescriptorGenerator.java new file mode 100644 index 00000000000..ad00cbf9b78 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/DescriptorGenerator.java @@ -0,0 +1,226 @@ +/* + * 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.dm.annotation.plugin.bnd; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import aQute.bnd.osgi.Analyzer; +import aQute.bnd.osgi.Clazz; +import aQute.bnd.osgi.EmbeddedResource; +import aQute.bnd.osgi.Resource; +import aQute.bnd.osgi.Clazz.QUERY; +import aQute.bnd.osgi.Descriptors.TypeRef; + +/** + * This helper parses all classes which contain DM annotations, and generates the corresponding component descriptors. + * + * @author Felix Project Team + */ +public class DescriptorGenerator +{ + /** + * This is the bnd analyzer used to lookup classes containing DM annotations. + */ + private Analyzer m_analyzer; + + /** + * This is the generated Dependency Manager descriptors. The hashtable key is the path + * to a descriptor. The value is a bnd Resource object which contains the content of a + * descriptor. + */ + Map m_resources = new HashMap(); + + /** + * This is the generated MetaType XML descriptor, if any Properties/Property annotations have been found. + */ + private Resource m_metaTypeResource; + + /** + * Object used to collect logs. + */ + private final Logger m_logger; + + /** + * List of imported services found from every ServiceDependency annotations. + */ + private Set m_importService = new HashSet(); + + /** + * List of exported services found from every service providing components. + */ + private Set m_exportService = new HashSet(); + + /** + * Creates a new descriptor generator. + * @param analyzer The bnd analyzer used to lookup classes containing DM annotations. + * @param debug + */ + public DescriptorGenerator(Analyzer analyzer, Logger logger) + { + m_analyzer = analyzer; + m_logger = logger; + } + + /** + * Starts the scanning. + * @return true if some annotations were successfully parsed, false if not. corresponding generated + * descriptors can then be retrieved by invoking the getDescriptors/getDescriptorPaths methods. + */ + public boolean execute() throws Exception + { + boolean annotationsFound = false; + // Try to locate any classes in the wildcarded universe + // that are annotated with the DependencyManager "Service" annotations. + Collection expanded = m_analyzer.getClasses("", + // Parse everything + QUERY.NAMED.toString(), "*"); + + for (Clazz c : expanded) + { + AnnotationCollector reader = new AnnotationCollector(m_logger, m_analyzer); + reader.baseClass(true); + m_logger.debug("scanning class %s", c.getClassName()); + c.parseClassFileWithCollector(reader); + + // parse inherited annotations. + while (reader.getSuperClass() != null) { + Clazz superClazz = m_analyzer.findClass(reader.getSuperClass()); + if (superClazz == null) { + m_logger.error("Can't find super class %s from %s", reader.getSuperClass(), c.getClassName()); + break; + } + if (isObject(reader.getSuperClass()) || isScalaObject(reader.getSuperClass())) { + /* don't scan java.lang.Object or scala object ! */ + break; + } + + m_logger.debug("scanning super class %s for class %s", reader.getSuperClass(), c.getClassName()); + reader.baseClass(false); + superClazz.parseClassFileWithCollector(reader); + } + + if (reader.finish()) + { + // And store the generated component descriptors in our resource list. + String name = c.getFQN(); + Resource resource = createComponentResource(reader); + m_resources.put("META-INF/dependencymanager/" + name, resource); + annotationsFound = true; + + m_importService.addAll(reader.getImportService()); + m_exportService.addAll(reader.getExportService()); + } + } + + return annotationsFound; + } + + /** + * Returns the path of the descriptor. + * @return the path of the generated descriptors. + */ + public String getDescriptorPaths() + { + StringBuilder descriptorPaths = new StringBuilder(); + String del = ""; + for (Map.Entry entry : m_resources.entrySet()) + { + descriptorPaths.append(del); + descriptorPaths.append(entry.getKey()); + del = ","; + } + return descriptorPaths.toString(); + } + + /** + * Returns the list of the generated descriptors. + * @return the list of the generated descriptors. + */ + public Map getDescriptors() + { + return m_resources; + } + + /** + * Returns the MetaType resource. + */ + public Resource getMetaTypeResource() { + return m_metaTypeResource; + } + + /** + * Returns set of all imported services. Imported services are deduced from every + * @ServiceDependency annotations. + * @return the list of imported services + */ + public Set getImportService() + { + return m_importService; + } + + /** + * Returns set of all exported services. Imported services are deduced from every + * annotations which provides a service (@Component, etc ...) + * @return the list of exported services + */ + public Set getExportService() + { + return m_exportService; + } + + /** + * Tests if a given type is java.lang.Object + */ + private boolean isObject(TypeRef typeRef) { + return (typeRef.isJava()); + } + + /** + * Tests if a given type is scala object. + */ + private boolean isScalaObject(TypeRef typeRef) { + return (typeRef.getBinary().equals("scala/ScalaObject")); + } + + /** + * Creates a bnd resource that contains the generated dm descriptor. + * @param collector + * @return + * @throws IOException + */ + private Resource createComponentResource(AnnotationCollector collector) throws IOException + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PrintWriter pw = new PrintWriter(new OutputStreamWriter(out, "UTF-8")); + collector.writeTo(pw); + pw.close(); + byte[] data = out.toByteArray(); + out.close(); + return new EmbeddedResource(data, 0); + } + +} \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/EntryParam.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/EntryParam.java new file mode 100644 index 00000000000..f34a89c1491 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/EntryParam.java @@ -0,0 +1,75 @@ +/* + * 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.dm.annotation.plugin.bnd; + +/** + * The type of parameters which can be found in a component descriptor. + * + * @author Felix Project Team + */ +public enum EntryParam +{ + init, + start, + stop, + destroy, + impl, + provides, + properties, + composition, + service, + filter, + defaultImpl, + required, + added, + changed, + removed, + swap, + autoConfig, + pid, + pidClass, + configType, // inject a proxy configuration type + configTypes, // inject multiple proxy configuration type interfaces + factoryPid, + factoryPidClass, + propagate, + updated, + timeout, + adapteeService, + adapteeFilter, + stateMask, + ranking, + factorySet, + factoryName, + factoryConfigure, + factoryMethod, + field, + name, + starter, + stopper, + bundleContextField, + dependencyManagerField, + componentField, + registrationField, + registered, + unregistered, + dereference, + scope, + bundle +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/EntryType.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/EntryType.java new file mode 100644 index 00000000000..274505b2fa7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/EntryType.java @@ -0,0 +1,38 @@ +/* + * 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.dm.annotation.plugin.bnd; + +/** + * The type of each entry (lines) stored in a component descriptor. + * + * @author Felix Project Team + */ +public enum EntryType +{ + Component, + AspectService, + AdapterService, + BundleAdapterService, + ResourceAdapterService, + FactoryConfigurationAdapterService, + ServiceDependency, + ConfigurationDependency, + BundleDependency, + ResourceDependency, +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/EntryWriter.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/EntryWriter.java new file mode 100644 index 00000000000..ba0c8286b18 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/EntryWriter.java @@ -0,0 +1,255 @@ +/* + * 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.dm.annotation.plugin.bnd; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import aQute.bnd.osgi.Annotation; + +/** + * This class encodes a component descriptor entry line, using json. + * We are using a slightly adapted version of the nice JsonSerializingImpl class from the Apache Felix Converter project. + * + * Internally, we store parameters in a map. The format of key/values stored in the map is the following: + * + * The JSON object has the following form: + * + * entry ::= String | String[] | dictionary + * dictionary ::= key-value-pair* + * key-value-pair ::= key value + * value ::= String | String[] | value-type + * value-type ::= jsonObject with value-type-info + * value-type-info ::= "type"=primitive java type + * "value"=String|String[] + * + * Exemple: + * + * {"string-param" : "string-value", + * "string-array-param" : ["string1", "string2"], + * "properties" : { + * "string-param" : "string-value", + * "string-array-param" : ["str1", "str2], + * "long-param" : {"type":"java.lang.Long", "value":"1"}} + * "long-array-param" : {"type":"java.lang.Long", "value":["1"]}} + * } + * } + * + * @author Felix Project Team + */ +public class EntryWriter +{ + /** + * Every descriptor entries contains a type parameter for identifying the kind of entry + */ + private final static String TYPE = "type"; + + /** + * All parameters as stored in a map object + */ + private final HashMap m_params = new HashMap<>(); + + /** The entry type */ + private final EntryType m_type; + + /** + * Makes a new component descriptor entry. + */ + public EntryWriter(EntryType type) + { + m_type = type; + m_params.put("type", type.toString()); + } + + /** + * Returns this entry type. + */ + EntryType getEntryType() + { + return m_type; + } + + /** + * Returns a string representation for the given component descriptor entry. + * @param m_logger + */ + public String toString() + { + return new JsonWriter(m_params).toString(); + } + + /** + * Adds a String parameter in this descritor entry. + */ + public void put(EntryParam param, String value) + { + checkType(param.toString()); + m_params.put(param.toString(), value); + } + + /** + * Adds a String[] parameter in this descriptor entry. + */ + public void put(EntryParam param, String[] values) + { + checkType(param.toString()); + m_params.put(param.toString(), Arrays.asList(values)); + } + + /** + * Adds a List of Strings in this descriptor entry. + */ + public void put(EntryParam param, List values) + { + checkType(param.toString()); + m_params.put(param.toString(), values); + } + + /** + * Returns a parameter value (either a String, or a List). + */ + public Object getParameter(EntryParam param) { + return m_params.get(param.toString()); + } + + /** + * Adds a property in this descriptor entry. + */ + @SuppressWarnings("unchecked") + public void addProperty(String name, Object value, Class type) + { + Map properties = (Map) m_params.get(EntryParam.properties.toString()); + if (properties == null) { + properties = new HashMap<>(); + m_params.put(EntryParam.properties.toString(), properties); + } + if (value.getClass().isArray()) + { + Object[] array = (Object[]) value; + if (array.length == 1) + { + value = array[0]; + } + } + + if (type.equals(String.class)) + { + properties.put(name, value.getClass().isArray() ? Arrays.asList((Object[]) value) : value); + } + else + { + Map val = new HashMap<>(); + val.put("type", type.getName()); + val.put("value", value.getClass().isArray() ? Arrays.asList((Object[]) value) : value); + properties.put(name, val); + } + } + + /** + * Get a String attribute value from an annotation and write it into this descriptor entry. + */ + public String putString(Annotation annotation, EntryParam param, String def) + { + checkType(param.toString()); + Object value = annotation.get(param.toString()); + if (value == null && def != null) + { + value = def; + } + if (value != null) + { + put(param, value.toString()); + } + return value == null ? null : value.toString(); + } + + /** + * Get a class attribute value from an annotation and write it into this descriptor entry. + */ + public void putClass(Annotation annotation, EntryParam param) + { + checkType(param.toString()); + String value = AnnotationCollector.parseClassAttrValue(annotation.get(param.toString())); + if (value != null) + { + put(param, value); + } + } + + /** + * Get a class array attribute value from an annotation and write it into this descriptor entry. + * + * @param annotation the annotation containing an array of classes + * @param param the attribute name corresponding to an array of classes + * @param def the default array of classes (String[]), if the attribute is not defined in the annotation + * @return the class array size. + */ + public int putClassArray(Annotation annotation, EntryParam param, Object def, Set collect) + { + checkType(param.toString()); + + boolean usingDefault = false; + Object value = annotation.get(param.toString()); + if (value == null && def != null) + { + value = def; + usingDefault = true; + } + if (value != null) + { + if (!(value instanceof Object[])) + { + throw new IllegalArgumentException("annotation parameter " + param + + " has not a class array type"); + } + + List classes = new ArrayList<>(); + for (Object v: ((Object[]) value)) + { + if (! usingDefault) + { + // Parse the annotation attribute value. + v = AnnotationCollector.parseClassAttrValue(v); + } + classes.add(v.toString()); + collect.add(v.toString()); + } + + m_params.put(param.toString(), classes); + return ((Object[]) value).length; + } + return 0; + } + + /** + * Check if the written key is not equals to "type" ("type" is an internal attribute we are using + * in order to identify a kind of descriptor entry (Service, ServiceDependency, etc ...). + */ + private void checkType(String key) + { + if (TYPE.equals(key)) + { + throw new IllegalArgumentException("\"" + TYPE + "\" parameter can't be overriden"); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/FieldTypeGetter.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/FieldTypeGetter.java new file mode 100644 index 00000000000..f3f7c61f61d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/FieldTypeGetter.java @@ -0,0 +1,102 @@ +/* + * 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.dm.annotation.plugin.bnd; + +import aQute.bnd.osgi.Clazz.FieldDef; + +/** + * Class extracted from bndtools (aQute.bnd.component.AnnotationScanner), which + * allows to determine class field collection type. + * For example, given "private Collection services;", this class allows to determine the + * "MyService" type. + */ +public class FieldTypeGetter { + public enum FieldCollectionType { + service, properties, reference, serviceobjects, tuple + } + + static String determineFieldType(Logger log, FieldDef member) { + String field = member.getName(); + String sig = member.getSignature(); + if (sig == null) + // no generics, the descriptor will be the class name. + sig = member.getDescriptor().toString(); + String[] sigs = sig.split("[<;>]"); + int sigLength = sigs.length; + int index = 0; + boolean isCollection = false; + + if ("Ljava/lang/Iterable".equals(sigs[index]) || "Ljava/util/Collection".equals(sigs[index]) || "Ljava/util/List".equals(sigs[index])) { + index++; + isCollection = true; + } + // Along with determining the FieldCollectionType, the following + // code positions index to read the service type. + FieldCollectionType fieldCollectionType = null; + if (sufficientGenerics(index, sigLength, sig)) { + if ("Lorg/osgi/framework/ServiceReference".equals(sigs[index])) { + if (sufficientGenerics(index++, sigLength, sig)) { + fieldCollectionType = FieldCollectionType.reference; + } + } else if ("Lorg/osgi/service/component/ComponentServiceObjects".equals(sigs[index])) { + if (sufficientGenerics(index++, sigLength, sig)) { + fieldCollectionType = FieldCollectionType.serviceobjects; + } + } else if ("Ljava/util/Map".equals(sigs[index])) { + if (sufficientGenerics(index++, sigLength, sig)) { + fieldCollectionType = FieldCollectionType.properties; + } + } else if ("Ljava/util/Map$Entry".equals(sigs[index]) && sufficientGenerics(index++ + 5, sigLength, sig)) { + if ("Ljava/util/Map".equals(sigs[index++]) && "Ljava/lang/String".equals(sigs[index++])) { + if ("Ljava/lang/Object".equals(sigs[index]) || "+Ljava/lang/Object".equals(sigs[index])) { + fieldCollectionType = FieldCollectionType.tuple; + index += 3; // ;>; + } else if ("*".equals(sigs[index])) { + fieldCollectionType = FieldCollectionType.tuple; + index += 2; // >; + } else { + index = sigLength;// no idea what service might + // be. + } + } + } else { + fieldCollectionType = FieldCollectionType.service; + } + } + if (isCollection) { + // def.fieldCollectionType = fieldCollectionType; + } + String annoService = null; + if (annoService == null && index < sigs.length) { + annoService = sigs[index].substring(1).replace('/', '.'); + } + return annoService; + } + + private static boolean sufficientGenerics(int index, int sigLength, String sig) { + if (index + 1 > sigLength) { + // analyzer.error( + // "In component %s, method %s, signature: %s does not have sufficient generic + // type information", + // component.effectiveName(), def.name, sig); + return false; + } + return true; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/JsonWriter.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/JsonWriter.java new file mode 100644 index 00000000000..0733c929984 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/JsonWriter.java @@ -0,0 +1,131 @@ +/* + * 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.dm.annotation.plugin.bnd; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.Function; + +/** + * Class used to serialize a map into the json format. + * Code adapted from Apache Felix Converter (org.apache.felix.serializer.impl.json.JsonSerializingImpl) + * + * The JSON output is parsed into an object structure in the following way: + *
        + *
      • Object names are represented as a {@link String}. + *
      • String values are represented as a {@link String}. + *
      • Numeric values without a decimal separator are represented as a {@link Long}. + *
      • Numeric values with a decimal separator are represented as a {@link Double}. + *
      • Boolean values are represented as a {@link Boolean}. + *
      • Nested JSON objects are parsed into a {@link java.util.Map Map<String, Object>}. + *
      • JSON lists are parsed into a {@link java.util.List} which may contain any of the above values. + *
      + */ +public class JsonWriter { + private final Object object; + private final boolean ignoreNull; + private final Function converter; + + JsonWriter(Object object) { + this(object, null, false); + } + + JsonWriter(Object object, Function converter, boolean ignoreNull) { + this.object = object; + this.converter = converter; + this.ignoreNull = ignoreNull; + } + + @Override + public String toString() { + return encode(object); + } + + @SuppressWarnings("rawtypes") + private String encode(Object obj) { + if (obj == null) { + return ignoreNull ? "" : "null"; + } + + if (obj instanceof Map) { + return encodeMap((Map) obj); + } else if (obj instanceof Collection) { + return encodeCollection((Collection) obj); + } else if (obj.getClass().isArray()) { + return encodeCollection(asCollection(obj)); + } else if (obj instanceof Number) { + return obj.toString(); + } else if (obj instanceof Boolean) { + return obj.toString(); + } + + String result = (converter != null) ? converter.apply(obj) : obj.toString(); + return "\"" + result + "\""; + } + + private Collection asCollection(Object arr) { + // Arrays.asList() doesn't work for primitive arrays + int len = Array.getLength(arr); + List l = new ArrayList<>(len); + for (int i=0; i collection) { + StringBuilder sb = new StringBuilder("["); + + boolean first = true; + for (Object o : collection) { + if (first) + first = false; + else + sb.append(','); + + sb.append(encode(o)); + } + + sb.append("]"); + return sb.toString(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private String encodeMap(Map m) { + StringBuilder sb = new StringBuilder("{"); + for (Entry entry : (Set) m.entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) + if (ignoreNull) + continue; + + if (sb.length() > 1) + sb.append(','); + sb.append('"'); + sb.append(entry.getKey().toString()); + sb.append("\":"); + sb.append(encode(entry.getValue())); + } + sb.append("}"); + + return sb.toString(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/Logger.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/Logger.java new file mode 100644 index 00000000000..bd16bf1c74e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/Logger.java @@ -0,0 +1,40 @@ +/* + * 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.dm.annotation.plugin.bnd; + +/** + * Base class for our logger. Under Maven, we log into the Maven logger. Under bnd, we log into the Bnd logger. + * + * @author Felix Project Team + */ +public abstract class Logger +{ + /** + * Log Levels. + */ + enum Level { + Error, Warn, Info, Debug + } + + public abstract void error(String msg, Object ... args); + public abstract void error(String msg, Throwable err, Object ... args); + public abstract void warn(String msg , Object ... args); + public abstract void info(String msg , Object ... args); + public abstract void debug(String msg, Object ... args); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/Patterns.java b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/Patterns.java new file mode 100644 index 00000000000..3535d16b620 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.annotation/src/org/apache/felix/dm/annotation/plugin/bnd/Patterns.java @@ -0,0 +1,212 @@ +/* + * 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.dm.annotation.plugin.bnd; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Class containings pattern matching helper methods. + * + * @author Felix Project Team + */ +public class Patterns +{ + // Pattern used to check if a method is void and does not take any params + public final static Pattern VOID = Pattern.compile("\\(\\)V"); + + // Pattern used to check if a method returns an array of Objects + public final static Pattern COMPOSITION = Pattern.compile("\\(\\)\\[Ljava/lang/Object;"); + + // Pattern used to parse service type from "bind(Component, ServiceReference, Service)" signature + public final static Pattern BIND_CLASS1 = Pattern.compile("\\((Lorg/apache/felix/dm/Component;)(Lorg/osgi/framework/ServiceReference;)L([^;]+);\\)V"); + + // Pattern used to parse service type from "bind(Component, Service)" signature + public final static Pattern BIND_CLASS2 = Pattern.compile("\\((Lorg/apache/felix/dm/Component;)L([^;]+);\\)V"); + + // Pattern used to parse service type from "bind(Component, Map, Service)" signature + public final static Pattern BIND_CLASS3 = Pattern.compile("\\((Lorg/apache/felix/dm/Component;)(Ljava/util/Map;)L([^;]+);\\)V"); + + // Pattern used to parse service type from "bind(ServiceReference, Service)" signature + public final static Pattern BIND_CLASS4 = Pattern.compile("\\((Lorg/osgi/framework/ServiceReference;)L([^;]+);\\)V"); + + // Pattern used to parse service type from "bind(Service)" signature + public final static Pattern BIND_CLASS5 = Pattern.compile("\\(L([^;]+);\\)V"); + + // Pattern used to parse service type from "bind(Service, Map)" signature + public final static Pattern BIND_CLASS6 = Pattern.compile("\\(L([^;]+);(Ljava/util/Map;)\\)V"); + + // Pattern used to parse service type from "bind(Map, Service)" signature + public final static Pattern BIND_CLASS7 = Pattern.compile("\\((Ljava/util/Map;)L([^;]+);\\)V"); + + // Pattern used to parse service type from "bind(Service, Dictionary)" signature + public final static Pattern BIND_CLASS8 = Pattern.compile("\\(L([^;]+);(Ljava/util/Dictionary;)\\)V"); + + // Pattern used to parse service type from "bind(Dictionary, Service)" signature + public final static Pattern BIND_CLASS9 = Pattern.compile("\\((Ljava/util/Dictionary;)L([^;]+);\\)V"); + + // Pattern used to parse classes from class descriptors; + public final static Pattern CLASS = Pattern.compile("L([^;]+);"); + + // Pattern used to parse the field on which a Publisher annotation may be applied on + public final static Pattern RUNNABLE = Pattern.compile("Ljava/lang/Runnable;"); + + // Pattern used to parse a field whose type is BundleContext + public final static Pattern BUNDLE_CONTEXT = Pattern.compile("Lorg/osgi/framework/BundleContext;"); + + // Pattern used to parse a field whose type is BundleContext + public final static Pattern BUNDLE = Pattern.compile("Lorg/osgi/framework/Bundle;"); + + // Pattern used to parse a field whose type is DependencyManager + public final static Pattern DEPENDENCY_MANAGER = Pattern.compile("Lorg/apache/felix/dm/DependencyManager;"); + + // Pattern used to parse a field whose type is Component + public final static Pattern COMPONENT = Pattern.compile("Lorg/apache/felix/dm/Component;"); + + // Pattern used to parse a field whose type is ServiceRegistration + public final static Pattern SERVICE_REGISTRATION = Pattern.compile("Lorg/osgi/framework/ServiceRegistration;"); + + // Pattern used to check if a method returns a Map + public final static Pattern METHOD_RETURN_MAP = Pattern.compile("\\(\\)Ljava/util/Map;"); + + // Pattern to detect a configuration updated callback without any config types, like for instance: + // updated(Dictionary) + // updated(Component, Dictionary) + public final static Pattern UPDATED_NO_CONFIG_TYPES = Pattern.compile + ("\\(((Ljava/util/Dictionary;)|(Lorg/apache/felix/dm/Component;Ljava/util/Dictionary;))\\)V"); + + // Pattern to detect a configuration updated callback with some config types, like for instance: + // updated(ConfigType1, ConfigType2, ...) + // updated(Dictionary, ConfigType1, ConfigType2, ...) + // updated(Component, Dictionary, ConfigType1, ConfigType2, ...) + public final static Pattern UPDATED_CONFIG_TYPES = Pattern.compile + ("((Ljava/util/Dictionary;)|(Lorg/apache/felix/dm/Component;)|(L([^;]+);))"); + + // Pattern to detect a service dependency type from a method which accepts as argument + // a ServiceReference or a ServiceObject + static final Pattern GENERIC_TYPES = Pattern.compile( + "\\(((Lorg/osgi/framework/ServiceReference;)|(Lorg/osgi/framework/ServiceObjects;))+\\)(V)"); + + /** + * Infer service dependency type from a bind method accepting a ServiceReference or a ServiceObject parameter + * @param m_logger + */ + public static String inferTypeFromGenericType(String methodDescriptor, String signature, Logger m_logger) { + String inferredService = null; + String plainType = null; + Matcher m = GENERIC_TYPES.matcher(methodDescriptor); + + if (m.matches()) { + if (m.group(2) != null) { + plainType = "Lorg/osgi/framework/ServiceReference<"; + } else if (m.group(3) != null) { + plainType = "Lorg/osgi/framework/ServiceObjects<"; + } + } + + if (inferredService == null && signature != null && plainType != null) { + int start = signature.indexOf(plainType); + if (start > -1) { + start += plainType.length(); + String[] sigs = signature.substring(start).split("[<;>]"); + if (sigs.length > 0) { + String sig = sigs[0]; + if (sig.startsWith("-")) { + inferredService = Object.class.getName(); + } else { + int index = sig.startsWith("+") ? 2 : 1; + inferredService = sig.substring(index).replace('/', '.'); + } + } + } + } + + m_logger.debug("inferTypeFromGenericType: methodDescriptor=%s, signature=%s, plainType=%s, inferred service=%s", + methodDescriptor, signature, plainType, inferredService); + + return inferredService; + } + + /** + * Parses a class. + * @param clazz the class to be parsed (the package is "/" separated). + * @param pattern the pattern used to match the class. + * @param group the pattern group index where the class can be retrieved. + * @return the parsed class. + */ + public static String parseClass(String clazz, Pattern pattern, int group) + { + return parseClass(clazz, pattern, group, true); + } + + /** + * Parses a class. + * @param clazz the class to be parsed (the package is "/" separated). + * @param pattern the pattern used to match the class. + * @param group the pattern group index where the class can be retrieved. + * @param throwException true if an Exception must be thrown in case the clazz does not match the pattern. + * @return the parsed class. + */ + public static String parseClass(String clazz, Pattern pattern, int group, boolean throwException) + { + Matcher matcher = pattern.matcher(clazz); + if (matcher.matches()) + { + return matcher.group(group).replace("/", "."); + } + else if (throwException) + { + throw new IllegalArgumentException("Invalid class descriptor: " + clazz); + } else { + return null; + } + } + + /** + * Checks if a method descriptor matches a given pattern. + * @param the method whose signature descriptor is checked + * @param pattern the pattern used to check the method signature descriptor + * @throws IllegalArgumentException if the method signature descriptor does not match the given pattern. + */ + public static void parseMethod(String method, String descriptor, Pattern pattern) + { + Matcher matcher = pattern.matcher(descriptor); + if (!matcher.matches()) + { + throw new IllegalArgumentException("Invalid method " + method + ", wrong signature: " + + descriptor); + } + } + + /** + * Checks if a field descriptor matches a given pattern. + * @param field the field whose type descriptor is checked + * @param descriptor the field descriptor to be checked + * @param pattern the pattern to use + * @throws IllegalArgumentException if the method signature descriptor does not match the given pattern. + */ + public static void parseField(String field, String descriptor, Pattern pattern) { + Matcher matcher = pattern.matcher(descriptor); + if (!matcher.matches()) + { + throw new IllegalArgumentException("Invalid field " + field + ", wrong signature: " + + descriptor); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.annotation/test/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.annotation/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/.classpath b/dependencymanager/org.apache.felix.dependencymanager.benchmark/.classpath new file mode 100644 index 00000000000..7a6fc254361 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.benchmark/.gitignore new file mode 100644 index 00000000000..90dde36e4ac --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/.gitignore @@ -0,0 +1,3 @@ +/bin/ +/bin_test/ +/generated/ diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/.project b/dependencymanager/org.apache.felix.dependencymanager.benchmark/.project new file mode 100644 index 00000000000..e601a910ba3 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/.project @@ -0,0 +1,23 @@ + + + org.apache.felix.dependencymanager.benchmark + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/README b/dependencymanager/org.apache.felix.dependencymanager.benchmark/README new file mode 100644 index 00000000000..932e6a6bcb0 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/README @@ -0,0 +1,182 @@ +/* + * 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. + */ + +Stress test for Dependency Manager +================================== + +This module provides a little framework used to perform some stress tests on Dependency Manager. +(but other DI frameworks can easily be integrated). + +What is doing this test ? +========================= + +This loader measures the time nedded to create a graph of service components that have dependencies +between each other. For sake of simplicity, a simple scenario domain is used (actually, this example +domain has been inspired from the "Java8 Lambdas" book, O'reilly) and the following services are used: + +- Artist service: An Artist is an individual or group of musicians, who creates some "Albums". One + Artist service depends on several Album services. + +- Album service: is a single release of musics, comprising several music Tracks. One Album depends + on several Track services. + +- Track service: A piece of music. + +The scenario consists in starting/stopping many times a bundle that will synchronously create the +graph of Artist/Album/Track components (630 by default). A scenario controller monitors the number +of created components and when the number of expected components are created, then the controller +stops the bundle, which will then unregister all components. Finally, when the controller detects +that all components are unregistered, the elapsed time is recorded in a list of time duration (in +nano seconds). + +The same is done by another bundle that does exactly the same, but using concurrent component +registration. + +At the end of the test (that is, when the bundle that creates the components has been +started/stopped many times), then the list of all time durations (start/stop) is sorted: the first +element of the list corresponds to the shortest elapsed time used by the bundle to create and destroy the +components; and the last element in the list corresponds to the slowest elapsed time. The +middle in the duration time list is the average. We display the first entry (fastest), the entry at 1/4 +of the list, the middle of the list, the entry at 3/4 of the list, and the last entry (slowest time). +We don't do an average, because usually, when running benchmark, some measurements don't reflect reality, +especially, when there is a full GC or when the JVM is warming up. (we actually do the same as in Java +Chronicle: https://github.com/peter-lawrey/Java-Chronicle). + +Bundle descriptions: +=================== + +- org.apache.felix.dm.benchmark.dependencymanager: a test bundle that creates the components synchronously + when the bundle is started. And when it is stopped, then the components are unregistered. + +- org.apache.felix.dm.benchmark.dependencymanager.parallel: same as before, but the components are + created concurrently. + +- org.apache.felix.dm.benchmark.scenario: this bundle contains the component classes that are + part of the scenario: we have an Artist service that depends on some Albums services, each Album + also depends on some music Track services. The components are bound using a special "id" service + property. + +- org.apache.felix.dm.benchmark.scenario.impl: the simple Artist/Albums/Track implementations. + +- org.apache.felix.dm.benchmark.controller: provides a ScenarioController service that is + injected in all Artist/Album/Track components. When an Artist, an Album, or a Track component is + started, it notifies the ScenerioController. Then when the controller detects that all components + are properly created, it then stops the bundle, which in turns unregisters all components. + +- org.apache.felix.dm.benchmark.controller.impl: this is the ScenarioController implementation. + +How to launch the stress test under bndtools: +============================================ + +The stress test performs two kinds of tests on DM and parallel DM: + +-noindex.bndrun: the tests is performed without using optimized DM filter indices. +-index.bndrun: the test is performed with the DM filter indices. + +To launch the stress test under BndTools, click on the noindex.bndrun file of the +"org.apache.felix.dm.benchmark" project, then click on "Run", then in "Run OSGi". + +You should see something like that in the eclipse console: + +>> -------------------------------------------------------------------------------------------------------------- +g! Starting benchmarks (each tested bundle will add/remove 630 components during bundle activation). + + [Starting benchmarks with no processing done in components start methods] + +Benchmarking bundle: org.apache.felix.dependencymanager.benchmark.dependencymanager .................... +-> results in nanos: [189,130,687 | 205,730,144 | 312,092,102 | 357,470,857 | 871,419,487] + +Benchmarking bundle: org.apache.felix.dependencymanager.benchmark.dependencymanager.parallel .................... +-> results in nanos: [85,158,366 | 103,439,337 | 122,633,515 | 157,082,407 | 284,332,202] + + [Starting benchmarks with processing done in components start methods] + +Benchmarking bundle: org.apache.felix.dependencymanager.benchmark.dependencymanager ..... +-> results in nanos: [2,748,431,149 | 2,750,475,610 | 2,756,254,193 | 2,772,447,115 | 2,774,345,245] + +Benchmarking bundle: org.apache.felix.dependencymanager.benchmark.dependencymanager.parallel ..... +-> results in nanos: [687,259,058 | 696,725,568 | 700,220,615 | 704,310,739 | 740,325,481] +----------------------------------------------------------------------------------------------------------------- + +You can also run the same test using optimized DM filter indices. +To do so, run "index.bndrun". But using DM filter indices has a CPU cost and are useful if you have many service dependencies. +To test filter indices, first increase the number of components created/removed during bundle +startup. To do so, edit the Artist.java and change the "Artists" from 30 to 300. + +You should then observe some significant performance improvements: + +for example, with Artist.ARTISTS=300, you should observe the following: + +noindex.bndrun (no filter indices used): + +>> -------------------------------------------------------------------------------------------------------------- + g! Starting benchmarks (each tested bundle will add/remove 6300 components during bundle activation). + + [Starting benchmarks with no processing done in components start methods] + +Benchmarking bundle: org.apache.felix.dependencymanager.benchmark.dependencymanager ..... +-> results in nanos: [17,436,869,644 | 17,525,534,346 | 18,080,624,001 | 18,246,597,908 | 20,715,696,669] + +Benchmarking bundle: org.apache.felix.dependencymanager.benchmark.dependencymanager.parallel ..... +-> results in nanos: [9,660,520,501 | 9,810,057,488 | 9,870,295,166 | 10,014,334,906 | 10,628,193,815] + + [Starting benchmarks with processing done in components start methods] + +Benchmarking bundle: org.apache.felix.dependencymanager.benchmark.dependencymanager ..... +-> results in nanos: [42,700,651,438 | 43,207,156,615 | 43,653,372,523 | 43,869,438,994 | 44,715,701,457] + +Benchmarking bundle: org.apache.felix.dependencymanager.benchmark.dependencymanager.parallel ..... +-> results in nanos: [15,021,876,153 | 15,091,340,552 | 15,202,305,936 | 15,248,728,826 | 15,398,221,492] +----------------------------------------------------------------------------------------------------------------- + +and with index.bndrun (using DM filter indices): + +>> -------------------------------------------------------------------------------------------------------------- +g! Starting benchmarks (each tested bundle will add/remove 6300 components during bundle activation). + + [Starting benchmarks with no processing done in components start methods] + +Benchmarking bundle: org.apache.felix.dependencymanager.benchmark.dependencymanager ..... +-> results in nanos: [3,142,869,517 | 3,564,970,695 | 4,023,603,870 | 6,206,640,362 | 6,918,113,818] + +Benchmarking bundle: org.apache.felix.dependencymanager.benchmark.dependencymanager.parallel ..... +-> results in nanos: [2,868,554,914 | 2,873,491,201 | 2,897,439,973 | 2,913,317,331 | 3,890,123,728] + + [Starting benchmarks with processing done in components start methods] + +Benchmarking bundle: org.apache.felix.dependencymanager.benchmark.dependencymanager ..... +-> results in nanos: [28,515,623,505 | 28,558,774,886 | 28,661,315,061 | 28,808,682,302 | 28,915,519,208] + +Benchmarking bundle: org.apache.felix.dependencymanager.benchmark.dependencymanager.parallel ..... +-> results in nanos: [7,702,400,991 | 7,749,145,806 | 7,760,650,323 | 7,832,386,237 | 7,854,739,136] +----------------------------------------------------------------------------------------------------------------- + + +How to interpret results: +======================== + +for each tested bundle, the time spent is displayed in nanos. +for example: + + -> results in nanos: [85,158,366 | 103,439,337 | 122,633,515 | 157,082,407 | 284,332,202] + +Here, the shortest time used to activate and bind the components with each other took around 85,158,366 nanos. +the value in the midle of the list represents the averate time (122,633,515 nanos. +the last entry is the slowest elapsed time (284,332,202). + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/bnd.bnd b/dependencymanager/org.apache.felix.dependencymanager.benchmark/bnd.bnd new file mode 100644 index 00000000000..57c6346496a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/bnd.bnd @@ -0,0 +1,32 @@ +# +# 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. +# +javac.source: 1.8 +javac.target: 1.8 +Bundle-Version: 1.0.0 +-buildpath: \ + org.apache.felix.dependencymanager;version=latest,\ + osgi.core;version=6.0,\ + osgi.cmpn;version=6.0 + +-sub: \ + *.bnd +Export-Package: \ + org.apache.felix.dm.benchmark.scenario.impl + +# we do not release this project in binary distribution. +-releaserepo: +-baseline: diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/controller.bnd b/dependencymanager/org.apache.felix.dependencymanager.benchmark/controller.bnd new file mode 100644 index 00000000000..d4fcced6d87 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/controller.bnd @@ -0,0 +1,21 @@ +# +# 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. +# +Private-Package: \ + org.apache.felix.dm.benchmark.controller.impl +Bundle-Activator: org.apache.felix.dm.benchmark.controller.impl.Activator +Export-Package: \ + org.apache.felix.dm.benchmark.controller \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/dependencymanager.bnd b/dependencymanager/org.apache.felix.dependencymanager.benchmark/dependencymanager.bnd new file mode 100644 index 00000000000..8e12db11736 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/dependencymanager.bnd @@ -0,0 +1,19 @@ +# +# 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. +# +Private-Package: \ + org.apache.felix.dm.benchmark.dependencymanager +Bundle-Activator: org.apache.felix.dm.benchmark.dependencymanager.Activator \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/dependencymanager.parallel.bnd b/dependencymanager/org.apache.felix.dependencymanager.benchmark/dependencymanager.parallel.bnd new file mode 100644 index 00000000000..b6ce3b27bcd --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/dependencymanager.parallel.bnd @@ -0,0 +1,19 @@ +# +# 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. +# +Bundle-Activator: org.apache.felix.dm.benchmark.dependencymanager.ParallelActivator +Private-Package: \ + org.apache.felix.dm.benchmark.dependencymanager \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/index.bndrun b/dependencymanager/org.apache.felix.dependencymanager.benchmark/index.bndrun new file mode 100644 index 00000000000..fb581164f48 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/index.bndrun @@ -0,0 +1,37 @@ +# +# 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. +# +-runbundles: \ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.metatype;version=1.0.10,\ + org.apache.felix.log;version=1.0.1,\ + ${gogo},\ + org.apache.felix.configadmin;version=1.8.8,\ + org.apache.felix.dependencymanager.benchmark.scenario;version=latest,\ + org.apache.felix.dependencymanager.benchmark.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.benchmark.dependencymanager.parallel;version=latest,\ + org.apache.felix.dependencymanager.benchmark.controller;version=latest + +-runfw: ${felix.framework} + +-runproperties: \ + ds.loglevel=warn,\ + org.osgi.framework.bootdelegation='sun.*,com.sun.*,org.netbeans.*',\ + org.apache.felix.dependencymanager.filterindex='objectClass,id' +-runvm: -server -Xmx1024m -Xms1024m +javac.source: 1.8 +javac.target: 1.8 diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/noindex.bndrun b/dependencymanager/org.apache.felix.dependencymanager.benchmark/noindex.bndrun new file mode 100644 index 00000000000..b4a1eb0c159 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/noindex.bndrun @@ -0,0 +1,37 @@ +# +# 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. +# +-runbundles: \ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.metatype;version=1.0.10,\ + org.apache.felix.log;version=1.0.1,\ + ${gogo},\ + org.apache.felix.configadmin;version=1.8.8,\ + org.apache.felix.dependencymanager.benchmark.scenario;version=latest,\ + org.apache.felix.dependencymanager.benchmark.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.benchmark.dependencymanager.parallel;version=latest,\ + org.apache.felix.dependencymanager.benchmark.controller;version=latest + +-runfw: ${felix.framework} + +-runproperties: \ + ds.loglevel=warn,\ + org.osgi.framework.bootdelegation='sun.*,com.sun.*,org.netbeans.*' +-runvm: -server -Xmx1024m -Xms1024m +-runee: JavaSE-1.8 +javac.source: 1.8 +javac.target: 1.8 diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/scenario.bnd b/dependencymanager/org.apache.felix.dependencymanager.benchmark/scenario.bnd new file mode 100644 index 00000000000..8ede3fc6f77 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/scenario.bnd @@ -0,0 +1,19 @@ +# +# 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. +# +Export-Package: \ + org.apache.felix.dm.benchmark.scenario.impl,\ + org.apache.felix.dm.benchmark.scenario \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/controller/ScenarioController.java b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/controller/ScenarioController.java new file mode 100644 index 00000000000..bca5cf591d4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/controller/ScenarioController.java @@ -0,0 +1,64 @@ +/* + * 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.dm.benchmark.controller; + +import org.apache.felix.dm.benchmark.scenario.Album; +import org.apache.felix.dm.benchmark.scenario.Artist; +import org.apache.felix.dm.benchmark.scenario.Track; + +/** + * This service is injected in each scenario bundle. All scenario bundle components must depend on this + * service, and must invoke the xxAdded() method once the component is fully initialized, and + * the xxRemoved() method when the component is stopped. + * This benchmark expect scenario bundles to register some "Artists" components. Each "Artist" component is + * then expected to depend on many "Albums", and each "Album" then depends on many music "Tracks". + * + * @author Felix Project Team + */ +public interface ScenarioController { + /** + * An Artist is added (service is started) + */ + void artistAdded(Artist artist); + + /** + * An Artist is removed (service is stopped) + */ + void artistRemoved(Artist artist); + + /** + * An Album is added (service is started) + */ + void albumAdded(Album artist); + + /** + * An Album is removed (service is stopped) + */ + void albumRemoved(Album artist); + + /** + * A Music Track is added (service is started) + */ + void trackAdded(Track artist); + + /** + * A Music Track is removed (service is stopped) + */ + void trackRemoved(Track artist); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/controller/impl/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/controller/impl/Activator.java new file mode 100644 index 00000000000..d2e4f9a036b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/controller/impl/Activator.java @@ -0,0 +1,36 @@ +/* + * 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.dm.benchmark.controller.impl; + +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; + +/** + * This activator triggers the scenario controller thread, which will do some microbenchmarks for a given + * set of scenario bundles. The controller thread is fired only once the framework is started. + * + * @author Felix Project Team + */ +public class Activator extends DependencyActivatorBase { + @Override + public void init(BundleContext context, DependencyManager m) throws Exception { + m.add(createComponent().setImplementation(ScenarioControllerImpl.class)); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/controller/impl/ScenarioControllerImpl.java b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/controller/impl/ScenarioControllerImpl.java new file mode 100644 index 00000000000..29bb32ec717 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/controller/impl/ScenarioControllerImpl.java @@ -0,0 +1,313 @@ +/* + * 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.dm.benchmark.controller.impl; + +import static java.lang.System.out; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; +import static org.apache.felix.dm.benchmark.scenario.Artist.ALBUMS; +import static org.apache.felix.dm.benchmark.scenario.Artist.ARTISTS; +import static org.apache.felix.dm.benchmark.scenario.Artist.TRACKS; +import static org.apache.felix.dm.benchmark.scenario.Helper.debug; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.LongStream; +import java.util.stream.Stream; + +import org.apache.felix.dm.benchmark.controller.ScenarioController; +import org.apache.felix.dm.benchmark.scenario.Album; +import org.apache.felix.dm.benchmark.scenario.Artist; +import org.apache.felix.dm.benchmark.scenario.Helper; +import org.apache.felix.dm.benchmark.scenario.Track; +import org.apache.felix.dm.benchmark.scenario.Unchecked; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; + +/** + * The controller which perform microbenchmarks on some scenario bundles. + * + * @author Felix Project Team + */ +public class ScenarioControllerImpl implements Runnable, ScenarioController { + /** + * List of bundles to be executed by the benchmark. + */ + final List TESTS = Arrays.asList( + "org.apache.felix.dependencymanager.benchmark.dependencymanager", + "org.apache.felix.dependencymanager.benchmark.dependencymanager.parallel" + ); + + /** + * Our injected bundle context, used to lookup the bundles to benchmark. + */ + private volatile BundleContext m_bctx; + + /** + * Latches used to detect when expected services are registered, or unregistered. + */ + private volatile CountDownLatch m_startLatch, m_stopLatch; + + /** + * When a component is called in its start or stop method, we'll perform some processing if the following + * attribute is true. + */ + private volatile boolean m_doProcessingInStartStop; + + /** + * Our component is starting: we'll first stop all bundles participating in the benchmark, then we'll + * fire a thread, and from that thread we'll iterate on all bundles in order to do a benchmark on each. + * (we'll call start/stop N times, and will display the elapsed times for each bundle). + */ + void start() { + new Thread(this).start(); + } + + void stop() { + } + + @Override + public void run() { + // wait a bit in order to let the gogo banner be displayed before we start the bench. + Unchecked.run(() -> Thread.sleep(500)); + + out.println("Starting benchmarks (each tested bundle will add/remove " + (ARTISTS + (ARTISTS * (ALBUMS + (ALBUMS * TRACKS)))) + + " components during bundle activation)."); + + // Stop all tested bundles. + forEachScenarioBundle(TESTS, Unchecked.consumer(bundle -> { + debug(() -> "Stopping bundle " + bundle.getSymbolicName()); + bundle.stop(); + })); + + // Register our controller service + m_bctx.registerService(ScenarioController.class.getName(), this, null); + + // Start/stop several times the tested bundles. (no processing done in components start methods). + m_doProcessingInStartStop = false; + out.println("\n\t[Starting benchmarks with no processing done in components start methods]"); + startStopScenarioBundles(TESTS, 50); + + // Start/stop several times the tested bundles (processing is done in components start methods). + m_doProcessingInStartStop = true; + out.println("\n\t[Starting benchmarks with processing done in components start methods]"); + startStopScenarioBundles(TESTS, 5); + } + + @Override + public void artistAdded(Artist artist) { + int size = artist.getAlbums().size(); + if (size != Artist.ALBUMS) { + throw new IllegalStateException("Artist has not created expected number of albums:" + size); + } + artist.play(); + componentAdded(); + Helper.debug(() -> "Artist added : " + artist); + } + + @Override + public void artistRemoved(Artist artist) { + componentRemoved(); + Helper.debug(() -> "Artist removed : " + artist); + } + + @Override + public void albumAdded(Album album) { + int size = album.getMusicTracks().size(); + if (size != Artist.TRACKS) { + throw new IllegalStateException("Album does not contain expected number of music tracks:" + size); + } + componentAdded(); + Helper.debug(() -> "Album added : " + album); + } + + @Override + public void albumRemoved(Album album) { + componentRemoved(); + Helper.debug(() -> "Album removed : " + album); + } + + @Override + public void trackAdded(Track track) { + componentAdded(); + Helper.debug(() -> "Track added : " + track); + } + + @Override + public void trackRemoved(Track track) { + componentRemoved(); + Helper.debug(() -> "Track removed : " + track); + } + + // ------------------- Private methods ----------------------------------------------------- + + private void startStopScenarioBundles(List tests, int iterations) { + forEachScenarioBundle(tests, bundle -> { + out.print("\nBenchmarking bundle: " + bundle.getSymbolicName() + " "); + List sortedResults = LongStream.range(0, iterations) + .peek(i -> out.print(".")) + .map(n -> durationOf(() -> start(bundle))) + .peek(n -> stop(bundle)) + .sorted().boxed().collect(toList()); + out.println(); + displaySortedResults(sortedResults); + Unchecked.run(() -> Thread.sleep(500)); + }); + } + + /** + * Displays meaningful values in the sorted results (first=fastest, midle=average, last entry=slowest) + * @param sortedResults + */ + private void displaySortedResults(List sortedResults) { + // We don't display an average of the duration times; Instead, we sort the results, + // and we display the significant results (the first entry is the fastest, the middle entry is the + // average, the last entry is the slowest ...) + out.printf("-> results in nanos: [%s]%n", + Stream.of(0f, 24.99f, 49.99f, 74.99f, 99.99f) + .mapToInt(perc -> (int) (perc * sortedResults.size() / 100)) + .mapToObj(sortedResults::get) + .map(this::formatNano) + .collect(joining(" | "))); + } + + /** + * Displays a nanosecond value using thousands separator. + * Example: 1000000 -> 1,000,000 + */ + private String formatNano(Long nanoseconds) { + DecimalFormat formatter = (DecimalFormat) NumberFormat.getInstance(Locale.US); + DecimalFormatSymbols symbols = formatter.getDecimalFormatSymbols(); + symbols.setGroupingSeparator(','); + return formatter.format(nanoseconds); + } + + private void componentAdded() { + doProcessing(); + m_startLatch.countDown(); + } + + private void componentRemoved() { + //doProcessing(); + m_stopLatch.countDown(); + } + + private void doProcessing() { + if (m_doProcessingInStartStop) { + long duration = TimeUnit.MILLISECONDS.toNanos(ThreadLocalRandom.current().nextLong(5)); + long t1 = System.nanoTime(); + while (System.nanoTime() - t1 < duration) + ; + } + } + + /** + * Maps a function to all bundles participating in the benchmark. + */ + private void forEachScenarioBundle(List tests, Consumer consumer) { + tests.stream().forEach(test -> { + Optional bundle = Stream.of(m_bctx.getBundles()).filter(b -> b.getSymbolicName().equals(test)).findFirst(); + bundle.ifPresent(consumer::accept); + }); + } + + /** + * This function does this: + * + * 1) start a bundle, and register the ScenarioController service (this will trigger all components activation) + * 2) wait for all expected components to be fully started + * + * @param b the benchmarked scenario bundle + */ + void start(Bundle b) { + try { + m_startLatch = new CountDownLatch(ARTISTS + + (ARTISTS * (ALBUMS + (ALBUMS * TRACKS)))); + + debug(() -> "starting bundle " + b.getSymbolicName()); + b.start(); + + if (! m_startLatch.await(60, TimeUnit.SECONDS)) { + out.println("Could not start components timely: current start latch=" + m_startLatch.getCount() + ", stop latch=" + m_stopLatch.getCount()); + Unchecked.run(() -> Thread.sleep(Integer.MAX_VALUE)); // FIXME + } + + // Make sure the threadpool is quiescent and has finished to register all components + if (! Helper.getThreadPool().awaitQuiescence(5, TimeUnit.SECONDS)) { + out.println("could not start components timely (thread pool is still active after 5 seconds)"); + Unchecked.run(() -> Thread.sleep(Integer.MAX_VALUE)); // FIXME + } + } catch (Throwable t) { + t.printStackTrace(); + } + } + + /** + * This function stops the bundle and wait for all expected components to be fully stopped + * + * @param b the benchmarked scenario bundle + */ + void stop(Bundle b) { + try { + m_stopLatch = new CountDownLatch(ARTISTS + + (ARTISTS * (ALBUMS + (ALBUMS * TRACKS)))); + + debug(() -> "stopping bundle " + b.getSymbolicName()); + b.stop(); + + // Make sure the threadpool is quiescent and has finished to register all components + if (! Helper.getThreadPool().awaitQuiescence(5, TimeUnit.SECONDS)) { + out.println("could not start components timely (thread pool is still active after 5 seconds)"); + Unchecked.run(() -> Thread.sleep(Integer.MAX_VALUE)); // FIXME + } + + // Wait for all component deactivations + if (! m_stopLatch.await(60, TimeUnit.SECONDS)) { + out.println("Could not stop components timely: current start latch=" + m_startLatch.getCount() + ", stop latch=" + m_stopLatch.getCount()); + Unchecked.run(() -> Thread.sleep(Integer.MAX_VALUE)); + } + } catch (Throwable t) { + t.printStackTrace(); + } + } + + /** + * Returns the time consumed by the given runnable, ²ch is executed by this method. + */ + private long durationOf(Runnable scenario) { + long start = System.nanoTime(); + long end = 0; + try { + scenario.run(); + end = System.nanoTime(); + } catch (Throwable t) { + t.printStackTrace(); + } + return (end - start); + } +} diff --git a/org.osgi.compendium/src/main/java/org/osgi/util/measurement/packageinfo b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/controller/packageinfo similarity index 100% rename from org.osgi.compendium/src/main/java/org/osgi/util/measurement/packageinfo rename to dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/controller/packageinfo diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/dependencymanager/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/dependencymanager/Activator.java new file mode 100644 index 00000000000..740fcf2449a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/dependencymanager/Activator.java @@ -0,0 +1,39 @@ +/* + * 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.dm.benchmark.dependencymanager; + +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.benchmark.controller.ScenarioController; +import org.osgi.framework.BundleContext; + +/** + * Activator for a scenario based on Dependency Manager 4.0 + * We'll create many Artists, each one is depending on many Albums, and each Album depends on many Tracks. + * + * @author Felix Project Team + */ +public class Activator extends DependencyActivatorBase { + @Override + public void init(BundleContext context, DependencyManager dm) throws Exception { + dm.add(createComponent() + .setImplementation(Benchmark.class) + .add(createServiceDependency().setService(ScenarioController.class).setRequired(true))); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/dependencymanager/Benchmark.java b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/dependencymanager/Benchmark.java new file mode 100644 index 00000000000..28b401dcedc --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/dependencymanager/Benchmark.java @@ -0,0 +1,100 @@ +/* + * 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.dm.benchmark.dependencymanager; + +import static org.apache.felix.dm.benchmark.scenario.Artist.ALBUMS; +import static org.apache.felix.dm.benchmark.scenario.Artist.ARTISTS; +import static org.apache.felix.dm.benchmark.scenario.Artist.TRACKS; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.benchmark.controller.ScenarioController; +import org.apache.felix.dm.benchmark.scenario.Album; +import org.apache.felix.dm.benchmark.scenario.Artist; +import org.apache.felix.dm.benchmark.scenario.Helper; +import org.apache.felix.dm.benchmark.scenario.Track; +import org.apache.felix.dm.benchmark.scenario.impl.AlbumImpl; +import org.apache.felix.dm.benchmark.scenario.impl.ArtistImpl; +import org.apache.felix.dm.benchmark.scenario.impl.TrackImpl; + +/** + * @author Felix Project Team + */ +public class Benchmark { + volatile DependencyManager m_dm; + volatile ScenarioController m_controller; + final List m_components = new ArrayList<>(); + + /** + * Initialize our Artists, Albums/Tracks, possibly using a parallel dependency manager. + */ + @SuppressWarnings("unused") + private void start() { + Helper.debug(() -> "Benchmark.start"); + + IntStream.range(0, ARTISTS) + // Creates a stream of Artist components + .mapToObj(i -> createArtists(m_dm)).peek(m_components::add) + // For each artist in the stream, creates a new stream of Album components + .flatMap(artist -> createAlbums(m_dm, artist)).peek(m_components::add) + // For each Album, creates a new stream of Track components + .flatMap(album -> createTracks(m_dm, album)).forEach(m_components::add); + + m_components.stream().forEach(m_dm::add); + } + + @SuppressWarnings("unused") + private void stop() { + m_components.forEach(m_dm::remove); + } + + private Component createArtists(DependencyManager dm) { + return dm.createComponent().setInterface(Artist.class.getName(), null).setImplementation(new ArtistImpl(m_controller)); + } + + private Stream createAlbums(DependencyManager dm, Component artist) { + return IntStream.range(0, ALBUMS).mapToObj(i -> { + long id = Helper.generateId(); + String filter = "(id=" + id + ")"; + artist.add(dm.createServiceDependency().setService(Album.class, filter).setRequired(true).setCallbacks("addAlbum", null)); + + Hashtable props = new Hashtable<>(); + props.put("id", String.valueOf(id)); + return dm.createComponent().setInterface(Album.class.getName(), props).setImplementation(new AlbumImpl(m_controller)); + }); + } + + private Stream createTracks(DependencyManager dm, Component album) { + return IntStream.range(0, TRACKS).mapToObj(i -> { + long id = Helper.generateId(); + String f = "(id=" + String.valueOf(id) + ")"; + album.add(dm.createServiceDependency().setService(Track.class, f).setRequired(true).setCallbacks("addTrack", null)); + + Hashtable p = new Hashtable<>(); + p.put("id", String.valueOf(id)); + return dm.createComponent().setInterface(Track.class.getName(), p).setImplementation(new TrackImpl(m_controller)); + }); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/dependencymanager/ParallelActivator.java b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/dependencymanager/ParallelActivator.java new file mode 100644 index 00000000000..cdcb6b34d35 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/dependencymanager/ParallelActivator.java @@ -0,0 +1,44 @@ +/* + * 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.dm.benchmark.dependencymanager; + +import java.util.concurrent.Executor; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentExecutorFactory; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.benchmark.scenario.Helper; +import org.osgi.framework.BundleContext; + +/** + * Parallel version of our default Activator. + * + * @author Felix Project Team + */ +public class ParallelActivator extends Activator { + public void init(BundleContext context, DependencyManager mgr) throws Exception { + context.registerService(ComponentExecutorFactory.class.getName(), new ComponentExecutorFactory() { + @Override + public Executor getExecutorFor(Component component) { + return Helper.getThreadPool(); // Return our thread pool shared for all components + } + }, null); + super.init(context, mgr); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/Album.java b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/Album.java new file mode 100644 index 00000000000..1d6ac0c1fef --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/Album.java @@ -0,0 +1,38 @@ +/* + * 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.dm.benchmark.scenario; + +import java.util.List; + +/** + * A single release of musics, comprising some music tracks. + * + * @author Felix Project Team + */ +public interface Album { + /** + * Returns the music tracks this Album is comprising. + */ + List getMusicTracks(); + + /** + * Play all tracks from all albums. + */ + void play(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/Artist.java b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/Artist.java new file mode 100644 index 00000000000..eb78702d9be --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/Artist.java @@ -0,0 +1,56 @@ +/* + * 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.dm.benchmark.scenario; + +import java.util.List; + +/** + * An individual who creates musical Albums + * + * @author Felix Project Team + */ +public interface Artist { + /** + * When a scenario bundles starts, it creates the following number of Artists (service) + * (you have to regenerate the SCR xml descriptor if you modify this, see README) + */ + public final int ARTISTS = 30; + + /** + * Each Artist creates the following number of musical Albums. + * (you have to regenerate the SCR xml descriptor if you modify this, see README) + */ + public final int ALBUMS = 5; + + /** + * Each Album contains the following number of musical Tracks. + * (you have to regenerate the SCR xml descriptor if you modify this, see README) + */ + public final int TRACKS = 3; + + /** + * Returns the Albums that this Artist has created + */ + List getAlbums(); + + /** + * Play all tracks from all albums (this test method invocation time between services). + */ + void play(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/Helper.java b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/Helper.java new file mode 100644 index 00000000000..9ada6939c08 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/Helper.java @@ -0,0 +1,69 @@ +/* + * 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.dm.benchmark.scenario; + +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; + +/** + * Helper class containing misc functions, and constants. + * + * @author Felix Project Team + */ +public class Helper { + /** + * Activate this flag for debugging. + */ + private final static boolean DEBUG = false; + + /** + * Generator used to create unique identifiers. + */ + private final static AtomicLong m_idGenerator = new AtomicLong(); + + /** + * Threadpool which can be optionally used by parallel scenarios. + */ + private final static int CORES = Runtime.getRuntime().availableProcessors(); + private final static ForkJoinPool TPOOL = new ForkJoinPool(CORES); + + /** + * Get the threadpool, possibly needed by some scenario supporting parallel mode + */ + public static ForkJoinPool getThreadPool() { + return TPOOL; + } + + /** + * Display some debug messages. + */ + public static void debug(Supplier message) { + if (DEBUG) { + System.out.println(message.get()); + } + } + + /** + * Generates a unique id. + */ + public static long generateId() { + return m_idGenerator.incrementAndGet(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/Track.java b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/Track.java new file mode 100644 index 00000000000..0e4d4144d16 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/Track.java @@ -0,0 +1,31 @@ +/* + * 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.dm.benchmark.scenario; + +/** + * A piece of music + * + * @author Felix Project Team + */ +public interface Track { + /** + * Play this single piece of music. + */ + void play(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/Unchecked.java b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/Unchecked.java new file mode 100644 index 00000000000..c9849d9e49e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/Unchecked.java @@ -0,0 +1,103 @@ +/* + * 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.dm.benchmark.scenario; + +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Helper functions used to work around the java.util.function.* functions, which don't support + * methods throwing a checked exception. + * + * @author Felix Project Team + */ +public class Unchecked { + /** + * Same functional interface as java.util.function.Consumer, except that the accept method may throw an exception. + */ + @FunctionalInterface + public static interface CheckedConsumer { + public void accept(T t) throws Exception; + } + + /** + * Same interface as Runnable, except that the run method may throw an exception. + */ + @FunctionalInterface + public static interface CheckedRunnable { + public void run() throws Exception; + } + + /** + * Same interface as Function, except that the accept method may throw an exception. + */ + @FunctionalInterface + public static interface CheckedFunction { + public U apply(T t) throws Exception; + } + + /** + * Wraps a Consumer whose accept method may throw an exception behind a regular java.util.function.Consumer + */ + public static Consumer consumer(CheckedConsumer c) { + return (t) -> { + try { + c.accept(t); + } + catch (Exception e) { + throw new RuntimeException(e); + } + catch (Throwable err) { + throw err; + } + }; + } + + /** + * Wraps a Consumer whose accept method may throw an exception behind a regular java.util.function.Consumer + */ + public static Function func(CheckedFunction f) { + return (t) -> { + try { + return f.apply(t); + } + catch (Exception e) { + throw new RuntimeException(e); + } + catch (Throwable err) { + throw err; + } + }; + } + + /** + * Runs a runnable which may throw an exception without having to catch it. + */ + public static void run(CheckedRunnable r) { + try { + r.run(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + catch (Throwable err) { + throw err; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/impl/AlbumImpl.java b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/impl/AlbumImpl.java new file mode 100644 index 00000000000..1de8bb7234c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/impl/AlbumImpl.java @@ -0,0 +1,64 @@ +/* + * 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.dm.benchmark.scenario.impl; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.felix.dm.benchmark.controller.ScenarioController; +import org.apache.felix.dm.benchmark.scenario.Album; +import org.apache.felix.dm.benchmark.scenario.Track; + +/** + * An album comprising several music tracks. + * + * @author Felix Project Team + */ +public class AlbumImpl implements Album { + final List m_musicTracks = new ArrayList<>(); + final ScenarioController m_controller; + + public AlbumImpl(ScenarioController controller) { + m_controller = controller; + } + + void addTrack(Track dep) { + m_musicTracks.add(dep); + } + + void start() { + m_controller.albumAdded(this); + } + + void stop() { + m_controller.albumRemoved(this); + } + + @Override + public List getMusicTracks() { + return m_musicTracks; + } + + @Override + public void play() { + for (Track track : m_musicTracks) { + track.play(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/impl/ArtistImpl.java b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/impl/ArtistImpl.java new file mode 100644 index 00000000000..2bfbc28f96b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/impl/ArtistImpl.java @@ -0,0 +1,63 @@ +/* + * 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.dm.benchmark.scenario.impl; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.felix.dm.benchmark.controller.ScenarioController; +import org.apache.felix.dm.benchmark.scenario.Album; +import org.apache.felix.dm.benchmark.scenario.Artist; + +/** + * One artist who depends on multiple Albums. + * + * @author Felix Project Team + */ +public class ArtistImpl implements Artist { + final List m_albums = new ArrayList<>(); + final ScenarioController m_controller; + + public ArtistImpl(ScenarioController controller) { + m_controller = controller; + } + + void addAlbum(Album dep) { + m_albums.add(dep); + } + + void start() { + m_controller.artistAdded(this); + } + + void stop() { + m_controller.artistRemoved(this); + } + + @Override + public List getAlbums() { + return m_albums; + } + + public void play() { + for (Album album : m_albums) { + album.play(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/impl/TrackImpl.java b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/impl/TrackImpl.java new file mode 100644 index 00000000000..3115b3be674 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/impl/TrackImpl.java @@ -0,0 +1,47 @@ +/* + * 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.dm.benchmark.scenario.impl; + +import org.apache.felix.dm.benchmark.controller.ScenarioController; +import org.apache.felix.dm.benchmark.scenario.Track; + +/** + * One single music. + * + * @author Felix Project Team + */ +public class TrackImpl implements Track { + final ScenarioController m_controller; + + public TrackImpl(ScenarioController controller) { + m_controller = controller; + } + + void start() { + m_controller.trackAdded(this); + } + + void stop() { + m_controller.trackRemoved(this); + } + + @Override + public void play() { + } +} diff --git a/org.osgi.compendium/src/main/java/org/osgi/util/position/packageinfo b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/impl/packageinfo similarity index 100% rename from org.osgi.compendium/src/main/java/org/osgi/util/position/packageinfo rename to dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/impl/packageinfo diff --git a/org.osgi.compendium/src/main/java/org/osgi/util/xml/packageinfo b/dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/packageinfo similarity index 100% rename from org.osgi.compendium/src/main/java/org/osgi/util/xml/packageinfo rename to dependencymanager/org.apache.felix.dependencymanager.benchmark/src/org/apache/felix/dm/benchmark/scenario/packageinfo diff --git a/dependencymanager/org.apache.felix.dependencymanager.benchmark/test/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.benchmark/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/org.apache.felix.dependencymanager.index.itest/.classpath b/dependencymanager/org.apache.felix.dependencymanager.index.itest/.classpath new file mode 100644 index 00000000000..7a6fc254361 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.index.itest/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.index.itest/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.index.itest/.gitignore new file mode 100644 index 00000000000..90dde36e4ac --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.index.itest/.gitignore @@ -0,0 +1,3 @@ +/bin/ +/bin_test/ +/generated/ diff --git a/dependencymanager/org.apache.felix.dependencymanager.index.itest/.project b/dependencymanager/org.apache.felix.dependencymanager.index.itest/.project new file mode 100644 index 00000000000..e3699c37399 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.index.itest/.project @@ -0,0 +1,23 @@ + + + org.apache.felix.dependencymanager.index.itest + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.index.itest/bnd.bnd b/dependencymanager/org.apache.felix.dependencymanager.index.itest/bnd.bnd new file mode 100644 index 00000000000..48b9256ed0c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.index.itest/bnd.bnd @@ -0,0 +1,54 @@ +# +# 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. +# +-runbundles: \ + org.apache.servicemix.bundles.junit;version=4.12,\ + org.apache.felix.metatype;version=1.1.2,\ + org.apache.felix.log;version=1.0.1,\ + org.apache.felix.configadmin;version=1.8.8,\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.dependencymanager.index.itest.dynamic.customindex;version=latest,\ + org.apache.felix.dependencymanager.index.itest.static.customindex;version=latest,\ + org.apache.felix.dependencymanager.index.itest.tests;version=latest,\ + org.apache.felix.gogo.runtime;version=1.0.6 + +-runee: JavaSE-1.8 +-runvm: -ea +-runfw: org.apache.felix.framework;version='[5.6.10,5.6.10]' +-buildpath: \ + osgi.core;version=6.0,\ + osgi.cmpn;version=6.0,\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + ${junit},\ + org.apache.felix.gogo.runtime;version=1.0 +-runsystempackages: \ + sun.reflect +Test-Cases: \ + ${classes;CONCRETE;EXTENDS;junit.framework.TestCase} +-runproperties: \ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true,\ + gosh.args=--noshutdown,\ + org.apache.felix.dependencymanager.filterindex=objectClass + +# we do not release this project in binary distribution. +-releaserepo: +-baseline: + +-sub: *.bnd diff --git a/dependencymanager/org.apache.felix.dependencymanager.index.itest/dynamic.customindex.bnd b/dependencymanager/org.apache.felix.dependencymanager.index.itest/dynamic.customindex.bnd new file mode 100644 index 00000000000..099e5092471 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.index.itest/dynamic.customindex.bnd @@ -0,0 +1,24 @@ +# +# 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. +# +Export-Package: \ + org.apache.felix.dm.index.itest.dynamiccustomindex +Private-Package: \ + org.apache.felix.dm.impl.index.multiproperty +Import-Package: * +Bundle-Category: test +Bundle-License: http://www.apache.org/licenses/LICENSE-2.0.txt +Bundle-Vendor: The Apache Software Foundation diff --git a/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/dynamiccustomindex/DynamicCustomFilterIndex.java b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/dynamiccustomindex/DynamicCustomFilterIndex.java new file mode 100644 index 00000000000..9d6148055de --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/dynamiccustomindex/DynamicCustomFilterIndex.java @@ -0,0 +1,54 @@ +/* + * 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.dm.index.itest.dynamiccustomindex; + +import org.apache.felix.dm.impl.index.multiproperty.MultiPropertyFilterIndex; +import org.osgi.framework.BundleContext; + +/** + * A + * @author nxuser + * + */ +@SuppressWarnings("restriction") +public class DynamicCustomFilterIndex extends MultiPropertyFilterIndex { + + /** + * System property set to true when our DynamicCustomFilterIndex has been opened + */ + private final static String OPENED = "org.apache.felix.dm.index.itest.dynamiccustomindex.CustomFilterIndex.opened"; + + public DynamicCustomFilterIndex(String configString) { + super(configString); + } + + @Override + public void open(BundleContext context) { + super.open(context); + // indicate to our StaticCustomIndexTest that we have been used + System.setProperty(OPENED, "true"); + } + + @Override + public void close() { + super.close(); + System.getProperties().remove(OPENED); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/staticcustomindex/StaticCustomFilterIndex.java b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/staticcustomindex/StaticCustomFilterIndex.java new file mode 100644 index 00000000000..e54170df2af --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/staticcustomindex/StaticCustomFilterIndex.java @@ -0,0 +1,49 @@ +/* + * 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.dm.index.itest.staticcustomindex; + +import org.apache.felix.dm.impl.index.multiproperty.MultiPropertyFilterIndex; +import org.osgi.framework.BundleContext; + +@SuppressWarnings("restriction") +public class StaticCustomFilterIndex extends MultiPropertyFilterIndex { + + /** + * System property set to true when our StaticCustomFilterIndex has been opened + */ + private final static String OPENED = "org.apache.felix.dm.index.itest.staticcustomindex.StaticCustomFilterIndex.opened"; + + public StaticCustomFilterIndex(String configString) { + super(configString); + } + + @Override + public void open(BundleContext context) { + super.open(context); + // indicate to our StaticCustomIndexTest that we have been used + System.setProperty(OPENED, "true"); + } + + @Override + public void close() { + super.close(); + System.getProperties().remove(OPENED); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/DynamicCustomIndexTest.java b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/DynamicCustomIndexTest.java new file mode 100644 index 00000000000..e96467c61ba --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/DynamicCustomIndexTest.java @@ -0,0 +1,150 @@ +/* + * 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.dm.index.itest.tests; +//import static org.ops4j.pax.exam.CoreOptions.waitForFrameworkStartupFor; +//import static org.ops4j.pax.exam.container.def.PaxRunnerOptions.vmOption; + +import java.util.function.Consumer; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.FilterIndex; +import org.apache.felix.dm.index.itest.dynamiccustomindex.DynamicCustomFilterIndex; +import org.junit.Assert; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceRegistration; + +/** + * @author Felix Project Team + */ +public class DynamicCustomIndexTest extends TestBase { + + /** + * This system property is set to true when the DynamicCustomFilterIndex index has been opened. + */ + private final static String OPENED = "org.apache.felix.dm.index.itest.dynamiccustomindex.CustomFilterIndex.opened"; + + private ServiceRegistration m_reg; + + private String m_systemConf; + + @SuppressWarnings("unchecked") + public void setUp() throws Exception { + System.setProperty(OPENED, "false"); + + // backup currently configured filter index + BundleContext context = FrameworkUtil.getBundle(this.getClass()).getBundleContext(); + m_systemConf = context.getProperty(DependencyManager.SERVICEREGISTRY_CACHE_INDICES); + + // Reset filter indices (we must initialize DependencyManager, so its static initializer will register + // the reset backdoor. + @SuppressWarnings("unused") + DependencyManager dm = new DependencyManager(context); + Consumer reset = (Consumer) System.getProperties().get("org.apache.felix.dependencymanager.filterindex.reset"); + reset.accept(null); // clear filter index + + // register our DynamicCustomFilterIndex service before calling super.setUp(). This will make + // the "getDM()" method return a DependencyManager that is using our DynamicCustomFilterIndex + m_reg = context.registerService(FilterIndex.class.getName(), new DynamicCustomFilterIndex("objectClass"), null); + super.setUp(); + } + + @SuppressWarnings("unchecked") + public void tearDown() throws Exception { + super.tearDown(); + try { + m_reg.unregister(); + } catch (IllegalStateException e) { // expected, normally we have already unregistered it + } + System.getProperties().remove(OPENED); + Consumer reset = (Consumer) System.getProperties().get("org.apache.felix.dependencymanager.filterindex.reset"); + reset.accept(m_systemConf); + } + + public void testUsingDynamicCustomIndex() throws Exception { + doTestUsingDynamicCustomIndex(); + + // Make sure our static custom index has been used + Assert.assertTrue(Boolean.getBoolean(OPENED)); + + // unregister our dynamic filter index + m_reg.unregister(); + + // clear the flag + System.setProperty(OPENED, "false"); + + // redo the test + doTestUsingDynamicCustomIndex(); + Assert.assertFalse(Boolean.getBoolean(OPENED)); + } + + private void doTestUsingDynamicCustomIndex() throws Exception { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a provider + Provider provider = new Provider(); + // activate it + Component p = m.createComponent() + .setInterface(Service.class.getName(), null) + .setImplementation(provider); + + Client consumer = new Client(e); + Component c = m.createComponent() + .setImplementation(consumer) + .add(m.createServiceDependency() + .setService(Service.class) + .setRequired(true) + ); + + m.add(p); + m.add(c); + e.waitForStep(1, 5000); + m.remove(p); + e.waitForStep(2, 5000); + m.remove(c); + m.clear(); + } + + public static class Client { + volatile Service m_service; + private final Ensure m_ensure; + + public Client(Ensure e) { + m_ensure = e; + } + + public void start() { + System.out.println("start"); + m_ensure.step(1); + } + + public void stop() { + System.out.println("stop"); + m_ensure.step(2); + } + } + + public static interface Service { + } + + public static class Provider implements Service { + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/Ensure.java b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/Ensure.java new file mode 100644 index 00000000000..2427805e821 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/Ensure.java @@ -0,0 +1,174 @@ +/* + * 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.dm.index.itest.tests; + +import java.io.PrintStream; + +import org.junit.Assert; + +/** + * Helper class to make sure that steps in a test happen in the correct order. Instantiate + * this class and subsequently invoke step(nr) with steps starting at 1. You + * can also have threads wait until you arrive at a certain step. + * + * @author Felix Project Team + */ +public class Ensure { + private final boolean DEBUG; + private static long INSTANCE = 0; + private static final int RESOLUTION = 100; + private static PrintStream STREAM = System.out; + int step = 0; + private Throwable m_throwable; + + public Ensure() { + this(true); + } + + public Ensure(boolean debug) { + DEBUG = debug; + if (DEBUG) { + INSTANCE++; + } + } + + public void setStream(PrintStream output) { + STREAM = output; + } + + /** + * Mark this point as step nr. + * + * @param nr the step we are in + */ + public synchronized void step(int nr) { + step++; + Assert.assertEquals(nr, step); + if (DEBUG) { + String info = getLineInfo(3); + STREAM.println("[Ensure " + INSTANCE + "] step " + step + " [" + currentThread() + "] " + info); + } + notifyAll(); + } + + private String getLineInfo(int depth) { + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + String info = trace[depth].getClassName() + "." + trace[depth].getMethodName() + ":" + trace[depth].getLineNumber(); + return info; + } + + /** + * Mark this point as the next step. + */ + public synchronized void step() { + step++; + if (DEBUG) { + String info = getLineInfo(3); + STREAM.println("[Ensure " + INSTANCE + "] next step " + step + " [" + currentThread() + "] " + info); + } + notifyAll(); + } + + /** + * Wait until we arrive at least at step nr in the process, or fail if that + * takes more than timeout milliseconds. If you invoke wait on a thread, + * you are effectively assuming some other thread will invoke the step(nr) + * method. + * + * @param nr the step to wait for + * @param timeout the number of milliseconds to wait + */ + public synchronized void waitForStep(int nr, int timeout) { + final int initialTimeout = timeout; + if (DEBUG) { + String info = getLineInfo(3); + STREAM.println("[Ensure " + INSTANCE + "] waiting for step " + nr + " [" + currentThread() + "] " + info); + } + while (step < nr && timeout > 0) { + try { + wait(RESOLUTION); + timeout -= RESOLUTION; + } + catch (InterruptedException e) {} + } + if (step < nr) { + throw new IllegalStateException("Timed out waiting for " + initialTimeout + " ms for step " + nr + ", we are still at step " + step); + } + if (DEBUG) { + String info = getLineInfo(3); + STREAM.println("[Ensure " + INSTANCE + "] arrived at step " + nr + " [" + currentThread() + "] " + info); + } + } + + private String currentThread() { + Thread thread = Thread.currentThread(); + return thread.getId() + " " + thread.getName(); + } + + public static Runnable createRunnableStep(final Ensure ensure, final int nr) { + return new Runnable() { public void run() { ensure.step(nr); }}; + } + + public synchronized void steps(Steps steps) { + steps.next(this); + } + + /** + * Helper class for naming a list of step numbers. If used with the steps(Steps) method + * you can define at which steps in time this point should be passed. That means you can + * check methods that will get invoked multiple times during a test. + */ + public static class Steps { + private final int[] m_steps; + private int m_stepIndex; + + /** + * Create a list of steps and initialize the step counter to zero. + */ + public Steps(int... steps) { + m_steps = steps; + m_stepIndex = 0; + } + + /** + * Ensure we're at the right step. Will throw an index out of bounds exception if we enter this step more often than defined. + */ + public void next(Ensure ensure) { + ensure.step(m_steps[m_stepIndex++]); + } + } + + /** + * Saves a thrown exception that occurred in a different thread. You can only save one exception + * at a time this way. + */ + public synchronized void throwable(Throwable throwable) { + m_throwable = throwable; + } + + /** + * Throws a Throwable if one occurred in a different thread and that thread saved it + * using the throwable() method. + */ + public synchronized void ensure() throws Throwable { + if (m_throwable != null) { + throw m_throwable; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/FELIX3008_FilterIndexStartupTest.java b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/FELIX3008_FilterIndexStartupTest.java new file mode 100644 index 00000000000..1f6eafdf927 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/FELIX3008_FilterIndexStartupTest.java @@ -0,0 +1,87 @@ +/* + * 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.dm.index.itest.tests; +//import static org.ops4j.pax.exam.CoreOptions.waitForFrameworkStartupFor; +//import static org.ops4j.pax.exam.container.def.PaxRunnerOptions.vmOption; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.Bundle; + +/** + * @author Felix Project Team + */ +public class FELIX3008_FilterIndexStartupTest extends TestBase { + + public void testNormalStart() throws Exception { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a provider + Provider provider = new Provider(); + // activate it + Component p = m.createComponent() + .setInterface(Service.class.getName(), null) + .setImplementation(provider); + + Consumer consumer = new Consumer(e); + Component c = m.createComponent() + .setImplementation(consumer) + .add(m.createServiceDependency() + .setService(Service.class) + .setRequired(true) + ); + + m.add(p); + m.add(c); + e.waitForStep(1, 5000); + m.remove(p); + e.waitForStep(2, 5000); + m.remove(c); + m.clear(); + Assert.assertEquals("Dependency manager bundle should be active.", Bundle.ACTIVE, context.getBundle().getState()); + } + + public static class Consumer { + volatile Service m_service; + private final Ensure m_ensure; + + public Consumer(Ensure e) { + m_ensure = e; + } + + public void start() { + System.out.println("start"); + m_ensure.step(1); + } + + public void stop() { + System.out.println("stop"); + m_ensure.step(2); + } + } + + public static interface Service { + } + + public static class Provider implements Service { + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/FELIX3057_EmptyServiceReferenceArray.java b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/FELIX3057_EmptyServiceReferenceArray.java new file mode 100644 index 00000000000..af5784d0d91 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/FELIX3057_EmptyServiceReferenceArray.java @@ -0,0 +1,68 @@ +/* + * 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.dm.index.itest.tests; +//import static org.ops4j.pax.exam.CoreOptions.waitForFrameworkStartupFor; +//import static org.ops4j.pax.exam.container.def.PaxRunnerOptions.vmOption; + +import org.junit.Assert; + +import java.util.function.Consumer; + +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; + +/** + * @author Felix Project Team + */ +public class FELIX3057_EmptyServiceReferenceArray extends TestBase { + + private String m_systemConf; + + @SuppressWarnings("unchecked") + public void testWithoutIndex() throws Exception { + // backup currently configured filter index + BundleContext context = FrameworkUtil.getBundle(this.getClass()).getBundleContext(); + m_systemConf = context.getProperty(DependencyManager.SERVICEREGISTRY_CACHE_INDICES); + + // Reset filter indices + Consumer reset = (Consumer) System.getProperties().get("org.apache.felix.dependencymanager.filterindex.reset"); + reset.accept(null); // clear filter index + + executeTest(context); // no filter index used + + reset.accept(m_systemConf); // reset filer index configured in our bnd.bnd + } + + public void testWithIndex() throws Exception { + executeTest(context); // SERVICEREGISTRY_CACHE_INDICES system property is configured in our bnd.bnd + } + + private void executeTest(BundleContext context) throws InvalidSyntaxException { + DependencyManager m = new DependencyManager(context); + Assert.assertNull("Looking up a non-existing service should return null.", m.getBundleContext().getServiceReferences(Service.class.getName(), "(objectClass=*)")); + Assert.assertNull("Looking up a non-existing service should return null.", m.getBundleContext().getAllServiceReferences(Service.class.getName(), "(objectClass=*)")); + Assert.assertNull("Looking up a non-existing service should return null.", m.getBundleContext().getServiceReference(Service.class.getName())); + m.clear(); + } + + /** Dummy interface for lookup. */ + public static interface Service {} +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/FilterIndexResourceAdapterTest.java b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/FilterIndexResourceAdapterTest.java new file mode 100644 index 00000000000..6f0eae95a44 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/FilterIndexResourceAdapterTest.java @@ -0,0 +1,85 @@ +/* + * 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.dm.index.itest.tests; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ResourceHandler; +import org.junit.Assert; + +/** + * @author Felix Project Team + */ +public class FilterIndexResourceAdapterTest extends TestBase { + + public void testBasicResourceAdapter() throws Exception { + DependencyManager m = getDM(); // filter index is configured in our bnd.bnd file + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a resource provider + ResourceProvider provider = new ResourceProvider(context, new URL("file://localhost/path/to/file1.txt")); + // activate it + m.add(m.createComponent().setImplementation(provider).add(m.createServiceDependency().setService(ResourceHandler.class).setCallbacks("add", "remove"))); + // create a resource adapter for our single resource + // note that we can provide an actual implementation instance here because there will be only one + // adapter, normally you'd want to specify a Class here + m.add(m.createResourceAdapterService("(&(path=/path/to/*.txt)(host=localhost))", false, null, "changed") + .setImplementation(new ResourceAdapter(e))); + // wait until the single resource is available + e.waitForStep(3, 5000); + // trigger a 'change' in our resource + provider.change(); + // wait until the changed callback is invoked + e.waitForStep(4, 5000); + m.clear(); + } + + static class ResourceAdapter { + protected URL m_resource; // injected by reflection. + private Ensure m_ensure; + + ResourceAdapter(Ensure e) { + m_ensure = e; + } + + public void start() { + m_ensure.step(1); + Assert.assertNotNull("resource not injected", m_resource); + m_ensure.step(2); + try { + @SuppressWarnings("unused") + InputStream in = m_resource.openStream(); + } + catch (FileNotFoundException e) { + m_ensure.step(3); + } + catch (IOException e) { + Assert.fail("We should not have gotten this exception."); + } + } + + public void changed() { + m_ensure.step(4); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/ResourceProvider.java b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/ResourceProvider.java new file mode 100644 index 00000000000..40f08f3812b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/ResourceProvider.java @@ -0,0 +1,122 @@ +/* + * 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.dm.index.itest.tests; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Dictionary; + +import org.junit.Assert; +import org.apache.felix.dm.ResourceHandler; +import org.apache.felix.dm.ResourceUtil; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; + +/** + * @author Felix Project Team + */ +public class ResourceProvider { + final URL[] m_resources; + final BundleContext m_context; + final Map m_handlers = new HashMap<>(); + + public ResourceProvider(BundleContext ctx, URL ... resources) { + m_context = ctx; + m_resources = resources; + } + + public void change() { + for (int i = 0; i < m_resources.length; i++) { + change(i); + } + } + + @SuppressWarnings("deprecation") + public void change(int resourceIndex) { + Map handlers = new HashMap<>(); + synchronized (m_handlers) { + handlers.putAll(m_handlers); + } + for (Map.Entry e : handlers.entrySet()) { + ResourceHandler handler = e.getKey(); + Filter filter = e.getValue(); + if (filter == null || filter.match((Dictionary) ResourceUtil.createProperties(m_resources[resourceIndex]))) { + handler.changed(m_resources[resourceIndex]); + } + } + } + + @SuppressWarnings("deprecation") + public void add(ServiceReference ref, ResourceHandler handler) { + String filterString = (String) ref.getProperty("filter"); + Filter filter = null; + if (filterString != null) { + try { + filter = m_context.createFilter(filterString); + } + catch (InvalidSyntaxException e) { + Assert.fail("Could not create filter for resource handler: " + e); + return; + } + } + for (int i = 0; i < m_resources.length; i++) { + if (filter == null || filter.match((Dictionary) ResourceUtil.createProperties(m_resources[i]))) { + synchronized (m_handlers) { + m_handlers.put(handler, filter); + } + handler.added(m_resources[i]); + } + } + } + + public void remove(ServiceReference ref, ResourceHandler handler) { + Filter filter; + synchronized (m_handlers) { + filter = (Filter) m_handlers.remove(handler); + } + if (filter != null) { + removeResources(handler, filter); + } + } + + @SuppressWarnings("deprecation") + private void removeResources(ResourceHandler handler, Filter filter) { + for (int i = 0; i < m_resources.length; i++) { + if (filter == null || filter.match((Dictionary) ResourceUtil.createProperties(m_resources[i]))) { + handler.removed(m_resources[i]); + } + } + } + + public void destroy() { + Map handlers = new HashMap<>(); + synchronized (m_handlers) { + handlers.putAll(m_handlers); + } + + for (Map.Entry e : handlers.entrySet()) { + ResourceHandler handler = e.getKey(); + Filter filter = e.getValue(); + removeResources(handler, filter); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/StaticCustomIndexTest.java b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/StaticCustomIndexTest.java new file mode 100644 index 00000000000..64a0a6cbf39 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/StaticCustomIndexTest.java @@ -0,0 +1,122 @@ +/* + * 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.dm.index.itest.tests; +//import static org.ops4j.pax.exam.CoreOptions.waitForFrameworkStartupFor; +//import static org.ops4j.pax.exam.container.def.PaxRunnerOptions.vmOption; + +import java.util.function.Consumer; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; + +/** + * @author Felix Project Team + */ +public class StaticCustomIndexTest extends TestBase { + + /** + * This system property is set to true when the StaticCustomFilterIndex index has been opened. + */ + private final static String OPENED = "org.apache.felix.dm.index.itest.staticcustomindex.StaticCustomFilterIndex.opened"; + + private String m_systemConf; + + @SuppressWarnings("unchecked") + public void setUp() throws Exception { + System.setProperty(OPENED, "false"); + + // backup currently configured filter index + BundleContext context = FrameworkUtil.getBundle(this.getClass()).getBundleContext(); + m_systemConf = context.getProperty(DependencyManager.SERVICEREGISTRY_CACHE_INDICES); + + // configure our filter index and use the special DM backdoor in order to reinitialize filter indices + Consumer reset = (Consumer) System.getProperties().get("org.apache.felix.dependencymanager.filterindex.reset"); + reset.accept("org.apache.felix.dm.index.itest.staticcustomindex.StaticCustomFilterIndex:objectClass"); + + // now call super.setUp() method: the getDM() method will return a DependencyManager that will use the filter index. + super.setUp(); + } + + @SuppressWarnings("unchecked") + public void tearDown() throws Exception { + super.tearDown(); + System.getProperties().remove(OPENED); + Consumer reset = (Consumer) System.getProperties().get("org.apache.felix.dependencymanager.filterindex.reset"); + reset.accept(m_systemConf); + } + + public void testUsingStaticCustomIndex() throws Exception { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a provider + Provider provider = new Provider(); + // activate it + Component p = m.createComponent() + .setInterface(Service.class.getName(), null) + .setImplementation(provider); + + Client consumer = new Client(e); + Component c = m.createComponent() + .setImplementation(consumer) + .add(m.createServiceDependency() + .setService(Service.class) + .setRequired(true) + ); + + m.add(p); + m.add(c); + e.waitForStep(1, 5000); + m.remove(p); + e.waitForStep(2, 5000); + m.remove(c); + m.clear(); + + // Make sure our static custom index has been used + Assert.assertTrue(Boolean.getBoolean(OPENED)); + } + + public static class Client { + volatile Service m_service; + private final Ensure m_ensure; + + public Client(Ensure e) { + m_ensure = e; + } + + public void start() { + System.out.println("start"); + m_ensure.step(1); + } + + public void stop() { + System.out.println("stop"); + m_ensure.step(2); + } + } + + public static interface Service { + } + + public static class Provider implements Service { + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/TestBase.java b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/TestBase.java new file mode 100644 index 00000000000..36a98fe7a80 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.index.itest/src/org/apache/felix/dm/index/itest/tests/TestBase.java @@ -0,0 +1,365 @@ +/* + * 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.dm.index.itest.tests; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentExecutorFactory; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.log.LogService; + +import junit.framework.TestCase; + +/** + * Base class for all integration tests. + * + * @author Felix Project Team + */ +public abstract class TestBase extends TestCase implements LogService, FrameworkListener { + // Default OSGI log service level. + protected final static int LOG_LEVEL = LogService.LOG_WARNING; + + // optional thread pool used by parallel dependency managers + protected volatile ForkJoinPool m_threadPool; + + // flag used to check if the threadpool must be used for a given test. + protected volatile boolean m_parallel; + + // Flag used to check if some errors have been logged during the execution of a given test. + private volatile boolean m_errorsLogged; + + // We implement OSGI log service. + protected ServiceRegistration logService; + + // Our bundle context + protected BundleContext context; + + // Our dependency manager used to create test components. + protected volatile DependencyManager m_dm; + + // The Registration for the DM threadpool. + private ServiceRegistration m_componentExecutorFactoryReg; + + public TestBase() { + } + + protected void setParallel() { + m_parallel = true; + } + + public void setUp() throws Exception { + warn("Setting up test " + getClass().getName()); + context = FrameworkUtil.getBundle(this.getClass()).getBundleContext(); + Hashtable props = new Hashtable<>(); + props.put(Constants.SERVICE_RANKING, new Integer(Integer.MAX_VALUE)); + logService = context.registerService(LogService.class.getName(), this, props); + context.addFrameworkListener(this); + m_dm = new DependencyManager(context); + if (m_parallel) { + warn("Using threadpool ..."); + m_threadPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors()); + m_componentExecutorFactoryReg = context.registerService(ComponentExecutorFactory.class.getName(), + new ComponentExecutorFactory() { + @Override + public Executor getExecutorFor(Component component) { + return m_threadPool; + } + }, + null); + } + } + + public void tearDown() throws Exception { + warn("Tearing down test " + getClass().getName()); + logService.unregister(); + context.removeFrameworkListener(this); + clearComponents(); + + if (m_parallel && m_componentExecutorFactoryReg != null) { + m_componentExecutorFactoryReg.unregister(); + m_threadPool.shutdown(); + try { + m_threadPool.awaitTermination(60, TimeUnit.SECONDS); + } catch (InterruptedException e) { + } + } + Assert.assertFalse(errorsLogged()); + } + + protected DependencyManager getDM() { + return m_dm; + } + + protected void clearComponents() { + m_dm.clear(); + warn("All component cleared."); + } + + /** + * Creates and provides an Ensure object with a name service property into the OSGi service registry. + */ + protected ServiceRegistration register(Ensure e, String name) { + Hashtable props = new Hashtable(); + props.put("name", name); + return context.registerService(Ensure.class.getName(), e, props); + } + + /** + * Helper method used to stop a given bundle. + * + * @param symbolicName + * the symbolic name of the bundle to be stopped. + */ + protected void stopBundle(String symbolicName) { + // Stop the test.annotation bundle + boolean found = false; + for (Bundle b : context.getBundles()) { + if (b.getSymbolicName().equals(symbolicName)) { + try { + found = true; + b.stop(); + } catch (BundleException e) { + e.printStackTrace(); + } + } + } + if (!found) { + throw new IllegalStateException("bundle " + symbolicName + " not found"); + } + } + + /** + * Helper method used to start a given bundle. + * + * @param symbolicName + * the symbolic name of the bundle to be started. + */ + protected void startBundle(String symbolicName) { + // Stop the test.annotation bundle + boolean found = false; + for (Bundle b : context.getBundles()) { + if (b.getSymbolicName().equals(symbolicName)) { + try { + found = true; + b.start(); + } catch (BundleException e) { + e.printStackTrace(); + } + } + } + if (!found) { + throw new IllegalStateException("bundle " + symbolicName + " not found"); + } + } + + /** + * Helper method used to get a given bundle. + * + * @param symbolicName + * the symbolic name of the bundle to get. + */ + protected Bundle getBundle(String symbolicName) { + for (Bundle b : context.getBundles()) { + if (b.getSymbolicName().equals(symbolicName)) { + return b; + } + } + throw new IllegalStateException("bundle " + symbolicName + " not found"); + } + + /** + * Suspend the current thread for a while. + * + * @param n + * the number of milliseconds to wait for. + */ + protected void sleep(int ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException e) { + } + } + + /** + * Helper method used to convert a dictionary which with untyped keys to a dictionary having a String key. + * (this method is useful when converting a Properties object into a compatible Dictionary + * object that is often needed in OSGI R6 API. + */ + @SuppressWarnings("unchecked") + public static Dictionary toR6Dictionary(Dictionary properties) { + return (Dictionary) properties; + } + + public void log(int level, String message) { + checkError(level, null); + if (LOG_LEVEL >= level) { + System.out.println(getLevel(level) + " - " + Thread.currentThread().getName() + " : " + message); + } + } + + public void log(int level, String message, Throwable exception) { + checkError(level, exception); + if (LOG_LEVEL >= level) { + StringBuilder sb = new StringBuilder(); + sb.append(getLevel(level) + " - " + Thread.currentThread().getName() + " : "); + sb.append(message); + parse(sb, exception); + System.out.println(sb.toString()); + } + } + + public void log(ServiceReference sr, int level, String message) { + checkError(level, null); + if (LOG_LEVEL >= level) { + StringBuilder sb = new StringBuilder(); + sb.append(getLevel(level) + " - " + Thread.currentThread().getName() + " : "); + sb.append(message); + System.out.println(sb.toString()); + } + } + + public void log(ServiceReference sr, int level, String message, Throwable exception) { + checkError(level, exception); + if (LOG_LEVEL >= level) { + StringBuilder sb = new StringBuilder(); + sb.append(getLevel(level) + " - " + Thread.currentThread().getName() + " : "); + sb.append(message); + parse(sb, exception); + System.out.println(sb.toString()); + } + } + + protected boolean errorsLogged() { + return m_errorsLogged; + } + + private void parse(StringBuilder sb, Throwable t) { + if (t != null) { + sb.append(" - "); + StringWriter buffer = new StringWriter(); + PrintWriter pw = new PrintWriter(buffer); + t.printStackTrace(pw); + sb.append(buffer.toString()); + m_errorsLogged = true; + } + } + + private String getLevel(int level) { + switch (level) { + case LogService.LOG_DEBUG : + return "DEBUG"; + case LogService.LOG_ERROR : + return "ERROR"; + case LogService.LOG_INFO : + return "INFO"; + case LogService.LOG_WARNING : + return "WARN"; + default : + return ""; + } + } + + private void checkError(int level, Throwable exception) { + if (level <= LOG_ERROR) { + m_errorsLogged = true; + } + if (exception != null) { + m_errorsLogged = true; + } + } + + public void frameworkEvent(FrameworkEvent event) { + int eventType = event.getType(); + String msg = getFrameworkEventMessage(eventType); + int level = (eventType == FrameworkEvent.ERROR) ? LOG_ERROR : LOG_WARNING; + if (msg != null) { + log(level, msg, event.getThrowable()); + } else { + log(level, "Unknown fwk event: " + event); + } + } + + private String getFrameworkEventMessage(int event) { + switch (event) { + case FrameworkEvent.ERROR : + return "FrameworkEvent: ERROR"; + case FrameworkEvent.INFO : + return "FrameworkEvent INFO"; + case FrameworkEvent.PACKAGES_REFRESHED : + return "FrameworkEvent: PACKAGE REFRESHED"; + case FrameworkEvent.STARTED : + return "FrameworkEvent: STARTED"; + case FrameworkEvent.STARTLEVEL_CHANGED : + return "FrameworkEvent: STARTLEVEL CHANGED"; + case FrameworkEvent.WARNING : + return "FrameworkEvent: WARNING"; + default : + return null; + } + } + + protected void warn(String msg, Object ... params) { + if (LOG_LEVEL >= LogService.LOG_WARNING) { + log(LogService.LOG_WARNING, params.length > 0 ? String.format(msg, params) : msg); + } + } + + @SuppressWarnings("unused") + protected void info(String msg, Object ... params) { + if (LOG_LEVEL >= LogService.LOG_INFO) { + log(LogService.LOG_INFO, params.length > 0 ? String.format(msg, params) : msg); + } + } + + @SuppressWarnings("unused") + protected void debug(String msg, Object ... params) { + if (LOG_LEVEL >= LogService.LOG_DEBUG) { + log(LogService.LOG_DEBUG, params.length > 0 ? String.format(msg, params) : msg); + } + } + + protected void error(String msg, Object ... params) { + log(LogService.LOG_ERROR, params.length > 0 ? String.format(msg, params) : msg); + } + + protected void error(String msg, Throwable err, Object ... params) { + log(LogService.LOG_ERROR, params.length > 0 ? String.format(msg, params) : msg, err); + } + + protected void error(Throwable err) { + log(LogService.LOG_ERROR, "error", err); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.index.itest/static.customindex.bnd b/dependencymanager/org.apache.felix.dependencymanager.index.itest/static.customindex.bnd new file mode 100644 index 00000000000..cdb1d1f1ce5 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.index.itest/static.customindex.bnd @@ -0,0 +1,23 @@ +# +# 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. +# +Private-Package: org.apache.felix.dm.index.itest.staticcustomindex +Fragment-Host: org.apache.felix.dependencymanager +Import-Package: !org.apache.felix.dm.*,* +Bundle-Name: Apache Felix Dependency Manager integration tests Static Custom Index +Bundle-Category: test +Bundle-License: http://www.apache.org/licenses/LICENSE-2.0.txt +Bundle-Vendor: The Apache Software Foundation diff --git a/dependencymanager/org.apache.felix.dependencymanager.index.itest/test/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.index.itest/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/org.apache.felix.dependencymanager.index.itest/tests.bnd b/dependencymanager/org.apache.felix.dependencymanager.index.itest/tests.bnd new file mode 100644 index 00000000000..a681f12e6d9 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.index.itest/tests.bnd @@ -0,0 +1,21 @@ +# +# 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. +# +Private-Package: org.apache.felix.dm.index.itest.tests +Bundle-Name: Apache Felix Dependency Manager FilterIndex tests +Bundle-Category: test +Bundle-License: http://www.apache.org/licenses/LICENSE-2.0.txt +Bundle-Vendor: The Apache Software Foundation diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/.classpath b/dependencymanager/org.apache.felix.dependencymanager.itest/.classpath new file mode 100644 index 00000000000..7a6fc254361 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.itest/.gitignore new file mode 100644 index 00000000000..90dde36e4ac --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/.gitignore @@ -0,0 +1,3 @@ +/bin/ +/bin_test/ +/generated/ diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/.project b/dependencymanager/org.apache.felix.dependencymanager.itest/.project new file mode 100644 index 00000000000..9aaf1ea9c98 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/.project @@ -0,0 +1,23 @@ + + + org.apache.felix.dependencymanager.itest + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/api.bnd b/dependencymanager/org.apache.felix.dependencymanager.itest/api.bnd new file mode 100644 index 00000000000..6e0cfcaf238 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/api.bnd @@ -0,0 +1,25 @@ +# +# 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. +# +Private-Package: \ + org.apache.felix.dm.itest.api +Export-Package: \ + org.apache.felix.dm.itest.util +Bundle-Name: Apache Felix Dependency Manager integration tests +Bundle-Description: Integration tests for Apache Felix Dependency Manager +Bundle-Category: test +Bundle-License: http://www.apache.org/licenses/LICENSE-2.0.txt +Bundle-Vendor: The Apache Software Foundation \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/bnd.bnd b/dependencymanager/org.apache.felix.dependencymanager.itest/bnd.bnd new file mode 100644 index 00000000000..3005063c94a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/bnd.bnd @@ -0,0 +1,53 @@ +# +# 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. +# +-runbundles: \ + org.apache.servicemix.bundles.junit;version=4.12,\ + org.mockito.mockito-core;version='[1.10.19,1.10.20)',\ + org.objenesis;version='[2.2.0,2.2.1)',\ + org.apache.felix.metatype;version=1.1.2,\ + org.apache.felix.log;version=1.0.1,\ + org.apache.felix.configadmin;version=1.8.8,\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.gogo.runtime;version=1.0.6,\ + org.apache.felix.gogo.shell,\ + org.apache.felix.gogo.command +-runee: JavaSE-1.8 +-runvm: -ea +-runfw: org.apache.felix.framework;version='[5.6.10,5.6.10]' +-buildpath: \ + osgi.core;version=6.0,\ + osgi.cmpn;version=6.0,\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + ${junit},\ + org.apache.felix.gogo.runtime;version=1.0 +-runsystempackages: \ + sun.reflect +-sub: \ + *.bnd +Test-Cases: \ + ${classes;CONCRETE;EXTENDS;junit.framework.TestCase} +-runproperties: \ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true,\ + gosh.args=--noshutdown + +# we do not release this project in binary distribution. +-releaserepo: +-baseline: diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/bundle.bnd b/dependencymanager/org.apache.felix.dependencymanager.itest/bundle.bnd new file mode 100644 index 00000000000..c2f69fa95a0 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/bundle.bnd @@ -0,0 +1,25 @@ +# +# 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. +# +Export-Package: \ + org.apache.felix.dm.itest.bundle +Bundle-Activator: org.apache.felix.dm.itest.bundle.Activator +Bundle-Name: Apache Felix Dependency Manager integration test bundle +Bundle-Description: Internal test bundle used by Apache Felix Dependency Manager integration \ + tests +Bundle-Category: test +Bundle-License: http://www.apache.org/licenses/LICENSE-2.0.txt +Bundle-Vendor: The Apache Software Foundation \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/bundle2.bnd b/dependencymanager/org.apache.felix.dependencymanager.itest/bundle2.bnd new file mode 100644 index 00000000000..4fd90f43912 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/bundle2.bnd @@ -0,0 +1,19 @@ +# +# 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. +# +Private-Package: org.apache.felix.dm.itest.bundle2 +Bundle-Activator: org.apache.felix.dm.itest.bundle2.Activator +Export-Package: org.apache.felix.dm.itest.bundle2 \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AbstractServiceDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AbstractServiceDependencyTest.java new file mode 100644 index 00000000000..fb0bbe0f3d3 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AbstractServiceDependencyTest.java @@ -0,0 +1,107 @@ +/* + * 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.dm.itest.api; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * @author Felix Project Team + */ +public class AbstractServiceDependencyTest extends TestBase { + public void testAbstractClassDependency() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = m.createComponent() + .setInterface(ServiceAbstract.class.getName(), null) + .setImplementation(new ServiceProvider(e)) + ; + Component sc = m.createComponent() + .setImplementation(new ServiceConsumer(e)) + .add(m.createServiceDependency() + .setService(ServiceAbstract.class) + .setRequired(true) + .setCallbacks("bind", "unbind") + ); + m.add(sp); + m.add(sc); + m.remove(sp); + // ensure we executed all steps inside the component instance + e.step(8); + m.clear(); + } + + static abstract class ServiceAbstract { + public abstract void invoke(); + } + + static class ServiceProvider extends ServiceAbstract { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + + public void start() { + m_ensure.step(1); + } + + public void invoke() { + m_ensure.step(4); + } + + public void stop() { + m_ensure.step(7); + } + } + + static class ServiceConsumer { + private volatile ServiceAbstract m_service; + private final Ensure m_ensure; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void bind(ServiceAbstract service) { + m_ensure.step(2); + m_service = service; + } + + public void start() { + m_ensure.step(3); + m_service.invoke(); + } + + public void stop() { + m_ensure.step(5); + } + + public void unbind(ServiceAbstract service) { + System.out.println("UNBINDDDDDDDDDDDDDDDDDDDDDDDDDDD"); + Assert.assertEquals(m_service, service); + m_ensure.step(6); + } + } +} \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterAndConsumerTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterAndConsumerTest.java new file mode 100644 index 00000000000..93949eee0fd --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterAndConsumerTest.java @@ -0,0 +1,113 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * @author Felix Project Team + */ +public class AdapterAndConsumerTest extends TestBase { + + public void testServiceWithAdapterAndConsumer() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + Component provider = m.createComponent() + .setInterface(OriginalService.class.getName(), null) + .setImplementation(new ServiceProvider(e)); + + Component consumer = m.createComponent() + .setImplementation(new ServiceConsumer(e)) + .add(m.createServiceDependency() + .setService(AdaptedService.class) + .setRequired(true) + ); + + Component adapter = m.createAdapterService(OriginalService.class, null) + .setInterface(AdaptedService.class.getName(), null) + .setImplementation(ServiceAdapter.class); + + // add the provider and the adapter + m.add(provider); + m.add(adapter); + // add a consumer that will invoke the adapter + // which will in turn invoke the original provider + m.add(consumer); + // now validate that both have been invoked in the right order + e.waitForStep(2, 5000); + // remove the provider again + m.remove(provider); + // ensure that the consumer is stopped + e.waitForStep(3, 5000); + // remove adapter and consumer + m.remove(adapter); + m.remove(consumer); + } + + static interface OriginalService { + public void invoke(); + } + + static interface AdaptedService { + public void invoke(); + } + + static class ServiceProvider implements OriginalService { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke() { + m_ensure.step(2); + } + } + + public static class ServiceAdapter implements AdaptedService { + private volatile OriginalService m_originalService; + + public void start() { System.out.println("start"); } + public void stop() { System.out.println("stop"); } + public void invoke() { + m_originalService.invoke(); + } + } + + static class ServiceConsumer { + private volatile AdaptedService m_service; + private final Ensure m_ensure; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + public void start() { + m_ensure.step(1); + m_service.invoke(); + } + public void stop() { + m_ensure.step(3); + } + } +} + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterNoAutoConfigIfInstanceCallbackIsUsed.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterNoAutoConfigIfInstanceCallbackIsUsed.java new file mode 100644 index 00000000000..4d28d081c3e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterNoAutoConfigIfInstanceCallbackIsUsed.java @@ -0,0 +1,77 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; + +/** + * This tests validates that autoconfiguration of adapted service on adapter class field(s) is not + * enabled when an instance callback is used when injected adapted service. + */ +public class AdapterNoAutoConfigIfInstanceCallbackIsUsed extends TestBase { + final static Ensure m_e = new Ensure(); + + public void testNoAutoConfigIfINstanceCallbackIsUsed() { + DependencyManager m = getDM(); + + // Declare S1 service + Component s1 = m.createComponent().setImplementation(S1Impl.class).setInterface(S1.class.getName(), null); + m.add(s1); + + // Declare S1 adapter + S1AdapterCallback s1AdapterCB = new S1AdapterCallback(); + Component s1Adapter = m.createAdapterService(S1.class, null, null, s1AdapterCB, "set", null, null, null, false) + .setImplementation(S1Adapter.class); + m.add(s1Adapter); + + // At this point, the s1AdapterCB.set(S1 s1) method should be called, and s1Adapter.start() method should then be called. + // but s1 should not be injected on s1Adapter class fields. + + m_e.waitForStep(3, 5000); + m.clear(); + } + + public interface S1 { + } + + public static class S1Impl implements S1 { + } + + public static class S1Adapter { + volatile S1 m_s1; // should not be injected by reflection + + void start() { + m_e.step(2); + Assert.assertNull(m_s1); + m_e.step(3); + } + } + + public static class S1AdapterCallback { + void set(S1 s1) { + Assert.assertNotNull(s1); + m_e.step(1); + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterTest.java new file mode 100644 index 00000000000..b0a02adf19f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterTest.java @@ -0,0 +1,85 @@ +/* + * 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.dm.itest.api; +import org.apache.felix.dm.AdapterComponent; +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.api.FELI5155_AdapterCallbackInstanceCalledTwice.S2; +import org.apache.felix.dm.itest.api.FELI5155_AdapterCallbackInstanceCalledTwice.S2Impl; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; + +public class AdapterTest extends TestBase { + static Ensure m_e; + + public void testAdapterCallbackInstanceCalledTwice() { + DependencyManager m = getDM(); + m_e = new Ensure(); + + S1AdapterImpl adapterImpl = new S1AdapterImpl(); + + Component s1 = m.createComponent().setImplementation(S1Impl.class).setInterface(S1.class.getName(), null); + Component s2 = m.createComponent().setImplementation(S2Impl.class).setInterface(S2.class.getName(), null); + + AdapterComponent s1Adapter = m.createAdapterComponent() + .setImplementation(adapterImpl) + .setAdaptee(S1.class, null) + .setAdapteeCallbacks("setS1", null, null, null) + .add(m.createServiceDependency().setService(S2.class).setRequired(true).setCallbacks("setS2", null, null)); + + m.add(s1); + m.add(s1Adapter); + m.add(s2); + + m_e.waitForStep(3, 5000); + clearComponents(); + } + + + public interface S1 { + } + + public static class S1Impl implements S1 { + } + + public interface S2 { + } + + public static class S2Impl implements S2 { + } + + public static class S1AdapterImpl { + volatile S1 m_s1; + + void setS1(S1 s1) { + m_e.step(1); + m_s1 = s1; + } + + void setS2(S2 s2) { + m_e.step(2); + } + + void start() { + Assert.assertNotNull("service s1 not injected", m_s1); + m_e.step(3); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithCallbackInstanceTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithCallbackInstanceTest.java new file mode 100644 index 00000000000..a7073f5a379 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithCallbackInstanceTest.java @@ -0,0 +1,161 @@ +/* + * 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.dm.itest.api; + +import java.util.Hashtable; +import java.util.Map; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.ServiceRegistration; + +/** + * @author Felix Project Team + */ +public class AdapterWithCallbackInstanceTest extends TestBase { + + public void testServiceWithAdapterAndConsumer() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + ServiceProvider serviceProvider = new ServiceProvider(e); + Component provider = m.createComponent() + .setInterface(OriginalService.class.getName(), null) + .setImplementation(serviceProvider); + + Component consumer = m.createComponent() + .setImplementation(new ServiceConsumer(e)) + .add(m.createServiceDependency() + .setService(AdaptedService.class) + .setRequired(true) + ); + + ServiceAdapterCallbackInstance callbackInstance = new ServiceAdapterCallbackInstance(e); + Component adapter = m.createAdapterService(OriginalService.class, null, "m_originalService", + callbackInstance, "set", "changed","unset", null, true) + .setInterface(AdaptedService.class.getName(), null) + .setImplementation(new ServiceAdapter(e)); + + // add the provider and the adapter + m.add(provider); + m.add(adapter); + // Checks if the callbackInstances is called, and if the adapter start method is called + e.waitForStep(2, 5000); + + // add a consumer that will invoke the adapter + // which will in turn invoke the original provider + m.add(consumer); + // now validate that both have been invoked in the right order + e.waitForStep(4, 5000); + + // change the service properties of the provider, and check that the adapter callback instance is changed. + serviceProvider.changeServiceProperties(); + e.waitForStep(5, 5000); + + // remove the provider + m.remove(provider); + // ensure that the consumer is stopped, the adapter callback is called in its unset method, and the adapter is stopped. + e.waitForStep(8, 5000); + // remove adapter and consumer + m.remove(adapter); + m.remove(consumer); + } + + static interface OriginalService { + public void invoke(); + } + + static interface AdaptedService { + public void invoke(); + } + + static class ServiceProvider implements OriginalService { + private final Ensure m_ensure; + private volatile ServiceRegistration m_registration; // auto injected when started. + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void changeServiceProperties() { + Hashtable props = new Hashtable<>(); + props.put("foo", "bar"); + m_registration.setProperties(props); + } + public void invoke() { + m_ensure.step(4); + } + } + + public static class ServiceAdapter implements AdaptedService { + private volatile OriginalService m_originalService; + private final Ensure m_ensure; + + public ServiceAdapter(Ensure e) { + m_ensure = e; + } + + public void start() { m_ensure.step(2); } + public void stop() { m_ensure.step(7); } + public void invoke() { + m_originalService.invoke(); + } + } + + public static class ServiceAdapterCallbackInstance { + private final Ensure m_ensure; + public ServiceAdapterCallbackInstance(Ensure e) { + m_ensure = e; + } + + public void set(OriginalService m_originalService) { + m_ensure.step(1); + } + + public void changed(Map props, OriginalService m_originalService) { + Assert.assertEquals("bar", props.get("foo")); + m_ensure.step(5); + } + + public void unset(Map props, OriginalService m_originalService) { + m_ensure.step(8); + } + } + + static class ServiceConsumer { + private volatile AdaptedService m_service; + private final Ensure m_ensure; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + public void start() { + m_ensure.step(3); + m_service.invoke(); + } + public void stop() { + m_ensure.step(6); + } + } +} + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithConfigurationAndMetaType.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithConfigurationAndMetaType.java new file mode 100644 index 00000000000..68eedd6a1f1 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithConfigurationAndMetaType.java @@ -0,0 +1,183 @@ +/* + * 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.dm.itest.api; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.BundleContext; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.metatype.AttributeDefinition; +import org.osgi.service.metatype.MetaTypeInformation; +import org.osgi.service.metatype.MetaTypeService; +import org.osgi.service.metatype.ObjectClassDefinition; + +/** + * Tests an Adapter which adapts A To B interface. + * And the Adapter also depends on a Configuration Dependency with MetaType support. + * + * @author Felix Project Team + */ +public class AdapterWithConfigurationAndMetaType extends TestBase { + final static String PID = "AdapterWithConfigurationAndMetaType"; + final static String PID_HEADING = "English Dictionary"; + final static String PID_DESC = "Configuration for the EnglishDictionary Service"; + final static String WORDS_HEADING = "English words"; + final static String WORDS_DESC = "Declare here some valid English words"; + final static String WORDS_PROPERTY = "words"; + + public void testAdapterWithConfigurationDependencyAndMetaType() { + DependencyManager m = getDM(); + Ensure e = new Ensure(); + + m.add(m.createAdapterService(A.class, null) + .setInterface(B.class.getName(), null) + .setImplementation(new BImpl(e)) + .add(m.createConfigurationDependency() + .setPid(PID) + .setHeading(PID_HEADING) + .setDescription(PID_DESC) + .add(m.createPropertyMetaData() + .setCardinality(Integer.MAX_VALUE) + .setType(String.class) + .setHeading(WORDS_HEADING) + .setDescription(WORDS_DESC) + .setDefaults(new String[] {"hello", "world"}) + .setId(WORDS_PROPERTY)))); + + m.add(m.createComponent() + .setInterface(A.class.getName(), null) + .setImplementation(new AImpl())); + + Component configurator = m.createComponent() + .setImplementation(new Configurator(e)) + .add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)) + .add(m.createServiceDependency().setService(MetaTypeService.class).setRequired(true)); + m.add(configurator); + + // Ensures that all components are started + e.waitForStep(4, 5000); + + // now stop configurator, and ensure that all components have been stopped + m.remove(configurator); + e.waitForStep(7, 5000); + m.clear(); + } + + public interface A { + } + + public interface B { + } + + public class AImpl implements A { + } + + public class Configurator { + volatile MetaTypeService m_metaType; + volatile ConfigurationAdmin m_cm; + volatile BundleContext m_ctx; + final Ensure m_ensure; + Configuration m_conf; + + Configurator(Ensure ensure) { + m_ensure = ensure; + } + + void start() { + m_ensure.step(1); + checkMetaTypeAndConfigure(); + } + + void stop() { + m_ensure.step(5); + if (m_conf != null) { + try { + m_ensure.step(6); + m_conf.delete(); + } + catch (IOException e) { + m_ensure.throwable(e); + } + } + } + + void checkMetaTypeAndConfigure() { + MetaTypeInformation info = m_metaType.getMetaTypeInformation(m_ctx.getBundle()); + Assert.assertNotNull(info); + Assert.assertEquals(PID, info.getPids()[0]); + ObjectClassDefinition ocd = info.getObjectClassDefinition(PID, null); + Assert.assertNotNull(ocd); + Assert.assertEquals(PID_HEADING, ocd.getName()); + Assert.assertEquals(PID_DESC, ocd.getDescription()); + AttributeDefinition[] defs = ocd.getAttributeDefinitions(ObjectClassDefinition.ALL); + Assert.assertNotNull(defs); + Assert.assertEquals(1, defs.length); + Assert.assertEquals(WORDS_HEADING, defs[0].getName()); + Assert.assertEquals(WORDS_DESC, defs[0].getDescription()); + Assert.assertEquals(WORDS_PROPERTY, defs[0].getID()); + m_ensure.step(2); + + try { + m_conf = m_cm.getConfiguration(PID, null); + Hashtable props = new Hashtable<>(); + props.put("foo", "bar"); + m_conf.update(props); + } catch (Throwable t) { + m_ensure.throwable(t); + } + } + } + + public class BImpl implements B, A { + final Ensure m_ensure; + volatile A m_a; + Dictionary m_conf; + + public BImpl(Ensure e) { + m_ensure = e; + } + + public void updated(Dictionary conf) { + if (conf != null) { + m_ensure.step(3); + m_conf = conf; + } + } + + public void start() { + Assert.assertNotNull(m_a); + Assert.assertNotNull(m_conf); + Assert.assertEquals("bar", m_conf.get("foo")); + m_ensure.step(4); + } + + public void stop() { + m_ensure.step(7); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithExtraDependenciesTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithExtraDependenciesTest.java new file mode 100644 index 00000000000..a0c6fb1ca1a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithExtraDependenciesTest.java @@ -0,0 +1,106 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + + +/** + * @author Felix Project Team + */ +public class AdapterWithExtraDependenciesTest extends TestBase { + public void testAdapterWithExtraDependenciesAndCallbacks() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + // create a service adapter that adapts to services S1 and has an optional dependency on services S2 + Component sa = m.createAdapterService(S1.class, null) + .setImplementation(SA.class) + .add(m.createServiceDependency().setService(S2.class).setCallbacks("add", "remove")); + m.add(sa); + + // create a service S1, which triggers the creation of the first adapter instance (A1) + Component s1 = m.createComponent().setInterface(S1.class.getName(), null).setImplementation(new S1Impl()); + m.add(s1); + + // create a service S2, which will be added to A1 + Component s2 = m.createComponent().setInterface(S2.class.getName(), null).setImplementation(new S2Impl(e)); + m.add(s2); + + // create a second service S1, which triggers the creation of the second adapter instance (A2) + Component s1b = m.createComponent().setInterface(S1.class.getName(), null).setImplementation(new S1Impl()); + m.add(s1b); + + // observe that S2 is also added to A2 + e.waitForStep(2, 5000); + + // remove S2 again + m.remove(s2); + + // make sure both adapters have their "remove" callbacks invoked + e.waitForStep(4, 5000); + + m.remove(s1); + m.remove(sa); + m.clear(); + } + + static interface S1 { + } + static interface S2 { + public void invoke(); + } + static class S1Impl implements S1 { + } + static class S2Impl implements S2 { + + private final Ensure m_e; + + public S2Impl(Ensure e) { + m_e = e; + } + + public void invoke() { + m_e.step(); + } + } + + public static class SA { + volatile S2 s2; + + public SA() { + System.out.println("Adapter created"); + } + public void init() { + System.out.println("Adapter init " + s2); + } + public void add(S2 s) { + System.out.println("adding " + s); + s.invoke(); + } + public void remove(S2 s) { + System.out.println("removing " + s); + s.invoke(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithInstanceBoundDependencyParallelTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithInstanceBoundDependencyParallelTest.java new file mode 100644 index 00000000000..4fe3edd6042 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithInstanceBoundDependencyParallelTest.java @@ -0,0 +1,28 @@ +/* + * 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.dm.itest.api; + +/** + * @author Felix Project Team + */ +public class AdapterWithInstanceBoundDependencyParallelTest extends AdapterWithInstanceBoundDependencyTest { + public AdapterWithInstanceBoundDependencyParallelTest() { + setParallel(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithInstanceBoundDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithInstanceBoundDependencyTest.java new file mode 100644 index 00000000000..77645a347da --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithInstanceBoundDependencyTest.java @@ -0,0 +1,155 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + + +/** + * @author Felix Project Team + */ +public class AdapterWithInstanceBoundDependencyTest extends TestBase { + public void testInstanceBoundDependency() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = m.createComponent() + .setInterface(ServiceInterface.class.getName(), null) + .setImplementation(new ServiceProvider(e)); + Component sp2 = m.createComponent() + .setInterface(ServiceInterface2.class.getName(), null) + .setImplementation(new ServiceProvider2(e)); + Component sc = m.createComponent() + .setImplementation(new ServiceConsumer(e)) + .add(m.createServiceDependency() + .setService(ServiceInterface3.class) + .setRequired(true)); + Component sa = m.createAdapterService(ServiceInterface.class, null) + .setInterface(ServiceInterface3.class.getName(), null) + .setImplementation(new ServiceAdapter(e)); + m.add(sc); + m.add(sp); + m.add(sp2); + m.add(sa); + e.waitForStep(5, 15000); + // cleanup + m.remove(sa); + m.remove(sp2); + m.remove(sp); + m.remove(sc); + m.clear(); + e.waitForStep(9, 5000); // make sure all components are stopped + } + + static interface ServiceInterface { + public void invoke(); + } + + static interface ServiceInterface2 { + public void invoke(); + } + + static interface ServiceInterface3 { + public void invoke(); + } + + static class ServiceProvider2 implements ServiceInterface2 { + private final Ensure m_ensure; + + public ServiceProvider2(Ensure ensure) { + m_ensure = ensure; + } + + public void invoke() { + m_ensure.step(4); + } + + public void stop() { + m_ensure.step(); + } + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke() { + m_ensure.step(5); + } + public void stop() { + m_ensure.step(); + } + } + + static class ServiceAdapter implements ServiceInterface3 { + private Ensure m_ensure; + private volatile ServiceInterface m_originalService; + private volatile ServiceInterface2 m_injectedService; + private volatile Component m_service; + private volatile DependencyManager m_manager; + + public ServiceAdapter(Ensure e) { + m_ensure = e; + } + public void init() { + m_ensure.step(1); + m_service.add(m_manager.createServiceDependency().setRequired(true).setService(ServiceInterface2.class)); + } + public void start() { + m_ensure.step(2); + } + public void invoke() { + m_ensure.step(3); + m_injectedService.invoke(); + m_originalService.invoke(); + } + + public void stop() { + m_ensure.step(); + } + } + + static class ServiceConsumer implements Runnable { + volatile ServiceInterface3 m_service; + final Ensure m_ensure; + + ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void init() { + Thread t = new Thread(this); + t.start(); + } + + public void run() { + m_service.invoke(); + } + public void stop() { + m_ensure.step(); + } + } +} + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithModifiedInstanceBoundDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithModifiedInstanceBoundDependencyTest.java new file mode 100644 index 00000000000..46ecf74a875 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithModifiedInstanceBoundDependencyTest.java @@ -0,0 +1,155 @@ +/* + * 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.dm.itest.api; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * Test for FELIX-4334 issue. + * + * Three components: A, B and C + * + * - A provided with property foo=bar + * - B adapts A, B has no filters on A, and B.init() method adds an instance bound required dependency to C. + * - C depends on A(foo=bar) + * - Now someone modifies the service properties of A: foo=bar2 + * - As a result of that, C becomes unavailable and is unbound from B. + * - Since B has an instance bound required dependency to C: B should not be destroyed: it should be called in B.stop(), B.remove(C), B.change(A, "foo=bar2)) + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class AdapterWithModifiedInstanceBoundDependencyTest extends TestBase { + public static interface A { + } + + static class AImpl implements A { + final Ensure m_e; + AImpl(Ensure e) { + m_e = e; + } + } + + public static interface C { + } + + static class CImpl implements C { + volatile A m_a; + } + + public static interface B { + } + + static class BImpl implements B { + final Ensure m_e; + volatile A m_a; + volatile C m_c; + + BImpl(Ensure e) { + m_e = e; + } + + public void add(A a) { + m_e.step(1); + } + + void init(Component c) { + m_e.step(2); + DependencyManager dm = c.getDependencyManager(); + c.add(dm.createServiceDependency().setService(C.class).setRequired(true).setCallbacks("add", "remove")); + } + + public void add(C c) { + m_e.step(3); + } + + public void start() { + m_e.step(4); + } + + public void stop() { // C becomes unsatisfied when A properties are changed to foo=bar2 + m_e.step(5); + } + + public void remove(C c) { + m_e.step(6); + } + + public void change(Map properties, A a) { + Assert.assertEquals("bar2", properties.get("foo")); + m_e.step(7); + } + + public void destroy() { + m_e.step(8); + } + + public void remove(A a) { + m_e.step(9); + } + } + + public void testAdapterWithChangedInstanceBoundDependency() { + DependencyManager m = getDM(); + Ensure e = new Ensure(); + + Dictionary props = new Hashtable(); + props.put("foo", "bar"); + Component a = m.createComponent() + .setImplementation(new AImpl(e)) + .setInterface(A.class.getName(), props); + + Component b = m.createAdapterService(A.class, null, "add", "change", "remove") + .setInterface(B.class.getName(), null) + .setImplementation(new BImpl(e)); + + Component c = m.createComponent() + .setImplementation(new CImpl()) + .setInterface(C.class.getName(), null) + .add(m.createServiceDependency().setService(A.class, "(foo=bar)").setRequired(true)); + + m.add(a); + m.add(c); + m.add(b); + + e.waitForStep(4, 5000); + + System.out.println("changing A props ..."); + props = new Hashtable(); + props.put("foo", "bar2"); + a.setServiceProperties(props); + + e.waitForStep(7, 5000); + + m.remove(c); + m.remove(a); + m.remove(b); + + e.waitForStep(9, 5000); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithPropagationTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithPropagationTest.java new file mode 100644 index 00000000000..29ed79ec88a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithPropagationTest.java @@ -0,0 +1,141 @@ +/* + * 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.dm.itest.api; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * Checks if a service adapter propagates its service properties, if + * the adapted service properties are changed: + * + * S1Impl provides S + * S1Adapter adapts S1Impl(S) to S2 + * S3 depends on S2 + * + * So, when S1Impl service properties are changed, S1Adapter shall propagate the changed properties to S3. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class AdapterWithPropagationTest extends TestBase { + public static interface S1 {} + + static class S1Impl implements S1 { + private Ensure m_ensure; + public S1Impl(Ensure e) { + m_ensure = e; + } + + public void start() { + m_ensure.step(1); + } + } + + public static interface S2 {} + + static class S1Adapter implements S2 { + private Ensure m_ensure; + public S1Adapter(Ensure e) { + m_ensure = e; + } + + public void add(Map properties, S1 s1) { + Assert.assertTrue("v1".equals(properties.get("p1"))); + Assert.assertTrue("v2overriden".equals(properties.get("p2"))); + m_ensure.step(2); + } + + public void change(Map properties, S1 s1) { + Assert.assertTrue("v1modified".equals(properties.get("p1"))); + Assert.assertTrue("v2overriden".equals(properties.get("p2"))); + m_ensure.step(4); + } + } + + static class S3 { + private final Ensure m_ensure; + + public S3(Ensure e) { + m_ensure = e; + } + + public void add(Map properties, S2 s2) { + Assert.assertTrue("v1".equals(properties.get("p1"))); + Assert.assertTrue("v2".equals(properties.get("p2"))); // s1 should not override adapter service properties + m_ensure.step(3); + } + + public void change(Map properties, S2 s2) { + Assert.assertTrue("v1modified".equals(properties.get("p1"))); + Assert.assertTrue("v2".equals(properties.get("p2"))); // s1 should not override adapter service properties + m_ensure.step(5); + } + } + + public void testAdapterWithPropagation() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + Dictionary s1Properties = new Hashtable(); + s1Properties.put("p1", "v1"); + s1Properties.put("p2", "v2overriden"); // should not override adapter + Component s1 = m.createComponent() + .setImplementation(new S1Impl(e)) + .setInterface(S1.class.getName(), s1Properties); + + Dictionary s1AdapterProperties = new Hashtable(); + s1AdapterProperties.put("p2", "v2"); + Component s1Adapter = m.createAdapterService(S1.class, null, "add", "change", null) + .setInterface(S2.class.getName(), s1AdapterProperties) + .setImplementation(new S1Adapter(e)); + + Component s3 = m.createComponent() + .setImplementation(new S3(e)) + .add(m.createServiceDependency() + .setService(S2.class) + .setRequired(true) + .setCallbacks("add", "change", null)); + + + m.add(s1); + m.add(s1Adapter); + m.add(s3); + + e.waitForStep(3, 5000); + + s1Properties = new Hashtable(); + s1Properties.put("p1", "v1modified"); + s1Properties.put("p2", "v2overriden"); + s1.setServiceProperties(s1Properties); + + e.waitForStep(5, 5000); + + m.clear(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithoutPropagationTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithoutPropagationTest.java new file mode 100644 index 00000000000..d50bbd39bc2 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AdapterWithoutPropagationTest.java @@ -0,0 +1,137 @@ +/* + * 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.dm.itest.api; + +import java.util.Dictionary; +import java.util.Hashtable; + +import org.junit.Assert; +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.ServiceRegistration; + +/** + * @author Felix Project Team + */ +public class AdapterWithoutPropagationTest extends TestBase { + + public void testAdapterNoPropagate() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + // The provider has a "foo=bar" property + Hashtable props = new Hashtable<>(); + props.put("foo", "bar"); + ServiceProvider serviceProvider = new ServiceProvider(e); + Component provider = m.createComponent() + .setInterface(OriginalService.class.getName(), props).setImplementation(serviceProvider); + + // The Adapter will see the "foo=bar" property from the adaptee + Component adapter = m.createAdapterService(OriginalService.class, null, null, + null, "set", "change", null, null, false) + .setInterface(AdaptedService.class.getName(), null) + .setImplementation(new ServiceAdapter(e)); + + // The consumer depends on the AdaptedService, but won't see foo=bar property from the adaptee + Component consumer = m.createComponent() + .setImplementation(new ServiceConsumer(e)) + .add(m.createServiceDependency() + .setService(AdaptedService.class) + .setRequired(true) + .setCallbacks("set", "change", null) + ); + + // add the provider and the adapter + m.add(provider); + m.add(adapter); + // Checks if the adapter has been started and has seen the adaptee properties + e.waitForStep(1, 5000); + + // add a consumer that must not see the adaptee service properties + m.add(consumer); + e.waitForStep(2, 5000); + + // change the service properties of the provider, and check that the adapter callback instance is caled. + serviceProvider.changeServiceProperties(); + e.waitForStep(3, 5000); + + // cleanup + m.clear(); + } + + static interface OriginalService { + } + + static interface AdaptedService { + } + + static class ServiceProvider implements OriginalService { + private volatile ServiceRegistration m_registration; // auto injected when started. + public ServiceProvider(Ensure e) { + } + public void changeServiceProperties() { + Hashtable props = new Hashtable<>(); + props.put("foo", "bar2"); + m_registration.setProperties(props); + } + } + + public static class ServiceAdapter implements AdaptedService { + private final Ensure m_ensure; + + public ServiceAdapter(Ensure e) { + m_ensure = e; + } + + public void set(OriginalService adaptee, Dictionary props) { + Assert.assertEquals("bar", props.get("foo")); + m_ensure.step(1); + } + + void change(OriginalService adapted, Dictionary props) { + Assert.assertEquals("bar2", props.get("foo")); + m_ensure.step(3); + } + } + + static class ServiceConsumer { + @SuppressWarnings("unused") + private volatile AdaptedService m_service; + private final Ensure m_ensure; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + void set(AdaptedService adapted, Dictionary props) { + Assert.assertNull(props.get("foo")); + m_ensure.step(2); + } + + void change(AdaptedService adapted, Dictionary props) { + Assert.assertNull(props.get("foo")); + Assert.fail("Change callback should not be called"); + } + } +} + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectBaseTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectBaseTest.java new file mode 100644 index 00000000000..e4d2d73bec0 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectBaseTest.java @@ -0,0 +1,227 @@ +/* + * 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.dm.itest.api; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.Properties; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.ServiceUtil; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; + +/** + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes", "unused"}) +public class AspectBaseTest extends TestBase { + + public void testSingleAspect() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + // create a service provider and consumer + ServiceProvider p = new ServiceProvider(e, "a"); + ServiceConsumer c = new ServiceConsumer(e); + Hashtable props = new Hashtable<>(); + props.put("name", "a"); + Component sp = m.createComponent() + .setInterface(ServiceInterface.class.getName(), props) + .setImplementation(p); + Component sc = m.createComponent() + .setImplementation(c) + .add(m.createServiceDependency() + .setService(ServiceInterface.class) + .setRequired(true) + .setCallbacks("add", "remove") + .setAutoConfig("m_service") + ); + Component sa = m.createAspectService(ServiceInterface.class, null, 20, null) + .setImplementation(ServiceAspect.class); + m.add(sc); + m.add(sp); + // after the provider was added, the consumer's add should have been invoked once + e.waitForStep(1, 2000); + Assert.assertEquals("a", c.invoke()); + m.add(sa); + // after the aspect was added, the consumer should get and add for the aspect and a remove + // for the original service + e.waitForStep(3, 2000); + Assert.assertEquals("aa", c.invoke()); + m.remove(sa); + // removing the aspect again should give a remove and add + e.waitForStep(5, 2000); + Assert.assertEquals("a", c.invoke()); + m.remove(sp); + // finally removing the original service should give a remove + e.waitForStep(6, 2000); + m.remove(sc); + e.step(7); + } + + @SuppressWarnings("serial") + public void testSingleAspectThatAlreadyExisted() { + DependencyManager m = new DependencyManager(context); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + // create a service provider and consumer + ServiceProvider p = new ServiceProvider(e, "a"); + ServiceConsumer c = new ServiceConsumer(e); + Component sp = m.createComponent().setImplementation(p).setInterface(ServiceInterface.class.getName(), new Hashtable() {{ put("name", "a"); }}); + Component sc = m.createComponent().setImplementation(c).add(m.createServiceDependency().setService(ServiceInterface.class).setRequired(true).setCallbacks("add", "remove").setAutoConfig("m_service")); + Component sa = m.createAspectService(ServiceInterface.class, null, 20, null).setImplementation(ServiceAspect.class); + // we first add the aspect + m.add(sa); + // then the service provider + m.add(sp); + // finally the consumer + m.add(sc); + + Assert.assertEquals("aa", c.invoke()); + + // now the consumer's added should be invoked once, as the aspect is already available and should + // directly hide the original service + e.waitForStep(1, 2000); + e.step(2); + + m.remove(sa); + // after removing the aspect, the consumer should get the original service back, so + // remove and add will be invoked + e.waitForStep(4, 2000); + + Assert.assertEquals("a", c.invoke()); + + m.remove(sp); + // after removing the original service, the consumer's remove should be called once + e.waitForStep(5, 2000); + + m.remove(sc); + e.step(6); + } + + @SuppressWarnings("serial") + public void testMultipleAspects() { + DependencyManager m = new DependencyManager(context); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + // create service providers and consumers + ServiceConsumer c = new ServiceConsumer(e); + Component sp = m.createComponent().setImplementation(new ServiceProvider(e, "a")).setInterface(ServiceInterface.class.getName(), new Hashtable() {{ put("name", "a"); }}); + Component sp2 = m.createComponent().setImplementation(new ServiceProvider(e, "b")).setInterface(ServiceInterface.class.getName(), new Hashtable() {{ put("name", "b"); }}); + Component sc = m.createComponent().setImplementation(c).add(m.createServiceDependency().setService(ServiceInterface.class).setRequired(true).setCallbacks("add", "remove")); + Component sa = m.createAspectService(ServiceInterface.class, null, 20, null).setImplementation(ServiceAspect.class); + Component sa2 = m.createAspectService(ServiceInterface.class, null, 10, null).setImplementation(ServiceAspect.class); + m.add(sp); + m.add(sp2); + m.add(sa); + m.add(sa2); + m.add(sc); + // the consumer will monitor progress, it should get it's add invoked twice, once for every + // (highest) aspect + e.waitForStep(2, 2000); + e.step(3); + + // now invoke all services the consumer collected + List list = c.invokeAll(); + // and make sure both of them are correctly invoked + Assert.assertTrue(list.size() == 2); + Assert.assertTrue(list.contains("aaa")); + Assert.assertTrue(list.contains("bbb")); + + m.remove(sc); + // removing the consumer now should get its removed method invoked twice + e.waitForStep(5, 2000); + e.step(6); + m.remove(sa2); + m.remove(sa); + m.remove(sp2); + m.remove(sp); + e.step(7); + } + + public static interface ServiceInterface { + public String invoke(String input); + } + + public static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + private final String m_name; + public ServiceProvider(Ensure e, String name) { + m_ensure = e; + m_name = name; + } + public String invoke(String input) { + return input + m_name; + } + } + + public static class ServiceAspect implements ServiceInterface { + private volatile ServiceInterface m_originalService; + private volatile ServiceRegistration m_registration; + + public String invoke(String input) { + String result = m_originalService.invoke(input); + String property = (String) m_registration.getReference().getProperty("name"); + return result + property; + } + } + + public static class ServiceConsumer { + private final Ensure m_ensure; + private volatile ServiceInterface m_service; + private List m_services = new ArrayList(); + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void add(ServiceReference ref, ServiceInterface si) { + System.out.println("add: " + ServiceUtil.toString(ref)); + m_services.add(si); + m_ensure.step(); + } + + public void remove(ServiceReference ref, ServiceInterface si) { + System.out.println("rem: " + ServiceUtil.toString(ref)); + m_services.remove(si); + m_ensure.step(); + } + + public String invoke() { + return m_service.invoke(""); + } + + public List invokeAll() { + List results = new ArrayList(); + for (ServiceInterface si : m_services) { + results.add(si.invoke("")); + } + return results; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectChainTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectChainTest.java new file mode 100644 index 00000000000..4d524fd88e5 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectChainTest.java @@ -0,0 +1,111 @@ +/* + * 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.dm.itest.api; +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * @author Felix Project Team + */ +public class AspectChainTest extends TestBase { + + public void testBuildAspectChain() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), null); + Component sc = m.createComponent().setImplementation(new ServiceConsumer(e)).add(m.createServiceDependency().setService(ServiceInterface.class).setRequired(true)); + Component sa2 = m.createAspectService(ServiceInterface.class, null, 20, null).setImplementation(new ServiceAspect(e, 3)); + Component sa3 = m.createAspectService(ServiceInterface.class, null, 30, null).setImplementation(new ServiceAspect(e, 2)); + Component sa1 = m.createAspectService(ServiceInterface.class, null, 10, null).setImplementation(new ServiceAspect(e, 4)); + m.add(sc); + + m.add(sp); + m.add(sa2); + m.add(sa3); + m.add(sa1); + e.step(); + e.waitForStep(5, 5000); + + m.remove(sa3); + m.remove(sa2); + m.remove(sa1); + m.remove(sp); + + m.remove(sc); + } + + static interface ServiceInterface { + public void invoke(Runnable run); + } + + static class ServiceProvider implements ServiceInterface { + @SuppressWarnings("unused") + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke(Runnable run) { + run.run(); + } + } + + static class ServiceAspect implements ServiceInterface { + private final Ensure m_ensure; + private volatile ServiceInterface m_parentService; + private final int m_step; + + public ServiceAspect(Ensure e, int step) { + m_ensure = e; + m_step = step; + } + public void start() { + } + + public void invoke(Runnable run) { + m_ensure.step(m_step); + m_parentService.invoke(run); + } + + public void stop() { + } + } + + static class ServiceConsumer implements Runnable { + private volatile ServiceInterface m_service; + private final Ensure m_ensure; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void init() { + Thread t = new Thread(this); + t.start(); + } + + public void run() { + m_ensure.waitForStep(1, 2000); + m_service.invoke(Ensure.createRunnableStep(m_ensure, 5)); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectDynamicsTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectDynamicsTest.java new file mode 100644 index 00000000000..7177edb094f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectDynamicsTest.java @@ -0,0 +1,194 @@ +/* + * 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.dm.itest.api; +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * @author Felix Project Team + */ +public class AspectDynamicsTest extends TestBase { + + public void testDynamicallyAddAndRemoveAspect() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + Ensure aspectStopEnsure = new Ensure(); + // create a service provider and consumer + Component provider = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), null); + Component provider2 = m.createComponent().setImplementation(new ServiceProvider2(e)).setInterface(ServiceInterface2.class.getName(), null); + Component consumer = m.createComponent().setImplementation(new ServiceConsumer(e)) + .add(m.createServiceDependency().setService(ServiceInterface.class).setRequired(true).setCallbacks("add", null, null, "swap")); + Component aspect = m.createAspectService(ServiceInterface.class, null, 1, null).setImplementation(new ServiceAspect(e, aspectStopEnsure)); + + m.add(consumer); + m.add(provider); + // the consumer should invoke the provider here, and when done, arrive at step 3 + // finally wait for step 6 before continuing + e.waitForStep(3, 15000); + + m.add(aspect); + // after adding the aspect, we wait for its init to be invoked, arriving at + // step 4 after an instance bound dependency was added (on a service provided by + // provider 2) + e.waitForStep(4, 15000); + + m.add(provider2); + + // after adding provider 2, we should now see the client being swapped, so + // we wait for step 5 to happen + e.waitForStep(5, 15000); + + // now we continue with step 6, which will trigger the next part of the consumer's + // run method to be executed + e.step(6); + + // invoking step 7, 8 and 9 when invoking the aspect which in turn invokes the + // dependency and the original service, so we wait for that to finish here, which + // is after step 10 has been reached (the client will now wait for step 12) + e.waitForStep(10, 15000); + + m.remove(aspect); + aspectStopEnsure.waitForStep(1, 15000); + // removing the aspect should trigger step 11 (in the swap method of the consumer) + e.waitForStep(11, 15000); + + // step 12 triggers the client to continue + e.step(12); + + // wait for step 13, the final invocation of the provided service (without aspect) + e.waitForStep(13, 15000); + + // clean up + m.remove(provider2); + m.remove(provider); + m.remove(consumer); + e.waitForStep(16, 15000); + m.clear(); + } + + static interface ServiceInterface { + public void invoke(Runnable run); + } + + static interface ServiceInterface2 { + public void invoke(); + } + + static class ServiceProvider2 implements ServiceInterface2 { + private final Ensure m_ensure; + + public ServiceProvider2(Ensure ensure) { + m_ensure = ensure; + } + + public void invoke() { + m_ensure.step(9); + } + public void stop() { + m_ensure.step(); + } + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke(Runnable run) { + run.run(); + } + public void stop() { + m_ensure.step(); + } + } + + static class ServiceAspect implements ServiceInterface { + private final Ensure m_ensure; + private volatile ServiceInterface m_originalService; + private volatile ServiceInterface2 m_injectedService; + private volatile Component m_service; + private volatile DependencyManager m_manager; + private final Ensure m_stopEnsure; + + public ServiceAspect(Ensure e, Ensure stopEnsure) { + m_ensure = e; + m_stopEnsure = stopEnsure; + } + public void init() { + m_service.add(m_manager.createServiceDependency() + .setService(ServiceInterface2.class) + .setRequired(true) + ); + m_ensure.step(4); + } + + public void invoke(Runnable run) { + m_ensure.step(7); + m_originalService.invoke(run); + m_injectedService.invoke(); + } + + public void stop() { + m_stopEnsure.step(1); + } + } + + static class ServiceConsumer implements Runnable { + private volatile ServiceInterface m_service; + private final Ensure m_ensure; + private final Ensure.Steps m_swapSteps = new Ensure.Steps(5, 11); + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + void add(ServiceInterface service) { + m_service = service; + } + + void swap(ServiceInterface oldService, ServiceInterface newService) { + System.out.println("swap: old=" + oldService + ", new=" + newService); + m_ensure.steps(m_swapSteps); + m_service = newService; + } + + public void init() { + Thread t = new Thread(this); + t.start(); + } + + public void run() { + m_ensure.step(1); + m_service.invoke(Ensure.createRunnableStep(m_ensure, 2)); + m_ensure.step(3); + m_ensure.waitForStep(6, 15000); + m_service.invoke(Ensure.createRunnableStep(m_ensure, 8)); + m_ensure.step(10); + m_ensure.waitForStep(12, 15000); + m_service.invoke(Ensure.createRunnableStep(m_ensure, 13)); + } + + public void stop() { + m_ensure.step(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectNotAlwaysInjectedWithRequiredDependencies.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectNotAlwaysInjectedWithRequiredDependencies.java new file mode 100644 index 00000000000..d7c08edeea8 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectNotAlwaysInjectedWithRequiredDependencies.java @@ -0,0 +1,97 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ServiceDependency; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * @author Felix Project Team + */ +public class AspectNotAlwaysInjectedWithRequiredDependencies extends TestBase { + final Ensure m_e = new Ensure(); + + public void testAspectNotAlwaysInjectedWithRequiredDependency() { + DependencyManager m = getDM(); + + Component orig = m.createComponent() + .setImplementation(new OriginalServiceImpl()) + .setInterface(OriginalService.class.getName(), null); + + Component aspect = m.createAspectService(OriginalService.class, null, 10) + .setImplementation(new Aspect()); + + m.add(orig); + m.add(aspect); + + m_e.waitForStep(2, 5000); + + Component aspectDep = m.createComponent() + .setImplementation(new AspectDependency()) + .setInterface(AspectDependency.class.getName(), null); + m.add(aspectDep); + m_e.waitForStep(3, 5000); + ServiceDependency sd = m.createServiceDependency().setService(AspectDependency.class).setRequired(true).setCallbacks("add", "remove"); + aspect.add(sd); + m_e.waitForStep(4, 5000); + aspect.remove(sd); + m_e.waitForStep(5, 5000); + m.remove(aspect); + m_e.waitForStep(6, 5000); + } + + public interface OriginalService {} + + public class OriginalServiceImpl implements OriginalService { + void start() { + m_e.step(1); + } + } + + class AspectDependency { + void start() { + m_e.step(3); + } + } + + public class Aspect implements OriginalService { + void add(AspectDependency dep) { + m_e.step(4); + } + + void remove(AspectDependency dep) { + m_e.step(5); + } + + + void start() { + m_e.step(2); + } + + void stop() { + m_e.step(6); + } + } + +} + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectRaceParallelTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectRaceParallelTest.java new file mode 100644 index 00000000000..1559a75e080 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectRaceParallelTest.java @@ -0,0 +1,28 @@ +/* + * 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.dm.itest.api; + +/** + * @author Felix Project Team + */ +public class AspectRaceParallelTest extends AspectRaceTest { + public AspectRaceParallelTest() { + setParallel(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectRaceTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectRaceTest.java new file mode 100644 index 00000000000..df4f4e52282 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectRaceTest.java @@ -0,0 +1,323 @@ +/* + * 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.dm.itest.api; + +import java.util.HashSet; +import java.util.Hashtable; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; + +/** + * This class validates that some aspect aware services are correctly managed and ordered when components and aspects are + * registered concurrently. + * + * By default, this class uses a custom threadpool, but a subclass may override this class and call "setParallel()" method, in + * this case we won't use any threadpool, since calling setParallel() method means we are using a parallel Dependency Manager. + * + * @see AspectRaceParallelTest + * @author Felix Project Team + */ +public class AspectRaceTest extends TestBase { + final static int SERVICES = 3; + final static int ASPECTS_PER_SERVICE = 10; + final static int ITERATIONS = 1000; + final AtomicInteger m_IDGenerator = new AtomicInteger(); + + public void testConcurrentAspects() { + try { + warn("starting aspect race test"); + initThreadPool(); // only if setParallel() has not been called (only if a parallel DM is not used). + + for (int loop = 1; loop <= ITERATIONS; loop++) { + // Perform concurrent injections of "S" service and S aspects into the Controller component; + debug("Iteration: " + loop); + + // Use a helper class to wait for components to be started/stopped. + int count = 1 /* for controller */ + SERVICES + (SERVICES * ASPECTS_PER_SERVICE); + ComponentTracker tracker = new ComponentTracker(count, count); + + // Create the components (controller / services / aspects) + Controller controller = new Controller(); + Factory f = new Factory(); + f.createComponents(controller, tracker); + + // Activate the components asynchronously + f.registerComponents(); + + // Wait for the components to be started (using the tracker) + if (!tracker.awaitStarted(5000)) { + throw new IllegalStateException("Could not start components timely."); + } + + // Check aspect chains consistency. + controller.checkConsistency(); + + // unregister all services and aspects. + f.unregisterComponents(); + + // use component tracker to wait for all components to be stopped. + if (!tracker.awaitStopped(5000)) { + throw new IllegalStateException("Could not stop components timely."); + } + + m_threadPool.awaitQuiescence(5000, TimeUnit.MILLISECONDS); + + if ((loop) % 50 == 0) { + warn("Performed " + loop + " tests."); + } + + if (super.errorsLogged()) { + throw new IllegalStateException("Race test interrupted (some error occured, see previous logs)"); + } + } + } + + catch (Throwable t) { + error("Test failed", t); + Assert.fail("Test failed: " + t.getMessage()); + } finally { + m_dm.clear(); + shutdownThreadPool(); + } + } + + private void initThreadPool() { + // Create a threadpool only if setParallel() method has not been called. + if (! m_parallel) { + int cores = Math.max(16, Runtime.getRuntime().availableProcessors()); + m_threadPool = new ForkJoinPool(cores); + } + } + + void shutdownThreadPool() { + if (! m_parallel && m_threadPool != null) { + m_threadPool.shutdown(); + try { + m_threadPool.awaitTermination(60, TimeUnit.SECONDS); + } catch (InterruptedException e) { + } + } + } + + public interface S { + void invoke(Ensure e); + + int getRank(); + } + + public static class SImpl implements S { + + SImpl() { + } + + public void invoke(Ensure e) { + e.step(1); + } + + public String toString() { + return "SImpl"; + } + + @Override + public int getRank() { + return Integer.MIN_VALUE; + } + } + + public class SAspect implements S { + volatile S m_next; + final int m_rank; + volatile Component m_component; + + SAspect(int rank) { + m_rank = rank; + } + + public synchronized void added(S s) { + debug("aspect.added: this rank=%d, next rank=%d", getRank(), s.getRank()); + m_next = s; + } + + public synchronized void swap(S oldS, S newS) { + debug("aspect.swap: this rank=%d, old rank=%d, next rank=%d", getRank(), oldS.getRank(), newS.getRank()); + m_next = newS; + } + + public synchronized void removed(S s) { + debug("aspect.remove: this rank=%d, removed rank=%d", getRank(), s.getRank()); + m_next = null; + } + + public synchronized void invoke(Ensure e) { + debug("aspect.invoke: this rank=%d, next rank=%d", this.getRank(), m_next.getRank()); + Assert.assertTrue(m_rank > m_next.getRank()); + m_next.invoke(e); + } + + public String toString() { + return "[Aspect/rank=" + m_rank + "], next=" + + ((m_next != null) ? m_next : "null"); + } + + @Override + public int getRank() { + return m_rank; + } + } + + class Factory { + int m_serviceId; + Component m_controller; + final ConcurrentLinkedQueue m_services = new ConcurrentLinkedQueue(); + final ConcurrentLinkedQueue m_aspects = new ConcurrentLinkedQueue(); + + private void createComponents(Controller controller, ComponentTracker tracker) { + // create the controller + int controllerID = m_IDGenerator.incrementAndGet(); + m_controller = m_dm.createComponent() + .setImplementation(controller) + .setComposition("getComposition") + .add(tracker); + for (int i = 0; i < SERVICES; i ++) { + m_controller.add(m_dm.createServiceDependency() + .setService(S.class, "(controller.id=" + controllerID + ")") + .setCallbacks("bind", null, "unbind", "swap") + .setRequired(true)); + } + + // create the services + for (int i = 1; i <= SERVICES; i++) { + int aspectId = m_IDGenerator.incrementAndGet(); + Component s = m_dm.createComponent(); + Hashtable props = new Hashtable(); + props.put("controller.id", String.valueOf(controllerID)); + props.put("aspect.id", String.valueOf(aspectId)); + s.setInterface(S.class.getName(), props) + .setImplementation(new SImpl()); + s.add(tracker); + m_services.add(s); + + // create the aspects for that service + for (int j = 1; j <= ASPECTS_PER_SERVICE; j++) { + final int rank = j; + SAspect sa = new SAspect(rank); + Component a = + m_dm.createAspectService(S.class, "(aspect.id=" + aspectId + ")", rank, "added", null, "removed", "swap") + .setImplementation(sa); + a.add(tracker); + m_aspects.add(a); + } + } + } + + public void registerComponents() { + // If setParallel() has been called (we are using a parallel dependency manager), then no needs to use a custom thread pool. + if (m_parallel) { // using a parallel DM. + for (final Component s : m_services) { + m_dm.add(s); + } + m_dm.add(m_controller); + for (final Component a : m_aspects) { + m_dm.add(a); + } + } else { + for (final Component s : m_services) { + m_threadPool.execute(new Runnable() { + public void run() { + m_dm.add(s); + } + }); + } + m_threadPool.execute(new Runnable() { + public void run() { + m_dm.add(m_controller); + } + }); + for (final Component a : m_aspects) { + m_threadPool.execute(new Runnable() { + public void run() { + m_dm.add(a); + } + }); + } + } + } + + public void unregisterComponents() throws InterruptedException, InvalidSyntaxException { + m_dm.remove(m_controller); + for (final Component s : m_services) { + m_dm.remove(s); + } + for (final Component a : m_aspects) { + m_dm.remove(a); + } + } + } + + public class Controller { + final Composition m_compo = new Composition(); + final HashSet m_services = new HashSet(); + + Object[] getComposition() { + return new Object[] { this, m_compo }; + } + + synchronized void bind(ServiceReference sr, Object service) { + debug("controller.bind: %s", service); + S s = (S) service; + m_services.add(s); + debug("bind: service count after bind: %d", m_services.size()); + } + + synchronized void swap(S previous, S current) { + debug("controller.swap: previous=%s, current=%s", previous, current); + if (!m_services.remove(previous)) { + debug("swap: unknow previous service: " + previous); + } + m_services.add(current); + debug("controller.swap: service count after swap: %d", m_services.size()); + } + + synchronized void unbind(S a) { + debug("unbind " + a); + m_services.remove(a); + } + + synchronized void checkConsistency() { + debug("service count: %d", m_services.size()); + for (S s : m_services) { + info("checking service: %s", s); + Ensure ensure = new Ensure(false); + s.invoke(ensure); + } + } + } + + public static class Composition { + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectServiceDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectServiceDependencyTest.java new file mode 100644 index 00000000000..365ee312bf3 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectServiceDependencyTest.java @@ -0,0 +1,122 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + + +/** + * @author Felix Project Team + */ +public class AspectServiceDependencyTest extends TestBase { + public void testServiceRegistrationAndConsumption() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), null); + Component sc = m.createComponent().setImplementation(new ServiceConsumer(e)).add(m.createServiceDependency() + .setService(ServiceInterface.class) + .setCallbacks("add", "remove") + .setRequired(true)); + Component asp = m.createAspectService(ServiceInterface.class, null, 100) + .setImplementation(ServiceProviderAspect.class); + m.add(sp); + m.add(sc); + m.add(asp); + m.remove(asp); + m.remove(sc); + m.remove(sp); + + // ensure we executed all steps inside the component instance + e.step(8); + } + + static interface ServiceInterface { + public void invoke(String caller); + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke(String caller) { + if (caller.equals("consumer.init")) { + m_ensure.step(3); + } else if (caller.equals("aspect.consumer.add")) { + m_ensure.step(5); + } + } + } + + static class ServiceProviderAspect implements ServiceInterface { + private volatile ServiceInterface m_service; + + public ServiceProviderAspect() { + } + + @Override + public void invoke(String caller) { + m_service.invoke("aspect." + caller); + } + } + + static class ServiceConsumer { + private volatile ServiceInterface m_service; + private final Ensure m_ensure; + private int addCount = 0; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void init() { + m_ensure.step(2); + m_service.invoke("consumer.init"); + } + + public void destroy() { + m_ensure.step(7); + } + + public void add(ServiceInterface service) { + m_service = service; + switch (addCount) { + case 0: m_ensure.step(1); + break; + case 1: m_ensure.step(4); + // aspect had been added + m_service.invoke("consumer.add"); + break; + case 2: m_ensure.step(6); + break; + default: + } + addCount ++; + } + public void remove(ServiceInterface service) { + m_service = null; + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectServiceDependencyWithSwapCallbackTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectServiceDependencyWithSwapCallbackTest.java new file mode 100644 index 00000000000..03ce7c1563f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectServiceDependencyWithSwapCallbackTest.java @@ -0,0 +1,124 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + + +/** + * @author Felix Project Team + */ +public class AspectServiceDependencyWithSwapCallbackTest extends TestBase { + public void testServiceRegistrationAndConsumption() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), null); + Component sc = m.createComponent().setImplementation(new ServiceConsumer(e)).add(m.createServiceDependency() + .setService(ServiceInterface.class) + .setCallbacks("add", null, "remove", "swap") + .setRequired(true)); + Component asp = m.createAspectService(ServiceInterface.class, null, 100) + .setImplementation(ServiceProviderAspect.class); + m.add(sp); + m.add(sc); + m.add(asp); + m.remove(asp); + m.remove(sc); + m.remove(sp); + + // ensure we executed all steps inside the component instance + e.step(7); + } + + static interface ServiceInterface { + public void invoke(String caller); + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke(String caller) { + if (caller.equals("consumer.init")) { + m_ensure.step(3); + } else if (caller.equals("aspect.consumer.add")) { + m_ensure.step(5); + } + } + } + + static class ServiceProviderAspect implements ServiceInterface { + private volatile ServiceInterface m_service; + + public ServiceProviderAspect() { + } + + @Override + public void invoke(String caller) { + m_service.invoke("aspect." + caller); + } + } + + static class ServiceConsumer { + private volatile ServiceInterface m_service; + private final Ensure m_ensure; + private int swapCount = 0; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void init() { + m_ensure.step(2); + m_service.invoke("consumer.init"); + } + + public void destroy() { + m_ensure.step(6); + } + + public void add(ServiceInterface service) { + m_service = service; + m_ensure.step(1); + } + + public void remove(ServiceInterface service) { + m_service = null; + } + + public void swap(ServiceInterface previous, ServiceInterface current) { + switch (swapCount) { + case 0: m_ensure.step(4); + break; + case 1: m_ensure.step(5); + break; + default: + } + m_service = current; + swapCount ++; + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectWhiteboardTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectWhiteboardTest.java new file mode 100644 index 00000000000..4e4181de0b8 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectWhiteboardTest.java @@ -0,0 +1,201 @@ +/* + * 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.dm.itest.api; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.ServiceUtil; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + +/** + * @author Felix Project Team + */ +@SuppressWarnings({"rawtypes", "unchecked", "unused"}) +public class AspectWhiteboardTest extends TestBase { + + public void testWhiteboardConsumer() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create service providers and consumer + Component sp1 = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), null); + Component sp2 = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), null); + ServiceConsumer sci = new ServiceConsumer(e); + Component sc = m.createComponent().setImplementation(sci).add(m.createServiceDependency().setService(ServiceInterface.class).setRequired(false).setCallbacks("add", "remove")); + Component sa2 = m.createAspectService(ServiceInterface.class, null, 20, null).setImplementation(new ServiceAspect(e, 3)); + Component sa1 = m.createAspectService(ServiceInterface.class, null, 10, null).setImplementation(new ServiceAspect(e, 4)); + + // start with a service consumer + System.out.println("Adding consumer"); + m.add(sc); + + // then add two providers, so the consumer will see two services + System.out.println("Adding 2 providers"); + m.add(sp1); + m.add(sp2); + + // make sure consumer sees both services + Assert.assertEquals(2, sci.services()); + + // add an aspect with ranking 20 + System.out.println("Adding aspect with rank 20"); + m.add(sa2); + + // make sure the consumer sees the two new aspects and no longer sees the two original services + Assert.assertEquals(2, sci.services()); + Assert.assertEquals(20, sci.highestRanking()); + Assert.assertEquals(20, sci.lowestRanking()); + + // add an aspect with ranking 10 + System.out.println("Adding aspect with rank 10"); + m.add(sa1); + + // make sure the consumer still sees the two aspects with ranking 20 + Assert.assertEquals(2, sci.services()); + Assert.assertEquals(20, sci.highestRanking()); + Assert.assertEquals(20, sci.lowestRanking()); + + // remove the aspect with ranking 20 + System.out.println("Removing aspect with rank 20"); + m.remove(sa2); + + // make sure the consumer now sees the aspects with ranking 10 + Assert.assertEquals(2, sci.services()); + Assert.assertEquals(10, sci.highestRanking()); + Assert.assertEquals(10, sci.lowestRanking()); + + // remove one of the original services + System.out.println("Removing 1 service"); + m.remove(sp1); + + // make sure the aspect of that service goes away + Assert.assertEquals(1, sci.services()); + Assert.assertEquals(10, sci.highestRanking()); + Assert.assertEquals(10, sci.lowestRanking()); + + // remove the aspect with ranking 10 + System.out.println("Removing aspect with rank 10"); + m.remove(sa1); + + // make sure only the original service remains + Assert.assertEquals(1, sci.services()); + Assert.assertEquals(0, sci.highestRanking()); + Assert.assertEquals(0, sci.lowestRanking()); + + System.out.println("Done with test"); + + // end of test + m.remove(sa2); + m.remove(sp2); + m.remove(sc); + } + + static interface ServiceInterface { + public void invoke(Runnable run); + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke(Runnable run) { + run.run(); + } + } + + static class ServiceAspect implements ServiceInterface { + private final Ensure m_ensure; + private volatile ServiceInterface m_parentService; + private final int m_step; + + public ServiceAspect(Ensure e, int step) { + m_ensure = e; + m_step = step; + } + public void start() { + } + + public void invoke(Runnable run) { + m_ensure.step(m_step); + m_parentService.invoke(run); + } + + public void stop() { + } + } + + static class ServiceConsumer implements Runnable { + private List m_services = new ArrayList(); + private final Ensure m_ensure; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void init() { + Thread t = new Thread(this); + t.start(); + } + + public void run() { + } + + public int services() { + return m_services.size(); + } + + public int highestRanking() { + int ranking = Integer.MIN_VALUE; + for (int i = 0; i < m_services.size(); i++) { + ServiceReference ref = (ServiceReference) m_services.get(i); + Integer r = (Integer) ref.getProperty(Constants.SERVICE_RANKING); + int rank = r == null ? 0 : r.intValue(); + ranking = Math.max(ranking, rank); + } + return ranking; + } + public int lowestRanking() { + int ranking = Integer.MAX_VALUE; + for (int i = 0; i < m_services.size(); i++) { + ServiceReference ref = (ServiceReference) m_services.get(i); + Integer r = (Integer) ref.getProperty(Constants.SERVICE_RANKING); + int rank = r == null ? 0 : r.intValue(); + ranking = Math.min(ranking, rank); + } + return ranking; + } + + public void add(ServiceReference ref, ServiceInterface svc) { + System.out.println("Added: " + ServiceUtil.toString(ref)); + m_services.add(ref); + } + public void remove(ServiceReference ref, ServiceInterface svc) { + System.out.println("Removed: " + ServiceUtil.toString(ref)); + m_services.remove(ref); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectWithCallbacksServiceDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectWithCallbacksServiceDependencyTest.java new file mode 100644 index 00000000000..afe581d47cf --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectWithCallbacksServiceDependencyTest.java @@ -0,0 +1,179 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + + +/** + * @author Felix Project Team + */ +public class AspectWithCallbacksServiceDependencyTest extends TestBase { + public void testServiceRegistrationAndConsumption() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), null); + Component sc = m.createComponent().setImplementation(new ServiceConsumer(e)).add(m.createServiceDependency() + .setService(ServiceInterface.class) + .setCallbacks("add", "remove") + .setRequired(true)); + Component asp = m.createAspectService(ServiceInterface.class, null, 100, "add", null, "remove", "swap") + .setImplementation(ServiceProviderAspect.class); + m.add(sp); + m.add(sc); + m.add(asp); + m.remove(asp); + m.remove(sc); + m.remove(sp); + + // ensure we executed all steps inside the component instance + e.step(8); + } + + public void testServiceRegistrationAndConsumptionWithAspectCallbackInstance() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), null); + Component sc = m.createComponent().setImplementation(new ServiceConsumer(e)).add(m.createServiceDependency() + .setService(ServiceInterface.class) + .setCallbacks("add", "remove") + .setRequired(true)); + ServiceProviderAspect providerAspect = new ServiceProviderAspect(); + ServiceProviderAspectCallbackInstance aspectCb = new ServiceProviderAspectCallbackInstance(providerAspect); + Component asp = m.createAspectService(ServiceInterface.class, null, 100, aspectCb, "add", null, "remove", "swap") + .setImplementation(providerAspect); + m.add(sp); + m.add(sc); + m.add(asp); + m.remove(asp); + m.remove(sc); + m.remove(sp); + + // ensure we executed all steps inside the component instance + e.step(8); + } + + static interface ServiceInterface { + public void invoke(String caller); + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke(String caller) { + if (caller.equals("consumer.init")) { + m_ensure.step(3); + } else if (caller.equals("aspect.consumer.add")) { + m_ensure.step(5); + } + } + } + + public static class ServiceProviderAspectCallbackInstance { + private final ServiceProviderAspect m_aspect; + + ServiceProviderAspectCallbackInstance(ServiceProviderAspect aspect) { + m_aspect = aspect; + } + + public void add(ServiceInterface service) { + m_aspect.add(service); + } + + public void remove(ServiceInterface service) { + m_aspect.remove(service); + } + + public void swap(ServiceInterface previous, ServiceInterface current) { + m_aspect.swap(previous, current); + } + } + + static class ServiceProviderAspect implements ServiceInterface { + private volatile ServiceInterface m_service; + + public ServiceProviderAspect() { + } + + @Override + public void invoke(String caller) { + m_service.invoke("aspect." + caller); + } + + public void add(ServiceInterface service) { + m_service = service; + } + + public void remove(ServiceInterface service) { + m_service = null; + } + + public void swap(ServiceInterface previous, ServiceInterface current) { + m_service = current; + } + } + + static class ServiceConsumer { + private volatile ServiceInterface m_service; + private final Ensure m_ensure; + private int addCount = 0; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void init() { + m_ensure.step(2); + m_service.invoke("consumer.init"); + } + + public void destroy() { + m_ensure.step(7); + } + + public void add(ServiceInterface service) { + m_service = service; + switch (addCount) { + case 0: m_ensure.step(1); + break; + case 1: m_ensure.step(4); + // aspect had been added + m_service.invoke("consumer.add"); + break; + case 2: m_ensure.step(6); + break; + default: + } + addCount ++; + } + public void remove(ServiceInterface service) { + m_service = null; + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectWithPropagationTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectWithPropagationTest.java new file mode 100644 index 00000000000..f90c2647dcc --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AspectWithPropagationTest.java @@ -0,0 +1,763 @@ +/* + * 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.dm.itest.api; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.ServiceUtil; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; + +/** + * Test for aspects with service properties propagations. + * + * @author Felix Project Team + */ +@SuppressWarnings({"rawtypes", "unchecked", "unused"}) +public class AspectWithPropagationTest extends TestBase { + private final static int ASPECTS = 3; + private final Set _randoms = new HashSet(); + private final Random _rnd = new Random(); + private static Ensure m_invokeStep; + private static Ensure m_changeStep; + + /** + * This test does the following: + * + * - Create S service with property "p=s" + * - Create SA (aspect of S) with property "p=aspect" + * - Create Client, depending on S (actually, on SA). + * - Client should see SA with properties p=aspect + * - Change S service property with "p=smodified": the Client should be changed with SA(p=aspect) + * - Change aspect service property with "p=aspectmodified": The client should be changed with SA(p=aspectmodified) + */ + public void testAspectsWithPropagationNotOverriding() { + System.out.println("----------- Running testAspectsWithPropagationNotOverriding ..."); + DependencyManager m = getDM(); + m_invokeStep = new Ensure(); + + // Create our original "S" service. + S s = new S() { + public void invoke() { + } + }; + Dictionary props = new Hashtable(); + props.put("p", "s"); + Component sComp = m.createComponent() + .setImplementation(s) + .setInterface(S.class.getName(), props); + + // Create SA (aspect of S) + S sa = new S() { + volatile S m_s; + public void invoke() { + } + }; + Component saComp = m.createAspectService(S.class, null, 1).setImplementation(sa); + props = new Hashtable(); + props.put("p", "aspect"); + saComp.setServiceProperties(props); + + // Create client depending on S + Object client = new Object() { + int m_changeCount; + void add(Map props, S s) { + Assert.assertEquals("aspect", props.get("p")); + m_invokeStep.step(1); + } + + void change(Map props, S s) { + switch (++m_changeCount) { + case 1: + Assert.assertEquals("aspect", props.get("p")); + m_invokeStep.step(2); + break; + case 2: + Assert.assertEquals("aspectmodified", props.get("p")); + m_invokeStep.step(3); + } + } + }; + Component clientComp = m.createComponent() + .add(m.createServiceDependency() + .setService(S.class) + .setRequired(true) + .setCallbacks("add", "change", null)) + .setImplementation(client); + + // Add components in dependency manager + m.add(sComp); + m.add(saComp); + m.add(clientComp); + + // client should have been added with SA aspect + m_invokeStep.waitForStep(1, 5000); + + // now change s "p=s" to "p=smodified": client should not see it + props = new Hashtable(); + props.put("p", "smodified"); + sComp.setServiceProperties(props); + m_invokeStep.waitForStep(2, 5000); + + // now change sa aspect "p=aspect" to "p=aspectmodified": client should see it + props = new Hashtable(); + props.put("p", "aspectmodified"); + saComp.setServiceProperties(props); + m_invokeStep.waitForStep(3, 5000); + + // remove components + m.remove(clientComp); + m.remove(saComp); + m.remove(sComp); + } + + /** + * This test does the following: + * + * - Create S service + * - Create some S Aspects + * - Create a Client, depending on S (actually, on the top-level S aspect) + * - Client has a "change" callback in order to track S service properties modifications. + * - First, invoke Client.invoke(): all S aspects, and finally original S service must be invoked orderly. + * - Modify S original service properties, and check if all aspects, and the client has been orderly called in their "change" callback. + * - Modify the First lowest ranked aspect (rank=1), and check if all aspects, and client have been orderly called in their "change" callback. + */ + public void testAspectsWithPropagation() { + System.out.println("----------- Running testAspectsWithPropagation ..."); + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + m_invokeStep = new Ensure(); + + // Create our original "S" service. + Dictionary props = new Hashtable(); + props.put("foo", "bar"); + Component s = m.createComponent() + .setImplementation(new SImpl()) + .setInterface(S.class.getName(), props); + + // Create an aspect aware client, depending on "S" service. + Client clientImpl; + Component client = m.createComponent() + .setImplementation((clientImpl = new Client())) + .add(m.createServiceDependency() + .setService(S.class) + .setRequired(true) + .setCallbacks("add", "change", "remove", "swap")); + + // Create some "S" aspects + Component[] aspects = new Component[ASPECTS]; + for (int rank = 1; rank <= ASPECTS; rank ++) { + aspects[rank-1] = m.createAspectService(S.class, null, rank, "add", "change", "remove", "swap") + .setImplementation(new A("A" + rank, rank)); + props = new Hashtable(); + props.put("a" + rank, "v" + rank); + aspects[rank-1].setServiceProperties(props); + } + + // Register client + m.add(client); + + // Randomly register aspects and original service + boolean originalServiceAdded = false; + for (int i = 0; i < ASPECTS; i ++) { + int index = getRandomAspect(); + m.add(aspects[index]); + if (! originalServiceAdded && _rnd.nextBoolean()) { + m.add(s); + originalServiceAdded = true; + } + } + if (! originalServiceAdded) { + m.add(s); + } + + // All set, check if client has inherited from top level aspect properties + original service properties + Map check = new HashMap(); + check.put("foo", "bar"); + for (int i = 1; i < (ASPECTS - 1); i ++) { + check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect. + } + check.put("a" + ASPECTS, "v" + ASPECTS); + checkServiceProperties(check, clientImpl.getServiceProperties()); + + // Now invoke client, which orderly calls all aspects in the chain, and finally the original service "S". + System.out.println("-------------------------- Invoking client."); + clientImpl.invoke(); + m_invokeStep.waitForStep(ASPECTS+1, 5000); + + // Now, change original service "S" properties: this will orderly trigger "change" callbacks on aspects, and on client. + System.out.println("-------------------------- Modifying original service properties."); + m_changeStep = new Ensure(); + props = new Hashtable(); + props.put("foo", "barModified"); + s.setServiceProperties(props); + + // Check if aspects and client have been orderly called in their "changed" callback + m_changeStep.waitForStep(ASPECTS+1, 5000); + + // Check if modified "foo" original service property has been propagated + check = new HashMap(); + check.put("foo", "barModified"); + for (int i = 1; i < (ASPECTS - 1); i ++) { + check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect. + } + check.put("a" + ASPECTS, "v" + ASPECTS); // we only see top-level aspect service properties + checkServiceProperties(check, clientImpl.getServiceProperties()); + + // Now, change the top-level ranked aspect: it must propagate to all upper aspects, as well as to the client + System.out.println("-------------------------- Modifying top-level aspect service properties."); + + m_changeStep = new Ensure(); + for (int i = 1; i <= ASPECTS; i ++) { + m_changeStep.step(i); // only client has to be changed. + } + props = new Hashtable(); + props.put("a" + ASPECTS, "v" + ASPECTS + "-Modified"); + aspects[ASPECTS-1].setServiceProperties(props); // That triggers change callbacks for upper aspects (with rank >= 2) + m_changeStep.waitForStep(ASPECTS+1, 5000); // check if client have been changed. + + // Check if top level aspect service properties have been propagated up to the client. + check = new HashMap(); + check.put("foo", "barModified"); + for (int i = 1; i < (ASPECTS - 1); i ++) { + check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect. + } + check.put("a" + ASPECTS, "v" + ASPECTS + "-Modified"); + checkServiceProperties(check, clientImpl.getServiceProperties()); + + // Clear all components. + m_changeStep = null; + m.clear(); + } + + /** + * This test does the following: + * + * - Create S service + * - Create some S Aspects without any callbacks (add/change/remove/swap) + * - Create a Client, depending on S (actually, on the top-level S aspect) + * - Client has a "change" callack in order to track S service properties modifications. + * - First, invoke Client.invoke(): all S aspects, and finally original S service must be invoked orderly. + * - Modify S original service properties, and check if the client has been called in its "change" callback. + */ + public void testAspectsWithPropagationAndNoCallbacks() { + System.out.println("----------- Running testAspectsWithPropagation ..."); + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + m_invokeStep = new Ensure(); + + // Create our original "S" service. + Dictionary props = new Hashtable(); + props.put("foo", "bar"); + Component s = m.createComponent() + .setImplementation(new SImpl()) + .setInterface(S.class.getName(), props); + + // Create an aspect aware client, depending on "S" service. + Client clientImpl; + Component client = m.createComponent() + .setImplementation((clientImpl = new Client())) + .add(m.createServiceDependency() + .setService(S.class) + .setRequired(true) + .setCallbacks("add", "change", "remove")); + + // Create some "S" aspects + Component[] aspects = new Component[ASPECTS]; + for (int rank = 1; rank <= ASPECTS; rank ++) { + aspects[rank-1] = m.createAspectService(S.class, null, rank) + .setImplementation(new A("A" + rank, rank)); + props = new Hashtable(); + props.put("a" + rank, "v" + rank); + aspects[rank-1].setServiceProperties(props); + } + + // Register client + m.add(client); + + // Randomly register aspects and original service + boolean originalServiceAdded = false; + for (int i = 0; i < ASPECTS; i ++) { + int index = getRandomAspect(); + m.add(aspects[index]); + if (! originalServiceAdded && _rnd.nextBoolean()) { + m.add(s); + originalServiceAdded = true; + } + } + if (! originalServiceAdded) { + m.add(s); + } + + // All set, check if client has inherited from top level aspect properties + original service properties + Map check = new HashMap(); + check.put("foo", "bar"); + for (int i = 1; i < (ASPECTS - 1); i ++) { + check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect. + } + check.put("a" + ASPECTS, "v" + ASPECTS); + checkServiceProperties(check, clientImpl.getServiceProperties()); + + // Now invoke client, which orderly calls all aspects in the chain, and finally the original service "S". + System.out.println("-------------------------- Invoking client."); + clientImpl.invoke(); + m_invokeStep.waitForStep(ASPECTS+1, 5000); + + // Now, change original service "S" properties: this will orderly trigger "change" callbacks on aspects, and on client. + System.out.println("-------------------------- Modifying original service properties."); + m_changeStep = new Ensure(); + for (int i = 1; i <= ASPECTS; i ++) { + m_changeStep.step(i); // skip aspects, which have no "change" callbacks. + } + props = new Hashtable(); + props.put("foo", "barModified"); + s.setServiceProperties(props); + + // Check if aspects and client have been orderly called in their "changed" callback + m_changeStep.waitForStep(ASPECTS+1, 5000); + + // Check if modified "foo" original service property has been propagated + check = new HashMap(); + check.put("foo", "barModified"); + for (int i = 1; i < (ASPECTS - 1); i ++) { + check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect. + } + check.put("a" + ASPECTS, "v" + ASPECTS); // we only see top-level aspect service properties + checkServiceProperties(check, clientImpl.getServiceProperties()); + + // Clear all components. + m_changeStep = null; + m.clear(); + } + + /** + * This test does the following: + * + * - Create S service + * - Create some S Aspects + * - Create S2 Adapter, which adapts S to S2 + * - Create Client2, which depends on S2. Client2 listens to S2 property change events. + * - Now, invoke Client2.invoke(): all S aspects, and finally original S service must be invoked orderly. + * - Modify S original service properties, and check if all aspects, S2 Adapter, and Client2 have been orderly called in their "change" callback. + */ + public void testAdapterWithAspectsAndPropagation() { + System.out.println("----------- Running testAdapterWithAspectsAndPropagation ..."); + + DependencyManager m = getDM(); + m_invokeStep = new Ensure(); + + // Create our original "S" service. + Dictionary props = new Hashtable(); + props.put("foo", "bar"); + Component s = m.createComponent() + .setImplementation(new SImpl()) + .setInterface(S.class.getName(), props); + + // Create some "S" aspects + Component[] aspects = new Component[ASPECTS]; + for (int rank = 1; rank <= ASPECTS; rank ++) { + aspects[rank-1] = m.createAspectService(S.class, null, rank, "add", "change", "remove", "swap") + .setImplementation(new A("A" + rank, rank)); + props = new Hashtable(); + props.put("a" + rank, "v" + rank); + aspects[rank-1].setServiceProperties(props); + } + + // Create S2 adapter (which adapts S1 to S2 interface) + Component adapter = m.createAdapterService(S.class, null, "add", "change", "remove", "swap") + .setInterface(S2.class.getName(), null) + .setImplementation(new S2Impl()); + + // Create Client2, which depends on "S2" service. + Client2 client2Impl; + Component client2 = m.createComponent() + .setImplementation((client2Impl = new Client2())) + .add(m.createServiceDependency() + .setService(S2.class) + .setRequired(true) + .setCallbacks("add", "change", null)); + + // Register client2 + m.add(client2); + + // Register S2 adapter + m.add(adapter); + + // Randomly register aspects, original service + boolean originalServiceAdded = false; + for (int i = 0; i < ASPECTS; i ++) { + int index = getRandomAspect(); + m.add(aspects[index]); + if (! originalServiceAdded && _rnd.nextBoolean()) { + m.add(s); + originalServiceAdded = true; + } + } + if (! originalServiceAdded) { + m.add(s); + } + + // Now invoke client2, which orderly calls all S1 aspects, then S1Impl, and finally S2 service + System.out.println("-------------------------- Invoking client2."); + client2Impl.invoke2(); + m_invokeStep.waitForStep(ASPECTS+2, 5000); + + // Now, change original service "S" properties: this will orderly trigger "change" callbacks on aspects, S2Impl, and Client2. + System.out.println("-------------------------- Modifying original service properties."); + m_changeStep = new Ensure(); + props = new Hashtable(); + props.put("foo", "barModified"); + s.setServiceProperties(props); + + // Check if aspects and Client2 have been orderly called in their "changed" callback + m_changeStep.waitForStep(ASPECTS+2, 5000); + + // Check if modified "foo" original service property has been propagated to Client2 + Map check = new HashMap(); + check.put("foo", "barModified"); + for (int i = 1; i < (ASPECTS - 1); i ++) { + check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect. + } + check.put("a" + ASPECTS, "v" + ASPECTS); + checkServiceProperties(check, client2Impl.getServiceProperties()); + + // Clear all components. + m_changeStep = null; + m.clear(); + } + + /** + * This test does the following: + * + * - Create S service + * - Create some S Aspects without any callbacks (add/change/remove) + * - Create S2 Adapter, which adapts S to S2 (but does not have any add/change/remove callbacks) + * - Create Client2, which depends on S2. Client2 listens to S2 property change events. + * - Now, invoke Client2.invoke(): all S aspects, and finally original S service must be invoked orderly. + * - Modify S original service properties, and check if all aspects, S2 Adapter, and Client2 have been orderly called in their "change" callback. + */ + public void testAdapterWithAspectsAndPropagationNoCallbacks() { + System.out.println("----------- Running testAdapterWithAspectsAndPropagationNoCallbacks ..."); + + DependencyManager m = getDM(); + m_invokeStep = new Ensure(); + + // Create our original "S" service. + Dictionary props = new Hashtable(); + props.put("foo", "bar"); + Component s = m.createComponent() + .setImplementation(new SImpl()) + .setInterface(S.class.getName(), props); + + // Create some "S" aspects + Component[] aspects = new Component[ASPECTS]; + for (int rank = 1; rank <= ASPECTS; rank ++) { + aspects[rank-1] = m.createAspectService(S.class, null, rank) + .setImplementation(new A("A" + rank, rank)); + props = new Hashtable(); + props.put("a" + rank, "v" + rank); + aspects[rank-1].setServiceProperties(props); + } + + // Create S2 adapter (which adapts S1 to S2 interface) + Component adapter = m.createAdapterService(S.class, null) + .setInterface(S2.class.getName(), null) + .setImplementation(new S2Impl()); + + // Create Client2, which depends on "S2" service. + Client2 client2Impl; + Component client2 = m.createComponent() + .setImplementation((client2Impl = new Client2())) + .add(m.createServiceDependency() + .setService(S2.class) + .setRequired(true) + .setCallbacks("add", "change", null)); + + // Register client2 + m.add(client2); + + // Register S2 adapter + m.add(adapter); + + // Randomly register aspects, original service + boolean originalServiceAdded = false; + for (int i = 0; i < ASPECTS; i ++) { + int index = getRandomAspect(); + m.add(aspects[index]); + if (! originalServiceAdded && _rnd.nextBoolean()) { + m.add(s); + originalServiceAdded = true; + } + } + if (! originalServiceAdded) { + m.add(s); + } + + // Now invoke client2, which orderly calls all S1 aspects, then S1Impl, and finally S2 service + System.out.println("-------------------------- Invoking client2."); + client2Impl.invoke2(); + m_invokeStep.waitForStep(ASPECTS+2, 5000); + + // Now, change original service "S" properties: this will orderly trigger "change" callbacks on aspects, S2Impl, and Client2. + System.out.println("-------------------------- Modifying original service properties."); + m_changeStep = new Ensure(); + for (int i = 1; i <= ASPECTS+1; i ++) { + m_changeStep.step(i); // skip all aspects and the adapter + } + props = new Hashtable(); + props.put("foo", "barModified"); + s.setServiceProperties(props); + + // Check if Client2 has been called in its "changed" callback + m_changeStep.waitForStep(ASPECTS+2, 5000); + + // Check if modified "foo" original service property has been propagated to Client2 + Map check = new HashMap(); + check.put("foo", "barModified"); + for (int i = 1; i < (ASPECTS - 1); i ++) { + check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect. + } + check.put("a" + ASPECTS, "v" + ASPECTS); + checkServiceProperties(check, client2Impl.getServiceProperties()); + + // Clear all components. + m_changeStep = null; + m.clear(); + } + + private void checkServiceProperties(Map check, Dictionary properties) { + for (Object key : check.keySet()) { + Object val = check.get(key); + if (val == null) { + Assert.assertNull(properties.get(key)); + } else { + Assert.assertEquals(val, properties.get(key)); + } + } + } + + private int getRandomAspect() { + int index = 0; + do { + index = _rnd.nextInt(ASPECTS); + } while (_randoms.contains(new Integer(index))); + _randoms.add(new Integer(index)); + return index; + } + + // S Service + public static interface S { + public void invoke(); + } + + // S ServiceImpl + static class SImpl implements S { + public SImpl() { + } + + public String toString() { + return "S"; + } + + public void invoke() { + m_invokeStep.step(ASPECTS+1); + } + } + + // S Aspect + static class A implements S { + private final String m_name; + private volatile ServiceRegistration m_registration; + private volatile S m_next; + private final int m_rank; + + public A(String name, int rank) { + m_name = name; + m_rank = rank; + } + + public String toString() { + return m_name; + } + + public void invoke() { + int rank = ServiceUtil.getRanking(m_registration.getReference()); + m_invokeStep.step(ASPECTS - rank + 1); + m_next.invoke(); + } + + public void add(ServiceReference ref, S s) { + System.out.println("+++ A" + m_rank + ".add:" + s + "/" + ServiceUtil.toString(ref)); + m_next = s; + } + + public void swap(ServiceReference oldSRef, S oldS, ServiceReference newSRef, S newS) { + System.out.println("+++ A" + m_rank + ".swap: new=" + newS + ", props=" + ServiceUtil.toString(newSRef)); + Assert.assertTrue(m_next == oldS); + m_next = newS; + } + + public void change(ServiceReference props, S s) { + System.out.println("+++ A" + m_rank + ".change: s=" + s + ", props=" + ServiceUtil.toString(props)); + if (m_changeStep != null) { + int rank = ServiceUtil.getRanking(m_registration.getReference()); + m_changeStep.step(rank); + } + } + + public void remove(ServiceReference props, S s) { + System.out.println("+++ A" + m_rank + ".remove: " + s + ", props=" + ServiceUtil.toString(props)); + } + } + + // Aspect aware client, depending of "S" service aspects. + static class Client { + private volatile S m_s; + private volatile ServiceReference m_sRef; + + public Client() { + } + + public Dictionary getServiceProperties() { + Dictionary props = new Hashtable(); + for (String key : m_sRef.getPropertyKeys()) { + props.put(key, m_sRef.getProperty(key)); + } + return props; + } + + public void invoke() { + m_s.invoke(); + } + + public String toString() { + return "Client"; + } + + public void add(ServiceReference ref, S s) { + System.out.println("+++ Client.add: " + s + "/" + ServiceUtil.toString(ref)); + m_s = s; + m_sRef = ref; + } + + public void swap(ServiceReference oldSRef, S oldS, ServiceReference newSRef, S newS) { + System.out.println("+++ Client.swap: m_s = " + m_s + ", old=" + oldS + ", oldProps=" + ServiceUtil.toString(oldSRef) + ", new=" + newS + ", props=" + ServiceUtil.toString(newSRef)); + Assert.assertTrue(m_s == oldS); + m_s = newS; + m_sRef = newSRef; + } + + public void change(ServiceReference properties, S s) { + System.out.println("+++ Client.change: s=" + s + ", props=" + ServiceUtil.toString(properties)); + if (m_changeStep != null) { + m_changeStep.step(ASPECTS+1); + } + } + + public void remove(ServiceReference props, S s) { + System.out.println("+++ Client.remove: " + s + ", props=" + ServiceUtil.toString(props)); + } + } + + // S2 Service + public static interface S2 { + public void invoke2(); + } + + // S2 impl, which adapts S1 interface to S2 interface + static class S2Impl implements S2 { + private volatile S m_s; // we shall see top-level aspect on S service + + public void add(ServiceReference ref, S s) { + System.out.println("+++ S2Impl.add: " + s + "/" + ServiceUtil.toString(ref)); + m_s = s; + } + + public void swap(ServiceReference oldSRef, S oldS, ServiceReference newSRef, S newS) { + System.out.println("+++ S2Impl.swap: new=" + newS + ", props=" + ServiceUtil.toString(newSRef)); + m_s = newS; + } + + public void change(ServiceReference properties, S s) { + System.out.println("+++ S2Impl.change: s=" + s + ", props=" + ServiceUtil.toString(properties)); + if (m_changeStep != null) { + m_changeStep.step(ASPECTS+1); + } + } + + public void remove(ServiceReference props, S s) { + System.out.println("+++ S2Impl.remove: " + s + ", props=" + ServiceUtil.toString(props)); + } + + public void invoke2() { + m_s.invoke(); + m_invokeStep.step(ASPECTS + 2); // All aspects, and S1Impl have been invoked + } + + public String toString() { + return "S2"; + } + } + + // Client2 depending on S2. + static class Client2 { + private volatile S2 m_s2; + private volatile ServiceReference m_s2Ref; + + public Dictionary getServiceProperties() { + Dictionary props = new Hashtable(); + for (String key : m_s2Ref.getPropertyKeys()) { + props.put(key, m_s2Ref.getProperty(key)); + } + return props; + } + + public void invoke2() { + m_s2.invoke2(); + } + + public String toString() { + return "Client2"; + } + + public void add(ServiceReference ref, S2 s2) { + System.out.println("+++ Client2.add: " + s2 + "/" + ServiceUtil.toString(ref)); + m_s2 = s2; + m_s2Ref = ref; + } + + public void change(ServiceReference props, S2 s2) { + System.out.println("+++ Client2.change: s2=" + s2 + ", props=" + ServiceUtil.toString(props)); + if (m_changeStep != null) { + m_changeStep.step(ASPECTS + 2); // S1Impl, all aspects, and S2 adapters have been changed before us. + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AutoConfigTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AutoConfigTest.java new file mode 100644 index 00000000000..c79249b2a1d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/AutoConfigTest.java @@ -0,0 +1,258 @@ +/* + * 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.dm.itest.api; + +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.framework.Constants; + +/** + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class AutoConfigTest extends TestBase { + private final Ensure m_ensure = new Ensure(); + + public void testField() throws Exception { + final DependencyManager dm = getDM(); + // Create a consumer, depending on some providers (autoconfig field). + ConsumeWithProviderField consumer = new ConsumeWithProviderField(); + Component c = createConsumer(dm, consumer); + // Create two providers + Component p1 = createProvider(dm, 10, new Provider() { + public String toString() { return "provider1"; } + public void run() { m_ensure.step(); } + }); + Component p2 = createProvider(dm, 20, new Provider() { + public String toString() { return "provider2"; } + public void run() { m_ensure.step(); } + }); + + // add the two providers + dm.add(p2); + dm.add(p1); + // add the consumer, which should have been injected with provider2 (highest rank) + dm.add(c); + m_ensure.waitForStep(1, 5000); + // remove the provider2, the consumer should now be injected with provider1 + dm.remove(p2); + Assert.assertNotNull(consumer.getProvider()); + Assert.assertEquals("provider1", consumer.getProvider().toString()); + // remove the provider1, the consumer should have been stopped + dm.remove(p1); + m_ensure.waitForStep(2, 5000); + dm.clear(); + } + + public void testIterableField() throws Exception { + final DependencyManager dm = getDM(); + ConsumerWithIterableField consumer = new ConsumerWithIterableField(); + Component c = createConsumer(dm, consumer); + Component p1 = createProvider(dm, 10, new Provider() { + public void run() { m_ensure.step(); } + public String toString() { return "provider1"; } + }); + Component p2 = createProvider(dm, 20, new Provider() { + public void run() { m_ensure.step();} + public String toString() { return "provider2"; } + }); + + dm.add(p2); + dm.add(p1); + dm.add(c); + // the consumer should have been injected with all providers. + m_ensure.waitForStep(3, 5000); + + // check if all providers are there + Assert.assertNotNull(consumer.getProvider("provider1")); + Assert.assertNotNull(consumer.getProvider("provider2")); + + // remove provider1 + dm.remove(p1); + + // check if provider1 has been removed and if provider2 is still there + Assert.assertNull(consumer.getProvider("provider1")); + Assert.assertNotNull(consumer.getProvider("provider2")); + + // remove provider2, the consumer should be stopped + dm.remove(p2); + m_ensure.waitForStep(4, 5000); + dm.clear(); + } + + public void testMapField() throws Exception { + final DependencyManager dm = getDM(); + ConsumerWithMapField consumer = new ConsumerWithMapField(); + Component c = createConsumer(dm, consumer); + Component p1 = createProvider(dm, 10, new Provider() { + public void run() { m_ensure.step(); } + public String toString() { return "provider1"; } + }); + Component p2 = createProvider(dm, 20, new Provider() { + public void run() { m_ensure.step();} + public String toString() { return "provider2"; } + }); + + dm.add(p2); + dm.add(p1); + dm.add(c); + // the consumer should have been injected with all providers. + m_ensure.waitForStep(3, 5000); + + // check if all providers are there + Assert.assertNotNull(consumer.getProvider("provider1")); + Assert.assertNotNull(consumer.getProvider("provider2")); + + // remove provider1 + dm.remove(p1); + + // check if provider1 has been removed and if provider2 is still there + Assert.assertNull(consumer.getProvider("provider1")); + Assert.assertNotNull(consumer.getProvider("provider2")); + + // remove provider2, the consumer should be stopped + dm.remove(p2); + m_ensure.waitForStep(4, 5000); + dm.clear(); + } + + private Component createProvider(DependencyManager dm, int rank, Provider provider) { + Hashtable props = new Hashtable(); + props.put(Constants.SERVICE_RANKING, new Integer(rank)); + return dm.createComponent() + .setImplementation(provider) + .setInterface(Provider.class.getName(), props); + } + + private Component createConsumer(DependencyManager dm, Object consumer) { + return dm.createComponent() + .setImplementation(consumer) + .add(dm.createServiceDependency().setService(Provider.class).setRequired(true)); + } + + public static interface Provider extends Runnable { + } + + public class ConsumeWithProviderField { + volatile Provider m_provider; + + void start() { + Assert.assertNotNull(m_provider); + Assert.assertEquals("provider2", m_provider.toString()); + m_ensure.step(1); + } + + public Provider getProvider() { + return m_provider; + } + + void stop() { + m_ensure.step(2); + } + } + + public class ConsumerWithIterableField { + final Iterable m_providers = new ConcurrentLinkedQueue<>(); + final List m_notInjectMe = new ArrayList(); + + void start() { + Assert.assertNotNull(m_providers); + int found = 0; + for (Provider provider : m_providers) { + provider.run(); + found ++; + } + Assert.assertTrue(found == 2); + // The "m_notInjectMe" should not be injected with anything + Assert.assertEquals(m_notInjectMe.size(), 0); + m_ensure.step(3); + } + + public Provider getProvider(String name) { + System.out.println("getProvider(" + name + ") : proviers=" + m_providers); + for (Provider provider : m_providers) { + if (provider.toString().equals(name)) { + return provider; + } + } + return null; + } + + void stop() { + m_ensure.step(4); + } + } + + public class ConsumerWithMapField { + final Map m_providers = new ConcurrentHashMap<>(); + final Map m_notInjectMe = new HashMap<>(); + + void start() { + Assert.assertNotNull(m_providers); + System.out.println("ConsumerMap.start: injected providers=" + m_providers); + Assert.assertTrue(m_providers.size() == 2); + Assert.assertEquals(0, m_notInjectMe.size()); + for (Map.Entry e : m_providers.entrySet()) { + Provider provider = e.getKey(); + Dictionary props = e.getValue(); + + provider.run(); + if (provider.toString().equals("provider1")) { + Assert.assertEquals(props.get(Constants.SERVICE_RANKING), 10); + } else if (provider.toString().equals("provider2")) { + Assert.assertEquals(props.get(Constants.SERVICE_RANKING), 20); + } else { + Assert.fail("Did not find any properties for provider " + provider); + } + } + + m_ensure.step(3); + } + + public Provider getProvider(String name) { + System.out.println("getProvider(" + name + ") : providers=" + m_providers); + for (Provider provider : m_providers.keySet()) { + if (provider.toString().equals(name)) { + return provider; + } + } + return null; + } + + Map getProviders() { + return m_providers; + } + + void stop() { + m_ensure.step(4); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/BundleAdapterTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/BundleAdapterTest.java new file mode 100644 index 00000000000..f35232e91d4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/BundleAdapterTest.java @@ -0,0 +1,146 @@ +/* + * 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.dm.itest.api; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.Bundle; + +/** + * @author Felix Project Team + */ +public class BundleAdapterTest extends TestBase { + public void testBundleAdapter() { + DependencyManager m = getDM(); + // create a bundle adapter service (one is created for each bundle) + Component adapter = m.createBundleAdapterService(Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE, null, false) + .setImplementation(BundleAdapter.class) + .setInterface(BundleAdapter.class.getName(), null); + + // create a service provider and consumer + Consumer c = new Consumer(); + Component consumer = m.createComponent().setImplementation(c) + .add(m.createServiceDependency().setService(BundleAdapter.class).setCallbacks("add", "remove")); + + // add the bundle adapter + m.add(adapter); + // add the service consumer + m.add(consumer); + // check if at least one bundle was found + c.check(); + // remove the consumer again + m.remove(consumer); + // check if all bundles were removed correctly + c.doubleCheck(); + // remove the bundle adapter + m.remove(adapter); + } + + public void testBundleAdapterWithCallbackInstance() { + DependencyManager m = getDM(); + // create a bundle adapter service (one is created for each bundle) + BundleAdapterWithCallback baWithCb = new BundleAdapterWithCallback(); + BundleAdapterCallbackInstance cbInstance = new BundleAdapterCallbackInstance(baWithCb); + + Component adapter = m.createBundleAdapterService(Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE, null, false, + cbInstance, "add", null, "remove") + .setImplementation(baWithCb) + .setInterface(BundleAdapter.class.getName(), null); + + // create a service provider and consumer + Consumer c = new Consumer(); + Component consumer = m.createComponent().setImplementation(c) + .add(m.createServiceDependency().setService(BundleAdapter.class).setCallbacks("add", "remove")); + + // add the bundle adapter + m.add(adapter); + // add the service consumer + m.add(consumer); + // check if at least one bundle was found + c.check(); + // remove the consumer again + m.remove(consumer); + // check if all bundles were removed correctly + c.doubleCheck(); + // remove the bundle adapter + m.remove(adapter); + } + + public static class BundleAdapter { + volatile Bundle m_bundle; + + Bundle getBundle() { + return m_bundle; + } + } + + public static class BundleAdapterWithCallback extends BundleAdapter { + void add(Bundle b) { + m_bundle = b; + } + + void remove(Bundle b) { + m_bundle = null; + } + } + + public static class BundleAdapterCallbackInstance { + final BundleAdapterWithCallback m_ba; + + BundleAdapterCallbackInstance(BundleAdapterWithCallback ba) { + m_ba = ba; + } + + void add(Bundle b) { + m_ba.add(b); + } + + void remove(Bundle b) { + m_ba.remove(b); + } + } + + static class Consumer { + private volatile int m_count = 0; + + public void add(BundleAdapter ba) { + Bundle b = ba.getBundle(); + System.out.println("Consumer.add(" + b.getSymbolicName() + ")"); + Assert.assertNotNull("bundle instance must not be null", b); + m_count++; + } + + public void check() { + Assert.assertTrue("we should have found at least one bundle", m_count > 0); + } + + public void remove(BundleAdapter ba) { + Bundle b = ba.getBundle(); + System.out.println("Consumer.remove(" + b.getSymbolicName() + ")"); + m_count--; + } + + public void doubleCheck() { + Assert.assertEquals("all bundles we found should have been removed again", 0, m_count); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/BundleAdapterWithCallbacksNotAutoConfiguredTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/BundleAdapterWithCallbacksNotAutoConfiguredTest.java new file mode 100644 index 00000000000..6c70c7bf2a1 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/BundleAdapterWithCallbacksNotAutoConfiguredTest.java @@ -0,0 +1,69 @@ +/* + * 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.dm.itest.api; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.Bundle; + +/** + * @author Felix Project Team + */ +public class BundleAdapterWithCallbacksNotAutoConfiguredTest extends TestBase { + final Ensure m_e = new Ensure(); + + public void testBundleAdapterWithCallbacksNotAutoConfigured() { + DependencyManager m = getDM(); + // create a bundle adapter service (one is created for each bundle) + BundleAdapterWithCallback baWithCb = new BundleAdapterWithCallback(); + String bsn = "org.apache.felix.dependencymanager"; + String filter = "(Bundle-SymbolicName=" + bsn + ")"; + + Component adapter = m.createBundleAdapterService(Bundle.ACTIVE, filter, false, null, "add", null, null) + .setImplementation(baWithCb); + + // add the bundle adapter + m.add(adapter); + + // Check if adapter has not been auto configured (because it has callbacks defined). + m_e.waitForStep(1, 3000); + Assert.assertNull("bundle adapter must not be auto configured", baWithCb.getBundle()); + + // remove the bundle adapters + m.remove(adapter); + } + + class BundleAdapterWithCallback { + volatile Bundle m_bundle; // must not be auto configured because we are using callbacks. + + Bundle getBundle() { + return m_bundle; + } + + void add(Bundle b) { + Assert.assertNotNull(b); + Assert.assertEquals("org.apache.felix.dependencymanager", b.getSymbolicName()); + m_e.step(1); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/BundleDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/BundleDependencyTest.java new file mode 100644 index 00000000000..36a2b6ddf5e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/BundleDependencyTest.java @@ -0,0 +1,177 @@ +/* + * 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.dm.itest.api; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.Bundle; + +/** + * @author Felix Project Team + */ +public class BundleDependencyTest extends TestBase { + private final static String BSN = "org.apache.felix.metatype"; + + public void testBundleDependencies() { + DependencyManager m = getDM(); + // create a service provider and consumer + Consumer c = new Consumer(); + Component consumer = m.createComponent().setImplementation(c).add(m.createBundleDependency().setCallbacks("add", "remove")); + // add the service consumer + m.add(consumer); + // check if at least one bundle was found + c.check(); + // remove the consumer again + m.remove(consumer); + // check if all bundles were removed correctly + c.doubleCheck(); + + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + Component consumerWithFilter = m.createComponent().setImplementation(new FilteredConsumer(e)).add(m.createBundleDependency().setFilter("(Bundle-SymbolicName=" + BSN + ")").setCallbacks("add", "remove")); + // add a consumer with a filter + m.add(consumerWithFilter); + e.step(2); + // remove the consumer again + m.remove(consumerWithFilter); + e.step(4); + } + + public void testRequiredBundleDependency() { + DependencyManager m = getDM(); + + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + Component consumerWithFilter = m.createComponent() + .setImplementation(new FilteredConsumerRequired(e)) + .add(m.createBundleDependency() + .setRequired(true) + .setFilter("(Bundle-SymbolicName=" + BSN + ")") + .setCallbacks("add", "remove") + ); + // add a consumer with a filter + m.add(consumerWithFilter); + e.waitForStep(1, 5000); + // remove the consumer again + m.remove(consumerWithFilter); + e.waitForStep(2, 5000); + } + + public void testRequiredBundleDependencyWithComponentArgInCallbackMethod() { + DependencyManager m = getDM(); + + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + Component consumerWithFilter = m.createComponent() + .setImplementation(new FilteredConsumerRequiredWithComponentArg(e)) + .add(m.createBundleDependency() + .setRequired(true) + .setFilter("(Bundle-SymbolicName=" + BSN + ")") + .setCallbacks("add", "remove") + ); + // add a consumer with a filter + m.add(consumerWithFilter); + e.waitForStep(1, 5000); + // remove the consumer again + m.remove(consumerWithFilter); + e.waitForStep(2, 5000); + } + + static class Consumer { + private volatile int m_count = 0; + + public void add(Bundle b) { + System.out.println("Consumer.add(" + b.getSymbolicName() + ")"); + Assert.assertNotNull("bundle instance must not be null", b); + m_count++; + } + + public void check() { + Assert.assertTrue("we should have found at least one bundle", m_count > 0); + } + + public void remove(Bundle b) { + System.out.println("Consumer.remove(" + b.getSymbolicName() + ")"); + m_count--; + } + + public void doubleCheck() { + Assert.assertEquals("all bundles we found should have been removed again", 0, m_count); + } + } + + static class FilteredConsumer { + private final Ensure m_ensure; + + public FilteredConsumer(Ensure e) { + m_ensure = e; + } + + public void add(Bundle b) { + m_ensure.step(1); + } + + public void remove(Bundle b) { + m_ensure.step(3); + } + } + + static class FilteredConsumerRequired { + private final Ensure m_ensure; + + public FilteredConsumerRequired(Ensure e) { + m_ensure = e; + } + + public void add(Bundle b) { + System.out.println("Bundle is " + b); +// Assert.assertNotNull(b); + if (b.getSymbolicName().equals(BSN)) { + m_ensure.step(1); + } + } + + public void remove(Bundle b) { + Assert.assertNotNull(b); + if (b.getSymbolicName().equals(BSN)) { + m_ensure.step(2); + } + } + } + + static class FilteredConsumerRequiredWithComponentArg extends FilteredConsumerRequired { + public FilteredConsumerRequiredWithComponentArg(Ensure e) { + super(e); + } + + public void add(Component component, Bundle b) { + Assert.assertNotNull(component); + super.add(b); + } + + public void remove(Component component, Bundle b) { + Assert.assertNotNull(component); + super.remove(b); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ComponentStateListenerTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ComponentStateListenerTest.java new file mode 100644 index 00000000000..5d24b1e536b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ComponentStateListenerTest.java @@ -0,0 +1,255 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentState; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; + +/** + * @author Felix Project Team + */ +public class ComponentStateListenerTest extends TestBase { + + public void testComponentLifeCycleCallbacks() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a simple service component + Component s = m.createComponent().setImplementation(new ComponentInstance(e)); + // add it, and since it has no dependencies, it should be activated immediately + m.add(s); + // remove it so it gets destroyed + m.remove(s); + // ensure we executed all steps inside the component instance + e.step(6); + } + + static class ComponentInstance { + private final Ensure m_ensure; + public ComponentInstance(Ensure e) { + m_ensure = e; + m_ensure.step(1); + } + public void init() { + m_ensure.step(2); + } + public void start() { + m_ensure.step(3); + } + public void stop() { + m_ensure.step(4); + } + public void destroy() { + m_ensure.step(5); + } + } + + public void testCustomComponentLifeCycleCallbacks() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a simple service component + Component s = m.createComponent() .setImplementation(new CustomComponentInstance(e)).setCallbacks("a", "b", "c", "d"); + // add it, and since it has no dependencies, it should be activated immediately + m.add(s); + // remove it so it gets destroyed + m.remove(s); + // ensure we executed all steps inside the component instance + e.step(6); + } + + static class CustomComponentInstance { + private final Ensure m_ensure; + public CustomComponentInstance(Ensure e) { + m_ensure = e; + m_ensure.step(1); + } + public void a() { + m_ensure.step(2); + } + public void b() { + m_ensure.step(3); + } + public void c() { + m_ensure.step(4); + } + public void d() { + m_ensure.step(5); + } + } + + public void testComponentStateListingLifeCycle() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a simple service component + ComponentStateListeningInstance implementation = new ComponentStateListeningInstance(e); + Component s = m.createComponent().setInterface(MyInterface.class.getName(), null).setImplementation(implementation); + // add the state listener + s.add(implementation); + // add it, and since it has no dependencies, it should be activated immediately + m.add(s); + // remove it so it gets destroyed + m.remove(s); + // remove the state listener + s.remove(implementation); + // ensure we executed all steps inside the component instance + e.step(10); + } + + public static interface MyInterface {} + + static class ComponentStateListeningInstance implements MyInterface, ComponentStateListener { + volatile ServiceRegistration m_registration; + private final Ensure m_ensure; + + public ComponentStateListeningInstance(Ensure e) { + m_ensure = e; + m_ensure.step(1); + } + + private void debug() { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + System.out.println("AT: " + stackTrace[2].getClassName() + "." + stackTrace[2].getMethodName() + "():" + stackTrace[2].getLineNumber()); + } + + public void init(Component c) { + debug(); + m_ensure.step(2); + } + + public void start(Component c) { + debug(); + m_ensure.step(4); + } + public void stop(Component c) { + debug(); + m_ensure.step(7); + } + + public void destroy(Component c) { + debug(); + m_ensure.step(9); + } + + @Override + public void changed(Component c, ComponentState state) { + debug(); + switch (state) { + case STARTING: + m_ensure.step(3); + break; + case TRACKING_OPTIONAL: + m_ensure.step(5); + ServiceReference reference = m_registration.getReference(); + Assert.assertNotNull("Service not yet registered.", reference); + break; + case STOPPING: + m_ensure.step(6); + break; + case STOPPED: + m_ensure.step(8); + break; + default: + break; + } + } + } + + public void testDynamicComponentStateListingLifeCycle() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a simple service component + Component s = m.createComponent().setInterface(MyInterface.class.getName(), null).setImplementation(new DynamicComponentStateListeningInstance(e)); + // add it, and since it has no dependencies, it should be activated immediately + m.add(s); + // remove it so it gets destroyed + m.remove(s); + // ensure we executed all steps inside the component instance + e.step(10); + } + + static class DynamicComponentStateListeningInstance implements MyInterface, ComponentStateListener { + volatile ServiceRegistration m_registration; + private final Ensure m_ensure; + + public DynamicComponentStateListeningInstance(Ensure e) { + m_ensure = e; + m_ensure.step(1); + } + + private void debug() { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + System.out.println("AT: " + stackTrace[2].getClassName() + "." + stackTrace[2].getMethodName() + "():" + stackTrace[2].getLineNumber()); + } + + public void init(Component c) { + debug(); + m_ensure.step(2); + c.add(this); + } + + public void start(Component c) { + debug(); + m_ensure.step(4); + } + public void stop(Component c) { + debug(); + m_ensure.step(7); + } + + public void destroy(Component c) { + debug(); + m_ensure.step(9); + c.remove(this); + } + + @Override + public void changed(Component c, ComponentState state) { + debug(); + switch (state) { + case STARTING: + m_ensure.step(3); + break; + case TRACKING_OPTIONAL: + m_ensure.step(5); + ServiceReference reference = m_registration.getReference(); + Assert.assertNotNull("Service not yet registered.", reference); + break; + case STOPPING: + m_ensure.step(6); + break; + case STOPPED: + m_ensure.step(8); + break; + default: + break; + } + } + } + +} \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ComponentTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ComponentTest.java new file mode 100644 index 00000000000..bb4ca749836 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ComponentTest.java @@ -0,0 +1,106 @@ +/* + * 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.dm.itest.api; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class ComponentTest extends TestBase { + private final Ensure m_ensure = new Ensure(); + + public void testSimple() throws Exception { + final DependencyManager dm = getDM(); + Component consumer = dm.createComponent(); + consumer + .setImplementation(new Consumer()) + .add(dm.createServiceDependency() + .setService(Provider.class, "(name=provider2)") + .setRequired(true) + .setCallbacks("add", "remove")) + .add(dm.createServiceDependency() + .setService(Provider.class, "(name=provider1)") + .setRequired(true) + .setAutoConfig("m_autoConfiguredProvider")); + + Dictionary props = new Hashtable(); + props.put("name", "provider1"); + Component provider1 = dm.createComponent() + .setImplementation(new Provider() { public String toString() { return "provider1";}}) + .setInterface(Provider.class.getName(), props); + props = new Hashtable(); + props.put("name", "provider2"); + Component provider2 = dm.createComponent() + .setImplementation(new Provider() { public String toString() { return "provider2";}}) + .setInterface(Provider.class.getName(), props); + dm.add(provider1); + dm.add(provider2); + dm.add(consumer); + m_ensure.waitForStep(2, 5000); + dm.remove(provider1); + dm.remove(provider2); + m_ensure.waitForStep(5, 5000); + dm.clear(); + } + + public static interface Provider { + } + + public class Consumer { + Provider m_provider; + Provider m_autoConfiguredProvider; + + void add(Map props, Provider provider) { + Assert.assertNotNull(provider); + Assert.assertEquals("provider2", props.get("name")); + m_provider = provider; + m_ensure.step(1); + } + + void start() { + Assert.assertNotNull(m_autoConfiguredProvider); + Assert.assertEquals("provider1", m_autoConfiguredProvider.toString()); + m_ensure.step(2); + } + + void stop() { + m_ensure.step(3); + } + + void destroy() { + m_ensure.step(4); + } + + void remove(Provider provider) { + Assert.assertEquals(m_provider, provider); + m_ensure.step(5); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ComponentTracker.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ComponentTracker.java new file mode 100644 index 00000000000..27d0b37dea4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ComponentTracker.java @@ -0,0 +1,66 @@ +/* + * 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.dm.itest.api; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentState; +import org.apache.felix.dm.ComponentStateListener; + +/** + * Helper class used to wait for a group of components to be started and stopped. + * + * @author Felix Project Team + */ +public class ComponentTracker implements ComponentStateListener { + + private final CountDownLatch m_startLatch; + private final CountDownLatch m_stopLatch; + + public ComponentTracker(int startCount, int stopCount) { + m_startLatch = new CountDownLatch(startCount); + m_stopLatch = new CountDownLatch(stopCount); + } + + @Override + public void changed(Component c, ComponentState state) { + switch (state) { + case TRACKING_OPTIONAL: + m_startLatch.countDown(); + break; + + case INACTIVE: + m_stopLatch.countDown(); + break; + + default: + } + } + + public boolean awaitStarted(long millis) throws InterruptedException { + return m_startLatch.await(millis, TimeUnit.MILLISECONDS); + } + + public boolean awaitStopped(long millis) throws InterruptedException { + return m_stopLatch.await(millis, TimeUnit.MILLISECONDS); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/CompositionTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/CompositionTest.java new file mode 100644 index 00000000000..bfdcdd12baf --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/CompositionTest.java @@ -0,0 +1,108 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + + +/** + * @author Felix Project Team + */ +public class CompositionTest extends TestBase { + public void testComposition() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), null); + Component sc = m.createComponent().setImplementation(new ServiceConsumer(e)) + .setComposition("getComposition") + .add(m.createServiceDependency().setService(ServiceInterface.class).setRequired(true).setCallbacks("add", null)); + m.add(sp); + m.add(sc); + // ensure we executed all steps inside the component instance + e.step(6); + m.clear(); + } + + static interface ServiceInterface { + public void invoke(); + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke() { + m_ensure.step(4); + } + } + + static class ServiceConsumer { + private final Ensure m_ensure; + private ServiceConsumerComposite m_composite; + @SuppressWarnings("unused") + private ServiceInterface m_service; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + m_composite = new ServiceConsumerComposite(m_ensure); + } + + public Object[] getComposition() { + return new Object[] { this, m_composite }; + } + + void add(ServiceInterface service) { + m_ensure.step(1); + m_service = service; // This method seems to not being called anymore + } + + void start() { + m_composite.invoke(); + m_ensure.step(5); + } + } + + static class ServiceConsumerComposite { + ServiceInterface m_service; + private Ensure m_ensure; + + ServiceConsumerComposite(Ensure ensure) + { + m_ensure = ensure; + } + + void add(ServiceInterface service) { + + m_ensure.step(2); + m_service = service; + } + + void invoke() + { + m_ensure.step(3); + m_service.invoke(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ConfigurationCreator.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ConfigurationCreator.java new file mode 100644 index 00000000000..b5411b1f39d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ConfigurationCreator.java @@ -0,0 +1,75 @@ +/* + * 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.dm.itest.api; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.apache.felix.dm.itest.util.Ensure; +import org.junit.Assert; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * Helper component used to create configurations. + */ +public class ConfigurationCreator { + private volatile ConfigurationAdmin m_ca; + private final Ensure m_ensure; + Configuration m_conf; + final String m_pid; + final int m_initStep; + final Dictionary m_parameters = new Hashtable<>(); + + public ConfigurationCreator(Ensure e, String pid, int initStep) { + this(e, pid, initStep, new String[] {}); + } + + public ConfigurationCreator(Ensure e, String pid, int initStep, String ... keyvalues) { + m_ensure = e; + m_pid = pid; + m_initStep = initStep; + for (int i = 0; i < keyvalues.length; i ++) { + String[] tokens = keyvalues[i].split("="); + String k = tokens[0].trim(); + Object v = tokens[1].trim(); + m_parameters.put(k, v); + } + } + + public void init() { + try { + Assert.assertNotNull(m_ca); + m_ensure.step(m_initStep); + m_conf = m_ca.getConfiguration(m_pid, null); + if (m_parameters.size() == 0) { + m_parameters.put("testkey", "testvalue"); + } + m_conf.update(m_parameters); + } + catch (IOException e) { + Assert.fail("Could not create configuration: " + e.getMessage()); + } + } + + public void destroy() throws IOException { + m_conf.delete(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ConfigurationDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ConfigurationDependencyTest.java new file mode 100644 index 00000000000..23ab0d1924b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ConfigurationDependencyTest.java @@ -0,0 +1,399 @@ +/* + * 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.dm.itest.api; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Properties; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedService; + + +/** + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class ConfigurationDependencyTest extends TestBase { + final static String PID = "ConfigurationDependencyTest.pid"; + + /** + * Tests that we can provision a type-safe configuration to a component. + */ + public void testComponentWithRequiredConfigurationWithTypeSafeConfiguration() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component s1 = m.createComponent().setImplementation(new ConfigurationConsumerWithTypeSafeConfiguration(e)).add(m.createConfigurationDependency().setCallback(null, "updated", MyConfig.class).setPid(PID).setPropagate(true)); + Component s2 = m.createComponent().setImplementation(new ConfigurationCreator(e, PID)).add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)); + m.add(s1); + m.add(s2); + e.waitForStep(1, 5000); // s2 called in init + e.waitForStep(3, 5000); // s1 called in updated(), then in init() + m.remove(s2); // remove conf + e.waitForStep(6, 5000); // s2 destroyed, s1 called in updated(null), s1 called in destroy() + m.remove(s1); + } + + public void testComponentWithRequiredConfigurationAndServicePropertyPropagation() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component s1 = m.createComponent().setImplementation(new ConfigurationConsumer(e)).setInterface(Runnable.class.getName(), null).add(m.createConfigurationDependency().setPid(PID).setPropagate(true)); + Component s2 = m.createComponent().setImplementation(new ConfigurationCreator(e, PID)).add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)); + Component s3 = m.createComponent().setImplementation(new ConfiguredServiceConsumer(e)).add(m.createServiceDependency().setService(Runnable.class, ("(testkey=testvalue)")).setRequired(true)); + m.add(s1); + m.add(s2); + m.add(s3); + e.waitForStep(4, 5000); + m.remove(s1); + m.remove(s2); + m.remove(s3); + // ensure we executed all steps inside the component instance + e.step(6); + } + + public void testComponentWithRequiredConfigurationWithComponentArgAndServicePropertyPropagation() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component s1 = m.createComponent().setImplementation(new ConfigurationConsumerWithComponentArg(e)).setInterface(Runnable.class.getName(), null).add(m.createConfigurationDependency().setPid(PID).setPropagate(true)); + Component s2 = m.createComponent().setImplementation(new ConfigurationCreator(e, PID)).add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)); + Component s3 = m.createComponent().setImplementation(new ConfiguredServiceConsumer(e)).add(m.createServiceDependency().setService(Runnable.class, ("(testkey=testvalue)")).setRequired(true)); + m.add(s1); + m.add(s2); + m.add(s3); + e.waitForStep(4, 50000000); + m.remove(s1); + m.remove(s2); + m.remove(s3); + // ensure we executed all steps inside the component instance + e.step(6); + } + + public void testComponentWithRequiredConfigurationAndCallbackInstanceAndServicePropertyPropagation() { + Ensure e = new Ensure(); + ConfigurationConsumerCallbackInstance callbackInstance = new ConfigurationConsumerCallbackInstance(e); + testComponentWithRequiredConfigurationAndCallbackInstanceAndServicePropertyPropagation(callbackInstance, "updateConfiguration", e); + } + + public void testComponentWithRequiredConfigurationAndCallbackInstanceWithComponentArgAndServicePropertyPropagation() { + Ensure e = new Ensure(); + ConfigurationConsumerCallbackInstanceWithComponentArg callbackInstance = new ConfigurationConsumerCallbackInstanceWithComponentArg(e); + testComponentWithRequiredConfigurationAndCallbackInstanceAndServicePropertyPropagation(callbackInstance, "updateConfiguration", e); + } + + public void testComponentWithRequiredConfigurationAndCallbackInstanceAndServicePropertyPropagation + (Object callbackInstance, String updateMethod, Ensure e) { + DependencyManager m = getDM(); + // create a service provider and consumer + Component s1 = m.createComponent().setImplementation(new ConfigurationConsumerWithCallbackInstance(e)) + .setInterface(Runnable.class.getName(), null) + .add(m.createConfigurationDependency().setPid(PID).setPropagate(true).setCallback(callbackInstance, updateMethod)); + Component s2 = m.createComponent().setImplementation(new ConfigurationCreator(e, PID)) + .add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)); + Component s3 = m.createComponent().setImplementation(new ConfiguredServiceConsumer(e)) + .add(m.createServiceDependency().setService(Runnable.class, ("(testkey=testvalue)")).setRequired(true)); + m.add(s1); + m.add(s2); + m.add(s3); + e.waitForStep(4, 5000); + m.remove(s1); + m.remove(s2); + m.remove(s3); + // ensure we executed all steps inside the component instance + e.step(6); + } + + public void testFELIX2987() { + // mimics testComponentWithRequiredConfigurationAndServicePropertyPropagation + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component s1 = m.createComponent().setImplementation(new ConfigurationConsumer2(e)).setInterface(Runnable.class.getName(), null).add(m.createConfigurationDependency().setPid(PID).setPropagate(true)); + Component s2 = m.createComponent().setImplementation(new ConfigurationCreator(e, PID)).add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)); + Component s3 = m.createComponent().setImplementation(new ConfiguredServiceConsumer(e)).add(m.createServiceDependency().setService(Runnable.class, ("(testkey=testvalue)")).setRequired(true)); + m.add(s1); + m.add(s2); + m.add(s3); + e.waitForStep(4, 5000); + m.remove(s1); + m.remove(s2); + m.remove(s3); + // ensure we executed all steps inside the component instance + e.step(6); + } + + public void testFELIX4907() { + // This test validates updated(null) is not invoked when a component that have a configuration dependency is stopped. + DependencyManager m = getDM(); + Ensure e = new Ensure(); + Component s1 = m.createComponent().setImplementation(new ConfigurationConsumer3(e)).add(m.createConfigurationDependency().setPid(PID)); + Component s2 = m.createComponent().setImplementation(new ConfigurationCreator(e, PID)).add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)); + m.add(s1); + m.add(s2); + e.waitForStep(3, 5000); + m.remove(s1); + e.waitForStep(4, 5000); + m.remove(s2); + // ensure we executed all steps inside the component instance + e.step(6); + } + + public void testFELIX4907_updated_with_null_dictionary_called_when_configuration_is_lost() { + DependencyManager m = getDM(); + Ensure e = new Ensure(); + Component s1 = m.createComponent().setImplementation(new ConfigurationConsumer4(e)).add(m.createConfigurationDependency().setPid(PID)); + Component s2 = m.createComponent().setImplementation(new ConfigurationCreator(e, PID)).add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)); + m.add(s1); + m.add(s2); + e.waitForStep(3, 5000); // component configured and started. + m.remove(s2); // configuration will be removed + e.waitForStep(4, 5000); // configuration creator destroyed + + e.waitForStep(5, 5000); // consumer called in updated(null) + e.waitForStep(6, 5000); // consumer des + m.remove(s1); + // ensure we executed all steps inside the component instance + e.step(7); + } + + class ConfigurationCreator { + private volatile ConfigurationAdmin m_ca; + private final Ensure m_ensure; + Configuration m_conf; + final String m_pid; + + public ConfigurationCreator(Ensure e, String pid) { + m_ensure = e; + m_pid = pid; + } + + public void init() { + try { + warn("ConfigurationCreator.init"); + Assert.assertNotNull(m_ca); + m_ensure.step(1); + m_conf = m_ca.getConfiguration(m_pid, null); + Hashtable props = new Properties(); + props.put("testkey", "testvalue"); + m_conf.update(props); + } + catch (IOException e) { + Assert.fail("Could not create configuration: " + e.getMessage()); + } + } + + public void destroy() throws IOException { + warn("ConfigurationCreator.destroy"); + m_ensure.step(); + m_conf.delete(); + } + } + + static class ConfigurationConsumer implements ManagedService, Runnable { + protected final Ensure m_ensure; + + public ConfigurationConsumer(Ensure e) { + m_ensure = e; + } + + public void updated(Dictionary props) throws ConfigurationException { + Assert.assertNotNull(props); + m_ensure.step(2); + if (!"testvalue".equals(props.get("testkey"))) { + Assert.fail("Could not find the configured property."); + } + } + + public void run() { + m_ensure.step(4); + } + } + + static class ConfigurationConsumer2 extends ConfigurationConsumer { + public ConfigurationConsumer2(Ensure e) { + super(e); + } + } + + static class ConfigurationConsumer3 extends ConfigurationConsumer { + public ConfigurationConsumer3(Ensure e) { + super(e); + } + + public void updated(Dictionary props) throws ConfigurationException { + Assert.assertNotNull(props); + if (!"testvalue".equals(props.get("testkey"))) { + Assert.fail("Could not find the configured property."); + } + m_ensure.step(2); + } + + public void start() { + m_ensure.step(3); + } + + public void stop() { + m_ensure.step(4); + } + } + + static class ConfigurationConsumer4 extends ConfigurationConsumer { + volatile boolean m_configured; + + public ConfigurationConsumer4(Ensure e) { + super(e); + } + + public void updated(Dictionary props) throws ConfigurationException { + if (! m_configured) { + Assert.assertNotNull(props); + if (!"testvalue".equals(props.get("testkey"))) { + Assert.fail("Could not find the configured property."); + } + m_configured = true; + m_ensure.step(2); + } else { + m_ensure.step(5); // loosing configuration + } + } + + public void start() { + m_ensure.step(3); + } + + public void stop() { + m_ensure.step(6); // stopped after configuration is lost + } + } + + static class ConfigurationConsumerWithComponentArg extends ConfigurationConsumer { + public ConfigurationConsumerWithComponentArg(Ensure e) { + super(e); + } + + public void updatedWithComponentArg(Component component, Dictionary props) throws ConfigurationException { + Assert.assertNotNull(component); + super.updated(props); + } + } + + static class ConfigurationConsumerCallbackInstance { + private final Ensure m_ensure; + + public ConfigurationConsumerCallbackInstance(Ensure e) { + m_ensure = e; + } + + public void updateConfiguration(Dictionary props) throws Exception { + Assert.assertNotNull(props); + m_ensure.step(2); + if (!"testvalue".equals(props.get("testkey"))) { + Assert.fail("Could not find the configured property."); + } + } + } + + static class ConfigurationConsumerCallbackInstanceWithComponentArg extends ConfigurationConsumerCallbackInstance { + + public ConfigurationConsumerCallbackInstanceWithComponentArg(Ensure e) { + super(e); + } + + public void updateConfigurationWithComponentArg(Component component, Dictionary props) throws Exception { + Assert.assertNotNull(component); + super.updateConfiguration(props); + } + } + + static class ConfigurationConsumerWithCallbackInstance implements Runnable { + private final Ensure m_ensure; + + public ConfigurationConsumerWithCallbackInstance(Ensure e) { + m_ensure = e; + } + + public void run() { + m_ensure.step(4); + } + } + + static class ConfiguredServiceConsumer { + private final Ensure m_ensure; + private volatile Runnable m_runnable; + + public ConfiguredServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void init() { + m_ensure.step(3); + m_runnable.run(); + } + } + + static interface MyConfig { + String getTestkey(); + } + + static class ConfigurationConsumerWithTypeSafeConfiguration { + private final Ensure m_ensure; + + public ConfigurationConsumerWithTypeSafeConfiguration(Ensure e) { + m_ensure = e; + } + + // configuration updates is always the first invoked callback (before init). + public void updated(Component component, MyConfig cfg) throws ConfigurationException { + if (cfg != null) { + Assert.assertNotNull(component); + Assert.assertNotNull(cfg); + m_ensure.step(2); + if (!"testvalue".equals(cfg.getTestkey())) { + Assert.fail("Could not find the configured property."); + } + } else { + m_ensure.step(); + } + } + + // called after configuration has been injected. + public void init() { + m_ensure.step(3); + } + + public void destroy() { + m_ensure.step(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/DiagnosticsTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/DiagnosticsTest.java new file mode 100644 index 00000000000..d9f48d318da --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/DiagnosticsTest.java @@ -0,0 +1,546 @@ +/* + * 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.dm.itest.api; + +import java.util.List; +import java.util.Properties; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentDeclaration; +import org.apache.felix.dm.ConfigurationDependency; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ServiceDependency; +import org.apache.felix.dm.context.AbstractDependency; +import org.apache.felix.dm.context.DependencyContext; +import org.apache.felix.dm.diagnostics.CircularDependency; +import org.apache.felix.dm.diagnostics.DependencyGraph; +import org.apache.felix.dm.diagnostics.DependencyGraph.ComponentState; +import org.apache.felix.dm.diagnostics.DependencyGraph.DependencyState; +import org.apache.felix.dm.diagnostics.MissingDependency; +import org.apache.felix.dm.itest.util.TestBase; + +public class DiagnosticsTest extends TestBase { + + // there are two components (TestComponent and HelloWorldServiceFactory) created by + // org.apache.felix.dm.itest.bundle + // These component is always registered, so we only need to take them into + // account when we build the graph with ALL components + private static final int InitialComponentCount = 2; + + private boolean checkComponentCount(int expected, int count) { + return count == expected + InitialComponentCount; + } + + public void testWithoutComponents() throws Exception { + + DependencyGraph graph = DependencyGraph.getGraph( + ComponentState.ALL, + DependencyState.ALL); + + assertTrue(checkComponentCount(0, graph.getAllComponents().size())); + assertTrue(graph.getAllDependencies().isEmpty()); + } + + public void testSingleComponent() throws Exception { + + DependencyManager dm = getDM(); + + Component component = dm.createComponent() + .setImplementation(Object.class); + + dm.add(component); + + DependencyGraph graph = DependencyGraph.getGraph( + ComponentState.ALL, DependencyState.ALL); + + assertTrue(checkComponentCount(1, graph.getAllComponents().size())); + assertTrue(graph.getAllDependencies().isEmpty()); + + graph = DependencyGraph.getGraph( + ComponentState.UNREGISTERED, DependencyState.ALL_UNAVAILABLE); + + assertTrue(graph.getAllComponents().isEmpty()); + assertTrue(graph.getAllDependencies().isEmpty()); + } + + public void testServiceDependencyMissing() throws Exception { + + DependencyManager dm = getDM(); + + ServiceDependency serviceDependency1 = dm.createServiceDependency() + .setService(S1.class) + .setRequired(true); + ServiceDependency serviceDependency2 = dm.createServiceDependency() + .setService(S2.class) + .setRequired(true); + + Component component1 = dm.createComponent() + .setImplementation(C0.class) + .add(serviceDependency1); + Component component2 = dm.createComponent() + .setImplementation(S1Impl1.class) + .setInterface(S1.class.getName(), null) + .add(serviceDependency2); + + dm.add(component1); + dm.add(component2); + + DependencyGraph graph = DependencyGraph.getGraph( + ComponentState.UNREGISTERED, DependencyState.REQUIRED_UNAVAILABLE); + + assertEquals(2, graph.getAllComponents().size()); + assertEquals(2, graph.getAllDependencies().size()); + + List missingDependencies = graph.getMissingDependencies("service"); + assertEquals(1, missingDependencies.size()); + assertEquals(S2.class.getName(), missingDependencies.get(0).getName()); + + assertTrue(graph.getMissingDependencies("configuration").isEmpty()); + assertTrue(graph.getMissingDependencies("bundle").isEmpty()); + assertTrue(graph.getMissingDependencies("resource").isEmpty()); + } + + public void testConfigurationDependencyMissing() throws Exception { + + DependencyManager dm = getDM(); + + ConfigurationDependency configurationDependency1 = dm.createConfigurationDependency() + .setPid("missing.configuration.pid"); + + Component component1 = dm.createComponent() + .setImplementation(Object.class) + .add(configurationDependency1); + m_dm.add(component1); + + DependencyGraph graph = DependencyGraph.getGraph(ComponentState.UNREGISTERED, DependencyState.REQUIRED_UNAVAILABLE); + + assertEquals(1, graph.getAllComponents().size()); + assertEquals(1, graph.getAllDependencies().size()); + + List missingServiceDependencies = graph.getMissingDependencies("service"); + assertTrue(missingServiceDependencies.isEmpty()); + + List missingConfigDependencies = graph.getMissingDependencies("configuration"); + assertEquals(1, missingConfigDependencies.size()); + + MissingDependency missingConfigDependency = missingConfigDependencies.get(0); + assertEquals("missing.configuration.pid", missingConfigDependency.getName()); + } + + public void testProvidersWithoutProperties() throws Exception { + DependencyManager dm = getDM(); + + ServiceDependency serviceDependency1 = dm.createServiceDependency() + .setService(S1.class) + .setRequired(true); + + Component component1 = dm.createComponent() + .setImplementation(C0.class) + .add(serviceDependency1); + + Component component2 = dm.createComponent() + .setImplementation(S1Impl1.class) + .setInterface(S1.class.getName(), null); + Component component3 = dm.createComponent() + .setImplementation(S1Impl2.class) + .setInterface(S1.class.getName(), null); + + dm.add(component1); + dm.add(component2); + dm.add(component3); + + DependencyGraph graph = DependencyGraph.getGraph(ComponentState.ALL, DependencyState.ALL); + + List providers = graph.getProviders(serviceDependency1); + assertEquals(2, providers.size()); + assertTrue(providers.contains(component2)); + assertTrue(providers.contains(component3)); + } + + public void testProvidersWithProperties() throws Exception { + + DependencyManager dm = getDM(); + + ServiceDependency serviceDependency1 = dm.createServiceDependency() + .setService(S1.class, "(key=value)") + .setRequired(true); + + Component component1 = dm.createComponent() + .setImplementation(C0.class) + .add(serviceDependency1); + + Properties component2Properties = new Properties(); + component2Properties.put("key", "value"); + Properties component4Properties = new Properties(); + component4Properties.put("key", "otherValue"); + + Component component2 = dm.createComponent() + .setImplementation(S1Impl1.class) + .setInterface(S1.class.getName(), component2Properties); + Component component3 = dm.createComponent() + .setImplementation(S1Impl2.class) + .setInterface(S1.class.getName(), null); + Component component4 = dm.createComponent() + .setImplementation(S1Impl3.class) + .setInterface(S1.class.getName(), component4Properties); + + m_dm.add(component1); + m_dm.add(component2); + m_dm.add(component3); + m_dm.add(component4); + + DependencyGraph graph = DependencyGraph.getGraph(ComponentState.ALL, DependencyState.ALL); + + List providers = graph.getProviders(serviceDependency1); + assertEquals(1, providers.size()); + assertTrue(providers.contains(component2)); + assertFalse(providers.contains(component3)); + assertFalse(providers.contains(component4)); + + } + + public void testCircularDependencies() throws Exception { + + DependencyManager dm = getDM(); + + Component component0 = dm.createComponent() + .setImplementation(C0.class) + .add(dm.createServiceDependency() + .setService(S1.class) + .setRequired(true)); + + Component component1 = dm.createComponent() + .setImplementation(S1Impl1.class) + .setInterface(S1.class.getName(), null) + .add(dm.createServiceDependency() + .setService(S2.class) + .setRequired(true)); + Component component2 = dm.createComponent() + .setImplementation(S2Impl1.class) + .setInterface(S2.class.getName(), null) + .add(dm.createServiceDependency() + .setService(S1.class) + .setRequired(true)); + + m_dm.add(component0); + m_dm.add(component1); + m_dm.add(component2); + + DependencyGraph graph = DependencyGraph.getGraph(ComponentState.UNREGISTERED,DependencyState.REQUIRED_UNAVAILABLE); + List circularDependencies = graph.getCircularDependencies(); + + assertEquals(1, circularDependencies.size()); + + List circularDependencyComponents = circularDependencies.get(0).getComponents(); + assertTrue(circularDependencyComponents.contains(component1)); + assertTrue(circularDependencyComponents.contains(component2)); + assertFalse(circularDependencyComponents.contains(component0)); + + } + + public void testWithTwoProvidersOneUnavailable() { + DependencyManager dm = getDM(); + + Component component0 = dm.createComponent() + .setImplementation(C0.class) + .add(dm.createServiceDependency() + .setService(S1.class) + .setRequired(true)); + Component component1 = dm.createComponent() + .setImplementation(S1Impl1.class) + .setInterface(S1.class.getName(), null); + Component component2 = dm.createComponent() + .setImplementation(S1Impl2.class) + .setInterface(S1.class.getName(), null) + .add(dm.createServiceDependency() + .setService(S2.class) + .setRequired(true)); + + dm.add(component0); + dm.add(component1); + dm.add(component2); + + DependencyGraph graph = DependencyGraph.getGraph(ComponentState.UNREGISTERED, DependencyState.REQUIRED_UNAVAILABLE); + + assertEquals(1, graph.getAllComponents().size()); + List missingDependencies = graph.getMissingDependencies("service"); + assertEquals(1, missingDependencies.size()); + + MissingDependency missingDependency = missingDependencies.get(0); + assertTrue(missingDependency.getName().equals(S2.class.getName())); + + } + + public void testGraphsWithSeveralComponentDependencyCombinations() throws Exception { + + DependencyManager dm = getDM(); + + Component component0 = dm.createComponent() + .setImplementation(C0.class) + .add(dm.createServiceDependency() + .setService(S1.class) + .setRequired(true)) + .add(dm.createServiceDependency() + .setService(S3.class) + .setRequired(true)) + .add(dm.createServiceDependency() + .setService(S4.class) + .setRequired(true)); + Component component1 = dm.createComponent() + .setImplementation(C1.class) + .add(dm.createServiceDependency() + .setService(S5.class) + .setRequired(true)) + .add(dm.createServiceDependency() + .setService(S6.class) + .setRequired(true)) + .add(dm.createServiceDependency() + .setService(S7.class) + .setRequired(true)); + Component s1Impl1 = dm.createComponent() + .setImplementation(S1Impl1.class) + .setInterface(S1.class.getName(), null) + .add(dm.createServiceDependency() + .setService(S2.class) + .setRequired(true)); + Component s1Impl2 = dm.createComponent() + .setImplementation(S1Impl2.class) + .setInterface(S1.class.getName(), null); + + Component s3Impl1 = dm.createComponent() + .setImplementation(S3Impl1.class) + .setInterface(S3.class.getName(), null) + .add(dm.createConfigurationDependency() + .setPid("missing.config.pid")); + Component s3s4Impl = dm.createComponent() + .setImplementation(S3S4Impl.class) + .setInterface( new String[] {S3.class.getName(), S4.class.getName()}, null); + Component s4Impl1 = dm.createComponent() + .setImplementation(S4Impl1.class) + .setInterface(S4.class.getName(), null); + Component s5Impl1 = dm.createComponent() + .setImplementation(S5Impl1.class) + .setInterface(S5.class.getName(), null); + Component s6s7Impl = dm.createComponent() + .setImplementation(S6S7Impl.class) + .setInterface( new String[] {S6.class.getName(), S7.class.getName()}, null) + .add(dm.createServiceDependency() + .setService(S8.class) + .setRequired(true)); + Component s8Impl1 = dm.createComponent() + .setImplementation(S8Impl1.class) + .setInterface(S8.class.getName(), null) + .add(dm.createServiceDependency() + .setService(S6.class) + .setRequired(true)); + dm.add(component0); + dm.add(component1); + dm.add(s1Impl1); dm.add(s1Impl2); + dm.add(s3Impl1); dm.add(s3s4Impl); + dm.add(s4Impl1); dm.add(s5Impl1); + dm.add(s6s7Impl); dm.add(s8Impl1); + + + // graph containing all components and dependencies + DependencyGraph graph = DependencyGraph.getGraph(ComponentState.ALL, DependencyState.ALL); + + List allComponents = graph.getAllComponents(); + assertTrue(checkComponentCount(10, allComponents.size())); + + List missingDependencies = graph.getMissingDependencies("service"); + assertEquals(1, missingDependencies.size()); + + missingDependencies = graph.getMissingDependencies("configuration"); + assertEquals(1, missingDependencies.size()); + + List circularDependencies = graph.getCircularDependencies(); + assertEquals(1, circularDependencies.size()); + CircularDependency circularDependency = circularDependencies.get(0); + + assertEquals(3, circularDependency.getComponents().size()); + assertTrue(circularDependency.getComponents().contains(s6s7Impl)); + assertTrue(circularDependency.getComponents().contains(s8Impl1)); + + // graph containing unregistered components and unavailable required dependencies + graph = null; + graph = DependencyGraph.getGraph(ComponentState.UNREGISTERED, DependencyState.REQUIRED_UNAVAILABLE); + + List unregComponents = graph.getAllComponents(); + assertEquals(5, unregComponents.size()); + assertTrue(unregComponents.contains(s1Impl1)); + assertTrue(unregComponents.contains(s3Impl1)); + assertTrue(unregComponents.contains(component1)); + assertTrue(unregComponents.contains(s6s7Impl)); + assertTrue(unregComponents.contains(s8Impl1)); + assertFalse(unregComponents.contains(component0)); + + circularDependencies = graph.getCircularDependencies(); + assertEquals(1, circularDependencies.size()); + circularDependency = circularDependencies.get(0); + + assertEquals(3, circularDependency.getComponents().size()); + assertTrue(circularDependency.getComponents().contains(s6s7Impl)); + assertTrue(circularDependency.getComponents().contains(s8Impl1)); + + missingDependencies = graph.getMissingDependencies("service"); + assertEquals(1, missingDependencies.size()); + + missingDependencies = graph.getMissingDependencies("configuration"); + assertEquals(1, missingDependencies.size()); + + // call getCircularDependencies again on the same graph + circularDependencies = graph.getCircularDependencies(); + assertEquals(1, circularDependencies.size()); + circularDependency = circularDependencies.get(0); + + assertEquals(3, circularDependency.getComponents().size()); + assertTrue(circularDependency.getComponents().contains(s6s7Impl)); + assertTrue(circularDependency.getComponents().contains(s8Impl1)); + + List allMissingDependencies = graph.getMissingDependencies(null); + assertEquals(2, allMissingDependencies.size()); + + } + + public void testMissingCustomDependencies() { + + DependencyManager dm = getDM(); + + Component component0 = dm.createComponent() + .setImplementation(C0.class) + .add(dm.createServiceDependency() + .setService(S1.class) + .setRequired(true)) + .add(dm.createServiceDependency() + .setService(S2.class) + .setRequired(true)); + + + Component component1 = dm.createComponent() + .setImplementation(S1Impl1.class) + .setInterface(S1.class.getName(), null) + .add(ToggleDependency.create()); + + m_dm.add(component0); + m_dm.add(component1); + + DependencyGraph graph = DependencyGraph.getGraph(ComponentState.UNREGISTERED,DependencyState.REQUIRED_UNAVAILABLE); + + List missingCustomDependencies = graph.getMissingCustomDependencies(); + assertEquals(1, missingCustomDependencies.size()); + + MissingDependency missingCustomeDependency = missingCustomDependencies.get(0); + assertTrue(missingCustomeDependency.getType().equals("toggle")); + assertTrue(missingCustomeDependency.getName().equals("false")); + + List missingDependencies = graph.getMissingDependencies("service"); + assertEquals(1, missingDependencies.size()); + } + + + static interface S1 {} + static interface S2 {} + static interface S3 {} + static interface S4 {} + static interface S5 {} + static interface S6 {} + static interface S7 {} + static interface S8 {} + static class C0 { + public C0() {} + } + static class C1 { + public C1() {} + } + static class S1Impl1 implements S1 { + public S1Impl1(){} + } + static class S1Impl2 implements S1 { + public S1Impl2() {} + } + static class S1Impl3 implements S1 { + public S1Impl3() {} + } + static class S2Impl1 implements S2 { + public S2Impl1() {} + } + static class S3Impl1 implements S3 { + public S3Impl1() {} + } + static class S3S4Impl implements S3, S4 { + public S3S4Impl() {} + } + static class S4Impl1 implements S4 { + public S4Impl1() {} + } + static class S5Impl1 implements S5 { + public S5Impl1() {} + } + static class S6S7Impl implements S6, S7 { + public S6S7Impl() {} + } + static class S8Impl1 implements S8 { + public S8Impl1() {} + } + + static class CA { + public CA(){} + } + + static interface ToggleDependency extends Dependency { + static ToggleDependency create() { + return new ToggleDependencyImpl(); + } + + } + + static class ToggleDependencyImpl extends AbstractDependency implements ToggleDependency { + + public ToggleDependencyImpl() { + setRequired(true); + } + + public ToggleDependencyImpl(ToggleDependencyImpl prototype) { + super(prototype); + } + + @Override + public Class getAutoConfigType() { + return null; + } + + @Override + public DependencyContext createCopy() { + return new ToggleDependencyImpl(this); + } + + @Override + public String getSimpleName() { + return "" + isAvailable(); + } + + @Override + public String getType() { + return "toggle"; + } + + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/DynamicProxyAspectTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/DynamicProxyAspectTest.java new file mode 100644 index 00000000000..b5e593849f7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/DynamicProxyAspectTest.java @@ -0,0 +1,199 @@ +/** + * 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.dm.itest.api; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * @author Felix Project Team + */ +@SuppressWarnings({"rawtypes"}) +public class DynamicProxyAspectTest extends TestBase { + public void testImplementGenericAspectWithDynamicProxy() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + // create two service providers, each providing a different service interface + Component sp1 = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), null); + Component sp2 = m.createComponent().setImplementation(new ServiceProvider2(e)).setInterface(ServiceInterface2.class.getName(), null); + + // create a dynamic proxy based aspect and hook it up to both services + Component a1 = m.createAspectService(ServiceInterface.class, null, 10, "m_service") + .setFactory(new Factory(e, ServiceInterface.class, "ServiceInterfaceProxy"), "create"); + Component a2 = m.createAspectService(ServiceInterface2.class, null, 10, "m_service") + .setFactory(new Factory(e, ServiceInterface2.class, "ServiceInterfaceProxy2"), "create"); + + // create a client that invokes a method on boths services, validate that it goes + // through the proxy twice + Component sc = m.createComponent() + .setImplementation(new ServiceConsumer(e)) + .add(m.createServiceDependency().setService(ServiceInterface.class).setRequired(true)) + .add(m.createServiceDependency().setService(ServiceInterface2.class).setRequired(true)) + ; + + // register both producers, validate that both services are started + m.add(sp1); + e.waitForStep(1, 2000); + m.add(sp2); + e.waitForStep(2, 2000); + + // add both aspects, and validate that both instances have been created + m.add(a1); + m.add(a2); + e.waitForStep(4, 4000); + + // add the client, which will automatically invoke both services + m.add(sc); + + // wait until both services have been invoked + e.waitForStep(6, 4000); + + // make sure the proxy has been called twice + Assert.assertEquals("Proxy should have been invoked this many times.", 2, DynamicProxyHandler.getCounter()); + + m.remove(sc); + m.remove(a2); + m.remove(a1); + m.remove(sp2); + m.remove(sp1); + m.remove(a2); + m.remove(a1); + + try { + Thread.sleep(2000); + } + catch (InterruptedException e1) { + // TODO Auto-generated catch block + e1.printStackTrace(); + } + } + + static interface ServiceInterface { + public void invoke(Runnable run); + } + + static interface ServiceInterface2 { + public void invoke(Runnable run); + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void start() { + m_ensure.step(1); + } + public void invoke(Runnable run) { + run.run(); + } + } + + static class ServiceProvider2 implements ServiceInterface2 { + private final Ensure m_ensure; + public ServiceProvider2(Ensure ensure) { + m_ensure = ensure; + } + public void start() { + m_ensure.step(2); + } + public void invoke(Runnable run) { + run.run(); + } + } + + static class ServiceConsumer implements Runnable { + private volatile ServiceInterface m_service; + private volatile ServiceInterface2 m_service2; + private final Ensure m_ensure; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void init() { + Thread t = new Thread(this); + t.start(); + } + + public void run() { + m_service.invoke(Ensure.createRunnableStep(m_ensure, 5)); + m_service2.invoke(Ensure.createRunnableStep(m_ensure, 6)); + } + } + + static class DynamicProxyHandler implements InvocationHandler { + public volatile Object m_service; // ISSUE, we cannot inject into "Object" at the moment + private final String m_label; + private static volatile int m_counter = 0; + + public DynamicProxyHandler(String label) { + m_label = label; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + System.out.println("IIIIIIINVOKE--------------------------" + method.getName()); + if (m_service == null) { + Assert.fail("No service was injected into dynamic proxy handler " + m_label); + } + Method m = m_service.getClass().getMethod(method.getName(), method.getParameterTypes()); + if (m == null) { + Assert.fail("No method " + method.getName() + " was found in instance " + m_service + " in dynamic proxy handler " + m_label); + } + if (method.getName().equals("invoke")) { + // only count methods called 'invoke' because those are actually the ones + // both interfaces implement (and the dynamic proxy might be invoked for + // other methods, such as toString() as well) + m_counter++; + } + return m.invoke(m_service, args); + } + + public static int getCounter() { + return m_counter; + } + } + + static class Factory { + private final String m_label; + private Class m_class; + private final Ensure m_ensure; + + public Factory(Ensure ensure, Class clazz, String label) { + m_ensure = ensure; + m_class = clazz; + m_label = label; + } + + public Object create() { + m_ensure.step(); + return Proxy.newProxyInstance(m_class.getClassLoader(), new Class[] { m_class }, new DynamicProxyHandler(m_label)); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/DynamicScopedServiceTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/DynamicScopedServiceTest.java new file mode 100644 index 00000000000..a26b4109bf7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/DynamicScopedServiceTest.java @@ -0,0 +1,216 @@ +/* + * 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.dm.itest.api; + +import java.util.Map; +import java.util.Properties; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Component.ServiceScope; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceRegistration; + +/** + * Validates a simple scoped service, which adds a dynamic dependency from init method. + * Notice the the prototype will add a dynamic dependency from its init method, and the dependency service + * properties will be propagated. + */ +public class DynamicScopedServiceTest extends TestBase { + static Ensure m_e; + static Ensure.Steps m_serviceImplInitSteps; + static Ensure.Steps m_serviceImplStartSteps; + static Ensure.Steps m_serviceImplStopSteps; + static Ensure.Steps m_serviceConsumerBindSteps; + static Ensure.Steps m_serviceConsumerUnbindSteps; + + public void setUp() throws Exception { + super.setUp(); + m_e = new Ensure(); + m_serviceImplInitSteps = new Ensure.Steps(1, 2, 5, 12, 13); + m_serviceImplStartSteps = new Ensure.Steps(3, 6, 14); + m_serviceImplStopSteps = new Ensure.Steps(9, 11, 17); + m_serviceConsumerBindSteps = new Ensure.Steps(4, 7, 15); + m_serviceConsumerUnbindSteps = new Ensure.Steps(8, 10, 16); + } + + public void testPrototypeComponentWithFactory() { + testPrototypeComponent(true); + } + + public void testPrototypeComponentWithoutFactory() { + testPrototypeComponent(false); + } + + private void testPrototypeComponent(boolean useFactory) { + DependencyManager m = getDM(); + + Component provider = m.createComponent() + .setScope(ServiceScope.PROTOTYPE) + .setInterface(Service.class.getName(), null) + .add(m.createServiceDependency().setService(Service3.class).setAutoConfig("m_service3")); + + if (useFactory) { + provider.setFactory(this, "createServiceImpl"); // Bundle and ServiceRegistration injected in class fields + } else { + provider.setImplementation(ServiceImplWithConstructor.class); // Bundle And ServiceRegistration injected in constructor only + } + + Properties props = new Properties(); + props.put("foo", "bar"); + Component service2 = m.createComponent() + .setInterface(Service2.class.getName(), props) + .setImplementation(new Service2() {}); + + Component service3 = m.createComponent() + .setInterface(Service3.class.getName(), null) + .setImplementation(new Service3() {}); + + Component consumer1 = m.createComponent() + .setImplementation(new ServiceConsumer()) + .add(m.createServiceDependency().setService(Service.class).setRequired(true).setCallbacks("bind", "unbind")); + + Component consumer2 = m.createComponent() + .setImplementation(new ServiceConsumer()) + .add(m.createServiceDependency().setService(Service.class).setRequired(true).setCallbacks("bind", "unbind")); + + m.add(service3); // add service3 (the provider has an optional callback on it) + m.add(provider); // add provider + m.add(service2); // add service2 (the prototype depends on it) + m.add(consumer1); // add first consumer + m_e.waitForStep(1, 5000); // Service prototype instance called in init + m_e.waitForStep(2, 5000); // first clone called in init + m_e.waitForStep(3, 5000); // first clone called in init + m_e.waitForStep(4, 5000); // first consumer bound to first clone + + m.add(consumer2); // add second consumer + m_e.waitForStep(5, 5000); // second clone called in init + m_e.waitForStep(6, 5000); // second clone called in start + m_e.waitForStep(7, 5000); // second consumer bound to second clone + + // make sure both consumers have a different provider instances. + ServiceConsumer consumer1Impl = consumer1.getInstance(); + Assert.assertNotNull(consumer1Impl.getService()); + ServiceConsumer consumer2Impl = consumer2.getInstance(); + Assert.assertNotNull(consumer2Impl.getService()); + Assert.assertNotEquals(consumer1Impl.getService(), consumer2Impl.getService()); + + m.remove(consumer1); // remove consumer1 + m_e.waitForStep(8, 5000); // consumer1 unbound from first clone + m_e.waitForStep(9, 5000); // first clone stopped + + m.remove(provider); // unregister the provider + m_e.waitForStep(10, 5000); // consumer2 unbound from second clone + m_e.waitForStep(11, 5000); // second clone stopped + + m.add(provider); // re-register the provider + m_e.waitForStep(12, 5000); // prototype init called + m_e.waitForStep(13, 5000); // third clone init method called (because consumer2 is active) + m_e.waitForStep(14, 5000); // third clone start method called + m_e.waitForStep(15, 5000); // consumer2 bound to third clone + + m.remove(service2); // remove the service2 (it will destroy the clone) + m_e.waitForStep(16, 5000); // consumer2 unbound + m_e.waitForStep(17, 5000); // third clone stopped + + m.remove(provider); + m.remove(service3); + m.clear(); + } + + @SuppressWarnings("unused") + private ServiceImpl createServiceImpl() { + return new ServiceImpl(); + } + + public interface Service { + } + + public interface Service2 { + } + + public interface Service3 { + } + + public static class ServiceImpl implements Service { + volatile Bundle m_bundle; // bundle requesting the service, injected by reflection or from constructor + volatile ServiceRegistration m_registration; // registration of the requested service, injected by reflection or from constructor + volatile Service2 m_service2; + private Service3 m_service3; + + void init(Component component) { // only called on prototype instance, not on clones + DependencyManager m = component.getDependencyManager(); + component.add(m.createServiceDependency().setService(Service2.class).setRequired(true).setCallbacks("bind", null).setPropagate(true)); + m_e.steps(m_serviceImplInitSteps); // 1, 2, 5, 12, 13 + } + + void bind(Service2 service2, Map properties) { + // check if prototype service properties has propagated the Service2 dependency service properties + Assert.assertEquals("bar", properties.get("foo")); + m_service2 = service2; + } + + void start() { + Assert.assertNotNull(m_bundle); + Assert.assertNotNull(m_registration); + Assert.assertNotNull(m_service2); + Assert.assertNotNull(m_service3); + m_e.steps(m_serviceImplStartSteps); // 3, 6, 14 + } + + Service3 getService3() { + return m_service3; + } + + void stop() { + m_e.steps(m_serviceImplStopSteps); // 9, 11, 17 + } + } + + public static class ServiceImplWithConstructor extends ServiceImpl { + /** + * Inject requesting bundle and service registration using class constructor, NOT using field reflection + */ + public ServiceImplWithConstructor(Bundle b, ServiceRegistration reg) { + m_bundle = b; + m_registration = reg; + } + } + + public class ServiceConsumer { + volatile Service m_myService; + + public void bind(Service service) { + m_myService = service; + m_e.steps(m_serviceConsumerBindSteps); // 4, 7, 15 + } + + public void unbind(Service service) { + Assert.assertEquals(m_myService, service); + m_e.steps(m_serviceConsumerUnbindSteps); // 8, 10, 16 + } + + public Service getService() { + return m_myService; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELI5155_AdapterCallbackInstanceCalledTwice.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELI5155_AdapterCallbackInstanceCalledTwice.java new file mode 100644 index 00000000000..dc6dcf5b722 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELI5155_AdapterCallbackInstanceCalledTwice.java @@ -0,0 +1,97 @@ +/* + * 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.dm.itest.api; +import org.apache.felix.dm.AdapterComponent; +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; + +public class FELI5155_AdapterCallbackInstanceCalledTwice extends TestBase { + static Ensure m_e; + + public void testAdapterCallbackInstanceCalledTwice() { + DependencyManager m = getDM(); + m_e = new Ensure(); + + S1AdapterImpl adapterImpl = new S1AdapterImpl(); + S2DependencyCallbackInstance cb = new S2DependencyCallbackInstance(adapterImpl); + + Component s1 = m.createComponent().setImplementation(S1Impl.class).setInterface(S1.class.getName(), null); + Component s2 = m.createComponent().setImplementation(S2Impl.class).setInterface(S2.class.getName(), null); + + Component s1Adapter = m.createAdapterService(S1.class, null, "setS1", null, null, null) + .setImplementation(adapterImpl) + .add(m.createServiceDependency().setService(S2.class).setRequired(true).setCallbacks(cb, "setS2", null, null, null)); + + + m.add(s1); + m.add(s1Adapter); + m.add(s2); + + m_e.waitForStep(2, 5000); + clearComponents(); + } + + + public interface S1 { + } + + public static class S1Impl implements S1 { + } + + public interface S2 { + } + + public static class S2Impl implements S2 { + } + + public static class S1AdapterImpl { + volatile S1 m_s1; + volatile S2 m_s2; + + void setS1(S1 s1) { + m_s1 = s1; + } + + void setS2(S2 s2) { + Assert.assertNull("service already injected: ", m_s2); + m_s2 = s2; + m_e.step(1); + } + + void start() { + Assert.assertNotNull("service s1 not injected", m_s1); + m_e.step(2); + } + } + + public static class S2DependencyCallbackInstance { + final S1AdapterImpl m_adapter; + + S2DependencyCallbackInstance(S1AdapterImpl adapter) { + m_adapter = adapter; + } + + void setS2(S2 s2) { + m_adapter.setS2(s2); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2078_ServiceDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2078_ServiceDependencyTest.java new file mode 100644 index 00000000000..030bd0b1fb2 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2078_ServiceDependencyTest.java @@ -0,0 +1,97 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * @author Felix Project Team + */ +public class FELIX2078_ServiceDependencyTest extends TestBase { + public void testRequiredServiceRegistrationAndConsumption() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), null); + Component sp2 = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), null); + Component sc = m.createComponent().setImplementation(new ServiceConsumer(e)).add(m.createServiceDependency().setService(ServiceInterface.class).setRequired(true).setCallbacks("add", "remove")); + m.add(sp); + m.add(sp2); + System.out.println("adding client"); + m.add(sc); + System.out.println("waiting"); + // wait until both services have been added to our consumer + e.waitForStep(2, 5000); + m.remove(sc); + m.remove(sp2); + m.remove(sp); + // ensure we executed all steps inside the component instance + } + + public void testOptionalServiceRegistrationAndConsumption() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), null); + Component sp2 = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), null); + Component sc = m.createComponent().setImplementation(new ServiceConsumer(e)).add(m.createServiceDependency().setService(ServiceInterface.class).setRequired(false).setCallbacks("add", "remove")); + m.add(sp); + m.add(sp2); + m.add(sc); + // wait until both services have been added to our consumer + e.waitForStep(2, 5000); + m.remove(sc); + m.remove(sp2); + m.remove(sp); + // ensure we executed all steps inside the component instance + } + + static interface ServiceInterface { + public void invoke(); + } + + static class ServiceProvider implements ServiceInterface { + public ServiceProvider(Ensure e) { + } + public void invoke() { + } + } + + static class ServiceConsumer { + private final Ensure m_ensure; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void add(ServiceInterface i) { + System.out.println("add " + i); + m_ensure.step(); + } + + public void remove(ServiceInterface i) { + + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2344_ExtraDependencyWithAutoConfigTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2344_ExtraDependencyWithAutoConfigTest.java new file mode 100644 index 00000000000..b87f7d152ee --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2344_ExtraDependencyWithAutoConfigTest.java @@ -0,0 +1,107 @@ +/* + * 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.dm.itest.api; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + + +/** + * @author Felix Project Team + */ +@SuppressWarnings("unused") +public class FELIX2344_ExtraDependencyWithAutoConfigTest extends TestBase { + /** + * Test if an auto config extra dependency is injected in the expected order. + */ + public void testExtraDependencyWithAutoConfig() { + DependencyManager m = getDM(); + // Helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // Create a service provider + Component sp = m.createComponent().setInterface(ProviderInterface.class.getName(), null).setImplementation(ProviderImpl.class); + // Create a service consumer with a required/autoconfig dependency over the service provider. + Client c1; + Component sc1 = m.createComponent().setImplementation((c1 = new Client(e, true, 1))); + // Create a second service consumer with an optional/autoconfig dependency over the service provider. + Client c2; + Component sc2 = m.createComponent().setImplementation(c2 = new Client(e, false, 3)); + + // Add service provider and consumer sc1 (required dependency over provider) + m.add(sc1); + m.add(sp); + e.waitForStep(2, 5000); + + // Remove provider and consumer + m.remove(sc1); + m.remove(sp); + + // Add consumer sc2 (optional dependency over provider) + m.add(sc2); + e.waitForStep(4, 5000); + m.clear(); + } + + public interface ProviderInterface { + public boolean action(); + } + + public static class ProviderImpl implements ProviderInterface { + public boolean action() + { + return true; + } + } + + // This client is not using callbacks, but instead, it uses auto config. + public static class Client { + volatile ProviderInterface m_provider; + private Ensure m_ensure; + private final boolean m_required; + private final int m_startStep; + + public Client(Ensure e, boolean required, int startStep) { + m_ensure = e; + m_required = required; + m_startStep = startStep; + } + + public void init(Component s) { + DependencyManager dm = s.getDependencyManager(); + m_ensure.step(m_startStep); + s.add(dm.createServiceDependency() + .setService(ProviderInterface.class) + .setRequired(m_required) + .setAutoConfig("m_provider")); + } + + public void start() { + // if required dependency: we must have been injected with the service provider + // else, we have been injected with a null object. + Assert.assertNotNull("provider has not been injected", m_provider); + Assert.assertEquals(m_required, m_provider.action()); // action returns false if null object + m_ensure.step(); + } + } +} + diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2344_ExtraDependencyWithCallbackTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2344_ExtraDependencyWithCallbackTest.java new file mode 100644 index 00000000000..a28c8ab45b6 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2344_ExtraDependencyWithCallbackTest.java @@ -0,0 +1,114 @@ +/* + * 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.dm.itest.api; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * Tests for extra dependencies which are declared from service's init method. + * + * @author Felix Project Team + */ +public class FELIX2344_ExtraDependencyWithCallbackTest extends TestBase { + /** + * Checks if an extra optional/required dependency is properly injected into a consumer, using callbacks. + */ + public void testExtraDependencyWithCallback() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service consumer and provider + Component sp = m.createComponent().setInterface(ProviderInterface.class.getName(), null).setImplementation(ProviderImpl.class); + Component sc = m.createComponent().setImplementation(new Client(e, false, 1)); + Component sc2 = m.createComponent().setImplementation(new Client(e, true, 5)); + Component sc3 = m.createComponent().setImplementation(new Client(e, true, 9)); + + // add the provider first, then add the consumer which initially will have no dependencies + // but via the init() method an optional dependency with a callback method will be added + m.add(sp); + m.add(sc); + // remove the consumer again + m.remove(sc); + e.waitForStep(4, 5000); + + // next up, add a second consumer, identical to the first, but with a required dependency + // with a callback method which will be added in the init() method + m.add(sc2); + // remove the consumer again + m.remove(sc2); + e.waitForStep(8, 5000); + + // now remove the provider, add a third consumer, identical to the second, and after the + // consumer has started, add the provider again + m.remove(sp); + m.add(sc3); + m.add(sp); + e.waitForStep(12, 5000); + m.clear(); + } + + public interface ProviderInterface { + } + + public static class ProviderImpl implements ProviderInterface { + } + + public static class Client { + ProviderInterface m_provider; + private Ensure m_ensure; + private final boolean m_required; + private final int m_startStep; + + public Client(Ensure e, boolean required, int startStep) { + m_ensure = e; + m_required = required; + m_startStep = startStep; + } + + public void init(Component s) { + DependencyManager dm = s.getDependencyManager(); + m_ensure.step(m_startStep); + s.add(dm.createServiceDependency() + .setService(ProviderInterface.class) + .setRequired(m_required) + .setCallbacks("bind", null)); + } + + // called before start() for required dependency, or after start for optional dependency + void bind(ProviderInterface provider) { + System.out.println("bind"); + m_ensure.step(m_required ? m_startStep + 1 : m_startStep + 3); + m_provider = provider; + } + + public void start() { + System.out.println("start"); + m_ensure.step(m_required ? m_startStep + 2: m_startStep + 1); + if (m_required) { + Assert.assertNotNull("Dependendency should have been injected", m_provider); + } + m_ensure.step(m_required ? m_startStep + 3: m_startStep + 2); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2348_ResourceAdapterTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2348_ResourceAdapterTest.java new file mode 100644 index 00000000000..fbf592dbba2 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2348_ResourceAdapterTest.java @@ -0,0 +1,71 @@ +/* + * 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.dm.itest.api; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URL; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ResourceHandler; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; + +/** + * @author Felix Project Team + */ +public class FELIX2348_ResourceAdapterTest extends TestBase { + public void testBasicResourceAdapter() throws Exception { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + m.add(m.createResourceAdapterService("(&(path=/path/to/*.txt)(host=localhost))", false, null, "changed") + .setImplementation(new ResourceAdapter(e))); + URL resourceURL = new URL("file://localhost/path/to/file1.txt"); + m.add(m.createComponent().setImplementation(new ResourceProvider(context, resourceURL)) + .add(m.createServiceDependency().setService(ResourceHandler.class).setCallbacks("add", "remove"))); + e.waitForStep(3, 5000); + m.clear(); + } + + static class ResourceAdapter { + protected URL m_resource; // injected by reflection. + private Ensure m_ensure; + + ResourceAdapter(Ensure e) { + m_ensure = e; + } + + public void start() { + m_ensure.step(1); + Assert.assertNotNull("resource not injected", m_resource); + m_ensure.step(2); + try { + m_resource.openStream(); + } + catch (FileNotFoundException e) { + m_ensure.step(3); + } + catch (IOException e) { + Assert.fail("We should not have gotten this exception."); + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2369_ExtraDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2369_ExtraDependencyTest.java new file mode 100644 index 00000000000..bb1ff013688 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2369_ExtraDependencyTest.java @@ -0,0 +1,111 @@ +/* + * 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.dm.itest.api; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ServiceDependency; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * This testcase verify that a Service is not started if one of its extra required dependencies + * is unavailable. + * + * @author Felix Project Team + */ +public class FELIX2369_ExtraDependencyTest extends TestBase +{ + public void testExtraDependencies() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service consumer and provider + Component sp1 = m.createComponent().setInterface(MyService1.class.getName(), null).setImplementation(new MyService1Impl()); + Component sc = m.createComponent().setImplementation(new MyClient(e, 1)); + + // provides the MyService1 service (but not the MyService2, which is required by MyClient). + m.add(sp1); + // add MyClient (it should not be invoked in its start() method because MyService2 is not there + m.add(sc); + // remove MyClient (it should not be invoked in its stop() method because it should not be active, since MyService2 is not there. + m.remove(sc); + e.waitForStep(2, 5000); + m.clear(); + } + + public interface MyService1 { + } + + public interface MyService2 { + } + + public static class MyService1Impl implements MyService1 { + } + + public static class MyService2Impl implements MyService2 { + } + + // This client is not using callbacks, but instead, it uses auto config. + public static class MyClient { + MyService1 m_myService2; // required/unavailable + private Ensure m_ensure; + private final int m_startStep; + + public MyClient(Ensure e, int startStep) { + m_ensure = e; + m_startStep = startStep; + } + + public void init(Component s) { + DependencyManager dm = s.getDependencyManager(); + m_ensure.step(m_startStep); + ServiceDependency d1 = + dm.createServiceDependency() // this dependency is available at this point + .setService(MyService1.class) + .setRequired(false) + .setCallbacks("bind", null); + ServiceDependency d2 = + dm.createServiceDependency() // not available: we should not be started + .setService(MyService2.class) + .setRequired(true) + .setAutoConfig("m_myService2"); + + s.add(d1, d2); // atomically add these two dependencies + } + + public void start() { + Assert.fail("start should not be called since MyService2 is unavailable"); + } + + void bind(MyService1 s1) { // optional/available + System.out.println("bound MyService1"); + } + + public void stop() { + Assert.fail("stop should not be called since we should not be active at this point"); + } + + public void destroy() { + m_ensure.step(m_startStep+1); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2696_ConfigurationAndServiceDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2696_ConfigurationAndServiceDependencyTest.java new file mode 100644 index 00000000000..6f2ea659852 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2696_ConfigurationAndServiceDependencyTest.java @@ -0,0 +1,133 @@ +/* + * 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.dm.itest.api; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedService; + + +/** + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class FELIX2696_ConfigurationAndServiceDependencyTest extends TestBase { + final static String PID = "FELIX2696_ConfigurationAndServiceDependencyTest.pid"; + + public void testComponentWithRequiredConfigurationAndServicePropertyPropagation() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component s1 = m.createComponent() + .setImplementation(new ConfigurationConsumer(e)) + .add(m.createServiceDependency().setService(ServiceInterface.class).setRequired(true)) + .add(m.createConfigurationDependency().setPid(PID)); + Component s2 = m.createComponent() + .setImplementation(new ConfigurationCreator(e)) + .add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)); + Component s3 = m.createComponent() + .setInterface(ServiceInterface.class.getName(), null) + .setImplementation(DependentServiceProvider.class); + m.add(s1); + m.add(s2); + m.add(s3); + e.waitForStep(2, 5000); + warn("removing s3"); + m.remove(s3); + warn("readding s3"); + m.add(s3); + // after adding the required dependency again, the issue in FELIX-2696 means that the + // updated() method is not invoked for the new instance, and init() is, so our step + // count will only go up to 3 (not 4) causing this test to fail + e.waitForStep(4, 5000); + m.remove(s3); + m.remove(s2); + m.remove(s1); + e.waitForStep(5, 5000); + } + + public static class ConfigurationCreator { + private volatile ConfigurationAdmin m_ca; + private final Ensure m_ensure; + Configuration m_conf; + + public ConfigurationCreator(Ensure e) { + m_ensure = e; + } + + @SuppressWarnings("unchecked") + public void init() { + try { + m_conf = m_ca.getConfiguration(PID, null); + Hashtable props = new Hashtable(); + props.put("testkey", "testvalue"); + m_conf.update(props); + } + catch (IOException e) { + Assert.fail("Could not create configuration: " + e.getMessage()); + } + } + + public void destroy() throws IOException { + m_conf.delete(); + m_ensure.step(5); + } + } + + public class ConfigurationConsumer implements ManagedService { + private final Ensure m_ensure; + + public ConfigurationConsumer(Ensure e) { + m_ensure = e; + } + + public void updated(Dictionary props) throws ConfigurationException { + warn("Consumer: updated %s", props); + if (props != null) { + if (!"testvalue".equals(props.get("testkey"))) { + Assert.fail("Could not find the configured property."); + } + m_ensure.step(); + } + } + + public void init() { + warn("Consumer: init"); + m_ensure.step(); + } + } + + public static interface ServiceInterface { + } + + public static class DependentServiceProvider implements ServiceInterface { + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2875_ServiceDependencyWithoutServiceNameTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2875_ServiceDependencyWithoutServiceNameTest.java new file mode 100644 index 00000000000..3cc2a90826d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2875_ServiceDependencyWithoutServiceNameTest.java @@ -0,0 +1,101 @@ +/* + * 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.dm.itest.api; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * @author Felix Project Team + */ +public class FELIX2875_ServiceDependencyWithoutServiceNameTest extends TestBase { + Ensure m_e; + + public void testServiceDependencyWithoutName() { + m_e = new Ensure(); + DependencyManager dm = getDM(); + Component consumer = dm.createComponent() + .setImplementation(new ConsumeServiceDependencyWithoutName()) + .add(dm.createServiceDependency() + .setService("(provider=*)").setRequired(true) + .setCallbacks("add", null)) + .add(dm.createServiceDependency() + .setService("(|(provider=provider1)(provider=provider2))").setRequired(true) + .setAutoConfig("m_providers")); + + Hashtable props = new Hashtable<>(); + props.put("provider", "provider1"); + Component provider1 = dm.createComponent() + .setImplementation(new Provider1()) + .setInterface(Provider.class.getName(), props); + + props = new Hashtable<>(); + props.put("provider", "provider2"); + Component provider2 = dm.createComponent() + .setImplementation(new Provider2()) + .setInterface(Provider.class.getName(), props); + + dm.add(provider1); + dm.add(provider2); + dm.add(consumer); + m_e.waitForStep(5, 5000); + dm.clear(); + } + + private class ConsumeServiceDependencyWithoutName { + volatile Map> m_providers; // autoconfig + + @SuppressWarnings("unused") + void add(Map props, Object service) { + if ("provider1".equals(props.get("provider"))) { + m_e.step(); + } else if ("provider2".equals(props.get("provider"))) { + m_e.step(); + } + } + + @SuppressWarnings("unused") + void start() { + // check if all providers have been injected in our autoconfig field. + for (Map.Entry> e : m_providers.entrySet()) { + if ("provider1".equals(e.getValue().get("provider"))) { + m_e.step(); + } else if ("provider2".equals(e.getValue().get("provider"))) { + m_e.step(); + } + } + m_e.step(5); + } + } + + public interface Provider { + } + + public class Provider1 implements Provider { + } + + public class Provider2 implements Provider { + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2955_ShellCommandTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2955_ShellCommandTest.java new file mode 100644 index 00000000000..4ee902a6064 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX2955_ShellCommandTest.java @@ -0,0 +1,165 @@ +/* + * 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.dm.itest.api; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.service.command.CommandProcessor; +import org.apache.felix.service.command.CommandSession; +import org.osgi.framework.Bundle; + +/** + * @author Felix Project Team + */ +public class FELIX2955_ShellCommandTest extends TestBase { + private long m_myBundleId; + private Bundle m_testBundle; + + public void testShellCommands() throws Throwable { + try { + m_myBundleId = context.getBundle().getBundleId(); + for (Bundle b : context.getBundles()) { + if (b.getSymbolicName().equals("org.apache.felix.dependencymanager.itest.bundle")) { + m_testBundle = b; + b.stop(); + break; + } + } + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + Component shellClient = m.createComponent(); + Component missing = m.createComponent(); + + long shellClientId = shellClient.getComponentDeclaration().getId(); + long missingId = missing.getComponentDeclaration().getId(); + shellClient.setImplementation(new ShellClient(e, shellClientId, missingId)) + .add(m.createServiceDependency() + .setService(CommandProcessor.class) + .setRequired(true)); + + m.add(shellClient); + e.waitForStep(3, 5000); + // now create a component with a missing dependency + missing.setImplementation(new Object() { public String toString() { return "Object"; }}) + .add(m.createServiceDependency() + .setService(Missing.class) // Warning: don't use Object, or Runnable, which are already registered by bndtools ? + .setRequired(true)); + + m.add(missing); + e.step(4); + e.waitForStep(5, 5000); + m.remove(missing); + // now start/stop our test bundle, which publishes a service that uses the dependency manager + m_testBundle.start(); + m_testBundle.stop(); + e.step(6); + e.waitForStep(7, 5000); + e.ensure(); + m.remove(shellClient); + m_testBundle.start(); // restart the runtime bundle + m.clear(); + } + + catch (Throwable t) { + error("test failed", t); + } + } + + public class ShellClient { + volatile CommandProcessor m_commandProcessor; + private final Ensure m_ensure; + private final long m_shellClientId; + private final long m_missingId; + + public ShellClient(Ensure e, long shellClientId, long missingId) { + m_ensure = e; + m_shellClientId = shellClientId; + m_missingId = missingId; + } + + public void start() throws InterruptedException { + Thread t = new Thread("Shell Client") { + public void run() { + String bsn = context.getBundle().getSymbolicName(); + m_ensure.step(1); + execute("dm bid " + m_myBundleId, + "[" + m_myBundleId + "] " + bsn + "\n" + + " [" + m_shellClientId + "] ShellClient registered\n" + + " org.apache.felix.service.command.CommandProcessor service required available\n", + ""); + + m_ensure.step(2); + // see if there's anything that's not available + execute("dm notavail bid " + m_myBundleId, + "", + ""); + m_ensure.step(3); + // check again, now there should be something missing + m_ensure.waitForStep(4, 5000); + execute("dm notavail bid " + m_myBundleId, + "[" + m_myBundleId + "] " + bsn + "\n" + + " [" + m_missingId + "] Object unregistered\n" + + " " + Missing.class.getName() + " service required unavailable\n", + ""); + m_ensure.step(5); + m_ensure.waitForStep(6, 5000); + // this next step actually triggers the bug in FELIX-2955 + execute("dm notavail bid " + m_myBundleId, + "", + ""); + m_ensure.step(7); + } + }; + t.start(); + } + + @Override + public String toString() { + return "ShellClient"; + } + + public void execute(String command, String expectedOutput, String expectedError) { + try { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + ByteArrayOutputStream error = new ByteArrayOutputStream(); + CommandSession session = m_commandProcessor.createSession(System.in, new PrintStream(output), new PrintStream(error)); + session.execute(command); + + String out = output.toString(); + Assert.assertEquals(expectedOutput, out.toString()); + Assert.assertEquals(expectedError, error.toString()); + } + catch (Throwable throwable) { + m_ensure.throwable(throwable); + } + } + } + + public static class Missing { + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX3337_UpdatedConfigurationDependencyWithPropagationTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX3337_UpdatedConfigurationDependencyWithPropagationTest.java new file mode 100644 index 00000000000..368acb8f0ca --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX3337_UpdatedConfigurationDependencyWithPropagationTest.java @@ -0,0 +1,151 @@ +/* + * 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.dm.itest.api; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Properties; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedService; + + +/** + * This test validates the following scenario: + * - Service S1 depends on a ConfigurationDependency with propagate = true + * - Service S2 depends on S1 (and has access to the S1 configuration using the S1 service + * properties (because the ConfigurationDependency is propagated) + * - then the S1 PID is updated from ConfigAdmin + * - S1 is then called in its updated callback + * - S2 is called in its "change" callback. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class FELIX3337_UpdatedConfigurationDependencyWithPropagationTest extends TestBase { + /* + * This Pojo creates the configuration pid "test". + */ + static class ConfigurationCreator { + private volatile ConfigurationAdmin m_ca; + org.osgi.service.cm.Configuration m_conf; + + public void init() { + try { + m_conf = m_ca.getConfiguration("test", null); + Hashtable props = new Properties(); + props.put("testkey", "testvalue"); + m_conf.update(props); + } + catch (IOException e) { + Assert.fail("Could not create configuration: " + e.getMessage()); + } + } + + public void update() { + try { + Hashtable props = new Properties(); + props.put("testkey", "testvalue"); + props.put("testkey2", "testvalue2"); + m_conf.update(props); + } catch (IOException e) { + Assert.fail("Could not update the configured property: " + e.toString()); + } + } + } + + static class S1 implements ManagedService { + private Ensure m_ensure; + private boolean m_initialized; + + public S1(Ensure e) { + m_ensure = e; + } + + public void updated(Dictionary props) throws ConfigurationException { + if (props != null) { + if (!m_initialized) { + m_ensure.step(1); + m_initialized = true; + } else { + // we are updated + m_ensure.step(3); + } + } + } + } + + static class S2 { + private final Ensure m_ensure; + + public S2(Ensure e) { + m_ensure = e; + } + + public void add(S1 s1) { + m_ensure.step(2); + } + + public void change(S1 runnable) { + m_ensure.step(4); + } + } + + public void testComponentWithRequiredUpdatedConfigurationAndServicePropertyPropagation() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + ConfigurationCreator confCreator = new ConfigurationCreator(); + Component s1 = m.createComponent() + .setImplementation(new S1(e)) + .setInterface(S1.class.getName(), null) + .add(m.createConfigurationDependency() + .setPid("test") + .setPropagate(true)); + Component s2 = m.createComponent() + .setImplementation(new S2(e)) + .add(m.createServiceDependency() + .setService(S1.class, ("(testkey=testvalue)")) + .setRequired(true) + .setCallbacks("add", "change", null)); + Component s3 = m.createComponent() + .setImplementation(confCreator) + .add(m.createServiceDependency() + .setService(ConfigurationAdmin.class) + .setRequired(true)); + + m.add(s1); + m.add(s2); + m.add(s3); + e.waitForStep(2, 5000); + confCreator.update(); + e.waitForStep(4, 5000); + m.remove(s1); + m.remove(s2); + m.remove(s3); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX4158_DependencyDeclarationTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX4158_DependencyDeclarationTest.java new file mode 100644 index 00000000000..11edb4904a3 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX4158_DependencyDeclarationTest.java @@ -0,0 +1,161 @@ +/* + * 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.dm.itest.api; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentDeclaration; +import org.apache.felix.dm.ComponentDependencyDeclaration; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.Bundle; +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class FELIX4158_DependencyDeclarationTest extends TestBase { + public void testServiceDependencyDeclaration() { + DependencyManager m = getDM(); + Component c = m.createComponent() + .setImplementation(new Object()) + .add(m.createServiceDependency().setService(LogService.class, "(foo=bar)")); + + ComponentDeclaration cd = c.getComponentDeclaration(); + ComponentDependencyDeclaration[] cdds = cd.getComponentDependencies(); + Assert.assertNotNull(cdds); + Assert.assertNotNull(cdds.length == 1); + Assert.assertEquals(cdds[0].getName(), "org.osgi.service.log.LogService (foo=bar)"); + Assert.assertEquals(cdds[0].getSimpleName(), "org.osgi.service.log.LogService"); + Assert.assertNotNull(cdds[0].getFilter()); + Assert.assertEquals(cdds[0].getFilter(), "(foo=bar)"); + m.clear(); + } + + public void testConfigurationDependencyDeclaration() { + DependencyManager m = getDM(); + Component c = m.createComponent() + .setImplementation(new Object()) + .add(m.createConfigurationDependency().setPid("foo")); + + ComponentDeclaration cd = c.getComponentDeclaration(); + ComponentDependencyDeclaration[] cdds = cd.getComponentDependencies(); + Assert.assertNotNull(cdds); + Assert.assertNotNull(cdds.length == 1); + Assert.assertEquals(cdds[0].getName(), "foo"); + Assert.assertEquals(cdds[0].getSimpleName(), "foo"); + Assert.assertNull(cdds[0].getFilter()); + m.clear(); + } + + public void testResourceDependencyDeclaration() throws MalformedURLException { + DependencyManager m = getDM(); + Component c = m.createComponent() + .setImplementation(new Object()) + .add(m.createResourceDependency() + .setResource(new URL("file://localhost/path/to/file1.txt"))); + + ComponentDeclaration cd = c.getComponentDeclaration(); + ComponentDependencyDeclaration[] cdds = cd.getComponentDependencies(); + Assert.assertNotNull(cdds); + Assert.assertNotNull(cdds.length == 1); + Assert.assertEquals(cdds[0].getName(), "file://localhost/path/to/file1.txt"); + Assert.assertNotNull(cdds[0].getSimpleName()); + Assert.assertEquals(cdds[0].getSimpleName(), "file://localhost/path/to/file1.txt"); + Assert.assertNull(cdds[0].getFilter()); + m.clear(); + } + + public void testResourceDependencyDeclarationWithFilter() { + DependencyManager m = getDM(); + Component c = m.createComponent() + .setImplementation(new Object()) + .add(m.createResourceDependency() + .setFilter("(&(path=/path/to/*.txt)(host=localhost))")); + + ComponentDeclaration cd = c.getComponentDeclaration(); + ComponentDependencyDeclaration[] cdds = cd.getComponentDependencies(); + Assert.assertNotNull(cdds); + Assert.assertNotNull(cdds.length == 1); + Assert.assertEquals(cdds[0].getName(), ("(&(path=/path/to/*.txt)(host=localhost))")); + Assert.assertNull(cdds[0].getSimpleName()); + Assert.assertNotNull(cdds[0].getFilter()); + Assert.assertEquals(cdds[0].getFilter(), "(&(path=/path/to/*.txt)(host=localhost))"); + m.clear(); + } + + public void testBundleDependencyDeclaration() throws MalformedURLException { + DependencyManager m = getDM(); + Component c = m.createComponent() + .setImplementation(new Object()) + .add(m.createBundleDependency()); + + ComponentDeclaration cd = c.getComponentDeclaration(); + ComponentDependencyDeclaration[] cdds = cd.getComponentDependencies(); + Assert.assertNotNull(cdds); + Assert.assertNotNull(cdds.length == 1); + Assert.assertEquals(cdds[0].getName(), "active installed resolved"); + Assert.assertNotNull(cdds[0].getSimpleName()); + Assert.assertEquals(cdds[0].getSimpleName(), "active installed resolved"); + Assert.assertNull(cdds[0].getFilter()); + m.clear(); + } + + public void testBundleDependencyDeclarationWithMask() throws MalformedURLException { + DependencyManager m = getDM(); + Component c = m.createComponent() + .setImplementation(new Object()) + .add(m.createBundleDependency() + .setStateMask( Bundle.ACTIVE | Bundle.RESOLVED)); + + ComponentDeclaration cd = c.getComponentDeclaration(); + ComponentDependencyDeclaration[] cdds = cd.getComponentDependencies(); + Assert.assertNotNull(cdds); + Assert.assertNotNull(cdds.length == 1); + Assert.assertEquals(cdds[0].getName(), "active resolved"); + Assert.assertNotNull(cdds[0].getSimpleName()); + Assert.assertEquals(cdds[0].getSimpleName(), "active resolved"); + Assert.assertNull(cdds[0].getFilter()); + m.clear(); + } + + public void testBundleDependencyDeclarationWithFilter() throws MalformedURLException { + DependencyManager m = getDM(); + Component c = m.createComponent() + .setImplementation(new Object()) + .add(m.createBundleDependency() + .setStateMask( Bundle.ACTIVE ) + .setFilter("(DependencyManager-Component=*)")); + + ComponentDeclaration cd = c.getComponentDeclaration(); + ComponentDependencyDeclaration[] cdds = cd.getComponentDependencies(); + Assert.assertNotNull(cdds); + Assert.assertNotNull(cdds.length == 1); + Assert.assertEquals(cdds[0].getName(), "active (DependencyManager-Component=*)"); + Assert.assertNotNull(cdds[0].getSimpleName()); + Assert.assertEquals(cdds[0].getSimpleName(), "active"); + Assert.assertNotNull(cdds[0].getFilter()); + Assert.assertEquals(cdds[0].getFilter(), "(DependencyManager-Component=*)"); + m.clear(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX4361_ConcurrentComponentListingTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX4361_ConcurrentComponentListingTest.java new file mode 100644 index 00000000000..0752c3f33d3 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX4361_ConcurrentComponentListingTest.java @@ -0,0 +1,109 @@ +/* + * 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.dm.itest.api; + +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Assert; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.TestBase; + + +/** + * Test for FELIX-4361 The DependencyManager.getComponents method failed in a concurrent situation on iterating the + * result of the method. + * + * @author Felix Project Team + */ +public class FELIX4361_ConcurrentComponentListingTest extends TestBase { + + @SuppressWarnings("rawtypes") + public void testConcurrentGetComponentsManipulation() { + DependencyManager dm = getDM(); + dm.add(dm.createComponent().setImplementation(Object.class)); + Iterator iterator = dm.getComponents().iterator(); + dm.add(dm.createComponent().setImplementation(Object.class)); + iterator.next(); + dm.clear(); + } + + public void testConcurrentGetComponentsMultipleThreads() { + final DependencyManager m = getDM(); + final AtomicInteger errors = new AtomicInteger(0); + final AtomicInteger componentsAdded = new AtomicInteger(0); + final int max = 10000; + final AtomicBoolean isRunning = new AtomicBoolean(true); + + ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1); + Runnable readTask = new Runnable() { + @SuppressWarnings({ "rawtypes", "unused" }) + public void run() { + while (isRunning.get()) { + try { + List components = m.getComponents(); + for (Object component : components) { + // Just iterating the components should check for concurrent modifications + } + } + catch (Exception ex) { + errors.addAndGet(1); + ex.printStackTrace(); + } + } + } + }; + + Callable modifyTask = new Callable() { + public Boolean call() throws Exception { + try { + m.add(m.createComponent().setImplementation(Object.class)); + componentsAdded.addAndGet(1); + return true; + } + catch (Exception ex) { + return false; + } + } + }; + + executorService.submit(readTask); + for (int i = 0; i < max; i++) { + executorService.submit(modifyTask); + } + isRunning.set(false); + executorService.shutdown(); + + try { + executorService.awaitTermination(30, TimeUnit.SECONDS); + } + catch (InterruptedException e) { + } + Assert.assertEquals(0, errors.get()); + Assert.assertEquals(max, componentsAdded.get()); + m.clear(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX4614_FactoryWithComponentInCreateParam.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX4614_FactoryWithComponentInCreateParam.java new file mode 100644 index 00000000000..b4ab1438f58 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX4614_FactoryWithComponentInCreateParam.java @@ -0,0 +1,74 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; + +/** + * @author Felix Project Team + */ +public class FELIX4614_FactoryWithComponentInCreateParam extends TestBase { + private final static Ensure m_ensure = new Ensure(); + + public void testSimpleFactory() { + DependencyManager manager = getDM(); + + Component service = manager.createComponent() + .setFactory(Factory.class, "create") + .setInterface(Service.class.getName(), null); + + Component client = manager.createComponent() + .setImplementation(Client.class) + .add(manager.createServiceDependency().setService(Service.class).setRequired(true)); + + manager.add(client); + manager.add(service); + m_ensure.waitForStep(3, 5000); + manager.clear(); + } + + public static class Factory { + public Object create(Component c) { + m_ensure.step(1); + Assert.assertNotNull(c); + m_ensure.step(2); + return new ServiceImpl(); + } + } + + public static interface Service { + } + + public static class ServiceImpl implements Service { + } + + public static class Client { + volatile Service m_service; + + void start() { + Assert.assertNotNull(m_service); + m_ensure.step(3); + } + } +} + diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX4869_CallbackNotCalled.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX4869_CallbackNotCalled.java new file mode 100644 index 00000000000..c9285209dc2 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX4869_CallbackNotCalled.java @@ -0,0 +1,89 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; + +/** + * @author Felix Project Team + */ +public class FELIX4869_CallbackNotCalled extends TestBase { + public void testAbstractClassDependency() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + TestComponent test = new TestComponent(e); + Component c = m.createComponent().setImplementation(test); + + // start the component + m.add(c); + e.waitForStep(1, 5000); + + // add two other services + ServiceRegistration sr1 = context.registerService(Object.class.getName(), new Object(), null); + ServiceRegistration sr2 = context.registerService(Object.class.getName(), new Object(), null); + + // now add a dependency to the just previously registered services + test.addDependency(); + + // and verify that the add callback has been invoked two times. + e.waitForStep(3, 5000); + + // Unregister the two services + sr1.unregister(); + sr2.unregister(); + + // and verify that the remove callbacks have been invoked two times + e.waitForStep(5, 5000); + + // Clear components. + m.clear(); + } + + public static class TestComponent { + private volatile DependencyManager dm; + private volatile Component c; + private final Ensure e; + + public TestComponent(Ensure e) { + this.e = e; + } + + public void start() { + e.step(1); + } + + public void addDependency(){ + c.add(dm.createServiceDependency().setService(Object.class).setCallbacks("add", "remove")); + } + + public void add(ServiceReference ref) { + e.step(); + } + + public void remove(ServiceReference ref) { + e.step(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX4913_OptionalCallbackInvokedTwiceTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX4913_OptionalCallbackInvokedTwiceTest.java new file mode 100644 index 00000000000..58f64ec4abc --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX4913_OptionalCallbackInvokedTwiceTest.java @@ -0,0 +1,152 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; + +/** + * This test validates a corner case: + * + * We have the following component players: A, BFactory, B. + * + * component A optionally depends on BFactory. + * component A optionally depends on B + * + * when A.bind(BFactory factory) is called, the factory.create() method is then invoked, which triggers a registration of the B Service. + * At this point A.bind(B) should be called back. + * + * @author Felix Project Team + */ +public class FELIX4913_OptionalCallbackInvokedTwiceTest extends TestBase { + + public void test_A_Defines_BDependency_FromInitMethod() throws Throwable { + final DependencyManager m = getDM(); + Ensure e = new Ensure(); + + Component bFactory = m.createComponent().setImplementation(new BFactory()).setInterface(BFactory.class.getName(), null); + Dependency dep = m.createServiceDependency().setService(B.class).setRequired(false).setCallbacks("bind", "unbind"); + Component a = m.createComponent() + .setImplementation(new A(e, dep)) + .add(m.createServiceDependency().setService(BFactory.class).setRequired(false).setCallbacks("bind", "unbind")); + + // Enable first bFactory. + m.add(bFactory); + + // Then Enable A. + m.add(a); + + // A should get BFactory, then it should instantiate B, and B should then be bound to A. + e.waitForStep(4, 5000); + + // Now, remove BFactory. The AComponent should then call bFactory.removeB(), abd A.unbind(B) should be called. + m.remove(bFactory); + e.waitForStep(6, 5000); + e.ensure(); + clearComponents(); + } + + public void test_A_Defines_BDependency_BeforeActivation() throws Throwable { + final DependencyManager m = getDM(); + Ensure e = new Ensure(); + + Component bFactory = m.createComponent() + .setImplementation(new BFactory()).setInterface(BFactory.class.getName(), null); + Component a = m.createComponent() + .setImplementation(new A(e, null)) + .add(m.createServiceDependency().setService(B.class).setRequired(false).setCallbacks("bind", "unbind")) + .add(m.createServiceDependency().setService(BFactory.class).setRequired(false).setCallbacks("bind", "unbind")); + + // Enable first bFactory. + m.add(bFactory); + + // Then Enable A. + m.add(a); + + // A should get BFactory, then it should instantiate B, and B should then be bound to A. + e.waitForStep(4, 5000); + + // Now, remove BFactory. The AComponent should then call bFactory.removeB(), abd A.unbind(B) should be called. + m.remove(bFactory); + e.waitForStep(6, 5000); + e.ensure(); + clearComponents(); + } + + public static class A { + final Ensure m_e; + final Dependency m_BDependency; + + public A(Ensure e, Dependency bfactoryDependency) { + m_e = e; + m_BDependency = bfactoryDependency; + } + + void init(Component component) { + m_e.step(1); + if (m_BDependency != null) { + component.add(m_BDependency); + } + } + + void start() { + m_e.step(2); + } + + void bind(BFactory bFactory) { + m_e.step(3); + bFactory.createB(); + } + + void unbind(BFactory bFactory) { + m_e.step(5); + bFactory.deleteB(); + } + + void bind(B b) { + m_e.step(4); + } + + void unbind(B b) { + m_e.step(6); + } + } + + public static class BFactory { + volatile BundleContext m_bctx; + ServiceRegistration m_registraiton; + + void createB() { + m_registraiton = m_bctx.registerService(B.class.getName(), new B(), null); + } + + void deleteB() { + m_registraiton.unregister(); + } + } + + public static class B { + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5045_OptionalDependencyCBCalledBeforeStartTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5045_OptionalDependencyCBCalledBeforeStartTest.java new file mode 100644 index 00000000000..158f65d5016 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5045_OptionalDependencyCBCalledBeforeStartTest.java @@ -0,0 +1,117 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; + +/** + * This test validates a corner case: + * + * We have the following component players: A, BFactory, B. + * + * component A defines from A.init() a required dependency on BFactory, and an optional dependency on B. + * component A has a "start" lifecycle callback. + * + * when A.bind(BFactory factory) is called, the factory.create() method is then invoked, which triggers a registration of the B Service. + * At this point B is available, then A.start() should be called before A.bind(B). + * + * @author Felix Project Team + */ +public class FELIX5045_OptionalDependencyCBCalledBeforeStartTest extends TestBase { + public void test_A_DependsOnBFactoryFromInit() throws Throwable { + final DependencyManager m = getDM(); + Ensure e = new Ensure(); + + Component bFactory = m.createComponent().setImplementation(new BFactory()).setInterface(BFactory.class.getName(), null); + Component a = m.createComponent().setImplementation(new A(e)); + + // Enable first bFactory. + m.add(bFactory); + + // Then enable A. + m.add(a); + + // A should get BFactory, then it should instantiate B, then A.start() should be called, then A.bind(B) should be called. + e.waitForStep(4, 5000); + + // Now, remove BFactory. A.unbind(B b) should be called, then + m.remove(bFactory); + e.waitForStep(6, 5000); + e.ensure(); + } + + public static class A { + final Ensure m_e; + + public A(Ensure e) { + m_e = e; + } + + void init(Component component) { + m_e.step(1); + DependencyManager dm = component.getDependencyManager(); + Dependency depBFactory = dm.createServiceDependency().setService(BFactory.class).setRequired(true).setCallbacks("bind", "unbind"); + Dependency depB = dm.createServiceDependency().setService(B.class).setRequired(false).setCallbacks("bind", "unbind"); + component.add(depBFactory, depB); + } + + void bind(BFactory bFactory) { + m_e.step(2); + bFactory.createB(); + } + + void start() { + m_e.step(3); + } + + void bind(B b) { + m_e.step(4); + } + + void unbind(B b) { + m_e.step(5); + } + + void unbind(BFactory bFactory) { + m_e.step(6); + } + } + + public static class BFactory { + volatile BundleContext m_bctx; + ServiceRegistration m_registraiton; + + void createB() { + m_registraiton = m_bctx.registerService(B.class.getName(), new B(), null); + } + + void deleteB() { + m_registraiton.unregister(); + } + } + + public static class B { + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5054_CleanupInstanceBoundDependenciesInDestroy.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5054_CleanupInstanceBoundDependenciesInDestroy.java new file mode 100644 index 00000000000..98704302c20 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5054_CleanupInstanceBoundDependenciesInDestroy.java @@ -0,0 +1,106 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * @author Felix Project Team + */ +public class FELIX5054_CleanupInstanceBoundDependenciesInDestroy extends TestBase { + Ensure m_ensure; + + public void testInstanceBoundDependencyNotReAddedTwice() { + DependencyManager m = getDM(); + m_ensure = new Ensure(); + + A aObject = new A(); + Component a = m.createComponent() + .setImplementation(aObject) + .add(m.createServiceDependency().setService(B.class).setRequired(true).setCallbacks("bindB", "unbindB")); + + Component b = m.createComponent() + .setImplementation(new B()) + .setInterface(B.class.getName(), null); + + Component c = m.createComponent() + .setImplementation(new C()) + .setInterface(C.class.getName(), null); + + m.add(b); + m.add(a); + m.add(c); + + m.remove(b); + m_ensure.waitForStep(5, 3000); + m.add(b); + m_ensure.waitForStep(8, 3000); + aObject.testDone(); + m.clear(); + } + + public class A { + private Ensure.Steps m_stepsBindB = new Ensure.Steps(1, 6); + private Ensure.Steps m_stepsUnbindB = new Ensure.Steps(5); + private Ensure.Steps m_stepsBindC = new Ensure.Steps(3, 8); + private Ensure.Steps m_stepsUnbindC = new Ensure.Steps(4); + private Ensure.Steps m_stepsInit = new Ensure.Steps(2, 7); + private Dependency m_depC; + private boolean m_done; + + void bindB(B b) { + m_ensure.steps(m_stepsBindB); + } + + public void testDone() { + m_done = true; + } + + void unbindB(B b) { + if (! m_done) m_ensure.steps(m_stepsUnbindB); + } + + void init(Component component) { + DependencyManager dm = component.getDependencyManager(); + m_depC = dm.createServiceDependency().setService(C.class).setRequired(true).setCallbacks("bindC", "unbindC"); + m_ensure.steps(m_stepsInit); + component.add(m_depC); + } + + void bindC(C c) { + m_ensure.steps(m_stepsBindC); + } + + void unbindC(C c) { + if (! m_done) m_ensure.steps(m_stepsUnbindC); + } + } + + public static class B { + + } + + public static class C { + + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5153_StopBeforeUngetServiceTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5153_StopBeforeUngetServiceTest.java new file mode 100644 index 00000000000..3773a9d8acb --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5153_StopBeforeUngetServiceTest.java @@ -0,0 +1,59 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.itest.bundle.HelloWorld; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceReference; + +/** + * @author Felix Project Team + */ +public class FELIX5153_StopBeforeUngetServiceTest extends TestBase { + public void testCallStopAfterUngetService() throws Throwable { + Bundle testBundle = null; + + try { + ServiceReference[] refs = context.getServiceReferences(HelloWorld.class.getName(), "(dm=dm4)"); + HelloWorld service = (HelloWorld) context.getService(refs[0]); + System.out.println(service.sayIt("DM4")); + + for (Bundle b : context.getBundles()) { + if (b.getSymbolicName().equals("org.apache.felix.dependencymanager.itest.bundle")) { + testBundle = b; + // Stop the test bundle. In this test bundle, we have a service factory component, and its unget method + // is expected to be called *before* its stop() method. + b.stop(); + break; + } + } + } + + catch (Throwable t) { + error("test failed", t); + } + + finally { + if (testBundle != null) { + testBundle.start(); // restart the test bundle + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5200_FactoryPidNotRestartedTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5200_FactoryPidNotRestartedTest.java new file mode 100644 index 00000000000..1bab6654829 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5200_FactoryPidNotRestartedTest.java @@ -0,0 +1,135 @@ +/* + * 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.dm.itest.api; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Properties; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * Use case: a factory configuration adapter has a required dependency on a "Required" service. + * The factory configuration is created, the "Required" service is registered, so a factory configuration adapter INSTANCE1 is then created. + * Now the "Required" service is unregistered: the factory config adapter INSTANCE1 is then stopped. + * And when the "Required" service comes up again, then a new factory config adapter INSTANCE2 should be re-created, updated and started. + * + * @author Felix Project Team + */ +public class FELIX5200_FactoryPidNotRestartedTest extends TestBase { + static FactoryPidComponent m_currentInstance; + + static Ensure m_ensure = new Ensure(); + + static Ensure.Steps m_steps = new Ensure.Steps( + 1, // updated is called the first time the adapter instance is created + 2, // component started + 3, // "Required" service is unregistered, and adapter instance is stopped + 4, // "Required" service is re-registered, a new adapter instance is created and called in updated + 5, // the new adapter instance is then started + 6 // the configuration is removed and the new adapter instance is stopped + ); + + public void testFactoryAdapterNotRestartedTest() { + DependencyManager dm = getDM(); + + Component adapter = dm.createFactoryConfigurationAdapterService("factory.pid", "updated", false) + .setImplementation(FactoryPidComponent.class) + .add(dm.createServiceDependency().setService(RequiredService.class).setRequired(true)); + + Component required = dm.createComponent() + .setImplementation(RequiredService.class) + .setInterface(RequiredService.class.getName(), null); + + Component configurator = dm.createComponent() + .setImplementation(new Configurator("factory.pid")) + .add(dm.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)); + + dm.add(configurator); + dm.add(adapter); + dm.add(required); + // adapter instance1 updated and started + m_ensure.waitForStep(2, 5000); + FactoryPidComponent currentInstance = m_currentInstance; + dm.remove(required); + // adapter instance1 stopped + m_ensure.waitForStep(3, 5000); + dm.add(required); + // adapter instance2 updated and started + m_ensure.waitForStep(5, 5000); + // a new adapter instance should have been created + Assert.assertNotEquals(currentInstance, m_currentInstance); + dm.remove(configurator); + // adapter instance2 stopped + m_ensure.waitForStep(6, 5000); + } + + public static class FactoryPidComponent { + RequiredService m_required; + + void updated(Dictionary properties) { + m_ensure.steps(m_steps); + } + + void start() { + m_ensure.steps(m_steps); + m_currentInstance = this; + } + + void stop() { + m_ensure.steps(m_steps); + } + } + + public static class RequiredService { + } + + class Configurator { + private volatile ConfigurationAdmin m_ca; + Configuration m_conf; + final String m_pid; + + public Configurator(String pid) { + m_pid = pid; + } + + public void init() { + try { + Assert.assertNotNull(m_ca); + m_conf = m_ca.createFactoryConfiguration(m_pid, null); + Properties props = new Properties(); + props.setProperty("some", "properties"); + m_conf.update(toR6Dictionary(props)); + } + catch (IOException e) { + Assert.fail("Could not create configuration: " + e.getMessage()); + } + } + + public void destroy() throws IOException { + m_conf.delete(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5238_TypeSafeConfigWithDefaultMethodTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5238_TypeSafeConfigWithDefaultMethodTest.java new file mode 100644 index 00000000000..3c8c92eb59b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5238_TypeSafeConfigWithDefaultMethodTest.java @@ -0,0 +1,155 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.ConfigurationException; + + +/** + * Tests for type-safe configuration using either an annotation or an interface having + * some default methods. + * + * @author Felix Project Team + */ +public class FELIX5238_TypeSafeConfigWithDefaultMethodTest extends TestBase { + final static String PID = "ConfigurationDependencyTest.pid"; + + /** + * Tests that we can provision a type-safe config annotation to a component. + */ + public void testComponentWithRequiredConfigurationWithTypeSafeConfigAnnotation() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + ConfigurationConsumer1 consumer = new ConfigurationConsumer1(e); + Component s1 = m.createComponent() + .setImplementation(consumer).add(m.createConfigurationDependency().setCallback("updated", MyConfigAnnot.class).setPid(PID).setPropagate(true)); + Component s2 = m.createComponent() + .setImplementation(new ConfigurationCreator(e, PID, 1)).add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)); + m.add(s1); + m.add(s2); + e.waitForStep(1, 5000); // s2 called in init + e.waitForStep(3, 5000); // s1 called in updated(), then in init() + m.remove(s2); // remove conf + e.waitForStep(5, 5000); // s1 called in updated(null), s1 called in destroy() + m.remove(s1); + } + + /** + * Tests that we can provision a type-safe config annotation to a component. + */ + public void testComponentWithRequiredConfigurationWithTypeSafeConfigInterfaceWithDefaultMethod() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + ConfigurationConsumer2 consumer = new ConfigurationConsumer2(e); + Component s1 = m.createComponent() + .setImplementation(consumer).add(m.createConfigurationDependency().setCallback("updated", MyConfigInterface.class).setPid(PID).setPropagate(true)); + Component s2 = m.createComponent() + .setImplementation(new ConfigurationCreator(e, PID, 1)).add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)); + m.add(s1); + m.add(s2); + e.waitForStep(1, 5000); // s2 called in init + e.waitForStep(3, 5000); // s1 called in updated(), then in init() + m.remove(s2); // remove conf + e.waitForStep(5, 5000); // s1 called in updated(null), s1 called in destroy() + m.remove(s1); + } + + public static @interface MyConfigAnnot { + String getTestkey(); + String getTestkey2() default "123"; + } + + public static interface MyConfigInterface { + String getTestkey(); + default String getTestkey2() { return "123"; } + } + + static class ConfigurationConsumerBase { + protected final Ensure m_ensure; + + public ConfigurationConsumerBase(Ensure e) { + m_ensure = e; + } + + // called after configuration has been injected. + public void init() { + m_ensure.step(3); + } + + public void destroy() { + m_ensure.step(); + } + } + + static class ConfigurationConsumer1 extends ConfigurationConsumerBase { + public ConfigurationConsumer1(Ensure e) { + super(e); + } + + // configuration updates is always the first invoked callback (before init). + public void updated(Component component, MyConfigAnnot cfg) throws ConfigurationException { + if (cfg != null) { + Assert.assertNotNull(component); + Assert.assertNotNull(cfg); + if (!"testvalue".equals(cfg.getTestkey())) { + Assert.fail("Could not find the configured property."); + } + if (!"123".equals(cfg.getTestkey2())) { + Assert.fail("Could not find the configured property."); + } + m_ensure.step(2); + } else { + m_ensure.step(); + } + } + } + + static class ConfigurationConsumer2 extends ConfigurationConsumerBase { + public ConfigurationConsumer2(Ensure e) { + super(e); + } + + // configuration updates is always the first invoked callback (before init). + public void updated(Component component, MyConfigInterface cfg) throws ConfigurationException { + if (cfg != null) { + Assert.assertNotNull(component); + Assert.assertNotNull(cfg); + if (!"testvalue".equals(cfg.getTestkey())) { + Assert.fail("Could not find the configured property."); + } + if (!"123".equals(cfg.getTestkey2())) { + Assert.fail("Could not find the configured property."); + } + m_ensure.step(2); + } else { + m_ensure.step(); + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5243_ConfigUpdateMissedIfComponentIsRestartingTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5243_ConfigUpdateMissedIfComponentIsRestartingTest.java new file mode 100644 index 00000000000..ed7762c4df2 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5243_ConfigUpdateMissedIfComponentIsRestartingTest.java @@ -0,0 +1,182 @@ +/* + * 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.dm.itest.api; + +import java.io.IOException; +import java.util.Hashtable; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * This test reproduces the following race condition: + * + * A "Pojo" component depends on a required "MyDependency" service. + * The Pojo also has a configuration dependency. + * + * Now, concurrently, the MyDependency is restarted and the configuration is updated. + * This means that the Pojo is restarted and should also get notified for the configuration update. + * + * @author Felix Project Team + */ +public class FELIX5243_ConfigUpdateMissedIfComponentIsRestartingTest extends TestBase { + final static String PID = "ConfigurationDependencyTest.pid"; + + public void testOptionalConfigurationConsumer() throws InterruptedException { + DependencyManager m = getDM(); + Ensure e = new Ensure(); + + UpdateController controller = new UpdateController(); + Pojo pojo = new Pojo(controller); + Configurator configurator = new Configurator(e, "my.pid"); + ThreadPoolExecutor tpool = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); + + try { + Component pojoComp = m.createComponent().setImplementation(pojo) + .add(m.createServiceDependency().setService(MyDependency.class).setRequired(true)) + .add(m.createConfigurationDependency().setCallback("updated", MyConfig.class).setPid("my.pid")); + + Component configuratorComp = m.createComponent().setImplementation(configurator) + .add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)); + + Component myDep = m.createComponent().setImplementation(new MyDependency()) + .setInterface(MyDependency.class.getName(), null); + + m.add(configuratorComp); + m.add(pojoComp); + m.add(myDep); + + e.waitForStep(1, 5000); // configurator is ready. + + // make sure pojo is updated with property=2 + controller.setExpectedUpdatedProperty(2); + configurator.update(2); + controller.awaitUpdated(); + + // Now, make a loop: for each loop: + // - concurrently remove/add the dependency in order to restart the pojo. + // - update the configuration. + // - and check if the component has not missed the configuration update + + for (int i = 3; i < 10000; i ++) { + controller.setExpectedUpdatedProperty(i); + tpool.execute(() -> { + m.remove(myDep); + m.add(myDep); + }); + configurator.update(i); + controller.awaitUpdated(); + } + + m.remove(configuratorComp); + m.remove(pojoComp); + m.remove(myDep); + } finally { + tpool.shutdown(); + } + } + + static interface MyConfig { + int getProperty(); + } + + static class UpdateController { + volatile CountDownLatch m_latch; + volatile int m_expected; + + public void setExpectedUpdatedProperty(int expected) { + m_expected = expected; + m_latch = new CountDownLatch(1); + } + + public void updated(int property) { + if (property == m_expected) { + m_latch.countDown(); + } + } + + public void awaitUpdated() throws InterruptedException { + if (! m_latch.await(5000, TimeUnit.MILLISECONDS)) { + throw new IllegalStateException("property not updated to value " + m_expected); + } + if (m_expected % 100 == 0) { + System.out.println("#updated " + m_expected); + } + } + } + + static class MyDependency { + } + + static class Pojo { + final UpdateController m_controller; + volatile MyDependency m_dependency; + + Pojo(UpdateController controller) { + m_controller = controller; + } + + void updated(MyConfig cnf) { + if (cnf != null) { + m_controller.updated(cnf.getProperty()); + } + } + } + + static class Configurator { + volatile ConfigurationAdmin m_ca; + volatile Configuration m_conf; + final String m_pid; + final Ensure m_e; + + public Configurator(Ensure e, String pid) { + m_pid = pid; + m_e = e; + } + + public void start() throws IOException { + m_conf = m_ca.getConfiguration(m_pid, null); + m_e.step(1); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void update(int property) { + try { + Hashtable props = new Properties(); + props.put("property", property); + m_conf.update(props); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void destroy() throws IOException { + m_conf.delete(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5268_AddRemoveServiceTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5268_AddRemoveServiceTest.java new file mode 100644 index 00000000000..d1211ef1d66 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5268_AddRemoveServiceTest.java @@ -0,0 +1,58 @@ +/* + * 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.dm.itest.api; + +import java.util.stream.Stream; + +import org.apache.felix.dm.itest.bundle2.AddRemoveService; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; +import org.osgi.framework.ServiceReference; + +/** + * FELIX_XXX: When a bundle activator is invoked because the bundle is starting, then if the + * activator adds/removes a DM service providing component, then the service should be also removed from the + * OSGI service registry. + */ +public class FELIX5268_AddRemoveServiceTest extends TestBase { + + public void testStartStop() throws BundleException { + // Lookup our test bundle which adds/removes a "AddRemoveService" component. + + Bundle testBundle = Stream.of(context.getBundles()) + .filter(b -> b.getSymbolicName().equals("org.apache.felix.dependencymanager.itest.bundle2")) + .findFirst() + .orElseThrow(() -> new RuntimeException("cound not find bundle2 test.")); + + // Stop it + testBundle.stop(); + + // Restart it + testBundle.start(); + + // At this point, then AddRemoveService should not be registered in the OSGI service registry. + ServiceReference ref = context.getServiceReference(AddRemoveService.class.getName()); + Assert.assertNull(ref); + + // We don't need this bundle anymore, stop it. + testBundle.stop(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5274_CleanupInstanceBoundDependenciesInDestroy.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5274_CleanupInstanceBoundDependenciesInDestroy.java new file mode 100644 index 00000000000..25b4a4a8fb8 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5274_CleanupInstanceBoundDependenciesInDestroy.java @@ -0,0 +1,110 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * @author Felix Project Team + */ +public class FELIX5274_CleanupInstanceBoundDependenciesInDestroy extends TestBase { + Ensure m_ensure; + + public void testInstanceBoundDependencyNotReAddedTwice() { + DependencyManager m = getDM(); + m_ensure = new Ensure(); + + A aObject = new A(); + Component a = m.createComponent() + .setImplementation(aObject) + .add(m.createServiceDependency().setService(B.class).setRequired(true).setCallbacks("bindB", "unbindB")); + + Component b = m.createComponent() + .setImplementation(new B()) + .setInterface(B.class.getName(), null); + + Component c = m.createComponent() + .setImplementation(new C()) + .setInterface(C.class.getName(), null); + + m.add(b); + m.add(a); + m.add(c); + + m.remove(b); + m_ensure.waitForStep(5, 3000); + m.add(b); + m_ensure.waitForStep(8, 3000); + aObject.testDone(); + m.clear(); + } + + public class A { + private Ensure.Steps m_stepsBindB = new Ensure.Steps(1, 6); + private Ensure.Steps m_stepsUnbindB = new Ensure.Steps(5); + private Ensure.Steps m_stepsBindC = new Ensure.Steps(3, 8); + private Ensure.Steps m_stepsUnbindC = new Ensure.Steps(4); + private Ensure.Steps m_stepsInit = new Ensure.Steps(2, 7); + private Dependency m_depC; + private boolean m_done; + + void bindB(B b) { + m_ensure.steps(m_stepsBindB); + } + + public void testDone() { + m_done = true; + } + + void unbindB(B b) { + if (! m_done) m_ensure.steps(m_stepsUnbindB); + } + + void init(Component component) { + DependencyManager dm = component.getDependencyManager(); + m_depC = dm.createServiceDependency().setService(C.class).setRequired(true).setCallbacks("bindC", "unbindC"); + m_ensure.steps(m_stepsInit); + component.add(m_depC); + } + + void destroy(Component component) { + component.remove(m_depC); + } + + void bindC(C c) { + m_ensure.steps(m_stepsBindC); + } + + void unbindC(C c) { + if (! m_done) m_ensure.steps(m_stepsUnbindC); + } + } + + public static class B { + + } + + public static class C { + + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5305_DirectInjectionShouldSeeServiceUpdates.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5305_DirectInjectionShouldSeeServiceUpdates.java new file mode 100644 index 00000000000..805db15f70a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5305_DirectInjectionShouldSeeServiceUpdates.java @@ -0,0 +1,175 @@ +/* + * 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.dm.itest.api; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.service.cm.ConfigurationAdmin; + +public class FELIX5305_DirectInjectionShouldSeeServiceUpdates extends TestBase { + + final static String PID = "my.service.pid"; + final static Ensure m_ensure = new Ensure(); + + public void testFieldInjectionWithFactoryConfigServices() throws InterruptedException { + DependencyManager m = getDM(); + + ConfigurationCreator configurator = new ConfigurationCreator(PID); + Component compConfigurator = m.createComponent() + .setImplementation(configurator) + .add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)); + m.add(compConfigurator); + + m_ensure.waitForStep(1, 3000); + + MyServices myServices = new MyServices(); + m.add(m.createComponent() + .setImplementation(myServices) + .setInterface(MyServices.class.getName(), null) + .add(m.createServiceDependency().setService(Service.class, "(provider=*)").setRequired(true))); + + m.add(m.createFactoryConfigurationAdapterService(PID, "update", false /* propagate */) + .setInterface(Service.class.getName(), null) + .setImplementation(ServiceImpl.class)); + + configurator.update("provider", "message1"); + Thread.sleep(500); + Assert.assertEquals("message1", myServices.getMessages().get("provider")); + + configurator.update("provider", "message2"); + Thread.sleep(500); + Assert.assertEquals("message2", myServices.getMessages().get("provider")); + } + + public interface Service { + String getMessage(); + } + + public static class ServiceImpl implements Service { + // Managed by Felix DM... + private volatile Component m_comp; + // Locally managed... + private volatile String m_msg; + + @Override + public String getMessage() { + return m_msg; + } + + /** + * Called by Felix DM. + */ + protected final void start(Component comp) throws Exception { + System.out.printf("ServiceImpl@%d started (msg = %s)%n", hashCode(), m_msg); + } + + /** + * Called by Felix DM. + */ + protected final void stop(Component comp) throws Exception { + System.out.printf("ServiceImpl@%d stopped (msg = %s)%n", hashCode(), m_msg); + } + + /** + * Called by Felix DM. + */ + protected final void update(Dictionary config) throws Exception { + String provider; + if (config != null) { + m_msg = (String) config.get("msg"); + provider = (String) config.get("provider"); + } else { + m_msg = ""; + provider = ""; + } + + System.out.printf("ServiceImpl@%d config updated (msg = %s; provider = %s)%n", hashCode(), m_msg, provider); + + Dictionary props = m_comp.getServiceProperties(); + if (props == null) { + props = new Hashtable<>(); + } + + props.put("provider", provider); + m_comp.setServiceProperties(props); + } + } + + public static class MyServices { + // Injected by Felix DM... + private volatile Map> m_services; + + public Map getMessages() { + Map> services = m_services; + + Map result = new HashMap<>(services.size()); + for (Service srv : services.keySet()) { + String provider = (String) services.get(srv).get("provider"); + result.put(provider, srv.getMessage()); + } + return result; + } + } + + public static class ConfigurationCreator { + private volatile ConfigurationAdmin m_ca; + private org.osgi.service.cm.Configuration m_conf; + private String m_factoryPid; + + public ConfigurationCreator(String factoryPid) { + m_factoryPid = factoryPid; + } + + public void start() { + m_ensure.step(1); + } + + public void update(String provider, String msg) { + try { + if (m_conf == null) { + m_conf = m_ca.createFactoryConfiguration(m_factoryPid, null); + } + Hashtable props = new Hashtable<>(); + props.put("msg", msg); + props.put("provider", provider); + m_conf.update(props); + } catch (IOException e) { + Assert.fail("Could not update configuration: " + e.getMessage()); + } + } + + public void stop() { + try { + System.out.println("Destroying conf"); + m_conf.delete(); + } catch (IOException e) { + Assert.fail("Could not remove configuration: " + e.toString()); + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5355_ConfigTypesWithDotsTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5355_ConfigTypesWithDotsTest.java new file mode 100644 index 00000000000..c8360f4cbb7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5355_ConfigTypesWithDotsTest.java @@ -0,0 +1,111 @@ +/* + * 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.dm.itest.api; + +import java.util.Arrays; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * Validates the case when a config type property contains some dot "." characters. + * In this case, the configuration type interface method must contain a "_" characeter which is mapped to ".". + */ +public class FELIX5355_ConfigTypesWithDotsTest extends TestBase +{ + Ensure m_ensure = new Ensure(); + + public void testPropertiesWithDots() { + DependencyManager m = getDM(); + + ConfigurationCreator configurator = new ConfigurationCreator(m_ensure, MyConfig.class.getName(), 2, + "foo.bar.param1=bar", + "foo_param2=bar2", + "param3=bar3", + "foo_BaR.Param4=bar4", + "foo.bar5.0=1", + "foo.bar5.1=2", + "foo.bar6.0=1", + "foo.bar6.1=2", + "foo.bar7.key=value", + "foo.bar8={key.value}"); + + Component confCreator = m.createComponent().setImplementation(configurator) + .add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)); + + Component myComponent = m.createComponent() + .setImplementation(new MyComponent()) + .add(m.createConfigurationDependency().setCallback("updated", MyConfig.class).setRequired(false)); + + m.add(myComponent); + m_ensure.waitForStep(1, 5000); + + m.add(confCreator); + m_ensure.waitForStep(3, 5000); + } + + public interface MyConfig { + public default String getFooBarParam1() { return "default value"; } // getFooBarParam1() -> foo.bar.param1 + public default String foo__param2() { return "default value2"; } // foo__param2() -> foo_param2 + public default String getParam3() { return "default value3"; } // getParam3() -> param3 + public default String foo__BaR_Param4() { return "default value4"; } // foo__BaR_Param4 -> foo_BaR.Param4 + public default String[] getFooBar5() { return new String[] {"default value1", "default value2"}; } + public default List getFooBar6() { return Arrays.asList("default value1", "default value2"); } + public default SortedMap getFooBar7() { TreeMap defMap = new TreeMap<>(); defMap.put("key", "default"); return defMap; } + public default SortedMap getFooBar8() { TreeMap defMap = new TreeMap<>(); defMap.put("key", "default"); return defMap; } + } + + public class MyComponent { + int step = 0; + + void updated(MyConfig cnf) { + step ++; + if (step == 1) { + TreeMap defMap = new TreeMap<>(); defMap.put("key", "default"); + Assert.assertEquals("default value", cnf.getFooBarParam1()); + Assert.assertEquals("default value2", cnf.foo__param2()); + Assert.assertEquals("default value3", cnf.getParam3()); + Assert.assertEquals("default value4", cnf.foo__BaR_Param4()); + Assert.assertArrayEquals(new String[] { "default value1", "default value2"}, cnf.getFooBar5()); + Assert.assertEquals(Arrays.asList("default value1", "default value2"), cnf.getFooBar6()); + Assert.assertEquals(defMap, cnf.getFooBar7()); + Assert.assertEquals(defMap, cnf.getFooBar8()); + m_ensure.step(1); + } else if (step == 2) { + TreeMap map = new TreeMap<>(); map.put("key", "value"); + Assert.assertEquals("bar", cnf.getFooBarParam1()); + Assert.assertEquals("bar2", cnf.foo__param2()); + Assert.assertEquals("bar3", cnf.getParam3()); + Assert.assertEquals("bar4", cnf.foo__BaR_Param4()); + Assert.assertArrayEquals(new String[] { "1", "2"}, cnf.getFooBar5()); + Assert.assertEquals(map, cnf.getFooBar7()); + Assert.assertEquals(map, cnf.getFooBar8()); + m_ensure.step(3); + } + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5399_DefaultListConfigType.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5399_DefaultListConfigType.java new file mode 100644 index 00000000000..573026600b3 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5399_DefaultListConfigType.java @@ -0,0 +1,68 @@ +/* + * 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.dm.itest.api; + +import java.util.Arrays; +import java.util.List; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; + +/** + * This test validates that a list specified by default from a configuration type is used when + * the configuration is unavailable or when the configuration is available but the key is not present in the + * configuration dictionary. + */ +public class FELIX5399_DefaultListConfigType extends TestBase +{ + Ensure m_ensure = new Ensure(); + + public void testDefaulValues() { + DependencyManager m = getDM(); + + Component myComponent = m.createComponent() + .setImplementation(new MyComponent()) + .add(m.createConfigurationDependency().setCallback("updated", MyConfig.class).setRequired(false)); + + // create the component: since there is no value for the map in the actual configuration, then default map + // provided by the configuration type default method should be returned. + m.add(myComponent); + m_ensure.waitForStep(1, 5000); + + } + + public interface MyConfig { + public default List getList() { + return Arrays.asList("default1", "default2"); + } + } + + public class MyComponent { + void updated(MyConfig cnf) { + List list = cnf.getList(); + Assert.assertEquals("default1", list.get(0)); + Assert.assertEquals("default2", list.get(1)); + m_ensure.step(1); + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5399_DefaultMapConfigType.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5399_DefaultMapConfigType.java new file mode 100644 index 00000000000..15f70237b9b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5399_DefaultMapConfigType.java @@ -0,0 +1,70 @@ +/* + * 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.dm.itest.api; + +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; + +/** + * This test validates that a map specified by default from a configuration type is used when + * the configuration is unavailable or when the configuration is available but the key is not present in the + * configuration dictionary. + */ +public class FELIX5399_DefaultMapConfigType extends TestBase +{ + Ensure m_ensure = new Ensure(); + + public void testDefaulValues() { + DependencyManager m = getDM(); + + Component myComponent = m.createComponent() + .setImplementation(new MyComponent()) + .add(m.createConfigurationDependency().setCallback("updated", MyConfig.class).setRequired(false)); + + + m.add(myComponent); + m_ensure.waitForStep(1, 5000); + } + + public interface MyConfig { + public default SortedMap getMap() { + SortedMap defaultMap = new TreeMap<>(); + defaultMap.put("key1", "default1"); + defaultMap.put("key2", "default2"); + return defaultMap; + } + } + + public class MyComponent { + void updated(MyConfig cnf) { + Map map = cnf.getMap(); + Assert.assertEquals("default1", map.get("key1")); + Assert.assertEquals("default2", map.get("key2")); + m_ensure.step(1); + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5400_OverrideDefaultMapConfigTypeWithEmptyList.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5400_OverrideDefaultMapConfigTypeWithEmptyList.java new file mode 100644 index 00000000000..f452e98c8ba --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5400_OverrideDefaultMapConfigTypeWithEmptyList.java @@ -0,0 +1,80 @@ +/* + * 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.dm.itest.api; + +import java.util.Arrays; +import java.util.List; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * This test validates that we can register in the actual configuration an empty list for overriding a non empty default + * list that the configuration type has defined by default. + * + * Example: if the configuration type provides a default list value [a, b] for property "list", and if + * there is a "list=[]" entry in the actual configuration, then the empty list should be returned instead of default [a,b]. + */ +public class FELIX5400_OverrideDefaultMapConfigTypeWithEmptyList extends TestBase { + Ensure m_ensure = new Ensure(); + + public void testDefaulValues() { + DependencyManager m = getDM(); + + ConfigurationCreator configurator = new ConfigurationCreator(m_ensure, MyConfig.class.getName(), 1, "list=[]"); + + Component confCreator = m.createComponent() + .setImplementation(configurator) + .add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)); + + Component myComponent = m.createComponent() + .setImplementation(new MyComponent()) + .add(m.createConfigurationDependency().setCallback("updated", MyConfig.class).setRequired(true)); + + + // register an empty configuration. + m.add(confCreator); + m_ensure.waitForStep(1, 5000); + + // create the component: since there is no value for the map in the actual configuration, then default map + // provided by the configuration type default method should be returned. + m.add(myComponent); + m_ensure.waitForStep(2, 5000); + } + + public interface MyConfig { + public default List getList() { + return Arrays.asList("a", "b"); + } + } + + public class MyComponent { + void updated(MyConfig cnf) { + if (cnf != null) { + List list = cnf.getList(); + Assert.assertEquals(0, list.size()); // the actual configuration contains "list=[]" and default "list=[a,b]" must not be returned. + m_ensure.step(2); + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5401_OverrideDefaultListConfigTypeWithEmptyMap.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5401_OverrideDefaultListConfigTypeWithEmptyMap.java new file mode 100644 index 00000000000..3a676dc588e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5401_OverrideDefaultListConfigTypeWithEmptyMap.java @@ -0,0 +1,85 @@ +/* + * 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.dm.itest.api; + +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * This test validates that we can register in the actual configuration an empty map for overriding a non empty default + * map that the configuration type has defined by default. + * + * Example: if the configuration type provides a default map value {foo=bar} for property "map", and if + * there is a "map={}" entry in the actual configuration, then the empty map should be returned instead of {foo=bar}. + */ +public class FELIX5401_OverrideDefaultListConfigTypeWithEmptyMap extends TestBase { + Ensure m_ensure = new Ensure(); + + public void testDefaulValues() { + DependencyManager m = getDM(); + + ConfigurationCreator configurator = new ConfigurationCreator(m_ensure, MyConfig.class.getName(), 1, + "map={}"); + + Component confCreator = m.createComponent() + .setImplementation(configurator) + .add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)); + + Component myComponent = m.createComponent() + .setImplementation(new MyComponent()) + .add(m.createConfigurationDependency().setCallback("updated", MyConfig.class).setRequired(true)); + + + // register an empty configuration. + m.add(confCreator); + m_ensure.waitForStep(1, 5000); + + // create the component: since there is no value for the map in the actual configuration, then default map + // provided by the configuration type default method should be returned. + m.add(myComponent); + m_ensure.waitForStep(2, 5000); + + } + + public interface MyConfig { + public default SortedMap getMap() { + SortedMap defaultMap = new TreeMap<>(); + defaultMap.put("foo", "bar"); + return defaultMap; + } + } + + public class MyComponent { + void updated(MyConfig cnf) { + if (cnf != null) { + Map map = cnf.getMap(); + Assert.assertEquals(0, map.size()); // the actual configuration contains "map={}" and default "map={foo=bar}" must not be returned + m_ensure.step(2); + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5426_OptionalCallbackNotCalledTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5426_OptionalCallbackNotCalledTest.java new file mode 100644 index 00000000000..97e886ee9f5 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5426_OptionalCallbackNotCalledTest.java @@ -0,0 +1,125 @@ +/* + * 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.dm.itest.api; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; + +/** + * Validates that optional dependency callbacks are properly called in a special use case, where + * a circular dependency exists between two components. + */ +public class FELIX5426_OptionalCallbackNotCalledTest extends TestBase { + + final Ensure m_ensure = new Ensure(); + final Ensure.Steps m_bookSteps = new Ensure.Steps(3, 5, 8, 10); + final Ensure.Steps m_bookStoreSteps = new Ensure.Steps(2, 4, 6, 7, 9, 11); + + public void testCleanupDependenciesDuringComponentRemove() { + DependencyManager m = getDM(); + + Component owner = m.createComponent() + .setImplementation(new Owner()) + .setInterface(Owner.class.getName(), null); + + BookStore store = new BookStore(); + Component bookStore = m.createComponent() + .setImplementation(store).setInterface(BookStore.class.getName(), null) + .add(m.createServiceDependency().setService(Book.class).setCallbacks("added", "removed").setRequired(false)) + .add(m.createServiceDependency().setService(Owner.class).setRequired(true).setCallbacks("added", "removed")); + + Component book1 = m.createComponent() + .setImplementation(new Book()).setInterface(Book.class.getName(), null) + .add(m.createServiceDependency().setService(BookStore.class).setRequired(true)); + + Component book2 = m.createComponent() + .setImplementation(new Book()).setInterface(Book.class.getName(), null) + .add(m.createServiceDependency().setService(BookStore.class).setRequired(true)); + + m.add(owner); + m.add(bookStore); + m.add(book1); + m.add(book2); + + m.remove(owner); + m_ensure.waitForStep(11, 5000); + Assert.assertEquals(0, store.getBooksCount()); + } + + class Owner { + void start() { + m_ensure.step(1); + } + } + + class BookStore { + final List m_books = new ArrayList<>(); + + void added(Owner owner) { + System.out.println("BookStore.added(" + owner + ")"); + m_ensure.steps(m_bookStoreSteps); // step 2 + } + + void removed(Owner owner) { + } + + void added(Book book) { // injected, optional + m_books.add(book); + System.out.println("BookStore.added(" + book + ")"); + m_ensure.steps(m_bookStoreSteps); // steps 4, 6 + } + + void removed(Book book) { + m_books.remove(book); + System.out.println("BookStore.remove(" + book + ")"); + m_ensure.steps(m_bookStoreSteps); // steps 7, 9 + } + + int getBooksCount() { + return m_books.size(); + } + + void stop() { + System.out.println("BookStore.stop"); + m_ensure.steps(m_bookStoreSteps); // step 11 + } + } + + class Book { + BookStore m_shop; // injected, required + + public void start() { + Assert.assertNotNull(m_shop); + System.out.println("Book.start"); + m_ensure.steps(m_bookSteps); // steps 3, 5 + } + + public void stop() { + System.out.println("Book.stop"); + m_ensure.steps(m_bookSteps); // steps 8, 10 + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5426_OptionalChangeCallbackNotCalledTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5426_OptionalChangeCallbackNotCalledTest.java new file mode 100644 index 00000000000..8dbdbcc5e27 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5426_OptionalChangeCallbackNotCalledTest.java @@ -0,0 +1,133 @@ +/* + * 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.dm.itest.api; + +import java.util.Dictionary; +import java.util.Properties; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentState; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; + +/** + * Use case: + * + * - A depends on B (optional) and C (required). A has some "add/change/remove" callbacks on B. + * - A started + * - A is stopping because C is lost, so A is called in A.unbind(B), A.stop() + * - then a ComponentStateListener is called and at this point, the listener is + * changing the service properties for B. + * + * then since A.stop has been called, then A.changed(B b) should never be called. + */ +public class FELIX5426_OptionalChangeCallbackNotCalledTest extends TestBase { + + final Ensure m_ensure = new Ensure(); + + public void testCleanupDependenciesDuringComponentRemove() { + DependencyManager m = getDM(); + + A aObject= new A(); + + Component a = m.createComponent() + .setImplementation(aObject) + .add(m.createServiceDependency().setService(B.class).setRequired(false).setCallbacks("add", "change", "remove")) + .add(m.createServiceDependency().setService(C.class).setRequired(true).setCallbacks("add", "remove")); + + Component b = m.createComponent() + .setImplementation(new B()) + .setInterface(B.class.getName(), null); + + Component c = m.createComponent() + .setImplementation(new C()) + .setInterface(C.class.getName(), null); + + ComponentStateListener listenerA = (comp, state) -> { + if (state == ComponentState.STOPPED) { + Properties properties = new Properties(); + b.setServiceProperties(properties); + } + }; + + a.add(listenerA); + + m.add(a); + m.add(b); + m.add(c); + + m_ensure.waitForStep(2, 5000); // A started + + m.remove(c); // A is stopping, it should be called in A.remove(C), A.remove(B), but never in A.change(B) + m_ensure.waitForStep(5, 5000); + + Assert.assertFalse(aObject.bChanged()); + } + + class A { + boolean m_started; + boolean m_bChanged; + + void start() { + m_started = true; + } + + void stop() { + m_started = false; + m_ensure.step(4); + } + + void add(C c) { + m_ensure.step(1); + } + + void add(B b) { + m_ensure.step(2); + } + + void remove(B b) { + m_ensure.step(3); + } + + void change(B b, Dictionary properties) { + m_bChanged = true; // should never be called + throw new RuntimeException("A.change() method called"); + } + + boolean bChanged() { + return m_bChanged; + } + + void remove(C c) { + m_ensure.step(5); + } + } + + class B { + + } + + class C { + + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5428_CleanupDependenciesWhenComponentIsStopped.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5428_CleanupDependenciesWhenComponentIsStopped.java new file mode 100644 index 00000000000..a9593c03e50 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5428_CleanupDependenciesWhenComponentIsStopped.java @@ -0,0 +1,94 @@ +/* + * 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.dm.itest.api; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; + +/** + * When a component is removed using DependencyManager.remove method, then this + * test checks if the component removes from its internal datastructure the dependencies + * which are unbound while the component is being removed. + */ +public class FELIX5428_CleanupDependenciesWhenComponentIsStopped extends TestBase { + + final Ensure m_ensure = new Ensure(); + + public void testCleanupDependenciesDuringComponentRemove() { + DependencyManager m = getDM(); + + Component p1 = m.createComponent() + .setImplementation(new ProviderImpl()).setInterface(Provider.class.getName(), null); + Component p2 = m.createComponent() + .setImplementation(new ProviderImpl()).setInterface(Provider.class.getName(), null); + Consumer c = new Consumer(); + Component consumer = m.createComponent() + .setImplementation(c) + .add(m.createServiceDependency().setService(Provider.class).setCallbacks("providerAdded", "providerRemoved")); + + m.add(p1); + m.add(p2); + m.add(consumer); + + Assert.assertEquals(2, c.getProvidersCount()); + + // remove and re-add the consumer. When the consumer is removed, the internal collection holding the list of producers should be cleared, + // else, when we re-add the consumer, it would be injected with 4 providers ! + + m.remove(consumer); + m.remove(p1); + m.remove(p2); + m.add(consumer); + m.add(p1); + m.add(p2); + Assert.assertEquals(2, c.getProvidersCount()); + } + + + interface Provider {} + + class ProviderImpl implements Provider { + + } + + @SuppressWarnings("unused") + class Consumer { + final List m_providers = new ArrayList<>(); + + private void providerAdded(Provider provider) { + m_providers.add(provider); + } + + private void providerRemoved(Provider provider) { + m_providers.remove(provider); + } + + public int getProvidersCount() { + return m_providers.size(); + } + } + + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5429_AspectSwapCallbackNotCalledForOptionalDepenency.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5429_AspectSwapCallbackNotCalledForOptionalDepenency.java new file mode 100644 index 00000000000..987743a3cbe --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5429_AspectSwapCallbackNotCalledForOptionalDepenency.java @@ -0,0 +1,139 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentState; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.Ensure.Steps; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; + +/** + * This test case verifies the following (corner case) scenario: + * + * - Component A depends on Required (required=true) + * - Component A depends on BService (required=false) + * - There is an aspect for the B service. + * - A ComponentStateListener on A removes the aspect if it detects that A is stopping. + * + * Now the test verifies the following: if the "Required" service is removed, then + * A must be : + * + * 1) swapped with the original B service (since the component state listener removes the B aspect while A is stopping), + * 2) called in A.remove(original B) + * 3) and called in A.stop() + */ +public class FELIX5429_AspectSwapCallbackNotCalledForOptionalDepenency extends TestBase { + + final Ensure m_ensure = new Ensure(); + + public void testOptionalAspectRemovedWhenComponentIsStopped() { + DependencyManager m = getDM(); + + Component requiredComp = m.createComponent() + .setImplementation(new Required()) + .setInterface(Required.class.getName(), null); + + Component aComp = m.createComponent() + .setImplementation(new A()) + .add(m.createServiceDependency().setService(BService.class).setRequired(false).setCallbacks("add", null, "remove", "swap")) + .add(m.createServiceDependency().setService(Required.class).setRequired(true).setCallbacks("add", "remove")); + + Component bComp = m.createComponent() + .setImplementation(new B()) + .setInterface(BService.class.getName(), null); + + Component bAspectComp = m.createAspectService(BService.class, null, 1) + .setImplementation(new BAspect()); + + aComp.add((comp, state) -> { + if (state == ComponentState.STOPPING) { + System.out.println("removing B aspect"); + m.remove(bAspectComp); + } + }); + + m.add(requiredComp); + m.add(aComp); + m.add(bComp); + m.add(bAspectComp); + + System.out.println("removing Required"); + m.remove(requiredComp); + System.out.println("---"); + m_ensure.waitForStep(7, 5000); + } + + class Required { + } + + class A { + private BService m_b; + private final Ensure.Steps m_swapSteps = new Steps(4, 5); + + void add(Required required) { + System.out.println("A.add(" + required.getClass().getSimpleName() + ")"); + m_ensure.step(1); + } + + void remove(Required required) { + System.out.println("A.remove(" + required.getClass().getSimpleName() + ")"); + } + + void start() { + System.out.println("A.start"); + m_ensure.step(2); + } + + void stop() { + System.out.println("A.stop"); + m_ensure.step(7); + } + + void add(BService b) { + m_b = b; + System.out.println("A.add(" + b.getClass().getSimpleName() + ")"); + m_ensure.step(3); + } + + void swap(BService oldB, BService newB) { + Assert.assertEquals(m_b, oldB); + m_b = newB; + System.out.println("A.swap(" + oldB.getClass().getSimpleName() + "," + newB.getClass().getSimpleName() + ")"); + m_ensure.steps(m_swapSteps); // 4, 5 + } + + void remove(BService b) { + System.out.println("A.remove(" + b.getClass().getSimpleName() + ")"); + m_ensure.step(6); + } + } + + interface BService { + } + + class B implements BService { + } + + class BAspect implements BService { + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5471_CyclicDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5471_CyclicDependencyTest.java new file mode 100644 index 00000000000..f7fc6e07ce4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5471_CyclicDependencyTest.java @@ -0,0 +1,110 @@ +/* + * 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.dm.itest.api; + +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentState; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * Verifies if a concurrent deactivation of two components depending on each other does not produce a deadlock. + */ +public class FELIX5471_CyclicDependencyTest extends TestBase { + + volatile Ensure m_ensure; + + public void testCyclicDependency() throws InterruptedException { + DependencyManager m = getDM(); + ForkJoinPool tpool = new ForkJoinPool(2); + try { + for (int count = 0; count < 1000; count++) { + m_ensure = new Ensure(false); + + Component a = m.createComponent() + .setImplementation(new A()) + .setInterface(A.class.getName(), null) + .add(m.createServiceDependency().setService(B.class).setRequired(true).setCallbacks("add", "remove")); + + Component b = m.createComponent() + .setImplementation(new B()) + .setInterface(B.class.getName(), null) + .add(m.createServiceDependency().setService(A.class).setRequired(false).setCallbacks("add", "remove")); + + m.add(a); + m.add(b); + + ComponentStateListener l = (c, s) -> { + if (s == ComponentState.INACTIVE) { + m_ensure.step(); + } + }; + a.add(l); + b.add(l); + + m_ensure.waitForStep(4, 5000); // A started, B started + + tpool.execute(() -> m.remove(a)); + tpool.execute(() -> m.remove(b)); + + m_ensure.waitForStep(10, 5000); // A unbound from B, stopped and inactive, B unbound from A, stopped and inactive + + tpool.awaitQuiescence(5000, TimeUnit.MILLISECONDS); + } + } finally { + tpool.shutdown(); + } + } + + public class Base { + void start() { + m_ensure.step(); + } + + void stop() { + m_ensure.step(); + } + + } + + public class A extends Base { + public void add(B b) { + m_ensure.step(); + } + + public void remove(B b) { + m_ensure.step(); + } + } + + public class B extends Base { + public void add(A a) { + m_ensure.step(); + } + + public void remove(A a) { + m_ensure.step(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5471_SynchronousUnbindTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5471_SynchronousUnbindTest.java new file mode 100644 index 00000000000..38d3f6d433b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5471_SynchronousUnbindTest.java @@ -0,0 +1,154 @@ +/* + * 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.dm.itest.api; + +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.junit.Assert; + +/** + * Verifies the following scenario: + * + * - DM concurrent mode is used (a threadpool is used to activate components) + * - A depends on M + * - M depends on X + * - A, M, X are started concurrently + * - X is removed. at this point, A should be called in A.unbind(M) while M is still active + * and M should be called in M.unbind(X) while X is still active, + */ +public class FELIX5471_SynchronousUnbindTest extends ServiceRaceTest { + + volatile Ensure m_ensure; + + public FELIX5471_SynchronousUnbindTest() { + setParallel(); // Configure DM to use a threadpool + } + + public void testSynchronousUnbind() { + DependencyManager dm = getDM(); + + IntStream.range(0, 1000).forEach(i -> { + m_ensure = new Ensure(false); + + Component a = dm.createComponent() + .setImplementation(new A()) + .add(dm.createServiceDependency().setService(M.class).setRequired(true).setCallbacks("add", "remove")); + + Component m = dm.createComponent() + .setImplementation(new M()) + .setInterface(M.class.getName(), null) + .add(dm.createServiceDependency().setService(X.class).setRequired(true).setCallbacks("add", "remove")); + + Component x = dm.createComponent() + .setImplementation(new X()) + .setInterface(X.class.getName(), null); + + dm.add(a); + dm.add(m); + dm.add(x); + m_ensure.waitForStep(3, 5000); + + // make sure the threadpool is quiescent + super.m_threadPool.awaitQuiescence(5000, TimeUnit.MILLISECONDS); + + dm.remove(x); + m_ensure.waitForStep(6, 5000); + + dm.remove(a); + dm.remove(m); + + // make sure the threadpool is quiescent + super.m_threadPool.awaitQuiescence(5000, TimeUnit.MILLISECONDS); + + if ((i + 1) % 100 == 0) { + warn("Performed 100 tests (total=%d).", (i + 1)); + } + + }); + } + + public class A { + void add(M m) { + Assert.assertTrue("A.add(M): M is not started", m.isStarted()); + m_ensure.step(3); + } + + void remove(M m) { + Assert.assertTrue("A.remove(M): M is not started", m.isStarted()); + m_ensure.step(4); + } + } + + public class M { + volatile boolean m_started; + + boolean isStarted() { + return m_started; + } + + void start() { + m_started = true; + m_ensure.step(2); + } + + void stop() { + m_started = false; + } + + void add(X x) { + Assert.assertTrue("M.add(X): X is not started", x.isStarted()); + } + + void remove(X x) { + Assert.assertTrue("M.add(X): X is not started", x.isStarted()); + m_ensure.step(5); + } + + public String toString() { + return "M (" + (m_started ? "started" : "stopped") + ")"; + } + } + + public class X { + volatile boolean m_started; + + boolean isStarted() { + return m_started; + } + + void start() { + m_started = true; + m_ensure.step(1); + } + + void stop() { + m_started = false; + m_ensure.step(6); + } + + public String toString() { + return "X (" + (m_started ? "started" : "stopped") + ")"; + } + } + +} \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5630_NullObjectTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5630_NullObjectTest.java new file mode 100644 index 00000000000..01898e3b98f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5630_NullObjectTest.java @@ -0,0 +1,155 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.framework.Bundle; + +/** + * @author Felix Project Team + */ +public class FELIX5630_NullObjectTest extends TestBase { + + private final static String BSN = "org.apache.felix.metatype"; + + /** + * scenario: + * + * - A requires B,C. + * - A,B,C are started + * - B,A,C are stopped + * - A is restarted: at this point A has a bug: it is injected with a null object for B ! + */ + public void testRequiredServiceDependency() { + DependencyManager manager = getDM(); + + Component aComponent = manager.createComponent().setInterface(A.class.getName(), null).setImplementation(new A()); + aComponent.add(manager.createServiceDependency().setService(B.class).setRequired(true)); + aComponent.add(manager.createServiceDependency().setService(C.class).setRequired(true)); + + Component bComponent = manager.createComponent().setInterface(B.class.getName(), null).setImplementation(new B()); + Component cComponent = manager.createComponent().setInterface(C.class.getName(), null).setImplementation(new C()); + + manager.add(bComponent); + manager.add(aComponent); + manager.add(cComponent); + + manager.remove(aComponent); + manager.remove(bComponent); + manager.add(aComponent); //throws Could not create null object for + + manager.clear(); + } + + /** + * - A requires bundle B, which is started + * - A started + * - A stopped + * - B stopped + * - A is restarted: at this point A has a bug: it is injected with a null object for the bundle dependency over B + */ + public void testRequiredBundleDependency() { + DependencyManager m = getDM(); + + Ensure e = new Ensure(); + BundleConsumer bundleConsumerImpl = new BundleConsumer(e); + Component bundleConsumer = m.createComponent() + .setImplementation(bundleConsumerImpl) + .add(m.createBundleDependency() + .setRequired(true) + .setFilter("(Bundle-SymbolicName=" + BSN + ")") + .setStateMask( Bundle.ACTIVE)); + // add a bundle consumer with a filter + m.add(bundleConsumer); + e.waitForStep(1, 5000); + // remove bundle consumer + m.remove(bundleConsumer); + e.waitForStep(2, 5000); + // stop the metatype bundle + stopBundle(BSN); + // add the bundle consumer again, which must *not* be started + m.add(bundleConsumer); + Assert.assertFalse(bundleConsumerImpl.isStarted()); + startBundle(BSN); + } + + + private class A { + @SuppressWarnings("unused") + public void init() { + System.out.println("init"); + } + + @SuppressWarnings("unused") + public void start() { + System.out.println("start"); + } + + @SuppressWarnings("unused") + public void stop() { + System.out.println("stop"); + } + + @SuppressWarnings("unused") + public void destroy() { + System.out.println("destroy"); + } + } + + private class B { + + } + + private class C { + + } + + static class BundleConsumer { + private final Ensure m_ensure; + volatile Bundle m_bundle; + private volatile boolean m_started; + + public BundleConsumer(Ensure e) { + m_ensure = e; + } + + public boolean isStarted() { + return m_started; + } + + public void start() { + m_started = true; + if (m_bundle != null && m_bundle.getSymbolicName() != null && m_bundle.getSymbolicName().equals(BSN)) { + m_ensure.step(); + } + } + + public void stop() { + m_started = false; + if (m_bundle != null && m_bundle.getSymbolicName().equals(BSN)) { + m_ensure.step(); + } + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5636_PropagateServicePropertiesToAspectInitCallback.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5636_PropagateServicePropertiesToAspectInitCallback.java new file mode 100644 index 00000000000..a61c9aaad51 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5636_PropagateServicePropertiesToAspectInitCallback.java @@ -0,0 +1,85 @@ +/* + * 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.dm.itest.api; + +import java.util.Map; +import java.util.Properties; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; + +/** + * @author Felix Project Team + */ +public class FELIX5636_PropagateServicePropertiesToAspectInitCallback extends TestBase { + + final static Ensure m_e = new Ensure(); + + public void testAbstractClassDependency() { + DependencyManager manager = getDM(); + + // Create a service X + Properties properties = new Properties(); + properties.put("PropKey", "PropValue"); + Component aComponent = manager.createComponent().setInterface(X.class.getName(), properties) + .setImplementation(new A()); + manager.add(aComponent); + + // Create a client of X + manager.add(manager.createComponent() + .setImplementation(C.class) + .add(manager.createServiceDependency().setService(X.class).setCallbacks("bind", null).setRequired(true))); + + // Create an aspect of service X: the init method should be passed the aspect Component which must has the original service + // properties + manager.add(manager.createAspectService(X.class, null, 100).setImplementation(B.class)); + + // Check if the aspect of service X see the original service properties from its init callback. + m_e.waitForStep(1, 3000); + } + + public interface X { + + } + + public static class A implements X { + public void init(Component component) { + System.out.println("Service properties in A: " + component.getServiceProperties()); + } + } + + public static class B implements X { + public void init(Component component) { + System.out.println("Service properties in B: " + component.getServiceProperties()); + Assert.assertNotNull(component.getServiceProperties()); + Assert.assertEquals("PropValue", component.getServiceProperties().get("PropKey")); + m_e.step(1); + } + } + + public static class C { + void bind(X x, Map props) { + System.out.println("C.bind(" + x + ", " + props); + } + } + +} \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5683_GetServicePropertiesReturnsNullTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5683_GetServicePropertiesReturnsNullTest.java new file mode 100644 index 00000000000..3a231b73a91 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX5683_GetServicePropertiesReturnsNullTest.java @@ -0,0 +1,48 @@ +/* + * 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.dm.itest.api; + +import java.util.Properties; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; + +/** + * @author Felix Project Team + */ +public class FELIX5683_GetServicePropertiesReturnsNullTest extends TestBase { + + public void testServiceProperties() throws Exception { + final DependencyManager dm = getDM(); + Component provider = dm.createComponent(); + + provider.setImplementation(new ProviderImpl()) + .setInterface(Provider.class.getName(), new Properties()); + Assert.assertNotNull(provider.getServiceProperties()); + dm.clear(); + } + + public interface Provider { + } + + public class ProviderImpl implements Provider { + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX_4168AdapterWithDynamicallyAddedDependencies.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX_4168AdapterWithDynamicallyAddedDependencies.java new file mode 100644 index 00000000000..cffdcf9cba1 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FELIX_4168AdapterWithDynamicallyAddedDependencies.java @@ -0,0 +1,110 @@ +/** + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * @author Felix Project Team + */ +public class FELIX_4168AdapterWithDynamicallyAddedDependencies extends TestBase { + public void testAdapterWithExtraDependenciesAndCallbacks() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + // create a service S2, which will be added to A1 (needs to be available) + Component s2 = m.createComponent().setInterface(S2.class.getName(), null).setImplementation(new S2Impl(e)); + m.add(s2); + + // create a service adapter that adapts to services S1 and has an optional dependency on services S2 + Component sa = m.createAdapterService(S1.class, null).setImplementation(SA.class); + m.add(sa); + + // create a service S1, which triggers the creation of the first adapter instance (A1) + Component s1 = m.createComponent().setInterface(S1.class.getName(), null).setImplementation(new S1Impl()); + m.add(s1); + + // create a second service S1, which triggers the creation of the second adapter instance (A2) + Component s1b = m.createComponent().setInterface(S1.class.getName(), null).setImplementation(new S1Impl()); + m.add(s1b); + + // observe that S2 is also added to A2 + e.waitForStep(2, 5000); + + // remove S2 again + m.remove(s2); + + // make sure both adapters have their "remove" callbacks invoked + e.waitForStep(4, 5000); + m.clear(); + } + + static interface S1 { + } + + static interface S2 { + public void invoke(); + } + + static class S1Impl implements S1 { + } + + static class S2Impl implements S2 { + + private final Ensure m_e; + + public S2Impl(Ensure e) { + m_e = e; + } + + public void invoke() { + m_e.step(); + } + } + + public static class SA { + volatile S2 s2; + volatile Component component; + volatile DependencyManager manager; + + public SA() { + System.out.println("Adapter created"); + } + + public void init() { + System.out.println("Adapter init " + s2); + component.add(manager.createServiceDependency() + .setService(S2.class).setCallbacks("add", "remove").setRequired(true)); + } + + public void add(S2 s) { + System.out.println("adding " + s); + s.invoke(); + } + + public void remove(S2 s) { + System.out.println("removing " + s); + s.invoke(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FactoryComponentWithMetaTypeTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FactoryComponentWithMetaTypeTest.java new file mode 100644 index 00000000000..175069266d6 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FactoryComponentWithMetaTypeTest.java @@ -0,0 +1,175 @@ +/* + * 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.dm.itest.api; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.function.Consumer; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.FactoryComponent; +import org.apache.felix.dm.PropertyMetaData; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.framework.BundleContext; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.metatype.AttributeDefinition; +import org.osgi.service.metatype.MetaTypeInformation; +import org.osgi.service.metatype.MetaTypeService; +import org.osgi.service.metatype.ObjectClassDefinition; + +/** + * Tests an Adapter which adapts A To B interface. + * And the Adapter also depends on a Configuration Dependency with MetaType support. + * + * @author Felix Project Team + */ +public class FactoryComponentWithMetaTypeTest extends TestBase { + final static String PID = "FactoryComponentWithMetaType"; + final static String PID_HEADING = "English Dictionary"; + final static String PID_DESC = "Configuration for the EnglishDictionary Service"; + final static String WORDS_HEADING = "English words"; + final static String WORDS_DESC = "Declare here some valid English words"; + final static String WORDS_PROPERTY = "words"; + + public void testFactoryComponentWithMetaType() { + DependencyManager m = getDM(); + Ensure e = new Ensure(); + + FactoryComponent factoryComponent = m.createFactoryComponent() + .setFactoryPid(PID) + .setHeading(PID_HEADING) + .setDesc(PID_DESC) + .setImplementation(new AImpl(e)) + .setInterface(A.class.getName(), null) + .add(m.createPropertyMetaData() + .setCardinality(Integer.MAX_VALUE) + .setType(String.class) + .setHeading(WORDS_HEADING) + .setDescription(WORDS_DESC) + .setDefaults(new String[] {"hello", "world"}) + .setId(WORDS_PROPERTY)); + + m.add(factoryComponent); + + Component configurator = m.createComponent() + .setImplementation(new Configurator(e)) + .add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)) + .add(m.createServiceDependency().setService(MetaTypeService.class).setRequired(true)); + m.add(configurator); + + // Ensures that all components are started + e.waitForStep(4, 5000); + + // now stop configurator, and ensure that all components have been stopped + m.remove(configurator); + e.waitForStep(7, 5000); + m.clear(); + } + + public interface A { + } + + public static class AImpl implements A { + Dictionary m_conf; + final Ensure m_ensure; + + public AImpl(Ensure e) { + m_ensure = e; + } + + public void updated(Dictionary conf) { + if (conf != null) { + m_ensure.step(3); + m_conf = conf; + } + } + + public void start() { + Assert.assertNotNull(m_conf); + Assert.assertEquals("bar", m_conf.get("foo")); + m_ensure.step(4); + } + + public void stop() { + m_ensure.step(7); + } + } + + public class Configurator { + volatile MetaTypeService m_metaType; + volatile ConfigurationAdmin m_cm; + volatile BundleContext m_ctx; + final Ensure m_ensure; + Configuration m_conf; + + Configurator(Ensure ensure) { + m_ensure = ensure; + } + + void start() { + m_ensure.step(1); + checkMetaTypeAndConfigure(); + } + + void stop() { + m_ensure.step(5); + if (m_conf != null) { + try { + m_ensure.step(6); + m_conf.delete(); + } + catch (IOException e) { + m_ensure.throwable(e); + } + } + } + + void checkMetaTypeAndConfigure() { + MetaTypeInformation info = m_metaType.getMetaTypeInformation(m_ctx.getBundle()); + Assert.assertNotNull(info); + Assert.assertEquals(PID, info.getFactoryPids()[0]); + ObjectClassDefinition ocd = info.getObjectClassDefinition(PID, null); + Assert.assertNotNull(ocd); + Assert.assertEquals(PID_HEADING, ocd.getName()); + Assert.assertEquals(PID_DESC, ocd.getDescription()); + AttributeDefinition[] defs = ocd.getAttributeDefinitions(ObjectClassDefinition.ALL); + Assert.assertNotNull(defs); + Assert.assertEquals(1, defs.length); + Assert.assertEquals(WORDS_HEADING, defs[0].getName()); + Assert.assertEquals(WORDS_DESC, defs[0].getDescription()); + Assert.assertEquals(WORDS_PROPERTY, defs[0].getID()); + m_ensure.step(2); + + try { + m_conf = m_cm.createFactoryConfiguration(PID, null); + Hashtable props = new Hashtable<>(); + props.put("foo", "bar"); + m_conf.update(props); + } catch (Throwable t) { + m_ensure.throwable(t); + } + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FactoryConfigurationAdapterTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FactoryConfigurationAdapterTest.java new file mode 100644 index 00000000000..11421342d81 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FactoryConfigurationAdapterTest.java @@ -0,0 +1,194 @@ +/* + * 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.dm.itest.api; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes", "serial"}) +public class FactoryConfigurationAdapterTest extends TestBase +{ + private static Ensure m_ensure; + + public void testFactoryConfigurationAdapter() { + testFactoryConfigurationAdapter(Adapter.class, "updated"); + } + + public void testFactoryConfigurationAdapterWithUpdatedCallbackThatTakesComponentAsParameter() { + testFactoryConfigurationAdapter(AdapterWithUpdateMethodThatTakesComponentAsParameter.class, "updatedWithComponent"); + } + + public void testFactoryConfigurationAdapter(Class adapterImplClass, String adapterUpdate) { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + m_ensure = new Ensure(); + + // Create a Configuration instance, which will create/update/remove a configuration for factoryPid "MyFactoryPid" + FactoryConfigurationCreator configurator = new FactoryConfigurationCreator(m_ensure, "MyFactoryPid", 1, "key", "value1"); + Component s1 = m.createComponent() + .setImplementation(configurator) + .add(m.createServiceDependency() + .setService(ConfigurationAdmin.class) + .setRequired(true)); + + // Create an Adapter that will be instantiated, once the configuration is created. + // This Adapter provides an AdapterService, and depends on an AdapterExtraDependency service. +// Component s2 = m.createFactoryConfigurationAdapterService("MyFactoryPid", adapterUpdate, true /* propagate CM settings */) +// .setInterface(AdapterService.class.getName(), new Hashtable() {{ put("foo", "bar"); }}) +// .setImplementation(adapterImplClass); + + Component s2 = m.createFactoryComponent() + .setFactoryPid("MyFactoryPid").setUpdated(adapterUpdate).setPropagate(true) + .setInterface(AdapterService.class.getName(), new Hashtable() {{ put("foo", "bar"); }}) + .setImplementation(adapterImplClass); + + s2.add(m.createServiceDependency() + .setService(AdapterExtraDependency.class) + .setRequired(true) + .setAutoConfig(true)); + + // Create extra adapter service dependency upon which our adapter depends on. + Component s3 = m.createComponent() + .setImplementation(new AdapterExtraDependency()) + .setInterface(AdapterExtraDependency.class.getName(), null); + + // Create an AdapterService Consumer + Component s4 = m.createComponent() + .setImplementation(AdapterServiceConsumer.class) + .add(m.createServiceDependency() + .setService(AdapterService.class) + .setRequired(true) + .setCallbacks("bind", "change", "remove")); + + // Start services + m.add(s1); + m.add(s2); + m.add(s3); + m.add(s4); + + // Wait for step 8: the AdapterService consumer has been injected with the AdapterService, and has called the doService method. + m_ensure.waitForStep(8, 10000); + + // Modify configuration. + configurator.update("key", "value2"); + + // Wait for step 13: the AdapterService has been updated, and the AdapterService consumer has seen the change + m_ensure.waitForStep(13, 10000); + + // Remove the configuration + m.remove(s1); // The stop method will remove the configuration + m_ensure.waitForStep(16, 10000); + m.clear(); + } + + public interface AdapterService { + public void doService(); + } + + public static class AdapterExtraDependency { + } + + public static class Adapter implements AdapterService { + volatile AdapterExtraDependency m_extraDependency; // extra dependency. + private int updateCount; + + void updated(Dictionary settings) { + updateCount ++; + if (updateCount == 1) { + m_ensure.step(2); + Assert.assertEquals(true, "value1".equals(settings.get("key"))); + m_ensure.step(3); + } else if (updateCount == 2) { + m_ensure.step(9); + Assert.assertEquals(true, "value2".equals(settings.get("key"))); + m_ensure.step(10); + } else { + Assert.fail("wrong call to updated method: count=" + updateCount); + } + } + + public void doService() { + m_ensure.step(8); + } + + public void start() { + m_ensure.step(4); + Assert.assertNotNull(m_extraDependency); + m_ensure.step(5); + } + + public void stop() { + m_ensure.step(16); + } + } + + public static class AdapterWithUpdateMethodThatTakesComponentAsParameter extends Adapter { + void updatedWithComponent(Component component, Dictionary settings) { + Assert.assertNotNull(component); + Assert.assertEquals(this, component.getInstance()); + super.updated(settings); + } + } + + public static class AdapterServiceConsumer { + private AdapterService m_adapterService; + private Map m_adapterServiceProperties; + + void bind(Map serviceProperties, AdapterService adapterService) { + m_ensure.step(6); + m_adapterService = adapterService; + m_adapterServiceProperties = serviceProperties; + } + + void change(Map serviceProperties, AdapterService adapterService) { + m_ensure.step(11); + Assert.assertEquals(true, "value2".equals(m_adapterServiceProperties.get("key"))); + m_ensure.step(12); + Assert.assertEquals(true, "bar".equals(m_adapterServiceProperties.get("foo"))); + m_ensure.step(13); + } + + public void start() { + m_ensure.step(7); + Assert.assertNotNull(m_adapterService); + Assert.assertEquals(true, "value1".equals(m_adapterServiceProperties.get("key"))); + Assert.assertEquals(true, "bar".equals(m_adapterServiceProperties.get("foo"))); + m_adapterService.doService(); + } + + public void stop() { + m_ensure.step(14); + } + + void remove(AdapterService adapterService) { + m_ensure.step(15); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FactoryConfigurationCreator.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FactoryConfigurationCreator.java new file mode 100644 index 00000000000..df815cc96f8 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FactoryConfigurationCreator.java @@ -0,0 +1,80 @@ +package org.apache.felix.dm.itest.api; + +import java.io.IOException; +import java.util.Hashtable; + +import org.apache.felix.dm.itest.util.Ensure; +import org.junit.Assert; +import org.osgi.service.cm.ConfigurationAdmin; + +/* + * 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. + */ +public class FactoryConfigurationCreator { + private volatile ConfigurationAdmin m_ca; + private volatile org.osgi.service.cm.Configuration m_conf; + private final String m_key; + private final String m_value; + private final String m_factoryPid; + private final Ensure m_e; + private final int m_firstStep; + + public FactoryConfigurationCreator(Ensure e, String factoryPid, int firstStep, String key, String value) { + m_factoryPid = factoryPid; + m_key = key; + m_value = value; + m_e = e; + m_firstStep = firstStep; + } + + public void start() { + try { + m_e.step(m_firstStep); + m_conf = m_ca.createFactoryConfiguration(m_factoryPid, null); + Hashtable props = new Hashtable<>(); + props.put(m_key, m_value); + m_conf.update(props); + } + catch (IOException e) { + Assert.fail("Could not create configuration: " + e.getMessage()); + } + } + + public void update(String key, String val) { + Hashtable props = new Hashtable<>(); + props.put(key, val); + try { + m_conf.update(props); + } + catch (IOException e) { + Assert.fail("Could not update configuration: " + e.getMessage()); + } + } + + public void stop() { + try + { + m_conf.delete(); + } + catch (IOException e) + { + Assert.fail("Could not remove configuration: " + e.toString()); + } + } +} + diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FactoryInjectedWithConfigurationBeforeTheCreateMethod.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FactoryInjectedWithConfigurationBeforeTheCreateMethod.java new file mode 100644 index 00000000000..f0991566a98 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/FactoryInjectedWithConfigurationBeforeTheCreateMethod.java @@ -0,0 +1,109 @@ +/* + * 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.dm.itest.api; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Properties; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * Use case: one component is instantiated using another factory object, and the + * factory object needs the configuration before the factory.create method is called. + * + * @author Felix Project Team + */ +public class FactoryInjectedWithConfigurationBeforeTheCreateMethod extends TestBase { + Ensure m_e; + + public void testServiceInjection() { + DependencyManager m = getDM(); + m_e = new Ensure(); + + // Create the component that creates a configuration. + Component configurator = m.createComponent().setImplementation(new Configurator("foobar")).add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)); + + // Create the object that has to be injected with the configuration before its create method is called. + MyFactory factory = new MyFactory(); + + // Create the Component for the MyComponent class that is created using the factory above. + Component myComponent = m.createComponent().setFactory(factory, "create").add(m.createConfigurationDependency().setPid("foobar").setCallback(factory, "updated")); + + // provide the configuration + m.add(configurator); + + m.add(myComponent); + m_e.waitForStep(4, 10000); + m.remove(myComponent); + m.remove(configurator); + } + + class Configurator { + private volatile ConfigurationAdmin m_ca; + Configuration m_conf; + final String m_pid; + + public Configurator(String pid) { + m_pid = pid; + } + + public void init() { + try { + Assert.assertNotNull(m_ca); + m_e.step(1); + m_conf = m_ca.getConfiguration(m_pid, null); + Properties props = new Properties(); + props.setProperty("testkey", "testvalue"); + m_conf.update(toR6Dictionary(props)); + } + catch (IOException e) { + Assert.fail("Could not create configuration: " + e.getMessage()); + } + } + + public void destroy() throws IOException { + m_conf.delete(); + } + } + + public class MyFactory { + public void updated(Dictionary conf) { + Assert.assertNotNull("configuration is null", conf); + m_e.step(2); + } + + public MyComponent create() { + m_e.step(3); + return new MyComponent(); + } + } + + public class MyComponent { + void start() { + m_e.step(4); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/InstanceBoundDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/InstanceBoundDependencyTest.java new file mode 100644 index 00000000000..988588d8de2 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/InstanceBoundDependencyTest.java @@ -0,0 +1,159 @@ +/* + * 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.dm.itest.api; + +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + +/** + * This test does some injection tests on components being in INSTANTIATED_AND_WAITING_FOR_REQUIRED state. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class InstanceBoundDependencyTest extends TestBase { + Ensure m_e; + + public void testServiceInjection() { + DependencyManager m = getDM(); + m_e = new Ensure(); + + // Create a "C" component: it depends on some S1 services, and on some S2 instance-bound services (declared from C.init() method) + C cimpl = new C(); + Component c = m.createComponent().setImplementation(cimpl); + c.add(m.createServiceDependency().setService(S1.class).setRequired(true).setCallbacks("addS1", "changeS1", "removeS1").setAutoConfig(true)); + m.add(c); + + // Add S1 (s1_1): C.add(S1 s1) is called, then init() is called where a dependency is declared on S2 + Hashtable s1_1_props = new Hashtable(); + s1_1_props.put("name", "s1_1"); + s1_1_props.put(Constants.SERVICE_RANKING, new Integer(10)); + S1Impl s1_1_impl = new S1Impl(); + Component s1_1 = m.createComponent().setImplementation(s1_1_impl).setInterface(S1.class.getName(), s1_1_props); + m.add(s1_1); + m_e.waitForStep(1, 5000); // wait until C.init called + ServiceReference ref = cimpl.getS1("s1_1"); + Assert.assertNotNull(ref); + Assert.assertNotNull(cimpl.getS1()); + Assert.assertEquals(s1_1_impl, cimpl.getS1()); + + // At this point, MyComponent is in INSTANTIATED_AND_WAITING_FOR_REQUIRED state. + // add now add another higher ranked S1 (s1_2) instance. C.add(s1_2) method should be called (the S1 dependency + // is not instance bound), and m_s1 autoconfig field should be updated. + Hashtable s1_2_props = new Hashtable(); + s1_2_props.put(Constants.SERVICE_RANKING, new Integer(20)); + s1_2_props.put("name", "s1_2"); + S1Impl s1_2_impl = new S1Impl(); + Component s1_2 = m.createComponent().setImplementation(s1_2_impl).setInterface(S1.class.getName(), s1_2_props); + m.add(s1_2); + ref = cimpl.getS1("s1_2"); + Assert.assertNotNull(ref); + Assert.assertNotNull(cimpl.getS1()); + Assert.assertEquals(s1_2_impl, cimpl.getS1()); // must return s1_2 with ranking = 20 + + // Now, change the s1_1 service properties: C.changed(s1_1) should be called, and C.m_s1AutoConfig should be updated + s1_1_props.put(Constants.SERVICE_RANKING, new Integer(30)); + s1_1.setServiceProperties(s1_1_props); + ref = cimpl.getS1("s1_1"); + Assert.assertNotNull(ref); + Assert.assertEquals(new Integer(30), ref.getProperty(Constants.SERVICE_RANKING)); + Assert.assertNotNull(cimpl.getS1()); + Assert.assertEquals(s1_1_impl, cimpl.getS1()); + + // Now, remove the s1_1: C.remove(s1_1) should be called, and C.m_s1AutoConfig should be updated + m.remove(s1_1); + ref = cimpl.getS1("s1_1"); + Assert.assertNull(cimpl.getS1("s1_1")); + Assert.assertNotNull(cimpl.getS1()); + Assert.assertEquals(s1_2_impl, cimpl.getS1()); + m.clear(); + } + + // C component depends on some S1 required services + public interface S1 { + } + + public class S1Impl implements S1 { + } + + public interface S2 { + } + + public class S2Impl implements S2 { + } + + // Our "C" component: it depends on S1 (required) and S2 (required/instance bound) + class C { + final Map m_s1Map = new HashMap(); + final Map m_s2Map = new HashMap(); + volatile S1 m_s1; // auto configured + + S1 getS1() { + return m_s1; + } + + void addS1(ServiceReference s1) { + m_s1Map.put((String) s1.getProperty("name"), s1); + } + + void changeS1(ServiceReference s1) { + m_s1Map.put((String) s1.getProperty("name"), s1); + } + + void removeS1(ServiceReference s1) { + m_s1Map.remove((String) s1.getProperty("name")); + } + + void addS2(ServiceReference s2) { + m_s2Map.put((String) s2.getProperty("name"), s2); + } + + void changeS2(ServiceReference s2) { + m_s2Map.put((String) s2.getProperty("name"), s2); + } + + void removeS2(ServiceReference s2) { + m_s2Map.remove((String) s2.getProperty("name")); + } + + ServiceReference getS1(String name) { + return m_s1Map.get(name); + } + + ServiceReference getS2(String name) { + return m_s2Map.get(name); + } + + void init(Component c) { + DependencyManager m = c.getDependencyManager(); + c.add(m.createServiceDependency().setService(S2.class).setRequired(true).setCallbacks("addS2", "changeS2", "removeS2")); + m_e.step(1); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ModifiedBundleDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ModifiedBundleDependencyTest.java new file mode 100644 index 00000000000..76d681c29ab --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ModifiedBundleDependencyTest.java @@ -0,0 +1,140 @@ +/* + * 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.dm.itest.api; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; + +/** + * Test for FELIX-4334 issue. + * + * Two components: A, B + * + * - A provided. + * - B has a bundle dependency on the dependency manager shell bundle, which is currently stopped. + * - B has an instance bound dependency on A. + * - Now unregister A. + * - As a result of that, B becomes unavailable and is unbound from A. But B is not destroyed, because A dependency + * is "instance bound". So B is still bound to the bundle dependency. + * - Now, someone starts the dependency manager shell bundle: B then shall be called in its "changed" callback. + * + * @author Felix Project Team + */ +public class ModifiedBundleDependencyTest extends TestBase { + public static interface A { + } + + static class AImpl implements A { + } + + public static interface B { + } + + static class BImpl implements B { + final Ensure m_e; + + BImpl(Ensure e) { + m_e = e; + } + + public void add(Bundle dmTest) { + m_e.step(1); + } + + void init(Component c) { + m_e.step(2); + DependencyManager dm = c.getDependencyManager(); + c.add(dm.createServiceDependency().setService(A.class).setRequired(true).setCallbacks("add", "remove")); + } + + public void add(A a) { + m_e.step(3); + } + + public void start() { + m_e.step(4); + } + + public void stop() { + m_e.step(5); + } + + public void remove(A a) { + m_e.step(6); + } + + public void change(Bundle dmTest) { // called two times: one for STARTING, one for STARTED + m_e.step(); + } + + public void destroy() { + m_e.step(9); + } + + public void remove(Bundle dmTest) { + m_e.step(10); + } + } + + public void testAdapterWithChangedInstanceBoundDependency() { + DependencyManager m = getDM(); + Ensure e = new Ensure(); + + Component a = m.createComponent() + .setImplementation(new AImpl()) + .setInterface(A.class.getName(), null); + + Component b = m.createComponent() + .setInterface(B.class.getName(), null) + .setImplementation(new BImpl(e)) + .add(m.createBundleDependency() + .setFilter("(Bundle-SymbolicName=org.apache.felix.metatype)") + .setStateMask(Bundle.INSTALLED|Bundle.ACTIVE|Bundle.RESOLVED|Bundle.STARTING) + .setRequired(true) + .setCallbacks("add", "change", "remove")); + + Bundle dmtest = getBundle("org.apache.felix.metatype"); + try { + dmtest.stop(); + } catch (BundleException e1) { + Assert.fail("could not find metatype bundle"); + } + + m.add(a); + m.add(b); + + e.waitForStep(4, 5000); + m.remove(a); // B will loose A and will enter into "waiting for required (instantiated)" state. + System.out.println("Starting metatype bundle ..."); + try { + dmtest.start(); + } catch (BundleException e1) { + Assert.fail("could not start metatype bundle"); + } + e.waitForStep(7, 5000); + m.remove(b); + e.waitForStep(10, 5000); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/MultipleExtraDependenciesTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/MultipleExtraDependenciesTest.java new file mode 100644 index 00000000000..32f78556624 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/MultipleExtraDependenciesTest.java @@ -0,0 +1,222 @@ +/* + * 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.dm.itest.api; + +import java.util.Hashtable; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + + +/** + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class MultipleExtraDependenciesTest extends TestBase { + /** + * Check that list of extra dependencies (defined from init method) are handled properly. + * The extra dependencies are added using a List object (Component.add(List)). + * A component c1 will define two extra dependencies over *available* c4/c5 services. + */ + public void testWithTwoAvailableExtraDependency() { + DependencyManager m = getDM(); + // Helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + Component c1 = m.createComponent() + .setInterface(Service1.class.getName(), null) + .setImplementation(new MyComponent1(e)) + .add(m.createServiceDependency() + .setService(Service2.class) + .setRequired(true) + .setAutoConfig("m_service2")); + + Component c2 = m.createComponent() + .setImplementation(new MyComponent2(e)) + .add(m.createServiceDependency() + .setService(Service1.class) + .setRequired(false) + .setAutoConfig(false) + .setCallbacks("added", null, null)); + + Component c3 = m.createComponent() + .setInterface(Service2.class.getName(), null) + .setImplementation(Service2Impl.class); + + Hashtable h = new Hashtable(); + h.put("type", "xx"); + Component c4 = m.createComponent() + .setInterface(Service3.class.getName(), h) + .setImplementation(Service3Impl1.class); + + h = new Hashtable(); + h.put("type", "yy"); + Component c5 = m.createComponent() + .setInterface(Service3.class.getName(), h) + .setImplementation(Service3Impl2.class); + + + System.out.println("\n+++ Adding c2 / MyComponent2"); + m.add(c2); + System.out.println("\n+++ Adding c3 / Service2"); + m.add(c3); + System.out.println("\n+++ Adding c4 / Service3(xx)"); + m.add(c4); + System.out.println("\n+++ Adding c5 / Service3(yy)"); + m.add(c5); + System.out.println("\n+++ Adding c1 / MyComponent1"); + // c1 have declared two extra dependency on Service3 (xx/yy). + // both extra dependencies are available, so the c1 component should be started immediately. + m.add(c1); + e.waitForStep(3, 3000); + m.clear(); + } + + /** + * Check that list of extra dependencies (defined from init method) are handled properly. + * The extra dependencies are added using a List object (Component.add(List)). + * A component c1 will define two extra dependencies over c4/c5. At the point c1.init() + * is adding the two extra dependencies from its init method, c4 is available, but not c5. + * So, c1 is not yet activated. + * Then c5 is added, and it triggers the c1 activation ... + */ + public void testWithOneAvailableExtraDependency() { + DependencyManager m = getDM(); + // Helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + Component c1 = m.createComponent() + .setInterface(Service1.class.getName(), null) + .setImplementation(new MyComponent1(e)) + .add(m.createServiceDependency() + .setService(Service2.class) + .setRequired(true) + .setAutoConfig("m_service2")); + + Component c2 = m.createComponent() + .setImplementation(new MyComponent2(e)) + .add(m.createServiceDependency() + .setService(Service1.class) + .setRequired(false) + .setAutoConfig(false) + .setCallbacks("added", null, null)); + + Component c3 = m.createComponent() + .setInterface(Service2.class.getName(), null) + .setImplementation(Service2Impl.class); + + Hashtable h = new Hashtable(); + h.put("type", "xx"); + Component c4 = m.createComponent() + .setInterface(Service3.class.getName(), h) + .setImplementation(Service3Impl1.class); + + h = new Hashtable(); + h.put("type", "yy"); + Component c5 = m.createComponent() + .setInterface(Service3.class.getName(), h) + .setImplementation(Service3Impl2.class); + + + System.out.println("\n+++ Adding c2 / MyComponent2"); + m.add(c2); + System.out.println("\n+++ Adding c3 / Service2"); + m.add(c3); + System.out.println("\n+++ Adding c4 / Service3(xx)"); + m.add(c4); + System.out.println("\n+++ Adding c1 / MyComponent1"); + m.add(c1); + + // c1 have declared two extra dependency on Service3 (xx/yy). + // So, because we have not yet added c5 (yy), c1 should not be started currently. + // But, now, we'll add c5 (Service3/yy) and c1 should then be started ... + System.out.println("\n+++ Adding c5 / Service3(yy)"); + m.add(c5); + e.waitForStep(3, 3000); + m.clear(); + } + + + public interface Service1 {} + public interface Service2 {} + public interface Service3 {} + + public static class Service2Impl implements Service2 {} + public static class Service3Impl1 implements Service3 {} + public static class Service3Impl2 implements Service3 {} + + public static class MyComponent1 implements Service1 { + Service2 m_service2; + Service3 m_service3_xx; + Service3 m_service3_yy; + Ensure m_ensure; + + public MyComponent1(Ensure e) { + m_ensure = e; + } + + void init(Component c) { + m_ensure.step(1); + DependencyManager dm = c.getDependencyManager(); + // Service3/xx currently available + Dependency d1 = + dm.createServiceDependency() + .setService(Service3.class, "(type=xx)") + .setRequired(true) + .setAutoConfig("m_service3_xx"); + + // Service3/yy not yet available + Dependency d2 = + dm.createServiceDependency() + .setService(Service3.class, "(type=yy)") + .setRequired(true) + .setAutoConfig("m_service3_yy"); + c.add(d1, d2); + } + + void start() { + System.out.println("MyComponent1.start"); + Assert.assertNotNull(m_service2); + Assert.assertNotNull(m_service3_xx); + Assert.assertNotNull(m_service3_yy); + m_ensure.step(2); + } + } + + public static class MyComponent2 { + Ensure m_ensure; + + public MyComponent2(Ensure e) { + m_ensure = e; + } + + void added(Service1 s1) { + System.out.println("MyComponent2.bind(" + s1 + ")"); + Assert.assertNotNull(s1); + m_ensure.step(3); + } + + void start() { + System.out.println("MyComponent2.start"); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/MultipleExtraDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/MultipleExtraDependencyTest.java new file mode 100644 index 00000000000..9816b0f042b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/MultipleExtraDependencyTest.java @@ -0,0 +1,226 @@ +/* + * 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.dm.itest.api; + +import java.util.Hashtable; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * Test which validates multi-dependencies combination. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes", "serial"}) +public class MultipleExtraDependencyTest extends TestBase { + public void testMultipleExtraDependencies() + { + DependencyManager m = getDM(); + Ensure e = new Ensure(); + + Component sp2 = m.createComponent() + .setImplementation(ServiceProvider2.class).setInterface(ServiceProvider2.class.getName(), null) + .add(m.createServiceDependency() + .setService(Runnable.class, "(foo=bar)") + .setRequired(false) + .setAutoConfig("m_runnable")) + .add(m.createServiceDependency() + .setService(Sequencer.class) + .setRequired(true) + .setCallbacks("bind", null)) + .setCallbacks(null, "start", "stop", null) + .setComposition("getComposition"); + + Component sp = m.createComponent() + .setImplementation(ServiceProvider.class) + .setInterface(ServiceInterface.class.getName(), + new Hashtable() {{ put("foo", "bar"); }}) + .add(m.createServiceDependency() + .setService(Sequencer.class) + .setRequired(true) + .setAutoConfig("m_sequencer")) + .add(m.createServiceDependency() + .setService(ServiceProvider2.class) + .setRequired(true) + .setCallbacks("bind", "unbind")) + .setCallbacks(null, "start", "stop", null); + + Component sc = m.createComponent() + .setImplementation(ServiceConsumer.class) + .add(m.createServiceDependency() + .setService(Sequencer.class) + .setRequired(true) + .setAutoConfig("m_sequencer")) + .add(m.createServiceDependency() + .setService(ServiceInterface.class, "(foo=bar)") + .setRequired(true) + .setAutoConfig("m_service")) + .setCallbacks(null, "start", "stop", null); + + Component sequencer = + m.createComponent().setImplementation(new SequencerImpl(e)) + .setInterface(Sequencer.class.getName(), null); + m.add(sp2); + m.add(sp); + m.add(sc); + m.add(sequencer); + + // Check if ServiceProvider component have been initialized orderly + e.waitForStep(7, 5000); + + // Stop the test.annotation bundle + m.remove(sequencer); + m.remove(sp); + m.remove(sp2); + m.remove(sc); + + // And check if ServiceProvider2 has been deactivated orderly + e.waitForStep(11, 5000); + } + + public interface Sequencer + { + void step(); + void step(int step); + void waitForStep(int step, int timeout); + } + + public static class SequencerImpl implements Sequencer { + Ensure m_ensure; + + public SequencerImpl(Ensure e) + { + m_ensure = e; + } + + public void step() + { + m_ensure.step(); + } + + public void step(int step) + { + m_ensure.step(step); + } + + public void waitForStep(int step, int timeout) + { + m_ensure.waitForStep(step, timeout); + } + } + + public interface ServiceInterface + { + public void doService(); + } + + public static class ServiceConsumer + { + volatile Sequencer m_sequencer; + volatile ServiceInterface m_service; + + void start() + { + m_sequencer.step(6); + m_service.doService(); + } + + void stop() + { + m_sequencer.step(8); + } + } + + public static class ServiceProvider implements ServiceInterface + { + Sequencer m_sequencer; + ServiceProvider2 m_serviceProvider2; + + void bind(ServiceProvider2 provider2) + { + m_serviceProvider2 = provider2; + } + + void start() + { + m_serviceProvider2.step(4); + m_sequencer.step(5); + } + + void stop() + { + m_sequencer.step(9); + } + + void unbind(ServiceProvider2 provider2) + { + m_sequencer.step(10); + } + + public void doService() + { + m_sequencer.step(7); + } + } + + public static class ServiceProvider2 + { + Composite m_composite = new Composite(); + Sequencer m_sequencer; + Runnable m_runnable; + + void bind(Sequencer seq) + { + m_sequencer = seq; + m_sequencer.step(1); + } + + void start() + { + m_sequencer.step(3); + m_runnable.run(); // NullObject + } + + public void step(int step) // called by ServiceProvider.start() method + { + m_sequencer.step(step); + } + + void stop() + { + m_sequencer.step(11); + } + + Object[] getComposition() + { + return new Object[] { this, m_composite }; + } + } + + public static class Composite + { + void bind(Sequencer seq) + { + seq.step(2); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/MultipleExtraDependencyTest2.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/MultipleExtraDependencyTest2.java new file mode 100644 index 00000000000..9889f3adbee --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/MultipleExtraDependencyTest2.java @@ -0,0 +1,257 @@ +/* + * 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.dm.itest.api; + +import java.util.Hashtable; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ServiceDependency; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * Tests for extra dependencies which are declared from service's init method. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes", "serial"}) +public class MultipleExtraDependencyTest2 extends TestBase { + public void testMultipleExtraDependencies() + { + DependencyManager m = getDM(); + Ensure e = new Ensure(); + + Component sp2 = m.createComponent() + .setImplementation(ServiceProvider2.class).setInterface(ServiceProvider2.class.getName(), null) + .setCallbacks("init", "start", "stop", null) + .setComposition("getComposition"); + + Component sp = m.createComponent() + .setImplementation(ServiceProvider.class) + .setInterface(ServiceInterface.class.getName(), new Hashtable() {{ put("foo", "bar"); }}) + .setCallbacks("init", "start", "stop", null); + + Component sc = m.createComponent() + .setImplementation(ServiceConsumer.class) + .setCallbacks("init", "start", "stop", null); + + // Provide the Sequencer service to the MultipleAnnotationsTest class. + Component sequencer = + m.createComponent().setImplementation(new SequencerImpl(e)) + .setInterface(Sequencer.class.getName(), null); + m.add(sp2); + m.add(sp); + m.add(sc); + m.add(sequencer); + + // Check if the test.annotation components have been initialized orderly + e.waitForStep(7, 10000); + + // Stop the test.annotation bundle + m.remove(sequencer); + m.remove(sp); + m.remove(sp2); + m.remove(sc); + +// m.remove(sp2); +// m.remove(sc); +// m.remove(sp); +// m.remove(sequencer); + + + + // And check if the test.annotation bundle has been deactivated orderly + e.waitForStep(11, 10000); + m.clear(); + } + + public interface Sequencer + { + void step(); + void step(int step); + void waitForStep(int step, int timeout); + } + + public static class SequencerImpl implements Sequencer { + final Ensure m_ensure; + + public SequencerImpl(Ensure e) + { + m_ensure = e; + } + + public void step() + { + m_ensure.step(); + } + + public void step(int step) + { + m_ensure.step(step); + } + + public void waitForStep(int step, int timeout) + { + m_ensure.waitForStep(step, timeout); + } + } + + public interface ServiceInterface + { + public void doService(); + } + + public static class ServiceConsumer { + volatile Sequencer m_sequencer; + volatile ServiceInterface m_service; + volatile Dependency m_d1, m_d2; + + public void init(Component s) { + DependencyManager m = s.getDependencyManager(); + m_d1 = m.createServiceDependency() + .setService(Sequencer.class) + .setRequired(true) + .setAutoConfig("m_sequencer"); + m_d2 = m.createServiceDependency() + .setService(ServiceInterface.class, "(foo=bar)") + .setRequired(true) + .setAutoConfig("m_service"); + s.add(m_d1, m_d2); + } + + void start() { + m_sequencer.step(6); + m_service.doService(); + } + + void stop() { + m_sequencer.step(8); + } + } + + public static class ServiceProvider implements ServiceInterface + { + volatile Sequencer m_sequencer; + volatile ServiceProvider2 m_serviceProvider2; + volatile ServiceDependency m_d1, m_d2; + + public void init(Component c) + { + DependencyManager m = c.getDependencyManager(); + m_d1 = m.createServiceDependency() + .setService(Sequencer.class) + .setRequired(true) + .setAutoConfig("m_sequencer"); + m_d2 = m.createServiceDependency() + .setService(ServiceProvider2.class) + .setRequired(true) + .setCallbacks("bind", "unbind"); + c.add(m_d1, m_d2); + } + + void bind(ServiceProvider2 provider2) + { + m_serviceProvider2 = provider2; + } + + void start() + { + m_serviceProvider2.step(4); + m_sequencer.step(5); + } + + void stop() + { + m_sequencer.step(9); + } + + void unbind(ServiceProvider2 provider2) + { + m_sequencer.step(10); + } + + public void doService() + { + m_sequencer.step(7); + } + } + + public static class ServiceProvider2 + { + final Composite m_composite = new Composite(); + volatile Sequencer m_sequencer; + volatile Runnable m_runnable; + volatile ServiceDependency m_d1, m_d2; + + public void init(Component c) + { + System.out.println("ServiceProvider2.init"); + DependencyManager m = c.getDependencyManager(); + + m_d1 = m.createServiceDependency() + .setService(Runnable.class, "(foo=bar)") + .setRequired(false) + .setAutoConfig("m_runnable"); + m_d2 = m.createServiceDependency() + .setService(Sequencer.class) + .setRequired(true) + .setCallbacks("bind", null); + c.add(m_d1, m_d2); + } + + void bind(Sequencer seq) + { + System.out.println("ServiceProvider2.bind(" + seq + ")"); + m_sequencer = seq; + m_sequencer.step(1); + } + + void start() + { + System.out.println("ServiceProvider2.start: m_runnable=" + m_runnable + ", m_sequencer = " + m_sequencer); + m_sequencer.step(3); + m_runnable.run(); // NullObject + } + + public void step(int step) // called by ServiceProvider.start() method + { + m_sequencer.step(step); + } + + void stop() + { + m_sequencer.step(11); + } + + Object[] getComposition() + { + return new Object[] { this, m_composite }; + } + } + + public static class Composite + { + void bind(Sequencer seq) + { + seq.step(2); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/MultipleServiceDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/MultipleServiceDependencyTest.java new file mode 100644 index 00000000000..36874eef6ac --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/MultipleServiceDependencyTest.java @@ -0,0 +1,150 @@ +/* + * 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.dm.itest.api; + +import java.util.Hashtable; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.Constants; + +/** + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes", "serial"}) +public class MultipleServiceDependencyTest extends TestBase { + public void testMultipleServiceRegistrationAndConsumption() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component provider = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), null); + Component providerWithHighRank = m.createComponent().setImplementation(new ServiceProvider2(e)).setInterface(ServiceInterface.class.getName(), new Hashtable() {{ put(Constants.SERVICE_RANKING, Integer.valueOf(5)); }}); + Component consumer = m.createComponent().setImplementation(new ServiceConsumer(e)).add(m.createServiceDependency().setService(ServiceInterface.class).setRequired(true)); + m.add(provider); + m.add(providerWithHighRank); + m.add(consumer); + e.waitForStep(3, 5000); + m.remove(providerWithHighRank); + e.step(4); + e.waitForStep(5, 5000); + m.remove(provider); + m.remove(consumer); + e.waitForStep(6, 5000); + } + + public void testReplacementAutoConfig() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component provider = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), null); + Component provider2 = m.createComponent().setImplementation(new ServiceProvider2(e)).setInterface(ServiceInterface.class.getName(), null); + Component consumer = m.createComponent().setImplementation(new ServiceConsumer(e)).add(m.createServiceDependency().setService(ServiceInterface.class).setRequired(true)); + m.add(provider2); + m.add(consumer); + e.waitForStep(3, 5000); + m.add(provider); + m.remove(provider2); + e.step(4); + e.waitForStep(5, 5000); + m.remove(provider); + m.remove(consumer); + e.waitForStep(6, 5000); + } + + public void testReplacementCallbacks() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component provider = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), null); + Component provider2 = m.createComponent().setImplementation(new ServiceProvider2(e)).setInterface(ServiceInterface.class.getName(), null); + Component consumer = m.createComponent().setImplementation(new ServiceConsumer(e)) + .add(m.createServiceDependency() + .setService(ServiceInterface.class) + .setRequired(true) + .setCallbacks("add", "remove")); + m.add(provider2); + m.add(consumer); + e.waitForStep(3, 15000); + m.add(provider); + m.remove(provider2); + e.step(4); + e.waitForStep(5, 15000); + m.remove(provider); + m.remove(consumer); + e.waitForStep(6, 15000); + } + + static interface ServiceInterface { + public void invoke(); + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke() { + m_ensure.step(5); + } + } + + static class ServiceProvider2 implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider2(Ensure e) { + m_ensure = e; + } + public void invoke() { + m_ensure.step(2); + } + } + + static class ServiceConsumer implements Runnable { + private volatile ServiceInterface m_service; + private final Ensure m_ensure; + + @SuppressWarnings("unused") + private void add(ServiceInterface service) { m_service = service; } + + @SuppressWarnings("unused") + private void remove(ServiceInterface service) { if (m_service == service) { m_service = null; }} + public ServiceConsumer(Ensure e) { m_ensure = e; } + + public void start() { + Thread t = new Thread(this); + t.start(); + } + + public void run() { + m_ensure.step(1); + m_service.invoke(); + m_ensure.step(3); + m_ensure.waitForStep(4, 15000); + m_service.invoke(); + } + + public void stop() { + m_ensure.step(6); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/OptionalConfigurationDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/OptionalConfigurationDependencyTest.java new file mode 100644 index 00000000000..9be16c02553 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/OptionalConfigurationDependencyTest.java @@ -0,0 +1,110 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * Integration test for Optional ConfigurationDependency. + * The following behaviors are tested: + * + * 1) an OptionalConfigurationConsumer component defines an optional configuration dependency. + * 2) then the component is activated: since there is no currently an available configuration, the component is called in its + * updated callback with a type-safe config, and the component can then be initialized using the default methods from the type-safe config interface. + * 3) then the configuration is registered: at this point the OptionalConfigurationConsumer is called again in its updated callback , but + * with the real just registered configuration. So, the component can be updated with the newly just registered config. + * 4) then the configuration is unregistered: at this point, the OptionalConfigurationConsumer is called again in updated callback, like in step 2). + * So, the component is re-initilized using the default methods from the type-safe config. + * + * @author Felix Project Team + */ +public class OptionalConfigurationDependencyTest extends TestBase { + final static String PID = "ConfigurationDependencyTest.pid"; + + public void testOptionalConfigurationConsumer() { + DependencyManager m = getDM(); + Ensure e = new Ensure(); + Component c1 = m.createComponent().setImplementation(new ConfigurationCreator(e, PID, 3)) + .add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)); + Component c2 = m.createComponent().setImplementation(new OptionalConfigurationConsumer(e)) + .add(m.createConfigurationDependency().setCallback("updated", MyConfig.class).setPid(PID).setPropagate(true).setRequired(false)); + m.add(c2); + e.waitForStep(1, 5000); // c2 called in updated with testKey="default" config + e.waitForStep(2, 5000); // c2 called in start() + m.add(c1); // c1 registers a "testKey=testvalue" configuration, using config admin. + e.waitForStep(3, 5000); // c1 created the conf. + e.waitForStep(4, 5000); // c2 called in updated with testKey="testvalue" + m.remove(c1); // remove configuration. + e.waitForStep(5, 5000); // c2 called in updated with default "testkey=default" property (using the default method from the config interface. + m.remove(c2); // stop the OptionalConfigurationConsumer component + e.waitForStep(6, 5000); // c2 stopped + } + + + public static interface MyConfig { + public default String getTestkey() { return "default"; } + } + + static class OptionalConfigurationConsumer { + protected final Ensure m_ensure; + protected volatile int m_updateCount; + + public OptionalConfigurationConsumer(Ensure e) { + m_ensure = e; + } + + public void updated(MyConfig cnf) { // optional configuration, called after start(), like any other optional dependency callbacks. + if (cnf != null) { + m_updateCount ++; + if (m_updateCount == 1) { + if (!"default".equals(cnf.getTestkey())) { + Assert.fail("Could not find the configured property."); + } + m_ensure.step(1); + } else if (m_updateCount == 2) { + if (!"testvalue".equals(cnf.getTestkey())) { + Assert.fail("Could not find the configured property."); + } + m_ensure.step(4); + } else if (m_updateCount == 3) { + if (!"default".equals(cnf.getTestkey())) { + Assert.fail("Could not find the configured property."); + } + m_ensure.step(5); + } + } else { + // configuration destroyed: should never happen + m_ensure.throwable(new Exception("lost configuration")); + } + } + + public void start() { + m_ensure.step(2); + } + + public void stop() { + m_ensure.step(6); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/RemovedDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/RemovedDependencyTest.java new file mode 100644 index 00000000000..5d1af039b07 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/RemovedDependencyTest.java @@ -0,0 +1,170 @@ +/* + * 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.dm.itest.api; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.Properties; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; + +/** + * One consumer, Three providers. The Consumer has two required dependency on provider1, provider2, and one + * instance-bound required dependency on provider3. + * When the three providers are there, the consumer is started. + * + * This test asserts the following correct behaviors: + * - when we remove the dependency on provider2, then the consumer is not stopped. + * - when we remove the (instance-bound) dependency on provider3, then the consumer os not stopped. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes", "unused"}) +public class RemovedDependencyTest extends TestBase { + public void testRemoveDependencyAndConsumerMustRemainStarted() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // Create two providers + Hashtable props = new Hashtable(); + props.put("name", "provider1"); + Component sp = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), props); + props = new Properties(); + props.put("name", "provider2"); + Component sp2 = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), props); + props = new Properties(); + props.put("name", "provider3"); + Component sp3 = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), props); + + // Create the consumer, and start it + Dependency d1 = m.createServiceDependency().setService(ServiceInterface.class, "(name=provider1)").setRequired(true).setCallbacks("add", "remove"); + Dependency d2 = m.createServiceDependency().setService(ServiceInterface.class, "(name=provider2)").setRequired(true).setCallbacks("add", "remove"); + Dependency d3 = m.createServiceDependency().setService(ServiceInterface.class, "(name=provider3)").setRequired(true).setCallbacks("add", "remove"); + + ServiceConsumer consumer = new ServiceConsumer(e, d3); + Component sc = m.createComponent().setImplementation(consumer).add(d1, d2); + + // Add the first two providers and the consumer + m.add(sp); + m.add(sp2); + m.add(sp3); + m.add(sc); + + // Check if consumer has been bound to the three providers + e.waitForStep(3, 5000); + Assert.assertEquals(3, consumer.getProvidersCount()); + Assert.assertNotNull(consumer.getProvider("provider1")); + Assert.assertNotNull(consumer.getProvider("provider2")); + Assert.assertNotNull(consumer.getProvider("provider3")); + + // Now remove the provider2, and check if the consumer is still alive + sc.remove(d2); + Assert.assertFalse(consumer.isStopped()); + Assert.assertEquals(2, consumer.getProvidersCount()); + Assert.assertNotNull(consumer.getProvider("provider1")); + Assert.assertNull(consumer.getProvider("provider2")); + Assert.assertNotNull(consumer.getProvider("provider3")); + + // Now remove the provider3 (the consumer has an instance bound dependency on it), and check if the consumer is still alive + sc.remove(d3); + Assert.assertFalse(consumer.isStopped()); + Assert.assertEquals(1, consumer.getProvidersCount()); + Assert.assertNotNull(consumer.getProvider("provider1")); + Assert.assertNull(consumer.getProvider("provider2")); + Assert.assertNull(consumer.getProvider("provider3")); + + m.clear(); + } + + static interface ServiceInterface { + public void invoke(); + } + + class ServiceProvider implements ServiceInterface { + final Ensure m_ensure; + + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke() { + m_ensure.step(); + } + } + + class ServiceConsumer { + private final Ensure m_ensure; + private final List m_providers = new ArrayList<>(); + private BundleContext m_bc; + private boolean m_stopped; + private final Dependency m_dependency3; + + public ServiceConsumer(Ensure e, Dependency dependency3) { + m_ensure = e; + m_dependency3 = dependency3; + } + + public void add(ServiceReference ref) { + debug("ServiceConsumer.add(%s)", ref); + m_providers.add(ref); + ServiceInterface s = (ServiceInterface) m_bc.getService(ref); + s.invoke(); + } + + public void remove(ServiceReference ref) { + debug("ServiceConsumer.remove(%s)", ref); + m_providers.remove(ref); + debug("ServiceConsumer: current providers list=%s", m_providers); + } + + public void init(Component c) { + c.add(m_dependency3); + } + + public int getProvidersCount() { + return m_providers.size(); + } + + public ServiceInterface getProvider(String name) { + for (ServiceReference ref : m_providers) { + Object n = ref.getProperty("name"); + if (n.equals(name)) { + return (ServiceInterface) m_bc.getService(ref); + } + } + return null; + } + + public void stop() { + m_stopped = true; + } + + public boolean isStopped() { + return m_stopped; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ResourceAdapterDependencyAddAndRemoveTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ResourceAdapterDependencyAddAndRemoveTest.java new file mode 100644 index 00000000000..6d844d1e1d7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ResourceAdapterDependencyAddAndRemoveTest.java @@ -0,0 +1,167 @@ +/* + * 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.dm.itest.api; + +import java.net.URL; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentState; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ResourceHandler; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * @author Felix Project Team + */ +public class ResourceAdapterDependencyAddAndRemoveTest extends TestBase { + public void testBasicResourceAdapter() throws Exception { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + // create and add a service provider + m.add(m.createComponent() + .setInterface(ServiceInterface.class.getName(), null) + .setImplementation(new ServiceProvider(e))); + + // create and add a resource provider + ResourceProvider provider = new ResourceProvider(context, new URL("file://localhost/path/to/file1.txt")); + m.add(m.createComponent() + .setImplementation(provider) + .add(m.createServiceDependency() + .setService(ResourceHandler.class) + .setCallbacks("add", "remove")) + ); + + // create a resource adapter for our single resource + // note that we can provide an actual implementation instance here because there will be only one + // adapter, normally you'd want to specify a Class here + // also, create a callback instance which will be used for both callbacks on resource changes and + // life cycle callbacks on the adapters themselves + + Dependency d = m.createServiceDependency() + .setService(ServiceInterface.class) + .setRequired(true); + CallbackInstance callbackInstance = new CallbackInstance(e, d); + Component component = m.createResourceAdapterService("(&(path=/path/to/*.txt)(host=localhost))", false, callbackInstance, "changed") + .setImplementation(new ResourceAdapter(e)) + .setCallbacks(callbackInstance, "init", "start", "stop", "destroy"); + + // add the resource adapter + m.add(component); + + // wait until the single resource is available (the adapter has been started) + e.waitForStep(1, 5000); + // trigger a 'change' in our resource + provider.change(); + // wait until the changed callback is invoked + e.waitForStep(2, 5000); + // and has completed (ensuring no "extra" steps are invoked in the mean time) + e.waitForStep(3, 5000); + + // remove the resource adapter again + // add a component state listener, in order to track resource adapter destruction + component.add(new ComponentStateListenerImpl(e)); + m.remove(component); + + // wait for the stopped callback in the state listener + e.waitForStep(4, 5000); + m.clear(); + } + + static class ResourceAdapter { + protected URL m_resource; // injected by reflection. + + ResourceAdapter(Ensure e) { + } + } + + static interface ServiceInterface { + public void invoke(); + } + + static class ServiceProvider implements ServiceInterface { + public ServiceProvider(Ensure e) { + } + public void invoke() { + } + } + + class CallbackInstance { + private final Dependency m_dependency; + private final Ensure m_ensure; + + + public CallbackInstance(Ensure e, Dependency d) { + m_ensure = e; + m_dependency = d; + } + + void init(Component c) { + debug("CallbackInstance.init"); + c.add(m_dependency); + } + + void start() { + debug("CallbackInstance.start"); + m_ensure.step(1); + } + + void stop() { + debug("CallbackInstance.stop"); + } + + void destroy() { + debug("CallbackInstance.destroy"); + } + + void changed(Component component) { + m_ensure.step(2); + Dependency oldDependency = m_dependency; + // and add a new dependency + component.add(component.getDependencyManager().createServiceDependency().setService(ServiceInterface.class).setRequired(true)); + // remove the old dependency + component.remove(oldDependency); + debug("CallbackInstance.changed the dependencies"); + m_ensure.step(3); + } + } + + class ComponentStateListenerImpl implements ComponentStateListener { + + private final Ensure m_ensure; + + public ComponentStateListenerImpl(Ensure e) { + this.m_ensure = e; + } + + public void changed(Component c, ComponentState state) { + debug("ComponentStateListenerImpl.changed: state=%s", state); + switch (state) { + case INACTIVE: + System.out.println("stopped"); + m_ensure.step(4); + default: + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ResourceAdapterDependencyAddAndRemoveTest2.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ResourceAdapterDependencyAddAndRemoveTest2.java new file mode 100644 index 00000000000..3cfcf799eff --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ResourceAdapterDependencyAddAndRemoveTest2.java @@ -0,0 +1,186 @@ +/* + * 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.dm.itest.api; + +import java.net.URL; +import java.util.Hashtable; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentState; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ResourceHandler; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * @author Felix Project Team + */ +public class ResourceAdapterDependencyAddAndRemoveTest2 extends TestBase { + public void testBasicResourceAdapter() throws Exception { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a resource provider + ResourceProvider provider = new ResourceProvider(context, new URL("file://localhost/path/to/file1.txt")); + // activate it + Hashtable props = new Hashtable(); + props.put("id", "1"); + m.add(m.createComponent() + .setInterface(ServiceInterface.class.getName(), props) + .setImplementation(new ServiceProvider(e)) + ); + + props = new Hashtable(); + props.put("id", "2"); + m.add(m.createComponent() + .setInterface(ServiceInterface.class.getName(), props) + .setImplementation(new ServiceProvider(e)) + ); + + m.add(m.createComponent() + .setImplementation(provider) + .add(m.createServiceDependency() + .setService(ResourceHandler.class) + .setCallbacks("add", "remove") + ) + ); + + // create a resource adapter for our single resource + // note that we can provide an actual implementation instance here because there will be only one + // adapter, normally you'd want to specify a Class here + Dependency d = m.createServiceDependency().setService(ServiceInterface.class, "(id=1)").setRequired(true); + ResourceAdapter service = new ResourceAdapter(e, d); + + CallbackInstance callbackInstance = new CallbackInstance(e, d); + Component component = m.createResourceAdapterService("(&(path=/path/to/*.txt)(host=localhost))", false, callbackInstance, "changed") + .setImplementation(service) + .setCallbacks(callbackInstance, "init", "start", "stop", "destroy"); + component.add(new ComponentStateListenerImpl(e)); + m.add(component); + // wait until the single resource is available + e.waitForStep(1, 5000); + // trigger a 'change' in our resource + provider.change(); + // wait until the changed callback is invoked + e.waitForStep(2, 5000); + + System.out.println("Done!"); + m.clear(); + } + + static class ResourceAdapter { + protected URL m_resource; // injected by reflection. + final Dependency m_dependency; + + ResourceAdapter(Ensure e, Dependency d) { + m_dependency = d; + } + } + + static interface ServiceInterface { + public void invoke(); + } + + static class ServiceProvider implements ServiceInterface { + public ServiceProvider(Ensure e) { + } + public void invoke() { + } + } + + static class CallbackInstance { + private final Ensure m_ensure; + private final Dependency m_dependency; + + public CallbackInstance(Ensure e, Dependency d) { + m_ensure = e; + m_dependency = d; + } + + void init(Component c) { + c.add(m_dependency); + System.out.println("init"); + m_ensure.step(1); + } + + void start() { + System.out.println("start"); + } + + void stop() { + System.out.println("stop"); + } + + void destroy() { + System.out.println("destroy"); + } + + void changed(Component component) { + m_ensure.step(2); + System.out.println("Changing the dependencies"); + Dependency oldDependency = m_dependency; + + // and add a new dependency + component.add(component.getDependencyManager().createServiceDependency().setService(ServiceInterface.class, "(id=2)").setRequired(true)); + // remove the old dependency + component.remove(oldDependency); + System.out.println("Changed the dependencies"); + } + } + + static class ComponentStateListenerImpl implements ComponentStateListener { + public ComponentStateListenerImpl(Ensure e) { + } + + @Override + public void changed(Component c, ComponentState state) { + switch (state) { + case INACTIVE: + System.out.println("INACTIVE"); + break; + case INSTANTIATED_AND_WAITING_FOR_REQUIRED: + System.out.println("INSTANTIATED_AND_WAITING_FOR_REQUIRED"); + break; + case WAITING_FOR_REQUIRED: + System.out.println("WAITING_FOR_REQUIRED"); + break; + case STARTING: + System.out.println("STARTING"); + break; + case STARTED: + System.out.println("STARTED"); + break; + case TRACKING_OPTIONAL: + System.out.println("TRACKING_OPTIONAL"); + break; + case STOPPING: + System.out.println("STOPING"); + break; + case STOPPED: + System.out.println("STOPPED"); + break; + default: + break; + + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ResourceAdapterTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ResourceAdapterTest.java new file mode 100644 index 00000000000..ad25a54595e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ResourceAdapterTest.java @@ -0,0 +1,84 @@ +/* + * 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.dm.itest.api; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URL; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ResourceHandler; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; + +/** + * @author Felix Project Team + */ +public class ResourceAdapterTest extends TestBase { + public void testBasicResourceAdapter() throws Exception { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a resource provider + ResourceProvider provider = new ResourceProvider(context, new URL("file://localhost/path/to/file1.txt")); + // activate it + m.add(m.createComponent().setImplementation(provider).add(m.createServiceDependency().setService(ResourceHandler.class).setCallbacks("add", "remove"))); + // create a resource adapter for our single resource + // note that we can provide an actual implementation instance here because there will be only one + // adapter, normally you'd want to specify a Class here + m.add(m.createResourceAdapterService("(&(path=/path/to/*.txt)(host=localhost))", false, null, "changed") + .setImplementation(new ResourceAdapter(e))); + // wait until the single resource is available + e.waitForStep(3, 5000); + // trigger a 'change' in our resource + provider.change(); + // wait until the changed callback is invoked + e.waitForStep(4, 5000); + m.clear(); + } + + static class ResourceAdapter { + protected URL m_resource; // injected by reflection. + private Ensure m_ensure; + + ResourceAdapter(Ensure e) { + m_ensure = e; + } + + public void start() { + m_ensure.step(1); + Assert.assertNotNull("resource not injected", m_resource); + m_ensure.step(2); + try { + m_resource.openStream(); + } + catch (FileNotFoundException e) { + m_ensure.step(3); + } + catch (IOException e) { + Assert.fail("We should not have gotten this exception."); + } + } + + public void changed() { + m_ensure.step(4); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ResourceDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ResourceDependencyTest.java new file mode 100644 index 00000000000..9595a9fe643 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ResourceDependencyTest.java @@ -0,0 +1,151 @@ +/* + * 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.dm.itest.api; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.net.MalformedURLException; +import java.net.URL; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ResourceHandler; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; + +/** + * @author Felix Project Team + */ +public class ResourceDependencyTest extends TestBase { + public void testResourceDependency() throws MalformedURLException { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + ResourceConsumer c = new ResourceConsumer(e); + Component consumer = m.createComponent() + .setImplementation(c) + .add(m.createResourceDependency() + .setFilter("(&(path=/path/to/*.txt)(host=localhost))") + .setCallbacks("add", "change", "remove")); + Component dynamicProxyConsumer = m.createComponent() + .setFactory(new ResourceConsumerFactory(e), "create") + .add(m.createResourceDependency() + .setFilter("(path=*.doc)") + .setCallbacks("add", null)); + ResourceProvider provider = new ResourceProvider(context, + new URL("file://localhost/path/to/file1.txt"), + new URL("file://localhost/path/to/file2.txt"), + new URL("file://localhost/path/to/file3.doc")); + Component resourceProvider = m.createComponent() + .setImplementation(provider) + .add(m.createServiceDependency() + .setService(ResourceHandler.class) + .setCallbacks("add", "remove")); + + // first add the consumer + m.add(consumer); + // then the resource provider, which will provide 3 resources, + // 2 of which match the consumers filter conditions + m.add(resourceProvider); + // make sure our consumer invoked openStream() on both resources, + // increasing the step counter to 2 + e.step(3); + + // now add another consumer, that matches only one resource, and uses + // a dynamic proxy as its implementation + m.add(dynamicProxyConsumer); + // ensure the resource was injected properly + e.waitForStep(4, 5000); + + // now change a resource and see if it gets propagated to the consumer + provider.change(0); + + // wait for change callback + e.waitForStep(5, 5000); + e.step(6); + + // cleanup + m.remove(dynamicProxyConsumer); + m.remove(resourceProvider); + m.remove(consumer); + + // validate that all consumed resources are "unconsumed" again + c.ensure(); + m.clear(); + } + + class ResourceConsumer { + private volatile int m_counter; + private Ensure m_ensure; + + public ResourceConsumer(Ensure ensure) { + m_ensure = ensure; + } + + public void add(URL resource) { + debug("ResourceConsumer.add(%s)", resource); + m_counter++; + m_ensure.step(); + } + public void change(URL resource) { + m_ensure.step(); + } + public void remove(URL resource) { + debug("ResourceConsumer.remove(%s)", resource); + m_counter--; + } + public void ensure() { + Assert.assertTrue("all resources should have been added and removed at this point, but " + m_counter + " are remaining", m_counter == 0); + } + } + + + class ResourceConsumerFactory { + private final Ensure m_ensure; + public ResourceConsumerFactory(Ensure ensure) { + m_ensure = ensure; + } + public Object create() { + ResourceConsumer resourceConsumer = new ResourceConsumer(m_ensure); + // create a dynamic proxy for the ResourceProvider + return Proxy.newProxyInstance(resourceConsumer.getClass().getClassLoader(), resourceConsumer.getClass().getInterfaces(), new DynamicProxyHandler(resourceConsumer, m_ensure)); + } + } + + static class DynamicProxyHandler implements InvocationHandler { + Ensure m_ensure; + ResourceConsumer resourceConsumer = null; + + public DynamicProxyHandler(ResourceConsumer resourceConsumer, Ensure ensure) { + this.resourceConsumer = resourceConsumer; + m_ensure = ensure; + } + + public void add(URL resource) { + m_ensure.step(4); + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return method.invoke(resourceConsumer, args); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ResourceProvider.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ResourceProvider.java new file mode 100644 index 00000000000..1bbf1b7ee2b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ResourceProvider.java @@ -0,0 +1,122 @@ +/* + * 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.dm.itest.api; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Dictionary; + +import org.junit.Assert; +import org.apache.felix.dm.ResourceHandler; +import org.apache.felix.dm.ResourceUtil; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; + +/** + * @author Felix Project Team + */ +class ResourceProvider { + final URL[] m_resources; + final BundleContext m_context; + final Map m_handlers = new HashMap<>(); + + ResourceProvider(BundleContext ctx, URL ... resources) { + m_context = ctx; + m_resources = resources; + } + + public void change() { + for (int i = 0; i < m_resources.length; i++) { + change(i); + } + } + + @SuppressWarnings("deprecation") + public void change(int resourceIndex) { + Map handlers = new HashMap<>(); + synchronized (m_handlers) { + handlers.putAll(m_handlers); + } + for (Map.Entry e : handlers.entrySet()) { + ResourceHandler handler = e.getKey(); + Filter filter = e.getValue(); + if (filter == null || filter.match((Dictionary) ResourceUtil.createProperties(m_resources[resourceIndex]))) { + handler.changed(m_resources[resourceIndex]); + } + } + } + + @SuppressWarnings("deprecation") + public void add(ServiceReference ref, ResourceHandler handler) { + String filterString = (String) ref.getProperty("filter"); + Filter filter = null; + if (filterString != null) { + try { + filter = m_context.createFilter(filterString); + } + catch (InvalidSyntaxException e) { + Assert.fail("Could not create filter for resource handler: " + e); + return; + } + } + for (int i = 0; i < m_resources.length; i++) { + if (filter == null || filter.match((Dictionary) ResourceUtil.createProperties(m_resources[i]))) { + synchronized (m_handlers) { + m_handlers.put(handler, filter); + } + handler.added(m_resources[i]); + } + } + } + + public void remove(ServiceReference ref, ResourceHandler handler) { + Filter filter; + synchronized (m_handlers) { + filter = (Filter) m_handlers.remove(handler); + } + if (filter != null) { + removeResources(handler, filter); + } + } + + @SuppressWarnings("deprecation") + private void removeResources(ResourceHandler handler, Filter filter) { + for (int i = 0; i < m_resources.length; i++) { + if (filter == null || filter.match((Dictionary) ResourceUtil.createProperties(m_resources[i]))) { + handler.removed(m_resources[i]); + } + } + } + + public void destroy() { + Map handlers = new HashMap<>(); + synchronized (m_handlers) { + handlers.putAll(m_handlers); + } + + for (Map.Entry e : handlers.entrySet()) { + ResourceHandler handler = e.getKey(); + Filter filter = e.getValue(); + removeResources(handler, filter); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ScopedAspectAdaptersServiceTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ScopedAspectAdaptersServiceTest.java new file mode 100644 index 00000000000..ea2dce2c298 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ScopedAspectAdaptersServiceTest.java @@ -0,0 +1,247 @@ +/* + * 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.dm.itest.api; + +import java.util.Dictionary; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.Component.ServiceScope; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.service.cm.ConfigurationAdmin; + +public class ScopedAspectAdaptersServiceTest extends TestBase { + final Ensure m_e = new Ensure(); + + public void testPrototypeAspect() { + DependencyManager m = getDM(); + Component provider = m.createComponent() + .setFactory(this, "createServiceImpl") + .setInterface(Service.class.getName(), null); + + Component consumer1 = m.createComponent() + .setFactory(this, "createServiceConsumer") + .add(m.createServiceDependency().setService(Service.class).setRequired(true).setCallbacks("bind", null)); + + Component consumer2 = m.createComponent() + .setFactory(this, "createServiceConsumer") + .add(m.createServiceDependency().setService(Service.class).setRequired(true).setCallbacks("bind", null)); + + Component aspect = m.createAspectComponent() + .setAspect(Service.class, null, 10) + .setScope(ServiceScope.PROTOTYPE) + .setFactory(this, "createAspectService"); + + m.add(provider); + m_e.waitForStep(1, 5000); // provider started + + m.add(aspect); + + m.add(consumer1); + m_e.waitForStep(3, 5000); // consumer1 is bound to a clone of the aspect + + m.add(consumer2); + m_e.waitForStep(5, 5000); // consumer2 is bound to a clone of the aspect + + ServiceConsumer consumer1Impl = (ServiceConsumer) consumer1.getInstance(); + Assert.assertNotNull(consumer1Impl.getService()); + ServiceConsumer consumer2Impl = (ServiceConsumer) consumer2.getInstance(); + Assert.assertNotNull(consumer2Impl.getService()); + Assert.assertNotEquals(consumer1Impl.getService(), consumer2Impl.getService()); + Assert.assertEquals(consumer1Impl.getService().getClass(), AspectService.class); + m.clear(); + } + + public void testPrototypeAdapter() { + DependencyManager m = getDM(); + Component provider = m.createComponent() + .setFactory(this, "createServiceImpl") + .setInterface(Service.class.getName(), null); + + Component consumer1 = m.createComponent() + .setFactory(this, "createAdapterServiceConsumer") + .add(m.createServiceDependency().setService(AdapterService.class).setRequired(true).setCallbacks("bind", null)); + + Component consumer2 = m.createComponent() + .setFactory(this, "createAdapterServiceConsumer") + .add(m.createServiceDependency().setService(AdapterService.class).setRequired(true).setCallbacks("bind", null)); + + Component adapter = m.createAdapterComponent() + .setAdaptee(Service.class, null) + .setPropagate(false) + .setScope(ServiceScope.PROTOTYPE) + .setInterface(AdapterService.class.getName(), null) + .setFactory(this, "createAdapterServiceImpl"); + + m.add(provider); + m_e.waitForStep(1, 5000); // provider started + + m.add(adapter); + + m.add(consumer1); + m_e.waitForStep(3, 5000); // consumer1 is bound to a clone of the adapter + + m.add(consumer2); + m_e.waitForStep(5, 5000); // consumer2 is bound to a clone of the adapter + + AdapterServiceConsumer consumer1Impl = (AdapterServiceConsumer) consumer1.getInstance(); + Assert.assertNotNull(consumer1Impl.getService()); + AdapterServiceConsumer consumer2Impl = (AdapterServiceConsumer) consumer2.getInstance(); + Assert.assertNotNull(consumer2Impl.getService()); + Assert.assertNotEquals(consumer1Impl.getService(), consumer2Impl.getService()); + m.clear(); + } + + /** + * Test for a factory configuration adapter: a component is created when its configuration pid is created, + * and when two consumers declares a dependendency on the component, then both consumers will get their own instance + * of the component. + */ + public void testPrototypeFactoryAdapter() { + DependencyManager m = getDM(); + + // Create a Configuration + Component configurator = m.createComponent() + .setImplementation(new FactoryConfigurationCreator(m_e, "prototype.factory", 1, "key", "value")) + .add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true)); + + // Create a config adapter component + Component provider = m.createFactoryComponent() + .setFactoryPid("prototype.factory") + .setUpdated("update") + .setScope(ServiceScope.PROTOTYPE) + .setFactory(this, "createServiceImpl") + .setInterface(Service.class.getName(), null); + + Component consumer1 = m.createComponent() + .setFactory(this, "createServiceConsumer") + .add(m.createServiceDependency().setService(Service.class).setRequired(true).setCallbacks("bind", null)); + + Component consumer2 = m.createComponent() + .setFactory(this, "createServiceConsumer") + .add(m.createServiceDependency().setService(Service.class).setRequired(true).setCallbacks("bind", null)); + + m.add(configurator); + m.add(provider); + + m.add(consumer1); // add first consumer + m_e.waitForStep(2, 5000); // provider prototype started + m_e.waitForStep(3, 5000); // the first consumer is injected with the prototype instance + + m.add(consumer2); // add second consumer + m_e.waitForStep(5, 5000); // a clone of the provider is instantiated and the consumer1 is bound with it. + + // make sure both consumers have a different provider instances. + ServiceConsumer consumer1Impl = (ServiceConsumer) consumer1.getInstance(); + Assert.assertNotNull(consumer1Impl.getService()); + ServiceConsumer consumer2Impl = (ServiceConsumer) consumer2.getInstance(); + Assert.assertNotNull(consumer2Impl.getService()); + Assert.assertNotEquals(consumer1Impl.getService(), consumer2Impl.getService()); + + m.clear(); + } + + @SuppressWarnings("unused") + private ServiceImpl createServiceImpl() { + return new ServiceImpl(); + } + + @SuppressWarnings("unused") + private AdapterServiceImpl createAdapterServiceImpl() { + return new AdapterServiceImpl(); + } + + @SuppressWarnings("unused") + private AdapterServiceConsumer createAdapterServiceConsumer() { + return new AdapterServiceConsumer(); + } + + @SuppressWarnings("unused") + private AspectService createAspectService() { + return new AspectService(); + } + + @SuppressWarnings("unused") + private ServiceConsumer createServiceConsumer() { + return new ServiceConsumer(); + } + + + public interface Service { + } + + public class ServiceImpl implements Service { + void update(Dictionary conf) { + } + + void start() { + m_e.step(); + } + } + + public class AspectService implements Service { + public AspectService() { + m_e.step(); + } + } + + public class ServiceConsumer { + volatile Service m_myService; + + public void bind(Service service) { + m_myService = service; + m_e.step(); + } + + public Service getService() { + return m_myService; + } + } + + public interface AdapterService { + + } + + public class AdapterServiceImpl implements AdapterService { + Service m_service; + + public AdapterServiceImpl() { + } + + void start() { + Assert.assertNotNull(m_service); + m_e.step(); + } + } + + public class AdapterServiceConsumer { + volatile AdapterService m_service; + + public void bind(AdapterService service) { + m_service = service; + m_e.step(); + } + + public AdapterService getService() { + return m_service; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ScopedServiceReferenceAndServiceObjectsTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ScopedServiceReferenceAndServiceObjectsTest.java new file mode 100644 index 00000000000..c86fc1ca14e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ScopedServiceReferenceAndServiceObjectsTest.java @@ -0,0 +1,109 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Component.ServiceScope; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceObjects; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; + +/** + * Validates a simple scoped service, and some service consumers using ServiceReference and ServiceObjects API. + */ +public class ScopedServiceReferenceAndServiceObjectsTest extends TestBase { + final static Ensure m_e = new Ensure(); + + public void testScopedComponent() { + DependencyManager m = getDM(); + + Component provider = m.createComponent() + .setScope(ServiceScope.PROTOTYPE) + .setImplementation(ServiceImpl.class) + .setInterface(Service.class.getName(), null); + + + Consumer1 c1 = new Consumer1(); + Component c1Comp = m.createComponent() + .setImplementation(c1) + .add(m.createServiceDependency().setService(Service.class).setRequired(true).setCallbacks("set", null).setDereference(false)); + + Consumer2 c2 = new Consumer2(); + Component c2Comp = m.createComponent() + .setImplementation(c2) + .add(m.createServiceDependency().setService(Service.class).setRequired(true).setCallbacks("set", null).setDereference(false)); + + m.add(provider); + m.add(c1Comp); + m_e.waitForStep(2, 5000); + + m.add(c2Comp); + m_e.waitForStep(4, 5000); + + Assert.assertNotNull(c1.getService()); + Assert.assertNotNull(c2.getService()); + Assert.assertNotEquals(c1.getService(), c2.getService()); + + m.clear(); + } + + public interface Service { + } + + public static class ServiceImpl implements Service { + volatile Bundle m_bundle; // bundle requesting the service + volatile ServiceRegistration m_registration; // registration of the requested service + + void start() { + m_e.step(); // 1, 3 + } + } + + public static class Consumer1 { + volatile Service m_service; + volatile BundleContext m_bc; + + void set(ServiceReference ref) { + ServiceObjects so = m_bc.getServiceObjects(ref); + m_service = so.getService(); + m_e.step(2); + } + + Service getService() { return m_service; } + } + + public static class Consumer2 { + volatile Service m_service; + volatile BundleContext m_bc; + + void set(ServiceObjects so) { + m_service = so.getService(); + m_e.step(4); + } + + Service getService() { return m_service; } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ScopedServiceTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ScopedServiceTest.java new file mode 100644 index 00000000000..725e9d07a2c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ScopedServiceTest.java @@ -0,0 +1,183 @@ +/* + * 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.dm.itest.api; + +import java.util.Map; +import java.util.Properties; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Component.ServiceScope; +import org.apache.felix.dm.ComponentState; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceRegistration; + +/** + * Validates a simple scoped service, which does not add some dynamic dependencies with a component init method. + */ +public class ScopedServiceTest extends TestBase implements ComponentStateListener { + final static Ensure m_e = new Ensure(); + final Ensure.Steps m_listenerSteps = new Ensure.Steps(1, 4, 11); + final static Ensure.Steps m_serviceImplStartSteps = new Ensure.Steps(2, 5, 12); + final static Ensure.Steps m_serviceImplStopSteps = new Ensure.Steps(8, 10, 16); + final Ensure.Steps m_serviceConsumerBindSteps = new Ensure.Steps(3, 6, 13); + final Ensure.Steps m_serviceConsumerUnbindSteps = new Ensure.Steps(7, 9, 15); + + public void testPrototypeComponent() { + DependencyManager m = getDM(); + + Component provider = m.createComponent() + .setScope(ServiceScope.PROTOTYPE) + .setAutoConfig(Bundle.class, false) + .setAutoConfig(ServiceRegistration.class, false) + .setImplementation(ServiceImpl.class) + .setInterface(Service.class.getName(), null) + .add(m.createServiceDependency().setRequired(true).setService(Service2.class).setCallbacks("bind", null)) + .add(this); + + Component service2 = m.createComponent() + .setInterface(Service2.class.getName(), null) + .setImplementation(new Service2() {}); + + Component consumer1 = m.createComponent() + .setFactory(this, "createServiceConsumer") + .add(m.createServiceDependency().setService(Service.class).setRequired(true).setCallbacks("bind", "change", "unbind")); + + Component consumer2 = m.createComponent() + .setFactory(this, "createServiceConsumer") + .add(m.createServiceDependency().setService(Service.class).setRequired(true).setCallbacks("bind", "unbind")); + + m.add(provider); // add provider + m.add(consumer1); // add first consumer + m.add(service2); // add service2 (the provider depends on it) + m_e.waitForStep(1, 5000); // our listener has seen the first clone starting + m_e.waitForStep(2, 5000); // first clone started + m_e.waitForStep(3, 5000); // first consumer bound to the first clone + + m.add(consumer2); // add second consumer + m_e.waitForStep(4, 5000); // our listener has seen the second clone starting + m_e.waitForStep(5, 5000); // second clone started. + m_e.waitForStep(6, 5000); // second consumer bound to the second clone + + // make sure both consumers have a different provider instances. + ServiceConsumer consumer1Impl = (ServiceConsumer) consumer1.getInstance(); + Assert.assertNotNull(consumer1Impl.getService()); + ServiceConsumer consumer2Impl = (ServiceConsumer) consumer2.getInstance(); + Assert.assertNotNull(consumer2Impl.getService()); + Assert.assertNotEquals(consumer1Impl.getService(), consumer2Impl.getService()); + + m.remove(consumer1); // remove consumer1 + m_e.waitForStep(7, 5000); // consumer1 unbound from first clone + m_e.waitForStep(8, 5000); // first clone stopped + + m.remove(provider); // unregister the provider + m_e.waitForStep(9, 5000); // consumer2 unbound from second clone + m_e.waitForStep(10, 5000); // second clone stopped + m.remove(consumer2); + + m.add(provider); // re-register the provider + m.add(consumer1); // re-add the consumer1 + m_e.waitForStep(11, 5000); // our listener has seen the third clone starting + m_e.waitForStep(12, 5000); // third clone started + m_e.waitForStep(13, 5000); // consumer1 bound to the first clone + + Properties props = new Properties(); + props.put("foo", "bar"); + provider.setServiceProperties(props); // update provider service properties + m_e.waitForStep(14, 5000); // consumer1 should be called in its change callback + + m.remove(service2); // remove the service2 (it will remove the provider + m_e.waitForStep(15, 5000); // consumer1 stopped + m_e.waitForStep(16, 5000); // third clone stopped + + m.clear(); + } + + @SuppressWarnings("unused") + public ServiceConsumer createServiceConsumer() { + return new ServiceConsumer(); + } + + @Override + public void changed(Component c, ComponentState state) { + if (state == ComponentState.STARTING) { + Assert.assertEquals(ServiceImpl.class, c.getInstance().getClass()); + m_e.steps(m_listenerSteps); // will enter in step 1, 4, 11 + } + } + + public interface Service { + } + + public interface Service2 { + } + + public static class ServiceImpl implements Service { + volatile Bundle m_bundle; // bundle requesting the service + volatile ServiceRegistration m_registration; // registration of the requested service + volatile Service2 m_service2; + + public ServiceImpl(Bundle bundle, ServiceRegistration registration) { + m_bundle = bundle; + m_registration = registration; + } + + void bind(Service2 service2) { + m_service2 = service2; + } + + void start() { + Assert.assertNotNull(m_bundle); + Assert.assertNotNull(m_registration); + Assert.assertNotNull(m_service2); + m_e.steps(m_serviceImplStartSteps); // 2, 5, 12 + } + + void stop() { + m_e.steps(m_serviceImplStopSteps); // 8, 10, 16 + } + } + + public class ServiceConsumer { + volatile Service m_myService; + + public void bind(Service service) { + m_myService = service; + m_e.steps(m_serviceConsumerBindSteps); // 3, 6, 13 + } + + public void change(Service service, Map properties) { + Assert.assertEquals("bar", properties.get("foo")); + m_e.step(14); + } + + public void unbind(Service service) { + Assert.assertEquals(m_myService, service); + m_e.steps(m_serviceConsumerUnbindSteps); // 7, 9, 15 + } + + public Service getService() { + return m_myService; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ScopedServiceUnexpectedlyInstantiatedTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ScopedServiceUnexpectedlyInstantiatedTest.java new file mode 100644 index 00000000000..6350ebec917 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ScopedServiceUnexpectedlyInstantiatedTest.java @@ -0,0 +1,86 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Component.ServiceScope; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * Make sure a scoped service does not instantiate + */ +public class ScopedServiceUnexpectedlyInstantiatedTest extends TestBase { + final static Ensure m_e = new Ensure(); + + public void testPrototypeComponent() { + DependencyManager m = getDM(); + + Component s1 = m.createComponent() + .setScope(ServiceScope.PROTOTYPE) + .setImplementation(S1Impl.class) + .setInterface(S1.class.getName(), null) + .add(m.createServiceDependency().setRequired(true).setService(S2.class).setCallbacks("bind", null)); + + Component s2 = m.createComponent() + .setInterface(S2.class.getName(), null) + .setScope(ServiceScope.PROTOTYPE) + .setImplementation(S2Impl.class); + + Component consumer1 = m.createComponent() + .setImplementation(S1Consumer.class) + .add(m.createServiceDependency().setService(S1.class).setRequired(true).setCallbacks("bind", null)); + + m.add(s1); + m.add(s2); + m.add(consumer1); + m_e.waitForStep(3, 5000); + + m.clear(); + } + + public interface S1 { + } + + public interface S2 { + } + + public static class S1Impl implements S1 { + void bind(S2 s2) { + m_e.step(2); + } + + void start() { + m_e.step(3); + } + } + + public static class S2Impl implements S2 { + void start() { + m_e.step(1); + } + } + + public static class S1Consumer { + public void bind(S1 service) { + m_e.step(4); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceDependencyCallbackSignaturesTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceDependencyCallbackSignaturesTest.java new file mode 100644 index 00000000000..e39f830be84 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceDependencyCallbackSignaturesTest.java @@ -0,0 +1,294 @@ +/* + * 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.dm.itest.api; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; + +/** + * @author Felix Project Team + */ +@SuppressWarnings("unused") +public class ServiceDependencyCallbackSignaturesTest extends TestBase { + volatile Ensure m_ensure; + + /** + * Tests if all possible dependency callbacks signatures supported by ServiceDependency. + */ + public void testDependencyCallbackSignatures() { + DependencyManager m = getDM(); + m_ensure = new Ensure(); + Hashtable props = new Hashtable<>(); + props.put("foo", "bar"); + Component provider = m.createComponent() + .setImplementation(new ProviderImpl()).setInterface(Provider.class.getName(), props); + + declareConsumer(m, new Object() { + void bind(Component c, ServiceReference ref, Provider provider) { + Assert.assertNotNull(c); + Assert.assertNotNull(ref); + Assert.assertNotNull(provider); + Assert.assertEquals("bar", ref.getProperty("foo")); + Assert.assertEquals(context.getService(ref), provider); + m_ensure.step(); + } + void change(Component c, ServiceReference ref, Provider provider) { + Assert.assertNotNull(c); + Assert.assertNotNull(ref); + Assert.assertNotNull(provider); + Assert.assertEquals("zoo", ref.getProperty("foo")); + Assert.assertEquals(context.getService(ref), provider); + m_ensure.step(); + } + }); + declareConsumer(m, new Object() { + void bind(Component c, ServiceReference ref, Object provider) { + Assert.assertNotNull(c); + Assert.assertNotNull(ref); + Assert.assertEquals("bar", ref.getProperty("foo")); + Assert.assertNotNull(provider); + Assert.assertEquals(context.getService(ref), provider); + m_ensure.step(); + } + void change(Component c, ServiceReference ref, Object provider) { + Assert.assertNotNull(c); + Assert.assertNotNull(ref); + Assert.assertEquals("zoo", ref.getProperty("foo")); + Assert.assertNotNull(provider); + Assert.assertEquals(context.getService(ref), provider); + m_ensure.step(); + } + }); + declareConsumer(m, new Object() { + void bind(Component c, ServiceReference ref) { + Assert.assertNotNull(c); + Assert.assertNotNull(ref); + Assert.assertEquals("bar", ref.getProperty("foo")); + Assert.assertNotNull(context.getService(ref)); + Assert.assertEquals(context.getService(ref).getClass(), ProviderImpl.class); + m_ensure.step(); + } + void change(Component c, ServiceReference ref) { + Assert.assertNotNull(c); + Assert.assertNotNull(ref); + Assert.assertEquals("zoo", ref.getProperty("foo")); + Assert.assertNotNull(context.getService(ref)); + Assert.assertEquals(context.getService(ref).getClass(), ProviderImpl.class); + m_ensure.step(); + } + }); + declareConsumer(m, new Object() { + void bind(Component c, Provider provider) { + Assert.assertNotNull(c); + Assert.assertNotNull(provider); + m_ensure.step(); + } + void change(Component c, Provider provider) { + Assert.assertNotNull(c); + Assert.assertNotNull(provider); + m_ensure.step(); + } + }); + declareConsumer(m, new Object() { + void bind(Component c, Object provider) { + Assert.assertNotNull(c); + Assert.assertNotNull(provider); + Assert.assertEquals(provider.getClass(), ProviderImpl.class); + m_ensure.step(); + } + void change(Component c, Object provider) { + Assert.assertNotNull(c); + Assert.assertNotNull(provider); + Assert.assertEquals(provider.getClass(), ProviderImpl.class); + m_ensure.step(); + } + }); + declareConsumer(m, new Object() { + void bind(Component c) { + Assert.assertNotNull(c); + m_ensure.step(); + } + void change(Component c) { + Assert.assertNotNull(c); + m_ensure.step(); + } + }); + declareConsumer(m, new Object() { + void bind(Component c, Map props, Provider provider) { + Assert.assertNotNull(c); + Assert.assertNotNull(props); + Assert.assertNotNull(provider); + Assert.assertEquals("bar", props.get("foo")); + Assert.assertEquals(provider.getClass(), ProviderImpl.class); + m_ensure.step(); + } + void change(Component c, Map props, Provider provider) { + Assert.assertNotNull(c); + Assert.assertNotNull(props); + Assert.assertNotNull(provider); + Assert.assertEquals("zoo", props.get("foo")); + Assert.assertEquals(provider.getClass(), ProviderImpl.class); + m_ensure.step(); + } + }); + declareConsumer(m, new Object() { + void bind(ServiceReference ref, Provider provider) { + Assert.assertNotNull(ref); + Assert.assertNotNull(provider); + Assert.assertEquals("bar", ref.getProperty("foo")); + m_ensure.step(); + } + void change(ServiceReference ref, Provider provider) { + Assert.assertNotNull(ref); + Assert.assertNotNull(provider); + Assert.assertEquals("zoo", ref.getProperty("foo")); + m_ensure.step(); + } + }); + declareConsumer(m, new Object() { + void bind(ServiceReference ref, Object provider) { + Assert.assertNotNull(ref); + Assert.assertNotNull(provider); + Assert.assertEquals("bar", ref.getProperty("foo")); + Assert.assertEquals(provider.getClass(), ProviderImpl.class); + m_ensure.step(); + } + void change(ServiceReference ref, Object provider) { + Assert.assertNotNull(ref); + Assert.assertNotNull(provider); + Assert.assertEquals("zoo", ref.getProperty("foo")); + Assert.assertEquals(provider.getClass(), ProviderImpl.class); + m_ensure.step(); + } + }); + declareConsumer(m, new Object() { + void bind(ServiceReference ref) { + Assert.assertNotNull(ref); + Assert.assertEquals("bar", ref.getProperty("foo")); + m_ensure.step(); + } + void change(ServiceReference ref) { + Assert.assertNotNull(ref); + Assert.assertEquals("zoo", ref.getProperty("foo")); + m_ensure.step(); + } + }); + declareConsumer(m, new Object() { + void bind(Provider provider) { + Assert.assertNotNull(provider); + m_ensure.step(); + } + void change(Provider provider) { + Assert.assertNotNull(provider); + m_ensure.step(); + } + }); + declareConsumer(m, new Object() { + void bind(Provider provider, Map props) { + Assert.assertNotNull(provider); + Assert.assertEquals("bar", props.get("foo")); + m_ensure.step(); + } + void change(Provider provider, Map props) { + Assert.assertNotNull(provider); + Assert.assertEquals("zoo", props.get("foo")); + m_ensure.step(); + } + }); + declareConsumer(m, new Object() { + void bind(Map props, Provider provider) { + Assert.assertNotNull(provider); + Assert.assertEquals("bar", props.get("foo")); + m_ensure.step(); + } + void change(Map props, Provider provider) { + Assert.assertNotNull(provider); + Assert.assertEquals("zoo", props.get("foo")); + m_ensure.step(); + } + }); + declareConsumer(m, new Object() { + void bind(Provider provider, Dictionary props) { + Assert.assertNotNull(provider); + Assert.assertEquals("bar", props.get("foo")); + m_ensure.step(); + } + void change(Provider provider, Dictionary props) { + Assert.assertNotNull(provider); + Assert.assertEquals("zoo", props.get("foo")); + m_ensure.step(); + } + }); + declareConsumer(m, new Object() { + void bind(Dictionary props, Provider provider) { + Assert.assertNotNull(provider); + Assert.assertEquals("bar", props.get("foo")); + m_ensure.step(); + } + void change(Dictionary props, Provider provider) { + Assert.assertNotNull(provider); + Assert.assertEquals("zoo", props.get("foo")); + m_ensure.step(); + } + }); + declareConsumer(m, new Object() { + void bind(Object provider) { + Assert.assertNotNull(provider); + Assert.assertEquals(provider.getClass(), ProviderImpl.class); + m_ensure.step(); + } + void change(Object provider) { + bind(provider); + } + }); + + m.add(provider); + m_ensure.waitForStep(16, 5000); + + props = new Hashtable<>(); + props.put("foo", "zoo"); + provider.setServiceProperties(props); + m_ensure.waitForStep(32, 5000); + + m.remove(provider); + m_ensure.waitForStep(48, 5000); + } + + private void declareConsumer(DependencyManager m, Object consumerImpl) { + Component consumer = m.createComponent().setImplementation(consumerImpl) + .add(m.createServiceDependency().setService(Provider.class).setCallbacks("bind", "change", "change").setRequired(true)); + m.add(consumer); + } + + public static interface Provider { + } + + public static class ProviderImpl implements Provider { + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceDependencyInjectionTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceDependencyInjectionTest.java new file mode 100644 index 00000000000..47581ff316c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceDependencyInjectionTest.java @@ -0,0 +1,156 @@ +/* + * 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.dm.itest.api; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * @author Felix Project Team + */ +public class ServiceDependencyInjectionTest extends TestBase { + public void testServiceInjection() { + DependencyManager m = getDM(); + Ensure e = new Ensure(); + // create a service provider and consumer + ServiceProvider provider = new ServiceProvider(e); + Component sp = m.createComponent().setImplementation(provider).setInterface(ServiceInterface2.class.getName(), null); + Component sc = m.createComponent().setImplementation(new ServiceConsumer()) + .add(m.createServiceDependency().setService(ServiceInterface2.class).setRequired(true)); + Component sc2 = m.createComponent() // all dependencies are optional + .setImplementation(new ServiceConsumerNamedInjection(false, false)) + .add(m.createServiceDependency().setService(ServiceInterface2.class).setRequired(false).setAutoConfig("m_service")) + .add(m.createServiceDependency().setService(ServiceInterface2.class).setRequired(false).setAutoConfig("m_service2")) + .add(m.createServiceDependency().setService(ServiceInterface2.class).setRequired(false).setAutoConfig("m_service3")) + ; + Component sc3 = m.createComponent() // second dependency is required, first and third are optional + .setImplementation(new ServiceConsumerNamedInjection(false, false)) + .add(m.createServiceDependency().setService(ServiceInterface2.class).setRequired(false).setAutoConfig("m_service")) + .add(m.createServiceDependency().setService(ServiceInterface2.class).setRequired(true).setAutoConfig("m_service2")) + .add(m.createServiceDependency().setService(ServiceInterface2.class).setRequired(false).setAutoConfig("m_service3")) + ; + Component sc4 = m.createComponent() + .setImplementation(new ServiceConsumerNamedInjection(true, false)); + Component sc5 = m.createComponent() + .setImplementation(new ServiceConsumerNamedInjection(true, true)); + m.add(sp); + m.add(sc); + m.remove(sc); + m.add(sc2); + m.remove(sc2); + m.add(sc3); + m.remove(sc4); + m.add(sc4); + m.remove(sc4); + m.add(sc5); + m.remove(sc5); + m.remove(sp); + e.waitForStep(11, 5000); + m.clear(); + } + + static interface ServiceInterface { + public void invoke(); + } + + static interface ServiceInterface2 extends ServiceInterface { + public void invoke2(); + } + + static class ServiceProvider implements ServiceInterface2 { + private final Ensure m_ensure; + private Ensure.Steps m_invokeSteps = new Ensure.Steps(4, 5, 7, 8, 10, 11, 13, 14); + private Ensure.Steps m_invoke2Steps = new Ensure.Steps(1, 2, 3, 6, 9, 12); + + public ServiceProvider(Ensure e) { + m_ensure = e; + } + + public void invoke() { + System.out.println("invoke"); + m_ensure.steps(m_invokeSteps); + } + + public void invoke2() { + System.out.println("invoke2"); + m_ensure.steps(m_invoke2Steps); + } + } + + static class ServiceConsumer { + private volatile ServiceInterface2 m_service; + private volatile ServiceInterface2 m_service2; + + public void init() { + // invoke the second method of the interface via both injected members, to ensure + // neither of them is a null object (or null) + m_service.invoke2(); + m_service2.invoke2(); + Assert.assertEquals("Both members should have been injected with the same service.", m_service, m_service2); + } + } + + class ServiceConsumerNamedInjection { + private volatile ServiceInterface2 m_service; + private volatile ServiceInterface m_service2; + private volatile Object m_service3; + private final boolean m_secondDependencyRequired; + private final boolean m_instanceBound; + + ServiceConsumerNamedInjection(boolean instanceBound, boolean withSecondRequired) { + m_secondDependencyRequired = withSecondRequired; + m_instanceBound = instanceBound; + } + + public void init(Component c) { + if (m_instanceBound) { + DependencyManager m = c.getDependencyManager(); + c.add(m.createServiceDependency().setService(ServiceInterface2.class).setRequired(false).setAutoConfig("m_service"), + m.createServiceDependency().setService(ServiceInterface2.class).setRequired(m_secondDependencyRequired).setAutoConfig("m_service2"), + m.createServiceDependency().setService(ServiceInterface2.class).setRequired(false).setAutoConfig("m_service3")); + } else { + check(); + } + } + + public void start() { + if (m_instanceBound) { + check(); + } + } + + public void check() { + warn("ServiceConsumerNamedInjectionInstanceBound: m_service=%s, m_service2=%s, m_service3=%s", m_service, m_service2, m_service3); + // invoke the second method + m_service.invoke2(); + // invoke the first method (twice) + m_service2.invoke(); + ((ServiceInterface) m_service3).invoke(); + Assert.assertNotNull("Should have been injected", m_service); + Assert.assertNotNull("Should have been injected", m_service2); + Assert.assertNotNull("Should have been injected", m_service3); + Assert.assertEquals("Members should have been injected with the same service.", m_service, m_service2); + Assert.assertEquals("Members should have been injected with the same service.", m_service, m_service3); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceDependencyPropagateTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceDependencyPropagateTest.java new file mode 100644 index 00000000000..2efb8985274 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceDependencyPropagateTest.java @@ -0,0 +1,133 @@ +/* + * 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.dm.itest.api; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.ServiceReference; + +/** + * Validates ServiceDependency service properties propagation. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes", "serial"}) +public class ServiceDependencyPropagateTest extends TestBase { + /** + * Checks that a ServiceDependency propagates the dependency service properties to the provided service properties. + */ + public void testServiceDependencyPropagate() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + Component c1 = m.createComponent() + .setImplementation(new C1(e)) + .add(m.createServiceDependency().setService(C2.class).setRequired(true).setCallbacks("bind", null)); + + Component c2 = m.createComponent() + .setInterface(C2.class.getName(), new Hashtable() {{ put("foo", "bar"); }}) + .setImplementation(new C2()) + .add(m.createServiceDependency().setService(C3.class).setRequired(true).setPropagate(true)); + + Component c3 = m.createComponent() + .setInterface(C3.class.getName(), new Hashtable() {{ put("foo2", "bar2"); put("foo", "overriden");}}) + .setImplementation(new C3()); + + m.add(c1); + m.add(c2); + m.add(c3); + + e.waitForStep(3, 10000); + + m.remove(c3); + m.remove(c2); + m.remove(c1); + m.clear(); + } + + /** + * Checks that a ServiceDependency propagates the dependency service properties to the provided service properties, + * using a callback method. + */ + public void testServiceDependencyPropagateCallback() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + Component c1 = m.createComponent() + .setImplementation(new C1(e)) + .add(m.createServiceDependency().setService(C2.class).setRequired(true).setCallbacks("bind", null)); + + C2 c2Impl = new C2(); + Component c2 = m.createComponent() + .setInterface(C2.class.getName(), new Hashtable() {{ put("foo", "bar"); }}) + .setImplementation(c2Impl) + .add(m.createServiceDependency().setService(C3.class).setRequired(true).setPropagate(c2Impl, "getServiceProperties")); + + Component c3 = m.createComponent() + .setInterface(C3.class.getName(), null) + .setImplementation(new C3()); + + m.add(c1); + m.add(c2); + m.add(c3); + + e.waitForStep(3, 10000); + m.clear(); + } + + public static class C1 { + private Map m_props; + private Ensure m_ensure; + + C1(Ensure ensure) { + m_ensure = ensure; + } + + void bind(Map props, C2 c2) { + m_props = props; + } + + void start() { + m_ensure.step(1); + if ("bar".equals(m_props.get("foo"))) { // "foo=overriden" from C2 should not override our own "foo" property + m_ensure.step(2); + } + if ("bar2".equals(m_props.get("foo2"))) { + m_ensure.step(3); + } + } + } + + public static class C2 { + C3 m_c3; + + public Dictionary getServiceProperties(ServiceReference ref) { + return new Hashtable() {{ put("foo2", "bar2"); put("foo", "overriden"); }}; + } + } + + public static class C3 { + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceDependencyTest.java new file mode 100644 index 00000000000..ae0b2ee9bf6 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceDependencyTest.java @@ -0,0 +1,95 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + + +/** + * @author Felix Project Team + */ +public class ServiceDependencyTest extends TestBase { + public void testServiceRegistrationAndConsumption() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = m.createComponent().setImplementation(new ServiceProvider(e)).setInterface(ServiceInterface.class.getName(), null); + Component sc = m.createComponent().setImplementation(new ServiceConsumer(e)).add(m.createServiceDependency().setService(ServiceInterface.class).setRequired(true)); + Component sc2 = m.createComponent().setImplementation(new ServiceConsumerCallbacks(e)).add(m.createServiceDependency().setService(ServiceInterface.class).setRequired(false).setCallbacks("add", "remove")); + m.add(sp); + m.add(sc); + m.remove(sc); + m.add(sc2); + m.remove(sp); + m.remove(sc2); + // ensure we executed all steps inside the component instance + e.step(6); + } + + static interface ServiceInterface { + public void invoke(); + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke() { + m_ensure.step(2); + } + } + + static class ServiceConsumer { + private volatile ServiceInterface m_service; + private final Ensure m_ensure; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void init() { + m_ensure.step(1); + m_service.invoke(); + } + + public void destroy() { + m_ensure.step(3); + } + } + + static class ServiceConsumerCallbacks { + private final Ensure m_ensure; + + public ServiceConsumerCallbacks(Ensure e) { + m_ensure = e; + } + + public void add(ServiceInterface service) { + m_ensure.step(4); + } + public void remove(ServiceInterface service) { + m_ensure.step(5); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceDependencyThroughCallbackInstanceTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceDependencyThroughCallbackInstanceTest.java new file mode 100644 index 00000000000..e376e294e45 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceDependencyThroughCallbackInstanceTest.java @@ -0,0 +1,91 @@ +/* + * 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.dm.itest.api; + +import org.junit.Assert; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.TestBase; +import org.osgi.framework.BundleContext; + +/** + * @author Felix Project Team + */ +public class ServiceDependencyThroughCallbackInstanceTest extends TestBase { + public void testServiceWithCallbacksAndOneDependency() { + invokeTest(context, 1); + } + + public void testServiceWithCallbacksAndThreeDependencies() { + invokeTest(context, 3); + } + + private void invokeTest(BundleContext context, int numberOfServices) { + DependencyManager m = getDM(); + // create a number of services + for (int i = 0; i < numberOfServices; i++) { + final int num = i; + m.add(m.createComponent() + .setInterface(Service.class.getName(), null) + .setImplementation(new Service() { + public String toString() { + return "A" + num; + } + } + ) + ); + } + + // create a service with dependency which will be invoked on a callback instance + CallbackInstance instance = new CallbackInstance(); + m.add(m.createComponent() + .setImplementation(new SimpleService() {}) + .add(m.createServiceDependency() + .setService(Service.class) + .setCallbacks(instance, "added", "removed") + .setRequired(true) + ) + ); + + Assert.assertEquals(numberOfServices, instance.getCount()); + m.clear(); + } + + public static interface Service { + } + + public static interface SimpleService { + } + + public static class CallbackInstance { + int m_count = 0; + + void added(Service service) { + System.out.println("added " + service); + m_count++; + } + + void removed(Service service) { + } + + int getCount() { + return m_count; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceRaceParallelTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceRaceParallelTest.java new file mode 100644 index 00000000000..ba97fbe7255 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceRaceParallelTest.java @@ -0,0 +1,28 @@ +/* + * 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.dm.itest.api; + +/** + * @author Felix Project Team + */ +public class ServiceRaceParallelTest extends ServiceRaceTest { + public ServiceRaceParallelTest() { + setParallel(); // Configure DM to use a threadpool + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceRaceParallelWithOrderedUnbindTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceRaceParallelWithOrderedUnbindTest.java new file mode 100644 index 00000000000..f4ee1f1167a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceRaceParallelWithOrderedUnbindTest.java @@ -0,0 +1,38 @@ +/* + * 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.dm.itest.api; + +/** + * This test class simulates a client having many dependencies being registered concurrently using DM concurrent mode. + * (that is: a ComponentExecutorFactory is registered in the osgi registry and the configured threadpool is then used + * to schedule component creations). + * Once the services are created, injected, and fully started, then they are unregistered from a single thread (like it is the case when the osgi + * framework is stopped where bundle are stopped synchronously). + * So, when unbind methods are called, we verify that unbound services are still started at the time unbind callbacks are invoked. + * + * NOTICE: when using DM and a ComponentExecutorFactory, component removal is done synchronously if possible. + * So if you remove components after all components have been fully started, then the component removal are done synchronously. + * + * @author Felix Project Team + */ +public class ServiceRaceParallelWithOrderedUnbindTest extends ServiceRaceWithOrderedUnbindTest { + public ServiceRaceParallelWithOrderedUnbindTest() { + setParallel(); // Configure DM to use a threadpool + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceRaceTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceRaceTest.java new file mode 100644 index 00000000000..4bad0e32bee --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceRaceTest.java @@ -0,0 +1,303 @@ +/* + * 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.dm.itest.api; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ConfigurationDependency; +import org.apache.felix.dm.ServiceDependency; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.ConfigurationException; + + +/** + * This test class simulates a client having many dependencies being registered/unregistered concurrently. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class ServiceRaceTest extends TestBase { + volatile ConfigurationAdmin m_cm; + final static int STEP_WAIT = 15000; + final static int DEPENDENCIES = 10; + final static int LOOPS = 3000; + final Ensure m_done = new Ensure(true); + + // Timestamp used to log the time consumed to execute 100 tests. + long m_timeStamp; + + public interface Dep { + } + + public class DepImpl implements Dep { + } + + /** + * Creates many service dependencies, and activate/deactivate them concurrently. + */ + public void testCreateParallelComponentRegistgrationUnregistration() { + m_dm.add(m_dm.createComponent() + .setImplementation(this) + .setCallbacks(null, "start", null, null) + .add(m_dm.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true))); + m_done.waitForStep(1, 60000); + m_dm.clear(); + Assert.assertFalse(super.errorsLogged()); + } + + void start() { + new Thread(new Runnable() { + public void run() { + doStart(); + }}).start(); + } + + void doStart() { + info("Starting createParallelComponentRegistgrationUnregistration test"); + initThreadPool(); // only if setParallel() has not been called (only if a parallel DM is not used). + + try { + m_timeStamp = System.currentTimeMillis(); + for (int loop = 0; loop < LOOPS; loop++) { + doTest(loop); + } + } + catch (Throwable t) { + error("got unexpected exception", t); + } + finally { + shutdownThreadPool(); + m_done.step(1); + } + } + + private void initThreadPool() { + if (! m_parallel) { + // We are not using a parallel DM, so we create a custom threadpool in order to add components concurrently. + int cores = Math.max(16, Runtime.getRuntime().availableProcessors()); + info("using " + cores + " cores."); + m_threadPool = new ForkJoinPool(Math.max(cores, DEPENDENCIES + 3 /* start/stop/configure */)); + } + } + + void shutdownThreadPool() { + if (! m_parallel && m_threadPool != null) { + m_threadPool.shutdown(); + try { + m_threadPool.awaitTermination(60, TimeUnit.SECONDS); + } catch (InterruptedException e) { + } + } + } + + void doTest(int loop) throws Throwable { + debug("loop#%d -------------------------", loop); + + final Ensure step = new Ensure(false); + + // Create one client component, which depends on many service dependencies + final Component client = m_dm.createComponent(); + final Client clientImpl = new Client(step); + client.setImplementation(clientImpl); + + // Create client service dependencies + final ServiceDependency[] dependencies = new ServiceDependency[DEPENDENCIES]; + for (int i = 0; i < DEPENDENCIES; i++) { + final String filter = "(id=loop" + loop + "." + i + ")"; + dependencies[i] = m_dm.createServiceDependency().setService(Dep.class, filter) + .setRequired(true) + .setCallbacks("add", "remove"); + client.add(dependencies[i]); + } + String pid = "pid." + loop; + final ConfigurationDependency confDependency = m_dm.createConfigurationDependency().setPid(pid); + client.add(confDependency); + + // Create Configuration (concurrently). + final Configuration conf = m_cm.getConfiguration(pid, null); + final Hashtable props = new Hashtable(); + props.put("foo", "bar"); + schedule(new Runnable() { + public void run() { + try { + conf.update(props); + } + catch (IOException e) { + error("update failed", e); + } + } + }); + + // Activate the client service dependencies concurrently. + List deps = new ArrayList(); + for (int i = 0; i < DEPENDENCIES; i++) { + Hashtable h = new Hashtable(); + h.put("id", "loop" + loop + "." + i); + final Component s = m_dm.createComponent() + .setInterface(Dep.class.getName(), h) + .setImplementation(new DepImpl()); + deps.add(s); + schedule(new Runnable() { + public void run() { + m_dm.add(s); + } + }); + } + + // Start the client (concurrently) + schedule(new Runnable() { + public void run() { + m_dm.add(client); + } + }); + + // Ensure that client has been started. + int expectedStep = 1 /* conf */ + DEPENDENCIES + 1 /* start */; + step.waitForStep(expectedStep, STEP_WAIT); + Assert.assertEquals(DEPENDENCIES, clientImpl.getDependencies()); + Assert.assertNotNull(clientImpl.getConfiguration()); + + // Stop all dependencies concurrently. + for (Component dep : deps) { + final Component dependency = dep; + schedule(new Runnable() { + public void run() { + m_dm.remove(dependency); + } + }); + } + + // Stop client concurrently. + schedule(new Runnable() { + public void run() { + m_dm.remove(client); + } + }); + + // Remove configuration (asynchronously) + final Ensure stepConfDeleted = new Ensure(false); + schedule(new Runnable() { + public void run() { + try { + conf.delete(); + stepConfDeleted.step(1); + } + catch (IOException e) { + warn("error while unconfiguring", e); + } + } + }); + + // Ensure that client has been stopped, then destroyed, then unbound from all dependencies + expectedStep += 2; // stop/destroy + expectedStep += DEPENDENCIES; // removed all dependencies + step.waitForStep(expectedStep, STEP_WAIT); + // Make sure configuration is removed + stepConfDeleted.waitForStep(1, STEP_WAIT); + step.ensure(); + Assert.assertEquals(0, clientImpl.getDependencies()); + + m_threadPool.awaitQuiescence(5000, TimeUnit.MILLISECONDS); + + if (super.errorsLogged()) { + throw new IllegalStateException("Race test interrupted (some error occured, see previous logs)"); + } + + debug("finished one test loop"); + if ((loop + 1) % 100 == 0) { + long duration = System.currentTimeMillis() - m_timeStamp; + warn("Performed 100 tests (total=%d) in %d ms.", (loop + 1), duration); + m_timeStamp = System.currentTimeMillis(); + } + } + + private void schedule(Runnable task) { + if (! m_parallel) { + // not using parallel DM, so use our custom threadpool. + m_threadPool.execute(task); + } else { + task.run(); + } + } + + public class Client { + final Ensure m_step; + volatile int m_dependencies; + volatile Dictionary m_conf; + + public Client(Ensure step) { + m_step = step; + } + + public void updated(Dictionary conf) throws ConfigurationException { + if (conf != null) { + try { + Assert.assertEquals("bar", conf.get("foo")); + m_conf = conf; + m_step.step(1); + } catch (Throwable t) { + m_step.throwable(t); + } + } + } + + void add(Dep d) { + Assert.assertNotNull(d); + m_dependencies ++; + m_step.step(); + } + + void remove(Dep d) { + Assert.assertNotNull(d); + m_dependencies --; + m_step.step(); + } + + void start() { + m_step.step((DEPENDENCIES + 1) /* deps + conf */ + 1 /* start */); + } + + void stop() { + m_step.step((DEPENDENCIES + 1) /* deps + conf */ + 1 /* start */ + 1 /* stop */); + } + + void destroy() { + m_step.step((DEPENDENCIES + 1) /* deps + conf */ + 1 /* start */ + 1 /* stop */ + 1 /* destroy */); + } + + int getDependencies() { + return m_dependencies; + } + + Dictionary getConfiguration() { + return m_conf; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceRaceWithOrderedUnbindTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceRaceWithOrderedUnbindTest.java new file mode 100644 index 00000000000..5c03d852616 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceRaceWithOrderedUnbindTest.java @@ -0,0 +1,255 @@ +/* + * 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.dm.itest.api; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.ServiceDependency; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; + + +/** + * This test class simulates a client having many dependencies being registered concurrently. + * (threads are created manually, and we are not using a ComponentExecutorFactory). + * The services are then unregistered from a single thread (like it is the case when the osgi + * framework is stopped where bunldes are stopped synchronously). + * So, when unbind methods are called, we verify that unbound services are still started at + * the time unbind callbacks are invoked. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class ServiceRaceWithOrderedUnbindTest extends TestBase { + final static int STEP_WAIT = 5000; + final static int DEPENDENCIES = 10; + final static int LOOPS = 3000; + final Ensure m_done = new Ensure(true); + + // Timestamp used to log the time consumed to execute 100 tests. + long m_timeStamp; + + public interface Dep { + boolean isStarted(); + } + + public class DepImpl implements Dep { + volatile boolean m_started; + + void start() { + m_started = true; + } + + void stop() { + m_started = false; + } + + public boolean isStarted() { + return m_started; + } + } + + /** + * Creates many service dependencies, and activate/deactivate them concurrently. + */ + public void testCreatesComponentsConcurrently() { + m_dm.add(m_dm.createComponent() + .setImplementation(this) + .setCallbacks(null, "start", null, null)); + m_done.waitForStep(1, 60000); + m_dm.clear(); + Assert.assertFalse(super.errorsLogged()); + } + + void start() { + new Thread(this::doStart).start(); + } + + void doStart() { + info("Starting createParallelComponentRegistgrationUnregistration test"); + initThreadPool(); // only if setParallel() has not been called (only if a parallel DM is not used). + + try { + m_timeStamp = System.currentTimeMillis(); + for (int loop = 0; loop < LOOPS; loop++) { + doTest(loop); + } + } + catch (Throwable t) { + error("got unexpected exception", t); + } + finally { + shutdownThreadPool(); + m_done.step(1); + } + } + + private void initThreadPool() { + if (! m_parallel) { + // We are not using a parallel DM, so we create a custom threadpool in order to add components concurrently. + int cores = Math.max(16, Runtime.getRuntime().availableProcessors()); + info("using " + cores + " cores."); + m_threadPool = new ForkJoinPool(Math.max(cores, DEPENDENCIES + 3 /* start/stop/configure */)); + } + } + + void shutdownThreadPool() { + if (! m_parallel && m_threadPool != null) { + m_threadPool.shutdown(); + try { + m_threadPool.awaitTermination(60, TimeUnit.SECONDS); + } catch (InterruptedException e) { + } + } + } + + void doTest(int loop) throws Throwable { + debug("loop#%d -------------------------", loop); + + final Ensure step = new Ensure(false); + + // Create one client component, which depends on many service dependencies + final Component client = m_dm.createComponent(); + final Client clientImpl = new Client(step); + client.setImplementation(clientImpl); + + // Before creating the client, register a component listener to check + // the client is really started or deactivated. + ComponentStateListener clientListener = (c, s) -> { + switch(s) { + case TRACKING_OPTIONAL: + step.step(1); + break; + case INACTIVE: + step.step(2); + break; + default: + break; + } + }; + client.add(clientListener); + + // Create client service dependencies + final ServiceDependency[] dependencies = new ServiceDependency[DEPENDENCIES]; + for (int i = 0; i < DEPENDENCIES; i++) { + final String filter = "(id=loop" + loop + "." + i + ")"; + dependencies[i] = m_dm.createServiceDependency().setService(Dep.class, filter) + .setRequired(true) + .setCallbacks("add", "remove"); + client.add(dependencies[i]); + } + + // Activate the client service dependencies concurrently. + List deps = new ArrayList(); + for (int i = 0; i < DEPENDENCIES; i++) { + Hashtable h = new Hashtable(); + h.put("id", "loop" + loop + "." + i); + final Component s = m_dm.createComponent() + .setInterface(Dep.class.getName(), h) + .setImplementation(new DepImpl()); + deps.add(s); + schedule(() -> m_dm.add(s)); + } + + // Start the client (concurrently) + schedule(() -> m_dm.add(client)); + + // Ensure that client has been started. + step.waitForStep(1, STEP_WAIT); // client has entered in TRACKING_OPTIONAL state + Assert.assertEquals(DEPENDENCIES, clientImpl.getDependencies()); + + // Make sure threadpool is quiescent, then deactivate all components. + if (! m_threadPool.awaitQuiescence(5000, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("Could not start components timely."); + } + + // Stop all dependencies, and client + schedule(() -> { + for (Component dep : deps) { + final Component dependency = dep; + m_dm.remove(dependency); + } + m_dm.remove(client); + }); + + // Ensure that client has been stopped, then destroyed, then unbound from all dependencies + step.waitForStep(2, STEP_WAIT); // Client entered in INACTIVE state + step.ensure(); + Assert.assertEquals(0, clientImpl.getDependencies()); + + // Make sure threadpool is quiescent before doing next iteration. + if (! m_threadPool.awaitQuiescence(5000, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("Could not start components timely."); + } + + if (super.errorsLogged()) { + throw new IllegalStateException("Race test interrupted (some error occured, see previous logs)"); + } + + debug("finished one test loop"); + if ((loop + 1) % 100 == 0) { + long duration = System.currentTimeMillis() - m_timeStamp; + warn("Performed 100 tests (total=%d) in %d ms.", (loop + 1), duration); + m_timeStamp = System.currentTimeMillis(); + } + } + + private void schedule(Runnable task) { + if (! m_parallel) { + // not using parallel DM, so use our custom threadpool. + m_threadPool.execute(task); + } else { + task.run(); + } + } + + public class Client { + final Ensure m_step; + volatile int m_dependencies; + + public Client(Ensure step) { + m_step = step; + } + + void add(Dep d) { + Assert.assertNotNull(d); + m_dependencies ++; + } + + void remove(Dep d) { + Assert.assertNotNull(d); + if (! d.isStarted()) { + Thread.dumpStack(); + } + Assert.assertTrue(d.isStarted()); + m_dependencies --; + } + + int getDependencies() { + return m_dependencies; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceTrackerTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceTrackerTest.java new file mode 100644 index 00000000000..d9076b4b3e9 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceTrackerTest.java @@ -0,0 +1,114 @@ +/* + * 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.dm.itest.api; + +import java.util.Hashtable; + +import org.junit.Assert; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.ServiceUtil; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.tracker.ServiceTracker; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceRegistration; + +/** + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes", "serial"}) +public class ServiceTrackerTest extends TestBase { + public void testPlainServiceTracker() { + ServiceTracker st = new ServiceTracker(context, ServiceInterface.class.getName(), null); + st.open(); + ServiceRegistration sr = context.registerService(ServiceInterface.class.getName(), new ServiceProvider(), null); + Assert.assertEquals("There should be one service that matches the tracker", 1, st.getServices().length); + sr.unregister(); + Assert.assertNull("There should be no service that matches the tracker", st.getServices()); + st.close(); + } + + public void testAspectServiceTracker() { + ServiceTracker st = new ServiceTracker(context, ServiceInterface.class.getName(), null); + st.open(); + + ServiceRegistration sr = context.registerService(ServiceInterface.class.getName(), new ServiceProvider(), null); + Assert.assertEquals("There should be one service that matches the tracker", 1, st.getServices().length); + + final long sid = ServiceUtil.getServiceId(sr.getReference()); + ServiceRegistration asr = context.registerService(ServiceInterface.class.getName(), new ServiceProvider(), + new Hashtable() {{ put(DependencyManager.ASPECT, sid); put(Constants.SERVICE_RANKING, 10); }}); + Assert.assertEquals("There should be one service that matches the tracker", 1, st.getServices().length); + Assert.assertEquals("Service ranking should be 10", Integer.valueOf(10), (Integer) st.getServiceReference().getProperty(Constants.SERVICE_RANKING)); + + ServiceRegistration asr2 = context.registerService(ServiceInterface.class.getName(), new ServiceProvider(), + new Hashtable() {{ put(DependencyManager.ASPECT, sid); put(Constants.SERVICE_RANKING, 20); }}); + Assert.assertEquals("There should be one service that matches the tracker", 1, st.getServices().length); + Assert.assertEquals("Service ranking should be 20", Integer.valueOf(20), (Integer) st.getServiceReference().getProperty(Constants.SERVICE_RANKING)); + + asr.unregister(); + Assert.assertEquals("There should be one service that matches the tracker", 1, st.getServices().length); + Assert.assertEquals("Service ranking should be 20", Integer.valueOf(20), (Integer) st.getServiceReference().getProperty(Constants.SERVICE_RANKING)); + + asr2.unregister(); + Assert.assertEquals("There should be one service that matches the tracker", 1, st.getServices().length); + Assert.assertNull("Service should not have a ranking", st.getServiceReference().getProperty(Constants.SERVICE_RANKING)); + + sr.unregister(); + Assert.assertNull("There should be no service that matches the tracker", st.getServices()); + + st.close(); + } + + public void testExistingAspectServiceTracker() { + ServiceTracker st = new ServiceTracker(context, ServiceInterface.class.getName(), null); + ServiceRegistration sr = context.registerService(ServiceInterface.class.getName(), new ServiceProvider(), null); + final long sid = ServiceUtil.getServiceId(sr.getReference()); + ServiceRegistration asr = context.registerService(ServiceInterface.class.getName(), new ServiceProvider(), + new Hashtable() {{ put(DependencyManager.ASPECT, sid); put(Constants.SERVICE_RANKING, 10); }}); + ServiceRegistration asr2 = context.registerService(ServiceInterface.class.getName(), new ServiceProvider(), + new Hashtable() {{ put(DependencyManager.ASPECT, sid); put(Constants.SERVICE_RANKING, 20); }}); + + st.open(); + Assert.assertEquals("There should be one service that matches the tracker", 1, st.getServices().length); + Assert.assertEquals("Service ranking should be 20", Integer.valueOf(20), (Integer) st.getServiceReference().getProperty(Constants.SERVICE_RANKING)); + + asr2.unregister(); + Assert.assertEquals("There should be one service that matches the tracker", 1, st.getServices().length); + Assert.assertEquals("Service ranking should be 10", Integer.valueOf(10), (Integer) st.getServiceReference().getProperty(Constants.SERVICE_RANKING)); + + asr.unregister(); + Assert.assertEquals("There should be one service that matches the tracker", 1, st.getServices().length); + Assert.assertNull("Service should not have a ranking", st.getServiceReference().getProperty(Constants.SERVICE_RANKING)); + + sr.unregister(); + Assert.assertNull("There should be no service that matches the tracker", st.getServices()); + + st.close(); + } + + static interface ServiceInterface { + public void invoke(); + } + + static class ServiceProvider implements ServiceInterface { + public void invoke() { + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceUpdateTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceUpdateTest.java new file mode 100644 index 00000000000..6bc1522f1a3 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceUpdateTest.java @@ -0,0 +1,155 @@ +/* + * 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.dm.itest.api; + +import java.net.URL; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Properties; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ResourceHandler; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * @author Felix Project Team + */ +public class ServiceUpdateTest extends TestBase { + public void testServiceUpdate() throws Exception { + final DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a resource provider + ResourceProvider provider = new ResourceProvider(context, new URL("file://localhost/path/to/file1.txt")); + // activate it + m.add(m.createComponent() + .setImplementation(new ServiceProvider(e)) + .add(m.createServiceDependency() + .setService(ServiceInterface.class) + .setRequired(true) + .setCallbacks("add", "change", "remove") + ) + ); + + m.add(m.createComponent() + .setImplementation(provider) + .add(m.createServiceDependency() + .setService(ResourceHandler.class) + .setCallbacks("add", "remove") + ) + ); + + // create a resource adapter for our single resource + // note that we can provide an actual implementation instance here because there will be only one + // adapter, normally you'd want to specify a Class here + CallbackInstance callbackInstance = new CallbackInstance(e); + Hashtable serviceProps = new Hashtable(); + serviceProps.put("number", "1"); + Component component = m.createResourceAdapterService("(&(path=/path/to/*.txt)(host=localhost))", false, callbackInstance, "changed") + .setImplementation(new ResourceAdapter(e)) + .setInterface(ServiceInterface.class.getName(), serviceProps) + .setCallbacks(callbackInstance, "init", "start", "stop", "destroy"); + m.add(component); + // wait until the single resource is available + e.waitForStep(1, 5000); + // wait until the component gets the dependency injected + e.waitForStep(2, 5000); + // trigger a 'change' in our resource + provider.change(); + // wait until the changed callback is invoked + e.waitForStep(3, 5000); + // wait until the changed event arrived at the component + e.waitForStep(4, 5000); + System.out.println("Done!"); + m.clear(); + } + + static class ResourceAdapter implements ServiceInterface { + protected URL m_resource; // injected by reflection. + + ResourceAdapter(Ensure e) { + } + + public void invoke() { + // TODO Auto-generated method stub + + } + } + + static interface ServiceInterface { + public void invoke(); + } + + static class ServiceProvider { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + void add(ServiceInterface i) { + m_ensure.step(2); + } + void change(ServiceInterface i) { + System.out.println("Change..."); + m_ensure.step(4); + } + void remove(ServiceInterface i) { + System.out.println("Remove..."); + } + } + + static class CallbackInstance { + private final Ensure m_ensure; + public CallbackInstance(Ensure e) { + m_ensure = e; + } + + void init() { + System.out.println("init"); + } + void start() { + System.out.println("start"); + m_ensure.step(1); + } + void stop() { + System.out.println("stop"); + } + void destroy() { + System.out.println("destroy"); + } + void changed(Component component) { + System.out.println("resource changed"); + m_ensure.step(3); + + Properties newProps = new Properties(); + // update the component's service properties + Dictionary dict = component.getServiceProperties(); + Enumeration e = dict.keys(); + while (e.hasMoreElements()) { + String key = e.nextElement(); + String value = dict.get(key); + newProps.setProperty(key, value); + } + newProps.setProperty("new-property", "2"); + component.getServiceRegistration().setProperties(newProps); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/TemporalServiceDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/TemporalServiceDependencyTest.java new file mode 100644 index 00000000000..2926ff2f8f6 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/TemporalServiceDependencyTest.java @@ -0,0 +1,270 @@ +/* + * 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.dm.itest.api; + +import java.util.Hashtable; + +import org.junit.Assert; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ServiceDependency; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes", "serial"}) +public class TemporalServiceDependencyTest extends TestBase { + public void testServiceConsumptionAndIntermittentAvailability() { + final DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + TemporalServiceProvider provider = new TemporalServiceProvider(e); + Component sp = m.createComponent().setImplementation(provider).setInterface(TemporalServiceInterface.class.getName(), null); + TemporalServiceProvider2 provider2 = new TemporalServiceProvider2(e); + Component sp2 = m.createComponent().setImplementation(provider2).setInterface(TemporalServiceInterface.class.getName(), null); + TemporalServiceConsumer consumer = new TemporalServiceConsumer(e); + Component sc = m.createComponent().setImplementation(consumer) + .add(m.createTemporalServiceDependency(10000).setService(TemporalServiceInterface.class).setRequired(true)); + // add the service consumer + m.add(sc); + // now add the first provider + m.add(sp); + e.waitForStep(2, 5000); + // and remove it again (this should not affect the consumer yet) + m.remove(sp); + // now add the second provider + m.add(sp2); + e.step(3); + e.waitForStep(4, 5000); + // and remove it again + m.remove(sp2); + // finally remove the consumer + m.remove(sc); + // ensure we executed all steps inside the component instance + e.step(6); + m.clear(); + } + + public void testServiceConsumptionWithCallbackAndIntermittentAvailability() { + final DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + TemporalServiceProvider provider = new TemporalServiceProvider(e); + Component sp = m.createComponent().setImplementation(provider).setInterface(TemporalServiceInterface.class.getName(), null); + TemporalServiceProvider2 provider2 = new TemporalServiceProvider2(e); + Component sp2 = m.createComponent().setImplementation(provider2).setInterface(TemporalServiceInterface.class.getName(), null); + TemporalServiceConsumerWithCallback consumer = new TemporalServiceConsumerWithCallback(e); + ServiceDependency temporalDep = m.createTemporalServiceDependency(10000).setService(TemporalServiceInterface.class).setRequired(true).setCallbacks("add", "remove"); + Component sc = m.createComponent().setImplementation(consumer).add(temporalDep); + + // add the service consumer + m.add(sc); + // now add the first provider + m.add(sp); + e.waitForStep(2, 5000); + // and remove it again (this should not affect the consumer yet) + m.remove(sp); + // now add the second provider + m.add(sp2); + e.step(3); + e.waitForStep(4, 5000); + // and remove it again + m.remove(sp2); + // finally remove the consumer + m.remove(sc); + // Wait for the consumer.remove callback + e.waitForStep(6, 5000); + // ensure we executed all steps inside the component instance + e.step(7); + m.clear(); + } + + // Same test as testServiceConsumptionWithCallbackAndIntermittentAvailability, but the consumer is now + // an adapter for the Adaptee interface. + public void testFELIX4858_ServiceAdapterConsumptionWithCallbackAndIntermittentAvailability() { + final DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + TemporalServiceProvider provider = new TemporalServiceProvider(e); + Component sp = m.createComponent().setImplementation(provider).setInterface(TemporalServiceInterface.class.getName(), null); + TemporalServiceProvider2 provider2 = new TemporalServiceProvider2(e); + Component sp2 = m.createComponent().setImplementation(provider2).setInterface(TemporalServiceInterface.class.getName(), null); + TemporalServiceConsumerAdapterWithCallback consumer = new TemporalServiceConsumerAdapterWithCallback(e); + ServiceDependency temporalDep = m.createTemporalServiceDependency(10000).setService(TemporalServiceInterface.class).setRequired(true).setCallbacks("add", "remove"); + Component sc = m.createAdapterService(Adaptee.class, null).setImplementation(consumer).add(temporalDep); + Component adaptee = m.createComponent().setImplementation(new Adaptee()).setInterface(Adaptee.class.getName(), null); + + // add the adapter service consumer + m.add(sc); + // add the adaptee (the adapter service depends on it) + m.add(adaptee); + // now add the first provider + m.add(sp); + e.waitForStep(2, 5000); + // and remove it again (this should not affect the consumer yet) + m.remove(sp); + // now add the second provider + m.add(sp2); + e.step(3); + e.waitForStep(4, 5000); + // and remove it again + m.remove(sp2); + // finally remove the consumer + m.remove(sc); + // Wait for the consumer.remove callback + e.waitForStep(6, 5000); + // ensure we executed all steps inside the component instance + e.step(7); + m.clear(); + } + + public void testFelix4602_PropagateServiceInvocationException() { + final DependencyManager m = getDM(); + final Ensure ensure = new Ensure(); + Runnable provider = new Runnable() { + public void run() { + throw new UncheckedException(); + } + }; + Hashtable props = new Hashtable(); + props.put("target", getClass().getSimpleName()); + Component providerComp = m.createComponent() + .setInterface(Runnable.class.getName(), props) + .setImplementation(provider); + + Object consumer = new Object() { + volatile Runnable m_provider; + @SuppressWarnings("unused") + void start() { + try { + ensure.step(1); + m_provider.run(); + } catch (UncheckedException e) { + ensure.step(2); + } + } + }; + Component consumerComp = m.createComponent() + .setImplementation(consumer) + .add(m.createTemporalServiceDependency(5000) + .setService(Runnable.class, "(target=" + getClass().getSimpleName() + ")") + .setRequired(true)); + m.add(consumerComp); + m.add(providerComp); + ensure.waitForStep(2, 5000); + m.clear(); + } + + static class UncheckedException extends RuntimeException { + } + + static interface TemporalServiceInterface { + public void invoke(); + } + + static class TemporalServiceProvider implements TemporalServiceInterface { + private final Ensure m_ensure; + public TemporalServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke() { + m_ensure.step(2); + } + } + + static class TemporalServiceProvider2 implements TemporalServiceInterface { + protected final Ensure m_ensure; + public TemporalServiceProvider2(Ensure e) { + m_ensure = e; + } + public void invoke() { + m_ensure.step(4); + } + } + + static class TemporalServiceConsumer implements Runnable { + protected volatile TemporalServiceInterface m_service; + protected final Ensure m_ensure; + + public TemporalServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void init() { + m_ensure.step(1); + Thread t = new Thread(this); + t.start(); + } + + public void run() { + m_service.invoke(); + m_ensure.waitForStep(3, 15000); + m_service.invoke(); + } + + public void destroy() { + m_ensure.step(5); + } + } + + static class TemporalServiceConsumerWithCallback extends TemporalServiceConsumer { + public TemporalServiceConsumerWithCallback(Ensure e) { + super(e); + } + + public void add(TemporalServiceInterface service) { + m_service = service; + } + + public void remove(TemporalServiceInterface service) { + Assert.assertTrue(m_service == service); + m_ensure.step(6); + } + } + + public static class Adaptee { + } + + static class TemporalServiceConsumerAdapterWithCallback extends TemporalServiceConsumer { + volatile Adaptee m_adaptee; + + public TemporalServiceConsumerAdapterWithCallback(Ensure e) { + super(e); + } + + public void start() { + Assert.assertTrue(m_adaptee != null); + } + + public void add(TemporalServiceInterface service) { + m_service = service; + } + + public void remove(TemporalServiceInterface service) { + Assert.assertTrue(m_service == service); + m_ensure.step(6); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/UngetAspectServiceTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/UngetAspectServiceTest.java new file mode 100644 index 00000000000..efca1865562 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/UngetAspectServiceTest.java @@ -0,0 +1,111 @@ +/* + * 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.dm.itest.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.junit.Assert; +import org.osgi.framework.ServiceReference; + +/** + * Use case: + * + * Client depends on Provider and holds the ServiceReference to it + * Provider is swapped with a Provider aspect + * Then Client and Aspect are stopped + * + * This tests verifies that the ServiceReference originally gotten by CLient to Provider is ungotten + */ +public class UngetAspectServiceTest extends TestBase { + final Ensure m_ensure = new Ensure(); + + public void testUngetSwappedService() { + DependencyManager m = getDM(); + + Client clientInstance = new Client(); + Component client = m.createComponent() + .setImplementation(clientInstance) + .add(m.createServiceDependency().setService(Provider.class).setRequired(true).setCallbacks("bind", null, null, "swap")); + + Component provider = m.createComponent() + .setImplementation(new ProviderImpl()) + .setInterface(Provider.class.getName(), null); + + Aspect aspectInstance = new Aspect(); + Component aspect = m.createAspectService(Provider.class, null, 1) + .setImplementation(aspectInstance); + + // add client, provider + m.add(client); + m.add(provider); + + // wait for client to be bound to provider + m_ensure.waitForStep(1, 5000); + + // add aspect + m.add(aspect); + + // check for client to be swapped with aspect + m_ensure.waitForStep(2, 5000); + + // remove client, and aspect + m.remove(client); + m.remove(aspect); + + // Now, no more references should point to the provider + Assert.assertEquals(false, this.context.ungetService(clientInstance.getServiceRef())); + } + + public interface Provider { + } + + public class ProviderImpl implements Provider { + } + + public class Aspect implements Provider { + private ServiceReference m_ref; + + void bind(ServiceReference provider) { + m_ref = provider; + } + + ServiceReference getRef() { + return m_ref; + } + } + + public class Client { + ServiceReference m_Aref; + + void bind(ServiceReference Aref, Provider provider) { + m_Aref = Aref; + m_ensure.step(1); + } + + void swap(Provider old, Provider replace) { + m_ensure.step(2); + } + + ServiceReference getServiceRef() { + return m_Aref; + } + } +} \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/UngetServiceTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/UngetServiceTest.java new file mode 100644 index 00000000000..99915787e11 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/UngetServiceTest.java @@ -0,0 +1,75 @@ +/* + * 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.dm.itest.api; + +import org.junit.Assert; +import org.osgi.framework.ServiceReference; +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; + +/** + * When a dm component is removed, check if the service it was bound to is unget. + */ +public class UngetServiceTest extends TestBase { + final Ensure m_ensure = new Ensure(); + + public void testUngetService() { + DependencyManager m = getDM(); + + Component service = m.createComponent() + .setImplementation(new Service()) + .setInterface(Service.class.getName(), null); + m.add(service); + + Client clientImpl = new Client(); + Component client = m.createComponent() + .setImplementation(clientImpl) + .add(m.createServiceDependency().setService(Service.class).setRequired(true).setCallbacks("bind", null)); + + m.add(client); + + m_ensure.waitForStep(1, 5000); + m.remove(client); + + // The client has been removed and the service reference must have been ungotten. + ServiceReference ref = clientImpl.getServiceRef(); + Assert.assertEquals(false, this.context.ungetService(ref)); + + m.remove(service); + } + + public class Service { + } + + public class Client { + ServiceReference m_ref; + + void bind(ServiceReference ref) { + m_ref = ref; + m_ensure.step(1); + } + + ServiceReference getServiceRef() { + return m_ref; + } + } + +} \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle/Activator.java new file mode 100644 index 00000000000..15fe6ebf42e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle/Activator.java @@ -0,0 +1,45 @@ +/* + * 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.dm.itest.bundle; + +import java.util.Dictionary; +import java.util.Hashtable; + +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; + +/** + * @author Felix Project Team + */ +public class Activator extends DependencyActivatorBase { + + @Override + public void init(BundleContext ctx, DependencyManager m) throws Exception { + m.add(createComponent() + .setImplementation(TestComponent.class) + .setInterface(TestService.class.getName(), null)); + + Dictionary props = new Hashtable<>(); + props.put("dm", "dm4"); + m.add(createComponent().setInterface(HelloWorld.class.getName(), props) + .setImplementation(new HelloWorldServiceFactory())); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle/HelloWorld.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle/HelloWorld.java new file mode 100644 index 00000000000..f31693faffa --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle/HelloWorld.java @@ -0,0 +1,24 @@ +/* + * 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.dm.itest.bundle; + +public interface HelloWorld { + + String sayIt(String msg); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle/HelloWorldService.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle/HelloWorldService.java new file mode 100644 index 00000000000..3c3aa6fb029 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle/HelloWorldService.java @@ -0,0 +1,27 @@ +/* + * 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.dm.itest.bundle; + +public class HelloWorldService implements HelloWorld { + + @Override + public String sayIt(String arg) { + return "Hello " + arg; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle/HelloWorldServiceFactory.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle/HelloWorldServiceFactory.java new file mode 100644 index 00000000000..3ee6a497f5b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle/HelloWorldServiceFactory.java @@ -0,0 +1,57 @@ +/* + * 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.dm.itest.bundle; + +import java.util.HashMap; +import java.util.Map; + +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceRegistration; + +public class HelloWorldServiceFactory implements ServiceFactory { + private final Map m_services = new HashMap<>(); + + public void stop() { + synchronized (m_services) { + if (!m_services.isEmpty()) { + throw new IllegalStateException("All services should be closed"); + } + } + } + + @Override + public HelloWorld getService(Bundle bundle, ServiceRegistration registration) { + HelloWorldService service = new HelloWorldService(); + synchronized (m_services) { + HelloWorldService old = m_services.put(registration, service); + assert old == null; + } + return service; + } + + @Override + public void ungetService(Bundle bundle, ServiceRegistration registration, Object service) { + synchronized (m_services) { + HelloWorldService old = m_services.remove(registration); + HelloWorldService serv = (HelloWorldService) service; + assert serv == old; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle/TestComponent.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle/TestComponent.java new file mode 100644 index 00000000000..2ff626f7f2c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle/TestComponent.java @@ -0,0 +1,28 @@ +/* + * 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.dm.itest.bundle; + +/** + * Test Component used by the FELIX2955_ShellCommandTest test. + * + * @author Felix Project Team + */ +public class TestComponent implements TestService { + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle/TestService.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle/TestService.java new file mode 100644 index 00000000000..6393075173a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle/TestService.java @@ -0,0 +1,26 @@ +/* + * 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.dm.itest.bundle; + +/** + * @author Felix Project Team + */ +public interface TestService { + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle2/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle2/Activator.java new file mode 100644 index 00000000000..1e80256fa83 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle2/Activator.java @@ -0,0 +1,41 @@ +/* + * 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.dm.itest.bundle2; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; + +/** + * Activator used to test the FELIX_5268 issue: a service providing component must be removed from + * the OSGI registry in case the component is removed from DependencyManager and if the bundle is starting. + */ +public class Activator extends DependencyActivatorBase { + + @Override + public void init(BundleContext context, DependencyManager dm) throws Exception { + Component c = createComponent() + .setImplementation(AddRemoveService.class) + .setInterface(AddRemoveService.class.getName(), null); + dm.add(c); + dm.remove(c); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle2/AddRemoveService.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle2/AddRemoveService.java new file mode 100644 index 00000000000..17d904ca665 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle2/AddRemoveService.java @@ -0,0 +1,26 @@ +/* + * 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.dm.itest.bundle2; + +/** + * a Service which is added, and then immediately removed from DependencyManager while the bundle is starting. + */ +public class AddRemoveService { + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle2/packageinfo b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle2/packageinfo new file mode 100644 index 00000000000..9ad81f6fa7c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/bundle2/packageinfo @@ -0,0 +1 @@ +version 1.0.0 diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/util/Ensure.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/util/Ensure.java new file mode 100644 index 00000000000..9c347417fae --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/util/Ensure.java @@ -0,0 +1,174 @@ +/* + * 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.dm.itest.util; + +import java.io.PrintStream; + +import org.junit.Assert; + +/** + * Helper class to make sure that steps in a test happen in the correct order. Instantiate + * this class and subsequently invoke step(nr) with steps starting at 1. You + * can also have threads wait until you arrive at a certain step. + * + * @author Felix Project Team + */ +public class Ensure { + private final boolean DEBUG; + private static long INSTANCE = 0; + private static final int RESOLUTION = 100; + private static PrintStream STREAM = System.out; + int step = 0; + private Throwable m_throwable; + + public Ensure() { + this(true); + } + + public Ensure(boolean debug) { + DEBUG = debug; + if (DEBUG) { + INSTANCE++; + } + } + + public void setStream(PrintStream output) { + STREAM = output; + } + + /** + * Mark this point as step nr. + * + * @param nr the step we are in + */ + public synchronized void step(int nr) { + step++; + Assert.assertEquals(nr, step); + if (DEBUG) { + String info = getLineInfo(3); + STREAM.println("[Ensure " + INSTANCE + "] step " + step + " [" + currentThread() + "] " + info); + } + notifyAll(); + } + + private String getLineInfo(int depth) { + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + String info = trace[depth].getClassName() + "." + trace[depth].getMethodName() + ":" + trace[depth].getLineNumber(); + return info; + } + + /** + * Mark this point as the next step. + */ + public synchronized void step() { + step++; + if (DEBUG) { + String info = getLineInfo(3); + STREAM.println("[Ensure " + INSTANCE + "] next step " + step + " [" + currentThread() + "] " + info); + } + notifyAll(); + } + + /** + * Wait until we arrive at least at step nr in the process, or fail if that + * takes more than timeout milliseconds. If you invoke wait on a thread, + * you are effectively assuming some other thread will invoke the step(nr) + * method. + * + * @param nr the step to wait for + * @param timeout the number of milliseconds to wait + */ + public synchronized void waitForStep(int nr, int timeout) { + final int initialTimeout = timeout; + if (DEBUG) { + String info = getLineInfo(3); + STREAM.println("[Ensure " + INSTANCE + "] waiting for step " + nr + " [" + currentThread() + "] " + info); + } + while (step < nr && timeout > 0) { + try { + wait(RESOLUTION); + timeout -= RESOLUTION; + } + catch (InterruptedException e) {} + } + if (step < nr) { + throw new IllegalStateException("Timed out waiting for " + initialTimeout + " ms for step " + nr + ", we are still at step " + step); + } + if (DEBUG) { + String info = getLineInfo(3); + STREAM.println("[Ensure " + INSTANCE + "] arrived at step " + nr + " [" + currentThread() + "] " + info); + } + } + + private String currentThread() { + Thread thread = Thread.currentThread(); + return thread.getId() + " " + thread.getName(); + } + + public static Runnable createRunnableStep(final Ensure ensure, final int nr) { + return new Runnable() { public void run() { ensure.step(nr); }}; + } + + public synchronized void steps(Steps steps) { + steps.next(this); + } + + /** + * Helper class for naming a list of step numbers. If used with the steps(Steps) method + * you can define at which steps in time this point should be passed. That means you can + * check methods that will get invoked multiple times during a test. + */ + public static class Steps { + private final int[] m_steps; + private int m_stepIndex; + + /** + * Create a list of steps and initialize the step counter to zero. + */ + public Steps(int... steps) { + m_steps = steps; + m_stepIndex = 0; + } + + /** + * Ensure we're at the right step. Will throw an index out of bounds exception if we enter this step more often than defined. + */ + public void next(Ensure ensure) { + ensure.step(m_steps[m_stepIndex++]); + } + } + + /** + * Saves a thrown exception that occurred in a different thread. You can only save one exception + * at a time this way. + */ + public synchronized void throwable(Throwable throwable) { + m_throwable = throwable; + } + + /** + * Throws a Throwable if one occurred in a different thread and that thread saved it + * using the throwable() method. + */ + public synchronized void ensure() throws Throwable { + if (m_throwable != null) { + throw m_throwable; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/util/ServiceUtil.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/util/ServiceUtil.java new file mode 100644 index 00000000000..628189c895d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/util/ServiceUtil.java @@ -0,0 +1,176 @@ +/* + * 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.dm.itest.util; + +import java.util.List; + +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + +/** + * OSGi service utilities (copied from dependency manager implementation). + * + * @author Felix Project Team + */ +public class ServiceUtil { + /** + * Returns the service ranking of a service, based on its service reference. If + * the service has a property specifying its ranking, that will be returned. If + * not, the default ranking of zero will be returned. + * + * @param ref the service reference to determine the ranking for + * @return the ranking + */ + public static int getRanking(ServiceReference ref) { + return getRankingAsInteger(ref).intValue(); + } + + /** + * Returns the service ranking of a service, based on its service reference. If + * the service has a property specifying its ranking, that will be returned. If + * not, the default ranking of zero will be returned. + * + * @param ref the service reference to determine the ranking for + * @return the ranking + */ + public static Integer getRankingAsInteger(ServiceReference ref) { + Integer rank = (Integer) ref.getProperty(Constants.SERVICE_RANKING); + if (rank != null) { + return rank; + } + return new Integer(0); + } + + /** + * Returns the service ID of a service, based on its service reference. This + * method is aware of service aspects as defined by the dependency manager and + * will return the ID of the orginal service if you give it an aspect. + * + * @param ref the service reference to determine the service ID of + * @return the service ID + */ + public static long getServiceId(ServiceReference ref) { + return getServiceIdAsLong(ref).longValue(); + } + + /** + * Returns the service ID of a service, based on its service reference. This + * method is aware of service aspects as defined by the dependency manager and + * will return the ID of the orginal service if you give it an aspect. + * + * @param ref the service reference to determine the service ID of + * @return the service ID + */ + public static Long getServiceIdAsLong(ServiceReference ref) { + return getServiceIdObject(ref); + } + + public static Long getServiceIdObject(ServiceReference ref) { + Long aid = (Long) ref.getProperty(DependencyManager.ASPECT); + if (aid != null) { + return aid; + } + Long sid = (Long) ref.getProperty(Constants.SERVICE_ID); + if (sid != null) { + return sid; + } + throw new IllegalArgumentException("Invalid service reference, no service ID found"); + } + + /** + * Determines if the service is an aspect as defined by the dependency manager. + * Aspects are defined by a property and this method will check for its presence. + * + * @param ref the service reference + * @return true if it's an aspect, false otherwise + */ + public static boolean isAspect(ServiceReference ref) { + Long aid = (Long) ref.getProperty(DependencyManager.ASPECT); + return (aid != null); + } + + /** + * Converts a service reference to a string, listing both the bundle it was + * registered from and all properties. + * + * @param ref the service reference + * @return a string representation of the service + */ + public static String toString(ServiceReference ref) { + if (ref == null) { + return "ServiceReference[null]"; + } + else { + StringBuffer buf = new StringBuffer(); + Bundle bundle = ref.getBundle(); + if (bundle != null) { + buf.append("ServiceReference["); + buf.append(bundle.getBundleId()); + buf.append("]{"); + } + else { + buf.append("ServiceReference[unregistered]{"); + } + buf.append(propertiesToString(ref, null)); + buf.append("}"); + return buf.toString(); + } + } + + /** + * Converts the properties of a service reference to a string. + * + * @param ref the service reference + * @param exclude a list of properties to exclude, or null to show everything + * @return a string representation of the service properties + */ + public static String propertiesToString(ServiceReference ref, List exclude) { + StringBuffer buf = new StringBuffer(); + String[] keys = ref.getPropertyKeys(); + for (int i = 0; i < keys.length; i++) { + if (i > 0) { + buf.append(','); + } + buf.append(keys[i]); + buf.append('='); + Object val = ref.getProperty(keys[i]); + if (exclude == null || !exclude.contains(val)) { + if (val instanceof String[]) { + String[] valArray = (String[]) val; + StringBuffer valBuf = new StringBuffer(); + valBuf.append('{'); + for (int j = 0; j < valArray.length; j++) { + if (valBuf.length() > 1) { + valBuf.append(','); + } + valBuf.append(valArray[j].toString()); + } + valBuf.append('}'); + buf.append(valBuf); + } + else { + buf.append(val.toString()); + } + } + } + return buf.toString(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/util/TestBase.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/util/TestBase.java new file mode 100644 index 00000000000..78da1484a0d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/util/TestBase.java @@ -0,0 +1,364 @@ +/* + * 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.dm.itest.util; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentExecutorFactory; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.log.LogService; + +import junit.framework.TestCase; + +/** + * Base class for all integration tests. + * + * @author Felix Project Team + */ +public abstract class TestBase extends TestCase implements LogService, FrameworkListener { + // Default OSGI log service level. + protected final static int LOG_LEVEL = LogService.LOG_WARNING; + + // optional thread pool used by parallel dependency managers + protected volatile ForkJoinPool m_threadPool; + + // flag used to check if the threadpool must be used for a given test. + protected volatile boolean m_parallel; + + // Flag used to check if some errors have been logged during the execution of a given test. + private volatile boolean m_errorsLogged; + + // We implement OSGI log service. + protected ServiceRegistration logService; + + // Our bundle context + protected BundleContext context; + + // Our dependency manager used to create test components. + protected volatile DependencyManager m_dm; + + // The Registration for the DM threadpool. + private ServiceRegistration m_componentExecutorFactoryReg; + + public TestBase() { + } + + protected void setParallel() { + m_parallel = true; + } + + public void setUp() throws Exception { + warn("Setting up test " + getClass().getName()); + context = FrameworkUtil.getBundle(this.getClass()).getBundleContext(); + Hashtable props = new Hashtable<>(); + props.put(Constants.SERVICE_RANKING, new Integer(Integer.MAX_VALUE)); + logService = context.registerService(LogService.class.getName(), this, props); + context.addFrameworkListener(this); + m_dm = new DependencyManager(context); + if (m_parallel) { + warn("Using threadpool ..."); + m_threadPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors()); + m_componentExecutorFactoryReg = context.registerService(ComponentExecutorFactory.class.getName(), + new ComponentExecutorFactory() { + @Override + public Executor getExecutorFor(Component component) { + return m_threadPool; + } + }, + null); + } + } + + public void tearDown() throws Exception { + warn("Tearing down test " + getClass().getName()); + logService.unregister(); + context.removeFrameworkListener(this); + clearComponents(); + if (m_parallel && m_componentExecutorFactoryReg != null) { + m_componentExecutorFactoryReg.unregister(); + m_threadPool.shutdown(); + try { + m_threadPool.awaitTermination(60, TimeUnit.SECONDS); + } catch (InterruptedException e) { + } + } + Assert.assertFalse(errorsLogged()); + } + + protected DependencyManager getDM() { + return m_dm; + } + + protected void clearComponents() { + m_dm.clear(); + warn("All component cleared."); + } + + /** + * Creates and provides an Ensure object with a name service property into the OSGi service registry. + */ + protected ServiceRegistration register(Ensure e, String name) { + Hashtable props = new Hashtable(); + props.put("name", name); + return context.registerService(Ensure.class.getName(), e, props); + } + + /** + * Helper method used to stop a given bundle. + * + * @param symbolicName + * the symbolic name of the bundle to be stopped. + */ + protected void stopBundle(String symbolicName) { + // Stop the test.annotation bundle + boolean found = false; + for (Bundle b : context.getBundles()) { + if (b.getSymbolicName().equals(symbolicName)) { + try { + found = true; + b.stop(); + } catch (BundleException e) { + e.printStackTrace(); + } + } + } + if (!found) { + throw new IllegalStateException("bundle " + symbolicName + " not found"); + } + } + + /** + * Helper method used to start a given bundle. + * + * @param symbolicName + * the symbolic name of the bundle to be started. + */ + protected void startBundle(String symbolicName) { + // Stop the test.annotation bundle + boolean found = false; + for (Bundle b : context.getBundles()) { + if (b.getSymbolicName().equals(symbolicName)) { + try { + found = true; + b.start(); + } catch (BundleException e) { + e.printStackTrace(); + } + } + } + if (!found) { + throw new IllegalStateException("bundle " + symbolicName + " not found"); + } + } + + /** + * Helper method used to get a given bundle. + * + * @param symbolicName + * the symbolic name of the bundle to get. + */ + protected Bundle getBundle(String symbolicName) { + for (Bundle b : context.getBundles()) { + if (b.getSymbolicName().equals(symbolicName)) { + return b; + } + } + throw new IllegalStateException("bundle " + symbolicName + " not found"); + } + + /** + * Suspend the current thread for a while. + * + * @param n + * the number of milliseconds to wait for. + */ + protected void sleep(int ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException e) { + } + } + + /** + * Helper method used to convert a dictionary which with untyped keys to a dictionary having a String key. + * (this method is useful when converting a Properties object into a compatible Dictionary + * object that is often needed in OSGI R6 API. + */ + @SuppressWarnings("unchecked") + public static Dictionary toR6Dictionary(Dictionary properties) { + return (Dictionary) properties; + } + + public void log(int level, String message) { + checkError(level, null); + if (LOG_LEVEL >= level) { + System.out.println(getLevel(level) + " - " + Thread.currentThread().getName() + " : " + message); + } + } + + public void log(int level, String message, Throwable exception) { + checkError(level, exception); + if (LOG_LEVEL >= level) { + StringBuilder sb = new StringBuilder(); + sb.append(getLevel(level) + " - " + Thread.currentThread().getName() + " : "); + sb.append(message); + parse(sb, exception); + System.out.println(sb.toString()); + } + } + + public void log(ServiceReference sr, int level, String message) { + checkError(level, null); + if (LOG_LEVEL >= level) { + StringBuilder sb = new StringBuilder(); + sb.append(getLevel(level) + " - " + Thread.currentThread().getName() + " : "); + sb.append(message); + System.out.println(sb.toString()); + } + } + + public void log(ServiceReference sr, int level, String message, Throwable exception) { + checkError(level, exception); + if (LOG_LEVEL >= level) { + StringBuilder sb = new StringBuilder(); + sb.append(getLevel(level) + " - " + Thread.currentThread().getName() + " : "); + sb.append(message); + parse(sb, exception); + System.out.println(sb.toString()); + } + } + + protected boolean errorsLogged() { + return m_errorsLogged; + } + + private void parse(StringBuilder sb, Throwable t) { + if (t != null) { + sb.append(" - "); + StringWriter buffer = new StringWriter(); + PrintWriter pw = new PrintWriter(buffer); + t.printStackTrace(pw); + sb.append(buffer.toString()); + m_errorsLogged = true; + } + } + + private String getLevel(int level) { + switch (level) { + case LogService.LOG_DEBUG : + return "DEBUG"; + case LogService.LOG_ERROR : + return "ERROR"; + case LogService.LOG_INFO : + return "INFO"; + case LogService.LOG_WARNING : + return "WARN"; + default : + return ""; + } + } + + private void checkError(int level, Throwable exception) { + if (level <= LOG_ERROR) { + m_errorsLogged = true; + } + if (exception != null) { + m_errorsLogged = true; + } + } + + public void frameworkEvent(FrameworkEvent event) { + int eventType = event.getType(); + String msg = getFrameworkEventMessage(eventType); + int level = (eventType == FrameworkEvent.ERROR) ? LOG_ERROR : LOG_WARNING; + if (msg != null) { + log(level, msg, event.getThrowable()); + } else { + log(level, "Unknown fwk event: " + event); + } + } + + private String getFrameworkEventMessage(int event) { + switch (event) { + case FrameworkEvent.ERROR : + return "FrameworkEvent: ERROR"; + case FrameworkEvent.INFO : + return "FrameworkEvent INFO"; + case FrameworkEvent.PACKAGES_REFRESHED : + return "FrameworkEvent: PACKAGE REFRESHED"; + case FrameworkEvent.STARTED : + return "FrameworkEvent: STARTED"; + case FrameworkEvent.STARTLEVEL_CHANGED : + return "FrameworkEvent: STARTLEVEL CHANGED"; + case FrameworkEvent.WARNING : + return "FrameworkEvent: WARNING"; + default : + return null; + } + } + + protected void warn(String msg, Object ... params) { + if (LOG_LEVEL >= LogService.LOG_WARNING) { + log(LogService.LOG_WARNING, params.length > 0 ? String.format(msg, params) : msg); + } + } + + @SuppressWarnings("unused") + protected void info(String msg, Object ... params) { + if (LOG_LEVEL >= LogService.LOG_INFO) { + log(LogService.LOG_INFO, params.length > 0 ? String.format(msg, params) : msg); + } + } + + @SuppressWarnings("unused") + protected void debug(String msg, Object ... params) { + if (LOG_LEVEL >= LogService.LOG_DEBUG) { + log(LogService.LOG_DEBUG, params.length > 0 ? String.format(msg, params) : msg); + } + } + + protected void error(String msg, Object ... params) { + log(LogService.LOG_ERROR, params.length > 0 ? String.format(msg, params) : msg); + } + + protected void error(String msg, Throwable err, Object ... params) { + log(LogService.LOG_ERROR, params.length > 0 ? String.format(msg, params) : msg, err); + } + + protected void error(Throwable err) { + log(LogService.LOG_ERROR, "error", err); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/util/packageinfo b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/util/packageinfo new file mode 100644 index 00000000000..e2525561ab2 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/util/packageinfo @@ -0,0 +1 @@ +version 1.0.0 \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/test/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.itest/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/.classpath b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/.classpath new file mode 100644 index 00000000000..7a6fc254361 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/.gitignore new file mode 100644 index 00000000000..57b341172a1 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/.gitignore @@ -0,0 +1,2 @@ +/bin/ +/generated/ diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/.project b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/.project new file mode 100644 index 00000000000..c302837df3a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/.project @@ -0,0 +1,23 @@ + + + org.apache.felix.dependencymanager.lambda.itest + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/.settings/org.eclipse.jdt.core.prefs b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..144c60c0ff4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/bnd.bnd b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/bnd.bnd new file mode 100644 index 00000000000..118cfb15519 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/bnd.bnd @@ -0,0 +1,56 @@ +# +# 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. +# +-buildpath: \ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.dependencymanager.lambda;version=latest,\ + ${junit},\ + osgi.core;version=6.0,\ + osgi.cmpn;version=6.0 +-runbundles: \ + org.apache.servicemix.bundles.junit;version=4.12,\ + org.mockito.mockito-core;version='[1.10.19,1.10.20)',\ + org.objenesis;version='[2.2.0,2.2.1)',\ + org.apache.felix.metatype;version=1.0.10,\ + org.apache.felix.log;version=1.0.1,\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.configadmin;version=1.8.8,\ + org.apache.felix.dependencymanager.lambda;version=latest,\ + org.apache.felix.gogo.runtime;version=1.0.6 + +-runee: JavaSE-1.8 +-runfw: org.apache.felix.framework;version='[5.6.10,5.6.10]' +-runvm: -ea +Bundle-Version: 0.0.0.${tstamp} +Test-Cases: ${classes;CONCRETE;EXTENDS;junit.framework.TestCase} +javac.source: 1.8 +javac.target: 1.8 + +# all tests are configured to assume that dependencies are required by default +-runproperties: \ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.dependencymanager.lambda.defaultRequiredDependency=org.apache.felix.dm.lambda.itest,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true,\ + gosh.args=--noshutdown + +# we do not release this project in binary distribution. +-releaserepo: +-baseline: + +-sub: *.bnd \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/build.gradle b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/build.gradle new file mode 100644 index 00000000000..e6d9792ce91 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/build.gradle @@ -0,0 +1,22 @@ +/* + * 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. + */ + +tasks.withType(JavaCompile) { + options.compilerArgs << "-parameters" << "-Xdiags:verbose" +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/lifecycle.bnd b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/lifecycle.bnd new file mode 100644 index 00000000000..9347ea60e12 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/lifecycle.bnd @@ -0,0 +1,18 @@ +# +# 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. +# +Export-Package: org.apache.felix.dm.lambda.itest.lifecycle +Bundle-Activator: org.apache.felix.dm.lambda.itest.lifecycle.Activator \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AbstractServiceDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AbstractServiceDependencyTest.java new file mode 100644 index 00000000000..92ecfd634a8 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AbstractServiceDependencyTest.java @@ -0,0 +1,101 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; + +/** + * @author Felix Project Team + */ +public class AbstractServiceDependencyTest extends TestBase { + public void testAbstractClassDependency() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + + Component sp = + component(m).provides(ServiceAbstract.class).impl(new ServiceProvider(e)).build(); + Component sc = + component(m).impl(new ServiceConsumer(e)).withSvc(ServiceAbstract.class, srv -> srv.add(ServiceConsumer::bind).remove(ServiceConsumer::unbind)).build(); + + m.add(sp); + m.add(sc); + m.remove(sp); + // ensure we executed all steps inside the component instance + e.step(8); + m.clear(); + } + + static abstract class ServiceAbstract { + public abstract void invoke(); + } + + static class ServiceProvider extends ServiceAbstract { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + + public void start() { + m_ensure.step(1); + } + + public void invoke() { + m_ensure.step(4); + } + + public void stop() { + m_ensure.step(7); + } + } + + static class ServiceConsumer { + private volatile ServiceAbstract m_service; + private final Ensure m_ensure; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void bind(ServiceAbstract service) { + m_ensure.step(2); + m_service = service; + } + + public void start() { + m_ensure.step(3); + m_service.invoke(); + } + + public void stop() { + m_ensure.step(5); + } + + public void unbind(ServiceAbstract service) { + System.out.println("UNBINDDDDDDDDDDDDDDDDDDDDDDDDDDD"); + Assert.assertEquals(m_service, service); + m_ensure.step(6); + } + } +} \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterAndConsumerTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterAndConsumerTest.java new file mode 100644 index 00000000000..1560d2deea5 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterAndConsumerTest.java @@ -0,0 +1,103 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.adapter; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; + +/** + * @author Felix Project Team + */ +public class AdapterAndConsumerTest extends TestBase { + + public void testServiceWithAdapterAndConsumer() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + Component provider = component(m).provides(OriginalService.class).impl(new ServiceProvider(e)).build(); + Component consumer = component(m).impl(new ServiceConsumer(e)).withSvc(AdaptedService.class, true).build(); + Component adapter = adapter(m, OriginalService.class).provides(AdaptedService.class).impl(ServiceAdapter.class).build(); + + // add the provider and the adapter + m.add(provider); + m.add(adapter); + // add a consumer that will invoke the adapter + // which will in turn invoke the original provider + m.add(consumer); + // now validate that both have been invoked in the right order + e.waitForStep(2, 5000); + // remove the provider again + m.remove(provider); + // ensure that the consumer is stopped + e.waitForStep(3, 5000); + // remove adapter and consumer + m.remove(adapter); + m.remove(consumer); + } + + static interface OriginalService { + public void invoke(); + } + + static interface AdaptedService { + public void invoke(); + } + + static class ServiceProvider implements OriginalService { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke() { + m_ensure.step(2); + } + } + + public static class ServiceAdapter implements AdaptedService { + private volatile OriginalService m_originalService; + + public void start() { System.out.println("start"); } + public void stop() { System.out.println("stop"); } + public void invoke() { + m_originalService.invoke(); + } + } + + static class ServiceConsumer { + private volatile AdaptedService m_service; + private final Ensure m_ensure; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + public void start() { + m_ensure.step(1); + m_service.invoke(); + } + public void stop() { + m_ensure.step(3); + } + } +} + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterNoAutoConfigIfInstanceCallbackIsUsed.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterNoAutoConfigIfInstanceCallbackIsUsed.java new file mode 100644 index 00000000000..95c32979549 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterNoAutoConfigIfInstanceCallbackIsUsed.java @@ -0,0 +1,93 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.adapter; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; + +/** + * This tests validates that autoconfiguration of adapted service on adapter class field(s) is not + * enabled when an instance callback is used when injected adapted service. + */ +public class AdapterNoAutoConfigIfInstanceCallbackIsUsed extends TestBase { + static Ensure m_e; + + public void testNoAutoConfigIfInstanceCallbackIsUsed() { + m_e = new Ensure(); + DependencyManager m = getDM(); + + // Declare S1 service + component(m, c -> c.impl(S1Impl.class).provides(S1.class)); + + // Declare S1 adapter + S1AdapterCallback s1AdapterCB = new S1AdapterCallback(); + adapter(m, S1.class, a -> a.impl(S1Adapter.class).callbackInstance(s1AdapterCB).add("set")); + + // At this point, the s1AdapterCB.set(S1 s1) method should be called, and s1Adapter.start() method should then be called. + // but s1 should not be injected on s1Adapter class fields. + + m_e.waitForStep(3, 5000); + m.clear(); + } + + public void testNoAutoConfigIfInstanceCallbackIsUsedRef() { + m_e = new Ensure(); + DependencyManager m = getDM(); + + // Declare S1 service + component(m, c -> c.impl(S1Impl.class).provides(S1.class)); + + // Declare S1 adapter + S1AdapterCallback s1AdapterCB = new S1AdapterCallback(); + adapter(m, S1.class, a -> a.impl(S1Adapter.class).add(s1AdapterCB::set)); + + // At this point, the s1AdapterCB.set(S1 s1) method should be called, and s1Adapter.start() method should then be called. + // but s1 should not be injected on s1Adapter class fields. + + m_e.waitForStep(3, 5000); + m.clear(); + } + + public interface S1 { + } + + public static class S1Impl implements S1 { + } + + public static class S1Adapter { + volatile S1 m_s1; // should not be injected by reflection + + void start() { + m_e.step(2); + Assert.assertNull(m_s1); + m_e.step(3); + } + } + + public static class S1AdapterCallback { + void set(S1 s1) { + Assert.assertNotNull(s1); + m_e.step(1); + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithCallbackInstanceTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithCallbackInstanceTest.java new file mode 100644 index 00000000000..4dd4c76408f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithCallbackInstanceTest.java @@ -0,0 +1,195 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.adapter; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import java.util.Hashtable; +import java.util.Map; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.ServiceRegistration; + +/** + * @author Felix Project Team + */ +public class AdapterWithCallbackInstanceTest extends TestBase { + + public void testServiceWithAdapterAndConsumer() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + ServiceProvider serviceProvider = new ServiceProvider(e); + Component provider = component(m).provides(OriginalService.class).impl(serviceProvider).build(); + Component consumer = component(m).impl(new ServiceConsumer(e)).withSvc(AdaptedService.class, true).build(); + + ServiceAdapterCallbackInstance callbackInstance = new ServiceAdapterCallbackInstance(e); + Component adapter = adapter(m, OriginalService.class) + .provides(AdaptedService.class).impl(new ServiceAdapter(e)).propagate(true) + .autoConfig("m_originalService") + .callbackInstance(callbackInstance).add("set").change("changed").remove("unset") + .build(); + + // add the provider and the adapter + m.add(provider); + m.add(adapter); + // Checks if the callbackInstances is called, and if the adapter start method is called + e.waitForStep(2, 5000); + + // add a consumer that will invoke the adapter + // which will in turn invoke the original provider + m.add(consumer); + // now validate that both have been invoked in the right order + e.waitForStep(4, 5000); + + // change the service properties of the provider, and check that the adapter callback instance is changed. + serviceProvider.changeServiceProperties(); + e.waitForStep(5, 5000); + + // remove the provider + m.remove(provider); + // ensure that the consumer is stopped, the adapter callback is called in its unset method, and the adapter is stopped. + e.waitForStep(8, 5000); + // remove adapter and consumer + m.remove(adapter); + m.remove(consumer); + } + + public void testServiceWithAdapterAndConsumerRef() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + ServiceProvider serviceProvider = new ServiceProvider(e); + Component provider = component(m).provides(OriginalService.class).impl(serviceProvider).build(); + Component consumer = component(m).impl(new ServiceConsumer(e)).withSvc(AdaptedService.class, true).build(); + + ServiceAdapterCallbackInstance callbackInstance = new ServiceAdapterCallbackInstance(e); + Component adapter = adapter(m, OriginalService.class, adp -> adp + .provides(AdaptedService.class).impl(new ServiceAdapter(e)) + .autoAdd(false).propagate(true) + .autoConfig("m_originalService") + .add(callbackInstance::set).change(callbackInstance::changed).remove(callbackInstance::unset)); + + // add the provider and the adapter + m.add(provider); + m.add(adapter); + // Checks if the callbackInstances is called, and if the adapter start method is called + e.waitForStep(2, 5000); + + // add a consumer that will invoke the adapter + // which will in turn invoke the original provider + m.add(consumer); + // now validate that both have been invoked in the right order + e.waitForStep(4, 5000); + + // change the service properties of the provider, and check that the adapter callback instance is changed. + serviceProvider.changeServiceProperties(); + e.waitForStep(5, 5000); + + // remove the provider + m.remove(provider); + // ensure that the consumer is stopped, the adapter callback is called in its unset method, and the adapter is stopped. + e.waitForStep(8, 5000); + // remove adapter and consumer + m.remove(adapter); + m.remove(consumer); + } + + static interface OriginalService { + public void invoke(); + } + + static interface AdaptedService { + public void invoke(); + } + + static class ServiceProvider implements OriginalService { + private final Ensure m_ensure; + private volatile ServiceRegistration m_registration; // auto injected when started. + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void changeServiceProperties() { + Hashtable props = new Hashtable<>(); + props.put("foo", "bar"); + m_registration.setProperties(props); + } + public void invoke() { + m_ensure.step(4); + } + } + + public static class ServiceAdapter implements AdaptedService { + private volatile OriginalService m_originalService; + private final Ensure m_ensure; + + public ServiceAdapter(Ensure e) { + m_ensure = e; + } + + public void start() { m_ensure.step(2); } + public void stop() { m_ensure.step(7); } + public void invoke() { + m_originalService.invoke(); + } + } + + public static class ServiceAdapterCallbackInstance { + private final Ensure m_ensure; + public ServiceAdapterCallbackInstance(Ensure e) { + m_ensure = e; + } + + public void set(OriginalService m_originalService, Map props) { + m_ensure.step(1); + } + + public void changed(OriginalService m_originalService, Map props) { + Assert.assertEquals("bar", props.get("foo")); + m_ensure.step(5); + } + + public void unset(OriginalService m_originalService, Map props) { + m_ensure.step(8); + } + } + + static class ServiceConsumer { + private volatile AdaptedService m_service; + private final Ensure m_ensure; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + public void start() { + m_ensure.step(3); + m_service.invoke(); + } + public void stop() { + m_ensure.step(6); + } + } +} + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithExtraDependenciesTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithExtraDependenciesTest.java new file mode 100644 index 00000000000..a62878b006f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithExtraDependenciesTest.java @@ -0,0 +1,141 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.adapter; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; + + +/** + * @author Felix Project Team + */ +public class AdapterWithExtraDependenciesTest extends TestBase { + + public void testAdapterWithExtraDependenciesAndCallbacks() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + // create a service adapter that adapts to services S1 and has an optional dependency on services S2 + Component sa = adapter(m, S1.class).impl(SA.class).withSvc(S2.class, s2 -> s2.add("add").remove("remove")).build(); + m.add(sa); + + // create a service S1, which triggers the creation of the first adapter instance (A1) + Component s1 = component(m).provides(S1.class).impl(new S1Impl()).build(); + m.add(s1); + + // create a service S2, which will be added to A1 + Component s2 = component(m).provides(S2.class).impl(new S2Impl(e)).build(); + m.add(s2); + + // create a second service S1, which triggers the creation of the second adapter instance (A2) + Component s1b = component(m).provides(S1.class).impl(new S1Impl()).build(); + m.add(s1b); + + // observe that S2 is also added to A2 + e.waitForStep(2, 5000); + + // remove S2 again + m.remove(s2); + + // make sure both adapters have their "remove" callbacks invoked + e.waitForStep(4, 5000); + + m.remove(s1); + m.remove(sa); + m.clear(); + } + + public void testAdapterWithExtraDependenciesAndCallbacksRef() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + // create a service adapter that adapts to services S1 and has an optional dependency on services S2 + Component sa = adapter(m, S1.class).impl(SA.class).withSvc(S2.class, s2 -> s2.add(SA::add).remove(SA::remove)).build(); + m.add(sa); + + // create a service S1, which triggers the creation of the first adapter instance (A1) + Component s1 = component(m).provides(S1.class).impl(new S1Impl()).build(); + m.add(s1); + + // create a service S2, which will be added to A1 + Component s2 = component(m).provides(S2.class).impl(new S2Impl(e)).build(); + m.add(s2); + + // create a second service S1, which triggers the creation of the second adapter instance (A2) + Component s1b = component(m).provides(S1.class).impl(new S1Impl()).build(); + m.add(s1b); + + // observe that S2 is also added to A2 + e.waitForStep(2, 5000); + + // remove S2 again + m.remove(s2); + + // make sure both adapters have their "remove" callbacks invoked + e.waitForStep(4, 5000); + + m.remove(s1); + m.remove(sa); + m.clear(); + } + + static interface S1 { + } + static interface S2 { + public void invoke(); + } + static class S1Impl implements S1 { + } + static class S2Impl implements S2 { + + private final Ensure m_e; + + public S2Impl(Ensure e) { + m_e = e; + } + + public void invoke() { + m_e.step(); + } + } + + public static class SA { + volatile S2 s2; + + public SA() { + System.out.println("Adapter created"); + } + public void init() { + System.out.println("Adapter init " + s2); + } + public void add(S2 s) { + System.out.println("adding " + s); + s.invoke(); + } + public void remove(S2 s) { + System.out.println("removing " + s); + s.invoke(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithInstanceBoundDependencyParallelTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithInstanceBoundDependencyParallelTest.java new file mode 100644 index 00000000000..c332654daa8 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithInstanceBoundDependencyParallelTest.java @@ -0,0 +1,28 @@ +/* + * 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.dm.lambda.itest; + +/** + * @author Felix Project Team + */ +public class AdapterWithInstanceBoundDependencyParallelTest extends AdapterWithInstanceBoundDependencyTest { + public AdapterWithInstanceBoundDependencyParallelTest() { + setParallel(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithInstanceBoundDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithInstanceBoundDependencyTest.java new file mode 100644 index 00000000000..c3df8d571b9 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithInstanceBoundDependencyTest.java @@ -0,0 +1,147 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.adapter; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; + + +/** + * @author Felix Project Team + */ +public class AdapterWithInstanceBoundDependencyTest extends TestBase { + + public void testInstanceBoundDependency() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + + Component sp = component(m).provides(ServiceInterface.class).impl(new ServiceProvider(e)).build(); + Component sp2 = component(m).provides(ServiceInterface2.class).impl(new ServiceProvider2(e)).build(); + Component sc = component(m).impl(new ServiceConsumer(e)).autoAdd(false).withSvc(ServiceInterface3.class, true).build(); + Component sa = adapter(m, ServiceInterface.class).provides(ServiceInterface3.class).impl(new ServiceAdapter(e)).build(); + m.add(sc); + m.add(sp); + m.add(sp2); + m.add(sa); + e.waitForStep(5, 15000); + // cleanup + m.remove(sa); + m.remove(sp2); + m.remove(sp); + m.remove(sc); + m.clear(); + e.waitForStep(9, 5000); // make sure all components are stopped + } + + static interface ServiceInterface { + public void invoke(); + } + + static interface ServiceInterface2 { + public void invoke(); + } + + static interface ServiceInterface3 { + public void invoke(); + } + + static class ServiceProvider2 implements ServiceInterface2 { + private final Ensure m_ensure; + + public ServiceProvider2(Ensure ensure) { + m_ensure = ensure; + } + + public void invoke() { + m_ensure.step(4); + } + + public void stop() { + m_ensure.step(); + } + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke() { + m_ensure.step(5); + } + public void stop() { + m_ensure.step(); + } + } + + static class ServiceAdapter implements ServiceInterface3 { + private Ensure m_ensure; + private volatile ServiceInterface m_originalService; + private volatile ServiceInterface2 m_injectedService; + private volatile Component m_component; + + public ServiceAdapter(Ensure e) { + m_ensure = e; + } + public void init() { + m_ensure.step(1); + component(m_component, c->c.withSvc(ServiceInterface2.class, true)); + } + public void start() { + m_ensure.step(2); + } + public void invoke() { + m_ensure.step(3); + m_injectedService.invoke(); + m_originalService.invoke(); + } + + public void stop() { + m_ensure.step(); + } + } + + static class ServiceConsumer implements Runnable { + volatile ServiceInterface3 m_service; + final Ensure m_ensure; + + ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void init() { + Thread t = new Thread(this); + t.start(); + } + + public void run() { + m_service.invoke(); + } + public void stop() { + m_ensure.step(); + } + } +} + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithModifiedInstanceBoundDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithModifiedInstanceBoundDependencyTest.java new file mode 100644 index 00000000000..d891c4ee787 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithModifiedInstanceBoundDependencyTest.java @@ -0,0 +1,169 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.adapter; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import java.util.Map; +import java.util.Properties; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; + +/** + * Test for FELIX-4334 issue. + * + * Three components: A, B and C + * + * - A provided with property foo=bar + * - B adapts A, B has no filters on A, and B.init() method adds an instance bound required dependency to C. + * - C depends on A(foo=bar) + * - Now someone modifies the service properties of A: foo=bar2 + * - As a result of that, C becomes unavailable and is unbound from B. + * - Since B has an instance bound required dependency to C: B should not be destroyed: it should be called in B.stop(), B.remove(C), B.change(A, "foo=bar2)) + * + * @author Felix Project Team + */ +public class AdapterWithModifiedInstanceBoundDependencyTest extends TestBase { + public static interface A { + } + + static class AImpl implements A { + final Ensure m_e; + AImpl(Ensure e) { + m_e = e; + } + } + + public static interface C { + } + + static class CImpl implements C { + volatile A m_a; + } + + public static interface B { + } + + public static class BImpl implements B { + final Ensure m_e; + volatile A m_a; + volatile C m_c; + + BImpl(Ensure e) { + m_e = e; + } + + void init(Component comp) { + m_e.step(2); + component(comp, c->c.withSvc(C.class, s->s.add("addC").remove("removeC"))); + } + + void addA(A a, Map properties) { + m_e.step(1); + } + + public void addC(C c) { + m_e.step(3); + } + + public void start() { + m_e.step(4); + } + + public void stop() { // C becomes unsatisfied when A properties are changed to foo=bar2 + m_e.step(5); + } + + public void removeC(C c) { + m_e.step(6); + } + + public void changeA(A a, Map properties) { + Assert.assertEquals("bar2", properties.get("foo")); + m_e.step(7); + } + + public void destroy() { + m_e.step(8); + } + + public void removeA(A a, Map properties) { + m_e.step(9); + } + } + + public void testAdapterWithChangedInstanceBoundDependency() { + DependencyManager m = getDM(); + Ensure e = new Ensure(); + + Component a = component(m).impl(new AImpl(e)).provides(A.class).properties("foo", "bar").build(); + Component b = adapter(m, A.class).provides(B.class).impl(new BImpl(e)).add("addA").change("changeA").remove("removeA").build(); + Component c = component(m).impl(new CImpl()).provides(C.class).withSvc(A.class, "(foo=bar)", true).build(); + + m.add(a); + m.add(c); + m.add(b); + + e.waitForStep(4, 5000); + + System.out.println("changing A props ..."); + Properties props = new Properties(); + props.put("foo", "bar2"); + a.setServiceProperties(props); + + e.waitForStep(7, 5000); + + m.remove(c); + m.remove(a); + m.remove(b); + + e.waitForStep(9, 5000); + } + + public void testAdapterWithChangedInstanceBoundDependencyRef() { + DependencyManager m = getDM(); + Ensure e = new Ensure(); + + Component a = component(m).impl(new AImpl(e)).provides(A.class).properties("foo", "bar").build(); + Component b = adapter(m, A.class).impl(new BImpl(e)).provides(B.class).add(BImpl::addA).change(BImpl::changeA).remove(BImpl::removeA).build(); + Component c = component(m).impl(new CImpl()).provides(C.class).withSvc(A.class, s -> s.filter("(foo=bar)")).build(); + + m.add(a); + m.add(c); + m.add(b); + + e.waitForStep(4, 5000); + + System.out.println("changing A props ..."); + Properties props = new Properties(); + props.put("foo", "bar2"); + a.setServiceProperties(props); + + e.waitForStep(7, 5000); + + m.remove(c); + m.remove(a); + m.remove(b); + + e.waitForStep(9, 5000); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithPropagationTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithPropagationTest.java new file mode 100644 index 00000000000..ae7799f5315 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithPropagationTest.java @@ -0,0 +1,123 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.adapter; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import java.util.Hashtable; +import java.util.Map; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; + +/** + * Checks if a service adapter propagates its service properties, if + * the adapted service properties are changed: + * + * S1Impl provides S + * S1Adapter adapts S1Impl(S) to S2 + * S3 depends on S2 + * + * So, when S1Impl service properties are changed, S1Adapter shall propagate the changed properties to S3. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class AdapterWithPropagationTest extends TestBase { + public static interface S1 {} + + static class S1Impl implements S1 { + private Ensure m_ensure; + public S1Impl(Ensure e) { + m_ensure = e; + } + + public void start() { + m_ensure.step(1); + } + } + + public static interface S2 {} + + static class S1Adapter implements S2 { + private Ensure m_ensure; + public S1Adapter(Ensure e) { + m_ensure = e; + } + + public void add(Map properties, S1 s1) { + Assert.assertTrue("v1".equals(properties.get("p1"))); + Assert.assertTrue("v2overriden".equals(properties.get("p2"))); + m_ensure.step(2); + } + + public void change(Map properties, S1 s1) { + Assert.assertTrue("v1modified".equals(properties.get("p1"))); + Assert.assertTrue("v2overriden".equals(properties.get("p2"))); + m_ensure.step(4); + } + } + + static class S3 { + private final Ensure m_ensure; + + public S3(Ensure e) { + m_ensure = e; + } + + public void add(Map properties, S2 s2) { + Assert.assertTrue("v1".equals(properties.get("p1"))); + Assert.assertTrue("v2".equals(properties.get("p2"))); // s1 should not override adapter service properties + m_ensure.step(3); + } + + public void change(Map properties, S2 s2) { + Assert.assertTrue("v1modified".equals(properties.get("p1"))); + Assert.assertTrue("v2".equals(properties.get("p2"))); // s1 should not override adapter service properties + m_ensure.step(5); + } + } + + public void testAdapterWithPropagation() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + Component s1 = component(m).impl(new S1Impl(e)).provides(S1.class).properties("p1", "v1", "p2", "v2overriden").build(); + Component s1Adapter = adapter(m, S1.class).add("add").change("change").impl(new S1Adapter(e)).provides(S2.class).properties("p2", "v2").build(); + Component s3 = component(m).impl(new S3(e)).withSvc(S2.class, s -> s.add("add").change("change")).build(); + + m.add(s1); + m.add(s1Adapter); + m.add(s3); + + e.waitForStep(3, 5000); + + Hashtable s1Properties = new Hashtable(); + s1Properties.put("p1", "v1modified"); + s1Properties.put("p2", "v2overriden"); + s1.setServiceProperties(s1Properties); + + e.waitForStep(5, 5000); + + m.clear(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithoutPropagationTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithoutPropagationTest.java new file mode 100644 index 00000000000..3fb90d1232e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AdapterWithoutPropagationTest.java @@ -0,0 +1,163 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.adapter; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import java.util.Dictionary; +import java.util.Hashtable; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.ServiceRegistration; + +/** + * @author Felix Project Team + */ +public class AdapterWithoutPropagationTest extends TestBase { + + public void testAdapterNoPropagate() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + // The provider has a "foo=bar" property + ServiceProvider serviceProvider = new ServiceProvider(e); + Component provider = component(m).provides(OriginalService.class).properties("foo", "bar").impl(serviceProvider).build(); + + // The Adapter will see the "foo=bar" property from the adaptee + Component adapter = adapter(m, OriginalService.class) + .propagate(false).add("set").change("change").provides(AdaptedService.class).impl(new ServiceAdapter(e)).build(); + + // The consumer depends on the AdaptedService, but won't see foo=bar property from the adaptee + Component consumer = component(m) + .impl(new ServiceConsumer(e)).withSvc(AdaptedService.class, b -> b.add("set").change("change")).build(); + + // add the provider and the adapter + m.add(provider); + m.add(adapter); + // Checks if the adapter has been started and has seen the adaptee properties + e.waitForStep(1, 5000); + + // add a consumer that must not see the adaptee service properties + m.add(consumer); + e.waitForStep(2, 5000); + + // change the service properties of the provider, and check that the adapter callback instance is caled. + serviceProvider.changeServiceProperties(); + e.waitForStep(3, 5000); + + // cleanup + m.clear(); + } + + public void testAdapterNoPropagateRef() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + // The provider has a "foo=bar" property + ServiceProvider serviceProvider = new ServiceProvider(e); + Component provider = component(m).provides(OriginalService.class).properties("foo", "bar").impl(serviceProvider).build(); + + // The Adapter will see the "foo=bar" property from the adaptee + ServiceAdapter saimpl = new ServiceAdapter(e); + Component adapter = adapter(m, OriginalService.class).propagate(false).impl(saimpl).provides(AdaptedService.class).add(saimpl::set).change(saimpl::change).build(); + + // The consumer depends on the AdaptedService, but won't see foo=bar property from the adaptee + ServiceConsumer scimpl = new ServiceConsumer(e); + Component consumer = component(m).impl(scimpl).withSvc(AdaptedService.class, s -> s.add(scimpl::set).change(scimpl::change)).build(); + + // add the provider and the adapter + m.add(provider); + m.add(adapter); + // Checks if the adapter has been started and has seen the adaptee properties + e.waitForStep(1, 5000); + + // add a consumer that must not see the adaptee service properties + m.add(consumer); + e.waitForStep(2, 5000); + + // change the service properties of the provider, and check that the adapter callback instance is caled. + serviceProvider.changeServiceProperties(); + e.waitForStep(3, 5000); + + // cleanup + m.clear(); + } + + static interface OriginalService { + } + + static interface AdaptedService { + } + + static class ServiceProvider implements OriginalService { + private volatile ServiceRegistration m_registration; // auto injected when started. + public ServiceProvider(Ensure e) { + } + public void changeServiceProperties() { + Hashtable props = new Hashtable<>(); + props.put("foo", "bar2"); + m_registration.setProperties(props); + } + } + + public static class ServiceAdapter implements AdaptedService { + private final Ensure m_ensure; + + public ServiceAdapter(Ensure e) { + m_ensure = e; + } + + public void set(OriginalService adaptee, Dictionary props) { + Assert.assertEquals("bar", props.get("foo")); + m_ensure.step(1); + } + + void change(OriginalService adapted, Dictionary props) { + Assert.assertEquals("bar2", props.get("foo")); + m_ensure.step(3); + } + } + + static class ServiceConsumer { + @SuppressWarnings("unused") + private volatile AdaptedService m_service; + private final Ensure m_ensure; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + void set(AdaptedService adapted, Dictionary props) { + Assert.assertNull(props.get("foo")); + m_ensure.step(2); + } + + void change(AdaptedService adapted, Dictionary props) { + Assert.assertNull(props.get("foo")); + Assert.fail("Change callback should not be called"); + } + } +} + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectBaseTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectBaseTest.java new file mode 100644 index 00000000000..3b9801b706e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectBaseTest.java @@ -0,0 +1,342 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.aspect; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; + +/** + * @author Felix Project Team + */ +public class AspectBaseTest extends TestBase { + + public void testSingleAspect() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + // create a service provider and consumer + ServiceProvider p = new ServiceProvider("a"); + ServiceConsumer c = new ServiceConsumer(e); + + Component sp = component(m).impl(p).provides(ServiceInterface.class).properties("name", "a").build(); + Component sc = component(m).impl(c).withSvc(ServiceInterface.class, srv -> srv.add("add").remove("remove").autoConfig("m_service")).build(); + Component sa = aspect(m, ServiceInterface.class).rank(20).impl(ServiceAspect.class).build(); + + m.add(sc); + m.add(sp); + // after the provider was added, the consumer's add should have been invoked once + e.waitForStep(1, 2000); + Assert.assertEquals("a", c.invoke()); + m.add(sa); + // after the aspect was added, the consumer should get and add for the aspect and a remove + // for the original service + e.waitForStep(3, 2000); + Assert.assertEquals("aa", c.invoke()); + m.remove(sa); + // removing the aspect again should give a remove and add + e.waitForStep(5, 2000); + Assert.assertEquals("a", c.invoke()); + m.remove(sp); + // finally removing the original service should give a remove + e.waitForStep(6, 2000); + m.remove(sc); + e.step(7); + clearComponents(); + } + + public void testSingleAspectRef() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + // create a service provider and consumer + ServiceProvider p = new ServiceProvider("a"); + ServiceConsumer c = new ServiceConsumer(e); + + Component sp = component(m).impl(p).provides(ServiceInterface.class).properties("name", "a").build(); + Component sc = component(m) + .impl(c).withSvc(ServiceInterface.class, srv -> srv.add(c::addRef).remove(c::removeRef).autoConfig("m_service")).build(); + Component sa = aspect(m, ServiceInterface.class).rank(20).impl(ServiceAspect.class).build(); + + m.add(sc); + m.add(sp); + // after the provider was added, the consumer's add should have been invoked once + e.waitForStep(1, 2000); + Assert.assertEquals("a", c.invoke()); + m.add(sa); + // after the aspect was added, the consumer should get and add for the aspect and a remove + // for the original service + e.waitForStep(3, 2000); + Assert.assertEquals("aa", c.invoke()); + m.remove(sa); + // removing the aspect again should give a remove and add + e.waitForStep(5, 2000); + Assert.assertEquals("a", c.invoke()); + m.remove(sp); + // finally removing the original service should give a remove + e.waitForStep(6, 2000); + m.remove(sc); + e.step(7); + clearComponents(); + } + + public void testSingleAspectThatAlreadyExisted() { + DependencyManager m = new DependencyManager(context); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + // create a service provider and consumer + ServiceProvider p = new ServiceProvider("a"); + ServiceConsumer c = new ServiceConsumer(e); + Component sp = component(m).impl(p).provides(ServiceInterface.class).properties("name", "a").build(); + Component sc = component(m).impl(c).withSvc(ServiceInterface.class, srv -> srv.add("add").remove("remove").autoConfig("m_service")).build(); + Component sa = aspect(m, ServiceInterface.class).rank(20).impl(ServiceAspect.class).build(); + + // we first add the aspect + m.add(sa); + // then the service provider + m.add(sp); + // finally the consumer + m.add(sc); + + Assert.assertEquals("aa", c.invoke()); + + // now the consumer's added should be invoked once, as the aspect is already available and should + // directly hide the original service + e.waitForStep(1, 2000); + e.step(2); + + m.remove(sa); + // after removing the aspect, the consumer should get the original service back, so + // remove and add will be invoked + e.waitForStep(4, 2000); + + Assert.assertEquals("a", c.invoke()); + + m.remove(sp); + // after removing the original service, the consumer's remove should be called once + e.waitForStep(5, 2000); + + m.remove(sc); + e.step(6); + } + + public void testSingleAspectThatAlreadyExistedRef() { + DependencyManager m = new DependencyManager(context); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + // create a service provider and consumer + ServiceProvider p = new ServiceProvider("a"); + ServiceConsumer c = new ServiceConsumer(e); + + Component sp = component(m).impl(p).provides(ServiceInterface.class).properties("name", "a").build(); + Component sc = component(m).impl(c).withSvc(ServiceInterface.class, srv -> srv.add(c::addRef).remove(c::removeRef).autoConfig("m_service")).build(); + Component sa = aspect(m, ServiceInterface.class).rank(20).impl(ServiceAspect.class).build(); + + // we first add the aspect + m.add(sa); + // then the service provider + m.add(sp); + // finally the consumer + m.add(sc); + + Assert.assertEquals("aa", c.invoke()); + + // now the consumer's added should be invoked once, as the aspect is already available and should + // directly hide the original service + e.waitForStep(1, 2000); + e.step(2); + + m.remove(sa); + // after removing the aspect, the consumer should get the original service back, so + // remove and add will be invoked + e.waitForStep(4, 2000); + + Assert.assertEquals("a", c.invoke()); + + m.remove(sp); + // after removing the original service, the consumer's remove should be called once + e.waitForStep(5, 2000); + + m.remove(sc); + e.step(6); + } + + public void testMultipleAspects() { + DependencyManager m = new DependencyManager(context); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + // create service providers and consumers + ServiceConsumer c = new ServiceConsumer(e); + Component sp = component(m).impl(new ServiceProvider("a")).provides(ServiceInterface.class).properties("name", "a").build(); + Component sp2 = component(m).impl(new ServiceProvider("b")).provides(ServiceInterface.class).properties("name", "b").build(); + Component sc = component(m).impl(c).withSvc(ServiceInterface.class, srv -> srv.add("add").remove("remove")).build(); + + Component sa = aspect(m, ServiceInterface.class).rank(20).impl(ServiceAspect.class).build(); + Component sa2 = aspect(m, ServiceInterface.class).rank(10).impl(ServiceAspect.class).build(); + + m.add(sp); + m.add(sp2); + m.add(sa); + m.add(sa2); + m.add(sc); + // the consumer will monitor progress, it should get it's add invoked twice, once for every + // (highest) aspect + e.waitForStep(2, 2000); + e.step(3); + + // now invoke all services the consumer collected + List list = c.invokeAll(); + // and make sure both of them are correctly invoked + Assert.assertTrue(list.size() == 2); + Assert.assertTrue(list.contains("aaa")); + Assert.assertTrue(list.contains("bbb")); + + m.remove(sc); + // removing the consumer now should get its removed method invoked twice + e.waitForStep(5, 2000); + e.step(6); + m.remove(sa2); + m.remove(sa); + m.remove(sp2); + m.remove(sp); + e.step(7); + } + + public void testMultipleAspectsRef() { + DependencyManager m = new DependencyManager(context); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + // create service providers and consumers + ServiceConsumer c = new ServiceConsumer(e); + Component sp = component(m).impl(new ServiceProvider("a")).provides(ServiceInterface.class).properties("name", "a").build(); + Component sp2 = component(m).impl(new ServiceProvider("b")).provides(ServiceInterface.class).properties("name", "b").build(); + Component sc = component(m).impl(c).withSvc(ServiceInterface.class, srv -> srv.add(c::addRef).remove(c::removeRef)).build(); + + Component sa = aspect(m, ServiceInterface.class).rank(20).impl(ServiceAspect.class).build(); + Component sa2 = aspect(m, ServiceInterface.class).rank(10).impl(ServiceAspect.class).build(); + + m.add(sp); + m.add(sp2); + m.add(sa); + m.add(sa2); + m.add(sc); + // the consumer will monitor progress, it should get it's add invoked twice, once for every + // (highest) aspect + e.waitForStep(2, 2000); + e.step(3); + + // now invoke all services the consumer collected + List list = c.invokeAll(); + // and make sure both of them are correctly invoked + Assert.assertTrue(list.size() == 2); + Assert.assertTrue(list.contains("aaa")); + Assert.assertTrue(list.contains("bbb")); + + m.remove(sc); + // removing the consumer now should get its removed method invoked twice + e.waitForStep(5, 2000); + e.step(6); + m.remove(sa2); + m.remove(sa); + m.remove(sp2); + m.remove(sp); + e.step(7); + } + + public static interface ServiceInterface { + public String invoke(String input); + } + + public static class ServiceProvider implements ServiceInterface { + private final String m_name; + public ServiceProvider(String name) { + m_name = name; + } + public String invoke(String input) { + return input + m_name; + } + } + + public static class ServiceAspect implements ServiceInterface { + private volatile ServiceInterface m_originalService; + private volatile ServiceRegistration m_registration; + + public String invoke(String input) { + String result = m_originalService.invoke(input); + String property = (String) m_registration.getReference().getProperty("name"); + return result + property; + } + } + + public static class ServiceConsumer { + private final Ensure m_ensure; + private volatile ServiceInterface m_service; + private List m_services = new ArrayList(); + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void addRef(ServiceInterface si, ServiceReference ref) { // method ref callback + add(ref, si); + } + + public void add(ServiceReference ref, ServiceInterface si) { // reflection callback + System.out.println("add: " + ServiceUtil.toString(ref)); + m_services.add(si); + m_ensure.step(); + } + + public void removeRef(ServiceInterface si, ServiceReference ref) { // method ref callback + remove(ref, si); + } + + public void remove(ServiceReference ref, ServiceInterface si) { // reflection callback + System.out.println("rem: " + ServiceUtil.toString(ref)); + m_services.remove(si); + m_ensure.step(); + } + + public String invoke() { + return m_service.invoke(""); + } + + public List invokeAll() { + List results = new ArrayList(); + for (ServiceInterface si : m_services) { + results.add(si.invoke("")); + } + return results; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectChainTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectChainTest.java new file mode 100644 index 00000000000..c2f43d38347 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectChainTest.java @@ -0,0 +1,113 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.aspect; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; + +/** + * @author Felix Project Team + */ +public class AspectChainTest extends TestBase { + + public void testBuildAspectChain() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class).build(); + Component sc = component(m).impl(new ServiceConsumer(e)).withSvc(ServiceInterface.class, true).build(); + Component sa2 = aspect(m, ServiceInterface.class).rank(20).impl(new ServiceAspect(e, 3)).build(); + Component sa3 = aspect(m, ServiceInterface.class).rank(30).impl(new ServiceAspect(e, 2)).build(); + Component sa1 = aspect(m, ServiceInterface.class).rank(10).impl(new ServiceAspect(e, 4)).build(); + m.add(sc); + + m.add(sp); + m.add(sa2); + m.add(sa3); + m.add(sa1); + e.step(); + e.waitForStep(5, 5000); + + m.remove(sa3); + m.remove(sa2); + m.remove(sa1); + m.remove(sp); + + m.remove(sc); + } + + static interface ServiceInterface { + public void invoke(Runnable run); + } + + static class ServiceProvider implements ServiceInterface { + @SuppressWarnings("unused") + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke(Runnable run) { + run.run(); + } + } + + static class ServiceAspect implements ServiceInterface { + private final Ensure m_ensure; + private volatile ServiceInterface m_parentService; + private final int m_step; + + public ServiceAspect(Ensure e, int step) { + m_ensure = e; + m_step = step; + } + public void start() { + } + + public void invoke(Runnable run) { + m_ensure.step(m_step); + m_parentService.invoke(run); + } + + public void stop() { + } + } + + static class ServiceConsumer implements Runnable { + private volatile ServiceInterface m_service; + private final Ensure m_ensure; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void init() { + Thread t = new Thread(this); + t.start(); + } + + public void run() { + m_ensure.waitForStep(1, 2000); + m_service.invoke(Ensure.createRunnableStep(m_ensure, 5)); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectDynamicsTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectDynamicsTest.java new file mode 100644 index 00000000000..b9812dd8170 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectDynamicsTest.java @@ -0,0 +1,252 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.aspect; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; + +/** + * @author Felix Project Team + */ +public class AspectDynamicsTest extends TestBase { + + public void testDynamicallyAddAndRemoveAspect() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + Ensure aspectStopEnsure = new Ensure(); + // create a service provider and consumer + Component provider = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class).build(); + Component provider2 = component(m).impl(new ServiceProvider2(e)).provides(ServiceInterface2.class.getName()).build(); + Component consumer = component(m).impl(new ServiceConsumer(e)).withSvc(ServiceInterface.class, s->s.add("add").swap("swap")).build(); + Component aspect = aspect(m, ServiceInterface.class).autoAdd(false).rank(1).impl(new ServiceAspect(e, aspectStopEnsure)).build(); + + m.add(consumer); + m.add(provider); + // the consumer should invoke the provider here, and when done, arrive at step 3 + // finally wait for step 6 before continuing + e.waitForStep(3, 15000); + + m.add(aspect); + // after adding the aspect, we wait for its init to be invoked, arriving at + // step 4 after an instance bound dependency was added (on a service provided by + // provider 2) + e.waitForStep(4, 15000); + + m.add(provider2); + + // after adding provider 2, we should now see the client being swapped, so + // we wait for step 5 to happen + e.waitForStep(5, 15000); + + // now we continue with step 6, which will trigger the next part of the consumer's + // run method to be executed + e.step(6); + + // invoking step 7, 8 and 9 when invoking the aspect which in turn invokes the + // dependency and the original service, so we wait for that to finish here, which + // is after step 10 has been reached (the client will now wait for step 12) + e.waitForStep(10, 15000); + + m.remove(aspect); + aspectStopEnsure.waitForStep(1, 15000); + // removing the aspect should trigger step 11 (in the swap method of the consumer) + e.waitForStep(11, 15000); + + // step 12 triggers the client to continue + e.step(12); + + // wait for step 13, the final invocation of the provided service (without aspect) + e.waitForStep(13, 15000); + + // clean up + m.remove(provider2); + m.remove(provider); + m.remove(consumer); + e.waitForStep(16, 15000); + m.clear(); + } + + public void testDynamicallyAddAndRemoveAspectRef() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + Ensure aspectStopEnsure = new Ensure(); + // create a service provider and consumer + Component provider = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class.getName()).build(); + Component provider2 = component(m).impl(new ServiceProvider2(e)).provides(ServiceInterface2.class.getName()).build(); + Component consumer = component(m).impl(new ServiceConsumer(e)).withSvc(ServiceInterface.class, s->s.add(ServiceConsumer::add).swap(ServiceConsumer::swap)).build(); + Component aspect = aspect(m, ServiceInterface.class).autoAdd(false).rank(1).impl(new ServiceAspect(e, aspectStopEnsure)).build(); + + m.add(consumer); + m.add(provider); + // the consumer should invoke the provider here, and when done, arrive at step 3 + // finally wait for step 6 before continuing + e.waitForStep(3, 15000); + + m.add(aspect); + // after adding the aspect, we wait for its init to be invoked, arriving at + // step 4 after an instance bound dependency was added (on a service provided by + // provider 2) + e.waitForStep(4, 15000); + + m.add(provider2); + + // after adding provider 2, we should now see the client being swapped, so + // we wait for step 5 to happen + e.waitForStep(5, 15000); + + // now we continue with step 6, which will trigger the next part of the consumer's + // run method to be executed + e.step(6); + + // invoking step 7, 8 and 9 when invoking the aspect which in turn invokes the + // dependency and the original service, so we wait for that to finish here, which + // is after step 10 has been reached (the client will now wait for step 12) + e.waitForStep(10, 15000); + + m.remove(aspect); + aspectStopEnsure.waitForStep(1, 15000); + // removing the aspect should trigger step 11 (in the swap method of the consumer) + e.waitForStep(11, 15000); + + // step 12 triggers the client to continue + e.step(12); + + // wait for step 13, the final invocation of the provided service (without aspect) + e.waitForStep(13, 15000); + + // clean up + m.remove(provider2); + m.remove(provider); + m.remove(consumer); + e.waitForStep(16, 15000); + m.clear(); + } + + static interface ServiceInterface { + public void invoke(Runnable run); + } + + static interface ServiceInterface2 { + public void invoke(); + } + + static class ServiceProvider2 implements ServiceInterface2 { + private final Ensure m_ensure; + + public ServiceProvider2(Ensure ensure) { + m_ensure = ensure; + } + + public void invoke() { + m_ensure.step(9); + } + public void stop() { + m_ensure.step(); + } + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke(Runnable run) { + run.run(); + } + public void stop() { + m_ensure.step(); + } + } + + static class ServiceAspect implements ServiceInterface { + private final Ensure m_ensure; + private volatile ServiceInterface m_originalService; + private volatile ServiceInterface2 m_injectedService; + private volatile Component m_service; + private volatile DependencyManager m_manager; + private final Ensure m_stopEnsure; + + public ServiceAspect(Ensure e, Ensure stopEnsure) { + m_ensure = e; + m_stopEnsure = stopEnsure; + } + public void init() { + m_service.add(m_manager.createServiceDependency() + .setService(ServiceInterface2.class) + .setRequired(true) + ); + m_ensure.step(4); + } + + public void invoke(Runnable run) { + m_ensure.step(7); + m_originalService.invoke(run); + m_injectedService.invoke(); + } + + public void stop() { + m_stopEnsure.step(1); + } + } + + static class ServiceConsumer implements Runnable { + private volatile ServiceInterface m_service; + private final Ensure m_ensure; + private final Ensure.Steps m_swapSteps = new Ensure.Steps(5, 11); + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + void add(ServiceInterface service) { + m_service = service; + } + + void swap(ServiceInterface oldService, ServiceInterface newService) { + System.out.println("swap: old=" + oldService + ", new=" + newService); + m_ensure.steps(m_swapSteps); + m_service = newService; + } + + public void init() { + Thread t = new Thread(this); + t.start(); + } + + public void run() { + m_ensure.step(1); + m_service.invoke(Ensure.createRunnableStep(m_ensure, 2)); + m_ensure.step(3); + m_ensure.waitForStep(6, 15000); + m_service.invoke(Ensure.createRunnableStep(m_ensure, 8)); + m_ensure.step(10); + m_ensure.waitForStep(12, 15000); + m_service.invoke(Ensure.createRunnableStep(m_ensure, 13)); + } + + public void stop() { + m_ensure.step(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectServiceDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectServiceDependencyTest.java new file mode 100644 index 00000000000..c555d794af9 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectServiceDependencyTest.java @@ -0,0 +1,120 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.aspect; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; + + +/** + * @author Felix Project Team + */ +public class AspectServiceDependencyTest extends TestBase { + public void testServiceRegistrationAndConsumption() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class.getName()).build(); + Component sc = component(m).impl(new ServiceConsumer(e)).withSvc(ServiceInterface.class, s->s.add("add").remove("remove")).build(); + Component asp = aspect(m, ServiceInterface.class).rank(100).impl(ServiceProviderAspect.class).build(); + + m.add(sp); + m.add(sc); + m.add(asp); + m.remove(asp); + m.remove(sc); + m.remove(sp); + + // ensure we executed all steps inside the component instance + e.step(8); + } + + static interface ServiceInterface { + public void invoke(String caller); + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke(String caller) { + if (caller.equals("consumer.init")) { + m_ensure.step(3); + } else if (caller.equals("aspect.consumer.add")) { + m_ensure.step(5); + } + } + } + + static class ServiceProviderAspect implements ServiceInterface { + private volatile ServiceInterface m_service; + + public ServiceProviderAspect() { + } + + @Override + public void invoke(String caller) { + m_service.invoke("aspect." + caller); + } + } + + static class ServiceConsumer { + private volatile ServiceInterface m_service; + private final Ensure m_ensure; + private int addCount = 0; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void init() { + m_ensure.step(2); + m_service.invoke("consumer.init"); + } + + public void destroy() { + m_ensure.step(7); + } + + public void add(ServiceInterface service) { + m_service = service; + switch (addCount) { + case 0: m_ensure.step(1); + break; + case 1: m_ensure.step(4); + // aspect had been added + m_service.invoke("consumer.add"); + break; + case 2: m_ensure.step(6); + break; + default: + } + addCount ++; + } + public void remove(ServiceInterface service) { + m_service = null; + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectServiceDependencyWithSwapCallbackTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectServiceDependencyWithSwapCallbackTest.java new file mode 100644 index 00000000000..89d01136900 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectServiceDependencyWithSwapCallbackTest.java @@ -0,0 +1,141 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.aspect; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; + + +/** + * @author Felix Project Team + */ +public class AspectServiceDependencyWithSwapCallbackTest extends TestBase { + public void testServiceRegistrationAndConsumption() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class).build(); + Component sc = component(m).impl(new ServiceConsumer(e)).withSvc(ServiceInterface.class, s->s.add("add").remove("remove").swap("swap")).build(); + Component asp = aspect(m, ServiceInterface.class).rank(100).impl(ServiceProviderAspect.class).build(); + m.add(sp); + m.add(sc); + m.add(asp); + m.remove(asp); + m.remove(sc); + m.remove(sp); + + // ensure we executed all steps inside the component instance + e.step(7); + } + + public void testServiceRegistrationAndConsumptionRef() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + ServiceConsumer scimpl = new ServiceConsumer(e); + Component sp = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class).build(); + Component sc = component(m).impl(scimpl).withSvc(ServiceInterface.class, s->s.add(scimpl::add).remove(scimpl::remove).swap(scimpl::swap)).build(); + Component asp = aspect(m, ServiceInterface.class).rank(100).impl(ServiceProviderAspect.class).build(); + m.add(sp); + m.add(sc); + m.add(asp); + m.remove(asp); + m.remove(sc); + m.remove(sp); + + // ensure we executed all steps inside the component instance + e.step(7); + } + + static interface ServiceInterface { + public void invoke(String caller); + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke(String caller) { + if (caller.equals("consumer.init")) { + m_ensure.step(3); + } else if (caller.equals("aspect.consumer.add")) { + m_ensure.step(5); + } + } + } + + static class ServiceProviderAspect implements ServiceInterface { + private volatile ServiceInterface m_service; + + public ServiceProviderAspect() { + } + + @Override + public void invoke(String caller) { + m_service.invoke("aspect." + caller); + } + } + + static class ServiceConsumer { + private volatile ServiceInterface m_service; + private final Ensure m_ensure; + private int swapCount = 0; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void init() { + m_ensure.step(2); + m_service.invoke("consumer.init"); + } + + public void destroy() { + m_ensure.step(6); + } + + public void add(ServiceInterface service) { + m_service = service; + m_ensure.step(1); + } + + public void remove(ServiceInterface service) { + m_service = null; + } + + public void swap(ServiceInterface previous, ServiceInterface current) { + switch (swapCount) { + case 0: m_ensure.step(4); + break; + case 1: m_ensure.step(5); + break; + default: + } + m_service = current; + swapCount ++; + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectWhiteboardTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectWhiteboardTest.java new file mode 100644 index 00000000000..827180833b7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectWhiteboardTest.java @@ -0,0 +1,290 @@ +/* + * 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.dm.lambda.itest; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.aspect; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + +/** + * @author Felix Project Team + */ +@SuppressWarnings({"rawtypes", "unchecked", "unused"}) +public class AspectWhiteboardTest extends TestBase { + + public void testWhiteboardConsumer() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create service providers and consumer + Component sp1 = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class.getName()).build(); + Component sp2 = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class.getName()).build(); + ServiceConsumer sci = new ServiceConsumer(e); + Component sc = component(m).impl(sci).withSvc(ServiceInterface.class, srv->srv.required(false).add("add").remove("remove")).build(); + Component sa2 = aspect(m, ServiceInterface.class).rank(20).autoAdd(false).impl(new ServiceAspect(e, 3)).build(); + Component sa1 = aspect(m, ServiceInterface.class).rank(10).autoAdd(false).impl(new ServiceAspect(e, 4)).build(); + + // start with a service consumer + System.out.println("Adding consumer"); + m.add(sc); + + // then add two providers, so the consumer will see two services + System.out.println("Adding 2 providers"); + m.add(sp1); + m.add(sp2); + + // make sure consumer sees both services + Assert.assertEquals(2, sci.services()); + + // add an aspect with ranking 20 + System.out.println("Adding aspect with rank 20"); + m.add(sa2); + + // make sure the consumer sees the two new aspects and no longer sees the two original services + Assert.assertEquals(2, sci.services()); + Assert.assertEquals(20, sci.highestRanking()); + Assert.assertEquals(20, sci.lowestRanking()); + + // add an aspect with ranking 10 + System.out.println("Adding aspect with rank 10"); + m.add(sa1); + + // make sure the consumer still sees the two aspects with ranking 20 + Assert.assertEquals(2, sci.services()); + Assert.assertEquals(20, sci.highestRanking()); + Assert.assertEquals(20, sci.lowestRanking()); + + // remove the aspect with ranking 20 + System.out.println("Removing aspect with rank 20"); + m.remove(sa2); + + // make sure the consumer now sees the aspects with ranking 10 + Assert.assertEquals(2, sci.services()); + Assert.assertEquals(10, sci.highestRanking()); + Assert.assertEquals(10, sci.lowestRanking()); + + // remove one of the original services + System.out.println("Removing 1 service"); + m.remove(sp1); + + // make sure the aspect of that service goes away + Assert.assertEquals(1, sci.services()); + Assert.assertEquals(10, sci.highestRanking()); + Assert.assertEquals(10, sci.lowestRanking()); + + // remove the aspect with ranking 10 + System.out.println("Removing aspect with rank 10"); + m.remove(sa1); + + // make sure only the original service remains + Assert.assertEquals(1, sci.services()); + Assert.assertEquals(0, sci.highestRanking()); + Assert.assertEquals(0, sci.lowestRanking()); + + System.out.println("Done with test"); + + // end of test + m.remove(sa2); + m.remove(sp2); + m.remove(sc); + } + + public void testWhiteboardConsumerRef() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create service providers and consumer + Component sp1 = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class.getName()).build(); + Component sp2 = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class.getName()).build(); + ServiceConsumer sci = new ServiceConsumer(e); + Component sc = component(m).impl(sci).withSvc(ServiceInterface.class, srv->srv.optional().add(sci::addRef).remove(sci::removeRef)).build(); + Component sa2 = aspect(m, ServiceInterface.class).rank(20).impl(new ServiceAspect(e, 3)).build(); + Component sa1 = aspect(m, ServiceInterface.class).rank(10).impl(new ServiceAspect(e, 4)).build(); + + // start with a service consumer + System.out.println("Adding consumer"); + m.add(sc); + + // then add two providers, so the consumer will see two services + System.out.println("Adding 2 providers"); + m.add(sp1); + m.add(sp2); + + // make sure consumer sees both services + Assert.assertEquals(2, sci.services()); + + // add an aspect with ranking 20 + System.out.println("Adding aspect with rank 20"); + m.add(sa2); + + // make sure the consumer sees the two new aspects and no longer sees the two original services + Assert.assertEquals(2, sci.services()); + Assert.assertEquals(20, sci.highestRanking()); + Assert.assertEquals(20, sci.lowestRanking()); + + // add an aspect with ranking 10 + System.out.println("Adding aspect with rank 10"); + m.add(sa1); + + // make sure the consumer still sees the two aspects with ranking 20 + Assert.assertEquals(2, sci.services()); + Assert.assertEquals(20, sci.highestRanking()); + Assert.assertEquals(20, sci.lowestRanking()); + + // remove the aspect with ranking 20 + System.out.println("Removing aspect with rank 20"); + m.remove(sa2); + + // make sure the consumer now sees the aspects with ranking 10 + Assert.assertEquals(2, sci.services()); + Assert.assertEquals(10, sci.highestRanking()); + Assert.assertEquals(10, sci.lowestRanking()); + + // remove one of the original services + System.out.println("Removing 1 service"); + m.remove(sp1); + + // make sure the aspect of that service goes away + Assert.assertEquals(1, sci.services()); + Assert.assertEquals(10, sci.highestRanking()); + Assert.assertEquals(10, sci.lowestRanking()); + + // remove the aspect with ranking 10 + System.out.println("Removing aspect with rank 10"); + m.remove(sa1); + + // make sure only the original service remains + Assert.assertEquals(1, sci.services()); + Assert.assertEquals(0, sci.highestRanking()); + Assert.assertEquals(0, sci.lowestRanking()); + + System.out.println("Done with test"); + + // end of test + m.remove(sa2); + m.remove(sp2); + m.remove(sc); + } + + static interface ServiceInterface { + public void invoke(Runnable run); + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke(Runnable run) { + run.run(); + } + } + + static class ServiceAspect implements ServiceInterface { + private final Ensure m_ensure; + private volatile ServiceInterface m_parentService; + private final int m_step; + + public ServiceAspect(Ensure e, int step) { + m_ensure = e; + m_step = step; + } + public void start() { + } + + public void invoke(Runnable run) { + m_ensure.step(m_step); + m_parentService.invoke(run); + } + + public void stop() { + } + } + + static class ServiceConsumer implements Runnable { + private List m_services = new ArrayList(); + private final Ensure m_ensure; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void init() { + Thread t = new Thread(this); + t.start(); + } + + public void run() { + } + + public int services() { + return m_services.size(); + } + + public int highestRanking() { + int ranking = Integer.MIN_VALUE; + for (int i = 0; i < m_services.size(); i++) { + ServiceReference ref = (ServiceReference) m_services.get(i); + Integer r = (Integer) ref.getProperty(Constants.SERVICE_RANKING); + int rank = r == null ? 0 : r.intValue(); + ranking = Math.max(ranking, rank); + } + return ranking; + } + public int lowestRanking() { + int ranking = Integer.MAX_VALUE; + for (int i = 0; i < m_services.size(); i++) { + ServiceReference ref = (ServiceReference) m_services.get(i); + Integer r = (Integer) ref.getProperty(Constants.SERVICE_RANKING); + int rank = r == null ? 0 : r.intValue(); + ranking = Math.min(ranking, rank); + } + return ranking; + } + + // method ref callback + public void addRef(ServiceInterface svc, ServiceReference ref) { + add(ref, svc); + } + + // refection callback + public void add(ServiceReference ref, ServiceInterface svc) { + System.out.println("Added: " + ServiceUtil.toString(ref)); + m_services.add(ref); + } + + // method ref callback + public void removeRef(ServiceInterface svc, ServiceReference ref) { + remove(ref, svc); + } + + // refection callback + public void remove(ServiceReference ref, ServiceInterface svc) { + System.out.println("Removed: " + ServiceUtil.toString(ref)); + m_services.remove(ref); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectWithCallbacksServiceDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectWithCallbacksServiceDependencyTest.java new file mode 100644 index 00000000000..06f0d35d2f4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectWithCallbacksServiceDependencyTest.java @@ -0,0 +1,198 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.aspect; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; + + +/** + * @author Felix Project Team + */ +public class AspectWithCallbacksServiceDependencyTest extends TestBase { + public void testServiceRegistrationAndConsumption() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class.getName()).build(); + Component sc = component(m).impl(new ServiceConsumer(e)).withSvc(ServiceInterface.class, s->s.add("add").remove("remove")).build(); + Component asp = aspect(m, ServiceInterface.class).rank(100).add("add").remove("remove").swap("swap").impl(ServiceProviderAspect.class).build(); + + m.add(sp); + m.add(sc); + m.add(asp); + m.remove(asp); + m.remove(sc); + m.remove(sp); + + // ensure we executed all steps inside the component instance + e.step(8); + } + + public void testServiceRegistrationAndConsumptionRef() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class.getName()).build(); + Component sc = component(m).impl(new ServiceConsumer(e)).withSvc(ServiceInterface.class, s->s.add(ServiceConsumer::add).remove(ServiceConsumer::remove)).build(); + Component asp = aspect(m, ServiceInterface.class) + .impl(ServiceProviderAspect.class).rank(100) + .add(ServiceProviderAspect::add).remove(ServiceProviderAspect::remove).swap(ServiceProviderAspect::swap) + .build(); + + m.add(sp); + m.add(sc); + m.add(asp); + m.remove(asp); + m.remove(sc); + m.remove(sp); + + // ensure we executed all steps inside the component instance + e.step(8); + } + + public void testServiceRegistrationAndConsumptionWithAspectCallbackInstance() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class).build(); + Component sc = component(m).impl(new ServiceConsumer(e)).withSvc(ServiceInterface.class, s->s.add("add").remove("remove")).build(); + + ServiceProviderAspect providerAspect = new ServiceProviderAspect(); + ServiceProviderAspectCallbackInstance aspectCb = new ServiceProviderAspectCallbackInstance(providerAspect); + Component asp = aspect(m, ServiceInterface.class).rank(100).impl(providerAspect).add(aspectCb::add).remove(aspectCb::remove).swap(aspectCb::swap).build(); + + m.add(sp); + m.add(sc); + m.add(asp); + m.remove(asp); + m.remove(sc); + m.remove(sp); + + // ensure we executed all steps inside the component instance + e.step(8); + } + + static interface ServiceInterface { + public void invoke(String caller); + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke(String caller) { + if (caller.equals("consumer.init")) { + m_ensure.step(3); + } else if (caller.equals("aspect.consumer.add")) { + m_ensure.step(5); + } + } + } + + public static class ServiceProviderAspectCallbackInstance { + private final ServiceProviderAspect m_aspect; + + ServiceProviderAspectCallbackInstance(ServiceProviderAspect aspect) { + m_aspect = aspect; + } + + public void add(ServiceInterface service) { + m_aspect.add(service); + } + + public void remove(ServiceInterface service) { + m_aspect.remove(service); + } + + public void swap(ServiceInterface previous, ServiceInterface current) { + m_aspect.swap(previous, current); + } + } + + static class ServiceProviderAspect implements ServiceInterface { + private volatile ServiceInterface m_service; + + public ServiceProviderAspect() { + } + + @Override + public void invoke(String caller) { + m_service.invoke("aspect." + caller); + } + + public void add(ServiceInterface service) { + m_service = service; + } + + public void remove(ServiceInterface service) { + m_service = null; + } + + public void swap(ServiceInterface previous, ServiceInterface current) { + m_service = current; + } + } + + static class ServiceConsumer { + private volatile ServiceInterface m_service; + private final Ensure m_ensure; + private int addCount = 0; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void init() { + m_ensure.step(2); + m_service.invoke("consumer.init"); + } + + public void destroy() { + m_ensure.step(7); + } + + public void add(ServiceInterface service) { + m_service = service; + switch (addCount) { + case 0: m_ensure.step(1); + break; + case 1: m_ensure.step(4); + // aspect had been added + m_service.invoke("consumer.add"); + break; + case 2: m_ensure.step(6); + break; + default: + } + addCount ++; + } + public void remove(ServiceInterface service) { + m_service = null; + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectWithPropagationTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectWithPropagationTest.java new file mode 100644 index 00000000000..ded56ed00cd --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AspectWithPropagationTest.java @@ -0,0 +1,716 @@ +/* + * 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.dm.lambda.itest; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.adapter; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.aspect; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import java.util.Dictionary; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; + + +/** + * Test for aspects with service properties propagations. + * + * @author Felix Project Team + */ +@SuppressWarnings({"rawtypes", "unchecked", "unused"}) +public class AspectWithPropagationTest extends TestBase { + private final static int ASPECTS = 3; + private final Set _randoms = new HashSet(); + private final Random _rnd = new Random(); + private static Ensure m_invokeStep; + private static Ensure m_changeStep; + + /** + * This test does the following: + * + * - Create S service with property "p=s" + * - Create SA (aspect of S) with property "p=aspect" + * - Create Client, depending on S (actually, on SA). + * - Client should see SA with properties p=aspect + * - Change S service property with "p=smodified": the Client should be changed with SA(p=aspect) + * - Change aspect service property with "p=aspectmodified": The client should be changed with SA(p=aspectmodified) + */ + public void testAspectsWithPropagationNotOverriding() { + System.out.println("----------- Running testAspectsWithPropagationNotOverriding ..."); + DependencyManager m = getDM(); + m_invokeStep = new Ensure(); + + // Create our original "S" service. + S s = new S() { + public void invoke() { + } + }; + Component sComp = component(m).impl(s).provides(S.class, "p", "s").build(); + + // Create SA (aspect of S) + S sa = new S() { + volatile S m_s; + public void invoke() { + } + }; + Component saComp = aspect(m, S.class).rank(1).impl(sa).properties("p", "aspect").build(); + + // Create client depending on S + Object client = new Object() { + int m_changeCount; + void add(Map props, S s) { + Assert.assertEquals("aspect", props.get("p")); + m_invokeStep.step(1); + } + + void change(Map props, S s) { + switch (++m_changeCount) { + case 1: + Assert.assertEquals("aspect", props.get("p")); + m_invokeStep.step(2); + break; + case 2: + Assert.assertEquals("aspectmodified", props.get("p")); + m_invokeStep.step(3); + } + } + }; + Component clientComp = component(m).impl(client).withSvc(S.class, srv->srv.add("add").change("change")).build(); + + // Add components in dependency manager + m.add(sComp); + m.add(saComp); + m.add(clientComp); + + // client should have been added with SA aspect + m_invokeStep.waitForStep(1, 5000); + + // now change s "p=s" to "p=smodified": client should not see it + Hashtable props = new Hashtable(); + props.put("p", "smodified"); + sComp.setServiceProperties(props); + m_invokeStep.waitForStep(2, 5000); + + // now change sa aspect "p=aspect" to "p=aspectmodified": client should see it + props = new Hashtable(); + props.put("p", "aspectmodified"); + saComp.setServiceProperties(props); + m_invokeStep.waitForStep(3, 5000); + + // remove components + m.remove(clientComp); + m.remove(saComp); + m.remove(sComp); + } + + /** + * This test does the following: + * + * - Create S service + * - Create some S Aspects + * - Create a Client, depending on S (actually, on the top-level S aspect) + * - Client has a "change" callback in order to track S service properties modifications. + * - First, invoke Client.invoke(): all S aspects, and finally original S service must be invoked orderly. + * - Modify S original service properties, and check if all aspects, and the client has been orderly called in their "change" callback. + * - Modify the First lowest ranked aspect (rank=1), and check if all aspects, and client have been orderly called in their "change" callback. + */ + public void testAspectsWithPropagation() { + System.out.println("----------- Running testAspectsWithPropagation ..."); + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + m_invokeStep = new Ensure(); + + // Create our original "S" service. + Dictionary props = new Hashtable(); + props.put("foo", "bar"); + Component s = component(m).impl(new SImpl()).provides(S.class, props).build(); + + // Create an aspect aware client, depending on "S" service. + Client clientImpl; + Component client = component(m).impl((clientImpl = new Client())).withSvc(S.class, srv -> srv.add("add").change("change").remove("remove").swap("swap")).build(); + + // Create some "S" aspects + Component[] aspects = new Component[ASPECTS]; + for (int rank = 1; rank <= ASPECTS; rank ++) { + aspects[rank-1] = aspect(m, S.class).rank(rank).impl(new A("A" + rank, rank)).add("add").change("change").remove("remove").swap("swap").build(); + props = new Hashtable(); + props.put("a" + rank, "v" + rank); + aspects[rank-1].setServiceProperties(props); + } + + // Register client + m.add(client); + + // Randomly register aspects and original service + boolean originalServiceAdded = false; + for (int i = 0; i < ASPECTS; i ++) { + int index = getRandomAspect(); + m.add(aspects[index]); + if (! originalServiceAdded && _rnd.nextBoolean()) { + m.add(s); + originalServiceAdded = true; + } + } + if (! originalServiceAdded) { + m.add(s); + } + + // All set, check if client has inherited from top level aspect properties + original service properties + Map check = new HashMap(); + check.put("foo", "bar"); + for (int i = 1; i < (ASPECTS - 1); i ++) { + check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect. + } + check.put("a" + ASPECTS, "v" + ASPECTS); + checkServiceProperties(check, clientImpl.getServiceProperties()); + + // Now invoke client, which orderly calls all aspects in the chain, and finally the original service "S". + System.out.println("-------------------------- Invoking client."); + clientImpl.invoke(); + m_invokeStep.waitForStep(ASPECTS+1, 5000); + + // Now, change original service "S" properties: this will orderly trigger "change" callbacks on aspects, and on client. + System.out.println("-------------------------- Modifying original service properties."); + m_changeStep = new Ensure(); + props = new Hashtable(); + props.put("foo", "barModified"); + s.setServiceProperties(props); + + // Check if aspects and client have been orderly called in their "changed" callback + m_changeStep.waitForStep(ASPECTS+1, 5000); + + // Check if modified "foo" original service property has been propagated + check = new HashMap(); + check.put("foo", "barModified"); + for (int i = 1; i < (ASPECTS - 1); i ++) { + check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect. + } + check.put("a" + ASPECTS, "v" + ASPECTS); // we only see top-level aspect service properties + checkServiceProperties(check, clientImpl.getServiceProperties()); + + // Now, change the top-level ranked aspect: it must propagate to all upper aspects, as well as to the client + System.out.println("-------------------------- Modifying top-level aspect service properties."); + + m_changeStep = new Ensure(); + for (int i = 1; i <= ASPECTS; i ++) { + m_changeStep.step(i); // only client has to be changed. + } + props = new Hashtable(); + props.put("a" + ASPECTS, "v" + ASPECTS + "-Modified"); + aspects[ASPECTS-1].setServiceProperties(props); // That triggers change callbacks for upper aspects (with rank >= 2) + m_changeStep.waitForStep(ASPECTS+1, 5000); // check if client have been changed. + + // Check if top level aspect service properties have been propagated up to the client. + check = new HashMap(); + check.put("foo", "barModified"); + for (int i = 1; i < (ASPECTS - 1); i ++) { + check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect. + } + check.put("a" + ASPECTS, "v" + ASPECTS + "-Modified"); + checkServiceProperties(check, clientImpl.getServiceProperties()); + + // Clear all components. + m_changeStep = null; + m.clear(); + } + + /** + * This test does the following: + * + * - Create S service + * - Create some S Aspects without any callbacks (add/change/remove/swap) + * - Create a Client, depending on S (actually, on the top-level S aspect) + * - Client has a "change" callack in order to track S service properties modifications. + * - First, invoke Client.invoke(): all S aspects, and finally original S service must be invoked orderly. + * - Modify S original service properties, and check if the client has been called in its "change" callback. + */ + public void testAspectsWithPropagationAndNoCallbacks() { + System.out.println("----------- Running testAspectsWithPropagation ..."); + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + m_invokeStep = new Ensure(); + + // Create our original "S" service. + Dictionary props = new Hashtable(); + props.put("foo", "bar"); + Component s = component(m).impl(new SImpl()).provides(S.class, props).build(); + + // Create an aspect aware client, depending on "S" service. + Client clientImpl; + Component client = component(m).impl((clientImpl = new Client())).withSvc(S.class, srv->srv.add("add").change("change").remove("remove")).build(); + + // Create some "S" aspects + Component[] aspects = new Component[ASPECTS]; + for (int rank = 1; rank <= ASPECTS; rank ++) { + aspects[rank-1] = aspect(m, S.class).rank(rank).impl(new A("A" + rank, rank)).build(); + props = new Hashtable(); + props.put("a" + rank, "v" + rank); + aspects[rank-1].setServiceProperties(props); + } + + // Register client + m.add(client); + + // Randomly register aspects and original service + boolean originalServiceAdded = false; + for (int i = 0; i < ASPECTS; i ++) { + int index = getRandomAspect(); + m.add(aspects[index]); + if (! originalServiceAdded && _rnd.nextBoolean()) { + m.add(s); + originalServiceAdded = true; + } + } + if (! originalServiceAdded) { + m.add(s); + } + + // All set, check if client has inherited from top level aspect properties + original service properties + Map check = new HashMap(); + check.put("foo", "bar"); + for (int i = 1; i < (ASPECTS - 1); i ++) { + check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect. + } + check.put("a" + ASPECTS, "v" + ASPECTS); + checkServiceProperties(check, clientImpl.getServiceProperties()); + + // Now invoke client, which orderly calls all aspects in the chain, and finally the original service "S". + System.out.println("-------------------------- Invoking client."); + clientImpl.invoke(); + m_invokeStep.waitForStep(ASPECTS+1, 5000); + + // Now, change original service "S" properties: this will orderly trigger "change" callbacks on aspects, and on client. + System.out.println("-------------------------- Modifying original service properties."); + m_changeStep = new Ensure(); + for (int i = 1; i <= ASPECTS; i ++) { + m_changeStep.step(i); // skip aspects, which have no "change" callbacks. + } + props = new Hashtable(); + props.put("foo", "barModified"); + s.setServiceProperties(props); + + // Check if aspects and client have been orderly called in their "changed" callback + m_changeStep.waitForStep(ASPECTS+1, 5000); + + // Check if modified "foo" original service property has been propagated + check = new HashMap(); + check.put("foo", "barModified"); + for (int i = 1; i < (ASPECTS - 1); i ++) { + check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect. + } + check.put("a" + ASPECTS, "v" + ASPECTS); // we only see top-level aspect service properties + checkServiceProperties(check, clientImpl.getServiceProperties()); + + // Clear all components. + m_changeStep = null; + m.clear(); + } + + /** + * This test does the following: + * + * - Create S service + * - Create some S Aspects + * - Create S2 Adapter, which adapts S to S2 + * - Create Client2, which depends on S2. Client2 listens to S2 property change events. + * - Now, invoke Client2.invoke(): all S aspects, and finally original S service must be invoked orderly. + * - Modify S original service properties, and check if all aspects, S2 Adapter, and Client2 have been orderly called in their "change" callback. + */ + public void testAdapterWithAspectsAndPropagation() { + System.out.println("----------- Running testAdapterWithAspectsAndPropagation ..."); + + DependencyManager m = getDM(); + m_invokeStep = new Ensure(); + + // Create our original "S" service. + Dictionary props = new Hashtable(); + props.put("foo", "bar"); + Component s = component(m).impl(new SImpl()).provides(S.class, props).build(); + + // Create some "S" aspects + Component[] aspects = new Component[ASPECTS]; + for (int rank = 1; rank <= ASPECTS; rank ++) { + aspects[rank-1] = aspect(m, S.class).rank(rank).impl(new A("A" + rank, rank)).add("add").change("change").remove("remove").swap("swap").build(); + props = new Hashtable(); + props.put("a" + rank, "v" + rank); + aspects[rank-1].setServiceProperties(props); + } + + // Create S2 adapter (which adapts S1 to S2 interface) + Component adapter = adapter(m, S.class).add("add").change("change").remove("remove").swap("swap").provides(S2.class).impl(new S2Impl()).build(); + + // Create Client2, which depends on "S2" service. + Client2 client2Impl; + Component client2 = component(m).impl((client2Impl = new Client2())).withSvc(S2.class, srv -> srv.add("add").change("change")).build(); + + // Register client2 + m.add(client2); + + // Register S2 adapter + m.add(adapter); + + // Randomly register aspects, original service + boolean originalServiceAdded = false; + for (int i = 0; i < ASPECTS; i ++) { + int index = getRandomAspect(); + m.add(aspects[index]); + if (! originalServiceAdded && _rnd.nextBoolean()) { + m.add(s); + originalServiceAdded = true; + } + } + if (! originalServiceAdded) { + m.add(s); + } + + // Now invoke client2, which orderly calls all S1 aspects, then S1Impl, and finally S2 service + System.out.println("-------------------------- Invoking client2."); + client2Impl.invoke2(); + m_invokeStep.waitForStep(ASPECTS+2, 5000); + + // Now, change original service "S" properties: this will orderly trigger "change" callbacks on aspects, S2Impl, and Client2. + System.out.println("-------------------------- Modifying original service properties."); + m_changeStep = new Ensure(); + props = new Hashtable(); + props.put("foo", "barModified"); + s.setServiceProperties(props); + + // Check if aspects and Client2 have been orderly called in their "changed" callback + m_changeStep.waitForStep(ASPECTS+2, 5000); + + // Check if modified "foo" original service property has been propagated to Client2 + Map check = new HashMap(); + check.put("foo", "barModified"); + for (int i = 1; i < (ASPECTS - 1); i ++) { + check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect. + } + check.put("a" + ASPECTS, "v" + ASPECTS); + checkServiceProperties(check, client2Impl.getServiceProperties()); + + // Clear all components. + m_changeStep = null; + m.clear(); + } + + /** + * This test does the following: + * + * - Create S service + * - Create some S Aspects without any callbacks (add/change/remove) + * - Create S2 Adapter, which adapts S to S2 (but does not have any add/change/remove callbacks) + * - Create Client2, which depends on S2. Client2 listens to S2 property change events. + * - Now, invoke Client2.invoke(): all S aspects, and finally original S service must be invoked orderly. + * - Modify S original service properties, and check if all aspects, S2 Adapter, and Client2 have been orderly called in their "change" callback. + */ + public void testAdapterWithAspectsAndPropagationNoCallbacks() { + System.out.println("----------- Running testAdapterWithAspectsAndPropagationNoCallbacks ..."); + + DependencyManager m = getDM(); + m_invokeStep = new Ensure(); + + // Create our original "S" service. + Dictionary props = new Hashtable(); + props.put("foo", "bar"); + Component s = component(m).impl(new SImpl()).provides(S.class, props).build(); + + // Create some "S" aspects + Component[] aspects = new Component[ASPECTS]; + for (int rank = 1; rank <= ASPECTS; rank ++) { + aspects[rank-1] = aspect(m, S.class).rank(rank).impl(new A("A" + rank, rank)).build(); + props = new Hashtable(); + props.put("a" + rank, "v" + rank); + aspects[rank-1].setServiceProperties(props); + } + + // Create S2 adapter (which adapts S1 to S2 interface) + Component adapter = adapter(m, S.class).provides(S2.class).impl(new S2Impl()).build(); + + // Create Client2, which depends on "S2" service. + Client2 client2Impl; + Component client2 = component(m).impl((client2Impl = new Client2())).withSvc(S2.class, srv->srv.add("add").change("change")).build(); + + // Register client2 + m.add(client2); + + // Register S2 adapter + m.add(adapter); + + // Randomly register aspects, original service + boolean originalServiceAdded = false; + for (int i = 0; i < ASPECTS; i ++) { + int index = getRandomAspect(); + m.add(aspects[index]); + if (! originalServiceAdded && _rnd.nextBoolean()) { + m.add(s); + originalServiceAdded = true; + } + } + if (! originalServiceAdded) { + m.add(s); + } + + // Now invoke client2, which orderly calls all S1 aspects, then S1Impl, and finally S2 service + System.out.println("-------------------------- Invoking client2."); + client2Impl.invoke2(); + m_invokeStep.waitForStep(ASPECTS+2, 5000); + + // Now, change original service "S" properties: this will orderly trigger "change" callbacks on aspects, S2Impl, and Client2. + System.out.println("-------------------------- Modifying original service properties."); + m_changeStep = new Ensure(); + for (int i = 1; i <= ASPECTS+1; i ++) { + m_changeStep.step(i); // skip all aspects and the adapter + } + props = new Hashtable(); + props.put("foo", "barModified"); + s.setServiceProperties(props); + + // Check if Client2 has been called in its "changed" callback + m_changeStep.waitForStep(ASPECTS+2, 5000); + + // Check if modified "foo" original service property has been propagated to Client2 + Map check = new HashMap(); + check.put("foo", "barModified"); + for (int i = 1; i < (ASPECTS - 1); i ++) { + check.put("a" + i, null); // we must not inherit from lower ranks, only from the top-level aspect. + } + check.put("a" + ASPECTS, "v" + ASPECTS); + checkServiceProperties(check, client2Impl.getServiceProperties()); + + // Clear all components. + m_changeStep = null; + m.clear(); + } + + private void checkServiceProperties(Map check, Dictionary properties) { + for (Object key : check.keySet()) { + Object val = check.get(key); + if (val == null) { + Assert.assertNull(properties.get(key)); + } else { + Assert.assertEquals(val, properties.get(key)); + } + } + } + + private int getRandomAspect() { + int index = 0; + do { + index = _rnd.nextInt(ASPECTS); + } while (_randoms.contains(new Integer(index))); + _randoms.add(new Integer(index)); + return index; + } + + // S Service + public static interface S { + public void invoke(); + } + + // S ServiceImpl + static class SImpl implements S { + public SImpl() { + } + + public String toString() { + return "S"; + } + + public void invoke() { + m_invokeStep.step(ASPECTS+1); + } + } + + // S Aspect + static class A implements S { + private final String m_name; + private volatile ServiceRegistration m_registration; + private volatile S m_next; + private final int m_rank; + + public A(String name, int rank) { + m_name = name; + m_rank = rank; + } + + public String toString() { + return m_name; + } + + public void invoke() { + int rank = ServiceUtil.getRanking(m_registration.getReference()); + m_invokeStep.step(ASPECTS - rank + 1); + m_next.invoke(); + } + + public void add(ServiceReference ref, S s) { + System.out.println("+++ A" + m_rank + ".add:" + s + "/" + ServiceUtil.toString(ref)); + m_next = s; + } + + public void swap(ServiceReference oldSRef, S oldS, ServiceReference newSRef, S newS) { + System.out.println("+++ A" + m_rank + ".swap: new=" + newS + ", props=" + ServiceUtil.toString(newSRef)); + Assert.assertTrue(m_next == oldS); + m_next = newS; + } + + public void change(ServiceReference props, S s) { + System.out.println("+++ A" + m_rank + ".change: s=" + s + ", props=" + ServiceUtil.toString(props)); + if (m_changeStep != null) { + int rank = ServiceUtil.getRanking(m_registration.getReference()); + m_changeStep.step(rank); + } + } + + public void remove(ServiceReference props, S s) { + System.out.println("+++ A" + m_rank + ".remove: " + s + ", props=" + ServiceUtil.toString(props)); + } + } + + // Aspect aware client, depending of "S" service aspects. + static class Client { + private volatile S m_s; + private volatile ServiceReference m_sRef; + + public Client() { + } + + public Dictionary getServiceProperties() { + Dictionary props = new Hashtable(); + for (String key : m_sRef.getPropertyKeys()) { + props.put(key, m_sRef.getProperty(key)); + } + return props; + } + + public void invoke() { + m_s.invoke(); + } + + public String toString() { + return "Client"; + } + + public void add(ServiceReference ref, S s) { + System.out.println("+++ Client.add: " + s + "/" + ServiceUtil.toString(ref)); + m_s = s; + m_sRef = ref; + } + + public void swap(ServiceReference oldSRef, S oldS, ServiceReference newSRef, S newS) { + System.out.println("+++ Client.swap: m_s = " + m_s + ", old=" + oldS + ", oldProps=" + ServiceUtil.toString(oldSRef) + ", new=" + newS + ", props=" + ServiceUtil.toString(newSRef)); + Assert.assertTrue(m_s == oldS); + m_s = newS; + m_sRef = newSRef; + } + + public void change(ServiceReference properties, S s) { + System.out.println("+++ Client.change: s=" + s + ", props=" + ServiceUtil.toString(properties)); + if (m_changeStep != null) { + m_changeStep.step(ASPECTS+1); + } + } + + public void remove(ServiceReference props, S s) { + System.out.println("+++ Client.remove: " + s + ", props=" + ServiceUtil.toString(props)); + } + } + + // S2 Service + public static interface S2 { + public void invoke2(); + } + + // S2 impl, which adapts S1 interface to S2 interface + static class S2Impl implements S2 { + private volatile S m_s; // we shall see top-level aspect on S service + + public void add(ServiceReference ref, S s) { + System.out.println("+++ S2Impl.add: " + s + "/" + ServiceUtil.toString(ref)); + m_s = s; + } + + public void swap(ServiceReference oldSRef, S oldS, ServiceReference newSRef, S newS) { + System.out.println("+++ S2Impl.swap: new=" + newS + ", props=" + ServiceUtil.toString(newSRef)); + m_s = newS; + } + + public void change(ServiceReference properties, S s) { + System.out.println("+++ S2Impl.change: s=" + s + ", props=" + ServiceUtil.toString(properties)); + if (m_changeStep != null) { + m_changeStep.step(ASPECTS+1); + } + } + + public void remove(ServiceReference props, S s) { + System.out.println("+++ S2Impl.remove: " + s + ", props=" + ServiceUtil.toString(props)); + } + + public void invoke2() { + m_s.invoke(); + m_invokeStep.step(ASPECTS + 2); // All aspects, and S1Impl have been invoked + } + + public String toString() { + return "S2"; + } + } + + // Client2 depending on S2. + static class Client2 { + private volatile S2 m_s2; + private volatile ServiceReference m_s2Ref; + + public Dictionary getServiceProperties() { + Dictionary props = new Hashtable(); + for (String key : m_s2Ref.getPropertyKeys()) { + props.put(key, m_s2Ref.getProperty(key)); + } + return props; + } + + public void invoke2() { + m_s2.invoke2(); + } + + public String toString() { + return "Client2"; + } + + public void add(ServiceReference ref, S2 s2) { + System.out.println("+++ Client2.add: " + s2 + "/" + ServiceUtil.toString(ref)); + m_s2 = s2; + m_s2Ref = ref; + } + + public void change(ServiceReference props, S2 s2) { + System.out.println("+++ Client2.change: s2=" + s2 + ", props=" + ServiceUtil.toString(props)); + if (m_changeStep != null) { + m_changeStep.step(ASPECTS + 2); // S1Impl, all aspects, and S2 adapters have been changed before us. + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AutoConfigTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AutoConfigTest.java new file mode 100644 index 00000000000..65eacfd6f2a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/AutoConfigTest.java @@ -0,0 +1,251 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.Constants; + +/** + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class AutoConfigTest extends TestBase { + private final Ensure m_ensure = new Ensure(); + + public void testField() throws Exception { + final DependencyManager dm = getDM(); + // Create a consumer, depending on some providers (autoconfig field). + ConsumeWithProviderField consumer = new ConsumeWithProviderField(); + Component c = createConsumer(dm, consumer); + // Create two providers + Component p1 = createProvider(dm, 10, new Provider() { + public String toString() { return "provider1"; } + public void run() { m_ensure.step(); } + }); + Component p2 = createProvider(dm, 20, new Provider() { + public String toString() { return "provider2"; } + public void run() { m_ensure.step(); } + }); + + // add the two providers + dm.add(p2); + dm.add(p1); + // add the consumer, which should have been injected with provider2 (highest rank) + dm.add(c); + m_ensure.waitForStep(1, 5000); + // remove the provider2, the consumer should now be injected with provider1 + dm.remove(p2); + Assert.assertNotNull(consumer.getProvider()); + Assert.assertEquals("provider1", consumer.getProvider().toString()); + // remove the provider1, the consumer should have been stopped + dm.remove(p1); + m_ensure.waitForStep(2, 5000); + dm.clear(); + } + + public void testIterableField() throws Exception { + final DependencyManager dm = getDM(); + ConsumerWithIterableField consumer = new ConsumerWithIterableField(); + Component c = createConsumer(dm, consumer); + Component p1 = createProvider(dm, 10, new Provider() { + public void run() { m_ensure.step(); } + public String toString() { return "provider1"; } + }); + Component p2 = createProvider(dm, 20, new Provider() { + public void run() { m_ensure.step();} + public String toString() { return "provider2"; } + }); + + dm.add(p2); + dm.add(p1); + dm.add(c); + // the consumer should have been injected with all providers. + m_ensure.waitForStep(3, 5000); + + // check if all providers are there + Assert.assertNotNull(consumer.getProvider("provider1")); + Assert.assertNotNull(consumer.getProvider("provider2")); + + // remove provider1 + dm.remove(p1); + + // check if provider1 has been removed and if provider2 is still there + Assert.assertNull(consumer.getProvider("provider1")); + Assert.assertNotNull(consumer.getProvider("provider2")); + + // remove provider2, the consumer should be stopped + dm.remove(p2); + m_ensure.waitForStep(4, 5000); + dm.clear(); + } + + public void testMapField() throws Exception { + final DependencyManager dm = getDM(); + ConsumerWithMapField consumer = new ConsumerWithMapField(); + Component c = createConsumer(dm, consumer); + Component p1 = createProvider(dm, 10, new Provider() { + public void run() { m_ensure.step(); } + public String toString() { return "provider1"; } + }); + Component p2 = createProvider(dm, 20, new Provider() { + public void run() { m_ensure.step();} + public String toString() { return "provider2"; } + }); + + dm.add(p2); + dm.add(p1); + dm.add(c); + // the consumer should have been injected with all providers. + m_ensure.waitForStep(3, 5000); + + // check if all providers are there + Assert.assertNotNull(consumer.getProvider("provider1")); + Assert.assertNotNull(consumer.getProvider("provider2")); + + // remove provider1 + dm.remove(p1); + + // check if provider1 has been removed and if provider2 is still there + Assert.assertNull(consumer.getProvider("provider1")); + Assert.assertNotNull(consumer.getProvider("provider2")); + + // remove provider2, the consumer should be stopped + dm.remove(p2); + m_ensure.waitForStep(4, 5000); + dm.clear(); + } + + private Component createProvider(DependencyManager dm, int rank, Provider provider) { + return component(dm).impl(provider).provides(Provider.class, Constants.SERVICE_RANKING, new Integer(rank)).build(); + } + + private Component createConsumer(DependencyManager dm, Object consumer) { + return component(dm).impl(consumer).withSvc(Provider.class, true).build(); + } + + public static interface Provider extends Runnable { + } + + public class ConsumeWithProviderField { + volatile Provider m_provider; + + void start() { + Assert.assertNotNull(m_provider); + Assert.assertEquals("provider2", m_provider.toString()); + m_ensure.step(1); + } + + public Provider getProvider() { + return m_provider; + } + + void stop() { + m_ensure.step(2); + } + } + + public class ConsumerWithIterableField { + final Iterable m_providers = new ConcurrentLinkedQueue<>(); + final List m_notInjectMe = new ArrayList(); + + void start() { + Assert.assertNotNull(m_providers); + int found = 0; + for (Provider provider : m_providers) { + provider.run(); + found ++; + } + Assert.assertTrue(found == 2); + // The "m_notInjectMe" should not be injected with anything + Assert.assertEquals(m_notInjectMe.size(), 0); + m_ensure.step(3); + } + + public Provider getProvider(String name) { + System.out.println("getProvider(" + name + ") : proviers=" + m_providers); + for (Provider provider : m_providers) { + if (provider.toString().equals(name)) { + return provider; + } + } + return null; + } + + void stop() { + m_ensure.step(4); + } + } + + public class ConsumerWithMapField { + final Map m_providers = new ConcurrentHashMap<>(); + final Map m_notInjectMe = new HashMap<>(); + + void start() { + Assert.assertNotNull(m_providers); + System.out.println("ConsumerMap.start: injected providers=" + m_providers); + Assert.assertTrue(m_providers.size() == 2); + Assert.assertEquals(0, m_notInjectMe.size()); + for (Map.Entry e : m_providers.entrySet()) { + Provider provider = e.getKey(); + Dictionary props = e.getValue(); + + provider.run(); + if (provider.toString().equals("provider1")) { + Assert.assertEquals(props.get(Constants.SERVICE_RANKING), 10); + } else if (provider.toString().equals("provider2")) { + Assert.assertEquals(props.get(Constants.SERVICE_RANKING), 20); + } else { + Assert.fail("Did not find any properties for provider " + provider); + } + } + + m_ensure.step(3); + } + + public Provider getProvider(String name) { + System.out.println("getProvider(" + name + ") : providers=" + m_providers); + for (Provider provider : m_providers.keySet()) { + if (provider.toString().equals(name)) { + return provider; + } + } + return null; + } + + Map getProviders() { + return m_providers; + } + + void stop() { + m_ensure.step(4); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/BundleAdapterTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/BundleAdapterTest.java new file mode 100644 index 00000000000..bfee9288fa5 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/BundleAdapterTest.java @@ -0,0 +1,196 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.bundleAdapter; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.Bundle; + +/** + * @author Felix Project Team + */ +public class BundleAdapterTest extends TestBase { + public void testBundleAdapter() { + DependencyManager m = getDM(); + // create a bundle adapter service (one is created for each bundle) + Component adapter = bundleAdapter(m) + .mask(Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE) + .impl(BundleAdapter.class) + .provides(BundleAdapter.class) + .build(); + + // create a service provider and consumer + Consumer c = new Consumer(); + Component consumer = m.createComponent().setImplementation(c) + .add(m.createServiceDependency().setService(BundleAdapter.class).setCallbacks("add", "remove")); + + // add the bundle adapter + m.add(adapter); + // add the service consumer + m.add(consumer); + // check if at least one bundle was found + c.check(); + // remove the consumer again + m.remove(consumer); + // check if all bundles were removed correctly + c.doubleCheck(); + // remove the bundle adapter + m.remove(adapter); + } + + public void testBundleAdapterWithCallbackInstance() { + DependencyManager m = getDM(); + // create a bundle adapter service (one is created for each bundle) + BundleAdapterWithCallback baWithCb = new BundleAdapterWithCallback(); + BundleAdapterCallbackInstance cbInstance = new BundleAdapterCallbackInstance(baWithCb); + + Component adapter = bundleAdapter(m) + .mask(Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE) + .callbackInstance(cbInstance) + .add("add").remove("remove") + .impl(baWithCb) + .provides(BundleAdapter.class.getName()) + .build(); + + // create a service provider and consumer + Consumer c = new Consumer(); + Component consumer = component(m) + .impl(c) + .withSvc(BundleAdapter.class, s->s.add("add").remove("remove")) + .build(); + + // add the bundle adapter + m.add(adapter); + // add the service consumer + m.add(consumer); + // check if at least one bundle was found + c.check(); + // remove the consumer again + m.remove(consumer); + // check if all bundles were removed correctly + c.doubleCheck(); + // remove the bundle adapter + m.remove(adapter); + } + + public void testBundleAdapterWithCallbackInstanceRef() { + DependencyManager m = getDM(); + // create a bundle adapter service (one is created for each bundle) + BundleAdapterWithCallback baWithCb = new BundleAdapterWithCallback(); + BundleAdapterCallbackInstance cbInstance = new BundleAdapterCallbackInstance(baWithCb); + + Component adapter = bundleAdapter(m) + .mask(Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE) + .add(cbInstance::addRef).remove(cbInstance::removeRef) + .impl(baWithCb) + .provides(BundleAdapter.class.getName()) + .build(); + + // create a service provider and consumer + Consumer c = new Consumer(); + Component consumer = component(m) + .impl(c) + .withSvc(BundleAdapter.class, s->s.add("add").remove("remove")) + .build(); + + // add the bundle adapter + m.add(adapter); + // add the service consumer + m.add(consumer); + // check if at least one bundle was found + c.check(); + // remove the consumer again + m.remove(consumer); + // check if all bundles were removed correctly + c.doubleCheck(); + // remove the bundle adapter + m.remove(adapter); + } + + public static class BundleAdapter { + volatile Bundle m_bundle; + + Bundle getBundle() { + return m_bundle; + } + } + + public static class BundleAdapterWithCallback extends BundleAdapter { + void add(Bundle b) { + m_bundle = b; + } + + void remove(Bundle b) { + m_bundle = null; + } + } + + public static class BundleAdapterCallbackInstance { + final BundleAdapterWithCallback m_ba; + + BundleAdapterCallbackInstance(BundleAdapterWithCallback ba) { + m_ba = ba; + } + + void add(Component c, Bundle b) { // reflection callback + m_ba.add(b); + } + + void addRef(Bundle b, Component c) { // method reference callback + add(c, b); + } + + void remove(Component c, Bundle b) { // reflection callback + m_ba.remove(b); + } + + void removeRef(Bundle b, Component c) { // method reference callback + remove (c, b); + } + } + + static class Consumer { + private volatile int m_count = 0; + + public void add(BundleAdapter ba) { + Bundle b = ba.getBundle(); + System.out.println("Consumer.add(" + b.getSymbolicName() + ")"); + Assert.assertNotNull("bundle instance must not be null", b); + m_count++; + } + + public void check() { + Assert.assertTrue("we should have found at least one bundle", m_count > 0); + } + + public void remove(BundleAdapter ba) { + Bundle b = ba.getBundle(); + System.out.println("Consumer.remove(" + b.getSymbolicName() + ")"); + m_count--; + } + + public void doubleCheck() { + Assert.assertEquals("all bundles we found should have been removed again", 0, m_count); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/BundleAdapterWithCallbacksNotAutoConfiguredTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/BundleAdapterWithCallbacksNotAutoConfiguredTest.java new file mode 100644 index 00000000000..2a55f03e324 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/BundleAdapterWithCallbacksNotAutoConfiguredTest.java @@ -0,0 +1,67 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.bundleAdapter; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.Bundle; + +/** + * @author Felix Project Team + */ +public class BundleAdapterWithCallbacksNotAutoConfiguredTest extends TestBase { + final Ensure m_e = new Ensure(); + + public void testBundleAdapterWithCallbacksNotAutoConfigured() { + DependencyManager m = getDM(); + // create a bundle adapter service (one is created for each bundle) + BundleAdapterWithCallback baWithCb = new BundleAdapterWithCallback(); + String bsn = "org.apache.felix.dependencymanager"; + String filter = "(Bundle-SymbolicName=" + bsn + ")"; + + Component adapter = bundleAdapter(m).mask(Bundle.ACTIVE).filter(filter).add("add").impl(baWithCb).build(); + + // add the bundle adapter + m.add(adapter); + + // Check if adapter has not been auto configured (because it has callbacks defined). + m_e.waitForStep(1, 3000); + Assert.assertNull("bundle adapter must not be auto configured", baWithCb.getBundle()); + + // remove the bundle adapters + m.remove(adapter); + } + + class BundleAdapterWithCallback { + volatile Bundle m_bundle; // must not be auto configured because we are using callbacks. + + Bundle getBundle() { + return m_bundle; + } + + void add(Bundle b) { + Assert.assertNotNull(b); + Assert.assertEquals("org.apache.felix.dependencymanager", b.getSymbolicName()); + m_e.step(1); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/BundleDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/BundleDependencyTest.java new file mode 100644 index 00000000000..5924d9326c3 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/BundleDependencyTest.java @@ -0,0 +1,232 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.Bundle; + +/** + * @author Felix Project Team + */ +public class BundleDependencyTest extends TestBase { + private final static String BSN = "org.apache.felix.metatype"; + + public void testBundleDependencies() { + DependencyManager m = getDM(); + // create a service provider and consumer + MyConsumer c = new MyConsumer(); + Component consumer = component(m, comp -> comp.impl(c).withBundle(bundle -> bundle.add("add").remove("remove"))); + + // check if at least one bundle was found + c.check(); + // remove the consumer again + m.remove(consumer); + // check if all bundles were removed correctly + c.doubleCheck(); + + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + String filter = "(Bundle-SymbolicName=" + BSN + ")"; + Component consumerWithFilter = component(m, comp -> comp.impl(new FilteredConsumer(e)) + .withBundle(bundle-> bundle.filter(filter).add("add").remove("remove"))); + e.step(2); + // remove the consumer again + m.remove(consumerWithFilter); + e.step(4); + } + + public void testBundleDependenciesRef() { + DependencyManager m = getDM(); + // create a service provider and consumer + MyConsumer c = new MyConsumer(); + Component consumer = component(m, comp -> comp.impl(c).withBundle(bundle -> bundle.add(MyConsumer::add).remove(MyConsumer::remove))); + + // check if at least one bundle was found + c.check(); + // remove the consumer again + m.remove(consumer); + // check if all bundles were removed correctly + c.doubleCheck(); + + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + String filter = "(Bundle-SymbolicName=" + BSN + ")"; + Component consumerWithFilter = component(m, comp -> comp.impl(new FilteredConsumer(e)) + .withBundle(bundle-> bundle.filter(filter).add(FilteredConsumer::add).remove(FilteredConsumer::remove))); + e.step(2); + // remove the consumer again + m.remove(consumerWithFilter); + e.step(4); + } + + public void testRequiredBundleDependency() { + DependencyManager m = getDM(); + + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + Component consumerWithFilter = component(m, c -> c.impl(new FilteredConsumerRequired(e)) + .withBundle(b -> b.filter("(Bundle-SymbolicName=" + BSN + ")").add("add").remove("remove"))); + e.waitForStep(1, 5000); + // remove the consumer again + m.remove(consumerWithFilter); + e.waitForStep(2, 5000); + } + + public void testRequiredBundleDependencyRef() { + DependencyManager m = getDM(); + + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + FilteredConsumerRequired impl = new FilteredConsumerRequired(e); + Component consumerWithFilter = component(m, c -> c.impl(impl) + .withBundle(b -> b.filter("(Bundle-SymbolicName=" + BSN + ")").add(impl::add).remove(impl::remove))); + e.waitForStep(1, 5000); + // remove the consumer again + m.remove(consumerWithFilter); + e.waitForStep(2, 5000); + } + + public void testRequiredBundleDependencyWithComponentArgInCallbackMethod() { + DependencyManager m = getDM(); + + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // add a consumer with a filter + FilteredConsumerRequiredWithComponentArg impl = new FilteredConsumerRequiredWithComponentArg(e); + Component consumerWithFilter = component(m, c -> c.impl(impl) + .withBundle(b -> b.filter("(Bundle-SymbolicName=" + BSN + ")").add("add").remove("remove"))); + e.waitForStep(1, 5000); + // remove the consumer again + m.remove(consumerWithFilter); + e.waitForStep(2, 5000); + } + + public void testRequiredBundleDependencyWithComponentArgInCallbackMethodRef() { + DependencyManager m = getDM(); + + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + FilteredConsumerRequiredWithComponentArg impl = new FilteredConsumerRequiredWithComponentArg(e); + Component consumerWithFilter = component(m).impl(impl) + .withBundle(b -> b.filter("(Bundle-SymbolicName=" + BSN + ")").add(impl::addRef).remove(impl::removeRef)).build(); + // add a consumer with a filter + m.add(consumerWithFilter); + e.waitForStep(1, 5000); + // remove the consumer again + m.remove(consumerWithFilter); + e.waitForStep(2, 5000); + } + + static class MyConsumer { + private volatile int m_count = 0; + + public void add(Bundle b) { + System.out.println("Consumer.add(" + b.getSymbolicName() + ")"); + Assert.assertNotNull("bundle instance must not be null", b); + m_count++; + } + + public void check() { + Assert.assertTrue("we should have found at least one bundle", m_count > 0); + } + + public void remove(Bundle b) { + System.out.println("Consumer.remove(" + b.getSymbolicName() + ")"); + m_count--; + } + + public void doubleCheck() { + Assert.assertEquals("all bundles we found should have been removed again", 0, m_count); + } + } + + static class FilteredConsumer { + private final Ensure m_ensure; + + public FilteredConsumer(Ensure e) { + m_ensure = e; + } + + public void add(Bundle b) { + m_ensure.step(1); + } + + public void remove(Bundle b) { + m_ensure.step(3); + } + } + + static class FilteredConsumerRequired { + private final Ensure m_ensure; + + public FilteredConsumerRequired(Ensure e) { + m_ensure = e; + } + + public void add(Bundle b) { + System.out.println("Bundle is " + b); +// Assert.assertNotNull(b); + if (b.getSymbolicName().equals(BSN)) { + m_ensure.step(1); + } + } + + public void remove(Bundle b) { + Assert.assertNotNull(b); + if (b.getSymbolicName().equals(BSN)) { + m_ensure.step(2); + } + } + } + + static class FilteredConsumerRequiredWithComponentArg { + private final Ensure m_ensure; + + public FilteredConsumerRequiredWithComponentArg(Ensure e) { + m_ensure = e; + } + + public void add(Component component, Bundle b) { // method ref callback + Assert.assertNotNull(component); + if (b.getSymbolicName().equals(BSN)) { + m_ensure.step(1); + } + } + + public void addRef(Bundle b, Component component) { // method ref callback + add(component, b); + } + + public void removeRef(Bundle b, Component component) { // method ref callback + remove(component, b); + } + + public void remove(Component component, Bundle b) { // method ref callback + Assert.assertNotNull(component); + Assert.assertNotNull(b); + if (b.getSymbolicName().equals(BSN)) { + m_ensure.step(2); + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/CFDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/CFDependencyTest.java new file mode 100644 index 00000000000..6b70661f57c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/CFDependencyTest.java @@ -0,0 +1,94 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import java.util.concurrent.CompletableFuture; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; + +/** + * CompletableFuture Dependency test. + * @author Felix Project Team + */ +public class CFDependencyTest extends TestBase { + private final Ensure m_ensure = new Ensure(); + + public void testComponentWithCFDependencyDefinedFromInit() throws Exception { + final DependencyManager dm = getDM(); + + // Create a consumer depending on a Provider and on the result of a CF. + // (the CF dependency is added from the Consumer.init() method). + CompletableFuture cf = new CompletableFuture<>(); + Component consumer = component(dm).impl(new Consumer(cf)).withSvc(Provider.class, true).build(); + + // Create provider + Component provider = component(dm).impl(new ProviderImpl()).provides(Provider.class).build(); + + dm.add(consumer); + dm.add(provider); + + // Sets the result in the CF: this will normally trigger the activation of the Consumer component. + cf.complete("cfFesult"); + + m_ensure.waitForStep(3, 5000); + dm.clear(); + } + + public static interface Provider { + void invoke(); + } + + public class ProviderImpl implements Provider { + @Override + public void invoke() { + m_ensure.step(3); + } + } + + public class Consumer { + private Provider m_provider; // injected + private String m_cfResult; // injected + private final CompletableFuture m_cf; + + Consumer(CompletableFuture cf) { + m_cf = cf; + } + + void init(Component c) { + component(c, comp -> comp.withFuture(m_cf, result -> result.complete(this::add))); + } + + void add(String cfResult) { + m_cfResult = cfResult; + Assert.assertNotNull(m_cfResult); + Assert.assertEquals("cfFesult", m_cfResult); + m_ensure.step(1); + } + + void start() { + Assert.assertNotNull(m_provider); + m_ensure.step(2); + m_provider.invoke(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ComponentTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ComponentTest.java new file mode 100644 index 00000000000..256cad89a88 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ComponentTest.java @@ -0,0 +1,114 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import java.util.Dictionary; + +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; + +/** + * @author Felix Project Team + */ +public class ComponentTest extends TestBase { + private final Ensure m_ensure = new Ensure(); + + public void testSimple() throws Exception { + final DependencyManager dm = getDM(); + + // Create consumer (dependency is required by default using builder api). + component(dm, comp -> comp + .factory(Consumer::new) + .withSvc(Provider.class, srv -> srv.filter("(name=provider2)").add(Consumer::add).remove(Consumer::remove)) + .withSvc(Provider.class, srv -> srv.filter("(name=provider1)").autoConfig("m_autoConfiguredProvider"))); + + // Create providers (auto added to dependency manager) + component(dm, comp -> comp + .impl(new Provider() { public String toString() { return "provider1";}}) + .provides(Provider.class).properties("name", "provider1")); + + component(dm, comp -> comp + .impl(new Provider() { public String toString() { return "provider2";}}) + .provides(Provider.class).properties("name", "provider2")); + + m_ensure.waitForStep(2, 5000); + dm.clear(); + m_ensure.waitForStep(5, 5000); + } + + public void testSimple2() throws Exception { + final DependencyManager dm = getDM(); + + // Create consumer (dependency is required by default using builder api). + component(dm, comp -> comp + .factory(Consumer::new) + .withSvc(Provider.class, srv -> srv.filter("(name=provider2)").add("add").remove("remove")) + .withSvc(Provider.class, "(name=provider1)", "m_autoConfiguredProvider", true)); + + // Create providers (auto added to dependency manager) + component(dm, comp -> comp + .impl(new Provider() { public String toString() { return "provider1";}}) + .provides(Provider.class).properties("name", "provider1")); + + component(dm, comp -> comp + .impl(new Provider() { public String toString() { return "provider2";}}) + .provides(Provider.class).properties("name", "provider2")); + + m_ensure.waitForStep(2, 5000); + dm.clear(); + m_ensure.waitForStep(5, 5000); + } + + + public static interface Provider { + } + + public class Consumer { + Provider m_provider; + Provider m_autoConfiguredProvider; + + void add(Provider provider, Dictionary props) { + Assert.assertNotNull(provider); + Assert.assertEquals("provider2", props.get("name")); + m_provider = provider; + m_ensure.step(1); + } + + void start() { + Assert.assertNotNull(m_autoConfiguredProvider); + Assert.assertEquals("provider1", m_autoConfiguredProvider.toString()); + m_ensure.step(2); + } + + void stop() { + m_ensure.step(3); + } + + void destroy() { + m_ensure.step(4); + } + + void remove(Provider provider, Dictionary props) { + Assert.assertEquals(m_provider, provider); + m_ensure.step(5); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/CompositionTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/CompositionTest.java new file mode 100644 index 00000000000..87cf2a919b2 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/CompositionTest.java @@ -0,0 +1,123 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; + + +/** + * @author Felix Project Team + */ +public class CompositionTest extends TestBase { + public void testComposition() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class).build(); + Component sc = component(m).impl(new ServiceConsumer(e)).composition("getComposition") + .withSvc(ServiceInterface.class, sb->sb.add("add")).build(); + m.add(sp); + m.add(sc); + // ensure we executed all steps inside the component instance + e.step(6); + m.clear(); + } + + public void testCompositionRef() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class).build(); + ServiceConsumer scimpl = new ServiceConsumer(e); + Component sc = component(m).impl(scimpl).composition(scimpl::getComposition) + .withSvc(ServiceInterface.class, sb->sb.add("add")).build(); + m.add(sp); + m.add(sc); + // ensure we executed all steps inside the component instance + e.step(6); + m.clear(); + } + + static interface ServiceInterface { + public void invoke(); + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke() { + m_ensure.step(4); + } + } + + static class ServiceConsumer { + private final Ensure m_ensure; + private ServiceConsumerComposite m_composite; + @SuppressWarnings("unused") + private ServiceInterface m_service; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + m_composite = new ServiceConsumerComposite(m_ensure); + } + + public Object[] getComposition() { + return new Object[] { this, m_composite }; + } + + void add(ServiceInterface service) { + m_ensure.step(1); + m_service = service; // This method seems to not being called anymore + } + + void start() { + m_composite.invoke(); + m_ensure.step(5); + } + } + + static class ServiceConsumerComposite { + ServiceInterface m_service; + private Ensure m_ensure; + + ServiceConsumerComposite(Ensure ensure) + { + m_ensure = ensure; + } + + void add(ServiceInterface service) { + + m_ensure.step(2); + m_service = service; + } + + void invoke() + { + m_ensure.step(3); + m_service.invoke(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/DynamicProxyAspectTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/DynamicProxyAspectTest.java new file mode 100644 index 00000000000..099d4178018 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/DynamicProxyAspectTest.java @@ -0,0 +1,254 @@ +/** + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.aspect; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; + +/** + * @author Felix Project Team + */ +@SuppressWarnings({"rawtypes"}) +public class DynamicProxyAspectTest extends TestBase { + public void testImplementGenericAspectWithDynamicProxyAndFactory() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + DynamicProxyHandler.resetCounter(); + + // create two service providers, each providing a different service interface + Component sp1 = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class).build(); + Component sp2 = component(m).impl(new ServiceProvider2(e)).provides(ServiceInterface2.class).build(); + + // create a dynamic proxy based aspect and hook it up to both services + Component a1 = aspect(m, ServiceInterface.class) + .rank(10) + .autoConfig("m_service") + .factory(new Factory(e, ServiceInterface.class, "ServiceInterfaceProxy"), "create") + .build(); + + Component a2 = aspect(m, ServiceInterface2.class) + .rank(10) + .autoConfig("m_service") + .factory(new Factory(e, ServiceInterface2.class, "ServiceInterfaceProxy2"), "create") + .build(); + + // create a client that invokes a method on boths services, validate that it goes + // through the proxy twice + Component sc = component(m) + .impl(new ServiceConsumer(e)) + .withSvc(true, ServiceInterface.class, ServiceInterface2.class).build(); + + // register both producers, validate that both services are started + m.add(sp1); + e.waitForStep(1, 2000); + m.add(sp2); + e.waitForStep(2, 2000); + + // add both aspects, and validate that both instances have been created + m.add(a1); + m.add(a2); + e.waitForStep(4, 4000); + + // add the client, which will automatically invoke both services + m.add(sc); + + // wait until both services have been invoked + e.waitForStep(6, 4000); + + // make sure the proxy has been called twice + Assert.assertEquals("Proxy should have been invoked this many times.", 2, DynamicProxyHandler.getCounter()); + + m.remove(sc); + m.remove(a2); + m.remove(a1); + m.remove(sp2); + m.remove(sp1); + m.remove(a2); + m.remove(a1); + } + + public void testImplementGenericAspectWithDynamicProxyAndFactoryRef() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + DynamicProxyHandler.resetCounter(); + + // create two service providers, each providing a different service interface + Component sp1 = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class).build(); + Component sp2 = component(m).impl(new ServiceProvider2(e)).provides(ServiceInterface2.class).build(); + + // create a dynamic proxy based aspect and hook it up to both services + Component a1 = aspect(m, ServiceInterface.class).rank(10).autoConfig("m_service") + .factory(() -> new Factory(e, ServiceInterface.class, "ServiceInterfaceProxy"), Factory::create).build(); + Component a2 = aspect(m, ServiceInterface2.class).rank(10).autoConfig("m_service") + .factory(() -> new Factory(e, ServiceInterface2.class, "ServiceInterfaceProxy2"), Factory::create).build(); + + // create a client that invokes a method on boths services, validate that it goes + // through the proxy twice + Component sc = component(m) + .impl(new ServiceConsumer(e)) + .withSvc(true, ServiceInterface.class, ServiceInterface2.class).build(); + + // register both producers, validate that both services are started + m.add(sp1); + e.waitForStep(1, 2000); + m.add(sp2); + e.waitForStep(2, 2000); + + // add both aspects, and validate that both instances have been created + m.add(a1); + m.add(a2); + e.waitForStep(4, 4000); + + // add the client, which will automatically invoke both services + m.add(sc); + + // wait until both services have been invoked + e.waitForStep(6, 4000); + + // make sure the proxy has been called twice + Assert.assertEquals("Proxy should have been invoked this many times.", 2, DynamicProxyHandler.getCounter()); + + m.remove(sc); + m.remove(a2); + m.remove(a1); + m.remove(sp2); + m.remove(sp1); + m.remove(a2); + m.remove(a1); + } + + static interface ServiceInterface { + public void invoke(Runnable run); + } + + static interface ServiceInterface2 { + public void invoke(Runnable run); + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void start() { + m_ensure.step(1); + } + public void invoke(Runnable run) { + run.run(); + } + } + + static class ServiceProvider2 implements ServiceInterface2 { + private final Ensure m_ensure; + public ServiceProvider2(Ensure ensure) { + m_ensure = ensure; + } + public void start() { + m_ensure.step(2); + } + public void invoke(Runnable run) { + run.run(); + } + } + + static class ServiceConsumer implements Runnable { + private volatile ServiceInterface m_service; + private volatile ServiceInterface2 m_service2; + private final Ensure m_ensure; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void init() { + Thread t = new Thread(this); + t.start(); + } + + public void run() { + m_service.invoke(Ensure.createRunnableStep(m_ensure, 5)); + m_service2.invoke(Ensure.createRunnableStep(m_ensure, 6)); + } + } + + static class DynamicProxyHandler implements InvocationHandler { + public volatile Object m_service; // ISSUE, we cannot inject into "Object" at the moment + private final String m_label; + private static volatile int m_counter = 0; + + public DynamicProxyHandler(String label) { + m_label = label; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + System.out.println("IIIIIIINVOKE--------------------------" + method.getName()); + if (m_service == null) { + Assert.fail("No service was injected into dynamic proxy handler " + m_label); + } + Method m = m_service.getClass().getMethod(method.getName(), method.getParameterTypes()); + if (m == null) { + Assert.fail("No method " + method.getName() + " was found in instance " + m_service + " in dynamic proxy handler " + m_label); + } + if (method.getName().equals("invoke")) { + // only count methods called 'invoke' because those are actually the ones + // both interfaces implement (and the dynamic proxy might be invoked for + // other methods, such as toString() as well) + m_counter++; + } + return m.invoke(m_service, args); + } + + public static int getCounter() { + return m_counter; + } + + public static void resetCounter() { + m_counter = 0; + } + } + + static class Factory { + private final String m_label; + private Class m_class; + private final Ensure m_ensure; + + public Factory(Ensure ensure, Class clazz, String label) { + m_ensure = ensure; + m_class = clazz; + m_label = label; + } + + public Object create() { + m_ensure.step(); + return Proxy.newProxyInstance(m_class.getClassLoader(), new Class[] { m_class }, new DynamicProxyHandler(m_label)); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/DynamicScopedServiceTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/DynamicScopedServiceTest.java new file mode 100644 index 00000000000..7dac78f469f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/DynamicScopedServiceTest.java @@ -0,0 +1,227 @@ +/* + * 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.dm.lambda.itest; + +import java.util.Map; +import java.util.Properties; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Component.ServiceScope; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceRegistration; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +/** + * Validates a simple scoped service, which adds a dynamic dependency from init method. + * Notice the the prototype will add a dynamic dependency from its init method, and the dependency service + * properties will be propagated. + */ +public class DynamicScopedServiceTest extends TestBase { + static Ensure m_e; + static Ensure.Steps m_serviceImplInitSteps; + static Ensure.Steps m_serviceImplStartSteps; + static Ensure.Steps m_serviceImplStopSteps; + static Ensure.Steps m_serviceConsumerBindSteps; + static Ensure.Steps m_serviceConsumerUnbindSteps; + + public void setUp() throws Exception { + super.setUp(); + m_e = new Ensure(); + m_serviceImplInitSteps = new Ensure.Steps(1, 2, 5, 12, 13); + m_serviceImplStartSteps = new Ensure.Steps(3, 6, 14); + m_serviceImplStopSteps = new Ensure.Steps(9, 11, 17); + m_serviceConsumerBindSteps = new Ensure.Steps(4, 7, 15); + m_serviceConsumerUnbindSteps = new Ensure.Steps(8, 10, 16); + } + + public void testPrototypeComponentWithFactory() { + testPrototypeComponent(true); + } + + public void testPrototypeComponentWithoutFactory() { + testPrototypeComponent(false); + } + + private void testPrototypeComponent(boolean useFactory) { + DependencyManager m = getDM(); + + Component provider = null; + + if (useFactory) { + provider = component(m) + .factory(this, "createServiceImpl") + .scope(ServiceScope.PROTOTYPE) + .provides(Service.class) + .withSvc(Service3.class, svc -> svc.optional().autoConfig("m_service3")) + .build(); + + } else { + provider = component(m) + .impl(ServiceImplWithConstructor.class) + .scope(ServiceScope.PROTOTYPE) + .provides(Service.class) + .withSvc(Service3.class, svc -> svc.optional().autoConfig("m_service3")) + .build(); + } + + Properties props = new Properties(); + props.put("foo", "bar"); + Component service2 = component(m) + .provides(Service2.class.getName(), props) + .impl(new Service2() {}) + .build(); + + Component service3 = component(m) + .provides(Service3.class.getName()) + .impl(new Service3() {}) + .build(); + + Component consumer1 = component(m) + .impl(new ServiceConsumer()) + .withSvc(Service.class, svc -> svc.required().add("bind").remove("unbind")) + .build(); + + Component consumer2 = component(m) + .impl(new ServiceConsumer()) + .withSvc(Service.class, svc -> svc.required().add("bind").remove("unbind")) + .build(); + + m.add(service3); // add service3 (the provider has an optional callback on it) + m.add(provider); // add provider + m.add(service2); // add service2 (the prototype depends on it) + m.add(consumer1); // add first consumer + m_e.waitForStep(1, 5000); // Service prototype instance called in init + m_e.waitForStep(2, 5000); // first clone called in init + m_e.waitForStep(3, 5000); // first clone called in init + m_e.waitForStep(4, 5000); // first consumer bound to first clone + + m.add(consumer2); // add second consumer + m_e.waitForStep(5, 5000); // second clone called in init + m_e.waitForStep(6, 5000); // second clone called in start + m_e.waitForStep(7, 5000); // second consumer bound to second clone + + // make sure both consumers have a different provider instances. + ServiceConsumer consumer1Impl = consumer1.getInstance(); + Assert.assertNotNull(consumer1Impl.getService()); + ServiceConsumer consumer2Impl = consumer2.getInstance(); + Assert.assertNotNull(consumer2Impl.getService()); + Assert.assertNotEquals(consumer1Impl.getService(), consumer2Impl.getService()); + + m.remove(consumer1); // remove consumer1 + m_e.waitForStep(8, 5000); // consumer1 unbound from first clone + m_e.waitForStep(9, 5000); // first clone stopped + + m.remove(provider); // unregister the provider + m_e.waitForStep(10, 5000); // consumer2 unbound from second clone + m_e.waitForStep(11, 5000); // second clone stopped + + m.add(provider); // re-register the provider + m_e.waitForStep(12, 5000); // prototype init called + m_e.waitForStep(13, 5000); // third clone init method called (because consumer2 is active) + m_e.waitForStep(14, 5000); // third clone start method called + m_e.waitForStep(15, 5000); // consumer2 bound to third clone + + m.remove(service2); // remove the service2 (it will destroy the clone) + m_e.waitForStep(16, 5000); // consumer2 unbound + m_e.waitForStep(17, 5000); // third clone stopped + + m.remove(provider); + m.remove(service3); + m.clear(); + } + + @SuppressWarnings("unused") + private ServiceImpl createServiceImpl() { + return new ServiceImpl(); + } + + public interface Service { + } + + public interface Service2 { + } + + public interface Service3 { + } + + public static class ServiceImpl implements Service { + volatile Bundle m_bundle; // bundle requesting the service, injected by reflection or from constructor + volatile ServiceRegistration m_registration; // registration of the requested service, injected by reflection or from constructor + volatile Service2 m_service2; + private Service3 m_service3; + + void init(Component c) { // only called on prototype instance, not on clones + component(c, comp -> + comp.withSvc(Service2.class, svc -> svc.required().add("bind").propagate())); + m_e.steps(m_serviceImplInitSteps); // 1, 2, 5, 12, 13 + } + + void bind(Service2 service2, Map properties) { + // check if prototype service properties has propagated the Service2 dependency service properties + Assert.assertEquals("bar", properties.get("foo")); + m_service2 = service2; + } + + void start() { + Assert.assertNotNull(m_bundle); + Assert.assertNotNull(m_registration); + Assert.assertNotNull(m_service2); + Assert.assertNotNull(m_service3); + m_e.steps(m_serviceImplStartSteps); // 3, 6, 14 + } + + Service3 getService3() { + return m_service3; + } + + void stop() { + m_e.steps(m_serviceImplStopSteps); // 9, 11, 17 + } + } + + public static class ServiceImplWithConstructor extends ServiceImpl { + /** + * Inject requesting bundle and service registration using class constructor, NOT using field reflection + */ + public ServiceImplWithConstructor(Bundle b, ServiceRegistration reg) { + m_bundle = b; + m_registration = reg; + } + } + + public class ServiceConsumer { + volatile Service m_myService; + + public void bind(Service service) { + m_myService = service; + m_e.steps(m_serviceConsumerBindSteps); // 4, 7, 15 + } + + public void unbind(Service service) { + Assert.assertEquals(m_myService, service); + m_e.steps(m_serviceConsumerUnbindSteps); // 8, 10, 16 + } + + public Service getService() { + return m_myService; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/Ensure.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/Ensure.java new file mode 100644 index 00000000000..20040d2b3e4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/Ensure.java @@ -0,0 +1,174 @@ +/* + * 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.dm.lambda.itest; + +import java.io.PrintStream; + +import org.junit.Assert; + +/** + * Helper class to make sure that steps in a test happen in the correct order. Instantiate + * this class and subsequently invoke step(nr) with steps starting at 1. You + * can also have threads wait until you arrive at a certain step. + * + * @author Felix Project Team + */ +public class Ensure { + private final boolean DEBUG; + private static long INSTANCE = 0; + private static final int RESOLUTION = 100; + private static PrintStream STREAM = System.out; + int step = 0; + private Throwable m_throwable; + + public Ensure() { + this(true); + } + + public Ensure(boolean debug) { + DEBUG = debug; + if (DEBUG) { + INSTANCE++; + } + } + + public void setStream(PrintStream output) { + STREAM = output; + } + + /** + * Mark this point as step nr. + * + * @param nr the step we are in + */ + public synchronized void step(int nr) { + step++; + Assert.assertEquals(nr, step); + if (DEBUG) { + String info = getLineInfo(3); + STREAM.println("[Ensure " + INSTANCE + "] step " + step + " [" + currentThread() + "] " + info); + } + notifyAll(); + } + + private String getLineInfo(int depth) { + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + String info = trace[depth].getClassName() + "." + trace[depth].getMethodName() + ":" + trace[depth].getLineNumber(); + return info; + } + + /** + * Mark this point as the next step. + */ + public synchronized void step() { + step++; + if (DEBUG) { + String info = getLineInfo(3); + STREAM.println("[Ensure " + INSTANCE + "] next step " + step + " [" + currentThread() + "] " + info); + } + notifyAll(); + } + + /** + * Wait until we arrive at least at step nr in the process, or fail if that + * takes more than timeout milliseconds. If you invoke wait on a thread, + * you are effectively assuming some other thread will invoke the step(nr) + * method. + * + * @param nr the step to wait for + * @param timeout the number of milliseconds to wait + */ + public synchronized void waitForStep(int nr, int timeout) { + final int initialTimeout = timeout; + if (DEBUG) { + String info = getLineInfo(3); + STREAM.println("[Ensure " + INSTANCE + "] waiting for step " + nr + " [" + currentThread() + "] " + info); + } + while (step < nr && timeout > 0) { + try { + wait(RESOLUTION); + timeout -= RESOLUTION; + } + catch (InterruptedException e) {} + } + if (step < nr) { + throw new IllegalStateException("Timed out waiting for " + initialTimeout + " ms for step " + nr + ", we are still at step " + step); + } + if (DEBUG) { + String info = getLineInfo(3); + STREAM.println("[Ensure " + INSTANCE + "] arrived at step " + nr + " [" + currentThread() + "] " + info); + } + } + + private String currentThread() { + Thread thread = Thread.currentThread(); + return thread.getId() + " " + thread.getName(); + } + + public static Runnable createRunnableStep(final Ensure ensure, final int nr) { + return new Runnable() { public void run() { ensure.step(nr); }}; + } + + public synchronized void steps(Steps steps) { + steps.next(this); + } + + /** + * Helper class for naming a list of step numbers. If used with the steps(Steps) method + * you can define at which steps in time this point should be passed. That means you can + * check methods that will get invoked multiple times during a test. + */ + public static class Steps { + private final int[] m_steps; + private int m_stepIndex; + + /** + * Create a list of steps and initialize the step counter to zero. + */ + public Steps(int... steps) { + m_steps = steps; + m_stepIndex = 0; + } + + /** + * Ensure we're at the right step. Will throw an index out of bounds exception if we enter this step more often than defined. + */ + public void next(Ensure ensure) { + ensure.step(m_steps[m_stepIndex++]); + } + } + + /** + * Saves a thrown exception that occurred in a different thread. You can only save one exception + * at a time this way. + */ + public synchronized void throwable(Throwable throwable) { + m_throwable = throwable; + } + + /** + * Throws a Throwable if one occurred in a different thread and that thread saved it + * using the throwable() method. + */ + public synchronized void ensure() throws Throwable { + if (m_throwable != null) { + throw m_throwable; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/FELIX5406_FluentPropertyWithDotTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/FELIX5406_FluentPropertyWithDotTest.java new file mode 100644 index 00000000000..aa6a7bdc1fd --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/FELIX5406_FluentPropertyWithDotTest.java @@ -0,0 +1,88 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import java.util.Map; + +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; + +/** + * @author Felix Project Team + */ +public class FELIX5406_FluentPropertyWithDotTest extends TestBase { + private final Ensure m_ensure = new Ensure(); + + public void testFluentServiceProperty() { + if (! isJava8()) return; + final DependencyManager dm = getDM(); + + component(dm, comp -> comp.factory(ProviderImpl::new).provides(Provider.class, foo -> "bar")); + component(dm, comp -> comp.impl(new Consumer("foo", "bar")) + .withSvc(Provider.class, svc -> svc.filter("(foo=bar)").required().add(Consumer::bind))); + + m_ensure.waitForStep(1, 5000); + } + + public void testFluentServicePropertyWithDot() { + if (! isJava8()) return; + final DependencyManager dm = getDM(); + + component(dm, comp -> comp.factory(ProviderImpl::new).provides(Provider.class, foo_bar -> "zoo")); + component(dm, comp -> comp.impl(new Consumer("foo.bar", "zoo")) + .withSvc(Provider.class, svc -> svc.filter("(foo.bar=zoo)").required().add(Consumer::bind))); + + m_ensure.waitForStep(1, 5000); + } + + public void testFluentServicePropertyWithUnderscore() { + if (! isJava8()) return; + final DependencyManager dm = getDM(); + + component(dm, comp -> comp.factory(ProviderImpl::new).provides(Provider.class, foo__bar -> "zoo")); + component(dm, comp -> comp.impl(new Consumer("foo_bar", "zoo")) + .withSvc(Provider.class, svc -> svc.filter("(foo_bar=zoo)").required().add(Consumer::bind))); + + m_ensure.waitForStep(1, 5000); + } + + interface Provider { + } + + public class ProviderImpl implements Provider { + } + + public class Consumer { + private final String m_key; + private final String m_value; + + Consumer(String key, String value) { + m_key = key; + m_value = value; + } + + void bind(Provider provider, Map properties) { + Assert.assertEquals(m_value, properties.get(m_key)); + m_ensure.step(1); + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/FELIX5516Test.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/FELIX5516Test.java new file mode 100644 index 00000000000..e4681d54187 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/FELIX5516Test.java @@ -0,0 +1,107 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.PrototypeServiceFactory; +import org.osgi.framework.ServiceObjects; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; + +/** + * Validates that a service dependency is not dereferenced internally by DM. + * When you use a method reference for the dependency callback, then dm-lambda auto-detect that the + * service dependency must not be internally dereferenced. + * But you when you method reflection based callbacks, you have to call the "dereference(false)" method + * from the ServiceDependencyBuilder, which indicates to DM that the service reference must not be + * dereferenced internally (using BundleContext.getServiceReference() method). + */ +public class FELIX5516Test extends TestBase { + private final Ensure m_ensure = new Ensure(); + + public void testServiceNotDereferencedInternallyUsingMethodReference() throws Exception { + final DependencyManager dm = getDM(); + Component service = component(dm).impl(new Factory()).provides(Service.class).build(); + Component client = component(dm).impl(new Client()).withSvc(Service.class, svc -> svc.required().add(Client::bind)).build(); + dm.add(service); + dm.add(client); + m_ensure.waitForStep(9, 5000); + dm.clear(); + } + + public void testServiceNotDereferencedInternallyUsingReflectionCallback() throws Exception { + final DependencyManager dm = getDM(); + Component service = component(dm).impl(new Factory()).provides(Service.class).build(); + Component client = component(dm).impl(new Client()).withSvc(Service.class, svc -> svc.required().dereference(false).add("bind")).build(); + dm.add(service); + dm.add(client); + m_ensure.waitForStep(9, 5000); + dm.clear(); + } + + public interface Service {} + + public class ServiceImpl implements Service { + + } + + public class Factory implements PrototypeServiceFactory { + @Override + public Service getService(Bundle bundle, ServiceRegistration registration) { + m_ensure.step(); + return new ServiceImpl(); + } + + @Override + public void ungetService(Bundle bundle, ServiceRegistration registration, Service service) { + m_ensure.step(); + } + } + + public class Client { + ServiceReference m_ref; + BundleContext m_ctx; + + void bind(ServiceReference ref) { + m_ref = ref; + } + + void start() { + ServiceObjects sobjs = m_ctx.getServiceObjects(m_ref); + + m_ensure.step(1); + Service s1 = sobjs.getService(); + + m_ensure.step(3); + Service s2 = sobjs.getService(); + + m_ensure.step(5); + sobjs.ungetService(s1); + + m_ensure.step(7); + sobjs.ungetService(s2); + m_ensure.step(9); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/FactoryConfigurationAdapterTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/FactoryConfigurationAdapterTest.java new file mode 100644 index 00000000000..2d425f7b49e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/FactoryConfigurationAdapterTest.java @@ -0,0 +1,276 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.factoryPidAdapter; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class FactoryConfigurationAdapterTest extends TestBase +{ + private static Ensure m_ensure; + + public void testFactoryConfigurationAdapter() { + testFactoryConfigurationAdapter(Adapter.class, "updated"); + } + + public void testFactoryConfigurationAdapterWithUpdatedCallbackThatTakesComponentAsParameter() { + testFactoryConfigurationAdapter(AdapterWithUpdateMethodThatTakesComponentAsParameter.class, "updatedWithComponent"); + } + + public void testFactoryConfigurationAdapter(Class adapterImplClass, String adapterUpdate) { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + m_ensure = new Ensure(); + + // Create a Configuration instance, which will create/update/remove a configuration for factoryPid "MyFactoryPid" + ConfigurationCreator configurator = new ConfigurationCreator("MyFactoryPid", "key", "value1"); + Component s1 = component(m).impl(configurator).withSvc(ConfigurationAdmin.class, true).build(); + + // Create an Adapter that will be instantiated, once the configuration is created. + // This Adapter provides an AdapterService, and depends on an AdapterExtraDependency service. + Component s2 = factoryPidAdapter(m) + .factoryPid("MyFactoryPid").impl(adapterImplClass).update(adapterUpdate).propagate().provides(AdapterService.class, "foo", "bar") + .withSvc(AdapterExtraDependency.class, true) + .build(); + + // Create extra adapter service dependency upon which our adapter depends on. + Component s3 = component(m) + .impl(new AdapterExtraDependency()).provides(AdapterExtraDependency.class).build(); + + // Create an AdapterService Consumer + Component s4 = component(m) + .impl(AdapterServiceConsumer.class).withSvc(AdapterService.class, srv -> srv.add("bind").change("change").remove("remove")).build(); + + // Start services + m.add(s1); + m.add(s2); + m.add(s3); + m.add(s4); + + // Wait for step 8: the AdapterService consumer has been injected with the AdapterService, and has called the doService method. + m_ensure.waitForStep(8, 1000000); + + // Modify configuration. + configurator.update("key", "value2"); + + // Wait for step 13: the AdapterService has been updated, and the AdapterService consumer has seen the change + m_ensure.waitForStep(13, 10000); + + // Remove the configuration + m.remove(s1); // The stop method will remove the configuration + m_ensure.waitForStep(16, 10000); + m.clear(); + } + + public void testFactoryConfigurationAdapterWithMethodRef() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + m_ensure = new Ensure(); + + // Create a Configuration instance, which will create/update/remove a configuration for factoryPid "MyFactoryPid" + ConfigurationCreator configurator = new ConfigurationCreator("MyFactoryPid", "key", "value1"); + Component s1 = component(m).impl(configurator).withSvc(ConfigurationAdmin.class, true).build(); + + // Create an Adapter that will be instantiated, once the configuration is created. + // This Adapter provides an AdapterService, and depends on an AdapterExtraDependency service. + Component s2 = factoryPidAdapter(m) + .factoryPid("MyFactoryPid") + .impl(Adapter.class) + .update(Adapter::updated) + .propagate() + .provides(AdapterService.class, "foo", "bar") + .withSvc(AdapterExtraDependency.class, true) + .build(); + + // Create extra adapter service dependency upon which our adapter depends on. + Component s3 = component(m) + .impl(new AdapterExtraDependency()).provides(AdapterExtraDependency.class).build(); + + // Create an AdapterService Consumer + Component s4 = component(m) + .impl(AdapterServiceConsumer.class).withSvc(AdapterService.class, srv -> srv.add("bind").change("change").remove("remove")).build(); + + // Start services + m.add(s1); + m.add(s2); + m.add(s3); + m.add(s4); + + // Wait for step 8: the AdapterService consumer has been injected with the AdapterService, and has called the doService method. + m_ensure.waitForStep(8, 10000); + + // Modify configuration. + configurator.update("key", "value2"); + + // Wait for step 13: the AdapterService has been updated, and the AdapterService consumer has seen the change + m_ensure.waitForStep(13, 10000); + + // Remove the configuration + m.remove(s1); // The stop method will remove the configuration + m_ensure.waitForStep(16, 10000); + m.clear(); + } + + public static class ConfigurationCreator { + private volatile ConfigurationAdmin m_ca; + private String m_key; + private String m_value; + private org.osgi.service.cm.Configuration m_conf; + private String m_factoryPid; + + public ConfigurationCreator(String factoryPid, String key, String value) { + m_factoryPid = factoryPid; + m_key = key; + m_value = value; + } + + public void start() { + try { + m_ensure.step(1); + m_conf = m_ca.createFactoryConfiguration(m_factoryPid, null); + Hashtable props = new Hashtable(); + props.put(m_key, m_value); + m_conf.update(props); + } + catch (IOException e) { + Assert.fail("Could not create configuration: " + e.getMessage()); + } + } + + public void update(String key, String val) { + Hashtable props = new Hashtable(); + props.put(key, val); + try { + m_conf.update(props); + } + catch (IOException e) { + Assert.fail("Could not update configuration: " + e.getMessage()); + } + } + + public void stop() { + try + { + m_conf.delete(); + } + catch (IOException e) + { + Assert.fail("Could not remove configuration: " + e.toString()); + } + } + } + + public interface AdapterService { + public void doService(); + } + + public static class AdapterExtraDependency { + } + + public static class Adapter implements AdapterService { + volatile AdapterExtraDependency m_extraDependency; // extra dependency. + private int updateCount; + + void updated(Dictionary settings) { + updateCount ++; + if (updateCount == 1) { + m_ensure.step(2); + Assert.assertEquals(true, "value1".equals(settings.get("key"))); + m_ensure.step(3); + } else if (updateCount == 2) { + m_ensure.step(9); + Assert.assertEquals(true, "value2".equals(settings.get("key"))); + m_ensure.step(10); + } else { + Assert.fail("wrong call to updated method: count=" + updateCount); + } + } + + public void doService() { + m_ensure.step(8); + } + + public void start() { + m_ensure.step(4); + Assert.assertNotNull(m_extraDependency); + m_ensure.step(5); + } + + public void stop() { + m_ensure.step(16); + } + } + + public static class AdapterWithUpdateMethodThatTakesComponentAsParameter extends Adapter { + void updatedWithComponent(Component component, Dictionary settings) { + Assert.assertNotNull(component); + Assert.assertEquals(this, component.getInstance()); + super.updated(settings); + } + } + + public static class AdapterServiceConsumer { + private AdapterService m_adapterService; + private Map m_adapterServiceProperties; + + void bind(Map serviceProperties, AdapterService adapterService) { + m_ensure.step(6); + m_adapterService = adapterService; + m_adapterServiceProperties = serviceProperties; + } + + void change(Map serviceProperties, AdapterService adapterService) { + m_ensure.step(11); + Assert.assertEquals(true, "value2".equals(m_adapterServiceProperties.get("key"))); + m_ensure.step(12); + Assert.assertEquals(true, "bar".equals(m_adapterServiceProperties.get("foo"))); + m_ensure.step(13); + } + + public void start() { + m_ensure.step(7); + Assert.assertNotNull(m_adapterService); + Assert.assertEquals(true, "value1".equals(m_adapterServiceProperties.get("key"))); + Assert.assertEquals(true, "bar".equals(m_adapterServiceProperties.get("foo"))); + m_adapterService.doService(); + } + + public void stop() { + m_ensure.step(14); + } + + void remove(AdapterService adapterService) { + m_ensure.step(15); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/FactoryConfigurationCreator.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/FactoryConfigurationCreator.java new file mode 100644 index 00000000000..f47391c6ff0 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/FactoryConfigurationCreator.java @@ -0,0 +1,79 @@ +/* + * 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.dm.lambda.itest; + +import java.io.IOException; +import java.util.Hashtable; + +import org.junit.Assert; +import org.osgi.service.cm.ConfigurationAdmin; + +public class FactoryConfigurationCreator { + private volatile ConfigurationAdmin m_ca; + private volatile org.osgi.service.cm.Configuration m_conf; + private final String m_key; + private final String m_value; + private final String m_factoryPid; + private final Ensure m_e; + private final int m_firstStep; + + public FactoryConfigurationCreator(Ensure e, String factoryPid, int firstStep, String key, String value) { + m_factoryPid = factoryPid; + m_key = key; + m_value = value; + m_e = e; + m_firstStep = firstStep; + } + + public void start() { + try { + m_e.step(m_firstStep); + m_conf = m_ca.createFactoryConfiguration(m_factoryPid, null); + Hashtable props = new Hashtable<>(); + props.put(m_key, m_value); + m_conf.update(props); + } + catch (IOException e) { + Assert.fail("Could not create configuration: " + e.getMessage()); + } + } + + public void update(String key, String val) { + Hashtable props = new Hashtable<>(); + props.put(key, val); + try { + m_conf.update(props); + } + catch (IOException e) { + Assert.fail("Could not update configuration: " + e.getMessage()); + } + } + + public void stop() { + try + { + m_conf.delete(); + } + catch (IOException e) + { + Assert.fail("Could not remove configuration: " + e.toString()); + } + } +} + diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/FactoryInjectedWithConfigurationBeforeTheCreateMethod.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/FactoryInjectedWithConfigurationBeforeTheCreateMethod.java new file mode 100644 index 00000000000..f4deb80f01e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/FactoryInjectedWithConfigurationBeforeTheCreateMethod.java @@ -0,0 +1,132 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + + +/** + * Use case: one component is instantiated using another factory object, and the + * factory object needs the configuration before the factory.create method is called. + * + * @author Felix Project Team + */ +public class FactoryInjectedWithConfigurationBeforeTheCreateMethod extends TestBase { + Ensure m_e; + + public void testServiceInjection() { + DependencyManager m = getDM(); + m_e = new Ensure(); + + // Create the component that creates a configuration. + Component configurator = component(m).impl(new Configurator("foobar")).withSvc(ConfigurationAdmin.class, true).build(); + + // Create the object that has to be injected with the configuration before its create method is called. + MyFactory factory = new MyFactory(); + + // Create the Component for the MyComponent class that is created using the factory above. + Component myComponent = component(m).factory(factory, "create").withCnf(b->b.pid("foobar").update(factory, "updated")).build(); + + // provide the configuration + m.add(configurator); + + m.add(myComponent); + m_e.waitForStep(4, 10000); + m.remove(myComponent); + m.remove(configurator); + } + + public void testServiceInjectionRef() { + DependencyManager m = getDM(); + m_e = new Ensure(); + + // Create the component that creates a configuration. + Component configurator = component(m).impl(new Configurator("foobar")).withSvc(ConfigurationAdmin.class, true).build(); + + // Create the object that has to be injected with the configuration before its create method is called. + MyFactory factory = new MyFactory(); + + // Create the Component for the MyComponent class that is created using the factory above. + Component myComponent = component(m).factory(factory, "create").withCnf(b->b.pid("foobar").update(factory::updated)).build(); + + // provide the configuration + m.add(configurator); + + m.add(myComponent); + m_e.waitForStep(4, 10000); + m.remove(myComponent); + m.remove(configurator); + } + + class Configurator { + private volatile ConfigurationAdmin m_ca; + Configuration m_conf; + final String m_pid; + + public Configurator(String pid) { + m_pid = pid; + } + + public void init() { + try { + Assert.assertNotNull(m_ca); + m_e.step(1); + m_conf = m_ca.getConfiguration(m_pid, null); + Hashtable props = new Hashtable<>(); + props.put("testkey", "testvalue"); + m_conf.update(props); + } + catch (IOException e) { + Assert.fail("Could not create configuration: " + e.getMessage()); + } + } + + public void destroy() throws IOException { + m_conf.delete(); + } + } + + public class MyFactory { + public void updated(Dictionary conf) { + Assert.assertNotNull("configuration is null", conf); + m_e.step(2); + } + + public MyComponent create() { + m_e.step(3); + return new MyComponent(); + } + } + + public class MyComponent { + void start() { + m_e.step(4); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/Felix5244Test.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/Felix5244Test.java new file mode 100644 index 00000000000..0d0c93d0a84 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/Felix5244Test.java @@ -0,0 +1,54 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; + +/** + * @author Felix Project Team + */ +public class Felix5244Test extends TestBase { + private final Ensure m_ensure = new Ensure(); + + public void testAbstractBindMethod() throws Exception { + final DependencyManager dm = getDM(); + Component myService = component(dm).impl(new MyService()).withSvc(MyDependency.class, svc -> svc.required().add(AbstractService::bind)).build(); + Component myDependency = component(dm).impl(new MyDependency() {}).provides(MyDependency.class).build(); + dm.add(myService); + dm.add(myDependency); + m_ensure.waitForStep(1, 5000); + dm.clear(); + } + + interface MyDependency {} + + public abstract class AbstractService { + void bind(MyDependency myDependency) { + Assert.assertNotNull(myDependency); + m_ensure.step(1); + } + } + + public class MyService extends AbstractService { + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/InstanceBoundDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/InstanceBoundDependencyTest.java new file mode 100644 index 00000000000..c2d3cd9aee2 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/InstanceBoundDependencyTest.java @@ -0,0 +1,158 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + +/** + * This test does some injection tests on components being in INSTANTIATED_AND_WAITING_FOR_REQUIRED state. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class InstanceBoundDependencyTest extends TestBase { + Ensure m_e; + + public void testServiceInjection() { + DependencyManager m = getDM(); + m_e = new Ensure(); + + // Create a "C" component: it depends on some S1 services, and on some S2 instance-bound services (declared from C.init() method) + C cimpl = new C(); + Component c = component(m).impl(cimpl) + .withSvc(S1.class, sb->sb.add("addS1").change("changeS1").remove("removeS1").autoConfig(true)).build(); + m.add(c); + + // Add S1 (s1_1): C.add(S1 s1) is called, then init() is called where a dependency is declared on S2 + Hashtable s1_1_props = new Hashtable(); + s1_1_props.put("name", "s1_1"); + s1_1_props.put(Constants.SERVICE_RANKING, new Integer(10)); + S1Impl s1_1_impl = new S1Impl(); + Component s1_1 = component(m).impl(s1_1_impl).provides(S1.class.getName(), s1_1_props).build(); + m.add(s1_1); + m_e.waitForStep(1, 5000); // wait until C.init called + ServiceReference ref = cimpl.getS1("s1_1"); + Assert.assertNotNull(ref); + Assert.assertNotNull(cimpl.getS1()); + Assert.assertEquals(s1_1_impl, cimpl.getS1()); + + // At this point, MyComponent is in INSTANTIATED_AND_WAITING_FOR_REQUIRED state. + // add now add another higher ranked S1 (s1_2) instance. C.add(s1_2) method should be called (the S1 dependency + // is not instance bound), and m_s1 autoconfig field should be updated. + Hashtable s1_2_props = new Hashtable(); + s1_2_props.put(Constants.SERVICE_RANKING, new Integer(20)); + s1_2_props.put("name", "s1_2"); + S1Impl s1_2_impl = new S1Impl(); + Component s1_2 = component(m).impl(s1_2_impl).provides(S1.class.getName(), s1_2_props).build(); + m.add(s1_2); + ref = cimpl.getS1("s1_2"); + Assert.assertNotNull(ref); + Assert.assertNotNull(cimpl.getS1()); + Assert.assertEquals(s1_2_impl, cimpl.getS1()); // must return s1_2 with ranking = 20 + + // Now, change the s1_1 service properties: C.changed(s1_1) should be called, and C.m_s1AutoConfig should be updated + s1_1_props.put(Constants.SERVICE_RANKING, new Integer(30)); + s1_1.setServiceProperties(s1_1_props); + ref = cimpl.getS1("s1_1"); + Assert.assertNotNull(ref); + Assert.assertEquals(new Integer(30), ref.getProperty(Constants.SERVICE_RANKING)); + Assert.assertNotNull(cimpl.getS1()); + Assert.assertEquals(s1_1_impl, cimpl.getS1()); + + // Now, remove the s1_1: C.remove(s1_1) should be called, and C.m_s1AutoConfig should be updated + m.remove(s1_1); + ref = cimpl.getS1("s1_1"); + Assert.assertNull(cimpl.getS1("s1_1")); + Assert.assertNotNull(cimpl.getS1()); + Assert.assertEquals(s1_2_impl, cimpl.getS1()); + m.clear(); + } + + // C component depends on some S1 required services + public interface S1 { + } + + public class S1Impl implements S1 { + } + + public interface S2 { + } + + public class S2Impl implements S2 { + } + + // Our "C" component: it depends on S1 (required) and S2 (required/instance bound) + // Class tested with reflection based callbacks + class C { + final Map m_s1Map = new HashMap(); + final Map m_s2Map = new HashMap(); + volatile S1 m_s1; // auto configured + + S1 getS1() { + return m_s1; + } + + void addS1(ServiceReference s1) { + m_s1Map.put((String) s1.getProperty("name"), s1); + } + + void changeS1(ServiceReference s1) { + m_s1Map.put((String) s1.getProperty("name"), s1); + } + + void removeS1(ServiceReference s1) { + m_s1Map.remove((String) s1.getProperty("name")); + } + + void addS2(ServiceReference s2) { + m_s2Map.put((String) s2.getProperty("name"), s2); + } + + void changeS2(ServiceReference s2) { + m_s2Map.put((String) s2.getProperty("name"), s2); + } + + void removeS2(ServiceReference s2) { + m_s2Map.remove((String) s2.getProperty("name")); + } + + ServiceReference getS1(String name) { + return m_s1Map.get(name); + } + + ServiceReference getS2(String name) { + return m_s2Map.get(name); + } + + void init(Component c) { + component(c, comp->comp.withSvc(S2.class, srv -> srv.add("addS2").change("changeS2").remove("removeS2"))); + m_e.step(1); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ModifiedBundleDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ModifiedBundleDependencyTest.java new file mode 100644 index 00000000000..474a7de6215 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ModifiedBundleDependencyTest.java @@ -0,0 +1,168 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; + +/** + * Test for FELIX-4334 issue. + * + * Two components: A, B + * + * - A provided. + * - B has a bundle dependency on the dependency manager shell bundle, which is currently stopped. + * - B has an instance bound dependency on A. + * - Now unregister A. + * - As a result of that, B becomes unavailable and is unbound from A. But B is not destroyed, because A dependency + * is "instance bound". So B is still bound to the bundle dependency. + * - Now, someone starts the dependency manager shell bundle: B then shall be called in its "changed" callback. + * + * @author Felix Project Team + */ +public class ModifiedBundleDependencyTest extends TestBase { + public static interface A { + } + + static class AImpl implements A { + } + + public static interface B { + } + + static class BImpl implements B { + final Ensure m_e; + + BImpl(Ensure e) { + m_e = e; + } + + public void add(Bundle dmTest) { + m_e.step(1); + } + + void init(Component c) { + m_e.step(2); + component(c, comp -> comp.withSvc(A.class, srv -> srv.add("add").remove("remove"))); + } + + public void add(A a) { + m_e.step(3); + } + + public void start() { + m_e.step(4); + } + + public void stop() { + m_e.step(5); + } + + public void remove(A a) { + m_e.step(6); + } + + public void change(Bundle dmTest) { // called two times: one for STARTING, one for STARTED + m_e.step(); + } + + public void destroy() { + m_e.step(9); + } + + public void remove(Bundle dmTest) { + m_e.step(10); + } + } + + public void testAdapterWithChangedInstanceBoundDependencyAndCallbacks() { + DependencyManager m = getDM(); + Ensure e = new Ensure(); + + Component a = component(m).impl(new AImpl()).provides(A.class).build(); + + String filter = "(Bundle-SymbolicName=org.apache.felix.metatype)"; + int mask = Bundle.INSTALLED|Bundle.ACTIVE|Bundle.RESOLVED|Bundle.STARTING; + Component b = component(m) + .provides(B.class).impl(new BImpl(e)).withBundle(bd -> bd.filter(filter).mask(mask).add("add").change("change").remove("remove")).build(); + + Bundle dmtest = getBundle("org.apache.felix.metatype"); + try { + dmtest.stop(); + } catch (BundleException e1) { + Assert.fail("could not find metatype bundle"); + } + + m.add(a); + m.add(b); + + e.waitForStep(4, 5000); + m.remove(a); // B will loose A and will enter into "waiting for required (instantiated)" state. + System.out.println("Starting metatype bundle ..."); + try { + dmtest.start(); + } catch (BundleException e1) { + Assert.fail("could not start metatype bundle"); + } + e.waitForStep(7, 5000); + m.remove(b); + e.waitForStep(10, 5000); + } + + public void testAdapterWithChangedInstanceBoundDependencyRef() { + DependencyManager m = getDM(); + Ensure e = new Ensure(); + + Component a = + component(m, comp -> comp.impl(new AImpl()).provides(A.class).autoAdd(false)); + + BImpl impl = new BImpl(e); + String filter = "(Bundle-SymbolicName=org.apache.felix.metatype)"; + int mask = Bundle.INSTALLED|Bundle.ACTIVE|Bundle.RESOLVED|Bundle.STARTING; + Component b = component(m).provides(B.class).impl(impl) + .withBundle(bd -> bd.filter(filter).mask(mask).add(impl::add).change(impl::change).remove(impl::remove)).build(); + + Bundle dmtest = getBundle("org.apache.felix.metatype"); + try { + dmtest.stop(); + } catch (BundleException e1) { + Assert.fail("could not find metatype bundle"); + } + + m.add(a); + m.add(b); + + e.waitForStep(4, 5000); + m.remove(a); // B will loose A and will enter into "waiting for required (instantiated)" state. + System.out.println("Starting metatype bundle ..."); + try { + dmtest.start(); + } catch (BundleException e1) { + Assert.fail("could not start metatype bundle"); + } + e.waitForStep(7, 5000); + m.remove(b); + e.waitForStep(10, 5000); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/MultipleExtraDependenciesTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/MultipleExtraDependenciesTest.java new file mode 100644 index 00000000000..70a132c9105 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/MultipleExtraDependenciesTest.java @@ -0,0 +1,153 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; + +/** + * @author Felix Project Team + */ +public class MultipleExtraDependenciesTest extends TestBase { + /** + * Check that list of extra dependencies (defined from init method) are handled properly. + * The extra dependencies are added using a List object (Component.add(List)). + * A component c1 will define two extra dependencies over *available* c4/c5 services. + */ + public void testWithTwoAvailableExtraDependency() { + DependencyManager m = getDM(); + // Helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + Component c1 = component(m).provides(Service1.class).impl(new MyComponent1(e)).withSvc(Service2.class, srv->srv.autoConfig("m_service2")).build(); + Component c2 = component(m).impl(new MyComponent2(e)).withSvc(Service1.class, srv->srv.required(false).autoConfig(false).add("added")).build(); + Component c3 = component(m).provides(Service2.class).impl(Service2Impl.class).build(); + Component c4 = component(m).impl(Service3Impl1.class).provides(Service3.class, "type", "xx").build(); + Component c5 = component(m).impl(Service3Impl2.class).provides(Service3.class, "type", "yy").build(); + + System.out.println("\n+++ Adding c2 / MyComponent2"); + m.add(c2); + System.out.println("\n+++ Adding c3 / Service2"); + m.add(c3); + System.out.println("\n+++ Adding c4 / Service3(xx)"); + m.add(c4); + System.out.println("\n+++ Adding c5 / Service3(yy)"); + m.add(c5); + System.out.println("\n+++ Adding c1 / MyComponent1"); + // c1 have declared two extra dependency on Service3 (xx/yy). + // both extra dependencies are available, so the c1 component should be started immediately. + m.add(c1); + e.waitForStep(3, 3000); + m.clear(); + } + + /** + * Check that list of extra dependencies (defined from init method) are handled properly. + * The extra dependencies are added using a List object (Component.add(List)). + * A component c1 will define two extra dependencies over c4/c5. At the point c1.init() + * is adding the two extra dependencies from its init method, c4 is available, but not c5. + * So, c1 is not yet activated. + * Then c5 is added, and it triggers the c1 activation ... + */ + public void testWithOneAvailableExtraDependency() { + DependencyManager m = getDM(); + // Helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + Component c1 = component(m).provides(Service1.class).impl(new MyComponent1(e)).withSvc(Service2.class, srv->srv.autoConfig("m_service2")).build(); + Component c2 = component(m).impl(new MyComponent2(e)).withSvc(Service1.class, srv->srv.required(false).autoConfig(false).add("added")).build(); + Component c3 = component(m).provides(Service2.class).impl(Service2Impl.class).build(); + Component c4 = component(m).impl(Service3Impl1.class).provides(Service3.class, "type", "xx").build(); + Component c5 = component(m).impl(Service3Impl2.class).provides(Service3.class, "type", "yy").build(); + + System.out.println("\n+++ Adding c2 / MyComponent2"); + m.add(c2); + System.out.println("\n+++ Adding c3 / Service2"); + m.add(c3); + System.out.println("\n+++ Adding c4 / Service3(xx)"); + m.add(c4); + System.out.println("\n+++ Adding c1 / MyComponent1"); + m.add(c1); + + // c1 have declared two extra dependency on Service3 (xx/yy). + // So, because we have not yet added c5 (yy), c1 should not be started currently. + // But, now, we'll add c5 (Service3/yy) and c1 should then be started ... + System.out.println("\n+++ Adding c5 / Service3(yy)"); + m.add(c5); + e.waitForStep(3, 3000); + m.clear(); + } + + + public interface Service1 {} + public interface Service2 {} + public interface Service3 {} + + public static class Service2Impl implements Service2 {} + public static class Service3Impl1 implements Service3 {} + public static class Service3Impl2 implements Service3 {} + + public static class MyComponent1 implements Service1 { + Service2 m_service2; + Service3 m_service3_xx; + Service3 m_service3_yy; + Ensure m_ensure; + + public MyComponent1(Ensure e) { + m_ensure = e; + } + + void init(Component c) { + m_ensure.step(1); + // Service3/xx currently available + // Service3/yy not yet available + + component(c, comp -> comp + .withSvc(Service3.class, srv->srv.filter("(type=xx)").autoConfig("m_service3_xx")) + .withSvc(Service3.class, srv->srv.filter("(type=yy)").autoConfig("m_service3_yy"))); + } + + void start() { + System.out.println("MyComponent1.start"); + Assert.assertNotNull(m_service2); + Assert.assertNotNull(m_service3_xx); + Assert.assertNotNull(m_service3_yy); + m_ensure.step(2); + } + } + + public static class MyComponent2 { + Ensure m_ensure; + + public MyComponent2(Ensure e) { + m_ensure = e; + } + + void added(Service1 s1) { + System.out.println("MyComponent2.bind(" + s1 + ")"); + Assert.assertNotNull(s1); + m_ensure.step(3); + } + + void start() { + System.out.println("MyComponent2.start"); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/MultipleExtraDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/MultipleExtraDependencyTest.java new file mode 100644 index 00000000000..395945912fe --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/MultipleExtraDependencyTest.java @@ -0,0 +1,208 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; + +/** + * Test which validates multi-dependencies combination. + * + * @author Felix Project Team + */ +public class MultipleExtraDependencyTest extends TestBase { + public void testMultipleExtraDependencies() + { + DependencyManager m = getDM(); + Ensure e = new Ensure(); + + Component sp2 = component(m) + .impl(ServiceProvider2.class).provides(ServiceProvider2.class) + .withSvc(Runnable.class, srv->srv.filter("(foo=bar)").required(false).autoConfig("m_runnable")) + .withSvc(Sequencer.class, srv->srv.add("bind")) + .composition("getComposition") + .build(); + + Component sp = component(m) + .impl(ServiceProvider.class) + .provides(ServiceInterface.class, "foo", "bar") + .start("start").stop("stop") + .withSvc(Sequencer.class, srv->srv.autoConfig("m_sequencer")) + .withSvc(ServiceProvider2.class, srv->srv.add("bind").remove("unbind")) + .build(); + + Component sc = component(m) + .impl(ServiceConsumer.class) + .start("start").stop("stop") + .withSvc(Sequencer.class, srv->srv.autoConfig("m_sequencer")) + .withSvc(ServiceInterface.class, srv->srv.filter("(foo=bar)").autoConfig("m_service")) + .build(); + + Component sequencer = component(m) + .impl(new SequencerImpl(e)) + .provides(Sequencer.class.getName()) + .build(); + + m.add(sp2); + m.add(sp); + m.add(sc); + m.add(sequencer); + + // Check if ServiceProvider component have been initialized orderly + e.waitForStep(7, 5000); + + // Stop the test.annotation bundle + m.remove(sequencer); + m.remove(sp); + m.remove(sp2); + m.remove(sc); + + // And check if ServiceProvider2 has been deactivated orderly + e.waitForStep(11, 5000); + } + + public interface Sequencer + { + void step(); + void step(int step); + void waitForStep(int step, int timeout); + } + + public static class SequencerImpl implements Sequencer { + Ensure m_ensure; + + public SequencerImpl(Ensure e) + { + m_ensure = e; + } + + public void step() + { + m_ensure.step(); + } + + public void step(int step) + { + m_ensure.step(step); + } + + public void waitForStep(int step, int timeout) + { + m_ensure.waitForStep(step, timeout); + } + } + + public interface ServiceInterface + { + public void doService(); + } + + public static class ServiceConsumer + { + volatile Sequencer m_sequencer; + volatile ServiceInterface m_service; + + void start() + { + m_sequencer.step(6); + m_service.doService(); + } + + void stop() + { + m_sequencer.step(8); + } + } + + public static class ServiceProvider implements ServiceInterface + { + Sequencer m_sequencer; + ServiceProvider2 m_serviceProvider2; + + void bind(ServiceProvider2 provider2) + { + m_serviceProvider2 = provider2; + } + + void start() + { + m_serviceProvider2.step(4); + m_sequencer.step(5); + } + + void stop() + { + m_sequencer.step(9); + } + + void unbind(ServiceProvider2 provider2) + { + m_sequencer.step(10); + } + + public void doService() + { + m_sequencer.step(7); + } + } + + public static class ServiceProvider2 + { + Composite m_composite = new Composite(); + Sequencer m_sequencer; + Runnable m_runnable; + + void bind(Sequencer seq) + { + m_sequencer = seq; + m_sequencer.step(1); + } + + void start() + { + m_sequencer.step(3); + m_runnable.run(); // NullObject + } + + public void step(int step) // called by ServiceProvider.start() method + { + m_sequencer.step(step); + } + + void stop() + { + m_sequencer.step(11); + } + + Object[] getComposition() + { + return new Object[] { this, m_composite }; + } + } + + public static class Composite + { + void bind(Sequencer seq) + { + seq.step(2); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/MultipleExtraDependencyTest2.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/MultipleExtraDependencyTest2.java new file mode 100644 index 00000000000..2375cf48dec --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/MultipleExtraDependencyTest2.java @@ -0,0 +1,235 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ServiceDependency; + +/** + * Tests for extra dependencies which are declared from service's init method. + * + * @author Felix Project Team + */ +public class MultipleExtraDependencyTest2 extends TestBase { + public void testMultipleExtraDependencies() { + DependencyManager m = getDM(); + Ensure e = new Ensure(); + + Component sp2 = component(m) + .impl(ServiceProvider2.class) + .provides(ServiceProvider2.class) + .init("init").start("start").stop("stop") + .composition("getComposition").build(); + + Component sp = component(m) + .impl(ServiceProvider.class) + .provides(ServiceInterface.class, "foo", "bar") + .init("init").start("start").stop("stop") + .build(); + + Component sc = component(m) + .impl(ServiceConsumer.class) + .init("init").start("start").stop("stop") + .build(); + + // Provide the Sequencer service to the MultipleAnnotationsTest class. + Component sequencer = component(m) + .impl(new SequencerImpl(e)) + .provides(Sequencer.class) + .build(); + + m.add(sp2); + m.add(sp); + m.add(sc); + m.add(sequencer); + + // Check if the test.annotation components have been initialized orderly + e.waitForStep(7, 10000); + + // Stop the test.annotation bundle + m.remove(sequencer); + m.remove(sp); + m.remove(sp2); + m.remove(sc); + +// m.remove(sp2); +// m.remove(sc); +// m.remove(sp); +// m.remove(sequencer); + + + + // And check if the test.annotation bundle has been deactivated orderly + e.waitForStep(11, 10000); + m.clear(); + } + + public interface Sequencer + { + void step(); + void step(int step); + void waitForStep(int step, int timeout); + } + + public static class SequencerImpl implements Sequencer { + final Ensure m_ensure; + + public SequencerImpl(Ensure e) + { + m_ensure = e; + } + + public void step() + { + m_ensure.step(); + } + + public void step(int step) + { + m_ensure.step(step); + } + + public void waitForStep(int step, int timeout) + { + m_ensure.waitForStep(step, timeout); + } + } + + public interface ServiceInterface + { + public void doService(); + } + + public static class ServiceConsumer { + volatile Sequencer m_sequencer; + volatile ServiceInterface m_service; + volatile Dependency m_d1, m_d2; + + public void init(Component s) { + component(s, comp->comp + .withSvc(Sequencer.class, srv->srv.autoConfig("m_sequencer")) + .withSvc(ServiceInterface.class, srv->srv.filter("(foo=bar)").autoConfig("m_service"))); + } + + void start() { + m_sequencer.step(6); + m_service.doService(); + } + + void stop() { + m_sequencer.step(8); + } + } + + public static class ServiceProvider implements ServiceInterface + { + volatile Sequencer m_sequencer; + volatile ServiceProvider2 m_serviceProvider2; + volatile ServiceDependency m_d1, m_d2; + + public void init(Component c) + { + component(c, comp->comp + .withSvc(Sequencer.class, srv->srv.autoConfig("m_sequencer")) + .withSvc(ServiceProvider2.class, srv->srv.add("bind").remove("unbind"))); + } + + void bind(ServiceProvider2 provider2) + { + m_serviceProvider2 = provider2; + } + + void start() + { + m_serviceProvider2.step(4); + m_sequencer.step(5); + } + + void stop() + { + m_sequencer.step(9); + } + + void unbind(ServiceProvider2 provider2) + { + m_sequencer.step(10); + } + + public void doService() + { + m_sequencer.step(7); + } + } + + public static class ServiceProvider2 + { + final Composite m_composite = new Composite(); + volatile Sequencer m_sequencer; + volatile Runnable m_runnable; + volatile ServiceDependency m_d1, m_d2; + + public void init(Component c) + { + component(c, comp->comp + .withSvc(Runnable.class, srv->srv.optional().filter("(foo=bar)")) + .withSvc(Sequencer.class, srv->srv.add("bind"))); + } + + void bind(Sequencer seq) + { + System.out.println("ServiceProvider2.bind(" + seq + ")"); + m_sequencer = seq; + m_sequencer.step(1); + } + + void start() + { + System.out.println("ServiceProvider2.start: m_runnable=" + m_runnable + ", m_sequencer = " + m_sequencer); + m_sequencer.step(3); + m_runnable.run(); // NullObject + } + + public void step(int step) // called by ServiceProvider.start() method + { + m_sequencer.step(step); + } + + void stop() + { + m_sequencer.step(11); + } + + Object[] getComposition() + { + return new Object[] { this, m_composite }; + } + } + + public static class Composite + { + void bind(Sequencer seq) + { + seq.step(2); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/MultipleServiceDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/MultipleServiceDependencyTest.java new file mode 100644 index 00000000000..292e9a480c0 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/MultipleServiceDependencyTest.java @@ -0,0 +1,143 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.Constants; + +/** + * @author Felix Project Team + */ +public class MultipleServiceDependencyTest extends TestBase { + public void testMultipleServiceRegistrationAndConsumption() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component provider = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class.getName()).build(); + Component providerWithHighRank = component(m).impl(new ServiceProvider2(e)).provides(ServiceInterface.class.getName(), Constants.SERVICE_RANKING, Integer.valueOf(5)).build(); + Component consumer = component(m).impl(new ServiceConsumer(e)).withSvc(ServiceInterface.class, true).build(); + m.add(provider); + m.add(providerWithHighRank); + m.add(consumer); + e.waitForStep(3, 5000); + m.remove(providerWithHighRank); + e.step(4); + e.waitForStep(5, 5000); + m.remove(provider); + m.remove(consumer); + e.waitForStep(6, 5000); + } + + public void testReplacementAutoConfig() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component provider = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class.getName()).build(); + Component provider2 = component(m).impl(new ServiceProvider2(e)).provides(ServiceInterface.class.getName()).build(); + Component consumer = component(m).impl(new ServiceConsumer(e)).withSvc(ServiceInterface.class, true).build(); + m.add(provider2); + m.add(consumer); + e.waitForStep(3, 5000); + m.add(provider); + m.remove(provider2); + e.step(4); + e.waitForStep(5, 5000); + m.remove(provider); + m.remove(consumer); + e.waitForStep(6, 5000); + } + + public void testReplacementCallbacks() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component provider = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class.getName()).build(); + Component provider2 = component(m).impl(new ServiceProvider2(e)).provides(ServiceInterface.class.getName()).build(); + Component consumer = component(m).impl(new ServiceConsumer(e)).withSvc(ServiceInterface.class, srv->srv.add("add").remove("remove")).build(); + m.add(provider2); + m.add(consumer); + e.waitForStep(3, 15000); + m.add(provider); + m.remove(provider2); + e.step(4); + e.waitForStep(5, 15000); + m.remove(provider); + m.remove(consumer); + e.waitForStep(6, 15000); + } + + static interface ServiceInterface { + public void invoke(); + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke() { + m_ensure.step(5); + } + } + + static class ServiceProvider2 implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider2(Ensure e) { + m_ensure = e; + } + public void invoke() { + m_ensure.step(2); + } + } + + static class ServiceConsumer implements Runnable { + private volatile ServiceInterface m_service; + private final Ensure m_ensure; + + @SuppressWarnings("unused") + private void add(ServiceInterface service) { m_service = service; } + + @SuppressWarnings("unused") + private void remove(ServiceInterface service) { if (m_service == service) { m_service = null; }} + public ServiceConsumer(Ensure e) { m_ensure = e; } + + public void start() { + Thread t = new Thread(this); + t.start(); + } + + public void run() { + m_ensure.step(1); + m_service.invoke(); + m_ensure.step(3); + m_ensure.waitForStep(4, 15000); + m_service.invoke(); + } + + public void stop() { + m_ensure.step(6); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/OptionalConfigurationTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/OptionalConfigurationTest.java new file mode 100644 index 00000000000..c70469d0481 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/OptionalConfigurationTest.java @@ -0,0 +1,151 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import java.io.IOException; +import java.util.Hashtable; +import java.util.Properties; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * Integration test for Optional ConfigurationDependency. + * The following behaviors are tested: + * + * 1) an OptionalConfigurationConsumer component defines an optional configuration dependency. + * 2) then the component is activated: since there is no currently an available configuration, the component is called in its + * updated callback with a type-safe config, and the component can then be initialized using the default methods from the type-safe config interface. + * 3) then the configuration is registered: at this point the OptionalConfigurationConsumer is called again in its updated callback , but + * with the real just registered configuration. So, the component can be updated with the newly just registered config. + * 4) then the configuration is unregistered: at this point, the OptionalConfigurationConsumer is called again in updated callback, like in step 2). + * So, the component is re-initilized using the default methods from the type-safe config. + * + * @author Felix Project Team + */ +public class OptionalConfigurationTest extends TestBase { + final static String PID = "ConfigurationDependencyTest.pid"; + + public void testOptionalConfigurationConsumer() { + DependencyManager m = getDM(); + Ensure e = new Ensure(); + + ConfigurationCreator confCreate = new ConfigurationCreator(e, PID, 3); + Component c1 = component(m, comp -> comp.impl(confCreate).autoAdd(false).withSvc(ConfigurationAdmin.class, true)); + + Pojo pojo = new Pojo(e); + Component c2 = component(m, comp -> comp.impl(pojo).autoAdd(false).withCnf(cnf -> cnf.update(Config.class, Pojo::updated).pid(PID).optional())); + + m.add(c2); + e.waitForStep(1, 5000); // c2 called in updated with testKey="default" config + e.waitForStep(2, 5000); // c2 called in start() + m.add(c1); // c1 registers a "testKey=testvalue" configuration, using config admin. + e.waitForStep(3, 5000); // c1 created the conf. + e.waitForStep(4, 5000); // c2 called in updated with testKey="testvalue" + m.remove(c1); // remove configuration. + e.waitForStep(5, 5000); // c2 called in updated with default "testkey=default" property (using the default method from the config interface. + m.remove(c2); // stop the OptionalConfigurationConsumer component + e.waitForStep(6, 5000); // c2 stopped + } + + + public static interface Config { + public default String getTestkey() { return "default"; } + } + + static class Pojo { + protected final Ensure m_ensure; + protected volatile int m_updateCount; + + public Pojo(Ensure e) { + m_ensure = e; + } + + public void updated(Config cnf) { // optional configuration, called after start(), like any other optional dependency callbacks. + if (cnf != null) { + m_updateCount ++; + if (m_updateCount == 1) { + if (!"default".equals(cnf.getTestkey())) { + Assert.fail("Could not find the configured property."); + } + m_ensure.step(1); + } else if (m_updateCount == 2) { + if (!"testvalue".equals(cnf.getTestkey())) { + Assert.fail("Could not find the configured property."); + } + m_ensure.step(4); + } else if (m_updateCount == 3) { + if (!"default".equals(cnf.getTestkey())) { + Assert.fail("Could not find the configured property."); + } + m_ensure.step(5); + } + } else { + // configuration destroyed: should never happen + m_ensure.throwable(new Exception("lost configuration")); + } + } + + public void start() { + m_ensure.step(2); + } + + public void stop() { + m_ensure.step(6); + } + } + + public static class ConfigurationCreator { + private volatile ConfigurationAdmin m_ca; + private final Ensure m_ensure; + Configuration m_conf; + final String m_pid; + final int m_initStep; + + public ConfigurationCreator(Ensure e, String pid, int initStep) { + m_ensure = e; + m_pid = pid; + m_initStep = initStep; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void init() { + try { + Assert.assertNotNull(m_ca); + m_ensure.step(m_initStep); + m_conf = m_ca.getConfiguration(m_pid, null); + Hashtable props = new Properties(); + props.put("testkey", "testvalue"); + m_conf.update(props); + } + catch (IOException e) { + Assert.fail("Could not create configuration: " + e.getMessage()); + } + } + + public void destroy() throws IOException { + m_conf.delete(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/RemovedDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/RemovedDependencyTest.java new file mode 100644 index 00000000000..9c1a4b110af --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/RemovedDependencyTest.java @@ -0,0 +1,173 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.serviceDependency; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.Properties; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; + +/** + * One consumer, Three providers. The Consumer has two required dependency on provider1, provider2, and one + * instance-bound required dependency on provider3. + * When the three providers are there, the consumer is started. + * + * This test asserts the following correct behaviors: + * - when we remove the dependency on provider2, then the consumer is not stopped. + * - when we remove the (instance-bound) dependency on provider3, then the consumer os not stopped. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes", "unused"}) +public class RemovedDependencyTest extends TestBase { + public void testRemoveDependencyAndConsumerMustRemainStarted() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // Create two providers + Hashtable props = new Hashtable(); + props.put("name", "provider1"); + Component sp = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class, props).build(); + props = new Properties(); + props.put("name", "provider2"); + Component sp2 = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class.getName(), props).build(); + props = new Properties(); + props.put("name", "provider3"); + Component sp3 = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class.getName(), props).build(); + + // Create the consumer, and start it + Dependency d3 = m.createServiceDependency().setService(ServiceInterface.class, "(name=provider3)").setRequired(true).setCallbacks("add", "remove"); + + ServiceConsumer consumer = new ServiceConsumer(e, d3); + Component sc = component(m).impl(consumer).build(); + + Dependency d1 = serviceDependency(sc, ServiceInterface.class).filter("(name=provider1)").add("add").remove("remove").build(); + Dependency d2 = serviceDependency(sc, ServiceInterface.class).filter("(name=provider2)").add("add").remove("remove").build(); + + sc.add(d1, d2); + + // Add the first two providers and the consumer + m.add(sp); + m.add(sp2); + m.add(sp3); + m.add(sc); + + // Check if consumer has been bound to the three providers + e.waitForStep(3, 5000); + Assert.assertEquals(3, consumer.getProvidersCount()); + Assert.assertNotNull(consumer.getProvider("provider1")); + Assert.assertNotNull(consumer.getProvider("provider2")); + Assert.assertNotNull(consumer.getProvider("provider3")); + + // Now remove the provider2, and check if the consumer is still alive + sc.remove(d2); + Assert.assertFalse(consumer.isStopped()); + Assert.assertEquals(2, consumer.getProvidersCount()); + Assert.assertNotNull(consumer.getProvider("provider1")); + Assert.assertNull(consumer.getProvider("provider2")); + Assert.assertNotNull(consumer.getProvider("provider3")); + + // Now remove the provider3 (the consumer has an instance bound dependency on it), and check if the consumer is still alive + sc.remove(d3); + Assert.assertFalse(consumer.isStopped()); + Assert.assertEquals(1, consumer.getProvidersCount()); + Assert.assertNotNull(consumer.getProvider("provider1")); + Assert.assertNull(consumer.getProvider("provider2")); + Assert.assertNull(consumer.getProvider("provider3")); + + m.clear(); + } + + static interface ServiceInterface { + public void invoke(); + } + + class ServiceProvider implements ServiceInterface { + final Ensure m_ensure; + + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke() { + m_ensure.step(); + } + } + + class ServiceConsumer { + private final Ensure m_ensure; + private final List m_providers = new ArrayList<>(); + private BundleContext m_bc; + private boolean m_stopped; + private final Dependency m_dependency3; + + public ServiceConsumer(Ensure e, Dependency dependency3) { + m_ensure = e; + m_dependency3 = dependency3; + } + + public void add(ServiceReference ref) { + debug("ServiceConsumer.add(%s)", ref); + m_providers.add(ref); + ServiceInterface s = (ServiceInterface) m_bc.getService(ref); + s.invoke(); + } + + public void remove(ServiceReference ref) { + debug("ServiceConsumer.remove(%s)", ref); + m_providers.remove(ref); + debug("ServiceConsumer: current providers list=%s", m_providers); + } + + public void init(Component c) { + c.add(m_dependency3); + } + + public int getProvidersCount() { + return m_providers.size(); + } + + public ServiceInterface getProvider(String name) { + for (ServiceReference ref : m_providers) { + Object n = ref.getProperty("name"); + if (n.equals(name)) { + return (ServiceInterface) m_bc.getService(ref); + } + } + return null; + } + + public void stop() { + m_stopped = true; + } + + public boolean isStopped() { + return m_stopped; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ResourceProvider.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ResourceProvider.java new file mode 100644 index 00000000000..54b4800cc23 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ResourceProvider.java @@ -0,0 +1,122 @@ +/* + * 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.dm.lambda.itest; + +import java.net.URL; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.dm.ResourceHandler; +import org.apache.felix.dm.ResourceUtil; +import org.junit.Assert; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; + +/** + * @author Felix Project Team + */ +class ResourceProvider { + final URL[] m_resources; + final BundleContext m_context; + final Map m_handlers = new HashMap<>(); + + ResourceProvider(BundleContext ctx, URL ... resources) { + m_context = ctx; + m_resources = resources; + } + + public void change() { + for (int i = 0; i < m_resources.length; i++) { + change(i); + } + } + + @SuppressWarnings({ "deprecation", "unchecked" }) + public void change(int resourceIndex) { + Map handlers = new HashMap<>(); + synchronized (m_handlers) { + handlers.putAll(m_handlers); + } + for (Map.Entry e : handlers.entrySet()) { + ResourceHandler handler = e.getKey(); + Filter filter = e.getValue(); + if (filter == null || filter.match((Dictionary)ResourceUtil.createProperties(m_resources[resourceIndex]))) { + handler.changed(m_resources[resourceIndex]); + } + } + } + + @SuppressWarnings({ "deprecation", "unchecked" }) + public void add(ResourceHandler handler, ServiceReference ref) { + String filterString = (String) ref.getProperty("filter"); + Filter filter = null; + if (filterString != null) { + try { + filter = m_context.createFilter(filterString); + } + catch (InvalidSyntaxException e) { + Assert.fail("Could not create filter for resource handler: " + e); + return; + } + } + for (int i = 0; i < m_resources.length; i++) { + if (filter == null || filter.match((Dictionary) ResourceUtil.createProperties(m_resources[i]))) { + synchronized (m_handlers) { + m_handlers.put(handler, filter); + } + handler.added(m_resources[i]); + } + } + } + + public void remove(ResourceHandler handler, ServiceReference ref) { + Filter filter; + synchronized (m_handlers) { + filter = (Filter) m_handlers.remove(handler); + } + if (filter != null) { + removeResources(handler, filter); + } + } + + @SuppressWarnings({ "deprecation", "unchecked" }) + private void removeResources(ResourceHandler handler, Filter filter) { + for (int i = 0; i < m_resources.length; i++) { + if (filter == null || filter.match((Dictionary)ResourceUtil.createProperties(m_resources[i]))) { + handler.removed(m_resources[i]); + } + } + } + + public void destroy() { + Map handlers = new HashMap<>(); + synchronized (m_handlers) { + handlers.putAll(m_handlers); + } + + for (Map.Entry e : handlers.entrySet()) { + ResourceHandler handler = e.getKey(); + Filter filter = e.getValue(); + removeResources(handler, filter); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ScopedServiceReferenceAndServiceObjectsTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ScopedServiceReferenceAndServiceObjectsTest.java new file mode 100644 index 00000000000..09670a13e79 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ScopedServiceReferenceAndServiceObjectsTest.java @@ -0,0 +1,110 @@ +/* + * 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.dm.lambda.itest; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Component.ServiceScope; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceObjects; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +/** + * Validates a simple scoped service, and some service consumers using ServiceReference and ServiceObjects API. + */ +public class ScopedServiceReferenceAndServiceObjectsTest extends TestBase { + final static Ensure m_e = new Ensure(); + + public void testScopedComponent() { + DependencyManager m = getDM(); + + Component provider = component(m, c -> c + .scope(ServiceScope.PROTOTYPE) + .impl(ServiceImpl.class) + .provides(Service.class.getName()) + .autoAdd(false)); + + Consumer1 c1 = new Consumer1(); + Component c1Comp = component(m, c -> c + .impl(c1) + .autoAdd(false) + .withSvc(Service.class, svc -> svc.required().add(Consumer1::set))); + + Consumer2 c2 = new Consumer2(); + Component c2Comp = component(m, c -> c + .impl(c2) + .autoAdd(false) + .withSvc(Service.class, svc -> svc.required().add(Consumer2::set))); + + m.add(provider); + m.add(c1Comp); + m_e.waitForStep(2, 5000); + + m.add(c2Comp); + m_e.waitForStep(4, 5000); + + Assert.assertNotNull(c1.getService()); + Assert.assertNotNull(c2.getService()); + Assert.assertNotEquals(c1.getService(), c2.getService()); + + m.clear(); + } + + public interface Service { + } + + public static class ServiceImpl implements Service { + volatile Bundle m_bundle; // bundle requesting the service + volatile ServiceRegistration m_registration; // registration of the requested service + + void start() { + m_e.step(); // 1, 3 + } + } + + public static class Consumer1 { + volatile Service m_service; + volatile BundleContext m_bc; + + void set(ServiceReference ref) { + ServiceObjects so = m_bc.getServiceObjects(ref); + m_service = so.getService(); + m_e.step(2); + } + + Service getService() { return m_service; } + } + + public static class Consumer2 { + volatile Service m_service; + volatile BundleContext m_bc; + + void set(ServiceObjects so) { + m_service = so.getService(); + m_e.step(4); + } + + Service getService() { return m_service; } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ScopedServiceTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ScopedServiceTest.java new file mode 100644 index 00000000000..f6ddf11463d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ScopedServiceTest.java @@ -0,0 +1,181 @@ +/* + * 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.dm.lambda.itest; + +import java.util.Map; +import java.util.Properties; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Component.ServiceScope; +import org.apache.felix.dm.ComponentState; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceRegistration; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +/** + * Validates a simple scoped service, which does not add some dynamic dependencies with a component init method. + */ +public class ScopedServiceTest extends TestBase implements ComponentStateListener { + final Ensure m_e = new Ensure(); + final Ensure.Steps m_listenerSteps = new Ensure.Steps(1, 4, 11); + final Ensure.Steps m_serviceImplStartSteps = new Ensure.Steps(2, 5, 12); + final Ensure.Steps m_serviceImplStopSteps = new Ensure.Steps(8, 10, 16); + final Ensure.Steps m_serviceConsumerBindSteps = new Ensure.Steps(3, 6, 13); + final Ensure.Steps m_serviceConsumerUnbindSteps = new Ensure.Steps(7, 9, 15); + + public void testPrototypeComponent() { + DependencyManager m = getDM(); + + Component provider = component(m, c -> c.scope(ServiceScope.PROTOTYPE) + .factory(this, "createServiceImpl") + .provides(Service.class) + .listener(this) + .autoAdd(false) + .withSvc(Service2.class, svc -> svc.required().add(ServiceImpl::bind))); + + Component service2 = component(m, c -> c + .provides(Service2.class) + .impl(new Service2() {}) + .autoAdd(false)); + + Component consumer1 = component(m, c -> c + .factory(this::createServiceConsumer) + .withSvc(Service.class, svc -> svc.required().add(ServiceConsumer::bind).change(ServiceConsumer::change).remove(ServiceConsumer::unbind))); + + Component consumer2 = component(m, c -> c + .factory(this::createServiceConsumer) + .withSvc(Service.class, svc -> svc.required().add(ServiceConsumer::bind).remove(ServiceConsumer::unbind))); + + m.add(provider); // add provider + m.add(consumer1); // add first consumer + m.add(service2); // add service2 (the provider depends on it) + m_e.waitForStep(1, 5000); // our listener has seen the first clone starting + m_e.waitForStep(2, 5000); // first clone started + m_e.waitForStep(3, 5000); // first consumer bound to the first clone + + m.add(consumer2); // add second consumer + m_e.waitForStep(4, 5000); // our listener has seen the second clone starting + m_e.waitForStep(5, 5000); // second clone started. + m_e.waitForStep(6, 5000); // second consumer bound to the second clone + + // make sure both consumers have a different provider instances. + ServiceConsumer consumer1Impl = (ServiceConsumer) consumer1.getInstance(); + Assert.assertNotNull(consumer1Impl.getService()); + ServiceConsumer consumer2Impl = (ServiceConsumer) consumer2.getInstance(); + Assert.assertNotNull(consumer2Impl.getService()); + Assert.assertNotEquals(consumer1Impl.getService(), consumer2Impl.getService()); + + m.remove(consumer1); // remove consumer1 + m_e.waitForStep(7, 5000); // consumer1 unbound from first clone + m_e.waitForStep(8, 5000); // first clone stopped + + m.remove(provider); // unregister the provider + m_e.waitForStep(9, 5000); // consumer2 unbound from second clone + m_e.waitForStep(10, 5000); // second clone stopped + m.remove(consumer2); + + m.add(provider); // re-register the provider + m.add(consumer1); // re-add the consumer1 + m_e.waitForStep(11, 5000); // our listener has seen the third clone starting + m_e.waitForStep(12, 5000); // third clone started + m_e.waitForStep(13, 5000); // consumer1 bound to the first clone + + Properties props = new Properties(); + props.put("foo", "bar"); + provider.setServiceProperties(props); // update provider service properties + m_e.waitForStep(14, 5000); // consumer1 should be called in its change callback + + m.remove(service2); // remove the service2 (it will remove the provider + m_e.waitForStep(15, 5000); // consumer1 stopped + m_e.waitForStep(16, 5000); // third clone stopped + + m.clear(); + } + + @SuppressWarnings("unused") + private ServiceImpl createServiceImpl() { + return new ServiceImpl(); + } + + @SuppressWarnings("unused") + private ServiceConsumer createServiceConsumer() { + return new ServiceConsumer(); + } + + @Override + public void changed(Component c, ComponentState state) { + if (state == ComponentState.STARTING) { + Assert.assertEquals(ServiceImpl.class, c.getInstance().getClass()); + m_e.steps(m_listenerSteps); // will enter in step 1, 4, 11 + } + } + + public interface Service { + } + + public interface Service2 { + } + + public class ServiceImpl implements Service { + volatile Bundle m_bundle; // bundle requesting the service + volatile ServiceRegistration m_registration; // registration of the requested service + volatile Service2 m_service2; + + void bind(Service2 service2) { + m_service2 = service2; + } + + void start() { + Assert.assertNotNull(m_bundle); + Assert.assertNotNull(m_registration); + Assert.assertNotNull(m_service2); + m_e.steps(m_serviceImplStartSteps); // 2, 5, 12 + } + + void stop() { + m_e.steps(m_serviceImplStopSteps); // 8, 10, 16 + } + } + + public class ServiceConsumer { + volatile Service m_myService; + + public void bind(Service service) { + m_myService = service; + m_e.steps(m_serviceConsumerBindSteps); // 3, 6, 13 + } + + public void change(Service service, Map properties) { + Assert.assertEquals("bar", properties.get("foo")); + m_e.step(14); + } + + public void unbind(Service service) { + Assert.assertEquals(m_myService, service); + m_e.steps(m_serviceConsumerUnbindSteps); // 7, 9, 15 + } + + public Service getService() { + return m_myService; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceDependencyCallbackSignaturesTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceDependencyCallbackSignaturesTest.java new file mode 100644 index 00000000000..76e3f66ce7d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceDependencyCallbackSignaturesTest.java @@ -0,0 +1,374 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.ServiceReference; + +/** + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class ServiceDependencyCallbackSignaturesTest extends TestBase { + volatile Ensure m_ensure; + + /** + * Tests if all possible dependency callbacks signatures supported by ServiceDependency. + */ + public void testDependencyCallbackSignatures() { + DependencyManager m = getDM(); + m_ensure = new Ensure(); + Hashtable props = new Hashtable<>(); + props.put("foo", "bar"); + Component provider = component(m) + .impl(new ProviderImpl()).provides(Provider.class.getName(), props).build(); + + component(m, c->c.impl(new Consumer1()).withSvc(Provider.class, srv->srv.add("bind").change("change").remove("remove"))); + component(m, c->c.impl(new Consumer2()).withSvc(Provider.class, srv->srv.add("bind").change("change").remove("remove"))); + component(m, c->c.impl(new Consumer3()).withSvc(Provider.class, srv->srv.add("bind").change("change").remove("remove"))); + component(m, c->c.impl(new Consumer4()).withSvc(Provider.class, srv->srv.add("bind").change("change").remove("remove"))); + component(m, c->c.impl(new Consumer5()).withSvc(Provider.class, srv->srv.add("bind").change("change").remove("remove"))); + component(m, c->c.impl(new Consumer6()).withSvc(Provider.class, srv->srv.add("bind").change("change").remove("remove"))); + component(m, c->c.impl(new Consumer7()).withSvc(Provider.class, srv->srv.add("bind").change("change").remove("remove"))); + component(m, c->c.impl(new Consumer8()).withSvc(Provider.class, srv->srv.add("bind").change("change").remove("remove"))); + component(m, c->c.impl(new Consumer9()).withSvc(Provider.class, srv->srv.add("bind").change("change").remove("remove"))); + + m.add(provider); + m_ensure.waitForStep(9, 5000); + + props = new Hashtable<>(); + props.put("foo", "zoo"); + provider.setServiceProperties(props); + m_ensure.waitForStep(18, 5000); + + m.remove(provider); + m_ensure.waitForStep(26, 5000); + } + + /** + * Tests if all possible dependency callbacks signatures supported by ServiceDependency. + */ + public void testDependencyCallbackSignaturesRef() { + DependencyManager m = getDM(); + m_ensure = new Ensure(); + Hashtable props = new Hashtable<>(); + props.put("foo", "bar"); + Component provider = component(m) + .impl(new ProviderImpl()).provides(Provider.class.getName(), props).build(); + + component(m, c->c.impl(new Consumer1()).withSvc(Provider.class, srv->srv.add(Consumer1::bind).change(Consumer1::change).remove(Consumer1::remove))); + component(m, c->c.impl(new Consumer2()).withSvc(Provider.class, srv->srv.add(Consumer2::bind).change(Consumer2::change).remove(Consumer2::remove))); + component(m, c->c.impl(new Consumer3()).withSvc(Provider.class, srv->srv.add(Consumer3::bind).change(Consumer3::change).remove(Consumer3::remove))); + component(m, c->c.impl(new Consumer4()).withSvc(Provider.class, srv->srv.add(Consumer4::bindRef).change(Consumer4::changeRef).remove(Consumer4::removeRef))); + component(m, c->c.impl(new Consumer5()).withSvc(Provider.class, srv->srv.add(Consumer5::bindRef).change(Consumer5::changeRef).remove(Consumer5::removeRef))); + component(m, c->c.impl(new Consumer6()).withSvc(Provider.class, srv->srv.add(Consumer6::bindRef).change(Consumer6::changeRef).remove(Consumer6::removeRef))); + component(m, c->c.impl(new Consumer7()).withSvc(Provider.class, srv->srv.add(Consumer7::bindRef).change(Consumer7::changeRef).remove(Consumer7::removeRef))); + component(m, c->c.impl(new Consumer8()).withSvc(Provider.class, srv->srv.add(Consumer8::bindRef).change(Consumer8::changeRef).remove(Consumer8::removeRef))); + component(m, c->c.impl(new Consumer9()).withSvc(Provider.class, srv->srv.add(Consumer9::bindRef).change(Consumer9::changeRef).remove(Consumer9::removeRef))); + + m.add(provider); + m_ensure.waitForStep(9, 5000); + + props = new Hashtable<>(); + props.put("foo", "zoo"); + provider.setServiceProperties(props); + m_ensure.waitForStep(18, 5000); + + m.remove(provider); + m_ensure.waitForStep(26, 5000); + } + + public static interface Provider { + } + + public static class ProviderImpl implements Provider { + } + + class Consumer1 { + void bind(Provider provider) { + Assert.assertNotNull(provider); + m_ensure.step(); + } + void change(Provider provider) { + Assert.assertNotNull(provider); + m_ensure.step(); + } + + void remove(Provider provider) { + Assert.assertNotNull(provider); + m_ensure.step(); + } + } + + class Consumer2 { + void bind(Provider provider, Map props) { + Assert.assertNotNull(provider); + Assert.assertEquals("bar", props.get("foo")); + m_ensure.step(); + } + void change(Provider provider, Map props) { + Assert.assertNotNull(provider); + Assert.assertEquals("zoo", props.get("foo")); + m_ensure.step(); + } + void remove(Provider provider, Map props) { + Assert.assertNotNull(provider); + Assert.assertEquals("zoo", props.get("foo")); + m_ensure.step(); + } + } + + class Consumer3 { + void bind(Provider provider, Dictionary props) { + Assert.assertNotNull(provider); + Assert.assertEquals("bar", props.get("foo")); + m_ensure.step(); + } + void change(Provider provider, Dictionary props) { + Assert.assertNotNull(provider); + Assert.assertEquals("zoo", props.get("foo")); + m_ensure.step(); + } + void remove(Provider provider, Dictionary props) { + Assert.assertNotNull(provider); + Assert.assertEquals("zoo", props.get("foo")); + m_ensure.step(); + } + } + + class Consumer4 { + void bindRef(Provider provider, ServiceReference ref) { // method ref callback + bind(ref, provider); + } + + void bind(ServiceReference ref, Provider provider) { // reflection based callback + Assert.assertNotNull(ref); + Assert.assertNotNull(provider); + Assert.assertEquals("bar", ref.getProperty("foo")); + m_ensure.step(); + } + + void changeRef(Provider provider, ServiceReference ref) { // method ref callback + change(ref, provider); + } + + void change(ServiceReference ref, Provider provider) { // reflection based callback + Assert.assertNotNull(ref); + Assert.assertNotNull(provider); + Assert.assertEquals("zoo", ref.getProperty("foo")); + m_ensure.step(); + } + + void removeRef(Provider provider, ServiceReference ref) { // method ref callback + remove(ref, provider); + } + + void remove(ServiceReference ref, Provider provider) { // reflection based callback + Assert.assertNotNull(ref); + Assert.assertNotNull(provider); + Assert.assertEquals("zoo", ref.getProperty("foo")); + m_ensure.step(); + } + } + + class Consumer5 { + void bindRef(Provider provider, ServiceReference ref) { // method ref callback + bind(ref); + } + + void bind(ServiceReference ref) { // reflection based callback + Assert.assertNotNull(ref); + Assert.assertEquals("bar", ref.getProperty("foo")); + m_ensure.step(); + } + + void changeRef(Provider provider, ServiceReference ref) { // method ref callback + change(ref); + } + + void change(ServiceReference ref) { // reflection based callback + Assert.assertNotNull(ref); + Assert.assertEquals("zoo", ref.getProperty("foo")); + m_ensure.step(); + } + + void removeRef(Provider provider, ServiceReference ref) { // method ref callback + remove(ref); + } + + void remove(ServiceReference ref) { // reflection based callback + Assert.assertNotNull(ref); + Assert.assertEquals("zoo", ref.getProperty("foo")); + m_ensure.step(); + } + } + + class Consumer6 { + + void bindRef(Provider p, Component c) { // method ref callback + bind(c); + } + + void bind(Component c) { // reflection based callback + Assert.assertNotNull(c); + m_ensure.step(); + } + + void changeRef(Provider p, Component c) { // method ref callback + change(c); + } + + void change(Component c) { // reflection based callback + Assert.assertNotNull(c); + m_ensure.step(); + } + + void removeRef(Provider p, Component c) { // method ref callback + remove(c); + } + + void remove(Component c) { // reflection based callback + Assert.assertNotNull(c); + m_ensure.step(); + } + } + + class Consumer7 { + void bindRef(Provider p, Component c, ServiceReference ref) { // reflection callback + bind(c, ref); + } + + void bind(Component c, ServiceReference ref) { // reflection callback + Assert.assertNotNull(c); + Assert.assertNotNull(ref); + Assert.assertEquals("bar", ref.getProperty("foo")); + Assert.assertNotNull(context.getService(ref)); + Assert.assertEquals(context.getService(ref).getClass(), ProviderImpl.class); + m_ensure.step(); + } + + void changeRef(Provider p, Component c, ServiceReference ref) { // reflection callback + change(c, ref); + } + + void change(Component c, ServiceReference ref) { // reflection callback + Assert.assertNotNull(c); + Assert.assertNotNull(ref); + Assert.assertEquals("zoo", ref.getProperty("foo")); + Assert.assertNotNull(context.getService(ref)); + Assert.assertEquals(context.getService(ref).getClass(), ProviderImpl.class); + m_ensure.step(); + } + + void removeRef(Provider p, Component c, ServiceReference ref) { // reflection callback + remove(c, ref); + } + + void remove(Component c, ServiceReference ref) { // reflection callback + Assert.assertNotNull(c); + Assert.assertNotNull(ref); + Assert.assertEquals("zoo", ref.getProperty("foo")); + Assert.assertNotNull(context.getService(ref)); + Assert.assertEquals(context.getService(ref).getClass(), ProviderImpl.class); + m_ensure.step(); + } + } + + class Consumer8 { + void bindRef(Provider p, Component c) { // method ref callback + bind(c, p); + } + + void bind(Component c, Provider provider) { // reflection callback + Assert.assertNotNull(c); + Assert.assertNotNull(provider); + m_ensure.step(); + } + + void changeRef(Provider p, Component c) { // method ref callback + change(c, p); + } + + void change(Component c, Provider provider) { // reflection callback + Assert.assertNotNull(c); + Assert.assertNotNull(provider); + m_ensure.step(); + } + + void removeRef(Provider p, Component c) { // method ref callback + remove(c, p); + } + + void remove(Component c, Provider provider) { // reflection callback + Assert.assertNotNull(c); + Assert.assertNotNull(provider); + m_ensure.step(); + } + } + + class Consumer9 { + void bindRef(Provider provider, Component c, ServiceReference ref) { // method ref callback + bind(c, ref, provider); + } + + void bind(Component c, ServiceReference ref, Provider provider) { // reflection callback + Assert.assertNotNull(c); + Assert.assertNotNull(ref); + Assert.assertNotNull(provider); + Assert.assertEquals("bar", ref.getProperty("foo")); + Assert.assertEquals(context.getService(ref), provider); + m_ensure.step(); + } + + void changeRef(Provider provider, Component c, ServiceReference ref) { // method ref callback + change(c, ref, provider); + } + + void change(Component c, ServiceReference ref, Provider provider) { // reflection callback + Assert.assertNotNull(c); + Assert.assertNotNull(ref); + Assert.assertNotNull(provider); + Assert.assertEquals("zoo", ref.getProperty("foo")); + Assert.assertEquals(context.getService(ref), provider); + m_ensure.step(); + } + + void removeRef(Provider provider, Component c, ServiceReference ref) { // method ref callback + remove(c, ref, provider); + } + + void remove(Component c, ServiceReference ref, Provider provider) { // reflection callback + Assert.assertNotNull(c); + Assert.assertNotNull(ref); + Assert.assertNotNull(provider); + Assert.assertEquals("zoo", ref.getProperty("foo")); + Assert.assertEquals(context.getService(ref), provider); + m_ensure.step(); + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceDependencyInjectionTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceDependencyInjectionTest.java new file mode 100644 index 00000000000..e88360be3aa --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceDependencyInjectionTest.java @@ -0,0 +1,157 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; + +/** + * @author Felix Project Team + */ +public class ServiceDependencyInjectionTest extends TestBase { + public void testServiceInjection() { + DependencyManager m = getDM(); + Ensure e = new Ensure(); + // create a service provider and consumer + ServiceProvider provider = new ServiceProvider(e); + Component sp = component(m).impl(provider).provides(ServiceInterface2.class.getName()).build(); + Component sc = component(m).impl(new ServiceConsumer()).withSvc(ServiceInterface2.class, true).build(); + + Component sc2 = component(m) // all dependencies are optional + .impl(new ServiceConsumerNamedInjection(false, false)) + .withSvc(ServiceInterface2.class, s->s.optional().autoConfig("m_service")) + .withSvc(ServiceInterface2.class, s->s.optional().autoConfig("m_service2")) + .withSvc(ServiceInterface2.class, s->s.optional().autoConfig("m_service3")) + .build(); + + Component sc3 = component(m) // second dependency is required, first and third are optional + .impl(new ServiceConsumerNamedInjection(false, false)) + .withSvc(ServiceInterface2.class, s->s.optional().autoConfig("m_service")) + .withSvc(ServiceInterface2.class, s->s.required().autoConfig("m_service2")) + .withSvc(ServiceInterface2.class, s->s.optional().autoConfig("m_service3")) + .build(); + + Component sc4 = component(m) + .impl(new ServiceConsumerNamedInjection(true, false)).build(); + Component sc5 = component(m) + .impl(new ServiceConsumerNamedInjection(true, true)).build(); + m.add(sp); + m.add(sc); + m.remove(sc); + m.add(sc2); + m.remove(sc2); + m.add(sc3); + m.remove(sc4); + m.add(sc4); + m.remove(sc4); + m.add(sc5); + m.remove(sc5); + m.remove(sp); + e.waitForStep(11, 5000); + m.clear(); + } + + static interface ServiceInterface { + public void invoke(); + } + + static interface ServiceInterface2 extends ServiceInterface { + public void invoke2(); + } + + static class ServiceProvider implements ServiceInterface2 { + private final Ensure m_ensure; + private Ensure.Steps m_invokeSteps = new Ensure.Steps(4, 5, 7, 8, 10, 11, 13, 14); + private Ensure.Steps m_invoke2Steps = new Ensure.Steps(1, 2, 3, 6, 9, 12); + + public ServiceProvider(Ensure e) { + m_ensure = e; + } + + public void invoke() { + System.out.println("invoke"); + m_ensure.steps(m_invokeSteps); + } + + public void invoke2() { + System.out.println("invoke2"); + m_ensure.steps(m_invoke2Steps); + } + } + + static class ServiceConsumer { + private volatile ServiceInterface2 m_service; + private volatile ServiceInterface2 m_service2; + + public void init() { + // invoke the second method of the interface via both injected members, to ensure + // neither of them is a null object (or null) + m_service.invoke2(); + m_service2.invoke2(); + Assert.assertEquals("Both members should have been injected with the same service.", m_service, m_service2); + } + } + + class ServiceConsumerNamedInjection { + private volatile ServiceInterface2 m_service; + private volatile ServiceInterface m_service2; + private volatile Object m_service3; + private final boolean m_secondDependencyRequired; + private final boolean m_instanceBound; + + ServiceConsumerNamedInjection(boolean instanceBound, boolean withSecondRequired) { + m_secondDependencyRequired = withSecondRequired; + m_instanceBound = instanceBound; + } + + public void init(Component c) { + if (m_instanceBound) { + DependencyManager m = c.getDependencyManager(); + c.add(m.createServiceDependency().setService(ServiceInterface2.class).setRequired(false).setAutoConfig("m_service"), + m.createServiceDependency().setService(ServiceInterface2.class).setRequired(m_secondDependencyRequired).setAutoConfig("m_service2"), + m.createServiceDependency().setService(ServiceInterface2.class).setRequired(false).setAutoConfig("m_service3")); + } else { + check(); + } + } + + public void start() { + if (m_instanceBound) { + check(); + } + } + + public void check() { + warn("ServiceConsumerNamedInjectionInstanceBound: m_service=%s, m_service2=%s, m_service3=%s", m_service, m_service2, m_service3); + // invoke the second method + m_service.invoke2(); + // invoke the first method (twice) + m_service2.invoke(); + ((ServiceInterface) m_service3).invoke(); + Assert.assertNotNull("Should have been injected", m_service); + Assert.assertNotNull("Should have been injected", m_service2); + Assert.assertNotNull("Should have been injected", m_service3); + Assert.assertEquals("Members should have been injected with the same service.", m_service, m_service2); + Assert.assertEquals("Members should have been injected with the same service.", m_service, m_service3); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceDependencyPropagateTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceDependencyPropagateTest.java new file mode 100644 index 00000000000..b71c8ecb52f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceDependencyPropagateTest.java @@ -0,0 +1,160 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.ServiceReference; + +/** + * Validates ServiceDependency service properties propagation. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes", "serial"}) +public class ServiceDependencyPropagateTest extends TestBase { + /** + * Checks that a ServiceDependency propagates the dependency service properties to the provided service properties. + */ + public void testServiceDependencyPropagate() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + + Component c1 = component(m) + .impl(new C1(e)) + .withSvc(C2.class, s->s.add("bind")).build(); + + Component c2 = component(m) + .provides(C2.class.getName(), new Hashtable() {{ put("foo", "bar"); }}) + .impl(new C2()) + .withSvc(C3.class, s->s.propagate()).build(); + + Component c3 = component(m) + .provides(C3.class.getName(), new Hashtable() {{ put("foo2", "bar2"); put("foo", "overriden");}}) + .impl(new C3()).build(); + + m.add(c1); + m.add(c2); + m.add(c3); + + e.waitForStep(3, 10000); + + m.remove(c3); + m.remove(c2); + m.remove(c1); + m.clear(); + } + + /** + * Checks that a ServiceDependency propagates the dependency service properties to the provided service properties, + * using a callback method. + */ + public void testServiceDependencyPropagateCallback() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + Component c1 = component(m) + .impl(new C1(e)) + .withSvc(C2.class, s->s.add("bind")).build(); + + C2 c2Impl = new C2(); + Component c2 = component(m) + .provides(C2.class.getName(), new Hashtable() {{ put("foo", "bar"); }}) + .impl(c2Impl) + .withSvc(C3.class, s->s.propagate(c2Impl, "getServiceProperties")).build(); + + Component c3 = component(m) + .provides(C3.class.getName()) + .impl(new C3()).build(); + + m.add(c1); + m.add(c2); + m.add(c3); + + e.waitForStep(3, 10000); + m.clear(); + } + + public void testServiceDependencyPropagateCallbackRef() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + Component c1 = component(m) + .impl(new C1(e)) + .withSvc(C2.class, s->s.add(C1::bind)).build(); + + C2 c2Impl = new C2(); + Component c2 = component(m) + .provides(C2.class.getName(), new Hashtable() {{ put("foo", "bar"); }}) + .impl(c2Impl) + .withSvc(C3.class, s->s.propagate(c2Impl::getServiceProperties)).build(); + + Component c3 = component(m) + .provides(C3.class.getName()) + .impl(new C3()).build(); + + m.add(c1); + m.add(c2); + m.add(c3); + + e.waitForStep(3, 10000); + m.clear(); + } + + public static class C1 { + private Map m_props; + private Ensure m_ensure; + + C1(Ensure ensure) { + m_ensure = ensure; + } + + void bind(C2 c2, Map props) { + m_props = props; + } + + void start() { + m_ensure.step(1); + if ("bar".equals(m_props.get("foo"))) { // "foo=overriden" from C2 should not override our own "foo" property + m_ensure.step(2); + } + if ("bar2".equals(m_props.get("foo2"))) { + m_ensure.step(3); + } + } + } + + public static class C2 { + C3 m_c3; + + public Dictionary getServiceProperties(ServiceReference ref) { + return new Hashtable() {{ put("foo2", "bar2"); put("foo", "overriden"); }}; + } + } + + public static class C3 { + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceDependencyTest.java new file mode 100644 index 00000000000..bc7b13226c8 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceDependencyTest.java @@ -0,0 +1,98 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; + +/** + * @author Felix Project Team + */ +public class ServiceDependencyTest extends TestBase { + public void testServiceRegistrationAndConsumption() { + DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + Component sp = component(m).impl(new ServiceProvider(e)).provides(ServiceInterface.class).build(); + Component sc = component(m).impl(new ServiceConsumer(e)).withSvc(ServiceInterface.class, true).build(); + + Component sc2 = component(m).impl(new ServiceConsumerCallbacks(e)) + .withSvc(ServiceInterface.class, srv -> srv.required(false).add(ServiceConsumerCallbacks::add).remove(ServiceConsumerCallbacks::remove)) + .build(); + + m.add(sp); + m.add(sc); + m.remove(sc); + m.add(sc2); + m.remove(sp); + m.remove(sc2); + // ensure we executed all steps inside the component instance + e.step(6); + } + + static interface ServiceInterface { + public void invoke(); + } + + static class ServiceProvider implements ServiceInterface { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke() { + m_ensure.step(2); + } + } + + static class ServiceConsumer { + private volatile ServiceInterface m_service; + private final Ensure m_ensure; + + public ServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void init() { + m_ensure.step(1); + m_service.invoke(); + } + + public void destroy() { + m_ensure.step(3); + } + } + + static class ServiceConsumerCallbacks { + private final Ensure m_ensure; + + public ServiceConsumerCallbacks(Ensure e) { + m_ensure = e; + } + + public void add(ServiceInterface service) { + m_ensure.step(4); + } + public void remove(ServiceInterface service) { + m_ensure.step(5); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceDependencyThroughCallbackInstanceTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceDependencyThroughCallbackInstanceTest.java new file mode 100644 index 00000000000..27b028e5a4e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceDependencyThroughCallbackInstanceTest.java @@ -0,0 +1,82 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.BundleContext; + +/** + * @author Felix Project Team + */ +public class ServiceDependencyThroughCallbackInstanceTest extends TestBase { + public void testServiceWithCallbacksAndOneDependency() { + invokeTest(context, 1); + } + + public void testServiceWithCallbacksAndThreeDependencies() { + invokeTest(context, 3); + } + + private void invokeTest(BundleContext context, int numberOfServices) { + DependencyManager m = getDM(); + // create a number of services + for (int i = 0; i < numberOfServices; i++) { + final int num = i; + component(m, comp -> comp + .provides(Service.class).impl(new Service() { + public String toString() { + return "A" + num; + }})); + } + + // create a service with dependency which will be invoked on a callback instance + CallbackInstance instance = new CallbackInstance(); + component(m, comp -> comp + .impl(new SimpleService() {}) + .withSvc(Service.class, srv -> srv.add(instance::added).remove(instance::removed))); + + Assert.assertEquals(numberOfServices, instance.getCount()); + m.clear(); + } + + public static interface Service { + } + + public static interface SimpleService { + } + + public static class CallbackInstance { + int m_count = 0; + + void added(Service service) { + System.out.println("added " + service); + m_count++; + } + + void removed(Service service) { + } + + int getCount() { + return m_count; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceUpdateTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceUpdateTest.java new file mode 100644 index 00000000000..4e2ac909c61 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceUpdateTest.java @@ -0,0 +1,148 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import java.net.URL; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Properties; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ResourceHandler; + +/** + * @author Felix Project Team + */ +public class ServiceUpdateTest extends TestBase { + public void testServiceUpdate() throws Exception { + final DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a resource provider + ResourceProvider provider = new ResourceProvider(context, new URL("file://localhost/path/to/file1.txt")); + // activate it + component(m, comp -> comp + .impl(new ServiceProvider(e)) + .withSvc(ServiceInterface.class, srv -> srv + .add(ServiceProvider::add).change(ServiceProvider::change).remove(ServiceProvider::remove))); + component(m, comp -> comp + .impl(provider) + .withSvc(ResourceHandler.class, srv -> srv.add(ResourceProvider::add).remove(ResourceProvider::remove))); + + // TODO implement resource adapters in new builder API and use it for the following resource adapter. + + // create a resource adapter for our single resource + // note that we can provide an actual implementation instance here because there will be only one + // adapter, normally you'd want to specify a Class here + CallbackInstance callbackInstance = new CallbackInstance(e); + Hashtable serviceProps = new Hashtable(); + serviceProps.put("number", "1"); + Component component = m.createResourceAdapterService("(&(path=/path/to/*.txt)(host=localhost))", false, callbackInstance, "changed") + .setImplementation(new ResourceAdapter(e)) + .setInterface(ServiceInterface.class.getName(), serviceProps) + .setCallbacks(callbackInstance, "init", "start", "stop", "destroy"); + m.add(component); + // wait until the single resource is available + e.waitForStep(1, 5000); + // wait until the component gets the dependency injected + e.waitForStep(2, 5000); + // trigger a 'change' in our resource + provider.change(); + // wait until the changed callback is invoked + e.waitForStep(3, 5000); + // wait until the changed event arrived at the component + e.waitForStep(4, 5000); + System.out.println("Done!"); + } + + static class ResourceAdapter implements ServiceInterface { + protected URL m_resource; // injected by reflection. + + ResourceAdapter(Ensure e) { + } + + public void invoke() { + // TODO Auto-generated method stub + + } + } + + static interface ServiceInterface { + public void invoke(); + } + + static class ServiceProvider { + private final Ensure m_ensure; + public ServiceProvider(Ensure e) { + m_ensure = e; + } + void add(ServiceInterface i) { + m_ensure.step(2); + } + void change(ServiceInterface i) { + System.out.println("Change..."); + m_ensure.step(4); + } + void remove(ServiceInterface i) { + System.out.println("Remove..."); + } + } + + static class CallbackInstance { + private final Ensure m_ensure; + public CallbackInstance(Ensure e) { + m_ensure = e; + } + + void init() { + System.out.println("init"); + } + void start() { + System.out.println("start"); + m_ensure.step(1); + } + void stop() { + System.out.println("stop"); + } + void destroy() { + System.out.println("destroy"); + } + @SuppressWarnings("unchecked") + void changed(Component component) { + System.out.println("resource changed"); + m_ensure.step(3); + + Properties newProps = new Properties(); + // update the component's service properties + Dictionary dict = component.getServiceProperties(); + Enumeration e = dict.keys(); + while (e.hasMoreElements()) { + String key = e.nextElement(); + String value = dict.get(key); + newProps.setProperty(key, value); + } + newProps.setProperty("new-property", "2"); + component.getServiceRegistration().setProperties(newProps); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceUtil.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceUtil.java new file mode 100644 index 00000000000..38628852f2f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/ServiceUtil.java @@ -0,0 +1,176 @@ +/* + * 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.dm.lambda.itest; + +import java.util.List; + +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + +/** + * OSGi service utilities (copied from dependency manager implementation). + * + * @author Felix Project Team + */ +public class ServiceUtil { + /** + * Returns the service ranking of a service, based on its service reference. If + * the service has a property specifying its ranking, that will be returned. If + * not, the default ranking of zero will be returned. + * + * @param ref the service reference to determine the ranking for + * @return the ranking + */ + public static int getRanking(ServiceReference ref) { + return getRankingAsInteger(ref).intValue(); + } + + /** + * Returns the service ranking of a service, based on its service reference. If + * the service has a property specifying its ranking, that will be returned. If + * not, the default ranking of zero will be returned. + * + * @param ref the service reference to determine the ranking for + * @return the ranking + */ + public static Integer getRankingAsInteger(ServiceReference ref) { + Integer rank = (Integer) ref.getProperty(Constants.SERVICE_RANKING); + if (rank != null) { + return rank; + } + return new Integer(0); + } + + /** + * Returns the service ID of a service, based on its service reference. This + * method is aware of service aspects as defined by the dependency manager and + * will return the ID of the orginal service if you give it an aspect. + * + * @param ref the service reference to determine the service ID of + * @return the service ID + */ + public static long getServiceId(ServiceReference ref) { + return getServiceIdAsLong(ref).longValue(); + } + + /** + * Returns the service ID of a service, based on its service reference. This + * method is aware of service aspects as defined by the dependency manager and + * will return the ID of the orginal service if you give it an aspect. + * + * @param ref the service reference to determine the service ID of + * @return the service ID + */ + public static Long getServiceIdAsLong(ServiceReference ref) { + return getServiceIdObject(ref); + } + + public static Long getServiceIdObject(ServiceReference ref) { + Long aid = (Long) ref.getProperty(DependencyManager.ASPECT); + if (aid != null) { + return aid; + } + Long sid = (Long) ref.getProperty(Constants.SERVICE_ID); + if (sid != null) { + return sid; + } + throw new IllegalArgumentException("Invalid service reference, no service ID found"); + } + + /** + * Determines if the service is an aspect as defined by the dependency manager. + * Aspects are defined by a property and this method will check for its presence. + * + * @param ref the service reference + * @return true if it's an aspect, false otherwise + */ + public static boolean isAspect(ServiceReference ref) { + Long aid = (Long) ref.getProperty(DependencyManager.ASPECT); + return (aid != null); + } + + /** + * Converts a service reference to a string, listing both the bundle it was + * registered from and all properties. + * + * @param ref the service reference + * @return a string representation of the service + */ + public static String toString(ServiceReference ref) { + if (ref == null) { + return "ServiceReference[null]"; + } + else { + StringBuffer buf = new StringBuffer(); + Bundle bundle = ref.getBundle(); + if (bundle != null) { + buf.append("ServiceReference["); + buf.append(bundle.getBundleId()); + buf.append("]{"); + } + else { + buf.append("ServiceReference[unregistered]{"); + } + buf.append(propertiesToString(ref, null)); + buf.append("}"); + return buf.toString(); + } + } + + /** + * Converts the properties of a service reference to a string. + * + * @param ref the service reference + * @param exclude a list of properties to exclude, or null to show everything + * @return a string representation of the service properties + */ + public static String propertiesToString(ServiceReference ref, List exclude) { + StringBuffer buf = new StringBuffer(); + String[] keys = ref.getPropertyKeys(); + for (int i = 0; i < keys.length; i++) { + if (i > 0) { + buf.append(','); + } + buf.append(keys[i]); + buf.append('='); + Object val = ref.getProperty(keys[i]); + if (exclude == null || !exclude.contains(val)) { + if (val instanceof String[]) { + String[] valArray = (String[]) val; + StringBuffer valBuf = new StringBuffer(); + valBuf.append('{'); + for (int j = 0; j < valArray.length; j++) { + if (valBuf.length() > 1) { + valBuf.append(','); + } + valBuf.append(valArray[j].toString()); + } + valBuf.append('}'); + buf.append(valBuf); + } + else { + buf.append(val.toString()); + } + } + } + return buf.toString(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/StopCallbackTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/StopCallbackTest.java new file mode 100644 index 00000000000..ad53911169e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/StopCallbackTest.java @@ -0,0 +1,54 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.lambda.itest.lifecycle.SimpleComponent; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; + +/** + * Checks if a component is stopped when its bundle is stopped. + * see FELIX-5768 + */ +public class StopCallbackTest extends TestBase { + private final Ensure m_ensure = new Ensure(); + + public void testStopCalledWhenBundleIsStopped() throws Exception { + final DependencyManager dm = getDM(); + + // Register the Ensure object in the osgi registry (the SimpleComponent depends on it). + component(dm, comp -> comp.impl(m_ensure).provides(Ensure.class).properties("name", SimpleComponent.class.getName())); + + // Make sure the SimpleComponent has been started + m_ensure.waitForStep(1, 5000); + + // Now, stop the bundle which contains the SimpleComponent + BundleContext bc = FrameworkUtil.getBundle(SimpleComponent.class).getBundleContext(); + bc.getBundle().stop(); + + // Make sure the SimpleComponent has been stopped + m_ensure.waitForStep(2, 5000); + + // Remove our Ensure component + dm.clear(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/TemporalServiceDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/TemporalServiceDependencyTest.java new file mode 100644 index 00000000000..f9f8ac1628c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/TemporalServiceDependencyTest.java @@ -0,0 +1,268 @@ +/* + * 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.dm.lambda.itest; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.adapter; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; +import static org.apache.felix.dm.lambda.DependencyManagerActivator.serviceDependency; + +import java.util.Hashtable; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ServiceDependency; +import org.junit.Assert; + +/** + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes", "serial"}) +public class TemporalServiceDependencyTest extends TestBase { + public void testServiceConsumptionAndIntermittentAvailability() { + final DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + TemporalServiceProvider provider = new TemporalServiceProvider(e); + Component sp = component(m).impl(provider).provides(TemporalServiceInterface.class.getName()).build(); + TemporalServiceProvider2 provider2 = new TemporalServiceProvider2(e); + Component sp2 = component(m).impl(provider2).provides(TemporalServiceInterface.class.getName()).build(); + TemporalServiceConsumer consumer = new TemporalServiceConsumer(e); + Component sc = component(m).impl(consumer).withSvc(TemporalServiceInterface.class, s->s.timeout(10000)).build(); + // add the service consumer + m.add(sc); + // now add the first provider + m.add(sp); + e.waitForStep(2, 5000); + // and remove it again (this should not affect the consumer yet) + m.remove(sp); + // now add the second provider + m.add(sp2); + e.step(3); + e.waitForStep(4, 5000); + // and remove it again + m.remove(sp2); + // finally remove the consumer + m.remove(sc); + // ensure we executed all steps inside the component instance + e.step(6); + m.clear(); + } + + public void testServiceConsumptionWithCallbackAndIntermittentAvailability() { + final DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + TemporalServiceProvider provider = new TemporalServiceProvider(e); + Component sp = component(m).impl(provider).provides(TemporalServiceInterface.class.getName()).build(); + TemporalServiceProvider2 provider2 = new TemporalServiceProvider2(e); + Component sp2 = component(m).impl(provider2).provides(TemporalServiceInterface.class.getName()).build(); + TemporalServiceConsumerWithCallback consumer = new TemporalServiceConsumerWithCallback(e); + Component sc = component(m).impl(consumer).withSvc(TemporalServiceInterface.class, srv->srv.add("add").remove("remove").timeout(10000)).build(); + + // add the service consumer + m.add(sc); + // now add the first provider + m.add(sp); + e.waitForStep(2, 5000); + // and remove it again (this should not affect the consumer yet) + m.remove(sp); + // now add the second provider + m.add(sp2); + e.step(3); + e.waitForStep(4, 5000); + // and remove it again + m.remove(sp2); + // finally remove the consumer + m.remove(sc); + // Wait for the consumer.remove callback + e.waitForStep(6, 5000); + // ensure we executed all steps inside the component instance + e.step(7); + m.clear(); + } + + // Same test as testServiceConsumptionWithCallbackAndIntermittentAvailability, but the consumer is now + // an adapter for the Adaptee interface. + public void testFELIX4858_ServiceAdapterConsumptionWithCallbackAndIntermittentAvailability() { + final DependencyManager m = getDM(); + // helper class that ensures certain steps get executed in sequence + Ensure e = new Ensure(); + // create a service provider and consumer + TemporalServiceProvider provider = new TemporalServiceProvider(e); + Component sp = component(m).impl(provider).provides(TemporalServiceInterface.class.getName()).build(); + TemporalServiceProvider2 provider2 = new TemporalServiceProvider2(e); + Component sp2 = component(m).impl(provider2).provides(TemporalServiceInterface.class.getName()).build(); + TemporalServiceConsumerAdapterWithCallback consumer = new TemporalServiceConsumerAdapterWithCallback(e); + Component sc = adapter(m, Adaptee.class).impl(consumer).build(); + ServiceDependency temporalDep = serviceDependency(sc, TemporalServiceInterface.class).timeout(10000).add("add").remove("remove").build(); + sc.add(temporalDep); + Component adaptee = component(m).impl(new Adaptee()).provides(Adaptee.class.getName()).build(); + + // add the adapter service consumer + m.add(sc); + // add the adaptee (the adapter service depends on it) + m.add(adaptee); + // now add the first provider + m.add(sp); + e.waitForStep(2, 5000); + // and remove it again (this should not affect the consumer yet) + m.remove(sp); + // now add the second provider + m.add(sp2); + e.step(3); + e.waitForStep(4, 5000); + // and remove it again + m.remove(sp2); + // finally remove the consumer + m.remove(sc); + // Wait for the consumer.remove callback + e.waitForStep(6, 5000); + // ensure we executed all steps inside the component instance + e.step(7); + m.clear(); + } + + public void testFelix4602_PropagateServiceInvocationException() { + final DependencyManager m = getDM(); + final Ensure ensure = new Ensure(); + Runnable provider = new Runnable() { + public void run() { + throw new UncheckedException(); + } + }; + Hashtable props = new Hashtable(); + props.put("target", getClass().getSimpleName()); + Component providerComp = component(m) + .provides(Runnable.class.getName(), props) + .impl(provider).build(); + + Object consumer = new Object() { + volatile Runnable m_provider; + @SuppressWarnings("unused") + void start() { + try { + ensure.step(1); + m_provider.run(); + } catch (UncheckedException e) { + ensure.step(2); + } + } + }; + Component consumerComp = component(m) + .impl(consumer) + .withSvc(Runnable.class, s->s.timeout(5000).filter("(target=" + getClass().getSimpleName() + ")")).build(); + m.add(consumerComp); + m.add(providerComp); + ensure.waitForStep(2, 5000); + m.clear(); + } + + static class UncheckedException extends RuntimeException { + } + + static interface TemporalServiceInterface { + public void invoke(); + } + + static class TemporalServiceProvider implements TemporalServiceInterface { + private final Ensure m_ensure; + public TemporalServiceProvider(Ensure e) { + m_ensure = e; + } + public void invoke() { + m_ensure.step(2); + } + } + + static class TemporalServiceProvider2 implements TemporalServiceInterface { + protected final Ensure m_ensure; + public TemporalServiceProvider2(Ensure e) { + m_ensure = e; + } + public void invoke() { + m_ensure.step(4); + } + } + + static class TemporalServiceConsumer implements Runnable { + protected volatile TemporalServiceInterface m_service; + protected final Ensure m_ensure; + + public TemporalServiceConsumer(Ensure e) { + m_ensure = e; + } + + public void init() { + m_ensure.step(1); + Thread t = new Thread(this); + t.start(); + } + + public void run() { + m_service.invoke(); + m_ensure.waitForStep(3, 15000); + m_service.invoke(); + } + + public void destroy() { + m_ensure.step(5); + } + } + + static class TemporalServiceConsumerWithCallback extends TemporalServiceConsumer { + public TemporalServiceConsumerWithCallback(Ensure e) { + super(e); + } + + public void add(TemporalServiceInterface service) { + m_service = service; + } + + public void remove(TemporalServiceInterface service) { + Assert.assertTrue(m_service == service); + m_ensure.step(6); + } + } + + public static class Adaptee { + } + + static class TemporalServiceConsumerAdapterWithCallback extends TemporalServiceConsumer { + volatile Adaptee m_adaptee; + + public TemporalServiceConsumerAdapterWithCallback(Ensure e) { + super(e); + } + + public void start() { + Assert.assertTrue(m_adaptee != null); + } + + public void add(TemporalServiceInterface service) { + m_service = service; + } + + public void remove(TemporalServiceInterface service) { + Assert.assertTrue(m_service == service); + m_ensure.step(6); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/TestBase.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/TestBase.java new file mode 100644 index 00000000000..efc034e0340 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/TestBase.java @@ -0,0 +1,360 @@ +/* + * 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.dm.lambda.itest; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Hashtable; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentExecutorFactory; +import org.apache.felix.dm.DependencyManager; +import org.junit.Assert; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.log.LogService; + +import junit.framework.TestCase; + +/** + * Base class for all integration tests. + * + * @author Felix Project Team + */ +public abstract class TestBase extends TestCase implements LogService, FrameworkListener { + // Default OSGI log service level. + protected final static int LOG_LEVEL = LogService.LOG_WARNING; + + // optional thread pool used by parallel dependency managers + private volatile ExecutorService m_threadPool; + + // flag used to check if the threadpool must be used for a given test. + protected volatile boolean m_parallel; + + // Flag used to check if some errors have been logged during the execution of a given test. + private volatile boolean m_errorsLogged; + + // We implement OSGI log service. + protected ServiceRegistration logService; + + // Our bundle context + protected BundleContext context; + + // Our dependency manager used to create test components. + protected volatile DependencyManager m_dm; + + // The Registration for the DM threadpool. + private ServiceRegistration m_componentExecutorFactoryReg; + + public TestBase() { + } + + protected void setParallel() { + m_parallel = true; + } + + public void setUp() throws Exception { + warn("Setting up test " + getClass().getName()); + context = FrameworkUtil.getBundle(this.getClass()).getBundleContext(); + Hashtable props = new Hashtable<>(); + props.put(Constants.SERVICE_RANKING, new Integer(Integer.MAX_VALUE)); + logService = context.registerService(LogService.class, this, props); + context.addFrameworkListener(this); + m_dm = new DependencyManager(context); + if (m_parallel) { + warn("Using threadpool ..."); + m_threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + m_componentExecutorFactoryReg = context.registerService(ComponentExecutorFactory.class.getName(), + new ComponentExecutorFactory() { + @Override + public Executor getExecutorFor(Component component) { + return m_threadPool; + } + }, + null); + } + } + + public void tearDown() throws Exception { + warn("Tearing down test " + getClass().getName()); + logService.unregister(); + context.removeFrameworkListener(this); + clearComponents(); + if (m_parallel && m_componentExecutorFactoryReg != null) { + m_componentExecutorFactoryReg.unregister(); + m_threadPool.shutdown(); + try { + m_threadPool.awaitTermination(60, TimeUnit.SECONDS); + } catch (InterruptedException e) { + } + } + Assert.assertFalse(errorsLogged()); + } + + protected boolean isJava8() { + return System.getProperty("java.version", "1.8").startsWith("1.8"); + } + + protected DependencyManager getDM() { + return m_dm; + } + + protected void clearComponents() { + m_dm.clear(); + warn("All component cleared."); + } + + /** + * Creates and provides an Ensure object with a name service property into the OSGi service registry. + */ + protected ServiceRegistration register(Ensure e, String name) { + Hashtable props = new Hashtable<>(); + props.put("name", name); + return context.registerService(Ensure.class.getName(), e, props); + } + + /** + * Helper method used to stop a given bundle. + * + * @param symbolicName + * the symbolic name of the bundle to be stopped. + */ + protected void stopBundle(String symbolicName) { + // Stop the test.annotation bundle + boolean found = false; + for (Bundle b : context.getBundles()) { + if (b.getSymbolicName().equals(symbolicName)) { + try { + found = true; + b.stop(); + } catch (BundleException e) { + e.printStackTrace(); + } + } + } + if (!found) { + throw new IllegalStateException("bundle " + symbolicName + " not found"); + } + } + + /** + * Helper method used to start a given bundle. + * + * @param symbolicName + * the symbolic name of the bundle to be started. + */ + protected void startBundle(String symbolicName) { + // Stop the test.annotation bundle + boolean found = false; + for (Bundle b : context.getBundles()) { + if (b.getSymbolicName().equals(symbolicName)) { + try { + found = true; + b.start(); + } catch (BundleException e) { + e.printStackTrace(); + } + } + } + if (!found) { + throw new IllegalStateException("bundle " + symbolicName + " not found"); + } + } + + /** + * Helper method used to get a given bundle. + * + * @param symbolicName + * the symbolic name of the bundle to get. + */ + protected Bundle getBundle(String symbolicName) { + for (Bundle b : context.getBundles()) { + if (b.getSymbolicName().equals(symbolicName)) { + return b; + } + } + throw new IllegalStateException("bundle " + symbolicName + " not found"); + } + + /** + * Suspend the current thread for a while. + * + * @param n + * the number of milliseconds to wait for. + */ + protected void sleep(int ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException e) { + } + } + + public void log(int level, String message) { + checkError(level, null); + if (LOG_LEVEL >= level) { + System.out.println(getLevel(level) + " - " + Thread.currentThread().getName() + " : " + message); + } + } + + public void log(int level, String message, Throwable exception) { + checkError(level, exception); + if (LOG_LEVEL >= level) { + StringBuilder sb = new StringBuilder(); + sb.append(getLevel(level) + " - " + Thread.currentThread().getName() + " : "); + sb.append(message); + parse(sb, exception); + System.out.println(sb.toString()); + } + } + + @SuppressWarnings("rawtypes") + public void log(ServiceReference sr, int level, String message) { + checkError(level, null); + if (LOG_LEVEL >= level) { + StringBuilder sb = new StringBuilder(); + sb.append(getLevel(level) + " - " + Thread.currentThread().getName() + " : "); + sb.append(message); + System.out.println(sb.toString()); + } + } + + @SuppressWarnings("rawtypes") + public void log(ServiceReference sr, int level, String message, Throwable exception) { + checkError(level, exception); + if (LOG_LEVEL >= level) { + StringBuilder sb = new StringBuilder(); + sb.append(getLevel(level) + " - " + Thread.currentThread().getName() + " : "); + sb.append(message); + parse(sb, exception); + System.out.println(sb.toString()); + } + } + + protected boolean errorsLogged() { + return m_errorsLogged; + } + + private void parse(StringBuilder sb, Throwable t) { + if (t != null) { + sb.append(" - "); + StringWriter buffer = new StringWriter(); + PrintWriter pw = new PrintWriter(buffer); + t.printStackTrace(pw); + sb.append(buffer.toString()); + m_errorsLogged = true; + } + } + + private String getLevel(int level) { + switch (level) { + case LogService.LOG_DEBUG : + return "DEBUG"; + case LogService.LOG_ERROR : + return "ERROR"; + case LogService.LOG_INFO : + return "INFO"; + case LogService.LOG_WARNING : + return "WARN"; + default : + return ""; + } + } + + private void checkError(int level, Throwable exception) { + if (level <= LOG_ERROR) { + m_errorsLogged = true; + } + if (exception != null) { + m_errorsLogged = true; + } + } + + public void frameworkEvent(FrameworkEvent event) { + int eventType = event.getType(); + String msg = getFrameworkEventMessage(eventType); + int level = (eventType == FrameworkEvent.ERROR) ? LOG_ERROR : LOG_WARNING; + if (msg != null) { + log(level, msg, event.getThrowable()); + } else { + log(level, "Unknown fwk event: " + event); + } + } + + private String getFrameworkEventMessage(int event) { + switch (event) { + case FrameworkEvent.ERROR : + return "FrameworkEvent: ERROR"; + case FrameworkEvent.INFO : + return "FrameworkEvent INFO"; + case FrameworkEvent.PACKAGES_REFRESHED : + return "FrameworkEvent: PACKAGE REFRESHED"; + case FrameworkEvent.STARTED : + return "FrameworkEvent: STARTED"; + case FrameworkEvent.STARTLEVEL_CHANGED : + return "FrameworkEvent: STARTLEVEL CHANGED"; + case FrameworkEvent.WARNING : + return "FrameworkEvent: WARNING"; + default : + return null; + } + } + + protected void warn(String msg, Object ... params) { + if (LOG_LEVEL >= LogService.LOG_WARNING) { + log(LogService.LOG_WARNING, params.length > 0 ? String.format(msg, params) : msg); + } + } + + @SuppressWarnings("unused") + protected void info(String msg, Object ... params) { + if (LOG_LEVEL >= LogService.LOG_INFO) { + log(LogService.LOG_INFO, params.length > 0 ? String.format(msg, params) : msg); + } + } + + @SuppressWarnings("unused") + protected void debug(String msg, Object ... params) { + if (LOG_LEVEL >= LogService.LOG_DEBUG) { + log(LogService.LOG_DEBUG, params.length > 0 ? String.format(msg, params) : msg); + } + } + + protected void error(String msg, Object ... params) { + log(LogService.LOG_ERROR, params.length > 0 ? String.format(msg, params) : msg); + } + + protected void error(String msg, Throwable err, Object ... params) { + log(LogService.LOG_ERROR, params.length > 0 ? String.format(msg, params) : msg, err); + } + + protected void error(Throwable err) { + log(LogService.LOG_ERROR, "error", err); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/lifecycle/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/lifecycle/Activator.java new file mode 100644 index 00000000000..49c4e391100 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/lifecycle/Activator.java @@ -0,0 +1,38 @@ +/* + * 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.dm.lambda.itest.lifecycle; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.lambda.DependencyManagerActivator; +import org.apache.felix.dm.lambda.itest.Ensure; +import org.osgi.framework.BundleContext; + +/** + * Activator for our SimpleComponent. + */ +public class Activator extends DependencyManagerActivator { + + @Override + protected void init(BundleContext ctx, DependencyManager dm) throws Exception { + component(comp -> comp + .impl(SimpleComponent.class) + .withSvc(Ensure.class, "(name=" + SimpleComponent.class.getName() + ")", true)); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/lifecycle/SimpleComponent.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/lifecycle/SimpleComponent.java new file mode 100644 index 00000000000..6e7ed61cff0 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/src/org/apache/felix/dm/lambda/itest/lifecycle/SimpleComponent.java @@ -0,0 +1,36 @@ +/* + * 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.dm.lambda.itest.lifecycle; + +import org.apache.felix.dm.lambda.itest.Ensure; + +/** + * Checks if component start/stop method are properly called when the bundle is started / stopped. + */ +public class SimpleComponent { + volatile Ensure m_ensure; + + void start() { + m_ensure.step(1); + } + + void stop() { + m_ensure.step(2); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/tests.bnd b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/tests.bnd new file mode 100644 index 00000000000..f27837d0864 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.itest/tests.bnd @@ -0,0 +1,18 @@ +# +# 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. +# +Export-Package: \ + org.apache.felix.dm.lambda.itest diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/.classpath b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/.classpath new file mode 100644 index 00000000000..7a6fc254361 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/.gitignore new file mode 100644 index 00000000000..90dde36e4ac --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/.gitignore @@ -0,0 +1,3 @@ +/bin/ +/bin_test/ +/generated/ diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/.project b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/.project new file mode 100644 index 00000000000..4e58a1cbb63 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/.project @@ -0,0 +1,23 @@ + + + org.apache.felix.dependencymanager.lambda.samples + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/.settings/org.eclipse.jdt.core.prefs b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..144c60c0ff4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/bnd.bnd b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/bnd.bnd new file mode 100644 index 00000000000..5b5b5a1066e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/bnd.bnd @@ -0,0 +1,18 @@ +-buildpath: \ + org.apache.felix.dependencymanager.lambda;version=latest,\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.gogo.runtime;version=latest,\ + osgi.core;version=6.0,\ + osgi.cmpn;version=6.0,\ + biz.aQute.bnd.annotation + +Bundle-Version: 0.0.0.${tstamp} +-sub: *.bnd +-metatype: * +Bundle-License: http://www.apache.org/licenses/LICENSE-2.0.txt +Bundle-DocURL: http://felix.apache.org/documentation/subprojects/apache-felix-dependency-manager.html +Bundle-Vendor: The Apache Software Foundation + +# we do not release this project in binary distribution. +-releaserepo: +-baseline: diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/build.gradle b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/build.gradle new file mode 100644 index 00000000000..024d8b6ff06 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/build.gradle @@ -0,0 +1,22 @@ +/* + * 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. + */ + +tasks.withType(JavaCompile) { + options.compilerArgs << "-parameters" +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/compositefactory.bnd b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/compositefactory.bnd new file mode 100644 index 00000000000..2b7dcdd0aae --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/compositefactory.bnd @@ -0,0 +1,18 @@ +# +# 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. +# +Private-Package: org.apache.felix.dm.lambda.samples.compositefactory +Bundle-Activator: org.apache.felix.dm.lambda.samples.compositefactory.Activator diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/compositefactory.bndrun b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/compositefactory.bndrun new file mode 100644 index 00000000000..c7c51698814 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/compositefactory.bndrun @@ -0,0 +1,36 @@ +# +# 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. +# +-runfw: ${felix.framework} +-runee: JavaSE-1.8 +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active;skip:="osgi.service" + +-runbundles: \ + ${gogo},\ + ${log},\ + ${configadmin},\ + ${metatype},\ + org.apache.felix.dependencymanager.lambda.samples.compositefactory;version=latest,\ + org.apache.felix.dependencymanager;version=4.2.0,\ + org.apache.felix.dependencymanager.shell;version=4.0.3,\ + org.apache.felix.dependencymanager.lambda;version=latest + +-runproperties: \ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/device.bnd b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/device.bnd new file mode 100644 index 00000000000..0cf4b65eb1f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/device.bnd @@ -0,0 +1,18 @@ +# +# 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. +# +Private-Package: org.apache.felix.dm.lambda.samples.device +Bundle-Activator: org.apache.felix.dm.lambda.samples.device.Activator diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/device.bndrun b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/device.bndrun new file mode 100644 index 00000000000..9896791edf5 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/device.bndrun @@ -0,0 +1,36 @@ +# +# 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. +# +-runfw: ${felix.framework} +-runee: JavaSE-1.8 +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active;skip:="osgi.service" + +-runbundles: \ + ${gogo},\ + ${log},\ + ${configadmin},\ + ${metatype},\ + org.apache.felix.dependencymanager.lambda.samples.device;version=latest,\ + org.apache.felix.dependencymanager;version=4.2.0,\ + org.apache.felix.dependencymanager.shell;version=4.0.3,\ + org.apache.felix.dependencymanager.lambda;version=latest + +-runproperties: \ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/dictionary.bnd b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/dictionary.bnd new file mode 100644 index 00000000000..41e8c6de05a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/dictionary.bnd @@ -0,0 +1,18 @@ +# +# 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. +# +Private-Package: org.apache.felix.dm.lambda.samples.dictionary +Bundle-Activator: org.apache.felix.dm.lambda.samples.dictionary.Activator diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/dictionary.bndrun b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/dictionary.bndrun new file mode 100644 index 00000000000..7533e6fe674 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/dictionary.bndrun @@ -0,0 +1,38 @@ +# +# 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. +# +-runfw: ${felix.framework} +-runee: JavaSE-1.8 +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active;skip:="osgi.service" + +-runbundles: \ + ${metatype},\ + ${log},\ + ${gogo},\ + ${configadmin},\ + ${eventadmin},\ + ${webconsole},\ + org.apache.felix.dependencymanager.lambda.samples.dictionary;version=latest,\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.dependencymanager.lambda;version=latest + +-runproperties: \ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/factory.bnd b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/factory.bnd new file mode 100644 index 00000000000..6de3b6229b8 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/factory.bnd @@ -0,0 +1,18 @@ +# +# 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. +# +Private-Package: org.apache.felix.dm.lambda.samples.factory +Bundle-Activator: org.apache.felix.dm.lambda.samples.factory.Activator diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/factory.bndrun b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/factory.bndrun new file mode 100644 index 00000000000..c5f88dec8e2 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/factory.bndrun @@ -0,0 +1,37 @@ +# +# 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. +# +-runfw: ${felix.framework} +-runee: JavaSE-1.8 +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active;skip:="osgi.service" + +-runbundles: \ + ${metatype},\ + ${log},\ + ${gogo},\ + ${configadmin},\ + ${eventadmin},\ + org.apache.felix.dependencymanager.lambda.samples.factory;version=latest,\ + org.apache.felix.dependencymanager;version=4.2.0,\ + org.apache.felix.dependencymanager.shell;version=4.0.3,\ + org.apache.felix.dependencymanager.lambda;version=latest + +-runproperties: \ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/future.bnd b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/future.bnd new file mode 100644 index 00000000000..0eb1e71c89b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/future.bnd @@ -0,0 +1,18 @@ +# +# 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. +# +Private-Package: org.apache.felix.dm.lambda.samples.future +Bundle-Activator: org.apache.felix.dm.lambda.samples.future.Activator diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/future.bndrun b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/future.bndrun new file mode 100644 index 00000000000..88209a282a5 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/future.bndrun @@ -0,0 +1,37 @@ +# +# 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. +# +-runfw: ${felix.framework} +-runee: JavaSE-1.8 +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active;skip:="osgi.service" + +-runbundles: \ + ${metatype},\ + ${log},\ + ${gogo},\ + ${eventadmin},\ + ${configadmin},\ + org.apache.felix.dependencymanager.lambda.samples.future;version=latest,\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.dependencymanager.lambda;version=latest + +-runproperties: \ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/hello.bnd b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/hello.bnd new file mode 100644 index 00000000000..3a7b482f3eb --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/hello.bnd @@ -0,0 +1,18 @@ +# +# 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. +# +Private-Package: org.apache.felix.dm.lambda.samples.hello +Bundle-Activator: org.apache.felix.dm.lambda.samples.hello.Activator diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/hello.bndrun b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/hello.bndrun new file mode 100644 index 00000000000..c74805fa1ca --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/hello.bndrun @@ -0,0 +1,37 @@ +# +# 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. +# +-runfw: ${felix.framework} +-runee: JavaSE-1.8 +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active;skip:="osgi.service" + +-runbundles: \ + ${metatype},\ + ${log},\ + ${gogo},\ + ${eventadmin},\ + ${configadmin},\ + org.apache.felix.dependencymanager.lambda.samples.hello;version=latest,\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.dependencymanager.lambda;version=latest + +-runproperties: \ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/Activator.java new file mode 100644 index 00000000000..dbd66e6b079 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/Activator.java @@ -0,0 +1,63 @@ +/* + * 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.dm.lambda.samples.compositefactory; + +import static java.lang.System.out; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.lambda.DependencyManagerActivator; +import org.osgi.framework.BundleContext; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.log.LogService; + +/** + * Creates a "Provider" service. The implementation for this service (ProviderImpl) is + * created using a factory class (ProviderFactory) that also creates some other helper classes + * that are internally used by ProviderImpl (ProviderComposite1 and ProviderComposite2). + * + * The ProviderFactory is also injected with a Configuration that can be used by the Factory + * when creating the ProviderImpl, ProviderComposite1, and ProviderComposite2 classes. + * + * The LogService in only injected to the ProviderImpl and the ProviderComposite1 classes. + * objects being part of the composition are called in "start" lifecycle callback when all + * required dependencies are available. + * + * @author Felix Project Team + */ +public class Activator extends DependencyManagerActivator { + @Override + public void init(BundleContext ctx, DependencyManager dm) throws Exception { + out.println("type \"log warn\" to see the logs emitted by this test."); + + // Create the Factory used to instantiate ProviderImpl, ProviderComposite1 and ProviderComposite2 + ProviderFactory factory = new ProviderFactory(); + + // Define the component which implementation is instantiated by the ProviderFactory. + // a LogService is injected in the ProviderImpl, as well as to the ProviderComposite1 class. + // And a configuration is injected directly to the ProviderFactory so it can use some configurations + // before creating the composition of classes. + component(comp -> comp + .factory(factory::create, factory::getComposition) + .withSvc(LogService.class, svc -> svc.required().add(ProviderImpl::bind).add(ProviderComposite1::bind)) + .withCnf(cnf -> cnf.update(MyConfig.class, factory::updated))); + + // Creates a configuration with pid name = "org.apache.felix.dependencymanager.lambda.samples.compositefactory.MyConfig" + component(comp -> comp.impl(Configurator.class).withSvc(ConfigurationAdmin.class, true)); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/Configurator.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/Configurator.java new file mode 100644 index 00000000000..1e8f9bf2435 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/Configurator.java @@ -0,0 +1,38 @@ +/* + * 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.dm.lambda.samples.compositefactory; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +public class Configurator { + volatile ConfigurationAdmin m_cm; // injected by reflection. + + void start() throws IOException { + // Configure the ServiceConsumer component + Configuration c = m_cm.getConfiguration(MyConfig.class.getName(), null); + Dictionary props = new Hashtable<>(); + props.put("foo", "bar"); + c.update(props); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/MyConfig.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/MyConfig.java new file mode 100644 index 00000000000..72ee2e27af6 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/MyConfig.java @@ -0,0 +1,27 @@ +/* + * 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.dm.lambda.samples.compositefactory; + +/** + * Our properties interface that is implemented by DependencyManager. + * @author Felix Project Team + */ +public interface MyConfig { + String getFoo(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/Provider.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/Provider.java new file mode 100644 index 00000000000..75c2773432e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/Provider.java @@ -0,0 +1,26 @@ +/* + * 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.dm.lambda.samples.compositefactory; + +/** + * @author Felix Project Team + */ +public interface Provider { + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/ProviderComposite1.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/ProviderComposite1.java new file mode 100644 index 00000000000..0ee4d91b7b1 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/ProviderComposite1.java @@ -0,0 +1,40 @@ +/* + * 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.dm.lambda.samples.compositefactory; + +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class ProviderComposite1 { + private volatile LogService m_log; + + public void bind(LogService log) { + m_log = log; + } + + void start() { + m_log.log(LogService.LOG_WARNING, "ProviderParticipant1.start()"); + } + + public String toString() { + return "ProviderComposite1"; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/ProviderComposite2.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/ProviderComposite2.java new file mode 100644 index 00000000000..12b4fa33dab --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/ProviderComposite2.java @@ -0,0 +1,28 @@ +/* + * 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.dm.lambda.samples.compositefactory; + +/** + * @author Felix Project Team + */ +public class ProviderComposite2 { + public String toString() { + return "ProviderComposite2"; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/ProviderFactory.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/ProviderFactory.java new file mode 100644 index 00000000000..eba4df2e451 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/ProviderFactory.java @@ -0,0 +1,59 @@ +/* + * 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.dm.lambda.samples.compositefactory; + +/** + * Pojo used to create all the objects composition used to implements the "Provider" Service. + * The manager is using a Configuration injected by Config Admin, in order to configure the + * various objects being part of the "Provider" service implementation. + * + * @author Felix Project Team + */ +public class ProviderFactory { + private ProviderComposite1 m_composite1; + private ProviderComposite2 m_composite2; + private ProviderImpl m_providerImpl; + @SuppressWarnings("unused") + private MyConfig m_conf; + + public void updated(MyConfig conf) { + m_conf = conf; // conf.getFoo() returns "bar" + } + + /** + * Builds the composition of objects used to implement the "Provider" service. + * The Configuration injected by Config Admin will be used to configure the components + * @return The "main" object providing the "Provider" service. + */ + ProviderImpl create() { + // Here, we can instantiate our object composition and configure them using the injected Configuration ... + m_composite1 = new ProviderComposite1(); // possibly configure this object using our configuration + m_composite2 = new ProviderComposite2(); // possibly configure this object using our configuration + m_providerImpl = new ProviderImpl(m_composite1, m_composite2); + return m_providerImpl; // Main object implementing the Provider service + } + + /** + * Returns the + * @return + */ + Object[] getComposition() { + return new Object[] { m_providerImpl, m_composite1, m_composite2 }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/ProviderImpl.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/ProviderImpl.java new file mode 100644 index 00000000000..1c69b850df5 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/ProviderImpl.java @@ -0,0 +1,49 @@ +/* + * 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.dm.lambda.samples.compositefactory; + +import org.osgi.service.log.LogService; + +/** + * This is the main implementation for our "Provider" service. + * This service is using a composition of two participants, which are used to provide the service + * (ProviderParticipant1, and ProviderParticipant2). + * + * This class is instantiated by the CompositionManager class. + * + * @author Felix Project Team + */ +public class ProviderImpl implements Provider { + private final ProviderComposite1 m_participant1; + private final ProviderComposite2 m_participant2; + private volatile LogService m_log; + + public void bind(LogService log) { + m_log = log; + } + + ProviderImpl(ProviderComposite1 participant1, ProviderComposite2 participant2) { + m_participant1 = participant1; + m_participant2 = participant2; + } + + void start() { + m_log.log(LogService.LOG_WARNING, "ProviderImpl.start(): participants=" + m_participant1 + "," + m_participant2); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/README b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/README new file mode 100644 index 00000000000..b9ab26074ae --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/compositefactory/README @@ -0,0 +1,33 @@ +/* + * 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. + */ + +This Activator is an example usage of DM composite components. A composite component is implemented +using a composition of multiple object instances, which are used to implement a given service. + +The sample also uses a Factory approach in order to instantiate the composition of objects: A +"CompositionManager" is first injected with a Configuration that can possibly be used to create +and configure all the composites. + +Dependencies are injected is some of the component implementation instances, using java8 method references. For instance, +the LogService is only injected in the ProviderImpl and the ProviderComposite1 class and not in the ProviderComposite2 class. + +To see logs, type this command under the gogo shell: + +g! log info|grep compositefactory + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/Activator.java new file mode 100644 index 00000000000..53a980fead0 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/Activator.java @@ -0,0 +1,60 @@ +/* + * 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.dm.lambda.samples.device; + +import static java.lang.System.out; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.lambda.DependencyManagerActivator; +import org.osgi.framework.BundleContext; +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class Activator extends DependencyManagerActivator { + @Override + public void init(BundleContext ctx, DependencyManager dm) throws Exception { + out.println("type \"log warn\" to see the logs emitted by this test."); + + // Create a pair of Device/DeviceParameter service with id=1 + createDeviceAndParameter(1); + + // Create a pair of Device/DeviceParameter service with id=2 + createDeviceAndParameter(2); + + // Create a DeviceParameter adapter: for each pair of Device/DeviceParameter services having the same id, + // a DeviceParameter adapter service will be created. + adapter(Device.class, adpt -> adpt.provides(DeviceAccess.class).impl(DeviceAccessImpl.class)); + + // Creates a component that simply displays all available DeviceParameter adapter services. + component(comp -> comp + .impl(DeviceAccessConsumer.class) + .withSvc(LogService.class, true) + .withSvc(DeviceAccess.class, svc -> svc.required().add(DeviceAccessConsumer::add))); + } + + private void createDeviceAndParameter(int id) { + // Creates a Device service with the provided id. + component(comp -> comp.factory(() -> new DeviceImpl(id)).provides(Device.class, "device.id", id)); + + // Creates a DeivceParameter with the provided id. + component(comp -> comp.factory(() -> new DeviceParameterImpl(id)).provides(DeviceParameter.class, "device.id", id)); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/Device.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/Device.java new file mode 100644 index 00000000000..0e4cf104768 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/Device.java @@ -0,0 +1,26 @@ +/* + * 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.dm.lambda.samples.device; + +/** + * @author Felix Project Team + */ +public interface Device { + int getDeviceId(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/DeviceAccess.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/DeviceAccess.java new file mode 100644 index 00000000000..f9f0ec44029 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/DeviceAccess.java @@ -0,0 +1,30 @@ +/* + * 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.dm.lambda.samples.device; + +/** + * Provides unified access to a pair of Device/DeviceParameter services having the same device ID. + + * @author Felix Project Team + */ +public interface DeviceAccess { + Device getDevice(); + + DeviceParameter getDeviceParameter(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/DeviceAccessConsumer.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/DeviceAccessConsumer.java new file mode 100644 index 00000000000..22dded4d3d6 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/DeviceAccessConsumer.java @@ -0,0 +1,37 @@ +/* + * 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.dm.lambda.samples.device; + +import java.util.Map; + +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class DeviceAccessConsumer { + volatile LogService log; + + void add(DeviceAccess deviceAccess, Map props) { + log.log(LogService.LOG_WARNING, "DeviceAccessConsumer: Handling device access: id=" + props.get("device.access.id") + + "\n\t device=" + deviceAccess.getDevice() + + "\n\t device parameter=" + deviceAccess.getDeviceParameter() + + "\n\t device access properties=" + props); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/DeviceAccessImpl.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/DeviceAccessImpl.java new file mode 100644 index 00000000000..dab5d1b3684 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/DeviceAccessImpl.java @@ -0,0 +1,50 @@ +/* + * 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.dm.lambda.samples.device; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import org.apache.felix.dm.Component; + +/** + * @author Felix Project Team + */ +public class DeviceAccessImpl implements DeviceAccess { + volatile Device device; + volatile DeviceParameter deviceParameter; + + void init(Component c) { + // Dynamically add an extra dependency on a DeviceParameter. + // Notice that we also add a "device.access.id" service property dynamically. + + component(c, builder -> builder + .properties("device.access.id", device.getDeviceId()) + .withSvc(DeviceParameter.class, svc -> svc.required().filter("(device.id=" + device.getDeviceId() + ")"))); + } + + @Override + public Device getDevice() { + return device; + } + + @Override + public DeviceParameter getDeviceParameter() { + return deviceParameter; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/DeviceImpl.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/DeviceImpl.java new file mode 100644 index 00000000000..28c2e159916 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/DeviceImpl.java @@ -0,0 +1,41 @@ +/* + * 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.dm.lambda.samples.device; + +/** + * @author Felix Project Team + */ +public class DeviceImpl implements Device { + final int id; + + public DeviceImpl(int id) { + this.id = id; + } + + @Override + public int getDeviceId() { + return id; + } + + + @Override + public String toString() { + return "Device #" + id; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/DeviceParameter.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/DeviceParameter.java new file mode 100644 index 00000000000..f6efdab804e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/DeviceParameter.java @@ -0,0 +1,26 @@ +/* + * 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.dm.lambda.samples.device; + +/** + * @author Felix Project Team + */ +public interface DeviceParameter { + int getDeviceId(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/DeviceParameterImpl.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/DeviceParameterImpl.java new file mode 100644 index 00000000000..a97ae4780b4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/DeviceParameterImpl.java @@ -0,0 +1,40 @@ +/* + * 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.dm.lambda.samples.device; + +/** + * @author Felix Project Team + */ +public class DeviceParameterImpl implements DeviceParameter { + final int id; + + public DeviceParameterImpl(int id) { + this.id = id; + } + + @Override + public int getDeviceId() { + return id; + } + + @Override + public String toString() { + return "DeviceParameter #" + id; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/README b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/README new file mode 100644 index 00000000000..d013c2844b6 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/device/README @@ -0,0 +1,30 @@ +/* + * 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. + */ + +This is an example showing a Dependency Manager "Adapter" in action. Two kinds of services are +registered in the registry: some Device, and some DeviceParameter services. For each Device (having +a given id), there is also a corresponding "DeviceParameter" service, having the same id. + +Then a "DeviceAccessImpl" adapter service is defined: it is used to "adapt" the "Device" service to +a "DeviceAccess" service, which provides the union of each pair of Device/DeviceParameter having the +same device.id . The adapter also dynamically propagate the service properties of the adapted Device +service. + +So see logs, just type this command under gogo shell: + +g! log info|grep device.api + diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/Activator.java new file mode 100644 index 00000000000..e9d6b097e06 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/Activator.java @@ -0,0 +1,62 @@ +/* + * 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.dm.lambda.samples.dictionary; + +import static java.lang.System.out; +import static org.apache.felix.service.command.CommandProcessor.COMMAND_FUNCTION; +import static org.apache.felix.service.command.CommandProcessor.COMMAND_SCOPE; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.lambda.DependencyManagerActivator; +import org.osgi.framework.BundleContext; +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class Activator extends DependencyManagerActivator { + @Override + public void init(BundleContext ctx, DependencyManager dm) throws Exception { + out.println("type \"log info\" to see the logs emitted by this test."); + + // Create the factory configuration for our DictionaryImpl service. An instance of the DictionaryImpl is created for each + // factory configuration that you add from webconsole (using the DictionaryConfiguration factory pid). + factoryPidAdapter(adapter -> adapter + .impl(DictionaryImpl.class) + .provides(DictionaryService.class) + .propagate() + .update(DictionaryConfiguration.class, DictionaryImpl::updated) + .withSvc(LogService.class, true)); + + // Create the Dictionary Aspect that decorates any registered Dictionary service. For each Dictionary, an instance of the + // DictionaryAspect service is created). + aspect(DictionaryService.class, aspect -> aspect + .impl(DictionaryAspect.class) + .filter("(lang=en)").rank(10) + .withCnf(cnf -> cnf.update(DictionaryAspectConfiguration.class, DictionaryAspect::addWords)) + .withSvc(LogService.class, true)); + + // Create the SpellChecker component. It depends on all available DictionaryService instances, possibly + // decorated by some DictionaryAspects. + component(comp -> comp + .impl(SpellChecker.class) + .provides(SpellChecker.class, COMMAND_SCOPE, "dictionary", COMMAND_FUNCTION, new String[] {"spellcheck"}) + .withSvc(true, LogService.class, DictionaryService.class)); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/DictionaryAspect.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/DictionaryAspect.java new file mode 100644 index 00000000000..0e7060c100b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/DictionaryAspect.java @@ -0,0 +1,81 @@ +/* + * 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.dm.lambda.samples.dictionary; + +import java.util.concurrent.CopyOnWriteArrayList; + +import org.osgi.service.log.LogService; + +/** + * This aspect applies to the English DictionaryService, and allows to decorate it with some + * custom English words, which are configurable from WebConsole. + * + * @author Felix Project Team + */ +public class DictionaryAspect implements DictionaryService { + /** + * This is the service this aspect is applying to. + */ + private volatile DictionaryService m_originalDictionary; + + /** + * We store all configured words in a thread-safe data structure, because ConfigAdmin may + * invoke our updated method at any time. + */ + private CopyOnWriteArrayList m_words = new CopyOnWriteArrayList(); + + /** + * We'll use the OSGi log service for logging. If no log service is available, then we'll + * use a NullObject. + */ + private LogService m_log; + + /** + * Defines a configuration dependency for retrieving our english custom words. + * Dependency Manager will inject a dynamic proxy that implements our DictionaryAspectConfiguration interface. + * The dynamic proxy is used to wrap the actual Dictionary configuration. + * The pid for our configuration is by default the fqdn of our DictionaryAspectConfiguration interface. + */ + protected void addWords(DictionaryAspectConfiguration cnf) { + if (cnf != null) { + m_words.clear(); + for (String word : cnf.words()) { + m_words.add(word); + } + } + } + + /** + * Our Aspect Service is starting and is about to be registered in the OSGi regsitry. + */ + protected void start() { + m_log.log(LogService.LOG_INFO, "Starting aspect Dictionary with words: " + m_words + + "; original dictionary service=" + m_originalDictionary); + } + + /** + * Checks if a word is found from our custom word list. if not, delegate to the decorated + * dictionary. + */ + public boolean checkWord(String word) { + m_log.log(LogService.LOG_INFO, "DictionaryAspect: checking word " + word + " (original dictionary=" + + m_originalDictionary + ")"); + if (m_words.contains(word)) { + return true; + } + return m_originalDictionary.checkWord(word); + } + + public String toString() { + return "DictionaryAspect: words=" + m_words + "; original dictionary=" + m_originalDictionary; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/DictionaryAspectConfiguration.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/DictionaryAspectConfiguration.java new file mode 100644 index 00000000000..8bfa8610526 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/DictionaryAspectConfiguration.java @@ -0,0 +1,39 @@ +/* + * 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.dm.lambda.samples.dictionary; + +import java.util.List; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +/** + * This interface describes the configuration for our DictionaryAspect component. We are using the bnd metatype + * annotations, allowing to configure our Dictionary Services from web console. + * + * @author Felix Project Team + */ +@ObjectClassDefinition( + name="Spell Checker Dictionary Aspect", + description = "Declare here the list of english words to be added into the default english dictionary", + pid="org.apache.felix.dm.lambda.samples.dictionary.DictionaryAspectConfiguration") +public interface DictionaryAspectConfiguration { + @AttributeDefinition(description = "Dictionary aspect words") + List words(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/DictionaryConfiguration.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/DictionaryConfiguration.java new file mode 100644 index 00000000000..c4811c08f63 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/DictionaryConfiguration.java @@ -0,0 +1,41 @@ +/* + * 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.dm.lambda.samples.dictionary; + +import java.util.List; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +/** + * This interface describes the configuration for our DictionaryImpl component. We are using the bnd metatype + * annotations, allowing to configure our Dictionary Services from web console. + * + * @author Felix Project Team + */ +@ObjectClassDefinition(name="Spell Checker Dictionary", + factoryPid = "org.apache.felix.dm.lambda.samples.dictionary.DictionaryConfiguration", + description = "Declare here some Dictionary instances, allowing to instantiates some DictionaryService services for a given dictionary language") +public interface DictionaryConfiguration { + @AttributeDefinition(description = "Describes the dictionary language", defaultValue = "en") + String lang(); + + @AttributeDefinition(description = "Declare here the list of words supported by this dictionary.") + List words(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/DictionaryImpl.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/DictionaryImpl.java new file mode 100644 index 00000000000..87328f9d0c4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/DictionaryImpl.java @@ -0,0 +1,88 @@ +/* + * 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.dm.lambda.samples.dictionary; + +import java.util.concurrent.CopyOnWriteArrayList; + +import org.osgi.service.log.LogService; + +/** + * A Dictionary Service, instantiated from webconsole, when you add some configurations instances to the + * DictionaryConfiguration factory pid. The Configuration metatype informations is described using the + * bnd metatype information (see the DictionaryConfiguration interface). + * + * You must configure at least one Dictionary from web console, since the SpellCheck won't start if no Dictionary + * Service is available. + * + * @author Felix Project Team + */ +public class DictionaryImpl implements DictionaryService { + /** + * The key of our config admin dictionary values. + */ + final static String WORDS = "words"; + + /** + * We store all configured words in a thread-safe data structure, because ConfigAdmin + * may invoke our updated method at any time. + */ + private CopyOnWriteArrayList m_words = new CopyOnWriteArrayList(); + + /** + * We'll use the OSGi log service for logging. If no log service is available, then we'll use a NullObject. + */ + private LogService m_log; + + /** + * Our Dictionary language. + */ + private String m_lang; + + /** + * Our service will be initialized from ConfigAdmin. + * @param config The configuration where we'll lookup our words list (key=".words"). + */ + protected void updated(DictionaryConfiguration cnf) { + m_lang = cnf.lang(); + m_words.clear(); + for (String word : cnf.words()) { + m_words.add(word); + } + } + + /** + * A new Dictionary Service is starting (because a new factory configuration has been created + * from webconsole). + */ + protected void start() { + m_log.log(LogService.LOG_INFO, "Starting Dictionary Service with language: " + m_lang); + } + + /** + * Check if a word exists if the list of words we have been configured from ConfigAdmin/WebConsole. + */ + public boolean checkWord(String word) { + return m_words.contains(word); + } + + @Override + public String toString() { + return "Dictionary: language=" + m_lang + ", words=" + m_words; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/DictionaryService.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/DictionaryService.java new file mode 100644 index 00000000000..535210a0f96 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/DictionaryService.java @@ -0,0 +1,35 @@ +/* + * 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.dm.lambda.samples.dictionary; + +/** + * A simple service interface that defines a dictionary service. A dictionary + * service simply verifies the existence of a word. + * + * @author Felix Project Team + */ +public interface DictionaryService { + /** + * Check for the existence of a word. + * + * @param word the word to be checked. + * @return true if the word is in the dictionary, false otherwise. + */ + public boolean checkWord(String word); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/README b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/README new file mode 100644 index 00000000000..ac0e55853f9 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/dictionary/README @@ -0,0 +1,41 @@ +/* + * 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. + */ + +This sample shows a "SpellChecker" application which provides a +"dictionary:spellcheck" GOGO shell command. The GOGO "dictionary:spellcheck" command accepts a +string as parameter, which is checked for proper exactness. The SpellChecker class has a +required/multiple (1..N) dependency over every available "DictionaryService" services, which are +internally used by the SpellChecker command, when checking word exactness. + +A DictionaryService is defined using a FactoryPid adapter , allowing to instantiate +many "DictionaryService" instances for each configuration that are added to the +factory pid "Spell Checker Configuration" from web console. +The factory pid configuration metatypes are defined using the bnd "metatype" annotations +(see DictionaryConfiguration.java). + +The DictionaryService is decorated with a DictionaryAspect, which you can instantiate by adding a +configuration to the "Spell Checker Aspect Dictionary" pid from web console. The +aspect configuration metatype is also declared using the bnd metatype annotations (see +DictionaryAspectConfiguration.java). + +Before running this sample, go to webconsole, and add some words in the "Spell Checker Configuration"factory PID, and +in the "Spell Checker Dictionary Aspect" PID. + +Then go to gogo shell, and type dm help. You will normally see the dictionary:spellcheck command. +Type dictionary:spellcheck Felix Project Team + */ +public class SpellChecker { + /** + * We'll use the OSGi log service for logging. If no log service is available, then we'll use a NullObject. + */ + private volatile LogService m_log; + + /** + * We'll store all Dictionaries in a concurrent list, in order to avoid method synchronization. + * (Auto-Injected from Activator, at any time). DependencyManager detects that the field is an + * Iterable of a DictionaryService and automatically adds any available DictionaryService, at any time. + */ + private final Iterable m_dictionaries = new ConcurrentLinkedQueue<>(); + + /** + * Lifecycle method callback, used to check if our service has been activated. + */ + protected void start() { + m_log.log(LogService.LOG_WARNING, "Spell Checker started"); + } + + /** + * Lifecycle method callback, used to check if our service has been activated. + */ + protected void stop() { + m_log.log(LogService.LOG_WARNING, "Spell Checker stopped"); + } + + // --- Gogo Shell command + + @Descriptor("checks if word is found from an available dictionary") + public void spellcheck(@Descriptor("the word to check") String word) { + m_log.log(LogService.LOG_INFO, "Checking spelling of word \"" + word + "\" using the following dictionaries: " + + m_dictionaries); + + for (DictionaryService dictionary : m_dictionaries) { + if (dictionary.checkWord(word)) { + System.out.println("word " + word + " is correct"); + return; + } + } + System.err.println("word " + word + " is incorrect"); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/factory/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/factory/Activator.java new file mode 100644 index 00000000000..26fb28df219 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/factory/Activator.java @@ -0,0 +1,41 @@ +/* + * 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.dm.lambda.samples.factory; + +import static java.lang.System.out; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.lambda.DependencyManagerActivator; +import org.osgi.framework.BundleContext; +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class Activator extends DependencyManagerActivator { + @Override + public void init(BundleContext ctx, DependencyManager dm) throws Exception { + out.println("type \"log warn\" to see the logs emitted by this test."); + + component(comp -> comp + .factory(ProviderFactory::new, ProviderFactory::create) + .provides(Provider.class) + .withSvc(LogService.class, log -> log.required().add(ProviderImpl::set))); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/factory/Provider.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/factory/Provider.java new file mode 100644 index 00000000000..7b06e63962e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/factory/Provider.java @@ -0,0 +1,25 @@ +/* + * 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.dm.lambda.samples.factory; + +/** + * @author Felix Project Team + */ +public interface Provider { +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/factory/ProviderFactory.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/factory/ProviderFactory.java new file mode 100644 index 00000000000..6ccff7d21fc --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/factory/ProviderFactory.java @@ -0,0 +1,25 @@ +/* + * 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.dm.lambda.samples.factory; + +public class ProviderFactory { + public ProviderImpl create() { + return new ProviderImpl(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/factory/ProviderImpl.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/factory/ProviderImpl.java new file mode 100644 index 00000000000..11d72f49c77 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/factory/ProviderImpl.java @@ -0,0 +1,38 @@ +/* + * 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.dm.lambda.samples.factory; + +import org.osgi.service.log.LogService; + +/** + * This is the main implementation for our "Provider" service. + * This service is using a composition of two participants, which are used to provide the service + * (ProviderParticipant1, and ProviderParticipant2). + * + * @author Felix Project Team + */ +public class ProviderImpl implements Provider { + private volatile LogService m_log; + + void set(LogService log) { m_log = log; } + + void start() { + m_log.log(LogService.LOG_WARNING, "ProviderImpl.start()"); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/factory/README b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/factory/README new file mode 100644 index 00000000000..3093738ee95 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/factory/README @@ -0,0 +1,23 @@ +/* + * 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. + */ + +This sample is an example usage of DM components that are created using a Factory object. +The Factory is defined using java8 method references. + +To see logs, type this command under the gogo shell: + +g! log info|grep compositefactory diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/future/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/future/Activator.java new file mode 100644 index 00000000000..3a11dd60482 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/future/Activator.java @@ -0,0 +1,66 @@ +/* + * 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.dm.lambda.samples.future; + +import static java.lang.System.out; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.lambda.DependencyManagerActivator; +import org.osgi.framework.BundleContext; +import org.osgi.service.log.LogService; + +/** + * This examples show how to use the new "Future" dependency available from the dependencymanager-lambda library. + * The PageLink component provides the list of available hrefs found from the Felix web site. + * The page is downloaded asynchronously using a CompletableFuture, and the component of the PageLinkImpl class + * will wait for the completion of the future before start. + * + * The interesting thing to look at is located in the PageLinkImpl.init() method. + * + * @author Felix Project Team + */ +public class Activator extends DependencyManagerActivator { + /** + * Initialize our components using new DM-lambda activator base. + */ + @Override + public void init(BundleContext ctx, DependencyManager dm) throws Exception { + out.println("type \"log warn\" to see the logs emitted by this test."); + + // System.setProperty("http.proxyHost","your.http.proxy.host"); + // System.setProperty("http.proxyPort", "your.http.proxy.port"); + + // Create the PageLinks service, which asynchronously downloads the content of the Felix web page. + // The PageLink service will be started once the page has been downloaded (using a CompletableFuture). + component(comp -> comp + .factory(() -> new PageLinksImpl("http://felix.apache.org/")) + .provides(PageLinks.class) + .withSvc(LogService.class, log -> log.required().add(PageLinksImpl::bind))); + + // Just wait for the PageLinks service and display all links found from the Felix web site. + component(comp -> comp.impl(this).withSvc(PageLinks.class, page -> page.add(this::setPageLinks))); + } + + /** + * display all the hrefs (links) found from the Felix web site. + */ + void setPageLinks(PageLinks page) { + out.println("Felix site links: " + page.getLinks()); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/future/PageLinks.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/future/PageLinks.java new file mode 100644 index 00000000000..90aebffb64f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/future/PageLinks.java @@ -0,0 +1,28 @@ +/* + * 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.dm.lambda.samples.future; + +import java.util.List; + +/** + * Service that displays all links found from a given web page. + */ +public interface PageLinks { + List getLinks(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/future/PageLinksImpl.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/future/PageLinksImpl.java new file mode 100644 index 00000000000..f00d3b48286 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/future/PageLinksImpl.java @@ -0,0 +1,95 @@ +/* + * 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.dm.lambda.samples.future; + +import static org.apache.felix.dm.lambda.DependencyManagerActivator.component; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.apache.felix.dm.Component; +import org.osgi.service.log.LogService; + +/** + * Provides all hrefs found from a given web page. + */ +public class PageLinksImpl implements PageLinks { + private LogService m_log; + private final static String HREF_PATTERN = "]*)\\s*>"; + private List m_links; // web page hrefs (links). + private String m_url; + + PageLinksImpl(String url) { + m_url = url; + } + + void bind(LogService log) { + m_log = log; + } + + void init(Component c) { + // asynchronously download the content of the URL specified in the constructor. + CompletableFuture> futureLinks = CompletableFuture + .supplyAsync(() -> download(m_url)) + .thenApply(this::parseLinks); + + // Add the future dependency so we'll be started once the CompletableFuture "futureLinks" has completed. + component(c, comp -> comp.withFuture(futureLinks, future -> future.complete(this::setLinks))); + } + + // Called when our future has completed. + void setLinks(List links) { + m_links = links; + } + + // once our future has completed, our component is started. + void start() { + m_log.log(LogService.LOG_WARNING, "Service starting: number of links found from Felix web site: " + m_links.size()); + } + + @Override + public List getLinks() { + return m_links; + } + + public static String download(String url) { + try (BufferedReader buffer = new BufferedReader(new InputStreamReader(new URL(url).openStream()))) { + return buffer.lines().collect(Collectors.joining("\n")); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private List parseLinks(String content) { + Pattern pattern = Pattern.compile(HREF_PATTERN, Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(content); + List result = new ArrayList<>(); + while (matcher.find()) + result.add(matcher.group(1)); + return result; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/future/README b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/future/README new file mode 100644 index 00000000000..a72c0f13445 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/future/README @@ -0,0 +1,16 @@ +The purpose of this sample is to show an example usage of the new "CompletableFuture" dependency that has been +added in the dm-lambda library. CompletableFuture java8 class provides functional operations and promotes an asynchronous event-driven model. + +In such model, you can use the new dm-lambda library to add dependencies on asynchronous events using the standard JDK CompletableFuture class. + +In this example, the Activator first defines a PageLink component that is used to download a given page from the web. The service then parses +the content of the page and returns all available hrefs (links) found from the web page. + +The PageLink is initialized with the Felix web site URL, which is asynchronously downloaded from the PageLink::init method, using a CompletableFuture. +The CF is then added as a "FutureDependency" in the PageLinkImpl.init() method, and when the CF completes, the PageLinkImpl.start() callback is invoked +and the service is registered. + +The Activator is then getting injected with the PageLink service, and displays the links (hrefs) found from the Felix web site. + +Caution: if you are using a corporate http proxy, you have to fix the Activator in order to configure the ip addr and port number of your +http proxy. \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/hello/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/hello/Activator.java new file mode 100644 index 00000000000..56568363d30 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/hello/Activator.java @@ -0,0 +1,44 @@ +/* + * 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.dm.lambda.samples.hello; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.lambda.DependencyManagerActivator; +import org.osgi.framework.BundleContext; +import org.osgi.service.log.LogService; + +/** + * Hello world example. + */ +public class Activator extends DependencyManagerActivator { + + @Override + public void init(BundleContext ctx, DependencyManager dm) throws Exception { + // Declare the Hello service. + component(comp -> comp + .impl(HelloServiceImpl.class).provides(HelloService.class)); + + // Declare an HelloClient component depending on the HelloService, and on LogService + component(comp -> comp + .impl(HelloClient.class) + .withSvc(HelloService.class, svc -> svc.required().add(HelloClient::bind)) + .withSvc(LogService.class, svc -> svc.optional())); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/hello/HelloClient.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/hello/HelloClient.java new file mode 100644 index 00000000000..6cfc2edeff4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/hello/HelloClient.java @@ -0,0 +1,59 @@ +/* + * 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.dm.lambda.samples.hello; + +import org.osgi.service.log.LogService; + +/** + * Our HelloClient, which has a required dependency on the HelloService service, as well as an optional + * dependency on the OSGi LogService. The LogService is injected on compatible class fields and a NullObject + * will be injected in case there is no LogService currently available. + */ +public class HelloClient { + + /** + * Injected by Dependency Manager, possibly as a null object in case the dependency is unavailable. + */ + volatile LogService m_log; + + /** + * Injected by Dependency Manager. + * @param hello the required HelloService instance + */ + void bind(HelloService hello) { + m_log.log(LogService.LOG_DEBUG, "bound hello service."); + System.out.println("HelloClient bound with HelloService: saying hello: " + hello.sayHello()); + } + + /** + * Our component is starting, it has been injected with the required HelloService, as well as the LogService, + * which is injected in the m_log class field (possibly with a NullObject in case the LogService is unavailable). + */ + void start() { + System.out.println("HelloClient started"); + } + + /** + * Our component is stopping, it has possibly lost the required HelloService, or our bundle is being stopped. + */ + void stop() { + System.out.println("HelloClient stopped"); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/hello/HelloService.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/hello/HelloService.java new file mode 100644 index 00000000000..27c6e4f4968 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/hello/HelloService.java @@ -0,0 +1,23 @@ +/* + * 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.dm.lambda.samples.hello; + +public interface HelloService { + String sayHello(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/hello/HelloServiceImpl.java b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/hello/HelloServiceImpl.java new file mode 100644 index 00000000000..74cb8a2d0b0 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/hello/HelloServiceImpl.java @@ -0,0 +1,37 @@ +/* + * 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.dm.lambda.samples.hello; + +/** + * The implementation for our HelloService interface. + */ +public class HelloServiceImpl implements HelloService { + + /** + * Our component is starting. + */ + void start() { + System.out.println("Hello service starting"); + } + + @Override + public String sayHello() { + return "hello !"; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/hello/README b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/hello/README new file mode 100644 index 00000000000..185e7cb2b73 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/src/org/apache/felix/dm/lambda/samples/hello/README @@ -0,0 +1,28 @@ +/* + * 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. + */ + +This sample provides a DM Activator declaring an "HelloService" on which an HelloClient component is depending on. +The HelloClient component also optionally depends on the LogService, which is injected as a NullObject in case +no LogService is currently available. + +Run the hello.bndrun and you should see the following output: + +Hello service starting +HelloClient bound with HelloService. +hello ! + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/test/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.lambda.samples/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/.classpath b/dependencymanager/org.apache.felix.dependencymanager.lambda/.classpath new file mode 100644 index 00000000000..7a6fc254361 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.lambda/.gitignore new file mode 100644 index 00000000000..90dde36e4ac --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/.gitignore @@ -0,0 +1,3 @@ +/bin/ +/bin_test/ +/generated/ diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/.project b/dependencymanager/org.apache.felix.dependencymanager.lambda/.project new file mode 100644 index 00000000000..fcd766fcc5b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/.project @@ -0,0 +1,23 @@ + + + org.apache.felix.dependencymanager.lambda + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/bnd.bnd b/dependencymanager/org.apache.felix.dependencymanager.lambda/bnd.bnd new file mode 100644 index 00000000000..13668d68720 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/bnd.bnd @@ -0,0 +1,31 @@ +# +# 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. +# +javac.source: 1.8 +javac.target: 1.8 +Bundle-Version: 1.2.1 +-buildpath: \ + org.apache.felix.dependencymanager;version=latest,\ + osgi.core;version=6.0,\ + osgi.cmpn;version=6.0,\ + osgi.annotation;version=6.0.1 +Export-Package: \ + org.apache.felix.dm.lambda,\ + org.apache.felix.dm.lambda.callbacks +-runfw: org.apache.felix.framework;version='[4.4.1,4.4.1]' +-runee: JavaSE-1.8 +Private-Package: org.apache.felix.dm.lambda.impl +Include-Resource: META-INF/=resources/,META-INF/changelog.txt=changelog.txt diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/changelog.txt b/dependencymanager/org.apache.felix.dependencymanager.lambda/changelog.txt new file mode 100644 index 00000000000..f1c45b1da4c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/changelog.txt @@ -0,0 +1,265 @@ +Release Notes - Felix - Version org.apache.felix.dependencymanager-r15 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.6.0 + * org.apache.felix.dependencymanager.shell; version=4.0.8 + * org.apache.felix.dependencymanager.runtime; version=4.0.7 + * org.apache.felix.dependencymanager.annotation; version=5.0.1 + * org.apache.felix.dependencymanager.lambda; version=1.2.1 + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r13 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.5.0 + * org.apache.felix.dependencymanager.shell; version=4.0.7 + * org.apache.felix.dependencymanager.runtime; version=4.0.6 + * org.apache.felix.dependencymanager.annotation; version=5.0.0 + * org.apache.felix.dependencymanager.lambda; version=1.2.0 + + +** Bug + * [FELIX-5683] - getServiceProperties returns null instead of empty dictionary + * [FELIX-5716] - Dead Lock in DM + * [FELIX-5768] - DM Lambda stop callback not being called + * [FELIX-5955] - Move changelog.txt to toplevel project dir + * [FELIX-5956] - NPE when invoking a lifecycle runnable method from init method + +** New Feature + * [FELIX-5336] - Add support for prototype scope services in DM4 + + +** Improvement + * [FELIX-5967] - DM does not support java9+ + * [FELIX-5937] - Refactor DM bndtools/gradle project + * [FELIX-5939] - DM annotations enhancements + * [FELIX-5941] - DM APi enhancements + * [FELIX-5957] - Check if a default implementation is used only on optional dependencies + + +** Task + * [FELIX-5960] - Do not supply MD5 checksum + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r11 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.4.1 + * org.apache.felix.dependencymanager.shell; version=4.0.6 + * org.apache.felix.dependencymanager.runtime; version=4.0.5 + * org.apache.felix.dependencymanager.annotation; version=4.2.1 + * org.apache.felix.dependencymanager.lambda; version=1.1.1 + +** Bug + * [FELIX-5630] - NullObject is created for a required dependency if the component is removed and added again to the dependency manager + * [FELIX-5636] - Component of aspect service does not have any service properties anymore + * [FELIX-5657] - DM released sources can't be rebuilt + +** Improvement + * [FELIX-5619] - MultiProperyFilterIndex memory consumption + * [FELIX-5623] - Improve performance of ComponentImpl.getName method + * [FELIX-5650] - Support latest version of Gogo + * [FELIX-5653] - Simplify DM-Lambda samples + * [FELIX-5658] - Include poms in dm artifacts + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r9 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.4.0 + * org.apache.felix.dependencymanager.shell; version=4.0.5 + * org.apache.felix.dependencymanager.runtime; version=4.0.4 + * org.apache.felix.dependencymanager.annotation; version=4.2.0 + * org.apache.felix.dependencymanager.lambda; version=1.1.0 + +** Bug + * [FELIX-5236] - Single @Property annotation on a type doesn't work + * [FELIX-5242] - Configuration updates may be missed when the component is restarting + * [FELIX-5244] - Can't inject service using a method ref on a parent class method. + * [FELIX-5245] - Typo in error logged when a component callback is not found. + * [FELIX-5268] - Service not unregistered while bundle is starting + * [FELIX-5273] - Wrong log when a callback is not found from component instance(s) + * [FELIX-5274] - remove callback fails after manually removing dynamic dependencies + * [FELIX-5399] - Unable to define default map or list config types + * [FELIX-5400] - Can't override default configuration type list value using an empty list + * [FELIX-5401] - Can't override default configuration type map value using an empty map + * [FELIX-5402] - Factory configuration adapter ignores factory method + * [FELIX-5411] - When you stop a component, the service references are not ungotten. + * [FELIX-5426] - Remove callbacks aren't called for optional dependencies in a "circular" dependency scenario + * [FELIX-5428] - Dependency events set not cleared when component is removed + * [FELIX-5429] - Aspect swap callback sometimes not called on optional dependencies + * [FELIX-5469] - Methodcache system size property is not used + * [FELIX-5471] - Ensure that unbound services are always handled synchronously + * [FELIX-5517] - @Inject annotation ignored when applied on ServiceRegistration + * [FELIX-5519] - services are not ungotten when swapped by an aspect + * [FELIX-5523] - required dependencies added to a started adapter (or aspect) are not injected + + + + +** Improvement + * [FELIX-5228] - Upgrade DM With latest release of BndTools + * [FELIX-5237] - Configurable invocation handler should use default method values + * [FELIX-5346] - Start annotation not propagated to sub classes + * [FELIX-5355] - Allow to use properties having dots with configuration proxies + * [FELIX-5403] - Improve the Javadoc for org.apache.felix.dm.ComponentStateListener + * [FELIX-5405] - Do not have org.apache.felix.dm.Logger invoke toString() of message parameters when enabled log level is not high enough + * [FELIX-5406] - DM lambda fluent service properties don't support dots + * [FELIX-5407] - DM annotation plugin generates temp log files even if logging is disabled + * [FELIX-5408] - Parallel DM should not stop components asynchronously + * [FELIX-5467] - MultiPropertyFilterIndex is unusable when a service reference contains a lot of values for one key + * [FELIX-5499] - Remove usage of json.org from dependency manager + * [FELIX-5515] - Upgrade DM to OSGi R6 API + * [FELIX-5516] - Allow to not dereference services internally + * [FELIX-5518] - Remove all eclipse warnings in DM code + * [FELIX-5520] - ComponentStateListener not supported in DM lambda + * [FELIX-5521] - add more callback method signature in DM lambda service dependency callbacks + * [FELIX-5522] - Refactor aspect service implementation + * [FELIX-5524] - add more signatures for aspect swap callbacks + * [FELIX-5526] - Allow to use generic custom DM dependencies when using dm lambda. + * [FELIX-5531] - Document dependency callback signatures + * [FELIX-5532] - Swap callback is missing in @ServiceDependency annotation + + + +** Task + * [FELIX-5533] - Fix a semantic versioning issue when releasing dependency manager + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r8 +====================================================================== + +** Bug + * [FELIX-5146] - Service adapters turn on autoconf even if callbacks are used + * [FELIX-5147] - Bundle Adapter auto configures class fields even if callbacks are used + * [FELIX-5153] - DM4 calls stop before ungetService() on ServiceFactory components + * [FELIX-5155] - Adapter/Aspect extra service dependencies injected twice if using callback instance + * [FELIX-5178] - Make some component parameters as volatile + * [FELIX-5181] - Only log info/debug if dm annotation log parameter is enabled + * [FELIX-5187] - No errog log when configuration dependency callback is not found + * [FELIX-5188] - No error log when a factory pid adapter update callback is not found + * [FELIX-5192] - ConfigurationDependency race condition when component is stopped + * [FELIX-5193] - Factory Pid Adapter race condition when component is stopped + * [FELIX-5200] - Factory configuration adapter not restarted + + +** New Feature + * [FELIX-4689] - Create a more fluent syntax for the dependency manager builder + + +** Improvement + * [FELIX-5126] - Build DM using Java 8 + * [FELIX-5164] - Add support for callback instance in Aspects + * [FELIX-5177] - Support injecting configuration proxies + * [FELIX-5180] - Support for Java8 Repeatable Properties in DM annotations. + * [FELIX-5182] - Cleanup DM samples + * [FELIX-5201] - Improve how components are displayed with gogo shell + + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r6 +====================================================================== + +** Bug + * [FELIX-4974] - DM filter indices not enabled if the dependencymanager bundle is started first + * [FELIX-5045] - DM Optional callbacks may sometimes be invoked before start callback + * [FELIX-5046] - Gradle wrapper is not included in DM source release + + + + +** Improvement + * [FELIX-4921] - Ensure binary equality of the same bundle between successive DM releases + * [FELIX-4922] - Simplify DM changelog management + * [FELIX-5054] - Clean-up instance bound dependencies when component is destroyed + * [FELIX-5055] - Upgrade DM to BndTools 3.0.0 + * [FELIX-5104] - Call a conf dependency callback Instance with an instantiated component + * [FELIX-5113] - Remove useless wrong test in ConfigurationDependencyImpl + * [FELIX-5114] - Schedule configuration update in Component executor synchronously + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r5: +====================================================================== + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r5 + + + +** Bug + * [FELIX-4907] - ConfigurationDependency calls updated(null) when component is stopped. + * [FELIX-4910] - ComponentExecutorFactory does not allow to return null from getExecutorFor method. + * [FELIX-4913] - DM Optional callbacks may sometimes be invoked twice + + + + +** Improvement + * [FELIX-4876] - DM Annotations bnd plugin compatibility with Bndtools 2.4.1 / 3.0.0 versions + * [FELIX-4877] - DM Annotations should detect service type using more method signatures. + * [FELIX-4915] - Skip unecessary manifest headers in DM bnd file + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r3: +===================================================================== + +** Bug + * [FELIX-4858] - DependencyManager: missing createCopy method in timed service dependency + * [FELIX-4869] - Callbacks not invoked for dependencies that are added after the component is initialized + + + + +** Improvement + * [FELIX-4614] - Factory create() method should have access to the component definition + * [FELIX-4873] - Enhance DM API to get missing and circular dependencies + * [FELIX-4878] - Support more signatures for Dependency callbacks + * [FELIX-4880] - Missing callback instance support for some adapters + * [FELIX-4889] - Refactor dm shell command to use the org.apache.dm.diagnostics api + + +** Wish + * [FELIX-4875] - Update DM integration test with latest ConfigAdmin + + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r2: +===================================================================== + +** Bug + * [FELIX-4832] - ClassCastException with autoconfig Iterable fields + * [FELIX-4833] - Revisit some javadocs in the DM annotations. + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r1: +====================================================================== + +** Bug + * [FELIX-4304] - DependencyManager ComponentImpl should not assume all service properties are stored in a Hashtable + * [FELIX-4394] - Race problems in DependencyManager Configuration Dependency + * [FELIX-4588] - createCopy method ConfigurationDependency produces a malfunctioning clone + * [FELIX-4594] - Propagation from dependencies overwrites service properties + * [FELIX-4598] - BundleDependency can effectively track only one bundle + * [FELIX-4602] - TemporalServiceDependency does not properly propagate RuntimeExceptions + * [FELIX-4709] - Incorrect Named Dependencies are binded to the Service Instance + + +** New Feature + * [FELIX-4426] - Allow DM to manage collections of services + * [FELIX-4807] - New thread model for Dependency Manager + + +** Improvement + * [FELIX-3914] - Log unsuccessful field injections + * [FELIX-4158] - ComponentDeclaration should give access to component information + * [FELIX-4667] - "top" command for the Dependency Manager Shell + * [FELIX-4672] - Allow callbacks to third party instance for adapters + * [FELIX-4673] - Log any error thrown when trying to create a null object. + * [FELIX-4777] - Dynamic initialization time configuration of @ConfigurationDependency + * [FELIX-4805] - Deprecate DM annotation metatypes + + +** Wish + * [FELIX-2706] - Support callback delegation for Configuration Dependecies + * [FELIX-4600] - Cherrypicking of propagated properties + * [FELIX-4676] - Add Provide-Capability for DependencyManager Runtime bundle + * [FELIX-4680] - Add more DM ServiceDependency callback signatures + * [FELIX-4683] - Allow to configure the DependencyManager shell scope + * [FELIX-4684] - Replace DependencyManager Runtime "factorySet" by a cleaner API + * [FELIX-4816] - bndtools-ify Dependency Manager + * [FELIX-4818] - New release process for Dependency Manager + diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/resources/DEPENDENCIES b/dependencymanager/org.apache.felix.dependencymanager.lambda/resources/DEPENDENCIES new file mode 100644 index 00000000000..9126cc88543 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/resources/DEPENDENCIES @@ -0,0 +1,21 @@ +Apache Felix Dependency Manager Lambda +Copyright 2011-2017 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +n/a + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2006). +Licensed under the Apache License 2.0. + +III. Overall License Summary + +- Apache License 2.0 diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/resources/LICENSE b/dependencymanager/org.apache.felix.dependencymanager.lambda/resources/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/resources/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/resources/NOTICE b/dependencymanager/org.apache.felix.dependencymanager.lambda/resources/NOTICE new file mode 100644 index 00000000000..092e1581ae7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/resources/NOTICE @@ -0,0 +1,12 @@ +Apache Felix Dependency Manager Lambda +Copyright 2011-2017 The Apache Software Foundation + + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2016). +Licensed under the Apache License 2.0. diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/BundleAdapterBuilder.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/BundleAdapterBuilder.java new file mode 100644 index 00000000000..17f26b3d047 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/BundleAdapterBuilder.java @@ -0,0 +1,298 @@ +/* + * 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.dm.lambda; + +import org.apache.felix.dm.lambda.callbacks.CbBundle; +import org.apache.felix.dm.lambda.callbacks.CbBundleComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbBundle; +import org.apache.felix.dm.lambda.callbacks.InstanceCbBundleComponent; + +/** + * Builds a Dependency Manager bundle adapter.

      The adapter created by this builder will be applied to any bundle that matches the specified + * bundle state mask and filter condition. For each matching bundle an adapter service will be created based on the adapter implementation class. + * The adapter will be registered with the specified interface and existing properties from the original bundle plus any extra properties + * you supply here. The bundle is injected by reflection in adapter class fields having a Bundle type, or using a callback method that you can + * specify. + * + * You can specify reflection based (using method names), or java8 method references for callbacks. + * + *

      Example which creates a BundleAdapter service for each started bundle (the bundle is added by reflection on + * a class field that has a "Bundle" type): + * + *

       {@code
      + * public class Activator extends DependencyManagerActivator {
      + *     public void init(BundleContext ctx, DependencyManager dm) throws Exception { 
      + *       bundleAdapter(adapt -> adapt
      + *           .impl(BundleAdapterImpl.class)
      + *           .provides(BundleAdapter.class)
      + *           .mask(Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE));
      + *    }
      + * }
      + * } 
      + * + * Example that creates a BundleAdapter service for each started bundle (the bundle is added using a method reference): + * + *
       {@code
      + * public class Activator extends DependencyManagerActivator {
      + *     public void init(BundleContext ctx, DependencyManager dm) throws Exception { 
      + *       bundleAdapter(adapt -> adapt
      + *           .impl(BundleAdapterImpl.class)
      + *           .provides(BundleAdapter.class)
      + *           .mask(Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE)
      + *           .add(BundleAdapterImpl::setBundle));
      + *    }
      + * }
      + * }
      + * + * Example that creates a BundleAdapter service for each started bundle (the bundle is added using a method name): + * + *
       {@code
      + * public class Activator extends DependencyManagerActivator {
      + *     public void init(BundleContext ctx, DependencyManager dm) throws Exception { 
      + *       bundleAdapter(adapt -> adapt
      + *           .impl(BundleAdapterImpl.class)
      + *           .provides(BundleAdapter.class)
      + *           .mask(Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE)
      + *           .add("setBundle"));
      + *    }
      + * }
      + * }
      + * + * @author Felix Project Team + */ +public interface BundleAdapterBuilder extends ComponentBuilder { + /** + * Sets the bundle state mask to depend on. The OSGi BundleTracker explains this mask in more detail, but + * it is basically a mask with flags for each potential state a bundle can be in. + * + * @param mask the mask to use + * @return this builder + */ + BundleAdapterBuilder mask(int mask); + + /** + * Sets the filter condition to depend on. Filters are matched against the full manifest of a bundle. + * + * @param filter the filter condition + * @return this builder + */ + BundleAdapterBuilder filter(String filter); + + /** + * Sets property propagation. If set to true any bundle manifest properties will be added + * to the service properties of the component that has this dependency (if it registers as a service). + * + * @param propagate true to propagate the bundle manifest properties + * @return this builder + */ + BundleAdapterBuilder propagate(boolean propagate); + + /** + * Enables property propagation. Any bundle manifest properties will be added + * to the service properties of the component that has this dependency (if it registers as a service). + * + * @return this builder + */ + BundleAdapterBuilder propagate(); + + /** + * Sets a "add" callback name invoked on the component implementation instance(s). + * The callback can be used as hooks whenever the dependency is added. When you specify a callback, + * the auto configuration feature is automatically turned off, because we're assuming you don't need it in this case. + * + * @param callback the method to call when a bundle was added + * + * The following method signature are supported: + *
      {@code
      +     * callback(Bundle b)
      +     * callback(Component c, Bundle b)
      +     * }
      + * + * @param callback the callback name + * @return this builder. + */ + BundleAdapterBuilder add(String callback); + + /** + * Sets a "change" callback name invoked on the component implementation instance(s). + * The callback can be used as hooks whenever the dependency is changed. When you specify a callback, + * the auto configuration feature is automatically turned off, because we're assuming you don't need it in this case. + * + * @param callback the method to call when a bundle was changed + * + * The following method signature are supported: + *
      {@code
      +     * callback(Bundle b)
      +     * callback(Component c, Bundle b)
      +     * }
      + * + * @param callback the callback name + * @return this builder. + */ + BundleAdapterBuilder change(String callback); + + /** + * Sets a "remove" callback name invoked on the component implementation instance(s). + * The callback can be used as hooks whenever the dependency is removed. When you specify a callback, + * the auto configuration feature is automatically turned off, because we're assuming you don't need it in this case. + * + * @param callback the method to call when a bundle was removed + * + * The following method signature are supported: + *
      {@code
      +      * callback(Bundle b)
      +      * callback(Component c, Bundle b)
      +      * }
      + * + * @param callback the callback name + * @return this builder. + */ + BundleAdapterBuilder remove(String callback); + + /** + * Sets a callback instance to use when invoking reflection based callbacks. + * + * @param callbackInstance the instance to call the reflection based callbacks on + * @return this builder. + * @see #add(String) + * @see #change(String) + * @see #remove(String) + */ + BundleAdapterBuilder callbackInstance(Object callbackInstance); + + /** + * Sets a reference to a callback method invoked on one of the component implementation classes. + * The method reference must point to a Component implementation class method, it is called when the bundle is added + * and takes as argument a Bundle. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param add the method reference invoked when a bundle is added. + * @return this builder + */ + BundleAdapterBuilder add(CbBundle add); + + /** + * Sets a reference to a callback method invoked on one of the component implementation classes. + * The method reference must point to a Component implementation class method, it is called when the bundle is changed + * and takes as argument a Bundle. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param change the method reference invoked when a bundle has changed. + * @return this builder + */ + BundleAdapterBuilder change(CbBundle change); + + /** + * Sets a reference to a callback method invoked on one of the component implementation classes. + * The method reference must point to a Component implementation class method, it is called when the bundle is removed + * and takes as argument a Bundle. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param remove the method reference invoked when a bundle is removed. + * @return this builder + */ + BundleAdapterBuilder remove(CbBundle remove); + + /** + * Sets a reference to a callback method invoked on one of the component implementation classes. + * The method reference must point to a Component implementation class method, it is called when the bundle is added + * and takes as argument a Bundle and a Component. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param add the method reference invoked when a bundle is added. + * @return this builder + */ + BundleAdapterBuilder add(CbBundleComponent add); + + /** + * Sets a reference to a callback method invoked on one of the component implementation classes. + * The method reference must point to a Component implementation class method, it is called when the bundle is changed + * and takes as argument a Bundle and a Component. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param change the method reference invoked when a bundle has changed. + * @return this builder + */ + BundleAdapterBuilder change(CbBundleComponent change); + + /** + * Sets a reference to a callback method invoked on one of the component implementation classes. + * The method reference must point to a Component implementation class method, it is called when the bundle is removed + * and takes as argument a Bundle and a Component. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param remove the method reference invoked when a bundle is removed. + * @return this builder + */ + BundleAdapterBuilder remove(CbBundleComponent remove); + + /** + * Sets a reference to a callback method invoked on a given Object instance. + * The method reference is invoked when the bundle is added and takes as argument a Bundle. + * + * @param add the method reference invoked when a bundle is added. + * @return this builder + */ + BundleAdapterBuilder add(InstanceCbBundle add); + + /** + * Sets a reference to a callback method invoked on a given Object instance. + * The method reference is invoked when the bundle has changed and takes as argument a Bundle. + * + * @param change the method reference invoked when a bundle has changed. + * @return this builder + */ + BundleAdapterBuilder change(InstanceCbBundle change); + + /** + * Sets a reference to a callback method invoked on a given Object instance. + * The method reference is invoked when the bundle is removed and takes as argument a Bundle. + * + * @param remove the method reference invoked when a bundle is removed. + * @return this builder + */ + BundleAdapterBuilder remove(InstanceCbBundle remove); + + /** + * Sets a reference to a callback method invoked on a given Object instance. + * The method reference is invoked when the bundle is added and takes as argument a Bundle and a Component. + * + * @param add the method reference invoked when a bundle is added. + * @return this builder + */ + BundleAdapterBuilder add(InstanceCbBundleComponent add); + + /** + * Sets a reference to a callback method invoked on a given Object instance. + * The method reference is invoked when the bundle has changed and takes as argument a Bundle and a Component. + * + * @param change the method reference invoked when a bundle has changed. + * @return this builder + */ + BundleAdapterBuilder change(InstanceCbBundleComponent change); + + /** + * Sets a reference to a callback method invoked on a given Object instance. + * The method reference is invoked when the bundle is removed and takes as argument a Bundle and a Component. + * + * @param remove the method reference invoked when a bundle is removed. + * @return this builder + */ + BundleAdapterBuilder remove(InstanceCbBundleComponent remove); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/BundleDependencyBuilder.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/BundleDependencyBuilder.java new file mode 100644 index 00000000000..f589f821ddb --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/BundleDependencyBuilder.java @@ -0,0 +1,315 @@ +/* + * 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.dm.lambda; + +import java.util.Dictionary; +import java.util.function.Function; + +import org.apache.felix.dm.BundleDependency; +import org.apache.felix.dm.lambda.callbacks.CbBundle; +import org.apache.felix.dm.lambda.callbacks.CbBundleComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbBundle; +import org.apache.felix.dm.lambda.callbacks.InstanceCbBundleComponent; +import org.osgi.framework.Bundle; + +/** + * Builds a Dependency Manager Bundle Dependency. + * + *

      Example of a Pojo Component which tracks a started bundle having a given bundle symbolic name: + * + *

       {@code
      + * public class Activator extends DependencyManagerActivator {
      + *     public void init(BundleContext ctx, DependencyManager dm) throws Exception { 
      + *         String BSN = "org.apache.felix.dependencymanager";
      + *         component(comp -> comp
      + *             .impl(Pojo.class)
      + *             .withBundle(b -> b.mask(Bundle.ACTIVE).filter("(Bundle-SymbolicName=" + BSN + ")").add(Pojo::add).remove(Pojo::remove)));
      + *    }
      + * }
      + * } 
      + * + * @author Felix Project Team + */ +public interface BundleDependencyBuilder extends DependencyBuilder { + /** + * Enables auto configuration for this dependency. This means the component implementation class fields will be + * injected with this bundle dependency automatically. + * + * @param autoConfig true to enable auto configuration + * @return the bundle dependency builder + */ + public BundleDependencyBuilder autoConfig(boolean autoConfig); + + /** + * Enables auto configuration for this dependency. This means the component implementation class fields will be + * injected with this bundle dependency automatically. + * + * @return the bundle dependency builder + */ + public BundleDependencyBuilder autoConfig(); + + /** + * Sets the dependency to be required. + * @param required true if this bundle dependency is required. + * @return the bundle dependency builder + */ + public BundleDependencyBuilder required(boolean required); + + /** + * Sets the dependency to be required. + * + * @return the bundle dependency builder + */ + public BundleDependencyBuilder required(); + + /** + * Sets the dependency to be optional. + * + * @return the bundle dependency builder + */ + public BundleDependencyBuilder optional(); + + /** + * Sets the bundle to depend on directly. + * + * @param bundle the bundle to depend on + * @return the bundle dependency builder + */ + public BundleDependencyBuilder bundle(Bundle bundle); + + /** + * Sets the filter condition to depend on. Filters are matched against the full manifest of a bundle. + * + * @param filter the filter condition + * @return the bundle dependency builder + */ + public BundleDependencyBuilder filter(String filter); + + /** + * Sets the bundle state mask to depend on. The OSGi BundleTracker explains this mask in more detail, but + * it is basically a mask with flags for each potential state a bundle can be in. + * + * @param mask the mask to use + * @return the bundle dependency builder + */ + public BundleDependencyBuilder mask(int mask); + + /** + * Sets property propagation. If set to true any bundle manifest properties will be added + * to the service properties of the component that declares this dependency (if it provides a service). + * + * @param propagate true to propagate the bundle manifest properties + * @return the bundle dependency builder + */ + public BundleDependencyBuilder propagate(boolean propagate); + + /** + * Sets property propagation. any bundle manifest properties will be added + * to the service properties of the component that has this dependency (if it registers as a service). + * + * @return the bundle dependency builder + */ + public BundleDependencyBuilder propagate(); + + /** + * Sets an Object instance and a callback method used to propagate some properties to the provided service properties. + * The method will be invoked on the specified object instance and must have one of the following signatures: + * + *

      • Dictionary callback(Bundle bundle)
      + * + * @param instance the Object instance which is used to retrieve propagated service properties + * @param method the method to invoke for retrieving the properties to be propagated to the service properties. + * @return this service dependency. builder + */ + public BundleDependencyBuilder propagate(Object instance, String method); + + /** + * Sets a reference to a method on an Object instance used to propagate some bundle properties to the provided service properties. + * + * @param propagate a function which accepts a Bundle argument and which returns some properties that will be + * propagated to the provided component service properties. + * @return this service dependency. builder + */ + public BundleDependencyBuilder propagate(Function> propagate); + + /** + * Sets a "add" callback method to invoke on the component implementation instance(s). + * The callback is invoked when the bundle is added, and the following signatures are supported: + * + *

        + *
      1. method(Bundle)
      2. + *
      3. method(Component, Bundle)
      4. + *
      + * + * @param callback the add callback + * @return this builder + */ + BundleDependencyBuilder add(String callback); + + /** + * Sets a "change" callback method to invoke on the component implementation instance(s). + * The callback is invoked when the bundle state has changed, and the following signatures are supported: + * + *

        + *
      1. method(Bundle)
      2. + *
      3. method(Component, Bundle)
      4. + *
      + * + * @param callback the change callback + * @return this builder + */ + BundleDependencyBuilder change(String callback); + + /** + * Sets a "remove" callback method to invoke on the component implementation instance(s). + * The callback is invoked when the bundle is removed, and the following signatures are supported: + *

        + *
      1. method(Bundle)
      2. + *
      3. method(Component, Bundle)
      4. + *
      + * + * @param callback the remove callback + * @return this builder + */ + BundleDependencyBuilder remove(String callback); + + /** + * Specifies a callback instance used to invoke the reflection based callbacks on it. + * @param callbackInstance the instance to invoke the reflection based callbacks on + * @return this builder + * @see #add(String) + * @see #change(String) + * @see #remove(String) + */ + BundleDependencyBuilder callbackInstance(Object callbackInstance); + + /** + * Sets a callback method reference which is invoked when a bundle is added. + * The method reference must point to a Component implementation class method, and takes as argument a Bundle. + * + * @param the type of the component implementation class on which the callback is invoked on. + * @param add the method reference invoked when a bundle is added. + * @return this builder + */ + BundleDependencyBuilder add(CbBundle add); + + /** + * Sets a callback method reference which is invoked when a bundle is changed. + * The method reference must point to a Component implementation class method, and takes as argument a Bundle. + * + * @param the type of the component implementation class on which the callback is invoked on. + * @param change the method reference invoked when a bundle has changed. + * @return this builder + */ + BundleDependencyBuilder change(CbBundle change); + + /** + * Sets a callback method reference which is invoked when a bundle is removed. + * The method reference must point to a Component implementation class method, and takes as argument a Bundle. + * + * @param the type of the component implementation class on which the callback is invoked on. + * @param remove the method reference invoked when a bundle is removed. + * @return this builder + */ + BundleDependencyBuilder remove(CbBundle remove); + + /** + * Sets a callback method reference which is invoked when a bundle is added. + * The method reference must point to a Component implementation class method, and takes as argument a Bundle and a Component. + * + * @param the type of the component implementation class on which the callback is invoked on. + * @param add the method reference invoked when a bundle is added. + * @return this builder + */ + BundleDependencyBuilder add(CbBundleComponent add); + + /** + * Sets a callback method reference which is invoked when a bundle is changed. + * The method reference must point to a Component implementation class method, and takes as argument a Bundle and a Component. + * + * @param the type of the component implementation class on which the callback is invoked on. + * @param change the method reference invoked when a bundle has changed. + * @return this builder + */ + BundleDependencyBuilder change(CbBundleComponent change); + + /** + * Sets a callback method reference which is invoked when a bundle is removed. + * The method reference must point to a Component implementation class method, and takes as argument a Bundle and a Component. + * + * @param the type of the component implementation class on which the callback is invoked on. + * @param remove the method reference invoked when a bundle is removed. + * @return this builder + */ + BundleDependencyBuilder remove(CbBundleComponent remove); + + /** + * Sets a method reference on an Object instance which is invoked when a bundle is added. + * The method reference must point to an Object instance method, and takes as argument a Bundle parameter. + * + * @param add the method reference invoked when a bundle is added. + * @return this builder + */ + BundleDependencyBuilder add(InstanceCbBundle add); + + /** + * Sets a method reference on an Object instance which is invoked when a bundle is changed. + * The method reference must point to an Object instance method, and takes as argument a Bundle parameter. + * + * @param change the method reference invoked when a bundle is changed. + * @return this builder + */ + BundleDependencyBuilder change(InstanceCbBundle change); + + /** + * Sets a method reference on an Object instance which is invoked when a bundle is removed. + * The method reference must point to an Object instance method, and takes as argument a Bundle parameter. + * + * @param remove the method reference invoked when a bundle is removed. + * @return this builder + */ + BundleDependencyBuilder remove(InstanceCbBundle remove); + + /** + * Sets a callback instance method reference which is invoked when a bundle is added. + * The method reference must point to an Object instance method, and takes as arguments a Bundle and a Component. + * + * @param add the method reference invoked when a bundle is added. + * @return this builder + */ + BundleDependencyBuilder add(InstanceCbBundleComponent add); + + /** + * Sets a callback instance method reference which is invoked when a bundle is changed. + * The method reference must point to an Object instance method, and takes as argument a Bundle and a Component. + * + * @param change the method reference invoked when a bundle is changed. + * @return this builder + */ + BundleDependencyBuilder change(InstanceCbBundleComponent change); + + /** + * Sets a callback instance method reference which is invoked when a bundle is removed. + * The method reference must point to an Object instance method, and takes as argument a Bundle and a Component. + * + * @param remove the method reference invoked when a bundle is removed. + * @return this builder + */ + BundleDependencyBuilder remove(InstanceCbBundleComponent remove); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/ComponentBuilder.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/ComponentBuilder.java new file mode 100644 index 00000000000..485dc4b9a6a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/ComponentBuilder.java @@ -0,0 +1,798 @@ +/* + * 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.dm.lambda; + +import java.util.Dictionary; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Component.ServiceScope; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.lambda.callbacks.InstanceCb; +import org.apache.felix.dm.lambda.callbacks.InstanceCbComponent; +import org.osgi.annotation.versioning.ProviderType; + +/** + * Builds a Dependency Manager Component.

      Components are the main building blocks for OSGi applications. + * They can publish themselves as a service, and they can have dependencies. + * These dependencies will influence their life cycle as component will only be activated when all + * required dependencies are available. This interface is also the base interface for extended components like + * aspects, adapters, etc ... + * + *

      Example of a component that depends on a LogServce service. The dependency is injected by reflection + * on fields having a compatible type with the LogService interface: + * + *

      {@code
      + * public class Activator extends DependencyManagerActivator {
      + *   public void init(BundleContext ctx, DependencyManager dm) throws Exception {
      + *       component(comp -> comp.impl(Pojo.class).withSvc(LogService.class));
      + *   }
      + * }
      + * } 
      + * + * @param the type of a builder that may extends this builder interface (aspect/adapter). + * @author Felix Project Team + */ +@ProviderType +public interface ComponentBuilder> { + + /** + * Configures the component scope. + */ + B scope(ServiceScope scope); + + /** + * Configures the component implementation. Can be a class name, or a component implementation object. + * + * @param impl the component implementation (a class, or an Object). + * @return this builder + */ + B impl(Object impl); + + /** + * Sets the factory to use when creating the implementation. You can specify both the factory class and method to invoke. The method should return the implementation, + * and can use any method to create it. Actually, this can be used together with setComposition to create a composition of instances that work together to implement + * a component. The factory itself can also be instantiated lazily by not specifying an instance, but a Class. + * + * @param factory the factory instance, or the factory class. + * @param createMethod the create method called on the factory in order to instantiate the component. + * @return this builder + */ + B factory(Object factory, String createMethod); + + /** + * Configures a factory that can be used to create this component implementation. + * Example: + * + *
       {@code
      +     * factory(ComponentImpl::new)", or "factory(() -> new ComponentImpl())
      +     * }
      + * + * @param create the factory used to create the component implementation. + * @return this builder + */ + B factory(Supplier create); + + /** + * Configures a factory used to create this component implementation using a Factory object and a method in the Factory object. + * Example: + * + *
       {@code
      +     * factory(Factory::new, Factory::create)
      +     * }
      + * + * @param the type of the factory returned by the supplier + * @param the type of the object that is returned by the factory create method. + * @param factory the function used to create the Factory itself + * @param create the method reference on the Factory method that is used to create the Component implementation + * @return this builder + */ + B factory(Supplier factory, Function create); + + /** + * Configures a factory used to create this component implementation using a Factory object and a "getComposition" factory method. + * the Factory method may then return multiple objects that will be part of this component implementation, and + * all of them will be searched when injecting any of the dependencies. + * + * Example: + * + *
       {@code
      +     * CompositionManager mngr = new CompositionManager();
      +     * ...
      +     * factory(mngr::create, mngr::getComposition)
      +     * }
      + * + * @param factory the supplier used to return the main component implementation instance + * @param getComposition the supplier that returns the list of instances that are part of the component implementation classes. + * @return this builder + */ + B factory(Supplier factory, Supplier getComposition); + + /** + * Configures a factory that also returns a composition of objects for this component implemenation. + * + * Example: + * + *
       {@code
      +     * factory(CompositionManager::new, CompositionManager::create, CompositionManager::getComposition).
      +     * }
      + * + * Here, the CompositionManager will act as a factory (the create method will return the component implementation object), the + * CompositionManager.getComposition() method will return all the objects that are also part of the component implementation, + * and all of them will be searched for injecting any of the dependencies. + * + * @param the type of the object returned by the supplier factory + * @param factory the function used to create the Factory itself + * @param create the Factory method used to create the main component implementation object + * @param getComposition the Factory method used to return the list of objects that are also part of the component implementation. + * @return this builder + */ + B factory(Supplier factory, Function create, Function getComposition); + + /** + * Sets the public interface under which this component should be registered in the OSGi service registry. + * + * @param iface the public interface to register in the OSGI service registry. + * @return this builder + */ + B provides(Class iface); + + /** + * Sets the public interface under which this component should be registered in the OSGi service registry. + * + * @param iface the public interface to register in the OSGI service registry. + * @param name a property name for the provided service + * @param value a property value for the provided service + * @param rest the rest of property name/value pairs. + * @return this builder. + */ + B provides(Class iface, String name, Object value, Object ... rest); + + /** + * Sets the public interface under which this component should be registered in the OSGi service registry. + * Warning: you can only use this method if you compile your application using the "-parameters" javac option. + * + * code example: + * + *
       {@code
      +     *  provides(MyService.class, property1 -> "value1", property2 -> 123);
      +     * }
      + * + * @param iface the public interface to register in the OSGI service registry. + * @param properties a list of fluent service properties for the provided service. You can specify a list of lambda expression, each one implementing the + * {@link FluentProperty} interface that allows to define a property name using a lambda parameter. + * @return this builder. + * @deprecated Fluent properties are only supported using java8 and this method will be removed in next DM release + */ + B provides(Class iface, FluentProperty ... properties); + + /** + * Sets the public interface under which this component should be registered in the OSGi service registry. + * @param iface the public interface to register in the OSGI service registry. + * @param properties the properties for the provided service + * @return this builder. + */ + B provides(Class iface, Dictionary properties); + + /** + * Sets the public interfaces under which this component should be registered in the OSGi service registry. + * + * @param ifaces list of services provided by the component. + * @return this builder. + */ + B provides(Class[] ifaces); + + /** + * Sets the public interfaces under which this component should be registered in the OSGi service registry. + * + * @param ifaces the public interfaces to register in the OSGI service registry. + * @param name a property name for the provided service + * @param value a property value for the provided service + * @param rest the rest of property name/value pairs. + * @return this builder. + */ + B provides(Class[] ifaces, String name, Object value, Object ... rest); + + /** + * Sets the public interfaces under which this component should be registered in the OSGi service registry. + * Warning: you can only use this method if you compile your application using the "-parameters" javac option. + * code example: + * + *
       {@code
      +     *    provides(new Class[] { MyService.class, MyService2.class }, property1 -> "value1", property2 -> 123);
      +     * }
      + * + * @param ifaces the public interfaces to register in the OSGI service registry. + * @param properties a list of fluent service properties for the provided service. You can specify a list of lambda expression, each one implementing the + * {@link FluentProperty} interface that allows to define a property name using a lambda parameter. + * @return this builder. + * @deprecated Fluent properties are only supported using java8 and this method will be removed in next DM release + */ + B provides(Class[] ifaces, FluentProperty ... properties); + + /** + * Sets the public interfaces under which this component should be registered in the OSGi service registry. + * + * @param ifaces the public interfaces to register in the OSGI service registry. + * @param properties the properties for the provided service + * @return this builder. + */ + B provides(Class[] ifaces, Dictionary properties); + + /** + * Sets the public interface under which this component should be registered in the OSGi service registry. + * + * @param iface the service provided by this component. + * @return this builder. + */ + B provides(String iface); + + /** + * Sets the public interface under which this component should be registered in the OSGi service registry. + * + * @param iface the public interface to register in the OSGI service registry. + * @param name a property name for the provided service + * @param value a property value for the provided service + * @param rest the rest of property name/value pairs. + * @return this builder. + */ + B provides(String iface, String name, Object value, Object ... rest); + + /** + * Sets the public interface under which this component should be registered in the OSGi service registry. + * Warning: you can only use this method if you compile your application using the "-parameters" javac option. + * code example: + * + *
       {@code 
      +     * provides(MyService.class, property1 -> "value1", property2 -> 123);
      +     * }
      + * + * @param iface the public interface to register in the OSGI service registry. + * @param properties a list of fluent service properties for the provided service. You can specify a list of lambda expression, each one implementing the + * {@link FluentProperty} interface that allows to define a property name using a lambda parameter. + * @return this builder. + * @deprecated Fluent properties are only supported using java8 and this method will be removed in next DM release + */ + B provides(String iface, FluentProperty ... properties); + + /** + * Sets the public interface under which this component should be registered in the OSGi service registry. + * @param iface the public interface to register in the OSGI service registry. + * @param properties the properties for the provided service + * @return this builder. + */ + B provides(String iface, Dictionary properties); + + /** + * Sets the public interfaces under which this component should be registered in the OSGi service registry. + * + * @param ifaces the list of services provided by the component. + * @return this builder. + */ + B provides(String[] ifaces); + + /** + * Sets the public interfaces under which this component should be registered in the OSGi service registry. + * + * @param ifaces the public interfaces to register in the OSGI service registry. + * @param name a property name for the provided service + * @param value a property value for the provided service + * @param rest the rest of property name/value pairs. + * @return this builder. + */ + B provides(String[] ifaces, String name, Object value, Object ... rest); + + /** + * Sets the public interfaces under which this component should be registered in the OSGi service registry. + * Warning: you can only use this method if you compile your application using the "-parameters" javac option. + * + * code example: + *
       {@code 
      +     * provides(new Class[] { MyService.class, MyService2.class }, property1 -> "value1", property2 -> 123);
      +     * }
      + * + * @param ifaces the public interfaces to register in the OSGI service registry. + * @param properties a list of fluent service properties for the provided service. You can specify a list of lambda expression, each one implementing the + * {@link FluentProperty} interface that allows to define a property name using a lambda parameter. + * @return this builder. + * @deprecated Fluent properties are only supported using java8 and this method will be removed in next DM release + */ + B provides(String[] ifaces, FluentProperty ... properties); + + /** + * Sets the public interfaces under which this component should be registered in the OSGi service registry. + * + * @param ifaces the public interfaces to register in the OSGI service registry. + * @param properties the properties for the provided service + * @return this builder. + */ + B provides(String[] ifaces, Dictionary properties); + + /** + * Sets the component's service properties + * @param properties the component's service properties + * @return this builder + */ + B properties(Dictionary properties); + + /** + * Sets the components's service properties using varargs. The number of parameters must be even, representing a list of pair property key-value. + * + *
       {@code 
      +     * Example: properties("param1", "value1", "service.ranking", 3)
      +     * }
      + * + * @param name the first property name + * @param value the first property value + * @param rest the rest of properties key/value pairs. + * @return this builder + */ + B properties(String name, Object value, Object ... rest); + + /** + * Sets the components's service properties using List of lamda properties. + * + * Example: + * + *
       {@code
      +     *   properties(param1 -> "value1, param2 -> 2);
      +     * }
      + * + * When you use this method, you must compile your source code using the "-parameters" option, and the "arg0" parameter + * name is now allowed. + * + * @param properties the fluent properties + * @return this builder + * @deprecated Fluent properties are only supported using java8 and this method will be removed in next DM release + */ + B properties(FluentProperty ... properties); + + /** + * Adds a service dependency built using a Consumer lambda that is provided with a ServiceDependencyBuilder. + * + * @param the type of the dependency service + * @param service the service + * @param consumer the lambda used to build the service dependency + * @return this builder. + */ + B withSvc(Class service, Consumer> consumer); + + /** + * Adds in one shot multiple service dependencies injected in compatible class fields. + * + * @param services some dependencies to inject in compatible class fields. + * @return this builder + */ + @SuppressWarnings("unchecked") + default B withSvc(Class ... services) { + Stream.of(services).forEach(s -> withSvc(s, svc -> svc.autoConfig())); + return (B) this; + } + + /** + * Adds in one shot multiple service dependencies injected in compatible class fields. + * + * @param required true if the dependency is required, false if not + * @param services some dependencies to inject in compatible class fields. + * @return this builder + */ + @SuppressWarnings("unchecked") + default B withSvc(boolean required, Class ... services) { + Stream.of(services).forEach(s -> withSvc(s, svc -> svc.required(required))); + return (B) this; + } + + /** + * Adds a service dependency injected in compatible class fields. + * + * @param service a service dependency + * @param required true if the dependency is required, false if not + * @return this builder + */ + @SuppressWarnings("unchecked") + default B withSvc(Class service, boolean required) { + withSvc(service, svc -> svc.required(required)); + return (B) this; + } + + /** + * Adds a service dependency injected in compatible class fields. + * + * @param the service dependency type + * @param service the service dependency. + * @param filter the service filter + * @param required true if the dependency is required, false if not + * @return this builder + */ + default B withSvc(Class service, String filter, boolean required) { + return withSvc(service, svc -> svc.filter(filter).required(required)); + } + + /** + * Adds a service dependency injected in a given compatible class field. + * + * @param the service dependency type + * @param service the service dependency + * @param filter the service filter + * @param field the class field when the dependency has to be injected + * @param required true if the dependency is required, false if not + * @return this builder + */ + default B withSvc(Class service, String filter, String field, boolean required) { + return withSvc(service, svc -> svc.filter(filter).autoConfig(field).required(required)); + } + + /** + * Adds a configuration dependency. + * @param consumer the lambda used to build the configuration dependency. + * @return this builder. + */ + B withCnf(Consumer consumer); + + /** + * Adds multiple configuration dependencies in one single call. All configurations are injected by default in the "updated" callback. + * @param pids list of configuration pids. + * @return this builder + */ + @SuppressWarnings("unchecked") + default B withCnf(String ... pids) { + Stream.of(pids).forEach(pid -> withCnf(cnf -> cnf.pid(pid))); + return (B) this; + } + + /** + * Adds a configuration dependency using a configuration type. The configuration is injected in an updated callback which takes in argument + * an implementation of the specified configuration type. + * + * @param configType the configuration type that will be injected to the "updated" callback + * @return this builder + * @see ConfigurationDependencyBuilder + */ + default B withCnf(Class configType) { + return withCnf(cnf -> cnf.update(configType, "updated")); + } + + /** + * Adds a bundle dependency. + * @param consumer the lambda used to build the bundle dependency. + * @return this builder. + */ + B withBundle(Consumer consumer); + + /** + * Adds a CompletableFuture dependency. + * + * @param the type of the result of the CompletableFuture. + * @param future a CompletableFuture on which the dependency will wait for + * @param consumer the builder used to build the dependency + * @return this builder. + */ + B withFuture(CompletableFuture future, Consumer> consumer); + + /** + * Adds a generic Dependency Manager dependency. You can use this method if you want to add a dependency + * that you have built using the Dependency Manager API, or a specific custom DM dependency (like toggles, etc ...). + */ + B withDep(Dependency dependency); + + /** + * Sets the name of the method used as the "init" callback. This method, when found, is + * invoked as part of the life cycle management of the component implementation. + * This method is useful because when it is invoked, all required dependencies defines in the Activator + * are already injected, and you can then add more extra dependencies from the init() method. + * And once all extra dependencies will be available and injected, then the "start" callback will be invoked. + *

      The dependency manager will look for a method of this name with the following signatures, + * in this order: + *

        + *
      1. method(Component component)
      2. + *
      3. method()
      4. + *
      + * + * @param callback the callback name + * @return this builder. + */ + B init(String callback); + + /** + * Sets a callback instance and the name of the method used as the "init" callback. This method, when found, is + * invoked as part of the life cycle management of the component implementation. + * This method is useful because when it is invoked, all required dependencies defines in the Activator + * are already injected, and you can then add more extra dependencies from the init() method. + * And once all extra dependencies will be available and injected, then the "start" callback will be invoked. + *

      The dependency manager will look for a method of this name with the following signatures, + * in this order: + *

        + *
      1. method(Component component)
      2. + *
      3. method()
      4. + *
      + * + * @param callbackInstance a callback instance object the callback is invoked on + * @param callback the callback name + * @return this builder. + */ + B init(Object callbackInstance, String callback); + + /** + * Sets an Object instance method reference used as the "init" callback. It is invoked as part of the life cycle management of the component + * implementation. + * This method is useful because when it is invoked, all required dependencies defines in the Activator + * are already injected, and you can then add more extra dependencies from the init() method. + * And once all extra dependencies will be available and injected, then the "start" callback will be invoked. + * The method does not take any parameters. + * + * @param callback an Object instance method reference. The method does not take any parameters. + * @return this builder + */ + B init(InstanceCb callback); + + /** + * Sets an Object instance method reference used as the "init" callback. It is invoked as part of the life cycle management of the component + * implementation. + * This method is useful because when it is invoked, all required dependencies defines in the Activator + * are already injected, and you can then add more extra dependencies from the init() method. + * And once all extra dependencies will be available and injected, then the "start" callback will be invoked. + * The method takes as argument a Component parameter. + * + * @param callback an Object instance method reference. The method takes as argument a Component parameter. + * @return this builder + */ + B init(InstanceCbComponent callback); + + /** + * Sets a callback instance and the name of the method used as the "start" callback. This method, when found, is + * invoked as part of the life cycle management of the component implementation.

      The + * dependency manager will look for a method of this name with the following signatures, + * in this order: + *

        + *
      1. method(Component component)
      2. + *
      3. method()
      4. + *
      + * + * @param callback the callback name + * @return this builder. + */ + B start(String callback); + + /** + * Sets the name of the method used as the "start" callback. This method, when found, is + * invoked as part of the life cycle management of the component implementation.

      The + * dependency manager will look for a method of this name with the following signatures, + * in this order: + *

        + *
      1. method(Component component)
      2. + *
      3. method()
      4. + *
      + * + * @param callbackInstance a callback instance object the callback is invoked on + * @param callback the callback name + * @return this builder. + */ + B start(Object callbackInstance, String callback); + + /** + * Sets an Object instance method reference used as the "start" callback. + * This method is invoked as part of the life cycle management of the component implementation. + * The method does not take any parameters. + * + * @param callback an Object instance method reference. The method does not take any parameters. + * @return this builder. + */ + B start(InstanceCb callback); + + /** + * Sets an Object instance method reference used as the "start" callback. + * This method is invoked as part of the life cycle management of the component implementation. + * The method takes as argument a Component parameter. + * + * @param callback an Object instance method reference. The method takes as argument a Component parameter. + * @return this builder. + */ + B start(InstanceCbComponent callback); + + /** + * Sets the name of the method used as the "stop" callback. This method, when found, is + * invoked as part of the life cycle management of the component implementation.

      The + * dependency manager will look for a method of this name with the following signatures, + * in this order: + *

        + *
      1. method(Component component)
      2. + *
      3. method()
      4. + *
      + * + * @param callback the callback name + * @return this builder. + */ + B stop(String callback); + + /** + * Sets a callback instance and the name of the method used as the "stop" callback. This method, when found, is + * invoked as part of the life cycle management of the component implementation.

      The + * dependency manager will look for a method of this name with the following signatures, + * in this order: + *

        + *
      1. method(Component component)
      2. + *
      3. method()
      4. + *
      + * + * @param callbackInstance a callback instance object the callback is invoked on + * @param callback the callback name + * @return this builder. + */ + B stop(Object callbackInstance, String callback); + + /** + * Sets an Object instance method reference used as the "stop" callback. It is invoked as part of the life cycle management of the component + * implementation. + * This method is useful because when it is invoked, all required dependencies defines in the Activator + * are already injected, and you can then add more extra dependencies from the init() method. + * And once all extra dependencies will be available and injected, then the "start" callback will be invoked. + * The method does not take any parameters. + * + * @param callback an Object instance method reference. The method does not take any parameters. + * @return this builder + */ + B stop(InstanceCb callback); + + /** + * Sets an Object instance method reference used as the "stop" callback. + * This method is invoked as part of the life cycle management of the component implementation. + * The method takes as argument a Component parameter. + * + * @param callback an Object instance method reference. The method takes as argument a Component parameter. + * @return this builder. + */ + B stop(InstanceCbComponent callback); + + /** + * Sets the name of the method used as the "destroy" callback. This method, when found, is + * invoked as part of the life cycle management of the component implementation.

      The + * dependency manager will look for a method of this name with the following signatures, + * in this order: + *

        + *
      1. method(Component component)
      2. + *
      3. method()
      4. + *
      + * + * @param callback the callback name + * @return this builder. + */ + B destroy(String callback); + + /** + * Sets a callback instance and the name of the method used as the "destroy" callback. This method, when found, is + * invoked as part of the life cycle management of the component implementation.

      The + * dependency manager will look for a method of this name with the following signatures, + * in this order: + *

        + *
      1. method(Component component)
      2. + *
      3. method()
      4. + *
      + * + * @param callbackInstance a callback instance object the callback is invoked on + * @param callback the callback name + * @return this builder. + */ + B destroy(Object callbackInstance, String callback); + + /** + * Sets an Object instance method reference used as the "destroy" callback. It is invoked as part of the life cycle management of the component + * implementation. + * This method is useful because when it is invoked, all required dependencies defines in the Activator + * are already injected, and you can then add more extra dependencies from the init() method. + * And once all extra dependencies will be available and injected, then the "start" callback will be invoked. + * The method does not take any parameters. + * + * @param callback an Object instance method reference. The method does not take any parameters. + * @return this builder + */ + B destroy(InstanceCb callback); + + /** + * Sets an Object instance method reference used as the "destroy" callback. + * This method is invoked as part of the life cycle management of the component implementation. + * The method takes as argument a Component parameter. + * + * @param callback an Object instance method reference. The method takes as argument a Component parameter. + * @return this builder. + */ + B destroy(InstanceCbComponent callback); + + /** + * Configures OSGi object (BundleContext, Component, etc ...) that will be injected in any field having the same OSGi object type. + * @param clazz the OSGi object type (BundleContext, Component, DependencyManager). + * @param autoConfig true if the OSGi object has to be injected, false if not + * @return this builder + */ + B autoConfig(Class clazz, boolean autoConfig); + + /** + * Configures OSGi object (BundleContext, Component, etc ...) that will be injected in a given field. + * @param clazz the OSGi object type (BundleContext, Component, DependencyManager). + * @param field the field that will be injected with the OSGI object + * @return this builder + */ + B autoConfig(Class clazz, String field); + + /** + * Activates debug mode + * @param label the debug label + * @return this builder + */ + B debug(String label); + + /** + * Automatically adds this component to its DependencyManager object. When a lambda builds a Component using this builder, by default + * the built component is auto added to its DependencyManager object, unless you invoke autoAdd(false). + * + * @param autoAdd true for automatically adding this component to the DependencyManager object, false if not + * @return this builder + */ + B autoAdd(boolean autoAdd); + + /** + * Sets the method to invoke on the service implementation to get back all + * instances that are part of a composition and need dependencies injected. + * All of them will be searched to inject any of the dependencies. The method that + * is invoked must return an Object[]. + * + * @param getCompositionMethod the method to invoke + * @return this builder + */ + B composition(String getCompositionMethod); + + /** + * Sets the instance and method to invoke to get back all instances that + * are part of a composition and need dependencies injected. All of them + * will be searched to inject any of the dependencies. The method that is + * invoked must return an Object[]. + * + * @param instance the instance that has the method + * @param getCompositionMethod the method to invoke + * @return this builder + */ + B composition(Object instance, String getCompositionMethod); + + /** + * Sets a java8 method reference to a Supplier that returns all instances that are part of a composition and need dependencies injected. + * All of them will be searched for any of the dependencies. The method that + * is invoked must return an Object[]. + * + * @param getCompositionMethod the method to invoke + * @return this builder + */ + B composition(Supplier getCompositionMethod); + + /** + * Adds a component state listener to this component. + * + * @param listener the state listener + */ + B listener(ComponentStateListener listener); + + /** + * Builds the real DependencyManager Component. + * @return the real DependencyManager Component. + */ + Component build(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/ConfigurationDependencyBuilder.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/ConfigurationDependencyBuilder.java new file mode 100644 index 00000000000..24a7859fa64 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/ConfigurationDependencyBuilder.java @@ -0,0 +1,371 @@ +/* + * 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.dm.lambda; + +import java.util.Collection; +import java.util.Dictionary; +import java.util.Map; + +import org.apache.felix.dm.ConfigurationDependency; +import org.apache.felix.dm.lambda.callbacks.CbConfiguration; +import org.apache.felix.dm.lambda.callbacks.CbConfigurationComponent; +import org.apache.felix.dm.lambda.callbacks.CbDictionary; +import org.apache.felix.dm.lambda.callbacks.CbDictionaryComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbConfiguration; +import org.apache.felix.dm.lambda.callbacks.InstanceCbConfigurationComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbDictionary; +import org.apache.felix.dm.lambda.callbacks.InstanceCbDictionaryComponent; +import org.osgi.annotation.versioning.ProviderType; + +/** + * Builds a Dependency Manager Configuration Dependency. + * (configuration dependencies are required by default). + * + * Two families of callbacks are supported:

      + * + *

        + *
      • reflection based callbacks: you specify a callback method name + *
      • method reference callbacks: you specify a java8 method reference + *
      + * + *

      Callbacks may accept a Dictionary, a Component, or a user defined configuration type interface. + * + * If you only specify a pid, by default the callback method name is assumed to be "updated". + * + *

      Configuration types are a new feature that allows you to specify an interface that is implemented + * by DM and such interface is then injected to your callback instead of the actual Dictionary. + * Using such configuration interface provides a way for creating type-safe configurations from a actual {@link Dictionary} that is + * normally injected by Dependency Manager. + * The callback accepts in argument an interface that you have to provide, and DM will inject a proxy that converts + * method calls from your configuration-type to lookups in the actual map or dictionary. The results of these lookups are then + * converted to the expected return type of the invoked configuration method.
      + * As proxies are injected, no implementations of the desired configuration-type are necessary! + *

      + *

      + * The lookups performed are based on the name of the method called on the configuration type. The method names are + * "mangled" to the following form: [lower case letter] [any valid character]*. Method names starting with + * get or is (JavaBean convention) are stripped from these prefixes. For example: given a dictionary + * with the key "foo" can be accessed from a configuration-type using the following method names: + * foo(), getFoo() and isFoo().

      + * If the property contains a dot (which is invalid in java method names), then dots (".") can be converted using the following conventions: + *

        + * + *
      • if the method name follows the javabean convention and/or kamel casing convention, then each capital letter is assumed to map to a "dot", + * followed by the same letter in lower case. This means only lower case properties are + * supported in this case. Example: getFooBar() or fooBar() will map to "foo.bar" property. + * + *
      • else, if the method name follows the standard OSGi metatype specification, then dots + * are encoded as "_"; and "_" is encoded as "__". (see OSGi r6 compendium, chapter 105.9.2). + * Example: "foo_BAR()" is mapped to "foo.BAR" property; "foo__BAR_zoo()" is mapped to "foo_BAR.zoo" property. + *
      + *

      + * The return values supported are: primitive types (or their object wrappers), strings, enums, arrays of + * primitives/strings, {@link Collection} types, {@link Map} types, {@link Class}es and interfaces. When an interface is + * returned, it is treated equally to a configuration type, that is, it is returned as a proxy. + *

      + *

      + * Arrays can be represented either as comma-separated values, optionally enclosed in square brackets. For example: + * [ a, b, c ] and a, b,c are both considered an array of length 3 with the values "a", "b" and "c". + * Alternatively, you can append the array index to the key in the dictionary to obtain the same: a dictionary with + * "arr.0" => "a", "arr.1" => "b", "arr.2" => "c" would result in the same array as the earlier examples. + *

      + *

      + * Maps can be represented as single string values similarly as arrays, each value consisting of both the key and value + * separated by a dot. Optionally, the value can be enclosed in curly brackets. Similar to array, you can use the same + * dot notation using the keys. For example, a dictionary with + * + *

      {@code "map" => "{key1.value1, key2.value2}"}
      + * + * and a dictionary with

      + * + *

      {@code "map.key1" => "value1", "map2.key2" => "value2"}
      + * + * result in the same map being returned. + * Instead of a map, you could also define an interface with the methods getKey1() and getKey2 and use + * that interface as return type instead of a {@link Map}. + *

      + *

      + * In case a lookup does not yield a value from the underlying map or dictionary, the following rules are applied: + *

        + *
      1. primitive types yield their default value, as defined by the Java Specification; + *
      2. string, {@link Class}es and enum values yield null; + *
      3. for arrays, collections and maps, an empty array/collection/map is returned; + *
      4. for other interface types that are treated as configuration type a null-object is returned. + *
      + *

      + * + * Sample codes: + * + *

      Code example with a component that defines a Configuration Dependency using a specific callback method reference, + * and the method accepts in argument a configuration type (the pid is assumed to be the fqdn of the configuration type): + * + *

       {@code
      + * public class Activator extends DependencyManagerActivator {
      + *     public void init(BundleContext ctx, DependencyManager dm) throws Exception { 
      + *         component(comp -> comp
      + *           .impl(ServiceImpl.class)
      + *           .withCnf(conf -> conf.update(MyConfig.class, ServiceImpl::modified)));  
      + *    }
      + * }
      + * }
      + * + *

      Code example with a component that defines a Configuration Dependency using a specific callback method reference + * which accepts a Dictionary in argument: + * + *

       {@code
      + * public class Activator extends DependencyManagerActivator {
      + *     public void init(BundleContext ctx, DependencyManager dm) throws Exception { 
      + *         component(comp -> comp
      + *           .impl(ServiceImpl.class)
      + *           .withCnf(conf -> conf.pid("my.pid").update(ServiceImpl::modified)));
      + *    }
      + * }
      + * }
      + * + *

      Code example which defines a configuration dependency injected in the "ServiceImpl.updated(Dictionary)" callback: + * + *

       {@code
      + * public class Activator extends DependencyManagerActivator {
      + *     public void init(BundleContext ctx, DependencyManager dm) throws Exception { 
      + *         component(comp -> comp.impl(ServiceImpl.class).withCnf("my.pid"));
      + *    }
      + * }
      + * }
      + * + *

      Code example with a component that defines a Configuration Dependency using a specific callback method name: + * + *

       {@code
      + * public class Activator extends DependencyManagerActivator {
      + *     public void init(BundleContext ctx, DependencyManager dm) throws Exception { 
      + *         component(comp -> comp.impl(ServiceImpl.class).withCnf(conf -> conf.pid("my.pid").update("modified")));  
      + *    }
      + * }
      + * }
      + * + * @author Felix Project Team + */ +@ProviderType +public interface ConfigurationDependencyBuilder extends DependencyBuilder { + /** + * Sets the required flag which determines if this configuration dependency is required or not. + * A configuration dependency is required by default. + * + * @param required the required flag + * @return this service dependency + */ + ConfigurationDependencyBuilder required(boolean required); + + /** + * Sets the dependency as required. A configuration dependency is required by default. + * + * @return this service dependency + */ + ConfigurationDependencyBuilder required(); + + /** + * Sets the dependency as optional. A configuration dependency is required by default. + * + * @return this service dependency + */ + ConfigurationDependencyBuilder optional(); + + /** + * Sets the pid for this configuration dependency. + * + * @param pid the configuration dependency pid. + * @return this builder + */ + ConfigurationDependencyBuilder pid(String pid); + + /** + * Sets propagation of the configuration to the service properties (false by default). + * All public configuration properties (not starting with a dot) will be propagated to the component service properties. + * + * @return this builder + */ + ConfigurationDependencyBuilder propagate(); + + /** + * Sets propagation of the configuration properties to the service properties (false by default). + * + * @param propagate true if all public configuration properties (not starting with a dot) must be propagated to the component service properties (false by default) + * @return this builder + */ + ConfigurationDependencyBuilder propagate(boolean propagate); + + /** + * Sets a callback method to call on the component implementation class(es) when the configuration is updated. When the configuration is lost, the callback is invoked + * with a null dictionary. + * + *

      The following callback signatures are supported and searched in the following order: + *

        + *
      1. method(Dictionary)
      2. + *
      3. method(Component, Dictionary)
      4. + *
      + * + * @param updateMethod the name of the callback + * @return this builder + */ + ConfigurationDependencyBuilder update(String updateMethod); + + /** + * Sets a callback method to call on the component implementation class(es) when the configuration is updated. The callback is invoked with a configuration type + * argument (null if the configuration is lost). + * + *

      The following callback signatures are supported and searched in the following order: + *

        + *
      1. method(Dictionary)
      2. + *
      3. method(Component, Dictionary)
      4. + *
      5. method(Configuration) // same type as the one specified in the "configType" argument
      6. + *
      7. method(Component, Configuration) // Configuration has the same type as the one specified in the "configType" argument
      8. + *
      + * + * @param configType the type of a configuration that is passed as argument to the callback + * @param updateMethod the callback to call on the component implementation class(es) when the configuration is updated. + * @return this builder + */ + ConfigurationDependencyBuilder update(Class configType, String updateMethod); + + /** + * Sets a callback method to call on a given Object instance when the configuration is updated. + * When the updated method is invoked, the Component implementation is not yet instantiated. This method + * can be typically used by a Factory object which needs the configuration before it can create the actual + * component implementation instance(s). + * + * When the configuration is lost, the callback is invoked with a null dictionary, and the following signatures are supported: + *
        + *
      1. method(Dictionary)
      2. + *
      3. method(Component, Dictionary)
      4. + *
      + * + * @param callbackInstance the object instance on which the updatedMethod is invoked + * @param updateMethod the callback to call on the callbackInstance when the configuration is updated. + * @return this builder + */ + ConfigurationDependencyBuilder update(Object callbackInstance, String updateMethod); + + /** + * Sets a callback method to call on a given Object instance when the configuration is updated. + * When the updated method is invoked, the Component implementation is not yet instantiated. This method + * can be typically used by a Factory object which needs the configuration before it can create the actual + * component implementation instance(s). + * The callback is invoked with a configuration type argument (null of the configuration is lost). + * + *

      The following callback signatures are supported and searched in the following order: + *

        + *
      1. method(Dictionary)
      2. + *
      3. method(Component, Dictionary)
      4. + *
      5. method(Configuration) // same type as the one specified in the "configType" argument
      6. + *
      7. method(Component, Configuration) // Configuration has the same type as the one specified in the "configType" argument
      8. + *
      + * + * @param configType the type of a configuration that is passed as argument to the callback + * @param callbackInstance the object instance on which the updatedMethod is invoked + * @param updateMethod the callback to call on the callbackInstance when the configuration is updated. + * @return this builder + */ + ConfigurationDependencyBuilder update(Class configType, Object callbackInstance, String updateMethod); + + /** + * Sets a reference to a "callback(Dictionary)" method from one of the component implementation classes. + * The method is invoked with a Dictionary argument (which is null if the configuration is lost). + * + * @param The type of the target component implementation class on which the method is invoked + * @param callback a reference to a method of one of the component implementation classes. + * @return this builder + */ + ConfigurationDependencyBuilder update(CbDictionary callback); + + /** + * Sets a reference to a "callback(Dictionary, Component)" method from one of the component implementation classes. + * The method is invoked with Dictionary/Component arguments. When the configuration is lost, the Dictionary argument + * is null. + * + * @param The type of the target component implementation class on which the method is invoked + * @param callback a reference to a method callback defined in one of the the component implementation classes. + * @return this builder + */ + ConfigurationDependencyBuilder update(CbDictionaryComponent callback); + + /** + * Sets a reference to a "callback(Configuration)" method from one of the component implementation classes. + * The method is invoked with a configuration type argument (null if the configuration is lost). + * + * @param The type of the target component implementation class on which the method is invoked + * @param the type of the configuration interface accepted by the callback method. + * @param configType the type of a configuration that is passed as argument to the callback + * @param callback the callback method reference which must point to a method from one of the component implementation classes. The method + * takes as argument an interface which will be implemented by a dynamic proxy that wraps the actual configuration properties. + * @return this builder + */ + ConfigurationDependencyBuilder update(Class configType, CbConfiguration callback); + + /** + * Sets a reference to a "callback(Configuration, Component)" method from one of the component implementation classes. + * The method is invoked with two args: configuration type, Component. The configuration type argument is null if the configuration is lost. + * + * @param The type of the target component implementation class on which the method is invoked + * @param the type of the configuration interface accepted by the callback method. + * @param configType the type of a configuration that is passed as argument to the callback + * @param callback the reference to a method from one of the component implementation classes. The method + * takes as argument an interface which will be implemented by a dynamic proxy that wraps the actual configuration properties. It also + * takes as the second argument a Component object. + * @return this builder + */ + ConfigurationDependencyBuilder update(Class configType, CbConfigurationComponent callback); + + /** + * Sets a reference to a "callback(Dictionary)" method from an Object instance. + * + * @param callback a reference to an Object instance which takes as argument a Dictionary (null if the configuration is lost). + * @return this builder + */ + ConfigurationDependencyBuilder update(InstanceCbDictionary callback); + + /** + * Sets a reference to a "callback(Dictionary, Component)" method from an Object instance. The method accepts + * a Dictionary and a Component object. The passed Dictionary is null in case the configuration is lost. + * + * @param callback a reference to method from an Object instance which takes as argument a Dictionary and a Component + * @return this builder + */ + ConfigurationDependencyBuilder update(InstanceCbDictionaryComponent callback); + + /** + * Sets a reference to a "callback(Configuration)" method from an Object instance. The configuration type argument is null if the configuration is lost. + * + * @param the type of the configuration interface accepted by the callback method. + * @param configType the class of the configuration that is passed as argument to the callback + * @param updated a reference to an Object instance which takes as argument the given configuration type + * @return this builder + */ + ConfigurationDependencyBuilder update(Class configType, InstanceCbConfiguration updated); + + /** + * Sets a reference to a "callback(Configuration, Component)" method from an Object instance. The method accepts + * a configuration type and a Component object. The configuration type argument is null if the configuration is lost. + * + * @param the type of the configuration interface accepted by the callback method. + * @param configType the class of the configuration that is passed as argument to the callback + * @param updated a reference to an Object instance which takes as argument a the given configuration type, and a Component object. + * @return this builder + */ + ConfigurationDependencyBuilder update(Class configType, InstanceCbConfigurationComponent updated); +} + diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/DependencyBuilder.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/DependencyBuilder.java new file mode 100644 index 00000000000..8baed8fc1db --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/DependencyBuilder.java @@ -0,0 +1,33 @@ +/* + * 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.dm.lambda; + +import org.apache.felix.dm.Dependency; + +/** + * Base class for all dependency builders + * @param the dependency type. + */ +public interface DependencyBuilder { + /** + * Builds a DependencyManager dependency. + * @return a real DependencyManager dependency + */ + T build(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/DependencyManagerActivator.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/DependencyManagerActivator.java new file mode 100644 index 00000000000..0b464b69b7b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/DependencyManagerActivator.java @@ -0,0 +1,444 @@ +/* + * 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.dm.lambda; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.lambda.impl.BundleAdapterBuilderImpl; +import org.apache.felix.dm.lambda.impl.BundleDependencyBuilderImpl; +import org.apache.felix.dm.lambda.impl.CompletableFutureDependencyImpl; +import org.apache.felix.dm.lambda.impl.ComponentBuilderImpl; +import org.apache.felix.dm.lambda.impl.ConfigurationDependencyBuilderImpl; +import org.apache.felix.dm.lambda.impl.FactoryPidAdapterBuilderImpl; +import org.apache.felix.dm.lambda.impl.ServiceAdapterBuilderImpl; +import org.apache.felix.dm.lambda.impl.ServiceAspectBuilderImpl; +import org.apache.felix.dm.lambda.impl.ServiceDependencyBuilderImpl; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +/** + * Defines a base for Activators in order to build DependencyManager Components using a java8 style.

      + * + * Code example using auto configured fields: + * + *

       {@code
      + * 
      + * import org.apache.felix.dm.lambda.DependencyManagerActivator;
      + *
      + * public class Activator extends DependencyManagerActivator {    
      + *     public void init(BundleContext ctx, DependencyManager dm) throws Exception {
      + *         component(comp -> comp
      + *             .provides(Service.class, property -> "value")
      + *             .impl(ServiceImpl.class)            
      + *             .withSvc(LogService.class, ConfigurationAdmni.class) // both services are required and injected in class fields with compatible types.           
      + *     }
      + * }
      + * }
      + * + * Code example using reflection callbacks: + * + *
       {@code
      + * import org.apache.felix.dm.lambda.DependencyManagerActivator;
      + *
      + * public class Activator extends DependencyManagerActivator {    
      + *     public void init(BundleContext ctx, DependencyManager dm) throws Exception {
      + *         component(comp -> comp
      + *             .provides(Service.class, property -> "value")
      + *             .impl(ServiceImpl.class)            
      + *             .withSvc(LogService.class, svc -> svc.add("setLog"))              
      + *             .withSvc(ConfigurationAdmni.class, svc -> svc.add("setConfigAdmin")))                
      + *     }
      + * }
      + * }
      + * + * Code example using method references: + * + *
       {@code
      + * import org.apache.felix.dm.lambda.DependencyManagerActivator;
      + *
      + * public class Activator extends DependencyManagerActivator {    
      + *     public void init(BundleContext ctx, DependencyManager dm) throws Exception {
      + *         component(comp -> comp
      + *             .provides(Service.class, property -> "value")
      + *             .impl(ServiceImpl.class)            
      + *             .withSvc(LogService.class, svc -> svc.add(ServiceImpl::setLog))              
      + *             .withSvc(ConfigurationAdmni.class, svc -> svc.add(ServiceImpl::setConfigAdmin)))                
      + *     }
      + * }
      + * }
      + * + * When a dependency is not explicitly defined as "required" or "optional", + * then it is assumed to be optional by default, like it is the case with the original DM API. + * You can change the default mode using the "org.apache.felix.dependencymanager.lambda.defaultRequiredDependency system property" + * (see Felix dm-lambda online documentation). + */ +public abstract class DependencyManagerActivator implements BundleActivator { + /** + * DependencyManager object used to create/register real DM Components that are built by this activator. + */ + private DependencyManager m_manager; + + /** + * Our Activator is starting. + */ + @Override + public void start(BundleContext context) throws Exception { + m_manager = new DependencyManager(context); + init(context, m_manager); + } + + /** + * Our Activator is stopped. + */ + @Override + public void stop(BundleContext context) throws Exception { + destroy(); + m_manager.clear(); + } + + /** + * Sub classes must override this method in order to build some DM components. + * @param ctx the context associated to the bundle + * @param dm the DependencyManager assocaited to this activator + * @throws Exception if the activation fails + */ + protected abstract void init(BundleContext ctx, DependencyManager dm) throws Exception; + + /** + * Sub classes may override this method that is called when the Activator is stopped. + * @throws Exception if the deactivation fails + */ + protected void destroy() throws Exception { + } + + /** + * Returns the DependencyManager used to create/managed DM Components. + * + * @return the DependencyManager associated to this Activator + */ + public DependencyManager getDM() { + return m_manager; + } + + /** + * Returns the bundle context that is associated with this bundle. + * + * @return the bundle context + */ + public BundleContext getBC() { + return m_manager.getBundleContext(); + } + + /** + * Creates a Component builder that can be used to create a DM Component. + * @return a Component builder that can be used to create a DM Component. + */ + protected ComponentBuilder component() { + return new ComponentBuilderImpl(m_manager); + } + + /** + * Creates a service Aspect builder that can be used to create a DM Aspect Component. + * + * @param the aspect service type + * @param aspectType the aspect service + * @return a service Aspect builder. + */ + protected ServiceAspectBuilder aspect(Class aspectType) { + ServiceAspectBuilderImpl aspectBuilder = new ServiceAspectBuilderImpl<>(m_manager, aspectType); + return aspectBuilder; + } + + /** + * Creates a service Adapter builder that can be used to create a DM Adapter Component. + * + * @param the adapted service type. + * @param adaptee the adapted service + * @return a service Adapter builder. + */ + protected ServiceAdapterBuilder adapter(Class adaptee) { + ServiceAdapterBuilderImpl adapterBuilder = new ServiceAdapterBuilderImpl<>(m_manager, adaptee); + return adapterBuilder; + } + + /** + * Builds a DM Component using a Java8 style ComponentBuilder. + * @param consumer the lambda that will use the ComponentBuilder for building the DM component. + * The component is auto-added to the DependencyManager, unless the lambda calls the ComponentBuilder.autoAdd(false) method. + * @return a newly built DM component. + */ + protected Component component(Consumer> consumer) { + return component(m_manager, consumer); + } + + /** + * Builds a DM Aspect Component using a Java8 style AspectBuilder. + * The component is auto-added to the DependencyManager, unless the lambda calls the AspectBuilder.autoAdd(false) method. + * + * @param the aspect service type + * @param aspect the aspect service + * @param consumer the lambda that will use the AspectBuilder for building the DM aspect component. + * @return the DM component build by the consumer of the aspect builder + */ + protected Component aspect(Class aspect, Consumer> consumer) { + return aspect(m_manager, aspect, consumer); + } + + /** + * Builds a DM Adapter Component using a Java8 style AdapterBuilder. + * The component is auto-added to the DependencyManager, unless the lambda calls the AdapterBuilder.autoAdd(false) method. + * + * @param the adapted service type + * @param adaptee the adapted service + * @param consumer the lambda that will use the AdapterBuilder for building the DM adapter component. + * @return a newly built DM component. + */ + protected Component adapter(Class adaptee, Consumer> consumer) { + return adapter(m_manager, adaptee, consumer); + } + + /** + * Builds a DM Factory Configuration Adapter Component using a Java8 style FactoryPidAdapterBuilder. + * The component is auto-added to the DependencyManager, unless the lambda calls the FactoryPidAdapterBuilder.autoAdd(false) method. + * + * @param consumer the lambda that will use the FactoryPidAdapterBuilder for building the DM factory configuration adapter component. + * @return a newly built DM component. + */ + protected Component factoryPidAdapter(Consumer consumer) { + return factoryPidAdapter(m_manager, consumer); + } + + /** + * Builds a DM Bundle Adapter Component. + * @param consumer the lambda used to build the actual bundle adapter. + * The component is auto-added to the DependencyManager, unless the lambda calls the BundleAdapter.autoAdd(false) method. + * @return a newly built DM component. + */ + protected Component bundleAdapter(Consumer consumer) { + return bundleAdapter(m_manager, consumer); + } + + // These static methods can be used when building DM components outside of an activator. + + /** + * Creates a Component builder that can be used to create a Component. + * + * @param dm the DependencyManager object used to create the component builder + * @return a Component builder that can be used to create a Component. + */ + public static ComponentBuilder component(DependencyManager dm) { + return new ComponentBuilderImpl(dm); + } + + /** + * Creates a service Aspect builder that can be used to create an Aspect Component. + * + * @param the aspect service type + * @param dm the DependencyManager object used to register the built component + * @param aspect the type of the aspect service + * @return a service Aspect builder that can be used to create an Aspect Component. + */ + public static ServiceAspectBuilder aspect(DependencyManager dm, Class aspect) { + ServiceAspectBuilderImpl aspectBuilder = new ServiceAspectBuilderImpl<>(dm, aspect); + return aspectBuilder; + } + + /** + * Creates a service Adapter builder that can be used to create an Adapter Component. + * + * @param the adapted service type + * @param dm the DependencyManager object used to register the built component + * @param adaptee the type of the adaptee service + * @return a service Adapter builder that can be used to create an Adapter Component. + */ + public static ServiceAdapterBuilder adapter(DependencyManager dm, Class adaptee) { + ServiceAdapterBuilderImpl adapterBuilder = new ServiceAdapterBuilderImpl<>(dm, adaptee); + return adapterBuilder; + } + + /** + * Creates a factory pid adapter that can be used to create a factory adapter Component. + * @param dm the DependencyManager object used to register the built component + * @return a factory pid adapter that can be used to create a factory adapter Component. + */ + public static FactoryPidAdapterBuilder factoryPidAdapter(DependencyManager dm) { + return new FactoryPidAdapterBuilderImpl(dm); + } + + /** + * Creates a bundle adapter builder that can be used to create a DM bundle adapter Component. + * + * @param dm the DependencyManager object used to create the bundle adapter builder. + * @return a bundle adapter builder that can be used to create a DM bundle adapter Component. + */ + public static BundleAdapterBuilder bundleAdapter(DependencyManager dm) { + return new BundleAdapterBuilderImpl(dm); + } + + /** + * Creates a DM ServiceDependency builder. + * + * @param the service dependency type + * @param component the component on which you want to build a new service dependency using the returned builder + * @param service the service dependency type. + * @return a DM ServiceDependency builder. + */ + public static ServiceDependencyBuilder serviceDependency(Component component, Class service) { + return new ServiceDependencyBuilderImpl<>(component, service); + } + + /** + * Creates a DM Configuration Dependency builder. + * + * @param component the component on which you want to build a new configuration dependency using the returned builder + * @return a DM Configuration Dependency builder. + */ + public static ConfigurationDependencyBuilder confDependency(Component component) { + return new ConfigurationDependencyBuilderImpl(component); + } + + /** + * Creates a DM Bundle Dependency builder. + * + * @param component the component on which you want to build a new bundle dependency using the returned builder + * @return a DM Configuration Dependency builder. + */ + public static BundleDependencyBuilder bundleDependency(Component component) { + return new BundleDependencyBuilderImpl(component); + } + + /** + * Creates a DM CompletableFuture Dependency builder. + * + * @param the type of the CompletableFuture result. + * @param component the component on which you want to build a new completable future dependency using the returned builder. + * @param future the future the dependency built using the returned builder will depend on. + * @return a CompletableFuture dependency builder. + */ + public static FutureDependencyBuilder futureDependency(Component component, CompletableFuture future) { + return new CompletableFutureDependencyImpl<>(component, future); + } + + /** + * Builds a component using a lambda and a component builder + * @param dm the DependencyManager where the component is auto-added (unless the component.autoAdd(false) is called) + * @param consumer a lambda that is called to build the component. When the lambda is called, it will be provided with a + * ComponentBuilder object that is used to build the actual DM component. + * + * @return the built DM component. + */ + public static Component component(DependencyManager dm, Consumer> consumer) { + ComponentBuilder componentBuilder = new ComponentBuilderImpl(dm); + consumer.accept(componentBuilder); + Component comp = componentBuilder.build(); + if (((ComponentBuilderImpl) componentBuilder).isAutoAdd()) { + dm.add(comp); + } + return comp; + } + + /** + * Update an existing component. Typically, this method can be used from a Component.init method, where more dependencies has to be added. + * @param comp an existing DM component + * @param consumer the lambda that will be used to update the component + */ + public static void component(Component comp, Consumer> consumer) { + ComponentBuilder componentBuilder = new ComponentBuilderImpl(comp, true /* update component */); + consumer.accept(componentBuilder); + componentBuilder.build(); + } + + /** + * Builds an aspect DM Component. + * + * @param the aspect service type + * @param dm the DependencyManager object used to register the built component + * @param aspect the type of the aspect service + * @param consumer a lambda used to build the DM aspect component + * @return a new DM aspect component. The aspect component is auto-added into the dm object, unless the lambda calls + * the AspectBuilder.autoAdd(false) method. + */ + public static Component aspect(DependencyManager dm, Class aspect, Consumer> consumer) { + ServiceAspectBuilderImpl aspectBuilder = new ServiceAspectBuilderImpl<>(dm, aspect); + consumer.accept(aspectBuilder); + Component comp = aspectBuilder.build(); + if (aspectBuilder.isAutoAdd()) { + dm.add(comp); + } + return comp; + } + + /** + * Builds an adapter DM Component. + * + * @param the adapted service type + * @param dm the DependencyManager object used to register the built component + * @param adaptee the type of the adapted service + * @param consumer a lambda used to build the DM adapter component + * @return a new DM adapter component. The adapter component is auto-added into the dm object, unless the lambda calls + * the AspectBuilder.autoAdd(false) method is called. + */ + public static Component adapter(DependencyManager dm, Class adaptee, Consumer> consumer) { + ServiceAdapterBuilderImpl adapterBuilder = new ServiceAdapterBuilderImpl<>(dm, adaptee); + consumer.accept(adapterBuilder); + Component comp = adapterBuilder.build(); + if (adapterBuilder.isAutoAdd()) { + dm.add(comp); + } + return comp; + } + + /** + * Builds a bundle adapter DM Component. + * + * @param dm the DependencyManager object used to register the built component + * @param consumer a lambda used to build the bundle adapter component + * @return a new bundle adapter component. The adapter component is auto-added into the dm object, unless the lambda calls + * the AspectBuilder.autoAdd(false) method is called. + */ + public static Component bundleAdapter(DependencyManager dm, Consumer consumer) { + BundleAdapterBuilderImpl adapterBuilder = new BundleAdapterBuilderImpl(dm); + consumer.accept(adapterBuilder); + Component comp = adapterBuilder.build(); + if (adapterBuilder.isAutoAdd()) { + dm.add(comp); + } + return comp; + } + + /** + * Builds a DM factory configuration adapter. + * @param dm the DependencyManager object used to create DM components. + * @param consumer a lambda used to build the DM factory configuration adapter component + * @return a new DM factory configuration adapter component. The adapter component is auto-added into the dm object, unless the lambda calls + * the FactoryPidAdapterBuilder.autoAdd(false) method is called + */ + public static Component factoryPidAdapter(DependencyManager dm, Consumer consumer) { + FactoryPidAdapterBuilderImpl factoryPidAdapter = new FactoryPidAdapterBuilderImpl(dm); + consumer.accept(factoryPidAdapter); + Component comp = factoryPidAdapter.build(); + if (factoryPidAdapter.isAutoAdd()) { + dm.add(comp); + } + return comp; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/FactoryPidAdapterBuilder.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/FactoryPidAdapterBuilder.java new file mode 100644 index 00000000000..89be1ddfbee --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/FactoryPidAdapterBuilder.java @@ -0,0 +1,253 @@ +/* + * 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.dm.lambda; + +import org.apache.felix.dm.lambda.callbacks.CbConfiguration; +import org.apache.felix.dm.lambda.callbacks.CbConfigurationComponent; +import org.apache.felix.dm.lambda.callbacks.CbDictionary; +import org.apache.felix.dm.lambda.callbacks.CbDictionaryComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbConfiguration; +import org.apache.felix.dm.lambda.callbacks.InstanceCbConfigurationComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbDictionary; +import org.apache.felix.dm.lambda.callbacks.InstanceCbDictionaryComponent; + +/** + * Builds a Dependency Manager Factory Configuration Adapter Component.

      For each new Config Admin factory configuration matching a given factory pid, + * an adapter will be created based on the adapter implementation class. The adapter will be registered with the specified interface, + * and with the specified adapter service properties. Depending on the propagate parameter, every public factory configuration properties + * (which don't start with ".") will be propagated along with the adapter service properties. + * + * This builded supports type safe configuration types. For a given factory configuration, you can specify an interface of your choice, + * and DM will implement it using a dynamic proxy that converts interface methods to lookups in the actual factory configuration dictionary. + * For more information about configuration types, please refer to {@link ConfigurationDependencyBuilder}. + * + *

      Example that defines a factory configuration adapter service for the "foo.bar" factory pid. For each factory pid instance, + * an instance of the DictionaryImpl component will be created. + * + *

       {@code
      + * public class Activator extends DependencyManagerActivator {
      + *     public void init(BundleContext ctx, DependencyManager dm) throws Exception { 
      + *         factoryPidAdapter(adapter -> adapter
      + *             .impl(DictionaryImpl.class)
      + *             .factoryPid("foo.bar")
      + *             .update(ServiceImpl::updated)
      + *             .propagate()
      + *             .withSvc(LogService.class, log -> log.optional()));
      + *    }
      + * }
      + * }
      + * + *

      Example that defines a factory configuration adapter using a user defined configuration type + * (the pid is by default assumed to match the fqdn of the configuration type): + * + *

       {@code
      + * 
      + * public interface DictionaryConfiguration {
      + *     public String getLanguage();
      + *     public List getWords();
      + * }
      + * 
      + * public class Activator extends DependencyManagerActivator {
      + *     public void init(BundleContext ctx, DependencyManager dm) throws Exception { 
      + *         factoryPidAdapter(adapter -> adapter
      + *             .impl(DictionaryImpl.class)
      + *             .factoryPid("foo.bar")
      + *             .update(DictionaryConfiguration.class, ServiceImpl::updated)
      + *             .propagate()
      + *             .withSvc(LogService.class, log -> log.optional()));
      + *    }
      + * }
      + * }
      + */ +public interface FactoryPidAdapterBuilder extends ComponentBuilder { + /** + * Specifies the factory pid used by the adapter. + * @param pid the factory pid. + * @return this builder + */ + FactoryPidAdapterBuilder factoryPid(String pid); + + /** + * Specifies if the public properties (not starting with a dot) should be propagated to the adapter service properties (false by default). + * + * @return this builder. + */ + FactoryPidAdapterBuilder propagate(); + + /** + * Specifies if the public properties (not starting with a dot) should be propagated to the adapter service properties (false by default). + * + * @param propagate true if the public properties should be propagated to the adapter service properties (false by default). + * @return this builder. + */ + FactoryPidAdapterBuilder propagate(boolean propagate); + + /** + * Specifies a callback method that will be called on the component implementation when the configuration is injected. + * + * @param updateMethod the method to call on the component implementation when the configuration is available ("updated" by default). + * + *

      The following method signatures are supported: + *

        + *
      1. method(Dictionary properties) + *
      2. method(Component component, Dictionary properties) + *
      + * + * @return this builder + */ + FactoryPidAdapterBuilder update(String updateMethod); + + /** + * Sets a callback method to call on the component implementation when the configuration is updated. + * The callback is invoked with a configuration type argument. + * + *

      The following callback signatures are supported and searched in the following order: + *

        + *
      1. method(Dictionary)
      2. + *
      3. method(Component, Dictionary)
      4. + *
      5. method(Configuration) // same type as the one specified in the "configType" argument
      6. + *
      7. method(Component, Configuration) // Configuration has the same type as the one specified in the "configType" argument
      8. + *
      + * + * @param configType the type of a configuration that is passed as argument to the callback + * @param updateMethod the callback to call on the component implementation when the configuration is updated. + * @return this builder + */ + FactoryPidAdapterBuilder update(Class configType, String updateMethod); + + /** + * Specifies a callback instance method that will be called on a given object instance when the configuration is injected. + * + * @param updateMethod the method to call on the given object instance when the configuration is available ("updated" by default). + * The following method signatures are supported: + * + *
       {@code
      +     *    method(Dictionary properties)
      +     *    method(Component component, Dictionary properties)
      +     * }
      + * + * @param callbackInstance the Object instance on which the updated callback will be invoked. + * @return this builder + */ + FactoryPidAdapterBuilder update(Object callbackInstance, String updateMethod); + + /** + * Specifies a callback instance method that will be called on a given object instance when the configuration is injected. + * The callback is invoked with a configuration type argument. + * + *

      The following callback signatures are supported and searched in the following order: + *

        + *
      1. method(Dictionary)
      2. + *
      3. method(Component, Dictionary)
      4. + *
      5. method(Configuration) // same type as the one specified in the "configType" argument
      6. + *
      7. method(Component, Configuration) // Configuration has the same type as the one specified in the "configType" argument
      8. + *
      + * + * @param configType the type of a configuration that is passed as argument to the callback + * @param callbackInstance the Object instance on which the updated callback will be invoked. + * @param updateMethod the method to call on the given object instance when the configuration is available. The callback is invoked + * with a configuration type argument (matching the configType you have specified. + * @return this builder + */ + FactoryPidAdapterBuilder update(Class configType, Object callbackInstance, String updateMethod); + + /** + * Specifies a method reference that will be called on one of the component classes when the configuration is injected. + * The callback is invoked with a Dictionary argument. + * + * @param the type of the component implementation class on which the callback is invoked on. + * @param callback the method to call on one of the component classes when the configuration is available. + * @return this builder + */ + FactoryPidAdapterBuilder update(CbDictionary callback); + + /** + * Specifies a method reference that will be called on one of the component classes when the configuration is injected. + * The callback is invoked with a configuration type argument. + * + * @param the type of the component implementation class on which the callback is invoked on. + * @param the configuration type accepted by the callback method. + * @param configType the type of a configuration that is passed as argument to the callback + * @param callback the method to call on one of the component classes when the configuration is available. + * @return this builder + */ + FactoryPidAdapterBuilder update(Class configType, CbConfiguration callback); + + /** + * Specifies a method reference that will be called on one of the component classes when the configuration is injected + * + * @param the type of the component implementation class on which the callback is invoked on. + * @param callback the reference to a method on one of the component classes. The method may takes as arguments a Dictionary and a Component. + * @return this builder + */ + FactoryPidAdapterBuilder update(CbDictionaryComponent callback); + + /** + * Specifies a method reference that will be called on one of the component classes when the configuration is injected. + * The callback is invoked with the following arguments: a configuration type, and a Component object. + * + * @param the type of the component implementation class on which the callback is invoked on. + * @param the configuration type accepted by the callback method. + * @param configType the type of a configuration that is passed as argument to the callback + * @param callback the reference to a method on one of the component classes. The method may takes as arguments a configuration type and a Component. + * @return this builder + */ + FactoryPidAdapterBuilder update(Class configType, CbConfigurationComponent callback); + + /** + * Specifies a method reference that will be called on a given object instance when the configuration is injected + * + * @param callback the method to call on a given object instance when the configuration is available. The callback takes as argument a + * a Dictionary parameter. + * @return this builder + */ + FactoryPidAdapterBuilder update(InstanceCbDictionary callback); + + /** + * Specifies a method reference that will be called on a given object instance when the configuration is injected. + * The callback is invoked with a type-safe configuration type argument. + * + * @param the configuration type accepted by the callback method. + * @param configType the type of a configuration that is passed as argument to the callback + * @param callback the method to call on a given object instance when the configuration is available. The callback takes as argument a + * a configuration type parameter. + * @return this builder + */ + FactoryPidAdapterBuilder update(Class configType, InstanceCbConfiguration callback); + + /** + * Specifies a method reference that will be called on a given object instance when the configuration is injected. + * + * @param callback the method to call on a given object instance when the configuration is available. The callback takes as argument a + * Dictionary, and a Component parameter. + * @return this builder + */ + FactoryPidAdapterBuilder update(InstanceCbDictionaryComponent callback); + + /** + * Specifies a method reference that will be called on a given object instance when the configuration is injected. + * + * @param the configuration type accepted by the callback method. + * @param configType the type of a configuration that is passed as argument to the callback + * @param callback the method to call on a given object instance when the configuration is available. The callback takes as arguments a + * configuration type, and a Component parameter. + * @return this builder + */ + FactoryPidAdapterBuilder update(Class configType, InstanceCbConfigurationComponent callback); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/FluentProperty.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/FluentProperty.java new file mode 100644 index 00000000000..d3663b6256d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/FluentProperty.java @@ -0,0 +1,71 @@ +/* + * 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.dm.lambda; + +import org.apache.felix.dm.lambda.callbacks.SerializableLambda; + +/** + * Lambda allowing to define fluent service properties. Property names are deduces from the lambda parameter name. + * + *

      Example of a component which provides fluent properties {"foo"="bar"; "foo2"=Integer(123)}: + * + *

      {@code
      + * public class Activator extends DependencyManagerActivator {
      + *   public void init(BundleContext ctx, DependencyManager dm) throws Exception {
      + *       component(comp -> comp.impl(MyComponentImpl.class).provides(MyService.class, foo->"bar", foo2 -> 123));
      + *   }
      + * } 
      + * }
      + * + * When a property name contains dots ("."), then since it's not possible to declare a lambda parameter with some dots, you can + * encode the dots using some underscores. A single underscore is then converted to a dot, unless is it followed by another + * underscore in which case the two consecutive underscores ("__") are converted to a single underscore. + * + * For example, assume your component provides a service using the two service.ranking and foo_bar service properties. In this case + * you would declare the service properties like this:

      + * + *

      {@code
      + * public class Activator extends DependencyManagerActivator {
      + *   public void init(BundleContext ctx, DependencyManager dm) throws Exception {
      + *       component(comp -> comp.impl(MyComponentImpl.class).provides(MyService.class, service_ranking->10, foo__bar -> "gabuzo"));
      + *   }
      + * } 
      + * }
      + * + * Caution: Fluent properties requires the usage of the "-parameter" javac option. + * + * Under eclipse, you can enable this option using: + * + *
      {@code
      + * Windows -> Preference -> Compiler -> Classfile Generation -> Store information about method parameters.
      + * }
      + * + *
      WARNING: this is interface is deprecated, it is only supported when using java8, not on java9+, and we will remove it in next DM release + * @deprecated this interface is only supported with java8 and will be removed in next dm release + */ +@FunctionalInterface +public interface FluentProperty extends SerializableLambda { + /** + * Represents a fluent property + * + * @param name the property name. The parameter used by the lambda will be intropsected and will be used as the actual property name. + * @return the property value + */ + public Object apply(String name); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/FutureDependencyBuilder.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/FutureDependencyBuilder.java new file mode 100644 index 00000000000..dec6378d425 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/FutureDependencyBuilder.java @@ -0,0 +1,150 @@ +/* + * 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.dm.lambda; + +import java.util.concurrent.Executor; + +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.lambda.callbacks.CbFuture; +import org.apache.felix.dm.lambda.callbacks.InstanceCbFuture; + +/** + * Defines a builder for a CompletableFuture dependency. + *

      Using such dependency allows your component to wait for the completion of a given asynchronous task + * represented by a standard jdk CompletableFuture object. + * + * A FutureDependency is required and unblock the Component once the CompletableFuture result has completed. + * + *

      Usage Example

      + * + *

      Here is an Activator that downloads a page from the web and injects the string result to the component before it is started. + * When the web page is downloaded, the result is injected in the MyComponent::setPage method and + * the component is then called in its "start" method: + * + *

      {@code
      + * 
      + * public class Activator extends DependencyManagerActivator {
      + *   public void init(BundleContext ctx, DependencyManager dm) throws Exception {  
      + *      // Download a web page asynchronously, using a CompletableFuture:
      + *        	
      + *      String url = "http://felix.apache.org/";
      + *      CompletableFuture page = CompletableFuture.supplyAsync(() -> downloadSite(url));				
      + *
      + *      // The component depends on a log service and on the content of the Felix site.
      + *      // The lambda passed to the "withFuture" method configures the callback that is 
      + *      // invoked with the result of the CompletableFuture (the page content).
      + *      
      + *      component(comp -> comp
      + *          .impl(MyComponent.class)
      + *          .withService(LogService.class)
      + *          .withFuture(page, result -> result.complete(MyComponent::setPage)));
      + *   }
      + * }
      + * 
      + * public class MyComponent {
      + *   volatile LogService log; // injected.
      + *   
      + *   void setPage(String page) {
      + *      // injected by the FutureDependency.
      + *   }
      + *   
      + *   void start() {
      + *      // all required dependencies injected.
      + *   }
      + * }
      + * 
      + * }
      + * + * @param the type of the CompletableFuture result. + */ +public interface FutureDependencyBuilder extends DependencyBuilder { + /** + * Sets the callback method name to invoke on the component implementation, once the CompletableFuture has completed. + * @param callback the callback method name to invoke on the component implementation, once the CompletableFuture on which we depend has completed. + * @return this dependency. + */ + FutureDependencyBuilder complete(String callback); + + /** + * Sets the callback instance method name to invoke on a given Object instance, once the CompletableFuture has completed. + * @param callbackInstance the object instance on which the callback must be invoked + * @param callback the callback method name to invoke on Object instance, once the CompletableFuture has completed. + * @return this dependency. + */ + FutureDependencyBuilder complete(Object callbackInstance, String callback); + + /** + * Sets the function to invoke when the future task has completed. The function is from one of the Component implementation classes, and it accepts the + * result of the completed future. + * + * @param the type of the CompletableFuture result. + * @param callback the function to perform when the future task as completed. + * @return this dependency + */ + FutureDependencyBuilder complete(CbFuture callback); + + /** + * Sets the function to invoke asynchronously when the future task has completed. The function is from one of the Component implementation classes, + * and it accepts the result of the completed future. + * + * @param the type of the CompletableFuture result. + * @param callback the function to perform when the future task as completed. + * @param async true if the callback should be invoked asynchronously using the default jdk execution facility, false if not. + * @return this dependency + */ + FutureDependencyBuilder complete(CbFuture callback, boolean async); + + /** + * Sets the function to invoke asynchronously when the future task has completed. The function is from one of the Component implementation classes, + * and it accepts the result of the completed future. + * + * @param the type of the CompletableFuture result. + * @param callback the function to perform when the future task as completed. + * @param executor the executor used to schedule the callback. + * @return this dependency + */ + FutureDependencyBuilder complete(CbFuture callback, Executor executor); + + /** + * Sets the callback instance to invoke when the future task has completed. The callback is a Consumer instance which accepts the + * result of the completed future. + * @param callback a Consumer instance which accepts the result of the completed future. + * @return this dependency + */ + FutureDependencyBuilder complete(InstanceCbFuture callback); + + /** + * Sets the callback instance to invoke when the future task has completed. The callback is a Consumer instance which accepts the + * result of the completed future. + * + * @param callback a Consumer instance which accepts the result of the completed future. + * @param async true if the callback should be invoked asynchronously using the default jdk execution facility, false if not. + * @return this dependency + */ + FutureDependencyBuilder complete(InstanceCbFuture callback, boolean async); + + /** + * Sets the callback instance to invoke when the future task has completed. The callback is a Consumer instance which accepts the + * result of the completed future. + * @param callback the action to perform when the future task as completed. + * @param executor the executor to use for asynchronous execution of the callback. + * @return this dependency + */ + FutureDependencyBuilder complete(InstanceCbFuture callback, Executor executor); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/ServiceAdapterBuilder.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/ServiceAdapterBuilder.java new file mode 100644 index 00000000000..1c07215a4c2 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/ServiceAdapterBuilder.java @@ -0,0 +1,77 @@ +/* + * 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.dm.lambda; + +/** + * Builds a Dependency Manager Service Adapter Component. + *

      The adapter will be applied to any service that matches the specified interface and filter. For each matching service an adapter will be created + * based on the adapter implementation class. The adapter will be registered with the specified interface and existing properties from the original + * service plus any extra properties you supply here.

      + * + * Code example that adapts a "Device" service to an HttpServlet service. The adapter is created using a ServiceAdapterBuilder that is passed to the lambda. + * + *

       {@code
      + * public class Activator extends DependencyManagerActivator {
      + *    public void init(BundleContext ctx, DependencyManager dm) throws Exception { 
      + *        adapter(Device.class, adapt -> adapt.impl(DeviceServlet.class).provides(HttpServlet.class).properties(alias -> "/device");                    
      + *    }
      + * }}
      + * + * @param the adaptee service + */ +public interface ServiceAdapterBuilder extends ComponentBuilder>, ServiceCallbacksBuilder> { + /** + * Specifies the filter used to match a given adapted service. + * + * @param adapteeFilter the filter used to match a given adapted service + * @return this builder + */ + ServiceAdapterBuilder filter(String adapteeFilter); + + /** + * Specifies whether or not the adapted service properties must be propagated to the adapter service (true by default). + * + * @param propagate true if the adapted service properties must be propagated to the adapter service (true by default). + * @return this builder + */ + ServiceAdapterBuilder propagate(boolean propagate); + + /** + * Injects this adapted service in all fields matching the adapted service type. + * + * @return this builder + */ + ServiceAdapterBuilder autoConfig(); + + /** + * Configures whether or not the adapted service can be injected in all fields matching the adapted service type. + * + * @param autoConfig true if the adapted service can be injected in all fields matching the adapted service type + * @return this builder + */ + ServiceAdapterBuilder autoConfig(boolean autoConfig); + + /** + * Injects this adapted service on the field matching the given name + * + * @param field the field name where the adapted service must be injected to. + * @return this builder + */ + ServiceAdapterBuilder autoConfig(String field); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/ServiceAspectBuilder.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/ServiceAspectBuilder.java new file mode 100644 index 00000000000..371085c862b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/ServiceAspectBuilder.java @@ -0,0 +1,91 @@ +/* + * 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.dm.lambda; + +/** + * Builds a Dependency Manager Aspect Component. + *

      The aspect will be applied to any service that matches the specified interface and filter (if any). For each matching service an aspect will be created based + * on the aspect implementation class. + * The aspect will be registered with the same interface and properties as the original service, plus any extra properties you supply here. + * Multiple Aspects of the same service are chained and ordered using aspect ranks. + * + *

      Code example which provides a "LogService" aspect that performs spell-checking of each log message. + * The aspect decorates a LogService. The aspect also depends on an Dictionary service that is internally used to perform log spell checking. + * The LogService and Dictionary services are injected in the aspect implementation using reflection on class fields: + * + *

      {@code
      + * public class Activator extends DependencyManagerActivator {
      + *    public void init(BundleContext ctx, DependencyManager dm) throws Exception { 
      + *       aspect(LogService.class, asp -> asp.impl(SpellCheckLogAspect.class).rank(10).withSvc(Dictionary.class));
      + *    }
      + * }} 
      + * + * Same example, but using callbacks for injecting LogService and Dictionary services in the aspect implementation class: + * + *
      {@code
      + * public class Activator extends DependencyManagerActivator {
      + *    public void init(BundleContext ctx, DependencyManager dm) throws Exception { 
      + *       aspect(LogService.class, asp -> asp
      + *          .impl(SpellCheckLogAspect.class).rank(10)
      + *          .add(SpellCheckLogAspect::setLogService)
      + *          .withSvc(Dictionary.class, svc -> svc.add(SpellCheckLogAspect::setDictionary)));
      + *    }
      + * }} 
      + * + * @param the aspect service + */ +public interface ServiceAspectBuilder extends ComponentBuilder>, ServiceCallbacksBuilder> { + /** + * Specifies the aspect service filter. + * + * @param filter the filter condition to use with the service interface the aspect will apply on + * @return this builder + */ + ServiceAspectBuilder filter(String filter); + + /** + * Specifies the aspect ranking. Aspects of a given service are ordered by their ranking property. + * + * @param ranking the aspect ranking + * @return this builder + */ + ServiceAspectBuilder rank(int ranking); + + /** + * Injects the aspect in all fields matching the aspect type. + * @return this builder + */ + ServiceAspectBuilder autoConfig(); + + /** + * Configures whether or not the aspect service can be injected in all fields matching the aspect type. + * + * @param autoConfig true if the aspect service can be injected in all fields matching the dependency type + * @return this builder + */ + ServiceAspectBuilder autoConfig(boolean autoConfig); + + /** + * Injects the aspect service on the field with the given name. + * + * @param field the field name where the aspect service must be injected + * @return this builder + */ + ServiceAspectBuilder autoConfig(String field); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/ServiceCallbacksBuilder.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/ServiceCallbacksBuilder.java new file mode 100644 index 00000000000..1a82ee1b8e8 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/ServiceCallbacksBuilder.java @@ -0,0 +1,895 @@ +/* + * 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.dm.lambda; + +import org.apache.felix.dm.lambda.callbacks.CbRef; +import org.apache.felix.dm.lambda.callbacks.CbRefComponent; +import org.apache.felix.dm.lambda.callbacks.CbRefRef; +import org.apache.felix.dm.lambda.callbacks.CbRefRefComponent; +import org.apache.felix.dm.lambda.callbacks.CbRefServiceRefService; +import org.apache.felix.dm.lambda.callbacks.CbRefServiceRefServiceComponent; +import org.apache.felix.dm.lambda.callbacks.CbService; +import org.apache.felix.dm.lambda.callbacks.CbServiceComponent; +import org.apache.felix.dm.lambda.callbacks.CbServiceComponentRef; +import org.apache.felix.dm.lambda.callbacks.CbServiceDict; +import org.apache.felix.dm.lambda.callbacks.CbServiceMap; +import org.apache.felix.dm.lambda.callbacks.CbServiceObjects; +import org.apache.felix.dm.lambda.callbacks.CbServiceObjectsServiceObjects; +import org.apache.felix.dm.lambda.callbacks.CbServiceRef; +import org.apache.felix.dm.lambda.callbacks.CbServiceService; +import org.apache.felix.dm.lambda.callbacks.CbServiceServiceComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbRef; +import org.apache.felix.dm.lambda.callbacks.InstanceCbRefComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbRefRef; +import org.apache.felix.dm.lambda.callbacks.InstanceCbRefRefComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbRefServiceRefService; +import org.apache.felix.dm.lambda.callbacks.InstanceCbRefServiceRefServiceComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbService; +import org.apache.felix.dm.lambda.callbacks.InstanceCbServiceComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbServiceComponentRef; +import org.apache.felix.dm.lambda.callbacks.InstanceCbServiceDict; +import org.apache.felix.dm.lambda.callbacks.InstanceCbServiceMap; +import org.apache.felix.dm.lambda.callbacks.InstanceCbServiceObjects; +import org.apache.felix.dm.lambda.callbacks.InstanceCbServiceObjectsServiceObjects; +import org.apache.felix.dm.lambda.callbacks.InstanceCbServiceRef; +import org.apache.felix.dm.lambda.callbacks.InstanceCbServiceService; +import org.apache.felix.dm.lambda.callbacks.InstanceCbServiceServiceComponent; +import org.osgi.annotation.versioning.ProviderType; + +/** + * Builds a service dependency callback. + * + *

      A Service may be injected in a bind-method of a component or an object instance using this builder. + * The builder supports reflection based callbacks (same as with the original DM API), as well as java8 method reference based callbacks. + * + *

      List of signatures supported using reflection based callbacks (same as original DM API): + * + *

       {@code
      + * method(S service)
      + * method(S service, Map serviceProperties)
      + * method(S service, Dictionary serviceProperties)
      + * method(ServiceReference serviceRef, S service),
      + * method(ServiceReference serviceRef)
      + * method(ServiceObjects serviceObjects)
      + * method(Component serviceComponent)
      + * method(Component serviceComponent, ServiceReference serviceRef)
      + * method(Component serviceComponent, S service) 
      + * method(Component serviceComponent, ServiceReference serviceRef, S service)
      + * swapMethod(S oldService, S newService)
      + * swapMethod(ServiceReference oldRef, S old, ServiceReference newRef, S newService)
      + * swapMethod(ServiceReference oldRef, ServiceReference newRef)
      + * swapMethod(ServiceObjects oldServiceObjects, ServiceObjects newServiceObjects)
      + * swapMethod(Component component, S oldService, S newService)
      + * swapMethod(Component component, ServiceReference oldService, ServiceReference newService)
      + * swapMethod(Component component, ServiceReference oldRef, S old, ServiceReference newRef, S newService)
      + * }
      + * + * List of signatures supported using java 8 method references: + * + *
       {@code
      + * method(S service)
      + * method(S service, ServiceReference serviceRef),
      + * method(S service, Map serviceProperties)
      + * method(S service, Dictionary serviceProperties)
      + * method(S service, Component serviceComponent)
      + * method(S service, Component serviceComponent, ServiceReference serviceRef)
      + * method(ServiceReference service)
      + * method(ServiceObjects service)
      + * method(ServiceReference service, Component serviceComponent)
      + * swapMethod(S oldService, S newService)
      + * swapMethod(ServiceReference<> oldRef, ServiceReference newRef)
      + * swapMethod(S oldService, S newService, Component component))
      + * swapMethod(ServiceReference<> oldRef, ServiceReference newRef, Component component)
      + * swapMethod(ServiceReference oldRef, S old, ServiceReference newRef, S newService)
      + * swapMethod(ServiceReference oldRef, S old, ServiceReference newRef, S newService, Component component)
      + * }
      + * + *

      Here is an example of a Component that defines a dependency of a LogService which is injected in the "setLog" method using a ServiceCallbacksBuilder: + * The withSvc(...)" declaration defines a method reference on the "Pojo::setLog" method (using a lambda): + * + *

       {@code
      + * public class Activator extends DependencyManagerActivator {
      + *    public void init(BundleContext ctx, DependencyManager dm) throws Exception { 
      + *       component(comp -> comp.impl(Pojo.class).withSvc(LogService.class, log -> log.add(Pojo::setLog)));
      + *    }
      + * }}
      + * + *

      Same example, but we inject the dependency to an object instance that we already have in hand: + * + *

       {@code
      + * public class Activator extends DependencyManagerActivator {
      + *    public void init(BundleContext ctx, DependencyManager dm) throws Exception {
      + *       Pojo impl = new Pojo();
      + *       component(comp -> comp.impl(impl).withSvc(LogService.class, log -> log.add(impl::setLog)));
      + *    }
      + * }}
      + * + *

      Here, we inject a service using method reflection (as it is the case in original DM api): + * + *

       {@code
      + * public class Activator extends DependencyManagerActivator {
      + *    public void init(BundleContext ctx, DependencyManager dm) throws Exception {
      + *       component(comp -> comp.impl(Pojo::class).withSvc(LogService.class, log -> log.add("setLog")));
      + *    }
      + * }}
      + * + * @param the service dependency type + * @param the type of a sub interface that may extends this interface. + * + * @author Felix Project Team + */ +@ProviderType +public interface ServiceCallbacksBuilder> { + + /** + * Sets the callback instance used for reflection based callbacks. + * @param callbackInstance the object on which reflection based callbacks are invoked on. + * @return this builder + */ + B callbackInstance(Object callbackInstance); + + /** + * Sets a callback method to invoke when a service is added. When a service matches the service + * filter, then the service is injected using the specified callback method. The callback is invoked on the component implementation, or on the callback + * instance, is specified using the {@link #callbackInstance(Object)} method. + * + * The following method signature are supported: + *
      {@code
      +     * method(S service)
      +     * method(S service, Map serviceProperties)
      +     * method(S service, Dictionary serviceProperties)
      +     * method(ServiceReference serviceRef, S service),
      +     * method(ServiceReference serviceRef)
      +     * method(Component serviceComponent)
      +     * method(Component serviceComponent, ServiceReference serviceRef)
      +     * method(Component serviceComponent, S service) 
      +     * method(Component serviceComponent, ServiceReference serviceRef, S service)
      +     * }
      + * + * @param callback the add callback + * @return this builder + * @see #callbackInstance(Object) + */ + B add(String callback); + + /** + * Sets a callback methods to invoke when a service is changed. When a changed service matches the service + * filter, then the service is injected using the specified callback method. The callback is invoked on the component implementation, or on the callback + * instance, is specified using the {@link #callbackInstance(Object)} method. + * + * The following method signature are supported: + *
      {@code
      +     * method(S service)
      +     * method(S service, Map serviceProperties)
      +     * method(S service, Dictionary serviceProperties)
      +     * method(ServiceReference serviceRef, S service),
      +     * method(ServiceReference serviceRef)
      +     * method(Component serviceComponent)
      +     * method(Component serviceComponent, ServiceReference serviceRef)
      +     * method(Component serviceComponent, S service) 
      +     * method(Component serviceComponent, ServiceReference serviceRef, S service)
      +     * }
      + * + * @param callback the change callback + * @return this builder + * @see #callbackInstance(Object) + */ + B change(String callback); + + /** + * Sets a callback method to invoke when a service is removed. When a removed service matches the service + * filter, then the specified callback in invoked with the removed service. The callback is invoked on the component implementation, or on the callback + * instance, is specified using the {@link #callbackInstance(Object)} method. + * + * The following method signature are supported: + *
      {@code
      +     * method(S service)
      +     * method(S service, Map serviceProperties)
      +     * method(S service, Dictionary serviceProperties)
      +     * method(ServiceReference serviceRef, S service),
      +     * method(ServiceReference serviceRef)
      +     * method(Component serviceComponent)
      +     * method(Component serviceComponent, ServiceReference serviceRef)
      +     * method(Component serviceComponent, S service) 
      +     * method(Component serviceComponent, ServiceReference serviceRef, S service)
      +     * }
      + * + * @param callback the remove callback + * @return this builder + * @see #callbackInstance(Object) + */ + B remove(String callback); + + /** + * Sets a callback method to invoke when a service is swapped. The callback is invoked on the component implementation, or on the callback + * instance, is specified using the {@link #callbackInstance(Object)} method. + * + * The following method signature are supported: + *
      {@code
      +     * swapMethod(S oldService, S newService)
      +     * swapMethod(ServiceReference oldRef, S old, ServiceReference newRef, S newService)
      +     * swapMethod(Component component, S oldService, S newService)
      +     * swapMethod(Component component, ServiceReference oldRef, S old, ServiceReference newRef, S newService)
      +     * }
      + * + * @param callback the remove callback + * @return this builder + * @see #callbackInstance(Object) + */ + B swap(String callback); + + /** + * Sets a component callback(Service) method reference. The callback is invoked when a service is added. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param add the method reference invoked when a service is added. + * @return this builder + */ + B add(CbService add); + + /** + * Sets a component callback(Service) method reference. The callback is invoked when a service is changed. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param change the method reference invoked when a service is changed. + * @return this builder + */ + B change(CbService change); + + /** + * Sets a component callback(Service) method reference. The callback is invoked when a service is removed. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param remove the method reference invoked when a service is removed. + * @return this builder + */ + B remove(CbService remove); + + /** + * Sets a {@code component callback(Service, Map)} method reference. The callback is invoked when a service is added. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param add the method reference invoked when a service is added. + * @return this builder + */ + B add(CbServiceMap add); + + /** + * Sets a {@code component callback(Service, Map)} method reference. The callback is invoked when a service is changed. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param change the method reference invoked when a service is changed. + * @return this builder + */ + B change(CbServiceMap change); + + /** + * Sets a {@code component callback(Service, Map)} method reference. The callback is invoked when a service is removed. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param remove the method reference invoked when a service is removed. + * @return this builder + */ + B remove(CbServiceMap remove); + + /** + * Sets a {@code component callback(Service, Dictionary)} method reference. The callback is invoked when a service is added. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param add the method reference invoked when a service is added. + * @return this builder + */ + B add(CbServiceDict add); + + /** + * Sets a {@code component callback(Service, Dictionary)} method reference. The callback is invoked when a service is changed. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param change the method reference invoked when a service is changed. + * @return this builder + */ + B change(CbServiceDict change); + + /** + * Sets a {@code component callback(Service, Dictionary)} method reference. The callback is invoked when a service is removed. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param remove the method reference invoked when a service is removed. + * @return this builder + */ + B remove(CbServiceDict remove); + + /** + * Sets a component callback(Service, ServiceReference) method reference. The callback is invoked when a service is added. + * The method reference must point to a Component implementation class method. + * @param the type of the component implementation class on which the callback is invoked. + * @param add the method reference invoked when a service is added. + * @return this builder + */ + B add(CbServiceRef add); + + /** + * Sets a component callback(Service, ServiceReference) method reference. The callback is invoked when a service is changed. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param change the method reference invoked when a service is changed. + * @return this builder + */ + B change(CbServiceRef change); + + /** + * Sets a component callback(Service, ServiceReference) method reference. The callback is invoked when a service is removed. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param remove the method reference invoked when a service is removed. + * @return this builder + */ + B remove(CbServiceRef remove); + + /** + * Sets a component callback(Service, Component) method reference. The callback is invoked when a service is added. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param add the method reference invoked when a service is added. + * @return this builder + */ + B add(CbServiceComponent add); + + /** + * Sets a component callback(Service, Component) method reference. The callback is invoked when a service is changed. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param change the method reference invoked when a service is changed. + * @return this builder + */ + B change(CbServiceComponent change); + + /** + * Sets a component callback(Service, Component) method reference. The callback is invoked when a service is removed. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param remove the method reference invoked when a service is removed. + * @return this builder + */ + B remove(CbServiceComponent remove); + + /** + * Sets a component callback(Service, Component, ServiceReference ref) method reference. The callback is invoked when a service is added. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param add the method reference invoked when a service is added. + * @return this builder + */ + B add(CbServiceComponentRef add); + + /** + * Sets a component callback(Service, Component, ServiceReference) method reference. The callback is invoked when a service is changed. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param change the method reference invoked when a service is changed. + * @return this builder + */ + B change(CbServiceComponentRef change); + + /** + * Sets a component callback(Service, Component, ServiceReference) method reference. The callback is invoked when a service is removed. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param remove the method reference invoked when a service is removed. + * @return this builder + */ + B remove(CbServiceComponentRef remove); + + /** + * Sets a component callback(ServiceReference ref) method reference. The callback is invoked when a service is added. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param add the method reference invoked when a service is added. + * @return this builder + */ + B add(CbRef add); + + /** + * Sets a component callback(ServiceReference) method reference. The callback is invoked when a service is changed. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param change the method reference invoked when a service is changed. + * @return this builder + */ + B change(CbRef change); + + /** + * Sets a component callback(ServiceReference) method reference. The callback is invoked when a service is removed. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param remove the method reference invoked when a service is removed. + * @return this builder + */ + B remove(CbRef remove); + + /** + * Sets a component callback(ServiceObjects ref) method reference. The callback is invoked when a service is added. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param add the method reference invoked when a service is added. + * @return this builder + */ + B add(CbServiceObjects add); + + /** + * Sets a component callback(ServiceObjects) method reference. The callback is invoked when a service is changed. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param change the method reference invoked when a service is changed. + * @return this builder + */ + B change(CbServiceObjects change); + + /** + * Sets a component callback(ServiceObjects) method reference. The callback is invoked when a service is removed. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param remove the method reference invoked when a service is removed. + * @return this builder + */ + B remove(CbServiceObjects remove); + + /** + * Sets a component callback(ServiceReference ref, Component comp) method reference. The callback is invoked when a service is added. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param add the method reference invoked when a service is added. + * @return this builder + */ + B add(CbRefComponent add); + + /** + * Sets a component callback(ServiceReference, Component comp) method reference. The callback is invoked when a service is changed. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param change the method reference invoked when a service is changed. + * @return this builder + */ + B change(CbRefComponent change); + + /** + * Sets a component callback(ServiceReference, Component comp) method reference. The callback is invoked when a service is removed. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param remove the method reference invoked when a service is removed. + * @return this builder + */ + B remove(CbRefComponent remove); + + /** + * Sets an Object instance callback(Service) method reference. The callback is invoked when a service is added. + * The method reference must point to a method from an Object instance. + * + * @param add the method reference invoked when a service is added. + * @return this builder + */ + B add(InstanceCbService add); + + /** + * Sets an Object instance callback(Service) method reference. The callback is invoked when a service is changed. + * The method reference must point to method from an Object instance. + * + * @param change the method reference invoked when a service is changed. + * @return this builder + */ + B change(InstanceCbService change); + + /** + * Sets an Object instance callback(Service) method reference. The callback is invoked when a service is removed. + * The method reference must point to method from an Object instance. + * + * @param remove the method reference invoked when a service is removed. + * @return this builder + */ + B remove(InstanceCbService remove); + + /** + * Sets an {@code Object instance callback(Service, Map)} method reference. The callback is invoked when a service is added. + * The method reference must point to a method from an Object instance. + * + * @param add the method reference invoked when a service is added. + * @return this builder + */ + B add(InstanceCbServiceMap add); + + /** + * Sets an {@code Object instance callback(Service, Map)} method reference. The callback is invoked when a service is changed. + * The method reference must point to method from an Object instance. + * + * @param change the method reference invoked when a service is changed. + * @return this builder + */ + B change(InstanceCbServiceMap change); + + /** + * Sets an {@code Object instance callback(Service, Map)} method reference. The callback is invoked when a service is removed. + * The method reference must point to method from an Object instance. + * + * @param remove the method reference invoked when a service is removed. + * @return this builder + */ + B remove(InstanceCbServiceMap remove); + + /** + * Sets an {@code Object instance callback(Service svc, Dictionary} method reference. The callback is invoked when a service is added. + * The method reference must point to a method from an Object instance. + * + * @param add the method reference invoked when a service is added. + * @return this builder + */ + B add(InstanceCbServiceDict add); + + /** + * Sets an {@code Object instance callback(Service, Dictionary)} method reference. The callback is invoked when a service is changed. + * The method reference must point to method from an Object instance. + * + * @param change the method reference invoked when a service is changed. + * @return this builder + */ + B change(InstanceCbServiceDict change); + + /** + * Sets an {@code Object instance callback(Service, Dictionary)} method reference. The callback is invoked when a service is removed. + * The method reference must point to method from an Object instance. + * + * @param remove the method reference invoked when a service is removed. + * @return this builder + */ + B remove(InstanceCbServiceDict remove); + + /** + * Sets an Object instance callback(Service, ServiceReference) method reference. The callback is invoked when a service is added. + * The method reference must point to a method from an Object instance. + * + * @param add the method reference invoked when a service is added. + * @return this builder + */ + B add(InstanceCbServiceRef add); + + /** + * Sets an Object instance callback(Service, ServiceReference) method reference. The callback is invoked when a service is changed. + * The method reference must point to method from an Object instance. + * + * @param change the method reference invoked when a service is changed. + * @return this builder + */ + B change(InstanceCbServiceRef change); + + /** + * Sets an Object instance callback(Service, ServiceReference) method reference. The callback is invoked when a service is removed. + * The method reference must point to method from an Object instance. + * + * @param remove the method reference invoked when a service is removed. + * @return this builder + */ + B remove(InstanceCbServiceRef remove); + + /** + * Sets an Object instance callback(ServiceReference) method reference. The callback is invoked when a service is added. + * The method reference must point to a method from an Object instance. + * + * @param add the method reference invoked when a service is added. + * @return this builder + */ + B add(InstanceCbRef add); + + /** + * Sets an Object instance callback(ServiceReference) method reference. The callback is invoked when a service is changed. + * The method reference must point to method from an Object instance. + * + * @param change the method reference invoked when a service is changed. + * @return this builder + */ + B change(InstanceCbRef change); + + /** + * Sets an Object instance callback(ServiceReference) method reference. The callback is invoked when a service is removed. + * The method reference must point to method from an Object instance. + * + * @param remove the method reference invoked when a service is removed. + * @return this builder + */ + B remove(InstanceCbRef remove); + + /** + * Sets an Object instance callback(ServiceObjects) method reference. The callback is invoked when a service is added. + * The method reference must point to a method from an Object instance. + * + * @param add the method reference invoked when a service is added. + * @return this builder + */ + B add(InstanceCbServiceObjects add); + + /** + * Sets an Object instance callback(ServiceObjects) method reference. The callback is invoked when a service is changed. + * The method reference must point to method from an Object instance. + * + * @param change the method reference invoked when a service is changed. + * @return this builder + */ + B change(InstanceCbServiceObjects change); + + /** + * Sets an Object instance callback(ServiceObjects) method reference. The callback is invoked when a service is removed. + * The method reference must point to method from an Object instance. + * + * @param remove the method reference invoked when a service is removed. + * @return this builder + */ + B remove(InstanceCbServiceObjects remove); + + /** + * Sets an Object instance callback(Service, Component) method reference. The callback is when a service is added. + * The method reference must point to a method from an Object instance. + * + * @param add the method reference invoked when a service is added. + * @return this builder + */ + B add(InstanceCbServiceComponent add); + + /** + * Sets an Object instance callback(Service, Component) method reference. The callback is when a service is changed. + * The method reference must point to method from an Object instance. + * + * @param change the method reference invoked when a service is changed. + * @return this builder + */ + B change(InstanceCbServiceComponent change); + + /** + * Sets an Object instance callback(Service, Component) method reference. The callback is invoked when a service is removed. + * The method reference must point to method from an Object instance. + * + * @param remove the method reference invoked when a service is removed. + * @return this builder + */ + B remove(InstanceCbServiceComponent remove); + + /** + * Sets an Object instance callback(Service, Component, ServiceReference) method reference. The callback is invoked when a service is added. + * The method reference must point to a method from an Object instance. + * + * @param add the method reference invoked when a service is added. + * @return this builder + */ + B add(InstanceCbServiceComponentRef add); + + /** + * Sets an Object instance callback(Service, Component, ServiceReference) method reference. The callback is invoked when a service is changed. + * The method reference must point to method from an Object instance. + * + * @param change the method reference invoked when a service is changed. + * @return this builder + */ + B change(InstanceCbServiceComponentRef change); + + /** + * Sets an Object instance callback(Service, Component, ServiceReference) method reference. The callback is invoked when a service is removed. + * The method reference must point to method from an Object instance. + * + * @param remove the method reference invoked when a service is removed. + * @return this builder + */ + B remove(InstanceCbServiceComponentRef remove); + + /** + * Sets an Object instance callback(ServiceReference, Component) method reference. The callback is invoked when a service is added. + * The method reference must point to a method from an Object instance. + * + * @param add the method reference invoked when a service is added. + * @return this builder + */ + B add(InstanceCbRefComponent add); + + /** + * Sets an Object instance callback(ServiceReference, Component) method reference. The callback is invoked when a service is changed. + * The method reference must point to method from an Object instance. + * + * @param change the method reference invoked when a service is changed. + * @return this builder + */ + B change(InstanceCbRefComponent change); + + /** + * Sets an Object instance callback(ServiceReference, Component) method reference. The callback is invoked when a service is removed. + * The method reference must point to method from an Object instance. + * + * @param remove the method reference invoked when a service is removed. + * @return this builder + */ + B remove(InstanceCbRefComponent remove); + + /** + * Sets a swap component callback(Service, Service) method reference. The callback is invoked when a service is swapped. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param swap the method reference invoked when the service is swapped. + * @return this builder + */ + B swap(CbServiceService swap); + + /** + * Sets a swap component callback(Service, Service) method reference. The callback is invoked when a service is swapped. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param swap the method reference invoked when the service is swapped. + * @return this builder + */ + B swap(CbRefRef swap); + + /** + * Sets a swap component callback(ServiceObjects, ServiceObjects) method reference. The callback is invoked when a service is swapped. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param swap the method reference invoked when the service is swapped. + * @return this builder + */ + B swap(CbServiceObjectsServiceObjects swap); + + /** + * Sets a swap component callback(Service, Service, Component) method reference. The callback is invoked when a service is swapped. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param swap the method reference invoked when the service is swapped. + * @return this builder + */ + B swap(CbServiceServiceComponent swap); + + /** + * Sets a swap component callback(ServiceRefere, ServiceReference, Component) method reference. The callback is invoked when a service is swapped. + * The method reference must point to a Component implementation class method. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param swap the method reference invoked when the service is swapped. + * @return this builder + */ + B swap(CbRefRefComponent swap); + + /** + * Sets a swap component callback(ServiceReference, Service, ServiceReference, Service) method reference. The callback is invoked when a service is swapped. + * The method reference must point to a Component implementation class method. + * the new service. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param swap the method reference invoked when the service is swapped. + * @return this builder + */ + B swap(CbRefServiceRefService swap); + + /** + * Sets a swap component callback(ServiceReference, Service, ServiceReference, Service, Component method reference. The callback is invoked when a service is swapped. + * The method reference must point to a Component implementation class method. + * the new service. + * + * @param the type of the component implementation class on which the callback is invoked. + * @param swap the method reference invoked when the service is swapped. + * @return this builder + */ + B swap(CbRefServiceRefServiceComponent swap); + + /** + * Sets a swap instance callback(Service, Service) method reference. The callback is invoked when a service is swapped. + * The method reference must point to a method from an Object instance. + * + * @param swap the method reference invoked when the service is swapped. + * @return this builder + */ + B swap(InstanceCbServiceService swap); + + /** + * Sets a swap instance callback(ServiceReference, ServiceReference) method reference. The callback is invoked when a service is swapped. + * The method reference must point to a method from an Object instance. + * + * @param swap the method reference invoked when the service is swapped. + * @return this builder + */ + B swap(InstanceCbRefRef swap); + + /** + * Sets a swap instance callback(ServiceObjects, ServiceObjects) method reference. The callback is invoked when a service is swapped. + * The method reference must point to a method from an Object instance. + * + * @param swap the method reference invoked when the service is swapped. + * @return this builder + */ + B swap(InstanceCbServiceObjectsServiceObjects swap); + + /** + * Sets a swap instance callback(ServiceReference, ServiceReference, Component) method reference. The callback is invoked when a service is swapped. + * The method reference must point to a method from an Object instance. + * + * @param swap the method reference invoked when the service is swapped. + * @return this builder + */ + B swap(InstanceCbRefRefComponent swap); + + /** + * Sets a swap instance callback(Service, Service, Component) method reference. The callback is invoked when a service is swapped. + * The method reference must point to a method from an Object instance. + * + * @param swap the method reference invoked when the service is swapped. + * @return this builder + */ + B swap(InstanceCbServiceServiceComponent swap); + + /** + * Sets a swap instance callback(ServiceReference, Service, ServiceReference, Service) method reference. The callback is invoked when a service is swapped. + * The method reference must point to a method from an Object instance. + * + * @param swap the method reference invoked when the service is swapped. + * @return this builder + */ + B swap(InstanceCbRefServiceRefService swap); + + /** + * Sets a swap instance callback(ServiceReference, Service, ServiceReference, Service, Component) method reference. The callback is invoked when a service is swapped. + * The method reference must point to a method from an Object instance. + * + * @param swap the method reference invoked when the service is swapped. + * @return this builder + */ + B swap(InstanceCbRefServiceRefServiceComponent swap); + + /** + * Configures whether or not this dependency should internally obtain the service object for all tracked service references. + * + * By default, if you use a java method reference as dependency callbacks, then DM lambda auto-detects if your method takes as argument a + * ServiceReference. And in this case, the service reference is not internally dereferenced by DM (using BundleContext.getServiceReference() method). + * But if you are using a reflection based callback method name, then by default DM always internally dereference the service. In this case + * you can invoke the dereference(false) method in case you don't want DM to internally dereference the service. + * You will use this method typically when you are using a method name as the callback and when for example you want to + * dereference yourself the service using the ServiceObjects OSGi API. + */ + B dereference(boolean obtainServiceBeforeInjection); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/ServiceDependencyBuilder.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/ServiceDependencyBuilder.java new file mode 100644 index 00000000000..cfcc5f8e9c1 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/ServiceDependencyBuilder.java @@ -0,0 +1,163 @@ +/* + * 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.dm.lambda; + +import java.util.Dictionary; +import java.util.function.BiFunction; +import java.util.function.Function; + +import org.apache.felix.dm.ServiceDependency; +import org.osgi.framework.ServiceReference; + +/** + * Builds a Dependency Manager Service Dependency. + *

      Sample code: + * + *

       {@code
      + * public class Activator extends DependencyManagerActivator {
      + *    public void init(BundleContext ctx, DependencyManager dm) throws Exception {
      + *       component(comp -> comp
      + *          .impl(Pojo.class)
      + *          .withSvc(ConfigurationAdmin.class, LogService.class) // varargs of optional (possibly NullObjects) dependencies injected in compatible class fields
      + *          .withSvc(true, Coordinator.class, LogService.class) // varargs of required dependencies injected in compatible class fields
      + *          .withSvc(ConfigurationAdmin.class, "(vendor=apache)") // service with a filter, injected in compatible class fields
      + *          .withSvc(ConfigurationAdmin.class, "(vendor=apache)", true) // required service with a filter, injected in compatible class fields
      + *          .withSvc(ConfigurationAdmin.class, "(vendor=apache)", true, "field") // required service with a filter, injected in a given class field name
      + *          .withSvc(HttpService.class, svc -> svc.required().add(Pojo::setHttpService)) // required dependency injected using a method reference
      + *          .withSvc(Tracked.class, svc -> svc.optional().add(Pojo::addTracked)) // optional dependency, injected using method ref, after the start() callback
      + *    }
      + * }}
      + * + * @param the type of the service dependency + */ +public interface ServiceDependencyBuilder extends DependencyBuilder, ServiceCallbacksBuilder> { + /** + * Configures the service dependency filter + * @param filter the service filter + * @return this builder + */ + ServiceDependencyBuilder filter(String filter); + + /** + * Configures this dependency with the given ServiceReference. + * @param ref the service reference + * @return this builder + */ + ServiceDependencyBuilder ref(ServiceReference ref); + + /** + * Configures this dependency as optional. + * @return this builder + */ + ServiceDependencyBuilder optional(); + + /** + * Configures this dependency as required. + * @return this builder + */ + ServiceDependencyBuilder required(); + + /** + * Configures whether this dependency is required or not. + * + * @param required true if the dependency is required, false if not. + * @return this builder + */ + ServiceDependencyBuilder required(boolean required); + + /** + * Configures debug mode + * @param label the label used by debug messages + * @return this builder + */ + ServiceDependencyBuilder debug(String label); + + /** + * Propagates the dependency properties to the component service properties. + * @return this builder + */ + ServiceDependencyBuilder propagate(); + + /** + * Configures whether the dependency properties must be propagated or not to the component service properties. + * + * @param propagate true if the service dependency properties should be propagated to the properties provided by the component using this dependency. + * @return this builder + */ + ServiceDependencyBuilder propagate(boolean propagate); + + /** + * Configures a method that can is called in order to get propagated service properties. + * + * @param instance an object instance + * @param method the method name to call on the object instance. This method returns the propagated service properties. + * @return this builder + */ + ServiceDependencyBuilder propagate(Object instance, String method); + + /** + * Specifies a function that is called to get the propagated service properties for this service dependency. + * @param propagate a function that is called to get the propagated service properties for this service dependency. + * @return this builder + */ + ServiceDependencyBuilder propagate(Function, Dictionary> propagate); + + /** + * Specifies a function that is called to get the propagated service properties for this service dependency. + * @param propagate a function that is called to get the propagated service properties for this service dependency. + * @return this builder + */ + ServiceDependencyBuilder propagate(BiFunction, S, Dictionary> propagate); + + /** + * Sets the default implementation if the service is not available. + * @param defaultImpl the implementation used by default when the service is not available. + * @return this builder + */ + ServiceDependencyBuilder defImpl(Object defaultImpl); + + /** + * Sets a timeout for this dependency. A timed dependency blocks the invoker thread if the required dependency is currently unavailable, until it comes up again. + * @param timeout the timeout to wait in milliseconds when the service disappears. If the timeout expires, an IllegalStateException is thrown + * when the missing service is invoked. + * + * @return this builder + */ + ServiceDependencyBuilder timeout(long timeout); + + /** + * Injects this dependency in all fields matching the dependency type. + * @return this builder + */ + ServiceDependencyBuilder autoConfig(); + + /** + * Configures whether or not the dependency can be injected in all fields matching the dependency type. + * @param autoConfig true if the dependency can be injected in all fields matching the dependency type + * @return this builder + */ + ServiceDependencyBuilder autoConfig(boolean autoConfig); + + /** + * Injects this dependency on the field with the given name + * @param field the field name where the dependency must be injected + * @return this builder + */ + ServiceDependencyBuilder autoConfig(String field); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/Cb.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/Cb.java new file mode 100644 index 00000000000..453a99563e9 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/Cb.java @@ -0,0 +1,43 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +/** + * Represents a java8 method reference to a zero-argument method from a given component implementation class. + *

      The type of the class on which the callback is invoked on is represented by the T generic parameter. + * @author Felix Project Team + */ +@FunctionalInterface +public interface Cb extends SerializableLambda { + /** + * Invokes the callback method on the given component implementation instance. + * @param t the component implementation instance the callback is invoked on. + */ + void accept(T t); + + default Cb andThen(Cb after) { + Objects.requireNonNull(after); + return (T t) -> { + accept(t); + after.accept(t); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbBundle.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbBundle.java new file mode 100644 index 00000000000..1a049f38fac --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbBundle.java @@ -0,0 +1,47 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.osgi.framework.Bundle; + +/** + * Represents a callback(Bundle) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbBundle extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param bundle the callback parameter + */ + void accept(T instance, Bundle bundle); + + default CbBundle andThen(CbBundle after) { + Objects.requireNonNull(after); + return (T instance, Bundle bundle) -> { + accept(instance, bundle); + after.accept(instance, bundle); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbBundleComponent.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbBundleComponent.java new file mode 100644 index 00000000000..05a70f44f41 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbBundleComponent.java @@ -0,0 +1,49 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.apache.felix.dm.Component; +import org.osgi.framework.Bundle; + +/** + * Represents a callback(Bundle, Component) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbBundleComponent extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param bundle the first callback parameter + * @param component the second callback parameter + */ + void accept(T instance, Bundle bundle, Component component); + + default CbBundleComponent andThen(CbBundleComponent after) { + Objects.requireNonNull(after); + return (T instance, Bundle bundle, Component component) -> { + accept(instance, bundle, component); + after.accept(instance, bundle, component); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbComponent.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbComponent.java new file mode 100644 index 00000000000..cf71d3f79fb --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbComponent.java @@ -0,0 +1,48 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.apache.felix.dm.Component; + +/** + * Represents a callback(Component) that is invoked on a Component implementation class. + * The type of the component implementation class on which the callback is invoked on is represented by the T generic parameter. + * The component callback accepts in argument a Component parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbComponent extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation class instance on which the callback is invoked on. + * @param component the callback parameter + */ + void accept(T instance, Component component); + + default CbComponent andThen(CbComponent after) { + Objects.requireNonNull(after); + return (T instance, Component component) -> { + accept(instance, component); + after.accept(instance, component); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbConfiguration.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbConfiguration.java new file mode 100644 index 00000000000..6cdc2e74bcf --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbConfiguration.java @@ -0,0 +1,55 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Dictionary; +import java.util.Objects; + +import org.apache.felix.dm.lambda.ConfigurationDependencyBuilder; + +/** + * Represents a callback(Configuration) that is invoked on a Component implementation class. + * The callback accepts a type-safe configuration class for wrapping properties behind a dynamic proxy interface. + * + *

      The T generic parameter represents the type of the class on which the callback is invoked on. + *

      The U generic parameter represents the type of the configuration class passed to the callback argument. + * + *

      Using such callback provides a way for creating type-safe configurations from the actual {@link Dictionary} that is + * normally injected by Dependency Manager. + * For more information about configuration types, please refer to {@link ConfigurationDependencyBuilder}. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbConfiguration extends SerializableLambda { + /** + * Handles the given arguments + * @param instance the Component implementation instance on which the callback is invoked on. + * @param configuration the configuration proxy + */ + void accept(T instance, U configuration); + + default CbConfiguration andThen(CbConfiguration after) { + Objects.requireNonNull(after); + return (T instance, U configuration) -> { + accept(instance, configuration); + after.accept(instance, configuration); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbConfigurationComponent.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbConfigurationComponent.java new file mode 100644 index 00000000000..5f61ad2c6d5 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbConfigurationComponent.java @@ -0,0 +1,57 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Dictionary; +import java.util.Objects; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.lambda.ConfigurationDependencyBuilder; + +/** + * Represents a callback(Configuration, Component) which accepts a configuration type for wrapping properties + * behind a dynamic proxy interface. + * + *

      The T generic parameter represents the type of the class on which the callback is invoked on. + *

      The U generic parameter represents the type of the configuration class passed to the callback argument. + * + *

      Using such callback provides a way for creating type-safe configurations from the actual {@link Dictionary} that is + * normally injected by Dependency Manager. + * For more information about configuration types, please refer to {@link ConfigurationDependencyBuilder}. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbConfigurationComponent extends SerializableLambda { + /** + * Handles the given arguments + * @param instance the Component implementation instance on which the callback is invoked on. + * @param configuration the configuration proxy + * @param component the callback Component + */ + void accept(T instance, U configuration, Component component); + + default CbConfigurationComponent andThen(CbConfigurationComponent after) { + Objects.requireNonNull(after); + return (T instance, U configuration, Component component) -> { + accept(instance, configuration, component); + after.accept(instance, configuration, component); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbDictionary.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbDictionary.java new file mode 100644 index 00000000000..f1a00234872 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbDictionary.java @@ -0,0 +1,46 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Dictionary; +import java.util.Objects; + +/** + * Represents a callback(Dictionary) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbDictionary extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param conf the callback argument + */ + void accept(T instance, Dictionary conf); + + default CbDictionary andThen(CbDictionary after) { + Objects.requireNonNull(after); + return (T instance, Dictionary conf) -> { + accept(instance, conf); + after.accept(instance, conf); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbDictionaryComponent.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbDictionaryComponent.java new file mode 100644 index 00000000000..937583a9e6a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbDictionaryComponent.java @@ -0,0 +1,49 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Dictionary; +import java.util.Objects; + +import org.apache.felix.dm.Component; + +/** + * Represents a callback(Dictionary, Component) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbDictionaryComponent extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param conf the first callback parameter + * @param component the second callback parameter + */ + void accept(T instance, Dictionary conf, Component component); + + default CbDictionaryComponent andThen(CbDictionaryComponent after) { + Objects.requireNonNull(after); + return (T instance, Dictionary conf, Component component) -> { + accept(instance, conf, component); + after.accept(instance, conf, component); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbFuture.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbFuture.java new file mode 100644 index 00000000000..56539416134 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbFuture.java @@ -0,0 +1,46 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +/** + * Represents a callback that accepts the result of a CompletableFuture operation. The callback is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * The type of the result of the CompletableFuture is represented by the F generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbFuture extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param future the result of a CompletableFuture operation. + */ + void accept(T instance, F future); + + default CbFuture andThen(CbFuture after) { + Objects.requireNonNull(after); + return (T instance, F future) -> { + accept(instance, future); + after.accept(instance, future); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbRef.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbRef.java new file mode 100644 index 00000000000..b96a84ff63f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbRef.java @@ -0,0 +1,47 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.osgi.framework.ServiceReference; + +/** + * Represents a callback(ServiceReference) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbRef extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param ref a Service Reference + */ + void accept(T instance, ServiceReference ref); + + default CbRef andThen(CbRef after) { + Objects.requireNonNull(after); + return (T instance, ServiceReference ref) -> { + accept(instance, ref); + after.accept(instance, ref); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbRefComponent.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbRefComponent.java new file mode 100644 index 00000000000..ab7d76b6001 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbRefComponent.java @@ -0,0 +1,49 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.apache.felix.dm.Component; +import org.osgi.framework.ServiceReference; + +/** + * Represents a callback(ServiceReference, Component) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbRefComponent extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param ref a Service Reference + * @param comp a Component + */ + void accept(T instance, ServiceReference ref, Component comp); + + default CbRefComponent andThen(CbRefComponent after) { + Objects.requireNonNull(after); + return (T instance, ServiceReference ref, Component comp) -> { + accept(instance, ref, comp); + after.accept(instance, ref, comp); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbRefRef.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbRefRef.java new file mode 100644 index 00000000000..5830f16979d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbRefRef.java @@ -0,0 +1,48 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.osgi.framework.ServiceReference; + +/** + * Represents a callback(ServiceReference, ServiceReference) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbRefRef extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param ref1 first callback arg + * @param ref2 second callback arg + */ + void accept(T instance, ServiceReference ref1, ServiceReference ref2); + + default CbRefRef andThen(CbRefRef after) { + Objects.requireNonNull(after); + return (T instance, ServiceReference ref1, ServiceReference ref2) -> { + accept(instance, ref1, ref2); + after.accept(instance, ref1, ref2); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbRefRefComponent.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbRefRefComponent.java new file mode 100644 index 00000000000..29e83454ae1 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbRefRefComponent.java @@ -0,0 +1,50 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.apache.felix.dm.Component; +import org.osgi.framework.ServiceReference; + +/** + * Represents a callback(ServiceReference, ServiceReference, Component) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbRefRefComponent extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param ref1 a Service Reference + * @param ref2 a Service Reference + * @param comp a Component + */ + void accept(T instance, ServiceReference ref1, ServiceReference ref2, Component comp); + + default CbRefRefComponent andThen(CbRefRefComponent after) { + Objects.requireNonNull(after); + return (T instance, ServiceReference ref1, ServiceReference ref2, Component comp) -> { + accept(instance, ref1, ref2, comp); + after.accept(instance, ref1, ref2, comp); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbRefServiceRefService.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbRefServiceRefService.java new file mode 100644 index 00000000000..3d519a3f280 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbRefServiceRefService.java @@ -0,0 +1,50 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.osgi.framework.ServiceReference; + +/** + * Represents a callback(ServiceReference, Service, ServiceReference, Service) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbRefServiceRefService extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param oldRef first callback param + * @param old second callback param + * @param replaceRef third callback param + * @param replace fourth callback param + */ + void accept(T instance, ServiceReference oldRef, S old, ServiceReference replaceRef, S replace); + + default CbRefServiceRefService andThen(CbRefServiceRefService after) { + Objects.requireNonNull(after); + return (T instance, ServiceReference oldRef, S old, ServiceReference replaceRef, S replace) -> { + accept(instance, oldRef, old, replaceRef, replace); + after.accept(instance, oldRef, old, replaceRef, replace); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbRefServiceRefServiceComponent.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbRefServiceRefServiceComponent.java new file mode 100644 index 00000000000..2161ebfc777 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbRefServiceRefServiceComponent.java @@ -0,0 +1,52 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.apache.felix.dm.Component; +import org.osgi.framework.ServiceReference; + +/** + * Represents a callback(ServiceReference, Service, ServiceReference, Service, Component) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbRefServiceRefServiceComponent extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param oldRef first callback arg + * @param old second callback arg + * @param replaceRef third callback arg + * @param replace fourth callback arg + * @param c fifth callback arg + */ + void accept(T instance, ServiceReference oldRef, S old, ServiceReference replaceRef, S replace, Component c); + + default CbRefServiceRefServiceComponent andThen(CbRefServiceRefServiceComponent after) { + Objects.requireNonNull(after); + return (T instance, ServiceReference oldRef, S old, ServiceReference replaceRef, S replace, Component c) -> { + accept(instance, oldRef, old, replaceRef, replace, c); + after.accept(instance, oldRef, old, replaceRef, replace, c); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbService.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbService.java new file mode 100644 index 00000000000..97ed19c3da6 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbService.java @@ -0,0 +1,45 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +/** + * Represents a callback(Service) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbService extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param service the callback argument. + */ + void accept(T instance, S service); + + default CbService andThen(CbService after) { + Objects.requireNonNull(after); + return (T instance, S service) -> { + accept(instance, service); + after.accept(instance, service); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceComponent.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceComponent.java new file mode 100644 index 00000000000..df8632f3a34 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceComponent.java @@ -0,0 +1,48 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.apache.felix.dm.Component; + +/** + * Represents a callback(Service, Component) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbServiceComponent extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param service the first callback argument + * @param c the second callback argument + */ + void accept(T instance, S service, Component c); + + default CbServiceComponent andThen(CbServiceComponent after) { + Objects.requireNonNull(after); + return (T instance, S s, Component c) -> { + accept(instance, s, c); + after.accept(instance, s, c); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceComponentRef.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceComponentRef.java new file mode 100644 index 00000000000..fdf20231e0e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceComponentRef.java @@ -0,0 +1,50 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.apache.felix.dm.Component; +import org.osgi.framework.ServiceReference; + +/** + * Represents a callback(Service, Component, ServiceReference) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbServiceComponentRef extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param service the first callback parameter + * @param c the second callback parameter + * @param ref the third callback parameter + */ + void accept(T instance, S service, Component c, ServiceReference ref); + + default CbServiceComponentRef andThen(CbServiceComponentRef after) { + Objects.requireNonNull(after); + return (T instance, S service, Component c, ServiceReference ref) -> { + accept(instance, service, c, ref); + after.accept(instance, service, c, ref); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceDict.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceDict.java new file mode 100644 index 00000000000..a57ce17f991 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceDict.java @@ -0,0 +1,47 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Dictionary; +import java.util.Objects; + +/** + * Represents a callback(Service, Dictionary) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbServiceDict extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param service first callback arg + * @param properties second callback arg + */ + void accept(T instance, S service, Dictionary properties); + + default CbServiceDict andThen(CbServiceDict after) { + Objects.requireNonNull(after); + return (T instance, S service, Dictionary properties) -> { + accept(instance, service, properties); + after.accept(instance, service, properties); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceMap.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceMap.java new file mode 100644 index 00000000000..6a20bc19395 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceMap.java @@ -0,0 +1,47 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Map; +import java.util.Objects; + +/** + * Represents a callback(Service, Map) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbServiceMap extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param service first callback arg + * @param properties second callback arg + */ + void accept(T instance, S service, Map properties); + + default CbServiceMap andThen(CbServiceMap after) { + Objects.requireNonNull(after); + return (T instance, S service, Map properties) -> { + accept(instance, service, properties); + after.accept(instance, service, properties); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceObjects.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceObjects.java new file mode 100644 index 00000000000..3d991b236bf --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceObjects.java @@ -0,0 +1,47 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.osgi.framework.ServiceObjects; + +/** + * Represents a callback(ServiceObjects) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbServiceObjects extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param ref a Service Reference + */ + void accept(T instance, ServiceObjects ref); + + default CbServiceObjects andThen(CbServiceObjects after) { + Objects.requireNonNull(after); + return (T instance, ServiceObjects ref) -> { + accept(instance, ref); + after.accept(instance, ref); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceObjectsServiceObjects.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceObjectsServiceObjects.java new file mode 100644 index 00000000000..24e4fe142fc --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceObjectsServiceObjects.java @@ -0,0 +1,48 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.osgi.framework.ServiceObjects; + +/** + * Represents a callback(ServiceObjects, ServiceObjects) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbServiceObjectsServiceObjects extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param ref1 first callback arg + * @param ref2 second callback arg + */ + void accept(T instance, ServiceObjects ref1, ServiceObjects ref2); + + default CbServiceObjectsServiceObjects andThen(CbServiceObjectsServiceObjects after) { + Objects.requireNonNull(after); + return (T instance, ServiceObjects ref1, ServiceObjects ref2) -> { + accept(instance, ref1, ref2); + after.accept(instance, ref1, ref2); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceRef.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceRef.java new file mode 100644 index 00000000000..e787ea42ab7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceRef.java @@ -0,0 +1,48 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.osgi.framework.ServiceReference; + +/** + * Represents a callback(Service, ServiceReference) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbServiceRef extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param service first callback arg + * @param ref second callback arg + */ + void accept(T instance, S service, ServiceReference ref); + + default CbServiceRef andThen(CbServiceRef after) { + Objects.requireNonNull(after); + return (T instance, S service, ServiceReference ref) -> { + accept(instance, service, ref); + after.accept(instance, service, ref); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceService.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceService.java new file mode 100644 index 00000000000..f4dd6a5ecd5 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceService.java @@ -0,0 +1,46 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +/** + * Represents a swap callback(Service, Service) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbServiceService extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param old first callback arg + * @param replace second callback arg + */ + void accept(T instance, S old, S replace); + + default CbServiceService andThen(CbServiceService after) { + Objects.requireNonNull(after); + return (T instance, S old, S replace) -> { + accept(instance, old, replace); + after.accept(instance, old, replace); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceServiceComponent.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceServiceComponent.java new file mode 100644 index 00000000000..94284e95633 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/CbServiceServiceComponent.java @@ -0,0 +1,49 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.apache.felix.dm.Component; + +/** + * Represents a swap callback(Service, Service, Component) that is invoked on a Component implementation class. + * The type of the class on which the callback is invoked on is represented by the T generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface CbServiceServiceComponent extends SerializableLambda { + /** + * Handles the given arguments. + * @param instance the Component implementation instance on which the callback is invoked on. + * @param old first callback arg + * @param replace second callback arg + * @param c third callback arg + */ + void accept(T instance, S old, S replace, Component c); + + default CbServiceServiceComponent andThen(CbServiceServiceComponent after) { + Objects.requireNonNull(after); + return (T instance, S old, S replace, Component c) -> { + accept(instance, old, replace, c); + after.accept(instance, old, replace, c); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCb.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCb.java new file mode 100644 index 00000000000..47da62c7beb --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCb.java @@ -0,0 +1,42 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +/** + * Represents a method reference to a no-args callback method from an arbitrary Object instance. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCb { + /** + * Implements the callback method. + */ + void callback(); + + default InstanceCb andThen(InstanceCb after) { + Objects.requireNonNull(after); + return () -> { + callback(); + after.callback(); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbBundle.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbBundle.java new file mode 100644 index 00000000000..4fe29bbf3df --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbBundle.java @@ -0,0 +1,45 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.osgi.framework.Bundle; + +/** + * Represents a callback(Bundle) on an Object instance. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbBundle extends SerializableLambda { + /** + * Handles the given argument. + * @param bundle the callback parameter + */ + void accept(Bundle bundle); + + default InstanceCbBundle andThen(InstanceCbBundle after) { + Objects.requireNonNull(after); + return (Bundle bundle) -> { + accept(bundle); + after.accept(bundle); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbBundleComponent.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbBundleComponent.java new file mode 100644 index 00000000000..700e4a5e7cd --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbBundleComponent.java @@ -0,0 +1,47 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.apache.felix.dm.Component; +import org.osgi.framework.Bundle; + +/** + * Represents a callback(Bundle, Component) on an Object instance. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbBundleComponent { + /** + * Handles the given arguments. + * @param component the callback parameter + * @param bundle the callback parameter + */ + void accept(Bundle bundle, Component component); + + default InstanceCbBundleComponent andThen(InstanceCbBundleComponent after) { + Objects.requireNonNull(after); + return (Bundle bundle, Component component) -> { + accept(bundle, component); + after.accept(bundle, component); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbComponent.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbComponent.java new file mode 100644 index 00000000000..a8b3b3170a6 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbComponent.java @@ -0,0 +1,45 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.apache.felix.dm.Component; + +/** + * Represents a callback(Component) invoked on an Object instance. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbComponent { + /** + * Handles the given argument. + * @param component the callback parameter + */ + void accept(Component component); + + default InstanceCbComponent andThen(InstanceCbComponent after) { + Objects.requireNonNull(after); + return (Component component) -> { + accept(component); + after.accept(component); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbConfiguration.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbConfiguration.java new file mode 100644 index 00000000000..1d0ea3f5763 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbConfiguration.java @@ -0,0 +1,52 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Dictionary; +import java.util.Objects; + +import org.apache.felix.dm.lambda.ConfigurationDependencyBuilder; + +/** + * Represents a reference to a callback on an Object instance that takes Configuration type as argument. + * + *

      The T generic parameter represents the type of the configuration class passed to the callback argument. + * + *

      Using such callback provides a way for creating type-safe configurations from the actual {@link Dictionary} that is + * normally injected by Dependency Manager. + * For more information about configuration types, please refer to {@link ConfigurationDependencyBuilder}. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbConfiguration extends SerializableLambda { + /** + * Handles the given argument. + * @param configType the configuration type + */ + void accept(T configType); + + default InstanceCbConfiguration andThen(InstanceCbConfiguration after) { + Objects.requireNonNull(after); + return (T configProxy) -> { + accept(configProxy); + after.accept(configProxy); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbConfigurationComponent.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbConfigurationComponent.java new file mode 100644 index 00000000000..6a983c1ebb1 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbConfigurationComponent.java @@ -0,0 +1,54 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Dictionary; +import java.util.Objects; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.lambda.ConfigurationDependencyBuilder; + +/** + * Represents a callback(Configuration, Component) invoked on an Object instance. + * + *

      The T generic parameter represents the type of the configuration class passed to the callback argument. + * + *

      Using such callback provides a way for creating type-safe configurations from the actual {@link Dictionary} that is + * normally injected by Dependency Manager. + * For more information about configuration types, please refer to {@link ConfigurationDependencyBuilder}. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbConfigurationComponent extends SerializableLambda { + /** + * Handles the given arguments + * @param configType the configuration type + * @param component the callback Component + */ + void accept(T configType, Component component); + + default InstanceCbConfigurationComponent andThen(InstanceCbConfigurationComponent after) { + Objects.requireNonNull(after); + return (T instance, Component component) -> { + accept(instance, component); + after.accept(instance, component); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbDictionary.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbDictionary.java new file mode 100644 index 00000000000..28cf2d0fbdd --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbDictionary.java @@ -0,0 +1,44 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Dictionary; +import java.util.Objects; + +/** + * Represents a callback(Dictionary) on an Object instance. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbDictionary { + /** + * Handles the given argument. + * @param conf the properties + */ + void accept(Dictionary conf); + + default InstanceCbDictionary andThen(InstanceCbDictionary after) { + Objects.requireNonNull(after); + return (Dictionary conf) -> { + accept(conf); + after.accept(conf); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbDictionaryComponent.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbDictionaryComponent.java new file mode 100644 index 00000000000..cddbfddea4f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbDictionaryComponent.java @@ -0,0 +1,47 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Dictionary; +import java.util.Objects; + +import org.apache.felix.dm.Component; + +/** + * Represents a callback(Dictionary, Component) on an Object instance. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbDictionaryComponent { + /** + * Handles the given arguments. + * @param properties some service properties + * @param component a Component + */ + void accept(Dictionary properties, Component component); + + default InstanceCbDictionaryComponent andThen(InstanceCbDictionaryComponent after) { + Objects.requireNonNull(after); + return (Dictionary properties, Component component) -> { + accept(properties, component); + after.accept(properties, component); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbFuture.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbFuture.java new file mode 100644 index 00000000000..d12a9d47db7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbFuture.java @@ -0,0 +1,43 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +/** + * Represents a callback that accepts the result of a CompletableFuture. The callback is invoked on an Object instance. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbFuture { + /** + * Handles the result of a CompletableFuture operation. + * @param future the result of a CompletableFuture operation. + */ + void accept(F future); + + default InstanceCbFuture andThen(InstanceCbFuture after) { + Objects.requireNonNull(after); + return (F f) -> { + accept(f); + after.accept(f); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbRef.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbRef.java new file mode 100644 index 00000000000..59f64370c7c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbRef.java @@ -0,0 +1,48 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.osgi.framework.ServiceReference; + +/** + * Represents a callback(ServiceReference) on an Object instance. + * + *

      The type of the service passed in argument to the callback is defined by the "S" generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbRef { + /** + * Handles the given arguments. + * @param ref a Service Reference + * @param service a Service + */ + void accept(ServiceReference ref); + + default InstanceCbRef andThen(InstanceCbRef after) { + Objects.requireNonNull(after); + return (ServiceReference ref) -> { + accept(ref); + after.accept(ref); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbRefComponent.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbRefComponent.java new file mode 100644 index 00000000000..aac384d6ed9 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbRefComponent.java @@ -0,0 +1,49 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.apache.felix.dm.Component; +import org.osgi.framework.ServiceReference; + +/** + * Represents a callback(ServiceReference, Component) on an Object instance. + * + *

      The type of the service passed in argument to the callback is defined by the "S" generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbRefComponent { + /** + * Handles the given arguments. + * @param ref a Service Reference + * @param comp a Component + */ + void accept(ServiceReference ref, Component comp); + + default InstanceCbRefComponent andThen(InstanceCbRefComponent after) { + Objects.requireNonNull(after); + return (ServiceReference ref, Component comp) -> { + accept(ref, comp); + after.accept(ref, comp); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbRefRef.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbRefRef.java new file mode 100644 index 00000000000..08d681b2d16 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbRefRef.java @@ -0,0 +1,48 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.osgi.framework.ServiceReference; + +/** + * Represents a swap callback(ServiceReference, ServiceReference) on an Object instance. + * + *

      The type of the service passed in argument to the callback is defined by the "S" generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbRefRef extends SerializableLambda { + /** + * Handles the given argument + * @param ref1 a Service Reference + * @param ref2 a Service Reference + */ + void accept(ServiceReference ref1, ServiceReference ref2); + + default InstanceCbRefRef andThen(InstanceCbRefRef after) { + Objects.requireNonNull(after); + return (ServiceReference ref1, ServiceReference ref2) -> { + accept(ref1, ref2); + after.accept(ref1, ref2); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbRefRefComponent.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbRefRefComponent.java new file mode 100644 index 00000000000..f5d0a9ad927 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbRefRefComponent.java @@ -0,0 +1,50 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.apache.felix.dm.Component; +import org.osgi.framework.ServiceReference; + +/** + * Represents a callback(ServiceReference, ServiceReference, Component) on an Object instance. + * + *

      The type of the service passed in argument to the callback is defined by the "S" generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbRefRefComponent { + /** + * Handles the given arguments. + * @param ref1 first service reference + * @param ref2 second service reference + * @param comp a Component + */ + void accept(ServiceReference ref1, ServiceReference ref2, Component comp); + + default InstanceCbRefRefComponent andThen(InstanceCbRefRefComponent after) { + Objects.requireNonNull(after); + return (ServiceReference ref1, ServiceReference ref2, Component comp) -> { + accept(ref1, ref2, comp); + after.accept(ref1, ref2, comp); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbRefServiceRefService.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbRefServiceRefService.java new file mode 100644 index 00000000000..29b97e70e2b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbRefServiceRefService.java @@ -0,0 +1,48 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.osgi.framework.ServiceReference; + +/** + * Represents a swap callback(ServiceReference, Service, ServiceReference, Service) on an Object instance. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbRefServiceRefService { + /** + * Handles the given arguments + * @param oldRef the old service reference + * @param old the old service + * @param replaceRef the replace service reference + * @param replace the replace service + */ + void accept(ServiceReference oldRef, S old, ServiceReference replaceRef, S replace); + + default InstanceCbRefServiceRefService andThen(InstanceCbRefServiceRefService after) { + Objects.requireNonNull(after); + return (ServiceReference oldRef, S old, ServiceReference replaceRef, S replace) -> { + accept(oldRef, old, replaceRef, replace); + after.accept(oldRef, old, replaceRef, replace); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbRefServiceRefServiceComponent.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbRefServiceRefServiceComponent.java new file mode 100644 index 00000000000..b227941b466 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbRefServiceRefServiceComponent.java @@ -0,0 +1,50 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.apache.felix.dm.Component; +import org.osgi.framework.ServiceReference; + +/** + * Represents a swap callback(ServiceReference, Service, ServiceReference, Service, Component) on an Object instance. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbRefServiceRefServiceComponent { + /** + * Handles the given arguments + * @param oldRef an old swapped service reference + * @param old an old swapped service + * @param replaceRef the new service reference + * @param replace the new service + * @param c a Component + */ + void accept(ServiceReference oldRef, S old, ServiceReference replaceRef, S replace, Component c); + + default InstanceCbRefServiceRefServiceComponent andThen(InstanceCbRefServiceRefServiceComponent after) { + Objects.requireNonNull(after); + return (ServiceReference oldRef, S old, ServiceReference replaceRef, S replace, Component c) -> { + accept(oldRef, old, replaceRef, replace, c); + after.accept(oldRef, old, replaceRef, replace, c); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbService.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbService.java new file mode 100644 index 00000000000..ec23afb2723 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbService.java @@ -0,0 +1,45 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +/** + * Represents a callback(Service) on an Object instance. + * + *

      The type of the service passed in argument to the callback is defined by the "S" generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbService { + /** + * Handles the given argument. + * @param service a Service + */ + void accept(S service); + + default InstanceCbService andThen(InstanceCbService after) { + Objects.requireNonNull(after); + return (S service) -> { + accept(service); + after.accept(service); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceComponent.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceComponent.java new file mode 100644 index 00000000000..be574b03651 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceComponent.java @@ -0,0 +1,48 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.apache.felix.dm.Component; + +/** + * Represents a callback(Service, Component) on an Object instance. + * + *

      The type of the service passed in argument to the callback is defined by the "S" generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbServiceComponent { + /** + * Handles the given arguments + * @param c the component + * @param service the service + */ + void accept(S service, Component c); + + default InstanceCbServiceComponent andThen(InstanceCbServiceComponent after) { + Objects.requireNonNull(after); + return (S service, Component c) -> { + accept(service, c); + after.accept(service, c); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceComponentRef.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceComponentRef.java new file mode 100644 index 00000000000..a0e4c17e91f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceComponentRef.java @@ -0,0 +1,50 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.apache.felix.dm.Component; +import org.osgi.framework.ServiceReference; + +/** + * Represents a callback(Service, Component, ServiceReference) on an Object instance. + * + *

      The type of the service passed in argument to the callback is defined by the "S" generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbServiceComponentRef { + /** + * Handles the given arguments. + * @param c a Component + * @param ref the service reference + * @param service the service + */ + void accept(S service, Component c, ServiceReference ref); + + default InstanceCbServiceComponentRef andThen(InstanceCbServiceComponentRef after) { + Objects.requireNonNull(after); + return (S service, Component c, ServiceReference ref) -> { + accept(service, c, ref); + after.accept(service, c, ref); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceDict.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceDict.java new file mode 100644 index 00000000000..f3843bc7687 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceDict.java @@ -0,0 +1,47 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Dictionary; +import java.util.Objects; + +/** + * Represents a callback(Service, Dictionary) on an Object instance. + * + *

      The type of the service passed in argument to the callback is defined by the "S" generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbServiceDict { + /** + * Handles the given arguments. + * @param service a Service + * @param properties a Dictionary + */ + void accept(S service, Dictionary properties); + + default InstanceCbServiceDict andThen(InstanceCbServiceDict after) { + Objects.requireNonNull(after); + return (S service, Dictionary properties) -> { + accept(service, properties); + after.accept(service, properties); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceMap.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceMap.java new file mode 100644 index 00000000000..c47d148bb68 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceMap.java @@ -0,0 +1,47 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Map; +import java.util.Objects; + +/** + * Represents a callback(Service, Map) on an Object instance. + * + *

      The type of the service passed in argument to the callback is defined by the "S" generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbServiceMap { + /** + * Handles the given arguments. + * @param service a Service + * @param properties a Map + */ + void accept(S service, Map properties); + + default InstanceCbServiceMap andThen(InstanceCbServiceMap after) { + Objects.requireNonNull(after); + return (S service, Map properties) -> { + accept(service, properties); + after.accept(service, properties); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceObjects.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceObjects.java new file mode 100644 index 00000000000..b64ad73c73a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceObjects.java @@ -0,0 +1,48 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.osgi.framework.ServiceObjects; + +/** + * Represents a callback(ServiceObjects) on an Object instance. + * + *

      The type of the service passed in argument to the callback is defined by the "S" generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbServiceObjects { + /** + * Handles the given arguments. + * @param ref a Service Reference + * @param service a Service + */ + void accept(ServiceObjects ref); + + default InstanceCbServiceObjects andThen(InstanceCbServiceObjects after) { + Objects.requireNonNull(after); + return (ServiceObjects ref) -> { + accept(ref); + after.accept(ref); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceObjectsServiceObjects.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceObjectsServiceObjects.java new file mode 100644 index 00000000000..d01c9baab3f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceObjectsServiceObjects.java @@ -0,0 +1,48 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.osgi.framework.ServiceObjects; + +/** + * Represents a swap callback(ServoceObjects, ServoceObjects) on an Object instance. + * + *

      The type of the service passed in argument to the callback is defined by the "S" generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbServiceObjectsServiceObjects extends SerializableLambda { + /** + * Handles the given argument + * @param ref1 a Service Reference + * @param ref2 a Service Reference + */ + void accept(ServiceObjects ref1, ServiceObjects ref2); + + default InstanceCbServiceObjectsServiceObjects andThen(InstanceCbServiceObjectsServiceObjects after) { + Objects.requireNonNull(after); + return (ServiceObjects ref1, ServiceObjects ref2) -> { + accept(ref1, ref2); + after.accept(ref1, ref2); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceRef.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceRef.java new file mode 100644 index 00000000000..b6849766fc7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceRef.java @@ -0,0 +1,48 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.osgi.framework.ServiceReference; + +/** + * Represents a callback(Service, ServiceReference) on an Object instance. + * + *

      The type of the service passed in argument to the callback is defined by the "S" generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbServiceRef { + /** + * Handles the given arguments. + * @param ref a Service Reference + * @param service a Service + */ + void accept(S service, ServiceReference ref); + + default InstanceCbServiceRef andThen(InstanceCbServiceRef after) { + Objects.requireNonNull(after); + return (S service, ServiceReference ref) -> { + accept(service, ref); + after.accept(service, ref); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceService.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceService.java new file mode 100644 index 00000000000..1122c7a37fb --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceService.java @@ -0,0 +1,46 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +/** + * Represents a swap callback(Service, Service) on an Object instance. + * + *

      The type of the service passed in argument to the callback is defined by the "S" generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbServiceService extends SerializableLambda { + /** + * Handles the given argument + * @param old a Service + * @param replace a Service + */ + void accept(S old, S replace); + + default InstanceCbServiceService andThen(InstanceCbServiceService after) { + Objects.requireNonNull(after); + return (S old, S replace) -> { + accept(old, replace); + after.accept(old, replace); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceServiceComponent.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceServiceComponent.java new file mode 100644 index 00000000000..da3ae1b683a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/InstanceCbServiceServiceComponent.java @@ -0,0 +1,49 @@ +/* + * 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.dm.lambda.callbacks; + +import java.util.Objects; + +import org.apache.felix.dm.Component; + +/** + * Represents a swap callback(Service, Service, Component) on an Object instance. + * + *

      The type of the service passed in argument to the callback is defined by the "S" generic parameter. + * + * @author Felix Project Team + */ +@FunctionalInterface +public interface InstanceCbServiceServiceComponent extends SerializableLambda { + /** + * Handles the given arguments. + * @param c the component + * @param old the old service + * @param replace the new service + */ + void accept(S old, S replace, Component c); + + default InstanceCbServiceServiceComponent andThen(InstanceCbServiceServiceComponent after) { + Objects.requireNonNull(after); + return (S old, S replace, Component c) -> { + accept(old, replace, c); + after.accept(old, replace, c); + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/SerializableLambda.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/SerializableLambda.java new file mode 100644 index 00000000000..bdcbd46f637 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/SerializableLambda.java @@ -0,0 +1,27 @@ +/* + * 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.dm.lambda.callbacks; + +import java.io.Serializable; + +/** + * Base interface for serializable lambdas. Some lambda must be serializable in order to allow to introspect their type and method signatures. + */ +public interface SerializableLambda extends Serializable { +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/packageinfo b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/packageinfo new file mode 100644 index 00000000000..897578ff5b3 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/callbacks/packageinfo @@ -0,0 +1 @@ +version 1.2.0 diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/AdapterBase.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/AdapterBase.java new file mode 100644 index 00000000000..af1ee5e9552 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/AdapterBase.java @@ -0,0 +1,323 @@ +/* + * 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.dm.lambda.impl; + +import java.util.Dictionary; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.apache.felix.dm.Component.ServiceScope; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.lambda.BundleDependencyBuilder; +import org.apache.felix.dm.lambda.ComponentBuilder; +import org.apache.felix.dm.lambda.ConfigurationDependencyBuilder; +import org.apache.felix.dm.lambda.FluentProperty; +import org.apache.felix.dm.lambda.FutureDependencyBuilder; +import org.apache.felix.dm.lambda.ServiceDependencyBuilder; +import org.apache.felix.dm.lambda.callbacks.InstanceCb; +import org.apache.felix.dm.lambda.callbacks.InstanceCbComponent; + +/** + * Methods common to extended components like adapters or aspects. + * + * TODO javadoc + */ +@SuppressWarnings({"unchecked"}) +public interface AdapterBase> extends ComponentBuilder { + + void andThenBuild(Consumer> builder); + + default B scope(ServiceScope scope) { + andThenBuild(compBuilder -> compBuilder.scope(scope)); + return (B) this; + } + + default B impl(Object impl) { + andThenBuild(compBuilder -> compBuilder.impl(impl)); + return (B) this; + } + + default B impl(Class implClass) { + andThenBuild(compBuilder -> compBuilder.impl(implClass)); + return (B) this; + } + + default B factory(Object factory, String createMethod) { + andThenBuild(compBuilder -> compBuilder.factory(factory, createMethod)); + return (B) this; + } + + default B factory(Supplier create) { + andThenBuild(compBuilder -> compBuilder.factory(create)); + return (B) this; + } + + default B factory(Supplier factory, Function create) { + andThenBuild(compBuilder -> compBuilder.factory(factory, create)); + return (B) this; + } + + default B factory(Supplier factory, Supplier getComposition) { + andThenBuild(compBuilder -> compBuilder.factory(factory, getComposition)); + return (B) this; + } + + default B factory(Supplier factory, Function create, Function getComposition) { + andThenBuild(compBuilder -> compBuilder.factory(factory, create, getComposition)); + return (B) this; + } + + default B provides(Class iface) { + andThenBuild(compBuilder -> compBuilder.provides(iface)); + return (B) this; + } + + default B provides(Class iface, String name, Object value, Object ... rest) { + andThenBuild(compBuilder -> compBuilder.provides(iface, name, value, rest)); + return (B) this; + } + + default B provides(Class iface, FluentProperty ... properties) { + andThenBuild(compBuilder -> compBuilder.provides(iface, properties)); + return (B) this; + } + + default B provides(Class iface, Dictionary properties) { + andThenBuild(compBuilder -> compBuilder.provides(iface, properties)); + return (B) this; + } + + default B provides(Class[] ifaces) { + andThenBuild(compBuilder -> compBuilder.provides(ifaces)); + return (B) this; + } + + default B provides(Class[] ifaces, String name, Object value, Object ... rest) { + andThenBuild(compBuilder -> compBuilder.provides(ifaces, name, value, rest)); + return (B) this; + } + + default B provides(Class[] ifaces, FluentProperty ... properties) { + andThenBuild(compBuilder -> compBuilder.provides(ifaces, properties)); + return (B) this; + } + + default B provides(Class[] ifaces, Dictionary properties) { + andThenBuild(compBuilder -> compBuilder.provides(ifaces, properties)); + return (B) this; + } + + default B provides(String iface) { + andThenBuild(compBuilder -> compBuilder.provides(iface)); + return (B) this; + } + + default B provides(String iface, String name, Object value, Object ... rest) { + andThenBuild(compBuilder -> compBuilder.provides(iface, name, value, rest)); + return (B) this; + } + + default B provides(String iface, FluentProperty ... properties) { + andThenBuild(compBuilder -> compBuilder.provides(iface, properties)); + return (B) this; + } + + default B provides(String iface, Dictionary properties) { + andThenBuild(compBuilder -> compBuilder.provides(iface, properties)); + return (B) this; + } + + default B provides(String[] ifaces) { + andThenBuild(compBuilder -> compBuilder.provides(ifaces)); + return (B) this; + } + + default B provides(String[] ifaces, String name, Object value, Object ... rest) { + andThenBuild(compBuilder -> compBuilder.provides(ifaces, name, value, rest)); + return (B) this; + } + + default B provides(String[] ifaces, FluentProperty ... properties) { + andThenBuild(compBuilder -> compBuilder.provides(ifaces, properties)); + return (B) this; + } + + default B provides(String[] ifaces, Dictionary properties) { + andThenBuild(compBuilder -> compBuilder.provides(ifaces, properties)); + return (B) this; + } + + default B properties(Dictionary properties) { + andThenBuild(compBuilder -> compBuilder.properties(properties)); + return (B) this; + } + + default B properties(String name, Object value, Object ... rest) { + andThenBuild(compBuilder -> compBuilder.properties(name, value, rest)); + return (B) this; + } + + default B properties(FluentProperty ...properties) { + andThenBuild(compBuilder -> compBuilder.properties(properties)); + return (B) this; + } + + default B withDep(Dependency dependency) { + andThenBuild(compBuilder -> compBuilder.withDep(dependency)); + return (B) this; + } + + default B withSvc(Class service, Consumer> consumer) { + andThenBuild(compBuilder -> compBuilder.withSvc(service, consumer)); + return (B) this; + } + + default B withCnf(Consumer consumer) { + andThenBuild(compBuilder -> compBuilder.withCnf(consumer)); + return (B) this; + } + + default B withBundle(Consumer consumer) { + andThenBuild(compBuilder -> compBuilder.withBundle(consumer)); + return (B) this; + } + + default B withFuture(CompletableFuture future, Consumer> consumer) { + andThenBuild(compBuilder -> compBuilder.withFuture(future, consumer)); + return (B) this; + } + + default B init(String callback) { + andThenBuild(compBuilder -> compBuilder.init(callback)); + return (B) this; + } + + default B init(Object callbackInstance, String callback) { + andThenBuild(compBuilder -> compBuilder.init(callbackInstance, callback)); + return (B) this; + } + + default B init(InstanceCb callback) { + andThenBuild(compBuilder -> compBuilder.init(callback)); + return (B) this; + } + + default B init(InstanceCbComponent callback) { + andThenBuild(compBuilder -> compBuilder.init(callback)); + return (B) this; + } + + default B start(String callback) { + andThenBuild(compBuilder -> compBuilder.start(callback)); + return (B) this; + } + + default B start(Object callbackInstance, String callback) { + andThenBuild(compBuilder -> compBuilder.start(callbackInstance, callback)); + return (B) this; + } + + default B start(InstanceCb callback) { + andThenBuild(compBuilder -> compBuilder.start(callback)); + return (B) this; + } + + default B start(InstanceCbComponent callback) { + andThenBuild(compBuilder -> compBuilder.start(callback)); + return (B) this; + } + + default B stop(String callback) { + andThenBuild(compBuilder -> compBuilder.stop(callback)); + return (B) this; + } + + default B stop(Object callbackInstance, String callback) { + andThenBuild(compBuilder -> compBuilder.stop(callbackInstance, callback)); + return (B) this; + } + + default B stop(InstanceCb callback) { + andThenBuild(compBuilder -> compBuilder.stop(callback)); + return (B) this; + } + + default B stop(InstanceCbComponent callback) { + andThenBuild(compBuilder -> compBuilder.stop(callback)); + return (B) this; + } + + default B destroy(String callback) { + andThenBuild(compBuilder -> compBuilder.destroy(callback)); + return (B) this; + } + + default B destroy(Object callbackInstance, String callback) { + andThenBuild(compBuilder -> compBuilder.destroy(callbackInstance, callback)); + return (B) this; + } + + default B destroy(InstanceCb callback) { + andThenBuild(compBuilder -> compBuilder.destroy(callback)); + return (B) this; + } + + default B destroy(InstanceCbComponent callback) { + andThenBuild(compBuilder -> compBuilder.destroy(callback)); + return (B) this; + } + + default B autoConfig(Class clazz, boolean autoConfig) { + andThenBuild(compBuilder -> compBuilder.autoConfig(clazz, autoConfig)); + return (B) this; + } + + default B autoConfig(Class clazz, String field) { + andThenBuild(compBuilder -> compBuilder.autoConfig(clazz, field)); + return (B) this; + } + + default B debug(String label) { + andThenBuild(compBuilder -> compBuilder.debug(label)); + return (B) this; + } + + default B composition(String getCompositionMethod) { + andThenBuild(compBuilder -> compBuilder.composition(getCompositionMethod)); + return (B) this; + } + + default B composition(Object instance, String getCompositionMethod) { + andThenBuild(compBuilder -> compBuilder.composition(instance, getCompositionMethod)); + return (B) this; + } + + default B composition(Supplier getCompositionMethod) { + andThenBuild(compBuilder -> compBuilder.composition(getCompositionMethod)); + return (B) this; + } + + default B listener(ComponentStateListener listener) { + andThenBuild(compBuilder -> compBuilder.listener(listener)); + return (B) this; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/BundleAdapterBuilderImpl.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/BundleAdapterBuilderImpl.java new file mode 100644 index 00000000000..87f2035a7c7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/BundleAdapterBuilderImpl.java @@ -0,0 +1,286 @@ +/* + * 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.dm.lambda.impl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.lambda.BundleAdapterBuilder; +import org.apache.felix.dm.lambda.ComponentBuilder; +import org.apache.felix.dm.lambda.callbacks.CbBundle; +import org.apache.felix.dm.lambda.callbacks.CbBundleComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbBundle; +import org.apache.felix.dm.lambda.callbacks.InstanceCbBundleComponent; +import org.osgi.framework.Bundle; + +public class BundleAdapterBuilderImpl implements AdapterBase, BundleAdapterBuilder { + private Consumer> m_compBuilder = (compBuilder -> {}); + protected final Map>> m_refs = new HashMap<>(); + private DependencyManager m_dm; + private boolean m_autoAdd; + private String m_filter; + private int m_stateMask = -1; + private boolean m_propagate; + private Object m_callbackInstance; + private String m_add; + private String m_change; + private String m_remove; + + enum Cb { + ADD, + CHG, + REM + }; + + @FunctionalInterface + interface MethodRef { + public void accept(I instance, Component c, Bundle b); + } + + public BundleAdapterBuilderImpl(DependencyManager dm) { + m_dm = dm; + } + + public void andThenBuild(Consumer> builder) { + m_compBuilder = m_compBuilder.andThen(builder); + } + + @Override + public BundleAdapterBuilderImpl autoAdd(boolean autoAdd) { + m_autoAdd = autoAdd; + return this; + } + + public boolean isAutoAdd() { + return m_autoAdd; + } + + public BundleAdapterBuilder mask(int mask) { + m_stateMask = mask; + return this; + } + + public BundleAdapterBuilder filter(String filter) { + m_filter = filter; + return this; + } + + public BundleAdapterBuilder propagate(boolean propagate) { + m_propagate = propagate; + return this; + } + + public BundleAdapterBuilder propagate() { + m_propagate = true; + return this; + } + + public BundleAdapterBuilder add(String callback) { + return callbacks(callback, null, null); + } + + public BundleAdapterBuilder change(String callback) { + return callbacks(null, callback, null); + } + + public BundleAdapterBuilder remove(String callback) { + return callbacks(null, null, callback); + } + + public BundleAdapterBuilder callbackInstance(Object callbackInstance) { + m_callbackInstance = callbackInstance; + return this; + } + + private BundleAdapterBuilder callbacks(String add, String change, String remove) { + checkHasNoMethodRefs(); + m_add = add != null ? add : m_add; + m_change = change != null ? change : m_change; + m_remove = remove != null ? remove : m_remove; + return this; + } + + public BundleAdapterBuilder add(CbBundle add) { + return callbacks(add, (CbBundle) null, (CbBundle) null); + } + + public BundleAdapterBuilder change(CbBundle change) { + return callbacks(null, change, null); + } + + public BundleAdapterBuilder remove(CbBundle remove) { + return callbacks(null, null, remove); + } + + private BundleAdapterBuilder callbacks(CbBundle add, CbBundle change, CbBundle remove) { + if (add != null) { + Class type = Helpers.getLambdaArgType(add, 0); + setComponentCallbackRef(Cb.ADD, type, (instance, component, bundle) -> { add.accept((T) instance, bundle); }); + } + if (change != null) { + Class type = Helpers.getLambdaArgType(change, 0); + setComponentCallbackRef(Cb.CHG, type, (instance, component, bundle) -> { change.accept((T) instance, bundle); }); + } + if (remove != null) { + Class type = Helpers.getLambdaArgType(remove, 0); + setComponentCallbackRef(Cb.REM, type, (instance, component, bundle) -> { remove.accept((T) instance, bundle); }); + } + return this; + } + + public BundleAdapterBuilder add(InstanceCbBundle add) { + return callbacks(add, null, null); + } + + public BundleAdapterBuilder change(InstanceCbBundle change) { + return callbacks(null, change, null); + } + + public BundleAdapterBuilder remove(InstanceCbBundle remove) { + return callbacks(null, null, remove); + } + + public BundleAdapterBuilder callbacks(InstanceCbBundle add, InstanceCbBundle change, InstanceCbBundle remove) { + if (add != null) setInstanceCallbackRef(Cb.ADD, (instance, component, bundle) -> { add.accept(bundle); }); + if (change != null) setInstanceCallbackRef(Cb.CHG, (instance, component, bundle) -> { change.accept(bundle); }); + if (remove != null) setInstanceCallbackRef(Cb.REM, (instance, component, bundle) -> { remove.accept(bundle); }); + return this; + } + + public BundleAdapterBuilder add(CbBundleComponent add) { + return callbacks(add, null, null); + } + + public BundleAdapterBuilder change(CbBundleComponent change) { + return callbacks(null, change, null); + } + + public BundleAdapterBuilder remove(CbBundleComponent remove) { + return callbacks(null, null, remove); + } + + public BundleAdapterBuilder callbacks(CbBundleComponent add, CbBundleComponent change, CbBundleComponent remove) { + if (add != null) { + Class type = Helpers.getLambdaArgType(add, 0); + return setComponentCallbackRef(Cb.ADD, type, (instance, component, bundle) -> { add.accept((T) instance, bundle, component); }); + } + if (change != null) { + Class type = Helpers.getLambdaArgType(change, 0); + return setComponentCallbackRef(Cb.CHG, type, (instance, component, bundle) -> { change.accept((T) instance, bundle, component); }); + } + if (remove != null) { + Class type = Helpers.getLambdaArgType(remove, 0); + return setComponentCallbackRef(Cb.ADD, type, (instance, component, bundle) -> { remove.accept((T) instance, bundle, component); }); + } + return this; + } + + public BundleAdapterBuilder add(InstanceCbBundleComponent add) { + return callbacks(add, null, null); + } + + public BundleAdapterBuilder change(InstanceCbBundleComponent change) { + return callbacks(null, change, null); + } + + public BundleAdapterBuilder remove(InstanceCbBundleComponent remove) { + return callbacks(null, null, remove); + } + + public BundleAdapterBuilder callbacks(InstanceCbBundleComponent add, InstanceCbBundleComponent change, InstanceCbBundleComponent remove) { + if (add != null) setInstanceCallbackRef(Cb.ADD, (instance, component, bundle) -> { add.accept(bundle, component); }); + if (change != null) setInstanceCallbackRef(Cb.CHG, (instance, component, bundle) -> { change.accept(bundle, component); }); + if (remove != null) setInstanceCallbackRef(Cb.REM, (instance, component, bundle) -> { remove.accept(bundle, component); }); + return this; + } + + @Override + public Component build() { + Component c = null; + + if (m_refs.size() > 0) { + @SuppressWarnings("unused") + Object wrapCallback = new Object() { + public void add(Component comp, Bundle bundle) { + invokeMethodRefs(Cb.ADD, comp, bundle); + } + + public void change(Component comp, Bundle bundle) { + invokeMethodRefs(Cb.CHG, comp, bundle); + } + + public void remove(Component comp, Bundle bundle) { + invokeMethodRefs(Cb.REM, comp, bundle); + } + }; + c = m_dm.createBundleAdapterService(m_stateMask, m_filter, m_propagate, wrapCallback, "add", "change", "remove"); + } else { + c = m_dm.createBundleAdapterService(m_stateMask, m_filter, m_propagate, m_callbackInstance, m_add, m_change, m_remove); + } + ComponentBuilderImpl cb = new ComponentBuilderImpl(c, false); + m_compBuilder.accept (cb); + return cb.build(); + } + + private BundleAdapterBuilder setInstanceCallbackRef(Cb cbType, MethodRef ref) { + checkHasNoReflectionCallbacks(); + List> list = m_refs.computeIfAbsent(cbType, l -> new ArrayList<>()); + list.add((instance, component, bundle) -> ref.accept(null, component, bundle)); + return this; + } + + @SuppressWarnings("unchecked") + private BundleAdapterBuilder setComponentCallbackRef(Cb cbType, Class type, MethodRef ref) { + checkHasNoReflectionCallbacks(); + List> list = m_refs.computeIfAbsent(cbType, l -> new ArrayList<>()); + list.add((instance, component, bundle) -> { + Object componentImpl = Stream.of(component.getInstances()) + .filter(impl -> type.isAssignableFrom(Helpers.getClass(impl))) + .findFirst() + .orElseThrow(() -> new IllegalStateException("The method reference " + ref + " does not match any available component impl classes.")); + ref.accept((U) componentImpl, component, bundle); + }); + return this; + } + + private void invokeMethodRefs(Cb cbType, Component comp, Bundle bundle) { + m_refs.computeIfPresent(cbType, (k, mrefs) -> { + mrefs.forEach(mref -> mref.accept(null, comp, bundle)); + return mrefs; + }); + } + + private void checkHasNoMethodRefs() { + if (m_refs.size() > 0) { + throw new IllegalStateException("Can't mix method references with reflection based callbacks"); + } + } + + private void checkHasNoReflectionCallbacks() { + if (m_add != null || m_change != null || m_remove != null) { + throw new IllegalStateException("Can't mix method references with reflection based callbacks"); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/BundleDependencyBuilderImpl.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/BundleDependencyBuilderImpl.java new file mode 100644 index 00000000000..614eeecde31 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/BundleDependencyBuilderImpl.java @@ -0,0 +1,399 @@ +/* + * 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.dm.lambda.impl; + +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.apache.felix.dm.BundleDependency; +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.lambda.BundleDependencyBuilder; +import org.apache.felix.dm.lambda.callbacks.CbBundle; +import org.apache.felix.dm.lambda.callbacks.CbBundleComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbBundle; +import org.apache.felix.dm.lambda.callbacks.InstanceCbBundleComponent; +import org.osgi.framework.Bundle; + +@SuppressWarnings("unchecked") +public class BundleDependencyBuilderImpl implements BundleDependencyBuilder { + private String m_added; + private String m_changed; + private String m_removed; + private Object m_instance; + private boolean m_autoConfig = true; + private boolean m_autoConfigInvoked = false; + private boolean m_required; + private Bundle m_bundle; + private String m_filter; + private int m_stateMask = -1; + private boolean m_propagate; + private Object m_propagateInstance; + private String m_propagateMethod; + private Function> m_propagateCallback; + private final Component m_component; + private boolean m_requiredSet; + + enum Cb { + ADD, + CHG, + REM + }; + + private final Map>> m_refs = new HashMap<>(); + + @FunctionalInterface + interface MethodRef { + public void accept(I instance, Component c, Bundle bundle); + } + + /** + * Class used to call a supplier that returns Propagated properties + */ + private class Propagate { + @SuppressWarnings("unused") + Dictionary propagate(Bundle bundle) { + return m_propagateCallback.apply(bundle); + } + } + + public BundleDependencyBuilderImpl (Component component) { + m_component = component; + } + + @Override + public BundleDependencyBuilder autoConfig(boolean autoConfig) { + m_autoConfig = autoConfig; + m_autoConfigInvoked = true; + return this; + } + + @Override + public BundleDependencyBuilder autoConfig() { + autoConfig(true); + return this; + } + + @Override + public BundleDependencyBuilder required(boolean required) { + m_required = required; + m_requiredSet = true; + return this; + } + + @Override + public BundleDependencyBuilder optional() { + return required(false); + } + + @Override + public BundleDependencyBuilder required() { + required(true); + return this; + } + + @Override + public BundleDependencyBuilder bundle(Bundle bundle) { + m_bundle = bundle; + return this; + } + + @Override + public BundleDependencyBuilder filter(String filter) throws IllegalArgumentException { + m_filter = filter; + return this; + } + + @Override + public BundleDependencyBuilder mask(int mask) { + m_stateMask = mask; + return this; + } + + @Override + public BundleDependencyBuilder propagate(boolean propagate) { + m_propagate = propagate; + return this; + } + + @Override + public BundleDependencyBuilder propagate() { + propagate(true); + return this; + } + + @Override + public BundleDependencyBuilder propagate(Object instance, String method) { + if (m_propagateCallback != null || m_propagate) throw new IllegalStateException("Propagate callback already set."); + Objects.nonNull(method); + Objects.nonNull(instance); + m_propagateInstance = instance; + m_propagateMethod = method; + return this; + } + + @Override + public BundleDependencyBuilder propagate(Function> instance) { + if (m_propagateInstance != null || m_propagate) throw new IllegalStateException("Propagate callback already set."); + m_propagateCallback = instance; + return this; + } + + @Override + public BundleDependencyBuilder callbackInstance(Object callbackInstance) { + m_instance = callbackInstance; + return this; + } + + @Override + public BundleDependencyBuilder add(String add) { + callbacks(add, null, null); + return this; + } + + @Override + public BundleDependencyBuilder change(String change) { + callbacks(null, change, null); + return this; + } + + @Override + public BundleDependencyBuilder remove(String remove) { + callbacks(null, null, remove); + return this; + } + + private BundleDependencyBuilder callbacks(String added, String changed, String removed) { + requiresNoMethodRefs(); + m_added = added != null ? added : m_added; + m_changed = changed != null ? changed : m_changed; + m_removed = removed != null ? removed : m_removed; + if (! m_autoConfigInvoked) m_autoConfig = false; + return this; + } + + @Override + public BundleDependencyBuilder add(CbBundle add) { + return callbacks(add, null, null); + } + + @Override + public BundleDependencyBuilder change(CbBundle change) { + return callbacks(null, change, null); + } + + @Override + public BundleDependencyBuilder remove(CbBundle remove) { + return callbacks(null, null, remove); + } + + private BundleDependencyBuilder callbacks(CbBundle add, CbBundle change, CbBundle remove) { + if (add != null) { + setComponentCallbackRef(Cb.ADD, Helpers.getLambdaArgType(add, 0), (inst, component, bundle) -> add.accept ((T) inst, bundle)); + } + if (change != null) { + setComponentCallbackRef(Cb.CHG, Helpers.getLambdaArgType(change, 0), (inst, component, bundle) -> change.accept ((T) inst, bundle)); + } + if (remove != null) { + setComponentCallbackRef(Cb.REM, Helpers.getLambdaArgType(remove, 0), (inst, component, bundle) -> remove.accept ((T) inst, bundle)); + } + return this; + } + + @Override + public BundleDependencyBuilder add(CbBundleComponent add) { + return callbacks(add, null, null); + } + + @Override + public BundleDependencyBuilder change(CbBundleComponent change) { + return callbacks(null, change, null); + } + + @Override + public BundleDependencyBuilder remove(CbBundleComponent remove) { + return callbacks(null, null, remove); + } + + private BundleDependencyBuilder callbacks(CbBundleComponent add, CbBundleComponent change, CbBundleComponent remove) { + if (add != null) { + setComponentCallbackRef(Cb.ADD, Helpers.getLambdaArgType(add, 0), (inst, component, bundle) -> add.accept ((T) inst, bundle, component)); + } + if (change != null) { + setComponentCallbackRef(Cb.CHG, Helpers.getLambdaArgType(change, 0), (inst, component, bundle) -> change.accept ((T) inst, bundle, component)); + } + if (remove != null) { + setComponentCallbackRef(Cb.REM, Helpers.getLambdaArgType(remove, 0), (inst, component, bundle) -> remove.accept ((T) inst, bundle, component)); + } + return this; + } + + @Override + public BundleDependencyBuilder add(InstanceCbBundle add) { + return callbacks(add, null, null); + } + + @Override + public BundleDependencyBuilder change(InstanceCbBundle change) { + return callbacks(null, change, null); + } + + @Override + public BundleDependencyBuilder remove(InstanceCbBundle remove) { + return callbacks(null, null, remove); + } + + private BundleDependencyBuilder callbacks(InstanceCbBundle add, InstanceCbBundle change, InstanceCbBundle remove) { + if (add != null) setInstanceCallbackRef(Cb.ADD, (inst, component, bundle) -> add.accept(bundle)); + if (change != null) setInstanceCallbackRef(Cb.CHG, (inst, component, bundle) -> change.accept(bundle)); + if (remove != null) setInstanceCallbackRef(Cb.REM, (inst, component, bundle) -> remove.accept(bundle)); + return this; + } + + @Override + public BundleDependencyBuilder add(InstanceCbBundleComponent add) { + return callbacks(add, null, null); + } + + @Override + public BundleDependencyBuilder change(InstanceCbBundleComponent add) { + return callbacks(add, null, null); + } + + @Override + public BundleDependencyBuilder remove(InstanceCbBundleComponent remove) { + return callbacks(null, null, remove); + } + + private BundleDependencyBuilder callbacks(InstanceCbBundleComponent add, InstanceCbBundleComponent change, InstanceCbBundleComponent remove) { + if (add != null) setInstanceCallbackRef(Cb.ADD, (inst, component, bundle) -> add.accept(bundle, component)); + if (change != null) setInstanceCallbackRef(Cb.CHG, (inst, component, bundle) -> change.accept(bundle, component)); + if (remove != null) setInstanceCallbackRef(Cb.REM, (inst, component, bundle) -> remove.accept(bundle, component)); + return this; + } + + @Override + public BundleDependency build() { + DependencyManager dm = m_component.getDependencyManager(); + + BundleDependency dep = dm.createBundleDependency(); + if (! m_requiredSet) { + m_required = Helpers.isDependencyRequiredByDefault(m_component); + } + dep.setRequired(m_required); + + if (m_filter != null) { + dep.setFilter(m_filter); + } + + if (m_bundle != null) { + dep.setBundle(m_bundle); + } + + if (m_stateMask != -1) { + dep.setStateMask(m_stateMask); + } + + if (m_propagate) { + dep.setPropagate(true); + } else if (m_propagateInstance != null) { + dep.setPropagate(m_propagateInstance, m_propagateMethod); + } else if (m_propagateCallback != null) { + dep.setPropagate(new Propagate(), "propagate"); + } + + if (m_added != null || m_changed != null || m_removed != null) { + dep.setCallbacks(m_instance, m_added, m_changed, m_removed); + } else if (m_refs.size() > 0) { + Object cb = createCallbackInstance(); + dep.setCallbacks(cb, "add", "change", "remove"); + } + + dep.setAutoConfig(m_autoConfig); + return dep; + } + + private BundleDependencyBuilder setInstanceCallbackRef(Cb cbType, MethodRef ref) { + requiresNoStringCallbacks(); + if (! m_autoConfigInvoked) m_autoConfig = false; + List> list = m_refs.computeIfAbsent(cbType, l -> new ArrayList<>()); + list.add((instance, component, bundle) -> ref.accept(null, component, bundle)); + return this; + } + + private BundleDependencyBuilder setComponentCallbackRef(Cb cbType, Class type, MethodRef ref) { + requiresNoStringCallbacks(); + if (! m_autoConfigInvoked) m_autoConfig = false; + List> list = m_refs.computeIfAbsent(cbType, l -> new ArrayList<>()); + list.add((instance, component, bundle) -> { + Object componentImpl = Stream.of(component.getInstances()) + .filter(impl -> type.isAssignableFrom(Helpers.getClass(impl))) + .findFirst() + .orElseThrow(() -> new IllegalStateException("The method reference " + ref + " does not match any available component impl classes.")); + ref.accept((T) componentImpl, component, bundle); + }); + return this; + } + + @SuppressWarnings("unused") + private Object createCallbackInstance() { + Object cb = null; + + cb = new Object() { + void add(Component c, Bundle bundle) { + invokeMethodRefs(Cb.ADD, c, bundle); + } + + void change(Component c, Bundle bundle) { + invokeMethodRefs(Cb.CHG, c, bundle); + } + + void remove(Component c, Bundle bundle) { + invokeMethodRefs(Cb.REM, c, bundle); + } + }; + + return cb; + } + + private void invokeMethodRefs(Cb cbType, Component c, Bundle bundle) { + m_refs.computeIfPresent(cbType, (k, mrefs) -> { + mrefs.forEach(mref -> mref.accept(null, c, bundle)); + return mrefs; + }); + } + + private void requiresNoStringCallbacks() { + if (m_added != null || m_changed != null || m_removed != null) { + throw new IllegalStateException("can't mix method references and string callbacks."); + } + } + + private void requiresNoMethodRefs() { + if (m_refs.size() > 0) { + throw new IllegalStateException("can't mix method references and string callbacks."); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/CompletableFutureDependencyImpl.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/CompletableFutureDependencyImpl.java new file mode 100644 index 00000000000..7535a16cb5b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/CompletableFutureDependencyImpl.java @@ -0,0 +1,259 @@ +/* + * 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.dm.lambda.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.stream.Stream; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.context.AbstractDependency; +import org.apache.felix.dm.context.DependencyContext; +import org.apache.felix.dm.context.Event; +import org.apache.felix.dm.context.EventType; +import org.apache.felix.dm.lambda.FutureDependencyBuilder; +import org.apache.felix.dm.lambda.callbacks.CbFuture; +import org.apache.felix.dm.lambda.callbacks.InstanceCbFuture; +import org.osgi.service.log.LogService; + +public class CompletableFutureDependencyImpl extends AbstractDependency> implements FutureDependencyBuilder { + + private final CompletableFuture m_future; + private Component m_comp; + private boolean m_async; + private Executor m_exec; + private InstanceCbFuture m_accept = (future) -> {}; + private CbFuture m_accept2; + private Class m_accept2Type; + + public CompletableFutureDependencyImpl(Component c, CompletableFuture future) { + super.setRequired(true); + m_future = future; + m_comp = c; + } + + /** + * Create a new PathDependency from an existing prototype. + * + * @param prototype + * the existing PathDependency. + */ + public CompletableFutureDependencyImpl(Component component, CompletableFutureDependencyImpl prototype) { + super(prototype); + m_future = prototype.m_future; + m_comp = component; + } + + @Override + public Dependency build() { + return this; + } + + @Override + public FutureDependencyBuilder complete(String callback) { + return complete(null, callback); + } + + @Override + public FutureDependencyBuilder complete(Object callbackInstance, String callback) { + super.setCallbacks(callbackInstance, callback, null); + return this; + } + + @Override + public FutureDependencyBuilder complete(CbFuture consumer) { + return complete(consumer, false); + } + + @SuppressWarnings("unchecked") + @Override + public FutureDependencyBuilder complete(CbFuture consumer, boolean async) { + m_accept2Type = Helpers.getLambdaArgType(consumer, 0);; + m_accept2 = (instance, result) -> consumer.accept((T) instance, result); + m_async = async; + return this; + } + + @Override + public FutureDependencyBuilder complete(CbFuture consumer, Executor executor) { + complete(consumer, true /* async */); + m_exec = executor; + return this; + } + + @Override + public FutureDependencyBuilder complete(InstanceCbFuture consumer) { + complete(consumer, false); + return this; + } + + @Override + public FutureDependencyBuilder complete(InstanceCbFuture consumer, boolean async) { + m_accept = m_accept.andThen(future -> consumer.accept(future)); + m_async = async; + return this; + } + + @Override + public FutureDependencyBuilder complete(InstanceCbFuture consumer, Executor executor) { + complete(consumer, true /* async */); + m_exec = executor; + return this; + } + + // ---------- DependencyContext interface ---------- + + @Override + public void start() { + try { + if (m_async) { + if (m_exec != null) { + m_future.whenCompleteAsync((result, error) -> completed(result, error), m_exec); + } else { + m_future.whenCompleteAsync((result, error) -> completed(result, error)); + } + } else { + m_future.whenComplete((result, error) -> completed(result, error)); + } + } catch (Throwable error) { + super.getComponentContext().getLogger().log(LogService.LOG_ERROR, "completable future failed", error); + } + super.start(); + } + + @Override + public DependencyContext createCopy() { + return new CompletableFutureDependencyImpl(m_comp, this); + } + + @Override + public Class getAutoConfigType() { + return null; // we don't support auto config mode + } + + // ---------- ComponentDependencyDeclaration interface ----------- + + /** + * Returns the name of this dependency (a generic name with optional info + * separated by spaces). The DM Shell will use this method when displaying + * the dependency + **/ + @Override + public String getSimpleName() { + return m_future.toString(); + } + + /** + * Returns the name of the type of this dependency. Used by the DM shell + * when displaying the dependency. + **/ + @Override + public String getType() { + return "future"; + } + + /** + * Called by DM component implementation when all required dependencies are satisfied. + */ + @Override + public void invokeCallback(EventType type, Event ... events) { + try { + switch (type) { + case ADDED: + if (m_add != null) { + // Inject result by reflection on a method name + injectByReflection(events[0].getEvent()); + return; + } + F result = events[0].getEvent(); + if (m_accept2 != null) { + if (m_accept2Type != null) { + // find the component instance that matches the given type + Object componentInstance = Stream.of(getComponentContext().getInstances()) + .filter(instance -> Helpers.getClass(instance).equals(m_accept2Type)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException( + "accept callback is not on one of the component instances: " + m_accept2 + " (type=" + m_accept2Type + ")")); + + m_accept2.accept(componentInstance, result); + } else { + // invoke a method in the main component instance that will handle the completed future. + m_accept2.accept(getComponentContext().getInstance(), result); + } + } else { + // Just invoke the Consumer with the completed future + m_accept.accept(result); + } + break; + + default: + break; + } + } catch (Throwable exc) { + super.getComponentContext().getLogger().log(LogService.LOG_ERROR, "completable future failed", exc); + } + } + + // ---------- Private methods ----------- + + /** + * Triggers component activation when the future has completed. + * @param result + * @param error + */ + private void completed(F result, Throwable error) { + if (error != null) { + super.getComponentContext().getLogger().log(LogService.LOG_ERROR, "completable future failed", error); + } else { + // Will trigger component activation (if other dependencies are satisfied), and our invokeCallback method will then be called. + m_component.handleEvent(this, EventType.ADDED, new Event(result)); + } + } + + /** + * Injects the completed future result in a method by reflection. + * We try to find a method which has in its signature a parameter that is compatible with the future result + * (including any interfaces the result may implements). + * + * @param result the result of the completable future. + */ + private void injectByReflection(Object result) { + List> types = new ArrayList<>(); + Class currentClazz = result.getClass(); + + while (currentClazz != null && currentClazz != Object.class) { + types.add(currentClazz); + Stream.of(currentClazz.getInterfaces()).forEach(types::add); + currentClazz = currentClazz.getSuperclass(); + } + + Class[][] classes = new Class[types.size() + 1][1]; + Object[][] results = new Object[types.size() + 1][1]; + for (int i = 0; i < types.size(); i ++) { + classes[i] = new Class[] { types.get(i) }; + results[i] = new Object[] { result }; + } + classes[types.size()] = new Class[0]; + results[types.size()] = new Object[0]; + m_component.invokeCallbackMethod(getInstances(), m_add, classes, results); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/ComponentBuilderImpl.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/ComponentBuilderImpl.java new file mode 100644 index 00000000000..6149a28be90 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/ComponentBuilderImpl.java @@ -0,0 +1,703 @@ +/* + * 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.dm.lambda.impl; + +import static org.apache.felix.dm.lambda.impl.ComponentBuilderImpl.ComponentCallback.DESTROY; +import static org.apache.felix.dm.lambda.impl.ComponentBuilderImpl.ComponentCallback.INIT; +import static org.apache.felix.dm.lambda.impl.ComponentBuilderImpl.ComponentCallback.START; +import static org.apache.felix.dm.lambda.impl.ComponentBuilderImpl.ComponentCallback.STOP; + +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Component.ServiceScope; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.context.ComponentContext; +import org.apache.felix.dm.lambda.BundleDependencyBuilder; +import org.apache.felix.dm.lambda.ComponentBuilder; +import org.apache.felix.dm.lambda.ConfigurationDependencyBuilder; +import org.apache.felix.dm.lambda.DependencyBuilder; +import org.apache.felix.dm.lambda.FluentProperty; +import org.apache.felix.dm.lambda.FutureDependencyBuilder; +import org.apache.felix.dm.lambda.ServiceDependencyBuilder; +import org.apache.felix.dm.lambda.callbacks.InstanceCb; +import org.apache.felix.dm.lambda.callbacks.InstanceCbComponent; + +public class ComponentBuilderImpl implements ComponentBuilder { + private final List> m_dependencyBuilders = new ArrayList<>(); + private final List m_dependencies = new ArrayList<>(); + private final Component m_component; + private final boolean m_componentUpdated; + private String[] m_serviceNames; + private Dictionary m_properties; + private Object m_impl; + private Object m_factory; + private boolean m_factoryHasComposite; + private boolean m_autoAdd = true; + protected final Map m_refs = new HashMap<>(); + private Object m_compositionInstance; + private String m_compositionMethod; + private String m_init; + private String m_start; + private String m_stop; + private String m_destroy; + private String m_factoryCreateMethod; + private boolean m_hasFactoryRef; + private boolean m_hasFactory; + private Object m_initCallbackInstance; + private Object m_startCallbackInstance; + private Object m_stopCallbackInstance; + private Object m_destroyCallbackInstance; + private final List m_listeners = new ArrayList<>(); + + enum ComponentCallback { INIT, START, STOP, DESTROY }; + + @FunctionalInterface + interface MethodRef { + public void accept(Component c); + } + + public ComponentBuilderImpl(DependencyManager dm) { + m_component = dm.createComponent(); + m_componentUpdated = false; + } + + public ComponentBuilderImpl(Component component, boolean update) { + m_component = component; + m_componentUpdated = update; + } + + @Override + public ComponentBuilderImpl scope(ServiceScope scope) { + m_component.setScope(scope); + return this; + } + + @Override + public ComponentBuilderImpl autoConfig(Class clazz, boolean autoConfig) { + m_component.setAutoConfig(clazz, autoConfig); + return this; + } + + @Override + public ComponentBuilderImpl autoConfig(Class clazz, String instanceName) { + m_component.setAutoConfig(clazz, instanceName); + return this; + } + + @Override + public ComponentBuilderImpl provides(Class iface) { + m_serviceNames = new String[] {iface.getName()}; + return this; + } + + @Override + public ComponentBuilderImpl provides(Class iface, String name, Object value, Object ... rest) { + provides(iface); + properties(name, value, rest); + return this; + } + + @Override + public ComponentBuilderImpl provides(Class iface, FluentProperty ... properties) { + provides(iface); + properties(properties); + return this; + } + + @Override + public ComponentBuilderImpl provides(Class iface, Dictionary properties) { + provides(iface); + properties(properties); + return this; + } + + @Override + public ComponentBuilderImpl provides(Class[] ifaces) { + m_serviceNames = Stream.of(ifaces).map(c -> c.getName()).toArray(String[]::new); + return this; + } + + @Override + public ComponentBuilderImpl provides(Class[] ifaces, String name, Object value, Object ... rest) { + provides(ifaces); + properties(name, value, rest); + return this; + } + + @Override + public ComponentBuilderImpl provides(Class[] ifaces, FluentProperty ... properties) { + provides(ifaces); + properties(properties); + return this; + } + + @Override + public ComponentBuilderImpl provides(Class[] ifaces, Dictionary properties) { + provides(ifaces); + properties(properties); + return this; + } + + @Override + public ComponentBuilderImpl provides(String iface) { + m_serviceNames = new String[] {iface}; + return this; + } + + @Override + public ComponentBuilderImpl provides(String iface, String name, Object value, Object ... rest) { + provides(iface); + properties(name, value, rest); + return this; + } + + @Override + public ComponentBuilderImpl provides(String iface, FluentProperty ... properties) { + provides(iface); + properties(properties); + return this; + } + + @Override + public ComponentBuilderImpl provides(String iface, Dictionary properties) { + provides(iface); + properties(properties); + return this; + } + + @Override + public ComponentBuilderImpl provides(String[] ifaces) { + m_serviceNames = ifaces; + return this; + } + + @Override + public ComponentBuilderImpl provides(String[] ifaces, String name, Object value, Object ... rest) { + provides(ifaces); + properties(name, value, rest); + return this; + } + + @Override + public ComponentBuilderImpl provides(String[] ifaces, FluentProperty ... properties) { + provides(ifaces); + properties(properties); + return this; + } + + @Override + public ComponentBuilderImpl provides(String[] ifaces, Dictionary properties) { + provides(ifaces); + properties(properties); + return this; + } + + @SuppressWarnings("unchecked") + @Override + public ComponentBuilderImpl properties(Dictionary properties) { + m_properties = (Dictionary) properties; + return this; + } + + @Override + public ComponentBuilderImpl properties(String name, Object value, Object ... rest) { + Objects.nonNull(name); + Objects.nonNull(value); + Properties props = new Properties(); + props.put(name, value); + if ((rest.length & 1) != 0) { + throw new IllegalArgumentException("Invalid number of specified properties (number of arguments must be even)."); + } + for (int i = 0; i < rest.length - 1; i += 2) { + String k = rest[i].toString().trim(); + Object v = rest[i+1]; + props.put(k, v); + } + m_properties = props; + return this; + } + + @Override + public ComponentBuilderImpl properties(FluentProperty ... properties) { + Dictionary props = new Hashtable<>(); + Stream.of(properties).forEach(property -> { + String name = Helpers.getLambdaParameterName(property, 0); + if (name.equals("arg0")) { + throw new IllegalArgumentException("arg0 property name not supported"); + } + Object value = property.apply(name); + props.put(convertDots(name), value); + }); + m_properties = props; + return this; + } + + @Override + public ComponentBuilderImpl debug(String label) { + m_component.setDebug(label); + return this; + } + + @Override + public ComponentBuilderImpl autoAdd(boolean autoAdd) { + m_autoAdd = autoAdd; + return this; + } + + public ComponentBuilderImpl autoAdd() { + m_autoAdd = true; + return this; + } + + public boolean isAutoAdd() { + return m_autoAdd; + } + + @Override + public ComponentBuilderImpl impl(Object instance) { + m_impl = instance; + return this; + } + + @Override + public ComponentBuilderImpl factory(Object factory, String createMethod) { + m_factory = factory; + m_factoryCreateMethod = createMethod; + ensureHasNoFactoryRef(); + m_hasFactory = true; + return this; + } + + @Override + public ComponentBuilderImpl factory(Supplier create) { + Objects.nonNull(create); + ensureHasNoFactory(); + m_hasFactoryRef = true; + m_factory = new Object() { + @SuppressWarnings("unused") + public Object create() { + return create.get(); + } + @Override + public String toString() { + return create.getClass().getName() + " (Factory)"; + } + }; + return this; + } + + @Override + public ComponentBuilderImpl factory(Supplier supplier, Function create) { + Objects.nonNull(supplier); + Objects.nonNull(create); + ensureHasNoFactory(); + m_hasFactoryRef = true; + + m_factory = new Object() { + @SuppressWarnings("unused") + public Object create() { + U factoryImpl = supplier.get(); + return create.apply(factoryImpl); + } + @Override + public String toString() { + return supplier.getClass().getName() + " (Factory)"; + } + }; + return this; + } + + @Override + public ComponentBuilderImpl factory(Supplier create, Supplier getComposite) { + Objects.nonNull(create); + Objects.nonNull(getComposite); + ensureHasNoFactory(); + m_hasFactoryRef = true; + + m_factory = new Object() { + @SuppressWarnings("unused") + public Object create() { // Create Factory instance + return create.get(); + } + + @SuppressWarnings("unused") + public Object[] getComposite() { // Create Factory instance + return getComposite.get(); + } + + @Override + public String toString() { + return create.getClass().getName() + " (Factory)"; + } + }; + m_factoryHasComposite = true; + return this; + } + + @Override + public ComponentBuilderImpl factory(Supplier factorySupplier, Function factoryCreate, Function factoryGetComposite) { + Objects.nonNull(factorySupplier); + Objects.nonNull(factoryCreate); + Objects.nonNull(factoryGetComposite); + ensureHasNoFactory(); + m_hasFactoryRef = true; + + m_factory = new Object() { + U m_factoryInstance; + + @SuppressWarnings("unused") + public Object create() { + m_factoryInstance = factorySupplier.get(); + return factoryCreate.apply(m_factoryInstance); + } + + @SuppressWarnings("unused") + public Object[] getComposite() { + return factoryGetComposite.apply(m_factoryInstance); + } + + @Override + public String toString() { + return factorySupplier.getClass().getName() + " (Factory)"; + } + }; + m_factoryHasComposite = true; + return this; + } + + public ComponentBuilderImpl composition(String getCompositionMethod) { + return composition(null, getCompositionMethod); + } + + public ComponentBuilderImpl composition(Object instance, String getCompositionMethod) { + m_compositionInstance = instance; + m_compositionMethod = getCompositionMethod; + return this; + } + + public ComponentBuilderImpl composition(Supplier getCompositionMethod) { + m_compositionInstance = new Object() { + @SuppressWarnings("unused") + public Object[] getComposition() { + return getCompositionMethod.get(); + } + }; + m_compositionMethod = "getComposition"; + return this; + } + + @Override + public ComponentBuilderImpl withDep(Dependency dep) { + m_dependencies.add(dep); + return this; + } + + @Override + public ComponentBuilderImpl withSvc(Class service, Consumer> consumer) { + ServiceDependencyBuilder dep = new ServiceDependencyBuilderImpl<>(m_component, service); + consumer.accept(dep); + m_dependencyBuilders.add(dep); + return this; + } + + @Override + public ComponentBuilderImpl withCnf(Consumer consumer) { + ConfigurationDependencyBuilder dep = new ConfigurationDependencyBuilderImpl(m_component); + consumer.accept(dep); + m_dependencyBuilders.add(dep); + return this; + } + + @Override + public ComponentBuilderImpl withBundle(Consumer consumer) { + BundleDependencyBuilder dep = new BundleDependencyBuilderImpl(m_component); + consumer.accept(dep); + m_dependencyBuilders.add(dep); + return this; + } + + @Override + public ComponentBuilderImpl withFuture(CompletableFuture future, Consumer> consumer) { + FutureDependencyBuilder dep = new CompletableFutureDependencyImpl<>(m_component, future); + consumer.accept(dep); + m_dependencyBuilders.add(dep); + return this; + } + + public ComponentBuilderImpl init(String callback) { + m_init = callback; + return this; + } + + public ComponentBuilderImpl init(Object callbackInstance, String callback) { + init(callback); + m_initCallbackInstance = callbackInstance; + return this; + } + + @Override + public ComponentBuilderImpl init(InstanceCb callback) { + setCallbackMethodRef(INIT, component -> callback.callback()); + m_init = null; + m_initCallbackInstance = null; + return this; + } + + @Override + public ComponentBuilderImpl init(InstanceCbComponent callback) { + setCallbackMethodRef(INIT, component -> callback.accept(component)); + m_init = null; + m_initCallbackInstance = null; + return this; + } + + public ComponentBuilderImpl start(String callback) { + m_start = callback; + return this; + } + + public ComponentBuilderImpl start(Object callbackInstance, String callback) { + start(callback); + m_startCallbackInstance = callbackInstance; + return this; + } + + @Override + public ComponentBuilderImpl start(InstanceCb callback) { + setCallbackMethodRef(START, component -> callback.callback()); + m_start = null; + m_startCallbackInstance = null; + return this; + } + + @Override + public ComponentBuilderImpl start(InstanceCbComponent callback) { + setCallbackMethodRef(START, component -> callback.accept(component)); + m_start = null; + m_startCallbackInstance = null; + return this; + } + + public ComponentBuilderImpl stop(String callback) { + m_stop = callback; + return this; + } + + public ComponentBuilderImpl stop(Object callbackInstance, String callback) { + stop(callback); + m_stopCallbackInstance = callbackInstance; + return this; + } + + @Override + public ComponentBuilderImpl stop(InstanceCb callback) { + setCallbackMethodRef(STOP, component -> callback.callback()); + m_stop = null; + m_stopCallbackInstance = null; + return this; + } + + @Override + public ComponentBuilderImpl stop(InstanceCbComponent callback) { + setCallbackMethodRef(STOP, component -> callback.accept(component)); + m_stop = null; + m_stopCallbackInstance = null; + return this; + } + + public ComponentBuilderImpl destroy(String callback) { + m_destroy = callback; + return this; + } + + public ComponentBuilderImpl destroy(Object callbackInstance, String callback) { + destroy(callback); + m_destroyCallbackInstance = callbackInstance; + return this; + } + + @Override + public ComponentBuilderImpl destroy(InstanceCb callback) { + setCallbackMethodRef(DESTROY, component -> callback.callback()); + m_destroy = null; + m_destroyCallbackInstance = null; + return this; + } + + @Override + public ComponentBuilderImpl destroy(InstanceCbComponent callback) { + setCallbackMethodRef(DESTROY, component -> callback.accept(component)); + m_destroy = null; + m_destroyCallbackInstance = null; + return this; + } + + public ComponentBuilderImpl listener(ComponentStateListener listener) { + m_listeners.add(listener); + return this; + } + + public Component build() { + if (m_serviceNames != null) { + m_component.setInterface(m_serviceNames, m_properties); + } + + if (m_properties != null) { + m_component.setServiceProperties(m_properties); + } + + m_listeners.stream().forEach(m_component::add); + + if (! m_componentUpdated) { // Don't override impl or set callbacks if component is being updated + if (m_impl != null) { + m_component.setImplementation(m_impl); + m_component.setComposition(m_compositionInstance, m_compositionMethod); + } else { + Objects.nonNull(m_factory); + if (m_hasFactoryRef) { + m_component.setFactory(m_factory, "create"); + if (m_factoryHasComposite) { + m_component.setComposition(m_factory, "getComposite"); + } + } else { + m_component.setFactory(m_factory, m_factoryCreateMethod); + } + } + + if (hasCallbacks()) { // either method refs on some object instances, or a callback (reflection) on some object instances. + if (m_refs.get(INIT) == null) { + setCallbackMethodRef(INIT, component -> invokeCallbacks(component, m_initCallbackInstance, m_init, "init")); + } + if (m_refs.get(START) == null) { + setCallbackMethodRef(START, component -> invokeCallbacks(component, m_startCallbackInstance, m_start, "start")); + } + if (m_refs.get(STOP) == null) { + setCallbackMethodRef(STOP, component -> invokeCallbacks(component, m_stopCallbackInstance, m_stop, "stop")); + } + if (m_refs.get(DESTROY) == null) { + setCallbackMethodRef(DESTROY, component -> invokeCallbacks(component, m_destroyCallbackInstance, m_destroy, "destroy")); + } + setInternalCallbacks(); + } + } + + if (m_dependencyBuilders.size() > 0) { + // add atomically in case we are building some component dependencies from a component init method. + List depList = new ArrayList<>(); + m_dependencyBuilders.stream().map(builder -> builder.build()).forEach(depList::add); + depList.addAll(m_dependencies); + m_component.add(depList.stream().toArray(Dependency[]::new)); + } + return m_component; + } + + private boolean hasCallbacks() { + return m_refs.size() > 0 || m_init != null || m_start != null || m_stop != null || m_destroy != null; + } + + private void invokeCallbacks(Component component, Object callbackInstance, String callback, String defaultCallback) { + boolean logIfNotFound = (callback != null); + callback = callback != null ? callback : defaultCallback; + ComponentContext ctx = (ComponentContext) component; + Object[] instances = callbackInstance != null ? new Object[] { callbackInstance } : ctx.getInstances(); + + ctx.invokeCallbackMethod(instances, callback, + new Class[][] {{ Component.class }, {}}, + new Object[][] {{ component }, {}}, + logIfNotFound); + } + + private ComponentBuilderImpl setCallbackMethodRef(ComponentCallback cbType, MethodRef ref) { + m_refs.put(cbType, ref); + return this; + } + + @SuppressWarnings("unused") + private void setInternalCallbacks() { + Object cb = new Object() { + void init(Component comp) { + invokeLifecycleCallback(ComponentCallback.INIT, comp); + } + + void start(Component comp) { + invokeLifecycleCallback(ComponentCallback.START, comp); + } + + void stop(Component comp) { + invokeLifecycleCallback(ComponentCallback.STOP, comp); + } + + void destroy(Component comp) { + invokeLifecycleCallback(ComponentCallback.DESTROY, comp); + } + }; + m_component.setCallbacks(cb, "init", "start", "stop", "destroy"); + } + + private void invokeLifecycleCallback(ComponentCallback cbType, Component component) { + m_refs.computeIfPresent(cbType, (k, mref) -> { + mref.accept(component); + return mref; + }); + } + + private void ensureHasNoFactoryRef() { + if (m_hasFactoryRef) { + throw new IllegalStateException("Can't mix factory method name and factory method reference"); + } + } + + private void ensureHasNoFactory() { + if (m_hasFactory) { + throw new IllegalStateException("Can't mix factory method name and factory method reference"); + } + } + + private String convertDots(String propertyName) { + StringBuilder sb = new StringBuilder(propertyName); + // replace "__" by "_" or "_" by ".": foo_bar -> foo.bar; foo__BAR_zoo -> foo_BAR.zoo + for (int i = 0; i < sb.length(); i ++) { + if (sb.charAt(i) == '_') { + if (i < (sb.length() - 1) && sb.charAt(i+1) == '_') { + // replace foo__bar -> foo_bar + sb.replace(i, i+2, "_"); + } else { + // replace foo_bar -> foo.bar + sb.replace(i, i+1, "."); + } + } + } + return sb.toString(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/ConfigurationDependencyBuilderImpl.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/ConfigurationDependencyBuilderImpl.java new file mode 100644 index 00000000000..052252e0220 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/ConfigurationDependencyBuilderImpl.java @@ -0,0 +1,250 @@ +/* + * 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.dm.lambda.impl; + +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ConfigurationDependency; +import org.apache.felix.dm.context.ComponentContext; +import org.apache.felix.dm.lambda.ConfigurationDependencyBuilder; +import org.apache.felix.dm.lambda.callbacks.CbConfiguration; +import org.apache.felix.dm.lambda.callbacks.CbConfigurationComponent; +import org.apache.felix.dm.lambda.callbacks.CbDictionary; +import org.apache.felix.dm.lambda.callbacks.CbDictionaryComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbConfiguration; +import org.apache.felix.dm.lambda.callbacks.InstanceCbConfigurationComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbDictionary; +import org.apache.felix.dm.lambda.callbacks.InstanceCbDictionaryComponent; + +public class ConfigurationDependencyBuilderImpl implements ConfigurationDependencyBuilder { + private String m_pid; + private boolean m_propagate; + private boolean m_required = true; + private final Component m_component; + private String m_updateMethodName = "updated"; + private Object m_updateCallbackInstance; + private boolean m_hasMethodRefs; + private boolean m_hasReflectionCallback; + private final List> m_refs = new ArrayList<>(); + private boolean m_hasComponentCallbackRefs; + private Class m_configType; + + @FunctionalInterface + interface MethodRef { + public void accept(I instance, Component c, Dictionary props); + } + + public ConfigurationDependencyBuilderImpl(Component component) { + m_component = component; + } + + @Override + public ConfigurationDependencyBuilder pid(String pid) { + m_pid = pid; + return this; + } + + @Override + public ConfigurationDependencyBuilder propagate() { + m_propagate = true; + return this; + } + + @Override + public ConfigurationDependencyBuilder propagate(boolean propagate) { + m_propagate = propagate; + return this; + } + + @Override + public ConfigurationDependencyBuilder required(boolean required) { + m_required = required; + return this; + } + + @Override + public ConfigurationDependencyBuilder required() { + m_required = true; + return this; + } + + @Override + public ConfigurationDependencyBuilder optional() { + m_required = false; + return this; + } + + public ConfigurationDependencyBuilder update(String update) { + checkHasNoMethodRefs(); + m_hasReflectionCallback = true; + m_updateMethodName = update; + return this; + } + + public ConfigurationDependencyBuilder update(Class configType, String updateMethod) { + m_configType = configType; + return update(updateMethod); + } + + public ConfigurationDependencyBuilder update(Object callbackInstance, String update) { + m_updateCallbackInstance = callbackInstance; + update(update); + return this; + } + + public ConfigurationDependencyBuilder update(Class configType, Object callbackInstance, String update) { + m_updateCallbackInstance = callbackInstance; + return update(callbackInstance, update); + } + + @Override + public ConfigurationDependencyBuilder update(CbDictionary callback) { + Class componentType = Helpers.getLambdaArgType(callback, 0); + return setComponentCallbackRef(componentType, (instance, component, props) -> { + callback.accept((T) instance, props); + }); + } + + @Override + public ConfigurationDependencyBuilder update(CbDictionaryComponent callback) { + Class componentType = Helpers.getLambdaArgType(callback, 0); + return setComponentCallbackRef(componentType, (instance, component, props) -> { + callback.accept((T) instance, props, component); + }); + } + + @Override + public ConfigurationDependencyBuilder update(Class configClass, CbConfiguration callback) { + Class componentType = Helpers.getLambdaArgType(callback, 0); + m_pid = m_pid == null ? configClass.getName() : m_pid; + return setComponentCallbackRef(componentType, (instance, component, props) -> { + U configProxy = ((ComponentContext) m_component).createConfigurationType(configClass, props); + callback.accept((T) instance, configProxy); + }); + } + + @Override + public ConfigurationDependencyBuilder update(Class configClass, CbConfigurationComponent callback) { + Class componentType = Helpers.getLambdaArgType(callback, 0); + m_pid = m_pid == null ? configClass.getName() : m_pid; + return setComponentCallbackRef(componentType, (instance, component, props) -> { + U configProxy = ((ComponentContext) m_component).createConfigurationType(configClass, props); + callback.accept((T) instance, configProxy, component); + }); + } + + @Override + public ConfigurationDependencyBuilder update(InstanceCbDictionary callback) { + return setInstanceCallbackRef((instance, component, props) -> { + callback.accept(props); + }); + } + + @Override + public ConfigurationDependencyBuilder update(InstanceCbDictionaryComponent callback) { + return setInstanceCallbackRef((instance, component, props) -> { + callback.accept(props, component); + }); + } + + public ConfigurationDependencyBuilder update(Class configClass, InstanceCbConfiguration updated) { + m_pid = m_pid == null ? configClass.getName() : m_pid; + return setInstanceCallbackRef((instance, component, props) -> { + T configProxy = ((ComponentContext) m_component).createConfigurationType(configClass, props); + updated.accept(configProxy); + }); + } + + public ConfigurationDependencyBuilder update(Class configClass, InstanceCbConfigurationComponent updated) { + m_pid = m_pid == null ? configClass.getName() : m_pid; + return setInstanceCallbackRef((instance, component, props) -> { + T configProxy = ((ComponentContext) m_component).createConfigurationType(configClass, props); + updated.accept(configProxy, component); + }); + } + + @Override + public ConfigurationDependency build() { + String pid = m_pid == null ? (m_configType != null ? m_configType.getName() : null) : m_pid; + if (pid == null) { + throw new IllegalStateException("Pid not specified"); + } + ConfigurationDependency dep = m_component.getDependencyManager().createConfigurationDependency(); + Objects.nonNull(m_pid); + dep.setPid(pid); + dep.setPropagate(m_propagate); + dep.setRequired(m_required); + if (m_updateMethodName != null) { + dep.setCallback(m_updateCallbackInstance, m_updateMethodName, m_configType); + } else if (m_refs.size() > 0) { + // setup an internal callback object. When config is updated, we have to call each registered + // method references. + // Notice that we need the component to be instantiated in case there is a mref on one of the component instances (unbound method ref), or is used + // called "needsInstance(true)". + dep.setCallback(new Object() { + @SuppressWarnings("unused") + void updated(Component comp, Dictionary props) { + m_refs.forEach(mref -> mref.accept(null, comp, props)); + } + }, "updated", m_hasComponentCallbackRefs); + } + return dep; + } + + private ConfigurationDependencyBuilder setInstanceCallbackRef(MethodRef ref) { + checkHasNoReflectionCallbacks(); + m_hasMethodRefs = true; + m_updateMethodName = null; + m_refs.add((instance, component, props) -> ref.accept(null, component, props)); + return this; + } + + @SuppressWarnings("unchecked") + private ConfigurationDependencyBuilder setComponentCallbackRef(Class type, MethodRef ref) { + checkHasNoReflectionCallbacks(); + m_updateMethodName = null; + m_hasMethodRefs = true; + m_hasComponentCallbackRefs = true; + m_refs.add((instance, component, props) -> { + Object componentImpl = Stream.of(component.getInstances()) + .filter(impl -> type.isAssignableFrom(Helpers.getClass(impl))) + .findFirst() + .orElseThrow(() -> new IllegalStateException("The method reference " + ref + " does not match any available component impl classes.")); + ref.accept((T) componentImpl, component, props); + }); + return this; + } + + private void checkHasNoMethodRefs() { + if (m_hasMethodRefs) { + throw new IllegalStateException("Can't mix method references with reflection based callbacks"); + } + } + + private void checkHasNoReflectionCallbacks() { + if (m_hasReflectionCallback) { + throw new IllegalStateException("Can't mix method references with reflection based callbacks"); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/FactoryPidAdapterBuilderImpl.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/FactoryPidAdapterBuilderImpl.java new file mode 100644 index 00000000000..c09d05937fd --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/FactoryPidAdapterBuilderImpl.java @@ -0,0 +1,236 @@ +/* + * 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.dm.lambda.impl; + +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.context.ComponentContext; +import org.apache.felix.dm.lambda.ComponentBuilder; +import org.apache.felix.dm.lambda.FactoryPidAdapterBuilder; +import org.apache.felix.dm.lambda.callbacks.CbConfiguration; +import org.apache.felix.dm.lambda.callbacks.CbConfigurationComponent; +import org.apache.felix.dm.lambda.callbacks.CbDictionary; +import org.apache.felix.dm.lambda.callbacks.CbDictionaryComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbConfiguration; +import org.apache.felix.dm.lambda.callbacks.InstanceCbConfigurationComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbDictionary; +import org.apache.felix.dm.lambda.callbacks.InstanceCbDictionaryComponent; + +public class FactoryPidAdapterBuilderImpl implements AdapterBase, FactoryPidAdapterBuilder { + private String m_factoryPid; + private boolean m_propagate; + private final DependencyManager m_dm; + private boolean m_autoAdd = true; + private String m_updateMethodName; + private Object m_updateCallbackInstance; + private boolean m_hasMethodRefs; + private boolean m_hasReflectionCallback; + private Consumer> m_compBuilder = (componentBuilder -> {}); + private final List> m_refs = new ArrayList<>(); + private Class m_configType; + + @FunctionalInterface + interface MethodRef { + public void accept(I instance, Component c, Dictionary props); + } + + public FactoryPidAdapterBuilderImpl(DependencyManager dm) { + m_dm = dm; + } + + public void andThenBuild(Consumer> builder) { + m_compBuilder = m_compBuilder.andThen(builder); + } + + @Override + public FactoryPidAdapterBuilderImpl autoAdd(boolean autoAdd) { + m_autoAdd = autoAdd; + return this; + } + + public boolean isAutoAdd() { + return m_autoAdd; + } + + @Override + public FactoryPidAdapterBuilder factoryPid(String pid) { + m_factoryPid = pid; + return this; + } + + @Override + public FactoryPidAdapterBuilder propagate() { + m_propagate = true; + return this; + } + + @Override + public FactoryPidAdapterBuilder propagate(boolean propagate) { + m_propagate = propagate; + return this; + } + + @Override + public FactoryPidAdapterBuilder update(String update) { + checkHasNoMethodRefs(); + m_hasReflectionCallback = true; + m_updateMethodName = update; + return this; + } + + @Override + public FactoryPidAdapterBuilder update(Class configType, String updateMethod) { + update(updateMethod); + m_configType = configType; + return this; + } + + @Override + public FactoryPidAdapterBuilder update(Object callbackInstance, String update) { + update(update); + m_updateCallbackInstance = callbackInstance; + return this; + } + + @Override + public FactoryPidAdapterBuilder update(Class configType, Object callbackInstance, String updateMethod) { + update(callbackInstance, updateMethod); + m_configType = configType; + return this; + } + + @Override + public FactoryPidAdapterBuilder update(CbDictionary callback) { + Class type = Helpers.getLambdaArgType(callback, 0); + return setComponentCallbackRef(type, (instance, component, props) -> { callback.accept((T) instance, props); }); + } + + @Override + public FactoryPidAdapterBuilder update(Class configType, CbConfiguration callback) { + Class type = Helpers.getLambdaArgType(callback, 0); + m_factoryPid = m_factoryPid == null ? configType.getName() : m_factoryPid; + return setComponentCallbackRef(type, (instance, component, props) -> { + U configProxy = ((ComponentContext) component).createConfigurationType(configType, props); + callback.accept((T) instance, configProxy); + }); + } + + @Override + public FactoryPidAdapterBuilder update(CbDictionaryComponent callback) { + Class type = Helpers.getLambdaArgType(callback, 0); + return setComponentCallbackRef(type, (instance, component, props) -> { callback.accept((T) instance, props, component); }); + } + + @Override + public FactoryPidAdapterBuilder update(Class configType, CbConfigurationComponent callback) { + Class type = Helpers.getLambdaArgType(callback, 0); + m_factoryPid = m_factoryPid == null ? configType.getName() : m_factoryPid; + return setComponentCallbackRef(type, (instance, component, props) -> { + U configProxy = ((ComponentContext) component).createConfigurationType(configType, props); + callback.accept((T) instance, configProxy, component); + }); + } + + @Override + public FactoryPidAdapterBuilder update(InstanceCbDictionary callback) { + return setInstanceCallbackRef((instance, component, props) -> { callback.accept(props); }); + } + + @Override + public FactoryPidAdapterBuilder update(Class configType, InstanceCbConfiguration callback) { + return setInstanceCallbackRef((instance, component, props) -> { + T configProxy = ((ComponentContext) component).createConfigurationType(configType, props); + callback.accept(configProxy); + }); + } + + @Override + public FactoryPidAdapterBuilder update(InstanceCbDictionaryComponent callback) { + return setInstanceCallbackRef((instance, component, props) -> { callback.accept(props, component); }); + } + + @Override + public FactoryPidAdapterBuilder update(Class configType, InstanceCbConfigurationComponent callback) { + return setInstanceCallbackRef((instance, component, props) -> { + T configProxy = ((ComponentContext) component).createConfigurationType(configType, props); + callback.accept(configProxy, component); + }); + } + + @Override + public Component build() { + Objects.nonNull(m_factoryPid); + Component c = null; + + if (m_hasMethodRefs) { + Object wrapCallback = new Object() { + @SuppressWarnings("unused") + public void updated(Component comp, Dictionary conf) { + m_refs.forEach(mref -> mref.accept(null, comp, conf)); + } + }; + c = m_dm.createFactoryConfigurationAdapterService(m_factoryPid, "updated", m_propagate, wrapCallback); + } else { + c = m_dm.createFactoryConfigurationAdapterService(m_factoryPid, m_updateMethodName, m_propagate, m_updateCallbackInstance, m_configType); + } + ComponentBuilderImpl cb = new ComponentBuilderImpl(c, false); + m_compBuilder.accept (cb); + return cb.build(); + } + + private FactoryPidAdapterBuilder setInstanceCallbackRef(MethodRef ref) { + checkHasNoReflectionCallbacks(); + m_hasMethodRefs = true; + m_refs.add((instance, component, props) -> ref.accept(null, component, props)); + return this; + } + + @SuppressWarnings("unchecked") + private FactoryPidAdapterBuilder setComponentCallbackRef(Class type, MethodRef ref) { + checkHasNoReflectionCallbacks(); + m_hasMethodRefs = true; + m_refs.add((instance, component, props) -> { + Object componentImpl = Stream.of(component.getInstances()) + .filter(impl -> type.isAssignableFrom(Helpers.getClass(impl))) + .findFirst() + .orElseThrow(() -> new IllegalStateException("The method reference " + ref + " does not match any available component impl classes.")); + ref.accept((T) componentImpl, component, props); + }); + return this; + } + + private void checkHasNoMethodRefs() { + if (m_hasMethodRefs) { + throw new IllegalStateException("Can't mix method references with reflection based callbacks"); + } + } + + private void checkHasNoReflectionCallbacks() { + if (m_hasReflectionCallback) { + throw new IllegalStateException("Can't mix method references with reflection based callbacks"); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/Helpers.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/Helpers.java new file mode 100644 index 00000000000..db0dc5c405b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/Helpers.java @@ -0,0 +1,191 @@ +/* + * 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.dm.lambda.impl; + +import java.lang.invoke.SerializedLambda; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.context.ComponentContext; +import org.apache.felix.dm.lambda.callbacks.SerializableLambda; +import org.osgi.framework.BundleContext; + +/** + * Various helper methods related to generics and lambda expressions. + */ +public class Helpers { + private final static Pattern LAMBDA_INSTANCE_METHOD_TYPE = Pattern.compile("(L[^;]+)+"); + private final static String DEFAULT_REQUIRED_DEPENDENCY = "org.apache.felix.dependencymanager.lambda.defaultRequiredDependency"; + + /** + * Gets the class name of a given object. + * @param obj the object whose class has to be returned. + */ + public static Class getClass(Object obj) { + Class clazz = obj.getClass(); + if (Proxy.isProxyClass(clazz)) { + return Proxy.getProxyClass(clazz.getClassLoader(), clazz); + } + return clazz; + } + + /** + * Extracts the type of a given generic lambda parameter. + * Example: for "BiConsumer", and with genericParamIndex=0, this method returns java.lang.String class. + * + * @param lambda a lambda expression, which must extends @link {@link SerializableLambda} interface. + * @param genericParamIndex the index of a given lambda generic parameter. + * @return the type of the lambda generic parameter that corresponds to the genericParamIndex + */ + @SuppressWarnings("unchecked") + public static Class getLambdaArgType(SerializableLambda lambda, int genericParamIndex) { + String[] lambdaParams = getGenericTypeStrings(lambda); + Class clazz; + try { + clazz = lambda.getClass().getClassLoader().loadClass(lambdaParams[genericParamIndex]); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Can't load class " + lambdaParams[genericParamIndex]); + } + return (Class) clazz; + } + + /** + * Extracts the first parameter of a lambda. + */ + public static String getLambdaParameterName(SerializableLambda lambda, int index) { + SerializedLambda serialized = getSerializedLambda(lambda); + Method m = getLambdaMethod(serialized, lambda.getClass().getClassLoader()); + Parameter p = m.getParameters()[index]; + + if (Objects.equals("arg0", p.getName())) { + throw new IllegalStateException("Can'f find lambda method name (Please check you are using javac -parameters option)."); + } + return p.getName(); + } + + /** + * Returns the SerializedObject of a given lambda. + */ + private static SerializedLambda getSerializedLambda(SerializableLambda lambda) { + if (lambda == null) { + throw new IllegalArgumentException(); + } + + for (Class clazz = lambda.getClass(); clazz != null; clazz = clazz.getSuperclass()) { + try { + Method replaceMethod = clazz.getDeclaredMethod("writeReplace"); + replaceMethod.setAccessible(true); + Object serializedForm = replaceMethod.invoke(lambda); + + if (serializedForm instanceof SerializedLambda) { + return (SerializedLambda) serializedForm; + } + } + catch (NoSuchMethodException e) { + // fall through the loop and try the next class + } + catch (Throwable t) { + throw new RuntimeException("Error while extracting serialized lambda", t); + } + } + + throw new RuntimeException("writeReplace method not found"); + } + + /** + * Finds a composite + * @param component + * @param type + * @return + */ + @SuppressWarnings("unchecked") + public static U findCompositeInstance(Component component, Class type) { + U instance = (U) Stream.of(component.getInstances()) + .filter(inst -> Objects.equals(Helpers.getClass(inst), type)) + .findFirst() + .orElseThrow(() -> new RuntimeException("Did not find a component instance matching type " + type)); + return instance; + } + + /** + * Is a dependency required by default ? + * + * @param c the component on which the dependency is added + * @param ctx the bundle context + * @return true if the dependency is required by default, false if not + */ + public static boolean isDependencyRequiredByDefault(Component c) { + BundleContext ctx = ((ComponentContext) c).getBundleContext(); + String defaultRequiredDependency = ctx.getProperty(DEFAULT_REQUIRED_DEPENDENCY); + if (defaultRequiredDependency != null) { + defaultRequiredDependency = defaultRequiredDependency.trim(); + String componentName = c.getComponentDeclaration().getClassName(); + for (String pkg : defaultRequiredDependency.split(",")) { + if (componentName.startsWith(pkg)) { + return true; + } + } + } + + return false; + } + + /** + * Extracts the actual types of all lambda generic parameters. + * Example: for "BiConsumer", this method returns ["java.lang.String", "java.lang.Integer"]. + */ + private static String[] getGenericTypeStrings(SerializableLambda lambda) { + // The only portable way to get the actual lambda generic parameters can be done using SerializedLambda. + SerializedLambda sl = getSerializedLambda(lambda); + String lambdaMethodType = sl.getInstantiatedMethodType(); + Matcher m = LAMBDA_INSTANCE_METHOD_TYPE.matcher(lambdaMethodType); + List results = new ArrayList<>(); + while (m.find()) { + results.add(m.group().substring(1).replace("/", ".")); + } + return results.toArray(new String[0]); + } + + /** + * Extracts the actual java method from a given lambda. + */ + private static Method getLambdaMethod(SerializedLambda lambda, ClassLoader loader) { + String implClassName = lambda.getImplClass().replace('/', '.'); + Class implClass; + try { + implClass = loader.loadClass(implClassName); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Lambda Method not found (can not instantiate class " + implClassName); + } + + return Stream.of(implClass.getDeclaredMethods()) + .filter(method -> Objects.equals(method.getName(), lambda.getImplMethodName())) + .findFirst() + .orElseThrow(() -> new RuntimeException("Lambda Method not found")); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/SRefAsDictionary.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/SRefAsDictionary.java new file mode 100644 index 00000000000..8c791938729 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/SRefAsDictionary.java @@ -0,0 +1,112 @@ +/* + * 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.dm.lambda.impl; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.NoSuchElementException; + +import org.osgi.framework.ServiceReference; + +/** + * Maps a ServiceReference to a Dictionary. + */ +public class SRefAsDictionary extends Dictionary { + private final ServiceReference m_ref; + private volatile int m_size = -1; + + public SRefAsDictionary(ServiceReference ref) { + m_ref = ref; + } + + @Override + public Object get(Object key) { + return m_ref.getProperty(key.toString()); + } + + @Override + public int size() { + return m_size != -1 ? m_size : (m_size = m_ref.getPropertyKeys().length); + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public Enumeration keys() { + return Collections.enumeration(Arrays.asList(m_ref.getPropertyKeys())); + } + + @Override + public Enumeration elements() { + final String[] keys = m_ref.getPropertyKeys(); + + return new Enumeration() { + int m_index = 0; + + @Override + public boolean hasMoreElements() { + return m_index < keys.length; + } + + @Override + public Object nextElement() { + if (m_index >= keys.length) { + throw new NoSuchElementException(); + } + return m_ref.getProperty(keys[m_index ++]); + } + }; + } + + @Override + public Object remove(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public Object put(String key, Object value) { + throw new UnsupportedOperationException(); + } + + public String toString() { + int max = size() - 1; + if (max == -1) + return "{}"; + + StringBuilder sb = new StringBuilder(); + String[] keys = m_ref.getPropertyKeys(); + sb.append('{'); + for (int i = 0; ; i++) { + String key = keys[i]; + Object value = m_ref.getProperty(key); + sb.append(key); + sb.append('='); + sb.append(value == this ? "(this Dictionary)" : value.toString()); + + if (i == max) + return sb.append('}').toString(); + sb.append(", "); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/SRefAsMap.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/SRefAsMap.java new file mode 100644 index 00000000000..49d2a3e1d6b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/SRefAsMap.java @@ -0,0 +1,102 @@ +/* + * 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.dm.lambda.impl; + +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import org.osgi.framework.ServiceReference; + +/** + * Maps a ServiceReference to a Map. + */ +public class SRefAsMap extends AbstractMap { + private final ServiceReference m_ref; + + public SRefAsMap(ServiceReference ref) { + m_ref = ref; + } + + public Object get(Object key) { + return m_ref.getProperty(key.toString()); + } + + @Override + public Set> entrySet() { + return new AbstractSet>() { + @Override + public Iterator> iterator() { + final Enumeration e = Collections.enumeration(Arrays.asList(m_ref.getPropertyKeys())); + + return new Iterator>() { + private String key; + + public boolean hasNext() { + return e.hasMoreElements(); + } + + public Entry next() { + key = e.nextElement(); + return new KeyEntry(key); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public int size() { + return m_ref.getPropertyKeys().length; + } + }; + } + + @Override + public Object put(String key, Object value) { + throw new UnsupportedOperationException(); + } + + class KeyEntry implements Map.Entry { + private final String key; + + KeyEntry(String key) { + this.key = key; + } + + public String getKey() { + return key; + } + + public Object getValue() { + return m_ref.getProperty(key); + } + + public Object setValue(Object value) { + return SRefAsMap.this.put(key, value); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/ServiceAdapterBuilderImpl.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/ServiceAdapterBuilderImpl.java new file mode 100644 index 00000000000..d9232e5e2bf --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/ServiceAdapterBuilderImpl.java @@ -0,0 +1,101 @@ +/* + * 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.dm.lambda.impl; + +import java.util.Objects; +import java.util.function.Consumer; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.lambda.ComponentBuilder; +import org.apache.felix.dm.lambda.ServiceAdapterBuilder; + +public class ServiceAdapterBuilderImpl extends ServiceCallbacksBuilderImpl> implements + AdapterBase>, ServiceAdapterBuilder +{ + private final Class m_adapteeType; + private String m_adapteeFilter; + private boolean m_propagate = true; + private final DependencyManager m_dm; + private boolean m_autoAdd = true; + private Consumer> m_compBuilder = (componentBuilder -> {}); + + public ServiceAdapterBuilderImpl(DependencyManager dm, Class adapterType) { + super(adapterType); + m_dm = dm; + m_adapteeType = adapterType; + } + + @Override + public void andThenBuild(Consumer> after) { + m_compBuilder = m_compBuilder.andThen(after); + } + + @Override + public ServiceAdapterBuilderImpl autoAdd(boolean autoAdd) { + m_autoAdd = autoAdd; + return this; + } + + public ServiceAdapterBuilderImpl autoAdd() { + m_autoAdd = true; + return this; + } + + public boolean isAutoAdd() { + return m_autoAdd; + } + + @Override + public ServiceAdapterBuilder filter(String adapteeFilter) { + m_adapteeFilter = adapteeFilter; + return this; + } + + @Override + public ServiceAdapterBuilder propagate(boolean propagate) { + m_propagate = propagate; + return this; + } + + @Override + public Component build() { + Objects.nonNull(m_adapteeFilter); + + String add = getAdded(), change = getChanged(), remove = getRemoved(), swap = getSwapped(); + Object cbInstance = getCallbackInstance(); + + if (hasRefs()) { + // if some method references have been set, use our own callback proxy to redispatch events to method refs. + cbInstance = createCallbackInstance(); + add = "add"; + change = "change"; + remove = "remove"; + swap = m_swapRefs.size() > 0 ? "swap" : null; + } + + Component c = m_dm.createAdapterService + (m_adapteeType, m_adapteeFilter, getAutoConfigField(), cbInstance, add, change, remove, swap, m_propagate); + + ComponentBuilderImpl cb = new ComponentBuilderImpl(c, false); + // m_compBuilder is a composed consumer that calls in sequence all necessary component builder methods. + m_compBuilder.accept (cb); + return cb.build(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/ServiceAspectBuilderImpl.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/ServiceAspectBuilderImpl.java new file mode 100644 index 00000000000..9e13b022d74 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/ServiceAspectBuilderImpl.java @@ -0,0 +1,104 @@ +/* + * 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.dm.lambda.impl; + +import java.util.Objects; +import java.util.function.Consumer; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.lambda.ComponentBuilder; +import org.apache.felix.dm.lambda.ServiceAspectBuilder; + +public class ServiceAspectBuilderImpl extends ServiceCallbacksBuilderImpl> implements + AdapterBase>, ServiceAspectBuilder +{ + private final DependencyManager m_dm; + private final Class m_aspectType; + private String m_aspectFilter; + private int m_aspectRanking; + private boolean m_autoAdd = true; + private Consumer> m_compBuilder = (componentBuilder -> {}); + + public ServiceAspectBuilderImpl(DependencyManager dm, Class aspectType) { + super(aspectType); + m_dm = dm; + m_aspectType = aspectType; + } + + @Override + public void andThenBuild(Consumer> after) { + m_compBuilder = m_compBuilder.andThen(after); + } + + @Override + public ServiceAspectBuilderImpl autoAdd(boolean autoAdd) { + m_autoAdd = autoAdd; + return this; + } + + public boolean isAutoAdd() { + return m_autoAdd; + } + + @Override + public ServiceAspectBuilder filter(String aspectFilter) { + m_aspectFilter = aspectFilter; + return this; + } + + @Override + public ServiceAspectBuilder rank(int ranking) { + m_aspectRanking = ranking; + return this; + } + + @Override + public Component build() { + Objects.nonNull(m_aspectType); + + if (getAutoConfigField() != null && (hasRefs()|| hasCallbacks())) { + throw new IllegalStateException("Can't mix autoConfig fields and aspect callbacks."); + } + + Component c = null; + if (getAutoConfigField() != null) { + c = m_dm.createAspectService(m_aspectType, m_aspectFilter, m_aspectRanking, getAutoConfigField()); + } else if (hasRefs()) { + Object cbInstance = createCallbackInstance(); + String add = "add"; + String change = "change"; + String remove = "remove"; + String swap = m_swapRefs.size() > 0 ? "swap" : null; + c = m_dm.createAspectService(m_aspectType, m_aspectFilter, m_aspectRanking, cbInstance, add, change, remove, swap); + } else if (hasCallbacks()) { + String add = getAdded(); + String change = getChanged(); + String remove = getRemoved(); + String swap = getSwapped(); + c = m_dm.createAspectService(m_aspectType, m_aspectFilter, m_aspectRanking, add, change, remove, swap); + } else { + c = m_dm.createAspectService(m_aspectType, m_aspectFilter, m_aspectRanking); + } + ComponentBuilderImpl cb = new ComponentBuilderImpl(c, false); + // m_compBuilder is a composed consumer that calls in sequence all necessary component builder methods. + m_compBuilder.accept (cb); + return cb.build(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/ServiceCallbacksBuilderImpl.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/ServiceCallbacksBuilderImpl.java new file mode 100644 index 00000000000..b50a0aabe25 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/ServiceCallbacksBuilderImpl.java @@ -0,0 +1,812 @@ +/* + * 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.dm.lambda.impl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.lambda.ServiceCallbacksBuilder; +import org.apache.felix.dm.lambda.callbacks.CbRef; +import org.apache.felix.dm.lambda.callbacks.CbRefComponent; +import org.apache.felix.dm.lambda.callbacks.CbRefRef; +import org.apache.felix.dm.lambda.callbacks.CbRefRefComponent; +import org.apache.felix.dm.lambda.callbacks.CbRefServiceRefService; +import org.apache.felix.dm.lambda.callbacks.CbRefServiceRefServiceComponent; +import org.apache.felix.dm.lambda.callbacks.CbService; +import org.apache.felix.dm.lambda.callbacks.CbServiceComponent; +import org.apache.felix.dm.lambda.callbacks.CbServiceComponentRef; +import org.apache.felix.dm.lambda.callbacks.CbServiceDict; +import org.apache.felix.dm.lambda.callbacks.CbServiceMap; +import org.apache.felix.dm.lambda.callbacks.CbServiceObjects; +import org.apache.felix.dm.lambda.callbacks.CbServiceObjectsServiceObjects; +import org.apache.felix.dm.lambda.callbacks.CbServiceRef; +import org.apache.felix.dm.lambda.callbacks.CbServiceService; +import org.apache.felix.dm.lambda.callbacks.CbServiceServiceComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbRef; +import org.apache.felix.dm.lambda.callbacks.InstanceCbRefComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbRefRef; +import org.apache.felix.dm.lambda.callbacks.InstanceCbRefRefComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbRefServiceRefService; +import org.apache.felix.dm.lambda.callbacks.InstanceCbRefServiceRefServiceComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbService; +import org.apache.felix.dm.lambda.callbacks.InstanceCbServiceComponent; +import org.apache.felix.dm.lambda.callbacks.InstanceCbServiceComponentRef; +import org.apache.felix.dm.lambda.callbacks.InstanceCbServiceDict; +import org.apache.felix.dm.lambda.callbacks.InstanceCbServiceMap; +import org.apache.felix.dm.lambda.callbacks.InstanceCbServiceObjects; +import org.apache.felix.dm.lambda.callbacks.InstanceCbServiceObjectsServiceObjects; +import org.apache.felix.dm.lambda.callbacks.InstanceCbServiceRef; +import org.apache.felix.dm.lambda.callbacks.InstanceCbServiceService; +import org.apache.felix.dm.lambda.callbacks.InstanceCbServiceServiceComponent; +import org.osgi.framework.ServiceReference; + +/** + * Service Dependency Callback management. + * + * @param the type of the service dependency + * @param the type of the sub-classes which may extend this class + */ +@SuppressWarnings({"unchecked", "unused"}) +public class ServiceCallbacksBuilderImpl> implements ServiceCallbacksBuilder { + protected boolean m_autoConfig = true; + protected boolean m_autoConfigInvoked = false; + protected String m_autoConfigField; + protected Object m_callbackInstance; + protected String m_added; + protected String m_changed; + protected String m_removed; + protected String m_swapped; + protected final Class m_serviceClass; + + enum Cb { + ADD, + CHG, + REM + }; + + /** + * List of service (add/change/remove) callbacks. + */ + protected final Map>> m_refs = new HashMap<>(); + + /** + * List of swap callbacks + */ + protected final List> m_swapRefs = new ArrayList<>(); + + /** + * Indicates if the service must always be internally deference by dependency manager. + */ + protected boolean m_dereferenceServiceInternally = true; + + /** + * This interface (lambda) is called when we want to invoke a method reference. the lambda is called with all necessary service dependency + * informations. + * + * When the lambda is called, it will invoke the proper callback on the given component instance. + * + * @param type of a component instance + * @param service dependency type + */ + @FunctionalInterface + interface MethodRef { + public void accept(I instance, Component c, ServiceReference ref, S service); + } + + /** + * This interface (lambda) is called when we want to invoke a swap method reference. the lambda is called with all necessary swap info. + * When the lambda is called, it will invoke the proper swap callback on the given component instance. + * + * @param type of a component instance + * @param service dependency type + */ + @FunctionalInterface + interface SwapMethodRef { + public void accept(I instance, Component c, ServiceReference oldRef, S oldService, ServiceReference newRef, S newService); + } + + public ServiceCallbacksBuilderImpl(Class serviceClass) { + m_serviceClass = serviceClass; + } + + public B autoConfig() { + autoConfig(true); + return (B) this; + } + + public B autoConfig(String field) { + m_autoConfigField = field; + m_autoConfigInvoked = true; + return (B) this; + } + + public B autoConfig(boolean autoConfig) { + m_autoConfig = autoConfig; + m_autoConfigInvoked = true; + return (B) this; + } + + public B callbackInstance(Object callbackInstance) { + m_callbackInstance = callbackInstance; + return (B) this; + } + + public B add(String add) { + return callbacks(add, null, null, null); + } + + public B change(String change) { + return callbacks(null, change, null, null); + } + + public B remove(String remove) { + return callbacks(null, null, remove, null); + } + + public B swap(String swap) { + return callbacks(null, null, null, swap); + } + + @Override + public B dereference(boolean dereferenceServiceInternally) { + m_dereferenceServiceInternally = dereferenceServiceInternally; + return (B) this; + } + + private B callbacks(String added, String changed, String removed, String swapped) { + requiresNoMethodRefs(); + m_added = added != null ? added : m_added; + m_changed = changed != null ? changed : m_changed; + m_removed = removed != null ? removed : m_removed; + m_swapped = swapped != null ? swapped : m_swapped; + if (! m_autoConfigInvoked) m_autoConfig = false; + return (B) this; + } + + public B add(CbService add) { + return callbacks(add, null, null); + } + + public B change(CbService change) { + return callbacks(null, change, null); + } + + public B remove(CbService remove) { + return callbacks(null, null, remove); + } + + private B callbacks(CbService add, CbService change, CbService remove) { + if (add != null) + setComponentCallbackRef(Cb.ADD, Helpers.getLambdaArgType(add, 0), (inst, comp, ref, srv) -> add.accept((T) inst, srv)); + if (change != null) + setComponentCallbackRef(Cb.CHG, Helpers.getLambdaArgType(change, 0), (inst, comp, ref, srv) -> change.accept((T) inst, srv)); + if (remove != null) + setComponentCallbackRef(Cb.REM, Helpers.getLambdaArgType(remove, 0), (inst, comp, ref, srv) -> remove.accept((T) inst, srv)); + return (B) this; + } + + public B add(InstanceCbService add) { + return callbacks(add, null, null); + } + + public B change(InstanceCbService change) { + return callbacks(null, change, null); + } + + public B remove(InstanceCbService remove) { + return callbacks(null, null, remove); + } + + public B callbacks(InstanceCbService add, InstanceCbService change, InstanceCbService remove) { + if (add != null) + setInstanceCallbackRef(Cb.ADD, (inst, comp, ref, srv) -> add.accept(srv)); + if (change != null) + setInstanceCallbackRef(Cb.CHG, (inst, comp, ref, srv) -> change.accept(srv)); + if (remove != null) + setInstanceCallbackRef(Cb.REM, (inst, comp, ref, srv) -> remove.accept(srv)); + return (B) this; + } + + public B add(CbServiceMap add) { + return callbacks(add, null, null); + } + + public B change(CbServiceMap change) { + return callbacks(null, change, null); + } + + public B remove(CbServiceMap remove) { + return callbacks(null, null, remove); + } + + public B callbacks(CbServiceMap add, CbServiceMap change, CbServiceMap remove) { + if (add != null) + setComponentCallbackRef(Cb.ADD, Helpers.getLambdaArgType(add, 0), (inst, comp, ref, srv) -> add.accept((T) inst, srv, new SRefAsMap(ref))); + if (change != null) + setComponentCallbackRef(Cb.CHG, Helpers.getLambdaArgType(change, 0), (inst, comp, ref, srv) -> change.accept((T) inst, srv, new SRefAsMap(ref))); + if (remove != null) + setComponentCallbackRef(Cb.REM, Helpers.getLambdaArgType(remove, 0), (inst, comp, ref, srv) -> remove.accept((T) inst, srv, new SRefAsMap(ref))); + return (B) this; + } + + public B add(InstanceCbServiceMap add) { + return callbacks(add, null, null); + } + + public B change(InstanceCbServiceMap change) { + return callbacks(null, change, null); + } + + public B remove(InstanceCbServiceMap remove) { + return callbacks(null, null, remove); + } + + public B callbacks(InstanceCbServiceMap add, InstanceCbServiceMap change, InstanceCbServiceMap remove) { + if (add != null) + setInstanceCallbackRef(Cb.ADD, (inst, comp, ref, srv) -> add.accept(srv, new SRefAsMap(ref))); + if (change != null) + setInstanceCallbackRef(Cb.CHG, (inst, comp, ref, srv) -> change.accept(srv, new SRefAsMap(ref))); + if (remove != null) + setInstanceCallbackRef(Cb.REM, (inst, comp, ref, srv) -> remove.accept(srv, new SRefAsMap(ref))); + return (B) this; + } + + public B add(CbServiceDict add) { + return callbacks(add, null, null); + } + + public B change(CbServiceDict change) { + return callbacks(null, change, null); + } + + public B remove(CbServiceDict remove) { + return callbacks(null, null, remove); + } + + public B callbacks(CbServiceDict add, CbServiceDict change, CbServiceDict remove) { + if (add != null) + setComponentCallbackRef(Cb.ADD, Helpers.getLambdaArgType(add, 0), (inst, comp, ref, srv) -> add.accept((T) inst, srv, new SRefAsDictionary(ref))); + if (change != null) + setComponentCallbackRef(Cb.CHG, Helpers.getLambdaArgType(change, 0), (inst, comp, ref, srv) -> change.accept((T) inst, srv, new SRefAsDictionary(ref))); + if (remove != null) + setComponentCallbackRef(Cb.REM, Helpers.getLambdaArgType(remove, 0), (inst, comp, ref, srv) -> remove.accept((T) inst, srv, new SRefAsDictionary(ref))); + return (B) this; + } + + public B add(InstanceCbServiceDict add) { + return callbacks(add, null, null); + } + + public B change(InstanceCbServiceDict change) { + return callbacks(null, change, null); + } + + public B remove(InstanceCbServiceDict remove) { + return callbacks(null, null, remove); + } + + public B callbacks(InstanceCbServiceDict add, InstanceCbServiceDict change, InstanceCbServiceDict remove) { + if (add != null) + setInstanceCallbackRef(Cb.ADD, (inst, comp, ref, srv) -> add.accept(srv, new SRefAsDictionary(ref))); + if (change != null) + setInstanceCallbackRef(Cb.CHG, (inst, comp, ref, srv) -> change.accept(srv, new SRefAsDictionary(ref))); + if (remove != null) + setInstanceCallbackRef(Cb.REM, (inst, comp, ref, srv) -> remove.accept(srv, new SRefAsDictionary(ref))); + return (B) this; + } + + public B add(CbServiceRef add) { + return callbacks(add, null, null); + } + + public B change(CbServiceRef change) { + return callbacks(null, change, null); + } + + public B remove(CbServiceRef remove) { + return callbacks(null, null, remove); + } + + public B callbacks(CbServiceRef add, CbServiceRef change, CbServiceRef remove) { + if (add != null) + setComponentCallbackRef(Cb.ADD, Helpers.getLambdaArgType(add, 0), (inst, comp, ref, srv) -> add.accept((T) inst, srv, ref)); + if (change != null) + setComponentCallbackRef(Cb.CHG, Helpers.getLambdaArgType(change, 0), (inst, comp, ref, srv) -> change.accept((T) inst, srv, ref)); + if (remove != null) + setComponentCallbackRef(Cb.REM, Helpers.getLambdaArgType(remove, 0), (inst, comp, ref, srv) -> remove.accept((T) inst, srv, ref)); + return (B) this; + } + + public B add(InstanceCbServiceRef add) { + return callbacks(add, null, null); + } + + public B change(InstanceCbServiceRef change) { + return callbacks(null, change, null); + } + + public B remove(InstanceCbServiceRef remove) { + return callbacks(null, null, remove); + } + + public B callbacks(InstanceCbServiceRef add, InstanceCbServiceRef change, InstanceCbServiceRef remove) { + if (add != null) + setInstanceCallbackRef(Cb.ADD, (inst, comp, ref, srv) -> add.accept(srv, ref)); + if (change != null) + setInstanceCallbackRef(Cb.CHG, (inst, comp, ref, srv) -> change.accept(srv, ref)); + if (remove != null) + setInstanceCallbackRef(Cb.REM, (inst, comp, ref, srv) -> remove.accept(srv, ref)); + return (B) this; + } + + public B add(InstanceCbRef add) { + return callbacks(add, null, null); + } + + public B change(InstanceCbRef change) { + return callbacks(null, change, null); + } + + public B remove(InstanceCbRef remove) { + return callbacks(null, null, remove); + } + + public B callbacks(InstanceCbRef add, InstanceCbRef change, InstanceCbRef remove) { + if (add != null) + setInstanceCallbackRef(Cb.ADD, (inst, comp, ref, srv) -> add.accept(ref)); + if (change != null) + setInstanceCallbackRef(Cb.CHG, (inst, comp, ref, srv) -> change.accept(ref)); + if (remove != null) + setInstanceCallbackRef(Cb.REM, (inst, comp, ref, srv) -> remove.accept(ref)); + m_dereferenceServiceInternally = false; + return (B) this; + } + + public B add(InstanceCbServiceObjects add) { + return callbacks(add, null, null); + } + + public B change(InstanceCbServiceObjects change) { + return callbacks(null, change, null); + } + + public B remove(InstanceCbServiceObjects remove) { + return callbacks(null, null, remove); + } + + public B callbacks(InstanceCbServiceObjects add, InstanceCbServiceObjects change, InstanceCbServiceObjects remove) { + if (add != null) + setInstanceCallbackRef(Cb.ADD, (inst, comp, ref, srv) -> add.accept(ref.getBundle().getBundleContext().getServiceObjects(ref))); + if (change != null) + setInstanceCallbackRef(Cb.CHG, (inst, comp, ref, srv) -> change.accept(ref.getBundle().getBundleContext().getServiceObjects(ref))); + if (remove != null) + setInstanceCallbackRef(Cb.REM, (inst, comp, ref, srv) -> remove.accept(ref.getBundle().getBundleContext().getServiceObjects(ref))); + m_dereferenceServiceInternally = false; + return (B) this; + } + + public B add(CbServiceComponent add) { + return callbacks(add, null, null); + } + + public B change(CbServiceComponent change) { + return callbacks(null, change, null); + } + + public B remove(CbServiceComponent remove) { + return callbacks(null, null, remove); + } + + private B callbacks(CbServiceComponent add, CbServiceComponent change, CbServiceComponent remove) { + if (add != null) + setComponentCallbackRef(Cb.ADD, Helpers.getLambdaArgType(add, 0), (inst, comp, ref, srv) -> add.accept((T) inst, srv, comp)); + if (change != null) + setComponentCallbackRef(Cb.CHG, Helpers.getLambdaArgType(change, 0), (inst, comp, ref, srv) -> change.accept((T) inst, srv, comp)); + if (remove != null) + setComponentCallbackRef(Cb.REM, Helpers.getLambdaArgType(remove, 0), (inst, comp, ref, srv) -> remove.accept((T) inst, srv, comp)); + return (B) this; + } + + public B add(InstanceCbServiceComponent add) { + return callbacks(add, null, null); + } + + public B change(InstanceCbServiceComponent change) { + return callbacks(null, change, null); + } + + public B remove(InstanceCbServiceComponent remove) { + return callbacks(null, null, remove); + } + + public B callbacks(InstanceCbServiceComponent add, InstanceCbServiceComponent change, InstanceCbServiceComponent remove) { + if (add != null) + setInstanceCallbackRef(Cb.ADD, (inst, comp, ref, srv) -> add.accept(srv, comp)); + if (change != null) + setInstanceCallbackRef(Cb.CHG, (inst, comp, ref, srv) -> change.accept(srv, comp)); + if (remove != null) + setInstanceCallbackRef(Cb.REM, (inst, comp, ref, srv) -> remove.accept(srv, comp)); + return (B) this; + } + + public B add(CbServiceComponentRef add) { + return callbacks(add, null, null); + } + + public B change(CbServiceComponentRef change) { + return callbacks(null, change, null); + } + + public B remove(CbServiceComponentRef remove) { + return callbacks(null, null, remove); + } + + private B callbacks(CbServiceComponentRef add, CbServiceComponentRef change, CbServiceComponentRef remove) { + if (add != null) + setComponentCallbackRef(Cb.ADD, Helpers.getLambdaArgType(add, 0), (inst, comp, ref, srv) -> add.accept((T) inst, srv, comp, ref)); + if (change != null) + setComponentCallbackRef(Cb.CHG, Helpers.getLambdaArgType(change, 0), (inst, comp, ref, srv) -> change.accept((T) inst, srv, comp, ref)); + if (remove != null) + setComponentCallbackRef(Cb.REM, Helpers.getLambdaArgType(remove, 0), (inst, comp, ref, srv) -> remove.accept((T) inst, srv, comp, ref)); + return (B) this; + } + + public B add(CbRef add) { + return callbacks(add, null, null); + } + + public B change(CbRef change) { + return callbacks(null, change, null); + } + + public B remove(CbRef remove) { + return callbacks(null, null, remove); + } + + private B callbacks(CbRef add, CbRef change, CbRef remove) { + if (add != null) + setComponentCallbackRef(Cb.ADD, Helpers.getLambdaArgType(add, 0), (inst, comp, ref, srv) -> add.accept((T) inst, ref)); + if (change != null) + setComponentCallbackRef(Cb.CHG, Helpers.getLambdaArgType(change, 0), (inst, comp, ref, srv) -> change.accept((T) inst, ref)); + if (remove != null) + setComponentCallbackRef(Cb.REM, Helpers.getLambdaArgType(remove, 0), (inst, comp, ref, srv) -> remove.accept((T) inst, ref)); + m_dereferenceServiceInternally = false; + return (B) this; + } + + public B add(CbServiceObjects add) { + return callbacks(add, null, null); + } + + public B change(CbServiceObjects change) { + return callbacks(null, change, null); + } + + public B remove(CbServiceObjects remove) { + return callbacks(null, null, remove); + } + + private B callbacks(CbServiceObjects add, CbServiceObjects change, CbServiceObjects remove) { + if (add != null) + setComponentCallbackRef(Cb.ADD, Helpers.getLambdaArgType(add, 0), (inst, comp, ref, srv) -> add.accept((T) inst, + ref.getBundle().getBundleContext().getServiceObjects(ref))); + if (change != null) + setComponentCallbackRef(Cb.CHG, Helpers.getLambdaArgType(change, 0), (inst, comp, ref, srv) -> change.accept((T) inst, + ref.getBundle().getBundleContext().getServiceObjects(ref))); + if (remove != null) + setComponentCallbackRef(Cb.REM, Helpers.getLambdaArgType(remove, 0), (inst, comp, ref, srv) -> remove.accept((T) inst, + ref.getBundle().getBundleContext().getServiceObjects(ref))); + m_dereferenceServiceInternally = false; + return (B) this; + } + + public B add(CbRefComponent add) { + return callbacks(add, null, null); + } + + public B change(CbRefComponent change) { + return callbacks(null, change, null); + } + + public B remove(CbRefComponent remove) { + return callbacks(null, null, remove); + } + + private B callbacks(CbRefComponent add, CbRefComponent change, CbRefComponent remove) { + if (add != null) + setComponentCallbackRef(Cb.ADD, Helpers.getLambdaArgType(add, 0), (inst, comp, ref, srv) -> add.accept((T) inst, ref, comp)); + if (change != null) + setComponentCallbackRef(Cb.CHG, Helpers.getLambdaArgType(change, 0), (inst, comp, ref, srv) -> change.accept((T) inst, ref, comp)); + if (remove != null) + setComponentCallbackRef(Cb.REM, Helpers.getLambdaArgType(remove, 0), (inst, comp, ref, srv) -> remove.accept((T) inst, ref, comp)); + return (B) this; + } + + public B add(InstanceCbServiceComponentRef add) { + return callbacks(add, null, null); + } + + public B change(InstanceCbServiceComponentRef change) { + return callbacks(null, change, null); + } + + public B remove(InstanceCbServiceComponentRef remove) { + return callbacks(null, null, remove); + } + + public B callbacks(InstanceCbServiceComponentRef add, InstanceCbServiceComponentRef change, InstanceCbServiceComponentRef remove) { + if (add != null) + setInstanceCallbackRef(Cb.ADD, (inst, comp, ref, srv) -> add.accept(srv, comp, ref)); + if (change != null) + setInstanceCallbackRef(Cb.CHG, (inst, comp, ref, srv) -> change.accept(srv, comp, ref)); + if (remove != null) + setInstanceCallbackRef(Cb.REM, (inst, comp, ref, srv) -> remove.accept(srv, comp, ref)); + return (B) this; + } + + public B add(InstanceCbRefComponent add) { + return callbacks(add, null, null); + } + + public B change(InstanceCbRefComponent change) { + return callbacks(null, change, null); + } + + public B remove(InstanceCbRefComponent remove) { + return callbacks(null, null, remove); + } + + public B callbacks(InstanceCbRefComponent add, InstanceCbRefComponent change, InstanceCbRefComponent remove) { + if (add != null) + setInstanceCallbackRef(Cb.ADD, (inst, comp, ref, srv) -> add.accept(ref, comp)); + if (change != null) + setInstanceCallbackRef(Cb.CHG, (inst, comp, ref, srv) -> change.accept(ref, comp)); + if (remove != null) + setInstanceCallbackRef(Cb.REM, (inst, comp, ref, srv) -> remove.accept(ref, comp)); + m_dereferenceServiceInternally = false; + return (B) this; + } + + public B swap(CbServiceService swap) { + Class type = Helpers.getLambdaArgType(swap, 0); + return setComponentSwapCallbackRef(type, (inst, component, oref, oserv, nref, nserv) -> + swap.accept((T) inst, oserv, nserv)); + } + + public B swap(CbRefRef swap) { + Class type = Helpers.getLambdaArgType(swap, 0); + m_dereferenceServiceInternally = false; + return setComponentSwapCallbackRef(type, (inst, component, oref, oserv, nref, nserv) -> + swap.accept((T) inst, oref, nref)); + } + + public B swap(CbServiceObjectsServiceObjects swap) { + Class type = Helpers.getLambdaArgType(swap, 0); + m_dereferenceServiceInternally = false; + return setComponentSwapCallbackRef(type, (inst, component, oref, oserv, nref, nserv) -> + swap.accept((T) inst, + oref.getBundle().getBundleContext().getServiceObjects(oref), + nref.getBundle().getBundleContext().getServiceObjects(nref))); + } + + @Override + public B swap(CbServiceServiceComponent swap) { + Class type = Helpers.getLambdaArgType(swap, 0); + return setComponentSwapCallbackRef(type, (inst, component, oref, oserv, nref, nserv) -> + swap.accept((T) inst, oserv, nserv, component)); + } + + @Override + public B swap(CbRefRefComponent swap) { + Class type = Helpers.getLambdaArgType(swap, 0); + m_dereferenceServiceInternally = false; + return setComponentSwapCallbackRef(type, (inst, component, oref, oserv, nref, nserv) -> + swap.accept((T) inst, oref, nref, component)); + } + + public B swap(CbRefServiceRefService swap) { + Class type = Helpers.getLambdaArgType(swap, 0); + return setComponentSwapCallbackRef(type, (inst, component, oref, oserv, nref, nserv) -> + swap.accept((T) inst, oref, oserv, nref, nserv)); + } + + public B swap(CbRefServiceRefServiceComponent swap) { + Class type = Helpers.getLambdaArgType(swap, 0); + return setComponentSwapCallbackRef(type, (inst, component, oref, oserv, nref, nserv) -> + swap.accept((T) inst, oref, oserv, nref, nserv, component)); + } + + public B swap(InstanceCbServiceService swap) { + return setInstanceSwapCallbackRef((inst, component, oref, oserv, nref, nserv) -> swap.accept(oserv, nserv)); + } + + public B swap(InstanceCbRefRef swap) { + m_dereferenceServiceInternally = false; + return setInstanceSwapCallbackRef((inst, component, oref, oserv, nref, nserv) -> swap.accept(oref, nref)); + } + + public B swap(InstanceCbServiceObjectsServiceObjects swap) { + m_dereferenceServiceInternally = false; + return setInstanceSwapCallbackRef((inst, component, oref, oserv, nref, nserv) -> + swap.accept(oref.getBundle().getBundleContext().getServiceObjects(oref), + nref.getBundle().getBundleContext().getServiceObjects(nref))); + } + + public B swap(InstanceCbServiceServiceComponent swap) { + return setInstanceSwapCallbackRef((inst, component, oref, oserv, nref, nserv) -> swap.accept(oserv, nserv, component)); + } + + public B swap(InstanceCbRefRefComponent swap) { + m_dereferenceServiceInternally = false; + return setInstanceSwapCallbackRef((inst, component, oref, oserv, nref, nserv) -> swap.accept(oref, nref, component)); + } + + public B swap(InstanceCbRefServiceRefService swap) { + return setInstanceSwapCallbackRef((inst, component, oref, oserv, nref, nserv) -> swap.accept(oref, oserv, nref, nserv)); + } + + public B swap(InstanceCbRefServiceRefServiceComponent swap) { + return setInstanceSwapCallbackRef((inst, component, oref, oserv, nref, nserv) -> swap.accept(oref, oserv, nref, nserv, component)); + } + + protected B setComponentCallbackRef(Cb cbType, Class type, MethodRef ref) { + requiresNoCallbacks(); + if (! m_autoConfigInvoked) m_autoConfig = false; + List> list = m_refs.computeIfAbsent(cbType, l -> new ArrayList<>()); + list.add((instance, component, sref, service) -> { + Object componentImpl = Stream.of(component.getInstances()) + .filter(impl -> type.isAssignableFrom(Helpers.getClass(impl))) + .findFirst() + .orElseThrow(() -> new IllegalStateException("The method reference " + ref + " does not match any available component impl classes.")); + ref.accept((I) componentImpl, component, sref, service); + }); + return (B) this; + } + + protected B setInstanceCallbackRef(Cb cbType, MethodRef ref) { + requiresNoCallbacks(); + if (! m_autoConfigInvoked) m_autoConfig = false; + List> list = m_refs.computeIfAbsent(cbType, l -> new ArrayList<>()); + list.add((instance, component, sref, service) -> { + ref.accept((T) component.getInstance(), component, sref, service); + }); + return (B) this; + } + + public B setComponentSwapCallbackRef(Class type, SwapMethodRef ref) { + requiresNoCallbacks(); + if (! m_autoConfigInvoked) m_autoConfig = false; + m_swapRefs.add((instance, component, oref, oservice, nref, nservice) -> { + Object componentImpl = Stream.of(component.getInstances()) + .filter(impl -> type.isAssignableFrom(Helpers.getClass(impl))) + .findFirst() + .orElseThrow(() -> new IllegalStateException("The method reference " + ref + " does not match any available component impl classes.")); + ref.accept((I) componentImpl, component, oref, oservice, nref, nservice); + }); + return (B) this; + } + + public B setInstanceSwapCallbackRef(SwapMethodRef ref) { + requiresNoCallbacks(); + if (! m_autoConfigInvoked) m_autoConfig = false; + m_swapRefs.add((instance, component, oref, oservice, nref, nservice) -> { + ref.accept((I) component.getInstance(), component, oref, oservice, nref, nservice); + }); + return (B) this; + } + + Object createCallbackInstance() { + if (m_dereferenceServiceInternally) { + return new Object() { + void add(Component c, ServiceReference ref, Object service) { + invokeMethodRefs(Cb.ADD, c, ref, (S) service); + } + + void change(Component c, ServiceReference ref, Object service) { + invokeMethodRefs(Cb.CHG, c, ref, (S) service); + } + + void remove(Component c, ServiceReference ref, Object service) { + invokeMethodRefs(Cb.REM, c, ref, (S) service); + } + + void swap(Component c, ServiceReference oldRef, Object oldSrv, ServiceReference newRef, Object newSrv) { + invokeSwapMethodRefs(c, oldRef, (S) oldSrv, newRef, (S) newSrv); + } + }; + } else { + return new Object() { + void add(Component c, ServiceReference ref) { + invokeMethodRefs(Cb.ADD, c, ref, null); + } + + void change(Component c, ServiceReference ref) { + invokeMethodRefs(Cb.CHG, c, ref, null); + } + + void remove(Component c, ServiceReference ref) { + invokeMethodRefs(Cb.REM, c, ref, null); + } + + void swap(Component c, ServiceReference oldRef, ServiceReference newRef) { + invokeSwapMethodRefs(c, oldRef, null, newRef, null); + } + }; + } + } + + boolean hasRefs() { + return m_refs.size() > 0 || m_swapRefs.size() > 0; + } + + boolean hasCallbacks() { + return m_callbackInstance != null || m_added != null || m_changed != null || m_removed != null || m_swapped != null; + } + + String getAutoConfigField() { + return m_autoConfigField; + } + + Object getCallbackInstance() { + return m_callbackInstance; + } + + String getAdded() { + return m_added; + } + + String getChanged() { + return m_changed; + } + + String getRemoved() { + return m_removed; + } + + String getSwapped() { + return m_swapped; + } + + private void invokeMethodRefs(Cb cbType, Component comp, ServiceReference ref, S service) { + m_refs.computeIfPresent(cbType, (k, mrefs) -> { + mrefs.forEach(mref -> mref.accept(null, comp, ref, service)); + return mrefs; + }); + } + + private void invokeSwapMethodRefs(Component c, ServiceReference oref, S osrv, ServiceReference nref, S nsrv) { + m_swapRefs.forEach(ref -> ref.accept(null, c, oref, osrv, nref, nsrv)); + } + + private void requiresNoCallbacks() { + if (hasCallbacks()) { + throw new IllegalStateException("can't mix method references and string callbacks."); + } + } + + private void requiresNoMethodRefs() { + if (hasRefs()) { + throw new IllegalStateException("can't mix method references and string callbacks."); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/ServiceDependencyBuilderImpl.java b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/ServiceDependencyBuilderImpl.java new file mode 100644 index 00000000000..05b239dcb28 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/impl/ServiceDependencyBuilderImpl.java @@ -0,0 +1,177 @@ +/* + * 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.dm.lambda.impl; + +import java.util.Dictionary; +import java.util.function.BiFunction; +import java.util.function.Function; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ServiceDependency; +import org.apache.felix.dm.lambda.ServiceDependencyBuilder; +import org.osgi.framework.ServiceReference; + +public class ServiceDependencyBuilderImpl extends ServiceCallbacksBuilderImpl> implements ServiceDependencyBuilder { + private final Class m_serviceIface; + private final Component m_component; + private String m_filter; + private ServiceReference m_ref; + private boolean m_required; + private String m_debug; + private boolean m_propagate; + private Object m_propagateInstance; + private String m_propagateMethod; + private Object m_defaultImpl; + private long m_timeout = -1; + private boolean m_requiredSet; + + public ServiceDependencyBuilderImpl(Component component, Class service) { + super(service); + m_serviceIface = service; + m_component = component; + } + + public ServiceDependencyBuilder filter(String filter) { + m_filter = filter; + return this; + } + + public ServiceDependencyBuilder ref(ServiceReference ref) { + m_ref = ref; + return this; + } + + public ServiceDependencyBuilder optional() { + return required(false); + } + + public ServiceDependencyBuilder required() { + return required(true); + } + + public ServiceDependencyBuilder required(boolean required) { + m_required = required; + m_requiredSet = true; + return this; + } + + public ServiceDependencyBuilder debug(String label) { + m_debug = label; + return this; + } + + public ServiceDependencyBuilder propagate() { + return propagate(true); + } + + public ServiceDependencyBuilder propagate(boolean propagate) { + m_propagate = propagate; + return this; + } + + public ServiceDependencyBuilder propagate(Object instance, String method) { + m_propagateInstance = instance; + m_propagateMethod = method; + return this; + } + + public ServiceDependencyBuilder propagate(Function, Dictionary> propagate) { + Object wrappedCallback = new Object() { + @SuppressWarnings("unused") + Dictionary propagate(ServiceReference ref) { + return propagate.apply(ref); + } + }; + propagate(wrappedCallback, "propagate"); + return this; + } + + public ServiceDependencyBuilder propagate(BiFunction, S, Dictionary> propagate) { + Object wrappedCallback = new Object() { + @SuppressWarnings("unused") + Dictionary propagate(ServiceReference ref, S service) { + return propagate.apply(ref, service); + } + }; + propagate(wrappedCallback, "propagate"); + return this; + } + + public ServiceDependencyBuilder defImpl(Object defaultImpl) { + m_defaultImpl = defaultImpl; + return this; + } + + public ServiceDependencyBuilder timeout(long timeout) { + m_timeout = timeout; + required(); + return this; + } + + // Build final ServiceDependency object. + @Override + public ServiceDependency build() { + DependencyManager dm = m_component.getDependencyManager(); + if (m_ref != null && m_filter != null) { + throw new IllegalArgumentException("Can not set ref and filter at the same time"); + } + if (m_serviceIface == null && (m_ref == null || m_filter == null)) { + throw new IllegalArgumentException("service interface not specified, and no service reference or service filter specified."); + } + ServiceDependency sd = m_timeout > -1 ? dm.createTemporalServiceDependency(m_timeout) : dm.createServiceDependency(); + if (m_ref != null) { + sd.setService(m_serviceIface, m_ref); + } else { + sd.setService(m_serviceIface, m_filter); + } + if (! m_requiredSet) { + m_required = Helpers.isDependencyRequiredByDefault(m_component); + } + sd.setRequired(m_required); + sd.setDefaultImplementation(m_defaultImpl); + if (m_debug != null) { + sd.setDebug(m_debug); + } + if (m_propagate) { + sd.setPropagate(true); + } else if (m_propagateInstance != null) { + if (m_propagateMethod == null) { + throw new IllegalArgumentException("propagate instance can't be null"); + } + sd.setPropagate(m_propagateInstance, m_propagateMethod); + } + if (hasCallbacks()) { + sd.setCallbacks(m_callbackInstance, m_added, m_changed, m_removed, m_swapped); + } else if (hasRefs()) { + Object cb = createCallbackInstance(); + sd.setCallbacks(cb, "add", "change", "remove", m_swapRefs.size() > 0 ? "swap" : null); + } + + if (m_autoConfigField != null) { + sd.setAutoConfig(m_autoConfigField); + } else { + sd.setAutoConfig(m_autoConfig); + } + + sd.setDereference(m_dereferenceServiceInternally); // false if the callback signature only contains service ref and/or component parameters. + return sd; + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/packageinfo b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/packageinfo new file mode 100644 index 00000000000..d96c0b8c06d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.lambda/src/org/apache/felix/dm/lambda/packageinfo @@ -0,0 +1 @@ +version 1.2.0 \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.lambda/test/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.lambda/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/.classpath b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/.classpath new file mode 100644 index 00000000000..7a6fc254361 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/.gitignore new file mode 100644 index 00000000000..90dde36e4ac --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/.gitignore @@ -0,0 +1,3 @@ +/bin/ +/bin_test/ +/generated/ diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/.project b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/.project new file mode 100644 index 00000000000..919fddb58c8 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/.project @@ -0,0 +1,23 @@ + + + org.apache.felix.dependencymanager.runtime.itest + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/bnd.bnd b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/bnd.bnd new file mode 100644 index 00000000000..239126ee5b4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/bnd.bnd @@ -0,0 +1,64 @@ +# +# 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. +# +-buildpath: \ + ${junit},\ + org.apache.felix.dependencymanager.annotation;version=latest,\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.runtime;version=latest,\ + org.apache.felix.dependencymanager.itest.api;version=latest,\ + osgi.core;version=6.0,\ + osgi.cmpn;version=6.0 +-runbundles: \ + org.apache.servicemix.bundles.junit;version=4.12,\ + org.mockito.mockito-core;version='[1.10.19,1.10.20)',\ + org.objenesis;version='[2.2.0,2.2.1)',\ + org.apache.felix.metatype;version=1.1.2,\ + org.apache.servicemix.bundles.junit;version=4.12,\ + org.apache.felix.configadmin;version=1.8.8,\ + org.apache.felix.gogo.runtime;version=1.0.6,\ + org.apache.felix.log;version=1.0.1,\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.runtime;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest +-runee: JavaSE-1.7 +-runfw: org.apache.felix.framework;version='[5.6.10,5.6.10]' +-runsystempackages: \ + sun.reflect +-runvm:-ea +Private-Package: \ + org.apache.felix.dm.runtime.itest.tests,\ + org.apache.felix.dm.runtime.itest.components,\ + org.apache.felix.dm.itest.util +-plugin: org.apache.felix.dm.annotation.plugin.bnd.AnnotationPlugin;log=debug;\ + path:=${workspace}/org.apache.felix.dependencymanager.annotation/generated/org.apache.felix.dependencymanager.annotation.jar +-runproperties: \ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true,\ + gosh.args=--noshutdown +Test-Cases: \ + ${classes;CONCRETE;EXTENDS;junit.framework.TestCase} +Bundle-Name: Apache Felix Dependency Manager Runtime integration tests +Bundle-Description: Integration tests for Apache Felix Dependency Manager Runtime +Bundle-Category: test +Bundle-License: http://www.apache.org/licenses/LICENSE-2.0.txt +Bundle-Vendor: The Apache Software Foundation + + +# we do not release this project in binary distribution. +-releaserepo: +-baseline: diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/AdapterAnnotation.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/AdapterAnnotation.java new file mode 100644 index 00000000000..cb695f4f627 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/AdapterAnnotation.java @@ -0,0 +1,204 @@ +/* + * 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.dm.runtime.itest.components; + +import java.util.Map; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.annotation.api.AdapterService; +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Inject; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.itest.util.Ensure; +import org.osgi.framework.BundleContext; + +/** + * @author Felix Project Team + */ +public class AdapterAnnotation { + public interface S1 { + public void run(); + } + + public interface S2 { + public void run2(); + } + + public interface S3 { + public void run3(); + } + + @Component + public static class S3Consumer { + private volatile Map m_serviceProperties; + private volatile S3 m_s3; + + @ServiceDependency + void bind(Map serviceProperties, S3 s3) { + m_serviceProperties = serviceProperties; + m_s3 = s3; + } + + @Start + void start() { + // The adapter service must inherit from adaptee service properties ... + if ("value1".equals(m_serviceProperties.get("param1")) // adaptee properties + && "true".equals(m_serviceProperties.get("adapter"))) // adapter properties + { + m_s3.run3(); + } + } + } + + @Property(name = "adapter", value = "true") + @AdapterService(adapteeService = S1.class) + public static class S1ToS3AdapterAutoConfig implements S3 { + public static final String ENSURE = "AdapterAnnotation.autoConfig"; + + // This is the adapted service + protected volatile S1 m_s1; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected volatile Ensure m_sequencer; + + // Check auto config injections + @Inject + volatile BundleContext m_bc; + BundleContext m_bcNotInjected; + + @Inject + volatile DependencyManager m_dm; + DependencyManager m_dmNotInjected; + + @Inject + volatile org.apache.felix.dm.Component m_component; + org.apache.felix.dm.Component m_componentNotInjected; + + public void run3() { + checkInjectedFields(); + m_s1.run(); + m_sequencer.step(3); + } + + private void checkInjectedFields() { + if (m_bc == null) { + m_sequencer.throwable(new Exception("Bundle Context not injected")); + return; + } + if (m_bcNotInjected != null) { + m_sequencer.throwable(new Exception("Bundle Context must not be injected")); + return; + } + + if (m_dm == null) { + m_sequencer.throwable(new Exception("DependencyManager not injected")); + return; + } + if (m_dmNotInjected != null) { + m_sequencer.throwable(new Exception("DependencyManager must not be injected")); + return; + } + + if (m_component == null) { + m_sequencer.throwable(new Exception("Component not injected")); + return; + } + if (m_componentNotInjected != null) { + m_sequencer.throwable(new Exception("Component must not be injected")); + return; + } + } + } + + @AdapterService(adapteeService = S1.class, field = "m_s1") + @Property(name = "adapter", value = "true") + public static class S1ToS3AdapterAutoConfigField implements S3 { + public final static String ENSURE = "AdapterAnnotation.autoConfig.field"; + // This is the adapted service + protected volatile S1 m_s1; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected volatile Ensure m_sequencer; + + public void run3() { + m_s1.run(); + m_sequencer.step(3); + } + } + + @AdapterService(adapteeService = S1.class, added = "bind", removed = "removed") + @Property(name = "adapter", value = "true") + public static class S1ToS3AdapterCallback implements S3 { + public final static String ENSURE = "AdapterAnnotation.callback"; + // This is the adapted service + protected Object m_s1; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected Ensure m_sequencer; + + void bind(S1 s1) { + m_s1 = s1; + } + + public void run3() { + ((S1) m_s1).run(); + } + + @Stop + void stop() { + m_sequencer.step(3); + } + + void removed(S1 s1) { + m_sequencer.step(4); + } + } + + @Component + @Property(name = "param1", value = "value1") + public static class S1Impl implements S1 { + public final static String ENSURE = "AdapterAnnotation.S1Impl"; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected Ensure m_sequencer; + + @ServiceDependency + protected S2 m_s2; + + public void run() { + m_sequencer.step(1); + m_s2.run2(); + } + } + + @Component + public static class S2Impl implements S2 { + public final static String ENSURE = "AdapterAnnotation.S2Impl"; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected Ensure m_sequencer; + + public void run2() { + m_sequencer.step(2); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/AdapterServiceTestWithPublisher.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/AdapterServiceTestWithPublisher.java new file mode 100644 index 00000000000..c8241e7146f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/AdapterServiceTestWithPublisher.java @@ -0,0 +1,126 @@ +/* + * 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.dm.runtime.itest.components; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.AdapterService; +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Init; +import org.apache.felix.dm.annotation.api.LifecycleController; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.itest.util.Ensure; + +/** + * Test an AdapterService which provides its interface using a @ServiceLifecycle. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes", "serial"}) +public class AdapterServiceTestWithPublisher { + public static final String ENSURE = "AdapterServiceTestWithPublisher"; + + public interface Provider { + } + + public interface Provider2 { + } + + @Component + public static class Consumer { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + @ServiceDependency(required = false, removed = "unbind") + void bind(Map properties, Provider2 provider) { + m_sequencer.step(1); + // check ProviderImpl properties + if ("bar".equals(properties.get("foo"))) { + m_sequencer.step(2); + } + // check extra ProviderImpl properties (returned by start method) + if ("bar2".equals(properties.get("foo2"))) { + m_sequencer.step(3); + } + // check Provider2Impl properties + if ("bar3".equals(properties.get("foo3"))) { + m_sequencer.step(4); + } + // check extra Provider2Impl properties (returned by start method) + if ("bar4".equals(properties.get("foo4"))) { + m_sequencer.step(5); + } + + } + + void unbind(Provider2 provider) { + m_sequencer.step(6); + } + } + + @Component + @Property(name = "foo", value = "bar") + public static class ProviderImpl implements Provider { + @Start + Map start() { + // Add some extra service properties ... they will be appended to the one we have defined + // in the @Service annotation. + return new HashMap() { + { + put("foo2", "bar2"); + } + }; + } + } + + @AdapterService(adapteeService = Provider.class) + @Property(name = "foo3", value = "bar3") + public static class Provider2Impl implements Provider2 { + @LifecycleController + volatile Runnable m_publisher; // injected and used to register our service + + @LifecycleController(start = false) + volatile Runnable m_unpublisher; // injected and used to unregister our service + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + @Init + void init() { + // register service in 1 second + Utils.schedule(m_publisher, 1000); + // unregister the service in 2 seconds + Utils.schedule(m_unpublisher, 2000); + } + + @Start + Map start() { + // Add some extra service properties ... they will be appended to the one we have defined + // in the @Service annotation. + return new HashMap() { + { + put("foo4", "bar4"); + } + }; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/AspectAnnotation.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/AspectAnnotation.java new file mode 100644 index 00000000000..8f8ca95035b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/AspectAnnotation.java @@ -0,0 +1,238 @@ +/* + * 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.dm.runtime.itest.components; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.annotation.api.AspectService; +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Destroy; +import org.apache.felix.dm.annotation.api.Init; +import org.apache.felix.dm.annotation.api.Inject; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.itest.util.Ensure; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; + +/** + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class AspectAnnotation { + public interface ServiceInterface { + public void invoke(Runnable run); + } + + @Component + public static class ServiceProvider implements ServiceInterface { + public final static String ENSURE = "AspectAnnotation.ServiceProvider"; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected volatile Ensure m_sequencer; + // Injected by reflection. + protected volatile ServiceRegistration m_sr; + + @Init + void init() { + System.out.println("ServiceProvider.init"); + } + + @Destroy + void destroy() { + System.out.println("ServiceProvider.destroy"); + } + + public void invoke(Runnable run) { + run.run(); + m_sequencer.step(6); + } + } + + @AspectService(ranking = 20) + public static class ServiceAspect2 implements ServiceInterface { + public final static String ENSURE = "AspectAnnotation.ServiceAspect2"; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected volatile Ensure m_sequencer; + // Injected by reflection. + private volatile ServiceInterface m_parentService; + + // Check auto config injections + @Inject + volatile BundleContext m_bc; + BundleContext m_bcNotInjected; + + @Inject + volatile DependencyManager m_dm; + DependencyManager m_dmNotInjected; + + @Inject + volatile org.apache.felix.dm.Component m_component; + org.apache.felix.dm.Component m_componentNotInjected; + + @Init + void init() { + System.out.println("ServiceAspect2.init"); + } + + @Destroy + void destroy() { + System.out.println("ServiceAspect2.destroy"); + } + + public void invoke(Runnable run) { + checkInjectedFields(); + m_sequencer.step(3); + m_parentService.invoke(run); + } + + private void checkInjectedFields() { + if (m_bc == null) { + m_sequencer.throwable(new Exception("Bundle Context not injected")); + return; + } + if (m_bcNotInjected != null) { + m_sequencer.throwable(new Exception("Bundle Context must not be injected")); + return; + } + + if (m_dm == null) { + m_sequencer.throwable(new Exception("DependencyManager not injected")); + return; + } + if (m_dmNotInjected != null) { + m_sequencer.throwable(new Exception("DependencyManager must not be injected")); + return; + } + + if (m_component == null) { + m_sequencer.throwable(new Exception("Component not injected")); + return; + } + if (m_componentNotInjected != null) { + m_sequencer.throwable(new Exception("Component must not be injected")); + return; + } + } + } + + @AspectService(ranking = 30, added = "add") + public static class ServiceAspect3 implements ServiceInterface { + public final static String ENSURE = "AspectAnnotation.ServiceAspect3"; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected volatile Ensure m_sequencer; + // Injected using add callback. + private volatile ServiceInterface m_parentService; + + @Init + void init() { + System.out.println("ServiceAspect3.init"); + } + + @Destroy + void destroy() { + System.out.println("ServiceAspect3.destroy"); + } + + void add(ServiceInterface si) { + m_parentService = si; + } + + public void invoke(Runnable run) { + m_sequencer.step(2); + m_parentService.invoke(run); + } + } + + @AspectService(ranking = 10, added = "added", removed = "removed") + public static class ServiceAspect1 implements ServiceInterface { + public final static String ENSURE = "AspectAnnotation.ServiceAspect1"; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected volatile Ensure m_sequencer; + // Injected by reflection. + private volatile ServiceInterface m_parentService; + + @Init + void init() { + System.out.println("ServiceAspect1.init"); + } + + @Destroy + void destroy() { + System.out.println("ServiceAspect1.destroy"); + } + + void added(ServiceInterface si) { + m_parentService = si; + } + + @Stop + void stop() { + m_sequencer.step(7); + } + + void removed(ServiceInterface si) { + m_sequencer.step(8); + } + + public void invoke(Runnable run) { + m_sequencer.step(4); + m_parentService.invoke(run); + } + } + + @Component + public static class ServiceConsumer implements Runnable { + public final static String ENSURE = "AspectAnnotation.ServiceConsumer"; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected volatile Ensure m_sequencer; + + @ServiceDependency + private volatile ServiceInterface m_service; + + private Thread m_thread; + + @Init + public void init() { + m_thread = new Thread(this, "ServiceConsumer"); + m_thread.start(); + } + + public void run() { + m_sequencer.waitForStep(1, 2000); + m_service.invoke(new Runnable() { + public void run() { + m_sequencer.step(5); + } + }); + } + + @Destroy + void destroy() { + m_thread.interrupt(); + try { + m_thread.join(); + } catch (InterruptedException e) { + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/AspectLifecycleAnnotation.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/AspectLifecycleAnnotation.java new file mode 100644 index 00000000000..841ba87fe77 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/AspectLifecycleAnnotation.java @@ -0,0 +1,102 @@ +/* + * 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.dm.runtime.itest.components; + +import org.apache.felix.dm.annotation.api.AspectService; +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Destroy; +import org.apache.felix.dm.annotation.api.Init; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.itest.util.Ensure; + +/** + * @author Felix Project Team + */ +public class AspectLifecycleAnnotation { + public interface ServiceInterface { + public void run(); + } + + /** + * Tests an aspect service, and ensure that its lifecycle methods are properly invoked (init/start/stop/destroy) + */ + @Component + public static class AspectLifecycleTest { + @ServiceDependency + void bind(ServiceInterface service) { + service.run(); + } + } + + @Component + public static class ServiceProvider implements ServiceInterface { + public final static String ENSURE = "AspectLifecycleAnnotation.ServiceProvider"; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected volatile Ensure m_sequencer; + + public void run() { + m_sequencer.step(); + } + } + + @AspectService(ranking = 10) + public static class ServiceProviderAspect implements ServiceInterface { + public final static String ENSURE = "AspectLifecycleAnnotation.ServiceProviderAspect"; + + protected volatile boolean m_initCalled; + protected volatile Ensure m_sequencer; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected void bind(Ensure sequencer) { + m_sequencer = sequencer; + m_sequencer.step(2); + } + + @Init + void init() { + m_initCalled = true; + } + + @Start + void start() { + if (!m_initCalled) { + throw new IllegalStateException("start method called, but init method was not called"); + } + m_sequencer.step(3); + } + + public void run() { + m_sequencer.step(4); + } + + @Stop() + void stop() { + // At this point, the AspectLifecycleTest class has been rebound to the original ServiceProvider. + m_sequencer.step(6); + } + + @Destroy + void destroy() { + m_sequencer.step(7); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/AspectLifecycleWithDynamicProxyAnnotation.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/AspectLifecycleWithDynamicProxyAnnotation.java new file mode 100644 index 00000000000..9164ab87618 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/AspectLifecycleWithDynamicProxyAnnotation.java @@ -0,0 +1,119 @@ +/* +* 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.dm.runtime.itest.components; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.apache.felix.dm.annotation.api.AspectService; +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Destroy; +import org.apache.felix.dm.annotation.api.Init; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.itest.util.Ensure; + +/** + * @author Felix Project Team + */ +public class AspectLifecycleWithDynamicProxyAnnotation { + public interface ServiceInterface { + public void run(); + } + + /** + * Tests an aspect service, and ensure that its lifecycle methods are properly invoked (init/start/stop/destroy) + */ + @Component + public static class AspectLifecycleTest { + @ServiceDependency + void bind(ServiceInterface service) { + service.run(); + } + } + + @Component + public static class ServiceProvider implements ServiceInterface { + public final static String ENSURE = "AspectLifecycleWithDynamicProxyAnnotation.ServiceProvider"; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected volatile Ensure m_sequencer; + + public void run() { + m_sequencer.step(); + } + } + + @AspectService(ranking = 10, service = ServiceInterface.class, factoryMethod = "create") + public static class ServiceProviderAspect implements InvocationHandler { + public final static String ENSURE = "AspectLifecycleWithDynamicProxyAnnotation.ServiceProviderAspect"; + + protected volatile boolean m_initCalled; + protected volatile Ensure m_sequencer; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected void bind(Ensure sequencer) { + m_sequencer = sequencer; + m_sequencer.step(2); + } + + public static Object create() { + return Proxy.newProxyInstance(ServiceProviderAspect.class.getClassLoader(), + new Class[]{ServiceInterface.class}, new ServiceProviderAspect()); + } + + @Init + void init() { + m_initCalled = true; + } + + @Start + void start() { + if (!m_initCalled) { + throw new IllegalStateException("start method called, but init method was not called"); + } + m_sequencer.step(3); + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (method.getName().equals("toString")) { + return "ServiceProviderAspect@" + System.identityHashCode(this); + } + + if (!method.getName().equals("run")) { + throw new IllegalStateException("wrong invoked method: " + method); + } + m_sequencer.step(4); + return null; + } + + @Stop() + void stop() { + // At this point, the AspectLifecycleTest class has been rebound to the original ServiceProvider. + m_sequencer.step(6); + } + + @Destroy + void destroy() { + m_sequencer.step(7); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/BundleAdapterServiceTestWithPublisher.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/BundleAdapterServiceTestWithPublisher.java new file mode 100644 index 00000000000..32f5489e506 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/BundleAdapterServiceTestWithPublisher.java @@ -0,0 +1,106 @@ +/* + * 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.dm.runtime.itest.components; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.BundleAdapterService; +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Init; +import org.apache.felix.dm.annotation.api.LifecycleController; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.itest.util.Ensure; +import org.osgi.framework.Bundle; + +/** + * Test a BundleAdapterService which provides its interface using a @ServiceLifecycle. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes", "serial"}) +public class BundleAdapterServiceTestWithPublisher { + public static final String ENSURE = "BundleAdapterServiceTestWithPublisher"; + + public interface Provider { + } + + @Component + public static class Consumer { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + @ServiceDependency(required = false, removed = "unbind") + void bind(Map properties, Provider provider) { + m_sequencer.step(1); + // check ProviderImpl properties + if ("bar".equals(properties.get("foo"))) { + m_sequencer.step(2); + } + // check extra ProviderImpl properties (returned by start method) + if ("bar2".equals(properties.get("foo2"))) { + m_sequencer.step(3); + } + // check properties propagated from bundle + if (Utils.DM_BSN.equals(properties.get("Bundle-SymbolicName"))) { + m_sequencer.step(4); + } else { + System.out.println("Consumer.bind: properties=" + properties); + } + } + + void unbind(Provider provider) { + m_sequencer.step(5); + } + } + + @BundleAdapterService(filter = "(Bundle-SymbolicName=" + Utils.DM_BSN + ")", stateMask = Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE, propagate = true) + @Property(name = "foo", value = "bar") + public static class ProviderImpl implements Provider { + @LifecycleController + volatile Runnable m_publisher; // injected and used to register our service + + @LifecycleController(start = false) + volatile Runnable m_unpublisher; // injected and used to unregister our service + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + @Init + void init() { + // register service in 1 second + Utils.schedule(m_publisher, 1000); + // unregister the service in 2 seconds + Utils.schedule(m_unpublisher, 2000); + } + + @Start + Map start() { + // Add some extra service properties ... they will be appended to the one we have defined + // in the @Service annotation. + return new HashMap() { + { + put("foo2", "bar2"); + } + }; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/BundleDependencyAnnotation.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/BundleDependencyAnnotation.java new file mode 100644 index 00000000000..169dca07ee1 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/BundleDependencyAnnotation.java @@ -0,0 +1,163 @@ +/* +* 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.dm.runtime.itest.components; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.annotation.api.BundleAdapterService; +import org.apache.felix.dm.annotation.api.BundleDependency; +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Inject; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.itest.util.Ensure; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; + +/** + * @author Felix Project Team + */ +public class BundleDependencyAnnotation { + public static final String ENSURE_CONSUMER = "BundleDependencyAnnotation.consumer"; + public static final String ENSURE_ADAPTER = "BundleDependencyAnnotation.adapter"; + public static final String METATYPE_BSN = "org.apache.felix.metatype"; + + public interface ServiceInterface extends Runnable { + } + + /** + * Simple Consumer which has a BundleDependency dependency. + */ + @Component + public static class Consumer { + @ServiceDependency(filter = "(name=" + ENSURE_CONSUMER + ")") + private volatile Ensure m_sequencer; + + @BundleDependency(required = true, removed = "removed", filter = "(Bundle-SymbolicName=" + METATYPE_BSN + ")", stateMask = Bundle.ACTIVE) + public void add(Bundle b) { + if (b != null && b.getSymbolicName().equals(METATYPE_BSN)) { + m_sequencer.step(1); + } + } + + @Start + public void start() { + m_sequencer.step(2); + } + + @Stop + public void stop() { + m_sequencer.step(3); + } + + protected void removed(Bundle b) { + if (b != null && b.getSymbolicName().equals(METATYPE_BSN)) { + m_sequencer.step(4); + } + } + } + + /** + * ServiceInterface Consumer. + */ + @Component + public static class ServiceConsumer { + @ServiceDependency(filter = "(name=" + ENSURE_ADAPTER + ")") + volatile Ensure m_sequencer; + + @ServiceDependency + volatile ServiceInterface m_service; + + @Start + void start() { + m_sequencer.step(2); + m_service.run(); + } + } + + /** + * A BundleAdapter test, which adapts the dependency manager bundle to the ServiceInterface service. + */ + @BundleAdapterService(filter = "(Bundle-SymbolicName=" + METATYPE_BSN + ")", stateMask = Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE, propagate = true) + @Property(name = "foo", value = "bar") + public static class ServiceProvider implements ServiceInterface { + // Adapted bundle (injected by reflection). + protected volatile Bundle m_bundle; + + // Our Sequencer required dependency + @ServiceDependency(filter = "(name=" + ENSURE_ADAPTER + ")") + volatile Ensure m_sequencer; + + // Check auto config injections + @Inject + volatile BundleContext m_bc; + BundleContext m_bcNotInjected; + + @Inject + volatile DependencyManager m_dm; + DependencyManager m_dmNotInjected; + + @Inject + volatile org.apache.felix.dm.Component m_component; + org.apache.felix.dm.Component m_componentNotInjected; + + @Start + void start() { + checkInjectedFields(); + m_sequencer.step(1); + } + + public void run() { + if (m_bundle == null || !m_bundle.getSymbolicName().equals(METATYPE_BSN)) { + throw new IllegalStateException("ServiceProvider did not get proper bundle: " + m_bundle); + } + m_sequencer.step(3); + } + + private void checkInjectedFields() { + if (m_bc == null) { + m_sequencer.throwable(new Exception("Bundle Context not injected")); + return; + } + if (m_bcNotInjected != null) { + m_sequencer.throwable(new Exception("Bundle Context must not be injected")); + return; + } + + if (m_dm == null) { + m_sequencer.throwable(new Exception("DependencyManager not injected")); + return; + } + if (m_dmNotInjected != null) { + m_sequencer.throwable(new Exception("DependencyManager must not be injected")); + return; + } + + if (m_component == null) { + m_sequencer.throwable(new Exception("Component not injected")); + return; + } + if (m_componentNotInjected != null) { + m_sequencer.throwable(new Exception("Component must not be injected")); + return; + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/CollectionFieldDependencyAnnotation.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/CollectionFieldDependencyAnnotation.java new file mode 100644 index 00000000000..00c6b49fc59 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/CollectionFieldDependencyAnnotation.java @@ -0,0 +1,125 @@ +/* + * 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.dm.runtime.itest.components; + +import java.util.Collection; +import java.util.Dictionary; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.itest.util.Ensure; +import org.junit.Assert; + +/** + * Tests if a consumer can be injected with some services using a field collection. + */ +public class CollectionFieldDependencyAnnotation { + + interface Provider { + } + + @Component + @Property(name="p", value="v1") + public static class ProviderImpl1 implements Provider { + public final static String ENSURE = "CollectionFieldDependencyAnnotation.ProviderImpl1"; + + @ServiceDependency(filter="(name=" + ENSURE + ")") + Ensure m_ensure; + + @Start + void start() { + m_ensure.step(); + } + } + + @Component + @Property(name="p", value="v2") + public static class ProviderImpl2 implements Provider { + public final static String ENSURE = "CollectionFieldDependencyAnnotation.ProviderImpl2"; + + @ServiceDependency(filter="(name=" + ENSURE + ")") + Ensure m_ensure; + + @Start + void start() { + m_ensure.step(); + } + } + + @Component + public static class Consumer { + public final static String ENSURE = "CollectionFieldDependencyAnnotation.Consumer"; + + @ServiceDependency(filter="(name=" + ENSURE + ")") + Ensure m_ensure; + + @ServiceDependency + volatile Iterable m_list1; + + @ServiceDependency + volatile Collection m_list2; + + @ServiceDependency + volatile Map> m_list3 = new ConcurrentHashMap<>(); + + @ServiceDependency(filter="(p=v1)") + volatile Provider m_p1; + + @ServiceDependency(filter="(p=v2)") + volatile Provider m_p2; + + @Start + void start() { + m_ensure.step(3); + + Assert.assertNotNull(m_p1); + Assert.assertNotNull(m_p2); + Assert.assertNotNull(m_list1); + int size = 0; + for (Provider p : m_list1) size ++; + Assert.assertEquals(2, size); + Assert.assertEquals(2, m_list2.size()); + Iterator it = m_list1.iterator(); + Provider p1 = it.next(); + Provider p2 = it.next(); + if (p1 == m_p1 && p2 == m_p2) { + m_ensure.step(4); + } else if (p1 == m_p2 && p2 == m_p1) { + m_ensure.step(4); + } + + Assert.assertEquals(2, m_list3.size()); + Dictionary p1Props = m_list3.get(m_p1); + Assert.assertNotNull(p1Props); + Assert.assertEquals("v1", p1Props.get("p")); + m_ensure.step(); + + Dictionary p2Props = m_list3.get(m_p2); + Assert.assertNotNull(p2Props); + Assert.assertEquals("v2", p2Props.get("p")); + m_ensure.step(); + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ComponentDMPropertyTypeAnnotation.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ComponentDMPropertyTypeAnnotation.java new file mode 100644 index 00000000000..d28246d6c04 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ComponentDMPropertyTypeAnnotation.java @@ -0,0 +1,65 @@ +/* + * 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.dm.runtime.itest.components; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.PropertyType; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.itest.util.Ensure; + +/** + * @author Felix Project Team + */ +public class ComponentDMPropertyTypeAnnotation { + public final static String ENSURE = "ComponentDMPropertyTypeAnnotation"; + + @PropertyType + @Retention(RetentionPolicy.CLASS) + @interface MyProperties { + double double_value() default 123; + String string_value() default "defstring"; + } + + @Component(provides=MyComponent.class) + @MyProperties(double_value=456) + public static class MyComponent { + } + + @Component + public static class MyConsumer { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_ensure; + + @ServiceDependency + void bind(MyComponent comp, Map props) { + m_ensure.step(1); + if ("defstring".equals(props.get("string.value"))) { + m_ensure.step(2); + } + if (new Double(456).equals(props.get("double.value"))) { + m_ensure.step(3); + } + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ComponentDMPropertyTypeArrayAnnotation.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ComponentDMPropertyTypeArrayAnnotation.java new file mode 100644 index 00000000000..8b6075f02e6 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ComponentDMPropertyTypeArrayAnnotation.java @@ -0,0 +1,79 @@ +/* + * 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.dm.runtime.itest.components; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.PropertyType; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.itest.util.Ensure; + +/** + * @author Felix Project Team + */ +public class ComponentDMPropertyTypeArrayAnnotation { + public final static String ENSURE = "ComponentDMPropertyTypeArrayAnnotation"; + + @PropertyType + @Retention(RetentionPolicy.CLASS) + @interface MyProperties { + String[] pattern(); + String[] pattern2(); + String prefix(); + } + + @Component(provides=MyComponent.class) + @MyProperties(pattern="/web", pattern2 = { "/web1", "/web2" }, prefix="/*") + public static class MyComponent { + } + + @Component + public static class MyConsumer { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_ensure; + + @ServiceDependency + void bind(MyComponent comp, Map props) { + m_ensure.step(1); + Object pattern = props.get("pattern"); + if (pattern instanceof String) { + if ("/web".equals(pattern.toString())) + m_ensure.step(2); + } + + Object pattern2 = props.get("pattern2"); + if (pattern2 != null) { + if (pattern2 instanceof String[]) { + String[] array = (String[]) pattern2; + if (array.length == 2 && array[0].equals("/web1") && array[1].equals("/web2")) { + m_ensure.step(3); + } + } + } + + if ("/*".equals(props.get("prefix"))) { + m_ensure.step(4); + } + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ComponentDMSingleValuedPropertyTypeAnnotation.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ComponentDMSingleValuedPropertyTypeAnnotation.java new file mode 100644 index 00000000000..f2baa071907 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ComponentDMSingleValuedPropertyTypeAnnotation.java @@ -0,0 +1,73 @@ +/* + * 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.dm.runtime.itest.components; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.PropertyType; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.itest.util.Ensure; + +/** + * @author Felix Project Team + */ +public class ComponentDMSingleValuedPropertyTypeAnnotation { + public final static String ENSURE = "ComponentDMSingleValuedPropertyTypeAnnotation"; + + @PropertyType + @Retention(RetentionPolicy.CLASS) + @interface MyProperty1 { + String PREFIX_ = "prefix1."; + String value() default "defval1"; // derived property name is prefix1.my.property1; + } + + @PropertyType + @Retention(RetentionPolicy.CLASS) + @interface MyProperty2 { + String PREFIX_ = "prefix2."; + String value() default "defval2"; // derived property name is prefix2.my.propert2; + } + + @Component(provides=MyComponent.class) + @MyProperty1("val1") + @MyProperty2 + public static class MyComponent { + } + + @Component + public static class MyConsumer { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_ensure; + + @ServiceDependency + void bind(MyComponent comp, Map props) { + m_ensure.step(1); + if ("val1".equals(props.get("prefix1.my.property1"))) { + m_ensure.step(2); + } + if ("defval2".equals(props.get("prefix2.my.property2"))) { + m_ensure.step(3); + } + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ComponentJaxrsResourceAnnotation.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ComponentJaxrsResourceAnnotation.java new file mode 100644 index 00000000000..e7a4d5cbf22 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ComponentJaxrsResourceAnnotation.java @@ -0,0 +1,55 @@ +/* + * 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.dm.runtime.itest.components; + +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.itest.util.Ensure; +import org.osgi.service.jaxrs.whiteboard.JaxRSWhiteboardConstants; +import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource; + +/** + * @author Felix Project Team + */ +public class ComponentJaxrsResourceAnnotation { + public final static String ENSURE = "ComponentJaxrsResourceAnnotation"; + + @Component(provides=MyComponent.class) + @JaxrsResource + public static class MyComponent { + } + + @Component + public static class MyConsumer { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_ensure; + + @ServiceDependency + void bind(MyComponent comp, Map props) { + m_ensure.step(1); + Object value = props.get(JaxRSWhiteboardConstants.JAX_RS_RESOURCE); + if (Boolean.TRUE.equals(value)) { + m_ensure.step(2); + } + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ComponentPropertyTypeWithDictionaryPassedInUpdateCallback.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ComponentPropertyTypeWithDictionaryPassedInUpdateCallback.java new file mode 100644 index 00000000000..f5e73dbd98b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ComponentPropertyTypeWithDictionaryPassedInUpdateCallback.java @@ -0,0 +1,111 @@ +/* + * 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.dm.runtime.itest.components; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.ConfigurationDependency; +import org.apache.felix.dm.annotation.api.PropertyType; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.itest.util.Ensure; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * @author Felix Project Team + */ +public class ComponentPropertyTypeWithDictionaryPassedInUpdateCallback { + public final static String ENSURE = "ComponentPropertyWithDictionaryPassedInUpdateCallback"; + + @PropertyType + @Retention(RetentionPolicy.CLASS) + @interface MyProperty { + String string_value(); + } + + @Component(provides=MyComponent.class) + @MyProperty(string_value="defstring") + public static class MyComponent { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_ensure; + + private Dictionary m_cnf; + private MyProperty m_prop; + + @ConfigurationDependency(propagate=true) + void updated(Dictionary cnf, MyProperty prop) { + m_cnf = cnf; + m_prop = prop; + } + + @Start + void start() { + System.out.println("MyComponent.start: cnf=" + m_cnf + ", string_value=" + m_prop.string_value()); + if ("configured".equals(m_cnf.get("string.value")) && "configured".equals(m_prop.string_value())) { + m_ensure.step(1); + } + } + } + + @Component + public static class MyConsumer { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_ensure; + Configuration m_conf; + + @ServiceDependency + void bind(ConfigurationAdmin cm) { + try { + m_conf = cm.getConfiguration(MyProperty.class.getName()); + Hashtable newprops = new Hashtable<>(); + newprops.put("string.value", "configured"); + m_conf.update(newprops); + } catch (IOException e) { + e.printStackTrace(); + } + } + + + @ServiceDependency(removed="remove", required=false) + void bind(MyComponent comp, Map props) { + try { + // first, we expect to be injected with MyComponent with the following properties: + // string.value=another_string and double_value=123 + if ("configured".equals(props.get("string.value"))) { + m_ensure.step(2); + m_conf.delete(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + void remove(MyComponent comp) { + m_ensure.step(3); + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/CompositeAnnotations.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/CompositeAnnotations.java new file mode 100644 index 00000000000..91fe0f10848 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/CompositeAnnotations.java @@ -0,0 +1,197 @@ +/* + * 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.dm.runtime.itest.components; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Composition; +import org.apache.felix.dm.annotation.api.Destroy; +import org.apache.felix.dm.annotation.api.Init; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.Registered; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.itest.util.Ensure; +import org.osgi.framework.ServiceRegistration; + +/** + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class CompositeAnnotations { + public interface C1Service { + } + + public interface Dependency extends Runnable { + } + + /** + * This service is also composed of the Component object. + */ + @Component + public static class C1 implements C1Service { + public final static String ENSURE = "CompositeAnnotations.C1"; + + /* We are composed of this object, which will also be injected with our dependencies */ + private final C2 m_c2 = new C2(); + + /* This dependency filter will be configured from our init method */ + @ServiceDependency(name = "D") + public volatile Dependency m_runnable; + + /* Object used to check that methods are called in the proper sequence */ + @ServiceDependency(filter = "(name=" + ENSURE + ")") + private volatile Ensure m_sequencer; + + /** + * Dynamically configure our "D" dependency, using a dependency customization map + */ + @Init + Map init() { + m_sequencer.step(1); + // Configure a filter for our dependency whose name is "D" + Map customization = new HashMap(); + customization.put("D.filter", "(foo=bar2)"); + customization.put("D.required", "true"); + return customization; + } + + /** + * Return the list of object our service is composed of + */ + @Composition + Object[] getComposition() { + return new Object[]{this, m_c2}; + } + + /** + * Our Service is starting, and our Composites will also be + */ + @Start + void start() { + m_sequencer.step(3); + m_runnable.run(); /* step 4 */ + // Our Component.start() method should be called once this method returns. + } + + /** + * Our provided service has been registered into the OSGi service registry. + */ + @Registered + void registered(ServiceRegistration sr) { + m_sequencer.step(7); + } + + /** + * Our Service is stopping, and our Composites will also be + */ + @Stop + void stop() { + m_sequencer.step(9); + // Our Component.stop() method should be called once this method returns. + } + + /** + * Our Service is destroying, and our Composites will also be. + */ + @Destroy + void destroy() { + m_sequencer.step(11); + // Our Component.destroy() method should be called once this method returns. + } + } + + /** + * The CompositeService is also made up of this Class. + */ + public static class C2 { + // Injected dependency (from CompositeService) + private volatile Ensure m_sequencer; + + // Injected dependency (from CompositeService) + public volatile Dependency m_runnable; + + // lifecycle callback (same method as the one from CompositeService) + void init() { + m_sequencer.step(2); + } + + // lifecycle callback (same method as the one from CompositeService) + void start() { + m_sequencer.step(5); + m_runnable.run(); /* step 6 */ + } + + void registered(ServiceRegistration sr) { + if (sr == null) { + m_sequencer.throwable(new Exception("null service registration")); + } + m_sequencer.step(8); + } + + // lifecycle callback (same method as the one from CompositeService) + void stop() { + m_sequencer.step(10); + } + + // lifecycle callback (same method as the one from CompositeService) + void destroy() { + m_sequencer.step(12); + } + } + + @Component + @Property(name = "foo", value = "bar1") + public static class Dependency1 implements Dependency { + public final static String ENSURE = "CompositeAnnotations.Dependency1"; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + @Start + void start() { + System.out.println("Dependency1.start"); + } + + public void run() { + m_sequencer.step(Integer.MAX_VALUE); // Makes the test fail. + } + } + + @Component + @Property(name = "foo", value = "bar2") + public static class Dependency2 implements Dependency { + public final static String ENSURE = "CompositeAnnotations.Dependency2"; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + @Start + void start() { + System.out.println("Dependency2.start"); + } + + public void run() { + m_sequencer.step(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ConfigurationDependencyAnnotation.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ConfigurationDependencyAnnotation.java new file mode 100644 index 00000000000..7ed926c7414 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ConfigurationDependencyAnnotation.java @@ -0,0 +1,118 @@ +/* + * 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.dm.runtime.itest.components; + +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.ConfigurationDependency; +import org.apache.felix.dm.annotation.api.Init; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.itest.util.Ensure; +import org.junit.Assert; + +/** + * @author Felix Project Team + */ +public class ConfigurationDependencyAnnotation { + @Component + public static class ConfigurableComponent { + public final static String ENSURE = "ConfigurableComponent"; + + volatile Ensure m_ensure; + volatile Dictionary m_conf; + + @ConfigurationDependency + void updated(Dictionary conf) { + m_conf = conf; + } + + @ServiceDependency(filter="(name=" + ENSURE + ")") + void bind(Ensure ensure) { + m_ensure = ensure; + Assert.assertNotNull(m_conf); + Assert.assertEquals("bar", m_conf.get("foo")); + m_ensure.step(1); + } + + @Start + void start() { + m_ensure.step(2); + } + + @Stop + void stop() { + m_ensure.step(3); + } + } + + @Component + public static class ConfigurableComponentWithDynamicExtraConfiguration { + public final static String ENSURE = "ConfigurableComponentWithDynamicExtraConfiguration"; + + volatile Ensure m_ensure; + volatile Dictionary m_conf; + + @ConfigurationDependency + void updated(Dictionary conf) { + m_conf = conf; + } + + @ServiceDependency(filter="(name=" + ENSURE + ")") + void bind(Ensure ensure) { + m_ensure = ensure; + Assert.assertNotNull(m_conf); + Assert.assertEquals("bar", m_conf.get("foo")); + m_ensure.step(1); + } + + @Init + Map init() { + Assert.assertNotNull(m_conf); + String dynamicPid = m_conf.get("dynamicPid"); + Assert.assertNotNull(dynamicPid); + Map map = new HashMap<>(); + map.put("dynamicConfig.pid", m_conf.get("dynamicPid")); + m_ensure.step(2); + return map; + } + + @ConfigurationDependency(name="dynamicConfig") + void extraConfiguration(Dictionary dynamicConf) { + if (dynamicConf != null) { + Assert.assertEquals("bar2", dynamicConf.get("foo2")); + m_ensure.step(3); + } + } + + @Start + void start() { + m_ensure.step(4); + } + + @Stop + void stop() { + m_ensure.step(5); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ConfigurationProxy.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ConfigurationProxy.java new file mode 100644 index 00000000000..1218e0abfc1 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ConfigurationProxy.java @@ -0,0 +1,111 @@ +/* + * 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.dm.runtime.itest.components; + +import java.io.IOException; +import java.util.Hashtable; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.ConfigurationDependency; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.itest.util.Ensure; +import org.junit.Assert; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * Tests for new Configuration Proxy Types (FELIX-5177) + */ +public class ConfigurationProxy { + // For ConfigurationDependency service + public final static String ENSURE_CONFIG_DEPENDENCY = "ConfigurationProxy.Ensure.Configuration"; + + public interface Config { + String getFoo(); + } + + // This component configures the Consumer component. + @Component + public static class ConsumerConfigurator { + @ServiceDependency(filter="(name=" + ENSURE_CONFIG_DEPENDENCY + ")") + Ensure m_ensure; + + @ServiceDependency + ConfigurationAdmin m_cm; + + private Configuration m_conf; + + @Start + void start() throws IOException { + m_conf = m_cm.getConfiguration(Config.class.getName()); + Hashtable props = new Hashtable<>(); + props.put("foo", "bar"); + m_conf.update(props); + } + + @Stop + void stop() throws IOException { + m_conf.delete(); + } + } + + // This consumer depends on the configuration and on the provider, using multiple method signatures. + @Component + public static class Consumer { + Config m_config; + Config m_config2; + + @ConfigurationDependency + void updated(Config cnf) { + if (cnf != null) { + Assert.assertNotNull(cnf); + m_config = cnf; + } else { + m_config = null; + m_ensure.step(); + } + } + + @ConfigurationDependency + void updated2(org.apache.felix.dm.Component comp, Config cnf) { + if (cnf != null) { + Assert.assertNotNull(comp); + Assert.assertNotNull(cnf); + m_config2 = cnf; + } else { + m_config2 = null; + m_ensure.step(); + } + } + + @ServiceDependency(filter="(name=" + ENSURE_CONFIG_DEPENDENCY + ")") + Ensure m_ensure; + + @Start + void start() { + Assert.assertNotNull(m_config); + Assert.assertNotNull(m_config2); + Assert.assertEquals("bar", m_config.getFoo()); + Assert.assertEquals("bar", m_config2.getFoo()); + m_ensure.step(1); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ExtraAdapterServiceProperties.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ExtraAdapterServiceProperties.java new file mode 100644 index 00000000000..76d7dc63869 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ExtraAdapterServiceProperties.java @@ -0,0 +1,95 @@ +/* + * 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.dm.runtime.itest.components; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.AdapterService; +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.itest.util.Ensure; + +/** + * This test validates that an adapter Service may specify some extra service properties + * from it's start callback. + * + * @author Felix Project Team + */ +@SuppressWarnings({"rawtypes", "serial"}) +public class ExtraAdapterServiceProperties { + public final static String ENSURE = "ExtraAdapterServiceProperties"; + + public interface Provider { + } + + public interface Provider2 { + } + + @Component + @Property(name = "foo", value = "bar") + public static class ProviderImpl implements Provider { + } + + @AdapterService(provides = Provider2.class, adapteeService = Provider.class) + @Property(name = "foo2", value = "bar2") + public static class Provider2Impl implements Provider2 { + protected Provider m_adaptee; + + @Start + Map start() { + return new HashMap() { + { + put("foo3", "bar3"); + } + }; + } + } + + @Component + public static class Consumer { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + private volatile Map m_properties; + + @ServiceDependency + void bind(Map properties, Provider2 provider2) { + m_properties = properties; + } + + @Start + void start() { + System.out.println("provider2 service properties: " + m_properties); + if ("bar".equals(m_properties.get("foo"))) { + m_sequencer.step(1); + } + + if ("bar2".equals(m_properties.get("foo2"))) { + m_sequencer.step(2); + } + + if ("bar3".equals(m_properties.get("foo3"))) { + m_sequencer.step(3); + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ExtraServiceProperties.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ExtraServiceProperties.java new file mode 100644 index 00000000000..bd0fb197e58 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ExtraServiceProperties.java @@ -0,0 +1,80 @@ +/* + * 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.dm.runtime.itest.components; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.itest.util.Ensure; + +/** + * This test validates that a basic Service may specify some extra service properties + * from it's start callback. + * + * @author Felix Project Team + */ +@SuppressWarnings({"rawtypes", "serial"}) +public class ExtraServiceProperties { + public final static String ENSURE = "ExtraServiceProperties"; + + public interface Provider { + } + + @Component + @Property(name = "foo", value = "bar") + public static class ProviderImpl implements Provider { + @Start + Map start() { + return new HashMap() { + { + put("foo2", "bar2"); + } + }; + } + } + + @Component + public static class Consumer { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + private volatile Map m_properties; + + @ServiceDependency + void bindProvider(Map properties, Provider m_provider) { + m_properties = properties; + } + + @Start + void start() { + System.out.println("provider service properties: " + m_properties); + if ("bar".equals(m_properties.get("foo"))) { + m_sequencer.step(1); + } + + if ("bar2".equals(m_properties.get("foo2"))) { + m_sequencer.step(2); + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/FELIX5337.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/FELIX5337.java new file mode 100644 index 00000000000..da9465b62b2 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/FELIX5337.java @@ -0,0 +1,138 @@ +/* +* 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.dm.runtime.itest.components; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Inject; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.annotation.api.ServiceDependency.Any; +import org.apache.felix.dm.itest.util.Ensure; +import org.junit.Assert; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; + +/** + * This test validates that all services can be discovered using annotation and using "(objectClass=*)" filter. + */ +@Component +public class FELIX5337 implements FrameworkListener { + /** + * We wait for an Ensure object which has a name matching this constant. + */ + public final static String ENSURE = "Felix5337"; + + /** + * We need the bundle context because we register a framework listener in order to detect + * when all bundles have been started. + */ + @Inject + volatile BundleContext m_bctx; + + /** + * We use DM api in order to count all available services. The services count will be compared to the + * number of services found using annotation. + */ + @Inject + DependencyManager m_dm; + + /** + * The Ensure service is registered by the FELIX5337Test test. + */ + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + /** + * Number of services found using annotations. + */ + volatile int m_services; + + /** + * Number of services found using DM api. + */ + volatile int m_servicesAPI; + + /** + * Component used to track all services (using DM API). + */ + volatile org.apache.felix.dm.Component m_component; + + /** + * The component used to track all available services (using DM API). + */ + public class ApiCallback { + void bindServiceAPI(Object service) { + m_servicesAPI++; + } + } + + /** + * Track all available services using annotation. + */ + @ServiceDependency(service=Any.class) + void bindService(Object service) { + m_services++; // thread safe, in DM, service callbacks are always thread safe. + } + + @Start + void start() { + m_sequencer.step(1); + // use DM api to count all available services. + m_component = m_dm.createComponent().setImplementation(new ApiCallback()) + .add(m_dm.createServiceDependency().setService("(objectClass=*)").setCallbacks("bindServiceAPI", null)); + m_dm.add(m_component); + + // Register a framework listener to detect when the framework is started (at this point, all services are registered). + if (m_bctx.getBundle(0).getState() != Bundle.ACTIVE) { + m_bctx.addFrameworkListener(this); + } else { + frameworkEvent(new FrameworkEvent(FrameworkEvent.STARTED, m_bctx.getBundle(), null)); + } + } + + @Stop + void stop() { + m_dm.remove(m_component); + m_sequencer.step(3); + } + + @Override + public void frameworkEvent(FrameworkEvent event) { + switch (event.getType()) { + case FrameworkEvent.STARTED: + // some services may be registered asynchronously. so, wait 2 seconds to be sure all services are registered (it's dirty, but how to avoid this ?) + new Thread(() -> { + try + { + Thread.sleep(2000); + } catch (InterruptedException e) + { + } + // Make sure all services found using DM api matches the number of services found using annotations. + Assert.assertEquals(m_services, m_servicesAPI); + m_sequencer.step(2); + }).start(); + break; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/FELIX5337_MatchAllServicesWithFilter.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/FELIX5337_MatchAllServicesWithFilter.java new file mode 100644 index 00000000000..9d24fc3e420 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/FELIX5337_MatchAllServicesWithFilter.java @@ -0,0 +1,83 @@ +/* +* 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.dm.runtime.itest.components; + +import java.util.Dictionary; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.ServiceDependency.Any; +import org.apache.felix.dm.itest.util.Ensure; +import org.junit.Assert; + +@Component +public class FELIX5337_MatchAllServicesWithFilter { + /** + * We wait for an Ensure object which has a name matching this constant. + */ + public final static String ENSURE = "FELIX5337_MatchAllServicesWithFilter"; + + @Component(provides=Service1.class) + @Property(name="matchall", value="foo") + public static class Service1 { + } + + @Component(provides=Service2.class) + @Property(name="matchall", value="foo") + public static class Service2 { + } + + @Component + public static class MatchAll { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + @ServiceDependency(service=Any.class, filter="(matchall=foo)") + Map> m_services; + + @Start + void start() { + System.out.println(m_services); + Assert.assertEquals(2, m_services.size()); + + AtomicBoolean service1Injected = new AtomicBoolean(false); + AtomicBoolean service2Injected = new AtomicBoolean(false); + + m_services.forEach((k,v) -> { + Assert.assertEquals("foo", v.get("matchall")); + if (k.getClass().equals(Service1.class)) service1Injected.set(true); + }); + + m_services.forEach((k,v) -> { + Assert.assertEquals("foo", v.get("matchall")); + if (k.getClass().equals(Service2.class)) service2Injected.set(true); + }); + + Assert.assertEquals(true, service1Injected.get()); + Assert.assertEquals(true, service2Injected.get()); + + m_sequencer.step(1); + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/FELIX5956.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/FELIX5956.java new file mode 100644 index 00000000000..af5bab3c0f9 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/FELIX5956.java @@ -0,0 +1,99 @@ +/* + * 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.dm.runtime.itest.components; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Init; +import org.apache.felix.dm.annotation.api.LifecycleController; +import org.apache.felix.dm.annotation.api.Registered; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.annotation.api.Unregistered; +import org.apache.felix.dm.itest.util.Ensure; +import org.osgi.framework.ServiceRegistration; + +/** + * Check if a lifecycle controller runnable method works when called synchronously from init method. + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class FELIX5956 { + + public final static String ENSURE = "FELIX5956"; + + public interface MyService { + public void deactivate(); + } + + @Component + public static class MyServiceImpl implements MyService { + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_ensure; + + @LifecycleController + volatile Runnable m_start; + + @LifecycleController(start=false) + volatile Runnable m_stop; + + @Init + protected void init() { + m_ensure.step(1); + m_start.run(); + m_ensure.step(2); + } + + @Start + protected void start() { + m_ensure.step(3); + } + + public void deactivate() { + m_stop.run(); + } + + @Stop + protected void stop() { + m_ensure.step(5); + } + } + + /** + * Consumes a service which is provided by the {@link MyServiceImpl} class. + */ + @Component + public static class Consumer { + @ServiceDependency + volatile MyService m_myService; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_ensure; + + + @Start + protected void start() { + if (m_myService != null) { + m_ensure.step(4); + } + m_myService.deactivate(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/FactoryConfigurationAdapterAnnotation.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/FactoryConfigurationAdapterAnnotation.java new file mode 100644 index 00000000000..1042e3ec4fc --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/FactoryConfigurationAdapterAnnotation.java @@ -0,0 +1,160 @@ +/* +* 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.dm.runtime.itest.components; + +import java.util.Dictionary; +import java.util.Map; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Inject; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.itest.util.Ensure; +import org.osgi.framework.BundleContext; + +/** + * @author Felix Project Team + */ +@SuppressWarnings({"rawtypes"}) +public class FactoryConfigurationAdapterAnnotation { + public interface ServiceInterface { + } + + @Component + public static class ServiceClient { + @ServiceDependency(filter="(name=" + ServiceProvider.ENSURE + ")") + private volatile Ensure m_sequencer; + + @ServiceDependency(changed = "changeServiceProvider", removed="removedServiceProvider") + void addServiceProvider(Map props, ServiceInterface si) { + // props should contain foo=bar, foo2=bar2 + if (!"bar".equals(props.get("foo"))) { + throw new IllegalArgumentException("configuration does not contain foo=bar: " + props); + } + if (!"bar2".equals(props.get("foo2"))) { + throw new IllegalArgumentException("configuration does not contain foo2=bar2: " + props); + } + m_sequencer.step(2); + } + + void changeServiceProvider(Map props, ServiceInterface si) { + System.out.println("ServiceClient: changeServiceProvider"); + // props should contain foo=bar, foo2=bar2_modified + if (!"bar".equals(props.get("foo"))) { + throw new IllegalArgumentException("configuration does not contain foo=bar: " + props); + } + if (!"bar2_modified".equals(props.get("foo2"))) { + throw new IllegalArgumentException("configuration does not contain foo2=bar2: " + props); + } + + m_sequencer.step(4); + } + + void removedServiceProvider(ServiceInterface si) { + m_sequencer.step(5); + } + } + + /** + * This service is instantiated when a factory configuration is created from ConfigAdmin + */ + @Component(factoryPid = "FactoryPidTest", propagate = true) + @Property(name = "foo", value = "bar") + public static class ServiceProvider implements ServiceInterface { + public final static String ENSURE = "FactoryConfigurationAdapterAnnotation.ServiceProvider"; + + @ServiceDependency(filter="(name=" + ENSURE + ")") + private volatile Ensure m_sequencer; + + private volatile boolean m_started; + + // Check auto config injections + @Inject + volatile BundleContext m_bc; + BundleContext m_bcNotInjected; + + @Inject + volatile DependencyManager m_dm; + DependencyManager m_dmNotInjected; + + @Inject + volatile org.apache.felix.dm.Component m_component; + org.apache.felix.dm.Component m_componentNotInjected; + + // Either initial config, or an updated config + protected void updated(Dictionary conf) { + if (m_started) { + // conf should contain foo2=bar2_modified + if (!"bar2_modified".equals(conf.get("foo2"))) { + m_sequencer.throwable(new Exception("configuration does not contain foo=bar")); + } + m_sequencer.step(3); + } else { + // conf should contain foo2=bar2 + if (!"bar2".equals(conf.get("foo2"))) { + throw new IllegalArgumentException("configuration does not contain foo2=bar2: " + conf); + } + } + } + + @Start + void start() { + checkInjectedFields(); + m_started = true; + m_sequencer.step(1); + } + + @Stop + void stop() { + m_sequencer.step(6); + } + + private void checkInjectedFields() { + if (m_bc == null) { + m_sequencer.throwable(new Exception("Bundle Context not injected")); + return; + } + if (m_bcNotInjected != null) { + m_sequencer.throwable(new Exception("Bundle Context must not be injected")); + return; + } + + if (m_dm == null) { + m_sequencer.throwable(new Exception("DependencyManager not injected")); + return; + } + if (m_dmNotInjected != null) { + m_sequencer.throwable(new Exception("DependencyManager must not be injected")); + return; + } + + if (m_component == null) { + m_sequencer.throwable(new Exception("Component not injected")); + return; + } + if (m_componentNotInjected != null) { + m_sequencer.throwable(new Exception("Component must not be injected")); + return; + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/FactoryConfigurationAdapterServiceTestWithPublisher.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/FactoryConfigurationAdapterServiceTestWithPublisher.java new file mode 100644 index 00000000000..5b4ae11c8dc --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/FactoryConfigurationAdapterServiceTestWithPublisher.java @@ -0,0 +1,139 @@ +/* + * 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.dm.runtime.itest.components; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Init; +import org.apache.felix.dm.annotation.api.LifecycleController; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.itest.util.Ensure; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * Test a FactoryConfigurationAdapterService which provides its interface using a @ServiceLifecycle. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes", "serial"}) +public class FactoryConfigurationAdapterServiceTestWithPublisher { + public final static String PID="FactoryConfigurationAdapterServiceTestWithPublisher.PID"; + public final static String ENSURE = "FactoryConfigurationAdapterServiceTestWithPublisher"; + + public interface Provider { + } + + @Component + public static class Consumer { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + @ServiceDependency(required = false, removed = "unbind") + void bind(Map properties, Provider provider) { + m_sequencer.step(1); + // check ProviderImpl properties + if ("bar".equals(properties.get("foo"))) { + m_sequencer.step(2); + } + // check extra ProviderImpl properties (returned by start method) + if ("bar2".equals(properties.get("foo2"))) { + m_sequencer.step(3); + } + // check Factory Configuration properties + if ("bar3".equals(properties.get("foo3"))) { + m_sequencer.step(4); + } + } + + void unbind(Provider provider) { + m_sequencer.step(5); + } + } + + @Component + public static class Configurator { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + volatile Configuration m_conf; + + @ServiceDependency + volatile ConfigurationAdmin m_cm; + + @Start + void start() throws IOException { + m_conf = m_cm.createFactoryConfiguration(PID, null); + m_conf.update(new Hashtable() { + { + put("foo3", "bar3"); + } + }); + } + + @Stop + void stop() throws IOException { + m_conf.delete(); + } + } + + @Component(propagate = true, factoryPid = PID, updated = "updated") + @Property(name = "foo", value = "bar") + public static class ProviderImpl implements Provider { + @LifecycleController + volatile Runnable m_publisher; // injected and used to register our service + + @LifecycleController(start = false) + volatile Runnable m_unpublisher; // injected and used to unregister our service + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + void updated(Dictionary conf) { + } + + @Init + void init() { + // register service in 1 second + Utils.schedule(m_publisher, 1000); + // unregister the service in 2 seconds + Utils.schedule(m_unpublisher, 2000); + } + + @Start + Map start() { + // Add some extra service properties ... they will be appended to the one we have defined + // in the @Service annotation. + return new HashMap() { + { + put("foo2", "bar2"); + } + }; + } + + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/FactoryPidWithPropertyTypeAnnotation.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/FactoryPidWithPropertyTypeAnnotation.java new file mode 100644 index 00000000000..554b9027744 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/FactoryPidWithPropertyTypeAnnotation.java @@ -0,0 +1,130 @@ +/* + * 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.dm.runtime.itest.components; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.PropertyType; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.itest.util.Ensure; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * A Factory Pid component which provides its service properties using property type annotations. + * The service properties can also be overriden from factory configuration. + * + * @author Felix Project Team + */ +public class FactoryPidWithPropertyTypeAnnotation { + public final static String ENSURE = "FactoryPidWithPropertyTypeAnnotation"; + + @PropertyType + @Retention(RetentionPolicy.CLASS) + @interface MyProperties { + String string_value() default "defstring"; + double double_value() default 123; + } + + @PropertyType + @Retention(RetentionPolicy.CLASS) + @interface MyProperties2 { + String string_value2() default "defstring2"; + double double_value2() default 456; + } + + /** + * Service properties are defined using the two annotations above, + * and we override some service properties using the factory configuration + */ + @Component(provides=MyComponent.class, propagate=true, factoryPid="org.apache.felix.dm.runtime.itest.components.FactoryPidWithPropertyTypeAnnotation$MyProperties") + @MyProperties(string_value="string") + @MyProperties2(string_value2="string2") + public static class MyComponent { + void updated(MyProperties props, MyProperties2 props2) { + if (props != null) { + System.out.println("MyComponent.updated: string_value=" + props.string_value() + ", double_value=" + props.double_value() + + "string_value2=" + props2.string_value2() + ", double_value2=" + props2.double_value2()); + } + } + } + + @Component + public static class MyConsumer { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + Ensure m_ensure; + + Configuration m_conf; + + @ServiceDependency + void bind(ConfigurationAdmin cm) { + try { + m_conf = cm.createFactoryConfiguration(MyProperties.class.getName()); + Hashtable newprops = new Hashtable<>(); + newprops.put("string.value", "CM"); + m_conf.update(newprops); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + @ServiceDependency(required=false, changed="change", removed="remove") + void add(MyComponent comp, Map props) { + try { + // first, we expect to be injected with MyComponent with the following properties: + // string.value=CM and double_value=123 + if ("CM".equals(props.get("string.value")) && new Double(123).equals(props.get("double.value")) && + "string2".equals(props.get("string.value2")) && new Double(456).equals(props.get("double.value2"))) { + m_ensure.step(1); + + // at this point, let's reconfigure the factory component + Hashtable newprops = new Hashtable<>(); + newprops.put("string.value", "CM modified"); + m_conf.update(newprops); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + void change(MyComponent comp, Map props) { + try { + System.out.println("MyConsumer.change: " + props); + if ("CM modified".equals(props.get("string.value")) && new Double(123).equals(props.get("double.value")) && + "string2".equals(props.get("string.value2")) && new Double(456).equals(props.get("double.value2"))) { + m_ensure.step(2); + m_conf.delete(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + void remove(MyComponent comp, Map props) { + m_ensure.step(3); + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/Felix4050.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/Felix4050.java new file mode 100644 index 00000000000..0d4810235af --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/Felix4050.java @@ -0,0 +1,150 @@ +/* +* 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.dm.runtime.itest.components; + +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Destroy; +import org.apache.felix.dm.annotation.api.Init; +import org.apache.felix.dm.annotation.api.Inject; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.itest.util.Ensure; +import org.osgi.framework.BundleContext; + +/** + * @author Felix Project Team + */ +public class Felix4050 { + public final static String ENSURE = "Felix4050"; + + @Component(provides = {A.class}) + public static class A { + + } + + public interface B { + void run(); + } + + @Component + @Property(name = "type", value = "b1") + public static class B1 implements B { + public void run() { + } + } + + @Component(provides = {}) + public static class B2 implements B { + @Inject + volatile BundleContext _ctx; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + @Start + void start() { + Thread t = new Thread(new Runnable() { + public void run() { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + } + System.out.println("Registering B2"); + Dictionary props = new Hashtable<>(); + props.put("type", "b2"); + _ctx.registerService(B.class.getName(), B2.this, props); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + } + } + }); + t.start(); + } + + public void run() { + m_sequencer.step(3); + } + } + + @Component + public static class S { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + @Inject + volatile org.apache.felix.dm.Component _component; + + volatile A m_a; + volatile B m_b; + + void bind(A a) { + System.out.println("bind(A): " + a); + m_a = a; + } + + @ServiceDependency(name = "B") + void bind(B b) { + System.out.println("bind(B): " + b); + m_b = b; + } + + @Init + Map init() { + m_sequencer.step(1); + _component.add(_component.getDependencyManager().createServiceDependency() + .setService(A.class).setRequired(true) + .setCallbacks("bind", null)); + Map props = new HashMap<>(); + props.put("B.required", "true"); + props.put("B.filter", "(type=b2)"); + return props; + } + + @Start + void start() { + if (m_a == null) { + throw new RuntimeException("A not injected"); + } + if (m_b == null) { + throw new RuntimeException("B not injected"); + } + m_sequencer.step(2); + m_b.run(); // step(3) + } + + @Stop + void stop() { + m_sequencer.step(4); + } + + @Destroy + void destroy() { + m_sequencer.step(5); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/Felix4357.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/Felix4357.java new file mode 100644 index 00000000000..c38182740dd --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/Felix4357.java @@ -0,0 +1,109 @@ +/* + * 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.dm.runtime.itest.components; + +import static org.apache.felix.dm.runtime.itest.components.Utils.assertArrayEquals; +import static org.apache.felix.dm.runtime.itest.components.Utils.assertEquals; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.Registered; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.itest.util.Ensure; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; + +/** + * Checks support of primitive types for @Property annotation. + * + * @author Felix Project Team + */ +@Component(provides=Felix4357.class) +@Property(name="v1", value="s") +@Property(name="v2", value={"s1", "s2"}) +@Property(name="v3", values={"s1", "s2"}) +@Property(name="v4", value="1", type=Long.class) +@Property(name="v5", longValue=1) +@Property(name="v6", longValue={1, 2}) +@Property(name="v7", value="1", type=Double.class) +@Property(name="v8", doubleValue=1) +@Property(name="v9", doubleValue={1, 2}) +@Property(name="v10", value="1", type=Float.class) +@Property(name="v11", floatValue=1) +@Property(name="v12", floatValue={1, 2}) +@Property(name="v13", value="1", type=Integer.class) +@Property(name="v14", intValue=1) +@Property(name="v15", intValue={1, 2}) +@Property(name="v16", value="65", type=Byte.class) +@Property(name="v17", byteValue=65) +@Property(name="v18", byteValue={65, 66}) +@Property(name="v19", value="A", type=Character.class) +@Property(name="v20", charValue='A') +@Property(name="v21", charValue={'A', 'B'}) +@Property(name="v22", value="true", type=Boolean.class) +@Property(name="v23", booleanValue=true) +@Property(name="v24", booleanValue={true, false}) +@Property(name="v25", value="1", type=Short.class) +@Property(name="v26", shortValue=1) +@Property(name="v27", shortValue={1, 2}) +@Property(name="v28", value="65", type=Character.class) +@Property(name="v29", charValue=65) +@Property(name="v30", charValue={65, 66}) +@SuppressWarnings("rawtypes") +public class Felix4357 { + public final static String ENSURE = "Felix4357"; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_ensure; + + @Registered + void registered(ServiceRegistration sr) { + ServiceReference ref = sr.getReference(); + assertEquals(m_ensure, ref, "v1", "s", 1); + assertArrayEquals(m_ensure, ref, "v2", new String[] {"s1", "s2"}, 2); + assertArrayEquals(m_ensure, ref, "v3", new String[] {"s1", "s2"}, 3); + assertEquals(m_ensure, ref, "v4", new Long(1), 4); + assertEquals(m_ensure, ref, "v5", new Long(1), 5); + assertArrayEquals(m_ensure, ref, "v6", new Long[] { 1L, 2L } , 6); + assertEquals(m_ensure, ref, "v7", new Double(1), 7); + assertEquals(m_ensure, ref, "v8", new Double(1), 8); + assertArrayEquals(m_ensure, ref, "v9", new Double[] { 1.0, 2.0 } , 9); + assertEquals(m_ensure, ref, "v10", new Float(1), 10); + assertEquals(m_ensure, ref, "v11", new Float(1), 11); + assertArrayEquals(m_ensure, ref, "v12", new Float[] { 1.f, 2.f } , 12); + assertEquals(m_ensure, ref, "v13", new Integer(1), 13); + assertEquals(m_ensure, ref, "v14", new Integer(1), 14); + assertArrayEquals(m_ensure, ref, "v15", new Integer[] { 1, 2 } , 15); + assertEquals(m_ensure, ref, "v16", Byte.valueOf("65"), 16); + assertEquals(m_ensure, ref, "v17", Byte.valueOf("65"), 17); + assertArrayEquals(m_ensure, ref, "v18", new Byte[] { Byte.valueOf("65"), Byte.valueOf("66") } , 18); + assertEquals(m_ensure, ref, "v19", Character.valueOf('A'), 19); + assertEquals(m_ensure, ref, "v20", Character.valueOf('A'), 20); + assertArrayEquals(m_ensure, ref, "v21", new Character[] { 'A', 'B' } , 21); + assertEquals(m_ensure, ref, "v22", Boolean.valueOf(true), 22); + assertEquals(m_ensure, ref, "v23", Boolean.valueOf(true), 23); + assertArrayEquals(m_ensure, ref, "v24", new Boolean[] { true, false } , 24); + assertEquals(m_ensure, ref, "v25", Short.valueOf((short) 1), 25); + assertEquals(m_ensure, ref, "v26", Short.valueOf((short) 1), 26); + assertArrayEquals(m_ensure, ref, "v27", new Short[] { 1, 2 } , 27); + assertEquals(m_ensure, ref, "v28", Character.valueOf('A'), 28); + assertEquals(m_ensure, ref, "v29", Character.valueOf('A'), 29); + assertArrayEquals(m_ensure, ref, "v30", new Character[] { 'A', 'B' } , 30); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/Felix5236.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/Felix5236.java new file mode 100644 index 00000000000..e815ad4fd9c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/Felix5236.java @@ -0,0 +1,48 @@ +/* + * 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.dm.runtime.itest.components; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.Registered; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.itest.util.Ensure; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; + +/** + * Checks support of primitive types for @Property annotation. + * + * @author Felix Project Team + */ +@Component(provides=Felix5236.class) +@Property(name="v1", value="s") +@SuppressWarnings("rawtypes") +public class Felix5236 { + public final static String ENSURE = "Felix5236"; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_ensure; + + @Registered + void registered(ServiceRegistration sr) { + ServiceReference ref = sr.getReference(); + Utils.assertEquals(m_ensure, ref, "v1", "s", 1); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/InheritedAnnotations.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/InheritedAnnotations.java new file mode 100644 index 00000000000..4675620b667 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/InheritedAnnotations.java @@ -0,0 +1,82 @@ +/* + * 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.dm.runtime.itest.components; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Init; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.itest.util.Ensure; +import org.junit.Assert; + +public class InheritedAnnotations +{ + public final static String ENSURE = "InheritedAnnotations"; + + @Component(provides=Service1.class) + public static class Service1 { + @ServiceDependency(filter="(name=" + ENSURE + ")") + Ensure m_ensure; + + } + + @Component(provides=Service2.class) + public static class Service2 { + @ServiceDependency(filter="(name=" + ENSURE + ")") + Ensure m_ensure; + } + + public static class Base { + @ServiceDependency(filter="(name=" + ENSURE + ")") + protected Ensure m_ensure; + + protected Service1 m_s1; + + @ServiceDependency + void bind(Service1 s1) { + m_s1 = s1; + } + + @Init + void init() { + Assert.assertNotNull(m_ensure); + Assert.assertNotNull(m_s1); + m_ensure.step(1); + } + } + + @Component + public static class Child extends Base { + private Service2 m_s2; + + @ServiceDependency + void bind(Service2 s2) { + m_s2 = s2; + } + + @Start + void start() { + Assert.assertNotNull(m_ensure); + Assert.assertNotNull(m_s1); + Assert.assertNotNull(m_s2); + m_ensure.step(2); + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/JaxrsComponentPropertyTypeAnnotation.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/JaxrsComponentPropertyTypeAnnotation.java new file mode 100644 index 00000000000..7d8a6bfaed3 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/JaxrsComponentPropertyTypeAnnotation.java @@ -0,0 +1,61 @@ +/* + * 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.dm.runtime.itest.components; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.ConfigurationDependency; +import org.apache.felix.dm.annotation.api.PropertyType; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.itest.util.Ensure; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsName; + +/** + * @author Felix Project Team + */ +public class JaxrsComponentPropertyTypeAnnotation { + + @Component(provides=JaxRsComponent.class) + @JaxrsName("foo") + public static class JaxRsComponent { + } + + @Component + public static class JaxRsConsumer { + public final static String ENSURE = "ComponentPropertyTypeAnnotations.JaxRsConsumer"; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_ensure; + + @ServiceDependency + void bind(JaxRsComponent jaxrs, Map props) { + m_ensure.step(1); + if ("foo".equals(props.get("osgi.jaxrs.name"))) { + m_ensure.step(2); + } + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/MethodSignatures.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/MethodSignatures.java new file mode 100644 index 00000000000..6d1823b614f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/MethodSignatures.java @@ -0,0 +1,338 @@ +/* + * 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.dm.runtime.itest.components; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.ConfigurationDependency; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.itest.util.Ensure; +import org.junit.Assert; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * Tests various bind method signatures + */ +@SuppressWarnings("rawtypes") +public class MethodSignatures { + // For Consumer service + public final static String ENSURE_SERVICE_DEPENDENCY = "MethodSignatures1"; + + // For FactoryPidComponent + public final static String ENSURE_FACTORYPID = "MethodSignatures2"; + + // This component configures the Consumer component. + @Component + public static class ConsumerConfigurator { + @ServiceDependency(filter="(name=" + ENSURE_SERVICE_DEPENDENCY + ")") + Ensure m_ensure; + + @ServiceDependency + ConfigurationAdmin m_cm; + + private Configuration m_conf; + private Configuration m_conf2; + + @Start + void start() throws IOException { + m_conf = m_cm.getConfiguration(Consumer.class.getName()); + Hashtable props = new Hashtable<>(); + props.put("foo", "bar"); + m_conf.update(props); + + m_conf2 = m_cm.getConfiguration(ConsumerConfig.class.getName()); + props = new Hashtable<>(); + props.put("foo", "bar"); + m_conf2.update(props); + } + + @Stop + void stop() throws IOException { + m_conf.delete(); + m_conf2.delete(); + } + } + + // A simple provider service + @Component(provides=Provider.class) + public static class Provider { + @ServiceDependency(filter="(name=" + ENSURE_SERVICE_DEPENDENCY + ")") + Ensure m_ensure; + } + + public interface ConsumerConfig { + String getFoo(); + } + + // This consumer depends on the configuration and on the provider, using multiple method signatures. + @Component + public static class Consumer { + Dictionary m_properties; + Dictionary m_properties2; + ConsumerConfig m_config; + ConsumerConfig m_config2; + + @ConfigurationDependency // pid=Consumer.class + void updated(Dictionary properties) { + if (properties != null) { + m_properties = properties; + } else { + m_ensure.step(); + } + } + + @ConfigurationDependency // pid = Consumer.class + void updated2(org.apache.felix.dm.Component component, Dictionary properties) { + if (properties != null) { + Assert.assertNotNull(component); + m_properties2 = properties; + } else { + m_ensure.step(); + } + } + + @ConfigurationDependency + void updated3(ConsumerConfig cnf) { + if (cnf != null) { + Assert.assertNotNull(cnf); + m_config = cnf; + } else { + m_ensure.step(); + } + } + + @ConfigurationDependency + void updated4(org.apache.felix.dm.Component comp, ConsumerConfig cnf) { + if (cnf != null) { + Assert.assertNotNull(comp); + Assert.assertNotNull(cnf); + m_config2 = cnf; + } else { + m_ensure.step(); + } + } + + @ServiceDependency(filter="(name=" + ENSURE_SERVICE_DEPENDENCY + ")") + Ensure m_ensure; + + @ServiceDependency + void bind(org.apache.felix.dm.Component component, ServiceReference ref, Provider provider) { + Assert.assertNotNull(component); + Assert.assertNotNull(ref); + Assert.assertNotNull(provider); + m_ensure.step(); + } + + @ServiceDependency + void bind(org.apache.felix.dm.Component component, Provider provider) { + Assert.assertNotNull(component); + Assert.assertNotNull(provider); + m_ensure.step(); + } + + @ServiceDependency + void bind(org.apache.felix.dm.Component component, Map properties, Provider provider) { + Assert.assertNotNull(component); + Assert.assertNotNull(properties); + Assert.assertNotNull(provider); + m_ensure.step(); + } + + @ServiceDependency + void bind(ServiceReference ref, Provider provider) { + Assert.assertNotNull(ref); + Assert.assertNotNull(provider); + m_ensure.step(); + } + + @ServiceDependency + void bind(Provider provider) { + Assert.assertNotNull(provider); + m_ensure.step(); + } + + @ServiceDependency + void bind(Provider provider, Map properties) { + Assert.assertNotNull(provider); + Assert.assertNotNull(properties); + m_ensure.step(); + } + + @ServiceDependency + void bind(Map properties, Provider provider) { + Assert.assertNotNull(properties); + Assert.assertNotNull(provider); + m_ensure.step(); + } + + @ServiceDependency + void bind(Provider provider, Dictionary properties) { + Assert.assertNotNull(properties); + Assert.assertNotNull(provider); + m_ensure.step(); + } + + @ServiceDependency + void bind(Dictionary properties, Provider provider) { + Assert.assertNotNull(properties); + Assert.assertNotNull(provider); + m_ensure.step(); + } + + @Start + void start() { + Assert.assertNotNull(m_properties); + Assert.assertNotNull(m_properties2); + Assert.assertEquals("bar", m_properties.get("foo")); + Assert.assertEquals("bar", m_properties2.get("foo")); + Assert.assertNotNull(m_config); + Assert.assertEquals("bar", m_config.getFoo()); + Assert.assertEquals("bar", m_config2.getFoo()); + m_ensure.step(10); + } + } + + public interface Config { + String getFoo(); + } + + // This component configures the FactoryPidComponent / FactoryPidComponent2 components. + @Component + public static class FactoryPidConfigurator { + @ServiceDependency(filter="(name=" + ENSURE_FACTORYPID + ")") + Ensure m_ensure; + + @ServiceDependency + ConfigurationAdmin m_cm; + + private Configuration m_conf1; + private Configuration m_conf2; + private Configuration m_conf3; + + @Start + void start() throws IOException { + m_conf1 = m_cm.createFactoryConfiguration(FactoryPidComponent.class.getName()); + Hashtable props = new Hashtable<>(); + props.put("foo", "bar"); + m_conf1.update(props); + + m_conf2 = m_cm.createFactoryConfiguration(FactoryPidComponent2.class.getName()); + props = new Hashtable<>(); + props.put("foo", "bar"); + m_conf2.update(props); + + m_conf3 = m_cm.createFactoryConfiguration(Config.class.getName()); + props = new Hashtable<>(); + props.put("foo", "bar"); + m_conf3.update(props); + } + + @Stop + void stop() throws IOException { + m_conf1.delete(); + m_conf2.delete(); + m_conf3.delete(); + } + } + + // This is a factory pid component with an updated callback having the "updated(Dictionary)" signature + @Component(factoryPid="org.apache.felix.dm.runtime.itest.components.MethodSignatures$FactoryPidComponent") + public static class FactoryPidComponent { + Dictionary m_properties; + + void updated(Dictionary properties) { + m_properties = properties; + } + + @ServiceDependency(filter="(name=" + ENSURE_FACTORYPID + ")") + Ensure m_ensure; + + @Start + void start() { + Assert.assertNotNull(m_properties); + Assert.assertEquals("bar", m_properties.get("foo")); + m_ensure.step(); + } + + @Stop + void stop() { + m_ensure.step(); + } + } + + // This is a factory pid component with an updated callback having the "updated(Component, Dictionary)" signature + @Component(factoryPid="org.apache.felix.dm.runtime.itest.components.MethodSignatures$FactoryPidComponent2") + public static class FactoryPidComponent2 { + Dictionary m_properties; + + void updated(org.apache.felix.dm.Component component, Dictionary properties) { + Assert.assertNotNull(component); + m_properties = properties; + } + + @ServiceDependency(filter="(name=" + ENSURE_FACTORYPID + ")") + Ensure m_ensure; + + @Start + void start() { + Assert.assertNotNull(m_properties); + Assert.assertEquals("bar", m_properties.get("foo")); + m_ensure.step(); + } + + @Stop + void stop() { + m_ensure.step(); + } + } + + // This is a factory pid component with an updated callback having the "updated(Config)" signature + @Component(factoryPid="org.apache.felix.dm.runtime.itest.components.MethodSignatures$Config") + public static class FactoryPidComponent3 { + Config m_properties; + + void updated(Config properties) { + m_properties = properties; + } + + @ServiceDependency(filter="(name=" + ENSURE_FACTORYPID + ")") + Ensure m_ensure; + + @Start + void start() { + Assert.assertNotNull(m_properties); + Assert.assertEquals("bar", m_properties.getFoo()); + m_ensure.step(); + } + + + @Stop + void stop() { + m_ensure.step(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/MultipleAnnotations.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/MultipleAnnotations.java new file mode 100644 index 00000000000..9bd622dc566 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/MultipleAnnotations.java @@ -0,0 +1,137 @@ +/* +* 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.dm.runtime.itest.components; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Composition; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.itest.util.Ensure; + + +/** + * @author Felix Project Team + */ +public class MultipleAnnotations { + public final static String ENSURE = "MultipleAnnotations"; + + public static class Composite { + void bind(Ensure seq) { + seq.step(2); + } + } + + @Component + public static class ServiceConsumer { + @ServiceDependency(filter="(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + @ServiceDependency(filter = "(foo=bar)") + volatile ServiceInterface m_service; + + @Start + void start() { + m_sequencer.step(6); + m_service.doService(); + } + + @Stop + void stop() { + m_sequencer.step(8); + } + } + + public interface ServiceInterface { + public void doService(); + } + + @Component + @Property(name = "foo", value = "bar") + public static class ServiceProvider implements ServiceInterface { + @ServiceDependency(filter="(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + volatile ServiceProvider2 m_serviceProvider2; + + @ServiceDependency(removed = "unbind") + void bind(ServiceProvider2 provider2) { + m_serviceProvider2 = provider2; + } + + @Start + void start() { + m_serviceProvider2.step(4); + m_sequencer.step(5); + } + + @Stop + void stop() { + m_sequencer.step(9); + } + + void unbind(ServiceProvider2 provider2) { + m_sequencer.step(10); + } + + public void doService() { + m_sequencer.step(7); + } + } + + @Component(provides = {ServiceProvider2.class}, factoryMethod = "create") + public static class ServiceProvider2 { + final Composite m_composite = new Composite(); + volatile Ensure m_sequencer; + + static ServiceProvider2 create() { + return new ServiceProvider2(); + } + + @ServiceDependency(required = false, filter = "(foo=bar)") // NullObject + volatile Runnable m_runnable; + + @ServiceDependency(service = Ensure.class, filter="(name=" + ENSURE + ")") + void bind(Ensure seq) { + m_sequencer = seq; + m_sequencer.step(1); + } + + @Start + void start() { + m_sequencer.step(3); + m_runnable.run(); // NullObject + } + + public void step(int step) { // called by ServiceProvider.start() method + m_sequencer.step(step); + } + + @Stop + void stop() { + m_sequencer.step(11); + } + + @Composition + Object[] getComposition() { + return new Object[]{this, m_composite}; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/MultipleReconfigurableComponentPropertyType.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/MultipleReconfigurableComponentPropertyType.java new file mode 100644 index 00000000000..713aa6c670f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/MultipleReconfigurableComponentPropertyType.java @@ -0,0 +1,107 @@ +/* + * 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.dm.runtime.itest.components; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.ConfigurationDependency; +import org.apache.felix.dm.annotation.api.PropertyType; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.itest.util.Ensure; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * @author Felix Project Team + */ +public class MultipleReconfigurableComponentPropertyType { + public final static String ENSURE = "MultipleReconfigurableComponentPropertyType"; + + @PropertyType + @Retention(RetentionPolicy.CLASS) + @interface MyProperties { + String string_value() default "defstring"; + double double_value() default 123; + } + + @PropertyType + @Retention(RetentionPolicy.CLASS) + @interface MyProperties2 { + String string_value2() default "defstring2"; + double double_value2() default 456; + } + + @Component(provides=MyComponent.class) + @MyProperties(string_value="string") + @MyProperties2(string_value2="string2") + public static class MyComponent { + @ConfigurationDependency(propagate=true, pidClass=MyComponent.class) + void updated(MyProperties props, MyProperties2 props2) { + if (props != null && props2 != null) { + System.out.println("MyComponent.updated: string_value=" + props.string_value() + ", double_value=" + props.double_value() + + "string_value2=" + props2.string_value2() + ", double_value2=" + props2.double_value2()); + } + } + } + + @Component + public static class MyConsumer { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_ensure; + + Configuration m_conf; + + @ServiceDependency + void bind(ConfigurationAdmin cm) { + try { + m_conf = cm.getConfiguration(MyComponent.class.getName()); + Hashtable newprops = new Hashtable<>(); + newprops.put("string.value", "string from CM"); + m_conf.update(newprops); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @ServiceDependency(required=false, removed="unbind") + void bind(MyComponent comp, Map props) { + try { + // first, we expect to be injected with MyComponent with the following properties: + // string.value=another_string and double_value=123 + if ("string from CM".equals(props.get("string.value")) && new Double(123).equals(props.get("double.value")) && + "string2".equals(props.get("string.value2")) && new Double(456).equals(props.get("double.value2"))) { + m_ensure.step(1); + m_conf.delete(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + void unbind(MyComponent comp) { + m_ensure.step(2); + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/OptionalConfiguration.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/OptionalConfiguration.java new file mode 100644 index 00000000000..a1b4282d79e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/OptionalConfiguration.java @@ -0,0 +1,117 @@ +/* +* 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.dm.runtime.itest.components; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.ConfigurationDependency; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.itest.util.Ensure; +import org.junit.Assert; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +public class OptionalConfiguration { + public final static String ENSURE_CONF_CREATOR = "OptionalConfiguration.Conf.Creator"; + public final static String ENSURE_CONF_CONSUMER = "OptionalConfiguration.Conf.Consumer"; + final static String PID = "OptionalConfiguration.pid"; + + public static interface MyConfig { + public default String getTestkey() { return "default"; } + } + + @Component + public static class OptionalConfigurationConsumer { + @ServiceDependency(required = true, filter = "(name=" + ENSURE_CONF_CONSUMER + ")") + public volatile Ensure m_ensure; + + protected volatile int m_updateCount; + + @ConfigurationDependency(pid=PID, required=false) + public void updated(MyConfig cnf) { // optional configuration, called after start(), like any other optional dependency callbacks. + if (cnf != null) { + m_updateCount ++; + if (m_updateCount == 1) { + if (!"default".equals(cnf.getTestkey())) { + Assert.fail("Could not find the configured property."); + } + } else if (m_updateCount == 2) { + if (!"testvalue".equals(cnf.getTestkey())) { + Assert.fail("Could not find the configured property."); + } + m_ensure.step(2); + } else if (m_updateCount == 3) { + if (!"default".equals(cnf.getTestkey())) { + Assert.fail("Could not find the configured property."); + } + m_ensure.step(3); + } + } else { + // configuration destroyed: should never happen + m_ensure.throwable(new Exception("lost configuration")); + } + } + + @Start + public void start() { + m_ensure.step(1); + } + + @Stop + public void stop() { + m_ensure.step(4); + } + } + + @Component + public static class ConfigurationCreator { + @ServiceDependency + private volatile ConfigurationAdmin m_ca; + + @ServiceDependency(required = true, filter = "(name=" + ENSURE_CONF_CREATOR + ")") + private Ensure m_ensure; + + Configuration m_conf; + + @Start + public void start() { + try { + Assert.assertNotNull(m_ca); + m_conf = m_ca.getConfiguration(PID, null); + Dictionary props = new Hashtable<>(); + props.put("testkey", "testvalue"); + m_conf.update(props); + } + catch (IOException e) { + Assert.fail("Could not create configuration: " + e.getMessage()); + } + } + + @Stop + public void stop() throws IOException { + m_conf.delete(); + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/PropagateAnnotation.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/PropagateAnnotation.java new file mode 100644 index 00000000000..a861f2f3cf6 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/PropagateAnnotation.java @@ -0,0 +1,73 @@ +/* + * 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.dm.runtime.itest.components; + +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.itest.util.Ensure; + +/** + * Verifies ServiceDependencyservice properties propagation. + * + * @author Felix Project Team + */ +@SuppressWarnings({"rawtypes"}) +public class PropagateAnnotation { + public final static String ENSURE = "PropagateAnnotation"; + + @Component + public static class Consumer { + private volatile Map m_producerProps; + + @ServiceDependency + void bind(Map props, Producer producer) { + m_producerProps = props; + } + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + @Start + void start() { + m_sequencer.step(1); + if ("bar".equals(m_producerProps.get("foo"))) { + m_sequencer.step(2); + } + if ("bar2".equals(m_producerProps.get("foo2"))) { + m_sequencer.step(3); + } + } + } + + @Component(provides = Producer.class) + @Property(name = "foo", value = "bar") + public static class Producer { + @ServiceDependency(propagate = true) + volatile Producer2 m_producer; + } + + @Component(provides = Producer2.class) + @Property(name = "foo2", value = "bar2") + public static class Producer2 { + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ProviderSwappedByAspect.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ProviderSwappedByAspect.java new file mode 100644 index 00000000000..367fa454b04 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ProviderSwappedByAspect.java @@ -0,0 +1,99 @@ +/* + * 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.dm.runtime.itest.components; + +import org.apache.felix.dm.annotation.api.AspectService; +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.itest.util.Ensure; + +/** + * Check if a client already bound to a service provider is called in swap method + * when an aspect service replaces the original service. + */ +public class ProviderSwappedByAspect { + + public interface ProviderService { + public void run(); + } + + /** + * Tests an aspect service, and ensure that its lifecycle methods are properly invoked (init/start/stop/destroy) + */ + @Component + public static class Consumer { + public final static String ENSURE = "ProviderSwappedByAspect.Consumer"; + final static Ensure.Steps m_steps = new Ensure.Steps( + 1, // provider bound the first time in bind method + 3, // swap called (aspect is replacing provider) + 6, // aspect removed , so swap called again with original service + 8 // provider removed + ); + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected volatile Ensure m_sequencer; + + @ServiceDependency(removed="unbind", swap="swap") + void bind(ProviderService service) { + m_sequencer.steps(m_steps); + service.run(); + } + + void swap(ProviderService old, ProviderService replace) { + System.out.println("swapped: old=" + old + ", replace=" + replace); + m_sequencer.steps(m_steps); + replace.run(); + } + + void unbind(ProviderService service) { + m_sequencer.steps(m_steps); + service.run(); + } + } + + @Component + public static class Provider implements ProviderService { + public final static String ENSURE = "ProviderSwappedByAspect.Provider"; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected volatile Ensure m_sequencer; + + public void run() { + m_sequencer.step(); + } + } + + @AspectService(ranking = 10, added="bind") + public static class ProviderAspect implements ProviderService { + public final static String ENSURE = "ProviderSwappedByAspect.ProviderAspect"; + private ProviderService m_next; + + void bind(ProviderService provider) { + m_next = provider; + } + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected volatile Ensure m_sequencer; + + public void run() { + m_sequencer.step(); + m_next.run(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ReconfigurableComponentPropertyTypeWithOptionalConfigAnnotation.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ReconfigurableComponentPropertyTypeWithOptionalConfigAnnotation.java new file mode 100644 index 00000000000..dfc844e87e8 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ReconfigurableComponentPropertyTypeWithOptionalConfigAnnotation.java @@ -0,0 +1,111 @@ +/* + * 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.dm.runtime.itest.components; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.ConfigurationDependency; +import org.apache.felix.dm.annotation.api.PropertyType; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.itest.util.Ensure; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * @author Felix Project Team + */ +public class ReconfigurableComponentPropertyTypeWithOptionalConfigAnnotation { + public final static String ENSURE = "ReconfigurableComponentPropertyTypeWithOptionalConfigAnnotation"; + + @PropertyType + @Retention(RetentionPolicy.CLASS) + @interface MyReconfigurableProperties { + String string_value() default "defstring"; + double double_value() default 123; + } + + @Component(provides=MyComponent.class) + @MyReconfigurableProperties(string_value="another_string") + public static class MyComponent { + @ConfigurationDependency(propagate=true, required=false) + void updated(MyReconfigurableProperties props) { + System.out.println("MyComponent.updated: string_value=" + props.string_value() + ", double_value=" + props.double_value()); + } + } + + @Component + public static class MyConsumer { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_ensure; + + @ServiceDependency + volatile ConfigurationAdmin m_cm; + + Configuration m_conf; + + @ServiceDependency(changed="change") + void bind(MyComponent comp, Map props) { + try { + // first, we expect to be injected with MyComponent with the following properties: + // string.value=another_string and double_value=123 + if ("another_string".equals(props.get("string.value")) && new Double(123).equals(props.get("double.value"))) { + m_ensure.step(1); + + // at this point, let's reconfigure ourself + m_conf = m_cm.getConfiguration(MyReconfigurableProperties.class.getName()); + Hashtable newprops = new Hashtable<>(); + newprops.put("string.value", "a_string_configured_from_CM"); + m_conf.update(newprops); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + void change(MyComponent comp, Map props) { + try { + System.out.println("MyConsumer.change: " + props); + if (m_conf != null) { + // CM is reconfiguring us: we expect to be injected with MyComponent with the following properties: + // string.value=a_string_configured_from_CM and double_value=123 + if ("a_string_configured_from_CM".equals(props.get("string.value")) + && new Double(123).equals(props.get("double.value"))) { + m_ensure.step(2); + Configuration conf = m_conf; + m_conf = null; + conf.delete(); + } + } else { + // configuration has been deleted, we expect to be injected with default service properties: + // string.value=another_string" and double_value=123 + if ("another_string".equals(props.get("string.value")) && new Double(123).equals(props.get("double.value"))) { + m_ensure.step(3); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ScopedServiceAnnotation.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ScopedServiceAnnotation.java new file mode 100644 index 00000000000..a040ec69ac4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ScopedServiceAnnotation.java @@ -0,0 +1,110 @@ +/* + * 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.dm.runtime.itest.components; + +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.PropertyType; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.ServiceScope; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.itest.util.Ensure; +import org.junit.Assert; +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceObjects; +import org.osgi.framework.ServiceRegistration; + +public class ScopedServiceAnnotation { + + public final static String ENSURE = "ScopedServiceAnnotation.ENSURE"; + + public interface PrototypeService { + } + + @PropertyType + public @interface MyConf { + public String foo() default "bar"; + } + + @Component(scope=ServiceScope.PROTOTYPE) + @MyConf(foo="bar2") + public static class PrototypeServiceImpl implements PrototypeService { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected volatile Ensure m_sequencer; + + Bundle m_clientBundle; + ServiceRegistration m_registration; + + public PrototypeServiceImpl(Bundle b, ServiceRegistration sr) { + m_clientBundle = b; + m_registration = sr; + } + + @Start + void start() { + Assert.assertNotNull(m_clientBundle); + Assert.assertNotNull(m_registration); + m_sequencer.step(); + } + + @Stop + void stop() { + m_sequencer.step(); + } + } + + @Component + public static class Consumer { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected volatile Ensure m_sequencer; + + private ServiceObjects m_so; + private PrototypeService m_service1; + private PrototypeService m_service2; + + @ServiceDependency + void bind(PrototypeService service, Map props) { + m_service1 = service; + Assert.assertEquals("bar2", props.get("foo")); + } + + @ServiceDependency + void bind2(ServiceObjects so) { + m_so = so; + m_service2 = m_so.getService(); + } + + @Start + void start() { + Assert.assertNotNull(m_service1); + Assert.assertNotNull(m_service2); + Assert.assertNotEquals(m_service1, m_service2); + m_sequencer.step(); + } + + @Stop + void stop() { + m_so.ungetService(m_service2); + m_sequencer.step(); + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ScopedServiceWithInitAnnotation.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ScopedServiceWithInitAnnotation.java new file mode 100644 index 00000000000..e850e9ba1b7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ScopedServiceWithInitAnnotation.java @@ -0,0 +1,154 @@ +/* + * 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.dm.runtime.itest.components; + +import java.util.Map; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Init; +import org.apache.felix.dm.annotation.api.Inject; +import org.apache.felix.dm.annotation.api.PropertyType; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.ServiceScope; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.itest.util.Ensure; +import org.junit.Assert; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceObjects; +import org.osgi.framework.ServiceRegistration; + +public class ScopedServiceWithInitAnnotation { + + public final static String ENSURE = "ScopedServiceWithInitAnnotation.ENSURE"; + + + public interface PrototypeService { + void ensure(); + } + + @PropertyType + public @interface MyConf { + public String foo() default "bar"; + } + + @Component(scope=ServiceScope.PROTOTYPE) + @MyConf(foo="bar2") + public static class PrototypeServiceImpl implements PrototypeService { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected volatile Ensure m_sequencer; + + Bundle m_clientBundle; + ServiceRegistration m_registration; + + volatile ExtraDendency m_extra; + + public PrototypeServiceImpl(Bundle b, ServiceRegistration sr) { + m_clientBundle = b; + m_registration = sr; + } + + @Init + void init(org.apache.felix.dm.Component c) { + DependencyManager dm = c.getDependencyManager(); + c.add(dm.createServiceDependency().setService(ExtraDendency.class).setRequired(true)); + } + + @Start + void start() { + Assert.assertNotNull(m_clientBundle); + Assert.assertNotNull(m_registration); + Assert.assertNotNull(m_extra); + m_sequencer.step(); + } + + @Stop + void stop() { + m_sequencer.step(); + } + + public void ensure() { + Assert.assertNotNull(m_clientBundle); + Assert.assertNotNull(m_registration); + Assert.assertNotNull(m_extra); + } + } + + @Component + public static class Consumer { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + protected volatile Ensure m_sequencer; + + private ServiceObjects m_so; + private PrototypeService m_service1; + private PrototypeService m_service2; + + @ServiceDependency + void bind(PrototypeService service, Map props) { + m_service1 = service; + Assert.assertEquals("bar2", props.get("foo")); + m_service1.ensure(); + } + + @ServiceDependency + void bind2(ServiceObjects so) { + m_so = so; + m_service2 = m_so.getService(); + } + + @Start + void start() { + Assert.assertNotNull(m_service1); + Assert.assertNotNull(m_service2); + Assert.assertNotEquals(m_service1, m_service2); + m_sequencer.step(); + } + + @Stop + void stop() { + m_so.ungetService(m_service2); + m_sequencer.step(); + } + } + + @Component + public static class ExtraDendency { + @Inject + BundleContext m_bc; + + @Start + void start() { + Thread t = new Thread(() -> { + try { + Thread.sleep(1000); + System.out.println("register ExtraDendency" ); + m_bc.registerService(ExtraDendency.class, this, null); + } catch (Exception e) { + e.printStackTrace(); + } + }); + t.start(); + + } + } + + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ServiceTestWthPublisher.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ServiceTestWthPublisher.java new file mode 100644 index 00000000000..d778ab606f7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/ServiceTestWthPublisher.java @@ -0,0 +1,97 @@ +/* + * 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.dm.runtime.itest.components; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Init; +import org.apache.felix.dm.annotation.api.LifecycleController; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.itest.util.Ensure; + +/** + * A Service that just registers/unregisters its service, using the @ServiceLifecycle annotation. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class ServiceTestWthPublisher { + public final static String ENSURE = "ServiceTestWthPublisher"; + + public interface Provider { + } + + @Component + public static class Consumer { + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + @ServiceDependency(required = false, removed = "unbind") + void bind(Map properties, Provider provider) { + m_sequencer.step(1); + if ("bar".equals(properties.get("foo"))) { + m_sequencer.step(2); + } + if ("bar2".equals(properties.get("foo2"))) { + m_sequencer.step(3); + } + } + + void unbind(Provider provider) { + m_sequencer.step(4); + } + } + + @Component + @Property(name = "foo", value = "bar") + public static class ProviderImpl implements Provider { + @LifecycleController + volatile Runnable m_publisher; // injected and used to register our service + + @LifecycleController(start = false) + volatile Runnable m_unpublisher; // injected and used to unregister our service + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + @Init + void init() { + // register service in 1 second + Utils.schedule(m_publisher, 1000); + // unregister the service in 2 seconds + Utils.schedule(m_unpublisher, 2000); + } + + @SuppressWarnings("serial") + @Start + Map start() { + // Add some extra service properties ... they will be appended to the one we have defined + // in the @Service annotation. + return new HashMap() { + { + put("foo2", "bar2"); + } + }; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/SimpleAnnotations.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/SimpleAnnotations.java new file mode 100644 index 00000000000..d00a1894639 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/SimpleAnnotations.java @@ -0,0 +1,186 @@ +/* + * 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.dm.runtime.itest.components; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Destroy; +import org.apache.felix.dm.annotation.api.Init; +import org.apache.felix.dm.annotation.api.Inject; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.Registered; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.annotation.api.Unregistered; +import org.apache.felix.dm.itest.util.Ensure; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class SimpleAnnotations { + /** + * Provides a Runnable service, which is required by the + * {@link Consumer} class. + */ + @Component + @Property(name = "foo", value = "bar") + @Property(name="type", value="SimpleAnnotations") + public static class Producer implements Runnable { + public final static String ENSURE = "SimpleAnnotations.Producer"; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure _ensure; + + @ServiceDependency + volatile LogService _logService; + + @Inject + volatile BundleContext _ctx; + + @Init + protected void init() { + _logService.log(LogService.LOG_INFO, "producer.init"); + // Our component is initializing (at this point: all required + // dependencies are injected). + _ensure.step(1); + } + + @Start + protected void start() { + // We are about to be registered in the OSGi registry. + _ensure.step(2); + } + + @Registered + protected void registered(ServiceRegistration sr) { + _logService.log(LogService.LOG_INFO, "Registered"); + if (sr == null) { + _ensure.throwable(new Exception("ServiceRegistration is null")); + } + if (!"bar".equals(sr.getReference().getProperty("foo"))) { + _ensure.throwable(new Exception("Invalid Service Properties")); + } + _ensure.step(3); + } + + public void run() { + _ensure.step(5); + } + + @Stop + protected void stop() { + // We are about to be unregistered from the OSGi registry, and we + // must stop. + _ensure.step(8); + } + + @Unregistered + protected void stopped() { + // We are unregistered from the OSGi registry. + _ensure.step(9); + } + + @Destroy + public void destroy() { + // Our component is shutting down. + _ensure.step(10); + } + } + + /** + * Consumes a service which is provided by the {@link Producer} class. + */ + @Component + public static class Consumer { + public final static String ENSURE = "SimpleAnnotations.Consumer"; + + @ServiceDependency + volatile LogService _logService; + + @ServiceDependency(filter="(type=SimpleAnnotations)") + volatile Runnable _runnable; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure _ensure; + + @Inject + volatile BundleContext _bc; + BundleContext _bcNotInjected; + + @Inject + volatile DependencyManager _dm; + DependencyManager _dmNotInjected; + + @Inject + volatile org.apache.felix.dm.Component _component; + org.apache.felix.dm.Component _componentNotInjected; + + @Start + protected void start() { + _logService.log(LogService.LOG_INFO, "Consumer.START: "); + checkInjectedFields(); + _ensure.step(4); + _runnable.run(); + } + + private void checkInjectedFields() { + if (_bc == null) { + _ensure.throwable(new Exception("Bundle Context not injected")); + return; + } + if (_bcNotInjected != null) { + _ensure.throwable(new Exception("Bundle Context must not be injected")); + return; + } + + if (_dm == null) { + _ensure.throwable(new Exception("DependencyManager not injected")); + return; + } + if (_dmNotInjected != null) { + _ensure.throwable(new Exception("DependencyManager must not be injected")); + return; + } + + if (_component == null) { + _ensure.throwable(new Exception("Component not injected")); + return; + } + if (_componentNotInjected != null) { + _ensure.throwable(new Exception("Component must not be injected")); + return; + } + } + + @Stop + protected void stop() { + _ensure.step(6); + } + + @Destroy + void destroy() { + _ensure.step(7); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/TemporalAnnotations.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/TemporalAnnotations.java new file mode 100644 index 00000000000..55cbf1c9990 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/TemporalAnnotations.java @@ -0,0 +1,69 @@ +/* + * 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.dm.runtime.itest.components; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.dm.itest.util.Ensure; + +/** + * Service using an annotated Temporal Service dependency. + * + * @author Felix Project Team + */ +@Component(provides = {}) +public class TemporalAnnotations implements Runnable { + public final static String ENSURE = "TemporalAnnotations"; + Thread m_thread; + + @ServiceDependency(filter = "(name=" + ENSURE + ")") + volatile Ensure m_sequencer; + + @ServiceDependency(timeout = 1000L, filter = "(test=temporal)") + volatile Runnable m_service; + + @Start + protected void start() { + m_thread = new Thread(this); + m_thread.start(); + } + + @Stop + protected void stop() { + m_thread.interrupt(); + try { + m_thread.join(); + } catch (InterruptedException e) { + } + } + + public void run() { + m_service.run(); + m_sequencer.waitForStep(2, 15000); + m_service.run(); // we should block here + m_sequencer.waitForStep(4, 15000); + try { + m_service.run(); // should raise IllegalStateException + } catch (IllegalStateException e) { + m_sequencer.step(5); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/Utils.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/Utils.java new file mode 100644 index 00000000000..a4feb3e3c5d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/Utils.java @@ -0,0 +1,68 @@ +/* + * 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.dm.runtime.itest.components; + +import org.apache.felix.dm.itest.util.Ensure; +import org.junit.Assert; +import org.osgi.framework.ServiceReference; + +/** + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class Utils { + public static final String DM_BSN = "org.apache.felix.dependencymanager"; + + public static void schedule(final Runnable task, final long n) { + Thread t = new Thread() { + public void run() { + try { + sleep(n); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + task.run(); + } + }; + t.start(); + } + + public static void assertEquals(Ensure e, ServiceReference ref, String property, Object expected, int step) { + Object value = ref.getProperty(property); + Assert.assertNotNull(value); + Assert.assertEquals(value.getClass(), expected.getClass()); + Assert.assertEquals(value, expected); + e.step(step); + } + + public static void assertArrayEquals(Ensure e, ServiceReference ref, String property, Object[] expected, int step) { + Object values = ref.getProperty(property); + Assert.assertNotNull(values); + Assert.assertTrue(values.getClass().isArray()); + Assert.assertEquals(values.getClass(), expected.getClass()); + Object[] array = (Object[]) values; + Assert.assertEquals(array.length, expected.length); + for (int i = 0; i < array.length; i ++) { + Assert.assertEquals(array[i].getClass(), expected[i].getClass()); + Assert.assertEquals(array[i], expected[i]); + } + e.step(step); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/packageinfo b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/packageinfo new file mode 100644 index 00000000000..a4f15462211 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/components/packageinfo @@ -0,0 +1 @@ +version 1.0 \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AdapterAnnotationParallelTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AdapterAnnotationParallelTest.java new file mode 100644 index 00000000000..8065dbee3d9 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AdapterAnnotationParallelTest.java @@ -0,0 +1,28 @@ +/* + * 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.dm.runtime.itest.tests; + +/** + * @author Felix Project Team + */ +public class AdapterAnnotationParallelTest extends AdapterAnnotationTest { + public AdapterAnnotationParallelTest() { + setParallel(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AdapterAnnotationTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AdapterAnnotationTest.java new file mode 100644 index 00000000000..3c6c541c275 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AdapterAnnotationTest.java @@ -0,0 +1,82 @@ +/* +* 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.AdapterAnnotation.S1Impl; +import org.apache.felix.dm.runtime.itest.components.AdapterAnnotation.S1ToS3AdapterAutoConfig; +import org.apache.felix.dm.runtime.itest.components.AdapterAnnotation.S1ToS3AdapterAutoConfigField; +import org.apache.felix.dm.runtime.itest.components.AdapterAnnotation.S1ToS3AdapterCallback; +import org.apache.felix.dm.runtime.itest.components.AdapterAnnotation.S2Impl; +import org.osgi.framework.ServiceRegistration; + +/** + * Use case: Verify Aspect Annotations usage. + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class AdapterAnnotationTest extends TestBase { + /** + * Check if an adapter gets injected with its adaptee using default auto config mode. + * @throws Throwable + */ + public void testAnnotatedAdapterAutoConfig() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration sr1 = register(e, S1ToS3AdapterAutoConfig.ENSURE); + ServiceRegistration sr2 = register(e, S1Impl.ENSURE); + ServiceRegistration sr3 = register(e, S2Impl.ENSURE); + e.waitForStep(3, 10000); + e.ensure(); + sr1.unregister(); + sr2.unregister(); + sr3.unregister(); + } + + /** + * Check if an adapter gets injected with its adaptee in a named class field. + */ + public void testAnnotatedAdapterAutoConfigField() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration sr1 = register(e, S1ToS3AdapterAutoConfigField.ENSURE); + ServiceRegistration sr2 = register(e, S1Impl.ENSURE); + ServiceRegistration sr3 = register(e, S2Impl.ENSURE); + e.waitForStep(3, 10000); + e.ensure(); + sr1.unregister(); + sr2.unregister(); + sr3.unregister(); + } + + /** + * Check if an adapter gets injected with its adaptee in a callback method. + */ + public void testAnnotatedAdapterCallback() { + Ensure e = new Ensure(); + ServiceRegistration sr1 = register(e, S1ToS3AdapterCallback.ENSURE); + ServiceRegistration sr2 = register(e, S1Impl.ENSURE); + ServiceRegistration sr3 = register(e, S2Impl.ENSURE); + e.waitForStep(2, 10000); + sr1.unregister(); + e.waitForStep(4, 10000); + sr2.unregister(); + sr3.unregister(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AspectAnnotationTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AspectAnnotationTest.java new file mode 100644 index 00000000000..41d869dff95 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AspectAnnotationTest.java @@ -0,0 +1,65 @@ +/* +* 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.AspectAnnotation.ServiceAspect1; +import org.apache.felix.dm.runtime.itest.components.AspectAnnotation.ServiceAspect2; +import org.apache.felix.dm.runtime.itest.components.AspectAnnotation.ServiceAspect3; +import org.apache.felix.dm.runtime.itest.components.AspectAnnotation.ServiceConsumer; +import org.apache.felix.dm.runtime.itest.components.AspectAnnotation.ServiceProvider; +import org.osgi.framework.ServiceRegistration; + +/** + * Use case: Verify Aspect Annotations usage. + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class AspectAnnotationTest extends TestBase { + + public void testAspectChain() throws Throwable { + Ensure e = new Ensure(); + // Activate service consumer + ServiceRegistration scSequencer = register(e, ServiceConsumer.ENSURE); + // Activate service provider + ServiceRegistration spSequencer = register(e, ServiceProvider.ENSURE); + // Activate service aspect 2 + ServiceRegistration sa2Sequencer = register(e, ServiceAspect2.ENSURE); + // Activate service aspect 3 + ServiceRegistration sa3Sequencer = register(e, ServiceAspect3.ENSURE); + // Activate service aspect 1 + ServiceRegistration sa1Sequencer = register(e, ServiceAspect1.ENSURE); + + e.step(); + e.waitForStep(6, 10000); + + // Deactivate service provider + spSequencer.unregister(); + // Make sure that service aspect 1 has been called in ts removed and stop callbacks + e.waitForStep(8, 10000); + e.ensure(); + + scSequencer.unregister(); + sa1Sequencer.unregister(); + sa2Sequencer.unregister(); + sa3Sequencer.unregister(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AspectLifecycleAnnotationParallelTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AspectLifecycleAnnotationParallelTest.java new file mode 100644 index 00000000000..1d14852024f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AspectLifecycleAnnotationParallelTest.java @@ -0,0 +1,28 @@ +/* + * 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.dm.runtime.itest.tests; + +/** + * @author Felix Project Team + */ +public class AspectLifecycleAnnotationParallelTest extends AspectLifecycleAnnotationTest { + public AspectLifecycleAnnotationParallelTest() { + setParallel(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AspectLifecycleAnnotationTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AspectLifecycleAnnotationTest.java new file mode 100644 index 00000000000..859205bde6b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AspectLifecycleAnnotationTest.java @@ -0,0 +1,52 @@ +/* +* 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.AspectLifecycleAnnotation.ServiceProvider; +import org.apache.felix.dm.runtime.itest.components.AspectLifecycleAnnotation.ServiceProviderAspect; +import org.osgi.framework.ServiceRegistration; + +/** + * Use case: Tests an aspect service, and ensure that its lifecycle methods are properly invoked + * (init/start/stop/destroy methods). + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class AspectLifecycleAnnotationTest extends TestBase { + + public void testAnnotatedAspect() { + Ensure e = new Ensure(); + // Provide the Sequencer server to the ServiceProvider service + ServiceRegistration sr1 = register(e, ServiceProvider.ENSURE); + // Check if the ServiceProvider has been injected in the AspectTest service. + e.waitForStep(1, 10000); + // Provide the Sequencer server to the ServiceProviderAspect service + ServiceRegistration sr2 = register(e, ServiceProviderAspect.ENSURE); + // Check if the AspectTest has been injected with the aspect + e.waitForStep(3, 10000); + // Stop the ServiceProviderAspect service. + sr2.unregister(); + // And check if the aspect has been called in its stop/destroy methods. + e.waitForStep(7, 10000); + sr1.unregister(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AspectLifecycleWithDynamicProxyAnnotationParalleTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AspectLifecycleWithDynamicProxyAnnotationParalleTest.java new file mode 100644 index 00000000000..df553dbf460 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AspectLifecycleWithDynamicProxyAnnotationParalleTest.java @@ -0,0 +1,28 @@ +/* + * 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.dm.runtime.itest.tests; + +/** + * @author Felix Project Team + */ +public class AspectLifecycleWithDynamicProxyAnnotationParalleTest extends AspectLifecycleWithDynamicProxyAnnotationTest{ + public AspectLifecycleWithDynamicProxyAnnotationParalleTest() { + setParallel(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AspectLifecycleWithDynamicProxyAnnotationParallelTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AspectLifecycleWithDynamicProxyAnnotationParallelTest.java new file mode 100644 index 00000000000..7e4e189924e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AspectLifecycleWithDynamicProxyAnnotationParallelTest.java @@ -0,0 +1,28 @@ +/* + * 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.dm.runtime.itest.tests; + +/** + * @author Felix Project Team + */ +public class AspectLifecycleWithDynamicProxyAnnotationParallelTest extends AspectLifecycleWithDynamicProxyAnnotationTest { + public AspectLifecycleWithDynamicProxyAnnotationParallelTest() { + setParallel(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AspectLifecycleWithDynamicProxyAnnotationTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AspectLifecycleWithDynamicProxyAnnotationTest.java new file mode 100644 index 00000000000..d6ddde2105b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/AspectLifecycleWithDynamicProxyAnnotationTest.java @@ -0,0 +1,51 @@ +/* +* 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.AspectLifecycleWithDynamicProxyAnnotation.ServiceProvider; +import org.apache.felix.dm.runtime.itest.components.AspectLifecycleWithDynamicProxyAnnotation.ServiceProviderAspect; +import org.osgi.framework.ServiceRegistration; + +/** + * Use case: Tests an aspect service implemented as a dynamic proxy, and ensure that its lifecycle methods are properly invoked + * (init/start/stop/destroy methods). + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class AspectLifecycleWithDynamicProxyAnnotationTest extends TestBase { + public void testAnnotatedAspect() { + Ensure e = new Ensure(); + // Provide the Sequencer server to the ServiceProvider service + ServiceRegistration sr1 = register(e, ServiceProvider.ENSURE); + // Check if the ServiceProvider has been injected in the AspectTest service. + e.waitForStep(1, 10000); + // Provide the Sequencer server to the ServiceProviderAspect service + ServiceRegistration sr2 = register(e, ServiceProviderAspect.ENSURE); + // Check if the AspectTest has been injected with the aspect + e.waitForStep(3, 10000); + // Remove the ServiceProviderAspect service + sr2.unregister(); + // And check if the aspect has been called in its stop/destroy methods. + e.waitForStep(7, 10000); + sr1.unregister(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/BundleDependencyAnnotationParallelTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/BundleDependencyAnnotationParallelTest.java new file mode 100644 index 00000000000..80087840253 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/BundleDependencyAnnotationParallelTest.java @@ -0,0 +1,28 @@ +/* + * 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.dm.runtime.itest.tests; + +/** + * @author Felix Project Team + */ +public class BundleDependencyAnnotationParallelTest extends BundleDependencyAnnotationTest { + public BundleDependencyAnnotationParallelTest() { + setParallel(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/BundleDependencyAnnotationTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/BundleDependencyAnnotationTest.java new file mode 100644 index 00000000000..f9ecf8ed390 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/BundleDependencyAnnotationTest.java @@ -0,0 +1,60 @@ +/* +* 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.BundleDependencyAnnotation; +import org.osgi.framework.ServiceRegistration; + +/** + * Use case: Verify Bundle Dependency annotations usage. + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class BundleDependencyAnnotationTest extends TestBase { + + /** + * Tests a simple Consumer, which has a BundleDependency over the dependency manager bundle. + * TODO: this test is not currently working. + */ + public void testBundleDependencyAnnotation() { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, BundleDependencyAnnotation.ENSURE_CONSUMER); + e.waitForStep(2, 10000); + stopBundle(BundleDependencyAnnotation.METATYPE_BSN); + e.waitForStep(4, 10000); + sr.unregister(); + startBundle(BundleDependencyAnnotation.METATYPE_BSN); + } + + /** + * Tests a Bundle Adapter, which adapts the dependency manager bundle to a "ServiceInterface" service. + * @throws Throwable + */ + public void testBundleAdapterServiceAnnotation() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, BundleDependencyAnnotation.ENSURE_ADAPTER); + e.waitForStep(3, 10000); + e.ensure(); + sr.unregister(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/CollectionFieldDependencyAnnotationTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/CollectionFieldDependencyAnnotationTest.java new file mode 100644 index 00000000000..6db1470b226 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/CollectionFieldDependencyAnnotationTest.java @@ -0,0 +1,56 @@ +/* +* 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.CollectionFieldDependencyAnnotation; +import org.osgi.framework.ServiceRegistration; + +/** + * Check if a client already bound to a service provider is called in swap method + * when an aspect service replaces the original service. + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class CollectionFieldDependencyAnnotationTest extends TestBase { + + public void testAnnotatedAspect() { + Ensure e = new Ensure(); + // register the two providers + ServiceRegistration provider1 = register(e, CollectionFieldDependencyAnnotation.ProviderImpl1.ENSURE); + ServiceRegistration provider2 = register(e, CollectionFieldDependencyAnnotation.ProviderImpl2.ENSURE); + e.waitForStep(2, 5000); + + // register the consumer + ServiceRegistration consumer = register(e, CollectionFieldDependencyAnnotation.Consumer.ENSURE); + e.waitForStep(3, 5000); + + // check if the first collection (m_list1) was properly injected in consumer + e.waitForStep(4, 5000); + + // check if the second collection (m_list2) was properly injected in consumer + e.waitForStep(6, 5000); + + provider1.unregister(); + provider2.unregister(); + consumer.unregister(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ComponentPropertyTypeAnnotationTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ComponentPropertyTypeAnnotationTest.java new file mode 100644 index 00000000000..41e7ea1fad9 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ComponentPropertyTypeAnnotationTest.java @@ -0,0 +1,114 @@ +/* + * 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.ComponentDMPropertyTypeAnnotation; +import org.apache.felix.dm.runtime.itest.components.ComponentDMPropertyTypeArrayAnnotation; +import org.apache.felix.dm.runtime.itest.components.ComponentDMSingleValuedPropertyTypeAnnotation; +import org.apache.felix.dm.runtime.itest.components.ComponentJaxrsResourceAnnotation; +import org.apache.felix.dm.runtime.itest.components.ComponentPropertyTypeWithDictionaryPassedInUpdateCallback; +import org.apache.felix.dm.runtime.itest.components.FactoryPidWithPropertyTypeAnnotation; +import org.apache.felix.dm.runtime.itest.components.JaxrsComponentPropertyTypeAnnotation; +import org.apache.felix.dm.runtime.itest.components.MultipleReconfigurableComponentPropertyType; +import org.apache.felix.dm.runtime.itest.components.ReconfigurableComponentPropertyTypeWithOptionalConfigAnnotation; +import org.osgi.framework.ServiceRegistration; + +/** + * Use case: Ensure that a Provider can be injected into a Consumer, using simple DM annotations. + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class ComponentPropertyTypeAnnotationTest extends TestBase { + + public void testJaxRsPropertyTypes() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration sr1 = register(e, JaxrsComponentPropertyTypeAnnotation.JaxRsConsumer.ENSURE); + e.waitForStep(2, 5000); + sr1.unregister(); + e.ensure(); + } + + public void testCustomPropertyTypes() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration sr1 = register(e, ComponentDMPropertyTypeAnnotation.ENSURE); + e.waitForStep(3, 5000); + sr1.unregister(); + e.ensure(); + } + + public void testCustomPropertyTypesArray() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration sr1 = register(e, ComponentDMPropertyTypeArrayAnnotation.ENSURE); + e.waitForStep(4, 5000); + sr1.unregister(); + e.ensure(); + } + + public void testSingleValuedPropertyTypes() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration sr1 = register(e, ComponentDMSingleValuedPropertyTypeAnnotation.ENSURE); + e.waitForStep(3, 5000); + sr1.unregister(); + e.ensure(); + } + + public void testJaxrsPropertyTypes() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration sr1 = register(e, ComponentJaxrsResourceAnnotation.ENSURE); + e.waitForStep(2, 5000); + sr1.unregister(); + e.ensure(); + } + + public void testCustomReconfigurablePropertyTypes() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration sr1 = register(e, ReconfigurableComponentPropertyTypeWithOptionalConfigAnnotation.ENSURE); + e.waitForStep(3, 5000); + sr1.unregister(); + e.ensure(); + } + + public void testDictionaryPassedInUpdatedCallback() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration sr1 = register(e, ComponentPropertyTypeWithDictionaryPassedInUpdateCallback.ENSURE); + e.waitForStep(3, 5000); + sr1.unregister(); + e.ensure(); + } + + public void testMultipleReconfigurablePropertyTypes() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration sr1 = register(e, MultipleReconfigurableComponentPropertyType.ENSURE); + e.waitForStep(2, 5000); + sr1.unregister(); + e.ensure(); + } + + public void testFactoryPidWithPropertyTypes() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration sr1 = register(e, FactoryPidWithPropertyTypeAnnotation.ENSURE); + e.waitForStep(3, 5000); + sr1.unregister(); + e.ensure(); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/CompositeAnnotationsParallelTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/CompositeAnnotationsParallelTest.java new file mode 100644 index 00000000000..8e5167f61fe --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/CompositeAnnotationsParallelTest.java @@ -0,0 +1,28 @@ +/* + * 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.dm.runtime.itest.tests; + +/** + * @author Felix Project Team + */ +public class CompositeAnnotationsParallelTest extends CompositeAnnotationsTest { + public CompositeAnnotationsParallelTest() { + setParallel(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/CompositeAnnotationsTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/CompositeAnnotationsTest.java new file mode 100644 index 00000000000..cff17717a78 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/CompositeAnnotationsTest.java @@ -0,0 +1,47 @@ +/* +* 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.CompositeAnnotations.C1; +import org.apache.felix.dm.runtime.itest.components.CompositeAnnotations.Dependency1; +import org.apache.felix.dm.runtime.itest.components.CompositeAnnotations.Dependency2; +import org.osgi.framework.ServiceRegistration; + +/** + * Use case: Verify Composite annotated services. + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class CompositeAnnotationsTest extends TestBase { + + public void testComposite() { + Ensure e = new Ensure(); + ServiceRegistration sr1 = register(e, C1.ENSURE); + ServiceRegistration sr2 = register(e, Dependency1.ENSURE); + ServiceRegistration sr3 = register(e, Dependency2.ENSURE); + e.waitForStep(4, 10000); + sr3.unregister(); + sr2.unregister(); + sr1.unregister(); + e.waitForStep(12, 10000); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ConfigurationDependencyAnnotationTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ConfigurationDependencyAnnotationTest.java new file mode 100644 index 00000000000..0330654ac19 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ConfigurationDependencyAnnotationTest.java @@ -0,0 +1,102 @@ +/* + * 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.dm.runtime.itest.tests; + +import java.io.IOException; +import java.util.Hashtable; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.ConfigurationDependencyAnnotation.ConfigurableComponent; +import org.apache.felix.dm.runtime.itest.components.ConfigurationDependencyAnnotation.ConfigurableComponentWithDynamicExtraConfiguration; +import org.junit.Assert; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * @author Felix Project Team + */ +public class ConfigurationDependencyAnnotationTest extends TestBase { + + private final static int MAXWAIT = 5000; + + + /** + * Tests the ConfigurationDependency annotation. + */ + @SuppressWarnings({ "serial", "rawtypes", "unchecked" }) + public void testConfigurationDependencyAnnotation() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, ConfigurableComponent.ENSURE); + ConfigurationAdmin cm = (ConfigurationAdmin) context.getService(context.getServiceReference(ConfigurationAdmin.class.getName())); + try { + org.osgi.service.cm.Configuration cf = cm.getConfiguration(ConfigurableComponent.class.getName(), null); + cf.update(new Hashtable() { + { + put("foo", "bar"); + } + }); + e.waitForStep(1, MAXWAIT); + cf.delete(); + e.waitForStep(3, MAXWAIT); + e.ensure(); + sr.unregister(); + } catch (IOException err) { + err.printStackTrace(); + Assert.fail("can't create factory configuration"); + } + } + + /** + * Tests a Component two ConfigurationDependency (the second one is "instance bound" + * and its pid is declared from the init method). + */ + @SuppressWarnings({ "serial", "rawtypes", "unchecked" }) + public void testConfigurationDependencyWithAnotherExtraDynamicConfigurationAnnotation() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, ConfigurableComponentWithDynamicExtraConfiguration.ENSURE); + ConfigurationAdmin cm = (ConfigurationAdmin) context.getService(context.getServiceReference(ConfigurationAdmin.class.getName())); + try { + org.osgi.service.cm.Configuration cf = cm.getConfiguration(ConfigurableComponentWithDynamicExtraConfiguration.class.getName(), null); + cf.update(new Hashtable() { + { + put("foo", "bar"); + put("dynamicPid", "dynamicPid"); // Pid of the second Configuration Dependency. + } + }); + org.osgi.service.cm.Configuration extraCf = cm.getConfiguration("dynamicPid", null); + extraCf.update(new Hashtable() { + { + put("foo2", "bar2"); + } + }); + + e.waitForStep(4, MAXWAIT); + cf.delete(); + extraCf.delete(); + e.waitForStep(5, MAXWAIT); + e.ensure(); + sr.unregister(); + } catch (IOException err) { + err.printStackTrace(); + Assert.fail("can't create factory configuration"); + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ConfigurationProxyTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ConfigurationProxyTest.java new file mode 100644 index 00000000000..18ecd83e95b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ConfigurationProxyTest.java @@ -0,0 +1,43 @@ +/* +* 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.ConfigurationProxy; +import org.osgi.framework.ServiceRegistration; + +/** + * Tests for new Configuration Proxy Types (FELIX-5177) + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class ConfigurationProxyTest extends TestBase { + /** + * Validates ServiceDependency method signatures. + */ + public void testConfigurationProxy() { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, ConfigurationProxy.ENSURE_CONFIG_DEPENDENCY); + e.waitForStep(1, 5000); + sr.unregister(); + e.waitForStep(3, 5000); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ExtraServicePropertiesParallelTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ExtraServicePropertiesParallelTest.java new file mode 100644 index 00000000000..7e523e896ec --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ExtraServicePropertiesParallelTest.java @@ -0,0 +1,28 @@ +/* + * 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.dm.runtime.itest.tests; + +/** + * @author Felix Project Team + */ +public class ExtraServicePropertiesParallelTest extends ExtraServicePropertiesTest { + public ExtraServicePropertiesParallelTest() { + setParallel(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ExtraServicePropertiesTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ExtraServicePropertiesTest.java new file mode 100644 index 00000000000..0b6669b0ed8 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ExtraServicePropertiesTest.java @@ -0,0 +1,65 @@ +/* + * 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.ExtraAdapterServiceProperties; +import org.apache.felix.dm.runtime.itest.components.ExtraServiceProperties; +import org.osgi.framework.ServiceRegistration; + +/** + * Use case: Verify the a Service may provide its service properties dynamically from its start method. + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class ExtraServicePropertiesTest extends TestBase { + + /** + * Tests if a Service can provide its service properties from its start method. + */ + public void testExtraServiceProperties() { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, ExtraServiceProperties.ENSURE); + e.waitForStep(2, 10000); + sr.unregister(); + } + + /** + * Tests if an AdapterService can provide its service properties from its start method. + */ + public void testExtraAdapterServiceProperties() { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, ExtraAdapterServiceProperties.ENSURE); + e.waitForStep(3, 10000); + sr.unregister(); + } + + /** + * Tests if an AspectService can provide its service properties from its start method. + */ + // TODO + public void testExtraAspectServiceProperties() { +// Ensure e = new Ensure(); +// ServiceRegistration sr = register(e, ExtraAspectServiceProperties.ENSURE); +// e.waitForStep(3, 10000); +// sr.unregister(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/FELIX5337Test.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/FELIX5337Test.java new file mode 100644 index 00000000000..284cd5559a0 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/FELIX5337Test.java @@ -0,0 +1,50 @@ +/* + * 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.FELIX5337; +import org.apache.felix.dm.runtime.itest.components.FELIX5337_MatchAllServicesWithFilter; +import org.osgi.framework.ServiceRegistration; + +/** + * Test test validates that we can lookup ALL existing services using annotation, and "(objectClass=*)" filter. + */ +@SuppressWarnings("rawtypes") +public class FELIX5337Test extends TestBase { + public void testCatchAllServicesUsingAnnotation() { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, FELIX5337.ENSURE); + // wait for S to be started + e.waitForStep(2, 5000); + // remove our sequencer: this will stop S + sr.unregister(); + // ensure that S is stopped and destroyed + e.waitForStep(3, 5000); + } + + public void testCatchAllServicesWithFiltersUsingAnnotation() { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, FELIX5337_MatchAllServicesWithFilter.ENSURE); + // wait for S to be started + e.waitForStep(1, 5000); + sr.unregister(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/FELIX5956Test.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/FELIX5956Test.java new file mode 100644 index 00000000000..f04624f5112 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/FELIX5956Test.java @@ -0,0 +1,38 @@ +/* + * 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.FELIX5956; +import org.osgi.framework.ServiceRegistration; + +/** + * Test test validates that we can lookup ALL existing services using annotation, and "(objectClass=*)" filter. + */ +@SuppressWarnings("rawtypes") +public class FELIX5956Test extends TestBase { + + public void testLifecycleRunnableCalledFromInit() { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, FELIX5956.ENSURE); + e.waitForStep(5, 5000); + sr.unregister(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/FactoryConfigurationAdapterAnnotationParallelTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/FactoryConfigurationAdapterAnnotationParallelTest.java new file mode 100644 index 00000000000..16752605345 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/FactoryConfigurationAdapterAnnotationParallelTest.java @@ -0,0 +1,28 @@ +/* + * 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.dm.runtime.itest.tests; + +/** + * @author Felix Project Team + */ +public class FactoryConfigurationAdapterAnnotationParallelTest extends FactoryConfigurationAdapterAnnotationTest { + public FactoryConfigurationAdapterAnnotationParallelTest() { + setParallel(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/FactoryConfigurationAdapterAnnotationTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/FactoryConfigurationAdapterAnnotationTest.java new file mode 100644 index 00000000000..8e181f526d9 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/FactoryConfigurationAdapterAnnotationTest.java @@ -0,0 +1,78 @@ +/* +* 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.dm.runtime.itest.tests; + +import java.io.IOException; +import java.util.Hashtable; + +import org.junit.Assert; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.FactoryConfigurationAdapterAnnotation.ServiceProvider; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.ConfigurationAdmin; + +/** + * Use case: Verify that an annotated Configuration Factory Adapter Service is properly created when a factory configuration + * is created from Config Admin. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class FactoryConfigurationAdapterAnnotationTest extends TestBase { + + private final static int MAXWAIT = 5000; + + @SuppressWarnings("serial") + public void testFactoryConfigurationAdapterAnnotation() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, ServiceProvider.ENSURE); + ConfigurationAdmin cm = (ConfigurationAdmin) context.getService(context.getServiceReference(ConfigurationAdmin.class.getName())); + try { + // Create a factory configuration in order to instantiate the ServiceProvider + org.osgi.service.cm.Configuration cf = cm.createFactoryConfiguration("FactoryPidTest", null); + cf.update(new Hashtable() { + { + put("foo2", "bar2"); + } + }); + // Wait for the ServiceProvider activation. + e.waitForStep(2, MAXWAIT); + // Update conf + cf.update(new Hashtable() { + { + put("foo2", "bar2_modified"); + } + }); + // Wait for effective update + e.waitForStep(4, MAXWAIT); + // Remove configuration. + cf.delete(); + // Check if ServiceProvider has been stopped. + e.waitForStep(6, MAXWAIT); + e.ensure(); + sr.unregister(); + } + catch (IOException err) { + err.printStackTrace(); + Assert.fail("can't create factory configuration"); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/Felix4050Test.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/Felix4050Test.java new file mode 100644 index 00000000000..3fb9eaedeb8 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/Felix4050Test.java @@ -0,0 +1,45 @@ +/* + * 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.Felix4050; +import org.osgi.framework.ServiceRegistration; + +/** + * Test for FELIX-4050 issue: It validates that component state calculation does not mess up + * when an @Init method adds an available dependency using the API, and also returns a Map for + * configuring a named dependency. + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class Felix4050Test extends TestBase { + public void testFelix4050() { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, Felix4050.ENSURE); + // wait for S to be started + e.waitForStep(3, 10000); + // remove our sequencer: this will stop S + sr.unregister(); + // ensure that S is stopped and destroyed + e.waitForStep(5, 10000); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/Felix4357ParallelTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/Felix4357ParallelTest.java new file mode 100644 index 00000000000..9ddbdb9c316 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/Felix4357ParallelTest.java @@ -0,0 +1,28 @@ +/* + * 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.dm.runtime.itest.tests; + +/** + * @author Felix Project Team + */ +public class Felix4357ParallelTest extends Felix4357Test { + public Felix4357ParallelTest() { + setParallel(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/Felix4357Test.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/Felix4357Test.java new file mode 100644 index 00000000000..3919bd1bd06 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/Felix4357Test.java @@ -0,0 +1,43 @@ +/* + * 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.Felix4357; +import org.osgi.framework.ServiceRegistration; + +/** + * Test for FELIX-4357 issue: It validates the types of some service component properties + * defined with @Property annotation. + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class Felix4357Test extends TestBase { + + public void testSingleProperty() { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, Felix4357.ENSURE); + // wait for S to be started + e.waitForStep(30, 10000); + // remove our sequencer: this will stop S + sr.unregister(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/Felix5236Test.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/Felix5236Test.java new file mode 100644 index 00000000000..b22fe6dc7a5 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/Felix5236Test.java @@ -0,0 +1,41 @@ +/* + * 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.Felix5236; +import org.osgi.framework.ServiceRegistration; + +/** + * Test for FELIX-4357 issue: It validates the types of some service component properties + * defined with @Property annotation. + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class Felix5236Test extends TestBase { + + public void testPropertiesWithTypes() { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, Felix5236.ENSURE); + e.waitForStep(1, 10000); + sr.unregister(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/InheritedAnnotationsTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/InheritedAnnotationsTest.java new file mode 100644 index 00000000000..c9278fe5507 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/InheritedAnnotationsTest.java @@ -0,0 +1,42 @@ +/* +* 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.InheritedAnnotations; +import org.osgi.framework.ServiceRegistration; + +/** + * Use case: Verify if a component may inherit from annotations defined in inherited classes. + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class InheritedAnnotationsTest extends TestBase { + + public void testInheritedAnnotation() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, InheritedAnnotations.ENSURE); + e.ensure(); + e.waitForStep(2, 5000); + sr.unregister(); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/MethodSignaturesTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/MethodSignaturesTest.java new file mode 100644 index 00000000000..5730f8ef61d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/MethodSignaturesTest.java @@ -0,0 +1,56 @@ +/* +* 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.MethodSignatures; +import org.osgi.framework.ServiceRegistration; + +/** + * Use case: Validates proper injection on various bind method signatures. + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class MethodSignaturesTest extends TestBase { + /** + * Validates ServiceDependency method signatures. + */ + public void testServiceDependencyBindSignatures() { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, MethodSignatures.ENSURE_SERVICE_DEPENDENCY); + e.waitForStep(10, 5000); + sr.unregister(); + // make sure configurations are deleted + e.waitForStep(14, 5000); + } + + /** + * Validates FactoryConfigurationAdapter updated callback signatures. + */ + public void testFactoryPidUpdatedSignature1() { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, MethodSignatures.ENSURE_FACTORYPID); + e.waitForStep(3, 5000); + sr.unregister(); + // make sure configurations are deleted + e.waitForStep(6, 5000); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/MultipleAnnotationsParallelTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/MultipleAnnotationsParallelTest.java new file mode 100644 index 00000000000..ffb4fcecb16 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/MultipleAnnotationsParallelTest.java @@ -0,0 +1,28 @@ +/* + * 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.dm.runtime.itest.tests; + +/** + * @author Felix Project Team + */ +public class MultipleAnnotationsParallelTest extends MultipleAnnotationsTest { + public MultipleAnnotationsParallelTest() { + setParallel(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/MultipleAnnotationsTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/MultipleAnnotationsTest.java new file mode 100644 index 00000000000..310a9b4895b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/MultipleAnnotationsTest.java @@ -0,0 +1,41 @@ +/* +* 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.MultipleAnnotations; +import org.osgi.framework.ServiceRegistration; + +/** + * Use case: Verify complex Annotation usage. + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class MultipleAnnotationsTest extends TestBase { + + public void testMultipleAnnotations() { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, MultipleAnnotations.ENSURE); + e.waitForStep(7, 10000); + sr.unregister(); + e.waitForStep(11, 10000); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/OptionalConfigurationTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/OptionalConfigurationTest.java new file mode 100644 index 00000000000..6640b6e765e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/OptionalConfigurationTest.java @@ -0,0 +1,46 @@ +/* +* 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.OptionalConfiguration; +import org.osgi.framework.ServiceRegistration; + +@SuppressWarnings("rawtypes") +public class OptionalConfigurationTest extends TestBase { + + public void testOptionalConfig() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration ensureConsumer = register(e, OptionalConfiguration.ENSURE_CONF_CONSUMER); + e.waitForStep(1, 5000); // consumer called in updated with testKey="default" config, and then called in start + + // will register a "testKey=testvalue" configuration, using config admin. + ServiceRegistration ensureConfCreator = register(e, OptionalConfiguration.ENSURE_CONF_CREATOR); + + e.waitForStep(2, 5000); // consumer called in updated with testKey="testvalue" + ensureConfCreator.unregister(); // remove configuration. + e.waitForStep(3, 5000); // consumer called in updated with default "testkey=default" property (using the default method from the config interface. + ensureConsumer.unregister(); // stop the OptionalConfigurationConsumer component + e.waitForStep(4, 5000); // consumer stopped + + e.ensure(); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/PropagateAnnotationParallelTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/PropagateAnnotationParallelTest.java new file mode 100644 index 00000000000..c250ddd99e5 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/PropagateAnnotationParallelTest.java @@ -0,0 +1,28 @@ +/* + * 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.dm.runtime.itest.tests; + +/** + * @author Felix Project Team + */ +public class PropagateAnnotationParallelTest extends PropagateAnnotationTest { + public PropagateAnnotationParallelTest() { + setParallel(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/PropagateAnnotationTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/PropagateAnnotationTest.java new file mode 100644 index 00000000000..22ae31c24ff --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/PropagateAnnotationTest.java @@ -0,0 +1,40 @@ +/* + * 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.PropagateAnnotation; +import org.osgi.framework.ServiceRegistration; + +/** + * Use case: Verify that dependency "propagate" option is properly propagating properties to provided service. + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class PropagateAnnotationTest extends TestBase { + + public void testServiceDependencyPropagate() { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, PropagateAnnotation.ENSURE); + e.waitForStep(3, 10000); + sr.unregister(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ProviderSwappedByAspectTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ProviderSwappedByAspectTest.java new file mode 100644 index 00000000000..16c81b2ab66 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ProviderSwappedByAspectTest.java @@ -0,0 +1,60 @@ +/* +* 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.ProviderSwappedByAspect; +import org.osgi.framework.ServiceRegistration; + +/** + * Check if a client already bound to a service provider is called in swap method + * when an aspect service replaces the original service. + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class ProviderSwappedByAspectTest extends TestBase { + + public void testAnnotatedAspect() { + Ensure e = new Ensure(); + ServiceRegistration consumer = register(e, ProviderSwappedByAspect.Consumer.ENSURE); + ServiceRegistration provider = register(e, ProviderSwappedByAspect.Provider.ENSURE); + + // client bound to provider (1), and client calls provider (2) + e.waitForStep(2, 5000); + + ServiceRegistration aspect = register(e, ProviderSwappedByAspect.ProviderAspect.ENSURE); + + // provider swapped with aspect (3), aspect called (4), original provider called (5) + e.waitForStep(5, 5000); + + aspect.unregister(); + + // aspect swapped by provider (6), provider called (7) + e.waitForStep(7, 5000); + + provider.unregister(); + + //provider unbound (8), provider called (9) + e.waitForStep(9, 5000); + + consumer.unregister(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/PublisherAnnotationTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/PublisherAnnotationTest.java new file mode 100644 index 00000000000..f69237394c3 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/PublisherAnnotationTest.java @@ -0,0 +1,74 @@ +/* + * 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.AdapterServiceTestWithPublisher; +import org.apache.felix.dm.runtime.itest.components.BundleAdapterServiceTestWithPublisher; +import org.apache.felix.dm.runtime.itest.components.FactoryConfigurationAdapterServiceTestWithPublisher; +import org.apache.felix.dm.runtime.itest.components.ServiceTestWthPublisher; +import org.osgi.framework.ServiceRegistration; + +/** + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class PublisherAnnotationTest extends TestBase { + + /** + * A Service that just registers/unregisters its service, using the @ServiceLifecycle annotation. + */ + public void testServiceWithPublisher() { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, ServiceTestWthPublisher.ENSURE); + e.waitForStep(4, 10000); + sr.unregister(); + } + + /** + * Test an AdapterService which provides its interface using a @ServiceLifecycle. + */ + public void testAdapterServiceWithPublisher() { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, AdapterServiceTestWithPublisher.ENSURE); + e.waitForStep(6, 10000); + sr.unregister(); + } + + /** + * Test a BundleAdapterService which provides its interface using a @ServiceLifecycle. + */ + public void testBundleAdapterServiceWithPublisher() { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, BundleAdapterServiceTestWithPublisher.ENSURE); + e.waitForStep(5, 10000); + sr.unregister(); + } + + /** + * Test a FactoryConfigurationAdapterService which provides its interface using a @ServiceLifecycle. + */ + public void testFactoryAdapterServiceWithPublisher() { + Ensure e = new Ensure(); + ServiceRegistration sr = register(e, FactoryConfigurationAdapterServiceTestWithPublisher.ENSURE); + e.waitForStep(5, 10000); + sr.unregister(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ScopedServiceAnnotationTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ScopedServiceAnnotationTest.java new file mode 100644 index 00000000000..19a5ba87f76 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/ScopedServiceAnnotationTest.java @@ -0,0 +1,69 @@ +/* +* 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.ScopedServiceAnnotation; +import org.apache.felix.dm.runtime.itest.components.ScopedServiceWithInitAnnotation; +import org.osgi.framework.ServiceRegistration; + +/** + * Use case: Verify Aspect Annotations usage. + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class ScopedServiceAnnotationTest extends TestBase { + + public void testScopedService() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration seq = register(e, ScopedServiceAnnotation.ENSURE); + // the consumer defines two dependencies, hence two prototype instances are created + e.waitForStep(2, 5000); + + // make sure Consumer has started + e.waitForStep(3, 5000); + + // Deactivate service provider + seq.unregister(); + + // make sure the two prototypes and the consumer have stopped + e.waitForStep(6, 5000); + e.ensure(); + } + + public void testScopedServiceWithInitMethod() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration seq = register(e, ScopedServiceWithInitAnnotation.ENSURE); + // the consumer defines two dependencies, hence two prototype instances are created + e.waitForStep(2, 5000); + + // make sure Consumer has started + e.waitForStep(3, 5000); + + // Deactivate service provider + seq.unregister(); + + // make sure the two prototypes and the consumer have stopped + e.waitForStep(6, 5000); + e.ensure(); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/SimpleAnnotationsParallelTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/SimpleAnnotationsParallelTest.java new file mode 100644 index 00000000000..75cfa3e0604 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/SimpleAnnotationsParallelTest.java @@ -0,0 +1,28 @@ +/* + * 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.dm.runtime.itest.tests; + +/** + * @author Felix Project Team + */ +public class SimpleAnnotationsParallelTest extends SimpleAnnotationsTest { + public SimpleAnnotationsParallelTest() { + setParallel(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/SimpleAnnotationsTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/SimpleAnnotationsTest.java new file mode 100644 index 00000000000..f4395a5e85a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/SimpleAnnotationsTest.java @@ -0,0 +1,48 @@ +/* + * 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.dm.runtime.itest.tests; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.SimpleAnnotations.Consumer; +import org.apache.felix.dm.runtime.itest.components.SimpleAnnotations.Producer; +import org.osgi.framework.ServiceRegistration; + +/** + * Use case: Ensure that a Provider can be injected into a Consumer, using simple DM annotations. + * + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class SimpleAnnotationsTest extends TestBase { + + public void testSimpleAnnotations() throws Throwable { + Ensure e = new Ensure(); + ServiceRegistration er = register(e, Producer.ENSURE); + e.waitForStep(3, 10000); // Producer registered + ServiceRegistration er2 = register(e, Consumer.ENSURE); + + er2.unregister(); // stop consumer + er.unregister(); // stop provider + + // And check if components have been deactivated orderly. + e.waitForStep(10, 10000); + e.ensure(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/TemporalAnnotationsParallelTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/TemporalAnnotationsParallelTest.java new file mode 100644 index 00000000000..e4bd4badf59 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/TemporalAnnotationsParallelTest.java @@ -0,0 +1,28 @@ +/* + * 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.dm.runtime.itest.tests; + +/** + * @author Felix Project Team + */ +public class TemporalAnnotationsParallelTest extends TemporalAnnotationsTest { + public TemporalAnnotationsParallelTest() { + setParallel(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/TemporalAnnotationsTest.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/TemporalAnnotationsTest.java new file mode 100644 index 00000000000..ac50edf5196 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/apache/felix/dm/runtime/itest/tests/TemporalAnnotationsTest.java @@ -0,0 +1,61 @@ +/* +* 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.dm.runtime.itest.tests; + +import java.util.Dictionary; +import java.util.Hashtable; + +import org.apache.felix.dm.itest.util.Ensure; +import org.apache.felix.dm.itest.util.TestBase; +import org.apache.felix.dm.runtime.itest.components.TemporalAnnotations; +import org.osgi.framework.ServiceRegistration; + +/** + * Use case: Verify Temporal Service dependency Annotations usage. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes", "serial"}) +public class TemporalAnnotationsTest extends TestBase { + + public void testTemporalServiceDependency() { + Ensure ensure = new Ensure(); + ServiceRegistration ensureReg = register(ensure, TemporalAnnotations.ENSURE); + Dictionary props = new Hashtable() { + { + put("test", "temporal"); + } + }; + Runnable r = Ensure.createRunnableStep(ensure, 1); + ServiceRegistration sr = context.registerService(Runnable.class.getName(), r, props); + ensure.waitForStep(1, 15000); + System.out.println("unregistering R"); + sr.unregister(); + ensure.step(2); + sleep(500); + r = Ensure.createRunnableStep(ensure, 3); + sr = context.registerService(Runnable.class.getName(), r, props); + ensure.waitForStep(3, 15000); + sr.unregister(); + ensure.step(4); + sleep(1500); + ensure.waitForStep(5, 15000); + ensureReg.unregister(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/osgi/service/component/annotations/ComponentPropertyType.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/osgi/service/component/annotations/ComponentPropertyType.java new file mode 100644 index 00000000000..09f4f9f3e3b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/osgi/service/component/annotations/ComponentPropertyType.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) OSGi Alliance (2016, 2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.service.component.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Identify the annotated annotation as a Component Property Type. + *

      + * Component Property Types can be applied as annotations to the implementation + * class of the Component. They can also be used as activation objects which + * means they can be used as parameter types for the component's constructor and + * life cycle methods {@link Activate}, {@link Deactivate}, and {@link Modified} + * as well as activation fields. + *

      + * Component Property Types do not have to be annotated with this annotation to + * be used as parameter types but they must be annotated with this annotation to + * be used as annotations on the implementation class of the Component. + *

      + * This annotation is not processed at runtime by Service Component Runtime. It + * must be processed by tools and used to add a Component Description to the + * bundle. + * + * @see "Component Property Types." + * @author $Id: 563e80fe5d699c0e02ac2963d08e167552d8928d $ + * @since 1.4 + */ +@Documented +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.ANNOTATION_TYPE) +public @interface ComponentPropertyType { + // meta-annotation +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/osgi/service/jaxrs/whiteboard/JaxRSWhiteboardConstants.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/osgi/service/jaxrs/whiteboard/JaxRSWhiteboardConstants.java new file mode 100644 index 00000000000..bdb04d5c861 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/osgi/service/jaxrs/whiteboard/JaxRSWhiteboardConstants.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.osgi.service.jaxrs.whiteboard; + +public class JaxRSWhiteboardConstants { + /** + * Service property specifying that a JAX-RS resource should be processed by + * the whiteboard. + *

      + * The value of this service property must be of type {@code String} or + * {@link Boolean} and set to "true" or true. + */ + public static final String JAX_RS_RESOURCE = "osgi.jaxrs.resource"; +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/osgi/service/jaxrs/whiteboard/annotations/RequireJaxRSWhiteboard.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/osgi/service/jaxrs/whiteboard/annotations/RequireJaxRSWhiteboard.java new file mode 100644 index 00000000000..c62ba609b3b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/osgi/service/jaxrs/whiteboard/annotations/RequireJaxRSWhiteboard.java @@ -0,0 +1,41 @@ +/* +* 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.osgi.service.jaxrs.whiteboard.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation can be used to require the Http Whiteboard implementation. It + * can be used directly, or as a meta-annotation. + *

      + * This annotation is applied to several of the Http Whiteboard component + * property annotations meaning that it does not normally need to be applied to + * DS components which use the Http Whiteboard. + * + * @author $Id: d8dd2f2d079255ec829406ab1e496054a4528d1b $ + * @since 1.0 + */ +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface RequireJaxRSWhiteboard { + // This is a purely informational annotation and has no elements. +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/osgi/service/jaxrs/whiteboard/propertytypes/JaxrsName.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/osgi/service/jaxrs/whiteboard/propertytypes/JaxrsName.java new file mode 100644 index 00000000000..b54dc4f5069 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/osgi/service/jaxrs/whiteboard/propertytypes/JaxrsName.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.service.jaxrs.whiteboard.propertytypes; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.osgi.service.component.annotations.ComponentPropertyType; + +/** + * Component Property Type for the {@code osgi.jaxrs.name} service property. + *

      + * This annotation can be used on a JAX-RS service to declare the value of the + * {@link org.osgi.service.jaxrs.whiteboard.JaxRSWhiteboardConstants#JAX_RS_NAME} + * service property. + * + * @see "Component Property Types" + * @author $Id: 5d90577770d30e749a85b3be11a7ced5bd91d9e7 $ + * @since 1.0 + */ +@ComponentPropertyType +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface JaxrsName { + /** + * Prefix for the property name. This value is prepended to each property + * name. + */ + String PREFIX_ = "osgi."; + + /** + * Service property identifying the name of a JAX-RS service for processing + * by the whiteboard. + * + * @return The JAX-RS service name. + * @see org.osgi.service.jaxrs.whiteboard.JaxRSWhiteboardConstants#JAX_RS_NAME + */ + String value(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/osgi/service/jaxrs/whiteboard/propertytypes/JaxrsResource.java b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/osgi/service/jaxrs/whiteboard/propertytypes/JaxrsResource.java new file mode 100644 index 00000000000..7fbc1f48862 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/src/org/osgi/service/jaxrs/whiteboard/propertytypes/JaxrsResource.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) OSGi Alliance (2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.osgi.service.jaxrs.whiteboard.propertytypes; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.osgi.service.component.annotations.ComponentPropertyType; +import org.osgi.service.jaxrs.whiteboard.annotations.RequireJaxRSWhiteboard; + +/** + * Component Property Type for the {@code osgi.jaxrs.resource} service property. + *

      + * This annotation can be used on a JAX-RS resource to declare the value of the + * {@link org.osgi.service.jaxrs.whiteboard.JaxRSWhiteboardConstants#JAX_RS_RESOURCE} + * service property. + * + * @see "Component Property Types" + * @author $Id: 6a574a8c0d6523e601a31c233d25c01e89107351 $ + * @since 1.0 + */ +@ComponentPropertyType +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +@RequireJaxRSWhiteboard +public @interface JaxrsResource { + /** + * Prefix for the property name. This value is prepended to each property + * name. + */ + String PREFIX_ = "osgi."; + + /** + * Service property identifying a JAX-RS resource for processing by the + * whiteboard. + * + * @return The JAX-RS resource value. + * @see org.osgi.service.jaxrs.whiteboard.JaxRSWhiteboardConstants#JAX_RS_RESOURCE + */ + //String value() default "true"; +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/test/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.runtime.itest/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/.classpath b/dependencymanager/org.apache.felix.dependencymanager.runtime/.classpath new file mode 100644 index 00000000000..7a6fc254361 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.runtime/.gitignore new file mode 100644 index 00000000000..90dde36e4ac --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/.gitignore @@ -0,0 +1,3 @@ +/bin/ +/bin_test/ +/generated/ diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/.project b/dependencymanager/org.apache.felix.dependencymanager.runtime/.project new file mode 100644 index 00000000000..56b71cc9860 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/.project @@ -0,0 +1,23 @@ + + + org.apache.felix.dependencymanager.runtime + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/.settings/org.eclipse.jdt.core.prefs b/dependencymanager/org.apache.felix.dependencymanager.runtime/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..c85c7421df9 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,306 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 +org.eclipse.jdt.core.formatter.align_type_members_on_columns=false +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_assignment=0 +org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80 +org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header=0 +org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_module_statements=16 +org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 +org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references=0 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 +org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=0 +org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_type_arguments=0 +org.eclipse.jdt.core.formatter.alignment_for_type_parameters=0 +org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 +org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_after_package=1 +org.eclipse.jdt.core.formatter.blank_lines_before_field=0 +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 +org.eclipse.jdt.core.formatter.blank_lines_before_method=1 +org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 +org.eclipse.jdt.core.formatter.blank_lines_before_package=0 +org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 +org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 +org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block=next_line +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=next_line +org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=next_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=next_line +org.eclipse.jdt.core.formatter.brace_position_for_switch=next_line +org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=next_line +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false +org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false +org.eclipse.jdt.core.formatter.comment.format_block_comments=false +org.eclipse.jdt.core.formatter.comment.format_header=false +org.eclipse.jdt.core.formatter.comment.format_html=true +org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=false +org.eclipse.jdt.core.formatter.comment.format_line_comments=false +org.eclipse.jdt.core.formatter.comment.format_source_code=true +org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true +org.eclipse.jdt.core.formatter.comment.indent_root_tags=true +org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=insert +org.eclipse.jdt.core.formatter.comment.line_length=80 +org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true +org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true +org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false +org.eclipse.jdt.core.formatter.compact_else_if=true +org.eclipse.jdt.core.formatter.continuation_indentation=1 +org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 +org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off +org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on +org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false +org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true +org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_empty_lines=false +org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true +org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true +org.eclipse.jdt.core.formatter.indentation.size=4 +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert +org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert +org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert +org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.join_lines_in_comments=true +org.eclipse.jdt.core.formatter.join_wrapped_lines=true +org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false +org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false +org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false +org.eclipse.jdt.core.formatter.lineSplit=120 +org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false +org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 +org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 +org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause=common_lines +org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true +org.eclipse.jdt.core.formatter.tabulation.char=space +org.eclipse.jdt.core.formatter.tabulation.size=4 +org.eclipse.jdt.core.formatter.use_on_off_tags=false +org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false +org.eclipse.jdt.core.formatter.wrap_before_assignment_operator=false +org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true +org.eclipse.jdt.core.formatter.wrap_before_conditional_operator=true +org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true +org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true +org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/.settings/org.eclipse.jdt.ui.prefs b/dependencymanager/org.apache.felix.dependencymanager.runtime/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000000..4cbb76b350b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +formatter_profile=_Apache Felix Eclipse Template +formatter_settings_version=13 diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/bnd.bnd b/dependencymanager/org.apache.felix.dependencymanager.runtime/bnd.bnd new file mode 100644 index 00000000000..9b9289f578f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/bnd.bnd @@ -0,0 +1,36 @@ +# +# 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. +# +Bundle-Version: 4.0.7 +-buildpath: \ + osgi.core;version=4.2,\ + osgi.cmpn;version=4.2,\ + org.apache.felix.dependencymanager;version=latest +Bundle-Activator:org.apache.felix.dm.runtime.Activator +Private-Package: \ + org.apache.felix.dm.runtime +Export-Package: \ + org.apache.felix.dm.runtime.api +Provide-Capability: osgi.extender; osgi.extender="org.apache.felix.dependencymanager.runtime";\ + uses:="org.apache.felix.dm";version:Version="4.0.0" +Include-Resource: META-INF/=resources/,META-INF/changelog.txt=changelog.txt +Bundle-Name: Apache Felix Dependency Manager Runtime +Bundle-Description: Loads Apache Felix Dependency Manager component descriptors from active \ + annoted bundles +Bundle-Category: osgi +Bundle-License: http://www.apache.org/licenses/LICENSE-2.0.txt +Bundle-Vendor: The Apache Software Foundation +Bundle-ContactAddress: http://felix.apache.org/documentation/subprojects/apache-felix-dependency-manager.html diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/changelog.txt b/dependencymanager/org.apache.felix.dependencymanager.runtime/changelog.txt new file mode 100644 index 00000000000..f1c45b1da4c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/changelog.txt @@ -0,0 +1,265 @@ +Release Notes - Felix - Version org.apache.felix.dependencymanager-r15 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.6.0 + * org.apache.felix.dependencymanager.shell; version=4.0.8 + * org.apache.felix.dependencymanager.runtime; version=4.0.7 + * org.apache.felix.dependencymanager.annotation; version=5.0.1 + * org.apache.felix.dependencymanager.lambda; version=1.2.1 + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r13 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.5.0 + * org.apache.felix.dependencymanager.shell; version=4.0.7 + * org.apache.felix.dependencymanager.runtime; version=4.0.6 + * org.apache.felix.dependencymanager.annotation; version=5.0.0 + * org.apache.felix.dependencymanager.lambda; version=1.2.0 + + +** Bug + * [FELIX-5683] - getServiceProperties returns null instead of empty dictionary + * [FELIX-5716] - Dead Lock in DM + * [FELIX-5768] - DM Lambda stop callback not being called + * [FELIX-5955] - Move changelog.txt to toplevel project dir + * [FELIX-5956] - NPE when invoking a lifecycle runnable method from init method + +** New Feature + * [FELIX-5336] - Add support for prototype scope services in DM4 + + +** Improvement + * [FELIX-5967] - DM does not support java9+ + * [FELIX-5937] - Refactor DM bndtools/gradle project + * [FELIX-5939] - DM annotations enhancements + * [FELIX-5941] - DM APi enhancements + * [FELIX-5957] - Check if a default implementation is used only on optional dependencies + + +** Task + * [FELIX-5960] - Do not supply MD5 checksum + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r11 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.4.1 + * org.apache.felix.dependencymanager.shell; version=4.0.6 + * org.apache.felix.dependencymanager.runtime; version=4.0.5 + * org.apache.felix.dependencymanager.annotation; version=4.2.1 + * org.apache.felix.dependencymanager.lambda; version=1.1.1 + +** Bug + * [FELIX-5630] - NullObject is created for a required dependency if the component is removed and added again to the dependency manager + * [FELIX-5636] - Component of aspect service does not have any service properties anymore + * [FELIX-5657] - DM released sources can't be rebuilt + +** Improvement + * [FELIX-5619] - MultiProperyFilterIndex memory consumption + * [FELIX-5623] - Improve performance of ComponentImpl.getName method + * [FELIX-5650] - Support latest version of Gogo + * [FELIX-5653] - Simplify DM-Lambda samples + * [FELIX-5658] - Include poms in dm artifacts + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r9 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.4.0 + * org.apache.felix.dependencymanager.shell; version=4.0.5 + * org.apache.felix.dependencymanager.runtime; version=4.0.4 + * org.apache.felix.dependencymanager.annotation; version=4.2.0 + * org.apache.felix.dependencymanager.lambda; version=1.1.0 + +** Bug + * [FELIX-5236] - Single @Property annotation on a type doesn't work + * [FELIX-5242] - Configuration updates may be missed when the component is restarting + * [FELIX-5244] - Can't inject service using a method ref on a parent class method. + * [FELIX-5245] - Typo in error logged when a component callback is not found. + * [FELIX-5268] - Service not unregistered while bundle is starting + * [FELIX-5273] - Wrong log when a callback is not found from component instance(s) + * [FELIX-5274] - remove callback fails after manually removing dynamic dependencies + * [FELIX-5399] - Unable to define default map or list config types + * [FELIX-5400] - Can't override default configuration type list value using an empty list + * [FELIX-5401] - Can't override default configuration type map value using an empty map + * [FELIX-5402] - Factory configuration adapter ignores factory method + * [FELIX-5411] - When you stop a component, the service references are not ungotten. + * [FELIX-5426] - Remove callbacks aren't called for optional dependencies in a "circular" dependency scenario + * [FELIX-5428] - Dependency events set not cleared when component is removed + * [FELIX-5429] - Aspect swap callback sometimes not called on optional dependencies + * [FELIX-5469] - Methodcache system size property is not used + * [FELIX-5471] - Ensure that unbound services are always handled synchronously + * [FELIX-5517] - @Inject annotation ignored when applied on ServiceRegistration + * [FELIX-5519] - services are not ungotten when swapped by an aspect + * [FELIX-5523] - required dependencies added to a started adapter (or aspect) are not injected + + + + +** Improvement + * [FELIX-5228] - Upgrade DM With latest release of BndTools + * [FELIX-5237] - Configurable invocation handler should use default method values + * [FELIX-5346] - Start annotation not propagated to sub classes + * [FELIX-5355] - Allow to use properties having dots with configuration proxies + * [FELIX-5403] - Improve the Javadoc for org.apache.felix.dm.ComponentStateListener + * [FELIX-5405] - Do not have org.apache.felix.dm.Logger invoke toString() of message parameters when enabled log level is not high enough + * [FELIX-5406] - DM lambda fluent service properties don't support dots + * [FELIX-5407] - DM annotation plugin generates temp log files even if logging is disabled + * [FELIX-5408] - Parallel DM should not stop components asynchronously + * [FELIX-5467] - MultiPropertyFilterIndex is unusable when a service reference contains a lot of values for one key + * [FELIX-5499] - Remove usage of json.org from dependency manager + * [FELIX-5515] - Upgrade DM to OSGi R6 API + * [FELIX-5516] - Allow to not dereference services internally + * [FELIX-5518] - Remove all eclipse warnings in DM code + * [FELIX-5520] - ComponentStateListener not supported in DM lambda + * [FELIX-5521] - add more callback method signature in DM lambda service dependency callbacks + * [FELIX-5522] - Refactor aspect service implementation + * [FELIX-5524] - add more signatures for aspect swap callbacks + * [FELIX-5526] - Allow to use generic custom DM dependencies when using dm lambda. + * [FELIX-5531] - Document dependency callback signatures + * [FELIX-5532] - Swap callback is missing in @ServiceDependency annotation + + + +** Task + * [FELIX-5533] - Fix a semantic versioning issue when releasing dependency manager + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r8 +====================================================================== + +** Bug + * [FELIX-5146] - Service adapters turn on autoconf even if callbacks are used + * [FELIX-5147] - Bundle Adapter auto configures class fields even if callbacks are used + * [FELIX-5153] - DM4 calls stop before ungetService() on ServiceFactory components + * [FELIX-5155] - Adapter/Aspect extra service dependencies injected twice if using callback instance + * [FELIX-5178] - Make some component parameters as volatile + * [FELIX-5181] - Only log info/debug if dm annotation log parameter is enabled + * [FELIX-5187] - No errog log when configuration dependency callback is not found + * [FELIX-5188] - No error log when a factory pid adapter update callback is not found + * [FELIX-5192] - ConfigurationDependency race condition when component is stopped + * [FELIX-5193] - Factory Pid Adapter race condition when component is stopped + * [FELIX-5200] - Factory configuration adapter not restarted + + +** New Feature + * [FELIX-4689] - Create a more fluent syntax for the dependency manager builder + + +** Improvement + * [FELIX-5126] - Build DM using Java 8 + * [FELIX-5164] - Add support for callback instance in Aspects + * [FELIX-5177] - Support injecting configuration proxies + * [FELIX-5180] - Support for Java8 Repeatable Properties in DM annotations. + * [FELIX-5182] - Cleanup DM samples + * [FELIX-5201] - Improve how components are displayed with gogo shell + + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r6 +====================================================================== + +** Bug + * [FELIX-4974] - DM filter indices not enabled if the dependencymanager bundle is started first + * [FELIX-5045] - DM Optional callbacks may sometimes be invoked before start callback + * [FELIX-5046] - Gradle wrapper is not included in DM source release + + + + +** Improvement + * [FELIX-4921] - Ensure binary equality of the same bundle between successive DM releases + * [FELIX-4922] - Simplify DM changelog management + * [FELIX-5054] - Clean-up instance bound dependencies when component is destroyed + * [FELIX-5055] - Upgrade DM to BndTools 3.0.0 + * [FELIX-5104] - Call a conf dependency callback Instance with an instantiated component + * [FELIX-5113] - Remove useless wrong test in ConfigurationDependencyImpl + * [FELIX-5114] - Schedule configuration update in Component executor synchronously + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r5: +====================================================================== + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r5 + + + +** Bug + * [FELIX-4907] - ConfigurationDependency calls updated(null) when component is stopped. + * [FELIX-4910] - ComponentExecutorFactory does not allow to return null from getExecutorFor method. + * [FELIX-4913] - DM Optional callbacks may sometimes be invoked twice + + + + +** Improvement + * [FELIX-4876] - DM Annotations bnd plugin compatibility with Bndtools 2.4.1 / 3.0.0 versions + * [FELIX-4877] - DM Annotations should detect service type using more method signatures. + * [FELIX-4915] - Skip unecessary manifest headers in DM bnd file + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r3: +===================================================================== + +** Bug + * [FELIX-4858] - DependencyManager: missing createCopy method in timed service dependency + * [FELIX-4869] - Callbacks not invoked for dependencies that are added after the component is initialized + + + + +** Improvement + * [FELIX-4614] - Factory create() method should have access to the component definition + * [FELIX-4873] - Enhance DM API to get missing and circular dependencies + * [FELIX-4878] - Support more signatures for Dependency callbacks + * [FELIX-4880] - Missing callback instance support for some adapters + * [FELIX-4889] - Refactor dm shell command to use the org.apache.dm.diagnostics api + + +** Wish + * [FELIX-4875] - Update DM integration test with latest ConfigAdmin + + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r2: +===================================================================== + +** Bug + * [FELIX-4832] - ClassCastException with autoconfig Iterable fields + * [FELIX-4833] - Revisit some javadocs in the DM annotations. + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r1: +====================================================================== + +** Bug + * [FELIX-4304] - DependencyManager ComponentImpl should not assume all service properties are stored in a Hashtable + * [FELIX-4394] - Race problems in DependencyManager Configuration Dependency + * [FELIX-4588] - createCopy method ConfigurationDependency produces a malfunctioning clone + * [FELIX-4594] - Propagation from dependencies overwrites service properties + * [FELIX-4598] - BundleDependency can effectively track only one bundle + * [FELIX-4602] - TemporalServiceDependency does not properly propagate RuntimeExceptions + * [FELIX-4709] - Incorrect Named Dependencies are binded to the Service Instance + + +** New Feature + * [FELIX-4426] - Allow DM to manage collections of services + * [FELIX-4807] - New thread model for Dependency Manager + + +** Improvement + * [FELIX-3914] - Log unsuccessful field injections + * [FELIX-4158] - ComponentDeclaration should give access to component information + * [FELIX-4667] - "top" command for the Dependency Manager Shell + * [FELIX-4672] - Allow callbacks to third party instance for adapters + * [FELIX-4673] - Log any error thrown when trying to create a null object. + * [FELIX-4777] - Dynamic initialization time configuration of @ConfigurationDependency + * [FELIX-4805] - Deprecate DM annotation metatypes + + +** Wish + * [FELIX-2706] - Support callback delegation for Configuration Dependecies + * [FELIX-4600] - Cherrypicking of propagated properties + * [FELIX-4676] - Add Provide-Capability for DependencyManager Runtime bundle + * [FELIX-4680] - Add more DM ServiceDependency callback signatures + * [FELIX-4683] - Allow to configure the DependencyManager shell scope + * [FELIX-4684] - Replace DependencyManager Runtime "factorySet" by a cleaner API + * [FELIX-4816] - bndtools-ify Dependency Manager + * [FELIX-4818] - New release process for Dependency Manager + diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/resources/DEPENDENCIES b/dependencymanager/org.apache.felix.dependencymanager.runtime/resources/DEPENDENCIES new file mode 100644 index 00000000000..aa7306b334b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/resources/DEPENDENCIES @@ -0,0 +1,19 @@ +Apache Felix Dependency Manager Runtime +Copyright 2011-2017 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2017). +Licensed under the Apache License 2.0. + +III. Overall License Summary + +- Apache License 2.0 diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/resources/LICENSE b/dependencymanager/org.apache.felix.dependencymanager.runtime/resources/LICENSE new file mode 100644 index 00000000000..de4b130f350 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/resources/LICENSE @@ -0,0 +1,204 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/resources/NOTICE b/dependencymanager/org.apache.felix.dependencymanager.runtime/resources/NOTICE new file mode 100644 index 00000000000..42cae552d0c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/resources/NOTICE @@ -0,0 +1,6 @@ +Apache Felix Dependency Manager Runtime +Copyright 2011-2017 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/AbstractBuilder.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/AbstractBuilder.java new file mode 100644 index 00000000000..1ad9bc422c2 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/AbstractBuilder.java @@ -0,0 +1,214 @@ +/* + * 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.dm.runtime; + +import java.util.List; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Component.ServiceScope; +import org.apache.felix.dm.ComponentState; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; + +/** + * Base class for all kind of DM component builders (for Component, Aspect, Adapters ...). + * + * @author Felix Project Team + */ +public abstract class AbstractBuilder +{ + /** + * Returns the service component type. + */ + abstract String getType(); + + /** + * Builds the service component. + * @param serviceMetaData the service component metadata parsed from the descriptor file. + * @param serviceDependencies the service component dependencies metadata parsed from the descriptor file. + */ + abstract void build(MetaData serviceMetaData, List serviceDependencies, Bundle b, DependencyManager dm) + throws Exception; + + /** + * Sets common Service parameters, if provided from our Component descriptor + */ + protected void setCommonServiceParams(Component c, MetaData srvMeta) throws Exception + { + // Set auto configured component fields. + DependencyManager dm = c.getDependencyManager(); + boolean autoConfigureComponents = "true".equals(dm.getBundleContext().getProperty( + Activator.CONF_ENABLE_AUTOCONFIG)); + + if (!autoConfigureComponents) + { + c.setAutoConfig(BundleContext.class, Boolean.FALSE); + c.setAutoConfig(ServiceRegistration.class, Boolean.FALSE); + c.setAutoConfig(DependencyManager.class, Boolean.FALSE); + c.setAutoConfig(Component.class, Boolean.FALSE); + c.setAutoConfig(Bundle.class, Boolean.FALSE); + } + + // See if BundleContext must be auto configured. + String bundleContextField = srvMeta.getString(Params.bundleContextField, null); + if (bundleContextField != null) + { + c.setAutoConfig(BundleContext.class, bundleContextField); + } + + // See if DependencyManager must be auto configured. + String dependencyManagerField = srvMeta.getString(Params.dependencyManagerField, null); + if (dependencyManagerField != null) + { + c.setAutoConfig(DependencyManager.class, dependencyManagerField); + } + + // See if Component must be auto configured. + String componentField = srvMeta.getString(Params.componentField, null); + if (componentField != null) + { + c.setAutoConfig(Component.class, componentField); + } + + // See if Service Registration must be auto configured + String registrationField = srvMeta.getString(Params.registrationField, null); + if (registrationField != null) + { + c.setAutoConfig(ServiceRegistration.class, registrationField); + } + + // Now, if the component has a @Started annotation, then add our component state listener, + // which will callback the corresponding annotated method, once the component is started. + String registered = srvMeta.getString(Params.registered, null); + String unregistered = srvMeta.getString(Params.unregistered, null); + + if (registered != null || unregistered != null) + { + c.add(new RegistrationListener(registered, unregistered)); + } + + // See if a service scope is provided + String scope = srvMeta.getString(Params.scope, null); + if (scope != null) { + ServiceScope serviceScope = ServiceScope.valueOf(scope); + c.setScope(serviceScope); + } + + // See if Bundle must be auto configured + String bundleField = srvMeta.getString(Params.bundle, null); + if (bundleField != null) + { + c.setAutoConfig(Bundle.class, bundleField); + } + } + + /** + * Registers all unnamed dependencies into a given service. Named dependencies are + * handled differently, and are managed by the ServiceLifecycleHandler class. + * @throws Exception + */ + protected static void addUnamedDependencies(Bundle b, DependencyManager dm, Component s, MetaData srvMeta, + List depsMeta) throws Exception + { + for (MetaData dependency : depsMeta) + { + String name = dependency.getString(Params.name, null); + if (name == null) + { + DependencyBuilder depBuilder = new DependencyBuilder(dependency); + Log.instance().info("adding dependency %s into service %s", dependency, srvMeta); + Dependency d = depBuilder.build(b, dm); + s.add(d); + } + } + } + + static class RegistrationListener implements ComponentStateListener + { + private final String m_registered; + private final String m_unregistered; + private volatile boolean m_isRegistered; + + RegistrationListener(String registered, String unregistered) + { + m_registered = registered; + m_unregistered = unregistered; + } + + public void changed(Component c, ComponentState state) + { + boolean wasRegistered = m_isRegistered; + switch (state) + { + case TRACKING_OPTIONAL: + // Started + m_isRegistered = true; + if (!wasRegistered && m_registered != null) + { + // The component has registered a service: notify all component instances + Object[] componentInstances = c.getInstances(); + for (Object instance : componentInstances) + { + try + { + Class[][] signatures = new Class[][] { { ServiceRegistration.class }, {} }; + Object[][] params = new Object[][] { { c.getServiceRegistration() }, {} }; + InvocationUtil.invokeCallbackMethod(instance, m_registered, signatures, params); + } + catch (Throwable t) + { + Log.instance().error("Exception caught while invoking method %s on component %s", t, + m_registered, instance); + } + } + } + break; + + case INSTANTIATED_AND_WAITING_FOR_REQUIRED: + // Stopped + m_isRegistered = false; + if (wasRegistered && m_unregistered != null) + { + Object[] componentInstances = c.getInstances(); + for (Object instance : componentInstances) + { + try + { + InvocationUtil.invokeCallbackMethod(instance, m_unregistered, new Class[][] { {} }, + new Object[][] { {} }); + } + catch (Throwable t) + { + Log.instance().error("Exception caught while invoking method %s on component %s", t, + m_registered, instance); + } + } + } + break; + + default: + break; + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/Activator.java new file mode 100644 index 00000000000..f693808ae54 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/Activator.java @@ -0,0 +1,94 @@ +/* + * 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.dm.runtime; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.service.log.LogService; +import org.osgi.service.packageadmin.PackageAdmin; + +/* + * This is the Activator for our DependencyManager Component Runtime. + * Here, we'll track started/stopped bundles which have some DependencyManager + * descriptors (META-INF/dependencymanager/*.dm). Such descriptors are generated + * by the Bnd plugin which parses DependencyManager annotations at compile time. + * + * @author Felix Project Team + */ +public class Activator extends DependencyActivatorBase +{ + /** + * Name of bundle context property telling if log service is required or not. + * (default = false) + */ + final static String CONF_LOG = "org.apache.felix.dependencymanager.runtime.log"; + + /** + * Name of bundle context property telling if Components must be auto configured + * with BundleContext/ServiceRegistration etc .. (default = false) + */ + final static String CONF_ENABLE_AUTOCONFIG = "org.apache.felix.dependencymanager.runtime.autoconfig"; + + /** + * Initialize our DependencyManager Runtime service. + * + * We depend on the OSGi LogService, and we track all started bundles which do have a + * "DependencyManager-Component" Manifest header. + * If the "dm.runtime.log=true" or "dm.runtime.packageAdmin=true" parameter is configured in the Felix config.properties + * then we'll use a required/temporal service dependency over the respective service. + * These temporal dependencies avoid us to be restarted if the respective service is temporarily + * unavailable (that is: when the service is updating). + * if the "dm.runtime.log" or "dm.runtime.packageAdmin" is not configured or it it is set to false, then we'll use + * an optional dependency over the respective service, in order to use a NullObject in case + * the service is not available. + */ + @Override + public void init(BundleContext context, DependencyManager dm) throws Exception + { + boolean logEnabled = "true".equalsIgnoreCase(context.getProperty(CONF_LOG)); + Log.instance().enableLogs(logEnabled); + Component component = createComponent() + .setImplementation(DependencyManagerRuntime.class) + .setComposition("getComposition") + .add(createBundleDependency() + .setRequired(false) + .setStateMask(Bundle.ACTIVE) + .setFilter("(DependencyManager-Component=*)") + .setCallbacks("bundleStarted", "bundleStopped")) + .add(createServiceDependency() + .setRequired(true) + .setService(PackageAdmin.class)) + .add(createServiceDependency() + .setRequired(logEnabled) + .setService(LogService.class)); + + dm.add(component); + } + + /** + * Our bundle is stopping: shutdown our Dependency Manager Runtime service. + */ + @Override + public void destroy(BundleContext context, DependencyManager dm) throws Exception + { + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/AdapterServiceBuilder.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/AdapterServiceBuilder.java new file mode 100644 index 00000000000..6acd109264a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/AdapterServiceBuilder.java @@ -0,0 +1,104 @@ +/* + * 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.dm.runtime; + +import java.util.Dictionary; +import java.util.List; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.Bundle; + +/** + * Builded called when the JSON parser find an adapter service descriptor. + * + * @author Felix Project Team + */ +public class AdapterServiceBuilder extends AbstractBuilder +{ + /** The type attribute specified in the JSON descriptor */ + private final static String TYPE = "AdapterService"; + + @Override + public String getType() + { + return TYPE; + } + + @Override + public void build(MetaData srvMeta, List depsMeta, Bundle b, DependencyManager dm) + throws Exception + { + Class adapterImplClass = b.loadClass(srvMeta.getString(Params.impl)); + String[] provides = srvMeta.getStrings(Params.provides, null); + Dictionary adapterProperties = srvMeta.getDictionary(Params.properties, null); + Class adapteeService = b.loadClass(srvMeta.getString(Params.adapteeService)); + String adapteeFilter = srvMeta.getString(Params.adapteeFilter, null); + String field = srvMeta.getString(Params.field, null); + String added = srvMeta.getString(Params.added, null); + String changed = srvMeta.getString(Params.changed, null); + String removed = srvMeta.getString(Params.removed, null); + String swap = srvMeta.getString(Params.swap, null); + boolean propagate = "true".equals(srvMeta.getString(Params.propagate, "true")); + + if (field != null && (added != null || changed != null || removed != null || swap != null)) + { + throw new IllegalArgumentException("autoconfig field " + field + " can't be defined with both added/changed/removed/swap calllbacks"); + } + + Component c; + + if (field != null) + { + c = dm.createAdapterService(adapteeService, adapteeFilter, field, null, null, null, null, null, propagate); + } + else + { + if (added != null || changed != null || removed != null || swap != null) + { + c = dm.createAdapterService(adapteeService, adapteeFilter, null, null, added, changed, removed, swap, propagate); + + } + else + { + c = dm.createAdapterService(adapteeService, adapteeFilter, null, null, null, null, null, null, propagate); + } + } + + setCommonServiceParams(c, srvMeta); + c.setInterface(provides, adapterProperties); + + String factoryMethod = srvMeta.getString(Params.factoryMethod, null); + if (factoryMethod == null) + { + c.setImplementation(adapterImplClass); + } + else + { + c.setFactory(adapterImplClass, factoryMethod); + } + c.setComposition(srvMeta.getString(Params.composition, null)); + ServiceLifecycleHandler lfcleHandler = new ServiceLifecycleHandler(c, b, dm, srvMeta, depsMeta); + // The dependencies will be plugged by our lifecycle handler. + c.setCallbacks(lfcleHandler, "init", "start", "stop", "destroy"); + // Adds dependencies (except named dependencies, which are managed by the lifecycle handler). + addUnamedDependencies(b, dm, c, srvMeta, depsMeta); + dm.add(c); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/AspectServiceBuilder.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/AspectServiceBuilder.java new file mode 100644 index 00000000000..d24546cc33f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/AspectServiceBuilder.java @@ -0,0 +1,103 @@ +/* + * 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.dm.runtime; + +import java.util.Dictionary; +import java.util.List; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.Bundle; + +/** + * Class used to build an aspect service using metadata found from DependencyManager runtime + * meta-inf descriptor. + * + * @author Felix Project Team + */ +public class AspectServiceBuilder extends AbstractBuilder +{ + private final static String TYPE = "AspectService"; + + @Override + public String getType() + { + return TYPE; + } + + @Override + public void build(MetaData srvMeta, List depsMeta, Bundle b, DependencyManager dm) + throws Exception + { + Log.instance().info("AspectServiceBuilder: building aspect service: %s with dependencies %s", + srvMeta, + depsMeta); + + Class serviceInterface = b.loadClass(srvMeta.getString(Params.service)); + String serviceFilter = srvMeta.getString(Params.filter, null); + Dictionary aspectProperties = srvMeta.getDictionary(Params.properties, null); + int ranking = srvMeta.getInt(Params.ranking, 1); + String implClassName = srvMeta.getString(Params.impl); + Object implClass = b.loadClass(implClassName); + String field = srvMeta.getString(Params.field, null); + String added = srvMeta.getString(Params.added, null); + String changed = srvMeta.getString(Params.changed, null); + String removed = srvMeta.getString(Params.removed, null); + String swap = srvMeta.getString(Params.swap, null); + + if (field != null && (added != null || changed != null || removed != null || swap != null)) + { + throw new IllegalArgumentException("autoconfig field " + field + " can't be defined with both added/changed/removed/swap calllbacks"); + } + + Component c; + if (field != null) + { + c = dm.createAspectComponent() + .setAspect(serviceInterface, serviceFilter, ranking) + .setAspectField(field) + .setServiceProperties(aspectProperties); + } + else + { + c = dm.createAspectService(serviceInterface, serviceFilter, ranking, null, added, changed, removed, swap) + .setServiceProperties(aspectProperties); + } + + setCommonServiceParams(c, srvMeta); + String factoryMethod = srvMeta.getString(Params.factoryMethod, null); + if (factoryMethod == null) + { + c.setImplementation(implClass); + } + else + { + c.setFactory(implClass, factoryMethod); + } + + c.setComposition(srvMeta.getString(Params.composition, null)); + ServiceLifecycleHandler lfcleHandler = new ServiceLifecycleHandler(c, b, dm, srvMeta, depsMeta); + // The dependencies will be plugged by our lifecycle handler. + c.setCallbacks(lfcleHandler, "init", "start", "stop", "destroy"); + // Adds dependencies (except named dependencies, which are managed by the lifecycle + // handler). + addUnamedDependencies(b, dm, c, srvMeta, depsMeta); + dm.add(c); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/BundleAdapterServiceBuilder.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/BundleAdapterServiceBuilder.java new file mode 100644 index 00000000000..94f8e6207a3 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/BundleAdapterServiceBuilder.java @@ -0,0 +1,75 @@ +/* + * 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.dm.runtime; + +import java.util.Dictionary; +import java.util.List; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.Bundle; + +/** + * Class used to build a bundle adapter service using metadata found from DependencyManager runtime + * meta-inf descriptor. + * + * @author Felix Project Team + */ +public class BundleAdapterServiceBuilder extends AbstractBuilder +{ + private final static String TYPE = "BundleAdapterService"; + + @Override + public String getType() + { + return TYPE; + } + + @Override + public void build(MetaData srvMeta, List depsMeta, Bundle b, DependencyManager dm) + throws Exception + { + int stateMask = srvMeta.getInt(Params.stateMask, Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE); + String filter = srvMeta.getString(Params.filter, null); + Class adapterImplClass = b.loadClass(srvMeta.getString(Params.impl)); + String[] provides = srvMeta.getStrings(Params.provides, null); + Dictionary properties = srvMeta.getDictionary(Params.properties, null); + boolean propagate = "true".equals(srvMeta.getString(Params.propagate, "false")); + Component c = dm.createBundleAdapterService(stateMask, filter, propagate); + c.setInterface(provides, properties); + String factoryMethod = srvMeta.getString(Params.factoryMethod, null); + if (factoryMethod == null) + { + c.setImplementation(adapterImplClass); + } + else + { + c.setFactory(adapterImplClass, factoryMethod); + } + + setCommonServiceParams(c, srvMeta); + c.setComposition(srvMeta.getString(Params.composition, null)); + ServiceLifecycleHandler lfcleHandler = new ServiceLifecycleHandler(c, b, dm, srvMeta, depsMeta); + // The dependencies will be plugged by our lifecycle handler. + c.setCallbacks(lfcleHandler, "init", "start", "stop", "destroy"); + // Adds dependencies (except named dependencies, which are managed by the lifecycle handler). + addUnamedDependencies(b, dm, c, srvMeta, depsMeta); + dm.add(c); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/ComponentBuilder.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/ComponentBuilder.java new file mode 100644 index 00000000000..b9ec8be9697 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/ComponentBuilder.java @@ -0,0 +1,127 @@ +/* + * 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.dm.runtime; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.Set; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.runtime.api.ComponentFactory; +import org.osgi.framework.Bundle; + +/** + * Builds a DependencyManager Component. + * + * @author Felix Project Team + */ +public class ComponentBuilder extends AbstractBuilder +{ + private final static String TYPE = "Component"; + public final static String FACTORY_NAME = "dm.factory.name"; + public final static String FACTORY_INSTANCE = "dm.factory.instance"; + + @Override + public String getType() + { + return TYPE; + } + + @Override + public void build(MetaData srvMeta, List depsMeta, Bundle b, DependencyManager dm) + throws Exception + { + Component c = dm.createComponent(); + String factory = srvMeta.getString(Params.factorySet, null); + String factoryName = srvMeta.getString(Params.factoryName, null); + + // Setup Component auto config fields + setCommonServiceParams(c, srvMeta); + + // Check if we must provide a Component factory set (deprecated), or a ComponentFactory. + if (factory == null && factoryName == null) + { + Log.instance().info("ComponentBuilder: building service %s with dependencies %s", + srvMeta, + depsMeta); + + String impl = srvMeta.getString(Params.impl); + String composition = srvMeta.getString(Params.composition, null); + String factoryMethod = srvMeta.getString(Params.factoryMethod, null); + if (factoryMethod == null) + { + c.setImplementation(b.loadClass(impl)); + } + else + { + c.setFactory(b.loadClass(impl), factoryMethod); + } + c.setComposition(composition); + + // Adds dependencies (except named dependencies, which are managed by the lifecycle + // handler). + addUnamedDependencies(b, dm, c, srvMeta, depsMeta); + // Creates a ServiceHandler, which will filter all service lifecycle callbacks. + ServiceLifecycleHandler lfcleHandler = + new ServiceLifecycleHandler(c, b, dm, srvMeta, depsMeta); + c.setCallbacks(lfcleHandler, "init", "start", "stop", "destroy"); + + // Set the provided services + Dictionary properties = srvMeta.getDictionary(Params.properties, null); + String[] services = srvMeta.getStrings(Params.provides, null); + c.setInterface(services, properties); + } + else if (factory != null) /* deprecated */ + { + Log.instance() + .info("ComponentBuilder: providing factory set for service %s with dependencies %s", + srvMeta, + depsMeta); + + // We don't instantiate the service, but instead we provide a Set in the registry. + // This Set will act as a factory and another component may registers some + // service configurations into it in order to fire some service instantiations. + FactorySet factorySet = new FactorySet(b, srvMeta, depsMeta); + c.setImplementation(factorySet); + c.setCallbacks(null, "start", "stop", null); + Hashtable props = new Hashtable(); + props.put(ComponentBuilder.FACTORY_NAME, factory); + c.setInterface(Set.class.getName(), props); + } + else if (factoryName != null) { + Log.instance() + .info("ComponentBuilder: providing component factory for service %s with dependencies %s", + srvMeta, + depsMeta); + + // We don't instantiate the service, but instead we provide a ComponentFactory in the registry. + // (similar to DS ComponentFactory). + ComponentFactoryImpl compFactory = new ComponentFactoryImpl(b, srvMeta, depsMeta); + c.setImplementation(compFactory); + c.setCallbacks(null, "start", "stop", null); + Hashtable props = new Hashtable(); + props.put(ComponentBuilder.FACTORY_NAME, factoryName); + c.setInterface(ComponentFactory.class.getName(), props); + } + + dm.add(c); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/ComponentFactoryImpl.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/ComponentFactoryImpl.java new file mode 100644 index 00000000000..3a15ded2270 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/ComponentFactoryImpl.java @@ -0,0 +1,121 @@ +/* + * 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.dm.runtime; + +import java.util.Dictionary; +import java.util.List; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.runtime.api.ComponentException; +import org.apache.felix.dm.runtime.api.ComponentFactory; +import org.apache.felix.dm.runtime.api.ComponentInstance; +import org.osgi.framework.Bundle; + +/** + * This class implements a DM Component factory service. + * When a Component contains a factoryName attribute, this class is provided + * into the OSGi registry with a org.apache.felix.dependencymanager.factory.name service property. + * + * @author Felix Project Team + */ +public class ComponentFactoryImpl implements ComponentFactory { + /** + * The list of Dependencies which are applied in the Service. + */ + private MetaData m_srvMeta; + + /** + * The list of Dependencies which are applied in the Service. + */ + private List m_depsMeta; + + /** + * The DependencyManager which is used to create Service instances. + */ + private DependencyManager m_dm; + + /** + * Flag used to check if our service is Active. + */ + private volatile boolean m_active; + + /** + * The bundle containing the Service annotated with the factory attribute. + */ + private final Bundle m_bundle; + + /** + * Sole constructor. + * @param b the bundle containing the Service annotated with the factory attribute + * @param srvMeta the component service metadata + * @param depsMeta teh component dependencies metadata + */ + public ComponentFactoryImpl(Bundle b, MetaData srvMeta, List depsMeta) { + m_bundle = b; + m_srvMeta = srvMeta; + m_depsMeta = depsMeta; + } + + /** + * Our Service is starting. + */ + public void start(Component c) { + m_active = true; + m_dm = c.getDependencyManager(); + } + + /** + * Our Service is stopping. + */ + public void stop() { + m_active = false; + } + + /** + * Create or Update a Service. + */ + public ComponentInstance newInstance(Dictionary conf) { + // Check parameter validity + if (conf == null) { + throw new NullPointerException("configuration parameter can't be null"); + } + + // Check if our service is running. + checkServiceAvailable(); + + try { + ComponentInstanceImpl instance = new ComponentInstanceImpl(m_dm,m_bundle, m_srvMeta, m_depsMeta, conf); + return instance; + + } catch (Throwable t) { + Log.instance().error("ServiceFactory: could not instantiate service %s", t, m_srvMeta); + throw new ComponentException("could not instantiate factory component", t); + } + } + + /** + * Checks if our Service is available (we are not stopped"). + */ + private void checkServiceAvailable() { + if (!m_active) { + throw new IllegalStateException("Service not available"); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/ComponentInstanceImpl.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/ComponentInstanceImpl.java new file mode 100644 index 00000000000..90c0b1f83ef --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/ComponentInstanceImpl.java @@ -0,0 +1,207 @@ +/* + * 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.dm.runtime; + +import java.lang.reflect.Method; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.List; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.runtime.api.ComponentInstance; +import org.osgi.framework.Bundle; + +/** + * Implementation for our DM Runtime ComponentInstance. + * + * @author Felix Project Team + */ +public class ComponentInstanceImpl implements ComponentInstance { + /** + * The list of Dependencies which are applied in the Service. + */ + private final MetaData m_srvMeta; + + /** + * The list of Dependencies which are applied in the Service. + */ + private final List m_depsMeta; + + /** + * The DependencyManager which is used to create Service instances. + */ + private final DependencyManager m_dm; + + /** + * The bundle containing the Service annotated with the factory attribute. + */ + private final Bundle m_bundle; + + /** + * The component + */ + private final Object m_impl; + + /** + * The DM Component used to define the component + */ + private final Component m_component; + + public ComponentInstanceImpl(DependencyManager dm, Bundle b, MetaData srvMeta, List depsMeta, Dictionary conf) throws Exception + { + m_bundle = b; + m_dm = dm; + m_srvMeta = srvMeta; + m_depsMeta = depsMeta; + m_component = m_dm.createComponent(); + + Class implClass = m_bundle.loadClass(m_srvMeta.getString(Params.impl)); + Object impl = conf.get(ComponentBuilder.FACTORY_INSTANCE); + if (impl == null) { + String factoryMethod = m_srvMeta.getString(Params.factoryMethod, null); + if (factoryMethod == null) { + impl = implClass.newInstance(); + } else { + Method m = implClass.getDeclaredMethod(factoryMethod); + m.setAccessible(true); + impl = m.invoke(null); + } + } + m_impl = impl; + + // Invoke "configure" callback + String configure = m_srvMeta.getString(Params.factoryConfigure, null); + + if (configure != null) { + invokeConfigure(impl, configure, conf); + } + + // Create Service + m_component.setImplementation(impl); + String[] provides = m_srvMeta.getStrings(Params.provides, null); + if (provides != null) { + // Merge service properties with the configuration provided by the factory. + Dictionary serviceProperties = m_srvMeta.getDictionary(Params.properties, null); + serviceProperties = mergeSettings(serviceProperties, conf); + m_component.setInterface(provides, serviceProperties); + } + + m_component.setComposition(m_srvMeta.getString(Params.composition, null)); + ServiceLifecycleHandler lfcleHandler = new ServiceLifecycleHandler(m_component, m_bundle, m_dm, m_srvMeta, m_depsMeta); + // The dependencies will be plugged by our lifecycle handler. + m_component.setCallbacks(lfcleHandler, "init", "start", "stop", "destroy"); + + // Adds dependencies (except named dependencies, which are managed by the lifecycle handler). + for (MetaData dependency : m_depsMeta) { + String name = dependency.getString(Params.name, null); + if (name == null) { + DependencyBuilder depBuilder = new DependencyBuilder(dependency); + Log.instance().info("ServiceLifecycleHandler.init: adding dependency %s into service %s", dependency, + m_srvMeta); + Dependency d = depBuilder.build(m_bundle, m_dm); + m_component.add(d); + } + } + + // Register the Service instance, and keep track of it. + Log.instance().info("ServiceFactory: created service %s", m_srvMeta); + m_dm.add(m_component); + } + + @Override + public void dispose() { + m_dm.remove(m_component); + } + + @Override + public void update(Dictionary conf) { + // Reconfigure an already existing Service. + String configure = m_srvMeta.getString(Params.factoryConfigure, null); + if (configure != null) { + Log.instance().info("ServiceFactory: updating service %s", m_impl); + invokeConfigure(m_impl, configure, conf); + } + + // Update service properties + String[] provides = m_srvMeta.getStrings(Params.provides, null); + if (provides != null) { + Dictionary serviceProperties = m_srvMeta.getDictionary(Params.properties, null); + serviceProperties = mergeSettings(serviceProperties, conf); + m_component.setServiceProperties(serviceProperties); + } + } + + /** + * Invokes the configure callback method on the service instance implemenatation. + * @param impl + * @param configure + * @param config + */ + private void invokeConfigure(Object impl, String configure, Dictionary config) { + try { + InvocationUtil.invokeCallbackMethod(impl, configure, new Class[][] { { Dictionary.class } }, + new Object[][] { { config } }); + } + + catch (Throwable t) { + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } else { + throw new RuntimeException("Could not invoke method " + configure + " on object " + impl, t); + } + } + } + + /** + * Merge factory configuration settings with the service properties. The private factory configuration + * settings are ignored. A factory configuration property is private if its name starts with a dot ("."). + * + * @param serviceProperties + * @param factoryConfiguration + * @return + */ + private Dictionary mergeSettings(Dictionary serviceProperties, + Dictionary factoryConfiguration) + { + Dictionary props = new Hashtable<>(); + + if (serviceProperties != null) { + Enumeration keys = serviceProperties.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + Object val = serviceProperties.get(key); + props.put(key, val); + } + } + + Enumeration keys = factoryConfiguration.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + if (!key.toString().startsWith(".")) { + // public properties are propagated + Object val = factoryConfiguration.get(key); + props.put(key, val); + } + } + return props; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/DependencyBuilder.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/DependencyBuilder.java new file mode 100644 index 00000000000..a5007f8853b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/DependencyBuilder.java @@ -0,0 +1,254 @@ +/* + * 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.dm.runtime; + +import org.apache.felix.dm.BundleDependency; +import org.apache.felix.dm.ConfigurationDependency; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ResourceDependency; +import org.apache.felix.dm.ServiceDependency; +import org.osgi.framework.Bundle; + +/** + * Class used to build a concrete dependency from meta data. + * + * @author Felix Project Team + */ +public class DependencyBuilder +{ + public enum DependencyType + { + ServiceDependency, + TemporalServiceDependency, + ConfigurationDependency, + BundleDependency, + ResourceDependency + } + + private MetaData m_metaData; + + public DependencyBuilder(MetaData dependencyMetaData) + { + m_metaData = dependencyMetaData; + } + + public Dependency build(Bundle b, DependencyManager dm) + throws Exception + { + Dependency dp = null; + DependencyType type; + + try + { + type = DependencyType.valueOf(m_metaData.getString(Params.type)); + } + catch (IllegalArgumentException e) + { + throw new IllegalArgumentException("no \"type\" parameter found from metaData: " + m_metaData); + } + + switch (type) + { + case ServiceDependency: + dp = createServiceDependency(b, dm); + break; + + case ConfigurationDependency: + dp = createConfigurationDependency(b, dm); + break; + + case BundleDependency: + dp = createBundleDependency(dm); + break; + + case ResourceDependency: + dp = createResourceDependency(dm); + break; + + default: + throw new IllegalArgumentException("Can't build service dependency: " + type); + } + return dp; + } + + private Dependency createServiceDependency(Bundle b, DependencyManager dm) + throws ClassNotFoundException + { + String service = m_metaData.getString(Params.service, null); + Class serviceClass = service != null ? b.loadClass(service) : null; + String serviceFilter = m_metaData.getString(Params.filter, null); + String defaultServiceImpl = m_metaData.getString(Params.defaultImpl, null); + Class defaultServiceImplClass = + (defaultServiceImpl != null) ? b.loadClass(defaultServiceImpl) : null; + String added = m_metaData.getString(Params.added, null); + long timeout = m_metaData.getLong(Params.timeout, -1L); + String changed = timeout != -1 ? null : m_metaData.getString(Params.changed, null); + String removed = timeout != -1 ? null : m_metaData.getString(Params.removed, null); + String swap = m_metaData.getString(Params.swap, null); + String autoConfigField = m_metaData.getString(Params.autoConfig, null); + boolean required = "true".equals(m_metaData.getString(Params.required, "true")); + boolean propagate = "true".equals(m_metaData.getString(Params.propagate, "false")); + boolean dereference = "true".equals(m_metaData.getString(Params.dereference, "true")); + + Dependency dp = createServiceDependency(dm, serviceClass, + serviceFilter, defaultServiceImplClass, added, changed, + removed, swap, autoConfigField, timeout, required, propagate, dereference); + return dp; + } + + private Dependency createServiceDependency(DependencyManager dm, Class serviceClass, + String serviceFilter, Class defaultServiceImplClass, String added, + String changed, String removed, String swap, String autoConfigField, long timeout, boolean required, + boolean propagate, boolean dereference) + { + ServiceDependency sd = timeout != -1 ? dm.createTemporalServiceDependency(timeout) + : dm.createServiceDependency(); + sd.setService(serviceClass, serviceFilter); + if (defaultServiceImplClass != null) + { + sd.setDefaultImplementation(defaultServiceImplClass); + } + sd.setCallbacks(added, changed, removed, swap); + if (autoConfigField != null) + { + sd.setAutoConfig(autoConfigField); + } + if (timeout == -1) + { + sd.setRequired(required); + } + + sd.setPropagate(propagate); + sd.setDereference(dereference); + return sd; + } + + private Dependency createConfigurationDependency(Bundle b, DependencyManager dm) throws Exception + { + String configType = m_metaData.getString(Params.configType, null); + String[] configTypes = m_metaData.getStrings(Params.configTypes, null); + configTypes = configTypes == null ? ((configType != null) ? new String[] { configType } : null) : configTypes; + String pid = m_metaData.getString(Params.pid); + boolean propagate = "true".equals(m_metaData.getString(Params.propagate, "false")); + String callback = m_metaData.getString(Params.updated, "updated"); + boolean required = "true".equals(m_metaData.getString(Params.required, "true")); + return createConfigurationDependency(dm, b, pid, callback, configTypes, propagate, required); + } + + private Dependency createConfigurationDependency(DependencyManager dm, Bundle b, String pid, String callback, String[] configTypes, boolean propagate, boolean required) + throws ClassNotFoundException + { + if (pid == null) + { + throw new IllegalArgumentException( + "pid attribute not provided in ConfigurationDependency declaration"); + } + ConfigurationDependency cd = dm.createConfigurationDependency(); + cd.setPid(pid); + cd.setCallback(callback); + cd.setPropagate(propagate, true /* configuration will override default service properties */); + cd.setRequired(required); + + if (configTypes != null) + { + Class[] configTypeClasses = new Class[configTypes.length]; + for (int i = 0; i < configTypes.length; i ++) + { + configTypeClasses[i] = b.loadClass(configTypes[i]); + } + cd.setConfigType(configTypeClasses); + } + return cd; + } + + /** + * Creates a BundleDependency that we parsed from a component descriptor entry. + * @param b + * @param dm + * @param parser + * @return + */ + private Dependency createBundleDependency(DependencyManager dm) + { + String added = m_metaData.getString(Params.added, null); + String changed = m_metaData.getString(Params.changed, null); + String removed = m_metaData.getString(Params.removed, null); + boolean required = "true".equals(m_metaData.getString(Params.required, "true")); + String filter = m_metaData.getString(Params.filter, null); + int stateMask = m_metaData.getInt(Params.stateMask, -1); + boolean propagate = "true".equals(m_metaData.getString(Params.propagate, "false")); + + Dependency dp = createBundleDependency(dm, added, changed, removed, required, propagate, filter, + stateMask); + return dp; + } + + private Dependency createBundleDependency(DependencyManager dm, String added, String changed, + String removed, boolean required, boolean propagate, String filter, int stateMask) + { + BundleDependency bd = dm.createBundleDependency(); + bd.setCallbacks(added, changed, removed); + bd.setRequired(required); + bd.setPropagate(propagate); + if (filter != null) + { + bd.setFilter(filter); + } + if (stateMask != -1) + { + bd.setStateMask(stateMask); + } + return bd; + } + + private Dependency createResourceDependency(DependencyManager dm) + { + String added = m_metaData.getString(Params.added, null); + String changed = m_metaData.getString(Params.changed, null); + String removed = m_metaData.getString(Params.removed, null); + String filter = m_metaData.getString(Params.filter, null); + boolean required = "true".equals(m_metaData.getString(Params.required, "true")); + boolean propagate = "true".equals(m_metaData.getString(Params.propagate, "false")); + String autoConfigField = m_metaData.getString(Params.autoConfig, null); + + Dependency dp = createResourceDependency(dm, added, changed, removed, required, filter, + propagate, autoConfigField); + return dp; + } + + private Dependency createResourceDependency(DependencyManager dm, String added, + String changed, String removed, boolean required, String filter, boolean propagate, + String autoConfigField) + { + ResourceDependency rd = dm.createResourceDependency(); + rd.setCallbacks(added, changed, removed); + rd.setRequired(required); + if (filter != null) + { + rd.setFilter(filter); + } + if (autoConfigField != null) + { + rd.setAutoConfig(autoConfigField); + } + rd.setPropagate(propagate); + return rd; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/DependencyManagerRuntime.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/DependencyManagerRuntime.java new file mode 100644 index 00000000000..f1bde97dfe7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/DependencyManagerRuntime.java @@ -0,0 +1,227 @@ +/* + * 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.dm.runtime; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.Bundle; +import org.osgi.service.packageadmin.PackageAdmin; + +/** + * This class parses service descriptors generated by the annotation bnd processor. + * The descriptors are located under META-INF/dependencymanager directory. Such files are actually + * referenced by a specific "DependendencyManager-Component" manifest header. + * + * @author Felix Project Team + */ +public class DependencyManagerRuntime +{ + /** + * Map between bundles and their corresponding DependencyManager objects used to create bundle's components. + * Notice that we can safely use this map without synchronization because we are relying on the new DM4 thread + * model which serialize all component events safely. + */ + private final Map m_managers = new HashMap(); + + /** + * Parser used to scan component descriptors defined in bundles meta data. + */ + private final DescriptorParser m_parser; + + /** + * We use the PackageAdmin service to allow support for annotations in fragment bundles. + */ + private volatile PackageAdmin m_packageAdmin; + + /** + * Our constructor. We'll initialize here our DM component builders. + */ + public DependencyManagerRuntime() + { + // Instantiates our descriptor parser, and register our service builders into it. + m_parser = new DescriptorParser(); + m_parser.addBuilder(new ComponentBuilder()); + m_parser.addBuilder(new AspectServiceBuilder()); + m_parser.addBuilder(new AdapterServiceBuilder()); + m_parser.addBuilder(new BundleAdapterServiceBuilder()); + m_parser.addBuilder(new FactoryConfigurationAdapterServiceBuilder()); + m_parser.addBuilder(new ResourceAdapterServiceBuilder()); + } + + /** + * Return our Object Composition (the Activator will inject dependencies into it) + */ + protected Object[] getComposition() + { + return new Object[] { this, Log.instance() }; + } + + /** + * Starts our Service (at this point, we have been injected with our bundle context, as well + * as with our log service. We'll listen to bundle start/stop events (we implement the + * SynchronousBundleListener interface). + */ + protected void start() + { + Log.instance().info("Starting Dependency Manager annotation runtime."); + } + + /** + * Stops our service. We'll stop all activated DependencyManager services. + */ + protected void stop() + { + Log.instance().info("Runtime: stopping services"); + for (DependencyManager dm : m_managers.values()) + { + List services = new ArrayList(dm.getComponents()); + for (Component service : services) + { + dm.remove(service); + } + } + + m_managers.clear(); + } + + /** + * Load the DM descriptors from the started bundle. We also check possible fragments + * attached to the bundle, which might also contain some DM descriptors. + * @param bundle the started bundle which contains a DependencyManager-Component header + */ + protected void bundleStarted(Bundle bundle) + { + Log.instance().info("Scanning started bundle %s", bundle.getSymbolicName()); + List descriptorURLs = new ArrayList(); + collectDescriptors(bundle, descriptorURLs); + Bundle[] fragments = m_packageAdmin.getFragments(bundle); + if (fragments != null) + { + for (Bundle fragment : fragments) + { + collectDescriptors(fragment, descriptorURLs); + } + } + for (URL descriptorURL : descriptorURLs) + { + loadDescriptor(bundle, descriptorURL); + } + } + + /** + * Unregisters all services for a stopping bundle. + * @param b + */ + protected void bundleStopped(Bundle b) + { + Log.instance().info("Runtime: Removing services from stopping bundle: %s", b.getSymbolicName()); + DependencyManager dm = m_managers.remove(b); + if (dm != null) + { + List services = new ArrayList(dm.getComponents()); + for (Component service : services) + { + Log.instance().info("Runtime: Removing service: %s", service); + dm.remove(service); + } + } + } + + /** + * Collect all descriptors found from a given bundle, including its possible attached fragments. + * @param bundle a started bundle containing some DM descriptors + * @param out the list of descriptors' URLS found from the started bundle, as well as from possibly + * attached fragments. + */ + private void collectDescriptors(Bundle bundle, List out) { + String descriptorPaths = (String) bundle.getHeaders().get("DependencyManager-Component"); + if (descriptorPaths == null) + { + return; + } + + for (String descriptorPath : descriptorPaths.split(",")) + { + URL descriptorURL = bundle.getEntry(descriptorPath); + if (descriptorURL == null) + { + Log.instance() + .error("Runtime: " + "DependencyManager component descriptor not found: %s", + descriptorPath); + continue; + } + out.add(descriptorURL); + } + } + + /** + * Load a DependencyManager component descriptor from a given bundle. + * @param b + * @param descriptorURL + */ + private void loadDescriptor(Bundle b, URL descriptorURL) + { + Log.instance().debug("Parsing descriptor %s from bundle %s", descriptorURL, b.getSymbolicName()); + + BufferedReader in = null; + try + { + in = new BufferedReader(new InputStreamReader(descriptorURL.openStream())); + DependencyManager dm = m_managers.get(b); + if (dm == null) + { + dm = new DependencyManager(b.getBundleContext()); + m_managers.put(b, dm); + } + + m_parser.parse(in, b, dm); + } + + catch (Throwable t) + { + Log.instance().error("Runtime: Error while parsing descriptor %s from bundle %s", + t, + descriptorURL, + b.getSymbolicName()); + } + + finally + { + if (in != null) + { + try + { + in.close(); + } + catch (IOException ignored) + { + } + } + } + } +} \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/DescriptorParser.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/DescriptorParser.java new file mode 100644 index 00000000000..77420020714 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/DescriptorParser.java @@ -0,0 +1,75 @@ +/* + * 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.dm.runtime; + +import java.io.BufferedReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.Bundle; + +/** + * This class parses files generated in META-INF/*.dm by the DependencyManager bnd plugin. + * Each descriptor contains a JSON definition of a Service, along with its corresponding + * dependencies. + * + * @author Felix Project Team + */ +public class DescriptorParser +{ + private Map m_builders = new HashMap(); + + public void addBuilder(AbstractBuilder sb) + { + m_builders.put(sb.getType(), sb); + } + + public void parse(BufferedReader reader, Bundle b, DependencyManager dm) throws Exception + { + String line; + + // The first line is a Service Component (a Service, an Aspect Service, etc ...) + line = reader.readLine(); + Log.instance().debug("DescriptorParser: parsing service %s", line); + JSONMetaData serviceMetaData = new JSONMetaData(line); + + String type = serviceMetaData.getString(Params.type); + + AbstractBuilder builder = m_builders.get(type); + if (builder == null) + { + throw new IllegalArgumentException("Invalid descriptor" + + ": invalid \"type\" parameter found in first line"); + } + + // Parse the rest of the lines (dependencies) + List serviceDependencies = new ArrayList(); + while ((line = reader.readLine()) != null) + { + Log.instance().debug("Parsing dependency %s", line); + serviceDependencies.add(new JSONMetaData(line)); + } + + // and Invoke the builder + builder.build(serviceMetaData, serviceDependencies, b, dm); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/FactoryConfigurationAdapterServiceBuilder.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/FactoryConfigurationAdapterServiceBuilder.java new file mode 100644 index 00000000000..74fc13d70d3 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/FactoryConfigurationAdapterServiceBuilder.java @@ -0,0 +1,104 @@ +/* + * 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.dm.runtime; + +import java.util.Dictionary; +import java.util.List; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.Bundle; + +/** + * Class used to build a factory configuration adapter service using metadata found from DependencyManager runtime + * meta-inf descriptor. + * + * @author Felix Project Team + */ +public class FactoryConfigurationAdapterServiceBuilder extends AbstractBuilder +{ + private final static String TYPE = "FactoryConfigurationAdapterService"; + + @Override + public String getType() + { + return TYPE; + } + + @Override + public void build(MetaData srvMeta, List depsMeta, Bundle b, DependencyManager dm) + throws Exception + { + Class implClass = b.loadClass(srvMeta.getString(Params.impl)); + String factoryPid = srvMeta.getString(Params.factoryPid); + String updated = srvMeta.getString(Params.updated); + String[] provides = srvMeta.getStrings(Params.provides, null); + Dictionary properties = srvMeta.getDictionary(Params.properties, null); + boolean propagate = "true".equals(srvMeta.getString(Params.propagate, "false")); + Class[] configTypes = getConfigTypes(b, srvMeta); + Component c = null; + + c = dm.createFactoryComponent() + .setFactoryPid(factoryPid) + .setUpdated(updated) + .setPropagate(propagate) + .setConfigType(configTypes); + + c.setInterface(provides, properties); + String factoryMethod = srvMeta.getString(Params.factoryMethod, null); + + if (factoryMethod == null) + { + c.setImplementation(implClass); + } + else + { + c.setFactory(implClass, factoryMethod); + } + setCommonServiceParams(c, srvMeta); + c.setComposition(srvMeta.getString(Params.composition, null)); + ServiceLifecycleHandler lfcleHandler = new ServiceLifecycleHandler(c, b, dm, srvMeta, depsMeta); + // The dependencies will be plugged by our lifecycle handler. + c.setCallbacks(lfcleHandler, "init", "start", "stop", "destroy"); + // Adds dependencies (except named dependencies, which are managed by the lifecycle handler). + addUnamedDependencies(b, dm, c, srvMeta, depsMeta); + dm.add(c); + } + + private Class[] getConfigTypes(Bundle b, MetaData srvMeta) throws ClassNotFoundException + { + String configType = srvMeta.getString(Params.configType, null); + if (configType != null) + { + return new Class[] { b.loadClass(configType) }; + } + + String[] configTypes = srvMeta.getStrings(Params.configTypes, null); + if (configTypes != null) + { + Class[] configTypeClasses = new Class[configTypes.length]; + for (int i = 0; i < configTypes.length; i++) + { + configTypeClasses[i] = b.loadClass(configTypes[i]); + } + return configTypeClasses; + } + return null; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/FactorySet.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/FactorySet.java new file mode 100644 index 00000000000..6d042f37215 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/FactorySet.java @@ -0,0 +1,528 @@ +/* + * 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.dm.runtime; + +import java.lang.reflect.Method; +import java.util.AbstractSet; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.Bundle; + +/** + * This class implements a java.util.Set which acts as a service factory. + * When a Service contains a factory attribute, this class is provided + * into the OSGi registry with a dm.factory.name service property. So, another factory component + * may be injected with this Set. And each time a Dictionary configuration is registered in the Set, + * then a new Service instance will be instantiated, and will be provided with the Dictionary passed to the + * Service instance. + * + * @author Felix Project Team + */ +@SuppressWarnings( { "unchecked", "rawtypes"}) +public class FactorySet extends AbstractSet +{ + /** + * When a Dictionary is registered in a factory Set, we use this special + * property key, whose value may provide the instance to use when + * creating a service. + */ + private final static String DM_FACTORY_INSTANCE = "dm.factory.instance"; + + /** + * The actual Service instance that is allocated for each dictionaries added in this Set. + */ + private volatile Object m_impl; + + /** + * The Service provided in the OSGi registry. + */ + private final String[] m_provide; + + /** + * The properties to be provided by the Service. + */ + private final Dictionary m_serviceProperties; + + /** + * The configure Service callback used to pass configuration added in this Set. + */ + private final String m_configure; + + /** + * The map between our Dictionaries and corresponding Service instances. + * This map is modified from our serial executor, or from the add method. + */ + private final ConcurrentHashMap m_services = new ConcurrentHashMap(); + + /** + * The list of Dependencies which are applied in the Service. + */ + private final MetaData m_srvMeta; + + /** + * The list of Dependencies which are applied in the Service. + */ + private final List m_depsMeta; + + /** + * The DependencyManager which is used to create Service instances. + */ + private volatile DependencyManager m_dm; + + /** + * This class is used to serialize concurrent method calls, and allow to leave our methods unsynchronized. + * This is required because some of our methods may invoke some Service callbacks, which must be called outside + * synchronized block (in order to avoid potential dead locks). + */ + private final SerialExecutor m_serialExecutor = new SerialExecutor(); + + /** + * Flag used to check if our service is Active. + */ + private volatile boolean m_active; + + /** + * The bundle containing the Service annotated with the factory attribute. + */ + private final Bundle m_bundle; + + /** + * Flag used to check if a service is being created + */ + private final static Object SERVICE_CREATING = new Object(); + + /** + * This class wraps Dictionary, allowing to store the dictionary into a Map, using + * reference-equality in place of object-equality when getting the Dictionary from the Map. + */ + private static class ServiceKey + { + private Dictionary m_dictionary; + + public ServiceKey(Dictionary dictionary) + { + m_dictionary = dictionary; + } + + Dictionary getDictionary() + { + return m_dictionary; + } + + @Override + public boolean equals(Object that) + { + return that instanceof ServiceKey ? (((ServiceKey) that).getDictionary() == m_dictionary) + : false; + } + + @Override + public int hashCode() + { + return System.identityHashCode(m_dictionary); + } + + @Override + public String toString() + { + return Dictionary.class.getName() + "@" + System.identityHashCode(m_dictionary); + } + } + + /** + * Sole constructor. + * @param b the bundle containing the Service annotated with the factory attribute + * @param impl The Service implementation class + * @param serviceProperties The Service properties + * @param provides The Services provided by this Service + * @param factoryConfigure The configure callback invoked in order to pass configurations added in this Set. + */ + public FactorySet(Bundle b, MetaData srvMeta, List depsMeta) + { + m_serviceProperties = srvMeta.getDictionary(Params.properties, null); + m_provide = srvMeta.getStrings(Params.provides, null); + m_configure = srvMeta.getString(Params.factoryConfigure, null); + m_bundle = b; + m_srvMeta = srvMeta; + m_depsMeta = depsMeta; + } + + /** + * Our Service is starting. + */ + public void start(Component c) + { + m_active = true; + m_dm = c.getDependencyManager(); + } + + /** + * Our Service is stopping: we have to remove all Service instances that we have created. + */ + public void stop() + { + try + { + clear(); + } + finally + { + m_active = false; + } + } + + /** + * Create or Update a Service. + */ + @Override + @SuppressWarnings({ "synthetic-access" }) + public boolean add(final Dictionary configuration) + { + // Check parameter validity + if (configuration == null) + { + throw new NullPointerException("configuration parameter can't be null"); + } + + // Check if our service is running. + checkServiceAvailable(); + + // Check if service is being created + ServiceKey serviceKey = new ServiceKey(configuration); + boolean creating = m_services.putIfAbsent(serviceKey, SERVICE_CREATING) == null; + + // Create or Update the Service. + m_serialExecutor.enqueue(new Runnable() + { + public void run() + { + doAdd(configuration); + } + }); + + m_serialExecutor.execute(); + return creating; + } + + /** + * Another Service wants to remove an existing Service. + * This method is not synchronized but uses a SerialExecutor for serializing concurrent method call. + * (This avoid potential dead locks, especially when Service callback methods are invoked). + */ + @Override + @SuppressWarnings("synthetic-access") + public boolean remove(final Object configuration) + { + // Check parameter validity. + if (configuration == null) + { + throw new NullPointerException("configuration parameter can't be null"); + } + if (!(configuration instanceof Dictionary)) + { + throw new IllegalArgumentException("configuration must be an instance of a Dictionary"); + } + + // Check if our service is active. + checkServiceAvailable(); + + // Check if service is created (or creating) + boolean found = m_services.containsKey(new ServiceKey((Dictionary) configuration)); + if (found) + { + // Create or Update the Service. + m_serialExecutor.enqueue(new Runnable() + { + public void run() + { + doRemove((Dictionary) configuration); + } + }); + m_serialExecutor.execute(); + } + return found; + } + + /** + * Another Service wants to remove all existing Services. + * This method is not synchronized but uses a SerialExecutor for serializing concurrent method call. + * (This avoid potential dead locks, especially when Service callback methods are invoked). + */ + @Override + @SuppressWarnings("synthetic-access") + public void clear() + { + if (!m_active) + { + return; + } + + // Make sure add/update/clear events are handled in FIFO order (serially). + m_serialExecutor.enqueue(new Runnable() + { + public void run() + { + doClear(); + } + }); + m_serialExecutor.execute(); + } + + /** + * returns the list of active Service instances configurations. + */ + @Override + public Iterator iterator() + { + throw new UnsupportedOperationException( + "iterator method is not supported by DependencyManager Set's service factories"); + } + + @Override + public String toString() + { + return FactorySet.class.getName() + "(" + m_services.size() + " active instances)"; + } + + /** + * Returns the number of active Service instances. + */ + @Override + public int size() + { + if (!m_active) + { + return 0; + } + return m_services.size(); + } + + /** + * Checks if our Service is available (we are not stopped"). + */ + private void checkServiceAvailable() + { + if (!m_active) + { + throw new IllegalStateException("Service not available"); + } + } + + /** + * Add or create a new Service instance, given its configuration. This method is invoked by the + * SerialExecutor, hence it's thread safe and we'll invoke Service's callbacks without being + * synchronized (hence this will avoid potential dead locks). + */ + private void doAdd(Dictionary configuration) + { + // Check if the service exists. + ServiceKey serviceKey = new ServiceKey(configuration); + Object service = m_services.get(serviceKey); + + if (service == null || service == SERVICE_CREATING) + { + try + { + // Create the Service / impl, unless it is explicitly provided from the + // configuration (using the specific key "dm.factory.instance"). + Component s = m_dm.createComponent(); + Class implClass = m_bundle.loadClass(m_srvMeta.getString(Params.impl)); + Object impl = configuration.get(DM_FACTORY_INSTANCE); + if (impl == null) + { + String factoryMethod = m_srvMeta.getString(Params.factoryMethod, null); + if (factoryMethod == null) + { + m_impl = implClass.newInstance(); + } + else + { + Method m = implClass.getDeclaredMethod(factoryMethod); + m.setAccessible(true); + m_impl = m.invoke(null); + } + } + else + { + m_impl = impl; + } + + // Invoke "configure" callback + if (m_configure != null) + { + invokeConfigure(m_impl, m_configure, configuration); + } + + // Create Service + s.setImplementation(m_impl); + if (m_provide != null) + { + // Merge service properties with the configuration provided by the factory. + Dictionary serviceProperties = mergeSettings(m_serviceProperties, configuration); + s.setInterface(m_provide, serviceProperties); + } + + s.setComposition(m_srvMeta.getString(Params.composition, null)); + ServiceLifecycleHandler lfcleHandler = new ServiceLifecycleHandler(s, m_bundle, m_dm, + m_srvMeta, m_depsMeta); + // The dependencies will be plugged by our lifecycle handler. + s.setCallbacks(lfcleHandler, "init", "start", "stop", "destroy"); + + // Adds dependencies (except named dependencies, which are managed by the lifecycle handler). + for (MetaData dependency: m_depsMeta) + { + String name = dependency.getString(Params.name, null); + if (name == null) + { + DependencyBuilder depBuilder = new DependencyBuilder(dependency); + Log.instance().info("ServiceLifecycleHandler.init: adding dependency %s into service %s", + dependency, m_srvMeta); + Dependency d = depBuilder.build(m_bundle, m_dm); + s.add(d); + } + } + + // Register the Service instance, and keep track of it. + Log.instance().info("ServiceFactory: created service %s", m_srvMeta); + m_dm.add(s); + m_services.put(serviceKey, s); + } + catch (Throwable t) + { + // Make sure the SERVICE_CREATING flag is also removed + m_services.remove(serviceKey); + Log.instance().error("ServiceFactory: could not instantiate service %s", + t, m_srvMeta); + } + } + else + { + // Reconfigure an already existing Service. + if (m_configure != null) + { + Log.instance().info("ServiceFactory: updating service %s", m_impl); + invokeConfigure(m_impl, m_configure, configuration); + } + + // Update service properties + if (m_provide != null) + { + Dictionary settings = mergeSettings(m_serviceProperties, configuration); + ((Component) service).setServiceProperties(settings); + } + } + } + + private void doRemove(Dictionary configuraton) + { + Log.instance().info("ServiceFactory: removing service %s", m_srvMeta); + ServiceKey serviceKey = new ServiceKey(configuraton); + Object service = m_services.remove(serviceKey); + if (service != null && service != SERVICE_CREATING) + { + m_dm.remove((Component) service); + } + } + + private void doClear() + { + for (Object service : m_services.values()) + { + if (service instanceof Component) + { + m_dm.remove((Component) service); + } + } + m_services.clear(); + } + + /** + * Merge factory configuration settings with the service properties. The private factory configuration + * settings are ignored. A factory configuration property is private if its name starts with a dot ("."). + * + * @param serviceProperties + * @param factoryConfiguration + * @return + */ + private Dictionary mergeSettings(Dictionary serviceProperties, Dictionary factoryConfiguration) + { + Dictionary props = new Hashtable(); + + if (serviceProperties != null) + { + Enumeration keys = serviceProperties.keys(); + while (keys.hasMoreElements()) + { + Object key = keys.nextElement(); + Object val = serviceProperties.get(key); + props.put(key, val); + } + } + + Enumeration keys = factoryConfiguration.keys(); + while (keys.hasMoreElements()) + { + Object key = keys.nextElement(); + if (!key.toString().startsWith(".")) + { + // public properties are propagated + Object val = factoryConfiguration.get(key); + props.put(key, val); + } + } + return props; + } + + /** + * Invokes the configure callback method on the service instance implemenatation. + * @param impl + * @param configure + * @param config + */ + private void invokeConfigure(Object impl, String configure, Dictionary config) + { + try + { + InvocationUtil.invokeCallbackMethod(impl, configure, + new Class[][] { { Dictionary.class } }, + new Object[][] { { config } }); + } + + catch (Throwable t) + { + if (t instanceof RuntimeException) + { + throw (RuntimeException) t; + } + else + { + throw new RuntimeException("Could not invoke method " + configure + + " on object " + impl, t); + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/InvocationUtil.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/InvocationUtil.java new file mode 100644 index 00000000000..fb4456ba092 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/InvocationUtil.java @@ -0,0 +1,111 @@ +/* + * 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.dm.runtime; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.security.AccessController; +import java.security.PrivilegedAction; + +/** + * Java reflexion utility methods. + * + * @author Felix Project Team + */ +public class InvocationUtil +{ + public static Object invokeCallbackMethod(Object instance, + String methodName, + Class[][] signatures, + Object[][] parameters) + throws NoSuchMethodException, IllegalArgumentException, IllegalAccessException, + InvocationTargetException + { + Class currentClazz = instance.getClass(); + while (currentClazz != null) + { + try + { + return invokeMethod(instance, currentClazz, methodName, signatures, parameters, false); + } + catch (NoSuchMethodException nsme) + { + // ignore + } + currentClazz = currentClazz.getSuperclass(); + } + throw new NoSuchMethodException(methodName); + } + + public static Object invokeMethod(Object object, + Class clazz, + String name, + Class[][] signatures, + Object[][] parameters, + boolean isSuper) + throws NoSuchMethodException, InvocationTargetException, IllegalArgumentException, + IllegalAccessException + { + if (object == null) + { + throw new IllegalArgumentException("Instance cannot be null"); + } + if (clazz == null) + { + throw new IllegalArgumentException("Class cannot be null"); + } + + // If we're talking to a proxy here, dig one level deeper to expose the + // underlying invocation handler ... + + if (Proxy.isProxyClass(clazz)) + { + object = Proxy.getInvocationHandler(object); + clazz = object.getClass(); + } + + for (int i = 0; i < signatures.length; i++) + { + Class[] signature = signatures[i]; + try + { + final Method m = clazz.getDeclaredMethod(name, signature); + if (!(isSuper && Modifier.isPrivate(m.getModifiers()))) + { + AccessController.doPrivileged(new PrivilegedAction() + { + public Object run() + { + m.setAccessible(true); + return null; + } + }); + return m.invoke(object, parameters[i]); + } + } + catch (NoSuchMethodException e) + { + // ignore this and keep looking + } + } + throw new NoSuchMethodException(name); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/JSONMetaData.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/JSONMetaData.java new file mode 100644 index 00000000000..b202f71ee8c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/JSONMetaData.java @@ -0,0 +1,471 @@ +/* + * 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.dm.runtime; + +import java.lang.reflect.Array; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +/** + * This class represents the parsed data found from meta-inf dependencymanager descriptors. + * + * @author Felix Project Team + */ +public class JSONMetaData implements MetaData, Cloneable +{ + /** + * The parsed Dependency or Service metadata. The map value is either a String, a String[], + * or a Dictionary, whose values are String or String[]. + */ + private HashMap m_metadata = new HashMap(); + + /** + * Decodes Json metadata for either a Service or a Dependency descriptor entry. + * The JSON object has the following form: + * + * entry ::= String | String[] | dictionary + * dictionary ::= key-value-pair* + * key-value-pair ::= key value + * value ::= String | String[] | value-type + * value-type ::= jsonObject with value-type-info + * value-type-info ::= "type"=primitive java type + * "value"=String|String[] + * + * Exemple: + * + * {"string-param" : "string-value", + * "string-array-param" : ["string1", "string2"], + * "properties" : { + * "string-param" : "string-value", + * "string-array-param" : ["str1", "str2], + * "long-param" : {"type":"java.lang.Long", "value":"1"}} + * "long-array-param" : {"type":"java.lang.Long", "value":["1"]}} + * } + * } + * + * @param input the JSON string that corresponds to a dependency manager descriptor entry line. + * @throws Exception + */ + @SuppressWarnings("unchecked") + public JSONMetaData(String input) throws Exception + { + JsonReader jreader = new JsonReader(input); + Map m = jreader.getParsed(); + + for (Map.Entry e : m.entrySet()) + { + String key = e.getKey(); + Object value = e.getValue(); + if (value instanceof String) + { + m_metadata.put(key, value); + } else if (value instanceof List) + { + // String array + m_metadata.put(key, decodeStringArray((List) value)); + } else if (value instanceof Map) + { + // properties + m_metadata.put(key, parseProperties((Map) value)); + } + } + } + + @SuppressWarnings("unchecked") + private Hashtable parseProperties(Map properties) throws Exception { + Hashtable parsedProps = new Hashtable(); + + for (Map.Entry entry : properties.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (value instanceof String) + { + // This property type is a simple string + parsedProps.put(key, value); + } + else if (value instanceof List) + { + // This property type is a simple string array + parsedProps.put(key, decodeStringArray((List) value)); + } + else if (value instanceof Map) + { + // This property type is a typed value, encoded as a JSONObject with two keys: "type"/"value" + Map json = ((Map) value); + String type = json.get("type").toString(); + Object typeValue = json.get("value"); + + if (type == null) + { + throw new Exception("missing type attribute in json metadata for key " + key); + } + if (typeValue == null) + { + throw new Exception("missing type value attribute in json metadata for key " + key); + } + + Class typeClass; + try + { + typeClass = Class.forName(type); + } + catch (ClassNotFoundException e) + { + throw new Exception("invalid type attribute (" + type + ") in json metadata for key " + + key); + } + + if (typeValue instanceof List) + { + parsedProps.put(key, toPrimitiveTypeArray(typeClass, (List) typeValue)); + } + else + { + parsedProps.put(key, toPrimitiveType(typeClass, typeValue.toString())); + } + } + } + + return parsedProps; + } + + private Object toPrimitiveType(Class type, String value) throws Exception { + if (type.equals(String.class)) + { + return value; + } + else if (type.equals(Long.class)) + { + return Long.parseLong(value); + } + else if (type.equals(Double.class)) + { + return Double.valueOf(value); + } + else if (type.equals(Float.class)) + { + return Float.valueOf(value); + } + else if (type.equals(Integer.class)) + { + return Integer.valueOf(value); + } + else if (type.equals(Byte.class)) + { + return Byte.valueOf(value); + } + else if (type.equals(Character.class)) + { + return Character.valueOf((char) Integer.parseInt(value)); + } + else if (type.equals(Boolean.class)) + { + return Boolean.valueOf(value); + } + else if (type.equals(Short.class)) + { + return Short.valueOf(value); + } + else + { + throw new Exception("invalid type (" + type + ") attribute in json metadata"); + } + } + + private Object toPrimitiveTypeArray(Class type, List array) throws Exception { + int len = array.size(); + Object result = Array.newInstance(type, len); + + if (type.equals(String.class)) + { + for (int i = 0; i < len; i ++) { + Array.set(result, i, array.get(i).toString()); + } + } + else if (type.equals(Long.class)) + { + for (int i = 0; i < len; i ++) { + Array.set(result, i, Long.valueOf(array.get(i).toString())); + } + } + else if (type.equals(Double.class)) + { + for (int i = 0; i < len; i ++) { + Array.set(result, i, Double.valueOf(array.get(i).toString())); + } + } + else if (type.equals(Float.class)) + { + for (int i = 0; i < len; i ++) { + Array.set(result, i, Float.valueOf(array.get(i).toString())); + } + } + else if (type.equals(Integer.class)) + { + for (int i = 0; i < len; i ++) { + Array.set(result, i, Integer.valueOf(array.get(i).toString())); + } + } + else if (type.equals(Byte.class)) + { + for (int i = 0; i < len; i ++) { + Array.set(result, i, Byte.valueOf(array.get(i).toString())); + } + } + else if (type.equals(Character.class)) + { + for (int i = 0; i < len; i ++) { + Array.set(result, i, Character.valueOf((char) Integer.parseInt(array.get(i).toString()))); + } + } + else if (type.equals(Boolean.class)) + { + for (int i = 0; i < len; i ++) { + Array.set(result, i, Boolean.valueOf(array.get(i).toString())); + } + } + else if (type.equals(Short.class)) + { + for (int i = 0; i < len; i ++) { + Array.set(result, i, Short.valueOf(array.get(i).toString())); + } + } + else + { + throw new Exception("invalid type (" + type + ") attribute in json metadata"); + } + return result; + } + + /** + * Close this class instance to another one. + */ + @SuppressWarnings("unchecked") + @Override + public Object clone() throws CloneNotSupportedException + { + JSONMetaData clone = (JSONMetaData) super.clone(); + clone.m_metadata = (HashMap) m_metadata.clone(); + return clone; + } + + public String getString(Params key) + { + Object value = m_metadata.get(key.toString()); + if (value == null) + { + throw new IllegalArgumentException("Parameter " + key + " not found"); + } + return value.toString(); + } + + public String getString(Params key, String def) + { + try + { + return getString(key); + } + catch (IllegalArgumentException e) + { + return def; + } + } + + public int getInt(Params key) + { + Object value = m_metadata.get(key.toString()); + if (value != null) + { + try + { + if (value instanceof Integer) { + return ((Integer) value).intValue(); + } + return Integer.parseInt(value.toString()); + } + catch (NumberFormatException e) + { + throw new IllegalArgumentException("parameter " + key + + " is not an int value: " + + value); + } + } + else + { + throw new IllegalArgumentException("missing " + key + + " parameter from annotation"); + } + } + + public int getInt(Params key, int def) + { + Object value = m_metadata.get(key.toString()); + if (value != null) + { + try + { + if (value instanceof Integer) { + return ((Integer) value).intValue(); + } + return Integer.parseInt(value.toString()); + } + catch (NumberFormatException e) + { + throw new IllegalArgumentException("parameter " + key + + " is not an int value: " + + value); + } + } + else + { + return def; + } + } + + public long getLong(Params key) + { + Object value = m_metadata.get(key.toString()); + if (value != null) + { + try + { + if (value instanceof Long) { + return ((Long) value).longValue(); + } + return Long.parseLong(value.toString()); + } + catch (NumberFormatException e) + { + throw new IllegalArgumentException("parameter " + key + + " is not a long value: " + + value); + } + } + else + { + throw new IllegalArgumentException("missing " + key + + " parameter from annotation"); + } + } + + public long getLong(Params key, long def) + { + Object value = m_metadata.get(key.toString()); + if (value != null) + { + try + { + if (value instanceof Long) { + return (Long) value; + } + return Long.parseLong(value.toString()); + } + catch (NumberFormatException e) + { + throw new IllegalArgumentException("parameter " + key + + " is not a long value: " + + value); + } + } + else + { + return def; + } + } + + public String[] getStrings(Params key) + { + Object array = m_metadata.get(key.toString()); + if (array == null) + { + throw new IllegalArgumentException("Parameter " + key + " not found"); + } + + if (!(array instanceof String[])) + { + throw new IllegalArgumentException("Parameter " + key + " is not a String[] (" + array.getClass() + + ")"); + } + return (String[]) array; + } + + public String[] getStrings(Params key, String[] def) + { + try + { + return getStrings(key); + } + catch (IllegalArgumentException t) + { + return def; + } + } + + @SuppressWarnings("unchecked") + public Dictionary getDictionary(Params key, + Dictionary def) + { + Object dictionary = m_metadata.get(key.toString()); + if (dictionary == null) + { + return def; + } + + if (!(dictionary instanceof Dictionary)) + { + throw new IllegalArgumentException("Parameter " + key + " is not a Dictionary (" + + dictionary.getClass() + ")"); + } + + return (Dictionary) dictionary; + } + + @Override + public String toString() + { + return m_metadata.toString(); + } + + public void setDictionary(Params key, Dictionary dictionary) + { + m_metadata.put(key.toString(), dictionary); + } + + public void setString(Params key, String value) + { + m_metadata.put(key.toString(), value); + } + + public void setStrings(Params key, String[] values) + { + m_metadata.put(key.toString(), values); + } + + /** + * Decodes a JSONArray into a String array (all JSON array values are supposed to be strings). + */ + private String[] decodeStringArray(List array) + { + return array.stream().map(x -> x.toString()).toArray(String[]::new); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/JsonReader.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/JsonReader.java new file mode 100644 index 00000000000..be047ca49a6 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/JsonReader.java @@ -0,0 +1,290 @@ +/* + * 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.dm.runtime; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A very small JSON parser. + * Code adapted from the org.apache.felix.serializer.impl.json.JsonParser.java, + * from the Apache Felix Converter project. + * + * The JSON input is parsed into an object structure in the following way: + *
        + *
      • Object names are represented as a {@link String}. + *
      • String values are represented as a {@link String}. + *
      • Numeric values without a decimal separator are represented as a {@link Long}. + *
      • Numeric values with a decimal separator are represented as a {@link Double}. + *
      • Boolean values are represented as a {@link Boolean}. + *
      • Nested JSON objects are parsed into a {@link java.util.Map Map<String, Object>}. + *
      • JSON lists are parsed into a {@link java.util.List} which may contain any of the above values. + *
      + */ +public class JsonReader { + private static final Pattern KEY_VALUE_PATTERN = Pattern.compile("^\\s*[\"](.+?)[\"]\\s*[:]\\s*(.+)$"); + + private enum Scope { QUOTE, CURLY, BRACKET; + static Scope getScope(char c) { + switch (c) { + case '"': + return QUOTE; + case '[': + case ']': + return BRACKET; + case '{': + case '}': + return CURLY; + default: + return null; + } + } + } + + static class Pair { + final K key; + final V value; + + Pair(K k, V v) { + key = k; + value = v; + } + } + + private final Map parsed; + + public JsonReader(CharSequence json) { + String str = json.toString(); + str = str.trim().replace('\n', ' '); + parsed = parseObject(str); + } + + public JsonReader(InputStream is) throws IOException { + this(readStreamAsString(is)); + } + + public Map getParsed() { + return parsed; + } + + private static Pair parseKeyValue(String jsonKeyValue) { + Matcher matcher = KEY_VALUE_PATTERN.matcher(jsonKeyValue); + if (!matcher.matches() || matcher.groupCount() < 2) { + throw new IllegalArgumentException("Malformatted JSON key-value pair: " + jsonKeyValue); + } + + return new Pair<>(matcher.group(1), parseValue(matcher.group(2))); + } + + private static Object parseValue(String jsonValue) { + jsonValue = jsonValue.trim(); + + switch (jsonValue.charAt(0)) { + case '\"': + if (!jsonValue.endsWith("\"")) + throw new IllegalArgumentException("Malformatted JSON string: " + jsonValue); + + return jsonValue.substring(1, jsonValue.length() - 1); + case '[': + List entries = new ArrayList<>(); + for (String v : parseListValuesRaw(jsonValue)) { + entries.add(parseValue(v)); + } + return entries; + case '{': + return parseObject(jsonValue); + case 't': + case 'T': + case 'f': + case 'F': + return Boolean.parseBoolean(jsonValue); + case 'n': + case 'N': + return null; + default: + if (jsonValue.contains(".")) { + return Double.parseDouble(jsonValue); + } + return Long.parseLong(jsonValue); + } + } + + private static Map parseObject(String jsonObject) { + if (!(jsonObject.startsWith("{") && jsonObject.endsWith("}"))) + throw new IllegalArgumentException("Malformatted JSON object: " + jsonObject); + + Map values = new HashMap<>(); + + jsonObject = jsonObject.substring(1, jsonObject.length() - 1).trim(); + if (jsonObject.length() == 0) + return values; + + for (String element : parseKeyValueListRaw(jsonObject)) { + Pair pair = parseKeyValue(element); + values.put(pair.key, pair.value); + } + + return values; + } + + private static List parseKeyValueListRaw(String jsonKeyValueList) { + if (jsonKeyValueList.trim().isEmpty()) + return Collections.emptyList(); + jsonKeyValueList = jsonKeyValueList + ","; // append comma to simplify parsing + List elements = new ArrayList<>(); + + int i=0; + int start=0; + Stack scopeStack = new Stack<>(); + while (i < jsonKeyValueList.length()) { + char curChar = jsonKeyValueList.charAt(i); + switch (curChar) { + case '"': + if (i > 0 && jsonKeyValueList.charAt(i-1) == '\\') { + // it's escaped, ignore for now + } else { + if (!scopeStack.empty() && scopeStack.peek() == Scope.QUOTE) { + scopeStack.pop(); + } else { + scopeStack.push(Scope.QUOTE); + } + } + break; + case '[': + case '{': + if ((scopeStack.empty() ? null : scopeStack.peek()) == Scope.QUOTE) { + // inside quotes, ignore + } else { + scopeStack.push(Scope.getScope(curChar)); + } + break; + case ']': + case '}': + Scope curScope = scopeStack.empty() ? null : scopeStack.peek(); + if (curScope == Scope.QUOTE) { + // inside quotes, ignore + } else { + Scope newScope = Scope.getScope(curChar); + if (curScope == newScope) { + scopeStack.pop(); + } else { + throw new IllegalArgumentException("Unbalanced closing " + + curChar + " in: " + jsonKeyValueList); + } + } + break; + case ',': + if (scopeStack.empty()) { + elements.add(jsonKeyValueList.substring(start, i)); + start = i+1; + } + break; + } + + i++; + } + return elements; + } + + private static List parseListValuesRaw(String jsonList) { + if (!(jsonList.startsWith("[") && jsonList.endsWith("]"))) + throw new IllegalArgumentException("Malformatted JSON list: " + jsonList); + + jsonList = jsonList.substring(1, jsonList.length() - 1); + return parseKeyValueListRaw(jsonList); + } + + private static String readStreamAsString(InputStream is) throws IOException { + byte [] bytes = readStream(is); + if (bytes.length < 5) + // need at least 5 bytes to establish the encoding + throw new IllegalArgumentException("Malformatted JSON"); + + int offset = 0; + if ((bytes[0] == -1 && bytes[1] == -2) + || (bytes[0] == -2 && bytes[1] == -1)) { + // Skip UTF16/UTF32 Byte Order Mark (BOM) + offset = 2; + } + + /* Infer the encoding as described in section 3 of http://www.ietf.org/rfc/rfc4627.txt + * which reads: + * Encoding + * + * JSON text SHALL be encoded in Unicode. The default encoding is + * UTF-8. + * + * Since the first two characters of a JSON text will always be ASCII + * characters [RFC0020], it is possible to determine whether an octet + * stream is UTF-8, UTF-16 (BE or LE), or UTF-32 (BE or LE) by looking + * at the pattern of nulls in the first four octets. + * + * 00 00 00 xx UTF-32BE + * 00 xx 00 xx UTF-16BE + * xx 00 00 00 UTF-32LE + * xx 00 xx 00 UTF-16LE + * xx xx xx xx UTF-8 + */ + String encoding; + if (bytes[offset + 2] == 0) { + if (bytes[offset + 1] != 0) { + encoding = "UTF-16"; + } else { + encoding = "UTF-32"; + } + } else if (bytes[offset + 1] == 0) { + encoding = "UTF-16"; + } else { + encoding = "UTF-8"; + } + return new String(bytes, encoding); + } + + public static byte [] readStream(InputStream is) throws IOException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] bytes = new byte[8192]; + + int length = 0; + int offset = 0; + + while ((length = is.read(bytes, offset, bytes.length - offset)) != -1) { + offset += length; + + if (offset == bytes.length) { + baos.write(bytes, 0, bytes.length); + offset = 0; + } + } + if (offset != 0) { + baos.write(bytes, 0, offset); + } + return baos.toByteArray(); + } finally { + is.close(); + } + } +} \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/Log.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/Log.java new file mode 100644 index 00000000000..c41b956a64a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/Log.java @@ -0,0 +1,92 @@ +/* + * 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.dm.runtime; + +import org.osgi.service.log.LogService; + +/** + * This class logs some formattable strings into the OSGi Log Service. + * + * @author Felix Project Team + */ +public class Log +{ + /** The wrap log service which is actually used (Injected by Activator) */ + private volatile LogService m_logService; + + /** Log INFO/DEBUG only if logs are enabled, else only log ERROR/WARN messages */ + private volatile boolean m_logEnabled; + + /** Our sole instance */ + private static Log m_instance = new Log(); + + public static Log instance() + { + return m_instance; + } + + public void enableLogs(boolean logEnabled) + { + m_logEnabled = logEnabled; + } + + public void error(String format, Object ... args) + { + m_logService.log(LogService.LOG_ERROR, String.format(format, args)); + } + + public void error(String format, Throwable t, Object ... args) + { + m_logService.log(LogService.LOG_ERROR, String.format(format, args), t); + } + + public void warn(String format, Object ... args) + { + m_logService.log(LogService.LOG_WARNING, String.format(format, args)); + } + + public void warn(String format, Throwable t, Object ... args) + { + m_logService.log(LogService.LOG_WARNING, String.format(format, args), t); + } + + public void info(String format, Object ... args) + { + if (m_logEnabled) + m_logService.log(LogService.LOG_INFO, String.format(format, args)); + } + + public void info(String format, Throwable t, Object ... args) + { + if (m_logEnabled) + m_logService.log(LogService.LOG_INFO, String.format(format, args), t); + } + + public void debug(String format, Object ... args) + { + if (m_logEnabled) + m_logService.log(LogService.LOG_DEBUG, String.format(format, args)); + } + + public void debug(String format, Throwable t, Object ... args) + { + if (m_logEnabled) + m_logService.log(LogService.LOG_DEBUG, String.format(format, args), t); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/MetaData.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/MetaData.java new file mode 100644 index 00000000000..435f95836cb --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/MetaData.java @@ -0,0 +1,94 @@ +/* + * 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.dm.runtime; + +import java.util.Dictionary; + +/** + * This class represents the meta data parsed from a descriptor entry (json) line. + * + * @author Felix Project Team + */ +public interface MetaData extends Cloneable +{ + /** + * Returns a String descriptor entry parameter value. + */ + String getString(Params key); + + /** + * Returns a String descriptor entry parameter value. + */ + String getString(Params key, String def); + + /** + * Returns a String descriptor entry parameter value. + */ + int getInt(Params key); + + /** + * Returns a String descriptor entry parameter value. + */ + int getInt(Params key, int def); + + /** + * Returns a String descriptor entry parameter value. + */ + long getLong(Params key); + + /** + * Returns a String descriptor entry parameter value. + */ + long getLong(Params key, long def); + + /** + * Returns a String array descriptor entry parameter value. + */ + String[] getStrings(Params key); + + /** + * Returns a String array descriptor entry parameter value. + */ + String[] getStrings(Params key, String[] def); + + /** + * Returns a descriptor entry value which is a complex value. + */ + Dictionary getDictionary(Params key, Dictionary def); + + /** + * Modifies a key Sring value + */ + void setString(Params key, String value); + + /** + * Modifies a String[] value. + */ + void setStrings(Params key, String[] values); + + /** + * Modifies a String[] value. + */ + void setDictionary(Params key, Dictionary dictionary); + + /** + * Clone this MetaData object. + */ + Object clone() throws CloneNotSupportedException; +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/Params.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/Params.java new file mode 100644 index 00000000000..115068e0ae7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/Params.java @@ -0,0 +1,74 @@ +/* + * 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.dm.runtime; + +/** + * List of descriptor parameters. + * + * @author Felix Project Team + */ +public enum Params +{ + type, + init, + start, + stop, + destroy, + impl, + provides, + properties, + composition, + service, + filter, + defaultImpl, + required, + added, + changed, + removed, + swap, + autoConfig, + pid, + propagate, + updated, + timeout, + adapteeService, + adapteeFilter, + stateMask, + ranking, + factoryPid, + factorySet, + factoryName, + factoryConfigure, + factoryMethod, + name, + field, + starter, + stopper, + bundleContextField, + dependencyManagerField, + componentField, + registrationField, + registered, + unregistered, + configType, + configTypes, + dereference, + scope, + bundle +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/ResourceAdapterServiceBuilder.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/ResourceAdapterServiceBuilder.java new file mode 100644 index 00000000000..71743960966 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/ResourceAdapterServiceBuilder.java @@ -0,0 +1,74 @@ +/* + * 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.dm.runtime; + +import java.util.Dictionary; +import java.util.List; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.Bundle; + +/** + * Class used to build a resource adapter service using metadata found from DependencyManager runtime + * meta-inf descriptor. + * + * @author Felix Project Team + */ +public class ResourceAdapterServiceBuilder extends AbstractBuilder +{ + private final static String TYPE = "ResourceAdapterService"; + + @Override + public String getType() + { + return TYPE; + } + + @Override + public void build(MetaData srvMeta, List depsMeta, Bundle b, DependencyManager dm) + throws Exception + { + String filter = srvMeta.getString(Params.filter, null); + Class implClass = b.loadClass(srvMeta.getString(Params.impl)); + String[] provides = srvMeta.getStrings(Params.provides, null); + Dictionary properties = srvMeta.getDictionary(Params.properties, null); + boolean propagate = "true".equals(srvMeta.getString(Params.propagate, "false")); + String changed = srvMeta.getString(Params.changed, null /* no change callback if not specified explicitly */); + Component c = dm.createResourceAdapterService(filter, propagate, null, changed); + c.setInterface(provides, properties); + String factoryMethod = srvMeta.getString(Params.factoryMethod, null); + if (factoryMethod == null) + { + c.setImplementation(implClass); + } + else + { + c.setFactory(implClass, factoryMethod); + } + setCommonServiceParams(c, srvMeta); + c.setComposition(srvMeta.getString(Params.composition, null)); + ServiceLifecycleHandler lfcleHandler = new ServiceLifecycleHandler(c, b, dm, srvMeta, depsMeta); + // The dependencies will be plugged by our lifecycle handler. + c.setCallbacks(lfcleHandler, "init", "start", "stop", "destroy"); + // Adds dependencies (except named dependencies, which are managed by the lifecycle handler). + addUnamedDependencies(b, dm, c, srvMeta, depsMeta); + dm.add(c); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/SerialExecutor.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/SerialExecutor.java new file mode 100644 index 00000000000..b7dae8fec83 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/SerialExecutor.java @@ -0,0 +1,92 @@ +/* + * 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.dm.runtime; + +import java.util.LinkedList; +import java.util.NoSuchElementException; + +/** + * Allows you to enqueue tasks from multiple threads and then execute + * them on one thread sequentially. It assumes more than one thread will + * try to execute the tasks and it will make an effort to pick the first + * task that comes along whilst making sure subsequent tasks return + * without waiting. + * + * @author Felix Project Team + */ +public final class SerialExecutor { + private static final Runnable DUMMY_RUNNABLE = new Runnable() { public void run() {} }; + private final LinkedList m_workQueue = new LinkedList<>(); + private Runnable m_active; + + /** + * Enqueue a new task for later execution. This method is + * thread-safe, so multiple threads can contribute tasks. + * + * @param runnable the runnable containing the actual task + */ + public synchronized void enqueue(final Runnable runnable) { + m_workQueue.addLast(new Runnable() { + public void run() { + try { + runnable.run(); + } + finally { + scheduleNext(); + } + } + }); + } + + /** + * Execute any pending tasks. This method is thread safe, + * so multiple threads can try to execute the pending + * tasks, but only the first will be used to actually do + * so. Other threads will return immediately. + */ + public void execute() { + Runnable active; + synchronized (this) { + active = m_active; + // for now just put some non-null value in there so we can never + // get a race condition when two threads enter this section after + // one another (causing sheduleNext() to be invoked twice below) + m_active = DUMMY_RUNNABLE; + } + if (active == null) { + scheduleNext(); + } + } + + private void scheduleNext() { + Runnable active; + synchronized (this) { + try { + m_active = (Runnable) m_workQueue.removeFirst(); + } + catch (NoSuchElementException e) { + m_active = null; + } + active = m_active; + } + if (active != null) { + active.run(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/ServiceLifecycleHandler.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/ServiceLifecycleHandler.java new file mode 100644 index 00000000000..48ddca059f4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/ServiceLifecycleHandler.java @@ -0,0 +1,453 @@ +/* + * 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.dm.runtime; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.Bundle; + +/** + * Caution: this class *MUST* be immutable, because it may be shared between Aspects/Adapters + * and concrete Aspect/Adapter instance. + * + * This class allows Services to configure dynamically their dependency filters from their init() method. + * Basically, this class acts as a service implementation lifecycle handler. When we detect that the Service is + * called in its init() method, and if init() returns a Map, then the Map is assumed to contain + * dependency filters, which will be applied to all named dependencies. The Map optionally returned by + * Service's init method may contain the following keys: + *
        + *
      • name.filter: the value must be a valid OSGi filter, and the "name" prefix must match a ServiceDependency + * name attribute
      • + *
      • name.required: the value is a boolean ("true"|"false") and the "name" prefix must match a + * ServiceDependency name attribute
      • + *
      + * + *

      Dependencies which provide a name attribute will be activated after the init method returns. Other + * dependencies are injected before the init method. + * + *

      Example of a Service whose dependency filter is configured from ConfigAdmin: + * + *

      + *  /**
      + *    * A Service whose service dependency filter/require attribute may be configured from ConfigAdmin
      + *    */
      + *  @Service
      + *  class X {
      + *      private Dictionary m_config;
      + *      
      + *      @ConfigurationDependency(pid="MyPid")
      + *      void configure(Dictionary conf) {
      + *           // Initialize our service from config ...
      + *           
      + *           // And store the config for later usage (from our init method)
      + *           m_config = config;
      + *      }
      + *      
      + *      @ServiceDependency(name="dependency1") 
      + *      void bindOtherService(OtherService other) {
      + *         // the filter and required flag will be configured from our init method.
      + *      }
      + *
      + *      // The returned Map will be used to configure our "dependency1" Dependency.
      + *      @Init
      + *      Map init() {
      + *          return new HashMap() {{
      + *              put("dependency1.filter", m_config.get("filter"));
      + *              put("dependency1.required", m_config.get("required"));
      + *          }};
      + *      } 
      + *  }
      + *  
      + * + * @author Felix Project Team + */ +public class ServiceLifecycleHandler +{ + private final String m_init; + private final String m_start; + private final String m_stop; + private final String m_destroy; + private final MetaData m_srvMeta; + private final List m_depsMeta; + private final Bundle m_bundle; + private final static Object SYNC = new Object(); + + /** + * Makes a new ServiceLifecycleHandler object. This objects allows to decorate the "init" service callback, in + * order to see if "init" callback returns a dependency customization map. + * + * @param srv The Service for the annotated class + * @param srvBundle the Service bundle + * @param dm The DependencyManager that was used to create the service + * @param srvMeta The Service MetaData + * @param depMeta The Dependencies MetaData + */ + public ServiceLifecycleHandler(Component srv, Bundle srvBundle, DependencyManager dm, + MetaData srvMeta, List depMeta) + { + m_srvMeta = srvMeta; + m_init = srvMeta.getString(Params.init, null); + m_start = srvMeta.getString(Params.start, null); + m_stop = srvMeta.getString(Params.stop, null); + m_destroy = srvMeta.getString(Params.destroy, null); + m_bundle = srvBundle; + m_depsMeta = depMeta; + } + + /** + * Handles an "init" lifecycle service callback. We just catch the "init" method, and callback + * the actual Service' init method, to see if a dependency customization map is returned. + * We also check if a Lifecycle Controller is used. In this case, we add a hidden custom dependency, + * allowing to take control of when the component is actually started/stopped. + * We also handle an edge case described in FELIX-4050, where component state calculation + * may mess up if some dependencies are added using the API from the init method. + * + * @param c The Annotated Component + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void init(Component c) + throws Exception + { + Object serviceInstance = c.getInstances()[0]; + DependencyManager dm = c.getDependencyManager(); + + // Check if a lifecycle controller is defined for this service. If true, then + // We'll use the ToggleServiceDependency in order to manually activate/deactivate + // the component ... + String starter = m_srvMeta.getString(Params.starter, null); + String stopper = m_srvMeta.getString(Params.stopper, null); + + if (starter != null) + { + // We'll inject two runnables: one that will start or service, when invoked, and the other + // that will stop our service, when invoked. We'll use a shared atomic boolean in order to + // synchronize both runnables. + Log.instance().debug("Setting up a lifecycle controller for service %s", serviceInstance); + String componentName = serviceInstance.getClass().getName(); + // Create a toggle service, used to start/stop our service. + ToggleServiceDependency toggle = new ToggleServiceDependency(); + AtomicBoolean startFlag = new AtomicBoolean(false); + // Add the toggle dependency to the component. + c.add(toggle); + + // Inject the runnable that will start our service, when invoked. + setField(serviceInstance, starter, Runnable.class, new ComponentStarter(componentName, toggle, startFlag)); + if (stopper != null) { + // Inject the runnable that will stop our service, when invoked. + setField(serviceInstance, stopper, Runnable.class, new ComponentStopper(componentName, toggle, startFlag)); + } + } + + // Before invoking an optional init method, we have to handle an edge case (FELIX-4050), where + // init may add dependencies using the API and also return a map for configuring some + // named dependencies. We have to add a hidden toggle dependency in the component, which we'll + // active *after* the init method is called, and possibly *after* named dependencies are configured. + + ToggleServiceDependency initToggle = null; + if (m_init != null) + { + initToggle = new ToggleServiceDependency(); + c.add(initToggle); + } + + // Invoke component and all composites init methods, and for each one, check if a dependency + // customization map is returned by the method. This map will be used to configure + // some dependency filters (or required flag). + + Map customization = new HashMap(); + Object[] composites = c.getInstances(); + for (Object composite: composites) + { + Object o = invokeMethod(composite, m_init, dm, c); + if (o != null && Map.class.isAssignableFrom(o.getClass())) + { + customization.putAll((Map) o); + } + } + + Log.instance().debug("ServiceLifecycleHandler.init: invoked init method from service %s " + + ", returned map: %s", serviceInstance, customization); + + // Apply name dependency filters possibly returned by the init() method. + + List nameDependencies = new ArrayList<>(); + for (MetaData dependency: m_depsMeta) + { + // Check if this dependency has a name, and if we find the name from the + // customization map, then apply filters and required flag from the map into it. + // Also parse optional pid/propagate flags for named Configuration dependencies + + String name = dependency.getString(Params.name, null); + if (name != null) + { + String filter = customization.get(name + ".filter"); + String required = customization.get(name + ".required"); + String pid = customization.get(name + ".pid"); + String propagate = customization.get(name + ".propagate"); + + if (filter != null || required != null || pid != null || propagate != null) + { + dependency = (MetaData) dependency.clone(); + if (filter != null) + { + dependency.setString(Params.filter, filter); + } + if (required != null) + { + dependency.setString(Params.required, required); + } + if (pid != null) + { + dependency.setString(Params.pid, pid); + } + if (propagate != null) + { + dependency.setString(Params.propagate, propagate); + } + } + + DependencyBuilder depBuilder = new DependencyBuilder(dependency); + Log.instance().info("ServiceLifecycleHandler.init: adding dependency %s into service %s", dependency, m_srvMeta); + nameDependencies.add(depBuilder.build(m_bundle, dm)); + } + } + + // Add all extra dependencies + if (nameDependencies.size() > 0) + { + Log.instance().info("ServiceLifecycleHandler.init: adding extra/named dependencies %s", nameDependencies); + c.add(nameDependencies.toArray(new Dependency[nameDependencies.size()])); + } + + // init method fully handled, and all possible named dependencies have been configured. Now, activate the + // hidden toggle, and then remove it from the component, because we don't need it anymore. + if (initToggle != null) + { + c.remove(initToggle); + } + } + + /** + * Handles the Service's start lifecycle callback. We just invoke the service "start" service callback on + * the service instance, as well as on all eventual service composites. + * We take care to check if a start callback returns a Map, which is meant to contain + * some additional properties which must be appended to existing service properties. + * Such extra properties takes precedence over existing service properties. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void start(Component service) + throws IllegalArgumentException, IllegalAccessException, InvocationTargetException + { + // Check if some extra service properties are returned by start method. + + DependencyManager dm = service.getDependencyManager(); + Map extraProperties = new HashMap(); + Object[] composites = service.getInstances(); + for (Object composite: composites) + { + Object o = invokeMethod(composite, m_start, dm, service); + if (o != null && Map.class.isAssignableFrom(o.getClass())) + { + extraProperties.putAll((Map) o); + } + } + + if (extraProperties.size() > 0) + { + // Store extra properties returned by start callbacks into existing service properties + Dictionary existingProperties = service.getServiceProperties(); + if (existingProperties != null) + { + Hashtable props = new Hashtable(); + Enumeration e = existingProperties.keys(); + while (e.hasMoreElements()) + { + Object key = e.nextElement(); + props.put(key, existingProperties.get(key)); + } + props.putAll(extraProperties); + service.setServiceProperties(props); + } + else + { + service.setServiceProperties(new Hashtable(extraProperties)); + } + } + } + + /** + * Handles the Service's stop lifecycle callback. We just invoke the service "stop" callback on + * the service instance, as well as on all eventual service composites. + */ + public void stop(Component service) + throws IllegalArgumentException, IllegalAccessException, InvocationTargetException + { + callbackComposites(service, m_stop); + } + + /** + * Handles the Service's destroy lifecycle callback. We just invoke the service "destroy" callback on + * the service instance, as well as on all eventual service composites. + */ + public void destroy(Component service) + throws IllegalArgumentException, IllegalAccessException, InvocationTargetException + { + callbackComposites(service, m_destroy); + } + + /** + * Invoke a callback on all Service compositions. + */ + private void callbackComposites(Component service, String callback) + throws IllegalArgumentException, IllegalAccessException, InvocationTargetException + { + Object[] composites = service.getInstances(); + for (Object composite: composites) + { + invokeMethod(composite, callback, service.getDependencyManager(), service); + } + } + + /** + * Invoke a callback on an Object instance. + */ + private Object invokeMethod(Object serviceInstance, String method, DependencyManager dm, Component c) + throws IllegalArgumentException, IllegalAccessException, InvocationTargetException + { + if (method != null) + { + try + { + return InvocationUtil.invokeCallbackMethod(serviceInstance, method, + new Class[][] { { Component.class }, {} }, + new Object[][] { { c }, {} } + ); + } + + catch (NoSuchMethodException e) + { + // ignore this + } + + // Other exception will be thrown up to the ServiceImpl.invokeCallbackMethod(), which is + // currently invoking our method. So, no need to log something here, since the invokeCallbackMethod + // method is already logging any thrown exception. + } + return null; + } + + /** + * Sets a field of an object by reflexion. + */ + private void setField(Object instance, String fieldName, Class fieldClass, Object fieldValue) + { + Object serviceInstance = instance; + Class serviceClazz = serviceInstance.getClass(); + if (Proxy.isProxyClass(serviceClazz)) + { + serviceInstance = Proxy.getInvocationHandler(serviceInstance); + serviceClazz = serviceInstance.getClass(); + } + while (serviceClazz != null) + { + Field[] fields = serviceClazz.getDeclaredFields(); + for (int j = 0; j < fields.length; j++) + { + Field field = fields[j]; + Class type = field.getType(); + if (field.getName().equals(fieldName) && type.isAssignableFrom(fieldClass)) + { + try + { + field.setAccessible(true); + // synchronized makes sure the field is actually written to immediately + synchronized (SYNC) + { + field.set(serviceInstance, fieldValue); + } + } + catch (Throwable e) + { + throw new RuntimeException("Could not set field " + field, e); + } + } + } + serviceClazz = serviceClazz.getSuperclass(); + } + } + + private static class ComponentStarter implements Runnable { + private final String m_componentName; + private final ToggleServiceDependency m_toggle; + private final AtomicBoolean m_startFlag; + + public ComponentStarter(String name, ToggleServiceDependency toggle, AtomicBoolean startFlag) + { + m_componentName = name; + m_toggle = toggle; + m_startFlag = startFlag; + } + + @SuppressWarnings("synthetic-access") + public void run() + { + if (m_startFlag.compareAndSet(false, true)) { + Log.instance().debug("Lifecycle controller is activating the component %s", + m_componentName); + m_toggle.activate(true); + } + } + } + + private static class ComponentStopper implements Runnable { + private final Object m_componentName; + private final ToggleServiceDependency m_toggle; + private final AtomicBoolean m_startFlag; + + public ComponentStopper(String componentName, ToggleServiceDependency toggle, AtomicBoolean startFlag) + { + m_componentName = componentName; + m_toggle = toggle; + m_startFlag = startFlag; + } + + @SuppressWarnings("synthetic-access") + public void run() + { + if (m_startFlag.compareAndSet(true, false)) { + Log.instance().debug("Lifecycle controller is deactivating the component %s", + m_componentName); + m_toggle.activate(false); + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/ToggleServiceDependency.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/ToggleServiceDependency.java new file mode 100644 index 00000000000..43adcd359b7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/ToggleServiceDependency.java @@ -0,0 +1,66 @@ +/* + * 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.dm.runtime; + +import org.apache.felix.dm.context.AbstractDependency; +import org.apache.felix.dm.context.DependencyContext; +import org.apache.felix.dm.context.Event; +import org.apache.felix.dm.context.EventType; + +/** + * This is a custom DependencyManager Dependency, allowing to take control of + * when the dependency is available or not. It's used in the context of the + * LifecycleController class, in order to activate/deactivate a Component on + * demand. + * + * @author Felix Project Team + */ +public class ToggleServiceDependency extends AbstractDependency { + public ToggleServiceDependency() { + super.setRequired(true); + } + + public ToggleServiceDependency(ToggleServiceDependency prototype) { + super(prototype); + } + + @Override + public DependencyContext createCopy() { + return new ToggleServiceDependency(this); + } + + public void activate(boolean active) { + m_component.handleEvent(this, active ? EventType.ADDED : EventType.REMOVED, new Event(active)); + } + + @Override + public String getSimpleName() { + return "" + isAvailable(); + } + + @Override + public String getType() { + return "toggle"; + } + + @Override + public Class getAutoConfigType() { + return null; // we don't support auto config mode. + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/api/ComponentException.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/api/ComponentException.java new file mode 100644 index 00000000000..1a05f6fea38 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/api/ComponentException.java @@ -0,0 +1,32 @@ +/* + * 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.dm.runtime.api; + +/** + * Exception thrown when a Component can't be instantiated using a {@link ComponentFactory#newInstance(java.util.Dictionary)} + * service. + * + * @author Felix Project Team + */ +@SuppressWarnings("serial") +public class ComponentException extends RuntimeException { + public ComponentException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/api/ComponentFactory.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/api/ComponentFactory.java new file mode 100644 index 00000000000..26e42d21f9a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/api/ComponentFactory.java @@ -0,0 +1,39 @@ +/* + * 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.dm.runtime.api; + +import java.util.Dictionary; + +/** + * When a Component is annotated with a DM "Component" annotation with a "factoryName" attribute, a corresponding + * ComponentFactory is registered in the OSGi service registry with a @link {@link ComponentFactory#FACTORY_NAME} + * servie property with the Component "factoryName" value. + * + * @author Felix Project Team + */ +public interface ComponentFactory { + /** + * Instantiates a Component instance. Any properties starts with a "." are considered as private. Other properties will be + * published as the component instance service properties (if the component provides a services). + * @param conf the properties passed to the component "configure" method which is specified with the "configure" attribute + * of the @Component annotation. + * @return the component instance. + */ + ComponentInstance newInstance(Dictionary conf); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/api/ComponentInstance.java b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/api/ComponentInstance.java new file mode 100644 index 00000000000..49ca07113a8 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/api/ComponentInstance.java @@ -0,0 +1,39 @@ +/* + * 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.dm.runtime.api; + +import java.util.Dictionary; + +/** + * A Component instance created using a {@link ComponentFactory} service + * + * @author Felix Project Team + */ +public interface ComponentInstance { + /** + * Destroy the component instance. + */ + void dispose(); + + /** + * Updates the component instance. + * @param conf the properties used to update the component. + */ + void update(Dictionary conf); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/api/packageinfo b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/api/packageinfo new file mode 100644 index 00000000000..a4f15462211 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.runtime/src/org/apache/felix/dm/runtime/api/packageinfo @@ -0,0 +1 @@ +version 1.0 \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.runtime/test/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.runtime/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/.classpath b/dependencymanager/org.apache.felix.dependencymanager.samples/.classpath new file mode 100644 index 00000000000..7a6fc254361 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.samples/.gitignore new file mode 100644 index 00000000000..90dde36e4ac --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/.gitignore @@ -0,0 +1,3 @@ +/bin/ +/bin_test/ +/generated/ diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/.project b/dependencymanager/org.apache.felix.dependencymanager.samples/.project new file mode 100644 index 00000000000..cd3ff99cd2c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/.project @@ -0,0 +1,23 @@ + + + org.apache.felix.dependencymanager.samples + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/README.samples b/dependencymanager/org.apache.felix.dependencymanager.samples/README.samples new file mode 100644 index 00000000000..5f8cf4d8d4e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/README.samples @@ -0,0 +1,35 @@ +/* + * 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. + */ + +This sub-project contains some examples using the Dependency Manager Api and annotations. + +To execute the samples under bndtools, click on the "org.apache.felix.dependencymanager.samples" project, +then Run as "Bnd OSGi Run Launcher". + +Each sample displays some logs using the OSGi log service. +Just type: + + log info + +To see a log for a given sample (for example org.apache.felix.dependencymanager.samples.device.api), just type: + + log info|grep org.apache.felix.dependencymanager.samples.device.api + +For more informations on each sample, please refer to each README files in the sample source directories: + + ./src/org/apache/felix/dependencymanager/samples/*/README + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/bnd.bnd b/dependencymanager/org.apache.felix.dependencymanager.samples/bnd.bnd new file mode 100644 index 00000000000..0c8e16ebd8c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/bnd.bnd @@ -0,0 +1,38 @@ +# +# 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. +# +Bundle-Version: 1.0.0.${tstamp} +-buildpath: \ + biz.aQute.bnd.annotation,\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.gogo.runtime;version=latest,\ + org.apache.felix.dependencymanager.runtime;version=latest,\ + org.apache.felix.dependencymanager.annotation;version=latest,\ + osgi.core;version=6.0,\ + osgi.cmpn;version=6.0 +-sub: \ + *.bnd +-metatype: * +Bundle-License: http://www.apache.org/licenses/LICENSE-2.0.txt +Bundle-DocURL: http://felix.apache.org/documentation/subprojects/apache-felix-dependency-manager.html +Bundle-Vendor: The Apache Software Foundation + +-plugin: org.apache.felix.dm.annotation.plugin.bnd.AnnotationPlugin;log=debug;\ + path:=${workspace}/org.apache.felix.dependencymanager.annotation/generated/org.apache.felix.dependencymanager.annotation.jar + +# we do not release this project in binary distribution. +-releaserepo: +-baseline: diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/composite.bnd b/dependencymanager/org.apache.felix.dependencymanager.samples/composite.bnd new file mode 100644 index 00000000000..d0a46f76d9b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/composite.bnd @@ -0,0 +1,21 @@ +# +# 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. +# +Private-Package: \ + org.apache.felix.dependencymanager.samples.composite +Bundle-Activator: org.apache.felix.dependencymanager.samples.composite.Activator +Bundle-Description: Dependency Manager composite example +Bundle-Name: Dependency Manager Composite Example \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/composite.bndrun b/dependencymanager/org.apache.felix.dependencymanager.samples/composite.bndrun new file mode 100644 index 00000000000..e4b9dd7aa4f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/composite.bndrun @@ -0,0 +1,40 @@ +# +# 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. +# +-runfw: ${felix.framework} +-runee: JavaSE-1.8 +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active;skip:="osgi.service" + +-runbundles: \ + ${metatype},\ + ${log},\ + ${gogo},\ + ${configadmin},\ + ${eventadmin},\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.dependencymanager.runtime;version=latest,\ + org.apache.felix.dependencymanager.samples.conf;version=latest,\ + org.apache.felix.dependencymanager.samples.composite;version=latest + +-runproperties: \ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/compositefactory.bnd b/dependencymanager/org.apache.felix.dependencymanager.samples/compositefactory.bnd new file mode 100644 index 00000000000..dfa7baa3a8a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/compositefactory.bnd @@ -0,0 +1,21 @@ +# +# 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. +# +Private-Package: \ + org.apache.felix.dependencymanager.samples.compositefactory +Bundle-Activator: org.apache.felix.dependencymanager.samples.compositefactory.Activator +Bundle-Description: Dependency Manager composite factory example +Bundle-Name: Dependency Manager Composite Factory Example \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/compositefactory.bndrun b/dependencymanager/org.apache.felix.dependencymanager.samples/compositefactory.bndrun new file mode 100644 index 00000000000..39d66d6bb02 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/compositefactory.bndrun @@ -0,0 +1,40 @@ +# +# 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. +# +-runfw: ${felix.framework} +-runee: JavaSE-1.8 +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active;skip:="osgi.service" + +-runbundles: \ + ${metatype},\ + ${log},\ + ${gogo},\ + ${configadmin},\ + ${eventadmin},\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.dependencymanager.runtime;version=latest,\ + org.apache.felix.dependencymanager.samples.conf;version=latest,\ + org.apache.felix.dependencymanager.samples.compositefactory;version=latest + +-runproperties: \ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/conf.bnd b/dependencymanager/org.apache.felix.dependencymanager.samples/conf.bnd new file mode 100644 index 00000000000..6752249f76f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/conf.bnd @@ -0,0 +1,21 @@ +# +# 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. +# +Private-Package: \ + org.apache.felix.dependencymanager.samples.conf +Bundle-Activator: org.apache.felix.dependencymanager.samples.conf.Activator +Bundle-Description: Dependency Manager examples configurations +Bundle-Name: Dependency Manager Examples Configurations \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/customdep.bnd b/dependencymanager/org.apache.felix.dependencymanager.samples/customdep.bnd new file mode 100644 index 00000000000..ad294bd9f4c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/customdep.bnd @@ -0,0 +1,21 @@ +# +# 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. +# +Private-Package: \ + org.apache.felix.dependencymanager.samples.customdep +Bundle-Activator: org.apache.felix.dependencymanager.samples.customdep.Activator +Bundle-Description: Dependency Manager custom dependencies example +Bundle-Name: Dependency Manager Custom Dependencies Example \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/customdep.bndrun b/dependencymanager/org.apache.felix.dependencymanager.samples/customdep.bndrun new file mode 100644 index 00000000000..1987cd10605 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/customdep.bndrun @@ -0,0 +1,39 @@ +# +# 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. +# +-runfw: ${felix.framework} +-runee: JavaSE-1.8 +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active;skip:="osgi.service" + +-runbundles: \ + ${metatype},\ + ${log},\ + ${gogo},\ + ${configadmin},\ + ${eventadmin},\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.dependencymanager.runtime;version=latest,\ + org.apache.felix.dependencymanager.samples.customdep;version=latest + +-runproperties: \ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/device.annot.bnd b/dependencymanager/org.apache.felix.dependencymanager.samples/device.annot.bnd new file mode 100644 index 00000000000..0d46a88ab47 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/device.annot.bnd @@ -0,0 +1,20 @@ +# +# 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. +# +Private-Package: \ + org.apache.felix.dependencymanager.samples.device.annot +Bundle-Description: Dependency Manager device example with annotations +Bundle-Name: Dependency Manager Device Example With Annotations diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/device.annot.bndrun b/dependencymanager/org.apache.felix.dependencymanager.samples/device.annot.bndrun new file mode 100644 index 00000000000..9cf140c36a1 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/device.annot.bndrun @@ -0,0 +1,40 @@ +# +# 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. +# +-runfw: ${felix.framework} +-runee: JavaSE-1.8 +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active;skip:="osgi.service" + +-runbundles: \ + ${metatype},\ + ${log},\ + ${gogo},\ + ${configadmin},\ + ${eventadmin},\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.dependencymanager.runtime;version=latest,\ + org.apache.felix.dependencymanager.samples.device.annot;version=latest + +-runproperties: \ + org.apache.felix.dependencymanager.runtime.log=false,\ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/device.api.bnd b/dependencymanager/org.apache.felix.dependencymanager.samples/device.api.bnd new file mode 100644 index 00000000000..433d1ed1198 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/device.api.bnd @@ -0,0 +1,21 @@ +# +# 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. +# +Private-Package: \ + org.apache.felix.dependencymanager.samples.device.api +Bundle-Activator: org.apache.felix.dependencymanager.samples.device.api.Activator +Bundle-Description: Dependency Manager device example with api +Bundle-Name: Dependency Manager Device Example \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/device.api.bndrun b/dependencymanager/org.apache.felix.dependencymanager.samples/device.api.bndrun new file mode 100644 index 00000000000..ad224a24001 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/device.api.bndrun @@ -0,0 +1,39 @@ +# +# 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. +# +-runfw: ${felix.framework} +-runee: JavaSE-1.8 +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active;skip:="osgi.service" + +-runbundles: \ + ${metatype},\ + ${log},\ + ${gogo},\ + ${configadmin},\ + ${eventadmin},\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.dependencymanager.runtime;version=latest,\ + org.apache.felix.dependencymanager.samples.device.api;version=latest + +-runproperties: \ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/dictionary.annot.bnd b/dependencymanager/org.apache.felix.dependencymanager.samples/dictionary.annot.bnd new file mode 100644 index 00000000000..d7fcb9190d6 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/dictionary.annot.bnd @@ -0,0 +1,20 @@ +# +# 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. +# +Private-Package: \ + org.apache.felix.dependencymanager.samples.dictionary.annot +Bundle-Description: Dependency Manager dictionary example with annotations +Bundle-Name: Dependency Manager Dictionary Example With Annotations diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/dictionary.annot.bndrun b/dependencymanager/org.apache.felix.dependencymanager.samples/dictionary.annot.bndrun new file mode 100644 index 00000000000..03bd8c8aa7a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/dictionary.annot.bndrun @@ -0,0 +1,43 @@ +# +# 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. +# +-runfw: ${felix.framework} +-runee: JavaSE-1.8 +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active;skip:="osgi.service" + +-runbundles: \ + ${metatype},\ + ${log},\ + ${gogo},\ + ${configadmin},\ + ${eventadmin},\ + ${webconsole},\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.dependencymanager.runtime;version=latest,\ + org.apache.felix.dependencymanager.samples.dictionary.annot;version=latest,\ + slf4j.simple;version=1.7.7,\ + slf4j.api;version=1.7.7 + +-runproperties: \ + org.apache.felix.dependencymanager.runtime.log=false,\ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/dictionary.api.bnd b/dependencymanager/org.apache.felix.dependencymanager.samples/dictionary.api.bnd new file mode 100644 index 00000000000..77a87be2425 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/dictionary.api.bnd @@ -0,0 +1,21 @@ +# +# 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. +# +Private-Package: \ + org.apache.felix.dependencymanager.samples.dictionary.api +Bundle-Activator: org.apache.felix.dependencymanager.samples.dictionary.api.Activator +Bundle-Description: Dependency Manager dictionary example with api +Bundle-Name: Dependency Manager Dictionary Example \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/dictionary.api.bndrun b/dependencymanager/org.apache.felix.dependencymanager.samples/dictionary.api.bndrun new file mode 100644 index 00000000000..47dcddd9198 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/dictionary.api.bndrun @@ -0,0 +1,43 @@ +# +# 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. +# +-runfw: ${felix.framework} +-runee: JavaSE-1.8 +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active;skip:="osgi.service" + +-runbundles: \ + ${metatype},\ + ${log},\ + ${gogo},\ + ${configadmin},\ + ${eventadmin},\ + ${webconsole},\ + ${bndlib},\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.dependencymanager.samples.dictionary.api;version=latest,\ + slf4j.simple;version=1.7.7,\ + slf4j.api;version=1.7.7 + +-runproperties: \ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/dictionary.metatype.bnd b/dependencymanager/org.apache.felix.dependencymanager.samples/dictionary.metatype.bnd new file mode 100644 index 00000000000..aba4bf7d134 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/dictionary.metatype.bnd @@ -0,0 +1,21 @@ +# +# 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. +# +Private-Package: \ + org.apache.felix.dependencymanager.samples.dictionary.metatype +Bundle-Activator: org.apache.felix.dependencymanager.samples.dictionary.metatype.Activator +Bundle-Description: Dependency Manager dictionary example with api and metatype +Bundle-Name: Dependency Manager Dictionary Example \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/dictionary.metatype.bndrun b/dependencymanager/org.apache.felix.dependencymanager.samples/dictionary.metatype.bndrun new file mode 100644 index 00000000000..cd71d4e207f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/dictionary.metatype.bndrun @@ -0,0 +1,43 @@ +# +# 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. +# +-runfw: ${felix.framework} +-runee: JavaSE-1.8 +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active;skip:="osgi.service" + +-runbundles: \ + ${metatype},\ + ${log},\ + ${gogo},\ + ${configadmin},\ + ${eventadmin},\ + ${webconsole},\ + ${bndlib},\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.dependencymanager.samples.dictionary.metatype;version=latest,\ + slf4j.simple;version=1.7.7,\ + slf4j.api;version=1.7.7 + +-runproperties: \ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/dynamicdep.annot.bnd b/dependencymanager/org.apache.felix.dependencymanager.samples/dynamicdep.annot.bnd new file mode 100644 index 00000000000..b54e15d13c4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/dynamicdep.annot.bnd @@ -0,0 +1,20 @@ +# +# 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. +# +Private-Package: \ + org.apache.felix.dependencymanager.samples.dynamicdep.annot +Bundle-Description: Dependency Manager dynamic dependency example with annotations +Bundle-Name: Dependency Manager Dynamic Dependency Example With Annotations diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/dynamicdep.annot.bndrun b/dependencymanager/org.apache.felix.dependencymanager.samples/dynamicdep.annot.bndrun new file mode 100644 index 00000000000..aa59eb02b6f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/dynamicdep.annot.bndrun @@ -0,0 +1,41 @@ +# +# 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. +# +-runfw: ${felix.framework} +-runee: JavaSE-1.8 +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active;skip:="osgi.service" + +-runbundles: \ + ${metatype},\ + ${log},\ + ${gogo},\ + ${configadmin},\ + ${eventadmin},\ + ${webconsole},\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.dependencymanager.runtime;version=latest,\ + org.apache.felix.dependencymanager.samples.dynamicdep.annot;version=latest + +-runproperties: \ + org.apache.felix.dependencymanager.runtime.log=false,\ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/dynamicdep.api.bnd b/dependencymanager/org.apache.felix.dependencymanager.samples/dynamicdep.api.bnd new file mode 100644 index 00000000000..c5071c03b1f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/dynamicdep.api.bnd @@ -0,0 +1,21 @@ +# +# 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. +# +Bundle-Activator: org.apache.felix.dependencymanager.samples.dynamicdep.api.Activator +Private-Package: \ + org.apache.felix.dependencymanager.samples.dynamicdep.api +Bundle-Description: Dependency Manager dynamic dependency example with api +Bundle-Name: Dependency Manager Dynamic Dependency Example \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/dynamicdep.api.bndrun b/dependencymanager/org.apache.felix.dependencymanager.samples/dynamicdep.api.bndrun new file mode 100644 index 00000000000..1d612421b4f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/dynamicdep.api.bndrun @@ -0,0 +1,42 @@ +# +# 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. +# +-runfw: ${felix.framework} +-runee: JavaSE-1.8 +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active;skip:="osgi.service" + +-runbundles: \ + ${metatype},\ + ${log},\ + ${gogo},\ + ${configadmin},\ + ${eventadmin},\ + ${webconsole},\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.dependencymanager.runtime;version=latest,\ + org.apache.felix.dependencymanager.samples.dynamicdep.api;version=latest + +-runproperties: \ + org.apache.felix.dependencymanager.runtime.log=warn,\ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/hello.annot.bnd b/dependencymanager/org.apache.felix.dependencymanager.samples/hello.annot.bnd new file mode 100644 index 00000000000..aff5a381c90 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/hello.annot.bnd @@ -0,0 +1,20 @@ +# +# 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. +# +Private-Package: \ + org.apache.felix.dependencymanager.samples.hello.annot +Bundle-Description: Dependency Manager hello example with annotations +Bundle-Name: Dependency Manager Hello Example With Annotations diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/hello.annot.bndrun b/dependencymanager/org.apache.felix.dependencymanager.samples/hello.annot.bndrun new file mode 100644 index 00000000000..dd4824e0d33 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/hello.annot.bndrun @@ -0,0 +1,41 @@ +# +# 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. +# +-runfw: ${felix.framework} +-runee: JavaSE-1.8 +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active;skip:="osgi.service" + +-runbundles: \ + ${metatype},\ + ${log},\ + ${gogo},\ + ${configadmin},\ + ${eventadmin},\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.dependencymanager.runtime;version=latest,\ + org.apache.felix.dependencymanager.samples.conf;version=latest,\ + org.apache.felix.dependencymanager.samples.hello.annot;version=latest + +-runproperties: \ + org.apache.felix.dependencymanager.runtime.log=false,\ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/hello.api.bnd b/dependencymanager/org.apache.felix.dependencymanager.samples/hello.api.bnd new file mode 100644 index 00000000000..7c5262fd1c8 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/hello.api.bnd @@ -0,0 +1,20 @@ +# +# 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. +# +Bundle-Activator: org.apache.felix.dependencymanager.samples.hello.api.Activator +Bundle-Description: Dependency Manager hello example with API +Bundle-Name: Dependency Manager Hello Example +Private-Package: org.apache.felix.dependencymanager.samples.hello.api \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/hello.api.bndrun b/dependencymanager/org.apache.felix.dependencymanager.samples/hello.api.bndrun new file mode 100644 index 00000000000..96527ade826 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/hello.api.bndrun @@ -0,0 +1,41 @@ +# +# 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. +# +-runfw: ${felix.framework} +-runee: JavaSE-1.8 +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active;skip:="osgi.service" + +-runbundles: \ + ${metatype},\ + ${log},\ + ${gogo},\ + ${configadmin},\ + ${eventadmin},\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.dependencymanager.runtime;version=latest,\ + org.apache.felix.dependencymanager.samples.conf;version=latest,\ + org.apache.felix.dependencymanager.samples.hello.api;version=latest + +-runproperties: \ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.samples/src/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/composite/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/composite/Activator.java new file mode 100644 index 00000000000..e49c878a092 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/composite/Activator.java @@ -0,0 +1,38 @@ +/* + * 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.dependencymanager.samples.composite; + +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class Activator extends DependencyActivatorBase { + @Override + public void init(BundleContext ctx, DependencyManager m) throws Exception { + m.add(createComponent() + .setImplementation(ProviderImpl.class) + .setComposition("getComposition") + .add(createConfigurationDependency().setPid(ProviderImpl.class.getName())) + .add(createServiceDependency().setService(LogService.class).setRequired(true))); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/composite/Provider.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/composite/Provider.java new file mode 100644 index 00000000000..96d917c2ead --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/composite/Provider.java @@ -0,0 +1,25 @@ +/* + * 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.dependencymanager.samples.composite; + +/** + * @author Felix Project Team + */ +public interface Provider { +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/composite/ProviderImpl.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/composite/ProviderImpl.java new file mode 100644 index 00000000000..6840981756a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/composite/ProviderImpl.java @@ -0,0 +1,51 @@ +/* + * 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.dependencymanager.samples.composite; + +import java.util.Dictionary; + +import org.osgi.service.log.LogService; + +/** + * This is the main implementation for our "Provider" service. + * This service is using a composition of two participants, which are used to provide the service + * (ProviderParticipant1, and ProviderParticipant2). + * + * @author Felix Project Team + */ +public class ProviderImpl implements Provider { + private final ProviderParticipant1 m_participant1 = new ProviderParticipant1(); + private final ProviderParticipant2 m_participant2 = new ProviderParticipant2(); + private volatile LogService m_log; + private Dictionary m_conf; + + public void updated(Dictionary conf) throws Exception { + // validate configuration and throw an exception if the properties are invalid + m_conf = conf; + } + + Object[] getComposition() { + return new Object[] { this, m_participant1, m_participant2 }; + } + + void start() { + m_log.log(LogService.LOG_WARNING, "ProviderImpl.start(): participants=" + m_participant1 + "," + m_participant2 + + ", conf=" + m_conf); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/composite/ProviderParticipant1.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/composite/ProviderParticipant1.java new file mode 100644 index 00000000000..11b2db46f4c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/composite/ProviderParticipant1.java @@ -0,0 +1,37 @@ +/* + * 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.dependencymanager.samples.composite; + +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class ProviderParticipant1 { + private volatile LogService m_log; // Injected + + void start() { + m_log.log(LogService.LOG_WARNING, "ProviderParticipant1.start()"); + } + + @Override + public String toString() { + return "ProviderParticipant1"; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/composite/ProviderParticipant2.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/composite/ProviderParticipant2.java new file mode 100644 index 00000000000..ddc4728d00b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/composite/ProviderParticipant2.java @@ -0,0 +1,37 @@ +/* + * 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.dependencymanager.samples.composite; + +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class ProviderParticipant2 { + private volatile LogService m_log; // Injected + + void start() { + m_log.log(LogService.LOG_WARNING, "ProviderParticipant2.start()"); + } + + @Override + public String toString() { + return "ProviderParticipant2"; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/composite/README b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/composite/README new file mode 100644 index 00000000000..9fa68f79aa7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/composite/README @@ -0,0 +1,33 @@ +/* + * 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. + */ + +This sample is an example usage of DM composite components. A composite component is implemented +using a composition of multiple object instances, which are used to implement a given complex +service. In this example, we define a "Provider" service, which is implemented by three object instances: +ProviderImpl (which is the main implementation class that provides the service), ProviderParticipant1, +and ProviderParticipant2. + +Dependencies are injected in all objects being part of the composition. + +To test, run click on the bndtools "composite.bndrun" descriptor, and run it, then type "log warn" in gogo shell: + +g! log warn + +2016.02.08 23:00:34 WARNING - Bundle: org.apache.felix.dependencymanager.samples.composite - ProviderParticipant2.start() +2016.02.08 23:00:34 WARNING - Bundle: org.apache.felix.dependencymanager.samples.composite - ProviderParticipant1.start() +2016.02.08 23:00:34 WARNING - Bundle: org.apache.felix.dependencymanager.samples.composite - ProviderImpl.start(): participants=ProviderParticipant1,ProviderParticipant2, conf={key=value, service.pid=org.apache.felix.dependencymanager.samples.composite.ProviderImpl} + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/Activator.java new file mode 100644 index 00000000000..980d0aaa77b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/Activator.java @@ -0,0 +1,43 @@ +/* + * 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.dependencymanager.samples.compositefactory; + +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; +import org.osgi.service.log.LogService; + +/** + * Defines a composite service using a composition manager that is used to + * instantiate component instances, depending of what is found from the configuration. + * The configuration is created from the org.apache.felix.dependencymanager.samples.conf.Configurator component. + * + * @author Felix Project Team + */ +public class Activator extends DependencyActivatorBase { + @Override + public void init(BundleContext ctx, DependencyManager m) throws Exception { + CompositionManager factory = new CompositionManager(); + m.add(createComponent() + .setFactory(factory, "create") + .setComposition(factory, "getComposition") + .add(createConfigurationDependency().setPid(CompositionManager.class.getName()).setCallback(factory, "updated")) + .add(createServiceDependency().setService(LogService.class).setRequired(true))); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/CompositionManager.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/CompositionManager.java new file mode 100644 index 00000000000..705d7dc082b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/CompositionManager.java @@ -0,0 +1,60 @@ +/* + * 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.dependencymanager.samples.compositefactory; + +import java.util.Dictionary; + +/** + * Pojo used to create all the objects composition used to implements the "Provider" Service. + * The manager is using a Configuration injected by Config Admin, in order to configure the + * various objects being part of the "Provider" service implementation. + * + * @author Felix Project Team + */ +public class CompositionManager { + private ProviderParticipant1 m_participant1; + private ProviderParticipant2 m_participant2; + private ProviderImpl m_providerImpl; + @SuppressWarnings("unused") + private Dictionary m_conf; + + public void updated(Dictionary conf) throws Exception { + // validate configuration and throw an exception if the properties are invalid + m_conf = conf; + } + + /** + * Builds the composition of objects used to implement the "Provider" service. + * The Configuration injected by Config Admin will be used to configure the components + * @return The "main" object providing the "Provider" service. + */ + Object create() { + // Here, we can instantiate our objects composition and configure them using the injected Configuration ... + // Notice that we can also instantiate some different implementation objects, based on the what we find + // from the configuration. + m_participant1 = new ProviderParticipant1(); // possibly configure this object using our configuration + m_participant2 = new ProviderParticipant2(); // possibly configure this object using our configuration + m_providerImpl = new ProviderImpl(m_participant1, m_participant2); + return m_providerImpl; // Main object implementing the Provider service + } + + Object[] getComposition() { + return new Object[] { m_providerImpl, m_participant1, m_participant2 }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/Provider.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/Provider.java new file mode 100644 index 00000000000..7b135965a26 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/Provider.java @@ -0,0 +1,26 @@ +/* + * 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.dependencymanager.samples.compositefactory; + +/** + * @author Felix Project Team + */ +public interface Provider { + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/ProviderImpl.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/ProviderImpl.java new file mode 100644 index 00000000000..a3673e65d52 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/ProviderImpl.java @@ -0,0 +1,46 @@ +/* + * 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.dependencymanager.samples.compositefactory; + +import org.osgi.service.log.LogService; + +/** + * This is the main implementation for our "Provider" service. + * This service is using a composition of two participants, which are used to provide the service + * (ProviderParticipant1, and ProviderParticipant2). + * + * This class is instantiated by the CompositionManager class. + * + * @author Felix Project Team + */ +public class ProviderImpl implements Provider { + private final ProviderParticipant1 m_participant1; + private final ProviderParticipant2 m_participant2; + + private volatile LogService m_log; // Injected + + ProviderImpl(ProviderParticipant1 participant1, ProviderParticipant2 participant2) { + m_participant1 = participant1; + m_participant2 = participant2; + } + + void start() { + m_log.log(LogService.LOG_WARNING, "ProviderImpl.start(): participants=" + m_participant1 + "," + m_participant2); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/ProviderParticipant1.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/ProviderParticipant1.java new file mode 100644 index 00000000000..4dfd3e46594 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/ProviderParticipant1.java @@ -0,0 +1,37 @@ +/* + * 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.dependencymanager.samples.compositefactory; + +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class ProviderParticipant1 { + private volatile LogService m_log; // Injected + + void start() { + m_log.log(LogService.LOG_WARNING, "ProviderParticipant1.start()"); + } + + @Override + public String toString() { + return "ProviderParticipant1"; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/ProviderParticipant2.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/ProviderParticipant2.java new file mode 100644 index 00000000000..533e5fbe4a9 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/ProviderParticipant2.java @@ -0,0 +1,37 @@ +/* + * 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.dependencymanager.samples.compositefactory; + +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class ProviderParticipant2 { + private volatile LogService m_log; // Injected + + void start() { + m_log.log(LogService.LOG_WARNING, "ProviderParticipant2.start()"); + } + + @Override + public String toString() { + return "ProviderParticipant2"; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/README b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/README new file mode 100644 index 00000000000..f23a1b089f3 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/compositefactory/README @@ -0,0 +1,33 @@ +/* + * 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. + */ + +This Activator is an example usage of DM composite. A composite component is implemented +using a composition of multiple object instances, which are used to implement a given service. + +The sample also uses a Factory approach in order to instantiate the composition of objects: A +"CompositionManager" is first injected with a Configuration that can possibly be used to create +and configure all the composites. + +Dependencies are injected in all objects in the composition. + +To test, run click on "compositefactory.bndrun" descriptor, and run it, then type "log warn" in gogo shell: + +g! log warn + +2016.02.08 22:26:05 WARNING - Bundle: org.apache.felix.dependencymanager.samples.compositefactory - ProviderParticipant2.start() +2016.02.08 22:26:05 WARNING - Bundle: org.apache.felix.dependencymanager.samples.compositefactory - ProviderParticipant1.start() +2016.02.08 22:26:05 WARNING - Bundle: org.apache.felix.dependencymanager.samples.compositefactory - ProviderImpl.start(): participants=ProviderParticipant1,ProviderParticipant2 diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/conf/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/conf/Activator.java new file mode 100644 index 00000000000..64982010d52 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/conf/Activator.java @@ -0,0 +1,38 @@ +/* + * 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.dependencymanager.samples.conf; + +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class Activator extends DependencyActivatorBase { + @Override + public void init(BundleContext context, DependencyManager dm) throws Exception { + dm.add(createComponent() + .setImplementation(Configurator.class) + .add(createServiceDependency().setService(LogService.class).setRequired(true)) + .add(createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true))); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/conf/Configurator.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/conf/Configurator.java new file mode 100644 index 00000000000..beafc6c8676 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/conf/Configurator.java @@ -0,0 +1,76 @@ +/* + * 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.dependencymanager.samples.conf; + +import java.io.IOException; +import java.util.Hashtable; + +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.log.LogService; + +/** + * Configurator class used to inject configuration into Configuration Admin Service. + * + * @author Felix Project Team + */ +public class Configurator { + private volatile ConfigurationAdmin m_ca; + volatile Configuration m_serviceConsumerConf; + volatile Configuration m_serviceConsumerAnnotConf; + volatile LogService m_log; + + public void start() { + try { + System.out.println("Configuring sample components ... please consult log messages to see example output, like this:"); + System.out.println("\"log warn\""); + // Provide configuration to the hello.ServiceConsumer component + m_serviceConsumerConf = m_ca.getConfiguration("org.apache.felix.dependencymanager.samples.hello.api.ServiceConsumerConf", null); + Hashtable props = new Hashtable<>(); + props.put("key", "value"); + m_serviceConsumerConf.update(props); + + // Provide configuration to the hello.annot.ServiceConsumer component + m_serviceConsumerAnnotConf = m_ca.getConfiguration("org.apache.felix.dependencymanager.samples.hello.annot.ServiceConsumerConf", null); + props = new Hashtable<>(); + props.put("key", "value"); + m_serviceConsumerAnnotConf.update(props); + + // Provide configuration to the composite component + m_serviceConsumerAnnotConf = m_ca.getConfiguration("org.apache.felix.dependencymanager.samples.composite.ProviderImpl", null); + props = new Hashtable<>(); + props.put("key", "value"); + m_serviceConsumerAnnotConf.update(props); + + // Provide configuration to the compositefactory component + m_serviceConsumerAnnotConf = m_ca.getConfiguration("org.apache.felix.dependencymanager.samples.compositefactory.CompositionManager", null); + props = new Hashtable<>(); + props.put("key", "value"); + m_serviceConsumerAnnotConf.update(props); + } + catch (IOException e) { + e.printStackTrace(); + } + } + + public void destroy() throws IOException { + m_serviceConsumerConf.delete(); + m_serviceConsumerAnnotConf.delete(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/conf/README b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/conf/README new file mode 100644 index 00000000000..b51f853e64a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/conf/README @@ -0,0 +1,19 @@ +/* + * 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. + */ + +This sample defines a component that is used to inject configuration into the Configuration Admin +service, in order to configure other components declared in the various samples. diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/customdep/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/customdep/Activator.java new file mode 100644 index 00000000000..3a6607ffc2e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/customdep/Activator.java @@ -0,0 +1,41 @@ +/* + * 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.dependencymanager.samples.customdep; + +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class Activator extends DependencyActivatorBase { + PathDependency createPathDependency(String path) { + return new PathDependencyImpl(path); + } + + @Override + public void init(BundleContext context, DependencyManager m) throws Exception { + m.add(createComponent() + .setImplementation(PathTracker.class) + .add(createServiceDependency().setService(LogService.class).setRequired(true)) + .add(createPathDependency("/tmp").setCallbacks("add", "remove").setRequired(true))); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/customdep/PathDependency.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/customdep/PathDependency.java new file mode 100644 index 00000000000..f05fe79b779 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/customdep/PathDependency.java @@ -0,0 +1,33 @@ +/* + * 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.dependencymanager.samples.customdep; + +import org.apache.felix.dm.Dependency; + +/** + * A custom Dependency Manager Path Dependency that can track a path directory. + * When a file is added or removed from the path dir, then the component is called + * in the corresponding add/remove callback. + * + * @author Felix Project Team + */ +public interface PathDependency extends Dependency { + PathDependency setRequired(boolean required); + PathDependency setCallbacks(String add, String remove); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/customdep/PathDependencyImpl.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/customdep/PathDependencyImpl.java new file mode 100644 index 00000000000..2b5edb472ec --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/customdep/PathDependencyImpl.java @@ -0,0 +1,179 @@ +/* + * 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.dependencymanager.samples.customdep; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchEvent.Kind; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.List; + +import org.apache.felix.dm.context.AbstractDependency; +import org.apache.felix.dm.context.DependencyContext; +import org.apache.felix.dm.context.Event; +import org.apache.felix.dm.context.EventType; + +/** + * This is our own "path" Dependency Manager Dependency, which can track the presence of files in a given path dir. + * Every DM custom dependency must implement the DependencyContext interface, but we extends the AbstractDependency + * which already implements most of the DependencyContext methods. + * + * @author Felix Project Team + */ +public class PathDependencyImpl extends AbstractDependency implements PathDependency, Runnable { + private final String m_path; + private volatile Thread m_thread; + + /** + * Creates a new custom DM "path" dependency. + * @param path the directory to watch for + */ + public PathDependencyImpl(String path) { + super.setRequired(true); + m_path = path; + } + + /** + * Create a new PathDependency from an existing prototype. + * @param prototype the existing PathDependency. + */ + public PathDependencyImpl(PathDependencyImpl prototype) { + super(prototype); + m_path = prototype.m_path; + } + + // ---------- DependencyContext interface ---------- + + @Override + public DependencyContext createCopy() { + return new PathDependencyImpl(this); + } + + @Override + public Class getAutoConfigType() { + return null; // we don't support auto config mode + } + + @Override + public void start() { + m_thread = new Thread(this); + m_thread.start(); + super.start(); + } + + @Override + public void stop() { + m_thread.interrupt(); + super.stop(); + } + + @Override + public void invokeCallback(EventType type, Event ...events) { + switch (type) { + case ADDED: + if (m_add != null) { + invoke(m_add, events[0], getInstances()); + } + break; + case REMOVED: + if (m_remove != null) { + invoke(m_remove, events[0], getInstances()); + } + break; + default: + // We don't support other kind of callbacks. + break; + } + } + + // ---------- ComponentDependencyDeclaration interface ----------- + + /** + * Returns the name of this dependency (a generic name with optional info separated by spaces). + * The DM Shell will use this method when displaying the dependency + **/ + @Override + public String getSimpleName() { + return m_path; + } + + /** + * Returns the name of the type of this dependency. Used by the DM shell when displaying the dependency. + **/ + @Override + public String getType() { + return "path"; + } + + // ---------- other methods ----------- + + /** + * Our start method fires a thread and this is our run method, which is watching for a given directory path + */ + public void run() { + Path myDir = Paths.get(m_path); + + try { + WatchService watcher = myDir.getFileSystem().newWatchService(); + myDir.register(watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_DELETE); + while (! Thread.currentThread().isInterrupted()) { + WatchKey watckKey = watcher.take(); + + List> events = watckKey.pollEvents(); + + for (@SuppressWarnings("rawtypes") WatchEvent event : events) { + final Kind kind = event.kind(); + if (StandardWatchEventKinds.OVERFLOW == kind) { + continue; + } + if (StandardWatchEventKinds.ENTRY_CREATE == kind) { + // Notify the component implementation context that a file has been created. + // Later, the component will call our invokeAdd method in order to inject the file + // in the component instance + m_component.handleEvent(this, EventType.ADDED, new Event(event.context().toString())); + } else if (StandardWatchEventKinds.ENTRY_DELETE == kind) { + // Notify the component implementation context that a file has been removed. + // Later, the component will call our invokeRemove method in order to call our component "remove" callback + m_component.handleEvent(this, EventType.REMOVED, new Event(event.context().toString())); + } + } + + watckKey.reset(); + } + } catch (Throwable e) { + m_component.getLogger().err("path dependency exception", e); + } + } + + /** + * Invoke either the "add" or "remove" callback of the component instance(s). + */ + private void invoke(String method, Event e, Object[] instances) { + // specific for this type of dependency + m_component.invokeCallbackMethod(instances, method, + new Class[][] { {String.class}, + {}}, + new Object[][] { { e.getEvent() }, + {}}); + } +} \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/customdep/PathTracker.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/customdep/PathTracker.java new file mode 100644 index 00000000000..443bc7c30a2 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/customdep/PathTracker.java @@ -0,0 +1,41 @@ +/* + * 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.dependencymanager.samples.customdep; + +import org.osgi.service.log.LogService; + +public class PathTracker { + volatile LogService logService; + + void start() { + logService.log(LogService.LOG_WARNING, "PathTracker.start"); + } + + void stop() { + logService.log(LogService.LOG_WARNING, "PathTracker.stop"); + } + + void add(String path) { + logService.log(LogService.LOG_WARNING, "PathTracker.add: " + path); + } + + void remove(String path) { + logService.log(LogService.LOG_WARNING, "PathTracker.remove: " + path); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/customdep/README b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/customdep/README new file mode 100644 index 00000000000..a67ad3c7b37 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/customdep/README @@ -0,0 +1,34 @@ +/* + * 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. + */ + +This directory contains an example of a custom dependency (PathDependency). +This dependency tracks all created files in /tmp and injects them in the PathTracker.add(String path) method ... + +The PathDependendency is a low level DM example, but shows how to create any custom dependencies. + +To test this sample, start the "customdep.bndrun" descriptor, then create a file in /tmp: + + $ echo "test" > /tmp/foo + +Then display logs (your Tracker component has normally been injected with the added file and has been started): + + g! log warn + +2016.02.08 22:17:46 WARNING - Bundle: org.apache.felix.dependencymanager.samples.customdep - PathTracker.start +2016.02.08 22:17:46 WARNING - Bundle: org.apache.felix.dependencymanager.samples.customdep - PathTracker.add: foo + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/Device.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/Device.java new file mode 100644 index 00000000000..b08062aa2f1 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/Device.java @@ -0,0 +1,26 @@ +/* + * 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.dependencymanager.samples.device.annot; + +/** + * @author Felix Project Team + */ +public interface Device { + int getDeviceId(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceAccess.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceAccess.java new file mode 100644 index 00000000000..2f101333842 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceAccess.java @@ -0,0 +1,30 @@ +/* + * 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.dependencymanager.samples.device.annot; + +/** + * Provides unified access to a pair of Device/DeviceParameter services having the same device ID. + * + * @author Felix Project Team + */ +public interface DeviceAccess { + Device getDevice(); + + DeviceParameter getDeviceParameter(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceAccessConsumer.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceAccessConsumer.java new file mode 100644 index 00000000000..bbab8645aa4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceAccessConsumer.java @@ -0,0 +1,43 @@ +/* + * 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.dependencymanager.samples.device.annot; + +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +@Component +public class DeviceAccessConsumer { + @ServiceDependency + volatile LogService log; + + // Injected after all required dependencies have been injected (including our logger) + @ServiceDependency(required=false) + void add(DeviceAccess deviceAccess, Map props) { + log.log(LogService.LOG_WARNING, "Handling device access: id=" + props.get("device.access.id") + + "\n\t device=" + deviceAccess.getDevice() + + "\n\t device parameter=" + deviceAccess.getDeviceParameter() + + "\n\t device access properties=" + props); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceAccessImpl.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceAccessImpl.java new file mode 100644 index 00000000000..a33252b30ce --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceAccessImpl.java @@ -0,0 +1,72 @@ +/* + * 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.dependencymanager.samples.device.annot; + +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.AdapterService; +import org.apache.felix.dm.annotation.api.Init; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +@AdapterService(adapteeService = Device.class) +public class DeviceAccessImpl implements DeviceAccess { + volatile Device device; + + @ServiceDependency(name = "deviceparam") + volatile DeviceParameter deviceParameter; + + @ServiceDependency + volatile LogService log; + + @Init + Map init() { + log.log(LogService.LOG_WARNING, "DeviceAccessImpl.init: device id=" + device.getDeviceId()); + // Dynamically configure our "deviceparam" dependency, using the already injected device service. + Map filters = new HashMap<>(); + filters.put("deviceparam.filter", "(device.id=" + device.getDeviceId() + ")"); + filters.put("deviceparam.required", "true"); + return filters; + } + + @Start + Map start() { + log.log(LogService.LOG_WARNING, "DeviceAccessImpl.start"); + // Dynamically add a service property, using the device.id + Map props = new Hashtable<>(); + props.put("device.access.id", device.getDeviceId()); + return props; + } + + @Override + public Device getDevice() { + return device; + } + + @Override + public DeviceParameter getDeviceParameter() { + return deviceParameter; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceAndParameterFactory.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceAndParameterFactory.java new file mode 100644 index 00000000000..06962723dd7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceAndParameterFactory.java @@ -0,0 +1,66 @@ +/* + * 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.dependencymanager.samples.device.annot; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.log.LogService; + +/** + * Component used to instantiate Device and DeviceParameter services, using DM component factory annotation. + * + * @author Felix Project Team + */ +@Component +public class DeviceAndParameterFactory { + @ServiceDependency + ConfigurationAdmin cm; + + @ServiceDependency + volatile LogService log; + + @Start + public void start() throws IOException { + log.log(LogService.LOG_INFO, "DeviceAndParameterFactory.start"); + for (int i = 0; i < 2; i++) { + createDeviceAndParameter(i); + } + } + + private void createDeviceAndParameter(int id) throws IOException { + log.log(LogService.LOG_INFO, "DeviceAndParameterFactory: creating Device/DeviceParameter with id=" + id); + + Dictionary device = new Hashtable<>(); + device.put("device.id", new Integer(id)); + Configuration deviceConf = cm.createFactoryConfiguration("device", "?"); + deviceConf.update(device); + + Dictionary param = new Hashtable<>(); + param.put("device.id", new Integer(id)); + Configuration paramConf = cm.createFactoryConfiguration("device.parameter", "?"); + paramConf.update(param); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceImpl.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceImpl.java new file mode 100644 index 00000000000..e059cfac2b5 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceImpl.java @@ -0,0 +1,46 @@ +/* + * 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.dependencymanager.samples.device.annot; + +import java.util.Dictionary; + +import org.apache.felix.dm.annotation.api.Component; + +/** + * @author Felix Project Team + */ +@Component(factoryPid = "device") +public class DeviceImpl implements Device { + int id; + + void updated(Dictionary configuration) { + this.id = (Integer) configuration.get("device.id"); + } + + @Override + public int getDeviceId() { + return id; + } + + + @Override + public String toString() { + return "Device #" + id; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceParameter.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceParameter.java new file mode 100644 index 00000000000..81f270507b6 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceParameter.java @@ -0,0 +1,26 @@ +/* + * 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.dependencymanager.samples.device.annot; + +/** + * @author Felix Project Team + */ +public interface DeviceParameter { + int getDeviceId(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceParameterImpl.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceParameterImpl.java new file mode 100644 index 00000000000..fc7432b1afc --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/DeviceParameterImpl.java @@ -0,0 +1,45 @@ +/* + * 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.dependencymanager.samples.device.annot; + +import java.util.Dictionary; + +import org.apache.felix.dm.annotation.api.Component; + +/** + * @author Felix Project Team + */ +@Component(factoryPid = "device.parameter", propagate=true) +public class DeviceParameterImpl implements DeviceParameter { + int id; + + void updated(Dictionary configuration) { + this.id = (Integer) configuration.get("device.id"); + } + + @Override + public int getDeviceId() { + return id; + } + + @Override + public String toString() { + return "DeviceParameter #" + id; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/README b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/README new file mode 100644 index 00000000000..ec3612d4ccf --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/annot/README @@ -0,0 +1,44 @@ +/* + * 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. + */ + +This is an example showing a Dependency Manager "Adapter" in action, using DM annotations. Two kinds +of services are registered in the registry: some Device, and some DeviceParameter services. For each +Device (having a given id), there is also a corresponding "DeviceParameter" service, having the same +id. + +Then a "DeviceAccessImpl" adapter service is defined: it is used to "adapt" the "Device" service to +a "DeviceAccess" service, which provides the union of each pair of Device/DeviceParameter having the +same device.id . The adapter also dynamically propagate the service properties of the adapted Device +service. + +Start the test by clicking on the "device.annot.bndrun" descriptor, then run it, and type under gogo shell: + +g! log warn + +2016.02.08 22:05:45 WARNING - Bundle: org.apache.felix.dependencymanager.samples.device.annot - Handling device access: id=1 + device=Device #1 + device parameter=DeviceParameter #1 + device access properties={service.bundleid=12, service.id=31, service.scope=singleton, objectClass=[Ljava.lang.String;@6e38921c, device.id=1, device.access.id=1} +2016.02.08 22:05:45 WARNING - Bundle: org.apache.felix.dependencymanager.samples.device.annot - DeviceAccessImpl.start +2016.02.08 22:05:45 WARNING - Bundle: org.apache.felix.dependencymanager.samples.device.annot - DeviceAccessImpl.init: device id=1 +2016.02.08 22:05:45 WARNING - Bundle: org.apache.felix.dependencymanager.samples.device.annot - Handling device access: id=0 + device=Device #0 + device parameter=DeviceParameter #0 + device access properties={service.bundleid=12, service.id=28, service.scope=singleton, device.id=0, objectClass=[Ljava.lang.String;@6e38921c, device.access.id=0} +2016.02.08 22:05:45 WARNING - Bundle: org.apache.felix.dependencymanager.samples.device.annot - DeviceAccessImpl.start +2016.02.08 22:05:45 WARNING - Bundle: org.apache.felix.dependencymanager.samples.device.annot - DeviceAccessImpl.init: device id=0 + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/Activator.java new file mode 100644 index 00000000000..08b8a1c7c2a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/Activator.java @@ -0,0 +1,59 @@ +/* + * 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.dependencymanager.samples.device.api; + +import java.util.Hashtable; + +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class Activator extends DependencyActivatorBase { + @Override + public void init(BundleContext context, DependencyManager dm) throws Exception { + createDeviceAndParameter(dm, 1); + createDeviceAndParameter(dm, 2); + + dm.add(createAdapterComponent() + .setAdaptee(Device.class, null) + .setImplementation(DeviceAccessImpl.class) + .setInterface(DeviceAccess.class.getName(), null)); + + dm.add(createComponent() + .setImplementation(DeviceAccessConsumer.class) + .add(createServiceDependency().setService(LogService.class).setRequired(true)) + .add(createServiceDependency().setService(DeviceAccess.class).setRequired(true).setCallbacks("add", null))); + } + + private void createDeviceAndParameter(DependencyManager dm, int id) { + Hashtable props = new Hashtable<>(); + props.put("device.id", id); + dm.add(createComponent() + .setImplementation(new DeviceImpl(id)).setInterface(Device.class.getName(), props)); + + props = new Hashtable<>(); + props.put("device.id", id); + dm.add(createComponent() + .setImplementation(new DeviceParameterImpl(id)).setInterface(DeviceParameter.class.getName(), props)); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/Device.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/Device.java new file mode 100644 index 00000000000..0903ff51519 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/Device.java @@ -0,0 +1,26 @@ +/* + * 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.dependencymanager.samples.device.api; + +/** + * @author Felix Project Team + */ +public interface Device { + int getDeviceId(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/DeviceAccess.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/DeviceAccess.java new file mode 100644 index 00000000000..fd5206146c8 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/DeviceAccess.java @@ -0,0 +1,30 @@ +/* + * 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.dependencymanager.samples.device.api; + +/** + * Provides unified access to a pair of Device/DeviceParameter services having the same device ID. + + * @author Felix Project Team + */ +public interface DeviceAccess { + Device getDevice(); + + DeviceParameter getDeviceParameter(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/DeviceAccessConsumer.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/DeviceAccessConsumer.java new file mode 100644 index 00000000000..68593eed008 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/DeviceAccessConsumer.java @@ -0,0 +1,37 @@ +/* + * 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.dependencymanager.samples.device.api; + +import java.util.Map; + +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class DeviceAccessConsumer { + volatile LogService log; + + void add(Map props, DeviceAccess deviceAccess) { + log.log(LogService.LOG_WARNING, "DeviceAccessConsumer: Handling device access: id=" + props.get("device.access.id") + + "\n\t device=" + deviceAccess.getDevice() + + "\n\t device parameter=" + deviceAccess.getDeviceParameter() + + "\n\t device access properties=" + props); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/DeviceAccessImpl.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/DeviceAccessImpl.java new file mode 100644 index 00000000000..610851b8ec4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/DeviceAccessImpl.java @@ -0,0 +1,54 @@ +/* + * 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.dependencymanager.samples.device.api; + +import java.util.Hashtable; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; + +/** + * @author Felix Project Team + */ +public class DeviceAccessImpl implements DeviceAccess { + volatile Device device; + volatile DeviceParameter deviceParameter; + + void init(Component component) { + // Dynamically add an extra dependency on a DeviceParameter. + // We also add a "device.access.id" property dynamically. + Hashtable props = new Hashtable<>(); + props.put("device.access.id", device.getDeviceId()); + + DependencyManager dm = component.getDependencyManager(); + component + .setServiceProperties(props) + .add(dm.createServiceDependency().setService(DeviceParameter.class, "(device.id=" + device.getDeviceId() + ")").setRequired(true)); + } + + @Override + public Device getDevice() { + return device; + } + + @Override + public DeviceParameter getDeviceParameter() { + return deviceParameter; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/DeviceImpl.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/DeviceImpl.java new file mode 100644 index 00000000000..9f1cecb41bb --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/DeviceImpl.java @@ -0,0 +1,40 @@ +/* + * 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.dependencymanager.samples.device.api; + +/** + * @author Felix Project Team + */ +public class DeviceImpl implements Device { + final int id; + + public DeviceImpl(int id) { + this.id = id; + } + + @Override + public int getDeviceId() { + return id; + } + + @Override + public String toString() { + return "Device #" + id; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/DeviceParameter.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/DeviceParameter.java new file mode 100644 index 00000000000..0f8a39e803b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/DeviceParameter.java @@ -0,0 +1,26 @@ +/* + * 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.dependencymanager.samples.device.api; + +/** + * @author Felix Project Team + */ +public interface DeviceParameter { + int getDeviceId(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/DeviceParameterImpl.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/DeviceParameterImpl.java new file mode 100644 index 00000000000..b7dbbc8b238 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/DeviceParameterImpl.java @@ -0,0 +1,40 @@ +/* + * 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.dependencymanager.samples.device.api; + +/** + * @author Felix Project Team + */ +public class DeviceParameterImpl implements DeviceParameter { + final int id; + + public DeviceParameterImpl(int id) { + this.id = id; + } + + @Override + public int getDeviceId() { + return id; + } + + @Override + public String toString() { + return "DeviceParameter #" + id; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/README b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/README new file mode 100644 index 00000000000..afd53adfdda --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/device/api/README @@ -0,0 +1,40 @@ +/* + * 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. + */ + +This is an example showing a Dependency Manager "Adapter" in action. Two kinds of services are +registered in the registry: some Device, and some DeviceParameter services. For each Device (having +a given id), there is also a corresponding "DeviceParameter" service, having the same id. + +Then a "DeviceAccessImpl" adapter service is defined: it is used to "adapt" the "Device" service to +a "DeviceAccess" service, which provides the union of each pair of Device/DeviceParameter having the +same device.id . The adapter also dynamically propagate the service properties of the adapted Device +service. + +Start the test by clicking on the "device.api.bndrun" descriptor, then run it, and type under gogo shell: + +g! log warn + +You will then see: + +2016.02.08 23:04:46 WARNING - Bundle: org.apache.felix.dependencymanager.samples.device.api - DeviceAccessConsumer: Handling device access: id=2 + device=Device #2 + device parameter=DeviceParameter #2 + device access properties={service.bundleid=12, service.id=28, service.scope=singleton, objectClass=[Ljava.lang.String;@679b62af, device.access.id=2, device.id=2} +2016.02.08 23:04:46 WARNING - Bundle: org.apache.felix.dependencymanager.samples.device.api - DeviceAccessConsumer: Handling device access: id=1 + device=Device #1 + device parameter=DeviceParameter #1 + device access properties={service.bundleid=12, service.id=29, service.scope=singleton, objectClass=[Ljava.lang.String;@679b62af, device.id=1, device.access.id=1} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/DictionaryAspect.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/DictionaryAspect.java new file mode 100644 index 00000000000..05c883acd12 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/DictionaryAspect.java @@ -0,0 +1,92 @@ +/* + * 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.dependencymanager.samples.dictionary.annot; + +import java.util.concurrent.CopyOnWriteArrayList; + +import org.apache.felix.dm.annotation.api.AspectService; +import org.apache.felix.dm.annotation.api.ConfigurationDependency; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.osgi.service.log.LogService; + +/** + * This aspect applies to the English DictionaryService, and allows to decorate it with some + * custom English words, which are configurable from WebConsole. + * + * @author Felix Project Team + */ +@AspectService(ranking = 10, filter = "(lang=en)") +public class DictionaryAspect implements DictionaryService { + /** + * This is the service this aspect is applying to. + */ + private volatile DictionaryService m_originalDictionary; + + /** + * We store all configured words in a thread-safe data structure, because ConfigAdmin may + * invoke our updated method at any time. + */ + private CopyOnWriteArrayList m_words = new CopyOnWriteArrayList(); + + /** + * We'll use the OSGi log service for logging. If no log service is available, then we'll + * use a NullObject. + */ + @ServiceDependency(required = false) + private LogService m_log; + + /** + * Our Configuration PID + */ + final static String PID = "dictionary.aspect"; + + /** + * Defines a configuration dependency for retrieving our english custom words (by default, + * our PID is our full class name). + */ + @ConfigurationDependency(pid=PID, propagate = false) + protected void updated(DictionaryAspectConfiguration cnf) { + if (cnf != null) { + m_words.clear(); + for (String word : cnf.words()) { + m_words.add(word); + } + } + } + + /** + * Our Aspect Service is starting and is about to be registered in the OSGi regsitry. + */ + @Start + protected void start() { + m_log.log(LogService.LOG_INFO, "Starting aspect Dictionary with words: " + m_words + + "; original dictionary service=" + m_originalDictionary); + } + + /** + * Checks if a word is found from our custom word list. if not, delegate to the decorated + * dictionary. + */ + public boolean checkWord(String word) { + m_log.log(LogService.LOG_INFO, "DictionaryAspect: checking word " + word + " (original dictionary=" + + m_originalDictionary + ")"); + if (m_words.contains(word)) { + return true; + } + return m_originalDictionary.checkWord(word); + } + + public String toString() { + return "DictionaryAspect: words=" + m_words + "; original dictionary=" + m_originalDictionary; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/DictionaryAspectConfiguration.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/DictionaryAspectConfiguration.java new file mode 100644 index 00000000000..683588a654c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/DictionaryAspectConfiguration.java @@ -0,0 +1,39 @@ +/* + * 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.dependencymanager.samples.dictionary.annot; + +import java.util.List; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +/** + * This interface describes the configuration for our DictionaryAspect component. We are using the bnd metatype + * annotations, allowing to configure our Dictionary Services from web console. + * + * @author Felix Project Team + */ +@ObjectClassDefinition( + name="Spell Checker Aspect Dictionary", + description = "Declare here the list of english words to be added into the default english dictionary", + pid=DictionaryAspect.PID) +public interface DictionaryAspectConfiguration { + @AttributeDefinition(description = "Dictionary aspect words") + List words(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/DictionaryConfiguration.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/DictionaryConfiguration.java new file mode 100644 index 00000000000..888e0e206b9 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/DictionaryConfiguration.java @@ -0,0 +1,42 @@ +/* + * 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.dependencymanager.samples.dictionary.annot; + +import java.util.List; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +/** + * This interface describes the configuration for our DictionaryImpl component. We are using the bnd metatype + * annotations, allowing to configure our Dictionary Services from web console. + * + * @author Felix Project Team + */ +@ObjectClassDefinition( + name="Spell Checker Dictionary", + factoryPid = DictionaryImpl.FACTORY_PID, + description = "Declare here some Dictionary instances, allowing to instantiates some DictionaryService services for a given dictionary language") +public interface DictionaryConfiguration { + @AttributeDefinition(description = "Describes the dictionary language", defaultValue = "en") + String lang(); + + @AttributeDefinition(description = "Declare here the list of words supported by this dictionary. This properties starts with a Dot and won't be propagated with Dictionary OSGi service properties") + List words(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/DictionaryImpl.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/DictionaryImpl.java new file mode 100644 index 00000000000..7fcb524d322 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/DictionaryImpl.java @@ -0,0 +1,104 @@ +/* + * 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.dependencymanager.samples.dictionary.annot; + +import java.util.concurrent.CopyOnWriteArrayList; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.osgi.service.log.LogService; + +/** + * A Dictionary Service. This service uses a FactoryConfigurationAdapterService + * annotation, allowing to instantiate this service from webconsole. This + * annotation will actually register a ManagedServiceFactory in the registry. + * The Configuration metatype informations is described using the bnd metatype + * information (see the DictionaryConfiguration interface). + * + * You must configure at least one Dictionary from web console, since the + * SpellCheck won't start if no Dictionary Service is available. + * + * @author Felix Project Team + */ +@Component(factoryPid = DictionaryImpl.FACTORY_PID, propagate = true) +public class DictionaryImpl implements DictionaryService { + + /** + * Our factory configuration pid. + */ + final static String FACTORY_PID = "spellchecker.Dictionary"; + + /** + * We store all configured words in a thread-safe data structure, because + * ConfigAdmin may invoke our updated method at any time. + */ + private CopyOnWriteArrayList m_words = new CopyOnWriteArrayList(); + + /** + * We'll use the OSGi log service for logging. If no log service is available, + * then we'll use a NullObject. + */ + @ServiceDependency(required = false) + private LogService m_log; + + /** + * Our Dictionary language. + */ + private String m_lang; + + /** + * Our service will be initialized from ConfigAdmin. + * + * @param config + * The configuration where we'll lookup our words list + * (key=".words"). + */ + protected void updated(DictionaryConfiguration cnf) { + if (cnf != null) { + m_lang = cnf.lang(); + m_words.clear(); + for (String word : cnf.words()) { + m_words.add(word); + } + } + } + + /** + * A new Dictionary Service is starting (because a new factory configuration has + * been created from webconsole). + */ + @Start + protected void start() { + m_log.log(LogService.LOG_INFO, "Starting Dictionary Service with language: " + m_lang); + } + + /** + * Check if a word exists if the list of words we have been configured from + * ConfigAdmin/WebConsole. + */ + public boolean checkWord(String word) { + return m_words.contains(word); + } + + @Override + public String toString() { + return "Dictionary: language=" + m_lang + ", words=" + m_words; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/DictionaryService.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/DictionaryService.java new file mode 100644 index 00000000000..ea498ec29c8 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/DictionaryService.java @@ -0,0 +1,35 @@ +/* + * 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.dependencymanager.samples.dictionary.annot; + +/** + * A simple service interface that defines a dictionary service. A dictionary + * service simply verifies the existence of a word. + * + * @author Felix Project Team + */ +public interface DictionaryService { + /** + * Check for the existence of a word. + * + * @param word the word to be checked. + * @return true if the word is in the dictionary, false otherwise. + */ + public boolean checkWord(String word); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/README b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/README new file mode 100644 index 00000000000..3a706f9358f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/README @@ -0,0 +1,43 @@ +/* + * 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. + */ + +This sample shows a "SpellChecker" application (using DM Annotations) which provides a +"dictionary.annotation:spellcheck" GOGO shell command. The GOGO "dictionary.annotation:spellcheck" command accepts a +string as parameter, which is checked for proper existence. The SpellChecker class has a +required/multiple (1..N) dependency over every available "DictionaryService" services, which are +internally used by the SpellChecker command, when checking word existence. + +A DictionaryService is defined using a FactoryConfigurationAdapterService , allowing to instantiate +many "DictionaryService" instances when some configurations are added to the +"Spell Checker Dictionary" factory pid from web console. +The factory pid configuration metatypes are defined using the bnd "metatype" annotations +(see DictionaryConfiguration.java). + +The DictionaryService is decorated with a DictionaryAspect, which you can instantiate by adding a +configuration to the "Spell Checker Aspect Dictionary" factory pid from web console. The +aspect configuration metatype is also declared using the bnd metatype annotations (see DictionaryAspectConfiguration.java). + +Start the test: click on "dictionary.annot.bndrun" descriptor, and run it. +Then go to webconsole: http://localhost:8080/system/console/configMgr (with user=admin, password=admin), and add some words in the +"Spell Checker Dictionary" section, and in the "Spell Checker Aspect Dictionary" configurations. + +Then go to gogo shell, and type dm help. You will normally see the dictionary.annotation:spellcheck command. +Then type: + + spellcheck + +and the command will check for proper word existence. diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/SpellChecker.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/SpellChecker.java new file mode 100644 index 00000000000..fec65e118ea --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/annot/SpellChecker.java @@ -0,0 +1,89 @@ +/* + * 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.dependencymanager.samples.dictionary.annot; + +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.apache.felix.dm.annotation.api.Stop; +import org.apache.felix.service.command.CommandProcessor; +import org.apache.felix.service.command.Descriptor; +import org.osgi.service.log.LogService; + +/** + * Felix "spellcheck" Gogo Shell Command. This command allows to check if some given words are valid or not. + * This command will be activated only if (at least) one DictionaryService has been injected. + * To create a Dictionary Service, you have to go the the web console and define a "Dictionary Services" factory + * configuration instance, which will fire an instantiation of the corresponding dictionary service. + * + * @author Felix Project Team + */ +@Component(provides = SpellChecker.class) +@Property(name = CommandProcessor.COMMAND_SCOPE, value = "dictionary.annotation") +@Property(name = CommandProcessor.COMMAND_FUNCTION, value = "spellcheck" ) +public class SpellChecker { + /** + * We'll use the OSGi log service for logging. If no log service is available, then we'll use a NullObject. + */ + @ServiceDependency(required = false) + private volatile LogService m_log; + + /** + * We'll store all Dictionaries in a concurrent list, in order to avoid method synchronization. + */ + @ServiceDependency + private final Collection m_dictionaries = new ConcurrentLinkedQueue<>(); + + /** + * Lifecycle method callback, used to check if our service has been activated. + */ + @Start + protected void start() { + m_log.log(LogService.LOG_WARNING, "Spell Checker started"); + } + + /** + * Lifecycle method callback, used to check if our service has been activated. + */ + @Stop + protected void stop() { + m_log.log(LogService.LOG_WARNING, "Spell Checker stopped"); + } + + // --- Gogo Shell command + + @Descriptor("checks if word is found from an available dictionary") + public void spellcheck(@Descriptor("the word to check") String word) { + m_log.log(LogService.LOG_INFO, "Checking spelling of word \"" + word + "\" using the following dictionaries: " + + m_dictionaries); + + for (DictionaryService dictionary : m_dictionaries) { + if (dictionary.checkWord(word)) { + System.out.println("word " + word + " is correct"); + return; + } + } + System.err.println("word " + word + " is incorrect"); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/Activator.java new file mode 100644 index 00000000000..dfc41c5550e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/Activator.java @@ -0,0 +1,65 @@ +/* + * 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.dependencymanager.samples.dictionary.api; + +import java.util.Hashtable; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.service.command.CommandProcessor; +import org.osgi.framework.BundleContext; +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class Activator extends DependencyActivatorBase { + @Override + public void init(BundleContext context, DependencyManager dm) throws Exception { + // Create the factory configuration for our DictionaryImpl service. + String pid = DictionaryConfiguration.class.getName(); + Class type = DictionaryConfiguration.class; + Component factoryComponent = createFactoryComponent() + .setFactoryPid(pid).setPropagate(true).setConfigType(type) + .setInterface(DictionaryService.class.getName(), null) + .setImplementation(DictionaryImpl.class) + .add(createServiceDependency().setService(LogService.class)); // NullObject + dm.add(factoryComponent); + + // Create the Dictionary Aspect + Component aspect = createAspectComponent() + .setAspect(DictionaryService.class, "(lang=en)", 10) + .setImplementation(DictionaryAspect.class) + .add(createConfigurationDependency().setPid(DictionaryAspectConfiguration.class.getName()).setCallback("updated", DictionaryConfiguration.class)) + .add(createServiceDependency().setService(LogService.class)); // NullObject + dm.add(aspect); + + // Create the SpellChecker component + Hashtable props = new Hashtable<>(); + props.put(CommandProcessor.COMMAND_SCOPE, "dictionary"); + props.put(CommandProcessor.COMMAND_FUNCTION, new String[] { "spellcheck" }); + Component spellcheck = createComponent() + .setImplementation(SpellChecker.class) + .setInterface(SpellChecker.class.getName(), props) + .add(createServiceDependency().setService(DictionaryService.class).setRequired(true)) + .add(createServiceDependency().setService(LogService.class)); // NullObject + dm.add(spellcheck); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/DictionaryAspect.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/DictionaryAspect.java new file mode 100644 index 00000000000..3e491f3aa7c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/DictionaryAspect.java @@ -0,0 +1,79 @@ +/* + * 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.dependencymanager.samples.dictionary.api; + +import java.util.concurrent.CopyOnWriteArrayList; + +import org.osgi.service.log.LogService; + +/** + * This aspect applies to the English DictionaryService, and allows to decorate it with some + * custom English words, which are configurable from WebConsole. + * + * @author Felix Project Team + */ +public class DictionaryAspect implements DictionaryService { + /** + * This is the service this aspect is applying to. + */ + private volatile DictionaryService m_originalDictionary; + + /** + * We store all configured words in a thread-safe data structure, because ConfigAdmin may + * invoke our updated method at any time. + */ + private CopyOnWriteArrayList m_words = new CopyOnWriteArrayList(); + + /** + * We'll use the OSGi log service for logging. If no log service is available, then we'll + * use a NullObject. + */ + private LogService m_log; + + /** + * Defines a configuration dependency for retrieving our english custom words (by default, + * our PID is our full class name). + */ + protected void updated(DictionaryConfiguration config) { + if (config != null) { + m_words.clear(); + for (String word : config.words()) { + m_words.add(word); + } + } + } + + /** + * Our Aspect Service is starting and is about to be registered in the OSGi regsitry. + */ + protected void start() { + m_log.log(LogService.LOG_INFO, "Starting aspect Dictionary with words: " + m_words + + "; original dictionary service=" + m_originalDictionary); + } + + /** + * Checks if a word is found from our custom word list. if not, delegate to the decorated + * dictionary. + */ + public boolean checkWord(String word) { + m_log.log(LogService.LOG_INFO, "DictionaryAspect: checking word " + word + " (original dictionary=" + + m_originalDictionary + ")"); + if (m_words.contains(word)) { + return true; + } + return m_originalDictionary.checkWord(word); + } + + public String toString() { + return "DictionaryAspect: words=" + m_words + "; original dictionary=" + m_originalDictionary; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/DictionaryAspectConfiguration.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/DictionaryAspectConfiguration.java new file mode 100644 index 00000000000..2126950a604 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/DictionaryAspectConfiguration.java @@ -0,0 +1,39 @@ +/* + * 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.dependencymanager.samples.dictionary.api; + +import java.util.List; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +/** + * This interface describes the configuration for our DictionaryAspect component. We are using the bnd metatype + * annotations, allowing to configure our Dictionary Services from web console. + * + * @author Felix Project Team + */ +@ObjectClassDefinition( + name="Spell Checker Aspect Dictionary", + description = "Declare here the list of english words to be added into the default english dictionary", + pid="org.apache.felix.dependencymanager.samples.dictionary.api.DictionaryAspectConfiguration") +public interface DictionaryAspectConfiguration { + @AttributeDefinition(description = "Dictionary aspect words") + List words(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/DictionaryConfiguration.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/DictionaryConfiguration.java new file mode 100644 index 00000000000..eae818226ed --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/DictionaryConfiguration.java @@ -0,0 +1,41 @@ +/* + * 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.dependencymanager.samples.dictionary.api; + +import java.util.List; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +/** + * This interface describes the configuration for our DictionaryImpl component. We are using the bnd metatype + * annotations, allowing to configure our Dictionary Services from web console. + * + * @author Felix Project Team + */ +@ObjectClassDefinition(name="Spell Checker Dictionary", + factoryPid = "org.apache.felix.dependencymanager.samples.dictionary.api.DictionaryConfiguration", + description = "Declare here some Dictionary instances, allowing to instantiates some DictionaryService services for a given dictionary language") +public interface DictionaryConfiguration { + @AttributeDefinition(description = "Describes the dictionary language", defaultValue = "en") + String lang(); + + @AttributeDefinition(description = "Declare here the list of words supported by this dictionary.") + List words(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/DictionaryImpl.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/DictionaryImpl.java new file mode 100644 index 00000000000..bc0a6636257 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/DictionaryImpl.java @@ -0,0 +1,88 @@ +/* + * 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.dependencymanager.samples.dictionary.api; + +import java.util.concurrent.CopyOnWriteArrayList; + +import org.osgi.service.log.LogService; + +/** + * A Dictionary Service, instantiated from webconsole, when you add some configurations instances to the + * DictionaryConfiguration factory pid. The Configuration metatype informations is described using the + * bnd metatype information (see the DictionaryConfiguration interface). + * + * You must configure at least one Dictionary from web console, since the SpellCheck won't start if no Dictionary + * Service is available. + * + * @author Felix Project Team + */ +public class DictionaryImpl implements DictionaryService { + /** + * The key of our config admin dictionary values. + */ + final static String WORDS = "words"; + + /** + * We store all configured words in a thread-safe data structure, because ConfigAdmin + * may invoke our updated method at any time. + */ + private CopyOnWriteArrayList m_words = new CopyOnWriteArrayList(); + + /** + * We'll use the OSGi log service for logging. If no log service is available, then we'll use a NullObject. + */ + private LogService m_log; + + /** + * Our Dictionary language. + */ + private String m_lang; + + /** + * Our service will be initialized from ConfigAdmin. + * @param config The configuration where we'll lookup our words list (key=".words"). + */ + protected void updated(DictionaryConfiguration cnf) { + m_lang = cnf.lang(); + m_words.clear(); + for (String word : cnf.words()) { + m_words.add(word); + } + } + + /** + * A new Dictionary Service is starting (because a new factory configuration has been created + * from webconsole). + */ + protected void start() { + m_log.log(LogService.LOG_INFO, "Starting Dictionary Service with language: " + m_lang); + } + + /** + * Check if a word exists if the list of words we have been configured from ConfigAdmin/WebConsole. + */ + public boolean checkWord(String word) { + return m_words.contains(word); + } + + @Override + public String toString() { + return "Dictionary: language=" + m_lang + ", words=" + m_words; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/DictionaryService.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/DictionaryService.java new file mode 100644 index 00000000000..cb60f3549cf --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/DictionaryService.java @@ -0,0 +1,35 @@ +/* + * 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.dependencymanager.samples.dictionary.api; + +/** + * A simple service interface that defines a dictionary service. A dictionary + * service simply verifies the existence of a word. + * + * @author Felix Project Team + */ +public interface DictionaryService { + /** + * Check for the existence of a word. + * + * @param word the word to be checked. + * @return true if the word is in the dictionary, false otherwise. + */ + public boolean checkWord(String word); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/README b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/README new file mode 100644 index 00000000000..c26e62c2813 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/README @@ -0,0 +1,45 @@ +/* + * 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. + */ + +This sample shows a "SpellChecker" application (using DM API) which provides a +"dictionary:spellcheck" GOGO shell command. The GOGO "dictionary:spellcheck" command accepts a +string as parameter, which is checked for proper existence. The SpellChecker class has a +required/multiple (1..N) dependency over every available "DictionaryService" services, which are +internally used by the SpellChecker command, when checking word existence. + +A DictionaryService is defined using a FactoryComponent , allowing to instantiate +many "DictionaryService" instances when some configurations are added to the +"Spell Checker Configuration" factory pid from web +console. The factory pid configuration metatypes are defined using the bnd "metatype" annotations +(see DictionaryConfiguration.java). + +The DictionaryService is decorated with a DictionaryAspect, which you can instantiate by adding a +configuration to the "Spell Checker Aspect Dictionary" pid from web console. The +aspect configuration metatype is also declared using the bnd metatype annotations (see +DictionaryAspectConfiguration.java). + +Start the test: click on "dictionary.api.bndrun" descriptor, and run it. +Then go to webconsole (http://localhost:8080/system/console/configMgr), and add some words in the +"Spell Checker Dictionary" section, and in the "Spell Checker Aspect Dictionary" configurations. + +Then go to gogo shell, and type dm help. You will normally see the dictionary.annotation:spellcheck command. +Then type: + + spellcheck + +and the command will check for proper word existence. + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/SpellChecker.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/SpellChecker.java new file mode 100644 index 00000000000..89c13bbea2b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/api/SpellChecker.java @@ -0,0 +1,75 @@ +/* + * 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.dependencymanager.samples.dictionary.api; + +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.apache.felix.service.command.Descriptor; +import org.osgi.service.log.LogService; + +/** + * Felix "spellcheck" Gogo Shell Command. This command allows to check if some given words are valid or not. + * This command will be activated only if (at least) one DictionaryService has been injected. + * To create a Dictionary Service, you have to go the the web console and add a configuration in the + * "Dictionary Configuration" factory pid. + * + * @author Felix Project Team + */ +public class SpellChecker { + /** + * We'll use the OSGi log service for logging. If no log service is available, then we'll use a NullObject. + */ + private volatile LogService m_log; + + /** + * We'll store all Dictionaries in a concurrent list, in order to avoid method synchronization. + * (Auto-Injected from Activator, at any time). + */ + private final Iterable m_dictionaries = new ConcurrentLinkedQueue<>(); + + /** + * Lifecycle method callback, used to check if our service has been activated. + */ + protected void start() { + m_log.log(LogService.LOG_WARNING, "Spell Checker started"); + } + + /** + * Lifecycle method callback, used to check if our service has been activated. + */ + protected void stop() { + m_log.log(LogService.LOG_WARNING, "Spell Checker stopped"); + } + + // --- Gogo Shell command + + @Descriptor("checks if word is found from an available dictionary") + public void spellcheck(@Descriptor("the word to check") String word) { + m_log.log(LogService.LOG_INFO, "Checking spelling of word \"" + word + "\" using the following dictionaries: " + + m_dictionaries); + + for (DictionaryService dictionary : m_dictionaries) { + if (dictionary.checkWord(word)) { + System.out.println("word " + word + " is correct"); + return; + } + } + System.err.println("word " + word + " is incorrect"); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/Activator.java new file mode 100644 index 00000000000..73a554efe27 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/Activator.java @@ -0,0 +1,76 @@ +/* + * 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.dependencymanager.samples.dictionary.metatype; + +import java.util.Hashtable; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.service.command.CommandProcessor; +import org.osgi.framework.BundleContext; +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class Activator extends DependencyActivatorBase { + @Override + public void init(BundleContext context, DependencyManager dm) throws Exception { + // Create the factory configuration for our DictionaryImpl service. + // We also provide metatype info in order to be able to configure our factory configuration + // from config admin. + + Component factory = createFactoryComponent() + .setFactoryPid(DictionaryConfiguration.class) + .setPropagate(true) + .setConfigType(DictionaryConfiguration.class) + .setHeading("Spell Checker Dictionary") + .setDesc("Declare here some Dictionary instances, allowing to instantiates some DictionaryService services for a given dictionary language") + .add(dm.createPropertyMetaData().setId("lang").setHeading("language").setDefaults("en").setDescription("Enter the dictionary language")) + .add(dm.createPropertyMetaData().setId("words").setHeading("words database").setDescription("Enter the dictionary words (comma separated)")) + .setInterface(DictionaryService.class.getName(), null) + .setImplementation(DictionaryImpl.class) + .add(createServiceDependency().setService(LogService.class)); // Null Object + + dm.add(factory); + + // Create the Dictionary Aspect + Component aspect = createAspectComponent() + .setAspect(DictionaryService.class, "(lang=en)", 10) + .setImplementation(DictionaryAspect.class) + .add(createServiceDependency().setService(LogService.class)) // NullObject + .add(createConfigurationDependency() + .setPid(DictionaryAspectConfiguration.class.getName()).setCallback("updated", DictionaryConfiguration.class) + .setHeading("Spell Checker Aspect Dictionary") + .setDescription("Enter english words (comma separated)") + .add(dm.createPropertyMetaData().setId("words").setHeading("Dictionary aspect words").setDescription("Enter enlish words"))); + dm.add(aspect); + + // Create the SpellChecker component + Hashtable props = new Hashtable<>(); + props.put(CommandProcessor.COMMAND_SCOPE, "dictionary"); + props.put(CommandProcessor.COMMAND_FUNCTION, new String[] { "spellcheck" }); + dm.add(createComponent() + .setImplementation(SpellChecker.class) + .setInterface(SpellChecker.class.getName(), props) + .add(createServiceDependency().setService(DictionaryService.class).setRequired(true)) + .add(createServiceDependency().setService(LogService.class))); // NullObject + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/DictionaryAspect.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/DictionaryAspect.java new file mode 100644 index 00000000000..268817b98c7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/DictionaryAspect.java @@ -0,0 +1,79 @@ +/* + * 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.dependencymanager.samples.dictionary.metatype; + +import java.util.concurrent.CopyOnWriteArrayList; + +import org.osgi.service.log.LogService; + +/** + * This aspect applies to the English DictionaryService, and allows to decorate it with some + * custom English words, which are configurable from WebConsole. + * + * @author Felix Project Team + */ +public class DictionaryAspect implements DictionaryService { + /** + * This is the service this aspect is applying to. + */ + private volatile DictionaryService m_originalDictionary; + + /** + * We store all configured words in a thread-safe data structure, because ConfigAdmin may + * invoke our updated method at any time. + */ + private CopyOnWriteArrayList m_words = new CopyOnWriteArrayList(); + + /** + * We'll use the OSGi log service for logging. If no log service is available, then we'll + * use a NullObject. + */ + private LogService m_log; + + /** + * Defines a configuration dependency for retrieving our english custom words (by default, + * our PID is our full class name). + */ + protected void updated(DictionaryConfiguration config) { + if (config != null) { + m_words.clear(); + for (String word : config.words()) { + m_words.add(word); + } + } + } + + /** + * Our Aspect Service is starting and is about to be registered in the OSGi regsitry. + */ + protected void start() { + m_log.log(LogService.LOG_INFO, "Starting aspect Dictionary with words: " + m_words + + "; original dictionary service=" + m_originalDictionary); + } + + /** + * Checks if a word is found from our custom word list. if not, delegate to the decorated + * dictionary. + */ + public boolean checkWord(String word) { + m_log.log(LogService.LOG_INFO, "DictionaryAspect: checking word " + word + " (original dictionary=" + + m_originalDictionary + ")"); + if (m_words.contains(word)) { + return true; + } + return m_originalDictionary.checkWord(word); + } + + public String toString() { + return "DictionaryAspect: words=" + m_words + "; original dictionary=" + m_originalDictionary; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/DictionaryAspectConfiguration.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/DictionaryAspectConfiguration.java new file mode 100644 index 00000000000..a0581a9a24b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/DictionaryAspectConfiguration.java @@ -0,0 +1,30 @@ +/* + * 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.dependencymanager.samples.dictionary.metatype; + +import java.util.List; + +/** + * This interface describes the configuration for our DictionaryAspect component. + * + * @author Felix Project Team + */ +public interface DictionaryAspectConfiguration { + List words(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/DictionaryConfiguration.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/DictionaryConfiguration.java new file mode 100644 index 00000000000..0e83e013669 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/DictionaryConfiguration.java @@ -0,0 +1,32 @@ +/* + * 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.dependencymanager.samples.dictionary.metatype; + +import java.util.List; + +/** + * This interface describes the configuration for our DictionaryImpl component. + * + * @author Felix Project Team + */ +public interface DictionaryConfiguration { + String lang(); + + List words(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/DictionaryImpl.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/DictionaryImpl.java new file mode 100644 index 00000000000..25eda3bb9cb --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/DictionaryImpl.java @@ -0,0 +1,88 @@ +/* + * 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.dependencymanager.samples.dictionary.metatype; + +import java.util.concurrent.CopyOnWriteArrayList; + +import org.osgi.service.log.LogService; + +/** + * A Dictionary Service, instantiated from webconsole, when you add some configurations instances to the + * DictionaryConfiguration factory pid. The Configuration metatype informations is described using the + * bnd metatype information (see the DictionaryConfiguration interface). + * + * You must configure at least one Dictionary from web console, since the SpellCheck won't start if no Dictionary + * Service is available. + * + * @author Felix Project Team + */ +public class DictionaryImpl implements DictionaryService { + /** + * The key of our config admin dictionary values. + */ + final static String WORDS = "words"; + + /** + * We store all configured words in a thread-safe data structure, because ConfigAdmin + * may invoke our updated method at any time. + */ + private CopyOnWriteArrayList m_words = new CopyOnWriteArrayList(); + + /** + * We'll use the OSGi log service for logging. If no log service is available, then we'll use a NullObject. + */ + private LogService m_log; + + /** + * Our Dictionary language. + */ + private String m_lang; + + /** + * Our service will be initialized from ConfigAdmin. + * @param config The configuration where we'll lookup our words list (key=".words"). + */ + protected void updated(DictionaryConfiguration cnf) { + m_lang = cnf.lang(); + m_words.clear(); + for (String word : cnf.words()) { + m_words.add(word); + } + } + + /** + * A new Dictionary Service is starting (because a new factory configuration has been created + * from webconsole). + */ + protected void start() { + m_log.log(LogService.LOG_INFO, "Starting Dictionary Service with language: " + m_lang); + } + + /** + * Check if a word exists if the list of words we have been configured from ConfigAdmin/WebConsole. + */ + public boolean checkWord(String word) { + return m_words.contains(word); + } + + @Override + public String toString() { + return "Dictionary: language=" + m_lang + ", words=" + m_words; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/DictionaryService.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/DictionaryService.java new file mode 100644 index 00000000000..b942d47a887 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/DictionaryService.java @@ -0,0 +1,35 @@ +/* + * 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.dependencymanager.samples.dictionary.metatype; + +/** + * A simple service interface that defines a dictionary service. A dictionary + * service simply verifies the existence of a word. + * + * @author Felix Project Team + */ +public interface DictionaryService { + /** + * Check for the existence of a word. + * + * @param word the word to be checked. + * @return true if the word is in the dictionary, false otherwise. + */ + public boolean checkWord(String word); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/README b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/README new file mode 100644 index 00000000000..7b87b12619b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/README @@ -0,0 +1,21 @@ +/* + * 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. + */ + +This is the same example from the +org.apache.felix.dependencymanager.samples.dictionary.api example, except that the +configuration metatypes are declared using dependency manager API instead of using +standard osgi metatypes annotations. diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/SpellChecker.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/SpellChecker.java new file mode 100644 index 00000000000..d157eb1522d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dictionary/metatype/SpellChecker.java @@ -0,0 +1,75 @@ +/* + * 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.dependencymanager.samples.dictionary.metatype; + +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.apache.felix.service.command.Descriptor; +import org.osgi.service.log.LogService; + +/** + * Felix "spellcheck" Gogo Shell Command. This command allows to check if some given words are valid or not. + * This command will be activated only if (at least) one DictionaryService has been injected. + * To create a Dictionary Service, you have to go the the web console and add a configuration in the + * "Dictionary Configuration" factory pid. + * + * @author Felix Project Team + */ +public class SpellChecker { + /** + * We'll use the OSGi log service for logging. If no log service is available, then we'll use a NullObject. + */ + private volatile LogService m_log; + + /** + * We'll store all Dictionaries in a concurrent list, in order to avoid method synchronization. + * (Auto-Injected from Activator, at any time). + */ + private final Iterable m_dictionaries = new ConcurrentLinkedQueue<>(); + + /** + * Lifecycle method callback, used to check if our service has been activated. + */ + protected void start() { + m_log.log(LogService.LOG_WARNING, "Spell Checker started"); + } + + /** + * Lifecycle method callback, used to check if our service has been activated. + */ + protected void stop() { + m_log.log(LogService.LOG_WARNING, "Spell Checker stopped"); + } + + // --- Gogo Shell command + + @Descriptor("checks if word is found from an available dictionary") + public void spellcheck(@Descriptor("the word to check") String word) { + m_log.log(LogService.LOG_INFO, "Checking spelling of word \"" + word + "\" using the following dictionaries: " + + m_dictionaries); + + for (DictionaryService dictionary : m_dictionaries) { + if (dictionary.checkWord(word)) { + System.out.println("word " + word + " is correct"); + return; + } + } + System.err.println("word " + word + " is incorrect"); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/annot/DynamicDependency.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/annot/DynamicDependency.java new file mode 100644 index 00000000000..0679f17e965 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/annot/DynamicDependency.java @@ -0,0 +1,92 @@ +/* + * 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.dependencymanager.samples.dynamicdep.annot; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.ConfigurationDependency; +import org.apache.felix.dm.annotation.api.Init; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.event.EventAdmin; +import org.osgi.service.log.LogService; + +/** + * This Component depends on the following services declared from the Activator: + * - LogService + * - Configuration with PID="org.apache.felix.dependencymanager.samples.dynamicdep.api.DynamicDependencyConfiguration" + * + * We the define a dynamic dependency on a Storage Service from our init method and we configure the dependency filter and + * required from using the injected configuration in our updated method. + * + * @author Felix Project Team + */ +@Component +public class DynamicDependency { + @ServiceDependency + volatile EventAdmin eventAdmin; + + @ServiceDependency + volatile LogService log; + + @ServiceDependency(name="storage") + volatile Storage storage; // dependency defined dynamically from our init() method + + private String storageType; // type of Storage to depend on (we get that from configadmin) + private boolean storageRequired; // is our Storage dependency required or not (we get that from configadmin) + + /** + * This is the first callback: we are injected with our configuration. + */ + @ConfigurationDependency + public void updated(DynamicDependencyConfiguration cnf) throws ConfigurationException { + if (cnf != null) { + storageType = cnf.storageType(); + storageRequired = cnf.storageRequired(); + } + } + + /** + * The configuration has been injected and also other required dependencies defined from the Activator. + * Now, define some dynamic dependencies (here we use the configuration injected from our updated method in + * order to configure the filter and required flag for the "Storage" dependency). + */ + @Init + Map init() { + log.log(LogService.LOG_WARNING, "init: storage type=" + storageType + ", storageRequired=" + storageRequired); + Map props = new HashMap<>(); + props.put("storage.required", Boolean.toString(storageRequired)); + props.put("storage.filter", "(type=" + storageType + ")"); + return props; + } + + /** + * All dependencies injected, including dynamic dependencies defined from init method. + */ + @Start + void start() { + log.log(LogService.LOG_WARNING, "start"); + // Use storage to load/store some key-value pairs ... + storage.store("gabu", "zo"); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/annot/DynamicDependencyConfiguration.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/annot/DynamicDependencyConfiguration.java new file mode 100644 index 00000000000..cb678f4b376 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/annot/DynamicDependencyConfiguration.java @@ -0,0 +1,49 @@ +/* + * 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.dependencymanager.samples.dynamicdep.annot; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.*; + +/** + * This interface describes the configuration for our DynamicDependencyComponent component. We are using the metatype + * annotations, allowing to configure our component from web console. + * + * @author Felix Project Team + */ +@ObjectClassDefinition( + name = "Dynamic Dependency Configuration", + description = "Declare here the configuration for the DynamicDependency component.", + pid="org.apache.felix.dependencymanager.samples.dynamicdep.annot.DynamicDependencyConfiguration") +public interface DynamicDependencyConfiguration { + + @AttributeDefinition(description = "Enter the storage type to use", + defaultValue = "mapdb", + options={ + @Option(label="Map DB Storage implementation", value="mapdb"), + @Option(label="File Storage implementation", value="file") + }) + String storageType(); + + @AttributeDefinition( + description = "Specifies here is the storage dependency is required or not (if false, a null object will be used)", + defaultValue = "true") + boolean storageRequired(); + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/annot/FileStorage.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/annot/FileStorage.java new file mode 100644 index 00000000000..09584ff2f8d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/annot/FileStorage.java @@ -0,0 +1,48 @@ +/* + * 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.dependencymanager.samples.dynamicdep.annot; + +import java.io.Serializable; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +@Component +@Property(name="type", value="file") +public class FileStorage implements Storage { + @ServiceDependency + volatile LogService log; // injected + + @Override + public void store(String key, Serializable data) { + log.log(LogService.LOG_WARNING, "FileStorage.store(" + key + "," + data + ")"); + } + + @Override + public Serializable get(String key) { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/annot/MapDBStorage.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/annot/MapDBStorage.java new file mode 100644 index 00000000000..fbfc2ccfdcf --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/annot/MapDBStorage.java @@ -0,0 +1,47 @@ +/* + * 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.dependencymanager.samples.dynamicdep.annot; + +import java.io.Serializable; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.Property; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +@Component +@Property(name="type", value="mapdb") +public class MapDBStorage implements Storage { + @ServiceDependency + volatile LogService log; // injected + + @Override + public void store(String key, Serializable data) { + log.log(LogService.LOG_WARNING, "MapDBStorage.store(" + key + "," + data + ")"); + } + + @Override + public Serializable get(String key) { + // TODO Auto-generated method stub + return null; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/annot/README b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/annot/README new file mode 100644 index 00000000000..b9b66a8d712 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/annot/README @@ -0,0 +1,52 @@ +/* + * 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. + */ + +This sample shows how to define a dynamic dependency using annotations. +Using annotation, you can still dynamically configure from your @Init method +some dependencies which has a "name" attribute. Such dependencies will then +be injected after your @Init method, but before your @Start method (if the dependencies +are required), or after your @Start method (if the dependencies are optional). + +When you declare a Component: + +- the configuration (if any) is first injected (updated callback). +- then all required dependencies are injected, except "named" dependencies whose required flag and filter can be configured +dynamically from the init method. +- then the init method (annotated with @Init) is invoked; And from there you are then able to return a Map that will be used +to configure the required flag and the filter of all named dependencies. +- then the start callback (annotated with @Start) is invoked when all required dependencies are injected, including named +dependencies that have been configured from the init method. + +In this sample, the "DynamicDependency" Components configures in its "init" method the dependency having a "storage" name. +the dependency "required" flag and filter string are loaded from a Configuration PID +(see the "Dynamic Dependency Configuration" PID, from webconsole), which is defined using +Bnd MetaType Annotations. + + +So, first, click on the "dynamicdep.annot.bndrun" descriptor, and run it. +Then go to webconsole (http://localhost:8080/system/console/configMgr), and configure the "Dynamic Dependency Configuration" PID. +then just type "log warn" under the gogo shell: + +log warn + +Then you normally see something like: + +2016.02.08 21:49:26 WARNING - Bundle: org.apache.felix.dependencymanager.samples.dynamicdep.annot - MapDBStorage.store(gabu,zo) +2016.02.08 21:49:26 WARNING - Bundle: org.apache.felix.dependencymanager.samples.dynamicdep.annot - start +2016.02.08 21:49:26 WARNING - Bundle: org.apache.felix.dependencymanager.samples.dynamicdep.annot - init: storage type=mapdb, storageRequired=true + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/annot/Storage.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/annot/Storage.java new file mode 100644 index 00000000000..8164d365ebd --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/annot/Storage.java @@ -0,0 +1,29 @@ +/* + * 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.dependencymanager.samples.dynamicdep.annot; + +import java.io.Serializable; + +/** + * @author Felix Project Team + */ +public interface Storage { + Serializable get(String key); + void store(String key, Serializable data); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/Activator.java new file mode 100644 index 00000000000..58016bffaea --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/Activator.java @@ -0,0 +1,55 @@ +/* + * 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.dependencymanager.samples.dynamicdep.api; + +import java.util.Properties; + +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; +import org.osgi.service.event.EventAdmin; +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class Activator extends DependencyActivatorBase { + + @Override + public void init(BundleContext bc, DependencyManager dm)throws Exception { + Properties props = new Properties(); + props.put("type", "mapdb"); + dm.add(createComponent() + .setImplementation(MapDBStorage.class).setInterface(Storage.class.getName(), props) + .add(createServiceDependency().setService(LogService.class).setRequired(true))); + + props = new Properties(); + props.put("type", "file"); + dm.add(createComponent() + .setImplementation(FileStorage.class).setInterface(Storage.class.getName(), props) + .add(createServiceDependency().setService(LogService.class).setRequired(true))); + + dm.add(createComponent() + .setImplementation(DynamicDependency.class) + .add(createServiceDependency().setService(LogService.class).setRequired(true)) + .add(createConfigurationDependency().setPid(DynamicDependencyConfiguration.class.getName()).setCallback("updated", DynamicDependencyConfiguration.class)) + .add(createServiceDependency().setService(EventAdmin.class).setRequired(true))); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/DynamicDependency.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/DynamicDependency.java new file mode 100644 index 00000000000..f7ed1ba0265 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/DynamicDependency.java @@ -0,0 +1,76 @@ +/* + * 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.dependencymanager.samples.dynamicdep.api; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.event.EventAdmin; +import org.osgi.service.log.LogService; + +/** + * This Component depends on the following services declared from the Activator: + * - LogService + * - Configuration with PID="org.apache.felix.dependencymanager.samples.dynamicdep.api.DynamicDependencyConfiguration" + * + * We then define a dynamic dependency on a Storage Service from our init method and we configure the dependency filter and + * required from using the configuration injected in our updated method. + * + * @author Felix Project Team + */ +public class DynamicDependency { + + volatile EventAdmin eventAdmin; // injected and defined in Activator + volatile LogService log; // injected and defined in Activator + volatile Storage storage; // dependency defined dynamically from our init() method + private String storageType; // type of Storage to depend on (we get that from configadmin) + private boolean storageRequired; // is our Storage dependency required or not (we get that from configadmin) + + /** + * This is the first callback: we are injected with our configuration. + */ + public void updated(DynamicDependencyConfiguration cnf) throws ConfigurationException { + if (cnf != null) { + storageType = cnf.storageType(); + storageRequired = cnf.storageRequired(); + } + } + + /** + * The configuration has been injected and also other required dependencies defined from the Activator. + * Now, define some dynamic dependencies (here we use the configuration injected from our updated method in + * order to configure the filter and required flag for the "Storage" dependency). + */ + public void init(Component c) { + log.log(LogService.LOG_WARNING, "init: storage type=" + storageType + ", storageRequired=" + storageRequired); + DependencyManager dm = c.getDependencyManager(); + // all dynamic dependencies must be declared atomically in the Component.add(...) method, which accepts varargs. + c.add(dm.createServiceDependency().setService(Storage.class, "(type=" + storageType + ")").setRequired(storageRequired)); + } + + /** + * All dependencies injected, including dynamic dependencies defined from init method. + */ + void start() { + log.log(LogService.LOG_WARNING, "start"); + // Use storage to load/store some key-value pairs ... + storage.store("gabu", "zo"); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/DynamicDependencyConfiguration.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/DynamicDependencyConfiguration.java new file mode 100644 index 00000000000..b2ca3c1c57a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/DynamicDependencyConfiguration.java @@ -0,0 +1,50 @@ +/* + * 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.dependencymanager.samples.dynamicdep.api; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import org.osgi.service.metatype.annotations.Option; + +/** + * This interface describes the configuration for our DynamicDependencyComponent component. We are using the metatype + * annotations, allowing to configure our component from web console. + * + * @author Felix Project Team + */ +@ObjectClassDefinition( + name = "Dynamic Dependency Configuration", + description = "Declare here the configuration for the DynamicDependency component.", + pid="org.apache.felix.dependencymanager.samples.dynamicdep.api.DynamicDependencyConfiguration") +public interface DynamicDependencyConfiguration { + + @AttributeDefinition(description = "Enter the storage type to use", + defaultValue = "mapdb", + options={ + @Option(label="Map DB Storage implementation", value="mapdb"), + @Option(label="File Storage implementation", value="file") + }) + String storageType(); + + @AttributeDefinition( + description = "Specifies here is the storage dependency is required or not (if false, a null object will be used)", + defaultValue = "true") + boolean storageRequired(); + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/FileStorage.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/FileStorage.java new file mode 100644 index 00000000000..5a3e233cc94 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/FileStorage.java @@ -0,0 +1,42 @@ +/* + * 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.dependencymanager.samples.dynamicdep.api; + +import java.io.Serializable; + +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class FileStorage implements Storage { + volatile LogService log; // injected + + @Override + public void store(String key, Serializable data) { + log.log(LogService.LOG_WARNING, "FileStorage.store(" + key + "," + data + ")"); + } + + @Override + public Serializable get(String key) { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/MapDBStorage.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/MapDBStorage.java new file mode 100644 index 00000000000..2a3d4b93da9 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/MapDBStorage.java @@ -0,0 +1,41 @@ +/* + * 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.dependencymanager.samples.dynamicdep.api; + +import java.io.Serializable; + +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class MapDBStorage implements Storage { + volatile LogService log; // injected + + @Override + public void store(String key, Serializable data) { + log.log(LogService.LOG_WARNING, "MapDBStorage.store(" + key + "," + data + ")"); + } + + @Override + public Serializable get(String key) { + // TODO Auto-generated method stub + return null; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/README b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/README new file mode 100644 index 00000000000..55b4e26207d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/README @@ -0,0 +1,43 @@ +/* + * 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. + */ + +This sample shows how to define a dynamic dependency from a Component's init method. + +Let's recap the lifecycle of a DM component: + +- the configuration (if any) is first injected (updated callback). +- then optional dependencies on class fields are injected. +- then all required dependencies are injected. +- then the init(Component c) method is invoked; And from there you are then able to add dynamic dependencies using any previously +injected services (either configuration injected in update method, or other injected services declared from the Activator). +- then the start callback is invoked when all required dependencies declared from the init method are injected. + +In this sample, the "DynamicDependency" Component defines in its "init" method a dynamic dependency on a Storage service. +But it first loads the "storage type" and "storage required" dependency informations from a Configuration PID +(see the "Dynamic Dependency Configuration" PID from webconsole), which is defined using Bnd MetaType Annotations. + +So, first, click on the "dynamicdep.api.bndrun" descriptor, and run it. +Then go to webconsole (http://localhost:8080/system/console/configMgr), and configure the "Dynamic Dependency Configuration" PID. +then just type "log warn" under the gogo shell: + +log warn + +Then you normally see something like: + +2016.02.08 21:54:16 WARNING - Bundle: org.apache.felix.dependencymanager.samples.dynamicdep.api - MapDBStorage.store(gabu,zo) +2016.02.08 21:54:16 WARNING - Bundle: org.apache.felix.dependencymanager.samples.dynamicdep.api - start +2016.02.08 21:54:16 WARNING - Bundle: org.apache.felix.dependencymanager.samples.dynamicdep.api - init: storage type=mapdb, storageRequired=true diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/Storage.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/Storage.java new file mode 100644 index 00000000000..8391cdcbb7d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/dynamicdep/api/Storage.java @@ -0,0 +1,29 @@ +/* + * 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.dependencymanager.samples.dynamicdep.api; + +import java.io.Serializable; + +/** + * @author Felix Project Team + */ +public interface Storage { + Serializable get(String key); + void store(String key, Serializable data); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/annot/README b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/annot/README new file mode 100644 index 00000000000..7f5569ceeaa --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/annot/README @@ -0,0 +1,30 @@ +/* + * 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. + */ + +This sample provides an example with one service consumer and a service provider, both declared +using DM Annotations. The ServiceConsumer is also depending on a configuration pid (see +org.apache.felix.dependencymanager.samples.conf.Configurator). + +Click on "hello.annot.bndrun" descriptor and run it. Then just type: + +g! log warn + +and you should see: + +2016.02.08 21:57:47 WARNING - Bundle: org.apache.felix.dependencymanager.samples.hello.annot - ServiceProviderImpl.hello +2016.02.08 21:57:47 WARNING - Bundle: org.apache.felix.dependencymanager.samples.hello.annot - ServiceConsumer.start: calling service.hello() ... + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/annot/ServiceConsumer.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/annot/ServiceConsumer.java new file mode 100644 index 00000000000..5f817109134 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/annot/ServiceConsumer.java @@ -0,0 +1,53 @@ +/* + * 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.dependencymanager.samples.hello.annot; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.ConfigurationDependency; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.apache.felix.dm.annotation.api.Start; +import org.osgi.service.log.LogService; + +/** + * Our service consumer. We depend on a ServiceProvider, and on a configuration. + * + * @author Felix Project Team + */ +@Component +public class ServiceConsumer { + @ServiceDependency + volatile ServiceProvider service; + + @ServiceDependency + volatile LogService log; + + ServiceConsumerConf conf; + + @ConfigurationDependency + protected void update(ServiceConsumerConf conf) { // type safe config + this.conf = conf; + } + + @Start + public void start() { + log.log(LogService.LOG_WARNING, "ServiceConsumer.start: configured key=" + conf.getKey()); + log.log(LogService.LOG_WARNING, "ServiceConsumer.start: calling service.hello() ..."); + this.service.hello(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/annot/ServiceConsumerConf.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/annot/ServiceConsumerConf.java new file mode 100644 index 00000000000..a26e5ccd12e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/annot/ServiceConsumerConf.java @@ -0,0 +1,28 @@ +/* + * 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.dependencymanager.samples.hello.annot; + +/** + * This is our type-safe configuration for the ServiceConsumer component. + * + * @author Felix Project Team + */ +public interface ServiceConsumerConf { + String getKey(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/annot/ServiceProvider.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/annot/ServiceProvider.java new file mode 100644 index 00000000000..40d81b27711 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/annot/ServiceProvider.java @@ -0,0 +1,28 @@ +/* + * 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.dependencymanager.samples.hello.annot; + +/** + * The interface for our service provider. + * + * @author Felix Project Team + */ +public interface ServiceProvider { + public void hello(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/annot/ServiceProviderImpl.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/annot/ServiceProviderImpl.java new file mode 100644 index 00000000000..eee8947a40a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/annot/ServiceProviderImpl.java @@ -0,0 +1,39 @@ +/* + * 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.dependencymanager.samples.hello.annot; + +import org.apache.felix.dm.annotation.api.Component; +import org.apache.felix.dm.annotation.api.ServiceDependency; +import org.osgi.service.log.LogService; + +/** + * The implementation for our service provider. + * + * @author Felix Project Team + */ +@Component +public class ServiceProviderImpl implements ServiceProvider { + @ServiceDependency + volatile LogService log; + + @Override + public void hello() { + log.log(LogService.LOG_WARNING, "ServiceProviderImpl.hello"); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/api/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/api/Activator.java new file mode 100644 index 00000000000..f688d940f14 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/api/Activator.java @@ -0,0 +1,43 @@ +/* + * 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.dependencymanager.samples.hello.api; + +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; +import org.osgi.service.log.LogService; + +/** + * @author Felix Project Team + */ +public class Activator extends DependencyActivatorBase { + @Override + public void init(BundleContext ctx, DependencyManager dm) throws Exception { + dm.add(createComponent() + .setImplementation(ServiceProviderImpl.class) + .add(createServiceDependency().setService(LogService.class).setRequired(true)) + .setInterface(ServiceProvider.class.getName(), null)); + + dm.add(createComponent() + .setImplementation(ServiceConsumer.class) + .add(createServiceDependency().setService(LogService.class).setRequired(true)) + .add(createConfigurationDependency().setCallback("updated", ServiceConsumerConf.class)) + .add(createServiceDependency().setService(ServiceProvider.class).setRequired(true))); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/api/README b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/api/README new file mode 100644 index 00000000000..05545f3d656 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/api/README @@ -0,0 +1,29 @@ +/* + * 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. + */ + +This sample provides a DM Activator declaring one service consumer and a service provider. The +ServiceConsumer is also depending on a configuration pid (see org.apache.felix.dependencymanager.samples.conf.Configurator). + +Click on "hello.api.bndrun" descriptor and run it. Then just type: + +g! log warn + +and you should see: + +2016.02.08 21:59:31 WARNING - Bundle: org.apache.felix.dependencymanager.samples.hello.api - ServiceProviderImpl.hello +2016.02.08 21:59:31 WARNING - Bundle: org.apache.felix.dependencymanager.samples.hello.api - ServiceConsumer.start: calling service.hello() + diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/api/ServiceConsumer.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/api/ServiceConsumer.java new file mode 100644 index 00000000000..b49fac057af --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/api/ServiceConsumer.java @@ -0,0 +1,43 @@ +/* + * 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.dependencymanager.samples.hello.api; + +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.log.LogService; + +/** + * Our service consumer. We depend on a ServiceProvider, and on a configuration. + * + * @author Felix Project Team + */ +public class ServiceConsumer { + volatile ServiceProvider service; + volatile LogService log; + ServiceConsumerConf conf; + + protected void updated(ServiceConsumerConf conf) throws ConfigurationException { + this.conf = conf; + } + + public void start() { + log.log(LogService.LOG_WARNING, "ServiceConsumer.start: configured key = " + conf.getKey()); + log.log(LogService.LOG_WARNING, "ServiceConsumer.start: calling service.hello()"); + this.service.hello(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/api/ServiceConsumerConf.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/api/ServiceConsumerConf.java new file mode 100644 index 00000000000..a464ff72ee2 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/api/ServiceConsumerConf.java @@ -0,0 +1,28 @@ +/* + * 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.dependencymanager.samples.hello.api; + +/** + * This is our type-safe configuration for the ServiceConsumer component. + * + * @author Felix Project Team + */ +public interface ServiceConsumerConf { + String getKey(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/api/ServiceProvider.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/api/ServiceProvider.java new file mode 100644 index 00000000000..e8d51913832 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/api/ServiceProvider.java @@ -0,0 +1,28 @@ +/* + * 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.dependencymanager.samples.hello.api; + +/** + * The interface for our service provider. + * + * @author Felix Project Team + */ +public interface ServiceProvider { + public void hello(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/api/ServiceProviderImpl.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/api/ServiceProviderImpl.java new file mode 100644 index 00000000000..400f67bb7dd --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/hello/api/ServiceProviderImpl.java @@ -0,0 +1,35 @@ +/* + * 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.dependencymanager.samples.hello.api; + +import org.osgi.service.log.LogService; + +/** + * The implementation for our service provider. + * + * @author Felix Project Team + */ +public class ServiceProviderImpl implements ServiceProvider { + volatile LogService log; + + @Override + public void hello() { + log.log(LogService.LOG_WARNING, "ServiceProviderImpl.hello"); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/tpool/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/tpool/Activator.java new file mode 100644 index 00000000000..7134c9caba3 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/tpool/Activator.java @@ -0,0 +1,52 @@ +/* + * 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.dependencymanager.samples.tpool; + +import java.util.Properties; + +import org.apache.felix.dependencymanager.samples.tpool.executor.ComponentExecutorFactoryImpl; +import org.apache.felix.dm.ComponentExecutorFactory; +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; + +/** + * See README file describing this Activator. + * + * @author Felix Project Team + */ +public class Activator extends DependencyActivatorBase { + @Override + public void init(BundleContext context, DependencyManager mgr) throws Exception { + mgr.add(createComponent() + .setInterface(ComponentExecutorFactory.class.getName(), null) + .setImplementation(ComponentExecutorFactoryImpl.class)); + + // Create two synchronous components + mgr.add(createComponent().setImplementation(new MyComponent("Component 1"))); + mgr.add(createComponent().setImplementation(new MyComponent("Component 2"))); + + // And two components which will be managed and started concurrently. + Properties properties = new Properties(); + properties.put("parallel", "true"); + + mgr.add(createComponent().setImplementation(new MyComponent("Parallel Component 3")).setServiceProperties(properties)); + mgr.add(createComponent().setImplementation(new MyComponent("Parallel Component 4")).setServiceProperties(properties)); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/tpool/MyComponent.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/tpool/MyComponent.java new file mode 100644 index 00000000000..d637db15db0 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/tpool/MyComponent.java @@ -0,0 +1,35 @@ +/* + * 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.dependencymanager.samples.tpool; + +/** + * A first component that is not handled in parallel. + * @author Felix Project Team + */ +public class MyComponent { + private final String m_name; + + MyComponent(String name) { + m_name = name; + } + + public void start() throws InterruptedException { + System.out.println("Starting Component " + m_name + " current thread=" + Thread.currentThread()); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/tpool/README b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/tpool/README new file mode 100644 index 00000000000..4e549fe1330 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/tpool/README @@ -0,0 +1,63 @@ +/* + * 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. + */ +The Activator you will find in this example registers a ComponentExecutorFactory in the OSGi service +registry to enable parallel activation of (some or all) components + +DM uses a whiteboard pattern approach in order to handle components concurrently: your application has to register a ComponentExecutorFactory in +the registry and DM will use it when deciding if components must be started concurrently (or not). By implementing yourself a ComponentExecutorFactory, +you are allowed to first decide if a given component should be started concurrently, and also choose the threadpool you like, +possibly a standard jdk threadpool, or some other advanced queuing libraries, like "hawtdispatch" ... + +The ComponentExecutorFactory has a single method: + + /** + * Returns an Executor used to handle and start the given component, or null if the component must be started synchronously. + */ + public Executor getExecutorFor(Component component) { + } + +To indicate that DM should "wait for" a ComponentExecutorFactory before starting to handle any components, then you have to declare +in the bundle context properties the following parameter: + + org.apache.felix.dependencymanager.parallelism=* + +Using the above property will ensure that DM cache any added DM components until ComponentExecutorFactory is available from the OSGi registry. +Hence, using the above property avoids you to use start level service in order to make sure all components are started concurrently, even if the +bundle containing your ComponentExecutorFactory service is started lastly. + +Finally, if you want to start all components in parallel, except some; then you can specify the list of packages to be excluded like this: + + org.apache.felix.dependencymanager.parallelism=!package.to.exclude,* + +Here, components having a package that is starting with "package.to.exclude" won't be started concurrently. + +Now, let's describe the example: it registers a ComponentExecutorFactory that only makes concurrent components +which provide a "parallel=true" service property (you can specify a service property even if the component does not register any services). +Since we define ComponentExecutorFactory using DM API, we also have to disable parallelism for it, by declaring its package in the following parameter from +the bundle context properties: + + org.apache.felix.dependencymanager.parallelism=!org.apache.felix.dependencymanager.samples.tpool.executor,* + +To start the example, click on "tpool.bndrun" descriptor and run it. +You will then see: + +Starting Component Component 1 current thread=Thread[main,5,main] +Starting Component Component 2 current thread=Thread[main,5,main] +Starting Component Parallel Component 3 current thread=Thread[pool-3-thread-1,5,main] +Starting Component Parallel Component 4 current thread=Thread[pool-3-thread-2,5,main] + +Here, the Component 3 and 4 are started in the threadpool, while the Component 2 is started synchronously from the main thread. \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/tpool/executor/ComponentExecutorFactoryImpl.java b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/tpool/executor/ComponentExecutorFactoryImpl.java new file mode 100644 index 00000000000..8518f17fbb7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/src/org/apache/felix/dependencymanager/samples/tpool/executor/ComponentExecutorFactoryImpl.java @@ -0,0 +1,50 @@ +/* + * 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.dependencymanager.samples.tpool.executor; + +import java.util.Dictionary; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentDeclaration; +import org.apache.felix.dm.ComponentExecutorFactory; + +/** + * @author Felix Project Team + */ +public class ComponentExecutorFactoryImpl implements ComponentExecutorFactory { + final static Executor m_threadPool = Executors.newFixedThreadPool(4); + + /** + * Make concurrent a component only if it has a "parallel=true" property. + */ + @Override + public Executor getExecutorFor(Component component) { + ComponentDeclaration decl = component.getComponentDeclaration(); + Dictionary properties = decl.getServiceProperties(); + if (properties != null && "true".equals(properties.get("parallel"))) { + // the component will be handled in the threadpool. + return m_threadPool; + } else { + // the component won't be handled in parallel. + return null; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/test/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.samples/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/tpool.bnd b/dependencymanager/org.apache.felix.dependencymanager.samples/tpool.bnd new file mode 100644 index 00000000000..809a5155163 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/tpool.bnd @@ -0,0 +1,22 @@ +# +# 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. +# +Private-Package: \ + org.apache.felix.dependencymanager.samples.tpool,\ + org.apache.felix.dependencymanager.samples.tpool.executor +Bundle-Activator: org.apache.felix.dependencymanager.samples.tpool.Activator +Bundle-Description: Dependency Manager threadpool used by examples +Bundle-Name: Dependency Manager Examples ThreadPool \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager.samples/tpool.bndrun b/dependencymanager/org.apache.felix.dependencymanager.samples/tpool.bndrun new file mode 100644 index 00000000000..7ee68cf2163 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.samples/tpool.bndrun @@ -0,0 +1,41 @@ +# +# 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. +# +-runfw: ${felix.framework} +-runee: JavaSE-1.8 +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active;skip:="osgi.service" + +-runbundles: \ + ${metatype},\ + ${log},\ + ${gogo},\ + ${configadmin},\ + ${eventadmin},\ + org.apache.felix.dependencymanager;version=latest,\ + org.apache.felix.dependencymanager.shell;version=latest,\ + org.apache.felix.dependencymanager.runtime;version=latest,\ + org.apache.felix.dependencymanager.samples.tpool;version=latest + +-runproperties: \ + org.apache.felix.dependencymanager.parallel='!org.apache.felix.dependencymanager.samples.tpool.executor, *',\ + org.apache.felix.dependencymanager.loglevel=2,\ + org.apache.felix.log.maxSize=100000,\ + org.apache.felix.log.storeDebug=true + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.shell/.classpath b/dependencymanager/org.apache.felix.dependencymanager.shell/.classpath new file mode 100644 index 00000000000..7a6fc254361 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.shell/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.shell/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.shell/.gitignore new file mode 100644 index 00000000000..90dde36e4ac --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.shell/.gitignore @@ -0,0 +1,3 @@ +/bin/ +/bin_test/ +/generated/ diff --git a/dependencymanager/org.apache.felix.dependencymanager.shell/.project b/dependencymanager/org.apache.felix.dependencymanager.shell/.project new file mode 100644 index 00000000000..bb43ab14295 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.shell/.project @@ -0,0 +1,23 @@ + + + org.apache.felix.dependencymanager.shell + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/dependencymanager/org.apache.felix.dependencymanager.shell/bnd.bnd b/dependencymanager/org.apache.felix.dependencymanager.shell/bnd.bnd new file mode 100644 index 00000000000..2eab932ba9e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.shell/bnd.bnd @@ -0,0 +1,37 @@ +# +# 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. +# +-buildpath: \ + ${mockito},\ + org.apache.felix.dependencymanager;version=latest,\ + ${junit},\ + osgi.core;version=6.0,\ + osgi.cmpn;version=6.0,\ + org.apache.felix.gogo.runtime;version=1.0 +Private-Package: \ + org.apache.felix.dm.shell +Bundle-Activator:org.apache.felix.dm.shell.Activator +Bundle-Version: 4.0.8 +Include-Resource: META-INF/=resources/,META-INF/changelog.txt=changelog.txt +Bundle-Name: Apache Felix Dependency Manager Shell +Bundle-Description: Gogo Shell commands for Apache Felix Dependency Manager +Bundle-Category: osgi +Bundle-License: http://www.apache.org/licenses/LICENSE-2.0.txt +Bundle-Vendor: The Apache Software Foundation +-testpath: \ + ${junit},\ + ${mockito},\ + org.objenesis;version=2.2 diff --git a/dependencymanager/org.apache.felix.dependencymanager.shell/changelog.txt b/dependencymanager/org.apache.felix.dependencymanager.shell/changelog.txt new file mode 100644 index 00000000000..f1c45b1da4c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.shell/changelog.txt @@ -0,0 +1,265 @@ +Release Notes - Felix - Version org.apache.felix.dependencymanager-r15 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.6.0 + * org.apache.felix.dependencymanager.shell; version=4.0.8 + * org.apache.felix.dependencymanager.runtime; version=4.0.7 + * org.apache.felix.dependencymanager.annotation; version=5.0.1 + * org.apache.felix.dependencymanager.lambda; version=1.2.1 + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r13 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.5.0 + * org.apache.felix.dependencymanager.shell; version=4.0.7 + * org.apache.felix.dependencymanager.runtime; version=4.0.6 + * org.apache.felix.dependencymanager.annotation; version=5.0.0 + * org.apache.felix.dependencymanager.lambda; version=1.2.0 + + +** Bug + * [FELIX-5683] - getServiceProperties returns null instead of empty dictionary + * [FELIX-5716] - Dead Lock in DM + * [FELIX-5768] - DM Lambda stop callback not being called + * [FELIX-5955] - Move changelog.txt to toplevel project dir + * [FELIX-5956] - NPE when invoking a lifecycle runnable method from init method + +** New Feature + * [FELIX-5336] - Add support for prototype scope services in DM4 + + +** Improvement + * [FELIX-5967] - DM does not support java9+ + * [FELIX-5937] - Refactor DM bndtools/gradle project + * [FELIX-5939] - DM annotations enhancements + * [FELIX-5941] - DM APi enhancements + * [FELIX-5957] - Check if a default implementation is used only on optional dependencies + + +** Task + * [FELIX-5960] - Do not supply MD5 checksum + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r11 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.4.1 + * org.apache.felix.dependencymanager.shell; version=4.0.6 + * org.apache.felix.dependencymanager.runtime; version=4.0.5 + * org.apache.felix.dependencymanager.annotation; version=4.2.1 + * org.apache.felix.dependencymanager.lambda; version=1.1.1 + +** Bug + * [FELIX-5630] - NullObject is created for a required dependency if the component is removed and added again to the dependency manager + * [FELIX-5636] - Component of aspect service does not have any service properties anymore + * [FELIX-5657] - DM released sources can't be rebuilt + +** Improvement + * [FELIX-5619] - MultiProperyFilterIndex memory consumption + * [FELIX-5623] - Improve performance of ComponentImpl.getName method + * [FELIX-5650] - Support latest version of Gogo + * [FELIX-5653] - Simplify DM-Lambda samples + * [FELIX-5658] - Include poms in dm artifacts + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r9 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.4.0 + * org.apache.felix.dependencymanager.shell; version=4.0.5 + * org.apache.felix.dependencymanager.runtime; version=4.0.4 + * org.apache.felix.dependencymanager.annotation; version=4.2.0 + * org.apache.felix.dependencymanager.lambda; version=1.1.0 + +** Bug + * [FELIX-5236] - Single @Property annotation on a type doesn't work + * [FELIX-5242] - Configuration updates may be missed when the component is restarting + * [FELIX-5244] - Can't inject service using a method ref on a parent class method. + * [FELIX-5245] - Typo in error logged when a component callback is not found. + * [FELIX-5268] - Service not unregistered while bundle is starting + * [FELIX-5273] - Wrong log when a callback is not found from component instance(s) + * [FELIX-5274] - remove callback fails after manually removing dynamic dependencies + * [FELIX-5399] - Unable to define default map or list config types + * [FELIX-5400] - Can't override default configuration type list value using an empty list + * [FELIX-5401] - Can't override default configuration type map value using an empty map + * [FELIX-5402] - Factory configuration adapter ignores factory method + * [FELIX-5411] - When you stop a component, the service references are not ungotten. + * [FELIX-5426] - Remove callbacks aren't called for optional dependencies in a "circular" dependency scenario + * [FELIX-5428] - Dependency events set not cleared when component is removed + * [FELIX-5429] - Aspect swap callback sometimes not called on optional dependencies + * [FELIX-5469] - Methodcache system size property is not used + * [FELIX-5471] - Ensure that unbound services are always handled synchronously + * [FELIX-5517] - @Inject annotation ignored when applied on ServiceRegistration + * [FELIX-5519] - services are not ungotten when swapped by an aspect + * [FELIX-5523] - required dependencies added to a started adapter (or aspect) are not injected + + + + +** Improvement + * [FELIX-5228] - Upgrade DM With latest release of BndTools + * [FELIX-5237] - Configurable invocation handler should use default method values + * [FELIX-5346] - Start annotation not propagated to sub classes + * [FELIX-5355] - Allow to use properties having dots with configuration proxies + * [FELIX-5403] - Improve the Javadoc for org.apache.felix.dm.ComponentStateListener + * [FELIX-5405] - Do not have org.apache.felix.dm.Logger invoke toString() of message parameters when enabled log level is not high enough + * [FELIX-5406] - DM lambda fluent service properties don't support dots + * [FELIX-5407] - DM annotation plugin generates temp log files even if logging is disabled + * [FELIX-5408] - Parallel DM should not stop components asynchronously + * [FELIX-5467] - MultiPropertyFilterIndex is unusable when a service reference contains a lot of values for one key + * [FELIX-5499] - Remove usage of json.org from dependency manager + * [FELIX-5515] - Upgrade DM to OSGi R6 API + * [FELIX-5516] - Allow to not dereference services internally + * [FELIX-5518] - Remove all eclipse warnings in DM code + * [FELIX-5520] - ComponentStateListener not supported in DM lambda + * [FELIX-5521] - add more callback method signature in DM lambda service dependency callbacks + * [FELIX-5522] - Refactor aspect service implementation + * [FELIX-5524] - add more signatures for aspect swap callbacks + * [FELIX-5526] - Allow to use generic custom DM dependencies when using dm lambda. + * [FELIX-5531] - Document dependency callback signatures + * [FELIX-5532] - Swap callback is missing in @ServiceDependency annotation + + + +** Task + * [FELIX-5533] - Fix a semantic versioning issue when releasing dependency manager + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r8 +====================================================================== + +** Bug + * [FELIX-5146] - Service adapters turn on autoconf even if callbacks are used + * [FELIX-5147] - Bundle Adapter auto configures class fields even if callbacks are used + * [FELIX-5153] - DM4 calls stop before ungetService() on ServiceFactory components + * [FELIX-5155] - Adapter/Aspect extra service dependencies injected twice if using callback instance + * [FELIX-5178] - Make some component parameters as volatile + * [FELIX-5181] - Only log info/debug if dm annotation log parameter is enabled + * [FELIX-5187] - No errog log when configuration dependency callback is not found + * [FELIX-5188] - No error log when a factory pid adapter update callback is not found + * [FELIX-5192] - ConfigurationDependency race condition when component is stopped + * [FELIX-5193] - Factory Pid Adapter race condition when component is stopped + * [FELIX-5200] - Factory configuration adapter not restarted + + +** New Feature + * [FELIX-4689] - Create a more fluent syntax for the dependency manager builder + + +** Improvement + * [FELIX-5126] - Build DM using Java 8 + * [FELIX-5164] - Add support for callback instance in Aspects + * [FELIX-5177] - Support injecting configuration proxies + * [FELIX-5180] - Support for Java8 Repeatable Properties in DM annotations. + * [FELIX-5182] - Cleanup DM samples + * [FELIX-5201] - Improve how components are displayed with gogo shell + + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r6 +====================================================================== + +** Bug + * [FELIX-4974] - DM filter indices not enabled if the dependencymanager bundle is started first + * [FELIX-5045] - DM Optional callbacks may sometimes be invoked before start callback + * [FELIX-5046] - Gradle wrapper is not included in DM source release + + + + +** Improvement + * [FELIX-4921] - Ensure binary equality of the same bundle between successive DM releases + * [FELIX-4922] - Simplify DM changelog management + * [FELIX-5054] - Clean-up instance bound dependencies when component is destroyed + * [FELIX-5055] - Upgrade DM to BndTools 3.0.0 + * [FELIX-5104] - Call a conf dependency callback Instance with an instantiated component + * [FELIX-5113] - Remove useless wrong test in ConfigurationDependencyImpl + * [FELIX-5114] - Schedule configuration update in Component executor synchronously + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r5: +====================================================================== + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r5 + + + +** Bug + * [FELIX-4907] - ConfigurationDependency calls updated(null) when component is stopped. + * [FELIX-4910] - ComponentExecutorFactory does not allow to return null from getExecutorFor method. + * [FELIX-4913] - DM Optional callbacks may sometimes be invoked twice + + + + +** Improvement + * [FELIX-4876] - DM Annotations bnd plugin compatibility with Bndtools 2.4.1 / 3.0.0 versions + * [FELIX-4877] - DM Annotations should detect service type using more method signatures. + * [FELIX-4915] - Skip unecessary manifest headers in DM bnd file + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r3: +===================================================================== + +** Bug + * [FELIX-4858] - DependencyManager: missing createCopy method in timed service dependency + * [FELIX-4869] - Callbacks not invoked for dependencies that are added after the component is initialized + + + + +** Improvement + * [FELIX-4614] - Factory create() method should have access to the component definition + * [FELIX-4873] - Enhance DM API to get missing and circular dependencies + * [FELIX-4878] - Support more signatures for Dependency callbacks + * [FELIX-4880] - Missing callback instance support for some adapters + * [FELIX-4889] - Refactor dm shell command to use the org.apache.dm.diagnostics api + + +** Wish + * [FELIX-4875] - Update DM integration test with latest ConfigAdmin + + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r2: +===================================================================== + +** Bug + * [FELIX-4832] - ClassCastException with autoconfig Iterable fields + * [FELIX-4833] - Revisit some javadocs in the DM annotations. + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r1: +====================================================================== + +** Bug + * [FELIX-4304] - DependencyManager ComponentImpl should not assume all service properties are stored in a Hashtable + * [FELIX-4394] - Race problems in DependencyManager Configuration Dependency + * [FELIX-4588] - createCopy method ConfigurationDependency produces a malfunctioning clone + * [FELIX-4594] - Propagation from dependencies overwrites service properties + * [FELIX-4598] - BundleDependency can effectively track only one bundle + * [FELIX-4602] - TemporalServiceDependency does not properly propagate RuntimeExceptions + * [FELIX-4709] - Incorrect Named Dependencies are binded to the Service Instance + + +** New Feature + * [FELIX-4426] - Allow DM to manage collections of services + * [FELIX-4807] - New thread model for Dependency Manager + + +** Improvement + * [FELIX-3914] - Log unsuccessful field injections + * [FELIX-4158] - ComponentDeclaration should give access to component information + * [FELIX-4667] - "top" command for the Dependency Manager Shell + * [FELIX-4672] - Allow callbacks to third party instance for adapters + * [FELIX-4673] - Log any error thrown when trying to create a null object. + * [FELIX-4777] - Dynamic initialization time configuration of @ConfigurationDependency + * [FELIX-4805] - Deprecate DM annotation metatypes + + +** Wish + * [FELIX-2706] - Support callback delegation for Configuration Dependecies + * [FELIX-4600] - Cherrypicking of propagated properties + * [FELIX-4676] - Add Provide-Capability for DependencyManager Runtime bundle + * [FELIX-4680] - Add more DM ServiceDependency callback signatures + * [FELIX-4683] - Allow to configure the DependencyManager shell scope + * [FELIX-4684] - Replace DependencyManager Runtime "factorySet" by a cleaner API + * [FELIX-4816] - bndtools-ify Dependency Manager + * [FELIX-4818] - New release process for Dependency Manager + diff --git a/dependencymanager/org.apache.felix.dependencymanager.shell/resources/DEPENDENCIES b/dependencymanager/org.apache.felix.dependencymanager.shell/resources/DEPENDENCIES new file mode 100644 index 00000000000..9bd2fd946cf --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.shell/resources/DEPENDENCIES @@ -0,0 +1,22 @@ +Apache Felix Dependency Manager Shell +Copyright 2011-2016 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +n/a + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2016). +Licensed under the Apache License 2.0. + +III. Overall License Summary + +- Apache License 2.0 + diff --git a/dependencymanager/org.apache.felix.dependencymanager.shell/resources/LICENSE b/dependencymanager/org.apache.felix.dependencymanager.shell/resources/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.shell/resources/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/dependencymanager/org.apache.felix.dependencymanager.shell/resources/NOTICE b/dependencymanager/org.apache.felix.dependencymanager.shell/resources/NOTICE new file mode 100644 index 00000000000..4946dbca0fc --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.shell/resources/NOTICE @@ -0,0 +1,7 @@ +Apache Felix Dependency Manager Shell +Copyright 2011-2016 The Apache Software Foundation + + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. diff --git a/dependencymanager/org.apache.felix.dependencymanager.shell/src/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.shell/src/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/org.apache.felix.dependencymanager.shell/src/org/apache/felix/dm/shell/Activator.java b/dependencymanager/org.apache.felix.dependencymanager.shell/src/org/apache/felix/dm/shell/Activator.java new file mode 100644 index 00000000000..d16305fecdd --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.shell/src/org/apache/felix/dm/shell/Activator.java @@ -0,0 +1,57 @@ +/* + * 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.dm.shell; + +import java.util.Hashtable; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +/** + * Bundle activator for the dependency manager shell command. + * + * @author Felix Project Team + */ +public class Activator implements BundleActivator { + /** + * You can configure the DM commands scope, by specifying this property in the bundle context. + */ + private final static String SCOPE = "org.apache.felix.dependencymanager.shell.scope"; + + /** + * Default gogo shell "scope" used. + */ + private final static String DEFAULT_SCOPE = "dependencymanager"; + + public void start(BundleContext context) throws Exception { + String scope = context.getProperty(SCOPE); + if (scope == null) { + scope = DEFAULT_SCOPE; + } + + Hashtable props = new Hashtable<>(); + props.put(org.apache.felix.service.command.CommandProcessor.COMMAND_SCOPE, scope); + props.put(org.apache.felix.service.command.CommandProcessor.COMMAND_FUNCTION, + new String[] { "dm" }); + context.registerService(DMCommand.class.getName(), new DMCommand(context), props); + } + + public void stop(BundleContext context) throws Exception { + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.shell/src/org/apache/felix/dm/shell/ComponentId.java b/dependencymanager/org.apache.felix.dependencymanager.shell/src/org/apache/felix/dm/shell/ComponentId.java new file mode 100644 index 00000000000..8d49ea70cc6 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.shell/src/org/apache/felix/dm/shell/ComponentId.java @@ -0,0 +1,100 @@ +/* + * 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.dm.shell; + +/** + * Unique identification of a component based on its name, type and bundle name. + * + * @author Felix Project Team + */ +public class ComponentId implements Comparable { + private final String name; + private final String type; + private final String bundleName; + + public ComponentId(String name, String type, String bundleName) { + super(); + this.name = name; + this.type = type; + this.bundleName = bundleName; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public String getBundleName() { + return bundleName; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((bundleName == null) ? 0 : bundleName.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ComponentId other = (ComponentId) obj; + if (bundleName == null) { + if (other.bundleName != null) + return false; + } + else if (!bundleName.equals(other.bundleName)) + return false; + if (name == null) { + if (other.name != null) + return false; + } + else if (!name.equals(other.name)) + return false; + if (type == null) { + if (other.type != null) + return false; + } + else if (!type.equals(other.type)) + return false; + return true; + } + + @Override + public String toString() { + return "ComponentId [name=" + name + ", type=" + type + ", bundleName=" + bundleName + "]"; + } + + public int compareTo(ComponentId o) { + // TODO it is common to have compareTo use the same fields that equals does + // if not for a good reason, document this + return name.compareTo(o.name); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.shell/src/org/apache/felix/dm/shell/DMCommand.java b/dependencymanager/org.apache.felix.dependencymanager.shell/src/org/apache/felix/dm/shell/DMCommand.java new file mode 100644 index 00000000000..e1ecb79f01d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.shell/src/org/apache/felix/dm/shell/DMCommand.java @@ -0,0 +1,682 @@ +/* + * 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.dm.shell; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentDeclaration; +import org.apache.felix.dm.ComponentDependencyDeclaration; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.diagnostics.CircularDependency; +import org.apache.felix.dm.diagnostics.DependencyGraph; +import org.apache.felix.dm.diagnostics.DependencyGraph.ComponentState; +import org.apache.felix.dm.diagnostics.DependencyGraph.DependencyState; +import org.apache.felix.dm.diagnostics.MissingDependency; +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Descriptor; +import org.apache.felix.service.command.Parameter; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; + +/** + * Shell command for showing all services and dependencies that are managed + * by the dependency manager. + * + * @author Felix Project Team + */ +@Descriptor("Commands used to dump all existing Dependency Manager components") +public class DMCommand { + /** + * Bundle context used to create OSGi filters. + */ + private final BundleContext m_context; + + /** + * Comparator used to compare component declarations based on their bundle ids + */ + private static final ComponentDeclarationComparator COMPONENT_DECLARATION_COMPARATOR = new ComponentDeclarationComparator(); + + /** + * Constant used by the wtf command, when listing missing services. + */ + private static final String SERVICE = "service"; + + /** + * Constant used by the wtf command, when listing missing configurations. + */ + private static final String CONFIGURATION = "configuration"; + + /** + * Constant used by the wtf command, when listing missing resource dependencies + */ + private static final String RESOURCE = "resource"; + + /** + * Constant used by the wtf command, when listing missing bundle dependencies + */ + private static final String BUNDLE = "bundle"; + + /** + * Name of a specific gogo shell variable, which may be used to configure "compact" mode. + * Example: g! dependencymanager.compact=true + */ + private final static String ENV_COMPACT = "dependencymanager.compact"; + + /** + * Name of a specific gogo shell variable, which may be used to configure an OSGi filter, normally + * passed to the "dm services" option. It is used to display only some service providing components + * matching the given filter. The filter can contain an "objectClass" option. + * Example: + * g! dependencymanager.services="(protocol=http)" + * g! dependencymanager.services="(&(objectClass=foo.Bar)(protocol=http))" + */ + private final static String ENV_SERVICES = "dependencymanager.services"; + + /** + * Name of a specific gogo shell variable, which may be used to configure a filter on the + * component implementation class name. + * The value of this shell variable may contain multiple regex (space separated), and each regex can + * be negated using "!". + * Example: g! dependencymanager.components="foo.bar.* ga.bu.zo.*" + */ + private final static String ENV_COMPONENTS = "dependencymanager.components"; + + /** + * Constructor. + */ + public DMCommand(BundleContext context) { + m_context = context; + } + + /** + * Dependency Manager "dm" command. We use gogo annotations, in order to automate documentation, + * and also to automatically manage optional flags/options and parameters ordering. + * + * @param session the gogo command session, used to get some variables declared in the shell + * This parameter is automatically passed by the gogo runtime. + * @param nodeps false means that dependencies are not displayed + * @param compact true means informations are displayed in a compact format. This parameter can also be + * set using the "dependencymanager.compact" gogo shell variable. + * @param notavail only unregistered components / unavailable dependencies are displayed + * @param stats true means some statistics are displayed + * @param services an osgi filter used to filter on some given osgi service properties. This parameter can also be + * set using the "dependencymanager.services" gogo shell variable. + * @param components a regular expression to match either component implementation class names. This parameter can also be + * set using the "dependencymanager.components" gogo shell variable. + * @param componentIds only components matching one of the specified components ids are displayed + * @param bundleIds a list of bundle ids or symbolic names, used to filter on some given bundles + */ + @Descriptor("List dependency manager components") + public void dm( + CommandSession session, + + @Descriptor("Hides component dependencies") + @Parameter(names = {"nodeps", "nd"}, presentValue = "true", absentValue = "false") + boolean nodeps, + + @Descriptor("Displays components using a compact form") + @Parameter(names = {"compact", "cp"}, presentValue = "true", absentValue = "") + String compact, + + @Descriptor("Only displays unavailable components") + @Parameter(names = {"notavail", "na"}, presentValue = "true", absentValue = "false") + boolean notavail, + + @Descriptor("Detects where are the root failures") + @Parameter(names = {"wtf"}, presentValue = "true", absentValue = "false") + boolean wtf, + + @Descriptor("Displays components statistics") + @Parameter(names = {"stats", "stat", "st"}, presentValue = "true", absentValue = "false") + boolean stats, + + @Descriptor("") + @Parameter(names = {"services", "s"}, absentValue = "") + String services, + + @Descriptor("") + @Parameter(names = {"components", "c"}, absentValue = "") + String components, + + @Descriptor("") + @Parameter(names = {"componentIds", "cid", "ci"}, absentValue = "") + String componentIds, + + @Descriptor("") + @Parameter(names = {"bundleIds", "bid", "bi", "b"}, absentValue = "") + String bundleIds, + + @Descriptor(" This command displays components callbacks (init/start) times>") + @Parameter(names = {"top"}, absentValue = "-1") + int top) throws Throwable + { + + boolean comp = Boolean.parseBoolean(getParam(session, ENV_COMPACT, compact)); + services = getParam(session, ENV_SERVICES, services); + String[] componentsRegex = getParams(session, ENV_COMPONENTS, components); + ArrayList bids = new ArrayList(); // list of bundle ids or bundle symbolic names + ArrayList cids = new ArrayList(); // list of component ids + + // Parse and check componentIds option + StringTokenizer tok = new StringTokenizer(componentIds, ", "); + while (tok.hasMoreTokens()) { + try { + cids.add(Long.parseLong(tok.nextToken())); + } catch (NumberFormatException e) { + System.out.println("Invalid value for componentIds option"); + return; + } + } + + // Parse services filter + Filter servicesFilter = null; + try { + if (services != null) { + servicesFilter = m_context.createFilter(services); + } + } catch (InvalidSyntaxException e) { + System.out.println("Invalid services OSGi filter: " + services); + e.printStackTrace(System.err); + return; + } + + // Parse and check bundleIds option + tok = new StringTokenizer(bundleIds, ", "); + while (tok.hasMoreTokens()) { + bids.add(tok.nextToken()); + } + + if (top != -1) { + showTopComponents(top); + return; + } + + if (wtf) { + wtf(); + return; + } + + DependencyGraph graph = null; + if(notavail) { + graph = DependencyGraph.getGraph(ComponentState.UNREGISTERED, DependencyState.ALL_UNAVAILABLE); + } else { + graph = DependencyGraph.getGraph(ComponentState.ALL, DependencyState.ALL); + } + + List allComponents = graph.getAllComponents(); + Collections.sort(allComponents, COMPONENT_DECLARATION_COMPARATOR); + long numberOfComponents = 0; + long numberOfDependencies = 0; + long lastBundleId = -1; + + for(ComponentDeclaration cd : allComponents) { + Bundle bundle = cd.getBundleContext().getBundle(); + if(!matchBundle(bundle, bids)) { + continue; + } + + Component component = (Component)cd; + String name = cd.getName(); + if (!mayDisplay(component, servicesFilter, componentsRegex, cids)) { + continue; + } + + numberOfComponents++; + long bundleId = bundle.getBundleId(); + if(lastBundleId != bundleId) { + lastBundleId = bundleId; + if (comp) { + System.out.println("[" + bundleId + "] " + compactName(bundle.getSymbolicName())); + } else { + System.out.println("[" + bundleId + "] " + bundle.getSymbolicName()); + } + } + if (comp) { + System.out.print(" [" + cd.getId() + "] " + compactName(name) + " " + + compactState(ComponentDeclaration.STATE_NAMES[cd.getState()])); + } else { + System.out.println(" [" + cd.getId() + "] " + name + " " + + ComponentDeclaration.STATE_NAMES[cd.getState()]); + } + + if(!nodeps) { + List dependencies = graph.getDependecies(cd); + if(!dependencies.isEmpty()) { + numberOfDependencies += dependencies.size(); + if (comp) { + System.out.print('('); + } + for(int j = 0; j < dependencies.size(); j ++) { + ComponentDependencyDeclaration dep = dependencies.get(j); + + String depName = dep.getName(); + String depType = dep.getType(); + int depState = dep.getState(); + + if (comp) { + if (j > 0) { + System.out.print(' '); + } + System.out.print(compactName(depName) + " " + compactState(depType) + " " + + compactState(ComponentDependencyDeclaration.STATE_NAMES[depState])); + } else { + System.out.println(" " + depName + " " + depType + " " + + ComponentDependencyDeclaration.STATE_NAMES[depState]); + } + + } + if (comp) { + System.out.print(')'); + } + } + } + if (comp) { + System.out.println(); + } + } + + if(stats) { + System.out.println("Statistics:"); + System.out.println(" - Dependency managers: " + DependencyManager.getDependencyManagers().size()); + System.out.println(" - Components: " + numberOfComponents); + if (!nodeps) { + System.out.println(" - Dependencies: " + numberOfDependencies); + } + } + + } + + /** + * Displays components callbacks (init/start/stop/destroy) elapsed time. + * The components are sorted (the most time consuming components are displayed first). + * @param max the max number of components to display (0 means all components) + */ + private void showTopComponents(int max) { + List components = new ArrayList<>(); + for (DependencyManager manager : DependencyManager.getDependencyManagers()) { + components.addAll(manager.getComponents()); + } + Collections.sort(components, new Comparator() { + @Override + public int compare(Component c1, Component c2) { + Map c1Times = c1.getComponentDeclaration().getCallbacksTime(); + Map c2Times = c2.getComponentDeclaration().getCallbacksTime(); + Long c1Start = c1Times.get("start"); + Long c2Start = c2Times.get("start"); + if (c1Start != null) { + if (c2Start != null) { + return c1Start > c2Start ? 1 : -1; + } else { + return 1; + } + } else { + if (c2Start != null) { + return -1; + } else { + return 0; + } + } + } + }); + + Collections.reverse(components); + + System.out.printf("%-100s %10s %10s%n%n", "Top components (sorted by start duration time)", "[init time]", "[start time]"); + + if (components.size() > 0) { + System.out.println(); + + max = max == 0 ? components.size() : Math.min(components.size(), max); + for (int i = 0 ; i < components.size() && i < max; i++) { + ComponentDeclaration decl = components.get(i).getComponentDeclaration(); + System.out.printf("%-100s %10d %10d%n", decl.getClassName(), + decl.getCallbacksTime().get("init"), decl.getCallbacksTime().get("start")); + } + } + } + + private boolean matchBundle(Bundle bundle, List ids) { + if (ids.size() == 0) { + return true; + } + + for (int i = 0; i < ids.size(); i ++) { + String id = ids.get(i); + try { + Long longId = Long.valueOf(id); + if (longId == bundle.getBundleId()) { + return true; + } + } catch (NumberFormatException e) { + // must match symbolic name + if (id.equals(bundle.getSymbolicName())) { + return true; + } + } + } + + return false; + } + + /** + * Returns the value of a command arg parameter, or from the gogo shell if the parameter is not passed to + * the command. + */ + private String getParam(CommandSession session, String param, String value) { + if (value != null && value.length() > 0) { + return value; + } + Object shellParamValue = session.get(param); + return shellParamValue != null ? shellParamValue.toString() : null; + } + + /** + * Returns the value of a command arg parameter, or from the gogo shell if the parameter is not passed to + * the command. The parameter value is meant to be a list of values separated by a blank or a comma. + * The values are split and returned as an array. + */ + private String[] getParams(CommandSession session, String name, String value) { + String values = null; + if (value == null || value.length() == 0) { + value = (String) session.get(name); + if (value != null) { + values = value; + } + } else { + values = value; + } + if (values == null) { + return new String[0]; + } + return values.trim().split(", "); + } + + /** + * Checks if a component can be displayed. We make a logical OR between the three following conditions: + * + * - the component service properties are matching a given service filter ("services" option) + * - the component implementation class name is matching some regex ("components" option) + * - the component declaration name is matching some regex ("names" option) + * + * If some component ids are provided, then the component must also match one of them. + */ + private boolean mayDisplay(Component component, Filter servicesFilter, String[] components, List componentIds) { + // Check component id + if (componentIds.size() > 0) { + long componentId = ((ComponentDeclaration) component).getId(); + if (componentIds.indexOf(componentId) == -1) { + return false; + } + } + + if (servicesFilter == null && components.length == 0) { + return true; + } + + // Check component service properties + boolean servicesMatches = servicesMatches(component, servicesFilter); + + // Check components regexs, which may match component implementation class name + boolean componentsMatches = componentMatches(((ComponentDeclaration) component).getClassName(), components); + + // Logical OR between service properties match and component service/impl match. + return servicesMatches || componentsMatches; + } + + /** + * Checks if a given filter is matching some service properties possibly provided by a component + */ + private boolean servicesMatches(Component component, Filter servicesFilter) { + boolean match = false; + if (servicesFilter != null) { + String[] services = ((ComponentDeclaration) component).getServices(); + if (services != null) { + Dictionary properties = component.getServiceProperties(); + if (properties == null) { + properties = new Hashtable(); + } + if (properties.get(Constants.OBJECTCLASS) == null) { + properties.put(Constants.OBJECTCLASS, services); + } + match = servicesFilter.match(properties); + } + } + return match; + } + + /** + * Checks if the component implementation class name (or some possible provided services) are matching + * some regular expressions. + */ + private boolean componentMatches(String description, String[] names) { + for (int i = 0; i < names.length; i ++) { + String name = names[i]; + boolean not = false; + if (name.startsWith("!")) { + name = name.substring(1); + not = true; + } + boolean match = false; + + if (description.matches(name)) { + match = true; + } + + if (not) { + match = !match; + } + + if (match) { + return true; + } + } + + return false; + } + + /** + * Compact names that look like state strings. State strings consist of + * one or more words. Each word will be shortened to the first letter, + * all letters concatenated and uppercased. + */ + private String compactState(String input) { + StringBuffer output = new StringBuffer(); + StringTokenizer st = new StringTokenizer(input); + while (st.hasMoreTokens()) { + output.append(st.nextToken().toUpperCase().charAt(0)); + } + return output.toString(); + } + + /** + * Compacts names that look like fully qualified class names. All packages + * will be shortened to the first letter, except for the last one. So + * something like "org.apache.felix.MyClass" will become "o.a.f.MyClass". + */ + private String compactName(String input) { + StringBuffer output = new StringBuffer(); + int lastIndex = 0; + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + switch (c) { + case '.' : + output.append(input.charAt(lastIndex)); + output.append('.'); + lastIndex = i + 1; + break; + case ' ' : + case ',' : + if (lastIndex < i) { + output.append(input.substring(lastIndex, i)); + } + output.append(c); + lastIndex = i + 1; + break; + default: + } + } + if (lastIndex < input.length()) { + output.append(input.substring(lastIndex)); + } + return output.toString(); + } + + public void wtf() { + + DependencyGraph graph = DependencyGraph.getGraph(ComponentState.UNREGISTERED, DependencyState.REQUIRED_UNAVAILABLE); + List unregisteredComponents = graph.getAllComponents(); + + if(unregisteredComponents.isEmpty()) { + System.out.println("No unregistered components found"); + } else { + String message = unregisteredComponents.size() + " unregistered components found"; + System.out.println(message); + System.out.println("----------------------------------------------------".substring(0, message.length())); + } + + listResolvedBundles(); + listInstalledBundles(); + + List circularDependencies = graph.getCircularDependencies(); + if(!circularDependencies.isEmpty()) { + System.out.println("Circular dependencies:"); + printCircularDependencies(circularDependencies); + } + + List missingConfigDependencies = graph.getMissingDependencies(CONFIGURATION); + if(!missingConfigDependencies.isEmpty()) { + System.out.println("The following configuration(s) are missing: "); + printMissingDependencies(missingConfigDependencies); + } + + List missingServiceDependencies = graph.getMissingDependencies(SERVICE); + if(!missingServiceDependencies.isEmpty()) { + System.out.println("The following service(s) are missing: "); + printMissingDependencies(missingServiceDependencies); + } + + + List missingResourceDependencies = graph.getMissingDependencies(RESOURCE); + if(!missingResourceDependencies.isEmpty()) { + System.out.println("The following resource(s) are missing: "); + printMissingDependencies(missingResourceDependencies); + } + + List missingBundleDependencies = graph.getMissingDependencies(BUNDLE); + if(!missingBundleDependencies.isEmpty()) { + System.out.println("The following bundle(s) are missing: "); + printMissingDependencies(missingBundleDependencies); + } + + List missingCustomDependencies = graph.getMissingCustomDependencies(); + if(!missingCustomDependencies.isEmpty()) { + System.out.println("The following custom dependency(ies) are missing: "); + printMissingCustomDependencies(missingCustomDependencies); + } + } + + private void printCircularDependencies(List circularDependencies) { + for(CircularDependency c : circularDependencies) { + System.out.print(" *"); + for(ComponentDeclaration cd : c.getComponents()) { + System.out.print(" -> " + cd.getName()); + } + System.out.println(); + } + } + + private void printMissingDependencies(List missingDependencies) { + for(MissingDependency m : missingDependencies) { + System.out.println(" * " + m.getName() + " for bundle " + m.getBundleName()); + } + } + + private void printMissingCustomDependencies(List missingDependencies) { + for(MissingDependency m : missingDependencies) { + System.out.println(" * " + m.getName() + "(" + m.getType() + ")" + " for bundle " + m.getBundleName()); + } + } + + private void listResolvedBundles() { + boolean areResolved = false; + for (Bundle b : m_context.getBundles()) { + if (b.getState() == Bundle.RESOLVED && !isFragment(b)) { + areResolved = true; + break; + } + } + if (areResolved) { + System.out.println("Please note that the following bundles are in the RESOLVED state:"); + for (Bundle b : m_context.getBundles()) { + if (b.getState() == Bundle.RESOLVED && !isFragment(b)) { + System.out.println(" * [" + b.getBundleId() + "] " + b.getSymbolicName()); + } + } + } + } + + private void listInstalledBundles() { + boolean areResolved = false; + for (Bundle b : m_context.getBundles()) { + if (b.getState() == Bundle.INSTALLED) { + areResolved = true; + break; + } + } + if (areResolved) { + System.out.println("Please note that the following bundles are in the INSTALLED state:"); + for (Bundle b : m_context.getBundles()) { + if (b.getState() == Bundle.INSTALLED) { + System.out.println(" * [" + b.getBundleId() + "] " + b.getSymbolicName()); + } + } + } + } + + private boolean isFragment(Bundle b) { + Dictionary headers = b.getHeaders(); + return headers.get("Fragment-Host") != null; + } + + public static class ComponentDeclarationComparator implements Comparator { + @Override + public int compare(ComponentDeclaration cd1, ComponentDeclaration cd2) { + long id1 = cd1.getBundleContext().getBundle().getBundleId(); + long id2 = cd2.getBundleContext().getBundle().getBundleId(); + if(id1 == id2) { + // sort by component id + long cid1 = cd1.getId(); + long cid2 = cd2.getId(); + return cid1 > cid2 ? 1 : -1; + } + return id1 > id2 ? 1 : -1; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager.shell/test/.gitignore b/dependencymanager/org.apache.felix.dependencymanager.shell/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/org.apache.felix.dependencymanager.shell/test/test/DMCommandTest.java b/dependencymanager/org.apache.felix.dependencymanager.shell/test/test/DMCommandTest.java new file mode 100644 index 00000000000..ccaf6040ac9 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager.shell/test/test/DMCommandTest.java @@ -0,0 +1,304 @@ +/* + * 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 test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.PrintStream; +import java.util.Hashtable; + +import javax.crypto.Cipher; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.shell.DMCommand; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; + +/** + * @author Felix Project Team + */ +public class DMCommandTest { + /** System output just used to debug **/ + private final static PrintStream OUT = + new PrintStream(new BufferedOutputStream(new FileOutputStream(FileDescriptor.out), 128)); + + /** Setup a ByteArrayOutputStream to capture the system out printlines */ + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + + private DependencyManager dm; + @Spy @InjectMocks private DMCommand dme; + @Mock private BundleContext m_bundleContext; + + @Before + public void setUp() throws Exception { + m_bundleContext = mock(BundleContext.class); + Bundle bundle = mock(Bundle.class); + when(m_bundleContext.getBundle()).thenReturn(bundle); + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(errContent)); + dm = new DependencyManager(m_bundleContext); + dme = new DMCommand(m_bundleContext); + } + + @After + public void cleanUp() { + System.setOut(null); + System.setErr(null); + } + + @Test + public void testWithoutAnyDependcyManagersShouldNotCrash() { + OUT.println("testWithoutAnyDependcyManagersShouldNotCrash"); + setupEmptyBundles(); + + dme.wtf(); + assertEquals("No unregistered components found\n", outContent.toString()); + } + + @Test + public void testASingleComponentShouldNotRegisterAsFailure() { + OUT.println("testASingleComponentShouldNotRegisterAsFailure"); + setupEmptyBundles(); + + dm.add(dm.createComponent() + .setImplementation(Object.class) + .setInterface(Object.class.getName(), null) + ); + dme.wtf(); + assertEquals("No unregistered components found\n", outContent.toString()); + } + + @Test + public void testComponentThatDependsOnAOtheComponentShouldRegisterAsFailure() { + OUT.println("testComponentThatDependsOnAOtheComponentShouldRegisterAsFailure"); + setupEmptyBundles(); + DependencyManager dm = new DependencyManager(m_bundleContext); + DependencyManager.getDependencyManagers().add(dm); + + Component component = dm.createComponent() + .setImplementation(Object.class) + .setInterface(Object.class.getName(), null) + .add(dm.createServiceDependency().setService(Math.class).setRequired(true)); + dm.add(component); + + dme.wtf(); + String output = outContent.toString(); + assertTrue(output.contains("1 unregistered")); + assertTrue(output.contains("java.lang.Math")); + + // remove the mess + dm.remove(component); + } + + @Test + public void testComponentThatHaveCycliclyDependencyOnAOtheComponentShouldRegisterAsFailure() { + OUT.println("testComponentThatHaveCycliclyDependencyOnAOtheComponentShouldRegisterAsFailure"); + setupEmptyBundles(); + DependencyManager dm = new DependencyManager(m_bundleContext); + DependencyManager.getDependencyManagers().add(dm); + + Component component1 = dm.createComponent() + .setImplementation(Cipher.class) + .setInterface(Cipher.class.getName(), null) + .add(dm.createServiceDependency().setService(Math.class).setRequired(true)); + dm.add(component1); + + Component component2 = dm.createComponent() + .setImplementation(Math.class) + .setInterface(Math.class.getName(), null) + .add(dm.createServiceDependency().setService(Cipher.class).setRequired(true)); + dm.add(component2); + + dme.wtf(); + String output = outContent.toString(); + assertTrue(output.contains("-> java.lang.Math -> javax.crypto.Cipher -> java.lang.Math") || + output.contains("-> javax.crypto.Cipher -> java.lang.Math -> javax.crypto.Cipher")); + + // remove the mess + dm.remove(component1); + dm.remove(component2); + } + + @Test + public void testCanFindRootFailure() { + OUT.println("testCanFindRootFailure"); + setupEmptyBundles(); + + Component component1 = dm.createComponent() + .setImplementation(Object.class) + .setInterface(Object.class.getName(), null) + .add(dm.createServiceDependency().setService(Math.class).setRequired(true)); + dm.add(component1); + + Component component2 = dm.createComponent() + .setImplementation(Math.class) + .setInterface(Math.class.getName(), null) + .add(dm.createServiceDependency().setService(String.class).setRequired(true)); + dm.add(component2); + + dme.wtf(); + String output = outContent.toString(); + assertTrue(output.contains("2 unregistered")); + assertTrue(output.contains("java.lang.String")); + + // remove the mess + dm.remove(component1); + dm.remove(component2); + } + + @Test + public void testCanFindRootFailureWithSecondair() { + OUT.println("testCanFindRootFailureWithSecondair"); + setupEmptyBundles(); + + Component component1 = dm.createComponent() + .setImplementation(Object.class) + .setInterface(Object.class.getName(), null) + .add(dm.createServiceDependency().setService(Math.class).setRequired(true)); + dm.add(component1); + + Component component2 = dm.createComponent() + .setImplementation(Math.class) + .setInterface(Math.class.getName(), null) + .add(dm.createServiceDependency().setService(Float.class).setRequired(true)); + dm.add(component2); + + Component component3 = dm.createComponent() + .setImplementation(Object.class) + .setInterface(new String[] {Object.class.getName(), Float.class.getName()}, null) + .add(dm.createServiceDependency().setService(String.class).setRequired(true)); + dm.add(component3); + + dme.wtf(); + String output = outContent.toString(); + assertTrue(output.contains("3 unregistered")); + assertTrue(output.contains("java.lang.String")); + assertFalse(output.contains("java.lang.Float")); + + // remove the mess + dm.remove(component1); + dm.remove(component2); + dm.remove(component3); + } + + @Test + public void testCanFindRootFailureWithTwoFailures() { + OUT.println("testCanFindRootFailureWithTwoFailures"); + setupEmptyBundles(); + + Component component1 = dm.createComponent() + .setImplementation(Object.class) + .setInterface(Object.class.getName(), null) + .add(dm.createServiceDependency().setService(Math.class).setRequired(true)) + .add(dm.createServiceDependency().setService(Long.class).setRequired(true)); + dm.add(component1); + + + dme.wtf(); + String output = outContent.toString(); + assertTrue(output.contains("1 unregistered")); + assertTrue(output.contains("java.lang.Math")); + assertTrue(output.contains("java.lang.Long")); + + // remove the mess + dm.remove(component1); + } + + @Test + public void testInstalledBundleListing() { + OUT.println("testInstalledBundleListing"); + Bundle bundle1 = mock(Bundle.class); + when(bundle1.getState()).thenReturn(Bundle.INSTALLED); + when(bundle1.getSymbolicName()).thenReturn("BadBundle"); + + setupBundles(bundle1); + + dme.wtf(); + String output = outContent.toString(); + assertTrue(output.contains("following bundles are in the INSTALLED")); + assertTrue(output.contains("[0] BadBundle")); + // Will print null if it gets bundle 2, that should not happen + assertFalse(output.contains("null")); + } + + @Test + public void testResolvedBundleListing() { + OUT.println("testResolvedBundleListing"); + Bundle bundle1 = mock(Bundle.class); + when(bundle1.getState()).thenReturn(Bundle.RESOLVED); + when(bundle1.getSymbolicName()).thenReturn("BadBundle"); + Hashtable headers = new Hashtable<>(); + when(bundle1.getHeaders()).thenReturn(headers); + + setupBundles(bundle1); + + dme.wtf(); + String output = outContent.toString(); + assertTrue(output.contains("following bundles are in the RESOLVED")); + assertTrue(output.contains("[0] BadBundle")); + assertFalse(output.contains("null")); + } + + @Test + public void testResolvedBundleListingButNoFragements() { + OUT.println("testResolvedBundleListingButNoFragements"); + Bundle bundle1 = mock(Bundle.class); + when(bundle1.getState()).thenReturn(Bundle.RESOLVED); + when(bundle1.getSymbolicName()).thenReturn("BadBundle"); + Hashtable headers = new Hashtable<>(); + headers.put("Fragment-Host", "some value"); + when(bundle1.getHeaders()).thenReturn(headers); + setupBundles(bundle1); + + dme.wtf(); + String output = outContent.toString(); + assertFalse(output.contains("following bundles are in the RESOLVED")); + // Will print null if it gets bundle 2, that should not happen + assertFalse(output.contains("null")); + } + + private void setupBundles( Bundle bundle1) { + Bundle bundle2 = mock(Bundle.class); + when(bundle2.getState()).thenReturn(Bundle.ACTIVE); + + when(m_bundleContext.getBundles()).thenReturn(new Bundle[] { bundle1, bundle2}); + } + + /** Sets up the bundle context without any bundles */ + private void setupEmptyBundles() { + when(m_bundleContext.getBundles()).thenReturn(new Bundle[] {}); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/.classpath b/dependencymanager/org.apache.felix.dependencymanager/.classpath new file mode 100644 index 00000000000..7a6fc254361 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager/.gitignore b/dependencymanager/org.apache.felix.dependencymanager/.gitignore new file mode 100644 index 00000000000..90dde36e4ac --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/.gitignore @@ -0,0 +1,3 @@ +/bin/ +/bin_test/ +/generated/ diff --git a/dependencymanager/org.apache.felix.dependencymanager/.project b/dependencymanager/org.apache.felix.dependencymanager/.project new file mode 100644 index 00000000000..8640bab88c7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/.project @@ -0,0 +1,23 @@ + + + org.apache.felix.dependencymanager + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/dependencymanager/org.apache.felix.dependencymanager/bnd.bnd b/dependencymanager/org.apache.felix.dependencymanager/bnd.bnd new file mode 100644 index 00000000000..436bf468276 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/bnd.bnd @@ -0,0 +1,53 @@ +# +# 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. +# +-buildpath: \ + ${mockito},\ + ${junit},\ + osgi.core;version=6.0,\ + osgi.cmpn;version=6.0,\ + osgi.annotation;version=6.0 +Private-Package: \ + org.apache.felix.dm.impl,\ + org.apache.felix.dm.impl.index,\ + org.apache.felix.dm.impl.index.multiproperty,\ + org.apache.felix.dm.impl.metatype +Export-Package: \ + org.apache.felix.dm,\ + org.apache.felix.dm.compat,\ + org.apache.felix.dm.tracker,\ + org.apache.felix.dm.context,\ + org.apache.felix.dm.diagnostics +Include-Resource: META-INF/=resources/,META-INF/changelog.txt=changelog.txt +Import-Package: !org.junit,!org.mockito.*,org.osgi.framework;version="[1.8, 2)",* +Bundle-Activator: org.apache.felix.dm.impl.Activator +Bundle-Version: 4.6.0 +Bundle-Name: Apache Felix Dependency Manager +Bundle-Description: Provides dynamic service and component dependency management +Bundle-License: http://www.apache.org/licenses/LICENSE-2.0.txt +Bundle-DocURL: http://felix.apache.org/documentation/subprojects/apache-felix-dependency-manager.html +Bundle-Vendor: The Apache Software Foundation +Bundle-Category: osgi + +-testpath: \ + ${junit},\ + ${mockito},\ + org.objenesis;version=2.2 + + + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager/changelog.txt b/dependencymanager/org.apache.felix.dependencymanager/changelog.txt new file mode 100644 index 00000000000..6d49062a457 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/changelog.txt @@ -0,0 +1,281 @@ +Release Notes - Felix - Version org.apache.felix.dependencymanager-r15 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.6.0 + * org.apache.felix.dependencymanager.shell; version=4.0.8 + * org.apache.felix.dependencymanager.runtime; version=4.0.7 + * org.apache.felix.dependencymanager.annotation; version=5.0.1 + * org.apache.felix.dependencymanager.lambda; version=1.2.1 + +** Task + * [FELIX-5996] - Remove generic parameter in DM Component interface + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r14 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.5.1 + * org.apache.felix.dependencymanager.shell; version=4.0.7 + * org.apache.felix.dependencymanager.runtime; version=4.0.6 + * org.apache.felix.dependencymanager.annotation; version=5.0.1 + * org.apache.felix.dependencymanager.lambda; version=1.2.0 + +** Bug + * [FELIX-5990] - DM ServiceTracker memory leak + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r13 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.5.0 + * org.apache.felix.dependencymanager.shell; version=4.0.7 + * org.apache.felix.dependencymanager.runtime; version=4.0.6 + * org.apache.felix.dependencymanager.annotation; version=5.0.0 + * org.apache.felix.dependencymanager.lambda; version=1.2.0 + + +** Bug + * [FELIX-5683] - getServiceProperties returns null instead of empty dictionary + * [FELIX-5716] - Dead Lock in DM + * [FELIX-5768] - DM Lambda stop callback not being called + * [FELIX-5955] - Move changelog.txt to toplevel project dir + * [FELIX-5956] - NPE when invoking a lifecycle runnable method from init method + +** New Feature + * [FELIX-5336] - Add support for prototype scope services in DM4 + + +** Improvement + * [FELIX-5967] - DM does not support java9+ + * [FELIX-5937] - Refactor DM bndtools/gradle project + * [FELIX-5939] - DM annotations enhancements + * [FELIX-5941] - DM APi enhancements + * [FELIX-5957] - Check if a default implementation is used only on optional dependencies + + +** Task + * [FELIX-5960] - Do not supply MD5 checksum + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r11 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.4.1 + * org.apache.felix.dependencymanager.shell; version=4.0.6 + * org.apache.felix.dependencymanager.runtime; version=4.0.5 + * org.apache.felix.dependencymanager.annotation; version=4.2.1 + * org.apache.felix.dependencymanager.lambda; version=1.1.1 + +** Bug + * [FELIX-5630] - NullObject is created for a required dependency if the component is removed and added again to the dependency manager + * [FELIX-5636] - Component of aspect service does not have any service properties anymore + * [FELIX-5657] - DM released sources can't be rebuilt + +** Improvement + * [FELIX-5619] - MultiProperyFilterIndex memory consumption + * [FELIX-5623] - Improve performance of ComponentImpl.getName method + * [FELIX-5650] - Support latest version of Gogo + * [FELIX-5653] - Simplify DM-Lambda samples + * [FELIX-5658] - Include poms in dm artifacts + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r9 +====================================================================== + +** List of bundles being part of the release: + * org.apache.felix.dependencymanager; version=4.4.0 + * org.apache.felix.dependencymanager.shell; version=4.0.5 + * org.apache.felix.dependencymanager.runtime; version=4.0.4 + * org.apache.felix.dependencymanager.annotation; version=4.2.0 + * org.apache.felix.dependencymanager.lambda; version=1.1.0 + +** Bug + * [FELIX-5236] - Single @Property annotation on a type doesn't work + * [FELIX-5242] - Configuration updates may be missed when the component is restarting + * [FELIX-5244] - Can't inject service using a method ref on a parent class method. + * [FELIX-5245] - Typo in error logged when a component callback is not found. + * [FELIX-5268] - Service not unregistered while bundle is starting + * [FELIX-5273] - Wrong log when a callback is not found from component instance(s) + * [FELIX-5274] - remove callback fails after manually removing dynamic dependencies + * [FELIX-5399] - Unable to define default map or list config types + * [FELIX-5400] - Can't override default configuration type list value using an empty list + * [FELIX-5401] - Can't override default configuration type map value using an empty map + * [FELIX-5402] - Factory configuration adapter ignores factory method + * [FELIX-5411] - When you stop a component, the service references are not ungotten. + * [FELIX-5426] - Remove callbacks aren't called for optional dependencies in a "circular" dependency scenario + * [FELIX-5428] - Dependency events set not cleared when component is removed + * [FELIX-5429] - Aspect swap callback sometimes not called on optional dependencies + * [FELIX-5469] - Methodcache system size property is not used + * [FELIX-5471] - Ensure that unbound services are always handled synchronously + * [FELIX-5517] - @Inject annotation ignored when applied on ServiceRegistration + * [FELIX-5519] - services are not ungotten when swapped by an aspect + * [FELIX-5523] - required dependencies added to a started adapter (or aspect) are not injected + + + + +** Improvement + * [FELIX-5228] - Upgrade DM With latest release of BndTools + * [FELIX-5237] - Configurable invocation handler should use default method values + * [FELIX-5346] - Start annotation not propagated to sub classes + * [FELIX-5355] - Allow to use properties having dots with configuration proxies + * [FELIX-5403] - Improve the Javadoc for org.apache.felix.dm.ComponentStateListener + * [FELIX-5405] - Do not have org.apache.felix.dm.Logger invoke toString() of message parameters when enabled log level is not high enough + * [FELIX-5406] - DM lambda fluent service properties don't support dots + * [FELIX-5407] - DM annotation plugin generates temp log files even if logging is disabled + * [FELIX-5408] - Parallel DM should not stop components asynchronously + * [FELIX-5467] - MultiPropertyFilterIndex is unusable when a service reference contains a lot of values for one key + * [FELIX-5499] - Remove usage of json.org from dependency manager + * [FELIX-5515] - Upgrade DM to OSGi R6 API + * [FELIX-5516] - Allow to not dereference services internally + * [FELIX-5518] - Remove all eclipse warnings in DM code + * [FELIX-5520] - ComponentStateListener not supported in DM lambda + * [FELIX-5521] - add more callback method signature in DM lambda service dependency callbacks + * [FELIX-5522] - Refactor aspect service implementation + * [FELIX-5524] - add more signatures for aspect swap callbacks + * [FELIX-5526] - Allow to use generic custom DM dependencies when using dm lambda. + * [FELIX-5531] - Document dependency callback signatures + * [FELIX-5532] - Swap callback is missing in @ServiceDependency annotation + + + +** Task + * [FELIX-5533] - Fix a semantic versioning issue when releasing dependency manager + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r8 +====================================================================== + +** Bug + * [FELIX-5146] - Service adapters turn on autoconf even if callbacks are used + * [FELIX-5147] - Bundle Adapter auto configures class fields even if callbacks are used + * [FELIX-5153] - DM4 calls stop before ungetService() on ServiceFactory components + * [FELIX-5155] - Adapter/Aspect extra service dependencies injected twice if using callback instance + * [FELIX-5178] - Make some component parameters as volatile + * [FELIX-5181] - Only log info/debug if dm annotation log parameter is enabled + * [FELIX-5187] - No errog log when configuration dependency callback is not found + * [FELIX-5188] - No error log when a factory pid adapter update callback is not found + * [FELIX-5192] - ConfigurationDependency race condition when component is stopped + * [FELIX-5193] - Factory Pid Adapter race condition when component is stopped + * [FELIX-5200] - Factory configuration adapter not restarted + + +** New Feature + * [FELIX-4689] - Create a more fluent syntax for the dependency manager builder + + +** Improvement + * [FELIX-5126] - Build DM using Java 8 + * [FELIX-5164] - Add support for callback instance in Aspects + * [FELIX-5177] - Support injecting configuration proxies + * [FELIX-5180] - Support for Java8 Repeatable Properties in DM annotations. + * [FELIX-5182] - Cleanup DM samples + * [FELIX-5201] - Improve how components are displayed with gogo shell + + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r6 +====================================================================== + +** Bug + * [FELIX-4974] - DM filter indices not enabled if the dependencymanager bundle is started first + * [FELIX-5045] - DM Optional callbacks may sometimes be invoked before start callback + * [FELIX-5046] - Gradle wrapper is not included in DM source release + + + + +** Improvement + * [FELIX-4921] - Ensure binary equality of the same bundle between successive DM releases + * [FELIX-4922] - Simplify DM changelog management + * [FELIX-5054] - Clean-up instance bound dependencies when component is destroyed + * [FELIX-5055] - Upgrade DM to BndTools 3.0.0 + * [FELIX-5104] - Call a conf dependency callback Instance with an instantiated component + * [FELIX-5113] - Remove useless wrong test in ConfigurationDependencyImpl + * [FELIX-5114] - Schedule configuration update in Component executor synchronously + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r5: +====================================================================== + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r5 + + + +** Bug + * [FELIX-4907] - ConfigurationDependency calls updated(null) when component is stopped. + * [FELIX-4910] - ComponentExecutorFactory does not allow to return null from getExecutorFor method. + * [FELIX-4913] - DM Optional callbacks may sometimes be invoked twice + + + + +** Improvement + * [FELIX-4876] - DM Annotations bnd plugin compatibility with Bndtools 2.4.1 / 3.0.0 versions + * [FELIX-4877] - DM Annotations should detect service type using more method signatures. + * [FELIX-4915] - Skip unecessary manifest headers in DM bnd file + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r3: +===================================================================== + +** Bug + * [FELIX-4858] - DependencyManager: missing createCopy method in timed service dependency + * [FELIX-4869] - Callbacks not invoked for dependencies that are added after the component is initialized + + + + +** Improvement + * [FELIX-4614] - Factory create() method should have access to the component definition + * [FELIX-4873] - Enhance DM API to get missing and circular dependencies + * [FELIX-4878] - Support more signatures for Dependency callbacks + * [FELIX-4880] - Missing callback instance support for some adapters + * [FELIX-4889] - Refactor dm shell command to use the org.apache.dm.diagnostics api + + +** Wish + * [FELIX-4875] - Update DM integration test with latest ConfigAdmin + + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r2: +===================================================================== + +** Bug + * [FELIX-4832] - ClassCastException with autoconfig Iterable fields + * [FELIX-4833] - Revisit some javadocs in the DM annotations. + +Release Notes - Felix - Version org.apache.felix.dependencymanager-r1: +====================================================================== + +** Bug + * [FELIX-4304] - DependencyManager ComponentImpl should not assume all service properties are stored in a Hashtable + * [FELIX-4394] - Race problems in DependencyManager Configuration Dependency + * [FELIX-4588] - createCopy method ConfigurationDependency produces a malfunctioning clone + * [FELIX-4594] - Propagation from dependencies overwrites service properties + * [FELIX-4598] - BundleDependency can effectively track only one bundle + * [FELIX-4602] - TemporalServiceDependency does not properly propagate RuntimeExceptions + * [FELIX-4709] - Incorrect Named Dependencies are binded to the Service Instance + + +** New Feature + * [FELIX-4426] - Allow DM to manage collections of services + * [FELIX-4807] - New thread model for Dependency Manager + + +** Improvement + * [FELIX-3914] - Log unsuccessful field injections + * [FELIX-4158] - ComponentDeclaration should give access to component information + * [FELIX-4667] - "top" command for the Dependency Manager Shell + * [FELIX-4672] - Allow callbacks to third party instance for adapters + * [FELIX-4673] - Log any error thrown when trying to create a null object. + * [FELIX-4777] - Dynamic initialization time configuration of @ConfigurationDependency + * [FELIX-4805] - Deprecate DM annotation metatypes + + +** Wish + * [FELIX-2706] - Support callback delegation for Configuration Dependecies + * [FELIX-4600] - Cherrypicking of propagated properties + * [FELIX-4676] - Add Provide-Capability for DependencyManager Runtime bundle + * [FELIX-4680] - Add more DM ServiceDependency callback signatures + * [FELIX-4683] - Allow to configure the DependencyManager shell scope + * [FELIX-4684] - Replace DependencyManager Runtime "factorySet" by a cleaner API + * [FELIX-4816] - bndtools-ify Dependency Manager + * [FELIX-4818] - New release process for Dependency Manager + diff --git a/dependencymanager/org.apache.felix.dependencymanager/design.txt b/dependencymanager/org.apache.felix.dependencymanager/design.txt new file mode 100644 index 00000000000..94dfce7bb60 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/design.txt @@ -0,0 +1,47 @@ +7 feb 2014 (marrs & uiterlix): + +This prototype demonstrates the new concurrency principles that form the basis for the DM: + + * All external events that influence the state of dependencies are recorded and given + to the serial executor of the component. We record whatever data comes in, so when the + actual job is run by the serial executor, we still have access to the original data + without having to access other sources whose state might have changed since. + * The serial executor of a component will execute a job immediately if it is being called + by the thread that is already executing jobs. + * If the serial executor of a component had not yet started a job, it will queue and start + it on the current thread. + * If the serial executor gets invoked from a different thread than the one currently + executing jobs, the job will be put at the end of the queue. As mentioned before, any + data associated with the event will also be recorded so it is available when the job + executes. + * State in the component and dependency can only be modified via the serial executor + thread. This means we don't need explicit synchronization anywhere. + +20 sept 2014 (pderop): + + * Added support for parallelism: To allow components to be started and handled in parallel, you can + now register in the OSGi service registry a ComponentExecutorFactory service that is used to get + an Executor for the management of all components dependencies/lifecycle callbacks. See javadoc + from the org.apache.felix.dm.ComponentExecutorFactory interface for more informations. + + You can also take a look at the the org.apache.felix.dependencymanager.samples project, which is + registering a ComponentExecutorFactory from org.apache.felix.dependencymanager.samples.tpool + bundle. + + See also the following property in the org.apache.felix.dependencymanager.samples/bnd.bnd + + org.apache.felix.dependencymanager.parallel=\ + '!org.apache.felix.dependencymanager.samples.tpool, *',\ + + Here, all components will be handled by Executors provided by the ComponentExecutorFactory, + except those having a package starting with "org.apache.felix.dependencymanager.samples.tpool" + (because the threadpool is itself defined using the Dependency Manager API). + + + + + + + + + diff --git a/dependencymanager/org.apache.felix.dependencymanager/resources/DEPENDENCIES b/dependencymanager/org.apache.felix.dependencymanager/resources/DEPENDENCIES new file mode 100644 index 00000000000..9fdadf31c7a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/resources/DEPENDENCIES @@ -0,0 +1,24 @@ +Apache Felix Dependency Manager +Copyright 2011-2017 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2016). +Licensed under the Apache License 2.0. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2016). +Licensed under the Apache License 2.0. + +III. Overall License Summary + +- Apache License 2.0 diff --git a/dependencymanager/org.apache.felix.dependencymanager/resources/LICENSE b/dependencymanager/org.apache.felix.dependencymanager/resources/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/resources/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/dependencymanager/org.apache.felix.dependencymanager/resources/NOTICE b/dependencymanager/org.apache.felix.dependencymanager/resources/NOTICE new file mode 100644 index 00000000000..195d532289c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/resources/NOTICE @@ -0,0 +1,12 @@ +Apache Felix Dependency Manager +Copyright 2011-2017 The Apache Software Foundation + + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2016). +Licensed under the Apache License 2.0. diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/.gitignore b/dependencymanager/org.apache.felix.dependencymanager/src/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/AdapterComponent.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/AdapterComponent.java new file mode 100644 index 00000000000..c583fd1ce88 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/AdapterComponent.java @@ -0,0 +1,391 @@ +/* + * 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.dm; + +import java.util.Dictionary; + +import org.apache.felix.dm.Component.ServiceScope; + +/** + * Interface used to configure the various parameters needed when defining + * a Dependency Manager adapter component. + * + * Adapters, like {@link AspectComponent}, are used to "extend" + * existing services, and can publish different services based on the existing one. + * An example would be implementing a management interface for an existing service, etc .... + *

      When you create an adapter component, it will be applied + * to any service that matches the implemented interface and filter. The adapter will be registered + * with the specified interface and existing properties from the original service plus any extra + * properties you supply here. If you declare the original service as a member it will be injected. + * + *

      Usage Examples

      + * + * Here is a sample showing a HelloServlet adapter component which creates a servlet each time a HelloService is registered in the + * osgi service registry with the "foo=bar" service property. + * + *
      + * {@code
      + * public class Activator extends DependencyActivatorBase {
      + *     &Override
      + *     public void init(BundleContext context, DependencyManager dm) throws Exception {
      + *         AdapterComponent adapterComponent = createAdapterComponent()
      + *             .setAdaptee(HelloService.class, "(foo=bar)")
      + *             .setInterface(HttpServlet.class.getName(), null)
      + *             .setImplementation(HelloServlet.class);
      + *         dm.add(adapterComponent);
      + *     }
      + * }
      + * 
      + * public interface HelloService {
      + *     String sayHello();
      + * }
      + * 
      + * public class HelloServlet extends HttpServlet {
      + *     volatile HelloService adatpee; // injected
      + *     
      + *     void doGet(HttpServletRequest req, HttpServletResponse resp) {
      + *         ...
      + *         resp.getWriter().println(adaptee.sayHello());
      + *     }
      + * }
      + * } 
      + * + *

      When you use callbacks to get injected with the adaptee service, the "add", "change", "remove" callbacks + * support the following method signatures: + * + *

      {@code
      + * (Component comp, ServiceReference ref, Service service)
      + * (Component comp, ServiceReference ref, Object service)
      + * (Component comp, ServiceReference ref)
      + * (Component comp, Service service)
      + * (Component comp, Object service)
      + * (Component comp)
      + * (Component comp, Map properties, Service service)
      + * (ServiceReference ref, Service service)
      + * (ServiceReference ref, Object service)
      + * (ServiceReference ref)
      + * (Service service)
      + * (Service service, Map propeerties)
      + * (Map properties, Service, service)
      + * (Service service, Dictionary properties)
      + * (Dictionary properties, Service service)
      + * (Object service)
      + * }
      + * + *

      For "swap" callbacks, the following method signatures are supported: + * + *

      {@code
      + * (Service old, Service replace)
      + * (Object old, Object replace)
      + * (ServiceReference old, Service old, ServiceReference replace, Service replace)
      + * (ServiceReference old, Object old, ServiceReference replace, Object replace)
      + * (Component comp, Service old, Service replace)
      + * (Component comp, Object old, Object replace)
      + * (Component comp, ServiceReference old, Service old, ServiceReference replace, Service replace)
      + * (Component comp, ServiceReference old, Object old, ServiceReference replace, Object replace)
      + * (ServiceReference old, ServiceReference replace)
      + * (Component comp, ServiceReference old, ServiceReference replace)
      + * }
      + * + * @see DependencyManager#createAdapterComponent() + */ +public interface AdapterComponent extends Component { + + /** + * Sets the component scope. + * @param scope the component scope (default=SINGLETON) + * + * @return this component + */ + AdapterComponent setScope(ServiceScope scope); + + /** + * Sets the implementation for this component. You can actually specify + * an instance you have instantiated manually, or a Class + * that will be instantiated using its default constructor when the + * required dependencies are resolved, effectively giving you a lazy + * instantiation mechanism. + * + * There are four special methods that are called when found through + * reflection to give you life cycle management options: + *
        + *
      1. init() is invoked after the instance has been + * created and dependencies have been resolved, and can be used to + * initialize the internal state of the instance or even to add more + * dependencies based on runtime state
      2. + *
      3. start() is invoked right before the service is + * registered
      4. + *
      5. stop() is invoked right after the service is + * unregistered
      6. + *
      7. destroy() is invoked after all dependencies are + * removed
      8. + *
      + * In short, this allows you to initialize your instance before it is + * registered, perform some post-initialization and pre-destruction code + * as well as final cleanup. If a method is not defined, it simply is not + * called, so you can decide which one(s) you need. If you need even more + * fine-grained control, you can register as a service state listener too. + * + * @param implementation the implementation + * @return this component + * @see ComponentStateListener + */ + AdapterComponent setImplementation(Object implementation); + + /** + * Adds dependency(ies) to this component, atomically. If the component is already active or if you add + * dependencies from the init method, then you should add all the dependencies in one single add method call + * (using the varargs argument), because this method may trigger component activation (like + * the ServiceTracker.open() method does). + * + * @param dependencies the dependencies to add. + * @return this component + */ + AdapterComponent add(Dependency ... dependencies); + + /** + * Removes a dependency from the component. + * @param d the dependency to remove + * @return this component + */ + AdapterComponent remove(Dependency d); + + /** + * Adds a component state listener to this component. + * + * @param listener the state listener + */ + AdapterComponent add(ComponentStateListener listener); + + /** + * Removes a component state listener from this component. + * + * @param listener the state listener + */ + AdapterComponent remove(ComponentStateListener listener); + + /** + * Sets the public interface under which this component should be registered + * in the OSGi service registry. + * + * @param serviceName the name of the service interface + * @param properties the properties for this service + * @return this component + */ + AdapterComponent setInterface(String serviceName, Dictionary properties); + + /** + * Sets the public interfaces under which this component should be registered + * in the OSGi service registry. + * + * @param serviceNames the names of the service interface + * @param properties the properties for these services + * @return this component + */ + AdapterComponent setInterface(String[] serviceNames, Dictionary properties); + + /** + * Sets the public interface under which this component should be registered + * in the OSGi service registry. + * + * @param serviceName the name of the service interface + * @param properties the properties for this service + * @return this component + */ + AdapterComponent setInterface(Class serviceName, Dictionary properties); + + /** + * Sets the public interfaces under which this component should be registered + * in the OSGi service registry. + * + * @param serviceNames the names of the service interface + * @param properties the properties for these services + * @return this component + */ + AdapterComponent setInterface(Class[] serviceNames, Dictionary properties); + + /** + * Configures auto configuration of injected classes in the component instance. + * The following injections are currently performed, unless you explicitly + * turn them off: + *
      + *
      BundleContext
      the bundle context of the bundle
      + *
      ServiceRegistration
      the service registration used to register your service
      + *
      DependencyManager
      the dependency manager instance
      + *
      Component
      the component instance of the dependency manager
      + *
      + * + * @param clazz the class (from the list above) + * @param autoConfig false to turn off auto configuration + */ + AdapterComponent setAutoConfig(Class clazz, boolean autoConfig); + + /** + * Configures auto configuration of injected classes in the component instance. + * + * @param clazz the class (from the list above) + * @param instanceName the name of the instance to inject the class into + * @see #setAutoConfig(Class, boolean) + */ + AdapterComponent setAutoConfig(Class clazz, String instanceName); + + /** + * Sets the service properties associated with the component. If the service + * was already registered, it will be updated. + * + * @param serviceProperties the properties + */ + AdapterComponent setServiceProperties(Dictionary serviceProperties); + + /** + * Sets the names of the methods used as callbacks. These methods, when found, are + * invoked as part of the life cycle management of the component implementation. The + * dependency manager will look for a method of this name with the following signatures, + * in this order: + *
        + *
      1. method(Component component)
      2. + *
      3. method()
      4. + *
      + * + * @param init the name of the init method + * @param start the name of the start method + * @param stop the name of the stop method + * @param destroy the name of the destroy method + * @return the component + */ + AdapterComponent setCallbacks(String init, String start, String stop, String destroy); + + /** + * Sets the names of the methods used as callbacks. These methods, when found, are + * invoked on the specified instance as part of the life cycle management of the component + * implementation. + *

      + * See setCallbacks(String init, String start, String stop, String destroy) for more + * information on the signatures. Specifying an instance means you can create a manager + * that will be invoked whenever the life cycle of a component changes and this manager + * can then decide how to expose this life cycle to the actual component, offering an + * important indirection when developing your own component models. + * + * @return this component + */ + AdapterComponent setCallbacks(Object instance, String init, String start, String stop, String destroy); + + /** + * Sets the factory to use to create the implementation. You can specify + * both the factory class and method to invoke. The method should return + * the implementation, and can use any method to create it. Actually, this + * can be used together with setComposition to create a + * composition of instances that work together to implement a component. The + * factory itself can also be instantiated lazily by not specifying an + * instance, but a Class. + * + * @param factory the factory instance or class + * @param createMethod the name of the create method + * @return this component + */ + AdapterComponent setFactory(Object factory, String createMethod); + + /** + * Sets the factory to use to create the implementation. You specify the + * method to invoke. The method should return the implementation, and can + * use any method to create it. Actually, this can be used together with + * setComposition to create a composition of instances that + * work together to implement a component. + *

      + * Note that currently, there is no default for the factory, so please use + * setFactory(factory, createMethod) instead. + * + * @param createMethod the name of the create method + * @return this component + */ + AdapterComponent setFactory(String createMethod); + + /** + * Sets the instance and method to invoke to get back all instances that + * are part of a composition and need dependencies injected. All of them + * will be searched for any of the dependencies. The method that is + * invoked must return an Object[]. + * + * @param instance the instance that has the method + * @param getMethod the method to invoke + * @return this component + */ + AdapterComponent setComposition(Object instance, String getMethod); + + /** + * Sets the method to invoke on the service implementation to get back all + * instances that are part of a composition and need dependencies injected. + * All of them will be searched for any of the dependencies. The method that + * is invoked must return an Object[]. + * + * @param getMethod the method to invoke + * @return this component + */ + AdapterComponent setComposition(String getMethod); + + /** + * Activate debug for this component. Informations related to dependency processing will be displayed + * using osgi log service, our to standard output if no log service is currently available. + * @param label + */ + AdapterComponent setDebug(String label); + + /** + * Sets the service interface to apply the adapter to + * @param service the service interface to apply the adapter to + * @param filter the filter condition to use with the service interface + * @return this adapter parameter instance + */ + AdapterComponent setAdaptee(Class service, String filter); + + /** + * Sets the name of the member to inject the service into + * @param autoConfig the name of the member to inject the service into + * @return this adapter parameter instance + */ + AdapterComponent setAdapteeField(String autoConfig); + + /** + * Sets the callbacks to invoke when injecting the adaptee service into the adapter component. + * @param add name of the callback method to invoke on add + * @param change name of the callback method to invoke on change + * @param remove name of the callback method to invoke on remove + * @param swap name of the callback method to invoke on swap + * @return this adapter parameter instance + */ + AdapterComponent setAdapteeCallbacks(String add, String change, String remove, String swap); + + /** + * Sets the instance to invoke the callbacks on (null by default, meaning the callbacks have to be invoked on the adapter itself) + * @param callbackInstance the instance to invoke the callbacks on (null by default, meaning the callbacks have to be invoked on the adapter itself) + * @return this adapter parameter instance + */ + AdapterComponent setAdapteeCallbackInstance(Object callbackInstance); + + /** + * Sets if the adaptee service properties should be propagated to the adapter service consumer (true by default) + * @param propagate true if the adaptee service properties should be propagated to the adapter service consumers. + * The provided adapter service properties take precedence over the propagated adaptee service properties. + * It means an adaptee service property won't override an adapter service property having the same name. + * + * @return this adapter parameter instance + */ + AdapterComponent setPropagate(boolean propagate); + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/AspectComponent.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/AspectComponent.java new file mode 100644 index 00000000000..b1b5d72a5b5 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/AspectComponent.java @@ -0,0 +1,383 @@ +/* + * 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.dm; + +import java.util.Dictionary; + +import org.apache.felix.dm.Component.ServiceScope; + +/** + * Interface used to configure the various parameters needed when defining + * a Dependency Manager aspect component. + * + * Aspects allow you to define an interceptor, or chain of interceptors + * for a service (to add features like caching or logging, etc ...). The dependency manager intercepts + * the original service, and allows you to execute some code before invoking the original service ... + * The aspect will be applied to any service that matches the specified interface and filter and + * will be registered with the same interface and properties as the original service, plus any + * extra properties you supply here. If you declare the original service as a member it will be injected. + * + *

      Usage Examples

      + * + * Here is a sample showing a DatabaseCache aspect which is created each time a Database interface is registered in the registry. + * + *
      + * {@code
      + * public class Activator extends DependencyActivatorBase {
      + *     &Override
      + *     public void init(BundleContext context, DependencyManager dm) throws Exception {
      + *         AspectComponent aspectComponent = createAspectComponent()
      + *             .setAspect(Database.class, null, 10)
      + *             .setImplementation(DatabaseCache.class);
      + *         dm.add(aspectComponent);
      + *     }
      + * }
      + * 
      + * interface Database {
      + *     String get(String key);
      + * }
      + * 
      + * class DatabaseCache implements Database {
      + *     volatile Database originalDatabase; // injected
      + *     
      + *     String get(String key) {
      + *         String value = cache.get(key);
      + *         if (value == null) {
      + *             value = this.originalDatabase.get(key);
      + *             store(key, value);
      + *         }
      + *         return value;
      + *     }
      + *     ... 
      + * }
      + * } 
      + * + *

      For "add", "change", "remove" callbacks, the following method signatures are supported: + * + *

      {@code
      + * (Component comp, ServiceReference ref, Service service)
      + * (Component comp, ServiceReference ref, Object service)
      + * (Component comp, ServiceReference ref)
      + * (Component comp, Service service)
      + * (Component comp, Object service)
      + * (Component comp)
      + * (Component comp, Map properties, Service service)
      + * (ServiceReference ref, Service service)
      + * (ServiceReference ref, Object service)
      + * (ServiceReference ref)
      + * (Service service)
      + * (Service service, Map propeerties)
      + * (Map properties, Service, service)
      + * (Service service, Dictionary properties)
      + * (Dictionary properties, Service service)
      + * (Object service)
      + * }
      + * + *

      For "swap" callbacks, the following method signatures are supported: + * + *

      {@code
      + * (Service old, Service replace)
      + * (Object old, Object replace)
      + * (ServiceReference old, Service old, ServiceReference replace, Service replace)
      + * (ServiceReference old, Object old, ServiceReference replace, Object replace)
      + * (Component comp, Service old, Service replace)
      + * (Component comp, Object old, Object replace)
      + * (Component comp, ServiceReference old, Service old, ServiceReference replace, Service replace)
      + * (Component comp, ServiceReference old, Object old, ServiceReference replace, Object replace)
      + * (ServiceReference old, ServiceReference replace)
      + * (Component comp, ServiceReference old, ServiceReference replace)
      + * }
      + */ +public interface AspectComponent extends Component { + + /** + * Sets the component scope. + * @param scope the component scope (default=SINGLETON) + * + * @return this component + */ + AspectComponent setScope(ServiceScope scope); + + /** + * Sets the implementation for this component. You can actually specify + * an instance you have instantiated manually, or a Class + * that will be instantiated using its default constructor when the + * required dependencies are resolved, effectively giving you a lazy + * instantiation mechanism. + * + * There are four special methods that are called when found through + * reflection to give you life cycle management options: + *
        + *
      1. init() is invoked after the instance has been + * created and dependencies have been resolved, and can be used to + * initialize the internal state of the instance or even to add more + * dependencies based on runtime state
      2. + *
      3. start() is invoked right before the service is + * registered
      4. + *
      5. stop() is invoked right after the service is + * unregistered
      6. + *
      7. destroy() is invoked after all dependencies are + * removed
      8. + *
      + * In short, this allows you to initialize your instance before it is + * registered, perform some post-initialization and pre-destruction code + * as well as final cleanup. If a method is not defined, it simply is not + * called, so you can decide which one(s) you need. If you need even more + * fine-grained control, you can register as a service state listener too. + * + * @param implementation the implementation + * @return this component + * @see ComponentStateListener + */ + AspectComponent setImplementation(Object implementation); + + /** + * Adds dependency(ies) to this component, atomically. If the component is already active or if you add + * dependencies from the init method, then you should add all the dependencies in one single add method call + * (using the varargs argument), because this method may trigger component activation (like + * the ServiceTracker.open() method does). + * + * @param dependencies the dependencies to add. + * @return this component + */ + AspectComponent add(Dependency ... dependencies); + + /** + * Removes a dependency from the component. + * @param d the dependency to remove + * @return this component + */ + AspectComponent remove(Dependency d); + + /** + * Adds a component state listener to this component. + * + * @param listener the state listener + */ + AspectComponent add(ComponentStateListener listener); + + /** + * Removes a component state listener from this component. + * + * @param listener the state listener + */ + AspectComponent remove(ComponentStateListener listener); + + /** + * Sets the public interface under which this component should be registered + * in the OSGi service registry. + * + * @param serviceName the name of the service interface + * @param properties the properties for this service + * @return this component + */ + AspectComponent setInterface(String serviceName, Dictionary properties); + + /** + * Sets the public interfaces under which this component should be registered + * in the OSGi service registry. + * + * @param serviceNames the names of the service interface + * @param properties the properties for these services + * @return this component + */ + AspectComponent setInterface(String[] serviceNames, Dictionary properties); + + /** + * Sets the public interface under which this component should be registered + * in the OSGi service registry. + * + * @param serviceName the name of the service interface + * @param properties the properties for this service + * @return this component + */ + AspectComponent setInterface(Class serviceName, Dictionary properties); + + /** + * Sets the public interfaces under which this component should be registered + * in the OSGi service registry. + * + * @param serviceNames the names of the service interface + * @param properties the properties for these services + * @return this component + */ + AspectComponent setInterface(Class[] serviceNames, Dictionary properties); + + /** + * Configures auto configuration of injected classes in the component instance. + * The following injections are currently performed, unless you explicitly + * turn them off: + *
      + *
      BundleContext
      the bundle context of the bundle
      + *
      ServiceRegistration
      the service registration used to register your service
      + *
      DependencyManager
      the dependency manager instance
      + *
      Component
      the component instance of the dependency manager
      + *
      + * + * @param clazz the class (from the list above) + * @param autoConfig false to turn off auto configuration + */ + AspectComponent setAutoConfig(Class clazz, boolean autoConfig); + + /** + * Configures auto configuration of injected classes in the component instance. + * + * @param clazz the class (from the list above) + * @param instanceName the name of the instance to inject the class into + * @see #setAutoConfig(Class, boolean) + */ + AspectComponent setAutoConfig(Class clazz, String instanceName); + + /** + * Sets the service properties associated with the component. If the service + * was already registered, it will be updated. + * + * @param serviceProperties the properties + */ + AspectComponent setServiceProperties(Dictionary serviceProperties); + + /** + * Sets the names of the methods used as callbacks. These methods, when found, are + * invoked as part of the life cycle management of the component implementation. The + * dependency manager will look for a method of this name with the following signatures, + * in this order: + *
        + *
      1. method(Component component)
      2. + *
      3. method()
      4. + *
      + * + * @param init the name of the init method + * @param start the name of the start method + * @param stop the name of the stop method + * @param destroy the name of the destroy method + * @return the component + */ + AspectComponent setCallbacks(String init, String start, String stop, String destroy); + + /** + * Sets the names of the methods used as callbacks. These methods, when found, are + * invoked on the specified instance as part of the life cycle management of the component + * implementation. + *

      + * See setCallbacks(String init, String start, String stop, String destroy) for more + * information on the signatures. Specifying an instance means you can create a manager + * that will be invoked whenever the life cycle of a component changes and this manager + * can then decide how to expose this life cycle to the actual component, offering an + * important indirection when developing your own component models. + * + * @return this component + */ + AspectComponent setCallbacks(Object instance, String init, String start, String stop, String destroy); + + /** + * Sets the factory to use to create the implementation. You can specify + * both the factory class and method to invoke. The method should return + * the implementation, and can use any method to create it. Actually, this + * can be used together with setComposition to create a + * composition of instances that work together to implement a component. The + * factory itself can also be instantiated lazily by not specifying an + * instance, but a Class. + * + * @param factory the factory instance or class + * @param createMethod the name of the create method + * @return this component + */ + AspectComponent setFactory(Object factory, String createMethod); + + /** + * Sets the factory to use to create the implementation. You specify the + * method to invoke. The method should return the implementation, and can + * use any method to create it. Actually, this can be used together with + * setComposition to create a composition of instances that + * work together to implement a component. + *

      + * Note that currently, there is no default for the factory, so please use + * setFactory(factory, createMethod) instead. + * + * @param createMethod the name of the create method + * @return this component + */ + AspectComponent setFactory(String createMethod); + + /** + * Sets the instance and method to invoke to get back all instances that + * are part of a composition and need dependencies injected. All of them + * will be searched for any of the dependencies. The method that is + * invoked must return an Object[]. + * + * @param instance the instance that has the method + * @param getMethod the method to invoke + * @return this component + */ + AspectComponent setComposition(Object instance, String getMethod); + + /** + * Sets the method to invoke on the service implementation to get back all + * instances that are part of a composition and need dependencies injected. + * All of them will be searched for any of the dependencies. The method that + * is invoked must return an Object[]. + * + * @param getMethod the method to invoke + * @return this component + */ + AspectComponent setComposition(String getMethod); + + /** + * Activate debug for this component. Informations related to dependency processing will be displayed + * using osgi log service, our to standard output if no log service is currently available. + * @param label + */ + AspectComponent setDebug(String label); + + /** + * Sets the service interface to apply the aspect to (required parameter) + * @param service the service interface to apply the aspect to + * @param filter the filter condition to use with the service aspect interface (null if no filter) + * @param ranking the level used to organize the aspect chain ordering + * @return this aspect parameter instance + */ + AspectComponent setAspect(Class service, String filter, int ranking); + + /** + * Sets the aspect implementation field name where to inject original service (optional parameter). + * If not set or null, any field matching the original service will be injected. + * @param autoConfig the aspect implementation field name where to inject original service + * @return this aspect parameter instance + */ + AspectComponent setAspectField(String autoConfig); + + /** + * Sets name of the callbacks method to invoke on add,change,remove, or swap callbacks (optional parameter). + * @param add name of the callback method to invoke on add + * @param change name of the callback method to invoke on change + * @param remove name of the callback method to invoke on remove + * @param swap name of the callback method to invoke on swap + * @return this aspect parameter instance + */ + AspectComponent setAspectCallbacks(String add, String change, String remove, String swap); + + /** + * Sets the instance to invoke the callbacks on (optional parameter). + * null means the callbacks will be invoked on the aspect implementation object. + * @param callbackInstance the instance to invoke the callbacks on + * @return this aspect parameter instance + */ + AspectComponent setAspectCallbackInstance(Object callbackInstance); + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/BundleComponent.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/BundleComponent.java new file mode 100644 index 00000000000..303eefbd968 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/BundleComponent.java @@ -0,0 +1,363 @@ +/* + * 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.dm; + +import java.util.Dictionary; + +import org.apache.felix.dm.Component.ServiceScope; + +/** + * Interface used to configure the various parameters needed when defining + * a Dependency Manager bundle adapter component. + * + * Bundle Adapters, like {@link AdapterComponent}, are used to "extend" + * existing bundles, and can publish an adapter services based on the existing bundle. + * An example would be implementing a video player which adapters a resource bundle having + * some specific headers. + *

      When you create a bundle adapter component, it will be applied + * to any bundle that matches the specified bundle state mask as well as the specified ldap filter + * used to match the bundle manifest headers. The bundle adapter will be registered + * with the specified bundle manifest headers as service properties, plus any extra + * properties you suppl. If you declare a bundle field in your bundle adapter class, + * it will be injected it will be injected with the original bundle. + * + *

      Usage Examples

      + * + * Here is a sample showing a VideoPlayer adapter component which plays a video found from + * a bundle having a Video-Path manifest header. + * + *
      + * {@code
      + * public class Activator extends DependencyActivatorBase {
      + *     &Override
      + *     public void init(BundleContext context, DependencyManager dm) throws Exception {
      + *         BundleComponent bundleComponent = createBundleComponent()
      + *             .setFilter(Bundle.ACTIVE, "(Video-Path=*)")
      + *             .setInterface(VideoPlayer.class.getName(), null)
      + *             .setImplementation(VideoPlayerImpl.class);
      + *         dm.add(bundleComponent);
      + *     }
      + * }
      + * 
      + * public interface VideoPlayer {
      + *     void play();
      + * }
      + * 
      + * public class VideoPlayerImpl implements VideoPlayer {
      + *     volatile Bundle bundle; // injected
      + *     String path;
      + *     
      + *     void start() {
      + *        path = bundle.getHeaders().get("Video-Path");
      + *     }
      + *     
      + *     void play() {
      + *         ...
      + *     }
      + * }
      + * } 
      + * + *

      When you use callbacks to get injected with the bundle, the "add", "change", "remove" callbacks + * support the following method signatures: + * + *

      {@code
      + * (Bundle)
      + * (Object)
      + * (COmponent, Bundle)
      + * }
      + * + * @see DependencyManager#createBundleComponent() + */ +public interface BundleComponent extends Component { + + /** + * Sets the component scope. + * @param scope the component scope (default=SINGLETON) + * + * @return this component + */ + BundleComponent setScope(ServiceScope scope); + + /** + * Sets the implementation for this component. You can actually specify + * an instance you have instantiated manually, or a Class + * that will be instantiated using its default constructor when the + * required dependencies are resolved, effectively giving you a lazy + * instantiation mechanism. + * + * There are four special methods that are called when found through + * reflection to give you life cycle management options: + *
        + *
      1. init() is invoked after the instance has been + * created and dependencies have been resolved, and can be used to + * initialize the internal state of the instance or even to add more + * dependencies based on runtime state
      2. + *
      3. start() is invoked right before the service is + * registered
      4. + *
      5. stop() is invoked right after the service is + * unregistered
      6. + *
      7. destroy() is invoked after all dependencies are + * removed
      8. + *
      + * In short, this allows you to initialize your instance before it is + * registered, perform some post-initialization and pre-destruction code + * as well as final cleanup. If a method is not defined, it simply is not + * called, so you can decide which one(s) you need. If you need even more + * fine-grained control, you can register as a service state listener too. + * + * @param implementation the implementation + * @return this component + * @see ComponentStateListener + */ + BundleComponent setImplementation(Object implementation); + + /** + * Adds dependency(ies) to this component, atomically. If the component is already active or if you add + * dependencies from the init method, then you should add all the dependencies in one single add method call + * (using the varargs argument), because this method may trigger component activation (like + * the ServiceTracker.open() method does). + * + * @param dependencies the dependencies to add. + * @return this component + */ + BundleComponent add(Dependency ... dependencies); + + /** + * Removes a dependency from the component. + * @param d the dependency to remove + * @return this component + */ + BundleComponent remove(Dependency d); + + /** + * Adds a component state listener to this component. + * + * @param listener the state listener + */ + BundleComponent add(ComponentStateListener listener); + + /** + * Removes a component state listener from this component. + * + * @param listener the state listener + */ + BundleComponent remove(ComponentStateListener listener); + + /** + * Sets the public interface under which this component should be registered + * in the OSGi service registry. + * + * @param serviceName the name of the service interface + * @param properties the properties for this service + * @return this component + */ + BundleComponent setInterface(String serviceName, Dictionary properties); + + /** + * Sets the public interfaces under which this component should be registered + * in the OSGi service registry. + * + * @param serviceNames the names of the service interface + * @param properties the properties for these services + * @return this component + */ + BundleComponent setInterface(String[] serviceNames, Dictionary properties); + + /** + * Sets the public interface under which this component should be registered + * in the OSGi service registry. + * + * @param serviceName the name of the service interface + * @param properties the properties for this service + * @return this component + */ + BundleComponent setInterface(Class serviceName, Dictionary properties); + + /** + * Sets the public interfaces under which this component should be registered + * in the OSGi service registry. + * + * @param serviceNames the names of the service interface + * @param properties the properties for these services + * @return this component + */ + BundleComponent setInterface(Class[] serviceNames, Dictionary properties); + + /** + * Configures auto configuration of injected classes in the component instance. + * The following injections are currently performed, unless you explicitly + * turn them off: + *
      + *
      BundleContext
      the bundle context of the bundle
      + *
      ServiceRegistration
      the service registration used to register your service
      + *
      DependencyManager
      the dependency manager instance
      + *
      Component
      the component instance of the dependency manager
      + *
      + * + * @param clazz the class (from the list above) + * @param autoConfig false to turn off auto configuration + */ + BundleComponent setAutoConfig(Class clazz, boolean autoConfig); + + /** + * Configures auto configuration of injected classes in the component instance. + * + * @param clazz the class (from the list above) + * @param instanceName the name of the instance to inject the class into + * @see #setAutoConfig(Class, boolean) + */ + BundleComponent setAutoConfig(Class clazz, String instanceName); + + /** + * Sets the service properties associated with the component. If the service + * was already registered, it will be updated. + * + * @param serviceProperties the properties + */ + BundleComponent setServiceProperties(Dictionary serviceProperties); + + /** + * Sets the names of the methods used as callbacks. These methods, when found, are + * invoked as part of the life cycle management of the component implementation. The + * dependency manager will look for a method of this name with the following signatures, + * in this order: + *
        + *
      1. method(Component component)
      2. + *
      3. method()
      4. + *
      + * + * @param init the name of the init method + * @param start the name of the start method + * @param stop the name of the stop method + * @param destroy the name of the destroy method + * @return the component + */ + BundleComponent setCallbacks(String init, String start, String stop, String destroy); + + /** + * Sets the names of the methods used as callbacks. These methods, when found, are + * invoked on the specified instance as part of the life cycle management of the component + * implementation. + *

      + * See setCallbacks(String init, String start, String stop, String destroy) for more + * information on the signatures. Specifying an instance means you can create a manager + * that will be invoked whenever the life cycle of a component changes and this manager + * can then decide how to expose this life cycle to the actual component, offering an + * important indirection when developing your own component models. + * + * @return this component + */ + BundleComponent setCallbacks(Object instance, String init, String start, String stop, String destroy); + + /** + * Sets the factory to use to create the implementation. You can specify + * both the factory class and method to invoke. The method should return + * the implementation, and can use any method to create it. Actually, this + * can be used together with setComposition to create a + * composition of instances that work together to implement a component. The + * factory itself can also be instantiated lazily by not specifying an + * instance, but a Class. + * + * @param factory the factory instance or class + * @param createMethod the name of the create method + * @return this component + */ + BundleComponent setFactory(Object factory, String createMethod); + + /** + * Sets the factory to use to create the implementation. You specify the + * method to invoke. The method should return the implementation, and can + * use any method to create it. Actually, this can be used together with + * setComposition to create a composition of instances that + * work together to implement a component. + *

      + * Note that currently, there is no default for the factory, so please use + * setFactory(factory, createMethod) instead. + * + * @param createMethod the name of the create method + * @return this component + */ + BundleComponent setFactory(String createMethod); + + /** + * Sets the instance and method to invoke to get back all instances that + * are part of a composition and need dependencies injected. All of them + * will be searched for any of the dependencies. The method that is + * invoked must return an Object[]. + * + * @param instance the instance that has the method + * @param getMethod the method to invoke + * @return this component + */ + BundleComponent setComposition(Object instance, String getMethod); + + /** + * Sets the method to invoke on the service implementation to get back all + * instances that are part of a composition and need dependencies injected. + * All of them will be searched for any of the dependencies. The method that + * is invoked must return an Object[]. + * + * @param getMethod the method to invoke + * @return this component + */ + BundleComponent setComposition(String getMethod); + + /** + * Activate debug for this component. Informations related to dependency processing will be displayed + * using osgi log service, our to standard output if no log service is currently available. + * @param label + */ + BundleComponent setDebug(String label); + + /** + * Sets the bundle state mask and bundle manifest headers filter. + * + * @param bundleStateMask the bundle state mask to apply + * @param bundleFilter the filter to apply to the bundle manifest + * @return this BundleComponent + */ + BundleComponent setBundleFilter(int bundleStateMask, String bundleFilter); + + /** + * Sets the callbacks to invoke when injecting the bundle into the adapter component. + * + * @param add name of the callback method to invoke on add + * @param change name of the callback method to invoke on change + * @param remove name of the callback method to invoke on remove + * @return this BundleComponent + */ + BundleComponent setBundleCallbacks(String add, String change, String remove); + + /** + * Sets the instance to invoke the callbacks on (null by default, meaning the callbacks have to be invoked on the adapter itself) + * + * @param callbackInstance the instance to invoke the callbacks on (null by default, meaning the callbacks have to be invoked on the adapter itself) + * @return this BundleComponent + */ + BundleComponent setBundleCallbackInstance(Object callbackInstance); + + /** + * Sets if the bundle manifest headers should be propagated to the bundle component adapter service consumer (true by default). + * The component service properties take precedence over the propagated bundle manifest headers. + * @param propagate true if the bundle manifest headers should be propagated to the adapter service consumers + * @return this BundleComponent + */ + BundleComponent setPropagate(boolean propagate); + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/BundleDependency.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/BundleDependency.java new file mode 100644 index 00000000000..26874066476 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/BundleDependency.java @@ -0,0 +1,143 @@ +/* + * 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.dm; + +import org.osgi.framework.Bundle; + +/** + * @author Felix Project Team + */ +public interface BundleDependency extends Dependency, ComponentDependencyDeclaration { + /** + * Sets the callbacks for this dependency. These callbacks can be used as hooks whenever a dependency is added or removed. + * When you specify callbacks, the auto configuration feature is automatically turned off, because we're assuming you don't + * need it in this case. + * + * @param added the method to call when a bundle was added + * @param removed the method to call when a bundle was removed + * @return the bundle dependency + */ + public BundleDependency setCallbacks(String added, String removed); + + /** + * Sets the callbacks for this dependency. These callbacks can be used as hooks whenever a dependency is added, changed or + * removed. When you specify callbacks, the auto configuration feature is automatically turned off, because we're assuming + * you don't need it in this case. + * + * @param added the method to call when a bundle was added + * @param changed the method to call when a bundle was changed + * @param removed the method to call when a bundle was removed + * @return the bundle dependency + */ + public BundleDependency setCallbacks(String added, String changed, String removed); + + /** + * Sets the callbacks for this dependency. These callbacks can be used as hooks whenever a dependency is added or removed. + * They are called on the instance you provide. When you specify callbacks, the auto configuration feature is automatically + * turned off, because we're assuming you don't need it in this case. + * + * @param instance the instance to call the callbacks on + * @param added the method to call when a bundle was added + * @param removed the method to call when a bundle was removed + * @return the bundle dependency + */ + public BundleDependency setCallbacks(Object instance, String added, String removed); + + /** + * Sets the callbacks for this dependency. These callbacks can be used as hooks whenever a dependency is added, changed or + * removed. They are called on the instance you provide. When you specify callbacks, the auto configuration feature is + * automatically turned off, because we're assuming you don't need it in this case. + * + * @param instance the instance to call the callbacks on + * @param added the method to call when a bundle was added + * @param changed the method to call when a bundle was changed + * @param removed the method to call when a bundle was removed + * @return the bundle dependency + */ + public BundleDependency setCallbacks(Object instance, String added, String changed, String removed); + + /** + * Enables auto configuration for this dependency. This means the component implementation (composition) will be + * injected with this bundle dependency automatically. + * + * @param autoConfig true to enable auto configuration + * @return the bundle dependency + */ + public BundleDependency setAutoConfig(boolean autoConfig); + + /** + * Sets the dependency to be required. + * + * @param required true if this bundle dependency is required + * @return the bundle dependency + */ + public BundleDependency setRequired(boolean required); + + /** + * Sets the bundle to depend on directly. + * + * @param bundle the bundle to depend on + * @return the bundle dependency + */ + public BundleDependency setBundle(Bundle bundle); + + /** + * Sets the filter condition to depend on. Filters are matched against the full manifest of a bundle. + * + * @param filter the filter condition + * @return the bundle dependency + * @throws IllegalArgumentException if the filter is invalid + */ + public BundleDependency setFilter(String filter) throws IllegalArgumentException; + + /** + * Sets the bundle state mask to depend on. The OSGi BundleTracker explains this mask in more detail, but + * it is basically a mask with flags for each potential state a bundle can be in. + * + * @param mask the mask to use + * @return the bundle dependency + */ + public BundleDependency setStateMask(int mask); + + /** + * Sets property propagation. If set to true any bundle manifest properties will be added + * to the service properties of the component that has this dependency (if it registers as a service). + * The provided service properties take precedence over the propagated bundle manifest headers. + * It means a bundle manifest header won't override a component service property having the same name. + * + * @param propagate true to propagate the bundle manifest properties + * @return the bundle dependency + */ + public BundleDependency setPropagate(boolean propagate); + + /** + * Sets an Object instance and a callback method used to propagate some properties to the provided service properties. + * The method will be invoked on the specified object instance and must have one of the following signatures: + *

      • Dictionary callback(ServiceReference, Object service) + *
      • Dictionary callback(ServiceReference) + *
      + * The provided service properties take precedence over the propagated bundle manifest headers. + * It means a bundle manifest header won't override a component service property having the same name. + * + * @param instance the Object instance which is used to retrieve propagated service properties + * @param method the method to invoke for retrieving the properties to be propagated to the service properties. + * @return this service dependency. + */ + public BundleDependency setPropagate(Object instance, String method); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/Component.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/Component.java new file mode 100644 index 00000000000..3c61e509bdf --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/Component.java @@ -0,0 +1,354 @@ +/* + * 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.dm; + +import java.util.Dictionary; + +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.ServiceRegistration; + + +/** + * Component interface. Components are the main building blocks for OSGi applications. + * They can publish themselves as a service, and they can have dependencies. These + * dependencies will influence their life cycle as component will only be activated + * when all required dependencies are available. + * + * @author Felix Project Team + */ +@ProviderType +public interface Component { + + /** + * Component service scopes + */ + enum ServiceScope { + /** + * When the component is registered as a service, it must be registered as a + * singleton: only a single instance of the component must be + * used for all service consumers. SINGLETON scope is the default scope. + */ + SINGLETON, + + /** + * When the component is registered as a service, it must be registered as a + * bundle scope service and an instance of the component must be created for + * each bundle using the service. + */ + BUNDLE, + + /** + * When the component is registered as a service, it must be registered as a + * prototype scope service and an instance of the component must be created + * for each distinct request for the service. + */ + PROTOTYPE + } + + /** + * Sets the component scope. + * @param scope the component scope (default=SINGLETON) + * + * @return this component + */ + Component setScope(ServiceScope scope); + + /** + * Sets the implementation for this component. You can actually specify + * an instance you have instantiated manually, or a Class + * that will be instantiated using its default constructor when the + * required dependencies are resolved, effectively giving you a lazy + * instantiation mechanism. + * + * There are four special methods that are called when found through + * reflection to give you life cycle management options: + *
        + *
      1. init() is invoked after the instance has been + * created and dependencies have been resolved, and can be used to + * initialize the internal state of the instance or even to add more + * dependencies based on runtime state
      2. + *
      3. start() is invoked right before the service is + * registered
      4. + *
      5. stop() is invoked right after the service is + * unregistered
      6. + *
      7. destroy() is invoked after all dependencies are + * removed
      8. + *
      + * In short, this allows you to initialize your instance before it is + * registered, perform some post-initialization and pre-destruction code + * as well as final cleanup. If a method is not defined, it simply is not + * called, so you can decide which one(s) you need. If you need even more + * fine-grained control, you can register as a service state listener too. + * + * @param implementation the implementation + * @return this component + * @see ComponentStateListener + */ + Component setImplementation(Object implementation); + + /** + * Adds dependency(ies) to this component, atomically. If the component is already active or if you add + * dependencies from the init method, then you should add all the dependencies in one single add method call + * (using the varargs argument), because this method may trigger component activation (like + * the ServiceTracker.open() method does). + * + * @param dependencies the dependencies to add. + * @return this component + */ + Component add(Dependency ... dependencies); + + /** + * Removes a dependency from the component. + * @param d the dependency to remove + * @return this component + */ + Component remove(Dependency d); + + /** + * Adds a component state listener to this component. + * + * @param listener the state listener + */ + Component add(ComponentStateListener listener); + + /** + * Removes a component state listener from this component. + * + * @param listener the state listener + */ + Component remove(ComponentStateListener listener); + + /** + * Sets the public interface under which this component should be registered + * in the OSGi service registry. + * + * @param serviceName the name of the service interface + * @param properties the properties for this service + * @return this component + */ + Component setInterface(String serviceName, Dictionary properties); + + /** + * Sets the public interfaces under which this component should be registered + * in the OSGi service registry. + * + * @param serviceNames the names of the service interface + * @param properties the properties for these services + * @return this component + */ + Component setInterface(String[] serviceNames, Dictionary properties); + + /** + * Sets the public interface under which this component should be registered + * in the OSGi service registry. + * + * @param serviceName the name of the service interface + * @param properties the properties for this service + * @return this component + */ + Component setInterface(Class serviceName, Dictionary properties); + + /** + * Sets the public interfaces under which this component should be registered + * in the OSGi service registry. + * + * @param serviceNames the names of the service interface + * @param properties the properties for these services + * @return this component + */ + Component setInterface(Class[] serviceNames, Dictionary properties); + + /** + * Configures auto configuration of injected classes in the component instance. + * The following injections are currently performed, unless you explicitly + * turn them off: + *
      + *
      BundleContext
      the bundle context of the bundle
      + *
      ServiceRegistration
      the service registration used to register your service
      + *
      DependencyManager
      the dependency manager instance
      + *
      Component
      the component instance of the dependency manager
      + *
      + * + * @param clazz the class (from the list above) + * @param autoConfig false to turn off auto configuration + */ + Component setAutoConfig(Class clazz, boolean autoConfig); + + /** + * Configures auto configuration of injected classes in the component instance. + * + * @param clazz the class (from the list above) + * @param instanceName the name of the instance to inject the class into + * @see #setAutoConfig(Class, boolean) + */ + Component setAutoConfig(Class clazz, String instanceName); + + /** + * Sets the service properties associated with the component. If the service + * was already registered, it will be updated. + * + * @param serviceProperties the properties + */ + Component setServiceProperties(Dictionary serviceProperties); + + /** + * Sets the names of the methods used as callbacks. These methods, when found, are + * invoked as part of the life cycle management of the component implementation. The + * dependency manager will look for a method of this name with the following signatures, + * in this order: + *
        + *
      1. method(Component component)
      2. + *
      3. method()
      4. + *
      + * + * @param init the name of the init method + * @param start the name of the start method + * @param stop the name of the stop method + * @param destroy the name of the destroy method + * @return the component + */ + Component setCallbacks(String init, String start, String stop, String destroy); + + /** + * Sets the names of the methods used as callbacks. These methods, when found, are + * invoked on the specified instance as part of the life cycle management of the component + * implementation. + *

      + * See setCallbacks(String init, String start, String stop, String destroy) for more + * information on the signatures. Specifying an instance means you can create a manager + * that will be invoked whenever the life cycle of a component changes and this manager + * can then decide how to expose this life cycle to the actual component, offering an + * important indirection when developing your own component models. + * + * @return this component + */ + Component setCallbacks(Object instance, String init, String start, String stop, String destroy); + + /** + * Sets the factory to use to create the implementation. You can specify + * both the factory class and method to invoke. The method should return + * the implementation, and can use any method to create it. Actually, this + * can be used together with setComposition to create a + * composition of instances that work together to implement a component. The + * factory itself can also be instantiated lazily by not specifying an + * instance, but a Class. + * + * @param factory the factory instance or class + * @param createMethod the name of the create method + * @return this component + */ + Component setFactory(Object factory, String createMethod); + + /** + * Sets the factory to use to create the implementation. You specify the + * method to invoke. The method should return the implementation, and can + * use any method to create it. Actually, this can be used together with + * setComposition to create a composition of instances that + * work together to implement a component. + *

      + * Note that currently, there is no default for the factory, so please use + * setFactory(factory, createMethod) instead. + * + * @param createMethod the name of the create method + * @return this component + */ + Component setFactory(String createMethod); + + /** + * Sets the instance and method to invoke to get back all instances that + * are part of a composition and need dependencies injected. All of them + * will be searched for any of the dependencies. The method that is + * invoked must return an Object[]. + * + * @param instance the instance that has the method + * @param getMethod the method to invoke + * @return this component + */ + Component setComposition(Object instance, String getMethod); + + /** + * Sets the method to invoke on the service implementation to get back all + * instances that are part of a composition and need dependencies injected. + * All of them will be searched for any of the dependencies. The method that + * is invoked must return an Object[]. + * + * @param getMethod the method to invoke + * @return this component + */ + Component setComposition(String getMethod); + + /** + * Activate debug for this component. Informations related to dependency processing will be displayed + * using osgi log service, our to standard output if no log service is currently available. + * @param label + */ + Component setDebug(String label); + + /** + * Returns the service registration for this component. The method + * will return null if no service registration is + * available, for example if this component is not registered as a + * service at all. + * + * @return the service registration + */ + @SuppressWarnings("rawtypes") + ServiceRegistration getServiceRegistration(); + + /** + * Returns the instance that make up this component. If the component has a composition of instances, + * then the first instance of the composition is returned. Null is returned if the component has not + * even been instantiated. + * + * @return the component instances + */ + public U getInstance(); + + /** + * Returns the composition instances that make up this component, or just the + * component instance if it does not have a composition, or an empty array if + * the component has not even been instantiated. + * + * @return the component instances + */ + public Object[] getInstances(); + + /** + * Returns the component service properties. + * The returned dictionary is either empty if no service properties were defined for this component, + * or copy of the existing service properties associated with this component. + * + * @return a copy of the service properties associated to this component or an empty dictionary + * if no service properties were defined for this component. + */ + public Dictionary getServiceProperties(); + + /** + * Returns the dependency manager associated with this component. + * @return the dependency manager associated with this component. + */ + public DependencyManager getDependencyManager(); + + /** + * Returns the component description (dependencies, service provided, etc ...). + * @return the component description (dependencies, service provided, etc ...). + */ + public ComponentDeclaration getComponentDeclaration(); + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ComponentDeclaration.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ComponentDeclaration.java new file mode 100644 index 00000000000..85353ef2d0a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ComponentDeclaration.java @@ -0,0 +1,61 @@ +/* + * 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.dm; + +import java.util.Dictionary; +import java.util.Map; + +import org.osgi.framework.BundleContext; + +/** + * Describes a component. Component declarations form descriptions of components + * that are managed by the dependency manager. They can be used to query their state + * for monitoring tools. The dependency manager shell command is an example of + * such a tool. + * + * @author Felix Project Team + */ +public interface ComponentDeclaration { + /** Names for the states of this component. */ + public static final String[] STATE_NAMES = { "unregistered", "registered" }; + /** State constant for an unregistered component. */ + public static final int STATE_UNREGISTERED = 0; + /** State constant for a registered component. */ + public static final int STATE_REGISTERED = 1; + /** Returns a list of dependencies associated with this component. */ + public ComponentDependencyDeclaration[] getComponentDependencies(); + /** Returns the description of this component (the classname or the provided service(s)) */ + public String getName(); + /** Returns the class name of the Component implementation. */ + public String getClassName(); + /** Returns the service optionally provided by this component, or null */ + public String[] getServices(); + /** Returns the service properties, or null */ + public Dictionary getServiceProperties(); + /** Returns the state of this component. */ + public int getState(); + /** Returns the instance id of this component. */ + public long getId(); + /** Returns the bundle context associated with this component. */ + public BundleContext getBundleContext(); + /** Returns the dependency manager for this component */ + public DependencyManager getDependencyManager(); + /** Returns the execution time in nanos for each component callbacks (init/start/stop/destroy) */ + public Map getCallbacksTime(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ComponentDependencyDeclaration.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ComponentDependencyDeclaration.java new file mode 100644 index 00000000000..b86f3905415 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ComponentDependencyDeclaration.java @@ -0,0 +1,61 @@ +/* + * 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.dm; + +/** + * Describes a component dependency. They form descriptions of dependencies + * that are managed by the dependency manager. They can be used to query their state + * for monitoring tools. The dependency manager shell command is an example of + * such a tool. + * + * @author Felix Project Team + */ +public interface ComponentDependencyDeclaration { + /** Names for the states of this dependency. */ + public static final String[] STATE_NAMES = { + "optional unavailable", + "optional available", + "required unavailable", + "required available", + "optional (not tracking)", + "required (not tracking)" + }; + /** State constant for an unavailable, optional dependency. */ + public static final int STATE_UNAVAILABLE_OPTIONAL = 0; + /** State constant for an available, optional dependency. */ + public static final int STATE_AVAILABLE_OPTIONAL = 1; + /** State constant for an unavailable, required dependency. */ + public static final int STATE_UNAVAILABLE_REQUIRED = 2; + /** State constant for an available, required dependency. */ + public static final int STATE_AVAILABLE_REQUIRED = 3; + /** State constant for an optional dependency that has not been started yet. */ + public static final int STATE_OPTIONAL = 4; + /** State constant for a required dependency that has not been started yet. */ + public static final int STATE_REQUIRED = 5; + /** Returns the name of this dependency (a generic name with optional info separated by spaces)*/ + public String getName(); + /** Returns the simple dependency name (service classname for example) */ + public String getSimpleName(); + /** Returns the Dependency filter or null */ + public String getFilter(); + /** Returns the name of the type of this dependency. */ + public String getType(); + /** Returns the state of this dependency. */ + public int getState(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ComponentExecutorFactory.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ComponentExecutorFactory.java new file mode 100644 index 00000000000..dd9a8b5a0ad --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ComponentExecutorFactory.java @@ -0,0 +1,171 @@ +/* + * 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.dm; + +import java.util.concurrent.Executor; + +/** + * A ComponentExecutorFactory service can be registered by any management agent bundle + * in order to enable parallel activation of Components.

      + * + * A ComponentExecutorFactory is part of the new concurrency model that forms the basis + * of Dependency Manager 4.0. Let's first give a brief overview of the default thread model used when + * no ComponentExecutorFactory is used. Then we'll explain the rationale and the usage of a + * ComponentExecutorFactory service. + *

      + * + *

      Default Thread Model

      + * + * By default, Dependency Manager uses a lock-free/single thread model: + *

        + * + *
      • When an external event that influence the state of a Component is taking place (for example, + * when a service dependency on which the Component is depending on is registered in the registry by + * a given thread), then DependencyManager does not perform any locking for the handling of the event. + * Instead of that, a job that will handle the event is inserted in an internal lock-free + * Serial Queue which is internally maintained in each Component. + * + *
      • all jobs scheduled in the Serial Queue are then executed in FIFO order, by the first + * thread which has triggered the first event. This avoid to use some blocking locks in DM internals, and + * also it simplifies the development of DM components, because all lifecycle callbacks + * (init/start/stop/destroy) and dependency injections are scheduled through the Serial Queue: + * This means that your component is not concurrently called in lifecycle callbacks and in dependency injection + * methods. + * + *
      • Now let's describe which thread is executing the jobs scheduled in a Component Serial Queue: + * When a job (J1) is scheduled in the queue while it is empty, then the current thread becomes the "master" + * and will immediately execute the Serial Queue tasks (synchronously). And if another thread + * triggers another event concurrently while the "master" thread is executing the job J1, then a job (J2) + * for this new event is just enqueued in the Serial Queue, but the other thread returns + * immediately to the caller, and the job J2 will then be executed by the "master" thread (after J1). + *
      + * + *

      + * This mechanism allows to serially handle all Component events (service dependencies) in FIFO order + * without maintaining any locks. + * + *

      Enabling parallelism with a ComponentExecutorFactory

      + * + * As described above, all the external events that influence the state of a given component are handed by + * jobs scheduled in the Serial Queue of the Component, and the jobs are getting executed serially + * by a single "master" thread. So usually, bundles are started from a single thread, meaning that all Components + * are then activated synchronously. + *

      + * + * But when you register in the OSGi service registry a ComponentExecutorFactory, that factory + * will be used by DependencyManager to create an Executor of your choice for each Component, typically a shared + * threadpool configured by yourself. And all the Component Serial Queues will be executed using + * the Executor returned by the {@link #getExecutorFor(Component)} method. + * However, jobs scheduled in the Serial Queue of a given Component are still executed one at a + * time, in FIFO order and the Component remains single threaded, and independent Components + * may then each be managed and activated concurrently with respect to each other. + *

      + * If you want to ensure that all Components are initialized after the ComponentExecutorFactory is + * registered in the OSGI registry, you can use the "org.apache.felix.dependencymanager.parallel" OSGi + * system property which specifies the list of components which must wait for the ComponentExecutorFactory + * service. This property value can be set to a wildcard ("*"), or a list of components implementation class + * prefixes (comma separated). So, all components whose class name starts with the specified prefixes will be cached + * until the ComponentExecutorFactory service is registered (In this way, it is not necessary to use + * the StartLevel service if you want to ensure that all components are started concurrently). + *

      + * + * Some class name prefixes can also be negated (using "!"), in order to exclude some components from the + * list of components using the ComponentExecutorFactory service. + *

      + * + * Notice that if the ComponentExecutorFactory itself and all its dependent services are defined using + * the Dependency Manager API, then you have to list the package of such components with a "!" + * prefix, in order to indicate that those components must not wait for a ComponentExecutorFactory service + * (since they are part of the ComponentExecutorFactory implementation !). + *

      + * + *

      Examples for the usage of the "org.apache.felix.dependencymanager.parallel" property:

      + * + *
      + * org.apache.felix.dependencymanager.parallel=*   
      + *      means all components must be cached until a ComponentExecutorFactory comes up.
      + * 
      + * org.apache.felix.dependencymanager.parallel=foo.bar, foo.zoo
      + *      means only components whose implementation class names are starting with "foo.bar" or "foo.zoo" 
      + *      must be handled using an Executor returned by the ComponentExecutorFactory service. Other Components
      + *      will be handled normally, as when there is no ComponentExecutorFactory available.
      + * 
      + * org.apache.felix.dependencymanager.parallel=!foo.threadpool, *
      + *      means all components must be delayed until the ComponentExecutorFactory comes up, except the 
      + *      components whose implementations class names are starting with "foo.threadpool" prefix). 
      + * 
      + * + *

      Examples of a ComponentExecutorFactory that provides a shared threadpool:

      + * + * First, we define the OSGi bundle context system property to enable parallelism for all DM Components + * excepts the one which declares the ComponentExecutorFactory: + * + *
      + *   org.apache.felix.dependencymanager.parallel=!com.acme.management.threadpool, *
      + * 
      + * + * Next, here is the Activator which declares the ComponentExecutorFactory: + * + *
      + *   package com.acme.management.threadpool;
      + *   import org.apache.felix.dm.*;
      + *   
      + *   public class Activator extends DependencyActivatorBase {      
      + *      public void init(BundleContext context, DependencyManager mgr) throws Exception {
      + *         mgr.add(createComponent()
      + *            .setInterface(ComponentExecutorFactory.class.getName(), null)
      + *            .setImplementation(ComponentExecutorFactoryImpl.class)
      + *            .add(createConfigurationDependency()
      + *                 .setPid("com.acme.management.threadpool.ComponentExecutorFactoryImpl")));
      + *      }
      + *   }
      + * 
      + * + * And here is the implementation for our ComponentExecutorFactory: + * + *
      + *   package com.acme.management.threadpool;
      + *   import org.apache.felix.dm.*;
      + *
      + *  public class ComponentExecutorFactoryImpl implements ComponentExecutorFactory {
      + *      volatile Executor m_threadPool;
      + *      
      + *      void updated(Dictionary conf) {
      + *          m_sharedThreadPool = Executors.newFixedThreadPool(Integer.parseInt("threadpool.size"));
      + *      }
      + *
      + *      @Override
      + *      public Executor getExecutorFor(Component component) {
      + *          return m_sharedThreadPool; // Use a shared threadpool for all Components
      + *      }
      + *  }
      + * 
      + * + * @author Felix Project Team + * @since 4.0.0 + */ +public interface ComponentExecutorFactory { + /** + * Returns an Executor (typically a shared thread pool) used to manage a given DependencyManager Component. + * + * @param component the Component to be managed by the returned Executor + * @return an Executor used to manage the given component, or null if the component must not be managed using any executor. + */ + Executor getExecutorFor(Component component); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ComponentState.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ComponentState.java new file mode 100644 index 00000000000..d60fc1a6329 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ComponentState.java @@ -0,0 +1,76 @@ +/* + * 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.dm; + +/** + * Component states. Any state listeners registered using @link {@link Component#add(ComponentStateListener)} method + * are notified with the following stated whenever the component state changes. + * + * @author Felix Project Team + */ +public enum ComponentState { + /** + * The component is not currently started, and is inactive. + */ + INACTIVE, + + /** + * The component is waiting for some required dependencies defined in the Activator. + * At this point the component has not yet been called in its init callback. + */ + WAITING_FOR_REQUIRED, + + /** + * The component is instantiated and waiting for dynamic required dependencies defined in init callback. + */ + INSTANTIATED_AND_WAITING_FOR_REQUIRED, + + /** + * The component is starting. At this point, all required dependencies have been injected (including + * dynamic dependencies added from the init method), but the start callback has not yet been called and + * the service has not been registered yet. + */ + STARTING, + + /** + * The component has been called in its started callback. At this point, the component has not yet been registered + * in the service registry. + */ + STARTED, + + /** + * The component is started. At this point, the component:

      + *

        + *
      • has been called in its start callback + *
      • the optional dependency callbacks have been invoked (if some optional dependencies are available) + *
      • and the service has been registered + *
      + */ + TRACKING_OPTIONAL, + + /** + * the component is stopping. At this point, the service is still registered. + */ + STOPPING, + + /** + * the component is stopped. At this point, the service has been unregistered. + */ + STOPPED +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ComponentStateListener.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ComponentStateListener.java new file mode 100644 index 00000000000..a1b54493acc --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ComponentStateListener.java @@ -0,0 +1,36 @@ +/* + * 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.dm; + +/** + * This interface can be used to register a component state listener. Component + * state listeners are called whenever a component state changes. You get notified + * when the internal component state is changed, when the component is starting, + * started, stopping and stopped. Each callback includes a reference to the component in question. + * + * @author Felix Project Team + */ +public interface ComponentStateListener { + /** + * Called on each component state change. + * @param c the component + * @param state the new component state + */ + public void changed(Component c, ComponentState state); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ConfigurationDependency.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ConfigurationDependency.java new file mode 100644 index 00000000000..9d7a1a310ac --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ConfigurationDependency.java @@ -0,0 +1,293 @@ +/* + * 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.dm; + +import java.util.Collection; +import java.util.Dictionary; +import java.util.Map; + +import org.osgi.annotation.versioning.ProviderType; + +/** + * Configuration dependency that can track the availability of a (valid) configuration. To use + * it, specify a PID for the configuration. The dependency is required by default. If you define + * an optional configuration dependency, the updated callback will be invoked with an empty Dictionary, + * or with a type-safe configuration (which in this case can provide some default methods that you can + * use to inialize your component). + *

      + * Also, only managed services are supported, not factories. If you need support for factories, then + * you can use + * {@link DependencyManager#createFactoryConfigurationAdapterService(String, String, boolean)}. + * There are a couple of things you need to be aware of when implementing the + * updated(Dictionary) method:

      + *

        + *
      • Make sure it throws a ConfigurationException or any other exception when you + * get a configuration that is invalid. In this case, the dependency will not change: + * if it was not available, it will still not be. If it was available, it will remain available + * and implicitly assume you keep working with your old configuration.
      • + *
      • This method will be called before all required dependencies are available. Make sure you + * do not depend on these to parse your settings.
      • + *
      • When the configuration is lost, updated callback is invoked with a null dictionary parameter, + * and then the component stop lifecycle callback is invoked. + *
      • When the DM component is stopped, then updated(null) is not invoked. + *
      + * + *

      The callback invoked when a configuration dependency is updated can supports the following signatures:

      + *

        + *
      • callback(Dictionary) + *
      • callback(Component, Dictionary) + *
      • callback(Component, Configuration ... configTypes) // type safe configuration interface(s) + *
      • callback(Configuration ... configTypes) // type safe configuration interface(s) + *
      • callback(Dictionary, Configuration ... configTypes) // type safe configuration interfaces(s) + *
      • callback(Component, Dictionary, Configuration ... configTypes) // type safe configuration interfaces(s) + *
      + * + *

      Support for a custom Configuration type is a new feature that allows you to specify an interface that is implemented + * by DM and such interface is then injected to your callback instead of the actual Dictionary. + * Using such configuration interface provides a way for creating type-safe configurations from a actual {@link Dictionary} that is + * normally injected by Dependency Manager. + * The callback accepts in argument an interface that you have to provide, and DM will inject a proxy that converts + * method calls from your configuration-type to lookups in the actual map or dictionary. The results of these lookups are then + * converted to the expected return type of the invoked configuration method.
      + * As proxies are injected, no implementations of the desired configuration-type are necessary! + *

      + *

      + * The lookups performed are based on the name of the method called on the configuration type. The method names are + * "mangled" to the following form: [lower case letter] [any valid character]*. Method names starting with + * get or is (JavaBean convention) are stripped from these prefixes. For example: given a dictionary + * with the key "foo" can be accessed from a configuration-type using the following method names: + * foo(), getFoo() and isFoo().

      + * + * If the property contains a dot (which is invalid in java method names), then dots (".") can be converted using the following conventions: + *

        + * + *
      • if the method name follows the javabean convention and/or kamel casing convention, then each capital letter is assumed to map to a "dot", + * followed by the same letter in lower case. This means only lower case properties are + * supported in this case. Example: getFooBar() or fooBar() will map to "foo.bar" property. + * + *
      • else, if the method name follows the standard OSGi metatype specification, then dots + * are encoded as "_"; and "_" is encoded as "__". (see OSGi r6 compendium, chapter 105.9.2). + * Example: "foo_BAR()" is mapped to "foo.BAR" property; "foo__BAR_zoo()" is mapped to "foo_BAR.zoo" property. + *
      + *

      + * The return values supported are: primitive types (or their object wrappers), strings, enums, arrays of + * primitives/strings, {@link Collection} types, {@link Map} types, {@link Class}es and interfaces. When an interface is + * returned, it is treated equally to a configuration type, that is, it is returned as a proxy. + *

      + *

      + * Arrays can be represented either as comma-separated values, optionally enclosed in square brackets. For example: + * [ a, b, c ] and a, b,c are both considered an array of length 3 with the values "a", "b" and "c". + * Alternatively, you can append the array index to the key in the dictionary to obtain the same: a dictionary with + * "arr.0" => "a", "arr.1" => "b", "arr.2" => "c" would result in the same array as the earlier examples. + *

      + *

      + * Maps can be represented as single string values similarly as arrays, each value consisting of both the key and value + * separated by a dot. Optionally, the value can be enclosed in curly brackets. Similar to array, you can use the same + * dot notation using the keys. For example, a dictionary with + * + *

      {@code "map" => "{key1.value1, key2.value2}"}
      + * + * and a dictionary with

      + * + *

      {@code "map.key1" => "value1", "map2.key2" => "value2"}
      + * + * result in the same map being returned. + * Instead of a map, you could also define an interface with the methods getKey1() and getKey2 and use + * that interface as return type instead of a {@link Map}. + *

      + * In case a lookup does not yield a value from the underlying map or dictionary, the following rules are applied: + *

        + *
      1. primitive types yield their default value, as defined by the Java Specification; + *
      2. string, {@link Class}es and enum values yield null; + *
      3. for arrays, collections and maps, an empty array/collection/map is returned; + *
      4. for other interface types that are treated as configuration type a null-object is returned. + *
      + *

      + * @author Felix Project Team + */ +@ProviderType +public interface ConfigurationDependency extends Dependency, ComponentDependencyDeclaration { + /** + * Sets the name of the callback method that should be invoked when a configuration + * is available. The contract for this method is identical to that of + * ManagedService.updated(Dictionary) throws ConfigurationException. + * By default, if this method is not called, the callback name is "updated". + * + *

      The callback is invoked on the instantiated component. + * + * @param callback the name of the callback method + */ + ConfigurationDependency setCallback(String callback); + + /** + * Sets the name of the callback method that should be invoked when a configuration + * is available. The contract for this method is identical to that of + * ManagedService.updated(Dictionary) throws ConfigurationException. + * + *

      the callback is invoked on the callback instance, and the component is not + * yet instantiated at the time the callback is invoked. + * + * @param instance the object to invoke the callback on + * @param callback the name of the callback method + */ + ConfigurationDependency setCallback(Object instance, String callback); + + /** + * Sets the name of the callback method that should be invoked when a configuration + * is available. The contract for this method is identical to that of + * ManagedService.updated(Dictionary) throws ConfigurationException. + * + *

      the callback is invoked on the callback instance, and if needsInstance is true, + * the component is instantiated at the time the callback is invoked + * + * @param instance the object to invoke the callback on. + * @param callback the name of the callback method + * @param needsInstance true if the component must be instantiated before the callback is invoked on the callback instance. + */ + ConfigurationDependency setCallback(Object instance, String callback, boolean needsInstance); + + /** + * Sets the name of the callback method that should be invoked when a configuration + * is available. The contract for this method is identical to that of + * ManagedService.updated(Dictionary) throws ConfigurationException with the difference that + * instead of a Dictionary it accepts an interface of the given configuration type.
      + * By default, the pid is assumed to match the fqdn of the configuration type. + * + *

      The callback is invoked on the instantiated component. + * + * @param callback the name of the callback method + * @param configType the configuration type that the callback method accepts. + */ + ConfigurationDependency setCallback(String callback, Class configType); + + /** + * Sets the name of the callback method that should be invoked when a configuration + * is available. The contract for this method is identical to that of + * ManagedService.updated(Dictionary) throws ConfigurationException with the difference that + * instead of a Dictionary it accepts an interface of the given configuration type.
      + * + *

      The callback is invoked on the callback instance, and at this point the component is not yet instantiated. + * + * @param instance the object to invoke the callback on. + * @param callback the name of the callback method + * @param configType the configuration type that the callback method accepts. + */ + ConfigurationDependency setCallback(Object instance, String callback, Class configType); + + /** + * Sets the name of the callback method that should be invoked when a configuration + * is available. The contract for this method is identical to that of + * ManagedService.updated(Dictionary) throws ConfigurationException with the difference that + * instead of a Dictionary it accepts an interface of the given configuration type.
      + * + *

      the callback is invoked on the callback instance, and if needsInstance is true, + * the component is instantiated at the time the callback is invoked + * + * @param instance the object to invoke the callback on. + * @param callback the name of the callback method + * @param configType the configuration type that the callback method accepts. + * @param needsInstance true if the component must be instantiated before the callback is invoked on the callback instance. + */ + ConfigurationDependency setCallback(Object instance, String callback, Class configType, boolean needsInstance); + + /** + * Sets the service.pid of the configuration you are depending on. + */ + ConfigurationDependency setPid(String pid); + + /** + * Sets propagation of the configuration properties to the service + * properties. Any additional service properties specified directly are + * merged with these. Configuration properties are not propagated by default. + * When configuration is propagated, component service properties won't be overriden by configuration properties having the same name, + * unless you invoke setPropagate(true, false) method. + * @param propagate true if configuration properties should be propagated to the component service properties. Configuration + * starting with a dot won't be propagated (because such property is considered as private, see Configuration Admin spec). + * @see #setPropagate(boolean, boolean) + */ + ConfigurationDependency setPropagate(boolean propagate); + + /** + * Sets propagation of the configuration properties to the service + * properties. Any additional service properties specified directly are + * merged with these. Configuration properties are not propagated by default. + * @param propagate true if the configuration properties must be propagated to the component service properties. Configuration + * starting with a dot won't be propagated (because such property is considered as private, see Configuration Admin spec). + * @param overrideServiceProperties true if propagated configuration properties should override the component service properties + * having the same property name + */ + ConfigurationDependency setPropagate(boolean propagate, boolean overrideServiceProperties); + + /** + * The label used to display the tab name (or section) where the properties + * are displayed. Example: "Printer Service". + * @return The label used to display the tab name where the properties are + * displayed (may be localized) + */ + ConfigurationDependency setHeading(String heading); + + /** + * A human readable description of the PID this configuration is associated + * with. Example: "Configuration for the PrinterService bundle". + * + * @return A human readable description of the PID this configuration is + * associated with (may be localized) + */ + ConfigurationDependency setDescription(String description); + + /** + * Points to the basename of the Properties file that can localize the Meta + * Type informations. The default localization base name for the properties + * is OSGI-INF/l10n/bundle, but can be overridden by the manifest + * Bundle-Localization header (see core specification, in section + * Localization on page 68). You can specify a specific localization + * basename file using this method (e.g. + * setLocalization("person") will match person_du_NL.properties + * in the root bundle directory. + */ + ConfigurationDependency setLocalization(String path); + + /** + * Adds a MetaData regarding a given configuration property. + */ + ConfigurationDependency add(PropertyMetaData properties); + + /** + * Sets the required flag which determines if this configuration dependency is required or not. + * A configuration dependency is required by default. + * + * @param required the required flag + * @return this service dependency + */ + ConfigurationDependency setRequired(boolean required); + + /** + * Specifies if the component instance should be instantiated when this dependency is started. + * The component is always instantiated when a Configuration dependency is defined, except + * if you set a callback instance or if you invoke this method with false + */ + ConfigurationDependency needsInstance(boolean needsInstance); + + /** + * Sets the configuration property type(s) that are passed to the updated callback parameters + * + * @param configType the configuration type(s) that the callback method accepts. + */ + ConfigurationDependency setConfigType(Class ... configType); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/Dependency.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/Dependency.java new file mode 100644 index 00000000000..50d7b462220 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/Dependency.java @@ -0,0 +1,93 @@ +/* + * 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.dm; + +import java.util.Dictionary; + +/** + * Generic dependency for a component. + * Can be added to a single component. Can be available, or not.. + * + * @author Felix Project Team + */ +public interface Dependency { + /** + * Returns true if this a required dependency. Required dependencies + * are dependencies that must be available before the component can be activated. + * + * @return true if the dependency is required + */ + public boolean isRequired(); + + /** + * Returns true if the dependency is available. + * + * @return true if the dependency is available + */ + public boolean isAvailable(); + + /** + * Returns true if auto configuration is enabled for this dependency. + * Auto configuration means that a dependency is injected in the component instance + * when it's available, and if it's unavailable, a "null object" will be inserted + * instead. + * + * @return true if auto configuration is enabled for this dependency + */ + public boolean isAutoConfig(); + + /** + * Returns the name of the member in the class of the component instance + * to inject into. If you specify this, not all members of the right + * type will be injected, only the member whose name matches. + * + * @return the name of the member in the class of the component instance to inject into + */ + public String getAutoConfigName(); + + /** + * Determines if the properties associated with this dependency should be propagated to + * the properties of the service registered by the component they belong to. + * + * @see Dependency#getProperties() + * + * @return true if the properties should be propagated + */ + public boolean isPropagated(); + + /** + * Determines if the propagated dependency properties must override the component service properties. + * By default, propagated service properties don't override the component service properties. It means + * that a given dependency property does not override the same property in the component service properties. + * + * @return true if the dependency service properties are propagated and override the component service properties. + */ + public default boolean overrideServiceProperties() { + return false; + } + + /** + * Returns the properties associated with this dependency. + * + * @see Dependency#isPropagated() + * + * @return the properties + */ + public Dictionary getProperties(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/DependencyActivatorBase.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/DependencyActivatorBase.java new file mode 100644 index 00000000000..c15d1fc4ff7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/DependencyActivatorBase.java @@ -0,0 +1,235 @@ +/* + * 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.dm; + +import java.util.function.Consumer; + +import org.apache.felix.dm.compat.DependencyActivatorBaseCompat; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +/** + * Base bundle activator class. Subclass this activator if you want to use dependency + * management in your bundle. There are two methods you should implement: + * init() and destroy(). Both methods take two arguments, + * the bundle context and the dependency manager. The dependency manager can be used + * to define all the dependencies. + * + * @author Felix Project Team + */ +public abstract class DependencyActivatorBase extends DependencyActivatorBaseCompat implements BundleActivator { + private BundleContext m_context; + private DependencyManager m_manager; + private Logger m_logger; + + /** + * Initialize the dependency manager. Here you can add all components and their dependencies. + * If something goes wrong and you do not want your bundle to be started, you can throw an + * exception. This exception will be passed on to the start() method of the + * bundle activator, causing the bundle not to start. + * + * @param context the bundle context + * @param manager the dependency manager + * @throws Exception if the initialization fails + */ + public abstract void init(BundleContext context, DependencyManager manager) throws Exception; + + /** + * Destroy the dependency manager. Here you can remove all components and their dependencies. + * Actually, the base class will clean up your dependencies anyway, so most of the time you + * don't need to do anything here. + *

      + * If something goes wrong and you do not want your bundle to be stopped, you can throw an + * exception. This exception will be passed on to the stop() method of the + * bundle activator, causing the bundle not to stop. + * + * @param context the bundle context + * @param manager the dependency manager + * @throws Exception if the destruction fails + */ + public void destroy(BundleContext context, DependencyManager manager) throws Exception { } + + /** + * Start method of the bundle activator. Initializes the dependency manager + * and calls init(). + * + * @param context the bundle context + */ + public void start(BundleContext context) throws Exception { + m_context = context; + m_logger = new Logger(context); + m_manager = new DependencyManager(context, m_logger); + setDependencyManager(m_manager); + init(m_context, m_manager); + } + + /** + * Stop method of the bundle activator. Calls the destroy() method + * and cleans up all left over dependencies. + * + * @param context the bundle context + */ + public void stop(BundleContext context) throws Exception { + destroy(m_context, m_manager); + m_manager.clear(); + m_manager = null; + m_context = null; + } + + /** + * Returns the bundle context that is associated with this bundle. + * + * @return the bundle context + */ + public BundleContext getBundleContext() { + return m_context; + } + + /** + * Returns the dependency manager that is associated with this bundle. + * + * @return the dependency manager + */ + public DependencyManager getDependencyManager() { + return m_manager; + } + + /** + * Returns the logger that is associated with this bundle. A logger instance + * is a proxy that will log to a real OSGi logservice if available and standard + * out if not. + * + * @return the logger + */ + public Logger getLogger() { + return m_logger; + } + + /** + * Creates a new component. + * + * @return the new component + */ + public Component createComponent() { + return m_manager.createComponent(); + } + + /** + * Creates a new bundle adapter component. + * + * @return the bundle adapter component + */ + public BundleComponent createBundleComponent() { + return m_manager.createBundleComponent(); + } + + /** + * Creates a new aspect component. + * + * @return the aspect component + * @see DependencyManager#createAspectComponent() + */ + public AspectComponent createAspectComponent() { + return m_manager.createAspectComponent(); + } + + /** + * Creates a new adapter service. + * + * @return the adapter service + * @see DependencyManager#createAdapterService(Class, String) + */ + public AdapterComponent createAdapterComponent() { + return m_manager.createAdapterComponent(); + } + + /** + * Creates a new factory configuration component. + * @return the factory configuration component + * @see DependencyManager#createFactoryComponent() + */ + public FactoryComponent createFactoryComponent() { + return m_manager.createFactoryComponent(); + } + + /** + * Creates a new service dependency. + * + * @return the service dependency + */ + public ServiceDependency createServiceDependency() { + return m_manager.createServiceDependency(); + } + + /** + * Creates a new temporal service dependency. + * + * @param timeout the max number of milliseconds to wait for a service availability. + * @return the service dependency + */ + public ServiceDependency createTemporalServiceDependency(long timeout) { + return m_manager.createTemporalServiceDependency(timeout); + } + + /** + * Creates a new configuration dependency. + * + * @return the configuration dependency + */ + public ConfigurationDependency createConfigurationDependency() { + return m_manager.createConfigurationDependency(); + } + + /** + * Creates a new configuration property metadata. + * + * @return the configuration property metadata + */ + public PropertyMetaData createPropertyMetaData() { + return m_manager.createPropertyMetaData(); + } + + /** + * Creates a new bundle dependency. + * + * @return the bundle dependency + */ + public BundleDependency createBundleDependency() { + return m_manager.createBundleDependency(); + } + + /** + * Creates a new resource dependency. + * + * @return the resource dependency + */ + public ResourceDependency createResourceDependency() { + return m_manager.createResourceDependency(); + } + + /** + * Creates a new resource adapter component. + * + * @return the resource adapter component + */ + public ResourceComponent createResourceComponent() { + return m_manager.createResourceComponent(); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/DependencyManager.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/DependencyManager.java new file mode 100644 index 00000000000..f2c0435613a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/DependencyManager.java @@ -0,0 +1,322 @@ +/* + * 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.dm; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.felix.dm.compat.DependencyManagerCompat; +import org.apache.felix.dm.impl.AdapterServiceImpl; +import org.apache.felix.dm.impl.AspectServiceImpl; +import org.apache.felix.dm.impl.BundleAdapterImpl; +import org.apache.felix.dm.impl.BundleDependencyImpl; +import org.apache.felix.dm.impl.ComponentImpl; +import org.apache.felix.dm.impl.ComponentScheduler; +import org.apache.felix.dm.impl.ConfigurationDependencyImpl; +import org.apache.felix.dm.impl.FactoryConfigurationAdapterImpl; +import org.apache.felix.dm.impl.ResourceAdapterImpl; +import org.apache.felix.dm.impl.ResourceDependencyImpl; +import org.apache.felix.dm.impl.ServiceDependencyImpl; +import org.apache.felix.dm.impl.TemporalServiceDependencyImpl; +import org.apache.felix.dm.impl.index.ServiceRegistryCache; +import org.apache.felix.dm.impl.index.ServiceRegistryCacheManager; +import org.apache.felix.dm.impl.metatype.PropertyMetaDataImpl; +import org.osgi.framework.BundleContext; + +/** + * The dependency manager manages all components and their dependencies. Using + * this API you can declare all components and their dependencies. Under normal + * circumstances, you get passed an instance of this class through the + * DependencyActivatorBase subclass you use as your + * BundleActivator, but it is also possible to create your + * own instance. + * + * @author Felix Project Team + */ +public class DependencyManager extends DependencyManagerCompat { + /** + * The DependencyManager Activator will wait for a threadpool before creating any DM components if the following + * OSGi system property is set to true. + */ + public final static String PARALLEL = "org.apache.felix.dependencymanager.parallel"; + + public static final String ASPECT = "org.apache.felix.dependencymanager.aspect"; + public static final String SERVICEREGISTRY_CACHE_INDICES = "org.apache.felix.dependencymanager.filterindex"; + public static final String METHOD_CACHE_SIZE = "org.apache.felix.dependencymanager.methodcache"; + + private final BundleContext m_context; + private final Logger m_logger; + private final ConcurrentHashMap m_components = new ConcurrentHashMap<>(); + + private static final Set> m_dependencyManagers = new HashSet<>(); + + /** + * Creates a new dependency manager. You need to supply the + * BundleContext to be used by the dependency + * manager to register services and communicate with the + * framework. + * + * @param context the bundle context + */ + public DependencyManager(BundleContext context) { + this(context, new Logger(context)); + } + + DependencyManager(BundleContext context, Logger logger) { + m_context = createContext(context); + m_logger = logger; + synchronized (m_dependencyManagers) { + m_dependencyManagers.add(new WeakReference(this)); + } + } + + public Logger getLogger() { + return m_logger; + } + + /** + * Returns the list of currently created dependency managers. + * @return the list of currently created dependency managers + */ + public static List getDependencyManagers() { + List result = new ArrayList<>(); + synchronized (m_dependencyManagers) { + Iterator> iterator = m_dependencyManagers.iterator(); + while (iterator.hasNext()) { + WeakReference reference = iterator.next(); + DependencyManager manager = reference.get(); + if (manager != null) { + try { + manager.getBundleContext().getBundle(); + result.add(manager); + continue; + } + catch (IllegalStateException e) { + } + } + iterator.remove(); + } + } + return result; + } + + /** + * Returns the bundle context associated with this dependency manager. + * @return the bundle context associated with this dependency manager. + */ + public BundleContext getBundleContext() { + return m_context; + } + + /** + * Adds a new component to the dependency manager. After the service is added + * it will be started immediately. + * + * @param c the service to add + */ + public void add(Component c) { + m_components.put(c, c); + ComponentScheduler.instance().add(c); + } + + /** + * Removes a service from the dependency manager. Before the service is removed + * it is stopped first. + * + * @param c the component to remove + */ + public void remove(Component c) { + ComponentScheduler.instance().remove(c); + m_components.remove(c); + } + + /** + * Creates a new component. + * + * @return the new component + */ + public Component createComponent() { + return new ComponentImpl(m_context, this, m_logger); + } + + /** + * Creates a new service dependency. + * + * @return the service dependency + */ + public ServiceDependency createServiceDependency() { + return new ServiceDependencyImpl(); + } + + /** + * Creates a new configuration dependency. + * + * @return the configuration dependency + */ + public ConfigurationDependency createConfigurationDependency() { + return new ConfigurationDependencyImpl(m_context, m_logger); + } + + /** + * Creates a new bundle dependency. + * + * @return a new BundleDependency instance. + */ + public BundleDependency createBundleDependency() { + return new BundleDependencyImpl(); + } + + /** + * Creates a new resource dependency. + * + * @return the resource dependency + */ + public ResourceDependency createResourceDependency() { + return new ResourceDependencyImpl(); + } + + /** + * Creates a new timed required service dependency. A timed dependency blocks the invoker thread is the required dependency + * is currently unavailable, until it comes up again. + * + * @return a new timed service dependency + */ + public ServiceDependency createTemporalServiceDependency(long timeout) { + return new TemporalServiceDependencyImpl(m_context, timeout); + } + + /** + * Creates a new adapter component. The adapter will be applied to any service that + * matches the specified interface and filter. For each matching service + * an adapter will be created based on the adapter implementation class. + * The adapter will be registered with the specified interface and existing properties + * from the original service plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + * @return an adapter component + */ + public AdapterComponent createAdapterComponent() { + return new AdapterServiceImpl(this); + } + + /** + * Creates a new Factory Component. For each new factory configuration matching + * the factoryPid, a component will be created based on the component implementation class. + * The component will be registered with the specified interface, and with the specified service properties. + * Depending on the propagate parameter, every public factory configuration properties + * (which don't start with ".") will be propagated along with the adapter service properties. + * It will also inherit all dependencies. + * + * @return a factory pid component + */ + public FactoryComponent createFactoryComponent() { + return new FactoryConfigurationAdapterImpl(this); + } + + /** + * Creates a new bundle adapter. The adapter will be applied to any bundle that + * matches the specified bundle state mask and filter condition. For each matching + * bundle an adapter will be created based on the adapter implementation class. + * The adapter will be registered with the specified interface + * + * TODO and existing properties from the original resource plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + * @return a service that acts as a factory for generating bundle adapters + */ + public BundleComponent createBundleComponent() { + return new BundleAdapterImpl(this); + } + + /** + * Creates a new resource adapter component. The adapter will be applied to any resource that + * matches the specified filter condition. For each matching resource + * an adapter will be created based on the adapter implementation class. + * The adapter will be registered with the specified interface and existing properties + * from the original resource plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + * @return a Resource Adapter Component + */ + public ResourceComponent createResourceComponent() { + return new ResourceAdapterImpl(this); + } + + /** + * Returns a list of components. + * + * @return a list of components + */ + public List getComponents() { + return Collections.list(m_components.elements()); + } + + /** + * Creates a new aspect component. The aspect will be applied to any service that + * matches the specified interface and filter. For each matching service + * an aspect will be created based on the aspect implementation class. + * The aspect will be registered with the same interface and properties + * as the original service, plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + * @return an aspect component + */ + public AspectComponent createAspectComponent() { + return new AspectServiceImpl(this); + } + + /** + * Removes all components and their dependencies. + */ + public void clear() { + for (Component component : m_components.keySet()) { + remove(component); + } + m_components.clear(); + } + + /** + * Creates a new configuration property metadata. + * + * @return the configuration property metadata. + */ + public PropertyMetaData createPropertyMetaData() { + return new PropertyMetaDataImpl(); + } + + private BundleContext createContext(BundleContext context) { + ServiceRegistryCache cache = ServiceRegistryCacheManager.getCache(); + if (cache != null) { + return cache.createBundleContextInterceptor(context); + } + else { + return context; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/FactoryComponent.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/FactoryComponent.java new file mode 100644 index 00000000000..f8ea1492a97 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/FactoryComponent.java @@ -0,0 +1,391 @@ +/* + * 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.dm; + +import java.util.Dictionary; + +import org.apache.felix.dm.Component.ServiceScope; + +/** + * Interface used to configure the various parameters needed when defining + * a Dependency Manager factory component. + * A factory component is a component which can be instantiated multiple times using standard + * OSGi Factory Configurations.

      + * + * When a factory configuration is created, an instance of the component is created and the configuration + * is injected by default in the "updated" callback, which supports the following signatures: + * + *

        + *
      • callback(Dictionary) + *
      • callback(Component, Dictionary) + *
      • callback(Component, Configuration ... configTypes) // type safe configuration interface(s) + *
      • callback(Configuration ... configTypes) // type safe configuration interface(s) + *
      • callback(Dictionary, Configuration ... configTypes) // type safe configuration interfaces(s) + *
      • callback(Component, Dictionary, Configuration ... configTypes) // type safe configuration interfaces(s) + *
      + * + *

      Usage Examples

      + * + * Here is a sample showing a Hello component, which can be instantiated multiple times using a factory configuration: + * + *
      + * {@code
      + * public class Activator extends DependencyActivatorBase {
      + *     &Override
      + *     public void init(BundleContext context, DependencyManager dm) throws Exception {
      + *         FactoryComponent factoryComponent = createFactoryComponent()
      + *             .setFactoryPid("my.factory.pid")
      + *             .setInterface(MySevice.class.getName(), null)
      + *             .setImplementation(MyComponent.class)
      + *             .setConfigType(MyConfig.class);
      + *         dm.add(factoryComponent);
      + *     }
      + * }
      + * 
      + * public interface MyConfig {
      + *     int getPort();
      + *     String getAddress();
      + * }
      + * 
      + * public class MyComponent implements MyService {
      + *     void updated(MyConfig cnf) {
      + *         int port = cnf.getPort();
      + *         String addr = cnf.getAddress();
      + *         ...
      + *     }
      + * }
      + * } 
      + * + * @see DependencyManager#createFactoryComponent() + */ +public interface FactoryComponent extends Component { + + /** + * Sets the component scope. + * @param scope the component scope (default=SINGLETON) + * + * @return this component + */ + FactoryComponent setScope(ServiceScope scope); + + /** + * Sets the implementation for this component. You can actually specify + * an instance you have instantiated manually, or a Class + * that will be instantiated using its default constructor when the + * required dependencies are resolved, effectively giving you a lazy + * instantiation mechanism. + * + * There are four special methods that are called when found through + * reflection to give you life cycle management options: + *
        + *
      1. init() is invoked after the instance has been + * created and dependencies have been resolved, and can be used to + * initialize the internal state of the instance or even to add more + * dependencies based on runtime state
      2. + *
      3. start() is invoked right before the service is + * registered
      4. + *
      5. stop() is invoked right after the service is + * unregistered
      6. + *
      7. destroy() is invoked after all dependencies are + * removed
      8. + *
      + * In short, this allows you to initialize your instance before it is + * registered, perform some post-initialization and pre-destruction code + * as well as final cleanup. If a method is not defined, it simply is not + * called, so you can decide which one(s) you need. If you need even more + * fine-grained control, you can register as a service state listener too. + * + * @param implementation the implementation + * @return this component + * @see ComponentStateListener + */ + FactoryComponent setImplementation(Object implementation); + + /** + * Adds dependency(ies) to this component, atomically. If the component is already active or if you add + * dependencies from the init method, then you should add all the dependencies in one single add method call + * (using the varargs argument), because this method may trigger component activation (like + * the ServiceTracker.open() method does). + * + * @param dependencies the dependencies to add. + * @return this component + */ + FactoryComponent add(Dependency ... dependencies); + + /** + * Removes a dependency from the component. + * @param d the dependency to remove + * @return this component + */ + FactoryComponent remove(Dependency d); + + /** + * Adds a component state listener to this component. + * + * @param listener the state listener + */ + FactoryComponent add(ComponentStateListener listener); + + /** + * Removes a component state listener from this component. + * + * @param listener the state listener + */ + FactoryComponent remove(ComponentStateListener listener); + + /** + * Sets the public interface under which this component should be registered + * in the OSGi service registry. + * + * @param serviceName the name of the service interface + * @param properties the properties for this service + * @return this component + */ + FactoryComponent setInterface(String serviceName, Dictionary properties); + + /** + * Sets the public interfaces under which this component should be registered + * in the OSGi service registry. + * + * @param serviceNames the names of the service interface + * @param properties the properties for these services + * @return this component + */ + FactoryComponent setInterface(String[] serviceNames, Dictionary properties); + + /** + * Sets the public interface under which this component should be registered + * in the OSGi service registry. + * + * @param serviceName the name of the service interface + * @param properties the properties for this service + * @return this component + */ + FactoryComponent setInterface(Class serviceName, Dictionary properties); + + /** + * Sets the public interfaces under which this component should be registered + * in the OSGi service registry. + * + * @param serviceNames the names of the service interface + * @param properties the properties for these services + * @return this component + */ + FactoryComponent setInterface(Class[] serviceNames, Dictionary properties); + + /** + * Configures auto configuration of injected classes in the component instance. + * The following injections are currently performed, unless you explicitly + * turn them off: + *
      + *
      BundleContext
      the bundle context of the bundle
      + *
      ServiceRegistration
      the service registration used to register your service
      + *
      DependencyManager
      the dependency manager instance
      + *
      Component
      the component instance of the dependency manager
      + *
      + * + * @param clazz the class (from the list above) + * @param autoConfig false to turn off auto configuration + */ + FactoryComponent setAutoConfig(Class clazz, boolean autoConfig); + + /** + * Configures auto configuration of injected classes in the component instance. + * + * @param clazz the class (from the list above) + * @param instanceName the name of the instance to inject the class into + * @see #setAutoConfig(Class, boolean) + */ + FactoryComponent setAutoConfig(Class clazz, String instanceName); + + /** + * Sets the service properties associated with the component. If the service + * was already registered, it will be updated. + * + * @param serviceProperties the properties + */ + FactoryComponent setServiceProperties(Dictionary serviceProperties); + + /** + * Sets the names of the methods used as callbacks. These methods, when found, are + * invoked as part of the life cycle management of the component implementation. The + * dependency manager will look for a method of this name with the following signatures, + * in this order: + *
        + *
      1. method(Component component)
      2. + *
      3. method()
      4. + *
      + * + * @param init the name of the init method + * @param start the name of the start method + * @param stop the name of the stop method + * @param destroy the name of the destroy method + * @return the component + */ + FactoryComponent setCallbacks(String init, String start, String stop, String destroy); + + /** + * Sets the names of the methods used as callbacks. These methods, when found, are + * invoked on the specified instance as part of the life cycle management of the component + * implementation. + *

      + * See setCallbacks(String init, String start, String stop, String destroy) for more + * information on the signatures. Specifying an instance means you can create a manager + * that will be invoked whenever the life cycle of a component changes and this manager + * can then decide how to expose this life cycle to the actual component, offering an + * important indirection when developing your own component models. + * + * @return this component + */ + FactoryComponent setCallbacks(Object instance, String init, String start, String stop, String destroy); + + /** + * Sets the factory to use to create the implementation. You can specify + * both the factory class and method to invoke. The method should return + * the implementation, and can use any method to create it. Actually, this + * can be used together with setComposition to create a + * composition of instances that work together to implement a component. The + * factory itself can also be instantiated lazily by not specifying an + * instance, but a Class. + * + * @param factory the factory instance or class + * @param createMethod the name of the create method + * @return this component + */ + FactoryComponent setFactory(Object factory, String createMethod); + + /** + * Sets the factory to use to create the implementation. You specify the + * method to invoke. The method should return the implementation, and can + * use any method to create it. Actually, this can be used together with + * setComposition to create a composition of instances that + * work together to implement a component. + *

      + * Note that currently, there is no default for the factory, so please use + * setFactory(factory, createMethod) instead. + * + * @param createMethod the name of the create method + * @return this component + */ + FactoryComponent setFactory(String createMethod); + + /** + * Sets the instance and method to invoke to get back all instances that + * are part of a composition and need dependencies injected. All of them + * will be searched for any of the dependencies. The method that is + * invoked must return an Object[]. + * + * @param instance the instance that has the method + * @param getMethod the method to invoke + * @return this component + */ + FactoryComponent setComposition(Object instance, String getMethod); + + /** + * Sets the method to invoke on the service implementation to get back all + * instances that are part of a composition and need dependencies injected. + * All of them will be searched for any of the dependencies. The method that + * is invoked must return an Object[]. + * + * @param getMethod the method to invoke + * @return this component + */ + FactoryComponent setComposition(String getMethod); + + /** + * Activate debug for this component. Informations related to dependency processing will be displayed + * using osgi log service, our to standard output if no log service is currently available. + * @param label + */ + FactoryComponent setDebug(String label); + + /** + * Sets the pid matching the factory configuration + * @param factoryPid the pid matching the factory configuration + */ + FactoryComponent setFactoryPid(String factoryPid); + + /** + * Sets the pid matching the factory configuration using the specified class. + * The FQDN of the specified class will be used as the factory pid. + * @param clazz the class whose FQDN name will be used for the factory pid + */ + FactoryComponent setFactoryPid(Class clazz); + + /** + * Sets the method name that will be notified when the factory configuration is created/updated. + * By default, the callback name used is updated + * TODO describe supported signatures + * @param update the method name that will be notified when the factory configuration is created/updated. + */ + FactoryComponent setUpdated(String update); + + /** + * Sets the object on which the updated callback will be invoked. + * By default, the callback is invoked on the component instance. + * @param updatedCallbackInstance the object on which the updated callback will be invoked. + */ + FactoryComponent setUpdateInstance(Object updatedCallbackInstance); + + /** + * Sets the propagate flag (true means all public configuration properties are propagated to service properties). + * By default, public configurations are not propagated. + * @param propagate the propagate flag (false, by default; true means all public configuration properties are propagated to service properties). + */ + FactoryComponent setPropagate(boolean propagate); + + /** + * Sets the configuration type to use instead of a dictionary. The updated callback is assumed to take + * as arguments the specified configuration types in the same order they are provided in this method. + * Optionally, the callback may define a Dictionary as the first argument, in order to also get the raw configuration. + * @param configTypes the configuration type to use instead of a dictionary + * @see ConfigurationDependency + */ + FactoryComponent setConfigType(Class ... configTypes); + + /** + * Sets the metatype label used to display the tab name (or section) where the properties are displayed. + * Example: "Printer Service" + * @param heading the label used to display the tab name (or section) where the properties are displayed. + */ + FactoryComponent setHeading(String heading); + + /** + * A metatype human readable description of the factory PID this configuration is associated with. + * @param desc + */ + FactoryComponent setDesc(String desc); + + /** + * Points to the metatype basename of the Properties file that can localize the Meta Type informations. + * The default localization base name for the properties is OSGI-INF/l10n/bundle, but can + * be overridden by the manifest Bundle-Localization header (see core specification, in section Localization + * on page 68). You can specify a specific localization basename file using this parameter + * (e.g. "person" will match person_du_NL.properties in the root bundle directory). + * @param localization + */ + FactoryComponent setLocalization(String localization); + + /** + * Sets metatype MetaData regarding configuration properties. + * @param metaData the metadata regarding configuration properties + */ + FactoryComponent add(PropertyMetaData ... metaData) ; + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/FilterIndex.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/FilterIndex.java new file mode 100644 index 00000000000..993228ba9f7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/FilterIndex.java @@ -0,0 +1,49 @@ +/* + * 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.dm; + +import java.util.List; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; + +/** + * A filter index is an interface you can implement to create your own, optimized index for specific filter expressions. + * + * @author Felix Project Team + */ +public interface FilterIndex { + /** Opens this filter index. */ + public void open(BundleContext context); + /** Closes this filter index. */ + public void close(); + /** Determines if the combination of class and filter is applicable for this filter index. */ + public boolean isApplicable(String clazz, String filter); + /** Returns all service references that match the specified class and filter. Never returns null. */ + @SuppressWarnings("rawtypes") + public List getAllServiceReferences(String clazz, String filter); + /** Invoked whenever a service event occurs. */ + public void serviceChanged(ServiceEvent event); + /** Adds a service listener to this filter index. */ + public void addServiceListener(ServiceListener listener, String filter); + /** Removes a service listener from this filter index. If the listener is not present in the filter index, this method does nothing. */ + public void removeServiceListener(ServiceListener listener); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/Logger.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/Logger.java new file mode 100644 index 00000000000..499300a7c10 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/Logger.java @@ -0,0 +1,327 @@ +/* + * 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.dm; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; +import org.osgi.service.log.LogService; + +/** + * This class mimics the standard OSGi LogService interface. An + * instance of this class is used by the dependency manager for all logging. + * By default this class logs messages to standard out. The log level can be set to + * control the amount of logging performed, where a higher number results in + * more logging. A log level of zero turns off logging completely. + * + * The log levels match those specified in the OSGi Log Service. + * This class also tracks log services and will use the highest ranking + * log service, if present, as a back end instead of printing to standard + * out. The class uses reflection to invoking the log service's method to + * avoid a dependency on the log interface, which is also why it does not + * actually implement LogService. This class is in many ways + * similar to the one used in the system bundle for that same purpose. + * + * @author Felix Project Team + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +public class Logger implements ServiceListener { + private static final String LOG_SINGLE_CONTEXT = "org.apache.felix.dependencymanager.singleContextLog"; + public static final int LOG_ERROR = 1; + public static final int LOG_WARNING = 2; + public static final int LOG_INFO = 3; + public static final int LOG_DEBUG = 4; + + private final BundleContext m_context; + + private final static int LOGGER_OBJECT_IDX = 0; + private final static int LOGGER_METHOD_IDX = 1; + private static final String ENABLED_LOG_LEVEL = "org.apache.felix.dependencymanager.loglevel"; + private ServiceReference m_logRef = null; + private Object[] m_logger = null; + private int m_enabledLevel = LogService.LOG_WARNING; + private String m_debugKey; + + public Logger(BundleContext context) { + if (context != null && "true".equals(context.getProperty(LOG_SINGLE_CONTEXT))) { + m_context = FrameworkUtil.getBundle(DependencyManager.class).getBundleContext(); + } else { + m_context = context; + } + if (m_context != null) { + String enabledLevel = m_context.getProperty(ENABLED_LOG_LEVEL); + if (enabledLevel != null) { + try { + m_enabledLevel = Integer.valueOf(enabledLevel); + } catch (NumberFormatException e) {} + } + startListeningForLogService(); + } + } + + public final void log(int level, String msg) { + _log(null, level, msg, null); + } + + public final void log(int level, String msg, Throwable throwable) { + _log(null, level, msg, throwable); + } + + public final void log(ServiceReference sr, int level, String msg) { + _log(sr, level, msg, null); + } + + public final void log(ServiceReference sr, int level, String msg, Throwable throwable) { + _log(sr, level, msg, throwable); + } + + protected void doLog(ServiceReference sr, int level, String msg, Throwable throwable) { + String s = (sr == null) ? null : "SvcRef " + sr; + s = (s == null) ? msg : s + " " + msg; + s = (throwable == null) ? s : s + " (" + throwable + ")"; + switch (level) { + case LOG_DEBUG: + System.out.println("DEBUG: " + s); + break; + case LOG_ERROR: + System.out.println("ERROR: " + s); + if (throwable != null) { + if ((throwable instanceof BundleException) && (((BundleException) throwable).getNestedException() != null)) { + throwable = ((BundleException) throwable).getNestedException(); + } + throwable.printStackTrace(); + } + break; + case LOG_INFO: + System.out.println("INFO: " + s); + break; + case LOG_WARNING: + System.out.println("WARNING: " + s); + break; + default: + System.out.println("UNKNOWN[" + level + "]: " + s); + } + } + + private void _log(ServiceReference sr, int level, String msg, Throwable throwable) { + if (level <= m_enabledLevel) { + StringBuilder sb = new StringBuilder("["); + if (m_debugKey != null) { + sb.append(m_debugKey).append(" - "); + } + sb.append(Thread.currentThread().getName()); + sb.append("] "); + sb.append(msg); + + // Save our own copy just in case it changes. We could try to do + // more conservative locking here, but let's be optimistic. + Object[] logger = m_logger; + // Use the log service if available. + if (logger != null) { + _logReflectively(logger, sr, level, sb.toString(), throwable); + } + // Otherwise, default logging action. + else { + doLog(sr, level, sb.toString(), throwable); + } + } + } + + private void _logReflectively(Object[] logger, ServiceReference sr, int level, String msg, Throwable throwable) { + if (logger != null) { + Object[] params = { sr, new Integer(level), msg, throwable }; + try { + ((Method) logger[LOGGER_METHOD_IDX]).invoke(logger[LOGGER_OBJECT_IDX], params); + } + catch (InvocationTargetException ex) { + System.err.println("Logger: " + ex); + } + catch (IllegalAccessException ex) { + System.err.println("Logger: " + ex); + } + } + } + + /** + * This method is called when the bundle context is set; + * it simply adds a service listener so that the bundle can track + * log services to be used as the back end of the logging mechanism. It also + * attempts to get an existing log service, if present, but in general + * there will never be a log service present since the system bundle is + * started before every other bundle. + */ + private synchronized void startListeningForLogService() { + try { + // add a service listener for log services, carefully avoiding any code dependency on it + m_context.addServiceListener(this, "(objectClass=org.osgi.service.log.LogService)"); + } + catch (InvalidSyntaxException ex) { + // this will never happen since the filter is hard coded + } + // try to get an existing log service + m_logRef = m_context.getServiceReference("org.osgi.service.log.LogService"); + // get the service object if available and set it in the logger + if (m_logRef != null) { + setLogger(m_context.getService(m_logRef)); + } + } + + /** + * This method implements the callback for the ServiceListener interface. + * It is public as a byproduct of implementing the interface and should + * not be called directly. This method tracks run-time changes to log + * service availability. If the log service being used by the framework's + * logging mechanism goes away, then this will try to find an alternative. + * If a higher ranking log service is registered, then this will switch + * to the higher ranking log service. + */ + public final synchronized void serviceChanged(ServiceEvent event) { + // if no logger is in use, then grab this one + if ((event.getType() == ServiceEvent.REGISTERED) && (m_logRef == null)) { + m_logRef = event.getServiceReference(); + // get the service object and set it in the logger + setLogger(m_context.getService(m_logRef)); + } + // if a logger is in use, but this one has a higher ranking, then swap + // it for the existing logger + else if ((event.getType() == ServiceEvent.REGISTERED) && (m_logRef != null)) { + ServiceReference ref = m_context.getServiceReference("org.osgi.service.log.LogService"); + if (!ref.equals(m_logRef)) { + m_context.ungetService(m_logRef); + m_logRef = ref; + setLogger(m_context.getService(m_logRef)); + } + } + // if the current logger is going away, release it and try to + // find another one + else if ((event.getType() == ServiceEvent.UNREGISTERING) && m_logRef != null && m_logRef.equals(event.getServiceReference())) { + // Unget the service object. + m_context.ungetService(m_logRef); + // Try to get an existing log service. + m_logRef = m_context.getServiceReference("org.osgi.service.log.LogService"); + // get the service object if available and set it in the logger + if (m_logRef != null) { + setLogger(m_context.getService(m_logRef)); + } + else { + setLogger(null); + } + } + } + + /** + * This method sets the new log service object. It also caches the method to + * invoke. The service object and method are stored in array to optimistically + * eliminate the need to locking when logging. + */ + private void setLogger(Object logObj) { + if (logObj == null) { + m_logger = null; + } + else { + Class[] formalParams = { ServiceReference.class, Integer.TYPE, String.class, Throwable.class }; + try { + Method logMethod = logObj.getClass().getMethod("log", formalParams); + logMethod.setAccessible(true); + m_logger = new Object[] { logObj, logMethod }; + } + catch (NoSuchMethodException ex) { + System.err.println("Logger: " + ex); + m_logger = null; + } + } + } + + public void setEnabledLevel(int enabledLevel) { + m_enabledLevel = enabledLevel; + } + + public void setDebugKey(String debugKey) { + m_debugKey = debugKey; + } + + public String getDebugKey() { + return m_debugKey; + } + + // --------------- Convenient helper log methods -------------------------------------------- + + public void err(String format, Object... params) { + if (m_enabledLevel >= LOG_ERROR) { + log(LogService.LOG_ERROR, String.format(format, params)); + } + } + + public void err(String format, Throwable err, Object... params) { + if (m_enabledLevel >= LOG_ERROR) { + log(LogService.LOG_ERROR, String.format(format, params), err); + } + } + + public void warn(String format, Object... params) { + if (m_enabledLevel >= LOG_WARNING) { + log(LogService.LOG_WARNING, String.format(format, params)); + } + } + + public void warn(String format, Throwable err, Object... params) { + if (m_enabledLevel >= LOG_WARNING) { + log(LogService.LOG_WARNING, String.format(format, params), err); + } + } + + public boolean info() { + return m_enabledLevel >= LogService.LOG_INFO; + } + + public void info(String format, Object... params) { + if (info()) { + log(LogService.LOG_INFO, String.format(format, params)); + } + } + + public void info(String format, Throwable err, Object... params) { + if (info()) { + log(LogService.LOG_INFO, String.format(format, params), err); + } + } + + public boolean debug() { + return m_enabledLevel >= LogService.LOG_DEBUG; + } + + public void debug(String format, Object... params) { + if (debug()) { + log(LogService.LOG_DEBUG, String.format(format, params)); + } + } + + public void debug(String format, Throwable err, Object... params) { + if (debug()) { + log(LogService.LOG_DEBUG, String.format(format, params), err); + } + } +} \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/PropertyMetaData.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/PropertyMetaData.java new file mode 100644 index 00000000000..3b5c0e51729 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/PropertyMetaData.java @@ -0,0 +1,105 @@ +/* + * 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.dm; + +/** + * This interface defines meta data regarding a given configuration property. + * + * @author Felix Project Team + */ +public interface PropertyMetaData { + /** + * The label used to display the property. Example: "Log Level". + * + * @return The label used to display the property (may be localized) + */ + public PropertyMetaData setHeading(String heading); + + /** + * The key of a ConfigurationAdmin property. Example: "printer.logLevel" + * + * @return The Configuration Admin property name + */ + public PropertyMetaData setId(String id); + + /** + * Returns the property primitive type. If must be either one of the following types:

      + *

        + *
      • String.class
      • + *
      • Long.class
      • + *
      • Integer.class
      • + *
      • Character.class
      • + *
      • Byte.class
      • + *
      • Double.class
      • + *
      • Float.class
      • + *
      • Boolean.class
      • + *
      + */ + public PropertyMetaData setType(Class type); + + /** + * Returns a default for this property. The object must be of the appropriate type as defined by the cardinality and getType(). + * The return type is a list of String objects that can be converted to the appropriate type. The cardinality of the return + * array must follow the absolute cardinality of this type. E.g. if the cardinality = 0, the array must contain 1 element. + * If the cardinality is 1, it must contain 0 or 1 elements. If it is -5, it must contain from 0 to max 5 elements. Note that + * the special case of a 0 cardinality, meaning a single value, does not allow arrays or vectors of 0 elements. + */ + public PropertyMetaData setDefaults(String[] defaults); + + /** + * Returns some defaults for this property. The object must be of the appropriate type as defined by the cardinality and getType(). + * The return type is a list of String objects that can be converted to the appropriate type. The cardinality of the return + * array must follow the absolute cardinality of this type. E.g. if the cardinality = 0, the array must contain 1 element. + * If the cardinality is 1, it must contain 0 or 1 elements. If it is -5, it must contain from 0 to max 5 elements. Note that + * the special case of a 0 cardinality, meaning a single value, does not allow arrays or vectors of 0 elements. + */ + public default PropertyMetaData setDefaults(String def, String ... defaults) { + String[] copy = new String[1 + defaults.length]; + copy[0] = def; + System.arraycopy(defaults, 0, copy, 1, defaults.length); + return setDefaults(copy); + } + + /** + * Returns the property description. The description may be localized and must describe the semantics of this type and any + * constraints. Example: "Select the log level for the Printer Service". + * + * @return a localizable description of the property. + */ + public PropertyMetaData setDescription(String description); + + /** + * Return the cardinality of this property. The OSGi environment handles multi valued properties in arrays ([]) or in Vector objects. + * The return value is defined as follows:

      + * + *

        + *
      • x = Integer.MIN_VALUE no limit, but use Vector
      • + *
      • x < 0 -x = max occurrences, store in Vector
      • + *
      • x > 0 x = max occurrences, store in array []
      • + *
      • x = Integer.MAX_VALUE no limit, but use array []
      • + *
      • x = 0 1 occurrence required
      • + *
      + */ + public PropertyMetaData setCardinality(int cardinality); + + /** + * Tells if this property is required or not. + */ + public PropertyMetaData setRequired(boolean required); + + /** + * Return a list of valid options for this property (the labels may be localized). + * + * @return the list of valid options for this property. + */ + public PropertyMetaData addOption(String optionLabel, String optionValue); +} \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ResourceComponent.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ResourceComponent.java new file mode 100644 index 00000000000..ab9d7c25dc1 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ResourceComponent.java @@ -0,0 +1,370 @@ +/* + * 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.dm; + +import java.util.Dictionary; + +import org.apache.felix.dm.Component.ServiceScope; + +/** + * Interface used to configure the various parameters needed when defining + * a Dependency Manager resource adapter component. + * + * The resource adapter will be applied to any resource that + * matches the specified filter condition. For each matching resource + * an adapter will be created based on the adapter implementation class. + * The adapter will be registered with the specified interface and existing properties + * from the original resource plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + *

      Usage Examples

      + * + * Here is a sample showing a VideoPlayer adapter component which plays a video found from + * a bundle having a Video-Path manifest header. + * + *
      + * {@code
      + * public class Activator extends DependencyActivatorBase {
      + *     &Override
      + *     public void init(BundleContext context, DependencyManager dm) throws Exception {
      + *          ResourceComponent resourceComponent = dm.createResourceComponent()
      + *             .setResourceFilter("(path=/videos/*.mkv)")
      + *             .setInterface(VideoPlayer.class, null)
      + *             .setImplementation(VideoPlayerImpl.class);
      + *         dm.add(resourceComponent);
      + *     }
      + * }
      + * 
      + * public interface VideoPlayer {
      + *     void play();
      + * }
      + * 
      + * public class VideoPlayerImpl implements VideoPlayer {
      + *     volatile URL resource; // injected 
      + *     
      + *     void play() {
      + *         ...
      + *     }
      + * }
      + * } 
      + * + *

      When you use callbacks to get injected with the resource, the "add", "change" callbacks + * support the following method signatures: + * + *

      + *

      {@code
      + * (Component, URL, Dictionary)
      + * (Component, URL)
      + * (Component) 
      + * (URL, Dictionary)
      + * (URL)
      + * (Object)
      + * }
      + * + * @see DependencyManager#createBundleComponent() + */ +public interface ResourceComponent extends Component { + + /** + * Sets the component scope. + * @param scope the component scope (default=SINGLETON) + * + * @return this component + */ + ResourceComponent setScope(ServiceScope scope); + + /** + * Sets the implementation for this component. You can actually specify + * an instance you have instantiated manually, or a Class + * that will be instantiated using its default constructor when the + * required dependencies are resolved, effectively giving you a lazy + * instantiation mechanism. + * + * There are four special methods that are called when found through + * reflection to give you life cycle management options: + *
        + *
      1. init() is invoked after the instance has been + * created and dependencies have been resolved, and can be used to + * initialize the internal state of the instance or even to add more + * dependencies based on runtime state
      2. + *
      3. start() is invoked right before the service is + * registered
      4. + *
      5. stop() is invoked right after the service is + * unregistered
      6. + *
      7. destroy() is invoked after all dependencies are + * removed
      8. + *
      + * In short, this allows you to initialize your instance before it is + * registered, perform some post-initialization and pre-destruction code + * as well as final cleanup. If a method is not defined, it simply is not + * called, so you can decide which one(s) you need. If you need even more + * fine-grained control, you can register as a service state listener too. + * + * @param implementation the implementation + * @return this component + * @see ComponentStateListener + */ + ResourceComponent setImplementation(Object implementation); + + /** + * Adds dependency(ies) to this component, atomically. If the component is already active or if you add + * dependencies from the init method, then you should add all the dependencies in one single add method call + * (using the varargs argument), because this method may trigger component activation (like + * the ServiceTracker.open() method does). + * + * @param dependencies the dependencies to add. + * @return this component + */ + ResourceComponent add(Dependency ... dependencies); + + /** + * Removes a dependency from the component. + * @param d the dependency to remove + * @return this component + */ + ResourceComponent remove(Dependency d); + + /** + * Adds a component state listener to this component. + * + * @param listener the state listener + */ + ResourceComponent add(ComponentStateListener listener); + + /** + * Removes a component state listener from this component. + * + * @param listener the state listener + */ + ResourceComponent remove(ComponentStateListener listener); + + /** + * Sets the public interface under which this component should be registered + * in the OSGi service registry. + * + * @param serviceName the name of the service interface + * @param properties the properties for this service + * @return this component + */ + ResourceComponent setInterface(String serviceName, Dictionary properties); + + /** + * Sets the public interfaces under which this component should be registered + * in the OSGi service registry. + * + * @param serviceNames the names of the service interface + * @param properties the properties for these services + * @return this component + */ + ResourceComponent setInterface(String[] serviceNames, Dictionary properties); + + /** + * Sets the public interface under which this component should be registered + * in the OSGi service registry. + * + * @param serviceName the name of the service interface + * @param properties the properties for this service + * @return this component + */ + ResourceComponent setInterface(Class serviceName, Dictionary properties); + + /** + * Sets the public interfaces under which this component should be registered + * in the OSGi service registry. + * + * @param serviceNames the names of the service interface + * @param properties the properties for these services + * @return this component + */ + ResourceComponent setInterface(Class[] serviceNames, Dictionary properties); + + /** + * Configures auto configuration of injected classes in the component instance. + * The following injections are currently performed, unless you explicitly + * turn them off: + *
      + *
      BundleContext
      the bundle context of the bundle
      + *
      ServiceRegistration
      the service registration used to register your service
      + *
      DependencyManager
      the dependency manager instance
      + *
      Component
      the component instance of the dependency manager
      + *
      + * + * @param clazz the class (from the list above) + * @param autoConfig false to turn off auto configuration + */ + ResourceComponent setAutoConfig(Class clazz, boolean autoConfig); + + /** + * Configures auto configuration of injected classes in the component instance. + * + * @param clazz the class (from the list above) + * @param instanceName the name of the instance to inject the class into + * @see #setAutoConfig(Class, boolean) + */ + ResourceComponent setAutoConfig(Class clazz, String instanceName); + + /** + * Sets the service properties associated with the component. If the service + * was already registered, it will be updated. + * + * @param serviceProperties the properties + */ + ResourceComponent setServiceProperties(Dictionary serviceProperties); + + /** + * Sets the names of the methods used as callbacks. These methods, when found, are + * invoked as part of the life cycle management of the component implementation. The + * dependency manager will look for a method of this name with the following signatures, + * in this order: + *
        + *
      1. method(Component component)
      2. + *
      3. method()
      4. + *
      + * + * @param init the name of the init method + * @param start the name of the start method + * @param stop the name of the stop method + * @param destroy the name of the destroy method + * @return the component + */ + ResourceComponent setCallbacks(String init, String start, String stop, String destroy); + + /** + * Sets the names of the methods used as callbacks. These methods, when found, are + * invoked on the specified instance as part of the life cycle management of the component + * implementation. + *

      + * See setCallbacks(String init, String start, String stop, String destroy) for more + * information on the signatures. Specifying an instance means you can create a manager + * that will be invoked whenever the life cycle of a component changes and this manager + * can then decide how to expose this life cycle to the actual component, offering an + * important indirection when developing your own component models. + * + * @return this component + */ + ResourceComponent setCallbacks(Object instance, String init, String start, String stop, String destroy); + + /** + * Sets the factory to use to create the implementation. You can specify + * both the factory class and method to invoke. The method should return + * the implementation, and can use any method to create it. Actually, this + * can be used together with setComposition to create a + * composition of instances that work together to implement a component. The + * factory itself can also be instantiated lazily by not specifying an + * instance, but a Class. + * + * @param factory the factory instance or class + * @param createMethod the name of the create method + * @return this component + */ + ResourceComponent setFactory(Object factory, String createMethod); + + /** + * Sets the factory to use to create the implementation. You specify the + * method to invoke. The method should return the implementation, and can + * use any method to create it. Actually, this can be used together with + * setComposition to create a composition of instances that + * work together to implement a component. + *

      + * Note that currently, there is no default for the factory, so please use + * setFactory(factory, createMethod) instead. + * + * @param createMethod the name of the create method + * @return this component + */ + ResourceComponent setFactory(String createMethod); + + /** + * Sets the instance and method to invoke to get back all instances that + * are part of a composition and need dependencies injected. All of them + * will be searched for any of the dependencies. The method that is + * invoked must return an Object[]. + * + * @param instance the instance that has the method + * @param getMethod the method to invoke + * @return this component + */ + ResourceComponent setComposition(Object instance, String getMethod); + + /** + * Sets the method to invoke on the service implementation to get back all + * instances that are part of a composition and need dependencies injected. + * All of them will be searched for any of the dependencies. The method that + * is invoked must return an Object[]. + * + * @param getMethod the method to invoke + * @return this component + */ + ResourceComponent setComposition(String getMethod); + + /** + * Activate debug for this component. Informations related to dependency processing will be displayed + * using osgi log service, our to standard output if no log service is currently available. + * @param label + */ + ResourceComponent setDebug(String label); + + /** + * Sets the resource filter used to match a given resource URL. + * + * @param filter the filter condition to use with the resource + * @return this ResourceComponent + */ + ResourceComponent setResourceFilter(String filter); + + /** + * Sets if properties from the resource should be propagated to the resource adapter service. true by default. + * + * @param propagate true if if properties from the resource should be propagated to the resource adapter service. + * true by default. + * The provided resource adapter service properties take precedence over the propagated resource service properties. + * @return this ResourceComponent + */ + ResourceComponent setPropagate(boolean propagate); + + /** + * Sets the propagate callback to invoke in order to propagate the resource properties to the adapter service. + * The provided resource adapter service properties take precedence over the propagated resource service properties. + * + * @param propagateCbInstance the object to invoke the propagate callback method on + * @param propagateCbMethod the method name to invoke in order to propagate the resource properties to the adapter service. + * @return this ResourceComponent + */ + ResourceComponent setPropagate(Object propagateCbInstance, String propagateCbMethod); + + /** + * Sets the callbacks to invoke when injecting the resource into the adapter component. + * + * @param add the method to invoke when injected the resource into the adapter component + * @param change the method to invoke when the resource properties have changed + * @return this ResourceComponent + */ + ResourceComponent setBundleCallbacks(String add, String change); + + /** + * Sets the instance to invoke the callbacks on (null by default, meaning the callbacks have to be invoked on the resource adapter itself) + * + * @param callbackInstance the instance to invoke the callbacks on (null by default, meaning the callbacks have to be invoked on the resource adapter itself) + * @return this ResourceComponent + */ + ResourceComponent setBundleCallbackInstance(Object callbackInstance); + + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ResourceDependency.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ResourceDependency.java new file mode 100644 index 00000000000..0eeab690eaf --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ResourceDependency.java @@ -0,0 +1,153 @@ +/* + * 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.dm; + +import java.net.URL; + +/** + * A resource dependency is a dependency on a resource. A resource in this context is an object that is + * identified by a URL. Resources should somehow be provided by an external component, the resource + * provider. These dependencies then react on them becoming available or not. Use cases for such dependencies + * are resources that are embedded in bundles, in a workspace or some remote or local repository, etc. + * + * When a resource is injected using callbacks, the following method signatures are supported: + *

      + *

      {@code
      + * (Component, URL, Dictionary)
      + * (Component, URL)
      + * (Component) 
      + * (URL, Dictionary)
      + * (URL)
      + * (Object)
      + * }
      + * + * @author Felix Project Team + */ +public interface ResourceDependency extends Dependency, ComponentDependencyDeclaration, ResourceHandler { + /** + * Sets the callbacks for this service. These callbacks can be used as hooks whenever a + * dependency is added or removed. When you specify callbacks, the auto configuration + * feature is automatically turned off, because we're assuming you don't need it in this + * case. + * + * @param added the method to call when a service was added + * @param removed the method to call when a service was removed + * @return this service dependency + */ + public ResourceDependency setCallbacks(String added, String removed); + + /** + * Sets the callbacks for this service. These callbacks can be used as hooks whenever a + * dependency is added, changed or removed. When you specify callbacks, the auto + * configuration feature is automatically turned off, because we're assuming you don't + * need it in this case. + * + * @param added the method to call when a service was added + * @param changed the method to call when a service was changed + * @param removed the method to call when a service was removed + * @return this service dependency + */ + public ResourceDependency setCallbacks(String added, String changed, String removed); + + /** + * Sets the callbacks for this service. These callbacks can be used as hooks whenever a + * dependency is added or removed. They are called on the instance you provide. When you + * specify callbacks, the auto configuration feature is automatically turned off, because + * we're assuming you don't need it in this case. + * + * @param instance the instance to call the callbacks on + * @param added the method to call when a service was added + * @param removed the method to call when a service was removed + * @return this service dependency + */ + public ResourceDependency setCallbacks(Object instance, String added, String removed); + + /** + * Sets the callbacks for this service. These callbacks can be used as hooks whenever a + * dependency is added, changed or removed. They are called on the instance you provide. When you + * specify callbacks, the auto configuration feature is automatically turned off, because + * we're assuming you don't need it in this case. + * + * @param instance the instance to call the callbacks on + * @param added the method to call when a service was added + * @param changed the method to call when a service was changed + * @param removed the method to call when a service was removed + * @return this service dependency + */ + public ResourceDependency setCallbacks(Object instance, String added, String changed, String removed); + + /** + * Sets auto configuration for this service. Auto configuration allows the + * dependency to fill in any attributes in the service implementation that + * are of the same type as this dependency. Default is on. + * + * @param autoConfig the value of auto config + * @return this service dependency + */ + public ResourceDependency setAutoConfig(boolean autoConfig); + + /** + * Sets auto configuration for this service. Auto configuration allows the + * dependency to fill in the attribute in the service implementation that + * has the same type and instance name. + * + * @param instanceName the name of attribute to auto config + * @return this service dependency + */ + public ResourceDependency setAutoConfig(String instanceName); + + /** + * Sets the resource for this dependency. + * + * @param resource the URL of the resource + */ + public ResourceDependency setResource(URL resource); + + /** + * Determines if this is a required dependency or not. + * + * @param required true if the dependency is required + */ + public ResourceDependency setRequired(boolean required); + + /** + * Sets the filter condition for this resource dependency. + * + * @param resourceFilter the filter condition + */ + public ResourceDependency setFilter(String resourceFilter); + + /** @see ResourceDependency#setPropagate(Object, String) */ + public ResourceDependency setPropagate(boolean propagate); + + /** + * Sets an Object instance and a callback method used to propagate some properties to the provided service properties. + * The method will be invoked on the specified object instance and must have one of the following signatures:

      + *

      • Dictionary callback(ServiceReference, Object service) + *
      • Dictionary callback(ServiceReference) + *
      + * The provided service properties take precedence over the propagated service dependency properties. It means a resource dependency + * property won't override a component service property having the same name. + * + * @param instance the Object instance which is used to retrieve propagated service properties + * @param method the method to invoke for retrieving the properties to be propagated to the service properties. + * @return this service dependency. + */ + public ResourceDependency setPropagate(Object instance, String method); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ResourceHandler.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ResourceHandler.java new file mode 100644 index 00000000000..55345800be5 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ResourceHandler.java @@ -0,0 +1,76 @@ +/* + * 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.dm; + +import java.net.URL; +import java.util.Dictionary; + +/** + * Service interface for anybody wanting to be notified of changes to resources. + * + * @author Felix Project Team + */ +public interface ResourceHandler { + /** Name of the property that's used to describe the filter condition for a resource. */ + public static final String FILTER = "filter"; + /** Exact URL that this handler is looking for. Can be used instead of a filter to be very explicit about the resource you're looking for. */ + public static final String URL = "url"; + /** The host part of the URL. */ + public static final String HOST = "host"; + /** The path part of the URL. */ + public static final String PATH = "path"; + /** The protocol part of the URL. */ + public static final String PROTOCOL = "protocol"; + /** The port part of the URL. */ + public static final String PORT = "port"; + + /** + * @deprecated Please use {@link #added(URL, Dictionary)} instead. When both are specified, + * the new method takes precedence and the deprecated one is not invoked. + */ + public void added(URL resource); + + /** + * Invoked whenever a new resource is added. + */ + public void added(URL resource, Dictionary resourceProperties); + + /** + * @deprecated Please use {@link #changed(URL, Dictionary)} instead. When both are specified, + * the new method takes precedence and the deprecated one is not invoked. + */ + public void changed(URL resource); + + /** + * Invoked whenever an existing resource changes. + */ + public void changed(URL resource, Dictionary resourceProperties); + + /** + * @deprecated Please use {@link #removed(URL, Dictionary)} instead. When both are specified, + * the new method takes precedence and the deprecated one is not invoked. + */ + public void removed(URL resource); + + /** + * Invoked whenever an existing resource is removed. + */ + public void removed(URL resource, Dictionary resourceProperties); +} + diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ResourceUtil.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ResourceUtil.java new file mode 100644 index 00000000000..2112ffea5e4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ResourceUtil.java @@ -0,0 +1,45 @@ +/* + * 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.dm; + +import java.net.URL; +import java.util.Dictionary; +import java.util.Hashtable; + +/** + * Utility class for resource handling. + * + * @author Felix Project Team + */ +public class ResourceUtil { + /** + * Creates a set of properties for a resource based on its URL. + * + * @param url the URL + * @return a set of properties + */ + public static Dictionary createProperties(URL url) { + Hashtable props = new Hashtable<>(); + props.put(ResourceHandler.PROTOCOL, url.getProtocol()); + props.put(ResourceHandler.HOST, url.getHost()); + props.put(ResourceHandler.PORT, Integer.toString(url.getPort())); + props.put(ResourceHandler.PATH, url.getPath()); + return props; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ServiceDependency.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ServiceDependency.java new file mode 100644 index 00000000000..334447cba11 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ServiceDependency.java @@ -0,0 +1,345 @@ +/* + * 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.dm; + +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.ServiceReference; + +/** + * Service dependency that can track an OSGi service. + * + * When defining dependency method callbacks; for "add", "change", "remove" callbacks, the following method signatures are supported: + * + *
      {@code
      + * (Component comp, ServiceReference ref, Service service)
      + * (Component comp, ServiceReference ref, Object service)
      + * (Component comp, ServiceReference ref)
      + * (Component comp, Service service)
      + * (Component comp, Object service)
      + * (Component comp)
      + * (Component comp, Map properties, Service service)
      + * (ServiceReference ref, Service service)
      + * (ServiceReference ref, Object service)
      + * (ServiceReference ref)
      + * (Service service)
      + * (Service service, Map propeerties)
      + * (Map properties, Service, service)
      + * (Service service, Dictionary properties)
      + * (Dictionary properties, Service service)
      + * (Object service)
      + * (ServiceObjects service)
      + * }
      + * + *

      For "swap" callbacks, the following method signatures are supported: + * + *

      {@code
      + * (Service old, Service replace)
      + * (Object old, Object replace)
      + * (ServiceReference old, Service old, ServiceReference replace, Service replace)
      + * (ServiceReference old, Object old, ServiceReference replace, Object replace)
      + * (Component comp, Service old, Service replace)
      + * (Component comp, Object old, Object replace)
      + * (Component comp, ServiceReference old, Service old, ServiceReference replace, Service replace)
      + * (Component comp, ServiceReference old, Object old, ServiceReference replace, Object replace)
      + * (ServiceReference old, ServiceReference replace)
      + * (Component comp, ServiceReference old, ServiceReference replace)
      + * (ServiceObjects old, ServiceObjects replace)
      + * (Component comp, ServiceObjects old, ServiceObjects replace)
      + * }
      + * + * @author Felix Project Team + */ +@ProviderType +public interface ServiceDependency extends Dependency, ComponentDependencyDeclaration { + /** + * Sets the callbacks for this service. These callbacks can be used as hooks whenever a + * dependency is added or removed. When you specify callbacks, the auto configuration + * feature is automatically turned off, because we're assuming you don't need it in this + * case. + * + * @param add the method to call when a service was added + * @param remove the method to call when a service was removed + * @return this service dependency + */ + public ServiceDependency setCallbacks(String add, String remove); + + /** + * Sets the callbacks for this service. These callbacks can be used as hooks whenever a + * dependency is added, changed or removed. When you specify callbacks, the auto + * configuration feature is automatically turned off, because we're assuming you don't + * need it in this case. + * + * @param add the method to call when a service was added + * @param change the method to call when a service was changed + * @param remove the method to call when a service was removed + * @return this service dependency + */ + public ServiceDependency setCallbacks(String add, String change, String remove); + + /** + * Sets the callbacks for this service. These callbacks can be used as hooks whenever a + * dependency is added, changed or removed. When you specify callbacks, the auto + * configuration feature is automatically turned off, because we're assuming you don't + * need it in this case. + * @param add the method to call when a service was added + * @param change the method to call when a service was changed + * @param remove the method to call when a service was removed + * @param swap the method to call when the service was swapped due to addition or + * removal of an aspect + * @return this service dependency + */ + public ServiceDependency setCallbacks(String add, String change, String remove, String swap); + + /** + * Sets the callbacks for this service. These callbacks can be used as hooks whenever a + * dependency is added or removed. They are called on the instance you provide. When you + * specify callbacks, the auto configuration feature is automatically turned off, because + * we're assuming you don't need it in this case. + * + * @param instance the instance to call the callbacks on + * @param add the method to call when a service was added + * @param remove the method to call when a service was removed + * @return this service dependency + */ + public ServiceDependency setCallbacks(Object instance, String add, String remove); + + /** + * Sets the callbacks for this service. These callbacks can be used as hooks whenever a + * dependency is added, changed or removed. They are called on the instance you provide. When you + * specify callbacks, the auto configuration feature is automatically turned off, because + * we're assuming you don't need it in this case. + * + * @param instance the instance to call the callbacks on + * @param add the method to call when a service was added + * @param change the method to call when a service was changed + * @param remove the method to call when a service was removed + * @return this service dependency + */ + public ServiceDependency setCallbacks(Object instance, String add, String change, String remove); + + /** + * Sets the callbacks for this service. These callbacks can be used as hooks whenever a + * dependency is added, changed or removed. When you specify callbacks, the auto + * configuration feature is automatically turned off, because we're assuming you don't + * need it in this case. + * @param instance the instance to call the callbacks on + * @param added the method to call when a service was added + * @param changed the method to call when a service was changed + * @param removed the method to call when a service was removed + * @param swapped the method to call when the service was swapped due to addition or + * removal of an aspect + * @return this service dependency + */ + public ServiceDependency setCallbacks(Object instance, String added, String changed, String removed, String swapped); + + /** + * Sets the required flag which determines if this service is required or not. + * A ServiceDependency is false by default. + * + * @param required the required flag + * @return this service dependency + */ + public ServiceDependency setRequired(boolean required); + + /** + * Sets auto configuration for this service. Auto configuration allows the + * dependency to fill in the attribute in the service implementation that + * has the same type and instance name. Dependency services will be injected + * in the following kind of fields:

      + *

        + *
      • a field having the same type as the dependency. If the field may be accessed by anythread, then + * the field should be declared volatile, in order to ensure visibility when the field is auto injected concurrently. + * + *
      • a field which is assignable to an Iterable<T> where T must match the dependency type. + * In this case, an Iterable will be injected by DependencyManager before the start callback is called. + * The Iterable field may then be traversed to inspect the currently available dependency services. The Iterable + * can possibly be set to a final value so you can choose the Iterable implementation of your choice + * (for example, a CopyOnWrite ArrayList, or a ConcurrentLinkedQueue). + * + *
      • a Map<K,V> where K must match the dependency type and V must exactly be equal to Dictionary. + * In this case, a ConcurrentHashMap will be injected by DependencyManager before the start callback is called. + * The Map may then be consulted to lookup current available dependency services, including the dependency service + * properties (the map key holds the dependency service, and the map value holds the dependency service properties). + * + * The Map field may be set to a final value so you can choose a Map of your choice (Typically a ConcurrentHashMap). + * + * A ConcurrentHashMap is "weakly consistent", meaning that when traversing + * the elements, you may or may not see any concurrent updates made on the map. So, take care to traverse + * the map using an iterator on the map entry set, which allows to atomically lookup pairs of Dependency service/Service properties. + *
      + * + *

      Here are some example using an Iterable: + *

      + * + *
      +     * 
      +     * public class SpellChecker {
      +     *    // can be traversed to inspect currently available dependencies
      +     *    final Iterable<DictionaryService> dictionaries = new ConcurrentLinkedQueue<>();
      +     *    
      +     *    Or
      +     *    
      +     *    // will be injected by DM automatically and can be traversed any time to inspect all currently available dependencies.
      +     *    volatile Iterable<DictionaryService> dictionaries = null;
      +     * }
      +     * 
      +     * 
      + *
      + * + * Here are some example using a Map: + *
      + * + *
      +     * 
      +     * public class SpellChecker {
      +     *    // can be traversed to inspect currently available dependencies
      +     *    final Map<DictionaryService, Dictionary> dictionaries = new ConcurrentLinkedQueue<>();
      +     *    
      +     *    or
      +     *    
      +     *    // will be injected by DM automatically and can be traversed to inspect currently available dependencies
      +     *    volatile Map<DictionaryService, Dictionary> dictionaries = null;
      +     *
      +     *    void iterateOnAvailableServices() {                 
      +     *       for (Map.Entry entry : this.services.entrySet()) {
      +     *           MyService currentService = entry.getKey();
      +     *           Dictionary currentServiceProperties = entry.getValue();
      +     *           // ...
      +     *       }
      +     *    }
      +     * }
      +     * 
      +     * 
      + *
      + * + * @param autoConfig the name of attribute to auto configure + * @return this service dependency + */ + public ServiceDependency setAutoConfig(boolean autoConfig); + + /** + * Sets auto configuration for this service. Auto configuration allows the + * dependency to fill in the attribute in the service implementation that + * has the same type and instance name. + * + * @param instanceName the name of attribute to auto config + * @return this service dependency + * @see #setAutoConfig(boolean) + */ + public ServiceDependency setAutoConfig(String instanceName); + + /** + * Sets the name of the service that should be tracked. + * + * @param serviceName the name of the service + * @return this service dependency + */ + public ServiceDependency setService(Class serviceName); + + /** + * Sets the name of the service that should be tracked. You can either specify + * only the name, or the name and a filter. In the latter case, the filter is used + * to track the service and should only return services of the type that was specified + * in the name. To make sure of this, the filter is actually extended internally to + * filter on the correct name. + * + * @param serviceName the name of the service + * @param serviceFilter the filter condition + * @return this service dependency + */ + public ServiceDependency setService(Class serviceName, String serviceFilter); + + /** + * Sets the filter for the services that should be tracked. Any service object + * matching the filter will be returned, without any additional filter on the + * class. + * + * @param serviceFilter the filter condition + * @return this service dependency + */ + public ServiceDependency setService(String serviceFilter); + + /** + * Sets the name of the service that should be tracked. You can either specify + * only the name, or the name and a reference. In the latter case, the service reference + * is used to track the service and should only return services of the type that was + * specified in the name. + * + * @param serviceName the name of the service + * @param serviceReference the service reference to track + * @return this service dependency + */ + @SuppressWarnings("rawtypes") + public ServiceDependency setService(Class serviceName, ServiceReference serviceReference); + + /** + * Sets the default implementation for an optional service dependency. You can use this to supply + * your own implementation that will be used instead of a Null Object when the dependency is + * not available. This is also convenient if the service dependency is not an interface + * (which would cause the Null Object creation to fail) but a class. + * Only use this attribute on an optional service dependency injected on a class field. + * + * @param implementation the instance to use or the class to instantiate if you want to lazily + * instantiate this implementation + * @return this service dependency + */ + public ServiceDependency setDefaultImplementation(Object implementation); + + /** + * Sets propagation of the service dependency properties to the provided service properties. Any additional + * service properties specified directly are merged with these. The provided service properties take precedence over the + * propagated service dependency properties. It means a service dependency property won't override a component service + * property having the same name. + * + * @param propagate true if the dependency service properties should be propagated to the component service properties. + */ + public ServiceDependency setPropagate(boolean propagate); + + /** + * Sets an Object instance and a callback method used to propagate some properties to the provided service properties. + * The method will be invoked on the specified object instance and must have one of the following signatures:

      + *

      • Dictionary callback(ServiceReference, Object service) + *
      • Dictionary callback(ServiceReference) + *
      + * @param instance the Object instance which is used to retrieve propagated service properties + * @param method the method to invoke for retrieving the properties to be propagated to the service properties. + * @return this service dependency. + */ + public ServiceDependency setPropagate(Object instance, String method); + + /** + * Enabled debug logging for this dependency instance. The logging is prefixed with the given identifier. + * @param debugKey a prefix log identifier + * @return this service dependency. + */ + public ServiceDependency setDebug(String debugKey); + + /** + * Configures whether or not this dependency should internally obtain the service object for all tracked service references. + * + * By default, DM internally dereferences all discovered service references (using + * BundleContext.getService(ServiceReference ref) methods. + * However, sometimes, your callback only needs the ServiceReference, and sometimes you don't want to dereference the service. + * So, in this case you can use the setDereference(false) method in order to tell to DM + * that it should never internally dereference the service dependency internally. + * + * @return false if the service must never be dereferenced by dependency manager (internally). + */ + public ServiceDependency setDereference(boolean dereferenceServiceInternally); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/compat/DependencyActivatorBaseCompat.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/compat/DependencyActivatorBaseCompat.java new file mode 100644 index 00000000000..eca0b1032c7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/compat/DependencyActivatorBaseCompat.java @@ -0,0 +1,269 @@ +/* + * 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.dm.compat; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.PropertyMetaData; + +/** + * This class contains some methods which have been deprecated from the DependencyActivatorBase class. + */ +public abstract class DependencyActivatorBaseCompat { + private DependencyManager m_manager; + + protected void setDependencyManager(DependencyManager manager) { + m_manager = manager; + } + + /** + * Creates a new aspect service. + * + * @return the aspect service + * @see DependencyManager#createAspectService(Class, String, int, String) + * @deprecated use {@link DependencyActivatorBase#createAspectComponent()} + */ + public Component createAspectService(Class serviceInterface, String serviceFilter, int ranking, String attributeName) { + return m_manager.createAspectService(serviceInterface, serviceFilter, ranking, attributeName); + } + + /** + * Creates a new aspect service. + * + * @return the aspect service + * @see DependencyManager#createAspectService(Class, String, int) + * @deprecated use {@link DependencyActivatorBase#createAspectComponent()} + */ + public Component createAspectService(Class serviceInterface, String serviceFilter, int ranking) { + return m_manager.createAspectService(serviceInterface, serviceFilter, ranking); + } + + /** + * Creates a new aspect service. + * + * @return the aspect service + * @see DependencyManager#createAspectService(Class, String, int, String, String, String) + * @deprecated use {@link DependencyActivatorBase#createAspectComponent()} + */ + public Component createAspectService(Class serviceInterface, String serviceFilter, int ranking, String add, String change, String remove) { + return m_manager.createAspectService(serviceInterface, serviceFilter, ranking, add, change, remove); + } + + /** + * Creates a new aspect service. + * + * @return the aspect service + * @see DependencyManager#createAspectService(Class, String, int, String, String, String, String) + * @deprecated use {@link DependencyActivatorBase#createAspectComponent()} + */ + public Component createAspectService(Class serviceInterface, String serviceFilter, int ranking, String add, String change, String remove, String swap) { + return m_manager.createAspectService(serviceInterface, serviceFilter, ranking, add, change, remove, swap); + } + + /** + * Creates a new aspect service. + * + * @return the aspect service + * @see DependencyManager#createAspectService(Class, String, int, Object, String, String, String, String) + * @deprecated use {@link DependencyActivatorBase#createAspectComponent()} + */ + public Component createAspectService(Class serviceInterface, String serviceFilter, int ranking, Object callbackInstance, String add, String change, String remove, String swap) { + return m_manager.createAspectService(serviceInterface, serviceFilter, ranking, callbackInstance, add, change, remove, swap); + } + + /** + * Creates a new adapter service. + * + * @return the adapter service + * @see DependencyManager#createAdapterService(Class, String) + * @deprecated use {@link DependencyActivatorBase#createAdapterComponent()} + */ + public Component createAdapterService(Class serviceInterface, String serviceFilter) { + return m_manager.createAdapterService(serviceInterface, serviceFilter); + } + + /** + * Creates a new adapter service. + * + * @return the adapter service + * @see DependencyManager#createAdapterService(Class, String, String) + * @deprecated use {@link DependencyActivatorBase#createAdapterComponent()} + */ + public Component createAdapterService(Class serviceInterface, String serviceFilter, String autoConfig) { + return m_manager.createAdapterService(serviceInterface, serviceFilter, autoConfig); + } + + /** + * Creates a new adapter service. + * + * @return the adapter service + * @see DependencyManager#createAdapterService(Class, String, String, String, String) + * @deprecated use {@link DependencyActivatorBase#createAdapterComponent()} + */ + public Component createAdapterService(Class serviceInterface, String serviceFilter, String add, String change, String remove) { + return m_manager.createAdapterService(serviceInterface, serviceFilter, add, change, remove); + } + + /** + * Creates a new adapter service. + * @return the adapter service + * @see DependencyManager#createAdapterService(Class, String, String, String, String, String) + * @deprecated use {@link DependencyActivatorBase#createAdapterComponent()} + */ + public Component createAdapterService(Class serviceInterface, String serviceFilter, String add, String change, String remove, String swap) { + return m_manager.createAdapterService(serviceInterface, serviceFilter, add, change, remove, swap); + } + + /** + * Creates a new adapter service. + * @return the adapter service + * @see DependencyManager#createAdapterService(Class, String, String, Object, String, String, String, String, boolean) + * @deprecated use {@link DependencyActivatorBase#createAdapterComponent()} + */ + public Component createAdapterService(Class serviceInterface, String serviceFilter, + String autoConfig, Object callbackInstance, String add, String change, String remove, String swap) { + return m_manager.createAdapterService(serviceInterface, serviceFilter, autoConfig, callbackInstance, add, change, remove, swap, true); + } + + /** + * Creates a new adapter service. + * @return the adapter service + * @see DependencyManager#createAdapterService(Class, String, String, Object, String, String, String, String, boolean) + * @deprecated use {@link DependencyActivatorBase#createAdapterComponent()} + */ + public Component createAdapterService(Class serviceInterface, String serviceFilter, + String autoConfig, Object callbackInstance, String add, String change, String remove, String swap, boolean propagate) { + return m_manager.createAdapterService(serviceInterface, serviceFilter, autoConfig, callbackInstance, add, change, remove, swap, propagate); + } + + /** + * Creates a new factory configuration adapter service. + * + * @return the factory configuration adapter service + * @deprecated use {@link DependencyActivatorBase#createFactoryComponent()} + */ + public Component createFactoryConfigurationAdapterService(String factoryPid, String update, boolean propagate) { + return m_manager.createFactoryConfigurationAdapterService(factoryPid, update, propagate); + } + + /** + * Creates a new factory configuration adapter service, using a specific callback instance + * + * @return the factory configuration adapter service + * @deprecated use {@link DependencyActivatorBase#createFactoryComponent()} + */ + public Component createFactoryConfigurationAdapterService(String factoryPid, String update, boolean propagate, Object callbackInstance) { + return m_manager.createFactoryConfigurationAdapterService(factoryPid, update, propagate, callbackInstance); + } + + /** + * Creates a new factory configuration adapter service, using a specific callback instance + * + * @return the factory configuration adapter service + * @see DependencyManager#createFactoryConfigurationAdapterService(String, String, boolean, Class) + * @deprecated use {@link DependencyActivatorBase#createFactoryComponent()} + */ + public Component createFactoryConfigurationAdapterService(String factoryPid, String update, boolean propagate, Class configType) { + return m_manager.createFactoryConfigurationAdapterService(factoryPid, update, propagate, configType); + } + + /** + * Creates a new factory configuration adapter service, using a specific callback instance + * + * @return the factory configuration adapter service + * @see DependencyManager#createFactoryConfigurationAdapterService(String, String, boolean, Object, Class) + * @deprecated use {@link DependencyActivatorBase#createFactoryComponent()} + */ + public Component createFactoryConfigurationAdapterService(String factoryPid, String update, boolean propagate, Object callbackInstance, Class configType) { + return m_manager.createFactoryConfigurationAdapterService(factoryPid, update, propagate, callbackInstance, configType); + } + + /** + * Creates a new factory configuration adapter service. + * + * @return the factory configuration adapter service + * @deprecated use {@link DependencyActivatorBase#createFactoryComponent()} + */ + public Component createFactoryConfigurationAdapterService(String factoryPid, String update, boolean propagate, String heading, String desc, String localization, PropertyMetaData[] propertiesMetaData) { + return m_manager.createAdapterFactoryConfigurationService(factoryPid, update, propagate, heading, desc, localization, propertiesMetaData); + } + + /** + * Creates a new bundle adapter service. + * + * @return the bundle adapter service + * @deprecated use {@link DependencyActivatorBase#createBundleComponent()} + */ + public Component createBundleAdapterService(int bundleStateMask, String bundleFilter, boolean propagate) { + return m_manager.createBundleAdapterService(bundleStateMask, bundleFilter, propagate); + } + + /** + * Creates a new bundle adapter service, using a specific callback instance + * + * @return the bundle adapter service + * @deprecated use {@link DependencyActivatorBase#createBundleComponent()} + */ + public Component createBundleAdapterService(int bundleStateMask, String bundleFilter, boolean propagate, + Object callbackInstance, String add, String change, String remove) { + return m_manager.createBundleAdapterService(bundleStateMask, bundleFilter, propagate, callbackInstance, add, change, remove); + } + + /** + * Creates a new resource adapter service. + * + * @return the resource adapter service + * @deprecated use {@link DependencyActivatorBase#createResourceComponent()} + */ + public Component createResourceAdapter(String resourceFilter, boolean propagate, Object callbackInstance, String callbackChanged) { + return m_manager.createResourceAdapterService(resourceFilter, propagate, callbackInstance, callbackChanged); + } + + /** + * Creates a new resource adapter service. + * + * @return the resource adapter service + * @deprecated use {@link DependencyActivatorBase#createResourceComponent()} + */ + public Component createResourceAdapter(String resourceFilter, boolean propagate, Object callbackInstance, String callbackSet, String callbackChanged) { + return m_manager.createResourceAdapterService(resourceFilter, propagate, callbackInstance, callbackSet, callbackChanged); + } + + /** + * Creates a new resource adapter service. + * + * @return the resource adapter service + * @deprecated use {@link DependencyActivatorBase#createResourceComponent()} + */ + public Component createResourceAdapter(String resourceFilter, Object propagateCallbackInstance, String propagateCallbackMethod, Object callbackInstance, String callbackChanged) { + return m_manager.createResourceAdapterService(resourceFilter, propagateCallbackInstance, propagateCallbackMethod, callbackInstance, null, callbackChanged); + } + + /** + * Creates a new resource adapter service. + * + * @return the resource adapter service + * @deprecated use {@link DependencyActivatorBase#createResourceComponent()} + */ + public Component createResourceAdapter(String resourceFilter, Object propagateCallbackInstance, String propagateCallbackMethod, Object callbackInstance, String callbackSet, String callbackChanged) { + return m_manager.createResourceAdapterService(resourceFilter, propagateCallbackInstance, propagateCallbackMethod, callbackInstance, callbackSet, callbackChanged); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/compat/DependencyManagerCompat.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/compat/DependencyManagerCompat.java new file mode 100644 index 00000000000..9c8a9a218b0 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/compat/DependencyManagerCompat.java @@ -0,0 +1,724 @@ +/* + * 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.dm.compat; + +import org.apache.felix.dm.AdapterComponent; +import org.apache.felix.dm.AspectComponent; +import org.apache.felix.dm.BundleComponent; +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ConfigurationDependency; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.FactoryComponent; +import org.apache.felix.dm.PropertyMetaData; +import org.apache.felix.dm.ResourceComponent; + +/** + * This class contains some methods which have been deprecated from the DependencyManager class. + */ +public abstract class DependencyManagerCompat { + + /** + * Creates a new aspect component. The aspect will be applied to any service that + * matches the specified interface and filter. For each matching service + * an aspect will be created based on the aspect implementation class. + * The aspect will be registered with the same interface and properties + * as the original service, plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + * @return an aspect component + */ + public abstract AspectComponent createAspectComponent(); + + /** + * Creates a new adapter component. The adapter will be applied to any service that + * matches the specified interface and filter. For each matching service + * an adapter will be created based on the adapter implementation class. + * The adapter will be registered with the specified interface and existing properties + * from the original service plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + * @return an adapter component + */ + public abstract AdapterComponent createAdapterComponent(); + + /** + * Creates a new Factory Component. For each new factory configuration matching + * the factoryPid, a component will be created based on the component implementation class. + * The component will be registered with the specified interface, and with the specified service properties. + * Depending on the propagate parameter, every public factory configuration properties + * (which don't start with ".") will be propagated along with the adapter service properties. + * It will also inherit all dependencies. + * + * @return a factory pid component + */ + public abstract FactoryComponent createFactoryComponent(); + + /** + * Creates a new bundle adapter. The adapter will be applied to any bundle that + * matches the specified bundle state mask and filter condition. For each matching + * bundle an adapter will be created based on the adapter implementation class. + * The adapter will be registered with the specified interface + * + * TODO and existing properties from the original resource plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + * @return a service that acts as a factory for generating bundle adapters + */ + public abstract BundleComponent createBundleComponent(); + + /** + * Creates a new resource adapter component. The adapter will be applied to any resource that + * matches the specified filter condition. For each matching resource + * an adapter will be created based on the adapter implementation class. + * The adapter will be registered with the specified interface and existing properties + * from the original resource plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + * @return a Resource Adapter Component + */ + public abstract ResourceComponent createResourceComponent(); + + /** + * Creates a new aspect. The aspect will be applied to any service that + * matches the specified interface and filter. For each matching service + * an aspect will be created based on the aspect implementation class. + * The aspect will be registered with the same interface and properties + * as the original service, plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + *

      Usage example: + * + *

      +     * manager.createAspectService(ExistingService.class, "(foo=bar)", 10, "m_service")
      +     *     .setImplementation(ExistingServiceAspect.class)
      +     * );
      +     * 
      + * + * @param serviceInterface the service interface to apply the aspect to + * @param serviceFilter the filter condition to use with the service interface + * @param ranking the level used to organize the aspect chain ordering + * @param autoConfig the aspect implementation field name where to inject original service. + * If null, any field matching the original service will be injected. + * @return a service that acts as a factory for generating aspects + * @deprecated use {@link DependencyManager#createAspectComponent()} + */ + public Component createAspectService(Class serviceInterface, String serviceFilter, int ranking, String autoConfig) { + return createAspectComponent() + .setAspect(serviceInterface, serviceFilter, ranking) + .setAspectField(autoConfig); + } + + /** + * Creates a new aspect. The aspect will be applied to any service that + * matches the specified interface and filter. For each matching service + * an aspect will be created based on the aspect implementation class. + * The aspect will be registered with the same interface and properties + * as the original service, plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + *

      Usage example: + * + *

      +     * manager.createAspectService(ExistingService.class, "(foo=bar)", 10)
      +     *     .setImplementation(ExistingServiceAspect.class)
      +     * );
      +     * 
      + * + * @param serviceInterface the service interface to apply the aspect to + * @param serviceFilter the filter condition to use with the service interface + * @param ranking the level used to organize the aspect chain ordering + * @return a service that acts as a factory for generating aspects + * @deprecated use {@link DependencyManager#createAspectComponent()} + */ + public Component createAspectService(Class serviceInterface, String serviceFilter, int ranking) { + return createAspectComponent() + .setAspect(serviceInterface, serviceFilter, ranking); + } + + /** + * Creates a new aspect. The aspect will be applied to any service that + * matches the specified interface and filter. For each matching service + * an aspect will be created based on the aspect implementation class. + * The aspect will be registered with the same interface and properties + * as the original service, plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + *

      Usage example: + * + *

      +     * manager.createAspectService(ExistingService.class, "(foo=bar)", 10, "add", "change", "remove")
      +     *     .setImplementation(ExistingServiceAspect.class)
      +     * );
      +     * 
      + * + * @param serviceInterface the service interface to apply the aspect to + * @param serviceFilter the filter condition to use with the service interface + * @param ranking the level used to organize the aspect chain ordering + * @param add name of the callback method to invoke on add + * @param change name of the callback method to invoke on change + * @param remove name of the callback method to invoke on remove + * @return a service that acts as a factory for generating aspects + * @deprecated use {@link DependencyManager#createAspectComponent()} + */ + public Component createAspectService(Class serviceInterface, String serviceFilter, int ranking, String add, + String change, String remove) + { + return createAspectComponent() + .setAspect(serviceInterface, serviceFilter, ranking) + .setAspectCallbacks(add, change, remove, null); + } + + /** + * Creates a new aspect. The aspect will be applied to any service that + * matches the specified interface and filter. For each matching service + * an aspect will be created based on the aspect implementation class. + * The aspect will be registered with the same interface and properties + * as the original service, plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + *

      Usage example: + * + *

      +     * manager.createAspectService(ExistingService.class, "(foo=bar)", 10, "add", "change", "remove")
      +     *     .setImplementation(ExistingServiceAspect.class)
      +     * );
      +     * 
      + * + * @param serviceInterface the service interface to apply the aspect to + * @param serviceFilter the filter condition to use with the service interface + * @param ranking the level used to organize the aspect chain ordering + * @param add name of the callback method to invoke on add + * @param change name of the callback method to invoke on change + * @param remove name of the callback method to invoke on remove + * @param swap name of the callback method to invoke on swap + * @return a service that acts as a factory for generating aspects + * @deprecated use {@link DependencyManager#createAspectComponent()} + */ + public Component createAspectService(Class serviceInterface, String serviceFilter, int ranking, String add, String change, String remove, String swap) + { + return createAspectComponent() + .setAspect(serviceInterface, serviceFilter, ranking) + .setAspectCallbacks(add, change, remove, swap); + } + + /** + * Creates a new aspect. The aspect will be applied to any service that + * matches the specified interface and filter. For each matching service + * an aspect will be created based on the aspect implementation class. + * The aspect will be registered with the same interface and properties + * as the original service, plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + *

      Usage example: + * + *

      +     * manager.createAspectService(ExistingService.class, "(foo=bar)", 10, "add", "change", "remove")
      +     *     .setImplementation(ExistingServiceAspect.class)
      +     * );
      +     * 
      + * + * @param serviceInterface the service interface to apply the aspect to + * @param serviceFilter the filter condition to use with the service interface + * @param ranking the level used to organize the aspect chain ordering + * @param callbackInstance the instance to invoke the callbacks on, or null if the callbacks have to be invoked on the aspect itself + * @param add name of the callback method to invoke on add + * @param change name of the callback method to invoke on change + * @param remove name of the callback method to invoke on remove + * @param swap name of the callback method to invoke on swap + * @return a service that acts as a factory for generating aspects + * @deprecated use {@link DependencyManager#createAspectComponent()} + */ + public Component createAspectService(Class serviceInterface, String serviceFilter, int ranking, Object callbackInstance, + String add, String change, String remove, String swap) + { + return createAspectComponent() + .setAspect(serviceInterface, serviceFilter, ranking) + .setAspectCallbackInstance(callbackInstance) + .setAspectCallbacks(add, change, remove, swap); + } + + /** + * Creates a new adapter. The adapter will be applied to any service that + * matches the specified interface and filter. For each matching service + * an adapter will be created based on the adapter implementation class. + * The adapter will be registered with the specified interface and existing properties + * from the original service plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + *

      Usage example: + * + *

      +     * manager.createAdapterService(AdapteeService.class, "(foo=bar)")
      +     *     .setInterface(AdapterService.class, new Hashtable() {{ put("extra", "property"); }})
      +     *     .setImplementation(AdapterImpl.class);
      +     * 
      + * + * @param serviceInterface the service interface to apply the adapter to + * @param serviceFilter the filter condition to use with the service interface + * @return a service that acts as a factory for generating adapters + * @deprecated use {@link DependencyManager#createAdapterComponent()} + */ + public Component createAdapterService(Class serviceInterface, String serviceFilter) { + return createAdapterComponent().setAdaptee(serviceInterface, serviceFilter); + } + + /** + * Creates a new adapter. The adapter will be applied to any service that + * matches the specified interface and filter. For each matching service + * an adapter will be created based on the adapter implementation class. + * The adapter will be registered with the specified interface and existing properties + * from the original service plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + *

      Usage example: + * + *

      +     * manager.createAdapterService(AdapteeService.class, "(foo=bar)", "m_service")
      +     *     .setInterface(AdapterService.class, new Hashtable() {{ put("extra", "property"); }})
      +     *     .setImplementation(AdapterImpl.class);
      +     * 
      + * + * @param serviceInterface the service interface to apply the adapter to + * @param serviceFilter the filter condition to use with the service interface + * @param autoConfig the name of the member to inject the service into + * @return a service that acts as a factory for generating adapters + * @deprecated use {@link DependencyManager#createAdapterComponent()} + */ + public Component createAdapterService(Class serviceInterface, String serviceFilter, String autoConfig) { + return createAdapterComponent() + .setAdaptee(serviceInterface, serviceFilter) + .setAdapteeField(autoConfig); + } + + /** + * Creates a new adapter. The adapter will be applied to any service that + * matches the specified interface and filter. For each matching service + * an adapter will be created based on the adapter implementation class. + * The adapter will be registered with the specified interface and existing properties + * from the original service plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + *

      Usage example: + * + *

      +     * manager.createAdapterService(AdapteeService.class, "(foo=bar)", "add", "change", "remove")
      +     *     .setInterface(AdapterService.class, new Hashtable() {{ put("extra", "property"); }})
      +     *     .setImplementation(AdapterImpl.class);
      +     * 
      + * + * @param serviceInterface the service interface to apply the adapter to + * @param serviceFilter the filter condition to use with the service interface + * @param add name of the callback method to invoke on add + * @param change name of the callback method to invoke on change + * @param remove name of the callback method to invoke on remove + * @return a service that acts as a factory for generating adapters + * @deprecated use {@link DependencyManager#createAdapterComponent()} + */ + public Component createAdapterService(Class serviceInterface, String serviceFilter, String add, String change, String remove) { + return createAdapterComponent() + .setAdaptee(serviceInterface, serviceFilter) + .setAdapteeCallbacks(add, change, remove, null); + } + + /** + * Creates a new adapter. The adapter will be applied to any service that + * matches the specified interface and filter. For each matching service + * an adapter will be created based on the adapter implementation class. + * The adapter will be registered with the specified interface and existing properties + * from the original service plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + *

      Usage example: + * + *

      +     * manager.createAdapterService(AdapteeService.class, "(foo=bar)", "add", "change", "remove", "swap")
      +     *     .setInterface(AdapterService.class, new Hashtable() {{ put("extra", "property"); }})
      +     *     .setImplementation(AdapterImpl.class);
      +     * 
      + * + * @param serviceInterface the service interface to apply the adapter to + * @param serviceFilter the filter condition to use with the service interface + * @param add name of the callback method to invoke on add + * @param change name of the callback method to invoke on change + * @param remove name of the callback method to invoke on remove + * @param swap name of the callback method to invoke on swap + * @return a service that acts as a factory for generating adapters + * @deprecated use {@link DependencyManager#createAdapterComponent()} + */ + public Component createAdapterService(Class serviceInterface, String serviceFilter, String add, String change, String remove, String swap) { + return createAdapterComponent() + .setAdaptee(serviceInterface, serviceFilter) + .setAdapteeCallbacks(add, change, remove, swap); + } + + /** + * Creates a new adapter. The adapter will be applied to any service that + * matches the specified interface and filter. For each matching service + * an adapter will be created based on the adapter implementation class. + * The adapter will be registered with the specified interface (and existing properties + * from the original service if you set the propagate flag) plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + *

      Usage example: + * + *

      +     * manager.createAdapterService(AdapteeService.class, "(foo=bar)", "add", "change", "remove", "swap")
      +     *     .setInterface(AdapterService.class, new Hashtable() {{ put("extra", "property"); }})
      +     *     .setImplementation(AdapterImpl.class);
      +     * 
      + * + * @param serviceInterface the service interface to apply the adapter to + * @param serviceFilter the filter condition to use with the service interface + * @param autoConfig the name of the member to inject the service into, or null. + * @param callbackInstance the instance to invoke the callbacks on, or null if the callbacks have to be invoked on the adapter itself + * @param add name of the callback method to invoke on add + * @param change name of the callback method to invoke on change + * @param remove name of the callback method to invoke on remove + * @param swap name of the callback method to invoke on swap + * @param propagate true if the adaptee service properties should be propagated to the adapter service consumers + * @return a service that acts as a factory for generating adapters + * @deprecated use {@link DependencyManager#createAdapterComponent()} + */ + public Component createAdapterService(Class serviceInterface, String serviceFilter, String autoConfig, Object callbackInstance, String add, String change, String remove, String swap, boolean propagate) { + return createAdapterComponent() + .setAdaptee(serviceInterface, serviceFilter) + .setAdapteeField(autoConfig) + .setAdapteeCallbackInstance(callbackInstance) + .setAdapteeCallbacks(add, change, remove, swap) + .setPropagate(propagate); + } + + /** + * Creates a new Factory Configuration Adapter. For each new factory configuration matching + * the factoryPid, an adapter will be created based on the adapter implementation class. + * The adapter will be registered with the specified interface, and with the specified adapter service properties. + * Depending on the propagate parameter, every public factory configuration properties + * (which don't start with ".") will be propagated along with the adapter service properties. + * It will also inherit all dependencies. + *

      The callback you specify may accept the following method signatures: + *

      • updated(Dictionary) + *
      • updated(Component, Dictionary) + *
      + * + *

      Usage example: + * + *

      +     *  manager.createFactoryConfigurationAdapterService("MyFactoryPid",  "update", true)
      +     *         // The interface to use when registering adapter
      +     *         .setInterface(AdapterService.class.getName(), new Hashtable() {{ put("foo", "bar"); }})
      +     *         // the implementation of the adapter
      +     *         .setImplementation(AdapterServiceImpl.class);
      +     * 
      + * + * @param factoryPid the pid matching the factory configuration + * @param update the adapter method name that will be notified when the factory configuration is created/updated. + * @param propagate true if public factory configuration should be propagated to the adapter service properties + * @return a service that acts as a factory for generating the managed service factory configuration adapter + * @deprecated use {@link DependencyManager#createFactoryComponent()} + */ + public Component createFactoryConfigurationAdapterService(String factoryPid, String update, boolean propagate) { + return createFactoryComponent() + .setFactoryPid(factoryPid) + .setUpdated(update) + .setPropagate(propagate); + } + + /** + * Creates a new Factory Configuration Adapter using a specific update callback instance. + * For each new factory configuration matching the factoryPid, an adapter will be created + * based on the adapter implementation class. + * The adapter will be registered with the specified interface, and with the specified adapter service properties. + * Depending on the propagate parameter, every public factory configuration properties + * (which don't start with ".") will be propagated along with the adapter service properties. + * It will also inherit all dependencies. + *

      The callback you specify may accept the following method signatures: + *

      • updated(Dictionary) + *
      • updated(Component, Dictionary) + *
      + * + * @param factoryPid the pid matching the factory configuration + * @param update the adapter method name that will be notified when the factory configuration is created/updated. + * @param propagate true if public factory configuration should be propagated to the adapter service properties + * @param callbackInstance the object on which the updated callback will be invoked. + * @return a service that acts as a factory for generating the managed service factory configuration adapter + * @deprecated use {@link DependencyManager#createFactoryComponent()} + */ + public Component createFactoryConfigurationAdapterService(String factoryPid, String update, boolean propagate, Object callbackInstance) { + return createFactoryComponent() + .setFactoryPid(factoryPid) + .setUpdated(update) + .setPropagate(propagate) + .setUpdateInstance(callbackInstance); + } + + /** + * Creates a new Factory Configuration Adapter. For each new factory configuration matching + * the factoryPid, an adapter will be created based on the adapter implementation class. + * The adapter will be registered with the specified interface, and with the specified adapter service properties. + * Depending on the propagate parameter, every public factory configuration properties + * (which don't start with ".") will be propagated along with the adapter service properties. + * It will also inherit all dependencies. + *

      The callback you specify may accept the following method signatures: + *

      • updated(Dictionary) + *
      • updated(Component, Dictionary) + *
      • updated(ConfigurationType) + *
      • updated(Component, ConfigurationType) + *
      + *

      The ConfigurationType parameter is an implementation of the configType argument. + * + * @param factoryPid the pid matching the factory configuration + * @param update the adapter method name that will be notified when the factory configuration is created/updated.

      + * @param propagate true if public factory configuration should be propagated to the adapter service properties + * @param configType the configuration type to use instead of a dictionary. See the javadoc from {@link ConfigurationDependency} for + * more informations about type-safe configuration. + * @return a service that acts as a factory for generating the managed service factory configuration adapter + * @see ConfigurationDependency + * @deprecated use {@link DependencyManager#createFactoryComponent()} + */ + public Component createFactoryConfigurationAdapterService(String factoryPid, String update, boolean propagate, Class configType) { + return createFactoryComponent() + .setFactoryPid(factoryPid) + .setUpdated(update) + .setPropagate(propagate) + .setConfigType(configType); + } + + /** + * Creates a new Factory Configuration Adapter using a specific update callback instance. + * For each new factory configuration matching the factoryPid, an adapter will be created + * based on the adapter implementation class. + * The adapter will be registered with the specified interface, and with the specified adapter service properties. + * Depending on the propagate parameter, every public factory configuration properties + * (which don't start with ".") will be propagated along with the adapter service properties. + * It will also inherit all dependencies. + *

      The callback you specify may accept the following method signatures: + *

      • updated(Dictionary) + *
      • updated(Component, Dictionary) + *
      • updated(ConfigurationType) + *
      • updated(Component, ConfigurationType) + *
      + *

      The ConfigurationType parameter is an implementation of the configType argument. + * + * @param factoryPid the pid matching the factory configuration + * @param propagate true if public factory configuration should be propagated to the adapter service properties + * @param callbackInstance the object on which the updated callback will be invoked. + * @param configType the configuration type to use instead of a dictionary. See the javadoc from {@link ConfigurationDependency} for + * more informations about type-safe configuration. + * @return a service that acts as a factory for generating the managed service factory configuration adapter + * @deprecated use {@link DependencyManager#createFactoryComponent()} + */ + public Component createFactoryConfigurationAdapterService(String factoryPid, String update, boolean propagate, Object callbackInstance, Class configType) { + return createFactoryComponent() + .setFactoryPid(factoryPid) + .setUpdated(update) + .setPropagate(propagate) + .setUpdateInstance(callbackInstance) + .setConfigType(configType); + } + + /** + * Creates a new Managed Service Factory Configuration Adapter with meta type support. For each new + * factory configuration matching the factoryPid, an adapter will be created based on the adapter implementation + * class. The adapter will be registered with the specified interface, and with the specified adapter service + * properties. Depending on the propagate parameter, every public factory configuration properties + * (which don't start with ".") will be propagated along with the adapter service properties. + * It will also inherit all dependencies. + * + *

      Usage example: + * + *

      +     *       PropertyMetaData[] propertiesMetaData = new PropertyMetaData[] {
      +     *            manager.createPropertyMetaData()
      +     *               .setCardinality(Integer.MAX_VALUE)
      +     *               .setType(String.class)
      +     *               .setHeading("English words")
      +     *               .setDescription("Declare here some valid english words")
      +     *               .setDefaults(new String[] {"hello", "world"})
      +     *               .setId("words")
      +     *       };
      +     * 
      +     *       manager.add(createFactoryConfigurationAdapterService("FactoryPid", 
      +     *                                                            "updated",
      +     *                                                            true, // propagate CM settings
      +     *                                                            "EnglishDictionary",
      +     *                                                            "English dictionary configuration properties",
      +     *                                                            null,
      +     *                                                            propertiesMetaData)
      +     *               .setImplementation(Adapter.class));
      +     * 
      + * + * @param factoryPid the pid matching the factory configuration + * @param update the adapter method name that will be notified when the factory configuration is created/updated.

      + * The following signatures are supported:

      + *

      • updated(Dictionary) + *
      • updated(Component, Dictionary) + *
      + * @param propagate true if public factory configuration should be propagated to the adapter service properties + * @param heading The label used to display the tab name (or section) where the properties are displayed. + * Example: "Printer Service" + * @param desc A human readable description of the factory PID this configuration is associated with. + * Example: "Configuration for the PrinterService bundle" + * @param localization Points to the basename of the Properties file that can localize the Meta Type informations. + * The default localization base name for the properties is OSGI-INF/l10n/bundle, but can + * be overridden by the manifest Bundle-Localization header (see core specification, in section Localization + * on page 68). You can specify a specific localization basename file using this parameter + * (e.g. "person" will match person_du_NL.properties in the root bundle directory). + * @param propertiesMetaData Array of MetaData regarding configuration properties + * @return a service that acts as a factory for generating the managed service factory configuration adapter + * @deprecated use {@link DependencyManager#createFactoryComponent()} + */ + public Component createAdapterFactoryConfigurationService(String factoryPid, String update, boolean propagate, + String heading, String desc, String localization, PropertyMetaData[] propertiesMetaData) + { + return createFactoryComponent() + .setFactoryPid(factoryPid) + .setUpdated(update) + .setPropagate(propagate) + .setHeading(heading) + .setDesc(desc) + .setLocalization(localization) + .add(propertiesMetaData); + } + + /** + * Creates a new bundle adapter. The adapter will be applied to any bundle that + * matches the specified bundle state mask and filter condition. For each matching + * bundle an adapter will be created based on the adapter implementation class. + * The adapter will be registered with the specified interface + * + * @param bundleStateMask the bundle state mask to apply + * @param bundleFilter the filter to apply to the bundle manifest + * @param propagate true if properties from the bundle should be propagated to the service + * @return a service that acts as a factory for generating bundle adapters + * @deprecated use {@link DependencyManager#createBundleComponent()} + */ + public Component createBundleAdapterService(int bundleStateMask, String bundleFilter, boolean propagate) { + return createBundleComponent() + .setBundleFilter(bundleStateMask, bundleFilter) + .setPropagate(propagate); + } + + /** + * Creates a new bundle adapter using specific callback instance. + * The adapter will be applied to any bundle that matches the specified bundle state mask and filter condition. + * For each matching bundle an adapter will be created based on the adapter implementation class, and + * The adapter will be registered with the specified interface. + * + * @param bundleStateMask the bundle state mask to apply + * @param bundleFilter the filter to apply to the bundle manifest + * @param propagate true if properties from the bundle should be propagated to the service + * @param callbackInstance the instance to invoke the callbacks on, or null if the callbacks have to be invoked on the adapter itself + * @param add name of the callback method to invoke on add + * @param change name of the callback method to invoke on change + * @param remove name of the callback method to invoke on remove + * @return a service that acts as a factory for generating bundle adapters + * @deprecated use {@link DependencyManager#createBundleComponent()} + */ + public Component createBundleAdapterService(int bundleStateMask, String bundleFilter, boolean propagate, + Object callbackInstance, String add, String change, String remove) { + return createBundleComponent() + .setBundleFilter(bundleStateMask, bundleFilter) + .setPropagate(propagate) + .setBundleCallbacks(add, change, remove) + .setBundleCallbackInstance(callbackInstance); + } + + /** + * Creates a new resource adapter. The adapter will be applied to any resource that + * matches the specified filter condition. For each matching resource + * an adapter will be created based on the adapter implementation class. + * The adapter will be registered with the specified interface and existing properties + * from the original resource plus any extra properties you supply here. + * It will also inherit all dependencies, and if you declare the original + * service as a member it will be injected. + * + * @param resourceFilter the filter condition to use with the resource + * @param propagate true if properties from the resource should be propagated to the service + * @param callbackInstance instance to invoke the callback on + * @param callbackChanged the name of the callback method + * @return a service that acts as a factory for generating resource adapters + * @deprecated use {@link DependencyManager#createResourceComponent()} + */ + public Component createResourceAdapterService(String resourceFilter, boolean propagate, Object callbackInstance, + String callbackChanged) + { + return createResourceComponent() + .setResourceFilter(resourceFilter) + .setPropagate(propagate) + .setBundleCallbackInstance( callbackInstance) + .setBundleCallbacks(null, callbackChanged); + } + + /** + * @see DependencyManager#createResourceAdapterService(String, boolean, Object, String) + * @deprecated use {@link DependencyManager#createResourceComponent()} + **/ + public Component createResourceAdapterService(String resourceFilter, boolean propagate, Object callbackInstance, + String callbackSet, String callbackChanged) + { + return createResourceComponent() + .setResourceFilter(resourceFilter) + .setPropagate(propagate) + .setBundleCallbackInstance( callbackInstance) + .setBundleCallbacks(callbackSet, callbackChanged); + } + + /** + * @see DependencyManager#createResourceAdapterService(String, boolean, Object, String) + * @deprecated use {@link DependencyManager#createResourceComponent()} + * */ + public Component createResourceAdapterService(String resourceFilter, Object propagateCallbackInstance, + String propagateCallbackMethod, Object callbackInstance, String callbackChanged) + { + return createResourceComponent() + .setResourceFilter(resourceFilter) + .setPropagate(propagateCallbackInstance, propagateCallbackMethod) + .setBundleCallbackInstance( callbackInstance) + .setBundleCallbacks(null, callbackChanged); + } + + /** + * @see DependencyManager#createResourceAdapterService(String, boolean, Object, String) + * @deprecated use {@link DependencyManager#createResourceComponent()} + **/ + public Component createResourceAdapterService(String resourceFilter, Object propagateCallbackInstance, + String propagateCallbackMethod, Object callbackInstance, String callbackSet, String callbackChanged) + { + return createResourceComponent() + .setResourceFilter(resourceFilter) + .setPropagate(propagateCallbackInstance, propagateCallbackMethod) + .setBundleCallbackInstance( callbackInstance) + .setBundleCallbacks(callbackSet, callbackChanged); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/compat/packageinfo b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/compat/packageinfo new file mode 100644 index 00000000000..e2525561ab2 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/compat/packageinfo @@ -0,0 +1 @@ +version 1.0.0 \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/context/AbstractDependency.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/context/AbstractDependency.java new file mode 100644 index 00000000000..69d877efc64 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/context/AbstractDependency.java @@ -0,0 +1,579 @@ +/* + * 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.dm.context; + +import java.util.Collection; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; + +import org.apache.felix.dm.ComponentDependencyDeclaration; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.ServiceDependency; + +/** + * Abstract class for implementing Dependencies. + * You can extends this class in order to supply your own custom dependencies to any Dependency Manager Component. + * + * @param The type of the interface representing a Dependency Manager Dependency (must extends the Dependency interface). + * @author Felix Project Team + */ +public abstract class AbstractDependency implements + Dependency, DependencyContext, ComponentDependencyDeclaration { + + /** + * The Component implementation is exposed to Dependencies through this interface. + */ + protected ComponentContext m_component; + + /** + * Is this Dependency available ? Volatile because the getState method (part of the + * {@link ComponentDependencyDeclaration} interface) may be called by any thread, at any time. + */ + protected volatile boolean m_available; + + /** + * Is this Dependency "instance bound" ? A dependency is "instance bound" if it is defined within the component's + * init method, meaning that it won't deactivate the component if it is not currently available when being added + * from the component's init method. + */ + protected boolean m_instanceBound; + + /** + * Is this dependency required (false by default) ? + */ + protected volatile boolean m_required; + + /** + * Component callback used to inject an added dependency. + */ + protected volatile String m_add; + + /** + * Component callback invoked when the dependency has changed. + */ + protected volatile String m_change; + + /** + * Component callback invoked when the dependency becomes unavailable. + */ + protected volatile String m_remove; + + /** + * Can this Dependency be auto configured in the component instance fields ? + */ + protected volatile boolean m_autoConfig = true; + + /** + * The Component field name where the Dependency can be injected (null means any field with a compatible type + * will be injected). + */ + protected volatile String m_autoConfigInstance; + + /** + * Indicates if the setAutoConfig method has been invoked. This flag is used to force autoconfig to "false" + * when the setCallbacks method is invoked, unless the setAutoConfig method has been called. + */ + protected volatile boolean m_autoConfigInvoked; + + /** + * Has this Dependency been started by the Component implementation ? Volatile because the getState method + * (part of the {@link ComponentDependencyDeclaration} interface) may be called by any thread, at any time. + */ + protected volatile boolean m_isStarted; + + /** + * The object instance on which the dependency callbacks are invoked on. Null means the dependency will be + * injected to the Component implementation instance(s). + */ + protected volatile Object m_callbackInstance; + + /** + * Tells if the dependency service properties have to be propagated to the Component service properties. + */ + protected volatile boolean m_propagate; + + /** + * Tells if the dependency service properties should override default component service properties (false by default). + */ + protected volatile boolean m_propagateOverrides; + + /** + * The propagate callback instance that is invoked in order to supply dynamically some dependency service properties. + */ + protected volatile Object m_propagateCallbackInstance; + + /** + * The propagate callback method that is invoked in order to supply dynamically some dependency service properties. + * @see {@link #m_propagateCallbackInstance} + */ + protected volatile String m_propagateCallbackMethod; + + /** + * Default empty dependency properties. + */ + protected final static Dictionary EMPTY_PROPERTIES = new Hashtable<>(0); + + /** + * Creates a new Dependency. By default, the dependency is optional and autoconfig. + */ + public AbstractDependency() { + } + + /** + * Create a clone of a given Dependency. + * @param prototype all the fields of the prototype will be copied to this dependency. + */ + public AbstractDependency(AbstractDependency prototype) { + m_instanceBound = prototype.m_instanceBound; + m_required = prototype.m_required; + m_add = prototype.m_add; + m_change = prototype.m_change; + m_remove = prototype.m_remove; + m_autoConfig = prototype.m_autoConfig; + m_autoConfigInstance = prototype.m_autoConfigInstance; + m_autoConfigInvoked = prototype.m_autoConfigInvoked; + m_callbackInstance = prototype.m_callbackInstance; + m_propagate = prototype.m_propagate; + m_propagateCallbackInstance = prototype.m_propagateCallbackInstance; + m_propagateCallbackMethod = prototype.m_propagateCallbackMethod; + m_propagateOverrides = prototype.m_propagateOverrides; + } + + @Override + public String toString() { + return new StringBuilder(getType()).append(" dependency [").append(getName()).append("]").toString(); + } + + // ----------------------- Dependency interface ----------------------------- + + /** + * Is this Dependency required (false by default) ? + */ + @Override + public boolean isRequired() { + return m_required; + } + + /** + * Is this Dependency satisfied and available ? + */ + @Override + public boolean isAvailable() { + return m_available; + } + + /** + * Can this dependency be injected in a component class field (by reflexion, true by default) ? + */ + @Override + public boolean isAutoConfig() { + return m_autoConfig; + } + + /** + * Returns the field name when the dependency can be injected to. + */ + @Override + public String getAutoConfigName() { + return m_autoConfigInstance; + } + + /** + * Returns the propagate callback method that is invoked in order to supply dynamically some dependency service properties. + * @see {@link #m_propagateCallbackInstance} + */ + @Override + public boolean isPropagated() { + return m_propagate; + } + + @Override + public boolean overrideServiceProperties() { + return m_propagateOverrides; + } + + /** + * Returns the dependency service properties (empty by default). + */ + @SuppressWarnings("unchecked") + @Override + public Dictionary getProperties() { + return (Dictionary) EMPTY_PROPERTIES; + } + + // -------------- DependencyContext interface ----------------------------------------------- + + /** + * Called by the Component implementation before the Dependency can be started. + */ + @Override + public void setComponentContext(ComponentContext component) { + m_component = component; + } + + /** + * A Component callback must be invoked with dependency event(s). + * @param type the dependency event type + * @param events the dependency service event to inject in the component. + * The number of events depends on the dependency event type: ADDED/CHANGED/REMOVED types only has one event parameter, + * but the SWAPPED type has two event parameters: the first one is the old event which must be replaced by the second one. + */ + @Override + public void invokeCallback(EventType type, Event ... events) { + } + + /** + * Starts this dependency. Subclasses can override this method but must then call super.start(). + */ + @Override + public void start() { + m_isStarted = true; + } + + /** + * Starts this dependency. Subclasses can override this method but must then call super.stop(). + */ + @Override + public void stop() { + m_isStarted = false; + m_available = false; + } + + /** + * Indicates if this dependency has been started by the Component implementation. + */ + @Override + public boolean isStarted() { + return m_isStarted; + } + + /** + * Called by the Component implementation when the dependency is considered to be available. + */ + @Override + public void setAvailable(boolean available) { + m_available = available; + } + + /** + * Is this Dependency "instance bound" (has been defined within the component's init method) ? + */ + public boolean isInstanceBound() { + return m_instanceBound; + } + + /** + * Called by the Component implementation when the dependency is declared within the Component's init method. + */ + public void setInstanceBound(boolean instanceBound) { + m_instanceBound = instanceBound; + } + + /** + * Tells if the Component must be first instantiated before starting this dependency (false by default). + */ + @Override + public boolean needsInstance() { + return false; + } + + /** + * Returns the type of the field where this dependency can be injected (auto config), or return null + * if autoconfig is not supported. + */ + @Override + public abstract Class getAutoConfigType(); + + /** + * Get the highest ranked available dependency service, or null. + */ + @Override + public Event getService() { + Event event = m_component.getDependencyEvent(this); + if (event == null) { + Object defaultService = getDefaultService(true); + if (defaultService != null) { + event = new Event(defaultService); + } + } + return event; + } + + /** + * Copy all dependency service instances to the given collection. + */ + @Override + public void copyToCollection(Collection services) { + Set events = m_component.getDependencyEvents(this); + if (events.size() > 0) { + for (Event e : events) { + services.add(e.getEvent()); + } + } else { + Object defaultService = getDefaultService(false); + if (defaultService != null) { + services.add(defaultService); + } + } + } + + /** + * Copy all dependency service instances to the given map (key = dependency service, value = dependency service properties. + */ + @Override + public void copyToMap(Map> map) { + Set events = m_component.getDependencyEvents(this); + if (events.size() > 0) { + for (Event e : events) { + map.put(e.getEvent(), e.getProperties()); + } + } else { + Object defaultService = getDefaultService(false); + if (defaultService != null) { + map.put(defaultService, EMPTY_PROPERTIES); + } + } + } + + /** + * Creates a copy of this Dependency. + */ + @Override + public abstract DependencyContext createCopy(); + + // -------------- ComponentDependencyDeclaration ----------------------------------------------- + + /** + * Returns a description of this dependency (like the dependency service class name with associated filters) + */ + @Override + public String getName() { + return getSimpleName(); + } + + /** + * Returns a simple name for this dependency (like the dependency service class name). + */ + @Override + public abstract String getSimpleName(); + + /** + * Returns the dependency symbolic type. + */ + @Override + public abstract String getType(); + + /** + * Returns the dependency filter, if any. + */ + @Override + public String getFilter() { + return null; + } + + /** + * Returns this dependency state. + */ + @Override + public int getState() { // Can be called from any threads, but our class attributes are volatile + if (m_isStarted) { + return (isAvailable() ? 1 : 0) + (isRequired() ? 2 : 0); + } else { + return isRequired() ? ComponentDependencyDeclaration.STATE_REQUIRED + : ComponentDependencyDeclaration.STATE_OPTIONAL; + } + } + + // -------------- Methods common to sub interfaces of Dependendency + + /** + * Activates Dependency service properties propagation (to the service properties of the component to which this + * dependency is added). + * + * @param propagate true if the dependency service properties must be propagated to the service properties of + * the component to which this dependency is added. + * @return this dependency instance + */ + @SuppressWarnings("unchecked") + public T setPropagate(boolean propagate) { + ensureNotActive(); + m_propagate = propagate; + return (T) this; + } + + @SuppressWarnings("unchecked") + public T setPropagate(boolean propagate, boolean overrideServiceProperties) { + ensureNotActive(); + m_propagate = propagate; + m_propagateOverrides = overrideServiceProperties; + return (T) this; + } + + /** + * Sets a callback instance which can ba invoked with the given method in order to dynamically retrieve the + * dependency service properties. + * + * @param instance the callback instance + * @param method the method to invoke on the callback instance + * @return this dependency instance + */ + @SuppressWarnings("unchecked") + public T setPropagate(Object instance, String method) { + setPropagate(instance != null && method != null); + m_propagateCallbackInstance = instance; + m_propagateCallbackMethod = method; + return (T) this; + } + + /** + * Sets the add/remove callbacks. + * @param add the callback to invoke when a dependency is added + * @param remove the callback to invoke when a dependency is removed + * @return this dependency instance + */ + public T setCallbacks(String add, String remove) { + return setCallbacks(add, null, remove); + } + + /** + * Sets the add/change/remove callbacks. + * @param add the callback to invoke when a dependency is added + * @param change the callback to invoke when a dependency has changed + * @param remove the callback to invoke when a dependency is removed + * @return this dependency instance + */ + public T setCallbacks(String add, String change, String remove) { + return setCallbacks(null, add, change, remove); + } + + /** + * Sets the callbacks for this service. These callbacks can be used as hooks whenever a + * dependency is added or removed. They are called on the instance you provide. When you + * specify callbacks, the auto configuration feature is automatically turned off, because + * we're assuming you don't need it in this case. + * + * @param instance the instance to call the callbacks on + * @param add the method to call when a service was added + * @param remove the method to call when a service was removed + * @return this service dependency + */ + public T setCallbacks(Object instance, String add, String remove) { + return setCallbacks(instance, add, null, remove); + } + + /** + * Sets the callbacks for this service. These callbacks can be used as hooks whenever a + * dependency is added, changed or removed. They are called on the instance you provide. When you + * specify callbacks, the auto configuration feature is automatically turned off, because + * we're assuming you don't need it in this case. + * + * @param instance the instance to call the callbacks on + * @param add the method to call when a service was added + * @param change the method to call when a service was changed + * @param remove the method to call when a service was removed + * @return this service dependency + */ + @SuppressWarnings("unchecked") + public T setCallbacks(Object instance, String add, String change, String remove) { + if ((add != null || change != null || remove != null) && !m_autoConfigInvoked) { + setAutoConfig(false); + } + m_callbackInstance = instance; + m_add = add; + m_change = change; + m_remove = remove; + return (T) this; + } + + /** + * Returns the dependency callback instances + * @return the dependency callback instances + */ + public Object[] getInstances() { + if (m_callbackInstance == null) { + return m_component.getInstances(); + } else { + return new Object[] { m_callbackInstance }; + } + } + + /** + * @see {@link ServiceDependency#setRequired(boolean)} + */ + @SuppressWarnings("unchecked") + public T setRequired(boolean required) { + m_required = required; + return (T) this; + } + + /** + * @see {@link ServiceDependency#setAutoConfig(boolean)} + */ + @SuppressWarnings("unchecked") + public T setAutoConfig(boolean autoConfig) { + if (autoConfig && getAutoConfigType() == null) { + throw new IllegalStateException("Dependency does not support auto config mode"); + } + m_autoConfig = autoConfig; + m_autoConfigInvoked = true; + return (T) this; + } + + /** + * @see {@link ServiceDependency#setAutoConfig(String instanceName)} + */ + @SuppressWarnings("unchecked") + public T setAutoConfig(String instanceName) { + if (instanceName != null && getAutoConfigType() == null) { + throw new IllegalStateException("Dependency does not support auto config mode"); + } + m_autoConfig = (instanceName != null); + m_autoConfigInstance = instanceName; + m_autoConfigInvoked = true; + return (T) this; + } + + /** + * Returns the component implementation context + * @return the component implementation context + */ + public ComponentContext getComponentContext() { + return m_component; + } + + /** + * Returns the default service, or null. + * @param nullObject if true, a null object may be returned. + * @return the default service + */ + protected Object getDefaultService(boolean nullObject) { + return null; + } + + /** + * Checks if the component dependency is not started. + */ + protected void ensureNotActive() { + if (isStarted()) { + throw new IllegalStateException("Cannot modify state while active."); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/context/ComponentContext.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/context/ComponentContext.java new file mode 100644 index 00000000000..8238f665c7f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/context/ComponentContext.java @@ -0,0 +1,202 @@ +/* + * 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.dm.context; + +import java.util.Dictionary; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.Logger; +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; + +/** + * This interface is the entry point to the Component implementation context. + * It is used by all DependencyManager Dependency implementations. + * + * @see DependencyContext interface + * @author Felix Project Team + */ +@ProviderType +public interface ComponentContext extends Component { + /** + * Returns the Component Executor gate that can be used to ensure proper component event serialization. + * When you schedule a task in the component executor, your task is executed safely and you do not need + * to managed synchronization (other external events, like service dependency events) will be queued + * until your task has been executed). + */ + public Executor getExecutor(); + + /** + * Returns the logger which can be used by the DependencyManager Dependencies implementations. + */ + public Logger getLogger(); + + /** + * Returns the Component's bundle context + * @return the Component's bundle context + */ + public BundleContext getBundleContext(); + + /** + * Returns the Compoent's bundle. + * @return the Compoent's bundle. + */ + public Bundle getBundle(); + + /** + * Sets a threadpool that the component will use when handling external events + * @param threadPool a threadpool used to handle component events and invoke the component's lifecycle callbacks + */ + public void setThreadPool(Executor threadPool); + + /** + * Starts the component. All initial dependencies previously added to the component will be started. + */ + public void start(); + + /** + * Stops the component. + */ + public void stop(); + + /** + * Is this component already started ? + * @return true if this component has been started + */ + public boolean isActive(); + + /** + * Is this component available (all required dependencies are available) ? + * @return true if this component is available (all dependencies are available), or false + */ + public boolean isAvailable(); + + /** + * Notifies the Component about a dependency event. + * An event is for example fired when:

      + *

        + *
      • a dependency service becomes available {@link EventType#ADDED}) + *
      • a dependenc service has changed is changed {@link EventType#CHANGED}) + *
      • a dependency service has been lost {@link EventType#REMOVED}) + *
      • a dependency service has been swapped by another {@link EventType#SWAPPED}) + *
      + * @param dc the dependency + * @param type the dependency event type + * @param e the dependency event + * @see EventType + */ + public void handleEvent(DependencyContext dc, EventType type, Event ... event); + + /** + * Returns the list of dependencies that has been registered on this component + * @return the list of dependencies that has been registered on this component + */ + public List getDependencies(); + + /** + * Invokes a callback method on a given set of objects. An error is logged if the callback is not found in any of the object instances. + * @param instances the component instances + * @param methodName the method name + * @param signatures the method signatures (types) + * @param parameters the method parameters + */ + public void invokeCallbackMethod(Object[] instances, String methodName, Class[][] signatures, Object[][] parameters); + + /** + * Invokes a callback method on a given set of objects. + * @param instances the component instances + * @param methodName the method name + * @param signatures the method signatures (types) + * @param parameters the method parameters + * @param logIfNotFound true if a warning message should be logged in case the callback is not found in any of the object instances. + */ + public void invokeCallbackMethod(Object[] instances, String methodName, Class[][] signatures, Object[][] parameters, boolean logIfNotFound); + + /** + * Invokes a callback method on a given set of objects. An error is logged if the callback is not found in any of the object instances. + * @param instances the component instances + * @param methodName the method name + * @param signatures the method signatures (types) + * @param paramsSupplier the supplier for the method parameters + * @param logIfNotFound true if a warning message should be logged in case the callback is not found in any of the object instances. + */ + public void invokeCallback(Object[] instances, String methodName, Class[][] signatures, Supplier[][] paramsSupplier, boolean logIfNotFound); + + /** + * Returns the component instances + * @return the component instances + */ + public Object[] getInstances(); + + /** + * Returns the component instance field that is assignable to a given class type + * @param clazz the type of an object that has to be injected in the component instance field + * @return the name of the component instance field that can be assigned to an object having the same type as + * the "clazz" parameter + */ + public String getAutoConfigInstance(Class clazz); + + /** + * Indicates if an object of the given class can be injected in one field of the component + * @param clazz the class of an object that has to be injected in one of the component fields + * @return true if the component can be injected with an object having the specified "clazz" type. + */ + public boolean getAutoConfig(Class clazz); + + /** + * Returns the highest ranked dependency service instance for a given dependency + * @param dc the dependency + * @return the highest ranked dependency service instance for a given dependency + */ + public Event getDependencyEvent(DependencyContext dc); + + /** + * Returns all the available dependency services for a given dependency + * @param dc the dependency + * @return all the available dependency services for a given dependency + */ + public Set getDependencyEvents(DependencyContext dc); + + /** + * Creates a configuration for a given type backed by a given dictionary. + * This method can be used by any custom Dependency Manager dependency that + * needs to expose some configuration through a dynamic proxy interface. + * + * @param type the configuration class, cannot be null; + * @param config the configuration to wrap, cannot be null. + * @return an instance of the given type that wraps the given configuration. + */ + public U createConfigurationType(Class type, Dictionary config); + + /** + * Instantiates the component instances. + * @return this component context. + */ + public ComponentContext instantiateComponent(); + + /** + * Indicates if the component fields and methods injections are disabled. + */ + public boolean injectionDisabled(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/context/DependencyContext.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/context/DependencyContext.java new file mode 100644 index 00000000000..42043e8df02 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/context/DependencyContext.java @@ -0,0 +1,129 @@ +/* + * 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.dm.context; + +import java.util.Collection; +import java.util.Dictionary; +import java.util.Map; + +import org.apache.felix.dm.Dependency; + +/** + * Every DependencyManager Dependency implementations must implement this interface. + * + * @see {@link AbstractDependency} which already implements most of the methods from this interface. + * @author Felix Project Team + */ +public interface DependencyContext extends Dependency { + /** + * Stores the Component implementation context in the Dependency Implementation. This object is the entry point to + * the Component implementation. + * @param component the Component implementation context + */ + public void setComponentContext(ComponentContext component); + + /** + * Returns the Component implementation context associated to this Dependency context. + */ + public ComponentContext getComponentContext(); + + /** + * The Component implementation asks this dependency to invoke a component dependency callback. + * + * @param type the type of the callback to invoke (add/change/remove/swap ...) + * @param events the dependency service event(s) that has previously been submitted to the component implementation using + * the ComponentContext.handleEvent method. The number of events depends on the event type: one event for ADDED/CHANGED/REMOVED, + * and two events for the SWAPPED event. + * @see ComponentContext#handleEvent(DependencyContext, EventType, Event...) + * @see EventType + */ + public void invokeCallback(EventType type, Event ... events); + + /** + * Invoked by the component context when the dependency should start working. + **/ + public void start(); + + /** + * Invoked by the component context when the dependency should stop working. + **/ + public void stop(); + + /** + * Returns true if the dependency has been started, false if not + * @return true if the dependency has been started, false if not + */ + public boolean isStarted(); + + /** + * Sets this dependency as available, meaning that at least one dependency service is available. + * @param available true to mark this dependency as available, false to mark it as unavailable + */ + public void setAvailable(boolean available); + + /** + * Sets this dependency as "instance bound". A dependency is "instance bound" if it is defined from the + * component's init method. + * @param true if the dependency has to be marked as "intance bound", false if not. + */ + public void setInstanceBound(boolean instanceBound); + + /** + * Is this dependency instance bound ? + * @return true if this dependency is instance bound, false if not + */ + public boolean isInstanceBound(); + + /** + * Does this dependency need the component instances to determine if the dependency is available or not. + * @return true if the dependency need the component instances before it can be started, false if not. + **/ + public boolean needsInstance(); + + /** + * Returns the type of the field which can be injected with the dependency service. + * @return the type of the field which can be injected with the dependency service, or null if the dependency does not + * support auto config mode. + */ + public Class getAutoConfigType(); + + /** + * Returns the highest ranked available dependency service instance, or null if the dependency is unavailable. + * @return the highest ranked available dependency service instance, or null + */ + public Event getService(); + + /** + * Copies all the dependency service instances to the given collection. + * @param coll the collection where the dependency service instances will be copied + */ + public void copyToCollection(Collection coll); + + /** + * Copies all the dependency service instances to the given map (key = dependency service, value = dependency servie properties). + * @param map the map where the dependency service instances (with the corresponding service properties) + */ + public void copyToMap(Map> map); + + /** + * Creates a clone of this dependency. + * @return a clone of this dependency. + */ + public DependencyContext createCopy(); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/context/Event.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/context/Event.java new file mode 100644 index 00000000000..1d48eaffa6a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/context/Event.java @@ -0,0 +1,79 @@ +/* + * 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.dm.context; + +import java.util.Dictionary; +import java.util.Hashtable; + +/** + * An event holds all data that belongs to some external event as it comes in via + * the 'changed' callback of a dependency. + * + * @author Felix Project Team + */ +public class Event implements Comparable { + protected final static Dictionary EMPTY_PROPERTIES = new Hashtable<>(); + private final Object m_event; // the actual event object (a Service, a Bundle, a Configuration, etc ...) + + public Event(Object event) { + m_event = event; + } + + /** + * Returns the actual event object wrapped by this event (a Service Dependency, a Bundle for Bundle Dependency, etc...). + */ + @SuppressWarnings("unchecked") + public T getEvent() { + return (T) m_event; + } + + /** + * Returns the properties of the actual event object wrapped by this event (Service Dependency properties, ...). + */ + @SuppressWarnings("unchecked") + public Dictionary getProperties() { + return (Dictionary) EMPTY_PROPERTIES; + } + + @Override + public int hashCode() { + return m_event.hashCode(); + } + + @Override + public boolean equals(Object obj) { + // an instanceof check here is not "strong" enough with subclasses overriding the + // equals: we need to be sure that a.equals(b) == b.equals(a) at all times + if (obj != null && obj.getClass().equals(Event.class)) { + return (((Event) obj).m_event).equals(m_event); + } + return false; + } + + @Override + public int compareTo(Event o) { + return 0; + } + + /** + * Release the resources this event is holding (like service reference for example). + */ + public void close() { + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/context/EventType.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/context/EventType.java new file mode 100644 index 00000000000..28f514faa36 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/context/EventType.java @@ -0,0 +1,46 @@ +/* + * 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.dm.context; + +/** + * Types of dependency events + * + * @author Felix Project Team + */ +public enum EventType { + /** + * A Dependency service becomes available. + */ + ADDED, + + /** + * A Dependency service has changed. + */ + CHANGED, + + /** + * A Dependency service becomes unavailable. + */ + REMOVED, + + /** + * A Dependency service has been swapped by another one. + */ + SWAPPED +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/context/packageinfo b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/context/packageinfo new file mode 100644 index 00000000000..7af54cb3ec7 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/context/packageinfo @@ -0,0 +1 @@ +version 4.4.1 diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/CircularDependency.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/CircularDependency.java new file mode 100644 index 00000000000..8d4de083861 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/CircularDependency.java @@ -0,0 +1,48 @@ +/* + * 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.dm.diagnostics; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.felix.dm.ComponentDeclaration; + +public class CircularDependency { + + private List m_components = new ArrayList<>(); + + void addComponent(ComponentDeclaration component) { + m_components.add(component); + } + + public List getComponents() { + return Collections.unmodifiableList(m_components); + } + + @Override + public String toString() { + String result = ""; + for(ComponentDeclaration c : m_components) { + result += " -> " + c.getName(); + } + return result; + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/ComponentNode.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/ComponentNode.java new file mode 100644 index 00000000000..8d8fcdb6cbc --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/ComponentNode.java @@ -0,0 +1,40 @@ +/* + * 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.dm.diagnostics; + +import org.apache.felix.dm.ComponentDeclaration; + +class ComponentNode extends DependencyGraphNode { + + private ComponentDeclaration m_componentDeclaration; + + public ComponentNode(ComponentDeclaration componentDeclaration) { + m_componentDeclaration = componentDeclaration; + } + + public ComponentDeclaration getComponentDeclaration() { + return m_componentDeclaration; + } + + @Override + public String toString() { + return m_componentDeclaration.getName(); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/DependencyGraph.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/DependencyGraph.java new file mode 100644 index 00000000000..0994e48fabe --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/DependencyGraph.java @@ -0,0 +1,415 @@ +/* + * 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.dm.diagnostics; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Stack; +import java.util.stream.Collectors; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentDeclaration; +import org.apache.felix.dm.ComponentDependencyDeclaration; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.Bundle; + +/** + * The dependency graph is a view of all components managed by the dependency manager + * and of their dependencies. Using this API you can get the dependencies of a given component, + * the components providing a given service, the circular dependencies that might exist. + * + * @author Felix Project Team + * + */ +public class DependencyGraph { + + /** + * Use this to specify which components the dependency graph should contain + */ + public enum ComponentState { + ALL, + UNREGISTERED + }; + + /** + * Use this to specify which dependencies the graph should contain + */ + public enum DependencyState { + ALL, + ALL_UNAVAILABLE, + REQUIRED_UNAVAILABLE + }; + + private static final String SERVICE = "service"; + private static final String CONFIGURATION = "configuration"; + private static final String BUNDLE = "bundle"; + private static final String RESOURCE = "resource"; + + private Map m_componentToNode = new HashMap<>(); + private Map m_dependencyToNode = new HashMap<>(); + private List> m_circularDependencies = new ArrayList<>(); + private Map m_parent = new HashMap<>(); + + private ComponentState m_componentState = ComponentState.ALL; + private DependencyState m_dependencyState = DependencyState.ALL; + + private DependencyGraph(ComponentState componentState, DependencyState dependencyState) { + + m_componentState = componentState; + m_dependencyState = dependencyState; + + buildComponentNodes(); + buildDependecyNodesAndEdges(); + + } + + private void buildComponentNodes() { + List dependencyManagers = DependencyManager.getDependencyManagers(); + for(DependencyManager dm : dependencyManagers) { + List components = dm.getComponents(); + for(Component c : components) { + ComponentDeclaration cd = c.getComponentDeclaration(); + if(componentMustBeAddedToGraph(cd)) { + m_componentToNode.put(cd, new ComponentNode(cd)); + } + } + } + } + + private boolean componentMustBeAddedToGraph(ComponentDeclaration cd) { + if(m_componentState == ComponentState.ALL) { + return true; + } else if(m_componentState == ComponentState.UNREGISTERED) { + return cd.getState() == ComponentDeclaration.STATE_UNREGISTERED; + } + return false; + } + + private void buildDependecyNodesAndEdges() { + + for(DependencyGraphNode node : m_componentToNode.values()) { + ComponentNode componentNode = (ComponentNode)node; + ComponentDependencyDeclaration[] dependencyDeclarations = componentNode.getComponentDeclaration().getComponentDependencies(); + + for(ComponentDependencyDeclaration cdd : dependencyDeclarations) { + if(dependencyMustBeAddedToGraph(cdd)) { + DependencyNode dependencyNode = new DependencyNode(cdd); + m_dependencyToNode.put(cdd, dependencyNode); + + // add edges from the component node to newly created dependency node + componentNode.addSuccessor(dependencyNode); + + // add edges from the newly created dependency node to the components + // providing those dependencies (only applicable to service dependencies) + List providerComponents = getProviderComponents(dependencyNode); + for(ComponentNode p : providerComponents) { + dependencyNode.addSuccessor(p); + } + } + } + } + } + + private List getProviderComponents(DependencyNode dependencyNode) { + List result = new ArrayList<>(); + + ComponentDependencyDeclaration cdd = dependencyNode.getDependencyDeclaration(); + if(!SERVICE.equals(cdd.getType())) { + return result; + } + + for(DependencyGraphNode n : m_componentToNode.values()) { + ComponentNode componentNode = (ComponentNode)n; + if(componentProvidesDependency(componentNode, dependencyNode)) { + result.add(componentNode); + } + } + + return result; + } + + private boolean componentProvidesDependency(ComponentNode componentNode, DependencyNode dependencyNode) { + ComponentDeclaration cd = componentNode.getComponentDeclaration(); + + String dependencyName = dependencyNode.getDependencyDeclaration().getName(); + String simpleName = getSimpleName(dependencyName); + Properties properties = parseProperties(dependencyName); + + String componentName = cd.getName(); + String simpleComponentName = componentName; + + int cuttOff = simpleComponentName.indexOf("("); + if (cuttOff != -1) { + simpleComponentName = simpleComponentName.substring(0, cuttOff).trim(); + } + for (String serviceName : simpleComponentName.split(",")) { + if (simpleName.equals(serviceName.trim()) && doPropertiesMatch(properties, parseProperties(componentName))) { + return true; + } + } + return false; + } + + private boolean dependencyMustBeAddedToGraph(ComponentDependencyDeclaration cdd) { + if(m_dependencyState == DependencyState.ALL) { + return true; + } else if(m_dependencyState == DependencyState.ALL_UNAVAILABLE) { + return + (cdd.getState() == ComponentDependencyDeclaration.STATE_UNAVAILABLE_REQUIRED) || + (cdd.getState() == ComponentDependencyDeclaration.STATE_UNAVAILABLE_OPTIONAL); + } else if(m_dependencyState == DependencyState.REQUIRED_UNAVAILABLE) { + return cdd.getState() == ComponentDependencyDeclaration.STATE_UNAVAILABLE_REQUIRED; + + } + return false; + } + + /** + * Build the dependency graph. It will contain all the components managed by the dependency manager, provided + * that those components are in the given state, and all dependencies of their dependencies, provided that the + * dependencies are in the given state. + * + *

      This implementation currently only builds a graph of unregistered components and + * required unavailable dependencies. + * + * @param componentState Include only the components in this state + * @param dependencyState Include only the dependencies in this state + * @return + */ + public static DependencyGraph getGraph(ComponentState componentState, DependencyState dependencyState) { + return new DependencyGraph(componentState, dependencyState); + } + + /** + * Returns the list of components in the graph + * @return the list of components in the graph + */ + public List getAllComponents() { + return new ArrayList(m_componentToNode.keySet()); + } + + /** + * Returns a list all dependencies in the graph + * @return the list of all dependencies in the graph + */ + public List getAllDependencies() { + return new ArrayList(m_dependencyToNode.keySet()); + + } + + /** + * For a given component declaration, it returns a list of its dependencies in the state + * specified when the graph was built. + * @param componentDeclaration + * @return the list of dependencies or null if the component declaration is not in the graph + */ + public List getDependecies(ComponentDeclaration componentDeclaration) { + List result = new ArrayList<>(); + + DependencyGraphNode node = m_componentToNode.get(componentDeclaration); + if(node == null) { + return null; + } + + for(DependencyGraphNode s : node.getSuccessors()) { + result.add( ((DependencyNode)s).getDependencyDeclaration() ); + } + + return result; + } + + /** + * Returns the list of components that provide the given dependency. This only returns the components + * managed by the dependency manager that are in the state specified when the graph was built. The + * dependency can only be a service dependency. + * + * @param dependency + * @return the list of components providing this dependency or null if the dependency declaration is + * not in the graph + */ + public List getProviders(ComponentDependencyDeclaration dependency) { + List result = new ArrayList<>(); + + DependencyGraphNode node = m_dependencyToNode.get(dependency); + if(node == null) { + return null; + } + + for(DependencyGraphNode s : node.getSuccessors()) { + result.add(((ComponentNode)s).getComponentDeclaration()); + } + + return result; + } + + /** + * Returns the list of circular dependencies in the graph + * @return the list of circular dependencies + */ + public List getCircularDependencies() { + List result = new ArrayList(); + + for(DependencyGraphNode n : m_componentToNode.values()) { + if(n.isUndiscovered()) { + depthFirstSearch(n); + } + } + + for(List cycle : m_circularDependencies) { + CircularDependency circularDependency = new CircularDependency(); + for(DependencyGraphNode n : cycle) { + if(n instanceof ComponentNode) { + circularDependency.addComponent(((ComponentNode) n).getComponentDeclaration()); + } + } + + result.add(circularDependency); + } + + return result; + } + + private void depthFirstSearch(DependencyGraphNode n) { + + n.setState(DependencyGraphNode.DependencyGraphNodeState.DISCOVERED); + for(DependencyGraphNode s : n.getSuccessors()) { + if(s.isUndiscovered()) { + m_parent.put(s, n); + depthFirstSearch(s); + } else if(s.isDiscovered()) { + addCycle(n, s); + } + } + n.setState(DependencyGraphNode.DependencyGraphNodeState.PROCESSED); + } + + private void addCycle(DependencyGraphNode n, DependencyGraphNode s) { + List cycle = new ArrayList<>(); + Stack stack = new Stack<>(); + + stack.push(s); + for(DependencyGraphNode p = n; p != s; p = m_parent.get(p)) { + stack.push(p); + } + stack.push(s); + + while(!stack.isEmpty()) { + cycle.add(stack.pop()); + } + m_circularDependencies.add(cycle); + } + + /** + * Returns all the missing dependencies of a given type. + * @param type The type of the dependencies to be returned. This can be either one of the types + * known by the DependencyManager (service, bundle, configuration, resource), + * a user defined type or null, in which case all missing dependencies must be returned. + * + * @return The missing dependencies of the given type or all the missing dependencies. + */ + public List getMissingDependencies(String type) { + + List result = new ArrayList<>(); + + // get all dependency nodes that have no out-going edges + List missingDependencies = new ArrayList<>(); + for(DependencyGraphNode node : m_dependencyToNode.values()) { + DependencyNode dependencyNode = (DependencyNode)node; + if(!dependencyNode.isUnavailable()) { + continue; + } + + if( (type != null) && (!dependencyNode.getDependencyDeclaration().getType().equals(type)) ) { + continue; + } + if (dependencyNode.getSuccessors().isEmpty()) { + missingDependencies.add(dependencyNode); + } + } + + for(DependencyNode node : missingDependencies) { + for(DependencyGraphNode p : node.getPredecessors()) { + ComponentNode componentNode = (ComponentNode)p; + Bundle bundle = componentNode.getComponentDeclaration().getBundleContext().getBundle(); + MissingDependency missingDependency = new MissingDependency( + node.getDependencyDeclaration().getName(), + node.getDependencyDeclaration().getType(), + bundle.getSymbolicName()); + result.add(missingDependency); + } + } + return result; + } + + /** + * Returns all custom missing dependencies. Custom dependencies are not 'out of the box' dependencies (service, resource, + * bundle, configuration). + * + * @return The missing dependencies which are not 'out of the box dependencies'. + */ + public List getMissingCustomDependencies() { + + List missingDependencies = getMissingDependencies(null); + return missingDependencies.stream().filter(m -> !isOutOfTheBoxDependencyType(m.getType())).collect(Collectors.toList()); + + } + + private boolean isOutOfTheBoxDependencyType(String type) { + return SERVICE.equals(type) || CONFIGURATION.equals(type) || BUNDLE.equals(type) || RESOURCE.equals(type); + } + + private String getSimpleName(String name) { + int cuttOff = name.indexOf("("); + if (cuttOff != -1) { + return name.substring(0, cuttOff).trim(); + } + return name.trim(); + } + + private Properties parseProperties(String name) { + Properties result = new Properties(); + int cuttOff = name.indexOf("("); + if (cuttOff != -1) { + String propsText = name.substring(cuttOff + 1, name.indexOf(")")); + String[] split = propsText.split(","); + for (String prop : split) { + String[] kv = prop.split("="); + if (kv.length == 2) { + result.put(kv[0], kv[1]); + } + } + } + return result; + } + + private boolean doPropertiesMatch(Properties need, Properties provide) { + for (Entry entry : need.entrySet()) { + Object prop = provide.get(entry.getKey()); + if (prop == null || !prop.equals(entry.getValue())) { + return false; + } + } + return true; + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/DependencyGraphNode.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/DependencyGraphNode.java new file mode 100644 index 00000000000..775cd28bfc3 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/DependencyGraphNode.java @@ -0,0 +1,71 @@ +/* + * 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.dm.diagnostics; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +class DependencyGraphNode { + + public enum DependencyGraphNodeState { + UNDISCOVERED, + DISCOVERED, + PROCESSED + }; + + private List m_successors = new ArrayList<>(); + private List m_predecessors = new ArrayList<>(); + private DependencyGraphNodeState m_state = DependencyGraphNodeState.UNDISCOVERED; + + public void addSuccessor(DependencyGraphNode successor) { + m_successors.add(successor); + successor.addPredecessor(this); + } + + private void addPredecessor(DependencyGraphNode predecessor) { + m_predecessors.add(predecessor); + } + + public List getSuccessors() { + return Collections.unmodifiableList(m_successors); + } + + public List getPredecessors() { + return Collections.unmodifiableList(m_predecessors); + } + + void setState(DependencyGraphNodeState state) { + m_state = state; + } + + boolean isDiscovered() { + return m_state == DependencyGraphNodeState.DISCOVERED; + } + + boolean isUndiscovered() { + return m_state == DependencyGraphNodeState.UNDISCOVERED; + } + + boolean isProcessed() { + return m_state == DependencyGraphNodeState.PROCESSED; + } + + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/DependencyNode.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/DependencyNode.java new file mode 100644 index 00000000000..5eab1d2b422 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/DependencyNode.java @@ -0,0 +1,53 @@ +/* + * 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.dm.diagnostics; + +import org.apache.felix.dm.ComponentDependencyDeclaration; + +class DependencyNode extends DependencyGraphNode { + + private ComponentDependencyDeclaration m_dependencyDeclaration; + + public DependencyNode(ComponentDependencyDeclaration dependencyDeclaration) { + m_dependencyDeclaration = dependencyDeclaration; + } + + public ComponentDependencyDeclaration getDependencyDeclaration() { + return m_dependencyDeclaration; + } + + public boolean isUnavailableRequired() { + return m_dependencyDeclaration.getState() == ComponentDependencyDeclaration.STATE_UNAVAILABLE_REQUIRED; + } + + public boolean isUnavailableOptional() { + return m_dependencyDeclaration.getState() == ComponentDependencyDeclaration.STATE_UNAVAILABLE_OPTIONAL; + } + + public boolean isUnavailable() { + return m_dependencyDeclaration.getState() == ComponentDependencyDeclaration.STATE_UNAVAILABLE_OPTIONAL + || m_dependencyDeclaration.getState() == ComponentDependencyDeclaration.STATE_UNAVAILABLE_REQUIRED; + } + + @Override + public String toString() { + return m_dependencyDeclaration.getName(); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/MissingDependency.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/MissingDependency.java new file mode 100644 index 00000000000..3d872e45925 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/MissingDependency.java @@ -0,0 +1,62 @@ +/* + * 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.dm.diagnostics; + +/** + * This represents a missing dependency. It can have any of the four types known to the Dependency Manager (service, + * configuration, bundle and resource) or it can be a type defined by the programmer. + * A missing dependency is defined by its name, its type and the bundle name of the bundle for which + * this dependency is unavailable. + * + * @author Felix Project Team + * + */ +public class MissingDependency { + + private final String name; + private final String type; + private final String bundleName; + + public MissingDependency(String name, String type, String bundleName) { + this.name = name; + this.type = type; + this.bundleName = bundleName; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public String getBundleName() { + return bundleName; + } + + @Override + public String toString() { + return "Missing dependency: " + + "name = " + name + " " + + "type = " + type + " " + + "bundleName = " + bundleName; + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/packageinfo b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/packageinfo new file mode 100644 index 00000000000..b1793a21a72 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/diagnostics/packageinfo @@ -0,0 +1 @@ +version 1.1.0 \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/AbstractDecorator.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/AbstractDecorator.java new file mode 100644 index 00000000000..9c961808d8e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/AbstractDecorator.java @@ -0,0 +1,236 @@ +/* + * 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.dm.impl; + +import java.net.URL; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.context.ComponentContext; +import org.apache.felix.dm.context.DependencyContext; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.ConfigurationException; + +/** + * @author Felix Project Team + */ +public abstract class AbstractDecorator { + protected volatile DependencyManager m_manager; + private final Map m_services = new ConcurrentHashMap<>(); + private volatile ComponentContext m_decoratorComponent; + private final Map m_depclones = new HashMap<>(); + + public abstract Component createService(Object[] properties) throws Exception; + + /** + * Catches our DependencyManager handle from our component init method. + */ + public void init(Component c) { + m_manager = c.getDependencyManager(); + m_decoratorComponent = (ComponentContext) c; + } + + /** + * Extra method, which may be used by sub-classes, when adaptee has changed. + * For now, it's only used by the FactoryConfigurationAdapterImpl class, + * but it might also make sense to use this for Resource Adapters ... + */ + public void updateService(Object[] properties) throws Exception { + throw new NoSuchMethodException("Method updateService not implemented"); + } + + /** + * Set some service properties to all already instantiated services. + */ + public void setServiceProperties(Dictionary serviceProperties) { + for (Component component : m_services.values()) { + component.setServiceProperties(serviceProperties); + } + } + + /** + * Remove a StateListener from all already instantiated services. + */ + public void addStateListener(ComponentStateListener listener) { + for (Component component : m_services.values()) { + component.add(listener); + } + } + + /** + * Remove a StateListener from all already instantiated services. + */ + public void removeStateListener(ComponentStateListener listener) { + for (Component component : m_services.values()) { + component.remove(listener); + } + } + + /** + * Add a Dependency to all already instantiated services. + */ + public void addDependency(Dependency ... dependencies) { + for (Component component : m_services.values()) { + Dependency[] copy = Stream.of(dependencies) + .map(d -> (DependencyContext) d) + .map(dc -> dc.createCopy()) + .toArray(Dependency[]::new); + for (int i = 0; i < dependencies.length; i ++) { + m_depclones.put(dependencies[i], copy[i]); + } + component.add(copy); + } + } + + /** + * Remove a Dependency from all instantiated services. + */ + public void removeDependency(Dependency d) { + for (Component component : m_services.values()) { + Dependency copy = m_depclones.remove(d); + if (copy != null) { + component.remove(copy); + } + } + } + + // callbacks for FactoryConfigurationAdapterImpl from the ConfigAdmin thread + @SuppressWarnings("rawtypes") + public void updated(String pid, Dictionary properties) throws ConfigurationException { + // FELIX-5193: invoke the updated callback in the internal decorator component queue, in order + // to safely detect if the component is still active or not. + InvocationUtil.invokeUpdated(m_decoratorComponent.getExecutor(), () -> updatedSafe(pid, properties)); + } + + @SuppressWarnings("rawtypes") + private void updatedSafe(String pid, Dictionary properties) throws Exception { + if (!m_decoratorComponent.isActive()) { + // Our decorator component has been removed: ignore the configuration update. + return; + } + Component service = m_services.get(pid); + if (service == null) { + service = createService(new Object[] { properties }); + m_services.put(pid, service); + m_manager.add(service); + } else { + updateService(new Object[] { properties, service }); + } + } + + public void deleted(String pid) { + Component service = m_services.remove(pid); + if (service != null) { + m_manager.remove(service); + } + } + + // callbacks for resources + public void added(URL resource) throws Exception { + Component newService = createService(new Object[] { resource }); + m_services.put(resource, newService); + m_manager.add(newService); + } + + public void removed(URL resource) { + Component newService = m_services.remove(resource); + if (newService == null) { + throw new IllegalStateException("Service should not be null here."); + } + m_manager.remove(newService); + } + + // callbacks for services + public void added(ServiceReference ref, Object service) throws Exception { + Component newService = createService(new Object[] { ref, service }); + m_services.put(ref, newService); + m_manager.add(newService); + } + + public void removed(ServiceReference ref, Object service) { + Component newService; + newService = (Component) m_services.remove(ref); + if (newService == null) { + throw new IllegalStateException("Service should not be null here."); + } + m_manager.remove(newService); + } + + public void swapped(ServiceReference oldRef, Object oldService, ServiceReference newRef, Object newService) { + Component service = (Component) m_services.remove(oldRef); + if (service == null) { + throw new IllegalStateException("Service should not be null here."); + } + m_services.put(newRef, service); + } + + // callbacks for bundles + public void added(Bundle bundle) throws Exception { + Component newService = createService(new Object[] { bundle }); + m_services.put(bundle, newService); + m_manager.add(newService); + } + + public void removed(Bundle bundle) { + Component newService; + newService = (Component) m_services.remove(bundle); + if (newService == null) { + throw new IllegalStateException("Service should not be null here."); + } + m_manager.remove(newService); + } + + public void stop() { + for (Component component : m_services.values()) { + m_manager.remove(component); + } + m_services.clear(); + } + + public void configureAutoConfigState(Component target, ComponentContext source) { + configureAutoConfigState(target, source, BundleContext.class); + configureAutoConfigState(target, source, ServiceRegistration.class); + configureAutoConfigState(target, source, DependencyManager.class); + configureAutoConfigState(target, source, Component.class); + } + + public Map getServices() { + return m_services; + } + + private void configureAutoConfigState(Component target, ComponentContext source, Class clazz) { + String name = source.getAutoConfigInstance(clazz); + if (name != null) { + target.setAutoConfig(clazz, name); + } + else { + target.setAutoConfig(clazz, source.getAutoConfig(clazz)); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/Activator.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/Activator.java new file mode 100644 index 00000000000..8d6ec9a2aca --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/Activator.java @@ -0,0 +1,103 @@ +/* + * 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.dm.impl; + +import org.apache.felix.dm.ComponentExecutorFactory; +import org.apache.felix.dm.FilterIndex; +import org.apache.felix.dm.impl.index.ServiceRegistryCacheManager; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Filter; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +/** + * DependencyManager Activator used to track a ComponentExecutorFactory service + * optionally registered by a management agent bundle. + * + * @see {@link ComponentExecutorFactory} + * @author Felix Project Team + */ +public class Activator implements BundleActivator { + private BundleContext m_context; + private ServiceTracker m_execTracker; + private ServiceTracker m_indexTracker; + + @Override + public void start(BundleContext context) throws Exception { + // make sure the backdoor is set in system properties (needed by tests) + ServiceRegistryCacheManager.init(); + + m_context = context; + Filter execFilter = context.createFilter("(objectClass=" + ComponentExecutorFactory.class.getName() + ")"); + m_execTracker = new ServiceTracker<>(context, execFilter, new ExecutorFactoryCustomizer()); + m_execTracker.open(); + + Filter indexFilter = context.createFilter("(objectClass=" + FilterIndex.class.getName() + ")"); + m_indexTracker = new ServiceTracker<>(context, indexFilter, new FilterIndexCustomizer()); + m_indexTracker.open(); + } + + @Override + public void stop(BundleContext context) throws Exception { + if (m_execTracker != null) { + m_execTracker.close(); + } + if (m_indexTracker != null) { + m_indexTracker.close(); + } + } + + private class ExecutorFactoryCustomizer implements ServiceTrackerCustomizer { + @Override + public ComponentExecutorFactory addingService(ServiceReference reference) { + ComponentExecutorFactory factory = (ComponentExecutorFactory) m_context.getService(reference); + ComponentScheduler.instance().bind(factory); + return factory; + } + + @Override + public void modifiedService(ServiceReference reference, ComponentExecutorFactory service) { + } + + @Override + public void removedService(ServiceReference reference, ComponentExecutorFactory factory) { + ComponentScheduler.instance().unbind(factory); + } + } + + private class FilterIndexCustomizer implements ServiceTrackerCustomizer { + @Override + public FilterIndex addingService(ServiceReference reference) { + FilterIndex index = m_context.getService(reference); + ServiceRegistryCacheManager.registerFilterIndex(index, m_context); + return index; + } + + @Override + public void modifiedService(ServiceReference reference, FilterIndex service) { + } + + @Override + public void removedService(ServiceReference reference, FilterIndex index) { + ServiceRegistryCacheManager.unregisterFilterIndex(index); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/AdapterServiceImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/AdapterServiceImpl.java new file mode 100644 index 00000000000..6eaa0c0254b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/AdapterServiceImpl.java @@ -0,0 +1,177 @@ +/* + * 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.dm.impl; + +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.List; + +import org.apache.felix.dm.AdapterComponent; +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ServiceDependency; +import org.apache.felix.dm.context.DependencyContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + +/** + * Adapter Service implementation. This class extends the FilterService in order to catch + * some Service methods for configuring actual adapter service implementation. + * + * @author Felix Project Team + */ +public class AdapterServiceImpl extends FilterComponent implements AdapterComponent { + + private volatile Class m_adapteeInterface; + private volatile String m_adapteeFilter; + private volatile String m_adapteeAutoConfig; + private volatile String m_adapteeAdd; + private volatile String m_adapteeChange; + private volatile String m_adapteeRemove; + private volatile String m_adapteeSwap; + private volatile boolean m_adapteePropage = true; + private volatile Object m_adapteeCallbackInstance; + + /** + * Creates a new Adapter Service implementation. + */ + public AdapterServiceImpl(DependencyManager dm) { + super(dm.createComponent()); + } + + @Override + public AdapterServiceImpl setAdaptee(Class service, String filter) { + m_adapteeInterface = service; + m_adapteeFilter = filter; + return this; + } + @Override + public AdapterServiceImpl setAdapteeField(String autoConfig) { + m_adapteeAutoConfig = autoConfig; + return this; + } + @Override + public AdapterServiceImpl setAdapteeCallbacks(String add, String change, String remove, String swap) { + m_adapteeAdd = add; + m_adapteeChange = change; + m_adapteeRemove = remove; + m_adapteeSwap = swap; + return this; + } + @Override + public AdapterServiceImpl setPropagate(boolean propagate) { + m_adapteePropage = propagate; + return this; + } + @Override + public AdapterServiceImpl setAdapteeCallbackInstance(Object callbackInstance) { + m_adapteeCallbackInstance = callbackInstance; + return this; + } + + @Override + protected void startInitial() { + DependencyManager dm = getDependencyManager(); + m_component + .setImplementation(new AdapterImpl()) + .setCallbacks("init", null, "stop", null) + .add(dm.createServiceDependency() + .setService(m_adapteeInterface, m_adapteeFilter) + .setAutoConfig(false) + .setCallbacks("added", null, "removed", "swapped")); + } + + public class AdapterImpl extends AbstractDecorator { + + public Component createService(Object[] properties) { + ServiceReference ref = (ServiceReference) properties[0]; + Object aspect = ref.getProperty(DependencyManager.ASPECT); + String serviceIdToTrack = (aspect != null) ? aspect.toString() : ref.getProperty(Constants.SERVICE_ID).toString(); + List dependencies = m_component.getDependencies(); + dependencies.remove(0); + ServiceDependency dependency = m_manager.createServiceDependency() + // create a dependency on both the service id we're adapting and possible aspects for this given service id + .setService(m_adapteeInterface, "(|(" + Constants.SERVICE_ID + "=" + serviceIdToTrack + + ")(" + DependencyManager.ASPECT + "=" + serviceIdToTrack + "))") + .setRequired(true); + if (m_adapteeAdd != null || m_adapteeChange != null || m_adapteeRemove != null || m_adapteeSwap != null) { + dependency.setCallbacks(m_adapteeCallbackInstance, m_adapteeAdd, m_adapteeChange, m_adapteeRemove, m_adapteeSwap); + } + if (m_adapteeAutoConfig != null) { + dependency.setAutoConfig(m_adapteeAutoConfig); + } + + if (m_adapteePropage) { + dependency.setPropagate(this, "propagateAdapteeProperties"); + } + +// dependency.setDebug("AdapterDependency#" + m_adapteeInterface.getSimpleName()); + + Component service = m_manager.createComponent() + .setInterface(m_serviceInterfaces, getServiceProperties(ref)) + .setImplementation(m_serviceImpl) + .setFactory(m_factory, m_factoryCreateMethod) // if not set, no effect + .setComposition(m_compositionInstance, m_compositionMethod) // if not set, no effect + .setCallbacks(m_callbackObject, m_init, m_start, m_stop, m_destroy) // if not set, no effect + .setScope(m_scope) + .add(dependency); + + configureAutoConfigState(service, m_component); + copyDependencies(dependencies, service); + + for (ComponentStateListener stateListener : m_stateListeners) { + service.add(stateListener); + } + return service; + } + + public String toString() { + return "Adapter for " + m_adapteeInterface + ((m_adapteeFilter != null) ? " with filter " + m_adapteeFilter : ""); + } + + public Dictionary getServiceProperties(ServiceReference ref) { + Dictionary props = new Hashtable<>(); + if (m_serviceProperties != null) { + Enumeration e = m_serviceProperties.keys(); + while (e.hasMoreElements()) { + String key = e.nextElement(); + props.put(key, m_serviceProperties.get(key)); + } + } + return props; + } + + public Dictionary propagateAdapteeProperties(ServiceReference ref) { + Dictionary props = new Hashtable<>(); + String[] keys = ref.getPropertyKeys(); + for (int i = 0; i < keys.length; i++) { + String key = keys[i]; + if (ServiceUtil.NOT_PROPAGATABLE_SERVICE_PROPERTIES.contains(key)) { + // do not copy this key which is not propagatable. + } + else { + props.put(key, ref.getProperty(key)); + } + } + return props; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/AspectServiceImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/AspectServiceImpl.java new file mode 100644 index 00000000000..a0dd3ed4b6a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/AspectServiceImpl.java @@ -0,0 +1,222 @@ +/* + * 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.dm.impl; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.List; + +import org.apache.felix.dm.AspectComponent; +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ServiceDependency; +import org.apache.felix.dm.context.DependencyContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + +/** + * @author Felix Project Team + */ +public class AspectServiceImpl extends FilterComponent implements AspectComponent { + + private volatile String m_add; + private volatile String m_change; + private volatile String m_remove; + private volatile String m_swap; + private volatile int m_ranking; + private volatile Object m_dependencyCallbackInstance; + private volatile Class m_serviceInterface; + private volatile String m_serviceFilter; + private volatile String m_aspectAutoConfig; + + public AspectServiceImpl(DependencyManager dm) { + super(dm.createComponent()); + } + + public AspectServiceImpl setAspect(Class serviceInterface, String filter, int ranking) { + m_serviceInterface = serviceInterface; + m_serviceFilter = filter; + m_ranking = ranking; + return this; + } + public AspectServiceImpl setAspectField(String autoConfig) { + m_aspectAutoConfig = autoConfig; + return this; + } + public AspectServiceImpl setAspectCallbacks(String add, String change, String remove, String swap) { + m_add = add; + m_change = change; + m_remove = remove; + m_swap = swap; + return this; + } + public AspectServiceImpl setAspectCallbackInstance(Object callbackInstance) { + m_dependencyCallbackInstance = callbackInstance; + return this; + } + + @Override + protected void startInitial() { + DependencyManager dm = getDependencyManager(); + m_component + .setImplementation(new AspectImpl(m_serviceInterface, m_aspectAutoConfig)) + .setCallbacks("init", null, "stop", null) + .add(dm.createServiceDependency() + .setService(m_serviceInterface, createDependencyFilterForAspect(m_serviceFilter)) + .setAutoConfig(false) + .setCallbacks("added", "removed")); + } + + private String createDependencyFilterForAspect(String aspectFilter) { + // we only want to match services which are not themselves aspects + if (aspectFilter == null || aspectFilter.length() == 0) { + return "(!(" + DependencyManager.ASPECT + "=*))"; + } + else { + return "(&(!(" + DependencyManager.ASPECT + "=*))" + aspectFilter + ")"; + } + } + + /** + * Returns the aspect service properties (not the inherited properties from the lower ranked aspect or from the original service). + * @param originalServiceRef + * @return + */ + private Hashtable getAspectServiceProperties(ServiceReference originalServiceRef) { + Hashtable props = new Hashtable<>(); + if (m_serviceProperties != null) { + Enumeration e = m_serviceProperties.keys(); + while (e.hasMoreElements()) { + String key = e.nextElement(); + props.put(key, m_serviceProperties.get(key)); + } + } + return props; + } + + class AspectImpl extends AbstractDecorator { + private final Class m_aspectInterface; + private final String m_autoConfig; + + public AspectImpl(Class aspectInterface, String autoConfig) { + this.m_aspectInterface = aspectInterface; + this.m_autoConfig = autoConfig; + } + + /** + * Creates an aspect implementation component for a new original service. + * @param param First entry contains the ref to the original service + */ + @SuppressWarnings("unchecked") + @Override + public Component createService(Object[] params) { + // Get the new original service reference. + ServiceReference originalServiceRef = (ServiceReference) params[0]; + List dependencies = m_component.getDependencies(); + // Remove our internal dependency, replace it with one that points to the specific service that just was passed in. + dependencies.remove(0); + Hashtable serviceProperties = getAspectServiceProperties(originalServiceRef); + String[] serviceInterfaces = getServiceInterfaces(); + + ServiceDependency aspectDependency = (ServiceDependencyImpl) m_manager.createServiceDependency() + .setService(m_aspectInterface, createAspectFilter(originalServiceRef)) + .setRequired(true) + .setPropagate(new AspectPropagateCallback(originalServiceRef), "propagateAspectPropertyChange") + .setCallbacks(m_dependencyCallbackInstance, m_add, m_change, m_remove, m_swap); + + //aspectDependency.setDebug("aspect " + m_ranking); + + if (m_autoConfig != null) { + aspectDependency.setAutoConfig(m_autoConfig); + } else if (m_add == null && m_change == null && m_remove == null && m_swap == null) { + // Since we have set callbacks, we must reactivate setAutoConfig because user has not specified any callbacks. + aspectDependency.setAutoConfig(true); + } + + Component service = m_manager.createComponent() + .setInterface(serviceInterfaces, serviceProperties) + .setImplementation(m_serviceImpl) + .setFactory(m_factory, m_factoryCreateMethod) // if not set, no effect + .setComposition(m_compositionInstance, m_compositionMethod) // if not set, no effect + .setCallbacks(m_callbackObject, m_init, m_start, m_stop, m_destroy) // if not set, no effect + .setScope(m_scope) + .add(aspectDependency); + + //service.setDebug("aspectimpl-" + m_ranking); + + configureAutoConfigState(service, m_component); + copyDependencies(dependencies, service); + m_stateListeners.forEach(service::add); + return service; + } + + private String[] getServiceInterfaces() { + List serviceNames = new ArrayList<>(); + // Of course, we provide the aspect interface. + serviceNames.add(m_aspectInterface.getName()); + // But also append additional aspect implementation interfaces. + if (m_serviceInterfaces != null) { + for (int i = 0; i < m_serviceInterfaces.length; i ++) { + if (!m_serviceInterfaces[i].equals(m_aspectInterface.getName())) { + serviceNames.add(m_serviceInterfaces[i]); + } + } + } + return serviceNames.toArray(new String[serviceNames.size()]); + } + + private String createAspectFilter(ServiceReference ref) { + Long sid = (Long) ref.getProperty(Constants.SERVICE_ID); + return "(&(|(!(" + Constants.SERVICE_RANKING + "=*))(" + Constants.SERVICE_RANKING + "<=" + (m_ranking - 1) + "))(|(" + Constants.SERVICE_ID + "=" + sid + ")(" + DependencyManager.ASPECT + "=" + sid + ")))"; + } + + public String toString() { + return "Aspect for " + m_aspectInterface.getName(); + } + } + + class AspectPropagateCallback { + private final ServiceReference m_originalServiceRef; + + AspectPropagateCallback(ServiceReference originalServiceRef) { + m_originalServiceRef = originalServiceRef; + } + + Hashtable propagateAspectPropertyChange(ServiceReference ref) { + // ignore ref, which might come from a lower ranked aspect only apply original service ref. + Hashtable props = new Hashtable<>(); + String[] keys = m_originalServiceRef.getPropertyKeys(); + for (int i = 0; i < keys.length; i++) { + String key = keys[i]; + if (ServiceUtil.NOT_PROPAGATABLE_SERVICE_PROPERTIES.contains(key)) { + // do not copy this key, which is not propagatable. + } + else { + props.put(key, m_originalServiceRef.getProperty(key)); + } + } + // finally add our aspect property + props.put(DependencyManager.ASPECT, m_originalServiceRef.getProperty(Constants.SERVICE_ID)); + // and the ranking + props.put(Constants.SERVICE_RANKING, Integer.valueOf(m_ranking)); + return props; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/BundleAdapterImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/BundleAdapterImpl.java new file mode 100644 index 00000000000..d7c8a7b8972 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/BundleAdapterImpl.java @@ -0,0 +1,132 @@ +/* + * 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.dm.impl; + +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.List; + +import org.apache.felix.dm.BundleComponent; +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.context.DependencyContext; +import org.osgi.framework.Bundle; + +/** + * Bundle Adapter Service implementation. This class extends the FilterService in order to catch + * some Service methods for configuring actual adapter service implementation. + * + * @author Felix Project Team + */ +public class BundleAdapterImpl extends FilterComponent implements BundleComponent +{ + private volatile boolean m_propagate; + private volatile int m_bundleStateMask; + private volatile String m_bundleFilter; + public volatile Object m_cbInstance; + public volatile String m_add; + public volatile String m_change; + public volatile String m_remove; + + /** + * Creates a new Bundle Adapter Service implementation. + */ + public BundleAdapterImpl(DependencyManager dm) + { + super(dm.createComponent()); // This service will be filtered by our super class, allowing us to take control. + } + + public BundleComponent setBundleFilter(int bundleStateMask, String bundleFilter) { + m_bundleStateMask = bundleStateMask; + m_bundleFilter = bundleFilter; + return this; + } + + public BundleComponent setBundleCallbacks(String add, String change, String remove) { + m_add = add; + m_change = change; + m_remove = remove; + return this; + } + + public BundleComponent setBundleCallbackInstance(Object callbackInstance) { + m_cbInstance = callbackInstance; + return this; + } + + public BundleComponent setPropagate(boolean propagate) { + m_propagate = propagate; + return this; + } + + @Override + protected void startInitial() { + DependencyManager dm = getDependencyManager(); + m_component + .setImplementation(new BundleAdapterDecorator()) + .add(dm.createBundleDependency() + .setFilter(m_bundleFilter) + .setStateMask(m_bundleStateMask) + .setCallbacks("added", "removed")) + .setCallbacks("init", null, "stop", null); + } + + public class BundleAdapterDecorator extends AbstractDecorator { + + @SuppressWarnings("unchecked") + public Component createService(Object[] properties) { + Bundle bundle = (Bundle) properties[0]; + Hashtable props = new Hashtable<>(); + if (m_serviceProperties != null) { + Enumeration e = m_serviceProperties.keys(); + while (e.hasMoreElements()) { + String key = e.nextElement(); + props.put(key, m_serviceProperties.get(key)); + } + } + List dependencies = m_component.getDependencies(); + // the first dependency is always the dependency on the bundle, which + // will be replaced with a more specific dependency below + dependencies.remove(0); + Component service = m_manager.createComponent() + .setInterface(m_serviceInterfaces, props) + .setImplementation(m_serviceImpl) + .setFactory(m_factory, m_factoryCreateMethod) // if not set, no effect + .setComposition(m_compositionInstance, m_compositionMethod) // if not set, no effect + .setCallbacks(m_callbackObject, m_init, m_start, m_stop, m_destroy) // if not set, no effect + .setScope(m_scope) + .add(m_manager.createBundleDependency() + .setBundle(bundle) + .setStateMask(m_bundleStateMask) + .setPropagate(m_propagate) + .setCallbacks(m_cbInstance, m_add, m_change, m_remove) // if no callbacks, autoconfig is enabled + .setRequired(true)); + + copyDependencies(dependencies, service); + + for (ComponentStateListener stateListener : m_stateListeners) { + service.add(stateListener); + } + configureAutoConfigState(service, m_component); + return service; + } + + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/BundleDependencyImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/BundleDependencyImpl.java new file mode 100644 index 00000000000..c315dc0e668 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/BundleDependencyImpl.java @@ -0,0 +1,300 @@ +/* + * 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.dm.impl; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; +import java.util.Dictionary; + +import org.apache.felix.dm.BundleDependency; +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentDependencyDeclaration; +import org.apache.felix.dm.context.AbstractDependency; +import org.apache.felix.dm.context.DependencyContext; +import org.apache.felix.dm.context.Event; +import org.apache.felix.dm.context.EventType; +import org.apache.felix.dm.tracker.BundleTracker; +import org.apache.felix.dm.tracker.BundleTrackerCustomizer; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; + +/** + * @author Felix Project Team + */ +public class BundleDependencyImpl extends AbstractDependency implements BundleDependency, BundleTrackerCustomizer, ComponentDependencyDeclaration { + private BundleTracker m_tracker; + private volatile int m_stateMask = Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE; + private Bundle m_bundleInstance; + private volatile Filter m_filter; + private volatile long m_bundleId = -1; + private Object m_nullObject; + private volatile boolean m_propagate; + private volatile Object m_propagateCallbackInstance; + private volatile String m_propagateCallbackMethod; + + public BundleDependencyImpl() { + } + + public BundleDependencyImpl(BundleDependencyImpl prototype) { + super(prototype); + m_stateMask = prototype.m_stateMask; + m_nullObject = prototype.m_nullObject; + m_bundleInstance = prototype.m_bundleInstance; + m_filter = prototype.m_filter; + m_bundleId = prototype.m_bundleId; + m_propagate = prototype.m_propagate; + m_propagateCallbackInstance = prototype.m_propagateCallbackInstance; + m_propagateCallbackMethod = prototype.m_propagateCallbackMethod; + } + + @Override + public DependencyContext createCopy() { + return new BundleDependencyImpl(this); + } + + @Override + public void start() { + m_tracker = new BundleTracker(m_component.getBundleContext(), m_stateMask, this); + m_tracker.open(); + super.start(); + } + + @Override + public void stop() { + m_tracker.close(); + m_tracker = null; + super.stop(); + } + + @Override + public String getName() { + StringBuilder sb = new StringBuilder(); + getSimpleName(sb); + if (m_filter != null) { + sb.append(" "); + sb.append(m_filter.toString()); + } + if (m_bundleId != -1) { + sb.append("{bundle.id=" + m_bundleId + "}"); + } + return sb.toString(); + } + + @Override + public String getSimpleName() { + // Return the state mask, but don't include the filter or bundle id. + StringBuilder sb = new StringBuilder(); + if ((m_stateMask & Bundle.ACTIVE) != 0) { + sb.append("active"); + } + if ((m_stateMask & Bundle.INSTALLED) != 0) { + if (sb.length() > 0) { + sb.append(" "); + } + sb.append("installed"); + } + if ((m_stateMask & Bundle.RESOLVED) != 0) { + if (sb.length() > 0) { + sb.append(" "); + } + sb.append("resolved"); + } + return sb.toString(); + } + + @Override + public String getFilter() { + if (m_filter != null || m_bundleId != -1) { + StringBuilder sb = new StringBuilder(); + if (m_filter != null) { + sb.append(m_filter.toString()); + } + if (m_bundleId != -1) { + sb.append("{bundle.id=" + m_bundleId + "}"); + } + return sb.toString(); + } + return null; + } + + @Override + public String getType() { + return "bundle"; + } + + public Object addingBundle(Bundle bundle, BundleEvent event) { + // if we don't like a bundle, we could reject it here by returning null + long bundleId = bundle.getBundleId(); + if (m_bundleId >= 0 && m_bundleId != bundleId) { + return null; + } + Filter filter = m_filter; + if (filter != null) { + Dictionary headers = bundle.getHeaders(); + if (!m_filter.match(headers)) { + return null; + } + } + return bundle; + } + + public void addedBundle(Bundle bundle, BundleEvent event, Object object) { + m_component.handleEvent(this, EventType.ADDED, new BundleEventImpl(bundle, event)); + } + + public void modifiedBundle(Bundle bundle, BundleEvent event, Object object) { + m_component.handleEvent(this, EventType.CHANGED, new BundleEventImpl(bundle, event)); + } + + public void removedBundle(Bundle bundle, BundleEvent event, Object object) { + m_component.handleEvent(this, EventType.REMOVED, new BundleEventImpl(bundle, event)); + } + + @Override + public void invokeCallback(EventType type, Event ... e) { + switch (type) { + case ADDED: + if (m_add != null) { + invoke(m_add, e[0]); + } + break; + case CHANGED: + if (m_change != null) { + invoke (m_change, e[0]); + } + break; + case REMOVED: + if (m_remove != null) { + invoke (m_remove, e[0]); + } + break; + default: + break; + } + } + + private void invoke(String method, Event e) { + BundleEventImpl be = (BundleEventImpl) e; + m_component.invokeCallbackMethod(getInstances(), method, + new Class[][] {{Bundle.class}, {Object.class}, {Component.class, Bundle.class}, {}}, + new Object[][] {{be.getBundle()}, {be.getBundle()}, {m_component, be.getBundle()}, {}}); + } + + public BundleDependency setBundle(Bundle bundle) { + m_bundleId = bundle.getBundleId(); + return this; + } + + public BundleDependency setFilter(String filter) throws IllegalArgumentException { + if (filter != null) { + try { + m_filter = FrameworkUtil.createFilter(filter); + } + catch (InvalidSyntaxException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + return this; + } + + public BundleDependency setStateMask(int mask) { + m_stateMask = mask; + return this; + } + + @Override + public Class getAutoConfigType() { + return Bundle.class; + } + + @SuppressWarnings("unchecked") + @Override + public Dictionary getProperties() { + Event event = getService(); + if (event != null) { + Bundle bundle = (Bundle) event.getEvent(); + if (m_propagateCallbackInstance != null && m_propagateCallbackMethod != null) { + try { + CallbackTypeDef callbackInfo = new CallbackTypeDef(Bundle.class, bundle); + return (Dictionary) InvocationUtil.invokeCallbackMethod(m_propagateCallbackInstance, m_propagateCallbackMethod, callbackInfo.m_sigs, callbackInfo.m_args); + } + catch (InvocationTargetException e) { + m_component.getLogger().warn("Exception while invoking callback method", e.getCause()); + } + catch (Throwable e) { + m_component.getLogger().warn("Exception while trying to invoke callback method", e); + } + throw new IllegalStateException("Could not invoke callback"); + } + else { + return ServiceUtil.toR6Dictionary(bundle.getHeaders()); + } + } + else { + throw new IllegalStateException("cannot find bundle"); + } + } + + @Override + public Object getDefaultService(boolean nullObject) { + Object service = null; + if (isAutoConfig()) { + // TODO does it make sense to add support for custom bundle impls? + // service = getDefaultImplementation(); + if (service == null && nullObject) { + service = getNullObject(); + } + } + return service; + } + + private Bundle getNullObject() { + if (m_nullObject == null) { + try { + m_nullObject = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[] { Bundle.class }, new DefaultNullObject()); + } + catch (Throwable e) { + m_component.getLogger().err("Could not create null object for Bundle.", e); + } + } + return (Bundle) m_nullObject; + } + + private void getSimpleName(StringBuilder sb) { + if ((m_stateMask & Bundle.ACTIVE) != 0) { + sb.append("active"); + } + if ((m_stateMask & Bundle.INSTALLED) != 0) { + if (sb.length() > 0) { + sb.append(" "); + } + sb.append("installed"); + } + if ((m_stateMask & Bundle.RESOLVED) != 0) { + if (sb.length() > 0) { + sb.append(" "); + } + sb.append("resolved"); + } + } +} + diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/BundleEventImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/BundleEventImpl.java new file mode 100644 index 00000000000..fc38a655cba --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/BundleEventImpl.java @@ -0,0 +1,70 @@ +/* + * 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.dm.impl; + +import java.util.Dictionary; + +import org.apache.felix.dm.context.Event; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleEvent; + +/** + * @author Felix Project Team + */ +public class BundleEventImpl extends Event { + final BundleEvent m_event; + + public BundleEventImpl(Bundle bundle, BundleEvent event) { + super(bundle); + m_event = event; + } + + public Bundle getBundle() { + return getEvent(); + } + + @SuppressWarnings("unchecked") + @Override + public Dictionary getProperties() { + return ServiceUtil.toR6Dictionary(getBundle().getHeaders()); + + } + + public BundleEvent getBundleEvent() { + return m_event; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof BundleEventImpl) { + return getBundle().getBundleId() == ((BundleEventImpl) obj).getBundle().getBundleId(); + } + return false; + } + + @Override + public int hashCode() { + return getBundle().hashCode(); + } + + @Override + public int compareTo(Event b) { + return Long.compare(getBundle().getBundleId(), ((BundleEventImpl) b).getBundle().getBundleId()); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/CallbackTypeDef.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/CallbackTypeDef.java new file mode 100644 index 00000000000..597fcb4d95b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/CallbackTypeDef.java @@ -0,0 +1,39 @@ +/* + * 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.dm.impl; + +/** + * Small wrapper to hold the various signatures and arguments for callback methods. + */ +public class CallbackTypeDef { + + public final Class[][] m_sigs; + public final Object[][] m_args; + + public CallbackTypeDef(Class sig, Object arg) { + this(new Class[][] { { sig } }, new Object[][] { { arg } }); + } + + public CallbackTypeDef(Class[][] sigs, Object[][] args) { + m_sigs = sigs; + m_args = args; + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ComponentImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ComponentImpl.java new file mode 100644 index 00000000000..d6994744fe5 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ComponentImpl.java @@ -0,0 +1,1992 @@ +/* + * 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.dm.impl; + +import static org.apache.felix.dm.ComponentState.INACTIVE; +import static org.apache.felix.dm.ComponentState.INSTANTIATED_AND_WAITING_FOR_REQUIRED; +import static org.apache.felix.dm.ComponentState.STARTED; +import static org.apache.felix.dm.ComponentState.STARTING; +import static org.apache.felix.dm.ComponentState.STOPPED; +import static org.apache.felix.dm.ComponentState.STOPPING; +import static org.apache.felix.dm.ComponentState.TRACKING_OPTIONAL; +import static org.apache.felix.dm.ComponentState.WAITING_FOR_REQUIRED; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentDeclaration; +import org.apache.felix.dm.ComponentDependencyDeclaration; +import org.apache.felix.dm.ComponentExecutorFactory; +import org.apache.felix.dm.ComponentState; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.Logger; +import org.apache.felix.dm.context.ComponentContext; +import org.apache.felix.dm.context.DependencyContext; +import org.apache.felix.dm.context.Event; +import org.apache.felix.dm.context.EventType; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.PrototypeServiceFactory; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.log.LogService; + +/** + * Dependency Manager Component implementation. + * + * @author Felix Project Team + */ +public class ComponentImpl implements Component, ComponentContext, ComponentDeclaration { + /** + * NullObject ServiceRegistration that is injected in components that don't provide any services. + */ + private static final ServiceRegistration NULL_REGISTRATION = (ServiceRegistration) Proxy + .newProxyInstance(ComponentImpl.class.getClassLoader(), + new Class[] { ServiceRegistration.class }, + new DefaultNullObject()); + + /** + * Constant Used to get empty constructor by reflection. + */ + private static final CallbackTypeDef VOID = new CallbackTypeDef(new Class[][] {{}}, new Object[][] {{}}); + + /** + * Default Component Executor, which is by default single threaded. The first thread which schedules a task + * is the master thread and will execute all tasks that are scheduled by other threads at the time the master + * thread is executing. Internal tasks scheduled by the master thread are executed immediately (inline execution). + * + * If a ComponentExecutorFactory is provided in the OSGI registry, then this executor will be replaced by the + * executor returned by the ComponentExecutorFactory (however, the same semantic of the default executor is used: + * all tasks are serially executed). + * + * @see @link {@link ComponentExecutorFactory} + */ + private volatile Executor m_executor = new SerialExecutor(new Logger(null)); + + /** + * The current state of the component state machine. + */ + private ComponentState m_state = ComponentState.INACTIVE; + + /** + * Indicates that the handleChange method is currently being executed. + */ + private boolean m_handlingChange; + + /** + * List of dependencies. We use a COW list in order to avoid ConcurrentModificationException while iterating on the + * list and while a component synchronously add more dependencies from one of its callback method. + */ + private final CopyOnWriteArrayList m_dependencies = new CopyOnWriteArrayList<>(); + + /** + * List of Component state listeners. We use a COW list in order to avoid ConcurrentModificationException while iterating on the + * list and while a component synchronously adds more listeners from one of its callback method. + */ + private final List m_listeners = new CopyOnWriteArrayList<>(); + + /** + * Is the component active ? + */ + private boolean m_isStarted; + + /** + * The Component logger. + */ + private final Logger m_logger; + + /** + * The Component bundle context. + */ + private final BundleContext m_context; + + /** + * The DependencyManager object that has created this component. + */ + private final DependencyManager m_manager; + + /** + * The object used to create the component. Can be a class name, or the component implementation instance. + */ + private volatile Object m_componentDefinition; + + /** + * The component instance. + */ + private Object m_componentInstance; + + /** + * The service(s) provided by this component. Can be a String, or a String array. + */ + private volatile Object m_serviceName; + + /** + * The service properties, if this component is providing a service. + */ + private volatile Dictionary m_serviceProperties; + + /** + * The component service registration. Can be a NullObject in case the component does not provide a service. + */ + private volatile ServiceRegistration m_registration; + + /** + * The component name as it must be returned by getName. + */ + private volatile String m_componentName; + + /** + * Map of auto configured fields (BundleContext, ServiceRegistration, DependencyManager, or Component). + * By default, all fields mentioned above are auto configured (injected in class fields having the same type). + */ + private final Map, Boolean> m_autoConfig = new ConcurrentHashMap<>(); + + /** + * Map of auto configured instance fields that will be used when injected auto configured fields. + * @see #m_autoConfig + */ + private final Map, String> m_autoConfigInstance = new ConcurrentHashMap<>(); + + /** + * Data structure used to record the elapsed time used by component lifecycle callbacks. + * Key = callback name ("init", "start", "stop", "destroy"). + * Value = elapsed time in nanos. + */ + private final Map m_stopwatch = new ConcurrentHashMap<>(); + + /** + * Unique component id. + */ + private final long m_id; + + /** + * Unique ID generator. + */ + private final static AtomicLong m_idGenerator = new AtomicLong(); + + /** + * Holds all the services of a given dependency context. Caution: the last entry in the skiplist is the highest + * ranked service. + */ + private final Map> m_dependencyEvents = new HashMap<>(); + + /** + * Flag used to check if this component has been added in a DependencyManager object. + */ + private final AtomicBoolean m_active = new AtomicBoolean(false); + + /** + * Init lifecycle callback. From that method, component are expected to add more extra dependencies. + * When this callback is invoked, all required dependencies have been injected. + */ + private volatile String m_callbackInit; + + /** + * Start lifecycle callback. When this method is called, all required + all extra required dependencies defined in the + * init callback have been injected. The component may then perform its initialization. + */ + private volatile String m_callbackStart; + + /** + * Stop callback. When this method is called, the component has been unregistered (if it provides any services), + * and all optional dependencies have been unbound. + */ + private volatile String m_callbackStop; + + /** + * Destroy callback. When this method is called, all required dependencies defined in the init method have been unbound. + * After this method is called, then all required dependencies defined in the Activator will be unbound. + */ + private volatile String m_callbackDestroy; + + /** + * By default, the init/start/stop/destroy callbacks are invoked on the component instance(s). + * But you can specify a separate callback instance. + */ + private volatile Object m_callbackInstance; + + /** + * Component Factory instance object, that can be used to instantiate the component instance. + */ + private volatile Object m_instanceFactory; + + /** + * Name of the Factory method to call. + */ + private volatile String m_instanceFactoryCreateMethod; + + /** + * Composition Manager that can be used to create a graph of objects that are used to implement the component. + */ + private volatile Object m_compositionManager; + + /** + * Name of the method used to invoke in order to get the list of component instance objects. + */ + private volatile String m_compositionManagerGetMethod; + + /** + * The composition manager instance object, if specified. + */ + private volatile Object m_compositionManagerInstance; + + /** + * The Component bundle. + */ + private final Bundle m_bundle; + + /** + * Cache of callback invocation used to avoid calling the same callback twice. + * This situation may sometimes happen when the state machine triggers a lifecycle callback ("bind" call), and + * when the bind method registers a service which is tracked by another optional component dependency. + * + * @see org.apache.felix.dm.itest.api.FELIX4913_OptionalCallbackInvokedTwiceTest which reproduces the use case. + */ + private final Map m_invokeCallbackCache = new IdentityHashMap<>(); + + /** + * Flag used to check if the start callback has been invoked. + * We use this flag to ensure that we only inject optional dependencies after the start callback has been called. + */ + private boolean m_startCalled; + + /** + * Used to track the last state we delivered to any state listeners. + */ + private ComponentState m_lastStateDeliveredToListeners = ComponentState.INACTIVE; + + /** + * Component service scope. + */ + private volatile ServiceScope m_scope = Component.ServiceScope.SINGLETON; + + /** + * Indicates if injection of methods or class fields is disabled for this component. + */ + private volatile boolean m_injectionDisabled; + + /** + * Prototype instance used when a component scope is not a singleton when when no init callback is defined. + */ + private final static Object PROTOTYPE_INSTANCE = new Object(); + + /** + * Default component declaration implementation. + */ + static class SCDImpl implements ComponentDependencyDeclaration { + private final String m_name; + private final int m_state; + private final String m_type; + + public SCDImpl(String name, int state, String type) { + m_name = name; + m_state = state; + m_type = type; + } + + public String getName() { + return m_name; + } + + public String getSimpleName() { + return m_name; + } + + public String getFilter() { + return null; + } + + public int getState() { + return m_state; + } + + public String getType() { + return m_type; + } + } + + class FactoryBase { + final Map m_clones = new ConcurrentHashMap<>(); + + public Object getService(Bundle bundle, ServiceRegistration registration) { + // create a clone of the prototype for the consumer + ComponentImpl clone = (ComponentImpl) m_manager.createComponent() + .setImplementation(m_componentDefinition) + .setFactory(m_instanceFactory, m_instanceFactoryCreateMethod) // if not set, no effect + .setComposition(m_compositionManager, m_compositionManagerGetMethod) // if not set, no effect + .setCallbacks(m_callbackInstance, m_callbackInit, m_callbackStart, m_callbackStop, m_callbackDestroy); // if not set, no effect + clone.setThreadPool(m_executor); + clone.setAutoConfig(ServiceRegistration.class, false); // don't inject the ServiceRegistration for the moment + configureAutoConfigState(clone, ComponentImpl.this); + // copy prototype dependencies, except instance bound dependencies + getDependencies().stream().filter(dc -> ! dc.isInstanceBound()).map(dc -> dc.createCopy()).forEach(clone::add); + clone.setCallbacks(m_callbackInit, m_callbackStart, m_callbackStop, m_callbackDestroy); + m_listeners.forEach(clone::add); + int ctorIndex = clone.instantiateComponent(createScopedComponentConstructorArgs(bundle, registration)); + // ctorIndex == -1 means we have used a factory to create the component + // if ctorIndex == 0 means we have used the first constructor (the one used to inject service registration and bundles + if (ctorIndex != 0) { + // we don't have injected the bundle and service registration in the constructor, so inject them in the class fields + autoConfigField(clone, ServiceRegistration.class, registration); + autoConfigField(clone, Bundle.class, bundle); + } + + clone.start(); + Object instance = clone.getInstance(); + if (instance == null) { + // Race condition: the prototype is probably deactivating. + throw new IllegalStateException("Can't create prototype instance"); + } + m_clones.put(instance, clone); + return instance; + } + + public void ungetService(Bundle bundle, ServiceRegistration registration, Object service) { + ComponentImpl clone = m_clones.remove(service); + if (clone != null) { + clone.stop(); + } + } + + private void autoConfigField(ComponentImpl clone, Class type, Object autoConfig) { + Boolean doAutoConfig = m_autoConfig.get(type); + clone.setAutoConfig(type, doAutoConfig == null ? true : doAutoConfig); + clone.autoConfigureImplementation(type, autoConfig); + } + } + + class ComponentServiceFactory extends FactoryBase implements ServiceFactory { + } + + class ComponentPrototypeServiceFactory extends FactoryBase implements PrototypeServiceFactory { + } + + /** + * Constructor. Only used for tests. + */ + public ComponentImpl() { + this(null, null, new Logger(null)); + } + + /** + * Constructor + * @param context the component bundle context + * @param manager the manager used to create the component + * @param logger the logger to use + */ + public ComponentImpl(BundleContext context, DependencyManager manager, Logger logger) { + m_context = context; + m_bundle = context != null ? context.getBundle() : null; + m_manager = manager; + m_logger = logger; + m_autoConfig.put(BundleContext.class, Boolean.TRUE); + m_autoConfig.put(ServiceRegistration.class, Boolean.TRUE); + m_autoConfig.put(DependencyManager.class, Boolean.TRUE); + m_autoConfig.put(Component.class, Boolean.TRUE); + m_autoConfig.put(Bundle.class, Boolean.TRUE); + m_callbackInit = "init"; + m_callbackStart = "start"; + m_callbackStop = "stop"; + m_callbackDestroy = "destroy"; + m_id = m_idGenerator.getAndIncrement(); + } + + @Override + public T createConfigurationType(Class type, Dictionary config) { + Dictionary declaredServiceProperties = getDeclaredServiceProperties(); + return Configurable.create(type, config, declaredServiceProperties); + } + + @Override + public Executor getExecutor() { + return m_executor; + } + + @Override + public ComponentImpl setDebug(String debugKey) { + // Force debug level in our logger + m_logger.setEnabledLevel(LogService.LOG_DEBUG); + m_logger.setDebugKey(debugKey); + return this; + } + + @Override + public ComponentImpl add(final Dependency ... dependencies) { + getExecutor().execute(() -> { + List instanceBoundDeps = new ArrayList<>(); + for (Dependency d : dependencies) { + DependencyContext dc = (DependencyContext) d; + if (dc.getComponentContext() != null) { + m_logger.err("%s can't be added to %s (dependency already added to component %s).", dc, ComponentImpl.this, dc.getComponentContext()); + continue; + } + m_dependencyEvents.put(dc, new ConcurrentSkipListSet()); + m_dependencies.add(dc); + generateNameBasedOnServiceAndProperties(); + dc.setComponentContext(ComponentImpl.this); + if (!(m_state == ComponentState.INACTIVE)) { + dc.setInstanceBound(true); + instanceBoundDeps.add(dc); + } + } + startDependencies(instanceBoundDeps); + handleChange(); + }); + return this; + } + + @Override + public ComponentImpl remove(final Dependency d) { + getExecutor().execute(() -> { + DependencyContext dc = (DependencyContext) d; + // First remove this dependency from the dependency list + m_dependencies.remove(d); + generateNameBasedOnServiceAndProperties(); + // Now we can stop the dependency (our component won't be deactivated, it will only be unbound with + // the removed dependency). + if (!(m_state == ComponentState.INACTIVE)) { + dc.stop(); + } + // Finally, cleanup the dependency events. + m_dependencyEvents.remove(d); + handleChange(); + }); + return this; + } + + @Override + public void start() { + checkParamsConsistency(); + if (m_active.compareAndSet(false, true)) { + getExecutor().execute(() -> { + m_isStarted = true; + handleChange(); + }); + } + } + + @Override + public void stop() { + if (m_active.compareAndSet(true, false)) { + // try to be synchronous, even if a threadpool is used (best effort). + schedule(true /* bypass threadpool if possible */, () -> { + m_isStarted = false; + handleChange(); + }); + } + } + + @SuppressWarnings("unchecked") + @Override + public ComponentImpl setInterface(String serviceName, Dictionary properties) { + ensureNotActive(); + m_serviceName = serviceName; + m_serviceProperties = (Dictionary) properties; + generateNameBasedOnServiceAndProperties(); + return this; + } + + @SuppressWarnings("unchecked") + @Override + public ComponentImpl setInterface(String[] serviceName, Dictionary properties) { + ensureNotActive(); + m_serviceName = serviceName; + m_serviceProperties = (Dictionary) properties; + generateNameBasedOnServiceAndProperties(); + return this; + } + + @Override + public ComponentImpl setInterface(Class serviceName, Dictionary properties) { + return setInterface(serviceName.getName(), properties); + } + + @Override + public ComponentImpl setInterface(Class[] serviceName, Dictionary properties) { + String[] serviceNameStr = Stream.of(serviceName).map(clazz -> clazz.getName()).toArray(String[]::new); + return setInterface(serviceNameStr, properties); + } + + @Override + public void handleEvent(final DependencyContext dc, final EventType type, final Event... event) { + // since this method can be invoked by anyone from any thread, we need to + // pass on the event to a runnable that we execute using the component's + // executor. There is one corner case: if this is a REMOVE event, and if we are using a threadpool, + // then make a best effort to try invoking the component unbind callback synchronously (to make + // sure the unbound service is not stopped at the time we call unbind on our component + // which depends on the removing service). + // This is just a best effort, and the removed event will be handled asynchronosly if our + // queue is currently being run by another thread, or by the threadpool. + + boolean bypassThreadPoolIfPossible = (type == EventType.REMOVED); + schedule(bypassThreadPoolIfPossible, () -> { + try { + switch (type) { + case ADDED: + handleAdded(dc, event[0]); + break; + case CHANGED: + handleChanged(dc, event[0]); + break; + case REMOVED: + handleRemoved(dc, event[0]); + break; + case SWAPPED: + handleSwapped(dc, event[0], event[1]); + break; + } + } finally { + // Clear cache of component callbacks invocation, except if we are currently called from handleChange(). + // (See FELIX-4913). + clearInvokeCallbackCache(); + } + }); + } + + @Override + public Event getDependencyEvent(DependencyContext dc) { + ConcurrentSkipListSet events = m_dependencyEvents.get(dc); + return events.size() > 0 ? events.last() : null; + } + + @Override + public Set getDependencyEvents(DependencyContext dc) { + return m_dependencyEvents.get(dc); + } + + @Override + public ComponentImpl setAutoConfig(Class clazz, boolean autoConfig) { + m_autoConfig.put(clazz, Boolean.valueOf(autoConfig)); + return this; + } + + @Override + public ComponentImpl setAutoConfig(Class clazz, String instanceName) { + m_autoConfig.put(clazz, Boolean.valueOf(instanceName != null)); + m_autoConfigInstance.put(clazz, instanceName); + return this; + } + + @Override + public boolean getAutoConfig(Class clazz) { + Boolean result = (Boolean) m_autoConfig.get(clazz); + return (result != null && result.booleanValue()); + } + + @Override + public String getAutoConfigInstance(Class clazz) { + return (String) m_autoConfigInstance.get(clazz); + } + + @SuppressWarnings("unchecked") + public T getInstance() { + Object[] instances = getCompositionInstances(); + return instances.length == 0 ? null : (T) instances[0]; + } + + public Object[] getInstances() { + return getCompositionInstances(); + } + + public void invokeCallbackMethod(Object[] instances, String methodName, Class[][] signatures, Object[][] parameters) { + invokeCallbackMethod(instances, methodName, signatures, parameters, true); + } + + public void invokeCallbackMethod(Object[] instances, String methodName, Class[][] signatures, Object[][] parameters, boolean logIfNotFound) { + boolean callbackFound = false; + for (int i = 0; i < instances.length; i++) { + try { + InvocationUtil.invokeCallbackMethod(instances[i], methodName, signatures, parameters); + callbackFound |= true; + } + catch (NoSuchMethodException e) { + // if the method does not exist, ignore it + } + catch (InvocationTargetException e) { + // the method itself threw an exception, log that + m_logger.log(Logger.LOG_ERROR, "Invocation of '" + methodName + "' failed.", e.getCause()); + callbackFound |= true; + } + catch (Throwable e) { // IllegalArgumentException (wrong params passed to the method), or IllegalAccessException (method can't be accessed) + m_logger.log(Logger.LOG_ERROR, "Could not invoke '" + methodName + "'.", e); + } + } + + // If the callback is not found, we don't log if the method is on an AbstractDecorator. + // (Aspect or Adapter are not interested in user dependency callbacks) + if (logIfNotFound && ! callbackFound && ! (getInstance() instanceof AbstractDecorator)) { + m_logger.log(LogService.LOG_ERROR, "\"" + methodName + "\" callback not found on component instances " + Arrays.toString(instances)); + } + } + + public void invokeCallback(Object[] instances, String methodName, Class[][] signatures, Supplier[][] parameters, boolean logIfNotFound) { + boolean callbackFound = false; + for (int i = 0; i < instances.length; i++) { + try { + InvocationUtil.invokeCallbackMethod(instances[i], methodName, signatures, parameters); + callbackFound |= true; + } + catch (NoSuchMethodException e) { + // if the method does not exist, ignore it + } + catch (InvocationTargetException e) { + // the method itself threw an exception, log that + m_logger.log(Logger.LOG_ERROR, "Invocation of '" + methodName + "' failed.", e.getCause()); + callbackFound |= true; + } + catch (Throwable e) { + m_logger.log(Logger.LOG_ERROR, "Could not invoke '" + methodName + "'.", e); + } + } + + // If the callback is not found, we don't log if the method is on an AbstractDecorator. + // (Aspect or Adapter are not interested in user dependency callbacks) + if (logIfNotFound && ! callbackFound && ! (getInstance() instanceof AbstractDecorator)) { + m_logger.log(LogService.LOG_ERROR, "\"" + methodName + "\" callback not found on component instances " + Arrays.toString(instances)); + } + } + + @Override + public boolean isAvailable() { + return m_state == TRACKING_OPTIONAL; + } + + @Override + public boolean isActive() { + return m_active.get(); + } + + @Override + public ComponentImpl add(ComponentStateListener listener) { + getExecutor().execute(() -> { + m_listeners.add(listener); + switch (m_lastStateDeliveredToListeners) { + case STARTING: + // this new listener missed the starting cb + listener.changed(ComponentImpl.this, STARTING); + break; + case TRACKING_OPTIONAL: + // this new listener missed the starting/started cb + listener.changed(ComponentImpl.this, STARTING); + listener.changed(ComponentImpl.this, TRACKING_OPTIONAL); + break; + case STOPPING: + // this new listener missed the starting/started/stopping cb + listener.changed(ComponentImpl.this, STARTING); + listener.changed(ComponentImpl.this, TRACKING_OPTIONAL); + listener.changed(ComponentImpl.this, STOPPING); + break; + case STOPPED: + // no need to call missed listener callbacks + break; + default: + break; + } + }); + return this; + } + + @Override + public ComponentImpl remove(ComponentStateListener listener) { + m_listeners.remove(listener); + return this; + } + + @SuppressWarnings("unchecked") + @Override + public List getDependencies() { + return (List) m_dependencies.clone(); + } + + @Override + public ComponentImpl setImplementation(Object implementation) { + m_componentDefinition = implementation; + return this; + } + + @SuppressWarnings("rawtypes") + @Override + public ServiceRegistration getServiceRegistration() { + return m_registration; + } + + @SuppressWarnings("unchecked") + @Override + public Dictionary getServiceProperties() { + return (Dictionary) calculateServiceProperties(); + } + + public Dictionary getDeclaredServiceProperties() { + return (m_serviceProperties != null) ? ServiceUtil.toR6Dictionary(m_serviceProperties) : ServiceUtil.toR6Dictionary(ServiceUtil.EMPTY_PROPERTIES); + +// if (m_serviceProperties != null) { +// // Applied patch from FELIX-4304 +// Hashtable serviceProperties = new Hashtable<>(); +// addTo(serviceProperties, m_serviceProperties); +// return (Dictionary) serviceProperties; +// } +// return (Dictionary) ServiceUtil.EMPTY_PROPERTIES; + } + + @Override + @SuppressWarnings("unchecked") + public ComponentImpl setServiceProperties(final Dictionary serviceProperties) { + getExecutor().execute(() -> { + Dictionary properties = null; + m_serviceProperties = (Dictionary) serviceProperties; + generateNameBasedOnServiceAndProperties(); + if ((m_registration != null) && (m_serviceName != null)) { + properties = calculateServiceProperties(); + m_registration.setProperties(properties); + } + }); + return this; + } + + public ComponentImpl setCallbacks(String init, String start, String stop, String destroy) { + ensureNotActive(); + m_callbackInit = init; + m_callbackStart = start; + m_callbackStop = stop; + m_callbackDestroy = destroy; + return this; + } + + public ComponentImpl setCallbacks(Object instance, String init, String start, String stop, String destroy) { + ensureNotActive(); + m_callbackInstance = instance; + m_callbackInit = init; + m_callbackStart = start; + m_callbackStop = stop; + m_callbackDestroy = destroy; + return this; + } + + @Override + public ComponentImpl setFactory(Object factory, String createMethod) { + ensureNotActive(); + m_instanceFactory = factory; + m_instanceFactoryCreateMethod = createMethod; + return this; + } + + @Override + public ComponentImpl setFactory(String createMethod) { + return setFactory(null, createMethod); + } + + @Override + public ComponentImpl setComposition(Object instance, String getMethod) { + ensureNotActive(); + m_compositionManager = instance; + m_compositionManagerGetMethod = getMethod; + return this; + } + + @Override + public ComponentImpl setComposition(String getMethod) { + return setComposition(null, getMethod); + } + + @Override + public DependencyManager getDependencyManager() { + return m_manager; + } + + @Override + public boolean injectionDisabled() { + return m_injectionDisabled; + } + + public ComponentDependencyDeclaration[] getComponentDependencies() { + List deps = getDependencies(); + if (deps != null) { + ComponentDependencyDeclaration[] result = new ComponentDependencyDeclaration[deps.size()]; + for (int i = 0; i < result.length; i++) { + DependencyContext dep = (DependencyContext) deps.get(i); + if (dep instanceof ComponentDependencyDeclaration) { + result[i] = (ComponentDependencyDeclaration) dep; + } + else { + result[i] = new SCDImpl(dep.toString(), (dep.isAvailable() ? 1 : 0) + (dep.isRequired() ? 2 : 0), dep.getClass().getName()); + } + } + return result; + } + return null; + } + + public String getName() { + StringBuilder sb = new StringBuilder(); + Object serviceName = m_serviceName; + + if( (!(serviceName instanceof String[])) && (!(serviceName instanceof String))) { + // The component does not provide a service, use the component definition as the name. + Object componentDefinition = m_componentDefinition; + if (componentDefinition != null) { + sb.append(toString(componentDefinition)); + } else { + // No component definition means we are using a factory. If the component instance is available use it as the component name, + // alse use teh factory object as the component name. + Object componentInstance = m_componentInstance; + if (componentInstance != null) { + sb.append(componentInstance.getClass().getName()); + } else { + // Check if a factory is set. + Object instanceFactory = m_instanceFactory; + if (instanceFactory != null) { + sb.append(toString(instanceFactory)); + } else { + sb.append(super.toString()); + } + } + } + m_componentName = sb.toString(); + } + return m_componentName; + } + + @Override + public ComponentImpl setScope(ServiceScope scope) { + m_scope = scope; + return this; + } + + ServiceScope getScope() { + return m_scope; + } + + private void checkParamsConsistency() { + switch (m_scope) { + case PROTOTYPE: + case BUNDLE: + if (m_serviceName == null) { + throw new IllegalStateException("No service interface specified for scoped service"); + } + + if ((!(m_componentDefinition instanceof Class)) && m_instanceFactoryCreateMethod == null) { + throw new IllegalStateException( + "The component instance must be created using either a class or a factory object when scope is not singleton."); + } + + } + + } + + private void generateNameBasedOnServiceAndProperties() { + StringBuilder sb = new StringBuilder(); + Object serviceName = m_serviceName; + + // If the component provides service(s), return the services as the component name. + if (serviceName instanceof String[]) { + String[] names = (String[]) serviceName; + for (int i = 0; i < names.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(names[i]); + } + ServiceUtil.appendProperties(sb, calculateServiceProperties()); + } else if(serviceName instanceof String) { + sb.append(serviceName.toString()); + ServiceUtil.appendProperties(sb, calculateServiceProperties()); + } + m_componentName = sb.toString(); + } + + @Override + public BundleContext getBundleContext() { + return m_context; + } + + @Override + public Bundle getBundle() { + return m_bundle; + } + + public long getId() { + return m_id; + } + + public String getClassName() { + Object serviceInstance = m_componentInstance; + if (serviceInstance != null) { + return serviceInstance.getClass().getName(); + } + + Object implementation = m_componentDefinition; + if (implementation != null) { + if (implementation instanceof Class) { + return ((Class) implementation).getName(); + } + return implementation.getClass().getName(); + } + + Object instanceFactory = m_instanceFactory; + if (instanceFactory != null) { + return toString(instanceFactory); + } else { + // unexpected. + return ComponentImpl.class.getName(); + } + } + + public String[] getServices() { + if (m_serviceName instanceof String[]) { + return (String[]) m_serviceName; + } else if (m_serviceName instanceof String) { + return new String[] { (String) m_serviceName }; + } else { + return null; + } + } + + public int getState() { + return (isAvailable() ? ComponentDeclaration.STATE_REGISTERED : ComponentDeclaration.STATE_UNREGISTERED); + } + + public void ensureNotActive() { + if (m_active.get()) { + throw new IllegalStateException("Can't modify an already started component."); + } + } + + public ComponentDeclaration getComponentDeclaration() { + return this; + } + + @Override + public String toString() { + if (m_logger.getDebugKey() != null) { + return m_logger.getDebugKey(); + } + return getClassName(); + } + + @Override + public void setThreadPool(Executor threadPool) { + ensureNotActive(); + m_executor = new DispatchExecutor(threadPool, m_logger); + } + + @Override + public Logger getLogger() { + return m_logger; + } + + @Override + public Map getCallbacksTime() { + return m_stopwatch; + } + + @Override + public ComponentContext instantiateComponent() { + CallbackTypeDef ctorArgs = null; + + switch (m_scope) { + case SINGLETON: + // When a component scope is singleton, we'll use the following constructor or factory "create" method signatures: + ctorArgs = VOID; + break; + + case PROTOTYPE: + case BUNDLE: + if (m_injectionDisabled) { + m_componentInstance = PROTOTYPE_INSTANCE; + return this; + } else { + ctorArgs = createScopedComponentConstructorArgs(null, null); + } + break; + + default: + throw new IllegalStateException("Invalid scope: " + m_scope); + } + + // Create the factory "create" method signatures types + instantiateComponent(ctorArgs); + return this; + } + + // ---------------------- Package/Private methods --------------------------- + + /** + * Creates a constructor argument descritor for a scoped component (having scope=BUNDLE|PROTOTYPE) + */ + private CallbackTypeDef createScopedComponentConstructorArgs(Bundle b, ServiceRegistration sr) { + return new CallbackTypeDef( + new Class[][] {{Bundle.class, ServiceRegistration.class}, {Bundle.class, ServiceRegistration.class, BundleContext.class}, {}}, + new Object[][] {{b,sr}, {b,sr, m_context}, {}}); + } + + /** + * Creates the component instance. + * @param ctorArgs the constructor argument signatures + * @return the index of the constructor argument signatures that was used when creating the component. -1 means the component has been created using a factory "create" method. + */ + private int instantiateComponent(CallbackTypeDef ctorArgs) { + m_logger.debug("instantiating component."); + int ctorArgsUsed = -1; + + if (m_componentInstance == null) { + if (m_componentDefinition instanceof Class) { + try { + InvocationUtil.ComponentInstance ci = InvocationUtil.createInstance((Class) m_componentDefinition, ctorArgs); + m_componentInstance = ci.m_instance; + ctorArgsUsed = ci.m_ctorIndex; + } catch (Exception e) { + m_logger.log(Logger.LOG_ERROR, "Could not instantiate class " + m_componentDefinition, e); + } + } else { + if (m_instanceFactoryCreateMethod != null) { + Object factory = null; + if (m_instanceFactory != null) { + if (m_instanceFactory instanceof Class) { + try { + InvocationUtil.ComponentInstance ci = InvocationUtil.createInstance((Class) m_instanceFactory, VOID); + factory = ci.m_instance; + } catch (Exception e) { + m_logger.log(Logger.LOG_ERROR, + "Could not create factory instance of class " + m_instanceFactory + ".", e); + } + } else { + factory = m_instanceFactory; + } + } else { + // TODO review if we want to try to default to something if not specified + // for now the JavaDoc of setFactory(method) reflects the fact that we need + // to review it + } + if (factory == null) { + m_logger.log(Logger.LOG_ERROR, "Factory cannot be null."); + } else { + try { + CallbackTypeDef factoryArgs = new CallbackTypeDef( + new Class[][] {{}, {Component.class}}, + new Object[][] {{}, {this}}); + m_componentInstance = InvocationUtil.invokeMethod(factory, factory.getClass(), + m_instanceFactoryCreateMethod, factoryArgs.m_sigs, factoryArgs.m_args, false); + } catch (Exception e) { + m_logger.log(Logger.LOG_ERROR, "Could not create service instance using factory " + factory + + " method " + m_instanceFactoryCreateMethod + ".", e); + } + } + } + } + + if (m_componentInstance == null) { + m_componentInstance = m_componentDefinition; + } + + // configure the bundle context + autoConfigureImplementation(BundleContext.class, m_context); + autoConfigureImplementation(ServiceRegistration.class, NULL_REGISTRATION); + autoConfigureImplementation(DependencyManager.class, m_manager); + autoConfigureImplementation(Component.class, this); + } + return ctorArgsUsed; + } + + private String toString(Object implementation) { + if (implementation instanceof Class) { + return (((Class) implementation).getName()); + } else { + // If the implementation instance does not override "toString", just display + // the class name, else display the component using its toString method + try { + Method m = implementation.getClass().getMethod("toString", new Class[0]); + if (m.getDeclaringClass().equals(Object.class)) { + return implementation.getClass().getName(); + } else { + return implementation.toString(); + } + } catch (java.lang.NoSuchMethodException e) { + // Just display the class name + return implementation.getClass().getName(); + } + } + } + + /** + * Runs the state machine, to see if a change event has to trigger some component state transition. + */ + private void handleChange() { + if (isHandlingChange()) { + return; + } + m_logger.debug("handleChanged"); + handlingChange(true); + try { + ComponentState oldState; + ComponentState newState; + do { + oldState = m_state; + newState = calculateNewState(oldState); + m_logger.debug("%s -> %s", oldState, newState); + m_state = newState; + } while (performTransition(oldState, newState)); + } finally { + handlingChange(false); + clearInvokeCallbackCache(); + m_logger.debug("end handling change."); + } + } + + /** + * Based on the current state, calculate the new state. + */ + private ComponentState calculateNewState(ComponentState currentState) { + if (currentState == INACTIVE) { + if (m_isStarted) { + return WAITING_FOR_REQUIRED; + } + } + if (currentState == WAITING_FOR_REQUIRED) { + if (!m_isStarted) { + return INACTIVE; + } + if (allRequiredAvailable()) { + return INSTANTIATED_AND_WAITING_FOR_REQUIRED; + } + } + if (currentState == INSTANTIATED_AND_WAITING_FOR_REQUIRED) { + if (m_isStarted && allRequiredAvailable()) { + if (allInstanceBoundAvailable()) { + return TRACKING_OPTIONAL; + } + return currentState; + } + return WAITING_FOR_REQUIRED; + } + if (currentState == TRACKING_OPTIONAL) { + if (m_isStarted && allRequiredAvailable() && allInstanceBoundAvailable()) { + return currentState; + } + return INSTANTIATED_AND_WAITING_FOR_REQUIRED; + } + return currentState; + } + + /** + * Perform all the actions associated with state transitions. + * @returns true if a transition was performed. + **/ + private boolean performTransition(ComponentState oldState, ComponentState newState) { + if (oldState == ComponentState.INACTIVE && newState == ComponentState.WAITING_FOR_REQUIRED) { + initPrototype(); + startDependencies(m_dependencies); + notifyListeners(newState); + return true; + } + if (oldState == WAITING_FOR_REQUIRED && newState == INSTANTIATED_AND_WAITING_FOR_REQUIRED) { + instantiateComponent(); + invokeAutoConfigDependencies(); + invokeAddRequiredDependencies(); + invoke(m_callbackInit); + notifyListeners(newState); + return true; + } + if (oldState == INSTANTIATED_AND_WAITING_FOR_REQUIRED && newState == TRACKING_OPTIONAL) { + invokeAutoConfigInstanceBoundDependencies(); + invokeAddRequiredInstanceBoundDependencies(); + notifyListeners(STARTING); + invokeStart(); + notifyListeners(STARTED); + invokeAddOptionalDependencies(); + registerService(); + notifyListeners(newState); + return true; + } + if (oldState == TRACKING_OPTIONAL && newState == INSTANTIATED_AND_WAITING_FOR_REQUIRED) { + notifyListeners(STOPPING); + unregisterService(); + invokeRemoveOptionalDependencies(); + invokeStop(); + invokeRemoveRequiredInstanceBoundDependencies(); + notifyListeners(newState); + notifyListeners(STOPPED); + return true; + } + if (oldState == INSTANTIATED_AND_WAITING_FOR_REQUIRED && newState == WAITING_FOR_REQUIRED) { + invoke(m_callbackDestroy); + removeInstanceBoundDependencies(); + invokeRemoveRequiredDependencies(); + notifyListeners(newState); + if (! someDependenciesNeedInstance()) { + destroyComponent(); + } + return true; + } + if (oldState == WAITING_FOR_REQUIRED && newState == INACTIVE) { + stopDependencies(); + destroyComponent(); + notifyListeners(newState); + cleanup(); + return true; + } + return false; + } + + private void cleanup() { + m_dependencyEvents.values().forEach(eventList -> { + eventList.forEach(event -> event.close()); + eventList.clear(); + }); + } + + private void invokeStart() { + // Only invoke start callback for singleton components. + // We don't invoke start callbacks for components with scope=prototype or bundle because + // prototypes (bundle/prototype) components are only used to register ServiceFactory (or PrototypeServiceFactory) services. + if (m_scope == ServiceScope.SINGLETON) { + invoke(m_callbackStart); + m_startCalled = true; + } + } + + private void invokeStop() { + // Only invoke stop callback for singleton components. + // We don't invoke stop callbacks for components with scope=prototype or bundle because + // prototypes (bundle/prototype) components are only used to register ServiceFactory (or PrototypeServiceFactory) services. + if (m_scope == ServiceScope.SINGLETON) { + invoke(m_callbackStop); + m_startCalled = false; + } + } + + /** + * Sets the m_handlingChange flag that indicates if the state machine is currently running the handleChange method. + */ + private void handlingChange(boolean transiting) { + m_handlingChange = transiting; + } + + /** + * Are we currently running the handleChange method ? + */ + private boolean isHandlingChange() { + return m_handlingChange; + } + + /** + * Then handleEvent calls this method when a dependency service is being added. + */ + private void handleAdded(DependencyContext dc, Event e) { + if (! m_isStarted) { + return; + } + m_logger.debug("handleAdded %s", e); + + Set dependencyEvents = m_dependencyEvents.get(dc); + dependencyEvents.add(e); + dc.setAvailable(true); + + // In the following switch block, we sometimes only recalculate state changes + // if the dependency is fully started. If the dependency is not started, + // it means it is actually starting (the service tracker is executing the open method). + // And in this case, depending on the state, we don't recalculate state changes now. + // + // All this is done for two reasons: + // 1- optimization: it is preferable to recalculate state changes once we know about all currently + // available dependency services (after the tracker has returned from its open method). + // 2- This also allows to determine the list of currently available dependency services before calling + // the component start() callback. + + switch (m_state) { + case WAITING_FOR_REQUIRED: + if (dc.isStarted() && dc.isRequired()) { + handleChange(); + } + break; + case INSTANTIATED_AND_WAITING_FOR_REQUIRED: + if (!dc.isInstanceBound()) { + invokeCallback(dc, EventType.ADDED, e); + updateInstance(dc, e, false, true); + } else { + if (dc.isStarted() && dc.isRequired()) { + handleChange(); + } + } + break; + case TRACKING_OPTIONAL: + invokeCallback(dc, EventType.ADDED, e); + updateInstance(dc, e, false, true); + break; + default: + } + } + + /** + * Then handleEvent calls this method when a dependency service is being changed. + */ + private void handleChanged(final DependencyContext dc, final Event e) { + if (! m_isStarted) { + return; + } + Set dependencyEvents = m_dependencyEvents.get(dc); + dependencyEvents.remove(e); + dependencyEvents.add(e); + + switch (m_state) { + case TRACKING_OPTIONAL: + invokeCallback(dc, EventType.CHANGED, e); + updateInstance(dc, e, true, false); + break; + + case INSTANTIATED_AND_WAITING_FOR_REQUIRED: + if (!dc.isInstanceBound()) { + invokeCallback(dc, EventType.CHANGED, e); + updateInstance(dc, e, true, false); + } + break; + default: + // noop + } + } + + /** + * Then handleEvent calls this method when a dependency service is being removed. + */ + private void handleRemoved(DependencyContext dc, Event e) { + try { + if (! m_isStarted) { + return; + } + // Check if the dependency is still available. + Set dependencyEvents = m_dependencyEvents.get(dc); + int size = dependencyEvents.size(); + if (dependencyEvents.contains(e)) { + size--; // the dependency is currently registered and is about to be removed. + } + dc.setAvailable(size > 0); + + // If the dependency is now unavailable, we have to recalculate state change. This will result in invoking the + // "removed" callback with the removed dependency (which we have not yet removed from our dependency events list.). + // But we don't recalculate the state if the dependency is not started (if not started, it means that it is currently starting, + // and the tracker is detecting a removed service). + if (size == 0 && dc.isStarted()) { + handleChange(); + } + + // Now, really remove the dependency event. + dependencyEvents.remove(e); + + // Depending on the state, we possible have to invoke the callbacks and update the component instance. + switch (m_state) { + case INSTANTIATED_AND_WAITING_FOR_REQUIRED: + if (!dc.isInstanceBound()) { + invokeCallback(dc, EventType.REMOVED, e); + updateInstance(dc, e, false, false); + } + break; + case TRACKING_OPTIONAL: + invokeCallback(dc, EventType.REMOVED, e); + updateInstance(dc, e, false, false); + break; + default: + } + } finally { + e.close(); + } + } + + private void handleSwapped(DependencyContext dc, Event oldEvent, Event newEvent) { + try { + if (! m_isStarted) { + return; + } + Set dependencyEvents = m_dependencyEvents.get(dc); + dependencyEvents.remove(oldEvent); + dependencyEvents.add(newEvent); + + // Depending on the state, we possible have to invoke the callbacks and update the component instance. + switch (m_state) { + case WAITING_FOR_REQUIRED: + // No need to swap, we don't have yet injected anything + break; + case INSTANTIATED_AND_WAITING_FOR_REQUIRED: + // Only swap *non* instance-bound dependencies + if (!dc.isInstanceBound()) { + invokeSwapCallback(dc, oldEvent, newEvent); + } + break; + case TRACKING_OPTIONAL: + invokeSwapCallback(dc, oldEvent, newEvent); + break; + default: + } + } finally { + oldEvent.close(); + } + } + + private boolean allRequiredAvailable() { + boolean available = true; + for (DependencyContext d : m_dependencies) { + if (d.isRequired() && !d.isInstanceBound()) { + if (!d.isAvailable()) { + available = false; + break; + } + } + } + return available; + } + + private boolean allInstanceBoundAvailable() { + boolean available = true; + for (DependencyContext d : m_dependencies) { + if (d.isRequired() && d.isInstanceBound()) { + if (!d.isAvailable()) { + available = false; + break; + } + } + } + return available; + } + + private boolean someDependenciesNeedInstance() { + for (DependencyContext d : m_dependencies) { + if (d.needsInstance()) { + return true; + } + } + return false; + } + + /** + * Updates the component instance(s). + * @param dc the dependency context for the updating dependency service + * @param event the event holding the updating service (service + properties) + * @param update true if dependency service properties are updating, false if not. If false, it means + * that a dependency service is being added or removed. (see the "add" flag). + * @param add true if the dependency service has been added, false if it has been removed. This flag is + * ignored if the "update" flag is true (because the dependency properties are just being updated). + */ + private void updateInstance(DependencyContext dc, Event event, boolean update, boolean add) { + if (dc.isAutoConfig()) { + updateImplementation(dc.getAutoConfigType(), dc, dc.getAutoConfigName(), event, update, add); + } + if (dc.isPropagated() && m_registration != null) { + m_registration.setProperties(calculateServiceProperties()); + } + } + + private void startDependencies(List dependencies) { + // Start first optional dependencies first. + m_logger.debug("startDependencies."); + List requiredDeps = new ArrayList<>(); + for (DependencyContext d : dependencies) { + if (d.isRequired()) { + requiredDeps.add(d); + continue; + } + if (d.needsInstance()) { + instantiateComponent(); + } + d.start(); + } + // now, start required dependencies. + for (DependencyContext d : requiredDeps) { + if (d.needsInstance()) { + instantiateComponent(); + } + d.start(); + } + } + + private void stopDependencies() { + for (DependencyContext d : m_dependencies) { + d.stop(); + } + } + + private void registerService() { + if (m_context != null && m_serviceName != null) { + ServiceRegistrationImpl wrapper = new ServiceRegistrationImpl(); + m_registration = wrapper; + autoConfigureImplementation(ServiceRegistration.class, m_registration); + + // service name can either be a string or an array of strings + ServiceRegistration registration; + + // determine service properties + Dictionary properties = calculateServiceProperties(); + + // register the service + try { + Object componentInstance = null; + switch (m_scope) { + case SINGLETON: componentInstance = m_componentInstance; break; + case BUNDLE: componentInstance = new ComponentServiceFactory(); break; + case PROTOTYPE: componentInstance = new ComponentPrototypeServiceFactory(); break; + } + + if (m_serviceName instanceof String) { + registration = m_context.registerService((String) m_serviceName, componentInstance, properties); + } + else { + registration = m_context.registerService((String[]) m_serviceName, componentInstance, properties); + } + wrapper.setServiceRegistration(registration); + } + catch (IllegalArgumentException iae) { + m_logger.log(Logger.LOG_ERROR, "Could not register service " + m_componentInstance, iae); + // set the registration to an illegal state object, which will make all invocations on this + // wrapper fail with an ISE (which also occurs when the SR becomes invalid) + wrapper.setIllegalState(); + } + } + } + + private void unregisterService() { + if (m_serviceName != null && m_registration != null) { + try { + if (m_bundle != null && (m_bundle.getState() == Bundle.STARTING || m_bundle.getState() == Bundle.ACTIVE || m_bundle.getState() == Bundle.STOPPING)) { + m_registration.unregister(); + } + } catch (IllegalStateException e) { /* Should we really log this ? */} + autoConfigureImplementation(ServiceRegistration.class, NULL_REGISTRATION); + m_registration = null; + } + } + + private Dictionary calculateServiceProperties() { + Dictionary properties = new Hashtable<>(); + // First add propagated dependency service properties which don't override our component service properties + Predicate dependencyPropagated = (dc) -> dc.isPropagated() && dc.isAvailable(); + + m_dependencies.stream() + .filter(dc -> dependencyPropagated.test(dc) && ! dc.overrideServiceProperties()) + .forEach(dc -> addTo(properties, dc.getProperties())); + + // Now add our component service properties, which override previously added propagated dependency service properties + addTo(properties, m_serviceProperties); + + // Finally, add dependency service properties which override our component service properties + m_dependencies.stream() + .filter(dc -> dependencyPropagated.test(dc) && dc.overrideServiceProperties()) + .forEach(dc -> addTo(properties, dc.getProperties())); + + return properties; // FELIX-5683: now we never return null + } + + private void addTo(Dictionary properties, Dictionary additional) { + if (properties == null) { + throw new IllegalArgumentException("Dictionary to add to cannot be null."); + } + if (additional != null) { + Enumeration e = additional.keys(); + while (e.hasMoreElements()) { + Object key = e.nextElement(); + properties.put(key.toString(), additional.get(key)); + } + } + } + + private void destroyComponent() { + m_componentInstance = null; + } + + private void invokeAddRequiredDependencies() { + for (DependencyContext d : m_dependencies) { + if (d.isRequired() && !d.isInstanceBound()) { + for (Event e : m_dependencyEvents.get(d)) { + invokeCallback(d, EventType.ADDED, e); + } + } + } + } + + private void invokeAutoConfigDependencies() { + for (DependencyContext d : m_dependencies) { + if (d.isAutoConfig() && !d.isInstanceBound()) { + configureImplementation(d.getAutoConfigType(), d, d.getAutoConfigName()); + } + } + } + + private void invokeAutoConfigInstanceBoundDependencies() { + for (DependencyContext d : m_dependencies) { + if (d.isAutoConfig() && d.isInstanceBound()) { + configureImplementation(d.getAutoConfigType(), d, d.getAutoConfigName()); + } + } + } + + private void invokeAddRequiredInstanceBoundDependencies() { + for (DependencyContext d : m_dependencies) { + if (d.isRequired() && d.isInstanceBound()) { + for (Event e : m_dependencyEvents.get(d)) { + invokeCallback(d, EventType.ADDED, e); + } + } + } + } + + private void invokeAddOptionalDependencies() { + for (DependencyContext d : m_dependencies) { + if (! d.isRequired()) { + for (Event e : m_dependencyEvents.get(d)) { + invokeCallback(d, EventType.ADDED, e); + } + } + } + } + + private void invokeRemoveRequiredDependencies() { + for (DependencyContext d : m_dependencies) { + if (!d.isInstanceBound() && d.isRequired()) { + for (Event e : m_dependencyEvents.get(d)) { + invokeCallback(d, EventType.REMOVED, e); + } + } + } + } + + private void invokeRemoveOptionalDependencies() { + for (DependencyContext d : m_dependencies) { + if (! d.isRequired()) { + for (Event e : m_dependencyEvents.get(d)) { + invokeCallback(d, EventType.REMOVED, e); + } + } + } + } + + private void invokeRemoveRequiredInstanceBoundDependencies() { + for (DependencyContext d : m_dependencies) { + if (d.isInstanceBound() && d.isRequired()) { + for (Event e : m_dependencyEvents.get(d)) { + invokeCallback(d, EventType.REMOVED, e); + } + } + } + } + + /** + * This method ensures that a dependency callback is invoked only one time; + * It also ensures that if the dependency callback is optional, then we only + * invoke the bind method if the component start callback has already been called. + */ + private void invokeCallback(DependencyContext dc, EventType type, Event event) { + // don't inject anything if the component instance is a hidden prototype instance + if (m_injectionDisabled) { + return; + } + + if (! dc.isRequired() && ! m_startCalled) { + return; + } + + // First, check if the callback has not already been called (see FELIX-4913) + if (m_invokeCallbackCache.put(event, event) == null) { + // FELIX-5155: we must not invoke callbacks on our special internal components (adapters/aspects) if the dependency is not the first one, or + // if the internal component is a Factory Pid Adapter. + // For aspects/adapters, the first dependency only need to be injected, not the other extra dependencies added by user. + // (in fact, we do this because extra dependencies (added by user) may contain a callback instance, and we really don't want to invoke the callbacks twice ! + Object mainComponentImpl = getInstance(); + if (mainComponentImpl instanceof AbstractDecorator) { + if (mainComponentImpl instanceof FactoryConfigurationAdapterImpl.AdapterImpl || dc != m_dependencies.get(0)) { + return; + } + } + dc.invokeCallback(type, event); + } + } + + /** + * Invokes a swap callback, except if the dependency is optional and the component is + * not started (optional dependencies are always injected while the component is started). + */ + private void invokeSwapCallback(DependencyContext dc, Event oldEvent, Event newEvent) { + if (! dc.isRequired() && ! m_startCalled) { + return; + } + dc.invokeCallback(EventType.SWAPPED, oldEvent, newEvent); + } + + /** + * Removes and closes all instance bound dependencies. + * This method is called when a component is destroyed. + */ + private void removeInstanceBoundDependencies() { + for (DependencyContext dep : m_dependencies) { + if (dep.isInstanceBound()) { + m_dependencies.remove(dep); + generateNameBasedOnServiceAndProperties(); + dep.stop(); + } + } + } + + /** + * Clears the cache of invoked components callbacks. + * We only clear the cache when the state machine is not running. + * The cache is used to avoid calling the same bind callback twice. + * See FELIX-4913. + */ + private void clearInvokeCallbackCache() { + if (! isHandlingChange()) { + m_invokeCallbackCache.clear(); + } + } + + private void invoke(String name) { + if (name != null) { + // if a callback instance was specified, look for the method there, if not, + // ask the service for its composition instances + Object[] instances = m_callbackInstance != null ? new Object[] { m_callbackInstance } : getCompositionInstances(); + + long t1 = System.nanoTime(); + try { + invokeCallbackMethod(instances, name, + new Class[][] {{ Component.class }, {}}, + new Object[][] {{ this }, {}}, + false); + } finally { + long t2 = System.nanoTime(); + m_stopwatch.put(name, t2 - t1); + } + } + } + + private void notifyListeners(ComponentState state) { + if (m_scope == ServiceScope.SINGLETON) { + m_lastStateDeliveredToListeners = state; + for (ComponentStateListener l : m_listeners) { + try { + l.changed(this, state); + } catch (Exception e) { + m_logger.log(Logger.LOG_ERROR, "Exception caught while invoking component state listener", e); + } + } + } + } + + void autoConfigureImplementation(Class clazz, Object instance) { + if (((Boolean) m_autoConfig.get(clazz)).booleanValue()) { + configureImplementation(clazz, instance, (String) m_autoConfigInstance.get(clazz)); + } + } + + /** + * Configure a field in the service implementation. The service implementation + * is searched for fields that have the same type as the class that was specified + * and for each of these fields, the specified instance is filled in. + * + * @param clazz the class to search for + * @param instance the object to fill in the implementation class(es) field + * @param instanceName the name of the instance to fill in, or null if not used + */ + void configureImplementation(Class clazz, Object instance, String fieldName) { + // don't inject anything if the component instance is a hidden prototype instance + if (m_injectionDisabled) { + return; + } + + Object[] targets = getInstances(); + if (! FieldUtil.injectField(targets, fieldName, clazz, instance, m_logger) && fieldName != null) { + // If the target is an abstract decorator (i.e: an adapter, or an aspect), we must not log warnings + // if field has not been injected. + if (! (getInstance() instanceof AbstractDecorator)) { + m_logger.log(Logger.LOG_ERROR, "Could not inject " + instance + " to field \"" + fieldName + + "\" at any of the following component instances: " + Arrays.toString(targets)); + } + } + } + + private void configureImplementation(Class clazz, DependencyContext dc, String fieldName) { + // don't inject anything if the component instance is a hidden prototype instance + if (m_injectionDisabled) { + return; + } + + Object[] targets = getInstances(); + if (! FieldUtil.injectDependencyField(targets, fieldName, clazz, dc, m_logger) && fieldName != null) { + // If the target is an abstract decorator (i.e: an adapter, or an aspect), we must not log warnings + // if field has not been injected. + if (! (getInstance() instanceof AbstractDecorator)) { + m_logger.log(Logger.LOG_ERROR, "Could not inject dependency " + clazz.getName() + " to field \"" + + fieldName + "\" at any of the following component instances: " + Arrays.toString(targets)); + } + } + } + + /** + * Update the component instances. + * + * @param clazz the class of the dependency service to inject in the component instances + * @param dc the dependency context for the updating dependency service + * @param fieldName the component instances fieldname to fill in with the updated dependency service + * @param event the event holding the updating service (service + properties) + * @param update true if dependency service properties are updating, false if not. If false, it means + * that a dependency service is being added or removed. (see the "add" flag). + * @param add true if the dependency service has been added, false if it has been removed. This flag is + * ignored if the "update" flag is true (because the dependency properties are just being updated). + */ + private void updateImplementation(Class clazz, DependencyContext dc, String fieldName, Event event, boolean update, boolean add) { + if (! m_injectionDisabled) { + Object[] targets = getInstances(); + FieldUtil.updateDependencyField(targets, fieldName, update, add, clazz, event, dc, m_logger); + } + } + + private Object[] getCompositionInstances() { + Object[] instances = null; + if (m_compositionManagerGetMethod != null) { + if (m_compositionManager != null) { + m_compositionManagerInstance = m_compositionManager; + } + else { + m_compositionManagerInstance = m_componentInstance; + } + if (m_compositionManagerInstance != null) { + try { + instances = (Object[]) InvocationUtil.invokeMethod(m_compositionManagerInstance, m_compositionManagerInstance.getClass(), m_compositionManagerGetMethod, new Class[][] {{}}, new Object[][] {{}}, false); + } + catch (Exception e) { + m_logger.log(Logger.LOG_ERROR, "Could not obtain instances from the composition manager.", e); + instances = m_componentInstance == null ? new Object[] {} : new Object[] { m_componentInstance }; + } + } else { + return new Object[0]; + } + } + else { + instances = m_componentInstance == null ? new Object[] {} : new Object[] { m_componentInstance }; + } + return instances; + } + + /** + * Executes a task using our component queue. The queue associated to this component is by default a "SerialExecutor", or + * a "DispatchExecutor" if a threadpool is used (that is: if a ComponentExecutorFactory has returned an executor this our + * component). + * + * A SerialExecutor always try to execute the task synchronously, except if another master thread is currently + * running the SerialExecutor queue. + * A DispatchExecutor always schedule the task asynchronously in a threadpool, but can optionally try to execute the task synchronously + * (the threadpool is bypassed only if the queue is not currently being run by the threadpool). + * + * The "bypassThreadPoolIfPossible" argument is only used in case there is a threadpool configured. If no threadpool is used, + * this argument is ignored and the task is run synchronously if the serial queue is not concurrently run from another + * master threads. + * + * @param bypassThreadPoolIfPossible This argument is only used if a threadpool is configured for this component. if true, + * it means that if a threadpool is configured, then we attempt to run the task synchronously and not using the threadpool, + * but if the queue is currently run from the threadpool, then the task is scheduled in it. + * false means we always use the threadpool and the task is executed asynchronously. + * @param task the task to execute. + */ + private void schedule(boolean bypassThreadPoolIfPossible, Runnable task) { + Executor exec = getExecutor(); + if (exec instanceof DispatchExecutor) { + // We are using a threadpool. If byPassThreadPoolIsPossible is true, it means we can try to run the task synchronously from the current + // thread. But if our queue is currently run from the threadpool, then the task will be run from it (asynchronously). + ((DispatchExecutor) exec).execute(task, ! bypassThreadPoolIfPossible); + } else { + // The queue is a serial queue: the task will be run synchronously, except if another master thread is currently running the queue. + exec.execute(task); + } + } + + private void configureAutoConfigState(Component target, ComponentContext source) { + configureAutoConfigState(target, source, BundleContext.class); + configureAutoConfigState(target, source, ServiceRegistration.class); + configureAutoConfigState(target, source, DependencyManager.class); + configureAutoConfigState(target, source, Component.class); + } + + private void configureAutoConfigState(Component target, ComponentContext source, Class clazz) { + String name = source.getAutoConfigInstance(clazz); + if (name != null) { + target.setAutoConfig(clazz, name); + } + else { + target.setAutoConfig(clazz, source.getAutoConfig(clazz)); + } + } + + private final static Class[][] INIT_SIGNATUTES = new Class[][] { { Component.class }, {} }; + private final static Class[][] FACTORY_CREATE_SIGNATURES = new Class[][] { {}, { Component.class } }; + + private boolean hasInitMethodWhenNotSingleton() { + if (m_callbackInit == null) { + return false; + } + if (m_componentDefinition instanceof Class) { + if (null != InvocationUtil.getCallbackMethod((Class) m_componentDefinition, m_callbackInit, INIT_SIGNATUTES)) { + return true; + } + } else if (m_instanceFactoryCreateMethod != null) { + if (m_instanceFactory != null) { + Class factoryClass = null; + + if (m_instanceFactory instanceof Class) { + factoryClass = (Class) m_instanceFactory; + } else if (m_instanceFactory != null) { + factoryClass = m_instanceFactory.getClass(); + } + Method create = InvocationUtil.getCallbackMethod(factoryClass, m_instanceFactoryCreateMethod, FACTORY_CREATE_SIGNATURES); + Class componentType = create.getReturnType(); + if (componentType != null && InvocationUtil.getCallbackMethod(componentType, m_callbackInit, INIT_SIGNATUTES) != null) { + return true; + } + } + } + return false; + } + + private void initPrototype() { + switch (m_scope) { + case PROTOTYPE: + case BUNDLE: + if (m_callbackInit == null || ! hasInitMethodWhenNotSingleton()) { + // when no init callback is defined, or if the component implementation does not actually has an init method, we disable injection for the component prototype instance. + m_injectionDisabled = true; + } + break; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ComponentScheduler.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ComponentScheduler.java new file mode 100644 index 00000000000..e5140afae64 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ComponentScheduler.java @@ -0,0 +1,147 @@ +/* + * 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.dm.impl; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentDeclaration; +import org.apache.felix.dm.ComponentExecutorFactory; +import org.apache.felix.dm.context.ComponentContext; +import org.osgi.framework.BundleContext; + +/** + * The Dependency Manager delegates all components addition/removal to this class. + * If a ComponentExecutorFactory is registered in the OSGi registry, this class will use it to get an + * Executor used for components management and lifecycle callbacks. + * + * @author Felix Project Team + */ +public class ComponentScheduler { + private final static ComponentScheduler m_instance = new ComponentScheduler(); + private final static String PARALLEL = "org.apache.felix.dependencymanager.parallel"; + private volatile ComponentExecutorFactory m_componentExecutorFactory; + private final Executor m_serial = new SerialExecutor(null); + private ConcurrentMap m_pending = new ConcurrentHashMap<>(); + + public static ComponentScheduler instance() { + return m_instance; + } + + protected void bind(final ComponentExecutorFactory componentExecutorFactory) { + m_componentExecutorFactory = componentExecutorFactory; + m_serial.execute(new Runnable() { + @Override + public void run() { + for (Component c : m_pending.keySet()) { + createComponentExecutor(m_componentExecutorFactory, c); + ((ComponentContext) c).start(); + } + m_pending.clear(); + } + }); + } + + protected void unbind(ComponentExecutorFactory threadPool) { + m_componentExecutorFactory = null; + } + + public void add(final Component c) { + if (mayStartNow(c)) { + ((ComponentContext) c).start(); + } + else { + // The component requires a threadpool: delay execution until one is available. + m_serial.execute(new Runnable() { + @Override + public void run() { + ComponentExecutorFactory execFactory = m_componentExecutorFactory; + if (execFactory == null) { + m_pending.put(c, c); + } + else { + createComponentExecutor(execFactory, c); + ((ComponentContext) c).start(); + } + } + }); + } + } + + public void remove(final Component c) { + m_pending.remove(c); + ((ComponentContext) c).stop(); + } + + private boolean mayStartNow(Component c) { + ComponentExecutorFactory execFactory = m_componentExecutorFactory; + BundleContext ctx = c.getDependencyManager().getBundleContext(); + String parallel = ctx.getProperty(PARALLEL); + + if (execFactory == null) { + // No ComponentExecutorFactory available. If a "parallel" OSGi system property is specified, + // we have to wait for a ComponentExecutorFactory servoce if the component class name is matching one of the + // prefixes specified in the "parallel" system property. + if (parallel != null && requiresThreadPool(c, parallel)) { + return false; // wait for a threadpool + } else { + return true; // no threadpool required, start the component now, synchronously + } + } + else { + // A threadpool is there. If the "parallel" OSGi system property is not specified, we can start the component + // now and we'll use the threadpool for it. + // But if the "parallel" system property is specified, the component will use the threadpool only if it's + // classname is starting with one of the prefixes specified in the property. + if (parallel == null || requiresThreadPool(c, parallel)) { + createComponentExecutor(execFactory, c); + } + return true; // start the component now, possibly using the threadpool (see above). + } + } + + private boolean requiresThreadPool(Component c, String parallel) { + // The component declared from our DM Activator can not be parallel. + ComponentDeclaration decl = c.getComponentDeclaration(); + if (ComponentScheduler.class.getName().equals(decl.getName())) { + return false; + } + + for (String prefix : parallel.trim().split(",")) { + prefix = prefix.trim(); + boolean not = prefix.startsWith("!"); + if (not) { + prefix = prefix.substring(1).trim(); + } + if ("*".equals(prefix) || c.getComponentDeclaration().getClassName().startsWith(prefix)) { + return !not; + } + } + return false; + } + + private void createComponentExecutor(ComponentExecutorFactory execFactory, Component c) { + Executor exec = execFactory.getExecutorFor(c); + if (exec != null) { + ((ComponentContext) c).setThreadPool(exec); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/Configurable.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/Configurable.java new file mode 100644 index 00000000000..9c328a2e6f1 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/Configurable.java @@ -0,0 +1,751 @@ +/* + * 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.dm.impl; + +import java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * Provides a way for creating type-safe configurations from a {@link Map} or {@link Dictionary}. + *

      + * This class takes a map or dictionary along with a class, the configuration-type, and returns a proxy that converts + * method calls from the configuration-type to lookups in the map or dictionary. The results of these lookups are then + * converted to the expected return type of the invoked configuration method.
      + * As proxies are returned, no implementations of the desired configuration-type are necessary! + *

      + *

      + * The lookups performed are based on the name of the method called on the configuration type. The method names are + * "mangled" to the following form: [lower case letter] [any valid character]*. Method names starting with + * get or is (JavaBean convention) are stripped from these prefixes. For example: given a dictionary + * with the key "foo" can be accessed from a configuration-type using the following method names: + * foo(), getFoo() and isFoo(). + * + * If the property name contains some dots, the the following conventions are used: + *

        + *
      • camel casing: if a property contains multiple words separated by dots, then you can indicate words boundaries using medial capitalization. + * For example, the property "foo.bar" could be accessed with a method name like "fooBar()" or "getFooBar()". + *
      • use underscore to wrap dots: underscore ("_") found in method names are converted to ".", unless they are followed by another underscore. + * (in this case, the double "__" is then converted to single underscore ("_"). + * For Example: foo_bar() will be mapped to "foo.bar" property, and foo__bar() will be mapped to "foo_bar" property. + *
      + *

      + *

      + * The return values supported are: primitive types (or their object wrappers), strings, enums, arrays of + * primitives/strings, {@link Collection} types, {@link Map} types, {@link Class}es and interfaces. When an interface is + * returned, it is treated equally to a configuration type, that is, it is returned as a proxy. + *

      + *

      + * Arrays can be represented either as comma-separated values, optionally enclosed in square brackets. For example: + * [ a, b, c ] and a, b,c are both considered an array of length 3 with the values "a", "b" and "c". + * Alternatively, you can append the array index to the key in the dictionary to obtain the same: a dictionary with + * "arr.0" => "a", "arr.1" => "b", "arr.2" => "c" would result in the same array as the earlier examples. + *

      + *

      + * Maps can be represented as single string values similarly as arrays, each value consisting of both the key and value + * separated by a dot. Optionally, the value can be enclosed in curly brackets. Similar to array, you can use the same + * dot notation using the keys. For example, a dictionary with "map" => "{key1.value1, key2.value2}" and a + * dictionary with "map.key1" => "value1", "map2.key2" => "value2" result in the same map being returned. + * Instead of a map, you could also define an interface with the methods getKey1() and getKey2 and use + * that interface as return type instead of a {@link Map}. + *

      + *

      + * In case a lookup does not yield a value from the underlying map or dictionary, the following rules are applied: + *

        + *
      1. primitive types yield their default value, as defined by the Java Specification; + *
      2. string, {@link Class}es and enum values yield null; + *
      3. for arrays, collections and maps, an empty array/collection/map is returned; + *
      4. for other interface types that are treated as configuration type a null-object is returned. + *
      + *

      + */ +public final class Configurable { + + static class ConfigHandler implements InvocationHandler { + private final ClassLoader m_cl; + private final Map m_config; + private Class m_configType; + + /** Constant for the single element method */ + private static final String VALUE_METHOD = "value"; + + /** Constant for the prefix constant. */ + private static final String PREFIX_CONSTANT = "PREFIX_"; + + /** Capture all methods defined by the annotation interface */ + private static final Set ANNOTATION_METHODS = new HashSet(); + static + { + for(final Method m : Annotation.class.getMethods()) + { + ANNOTATION_METHODS.add(m); + } + } + + public ConfigHandler(Class type, ClassLoader cl, Map config) { + m_configType = type; + m_cl = cl; + m_config = config; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (method.getName().equals("toString")) { + return m_config.toString(); + } + String name = getPropertyName(method.getName()); + + Object result = convert(method.getGenericReturnType(), name, m_config.get(name), false /* useImplicitDefault */); + if (result == null) { + Object defaultValue = getDefaultValue(proxy, args, method, name); + if (defaultValue != null) { + return defaultValue; + } + } + + return result; + } + + @SuppressWarnings("unchecked") + private Object convertParameterizedType(ParameterizedType type, String key, Object value, boolean useImplicitDefault) throws Exception { + Class resultType = (Class) type.getRawType(); + if (Class.class.isAssignableFrom(resultType)) { + if (value == null) { + return null; + } + return m_cl.loadClass(value.toString()); + } + else if (Collection.class.isAssignableFrom(resultType)) { + Collection input = toCollection(key, value); + if (input == null && ! useImplicitDefault) { + return null; + } + + if (resultType == Collection.class || resultType == List.class) { + resultType = ArrayList.class; + } + else if (resultType == Set.class || resultType == SortedSet.class) { + resultType = TreeSet.class; + } + else if (resultType == Queue.class) { + resultType = LinkedList.class; + } + else if (resultType.isInterface()) { + throw new RuntimeException("Unknown collection interface: " + resultType); + } + + Collection result = (Collection) resultType.newInstance(); + if (input != null) { + Type componentType = type.getActualTypeArguments()[0]; + for (Object i : input) { + result.add(convert(componentType, key, i, false /* useImplicitDefault */)); + } + } + return result; + } + else if (Map.class.isAssignableFrom(resultType)) { + Map input = toMap(key, value); + if (input == null && ! useImplicitDefault) { + return null; + } + + if (resultType == SortedMap.class) { + resultType = TreeMap.class; + } + else if (resultType == Map.class) { + resultType = LinkedHashMap.class; + } + else if (resultType.isInterface()) { + throw new RuntimeException("Unknown map interface: " + resultType); + } + + Map result = (Map) resultType.newInstance(); + Type keyType = type.getActualTypeArguments()[0]; + Type valueType = type.getActualTypeArguments()[1]; + + if (input != null) { + for (Map.Entry entry : input.entrySet()) { + result.put(convert(keyType, key, entry.getKey(), false /* useImplicitDefault */), convert(valueType, key, entry.getValue(), false /* useImplicitDefault */)); + } + } + return result; + } + + throw new RuntimeException("Unhandled type: " + type); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Object convert(Type type, String key, Object value, boolean useImplicitDefault) throws Exception { + if (type instanceof ParameterizedType) { + return convertParameterizedType((ParameterizedType) type, key, value, useImplicitDefault); + } + if (type instanceof GenericArrayType) { + return convertArray(((GenericArrayType) type).getGenericComponentType(), key, value, useImplicitDefault); + } + Class resultType = (Class) type; + if (resultType.isArray()) { + return convertArray(resultType.getComponentType(), key, value, useImplicitDefault); + } + if (resultType.isInstance(value)) { + return value; + } + + if (Boolean.class.equals(resultType) || Boolean.TYPE.equals(resultType)) { + if (value == null) { + return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_BOOLEAN : null; + } + return Boolean.valueOf(value.toString()); + } + else if (Byte.class.equals(resultType) || Byte.TYPE.equals(resultType)) { + if (value == null) { + return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_BYTE : null; + } + if (value instanceof Number) { + return ((Number) value).byteValue(); + } + return Byte.valueOf(value.toString()); + } + else if (Short.class.equals(resultType) || Short.TYPE.equals(resultType)) { + if (value == null) { + return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_SHORT : null; + } + if (value instanceof Number) { + return ((Number) value).shortValue(); + } + return Short.valueOf(value.toString()); + } + else if (Integer.class.equals(resultType) || Integer.TYPE.equals(resultType)) { + if (value == null) { + return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_INT : null; + } + if (value instanceof Number) { + return ((Number) value).intValue(); + } + return Integer.valueOf(value.toString()); + } + else if (Long.class.equals(resultType) || Long.TYPE.equals(resultType)) { + if (value == null) { + return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_LONG : null; + } + if (value instanceof Number) { + return ((Number) value).longValue(); + } + return Long.valueOf(value.toString()); + } + else if (Float.class.equals(resultType) || Float.TYPE.equals(resultType)) { + if (value == null) { + return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_FLOAT : null; + } + if (value instanceof Number) { + return ((Number) value).floatValue(); + } + return Float.valueOf(value.toString()); + } + else if (Double.class.equals(resultType) || Double.TYPE.equals(resultType)) { + if (value == null) { + return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_DOUBLE : null; + } + if (value instanceof Number) { + return ((Number) value).doubleValue(); + } + return Double.valueOf(value.toString()); + } + else if (Number.class.equals(resultType)) { + if (value == null) { + return null; + } + String numStr = value.toString(); + if (numStr.indexOf('.') > 0) { + return Double.valueOf(numStr); + } + return Long.valueOf(numStr); + } + else if (String.class.isAssignableFrom(resultType)) { + return value == null ? null : value.toString(); + } + else if (Enum.class.isAssignableFrom(resultType)) { + if (value == null) { + return null; + } + Class enumType = (Class) resultType; + return Enum.valueOf(enumType, value.toString().toUpperCase()); + } + else if (resultType.isInterface()) { + Map map = toMap(key, value); + if (map == null) { + return useImplicitDefault ? create(resultType, Collections.emptyMap()) : null; + } + return create(resultType, map); + } + + throw new RuntimeException("Unhandled type: " + type); + } + + private Object convertArray(Type type, String key, Object value, boolean useImplicitDefault) throws Exception { + if (value instanceof String) { + String str = (String) value; + if (type == Byte.class || type == byte.class) { + return str.getBytes("UTF-8"); + } + if (type == Character.class || type == char.class) { + return str.toCharArray(); + } + } + + Collection input = toCollection(key, value); + if (input == null && useImplicitDefault) { + input = Collections.emptyList(); + } + if (input == null) { + return null; + } + + Class componentClass = getRawClass(type); + Object array = Array.newInstance(componentClass, input.size()); + + int i = 0; + for (Object next : input) { + Array.set(array, i++, convert(type, key, next, false /* useImplicitDefault */)); + } + return array; + } + + private Object getDefaultValue(Object proxy, Object[] args, Method method, String key) throws Throwable { + Object def = null; + // Handle cases where the method is part of an annotation or is a java8 default method. + Class methodClass = method.getDeclaringClass(); + if (methodClass.isAnnotation()) { + // the config type is an annotation: simply invoke the default value + def = method.getDefaultValue(); + } else if (method.isDefault()) { + if (System.getProperty("java.version", "1.8").startsWith("1.8")) { + // The config type is a java8 interface with a default method, invoke it. + // But it's challenging to invoke a default method from a dynamic proxy ... we have to use the MethodHandles. + // see https://zeroturnaround.com/rebellabs/recognize-and-conquer-java-proxies-default-methods-and-method-handles + + Constructor constructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class); + constructor.setAccessible(true); + def = constructor.newInstance(methodClass, MethodHandles.Lookup.PRIVATE) + .unreflectSpecial(method, methodClass) + .bindTo(proxy) + .invokeWithArguments(args); + } else { + // see https://dzone.com/articles/correct-reflective-access-to-interface-default-methods + def = MethodHandles.lookup() + .findSpecial(methodClass, + method.getName(), + MethodType.methodType(method.getReturnType(), method.getParameterTypes()), + methodClass) + .bindTo(proxy) + .invokeWithArguments(); + } + } + return convert(method.getGenericReturnType(), key, def, true /* useImplicitDefault */); + } + + private Class getRawClass(Type type) { + if (type instanceof ParameterizedType) { + return (Class) ((ParameterizedType) type).getRawType(); + } + if (type instanceof Class) { + return (Class) type; + } + throw new RuntimeException("Unhandled type: " + type); + } + + private Collection toCollection(String prefix, Object value) { + if (value instanceof Collection) { + return (Collection) value; + } + + if (value == null) { + List result = new ArrayList<>(); + + String needle = prefix.concat("."); + for (Map.Entry entry : m_config.entrySet()) { + String key = entry.getKey().toString(); + if (!key.startsWith(needle)) { + continue; + } + + int idx = 0; + try { + idx = Integer.parseInt(key.substring(needle.length())); + } + catch (NumberFormatException e) { + // Ignore + } + + result.add(Math.min(result.size(), idx), entry.getValue()); + } + + return result.size() == 0 ? null : result; + } + + if (value.getClass().isArray()) { + if (value.getClass().getComponentType().isPrimitive()) { + int length = Array.getLength(value); + List result = new ArrayList(length); + for (int i = 0; i < length; i++) { + result.add(Array.get(value, i)); + } + return result; + } + return Arrays.asList((Object[]) value); + } + + if (value instanceof String) { + String str = (String) value; + if (str.startsWith("[") && str.endsWith("]")) { + str = str.substring(1, str.length() - 1); + } + // don't split in case we are parsing an empty [] list, in which case we need to return an empty list. + return str.length() == 0 ? Collections.emptyList() : Arrays.asList(str.split("\\s*,\\s*")); + } + + return Arrays.asList(value); + } + + private Map toMap(String prefix, Object value) { + if (value instanceof Map) { + return (Map) value; + } + + Map result = new HashMap<>(); + if (value == null) { + String needle = prefix.concat("."); + for (Map.Entry entry : m_config.entrySet()) { + String key = entry.getKey().toString(); + if (key.startsWith(needle)) { + result.put(key.substring(needle.length()), entry.getValue()); + } + } + if (result.size() == 0) { + return null; + } + } + else if (value instanceof String) { + String str = (String) value; + if (str.startsWith("{") && str.endsWith("}")) { + str = str.substring(1, str.length() - 1); + } + for (String entry : str.split("\\s*,\\s*")) { + String[] pair = entry.split("\\s*\\.\\s*", 2); + if (pair.length == 2) { + result.put(pair[0], pair[1]); + } + } + } + + return result; + } + + private String getPropertyName(String methodName) { + // First, check if the config type defines a standard PREFIX_ string. + String prefix = getPrefix(m_configType); + + // If the configuration type is a single valued annotation, derive the property name + // from the interface name, using OSGi R7 Scr convention + + if (isSingleElementAnnotation(m_configType) && methodName.equals(VALUE_METHOD)) { + String propertyName = mapTypeNameToKey(m_configType.getSimpleName()); + return prefix == null ? propertyName : prefix.concat(propertyName); + } + + // Now, derive the property name from the method name, using simple javabean convention. + // i.e: fooBar() or getFooBar() will map to "fooBar" property. + + String javaBeanMethodName = derivePropertyNameUsingJavaBeanConvention(methodName); + if (hasValueFor(javaBeanMethodName)) { + // there is a value in the actual configuration for the derived property name. + return javaBeanMethodName; + } + + // Derive the property name from the method name, using javabeans and/or camel casing convention, + // where each capital letter is assumed to map a "dot". + // i.e: fooBar() or getFooBar() will map to "foo.bar" property. + + String camelCasePropertyName = derivePropertyNameUsingCamelCaseConvention(javaBeanMethodName); + if (hasValueFor(camelCasePropertyName)) { + // there is a value in the actual configuration for the derived property name. + return camelCasePropertyName; + } + + // Derive the property name from the method name, using OSGi metatype convention, + // where a "_" is mapped to a dot, except if the understcore is followed by another undescore + // (in this case, the double "__" is replaced by "_"). + // i.e: foo_bar() will map to "foo.bar" property and foo__bar() will map to "foo_bar" property. + + String metaTypePropertyName = derivePropertyNameUsingMetaTypeConvention(methodName); + if (hasValueFor(metaTypePropertyName)) { + // there is a value in the actual configuration for the derived property name. + return metaTypePropertyName; + } + + // No value could be found, return by default a property name derived from javabean convention. + return javaBeanMethodName; + } + + private String derivePropertyNameUsingJavaBeanConvention(String methodName) { + StringBuilder sb = new StringBuilder(methodName); + + if (methodName.startsWith("get")) { + sb.delete(0, 3); + } else if (methodName.startsWith("is")) { + sb.delete(0, 2); + } + + char c = sb.charAt(0); + if (Character.isUpperCase(c)) { + sb.setCharAt(0, Character.toLowerCase(c)); + } + + return (sb.toString()); + } + + private String derivePropertyNameUsingCamelCaseConvention(String methodName) { + StringBuilder sb = new StringBuilder(methodName); + for (int i = 0; i < sb.length(); i++) { + char c = sb.charAt(i); + if (Character.isUpperCase(c)) { + // camel casing: replace fooBar -> foo.bar + sb.setCharAt(i, Character.toLowerCase(c)); + sb.insert(i, "."); + } + } + return sb.toString(); + } + + // see metatype spec, chapter 105.9.2 in osgi r6 cmpn. + private String derivePropertyNameUsingMetaTypeConvention(String methodName) { + StringBuilder sb = new StringBuilder(methodName); + // replace "__" by "_" or "_" by ".": foo_bar -> foo.bar; foo__BAR_zoo -> foo_BAR.zoo + for (int i = 0; i < sb.length(); i ++) { + if (sb.charAt(i) == '_') { + if (i < (sb.length() - 1) && sb.charAt(i+1) == '_') { + // replace foo__bar -> foo_bar + sb.replace(i, i+2, "_"); + } else { + // replace foo_bar -> foo.bar + sb.replace(i, i+1, "."); + } + } else if (sb.charAt(i) == '$') { + if (i < (sb.length() - 1) && sb.charAt(i+1) == '$') { + // replace foo__bar -> foo_bar + sb.replace(i, i+2, "$"); + } else { + // remove single dollar character. + sb.delete(i, i+1); + } + } + } + return sb.toString(); + } + + /** + * Checks if a property name has a given value. This method takes care about special array values (arr.0, arr.1,...) + * and about map values (map.key1, map.key2, ...). + * + * @param property name + * @return true if the given property has a value in the actual configuration, false if not. + */ + private boolean hasValueFor(String property) + { + if (m_config.containsKey(property)) { + return true; + } + String needle = property.concat("."); + for (Map.Entry entry : m_config.entrySet()) { + String key = entry.getKey().toString(); + if (key.startsWith(needle)) { + return true; + } + } + return false; + } + + // Code derived from Apache Felix SCR (See org.apache.felix.scr.impl.inject.Annotations.java) + private String getPrefix(Class clazz) + { + try + { + final Field f = clazz.getField(PREFIX_CONSTANT); + if ( Modifier.isStatic(f.getModifiers()) + && Modifier.isPublic(f.getModifiers()) + && Modifier.isFinal(f.getModifiers()) + && String.class.isAssignableFrom(f.getType())) + { + final Object value = f.get(null); + if ( value != null ) + { + return value.toString(); + } + } + } + catch ( final Exception ignore) + { + // ignore + } + return null; + } + + /** + * Check whether the provided type is a single element annotation. + * A single element annotation has a method named "value" and all + * other annotation methods must have a default value. + * @param clazz The provided type + * @return {@code true} if the type is a single element annotation. + */ + static public boolean isSingleElementAnnotation(final Class clazz) + { + boolean result = false; + if ( clazz.isAnnotation() ) + { + result = true; + boolean hasValue = false; + for ( final Method method: clazz.getMethods() ) + { + // filter out methods from Annotation + boolean isFromAnnotation = false; + for(final Method objMethod : ANNOTATION_METHODS) + { + if ( objMethod.getName().equals(method.getName()) + && Arrays.equals(objMethod.getParameterTypes(), method.getParameterTypes()) ) + { + isFromAnnotation = true; + break; + } + } + if ( isFromAnnotation ) + { + continue; + } + if ( VALUE_METHOD.equals(method.getName()) ) + { + hasValue = true; + continue; + } + if ( method.getDefaultValue() == null ) + { + result = false; + break; + } + + } + if ( result ) + { + result = hasValue; + } + + } + return result; + } + + static String mapTypeNameToKey(String name) + { + final StringBuilder sb = new StringBuilder(); + boolean lastLow = false; + for(final char c : name.toCharArray()) + { + if ( lastLow && (Character.isLetter(c) || Character.isDigit(c)) && Character.isUpperCase(c) ) + { + sb.append('.'); + } + lastLow = false; + if ( (Character.isLetter(c) || Character.isDigit(c)) && Character.isLowerCase(c)) + { + lastLow = true; + } + sb.append(Character.toLowerCase(c)); + } + return sb.toString(); + } + + } + + private static final Boolean DEFAULT_BOOLEAN = Boolean.FALSE; + private static final Byte DEFAULT_BYTE = new Byte((byte) 0); + private static final Short DEFAULT_SHORT = new Short((short) 0); + private static final Integer DEFAULT_INT = new Integer(0); + private static final Long DEFAULT_LONG = new Long(0); + private static final Float DEFAULT_FLOAT = new Float(0.0f); + private static final Double DEFAULT_DOUBLE = new Double(0.0); + + /** + * Creates a configuration for a given type backed by a given dictionary. + * + * @param type the configuration class, cannot be null; + * @param config the configuration to wrap, cannot be null. + * @param serviceProperties the component service properties, cannot be null. + * @return an instance of the given type that wraps the given configuration. + */ + public static T create(Class type, Dictionary config, Dictionary serviceProperties) { + Map map = new HashMap(); + for (Enumeration e = serviceProperties.keys(); e.hasMoreElements();) { + Object key = e.nextElement(); + map.put(key, serviceProperties.get(key)); + } + for (Enumeration e = config.keys(); e.hasMoreElements();) { + Object key = e.nextElement(); + map.put(key, config.get(key)); + } + return create(type, map); + } + + /** + * Creates a configuration for a given type backed by a given map. + * + * @param type the configuration class, cannot be null; + * @param config the configuration to wrap, cannot be null. + * @return an instance of the given type that wraps the given configuration. + */ + public static T create(Class type, Map config) { + ClassLoader cl = type.getClassLoader(); + Object result = Proxy.newProxyInstance(cl, new Class[] { type }, new ConfigHandler(type, cl, config)); + return type.cast(result); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ConfigurationDependencyImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ConfigurationDependencyImpl.java new file mode 100644 index 00000000000..98ec4c02184 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ConfigurationDependencyImpl.java @@ -0,0 +1,474 @@ +/* + * 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.dm.impl; + +import static org.apache.felix.dm.impl.ServiceUtil.toR6Dictionary; + +import java.util.Arrays; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Objects; +import java.util.Properties; +import java.util.stream.Stream; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ConfigurationDependency; +import org.apache.felix.dm.Logger; +import org.apache.felix.dm.PropertyMetaData; +import org.apache.felix.dm.context.AbstractDependency; +import org.apache.felix.dm.context.DependencyContext; +import org.apache.felix.dm.context.Event; +import org.apache.felix.dm.context.EventType; +import org.apache.felix.dm.impl.metatype.MetaTypeProviderImpl; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedService; +import org.osgi.service.metatype.MetaTypeProvider; + +/** + * Implementation for a configuration dependency. + * + * @author Felix Project Team + */ +public class ConfigurationDependencyImpl extends AbstractDependency implements ConfigurationDependency, ManagedService { + private volatile Dictionary m_settings; + private volatile String m_pid; + private ServiceRegistration m_registration; + private volatile Class[] m_configTypes; + private volatile MetaTypeProviderImpl m_metaType; + private boolean m_mayInvokeUpdateCallback; + private final Logger m_logger; + private final BundleContext m_context; + private volatile boolean m_needsInstance = true; + private volatile boolean m_optional; + private volatile boolean m_needsInstanceCalled; + + public ConfigurationDependencyImpl() { + this(null, null); + } + + public ConfigurationDependencyImpl(BundleContext context, Logger logger) { + m_context = context; + m_logger = logger; + setRequired(true); + setCallback("updated"); + } + + public ConfigurationDependencyImpl(ConfigurationDependencyImpl prototype) { + super(prototype); + m_context = prototype.m_context; + m_pid = prototype.m_pid; + m_logger = prototype.m_logger; + m_metaType = prototype.m_metaType != null ? new MetaTypeProviderImpl(prototype.m_metaType, this, null) : null; + m_needsInstance = prototype.needsInstance(); + m_configTypes = prototype.m_configTypes; + } + + @Override + public ConfigurationDependencyImpl setRequired(boolean required) { + m_optional = ! required; + super.setRequired(true); // always required + return this; + } + + @Override + public Class getAutoConfigType() { + return null; // we don't support auto config mode. + } + + @Override + public DependencyContext createCopy() { + return new ConfigurationDependencyImpl(this); + } + + /** + * Sets a callback method invoked on the instantiated component. + */ + public ConfigurationDependencyImpl setCallback(String callback) { + super.setCallbacks(callback, null); + return this; + } + + /** + * Sets a callback method on an external callback instance object. + * The component is not yet instantiated at the time the callback is invoked. + * We check if callback instance is null, in this case, the callback will be invoked on the instantiated component. + */ + public ConfigurationDependencyImpl setCallback(Object instance, String callback) { + boolean needsInstantiatedComponent = (m_needsInstanceCalled) ? m_needsInstance : (instance == null); + return setCallback(instance, callback, needsInstantiatedComponent); + } + + /** + * Sets a callback method on an external callback instance object. + * If needsInstance == true, the component is instantiated at the time the callback is invoked. + * We check if callback instance is null, in this case, the callback will be invoked on the instantiated component. + */ + public ConfigurationDependencyImpl setCallback(Object instance, String callback, boolean needsInstance) { + super.setCallbacks(instance, callback, null); + needsInstance(needsInstance); + return this; + } + + /** + * Sets a type-safe callback method invoked on the instantiated component. + */ + public ConfigurationDependency setCallback(String callback, Class configType) { + Objects.nonNull(configType); + setCallback(callback); + m_configTypes = configType == null ? null : new Class[] { configType }; + m_pid = (m_pid == null) ? configType.getName() : m_pid; + return this; + } + + /** + * Sets a type-safe callback method on an external callback instance object. + * The component is not yet instantiated at the time the callback is invoked. + */ + public ConfigurationDependency setCallback(Object instance, String callback, Class configType) { + Objects.nonNull(configType); + setCallback(instance, callback); + m_configTypes = configType == null ? null : new Class[] { configType }; + m_pid = (m_pid == null) ? configType.getName() : m_pid; + return this; + } + + /** + * Sets a type-safe callback method on an external callback instance object. + * If needsInstance == true, the component is instantiated at the time the callback is invoked. + */ + public ConfigurationDependencyImpl setCallback(Object instance, String callback, Class configType, boolean needsInstance) { + setCallback(instance, callback, needsInstance); + m_configTypes = configType == null ? null : new Class[] { configType }; + return this; + } + + /** + * Specifies if the component instance must be started when this dependency is started. True by default. + */ + @Override + public ConfigurationDependencyImpl needsInstance(boolean needsInstance) { + m_needsInstance = needsInstance; + m_needsInstanceCalled = true; + return this; + } + + @Override + public ConfigurationDependencyImpl setConfigType(Class ... configTypes) { + m_configTypes = configTypes; + return this; + } + + /** + * This method indicates to ComponentImpl if the component must be instantiated when this Dependency is started. + * If the callback has to be invoked on the component instance, then the component + * instance must be instantiated at the time the Dependency is started because when "CM" calls ConfigurationDependencyImpl.updated() + * callback, then at this point we have to synchronously delegate the callback to the component instance, and re-throw to CM + * any exceptions (if any) thrown by the component instance updated callback. + */ + @Override + public boolean needsInstance() { + return m_needsInstance; + } + + @Override + public void start() { + BundleContext context = m_component.getBundleContext(); + if (context != null) { // If null, we are in a test environment + Properties props = new Properties(); + props.put(Constants.SERVICE_PID, m_pid); + ManagedService ms = this; + if (m_metaType != null) { + ms = m_metaType; + props.put(MetaTypeProvider.METATYPE_PID, m_pid); + String[] ifaces = new String[] { ManagedService.class.getName(), MetaTypeProvider.class.getName() }; + m_registration = context.registerService(ifaces, ms, toR6Dictionary(props)); + } else { + m_registration = context.registerService(ManagedService.class.getName(), ms, toR6Dictionary(props)); + } + } + super.start(); + } + + @Override + public void stop() { + if (m_registration != null) { + try { + m_registration.unregister(); + } catch (IllegalStateException e) {} + m_registration = null; + } + super.stop(); + } + + public ConfigurationDependency setPid(String pid) { + ensureNotActive(); + m_pid = pid; + return this; + } + + @Override + public String getSimpleName() { + return m_pid; + } + + @Override + public String getFilter() { + return null; + } + + public String getType() { + return "configuration"; + } + + public ConfigurationDependency add(PropertyMetaData properties) + { + createMetaTypeImpl(); + m_metaType.add(properties); + return this; + } + + public ConfigurationDependency setDescription(String description) + { + createMetaTypeImpl(); + m_metaType.setDescription(description); + return this; + } + + public ConfigurationDependency setHeading(String heading) + { + createMetaTypeImpl(); + m_metaType.setName(heading); + return this; + } + + public ConfigurationDependency setLocalization(String path) + { + createMetaTypeImpl(); + m_metaType.setLocalization(path); + return this; + } + + @SuppressWarnings("unchecked") + @Override + public Dictionary getProperties() { + if (m_settings == null) { + throw new IllegalStateException("cannot find configuration"); + } + return m_settings; + } + + @SuppressWarnings("rawtypes") + @Override + public void updated(Dictionary settings) throws ConfigurationException { + // Handle the update in the component executor thread. Any exception thrown during the component updated callback will be + // synchronously awaited and re-thrown to the CM thread. + // We schedule the update in the component executor in order to avoid race conditions, + // like when the component is stopping while we receive a configuration update, or if the component restarts + // while the configuration is being updated, or if the getProperties method is invoked while we are losing the configuration ... + // there are many racy situations, and the safe way to handle them is to schedule the updated callback in the component executor. + InvocationUtil.invokeUpdated(m_component.getExecutor(), () -> doUpdated(settings)); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void doUpdated(Dictionary settings) throws Exception { + // Reset the flag that tells if the callback can be invoked. + m_mayInvokeUpdateCallback = true; + Dictionary oldSettings = m_settings; + + // FELIX-5192: we have to handle the following race condition: one thread stops a component (removes it from a DM object); + // another thread removes the configuration (from ConfigurationAdmin). in this case we may be called in our + // ManagedService.updated(null), but our component instance has been destroyed and does not exist anymore. + // In this case: do nothing. + if (! super.isStarted()) { + return; + } + + if (settings == null && m_optional) { + // Provide a default empty configuration + settings = new Hashtable<>(); + settings.put(Constants.SERVICE_PID, m_pid); + } + + if (oldSettings == null && settings == null) { + // CM has started but our configuration is not still present in the CM database. + return; + } + + // If this is initial settings, or a configuration update, we handle it synchronously. + // We'll conclude that the dependency is available only if invoking updated did not cause + // any ConfigurationException. + invokeUpdated(settings); + + // At this point, we have accepted the configuration. + m_settings = settings; + + if ((oldSettings == null) && (settings != null)) { + // Notify the component that our dependency is available. + m_component.handleEvent(this, EventType.ADDED, new ConfigurationEventImpl(m_pid, settings)); + } + else if ((oldSettings != null) && (settings != null)) { + // Notify the component that our dependency has changed. + m_component.handleEvent(this, EventType.CHANGED, new ConfigurationEventImpl(m_pid, settings)); + } + else if ((oldSettings != null) && (settings == null)) { + // Notify the component that our dependency has been removed. + // Notice that the component will be stopped, and then all required dependencies will be unbound + // (including our configuration dependency). + m_component.handleEvent(this, EventType.REMOVED, new ConfigurationEventImpl(m_pid, oldSettings)); + } + } + + @Override + public void invokeCallback(EventType type, Event ... event) { + switch (type) { + case ADDED: + try { + // Won't invoke if we already invoked from the doUpdate method. + // The case when we really invoke may happen when the component is stopped , then restarted. + // At this point, we have to re-invoke the component updated callback. + invokeUpdated(((ConfigurationEventImpl) event[0]).getProperties()); + } catch (Throwable err) { + logConfigurationException(err); + } + break; + case CHANGED: + // We already did that synchronously, from our doUpdated method + break; + case REMOVED: + // The state machine is stopping us. Reset for the next time the state machine calls invokeCallback(ADDED) + m_mayInvokeUpdateCallback = true; + break; + default: + break; + } + } + + private static T[] concat(T first, T[] second) { + T[] result = Arrays.copyOf(second, second.length + 1); + result[0] = first; + System.arraycopy(second, 0, result, 1, second.length); + return result; + } + + private static T[] concat(T first, T second, T[] third) { + T[] result = Arrays.copyOf(third, third.length + 2); + result[0] = first; + result[1] = second; + System.arraycopy(third, 0, result, 2, third.length); + return result; + } + + static CallbackTypeDef createCallbackType(Logger logger, Component service, Class[] configTypes, Dictionary settings) { + Class[][] sigs = new Class[][] { { Dictionary.class }, { Component.class, Dictionary.class }, {} }; + Object[][] args = new Object[][] { { settings }, { service, settings }, {} }; + + if (configTypes != null && configTypes.length > 0 && configTypes[0] != null) { + try { + // if the configuration is null, it means we are losing it, and since we pass a null dictionary for other callback + // (that accepts a Dictionary), then we should have the same behavior and also pass a null conf proxy object when + // the configuration is lost. + Dictionary declaredServiceProperties = ServiceUtil.toR6Dictionary(EMPTY_PROPERTIES); + if (service instanceof ComponentImpl) { + declaredServiceProperties = ((ComponentImpl) service).getDeclaredServiceProperties(); + } + Object[] configurables = new Object[configTypes.length]; + for (int i = 0 ; i < configTypes.length; i ++) { + configurables[i] = settings != null ? Configurable.create(configTypes[i], settings, declaredServiceProperties) : null; + logger.debug("Using configuration-type injecting using %s as possible configType.", configTypes[i].getSimpleName()); + } + + sigs = new Class[][] { + { Dictionary.class }, + { Component.class, Dictionary.class }, + concat(Component.class, configTypes), + configTypes , + concat(Dictionary.class, configTypes), + concat(Component.class, Dictionary.class, configTypes), + {} + }; + args = new Object[][] { + { settings }, + { service, settings }, + concat(service, configurables), + configurables, + concat(settings, configurables), + concat(service, settings, configurables), + {} + }; + } + catch (Exception e) { + // This is not something we can recover from, use the defaults above... + logger.warn("Failed to create configurable for configuration type %s!", e, configTypes != null ? Arrays.toString(configTypes) : null); + } + } + + return new CallbackTypeDef(sigs, args); + } + + // Called from the configuration component internal queue. + private void invokeUpdated(Dictionary settings) throws Exception { + if (m_mayInvokeUpdateCallback) { + m_mayInvokeUpdateCallback = false; + + // FELIX-5155: if component impl is an internal DM adapter, we must not invoke the callback on it + // because in case there is an external callback instance specified for the configuration callback, + // then we don't want to invoke it now. The external callback instance will be invoked + // on the other actual configuration dependency copied into the actual component instance created by the + // adapter. + + Object mainComponentInstance = m_component.getInstance(); + if (mainComponentInstance instanceof AbstractDecorator || m_component.injectionDisabled()) { + return; + } + + Object[] instances = super.getInstances(); // never null, either the callback instance or the component instances + + CallbackTypeDef callbackInfo = createCallbackType(m_logger, m_component, m_configTypes, settings); + boolean callbackFound = false; + for (int i = 0; i < instances.length; i++) { + try { + // Only inject if the component instance is not a prototype instance + InvocationUtil.invokeCallbackMethod(instances[i], m_add, callbackInfo.m_sigs, callbackInfo.m_args); + callbackFound |= true; + } + catch (NoSuchMethodException e) { + // if the method does not exist, ignore it + } + } + + if (! callbackFound) { + String[] instanceClasses = Stream.of(instances).map(c -> c.getClass().getName()).toArray(String[]::new); + m_logger.log(Logger.LOG_ERROR, "\"" + m_add + "\" configuration callback not found in any of the component classes: " + Arrays.toString(instanceClasses)); + } + } + } + + private synchronized void createMetaTypeImpl() { + if (m_metaType == null) { + m_metaType = new MetaTypeProviderImpl(m_pid, m_context, m_logger, this, null); + } + } + + private void logConfigurationException(Throwable err) { + m_logger.log(Logger.LOG_ERROR, "Got exception while handling configuration update for pid " + m_pid, err); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ConfigurationEventImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ConfigurationEventImpl.java new file mode 100644 index 00000000000..ac2c7ce0d50 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ConfigurationEventImpl.java @@ -0,0 +1,62 @@ +/* + * 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.dm.impl; + +import java.util.Dictionary; + +import org.apache.felix.dm.context.Event; + +/** + * Implementation for a configuration event. + * + * @author Felix Project Team + */ +public class ConfigurationEventImpl extends Event { + private final String m_pid; + + public ConfigurationEventImpl(String pid, Dictionary conf) { + super(conf); + m_pid = pid; + } + + public String getPid() { + return m_pid; + } + + @Override + public int compareTo(Event other) { + return m_pid.compareTo(((ConfigurationEventImpl) other).m_pid); + } + + @Override + public int hashCode() { + return m_pid.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return m_pid.equals(((ConfigurationEventImpl) obj).m_pid); + } + + @SuppressWarnings("unchecked") + @Override + public Dictionary getProperties() { + return getEvent(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/DefaultNullObject.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/DefaultNullObject.java new file mode 100644 index 00000000000..eede17d8c66 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/DefaultNullObject.java @@ -0,0 +1,71 @@ +/* + * 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.dm.impl; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; + + +/** + * Default null object implementation. Uses a dynamic proxy. Null objects are used + * as placeholders for services that are not available. + * + * @author Felix Project Team + */ +public final class DefaultNullObject implements InvocationHandler { + private static final Boolean DEFAULT_BOOLEAN = Boolean.FALSE; + private static final Byte DEFAULT_BYTE = new Byte((byte) 0); + private static final Short DEFAULT_SHORT = new Short((short) 0); + private static final Integer DEFAULT_INT = new Integer(0); + private static final Long DEFAULT_LONG = new Long(0); + private static final Float DEFAULT_FLOAT = new Float(0.0f); + private static final Double DEFAULT_DOUBLE = new Double(0.0); + + /** + * Invokes a method on this null object. The method will return a default + * value without doing anything. + */ + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Class returnType = method.getReturnType(); + if (returnType.equals(Boolean.class) || returnType.equals(Boolean.TYPE)) { + return DEFAULT_BOOLEAN; + } + else if (returnType.equals(Byte.class) || returnType.equals(Byte.TYPE)) { + return DEFAULT_BYTE; + } + else if (returnType.equals(Short.class) || returnType.equals(Short.TYPE)) { + return DEFAULT_SHORT; + } + else if (returnType.equals(Integer.class) || returnType.equals(Integer.TYPE)) { + return DEFAULT_INT; + } + else if (returnType.equals(Long.class) || returnType.equals(Long.TYPE)) { + return DEFAULT_LONG; + } + else if (returnType.equals(Float.class) || returnType.equals(Float.TYPE)) { + return DEFAULT_FLOAT; + } + else if (returnType.equals(Double.class) || returnType.equals(Double.TYPE)) { + return DEFAULT_DOUBLE; + } + else { + return null; + } + } +} \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/DispatchExecutor.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/DispatchExecutor.java new file mode 100644 index 00000000000..a7802313701 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/DispatchExecutor.java @@ -0,0 +1,187 @@ +/* + * 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.dm.impl; + +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.felix.dm.Logger; +import org.osgi.service.log.LogService; + +/** + * A DispatchExecutor is a queue that can execute FIFO tasks in a shared threadpool configured for the dispatcher. + * Each task scheduled in a given DispatchExecutor will be executed serially in FIFO order; and multiple + * DispatchExecutor instances may each run concurrently with respect to each other. + *

      + * + * This class also supports synchronous scheduling, like the @link {@link SerialExecutor} class; and in this case, + * only one caller thread will execute the tasks scheduled in the DispatchQueue (and the internal + * threadpool won't be used). + * + *

      + * + * This class is lock free by design and ensures "safe object publication" between scheduling threads and + * actual executing thread: if one thread T1 schedules a task, but another thread T2 actually + * executes it, then all the objects from the T1 thread will be "safely published" to the executing T2 thread. + * Safe publication is ensured because we are using a ConcurrentLinkedQueue, and volatile attributes. + * (see [1], chapter 3.5.3 (Safe publication idioms). + * + * [1] Java Concurrency In Practice, Addison Wesley + * + * @author Felix Project Team + */ +public class DispatchExecutor implements Executor, Runnable { + /** + * The threadpool used for the execution of the tasks that are scheduled in this queue. + */ + private final Executor m_threadPool; + + /** + * List of tasks scheduled in our queue. + */ + protected final ConcurrentLinkedQueue m_tasks = new ConcurrentLinkedQueue<>(); + + /** + * Marker used to remember the id of the thread currently executing this dispatch queue. + */ + private volatile Thread m_executingThread; + + /** + * Flag telling if this dispatch queue is already scheduled for execution in the threadpool. + */ + private final AtomicBoolean m_scheduled = new AtomicBoolean(); + + /** + * Logger used to log exceptions thrown by scheduled tasks. + */ + private final Logger m_logger; + + /** + * Creates a new DispatchQueue, which can be executed within a fixed thread pool. Multiple queue + * can be executed concurrently, but all runnables scheduled in a given queue will be executed serially, + * in FIFO order. + * + * @param threadPool the executor (typically a threadpool) used to execute this DispatchExecutor. + * @param logger the Logger used when errors are taking place + */ + public DispatchExecutor(Executor threadPool, Logger logger) { + m_logger = logger; + m_threadPool = threadPool; + } + + /** + * Enqueues a task for later execution. You must call {@link #execute()} in order + * to trigger the actual execution of all scheduled tasks (in FIFO order). + */ + public void schedule(Runnable task) { + m_tasks.add(task); + } + + /** + * Submits a task in this queue, and schedule the execution of this DispatchQueue in the threadpool. + * The task is immediately executed (inline execution) if the queue is currently being executed by + * the current thread. + */ + public void execute(Runnable task) { + execute(task, true); + } + + /** + * Schedules a task in this queue. + * If the queue is currently being executed by the current thread, then the task is executed immediately. + * @tasks the task to schedule + * @threadpool true if the queue should be executed in the threadpool, false if the queue must be executed by + * only one caller thread. + */ + public void execute(Runnable task, boolean threadpool) { + Thread currThread = Thread.currentThread(); + if (m_executingThread == currThread) { + runTask(task); + } else { + schedule(task); + execute(threadpool); + } + } + + /** + * Schedules the execution of this DispatchQueue in the threadpool. + */ + public void execute() { + execute(true); + } + + /** + * Schedules the execution of this DispatchQueue in the threadpool, or from a single caller thread. + * + * @param threadpool true means the DispatchQueue is executed in the threadpool, false means the queue is executed from the + * caller thread. + */ + public void execute(boolean threadpool) { + if (m_scheduled.compareAndSet(false, true)) { // schedules our run method in the tpool. + try { + if (threadpool) { + m_threadPool.execute(this); + } else { + run(); // run all queue tasks from the caller thread + } + } catch (RejectedExecutionException e) { + // The threadpool seems stopped (maybe the framework is being stopped). Anyway, just execute our tasks + // from the current thread. + run(); + } + } + } + + /** + * Run all tasks scheduled in this queue, in FIFO order. This method may be executed either in the threadpool, or from + * the caller thread. + */ + @Override + public void run() { + try { + // We do a memory barrier in order to ensure consistent per-thread + // memory visibility + m_executingThread = Thread.currentThread(); + Runnable task; + while ((task = m_tasks.poll()) != null) { + runTask(task); + } + } finally { + m_scheduled.set(false); + m_executingThread = null; + if (m_tasks.peek() != null) { + execute(); + } + } + } + + /** + * Runs a given task + * @param task the task to execute + */ + private void runTask(Runnable task) { + try { + task.run(); + } catch (Throwable t) { + m_logger.log(LogService.LOG_ERROR, "Error processing tasks", t); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/FactoryConfigurationAdapterImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/FactoryConfigurationAdapterImpl.java new file mode 100644 index 00000000000..17f9a33067c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/FactoryConfigurationAdapterImpl.java @@ -0,0 +1,396 @@ +/* + * 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.dm.impl; + +import static org.apache.felix.dm.impl.ConfigurationDependencyImpl.createCallbackType; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.List; +import java.util.stream.Stream; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ConfigurationDependency; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.FactoryComponent; +import org.apache.felix.dm.Logger; +import org.apache.felix.dm.PropertyMetaData; +import org.apache.felix.dm.context.ComponentContext; +import org.apache.felix.dm.impl.metatype.MetaTypeProviderImpl; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.service.cm.ManagedServiceFactory; +import org.osgi.service.metatype.MetaTypeProvider; +import org.osgi.service.metatype.ObjectClassDefinition; + +/** + * Factory configuration adapter service implementation. This class extends the FilterService in order to catch + * some Service methods for configuring actual adapter service implementation. + * + * @author Felix Project Team + */ +public class FactoryConfigurationAdapterImpl extends FilterComponent implements FactoryComponent { + /** + * The pid matching the factory configuration. + */ + private volatile String m_factoryPid; + + /** + * The adapter method name that will be notified when the factory configuration is created/updated. + */ + private volatile String m_update = "updated"; + + /** + * true if public factory configuration should be propagated to the adapter service properties. + */ + private volatile boolean m_propagate; + + /** + * The object on which the updated callback will be invoked. + */ + private volatile Object m_callbackInstance; + + /** + * the configuration type's) to use instead of a dictionary. See the javadoc from {@link ConfigurationDependency} for + */ + private volatile Class[] m_configTypes; + + /** + * The label used to display the tab name (or section) where the properties are displayed. + */ + private volatile String m_heading; + + /** + * A human readable description of the factory PID this configuration is associated with. + */ + private volatile String m_desc; + + /** + * Points to the basename of the Properties file that can localize the Meta Type informations. + */ + private volatile String m_localization; + + /** + * Array of MetaData regarding configuration properties. + */ + private volatile List m_propertiesMetaData = new ArrayList<>(0); + + // Our logger + protected final Logger m_logger; + + public FactoryConfigurationAdapterImpl(DependencyManager dm) { + super(dm.createComponent()); + m_logger = dm.getLogger(); + } + + /** + * Sets the pid matching the factory configuration + * @param factoryPid the pid matching the factory configuration + */ + public FactoryConfigurationAdapterImpl setFactoryPid(String factoryPid) { + this.m_factoryPid = factoryPid; + return this; + } + + /** + * Sets the pid matching the factory configuration using the specified class. + * The FQDN of the specified class will be used as the class name. + * @param m_factoryPid the pid matching the factory configuration + */ + public FactoryConfigurationAdapterImpl setFactoryPid(Class clazz) { + this.m_factoryPid = clazz.getName(); + return this; + } + + /** + * Sets the method name that will be notified when the factory configuration is created/updated. + * By default, the callback name used is updated + * @TODO describe supported signatures + * @param update the method name that will be notified when the factory configuration is created/updated. + */ + public FactoryConfigurationAdapterImpl setUpdated(String update) { + this.m_update = update; + return this; + } + + /** + * Sets the propagate flag (true means all public configuration properties are propagated to service properties). + * By default, public configurations are not propagated. + * @param propagate the propagate flag (true means all public configuration properties are propagated to service properties). + */ + public FactoryConfigurationAdapterImpl setPropagate(boolean propagate) { + this.m_propagate = propagate; + return this; + } + + /** + * Sets the object on which the updated callback will be invoked. + * By default, the callback is invoked on the component instance. + * @param callbackInstance the object on which the updated callback will be invoked. + */ + public FactoryConfigurationAdapterImpl setUpdateInstance(Object callbackInstance) { + this.m_callbackInstance = callbackInstance; + return this; + } + + /** + * Sets the configuration type to use instead of a dictionary. The updated callback is assumed to take + * as arguments the specified configuration types in the same order they are provided in this method. + * @param configTypes the configuration type to use instead of a dictionary + * @see ConfigurationDependency + */ + public FactoryConfigurationAdapterImpl setConfigType(Class ... configTypes) { + this.m_configTypes = configTypes; + return this; + } + + /** + * Sets the label used to display the tab name (or section) where the properties are displayed. + * Example: "Printer Service" + * @param heading the label used to display the tab name (or section) where the properties are displayed. + */ + public FactoryConfigurationAdapterImpl setHeading(String heading) { + this.m_heading = heading; + return this; + } + + /** + * A human readable description of the factory PID this configuration is associated with. + * @param desc + */ + public FactoryConfigurationAdapterImpl setDesc(String desc) { + this.m_desc = desc; + return this; + } + + /** + * Points to the basename of the Properties file that can localize the Meta Type informations. + * The default localization base name for the properties is OSGI-INF/l10n/bundle, but can + * be overridden by the manifest Bundle-Localization header (see core specification, in section Localization + * on page 68). You can specify a specific localization basename file using this parameter + * (e.g. "person" will match person_du_NL.properties in the root bundle directory). + * @param localization + */ + public FactoryConfigurationAdapterImpl setLocalization(String localization) { + this.m_localization = localization; + return this; + } + + /** + * Sets MetaData regarding configuration properties. + * @param metaData the metadata regarding configuration properties + */ + public FactoryConfigurationAdapterImpl add(PropertyMetaData ... metaData) { + Stream.of(metaData).forEach(m_propertiesMetaData::add); + return this; + } + + @Override + protected void startInitial() { + Hashtable props = new Hashtable<>(); + props.put(Constants.SERVICE_PID, m_factoryPid); + + if (m_propertiesMetaData.size() == 0) { + m_component + .setInterface(ManagedServiceFactory.class.getName(), props) + .setImplementation(new AdapterImpl()) + .setCallbacks("init", null, "stop", null); + } else { + String[] ifaces = { ManagedServiceFactory.class.getName(), MetaTypeProvider.class.getName() }; + props.put(MetaTypeProvider.METATYPE_FACTORY_PID, m_factoryPid); + m_component + .setInterface(ifaces, props) + .setImplementation(new MetaTypeAdapterImpl()) + .setCallbacks("init", null, "stop", null); + } + } + + public String getName() { + return "Adapter for factory pid " + m_factoryPid; + } + + /** + * Creates, updates, or removes a service, when a ConfigAdmin factory configuration is created/updated or deleted. + */ + public class AdapterImpl extends AbstractDecorator implements ManagedServiceFactory { + + /** + * Returns the managed service factory name. + */ + public String getName() { + return m_factoryPid; + } + + /** + * Method called from our superclass, when we need to create a service. + */ + @SuppressWarnings("unchecked") + public Component createService(Object[] properties) throws Exception { + Dictionary settings = (Dictionary) properties[0]; + Component newService = m_manager.createComponent(); + + // Merge adapter service properties, with CM settings + Dictionary serviceProperties = getServiceProperties(settings); + newService.setInterface(m_serviceInterfaces, serviceProperties); + newService.setImplementation(m_serviceImpl); + newService.setFactory(m_factory, m_factoryCreateMethod); // if not set, no effect + newService.setComposition(m_compositionInstance, m_compositionMethod); // if not set, no effect + newService.setCallbacks(m_callbackObject, m_init, m_start, m_stop, m_destroy); // if not set, no effect + newService.setScope(m_scope); + configureAutoConfigState(newService, m_component); + + copyDependencies(m_component.getDependencies(), newService); + + for (int i = 0; i < m_stateListeners.size(); i ++) { + newService.add(m_stateListeners.get(i)); + } + + // Instantiate the component, because we need to invoke the updated callback synchronously, in the CM calling thread. + ((ComponentContext) newService).instantiateComponent(); + + CallbackTypeDef callbackInfo = createCallbackType(m_logger, newService, m_configTypes, settings); + invokeUpdated(newService, callbackInfo); + + return newService; + } + + /** + * Method called from our superclass, when we need to update a Service, because + * the configuration has changed. + */ + @SuppressWarnings("unchecked") + public void updateService(Object[] properties) throws Exception { + Dictionary cmSettings = (Dictionary) properties[0]; + Component service = (Component) properties[1]; + CallbackTypeDef callbackInfo = createCallbackType(m_logger, service, m_configTypes, cmSettings); + + invokeUpdated(service, callbackInfo); + + if (m_serviceInterfaces != null && m_propagate == true) { + Dictionary serviceProperties = getServiceProperties(cmSettings); + service.setServiceProperties(serviceProperties); + } + } + + private void invokeUpdated(Component service, CallbackTypeDef callbackInfo) throws Exception { + if (((ComponentContext) service).injectionDisabled()) { + return; + } + boolean callbackFound = false; + Object[] instances = getUpdateCallbackInstances(service); + for (Object instance : instances) { + try { + // Only inject if the component instance is not a prototype instance + InvocationUtil.invokeCallbackMethod(instance, m_update, callbackInfo.m_sigs, callbackInfo.m_args); + callbackFound |= true; + } + catch (NoSuchMethodException e) { + // if the method does not exist, ignore it + } + } + + if (! callbackFound) { + String[] instanceClasses = Stream.of(instances).map(c -> c.getClass().getName()).toArray(String[]::new); + m_logger.log(Logger.LOG_ERROR, "\"" + m_update + "\" configuration callback not found in any of the component classes: " + Arrays.toString(instanceClasses)); + } + } + + /** + * Returns the Update callback instances. + */ + private Object[] getUpdateCallbackInstances(Component comp) { + if (m_callbackInstance == null) { + return comp.getInstances(); + } else { + return new Object[] { m_callbackInstance }; + } + } + + /** + * Merge CM factory configuration setting with the adapter service properties. The private CM factory configuration + * settings are ignored. A CM factory configuration property is private if its name starts with a dot ("."). + * + * @param adapterProperties + * @param settings + * @return + */ + private Dictionary getServiceProperties(Dictionary settings) { + Dictionary props = new Hashtable<>(); + + // Add adapter Service Properties + if (m_serviceProperties != null) { + Enumeration keys = m_serviceProperties.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + Object val = m_serviceProperties.get(key); + props.put(key, val); + } + } + + if (m_propagate) { + // Add CM setting into adapter service properties. + // (CM setting will override existing adapter service properties). + Enumeration keys = settings.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + if (! key.toString().startsWith(".")) { + // public properties are propagated + Object val = settings.get(key); + props.put(key, val); + } + } + } + + return props; + } + } + + /** + * Extends AdapterImpl for MetaType support (deprecated, now users can directly use bnd metatypes). + */ + class MetaTypeAdapterImpl extends AdapterImpl implements MetaTypeProvider { + // Our MetaType Provider for describing our properties metadata + private final MetaTypeProviderImpl m_metaType; + + public MetaTypeAdapterImpl() { + BundleContext bctx = getBundleContext(); + Logger logger = getLogger(); + m_metaType = new MetaTypeProviderImpl(m_factoryPid, bctx, logger, null, this); + m_metaType.setName(m_heading); + m_metaType.setDescription(m_desc); + if (m_localization != null) { + m_metaType.setLocalization(m_localization); + } + for (PropertyMetaData properyMetaData : m_propertiesMetaData) { + m_metaType.add(properyMetaData); + } + } + + public String[] getLocales() { + return m_metaType.getLocales(); + } + + public ObjectClassDefinition getObjectClassDefinition(String id, String locale) { + return m_metaType.getObjectClassDefinition(id, locale); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/FieldUtil.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/FieldUtil.java new file mode 100644 index 00000000000..ba92677e803 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/FieldUtil.java @@ -0,0 +1,352 @@ +/* + * 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.dm.impl; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Dictionary; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.apache.felix.dm.Logger; +import org.apache.felix.dm.context.DependencyContext; +import org.apache.felix.dm.context.Event; + +/** + * Reflection Helper methods, used to inject autoconfig fields in component instances. + * + * @author Felix Project Team + */ +public class FieldUtil { + /** + * Callbacks for fields to be injected + */ + private interface FieldFunction { + // Inject an updated service in the given field for the the given target. + void injectField(Field f, Object target); + + // Inject an Iterable Field in the given target + void injectIterableField(Field f, Object target); + + // Inject a Map field in the given target (key = dependency service, value = Dictionary with dependency service properties). + void injectMapField(Field f, Object target); + } + + /** + * Injects some component instances (on a given field, if provided), with an object of a given class. + * @param targets the component instances to fill in + * @param fieldName the fieldname, or null. If null, the field must exaclty match the injected service classname. + * @param clazz the injected service class + * @param service the injected service + * @param logger the component logger. + */ + public static boolean injectField(Object[] targets, String fieldName, Class clazz, final Object service, + final Logger logger) + { + if (service == null) { + return true; // TODO why service can be null ? + } + return mapField(true, clazz, targets, fieldName, logger, new FieldFunction() { + public void injectField(Field f, Object target) { + try { + f.setAccessible(true); + f.set(target, service); + } catch (Throwable e) { + logger.log(Logger.LOG_ERROR, "Could not set field " + f + " in class " + + target.getClass().getName(), e); + } + } + + public void injectIterableField(Field f, Object target) { // never called + } + + public void injectMapField(Field f, Object target) { // never called + } + }); + } + + /** + * Injects a dependency service in some component instances. + * Here, we'll inject the dependency services in the component if the field is of the same type of the injected services, + * or if the field is a Collection of the injected service, or if the field is a Map clazz, + final DependencyContext dc, final Logger logger) + { + final Event event = dc.getService(); + if (event == null) { + return true; // TODO check why event can be null + } + return mapField(false, clazz, targets, fieldName, logger, new FieldFunction() { + public void injectField(Field f, Object target) { + try { + f.setAccessible(true); + f.set(target, event.getEvent()); + } catch (Throwable e) { + logger.log(Logger.LOG_ERROR, "Could not set field " + f + " in class " + + target.getClass().getName(), e); + } + } + + @SuppressWarnings("unchecked") + public void injectIterableField(Field f, Object target) { + f.setAccessible(true); + + try { + Iterable iter = (Iterable) f.get(target); + if (iter == null) { + iter = new ConcurrentLinkedQueue(); + f.set(target, iter); + } + dc.copyToCollection((Collection) iter); + } catch (Throwable e) { + logger.log(Logger.LOG_ERROR, "Could not set field " + f + " in class " + + target.getClass().getName(), e); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public void injectMapField(Field f, Object target) { + f.setAccessible(true); + try { + Map> map = (Map) f.get(target); + if (map == null) { + map = new ConcurrentHashMap<>(); + f.set(target, map); + } + dc.copyToMap(map); + } catch (Throwable e) { + logger.log(Logger.LOG_ERROR, "Could not set field " + f + " in class " + + target.getClass().getName(), e); + } + } + }); + } + + /** + * Adds, or removes, or update some component instances with an updated dependency service + * @param targets the component instances to fill in with the updated service + * @param fieldName the component instance fieldname + * @param update true if it's a dependency service update, false if the dependency service is added or removed + * @param add true if the dependency service has been added, false the dependency service has been removed. + * This flag is ignored if the "update" parameter is "true". + * @param clazz the clazz of the dependency service + * @param event the event holding the dependency service + * @param dc the dependency service context + * @param logger the logger used when problems occure. + */ + public static void updateDependencyField(Object[] targets, String fieldName, final boolean update, + final boolean add, Class clazz, final Event event, final DependencyContext dc, final Logger logger) + { + mapField(false, clazz, targets, fieldName, logger, new FieldFunction() { + public void injectField(Field f, Object target) { + try { + f.setAccessible(true); + f.set(target, dc.getService().getEvent()); + } catch (Throwable e) { + logger.log(Logger.LOG_ERROR, "Could not set field " + f + " in class " + + target.getClass().getName(), e); + } + } + + @SuppressWarnings("unchecked") + public void injectIterableField(Field f, Object target) { + if (update) { + return; + } + + f.setAccessible(true); + + try { + Collection coll = (Collection) f.get(target); + if (add) { + coll.add(event.getEvent()); + } else { + coll.remove(event.getEvent()); + } + } catch (Throwable e) { + logger.log(Logger.LOG_ERROR, "Could not set field " + f + " in class " + + target.getClass().getName(), e); + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public void injectMapField(Field f, Object target) { + f.setAccessible(true); + + try { + Map> map = (Map) f.get(target); + if (add || update) { + map.put(event.getEvent(), event.getProperties()); + } else { + map.remove(event.getEvent()); + } + } catch (Throwable e) { + logger.log(Logger.LOG_ERROR, "Could not set field " + f + " in class " + + target.getClass().getName(), e); + } + } + }); + } + + /** + * Scans component instances for fields having either the same dependency service type, or being a + * Collection of the dependency service, or being a Map (the Dictionary + * corresponds to the dependency service properties). + * @param strict true if we are only looking for fields having exactly the same type as the dependency service. + * In other words, if strict = true, we don't lookup for fields of "Collection" or "Map" types. + * @param clazz the dependency service class + * @param targets the component instances + * @param fieldName the component instances field name or null + * @param logger a logger used when exceptions are occuring + * @param func the callback used to notify when we find either a field with the same dependency service type, or + * with a "Collection" type, or with a "Map" type. + */ + private static boolean mapField(boolean strict, Class clazz, Object[] targets, String fieldName, Logger logger, + FieldFunction func) + { + boolean injected = false; + if (targets != null && clazz != null) { + for (int i = 0; i < targets.length; i++) { + Object target = targets[i]; + Class targetClass = target.getClass(); + if (Proxy.isProxyClass(targetClass)) { + target = Proxy.getInvocationHandler(target); + targetClass = target.getClass(); + } + while (targetClass != null) { + Field[] fields = targetClass.getDeclaredFields(); + for (int j = 0; j < fields.length; j++) { + Field field = fields[j]; + Class fieldType = field.getType(); + + if (fieldName == null) { + // Field type class must match injected service type + if (fieldType.equals(clazz)) { + injected = true; + func.injectField(field, target); + } else if (!strict && mayInjectToIterable(clazz, field, true)) { + injected = true; + func.injectIterableField(field, target); + } else if (!strict && mayInjectToMap(clazz, field, true)) { + injected = true; + func.injectMapField(field, target); + } + } else if (field.getName().equals(fieldName)) { + // Field type may be a superclass of the service type + if (fieldType.isAssignableFrom(clazz)) { + injected = true; + func.injectField(field, target); + } else if (!strict && mayInjectToIterable(clazz, field, false)) { + injected = true; + func.injectIterableField(field, target); + } else if (!strict && mayInjectToMap(clazz, field, false)) { + injected = true; + func.injectMapField(field, target); + } else { + logger.log( + Logger.LOG_ERROR, + "Could not set field " + field + " in class " + target.getClass().getName() + + ": the type of the field type should be either assignable from " + + clazz.getName() + " or Collection, or Map"); + } + } + } + targetClass = targetClass.getSuperclass(); + } + } + } + return injected; + } + + private static boolean mayInjectToIterable(Class clazz, Field field, boolean strictClassEquality) { + Class fieldType = field.getType(); + if (Iterable.class.isAssignableFrom(fieldType)) { + Type type = field.getGenericType(); + + // The field must be a parameterized map (generics). + if (! (type instanceof ParameterizedType)) { + return false; + } + ParameterizedType parameterType = (ParameterizedType) type; + Type[] types = parameterType.getActualTypeArguments(); + if (types == null || types.length != 1) { + return false; + } + if (types[0] instanceof Class) { + Class parameterizedTypeClass = (Class) types[0]; + return strictClassEquality ? parameterizedTypeClass.equals(clazz) + : parameterizedTypeClass.isAssignableFrom(clazz); + } + } + return false; + } + + private static boolean mayInjectToMap(Class clazz, Field field, boolean strictClassEquality) { + Class fieldType = field.getType(); + if (Map.class.isAssignableFrom(fieldType)) { + Type type = field.getGenericType(); + + // The field must be a parameterized map (generics). + if (! (type instanceof ParameterizedType)) { + return false; + } + ParameterizedType parameterType = (ParameterizedType) type; + Type[] types = parameterType.getActualTypeArguments(); + if (types == null || types.length != 2) { + return false; + } + + // The map field generic key parameter must be "Class". + if (! (types[0] instanceof Class)) { + return false; + } + + // The map generic value parameter must be Dictionary, or Dictionary + if (types[1] instanceof Class) { + // The map field is in the form "Map m_field" + Class mapValueGenericType = (Class) types[1]; + if (! mapValueGenericType.equals(Dictionary.class)) { + return false; + } + } else if (types[1] instanceof ParameterizedType) { + // The map field is in the form "Map m_field" + ParameterizedType mapValueGenericType = (ParameterizedType) types[1]; + if (! mapValueGenericType.getRawType().equals(Dictionary.class)) { + return false; + } + } + + Class K = (Class) types[0]; + return strictClassEquality ? K.equals(clazz) : K.isAssignableFrom(clazz); + } + return false; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/FilterComponent.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/FilterComponent.java new file mode 100644 index 00000000000..cd7ad143147 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/FilterComponent.java @@ -0,0 +1,402 @@ +/* + * 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.dm.impl; + +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentDeclaration; +import org.apache.felix.dm.ComponentDependencyDeclaration; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.Logger; +import org.apache.felix.dm.context.ComponentContext; +import org.apache.felix.dm.context.DependencyContext; +import org.apache.felix.dm.context.Event; +import org.apache.felix.dm.context.EventType; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; + +/** + * This class allows to filter a Component interface. All Aspect/Adapters extend this class + * in order to add functionality to the default Component implementation. + * + * @author Felix Project Team + */ +public abstract class FilterComponent implements Component, ComponentContext, ComponentDeclaration { + protected volatile ComponentImpl m_component; + protected volatile List m_stateListeners = new CopyOnWriteArrayList<>(); + protected volatile String m_init = "init"; + protected volatile String m_start = "start"; + protected volatile String m_stop = "stop"; + protected volatile String m_destroy = "destroy"; + protected volatile Object m_callbackObject; + protected volatile Object m_compositionInstance; + protected volatile String m_compositionMethod; + protected volatile String[] m_serviceInterfaces; + protected volatile Object m_serviceImpl; + protected volatile Object m_factory; + protected volatile String m_factoryCreateMethod; + protected volatile Dictionary m_serviceProperties; + protected volatile ServiceScope m_scope = ServiceScope.SINGLETON; + private boolean m_started; + private final List m_dependencies = new ArrayList<>(); + + public FilterComponent(Component service) { + m_component = (ComponentImpl) service; + } + + @Override + public boolean injectionDisabled() { + return m_component.injectionDisabled(); + } + + @Override + public U createConfigurationType(Class type, Dictionary config) { + return m_component.createConfigurationType(type, config); + } + + @Override + public Executor getExecutor() { + return m_component.getExecutor(); + } + + @Override + public String toString() { + return m_component.toString(); + } + + public T setScope(ServiceScope scope) { + m_scope = scope; + return (T) this; + } + + public T add(Dependency ... dependencies) { + m_component.getExecutor().execute(() -> { + if (! m_started) { + Stream.of(dependencies).forEach(m_dependencies::add); + return; + } + m_component.add(dependencies); + Object instance = m_component.getInstance(); + if (instance instanceof AbstractDecorator) { + AbstractDecorator ad = (AbstractDecorator) instance; + ad.addDependency(dependencies); // will clone the dependencies for each component instance + } + }); + return (T) this; + } + + public T remove(Dependency dependency) { + m_component.getExecutor().execute(() -> { + m_component.remove(dependency); + Object instance = m_component.getInstance(); + if (instance != null && instance instanceof AbstractDecorator) { + ((AbstractDecorator) instance).removeDependency(dependency); // will remove the previously cloned dependency + } + }); + return (T) this; + } + + public T add(ComponentStateListener listener) { + m_component.getExecutor().execute(() -> { + m_stateListeners.add(listener); + // Add the listener to all already instantiated services. + Object instance = m_component.getInstance(); + if (instance instanceof AbstractDecorator) { + ((AbstractDecorator) instance).addStateListener(listener); + } + }); + return (T) this; + } + + public List getDependencies() { + return m_component.getDependencies(); + } + + public String getClassName() { + return m_component.getClassName(); + } + + @SuppressWarnings("unchecked") + public Dictionary getServiceProperties() { + return m_serviceProperties; + } + + public ServiceRegistration getServiceRegistration() { + return m_component.getServiceRegistration(); + } + + public T remove(ComponentStateListener listener) { + m_stateListeners.remove(listener); + // Remove the listener from all already instantiated services. + Object instance = m_component.getInstance(); + if (instance != null && instance instanceof AbstractDecorator) { + ((AbstractDecorator) instance).removeStateListener(listener); + } + return (T) this; + } + + public T setCallbacks(Object instance, String init, String start, String stop, String destroy) { + m_component.ensureNotActive(); + m_callbackObject = instance; + m_init = init; + m_start = start; + m_stop = stop; + m_destroy = destroy; + return (T) this; + } + + public T setCallbacks(String init, String start, String stop, String destroy) { + setCallbacks(null, init, start, stop, destroy); + return (T) this; + } + + public T setComposition(Object instance, String getMethod) { + m_component.ensureNotActive(); + m_compositionInstance = instance; + m_compositionMethod = getMethod; + return (T) this; + } + + public T setComposition(String getMethod) { + m_component.ensureNotActive(); + m_compositionMethod = getMethod; + return (T) this; + } + + public T setFactory(Object factory, String createMethod) { + m_component.ensureNotActive(); + m_factory = factory; + m_factoryCreateMethod = createMethod; + return (T) this; + } + + public T setFactory(String createMethod) { + return setFactory(null, createMethod); + } + + public T setImplementation(Object implementation) { + m_component.ensureNotActive(); + m_serviceImpl = implementation; + return (T) this; + } + + public T setInterface(String serviceName, Dictionary properties) { + return setInterface(new String[] { serviceName }, properties); + } + + @SuppressWarnings("unchecked") + public T setInterface(String[] serviceInterfaces, Dictionary properties) { + m_component.ensureNotActive(); + if (serviceInterfaces != null) { + m_serviceInterfaces = new String[serviceInterfaces.length]; + System.arraycopy(serviceInterfaces, 0, m_serviceInterfaces, 0, serviceInterfaces.length); + m_serviceProperties = (Dictionary) properties; + } + return (T) this; + } + + public T setInterface(Class serviceName, Dictionary properties) { + return setInterface(serviceName.getName(), properties); + } + + public T setInterface(Class[] serviceInterfaces, Dictionary properties) { + String[] ifaces = Stream.of(serviceInterfaces).map(clazz -> clazz.getName()).toArray(String[]::new); + return setInterface(ifaces, properties); + } + + @SuppressWarnings("unchecked") + public T setServiceProperties(Dictionary serviceProperties) { + m_serviceProperties = (Dictionary) serviceProperties; + // Set the properties to all already instantiated services. + if (serviceProperties != null) { + Object instance = m_component.getInstance(); + if (instance instanceof AbstractDecorator) { + ((AbstractDecorator) instance).setServiceProperties(serviceProperties); + } + } + return (T) this; + } + + public void start() { + m_component.getExecutor().execute(() -> { + if (! m_started) { + m_started = true; + // first initialize concrete adapters, which need to add + // their internal dependencies first. + startInitial(); + // Now, add extra dependencies + for (Dependency dep : m_dependencies) { + m_component.add(dep); + } + m_dependencies.clear(); + } + m_component.start(); + }); + } + + protected abstract void startInitial(); + + public void stop() { + m_component.stop(); + } + + public void invokeCallbackMethod(Object[] instances, String methodName, Class[][] signatures, Object[][] parameters) { + m_component.invokeCallbackMethod(instances, methodName, signatures, parameters); + } + + public void invokeCallbackMethod(Object[] instances, String methodName, Class[][] signatures, Object[][] parameters, boolean logIfNotFound) { + m_component.invokeCallbackMethod(instances, methodName, signatures, parameters, logIfNotFound); + } + + public void invokeCallback(Object[] instances, String methodName, Class[][] signatures, Supplier[][] paramsSupplier, boolean logIfNotFound) { + m_component.invokeCallbackMethod(instances, methodName, signatures, paramsSupplier, logIfNotFound); + } + + public DependencyManager getDependencyManager() { + return m_component.getDependencyManager(); + } + + public T setAutoConfig(Class clazz, boolean autoConfig) { + m_component.setAutoConfig(clazz, autoConfig); + return (T) this; + } + + public T setAutoConfig(Class clazz, String instanceName) { + m_component.setAutoConfig(clazz, instanceName); + return (T) this; + } + + public boolean getAutoConfig(Class clazz) { + return m_component.getAutoConfig(clazz); + } + + public String getAutoConfigInstance(Class clazz) { + return m_component.getAutoConfigInstance(clazz); + } + + public ComponentDependencyDeclaration[] getComponentDependencies() { + return m_component.getComponentDependencies(); + } + + public String getName() { + return m_component.getName(); + } + + public int getState() { + return m_component.getState(); + } + + public long getId() { + return m_component.getId(); + } + + public String[] getServices() { + return m_component.getServices(); + } + + public BundleContext getBundleContext() { + return m_component.getBundleContext(); + } + + @Override + public boolean isActive() { + return m_component.isActive(); + } + + @Override + public boolean isAvailable() { + return m_component.isAvailable(); + } + + @Override + public void handleEvent(DependencyContext dc, EventType type, Event ... e) { + m_component.handleEvent(dc, type, e); + } + + @Override + public U getInstance() { + return m_component.getInstance(); + } + + @Override + public Object[] getInstances() { + return m_component.getInstances(); + } + + @Override + public Event getDependencyEvent(DependencyContext dc) { + return m_component.getDependencyEvent(dc); + } + + @Override + public Set getDependencyEvents(DependencyContext dc) { + return m_component.getDependencyEvents(dc); + } + + public ComponentDeclaration getComponentDeclaration() { + return this; + } + + @Override + public T setDebug(String label) { + m_component.setDebug(label); + return (T) this; + } + + @Override + public void setThreadPool(Executor threadPool) { + m_component.setThreadPool(threadPool); + } + + @Override + public Map getCallbacksTime() { + return m_component.getCallbacksTime(); + } + + @Override + public Bundle getBundle() { + return m_component.getBundle(); + } + + @Override + public Logger getLogger() { + return m_component.getLogger(); + } + + protected void copyDependencies(List dependencies, Component component) { + dependencies.stream().map(dc -> dc.createCopy()).forEach(component::add); + } + + @Override + public ComponentContext instantiateComponent() { + return m_component.instantiateComponent(); + } +} \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/InvocationUtil.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/InvocationUtil.java new file mode 100644 index 00000000000..76d3cd031c9 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/InvocationUtil.java @@ -0,0 +1,514 @@ +/* + * 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.dm.impl; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.FutureTask; +import java.util.function.Supplier; + +import org.apache.felix.dm.DependencyManager; +import org.osgi.service.cm.ConfigurationException; + +/** + * Utility methods for invoking callbacks. Lookups of callbacks are accellerated by using a LRU cache. + * + * @author Felix Project Team + */ +public class InvocationUtil { + /** + * Constant Used to get empty constructor by reflection. + */ + private static final Class[] VOID = new Class[] {}; + + private static final Map m_methodCache; + static { + int size = 4096; + try { + String value = System.getProperty(DependencyManager.METHOD_CACHE_SIZE); + if (value != null) { + size = Integer.parseInt(value); + } + } + catch (Exception e) {} + m_methodCache = new LRUMap(Math.max(size, 64)); + } + + /** + * Interface internally used to handle a ConfigurationAdmin update synchronously, in a component executor queue. + */ + @FunctionalInterface + public interface ConfigurationHandler { + public void handle() throws Exception; + } + + /** + * Represents a component instance + */ + public static final class ComponentInstance { + /** + * The component instance + */ + public final Object m_instance; + + /** + * The index of the constructor used when creating the component instance + */ + public final int m_ctorIndex; + + /** + * creates a component instance + * @param instance the component instance + * @param ctorIndex the index of the CallbackTypeDef used when creating the component instance + */ + public ComponentInstance(Object instance, int ctorIndex) { + m_instance = instance; + m_ctorIndex = ctorIndex; + } + } + + /** + * Invokes a callback method on an instance. The code will search for a callback method with + * the supplied name and any of the supplied signatures in order, invoking the first one it finds. + * + * @param instance the instance to invoke the method on + * @param methodName the name of the method + * @param signatures the ordered list of signatures to look for + * @param parameters the parameter values to use for each potential signature + * @return whatever the method returns + * @throws NoSuchMethodException when no method could be found + * @throws IllegalArgumentException when illegal values for this methods arguments are supplied + * @throws IllegalAccessException when the method cannot be accessed + * @throws InvocationTargetException when the method that was invoked throws an exception + */ + public static Object invokeCallbackMethod(Object instance, String methodName, Class[][] signatures, Object[][] parameters) throws NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException { + Class currentClazz = instance.getClass(); + while (currentClazz != null && currentClazz != Object.class) { + try { + return invokeMethod(instance, currentClazz, methodName, signatures, parameters, false); + } + catch (NoSuchMethodException nsme) { + // ignore + } + currentClazz = currentClazz.getSuperclass(); + } + throw new NoSuchMethodException(methodName); + } + + /** + * Invoke a method on an instance. + * + * @param object the instance to invoke the method on + * @param clazz the class of the instance + * @param name the name of the method + * @param signatures the signatures to look for in order + * @param parameters the parameter values for the signatures + * @param isSuper true if this is a superclass and we should therefore not look for private methods + * @return whatever the method returns + * @throws NoSuchMethodException when no method could be found + * @throws IllegalArgumentException when illegal values for this methods arguments are supplied + * @throws IllegalAccessException when the method cannot be accessed + * @throws InvocationTargetException when the method that was invoked throws an exception + */ + public static Object invokeMethod(Object object, Class clazz, String name, Class[][] signatures, Object[][] parameters, boolean isSuper) throws NoSuchMethodException, InvocationTargetException, IllegalArgumentException, IllegalAccessException { + if (object == null) { + throw new IllegalArgumentException("Instance cannot be null"); + } + if (clazz == null) { + throw new IllegalArgumentException("Class cannot be null"); + } + + // if we're talking to a proxy here, dig one level deeper to expose the + // underlying invocation handler (we do the same for injecting instances) + if (Proxy.isProxyClass(clazz)) { + object = Proxy.getInvocationHandler(object); + clazz = object.getClass(); + } + + Method m = null; + for (int i = 0; i < signatures.length; i++) { + Class[] signature = signatures[i]; + m = getDeclaredMethod(clazz, name, signature, isSuper); + if (m != null) { + return m.invoke(object, parameters[i]); + } + } + throw new NoSuchMethodException(name); + } + + /** + * Invokes a callback method on an instance. The code will search for a callback method with + * the supplied name and any of the supplied signatures in order, invoking the first one it finds. + * + * @param instance the instance to invoke the method on + * @param methodName the name of the method + * @param signatures the ordered list of signatures to look for + * @param parameters the parameter values to use for each potential signature + * @return whatever the method returns + * @throws NoSuchMethodException when no method could be found + * @throws IllegalArgumentException when illegal values for this methods arguments are supplied + * @throws IllegalAccessException when the method cannot be accessed + * @throws InvocationTargetException when the method that was invoked throws an exception + */ + public static Object invokeCallbackMethod(Object instance, String methodName, Class[][] signatures, Supplier[][] parameters) throws NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException { + Class currentClazz = instance.getClass(); + while (currentClazz != null && currentClazz != Object.class) { + try { + return invokeMethod(instance, currentClazz, methodName, signatures, parameters, false); + } + catch (NoSuchMethodException nsme) { + // ignore + } + currentClazz = currentClazz.getSuperclass(); + } + throw new NoSuchMethodException(methodName); + } + + /** + * Invoke a method on an instance. + * + * @param object the instance to invoke the method on + * @param clazz the class of the instance + * @param name the name of the method + * @param signatures the signatures to look for in order + * @param paramsSupplier the parameter values for the signatures + * @param isSuper true if this is a superclass and we should therefore not look for private methods + * @return whatever the method returns + * @throws NoSuchMethodException when no method could be found + * @throws IllegalArgumentException when illegal values for this methods arguments are supplied + * @throws IllegalAccessException when the method cannot be accessed + * @throws InvocationTargetException when the method that was invoked throws an exception + */ + public static Object invokeMethod(Object object, Class clazz, String name, Class[][] signatures, Supplier[][] paramsSupplier, boolean isSuper) throws NoSuchMethodException, InvocationTargetException, IllegalArgumentException, IllegalAccessException { + if (object == null) { + throw new IllegalArgumentException("Instance cannot be null"); + } + if (clazz == null) { + throw new IllegalArgumentException("Class cannot be null"); + } + + // if we're talking to a proxy here, dig one level deeper to expose the + // underlying invocation handler (we do the same for injecting instances) + if (Proxy.isProxyClass(clazz)) { + object = Proxy.getInvocationHandler(object); + clazz = object.getClass(); + } + + Method m = null; + for (int i = 0; i < signatures.length; i++) { + Class[] signature = signatures[i]; + m = getDeclaredMethod(clazz, name, signature, isSuper); + if (m != null) { + Object[] params = new Object[paramsSupplier[i].length]; + for (int j = 0; j < params.length; j ++) { + params[j] = paramsSupplier[i][j].get(); + } + return m.invoke(object, params); + } + } + throw new NoSuchMethodException(name); + } + + /** + * Gets a callback method on an instance. The code will search for a callback method with + * the supplied name and any of the supplied signatures in order, get the first one it finds. + * + * @param instance the instance to invoke the method on + * @param methodName the name of the method + * @param signatures the ordered list of signatures to look for + * @return the method found, or null + */ + public static Method getCallbackMethod(Object instance, String methodName, Class[][] signatures) { + Class currentClazz = instance.getClass(); + while (currentClazz != null && currentClazz != Object.class) { + Method m = getMethod(instance, currentClazz, methodName, signatures, false); + if (m != null) { + return m; + } + currentClazz = currentClazz.getSuperclass(); + } + return null; + } + + /** + * Gets a callback method on a class. The code will search for a callback method with + * the supplied name and any of the supplied signatures in order, get the first one it finds. + * + * @param instance the instance to invoke the method on + * @param methodName the name of the method + * @param signatures the ordered list of signatures to look for + * @return the method found, or null + */ + public static Method getCallbackMethod(Class type, String methodName, Class[][] signatures) { + Class currentClazz = type; + while (currentClazz != null && currentClazz != Object.class) { + Method m = getMethod(currentClazz, methodName, signatures, false); + if (m != null) { + return m; + } + currentClazz = currentClazz.getSuperclass(); + } + return null; + } + + /** + * Get a method on an instance. + * TODO: rework this class to avoid code duplication with invokeMethod ! + * + * @param object the instance to invoke the method on + * @param clazz the class of the instance + * @param name the name of the method + * @param signatures the signatures to look for in order + * @param isSuper true if this is a superclass and we should therefore not look for private methods + * @return the found method, or null if not found + */ + public static Method getMethod(Object object, Class clazz, String name, Class[][] signatures, boolean isSuper) { + if (object == null) { + throw new IllegalArgumentException("Instance cannot be null"); + } + if (clazz == null) { + throw new IllegalArgumentException("Class cannot be null"); + } + + // if we're talking to a proxy here, dig one level deeper to expose the + // underlying invocation handler (we do the same for injecting instances) + if (Proxy.isProxyClass(clazz)) { + object = Proxy.getInvocationHandler(object); + clazz = object.getClass(); + } + + return getMethod(clazz, name, signatures, isSuper); + } + + /** + * Get a method on an instance. + * TODO: rework this class to avoid code duplication with invokeMethod ! + * + * @param object the instance to invoke the method on + * @param clazz the class of the instance + * @param name the name of the method + * @param signatures the signatures to look for in order + * @param isSuper true if this is a superclass and we should therefore not look for private methods + * @return the found method, or null if not found + */ + private static Method getMethod(Class clazz, String name, Class[][] signatures, boolean isSuper) { + if (clazz == null) { + throw new IllegalArgumentException("Class cannot be null"); + } + + Method m = null; + for (int i = 0; i < signatures.length; i++) { + Class[] signature = signatures[i]; + m = getDeclaredMethod(clazz, name, signature, isSuper); + if (m != null) { + break; + } + } + return m; + } + + public static ComponentInstance createInstance(Class clazz, CallbackTypeDef ctorArgs) throws Exception { + Constructor[] ctors = clazz.getConstructors(); + for (int index = 0; index < ctorArgs.m_sigs.length; index ++) { + Class[] sigs = ctorArgs.m_sigs[index]; + for (Constructor ctor : ctors) { + Class[] ctorParamTypes = ctor.getParameterTypes(); + if (Arrays.equals(sigs, ctorParamTypes)) { + ctor.setAccessible(true); + Object instance = ctor.newInstance(ctorArgs.m_args[index]); + return new ComponentInstance(instance, index); + } + } + } + throw new InstantiationException("No suitable constructor found for class " + clazz.getName()); + } + + /** + * Instantiates a component. The first public constructor which contains any of the specified arguments is used. + * @param clazz the class name used to instantiate the component + * @param ctorArgs constructor arguments. An empty map means the first public constructor is used. + * @return the component instance + */ + public static Object createInstance(Class clazz, Map, Object> ctorArgs) throws SecurityException, NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + if (ctorArgs.size() == 0) { + Constructor ctor = clazz.getConstructor(VOID); + ctor.setAccessible(true); + return ctor.newInstance(); + } + + Constructor[] ctors = clazz.getConstructors(); + for (Constructor ctor : ctors) { + Class[] ctorParamTypes = ctor.getParameterTypes(); + boolean match = true; + for (Class ctopParamType : ctorParamTypes) { + if (ctorArgs.get(ctopParamType) == null) { + match = false; + break; + } + } + if (match) { + Object[] paramValues = new Object[ctorParamTypes.length]; + int index = 0; + for (Class ctorParamType : ctorParamTypes) { + paramValues[index ++] = ctorArgs.get(ctorParamType); + } + ctor.setAccessible(true); + return ctor.newInstance(paramValues); + } + } + throw new InstantiationException("No suitable constructor found for class " + clazz.getName()); + } + + private static Method getDeclaredMethod(Class clazz, String name, Class[] signature, boolean isSuper) { + // first check our cache + Key key = new Key(clazz, name, signature); + Method m = null; + synchronized (m_methodCache) { + m = (Method) m_methodCache.get(key); + if (m != null) { + return m; + } + else if (m_methodCache.containsKey(key)) { + // the key is in our cache, it just happens to have a null value + return null; + } + } + // then do a lookup + try { + m = clazz.getDeclaredMethod(name, signature); + if (!(isSuper && Modifier.isPrivate(m.getModifiers()))) { + m.setAccessible(true); + } + } + catch (NoSuchMethodException e) { + } + synchronized (m_methodCache) { + m_methodCache.put(key, m); + } + return m; + } + + public static class Key { + private final Class m_clazz; + private final String m_name; + private final Class[] m_signature; + + public Key(Class clazz, String name, Class[] signature) { + m_clazz = clazz; + m_name = name; + m_signature = signature; + } + + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((m_clazz == null) ? 0 : m_clazz.hashCode()); + result = prime * result + ((m_name == null) ? 0 : m_name.hashCode()); + result = prime * result + Arrays.hashCode(m_signature); + return result; + } + + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Key other = (Key) obj; + if (m_clazz == null) { + if (other.m_clazz != null) + return false; + } + else if (!m_clazz.equals(other.m_clazz)) + return false; + if (m_name == null) { + if (other.m_name != null) + return false; + } + else if (!m_name.equals(other.m_name)) + return false; + if (!Arrays.equals(m_signature, other.m_signature)) + return false; + return true; + } + } + + @SuppressWarnings("serial") + public static class LRUMap extends LinkedHashMap { + private final int m_size; + + public LRUMap(int size) { + m_size = size; + } + + protected boolean removeEldestEntry(java.util.Map.Entry eldest) { + return size() > m_size; + } + } + + /** + * Invokes a configuration update callback synchronously, but through the component executor queue. + */ + public static void invokeUpdated(Executor queue, ConfigurationHandler handler) throws ConfigurationException { + Callable result = () -> { + try { + handler.handle(); + } catch (Exception e) { + return e; + } + return null; + }; + + FutureTask ft = new FutureTask<>(result); + queue.execute(ft); + + try { + Exception err = ft.get(); + if (err != null) { + throw err; + } + } + + catch (ConfigurationException e) { + throw e; + } + + catch (Throwable error) { + Throwable rootCause = error.getCause(); + if (rootCause != null) { + if (rootCause instanceof ConfigurationException) { + throw (ConfigurationException) rootCause; + } + throw new ConfigurationException("", "Configuration update error, unexpected exception.", rootCause); + } else { + throw new ConfigurationException("", "Configuration update error, unexpected exception.", error); + } + } + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ResourceAdapterImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ResourceAdapterImpl.java new file mode 100644 index 00000000000..3fc1b0e16ba --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ResourceAdapterImpl.java @@ -0,0 +1,140 @@ +/* + * 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.dm.impl; + +import java.net.URL; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.List; + +import org.apache.felix.dm.BundleComponent; +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.ResourceComponent; +import org.apache.felix.dm.ResourceDependency; +import org.apache.felix.dm.context.DependencyContext; + +/** + * Resource adapter service implementation. This class extends the FilterService in order to catch + * some Service methods for configuring actual resource adapter service implementation. + * + * @author Felix Project Team + */ +public class ResourceAdapterImpl extends FilterComponent implements ResourceComponent { + private volatile Object m_callbackInstance; + private volatile String m_callbackChanged; + private volatile String m_callbackAdded; + private volatile String m_resourceFilter; + private volatile boolean m_propagate = true; + private volatile Object m_propagateCallbackInstance; + private volatile String m_propagateCallbackMethod; + + public ResourceAdapterImpl(DependencyManager dm) { + super(dm.createComponent()); // This service will be filtered by our super class, allowing us to take control. + } + + public ResourceComponent setResourceFilter(String filter) { + m_resourceFilter = filter; + return this; + } + + public ResourceComponent setPropagate(boolean propagate) { + m_propagate = propagate; + return this; + } + + public ResourceComponent setPropagate(Object propagateCbInstance, String propagateCbMethod) { + m_propagateCallbackInstance = propagateCbInstance; + m_propagateCallbackMethod = propagateCbMethod; + return this; + } + + public ResourceComponent setBundleCallbacks(String add, String change) { + m_callbackAdded = add; + m_callbackChanged = change; + return this; + } + + public ResourceComponent setBundleCallbackInstance(Object callbackInstance) { + m_callbackInstance = callbackInstance; + return this; + } + + @Override + protected void startInitial() { + DependencyManager dm = getDependencyManager(); + m_component.setImplementation(new ResourceAdapterDecorator()) + .add(dm.createResourceDependency() + .setFilter(m_resourceFilter) + .setAutoConfig(false) + .setCallbacks("added", "removed")) + .setCallbacks("init", null, "stop", null); + } + + public String getName() { + return "Resource Adapter" + ((m_resourceFilter != null) ? " with filter " + m_resourceFilter : ""); + } + + public class ResourceAdapterDecorator extends AbstractDecorator { + + public Component createService(Object[] properties) { + URL resource = (URL) properties[0]; + Hashtable props = new Hashtable<>(); + if (m_serviceProperties != null) { + Enumeration e = m_serviceProperties.keys(); + while (e.hasMoreElements()) { + String key = e.nextElement(); + props.put(key, m_serviceProperties.get(key)); + } + } + List dependencies = m_component.getDependencies(); + // the first dependency is always the dependency on the resource, which + // will be replaced with a more specific dependency below + dependencies.remove(0); + ResourceDependency resourceDependency = m_manager.createResourceDependency() + .setResource(resource) + .setCallbacks(m_callbackInstance, m_callbackAdded, m_callbackChanged, null) + .setAutoConfig(m_callbackAdded == null) + .setRequired(true); + if (m_propagateCallbackInstance != null && m_propagateCallbackMethod != null) { + resourceDependency.setPropagate(m_propagateCallbackInstance, m_propagateCallbackMethod); + } else { + resourceDependency.setPropagate(m_propagate); + } + Component service = m_manager.createComponent() + .setInterface(m_serviceInterfaces, props) + .setImplementation(m_serviceImpl) + .setFactory(m_factory, m_factoryCreateMethod) // if not set, no effect + .setComposition(m_compositionInstance, m_compositionMethod) // if not set, no effect + .setCallbacks(m_callbackObject, m_init, m_start, m_stop, m_destroy) // if not set, no effect + .setScope(m_scope) + .add(resourceDependency); + + configureAutoConfigState(service, m_component); + + copyDependencies(dependencies, service); + + for (ComponentStateListener stateListener : m_stateListeners) { + service.add(stateListener); + } + return service; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ResourceDependencyImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ResourceDependencyImpl.java new file mode 100644 index 00000000000..ca36c955c0a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ResourceDependencyImpl.java @@ -0,0 +1,265 @@ +/* + * 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.dm.impl; + +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentDependencyDeclaration; +import org.apache.felix.dm.ResourceDependency; +import org.apache.felix.dm.ResourceHandler; +import org.apache.felix.dm.context.AbstractDependency; +import org.apache.felix.dm.context.DependencyContext; +import org.apache.felix.dm.context.Event; +import org.apache.felix.dm.context.EventType; +import org.osgi.framework.ServiceRegistration; + +/** + * @author Felix Project Team + */ +public class ResourceDependencyImpl extends AbstractDependency implements ResourceDependency, ResourceHandler, ComponentDependencyDeclaration { + private volatile ServiceRegistration m_registration; + private volatile String m_resourceFilter; + private volatile URL m_trackedResource; + + public ResourceDependencyImpl() { + } + + public ResourceDependencyImpl(ResourceDependencyImpl prototype) { + super(prototype); + m_resourceFilter = prototype.m_resourceFilter; + m_trackedResource = prototype.m_trackedResource; + } + + @Override + public DependencyContext createCopy() { + return new ResourceDependencyImpl(this); + } + + @Override + public void start() { + Dictionary props = null; + if (m_trackedResource != null) { + props = new Hashtable<>(); + props.put(ResourceHandler.URL, m_trackedResource); + } else { + if (m_resourceFilter != null) { + props = new Hashtable<>(); + props.put(ResourceHandler.FILTER, m_resourceFilter); + } + } + m_registration = m_component.getBundleContext().registerService(ResourceHandler.class.getName(), this, props); + super.start(); + } + + @Override + public void stop() { + m_registration.unregister(); + m_registration = null; + super.stop(); + } + + public void added(URL resource) { + if (m_trackedResource == null || m_trackedResource.equals(resource)) { + getComponentContext().handleEvent(this, EventType.ADDED, new ResourceEventImpl(resource, null)); + } + } + + public void added(URL resource, Dictionary resourceProperties) { + if (m_trackedResource == null || m_trackedResource.equals(resource)) { + getComponentContext().handleEvent(this, EventType.ADDED, new ResourceEventImpl(resource, resourceProperties)); + } + } + + public void changed(URL resource) { + if (m_trackedResource == null || m_trackedResource.equals(resource)) { + m_component.handleEvent(this, EventType.CHANGED, new ResourceEventImpl(resource, null)); + } + } + + public void changed(URL resource, Dictionary resourceProperties) { + if (m_trackedResource == null || m_trackedResource.equals(resource)) { + m_component.handleEvent(this, EventType.CHANGED, new ResourceEventImpl(resource, resourceProperties)); + } + } + + public void removed(URL resource) { + if (m_trackedResource == null || m_trackedResource.equals(resource)) { + m_component.handleEvent(this, EventType.REMOVED, new ResourceEventImpl(resource, null)); + } + } + + public void removed(URL resource, Dictionary resourceProperties) { + if (m_trackedResource == null || m_trackedResource.equals(resource)) { + m_component.handleEvent(this, EventType.REMOVED, new ResourceEventImpl(resource, resourceProperties)); + } + } + + @Override + public void invokeCallback(EventType type, Event ... e) { + switch (type) { + case ADDED: + if (m_add != null) { + invoke(m_add, e[0]); + } + break; + case CHANGED: + if (m_change != null) { + invoke (m_change, e[0]); + } + break; + case REMOVED: + if (m_remove != null) { + invoke (m_remove, e[0]); + } + break; + default: + break; + } + } + + private void invoke(String method, Event e) { + ResourceEventImpl re = (ResourceEventImpl) e; + URL serviceInstance = re.getResource(); + Dictionary resourceProperties = re.getProperties(); + + m_component.invokeCallbackMethod(getInstances(), method, + new Class[][] { + { Component.class, URL.class, Dictionary.class }, + { Component.class, URL.class }, + { Component.class }, + { URL.class, Dictionary.class }, + { URL.class }, + { Object.class }, + {}}, + new Object[][] { + { m_component, serviceInstance, resourceProperties }, + { m_component, serviceInstance }, + { m_component }, + { serviceInstance, resourceProperties }, + { serviceInstance }, + { serviceInstance }, + {}} + ); + + } + + public ResourceDependency setResource(URL resource) { + m_trackedResource = resource; + return this; + } + + public ResourceDependency setFilter(String resourceFilter) { + ensureNotActive(); + m_resourceFilter = resourceFilter; + return this; + } + + public ResourceDependency setFilter(String resourceFilter, String resourcePropertiesFilter) { + ensureNotActive(); + m_resourceFilter = resourceFilter; + return this; + } + + @Override + public Class getAutoConfigType() { + return URL.class; + } + + @SuppressWarnings("unchecked") + public Dictionary getProperties() { + ResourceEventImpl re = (ResourceEventImpl) m_component.getDependencyEvent(this); + if (re != null) { + URL resource = re.getResource(); + Dictionary resourceProperties = re.getProperties(); + if (m_propagateCallbackInstance != null && m_propagateCallbackMethod != null) { + try { + CallbackTypeDef callbackInfo = new CallbackTypeDef(URL.class, resource); + return (Dictionary) InvocationUtil.invokeCallbackMethod(m_propagateCallbackInstance, m_propagateCallbackMethod, callbackInfo.m_sigs, callbackInfo.m_args); + } + catch (InvocationTargetException e) { + m_component.getLogger().warn("Exception while invoking callback method", e.getCause()); + } + catch (Throwable e) { + m_component.getLogger().warn("Exception while trying to invoke callback method", e); + } + throw new IllegalStateException("Could not invoke callback"); + } + else { + Hashtable props = new Hashtable<>(); + props.put(ResourceHandler.HOST, resource.getHost()); + props.put(ResourceHandler.PATH, resource.getPath()); + props.put(ResourceHandler.PROTOCOL, resource.getProtocol()); + props.put(ResourceHandler.PORT, Integer.toString(resource.getPort())); + // add the custom resource properties + if (resourceProperties != null) { + Enumeration properyKeysEnum = resourceProperties.keys(); + while (properyKeysEnum.hasMoreElements()) { + String key = properyKeysEnum.nextElement(); + if (!key.equals(ResourceHandler.HOST) && + !key.equals(ResourceHandler.PATH) && + !key.equals(ResourceHandler.PROTOCOL) && + !key.equals(ResourceHandler.PORT)) { + props.put(key, resourceProperties.get(key).toString()); + } else { + m_component.getLogger().warn( + "Custom resource property is overlapping with the default resource property for key: %s", + key); + } + } + } + return props; + } + } + else { + throw new IllegalStateException("cannot find resource"); + } + } + + @Override + public String getName() { + StringBuilder sb = new StringBuilder(); + if (m_trackedResource != null) { + sb.append(m_trackedResource.toString()); + } + if (m_resourceFilter != null) { + sb.append(m_resourceFilter); + } + return sb.toString(); + } + + @Override + public String getSimpleName() { + return m_trackedResource != null ? m_trackedResource.toString() : null; + } + + @Override + public String getFilter() { + return m_resourceFilter; + } + + @Override + public String getType() { + return "resource"; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ResourceEventImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ResourceEventImpl.java new file mode 100644 index 00000000000..b655bedc3d2 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ResourceEventImpl.java @@ -0,0 +1,87 @@ +/* + * 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.dm.impl; + +import java.net.URL; +import java.util.Dictionary; + +import org.apache.felix.dm.context.Event; + +/** + * @author Felix Project Team + */ +public class ResourceEventImpl extends Event { + final Dictionary m_resourceProperties; + + @SuppressWarnings("unchecked") + public ResourceEventImpl(URL resource, Dictionary resourceProperties) { + super(resource); + m_resourceProperties = (Dictionary) resourceProperties; + } + + @SuppressWarnings("unchecked") + @Override + public Dictionary getProperties() { + return (Dictionary) ((Dictionary) m_resourceProperties == null ? EMPTY_PROPERTIES : m_resourceProperties); + } + + public URL getResource() { + return getEvent(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ResourceEventImpl) { + ResourceEventImpl r1 = this; + ResourceEventImpl r2 = (ResourceEventImpl) obj; + boolean match = r1.getResource().equals(r2.getResource()); + if (match) { + Dictionary d1 = getProperties(); + Dictionary d2 = r2.getProperties(); + + if (d1 == null) { + return d2 == null ? match : false; + } + else { + return d1.equals(d2); + } + } + } + return false; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + getResource().hashCode(); + result = prime * result + ((getProperties() == null) ? 0 : getProperties().hashCode()); + return result; + } + + @Override + public int compareTo(Event that) { + if (this.equals(that)) { + return 0; + } + + // Sort by resource name. + return getResource().toString().compareTo(((ResourceEventImpl) that).getResource().toString()); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/SerialExecutor.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/SerialExecutor.java new file mode 100644 index 00000000000..9ef6c9fcc99 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/SerialExecutor.java @@ -0,0 +1,153 @@ +/* + * 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.dm.impl; + +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.felix.dm.Logger; +import org.osgi.service.log.LogService; + +/** + * Allows you to enqueue tasks from multiple threads and then execute + * them on one thread sequentially. It assumes no more than one thread will + * try to execute the tasks and it will make an effort to pick the first + * task that comes along whilst making sure subsequent tasks return + * without waiting.

      + * + * This class is lock free by design and ensures "safe object publication" between scheduling threads and + * actual executing thread: if one thread T1 schedules a task, but another thread T2 actually + * executes it, then all the objects from the T1 thread will be "safely published" to the executing T2 thread. + * Safe publication is ensured because we are using a ConcurrentLinkedQueue. + * (see [1], chapter 3.5.3 (Safe publication idioms). + * + * [1] Java Concurrency In Practice, Addison Wesley + * + * @author Felix Project Team + */ +public class SerialExecutor implements Executor { + /** + * All tasks scheduled are stored there and only one thread may run them. + **/ + protected final ConcurrentLinkedQueue m_tasks = new ConcurrentLinkedQueue(); + + /** + * Thread currently executing the task queue. + **/ + protected final AtomicReference m_runningThread = new AtomicReference<>(); + + /** + * Logger used when a task execution throws an exception + **/ + private final Logger m_logger; + + /** + * Makes a new SerialExecutor + * @param logger used when a task execution throws an exception. Can be null if no exception should be logger. + */ + public SerialExecutor(Logger logger) { + m_logger = logger; + } + + /** + * Enqueues a task for later execution. You must call {@link #execute()} in order + * to trigger the task execution, which may or may not be executed by + * your current thread. + */ + public void schedule(Runnable task) { + m_tasks.add(task); // No need to synchronize, m_tasks is a concurrent linked queue. + } + + /** + * Executes any pending tasks, enqueued using the {@link SerialExecutor#schedule(Runnable)} method. + * This method is thread safe, so multiple threads can try to execute the pending + * tasks, but only the first will be used to actually do so. Other threads will return immediately. + */ + public void execute() { + Thread currentThread = Thread.currentThread(); + if (m_runningThread.compareAndSet(null, currentThread)) { + runTasks(currentThread); + } + } + + /** + * Schedules a task for execution, and then attempts to execute it. This method is thread safe, so + * multiple threads can try to execute a task but only the first will be executed, other threads will + * return immediately, and the first thread will execute the tasks scheduled by the other threads.

      + *

      + * This method is reentrant: if the current thread is currently being executed by this executor, then + * the task passed to this method will be executed immediately, from the current invoking thread + * (inline execution). + */ + public void execute(Runnable task) { + Thread currentThread = Thread.currentThread(); + if (m_runningThread.get() == currentThread) { + runTask(task); + } else { + schedule(task); + execute(); + } + } + + /** + * Run all pending tasks + * @param currentRunninghread the current executing thread + */ + private void runTasks(Thread currentRunninghread) { + do { + try { + Runnable task; + ConcurrentLinkedQueue tasks = m_tasks; + + while ((task = tasks.poll()) != null) { + runTask(task); + } + } + finally { + m_runningThread.set(null); + } + } + // We must test again if some tasks have been scheduled after our "while" loop above, but before the + // m_runningThread reference has been reset to null. + while (!m_tasks.isEmpty() && m_runningThread.compareAndSet(null, currentRunninghread)); + } + + /** + * Run a given task. + * @param task the task to execute. + */ + void runTask(Runnable command) { + try { + command.run(); + } + catch (Throwable t) { + if (m_logger != null) { + m_logger.log(LogService.LOG_ERROR, "Error processing tasks", t); + } else { + t.printStackTrace(); + } + } + } + + @Override + public String toString() { + return "[Executor: queue size: " + m_tasks.size() + "]"; + } +} \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ServiceDependencyImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ServiceDependencyImpl.java new file mode 100644 index 00000000000..01408e67097 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ServiceDependencyImpl.java @@ -0,0 +1,607 @@ +/* + * 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.dm.impl; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Dictionary; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentDeclaration; +import org.apache.felix.dm.ServiceDependency; +import org.apache.felix.dm.context.AbstractDependency; +import org.apache.felix.dm.context.DependencyContext; +import org.apache.felix.dm.context.Event; +import org.apache.felix.dm.context.EventType; +import org.apache.felix.dm.tracker.ServiceTracker; +import org.apache.felix.dm.tracker.ServiceTrackerCustomizer; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceObjects; +import org.osgi.framework.ServiceReference; + +/** + * @author Felix Project Team + */ +public class ServiceDependencyImpl extends AbstractDependency implements ServiceDependency, ServiceTrackerCustomizer { + protected volatile ServiceTracker m_tracker; + protected volatile String m_swap; + protected volatile Class m_trackedServiceName; + private volatile String m_trackedServiceFilter; + private volatile String m_trackedServiceFilterUnmodified; + private volatile ServiceReference m_trackedServiceReference; + private volatile Object m_defaultImplementation; + private volatile Object m_defaultImplementationInstance; + private volatile Object m_nullObject; + private volatile boolean m_debug = false; + private volatile String m_debugKey; + private volatile long m_trackedServiceReferenceId; + private volatile boolean m_obtainServiceBeforeInjection = true; + + public ServiceDependency setDebug(String debugKey) { + m_debugKey = debugKey; + m_debug = true; + return this; + } + + /** + * Entry to wrap service properties behind a Map. + */ + private static final class ServicePropertiesMapEntry implements Map.Entry { + private final String m_key; + private Object m_value; + + public ServicePropertiesMapEntry(String key, Object value) { + m_key = key; + m_value = value; + } + + public String getKey() { + return m_key; + } + + public Object getValue() { + return m_value; + } + + public String toString() { + return m_key + "=" + m_value; + } + + public Object setValue(Object value) { + Object oldValue = m_value; + m_value = value; + return oldValue; + } + + @SuppressWarnings("unchecked") + public boolean equals(Object o) { + if (!(o instanceof Map.Entry)) { + return false; + } + Map.Entry e = (Map.Entry) o; + return eq(m_key, e.getKey()) && eq(m_value, e.getValue()); + } + + public int hashCode() { + return ((m_key == null) ? 0 : m_key.hashCode()) ^ ((m_value == null) ? 0 : m_value.hashCode()); + } + + private static final boolean eq(Object o1, Object o2) { + return (o1 == null ? o2 == null : o1.equals(o2)); + } + } + + /** + * Wraps service properties behind a Map. + */ + private final static class ServicePropertiesMap extends AbstractMap { + private final ServiceReference m_ref; + + public ServicePropertiesMap(ServiceReference ref) { + m_ref = ref; + } + + public Object get(Object key) { + return m_ref.getProperty(key.toString()); + } + + public int size() { + return m_ref.getPropertyKeys().length; + } + + public Set> entrySet() { + Set> set = new HashSet<>(); + String[] keys = m_ref.getPropertyKeys(); + for (int i = 0; i < keys.length; i++) { + set.add(new ServicePropertiesMapEntry(keys[i], m_ref.getProperty(keys[i]))); + } + return set; + } + } + + public ServiceDependencyImpl() { + } + + public ServiceDependencyImpl(ServiceDependencyImpl prototype) { + super(prototype); + m_trackedServiceName = prototype.m_trackedServiceName; + m_nullObject = prototype.m_nullObject; + m_trackedServiceFilter = prototype.m_trackedServiceFilter; + m_trackedServiceFilterUnmodified = prototype.m_trackedServiceFilterUnmodified; + m_trackedServiceReference = prototype.m_trackedServiceReference; + m_autoConfigInstance = prototype.m_autoConfigInstance; + m_defaultImplementation = prototype.m_defaultImplementation; + m_autoConfig = prototype.m_autoConfig; + m_obtainServiceBeforeInjection = prototype.m_obtainServiceBeforeInjection; + } + + // --- CREATION + + public ServiceDependency setCallbacks(Object instance, String added, String changed, String removed, String swapped) { + setCallbacks(instance, added, changed, removed); + m_swap = swapped; + return this; + } + + public ServiceDependency setCallbacks(String added, String changed, String removed, String swapped) { + setCallbacks(added, changed, removed); + m_swap = swapped; + return this; + } + + @Override + public ServiceDependency setDefaultImplementation(Object implementation) { + ensureNotActive(); + m_defaultImplementation = implementation; + return this; + } + + @Override + public ServiceDependency setService(Class serviceName) { + setService(serviceName, null, null); + return this; + } + + public ServiceDependency setService(Class serviceName, String serviceFilter) { + setService(serviceName, null, serviceFilter); + return this; + } + + public ServiceDependency setService(String serviceFilter) { + if (serviceFilter == null) { + throw new IllegalArgumentException("Service filter cannot be null."); + } + setService(null, null, serviceFilter); + return this; + } + + @SuppressWarnings("rawtypes") + public ServiceDependency setService(Class serviceName, ServiceReference serviceReference) { + setService(serviceName, serviceReference, null); + return this; + } + + @Override + public ServiceDependency setDereference(boolean obtainServiceBeforeInjection) { + m_obtainServiceBeforeInjection = obtainServiceBeforeInjection; + return this; + } + + @Override + public void start() { + if (m_trackedServiceName != null) { + BundleContext ctx = m_component.getBundleContext(); + if (m_trackedServiceFilter != null) { + try { + m_tracker = new ServiceTracker(ctx, ctx.createFilter(m_trackedServiceFilter), this); + } catch (InvalidSyntaxException e) { + throw new IllegalStateException("Invalid filter definition for dependency: " + + m_trackedServiceFilter); + } + } else if (m_trackedServiceReference != null) { + m_tracker = new ServiceTracker(ctx, m_trackedServiceReference, this); + } else { + m_tracker = new ServiceTracker(ctx, m_trackedServiceName.getName(), this); + } + } else { + throw new IllegalStateException("Could not create tracker for dependency, no service name specified."); + } + if (m_debug) { + m_tracker.setDebug(m_debugKey); + } + m_tracker.open(); + super.start(); + } + + @Override + public void stop() { + m_tracker.close(); + m_tracker = null; + super.stop(); + } + + @Override + public Object addingService(@SuppressWarnings("rawtypes") ServiceReference reference) { + try { + ServiceEventImpl event = new ServiceEventImpl(m_component, reference, null); + if (obtainServiceBeforeInjecting()) { + Object service = event.getEvent(); // will dereference the service object. + if (service == null) { + // service concurrently removed, ignore + return null; + } + } + return event; + } catch (IllegalStateException e) { + // most likely our bundle is being stopped. Only log an exception if our component is enabled. + if (m_component.isActive()) { + m_component.getLogger().warn("could not handle service dependency for component %s", e, + m_component.getComponentDeclaration().getClassName()); + } + return null; + } + } + + @Override + public void addedService(@SuppressWarnings("rawtypes") ServiceReference reference, Object event) { + ServiceEventImpl evt = (ServiceEventImpl) event; + if (m_debug) { + System.out.println(m_debugKey + " addedService: ref=" + reference); + } + m_component.handleEvent(this, EventType.ADDED, evt); + } + + @Override + public void modifiedService(@SuppressWarnings("rawtypes") ServiceReference reference, Object event) { + ServiceEventImpl evt = (ServiceEventImpl) event; + m_component.handleEvent(this, EventType.CHANGED, evt); + } + + @Override + public void removedService(@SuppressWarnings("rawtypes") ServiceReference reference, Object event) { + ServiceEventImpl evt = (ServiceEventImpl) event; + m_component.handleEvent(this, EventType.REMOVED, evt); + } + + @SuppressWarnings("rawtypes") + @Override + public void swappedService(final ServiceReference reference, final Object service, final ServiceReference newReference, final Object newService) { + ServiceEventImpl evt = (ServiceEventImpl) service; + ServiceEventImpl newEvt = (ServiceEventImpl) newService; + + if (obtainServiceBeforeInjecting()) { + try { + newEvt.getEvent(); + } catch (IllegalStateException e) { + // most likely our bundle is being stopped. Only log an exception if our component is enabled. + if (m_component.isActive()) { + m_component.getLogger().warn("could not handle service dependency for component %s", e, + m_component.getComponentDeclaration().getClassName()); + } + return; + } + } + + if (m_swap != null) { + // it will not trigger a state change, but the actual swap should be scheduled to prevent things + // getting out of order. + // We delegate the swap handling to the ComponentImpl, which is the class responsible for state management. + // The ComponentImpl will first check if the component is in the proper state so the swap method can be invoked. + m_component.handleEvent(this, EventType.SWAPPED, evt, newEvt); + } else { + addedService(newReference, newService); + removedService(reference, service); + } + } + + @Override + public void invokeCallback(EventType type, Event ... events) { + switch (type) { + case ADDED: + if (m_add != null) { + invoke (m_add, events[0], getInstances()); + } + break; + case CHANGED: + if (m_change != null) { + invoke (m_change, events[0], getInstances()); + } + break; + case REMOVED: + if (m_remove != null) { + invoke (m_remove, events[0], getInstances()); + } + break; + case SWAPPED: + if (m_swap != null) { + ServiceEventImpl oldEvent = (ServiceEventImpl) events[0]; + ServiceEventImpl newEvent = (ServiceEventImpl) events[1]; + invokeSwap(m_swap, oldEvent, newEvent, getInstances()); + } + break; + } + } + + @Override + public Class getAutoConfigType() { + return m_trackedServiceName; + } + + @Override + public DependencyContext createCopy() { + return new ServiceDependencyImpl(this); + } + + @Override + public String getName() { + StringBuilder sb = new StringBuilder(); + if (m_trackedServiceName != null) { + sb.append(m_trackedServiceName.getName()); + if (m_trackedServiceFilterUnmodified != null) { + sb.append(' '); + sb.append(m_trackedServiceFilterUnmodified); + } + } + if (m_trackedServiceReference != null) { + sb.append("{service.id=" + m_trackedServiceReference.getProperty(Constants.SERVICE_ID) + "}"); + } + return sb.toString(); + } + + @Override + public String getSimpleName() { + if (m_trackedServiceName != null) { + return m_trackedServiceName.getName(); + } + return null; + } + + @Override + public String getFilter() { + if (m_trackedServiceFilterUnmodified != null) { + return m_trackedServiceFilterUnmodified; + } else if (m_trackedServiceReference != null) { + return new StringBuilder("(").append(Constants.SERVICE_ID).append("=").append( + String.valueOf(m_trackedServiceReferenceId)).append(")").toString(); + } else { + return null; + } + } + + @Override + public String getType() { + return "service"; + } + + @SuppressWarnings("unchecked") + @Override + public Dictionary getProperties() { + ServiceEventImpl se = (ServiceEventImpl) m_component.getDependencyEvent(this); + if (se != null) { + if (m_propagateCallbackInstance != null && m_propagateCallbackMethod != null) { + try { + CallbackTypeDef callbackInfo = new CallbackTypeDef(new Class[][] { { ServiceReference.class, Object.class }, { ServiceReference.class } }, + new Object[][] { { se.getReference(), se.getEvent() }, { se.getReference() } }); + return (Dictionary) InvocationUtil.invokeCallbackMethod(m_propagateCallbackInstance, m_propagateCallbackMethod, callbackInfo.m_sigs, callbackInfo.m_args); + } catch (InvocationTargetException e) { + m_component.getLogger().warn("Exception while invoking callback method", e.getCause()); + } catch (Throwable e) { + m_component.getLogger().warn("Exception while trying to invoke callback method", e); + } + throw new IllegalStateException("Could not invoke callback"); + } else { + Hashtable props = new Hashtable<>(); + String[] keys = se.getReference().getPropertyKeys(); + for (int i = 0; i < keys.length; i++) { + if (!(keys[i].equals(Constants.SERVICE_ID) || keys[i].equals(Constants.SERVICE_PID))) { + props.put(keys[i], se.getReference().getProperty(keys[i])); + } + } + return props; + } + } else { + throw new IllegalStateException("cannot find service reference"); + } + } + + /** Internal method to set the name, service reference and/or filter. */ + private void setService(Class serviceName, ServiceReference serviceReference, String serviceFilter) { + ensureNotActive(); + if (serviceName == null) { + m_trackedServiceName = Object.class; + } + else { + m_trackedServiceName = serviceName; + } + if (serviceFilter != null) { + m_trackedServiceFilterUnmodified = serviceFilter; + if (serviceName == null) { + m_trackedServiceFilter = serviceFilter; + } + else { + m_trackedServiceFilter = "(&(" + Constants.OBJECTCLASS + "=" + serviceName.getName() + ")" + serviceFilter + ")"; + } + } + else { + m_trackedServiceFilterUnmodified = null; + m_trackedServiceFilter = null; + } + if (serviceReference != null) { + m_trackedServiceReference = serviceReference; + if (serviceFilter != null) { + throw new IllegalArgumentException("Cannot specify both a filter and a service reference."); + } + m_trackedServiceReferenceId = (Long) m_trackedServiceReference.getProperty(Constants.SERVICE_ID); + } + else { + m_trackedServiceReference = null; + } + } + + @Override + public Object getDefaultService(boolean nullObject) { + Object service = null; + if (isAutoConfig()) { + service = getDefaultImplementation(); + if (service == null && nullObject) { + service = getNullObject(); + } + } + return service; + } + + private Object getNullObject() { + if (m_nullObject == null) { + Class trackedServiceName; + trackedServiceName = m_trackedServiceName; + try { + m_nullObject = Proxy.newProxyInstance(trackedServiceName.getClassLoader(), + new Class[] { trackedServiceName }, new DefaultNullObject()); + } + catch (Throwable err) { + m_component.getLogger().err("Could not create null object for %s.", err, trackedServiceName); + } + } + return m_nullObject; + } + + private Object getDefaultImplementation() { + if (m_defaultImplementation != null) { + if (m_defaultImplementation instanceof Class) { + try { + m_defaultImplementationInstance = ((Class) m_defaultImplementation).newInstance(); + } + catch (Throwable e) { + m_component.getLogger().err("Could not create default implementation instance of class %s.", e, + m_defaultImplementation); + } + } + else { + m_defaultImplementationInstance = m_defaultImplementation; + } + } + return m_defaultImplementationInstance; + } + + public void invoke(String method, Event e, Object[] instances) { + ServiceEventImpl se = (ServiceEventImpl) e; + // Be as lazy as possible, in case no properties or map has to be injected + Supplier> properties = () -> se.getProperties(); + Supplier propertiesMap = () -> new ServicePropertiesMap(se.getReference()); + + m_component.invokeCallback(instances, method, + new Class[][]{ + {Component.class, ServiceReference.class, m_trackedServiceName}, + {Component.class, ServiceReference.class, Object.class}, + {Component.class, ServiceReference.class}, + {Component.class, m_trackedServiceName}, + {Component.class, Object.class}, + {Component.class}, + {Component.class, Map.class, m_trackedServiceName}, + {ServiceReference.class, m_trackedServiceName}, + {ServiceReference.class, Object.class}, + {ServiceReference.class}, + {m_trackedServiceName}, + {m_trackedServiceName, Map.class}, + {Map.class, m_trackedServiceName}, + {m_trackedServiceName, Dictionary.class}, + {Dictionary.class, m_trackedServiceName}, + {Object.class}, + {ServiceObjects.class}, + {}}, + + new Supplier[][]{ + new Supplier[] {() -> m_component, () -> se.getReference(), () -> se.getEvent()}, + new Supplier[] {() -> m_component, () -> se.getReference(), () -> se.getEvent()}, + new Supplier[] {() -> m_component, () -> se.getReference()}, + new Supplier[] {() -> m_component, () -> se.getEvent()}, + new Supplier[] {() -> m_component, () -> se.getEvent()}, + new Supplier[] {() -> m_component}, + new Supplier[] {() -> m_component, () -> propertiesMap.get(), () -> se.getEvent()}, + new Supplier[] {() -> se.getReference(), () -> se.getEvent()}, + new Supplier[] {() -> se.getReference(), () -> se.getEvent()}, + new Supplier[] {() -> se.getReference()}, + new Supplier[] {() -> se.getEvent()}, + new Supplier[] {() -> se.getEvent(), () -> propertiesMap.get()}, + new Supplier[] {() -> propertiesMap.get(), () -> se.getEvent()}, + new Supplier[] {() -> se.getEvent(), () -> properties.get()}, + new Supplier[] {() -> properties.get(), () -> se.getEvent()}, + new Supplier[] {() -> se.getEvent()}, + new Supplier[] {() -> se.getServiceObjects()}, + {}}, + true // log if method is not found + ); + } + + private void invokeSwap(String swapMethod, ServiceEventImpl previous, ServiceEventImpl current, Object[] instances) { + if (m_debug) { + System.out.println("invoke swap: " + swapMethod + " on component " + m_component + ", instances: " + Arrays.toString(instances) + " - " + ((ComponentDeclaration)m_component).getState()); + } + try { + m_component.invokeCallback(instances, swapMethod, + new Class[][]{ + {m_trackedServiceName, m_trackedServiceName}, + {Object.class, Object.class}, + {ServiceReference.class, m_trackedServiceName, ServiceReference.class, m_trackedServiceName}, + {ServiceReference.class, Object.class, ServiceReference.class, Object.class}, + {Component.class, m_trackedServiceName, m_trackedServiceName}, + {Component.class, Object.class, Object.class}, + {Component.class, ServiceReference.class, m_trackedServiceName, ServiceReference.class, m_trackedServiceName}, + {Component.class, ServiceReference.class, Object.class, ServiceReference.class, Object.class}, + {ServiceReference.class, ServiceReference.class}, + {Component.class, ServiceReference.class, ServiceReference.class}, + {ServiceObjects.class, ServiceObjects.class}, + {Component.class, ServiceObjects.class, ServiceObjects.class}, + }, + + new Supplier[][]{ + new Supplier[] {() -> previous.getEvent(), () -> current.getEvent()}, + new Supplier[] {() -> previous.getEvent(), () -> current.getEvent()}, + new Supplier[] {() -> previous.getReference(), () -> previous.getEvent(), () -> current.getReference(), () -> current.getEvent()}, + new Supplier[] {() -> previous.getReference(), () -> previous.getEvent(), () -> current.getReference(), () -> current.getEvent()}, + new Supplier[] {() -> m_component, () -> previous.getEvent(), () -> current.getEvent()}, + new Supplier[] {() -> m_component, () -> previous.getEvent(), () -> current.getEvent()}, + new Supplier[] {() -> m_component, () -> previous.getReference(), () -> previous.getEvent(), () -> current.getReference(), () -> current.getEvent()}, + new Supplier[] {() -> m_component, () -> previous.getReference(), () -> previous.getEvent(), () -> current.getReference(), () -> current.getEvent()}, + new Supplier[] {() -> previous.getReference(), () -> current.getReference()}, + new Supplier[] {() -> m_component, () -> previous.getReference(), () -> current.getReference()}, + new Supplier[] {() -> previous.getServiceObjects(), () -> current.getServiceObjects()}, + new Supplier[] {() -> m_component, () -> previous.getServiceObjects(), () -> current.getServiceObjects()} + }, + + true); // log if not found + } catch (Throwable e) { + m_component.getLogger().err("Could not invoke swap callback", e); + } + } + + private boolean obtainServiceBeforeInjecting() { + return m_obtainServiceBeforeInjection && ! m_component.injectionDisabled(); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ServiceEventImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ServiceEventImpl.java new file mode 100644 index 00000000000..b9385d14a2c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ServiceEventImpl.java @@ -0,0 +1,196 @@ +/* + * 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.dm.impl; + +import java.util.Dictionary; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + +import org.apache.felix.dm.Logger; +import org.apache.felix.dm.context.ComponentContext; +import org.apache.felix.dm.context.Event; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceObjects; +import org.osgi.framework.ServiceReference; + +/** + * An event for a service dependency. + * Not thread safe, but this class is assumed to be used under the protection of the component serial queue. + */ +public class ServiceEventImpl extends Event { + /** + * The service reference on which a service dependency depends on + */ + private final ServiceReference m_reference; + + /** + * The bundle context of the bundle which has created the service dependency. If not null, + * will be used in close method when ugetting the service reference of the dependency. + */ + private final BundleContext m_bundleContext; + + /** + * The Bundle Context ServiceObjects + */ + private final ServiceObjects m_serviceObjects; + + /** + * The bundle which has created the service dependency. + */ + private final Bundle m_bundle; + + /** + * Protects in case close is called twice. + */ + private final AtomicBoolean m_closed = new AtomicBoolean(false); + + /** + * Our logger. + */ + private final Logger m_logger; + + /** + * The actual service. + */ + private volatile Object m_service; + + public ServiceEventImpl(ComponentContext ctx, ServiceReference reference, Object service) { + super(service); + m_service = service; + m_bundle = ctx.getBundle(); + m_bundleContext = ctx.getBundleContext(); + m_serviceObjects = (m_bundleContext != null) ? m_bundleContext.getServiceObjects(reference) : null; + m_reference = reference; + m_logger = ctx.getLogger(); + } + + /** + * Returns the actual service, or null if the service reference is invalid. + * @return the service or null if the service is not available anymore. + */ + @SuppressWarnings("unchecked") + @Override + public T getEvent() { + if (m_service == null) { + try { + Object scope = m_reference.getProperty(Constants.SERVICE_SCOPE); + if (Constants.SCOPE_PROTOTYPE.equals(scope)) { + if (m_serviceObjects != null) { + m_service = m_serviceObjects.getService(); + } + } else { + m_service = m_bundleContext.getService(m_reference); + } + if (m_service == null) { + debug(() -> "Service " + m_reference + " unavailable"); + } + } catch (Exception t) { + error(() -> "Could not get service from service reference " + m_reference, t); + } + } + return (T) m_service; + } + + /** + * Returns the bundle which has declared a service dependency. + */ + public Bundle getBundle() { + return m_bundle; + } + + /** + * Returns the context of the bundle which has declared a service dependency. + */ + public BundleContext getBundleContext() { + return m_bundleContext; + } + + /** + * Returns the reference service dependency. + */ + public ServiceReference getReference() { + return m_reference; + } + + /** + * Returns the reference service object. + */ + public ServiceObjects getServiceObjects() { + return m_serviceObjects; + } + + @SuppressWarnings("unchecked") + @Override + public Dictionary getProperties() { + return ServiceUtil.propertiesToDictionary(m_reference); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ServiceEventImpl) { + return getReference().equals(((ServiceEventImpl) obj).getReference()); + } + return false; + } + + @Override + public int hashCode() { + return getReference().hashCode(); + } + + @Override + public int compareTo(Event b) { + return getReference().compareTo(((ServiceEventImpl) b).getReference()); + } + + @Override + public String toString() { + return getEvent().toString(); + } + + @Override + public void close() { + if (m_closed.compareAndSet(false, true)) { + if (m_service != null) { + try { + Object scope = m_reference.getProperty(Constants.SERVICE_SCOPE); + if (Constants.SCOPE_PROTOTYPE.equals(scope) && m_serviceObjects != null) { + m_serviceObjects.ungetService(m_service); + } else { + m_bundleContext.ungetService(m_reference); + } + } catch (IllegalStateException e) {} + } + } + } + + private void error(Supplier msg, Exception err) { + if (m_logger != null) { + m_logger.err("%s", err, msg.get()); + } + } + + private void debug(Supplier msg) { + if (m_logger != null) { + m_logger.debug("%s", msg.get()); + } + } +} \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ServiceRegistrationImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ServiceRegistrationImpl.java new file mode 100644 index 00000000000..dd0b6a9965c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ServiceRegistrationImpl.java @@ -0,0 +1,99 @@ +/* + * 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.dm.impl; + +import java.util.Dictionary; + +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; + +/** + * A wrapper around a service registration that blocks until the + * service registration is available. + * + * @author Felix Project Team + */ +public final class ServiceRegistrationImpl implements ServiceRegistration { + public static final ServiceRegistrationImpl ILLEGAL_STATE = new ServiceRegistrationImpl(); + private volatile ServiceRegistration m_registration; + + public ServiceRegistrationImpl() { + m_registration = null; + } + + public ServiceReference getReference() { + return ensureRegistration().getReference(); + } + + public void setProperties(Dictionary dictionary) { + ensureRegistration().setProperties(dictionary); + } + + public void unregister() { + ensureRegistration().unregister(); + } + + public boolean equals(Object obj) { + return ensureRegistration().equals(obj); + } + + public int hashCode() { + return ensureRegistration().hashCode(); + } + + public String toString() { + return ensureRegistration().toString(); + } + + private synchronized ServiceRegistration ensureRegistration() { + while (m_registration == null) { + try { + wait(); + } + catch (InterruptedException ie) { + // we were interrupted so hopefully we will now have a + // service registration ready; if not we wait again + } + } + // check if we're in an illegal state and throw an exception + if (ILLEGAL_STATE == m_registration) { + throw new IllegalStateException("Service is not registered."); + } + return m_registration; + } + + /** + * Sets the service registration and notifies all waiting parties. + */ + @SuppressWarnings("unchecked") + void setServiceRegistration(ServiceRegistration registration) { + synchronized (this) { + m_registration = (ServiceRegistration) registration; + notifyAll(); + } + } + + /** + * Sets this wrapper to an illegal state, which will cause all threads + * that are waiting for this service registration to fail. + */ + void setIllegalState() { + setServiceRegistration(ServiceRegistrationImpl.ILLEGAL_STATE); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ServiceUtil.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ServiceUtil.java new file mode 100644 index 00000000000..19554e056bc --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ServiceUtil.java @@ -0,0 +1,306 @@ +/* + * 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.dm.impl; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.List; +import java.util.Set; + +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + +/** + * OSGi service utilities. + * + * @author Felix Project Team + */ +public class ServiceUtil { + /** + * Useful when needing to provide empty service properties. + */ + public final static Dictionary EMPTY_PROPERTIES = new Hashtable<>(); + + /** + * Defines service properties which must not be propagated from the dependencies to component service properties. + */ + public final static Set NOT_PROPAGATABLE_SERVICE_PROPERTIES = + Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + Constants.SERVICE_ID, + Constants.SERVICE_RANKING, + Constants.SERVICE_BUNDLEID, + Constants.SERVICE_SCOPE, + Constants.OBJECTCLASS, + DependencyManager.ASPECT))); + + /** + * Helper method used to convert a dictionary with untyped keys to a dictionary having a String key. + * (this method is useful when converting a Properties object into a compatible Dictionary + * object that is often needed in OSGI R6 API. + */ + @SuppressWarnings("unchecked") + public static Dictionary toR6Dictionary(Dictionary properties) { + return (Dictionary) properties; + } + + /** + * Dump some service properties in a petty form. + */ + public static void appendProperties(StringBuilder result, Dictionary properties) { + if (properties != null && properties.size() > 0) { + result.append("("); + Enumeration enumeration = properties.keys(); + while (enumeration.hasMoreElements()) { + Object key = enumeration.nextElement(); + result.append(key.toString()); + result.append('='); + Object value = properties.get(key); + if (value instanceof String[]) { + String[] values = (String[]) value; + result.append('{'); + for (int i = 0; i < values.length; i++) { + if (i > 0) { + result.append(','); + } + result.append(values[i].toString()); + } + result.append('}'); + } + else { + result.append(value.toString()); + } + if (enumeration.hasMoreElements()) { + result.append(','); + } + } + result.append(")"); + } + } + + /** + * Returns the service ranking of a service, based on its service reference. If + * the service has a property specifying its ranking, that will be returned. If + * not, the default ranking of zero will be returned. + * + * @param ref the service reference to determine the ranking for + * @return the ranking + */ + public static int getRanking(ServiceReference ref) { + return getRankingAsInteger(ref).intValue(); + } + + /** + * Returns the service ranking of a service, based on its service reference. If + * the service has a property specifying its ranking, that will be returned. If + * not, the default ranking of zero will be returned. + * + * @param ref the service reference to determine the ranking for + * @return the ranking + */ + public static Integer getRankingAsInteger(ServiceReference ref) { + Integer rank = (Integer) ref.getProperty(Constants.SERVICE_RANKING); + if (rank != null) { + return rank; + } + return new Integer(0); + } + + /** + * Returns the service ID of a service, based on its service reference. This + * method is aware of service aspects as defined by the dependency manager and + * will return the ID of the orginal service if you give it an aspect. + * + * @param ref the service reference to determine the service ID of + * @return the service ID + */ + public static long getServiceId(ServiceReference ref) { + return getServiceIdAsLong(ref).longValue(); + } + + /** + * Returns the service ID of a service, based on its service reference. This + * method is aware of service aspects as defined by the dependency manager and + * will return the ID of the orginal service if you give it an aspect. + * + * @param ref the service reference to determine the service ID of + * @return the service ID + */ + public static Long getServiceIdAsLong(ServiceReference ref) { + return getServiceIdObject(ref); + } + + public static Long getServiceIdObject(ServiceReference ref) { + Long aid = (Long) ref.getProperty(DependencyManager.ASPECT); + if (aid != null) { + return aid; + } + Long sid = (Long) ref.getProperty(Constants.SERVICE_ID); + if (sid != null) { + return sid; + } + throw new IllegalArgumentException("Invalid service reference, no service ID found"); + } + + /** + * Determines if the service is an aspect as defined by the dependency manager. + * Aspects are defined by a property and this method will check for its presence. + * + * @param ref the service reference + * @return true if it's an aspect, false otherwise + */ + public static boolean isAspect(ServiceReference ref) { + Long aid = (Long) ref.getProperty(DependencyManager.ASPECT); + return (aid != null); + } + + /** + * Converts a service reference to a string, listing both the bundle it was + * registered from and all properties. + * + * @param ref the service reference + * @return a string representation of the service + */ + public static String toString(ServiceReference ref) { + if (ref == null) { + return "ServiceReference[null]"; + } + else { + StringBuffer buf = new StringBuffer(); + Bundle bundle = ref.getBundle(); + if (bundle != null) { + buf.append("ServiceReference["); + buf.append(bundle.getBundleId()); + buf.append("]{"); + } + else { + buf.append("ServiceReference[unregistered]{"); + } + buf.append(propertiesToString(ref, null)); + buf.append("}"); + return buf.toString(); + } + } + + /** + * Converts the properties of a service reference to a string. + * + * @param ref the service reference + * @param exclude a list of properties to exclude, or null to show everything + * @return a string representation of the service properties + */ + public static String propertiesToString(ServiceReference ref, List exclude) { + StringBuffer buf = new StringBuffer(); + String[] keys = ref.getPropertyKeys(); + for (int i = 0; i < keys.length; i++) { + if (i > 0) { + buf.append(','); + } + buf.append(keys[i]); + buf.append('='); + Object val = ref.getProperty(keys[i]); + if (exclude == null || !exclude.contains(val)) { + if (val instanceof String[]) { + String[] valArray = (String[]) val; + StringBuffer valBuf = new StringBuffer(); + valBuf.append('{'); + for (int j = 0; j < valArray.length; j++) { + if (valBuf.length() > 1) { + valBuf.append(','); + } + valBuf.append(valArray[j].toString()); + } + valBuf.append('}'); + buf.append(valBuf); + } + else { + buf.append(val.toString()); + } + } + } + return buf.toString(); + } + + /** + * Wraps ServiceReference properties behind a Dictionary object. + * @param ref the ServiceReference to wrap + * @return a new Dictionary used to wrap the ServiceReference properties + */ + public static Dictionary propertiesToDictionary(final ServiceReference ref) { + return new Dictionary() { + private Dictionary m_wrapper; + + @Override + public int size() { + return getWrapper().size(); + } + + @Override + public boolean isEmpty() { + return getWrapper().isEmpty(); + } + + @Override + public Enumeration keys() { + return getWrapper().keys(); + } + + @Override + public Enumeration elements() { + return getWrapper().elements(); + } + + @Override + public Object get(Object key) { + return ref.getProperty(key.toString()); + } + + @Override + public Object put(String key, Object value) { + throw new UnsupportedOperationException("Unmodified Dictionary."); + } + + @Override + public Object remove(Object key) { + throw new UnsupportedOperationException("Unmodified Dictionary."); + } + + @Override + public String toString() { + return getWrapper().toString(); + } + + private synchronized Dictionary getWrapper() { + if (m_wrapper == null) { + m_wrapper = new Hashtable(); + String[] keys = ref.getPropertyKeys(); + for (String key : keys) { + m_wrapper.put(key, ref.getProperty(key)); + } + } + return m_wrapper; + } + }; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/TemporalServiceDependencyImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/TemporalServiceDependencyImpl.java new file mode 100644 index 00000000000..943a1407d41 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/TemporalServiceDependencyImpl.java @@ -0,0 +1,193 @@ +/* + * 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.dm.impl; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.ServiceDependency; +import org.apache.felix.dm.context.DependencyContext; +import org.apache.felix.dm.context.EventType; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; + +/** + * Temporal Service dependency implementation, used to hide temporary service dependency "outage". + * Only works with a required dependency. + * + * @author Felix Project Team + */ +public class TemporalServiceDependencyImpl extends ServiceDependencyImpl implements ServiceDependency, InvocationHandler { + // Max millis to wait for service availability. + private final long m_timeout; + + // Framework bundle (we use it to detect if the framework is stopping) + private final Bundle m_frameworkBundle; + + // The service proxy, which blocks when service is not available. + private volatile Object m_serviceInstance; + + /** + * Creates a new Temporal Service Dependency. + * + * @param context The bundle context of the bundle which is instantiating this dependency object + * @param logger the logger our Internal logger for logging events. + * @see DependencyActivatorBase#createTemporalServiceDependency() + */ + public TemporalServiceDependencyImpl(BundleContext context, long timeout) { + super.setRequired(true); + if (timeout < 0) { + throw new IllegalArgumentException("Invalid timeout value: " + timeout); + } + m_timeout = timeout; + m_frameworkBundle = context.getBundle(0); + } + + /** + * Creates a clone of an existing temporal service dependency. + */ + public TemporalServiceDependencyImpl(TemporalServiceDependencyImpl prototype) { + super(prototype); + super.setRequired(true); + m_timeout = prototype.m_timeout; + m_frameworkBundle = prototype.m_frameworkBundle; + } + + @Override + public DependencyContext createCopy() { + return new TemporalServiceDependencyImpl(this); + } + + /** + * Sets the required flag which determines if this service is required or not. This method + * just override the superclass method in order to check if the required flag is true + * (optional dependency is not supported by this class). + * + * @param required the required flag, which must be set to true + * @return this service dependency + * @throws IllegalArgumentException if the "required" parameter is not true. + */ + @Override + public ServiceDependency setRequired(boolean required) { + if (! required) { + throw new IllegalArgumentException("A Temporal Service dependency can't be optional"); + } + super.setRequired(required); + return this; + } + + /** + * The ServiceTracker calls us here in order to inform about a service arrival. + */ + @SuppressWarnings("rawtypes") + @Override + public void addedService(ServiceReference ref, Object event) { + // Update our service cache, using the tracker. We do this because the + // just added service might not be the service with the highest rank ... + boolean makeAvailable = false; + synchronized (this) { + if (m_serviceInstance == null) { + m_serviceInstance = Proxy.newProxyInstance(m_trackedServiceName.getClassLoader(), new Class[] { m_trackedServiceName }, this); + makeAvailable = true; + } + } + if (makeAvailable) { + getComponentContext().handleEvent(this, EventType.ADDED, + new ServiceEventImpl(m_component, ref, m_serviceInstance)); + } else { + // This added will possibly unblock our invoke() method (if it's blocked in m_tracker.waitForService method). + } + } + + /** + * The ServiceTracker calls us here when a tracked service properties are modified. + */ + @SuppressWarnings("rawtypes") + @Override + public void modifiedService(ServiceReference ref, Object service) { + // We don't care. + } + + /** + * The ServiceTracker calls us here when a tracked service is lost. + */ + @SuppressWarnings("rawtypes") + @Override + public void removedService(ServiceReference ref, Object event) { + ServiceEventImpl eventImpl = (ServiceEventImpl) event; + + // If we detect that the fwk is stopping, we behave as our superclass. That is: + // the lost dependency has to trigger our service deactivation, since the fwk is stopping + // and the lost dependency won't come up anymore. + if (m_frameworkBundle.getState() == Bundle.STOPPING) { + // Important: Notice that calling "super.removedService() might invoke our service "stop" + // callback, which in turn might invoke the just removed service dependency. In this case, + // our "invoke" method won't use the tracker to get the service dependency (because at this point, + // the tracker has withdrawn its reference to the lost service). So, you will see that the "invoke" + // method will use the "m_cachedService" instead ... + boolean makeUnavailable = false; + synchronized (this) { + if (m_tracker.getService() == null) { + makeUnavailable = true; + } + } + if (makeUnavailable) { + // the event.close method will unget the service. + m_component.handleEvent(this, EventType.REMOVED, new ServiceEventImpl(m_component, ref, m_serviceInstance)); + } + } else { + eventImpl.close(); // will unget the service. + // if there is no available services, the next call to invoke() method will block until another service + // becomes available. Else the next call to invoke() will return that highest ranked available service. + } + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + ServiceEventImpl event = null; + try { + event = (ServiceEventImpl) m_tracker.waitForService(m_timeout); + } catch (InterruptedException e) { + } + + if (event == null) { + throw new IllegalStateException("Service unavailable: " + m_trackedServiceName.getName()); + } + + Object service = event.getEvent(); + if (service == null) { + throw new IllegalStateException("Service unavailable: " + m_trackedServiceName.getName()); + } + + try { + try { + return method.invoke(service, args); + } catch (IllegalAccessException iae) { + method.setAccessible(true); + return method.invoke(service, args); + } + } catch (InvocationTargetException e) { + throw e.getTargetException(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/AbstractFactoryFilterIndex.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/AbstractFactoryFilterIndex.java new file mode 100644 index 00000000000..c839b57c8cd --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/AbstractFactoryFilterIndex.java @@ -0,0 +1,102 @@ +/* + * 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.dm.impl.index; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.felix.dm.impl.ServiceUtil; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; + +/** + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public abstract class AbstractFactoryFilterIndex { + protected final Map> m_sidToServiceReferencesMap = new HashMap<>(); + protected final Map m_listenerToFilterMap = new HashMap<>(); + + public void addedService(ServiceReference reference, Object service) { + add(reference); + } + + public void modifiedService(ServiceReference reference, Object service) { + modify(reference); + } + + public void removedService(ServiceReference reference, Object service) { + remove(reference); + } + + public void swappedService(ServiceReference reference, Object service, + ServiceReference newReference, Object newService) { + addedService(newReference, newService); + removedService(reference, service); + } + + public void add(ServiceReference reference) { + Long sid = ServiceUtil.getServiceIdObject(reference); + synchronized (m_sidToServiceReferencesMap) { + SortedSet list = m_sidToServiceReferencesMap.get(sid); + if (list == null) { + list = new TreeSet(); + m_sidToServiceReferencesMap.put(sid, list); + } + list.add(reference); + } + } + + public void modify(ServiceReference reference) { + remove(reference); + add(reference); + } + + public void remove(ServiceReference reference) { + Long sid = ServiceUtil.getServiceIdObject(reference); + synchronized (m_sidToServiceReferencesMap) { + SortedSet list = m_sidToServiceReferencesMap.get(sid); + if (list != null) { + list.remove(reference); + } + } + } + + protected boolean referenceMatchesObjectClass(ServiceReference ref, String objectClass) { + boolean matches = false; + Object value = ref.getProperty(Constants.OBJECTCLASS); + matches = Arrays.asList((String[])value).contains(objectClass); + return matches; + } + + /** Structure to hold internal filter data. */ + protected static class FilterData { + public long m_serviceId; + public String m_objectClass; + public int m_ranking; + + public String toString() { + return "FilterData [serviceId=" + m_serviceId + "]"; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/AdapterFilterIndex.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/AdapterFilterIndex.java new file mode 100644 index 00000000000..cbfe20c7a1f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/AdapterFilterIndex.java @@ -0,0 +1,225 @@ +/* + * 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.dm.impl.index; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.FilterIndex; +import org.apache.felix.dm.impl.ServiceUtil; +import org.apache.felix.dm.tracker.ServiceTracker; +import org.apache.felix.dm.tracker.ServiceTrackerCustomizer; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; + +/** + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class AdapterFilterIndex extends AbstractFactoryFilterIndex implements FilterIndex, ServiceTrackerCustomizer { + // (&(objectClass=foo.Bar)(|(service.id=18233)(org.apache.felix.dependencymanager.aspect=18233))) + private static final String FILTER_REGEXP = "\\(&\\(" + Constants.OBJECTCLASS + "=([a-zA-Z\\.\\$0-9]*)\\)\\(\\|\\(" + + Constants.SERVICE_ID + "=([0-9]*)\\)\\(" + + DependencyManager.ASPECT + "=([0-9]*)\\)\\)\\)"; + private static final Pattern PATTERN = Pattern.compile(FILTER_REGEXP); + private final Object m_lock = new Object(); + private ServiceTracker m_tracker; + private BundleContext m_context; + private final Map> m_sidToListenersMap = new HashMap<>(); + protected final Map m_listenerToObjectClassMap = new HashMap<>(); + + public void open(BundleContext context) { + synchronized (m_lock) { + if (m_context != null) { + throw new IllegalStateException("Filter already open."); + } + try { + m_tracker = new ServiceTracker(context, context.createFilter("(" + Constants.OBJECTCLASS + "=*)"), this); + } + catch (InvalidSyntaxException e) { + throw new Error(); + } + m_context = context; + } + m_tracker.open(true, true); + } + + public void close() { + ServiceTracker tracker; + synchronized (m_lock) { + if (m_context == null) { + throw new IllegalStateException("Filter already closed."); + } + tracker = m_tracker; + m_tracker = null; + m_context = null; + } + tracker.close(); + } + + public boolean isApplicable(String clazz, String filter) { + return getFilterData(clazz, filter) != null; + } + + /** Returns a value object with the relevant filter data, or null if this filter was not valid. */ + private FilterData getFilterData(String clazz, String filter) { + // something like: + // (&(objectClass=foo.Bar)(|(service.id=18233)(org.apache.felix.dependencymanager.aspect=18233))) + FilterData resultData = null; + if (filter != null) { + Matcher matcher = PATTERN.matcher(filter); + if (matcher.matches()) { + String sid = matcher.group(2); + String sid2 = matcher.group(3); + if (sid.equals(sid2)) { + resultData = new FilterData(); + resultData.m_serviceId = Long.parseLong(sid); + } + } + } + return resultData; + } + + public List getAllServiceReferences(String clazz, String filter) { + List result = new ArrayList<>(); + Matcher matcher = PATTERN.matcher(filter); + if (matcher.matches()) { + FilterData data = getFilterData(clazz, filter); + if (data != null) { + SortedSet list = null; + synchronized (m_sidToServiceReferencesMap) { + list = m_sidToServiceReferencesMap.get(Long.valueOf(data.m_serviceId)); + if (list != null) { + Iterator iterator = list.iterator(); + while (iterator.hasNext()) { + ServiceReference ref = (ServiceReference) iterator.next(); + String objectClass = matcher.group(1); + if (referenceMatchesObjectClass(ref, objectClass)) { + result.add(ref); + } + } + } + } + } + } + return result; + } + + public void serviceChanged(ServiceEvent event) { + ServiceReference reference = event.getServiceReference(); + Long sid = ServiceUtil.getServiceIdObject(reference); + List notificationList = new ArrayList<>(); + synchronized (m_sidToListenersMap) { + List list = m_sidToListenersMap.get(sid); + if (list != null) { + Iterator iterator = list.iterator(); + while (iterator.hasNext()) { + ServiceListener listener = (ServiceListener) iterator.next(); + String objectClass = m_listenerToObjectClassMap.get(listener); + if (referenceMatchesObjectClass(reference, objectClass)) { + notificationList.add(listener); + } + } + } + } + // notify + Iterator iterator = notificationList.iterator(); + while (iterator.hasNext()) { + ServiceListener listener = (ServiceListener) iterator.next(); + listener.serviceChanged(event); + } + } + + public void addServiceListener(ServiceListener listener, String filter) { + FilterData data = getFilterData(null, filter); + if (data != null) { + Long sidObject = Long.valueOf(data.m_serviceId); + synchronized (m_sidToListenersMap) { + List listeners = m_sidToListenersMap.get(sidObject); + if (listeners == null) { + listeners = new ArrayList<>(); + m_sidToListenersMap.put(sidObject, listeners); + } + listeners.add(listener); + m_listenerToFilterMap.put(listener, filter); + Matcher matcher = PATTERN.matcher(filter); + if (matcher.matches()) { + String objectClass = matcher.group(1); + m_listenerToObjectClassMap.put(listener, objectClass); + } else { + throw new IllegalArgumentException("Filter string does not match index pattern"); + } + + } + } + } + + public void removeServiceListener(ServiceListener listener) { + synchronized (m_sidToListenersMap) { + m_listenerToObjectClassMap.remove(listener); + String filter = (String) m_listenerToFilterMap.remove(listener); + if (filter != null) { + // the listener does exist + FilterData data = getFilterData(null, filter); + if (data != null) { + Long sidObject = Long.valueOf(data.m_serviceId); + List listeners = m_sidToListenersMap.get(sidObject); + if (listeners != null) { + listeners.remove(listener); + } + } + } + } + } + + @SuppressWarnings("unchecked") + public Object addingService(ServiceReference reference) { + BundleContext context; + synchronized (m_lock) { + context = m_context; + } + if (context != null) { + return context.getService(reference); + } + else { + throw new IllegalStateException("No valid bundle context."); + } + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("AdapterFilterIndex["); + sb.append("S2L: " + m_sidToListenersMap.size()); + sb.append(", S2SR: " + m_sidToServiceReferencesMap.size()); + sb.append(", L2F: " + m_listenerToFilterMap.size()); + sb.append("]"); + return sb.toString(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/AspectFilterIndex.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/AspectFilterIndex.java new file mode 100644 index 00000000000..ffe2c3f4c55 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/AspectFilterIndex.java @@ -0,0 +1,267 @@ +/* + * 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.dm.impl.index; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.FilterIndex; +import org.apache.felix.dm.impl.ServiceUtil; +import org.apache.felix.dm.tracker.ServiceTracker; +import org.apache.felix.dm.tracker.ServiceTrackerCustomizer; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; + +/** + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class AspectFilterIndex extends AbstractFactoryFilterIndex implements FilterIndex, ServiceTrackerCustomizer { + // (&(objectClass=foo.Bar)(|(!(service.ranking=*))(service.ranking<=99))(|(service.id=4451)(org.apache.felix.dependencymanager.aspect=4451))) + private static final String FILTER_START = "(&(" + Constants.OBJECTCLASS + "="; + private static final String FILTER_SUBSTRING_0 = ")(&(|(!(" + Constants.SERVICE_RANKING + "=*))(" + Constants.SERVICE_RANKING + "<="; + private static final String FILTER_SUBSTRING_1 = "))(|(" + Constants.SERVICE_ID + "="; + private static final String FILTER_SUBSTRING_2 = ")(" + DependencyManager.ASPECT + "="; + private static final String FILTER_END = "))))"; + private final Object m_lock = new Object(); + private ServiceTracker m_tracker; + private BundleContext m_context; + + private final Map>>> m_sidToObjectClassToRankingToListenersMap = new HashMap<>(); + + public void open(BundleContext context) { + synchronized (m_lock) { + if (m_context != null) { + throw new IllegalStateException("Filter already open."); + } + try { + m_tracker = new ServiceTracker(context, context.createFilter("(" + Constants.OBJECTCLASS + "=*)"), this); + } + catch (InvalidSyntaxException e) { + throw new Error(); + } + m_context = context; + } + m_tracker.open(true, true); + } + + public void close() { + ServiceTracker tracker; + synchronized (m_lock) { + if (m_context == null) { + throw new IllegalStateException("Filter already closed."); + } + tracker = m_tracker; + m_tracker = null; + m_context = null; + } + tracker.close(); + } + + public boolean isApplicable(String clazz, String filter) { + return getFilterData(clazz, filter) != null; + } + + /** Returns a value object with the relevant filter data, or null if this filter was not valid. */ + private FilterData getFilterData(String clazz, String filter) { + // something like: + // (&(objectClass=foo.Bar)(&(|(!(service.ranking=*))(service.ranking<=9))(|(service.id=37)(org.apache.felix.dependencymanager.aspect=37)))) + if ((filter != null) + && (filter.startsWith(FILTER_START)) // (&(objectClass= + && (filter.endsWith(FILTER_END)) // )))) + ) { + int i0 = filter.indexOf(FILTER_SUBSTRING_0); + if (i0 == -1) { + return null; + } + int i1 = filter.indexOf(FILTER_SUBSTRING_1); + if (i1 == -1 || i1 <= i0) { + return null; + } + int i2 = filter.indexOf(FILTER_SUBSTRING_2); + if (i2 == -1 || i2 <= i1) { + return null; + } + long sid = Long.parseLong(filter.substring(i1 + FILTER_SUBSTRING_1.length(), i2)); + long sid2 = Long.parseLong(filter.substring(i2 + FILTER_SUBSTRING_2.length(), filter.length() - FILTER_END.length())); + if (sid != sid2) { + return null; + } + FilterData result = new FilterData(); + result.m_objectClass = filter.substring(FILTER_START.length(), i0); + result.m_serviceId = sid; + result.m_ranking = Integer.parseInt(filter.substring(i0 + FILTER_SUBSTRING_0.length(), i1)); + return result; + } + return null; + } + + public List getAllServiceReferences(String clazz, String filter) { + List result = new ArrayList<>(); + FilterData data = getFilterData(clazz, filter); + if (data != null) { + SortedSet list = null; + synchronized (m_sidToServiceReferencesMap) { + list = m_sidToServiceReferencesMap.get(Long.valueOf(data.m_serviceId)); + if (list != null) { + Iterator iterator = list.iterator(); + while (iterator.hasNext()) { + ServiceReference reference = (ServiceReference) iterator.next(); + if (referenceMatchesObjectClass(reference, data.m_objectClass) && ServiceUtil.getRanking(reference) <= data.m_ranking) { + result.add(reference); + } + } + } + } + } + return result; + } + + public void serviceChanged(ServiceEvent event) { + List list = new ArrayList<>(); + ServiceReference reference = event.getServiceReference(); + Long sidObject = ServiceUtil.getServiceIdObject(reference); + int ranking = ServiceUtil.getRanking(reference); + String[] objectClasses = (String[]) reference.getProperty(Constants.OBJECTCLASS); + + synchronized (m_sidToObjectClassToRankingToListenersMap) { + for (int i = 0; i < objectClasses.length; i++) { + // handle each of the object classes separately since aspects only work on one object class at a time + String objectClass = objectClasses[i]; + Map>> objectClassToRankingToListenersMap = m_sidToObjectClassToRankingToListenersMap.get(sidObject); + if (objectClassToRankingToListenersMap != null) { + SortedMap> rankingToListenersMap = objectClassToRankingToListenersMap.get(objectClass); + if (rankingToListenersMap != null) { + Iterator>> iterator = rankingToListenersMap.entrySet().iterator(); + while (iterator.hasNext()) { + Entry> entry = iterator.next(); + if (ranking <= ((Integer) entry.getKey()).intValue()) { + list.addAll(entry.getValue()); + } + } + } + } + } + } + Iterator iterator = list.iterator(); + while (iterator.hasNext()) { + ServiceListener listener = iterator.next(); + listener.serviceChanged(event); + } + } + + public void addServiceListener(ServiceListener listener, String filter) { + FilterData data = getFilterData(null, filter); + if (data != null) { + Long sidObject = Long.valueOf(data.m_serviceId); + synchronized (m_sidToObjectClassToRankingToListenersMap) { + Map>> objectClassToRankingToListenersMap = m_sidToObjectClassToRankingToListenersMap.get(sidObject); + if (objectClassToRankingToListenersMap == null) { + objectClassToRankingToListenersMap = new TreeMap<>(); + m_sidToObjectClassToRankingToListenersMap.put(sidObject, objectClassToRankingToListenersMap); + } + + SortedMap> rankingToListenersMap = objectClassToRankingToListenersMap.get(data.m_objectClass); + if (rankingToListenersMap == null) { + rankingToListenersMap = new TreeMap<>(); + objectClassToRankingToListenersMap.put(data.m_objectClass, rankingToListenersMap); + } + + List listeners = rankingToListenersMap.get(Integer.valueOf(data.m_ranking)); + if (listeners == null) { + listeners = new ArrayList<>(); + rankingToListenersMap.put(Integer.valueOf(data.m_ranking), listeners); + } + + listeners.add(listener); + m_listenerToFilterMap.put(listener, filter); + } + } + } + + public void removeServiceListener(ServiceListener listener) { + synchronized (m_sidToObjectClassToRankingToListenersMap) { + String filter = (String) m_listenerToFilterMap.remove(listener); + if (filter != null) { + // the listener does exist + FilterData data = getFilterData(null, filter); + if (data != null) { + // this index is applicable + Long sidObject = Long.valueOf(data.m_serviceId); + Map>> objectClassToRankingToListenersMap = m_sidToObjectClassToRankingToListenersMap.get(sidObject); + if (objectClassToRankingToListenersMap != null) { + SortedMap> rankingToListenersMap = objectClassToRankingToListenersMap.get(data.m_objectClass); + if (rankingToListenersMap != null) { + List listeners = rankingToListenersMap.get(Integer.valueOf(data.m_ranking)); + if (listeners != null) { + listeners.remove(listener); + } + // cleanup + if (listeners != null && listeners.isEmpty()) { + rankingToListenersMap.remove(Integer.valueOf(data.m_ranking)); + } + if (rankingToListenersMap.isEmpty()) { + objectClassToRankingToListenersMap.remove(data.m_objectClass); + } + if (objectClassToRankingToListenersMap.isEmpty()) { + m_sidToObjectClassToRankingToListenersMap.remove(sidObject); + } + } + } + } + } + } + } + + @SuppressWarnings("unchecked") + public Object addingService(ServiceReference reference) { + BundleContext context; + synchronized (m_lock) { + context = m_context; + } + if (context != null) { + return context.getService(reference); + } + else { + throw new IllegalStateException("No valid bundle context."); + } + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("AspectFilterIndex["); + sb.append("S2R2L: " + m_sidToObjectClassToRankingToListenersMap.size()); + sb.append(", S2SR: " + m_sidToServiceReferencesMap.size()); + sb.append(", L2F: " + m_listenerToFilterMap.size()); + sb.append("]"); + return sb.toString(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/BundleContextInterceptor.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/BundleContextInterceptor.java new file mode 100644 index 00000000000..04acdf83417 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/BundleContextInterceptor.java @@ -0,0 +1,179 @@ +/* + * 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.dm.impl.index; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import org.apache.felix.dm.FilterIndex; +import org.apache.felix.dm.Logger; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; + +/** + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class BundleContextInterceptor extends BundleContextInterceptorBase { + protected static final String INDEX_LOG_TRESHOLD = "org.apache.felix.dm.index.log.treshold"; + private final ServiceRegistryCache m_cache; + private final boolean m_perfmon; + private Logger m_logger; + private long m_threshold; + + public BundleContextInterceptor(ServiceRegistryCache cache, BundleContext context) { + super(context); + m_cache = cache; + m_perfmon = context.getProperty(INDEX_LOG_TRESHOLD) != null; + if (m_perfmon) { + m_threshold = Long.parseLong(context.getProperty(INDEX_LOG_TRESHOLD)); + m_logger = new Logger(context); + } + } + + public void addServiceListener(ServiceListener listener, String filter) throws InvalidSyntaxException { + FilterIndex filterIndex = m_cache.hasFilterIndexFor(null, filter); + if (filterIndex != null) { + filterIndex.addServiceListener(listener, filter); + } + else { + m_context.addServiceListener(listener, filter); + } + } + + public void addServiceListener(ServiceListener listener) { + FilterIndex filterIndex = m_cache.hasFilterIndexFor(null, null); + if (filterIndex != null) { + filterIndex.addServiceListener(listener, null); + } + else { + m_context.addServiceListener(listener); + } + } + + public void removeServiceListener(ServiceListener listener) { + // remove servicelistener. although it would be prettier to find the correct filterindex first it's + // probaby faster to do a brute force removal. + Iterator filterIndexIterator = m_cache.getFilterIndices().iterator(); + while (filterIndexIterator.hasNext()) { + filterIndexIterator.next().removeServiceListener(listener); + } + m_context.removeServiceListener(listener); + } + + public ServiceReference[] getServiceReferences(String clazz, String filter) throws InvalidSyntaxException { + long start = 0L; + if (m_perfmon) { + start = System.currentTimeMillis(); + } + // first we ask the cache if there is an index for our request (class and filter combination) + FilterIndex filterIndex = m_cache.hasFilterIndexFor(clazz, filter); + if (filterIndex != null) { + List result = filterIndex.getAllServiceReferences(clazz, filter); + Iterator iterator = result.iterator(); + while (iterator.hasNext()) { + ServiceReference reference = iterator.next(); + String[] list = (String[]) reference.getProperty(Constants.OBJECTCLASS); + for (int i = 0; i < list.length; i++) { + if (!reference.isAssignableTo(m_context.getBundle(), list[i])) { + iterator.remove(); + break; + } + } + } + if (m_perfmon) { + long duration = System.currentTimeMillis() - start; + if (duration > m_threshold) { + m_logger.log(Logger.LOG_DEBUG, "Indexed filter exceeds lookup time threshold (" + duration + " ms): " + clazz + " " + filter); + } + } + if (result.size() == 0) { + return null; + } + return (ServiceReference[]) result.toArray(new ServiceReference[result.size()]); + } + else { + // if they don't know, we ask the real bundle context instead + ServiceReference[] serviceReferences = m_context.getServiceReferences(clazz, filter); + if (m_perfmon) { + long duration = System.currentTimeMillis() - start; + if (duration > m_threshold) { + m_logger.log(Logger.LOG_DEBUG, "Unindexed filter exceeds lookup time threshold (" + duration + " ms): " + clazz + " " + filter); + } + } + return serviceReferences; + } + } + + public ServiceReference[] getAllServiceReferences(String clazz, String filter) throws InvalidSyntaxException { + // first we ask the cache if there is an index for our request (class and filter combination) + FilterIndex filterIndex = m_cache.hasFilterIndexFor(clazz, filter); + if (filterIndex != null) { + List result = filterIndex.getAllServiceReferences(clazz, filter); + if (result == null || result.size() == 0) { + return null; + } + return (ServiceReference[]) result.toArray(new ServiceReference[result.size()]); + } + else { + // if they don't know, we ask the real bundle context instead + return m_context.getAllServiceReferences(clazz, filter); + } + } + + public ServiceReference getServiceReference(String clazz) { + ServiceReference[] references; + try { + references = getServiceReferences(clazz, null); + if (references == null || references.length == 0) { + return null; + } + Arrays.sort(references); + return references[references.length - 1]; + } + catch (InvalidSyntaxException e) { + throw new Error("Invalid filter syntax thrown for null filter.", e); + } + } + + public void serviceChanged(ServiceEvent event) { + m_cache.serviceChangedForFilterIndices(event); + } + + @SuppressWarnings("unchecked") + @Override + public ServiceReference getServiceReference(Class clazz) + { + return getServiceReference(clazz.getName()); + } + + @SuppressWarnings("unchecked") + @Override + public Collection> getServiceReferences(Class clazz, String filter) + throws InvalidSyntaxException + { + return Arrays.asList(getServiceReferences(clazz.getName(), filter)); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/BundleContextInterceptorBase.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/BundleContextInterceptorBase.java new file mode 100644 index 00000000000..e45cab803e3 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/BundleContextInterceptorBase.java @@ -0,0 +1,198 @@ +/* + * 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.dm.impl.index; + +import java.io.File; +import java.io.InputStream; +import java.util.Collection; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.BundleListener; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceObjects; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; + +/** + * Base class for bundle context interceptors that keep track of service listeners and delegate incoming changes to them. + * + * @author Felix Project Team + */ +@SuppressWarnings({ "unchecked", "rawtypes" }) +public abstract class BundleContextInterceptorBase implements BundleContext, ServiceListener { + protected final BundleContext m_context; + /** Keeps track of all service listeners and their optional filters. */ + private final Map m_serviceListenerFilterMap = new HashMap<>(); + private long m_currentVersion = 0; + private long m_entryVersion = -1; + private Entry[] m_serviceListenerFilterMapEntries; + + public BundleContextInterceptorBase(BundleContext context) { + m_context = context; + } + + public String getProperty(String key) { + return m_context.getProperty(key); + } + + public Bundle getBundle() { + return m_context.getBundle(); + } + + public Bundle installBundle(String location) throws BundleException { + return m_context.installBundle(location); + } + + public Bundle installBundle(String location, InputStream input) throws BundleException { + return m_context.installBundle(location, input); + } + + public Bundle getBundle(long id) { + return m_context.getBundle(id); + } + + public Bundle[] getBundles() { + return m_context.getBundles(); + } + + public void addServiceListener(ServiceListener listener, String filter) throws InvalidSyntaxException { + synchronized (m_serviceListenerFilterMap) { + m_serviceListenerFilterMap.put(listener, filter); + m_currentVersion++; + } + } + + public void addServiceListener(ServiceListener listener) { + synchronized (m_serviceListenerFilterMap) { + m_serviceListenerFilterMap.put(listener, null); + m_currentVersion++; + } + } + + public void removeServiceListener(ServiceListener listener) { + synchronized (m_serviceListenerFilterMap) { + m_serviceListenerFilterMap.remove(listener); + m_currentVersion++; + } + } + + public void addBundleListener(BundleListener listener) { + m_context.addBundleListener(listener); + } + + public void removeBundleListener(BundleListener listener) { + m_context.removeBundleListener(listener); + } + + public void addFrameworkListener(FrameworkListener listener) { + m_context.addFrameworkListener(listener); + } + + public void removeFrameworkListener(FrameworkListener listener) { + m_context.removeFrameworkListener(listener); + } + + public ServiceRegistration registerService(String[] clazzes, Object service, Dictionary properties) { + return m_context.registerService(clazzes, service, properties); + } + + public ServiceRegistration registerService(String clazz, Object service, Dictionary properties) { + return m_context.registerService(clazz, service, properties); + } + + @Override + public ServiceRegistration registerService(Class clazz, S service, Dictionary properties) { + return m_context.registerService(clazz, service, properties); + } + + @Override + public ServiceRegistration registerService(Class clazz, ServiceFactory factory, Dictionary properties) { + return m_context.registerService(clazz, factory, properties); + } + + public ServiceReference[] getServiceReferences(String clazz, String filter) throws InvalidSyntaxException { + return m_context.getServiceReferences(clazz, filter); + } + + public ServiceReference[] getAllServiceReferences(String clazz, String filter) throws InvalidSyntaxException { + return m_context.getAllServiceReferences(clazz, filter); + } + + public ServiceReference getServiceReference(String clazz) { + return m_context.getServiceReference(clazz); + } + + @Override + public ServiceReference getServiceReference(Class clazz) { + return m_context.getServiceReference(clazz); + } + + @Override + public Collection> getServiceReferences(Class clazz, String filter) throws InvalidSyntaxException { + return m_context.getServiceReferences(clazz, filter); + } + + public Object getService(ServiceReference reference) { + return m_context.getService(reference); + } + + @Override + public ServiceObjects getServiceObjects(ServiceReference reference) + { + return m_context.getServiceObjects(reference); + } + + public boolean ungetService(ServiceReference reference) { + return m_context.ungetService(reference); + } + + public File getDataFile(String filename) { + return m_context.getDataFile(filename); + } + + public Filter createFilter(String filter) throws InvalidSyntaxException { + return m_context.createFilter(filter); + } + + @Override + public Bundle getBundle(String location) { + return m_context.getBundle(location); + } + + protected Entry[] synchronizeCollection() { + // lazy copy on write: we make a new copy only if writes have changed the collection + synchronized (m_serviceListenerFilterMap) { + if (m_currentVersion != m_entryVersion) { + m_serviceListenerFilterMapEntries = (Entry[]) m_serviceListenerFilterMap.entrySet().toArray(new Entry[m_serviceListenerFilterMap.size()]); + m_entryVersion = m_currentVersion; + } + } + return m_serviceListenerFilterMapEntries; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/FilterIndexBundleContext.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/FilterIndexBundleContext.java new file mode 100644 index 00000000000..9a6a266a17a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/FilterIndexBundleContext.java @@ -0,0 +1,64 @@ +/* + * 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.dm.impl.index; + +import java.util.Map.Entry; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; + +/** + * @author Felix Project Team + */ +public class FilterIndexBundleContext extends BundleContextInterceptorBase { + public FilterIndexBundleContext(BundleContext context) { + super(context); + } + + public void serviceChanged(ServiceEvent event) { + Entry[] entries = synchronizeCollection(); + for (int i = 0; i < entries.length; i++) { + Entry serviceListenerFilterEntry = entries[i]; + ServiceListener serviceListener = serviceListenerFilterEntry.getKey(); + String filter = serviceListenerFilterEntry.getValue(); + if (filter == null) { + serviceListener.serviceChanged(event); + } + else { + // call service changed on the listener if the filter matches the event + // TODO review if we can be smarter here + try { + if ("(objectClass=*)".equals(filter)) { + serviceListener.serviceChanged(event); + } + else { + if (m_context.createFilter(filter).match(event.getServiceReference())) { + serviceListener.serviceChanged(event); + } + } + } + catch (InvalidSyntaxException e) { + e.printStackTrace(); + } + } + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/ServiceRegistryCache.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/ServiceRegistryCache.java new file mode 100644 index 00000000000..2fdc1f89b0f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/ServiceRegistryCache.java @@ -0,0 +1,129 @@ +/* + * 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.dm.impl.index; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.apache.felix.dm.FilterIndex; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; + +/** + * @author Felix Project Team + */ +public class ServiceRegistryCache implements ServiceListener/*, CommandProvider*/ { + private final List m_filterIndexList = new CopyOnWriteArrayList<>(); + private final BundleContext m_context; + private final FilterIndexBundleContext m_filterIndexBundleContext; + private final Map m_bundleContextInterceptorMap = new HashMap<>(); + private long m_currentVersion = 0; + private long m_arrayVersion = -1; + + public ServiceRegistryCache(BundleContext context) { + m_context = context; + m_filterIndexBundleContext = new FilterIndexBundleContext(m_context); + } + + public void open() { + m_context.addServiceListener(this); + } + + public void close() { + m_context.removeServiceListener(this); + } + + public void addFilterIndex(FilterIndex index) { + m_filterIndexList.add(index); + index.open(m_filterIndexBundleContext); + } + + public void removeFilterIndex(FilterIndex index) { + index.close(); + m_filterIndexList.remove(index); + } + + public int getSize() { + return m_filterIndexList.size(); + } + + public void serviceChanged(ServiceEvent event) { + // any incoming event is first dispatched to the list of filter indices + m_filterIndexBundleContext.serviceChanged(event); + // and then all the other listeners can access it + synchronized (m_bundleContextInterceptorMap) { + if (m_currentVersion != m_arrayVersion) { + // if our copy is out of date, we make a new one + m_arrayVersion = m_currentVersion; + } + } + + serviceChangedForFilterIndices(event); + } + + /** Creates an interceptor for a bundle context that uses our cache. */ + public BundleContext createBundleContextInterceptor(BundleContext context) { + synchronized (m_bundleContextInterceptorMap) { + BundleContextInterceptor bundleContextInterceptor = m_bundleContextInterceptorMap.get(context); + if (bundleContextInterceptor == null) { + bundleContextInterceptor = new BundleContextInterceptor(this, context); + m_bundleContextInterceptorMap.put(context, bundleContextInterceptor); + m_currentVersion++; + // TODO figure out a good way to clean up bundle contexts that are no longer valid so they can be garbage collected + } + return bundleContextInterceptor; + } + } + + public FilterIndex hasFilterIndexFor(String clazz, String filter) { + Iterator iterator = m_filterIndexList.iterator(); + while (iterator.hasNext()) { + FilterIndex filterIndex = iterator.next(); + if (filterIndex.isApplicable(clazz, filter)) { + return filterIndex; + } + } + return null; + } + + public void serviceChangedForFilterIndices(ServiceEvent event) { + Iterator iterator = m_filterIndexList.iterator(); + while (iterator.hasNext()) { + FilterIndex filterIndex = iterator.next(); + filterIndex.serviceChanged(event); + } + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("ServiceRegistryCache["); + sb.append("FilterIndices: " + m_filterIndexList.size()); + sb.append(", BundleContexts intercepted: " + m_bundleContextInterceptorMap.size()); + sb.append("]"); + return sb.toString(); + } + + public List getFilterIndices() { + return m_filterIndexList; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/ServiceRegistryCacheManager.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/ServiceRegistryCacheManager.java new file mode 100644 index 00000000000..87ca2efa261 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/ServiceRegistryCacheManager.java @@ -0,0 +1,216 @@ +/* + * 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.dm.impl.index; + +import java.lang.reflect.Constructor; +import java.util.function.Consumer; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.FilterIndex; +import org.apache.felix.dm.impl.index.multiproperty.MultiPropertyFilterIndex; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.FrameworkUtil; + +/** + * This class manages the service registry cache creation. + */ +public class ServiceRegistryCacheManager { + + /** + * The Service Registry cache, which is created if you specify the "org.apache.felix.dependencymanager.filterindex" system property + * or if you register as a service a FilterIndex in the service registry. + */ + private static volatile ServiceRegistryCache m_cache; + + /** + * Backdoor only used by tests: a Consumer is added in system properties using the following key and + * when tests invoke it, the cache is reintialized as if the JVM would have been restarted. + */ + private final static String RESET = "org.apache.felix.dependencymanager.filterindex.reset"; + + /** + * the DependendencyManager bundle context used by the ServiceRegistryCache. + */ + private static volatile BundleContext m_context; + + /** + * Boolean used to check if we are already initialized + */ + private static volatile boolean m_init = false; + + /** + * Static initializer: we create the registry cache in case "org.apache.felix.dependencymanager.filterindex" system property is configured. + */ + static { + init(); + } + + /** + * Gets the service registry cache, if enabled. + * @return the service registry cache, if enabled, or null + */ + public static ServiceRegistryCache getCache() { + return m_cache; + } + + /** + * Registers a FilterIndex into the service registry cache. The cache is created if necessary. + * @param index a new FilterIndex to be registered in the cache + * @param context the bundle context used by the cache + */ + public static void registerFilterIndex(FilterIndex index, BundleContext context) { + ServiceRegistryCache cache = createCache(context); + cache.addFilterIndex(index); + } + + /** + * Unregister a FilterIndex from the cache, and possibly close the cache in case it becomes empty. + * @param index the FilterIndex to unregister + */ + public static void unregisterFilterIndex(FilterIndex index) { + ServiceRegistryCache cache = ServiceRegistryCacheManager.getCache(); + if (cache != null) { + cache.removeFilterIndex(index); + boolean close = false; + synchronized (ServiceRegistryCacheManager.class) { + if (cache.getSize() == 0) { + m_cache = null; + close = true; + } + } + if (close) { + cache.close(); + } + } + } + + /** + * Creates the cache if it does not exist. + * @param context the bundle context that will be used by the case + * @return the created cache (or the existing cache) + */ + private static ServiceRegistryCache createCache(BundleContext context) { + ServiceRegistryCache cache = null; + boolean open = false; + synchronized (ServiceRegistryCacheManager.class) { + if (m_cache == null) { + m_cache = new ServiceRegistryCache(context); + open = true; + } + cache = m_cache; + } + if (open) { + cache.open(); + } + return cache; + } + + /** + * Initialize the service registry cache. + */ + public static void init() { + try { + if (m_init) { + return; + } + Bundle bundle = FrameworkUtil.getBundle(ServiceRegistryCacheManager.class); + if (bundle != null) { + if (bundle.getState() != Bundle.STARTING && bundle.getState() != Bundle.ACTIVE) { + bundle.start(); // take care: may callback our registerFilterIndex method + } + m_context = bundle.getBundleContext(); + String index = m_context.getProperty(DependencyManager.SERVICEREGISTRY_CACHE_INDICES); + if (index != null) { + resetIndices(index); + } + } + Consumer reset = ServiceRegistryCacheManager::reset; + System.getProperties().put(RESET, reset); + m_init = true; + } + + catch (BundleException e) { + // if we cannot start ourselves, we cannot use the indices + e.printStackTrace(); + } + } + + private static void resetIndices(String index) { + try { + if (index != null) { + ServiceRegistryCache cache = createCache(m_context); // may already exist, in case the Activator has called back our registerFilterIndex method + + String[] props = index.split(";"); + for (int i = 0; i < props.length; i++) { + // Check if a classname is specified for the current index. + // (syntax is "full_class_name:index") + int colon = props[i].indexOf(":"); + if (colon != -1) { + String className = props[i].substring(0, colon).trim(); + String indexDefinition = props[i].substring(colon + 1); + cache.addFilterIndex(createCustomIndex(className, indexDefinition)); + } else if (props[i].equals("*aspect*")) { + cache.addFilterIndex(new AspectFilterIndex()); + } else if (props[i].equals("*adapter*")) { + cache.addFilterIndex(new AdapterFilterIndex()); + } else { + cache.addFilterIndex(new MultiPropertyFilterIndex(props[i])); + } + } + } + } + + catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Creates a custom index using its classname, that has been specified in the org.apache.felix.dependencymanager.filterindex system property. + */ + private static FilterIndex createCustomIndex(String className, String indexDefinition) { + try { + Class customIndexClass = Class.forName(className); + Constructor ctor = customIndexClass.getConstructor(String.class); + FilterIndex index = (FilterIndex) ctor.newInstance(indexDefinition); + return index; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * Reinitialize the cache. Only called by tests. This method is registered in the system properties in the form of a Runnable, + * with key=ServiceRegistryCacheManager.RESET. + */ + private static void reset(String indices) { + if (m_cache != null) { + for (FilterIndex index : m_cache.getFilterIndices()) { + index.close(); + } + m_cache.close(); + } + m_cache = null; + resetIndices(indices); + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/multiproperty/Filter.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/multiproperty/Filter.java new file mode 100644 index 00000000000..18118d3d1dd --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/multiproperty/Filter.java @@ -0,0 +1,137 @@ +/* + * 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.dm.impl.index.multiproperty; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.TreeSet; + +/** + * @author Felix Project Team + */ +public class Filter { + private boolean m_valid = true; + private Map m_properties = new HashMap<>(); + private Set m_propertyKeys = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + + private Filter() { + } + + // Sample valid filter string (&(objectClass=OBJECTCLASS)(&(model=MODEL)(concept=CONCEPT)(role=ROLE)(!(context=*)))) + public static Filter parse(String filterString) { + Filter filter = new Filter(); + StringTokenizer tokenizer = new StringTokenizer(filterString, "(&|=)", true); + + String token = null; + String prevToken = null; + String key = null; + StringBuilder valueBuilder = new StringBuilder(); + boolean negate = false; + + while (tokenizer.hasMoreTokens()) { + prevToken = token; + token = tokenizer.nextToken(); + if (token.equals("|")) { + // we're not into OR's + filter.m_valid = false; + break; + } + if (token.equals("!")) { + negate = true; + } else if (token.equals("=")) { + key = prevToken.toLowerCase(); + } else if (key != null) { + if (!token.equals(")")) { + valueBuilder.append(token); // might be superseded by a & + } + if (token.equals(")")) { + // set complete + if (filter.m_properties.containsKey(key)) { + // set current property to multivalue + Property property = filter.m_properties.get(key); + property.addValue(valueBuilder.toString(), negate); + } else { + Property property = new Property(negate, key, valueBuilder.toString()); + filter.m_properties.put(key, property); + filter.m_propertyKeys.add(key); + } + negate = false; + key = null; + valueBuilder = new StringBuilder(); + } + } + } + return filter; + } + + public boolean containsProperty(String propertyKey) { + return m_properties.containsKey(propertyKey); + } + + public Set getPropertyKeys() { + return m_properties.keySet(); + } + + public Property getProperty(String key) { + return m_properties.get(key); + } + + public boolean isValid() { + if (!m_valid) { + return m_valid; + } else { + // also check the properties + Iterator propertiesIterator = m_properties.values().iterator(); + while (propertiesIterator.hasNext()) { + Property property = propertiesIterator.next(); + if (!property.isValid()) { + return false; + } + } + } + return true; + } + + protected MultiPropertyKey createKey(int filterSize) { + MultiPropertyKey multiPropertyKey = new MultiPropertyKey(0); + Iterator keys = m_propertyKeys.iterator(); + while (keys.hasNext()) { + String key = keys.next(); + Property prop = m_properties.get(key); + if (!prop.isWildcard()) { + Iterator values = prop.getValues().iterator(); + while (values.hasNext()) { + String value = values.next(); + multiPropertyKey.append(toKey(key, value)); + } + } + } + return multiPropertyKey; + } + + protected MultiPropertyKey toKey(String key, Object value) { + MultiPropertyKey multiPropertyKey = new MultiPropertyKey(1); + multiPropertyKey.add(key, value.toString()); + return multiPropertyKey; + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/multiproperty/MultiPropertyFilterIndex.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/multiproperty/MultiPropertyFilterIndex.java new file mode 100644 index 00000000000..d4374ae29ae --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/multiproperty/MultiPropertyFilterIndex.java @@ -0,0 +1,519 @@ +/* + * 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.dm.impl.index.multiproperty; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.apache.felix.dm.FilterIndex; +import org.apache.felix.dm.tracker.ServiceTracker; +import org.apache.felix.dm.tracker.ServiceTrackerCustomizer; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; + +/** + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class MultiPropertyFilterIndex implements FilterIndex, ServiceTrackerCustomizer { + + private final Object m_lock = new Object(); + + private ServiceTracker m_tracker; + + private BundleContext m_context; + + private Map m_configProperties = new LinkedHashMap<>(); + + private List m_negatePropertyKeys = new ArrayList<>(); + + private final Map> m_keyToServiceReferencesMap = new HashMap<>(); + + private final Map> m_keyToListenersMap = new HashMap<>(); + + private final Map m_listenerToFilterMap = new HashMap<>(); + + public MultiPropertyFilterIndex(String configString) { + parseConfig(configString); + } + + public boolean isApplicable(String clazz, String filterString) { + Filter filter = createFilter(clazz, filterString); + + if (!filter.isValid()) { + return false; + } + // compare property keys to the ones in the configuration + Set filterPropertyKeys = filter.getPropertyKeys(); + if (m_configProperties.size() != filterPropertyKeys.size()) { + return false; + } + + Iterator filterPropertyKeysIterator = filterPropertyKeys.iterator(); + while (filterPropertyKeysIterator.hasNext()) { + String filterPropertyKey = filterPropertyKeysIterator.next(); + if (!m_configProperties.containsKey(filterPropertyKey)) { + return false; + } else if ((m_configProperties.get(filterPropertyKey)).isNegate() != filter.getProperty(filterPropertyKey).isNegate()) { + // negation should be equal + return false; + } else if (!filter.getProperty(filterPropertyKey).isNegate() && filter.getProperty(filterPropertyKey).getValue().equals("*")) { + // no wildcards without negation allowed + return false; + } + } + // our properties match so we're applicable + return true; + } + + public boolean isApplicable(ServiceReference ref) { + String[] propertyKeys = ref.getPropertyKeys(); + TreeSet referenceProperties = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + for (int i = 0; i < propertyKeys.length; i++) { + referenceProperties.add(propertyKeys[i]); + } + Iterator iterator = m_configProperties.keySet().iterator(); + while (iterator.hasNext()) { + String item = iterator.next(); + Property configProperty = m_configProperties.get(item); + if (!configProperty.isNegate() && !(referenceProperties.contains(item))) { + return false; + } else if (configProperty.isNegate() && referenceProperties.contains(item)) { + return false; + } + } + return true; + } + + private void parseConfig(String configString) { + String[] propertyConfigs = configString.split(","); + for (int i = 0; i < propertyConfigs.length; i++) { + String propertyConfig = propertyConfigs[i]; + Property property = new Property(); + String key; + String value = null; + if (propertyConfig.startsWith("!")) { + property.setNegate(true); + key = propertyConfig.substring(1); + } else if (propertyConfig.startsWith("#")) { + property.setPermute(false); + key = propertyConfig.substring(1); + } else { + key = propertyConfig; + } + if (key.endsWith("*")) { + key = key.substring(0, key.indexOf("*")); + value = "*"; + } + property.setKey(key.toLowerCase()); + property.addValue(value, property.isNegate()); + m_configProperties.put(key.toLowerCase(), property); + if (property.isNegate()) { + m_negatePropertyKeys.add(key); + } + } + } + + protected Collection getProperties() { + return m_configProperties.values(); + } + + protected MultiPropertyKey createKeyFromFilter(String clazz, String filterString) { + int filterSize = m_configProperties.size(); + return createFilter(clazz, filterString).createKey(filterSize); + } + + //KEYS OF A FILTER + private Filter createFilter(String clazz, String filterString) { + String filterStringWithObjectClass = filterString; + if (clazz != null && !clazz.isEmpty()) { + if (filterString != null) { + if (!filterStringWithObjectClass.startsWith("(&(objectClass=")) { + filterStringWithObjectClass = "(&(objectClass=" + clazz + ")" + filterString + ")"; + } + } else { + filterStringWithObjectClass = "(objectClass=" + clazz + ")"; + } + } + Filter filter = Filter.parse(filterStringWithObjectClass); + return filter; + } + + public List createKeys(ServiceReference reference) { + List results = new ArrayList<>(); + + String[] keys = reference.getPropertyKeys(); + Arrays.sort(keys, String.CASE_INSENSITIVE_ORDER); + + MultiPropertyKey multiPropertyKey = new MultiPropertyKey(0); + List permutations = new ArrayList<>(); + + List> sets = new ArrayList<>(); + + for (int i = 0; i < keys.length; i++) { + String key = null; + boolean hasUpperCase = !keys[i].equals(keys[i].toLowerCase()); + if(hasUpperCase) { + key = keys[i].toLowerCase(); + } else { + key = keys[i]; + } + + if (m_configProperties.containsKey(key)) { + Object valueObject = reference.getProperty(key); + if (valueObject instanceof String[]) { + String[] values = (String[]) valueObject; + if (m_configProperties.get(key).isPermute()) { + sets.add(getPermutations(key, values)); + } else { + List singleValues = new ArrayList<>(); + for (int v = 0; v < values.length; v++) { + MultiPropertyKey single = new MultiPropertyKey(0); + single.append(toKey(key, values[v])); + singleValues.add(single); + } + sets.add(singleValues); + } + } else { + multiPropertyKey.append(toKey(key, (String) valueObject.toString())); + } + } + } + + if (permutations != null && !permutations.isEmpty()) { + for (MultiPropertyKey permutation : permutations) { + permutation.append(multiPropertyKey); + results.add(permutation); + } + } else { + if (!sets.isEmpty()) { + + List> carthesianProductMultiProperty = carthesianProductMultiProperty(0, sets); + for (List keyList : carthesianProductMultiProperty) { + MultiPropertyKey merged = new MultiPropertyKey(0); + merged.append(multiPropertyKey); + for (MultiPropertyKey single : keyList) { + + merged.append(single); + + } + results.add(merged); + } + + } else { + + results.add(multiPropertyKey); + } + } + return results; + } + + private List> carthesianProductMultiProperty(int index, List> sets) { + List> result = new ArrayList<>(); + if (index == sets.size()) { + result.add(new ArrayList()); + } else { + List set = sets.get(index); + for (int i = 0; i < set.size(); i++) { + MultiPropertyKey object = set.get(i); + List> pSets = carthesianProductMultiProperty(index + 1, sets); + for (int j = 0; j < pSets.size(); j++) { + List pSet = pSets.get(j); + pSet.add(object); + result.add(pSet); + } + } + } + return result; + } + + + List getPermutations(String key, String[] values) { + List results = new ArrayList<>(); + Arrays.sort(values, String.CASE_INSENSITIVE_ORDER); + for (int v = 0; v < values.length; v++) { + String processValue = values[v]; + List items = new ArrayList<>(); + items.add(processValue); + // per value get combinations + List subItems = new ArrayList<>(items); + for (int w = v; w < values.length; w++) { + // make a copy of the current list + subItems = new ArrayList<>(subItems); + if (w != v) { + String value = values[w]; + subItems.add(value); + } + results.add(toKey(key, subItems)); + } + } + return results; + } + + + protected MultiPropertyKey toKey(String key, List values) { + MultiPropertyKey kvc = new MultiPropertyKey(values.size()); + + for (int i = 0; i < values.size(); i++) { + kvc.add(key, values.get(i)); + } + return kvc; + } + + protected MultiPropertyKey toKey(String key, Object value) { + MultiPropertyKey kvc = new MultiPropertyKey(1); + kvc.add(key, value.toString()); + return kvc; + } + + @SuppressWarnings("unchecked") + public Object addingService(ServiceReference reference) { + BundleContext context; + synchronized (m_lock) { + context = m_context; + } + if (context != null) { + return context.getService(reference); + } else { + throw new IllegalStateException("No valid bundle context."); + } + } + + public void addedService(ServiceReference reference, Object service) { + if (isApplicable(reference) && shouldBeIndexed(reference)) { + handleServiceAdd(reference); + } + } + + public void modifiedService(ServiceReference reference, Object service) { + if (isApplicable(reference)) { + handleServicePropertiesChange(reference); + } + } + + public void removedService(ServiceReference reference, Object service) { + if (isApplicable(reference) && shouldBeIndexed(reference)) { + handleServiceRemove(reference); + } + } + + protected void handleServiceAdd(ServiceReference reference) { + List keys = createKeys(reference); + + synchronized (m_keyToServiceReferencesMap) { + for (int i = 0; i < keys.size(); i++) { + List references = m_keyToServiceReferencesMap.get(keys.get(i)); + if (references == null) { + references = new ArrayList<>(1); + m_keyToServiceReferencesMap.put(keys.get(i), references); + } + references.add(reference); + } + + } + } + + protected void handleServicePropertiesChange(ServiceReference reference) { + + synchronized (m_keyToServiceReferencesMap) { + // TODO this is a quite expensive linear scan over the existing collection + // because we first need to remove any existing references and they can be + // all over the place :) + Iterator> iterator = m_keyToServiceReferencesMap.values().iterator(); + while (iterator.hasNext()) { + List list = iterator.next(); + if (list != null) { + Iterator i2 = list.iterator(); + while (i2.hasNext()) { + ServiceReference ref = i2.next(); + if (ref.equals(reference)) { + i2.remove(); + } + } + } + } + // only re-add the reference when it is still applicable for this filter index + if (shouldBeIndexed(reference)) { + List keys = createKeys(reference); + for (int i = 0; i < keys.size(); i++) { + List references = m_keyToServiceReferencesMap.get(keys.get(i)); + if (references == null) { + references = new ArrayList<>(keys.size()); + m_keyToServiceReferencesMap.put(keys.get(i), references); + } + references.add(reference); + } + } + } + } + + protected void handleServiceRemove(ServiceReference reference) { + List keys = createKeys(reference); + synchronized (m_keyToServiceReferencesMap) { + for (int i = 0; i < keys.size(); i++) { + List references = m_keyToServiceReferencesMap.get(keys.get(i)); + if (references != null) { + references.remove(reference); + if (references.isEmpty()) { + m_keyToServiceReferencesMap.remove(keys.get(i)); + } else { + ((ArrayList) reference).trimToSize(); + } + } + } + } + } + + protected boolean shouldBeIndexed(ServiceReference reference) { + // is already applicable, so we should only check whether there's a negate field in the filter which has a value in the reference + Iterator negatePropertyKeyIterator = m_negatePropertyKeys.iterator(); + while (negatePropertyKeyIterator.hasNext()) { + String negatePropertyKey = negatePropertyKeyIterator.next(); + if (reference.getProperty(negatePropertyKey) != null) { + return false; + } + } + return true; + } + + public void open(BundleContext context) { + synchronized (m_lock) { + if (m_context != null) { + throw new IllegalStateException("Filter already open."); + } + try { + m_tracker = new ServiceTracker(context, context.createFilter("(" + Constants.OBJECTCLASS + "=*)"), this); + } catch (InvalidSyntaxException e) { + throw new Error(); + } + m_context = context; + } + m_tracker.open(true, true); + } + + public void close() { + ServiceTracker tracker; + synchronized (m_lock) { + if (m_context == null) { + throw new IllegalStateException("Filter already closed."); + } + tracker = m_tracker; + m_tracker = null; + m_context = null; + } + tracker.close(); + } + + public List getAllServiceReferences(String clazz, String filter) { + List result = new ArrayList<>(); + MultiPropertyKey key = createKeyFromFilter(clazz, filter); + synchronized (m_keyToServiceReferencesMap) { + List references = m_keyToServiceReferencesMap.get(key); + if (references != null) { + result.addAll(references); + } + } + return result; + } + + public void serviceChanged(ServiceEvent event) { + if (isApplicable(event.getServiceReference())) { + List keys = createKeys(event.getServiceReference()); + List list = new ArrayList(); + synchronized (m_keyToListenersMap) { + for (int i = 0; i < keys.size(); i++) { + //TODO fix + MultiPropertyKey key = keys.get(i); + List listeners = m_keyToListenersMap.get(key); + if (listeners != null) { + list.addAll(listeners); + } + } + } + if (list != null) { + Iterator iterator = list.iterator(); + while (iterator.hasNext()) { + ServiceListener listener = iterator.next(); + listener.serviceChanged(event); + } + } + } + } + + public void addServiceListener(ServiceListener listener, String filter) { + MultiPropertyKey key = createKeyFromFilter(null, filter); + + synchronized (m_keyToListenersMap) { + List listeners = m_keyToListenersMap.get(key); + if (listeners == null) { + listeners = new CopyOnWriteArrayList(); + m_keyToListenersMap.put(key, listeners); + } + listeners.add(listener); + m_listenerToFilterMap.put(listener, filter); + } + } + + public void removeServiceListener(ServiceListener listener) { + synchronized (m_keyToListenersMap) { + String filter = m_listenerToFilterMap.remove(listener); + if (filter != null) { + // the listener does exist + MultiPropertyKey key = createKeyFromFilter(null, filter); + + boolean result = filter != null; + if (result) { + List listeners = m_keyToListenersMap.get(key); + if (listeners != null) { + listeners.remove(listener); + if (listeners.isEmpty()) { + m_keyToListenersMap.remove(key); + } + } + // TODO actually, if listeners == null that would be strange.... + } + } + } + } + + protected Collection getServiceListeners() { + return m_listenerToFilterMap.keySet(); + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append(" dMultiPropertyExactFilter["); + sb.append("K2L: " + m_keyToListenersMap.size()); + sb.append(", K2SR: " + m_keyToServiceReferencesMap.size()); + sb.append(", L2F: " + m_listenerToFilterMap.size()); + sb.append("]"); + return sb.toString(); + } + + @Override + public void swappedService(ServiceReference reference, Object service, ServiceReference newReference, Object newService) { + addedService(newReference, newService); + removedService(reference, service); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/multiproperty/MultiPropertyKey.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/multiproperty/MultiPropertyKey.java new file mode 100644 index 00000000000..afc947e7517 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/multiproperty/MultiPropertyKey.java @@ -0,0 +1,141 @@ +/* + * 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.dm.impl.index.multiproperty; + +import java.util.Arrays; + +public class MultiPropertyKey { + + private MultiPropertyKeyPart[] properties; + + public MultiPropertyKey(int filterSize) { + properties = new MultiPropertyKeyPart[0]; + } + + // Initial adding key values. + public void add(String key, String value) { + int newLength = properties.length + 1; + MultiPropertyKeyPart[] tmpKeys = new MultiPropertyKeyPart[newLength]; + + System.arraycopy(properties, 0, tmpKeys, 0, properties.length); + tmpKeys[newLength-1] = new MultiPropertyKeyPart(key, value); + + this.properties = tmpKeys; + Arrays.sort(properties); + } + + public void append(MultiPropertyKey other) { + int newLength = properties.length + other.properties.length; + MultiPropertyKeyPart[] tmpKeys = new MultiPropertyKeyPart[newLength]; + + System.arraycopy(properties, 0, tmpKeys, 0, properties.length); + System.arraycopy(other.properties, 0, tmpKeys, properties.length, other.properties.length); + + this.properties = tmpKeys; + Arrays.sort(properties); + } + + @Override + public int hashCode() { + return Arrays.hashCode(properties); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof MultiPropertyKey)) { + return false; + } + + MultiPropertyKey other = (MultiPropertyKey) obj; + + if (properties.length != other.properties.length) { + return false; + } + + for (int i = 0; i < properties.length; i++) { + if (!(properties[i].equals(other.properties[i]))) { + return false; + } + } + return true; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < properties.length; i++) { + builder.append(properties[i].key); + builder.append('='); + builder.append(properties[i].value); + if (i < properties.length - 1) { + builder.append(';'); + } + } + return builder.toString(); + } + + // (key=value) part of a multi property index (&(key1=val1)(key2=val2) .. ) + private class MultiPropertyKeyPart implements Comparable { + + private final String key; + private final String value; + + public MultiPropertyKeyPart(String key, String value) { + this.key = key; + this.value = value; + } + + @Override + public int hashCode() { + int h = 0; + h += 31 * key.hashCode(); + h += value.hashCode(); + return h; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof MultiPropertyKeyPart)) + return false; + + MultiPropertyKeyPart object = (MultiPropertyKeyPart) obj; + + if (!(this.key.equals(object.key))) + return false; + if (!(this.value.equals(object.value))) + return false; + + return true; + } + + @Override + public int compareTo(MultiPropertyKeyPart o) { + if (this.key.compareTo(o.key) == 0) { + return this.value.compareTo(o.value); + } + return this.key.compareTo(o.key); + } + } +} \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/multiproperty/Property.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/multiproperty/Property.java new file mode 100644 index 00000000000..2d4333fef35 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/index/multiproperty/Property.java @@ -0,0 +1,108 @@ +/* + * 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.dm.impl.index.multiproperty; + +import java.util.Set; +import java.util.TreeSet; +/** + * @author Felix Project Team + */ + +public class Property { + boolean m_negate; + boolean m_permute = true; + boolean m_valid = true; + String m_key; + String m_value; + Set m_values = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + + public Property() { + } + + public Property(boolean negate, String key, String value) { + super(); + m_negate = negate; + m_key = key.toLowerCase(); + m_values.add(value); + m_value = value; + } + + public void setNegate(boolean negate) { + this.m_negate = negate; + } + + public void setPermute(boolean permute) { + this.m_permute = permute; + } + + public void setKey(String key) { + this.m_key = key.toLowerCase(); + } + + public void addValue(String value, boolean negate) { + if (this.m_negate != negate) { + // multiproperty with different negations, causes invalid configuration. + m_valid = false; + } + if (this.m_value == null) { + // value has not bee set yet + this.m_value = value; + } + if (value != null) { + m_values.add(value); + } + } + + public boolean isNegate() { + return m_negate; + } + + public boolean isPermute() { + return m_permute; + } + + public String getKey() { + return m_key; + } + + public String getValue() { + return m_value; + } + + public Set getValues() { + return m_values; + } + + public boolean isWildcard() { + return "*".equals(m_value); + } + + public boolean isMultiValue() { + return m_values.size() > 1; + } + + public String toString() { + return "Property [negate=" + m_negate + ", key=" + m_key + ", values=" + + m_values + "]"; + } + + public boolean isValid() { + return m_valid; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/metatype/AttributeDefinitionImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/metatype/AttributeDefinitionImpl.java new file mode 100644 index 00000000000..3a72c95373f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/metatype/AttributeDefinitionImpl.java @@ -0,0 +1,76 @@ +/* + * 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.dm.impl.metatype; + +import org.osgi.service.metatype.AttributeDefinition; + +/** + * @author Felix Project Team + */ +public class AttributeDefinitionImpl implements AttributeDefinition { + private PropertyMetaDataImpl m_propertyMetaData; + private Resource m_resource; + + public AttributeDefinitionImpl(PropertyMetaDataImpl propertyMetaData, Resource resource) { + m_propertyMetaData = propertyMetaData; + m_resource = resource; + } + + public int getCardinality() { + return m_propertyMetaData.getCardinality(); + } + + public String[] getDefaultValue() { + return m_propertyMetaData.getDefaults(); + } + + public String getDescription() { + return m_resource.localize(m_propertyMetaData.getDescription()); + } + + public String getID() { + return m_propertyMetaData.getId(); + } + + public String getName() { + return m_resource.localize(m_propertyMetaData.getHeading()); + } + + public String[] getOptionLabels() { + String[] labels = m_propertyMetaData.getOptionLabels(); + if (labels != null) { + for (int i = 0; i < labels.length; i++) { + labels[i] = m_resource.localize(labels[i]); + } + } + return labels; + } + + public String[] getOptionValues() { + return m_propertyMetaData.getOptionValues(); + } + + public int getType() { + return m_propertyMetaData.getType(); + } + + public String validate(String value) { + return null; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/metatype/MetaTypeProviderImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/metatype/MetaTypeProviderImpl.java new file mode 100644 index 00000000000..a75cf221870 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/metatype/MetaTypeProviderImpl.java @@ -0,0 +1,273 @@ +/* + * 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.dm.impl.metatype; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Locale; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.TreeSet; + +import org.apache.felix.dm.Logger; +import org.apache.felix.dm.PropertyMetaData; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedService; +import org.osgi.service.cm.ManagedServiceFactory; +import org.osgi.service.log.LogService; +import org.osgi.service.metatype.MetaTypeProvider; +import org.osgi.service.metatype.ObjectClassDefinition; + +/** + * When a ConfigurationDepdendency is configured with properties metadata, we provide + * a specific ManagedService which also implements the MetaTypeProvider interface. This interface + * allows the MetaTypeService to retrieve our properties metadata, which will then be handled by webconsole. + * + * @author Felix Project Team + */ +public class MetaTypeProviderImpl implements MetaTypeProvider, ManagedService, ManagedServiceFactory { + private ManagedService m_managedServiceDelegate; + private ManagedServiceFactory m_managedServiceFactoryDelegate; + private ArrayList m_propertiesMetaData = new ArrayList<>(); + private String m_description; + private String m_heading; + private String m_localization; + private HashMap m_localesProperties = new HashMap<>(); + private Logger m_logger; + private BundleContext m_bctx; + private String m_pid; + + public MetaTypeProviderImpl(String pid, BundleContext ctx, Logger logger, ManagedService msDelegate, ManagedServiceFactory msfDelegate) { + m_pid = pid; + m_bctx = ctx; + m_logger = logger; + m_managedServiceDelegate = msDelegate; + m_managedServiceFactoryDelegate = msfDelegate; + // Set the default localization file base name (see core specification, in section Localization on page 68). + // By default, this file can be stored in OSGI-INF/l10n/bundle.properties (and corresponding localized version + // in OSGI-INF/l10n/bundle_en_GB_welsh.properties, OSGI-INF/l10n/bundle_en_GB.properties, etc ... + // This default localization property file name can be overriden using the PropertyMetaData.setLocalization method. + m_localization = (String) m_bctx.getBundle().getHeaders().get(Constants.BUNDLE_LOCALIZATION); + if (m_localization == null) { + m_localization = Constants.BUNDLE_LOCALIZATION_DEFAULT_BASENAME; + } + } + + @SuppressWarnings("unchecked") + public MetaTypeProviderImpl(MetaTypeProviderImpl prototype, ManagedService msDelegate, ManagedServiceFactory msfDelegate) { + m_pid = prototype.m_pid; + m_bctx = prototype.m_bctx; + m_logger = prototype.m_logger; + m_localization = prototype.m_localization; + m_propertiesMetaData = prototype.m_propertiesMetaData != null ? (ArrayList) prototype.m_propertiesMetaData.clone() : null; + m_description = prototype.m_description; + m_heading = prototype.m_heading; + m_localization = prototype.m_localization; + m_localesProperties = prototype.m_localesProperties != null ? (HashMap) prototype.m_localesProperties.clone() : null; + m_managedServiceDelegate = msDelegate; + m_managedServiceFactoryDelegate = msfDelegate; + } + + + /** + * Registers the metatype information of a given configuration property + * @param property + */ + public void add(PropertyMetaData property) { + m_propertiesMetaData.add(property); + } + + /** + * A human readable description of the PID this annotation is associated with. Example: "Configuration for the PrinterService bundle". + * @return A human readable description of the PID this annotation is associated with (may be localized) + */ + public void setDescription(String description) { + m_description = description; + } + + /** + * The label used to display the tab name (or section) where the properties are displayed. Example: "Printer Service". + * @return The label used to display the tab name where the properties are displayed (may be localized) + */ + public void setName(String heading) { + m_heading = heading; + } + + /** + * Points to the basename of the Properties file that can localize the Meta Type informations. + * By default, (e.g. setLocalization("person") would match person_du_NL.properties in the root bundle directory. + * The default localization base name for the properties is OSGI-INF/l10n/bundle, but can + * be overridden by the manifest Bundle-Localization header (see core specification, in section Localization on page 68). + */ + public void setLocalization(String path) { + if (path.endsWith(".properties")) { + throw new IllegalArgumentException( + "path must point to the base name of the propertie file, " + + "excluding local suffixes. For example: " + + "foo/bar/person is valid and matches the property file \"foo/bar/person_bundle_en_GB_welsh.properties\""); + } + m_localization = path.startsWith("/") ? path.substring(1) : path; + } + + // --------------- MetaTypeProvider interface ------------------------------------------------- + + /** + * Returns all the Locales our bundle is containing. For instance, if our bundle contains the following localization files: + * OSGI-INF/l10n/bundle_en_GB_welsh.properties and OSGI-INF/l10n/bundle_en_GB.properties, then this method will return + * "en_GB", "en_GB_welsh" ... + * @return the list of Locale supported by our bundle. + */ + @SuppressWarnings("rawtypes") + public String[] getLocales() { + int lastSlash = m_localization.lastIndexOf("/"); + String path = (lastSlash == -1) ? "/" : ("/" + m_localization.substring(0, lastSlash - 1)); + String base = (lastSlash == -1) ? m_localization : m_localization.substring(lastSlash + 1); + Enumeration e = m_bctx.getBundle().findEntries(path, + base + "*.properties", false); + if (e == null) { + return null; + } + + TreeSet set = new TreeSet<>(); + while (e.hasMoreElements()) { + // We have found a locale property file in the form of "path/file[_language[_ country[_variation]].properties" + // And now, we have to get the "language[_country[_variation]]" part ... + URL url = (URL) e.nextElement(); + String name = url.getPath(); + name = name.substring(name.lastIndexOf("/") + 1); + int underscore = name.indexOf("_"); + if (underscore != -1) { + name = name.substring(underscore + 1, name.length() - ".properties".length()); + } + if (name.length() > 0) { + set.add(name); + } + } + + String[] locales = (String[]) set.toArray(new String[set.size()]); + return locales.length == 0 ? null : locales; + } + + /** + * Returns the ObjectClassDefinition for a given Pid/Locale. + */ + public ObjectClassDefinition getObjectClassDefinition(String id, String locale) { + try { + // Check if the id matches our PID + if (!id.equals(m_pid)) { + m_logger.log(LogService.LOG_ERROR, "id " + id + " does not match pid " + m_pid); + return null; + } + + Properties localeProperties = getLocaleProperties(locale); + return new ObjectClassDefinitionImpl(m_pid, m_heading, + m_description, m_propertiesMetaData, new Resource(localeProperties)); + } + + catch (Throwable t) { + m_logger.log( + Logger.LOG_ERROR, + "Unexpected exception while geting ObjectClassDefinition for " + id + " (locale=" + + locale + ")", t); + return null; + } + } + + /** + * We also implements the ManagedService and we just delegates the configuration handling to + * our associated ConfigurationDependency. + */ + public void updated(Dictionary properties) throws ConfigurationException { + m_managedServiceDelegate.updated(properties); + } + + /** + * Gets the properties for a given Locale. + * @param locale + * @return + * @throws IOException + */ + private synchronized Properties getLocaleProperties(String locale) throws IOException { + locale = locale == null ? Locale.getDefault().toString() : locale; + Properties properties = (Properties) m_localesProperties.get(locale); + if (properties == null) { + properties = new Properties(); + URL url = m_bctx.getBundle().getEntry(m_localization + ".properties"); + if (url != null) { + loadLocale(properties, url); + } + + String path = m_localization; + StringTokenizer tok = new StringTokenizer(locale, "_"); + while (tok.hasMoreTokens()) { + path += "_" + tok.nextToken(); + url = m_bctx.getBundle().getEntry(path + ".properties"); + if (url != null) { + properties = new Properties(properties); + loadLocale(properties, url); + } + } + m_localesProperties.put(locale, properties); + } + return properties; + } + + /** + * Loads a Locale Properties file. + * @param properties + * @param url + * @throws IOException + */ + private void loadLocale(Properties properties, URL url) throws IOException { + InputStream in = null; + try { + in = url.openStream(); + properties.load(in); + } + finally { + if (in != null) { + try { + in.close(); + } + catch (IOException ignored) { + } + } + } + } + + // ManagedServiceFactory implementation + public void deleted(String pid) { + m_managedServiceFactoryDelegate.deleted(pid); + } + + public String getName() { + return m_pid; + } + + public void updated(String pid, Dictionary properties) throws ConfigurationException { + m_managedServiceFactoryDelegate.updated(pid, properties); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/metatype/ObjectClassDefinitionImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/metatype/ObjectClassDefinitionImpl.java new file mode 100644 index 00000000000..bd0a980c22a --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/metatype/ObjectClassDefinitionImpl.java @@ -0,0 +1,103 @@ +/* + * 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.dm.impl.metatype; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.apache.felix.dm.PropertyMetaData; +import org.osgi.service.metatype.AttributeDefinition; +import org.osgi.service.metatype.ObjectClassDefinition; + +/** + * ObjectClassDefinition implementation. + * + * @author Felix Project Team + */ +public class ObjectClassDefinitionImpl implements ObjectClassDefinition { + // Our OCD name (may be localized) + private String m_name; + + // Our OCD description (may be localized) + private String m_description; + + // Our OCD id + private String m_id; + + // The list of Properties MetaData objects (from DependencyManager API) + private final List m_propertiesMetaData; + + // The localized resource that can be used when localizing some parameters + private Resource m_resource; + + public ObjectClassDefinitionImpl(String id, String name, String description, List propertiesMetaData, Resource resource) { + m_id = id; + m_name = name; + m_description = description; + m_propertiesMetaData = propertiesMetaData; + m_resource = resource; + } + + // --------------------- ObjectClassDefinition ---------------------------------------- + + public AttributeDefinition[] getAttributeDefinitions(int filter) { + List attrs = new ArrayList<>(); + for (int i = 0; i < m_propertiesMetaData.size(); i++) { + PropertyMetaDataImpl metaData = (PropertyMetaDataImpl) m_propertiesMetaData.get(i); + switch (filter) { + case ObjectClassDefinition.ALL: + attrs.add(new AttributeDefinitionImpl(metaData, m_resource)); + break; + case ObjectClassDefinition.OPTIONAL: + if (!metaData.isRequired()) { + attrs.add(new AttributeDefinitionImpl(metaData, m_resource)); + } + break; + case ObjectClassDefinition.REQUIRED: + if (metaData.isRequired()) { + attrs.add(new AttributeDefinitionImpl(metaData, m_resource)); + } + break; + default: + } + } + + AttributeDefinition[] array = new AttributeDefinitionImpl[attrs.size()]; + return attrs.toArray(array); + } + + public String getDescription() { + return m_resource.localize(m_description); + } + + public String getID() { + return m_id; + } + + public InputStream getIcon(int size) throws IOException { + // TODO Auto-generated method stub + return null; + } + + public String getName() { + return m_resource.localize(m_name); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/metatype/PropertyMetaDataImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/metatype/PropertyMetaDataImpl.java new file mode 100644 index 00000000000..583b014c865 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/metatype/PropertyMetaDataImpl.java @@ -0,0 +1,205 @@ +/* + * 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.dm.impl.metatype; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.felix.dm.PropertyMetaData; +import org.osgi.service.metatype.AttributeDefinition; + +/** + * DependencyManager PropertyMetaData Implementation. This class describes meta informations regarding + * one given configuration property. + * + * @author Felix Project Team + */ +public class PropertyMetaDataImpl implements PropertyMetaData { + /** + * List of option labels (may be localized) + */ + List m_optionsLabels = new ArrayList<>(); + + /** + * List of option values + */ + List m_optionsValues = new ArrayList<>(); + + /** + * Property cardinality. + * @see {@link AttributeDefinition#getCardinality()} + */ + private int m_cardinality; + + /** + * Valid default property values + */ + private String[] m_defaults; + + /** + * Property description. + */ + private String m_description; + + /** + * Property title. + */ + private String m_heading; + + /** + * Property unique Id + */ + private String m_id; + + /** + * Required flag. + */ + private boolean m_required; + + /** + * Property Type. + * @see {@link AttributeDefinition#getType()} + */ + private int m_type = AttributeDefinition.STRING; + + /** + * Mapping between java types and valid MetaType types. + * @see {@link AttributeDefinition#getType()} + */ + @SuppressWarnings({ "unchecked", "serial", "rawtypes" }) + private final static Map, Integer> m_typeMapping = new HashMap() {{ + put(Boolean.class, new Integer(AttributeDefinition.BOOLEAN)); + put(Byte.class, new Integer(AttributeDefinition.BYTE)); + put(Character.class, new Integer(AttributeDefinition.CHARACTER)); + put(Double.class, new Integer(AttributeDefinition.FLOAT)); + put(Integer.class, new Integer(AttributeDefinition.INTEGER)); + put(Long.class, new Integer(AttributeDefinition.LONG)); + put(Short.class, new Integer(AttributeDefinition.SHORT)); + put(String.class, new Integer(AttributeDefinition.STRING)); + }}; + + public PropertyMetaData addOption(String optionLabel, String optionValue) { + m_optionsLabels.add(optionLabel); + m_optionsValues.add(optionValue); + return this; + } + + public PropertyMetaData setCardinality(int cardinality) { + m_cardinality = cardinality; + return this; + } + + public PropertyMetaData setDefaults(String[] defaults) { + m_defaults = defaults; + return this; + } + + public PropertyMetaData setDescription(String description) { + m_description = description; + return this; + } + + public PropertyMetaData setHeading(String heading) { + m_heading = heading; + return this; + } + + public PropertyMetaData setId(String id) { + m_id = id; + return this; + } + + public PropertyMetaData setRequired(boolean required) { + m_required = required; + return this; + } + + public PropertyMetaData setType(Class classType) { + Integer type = (Integer) m_typeMapping.get(classType); + if (type == null) { + throw new IllegalArgumentException("Invalid type: " + classType + ". Valid types are " + + m_typeMapping.keySet()); + } + m_type = type.intValue(); + return this; + } + + public String[] getOptionLabels() { + String[] optionLabels = new String[m_optionsLabels.size()]; + return (String[]) m_optionsLabels.toArray(optionLabels); + } + + public String[] getOptionValues() { + String[] optionValues = new String[m_optionsValues.size()]; + return (String[]) m_optionsValues.toArray(optionValues); + } + + public int getCardinality() { + return m_cardinality; + } + + public String[] getDefaults() { + return m_defaults; + } + + public String getDescription() { + return m_description; + } + + public String getHeading() { + return m_heading; + } + + public String getId() { + return m_id; + } + + public boolean isRequired() { + return m_required; + } + + public int getType() { + return m_type; + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("cardinality=").append(m_cardinality); + sb.append("; defaults="); + for (int i = 0; i < m_defaults.length; i ++) { + sb.append(m_defaults[i]).append(" "); + } + sb.append("; description=").append(m_description); + sb.append("; heading=").append(m_heading); + sb.append("; id=").append(m_id); + sb.append("; required=").append(m_required); + sb.append("; type=").append(getType()); + sb.append("; optionLabels="); + for (int i = 0; i < m_optionsLabels.size(); i ++) { + sb.append(m_optionsLabels.get(i)).append(" "); + } + sb.append("; optionValues="); + for (int i = 0; i < m_optionsValues.size(); i ++) { + sb.append(m_optionsValues.get(i)).append(" "); + } + return sb.toString(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/metatype/Resource.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/metatype/Resource.java new file mode 100644 index 00000000000..c588a0cde2d --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/metatype/Resource.java @@ -0,0 +1,42 @@ +/* + * 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.dm.impl.metatype; + +import java.util.Properties; + +/** + * Helper class used to localize a given Property Meta Data. + * + * @author Felix Project Team + */ +public class Resource { + private Properties m_properties; + + public Resource(Properties properties) { + m_properties = properties; + } + + public String localize(String param) { + if (m_properties != null && param != null && param.startsWith("%")) { + param = param.substring(1); + return m_properties.getProperty(param); + } + return param; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/packageinfo b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/packageinfo new file mode 100644 index 00000000000..1481887fa01 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/packageinfo @@ -0,0 +1 @@ +version 4.6.0 diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/AbstractCustomizerActionSet.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/AbstractCustomizerActionSet.java new file mode 100644 index 00000000000..4add2c2b726 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/AbstractCustomizerActionSet.java @@ -0,0 +1,100 @@ +/* + * 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.dm.tracker; + +import java.util.ArrayList; +import java.util.List; + +/** + * Actions which can be performed on a given customizer interface. + * + * @author Felix Project Team + */ +public abstract class AbstractCustomizerActionSet { + + enum Type { ADDED, MODIFIED, REMOVED } + + final List m_actions = new ArrayList<>(); + + public void addCustomizerAdded(Object item, Object related, Object object) { + m_actions.add(new CustomizerAction(Type.ADDED, item, related, object)); + } + + public void addCustomizerModified(Object item, Object related, Object object) { + m_actions.add(new CustomizerAction(Type.MODIFIED, item, related, object)); + } + + public void addCustomizerRemoved(Object item, Object related, Object object) { + m_actions.add(new CustomizerAction(Type.REMOVED, item, related, object)); + } + + public void appendActionSet(AbstractCustomizerActionSet actionSet) { + for (CustomizerAction action : actionSet.getActions()) { + m_actions.add(action); + } + } + + abstract void execute(); + + public List getActions() { + return m_actions; + } + + @Override + public String toString() { + return "AbstractCustomizerActionSet [m_actions=" + m_actions + "]"; + } + + static class CustomizerAction { + private final Type m_type; + private final Object m_item; + private final Object m_related; + private final Object m_object; + + public CustomizerAction(Type type, Object item, Object related, Object object) { + m_type = type; + m_item = item; + m_related = related; + m_object = object; + } + + public Type getType() { + return m_type; + } + + public Object getItem() { + return m_item; + } + + public Object getRelated() { + return m_related; + } + + public Object getObject() { + return m_object; + } + + @Override + public String toString() { + return "CustomizerAction [m_type=" + m_type + ", m_item=" + m_item + + ", m_related=" + m_related + ", m_object=" + m_object + + "]"; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/AbstractTracked.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/AbstractTracked.java new file mode 100644 index 00000000000..d452cb4d05c --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/AbstractTracked.java @@ -0,0 +1,480 @@ +package org.apache.felix.dm.tracker; +/* + * Copyright (c) OSGi Alliance (2007, 2008). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.apache.felix.dm.impl.SerialExecutor; + +/** + * Abstract class to track items. If a Tracker is reused (closed then reopened), + * then a new AbstractTracked object is used. This class acts a map of tracked + * item -> customized object. Subclasses of this class will act as the listener + * object for the tracker. This class is used to synchronize access to the + * tracked items. This is not a public class. It is only for use by the + * implementation of the Tracker class. + * + * @ThreadSafe + * @version $Revision: 5871 $ + * @since 1.4 + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +abstract class AbstractTracked { + /* set this to true to compile in debug messages */ + private static final boolean DEBUG = false; + + /** + * Map of tracked items to customized objects. + * + * @GuardedBy this + */ + private Map tracked; + + /** + * Modification count. This field is initialized to zero and incremented by + * modified. + * + * @GuardedBy this + */ + private int trackingCount; + + /** + * List of items in the process of being added. This is used to deal with + * nesting of events. Since events may be synchronously delivered, events + * can be nested. For example, when processing the adding of a service and + * the customizer causes the service to be unregistered, notification to the + * nested call to untrack that the service was unregistered can be made to + * the track method. + * + * Since the ArrayList implementation is not synchronized, all access to + * this list must be protected by the same synchronized object for + * thread-safety. + * + * @GuardedBy this + */ + private final List adding; + + /** + * true if the tracked object is closed. + * + * This field is volatile because it is set by one thread and read by + * another. + */ + volatile boolean closed; + + /** + * Initial list of items for the tracker. This is used to correctly process + * the initial items which could be modified before they are tracked. This + * is necessary since the initial set of tracked items are not "announced" + * by events and therefore the event which makes the item untracked could be + * delivered before we track the item. + * + * An item must not be in both the initial and adding lists at the same + * time. An item must be moved from the initial list to the adding list + * "atomically" before we begin tracking it. + * + * Since the LinkedList implementation is not synchronized, all access to + * this list must be protected by the same synchronized object for + * thread-safety. + * + * @GuardedBy this + */ + private final LinkedList initial; + + private final SerialExecutor m_executor = new SerialExecutor(null); + + /** + * AbstractTracked constructor. + */ + AbstractTracked() { + this.tracked = new HashMap(); + trackingCount = 0; + adding = new ArrayList(6); + initial = new LinkedList(); + closed = false; + } + + void setTracked(HashMap map) { + this.tracked = map; + } + + /** + * Set initial list of items into tracker before events begin to be + * received. + * + * This method must be called from Tracker's open method while synchronized + * on this object in the same synchronized block as the add listener call. + * + * @param list The initial list of items to be tracked. null + * entries in the list are ignored. + * @GuardedBy this + */ + void setInitial(Object[] list) { + if (list == null) { + return; + } + int size = list.length; + for (int i = 0; i < size; i++) { + Object item = list[i]; + if (item == null) { + continue; + } + if (DEBUG) { + System.out.println("AbstractTracked.setInitial: " + item); //$NON-NLS-1$ + } + initial.add(item); + } + } + + /** + * Track the initial list of items. This is called after events can begin to + * be received. + * + * This method must be called from Tracker's open method while not + * synchronized on this object after the add listener call. + * + */ + void trackInitial() { + while (true) { + Object item; + synchronized (this) { + if (closed || (initial.size() == 0)) { + /* + * if there are no more initial items + */ + return; /* we are done */ + } + /* + * move the first item from the initial list to the adding list + * within this synchronized block. + */ + item = initial.removeFirst(); + if (tracked.get(item) != null) { + /* if we are already tracking this item */ + if (DEBUG) { + System.out + .println("AbstractTracked.trackInitial[already tracked]: " + item); //$NON-NLS-1$ + } + continue; /* skip this item */ + } + if (adding.contains(item)) { + /* + * if this item is already in the process of being added. + */ + if (DEBUG) { + System.out + .println("AbstractTracked.trackInitial[already adding]: " + item); //$NON-NLS-1$ + } + continue; /* skip this item */ + } + adding.add(item); + } + + final AbstractCustomizerActionSet actionSet = trackAdding(item, null); + m_executor.schedule(() -> actionSet.execute()); + + if (DEBUG) { + System.out.println("AbstractTracked.trackInitial: " + item); //$NON-NLS-1$ + } + } + } + + /** + * Called by the owning Tracker object when it is closed. + */ + void close() { + closed = true; + } + + abstract AbstractCustomizerActionSet createCustomizerActionSet(); + + /** + * Begin to track an item. + * + * @param item Item to be tracked. + * @param related Action related object. + */ + AbstractCustomizerActionSet track(final Object item, final Object related) { + final Object object; + final AbstractCustomizerActionSet actionSet = createCustomizerActionSet(); + synchronized (this) { + if (closed) { + return actionSet; + } + object = tracked.get(item); + if (object == null) { /* we are not tracking the item */ + if (adding.contains(item)) { + /* if this item is already in the process of being added. */ + if (DEBUG) { + System.out + .println("AbstractTracked.track[already adding]: " + item); //$NON-NLS-1$ + } + return actionSet; + } + adding.add(item); /* mark this item is being added */ + } + else { /* we are currently tracking this item */ + if (DEBUG) { + System.out + .println("AbstractTracked.track[modified]: " + item); //$NON-NLS-1$ + } + modified(); /* increment modification count */ + } + } + + if (object == null) { /* we are not tracking the item */ + actionSet.appendActionSet(trackAdding(item, related)); + } + else { + /* Call customizer outside of synchronized region */ + actionSet.addCustomizerModified(item, related, object); + /* + * If the customizer throws an unchecked exception, it is safe to + * let it propagate + */ + } + return actionSet; + } + + /** + * Common logic to add an item to the tracker used by track and + * trackInitial. The specified item must have been placed in the adding list + * before calling this method. + * + * @param item Item to be tracked. + * @param related Action related object. + */ + private AbstractCustomizerActionSet trackAdding(final Object item, final Object related) { + final AbstractCustomizerActionSet actionSet = createCustomizerActionSet(); + if (DEBUG) { + System.out.println("AbstractTracked.trackAdding: " + item); //$NON-NLS-1$ + } + Object object = null; + boolean becameUntracked = false; + /* Call customizer outside of synchronized region */ + try { + object = customizerAdding(item, related); + /* + * If the customizer throws an unchecked exception, it will + * propagate after the finally + */ + } + finally { + boolean needToCallback = false; + synchronized (this) { + if (adding.remove(item) && !closed) { + /* + * if the item was not untracked during the customizer + * callback + */ + if (object != null) { + tracked.put(item, object); + modified(); /* increment modification count */ + notifyAll(); /* notify any waiters */ + needToCallback = true; /* marrs: invoke added callback */ + } + } + else { + becameUntracked = true; + } + } + if (needToCallback) { + actionSet.addCustomizerAdded(item, related, object); + } + } + /* + * The item became untracked during the customizer callback. + */ + if (becameUntracked && (object != null)) { + if (DEBUG) { + System.out + .println("AbstractTracked.trackAdding[removed]: " + item); //$NON-NLS-1$ + } + /* Call customizer outside of synchronized region */ + actionSet.addCustomizerRemoved(item, related, object); + /* + * If the customizer throws an unchecked exception, it is safe to + * let it propagate + */ + } + return actionSet; + } + + /** + * Discontinue tracking the item. + * + * @param item Item to be untracked. + * @param related Action related object. + */ + AbstractCustomizerActionSet untrack(final Object item, final Object related) { + AbstractCustomizerActionSet actionSet = createCustomizerActionSet(); + final Object object; + synchronized (this) { + if (initial.remove(item)) { /* + * if this item is already in the list + * of initial references to process + */ + if (DEBUG) { + System.out + .println("AbstractTracked.untrack[removed from initial]: " + item); //$NON-NLS-1$ + } + return actionSet; /* + * we have removed it from the list and it will not be + * processed + */ + } + + if (adding.remove(item)) { /* + * if the item is in the process of + * being added + */ + if (DEBUG) { + System.out + .println("AbstractTracked.untrack[being added]: " + item); //$NON-NLS-1$ + } + return actionSet; /* + * in case the item is untracked while in the process of + * adding + */ + } + object = tracked.remove(item); /* + * must remove from tracker before + * calling customizer callback + */ + if (object == null) { /* are we actually tracking the item */ + return actionSet; + } + modified(); /* increment modification count */ + } + if (DEBUG) { + System.out.println("AbstractTracked.untrack[removed]: " + item); //$NON-NLS-1$ + } + /* Call customizer outside of synchronized region */ + actionSet.addCustomizerRemoved(item, related, object); + /* + * If the customizer throws an unchecked exception, it is safe to let it + * propagate + */ + return actionSet; + } + + /** + * Returns the number of tracked items. + * + * @return The number of tracked items. + * + * @GuardedBy this + */ + int size() { + return tracked.size(); + } + + /** + * Return the customized object for the specified item + * + * @param item The item to lookup in the map + * @return The customized object for the specified item. + * + * @GuardedBy this + */ + Object getCustomizedObject(final Object item) { + return tracked.get(item); + } + + /** + * Return the list of tracked items. + * + * @param list An array to contain the tracked items. + * @return The specified list if it is large enough to hold the tracked + * items or a new array large enough to hold the tracked items. + * @GuardedBy this + */ + Object[] getTracked(final Object[] list) { + return tracked.keySet().toArray(list); + } + + /** + * Increment the modification count. If this method is overridden, the + * overriding method MUST call this method to increment the tracking count. + * + * @GuardedBy this + */ + void modified() { + trackingCount++; + } + + /** + * Returns the tracking count for this ServiceTracker object. + * + * The tracking count is initialized to 0 when this object is opened. Every + * time an item is added, modified or removed from this object the tracking + * count is incremented. + * + * @GuardedBy this + * @return The tracking count for this object. + */ + int getTrackingCount() { + return trackingCount; + } + + /** + * Returns the serial executor used by this tracked. + * @return + */ + SerialExecutor getExecutor() { + return m_executor; + } + + /** + * Call the specific customizer adding method. This method must not be + * called while synchronized on this object. + * + * @param item Item to be tracked. + * @param related Action related object. + * @return Customized object for the tracked item or null if + * the item is not to be tracked. + */ + abstract Object customizerAdding(final Object item, final Object related); + + /** marrs: Call the specific customizer added method. */ + abstract void customizerAdded(final Object item, final Object related, final Object object); + + /** + * Call the specific customizer modified method. This method must not be + * called while synchronized on this object. + * + * @param item Tracked item. + * @param related Action related object. + * @param object Customized object for the tracked item. + */ + abstract void customizerModified(final Object item, final Object related, + final Object object); + + /** + * Call the specific customizer removed method. This method must not be + * called while synchronized on this object. + * + * @param item Tracked item. + * @param related Action related object. + * @param object Customized object for the tracked item. + */ + abstract void customizerRemoved(final Object item, final Object related, + final Object object); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/BundleTracker.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/BundleTracker.java new file mode 100644 index 00000000000..e3b16778778 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/BundleTracker.java @@ -0,0 +1,505 @@ +package org.apache.felix.dm.tracker; +/* + * Copyright (c) OSGi Alliance (2007, 2008). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.SynchronousBundleListener; + +/** + * The BundleTracker class simplifies tracking bundles much like + * the ServiceTracker simplifies tracking services. + *

      + * A BundleTracker is constructed with state criteria and a + * BundleTrackerCustomizer object. A BundleTracker can + * use the BundleTrackerCustomizer to select which bundles are + * tracked and to create a customized object to be tracked with the bundle. The + * BundleTracker can then be opened to begin tracking all bundles + * whose state matches the specified state criteria. + *

      + * The getBundles method can be called to get the + * Bundle objects of the bundles being tracked. The + * getObject method can be called to get the customized object for + * a tracked bundle. + *

      + * The BundleTracker class is thread-safe. It does not call a + * BundleTrackerCustomizer while holding any locks. + * BundleTrackerCustomizer implementations must also be + * thread-safe. + * + * @ThreadSafe + * @version $Revision: 5894 $ + * @since 1.4 + */ +public class BundleTracker implements BundleTrackerCustomizer { + /* set this to true to compile in debug messages */ + static final boolean DEBUG = false; + + /** + * The Bundle Context used by this BundleTracker. + */ + protected final BundleContext context; + + /** + * The BundleTrackerCustomizer object for this tracker. + */ + final BundleTrackerCustomizer customizer; + + /** + * Tracked bundles: Bundle object -> customized Object and + * BundleListener object + */ + private volatile Tracked tracked; + + /** + * Accessor method for the current Tracked object. This method is only + * intended to be used by the unsynchronized methods which do not modify the + * tracked field. + * + * @return The current Tracked object. + */ + private Tracked tracked() { + return tracked; + } + + /** + * State mask for bundles being tracked. This field contains the ORed values + * of the bundle states being tracked. + */ + final int mask; + + /** + * Create a BundleTracker for bundles whose state is present in + * the specified state mask. + * + *

      + * Bundles whose state is present on the specified state mask will be + * tracked by this BundleTracker. + * + * @param context The BundleContext against which the tracking + * is done. + * @param stateMask The bit mask of the ORing of the bundle + * states to be tracked. + * @param customizer The customizer object to call when bundles are added, + * modified, or removed in this BundleTracker. If + * customizer is null, then this + * BundleTracker will be used as the + * BundleTrackerCustomizer and this + * BundleTracker will call the + * BundleTrackerCustomizer methods on itself. + * @see Bundle#getState() + */ + public BundleTracker(BundleContext context, int stateMask, + BundleTrackerCustomizer customizer) { + this.context = context; + this.mask = stateMask; + this.customizer = (customizer == null) ? this : customizer; + } + + /** + * Open this BundleTracker and begin tracking bundles. + * + *

      + * Bundle which match the state criteria specified when this + * BundleTracker was created are now tracked by this + * BundleTracker. + * + * @throws java.lang.IllegalStateException If the BundleContext + * with which this BundleTracker was created is no + * longer valid. + * @throws java.lang.SecurityException If the caller and this class do not + * have the appropriate + * AdminPermission[context bundle,LISTENER], and the + * Java Runtime Environment supports permissions. + */ + public void open() { + final Tracked t; + synchronized (this) { + if (tracked != null) { + return; + } + if (DEBUG) { + System.out.println("BundleTracker.open"); //$NON-NLS-1$ + } + t = new Tracked(); + synchronized (t) { + context.addBundleListener(t); + Bundle[] bundles = context.getBundles(); + if (bundles != null) { + int length = bundles.length; + for (int i = 0; i < length; i++) { + int state = bundles[i].getState(); + if ((state & mask) == 0) { + /* null out bundles whose states are not interesting */ + bundles[i] = null; + } + } + /* set tracked with the initial bundles */ + t.setInitial(bundles); + } + } + tracked = t; + } + /* Call tracked outside of synchronized region */ + t.trackInitial(); /* process the initial references */ + } + + /** + * Close this BundleTracker. + * + *

      + * This method should be called when this BundleTracker should + * end the tracking of bundles. + * + *

      + * This implementation calls {@link #getBundles()} to get the list of + * tracked bundles to remove. + */ + public void close() { + final Bundle[] bundles; + final Tracked outgoing; + synchronized (this) { + outgoing = tracked; + if (outgoing == null) { + return; + } + if (DEBUG) { + System.out.println("BundleTracker.close"); //$NON-NLS-1$ + } + outgoing.close(); + bundles = getBundles(); + tracked = null; + try { + context.removeBundleListener(outgoing); + } + catch (IllegalStateException e) { + /* In case the context was stopped. */ + } + } + if (bundles != null) { + for (int i = 0; i < bundles.length; i++) { + outgoing.untrack(bundles[i], null); + } + } + } + + /** + * Default implementation of the + * BundleTrackerCustomizer.addingBundle method. + * + *

      + * This method is only called when this BundleTracker has been + * constructed with a null BundleTrackerCustomizer argument. + * + *

      + * This implementation simply returns the specified Bundle. + * + *

      + * This method can be overridden in a subclass to customize the object to be + * tracked for the bundle being added. + * + * @param bundle The Bundle being added to this + * BundleTracker object. + * @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 specified bundle. + * @see BundleTrackerCustomizer#addingBundle(Bundle, BundleEvent) + */ + public Object addingBundle(Bundle bundle, BundleEvent event) { + return bundle; + } + + public void addedBundle(Bundle bundle, BundleEvent event, Object object) { + /* do nothing */ + } + + /** + * Default implementation of the + * BundleTrackerCustomizer.modifiedBundle method. + * + *

      + * This method is only called when this BundleTracker has been + * constructed with a null BundleTrackerCustomizer argument. + * + *

      + * This implementation does nothing. + * + * @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 customized object for the specified Bundle. + * @see BundleTrackerCustomizer#modifiedBundle(Bundle, BundleEvent, Object) + */ + public void modifiedBundle(Bundle bundle, BundleEvent event, Object object) { + /* do nothing */ + } + + /** + * Default implementation of the + * BundleTrackerCustomizer.removedBundle method. + * + *

      + * This method is only called when this BundleTracker has been + * constructed with a null BundleTrackerCustomizer argument. + * + *

      + * This implementation does nothing. + * + * @param bundle The Bundle being 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 customized object for the specified bundle. + * @see BundleTrackerCustomizer#removedBundle(Bundle, BundleEvent, Object) + */ + public void removedBundle(Bundle bundle, BundleEvent event, Object object) { + /* do nothing */ + } + + /** + * Return an array of Bundles for all bundles being tracked by + * this BundleTracker. + * + * @return An array of Bundles or null if no + * bundles are being tracked. + */ + public Bundle[] getBundles() { + final Tracked t = tracked(); + if (t == null) { /* if BundleTracker is not open */ + return null; + } + synchronized (t) { + int length = t.size(); + if (length == 0) { + return null; + } + return (Bundle[]) t.getTracked(new Bundle[length]); + } + } + + /** + * Returns the customized object for the specified Bundle if + * the specified bundle is being tracked by this BundleTracker. + * + * @param bundle The Bundle being tracked. + * @return The customized object for the specified Bundle or + * null if the specified Bundle is not + * being tracked. + */ + public Object getObject(Bundle bundle) { + final Tracked t = tracked(); + if (t == null) { /* if BundleTracker is not open */ + return null; + } + synchronized (t) { + return t.getCustomizedObject(bundle); + } + } + + /** + * Remove a bundle from this BundleTracker. + * + * The specified bundle will be removed from this BundleTracker + * . If the specified bundle was being tracked then the + * BundleTrackerCustomizer.removedBundle method will be called + * for that bundle. + * + * @param bundle The Bundle to be removed. + */ + public void remove(Bundle bundle) { + final Tracked t = tracked(); + if (t == null) { /* if BundleTracker is not open */ + return; + } + t.untrack(bundle, null); + } + + /** + * Return the number of bundles being tracked by this + * BundleTracker. + * + * @return The number of bundles being tracked. + */ + public int size() { + final Tracked t = tracked(); + if (t == null) { /* if BundleTracker is not open */ + return 0; + } + synchronized (t) { + return t.size(); + } + } + + /** + * Returns the tracking count for this BundleTracker. + * + * The tracking count is initialized to 0 when this + * BundleTracker is opened. Every time a bundle is added, + * modified or removed from this BundleTracker the tracking + * count is incremented. + * + *

      + * The tracking count can be used to determine if this + * BundleTracker has added, modified or removed a bundle by + * comparing a tracking count value previously collected with the current + * tracking count value. If the value has not changed, then no bundle has + * been added, modified or removed from this BundleTracker + * since the previous tracking count was collected. + * + * @return The tracking count for this BundleTracker or -1 if + * this BundleTracker is not open. + */ + public int getTrackingCount() { + final Tracked t = tracked(); + if (t == null) { /* if BundleTracker is not open */ + return -1; + } + synchronized (t) { + return t.getTrackingCount(); + } + } + + /** + * Inner class which subclasses AbstractTracked. This class is the + * SynchronousBundleListener object for the tracker. + * + * @ThreadSafe + * @since 1.4 + */ + class Tracked extends AbstractTracked implements SynchronousBundleListener { + /** + * Tracked constructor. + */ + Tracked() { + super(); + } + + /** + * BundleListener method for the BundleTracker + * class. This method must NOT be synchronized to avoid deadlock + * potential. + * + * @param event BundleEvent object from the framework. + */ + public void bundleChanged(final BundleEvent event) { + /* + * Check if we had a delayed call (which could happen when we + * close). + */ + if (closed) { + return; + } + final Bundle bundle = event.getBundle(); + final int state = bundle.getState(); + if (DEBUG) { + System.out + .println("BundleTracker.Tracked.bundleChanged[" + state + "]: " + bundle); //$NON-NLS-1$ //$NON-NLS-2$ + } + + if ((state & mask) != 0) { + track(bundle, event); + /* + * If the customizer throws an unchecked exception, it is safe + * to let it propagate + */ + } + else { + untrack(bundle, event); + /* + * If the customizer throws an unchecked exception, it is safe + * to let it propagate + */ + } + } + + /** + * Call the specific customizer adding method. This method must not be + * called while synchronized on this object. + * + * @param item Item to be tracked. + * @param related Action related object. + * @return Customized object for the tracked item or null + * if the item is not to be tracked. + */ + Object customizerAdding(final Object item, + final Object related) { + return customizer + .addingBundle((Bundle) item, (BundleEvent) related); + } + + void customizerAdded(final Object item, final Object related, final Object object) { + customizer.addedBundle((Bundle) item, (BundleEvent) related, object); + } + + /** + * Call the specific customizer modified method. This method must not be + * called while synchronized on this object. + * + * @param item Tracked item. + * @param related Action related object. + * @param object Customized object for the tracked item. + */ + void customizerModified(final Object item, + final Object related, final Object object) { + customizer.modifiedBundle((Bundle) item, (BundleEvent) related, + object); + } + + /** + * Call the specific customizer removed method. This method must not be + * called while synchronized on this object. + * + * @param item Tracked item. + * @param related Action related object. + * @param object Customized object for the tracked item. + */ + void customizerRemoved(final Object item, + final Object related, final Object object) { + customizer.removedBundle((Bundle) item, (BundleEvent) related, + object); + } + + @Override + AbstractCustomizerActionSet createCustomizerActionSet() { + return new AbstractCustomizerActionSet() { + + @Override + public void addCustomizerAdded(Object item, Object related, Object object) { + customizerAdded(item, related, object); + } + + @Override + public void addCustomizerModified(Object item, Object related, + Object object) { + customizerModified(item, related, object); + } + @Override + public void addCustomizerRemoved(Object item, Object related, + Object object) { + customizerRemoved(item, related, object); + } + + @Override + void execute() { + // nothing to be done here, since this actionSet executes the actions immediately. + } + }; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/BundleTrackerCustomizer.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/BundleTrackerCustomizer.java new file mode 100644 index 00000000000..0fd340ef7af --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/BundleTrackerCustomizer.java @@ -0,0 +1,106 @@ +package org.apache.felix.dm.tracker; +/* + * Copyright (c) OSGi Alliance (2007, 2008). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleEvent; + +/** + * The BundleTrackerCustomizer interface allows a + * BundleTracker to customize the Bundles that are + * tracked. A BundleTrackerCustomizer is called when a bundle is + * being added to a BundleTracker. The + * BundleTrackerCustomizer can then return an object for the + * tracked bundle. A BundleTrackerCustomizer is also called when a + * tracked bundle is modified or has been removed from a + * BundleTracker. + * + *

      + * The methods in this interface may be called as the result of a + * BundleEvent being received by a BundleTracker. + * Since BundleEvents are received synchronously by the + * BundleTracker, it is highly recommended that implementations of + * these methods do not alter bundle states while being synchronized on any + * object. + * + *

      + * The BundleTracker class is thread-safe. It does not call a + * BundleTrackerCustomizer while holding any locks. + * BundleTrackerCustomizer implementations must also be + * thread-safe. + * + * @ThreadSafe + * @version $Revision: 5874 $ + * @since 1.4 + */ +public interface BundleTrackerCustomizer { + /** + * 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 + * {@link BundleTracker#getObject(Bundle) 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. + */ + public Object addingBundle(Bundle bundle, BundleEvent event); + + /** marrs: A bundle has been added to the BundleTracker. */ + public void addedBundle(Bundle bundle, BundleEvent event, Object object); + + /** + * 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. + */ + public void modifiedBundle(Bundle bundle, BundleEvent event, + Object object); + + /** + * 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. + */ + public void removedBundle(Bundle bundle, BundleEvent event, + Object object); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/ServiceTracker.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/ServiceTracker.java new file mode 100644 index 00000000000..9148a5d005e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/ServiceTracker.java @@ -0,0 +1,1482 @@ +package org.apache.felix.dm.tracker; +/* + * Copyright (c) OSGi Alliance (2000, 2009). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.felix.dm.impl.ServiceUtil; +import org.osgi.framework.AllServiceListener; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.Version; + +/** + * The ServiceTracker class simplifies using services from the + * Framework's service registry. + *

      + * A ServiceTracker object is constructed with search criteria and + * a ServiceTrackerCustomizer object. A ServiceTracker + * can use a ServiceTrackerCustomizer to customize the service + * objects to be tracked. The ServiceTracker can then be opened to + * begin tracking all services in the Framework's service registry that match + * the specified search criteria. The ServiceTracker correctly + * handles all of the details of listening to ServiceEvents and + * getting and ungetting services. + *

      + * The getServiceReferences method can be called to get references + * to the services being tracked. The getService and + * getServices methods can be called to get the service objects for + * the tracked service. + *

      + * The ServiceTracker class is thread-safe. It does not call a + * ServiceTrackerCustomizer while holding any locks. + * ServiceTrackerCustomizer implementations must also be + * thread-safe. + * + * @ThreadSafe + * @version $Revision: 6386 $ + */ +@SuppressWarnings("rawtypes") +public class ServiceTracker implements ServiceTrackerCustomizer { + /* set this to true to compile in debug messages */ + static final boolean DEBUG = false; + /** + * The Bundle Context used by this ServiceTracker. + */ + protected final BundleContext context; + /** + * The Filter used by this ServiceTracker which specifies the + * search criteria for the services to track. + * + * @since 1.1 + */ + protected final Filter filter; + /** + * The ServiceTrackerCustomizer for this tracker. + */ + final ServiceTrackerCustomizer customizer; + /** + * Filter string for use when adding the ServiceListener. If this field is + * set, then certain optimizations can be taken since we don't have a user + * supplied filter. + */ + final String listenerFilter; + /** + * Class name to be tracked. If this field is set, then we are tracking by + * class name. + */ + private final String trackClass; + /** + * Reference to be tracked. If this field is set, then we are tracking a + * single ServiceReference. + */ + private final ServiceReference trackReference; + /** + * Tracked services: ServiceReference -> customized Object and + * ServiceListener object + */ + private volatile Tracked tracked; + + /** + * Accessor method for the current Tracked object. This method is only + * intended to be used by the unsynchronized methods which do not modify the + * tracked field. + * + * @return The current Tracked object. + */ + private Tracked tracked() { + return tracked; + } + + /** + * Cached ServiceReference for getServiceReference. + * + * This field is volatile since it is accessed by multiple threads. + */ + private volatile ServiceReference cachedReference; + /** + * Cached service object for getService. + * + * This field is volatile since it is accessed by multiple threads. + */ + private volatile Object cachedService; + + /** + * org.osgi.framework package version which introduced + * {@link ServiceEvent#MODIFIED_ENDMATCH} + */ + private static final Version endMatchVersion = new Version(1, 5, 0); + + /** + * Flag that gets set when opening the tracker, determines if the tracker should + * track all aspects or just the highest ranked ones. + */ + public boolean m_trackAllAspects; + + private boolean debug = false; + private String debugKey; + + public void setDebug(String debugKey) { + this.debug = true; + this.debugKey = debugKey; + } + + /** + * Create a ServiceTracker on the specified + * ServiceReference. + * + *

      + * The service referenced by the specified ServiceReference + * will be tracked by this ServiceTracker. + * + * @param context The BundleContext against which the tracking + * is done. + * @param reference The ServiceReference for the service to be + * tracked. + * @param customizer The customizer object to call when services are added, + * modified, or removed in this ServiceTracker. If + * customizer is null, then this + * ServiceTracker will be used as the + * ServiceTrackerCustomizer and this + * ServiceTracker will call the + * ServiceTrackerCustomizer methods on itself. + */ + public ServiceTracker(final BundleContext context, + final ServiceReference reference, + final ServiceTrackerCustomizer customizer) { + this.context = context; + this.trackReference = reference; + this.trackClass = null; + this.customizer = (customizer == null) ? this : customizer; + this.listenerFilter = "(" + Constants.SERVICE_ID + "=" + + reference.getProperty(Constants.SERVICE_ID).toString() + ")"; + try { + this.filter = context.createFilter(listenerFilter); + } + catch (InvalidSyntaxException e) { + /* + * we could only get this exception if the ServiceReference was + * invalid + */ + IllegalArgumentException iae = new IllegalArgumentException( + "unexpected InvalidSyntaxException: " + e.getMessage()); + iae.initCause(e); + throw iae; + } + } + + /** + * Create a ServiceTracker on the specified class name. + * + *

      + * Services registered under the specified class name will be tracked by + * this ServiceTracker. + * + * @param context The BundleContext against which the tracking + * is done. + * @param clazz The class name of the services to be tracked. + * @param customizer The customizer object to call when services are added, + * modified, or removed in this ServiceTracker. If + * customizer is null, then this + * ServiceTracker will be used as the + * ServiceTrackerCustomizer and this + * ServiceTracker will call the + * ServiceTrackerCustomizer methods on itself. + */ + public ServiceTracker(final BundleContext context, final String clazz, + final ServiceTrackerCustomizer customizer) { + this.context = context; + this.trackReference = null; + this.trackClass = clazz; + this.customizer = (customizer == null) ? this : customizer; + // we call clazz.toString to verify clazz is non-null! + this.listenerFilter = "(" + Constants.OBJECTCLASS + "=" + + clazz.toString() + ")"; + try { + this.filter = context.createFilter(listenerFilter); + } + catch (InvalidSyntaxException e) { + /* + * we could only get this exception if the clazz argument was + * malformed + */ + IllegalArgumentException iae = new IllegalArgumentException( + "unexpected InvalidSyntaxException: " + e.getMessage()); + iae.initCause(e); + throw iae; + } + } + + /** + * Create a ServiceTracker on the specified Filter + * object. + * + *

      + * Services which match the specified Filter object will be + * tracked by this ServiceTracker. + * + * @param context The BundleContext against which the tracking + * is done. + * @param filter The Filter to select the services to be + * tracked. + * @param customizer The customizer object to call when services are added, + * modified, or removed in this ServiceTracker. If + * customizer is null, then this ServiceTracker will be + * used as the ServiceTrackerCustomizer and this + * ServiceTracker will call the + * ServiceTrackerCustomizer methods on itself. + * @since 1.1 + */ + public ServiceTracker(final BundleContext context, final Filter filter, + final ServiceTrackerCustomizer customizer) { + this.context = context; + this.trackReference = null; + this.trackClass = null; + final Version frameworkVersion = (Version) AccessController + .doPrivileged(new PrivilegedAction() { + public Version run() { + String version = context + .getProperty(Constants.FRAMEWORK_VERSION); + return (version == null) ? Version.emptyVersion + : new Version(version); + } + }); + final boolean endMatchSupported = (frameworkVersion + .compareTo(endMatchVersion) >= 0); + this.listenerFilter = endMatchSupported ? filter.toString() : null; + this.filter = filter; + this.customizer = (customizer == null) ? this : customizer; + if ((context == null) || (filter == null)) { + /* + * we throw a NPE here to be consistent with the other constructors + */ + throw new NullPointerException(); + } + } + + /** + * Open this ServiceTracker and begin tracking services. + * + *

      + * This implementation calls open(false). + * + * @throws java.lang.IllegalStateException If the BundleContext + * with which this ServiceTracker was created is no + * longer valid. + * @see #open(boolean) + */ + public void open() { + open(false); + } + + /** + * Open this ServiceTracker and begin tracking services. + * + *

      + * Services which match the search criteria specified when this + * ServiceTracker was created are now tracked by this + * ServiceTracker. + * + * @param trackAllServices If true, then this + * ServiceTracker will track all matching services + * regardless of class loader accessibility. If false, + * then this ServiceTracker will only track matching + * services which are class loader accessible to the bundle whose + * BundleContext is used by this + * ServiceTracker. + * @throws java.lang.IllegalStateException If the BundleContext + * with which this ServiceTracker was created is no + * longer valid. + * @since 1.3 + */ + public void open(boolean trackAllServices) { + open(trackAllServices, false); + } + + /** + * Open this ServiceTracker and begin tracking services. + * + *

      + * Services which match the search criteria specified when this + * ServiceTracker was created are now tracked by this + * ServiceTracker. + * + * @param trackAllServices If true, then this + * ServiceTracker will track all matching services + * regardless of class loader accessibility. If false, + * then this ServiceTracker will only track matching + * services which are class loader accessible to the bundle whose + * BundleContext is used by this + * ServiceTracker. + * @param trackAllAspects If true then this + * ServiceTracker will track all aspects regardless + * of their rank. If false only the highest ranked + * aspects (or the original service if there are no aspects) will + * be tracked. The latter is the default. + * @throws java.lang.IllegalStateException If the BundleContext + * with which this ServiceTracker was created is no + * longer valid. + */ + public void open(boolean trackAllServices, boolean trackAllAspects) { + if (debug) { + System.out.println("[ServiceTracker] " + debugKey + " T" + Thread.currentThread().getId() + " open"); + } + final Tracked t; + synchronized (this) { + if (tracked != null) { + return; + } + if (DEBUG) { + System.out.println("ServiceTracker.open: " + filter); + } + m_trackAllAspects = trackAllAspects; + t = trackAllServices ? new AllTracked() : new Tracked(); + synchronized (t) { + try { + context.addServiceListener(t, listenerFilter); + ServiceReference[] references = null; + if (trackClass != null) { + references = getInitialReferences(trackAllServices, + trackClass, null); + } + else { + if (trackReference != null) { + if (trackReference.getBundle() != null) { + references = new ServiceReference[] {trackReference}; + } + } + else { /* user supplied filter */ + references = getInitialReferences(trackAllServices, + null, + (listenerFilter != null) ? listenerFilter + : filter.toString()); + } + } + /* set tracked with the initial references */ + t.setInitial(references); + + } + catch (InvalidSyntaxException e) { + throw new RuntimeException( + "unexpected InvalidSyntaxException: " + + e.getMessage(), e); + } + } + tracked = t; + } + /* Call tracked outside of synchronized region */ + + // schedule trackInitial call: this method will invoke "addingService" customizer callbacks, but will schedule other customizer callbacks (added/modified/removed). + t.getExecutor().schedule(() -> t.trackInitial()); + + // Just trigger the execution of everything, but serially. + t.getExecutor().execute(); + } + + /** + * Returns the list of initial ServiceReferences that will be + * tracked by this ServiceTracker. + * + * @param trackAllServices If true, use + * getAllServiceReferences. + * @param className The class name with which the service was registered, or + * null for all services. + * @param filterString The filter criteria or null for all + * services. + * @return The list of initial ServiceReferences. + * @throws InvalidSyntaxException If the specified filterString has an + * invalid syntax. + */ + private ServiceReference[] getInitialReferences(boolean trackAllServices, + String className, String filterString) + throws InvalidSyntaxException { + if (trackAllServices) { + return context.getAllServiceReferences(className, filterString); + } + return context.getServiceReferences(className, filterString); + } + + /** + * Close this ServiceTracker. + * + *

      + * This method should be called when this ServiceTracker should + * end the tracking of services. + * + *

      + * This implementation calls {@link #getServiceReferences()} to get the list + * of tracked services to remove. + */ + public void close() { + final Tracked outgoing; + final ServiceReference[] references; + synchronized (this) { + outgoing = tracked; + if (outgoing == null) { + return; + } + if (DEBUG) { + System.out.println("ServiceTracker.close: " + filter); + } + outgoing.close(); + references = getServiceReferences(); + tracked = null; + try { + context.removeServiceListener(outgoing); + } + catch (IllegalStateException e) { + /* In case the context was stopped. */ + } + } + modified(); /* clear the cache */ + synchronized (outgoing) { + outgoing.notifyAll(); /* wake up any waiters */ + } + if (references != null) { + for (int i = 0; i < references.length; i++) { + outgoing.untrack(references[i], null).execute(); + } + } + if (DEBUG) { + if ((cachedReference == null) && (cachedService == null)) { + System.out + .println("ServiceTracker.close[cached cleared]: " + + filter); + } + } + } + + /** + * Default implementation of the + * ServiceTrackerCustomizer.addingService method. + * + *

      + * This method is only called when this ServiceTracker has been + * constructed with a null ServiceTrackerCustomizer argument. + * + *

      + * This implementation returns the result of calling getService + * on the BundleContext with which this + * ServiceTracker was created passing the specified + * ServiceReference. + *

      + * This method can be overridden in a subclass to customize the service + * object to be tracked for the service being added. In that case, take care + * not to rely on the default implementation of + * {@link #removedService(ServiceReference, Object) removedService} to unget + * the service. + * + * @param reference The reference to the service being added to this + * ServiceTracker. + * @return The service object to be tracked for the service added to this + * ServiceTracker. + * @see ServiceTrackerCustomizer#addingService(ServiceReference) + */ + @SuppressWarnings("unchecked") + public Object addingService(ServiceReference reference) { + return context.getService(reference); + } + + public void addedService(ServiceReference reference, Object service) { + /* do nothing */ + } + + /** + * Default implementation of the + * ServiceTrackerCustomizer.modifiedService method. + * + *

      + * This method is only called when this ServiceTracker has been + * constructed with a null ServiceTrackerCustomizer argument. + * + *

      + * This implementation does nothing. + * + * @param reference The reference to modified service. + * @param service The service object for the modified service. + * @see ServiceTrackerCustomizer#modifiedService(ServiceReference, Object) + */ + public void modifiedService(ServiceReference reference, Object service) { + /* do nothing */ + } + + /** + * Default implementation of the + * ServiceTrackerCustomizer.removedService method. + * + *

      + * This method is only called when this ServiceTracker has been + * constructed with a null ServiceTrackerCustomizer argument. + * + *

      + * This implementation calls ungetService, on the + * BundleContext with which this ServiceTracker + * was created, passing the specified ServiceReference. + *

      + * This method can be overridden in a subclass. If the default + * implementation of {@link #addingService(ServiceReference) addingService} + * method was used, this method must unget the service. + * + * @param reference The reference to removed service. + * @param service The service object for the removed service. + * @see ServiceTrackerCustomizer#removedService(ServiceReference, Object) + */ + public void removedService(ServiceReference reference, Object service) { + context.ungetService(reference); + } + + /** + * Wait for at least one service to be tracked by this + * ServiceTracker. This method will also return when this + * ServiceTracker is closed. + * + *

      + * It is strongly recommended that waitForService is not used + * during the calling of the BundleActivator methods. + * BundleActivator methods are expected to complete in a short + * period of time. + * + *

      + * This implementation calls {@link #getService()} to determine if a service + * is being tracked. + * + * @param timeout The time interval in milliseconds to wait. If zero, the + * method will wait indefinitely. + * @return Returns the result of {@link #getService()}. + * @throws InterruptedException If another thread has interrupted the + * current thread. + * @throws IllegalArgumentException If the value of timeout is negative. + */ + public Object waitForService(long timeout) throws InterruptedException { + if (timeout < 0) { + throw new IllegalArgumentException("timeout value is negative"); + } + Object object = getService(); + while (object == null) { + final Tracked t = tracked(); + if (t == null) { /* if ServiceTracker is not open */ + return null; + } + synchronized (t) { + if (t.size() == 0) { + t.wait(timeout); + } + } + object = getService(); + if (timeout > 0) { + return object; + } + } + return object; + } + + /** + * Return an array of ServiceReferences for all services being + * tracked by this ServiceTracker. + * + * @return Array of ServiceReferences or null if + * no services are being tracked. + */ + public ServiceReference[] getServiceReferences() { + final Tracked t = tracked(); + if (t == null) { /* if ServiceTracker is not open */ + return null; + } + synchronized (t) { + int length = t.size(); + if (length == 0) { + return null; + } + return (ServiceReference[]) t + .getTracked(new ServiceReference[length]); + } + } + + /** + * Returns a boolean indicating whether this ServiceTracker is tracking any services. + * + * @return true if services are being tracked, false if no services are being tracked. + */ + public boolean hasReference() { + if (cachedReference != null) { + return true; + } + final Tracked t = tracked(); + if (t == null) { /* if ServiceTracker is not open */ + return false; + } + synchronized (t) { + int length = t.size(); + return length > 0; + } + } + + /** + * Returns a ServiceReference for one of the services being + * tracked by this ServiceTracker. + * + *

      + * If multiple services are being tracked, the service with the highest + * ranking (as specified in its service.ranking property) is + * returned. If there is a tie in ranking, the service with the lowest + * service ID (as specified in its service.id property); that + * is, the service that was registered first is returned. This is the same + * algorithm used by BundleContext.getServiceReference. + * + *

      + * This implementation calls {@link #getServiceReferences()} to get the list + * of references for the tracked services. + * + * @return A ServiceReference or null if no + * services are being tracked. + * @since 1.1 + */ + public ServiceReference getServiceReference() { + ServiceReference reference = cachedReference; + if (reference != null) { + if (DEBUG) { + System.out + .println("ServiceTracker.getServiceReference[cached]: " + + filter); + } + return reference; + } + if (DEBUG) { + System.out.println("ServiceTracker.getServiceReference: " + filter); + } + ServiceReference[] references = getServiceReferences(); + int length = (references == null) ? 0 : references.length; + if (length == 0) { /* if no service is being tracked */ + return null; + } + int index = 0; + if (length > 1) { /* if more than one service, select highest ranking */ + int rankings[] = new int[length]; + int count = 0; + int maxRanking = Integer.MIN_VALUE; + for (int i = 0; i < length; i++) { + Object property = references[i] + .getProperty(Constants.SERVICE_RANKING); + int ranking = (property instanceof Integer) ? ((Integer) property) + .intValue() + : 0; + rankings[i] = ranking; + if (ranking > maxRanking) { + index = i; + maxRanking = ranking; + count = 1; + } + else { + if (ranking == maxRanking) { + count++; + } + } + } + if (count > 1) { /* if still more than one service, select lowest id */ + long minId = Long.MAX_VALUE; + for (int i = 0; i < length; i++) { + if (rankings[i] == maxRanking) { + long id = ((Long) (references[i] + .getProperty(Constants.SERVICE_ID))) + .longValue(); + if (id < minId) { + index = i; + minId = id; + } + } + } + } + } + return cachedReference = references[index]; + } + + /** + * Returns the service object for the specified + * ServiceReference if the specified referenced service is + * being tracked by this ServiceTracker. + * + * @param reference The reference to the desired service. + * @return A service object or null if the service referenced + * by the specified ServiceReference is not being + * tracked. + */ + public Object getService(ServiceReference reference) { + final Tracked t = tracked(); + if (t == null) { /* if ServiceTracker is not open */ + return null; + } + synchronized (t) { + return t.getCustomizedObject(reference); + } + } + + /** + * Return an array of service objects for all services being tracked by this + * ServiceTracker. + * + *

      + * This implementation calls {@link #getServiceReferences()} to get the list + * of references for the tracked services and then calls + * {@link #getService(ServiceReference)} for each reference to get the + * tracked service object. + * + * @return An array of service objects or null if no services + * are being tracked. + */ + public Object[] getServices() { + final Tracked t = tracked(); + if (t == null) { /* if ServiceTracker is not open */ + return null; + } + synchronized (t) { + ServiceReference[] references = getServiceReferences(); + int length = (references == null) ? 0 : references.length; + if (length == 0) { + return null; + } + Object[] objects = new Object[length]; + for (int i = 0; i < length; i++) { + objects[i] = getService(references[i]); + } + return objects; + } + } + + /** + * Returns a service object for one of the services being tracked by this + * ServiceTracker. + * + *

      + * If any services are being tracked, this implementation returns the result + * of calling getService(getServiceReference()). + * + * @return A service object or null if no services are being + * tracked. + */ + public Object getService() { + Object service = cachedService; + if (service != null) { + if (DEBUG) { + System.out + .println("ServiceTracker.getService[cached]: " + + filter); + } + return service; + } + if (DEBUG) { + System.out.println("ServiceTracker.getService: " + filter); + } + ServiceReference reference = getServiceReference(); + if (reference == null) { + return null; + } + return cachedService = getService(reference); + } + + /** + * Remove a service from this ServiceTracker. + * + * The specified service will be removed from this + * ServiceTracker. If the specified service was being tracked + * then the ServiceTrackerCustomizer.removedService method will + * be called for that service. + * + * @param reference The reference to the service to be removed. + */ + public void remove(ServiceReference reference) { + final Tracked t = tracked(); + if (t == null) { /* if ServiceTracker is not open */ + return; + } + t.untrack(reference, null).execute(); + } + + /** + * Return the number of services being tracked by this + * ServiceTracker. + * + * @return The number of services being tracked. + */ + public int size() { + final Tracked t = tracked(); + if (t == null) { /* if ServiceTracker is not open */ + return 0; + } + synchronized (t) { + return t.size(); + } + } + + /** + * Returns the tracking count for this ServiceTracker. + * + * The tracking count is initialized to 0 when this + * ServiceTracker is opened. Every time a service is added, + * modified or removed from this ServiceTracker, the tracking + * count is incremented. + * + *

      + * The tracking count can be used to determine if this + * ServiceTracker has added, modified or removed a service by + * comparing a tracking count value previously collected with the current + * tracking count value. If the value has not changed, then no service has + * been added, modified or removed from this ServiceTracker + * since the previous tracking count was collected. + * + * @since 1.2 + * @return The tracking count for this ServiceTracker or -1 if + * this ServiceTracker is not open. + */ + public int getTrackingCount() { + final Tracked t = tracked(); + if (t == null) { /* if ServiceTracker is not open */ + return -1; + } + synchronized (t) { + return t.getTrackingCount(); + } + } + + /** + * Called by the Tracked object whenever the set of tracked services is + * modified. Clears the cache. + */ + /* + * This method must not be synchronized since it is called by Tracked while + * Tracked is synchronized. We don't want synchronization interactions + * between the listener thread and the user thread. + */ + void modified() { + cachedReference = null; /* clear cached value */ + cachedService = null; /* clear cached value */ + if (DEBUG) { + System.out.println("ServiceTracker.modified: " + filter); + } + } + + /** + * Inner class which subclasses AbstractTracked. This class is the + * ServiceListener object for the tracker. + * + * @ThreadSafe + */ + class Tracked extends AbstractTracked implements ServiceListener { + /** + * A list of services that are currently hidden because there is an aspect available with a higher ranking. + * @GuardedBy this + */ + private final Map> m_highestTrackedCache = new HashMap<>(); + private final Map> m_highestHiddenCache = new HashMap<>(); + + private ServiceReference highestTrackedCache(long serviceId) { + Long sid = Long.valueOf(serviceId); + synchronized (this) { + TreeSet services = m_highestTrackedCache.get(sid); + if (services != null && services.size() > 0) { + ServiceReference result = (ServiceReference) services.last(); + return result; + } + } + return null; + } + + private void addHighestTrackedCache(ServiceReference reference) { + Long serviceId = ServiceUtil.getServiceIdObject(reference); + synchronized (this) { + TreeSet services = m_highestTrackedCache.get(serviceId); + if (services == null) { + services = new TreeSet(); + m_highestTrackedCache.put(serviceId, services); + } + services.add(reference); + } + } + + private void removeHighestTrackedCache(ServiceReference reference) { + Long serviceId = ServiceUtil.getServiceIdObject(reference); + synchronized (this) { + TreeSet services = m_highestTrackedCache.get(serviceId); + if (services != null) { + services.remove(reference); + if (services.size() == 0) { + m_highestTrackedCache.remove(serviceId); + } + } + } + } + + private void clearHighestTrackedCache() { + synchronized (this) { + m_highestTrackedCache.clear(); + } + } + + private ServiceReference highestHiddenCache(long serviceId) { + Long sid = Long.valueOf(serviceId); + synchronized (this) { + TreeSet services = m_highestHiddenCache.get(sid); + if (services != null && services.size() > 0) { + ServiceReference result = (ServiceReference) services.last(); + return result; + } + } + return null; + } + + private void addHighestHiddenCache(ServiceReference reference) { + Long serviceId = ServiceUtil.getServiceIdObject(reference); + synchronized (this) { + TreeSet services = m_highestHiddenCache.get(serviceId); + if (services == null) { + services = new TreeSet(); + m_highestHiddenCache.put(serviceId, services); + } + services.add(reference); + } + } + + private void removeHighestHiddenCache(ServiceReference reference) { + Long serviceId = ServiceUtil.getServiceIdObject(reference); + synchronized (this) { + TreeSet services = m_highestHiddenCache.get(serviceId); + if (services != null) { + services.remove(reference); + } + } + } + + /** + * Hide a service reference, placing it in the list of hidden services. + * + * @param ref the service reference to add to the hidden list + */ + private void hide(ServiceReference ref) { + addHighestHiddenCache(ref); + } + + /** + * Unhide a service reference, removing it from the list of hidden services. + * + * @param ref the service reference to remove from the hidden list + */ + private void unhide(ServiceReference ref) { + removeHighestHiddenCache(ref); + } + + /** + * Tracked constructor. + */ + Tracked() { + super(); + setTracked(new HashMapCache()); + } + + void setInitial(Object[] list) { + if (list == null) { + return; + } + if (m_trackAllAspects) { + // not hiding aspects + super.setInitial(list); + } else { + Map highestRankedServiceMap = new HashMap<>(); + for (int i = 0; i < list.length; i++) { + ServiceReference sr = (ServiceReference) list[i]; + if (sr != null) { + Long serviceId = ServiceUtil.getServiceIdAsLong(sr); + int ranking = ServiceUtil.getRanking(sr); + + RankedService rs = (RankedService) highestRankedServiceMap.get(serviceId); + if (rs == null) { + // the service did not exist yet in our map + highestRankedServiceMap.put(serviceId, new RankedService(ranking, sr)); + } + else if (ranking > rs.getRanking()) { + // the service replaces a lower ranked one + hide(rs.getServiceReference()); + rs.update(ranking, sr); + } + else { + // the service does NOT replace a lower ranked one + hide(sr); + } + } + } + if (highestRankedServiceMap.size() > 0) { + Object[] result = new Object[highestRankedServiceMap.size()]; + int index = 0; + for(Iterator> it = highestRankedServiceMap.entrySet().iterator(); it.hasNext(); ) { + Entry entry = it.next(); + result[index] = ((RankedService)entry.getValue()).getServiceReference(); + index++; + } + super.setInitial(result); + } + } + } + + /** + * ServiceListener method for the + * ServiceTracker class. This method must NOT be + * synchronized to avoid deadlock potential. + * + * @param event ServiceEvent object from the framework. + */ + public void serviceChanged(final ServiceEvent event) { + if (m_trackAllAspects) { + serviceChangedIncludeAspects(event); + } + else { + serviceChangedHideAspects(event); + } + } + + public void serviceChangedIncludeAspects(final ServiceEvent event) { + /* + * Check if we had a delayed call (which could happen when we + * close). + */ + if (closed) { + return; + } + final ServiceReference reference = event.getServiceReference(); + if (debug) { + System.out.println("[ServiceTracker] " + debugKey + " T" + Thread.currentThread().getId() + " [serviceChangedIncludeAspects] " + reference.getProperty("service.ranking")); + } + if (DEBUG) { + System.out + .println("ServiceTracker.Tracked.serviceChanged[" + + event.getType() + "]: " + reference); + } + + switch (event.getType()) { + case ServiceEvent.REGISTERED : + case ServiceEvent.MODIFIED : + if (listenerFilter != null) { // service listener added with + // filter + track(reference, event).execute(); + /* + * If the customizer throws an unchecked exception, it + * is safe to let it propagate + */ + } + else { // service listener added without filter + if (filter.match(reference)) { + track(reference, event).execute(); + /* + * If the customizer throws an unchecked exception, + * it is safe to let it propagate + */ + } + else { + untrack(reference, event).execute(); + /* + * If the customizer throws an unchecked exception, + * it is safe to let it propagate + */ + } + } + break; + case 8 /* ServiceEvent.MODIFIED_ENDMATCH */ : + case ServiceEvent.UNREGISTERING : + untrack(reference, event).execute(); + /* + * If the customizer throws an unchecked exception, it is + * safe to let it propagate + */ + break; + } + } + + private boolean isModifiedEndmatchSupported() { + return listenerFilter != null; + } + + private AtomicInteger step = new AtomicInteger(); + + public void serviceChangedHideAspects(final ServiceEvent event) { + int n = step.getAndIncrement(); + /* + * Check if we had a delayed call (which could happen when we + * close). + */ + if (closed) { + return; + } + final ServiceReference reference = event.getServiceReference(); + if (DEBUG) { + System.out + .println(n + " ServiceTracker.Tracked.serviceChanged[" + + event.getType() + "]: " + reference); + } + + long sid = ServiceUtil.getServiceId(reference); + AbstractCustomizerActionSet actionSet = null; + synchronized(this) { + switch (event.getType()) { + case ServiceEvent.REGISTERED : + case ServiceEvent.MODIFIED : + ServiceReference higherRankedReference = null; + ServiceReference lowerRankedReference = null; + ServiceReference highestTrackedReference = highestTrackedCache(sid); + if (highestTrackedReference != null) { + int ranking = ServiceUtil.getRanking(reference); + int highestTrackedRanking = ServiceUtil.getRanking(highestTrackedReference); + if (ranking > highestTrackedRanking) { + // found a higher ranked one! + if (DEBUG) { + System.out.println("ServiceTracker.Tracked.serviceChanged[" + event.getType() + "]: Found a higher ranked aspect: " + ServiceUtil.toString(reference) + " vs " + ServiceUtil.toString(highestTrackedReference)); + } + higherRankedReference = highestTrackedReference; + } + else if (ranking < highestTrackedRanking) { + // found lower ranked one! + if (DEBUG) { + System.out.println("ServiceTracker.Tracked.serviceChanged[" + event.getType() + "]: Found a lower ranked aspect: " + ServiceUtil.toString(reference) + " vs " + ServiceUtil.toString(highestTrackedReference)); + } + lowerRankedReference = highestTrackedReference; + } + } + if (isModifiedEndmatchSupported()) { // either registered or modified + actionSet = registerOrUpdate(event, reference, higherRankedReference, lowerRankedReference); + } + else { // service listener added without filter + if (filter.match(reference)) { + actionSet = registerOrUpdate(event, reference, higherRankedReference, lowerRankedReference); + } + else { + actionSet = unregister(event, reference, sid); + } + } + break; + case 8 /* ServiceEvent.MODIFIED_ENDMATCH */ : // handle as unregister + case ServiceEvent.UNREGISTERING : + actionSet = unregister(event, reference, sid); + /* + * If the customizer throws an unchecked exception, it is + * safe to let it propagate + */ + break; + } + // schedule the actionset for execution. We'll use a serial executor to prevent the actions to + // be performed out of order. + final AbstractCustomizerActionSet commandActionSet = actionSet; + if (commandActionSet != null) { + getExecutor().schedule(new Runnable() { + + @Override + public void run() { + commandActionSet.execute(); + } + + }); + } + } + getExecutor().execute(); + } + + private AbstractCustomizerActionSet registerOrUpdate(final ServiceEvent event, + final ServiceReference reference, ServiceReference higher, + ServiceReference lower) { + if (debug) { +// System.out.println("[ServiceTracker] " + debugKey + " T" + Thread.currentThread().getId() + " [registerOrUpdate] lower: " + lower + ", higher: " + higher); + } + AbstractCustomizerActionSet actionSet = null; + if (lower != null) { + hide(reference); + } + else { + actionSet = track(reference, event); + if (higher != null) { + actionSet.appendActionSet(untrack(higher, null)); + hide(higher); + } + } + /* + * If the customizer throws an unchecked exception, it + * is safe to let it propagate + */ + return actionSet; + } + + private AbstractCustomizerActionSet unregister(final ServiceEvent event, + final ServiceReference reference, long sid) { + if (debug) { + System.out.println("[ServiceTracker] " + debugKey + " T" + Thread.currentThread().getId() + " [unregister] " + reference.getProperty("service.ranking")); + } + AbstractCustomizerActionSet actionSet = null; + ServiceReference ht = highestTrackedCache(sid); + if (reference.equals(ht)) { + ServiceReference hh = highestHiddenCache(sid); + + if (hh != null) { + unhide(hh); + actionSet = track(hh, null); + } + if (actionSet == null) { + actionSet = untrack(reference, event); + } else { + actionSet.appendActionSet(untrack(reference, event)); + } + } + else { + unhide(reference); + } + return actionSet; + } + + + + /** + * Increment the tracking count and tell the tracker there was a + * modification. + * + * @GuardedBy this + */ + void modified() { + super.modified(); /* increment the modification count */ + ServiceTracker.this.modified(); + } + + /** + * Call the specific customizer adding method. This method must not be + * called while synchronized on this object. + * + * @param item Item to be tracked. + * @param related Action related object. + * @return Customized object for the tracked item or null + * if the item is not to be tracked. + */ + Object customizerAdding(final Object item, + final Object related) { + return customizer.addingService((ServiceReference) item); + } + + void customizerAdded(final Object item, final Object related, final Object object) { + customizer.addedService((ServiceReference) item, object); + } + + /** + * Call the specific customizer modified method. This method must not be + * called while synchronized on this object. + * + * @param item Tracked item. + * @param related Action related object. + * @param object Customized object for the tracked item. + */ + void customizerModified(final Object item, + final Object related, final Object object) { + customizer.modifiedService((ServiceReference) item, object); + } + + /** + * Call the specific customizer removed method. This method must not be + * called while synchronized on this object. + * + * @param item Tracked item. + * @param related Action related object. + * @param object Customized object for the tracked item. + */ + void customizerRemoved(final Object item, + final Object related, final Object object) { + customizer.removedService((ServiceReference) item, object); + } + + class HashMapCache extends LinkedHashMap { + + private static final long serialVersionUID = 1627005136730183946L; + + public V put(K key, V value) { + addHighestTrackedCache((ServiceReference) key); + return super.put(key, value); + } + + public void putAll(Map m) { + Iterator i = m.keySet().iterator(); + while (i.hasNext()) { + addHighestTrackedCache((ServiceReference) i.next()); + } + super.putAll(m); + } + + public V remove(Object key) { + removeHighestTrackedCache((ServiceReference) key); + return super.remove(key); + } + + public void clear() { + clearHighestTrackedCache(); + super.clear(); + } + } + + @Override + AbstractCustomizerActionSet createCustomizerActionSet() { + // This actions set deliberately postpones invocation of the customizer methods to be able to combine added and removed + // into a single swap call. + return new AbstractCustomizerActionSet() { + + @Override + public void addCustomizerAdded(Object item, Object related, + Object object) { + if (debug) { +// System.out.println("[ServiceTracker] " + debugKey + " T" + Thread.currentThread().getId() + " addCustomizerAdded " + object); + } + super.addCustomizerAdded(item, related, object); + } + + @Override + public void addCustomizerModified(Object item, Object related, + Object object) { + if (debug) { +// System.out.println("[ServiceTracker] " + debugKey + " T" + Thread.currentThread().getId() + " addCustomizerModified " + object); + } + super.addCustomizerModified(item, related, object); + } + + @Override + public void addCustomizerRemoved(Object item, Object related, + Object object) { + if (debug) { +// System.out.println("[ServiceTracker] " + debugKey + " T" + Thread.currentThread().getId() + " addCustomizerRemoved " + object); + } + super.addCustomizerRemoved(item, related, object); + } + + @Override + void execute() { + // inspect the actions and check whether we should perform a swap + List actions = getActions(); + if (actions.size() > 2) { + throw new IllegalStateException("Unexpected action count: " + actions.size()); + } + if (actions.size() == 2 && actions.get(0).getType() == Type.ADDED && actions.get(1).getType() == Type.REMOVED) { + // ignore related + // item = ServiceReference + // object = service + debug("swapped"); + if (debug) { + System.out.println("[ServiceTracker] " + debugKey + " T" + Thread.currentThread().getId() + " swapping " + actions.get(1).getObject() + " with " + actions.get(0).getObject()); + } + customizer.swappedService((ServiceReference)actions.get(1).getItem(), actions.get(1).getObject(), (ServiceReference)actions.get(0).getItem(), actions.get(0).getObject()); + } else { + // just sequentially call the customizer methods + for (CustomizerAction action : getActions()) { + try { + switch (action.getType()) { + case ADDED: + debug(Thread.currentThread().getId() + " added"); + if (debug) { + System.out.println("[ServiceTracker] " + debugKey + " T" + Thread.currentThread().getId() + " adding " + action.getObject()); + } + customizerAdded(action.getItem(), action.getRelated(), action.getObject()); + break; + case MODIFIED: + debug("modified"); + customizerModified(action.getItem(), action.getRelated(), action.getObject()); + break; + case REMOVED: + debug("removed"); + if (debug) { + System.out.println("[ServiceTracker] " + debugKey + " T" + Thread.currentThread().getId() + " removing " + action.getObject()); + } + customizerRemoved(action.getItem(), action.getRelated(), action.getObject()); + } + } catch (Exception e) { + // just continue. log messages will be printed elsewhere. + } + } + } + } + }; + } + + } + + private void debug(String message) { + if (customizer.toString().equals("ServiceDependency[interface dm.it.AspectRaceTest$S (&(!(org.apache.felix.dependencymanager.aspect=*))(id=1))]")) { +// System.out.println(message); + } + } + + /** + * Subclass of Tracked which implements the AllServiceListener interface. + * This class is used by the ServiceTracker if open is called with true. + * + * @since 1.3 + * @ThreadSafe + */ + class AllTracked extends Tracked implements AllServiceListener { + /** + * AllTracked constructor. + */ + AllTracked() { + super(); + setTracked(new HashMapCache()); + } + } + + /** + * Holds a ranking and a service reference that can be updated if necessary. + */ + private static final class RankedService { + private int m_ranking; + private ServiceReference m_serviceReference; + + public RankedService(int ranking, ServiceReference serviceReference) { + m_ranking = ranking; + m_serviceReference = serviceReference; + } + + public void update(int ranking, ServiceReference serviceReference) { + m_ranking = ranking; + m_serviceReference = serviceReference; + } + + public int getRanking() { + return m_ranking; + } + + public ServiceReference getServiceReference() { + return m_serviceReference; + } + } + + @Override + public void swappedService(ServiceReference reference, Object service, + ServiceReference newReference, Object newService) { + + } + + // Package private, used for unit testing Tracked + Tracked getTracked() { + return tracked; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/ServiceTrackerCustomizer.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/ServiceTrackerCustomizer.java new file mode 100644 index 00000000000..1077cca2173 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/ServiceTrackerCustomizer.java @@ -0,0 +1,116 @@ +package org.apache.felix.dm.tracker; +/* + * Copyright (c) OSGi Alliance (2000, 2008). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.osgi.framework.ServiceReference; + +/** + * The ServiceTrackerCustomizer interface allows a + * ServiceTracker to customize the service objects that are + * tracked. A ServiceTrackerCustomizer is called when a service is + * being added to a ServiceTracker. The + * ServiceTrackerCustomizer can then return an object for the + * tracked service. A ServiceTrackerCustomizer is also called when + * a tracked service is modified or has been removed from a + * ServiceTracker. + * + *

      + * The methods in this interface may be called as the result of a + * ServiceEvent being received by a ServiceTracker. + * Since ServiceEvents are synchronously delivered by the + * Framework, it is highly recommended that implementations of these methods do + * not register (BundleContext.registerService), modify ( + * ServiceRegistration.setProperties) or unregister ( + * ServiceRegistration.unregister) a service while being + * synchronized on any object. + * + *

      + * The ServiceTracker class is thread-safe. It does not call a + * ServiceTrackerCustomizer while holding any locks. + * ServiceTrackerCustomizer implementations must also be + * thread-safe. + * + * @ThreadSafe + * @version $Revision: 5874 $ + */ +@SuppressWarnings("rawtypes") +public interface ServiceTrackerCustomizer { + /** + * A service is being added to the ServiceTracker. + * + *

      + * This method is called before a service which matched the search + * parameters of the ServiceTracker is added to the + * ServiceTracker. This method should return the service object + * to be tracked for the specified ServiceReference. The + * returned service object is stored in the ServiceTracker and + * is available from the getService and + * getServices methods. + * + * @param reference The reference to the service being added to the + * ServiceTracker. + * @return The service object to be tracked for the specified referenced + * service or null if the specified referenced service + * should not be tracked. + */ + public Object addingService(ServiceReference reference); + + /** marrs: A service has been added to the ServiceTracker. */ + public void addedService(ServiceReference reference, Object service); + + /** + * A service tracked by the ServiceTracker has been modified. + * + *

      + * This method is called when a service being tracked by the + * ServiceTracker has had it properties modified. + * + * @param reference The reference to the service that has been modified. + * @param service The service object for the specified referenced service. + */ + public void modifiedService(ServiceReference reference, Object service); + + /** + * A service tracked by the ServiceTracker has an aspect service + * added or removed for a tracked service. + * + *

      + * This method is called when an aspect service has been either added or removed + * for a tracked service. This method will only be called when there's a new + * highest ranked service as result of adding or removal of the aspect service. + * In this case the previously highest ranked service is 'swapped' for the new + * highest ranked service ensuring the client always gets the highest ranked + * aspect. + * + * @param reference The reference for the old highest ranked service. + * @param service The service object for the old highest ranked service. + * @param newReference The reference to the new highest ranked service. + * @param newService The service object for the new highest ranked service. + */ + public void swappedService(ServiceReference reference, Object service, ServiceReference newReference, Object newService); + + /** + * A service tracked by the ServiceTracker has been removed. + * + *

      + * This method is called after a service is no longer being tracked by the + * ServiceTracker. + * + * @param reference The reference to the service that has been removed. + * @param service The service object for the specified referenced service. + */ + public void removedService(ServiceReference reference, Object service); +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/packageinfo b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/packageinfo new file mode 100644 index 00000000000..bd333699d2e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/tracker/packageinfo @@ -0,0 +1 @@ +version 4.0.0 \ No newline at end of file diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/.gitignore b/dependencymanager/org.apache.felix.dependencymanager/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/org/apache/felix/dm/impl/ConfigurableTest.java b/dependencymanager/org.apache.felix.dependencymanager/test/org/apache/felix/dm/impl/ConfigurableTest.java new file mode 100644 index 00000000000..40701373825 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/test/org/apache/felix/dm/impl/ConfigurableTest.java @@ -0,0 +1,271 @@ +/* + * 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.dm.impl; + +import static org.apache.felix.dm.impl.Configurable.create; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.SortedMap; +import java.util.Stack; +import java.util.TreeMap; + +import org.junit.Test; + +/** + * Test cases for {@link Configurable}. + */ +public class ConfigurableTest { + private static final double ERROR_MARGIN = 0.0001; + + static enum EnumValue { + ONE, TWO, THREE; + } + + static interface IfaceA { + int getInt(); + + double getDouble(); + + Byte getByte(); + + String getString(); + + int[] getArray(); + + boolean isBoolean(); + + List getList(); + + SortedMap getMap(); + + EnumValue getEnum(); + + Map> getMapStringList(); + + List[] getArrayOfList(); + } + + static interface IfaceB { + Stack getA(); + + Set getB(); + + Queue getC(); + } + + static interface IfaceC { + IfaceA getIfaceA(); + + Class getType(); + } + + @Test + public void testHandleImplicitDefaultValues() { + IfaceA ifaceA = create(IfaceA.class, createMap()); + assertNotNull(ifaceA); + + assertEquals(0, ifaceA.getInt()); + assertEquals(0.0, ifaceA.getDouble(), ERROR_MARGIN); + assertEquals(null, ifaceA.getString()); + assertArrayEquals(new int[0], ifaceA.getArray()); + assertEquals(Collections.emptyList(), ifaceA.getList()); + assertEquals(Collections.emptyMap(), ifaceA.getMap()); + assertEquals(null, ifaceA.getEnum()); + + IfaceB ifaceB = create(IfaceB.class, createMap()); + assertNotNull(ifaceB); + + assertEquals(new Stack<>(), ifaceB.getA()); + assertEquals(Collections.emptySet(), ifaceB.getB()); + assertEquals(new LinkedList<>(), ifaceB.getC()); + + IfaceC ifaceC = create(IfaceC.class, createMap()); + assertNotNull(ifaceC); + + assertNotNull(ifaceC.getIfaceA()); + assertNull(ifaceC.getType()); + } + + @Test + public void testHandleInt() { + IfaceA cfg = create(IfaceA.class, createMap("int", 41)); + assertNotNull(cfg); + + assertEquals(41, cfg.getInt()); + } + + @Test + public void testHandleDouble() { + IfaceA cfg = create(IfaceA.class, createMap("double", 3.141)); + assertNotNull(cfg); + + assertEquals(3.141, cfg.getDouble(), ERROR_MARGIN); + } + + @Test + public void testHandleString() { + IfaceA cfg = create(IfaceA.class, createMap("string", "hello")); + assertNotNull(cfg); + + assertEquals("hello", cfg.getString()); + } + + @Test + public void testHandleEnum() { + IfaceA cfg = create(IfaceA.class, createMap("enum", "two")); + assertNotNull(cfg); + + assertEquals(EnumValue.TWO, cfg.getEnum()); + } + + @Test + public void testHandleMapFromString() { + IfaceA cfg = create(IfaceA.class, createMap("map.a", "1", "map.b", "2", "map.c", "hello world", "map.d", "[4, 5, 6]")); + assertNotNull(cfg); + + Map map = cfg.getMap(); + assertEquals("1", map.get("a")); + assertEquals("2", map.get("b")); + assertEquals("hello world", map.get("c")); + assertEquals("[4, 5, 6]", map.get("d")); + + cfg = create(IfaceA.class, createMap("map", "{a.1, b.2, c.3}")); + assertNotNull(cfg); + + map = cfg.getMap(); + assertEquals("1", map.get("a")); + assertEquals("2", map.get("b")); + assertEquals("3", map.get("c")); + } + + @Test + public void testHandleObjectFromString() { + IfaceC cfg = create(IfaceC.class, createMap( + "ifaceA.array", "[4, 5, 6]", + "ifaceA.arrayOfList.0", "[foo, bar]", + "ifaceA.arrayOfList.1", "[qux]", + "ifaceA.byte", "127", + "ifaceA.double", "3.141", + "ifaceA.enum", "THREE", + "ifaceA.int", "123", + "ifaceA.list", "[qux, quu]", + "ifaceA.map.c", "d", + "ifaceA.map.a", "b", + "ifaceA.mapStringList.x", "[1, 2, 3]", + "ifaceA.mapStringList.y", "[4, 5, 6]", + "ifaceA.mapStringList.z", "[7, 8, 9]", + "ifaceA.string", "hello world", + "ifaceA.boolean", "true", + "type", "java.lang.String")); + assertNotNull(cfg); + + SortedMap sortedMap = new TreeMap<>(); + sortedMap.put("a", "b"); + sortedMap.put("c", "d"); + + Map> mapStringList = new HashMap<>(); + mapStringList.put("x", Arrays. asList(1L, 2L, 3L)); + mapStringList.put("y", Arrays. asList(4L, 5L, 6L)); + mapStringList.put("z", Arrays. asList(7L, 8L, 9L)); + + assertEquals(String.class, cfg.getType()); + + IfaceA ifaceA = cfg.getIfaceA(); + assertNotNull(ifaceA); + + assertArrayEquals(new int[] { 4, 5, 6 }, ifaceA.getArray()); + assertArrayEquals(new List[] { Arrays.asList("foo", "bar"), Arrays.asList("qux") }, ifaceA.getArrayOfList()); + assertEquals(Byte.valueOf("127"), ifaceA.getByte()); + assertEquals(3.141, ifaceA.getDouble(), ERROR_MARGIN); + assertEquals(EnumValue.THREE, ifaceA.getEnum()); + assertEquals(123, ifaceA.getInt()); + assertEquals(Arrays.asList("qux", "quu"), ifaceA.getList()); + assertEquals(sortedMap, ifaceA.getMap()); + assertEquals(mapStringList, ifaceA.getMapStringList()); + assertEquals("hello world", ifaceA.getString()); + assertEquals(true, ifaceA.isBoolean()); + } + + @Test + public void testHandleArrayDirect() { + IfaceA cfg = create(IfaceA.class, createMap("array", new int[] { 2, 3, 4, 5 })); + assertNotNull(cfg); + + int[] vals = cfg.getArray(); + assertEquals(2, vals[0]); + assertEquals(3, vals[1]); + assertEquals(4, vals[2]); + assertEquals(5, vals[3]); + } + + @Test + public void testHandleArrayFromString() { + IfaceA cfg = create(IfaceA.class, createMap("array", "[2,3,4,5]")); + assertNotNull(cfg); + + int[] vals = cfg.getArray(); + assertEquals(2, vals[0]); + assertEquals(3, vals[1]); + assertEquals(4, vals[2]); + assertEquals(5, vals[3]); + } + + @Test + public void testHandleListDirect() { + IfaceA cfg = create(IfaceA.class, createMap("list", Arrays.asList("2", "3", "4", "5"))); + assertNotNull(cfg); + + List list = cfg.getList(); + assertEquals("2", list.get(0)); + assertEquals("3", list.get(1)); + assertEquals("4", list.get(2)); + assertEquals("5", list.get(3)); + } + + @Test + public void testHandleListFromString() { + IfaceA cfg = create(IfaceA.class, createMap("list", "[2,3,4,5]")); + assertNotNull(cfg); + + List list = cfg.getList(); + assertEquals("2", list.get(0)); + assertEquals("3", list.get(1)); + assertEquals("4", list.get(2)); + assertEquals("5", list.get(3)); + } + + private static Map createMap(Object... vals) { + Map result = new HashMap<>(); + for (int i = 0; i < vals.length; i += 2) { + result.put(vals[i].toString(), vals[i + 1]); + } + return result; + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/org/apache/felix/dm/impl/ConfigurationDependencyImplTest.java b/dependencymanager/org.apache.felix.dependencymanager/test/org/apache/felix/dm/impl/ConfigurationDependencyImplTest.java new file mode 100644 index 00000000000..04bebdc500e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/test/org/apache/felix/dm/impl/ConfigurationDependencyImplTest.java @@ -0,0 +1,258 @@ +/* + * 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.dm.impl; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.apache.felix.dm.Logger; +import org.apache.felix.dm.context.ComponentContext; +import org.junit.Test; +import org.osgi.framework.BundleContext; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedService; + +import test.Ensure; + +/** + * @author Felix Project Team + */ +public class ConfigurationDependencyImplTest { + public static interface MyConfiguration { + String[] getArgArray(); + + List getArgList(); + + long getLongValue(); + + MyMap getMap(); + + String getMessage(); + + double getPi(); + + int getValue(); + + boolean isTrue(); + } + + public static interface MyMap { + String getFoo(); + + String[] getQuu(); + + int getQux(); + } + + static class AManagedService extends PlainService implements ManagedService { + public AManagedService(Ensure ensure) { + super(ensure); + } + + @SuppressWarnings("rawtypes") + @Override + public void updated(Dictionary config) throws ConfigurationException { + super.updated(config); + } + } + + static class FancyService { + final Ensure m_ensure; + + public FancyService(Ensure ensure) { + m_ensure = ensure; + } + + public void updated(MyConfiguration config) throws ConfigurationException { + m_ensure.step(); + + if (config != null) { + assertConfiguration(config); + } + else { + assertNull(config); + } + } + + public void stop() { + m_ensure.step(); + } + + private void assertConfiguration(MyConfiguration cfg) { + assertEquals("isTrue", true, cfg.isTrue()); + assertEquals("getValue", 42, cfg.getValue()); + assertEquals("getLongValue", 1234567890L, cfg.getLongValue()); + assertEquals("getPi", 3.141, cfg.getPi(), 0.001); + assertArrayEquals("getArgArray", new String[] { "a", "b", "c" }, cfg.getArgArray()); + assertEquals("getArgList", Arrays.asList("d", "e", "f"), cfg.getArgList()); + assertEquals("getMessage", "hello world!", cfg.getMessage()); + + MyMap map = cfg.getMap(); + assertEquals("getMap.getFoo", "bar", map.getFoo()); + assertEquals("getMap.getQux", 123, map.getQux()); + assertArrayEquals("getMap.getQuu", new String[] { "x", "y", "z" }, map.getQuu()); + } + } + + static class PlainService { + final Ensure m_ensure; + + public PlainService(Ensure ensure) { + m_ensure = ensure; + } + + @SuppressWarnings("rawtypes") + public void updated(Dictionary config) throws ConfigurationException { + m_ensure.step(); + + if (config != null) { + assertConfiguration(config); + } + else { + assertNull(config); + } + } + + public void stop() { + m_ensure.step(); + } + + @SuppressWarnings("rawtypes") + private void assertConfiguration(Dictionary cfg) { + assertEquals("isTrue", "true", cfg.get("true")); + assertEquals("getValue", "42", cfg.get("value")); + assertEquals("getLongValue", "1234567890", cfg.get("longValue")); + assertEquals("getPi", "3.141", cfg.get("pi")); + assertEquals("getArgArray", "[a, b, c]", cfg.get("argArray")); + assertEquals("getArgList", "[d, e, f]", cfg.get("argList")); + assertEquals("getMap.foo", "bar", cfg.get("map.foo")); + assertEquals("getMap.qux", "123", cfg.get("map.qux")); + assertEquals("getMap.quu", "[x, y, z]", cfg.get("map.quu")); + assertEquals("getMessage", "hello world!", cfg.get("message")); + } + } + + @Test + public void testDoNotInvokeFancyUpdatedMethodWithWrongSignatureOk() throws Exception { + Ensure ensure = createEnsure(); + FancyService service = new FancyService(ensure); + + ConfigurationDependencyImpl cdi = createConfigurationDependency(service); + cdi.setCallback(service, "updated", Dictionary.class); + ensure.step(1); + + cdi.updated(createDictionary()); + + TimeUnit.SECONDS.sleep(1L); + + // Our step shouldn't be changed... + ensure.waitForStep(1, 1000); + } + + @Test + public void testInvokeFancyUpdatedMethodOk() throws Exception { + Ensure ensure = createEnsure(); + FancyService service = new FancyService(ensure); + + ConfigurationDependencyImpl cdi = createConfigurationDependency(service); + cdi.setCallback(service, "updated", MyConfiguration.class); + cdi.updated(createDictionary()); + + ensure.waitForStep(1, 1000); + + cdi.updated(null); + + ensure.waitForStep(2, 1000); + } + + @Test + public void testInvokeManagedServiceUpdatedMethodOk() throws Exception { + Ensure ensure = createEnsure(); + AManagedService service = new AManagedService(ensure); + + ConfigurationDependencyImpl cdi = createConfigurationDependency(service); + cdi.updated(createDictionary()); + + ensure.waitForStep(1, 1000); + + cdi.updated(null); + + ensure.waitForStep(2, 1000); + } + + @Test + public void testInvokePlainUpdatedMethodOk() throws Exception { + Ensure ensure = createEnsure(); + PlainService service = new PlainService(ensure); + + ConfigurationDependencyImpl cdi = createConfigurationDependency(service); + cdi.updated(createDictionary()); + + ensure.waitForStep(1, 1000); + + cdi.updated(null); + + ensure.waitForStep(2, 1000); + } + + private ConfigurationDependencyImpl createConfigurationDependency(Object service) { + BundleContext bc = mock(BundleContext.class); + Logger mockLogger = mock(Logger.class); + + ComponentContext component = mock(ComponentContext.class); + when(component.getExecutor()).thenReturn(Executors.newSingleThreadExecutor()); + when(component.getLogger()).thenReturn(mockLogger); + + ConfigurationDependencyImpl result = new ConfigurationDependencyImpl(bc, mockLogger); + result.setCallback(service, "updated").setPid("does.not.matter"); + result.setComponentContext(component); + result.start(); + return result; + } + + @SuppressWarnings("rawtypes") + private Dictionary createDictionary() { + Dictionary result = new Hashtable<>(); + result.put("true", "true"); + result.put("value", "42"); + result.put("longValue", "1234567890"); + result.put("pi", "3.141"); + result.put("argArray", "[a, b, c]"); + result.put("argList", "[d, e, f]"); + result.put("map.foo", "bar"); + result.put("map.qux", "123"); + result.put("map.quu", "[x, y, z]"); + result.put("message", "hello world!"); + return result; + } + + private Ensure createEnsure() { + return new Ensure(false); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/org/apache/felix/dm/impl/FactoryConfigurationAdapterImplTest.java b/dependencymanager/org.apache.felix.dependencymanager/test/org/apache/felix/dm/impl/FactoryConfigurationAdapterImplTest.java new file mode 100644 index 00000000000..d7727d3d0a4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/test/org/apache/felix/dm/impl/FactoryConfigurationAdapterImplTest.java @@ -0,0 +1,158 @@ +/* + * 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.dm.impl; + +import static org.mockito.Mockito.mock; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.impl.ConfigurationDependencyImplTest.AManagedService; +import org.apache.felix.dm.impl.ConfigurationDependencyImplTest.FancyService; +import org.apache.felix.dm.impl.ConfigurationDependencyImplTest.MyConfiguration; +import org.apache.felix.dm.impl.ConfigurationDependencyImplTest.PlainService; +import org.junit.Test; +import org.osgi.framework.BundleContext; +import org.osgi.service.cm.ManagedServiceFactory; + +import test.Ensure; + +/** + * @author Felix Project Team + */ +public class FactoryConfigurationAdapterImplTest { + + private static final String CONF_PID = "bogus"; + + @Test + public void testDoNotInvokeFancyUpdatedMethodWithWrongSignatureOk() throws Exception { + Ensure ensure = createEnsure(); + FancyService service = new FancyService(ensure); + + FactoryConfigurationAdapterImpl cdi = createConfigurationDependency(service, Map.class); + ensure.step(1); + + ((ManagedServiceFactory) cdi.m_component.getInstance()).updated(CONF_PID, (Dictionary) createDictionary()); + + TimeUnit.SECONDS.sleep(1L); + + // Our step shouldn't be changed... + ensure.waitForStep(1, 1000); + } + + @Test + public void testInvokeFancyUpdatedMethodOk() throws Exception { + Ensure ensure = createEnsure(); + FancyService service = new FancyService(ensure); + + FactoryConfigurationAdapterImpl cdi = createConfigurationDependency(service, MyConfiguration.class); + + ((ManagedServiceFactory) cdi.m_component.getInstance()).updated(CONF_PID, (Dictionary) createDictionary()); + + ensure.waitForStep(1, 1000); + + ((ManagedServiceFactory) cdi.m_component.getInstance()).deleted(CONF_PID); + + ensure.waitForStep(2, 1000); + } + + @Test + public void testInvokeManagedServiceUpdatedMethodOk() throws Exception { + Ensure ensure = createEnsure(); + AManagedService service = new AManagedService(ensure); + + FactoryConfigurationAdapterImpl cdi = createConfigurationDependency(service); + + ((ManagedServiceFactory) cdi.m_component.getInstance()).updated(CONF_PID, (Dictionary) createDictionary()); + + ensure.waitForStep(1, 1000); + + ((ManagedServiceFactory) cdi.m_component.getInstance()).deleted(CONF_PID); + + ensure.waitForStep(2, 1000); + } + + @Test + public void testInvokePlainUpdatedMethodOk() throws Exception { + Ensure ensure = createEnsure(); + PlainService service = new PlainService(ensure); + + FactoryConfigurationAdapterImpl cdi = createConfigurationDependency(service); + + ((ManagedServiceFactory) cdi.m_component.getInstance()).updated(CONF_PID, (Dictionary) createDictionary()); + + ensure.waitForStep(1, 1000); + + ((ManagedServiceFactory) cdi.m_component.getInstance()).deleted(CONF_PID); + + ensure.waitForStep(2, 1000); + } + + + private FactoryConfigurationAdapterImpl createConfigurationDependency(Object service) { + return createConfigurationDependency(service, null); + } + + private FactoryConfigurationAdapterImpl createConfigurationDependency(Object service, Class configType) { + BundleContext bc = mock(BundleContext.class); + + DependencyManager dm = new DependencyManager(bc); + + Component result = dm.createFactoryConfigurationAdapterService("does.not.matter", "updated", false, service, configType); + + // Normally, when creating a factory pid adapter, you specify the class of the adapter implementation which will be instantiated + // for each created factory pid. To do so, you invoke the setImplementation(Object impl) method, and this methods + // accepts a class parameter, or an object instance. Usually, you always pass a class, because the intent of a factory pid adapter is to + // create a component instance for each created factory pid. But in our case, the "service" parameter represents our adapter instance, + // so just use it as the factory adapter implementation instance: + result.setImplementation(service); + + // *Important note:* the semantic of the factory conf pid adapter is really similar to a ManagedServiceFactory: + // - when the factory pid is created, a component is created; called in updated; and called in start(). + // - when the factory pid is updated, the component is called in updated(). + // - but when the factory pid is removed, updated(null) is not invoked (unlike in case of ConfigurationDependency), and the component is simply + // stopped. This is actually the same semantic as ManagedServiceFactory: when factory pid is removed, ManagedServiceFactory.deleted() is called + // and the deleted() method is assumed to stop and unregister the service that was registered for the pid being removed. + dm.add(result); + return (FactoryConfigurationAdapterImpl) result; + } + + private Dictionary createDictionary() { + Dictionary result = new Hashtable<>(); + result.put("true", "true"); + result.put("value", "42"); + result.put("longValue", "1234567890"); + result.put("pi", "3.141"); + result.put("argArray", "[a, b, c]"); + result.put("argList", "[d, e, f]"); + result.put("map.foo", "bar"); + result.put("map.qux", "123"); + result.put("map.quu", "[x, y, z]"); + result.put("message", "hello world!"); + return result; + } + + private Ensure createEnsure() { + return new Ensure(false); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/org/apache/felix/dm/tracker/TrackedTest.java b/dependencymanager/org.apache.felix.dependencymanager/test/org/apache/felix/dm/tracker/TrackedTest.java new file mode 100644 index 00000000000..5b12dbce962 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/test/org/apache/felix/dm/tracker/TrackedTest.java @@ -0,0 +1,315 @@ +/* + * 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.dm.tracker; + +import static org.junit.Assert.assertArrayEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.dm.tracker.ServiceTracker.Tracked; +import org.junit.Test; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceReference; + +/** + * @author Felix Project Team + */ +@SuppressWarnings("rawtypes") +public class TrackedTest { + + @Test + public void testSetInitialHideAspects() { + System.out.println("testSetInitialHideAspects"); + TestCustomizer customizer = new TestCustomizer(); + + ServiceTracker tracker = new TestTracker(customizer); + tracker.open(); + Tracked tracked = tracker.getTracked(); + + Object[] initialReferences = new Object[] { + createServiceReference(1L), + createServiceReference(2L, 1L, 10), + createServiceReference(3L), + createServiceReference(4L, 1L, 5), + createServiceReference(5L, 3L, 5), + }; + tracked.setInitial(initialReferences); + tracked.trackInitial(); + tracked.getExecutor().execute(); + assertArrayEquals(new Long[] { 2L, 5L }, customizer.getServiceReferenceIds()); + } + + @Test + public void testUnHideAspect() { + System.out.println("testUnhideAspect"); + TestCustomizer customizer = new TestCustomizer(); + + ServiceTracker tracker = new TestTracker(customizer); + tracker.open(); + Tracked tracked = tracker.getTracked(); + + ServiceReference[] initialReferences = new ServiceReference[] { + createServiceReference(1L), + createServiceReference(2L, 1L, 10), + createServiceReference(3L), + createServiceReference(4L, 1L, 5), + createServiceReference(5L, 3L, 5), + }; + tracked.setInitial(initialReferences); + tracked.trackInitial(); + tracked.getExecutor().execute(); + assertArrayEquals(new Long[] { 2L, 5L }, customizer.getServiceReferenceIds()); + + // create a service event that unregisters service with id 2, we would expect it to be swapped with 4. + ServiceEvent event = new ServiceEvent(ServiceEvent.UNREGISTERING, initialReferences[1]); + tracked.serviceChanged(event); + assertArrayEquals(new Long[] { 5L, 4L }, customizer.getServiceReferenceIds()); + // create a service event that unregisters service with id 4, we would expect it to be swapped with 1. + event = new ServiceEvent(ServiceEvent.UNREGISTERING, initialReferences[3]); + tracked.serviceChanged(event); + assertArrayEquals(new Long[] { 5L, 1L }, customizer.getServiceReferenceIds()); + } + + @Test + public void testHideAspect() { + System.out.println("testHideAspect"); + TestCustomizer customizer = new TestCustomizer(); + + ServiceTracker tracker = new TestTracker(customizer); + tracker.open(); + Tracked tracked = tracker.getTracked(); + + ServiceReference[] initialReferences = new ServiceReference[] { + createServiceReference(1L), + createServiceReference(2L, 1L, 10), + createServiceReference(3L), + createServiceReference(4L, 1L, 5), + createServiceReference(5L, 3L, 5), + }; + tracked.setInitial(initialReferences); + tracked.trackInitial(); + tracked.getExecutor().execute(); + assertArrayEquals(new Long[] { 2L, 5L }, customizer.getServiceReferenceIds()); + + // create a service event that registers another but lower ranked aspect for service with id 1. + ServiceReference newReference = createServiceReference(6L, 1L, 8); + ServiceEvent event = new ServiceEvent(ServiceEvent.REGISTERED, newReference); + tracked.serviceChanged(event); + assertArrayEquals(new Long[] { 2L, 5L }, customizer.getServiceReferenceIds()); + + // create a service event that unregisters service with id 2, we would expect it to be swapped with 6. + event = new ServiceEvent(ServiceEvent.UNREGISTERING, initialReferences[1]); + tracked.serviceChanged(event); + assertArrayEquals(new Long[] { 5L, 6L }, customizer.getServiceReferenceIds()); + + // create a service event that unregisters service with id 6, we would expect it to be swapped with 4. + event = new ServiceEvent(ServiceEvent.UNREGISTERING, newReference); + tracked.serviceChanged(event); + assertArrayEquals(new Long[] { 5L, 4L }, customizer.getServiceReferenceIds()); + + // create a service event that registers a higher ranked aspect for service with id 1. + ServiceReference higherRankedReference = createServiceReference(7L, 1L, 15); + ServiceEvent addHigherRankedEvent = new ServiceEvent(ServiceEvent.REGISTERED, higherRankedReference); + tracked.serviceChanged(addHigherRankedEvent); + assertArrayEquals(new Long[] { 5L, 7L }, customizer.getServiceReferenceIds()); + } + + @Test + public void testSetInitialTrackAspects() { + System.out.println("testSetInitialTrackAspects"); + TestCustomizer customizer = new TestCustomizer(); + + ServiceTracker tracker = new TestTracker(customizer); + tracker.open(false, true); + Tracked tracked = tracker.getTracked(); + + Object[] initialReferences = new Object[] { + createServiceReference(1L), + createServiceReference(2L, 1L, 10), + createServiceReference(3L, 1L, 5) + }; + tracked.setInitial(initialReferences); + tracked.trackInitial(); + tracked.getExecutor().execute(); + assertArrayEquals(new Long[] { 1L, 2L, 3L }, customizer.getServiceReferenceIds()); + } + + private static BundleContext createBundleContext() { + BundleContext context = mock(BundleContext.class); + when(context.getProperty(Constants.FRAMEWORK_VERSION)).thenReturn(null); + return context; + } + + private ServiceReference createServiceReference(Long serviceId) { + return createServiceReference(serviceId, null, null); + } + + private ServiceReference createServiceReference(Long serviceId, Long aspectId, Integer ranking) { + return new TestServiceReference(serviceId, aspectId, ranking); + } + + class TestTracker extends ServiceTracker { + + public TestTracker(ServiceTrackerCustomizer customizer) { + super(createBundleContext(), "(objectClass=*)", customizer); + } + + } + + class TestCustomizer implements ServiceTrackerCustomizer { + + List> serviceReferences = new ArrayList<>(); + + @Override + public Object addingService(ServiceReference reference) { + System.out.println("adding service: " + reference); + return new Object(); + } + + @Override + public void addedService(ServiceReference reference, Object service) { + System.out.println("added service: " + reference); + serviceReferences.add(reference); + } + + @Override + public void modifiedService(ServiceReference reference, Object service) { + System.out.println("modified service: " + reference); + } + + @Override + public void swappedService(ServiceReference reference, Object service, + ServiceReference newReference, Object newService) { + System.out.println("swapped service: " + reference); + serviceReferences.remove(reference); + serviceReferences.add(newReference); + } + + @Override + public void removedService(ServiceReference reference, Object service) { + System.out.println("removed service: " + reference); + serviceReferences.remove(reference); + } + + public Long[] getServiceReferenceIds() { + Long[] ids = new Long[serviceReferences.size()]; + for (int i = 0; i < serviceReferences.size(); i++) { + ids[i] = (Long) serviceReferences.get(i).getProperty(Constants.SERVICE_ID); + } + return ids; + } + + } + + class TestServiceReference implements ServiceReference { + + Properties props = new Properties(); + + public TestServiceReference(Long serviceId, Long aspectId, + Integer ranking) { + props.put(Constants.SERVICE_ID, serviceId); + if (aspectId != null) { + props.put(DependencyManager.ASPECT, aspectId); + } + if (ranking != null) { + props.put(Constants.SERVICE_RANKING, ranking); + } + } + + @Override + public Object getProperty(String key) { + return props.get(key); + } + + @Override + public String[] getPropertyKeys() { + return props.keySet().toArray(new String[]{}); + } + + @Override + public Bundle getBundle() { + return null; + } + + @Override + public Bundle[] getUsingBundles() { + return null; + } + + @Override + public boolean isAssignableTo(Bundle bundle, String className) { + return false; + } + + @Override + public int compareTo(Object reference) // Kindly borrowed from the Apache Felix ServiceRegistrationImpl.ServiceReferenceImpl + { + ServiceReference other = (ServiceReference) reference; + + Long id = (Long) getProperty(Constants.SERVICE_ID); + Long otherId = (Long) other.getProperty(Constants.SERVICE_ID); + + if (id.equals(otherId)) + { + return 0; // same service + } + + Object rankObj = getProperty(Constants.SERVICE_RANKING); + Object otherRankObj = other.getProperty(Constants.SERVICE_RANKING); + + // If no rank, then spec says it defaults to zero. + rankObj = (rankObj == null) ? new Integer(0) : rankObj; + otherRankObj = (otherRankObj == null) ? new Integer(0) : otherRankObj; + + // If rank is not Integer, then spec says it defaults to zero. + Integer rank = (rankObj instanceof Integer) + ? (Integer) rankObj : new Integer(0); + Integer otherRank = (otherRankObj instanceof Integer) + ? (Integer) otherRankObj : new Integer(0); + + // Sort by rank in ascending order. + if (rank.compareTo(otherRank) < 0) + { + return -1; // lower rank + } + else if (rank.compareTo(otherRank) > 0) + { + return 1; // higher rank + } + + // If ranks are equal, then sort by service id in descending order. + return (id.compareTo(otherId) < 0) ? 1 : -1; + } + + @Override + public String toString() { + return "TestServiceReference [props=" + props + "]"; + } + + } + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/test/ComponentTest.java b/dependencymanager/org.apache.felix.dependencymanager/test/test/ComponentTest.java new file mode 100644 index 00000000000..dbcc91e6316 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/test/test/ComponentTest.java @@ -0,0 +1,697 @@ +/* + * 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 test; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentState; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.impl.ComponentImpl; +import org.junit.Assert; +import org.junit.Test; + +/** + * @author Felix Project Team + */ +@SuppressWarnings("unused") +public class ComponentTest { + static class MyComponent { + public MyComponent() { + } + } + + @Test + public void createStartAndStopComponent() { + ComponentImpl c = new ComponentImpl(); + c.setImplementation(MyComponent.class); + Assert.assertEquals("should not be available until started", false, c.isAvailable()); + c.start(); + Assert.assertEquals("should be available", true, c.isAvailable()); + c.stop(); + Assert.assertEquals("should no longer be available when stopped", false, c.isAvailable()); + } + + @Test + public void testInitCallbackOfComponent() { + final Ensure e = new Ensure(); + ComponentImpl c = new ComponentImpl(); + c.setImplementation(new Object() { + void init() { + e.step(2); + } + void start() { + e.step(3); + } + void stop() { + e.step(5); + } + void destroy() { + e.step(6); + } + }); + e.step(1); + c.start(); + e.step(4); + c.stop(); + e.step(7); + } + + @Test + public void testAddDependencyFromInitCallback() { + final Ensure e = new Ensure(); + final SimpleServiceDependency d = new SimpleServiceDependency(); + d.setRequired(true); + ComponentImpl c = new ComponentImpl(); + c.setImplementation(new Object() { + void init(Component c) { + e.step(2); + c.add(d); + } + void start() { + e.step(4); + } + void stop() { + e.step(6); + } + void destroy() { + e.step(7); + } + }); + e.step(1); + c.start(); + e.step(3); + d.add(new EventImpl()); // NPE?! + e.step(5); + d.remove(new EventImpl()); + c.stop(); + e.step(8); + } + + @Test + public void testAddAvailableDependencyFromInitCallback() { + final Ensure e = new Ensure(); + final SimpleServiceDependency d = new SimpleServiceDependency(); + d.setRequired(true); + final SimpleServiceDependency d2 = new SimpleServiceDependency(); + d2.setRequired(true); + ComponentImpl c = new ComponentImpl(); + c.setImplementation(new Object() { + void init(Component c) { + System.out.println("init"); + e.step(2); + c.add(d); + d.add(new EventImpl()); + c.add(d2); + } + void start() { + System.out.println("start"); + e.step(); + } + void stop() { + System.out.println("stop"); + e.step(); + } + void destroy() { + System.out.println("destroy"); + e.step(7); + } + }); + e.step(1); + c.start(); + e.step(3); + d2.add(new EventImpl()); + e.step(5); + d.remove(new EventImpl()); + c.stop(); + e.step(8); + } + + @Test + public void testAtomicallyAddMultipleDependenciesFromInitCallback() { + final Ensure e = new Ensure(); + final SimpleServiceDependency d = new SimpleServiceDependency(); + d.setRequired(true); + + final SimpleServiceDependency d2 = new SimpleServiceDependency(); + d2.setRequired(true); + + ComponentImpl c = new ComponentImpl(); + c.setImplementation(new Object() { + void init(Component c) { + System.out.println("init"); + e.step(2); + c.add(d, d2); + d.add(new EventImpl()); // won't trigger start because d2 is not yet available + } + void start() { + System.out.println("start"); + e.step(4); + } + void stop() { + System.out.println("stop"); + e.step(6); + } + void destroy() { + System.out.println("destroy"); + e.step(7); + } + }); + e.step(1); + c.start(); + e.step(3); + d2.add(new EventImpl()); + e.step(5); + d.remove(new EventImpl()); + c.stop(); + e.step(8); + } + + @Test + public void createComponentAddDependencyAndStartComponent() { + ComponentImpl c = new ComponentImpl(); + c.setImplementation(MyComponent.class); + SimpleServiceDependency d = new SimpleServiceDependency(); + d.setRequired(true); + c.add(d); + c.start(); + Assert.assertEquals("should not be available when started because of missing dependency", false, c.isAvailable()); + c.stop(); + c.remove(d); + } + + @Test + public void createComponentStartItAndAddDependency() { + ComponentImpl c = new ComponentImpl(); + c.setImplementation(MyComponent.class); + SimpleServiceDependency d = new SimpleServiceDependency(); + d.setRequired(true); + c.start(); + Assert.assertEquals("should be available when started", true, c.isAvailable()); + c.add(d); + Assert.assertEquals("dependency should not be available", false, d.isAvailable()); + Assert.assertEquals("Component should not be available", false, c.isAvailable()); + c.remove(d); + c.stop(); + } + + @Test + public void createComponentStartItAddDependencyAndMakeDependencyAvailable() { + ComponentImpl c = new ComponentImpl(); + c.setImplementation(MyComponent.class); + SimpleServiceDependency d = new SimpleServiceDependency(); + d.setRequired(true); + c.start(); + c.add(d); + Assert.assertEquals("Component should not be available: it is started but the dependency is not available", false, c.isAvailable()); + d.add(new EventImpl()); + Assert.assertEquals("dependency is available, component should be too", true, c.isAvailable()); + d.remove(new EventImpl()); + Assert.assertEquals("dependency is no longer available, component should not be either", false, c.isAvailable()); + c.remove(d); + Assert.assertEquals("dependency is removed, component should be available again", true, c.isAvailable()); + c.stop(); + Assert.assertEquals("Component is stopped, should be unavailable now", false, c.isAvailable()); + } + + @Test + public void createComponentStartItAddDependencyAndListenerMakeDependencyAvailableAndUnavailableImmediately() { + ComponentImpl c = new ComponentImpl(); + c.setImplementation(MyComponent.class); + final SimpleServiceDependency d = new SimpleServiceDependency(); + d.setRequired(true); + ComponentStateListener l = new ComponentStateListener() { + @Override + public void changed(Component c, ComponentState state) { + // make the dependency unavailable + d.remove(new EventImpl()); + } + }; + c.start(); + c.add(d); + // we add a listener here which immediately triggers an 'external event' that + // makes the dependency unavailable again as soon as it's invoked + c.add(l); + Assert.assertEquals("Component unavailable, dependency unavailable", false, c.isAvailable()); + // so even though we make the dependency available here, before our call returns it + // is made unavailable again + d.add(new EventImpl()); + Assert.assertEquals("Component *still* unavailable, because the listener immediately makes the dependency unavailable", false, c.isAvailable()); + c.remove(l); + Assert.assertEquals("listener removed, component still unavailable", false, c.isAvailable()); + c.remove(d); + Assert.assertEquals("dependency removed, component available", true, c.isAvailable()); + c.stop(); + Assert.assertEquals("Component stopped, should be unavailable", false, c.isAvailable()); + } + + @Test + public void createComponentAddTwoDependenciesMakeBothAvailableAndUnavailable() { + ComponentImpl c = new ComponentImpl(); + c.setImplementation(MyComponent.class); + SimpleServiceDependency d1 = new SimpleServiceDependency(); + d1.setRequired(true); + SimpleServiceDependency d2 = new SimpleServiceDependency(); + d2.setRequired(true); + c.start(); + c.add(d1); + c.add(d2); + Assert.assertEquals("Component should be unavailable, both dependencies are too", false, c.isAvailable()); + d1.add(new EventImpl()); + Assert.assertEquals("one dependency available, component should still be unavailable", false, c.isAvailable()); + d2.add(new EventImpl()); + Assert.assertEquals("both dependencies available, component should be available", true, c.isAvailable()); + d1.remove(new EventImpl()); + Assert.assertEquals("one dependency unavailable again, component should be unavailable too", false, c.isAvailable()); + d2.remove(new EventImpl()); + Assert.assertEquals("both dependencies unavailable, component should be too", false, c.isAvailable()); + c.remove(d2); + Assert.assertEquals("removed one dependency, still unavailable", false, c.isAvailable()); + c.remove(d1); + Assert.assertEquals("removed the other dependency, component should be available now", true, c.isAvailable()); + c.stop(); + Assert.assertEquals("Component stopped, should be unavailable again", false, c.isAvailable()); + } + + @Test + public void createComponentAddDependencyMakeAvailableAndUnavailableWithCallbacks() { + final Ensure e = new Ensure(); + ComponentImpl c = new ComponentImpl(); + c.setImplementation(new Object() { + public void add() { + e.step(1); + } + public void remove() { + e.step(3); + } + }); + SimpleServiceDependency d1 = new SimpleServiceDependency(); + d1.setCallbacks("add", "remove"); + d1.setRequired(true); + // add the dependency to the component + c.add(d1); + // start the component + c.start(); + // make the dependency available, we expect the add callback + // to be invoked here + d1.add(new EventImpl()); + e.step(2); + // remove the dependency, should trigger the remove callback + d1.remove(new EventImpl()); + e.step(4); + c.stop(); + c.remove(d1); + Assert.assertEquals("Component stopped, should be unavailable again", false, c.isAvailable()); + } + + @Test + public void createAndStartComponentAddDependencyMakeAvailableAndUnavailableWithCallbacks() { + final Ensure e = new Ensure(); + ComponentImpl c = new ComponentImpl(); + c.setImplementation(new Object() { + public void add() { + e.step(1); + } + public void remove() { + e.step(3); + } + }); + SimpleServiceDependency d1 = new SimpleServiceDependency(); + d1.setCallbacks("add", "remove"); + d1.setRequired(true); + // start the ComponentImpl (it should become available) + c.start(); + // add the dependency (it should become unavailable) + c.add(d1); + // make the dependency available, which should invoke the + // add callback + d1.add(new EventImpl()); + e.step(2); + // make the dependency unavailable, should trigger the + // remove callback + d1.remove(new EventImpl()); + e.step(4); + c.remove(d1); + c.stop(); + Assert.assertEquals("Component stopped, should be unavailable again", false, c.isAvailable()); + } + + @Test + public void createComponentAddTwoDependenciesMakeBothAvailableAndUnavailableWithCallbacks() { + final Ensure e = new Ensure(); + ComponentImpl c = new ComponentImpl(); + c.setImplementation(new Object() { + public void add() { + e.step(); + } + public void remove() { + e.step(); + } + }); + SimpleServiceDependency d1 = new SimpleServiceDependency(); + d1.setCallbacks("add", "remove"); + d1.setRequired(true); + SimpleServiceDependency d2 = new SimpleServiceDependency(); + d2.setCallbacks("add", "remove"); + d2.setRequired(true); + // start the component, which should become active because there are no + // dependencies yet + c.start(); + // now add the dependencies, making the ComponentImpl unavailable + c.add(d1); + c.add(d2); + // make the first dependency available, should have no effect on the + // component + d1.add(new EventImpl()); + e.step(1); + // second dependency available, now all the add callbacks should be + // invoked + d2.add(new EventImpl()); + e.step(4); + // remove the first dependency, triggering the remove callbacks + d1.remove(new EventImpl()); + e.step(7); + // remove the second dependency, should not trigger more callbacks + d2.remove(new EventImpl()); + e.step(8); + c.remove(d2); + c.remove(d1); + c.stop(); + // still, no more callbacks should have been invoked + e.step(9); + Assert.assertEquals("Component stopped, should be unavailable again", false, c.isAvailable()); + } + + @Test + public void createAndStartComponentAddTwoDependenciesMakeBothAvailableAndUnavailableWithCallbacks() { + final Ensure e = new Ensure(); + ComponentImpl c = new ComponentImpl(); + c.setImplementation(new Object() { + public void add() { + e.step(); + } + public void remove() { + e.step(); + } + }); + // start the component, it should become available + c.start(); + SimpleServiceDependency d1 = new SimpleServiceDependency(); + d1.setCallbacks("add", "remove"); + d1.setRequired(true); + SimpleServiceDependency d2 = new SimpleServiceDependency(); + d2.setCallbacks("add", "remove"); + d2.setRequired(true); + // add the first dependency, ComponentImpl should be unavailable + c.add(d1); + c.add(d2); + // make first dependency available, ComponentImpl should still be unavailable + d1.add(new EventImpl()); + e.step(1); + // make second dependency available, ComponentImpl available, callbacks should + // be invoked + d2.add(new EventImpl()); + e.step(4); + // remove the first dependency, callbacks should be invoked + d1.remove(new EventImpl()); + e.step(7); + // remove second dependency, no callbacks should be invoked + d2.remove(new EventImpl()); + e.step(8); + c.remove(d2); + c.remove(d1); + c.stop(); + e.step(9); + Assert.assertEquals("Component stopped, should be unavailable again", false, c.isAvailable()); + } + + @Test + public void createAndStartComponentAddTwoDependenciesWithMultipleServicesWithCallbacks() { + final Ensure e = new Ensure(); + ComponentImpl c = new ComponentImpl(); + c.setImplementation(new Object() { + public void add() { + e.step(); + } + public void remove() { + e.step(); + } + }); + // start component + c.start(); + SimpleServiceDependency d1 = new SimpleServiceDependency(); + d1.setCallbacks("add", "remove"); + d1.setRequired(true); + SimpleServiceDependency d2 = new SimpleServiceDependency(); + d2.setCallbacks("add", "remove"); + d2.setRequired(true); + c.add(d1); + c.add(d2); + // add three instances to first dependency, no callbacks should + // be triggered + d1.add(new EventImpl(1)); + d1.add(new EventImpl(2)); + d1.add(new EventImpl(3)); + e.step(1); + // add two instances to the second dependency, callbacks should + // be invoked (4x) + d2.add(new EventImpl(1)); + e.step(6); + // add another dependency, triggering another callback + d2.add(new EventImpl(2)); + e.step(8); + // remove first dependency (all three of them) which makes the + // ComponentImpl unavailable so it should trigger calling remove for + // all of them (so 5x) + d1.remove(new EventImpl(1)); + d1.remove(new EventImpl(2)); + d1.remove(new EventImpl(3)); + e.step(14); + // remove second dependency, should not trigger further callbacks + d2.remove(new EventImpl(1)); + d2.remove(new EventImpl(2)); + e.step(15); + c.remove(d2); + c.remove(d1); + c.stop(); + e.step(16); + Assert.assertEquals("Component stopped, should be unavailable again", false, c.isAvailable()); + } + + @Test + public void createComponentAddDependencyMakeAvailableChangeAndUnavailableWithCallbacks() { + final Ensure e = new Ensure(); + ComponentImpl c = new ComponentImpl(); + c.setImplementation(new Object() { + public void add() { + e.step(1); + } + public void change() { + e.step(3); + } + public void remove() { + e.step(5); + } + }); + SimpleServiceDependency d1 = new SimpleServiceDependency(); + d1.setCallbacks("add", "change", "remove"); + d1.setRequired(true); + // add the dependency to the component + c.add(d1); + // start the component + c.start(); + // make the dependency available, we expect the add callback + // to be invoked here + d1.add(new EventImpl()); + e.step(2); + // change the dependency + d1.change(new EventImpl()); + e.step(4); + // remove the dependency, should trigger the remove callback + d1.remove(new EventImpl()); + e.step(6); + c.stop(); + c.remove(d1); + Assert.assertEquals("Component stopped, should be unavailable again", false, c.isAvailable()); + } + + @Test + public void createComponentWithOptionalDependency() { + final Ensure e = new Ensure(); + ComponentImpl c = new ComponentImpl(); + c.setImplementation(new Object() { + public void add() { + e.step(1); + } + public void change() { + e.step(3); + } + public void remove() { + e.step(5); + } + }); + SimpleServiceDependency d1 = new SimpleServiceDependency(); + d1.setCallbacks("add", "change", "remove"); + d1.setRequired(false); + // add the dependency to the component + c.add(d1); + // start the component + c.start(); + Assert.assertEquals("Component started with an optional dependency, should be available", true, c.isAvailable()); + // make the dependency available, we expect the add callback + // to be invoked here + d1.add(new EventImpl()); + e.step(2); + Assert.assertEquals("Component started with an optional dependency, should be available", true, c.isAvailable()); + // change the dependency + d1.change(new EventImpl()); + e.step(4); + Assert.assertEquals("Component started with an optional dependency, should be available", true, c.isAvailable()); + // remove the dependency, should trigger the remove callback + d1.remove(new EventImpl()); + Assert.assertEquals("Component started with an optional dependency, should be available", true, c.isAvailable()); + e.step(6); + c.stop(); + c.remove(d1); + Assert.assertEquals("Component stopped, should be unavailable again", false, c.isAvailable()); + } + + @Test + public void createComponentWithOptionalAndRequiredDependency() { + final Ensure e = new Ensure(); + ComponentImpl c = new ComponentImpl(); + c.setImplementation(new Object() { + public void add() { + e.step(); + } + public void remove() { + e.step(); + } + }); + SimpleServiceDependency d1 = new SimpleServiceDependency(); + d1.setCallbacks("add", "remove"); + d1.setRequired(false); + SimpleServiceDependency d2 = new SimpleServiceDependency(); + d2.setCallbacks("add", "remove"); + d2.setRequired(true); + // add the dependencies to the component + c.add(d1); + c.add(d2); + // start the component + c.start(); + Assert.assertEquals("Component started with a required and optional dependency, should not be available", false, c.isAvailable()); + // make the optional dependency available + d1.add(new EventImpl()); + e.step(1); + Assert.assertEquals("Component should not be available", false, c.isAvailable()); + // make the required dependency available + d2.add(new EventImpl()); + e.step(4); + Assert.assertEquals("Component should be available", true, c.isAvailable()); + // remove the optional dependency + d1.remove(new EventImpl()); + e.step(6); + Assert.assertEquals("Component should be available", true, c.isAvailable()); + // remove the required dependency + d1.remove(new EventImpl()); + e.step(8); + Assert.assertEquals("Component should be available", true, c.isAvailable()); + c.stop(); + c.remove(d1); + Assert.assertEquals("Component stopped, should be unavailable again", false, c.isAvailable()); + } + + @Test + public void createComponentAddAvailableDependencyRemoveDependencyCheckStopCalledBeforeUnbind() { + final Ensure e = new Ensure(); + ComponentImpl c = new ComponentImpl(); + c.setImplementation(new Object() { + void add() { + e.step(1); + } + void start() { + e.step(2); + } + void stop() { + e.step(4); + } + void remove() { + e.step(5); + } + }); + SimpleServiceDependency d = new SimpleServiceDependency(); + d.setCallbacks("add", "remove"); + d.setRequired(true); + // add the dependency to the component + c.add(d); + // start the component + c.start(); + // make the dependency available, we expect the add callback + // to be invoked here, then start is called. + d.add(new EventImpl()); + e.step(3); + // remove the dependency, should trigger the stop, then remove callback + d.remove(new EventImpl()); + e.step(6); + c.stop(); + c.remove(d); + Assert.assertEquals("Component stopped, should be unavailable", false, c.isAvailable()); + } + + @Test + public void createDependenciesWithCallbackInstance() { + final Ensure e = new Ensure(); + ComponentImpl c = new ComponentImpl(); + c.setImplementation(new Object() { + void start() { + e.step(2); + } + + void stop() { + e.step(4); + } + }); + + Object callbackInstance = new Object() { + void add() { + e.step(1); + } + + void remove() { + e.step(5); + } + }; + + SimpleServiceDependency d = new SimpleServiceDependency(); + d.setCallbacks(callbackInstance, "add", "remove"); + d.setRequired(true); + // add the dependency to the component + c.add(d); + // start the component + c.start(); + // make the dependency available, we expect the add callback + // to be invoked here, then start is called. + d.add(new EventImpl()); + e.step(3); + // remove the dependency, should trigger the stop, then remove callback + d.remove(new EventImpl()); + e.step(6); + c.stop(); + c.remove(d); + Assert.assertEquals("Component stopped, should be unavailable", false, c.isAvailable()); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/test/ConcurrencyTest.java b/dependencymanager/org.apache.felix.dependencymanager/test/test/ConcurrencyTest.java new file mode 100644 index 00000000000..7f999bc1d4e --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/test/test/ConcurrencyTest.java @@ -0,0 +1,120 @@ +/* + * 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 test; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentState; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.impl.ComponentImpl; +import org.junit.Assert; +import org.junit.Test; + +/** + * @author Felix Project Team + */ +public class ConcurrencyTest { + + /** + * Ensure actions from another thread than the current thread executing in the SerialExecutor are being + * scheduled (added to the queue) rather than being executed immediately. + */ + @Test + public void createComponentAddDependencyAndListenerAndAddAnotherDependencyInAParallelThread() { + final Semaphore s = new Semaphore(0); + final ComponentImpl c = new ComponentImpl(); + final SimpleServiceDependency d = new SimpleServiceDependency(); + d.setRequired(true); + final SimpleServiceDependency d2 = new SimpleServiceDependency(); + d2.setRequired(true); + final Thread t = new Thread() { + public void run() { + c.add(d2); + s.release(); + } + }; + ComponentStateListener l = new ComponentStateListener() { + @Override + public void changed(Component component, ComponentState state) { + try { + c.remove(this); + // launch a second thread interacting with our ComponentImpl and block this thread until the + // second thread finished its interaction with our component. We want to ensure the work of + // the second thread is scheduled after our current job in the serial executor and does not + // get executed immediately. + t.start(); + s.acquire(); + Assert.assertEquals("dependency count should be 1", 1, c.getDependencies().size()); + } + catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + + c.setImplementation(new Object()); // If not, we may see NullPointers when invoking lifecycle callbacks + c.start(); + c.add(d); + c.add(l); + Assert.assertEquals("component should not be available", false, c.isAvailable()); + d.add(new EventImpl()); // sets dependency d to available and triggers our ComponentStateListener + + // due to the dependency added by the second thread in the serial executor we still expect our component + // to be unavailable. This work was scheduled in the serial executor and will be executed by the current + // thread after it finished handling the job for handling the changed() method. + Assert.assertEquals("component should not be available", false, c.isAvailable()); + c.remove(l); + Assert.assertEquals("component should not be available", false, c.isAvailable()); + c.remove(d); + Assert.assertEquals("component should not be available", false, c.isAvailable()); + c.remove(d2); + Assert.assertEquals("component should be available", true, c.isAvailable()); + c.stop(); + Assert.assertEquals("component should not be available", false, c.isAvailable()); + } + + @Test + public void createComponentAddAndRemoveDependenciesInParallelThreads() throws Exception { + final ComponentImpl c = new ComponentImpl(); + c.setImplementation(new Object()); // If not, we may see NullPointers when invoking lifecycle callbacks + ExecutorService e = Executors.newFixedThreadPool(16); + c.start(); + for (int i = 0; i < 1000; i++) { + e.execute(new Runnable() { + @Override + public void run() { + SimpleServiceDependency d = new SimpleServiceDependency(); + d.setRequired(true); + c.add(d); +// d.changed(new EventImpl(true)); +// d.changed(new EventImpl(false)); + c.remove(d); + }}); + } + e.shutdown(); + e.awaitTermination(10, TimeUnit.SECONDS); +// Assert.assertEquals("component should not be available", false, c.isAvailable()); + c.stop(); + Assert.assertEquals("component should not be available", false, c.isAvailable()); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/test/ConfigurationTest.java b/dependencymanager/org.apache.felix.dependencymanager/test/test/ConfigurationTest.java new file mode 100644 index 00000000000..98f4e8f0578 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/test/test/ConfigurationTest.java @@ -0,0 +1,132 @@ +/* + * 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 test; + +import java.util.Dictionary; +import java.util.Hashtable; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.impl.ComponentImpl; +import org.apache.felix.dm.impl.ConfigurationDependencyImpl; +import org.junit.Test; +import org.osgi.service.cm.ConfigurationException; + +/** + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes", "unused"}) +public class ConfigurationTest extends TestBase { + @Test + public void testConfigurationFailure() throws Throwable { + final Ensure e = new Ensure(); + + // Create our configuration dependency + final ConfigurationDependencyImpl conf = new ConfigurationDependencyImpl(); + conf.setPid("some.pid"); + + // Create another required dependency + final SimpleServiceDependency requiredDependency = new SimpleServiceDependency(); + requiredDependency.setRequired(true); + requiredDependency.setCallbacks("addDep", null); + + // Create our component, which will fail when handling configuration update + ComponentImpl c = new ComponentImpl(); + + c.setImplementation(new Object() { + volatile Dictionary m_conf; + + public void updated(Dictionary conf) { + debug("updated: conf=%s", conf); + m_conf = conf; + if ("invalid".equals(conf.get("conf"))) { + // We refuse the first configuration. + debug("refusing configuration"); + e.step(1); + // Set our acceptUpdate flag to true, so next update will be successful + throw new RuntimeException("update failed (expected)"); + } + else { + debug("accepting configuration"); + e.step(2); + } + } + + public void addDep() { + if ("invalid".equals(m_conf.get("conf"))) { + e.throwable(new Exception("addDep should not be called")); + } + e.step(3); + debug("addDep"); + } + + void init(Component c) { + if ("invalid".equals(m_conf.get("conf"))) { + e.throwable(new Exception("init should not be called")); + } + e.step(4); + debug("init"); + } + + void start() { + if ("invalid".equals(m_conf.get("conf"))) { + e.throwable(new Exception("start should not be called")); + } + e.step(5); + debug("start"); + } + }); + + // Add the dependencies + c.add(conf); + c.add(requiredDependency); + + // Start our component ("requiredDependency" is not yet available, so we'll stay in WAITING_FOR_REQUIRED state). + c.start(); + + // Enabled "requiredDependency" + requiredDependency.add(new EventImpl()); + + // Now, act as the configuration admin service and inject a wrong dependency + try { + Hashtable props = new Hashtable(); + props.put("conf", "invalid"); + conf.updated(props); + } + catch (ConfigurationException err) { + warn("got expected configuration error"); + } + e.waitForStep(1, 5000); + e.ensure(); + + // Now, inject another valid configuration + try { + Hashtable props = new Hashtable(); + props.put("conf", "valid"); + conf.updated(props); + } + catch (ConfigurationException err) { + warn("got unexpected configuration error"); + e.throwable(err); + } + + // This time, our component should be started properly. + e.waitForStep(5, 5000); + e.ensure(); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/test/Ensure.java b/dependencymanager/org.apache.felix.dependencymanager/test/test/Ensure.java new file mode 100644 index 00000000000..ff2c308e209 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/test/test/Ensure.java @@ -0,0 +1,186 @@ +/* + * 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 test; + +import java.io.PrintStream; + +import org.junit.Assert; + +/** + * Helper class to make sure that steps in a test happen in the correct order. Instantiate + * this class and subsequently invoke step(nr) with steps starting at 1. You + * can also have threads wait until you arrive at a certain step. + * + * @author Felix Project Team + */ +public class Ensure { + private final boolean DEBUG; + private static long INSTANCE = 0; + private static final int RESOLUTION = 100; + private static PrintStream STREAM = System.out; + int step = 0; + private Throwable m_throwable; + private boolean previousStepFailed; + + public Ensure() { + this(true); + } + + public Ensure(boolean debug) { + DEBUG = debug; + if (DEBUG) { + INSTANCE++; + } + } + + public static void setStream(PrintStream output) { + STREAM = output; + } + + /** + * Mark this point as step nr. + * + * @param nr the step we are in + */ + public synchronized void step(int nr) { + if (previousStepFailed) { + throw new RuntimeException("can not enter into step " + nr + " (some previous steps failed)"); + } + step++; + try { + Assert.assertEquals(nr, step); + } catch (Throwable e) { + previousStepFailed = true; + throw e; + } + if (DEBUG) { + String info = getLineInfo(3); + STREAM.println("[Ensure " + INSTANCE + "] step " + step + " [" + currentThread() + "] " + info); + } + notifyAll(); + } + + private String getLineInfo(int depth) { + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + String info = trace[depth].getClassName() + "." + trace[depth].getMethodName() + ":" + trace[depth].getLineNumber(); + return info; + } + + /** + * Mark this point as the next step. + */ + public synchronized void step() { + if (previousStepFailed) { + throw new RuntimeException("can not enter into step " + (step+1) + " (some previous steps failed)"); + } + step++; + if (DEBUG) { + String info = getLineInfo(3); + STREAM.println("[Ensure " + INSTANCE + "] next step " + step + " [" + currentThread() + "] " + info); + } + notifyAll(); + } + + /** + * Wait until we arrive at least at step nr in the process, or fail if that + * takes more than timeout milliseconds. If you invoke wait on a thread, + * you are effectively assuming some other thread will invoke the step(nr) + * method. + * + * @param nr the step to wait for + * @param timeout the number of milliseconds to wait + */ + public synchronized void waitForStep(int nr, int timeout) { + final int initialTimeout = timeout; + if (DEBUG) { + String info = getLineInfo(3); + STREAM.println("[Ensure " + INSTANCE + "] waiting for step " + nr + " [" + currentThread() + "] " + info); + } + while (step < nr && timeout > 0) { + try { + wait(RESOLUTION); + timeout -= RESOLUTION; + } + catch (InterruptedException e) {} + } + if (step < nr) { + throw new IllegalStateException("Timed out waiting for " + initialTimeout + " ms for step " + nr + ", we are still at step " + step); + } + if (DEBUG) { + String info = getLineInfo(3); + STREAM.println("[Ensure " + INSTANCE + "] arrived at step " + nr + " [" + currentThread() + "] " + info); + } + } + + private String currentThread() { + Thread thread = Thread.currentThread(); + return thread.getId() + " " + thread.getName(); + } + + public static Runnable createRunnableStep(final Ensure ensure, final int nr) { + return new Runnable() { public void run() { ensure.step(nr); }}; + } + + public synchronized void steps(Steps steps) { + steps.next(this); + } + + /** + * Helper class for naming a list of step numbers. If used with the steps(Steps) method + * you can define at which steps in time this point should be passed. That means you can + * check methods that will get invoked multiple times during a test. + */ + public static class Steps { + private final int[] m_steps; + private int m_stepIndex; + + /** + * Create a list of steps and initialize the step counter to zero. + */ + public Steps(int... steps) { + m_steps = steps; + m_stepIndex = 0; + } + + /** + * Ensure we're at the right step. Will throw an index out of bounds exception if we enter this step more often than defined. + */ + public void next(Ensure ensure) { + ensure.step(m_steps[m_stepIndex++]); + } + } + + /** + * Saves a thrown exception that occurred in a different thread. You can only save one exception + * at a time this way. + */ + public synchronized void throwable(Throwable throwable) { + m_throwable = throwable; + } + + /** + * Throws a Throwable if one occurred in a different thread and that thread saved it + * using the throwable() method. + */ + public synchronized void ensure() throws Throwable { + if (m_throwable != null) { + throw m_throwable; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/test/EventImpl.java b/dependencymanager/org.apache.felix.dependencymanager/test/test/EventImpl.java new file mode 100644 index 00000000000..28c5455bbdf --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/test/test/EventImpl.java @@ -0,0 +1,71 @@ +/* + * 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 test; + +import org.apache.felix.dm.context.Event; + +/** in real life, this event might contain a service reference and service instance + * or something similar + * + * @author Felix Project Team + */ +public class EventImpl extends Event { // the actual event object (a Service, a Bundle, a Configuration, etc ...) + private final int m_id; + + public EventImpl() { + this(1); + } + /** By constructing events with different IDs, we can simulate different unique instances. */ + public EventImpl(int id) { + this (id, null); + } + + public EventImpl(int id, Object event) { + super(event); + m_id = id; + } + + + @Override + public int hashCode() { + return m_id; + } + + @Override + public boolean equals(Object obj) { + // an instanceof check here is not "strong" enough with subclasses overriding the + // equals: we need to be sure that a.equals(b) == b.equals(a) at all times + if (obj != null && obj.getClass().equals(EventImpl.class)) { + return ((EventImpl) obj).m_id == m_id; + } + return false; + } + + @Override + public int compareTo(Event o) { + EventImpl a = this, b = (EventImpl) o; + if (a.m_id < b.m_id) { + return -1; + } else if (a.m_id == b.m_id){ + return 0; + } else { + return 1; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/test/MultiPropertyFilterIndexPerformanceTest.java b/dependencymanager/org.apache.felix.dependencymanager/test/test/MultiPropertyFilterIndexPerformanceTest.java new file mode 100644 index 00000000000..95c7d7935a4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/test/test/MultiPropertyFilterIndexPerformanceTest.java @@ -0,0 +1,102 @@ +/* + * 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 test; + +import static org.junit.Assert.assertFalse; + +import java.util.List; + +import org.apache.felix.dm.impl.index.multiproperty.MultiPropertyFilterIndex; +import org.junit.Test; +import org.osgi.framework.ServiceReference; + +public class MultiPropertyFilterIndexPerformanceTest { + + final int testSize = 5000; + + final int iterations = 10; + + @Test + public void MultiPropertyFilterIndexTest() { + MultiPropertyFilterIndex stringIndex; + + for (int i = 0; i < iterations; i++) { + stringIndex = new MultiPropertyFilterIndex("component-identifier,model,concept,role"); + testPerformance(stringIndex, testSize, "bystring"); + } + } + + + @SuppressWarnings("rawtypes") + private void testPerformance(MultiPropertyFilterIndex filterIndex, int runSize, String indexName) { + System.gc(); + long start = System.currentTimeMillis(); + long memoryBefore = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + + for (int i = 0; i < runSize; i++) { + TestReference newReference = new TestReference(); + String[] multiValue = { "CREATES", "UPDATES" }; + String[] mv2 = { "extra1", "extra2", "extra3" }; + + newReference.addProperty("component-identifier", + "org.acme.xyz.platform.interfaces.modeldrivenservices.DialogService"); + newReference.addProperty("model", "//Housing benefit request/Housing benefit request.model" + i); + newReference.addProperty("concept", + "//Housing benefit request/2000 Requests/40000/Housing benefit request.model#concept" + i); + newReference.addProperty("role", multiValue); + newReference.addProperty("extra", mv2); + + filterIndex.addedService(newReference, new Object()); + } + + double writeTime = ((System.currentTimeMillis() - start) / 1000.0); + long startReading = System.currentTimeMillis(); + + for (int i = 0; i < runSize; i++) { + List allServiceReferences = filterIndex.getAllServiceReferences(null, + "(&(component-identifier=org.acme.xyz.platform.interfaces.modeldrivenservices.DialogService)" + + "(model=//Housing benefit request/Housing benefit request.model" + i + ")" + + "(concept=//Housing benefit request/2000 Requests/40000/Housing benefit request.model#concept" + + i + ")" + "(role=CREATES))"); + + if (allServiceReferences.size() != 1) { + throw new AssertionError("Failed to find reference in cache"); + } + + } + + // Sanitiy check + List allServiceReferences = filterIndex.getAllServiceReferences(null, + "(&(model=mymodel1)(concept=abracadabrra)(role=CREATES))"); + if (allServiceReferences.size() != 0) { + assertFalse(allServiceReferences.size() != 0); + } + + double readTime = ((System.currentTimeMillis() - startReading) / 1000.0); + + System.gc(); + long memoryAfter = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + double consumed = (memoryAfter - memoryBefore) / 1048576.0; + + System.err.println("w: " + writeTime + ", r: " + readTime + ",m: " + consumed + " # of iterations: " + runSize + " -- " + + indexName); + } + + +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/test/MultiPropertyFilterIndexReferencedTest.java b/dependencymanager/org.apache.felix.dependencymanager/test/test/MultiPropertyFilterIndexReferencedTest.java new file mode 100644 index 00000000000..1fd8fd044c4 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/test/test/MultiPropertyFilterIndexReferencedTest.java @@ -0,0 +1,301 @@ +/* + * 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 test; + +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.Properties; + +import org.apache.felix.dm.impl.index.multiproperty.MultiPropertyFilterIndex; +import org.junit.Test; +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceReference; + +public class MultiPropertyFilterIndexReferencedTest { + + + @SuppressWarnings("rawtypes") + @Test + public void noContext() { + MultiPropertyFilterIndex singleValueFilterIndex = new MultiPropertyFilterIndex("objectClass,!context"); + + TestReference ref1 = new TestReference(); + ref1.addProperty("service.id", 4711); + ref1.addProperty("objectclass", "java.lang.String"); + ref1.addProperty("context", "context"); + + TestReference ref2 = new TestReference(); + ref2.addProperty("service.id", 1000); + ref2.addProperty("objectclass", "java.lang.String"); + + singleValueFilterIndex.addedService(ref1, new String("Service1")); + + singleValueFilterIndex.addedService(ref2, new String("Service2")); + + List allServiceReferencesByClass = singleValueFilterIndex.getAllServiceReferences("java.lang.String", null); + List allServiceReferencesByFilter = singleValueFilterIndex.getAllServiceReferences(null, "(objectClass=java.lang.String)"); + + assertTrue(allServiceReferencesByClass.size() == allServiceReferencesByFilter.size()); + assertTrue(allServiceReferencesByFilter.size() == 1); + } + + + @SuppressWarnings("rawtypes") + @Test + public void noContextCid() { + MultiPropertyFilterIndex multiPropertyIndex_new = new MultiPropertyFilterIndex("objectClass,cid,!context"); + + TestReference ref1 = new TestReference(); + ref1.addProperty("service.id", 4711); + ref1.addProperty("objectclass", "java.lang.String"); + ref1.addProperty("context", "context"); + + TestReference ref2 = new TestReference(); + ref2.addProperty("service.id", 1000); + ref2.addProperty("cid", "cid2"); + ref2.addProperty("objectclass", "java.lang.String"); + + TestReference ref3 = new TestReference(); + ref3.addProperty("cid", "cid2"); + ref3.addProperty("rank", "1"); + ref3.addProperty("service.id", 1000); + ref3.addProperty("objectclass", "java.lang.String"); + + multiPropertyIndex_new.addedService(ref1, new String("Service1")); + + multiPropertyIndex_new.addedService(ref2, new String("Service2")); + + multiPropertyIndex_new.addedService(ref3, new String("Service3")); + + List result_new = multiPropertyIndex_new.getAllServiceReferences("java.lang.String", "(cid=cid2)"); + + assertTrue(result_new.size() == 2); + } + + + + + @SuppressWarnings("rawtypes") + @Test + public void singleKeyfilterIndex() { + MultiPropertyFilterIndex multiPropertyIndex = new MultiPropertyFilterIndex("objectClass"); + TestReference ref1 = new TestReference(); + ref1.addProperty("service.id", 4711); + ref1.addProperty("objectclass", "java.lang.String"); + + TestReference ref2 = new TestReference(); + ref2.addProperty("service.id", 4711); + ref2.addProperty("objectclass", "java.lang.Object"); + + multiPropertyIndex.addedService(ref1, new String("Service1")); + multiPropertyIndex.addedService(ref2, new Object()); + + //find by classname; + assertTrue(multiPropertyIndex.isApplicable("java.lang.String", "")); + List byClazzName = multiPropertyIndex.getAllServiceReferences("java.lang.String", ""); + assertTrue(byClazzName.size() == 1); + assertTrue(byClazzName.get(0).equals(ref1)); + + //find by filter + assertTrue(multiPropertyIndex.isApplicable(null, "(objectClass=java.lang.String)")); + byClazzName = multiPropertyIndex.getAllServiceReferences(null, "(objectClass=java.lang.String)"); + assertTrue(byClazzName.size() == 1); + assertTrue(byClazzName.get(0).equals(ref1)); + + //Add extra service + TestReference ref3 = new TestReference(); + ref3.addProperty("service.id", 4712); + ref3.addProperty("objectclass", "java.lang.String"); + multiPropertyIndex.addedService(ref3, new String("Service3")); + + byClazzName = multiPropertyIndex.getAllServiceReferences("java.lang.String", null); + assertTrue(byClazzName.size() == 2); + assertTrue(byClazzName.get(0).equals(ref1)); + assertTrue(byClazzName.get(1).equals(ref3)); + } + + @SuppressWarnings("rawtypes") + @Test + public void propertyIndexWithDoubleNoPermutationKeys() { + String filterConfig = "objectClass,#related-concept-absoluteidentifier,#context-concept-absoluteidentifier,StoreClass"; + + MultiPropertyFilterIndex multiPropertyIndex_new = new MultiPropertyFilterIndex(filterConfig); + + String[] relatedConcepts = {"rel-a", "rel-b"}; + String[] contextConcepts = {"cca-a", "cca-b"}; + + TestReference ref1 = new TestReference(); + ref1.addProperty("service.id", 4711); + ref1.addProperty("objectclass", "java.lang.String"); + ref1.addProperty("related-concept-absoluteidentifier", relatedConcepts); + ref1.addProperty("context-concept-absoluteidentifier", contextConcepts); + ref1.addProperty("StoreClass", "NoteStore"); + + multiPropertyIndex_new.addedService(ref1, new String("Service1")); + + List result_new = multiPropertyIndex_new.getAllServiceReferences("java.lang.String", "(&(context-concept-absoluteidentifier=cca-a)(related-concept-absoluteidentifier=rel-b)(storeclass=NoteStore))"); + + assertTrue(result_new.size() == 1); + } + + @SuppressWarnings("rawtypes") + @Test + public void propertyIndexWithDoubleMultiProperty() { + String filterConfig = "objectClass,related-concept-absoluteidentifier,context-concept-absoluteidentifier,StoreClass"; + MultiPropertyFilterIndex multiPropertyIndex_new = new MultiPropertyFilterIndex(filterConfig); + + + String[] relatedConcepts = {"rel-a", "rel-b"}; + String[] contextConcepts = {"cca-a", "cca-b"}; + + + TestReference ref1 = new TestReference(); + ref1.addProperty("service.id", 4711); + ref1.addProperty("objectclass", "java.lang.String"); + ref1.addProperty("related-concept-absoluteidentifier", relatedConcepts); + ref1.addProperty("context-concept-absoluteidentifier", contextConcepts); + ref1.addProperty("StoreClass", "NoteStore"); + + multiPropertyIndex_new.addedService(ref1, new String("Service1")); + + List result_new = multiPropertyIndex_new.getAllServiceReferences("java.lang.String", "(&(context-concept-absoluteidentifier=cca-a)(related-concept-absoluteidentifier=rel-b)(storeclass=NoteStore))"); + + assertTrue(result_new.size() == 1); + } + + + @SuppressWarnings("rawtypes") + @Test + public void MultiPropertyFilterIndexTypes() { + + long serviceId = 4711; + int ranking = 46; + boolean isSomeBoolean = true; + String[] interfaces = {"A", "B", "C"}; + + MultiPropertyFilterIndex multiPropertyIndexSingleFilter = new MultiPropertyFilterIndex("objectClass"); + MultiPropertyFilterIndex multiPropertyIndexMultiple = new MultiPropertyFilterIndex("objectClass,service.id,ranking,interfaces"); + + TestReference newReference = new TestReference(); + newReference.addProperty("service.id", serviceId); + newReference.addProperty("objectclass", "java.lang.Object"); + newReference.addProperty("ranking", ranking); + newReference.addProperty("someboolvalue", isSomeBoolean); + newReference.addProperty("interfaces", interfaces); + + multiPropertyIndexMultiple.addedService(newReference, new Object()); + multiPropertyIndexSingleFilter.addedService(newReference, new Object()); + + List noFilter = multiPropertyIndexSingleFilter.getAllServiceReferences("java.lang.Object", null); + assertTrue(noFilter.size() == 1); + + List noClazz = multiPropertyIndexMultiple.getAllServiceReferences(null, "(&(objectClass=java.lang.Object)(&(service.id=4711)(ranking=46)(interfaces=B)))"); + assertTrue(noClazz.size() == 1); + + List combi = multiPropertyIndexMultiple.getAllServiceReferences("java.lang.Object", "(&(ranking=46)(interfaces=C)(service.id=4711))"); + assertTrue(combi.size() == 1); + } + + @SuppressWarnings("rawtypes") + @Test + public void MultiPropertyFilterIndexKeyGen() { + String key = "(&(objectClass=org.acme.xyz.framework.internationalization.Translatable)(component-identifier=org.acme.xyz.framework.webui.engine.impl.CompoundProcessContextGroupingPanelFactory))"; + + long serviceId = 4711; + int ranking = 46; + + MultiPropertyFilterIndex biIndex = new MultiPropertyFilterIndex("objectClass,component-identifier"); + TestReference newReference = new TestReference(); + newReference.addProperty("service.id", serviceId); + newReference.addProperty("ranking", ranking); + newReference.addProperty("component-identifier", "org.acme.xyz.framework.webui.engine.impl.CompoundProcessContextGroupingPanelFactory"); + newReference.addProperty("objectclass", "org.acme.xyz.framework.internationalization.Translatable"); + + biIndex.addedService(newReference, new Object()); + + + List noClazz = biIndex.getAllServiceReferences(null, key); + + assertTrue(noClazz.size() == 1); + } + + + @SuppressWarnings("rawtypes") + class TestReference implements ServiceReference { + Properties props = new Properties(); + + TestReference() { + } + + public void addProperty(String key, String value) { + /* Property keys are case-insensitive. -> see @ org.osgi.framework.ServiceReference */ + props.put(key.toLowerCase(), value); + } + + public void addProperty(String key, long value) { + props.put(key, value); + } + + public void addProperty(String key, int value) { + props.put(key, value); + } + + public void addProperty(String key, boolean value) { + props.put(key, value); + } + + public void addProperty(String key, String[] multiValue) { + props.put(key, multiValue); + } + + + @Override + public Object getProperty(String key) { + return props.get(key); + } + + @Override + public String[] getPropertyKeys() { + return props.keySet().toArray(new String[]{}); + } + + @Override + public Bundle getBundle() { + return null; + } + + @Override + public Bundle[] getUsingBundles() { + return null; + } + + @Override + public boolean isAssignableTo(Bundle bundle, String className) { + return false; + } + + @Override + public int compareTo(Object reference) { + // TODO Auto-generated method stub + return 0; + } + + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/test/MultiPropertyKeyTest.java b/dependencymanager/org.apache.felix.dependencymanager/test/test/MultiPropertyKeyTest.java new file mode 100644 index 00000000000..c1a4e6f3d81 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/test/test/MultiPropertyKeyTest.java @@ -0,0 +1,116 @@ +/* + * 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 test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.apache.felix.dm.impl.index.multiproperty.MultiPropertyFilterIndex; +import org.apache.felix.dm.impl.index.multiproperty.MultiPropertyKey; +import org.junit.Test; + +public class MultiPropertyKeyTest { + + @Test + public void sameKeysAndValues() { + MultiPropertyKey actual = new MultiPropertyKey(2); + actual.add("key1", "abc"); + actual.add("key2", "efg"); + + MultiPropertyKey other = new MultiPropertyKey(2); + other.add("key1", "abc"); + other.add("key2", "efg"); + + assertTrue(actual.equals(other)); + assertTrue(actual.hashCode() == other.hashCode()); + } + + @Test + public void sameKeysAndValuesDifferentOrder() { + MultiPropertyKey actual = new MultiPropertyKey(2); + actual.add("key1", "abc"); + actual.add("key2", "efg"); + + MultiPropertyKey other = new MultiPropertyKey(2); + other.add("key2", "efg"); + other.add("key1", "abc"); + + assertTrue(actual.equals(other)); + assertTrue(actual.hashCode() == other.hashCode()); + } + + @Test + public void sameKeysDifferentValues() { + MultiPropertyKey actual = new MultiPropertyKey(2); + actual.add("key1", "abc"); + actual.add("key2", "efg"); + + MultiPropertyKey other = new MultiPropertyKey(2); + other.add("key1", "efg"); + other.add("key2", "abc"); + + assertFalse(actual.equals(other)); + assertFalse(actual.hashCode() == other.hashCode()); + } + + @Test + public void subSetOfKeyValues() { + MultiPropertyKey actual = new MultiPropertyKey(3); + actual.add("key1", "abc"); + actual.add("key2", "efg"); + actual.add("key3", "hij"); + + MultiPropertyKey other = new MultiPropertyKey(2); + other.add("key1", "abc"); + other.add("key2", "efg"); + + assertFalse(actual.equals(other)); + assertFalse(actual.hashCode() == other.hashCode()); + } + + @Test + public void noPermutations() { + MultiPropertyFilterIndex singleValueFilterIndex = new MultiPropertyFilterIndex("objectClass,#nopermutation"); + + TestReference ref1 = new TestReference(); + ref1.addProperty("service.id", 4711); + ref1.addProperty("objectclass", "java.lang.String"); + ref1.addProperty("nopermutation", new String[] { "a", "b", "c", "d" }); + + List keys = singleValueFilterIndex.createKeys(ref1); + + assertTrue(keys.size() == 4); + } + + @Test + public void permutations() { + MultiPropertyFilterIndex singleValueFilterIndex = new MultiPropertyFilterIndex("objectClass,permutation"); + + TestReference ref1 = new TestReference(); + ref1.addProperty("service.id", 4711); + ref1.addProperty("objectclass", "java.lang.String"); + ref1.addProperty("permutation", new String[] { "a", "b", "c", "d" }); + + List keys = singleValueFilterIndex.createKeys(ref1); + + assertTrue(keys.size() == 10); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/test/SerialExecutorTest.java b/dependencymanager/org.apache.felix.dependencymanager/test/test/SerialExecutorTest.java new file mode 100644 index 00000000000..feb04c4bdfa --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/test/test/SerialExecutorTest.java @@ -0,0 +1,135 @@ +/* + * 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 test; + +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.felix.dm.Logger; +import org.apache.felix.dm.impl.SerialExecutor; +import org.junit.Assert; +import org.junit.Test; + +/** + * Validates SerialExecutor used by DM implementation. + * + * @author Felix Project Team + */ +public class SerialExecutorTest extends TestBase { + final Random m_rnd = new Random(); + final int TESTS = 100000; + + @Test + public void testSerialExecutor() { + info("Testing serial executor"); + int cores = Math.max(10, Runtime.getRuntime().availableProcessors()); + ExecutorService threadPool = null; + + try { + threadPool = Executors.newFixedThreadPool(cores); + final SerialExecutor serial = new SerialExecutor(new Logger(null)); + + long timeStamp = System.currentTimeMillis(); + for (int i = 0; i < TESTS; i++) { + final CountDownLatch latch = new CountDownLatch(cores * 2 /* each task reexecutes itself one time */); + final SerialTask task = new SerialTask(serial, latch); + for (int j = 0; j < cores; j ++) { + threadPool.execute(new Runnable() { + public void run() { + serial.execute(task); + } + }); + } + Assert.assertTrue("Test " + i + " did not terminate timely", latch.await(20000, TimeUnit.MILLISECONDS)); + } + long now = System.currentTimeMillis(); + System.out.println("Performed " + TESTS + " tests in " + (now - timeStamp) + " ms."); + timeStamp = now; + } + catch (Throwable t) { + t.printStackTrace(); + Assert.fail("Test failed: " + t.getMessage()); + } + finally { + if (threadPool != null) { + shutdown(threadPool); + } + } + } + + void shutdown(ExecutorService exec) { + exec.shutdown(); + try { + exec.awaitTermination(5, TimeUnit.SECONDS); + } + catch (InterruptedException e) { + } + } + + class SerialTask implements Runnable { + final AtomicReference m_executingThread = new AtomicReference(); + final CountDownLatch m_latch; + private boolean m_firstExecution; + private final SerialExecutor m_exec; + + SerialTask(SerialExecutor exec, CountDownLatch latch) { + m_latch = latch; + m_exec = exec; + m_firstExecution = true; + } + + public void run() { + Thread self = Thread.currentThread(); + if (m_firstExecution) { + // The first time we are executed, the previous executing thread stored in our m_executingThread should be null + if (!m_executingThread.compareAndSet(null, self)) { + System.out.println("detected concurrent call to SerialTask: currThread=" + self + + ", other executing thread=" + m_executingThread); + return; + } + } else { + // The second time we are executed, the previous executing thread stored in our m_executingThread should be + // the current running thread. + if (m_executingThread.get() != self) { + System.out.println("expect to execute reentrant tasks in same thread, but current thread=" + self + + ", while expected is " + m_executingThread); + return; + } + } + + if (m_firstExecution) { + m_firstExecution = false; + m_exec.execute(this); // Our run method must be called immediately + } else { + if (! m_executingThread.compareAndSet(self, null)) { + System.out.println("detected concurrent call to SerialTask: currThread=" + self + + ", other executing thread=" + m_executingThread); + return; + } + m_firstExecution = true; + } + + m_latch.countDown(); + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/test/ServiceRaceTest.java b/dependencymanager/org.apache.felix.dependencymanager/test/test/ServiceRaceTest.java new file mode 100644 index 00000000000..abe7c58367b --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/test/test/ServiceRaceTest.java @@ -0,0 +1,253 @@ +/* + * 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 test; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.ComponentState; +import org.apache.felix.dm.ComponentStateListener; +import org.apache.felix.dm.context.Event; +import org.apache.felix.dm.impl.ComponentImpl; +import org.apache.felix.dm.impl.ConfigurationDependencyImpl; +import org.junit.Assert; +import org.junit.Test; +import org.osgi.service.cm.ConfigurationException; + +/** + * This test class simulates a client having many dependencies being registered/unregistered concurrently. + * + * @author Felix Project Team + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class ServiceRaceTest extends TestBase { + final static int STEP_WAIT = 5000; + final static int DEPENDENCIES = 10; + final static int LOOPS = 10000; + + // Executor used to bind/unbind service dependencies. + ExecutorService m_threadpool; + // Timestamp used to log the time consumed to execute 100 tests. + long m_timeStamp; + + /** + * Creates many service dependencies, and activate/deactivate them concurrently. + */ + @Test + public void createParallelComponentRegistgrationUnregistration() { + info("Starting createParallelComponentRegistgrationUnregistration test"); + int cores = Math.max(16, Runtime.getRuntime().availableProcessors()); + info("using " + cores + " cores."); + + m_threadpool = Executors.newFixedThreadPool(Math.max(cores, DEPENDENCIES + 3 /* start/stop/configure */)); + + try { + m_timeStamp = System.currentTimeMillis(); + for (int loop = 0; loop < LOOPS; loop++) { + doTest(loop); + } + } + catch (Throwable t) { + warn("got unexpected exception", t); + } + finally { + shutdown(m_threadpool); + } + } + + void shutdown(ExecutorService exec) { + exec.shutdown(); + try { + exec.awaitTermination(5, TimeUnit.SECONDS); + } + catch (InterruptedException e) { + } + } + + void doTest(int loop) throws Throwable { + debug("loop#%d -------------------------", loop); + + final Ensure step = new Ensure(false); + + // Create one client component, which depends on many service dependencies + final ComponentImpl client = new ComponentImpl(); + final Client theClient = new Client(step); + client.setImplementation(theClient); + + // Create client service dependencies + final SimpleServiceDependency[] dependencies = new SimpleServiceDependency[DEPENDENCIES]; + for (int i = 0; i < DEPENDENCIES; i++) { + dependencies[i] = new SimpleServiceDependency(); + dependencies[i].setRequired(true); + dependencies[i].setCallbacks("add", "remove"); + client.add(dependencies[i]); + } + final ConfigurationDependencyImpl confDependency = new ConfigurationDependencyImpl(); + confDependency.setPid("mypid"); + client.add(confDependency); + + // Create Configuration (concurrently). + // We have to simulate the configuration update, using a component state listener, which will + // trigger an update thread, but only once the component is started. + final ComponentStateListener listener = new ComponentStateListener() { + private volatile Dictionary m_conf; + + public void changed(Component c, ComponentState state) { + if (state == ComponentState.WAITING_FOR_REQUIRED && m_conf == null) { + m_conf = new Hashtable(); + m_conf.put("foo", "bar"); + m_threadpool.execute(new Runnable() { + public void run() { + try { + confDependency.updated(m_conf); + } + catch (ConfigurationException e) { + warn("configuration failed", e); + } + } + }); + } + } + }; + client.add(listener); + + + // Start the client (concurrently) + m_threadpool.execute(new Runnable() { + public void run() { + client.start(); + + // Activate the client service dependencies concurrently. + // We *must* do this after having started the component (in a reality, the dependencies can be + // injected only one the tracker has been opened ... + for (int i = 0; i < DEPENDENCIES; i++) { + final SimpleServiceDependency dep = dependencies[i]; + final Event added = new EventImpl(i); + m_threadpool.execute(new Runnable() { + public void run() { + dep.add(added); + } + }); + } + + } + }); + + // Ensure that client has been started. + int expectedStep = 1 /* conf */ + DEPENDENCIES + 1 /* start */; + step.waitForStep(expectedStep, STEP_WAIT); + Assert.assertEquals(DEPENDENCIES, theClient.getDependencies()); + Assert.assertNotNull(theClient.getConfiguration()); + client.remove(listener); + + // Stop the client and all dependencies concurrently. + for (int i = 0; i < DEPENDENCIES; i++) { + final SimpleServiceDependency dep = dependencies[i]; + final Event removed = new EventImpl(i); + m_threadpool.execute(new Runnable() { + public void run() { + dep.remove(removed); + } + }); + } + m_threadpool.execute(new Runnable() { + public void run() { + client.stop(); + } + }); + m_threadpool.execute(new Runnable() { + public void run() { + try { + // simulate a configuration suppression. + confDependency.updated(null); + } + catch (ConfigurationException e) { + warn("error while unconfiguring", e); + } + } + }); + + // Ensure that client has been stopped, then destroyed, then unbound from all dependencies + expectedStep += 2; // stop/destroy + expectedStep += DEPENDENCIES; // removed all dependencies + step.waitForStep(expectedStep, STEP_WAIT); + step.ensure(); + Assert.assertEquals(0, theClient.getDependencies()); + + debug("finished one test loop"); + if ((loop + 1) % 100 == 0) { + long duration = System.currentTimeMillis() - m_timeStamp; + warn("Performed 100 tests (total=%d) in %d ms.", (loop + 1), duration); + m_timeStamp = System.currentTimeMillis(); + } + } + + public class Client { + final Ensure m_step; + int m_dependencies; + volatile Dictionary m_conf; + + public Client(Ensure step) { + m_step = step; + } + + public void updated(Dictionary conf) throws ConfigurationException { + if (conf != null) { + Assert.assertNotNull(conf); + Assert.assertEquals("bar", conf.get("foo")); + m_conf = conf; + m_step.step(1); + } + } + + synchronized void add() { + m_step.step(); + m_dependencies++; + } + + synchronized void remove() { + m_step.step(); + m_dependencies--; + } + + void start() { + m_step.step((DEPENDENCIES + 1) /* deps + conf */ + 1 /* start */); + } + + void stop() { + m_step.step((DEPENDENCIES + 1) /* deps + conf */ + 1 /* start */ + 1 /* stop */); + } + + void destroy() { + m_step.step((DEPENDENCIES + 1) /* deps + conf */ + 1 /* start */ + 1 /* stop */ + 1 /* destroy */); + } + + synchronized int getDependencies() { + return m_dependencies; + } + + Dictionary getConfiguration() { + return m_conf; + } + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/test/SimpleServiceDependency.java b/dependencymanager/org.apache.felix.dependencymanager/test/test/SimpleServiceDependency.java new file mode 100644 index 00000000000..323fffc613f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/test/test/SimpleServiceDependency.java @@ -0,0 +1,94 @@ +/* + * 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 test; + +import org.apache.felix.dm.Dependency; +import org.apache.felix.dm.context.AbstractDependency; +import org.apache.felix.dm.context.DependencyContext; +import org.apache.felix.dm.context.Event; +import org.apache.felix.dm.context.EventType; + +/** + * @author Felix Project Team + */ +public class SimpleServiceDependency extends AbstractDependency { + @Override + public String getType() { + return "SimpleServiceDependency"; + } + + @Override + public String getSimpleName() { + return "SimpleServiceDependency"; + } + + @Override + public DependencyContext createCopy() { + return new SimpleServiceDependency(); + } + + @Override + public void invokeCallback(EventType type, Event ... e) { + switch (type) { + case ADDED: + if (m_add != null) { + invoke (m_add, e[0], getInstances()); + } + break; + case CHANGED: + if (m_change != null) { + invoke (m_change, e[0], getInstances()); + } + break; + case REMOVED: + if (m_remove != null) { + invoke (m_remove, e[0], getInstances()); + } + break; + default: + break; + } + } + + public void invoke(String method, Event e, Object[] instances) { + // specific for this type of dependency + m_component.invokeCallbackMethod(instances, method, new Class[][] { {} }, new Object[][] { {} }); + } + + public void add(final Event e) { + m_component.handleEvent(this, EventType.ADDED, e); + } + + public void change(final Event e) { + m_component.handleEvent(this, EventType.CHANGED, e); + } + + public void remove(final Event e) { + m_component.handleEvent(this, EventType.REMOVED, e); + } + + public void swap(final Event event, final Event newEvent) { + m_component.handleEvent(this, EventType.SWAPPED, event, newEvent); + } + + @Override + public Class getAutoConfigType() { + return null; // we don't support auto config mode. + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/test/TestBase.java b/dependencymanager/org.apache.felix.dependencymanager/test/test/TestBase.java new file mode 100644 index 00000000000..948a596515f --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/test/test/TestBase.java @@ -0,0 +1,70 @@ +/* + * 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 test; + +import static java.lang.System.out; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * Base class for all tests. + * For now, this class provides logging support. + * + * @author Felix Project Team + */ +public class TestBase { + final int WARN = 1; + final int INFO = 2; + final int DEBUG = 3; + + // Set the enabled log level. + final int m_level = WARN; + + @SuppressWarnings("unused") + void debug(String format, Object ... params) { + if (m_level >= DEBUG) { + out.println(Thread.currentThread().getName() + " - " + String.format(format, params)); + } + } + + void warn(String format, Object ... params) { + warn(format, null, params); + } + + @SuppressWarnings("unused") + void info(String format, Object ... params) { + if (m_level >= INFO) { + out.println(Thread.currentThread().getName() + " - " + String.format(format, params)); + } + } + + void warn(String format, Throwable t, Object ... params) { + StringBuilder sb = new StringBuilder(); + sb.append(Thread.currentThread().getName()).append(" - ").append(String.format(format, params)); + if (t != null) { + StringWriter buffer = new StringWriter(); + PrintWriter pw = new PrintWriter(buffer); + t.printStackTrace(pw); + sb.append(System.getProperty("line.separator")); + sb.append(buffer.toString()); + } + System.out.println(sb.toString()); + } +} diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/test/TestReference.java b/dependencymanager/org.apache.felix.dependencymanager/test/test/TestReference.java new file mode 100644 index 00000000000..73ebc6bea12 --- /dev/null +++ b/dependencymanager/org.apache.felix.dependencymanager/test/test/TestReference.java @@ -0,0 +1,88 @@ +/* + * 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 test; + +import java.util.Properties; + +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceReference; + +@SuppressWarnings("rawtypes") +class TestReference implements ServiceReference { + Properties props = new Properties(); + + public TestReference() { + } + + public void addProperty(String key, String value) { + /* + * Property keys are case-insensitive. -> see @ + * org.osgi.framework.ServiceReference + */ + props.put(key.toLowerCase(), value); + } + + public void addProperty(String key, long value) { + props.put(key, value); + } + + public void addProperty(String key, int value) { + props.put(key, value); + } + + public void addProperty(String key, boolean value) { + props.put(key, value); + } + + public void addProperty(String key, String[] multiValue) { + props.put(key, multiValue); + } + + @Override + public Object getProperty(String key) { + return props.get(key); + } + + @Override + public String[] getPropertyKeys() { + return props.keySet().toArray(new String[] {}); + } + + @Override + public Bundle getBundle() { + return null; + } + + @Override + public Bundle[] getUsingBundles() { + return null; + } + + @Override + public boolean isAssignableTo(Bundle bundle, String className) { + return false; + } + + @Override + public int compareTo(Object reference) { + // TODO Auto-generated method stub + return 0; + } + +} \ No newline at end of file diff --git a/dependencymanager/release/.classpath b/dependencymanager/release/.classpath new file mode 100644 index 00000000000..7a6fc254361 --- /dev/null +++ b/dependencymanager/release/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dependencymanager/release/.gitignore b/dependencymanager/release/.gitignore new file mode 100644 index 00000000000..90dde36e4ac --- /dev/null +++ b/dependencymanager/release/.gitignore @@ -0,0 +1,3 @@ +/bin/ +/bin_test/ +/generated/ diff --git a/dependencymanager/release/.project b/dependencymanager/release/.project new file mode 100644 index 00000000000..46fcd83084a --- /dev/null +++ b/dependencymanager/release/.project @@ -0,0 +1,23 @@ + + + release + + + + + + org.eclipse.jdt.core.javabuilder + + + + + bndtools.core.bndbuilder + + + + + + org.eclipse.jdt.core.javanature + bndtools.core.bndnature + + diff --git a/dependencymanager/release/README.release b/dependencymanager/release/README.release new file mode 100644 index 00000000000..2545e810abb --- /dev/null +++ b/dependencymanager/release/README.release @@ -0,0 +1,128 @@ + + + +Apache Felix Dependency Manager Release Guide +This document describes how to do a source release. It is based on the Release FAQ [1] + +Prerequisites +============= + +To create a release you must: + +* Have a Jdk8 (at least version 1.8.0_71) installed on your system; +* Have Subversion installed on your system; +* Have gpg installed on your system; +* Have a public key added to the keys file, and committed to [2] +* If you are using an http proxy, configure the following; for example: + + export GRADLE_OPTS="-Dhttps.proxyHost=www.somehost.org -Dhttps.proxyPort=8080" + export http_proxy=www.somehost.org:8080 + export https_proxy=www.somehost.org:8080 + +Before you can start staging a release candidate, you must: + +* make sure there are no dependencies on snapshots/unreleased versions; +* increment the ext.dmRelease parameter in release/build.gradle, if not already done, and commit. +* check if workspace and module changelogs are up-to-date. +* in the changelog files, also make sure that all bundles which are part of the release are all referenced (bundle names + versions). +* release the bundles that are part of the dm binary distribution. +* create a tagged version of the sources in preparation of the release candidate. + +Create a tagged version +======================= + +Creating a tagged version of the sources can be done directly through svn (replace r by the actual release number, like "r1"): + +svn copy https://svn.apache.org/repos/asf/felix/trunk/dependencymanager https://svn.apache.org/repos/asf/felix/releases/org.apache.felix.dependencymanager-r -m "Release of Apache Felix Dependency Manager r" + +Staging a release candidate +=========================== + +Staging a release starts by checking out a tagged version of the sources (replace r by the actual release number, like "r1"): + +svn co https://svn.apache.org/repos/asf/felix/releases/org.apache.felix.dependencymanager-r + +The next step is to build/test the software and create the release/staging/ directory (where the source/jars will be packaged): +(replace r by the actual release number, like "r1") + +Use a Java8 JDK + +$ cd org.apache.felix.dependencymanager-r +$ ./gradlew rat +$ ./gradlew org.apache.felix.dependencymanager.annotation:jar +$ ./gradlew jar +$ ./gradlew test +$ ./gradlew check +$ ./gradlew release --no-daemon + +The last command (gradlew release) will create the binary release artifacts in cnf/releaserepo/ directory. + +- create the staging: + +$ ./gradlew makeStaging --no-daemon + +The above will create ./release/staging/ directory. Then you sign archives by invoking the following task: + +$ ./gradlew signStaging --no-daemon + +(you will be asked to enter your PGP key password; don't forget the --no-daemon option, else stdin won't be read) + +You can upload the archives and the signatures to our development area, which we use to stage this release candidate. This development area can be found at +https://dist.apache.org/repos/dist/dev/felix and adding files to it can be done using "svnpubsub" which is taken care of by the following target: + +$ ./gradlew commitToStaging --no-daemon + +Voting on the release +===================== + +Start a vote on the dev@felix.apache.org list, for example (be sure to replace r with the correct release number, like "r1"): + +>>> +To: "Felix Developers List" +Subject: [VOTE] Release of Apache Felix Dependency Manager release r + +Hi, + +We solved N issues in this release: +http://issues.apache.org/jira/... + +There are still some outstanding issues: +http://issues.apache.org/jira/... + +Staging repository: +https://dist.apache.org/repos/dist/dev/felix/apache-felix-dependencymanager-r/ + +You can use this UNIX script to download the release and verify the signatures: +http://svn.apache.org/repos/asf/felix/trunk/dependencymanager/release/check_staged_release.sh + +Usage: +sh check_staged_release.sh r /tmp/felix-staging + +This script, unlike the original Felix check_stage_release.sh, will download staging from https://dist.apache.org/repos/dist/dev/felix instead of +http://repository.apache.org/content/repositories. + + +Please vote to approve this release: + +[ ] +1 Approve the release +[ ] -1 Veto the release (please provide specific comments) + +This vote will be open for 72 hours. +<<< + +Promoting the release: +===================== + +Move the artifacts from the development area to the final release location at +https://dist.apache.org/repos/dist/release/felix by invoking the following target: + +$ ./gradlew promoteToRelease --no-daemon + +Cancelling the release +====================== + +$ ./gradlew deleteFromStaging --no-daemon + + +[1] http://www.apache.org/dev/release.html +[2] http://www.apache.org/dist/felix/KEYS diff --git a/dependencymanager/release/bnd.bnd b/dependencymanager/release/bnd.bnd new file mode 100644 index 00000000000..8cd826073c9 --- /dev/null +++ b/dependencymanager/release/bnd.bnd @@ -0,0 +1,27 @@ +# +# 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. +# +-nobundles: +-dependson \ + org.apache.felix.dependencymanager,\ + org.apache.felix.dependencymanager.shell,\ + org.apache.felix.dependencymanager.annotation,\ + org.apache.felix.dependencymanager.runtime + + +# we do not release this project in binary distribution. +-releaserepo: +-baseline: diff --git a/dependencymanager/release/build.gradle b/dependencymanager/release/build.gradle new file mode 100644 index 00000000000..d168524f2f9 --- /dev/null +++ b/dependencymanager/release/build.gradle @@ -0,0 +1,285 @@ +/* + * 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. + */ + +/** + * Gradle script used to perform DM releases (really similar to Apache ACE build.xml) + */ +import aQute.bnd.build.Workspace + +buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath bnd_plugin + } +} + +// Our release number, which has to be monotonically incremented each time we make a new release. +ext.dmRelease = "r16" + +// Our Apache svn Staging repo +ext.svnStagingPath = "https://dist.apache.org/repos/dist/dev/felix" + +// Our Apache svn Release repo +ext.svnReleasePath = "https://dist.apache.org/repos/dist/release/felix" + +apply plugin: 'java' +apply from: file("rat.gradle") + +// Add bnd as a build dependency +buildscript { + dependencies { + classpath files('cnf/gradle/biz.aQute.bnd.gradle.jar') + } +} + +// Configure RAT plugin to ignore some files +rat { + excludes = [ + 'rat-report.xml', + '**/.git/**', + '**/.gradle/**', + '**/.project', + '**/.settings/**', + '**/*.iml', + '**/*.iws', + '**/*.ipr', + '**/.classpath', + 'cnf/**', + 'gradle/wrapper/**', + 'release/**', + 'gradlew', + 'README', + '**/DEPENDENCIES', + '**/README', + '**/.gitignore', + '**/generated/**', + 'doc/**', + '**/packageinfo', + '**/*.txt', + 'docs/**', + '.metadata/**' + ] +} + +// Setup the workspace +Workspace workspace +workspace = Workspace.getWorkspace(".") + +task makeStaging { + doLast { + description = 'Packages the source and binary distributions.' + + // Package source distributions. + logger.lifecycle(" Packaging source distributions.") + def topdir="org.apache.felix.dependencymanager-" + dmRelease + ant.zip(destfile: "staging/"+topdir+'-src.zip') { + zipfileset(dir: '../cnf', prefix: topdir+"-src/cnf", includes: ".project,.classpath,src/**,*.bnd,*.mvn,ext/**") + zipfileset(dir: '..', prefix: topdir+"-src", includes: '*.gradle,*.properties') + zipfileset(dir: 'resources/src', prefix: topdir+"-src", includes: '*') + ant.zipfileset(dir: '..', prefix: topdir+"-src", includes: 'gradlew') + ant.zipfileset(dir: '..', prefix: topdir+"-src", includes: 'changelog.txt') + ant.zipfileset(dir: '../.gradle-wrapper', prefix: topdir+"-src/.gradle-wrapper", includes: 'gradle-wrapper.properties') + new File('.').eachFile { + if(new File(it, 'bnd.bnd').exists()) { + def bndProject = workspace.getProject(it.name) + if (! bndProject.isNoBundles() && ! bndProject.getName().endsWith(".benchmark")) { + zipfileset(dir: "../${bndProject.name}", prefix: topdir+"-src/${bndProject.name}", + includes: "*.gradle,.project,.classpath,.settings/**,src/**,test/**,*.bnd,*.bndrun,run-*/conf/**,resources/**,README*,changelog.txt") + } + } + } + } + + // Package binaries as a simple collection of bundles. We use same license files as for src distrib. + logger.lifecycle(" Packaging binary distribution.") + + // First, get list of latest released bundles available from our Release repository + def released = [] + def releaseRepo = workspace.getRepository("Release") + def bundles=releaseRepo.list(null) + bundles.each { + def sortedVersions = releaseRepo.versions(it) + def latestVersion = sortedVersions.last() + def latestBundle = releaseRepo.get(it, latestVersion, null) + released << latestBundle + } + + // Before adding all latest released bundles in the binary distribution, check if they contain proper license files. + logger.lifecycle(" Checking META-INF mandatory files.") + released.each { jarfile -> + if (jarfile.isFile()) { + new ByteArrayOutputStream().withStream { os -> + def result = exec { + executable = 'jar' + args = ['tf', jarfile, "META-INF"] + standardOutput = os + } + def files = os.toString().split("\n") + def mandatory = ["META-INF/LICENSE", "META-INF/NOTICE", "META-INF/DEPENDENCIES", "META-INF/changelog.txt"] + mandatory.each { resource -> + if (! files.contains(resource)) { + throw new GradleException("Missing mandatory license files in " + jarfile) + } + } + } + } + } + + // Now, add all the latest released bundles in the binary distribution + ant.zip(destfile: "staging/"+topdir+"-bin.zip") { + // include workspace changelog + zipfileset(dir: '..', prefix: topdir+"-bin/", includes: "changelog.txt") + + // simply include all released bundle. + released.each { + file=it + ant.mappedresources() { + ant.filelist(files: file) + ant.chainedmapper() { + ant.flattenmapper() + ant.globmapper(from: '*', to: topdir+'-bin/*') + } + } + } + ant.mappedresources() { + ant.fileset(dir: 'resources/bin', includes: '*') + ant.chainedmapper() { + ant.flattenmapper() + ant.globmapper(from: '*', to: topdir+'-bin/*') + } + } + } + } +} + + + + +// Sign staging directory (you must use "--no-daemon" gradle option) +task signStaging { + doLast { + ant.input(message: 'Enter PGP password:', addproperty: 'pass') + + description = 'Signs the local staging distribution.' + fileTree("staging").visit { FileVisitDetails details -> + logger.lifecycle(" Signing " + details.file.path) + ant.exec(executable: 'gpg', dir: 'staging') { + ant.arg(line: '--batch') + ant.arg(line: '--passphrase') + ant.arg(line: ant.properties.pass) + ant.arg(line: '--armor') + ant.arg(line: '--output') + ant.arg(line: details.file.name + ".asc") + ant.arg(line: "--detach-sig") + ant.arg(line: details.file.name) + } + + ant.exec(executable: 'gpg', dir: 'staging', output: "staging/" + details.file.name + ".sha1") { + ant.arg(line: '--print-md') + ant.arg(line: 'SHA1') + ant.arg(line: details.file.name) + } + + ant.exec(executable: 'gpg', dir: 'staging', output: "staging/" + details.file.name + ".sha512") { + ant.arg(line: '--print-md') + ant.arg(line: 'SHA512') + ant.arg(line: details.file.name) + } + } + } +} + + + +// Moves the source and binary distributions to staging. +task commitToStaging { + doLast { + description = 'Commits the local staging to the Apache svn staging repository.' + getProject().exec { + commandLine 'svn', + 'import', 'staging', svnStagingPath + "/org.apache.felix.dependencymanager-" + dmRelease + "/", + '-m', "Staging Apache Felix Dependency Manager release " + dmRelease + ".", "--force-interactive" + } + } +} + +// Promotes the staged distributions to release +task promoteToRelease { + doLast { + description = 'Moves the staging repository to the Apache release repository.' + + // Move all artifacts from the staging repo to the release repo + new ByteArrayOutputStream().withStream { os -> + def result = exec { + executable = 'svn' + args = ['list', svnStagingPath+"/org.apache.felix.dependencymanager-" + dmRelease] + standardOutput = os + } + def outputAsString = os.toString() + + outputAsString.split("\n").each { artifact -> + logger.lifecycle(" Moving " + artifact + " to release repository ...") + getProject().exec { + commandLine 'svn', + 'move', svnStagingPath+"/org.apache.felix.dependencymanager-" + dmRelease + "/" + artifact , + svnReleasePath, '-m', "Releasing Apache Felix Dependency Manager release " + dmRelease + ".", "--force-interactive" + } + } + } + + // And remove the toplevel release path from the staging repo + logger.lifecycle(" Removing org.apache.felix.dependencymanager-" + dmRelease + " from staging ...") + getProject().exec { + commandLine 'svn', + 'rm', svnStagingPath+"/org.apache.felix.dependencymanager-" + dmRelease, "-m", + "Releasing Apache Felix Dependency Manager release " + dmRelease + "." + } + } +} + +// Removes the staged distributions from staging +task deleteFromStaging { + doLast { + description = 'Cancels the staged distribution from the Apache staging repository.' + getProject().exec { + commandLine 'svn', + 'delete', svnStagingPath+"/org.apache.felix.dependencymanager-" + dmRelease + "/", + "-m", "Removing Apache Felix Dependency Manager release " + dmRelease + " from staging." + } + } +} + +// Clean staging directory + +clean.doLast { + new File("release/staging").deleteDir() + new File("rat-report.xml").delete() +} + +// Only clean the staging directory +task cleanStaging { + doLast { + description = 'Clean the local staging directory.' + new File("release/staging").deleteDir() + new File("release/staging-copy").deleteDir() + } +} diff --git a/dependencymanager/release/check_staged_release.sh b/dependencymanager/release/check_staged_release.sh new file mode 100755 index 00000000000..042c383d54c --- /dev/null +++ b/dependencymanager/release/check_staged_release.sh @@ -0,0 +1,119 @@ +#!/bin/bash +# +# 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. +# This script verifies the signatures and checksums of a release. +# +# This script can be used to check the signatures and checksums of staged +# Apache Felix Dependency Manager release using gpg. +# Usage: +# +# check_staged_dependencymanager.sh [] +# +# Where: +# represents the staged release version, e.g., 2.0.0; +# represents the location where the release artifacts +# should be stored, defaults to /tmp/felix-staging if +# omitted. + + +version=${1} +tmpDir=${2:-/tmp/felix-staging} + +if [ ! -d "${tmpDir}" ]; then + mkdir "${tmpDir}" +fi + +if [ -z "${version}" -o ! -d "${tmpDir}" ]; then + echo "Usage: check_staged_dependencymanager.sh [temp-directory]" + exit +fi + +checkSig() { + sigFile="$1.asc" + if [ ! -f $sigFile ]; then + echo "$sigFile is missing!!!" + exit 1 + fi + + gpg --verify $sigFile 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then echo "OK"; else echo "BAD!!!"; fi +} + +checkSum() { + archive=$1 + sumFile=$2 + alg=$3 + if [ ! -f $sumFile ]; then + echo "$sumFile is missing!!!" + exit 1 + fi + + orig=`cat $sumFile | sed 's/.*: *//' | tr -d ' \t\n\r'` + actual=`gpg --print-md $alg $archive | sed 's/.*: *//' | tr -d ' \t\n\r'` + if [ "$orig" = "$actual" ]; then echo "OK"; else echo "BAD!!!"; fi +} + +KEYS_URL="http://www.apache.org/dist/felix/KEYS" +REL_URL="https://dist.apache.org/repos/dist/dev/felix/org.apache.felix.dependencymanager-${version}/" +PWD=`pwd` + +echo "################################################################################" +echo " IMPORTING KEYS " +echo "################################################################################" +if [ ! -e "${tmpDir}/KEYS" ]; then + wget --no-check-certificate -P "${tmpDir}" $KEYS_URL +fi +gpg --import "${tmpDir}/KEYS" + +if [ ! -e "${tmpDir}/org.apache.felix.dependencymanager-${version}" ] +then + echo "################################################################################" + echo " DOWNLOAD STAGED REPOSITORY " + echo "################################################################################" + + wget \ + -e "robots=off" --wait 1 -nv -r -np "--reject=html,html.tmp,txt" "--follow-tags=" \ + -P "${tmpDir}/org.apache.felix.dependencymanager-${version}" -nH "--cut-dirs=5" \ + $REL_URL +else + echo "################################################################################" + echo " USING EXISTING STAGED REPOSITORY " + echo "################################################################################" + echo "${tmpDir}/org.apache.felix.dependencymanager-${version}" +fi + +echo "################################################################################" +echo " CHECK SIGNATURES AND DIGESTS " +echo "################################################################################" + +cd ${tmpDir}/org.apache.felix.dependencymanager-${version} +for f in `find . -type f | grep -v '\.\(asc\|sha1\|sha512\|md5\)$'`; do + echo "checking $f" + + echo -e " ASC: \c" + checkSig $f + echo -e " SHA1: \c" + checkSum $f "$f.sha1" SHA1 + echo -e " SHA512: \c" + checkSum $f "$f.sha512" SHA512 + echo "" +done + +cd $PWD +echo "################################################################################" + diff --git a/dependencymanager/release/rat.gradle b/dependencymanager/release/rat.gradle new file mode 100644 index 00000000000..a15d798d876 --- /dev/null +++ b/dependencymanager/release/rat.gradle @@ -0,0 +1,93 @@ +/* + * 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. + */ + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.internal.project.IsolatedAntBuilder + +apply plugin: RatPlugin + +class RatTask extends DefaultTask { + @Input + List excludes + + def reportPath = '.' + def xmlReport = reportPath + '/rat-report.xml' + + def generateXmlReport(File reportDir) { + def antBuilder = services.get(IsolatedAntBuilder) + def ratClasspath = project.configurations.rat + antBuilder.withClasspath(ratClasspath).execute { + ant.taskdef(resource: 'org/apache/rat/anttasks/antlib.xml') + ant.report(format: 'xml', reportFile: xmlReport) { + fileset(dir: ".") { + patternset { + excludes.each { + exclude(name: it) + } + } + } + } + } + } + + def printUnknownFiles() { + def ratXml = new XmlParser().parse(xmlReport) + ratXml.resource.each { resource -> + if (resource.'license-approval'.@name[0] == "false") { + println('Unknown license: ' + resource.@name) + } + } + } + + @TaskAction + def rat() { + File reportDir = new File(reportPath) + if (!reportDir.exists()) { + reportDir.mkdirs() + } + generateXmlReport(reportDir) + printUnknownFiles() + } +} + +class RatPlugin implements Plugin { + void apply(Project project) { + configureDependencies(project) + project.plugins.apply(JavaBasePlugin); + Task ratTask = project.task("rat", + type: RatTask, + group: 'Build', + description: 'Runs Apache Rat checks.') + project.tasks[JavaBasePlugin.CHECK_TASK_NAME].dependsOn ratTask + } + + void configureDependencies(final Project project) { + project.configurations { + rat + } + project.repositories { + mavenCentral() + } + project.dependencies { + rat 'org.apache.rat:apache-rat-tasks:0.8' + } + } +} diff --git a/dependencymanager/release/resources/bin/LICENSE b/dependencymanager/release/resources/bin/LICENSE new file mode 100644 index 00000000000..6b0b1270ff0 --- /dev/null +++ b/dependencymanager/release/resources/bin/LICENSE @@ -0,0 +1,203 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/dependencymanager/release/resources/bin/NOTICE b/dependencymanager/release/resources/bin/NOTICE new file mode 100644 index 00000000000..75032fec8db --- /dev/null +++ b/dependencymanager/release/resources/bin/NOTICE @@ -0,0 +1,11 @@ +Apache Felix Dependency Manager +Copyright 2011-2018 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2015). +Licensed under the Apache License 2.0. diff --git a/dependencymanager/release/resources/bin/README.bin b/dependencymanager/release/resources/bin/README.bin new file mode 100644 index 00000000000..adf8b2c0287 --- /dev/null +++ b/dependencymanager/release/resources/bin/README.bin @@ -0,0 +1,36 @@ +/* + * 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. + */ + +Welcome to Apache Felix Dependency Manager +========================================== + +Apache Felix Dependency Manager is a versatile java API, allowing to declaratively +register, acquire, and manage dynamic OSGi services. + +Getting Started +=============== + +To start using Apache Felix Dependency Manager, please go to our website and read the +getting started guide for users: + + http://felix.apache.org/documentation/subprojects/apache-felix-dependency-manager.html + +You can also find examples from ./org.apache.felix.dependencymanager.samples/README + +Many thanks for using Apache Felix Dependency Manager. + +The Felix Team diff --git a/dependencymanager/release/resources/src/LICENSE b/dependencymanager/release/resources/src/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/dependencymanager/release/resources/src/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/dependencymanager/release/resources/src/NOTICE b/dependencymanager/release/resources/src/NOTICE new file mode 100644 index 00000000000..75032fec8db --- /dev/null +++ b/dependencymanager/release/resources/src/NOTICE @@ -0,0 +1,11 @@ +Apache Felix Dependency Manager +Copyright 2011-2018 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2015). +Licensed under the Apache License 2.0. diff --git a/dependencymanager/release/resources/src/README.src b/dependencymanager/release/resources/src/README.src new file mode 100644 index 00000000000..ce9628e29c8 --- /dev/null +++ b/dependencymanager/release/resources/src/README.src @@ -0,0 +1,94 @@ +/* + * 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. + */ + +Welcome to Apache Felix Dependency Manager +========================================== + +Apache Felix Dependency Manager is a versatile java API, allowing to declaratively +register, acquire, and manage dynamic OSGi services. + +In addition to the original DM API, a new DM-lambda library also allows to define +OSGI components using a fluent, concise, and more type-safe API, based on java8 lambdas. + +Building and testing Apache Felix Dependency Manager +==================================================== + +** Compilation Using gradle: + +- If necessary, configure your https proxy settings, for example: + + export GRADLE_OPTS="-Dhttps.proxyHost=www.somehost.org -Dhttps.proxyPort=8080" + export http_proxy=www.somehost.org:8080 + export https_proxy=www.somehost.org:8080 + +- Install a recent java8 JDK (the software has been built and tested using java version 1.8.0_74) + +- Compile Dependendency Manager annotations bndtools plugin: + +$ ./gradlew --no-daemon org.apache.felix.dependencymanager.annotation:jar + +- Compile all other bundles: + +$ ./gradlew --no-daemon jar + +- run junit tests: + +$ ./gradlew --no-daemon test + +- run integration tests: + +$ ./gradlew --no-daemon check + +** Compilation Using Eclipse: + +- Install latest Eclipse Mars, and configure a JRE for java8: + +* go to Windows -> Preferences -> Java -> Installed JREs +* Then add a java8 JRE of your choice. + +- Install BndTools 3.5.0 (or latest) + +- Open BndTools perspective + +- Import Dependency Manager into Eclipse, and compile everything + +- if it's the first time you import the project into eclipse, it may happen that some modules that requires the +Dependency Manager Annotations bnd plugin don't compile: It's a know issue. To work around, restart eclipse and +rebuild every modules. + +- Click on org.apache.felix.dependencymanager project and run it as "JUnit test". + +- Click on org.apache.felix.dependencymanager.shell and run it as "JUnit test" + +- Click on org.apache.felix.dependencymanager.itest and run it as "Bnd OSGi Test Launcer (Junit)". + +- Click on org.apache.felix.dependencymanager.runtime.itest and run it as ""Bnd OSGi Test Launcer (Junit)". + +Getting Started +=============== + +To start using Apache Felix Dependency Manager, please go to our website and read the +getting started guide for users: + + http://felix.apache.org/documentation/subprojects/apache-felix-dependency-manager.html + +Many examples are also available from the dependency manager samples module. +See ./org.apache.felix.dependencymanager.samples/README.samples + +Many thanks for using Apache Felix Dependency Manager. + +The Felix Team diff --git a/dependencymanager/release/src/.gitignore b/dependencymanager/release/src/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/release/test/.gitignore b/dependencymanager/release/test/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dependencymanager/settings.gradle b/dependencymanager/settings.gradle new file mode 100644 index 00000000000..10960b29ae2 --- /dev/null +++ b/dependencymanager/settings.gradle @@ -0,0 +1,25 @@ +/* + * 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. + */ + +buildscript { + repositories { mavenCentral() } + dependencies { classpath "biz.aQute.bnd:biz.aQute.bnd.gradle:${bnd_version}" } +} + +apply plugin: 'biz.aQute.bnd.workspace' diff --git a/deploymentadmin/autoconf/DEPENDENCIES b/deploymentadmin/autoconf/DEPENDENCIES new file mode 100644 index 00000000000..00e508d843a --- /dev/null +++ b/deploymentadmin/autoconf/DEPENDENCIES @@ -0,0 +1,29 @@ +Apache Felix AutoConf Resource Processor +Copyright 2011-2016 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +This product includes software from http://kxml.sourceforge.net. +Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany. +Licensed under BSD License. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +III. Overall License Summary + +- Apache License 2.0 +- BSD License diff --git a/deploymentadmin/autoconf/LICENSE b/deploymentadmin/autoconf/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/deploymentadmin/autoconf/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/deploymentadmin/autoconf/LICENSE.kxml2 b/deploymentadmin/autoconf/LICENSE.kxml2 new file mode 100644 index 00000000000..1fe595b0392 --- /dev/null +++ b/deploymentadmin/autoconf/LICENSE.kxml2 @@ -0,0 +1,19 @@ +Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/deploymentadmin/autoconf/NOTICE b/deploymentadmin/autoconf/NOTICE new file mode 100644 index 00000000000..3b625ade245 --- /dev/null +++ b/deploymentadmin/autoconf/NOTICE @@ -0,0 +1,15 @@ +Apache Felix AutoConf Resource Processor +Copyright 2011-2016 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +This product includes software from http://kxml.sourceforge.net. +Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany. +Licensed under BSD License. diff --git a/deploymentadmin/autoconf/changelog.txt b/deploymentadmin/autoconf/changelog.txt new file mode 100644 index 00000000000..f10405c0689 --- /dev/null +++ b/deploymentadmin/autoconf/changelog.txt @@ -0,0 +1,26 @@ +Release 0.1.8 +------------- + +FELIX-5169 AutoConf incorrectly references constants from DeploymentAdmin + +Release 0.1.6 +------------- + +FELIX-4912 Upgrade autoconf to Dependency Manager 4 + +Release 0.1.5 +------------- + +FELIX-3355 Autoconf can't find Metatype service +FELIX-4314 Split service registration to solve visibility issue in autoconf + +Release 0.1.4 +------------- + +FELIX-3243 Autoconf does not recognize non-local non-factory OCDs +FELIX-3245 Autoconf handles metatype 1.1 cardinalty wrong +FELIX-3400 Nullpointer in autoconfprocessor for invalid metatype files + + +Initial Release 0.1.0 +--------------------- diff --git a/deploymentadmin/autoconf/pom.xml b/deploymentadmin/autoconf/pom.xml new file mode 100644 index 00000000000..dc4e9fc2c02 --- /dev/null +++ b/deploymentadmin/autoconf/pom.xml @@ -0,0 +1,113 @@ + + + + 4.0.0 + + org.apache.felix + felix-parent + 3 + ../../pom/pom.xml + + + 4.2.0 + + bundle + Apache Felix AutoConf Resource Processor + 0.1.10-SNAPSHOT + org.apache.felix.deployment.rp.autoconf + + + org.osgi + org.osgi.core + ${osgi.version} + + + org.osgi + org.osgi.compendium + ${osgi.version} + + + org.apache.felix + org.apache.felix.dependencymanager + 4.1.1 + + + org.apache.felix + org.apache.felix.deploymentadmin + 0.9.6 + + + org.apache.felix + org.apache.felix.metatype + 1.1.2 + + + + + + . + META-INF + + LICENSE* + NOTICE* + DEPENDENCIES* + *.txt + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.4 + true + + + org.apache.felix.deployment.rp.autoconf + org.apache.felix.deployment.rp.autoconf.Activator + Apache Felix AutoConf Resource Processor + A customizer bundle that publishes a Resource Processor service that processes configuration resources shipped in a Deployment Package. + The Apache Software Foundation + + org.apache.felix.deployment.rp.autoconf, + org.apache.felix.metatype, + org.apache.felix.metatype.internal.l10n, + org.apache.felix.metatype.internal, + org.kxml2.io; -split-package:=merge-first, + org.xmlpull.v1; -split-package:=merge-first, + org.osgi.service.metatype; -split-package:=merge-first + + + org.osgi.service.deploymentadmin.spi; -split-package:=merge-last;version="1.0", + org.osgi.service.metatype; -split-package:=merge-last;version="1.2" + + true + org.osgi.deployment.rp.autoconf + + + + + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/deploymentadmin/autoconf + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/deploymentadmin/autoconf + http://svn.apache.org/viewvc/felix/trunk/deploymentadmin/autoconf + + diff --git a/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/Activator.java b/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/Activator.java new file mode 100644 index 00000000000..0a31cfab07e --- /dev/null +++ b/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/Activator.java @@ -0,0 +1,58 @@ +/* + * 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.deployment.rp.autoconf; + +import java.util.Dictionary; +import java.util.Properties; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.service.deploymentadmin.spi.ResourceProcessor; +import org.osgi.service.log.LogService; +import org.osgi.service.metatype.MetaTypeService; + +/** + * Bundle activator for the AutoConf Resource Processor Customizer bundle + */ +public class Activator extends DependencyActivatorBase { + + public void init(BundleContext context, DependencyManager manager) throws Exception { + Dictionary properties = new Properties(); + properties.put(Constants.SERVICE_PID, "org.osgi.deployment.rp.autoconf"); + + AutoConfResourceProcessor processor = new AutoConfResourceProcessor(); + manager.add(createComponent() + .setInterface(ResourceProcessor.class.getName(), properties) + .setAutoConfig(Component.class, false) + .setImplementation(processor) + .add(createServiceDependency() + .setService(MetaTypeService.class) + .setRequired(false)) + .add(createServiceDependency() + .setService(LogService.class) + .setRequired(false))); + } + + public void destroy(BundleContext context, DependencyManager manager) throws Exception { + // do nothing + } +} \ No newline at end of file diff --git a/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/AttributeDefinitionImpl.java b/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/AttributeDefinitionImpl.java new file mode 100644 index 00000000000..6cef5987163 --- /dev/null +++ b/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/AttributeDefinitionImpl.java @@ -0,0 +1,68 @@ +/* + * 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.deployment.rp.autoconf; + +import org.apache.felix.metatype.AD; +import org.osgi.service.metatype.AttributeDefinition; + +public class AttributeDefinitionImpl implements AttributeDefinition { + + private final AD m_ad; + + public AttributeDefinitionImpl(AD ad) { + m_ad = ad; + } + + public int getCardinality() { + return m_ad.getCardinality(); + } + + public String[] getDefaultValue() { + return m_ad.getDefaultValue(); + } + + public String getDescription() { + return m_ad.getDescription(); + } + + public String getID() { + return m_ad.getID(); + } + + public String getName() { + return m_ad.getName(); + } + + public String[] getOptionLabels() { + return m_ad.getOptionLabels(); + } + + public String[] getOptionValues() { + return m_ad.getOptionValues(); + } + + public int getType() { + return m_ad.getType(); + } + + public String validate(String value) { + return m_ad.validate(value); + } + +} diff --git a/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/AutoConfResource.java b/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/AutoConfResource.java new file mode 100644 index 00000000000..ca7e936b567 --- /dev/null +++ b/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/AutoConfResource.java @@ -0,0 +1,163 @@ +/* + * 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.deployment.rp.autoconf; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.Dictionary; + +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; + +public class AutoConfResource implements Serializable +{ + private static final long serialVersionUID = 1L; + + private final String m_pid; + private final String m_factoryPid; + private final Dictionary m_properties; + private final String m_bundleLoc; + private final boolean m_merge; + private final String m_name; + + private transient Filter m_filter; + private String m_alias = null; + + public AutoConfResource(String name, String pid, String factoryPid, String bundleLocation, boolean merge, Dictionary properties, Filter filter) + { + m_name = name; + m_pid = pid; + m_filter = filter; + m_factoryPid = (factoryPid == null) ? "" : factoryPid; + m_bundleLoc = bundleLocation; + m_merge = merge; + m_properties = properties; + } + + public String getName() + { + return m_name; + } + + public String getPid() + { + return m_pid; + } + + public Filter getFilter() + { + return m_filter; + } + + /** + * Returns empty string if this configuration is not a factory configuration, otherwise the factory + * PID is returned. + * + * @return Empty string if this is not a factory configuration resource, else the factory PID is returned. + */ + public String getFactoryPid() + { + return m_factoryPid; + } + + public Dictionary getProperties() + { + return m_properties; + } + + public String getBundleLocation() + { + return m_bundleLoc; + } + + public boolean isMerge() + { + return m_merge; + } + + public boolean isFactoryConfig() + { + return !(m_factoryPid == null || "".equals(m_factoryPid)); + } + + public void setGeneratedPid(String alias) + { + m_alias = alias; + } + + public String getGeneratedPid() + { + if (m_alias == null) + { + throw new IllegalStateException("Must set an alias first."); + } + return m_alias; + } + + /** + * Determine if the specified AutoConfResource is meant to be used for the same Configuration as this object. + * + * @param resource The AutoConfResource to compare with. + * @return Returns true if the two resources are meant to be used for the same Configuration object, false otherwise. + */ + public boolean equalsTargetConfiguration(AutoConfResource resource) + { + if (isFactoryConfig()) + { + return m_pid.equals(resource.getPid()) && m_factoryPid.equals(resource.getFactoryPid()); + } + else + { + return m_pid.equals(resource.getPid()); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException + { + out.defaultWriteObject(); + if (m_filter != null) + { + out.writeUTF(m_filter.toString()); + } + else + { + out.writeUTF(""); + } + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException + { + in.defaultReadObject(); + String filter = in.readUTF(); + if (!"".equals(filter)) + { + try + { + m_filter = FrameworkUtil.createFilter(filter); + } + catch (InvalidSyntaxException e) + { + throw new IOException("Unable to parse serialized filter: " + e.getMessage()); + } + } + } +} diff --git a/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/AutoConfResourceProcessor.java b/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/AutoConfResourceProcessor.java new file mode 100644 index 00000000000..415f4cc2fea --- /dev/null +++ b/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/AutoConfResourceProcessor.java @@ -0,0 +1,782 @@ +/* + * 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.deployment.rp.autoconf; + +import static org.osgi.service.deploymentadmin.spi.ResourceProcessorException.CODE_OTHER_ERROR; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.apache.felix.metatype.Designate; +import org.apache.felix.metatype.DesignateObject; +import org.apache.felix.metatype.MetaData; +import org.apache.felix.metatype.MetaDataReader; +import org.apache.felix.metatype.OCD; +import org.osgi.framework.Bundle; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.deploymentadmin.spi.DeploymentSession; +import org.osgi.service.deploymentadmin.spi.ResourceProcessor; +import org.osgi.service.deploymentadmin.spi.ResourceProcessorException; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventConstants; +import org.osgi.service.event.EventHandler; +import org.osgi.service.log.LogService; +import org.osgi.service.metatype.MetaTypeInformation; +import org.osgi.service.metatype.MetaTypeService; +import org.osgi.service.metatype.ObjectClassDefinition; + +public class AutoConfResourceProcessor implements ResourceProcessor, EventHandler +{ + public static final String CONFIGURATION_ADMIN_FILTER_ATTRIBUTE = "filter"; + + private static final String LOCATION_PREFIX = "osgi-dp:"; + /** FELIX-5169 - do not reference this constant from the Constants class in DA! */ + private static final String EVENTTOPIC_COMPLETE = "org/osgi/service/deployment/COMPLETE"; + + // dependencies injected by Dependency Manager + private volatile LogService m_log; + private volatile MetaTypeService m_metaService; + private volatile DependencyManager m_dm; + // Locally managed + private Component m_component; + private PersistencyManager m_persistencyManager; + + private final Object m_lock; // protects the members below + private final Map> m_toBeInstalled; + private final Map> m_toBeDeleted; + private final AtomicReference m_sessionRef; + private final List m_configurationAdminTasks; + private final List m_postCommitTasks; + + public AutoConfResourceProcessor() + { + m_lock = new Object(); + m_sessionRef = new AtomicReference(); + m_toBeInstalled = new HashMap>(); + m_toBeDeleted = new HashMap>(); + m_configurationAdminTasks = new ArrayList(); + m_postCommitTasks = new ArrayList(); + } + + /** + * Called by Felix DM for the component created in {@link #commit()}. + */ + public void addConfigurationAdmin(ServiceReference ref, ConfigurationAdmin ca) + { + m_log.log(LogService.LOG_DEBUG, "found configuration admin " + ref); + + List configAdminTasks; + synchronized (m_lock) + { + configAdminTasks = new ArrayList(m_configurationAdminTasks); + } + + for (ConfigurationAdminTask task : configAdminTasks) + { + try + { + Filter filter = task.getFilter(); + if ((filter == null) || (filter != null && filter.match(ref))) + { + task.run(m_persistencyManager, ca); + } + } + catch (Exception e) + { + m_log.log(LogService.LOG_ERROR, "Exception during configuration to " + ca + ". Trying to continue.", e); + } + } + + m_log.log(LogService.LOG_DEBUG, "found configuration admin " + ref + " done"); + } + + public void begin(DeploymentSession session) + { + m_log.log(LogService.LOG_DEBUG, "beginning session " + session); + + synchronized (m_lock) + { + DeploymentSession current = m_sessionRef.get(); + if (current != null) + { + throw new IllegalArgumentException("Trying to begin new deployment session while already in one."); + } + if (session == null) + { + throw new IllegalArgumentException("Trying to begin new deployment session with a null session."); + } + if (!m_toBeInstalled.isEmpty() || !m_toBeDeleted.isEmpty() || !m_configurationAdminTasks.isEmpty() || !m_postCommitTasks.isEmpty() || m_component != null) + { + throw new IllegalStateException("State not reset correctly at start of session."); + } + m_sessionRef.set(session); + } + } + + public void cancel() + { + m_log.log(LogService.LOG_DEBUG, "cancel"); + rollback(); + } + + public void commit() + { + m_log.log(LogService.LOG_DEBUG, "commit"); + + Dictionary properties = new Properties(); + properties.put(EventConstants.EVENT_TOPIC, EVENTTOPIC_COMPLETE); + m_component = m_dm.createComponent() + .setInterface(EventHandler.class.getName(), properties) + .setImplementation(this) + .setCallbacks(null, null, null, null) + .setAutoConfig(Component.class, false) + .add(m_dm.createServiceDependency() + .setService(ConfigurationAdmin.class) + .setCallbacks("addConfigurationAdmin", null) + .setRequired(false) + ); + m_dm.add(m_component); + + m_log.log(LogService.LOG_DEBUG, "commit done"); + } + + public void dropAllResources() throws ResourceProcessorException + { + m_log.log(LogService.LOG_DEBUG, "drop all resources"); + + assertInDeploymentSession("Can not drop all resources without a Deployment Session"); + + for (String name : m_persistencyManager.getResourceNames()) + { + dropped(name); + } + + m_log.log(LogService.LOG_DEBUG, "drop all resources done"); + } + + public void dropped(String name) throws ResourceProcessorException + { + m_log.log(LogService.LOG_DEBUG, "dropped " + name); + + assertInDeploymentSession("Can not drop resource without a Deployment Session"); + + Map> toBeDeleted; + synchronized (m_lock) + { + toBeDeleted = new HashMap>(m_toBeDeleted); + } + + try + { + List resources = m_persistencyManager.load(name); + + if (!toBeDeleted.containsKey(name)) + { + toBeDeleted.put(name, new ArrayList()); + } + toBeDeleted.get(name).addAll(resources); + } + catch (IOException ioe) + { + throw new ResourceProcessorException(CODE_OTHER_ERROR, "Unable to drop resource: " + name, ioe); + } + + synchronized (m_lock) + { + m_toBeDeleted.putAll(toBeDeleted); + } + + m_log.log(LogService.LOG_DEBUG, "dropped " + name + " done"); + } + + public void handleEvent(Event event) + { + // regardless of the outcome, we simply invoke postcommit + postcommit(); + } + + public void postcommit() + { + m_log.log(LogService.LOG_DEBUG, "post commit"); + + List postCommitTasks; + synchronized (m_lock) + { + postCommitTasks = new ArrayList(m_postCommitTasks); + } + + for (PostCommitTask task : postCommitTasks) + { + try + { + task.run(m_persistencyManager); + } + catch (Exception e) + { + m_log.log(LogService.LOG_ERROR, "Exception during post commit wrap-up. Trying to continue.", e); + } + } + + endSession(); + + m_log.log(LogService.LOG_DEBUG, "post commit done"); + } + + public void prepare() throws ResourceProcessorException + { + m_log.log(LogService.LOG_DEBUG, "prepare"); + + assertInDeploymentSession("Can not prepare resource without a Deployment Session"); + + Map> toBeDeleted; + Map> toBeInstalled; + synchronized (m_lock) + { + toBeDeleted = new HashMap>(m_toBeDeleted); + toBeInstalled = new HashMap>(m_toBeInstalled); + } + + List configAdminTasks = new ArrayList(); + List postCommitTasks = new ArrayList(); + + m_log.log(LogService.LOG_DEBUG, "prepare delete"); + // delete dropped resources + for (Map.Entry> entry : toBeDeleted.entrySet()) + { + String name = entry.getKey(); + for (AutoConfResource resource : entry.getValue()) + { + configAdminTasks.add(new DropResourceTask(resource)); + } + postCommitTasks.add(new DeleteResourceTask(name)); + } + + m_log.log(LogService.LOG_DEBUG, "prepare install/update"); + // install new/updated resources + for (Map.Entry> entry : toBeInstalled.entrySet()) + { + String name = entry.getKey(); + + List existingResources = null; + try + { + existingResources = m_persistencyManager.load(name); + } + catch (IOException ioe) + { + throw new ResourceProcessorException(ResourceProcessorException.CODE_PREPARE, "Unable to read existing resources for resource " + name, ioe); + } + + List resources = entry.getValue(); + for (AutoConfResource resource : resources) + { + // When updating existing configurations, make sure that we delete the ones that have become obsolete... + if (existingResources != null) + { + Iterator iter = existingResources.iterator(); + while (iter.hasNext()) + { + AutoConfResource existing = iter.next(); + if (existing.equalsTargetConfiguration(resource)) + { + iter.remove(); + } + } + } + + configAdminTasks.add(new InstallOrUpdateResourceTask(resource)); + } + // remove existing configurations that were not in the new version of the resource + for (AutoConfResource existingResource : existingResources) + { + configAdminTasks.add(new DropResourceTask(existingResource)); + } + + postCommitTasks.add(new StoreResourceTask(name, resources)); + } + + synchronized (m_lock) + { + m_configurationAdminTasks.addAll(configAdminTasks); + m_postCommitTasks.addAll(postCommitTasks); + } + + m_log.log(LogService.LOG_DEBUG, "prepare done"); + } + + public void process(String name, InputStream stream) throws ResourceProcessorException + { + m_log.log(LogService.LOG_DEBUG, "processing " + name); + + // initial validation + assertInDeploymentSession("Can not process resource without a Deployment Session"); + + Map> toBeInstalled; + synchronized (m_lock) + { + toBeInstalled = new HashMap>(m_toBeInstalled); + } + + MetaData data = parseAutoConfResource(stream); + // process resources + Filter filter = getFilter(data); + + // add to session data + if (!toBeInstalled.containsKey(name)) + { + toBeInstalled.put(name, new ArrayList()); + } + + List designates = data.getDesignates(); + if (designates == null || designates.isEmpty()) + { + // if there are no designates, there's nothing to process + m_log.log(LogService.LOG_INFO, "No designates found in the resource, so there's nothing to process."); + return; + } + + Map localOcds = data.getObjectClassDefinitions(); + if (localOcds == null) + { + localOcds = Collections.emptyMap(); + } + + for (Designate designate : designates) + { + // check object + DesignateObject objectDef = designate.getObject(); + if (objectDef == null) + { + throw new ResourceProcessorException(CODE_OTHER_ERROR, "Designate Object child missing or invalid"); + } + + // check attributes + if (objectDef.getAttributes() == null || objectDef.getAttributes().isEmpty()) + { + throw new ResourceProcessorException(CODE_OTHER_ERROR, "Object Attributes child missing or invalid"); + } + + // check ocdRef + String ocdRef = objectDef.getOcdRef(); + if (ocdRef == null || "".equals(ocdRef)) + { + throw new ResourceProcessorException(CODE_OTHER_ERROR, "Object ocdRef attribute missing or invalid"); + } + + // determine OCD + ObjectClassDefinition ocd = null; + OCD localOcd = localOcds.get(ocdRef); + // ask meta type service for matching OCD if no local OCD has been defined + ocd = (localOcd != null) ? new ObjectClassDefinitionImpl(localOcd) : getMetaTypeOCD(data, designate); + if (ocd == null) + { + throw new ResourceProcessorException(CODE_OTHER_ERROR, "No Object Class Definition found with id=" + ocdRef); + } + + // determine configuration data based on the values and their type definition + Dictionary dict = MetaTypeUtil.getProperties(designate, ocd); + if (dict == null) + { + // designate does not match it's definition, but was marked optional, ignore it + continue; + } + + AutoConfResource resource = new AutoConfResource(name, designate.getPid(), designate.getFactoryPid(), designate.getBundleLocation(), designate.isMerge(), dict, filter); + + toBeInstalled.get(name).add(resource); + } + + synchronized (m_lock) + { + m_toBeInstalled.putAll(toBeInstalled); + } + + m_log.log(LogService.LOG_DEBUG, "processing " + name + " done"); + } + + public void rollback() + { + m_log.log(LogService.LOG_DEBUG, "rollback"); + + Map> toBeInstalled; + synchronized (m_lock) + { + toBeInstalled = new HashMap>(m_toBeInstalled); + } + + for (Map.Entry> entry : toBeInstalled.entrySet()) + { + for (AutoConfResource resource : entry.getValue()) + { + String name = resource.getName(); + try + { + dropped(name); + } + catch (ResourceProcessorException e) + { + m_log.log(LogService.LOG_ERROR, "Unable to roll back resource '" + name + "', reason: " + e.getMessage() + ", caused by: " + e.getCause().getMessage()); + } + break; + } + } + + endSession(); + + m_log.log(LogService.LOG_DEBUG, "rollback done"); + } + + /** + * Called by Felix DM when starting this component. + */ + public void start() throws IOException + { + File root = m_dm.getBundleContext().getDataFile(""); + if (root == null) + { + throw new IOException("No file system support"); + } + m_persistencyManager = new PersistencyManager(root); + } + + private void assertInDeploymentSession(String msg) throws ResourceProcessorException + { + synchronized (m_lock) + { + DeploymentSession current = m_sessionRef.get(); + if (current == null) + { + throw new ResourceProcessorException(CODE_OTHER_ERROR, msg); + } + } + } + + private void endSession() + { + if (m_component != null) + { + m_dm.remove(m_component); + m_component = null; + } + synchronized (m_lock) + { + m_toBeInstalled.clear(); + m_toBeDeleted.clear(); + m_postCommitTasks.clear(); + m_configurationAdminTasks.clear(); + m_sessionRef.set(null); + } + } + + private Bundle getBundle(String bundleLocation, boolean isFactory) throws ResourceProcessorException + { + Bundle bundle = null; + if (!isFactory) + { + // singleton configuration, no foreign bundles allowed, use source deployment package to find specified bundle + if (bundleLocation.startsWith(LOCATION_PREFIX)) + { + DeploymentSession session = m_sessionRef.get(); + bundle = session.getSourceDeploymentPackage().getBundle(bundleLocation.substring(LOCATION_PREFIX.length())); + } + } + else + { + // factory configuration, foreign bundles allowed, use bundle context to find the specified bundle + Bundle[] bundles = m_dm.getBundleContext().getBundles(); + for (int i = 0; i < bundles.length; i++) + { + String location = bundles[i].getLocation(); + if (bundleLocation.equals(location)) + { + bundle = bundles[i]; + break; + } + } + } + return bundle; + } + + private Filter getFilter(MetaData data) throws ResourceProcessorException + { + Map optionalAttributes = data.getOptionalAttributes(); + if (optionalAttributes != null) + { + try + { + return FrameworkUtil.createFilter((String) optionalAttributes.get(AutoConfResourceProcessor.CONFIGURATION_ADMIN_FILTER_ATTRIBUTE)); + } + catch (InvalidSyntaxException e) + { + throw new ResourceProcessorException(CODE_OTHER_ERROR, "Unable to create filter!", e); + } + } + return null; + } + + /** + * Determines the object class definition matching the specified designate. + * + * @param data The meta data containing 'local' object class definitions. + * @param designate The designate whose object class definition should be determined. + * @return + * @throws ResourceProcessorException + */ + private ObjectClassDefinition getMetaTypeOCD(MetaData data, Designate designate) throws ResourceProcessorException + { + boolean isFactoryConfig = isFactoryConfig(designate); + + Bundle bundle = getBundle(designate.getBundleLocation(), isFactoryConfig); + if (bundle == null) + { + return null; + } + + MetaTypeInformation mti = m_metaService.getMetaTypeInformation(bundle); + if (mti == null) + { + return null; + } + + String pid = isFactoryConfig ? pid = designate.getFactoryPid() : designate.getPid(); + try + { + ObjectClassDefinition tempOcd = mti.getObjectClassDefinition(pid, null); + // tempOcd will always have a value, if pid was not known IAE will be thrown + String ocdRef = designate.getObject().getOcdRef(); + if (ocdRef.equals(tempOcd.getID())) + { + return tempOcd; + } + } + catch (IllegalArgumentException iae) + { + // let null be returned + } + + return null; + } + + private boolean isFactoryConfig(Designate designate) + { + String factoryPid = designate.getFactoryPid(); + return (factoryPid != null && !"".equals(factoryPid)); + } + + private MetaData parseAutoConfResource(InputStream stream) throws ResourceProcessorException + { + MetaDataReader reader = new MetaDataReader(); + MetaData data = null; + try + { + data = reader.parse(stream); + } + catch (IOException e) + { + throw new ResourceProcessorException(CODE_OTHER_ERROR, "Unable to process resource.", e); + } + if (data == null) + { + throw new ResourceProcessorException(CODE_OTHER_ERROR, "Supplied configuration is not conform the metatype xml specification."); + } + return data; + } +} + +interface ConfigurationAdminTask +{ + public Filter getFilter(); + + public void run(PersistencyManager persistencyManager, ConfigurationAdmin configAdmin) throws Exception; +} + +class DeleteResourceTask implements PostCommitTask +{ + private final String m_name; + + public DeleteResourceTask(String name) + { + m_name = name; + } + + public void run(PersistencyManager manager) throws Exception + { + manager.delete(m_name); + } +} + +class DropResourceTask implements ConfigurationAdminTask +{ + private final AutoConfResource m_resource; + + public DropResourceTask(AutoConfResource resource) + { + m_resource = resource; + } + + public Filter getFilter() + { + return m_resource.getFilter(); + } + + public void run(PersistencyManager persistencyManager, ConfigurationAdmin configAdmin) throws Exception + { + String pid; + if (m_resource.isFactoryConfig()) + { + pid = m_resource.getGeneratedPid(); + } + else + { + pid = m_resource.getPid(); + } + Configuration configuration = configAdmin.getConfiguration(pid, m_resource.getBundleLocation()); + configuration.delete(); + } +} + +class InstallOrUpdateResourceTask implements ConfigurationAdminTask +{ + private final AutoConfResource m_resource; + + public InstallOrUpdateResourceTask(AutoConfResource resource) + { + m_resource = resource; + } + + public Filter getFilter() + { + return m_resource.getFilter(); + } + + public void run(PersistencyManager persistencyManager, ConfigurationAdmin configAdmin) throws Exception + { + String name = m_resource.getName(); + Dictionary properties = m_resource.getProperties(); + String bundleLocation = m_resource.getBundleLocation(); + Configuration configuration = null; + + List existingResources = null; + try + { + existingResources = persistencyManager.load(name); + } + catch (IOException ioe) + { + throw new ResourceProcessorException(ResourceProcessorException.CODE_PREPARE, "Unable to read existing resources for resource " + name, ioe); + } + + // update configuration + if (m_resource.isFactoryConfig()) + { + // check if this is an factory config instance update + for (Iterator i = existingResources.iterator(); i.hasNext();) + { + AutoConfResource existingResource = (AutoConfResource) i.next(); + if (m_resource.equalsTargetConfiguration(existingResource)) + { + // existing instance found + configuration = configAdmin.getConfiguration(existingResource.getGeneratedPid(), bundleLocation); + existingResources.remove(existingResource); + break; + } + } + if (configuration == null) + { + // no existing instance, create new + configuration = configAdmin.createFactoryConfiguration(m_resource.getFactoryPid(), bundleLocation); + } + m_resource.setGeneratedPid(configuration.getPid()); + } + else + { + for (Iterator i = existingResources.iterator(); i.hasNext();) + { + AutoConfResource existingResource = (AutoConfResource) i.next(); + if (m_resource.getPid().equals(existingResource.getPid())) + { + // existing resource found + existingResources.remove(existingResource); + break; + } + } + configuration = configAdmin.getConfiguration(m_resource.getPid(), bundleLocation); + if (!bundleLocation.equals(configuration.getBundleLocation())) + { + // an existing configuration exists that is bound to a different location, which is not allowed + throw new ResourceProcessorException(ResourceProcessorException.CODE_PREPARE, + "Existing configuration was bound to " + configuration.getBundleLocation() + " instead of " + bundleLocation); + } + } + if (m_resource.isMerge()) + { + Dictionary existingProperties = configuration.getProperties(); + if (existingProperties != null) + { + Enumeration keys = existingProperties.keys(); + while (keys.hasMoreElements()) + { + Object key = keys.nextElement(); + properties.put(key, existingProperties.get(key)); + } + } + } + configuration.update(properties); + } +} + +interface PostCommitTask +{ + public void run(PersistencyManager manager) throws Exception; +} + +class StoreResourceTask implements PostCommitTask +{ + private final String m_name; + private final List m_resources; + + public StoreResourceTask(String name, List resources) + { + m_name = name; + m_resources = resources; + } + + public void run(PersistencyManager manager) throws Exception + { + manager.store(m_name, m_resources); + } +} \ No newline at end of file diff --git a/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/MetaTypeUtil.java b/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/MetaTypeUtil.java new file mode 100644 index 00000000000..7abbb91e17c --- /dev/null +++ b/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/MetaTypeUtil.java @@ -0,0 +1,233 @@ +/* + * 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.deployment.rp.autoconf; + +import static org.osgi.service.deploymentadmin.spi.ResourceProcessorException.CODE_OTHER_ERROR; + +import java.util.Arrays; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.Vector; + +import org.apache.felix.metatype.Attribute; +import org.apache.felix.metatype.Designate; +import org.osgi.service.deploymentadmin.spi.ResourceProcessorException; +import org.osgi.service.metatype.AttributeDefinition; +import org.osgi.service.metatype.ObjectClassDefinition; + +/** + * Convenience methods to work with MetaType structures. + */ +public class MetaTypeUtil +{ + private MetaTypeUtil() + { + // Nop + } + + /** + * Determines the actual configuration data based on the specified designate and object class definition + * + * @param designate The designate object containing the values for the properties + * @param ocd The object class definition + * @return A dictionary containing data as described in the designate and ocd objects, or null if the designate does not match it's + * definition and the designate was marked as optional. + * @throws ResourceProcessorException If the designate does not match the ocd and the designate is not marked as optional. + */ + public static Dictionary getProperties(Designate designate, ObjectClassDefinition ocd) throws ResourceProcessorException + { + Dictionary properties = new Hashtable(); + AttributeDefinition[] attributeDefs = ocd.getAttributeDefinitions(ObjectClassDefinition.ALL); + + List attributes = designate.getObject().getAttributes(); + for (Attribute attribute : attributes) + { + String adRef = attribute.getAdRef(); + boolean found = false; + for (int j = 0; j < attributeDefs.length; j++) + { + AttributeDefinition ad = attributeDefs[j]; + if (adRef.equals(ad.getID())) + { + // found attribute definition + Object value = getValue(attribute, ad); + if (value == null) + { + if (designate.isOptional()) + { + properties = null; + break; + } + else + { + throw new ResourceProcessorException(CODE_OTHER_ERROR, "Could not match attribute to it's definition: adref=" + adRef); + } + } + properties.put(adRef, value); + found = true; + break; + } + } + if (!found) + { + if (designate.isOptional()) + { + properties = null; + break; + } + else + { + throw new ResourceProcessorException(CODE_OTHER_ERROR, "Could not find attribute definition: adref=" + adRef); + } + } + } + + return properties; + } + + /** + * Determines the value of an attribute based on an attribute definition + * + * @param attribute The attribute containing value(s) + * @param ad The attribute definition + * @return An Object reflecting what was specified in the attribute and it's definition or null if the value did not match it's definition. + * @throws ResourceProcessorException in case we're unable to parse the value of an attribute. + */ + private static Object getValue(Attribute attribute, AttributeDefinition ad) throws ResourceProcessorException + { + if (attribute == null || ad == null || !attribute.getAdRef().equals(ad.getID())) + { + // wrong attribute or definition + return null; + } + String[] content = attribute.getContent(); + + // verify correct type of the value(s) + int type = ad.getType(); + Object[] typedContent = null; + try + { + for (int i = 0; i < content.length; i++) + { + String value = content[i]; + switch (type) + { + case AttributeDefinition.BOOLEAN: + typedContent = (typedContent == null) ? new Boolean[content.length] : typedContent; + typedContent[i] = Boolean.valueOf(value); + break; + case AttributeDefinition.BYTE: + typedContent = (typedContent == null) ? new Byte[content.length] : typedContent; + typedContent[i] = Byte.valueOf(value); + break; + case AttributeDefinition.CHARACTER: + typedContent = (typedContent == null) ? new Character[content.length] : typedContent; + char[] charArray = value.toCharArray(); + if (charArray.length == 1) + { + typedContent[i] = new Character(charArray[0]); + } + else + { + throw new ResourceProcessorException(CODE_OTHER_ERROR, "Unable to parse value for definition: adref=" + ad.getID()); + } + break; + case AttributeDefinition.DOUBLE: + typedContent = (typedContent == null) ? new Double[content.length] : typedContent; + typedContent[i] = Double.valueOf(value); + break; + case AttributeDefinition.FLOAT: + typedContent = (typedContent == null) ? new Float[content.length] : typedContent; + typedContent[i] = Float.valueOf(value); + break; + case AttributeDefinition.INTEGER: + typedContent = (typedContent == null) ? new Integer[content.length] : typedContent; + typedContent[i] = Integer.valueOf(value); + break; + case AttributeDefinition.LONG: + typedContent = (typedContent == null) ? new Long[content.length] : typedContent; + typedContent[i] = Long.valueOf(value); + break; + case AttributeDefinition.SHORT: + typedContent = (typedContent == null) ? new Short[content.length] : typedContent; + typedContent[i] = Short.valueOf(value); + break; + case AttributeDefinition.STRING: + typedContent = (typedContent == null) ? new String[content.length] : typedContent; + typedContent[i] = value; + break; + default: + // unsupported type + throw new ResourceProcessorException(CODE_OTHER_ERROR, "Unsupported value-type for definition: adref=" + ad.getID()); + } + } + } + catch (NumberFormatException nfe) + { + throw new ResourceProcessorException(CODE_OTHER_ERROR, "Unable to parse value for definition: adref=" + ad.getID()); + } + + // verify cardinality of value(s) + int cardinality = ad.getCardinality(); + Object result = null; + if (cardinality == 0) + { + if (typedContent.length == 1) + { + result = typedContent[0]; + } + else + { + result = null; + } + } + else if (cardinality == Integer.MIN_VALUE) + { + result = new Vector(Arrays.asList(typedContent)); + } + else if (cardinality == Integer.MAX_VALUE) + { + result = typedContent; + } + else if (cardinality < 0) + { + if (typedContent.length <= Math.abs(cardinality)) + { + result = new Vector(Arrays.asList(typedContent)); + } + else + { + result = null; + } + } + else if (cardinality > 0) + { + if (typedContent.length <= cardinality) + { + result = typedContent; + } + else + { + result = null; + } + } + return result; + } +} diff --git a/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/ObjectClassDefinitionImpl.java b/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/ObjectClassDefinitionImpl.java new file mode 100644 index 00000000000..82634c3622e --- /dev/null +++ b/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/ObjectClassDefinitionImpl.java @@ -0,0 +1,81 @@ +/* + * 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.deployment.rp.autoconf; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.apache.felix.metatype.AD; +import org.apache.felix.metatype.OCD; +import org.osgi.service.metatype.AttributeDefinition; +import org.osgi.service.metatype.ObjectClassDefinition; + +public class ObjectClassDefinitionImpl implements ObjectClassDefinition { + + private final OCD m_ocd; + + public ObjectClassDefinitionImpl(OCD ocd) { + m_ocd = ocd; + } + + public AttributeDefinition[] getAttributeDefinitions(int filter) { + if (m_ocd.getAttributeDefinitions() == null) { + return null; + } + if (filter != ObjectClassDefinition.OPTIONAL && filter != ObjectClassDefinition.REQUIRED && filter != ObjectClassDefinition.ALL) { + return null; + } + + List result = new ArrayList(); + for (Iterator i = m_ocd.getAttributeDefinitions().values().iterator(); i.hasNext();) { + AD ad = (AD) i.next(); + if (filter != ObjectClassDefinition.ALL) { + if (ad.isRequired() && filter == ObjectClassDefinition.REQUIRED) { + result.add(new AttributeDefinitionImpl(ad)); + } + else if (!ad.isRequired() && filter == ObjectClassDefinition.OPTIONAL) { + result.add(new AttributeDefinitionImpl(ad)); + } + } else { + result.add(new AttributeDefinitionImpl(ad)); + } + } + + return (AttributeDefinition[]) result.toArray(new AttributeDefinition[result.size()]); + } + + public InputStream getIcon(int size) throws IOException { + return null; + } + + public String getDescription() { + return m_ocd.getDescription(); + } + + public String getID() { + return m_ocd.getID(); + } + + public String getName() { + return m_ocd.getName(); + } +} diff --git a/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/PersistencyManager.java b/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/PersistencyManager.java new file mode 100644 index 00000000000..cd41d3089ed --- /dev/null +++ b/deploymentadmin/autoconf/src/main/java/org/apache/felix/deployment/rp/autoconf/PersistencyManager.java @@ -0,0 +1,183 @@ +/* + * 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.deployment.rp.autoconf; + +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.List; + +public class PersistencyManager +{ + private static final FileFilter FILES_ONLY_FILTER = new FileFilter() + { + public boolean accept(File pathname) + { + return pathname.isFile(); + } + }; + + private final File m_root; + + public PersistencyManager(File root) + { + m_root = root; + } + + /** + * Deletes a resource. + * + * @param name Name of the resource. + * @throws IOException If the resource could not be deleted. + */ + public void delete(String name) throws IOException + { + name = name.replace('/', File.separatorChar); + File target = new File(m_root, name); + if (target.exists() && !target.delete()) + { + throw new IOException("Unable to delete file: " + target.getAbsolutePath()); + } + while (target.getParentFile().list().length == 0 && !target.getParentFile().getAbsolutePath().equals(m_root.getAbsolutePath())) + { + target = target.getParentFile(); + target.delete(); + } + } + + /** + * Returns the names of all persisted resources. + * @return a list of resource names, never null. + */ + public List getResourceNames() + { + List result = new ArrayList(); + + File[] list = m_root.listFiles(FILES_ONLY_FILTER); + if (list != null && list.length > 0) + { + for (File resource : list) + { + result.add(resource.getName()); + } + } + return result; + } + + /** + * Loads a stored resource. + * + * @param name Name of the resource. + * @return List of AutoConfResources representing the specified resource, if the resource is unknown an empty list is returned. + * @throws IOException If the resource could not be properly read. + */ + public List load(String name) throws IOException + { + List resources = new ArrayList(); + name = name.replace('/', File.separatorChar); + File resourcesFile = new File(m_root, name); + if (!resourcesFile.exists()) + { + return resources; + } + + ObjectInputStream in = null; + try + { + in = new ObjectInputStream(new FileInputStream(resourcesFile)); + resources = (List) in.readObject(); + } + catch (FileNotFoundException e) + { + throw new IOException("Resource does not exist: " + name); + } + catch (ClassNotFoundException e) + { + throw new IOException("Unable to recreate persisted object from file: " + name); + } + finally + { + if (in != null) + { + try + { + in.close(); + } + catch (Exception e) + { + // not much we can do + } + } + } + return resources; + } + + /** + * Stores a resource. + * + * @param name Name of the resource. + * @param configs List of AutoConfResources representing the specified resource. + * @throws IOException If the resource could not be stored. + */ + public void store(String name, List configs) throws IOException + { + File targetDir = m_root; + name = name.replace('/', File.separatorChar); + + if (name.startsWith(File.separator)) + { + name = name.substring(1); + } + int lastSeparator = name.lastIndexOf(File.separator); + File target = null; + if (lastSeparator != -1) + { + targetDir = new File(targetDir, name.substring(0, lastSeparator)); + targetDir.mkdirs(); + } + target = new File(targetDir, name.substring(lastSeparator + 1)); + + ObjectOutputStream out = null; + try + { + out = new ObjectOutputStream(new FileOutputStream(target)); + out.writeObject(configs); + } + finally + { + if (out != null) + { + try + { + out.close(); + } + catch (Exception e) + { + // not much we can do + } + } + } + } +} diff --git a/deploymentadmin/autoconf/src/test/java/org/apache/felix/deployment/rp/autoconf/AutoConfResourceProcessorTest.java b/deploymentadmin/autoconf/src/test/java/org/apache/felix/deployment/rp/autoconf/AutoConfResourceProcessorTest.java new file mode 100644 index 00000000000..271520c89cc --- /dev/null +++ b/deploymentadmin/autoconf/src/test/java/org/apache/felix/deployment/rp/autoconf/AutoConfResourceProcessorTest.java @@ -0,0 +1,627 @@ +/* + * 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.deployment.rp.autoconf; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.Dictionary; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; + +import junit.framework.TestCase; + +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.deploymentadmin.DeploymentPackage; +import org.osgi.service.deploymentadmin.spi.DeploymentSession; +import org.osgi.service.deploymentadmin.spi.ResourceProcessorException; +import org.osgi.service.log.LogService; + +public class AutoConfResourceProcessorTest extends TestCase +{ + private static class ConfigurationAdminImpl implements ConfigurationAdmin + { + private final String[] m_expectedPIDs; + private final String[] m_expectedFactoryPIDs; + private final Map m_configs; + + public ConfigurationAdminImpl(String... expectedPIDs) + { + this(expectedPIDs, new String[0]); + } + + public ConfigurationAdminImpl(String[] expectedPIDs, String[] expectedFactoryPIDs) + { + m_expectedPIDs = expectedPIDs; + m_expectedFactoryPIDs = expectedFactoryPIDs; + + m_configs = new LinkedHashMap(); + } + + public Configuration createFactoryConfiguration(String factoryPid) throws IOException + { + return createFactoryConfiguration(factoryPid, null); + } + + public Configuration createFactoryConfiguration(String factoryPid, String location) throws IOException + { + if (!isExpected(m_expectedFactoryPIDs, factoryPid)) + { + throw new IOException("Unexpected factory PID: " + factoryPid); + } + // This should be unique enough for our use cases... + String pid = String.format("pid%d", m_configs.size()); + + ConfigurationImpl config = m_configs.get(pid); + if (config == null) + { + config = new ConfigurationImpl(factoryPid, pid, location); + m_configs.put(pid, config); + } + config.setBundleLocation(location); + return config; + } + + public Configuration getConfiguration(String pid) throws IOException + { + return getConfiguration(pid, null); + } + + public Configuration getConfiguration(String pid, String location) throws IOException + { + if (!isExpected(m_expectedPIDs, pid)) + { + throw new IOException("Unexpected PID: " + pid); + } + + ConfigurationImpl config = m_configs.get(pid); + if (config == null) + { + config = new ConfigurationImpl(null, pid, location); + m_configs.put(pid, config); + } + config.setBundleLocation(location); + return config; + } + + public Configuration[] listConfigurations(String filter) throws IOException, InvalidSyntaxException + { + return null; + } + + private boolean isExpected(String[] expectedPIDs, String actualPID) + { + for (String expectedPID : expectedPIDs) + { + if (actualPID.equals(expectedPID)) + { + return true; + } + } + return false; + } + } + + private static class ConfigurationImpl implements Configuration + { + private final String m_factoryPID; + private final String m_pid; + private String m_bundleLocation; + private Dictionary m_properties; + private boolean m_deleted; + + public ConfigurationImpl(String factoryPid, String pid, String bundleLocation) + { + m_factoryPID = factoryPid; + m_pid = pid; + m_bundleLocation = bundleLocation; + } + + public void delete() throws IOException + { + m_deleted = true; + } + + public String getBundleLocation() + { + return m_bundleLocation; + } + + public String getFactoryPid() + { + return m_factoryPID; + } + + public String getPid() + { + return m_pid; + } + + public Dictionary getProperties() + { + return m_properties; + } + + public void setBundleLocation(String bundleLocation) + { + if (m_bundleLocation != null && !m_bundleLocation.equals(bundleLocation)) + { + throw new RuntimeException("Configuration already bound to location: " + m_bundleLocation + " (trying to set to: " + bundleLocation + ")"); + } + m_bundleLocation = bundleLocation; + } + + public void update() throws IOException + { + } + + public void update(Dictionary properties) throws IOException + { + m_properties = properties; + } + } + + /** Dummy session. */ + private static class DeploymentSessionImpl implements DeploymentSession + { + public File getDataFile(Bundle bundle) + { + return null; + } + + public DeploymentPackage getSourceDeploymentPackage() + { + return null; + } + + public DeploymentPackage getTargetDeploymentPackage() + { + return null; + } + + @Override + public String toString() + { + return "Test DeploymentSession @ 0x" + System.identityHashCode(this); + } + } + + private static class LogServiceImpl implements LogService + { + private static final String[] LEVEL = { "", "[ERROR]", "[WARN ]", "[INFO ]", "[DEBUG]" }; + private Throwable m_exception; + + public void failOnException() throws Throwable + { + if (m_exception != null) + { + throw m_exception; + } + } + + public void log(int level, String message) + { + System.out.println(LEVEL[level] + " - " + message); + } + + public void log(int level, String message, Throwable exception) + { + System.out.println(LEVEL[level] + " - " + message + " - " + exception.getMessage()); + m_exception = exception; + } + + public void log(ServiceReference sr, int level, String message) + { + System.out.println(LEVEL[level] + " - " + message); + } + + public void log(ServiceReference sr, int level, String message, Throwable exception) + { + System.out.println(LEVEL[level] + " - " + message + " - " + exception.getMessage()); + m_exception = exception; + } + } + + private static class ServiceReferenceImpl implements ServiceReference + { + private final Properties m_properties; + + public ServiceReferenceImpl() + { + this(new Properties()); + } + + public ServiceReferenceImpl(Properties properties) + { + m_properties = properties; + } + + public int compareTo(Object reference) + { + return 0; + } + + public Bundle getBundle() + { + return null; + } + + public Object getProperty(String key) + { + return m_properties.get(key); + } + + public String[] getPropertyKeys() + { + return Collections.list(m_properties.keys()).toArray(new String[0]); + } + + public Bundle[] getUsingBundles() + { + return null; + } + + public boolean isAssignableTo(Bundle bundle, String className) + { + return false; + } + + @Override + public String toString() + { + return "Test ConfigAdmin @ 0x" + System.identityHashCode(this); + } + } + + private File m_tempDir; + private LogServiceImpl m_logger; + + /** Go through a simple session, containing two empty configurations. */ + public void testBasicConfigurationSession() throws Throwable + { + AutoConfResourceProcessor p = createAutoConfRP(); + + createNewSession(p); + String config = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"; + p.process("basic", new ByteArrayInputStream(config.getBytes())); + p.prepare(); + p.commit(); + p.addConfigurationAdmin(new ServiceReferenceImpl(), new ConfigurationAdminImpl("simple")); + p.postcommit(); + m_logger.failOnException(); + } + + /** Go through a simple session, containing two empty configurations. */ + public void testFilteredConfigurationSession() throws Throwable + { + AutoConfResourceProcessor p = createAutoConfRP(); + + createNewSession(p); + String config = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"; + p.process("basic", new ByteArrayInputStream(config.getBytes())); + p.prepare(); + p.commit(); + + Properties props = new Properties(); + props.put("id", Integer.valueOf(42)); + + ConfigurationAdminImpl ca1 = new ConfigurationAdminImpl("simple"); + ConfigurationAdminImpl ca2 = new ConfigurationAdminImpl(); + + p.addConfigurationAdmin(new ServiceReferenceImpl(props), ca1); + p.addConfigurationAdmin(new ServiceReferenceImpl(), ca2); + p.postcommit(); + + m_logger.failOnException(); + + assertEquals("test", ca1.m_configs.get("simple").getProperties().get("name")); + assertTrue(ca2.m_configs.isEmpty()); + } + + /** Go through a simple session, containing two empty configurations. */ + public void testMissingMandatoryValueInConfig() throws Throwable + { + AutoConfResourceProcessor p = createAutoConfRP(); + + createNewSession(p); + + String config = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"; + + try + { + p.process("missing-value", new ByteArrayInputStream(config.getBytes())); + fail("Expected ResourceProcessorException for missing value!"); + } + catch (ResourceProcessorException e) + { + // Ok; expected... + assertEquals("Unable to parse value for definition: adref=name", e.getMessage()); + } + } + + /** Make sure the processor does not accept a 'null' session. */ + public void testNullSession() throws Exception + { + AutoConfResourceProcessor p = new AutoConfResourceProcessor(); + try + { + p.begin(null); + fail("Should have gotten an exception when trying to begin with null session."); + } + catch (Exception e) + { + // expected + } + } + + /** Go through a simple session, containing two empty configurations. */ + public void testSimpleInstallAndUninstallSession() throws Throwable + { + AutoConfResourceProcessor p = createAutoConfRP(); + + createNewSession(p); + + p.process("a", new ByteArrayInputStream("".getBytes())); + p.prepare(); + p.commit(); + p.postcommit(); + m_logger.failOnException(); + + createNewSession(p); + + p.dropAllResources(); + p.prepare(); + p.commit(); + p.postcommit(); + m_logger.failOnException(); + } + + /** Go through a simple session, containing two empty configurations. */ + public void testSimpleSession() throws Throwable + { + AutoConfResourceProcessor p = createAutoConfRP(); + + createNewSession(p); + p.process("a", new ByteArrayInputStream("".getBytes())); + p.process("b", new ByteArrayInputStream("".getBytes())); + p.prepare(); + p.commit(); + p.postcommit(); + m_logger.failOnException(); + } + + /** Tests that we can update an existing configuration and properly handling deleted & updated configurations. */ + public void testUpdateConfigurationSession() throws Throwable + { + AutoConfResourceProcessor p = createAutoConfRP(); + + createNewSession(p); + + String config1 = "" + + "" + + " " + + "" + + "" + + " " + + "" + + "" + + " " + + " " + + " " + + " " + + " " + + "" + + "" + + " " + + " " + + " " + + " " + + " " + + "" + + ""; + + ConfigurationAdminImpl ca = new ConfigurationAdminImpl("pid1", "pid2", "pid3"); + + p.process("update", new ByteArrayInputStream(config1.getBytes())); + p.prepare(); + p.commit(); + p.addConfigurationAdmin(new ServiceReferenceImpl(), ca); + p.postcommit(); + m_logger.failOnException(); + + assertEquals(2, ca.m_configs.size()); + assertTrue(ca.m_configs.containsKey("pid1")); + assertFalse(ca.m_configs.get("pid1").m_deleted); + assertEquals("test1", ca.m_configs.get("pid1").getProperties().get("nameA")); + + assertTrue(ca.m_configs.containsKey("pid2")); + assertFalse(ca.m_configs.get("pid2").m_deleted); + assertEquals("test2", ca.m_configs.get("pid2").getProperties().get("nameB")); + + String config2 = "" + + "" + + " " + + "" + + "" + + " " + + "" + + "" + + " " + + " " + + " " + + " " + + " " + + "" + + "" + + " " + + " " + + " " + + " " + + " " + + "" + + ""; + + createNewSession(p); + + p.process("update", new ByteArrayInputStream(config2.getBytes())); + p.prepare(); + p.commit(); + p.addConfigurationAdmin(new ServiceReferenceImpl(), ca); + p.postcommit(); + m_logger.failOnException(); + + assertEquals(3, ca.m_configs.size()); + assertTrue(ca.m_configs.containsKey("pid1")); + assertTrue(ca.m_configs.get("pid1").m_deleted); + assertEquals("test1", ca.m_configs.get("pid1").getProperties().get("nameA")); + + assertTrue(ca.m_configs.containsKey("pid2")); + assertFalse(ca.m_configs.get("pid2").m_deleted); + assertEquals("test4", ca.m_configs.get("pid2").getProperties().get("nameB")); + + assertTrue(ca.m_configs.containsKey("pid3")); + assertFalse(ca.m_configs.get("pid3").m_deleted); + assertEquals("test3", ca.m_configs.get("pid3").getProperties().get("nameC")); + } + + @Override + protected void setUp() throws IOException + { + m_tempDir = File.createTempFile("persistence", "dir"); + m_tempDir.delete(); + m_tempDir.mkdirs(); + + m_logger = new LogServiceImpl(); + } + + @Override + protected void tearDown() throws Exception + { + Utils.removeDirectoryWithContent(m_tempDir); + } + + private AutoConfResourceProcessor createAutoConfRP() + { + AutoConfResourceProcessor p = new AutoConfResourceProcessor(); + Utils.configureObject(p, LogService.class, m_logger); + Utils.configureObject(p, DependencyManager.class, createMockDM()); + Utils.configureObject(p, PersistencyManager.class, new PersistencyManager(m_tempDir)); + return p; + } + + @SuppressWarnings("unused") + private BundleContext createMockBundleContext() + { + return Utils.createMockObjectAdapter(BundleContext.class, new Object() + { + public Filter createFilter(String condition) + { + return Utils.createMockObjectAdapter(Filter.class, new Object() + { + public boolean match(ServiceReference ref) + { + Object id = ref.getProperty("id"); + if (id != null && id.equals(Integer.valueOf(42))) + { + return true; + } + return false; + } + + public void remove(Component service) + { + } + }); + } + }); + } + + @SuppressWarnings("unused") + private Component createMockComponent() + { + return Utils.createMockObjectAdapter(Component.class, new Object() + { + public DependencyManager getDependencyManager() + { + return new DependencyManager(createMockBundleContext()); + } + }); + } + + private DependencyManager createMockDM() + { + return new DependencyManager(createMockBundleContext()) + { + public void remove(Component service) + { + } + }; + } + + private DeploymentSession createNewSession(AutoConfResourceProcessor p) + { + DeploymentSessionImpl s = new DeploymentSessionImpl(); + p.begin(s); + Utils.configureObject(p, Component.class, createMockComponent()); + return s; + } +} diff --git a/deploymentadmin/autoconf/src/test/java/org/apache/felix/deployment/rp/autoconf/DefaultNullObject.java b/deploymentadmin/autoconf/src/test/java/org/apache/felix/deployment/rp/autoconf/DefaultNullObject.java new file mode 100644 index 00000000000..e6b827c3af5 --- /dev/null +++ b/deploymentadmin/autoconf/src/test/java/org/apache/felix/deployment/rp/autoconf/DefaultNullObject.java @@ -0,0 +1,80 @@ +/* + * 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.deployment.rp.autoconf; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; + +/** + * Default null object implementation. Uses a dynamic proxy. Null objects are used + * as placeholders for services that are not available. + * + * @author Felix Project Team + */ +public class DefaultNullObject implements InvocationHandler +{ + private static final Boolean DEFAULT_BOOLEAN = Boolean.FALSE; + private static final Byte DEFAULT_BYTE = new Byte((byte) 0); + private static final Short DEFAULT_SHORT = new Short((short) 0); + private static final Integer DEFAULT_INT = new Integer(0); + private static final Long DEFAULT_LONG = new Long(0); + private static final Float DEFAULT_FLOAT = new Float(0.0f); + private static final Double DEFAULT_DOUBLE = new Double(0.0); + + /** + * Invokes a method on this null object. The method will return a default + * value without doing anything. + */ + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable + { + Class returnType = method.getReturnType(); + if (returnType.equals(Boolean.class) || returnType.equals(Boolean.TYPE)) + { + return DEFAULT_BOOLEAN; + } + else if (returnType.equals(Byte.class) || returnType.equals(Byte.TYPE)) + { + return DEFAULT_BYTE; + } + else if (returnType.equals(Short.class) || returnType.equals(Short.TYPE)) + { + return DEFAULT_SHORT; + } + else if (returnType.equals(Integer.class) || returnType.equals(Integer.TYPE)) + { + return DEFAULT_INT; + } + else if (returnType.equals(Long.class) || returnType.equals(Long.TYPE)) + { + return DEFAULT_LONG; + } + else if (returnType.equals(Float.class) || returnType.equals(Float.TYPE)) + { + return DEFAULT_FLOAT; + } + else if (returnType.equals(Double.class) || returnType.equals(Double.TYPE)) + { + return DEFAULT_DOUBLE; + } + else + { + return null; + } + } +} \ No newline at end of file diff --git a/deploymentadmin/autoconf/src/test/java/org/apache/felix/deployment/rp/autoconf/PersistencyManagerTest.java b/deploymentadmin/autoconf/src/test/java/org/apache/felix/deployment/rp/autoconf/PersistencyManagerTest.java new file mode 100644 index 00000000000..07bfe30700a --- /dev/null +++ b/deploymentadmin/autoconf/src/test/java/org/apache/felix/deployment/rp/autoconf/PersistencyManagerTest.java @@ -0,0 +1,99 @@ +/* + * 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.deployment.rp.autoconf; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Hashtable; + +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; + +import junit.framework.TestCase; + +/** + * Test cases for {@link PersistencyManager}. + */ +public class PersistencyManagerTest extends TestCase +{ + private File m_tempDir; + + public void testHandleNonExistingDirectory() throws Exception + { + PersistencyManager pm = new PersistencyManager(new File("/does/not/exist")); + assertNotNull(pm); + + assertEquals(0, pm.getResourceNames().size()); + } + + public void testHandleEmptyExistingDirectory() throws Exception + { + PersistencyManager pm = new PersistencyManager(m_tempDir); + assertNotNull(pm); + + assertEquals(0, pm.getResourceNames().size()); + } + + public void testLoadNonExistingResource() throws Exception + { + PersistencyManager pm = new PersistencyManager(m_tempDir); + assertEquals(0, pm.load("doesNotExist").size()); + } + + public void testSaveResourceWithoutFilter() throws Exception + { + AutoConfResource res1 = new AutoConfResource("res1", "pid1", null, "osgi-dp:locationA", false, new Hashtable(), null); + AutoConfResource res2 = new AutoConfResource("res2", "pid2", null, "osgi-dp:locationB", false, new Hashtable(), null); + + PersistencyManager pm = new PersistencyManager(m_tempDir); + pm.store("test1", Arrays.asList(res1, res2)); + + assertEquals(2, pm.load("test1").size()); + } + + public void testSaveResourceWithFilter() throws Exception + { + Filter f = FrameworkUtil.createFilter("(name=test)"); + + AutoConfResource res1 = new AutoConfResource("res1", "pid1", null, "osgi-dp:locationA", false, new Hashtable(), f); + AutoConfResource res2 = new AutoConfResource("res2", "pid2", null, "osgi-dp:locationB", false, new Hashtable(), null); + + PersistencyManager pm = new PersistencyManager(m_tempDir); + pm.store("test1", Arrays.asList(res1, res2)); + + assertEquals(2, pm.load("test1").size()); + } + + @Override + protected void setUp() throws IOException + { + m_tempDir = File.createTempFile("persistence", "dir"); + m_tempDir.delete(); + m_tempDir.mkdirs(); + } + + @Override + protected void tearDown() throws Exception + { + Utils.removeDirectoryWithContent(m_tempDir); + } + +} diff --git a/deploymentadmin/autoconf/src/test/java/org/apache/felix/deployment/rp/autoconf/Utils.java b/deploymentadmin/autoconf/src/test/java/org/apache/felix/deployment/rp/autoconf/Utils.java new file mode 100644 index 00000000000..a13d5e79c36 --- /dev/null +++ b/deploymentadmin/autoconf/src/test/java/org/apache/felix/deployment/rp/autoconf/Utils.java @@ -0,0 +1,148 @@ +/* + * 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.deployment.rp.autoconf; + +import java.io.File; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +/** + * Utility class that injects dependencies. Can be used to unit test service implementations. + */ +public class Utils +{ + /** + * Configures an object to use a null object for the specified service interface. + * + * @param object the object + * @param iface the service interface + */ + public static void configureObject(Object object, Class iface) + { + configureObject(object, iface, createNullObject(iface)); + } + + /** + * Creates a null object for a service interface. + * + * @param iface the service interface + * @return a null object + */ + public static T createNullObject(Class iface) + { + return (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class[] { iface }, new DefaultNullObject()); + } + + /** + * Wraps the given handler in an adapter that will try to pass on received invocations to the hander if that has + * an applicable methods else it defaults to a NullObject. + * + * @param iface the service interface + * @param handler the handler to pass invocations to. + * @return an adapter that will try to pass on received invocations to the given handler + */ + public static T createMockObjectAdapter(Class iface, final Object handler) + { + return (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class[] { iface }, new DefaultNullObject() + { + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable + { + try + { + Method bridge = handler.getClass().getMethod(method.getName(), method.getParameterTypes()); + bridge.setAccessible(true); + return bridge.invoke(handler, args); + } + catch (NoSuchMethodException ex) + { + return super.invoke(proxy, method, args); + } + catch (InvocationTargetException ex) + { + throw ex.getCause(); + } + } + }); + } + + /** + * Configures an object to use a specific implementation for the specified service interface. + * + * @param object the object + * @param iface the service interface + * @param instance the implementation + */ + public static void configureObject(Object object, Class iface, Object instance) + { + Class serviceClazz = object.getClass(); + + while (serviceClazz != null) + { + Field[] fields = serviceClazz.getDeclaredFields(); + AccessibleObject.setAccessible(fields, true); + for (int j = 0; j < fields.length; j++) + { + if (fields[j].getType().equals(iface)) + { + try + { + // synchronized makes sure the field is actually written to immediately + synchronized (new Object()) + { + fields[j].set(object, instance); + } + } + catch (Exception e) + { + throw new IllegalStateException("Could not set field " + fields[j].getName() + " on " + object); + } + } + } + serviceClazz = serviceClazz.getSuperclass(); + } + } + + /** + * Remove the given directory and all it's files and subdirectories + * + * @param directory the name of the directory to remove + */ + public static void removeDirectoryWithContent(File directory) + { + if ((directory == null) || !directory.exists()) + { + return; + } + File[] filesAndSubDirs = directory.listFiles(); + for (int i = 0; i < filesAndSubDirs.length; i++) + { + File file = filesAndSubDirs[i]; + if (file.isDirectory()) + { + removeDirectoryWithContent(file); + } + // else just remove the file + file.delete(); + } + directory.delete(); + } +} diff --git a/deploymentadmin/deploymentadmin/DEPENDENCIES b/deploymentadmin/deploymentadmin/DEPENDENCIES new file mode 100644 index 00000000000..06bd39ade61 --- /dev/null +++ b/deploymentadmin/deploymentadmin/DEPENDENCIES @@ -0,0 +1,24 @@ +Apache Felix Deployment Admin +Copyright 2011-2016 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +III. Overall License Summary + +- Apache License 2.0 diff --git a/deploymentadmin/deploymentadmin/LICENSE b/deploymentadmin/deploymentadmin/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/deploymentadmin/deploymentadmin/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/deploymentadmin/deploymentadmin/NOTICE b/deploymentadmin/deploymentadmin/NOTICE new file mode 100644 index 00000000000..0b50d4ea6aa --- /dev/null +++ b/deploymentadmin/deploymentadmin/NOTICE @@ -0,0 +1,11 @@ +Apache Felix Deployment Admin +Copyright 2011-2016 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. diff --git a/deploymentadmin/deploymentadmin/changelog.txt b/deploymentadmin/deploymentadmin/changelog.txt new file mode 100644 index 00000000000..72f74ffc3b5 --- /dev/null +++ b/deploymentadmin/deploymentadmin/changelog.txt @@ -0,0 +1,39 @@ +Release 0.9.8 +------------- + +FELIX-4912 Upgrade autoconf to Dependency Manager 4 +FELIX-4719 SnapshotCommand never properly restores archived data-areas +FELIX-4718 SnapshotCommand creates invalid snapshot archives +FELIX-4491 ResourceProcessors should be optional for processed resources +FELIX-4486 Possible thread leakage in DA +FELIX-4485 Incorrect version of exported API +FELIX-4484 uninstall incorrectly marks a DP as stale when its uninstallation fails +FELIX-518 Wrong processing order of Localization-, Bundle and Processed-Resources +FELIX-3780 Allow using configuration admin in addition to system properties + +Release 0.9.6 +------------- + +FELIX-1835 Installing of deployment package fails if has it's bundles in subdirectory +FELIX-4409 Improve exception messages to be more explicit and helpful +FELIX-4410 Exceptions during rollback are not always properly handled +FELIX-4463 DA does not consistently handle non-unique resources +FELIX-4466 Deployment Admin does not always fire events + +Release 0.9.4 +------------- + +FELIX-3336 Exceptions related to the pipe used in deployment admin +FELIX-3272 Add property to allow foreign resource processors +FELIX-3515 DeploymentAdmin triggers IOException on install +FELIX-1307 Log situation in DeploymentAdmin impl very unclear +FELIX-3270 Deployment admin incorrectly takes snapshots of bundle data areas +FELIX-3526 DeploymentAdmin fails on windows due to unclosed iostreams +FELIX-1828 Mistake in code of the class UpdateCommand +FELIX-1829 Method AbstractDeploymentPackage.getBundle(...) throws NullPointerException +FELIX-3678 org.apache.felix.deploymentadmin imports wrong version of org.osgi.service.deploymentadmin +FELIX-3139 Implement uninstall() for deployment admin. + + +Initial Release 0.9.0 +--------------------- diff --git a/deploymentadmin/deploymentadmin/pom.xml b/deploymentadmin/deploymentadmin/pom.xml new file mode 100644 index 00000000000..a4d31b5bcc8 --- /dev/null +++ b/deploymentadmin/deploymentadmin/pom.xml @@ -0,0 +1,96 @@ + + + + 4.0.0 + + org.apache.felix + felix-parent + 3 + ../../pom/pom.xml + + + 4.2.0 + + Apache Felix Deployment Admin + org.apache.felix + org.apache.felix.deploymentadmin + 0.9.12-SNAPSHOT + bundle + + + org.osgi + org.osgi.core + ${osgi.version} + + + org.osgi + org.osgi.compendium + ${osgi.version} + + + org.apache.felix + org.apache.felix.dependencymanager + 4.1.1 + + + org.mockito + mockito-all + test + + + junit + junit + test + + + + + + . + META-INF + + LICENSE* + NOTICE* + DEPENDENCIES* + *.txt + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.4 + true + + + org.apache.felix.deploymentadmin + org.apache.felix.deploymentadmin.Activator + Apache Felix Deployment Admin + A bundle that implements the Deployment Admin. + The Apache Software Foundation + org.apache.felix.deploymentadmin.* + org.osgi.service.deploymentadmin;version="1.1", + org.osgi.service.deploymentadmin.spi;version="1.0" + org.osgi.service.deploymentadmin;version="[1.1,2.0)", + org.osgi.service.deploymentadmin.spi;version="[1.0,2.0)",* + + + + + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/deploymentadmin/deploymentadmin + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/deploymentadmin/deploymentadmin + http://svn.apache.org/viewvc/felix/trunk/deploymentadmin/deploymentadmin + + diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/AbstractDeploymentPackage.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/AbstractDeploymentPackage.java new file mode 100644 index 00000000000..512381b93ad --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/AbstractDeploymentPackage.java @@ -0,0 +1,516 @@ +/* + * 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.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.jar.Manifest; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.Version; +import org.osgi.service.deploymentadmin.BundleInfo; +import org.osgi.service.deploymentadmin.DeploymentException; +import org.osgi.service.deploymentadmin.DeploymentPackage; +import org.osgi.service.deploymentadmin.spi.ResourceProcessor; +import org.osgi.service.log.LogService; + +/** + * Base class for various types of deployment packages. Indifferent in regard to + * how the deployment package data is obtained, this should be handled by + * extending classes. + */ +public abstract class AbstractDeploymentPackage implements DeploymentPackage, Constants { + /** + * Represents an empty deployment package. + */ + private static final class EmptyDeploymentPackage extends AbstractDeploymentPackage { + private static final String[] STRINGS = new String[] {}; + private static final ResourceInfoImpl[] RESOURCE_INFO_IMPLS = new ResourceInfoImpl[] {}; + private static final BundleInfoImpl[] BUNDLE_INFO_IMPLS = new BundleInfoImpl[] {}; + + public Bundle getBundle(String symbolicName) { + return null; + } + + public BundleInfoImpl[] getBundleInfoImpls() { + return BUNDLE_INFO_IMPLS; + } + + public BundleInfo[] getBundleInfos() { + return BUNDLE_INFO_IMPLS; + } + + public InputStream getBundleStream(String symbolicName) throws IOException { + return null; + } + + public InputStream getCurrentEntryStream() { + throw new UnsupportedOperationException(); + } + + public String getDisplayName() { + return ""; + } + + public String getHeader(String header) { + if (DEPLOYMENTPACKAGE_SYMBOLICMAME.equals(header)) { + return ""; + } + else if (DEPLOYMENTPACKAGE_VERSION.equals(header)) { + return Version.emptyVersion.toString(); + } + else { + return null; + } + } + + public URL getIcon() { + return null; + } + + public String getName() { + return ""; + } + + public AbstractInfo getNextEntry() throws IOException { + throw new UnsupportedOperationException(); + } + + public BundleInfoImpl[] getOrderedBundleInfos() { + return BUNDLE_INFO_IMPLS; + } + + public ResourceInfoImpl[] getOrderedResourceInfos() { + return RESOURCE_INFO_IMPLS; + } + + public String getResourceHeader(String resource, String header) { + return null; + } + + public ResourceInfoImpl[] getResourceInfos() { + return RESOURCE_INFO_IMPLS; + } + + public ServiceReference getResourceProcessor(String resource) { + return null; + } + + public String[] getResources() { + return STRINGS; + } + + public Version getVersion() { + return Version.emptyVersion; + } + + public boolean isStale() { + return true; + } + + public void uninstall() throws DeploymentException { + throw new IllegalStateException("Can not uninstall stale DeploymentPackage"); + } + + public boolean uninstallForced() throws DeploymentException { + throw new IllegalStateException("Can not uninstall stale DeploymentPackage"); + } + } + + protected static final AbstractDeploymentPackage EMPTY_PACKAGE = new EmptyDeploymentPackage(); + + private final BundleContext m_bundleContext; + private final DeploymentAdminImpl m_deploymentAdmin; + private final DeploymentPackageManifest m_manifest; + private final Map m_nameToBundleInfo = new HashMap(); + private final Map m_pathToEntry = new HashMap(); + private final BundleInfoImpl[] m_bundleInfos; + private final ResourceInfoImpl[] m_resourceInfos; + private final String[] m_resourcePaths; + private final boolean m_isFixPackage; + private boolean m_isStale; + + /** + * Creates an instance of this class. + * + * @param manifest The manifest of the deployment package. + * @param bundleContext The bundle context. + * @throws DeploymentException Thrown if the specified manifest does not + * describe a valid deployment package. + */ + public AbstractDeploymentPackage(Manifest manifest, BundleContext bundleContext, DeploymentAdminImpl deploymentAdmin) throws DeploymentException { + m_manifest = new DeploymentPackageManifest(manifest); + m_isFixPackage = m_manifest.getFixPackage() != null; + m_bundleContext = bundleContext; + m_deploymentAdmin = deploymentAdmin; + + List bundleInfos = m_manifest.getBundleInfos(); + m_bundleInfos = (BundleInfoImpl[]) bundleInfos.toArray(new BundleInfoImpl[bundleInfos.size()]); + + for (int i = 0; i < m_bundleInfos.length; i++) { + String bsn = m_bundleInfos[i].getSymbolicName(); + if (m_nameToBundleInfo.put(bsn, m_bundleInfos[i]) != null) { + // FELIX-4463: make sure that the DP is consistent... + throw new DeploymentException(CODE_OTHER_ERROR, "Duplicate bundle present in deployment package: " + bsn); + } + + String path = m_bundleInfos[i].getPath(); + if (m_pathToEntry.put(path, m_bundleInfos[i]) != null) { + // FELIX-4463: make sure that the DP is consistent... + throw new DeploymentException(CODE_OTHER_ERROR, "Non-unique path present in deployment package: " + path); + } + } + + List resourceInfos = m_manifest.getResourceInfos(); + m_resourceInfos = (ResourceInfoImpl[]) resourceInfos.toArray(new ResourceInfoImpl[resourceInfos.size()]); + + for (int i = 0; i < m_resourceInfos.length; i++) { + String path = m_resourceInfos[i].getPath(); + if (m_pathToEntry.put(path, m_resourceInfos[i]) != null) { + // FELIX-4463: make sure that the DP is consistent... + throw new DeploymentException(CODE_OTHER_ERROR, "Non-unique path present in deployment package: " + path); + } + } + m_resourcePaths = (String[]) m_pathToEntry.keySet().toArray(new String[m_pathToEntry.size()]); + } + + /* Constructor only for use by the emptyPackage static variable */ + private AbstractDeploymentPackage() { + m_bundleContext = null; + m_manifest = null; + m_bundleInfos = null; + m_resourceInfos = null; + m_resourcePaths = null; + m_isFixPackage = false; + m_deploymentAdmin = null; + } + + public Bundle getBundle(String symbolicName) { + if (isStale()) { + throw new IllegalStateException("Can not get bundle from stale deployment package."); + } + + BundleInfo bundleInfo = (BundleInfo) m_nameToBundleInfo.get(symbolicName); + if (bundleInfo != null) { + Version version = bundleInfo.getVersion(); + + Bundle[] bundles = m_bundleContext.getBundles(); + for (int i = 0; i < bundles.length; i++) { + if (symbolicName.equals(bundles[i].getSymbolicName()) && version.equals(bundles[i].getVersion())) { + return bundles[i]; + } + } + } + return null; + } + + /** + * Determines the info about a bundle resource based on the bundle symbolic + * name. + * + * @param symbolicName String containing a bundle symbolic name + * @return BundleInfoImpl for the bundle identified by the + * specified symbolic name or null if the symbolic name is unknown + */ + public BundleInfoImpl getBundleInfoByName(String symbolicName) { + return (BundleInfoImpl) m_nameToBundleInfo.get(symbolicName); + } + + /** + * Determines the info about a bundle based on it's path/resource-id. + * + * @param path String containing a bundle path + * @return BundleInfoImpl for the bundle resource identified by + * the specified path or null if the path is unknown or does not + * describe a bundle resource + */ + public BundleInfoImpl getBundleInfoByPath(String path) { + AbstractInfo info = (AbstractInfo) m_pathToEntry.get(path); + if (info instanceof BundleInfoImpl) { + return (BundleInfoImpl) info; + } + return null; + } + + /** + * Returns the bundles of this deployment package as an array of + * BundleInfoImpl objects. + * + * @return Array containing BundleInfoImpl objects for each + * bundle this deployment package. + */ + public BundleInfoImpl[] getBundleInfoImpls() { + return (BundleInfoImpl[]) m_bundleInfos.clone(); + } + + public BundleInfo[] getBundleInfos() { + return (BundleInfo[]) m_bundleInfos.clone(); + } + + /** + * Determines the data stream of a bundle resource based on the bundle + * symbolic name + * + * @param symbolicName Bundle symbolic name + * @return Stream to the bundle identified by the specified symbolic name or + * null if no such bundle exists in this deployment package. + * @throws IOException If the bundle can not be properly offered as an + * inputstream + */ + public abstract InputStream getBundleStream(String symbolicName) throws IOException; + + /** + * Determines the data stream to the current entry of this deployment + * package, use this together with the getNextEntry method. + * + * @return Stream to the current resource in the deployment package (as + * determined by the order in which the deployment package was + * received originally) or null if there is no entry + */ + public abstract InputStream getCurrentEntryStream(); + + public String getDisplayName() { + return getHeader(DEPLOYMENTPACKAGE_NAME); + } + + public String getHeader(String header) { + return m_manifest.getHeader(header); + } + + public URL getIcon() { + String icon = getHeader(DEPLOYMENTPACKAGE_ICON); + if (icon == null) { + return null; + } + else { + try { + // TODO spec states this must be a local resource, but we don't make sure of that yet + return new URL(icon); + } + catch (MalformedURLException e) { + return null; + } + } + } + + public String getName() { + return m_manifest.getSymbolicName(); + } + + /** + * Determines the next resource entry in this deployment package based on + * the order in which the resources appeared when the package was originally + * received. + * + * @return AbstractInfo describing the next resource entry (as + * determined by the order in which the deployment package was + * received originally) or null if there is no next entry + * @throws IOException if the next entry can not be properly determined + */ + public abstract AbstractInfo getNextEntry() throws IOException; + + /** + * Determines the bundles of this deployment package in the order in which + * they were originally received. + * + * @return Array containing BundleInfoImpl objects of the + * bundles in this deployment package, ordered in the way they + * appeared when the deployment package was first received. + */ + public abstract BundleInfoImpl[] getOrderedBundleInfos(); + + /** + * Determines the resources of this deployment package in the order in which + * they were originally received. + * + * @return Array containing ResourceInfoImpl objects of all + * processed resources in this deployment package, ordered in the + * way they appeared when the deployment package was first received + */ + public abstract ResourceInfoImpl[] getOrderedResourceInfos(); + + public String getResourceHeader(String resource, String header) { + AbstractInfo info = (AbstractInfo) m_pathToEntry.get(resource); + if (info != null) { + return info.getHeader(header); + } + return null; + } + + /** + * Determines the info about a processed resource based on it's + * path/resource-id. + * + * @param path String containing a (processed) resource path + * @return ResourceInfoImpl for the resource identified by the + * specified path or null if the path is unknown or does not + * describe a processed resource + */ + public ResourceInfoImpl getResourceInfoByPath(String path) { + AbstractInfo info = (AbstractInfo) m_pathToEntry.get(path); + if (info instanceof ResourceInfoImpl) { + return (ResourceInfoImpl) info; + } + return null; + } + + /** + * Returns the processed resources of this deployment package as an array of + * ResourceInfoImpl objects. + * + * @return Array containing ResourceInfoImpl objects for each + * processed resource of this deployment package. + */ + public ResourceInfoImpl[] getResourceInfos() { + return (ResourceInfoImpl[]) m_resourceInfos.clone(); + } + + public ServiceReference getResourceProcessor(String resource) { + if (isStale()) { + throw new IllegalStateException("Can not get bundle from stale deployment package."); + } + AbstractInfo info = (AbstractInfo) m_pathToEntry.get(resource); + if (info instanceof ResourceInfoImpl) { + String processor = ((ResourceInfoImpl) info).getResourceProcessor(); + if (processor != null) { + try { + ServiceReference[] services = m_bundleContext.getServiceReferences(ResourceProcessor.class.getName(), "(" + SERVICE_PID + "=" + processor + ")"); + if (services != null && services.length > 0) { + return services[0]; + } + else { + return null; + } + } + catch (InvalidSyntaxException e) { + m_deploymentAdmin.getLog().log(LogService.LOG_WARNING, "Invalid resource processor name: " + processor, e); + return null; + } + } + } + return null; + } + + public String[] getResources() { + return (String[]) m_resourcePaths.clone(); + } + + public Version getVersion() { + return m_manifest.getVersion(); + } + + /** + * If this deployment package is a fix package this method determines the + * version range this deployment package can be applied to. + * + * @return VersionRange the fix package can be applied to or + * null if it is not a fix package. + */ + public VersionRange getVersionRange() { + return m_manifest.getFixPackage(); + } + + /** + * Determines whether this deployment package is a fix package. + * + * @return True if this deployment package is a fix package, false + * otherwise. + */ + public boolean isFixPackage() { + return m_isFixPackage; + } + + /** + * @return true if this package is actually an empty package + * used for installing new deployment packages, false + * otherwise. + */ + public boolean isNew() { + return this == EMPTY_PACKAGE; + } + + public boolean isStale() { + return m_isStale; + } + + public void setStale(boolean isStale) { + m_isStale = isStale; + } + + public void uninstall() throws DeploymentException { + if (isStale()) { + throw new IllegalStateException("Deployment package is stale, cannot uninstall!"); + } + + m_deploymentAdmin.uninstallDeploymentPackage(this, false /* force */); + // FELIX-4484: only mark a DP as stale when it is *successfully* uninstalled... + setStale(true); + } + + public boolean uninstallForced() throws DeploymentException { + if (isStale()) { + throw new IllegalStateException("Deployment package is stale, cannot force uninstallation!"); + } + + try { + m_deploymentAdmin.uninstallDeploymentPackage(this, true /* force */); + } + finally { + // FELIX-4484: this is a best-effort method, if it fails, we cannot do anything about it anymore... + setStale(true); + } + return true; + } + + /** + * Determines the info about either a bundle or processed resource based on + * it's path/resource-id. + * + * @param path String containing a resource path (either bundle or processed + * resource) + * @return AbstractInfoImpl for the resource identified by the + * specified path or null if the path is unknown + */ + protected AbstractInfo getAbstractInfoByPath(String path) { + return (AbstractInfo) m_pathToEntry.get(path); + } + + /** + * Returns whether the given name (which is expected to be the name of a + * JarEntry) is a signature file or the JAR index file. + * + * @param name the name of the JAR entry to test, cannot be + * null. + * @return true if the given JAR entry name is a signature file + * or JAR index file, false otherwise. + */ + protected boolean isMetaInfFile(String name) { + name = name.toUpperCase(Locale.US); + return name.startsWith("META-INF/") && (name.endsWith("/INDEX.LIST") || name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA") || name.endsWith(".EC")); + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/AbstractInfo.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/AbstractInfo.java new file mode 100644 index 00000000000..f0b2af11f78 --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/AbstractInfo.java @@ -0,0 +1,139 @@ +/* + * 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.util.BitSet; +import java.util.jar.Attributes; + +import org.osgi.service.deploymentadmin.DeploymentException; + +/** + * Objects of this class represent the meta data for a resource from a deployment package, this + * can be either bundle resources or processed resources. + */ +public class AbstractInfo implements Constants { + + private static final BitSet VALID_RESOURCE_PATH_CHARS; + static { + VALID_RESOURCE_PATH_CHARS = new BitSet(); + for (int i = 'a'; i <= 'z'; i++) { + VALID_RESOURCE_PATH_CHARS.set(i); + } + for (int i = 'A'; i <= 'Z'; i++) { + VALID_RESOURCE_PATH_CHARS.set(i); + } + for (int i = '0'; i <= '9'; i++) { + VALID_RESOURCE_PATH_CHARS.set(i); + } + VALID_RESOURCE_PATH_CHARS.set('.'); + VALID_RESOURCE_PATH_CHARS.set('-'); + VALID_RESOURCE_PATH_CHARS.set('_'); + VALID_RESOURCE_PATH_CHARS.set('/'); + } + + private final String m_path; + private final Attributes m_attributes; + private final boolean m_missing; + + /** + * Create an instance + * + * @param path Resource-id aka path of the resource + * @param attributes Attributes containing the meta data of the resource + * @throws DeploymentException If the specified attributes do not match the correct syntax for a deployment package resource. + */ + public AbstractInfo(String path, Attributes attributes) throws DeploymentException { + verifyEntryName(path); + m_path = path; + m_attributes = attributes; + m_missing = parseBooleanHeader(attributes, DEPLOYMENTPACKAGE_MISSING); + } + + /** + * @return The path of the resource + */ + public String getPath() { + return m_path; + } + + /** + * Return the value of a header for this resource + * + * @param header Name of the header + * @return Value of the header specified by the given header name + */ + public String getHeader(String header) { + return m_attributes.getValue(header); + } + + private void verifyEntryName(String name) throws DeploymentException { + byte[] bytes = name.getBytes(); + boolean delimiterSeen = false; + for (int j = 0; j < bytes.length; j++) { + if (!VALID_RESOURCE_PATH_CHARS.get(bytes[j])) { + throw new DeploymentException(CODE_BAD_HEADER, "Resource ID '" + name + "' contains invalid character(s)"); + } + if (bytes[j] == '/') { + if (delimiterSeen) { + throw new DeploymentException(CODE_BAD_HEADER, "Resource ID '" + name + "' contains multiple consequetive path seperators"); + } + else { + delimiterSeen = true; + } + } + else { + delimiterSeen = false; + } + } + } + + /** + * Determine if a resource is missing or not + * + * @return True if the actual data for this resource is not present, false otherwise + */ + public boolean isMissing() { + return m_missing; + } + + /** + * Parses a header that is allowed to have only boolean values. + * + * @param attributes Set of attributes containing the header + * @param header The header to verify + * @return true if the value of the header was "true", false if the value was "false" + * @throws DeploymentException if the value was not "true" or "false" + */ + protected boolean parseBooleanHeader(Attributes attributes, String header) throws DeploymentException { + String value = attributes.getValue(header); + if (value != null) { + if ("true".equals(value)) { + return true; + } + else if ("false".equals(value)) { + return false; + } + else { + throw new DeploymentException(CODE_BAD_HEADER, "Invalid '" + header + "' header for manifest " + "entry '" + getPath() + "' header, should be either 'true' or 'false' or not present"); + } + } + return false; + } + +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Activator.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Activator.java new file mode 100644 index 00000000000..7013e78bad6 --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Activator.java @@ -0,0 +1,54 @@ +/* + * 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 org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; +import org.osgi.service.deploymentadmin.DeploymentAdmin; +import org.osgi.service.event.EventAdmin; +import org.osgi.service.log.LogService; +import org.osgi.service.packageadmin.PackageAdmin; + +/** + * Bundle activator for the deployment admin bundle + * + * @author Felix Project Team + */ +public class Activator extends DependencyActivatorBase { + + public void init(BundleContext context, DependencyManager manager) throws Exception { + manager.add(createComponent() + .setInterface(DeploymentAdmin.class.getName(), null) + .setImplementation(DeploymentAdminImpl.class) + .add(createServiceDependency() + .setService(PackageAdmin.class) + .setRequired(true)) + .add(createServiceDependency() + .setService(EventAdmin.class) + .setRequired(false)) + .add(createServiceDependency() + .setService(LogService.class) + .setRequired(false))); + } + + public void destroy(BundleContext context, DependencyManager manager) throws Exception { + // do nothing + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/BundleInfoImpl.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/BundleInfoImpl.java new file mode 100644 index 00000000000..bdfa8c119de --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/BundleInfoImpl.java @@ -0,0 +1,112 @@ +/* + * 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.util.StringTokenizer; +import java.util.jar.Attributes; + +import org.osgi.framework.Version; +import org.osgi.service.deploymentadmin.BundleInfo; +import org.osgi.service.deploymentadmin.DeploymentException; + +/** + * Implementation of the BundleInfo interface as defined by the OSGi mobile specification. + */ +public class BundleInfoImpl extends AbstractInfo implements BundleInfo { + + private final Version m_version; + private final String m_symbolicName; + private final boolean m_customizer; + + /** + * Creates an instance of this class. + * + * @param path The path / resource-id of the bundle resource. + * @param attributes Set of attributes describing the bundle resource. + * @throws DeploymentException If the specified attributes do not describe a valid bundle. + */ + public BundleInfoImpl(String path, Attributes attributes) throws DeploymentException { + super(path, attributes); + + String bundleSymbolicName = attributes.getValue(BUNDLE_SYMBOLICNAME); + if (bundleSymbolicName == null) { + throw new DeploymentException(CODE_MISSING_HEADER, "Missing '" + BUNDLE_SYMBOLICNAME + "' header for manifest entry '" + getPath() + "'"); + } + else if (bundleSymbolicName.trim().equals("")) { + throw new DeploymentException(CODE_BAD_HEADER, "Invalid '" + BUNDLE_SYMBOLICNAME + "' header for manifest entry '" + getPath() + "'"); + } + else { + m_symbolicName = parseSymbolicName(bundleSymbolicName); + } + + String version = attributes.getValue(BUNDLE_VERSION); + if (version == null || version == "") { + throw new DeploymentException(CODE_BAD_HEADER, "Invalid '" + BUNDLE_VERSION + "' header for manifest entry '" + getPath() + "'"); + } + try { + m_version = Version.parseVersion(version); + } + catch (IllegalArgumentException e) { + throw new DeploymentException(CODE_BAD_HEADER, "Invalid '" + BUNDLE_VERSION + "' header for manifest entry '" + getPath() + "'"); + } + + m_customizer = parseBooleanHeader(attributes, DEPLOYMENTPACKAGE_CUSTOMIZER); + } + + /** + * Strips parameters from the bundle symbolic name such as "foo;singleton:=true". + * + * @param name full name as found in the manifest of the deployment package + * @return name without parameters + */ + private String parseSymbolicName(String name) { + // note that we don't explicitly check if there are tokens, because that + // check has already been made before we are invoked here + StringTokenizer st = new StringTokenizer(name, ";"); + return st.nextToken(); + } + + public String getSymbolicName() { + return m_symbolicName; + } + + public Version getVersion() { + return m_version; + } + + /** + * Determine whether this bundle resource is a customizer bundle. + * + * @return True if the bundle is a customizer bundle, false otherwise. + */ + public boolean isCustomizer() { + return m_customizer; + } + + /** + * Verify if the specified attributes describe a bundle resource. + * + * @param attributes Attributes describing the resource + * @return true if the attributes describe a bundle resource, false otherwise + */ + public static boolean isBundleResource(Attributes attributes) { + return (attributes.getValue(BUNDLE_SYMBOLICNAME) != null); + } + +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Constants.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Constants.java new file mode 100644 index 00000000000..7a524b3b9ab --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Constants.java @@ -0,0 +1,75 @@ +/* + * 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 org.osgi.service.deploymentadmin.DeploymentException; +import org.osgi.service.deploymentadmin.DeploymentPackage; + +public interface Constants extends org.osgi.framework.Constants { + + // manifest main attribute header constants + String DEPLOYMENTPACKAGE_SYMBOLICMAME = "DeploymentPackage-SymbolicName"; + String DEPLOYMENTPACKAGE_VERSION = "DeploymentPackage-Version"; + String DEPLOYMENTPACKAGE_FIXPACK = "DeploymentPackage-FixPack"; + String DEPLOYMENTPACKAGE_NAME = "DeploymentPackage-Name"; + String DEPLOYMENTPACKAGE_ICON = "DeploymentPackage-Icon"; + + // manifest 'name' section header constants + String RESOURCE_PROCESSOR = "Resource-Processor"; + String DEPLOYMENTPACKAGE_MISSING = "DeploymentPackage-Missing"; + String DEPLOYMENTPACKAGE_CUSTOMIZER = "DeploymentPackage-Customizer"; + + // event topics and properties + String EVENTTOPIC_INSTALL = "org/osgi/service/deployment/INSTALL"; + String EVENTTOPIC_UNINSTALL = "org/osgi/service/deployment/UNINSTALL"; + String EVENTTOPIC_COMPLETE = "org/osgi/service/deployment/COMPLETE"; + + String EVENTPROPERTY_DEPLOYMENTPACKAGE_NAME = DeploymentPackage.EVENT_DEPLOYMENTPACKAGE_NAME; + String EVENTPROPERTY_DEPLOYMENTPACKAGE_READABLENAME = DeploymentPackage.EVENT_DEPLOYMENTPACKAGE_READABLENAME; + String EVENTPROPERTY_DEPLOYMENTPACKAGE_CURRENTVERSION = DeploymentPackage.EVENT_DEPLOYMENTPACKAGE_CURRENTVERSION; + String EVENTPROPERTY_DEPLOYMENTPACKAGE_NEXTVERSION = DeploymentPackage.EVENT_DEPLOYMENTPACKAGE_NEXTVERSION; + String EVENTPROPERTY_SUCCESSFUL = "successful"; + + // miscellaneous constants + String BUNDLE_LOCATION_PREFIX = "osgi-dp:"; + + // inlined constants for convenience & readability + int CODE_CANCELLED = DeploymentException.CODE_CANCELLED; + int CODE_NOT_A_JAR = DeploymentException.CODE_NOT_A_JAR; + int CODE_ORDER_ERROR = DeploymentException.CODE_ORDER_ERROR; + int CODE_MISSING_HEADER = DeploymentException.CODE_MISSING_HEADER; + int CODE_BAD_HEADER = DeploymentException.CODE_BAD_HEADER; + int CODE_MISSING_FIXPACK_TARGET = DeploymentException.CODE_MISSING_FIXPACK_TARGET; + int CODE_MISSING_BUNDLE = DeploymentException.CODE_MISSING_BUNDLE; + int CODE_MISSING_RESOURCE = DeploymentException.CODE_MISSING_RESOURCE; + int CODE_SIGNING_ERROR = DeploymentException.CODE_SIGNING_ERROR; + int CODE_BUNDLE_NAME_ERROR = DeploymentException.CODE_BUNDLE_NAME_ERROR; + int CODE_FOREIGN_CUSTOMIZER = DeploymentException.CODE_FOREIGN_CUSTOMIZER; + int CODE_BUNDLE_SHARING_VIOLATION = DeploymentException.CODE_BUNDLE_SHARING_VIOLATION; + int CODE_RESOURCE_SHARING_VIOLATION = DeploymentException.CODE_RESOURCE_SHARING_VIOLATION; + int CODE_COMMIT_ERROR = DeploymentException.CODE_COMMIT_ERROR; + int CODE_OTHER_ERROR = DeploymentException.CODE_OTHER_ERROR; + int CODE_PROCESSOR_NOT_FOUND = DeploymentException.CODE_PROCESSOR_NOT_FOUND; + int CODE_TIMEOUT = DeploymentException.CODE_TIMEOUT; + + String BUNDLE_SYMBOLICNAME = org.osgi.framework.Constants.BUNDLE_SYMBOLICNAME; + String BUNDLE_VERSION = org.osgi.framework.Constants.BUNDLE_VERSION; + String SERVICE_PID = org.osgi.framework.Constants.SERVICE_PID; + +} \ No newline at end of file diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/ContentCopyingJarInputStream.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/ContentCopyingJarInputStream.java new file mode 100644 index 00000000000..7dff1da06a0 --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/ContentCopyingJarInputStream.java @@ -0,0 +1,151 @@ +/* + * 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 static org.apache.felix.deploymentadmin.Utils.closeSilently; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; +import java.util.zip.GZIPOutputStream; +import java.util.zip.ZipEntry; + +/** + * Provides a custom {@link JarInputStream} that copies all entries read from the original {@link InputStream} to a + * given directory and index file. It does this by tracking thecommon usage of the {@link JarInputStream} API. For each + * entry that is read it streams all read bytes to a separate file compressing it on the fly. The caller does not notice + * anything, although it might be that the {@link #read(byte[], int, int)} is blocked for a little while during the + * writing of the file contents. + *

      + * This implementation replaces the old ExplodingOutputtingInputStream that used + * at least two threads and was difficult to understand and maintain. See FELIX-4486. + *

      + */ +class ContentCopyingJarInputStream extends JarInputStream { + private static final String MANIFEST_FILE = JarFile.MANIFEST_NAME; + + private final File m_contentDir; + + private PrintWriter m_indexFileWriter; + /** Used to copy the contents of the *next* entry. */ + private OutputStream m_entryOS; + + public ContentCopyingJarInputStream(InputStream in, File indexFile, File contentDir) throws IOException { + super(in, true /* verify */); + + m_contentDir = contentDir; + + m_indexFileWriter = new PrintWriter(new FileWriter(indexFile)); + m_entryOS = null; + + // the manifest of the JAR is already read by JarInputStream, so we need to write this one as well... + Manifest manifest = getManifest(); + if (manifest != null) { + copyManifest(manifest); + } + } + + public void close() throws IOException { + closeCopy(); + closeIndex(); + // Do NOT close our parent, as it is the original input stream which is not under our control... + } + + public void closeEntry() throws IOException { + closeCopy(); + super.closeEntry(); + } + + public ZipEntry getNextEntry() throws IOException { + closeCopy(); + + ZipEntry entry = super.getNextEntry(); + if (entry != null) { + File current = new File(m_contentDir, entry.getName()); + if (!entry.isDirectory()) { + addToIndex(entry.getName()); + + m_entryOS = createOutputStream(current); + } + } + + return entry; + } + + public int read(byte[] b, int off, int len) throws IOException { + int r = super.read(b, off, len); + if (m_entryOS != null) { + if (r > 0) { + m_entryOS.write(b, off, r); + } + else { + closeCopy(); + } + } + return r; + } + + private void addToIndex(String name) throws IOException { + m_indexFileWriter.println(name); + m_indexFileWriter.flush(); + } + + private void closeCopy() { + closeSilently(m_entryOS); + m_entryOS = null; + } + + private void closeIndex() { + closeSilently(m_indexFileWriter); + m_indexFileWriter = null; + } + + /** + * Creates a verbatim copy of the manifest, when it is read from the original JAR. + */ + private void copyManifest(Manifest manifest) throws IOException { + addToIndex(MANIFEST_FILE); + + OutputStream os = createOutputStream(new File(m_contentDir, MANIFEST_FILE)); + try { + manifest.write(os); + } + finally { + closeSilently(os); + } + } + + private OutputStream createOutputStream(File file) throws IOException { + File parent = file.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + if (!file.createNewFile()) { + throw new IOException("Attempt to overwrite file: " + file); + } + return new GZIPOutputStream(new FileOutputStream(file)); + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentAdminConfig.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentAdminConfig.java new file mode 100644 index 00000000000..bdafcbb445a --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentAdminConfig.java @@ -0,0 +1,92 @@ +/* + * 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 org.osgi.framework.BundleContext; + +/** + * Provides the configuration options for this DeploymentAdmin implementation. + */ +public class DeploymentAdminConfig { + /** Prefix used for the configuration properties of DA. */ + private static final String PREFIX = "org.apache.felix.deploymentadmin."; + + /** + * Configuration key used to stop only bundles mentioned in a DP instead of all bundles. + * @deprecated incorrect name append the 's' + */ + static final String KEY_STOP_UNAFFECTED_BUNDLE = PREFIX.concat("stopUnaffectedBundle"); + /** Configuration key used to stop only bundles mentioned in a DP instead of all bundles. */ + static final String KEY_STOP_UNAFFECTED_BUNDLES = PREFIX.concat("stopUnaffectedBundles"); + /** Configuration key used to allow usage of customizers outside a DP. */ + static final String KEY_ALLOW_FOREIGN_CUSTOMIZERS = PREFIX.concat("allowForeignCustomizers"); + + static final boolean DEFAULT_STOP_UNAFFECTED_BUNDLES = true; + static final boolean DEFAULT_ALLOW_FOREIGN_CUSTOMIZERS = false; + + private final boolean m_stopUnaffectedBundles; + private final boolean m_allowForeignCustomizers; + + /** + * Creates a new {@link DeploymentAdminConfig} instance with the default settings. + */ + public DeploymentAdminConfig(BundleContext context) { + // Allow the constant to be used in singular or plural form... + String value = getFrameworkProperty(context, KEY_STOP_UNAFFECTED_BUNDLE); + if (value == null) { + value = getFrameworkProperty(context, KEY_STOP_UNAFFECTED_BUNDLES); + } + m_stopUnaffectedBundles = parseBoolean(value, DEFAULT_STOP_UNAFFECTED_BUNDLES); + + value = getFrameworkProperty(context, KEY_ALLOW_FOREIGN_CUSTOMIZERS); + m_allowForeignCustomizers = parseBoolean(value, DEFAULT_ALLOW_FOREIGN_CUSTOMIZERS); + } + + /** + * @return true if foreign customizers (that are not part of a DP) are allowed, false if + * all customizers should be provided by this or an earlier DP. + */ + public boolean isAllowForeignCustomizers() { + return m_allowForeignCustomizers; + } + + /** + * @return true if all bundles should be stopped during the installation of a DP, false if + * only affected bundles should be stopped. + */ + public boolean isStopUnaffectedBundles() { + return m_stopUnaffectedBundles; + } + + private static boolean parseBoolean(String value, boolean dflt) { + if (value == null || "".equals(value.trim())) { + return dflt; + } + return Boolean.parseBoolean(value); + } + + private static String getFrameworkProperty(BundleContext context, String key) { + String prop = context.getProperty(key); + if (prop == null) { + // be lenient wrt the naming... + prop = context.getProperty(key.toLowerCase()); + } + return prop; + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentAdminImpl.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentAdminImpl.java new file mode 100644 index 00000000000..42f2c3292ef --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentAdminImpl.java @@ -0,0 +1,617 @@ +/* + * 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.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, Constants { + 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 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 BundleContext + * + * @return This bundle's BundleContext + */ + public BundleContext getBundleContext() { + return m_context; + } + + 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 LogService. + */ + public LogService getLog() { + return m_log; + } + + /** + * Returns reference to the current package admin defined in the framework. + * + * @return Currently active PackageAdmin. + */ + 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) { + Utils.closeSilently(jarInput); + + 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, new DeploymentAdminConfig(m_context)); + m_session.call(false /* ignoreExceptions */); + } + catch (DeploymentException de) { + throw de; + } + finally { + // We're done at this point with the JAR input stream, close it here as to avoid keeping + // files open unnecessary (otherwise it fails on Windows)... + Utils.closeSilently(jarInput); + } + + 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 { + 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(); + } + + /** + * Uninstalls the given deployment package from the system. + * + * @param dp the deployment package to uninstall, cannot be null; + * @param forced true 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, new DeploymentAdminConfig(m_context)); + 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(); + } + } + + /** + * 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 null. + */ + 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 bundle to return the containing deployment package for, + * cannot be null. + * @return the deployment package containing the given bundle, or null 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 null. + * @return a deployment package, never null. + */ + 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 null. + */ + 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 true if the installation was successful, false 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!"); + } +} \ No newline at end of file diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentPackageManifest.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentPackageManifest.java new file mode 100644 index 00000000000..00ac13bbf60 --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/DeploymentPackageManifest.java @@ -0,0 +1,167 @@ +/* + * 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.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import org.osgi.framework.Version; +import org.osgi.service.deploymentadmin.DeploymentException; + +/** + * This class represents a manifest file used to describe the contents of a deployment package. It can verify the correctness of a + * deployment package manifest and can interpret the various manifest entries and headers the OSGi specification defines. + */ +public class DeploymentPackageManifest implements Constants { + private final Manifest m_manifest; + private final Version m_version; + private final List m_bundleInfos = new ArrayList(); + private final List m_resourceInfos = new ArrayList(); + private final String m_symbolicName; + private final VersionRange m_fixPackage; + + /** + * Creates an instance of this class. + * + * @param manifest The manifest file to be used as deployment manifest + * @throws DeploymentException If the specified manifest is not a valid deployment package manifest file. + */ + public DeploymentPackageManifest(Manifest manifest) throws DeploymentException { + if ((manifest == null) || (manifest.getMainAttributes() == null)) { + throw new DeploymentException(CODE_BAD_HEADER); + } + m_manifest = manifest; + + Attributes mainAttributes = m_manifest.getMainAttributes(); + + // TODO: verify symbolic name and entry-names for valid format/chars + m_symbolicName = getNonNullHeader(mainAttributes.getValue(DEPLOYMENTPACKAGE_SYMBOLICMAME)); + + String version = getNonNullHeader(mainAttributes.getValue(DEPLOYMENTPACKAGE_VERSION)); + try { + m_version = new Version(version); + } + catch (IllegalArgumentException e) { + throw new DeploymentException(CODE_BAD_HEADER); + } + + String fixPackage = mainAttributes.getValue(DEPLOYMENTPACKAGE_FIXPACK); + if (fixPackage != null) { + try { + m_fixPackage = VersionRange.parse(fixPackage); + } + catch (IllegalArgumentException iae) { + throw new DeploymentException(CODE_BAD_HEADER, "Invalid version range for header: " + DEPLOYMENTPACKAGE_FIXPACK); + } + } + else { + m_fixPackage = null; + } + + Map entries = m_manifest.getEntries(); + for (Iterator i = entries.keySet().iterator(); i.hasNext();) { + String key = (String) i.next(); + processEntry(key, (Attributes) entries.get(key), (m_fixPackage != null)); + } + } + + /** + * Determines the value of a header in the main section of the manifest. + * + * @param header Name of the header to retrieve. + * @return Value of the header or null if the header was not defined. + */ + public String getHeader(String header) { + return m_manifest.getMainAttributes().getValue(header); + } + + /** + * Determines the version range a fix package can be applied to + * + * @return A VersionRange describing the versions the fixpackage applies to, null if the package is not a fix package. + */ + public VersionRange getFixPackage() { + return m_fixPackage; + } + + /** + * Determines the symbolic name of the deployment package. + * + * @return String containing the symbolic name of the deployment package. + */ + public String getSymbolicName() { + return m_symbolicName; + } + + /** + * Determines the version of the deployment package. + * + * @return Version of the deployment package. + */ + public Version getVersion() { + return m_version; + } + + /** + * Determines which bundle resources are part of the deployment package, this includes customizer bundles. + * + * @return A List of BundleInfoImpl objects describing the bundle resources of the deployment package. + */ + public List getBundleInfos() { + return m_bundleInfos; + } + + /** + * Determines which processed resources are part of the deployment package. + * + * @return A list of ResourceInfoImpl objects describing the processed resources of the deployment package. + */ + public List getResourceInfos() { + return m_resourceInfos; + } + + private void processEntry(String key, Attributes attributes, boolean isFixPack) throws DeploymentException { + if (BundleInfoImpl.isBundleResource(attributes)) { + BundleInfoImpl bundleInfo = new BundleInfoImpl(key, attributes); + if (bundleInfo.isMissing() && !isFixPack) { + throw new DeploymentException(CODE_BAD_HEADER, "Header '" + DEPLOYMENTPACKAGE_MISSING + "' for manifest entry '" + key + "' may only be 'true' if " + DEPLOYMENTPACKAGE_FIXPACK + + " manifest header is 'true'"); + } + m_bundleInfos.add(bundleInfo); + } + else { + m_resourceInfos.add(new ResourceInfoImpl(key, attributes)); + } + } + + private String getNonNullHeader(String header) throws DeploymentException { + if (header == null) { + throw new DeploymentException(CODE_MISSING_HEADER); + } + else if ("".equals(header.trim())) { + throw new DeploymentException(CODE_BAD_HEADER); + } + return header; + } + +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/FileDeploymentPackage.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/FileDeploymentPackage.java new file mode 100644 index 00000000000..f2a0454a840 --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/FileDeploymentPackage.java @@ -0,0 +1,96 @@ +/* + * 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.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.zip.GZIPInputStream; + +import org.osgi.framework.BundleContext; +import org.osgi.service.deploymentadmin.DeploymentException; + +/** + * Implementation of a DeploymentPackage that is persisted on disk. + */ +public class FileDeploymentPackage extends AbstractDeploymentPackage { + private final List m_index; + private final File m_contentsDir; + + /** + * Creates a new instance of a deployment package stored on disk. + * + * @param index Reference to the index file that contains the order in which all the resources of this deployment package were received + * @param packageDir Reference to the directory in which the index and package contents are stored. + * @param bundleContext The bundle context + * @throws DeploymentException Thrown if the disk contents do not resemble a valid deployment package. + * @throws IOException Thrown if there was a problem reading the resources from disk. + */ + public FileDeploymentPackage(File index, File packageDir, BundleContext bundleContext, DeploymentAdminImpl deploymentAdmin) throws DeploymentException, IOException { + this(Utils.readIndex(index), packageDir, bundleContext, deploymentAdmin); + } + + private FileDeploymentPackage(List index, File packageDir, BundleContext bundleContext, DeploymentAdminImpl deploymentAdmin) throws DeploymentException, IOException { + super(Utils.readManifest(new File(packageDir, (String) index.remove(0))), bundleContext, deploymentAdmin); + m_index = index; + m_contentsDir = packageDir; + } + + public InputStream getBundleStream(String symbolicName) throws IOException { + BundleInfoImpl bundleInfo = getBundleInfoByName(symbolicName); + if (bundleInfo != null) { + return new GZIPInputStream(new FileInputStream(new File(m_contentsDir, bundleInfo.getPath()))); + } + return null; + } + + public InputStream getCurrentEntryStream() { + throw new UnsupportedOperationException("Not implemented for file-based deployment package"); + } + + public AbstractInfo getNextEntry() throws IOException { + throw new UnsupportedOperationException("Not implemented for file-based deployment package"); + } + + public BundleInfoImpl[] getOrderedBundleInfos() { + List result = new ArrayList(); + for (Iterator i = m_index.iterator(); i.hasNext();) { + AbstractInfo bundleInfo = getBundleInfoByPath((String) i.next()); + if (bundleInfo != null) { + result.add(bundleInfo); + } + } + return (BundleInfoImpl[]) result.toArray(new BundleInfoImpl[result.size()]); + } + + public ResourceInfoImpl[] getOrderedResourceInfos() { + List result = new ArrayList(); + for (Iterator i = m_index.iterator(); i.hasNext();) { + AbstractInfo resourceInfo = getResourceInfoByPath((String) i.next()); + if (resourceInfo != null) { + result.add(resourceInfo); + } + } + return (ResourceInfoImpl[]) result.toArray(new ResourceInfoImpl[result.size()]); + } +} \ No newline at end of file diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/NonCloseableStream.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/NonCloseableStream.java new file mode 100644 index 00000000000..7c68997ecbf --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/NonCloseableStream.java @@ -0,0 +1,95 @@ +/* + * 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.IOException; +import java.io.InputStream; + +/** + * Stream that does nothing when close() is invoked, calls to one of the read methods will throw an IOException + * after close() is called. Also, mark/reset is not supported. Deployment Admin can use this class to pass on as an InputStream + * to a resource processor. + * + */ +public class NonCloseableStream extends InputStream { + private final InputStream m_input; + private volatile boolean m_closed; + + public NonCloseableStream(InputStream input) { + m_input = input; + } + + public int available() throws IOException { + return m_input.available(); + } + + public void close() throws IOException { + // stream should not be actually closed, subsequent calls to read methods will throw an exception though + assertOpen(); + m_closed = true; + } + + public boolean equals(Object obj) { + return m_input.equals(obj); + } + + public int hashCode() { + return m_input.hashCode(); + } + + public void mark(int readlimit) { + // do nothing + } + + public boolean markSupported() { + return false; + } + + public int read() throws IOException { + return m_input.read(); + } + + public int read(byte[] b) throws IOException { + assertOpen(); + return m_input.read(b); + } + + public int read(byte[] b, int off, int len) throws IOException { + assertOpen(); + return m_input.read(b, off, len); + } + + public void reset() throws IOException { + throw new IOException("Mark and reset are not available on this type of stream."); + } + + public long skip(long n) throws IOException { + return m_input.skip(n); + } + + public String toString() { + return m_input.toString(); + } + + private void assertOpen() throws IOException { + if (m_closed) { + throw new IOException("Unable to read, stream is closed."); + } + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/ResourceInfoImpl.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/ResourceInfoImpl.java new file mode 100644 index 00000000000..dc43c3800b3 --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/ResourceInfoImpl.java @@ -0,0 +1,66 @@ +/* + * 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.util.jar.Attributes; + +import org.osgi.service.deploymentadmin.DeploymentException; + +/** + * This class represents the meta data of a processed resource as used by the Deployment Admin. + */ +public class ResourceInfoImpl extends AbstractInfo { + + private final String m_resourceProcessor; + + /** + * Create an instance of this class. + * + * @param path String containing the path / resource-id of the processed resource. + * @param attributes Attributes containing the meta data of the resource. + * @throws DeploymentException If the specified attributes do not describe a processed resource. + */ + public ResourceInfoImpl(String path, Attributes attributes) throws DeploymentException { + super(path, attributes); + + String rp = attributes.getValue(RESOURCE_PROCESSOR); + if (rp != null && "".equals(rp.trim())) { + rp = null; + } + + m_resourceProcessor = rp; + } + + /** + * @return true if this resource needs to be processed by a customizer/resource processor, + * false otherwise. + */ + public boolean isProcessedResource() { + return m_resourceProcessor != null; + } + + /** + * Determines the resource processor for this processed resource. + * + * @return String containing the PID of the resource processor that should handle this processed resource. + */ + public String getResourceProcessor() { + return m_resourceProcessor; + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Semaphore.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Semaphore.java new file mode 100644 index 00000000000..a9f20e201d6 --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Semaphore.java @@ -0,0 +1,121 @@ +/* + * 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; + +/** + * A semaphore, that maintains one single permit. An acquire() blocks until a permit is + * available, whilst release() will unblock it. + */ +public class Semaphore { + private boolean m_available; + + /** + * Creates a new semaphore that is available. + */ + public Semaphore() { + m_available = true; + } + + /** + * Creates a new semaphore and allows you to specify if it's available or not. + * + * @param isAvailable should the semaphore be available or not + */ + public Semaphore(boolean isAvailable) { + m_available = isAvailable; + } + + /** + * Acquires the semaphore, or blocks until it's available or the thread is interrupted. + * + * @throws InterruptedException when the thread is interrupted + */ + public void acquire() throws InterruptedException { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + synchronized (this) { + try { + if (!m_available) { + wait(); + } + m_available = false; + } + catch (InterruptedException ie) { + notify(); + throw ie; + } + } + } + + /** + * Tries to acquire the semaphore and waits for the duration of the specified timeout + * until it becomes available. + * + * @param timeout the number of milliseconds to wait + * @return true if the semaphore was acquired, false if it was + * not after waiting for the specified amount of time + * @throws InterruptedException when the thread is interrupted + */ + public boolean tryAcquire(long timeout) throws InterruptedException { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + synchronized (this) { + if (m_available) { + m_available = false; + return true; + } + else if (timeout <= 0) { + return false; + } + else { + long startTime = System.currentTimeMillis(); + try { + while (true) { + wait(timeout); + if (m_available) { + m_available = false; + return true; + } + else { + timeout -= (System.currentTimeMillis() - startTime); + if (timeout <= 0) { + return false; + } + } + } + } + catch (InterruptedException ie) { + notify(); + throw ie; + } + } + } + } + + /** + * Releases the semaphore. If threads were waiting, one of them is + * notified. + */ + public synchronized void release() { + m_available = true; + notify(); + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/StreamDeploymentPackage.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/StreamDeploymentPackage.java new file mode 100644 index 00000000000..4c88d9c722d --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/StreamDeploymentPackage.java @@ -0,0 +1,117 @@ +/* + * 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.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.jar.JarInputStream; +import java.util.zip.ZipEntry; + +import org.osgi.framework.BundleContext; +import org.osgi.service.deploymentadmin.DeploymentException; + +/** + * This class represents a deployment package that is read from a jar stream. + */ +public class StreamDeploymentPackage extends AbstractDeploymentPackage { + private final JarInputStream m_input; + private final List m_names = new ArrayList(); + private boolean m_inMetaInf = true; + + /** + * Creates an instance of this class. + * + * @param input The stream from which the deployment package can be read. + * @param bundleContext The bundle context. + * @throws DeploymentException If it was not possible to read a valid deployment package from the specified stream. + */ + public StreamDeploymentPackage(JarInputStream input, BundleContext bundleContext, DeploymentAdminImpl deploymentAdmin) throws DeploymentException { + super(input.getManifest(), bundleContext, deploymentAdmin); + m_input = input; + } + + public InputStream getBundleStream(String symbolicName) { + throw new UnsupportedOperationException("Not applicable for stream-based deployment package"); + } + + public InputStream getCurrentEntryStream() { + return new NonCloseableStream(m_input); + } + + public AbstractInfo getNextEntry() throws IOException { + String name; + + boolean metaInfFile = true; + do { + ZipEntry nextEntry = m_input.getNextJarEntry(); + if (nextEntry == null) { + return null; + } + name = nextEntry.getName(); + + // FELIX-518: do not try to process signature or localization files... + metaInfFile = isMetaInfFile(name); + if (metaInfFile) { + if (!m_inMetaInf) { + throw new IOException("Unexpected signature file found after manifest files: " + name); + } + else { + continue; + } + } + } + while (metaInfFile); + + m_inMetaInf = false; + m_names.add(name); + + return getAbstractInfoByPath(name); + } + + // This only works for those resources that have been read from the stream already, no guarantees for remainder of + // stream + public BundleInfoImpl[] getOrderedBundleInfos() { + List result = new ArrayList(); + + // add all bundle resources ordered by location in stream + for (Iterator i = m_names.iterator(); i.hasNext();) { + String indexEntry = (String) i.next(); + AbstractInfo bundleInfo = getBundleInfoByPath(indexEntry); + if (bundleInfo != null) { + result.add(bundleInfo); + } + } + + // add bundle resources marked missing to the end of the result + BundleInfoImpl[] bundleInfoImpls = getBundleInfoImpls(); + for (int i = 0; i < bundleInfoImpls.length; i++) { + if (bundleInfoImpls[i].isMissing()) { + result.add(bundleInfoImpls[i]); + } + } + return (BundleInfoImpl[]) result.toArray(new BundleInfoImpl[result.size()]); + } + + public ResourceInfoImpl[] getOrderedResourceInfos() { + throw new UnsupportedOperationException("Not applicable for stream-based deployment package"); + } +} \ No newline at end of file diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Utils.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Utils.java new file mode 100644 index 00000000000..daebd3edb6b --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/Utils.java @@ -0,0 +1,241 @@ +/* + * 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.BufferedReader; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.jar.Attributes.Name; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +public class Utils { + private static final String MANIFEST_NAME = JarFile.MANIFEST_NAME; + + public static Manifest readManifest(File manifestFile) throws IOException { + InputStream is = null; + Manifest mf = null; + try { + is = new GZIPInputStream(new FileInputStream(manifestFile)); + mf = new Manifest(is); + } + finally { + closeSilently(is); + } + return mf; + } + + public static boolean replace(File target, File source) { + return delete(target, true /* deleteRoot */) && rename(source, target); + } + + public static boolean copy(File from, File to) { + boolean result = true; + if (from.isDirectory()) { + if (!to.isDirectory()) { + if (!to.mkdirs()) { + return false; + } + File[] files = from.listFiles(); + if (files == null) { + return false; + } + for (int i = 0; i < files.length; i++) { + result &= copy(files[i], new File(to, files[i].getName())); + } + } + } + else { + InputStream input = null; + OutputStream output = null; + try { + input = new FileInputStream(from); + output = new FileOutputStream(to); + byte[] buffer = new byte[4096]; + for (int i = input.read(buffer); i > -1; i = input.read(buffer)) { + output.write(buffer, 0, i); + } + } + catch (IOException e) { + return false; + } + finally { + if (!closeSilently(output)) { + result = false; + } + if (!closeSilently(input)) { + result = false; + } + } + } + return result; + } + + public static boolean rename(File from, File to) { + if (!from.renameTo(to)) { + if (copy(from, to)) { + if (!delete(from, true /* deleteRoot */)) { + return false; + } + } + else { + return false; + } + } + return true; + } + + public static boolean delete(File root, boolean deleteRoot) { + boolean result = true; + if (root.isDirectory()) { + File[] files = root.listFiles(); + for (int i = 0; i < files.length; i++) { + if (files[i].isDirectory()) { + result &= delete(files[i], true); + } + else { + result &= files[i].delete(); + } + } + } + if (deleteRoot) { + if (root.exists()) { + result &= root.delete(); + } + } + return result; + } + + public static void merge(File targetIndex, File target, File sourceIndex, File source) throws IOException { + List targetFiles = readIndex(targetIndex); + List sourceFiles = readIndex(sourceIndex); + List result = new ArrayList(targetFiles); + + File manifestFile = new File(source, (String) sourceFiles.remove(0)); + Manifest resultManifest = Utils.readManifest(manifestFile); + + resultManifest.getMainAttributes().remove(new Name(Constants.DEPLOYMENTPACKAGE_FIXPACK)); + + for (Iterator i = result.iterator(); i.hasNext();) { + String targetFile = (String) i.next(); + if (!MANIFEST_NAME.equals(targetFile) && !resultManifest.getEntries().containsKey(targetFile)) { + i.remove(); + } + } + + for (Iterator iter = sourceFiles.iterator(); iter.hasNext();) { + String path = (String) iter.next(); + File from = new File(source, path); + File to = new File(target, path); + if (targetFiles.contains(path)) { + if (!to.delete()) { + throw new IOException("Could not delete " + to); + } + } + else { + result.add(path); + } + if (!rename(from, to)) { + throw new IOException("Could not rename " + from + " to " + to); + } + } + + targetFiles.removeAll(sourceFiles); + + for (Iterator iter = resultManifest.getEntries().keySet().iterator(); iter.hasNext();) { + String path = (String) iter.next(); + Attributes sourceAttribute = (Attributes) resultManifest.getEntries().get(path); + if ("true".equals(sourceAttribute.remove(new Name(Constants.DEPLOYMENTPACKAGE_MISSING)))) { + targetFiles.remove(path); + } + } + + for (Iterator iter = targetFiles.iterator(); iter.hasNext();) { + String path = (String) iter.next(); + File targetFile = new File(target, path); + if (!targetFile.delete()) { + throw new IOException("Could not delete " + targetFile); + } + } + + GZIPOutputStream outputStream = new GZIPOutputStream(new FileOutputStream(new File(target, MANIFEST_NAME))); + try { + resultManifest.write(outputStream); + } + finally { + outputStream.close(); + } + writeIndex(targetIndex, result); + } + + public static List readIndex(File index) throws IOException { + BufferedReader reader = null; + try { + reader = new BufferedReader(new FileReader(index)); + List result = new ArrayList(); + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + result.add(line); + } + return result; + } + finally { + closeSilently(reader); + } + } + + static boolean closeSilently(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } + catch (IOException exception) { + // Ignore; nothing we can do about this... + return false; + } + } + return true; + } + + private static void writeIndex(File index, List input) throws IOException { + PrintWriter writer = null; + try { + writer = new PrintWriter(new FileWriter(index)); + for (Iterator iterator = input.iterator(); iterator.hasNext();) { + writer.println(iterator.next()); + } + } + finally { + closeSilently(writer); + } + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/VersionRange.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/VersionRange.java new file mode 100644 index 00000000000..f1e28382eb3 --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/VersionRange.java @@ -0,0 +1,156 @@ +/* + * 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 org.osgi.framework.Version; + +/** + * This class represents a version range as defined in section 3.2.5 of the OSGi r4 specification. + */ +public class VersionRange { + + public static final VersionRange infiniteRange = new VersionRange(Version.emptyVersion, true, null, true); + + private Version m_low = null; + private boolean m_isLowInclusive = false; + private Version m_high = null; + private boolean m_isHighInclusive = false; + private String m_toString = null; + + /** + * Create an instance of the VersionRange class. + * + * @param low Lower bound version + * @param isLowInclusive True if lower bound should be included in the range + * @param high Upper bound version + * @param isHighInclusive True if upper bound should be included in the range + */ + public VersionRange(Version low, boolean isLowInclusive, Version high, boolean isHighInclusive) { + m_low = low; + m_isLowInclusive = isLowInclusive; + m_high = high; + m_isHighInclusive = isHighInclusive; + } + + /** + * Creates an instance of the VersionRange class which resembles [version,*) + * + * @param version The lower boundary of the version range + */ + public VersionRange(Version version) { + this(version, true, null, false); + } + + /** + * Get the lower boundary of the version range, the boundary being inclusive or not is not taken into account. + * + * @return Version resembling the lower boundary of the version range + */ + public Version getLow() { + return m_low; + } + + /** + * Determines whether the lower boundary is inclusive or not. + * + * @return True if the lower boundary is inclusive, false otherwise. + */ + public boolean isLowInclusive() { + return m_isLowInclusive; + } + + /** + * Get the upper boundary of the version range, the boundary being inclusive or not is not taken in to account. + * + * @return Version resembling the upper boundary of the version range. + */ + public Version getHigh() { + return m_high; + } + + /** + * Determines whether the upper boundary is inclusive or not. + * + * @return True if the upper boundary is inclusive, false otherwise. + */ + public boolean isHighInclusive() { + return m_isHighInclusive; + } + + /** + * Determine if the specified version is part of the version range or not. + * + * @param version The version to verify + * @return True if the specified version is included in this version range, false otherwise. + */ + public boolean isInRange(Version version) { + // We might not have an upper end to the range. + if (m_high == null) { + return (version.compareTo(m_low) >= 0); + } + else if (isLowInclusive() && isHighInclusive()) { + return (version.compareTo(m_low) >= 0) && (version.compareTo(m_high) <= 0); + } + else if (isHighInclusive()) { + return (version.compareTo(m_low) > 0) && (version.compareTo(m_high) <= 0); + } + else if (isLowInclusive()) { + return (version.compareTo(m_low) >= 0) && (version.compareTo(m_high) < 0); + } + return (version.compareTo(m_low) > 0) && (version.compareTo(m_high) < 0); + } + + /** + * Parses a version range from the specified string. + * + * @param range String representation of the version range. + * @return A VersionRange object representing the version range. + * @throws IllegalArgumentException If range is improperly formatted. + */ + public static VersionRange parse(String range) throws IllegalArgumentException { + // Check if the version is an interval. + if (range.indexOf(',') >= 0) { + String s = range.substring(1, range.length() - 1); + String vlo = s.substring(0, s.indexOf(',')).trim(); + String vhi = s.substring(s.indexOf(',') + 1, s.length()).trim(); + return new VersionRange(new Version(vlo), (range.charAt(0) == '['), new Version(vhi), (range.charAt(range.length() - 1) == ']')); + } + else { + return new VersionRange(new Version(range), true, null, false); + } + } + + public String toString() { + if (m_toString == null) { + if (m_high != null) { + StringBuffer sb = new StringBuffer(); + sb.append(m_isLowInclusive ? '[' : '('); + sb.append(m_low.toString()); + sb.append(','); + sb.append(m_high.toString()); + sb.append(m_isHighInclusive ? ']' : ')'); + m_toString = sb.toString(); + } + else { + m_toString = m_low.toString(); + } + } + return m_toString; + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/AbstractAction.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/AbstractAction.java new file mode 100644 index 00000000000..44d1c9499fe --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/AbstractAction.java @@ -0,0 +1,44 @@ +/* + * 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.spi; + +/** + * Base implementation for commit/rollback actions with proper error handling. + * + * @author Felix Project Team + */ +abstract class AbstractAction implements Runnable { + /** + * Runs this action by calling {@link #doRun()} and in case of failure, {@link #onFailure(Exception)}. + */ + public final void run() { + try { + doRun(); + } + catch (Exception e) { + onFailure(e); + } + } + + protected abstract void doRun() throws Exception; + + protected void onFailure(Exception e) { + // Nop + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/Command.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/Command.java new file mode 100644 index 00000000000..4529de00ff5 --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/Command.java @@ -0,0 +1,186 @@ +/* + * 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.spi; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; + +import org.apache.felix.deploymentadmin.Constants; +import org.osgi.framework.Bundle; +import org.osgi.service.deploymentadmin.DeploymentException; + +/** + * Commands describe a group of tasks to be executed within the execution a + * deployment session. A command that has already executed can be rolled back + * and a command that is currently in progress can be signaled to stop it's + * activities by canceling it. + */ +public abstract class Command implements Constants { + + private final List m_rollback = new ArrayList(); + private final List m_commit = new ArrayList(); + + private volatile boolean m_cancelled; + + /** + * Executes the command, the specified DeploymentSession can be + * used to obtain various information about the deployment session which the + * command is part of. + * + * @param session The deployment session this command is part of. + * @throws DeploymentException Thrown if the command could not successfully + * execute. + */ + public final void execute(DeploymentSessionImpl session) throws DeploymentException { + try { + doExecute(session); + } + catch (Exception e) { + if (e instanceof DeploymentException) { + throw (DeploymentException) e; + } + throw new DeploymentException(CODE_OTHER_ERROR, "Command failed!", e); + } + } + + /** + * Executes the command, the specified DeploymentSession can be + * used to obtain various information about the deployment session which the + * command is part of. + * + * @param session The deployment session this command is part of. + * @throws Exception in case of this command could not terminate + * successfully. + */ + protected abstract void doExecute(DeploymentSessionImpl session) throws Exception; + + /** + * Rolls back all actions that were added through the + * addRollback(Runnable r) method (in reverse adding order). It + * is not guaranteed that the state of everything related to the command + * will be as if the command was never executed, a best effort should be + * made though. + * + * @param session the deployment session to rollback, should be used for + * logging purposes. + */ + protected void rollback(DeploymentSessionImpl session) { + try { + for (ListIterator i = m_rollback.listIterator(m_rollback.size()); i.hasPrevious();) { + Runnable runnable = (Runnable) i.previous(); + runnable.run(); + } + } + finally { + cleanUp(); + } + } + + /** + * Commits all changes the command may have defined when it was executed by + * calling the execute() method. + * + * @param session the deployment session to commit, should be used for + * logging purposes. + */ + protected final void commit(DeploymentSessionImpl session) { + try { + for (ListIterator i = m_commit.listIterator(); i.hasNext();) { + Runnable runnable = (Runnable) i.next(); + runnable.run(); + } + } + finally { + cleanUp(); + } + } + + private void cleanUp() { + m_rollback.clear(); + m_commit.clear(); + m_cancelled = false; + } + + /** + * Determines if the command was canceled. This method should be used + * regularly by implementing classes to determine if their command was + * canceled, if so they should return as soon as possible from their + * operations. + * + * @return true if the command was canceled, false otherwise. + */ + protected boolean isCancelled() { + return m_cancelled; + } + + /** + * Adds an action to be executed in case of a roll back. + * + * @param runnable The runnable to be executed in case of a roll back. + */ + protected void addRollback(AbstractAction runnable) { + m_rollback.add(runnable); + } + + /** + * Adds an action to be executed in case of a commit + * + * @param runnable The runnable to be executes in case of a commit. + */ + protected void addCommit(AbstractAction runnable) { + m_commit.add(runnable); + } + + /** + * Sets the command to being cancelled, this does not have an immediate + * effect. Commands that are executing should check regularly if they were + * cancelled and if so they should make an effort to stop their operations + * as soon as possible followed by throwing an + * DeploymentException.CODE_CANCELLED exception. + */ + public void cancel() { + m_cancelled = true; + } + + /** + * Determines whether a given bundle is actually a fragment bundle. + * + * @param bundle the bundle to test, cannot be null. + * @return true if the given bundle is actually a fragment + * bundle, false otherwise. + */ + static final boolean isFragmentBundle(Bundle bundle) { + Object fragmentHost = bundle.getHeaders().get(FRAGMENT_HOST); + return fragmentHost != null; + } + + static final void closeSilently(Closeable resource) { + if (resource != null) { + try { + resource.close(); + } + catch (IOException e) { + // Not much we can do... + } + } + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/CommitResourceCommand.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/CommitResourceCommand.java new file mode 100644 index 00000000000..171337a02ca --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/CommitResourceCommand.java @@ -0,0 +1,105 @@ +/* + * 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.spi; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import org.osgi.service.deploymentadmin.DeploymentException; +import org.osgi.service.deploymentadmin.spi.ResourceProcessor; +import org.osgi.service.deploymentadmin.spi.ResourceProcessorException; +import org.osgi.service.log.LogService; + +/** + * Command that commits all the resource processors that were added to the + * command. + */ +public class CommitResourceCommand extends Command { + + private final List m_processors = new ArrayList(); + + protected void doExecute(DeploymentSessionImpl session) throws Exception { + for (ListIterator i = m_processors.listIterator(m_processors.size()); i.hasPrevious();) { + ResourceProcessor processor = (ResourceProcessor) i.previous(); + try { + processor.prepare(); + } + catch (ResourceProcessorException e) { + session.getLog().log(LogService.LOG_ERROR, "Preparing commit for resource processor failed", e); + // Check what error code we got... + if (e.getCode() == ResourceProcessorException.CODE_PREPARE) { + throw new DeploymentException(CODE_COMMIT_ERROR, "Preparing commit for resource processor failed!", e); + } + throw new DeploymentException(e.getCode(), "Preparing commit for resource processor failed!", e); + } + catch (Exception e) { + session.getLog().log(LogService.LOG_ERROR, "Preparing commit for resource processor failed", e); + throw new DeploymentException(CODE_OTHER_ERROR, "Preparing commit for resource processor failed", e); + } + } + for (ListIterator i = m_processors.listIterator(m_processors.size()); i.hasPrevious();) { + ResourceProcessor processor = (ResourceProcessor) i.previous(); + try { + processor.commit(); + } + catch (Exception e) { + // We cannot throw an exception, see OSGi spec. + session.getLog().log(LogService.LOG_ERROR, "Committing resource processor '" + processor + "' failed", e); + } + } + m_processors.clear(); + } + + protected void rollback(DeploymentSessionImpl session) { + for (ListIterator i = m_processors.listIterator(m_processors.size()); i.hasPrevious();) { + ResourceProcessor processor = (ResourceProcessor) i.previous(); + try { + processor.rollback(); + } + catch (Exception e) { + // We cannot throw an exception, see OSGi spec. + session.getLog().log(LogService.LOG_ERROR, "Rollback of resource processor '" + processor + "' failed", e); + } + finally { + i.remove(); + } + } + } + + /** + * Add a resource processor, all resource processors that are added will be + * committed when the command is executed. + * + * @param processor The resource processor to add. + * @return true if the resource processor was added, false if it was already + * added. + */ + public boolean addResourceProcessor(ResourceProcessor processor) { + for (Iterator i = m_processors.iterator(); i.hasNext();) { + ResourceProcessor proc = (ResourceProcessor) i.next(); + if (proc == processor) { + return false; + } + } + m_processors.add(processor); + return true; + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/DeploymentSessionImpl.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/DeploymentSessionImpl.java new file mode 100644 index 00000000000..3c8cc1da664 --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/DeploymentSessionImpl.java @@ -0,0 +1,214 @@ +/* + * 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.spi; + +import java.io.File; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import org.apache.felix.deploymentadmin.AbstractDeploymentPackage; +import org.apache.felix.deploymentadmin.Constants; +import org.apache.felix.deploymentadmin.DeploymentAdminConfig; +import org.apache.felix.deploymentadmin.DeploymentAdminImpl; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.service.deploymentadmin.DeploymentException; +import org.osgi.service.deploymentadmin.DeploymentPackage; +import org.osgi.service.deploymentadmin.spi.DeploymentSession; +import org.osgi.service.log.LogService; +import org.osgi.service.packageadmin.PackageAdmin; + +/** + * Represents a running deployment session. + */ +public class DeploymentSessionImpl implements DeploymentSession, Constants { + + private final AbstractDeploymentPackage m_target; + private final AbstractDeploymentPackage m_source; + private final List m_commands; + private final DeploymentAdminImpl m_admin; + private final DeploymentAdminConfig m_config; + + private volatile Command m_currentCommand = null; + private volatile boolean m_cancelled; + + public DeploymentSessionImpl(AbstractDeploymentPackage source, AbstractDeploymentPackage target, List commands, DeploymentAdminImpl admin, DeploymentAdminConfig config) { + m_source = source; + m_target = target; + m_commands = commands; + m_admin = admin; + m_config = config; + } + + /** + * Calling this method will cause the commands specified for this session to + * be executed. the commands will be rolled back if the session is canceled + * or if an exception is caused by one of the commands. + * + * @throws DeploymentException If the session was canceled ( + * DeploymentException.CODE_CANCELLED) or if one of the + * commands caused an exception (DeploymentException.*) + */ + public void call(boolean ignoreExceptions) throws DeploymentException { + List executedCommands = new ArrayList(); + for (Iterator i = m_commands.iterator(); i.hasNext();) { + if (m_cancelled) { + // previous command did not pick up on cancel + rollback(executedCommands); + throw new DeploymentException(CODE_CANCELLED); + } + m_currentCommand = (Command) i.next(); + try { + executedCommands.add(m_currentCommand); + m_currentCommand.execute(this); + } + catch (DeploymentException de) { + if (!ignoreExceptions) { + // XXX catch exception and verify whether it is possible to + // have exceptions during a rollback + rollback(executedCommands); + throw de; + } else { + m_admin.getLog().log(LogService.LOG_DEBUG, "Ignoring exception as requested!", de); + } + } + } + for (Iterator i = m_commands.iterator(); i.hasNext();) { + ((Command) i.next()).commit(this); + } + m_currentCommand = null; + } + + /** + * Cancels the session if it is in progress. + * + * @return true if a session was in progress and now canceled, false + * otherwise. + */ + public boolean cancel() { + m_cancelled = true; + + Command currentCommand = m_currentCommand; + if (currentCommand != null) { + currentCommand.cancel(); + return true; + } + + return false; + } + + /** + * Returns the bundle context of the bundle this class is part of. + * + * @return The BundleContext. + */ + public BundleContext getBundleContext() { + return m_admin.getBundleContext(); + } + + /** + * @return the configuration for this session, is guaranteed to remain stable during this session, never + * null. + */ + public DeploymentAdminConfig getConfiguration() { + return m_config; + } + + /** + * Retrieve the base directory of the persistent storage area according to + * OSGi Core R4 6.1.6.10 for the given BundleContext. + * + * @param bundle of which the storage area will be returned + * @return a File that represents the base directory of the + * persistent storage area for the bundle + */ + public File getDataFile(Bundle bundle) { + File result = null; + + BundleContext context = bundle.getBundleContext(); + if (context != null) { + result = context.getDataFile(""); + } + else { + // TODO this method should not return null or throw an exception; we + // need to resolve this... + throw new IllegalStateException("Could not retrieve valid bundle context from bundle " + bundle.getSymbolicName()); + } + + if (result == null) { + throw new IllegalStateException("Could not retrieve base directory for bundle " + bundle.getSymbolicName()); + } + return result; + } + + /** + * Returns the currently present log service. + * + * @return The LogService. + */ + public LogService getLog() { + return m_admin.getLog(); + } + + /** + * Returns the currently present package admin. + * + * @return The PackageAdmin + */ + public PackageAdmin getPackageAdmin() { + return m_admin.getPackageAdmin(); + } + + /** + * Returns the source deployment package as an + * AbstractDeploymentPackage. + * + * @return The source deployment packge of the session. + */ + public AbstractDeploymentPackage getSourceAbstractDeploymentPackage() { + return m_source; + } + + public DeploymentPackage getSourceDeploymentPackage() { + return m_source; + } + + /** + * Returns the target deployment package as an + * AbstractDeploymentPackage. + * + * @return The target deployment package of the session. + */ + public AbstractDeploymentPackage getTargetAbstractDeploymentPackage() { + return m_target; + } + + public DeploymentPackage getTargetDeploymentPackage() { + return m_target; + } + + private void rollback(List executedCommands) { + for (ListIterator i = executedCommands.listIterator(executedCommands.size()); i.hasPrevious();) { + Command command = (Command) i.previous(); + command.rollback(this); + } + } +} \ No newline at end of file diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/DropAllBundlesCommand.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/DropAllBundlesCommand.java new file mode 100644 index 00000000000..48ca3ffe490 --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/DropAllBundlesCommand.java @@ -0,0 +1,86 @@ +/* + * 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.spi; + +import java.io.InputStream; + +import org.apache.felix.deploymentadmin.AbstractDeploymentPackage; +import org.apache.felix.deploymentadmin.BundleInfoImpl; +import org.osgi.framework.Bundle; +import org.osgi.service.log.LogService; + +/** + * Command that uninstalls all bundles, if rolled back the bundles are restored. + */ +public class DropAllBundlesCommand extends Command { + + protected void doExecute(DeploymentSessionImpl session) throws Exception { + AbstractDeploymentPackage target = session.getTargetAbstractDeploymentPackage(); + LogService log = session.getLog(); + + BundleInfoImpl[] orderedTargetBundles = target.getOrderedBundleInfos(); + for (int i = orderedTargetBundles.length - 1; i >= 0; i--) { + BundleInfoImpl bundleInfo = orderedTargetBundles[i]; + // stale bundle, save a copy for rolling back and uninstall it + String symbolicName = bundleInfo.getSymbolicName(); + try { + Bundle bundle = target.getBundle(symbolicName); + if (bundle != null) { + bundle.uninstall(); + addRollback(new InstallBundleRunnable(bundle, target, log)); + } + } + catch (Exception be) { + log.log(LogService.LOG_WARNING, "Bundle '" + symbolicName + "' could not be uninstalled", be); + } + } + } + + private static class InstallBundleRunnable extends AbstractAction { + private final AbstractDeploymentPackage m_target; + private final Bundle m_bundle; + private final LogService m_log; + + public InstallBundleRunnable(Bundle bundle, AbstractDeploymentPackage target, LogService log) { + m_bundle = bundle; + m_target = target; + m_log = log; + } + + protected void doRun() throws Exception { + InputStream is = null; + try { + is = m_target.getBundleStream(m_bundle.getSymbolicName()); + if (is != null) { + m_bundle.update(is); + } + else { + throw new RuntimeException("Unable to get inputstream for bundle '" + m_bundle.getSymbolicName() + "'"); + } + } + finally { + closeSilently(is); + } + } + + protected void onFailure(Exception e) { + m_log.log(LogService.LOG_WARNING, "Could not rollback uninstallation of bundle '" + m_bundle.getSymbolicName() + "'", e); + } + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/DropAllResourcesCommand.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/DropAllResourcesCommand.java new file mode 100644 index 00000000000..105faa09da9 --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/DropAllResourcesCommand.java @@ -0,0 +1,115 @@ +/* + * 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.spi; + +import java.util.HashSet; +import java.util.Set; + +import org.apache.felix.deploymentadmin.AbstractDeploymentPackage; +import org.apache.felix.deploymentadmin.ResourceInfoImpl; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.deploymentadmin.DeploymentException; +import org.osgi.service.deploymentadmin.spi.ResourceProcessor; +import org.osgi.service.log.LogService; + +/** + * Command that drops resources. + */ +public class DropAllResourcesCommand extends Command { + private final CommitResourceCommand m_commitCommand; + + /** + * Creates an instance of this command. The commit command is used to make + * sure the resource processors used to drop all resources will be committed + * at a later stage in the process. + * + * @param commitCommand The commit command that will be executed at a later + * stage in the process. + */ + public DropAllResourcesCommand(CommitResourceCommand commitCommand) { + m_commitCommand = commitCommand; + } + + protected void doExecute(DeploymentSessionImpl session) throws Exception { + // Allow proper rollback in case the drop fails... + addRollback(new RollbackCommitAction(session)); + + AbstractDeploymentPackage target = session.getTargetAbstractDeploymentPackage(); + BundleContext context = session.getBundleContext(); + LogService log = session.getLog(); + + // Collect all unique paths of all processed resources... + Set resourceProcessors = new HashSet(); + + ResourceInfoImpl[] orderedTargetResources = target.getOrderedResourceInfos(); + for (int i = orderedTargetResources.length - 1; i >= 0; i--) { + ResourceInfoImpl resourceInfo = orderedTargetResources[i]; + // FELIX-4491: only resources that need to be processed should be handled... + if (!resourceInfo.isProcessedResource()) { + session.getLog().log(LogService.LOG_INFO, "Ignoring non-processed resource: " + resourceInfo.getPath()); + continue; + } + + String rpName = resourceInfo.getResourceProcessor(); + String path = resourceInfo.getPath(); + + // Keep track of which resource processors we've seen already... + if (!resourceProcessors.add(rpName)) { + // Already seen this RP; continue on the next one... + continue; + } + + ServiceReference ref = target.getResourceProcessor(path); + if (ref == null) { + log.log(LogService.LOG_ERROR, "Failed to find resource processor for '" + rpName + "'!"); + throw new DeploymentException(CODE_PROCESSOR_NOT_FOUND, "Failed to find resource processor '" + rpName + "'!"); + } + + ResourceProcessor resourceProcessor = (ResourceProcessor) context.getService(ref); + if (resourceProcessor == null) { + log.log(LogService.LOG_ERROR, "Failed to find resource processor for '" + rpName + "'!"); + throw new DeploymentException(CODE_PROCESSOR_NOT_FOUND, "Failed to find resource processor '" + rpName + "'!"); + } + + try { + if (m_commitCommand.addResourceProcessor(resourceProcessor)) { + resourceProcessor.begin(session); + } + resourceProcessor.dropAllResources(); + } + catch (Exception e) { + log.log(LogService.LOG_ERROR, "Failed to drop all resources for resource processor '" + rpName + "'!", e); + throw new DeploymentException(CODE_OTHER_ERROR, "Failed to drop all resources for resource processor '" + rpName + "'!", e); + } + } + } + + private class RollbackCommitAction extends AbstractAction { + private final DeploymentSessionImpl m_session; + + public RollbackCommitAction(DeploymentSessionImpl session) { + m_session = session; + } + + protected void doRun() { + m_commitCommand.rollback(m_session); + } + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/DropBundleCommand.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/DropBundleCommand.java new file mode 100644 index 00000000000..7ad59c24cac --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/DropBundleCommand.java @@ -0,0 +1,88 @@ +/* + * 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.spi; + +import java.io.InputStream; + +import org.apache.felix.deploymentadmin.AbstractDeploymentPackage; +import org.apache.felix.deploymentadmin.BundleInfoImpl; +import org.osgi.framework.Bundle; +import org.osgi.service.log.LogService; + +/** + * Command that uninstalls bundles, if rolled back the bundles are restored. + */ +public class DropBundleCommand extends Command { + + protected void doExecute(DeploymentSessionImpl session) throws Exception { + AbstractDeploymentPackage target = session.getTargetAbstractDeploymentPackage(); + AbstractDeploymentPackage source = session.getSourceAbstractDeploymentPackage(); + LogService log = session.getLog(); + + BundleInfoImpl[] orderedTargetBundles = target.getOrderedBundleInfos(); + for (int i = orderedTargetBundles.length - 1; i >= 0; i--) { + BundleInfoImpl bundleInfo = orderedTargetBundles[i]; + String symbolicName = bundleInfo.getSymbolicName(); + + if (!bundleInfo.isCustomizer() && source.getBundleInfoByName(symbolicName) == null) { + // stale bundle, save a copy for rolling back and uninstall it + try { + Bundle bundle = target.getBundle(symbolicName); + bundle.uninstall(); + addRollback(new InstallBundleRunnable(bundle, target, log)); + } + catch (Exception be) { + log.log(LogService.LOG_WARNING, "Bundle '" + symbolicName + "' could not be uninstalled", be); + } + } + } + } + + private static class InstallBundleRunnable extends AbstractAction { + private final AbstractDeploymentPackage m_target; + private final Bundle m_bundle; + private final LogService m_log; + + public InstallBundleRunnable(Bundle bundle, AbstractDeploymentPackage target, LogService log) { + m_bundle = bundle; + m_target = target; + m_log = log; + } + + protected void doRun() throws Exception { + InputStream is = null; + try { + is = m_target.getBundleStream(m_bundle.getSymbolicName()); + if (is != null) { + m_bundle.update(is); + } + else { + throw new RuntimeException("Unable to get Inputstream for bundle '" + m_bundle.getSymbolicName() + "'"); + } + } + finally { + closeSilently(is); + } + } + + protected void onFailure(Exception e) { + m_log.log(LogService.LOG_WARNING, "Could not rollback uninstallation of bundle '" + m_bundle.getSymbolicName() + "'", e); + } + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/DropResourceCommand.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/DropResourceCommand.java new file mode 100644 index 00000000000..7f86867397f --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/DropResourceCommand.java @@ -0,0 +1,97 @@ +/* + * 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.spi; + +import org.apache.felix.deploymentadmin.AbstractDeploymentPackage; +import org.apache.felix.deploymentadmin.ResourceInfoImpl; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.deploymentadmin.spi.ResourceProcessor; +import org.osgi.service.log.LogService; + +/** + * Command that drops resources. + */ +public class DropResourceCommand extends Command { + + private final CommitResourceCommand m_commitCommand; + + /** + * Creates an instance of this command. The commit command is used to make + * sure the resource processors used to drop resources will be committed at + * a later stage in the process. + * + * @param commitCommand The commit command that will be executed at a later + * stage in the process. + */ + public DropResourceCommand(CommitResourceCommand commitCommand) { + m_commitCommand = commitCommand; + } + + protected void doExecute(DeploymentSessionImpl session) throws Exception { + // Allow proper rollback in case the drop fails... + addRollback(new RollbackCommitAction(session)); + + AbstractDeploymentPackage target = session.getTargetAbstractDeploymentPackage(); + AbstractDeploymentPackage source = session.getSourceAbstractDeploymentPackage(); + BundleContext context = session.getBundleContext(); + LogService log = session.getLog(); + + ResourceInfoImpl[] orderedTargetResources = target.getOrderedResourceInfos(); + for (int i = orderedTargetResources.length - 1; i >= 0; i--) { + ResourceInfoImpl resourceInfo = orderedTargetResources[i]; + // FELIX-4491: only resources that need to be processed should be handled... + if (!resourceInfo.isProcessedResource()) { + session.getLog().log(LogService.LOG_INFO, "Ignoring non-processed resource: " + resourceInfo.getPath()); + continue; + } + + String path = resourceInfo.getPath(); + if (source.getResourceInfoByPath(path) == null) { + ServiceReference ref = target.getResourceProcessor(path); + if (ref != null) { + ResourceProcessor resourceProcessor = (ResourceProcessor) context.getService(ref); + if (resourceProcessor != null) { + try { + if (m_commitCommand.addResourceProcessor(resourceProcessor)) { + resourceProcessor.begin(session); + } + resourceProcessor.dropped(path); + } + catch (Exception e) { + log.log(LogService.LOG_WARNING, "Not allowed to drop resource '" + path + "'", e); + } + } + } + } + } + } + + private class RollbackCommitAction extends AbstractAction { + private final DeploymentSessionImpl m_session; + + public RollbackCommitAction(DeploymentSessionImpl session) { + m_session = session; + } + + protected void doRun() { + m_commitCommand.rollback(m_session); + } + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/GetStorageAreaCommand.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/GetStorageAreaCommand.java new file mode 100644 index 00000000000..d75c6197c8e --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/GetStorageAreaCommand.java @@ -0,0 +1,70 @@ +/* + * 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.spi; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import org.osgi.framework.Bundle; +import org.osgi.service.deploymentadmin.BundleInfo; +import org.osgi.service.deploymentadmin.DeploymentException; +import org.osgi.service.deploymentadmin.DeploymentPackage; +import org.osgi.service.log.LogService; + +/** + * Command that determines the storage area's of all bundles in the source + * deployment package of a deployment session. + */ +public class GetStorageAreaCommand extends Command { + + private final Map m_storageAreas = new HashMap(); + + protected void doExecute(DeploymentSessionImpl session) throws Exception { + DeploymentPackage target = session.getTargetDeploymentPackage(); + BundleInfo[] infos = target.getBundleInfos(); + for (int i = 0; i < infos.length; i++) { + if (isCancelled()) { + throw new DeploymentException(CODE_CANCELLED); + } + Bundle bundle = target.getBundle(infos[i].getSymbolicName()); + if (bundle != null) { + try { + File root = session.getDataFile(bundle); + m_storageAreas.put(bundle.getSymbolicName(), root); + } + catch (IllegalStateException ise) { + session.getLog().log(LogService.LOG_WARNING, "Could not get reference to storage area of bundle '" + bundle.getSymbolicName() + "'"); + } + } + } + } + + /** + * Determines the storage area's of all bundles in the source deployment + * package of a deployment session. + * + * @return Map with File object references to the + * storage area's, they bundle symbolic name is used as a key in the + * Map. + */ + public Map getStorageAreas() { + return m_storageAreas; + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/ProcessResourceCommand.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/ProcessResourceCommand.java new file mode 100644 index 00000000000..9c0674b7a47 --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/ProcessResourceCommand.java @@ -0,0 +1,151 @@ +/* + * 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.spi; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.deploymentadmin.AbstractDeploymentPackage; +import org.apache.felix.deploymentadmin.AbstractInfo; +import org.apache.felix.deploymentadmin.ResourceInfoImpl; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.deploymentadmin.DeploymentException; +import org.osgi.service.deploymentadmin.spi.ResourceProcessor; +import org.osgi.service.deploymentadmin.spi.ResourceProcessorException; +import org.osgi.service.log.LogService; + +/** + * Command that processes all the processed resources in the source deployment + * package of a deployment session by finding their Resource Processors and + * having those process the resources. System property + * org.apache.felix.deploymentadmin.allowforeigncustomizers allows + * you to skip the source handling of resource processors, allowing the use of + * processors already on the system. Defaults to false. + */ +public class ProcessResourceCommand extends Command { + + private final CommitResourceCommand m_commitCommand; + + /** + * Creates an instance of this command, the CommitCommand is + * used to ensure that all used ResourceProcessors will be + * committed at a later stage in the deployment session. + * + * @param commitCommand The CommitCommand that will commit all + * resource processors used in this command. + */ + public ProcessResourceCommand(CommitResourceCommand commitCommand) { + m_commitCommand = commitCommand; + } + + protected void doExecute(DeploymentSessionImpl session) throws Exception { + // Allow proper rollback in case the drop fails... + addRollback(new RollbackCommitAction(session)); + + AbstractDeploymentPackage source = session.getSourceAbstractDeploymentPackage(); + BundleContext context = session.getBundleContext(); + + Map expectedResources = new HashMap(); + AbstractInfo[] resourceInfos = (AbstractInfo[]) source.getResourceInfos(); + for (int i = 0; i < resourceInfos.length; i++) { + AbstractInfo resourceInfo = resourceInfos[i]; + if (!resourceInfo.isMissing()) { + expectedResources.put(resourceInfo.getPath(), resourceInfo); + } + } + + try { + while (!expectedResources.isEmpty()) { + AbstractInfo jarEntry = source.getNextEntry(); + if (jarEntry == null) { + throw new DeploymentException(CODE_OTHER_ERROR, "Expected more resources in the stream: " + expectedResources.keySet()); + } + + String name = jarEntry.getPath(); + + ResourceInfoImpl resourceInfo = (ResourceInfoImpl) expectedResources.remove(name); + if (resourceInfo == null) { + throw new DeploymentException(CODE_OTHER_ERROR, "Resource '" + name + "' is not described in the manifest."); + } + // FELIX-4491: only resources that need to be processed should be handled... + if (!resourceInfo.isProcessedResource()) { + session.getLog().log(LogService.LOG_INFO, "Ignoring non-processed resource: " + resourceInfo.getPath()); + continue; + } + + ServiceReference ref = source.getResourceProcessor(name); + if (ref == null) { + throw new DeploymentException(CODE_PROCESSOR_NOT_FOUND, "No resource processor for resource: '" + name + "'"); + } + if (!isValidCustomizer(session, ref)) { + throw new DeploymentException(CODE_FOREIGN_CUSTOMIZER, "Resource processor for resource '" + name + "' belongs to foreign deployment package"); + } + + ResourceProcessor resourceProcessor = (ResourceProcessor) context.getService(ref); + if (resourceProcessor == null) { + throw new DeploymentException(CODE_PROCESSOR_NOT_FOUND, "No resource processor for resource: '" + name + "'"); + } + + try { + if (m_commitCommand.addResourceProcessor(resourceProcessor)) { + resourceProcessor.begin(session); + } + resourceProcessor.process(name, source.getCurrentEntryStream()); + } + catch (ResourceProcessorException rpe) { + if (rpe.getCode() == ResourceProcessorException.CODE_RESOURCE_SHARING_VIOLATION) { + throw new DeploymentException(CODE_RESOURCE_SHARING_VIOLATION, "Sharing violation while processing resource '" + name + "'", rpe); + } + else { + throw new DeploymentException(CODE_OTHER_ERROR, "Error while processing resource '" + name + "'", rpe); + } + } + } + } + catch (IOException e) { + throw new DeploymentException(CODE_OTHER_ERROR, "Problem while reading stream", e); + } + } + + private boolean isValidCustomizer(DeploymentSessionImpl session, ServiceReference ref) { + if (session.getConfiguration().isAllowForeignCustomizers()) { + // If foreign customizers are allowed, any non-null customizer will do... + return ref != null; + } + + AbstractDeploymentPackage source = session.getSourceAbstractDeploymentPackage(); + String serviceOwnerSymName = ref.getBundle().getSymbolicName(); + // If only local customizers are allowed, we must be able to find this customizer in our DP... + return source.getBundleInfoByName(serviceOwnerSymName) != null; + } + + private class RollbackCommitAction extends AbstractAction { + private final DeploymentSessionImpl m_session; + + public RollbackCommitAction(DeploymentSessionImpl session) { + m_session = session; + } + + protected void doRun() { + m_commitCommand.rollback(m_session); + } + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/SnapshotCommand.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/SnapshotCommand.java new file mode 100644 index 00000000000..29c2c2c2b3d --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/SnapshotCommand.java @@ -0,0 +1,226 @@ +/* + * 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.spi; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import org.apache.felix.deploymentadmin.AbstractDeploymentPackage; +import org.apache.felix.deploymentadmin.Utils; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.service.deploymentadmin.BundleInfo; +import org.osgi.service.deploymentadmin.DeploymentException; +import org.osgi.service.log.LogService; + +public class SnapshotCommand extends Command { + /** The ZIP specification mandates that directory-entries end with a forward slash (on all platforms). */ + static final String FORWARD_SLASH = "/"; + + private final GetStorageAreaCommand m_getStorageAreaCommand; + + public SnapshotCommand(GetStorageAreaCommand getStorageAreaCommand) { + m_getStorageAreaCommand = getStorageAreaCommand; + } + + protected void doExecute(DeploymentSessionImpl session) throws Exception { + AbstractDeploymentPackage target = session.getTargetAbstractDeploymentPackage(); + BundleContext context = session.getBundleContext(); + + BundleInfo[] infos = target.getBundleInfos(); + Map storageAreas = m_getStorageAreaCommand.getStorageAreas(); + for (int i = 0; i < infos.length; i++) { + if (isCancelled()) { + throw new DeploymentException(CODE_CANCELLED); + } + + String symbolicName = infos[i].getSymbolicName(); + Bundle bundle = target.getBundle(symbolicName); + if (bundle != null) { + File root = (File) storageAreas.get(symbolicName); + if (root != null) { + File snapshot = context.getDataFile("snapshots"); + snapshot.mkdirs(); + snapshot = new File(snapshot, infos[i].getSymbolicName()); + try { + snapshot.createNewFile(); + store(root, snapshot); + addRollback(new RestoreSnapshotRunnable(session, snapshot, root)); + addCommit(new DeleteSnapshotRunnable(session, snapshot)); + } + catch (Exception e) { + session.getLog().log(LogService.LOG_WARNING, "Could not access storage area of bundle '" + symbolicName + "'!", e); + snapshot.delete(); + } + } + else { + session.getLog().log(LogService.LOG_WARNING, "Could not retrieve storage area of bundle '" + symbolicName + "', skipping it."); + } + } + } + } + + protected static void restore(File archiveFile, File targetDir) throws IOException { + ZipInputStream input = null; + try { + input = new ZipInputStream(new FileInputStream(archiveFile)); + + ZipEntry entry; + while ((entry = input.getNextEntry()) != null) { + File targetEntry = new File(targetDir, entry.getName()); + + if (entry.isDirectory()) { + if (!targetEntry.mkdirs()) { + throw new IOException("Failed to create one or more sub-directories!"); + } + } + else { + OutputStream output = null; + try { + output = new FileOutputStream(targetEntry); + copy(input, output); + } + finally { + closeSilently(output); + } + } + + input.closeEntry(); + } + } + finally { + closeSilently(input); + } + } + + protected static void store(File sourceDir, File archiveFile) throws IOException { + ZipOutputStream output = null; + try { + output = new ZipOutputStream(new FileOutputStream(archiveFile)); + // Traverse source directory recursively, and store all entries... + store(output, sourceDir, ""); + } + finally { + closeSilently(output); + } + } + + protected static void copy(InputStream is, OutputStream os) throws IOException { + byte[] buffer = new byte[8192]; + int read; + try { + while ((read = is.read(buffer)) != -1) { + os.write(buffer, 0, read); + } + } + finally { + os.flush(); + } + } + + private static void store(ZipOutputStream output, File sourceDir, String entryName) throws IOException { + File entry = new File(sourceDir, entryName); + + if (entry.isFile()) { + ZipEntry zipEntry = new ZipEntry(entryName); + zipEntry.setSize(entry.length()); + zipEntry.setTime(entry.lastModified()); + + output.putNextEntry(zipEntry); + + InputStream input = null; + try { + input = new FileInputStream(entry); + copy(input, output); + } + finally { + closeSilently(input); + output.closeEntry(); + } + } + else if (entry.isDirectory()) { + String baseDir = ""; + if (!"".equals(entryName)) { + baseDir = entryName; + // Directories *must* use forward slashes... + if (!baseDir.endsWith(FORWARD_SLASH)) { + baseDir = baseDir.concat(FORWARD_SLASH); + } + + output.putNextEntry(new ZipEntry(baseDir)); + output.closeEntry(); + } + + String[] entries = entry.list(); + for (int i = 0; i < entries.length; i++) { + store(output, sourceDir, baseDir.concat(entries[i])); + } + } + } + + private static class DeleteSnapshotRunnable extends AbstractAction { + private final DeploymentSessionImpl m_session; + private final File m_snapshot; + + private DeleteSnapshotRunnable(DeploymentSessionImpl session, File snapshot) { + m_session = session; + m_snapshot = snapshot; + } + + protected void doRun() { + if (!m_snapshot.delete()) { + m_session.getLog().log(LogService.LOG_WARNING, "Failed to delete snapshot in " + m_snapshot + "!"); + } + } + } + + private static class RestoreSnapshotRunnable extends AbstractAction { + private final DeploymentSessionImpl m_session; + private final File m_snapshot; + private final File m_root; + + private RestoreSnapshotRunnable(DeploymentSessionImpl session, File snapshot, File root) { + m_session = session; + m_snapshot = snapshot; + m_root = root; + } + + protected void doRun() throws Exception { + try { + Utils.delete(m_root, false /* deleteRoot */); + restore(m_snapshot, m_root); + } + finally { + m_snapshot.delete(); + } + } + + protected void onFailure(Exception e) { + m_session.getLog().log(LogService.LOG_WARNING, "Failed to restore snapshot!", e); + } + } +} \ No newline at end of file diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/StartBundleCommand.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/StartBundleCommand.java new file mode 100644 index 00000000000..edb4502a8ca --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/StartBundleCommand.java @@ -0,0 +1,131 @@ +/* + * 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.spi; + +import org.apache.felix.deploymentadmin.AbstractDeploymentPackage; +import org.apache.felix.deploymentadmin.BundleInfoImpl; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.service.log.LogService; +import org.osgi.service.packageadmin.PackageAdmin; + +/** + * Command that starts all bundles described in the source deployment package of + * a deployment session. + */ +public class StartBundleCommand extends Command { + private final RefreshPackagesMonitor m_refreshMonitor = new RefreshPackagesMonitor(); + + protected void doExecute(DeploymentSessionImpl session) throws Exception { + AbstractDeploymentPackage source = session.getSourceAbstractDeploymentPackage(); + PackageAdmin packageAdmin = session.getPackageAdmin(); + RefreshPackagesListener listener = new RefreshPackagesListener(); + LogService log = session.getLog(); + + session.getBundleContext().addFrameworkListener(listener); + packageAdmin.refreshPackages(null); + m_refreshMonitor.waitForRefresh(); + session.getBundleContext().removeFrameworkListener(listener); + + // start source bundles + BundleInfoImpl[] bundleInfos = source.getOrderedBundleInfos(); + for (int i = 0; i < bundleInfos.length; i++) { + BundleInfoImpl bundleInfoImpl = bundleInfos[i]; + if (!bundleInfoImpl.isCustomizer()) { + String symbolicName = bundleInfoImpl.getSymbolicName(); + + Bundle bundle = source.getBundle(symbolicName); + if (bundle != null) { + if (isFragmentBundle(bundle)) { + log.log(LogService.LOG_INFO, "Skipping fragment bundle '" + symbolicName + "'"); + } + else { + try { + bundle.start(); + } + catch (Exception be) { + log.log(LogService.LOG_WARNING, "Could not start bundle '" + symbolicName + "'", be); + } + } + } + else { + log.log(LogService.LOG_WARNING, "Could not start bundle '" + symbolicName + "' because it is not present in the framework"); + } + } + } + } + + /** + * RefreshPackagesListener is only listing to FrameworkEvents of the type + * PACKAGES_REFRESHED. It will notify any object waiting the completion of a + * refreshpackages() call. + */ + private class RefreshPackagesListener implements FrameworkListener { + public void frameworkEvent(FrameworkEvent event) { + if (event.getType() == FrameworkEvent.PACKAGES_REFRESHED) { + m_refreshMonitor.proceed(); + } + } + } + + /** + * Use this monitor when its desired to wait for the completion of the + * asynchronous PackageAdmin.refreshPackages() call. + */ + private static class RefreshPackagesMonitor { + private static final int REFRESH_TIMEOUT = 10000; + + private volatile boolean m_alreadyNotified = false; + + /** + * Waits for the completion of the PackageAdmin.refreshPackages() call. + * Because its not sure whether all OSGi framework implementations + * implement this method as specified we have build in a timeout. So if + * a event about the completion of the refreshpackages() is never + * received, we continue after the timeout whether the refresh was done + * or not. + */ + public synchronized void waitForRefresh() { + if (!m_alreadyNotified) { + try { + wait(REFRESH_TIMEOUT); + } + catch (InterruptedException ie) {} + finally { + m_alreadyNotified = false; + } + } + else { + // just reset the misted notification variable, this Monitor + // object might be reused. + m_alreadyNotified = false; + } + } + + /** + * After a PACKAGES_REFRESHED event notify all the parties interested in + * the completion of the PackageAdmin.refreshPackages() call. + */ + public synchronized void proceed() { + m_alreadyNotified = true; + notifyAll(); + } + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/StartCustomizerCommand.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/StartCustomizerCommand.java new file mode 100644 index 00000000000..610e9ab2695 --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/StartCustomizerCommand.java @@ -0,0 +1,97 @@ +/* + * 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.spi; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.apache.felix.deploymentadmin.AbstractDeploymentPackage; +import org.apache.felix.deploymentadmin.BundleInfoImpl; +import org.osgi.framework.Bundle; +import org.osgi.service.deploymentadmin.DeploymentException; +import org.osgi.service.log.LogService; + +/** + * Command that starts all customizer bundles defined in the source deployment + * packages of a deployment session. In addition all customizer bundles of the + * target deployment package that are not present in the source deployment + * package are started as well. + */ +public class StartCustomizerCommand extends Command { + + protected void doExecute(DeploymentSessionImpl session) throws Exception { + AbstractDeploymentPackage target = session.getTargetAbstractDeploymentPackage(); + AbstractDeploymentPackage source = session.getSourceAbstractDeploymentPackage(); + + Set bundles = new HashSet(); + Set sourceBundlePaths = new HashSet(); + + BundleInfoImpl[] targetInfos = target.getBundleInfoImpls(); + BundleInfoImpl[] sourceInfos = source.getBundleInfoImpls(); + for (int i = 0; i < sourceInfos.length; i++) { + if (sourceInfos[i].isCustomizer()) { + sourceBundlePaths.add(sourceInfos[i].getPath()); + Bundle bundle = source.getBundle(sourceInfos[i].getSymbolicName()); + if (bundle != null) { + bundles.add(bundle); + } + } + } + + for (int i = 0; i < targetInfos.length; i++) { + if (targetInfos[i].isCustomizer() && !sourceBundlePaths.contains(targetInfos[i].getPath())) { + Bundle bundle = target.getBundle(targetInfos[i].getSymbolicName()); + if (bundle != null) { + bundles.add(bundle); + } + } + } + + for (Iterator i = bundles.iterator(); i.hasNext();) { + Bundle bundle = (Bundle) i.next(); + try { + bundle.start(); + } + catch (Exception be) { + throw new DeploymentException(CODE_OTHER_ERROR, "Could not start customizer bundle '" + bundle.getSymbolicName() + "'", be); + } + addRollback(new StopCustomizerRunnable(session, bundle)); + } + } + + private static class StopCustomizerRunnable extends AbstractAction { + private final DeploymentSessionImpl m_session; + + private final Bundle m_bundle; + + public StopCustomizerRunnable(DeploymentSessionImpl session, Bundle bundle) { + m_session = session; + m_bundle = bundle; + } + + protected void doRun() throws Exception { + m_bundle.stop(); + } + + protected void onFailure(Exception e) { + m_session.getLog().log(LogService.LOG_WARNING, "Failed to stop bundle '" + m_bundle.getSymbolicName() + "'", e); + } + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/StopBundleCommand.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/StopBundleCommand.java new file mode 100644 index 00000000000..1991ce57863 --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/StopBundleCommand.java @@ -0,0 +1,124 @@ +/* + * 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.spi; + +import org.apache.felix.deploymentadmin.AbstractDeploymentPackage; +import org.apache.felix.deploymentadmin.BundleInfoImpl; +import org.osgi.framework.Bundle; +import org.osgi.service.deploymentadmin.BundleInfo; +import org.osgi.service.deploymentadmin.DeploymentException; +import org.osgi.service.log.LogService; + +/** + * Command that stops all bundles described in the target deployment package of + * a deployment session. + * + * By spec every single bundle of the target package should be stopped, even if + * this is not strictly necessary because of bundles being unaffected by an + * update. To be able to skip the stopping of unaffected bundles the following + * system property can be defined: + * org.apache.felix.deploymentadmin.stopunaffectedbundle. If this + * property has value false (case insensitive) then unaffected + * bundles will not be stopped, in all other cases the bundles will be stopped + * according to the OSGi specification. + */ +public class StopBundleCommand extends Command { + + protected void doExecute(DeploymentSessionImpl session) throws Exception { + LogService log = session.getLog(); + + AbstractDeploymentPackage target = session.getTargetAbstractDeploymentPackage(); + BundleInfo[] bundleInfos = target.getOrderedBundleInfos(); + for (int i = 0; i < bundleInfos.length; i++) { + if (isCancelled()) { + throw new DeploymentException(CODE_CANCELLED); + } + + String symbolicName = bundleInfos[i].getSymbolicName(); + Bundle bundle = target.getBundle(symbolicName); + if (bundle != null) { + if (omitBundleStop(session, symbolicName)) { + continue; + } + if (isFragmentBundle(bundle)) { + log.log(LogService.LOG_INFO, "Skipping fragment bundle '" + symbolicName + "'"); + } + else { + addRollback(new StartBundleRunnable(session, bundle)); + try { + bundle.stop(); + } + catch (Exception e) { + log.log(LogService.LOG_WARNING, "Could not stop bundle '" + symbolicName + "'", e); + } + } + } + else { + log.log(LogService.LOG_WARNING, "Could not stop bundle '" + symbolicName + "' because it was not present in the framework"); + } + } + } + + /** + * Determines whether stopping a bundle is strictly needed. + * + * @param session The current deployment session. + * @param symbolicName The symbolic name of the bundle to inspect. + * + * @return Returns true if + * Constants.DEPLOYMENTPACKAGE_MISSING is true for the + * specified bundle in the source deployment package or if the + * version of the bundle is the same in both source and target + * deployment package. Returns false otherwise. + */ + private boolean omitBundleStop(DeploymentSessionImpl session, String symbolicName) { + boolean stopUnaffectedBundles = session.getConfiguration().isStopUnaffectedBundles(); + if (stopUnaffectedBundles) { + // Default behavior: stop all bundles (see spec)... + return false; + } + + BundleInfoImpl sourceBundleInfo = session.getSourceAbstractDeploymentPackage().getBundleInfoByName(symbolicName); + BundleInfoImpl targetBundleInfo = session.getTargetAbstractDeploymentPackage().getBundleInfoByName(symbolicName); + + boolean fixPackageMissing = sourceBundleInfo != null && sourceBundleInfo.isMissing(); + boolean sameVersion = (targetBundleInfo != null && sourceBundleInfo != null && targetBundleInfo.getVersion().equals(sourceBundleInfo.getVersion())); + + return (fixPackageMissing || sameVersion); + } + + private static class StartBundleRunnable extends AbstractAction { + private final DeploymentSessionImpl m_session; + + private final Bundle m_bundle; + + public StartBundleRunnable(DeploymentSessionImpl session, Bundle bundle) { + m_session = session; + m_bundle = bundle; + } + + protected void doRun() throws Exception { + m_bundle.start(); + } + + protected void onFailure(Exception e) { + m_session.getLog().log(LogService.LOG_WARNING, "Failed to start bundle '" + m_bundle.getSymbolicName() + "'", e); + } + } +} diff --git a/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/UpdateCommand.java b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/UpdateCommand.java new file mode 100644 index 00000000000..bda044958a1 --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/main/java/org/apache/felix/deploymentadmin/spi/UpdateCommand.java @@ -0,0 +1,203 @@ +/* + * 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.spi; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.deploymentadmin.AbstractDeploymentPackage; +import org.apache.felix.deploymentadmin.AbstractInfo; +import org.apache.felix.deploymentadmin.BundleInfoImpl; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Version; +import org.osgi.service.deploymentadmin.DeploymentException; +import org.osgi.service.log.LogService; + +/** + * Command that installs all bundles described in the source deployment package + * of a deployment session. If a bundle was already defined in the target + * deployment package of the same session it is updated, otherwise the bundle is + * simply installed. + */ +public class UpdateCommand extends Command { + + protected void doExecute(DeploymentSessionImpl session) throws Exception { + AbstractDeploymentPackage source = session.getSourceAbstractDeploymentPackage(); + AbstractDeploymentPackage targetPackage = session.getTargetAbstractDeploymentPackage(); + BundleContext context = session.getBundleContext(); + LogService log = session.getLog(); + + Map expectedBundles = new HashMap(); + AbstractInfo[] bundleInfos = (AbstractInfo[]) source.getBundleInfos(); + for (int i = 0; i < bundleInfos.length; i++) { + AbstractInfo bundleInfo = bundleInfos[i]; + if (!bundleInfo.isMissing()) { + expectedBundles.put(bundleInfo.getPath(), bundleInfo); + } + } + + try { + while (!expectedBundles.isEmpty()) { + AbstractInfo entry = source.getNextEntry(); + if (entry == null) { + throw new DeploymentException(CODE_OTHER_ERROR, "Expected more bundles in the stream: " + expectedBundles.keySet()); + } + + String name = entry.getPath(); + BundleInfoImpl bundleInfo = (BundleInfoImpl) expectedBundles.remove(name); + if (bundleInfo == null) { + if (isLocalizationFile(name)) { + // FELIX-518: do not try to process signature or localization files... + continue; + } + throw new DeploymentException(CODE_OTHER_ERROR, "Resource '" + name + "' is not described in the manifest."); + } + + String bsn = bundleInfo.getSymbolicName(); + Version sourceVersion = bundleInfo.getVersion(); + + Bundle bundle = targetPackage.getBundle(bsn); + try { + if (bundle == null) { + // new bundle, install it + bundle = context.installBundle(BUNDLE_LOCATION_PREFIX + bsn, new BundleInputStream(source.getCurrentEntryStream())); + addRollback(new UninstallBundleRunnable(bundle, log)); + } + else { + // existing bundle, update it + Version currentVersion = getVersion(bundle); + if (!sourceVersion.equals(currentVersion)) { + bundle.update(new BundleInputStream(source.getCurrentEntryStream())); + addRollback(new UpdateBundleRunnable(bundle, targetPackage, log)); + } + } + } + catch (Exception be) { + if (isCancelled()) { + return; + } + throw new DeploymentException(CODE_OTHER_ERROR, "Could not install new bundle '" + name + "' (" + bsn + ")", be); + } + + if (!bundle.getSymbolicName().equals(bsn)) { + throw new DeploymentException(CODE_BUNDLE_NAME_ERROR, "Installed/updated bundle symbolicname (" + bundle.getSymbolicName() + ") do not match what was installed/updated: " + bsn); + } + + Version targetVersion = getVersion(bundle); + if (!sourceVersion.equals(targetVersion)) { + throw new DeploymentException(CODE_OTHER_ERROR, + "Installed/updated bundle version (" + targetVersion + ") do not match what was installed/updated: " + sourceVersion + ", offending bundle = " + bsn); + } + } + } + catch (IOException e) { + throw new DeploymentException(CODE_OTHER_ERROR, "Problem while reading stream", e); + } + } + + private Version getVersion(Bundle bundle) { + return Version.parseVersion((String) bundle.getHeaders().get(BUNDLE_VERSION)); + } + + private boolean isLocalizationFile(String name) { + return name.startsWith("OSGI-INF/l10n/"); + } + + private static class UninstallBundleRunnable extends AbstractAction { + private final Bundle m_bundle; + private final LogService m_log; + + public UninstallBundleRunnable(Bundle bundle, LogService log) { + m_bundle = bundle; + m_log = log; + } + + protected void doRun() throws Exception { + m_bundle.uninstall(); + } + + protected void onFailure(Exception e) { + m_log.log(LogService.LOG_WARNING, "Could not rollback update of bundle '" + m_bundle.getSymbolicName() + "'", e); + } + } + + private static class UpdateBundleRunnable extends AbstractAction { + private final AbstractDeploymentPackage m_targetPackage; + private final Bundle m_bundle; + private final LogService m_log; + + public UpdateBundleRunnable(Bundle bundle, AbstractDeploymentPackage targetPackage, LogService log) { + m_bundle = bundle; + m_targetPackage = targetPackage; + m_log = log; + } + + protected void doRun() throws Exception { + InputStream is = null; + try { + is = m_targetPackage.getBundleStream(m_bundle.getSymbolicName()); + if (is != null) { + m_bundle.update(is); + } + else { + throw new RuntimeException("Unable to get inputstream for bundle " + m_bundle.getSymbolicName()); + } + } + finally { + closeSilently(is); + } + } + + protected void onFailure(Exception e) { + m_log.log(LogService.LOG_WARNING, "Could not rollback update of bundle '" + m_bundle.getSymbolicName() + "'", e); + } + } + + private final class BundleInputStream extends InputStream { + private final InputStream m_inputStream; + + private BundleInputStream(InputStream jarInputStream) { + m_inputStream = jarInputStream; + } + + public int read() throws IOException { + checkCancel(); + return m_inputStream.read(); + } + + public int read(byte[] buffer) throws IOException { + checkCancel(); + return m_inputStream.read(buffer); + } + + public int read(byte[] buffer, int off, int len) throws IOException { + checkCancel(); + return m_inputStream.read(buffer, off, len); + } + + private void checkCancel() throws IOException { + if (isCancelled()) { + throw new IOException("Stream was cancelled"); + } + } + } +} diff --git a/deploymentadmin/deploymentadmin/src/test/java/org/apache/felix/deploymentadmin/ContentCopyingJarInputStreamTest.java b/deploymentadmin/deploymentadmin/src/test/java/org/apache/felix/deploymentadmin/ContentCopyingJarInputStreamTest.java new file mode 100644 index 00000000000..d2354a317a9 --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/test/java/org/apache/felix/deploymentadmin/ContentCopyingJarInputStreamTest.java @@ -0,0 +1,257 @@ +/* + * 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.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.GZIPInputStream; +import java.util.zip.ZipEntry; + +import junit.framework.TestCase; + +/** + * Test cases for {@link ContentCopyingJarInputStream}. + */ +public class ContentCopyingJarInputStreamTest extends TestCase +{ + private static final String MANIFEST_NAME = JarFile.MANIFEST_NAME; + private static final String INDEX_NAME = "META-INF/INDEX.LIST"; + + private File m_tempDir; + private File m_jarFile; + + /** + * Tests that we can copy a {@link JarInputStream} containing only a manifest. + */ + public void testCopyEmptyJarWithManifestOnlyOk() throws Exception + { + Manifest man = createManifest(); + + createEmptyJar(man); + + assertJarContents(man); + } + + /** + * Tests that we can copy a simple {@link JarInputStream}. + */ + public void testCopyJarWithIndexAndWithManifestOk() throws Exception + { + Manifest man = createManifest(); + + createJar(man, true /* includeIndex */); + + assertJarContents(man); + } + + /** + * Tests that we can copy a {@link JarInputStream} even if it does not contains a manifest file. + */ + public void testCopyJarWithIndexAndWithoutManifestOk() throws Exception + { + Manifest man = null; + + createJar(man, true /* includeIndex */); + + assertJarContents(man); + } + + /** + * Tests that we can copy a simple {@link JarInputStream}. + */ + public void testCopyJarWithoutIndexAndWithManifestOk() throws Exception + { + Manifest man = createManifest(); + + createJar(man, false /* includeIndex */); + + assertJarContents(man); + } + + /** + * Tests that we can copy a {@link JarInputStream} even if it does not contains a manifest file. + */ + public void testCopyJarWithoutIndexAndWithoutManifestOk() throws Exception + { + Manifest man = null; + + createJar(man, false /* includeIndex */); + + assertJarContents(man); + } + + protected void setUp() throws IOException + { + m_tempDir = createTempDir(); + m_jarFile = new File(m_tempDir, "input.jar"); + } + + protected void tearDown() + { + Utils.delete(m_tempDir, true); + } + + private void appendFiles(JarOutputStream jos, int count) throws IOException + { + int size = 1024; + + for (int i = 0, j = 1; i < count; i++, j++) + { + JarEntry entry = new JarEntry("sub/" + j); + jos.putNextEntry(entry); + for (int k = 0; k < size; k++) + { + jos.write('0' + j); + } + jos.closeEntry(); + } + } + + private void assertJarContents(Manifest man) throws IOException + { + File indexFile = new File(m_tempDir, "index.txt"); + + FileInputStream fis = new FileInputStream(m_jarFile); + JarInputStream jis = new ContentCopyingJarInputStream(fis, indexFile, m_tempDir); + + try + { + JarEntry entry; + while ((entry = jis.getNextJarEntry()) != null) + { + File f = new File(m_tempDir, entry.getName()); + + // Without reading the actual contents, the copy should already exist... + assertTrue(entry.getName() + " does not exist?!", f.exists()); + + int size = (INDEX_NAME.equals(entry.getName()) ? 33 : 1024); + + byte[] input = new byte[size]; + int read = jis.read(input); + + assertEquals("Not all bytes were read: " + entry.getName(), size, read); + + // Contents will only be completely written after closing the JAR entry itself... + jis.closeEntry(); + + verifyContents(f, input); + } + + assertEquals("Manifest not as expected", man, jis.getManifest()); + } + finally + { + jis.close(); + } + } + + private void createEmptyJar(Manifest man) throws IOException + { + FileOutputStream fos = new FileOutputStream(m_jarFile); + JarOutputStream jos = new JarOutputStream(fos, man); + jos.close(); + } + + private void createJar(Manifest man, boolean includeIndex) throws IOException + { + FileOutputStream fos = new FileOutputStream(m_jarFile); + JarOutputStream jos; + + if (man == null || includeIndex) + { + jos = new JarOutputStream(fos); + } + else + { + jos = new JarOutputStream(fos, man); + } + + if (includeIndex) + { + // Write the INDEX.LIST file as first entry... + jos.putNextEntry(new ZipEntry(INDEX_NAME)); + jos.write(("JarIndex-Version: 1.0\n\n" + m_jarFile.getName() + "\n").getBytes()); + jos.closeEntry(); + + if (man != null) + { + jos.putNextEntry(new ZipEntry(MANIFEST_NAME)); + man.write(jos); + jos.closeEntry(); + } + } + + try + { + appendFiles(jos, 5); + } + finally + { + jos.close(); + } + } + + private Manifest createManifest() + { + Manifest mf = new Manifest(); + mf.getMainAttributes().putValue("Manifest-Version", "1.0"); + mf.getMainAttributes().putValue("Bundle-ManifestVersion", "2"); + mf.getMainAttributes().putValue("Bundle-Version", "1.0.0"); + mf.getMainAttributes().putValue("Bundle-SymbolicName", "com.foo.bar"); + return mf; + } + + private File createTempDir() throws IOException + { + File tmpFile = File.createTempFile("ccjis_test", null); + tmpFile.delete(); + tmpFile.mkdir(); + return tmpFile; + } + + private void verifyContents(File file, byte[] expected) throws IOException + { + FileInputStream fis = new FileInputStream(file); + GZIPInputStream gis = new GZIPInputStream(fis); + try + { + byte[] b = new byte[expected.length]; + + int read = gis.read(b); + assertEquals(b.length, read); + + for (int i = 0; i < expected.length; i++) + { + assertEquals(expected[i], b[i]); + } + } + finally + { + gis.close(); + fis.close(); + } + } +} diff --git a/deploymentadmin/deploymentadmin/src/test/java/org/apache/felix/deploymentadmin/DeploymentAdminConfigTest.java b/deploymentadmin/deploymentadmin/src/test/java/org/apache/felix/deploymentadmin/DeploymentAdminConfigTest.java new file mode 100644 index 00000000000..f2040e40d40 --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/test/java/org/apache/felix/deploymentadmin/DeploymentAdminConfigTest.java @@ -0,0 +1,139 @@ +/* + * 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.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +import org.mockito.Matchers; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.osgi.framework.BundleContext; +import org.osgi.service.cm.ConfigurationException; + +/** + * Test cases for {@link DeploymentAdminConfig}. + */ +public class DeploymentAdminConfigTest extends TestCase { + private static final String KEY_STOP_UNAFFECTED_BUNDLE = DeploymentAdminConfig.KEY_STOP_UNAFFECTED_BUNDLE; + private static final String KEY_STOP_UNAFFECTED_BUNDLES = DeploymentAdminConfig.KEY_STOP_UNAFFECTED_BUNDLES; + private static final String KEY_ALLOW_FOREIGN_CUSTOMIZERS = DeploymentAdminConfig.KEY_ALLOW_FOREIGN_CUSTOMIZERS; + + private static final boolean DEFAULT_STOP_UNAFFECTED_BUNDLES = DeploymentAdminConfig.DEFAULT_STOP_UNAFFECTED_BUNDLES; + private static final boolean DEFAULT_ALLOW_FOREIGN_CUSTOMIZERS = DeploymentAdminConfig.DEFAULT_ALLOW_FOREIGN_CUSTOMIZERS; + + private final Map m_fwProperties = new HashMap(); + + /** + * Tests the configuration values of {@link DeploymentAdminImpl} without any explicit configuration. + */ + public void testDefaultConfigurationOk() throws ConfigurationException { + DeploymentAdminConfig config = createDeploymentAdminConfig(); + + assertEquals(DEFAULT_STOP_UNAFFECTED_BUNDLES, config.isStopUnaffectedBundles()); + assertEquals(DEFAULT_ALLOW_FOREIGN_CUSTOMIZERS, config.isAllowForeignCustomizers()); + } + + /** + * Tests the configuration values of {@link DeploymentAdminImpl} without any explicit configuration. + */ + public void testFrameworkConfigurationOk() throws ConfigurationException { + m_fwProperties.put(KEY_STOP_UNAFFECTED_BUNDLES, Boolean.toString(!DEFAULT_STOP_UNAFFECTED_BUNDLES)); + m_fwProperties.put(KEY_ALLOW_FOREIGN_CUSTOMIZERS, Boolean.toString(!DEFAULT_ALLOW_FOREIGN_CUSTOMIZERS)); + + DeploymentAdminConfig config = createDeploymentAdminConfig(); + + assertEquals(!DEFAULT_STOP_UNAFFECTED_BUNDLES, config.isStopUnaffectedBundles()); + assertEquals(!DEFAULT_ALLOW_FOREIGN_CUSTOMIZERS, config.isAllowForeignCustomizers()); + } + + /** + * Tests the configuration values of {@link DeploymentAdminImpl} without any explicit configuration. + */ + public void testFrameworkConfigurationDeprecatedKeyOk() throws ConfigurationException { + m_fwProperties.put(KEY_STOP_UNAFFECTED_BUNDLE, Boolean.toString(!DEFAULT_STOP_UNAFFECTED_BUNDLES)); + + DeploymentAdminConfig config = createDeploymentAdminConfig(); + + assertEquals(!DEFAULT_STOP_UNAFFECTED_BUNDLES, config.isStopUnaffectedBundles()); + } + + /** + * Tests the configuration values of {@link DeploymentAdminImpl} without any explicit configuration. + */ + public void testSystemConfigurationOk() throws ConfigurationException { + String stopUnaffectedBundle = KEY_STOP_UNAFFECTED_BUNDLES; + String allowForeignCustomizers = KEY_ALLOW_FOREIGN_CUSTOMIZERS; + + System.setProperty(stopUnaffectedBundle, Boolean.toString(!DEFAULT_STOP_UNAFFECTED_BUNDLES)); + System.setProperty(allowForeignCustomizers, Boolean.toString(!DEFAULT_ALLOW_FOREIGN_CUSTOMIZERS)); + + try { + DeploymentAdminConfig config = createDeploymentAdminConfig(); + + assertEquals(!DEFAULT_STOP_UNAFFECTED_BUNDLES, config.isStopUnaffectedBundles()); + assertEquals(!DEFAULT_ALLOW_FOREIGN_CUSTOMIZERS, config.isAllowForeignCustomizers()); + } + finally { + System.clearProperty(stopUnaffectedBundle); + System.clearProperty(allowForeignCustomizers); + } + + System.setProperty(stopUnaffectedBundle.toLowerCase(), Boolean.toString(!DEFAULT_STOP_UNAFFECTED_BUNDLES)); + System.setProperty(allowForeignCustomizers.toLowerCase(), Boolean.toString(!DEFAULT_ALLOW_FOREIGN_CUSTOMIZERS)); + + try { + DeploymentAdminConfig config = createDeploymentAdminConfig(); + + assertEquals(!DEFAULT_STOP_UNAFFECTED_BUNDLES, config.isStopUnaffectedBundles()); + assertEquals(!DEFAULT_ALLOW_FOREIGN_CUSTOMIZERS, config.isAllowForeignCustomizers()); + } + finally { + System.clearProperty(stopUnaffectedBundle.toLowerCase()); + System.clearProperty(allowForeignCustomizers.toLowerCase()); + } + } + + protected void setUp() throws Exception { + m_fwProperties.clear(); + } + + private DeploymentAdminConfig createDeploymentAdminConfig() throws ConfigurationException { + return new DeploymentAdminConfig(createMockBundleContext()); + } + + private BundleContext createMockBundleContext() { + BundleContext result = (BundleContext) Mockito.mock(BundleContext.class); + Mockito.when(result.getProperty(Matchers.anyString())).thenAnswer(new Answer() { + public Object answer(InvocationOnMock invocation) throws Throwable { + String prop = (String) invocation.getArguments()[0]; + + Object result = m_fwProperties.get(prop); + if (result == null) { + result = System.getProperty(prop); + } + return result; + } + }); + return result; + } +} diff --git a/deploymentadmin/deploymentadmin/src/test/java/org/apache/felix/deploymentadmin/spi/SnapshotCommandTest.java b/deploymentadmin/deploymentadmin/src/test/java/org/apache/felix/deploymentadmin/spi/SnapshotCommandTest.java new file mode 100644 index 00000000000..87b27d539b3 --- /dev/null +++ b/deploymentadmin/deploymentadmin/src/test/java/org/apache/felix/deploymentadmin/spi/SnapshotCommandTest.java @@ -0,0 +1,229 @@ +/* + * 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.spi; + +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import junit.framework.TestCase; + +import org.apache.felix.deploymentadmin.Utils; + +/** + * Test cases for {@link SnapshotCommand}. + * + * @author Felix Project Team + */ +public class SnapshotCommandTest extends TestCase { + private final List m_cleanup = new ArrayList(); + + /** + * Tests that an archive can be correctly restored. + *

      + * This tests FELIX-4719. + *

      + */ + public void testRestoreArchiveOk() throws Exception { + // Set up a file-hierarchy we can archive... + File baseDir = createFileHierarchy(); + + File archiveFile = new File(baseDir, "../archive.zip"); + m_cleanup.add(archiveFile.getCanonicalFile()); + + SnapshotCommand.store(baseDir, archiveFile); + assertTrue("Archive not created?!", archiveFile.exists()); + + File targetDir = createTempDir(); + SnapshotCommand.restore(archiveFile, targetDir); + + verifyDirContents(baseDir, targetDir); + } + + /** + * Tests that a directory (data-area) is correctly archived, and that the contents of that archive are as expected. + *

      + * This tests FELIX-4718. + *

      + */ + public void testStoreDataAreaOk() throws Exception { + // Set up a file-hierarchy we can archive... + File baseDir = createFileHierarchy(); + + File archiveFile = new File(baseDir, "../archive.zip"); + m_cleanup.add(archiveFile.getCanonicalFile()); + + SnapshotCommand.store(baseDir, archiveFile); + assertTrue("Archive not created?!", archiveFile.exists()); + + verifyArchiveContents(archiveFile, 3 /* dirs */, 6 /* files */); + } + + protected void tearDown() throws Exception { + Iterator iter = m_cleanup.iterator(); + while (iter.hasNext()) { + File file = (File) iter.next(); + if (file.isFile()) { + file.delete(); + } + else if (file.isDirectory()) { + Utils.delete(file, true /* deleteRoot */); + } + iter.remove(); + } + } + + private void close(Closeable resource) throws IOException { + if (resource != null) { + resource.close(); + } + } + + private void createFile(File file, int size) throws IOException { + assertTrue(file.createNewFile()); + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file); + + byte[] data = new byte[size]; + new Random().nextBytes(data); + + fos.write(data); + } + finally { + close(fos); + } + } + + /** + * Creates a simple file hierarchy with a couple of files and directories (6 files and 3 directories). + * + * @return the base directory where the file hierarchy is stored, never null. + */ + private File createFileHierarchy() throws IOException { + File base = createTempDir(); + + File dir1 = new File(base, "dir1"); + assertTrue(dir1.mkdir()); + + File dir2 = new File(dir1, "dir1a"); + assertTrue(dir2.mkdir()); + + File dir3 = new File(dir1, "dir1b"); + assertTrue(dir3.mkdir()); + + createFile(new File(base, "file1"), 1024); + createFile(new File(dir1, "file2"), 2048); + createFile(new File(dir2, "file3"), 4096); + createFile(new File(base, "file4"), 8192); + createFile(new File(dir2, "file5"), 16384); + createFile(new File(dir3, "file6"), 32768); + + return base; + } + + private File createTempDir() throws IOException { + File dir = File.createTempFile("felix4718-", ""); + assertTrue(dir.delete()); + assertTrue(dir.mkdirs()); + // For test cleanup... + m_cleanup.add(dir); + return dir; + } + + private void verifyArchiveContents(File archive, int expectedDirCount, int expectedFileCount) throws IOException { + FileInputStream fis = null; + ZipInputStream zis = null; + + try { + fis = new FileInputStream(archive); + zis = new ZipInputStream(fis); + + int dirCount = 0; + int fileCount = 0; + + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.isDirectory()) { + dirCount++; + } + else { + fileCount++; + } + zis.closeEntry(); + } + + assertEquals("Unexpected number of files", expectedFileCount, fileCount); + assertEquals("Unexpected number of directories", expectedDirCount, dirCount); + } + finally { + close(fis); + close(zis); + } + } + + private void verifyDirContents(File expectedBase, File actualBase) throws IOException { + String[] expectedFiles = expectedBase.list(); + for (int i = 0; i < expectedFiles.length; i++) { + File expected = new File(expectedBase, expectedFiles[i]); + File actual = new File(actualBase, expectedFiles[i]); + + if (expected.isDirectory()) { + assertTrue("Directory '" + expectedFiles[i] + "' does not exist in " + actualBase, actual.isDirectory()); + verifyDirContents(expected, actual); + } + else if (expected.isFile()) { + assertTrue("File '" + expectedFiles[i] + "' does not exist in " + actualBase, actual.isFile()); + verifyFileContents(expected, actual); + } + else { + fail("Unknown entity: '" + expectedFiles[i] + "'"); + } + } + } + + private void verifyFileContents(File expected, File actual) throws IOException { + assertEquals("File size mismatch!", expected.length(), actual.length()); + + FileInputStream fis1 = null; + FileInputStream fis2 = null; + try { + fis1 = new FileInputStream(expected); + fis2 = new FileInputStream(actual); + + int eb; + while ((eb = fis1.read()) != -1) { + int ab = fis2.read(); + assertEquals(eb, ab); + } + } + finally { + close(fis2); + close(fis1); + } + } +} diff --git a/deploymentadmin/itest/pom.xml b/deploymentadmin/itest/pom.xml new file mode 100644 index 00000000000..5a0296d47ac --- /dev/null +++ b/deploymentadmin/itest/pom.xml @@ -0,0 +1,207 @@ + + + + 4.0.0 + + org.apache.felix + felix-parent + 3 + ../../pom/pom.xml + + + 4.2.0 + 3.6.0 + 1.6.0 + + Apache Felix DeploymentAdmin Integration Tests + 0.1.1-SNAPSHOT + org.apache.felix.deploymentadmin.itest + jar + + + + org.apache.felix + org.apache.felix.framework + 4.4.1 + test + + + + org.osgi + org.osgi.compendium + ${osgi.version} + test + + + + org.apache.felix + org.apache.felix.dependencymanager + 4.1.1 + test + + + org.apache.felix + org.apache.felix.deploymentadmin + 0.9.8-SNAPSHOT + test + + + org.apache.felix + org.apache.felix.metatype + 1.1.2 + test + + + org.apache.felix + org.apache.felix.configadmin + 1.8.0 + test + + + org.apache.felix + org.apache.felix.eventadmin + 1.3.2 + test + + + org.apache.felix + org.apache.felix.log + 1.0.1 + test + + + junit + junit + test + + + commons-codec + commons-codec + 1.10 + test + + + + org.ops4j.pax.exam + pax-exam-junit4 + ${pax.exam.version} + test + + + org.ops4j.pax.exam + pax-exam-container-native + ${pax.exam.version} + test + + + org.ops4j.pax.exam + pax-exam-link-mvn + ${pax.exam.version} + test + + + org.ops4j.pax.url + pax-url-aether + ${pax.url.version} + test + + + + org.bouncycastle + bcprov-jdk15on + 1.54 + test + + + org.bouncycastle + bcpkix-jdk15on + 1.54 + test + + + ch.qos.logback + logback-core + 1.1.3 + test + + + ch.qos.logback + logback-classic + 1.1.3 + test + + + + + + + org.apache.servicemix.tooling + depends-maven-plugin + + + generate-depends-file + + generate-depends-file + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.7 + 1.7 + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + + org.apache.servicemix.tooling + + + depends-maven-plugin + + + [1.2,) + + + + generate-depends-file + + + + + + + + + + + + + + + diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/BaseIntegrationTest.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/BaseIntegrationTest.java new file mode 100644 index 00000000000..546956a45d5 --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/BaseIntegrationTest.java @@ -0,0 +1,317 @@ +/* + * 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.itest; + +import static org.ops4j.pax.exam.CoreOptions.*; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.Security; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.inject.Inject; + +import junit.framework.TestCase; + +import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.After; +import org.junit.Before; +import org.ops4j.pax.exam.Configuration; +import org.ops4j.pax.exam.Option; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.Version; +import org.osgi.framework.wiring.FrameworkWiring; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.deploymentadmin.DeploymentAdmin; +import org.osgi.service.deploymentadmin.DeploymentException; +import org.osgi.service.deploymentadmin.DeploymentPackage; +import org.osgi.util.tracker.ServiceTracker; + +/** + * Provides a common base class for all deployment admin related integration tests. + */ +public abstract class BaseIntegrationTest extends TestCase { + + protected static final int DEFAULT_TIMEOUT = 10000; + + protected static final String TEST_SERVICE_NAME = "org.apache.felix.deploymentadmin.test.bundle1.TestService"; + protected static final String TEST_FAILING_BUNDLE_RP1 = "org.apache.felix.deploymentadmin.test.rp1"; + + @Inject + protected volatile BundleContext m_context; + @Inject + protected volatile DeploymentAdmin m_deploymentAdmin; + @Inject + protected volatile ConfigurationAdmin m_configAdmin; + + protected volatile AtomicInteger m_gate = new AtomicInteger(0); + protected volatile String m_testBundleBasePath; + protected volatile Map> m_initialBundles; + + private int cnt = 0; + + @Configuration + public Option[] config() throws Exception { + return options(bootDelegationPackage("sun.*"), systemProperty("org.ops4j.pax.logging.DefaultServiceLog.level").value("ERROR"), + + mavenBundle("org.apache.felix", "org.apache.felix.metatype").versionAsInProject(), + mavenBundle("org.apache.felix", "org.apache.felix.dependencymanager").versionAsInProject(), + mavenBundle("org.apache.felix", "org.apache.felix.deploymentadmin").versionAsInProject(), + mavenBundle("org.apache.felix", "org.apache.felix.eventadmin").versionAsInProject(), + mavenBundle("org.apache.felix", "org.apache.felix.configadmin").versionAsInProject(), + mavenBundle("commons-codec", "commons-codec").versionAsInProject(), + mavenBundle("org.bouncycastle", "bcprov-jdk15on").versionAsInProject(), + mavenBundle("org.bouncycastle", "bcpkix-jdk15on").versionAsInProject(), + + junitBundles()); + } + + @Before + public void setUp() throws Exception { + assertNotNull("No bundle context?!", m_context); + + File f = new File("../testbundles").getAbsoluteFile(); + assertTrue("Failed to find test bundles directory?!", f.exists() && f.isDirectory()); + + m_testBundleBasePath = f.getAbsolutePath(); + + m_context.addFrameworkListener(new FrameworkListener() { + public void frameworkEvent(FrameworkEvent event) { + if (event.getType() == FrameworkEvent.PACKAGES_REFRESHED) { + m_gate.getAndIncrement(); + } + } + }); + + m_initialBundles = new HashMap>(); + + for (Bundle bundle : m_context.getBundles()) { + List versions = m_initialBundles.get(bundle.getSymbolicName()); + if (versions == null) { + versions = new ArrayList(); + m_initialBundles.put(bundle.getSymbolicName(), versions); + } + versions.add(bundle.getVersion()); + } + + Security.addProvider(new BouncyCastleProvider()); + } + + @After + public void tearDown() throws Exception { + System.setProperty("rp1", ""); + System.setProperty("bundle3", ""); + + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); + } + + protected void assertBundleExists(String symbolicName, String version) { + boolean result = isBundleAdded(symbolicName, version); + if (!result) { + fail("Bundle " + symbolicName + ", v" + version + " does not exist?!\nCurrent additional bundles are: " + getCurrentBundles()); + } + } + + protected void assertBundleNotExists(String symbolicName, String version) { + boolean result = isBundleAdded(symbolicName, version); + if (result) { + fail("Bundle " + symbolicName + ", v" + version + " does (still) exist?!\nCurrent additional bundles are: " + getCurrentBundles()); + } + } + + protected void assertDeploymentException(int expectedCode, DeploymentException exception) { + assertEquals("Invalid exception code?!\nException = " + exception, expectedCode, exception.getCode()); + } + + protected void awaitRefreshPackagesEvent() throws Exception { + long start = System.currentTimeMillis(); + while ((m_gate.get() == 0) && ((System.currentTimeMillis() - start) < DEFAULT_TIMEOUT)) { + TimeUnit.MILLISECONDS.sleep(100); + } + assertTrue("Failed to obtain refresh packages event?! " + m_gate.get(), m_gate.get() > 0); + m_gate.set(0); + } + + protected T awaitService(String serviceName) throws Exception { + ServiceTracker tracker = new ServiceTracker(m_context, serviceName, null); + tracker.open(); + T result; + try { + result = (T) tracker.waitForService(DEFAULT_TIMEOUT); + } + finally { + tracker.close(); + } + return result; + } + + protected final DeploymentPackage installDeploymentPackage(DeploymentPackageBuilder dpBuilder) throws Exception { + return installDeploymentPackage(dpBuilder.generate()); + } + + protected final DeploymentPackage installDeploymentPackage(InputStream is) throws Exception { + try { + return m_deploymentAdmin.installDeploymentPackage(is); + } + finally { + try { + is.close(); + } + catch (IOException e) { + // Nothing we can do about this, but log it... + e.printStackTrace(); + } + } + } + + protected final int countDeploymentPackages() { + return m_deploymentAdmin.listDeploymentPackages().length; + } + + protected DeploymentPackageBuilder createNewDeploymentPackageBuilder(String version) { + return createDeploymentPackageBuilder(String.format("itest%d", ++cnt), version); + } + + protected DeploymentPackageBuilder createDeploymentPackageBuilder(String symName, String version) { + return DeploymentPackageBuilder.create(symName, version); + } + + protected Map> getCurrentBundles() { + Map> bundles = new HashMap>(); + for (Bundle bundle : m_context.getBundles()) { + String symbolicName = bundle.getSymbolicName(); + Version version = bundle.getVersion(); + + // Is is not part of any of the initially provisioned bundles? + List versions = m_initialBundles.get(symbolicName); + if ((versions == null) || !versions.contains(version)) { + List versions2 = bundles.get(symbolicName); + if (versions2 == null) { + versions2 = new ArrayList(); + bundles.put(symbolicName, versions2); + } + versions2.add(version); + } + } + return bundles; + } + + protected String getSymbolicName(String baseName) { + return "testbundles.".concat(baseName); + } + + protected URL getTestResource(String resourceName) { + if (!resourceName.startsWith("/")) { + resourceName = "/".concat(resourceName); + } + URL resource = getClass().getResource(resourceName); + assertNotNull("No such resource: " + resourceName, resource); + return resource; + } + + protected Bundle getBundle(String bsn) { + for (Bundle b : m_context.getBundles()) { + if (bsn.equals(b.getSymbolicName())) { + return b; + } + } + return null; + } + + protected URL getTestBundleURL(String baseName) throws MalformedURLException { + return getTestBundleURL(baseName, "1.0.0"); + } + + protected URL getTestBundleURL(String baseName, String version) throws MalformedURLException { + return getTestBundleURL(baseName, baseName, version); + } + + protected URL getTestBundleURL(String artifactName, String baseName, String version) throws MalformedURLException { + assertNotNull("Version cannot be null!", version); + File f = new File(m_testBundleBasePath, String.format("%1$s/target/org.apache.felix.deploymentadmin.test.%2$s-%3$s.jar", artifactName, baseName, version)); + assertTrue("No such bundle: " + f, f.exists() && f.isFile()); + return f.toURI().toURL(); + } + + protected boolean isBundleActive(Bundle bundle) { + return isBundleInState(bundle, Bundle.ACTIVE); + } + + protected boolean isBundleAdded(String symbolicName, String version) { + return isBundleAdded(symbolicName, new Version(version)); + } + + protected boolean isBundleAdded(String symbolicName, Version version) { + Map> bundles = getCurrentBundles(); + + List availableVersions = bundles.get(symbolicName); + return (availableVersions != null) && availableVersions.contains(version); + } + + protected boolean isBundleInstalled(Bundle bundle) { + return isBundleInState(bundle, Bundle.INSTALLED); + } + + protected boolean isBundleInState(Bundle bundle, int state) { + return ((bundle.getState() & state) != 0); + } + + protected boolean isBundleRemoved(String symbolicName, String version) { + return isBundleRemoved(symbolicName, new Version(version)); + } + + protected boolean isBundleRemoved(String symbolicName, Version version) { + Map> bundles = getCurrentBundles(); + + List availableVersions = bundles.get(symbolicName); + return (availableVersions == null) || !availableVersions.contains(version); + } + + protected boolean isBundleResolved(Bundle bundle) { + return isBundleInState(bundle, Bundle.RESOLVED); + } + + protected boolean resolveBundles(Bundle... bundles) throws Exception { + Bundle systemBundle = m_context.getBundle(0L); + + FrameworkWiring frameworkWiring = systemBundle.adapt(FrameworkWiring.class); + frameworkWiring.resolveBundles(Arrays.asList(bundles)); + + for (Bundle bundle : bundles) { + if ((bundle.getState() & Bundle.RESOLVED) == 0) { + return false; + } + } + + return true; + } +} diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/CustomizerTest.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/CustomizerTest.java new file mode 100644 index 00000000000..44c94d1cb0c --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/CustomizerTest.java @@ -0,0 +1,254 @@ +/* + * 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.itest; + +import static org.osgi.service.deploymentadmin.DeploymentException.CODE_FOREIGN_CUSTOMIZER; +import static org.osgi.service.deploymentadmin.DeploymentException.CODE_OTHER_ERROR; +import static org.osgi.service.deploymentadmin.DeploymentException.CODE_PROCESSOR_NOT_FOUND; + +import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerMethod; +import org.osgi.service.deploymentadmin.DeploymentException; +import org.osgi.service.deploymentadmin.DeploymentPackage; + +/** + * Provides test cases on the use of customizers in Deployment Admin. + */ +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerMethod.class) +public class CustomizerTest extends BaseIntegrationTest { + + /** + * FELIX-4491 - Only resources with a resource processor defined should be processed. + */ + @Test + public void testInstallBundleWithNonProcessedResourcesOk() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createResource().setNeedResourceProcessor(false).setUrl(getTestResource("LICENSE"))) + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))) + .add(dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + assertEquals("Expected a single deployment package?!", 1, countDeploymentPackages()); + + awaitRefreshPackagesEvent(); + + assertBundleExists(getSymbolicName("bundle1"), "1.0.0"); + + // Check that only the processed resources are dropped... + dp.uninstall(); + + assertEquals("Expected no deployment package?!", 0, countDeploymentPackages()); + + awaitRefreshPackagesEvent(); + + assertBundleNotExists(getSymbolicName("bundle1"), "1.0.0"); + } + + /** + * Tests that if an exception is thrown during the commit-phase, the installation proceeds and succeeds. + */ + @Test + public void testInstallBundleWithExceptionThrowingInCommitCauseNoRollbackOk() throws Exception { + System.setProperty("rp1", "commit"); + + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))) + .add(dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle3"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + // Though the commit failed; the package should be installed... + assertBundleExists(getSymbolicName("rp1"), "1.0.0"); + assertBundleExists(getSymbolicName("bundle3"), "1.0.0"); + + assertEquals("Expected a single deployment package?!", 1, countDeploymentPackages()); + } + + /** + * Tests that if an exception is thrown during the prepare-phase, the installation is cancelled and rolled back. + */ + @Test + public void testInstallBundleWithExceptionThrowingInPrepareCausesRollbackOk() throws Exception { + System.setProperty("rp1", "prepare"); + + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))) + .add(dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml"))); + + try { + installDeploymentPackage(dpBuilder); + fail("Succeeded into installing a failing deployment package?!"); + } + catch (DeploymentException exception) { + // Ok; expected + assertDeploymentException(DeploymentException.CODE_COMMIT_ERROR, exception); + } + + assertTrue("No bundles should be started!", getCurrentBundles().isEmpty()); + + assertEquals("Expected no deployment package?!", 0, countDeploymentPackages()); + } + + /** + * Tests that if an exception is thrown during the processing of a resource, the installation is cancelled and rolled back. + */ + @Test + public void testInstallResourceWithExceptionThrowingInProcessCausesRollbackOk() throws Exception { + System.setProperty("rp1", "process"); + + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))) + .add(dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml"))); + + try { + installDeploymentPackage(dpBuilder); + fail("Succeeded into installing a failing deployment package?!"); + } + catch (DeploymentException exception) { + // Ok; expected + assertDeploymentException(DeploymentException.CODE_RESOURCE_SHARING_VIOLATION, exception); + } + + assertTrue("No bundles should be started!", getCurrentBundles().isEmpty()); + + assertEquals("Expected no deployment package?!", 0, countDeploymentPackages()); + } + + /** + * Tests that if an exception is thrown during the dropping of a resource, the installation is continued and finishes normally. + */ + @Test + public void testDropResourceWithExceptionThrowingInDroppedCausesRollbackOk() throws Exception { + System.setProperty("rp1", "dropped"); + + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))) + .add(dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + assertTrue("One bundle should be started!", getCurrentBundles().size() == 1); + + assertEquals("Expected no deployment package?!", 1, countDeploymentPackages()); + } + + /** + * Tests that if an exception is thrown during the commit-phase, the installation proceeds and succeeds. + */ + @Test + public void testInstallResourceProcessorWithExceptionThrowingInStartCausesRollbackOk() throws Exception { + System.setProperty("rp1", "start"); + + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))) + .add(dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle3"))); + + try { + installDeploymentPackage(dpBuilder); + fail("Succeeded into installing a failing RP?!"); + } + catch (DeploymentException exception) { + // Ok; expected... + assertDeploymentException(CODE_OTHER_ERROR, exception); + } + + assertEquals("Expected no deployment package?!", 0, countDeploymentPackages()); + assertTrue("Expected no artifacts to be installed?!", getCurrentBundles().isEmpty()); + } + + /** + * Tests that if a resource is installed which mentions a RP that does not belong to the same package, a rollback takes place. + */ + @Test + public void testInstallResourceWithForeignCustomizerFail() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))); + + installDeploymentPackage(dpBuilder); + + awaitRefreshPackagesEvent(); + + assertEquals("Expected no deployment package?!", 1, countDeploymentPackages()); + assertBundleExists(getSymbolicName("rp1"), "1.0.0"); + + dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .disableVerification() + .add(dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml"))); + + try { + installDeploymentPackage(dpBuilder); + fail("Succeeded into installing a resource with an non-existing RP?!"); + } + catch (DeploymentException exception) { + // Ok; expected... + assertDeploymentException(CODE_FOREIGN_CUSTOMIZER, exception); + } + + assertEquals("Expected no deployment package?!", 1, countDeploymentPackages()); + assertTrue("Expected no additional artifacts to be installed?!", getCurrentBundles().size() == 1); + } + + /** + * Tests that if a resource is installed which mentions a RP that does not exist a rollback takes place. + */ + @Test + public void testInstallResourceWithNonAvailableCustomizerFail() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .disableVerification() + .add(dpBuilder.createResource().setResourceProcessorPID("my.unknown.rp").setUrl(getTestResource("test-config1.xml"))); + + try { + installDeploymentPackage(dpBuilder); + fail("Succeeded into installing a resource with an non-existing RP?!"); + } + catch (DeploymentException exception) { + // Ok; expected... + assertDeploymentException(CODE_PROCESSOR_NOT_FOUND, exception); + } + + assertEquals("Expected no deployment package?!", 0, countDeploymentPackages()); + assertTrue("Expected no artifacts to be installed?!", getCurrentBundles().isEmpty()); + } + +} diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/DPSignerTest.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/DPSignerTest.java new file mode 100644 index 00000000000..499906bd580 --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/DPSignerTest.java @@ -0,0 +1,84 @@ +/* + * 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.itest; + +import static junit.framework.TestCase.assertNotNull; +import static org.apache.felix.deploymentadmin.itest.BaseIntegrationTest.TEST_FAILING_BUNDLE_RP1; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.net.URL; +import java.security.Security; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; + +import org.apache.felix.deploymentadmin.itest.util.CertificateUtil; +import org.apache.felix.deploymentadmin.itest.util.CertificateUtil.KeyType; +import org.apache.felix.deploymentadmin.itest.util.CertificateUtil.SignerInfo; +import org.apache.felix.deploymentadmin.itest.util.DPSigner; +import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Test cases for {@link DPSigner}. + */ +public class DPSignerTest { + + @Test + public void testSignArtifactsOk() throws Exception { + URL dpProps = getClass().getResource("/dp.properties"); + assertNotNull(dpProps); + + DeploymentPackageBuilder builder = DeploymentPackageBuilder.create("dpSignerTest1", "1.0.0"); + builder.add(builder.createLocalizationResource().setUrl(dpProps).setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setFilename("dp.properties")); + + SignerInfo signerInfo = CertificateUtil.createSelfSignedCert("CN=testCert", KeyType.RSA); + + DPSigner signer = new DPSigner(); + + byte[] rawData; + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + signer.sign(builder, signerInfo.getPrivate(), signerInfo.getCert(), baos); + rawData = baos.toByteArray(); + } + + try (ByteArrayInputStream bais = new ByteArrayInputStream(rawData); JarInputStream jis = new JarInputStream(bais, true)) { + JarEntry entry; + while ((entry = jis.getNextJarEntry()) != null) { + assertNotNull(entry); + jis.closeEntry(); + } + + assertNotNull(jis.getManifest()); + } + } + + @Before + public void setUp() { + Security.addProvider(new BouncyCastleProvider()); + } + + @After + public void tearDown() { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); + } +} diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/DeploymentAdminEventTest.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/DeploymentAdminEventTest.java new file mode 100644 index 00000000000..d42e1842209 --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/DeploymentAdminEventTest.java @@ -0,0 +1,303 @@ +/* + * 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.itest; + +import java.util.Dictionary; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.felix.deploymentadmin.Constants; +import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.deploymentadmin.DeploymentException; +import org.osgi.service.deploymentadmin.DeploymentPackage; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventConstants; +import org.osgi.service.event.EventHandler; + +/** + * Test cases for FELIX-4466 - DA does not always fire events. + */ +@RunWith(PaxExam.class) +public class DeploymentAdminEventTest extends BaseIntegrationTest +{ + /** + * FELIX-4466 - test that an event is fired when an installation of a DP fails. + */ + @Test + public void testFailedInstallationCausesCompletionEventOk() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + // incluse two different versions of the same bundle (with the same BSN), this is *not* allowed per the DA spec... + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleapi1", "bundleapi1", "1.0.0"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleapi2", "bundleapi2", "2.0.0"))); + + final AtomicReference completionEventRef = new AtomicReference(); + final AtomicReference installEventRef = new AtomicReference(); + final CountDownLatch cdl = new CountDownLatch(1); + + EventHandler eventHandler = new EventHandler() + { + @Override + public void handleEvent(Event event) + { + if (Constants.EVENTTOPIC_COMPLETE.equals(event.getTopic())) + { + assertTrue("Multiple events received?!", completionEventRef.compareAndSet(null, event)); + cdl.countDown(); + } + else if (Constants.EVENTTOPIC_INSTALL.equals(event.getTopic())) + { + assertTrue("Multiple events received?!", installEventRef.compareAndSet(null, event)); + } + } + }; + + Dictionary props = new Properties(); + props.put(EventConstants.EVENT_TOPIC, new String[] { Constants.EVENTTOPIC_COMPLETE, Constants.EVENTTOPIC_INSTALL }); + + ServiceRegistration sreg = m_context.registerService(EventHandler.class, eventHandler, props); + + try + { + installDeploymentPackage(dpBuilder); + fail("DeploymentException expected!"); + } + catch (DeploymentException e) + { + // Ok; expected... + assertTrue("Not all events were received in time?!", cdl.await(5, TimeUnit.SECONDS)); + + Event event; + // Verify we've got the expected events... + event = installEventRef.get(); + // The install event is send *after* the DP have been created, which fails in this test... + assertNull("No install event received?!", event); + + event = completionEventRef.get(); + assertNotNull("No completion event received?!", event); + assertTrue("Completion property set to true?!", Boolean.FALSE.equals(event.getProperty(Constants.EVENTPROPERTY_SUCCESSFUL))); + } + finally + { + sreg.unregister(); + } + } + + /** + * FELIX-4466 - test that an event is fired when an installation of a DP succeeds. + */ + @Test + public void testSuccessfulInstallationCausesCompletionEventOk() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleapi1", "bundleapi1", "1.0.0"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleimpl1", "bundleimpl1", "1.0.0"))); + + final AtomicReference completionEventRef = new AtomicReference(); + final AtomicReference installEventRef = new AtomicReference(); + final CountDownLatch cdl = new CountDownLatch(2); + + EventHandler eventHandler = new EventHandler() + { + @Override + public void handleEvent(Event event) + { + if (Constants.EVENTTOPIC_COMPLETE.equals(event.getTopic())) + { + assertTrue("Multiple events received?!", completionEventRef.compareAndSet(null, event)); + cdl.countDown(); + } + else if (Constants.EVENTTOPIC_INSTALL.equals(event.getTopic())) + { + assertTrue("Multiple events received?!", installEventRef.compareAndSet(null, event)); + cdl.countDown(); + } + } + }; + + Dictionary props = new Properties(); + props.put(EventConstants.EVENT_TOPIC, new String[] { Constants.EVENTTOPIC_COMPLETE, Constants.EVENTTOPIC_INSTALL }); + + ServiceRegistration sreg = m_context.registerService(EventHandler.class, eventHandler, props); + + try + { + installDeploymentPackage(dpBuilder); + + assertTrue("Not all events were received in time?!", cdl.await(5, TimeUnit.SECONDS)); + + Event event; + // Verify we've got the expected events... + event = installEventRef.get(); + assertNotNull("No install event received?!", event); + + event = completionEventRef.get(); + assertNotNull("No completion event received?!", event); + assertTrue("Completion property set to false?!", Boolean.TRUE.equals(event.getProperty(Constants.EVENTPROPERTY_SUCCESSFUL))); + } + finally + { + sreg.unregister(); + } + } + + /** + * FELIX-4466 - test that an event is fired when a DP is uninstalled. + */ + @Test + public void testSuccessfulUninstallationCausesCompletionEventOk() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleapi1", "bundleapi1", "1.0.0"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleimpl1", "bundleimpl1", "1.0.0"))); + + final AtomicReference completionEventRef = new AtomicReference(); + final AtomicReference uninstallEventRef = new AtomicReference(); + final CountDownLatch cdl = new CountDownLatch(2); + + EventHandler eventHandler = new EventHandler() + { + @Override + public void handleEvent(Event event) + { + if (Constants.EVENTTOPIC_COMPLETE.equals(event.getTopic())) + { + assertTrue("Multiple events received?!", completionEventRef.compareAndSet(null, event)); + cdl.countDown(); + } + else if (Constants.EVENTTOPIC_UNINSTALL.equals(event.getTopic())) + { + assertTrue("Multiple events received?!", uninstallEventRef.compareAndSet(null, event)); + cdl.countDown(); + } + } + }; + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull(dp); + + awaitRefreshPackagesEvent(); + + Dictionary props = new Properties(); + props.put(EventConstants.EVENT_TOPIC, new String[] { Constants.EVENTTOPIC_COMPLETE, Constants.EVENTTOPIC_UNINSTALL }); + + ServiceRegistration sreg = m_context.registerService(EventHandler.class, eventHandler, props); + + try + { + dp.uninstall(); + + assertTrue("Not all events were received in time?!", cdl.await(5, TimeUnit.SECONDS)); + + Event event; + // Verify we've got the expected events... + event = uninstallEventRef.get(); + assertNotNull("No uninstall event received?!", event); + + event = completionEventRef.get(); + assertNotNull("No completion event received?!", event); + assertTrue("Completion property set to false?!", Boolean.TRUE.equals(event.getProperty(Constants.EVENTPROPERTY_SUCCESSFUL))); + } + finally + { + sreg.unregister(); + } + } + + /** + * FELIX-4466 - test that an event is fired when a DP is uninstalled, but fails. + */ + @Test + public void testFailedUninstallationCausesCompletionEventOk() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))) + .add(dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml"))); + + final AtomicReference completionEventRef = new AtomicReference(); + final AtomicReference uninstallEventRef = new AtomicReference(); + final CountDownLatch cdl = new CountDownLatch(2); + + EventHandler eventHandler = new EventHandler() + { + @Override + public void handleEvent(Event event) + { + if (Constants.EVENTTOPIC_COMPLETE.equals(event.getTopic())) + { + assertTrue("Multiple events received?!", completionEventRef.compareAndSet(null, event)); + cdl.countDown(); + } + else if (Constants.EVENTTOPIC_UNINSTALL.equals(event.getTopic())) + { + assertTrue("Multiple events received?!", uninstallEventRef.compareAndSet(null, event)); + cdl.countDown(); + } + } + }; + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + // Should cause the uninstall of the DP to fail... + dp.getBundle(getSymbolicName("rp1")).uninstall(); + + Dictionary props = new Properties(); + props.put(EventConstants.EVENT_TOPIC, new String[] { Constants.EVENTTOPIC_COMPLETE, Constants.EVENTTOPIC_UNINSTALL }); + + ServiceRegistration sreg = m_context.registerService(EventHandler.class, eventHandler, props); + + try + { + dp.uninstall(); + fail("DeploymentException expected!"); + } + catch (DeploymentException e) + { + // Ok, expected... + assertTrue("Not all events were received in time?!", cdl.await(5, TimeUnit.SECONDS)); + + Event event; + // Verify we've got the expected events... + event = uninstallEventRef.get(); + assertNotNull("No uninstall event received?!", event); + + event = completionEventRef.get(); + assertNotNull("No completion event received?!", event); + assertTrue("Completion property set to true?!", Boolean.FALSE.equals(event.getProperty(Constants.EVENTPROPERTY_SUCCESSFUL))); + } + finally + { + sreg.unregister(); + } + } +} diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/DeploymentAdminTest.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/DeploymentAdminTest.java new file mode 100644 index 00000000000..2b02a2eaea9 --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/DeploymentAdminTest.java @@ -0,0 +1,158 @@ +/* + * 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.itest; + +import static org.osgi.service.deploymentadmin.DeploymentException.CODE_BUNDLE_NAME_ERROR; +import static org.osgi.service.deploymentadmin.DeploymentException.CODE_OTHER_ERROR; + + +import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder; +import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder.JarManifestManipulatingFilter; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.osgi.service.deploymentadmin.DeploymentAdmin; +import org.osgi.service.deploymentadmin.DeploymentException; +import org.osgi.service.deploymentadmin.DeploymentPackage; + +/** + * Generic tests for {@link DeploymentAdmin}. + */ +@RunWith(PaxExam.class) +public class DeploymentAdminTest extends BaseIntegrationTest +{ + /** + * Tests that we can update the configuration of {@link DeploymentAdmin} at runtime. Based on the test case for FELIX-4184, see + * {@link org.apache.felix.deploymentadmin.itest.InstallFixPackageTest#testInstallAndUpdateImplementationBundleWithSeparateAPIBundle_FELIX4184()} + */ + @Test + public void testUpdateConfigurationOk() throws Exception + { + System.setProperty("org.apache.felix.deploymentadmin.stopUnaffectedBundles", "false"); + System.setProperty("org.apache.felix.deploymentadmin.allowForeignCustomizers", "false"); + + // This test case will only work if stopUnaffectedBundle is set to 'false'... + try + { + // first, install a deployment package with implementation and api bundles in version 1.0.0 + DeploymentPackageBuilder dpBuilder = createDeploymentPackageBuilder("a", "1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleimpl1", "bundleimpl1", "1.0.0"))); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleapi1", "bundleapi1", "1.0.0"))); + + DeploymentPackage dp1 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp1); + + assertEquals("Expected a single deployment package?!", 1, countDeploymentPackages()); + + // then, install a fix package with implementation and api bundles in version 2.0.0 + dpBuilder = createDeploymentPackageBuilder("a", "2.0.0").setFixPackage("[1.0.0,2.0.0]"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleimpl2", "bundleimpl2", "2.0.0"))); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleapi2", "bundleapi2", "2.0.0"))); + + DeploymentPackage dp2 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp2); + + awaitRefreshPackagesEvent(); + + assertBundleExists(getSymbolicName("bundleimpl"), "2.0.0"); + assertBundleExists(getSymbolicName("bundleapi"), "2.0.0"); + assertBundleNotExists(getSymbolicName("bundleimpl"), "1.0.0"); + assertBundleNotExists(getSymbolicName("bundleapi"), "1.0.0"); + } + finally + { + System.clearProperty("org.apache.felix.deploymentadmin.stopUnaffectedBundles"); + System.clearProperty("org.apache.felix.deploymentadmin.allowForeignCustomizers"); + } + } + + @Test + public void testBundleSymbolicNameMustMatchManifestEntry() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))).add( + dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2")).setFilter(new JarManifestManipulatingFilter("Bundle-SymbolicName", "foo"))); + + try + { + installDeploymentPackage(dpBuilder); + fail("Succeeded into installing a bundle with a fake symbolic name?!"); + } + catch (DeploymentException exception) + { + // Ok; expected... + assertDeploymentException(CODE_BUNDLE_NAME_ERROR, exception); + } + } + + @Test + public void testBundleVersionMustMatchManifestEntry() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))).add( + dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2")).setFilter(new JarManifestManipulatingFilter("Bundle-Version", "1.1.0"))); + + try + { + installDeploymentPackage(dpBuilder); + fail("Succeeded into installing a bundle with a fake version?!"); + } + catch (DeploymentException exception) + { + // Ok; expected... + assertDeploymentException(CODE_OTHER_ERROR, exception); + } + } + + @Test + public void testManifestEntryMustMatchBundleSymbolicName() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))).add(dpBuilder.createBundleResource().setSymbolicName("foo").setUrl(getTestBundleURL("bundle2"))); + + try + { + installDeploymentPackage(dpBuilder); + fail("Succeeded into installing a bundle with a fake symbolic name?!"); + } + catch (DeploymentException exception) + { + // Ok; expected... + assertDeploymentException(CODE_BUNDLE_NAME_ERROR, exception); + } + } + + @Test + public void testManifestEntryMustMatchBundleVersion() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))).add(dpBuilder.createBundleResource().setVersion("1.1.0").setUrl(getTestBundleURL("bundle2"))); + + try + { + installDeploymentPackage(dpBuilder); + fail("Succeeded into installing a bundle with a fake version?!"); + } + catch (DeploymentException exception) + { + // Ok; expected... + assertDeploymentException(CODE_OTHER_ERROR, exception); + } + } +} diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/DeploymentPackageBuilderTest.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/DeploymentPackageBuilderTest.java new file mode 100644 index 00000000000..e38d53898b4 --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/DeploymentPackageBuilderTest.java @@ -0,0 +1,301 @@ +/* + * 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.itest; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; + +import junit.framework.TestCase; + +import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder; +import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder.JarManifestManipulatingFilter; +import org.junit.Before; +import org.junit.Test; + +/** + * Test cases for {@link DeploymentPackageBuilder}. + */ +public class DeploymentPackageBuilderTest extends TestCase { + + private String m_testBundleBasePath; + + @Before + public void setUp() throws Exception { + File f = new File("../testbundles").getAbsoluteFile(); + assertTrue("Failed to find test bundles directory?!", f.exists() && f.isDirectory()); + + m_testBundleBasePath = f.getAbsolutePath(); + } + + /** + * Tests that we can build a deployment package with a bundle resource. + */ + @Test + public void testCreateMissingBundleResourceOk() throws Exception { + DeploymentPackageBuilder dpBuilder = DeploymentPackageBuilder.create("dp-test", "1.0.0"); + dpBuilder + .setFixPackage() + .add(dpBuilder.createBundleResource() + .setUrl(getTestBundle("bundle1")).setMissing() + ); + + JarInputStream jis = new JarInputStream(dpBuilder.generate()); + assertNotNull(jis); + + Manifest manifest = jis.getManifest(); + assertManifestHeader(manifest, "DeploymentPackage-SymbolicName", "dp-test"); + assertManifestHeader(manifest, "DeploymentPackage-Version", "1.0.0"); + + String filename = getBundleName("bundle1"); + + assertManifestEntry(manifest, filename, "Name", filename); + assertManifestEntry(manifest, filename, "Bundle-SymbolicName", "testbundles.bundle1"); + assertManifestEntry(manifest, filename, "Bundle-Version", "1.0.0"); + assertManifestEntry(manifest, filename, "DeploymentPackage-Missing", "true"); + + int count = countJarEntries(jis); + + assertEquals("Expected two entries in the JAR!", 0, count); + } + + /** + * Tests that we can build a deployment package with a bundle resource. + */ + @Test + public void testCreateMinimalSingleBundleResourceOk() throws Exception { + DeploymentPackageBuilder dpBuilder = DeploymentPackageBuilder.create("dp-test", "1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource() + .setUrl(getTestBundle("bundle1")) + ); + + JarInputStream jis = new JarInputStream(dpBuilder.generate()); + assertNotNull(jis); + + Manifest manifest = jis.getManifest(); + assertManifestHeader(manifest, "DeploymentPackage-SymbolicName", "dp-test"); + assertManifestHeader(manifest, "DeploymentPackage-Version", "1.0.0"); + + String filename = getBundleName("bundle1"); + + assertManifestEntry(manifest, filename, "Name", filename); + assertManifestEntry(manifest, filename, "Bundle-SymbolicName", "testbundles.bundle1"); + assertManifestEntry(manifest, filename, "Bundle-Version", "1.0.0"); + + int count = countJarEntries(jis); + + assertEquals("Expected two entries in the JAR!", 1, count); + } + + /** + * Tests that we can filter a resource. + */ + @Test + public void testResourceFilterOk() throws Exception { + DeploymentPackageBuilder dpBuilder = DeploymentPackageBuilder.create("dp-test", "1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource() + .setUrl(getTestBundle("bundle2"))) + .add(dpBuilder.createBundleResource() + .setVersion("1.1.0") + .setFilter(new JarManifestManipulatingFilter("Bundle-Version", "1.1.0", "Foo", "bar")) + .setUrl(getTestBundle("bundle1"))); + + JarInputStream jis = new JarInputStream(dpBuilder.generate()); + assertNotNull(jis); + + Manifest manifest = jis.getManifest(); + assertManifestHeader(manifest, "DeploymentPackage-SymbolicName", "dp-test"); + assertManifestHeader(manifest, "DeploymentPackage-Version", "1.0.0"); + + String filename = getBundleName("bundle1"); + + assertManifestEntry(manifest, filename, "Name", filename); + assertManifestEntry(manifest, filename, "Bundle-SymbolicName", "testbundles.bundle1"); + assertManifestEntry(manifest, filename, "Bundle-Version", "1.1.0"); + + filename = getBundleName("bundle2"); + + assertManifestEntry(manifest, filename, "Name", filename); + assertManifestEntry(manifest, filename, "Bundle-SymbolicName", "testbundles.bundle2"); + assertManifestEntry(manifest, filename, "Bundle-Version", "1.0.0"); + + try { + byte[] buf = new byte[32 * 1024]; + + JarEntry entry; + while ((entry = jis.getNextJarEntry()) != null) { + if (entry.getName().endsWith("valid-bundle1.jar")) { + int read = jis.read(buf); + + JarInputStream jis2 = new JarInputStream(new ByteArrayInputStream(Arrays.copyOf(buf, read))); + Manifest manifest2 = jis2.getManifest(); + + Attributes mainAttributes = manifest2.getMainAttributes(); + assertEquals("1.1.0", mainAttributes.getValue("Bundle-Version")); + assertEquals("bar", mainAttributes.getValue("Foo")); + + jis2.close(); + } + jis.closeEntry(); + } + } + finally { + jis.close(); + } + } + + /** + * Tests that we can build a deployment package with a "plain" resource and resource processor. + */ + @Test + public void testCreateMinimalSingleResourceAndProcessorOk() throws Exception { + DeploymentPackageBuilder dpBuilder = DeploymentPackageBuilder.create("dp-test", "1.0.0"); + dpBuilder + .add(dpBuilder.createResourceProcessorResource() + .setUrl(getTestBundle("rp1"))) + .add(dpBuilder.createResource() + .setResourceProcessorPID("org.apache.felix.deploymentadmin.test.rp1") + .setUrl(getTestResource("test-config1.xml")) + ); + + JarInputStream jis = new JarInputStream(dpBuilder.generate()); + assertNotNull(jis); + + Manifest manifest = jis.getManifest(); + assertManifestHeader(manifest, "DeploymentPackage-SymbolicName", "dp-test"); + assertManifestHeader(manifest, "DeploymentPackage-Version", "1.0.0"); + + String filename = getBundleName("rp1"); + + assertManifestEntry(manifest, filename, "Name", filename); + assertManifestEntry(manifest, filename, "Bundle-SymbolicName", "testbundles.rp1"); + assertManifestEntry(manifest, filename, "Bundle-Version", "1.0.0"); + + filename = "test-config1.xml"; + + assertManifestEntry(manifest, filename, "Name", filename); + assertManifestEntry(manifest, filename, "Resource-Processor", "org.apache.felix.deploymentadmin.test.rp1"); + + int count = countJarEntries(jis); + + assertEquals("Expected two entries in the JAR!", 2, count); + } + + /** + * Tests that we can build a deployment package with two bundle resources. + */ + @Test + public void testCreateMinimalTwoBundleResourcesOk() throws Exception { + DeploymentPackageBuilder dpBuilder = DeploymentPackageBuilder.create("dp-test", "1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource() + .setUrl(getTestBundle("bundle1")) + ) + .add(dpBuilder.createBundleResource() + .setUrl(getTestBundle("bundle2")) + ); + + JarInputStream jis = new JarInputStream(dpBuilder.generate()); + assertNotNull(jis); + + Manifest manifest = jis.getManifest(); + assertManifestHeader(manifest, "DeploymentPackage-SymbolicName", "dp-test"); + assertManifestHeader(manifest, "DeploymentPackage-Version", "1.0.0"); + + String filename = getBundleName("bundle1"); + + assertManifestEntry(manifest, filename, "Name", filename); + assertManifestEntry(manifest, filename, "Bundle-SymbolicName", "testbundles.bundle1"); + assertManifestEntry(manifest, filename, "Bundle-Version", "1.0.0"); + + filename = getBundleName("bundle2"); + + assertManifestEntry(manifest, filename, "Name", filename); + assertManifestEntry(manifest, filename, "Bundle-SymbolicName", "testbundles.bundle2"); + assertManifestEntry(manifest, filename, "Bundle-Version", "1.0.0"); + + int count = countJarEntries(jis); + + assertEquals("Expected two entries in the JAR!", 2, count); + } + + private void assertAttributes(Attributes attributes, String headerName, String expectedValue) + throws RuntimeException { + assertNotNull("No attributes given!", attributes); + assertEquals(headerName, expectedValue, attributes.getValue(headerName)); + } + + private void assertManifestEntry(Manifest manifest, String key, String headerName, String expectedValue) + throws RuntimeException { + Attributes attributes = manifest.getEntries().get(key); + assertNotNull("No attributes found for: " + key, attributes); + assertAttributes(attributes, headerName, expectedValue); + } + + private void assertManifestHeader(Manifest manifest, String headerName, String expectedValue) + throws RuntimeException { + assertAttributes(manifest.getMainAttributes(), headerName, expectedValue); + } + + private int countJarEntries(JarInputStream jis) throws IOException { + int count = 0; + try { + while (jis.getNextJarEntry() != null) { + count++; + jis.closeEntry(); + } + } + finally { + jis.close(); + } + return count; + } + + private String getBundleName(String baseName) { + return String.format("org.apache.felix.deploymentadmin.test.%1$s-1.0.0.jar", baseName); + } + + private String getBundleFilename(String baseName) { + return String.format("%1$s/target/org.apache.felix.deploymentadmin.test.%1$s-1.0.0.jar", baseName); + } + + private URL getTestBundle(String baseName) throws MalformedURLException { + File f = new File(m_testBundleBasePath, getBundleFilename(baseName)); + assertTrue("No such bundle: " + f, f.exists() && f.isFile()); + return f.toURI().toURL(); + } + + private URL getTestResource(String resourceName) { + if (!resourceName.startsWith("/")) { + resourceName = "/".concat(resourceName); + } + URL resource = getClass().getResource(resourceName); + assertNotNull("No such resource: " + resourceName, resource); + return resource; + } +} diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/InstallDeploymentPackageTest.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/InstallDeploymentPackageTest.java new file mode 100644 index 00000000000..24f4ffa97f3 --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/InstallDeploymentPackageTest.java @@ -0,0 +1,609 @@ +/* + * 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.itest; + +import static org.apache.felix.deploymentadmin.itest.util.CertificateUtil.createSelfSignedCert; + +import java.io.File; +import java.net.URL; + +import org.apache.felix.deploymentadmin.itest.util.CertificateUtil.KeyType; +import org.apache.felix.deploymentadmin.itest.util.CertificateUtil.SignerInfo; +import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder; +import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder.JarManifestManipulatingFilter; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.osgi.framework.Bundle; +import org.osgi.service.deploymentadmin.BundleInfo; +import org.osgi.service.deploymentadmin.DeploymentException; +import org.osgi.service.deploymentadmin.DeploymentPackage; + +/** + * Provides test cases regarding the use of "normal" deployment packages in DeploymentAdmin. + */ +@RunWith(PaxExam.class) +public class InstallDeploymentPackageTest extends BaseIntegrationTest { + /** + * FELIX-518 - Test that DP with localization and signature files are properly deployed. + */ + @Test + public void testInstallDeploymentPackageWithLocalizationAndSignatureFilesOk() throws Exception { + URL dpProps = getClass().getResource("/dp.properties"); + assertNotNull(dpProps); + + SignerInfo signer = createSelfSignedCert("CN=dpTest", KeyType.EC); + + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.signOutput(signer.getPrivate(), signer.getCert()) + .add(dpBuilder.createLocalizationResource().setUrl(dpProps).setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setFilename("dp.properties")) + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2"))); + + installDeploymentPackage(dpBuilder); // should succeed. + + assertBundleExists("testbundles.bundle1", "1.0.0"); + assertBundleExists("testbundles.bundle2", "1.0.0"); + assertBundleExists("testbundles.rp1", "1.0.0"); + } + + /** + * FELIX-4409/4410/4463 - test the installation of an invalid deployment package. + */ + @Test + public void testInstallInvalidDeploymentPackageFail() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + // incluse two different versions of the same bundle (with the same BSN), this is *not* allowed per the DA + // spec... + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleapi1", "bundleapi1", "1.0.0"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleapi2", "bundleapi2", "2.0.0"))); + + try { + installDeploymentPackage(dpBuilder); + fail("DeploymentException expected!"); + } + catch (DeploymentException e) { + // Ok; expected... + } + + // Verify that none of the bundles are installed... + assertBundleNotExists("testbundles.bundleapi", "1.0.0"); + assertBundleNotExists("testbundles.bundleapi", "2.0.0"); + } + + /** + * FELIX-1835 - test whether we can install bundles with a non-root path inside the DP. + */ + @Test + public void testInstallBundlesWithPathsOk() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + // incluse two different versions of the same bundle (with the same BSN), this is *not* allowed per the DA + // spec... + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleapi1", "bundleapi1", "1.0.0")).setFilename("bundles/bundleapi1.jar")) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleimpl1", "bundleimpl1", "1.0.0")).setFilename("bundles/bundleimpl1.jar")); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull(dp); + + BundleInfo[] bundleInfos = dp.getBundleInfos(); + assertEquals(2, bundleInfos.length); + + // Verify that none of the bundles are installed... + assertBundleExists("testbundles.bundleapi", "1.0.0"); + assertBundleExists("testbundles.bundleimpl", "1.0.0"); + } + + /** + * Tests that adding the dependency for a bundle in an update package causes the depending bundle to be resolved and + * started. + */ + @Test + public void testInstallBundleWithDependencyInPackageUpdateOk() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + // missing bundle1 as dependency... + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2"))); + + DeploymentPackage dp1 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp1); + + awaitRefreshPackagesEvent(); + + Bundle bundle = dp1.getBundle(getSymbolicName("bundle2")); + assertNotNull("Failed to obtain bundle from deployment package?!", bundle); + + assertTrue(isBundleInstalled(dp1.getBundle(getSymbolicName("bundle2")))); + + dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1"); + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))); + + DeploymentPackage dp2 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp2); + + awaitRefreshPackagesEvent(); + + assertTrue(isBundleActive(dp2.getBundle(getSymbolicName("bundle1")))); + assertTrue(isBundleActive(dp2.getBundle(getSymbolicName("bundle2")))); + } + + /** + * Tests that installing a bundle with a dependency installed by another deployment package is not started, but is + * resolved. + */ + @Test + public void testInstallBundleWithDependencyInSeparatePackageOk() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2"))); + + DeploymentPackage dp1 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp1); + + awaitRefreshPackagesEvent(); + + assertBundleExists(getSymbolicName("bundle2"), "1.0.0"); + + // We shouldn't be able to resolve the deps for bundle2... + assertFalse(resolveBundles(dp1.getBundle(getSymbolicName("bundle2")))); + + assertTrue(isBundleInstalled(dp1.getBundle(getSymbolicName("bundle2")))); + + dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + // as missing bundle1... + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))); + + DeploymentPackage dp2 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp2); + + awaitRefreshPackagesEvent(); + + assertBundleExists(getSymbolicName("bundle1"), "1.0.0"); + assertBundleExists(getSymbolicName("bundle2"), "1.0.0"); + + // Now we should be able to resolve the dependencies for bundle2... + assertTrue(resolveBundles(dp1.getBundle(getSymbolicName("bundle2")))); + + assertTrue(isBundleActive(dp2.getBundle(getSymbolicName("bundle1")))); + assertTrue(isBundleResolved(dp1.getBundle(getSymbolicName("bundle2")))); + } + + /** + * Tests that if an exception is thrown in the start method of a bundle, the installation is not rolled back. + */ + @Test + public void testInstallBundleWithExceptionThrownInStartCausesNoRollbackOk() throws Exception { + System.setProperty("bundle3", "start"); + + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle3"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + assertBundleExists(getSymbolicName("bundle1"), "1.0.0"); + assertBundleExists(getSymbolicName("bundle3"), "1.0.0"); + + assertTrue(isBundleActive(dp.getBundle(getSymbolicName("bundle1")))); + // the bundle threw an exception during start, so it is not active... + assertFalse(isBundleActive(dp.getBundle(getSymbolicName("bundle3")))); + + assertEquals("Expected a single deployment package?!", 1, countDeploymentPackages()); + } + + /** + * Tests that installing a bundle along with a fragment bundle succeeds (DA should not try to start the fragment, + * see FELIX-4167). + */ + @Test + public void testInstallBundleWithFragmentOk() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("fragment1"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + assertBundleExists(getSymbolicName("bundle1"), "1.0.0"); + assertBundleExists(getSymbolicName("fragment1"), "1.0.0"); + + assertTrue(isBundleActive(dp.getBundle(getSymbolicName("bundle1")))); + assertFalse(isBundleActive(dp.getBundle(getSymbolicName("fragment1")))); + + assertEquals("Expected a single deployment package?!", 1, countDeploymentPackages()); + } + + /** + * Tests that installing a bundle whose dependencies cannot be met, is installed, but not started. + */ + @Test + public void testInstallBundleWithMissingDependencyOk() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + Bundle bundle = dp.getBundle(getSymbolicName("bundle2")); + assertNotNull("Failed to obtain bundle from deployment package?!", bundle); + + assertBundleExists(getSymbolicName("bundle2"), "1.0.0"); + + assertTrue(isBundleInstalled(dp.getBundle(getSymbolicName("bundle2")))); + } + + /** + * Tests that installing a bundle along with other (non-bundle) artifacts succeeds. + */ + @Test + public void testInstallBundleWithOtherArtifactsOk() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))) + .add(dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle3"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + // Though the commit failed; the package should be installed... + assertBundleExists(getSymbolicName("rp1"), "1.0.0"); + assertBundleExists(getSymbolicName("bundle3"), "1.0.0"); + + assertEquals("Expected a single deployment package?!", 1, countDeploymentPackages()); + } + + /** + * Tests that installing a new bundle works as expected. + */ + @Test + public void testInstallSingleValidBundleOk() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + assertNotNull("Failed to obtain test service?!", awaitService(TEST_SERVICE_NAME)); + + assertBundleExists(getSymbolicName("bundle1"), "1.0.0"); + assertTrue(isBundleActive(dp.getBundle(getSymbolicName("bundle1")))); + } + + /** + * Tests that installing two bundles works as expected. + */ + @Test + public void testInstallTwoValidBundlesOk() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + assertNotNull("Failed to obtain test service?!", awaitService(TEST_SERVICE_NAME)); + + assertBundleExists(getSymbolicName("bundle1"), "1.0.0"); + assertBundleExists(getSymbolicName("bundle2"), "1.0.0"); + + assertTrue(isBundleActive(dp.getBundle(getSymbolicName("bundle1")))); + assertTrue(isBundleActive(dp.getBundle(getSymbolicName("bundle2")))); + } + + /** + * Tests that if an exception is thrown during the uninstall of a bundle, the installation/update continues and + * succeeds. + */ + @Test + public void testUninstallBundleWithExceptionThrownInStopCauseNoRollbackOk() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle3"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + assertBundleExists(getSymbolicName("bundle3"), "1.0.0"); + + System.setProperty("bundle3", "stop"); + + dpBuilder = dpBuilder.create("1.0.1"); + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2"))); + + dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + assertBundleExists(getSymbolicName("bundle1"), "1.0.0"); + assertBundleExists(getSymbolicName("bundle2"), "1.0.0"); + assertBundleNotExists(getSymbolicName("bundle3"), "1.0.0"); + + assertTrue(isBundleActive(dp.getBundle(getSymbolicName("bundle1")))); + assertTrue(isBundleActive(dp.getBundle(getSymbolicName("bundle2")))); + + assertEquals("Expected a single deployment package?!", 1, countDeploymentPackages()); + } + + /** + * Tests that if an exception is thrown during the stop of a bundle, the installation/update continues and succeeds. + */ + @Test + public void testUpdateBundleWithExceptionThrownInStopCauseNoRollbackOk() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle3"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + assertBundleExists(getSymbolicName("bundle3"), "1.0.0"); + + System.setProperty("bundle3", "stop"); + + dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1"); + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle3"))); + + dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + assertBundleExists(getSymbolicName("bundle1"), "1.0.0"); + assertBundleExists(getSymbolicName("bundle2"), "1.0.0"); + assertBundleExists(getSymbolicName("bundle3"), "1.0.0"); + + assertTrue(isBundleActive(dp.getBundle(getSymbolicName("bundle1")))); + assertTrue(isBundleActive(dp.getBundle(getSymbolicName("bundle2")))); + assertTrue(isBundleActive(dp.getBundle(getSymbolicName("bundle3")))); + + assertEquals("Expected a single deployment package?!", 1, countDeploymentPackages()); + } + + /** + * Tests that we can correctly rollback the installation of a deployment package for bundles that have their data + * area populated. + */ + @Test + public void testRollbackWithPopulatedDataAreaOk() throws Exception { + // Install a first version, in which we're going to change the data area of a bundle... + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + Bundle bundle1 = getBundle("testbundles.bundle1"); + assertNotNull("Unable to get installed test bundle?!", bundle1); + + File dataArea = bundle1.getDataFile(""); + assertNotNull("No data area obtained for test bundle?!", dataArea); + + // Populate the data area... + assertTrue("No file created?!", new File(dataArea, "file1").createNewFile()); + assertTrue("No file created?!", new File(dataArea, "file2").createNewFile()); + assertTrue("No file created?!", new File(dataArea, "file3").createNewFile()); + + // This will cause the new bundle to fail in its stop method... + System.setProperty("rp1", "process"); + + // Simulate an upgrade for our bundle, which should cause its data area to be retained... + dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1"); + dpBuilder + .add(dpBuilder.createBundleResource().setVersion("1.1.0").setUrl(getTestBundleURL("bundle1")).setFilter(new JarManifestManipulatingFilter("Bundle-Version", "1.1.0"))) + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))) + .add(dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml"))); + + try { + dp = installDeploymentPackage(dpBuilder); // should fail! + fail("Deployment of upgrade package should have failed?!"); + } + catch (DeploymentException e) { + // Ok; expected... + } + + // We should still have this bundle.. + bundle1 = getBundle("testbundles.bundle1"); + assertNotNull("Unable to get installed test bundle?!", bundle1); + + dataArea = bundle1.getDataFile(""); + assertNotNull("No data area obtained for test bundle?!", dataArea); + + // Data area should be restored exactly as-is... + assertTrue("File not restored?!", new File(dataArea, "file1").exists()); + assertTrue("File not restored?!", new File(dataArea, "file2").exists()); + assertTrue("File not restored?!", new File(dataArea, "file3").exists()); + } + + /** + * Tests that we can correctly install a deployment package with bundles that have their data area populated. + */ + @Test + public void testUpgradeWithPopulatedDataAreaOk() throws Exception { + // Install a first version, in which we're going to change the data area of a bundle... + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource().setVersion("1.0.0").setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + Bundle bundle1 = getBundle("testbundles.bundle1"); + assertNotNull("Unable to get installed test bundle?!", bundle1); + + File dataArea = bundle1.getDataFile(""); + assertNotNull("No data area obtained for test bundle?!", dataArea); + + // Populate the data area... + assertTrue("No file created?!", new File(dataArea, "file1").createNewFile()); + assertTrue("No file created?!", new File(dataArea, "file2").createNewFile()); + assertTrue("No file created?!", new File(dataArea, "file3").createNewFile()); + + dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1"); + dpBuilder + .add(dpBuilder.createBundleResource().setVersion("1.1.0").setUrl(getTestBundleURL("bundle1")).setFilter(new JarManifestManipulatingFilter("Bundle-Version", "1.1.0"))) + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))) + .add(dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml"))); + + dp = installDeploymentPackage(dpBuilder); // should succeed! + + // We should still have this bundle.. + bundle1 = getBundle("testbundles.bundle1"); + assertNotNull("Unable to get installed test bundle?!", bundle1); + + dataArea = bundle1.getDataFile(""); + assertNotNull("No data area obtained for test bundle?!", dataArea); + + // Data area should be restored exactly as-is... + assertTrue("File not restored?!", new File(dataArea, "file1").exists()); + assertTrue("File not restored?!", new File(dataArea, "file2").exists()); + assertTrue("File not restored?!", new File(dataArea, "file3").exists()); + } + + /** + * Tests that we can correctly install a deployment package with bundles that have their data area populated. + */ + @Test + public void testUninstallBundleWithPopulatedDataAreaOk() throws Exception { + // Install a first version, in which we're going to change the data area of a bundle... + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource().setVersion("1.0.0").setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + Bundle bundle1 = getBundle("testbundles.bundle1"); + assertNotNull("Unable to get installed test bundle?!", bundle1); + + File dataArea = bundle1.getDataFile(""); + assertNotNull("No data area obtained for test bundle?!", dataArea); + + // Populate the data area... + assertTrue("No file created?!", new File(dataArea, "file1").createNewFile()); + assertTrue("No file created?!", new File(dataArea, "file2").createNewFile()); + assertTrue("No file created?!", new File(dataArea, "file3").createNewFile()); + + dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1"); + dpBuilder + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))) + .add(dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml"))); + + dp = installDeploymentPackage(dpBuilder); // should succeed! + + // We should no longer have this bundle.. + bundle1 = getBundle("testbundles.bundle1"); + assertNull("Unable to get installed test bundle?!", bundle1); + + // Data area should be restored exactly as-is... + assertFalse("Data area not purged?!", dataArea.exists()); + } + + /** + * Tests that we can correctly install a deployment package with bundles that have their data area populated. + */ + @Test + public void testRollbackUninstallBundleWithPopulatedDataAreaOk() throws Exception { + // Install a first version, in which we're going to change the data area of a bundle... + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource().setVersion("1.0.0").setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + Bundle bundle1 = getBundle("testbundles.bundle1"); + assertNotNull("Unable to get installed test bundle?!", bundle1); + + File dataArea = bundle1.getDataFile(""); + assertNotNull("No data area obtained for test bundle?!", dataArea); + + // Populate the data area... + assertTrue("No file created?!", new File(dataArea, "file1").createNewFile()); + assertTrue("No file created?!", new File(dataArea, "file2").createNewFile()); + assertTrue("No file created?!", new File(dataArea, "file3").createNewFile()); + + // This will cause the new bundle to fail in its stop method... + System.setProperty("rp1", "process"); + + dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1"); + dpBuilder + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))) + .add(dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml"))); + + try { + dp = installDeploymentPackage(dpBuilder); // should fail! + fail("Deployment of upgrade package should have failed?!"); + } + catch (DeploymentException e) { + // Ok; expected... + } + + // We should still have this bundle.. + bundle1 = getBundle("testbundles.bundle1"); + assertNotNull("Unable to get installed test bundle?!", bundle1); + + dataArea = bundle1.getDataFile(""); + assertNotNull("No data area obtained for test bundle?!", dataArea); + + // Data area should be restored exactly as-is... + assertTrue("File not restored?!", new File(dataArea, "file1").exists()); + assertTrue("File not restored?!", new File(dataArea, "file2").exists()); + assertTrue("File not restored?!", new File(dataArea, "file3").exists()); + } +} diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/InstallFixPackageTest.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/InstallFixPackageTest.java new file mode 100644 index 00000000000..d45a1eba87e --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/InstallFixPackageTest.java @@ -0,0 +1,603 @@ +/* + * 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.itest; + +import static org.osgi.service.deploymentadmin.DeploymentException.CODE_BAD_HEADER; +import static org.osgi.service.deploymentadmin.DeploymentException.CODE_BUNDLE_SHARING_VIOLATION; +import static org.osgi.service.deploymentadmin.DeploymentException.CODE_MISSING_BUNDLE; +import static org.osgi.service.deploymentadmin.DeploymentException.CODE_MISSING_FIXPACK_TARGET; +import static org.osgi.service.deploymentadmin.DeploymentException.CODE_MISSING_RESOURCE; + +import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.osgi.framework.Bundle; +import org.osgi.service.deploymentadmin.DeploymentException; +import org.osgi.service.deploymentadmin.DeploymentPackage; + +/** + * Provides test cases regarding the use of "fix-packages" in DeploymentAdmin. + */ +@RunWith(PaxExam.class) +public class InstallFixPackageTest extends BaseIntegrationTest +{ + + /** + * Tests that we can install a new bundle through a fix-package. + */ + @Test + public void testInstallBundleWithDependencyInFixPackageUpdateOk() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2"))); + + DeploymentPackage dp1 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp1); + + awaitRefreshPackagesEvent(); + + Bundle bundle = dp1.getBundle(getSymbolicName("bundle2")); + assertNotNull("Failed to obtain bundle from deployment package?!", bundle); + + assertEquals(Bundle.INSTALLED, bundle.getState()); + + dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1"); + dpBuilder.setFixPackage("[1.0,2.0)").add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))).add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2")).setMissing()); + + DeploymentPackage dp2 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp2); + + awaitRefreshPackagesEvent(); + + bundle = dp2.getBundle(getSymbolicName("bundle2")); + assertNotNull("Failed to obtain bundle from bundle context?!", bundle); + + assertBundleExists(getSymbolicName("bundle1"), "1.0.0"); + assertBundleExists(getSymbolicName("bundle2"), "1.0.0"); + + assertTrue(isBundleActive(bundle)); + } + + /** + * Tests that it is not possible to install a fix package if it specifies a fix-version range that falls outside the installed target deployment package. + */ + @Test + public void testInstallFixPackageOutsideLowerTargetRangeFail() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2"))); + + DeploymentPackage dp1 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp1); + + awaitRefreshPackagesEvent(); + + Bundle bundle = dp1.getBundle(getSymbolicName("bundle2")); + assertNotNull("Failed to obtain bundle from deployment package?!", bundle); + + assertEquals(Bundle.INSTALLED, bundle.getState()); + + dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1"); + dpBuilder.setFixPackage("(1.0,2.0)") // should not include version 1.0.0! + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))).add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2")).setMissing()); + + try + { + installDeploymentPackage(dpBuilder); + fail("Succeeded into installing fix package for undefined target package?!"); + } + catch (DeploymentException exception) + { + // Ok; expected + assertDeploymentException(CODE_MISSING_FIXPACK_TARGET, exception); + } + } + + /** + * Tests that it is not possible to install a fix package if it specifies a fix-version range that falls outside the installed target deployment package. + */ + @Test + public void testInstallFixPackageOutsideUpperTargetRangeFail() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2"))); + + DeploymentPackage dp1 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp1); + + awaitRefreshPackagesEvent(); + + Bundle bundle = dp1.getBundle(getSymbolicName("bundle2")); + assertNotNull("Failed to obtain bundle from deployment package?!", bundle); + + assertEquals(Bundle.INSTALLED, bundle.getState()); + + dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1"); + dpBuilder.setFixPackage("[0.9,1.0)") // should not include version 1.0.0! + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))).add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2")).setMissing()); + + try + { + installDeploymentPackage(dpBuilder); + fail("Succeeded into installing fix package for undefined target package?!"); + } + catch (DeploymentException exception) + { + // Ok; expected + assertDeploymentException(CODE_MISSING_FIXPACK_TARGET, exception); + } + } + + /** + * Tests that a fix package can only be installed after at least one version of the denoted target package is installed. + */ + @Test + public void testInstallFixPackageWithoutTargetFail() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.setFixPackage("[1.0,2.0)").add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))).add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2"))); + + try + { + installDeploymentPackage(dpBuilder); + fail("Should not be able to install fix package without target?!"); + } + catch (DeploymentException exception) + { + // Ok; expected + assertDeploymentException(CODE_MISSING_FIXPACK_TARGET, exception); + } + } + + /** + * Tests that installing a fix-package causes the original target package to be replaced. + */ + @Test + public void testInstallFixPackageReplacesOriginalTargetPackageOk() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2"))); + + DeploymentPackage dp1 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp1); + + assertEquals("Expected only a single deployment package?!", 1, countDeploymentPackages()); + + awaitRefreshPackagesEvent(); + + dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1"); + dpBuilder.setFixPackage("[1.0,2.0)").add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))).add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2")).setMissing()); + + DeploymentPackage dp2 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp2); + + awaitRefreshPackagesEvent(); + + assertEquals("Expected only a single deployment package?!", 1, countDeploymentPackages()); + } + + /** + * Tests that installing a fix-package that mentions a bundle that is not in the target package fails. + */ + @Test + public void testInstallFixPackageWithMissingTargetBundleFail() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + // missed valid-bundle1 as dependency... + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))); + + DeploymentPackage dp1 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp1); + + assertEquals("Expected only a single deployment package?!", 1, countDeploymentPackages()); + + awaitRefreshPackagesEvent(); + + dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1"); + dpBuilder.setFixPackage("[1.0,2.0)").add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle3"))).add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2")).setMissing()); + + try + { + installDeploymentPackage(dpBuilder); + fail("Succeeded into installing a fix-package with a missing bundle on target?!"); + } + catch (DeploymentException exception) + { + assertDeploymentException(CODE_MISSING_BUNDLE, exception); + } + } + + /** + * Tests that installing a fix-package that mentions a resource that is not in the target package fails. + */ + @Test + public void testInstallFixPackageWithMissingTargetResourceFail() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))); + + DeploymentPackage dp1 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp1); + + assertEquals("Expected only a single deployment package?!", 1, countDeploymentPackages()); + + awaitRefreshPackagesEvent(); + + dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1"); + dpBuilder.setFixPackage("[1.0,2.0)").add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))).add( + dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml")).setMissing()); + + try + { + installDeploymentPackage(dpBuilder); + fail("Succeeded into installing a fix-package with a missing bundle on target?!"); + } + catch (DeploymentException exception) + { + assertDeploymentException(CODE_MISSING_RESOURCE, exception); + } + } + + /** + * Tests that installing a fix-package that mentions a resource processor that is not in the target package fails. + */ + @Test + public void testInstallFixPackageWithMissingTargetResourceProcessorFail() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))); + + DeploymentPackage dp1 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp1); + + assertEquals("Expected only a single deployment package?!", 1, countDeploymentPackages()); + + awaitRefreshPackagesEvent(); + + dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1"); + dpBuilder.setFixPackage("[1.0,2.0)").add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1")).setMissing()).add( + dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml"))); + + try + { + installDeploymentPackage(dpBuilder); + fail("Succeeded into installing a fix-package with a missing bundle on target?!"); + } + catch (DeploymentException exception) + { + assertDeploymentException(CODE_MISSING_BUNDLE, exception); + } + } + + /** + * Tests that installing a fix-package that mentions a bundle that does exist (in another DP), but is not in the target package fails. + */ + @Test + public void testInstallFixPackageWithMissingTargetBundleFromOtherPackageFail() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))); + + DeploymentPackage dp1 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp1); + + awaitRefreshPackagesEvent(); + + assertEquals("Expected only a single deployment package?!", 1, countDeploymentPackages()); + + dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2"))); + + DeploymentPackage dp2 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp2); + + awaitRefreshPackagesEvent(); + + assertEquals("Expected only a single deployment package?!", 2, countDeploymentPackages()); + + dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1"); + dpBuilder.setFixPackage("[1.0,2.0)").add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1")).setMissing()).add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle3"))); + + try + { + installDeploymentPackage(dpBuilder); + fail("Succeeded into installing a fix-package with a missing bundle on target?!"); + } + catch (DeploymentException exception) + { + assertDeploymentException(CODE_BUNDLE_SHARING_VIOLATION, exception); + } + } + + /** + * Tests that only in a fix-package bundle can be marked as missing. + */ + @Test + public void testMissingBundlesOnlyInFixPackageFail() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2"))); + + DeploymentPackage dp1 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp1); + + awaitRefreshPackagesEvent(); + + Bundle bundle = dp1.getBundle(getSymbolicName("bundle2")); + assertNotNull("Failed to obtain bundle from deployment package?!", bundle); + + assertEquals(Bundle.INSTALLED, bundle.getState()); + + dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1"); + dpBuilder.disableVerification().add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))).add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2")).setMissing()); + + try + { + installDeploymentPackage(dpBuilder); + fail("Failed to install missing bundle?!"); + } + catch (DeploymentException exception) + { + // Ok; expected... + assertEquals("Invalid exception code?!", CODE_BAD_HEADER, exception.getCode()); + } + } + + /** + * Tests the removal of a bundle through a fix package. + */ + @Test + public void testRemoveBundleInFixPackageUpdateOk() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))).add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2"))); + + DeploymentPackage dp1 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp1); + + awaitRefreshPackagesEvent(); + + Bundle bundle = dp1.getBundle(getSymbolicName("bundle2")); + assertNotNull("Failed to obtain bundle from deployment package?!", bundle); + + assertEquals(Bundle.ACTIVE, bundle.getState()); + + // valid-bundle2 is to be removed by this fix package... + dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1"); + dpBuilder.setFixPackage("[1.0,2.0)").add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1")).setMissing()); + + DeploymentPackage dp2 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp2); + + awaitRefreshPackagesEvent(); + + assertBundleExists(getSymbolicName("bundle1"), "1.0.0"); + assertBundleNotExists(getSymbolicName("bundle2"), "1.0.0"); + } + + /** + * Tests that we can uninstall a fix-package. + */ + @Test + public void testUninstallBundleAddedInFixPackageOk() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2"))); + + DeploymentPackage dp1 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp1); + + awaitRefreshPackagesEvent(); + + // Add valid-bundle1 through fix-package... + dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1"); + dpBuilder.setFixPackage("[1.0,2.0)").add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))).add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2")).setMissing()); + + DeploymentPackage dp2 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp2); + + awaitRefreshPackagesEvent(); + + assertEquals("Expected a single deployment package?!", 1, countDeploymentPackages()); + + assertBundleExists(getSymbolicName("bundle1"), "1.0.0"); + assertBundleExists(getSymbolicName("bundle2"), "1.0.0"); + + // Uninstall the deployment package; should yield the original situation again... + dp2.uninstall(); + + awaitRefreshPackagesEvent(); + + assertEquals("Expected no deployment package?!", 0, countDeploymentPackages()); + + // None of our installed bundles should remain... + assertBundleNotExists(getSymbolicName("bundle1"), "1.0.0"); + assertBundleNotExists(getSymbolicName("bundle2"), "1.0.0"); + } + + /** + * Tests that we can uninstall a fix-package. + */ + @Test + public void testUninstallBundleRemovedInFixPackageOk() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))).add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2"))); + + DeploymentPackage dp1 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp1); + + awaitRefreshPackagesEvent(); + + // remove valid-bundle1 through fix package... + dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1"); + dpBuilder.setFixPackage("[1.0,2.0)").add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))); + + DeploymentPackage dp2 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp2); + + awaitRefreshPackagesEvent(); + + assertEquals("Expected a single deployment package?!", 1, countDeploymentPackages()); + + assertBundleExists(getSymbolicName("bundle1"), "1.0.0"); + assertBundleNotExists(getSymbolicName("bundle2"), "1.0.0"); + + // Uninstall the deployment package; should yield the initial situation again... + dp2.uninstall(); + + awaitRefreshPackagesEvent(); + + assertEquals("Expected no deployment package?!", 0, countDeploymentPackages()); + + // None of our installed bundles should remain... + assertBundleNotExists(getSymbolicName("bundle1"), "1.0.0"); + assertBundleNotExists(getSymbolicName("bundle2"), "1.0.0"); + } + + /** + * Tests that we can uninstall a fix-package and that this will only uninstall the bundles installed by the fix-package. + */ + @Test + public void testUninstallFixPackageOnlyRemovesOwnArtifactsOk() throws Exception + { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))); + + DeploymentPackage dp1 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp1); + + assertEquals("Expected a single deployment package?!", 1, countDeploymentPackages()); + + dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2"))); + + DeploymentPackage dp2 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp2); + + awaitRefreshPackagesEvent(); + + assertEquals("Expected two deployment packages?!", 2, countDeploymentPackages()); + + // add bundle2 through fix package... + dpBuilder = createDeploymentPackageBuilder(dpBuilder.getSymbolicName(), "1.0.1"); + dpBuilder.setFixPackage("[1.0,2.0)").add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle3"))).add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle2")).setMissing()); + + DeploymentPackage dp3 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp3); + + awaitRefreshPackagesEvent(); + + assertEquals("Expected two deployment packages?!", 2, countDeploymentPackages()); + + assertBundleExists(getSymbolicName("bundle1"), "1.0.0"); + assertBundleExists(getSymbolicName("bundle2"), "1.0.0"); + assertBundleExists(getSymbolicName("bundle3"), "1.0.0"); + + // Uninstall the deployment package; should yield the initial situation again... + dp3.uninstall(); + + awaitRefreshPackagesEvent(); + + assertEquals("Expected a single deployment package?!", 1, countDeploymentPackages()); + + // None of our installed bundles should remain... + assertBundleNotExists(getSymbolicName("bundle3"), "1.0.0"); + assertBundleNotExists(getSymbolicName("bundle2"), "1.0.0"); + // The bundle installed in another deployment package should still remain... + assertBundleExists(getSymbolicName("bundle1"), "1.0.0"); + } + + @Test + public void testInstallAndUpdateImplementationBundleWithSeparateAPIBundle_FELIX4184() throws Exception + { + String value = System.getProperty("org.apache.felix.deploymentadmin.stopunaffectedbundle"); + System.setProperty("org.apache.felix.deploymentadmin.stopunaffectedbundle", "false"); + // first, install a deployment package with implementation and api bundles in version 1.0.0 + + DeploymentPackageBuilder dpBuilder = createDeploymentPackageBuilder("a", "1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleimpl1", "bundleimpl1", "1.0.0"))); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleapi1", "bundleapi1", "1.0.0"))); + + DeploymentPackage dp1 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp1); + + assertEquals("Expected a single deployment package?!", 1, countDeploymentPackages()); + + // then, install a fix package with implementation and api bundles in version 2.0.0 + dpBuilder = createDeploymentPackageBuilder("a", "2.0.0").setFixPackage("[1.0.0,2.0.0]"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleimpl2", "bundleimpl2", "2.0.0"))); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleapi2", "bundleapi2", "2.0.0"))); + + DeploymentPackage dp2 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp2); + + awaitRefreshPackagesEvent(); + + assertBundleExists(getSymbolicName("bundleimpl"), "2.0.0"); + assertBundleExists(getSymbolicName("bundleapi"), "2.0.0"); + assertBundleNotExists(getSymbolicName("bundleimpl"), "1.0.0"); + assertBundleNotExists(getSymbolicName("bundleapi"), "1.0.0"); + if (value != null) + { + System.setProperty("org.apache.felix.deploymentadmin.stopunaffectedbundle", value); + } + } + + /** + * Tests that if we try to update with a DP containing a duplicate bundle (of which one is already installed in + * an earlier DP) that this will fail the installation. See FELIX-4463. + */ + @Test + public void testUpdateWithDuplicateBundleFail() throws Exception + { + DeploymentPackageBuilder dpBuilder = createDeploymentPackageBuilder("c", "1.0.0"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleimpl1", "bundleimpl1", "1.0.0"))); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleapi1", "bundleapi1", "1.0.0"))); + + // Should succeed, as the DP is correct... + DeploymentPackage dp1 = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp1); + + // then, install a fix package with implementation and api bundles in version 2.0.0, but *also* containing the original implementation + // bundle, which is incorrect (no bundles with the same BSN may exist in a DP)... + dpBuilder = createDeploymentPackageBuilder("c", "2.0.0").setFixPackage("[1.0.0,2.0.0]"); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleapi1", "bundleapi1", "1.0.0"))); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleimpl2", "bundleimpl2", "2.0.0"))); + dpBuilder.add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundleimpl1", "bundleimpl1", "1.0.0"))); + + try + { + installDeploymentPackage(dpBuilder); + fail("DeploymentException expected!"); + } + catch (DeploymentException e) + { + // Ok; expected... + assertEquals(DeploymentException.CODE_OTHER_ERROR, e.getCode()); + } + + awaitRefreshPackagesEvent(); + + // Nothing should be updated... + assertBundleExists(getSymbolicName("bundleimpl"), "1.0.0"); + assertBundleExists(getSymbolicName("bundleapi"), "1.0.0"); + } +} diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/ResourceSharingTest.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/ResourceSharingTest.java new file mode 100644 index 00000000000..31d420af4c7 --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/ResourceSharingTest.java @@ -0,0 +1,132 @@ +/* + * 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.itest; + +import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder; +import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder.JarManifestManipulatingFilter; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.osgi.framework.Bundle; +import org.osgi.service.deploymentadmin.DeploymentAdmin; +import org.osgi.service.deploymentadmin.DeploymentException; +import org.osgi.service.deploymentadmin.DeploymentPackage; + +/** + * Generic tests for {@link DeploymentAdmin}. + */ +@RunWith(PaxExam.class) +public class ResourceSharingTest extends BaseIntegrationTest { + + @Test + public void testBundleCanBelongToOneDeploymentPackageOnly() throws Exception { + DeploymentPackageBuilder dpBuilder1 = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder1 + .add(dpBuilder1.createBundleResource() + .setUrl(getTestBundleURL("bundle1")) + ) + .add(dpBuilder1.createBundleResource() + .setUrl(getTestBundleURL("bundle2")) + ); + + DeploymentPackageBuilder dpBuilder2 = createNewDeploymentPackageBuilder("0.8.0"); + dpBuilder2 + .add(dpBuilder2.createBundleResource() + .setUrl(getTestBundleURL("bundle1")) + ); + + DeploymentPackage dp1 = installDeploymentPackage(dpBuilder1); + assertNotNull("No deployment package returned?!", dp1); + + awaitRefreshPackagesEvent(); + + try { + // should fail: valid-bundle1 belongs to another DP... + installDeploymentPackage(dpBuilder2); + fail("Expected a DeploymentException with code " + DeploymentException.CODE_BUNDLE_SHARING_VIOLATION); + } + catch (DeploymentException exception) { + // Ok; expected... + assertEquals(DeploymentException.CODE_BUNDLE_SHARING_VIOLATION, exception.getCode()); + } + } + + @Test + public void testBundleCannotBeSharedWithNonDeploymentPackagedBundle() throws Exception { + // Manually install a bundle... + Bundle result = m_context.installBundle(getTestBundleURL("bundle1").toExternalForm()); + assertNotNull(result); + + assertBundleExists(getSymbolicName("bundle1"), "1.0.0"); + + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource() + .setUrl(getTestBundleURL("bundle1")) + ) + .add(dpBuilder.createBundleResource() + .setUrl(getTestBundleURL("bundle2")) + ); + + try { + // should fail: valid-bundle1 is installed, but does not belong to any DP... + installDeploymentPackage(dpBuilder); + fail("Expected a DeploymentException with code " + DeploymentException.CODE_BUNDLE_SHARING_VIOLATION); + } + catch (DeploymentException exception) { + // Ok; expected... + assertEquals(DeploymentException.CODE_BUNDLE_SHARING_VIOLATION, exception.getCode()); + } + } + + @Test + public void testForeignBundleCanCoexistWithPackagedBundleIfVersionsDiffer() throws Exception { + // Manually install a bundle... + Bundle result = m_context.installBundle(getTestBundleURL("bundle1").toExternalForm()); + assertNotNull(result); + + long bundleId = result.getBundleId(); + + assertBundleExists(getSymbolicName("bundle1"), "1.0.0"); + assertTrue(isBundleInstalled(result)); + + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource() + .setVersion("1.1.0") + .setUrl(getTestBundleURL("bundle1")) + .setFilter(new JarManifestManipulatingFilter("Bundle-Version", "1.1.0")) + ) + .add(dpBuilder.createBundleResource() + .setUrl(getTestBundleURL("bundle2")) + ); + + // should succeed: valid-bundle1 is installed, but has a different version than the one in our DP... + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + + awaitRefreshPackagesEvent(); + + assertBundleExists(getSymbolicName("bundle1"), "1.0.0"); + assertBundleExists(getSymbolicName("bundle1"), "1.1.0"); + + // The manually installed bundle should still be in the installed or resolved state... + assertTrue(isBundleInstalled(m_context.getBundle(bundleId)) || isBundleResolved(m_context.getBundle(bundleId))); + assertTrue(isBundleActive(dp.getBundle(getSymbolicName("bundle1")))); + } +} diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/UninstallDeploymentPackageTest.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/UninstallDeploymentPackageTest.java new file mode 100644 index 00000000000..f10c029cf99 --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/UninstallDeploymentPackageTest.java @@ -0,0 +1,311 @@ +/* + * 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.itest; + +import static org.osgi.service.deploymentadmin.DeploymentException.CODE_COMMIT_ERROR; +import static org.osgi.service.deploymentadmin.DeploymentException.CODE_OTHER_ERROR; +import static org.osgi.service.deploymentadmin.DeploymentException.CODE_PROCESSOR_NOT_FOUND; + +import org.apache.felix.deploymentadmin.itest.util.DeploymentPackageBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.osgi.framework.Bundle; +import org.osgi.service.deploymentadmin.DeploymentException; +import org.osgi.service.deploymentadmin.DeploymentPackage; + +/** + * Provides test cases regarding the use of "normal" deployment packages in DeploymentAdmin. + */ +@RunWith(PaxExam.class) +public class UninstallDeploymentPackageTest extends BaseIntegrationTest { + + /** + * Tests that if a resource processor is missing (uninstalled) during the forced uninstallation of a deployment package this will ignored and the uninstall completes. + */ + @Test + public void testForcedUninstallDeploymentPackageWithMissingResourceProcessorSucceeds() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))) + .add(dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + assertTrue("Two bundles should be started!", getCurrentBundles().size() == 2); + + Bundle rpBundle = dp.getBundle(getSymbolicName("rp1")); + rpBundle.uninstall(); + + assertTrue("One bundle should be started!", getCurrentBundles().size() == 1); + + assertEquals("Expected no deployment package?!", 1, countDeploymentPackages()); + + assertTrue(dp.uninstallForced()); + + // FELIX-4484: after a forced uninstall, the DP should be marked as stale... + assertTrue(dp.isStale()); + + assertTrue("No bundle should be started!", getCurrentBundles().isEmpty()); + + assertEquals("Expected no deployment package?!", 0, countDeploymentPackages()); + } + + /** + * Tests that uninstalling a DP containing a bundle along with a fragment bundle succeeds (DA should not try to stop the fragment, see FELIX-4167). + */ + @Test + public void testUninstallBundleWithFragmentOk() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("fragment1"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + assertBundleExists(getSymbolicName("bundle1"), "1.0.0"); + assertBundleExists(getSymbolicName("fragment1"), "1.0.0"); + + assertTrue(isBundleActive(dp.getBundle(getSymbolicName("bundle1")))); + assertFalse(isBundleActive(dp.getBundle(getSymbolicName("fragment1")))); + + // Should succeed... + dp.uninstall(); + + assertEquals("Expected no deployment package?!", 0, countDeploymentPackages()); + + // Both bundles should be uninstalled... + assertBundleNotExists(getSymbolicName("bundle1"), "1.0.0"); + assertBundleNotExists(getSymbolicName("fragment1"), "1.0.0"); + } + + /** + * Tests that uninstalling a DP with a bundle along with other (non-bundle) artifacts succeeds. + */ + @Test + public void testUninstallBundleWithOtherArtifactsOk() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))) + .add( + dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1) + .setUrl(getTestResource("test-config1.xml"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle3"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + // Though the commit failed; the package should be installed... + assertBundleExists(getSymbolicName("rp1"), "1.0.0"); + assertBundleExists(getSymbolicName("bundle3"), "1.0.0"); + + assertEquals("Expected a single deployment package?!", 1, countDeploymentPackages()); + + // Should succeed... + dp.uninstall(); + + assertEquals("Expected no deployment package?!", 0, countDeploymentPackages()); + + assertBundleNotExists(getSymbolicName("rp1"), "1.0.0"); + assertBundleNotExists(getSymbolicName("bundle3"), "1.0.0"); + } + + /** + * Tests that if an exception is thrown during the commit-phase, the installation is continued normally. + */ + @Test + public void testUninstallDeploymentPackageWithExceptionThrowingInCommitCausesNoRollbackOk() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))) + .add(dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + assertTrue("Two bundles should be started!", getCurrentBundles().size() == 2); + + assertEquals("Expected no deployment package?!", 1, countDeploymentPackages()); + + System.setProperty("rp1", "commit"); + + dp.uninstall(); + + assertTrue("No bundles should be started! " + getCurrentBundles(), getCurrentBundles().isEmpty()); + + assertEquals("Expected no deployment package?!", 0, countDeploymentPackages()); + } + + /** + * Tests that if an exception is thrown during the dropping of a resource, the installation is rolled back. + */ + @Test + public void testUninstallDeploymentPackageWithExceptionThrowingInDropAllResourcesCausesRollbackOk() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))) + .add(dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + assertTrue("Two bundles should be started!", getCurrentBundles().size() == 2); + + assertEquals("Expected no deployment package?!", 1, countDeploymentPackages()); + + System.setProperty("rp1", "dropAllResources"); + + try { + dp.uninstall(); + fail("Expected uninstall to fail and rollback!"); + } + catch (DeploymentException exception) { + // Ok; expected + assertDeploymentException(CODE_OTHER_ERROR, exception); + } + + // FELIX-4484: only after a successful uninstall, the DP should be marked as stale... + assertFalse(dp.isStale()); + + assertTrue("Two bundles should be started!", getCurrentBundles().size() == 2); + + assertEquals("Expected no deployment package?!", 1, countDeploymentPackages()); + } + + /** + * Tests that if an exception is thrown during the prepare-phase, the installation is rolled back. + */ + @Test + public void testUninstallDeploymentPackageWithExceptionThrowingInPrepareCausesRollbackOk() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))) + .add(dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + assertTrue("Two bundles should be started!", getCurrentBundles().size() == 2); + + assertEquals("Expected no deployment package?!", 1, countDeploymentPackages()); + + System.setProperty("rp1", "prepare"); + + try { + dp.uninstall(); + fail("Expected uninstall to fail and rollback!"); + } + catch (DeploymentException exception) { + // Ok; expected + assertDeploymentException(CODE_COMMIT_ERROR, exception); + } + + assertTrue("Two bundles should be started!", getCurrentBundles().size() == 2); + + assertEquals("Expected no deployment package?!", 1, countDeploymentPackages()); + } + + /** + * Tests that if an exception is thrown during the uninstall of a bundle, the installation/update continues and succeeds. + */ + @Test + public void testUninstallDeploymentPackageWithExceptionThrownInStopCauseNoRollbackOk() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle3"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + assertBundleExists(getSymbolicName("bundle3"), "1.0.0"); + + System.setProperty("bundle3", "stop"); + + dp.uninstall(); // should succeed. + + // FELIX-4484: only after a successful uninstall, the DP should be marked as stale... + assertTrue(dp.isStale()); + + awaitRefreshPackagesEvent(); + + assertEquals("Expected no deployment package?!", 0, countDeploymentPackages()); + + assertTrue("Expected no bundles to remain?!", getCurrentBundles().isEmpty()); + } + + /** + * Tests that if a resource processor is missing (uninstalled) during the uninstallation of a deployment package, this is regarded an error and a rollback is performed. + */ + @Test + public void testUninstallDeploymentPackageWithMissingResourceProcessorCausesRollback() throws Exception { + DeploymentPackageBuilder dpBuilder = createNewDeploymentPackageBuilder("1.0.0"); + dpBuilder + .add(dpBuilder.createBundleResource().setUrl(getTestBundleURL("bundle1"))) + .add(dpBuilder.createResourceProcessorResource().setUrl(getTestBundleURL("rp1"))) + .add(dpBuilder.createResource().setResourceProcessorPID(TEST_FAILING_BUNDLE_RP1).setUrl(getTestResource("test-config1.xml"))); + + DeploymentPackage dp = installDeploymentPackage(dpBuilder); + assertNotNull("No deployment package returned?!", dp); + + awaitRefreshPackagesEvent(); + + assertTrue("Two bundles should be started!", getCurrentBundles().size() == 2); + + Bundle rpBundle = dp.getBundle(getSymbolicName("rp1")); + rpBundle.uninstall(); + + assertTrue("One bundle should be started!", getCurrentBundles().size() == 1); + + assertEquals("Expected no deployment package?!", 1, countDeploymentPackages()); + + try { + dp.uninstall(); + fail("Expected uninstall to fail and rollback!"); + } + catch (DeploymentException exception) { + // Ok; expected + assertDeploymentException(CODE_PROCESSOR_NOT_FOUND, exception); + } + + assertTrue("One bundle should be started!", getCurrentBundles().size() == 1); + + assertEquals("Expected one deployment package?!", 1, countDeploymentPackages()); + } +} diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/ArtifactData.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/ArtifactData.java new file mode 100644 index 00000000000..9ca98e6831c --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/ArtifactData.java @@ -0,0 +1,120 @@ +/* + * 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.itest.util; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +public class ArtifactData { + + private final URL m_url; + private boolean m_isBundle; + private String m_filename; + private String m_bundleSymbolicName; + private String m_bundleVersion; + private boolean m_isCustomizer; + private boolean m_needRP; + private String m_processorPID; + private boolean m_missing; + private ResourceFilter m_filter; + + ArtifactData(URL url, String filename) { + m_url = url; + m_filename = filename; + } + + public final InputStream createInputStream() throws IOException { + if (m_filter != null) { + return m_filter.createInputStream(m_url); + } + return m_url.openStream(); + } + + public String getFilename() { + return m_filename; + } + + public ResourceFilter getFilter() { + return m_filter; + } + + public String getProcessorPid() { + return m_processorPID; + } + + public String getSymbolicName() { + return m_bundleSymbolicName; + } + + public URL getURL() { + return m_url; + } + + public String getVersion() { + return m_bundleVersion; + } + + public boolean isBundle() { + return m_isBundle; + } + + public boolean isLocalizationFile() { + return m_filename.startsWith("OSGI-INF/l10n/"); + } + + public boolean isCustomizer() { + return m_isCustomizer; + } + + public boolean isMissing() { + return m_missing; + } + + public boolean isResourceProcessorNeeded() { + return m_needRP; + } + + void setArtifactResourceProcessor(String processorPID) { + m_processorPID = processorPID; + } + + void setBundleMetadata(String bundleSymbolicName, String bundleVersion) { + m_isBundle = true; + m_bundleSymbolicName = bundleSymbolicName; + m_bundleVersion = bundleVersion; + } + + void setFilter(ResourceFilter filter) { + m_filter = filter; + } + + void setMissing(boolean missing) { + m_missing = missing; + } + + void setNeedResourceProcessor(boolean needRP) { + m_needRP = needRP; + } + + void setResourceProcessor(String processorPID) { + m_isCustomizer = true; + m_processorPID = processorPID; + } +} diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/ArtifactDataBuilder.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/ArtifactDataBuilder.java new file mode 100644 index 00000000000..b2db0600b24 --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/ArtifactDataBuilder.java @@ -0,0 +1,86 @@ +/* + * 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.itest.util; + +import java.net.MalformedURLException; +import java.net.URL; + +/** + * Provides a builder for creating {@link ArtifactData} instances. + */ +public abstract class ArtifactDataBuilder> { + + protected URL m_url; + protected String m_filename; + protected ResourceFilter m_filter; + protected boolean m_missing; + + ArtifactDataBuilder() { + m_filter = null; + m_missing = false; + } + + public TYPE setFilename(String filename) { + m_filename = filename; + return getThis(); + } + + public TYPE setFilter(ResourceFilter filter) { + m_filter = filter; + return getThis(); + } + + public TYPE setMissing() { + return setMissing(true); + } + + public TYPE setMissing(boolean missing) { + m_missing = missing; + return getThis(); + } + + public TYPE setUrl(String url) throws MalformedURLException { + return setUrl(new URL(url)); + } + + public TYPE setUrl(URL url) { + m_url = url; + return getThis(); + } + + ArtifactData build() { + validate(); + + ArtifactData result = new ArtifactData(m_url, m_filename); + result.setFilter(m_filter); + result.setMissing(m_missing); + return result; + } + + abstract TYPE getThis(); + + void validate() throws RuntimeException { + if (m_url == null) { + throw new RuntimeException("URL is missing!"); + } + if (m_filename == null || "".equals(m_filename.trim())) { + throw new RuntimeException("Filename is missing!"); + } + } +} diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/BundleDataBuilder.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/BundleDataBuilder.java new file mode 100644 index 00000000000..9d27e13e3f4 --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/BundleDataBuilder.java @@ -0,0 +1,120 @@ +/* + * 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.itest.util; + +import java.io.File; +import java.io.IOException; +import java.util.jar.Attributes; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; + +/** + * Provides a bundle data builder. + */ +public class BundleDataBuilder> extends ArtifactDataBuilder { + + private String m_symbolicName; + private String m_version; + + public BundleDataBuilder() { + super(); + } + + public TYPE setSymbolicName(String symbolicName) { + m_symbolicName = symbolicName; + return getThis(); + } + + public TYPE setVersion(String version) { + m_version = version; + return getThis(); + } + + @Override + ArtifactData build() { + ArtifactData result = super.build(); + result.setBundleMetadata(m_symbolicName, m_version); + return result; + } + + String getRequiredHeader(Attributes mainAttributes, String headerName) throws RuntimeException { + String value = mainAttributes.getValue(headerName); + if (value == null || value.equals("")) { + throw new RuntimeException("Missing or invalid " + headerName + " header."); + } + return value; + } + + @Override + TYPE getThis() { + return (TYPE) this; + } + + void retrieveAndSetBundleInformation() throws RuntimeException { + JarInputStream jis = null; + try { + jis = new JarInputStream(m_url.openStream()); + + Manifest bundleManifest = jis.getManifest(); + if (bundleManifest == null) { + throw new RuntimeException("Not a valid manifest in: " + m_url); + } + + Attributes attributes = bundleManifest.getMainAttributes(); + + if (m_symbolicName == null || "".equals(m_symbolicName.trim())) { + setSymbolicName(getRequiredHeader(attributes, "Bundle-SymbolicName")); + } + + if (m_version == null || "".equals(m_version.trim())) { + setVersion(getRequiredHeader(attributes, "Bundle-Version")); + } + + if (m_filename == null || "".equals(m_filename.trim())) { + setFilename(new File(m_url.getFile()).getName()); + } + + setAdditionalBundleInformation(bundleManifest); + } + catch (IOException exception) { + throw new RuntimeException("Failed to retrieve bundle information; set symbolic name and version!", exception); + } + finally { + if (jis != null) { + try { + jis.close(); + } + catch (IOException exception) { + // Ignore... + } + } + } + } + + void setAdditionalBundleInformation(Manifest bundleManifest) { + // Nop + } + + @Override + void validate() throws RuntimeException { + retrieveAndSetBundleInformation(); + + super.validate(); + } +} diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/CertificateUtil.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/CertificateUtil.java new file mode 100644 index 00000000000..ad81fbb6fa7 --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/CertificateUtil.java @@ -0,0 +1,123 @@ +/* + * 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.itest.util; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.Random; + +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +/** + * @author Felix Project Team + */ +public class CertificateUtil { + + public enum KeyType { + RSA, DSA, EC; + } + + public static class SignerInfo { + private final KeyPair m_keyPair; + private final X509Certificate m_cert; + + public SignerInfo(KeyPair keyPair, X509Certificate cert) { + m_keyPair = keyPair; + m_cert = cert; + } + + public X509Certificate getCert() { + return m_cert; + } + + public PrivateKey getPrivate() { + return m_keyPair.getPrivate(); + } + + public PublicKey getPublic() { + return m_keyPair.getPublic(); + } + } + + public static SignerInfo createSelfSignedCert(String commonName, KeyType type) throws Exception { + KeyPairGenerator kpGen; + switch (type) { + case RSA: + kpGen = KeyPairGenerator.getInstance("RSA"); + kpGen.initialize(1024); + break; + case DSA: + kpGen = KeyPairGenerator.getInstance("DSA"); + break; + case EC: + kpGen = KeyPairGenerator.getInstance("EC"); + break; + default: + throw new IllegalArgumentException("Invalid key type!"); + } + + KeyPair keyPair = kpGen.generateKeyPair(); + X509Certificate cert = createSelfSignedCert(commonName, keyPair); + + return new SignerInfo(keyPair, cert); + } + + private static X509Certificate createSelfSignedCert(String commonName, KeyPair keypair) throws Exception { + PublicKey publicKey = keypair.getPublic(); + String keyAlg = DPSigner.getSignatureAlgorithm(publicKey); + + X500Name issuer = new X500Name(commonName); + BigInteger serial = BigInteger.probablePrime(16, new Random()); + Date notBefore = new Date(System.currentTimeMillis() - 1000); + Date notAfter = new Date(notBefore.getTime() + 6000); + + SubjectPublicKeyInfo pubKeyInfo; + try (ASN1InputStream is = new ASN1InputStream(publicKey.getEncoded())) { + pubKeyInfo = SubjectPublicKeyInfo.getInstance(is.readObject()); + } + + X509v3CertificateBuilder builder = new X509v3CertificateBuilder(issuer, serial, notBefore, notAfter, issuer, pubKeyInfo); + builder.addExtension(new Extension(Extension.basicConstraints, true, new DEROctetString(new BasicConstraints(false)))); + + X509CertificateHolder certHolder = builder.build(new JcaContentSignerBuilder(keyAlg).build(keypair.getPrivate())); + return new JcaX509CertificateConverter().getCertificate(certHolder); + } + + /** + * Creates a new CertificateUtil instance. + */ + private CertificateUtil() { + // Nop + } + +} diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/DPSigner.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/DPSigner.java new file mode 100644 index 00000000000..351533cbbfb --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/DPSigner.java @@ -0,0 +1,273 @@ +/* + * 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.itest.util; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.security.interfaces.DSAKey; +import java.security.interfaces.ECKey; +import java.security.interfaces.RSAKey; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.apache.commons.codec.binary.Base64; +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.DigestCalculatorProvider; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; + +/** + * Signs a deployment package using a given keypair. + */ +public class DPSigner { + private static final String META_INF = "META-INF/"; + + public static String getSignatureAlgorithm(Key key) { + if (key instanceof RSAKey) { + return "SHA256withRSA"; + } + else if (key instanceof DSAKey) { + return "SHA1withDSA"; + } + else if (key instanceof ECKey) { + return "SHA256withECDSA"; + } + else { + throw new IllegalArgumentException("Invalid/unsupported key: " + key.getClass().getName()); + } + } + private static String getBlockFileExtension(Key key) { + if (key instanceof RSAKey) { + return ".RSA"; + } + else if (key instanceof DSAKey) { + return ".DSA"; + } + else if (key instanceof ECKey) { + return ".EC"; + } + else { + throw new IllegalArgumentException("Invalid/unsupported key: " + key.getClass().getName()); + } + } + private final MessageDigest m_digest; + + private final String m_digestAlg; + + private final String m_baseName; + + public DPSigner() { + this("DP"); + } + + public DPSigner(String baseName) { + try { + m_baseName = META_INF.concat(baseName); + m_digest = MessageDigest.getInstance("SHA-256"); + m_digestAlg = m_digest.getAlgorithm(); + } + catch (NoSuchAlgorithmException e) { + throw new RuntimeException("SHA-256 not supported by default?!"); + } + } + + public void addDigestAttribute(Attributes attrs, ArtifactData file) throws IOException { + attrs.putValue(m_digestAlg.concat("-Digest"), calculateDigest(file)); + } + + public void sign(DeploymentPackageBuilder builder, PrivateKey privKey, X509Certificate cert, OutputStream os) throws Exception { + Manifest manifest = builder.createManifest(); + List artifacts = builder.getArtifactList(); + sign(manifest, artifacts, privKey, cert, os); + } + + public void sign(Manifest manifest, List files, PrivateKey privKey, X509Certificate cert, OutputStream os) throws Exception { + // For each file, add its signature to the manifest + for (ArtifactData file : files) { + String filename = file.getFilename(); + Attributes attrs = manifest.getAttributes(filename); + addDigestAttribute(attrs, file); + } + + try (ZipOutputStream zos = new ZipOutputStream(os)) { + writeSignedManifest(manifest, zos, privKey, cert); + + for (ArtifactData file : files) { + ZipEntry entry = new ZipEntry(file.getFilename()); + zos.putNextEntry(entry); + + try (InputStream is = file.createInputStream()) { + byte[] buf = new byte[1024]; + int read; + while ((read = is.read(buf)) > 0) { + zos.write(buf, 0, read); + } + } + } + } + } + + public void writeSignedManifest(Manifest manifest, ZipOutputStream zos, PrivateKey privKey, X509Certificate cert) throws Exception { + zos.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME)); + manifest.write(zos); + zos.closeEntry(); + + long now = System.currentTimeMillis(); + + // Determine the signature-file manifest... + Manifest sf = createSignatureFile(manifest); + + byte[] sfRawBytes; + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + sf.write(baos); + sfRawBytes = baos.toByteArray(); + } + + ZipEntry sigFileEntry = new ZipEntry(m_baseName.concat(".SF")); + sigFileEntry.setTime(now); + zos.putNextEntry(sigFileEntry); + // Write the actual entry data... + zos.write(sfRawBytes, 0, sfRawBytes.length); + zos.closeEntry(); + + // Create a PKCS#7 signature... + byte[] encoded = calculateSignatureBlock(privKey, cert, sfRawBytes); + + ZipEntry blockFileEntry = new ZipEntry(m_baseName.concat(getBlockFileExtension(privKey))); + blockFileEntry.setTime(now); + zos.putNextEntry(blockFileEntry); + zos.write(encoded); + zos.closeEntry(); + } + + private String calculateDigest(ArtifactData file) throws IOException { + m_digest.reset(); + try (InputStream is = file.createInputStream()) { + byte[] buffer = new byte[1024]; + int read; + while ((read = is.read(buffer)) > 0) { + m_digest.update(buffer, 0, read); + } + } + return Base64.encodeBase64String(m_digest.digest()); + } + + private String calculateDigest(byte[] rawData) throws IOException { + m_digest.reset(); + m_digest.update(rawData, 0, rawData.length); + return Base64.encodeBase64String(m_digest.digest()); + } + + private byte[] calculateSignatureBlock(PrivateKey privKey, X509Certificate cert, byte[] sfRawBytes) throws Exception { + String signatureAlgorithm = getSignatureAlgorithm(privKey); + + DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().build(); + ContentSigner signer = new JcaContentSignerBuilder(signatureAlgorithm).build(privKey); + + CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); + gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider).build(signer, cert)); + gen.addCertificates(new JcaCertStore(Arrays.asList(cert))); + + CMSSignedData sigData = gen.generate(new CMSProcessableByteArray(sfRawBytes)); + + return sigData.getEncoded(); + } + + private Manifest createSignatureFile(Manifest manifest) throws IOException { + byte[] mfRawBytes; + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + manifest.write(baos); + mfRawBytes = baos.toByteArray(); + } + + Manifest sf = new Manifest(); + Attributes sfMain = sf.getMainAttributes(); + Map sfEntries = sf.getEntries(); + + sfMain.put(Attributes.Name.SIGNATURE_VERSION, "1.0"); + sfMain.putValue("Created-By", "Apache Felix DeploymentPackageBuilder"); + sfMain.putValue(m_digestAlg + "-Digest-Manifest", calculateDigest(mfRawBytes)); + sfMain.putValue(m_digestAlg + "-Digest-Manifest-Main-Attribute", calculateDigest(getRawBytesMainAttributes(manifest))); + + for (Entry entry : manifest.getEntries().entrySet()) { + String name = entry.getKey(); + byte[] entryData = getRawBytesAttributes(entry.getValue()); + + sfEntries.put(name, getDigestAttributes(entryData)); + } + return sf; + } + + private Attributes getDigestAttributes(byte[] rawData) throws IOException { + Attributes attrs = new Attributes(); + attrs.putValue(m_digestAlg + "-Digest", calculateDigest(rawData)); + return attrs; + } + + private byte[] getRawBytesAttributes(Attributes attrs) throws IOException { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos)) { + + Method m = Attributes.class.getDeclaredMethod("write", DataOutputStream.class); + m.setAccessible(true); + m.invoke(attrs, dos); + + return baos.toByteArray(); + } + catch (NoSuchMethodException | SecurityException | InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException("Failed to get raw bytes of main attributes!", e); + } + } + + private byte[] getRawBytesMainAttributes(Manifest manifest) throws IOException { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos)) { + Attributes attrs = manifest.getMainAttributes(); + + Method m = Attributes.class.getDeclaredMethod("writeMain", DataOutputStream.class); + m.setAccessible(true); + m.invoke(attrs, dos); + + return baos.toByteArray(); + } + catch (NoSuchMethodException | SecurityException | InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException("Failed to get raw bytes of main attributes!", e); + } + } +} diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/DeploymentPackageBuilder.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/DeploymentPackageBuilder.java new file mode 100644 index 00000000000..a050ef32568 --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/DeploymentPackageBuilder.java @@ -0,0 +1,437 @@ +/* + * 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.itest.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +import org.osgi.framework.Version; + +/** + * Builder for deployment packages. Can handle bundles, resource processors and artifacts. + */ +public class DeploymentPackageBuilder { + + /** + * Convenience resource filter for manipulating JAR manifests. + */ + public abstract static class JarManifestFilter implements ResourceFilter { + + public final InputStream createInputStream(URL url) throws IOException { + byte[] buffer = new byte[BUFFER_SIZE]; + + JarInputStream jis = new JarInputStream(url.openStream()); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + JarOutputStream jos = new JarOutputStream(baos, filterManifest(jis.getManifest())); + + JarEntry input; + while ((input = jis.getNextJarEntry()) != null) { + jos.putNextEntry(input); + int read; + while ((read = jis.read(buffer)) > 0) { + jos.write(buffer, 0, read); + } + jos.closeEntry(); + } + jos.close(); + jis.close(); + + return new ByteArrayInputStream(baos.toByteArray()); + } + + protected abstract Manifest filterManifest(Manifest manifest); + } + + /** + * Simple manifest JAR manipulator implementation. + */ + public static class JarManifestManipulatingFilter extends JarManifestFilter { + private final String[] m_replacementEntries; + + public JarManifestManipulatingFilter(String... replacementEntries) { + if (replacementEntries == null || ((replacementEntries.length) % 2 != 0)) { + throw new IllegalArgumentException("Entries must be a multiple of two!"); + } + m_replacementEntries = Arrays.copyOf(replacementEntries, replacementEntries.length); + } + + @Override + protected Manifest filterManifest(Manifest manifest) { + for (int i = 0; i < m_replacementEntries.length; i += 2) { + String key = m_replacementEntries[i]; + String value = m_replacementEntries[i + 1]; + manifest.getMainAttributes().putValue(key, value); + } + return manifest; + } + } + + private static final int BUFFER_SIZE = 32 * 1024; + + private final DPSigner m_signer; + private final String m_symbolicName; + private final String m_version; + private final List m_localizationFiles = new ArrayList(); + private final List m_bundles = new ArrayList(); + private final List m_processors = new ArrayList(); + private final List m_artifacts = new ArrayList(); + + private String m_fixPackageVersion; + private boolean m_verification; + private PrivateKey m_signingKey; + private X509Certificate m_signingCert; + + private DeploymentPackageBuilder(String symbolicName, String version) { + m_symbolicName = symbolicName; + m_version = version; + + m_verification = true; + m_signer = new DPSigner(); + } + + /** + * Creates a new deployment package builder. + * + * @param name the name of the deployment package + * @param version the version of the deployment package + * @return a builder to further add data to the deployment package + */ + public static DeploymentPackageBuilder create(String name, String version) { + return new DeploymentPackageBuilder(name, version); + } + + static void closeSilently(Closeable resource) { + if (resource != null) { + try { + resource.close(); + } + catch (IOException e) { + // Ignore... + } + } + } + + /** + * Adds an artifact to the deployment package. + * + * @param builder the artifact data builder to use. + * @return this builder. + * @throws Exception if something goes wrong while building the artifact. + */ + public DeploymentPackageBuilder add(ArtifactDataBuilder builder) throws Exception { + ArtifactData artifactData = builder.build(); + if (artifactData.isCustomizer()) { + m_processors.add(artifactData); + } + else if (artifactData.isBundle()) { + m_bundles.add(artifactData); + } + else if (artifactData.isLocalizationFile()) { + m_localizationFiles.add(artifactData); + } + else { + m_artifacts.add(artifactData); + } + return this; + } + + /** + * Creates a new deployment package builder with the same symbolic name as this builder. + * + * @param name the name of the deployment package + * @param version the version of the deployment package + * @return a builder to further add data to the deployment package + */ + public DeploymentPackageBuilder create(String version) { + return new DeploymentPackageBuilder(getSymbolicName(), version); + } + + public BundleDataBuilder createBundleResource() { + return new BundleDataBuilder(); + } + + public LocalizationResourceDataBuilder createLocalizationResource() { + return new LocalizationResourceDataBuilder(); + } + + public ResourceDataBuilder createResource() { + return new ResourceDataBuilder(); + } + + public ResourceProcessorDataBuilder createResourceProcessorResource() { + return new ResourceProcessorDataBuilder(); + } + + /** + * Disables the verification of the generated deployment package, potentially causing an erroneous result to be + * generated. + * + * @return this builder. + */ + public DeploymentPackageBuilder disableVerification() { + m_verification = false; + return this; + } + + /** + * Generates a deployment package and streams it to the output stream you provide. Before + * it starts generating, it will first validate that you have actually specified a + * resource processor for each type of artifact you provided. + * + * @return the input stream containing the deployment package. + * @throws Exception if something goes wrong while validating or generating + */ + public InputStream generate() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + generate(baos); + return new ByteArrayInputStream(baos.toByteArray()); + } + + /** + * Generates a deployment package and streams it to the output stream you provide. Before + * it starts generating, it will first validate that you have actually specified a + * resource processor for each type of artifact you provided. + * + * @param output the output stream to write to + * @throws Exception if something goes wrong while validating or generating + */ + public void generate(OutputStream output) throws Exception { + Manifest m = createManifest(); + List artifacts = getArtifactList(); + writeStream(artifacts, m, output); + } + + /** + * @return the symbolic name of the deployment package. + */ + public String getSymbolicName() { + return m_symbolicName; + } + + /** + * @return the version of the deployment package. + */ + public String getVersion() { + return m_version; + } + + /** + * Marks this deployment package as a 'fix' package. + * + * @return this builder. + */ + public DeploymentPackageBuilder setFixPackage() { + Version v1 = new Version(m_version); + Version v2 = new Version(v1.getMajor() + 1, 0, 0); + String version = String.format("[%d.%d, %d.%d)", v1.getMajor(), v1.getMinor(), v2.getMajor(), v2.getMinor()); + return setFixPackage(version); + } + + /** + * Marks this deployment package as a 'fix' package. + * + * @param versionRange the version range in which this fix-package should be applied. + * @return this builder. + */ + public DeploymentPackageBuilder setFixPackage(String versionRange) { + m_fixPackageVersion = versionRange; + return this; + } + + /** + * Enables the creating of a signed deployment package, equivalent to creating a signed JAR file. + *

      + * This method assumes the use of self-signed certificates for the signing process. + *

      + * + * @param signingKey the private key of the signer; + * @param signingCert the public certificate of the signer. + * @return this builder. + */ + public DeploymentPackageBuilder signOutput(PrivateKey signingKey, X509Certificate signingCert) { + m_signingKey = signingKey; + m_signingCert = signingCert; + return this; + } + + final Manifest createManifest() throws Exception { + List artifacts = new ArrayList(); + artifacts.addAll(m_localizationFiles); + artifacts.addAll(m_bundles); + artifacts.addAll(m_processors); + artifacts.addAll(m_artifacts); + + if (m_verification) { + validateProcessedArtifacts(); + validateMissingArtifacts(artifacts); + } + + return createManifest(artifacts); + } + + final List getArtifactList() { + // The order in which the actual entries are added to the JAR is different than we're using for the manifest... + List artifacts = new ArrayList(); + artifacts.addAll(m_bundles); + artifacts.addAll(m_processors); + artifacts.addAll(m_localizationFiles); + artifacts.addAll(m_artifacts); + return artifacts; + } + + private Manifest createManifest(List files) throws Exception { + Manifest manifest = new Manifest(); + Attributes main = manifest.getMainAttributes(); + main.putValue("Manifest-Version", "1.0"); + main.putValue("DeploymentPackage-SymbolicName", m_symbolicName); + main.putValue("DeploymentPackage-Version", m_version); + + if ((m_fixPackageVersion != null) && !"".equals(m_fixPackageVersion)) { + main.putValue("DeploymentPackage-FixPack", m_fixPackageVersion); + } + + Map entries = manifest.getEntries(); + + for (ArtifactData file : files) { + Attributes attrs = new Attributes(); + attrs.putValue("Name", file.getFilename()); + + if (file.isBundle()) { + attrs.putValue("Bundle-SymbolicName", file.getSymbolicName()); + attrs.putValue("Bundle-Version", file.getVersion()); + if (file.isCustomizer()) { + attrs.putValue("DeploymentPackage-Customizer", "true"); + attrs.putValue("Deployment-ProvidesResourceProcessor", file.getProcessorPid()); + } + } + else if (file.isResourceProcessorNeeded()) { + attrs.putValue("Resource-Processor", file.getProcessorPid()); + } + + if (file.isMissing()) { + attrs.putValue("DeploymentPackage-Missing", "true"); + } + + if (isAddSignatures()) { + m_signer.addDigestAttribute(attrs, file); + } + + entries.put(file.getFilename(), attrs); + } + + return manifest; + } + + private boolean isAddSignatures() { + return m_signingKey != null && m_signingCert != null; + } + + private void validateMissingArtifacts(List files) throws Exception { + boolean missing = false; + + Iterator artifactIter = files.iterator(); + while (artifactIter.hasNext() && !missing) { + ArtifactData data = artifactIter.next(); + + if (data.isMissing()) { + missing = true; + } + } + + if (missing && (m_fixPackageVersion == null || "".equals(m_fixPackageVersion))) { + throw new Exception("Artifact cannot be missing without a fix package version!"); + } + } + + private void validateProcessedArtifacts() throws Exception { + Iterator artifactIter = m_artifacts.iterator(); + while (artifactIter.hasNext()) { + ArtifactData data = artifactIter.next(); + String pid = data.getProcessorPid(); + boolean found = pid == null; + + Iterator processorIter = m_processors.iterator(); + while (!found && processorIter.hasNext()) { + ArtifactData processor = processorIter.next(); + if (pid.equals(processor.getProcessorPid())) { + found = true; + } + } + + if (!found && data.isResourceProcessorNeeded()) { + throw new Exception("No resource processor found for artifact " + data.getURL() + " with processor PID " + pid); + } + } + } + + private void writeStream(List files, Manifest manifest, OutputStream outputStream) throws Exception { + byte[] buffer = new byte[BUFFER_SIZE]; + + try (JarOutputStream output = new JarOutputStream(outputStream)) { + // Write out the manifest... + if (isAddSignatures()) { + m_signer.writeSignedManifest(manifest, output, m_signingKey, m_signingCert); + } + else { + output.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME)); + manifest.write(output); + output.closeEntry(); + } + + for (ArtifactData file : files) { + if (file.isMissing()) { + // No need to write the 'missing' files... + continue; + } + + output.putNextEntry(new JarEntry(file.getFilename())); + + try (InputStream is = file.createInputStream()) { + int bytes; + while ((bytes = is.read(buffer)) != -1) { + output.write(buffer, 0, bytes); + } + } + finally { + output.closeEntry(); + } + } + } + } +} diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/LocalizationResourceDataBuilder.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/LocalizationResourceDataBuilder.java new file mode 100644 index 00000000000..a04cea9997f --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/LocalizationResourceDataBuilder.java @@ -0,0 +1,43 @@ +/* + * 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.itest.util; + +/** + * Provides a resource data builder. + */ +public class LocalizationResourceDataBuilder extends ResourceDataBuilder { + + public LocalizationResourceDataBuilder() { + // Nop + } + + @Override + public LocalizationResourceDataBuilder setFilename(String filename) { + String prefix = "OSGI-INF/l10n/"; + if (!filename.startsWith(prefix)) { + filename = prefix.concat(filename); + } + return (LocalizationResourceDataBuilder) super.setFilename(filename); + } + + @Override + LocalizationResourceDataBuilder getThis() { + return this; + } +} diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/ResourceDataBuilder.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/ResourceDataBuilder.java new file mode 100644 index 00000000000..50551eb66ae --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/ResourceDataBuilder.java @@ -0,0 +1,68 @@ +/* + * 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.itest.util; + +import java.io.File; + +/** + * Provides a resource data builder. + */ +public class ResourceDataBuilder extends ArtifactDataBuilder { + private boolean m_needRP; + private String m_resourceProcessorPID; + + public ResourceDataBuilder() { + m_needRP = true; + } + + public ResourceDataBuilder setNeedResourceProcessor(boolean needRP) { + m_needRP = needRP; + return this; + } + + public ResourceDataBuilder setResourceProcessorPID(String resourceProcessorPID) { + m_resourceProcessorPID = resourceProcessorPID; + return this; + } + + @Override ArtifactData build() { + ArtifactData result = super.build(); + result.setArtifactResourceProcessor(m_resourceProcessorPID); + result.setNeedResourceProcessor(m_needRP); + return result; + } + + @Override + ResourceDataBuilder getThis() { + return this; + } + + @Override + void validate() throws RuntimeException { + if (m_needRP && ((m_resourceProcessorPID == null || "".equals(m_resourceProcessorPID.trim())))) { + throw new RuntimeException("Artifact resource processor PID is missing!"); + } + + if (m_filename == null || "".equals(m_filename.trim())) { + setFilename(new File(m_url.getFile()).getName()); + } + + super.validate(); + } +} diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/ResourceFilter.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/ResourceFilter.java new file mode 100644 index 00000000000..5ce03dd4709 --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/ResourceFilter.java @@ -0,0 +1,29 @@ +/* + * 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.itest.util; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +public interface ResourceFilter { + + InputStream createInputStream(URL url) throws IOException; + +} diff --git a/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/ResourceProcessorDataBuilder.java b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/ResourceProcessorDataBuilder.java new file mode 100644 index 00000000000..4170800cdb0 --- /dev/null +++ b/deploymentadmin/itest/src/test/java/org/apache/felix/deploymentadmin/itest/util/ResourceProcessorDataBuilder.java @@ -0,0 +1,66 @@ +/* + * 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.itest.util; + +import java.util.jar.Manifest; + +/** + * Provides a resource processor data builder. + */ +public class ResourceProcessorDataBuilder extends BundleDataBuilder { + + private String m_resourceProcessorPID; + + public ResourceProcessorDataBuilder() { + super(); + } + + public ResourceProcessorDataBuilder setResourceProcessorPID(String resourceProcessorPID) { + m_resourceProcessorPID = resourceProcessorPID; + return this; + } + + @Override ArtifactData build() { + ArtifactData result = super.build(); + result.setResourceProcessor(m_resourceProcessorPID); + return result; + } + + @Override + ResourceProcessorDataBuilder getThis() { + return this; + } + + @Override + void setAdditionalBundleInformation(Manifest bundleManifest) { + String processorPID = getRequiredHeader(bundleManifest.getMainAttributes(), "Deployment-ProvidesResourceProcessor"); + if (m_resourceProcessorPID == null || "".equals(m_resourceProcessorPID.trim())) { + setResourceProcessorPID(processorPID); + } + } + + @Override + void validate() throws RuntimeException { + super.validate(); + + if (m_resourceProcessorPID == null || "".equals(m_resourceProcessorPID.trim())) { + throw new RuntimeException("Resource processor PID is missing!"); + } + } +} diff --git a/deploymentadmin/itest/src/test/resources/LICENSE b/deploymentadmin/itest/src/test/resources/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/deploymentadmin/itest/src/test/resources/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/deploymentadmin/itest/src/test/resources/dp.properties b/deploymentadmin/itest/src/test/resources/dp.properties new file mode 100644 index 00000000000..687d0153f49 --- /dev/null +++ b/deploymentadmin/itest/src/test/resources/dp.properties @@ -0,0 +1,18 @@ +# 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. +# +# no additional properties \ No newline at end of file diff --git a/deploymentadmin/itest/src/test/resources/logback.xml b/deploymentadmin/itest/src/test/resources/logback.xml new file mode 100644 index 00000000000..4d2979def14 --- /dev/null +++ b/deploymentadmin/itest/src/test/resources/logback.xml @@ -0,0 +1,30 @@ + + + + + + %-5level %logger{36} - %msg%n + + + + + + + diff --git a/deploymentadmin/itest/src/test/resources/logo.png b/deploymentadmin/itest/src/test/resources/logo.png new file mode 100644 index 00000000000..dccbddcc35c Binary files /dev/null and b/deploymentadmin/itest/src/test/resources/logo.png differ diff --git a/deploymentadmin/itest/src/test/resources/test-config1.xml b/deploymentadmin/itest/src/test/resources/test-config1.xml new file mode 100644 index 00000000000..27379b46051 --- /dev/null +++ b/deploymentadmin/itest/src/test/resources/test-config1.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/deploymentadmin/pom.xml b/deploymentadmin/pom.xml new file mode 100644 index 00000000000..76522157ccd --- /dev/null +++ b/deploymentadmin/pom.xml @@ -0,0 +1,38 @@ + + + + 4.0.0 + + org.apache.felix + felix-parent + 3 + ../pom/pom.xml + + pom + Apache Felix Deployment Admin Subproject + deploymentadmin-reactor + 1 + + deploymentadmin + autoconf + testbundles + itest + + diff --git a/deploymentadmin/testbundles/bundle1/bnd.bnd b/deploymentadmin/testbundles/bundle1/bnd.bnd new file mode 100644 index 00000000000..5af0bbad4a4 --- /dev/null +++ b/deploymentadmin/testbundles/bundle1/bnd.bnd @@ -0,0 +1,21 @@ +# 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. +Bundle-Version: 1.0.0 +Bundle-SymbolicName: testbundles.bundle1 +Bundle-Activator: org.apache.felix.deploymentadmin.test.bundle1.impl.Activator +Private-Package: org.apache.felix.deploymentadmin.test.bundle1.impl +Export-Package: org.apache.felix.deploymentadmin.test.bundle1 diff --git a/deploymentadmin/testbundles/bundle1/pom.xml b/deploymentadmin/testbundles/bundle1/pom.xml new file mode 100644 index 00000000000..9e4d6e32065 --- /dev/null +++ b/deploymentadmin/testbundles/bundle1/pom.xml @@ -0,0 +1,46 @@ + + + + 4.0.0 + + org.apache.felix + org.apache.felix.deploymentadmin.testbundles + 1 + ../pom.xml + + Apache Felix DeploymentAdmin Test Bundle 1 + 1.0.0 + org.apache.felix.deploymentadmin.test.bundle1 + bundle + + + org.osgi + org.osgi.core + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.4 + true + + + <_include>bnd.bnd + + + + + + diff --git a/deploymentadmin/testbundles/bundle1/src/main/java/org/apache/felix/deploymentadmin/test/bundle1/TestService.java b/deploymentadmin/testbundles/bundle1/src/main/java/org/apache/felix/deploymentadmin/test/bundle1/TestService.java new file mode 100644 index 00000000000..2effe5a8ff0 --- /dev/null +++ b/deploymentadmin/testbundles/bundle1/src/main/java/org/apache/felix/deploymentadmin/test/bundle1/TestService.java @@ -0,0 +1,31 @@ +/* + * 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.test.bundle1; + +/** + * Provides a test service. + */ +public interface TestService { + /** + * Does something. + * + * @return a string result, never null. + */ + String doIt(); +} diff --git a/deploymentadmin/testbundles/bundle1/src/main/java/org/apache/felix/deploymentadmin/test/bundle1/impl/Activator.java b/deploymentadmin/testbundles/bundle1/src/main/java/org/apache/felix/deploymentadmin/test/bundle1/impl/Activator.java new file mode 100644 index 00000000000..a1758234262 --- /dev/null +++ b/deploymentadmin/testbundles/bundle1/src/main/java/org/apache/felix/deploymentadmin/test/bundle1/impl/Activator.java @@ -0,0 +1,34 @@ +/* + * 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.test.bundle1.impl; + +import org.apache.felix.deploymentadmin.test.bundle1.TestService; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +public class Activator implements BundleActivator { + + public void start(BundleContext context) throws Exception { + context.registerService(TestService.class.getName(), new TestServiceImpl(), null); + } + + public void stop(BundleContext context) throws Exception { + // No-op + } +} diff --git a/deploymentadmin/testbundles/bundle1/src/main/java/org/apache/felix/deploymentadmin/test/bundle1/impl/TestServiceImpl.java b/deploymentadmin/testbundles/bundle1/src/main/java/org/apache/felix/deploymentadmin/test/bundle1/impl/TestServiceImpl.java new file mode 100644 index 00000000000..581d7dc406c --- /dev/null +++ b/deploymentadmin/testbundles/bundle1/src/main/java/org/apache/felix/deploymentadmin/test/bundle1/impl/TestServiceImpl.java @@ -0,0 +1,31 @@ +/* + * 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.test.bundle1.impl; + +import org.apache.felix.deploymentadmin.test.bundle1.TestService; + +/** + * Provides a default implementation for {@link TestService}. + */ +public class TestServiceImpl implements TestService { + + public String doIt() { + return "Hello World!"; + } +} diff --git a/deploymentadmin/testbundles/bundle2/bnd.bnd b/deploymentadmin/testbundles/bundle2/bnd.bnd new file mode 100644 index 00000000000..e9f9739315b --- /dev/null +++ b/deploymentadmin/testbundles/bundle2/bnd.bnd @@ -0,0 +1,21 @@ +# 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. +Bundle-Version: 1.0.0 +Bundle-SymbolicName: testbundles.bundle2 +Bundle-Activator: org.apache.felix.deploymentadmin.test.bundle2.impl.Activator +Private-Package: org.apache.felix.deploymentadmin.test.bundle2.impl +Import-Package: org.apache.felix.deploymentadmin.test.bundle1, org.osgi.framework, org.osgi.util.tracker, * diff --git a/deploymentadmin/testbundles/bundle2/pom.xml b/deploymentadmin/testbundles/bundle2/pom.xml new file mode 100644 index 00000000000..2ef211b49aa --- /dev/null +++ b/deploymentadmin/testbundles/bundle2/pom.xml @@ -0,0 +1,55 @@ + + + + 4.0.0 + + org.apache.felix + org.apache.felix.deploymentadmin.testbundles + 1 + ../pom.xml + + Apache Felix DeploymentAdmin Test Bundle 2 + 1.0.0 + org.apache.felix.deploymentadmin.test.bundle2 + bundle + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + ${pom.groupId} + org.apache.felix.deploymentadmin.test.bundle1 + 1.0.0 + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.4 + true + + + <_include>bnd.bnd + + + + + + diff --git a/deploymentadmin/testbundles/bundle2/src/main/java/org/apache/felix/deploymentadmin/test/bundle2/impl/Activator.java b/deploymentadmin/testbundles/bundle2/src/main/java/org/apache/felix/deploymentadmin/test/bundle2/impl/Activator.java new file mode 100644 index 00000000000..8c8de7a05a4 --- /dev/null +++ b/deploymentadmin/testbundles/bundle2/src/main/java/org/apache/felix/deploymentadmin/test/bundle2/impl/Activator.java @@ -0,0 +1,56 @@ +/* + * 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.test.bundle2.impl; + +import org.apache.felix.deploymentadmin.test.bundle1.TestService; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +public class Activator implements BundleActivator, ServiceTrackerCustomizer { + + ServiceTracker m_tracker; + BundleContext m_context; + + public void start(BundleContext context) throws Exception { + m_context = context; + + m_tracker = new ServiceTracker(context, TestService.class.getName(), this); + m_tracker.open(); + } + + public void stop(BundleContext context) throws Exception { + // Nop + m_tracker.close(); + } + + public Object addingService(ServiceReference reference) { + return null; + } + + public void modifiedService(ServiceReference reference, Object service) { + // Nop + } + + public void removedService(ServiceReference reference, Object service) { + // Nop + } +} diff --git a/deploymentadmin/testbundles/bundle3/bnd.bnd b/deploymentadmin/testbundles/bundle3/bnd.bnd new file mode 100644 index 00000000000..586a6e87dfd --- /dev/null +++ b/deploymentadmin/testbundles/bundle3/bnd.bnd @@ -0,0 +1,20 @@ +# 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. +Bundle-Version: 1.0.0 +Bundle-SymbolicName: testbundles.bundle3 +Bundle-Activator: org.apache.felix.deploymentadmin.test.bundle3.impl.Activator +Private-Package: org.apache.felix.deploymentadmin.test.bundle3.impl diff --git a/deploymentadmin/testbundles/bundle3/pom.xml b/deploymentadmin/testbundles/bundle3/pom.xml new file mode 100644 index 00000000000..6e0e675b616 --- /dev/null +++ b/deploymentadmin/testbundles/bundle3/pom.xml @@ -0,0 +1,46 @@ + + + + 4.0.0 + + org.apache.felix + org.apache.felix.deploymentadmin.testbundles + 1 + ../pom.xml + + Apache Felix DeploymentAdmin Test Bundle 3 + 1.0.0 + org.apache.felix.deploymentadmin.test.bundle3 + bundle + + + org.osgi + org.osgi.core + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.4 + true + + + <_include>bnd.bnd + + + + + + diff --git a/deploymentadmin/testbundles/bundle3/src/main/java/org/apache/felix/deploymentadmin/test/bundle3/impl/Activator.java b/deploymentadmin/testbundles/bundle3/src/main/java/org/apache/felix/deploymentadmin/test/bundle3/impl/Activator.java new file mode 100644 index 00000000000..cbc3e803ea9 --- /dev/null +++ b/deploymentadmin/testbundles/bundle3/src/main/java/org/apache/felix/deploymentadmin/test/bundle3/impl/Activator.java @@ -0,0 +1,48 @@ +/* + * 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.test.bundle3.impl; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +/** + * Throws an exception upon start or stop, depending on a system property. + */ +public class Activator implements BundleActivator { + + public void start(BundleContext context) throws Exception { + checkMethodRuntimeFailure("start"); + } + + public void stop(BundleContext context) throws Exception { + checkMethodRuntimeFailure("stop"); + } + + private void checkMethodRuntimeFailure(String methodName) { + if (shouldFail(methodName)) { + throw new RuntimeException(methodName + " fails forcedly!"); + } + } + + private boolean shouldFail(String methodName) { + String value = System.getProperty("bundle3", ""); + return methodName.equals(value); + } + +} diff --git a/deploymentadmin/testbundles/bundleapi1/bnd.bnd b/deploymentadmin/testbundles/bundleapi1/bnd.bnd new file mode 100644 index 00000000000..41751f3843f --- /dev/null +++ b/deploymentadmin/testbundles/bundleapi1/bnd.bnd @@ -0,0 +1,19 @@ +# 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. +Bundle-Version: 1.0.0 +Bundle-SymbolicName: testbundles.bundleapi +Export-Package: org.apache.felix.deploymentadmin.test.bundleapi;version="1.0.0" diff --git a/deploymentadmin/testbundles/bundleapi1/pom.xml b/deploymentadmin/testbundles/bundleapi1/pom.xml new file mode 100644 index 00000000000..b6961fdb501 --- /dev/null +++ b/deploymentadmin/testbundles/bundleapi1/pom.xml @@ -0,0 +1,46 @@ + + + + 4.0.0 + + org.apache.felix + org.apache.felix.deploymentadmin.testbundles + 1 + ../pom.xml + + Apache Felix DeploymentAdmin API Bundle 1 + 1.0.0 + org.apache.felix.deploymentadmin.test.bundleapi1 + bundle + + + org.osgi + org.osgi.core + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.4 + true + + + <_include>bnd.bnd + + + + + + diff --git a/deploymentadmin/testbundles/bundleapi1/src/main/java/org/apache/felix/deploymentadmin/test/bundleapi/MyService.java b/deploymentadmin/testbundles/bundleapi1/src/main/java/org/apache/felix/deploymentadmin/test/bundleapi/MyService.java new file mode 100644 index 00000000000..e33e8211567 --- /dev/null +++ b/deploymentadmin/testbundles/bundleapi1/src/main/java/org/apache/felix/deploymentadmin/test/bundleapi/MyService.java @@ -0,0 +1,24 @@ +/* + * 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.test.bundleapi; + +public interface MyService { + public void a(); + public void b(); +} \ No newline at end of file diff --git a/deploymentadmin/testbundles/bundleapi2/bnd.bnd b/deploymentadmin/testbundles/bundleapi2/bnd.bnd new file mode 100644 index 00000000000..282125f01ce --- /dev/null +++ b/deploymentadmin/testbundles/bundleapi2/bnd.bnd @@ -0,0 +1,19 @@ +# 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. +Bundle-Version: 2.0.0 +Bundle-SymbolicName: testbundles.bundleapi +Export-Package: org.apache.felix.deploymentadmin.test.bundleapi;version="2.0.0" diff --git a/deploymentadmin/testbundles/bundleapi2/pom.xml b/deploymentadmin/testbundles/bundleapi2/pom.xml new file mode 100644 index 00000000000..38cbf79f259 --- /dev/null +++ b/deploymentadmin/testbundles/bundleapi2/pom.xml @@ -0,0 +1,46 @@ + + + + 4.0.0 + + org.apache.felix + org.apache.felix.deploymentadmin.testbundles + 1 + ../pom.xml + + Apache Felix DeploymentAdmin API Bundle 2 + 2.0.0 + org.apache.felix.deploymentadmin.test.bundleapi2 + bundle + + + org.osgi + org.osgi.core + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.4 + true + + + <_include>bnd.bnd + + + + + + diff --git a/deploymentadmin/testbundles/bundleapi2/src/main/java/org/apache/felix/deploymentadmin/test/bundleapi/MyService.java b/deploymentadmin/testbundles/bundleapi2/src/main/java/org/apache/felix/deploymentadmin/test/bundleapi/MyService.java new file mode 100644 index 00000000000..93314228a10 --- /dev/null +++ b/deploymentadmin/testbundles/bundleapi2/src/main/java/org/apache/felix/deploymentadmin/test/bundleapi/MyService.java @@ -0,0 +1,24 @@ +/* + * 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.test.bundleapi; + +public interface MyService { + public void a(); + public void c(); +} \ No newline at end of file diff --git a/deploymentadmin/testbundles/bundleimpl1/bnd.bnd b/deploymentadmin/testbundles/bundleimpl1/bnd.bnd new file mode 100644 index 00000000000..675c4d33697 --- /dev/null +++ b/deploymentadmin/testbundles/bundleimpl1/bnd.bnd @@ -0,0 +1,20 @@ +# 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. +Bundle-Version: 1.0.0 +Bundle-SymbolicName: testbundles.bundleimpl +Import-Package: org.apache.felix.deploymentadmin.test.bundleapi;version="[1.0.0,2.0.0)" +Private-Package: org.apache.felix.deploymentadmin.test.bundleimpl diff --git a/deploymentadmin/testbundles/bundleimpl1/pom.xml b/deploymentadmin/testbundles/bundleimpl1/pom.xml new file mode 100644 index 00000000000..c79e1551f54 --- /dev/null +++ b/deploymentadmin/testbundles/bundleimpl1/pom.xml @@ -0,0 +1,51 @@ + + + + 4.0.0 + + org.apache.felix + org.apache.felix.deploymentadmin.testbundles + 1 + ../pom.xml + + Apache Felix DeploymentAdmin Impl Bundle 1 + 1.0.0 + org.apache.felix.deploymentadmin.test.bundleimpl1 + bundle + + + org.osgi + org.osgi.core + + + org.apache.felix + org.apache.felix.deploymentadmin.test.bundleapi1 + 1.0.0 + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.4 + true + + + <_include>bnd.bnd + + + + + + diff --git a/deploymentadmin/testbundles/bundleimpl1/src/main/java/org/apache/felix/deploymentadmin/test/bundleimpl/MyServiceImpl.java b/deploymentadmin/testbundles/bundleimpl1/src/main/java/org/apache/felix/deploymentadmin/test/bundleimpl/MyServiceImpl.java new file mode 100644 index 00000000000..979e53a24f7 --- /dev/null +++ b/deploymentadmin/testbundles/bundleimpl1/src/main/java/org/apache/felix/deploymentadmin/test/bundleimpl/MyServiceImpl.java @@ -0,0 +1,26 @@ +/* + * 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.test.bundleimpl; + +import org.apache.felix.deploymentadmin.test.bundleapi.MyService; + +public class MyServiceImpl implements MyService { + public void a() {}; + public void b() {}; +} \ No newline at end of file diff --git a/deploymentadmin/testbundles/bundleimpl2/bnd.bnd b/deploymentadmin/testbundles/bundleimpl2/bnd.bnd new file mode 100644 index 00000000000..7950f652cd3 --- /dev/null +++ b/deploymentadmin/testbundles/bundleimpl2/bnd.bnd @@ -0,0 +1,20 @@ +# 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. +Bundle-Version: 2.0.0 +Bundle-SymbolicName: testbundles.bundleimpl +Import-Package: org.apache.felix.deploymentadmin.test.bundleapi;version="[2.0.0,3.0.0)" +Private-Package: org.apache.felix.deploymentadmin.test.bundleimpl diff --git a/deploymentadmin/testbundles/bundleimpl2/pom.xml b/deploymentadmin/testbundles/bundleimpl2/pom.xml new file mode 100644 index 00000000000..bdbd70fdb69 --- /dev/null +++ b/deploymentadmin/testbundles/bundleimpl2/pom.xml @@ -0,0 +1,51 @@ + + + + 4.0.0 + + org.apache.felix + org.apache.felix.deploymentadmin.testbundles + 1 + ../pom.xml + + Apache Felix DeploymentAdmin Impl Bundle 2 + 2.0.0 + org.apache.felix.deploymentadmin.test.bundleimpl2 + bundle + + + org.osgi + org.osgi.core + + + org.apache.felix + org.apache.felix.deploymentadmin.test.bundleapi2 + 2.0.0 + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.4 + true + + + <_include>bnd.bnd + + + + + + diff --git a/deploymentadmin/testbundles/bundleimpl2/src/main/java/org/apache/felix/deploymentadmin/test/bundleimpl/MyServiceImpl.java b/deploymentadmin/testbundles/bundleimpl2/src/main/java/org/apache/felix/deploymentadmin/test/bundleimpl/MyServiceImpl.java new file mode 100644 index 00000000000..a72926ce8f4 --- /dev/null +++ b/deploymentadmin/testbundles/bundleimpl2/src/main/java/org/apache/felix/deploymentadmin/test/bundleimpl/MyServiceImpl.java @@ -0,0 +1,26 @@ +/* + * 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.test.bundleimpl; + +import org.apache.felix.deploymentadmin.test.bundleapi.MyService; + +public class MyServiceImpl implements MyService { + public void a() {}; + public void c() {}; +} \ No newline at end of file diff --git a/deploymentadmin/testbundles/fragment1/bnd.bnd b/deploymentadmin/testbundles/fragment1/bnd.bnd new file mode 100644 index 00000000000..f2791f687b5 --- /dev/null +++ b/deploymentadmin/testbundles/fragment1/bnd.bnd @@ -0,0 +1,20 @@ +# 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. +Bundle-Version: 1.0.0 +Bundle-SymbolicName: testbundles.fragment1 +Fragment-Host: testbundles.bundle1 +Private-Package: org.apache.felix.deploymentadmin.test.fragment1.impl diff --git a/deploymentadmin/testbundles/fragment1/pom.xml b/deploymentadmin/testbundles/fragment1/pom.xml new file mode 100644 index 00000000000..2b0312fc2c7 --- /dev/null +++ b/deploymentadmin/testbundles/fragment1/pom.xml @@ -0,0 +1,46 @@ + + + + 4.0.0 + + org.apache.felix + org.apache.felix.deploymentadmin.testbundles + 1 + ../pom.xml + + Apache Felix DeploymentAdmin Test Fragment 1 + 1.0.0 + org.apache.felix.deploymentadmin.test.fragment1 + bundle + + + org.osgi + org.osgi.core + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.4 + true + + + <_include>bnd.bnd + + + + + + diff --git a/deploymentadmin/testbundles/fragment1/src/main/java/org/apache/felix/deploymentadmin/test/fragment1/impl/Pojo.java b/deploymentadmin/testbundles/fragment1/src/main/java/org/apache/felix/deploymentadmin/test/fragment1/impl/Pojo.java new file mode 100644 index 00000000000..1546300106a --- /dev/null +++ b/deploymentadmin/testbundles/fragment1/src/main/java/org/apache/felix/deploymentadmin/test/fragment1/impl/Pojo.java @@ -0,0 +1,27 @@ +/* + * 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.test.fragment1.impl; + + +/** + * Empty pojo, for testing FELIX-4167. + */ +public class Pojo { + // Nop +} diff --git a/deploymentadmin/testbundles/pom.xml b/deploymentadmin/testbundles/pom.xml new file mode 100644 index 00000000000..218ee2df3d9 --- /dev/null +++ b/deploymentadmin/testbundles/pom.xml @@ -0,0 +1,54 @@ + + + + 4.0.0 + + org.apache.felix + felix-parent + 3 + ../../pom/pom.xml + + + 4.2.0 + + Apache Felix DeploymentAdmin Test Bundles + org.apache.felix.deploymentadmin.testbundles + 1 + pom + + + + org.osgi + org.osgi.core + ${osgi.version} + + + org.osgi + org.osgi.compendium + ${osgi.version} + + + + + bundle1 + bundle2 + bundle3 + fragment1 + rp1 + rp2 + bundleapi1 + bundleapi2 + bundleimpl1 + bundleimpl2 + + diff --git a/deploymentadmin/testbundles/rp1/bnd.bnd b/deploymentadmin/testbundles/rp1/bnd.bnd new file mode 100644 index 00000000000..875d428f6e2 --- /dev/null +++ b/deploymentadmin/testbundles/rp1/bnd.bnd @@ -0,0 +1,22 @@ +# 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. +Bundle-Version: 1.0.0 +Bundle-SymbolicName: testbundles.rp1 +Bundle-Activator: org.apache.felix.deploymentadmin.test.rp1.impl.Activator +Private-Package: org.apache.felix.deploymentadmin.test.rp1.impl +DeploymentPackage-Customizer: true +Deployment-ProvidesResourceProcessor: org.apache.felix.deploymentadmin.test.rp1 diff --git a/deploymentadmin/testbundles/rp1/pom.xml b/deploymentadmin/testbundles/rp1/pom.xml new file mode 100644 index 00000000000..6517dccd2db --- /dev/null +++ b/deploymentadmin/testbundles/rp1/pom.xml @@ -0,0 +1,50 @@ + + + + 4.0.0 + + org.apache.felix + org.apache.felix.deploymentadmin.testbundles + 1 + ../pom.xml + + Apache Felix DeploymentAdmin Test RP 1 + 1.0.0 + org.apache.felix.deploymentadmin.test.rp1 + bundle + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.4 + true + + + <_include>bnd.bnd + + + + + + diff --git a/deploymentadmin/testbundles/rp1/src/main/java/org/apache/felix/deploymentadmin/test/rp1/impl/Activator.java b/deploymentadmin/testbundles/rp1/src/main/java/org/apache/felix/deploymentadmin/test/rp1/impl/Activator.java new file mode 100644 index 00000000000..009b7094a2c --- /dev/null +++ b/deploymentadmin/testbundles/rp1/src/main/java/org/apache/felix/deploymentadmin/test/rp1/impl/Activator.java @@ -0,0 +1,97 @@ +/* + * 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.test.rp1.impl; + +import java.io.InputStream; +import java.util.Properties; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.service.deploymentadmin.spi.DeploymentSession; +import org.osgi.service.deploymentadmin.spi.ResourceProcessor; +import org.osgi.service.deploymentadmin.spi.ResourceProcessorException; + +/** + * Provides a bundle activator for registering the resource processor. + */ +public class Activator implements BundleActivator, ResourceProcessor { + + public void start(BundleContext context) throws Exception { + checkMethodRuntimeFailure("start"); + + Properties props = new Properties(); + props.put(Constants.SERVICE_PID, "org.apache.felix.deploymentadmin.test.rp1"); + + context.registerService(ResourceProcessor.class.getName(), this, props); + } + + public void stop(BundleContext context) throws Exception { + checkMethodRuntimeFailure("stop"); + } + + public void begin(DeploymentSession session) { + checkMethodRuntimeFailure("begin"); + } + + public void process(String name, InputStream stream) throws ResourceProcessorException { + checkMethodFailure(ResourceProcessorException.CODE_RESOURCE_SHARING_VIOLATION, "process"); + } + + public void dropped(String resource) throws ResourceProcessorException { + checkMethodFailure(ResourceProcessorException.CODE_OTHER_ERROR, "dropped"); + } + + public void dropAllResources() throws ResourceProcessorException { + checkMethodFailure(ResourceProcessorException.CODE_OTHER_ERROR, "dropAllResources"); + } + + public void prepare() throws ResourceProcessorException { + checkMethodFailure(ResourceProcessorException.CODE_PREPARE, "prepare"); + } + + public void commit() { + checkMethodRuntimeFailure("commit"); + } + + public void rollback() { + checkMethodRuntimeFailure("rollback"); + } + + public void cancel() { + checkMethodRuntimeFailure("cancel"); + } + + private void checkMethodFailure(int code, String methodName) throws ResourceProcessorException { + if (shouldFail(methodName)) { + throw new ResourceProcessorException(code, methodName + " fails forcedly!"); + } + } + + private void checkMethodRuntimeFailure(String methodName) { + if (shouldFail(methodName)) { + throw new RuntimeException(methodName + " fails forcedly!"); + } + } + + private boolean shouldFail(String methodName) { + String value = System.getProperty("rp1", ""); + return methodName.equals(value); + } +} diff --git a/deploymentadmin/testbundles/rp2/bnd.bnd b/deploymentadmin/testbundles/rp2/bnd.bnd new file mode 100644 index 00000000000..39ca0925251 --- /dev/null +++ b/deploymentadmin/testbundles/rp2/bnd.bnd @@ -0,0 +1,22 @@ +# 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. +Bundle-Version: 1.0.0 +Bundle-SymbolicName: testbundles.rp2 +Bundle-Activator: org.apache.felix.deploymentadmin.test.rp2.impl.Activator +Private-Package: org.apache.felix.deploymentadmin.test.rp2.impl +DeploymentPackage-Customizer: true +Deployment-ProvidesResourceProcessor: org.apache.felix.deploymentadmin.test.rp2 diff --git a/deploymentadmin/testbundles/rp2/pom.xml b/deploymentadmin/testbundles/rp2/pom.xml new file mode 100644 index 00000000000..3f07541e0b9 --- /dev/null +++ b/deploymentadmin/testbundles/rp2/pom.xml @@ -0,0 +1,53 @@ + + + + 4.0.0 + + org.apache.felix + org.apache.felix.deploymentadmin.testbundles + 1 + ../pom.xml + + + 4.2.0 + + Apache Felix DeploymentAdmin Test RP 2 + 1.0.0 + org.apache.felix.deploymentadmin.test.rp2 + bundle + + + org.osgi + org.osgi.core + + + org.osgi + org.osgi.compendium + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.4 + true + + + <_include>bnd.bnd + + + + + + diff --git a/deploymentadmin/testbundles/rp2/src/main/java/org/apache/felix/deploymentadmin/test/rp2/impl/Activator.java b/deploymentadmin/testbundles/rp2/src/main/java/org/apache/felix/deploymentadmin/test/rp2/impl/Activator.java new file mode 100644 index 00000000000..4ee24ba6cf6 --- /dev/null +++ b/deploymentadmin/testbundles/rp2/src/main/java/org/apache/felix/deploymentadmin/test/rp2/impl/Activator.java @@ -0,0 +1,100 @@ +/* + * 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.test.rp2.impl; + +import java.io.InputStream; +import java.util.Properties; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.service.deploymentadmin.spi.DeploymentSession; +import org.osgi.service.deploymentadmin.spi.ResourceProcessor; +import org.osgi.service.deploymentadmin.spi.ResourceProcessorException; + +/** + * Provides a bundle activator and resource processor implementation that can delay methods for a + * certain amount of time (defined by system property "rp2.delay", defaults to 2000 milliseconds). + */ +public class Activator implements BundleActivator, ResourceProcessor { + + public void start(BundleContext context) throws Exception { + Properties props = new Properties(); + props.put(Constants.SERVICE_PID, "org.apache.felix.deploymentadmin.bundle.rp2"); + + context.registerService(ResourceProcessor.class.getName(), this, props); + } + + public void stop(BundleContext context) throws Exception { + delayMethod("stop"); + } + + public void begin(DeploymentSession session) { + delayMethod("begin"); + } + + public void process(String name, InputStream stream) throws ResourceProcessorException { + delayMethod("process"); + } + + public void dropped(String resource) throws ResourceProcessorException { + delayMethod("dropped"); + } + + public void dropAllResources() throws ResourceProcessorException { + delayMethod("dropAllResources"); + } + + public void prepare() throws ResourceProcessorException { + delayMethod("prepare"); + } + + public void commit() { + delayMethod("commit"); + } + + public void rollback() { + delayMethod("rollback"); + } + + public void cancel() { + delayMethod("cancel"); + } + + private void delayMethod(String methodName) { + try { + if (shouldDelayMethod(methodName)) { + String value = System.getProperty("rp2.delay", "2000"); + + Thread.sleep(Long.parseLong(value)); + } + } + catch (NumberFormatException exception) { + exception.printStackTrace(); + } + catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + } + } + + private boolean shouldDelayMethod(String methodName) { + String value = System.getProperty("rp2", ""); + return methodName.equals(value); + } +} diff --git a/deviceaccess/pom.xml b/deviceaccess/pom.xml new file mode 100644 index 00000000000..be67f46a232 --- /dev/null +++ b/deviceaccess/pom.xml @@ -0,0 +1,95 @@ + + + + + 4.0.0 + + + org.apache.felix + felix-parent + 2 + ../pom/pom.xml + + + org.apache.felix.devicemanager + 0.9.0-SNAPSHOT + bundle + + Apache Felix Device Manager + + + Implementation of the OSGi Device Access Specification 1.1 + + + + + org.osgi + org.osgi.core + 4.2.0 + provided + + + org.osgi + org.osgi.compendium + 4.2.0 + provided + + + ${pom.groupId} + org.apache.felix.framework + 1.6.0 + test + + + ${pom.groupId} + org.apache.felix.dependencymanager + 3.0.0 + provided + + + + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + org.apache.felix.das + Apache Felix Device Manager + A bundle that provides a run-time device manager. + Apache Software Foundation + org.apache.felix.das.Activator + org.apache.felix.das, org.apache.felix.das.util + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + + diff --git a/deviceaccess/src/main/java/org/apache/felix/das/Activator.java b/deviceaccess/src/main/java/org/apache/felix/das/Activator.java new file mode 100644 index 00000000000..c7f918ab6a8 --- /dev/null +++ b/deviceaccess/src/main/java/org/apache/felix/das/Activator.java @@ -0,0 +1,115 @@ +/* + * 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.das; + + +import org.apache.felix.das.util.Util; +import org.apache.felix.dm.Component; +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.service.device.Device; +import org.osgi.service.device.Driver; +import org.osgi.service.device.DriverLocator; +import org.osgi.service.device.DriverSelector; +import org.osgi.service.log.LogService; + + +/** + * TODO: add javadoc + * + * @author Felix Project Team + */ +public class Activator extends DependencyActivatorBase +{ + + /** the bundle context */ + private volatile BundleContext m_context; + + /** the dependency manager */ + private volatile DependencyManager m_manager; + + /** the device manager */ + private DeviceManager m_deviceManager; + + + /** + * Here, we create and start the device manager, but we do not register it + * within the framework, since that is not specified by the specification. + * + * @see org.apache.felix.dependencymanager.DependencyActivatorBase#init(org.osgi.framework.BundleContext, + * org.apache.felix.dependencymanager.DependencyManager) + */ + public void init( BundleContext context, DependencyManager manager ) throws Exception + { + + m_context = context; + m_manager = manager; + + m_deviceManager = new DeviceManager( m_context ); + + // the real device manager + startDeviceManager(); + + } + + + private void startDeviceManager() { + + final String driverFilter = Util.createFilterString( "(%s=%s)", new String[] + { org.osgi.service.device.Constants.DRIVER_ID, "*" } ); + + final String deviceFilter = Util.createFilterString( "(|(%s=%s)(&(%s=%s)(%s=%s)))", new String[] + { + Constants.OBJECTCLASS, Device.class.getName(), + Constants.OBJECTCLASS, "*", + org.osgi.service.device.Constants.DEVICE_CATEGORY, "*" + } ); + + Component svc = createComponent(); + + svc.setImplementation( m_deviceManager ); + + svc.add( createServiceDependency().setService( LogService.class ).setRequired( false ) ); + + svc.add( createServiceDependency().setService( DriverSelector.class ).setRequired( false ).setAutoConfig( false ) + .setCallbacks( "selectorAdded", "selectorRemoved" ) ); + + svc.add( createServiceDependency().setService( DriverLocator.class ).setRequired( false ).setAutoConfig( false ) + .setCallbacks( "locatorAdded", "locatorRemoved" ) ); + + svc.add( createServiceDependency().setService( Driver.class, driverFilter ).setRequired( false ) + .setCallbacks( "driverAdded", "driverModified", "driverRemoved" ) ); + + svc.add( createServiceDependency().setService( deviceFilter ).setRequired( false ) + .setCallbacks( "deviceAdded", "deviceModified", "deviceRemoved" ) ); + + m_manager.add( svc ); + + } + + + public void destroy( BundleContext context, DependencyManager manager ) throws Exception + { + // TODO Auto-generated method stub + + } + +} diff --git a/deviceaccess/src/main/java/org/apache/felix/das/DeviceManager.java b/deviceaccess/src/main/java/org/apache/felix/das/DeviceManager.java new file mode 100644 index 00000000000..ed7159af548 --- /dev/null +++ b/deviceaccess/src/main/java/org/apache/felix/das/DeviceManager.java @@ -0,0 +1,728 @@ +/* + * 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.das; + + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.apache.felix.das.util.DriverLoader; +import org.apache.felix.das.util.DriverMatcher; +import org.apache.felix.das.util.Util; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.device.Constants; +import org.osgi.service.device.Device; +import org.osgi.service.device.Driver; +import org.osgi.service.device.DriverLocator; +import org.osgi.service.device.DriverSelector; +import org.osgi.service.device.Match; +import org.osgi.service.log.LogService; + + +/** + * This class represents the Apache Felix implementation of the device access specification. + * It is based on version 1.1 of the spec. + * + * + * @author Felix Project Team + */ +public class DeviceManager implements Log +{ + + private final long DEFAULT_TIMEOUT_SEC = 1; + + // the logger + private volatile LogService m_log; + + // the bundle context + private final BundleContext m_context; + + // the driver selector + private volatile DriverSelector m_selector; + + // the driver locators + private List m_locators; + + // the devices + private Map m_devices; + + // the drivers + private Map m_drivers; + + // performs all the background actions + private ExecutorService m_worker; + + // used to add delayed actions + private ScheduledExecutorService m_delayed; + + //the devices filter + private Filter m_deviceImplFilter; + + //the drivers filter + private Filter m_driverImplFilter; + + + /** + * Public constructor. Used by the Activator in this Bundle + * to instantiate one instance. + * + * @param context the BundleContext + */ + public DeviceManager( BundleContext context ) + { + m_context = context; + } + + + public void debug( String message ) + { + m_log.log( LogService.LOG_DEBUG, message ); + } + + + public void info( String message ) + { + m_log.log( LogService.LOG_INFO, message ); + } + + + public void warning( String message ) + { + m_log.log( LogService.LOG_WARNING, message ); + } + + + public void error( String message, Throwable e ) + { + System.err.println( message ); + if ( e != null ) + { + e.printStackTrace(); + } + m_log.log( LogService.LOG_ERROR, message, e ); + } + + + // dependency manager methods + @SuppressWarnings("unused") + private void init() throws InvalidSyntaxException + { + m_locators = Collections.synchronizedList( new ArrayList() ); + m_worker = Executors.newSingleThreadExecutor( new NamedThreadFactory( "Apache Felix Device Manager" ) ); + m_delayed = Executors.newScheduledThreadPool( 1, new NamedThreadFactory( + "Apache Felix Device Manager - delayed" ) ); + m_deviceImplFilter = Util.createFilter( "(%s=%s)", new Object[] + { org.osgi.framework.Constants.OBJECTCLASS, Device.class.getName() } ); + m_driverImplFilter = Util.createFilter( "(%s=%s)", new Object[] + { org.osgi.framework.Constants.OBJECTCLASS, Driver.class.getName() } ); + } + + + @SuppressWarnings("unused") + private void start() + { + m_drivers = new HashMap(); + m_devices = new HashMap(); + submit( new WaitForStartFramework() ); + } + + + public void stop() + { + // nothing to do ? + } + + + public void destroy() + { + m_worker.shutdownNow(); + m_delayed.shutdownNow(); + } + + + // callback methods + + public void selectorAdded( DriverSelector selector ) + { + m_selector = selector; + debug( "driver selector appeared" ); + } + + + public void selectorRemoved( DriverSelector selector ) + { + m_selector = null; + debug( "driver selector lost" ); + } + + public void locatorAdded( DriverLocator locator ) + { + m_locators.add( locator ); + debug( "driver locator appeared" ); + } + + + public void locatorRemoved( DriverLocator locator ) + { + m_locators.remove( locator ); + debug( "driver locator lost" ); + } + + + public void driverAdded( ServiceReference ref, Object obj ) + { + final Driver driver = Driver.class.cast( obj ); + m_drivers.put( ref, new DriverAttributes( ref, driver ) ); + + debug( "driver appeared: " + Util.showDriver( ref ) ); + + //immediately check for idle devices + submit( new CheckForIdleDevices() ); + } + + public void driverModified( ServiceReference ref, Object obj ) + { + final Driver driver = Driver.class.cast( obj ); + + debug( "driver modified: " + Util.showDriver( ref ) ); + m_drivers.remove( ref ); + m_drivers.put( ref , new DriverAttributes( ref, driver ) ); + + // check if devices have become idle + // after some time + schedule( new CheckForIdleDevices() ); + } + + public void driverRemoved( ServiceReference ref ) + { + debug( "driver lost: " + Util.showDriver( ref ) ); + m_drivers.remove( ref ); + + // check if devices have become idle + // after some time + schedule( new CheckForIdleDevices() ); + + } + + + public void deviceAdded( ServiceReference ref, Object device ) + { + m_devices.put( ref, device ); + debug( "device appeared: " + Util.showDevice( ref ) ); + submit( new DriverAttachAlgorithm( ref, device ) ); + } + + + public void deviceModified( ServiceReference ref, Object device ) + { + debug( "device modified: " + Util.showDevice( ref ) ); + // nothing further to do ? + // DeviceAttributes da = m_devices.get(ref); + // submit(new DriverAttachAlgorithm(da)); + } + + + public void deviceRemoved( ServiceReference ref ) + { + debug( "device removed: " + Util.showDevice( ref ) ); + m_devices.remove( ref ); + // nothing further to do ? + // the services that use this + // device should track it. + } + + + /** + * perform this task as soon as possible. + * + * @param task + * the task + */ + private void submit( Callable task ) + { + m_worker.submit( new LoggedCall( task ) ); + } + + + /** + * perform this task after the default delay. + * + * @param task + * the task + */ + private void schedule( Callable task ) + { + m_delayed.schedule( new DelayedCall( task ), DEFAULT_TIMEOUT_SEC, TimeUnit.SECONDS ); + } + + // worker callables + + /** + * Callable used to start the DeviceManager. It either waits (blocking the + * worker thread) for the framework to start, or if it has already started, + * returns immediately, freeing up the worker thread. + * + * @author Felix Project Team + */ + private class WaitForStartFramework implements Callable, FrameworkListener + { + + private final CountDownLatch m_latch = new CountDownLatch( 1 ); + + + public Object call() throws Exception + { + boolean addedAsListener = false; + if ( m_context.getBundle( 0 ).getState() == Bundle.ACTIVE ) + { + m_latch.countDown(); + debug( "Starting Device Manager immediately" ); + } + else + { + m_context.addFrameworkListener( this ); + addedAsListener = true; + debug( "Waiting for framework to start" ); + } + + m_latch.await(); + for ( Map.Entry entry : m_devices.entrySet() ) + { + submit( new DriverAttachAlgorithm( entry.getKey(), entry.getValue() ) ); + } + // cleanup + if ( addedAsListener ) + { + m_context.removeFrameworkListener( this ); + } + return null; + } + + + // FrameworkListener method + public void frameworkEvent( FrameworkEvent event ) + { + switch ( event.getType() ) + { + case FrameworkEvent.STARTED: + debug( "Framework has started" ); + m_latch.countDown(); + break; + } + } + + + @Override + public String toString() + { + return getClass().getSimpleName(); + } + } + + private class LoggedCall implements Callable + { + + private final Callable m_call; + + + public LoggedCall( Callable call ) + { + m_call = call; + } + + + private String getName() + { + return m_call.getClass().getSimpleName(); + } + + + public Object call() throws Exception + { + + try + { + return m_call.call(); + } + catch ( Exception e ) + { + error( "call failed: " + getName(), e ); + throw e; + } + catch ( Throwable e ) + { + error( "call failed: " + getName(), e ); + throw new RuntimeException( e ); + } + } + + } + + private class DelayedCall implements Callable + { + + private final Callable m_call; + + + public DelayedCall( Callable call ) + { + m_call = call; + } + + + private String getName() + { + return m_call.getClass().getSimpleName(); + } + + + public Object call() throws Exception + { + info( "Delayed call: " + getName() ); + return m_worker.submit( m_call ); + } + } + + /** + * Checks for Idle devices, and attaches them + * + * @author Felix Project Team + */ + private class CheckForIdleDevices implements Callable + { + + public Object call() throws Exception + { + debug( "START - check for idle devices" ); + for ( ServiceReference ref : getIdleDevices() ) + { + info( "IDLE: " + ref.getBundle().getSymbolicName() ); + submit( new DriverAttachAlgorithm( ref, m_devices.get( ref ) ) ); + } + + submit( new IdleDriverUninstallAlgorithm() ); + debug( "STOP - check for idle devices" ); + return null; + } + + + /** + * get a list of all idle devices. + * + * @return + */ + private List getIdleDevices() + { + List list = new ArrayList(); + + for ( ServiceReference ref : m_devices.keySet() ) + { + info( "checking if idle: " + ref.getBundle().getSymbolicName() ); + + final Bundle[] usingBundles = ref.getUsingBundles(); + for ( Bundle bundle : usingBundles ) + { + if ( isDriverBundle( bundle ) ) + { + info( "used by driver: " + bundle.getSymbolicName() ); + debug( "not idle: " + ref.getBundle().getSymbolicName() ); + break; + } + + list.add( ref ); + + } + } + return list; + } + } + + + private boolean isDriverBundle( Bundle bundle ) + { + ServiceReference[] refs = bundle.getRegisteredServices(); + + if (refs == null) { + return false; + } + + for ( ServiceReference ref : refs ) + { + if ( m_driverImplFilter.match( ref ) ) + { + return true; + } + } + return false; + } + + /** + * + * Used to uninstall unused drivers + * + * @author Felix Project Team + */ + private class IdleDriverUninstallAlgorithm implements Callable + { + + public Object call() throws Exception + { + + info( "cleaning driver cache" ); + for ( DriverAttributes da : m_drivers.values() ) + { + // just call the tryUninstall; the da itself + // will know if it should really uninstall the driver. + try + { + da.tryUninstall(); + } + catch (Exception e) + { + debug(da.getDriverId() + " uninstall failed"); + } + } + + return null; + } + } + + private class DriverAttachAlgorithm implements Callable + { + + private final ServiceReference m_ref; + + private final Device m_device; + + private List m_included; + + private List m_excluded; + + private final DriverLoader m_driverLoader; + + private DriverAttributes m_finalDriver; + + + public DriverAttachAlgorithm( ServiceReference ref, Object obj ) + { + m_ref = ref; + if ( m_deviceImplFilter.match( ref ) ) + { + m_device = Device.class.cast( obj ); + } + else + { + m_device = null; + } + + m_driverLoader = new DriverLoader( DeviceManager.this, m_context ); + } + + + @SuppressWarnings("all") + private Dictionary createDictionary( ServiceReference ref ) + { + final Properties p = new Properties(); + + for ( String key : ref.getPropertyKeys() ) + { + p.put( key, ref.getProperty( key ) ); + } + return p; + } + + + @SuppressWarnings("all") + public Object call() throws Exception + { + info( "finding suitable driver for: " + Util.showDevice( m_ref ) ); + + final Dictionary dict = createDictionary( m_ref ); + + // first create a copy of all the drivers that are already there. + // during the process, drivers will be added, but also excluded. + m_included = new ArrayList( m_drivers.values() ); + m_excluded = new ArrayList(); + + // first find matching driver bundles + // if there are no driver locators + // we'll have to do with the drivers that were + // added 'manually' + Set driverIds = m_driverLoader.findDrivers( m_locators, dict ); + + // remove the driverIds that are already available + for ( DriverAttributes da : m_drivers.values() ) + { + driverIds.remove( da.getDriverId() ); + } + driverIds.removeAll( m_drivers.keySet() ); + try + { + debug("entering attach phase for " + Util.showDevice( m_ref ) ); + return driverAttachment( dict, driverIds.toArray( new String[0] ) ); + } + finally + { + // unload loaded drivers + // that were unnecessarily loaded + m_driverLoader.unload( m_finalDriver ); + } + } + + + @SuppressWarnings("all") + private Object driverAttachment( Dictionary dict, String[] driverIds ) throws Exception + { + m_finalDriver = null; + + // remove the excluded drivers + m_included.removeAll( m_excluded ); + + // now load the drivers + List driverRefs = m_driverLoader.loadDrivers( m_locators, driverIds ); + // these are the possible driver references that have been added + // add them to the list of included drivers + for ( ServiceReference serviceReference : driverRefs ) + { + DriverAttributes da = m_drivers.get( serviceReference ); + if ( da != null ) + { + m_included.add( da ); + } + } + + // now start matching all drivers + final DriverMatcher mi = new DriverMatcher( DeviceManager.this ); + + for ( DriverAttributes driver : m_included ) + { + try + { + int match = driver.match( m_ref ); + if ( match <= Device.MATCH_NONE ) + { + continue; + } + mi.add( match, driver ); + } + catch ( Throwable t ) + { + error( "match threw an exception", new Exception( t ) ); + } + } + + // get the best match + Match bestMatch = null; + + // local copy + final DriverSelector selector = m_selector; + + if ( selector != null ) + { + bestMatch = mi.selectBestMatch( m_ref, selector ); + if (bestMatch != null) { + debug(String.format("DriverSelector (%s) found best match: %s", selector.getClass().getName(), Util.showDriver(bestMatch.getDriver()))); + } + } + + if (bestMatch == null) + { + bestMatch = mi.getBestMatch(); + } + + if ( bestMatch == null ) + { + noDriverFound(); + // really return + return null; + } + + String driverId = String.class.cast( bestMatch.getDriver().getProperty( Constants.DRIVER_ID ) ); + + debug( "best match: " + driverId ); + m_finalDriver = m_drivers.get( bestMatch.getDriver() ); + + if ( m_finalDriver == null ) + { + error( "we found a driverId, but not the corresponding driver: " + driverId, null ); + noDriverFound(); + return null; + } + + // here we get serious... + try + { + debug( "attaching to: " + driverId ); + String newDriverId = m_finalDriver.attach( m_ref ); + if ( newDriverId == null ) + { + // successful attach + return null; + } + // its a referral + info( "attach led to a referral to: " + newDriverId ); + m_excluded.add( m_finalDriver ); + return driverAttachment( dict, new String[]{ newDriverId } ); + } + catch ( Throwable t ) + { + error( "attach failed due to an exception", t ); + } + m_excluded.add( m_finalDriver ); + return driverAttachment( dict, driverIds ); + } + + + private void noDriverFound() + { + debug( "no suitable driver found for: " + Util.showDevice( m_ref ) ); + if ( m_device != null ) + { + m_device.noDriverFound(); + } + } + + + @Override + public String toString() + { + return getClass().getSimpleName();// + ": " + + // Util.showDevice(m_ref); + } + + } +} diff --git a/deviceaccess/src/main/java/org/apache/felix/das/DriverAttributes.java b/deviceaccess/src/main/java/org/apache/felix/das/DriverAttributes.java new file mode 100644 index 00000000000..a60b5432523 --- /dev/null +++ b/deviceaccess/src/main/java/org/apache/felix/das/DriverAttributes.java @@ -0,0 +1,114 @@ +/* + * 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.das; + + +import org.apache.felix.das.util.DriverLoader; +import org.apache.felix.das.util.Util; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.device.Constants; +import org.osgi.service.device.Driver; + + +/** + * TODO: add javadoc + * + * @author Felix Project Team + */ +public class DriverAttributes +{ + + private final Bundle m_bundle; + + private final ServiceReference m_ref; + + private final Driver m_driver; + + private final boolean m_dynamic; + + public DriverAttributes( ServiceReference ref, Driver driver ) + { + m_ref = ref; + m_driver = driver; + m_bundle = ref.getBundle(); + m_dynamic = m_bundle.getLocation().startsWith( DriverLoader.DRIVER_LOCATION_PREFIX ); + } + + + public ServiceReference getReference() + { + return m_ref; + } + + + public String getDriverId() + { + return m_ref.getProperty( Constants.DRIVER_ID ).toString(); + } + + + public int match( ServiceReference ref ) throws Exception + { + return m_driver.match( ref ); + } + + + /** + * a driver bundle is idle if it isn't connected to a device bundle. + * + * @return + */ + private boolean isInUse() + { + ServiceReference[] used = m_bundle.getServicesInUse(); + if ( used == null || used.length == 0 ) + { + return false; + } + + for ( ServiceReference ref : used ) + { + if ( Util.isDevice( ref ) ) + { + return true; + } + } + return false; + } + + + public String attach( ServiceReference ref ) throws Exception + { + return m_driver.attach( ref ); + } + + + public void tryUninstall() throws BundleException + { + + // only install if _we_ loaded the driver + if ( !isInUse() && m_dynamic ) + { + m_bundle.uninstall(); + } + } + +} diff --git a/deviceaccess/src/main/java/org/apache/felix/das/Log.java b/deviceaccess/src/main/java/org/apache/felix/das/Log.java new file mode 100644 index 00000000000..698f4cf7725 --- /dev/null +++ b/deviceaccess/src/main/java/org/apache/felix/das/Log.java @@ -0,0 +1,41 @@ +/* + * 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.das; + + +/** + * TODO: add javadoc + * + * @author Felix Project Team + */ +public interface Log +{ + + public void debug( String message ); + + + public void info( String message ); + + + public void warning( String message ); + + + public void error( String message, Throwable e ); + +} \ No newline at end of file diff --git a/deviceaccess/src/main/java/org/apache/felix/das/NamedThreadFactory.java b/deviceaccess/src/main/java/org/apache/felix/das/NamedThreadFactory.java new file mode 100644 index 00000000000..c4ad6b3f1fe --- /dev/null +++ b/deviceaccess/src/main/java/org/apache/felix/das/NamedThreadFactory.java @@ -0,0 +1,47 @@ +/* + * 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.das; + + +import java.util.concurrent.ThreadFactory; + + +/** + * This class is able to create a named Thread. + * + * @author Felix Project Team + */ +public class NamedThreadFactory implements ThreadFactory +{ + + private final String m_name; + + + public NamedThreadFactory( String name ) + { + m_name = name; + } + + + public Thread newThread( Runnable r ) + { + return new Thread( r, m_name ); + } + +} diff --git a/deviceaccess/src/main/java/org/apache/felix/das/util/DeviceAnalyzer.java b/deviceaccess/src/main/java/org/apache/felix/das/util/DeviceAnalyzer.java new file mode 100644 index 00000000000..0c29f7445c7 --- /dev/null +++ b/deviceaccess/src/main/java/org/apache/felix/das/util/DeviceAnalyzer.java @@ -0,0 +1,99 @@ +/* + * 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.das.util; + + +import org.osgi.framework.BundleContext; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.device.Constants; +import org.osgi.service.device.Device; +import org.osgi.service.log.LogService; + + +/** + * TODO: add javadoc + * + * @author Felix Project Team + */ +public class DeviceAnalyzer +{ + + private LogService m_log; + + private Filter deviceImpl; + + private Filter validCategory; + + private final BundleContext m_context; + + + public DeviceAnalyzer( BundleContext context ) + { + m_context = context; + } + + + @SuppressWarnings("unused") + private void start() throws InvalidSyntaxException + { + String deviceString = Util.createFilterString( "(%s=%s)", new Object[] + { org.osgi.framework.Constants.OBJECTCLASS, Device.class.getName() } ); + + deviceImpl = m_context.createFilter( deviceString ); + + String categoryString = Util.createFilterString( "(%s=%s)", new Object[] + { Constants.DEVICE_CATEGORY, "*" } ); + + validCategory = m_context.createFilter( categoryString ); + } + + + /** + * used to analyze invalid devices + * + * @param ref + */ + public void deviceAdded( ServiceReference ref ) + { + + if ( deviceImpl.match( ref ) ) + { + return; + } + if ( validCategory.match( ref ) ) + { + Object cat = ref.getProperty( Constants.DEVICE_CATEGORY ); + if ( !String[].class.isInstance( cat ) ) + { + m_log.log( LogService.LOG_ERROR, "invalid device: invalid device category: " + Util.showDevice( ref ) ); + return; + } + if ( String[].class.cast( cat ).length == 0 ) + { + m_log.log( LogService.LOG_ERROR, "invalid device: empty device category: " + Util.showDevice( ref ) ); + } + } + else + { + m_log.log( LogService.LOG_ERROR, "invalid device: no device category: " + Util.showDevice( ref ) ); + } + } +} diff --git a/deviceaccess/src/main/java/org/apache/felix/das/util/DriverAnalyzer.java b/deviceaccess/src/main/java/org/apache/felix/das/util/DriverAnalyzer.java new file mode 100644 index 00000000000..0197f32464e --- /dev/null +++ b/deviceaccess/src/main/java/org/apache/felix/das/util/DriverAnalyzer.java @@ -0,0 +1,60 @@ +/* + * 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.das.util; + + +import org.osgi.framework.ServiceReference; +import org.osgi.service.device.Constants; +import org.osgi.service.log.LogService; + + +/** + * TODO: add javadoc + * + * @author Felix Project Team + */ +public class DriverAnalyzer +{ + + private LogService m_log; + + + /** + * used to analyze invalid drivers + * + * @param ref + */ + public void driverAdded( ServiceReference ref ) + { + Object driverId = ref.getProperty( Constants.DRIVER_ID ); + if ( driverId == null || !String.class.isInstance( driverId ) ) + { + m_log.log( LogService.LOG_ERROR, "invalid driver: no driver id: " + Util.showDriver( ref ) ); + return; + } + if ( String.class.isInstance( driverId ) ) + { + String value = (String)( driverId ); + if ( value.length() == 0 ) + { + m_log.log( LogService.LOG_ERROR, "invalid driver: empty driver id: " + Util.showDriver( ref ) ); + } + } + } +} diff --git a/deviceaccess/src/main/java/org/apache/felix/das/util/DriverLoader.java b/deviceaccess/src/main/java/org/apache/felix/das/util/DriverLoader.java new file mode 100644 index 00000000000..321b2adb6a3 --- /dev/null +++ b/deviceaccess/src/main/java/org/apache/felix/das/util/DriverLoader.java @@ -0,0 +1,194 @@ +/* + * 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.das.util; + + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Dictionary; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.felix.das.DriverAttributes; +import org.apache.felix.das.Log; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.device.Constants; +import org.osgi.service.device.DriverLocator; + + +/** + * TODO: add javadoc + * + * @author Felix Project Team + */ +public class DriverLoader +{ + + private final BundleContext m_context; + + private final Log m_log; + + public final static String DRIVER_LOCATION_PREFIX = "_DD_"; + + /** + * to keep track of all loaded drivers + */ + private final List m_loadedDrivers; + + + public DriverLoader( Log log, BundleContext context ) + { + m_log = log; + m_context = context; + m_loadedDrivers = new ArrayList(); + } + + + @SuppressWarnings("all") + public Set findDrivers( Collection locators, Dictionary dict ) + { + final Set list = new HashSet(); + for ( DriverLocator locator : locators ) + { + list.addAll( findDrivers( locator, dict ) ); + } + + return list; + } + + + @SuppressWarnings("all") + private List findDrivers( DriverLocator locator, Dictionary dict ) + { + final List list = new ArrayList(); + try + { + String[] ids = locator.findDrivers( dict ); + if ( ids != null ) + { + list.addAll( Arrays.asList( ids ) ); + } + } + catch ( Exception e ) + { + // ignore, will also frequently happen (?) + // m_log.error("findDrivers failed for: " + locator, e); + } + + return list; + } + + + /** + * load all drivers that belong to the given driver Ids + * + * @param locator + * @param driverIds + */ + public List loadDrivers( List locators, String[] driverIds ) + { + List driverRefs = new ArrayList(); + + for ( String driverId : driverIds ) + { + driverRefs.addAll( loadDriver( locators, driverId ) ); + } + + return driverRefs; + } + + + private List loadDriver( List locators, String driverId ) + { + List driverRefs = new ArrayList(); + + for ( DriverLocator driverLocator : locators ) + { + List drivers = loadDriver( driverLocator, driverId ); + driverRefs.addAll( drivers ); + if ( drivers.size() > 0 ) + { + break; + } + } + + return driverRefs; + } + + + private List loadDriver( DriverLocator locator, String driverId ) + { + List driverRefs = new ArrayList(); + try + { + InputStream in = locator.loadDriver( driverId ); + // System.out.println(driverId + ", " + locator + " returned: " + + // in); + Bundle driverBundle = m_context.installBundle( DRIVER_LOCATION_PREFIX + driverId, in ); + + driverBundle.start(); + + ServiceReference[] refs = driverBundle.getRegisteredServices(); + + driverRefs.addAll( Arrays.asList( refs ) ); + // keep track of them locally + m_loadedDrivers.addAll( Arrays.asList( refs ) ); + + } + catch ( Throwable t ) + { + // ignore, this will happen frequently, if there are multiple + // locators + } + return driverRefs; + } + + + public void unload( DriverAttributes finalDriver ) + { + + ServiceReference finalRef = null; + if ( finalDriver != null ) + { + finalRef = finalDriver.getReference(); + m_log.debug( "unloading all except: " + finalRef.getProperty( Constants.DRIVER_ID ) ); + } + for ( ServiceReference ref : m_loadedDrivers ) + { + if ( !ref.equals( finalRef ) ) + { + try + { + m_log.debug( "uninstalling: " + ref.getProperty( Constants.DRIVER_ID ) ); + ref.getBundle().uninstall(); + } + catch ( BundleException e ) + { + m_log.warning( "unable to uninstall: " + ref.getProperty( Constants.DRIVER_ID ) ); + } + } + } + } +} diff --git a/deviceaccess/src/main/java/org/apache/felix/das/util/DriverMatcher.java b/deviceaccess/src/main/java/org/apache/felix/das/util/DriverMatcher.java new file mode 100644 index 00000000000..f97c97492e6 --- /dev/null +++ b/deviceaccess/src/main/java/org/apache/felix/das/util/DriverMatcher.java @@ -0,0 +1,209 @@ +/* + * 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.das.util; + + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.apache.felix.das.DriverAttributes; +import org.apache.felix.das.Log; +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceReference; +import org.osgi.service.device.Constants; +import org.osgi.service.device.DriverSelector; +import org.osgi.service.device.Match; + + +/** + * TODO: add javadoc + * + * @author Felix Project Team + */ +public class DriverMatcher +{ + + private final Log m_log; + + SortedMap> m_map = new TreeMap>(); + + List m_matches = new ArrayList(); + + + public DriverMatcher( Log log ) + { + m_log = log; + } + + + // we keep track of the driver attributes in two + // lists, one to aid us if there is no driver selector, one + // if there is... + public void add( Integer match, DriverAttributes value ) + { + List da = get( match ); + da.add( value ); + m_matches.add( new MatchImpl( value.getReference(), match ) ); + } + + + private List get( Integer key ) + { + List da = m_map.get( key ); + if ( da == null ) + { + m_map.put( ( Integer ) key, new ArrayList() ); + } + return m_map.get( key ); + } + + + public Match getBestMatch() + { + if ( m_map.isEmpty() ) + { + return null; + } + + int matchValue = m_map.lastKey(); + + // these are the matches that + // got the highest match value + List das = m_map.get( matchValue ); + if ( das.size() == 1 ) + { + // a shortcut: there's only one with the highest match + return new MatchImpl( das.get( 0 ).getReference(), matchValue ); + } + + // get the highest ranking driver + final SortedMap matches = new TreeMap( new ServicePriority() ); + + for ( DriverAttributes da : das ) + { + matches.put( da.getReference(), new MatchImpl( da.getReference(), matchValue ) ); + } + + ServiceReference last = matches.lastKey(); + return matches.get( last ); + } + + + public Match selectBestMatch( ServiceReference deviceRef, DriverSelector selector ) + { +// Match[] matches = m_matches.toArray( new Match[0] ); + + //(re)check bundle status + List activeMatches = new ArrayList(); + for (Match match : m_matches) { + if (match.getDriver().getBundle().getState() == Bundle.ACTIVE) { + activeMatches.add(match); + } else { + m_log.debug("skipping: " + match + ", it's bundle is: " + match.getDriver().getBundle().getState()); + } + } + try + { + Match[] matches = activeMatches.toArray( new Match[0] ); + int index = selector.select( deviceRef, matches ); + if ( index != DriverSelector.SELECT_NONE && index >= 0 && index < matches.length ) + { + return matches[index]; + } + } + catch ( Exception e ) + { + m_log.error( "exception thrown in DriverSelector.select()", e ); + } + return null; + } + + private class MatchImpl implements Match + { + + private final ServiceReference ref; + private final int match; + + + public MatchImpl( ServiceReference ref, int match ) + { + this.ref = ref; + this.match = match; + } + + + public ServiceReference getDriver() + { + return ref; + } + + + public int getMatchValue() + { + return match; + } + + + public String toString() + { + return "[MatchImpl: DRIVER_ID=" + ref.getProperty( Constants.DRIVER_ID ) + ", match=" + match + "]"; + } + + } + + private class ServicePriority implements Comparator + { + + private int getValue( ServiceReference ref, String key, int defaultValue ) + { + Object obj = ref.getProperty( key ); + if ( obj == null ) + { + return defaultValue; + } + try + { + return Integer.class.cast( obj ); + } + catch ( Exception e ) + { + return defaultValue; + } + } + + + public int compare( ServiceReference o1, ServiceReference o2 ) + { + int serviceRanking1 = getValue( o1, org.osgi.framework.Constants.SERVICE_RANKING, 0 ); + int serviceRanking2 = getValue( o2, org.osgi.framework.Constants.SERVICE_RANKING, 0 ); + + if ( serviceRanking1 != serviceRanking2 ) + { + return ( serviceRanking1 - serviceRanking2 ); + } + int serviceId1 = getValue( o1, org.osgi.framework.Constants.SERVICE_ID, Integer.MAX_VALUE ); + int serviceId2 = getValue( o2, org.osgi.framework.Constants.SERVICE_ID, Integer.MAX_VALUE ); + + return ( serviceId2 - serviceId1 ); + } + } +} diff --git a/deviceaccess/src/main/java/org/apache/felix/das/util/Util.java b/deviceaccess/src/main/java/org/apache/felix/das/util/Util.java new file mode 100644 index 00000000000..09ed8c81603 --- /dev/null +++ b/deviceaccess/src/main/java/org/apache/felix/das/util/Util.java @@ -0,0 +1,179 @@ +/* + * 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.das.util; + + +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.device.Device; + + +/** + * TODO: add javadoc + * + * @author Felix Project Team + */ +public class Util +{ + + private Util() + { + } + + + public static String showDriver( ServiceReference ref ) + { + Object objectClass = ref.getProperty( Constants.OBJECTCLASS ); + Object driverId = ref.getProperty( org.osgi.service.device.Constants.DRIVER_ID ); + StringBuffer buffer = new StringBuffer(); + + buffer.append( "Driver: " ); + buffer.append( Constants.OBJECTCLASS ).append( "=" ); + + if ( String[].class.isInstance( objectClass ) ) + { + buffer.append( enumerateStringArray( String[].class.cast( objectClass ) ) ); + } + else + { + buffer.append( objectClass ); + } + buffer.append( " " ); + + buffer.append( org.osgi.service.device.Constants.DRIVER_ID ).append( "=" ); + buffer.append( driverId ); + return buffer.toString(); + } + + public static boolean isDevice( ServiceReference ref ) + { + try + { + Filter device = createFilter( "(|(%s=%s)(%s=%s))", new Object[] + { + Constants.OBJECTCLASS, Device.class.getName(), + org.osgi.service.device.Constants.DEVICE_CATEGORY, "*" + } + ); + return device.match( ref ); + } + catch ( Exception e ) + { + e.printStackTrace(); + } + return false; + } + + public static boolean isDeviceInstance( ServiceReference ref ) + { + try + { + Filter device = createFilter( "(%s=%s)", new Object[] + { Constants.OBJECTCLASS, Device.class.getName() } ); + return device.match( ref ); + } + catch ( Exception e ) + { + e.printStackTrace(); + } + return false; + } + + + public static String createFilterString( String input, Object[] data ) + { + return String.format( input, data ); + } + + + public static Filter createFilter( String input, Object[] data ) throws InvalidSyntaxException + { + return FrameworkUtil.createFilter( String.format( input, data ) ); + } + + + public static String showDevice( ServiceReference ref ) + { + Object objectClass = ref.getProperty( Constants.OBJECTCLASS ); + Object category = ref.getProperty( org.osgi.service.device.Constants.DEVICE_CATEGORY ); + StringBuffer buffer = new StringBuffer(); + + buffer.append( "Device: " ); + buffer.append( Constants.OBJECTCLASS ).append( "=" ); + + appendObject( buffer, objectClass ); + buffer.append( " " ); + + buffer.append( org.osgi.service.device.Constants.DEVICE_CATEGORY ).append( "=" ); + appendObject( buffer, category ); + + buffer.append( "\n{ " ); + String[] keys = ref.getPropertyKeys(); + + for ( String key : keys ) + { + if ( key.equals( Constants.OBJECTCLASS ) ) + { + continue; + } + if ( key.equals( org.osgi.service.device.Constants.DEVICE_CATEGORY ) ) + { + continue; + } + buffer.append( key ).append( "=" ); + appendObject( buffer, ref.getProperty( key ) ); + buffer.append( " " ); + } + buffer.append( "}\n" ); + + return buffer.toString(); + } + + + private static void appendObject( StringBuffer buffer, Object obj ) + { + if ( String[].class.isInstance( obj ) ) + { + buffer.append( enumerateStringArray( String[].class.cast( obj ) ) ); + } + else + { + buffer.append( obj ); + } + } + + + private static String enumerateStringArray( String[] strings ) + { + StringBuffer buffer = new StringBuffer(); + + buffer.append( "[" ); + for ( String str : strings ) + { + buffer.append( str ); + buffer.append( " " ); + } + buffer.deleteCharAt( buffer.length() - 1 ); + buffer.append( "]" ); + return buffer.toString(); + } +} diff --git a/deviceaccess/src/test/java/org/apache/felix/das/DeviceManagerTest.java b/deviceaccess/src/test/java/org/apache/felix/das/DeviceManagerTest.java new file mode 100644 index 00000000000..35ee49618c5 --- /dev/null +++ b/deviceaccess/src/test/java/org/apache/felix/das/DeviceManagerTest.java @@ -0,0 +1,913 @@ +/* + * 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.das; + + +import java.io.IOException; +import java.io.InputStream; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import junit.framework.Assert; + +import org.apache.felix.das.util.DriverLoader; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.device.Constants; +import org.osgi.service.device.Device; +import org.osgi.service.device.Driver; +import org.osgi.service.device.DriverLocator; +import org.osgi.service.device.DriverSelector; +import org.osgi.service.device.Match; +import org.osgi.service.log.LogService; + + + +/** + * Test the actual implementation. + * + * @author Felix Project Team + */ +public class DeviceManagerTest +{ + + @Mock + private DeviceManager m_manager; + + @Mock Bundle m_systemBundle; + + @Mock + private LogService m_log; + + + private BundleContext m_context; + + + private OSGiMock m_osgi; + + + @Before + public void setUp() throws Exception + { + m_osgi = new OSGiMock(); + + MockitoAnnotations.initMocks(this); + + m_context = m_osgi.getBundleContext(); + + m_manager = new DeviceManager( m_context ); + + Utils.invoke( m_manager, "init" ); + + Utils.inject( m_manager, LogService.class, m_log ); + + Mockito.when( m_context.getBundle( 0 ) ).thenReturn( m_systemBundle ); + + final CountDownLatch latch = new CountDownLatch( 1 ); + + Answer answer = new Answer() + { + public Integer answer(InvocationOnMock invocation) throws Throwable + { + latch.countDown(); + return Bundle.ACTIVE; + } + }; + + Mockito.when( m_systemBundle.getState() ).thenAnswer( answer ); + + Utils.invoke( m_manager, "start" ); + latch.await( 5, TimeUnit.SECONDS ); + + Mockito.when( m_context.installBundle(Mockito.isA( String.class ), ( InputStream ) Mockito.isNull() ) ) + .thenThrow(new NullPointerException( "inputstream is null exception" ) ); + } + + + @After + public void tearDown() throws Exception + { + Utils.invoke( m_manager, "stop" ); + Utils.invoke( m_manager, "destroy" ); + } + + private Driver tstCreateDriver( String driverId, int match ) throws Exception + { + Properties p = new Properties(); + p.put( Constants.DRIVER_ID, driverId ); + p.put( "match", Integer.toString( match ) ); + + return tstCreateDriver( p ); + } + + + private Driver tstCreateDriver( Properties p ) throws Exception + { + Driver driver = Mockito.mock( Driver.class ); + + ServiceReference ref = m_osgi.registerService( + new String[]{ Driver.class.getName() }, driver, p ); + + MatchAnswer answer = new MatchAnswer( ref ); + + Mockito.when( driver.match( Mockito.isA( ServiceReference.class ) ) ) + .thenAnswer( answer ); + + Bundle bundle = m_osgi.getBundle( ref ); + Mockito.when( bundle.getLocation() ) + .thenReturn( + DriverLoader.DRIVER_LOCATION_PREFIX + p.getProperty( Constants.DRIVER_ID )); + + return driver; + } + + + private Device tstCreateDevice( String[] cat ) + { + return tstCreateDevice( cat, true ); + } + + + private Device tstCreateDevice( String[] cat, boolean isDevice ) + { + Properties p = new Properties(); + p.put( Constants.DEVICE_CATEGORY, cat ); + if ( isDevice ) + { + return ( Device ) tstCreateService( p, Device.class ); + } + return tstCreateService( p, Object.class ); + } + + + @SuppressWarnings("unchecked") + private T tstCreateService( Properties p, Class iface ) + { + T svc = ( T ) Mockito.mock( iface, iface.getSimpleName() ); + + m_osgi.registerService( new String[] + { iface.getName() }, svc, p ); + return svc; + } + + + /** + * + * prepared all expected behavior for the installation of a dynamic driver + * bundle based on an acquired InputStream. + * + * + * @param driverId + * @param match + * @param in + * @return + * @throws BundleException + * @throws Exception + */ + private Driver tstExpectInstallDriverBundle( String driverId, int match, InputStream in ) throws BundleException, + Exception + { + + Bundle bundle = Mockito.mock( Bundle.class, "driverBundle" ); + Mockito.when( m_context.installBundle( + Mockito.eq( "_DD_" + driverId ), Mockito.eq( in ) ) ) + .thenReturn( bundle ); + + final Driver driver = tstCreateDriver( driverId, match ); + final ServiceReference driverRef = m_osgi.getReference( driver ); + + Answer answer = new Answer() + { + + public Object answer(InvocationOnMock invocation) throws Throwable + { + m_manager.driverAdded( driverRef, driver ); + return null; + } + }; + + //bundle start leads to the addition of the driver to + //the device manager. + Mockito.doAnswer(answer).when(bundle).start(); + + Mockito.when( bundle.getRegisteredServices() ) + .thenReturn( new ServiceReference[]{ driverRef } ); + + return driver; + } + + + /** + * returns a CountDownLatch. + * This countdown latch will count down as soon as Device.noDriverFound() + * has been called. + * @param device the Device + * @return the countdown latch + */ + private CountDownLatch tstExpectNoDriverFound( Device device ) + { + final CountDownLatch latch = new CountDownLatch( 1 ); + + //countdown when noDriverFound is called + Answer answer = new Answer() + { + public Object answer(InvocationOnMock invocation) throws Throwable + { + latch.countDown(); + return null; + } + }; + + Mockito.doAnswer( answer ).when(device).noDriverFound(); + + return latch; + + } + + + private CountDownLatch tstExpectAttach( Driver driver, Object device ) throws Exception + { + final CountDownLatch latch = new CountDownLatch( 1 ); + Answer answer = new Answer() + { + public String answer(InvocationOnMock invocation) throws Throwable + { + latch.countDown(); + return null; + } + }; + + //successful attach + Mockito.when( driver.attach( m_osgi.getReference( device ) ) ) + .thenAnswer( answer ); + + return latch; + } + + private CountDownLatch tstExpectUnloadDriverBundle( Driver driver ) throws BundleException { + + + final CountDownLatch latch = new CountDownLatch( 1 ); + Answer answer = new Answer() + { + public String answer(InvocationOnMock invocation) throws Throwable + { + latch.countDown(); + return null; + } + }; + + Bundle bundle = m_osgi.getBundle( m_osgi.getReference( driver ) ); + + Mockito.doAnswer(answer).when( bundle ).uninstall(); + + return latch; + } + + /** + * This method generates behavior on the provided DriverLocator. + * + * The given driver Ids and their matches are expected as drivers found + * by this driver locator. + * Also, if a driver is found, we can also expect that loadDriver is called; + * resulting in an InputStream. That particular input stream should, when installed + * using a bundle context, lead to the registration of a driver with + * the correcsponding driver id. + * + * @param locator + * @param driverIds + * @param matches + * @return + * @throws Exception + */ + private Map tstExpectDriverLocatorFor( final DriverLocator locator, final String[] driverIds, + int[] matches ) throws Exception + { + + Mockito.when( locator.findDrivers( Mockito.isA( Dictionary.class ) ) ) + .thenReturn( driverIds ); + + Map drivers = new HashMap(); + + final Map streams = new HashMap(); + + for ( String driverId : driverIds ) + { + InputStream in = Mockito.mock(InputStream.class, "[InputStream for: " + driverId + "]"); + streams.put( driverId, in ); + } + + Answer answer = new Answer() + { + public InputStream answer(InvocationOnMock invocation) throws Throwable + { + final String id = invocation.getArguments()[0].toString(); + + for ( String driverId : driverIds ) + { + if ( id.equals( driverId ) ) + { + return streams.get( id ); + } + } + throw new IOException( "no such driverId defined in this locator: " + locator ); + } + }; + + + Mockito.when( locator.loadDriver( Mockito.isA( String.class ) ) ) + .thenAnswer( answer ); + + int i = 0; + for ( String driverId : driverIds ) + { + Driver driver = tstExpectInstallDriverBundle( driverId, matches[i], streams.get( driverId ) ); + drivers.put( driverId, driver ); + i++; + } + + return drivers; + + } + + + /** + * does not really test anything special, but ensures that the internal + * structure is able to parse the addition + */ + @Test + public void LocatorAdded() + { + + DriverLocator locator = Mockito.mock( DriverLocator.class ); + m_manager.locatorAdded( locator ); + + } + + + /** + * does not really test anything special, but ensures that the internal + * structure is able to parse the addition/ removal + */ + @Test + public void LocatorRemoved() + { + + DriverLocator locator = Mockito.mock( DriverLocator.class ); + + m_manager.locatorAdded( locator ); + m_manager.locatorRemoved( locator ); + + } + + + /** + * does not really test anything special, but ensures that the internal + * structure is able to parse the addition + * @throws Exception + */ + @Test + public void DriverAdded() throws Exception + { + + Driver driver = tstCreateDriver( "org.apache.felix.driver-1.0", 1 ); + + m_manager.driverAdded( m_osgi.getReference( driver ), driver ); + + } + + + /** + * does not really test anything special, but ensures that the internal + * structure is able to parse the addition/ removal + * @throws Exception + */ + @Test + public void DriverRemoved() throws Exception + { + + Driver driver = tstCreateDriver( "org.apache.felix.driver-1.0", 1 ); + + ServiceReference ref = m_osgi.getReference( driver ); + + m_manager.driverAdded( ref, driver ); + m_manager.driverRemoved( ref ); + } + + /** + * does not really test anything special, but ensures that the internal + * structure is able to parse the addition/ removal + * @throws Exception + */ + @Test + public void DeviceRemoved() throws Exception + { + + Properties p = new Properties(); + p.put(Constants.DEVICE_CATEGORY, new String[]{"dummy"}); + + ServiceReference ref = OSGiMock.createReference(p); + + Object device = new Object(); + + m_manager.deviceAdded( ref, device ); + m_manager.deviceRemoved( ref ); + } + + /** + * does not really test anything special, but ensures that the internal + * structure is able to parse the addition/ removal + * @throws Exception + */ + @Test + public void DeviceModified() throws Exception + { + + Properties p = new Properties(); + p.put(Constants.DEVICE_CATEGORY, new String[]{"dummy"}); + + ServiceReference ref = OSGiMock.createReference(p); + Object device = new Object(); + + m_manager.deviceAdded( ref, new Object() ); + m_manager.deviceModified(ref, device); + } + //intended flow, various configurations + /** + * We add a device, but there are no driver locators, so + * the noDriverFound method must be called + * @throws InterruptedException + */ + @Test + public void DeviceAddedNoDriverLocator() throws InterruptedException + { + + //create a mocked device + Device device = tstCreateDevice( new String[] + { "org.apache.felix" } ); + + CountDownLatch latch = tstExpectNoDriverFound( device ); + + m_manager.deviceAdded( m_osgi.getReference( device ), device ); + + if ( !latch.await( 5, TimeUnit.SECONDS ) ) + { + Assert.fail( "expected call noDriverFound" ); + } + + } + + + /** + * We add a device, but there are no driver locators, however, there is a driver + * that matches. Thus an attach must follow. + * @throws Exception + */ + @Test + public void DeviceAddedNoDriverLocatorSuccessfulAttach() throws Exception + { + + Device device = tstCreateDevice( new String[] { "org.apache.felix" } ); + Driver driver = tstCreateDriver( "org.apache.felix.driver-1.0", 1 ); + + CountDownLatch attachLatch = tstExpectAttach( driver, device ); + + m_manager.driverAdded( m_osgi.getReference( driver ), driver ); + m_manager.deviceAdded( m_osgi.getReference( device ), device ); + + if ( !attachLatch.await( 5, TimeUnit.SECONDS ) ) + { + Assert.fail( "expected attach" ); + } + + } + + + /** + * We add a device, but there are no driver locators, however, there is a driver + * but it sadly doesn't match. Thus a noDriverFound() is called. + * + * @throws Exception + */ + @Test + public void DeviceAddedNoDriverLocatorAttachFails() throws Exception + { + + Device device = tstCreateDevice( new String[] { "org.apache.felix" } ); + Driver driver = tstCreateDriver( "org.apache.felix.driver-1.0", Device.MATCH_NONE ); + + CountDownLatch attachLatch = tstExpectNoDriverFound( device ); + + m_manager.driverAdded( m_osgi.getReference( driver ), driver ); + m_manager.deviceAdded( m_osgi.getReference( device ), device ); + + if ( !attachLatch.await( 5, TimeUnit.SECONDS ) ) + { + Assert.fail( "expected attach" ); + } + + } + + + /** + * We add a device while there's one driverlocator that will successfully + * locate and load two driver bundles. We expect a Driver.attach() for + * the best matching driver. There's already a driver loaded that should not match. + * + * @throws Exception + */ + @Test + public void DeviceAddedWithADriverLocator() throws Exception + { + + final String driverId1 = "org.apache.felix.driver-1.0"; + final String driverId2 = "org.apache.felix.driver-1.1"; + final String notMatchingButLoadedDriverId = "dotorg.apache.felix.driver-1.0"; + + + DriverLocator locator = Mockito.mock( DriverLocator.class ); + + Map drivers = tstExpectDriverLocatorFor( locator, + new String[] { driverId1, driverId2 }, + new int[] { 30, 3 } ); + + Driver noMatcher = tstCreateDriver( notMatchingButLoadedDriverId, 100 ); + + Device device = tstCreateDevice( new String[]{ "org.apache.felix" } ); + + final CountDownLatch attachLatch = tstExpectAttach( drivers.get( driverId1 ), device ); + + final CountDownLatch unloadDriverLatch = tstExpectUnloadDriverBundle( drivers.get ( driverId2 ) ); + + m_manager.locatorAdded( locator ); + + m_manager.driverAdded( m_osgi.getReference( noMatcher ), noMatcher ); + + m_manager.deviceAdded( m_osgi.getReference( device ), device ); + + if ( !attachLatch.await( 5, TimeUnit.SECONDS ) ) + { + Assert.fail( "expected an attach" ); + } + + //since driver1 is attached, we expect an uninstall() + //of all other (dynamically loaded) driver bundles + if ( !unloadDriverLatch.await( 5, TimeUnit.SECONDS ) ) + { + Assert.fail( "expected an unload" ); + } + + } + + @Test + public void DeviceAddedWithADriverLocatorUnloadFails() throws Exception + { + + final String driverId1 = "org.apache.felix.driver-1.0"; + final String driverId2 = "org.apache.felix.driver-1.1"; + final String notMatchingButLoadedDriverId = "dotorg.apache.felix.driver-1.0"; + + + DriverLocator locator = Mockito.mock( DriverLocator.class ); + + Map drivers = tstExpectDriverLocatorFor( locator, + new String[] { driverId1, driverId2 }, + new int[] { 30, 3 } ); + + Driver noMatcher = tstCreateDriver( notMatchingButLoadedDriverId, 100 ); + + Device device = tstCreateDevice( new String[]{ "org.apache.felix" } ); + + final CountDownLatch attachLatch = tstExpectAttach( drivers.get( driverId1 ), device ); + + final CountDownLatch unloadDriverLatch = new CountDownLatch( 1 ); + + ServiceReference driver2Ref = m_osgi.getReference( drivers.get( driverId2 ) ); + Bundle driver2Bundle = m_osgi.getBundle( driver2Ref ); + + Answer answer = new Answer() { + + public Object answer(InvocationOnMock invocation) throws Throwable { + try { + throw new BundleException("test driverBundle uninstall failed"); + } + finally { + unloadDriverLatch.countDown(); + } + } + }; + + Mockito.doAnswer(answer).when(driver2Bundle).uninstall(); + + m_manager.locatorAdded( locator ); + + m_manager.driverAdded( m_osgi.getReference( noMatcher ), noMatcher ); + + m_manager.deviceAdded( m_osgi.getReference( device ), device ); + + if ( !attachLatch.await( 5, TimeUnit.SECONDS ) ) + { + Assert.fail( "expected an attach" ); + } + + if ( !unloadDriverLatch.await( 5, TimeUnit.SECONDS ) ) + { + Assert.fail( "expected an unload" ); + } + + + //since driver1 is attached, we expect an uninstall() + //of all other (dynamically loaded) driver bundles + //Driver driver = drivers.get( driverId2 ); + //tstVerifyBundleUninstall( driver ); + } + + /** + * Two drivers equally match the device. There is a driver selector + * that comes to the rescue that selects driver2. + * + * @throws Exception + */ + @Test + public void EqualMatchWithDriverSelector() throws Exception + { + + final String driverId1 = "org.apache.felix.driver-1.0"; + final String driverId2 = "org.apache.felix.driver-1.1"; + + DriverLocator locator = Mockito.mock( DriverLocator.class ); + + Map drivers = tstExpectDriverLocatorFor( locator, + new String[] { driverId1, driverId2 }, + new int[] { 20, 20 } ); + + Device device = tstCreateDevice( new String[]{ "org.apache.felix" } ); + + DriverSelector selector = Mockito.mock( DriverSelector.class ); + + SelectorMatcher matcher = new SelectorMatcher( driverId2 ); + + Mockito.when( selector.select( + Mockito.eq( m_osgi.getReference( device ) ), + Mockito.isA(Match[].class) ) ).thenAnswer( matcher ); + + final CountDownLatch attachLatch = tstExpectAttach( drivers.get( driverId2 ), device ); + + + Utils.inject( m_manager, DriverSelector.class, selector ); + + m_manager.locatorAdded( locator ); + + m_manager.deviceAdded( m_osgi.getReference( device ), device ); + + if ( !attachLatch.await( 5, TimeUnit.SECONDS ) ) + { + Assert.fail( "expected an attach" ); + } + + + //driver2 is attached, so driver1 bundle should uninstall. + //Driver driver = drivers.get( driverId1 ); + //tstVerifyBundleUninstall( driver ); + + } + + + //exceptional flow + @Test + public void DriverLocator_findDriverFails() throws Exception + { + + final CountDownLatch latch = new CountDownLatch( 1 ); + + Answer answer = new Answer() + { + + public String[] answer(InvocationOnMock invocation) throws Throwable + { + latch.countDown(); + throw new RuntimeException( "test exception" ); + } + }; + + DriverLocator locator = Mockito.mock( DriverLocator.class, "locator" ); + Mockito.when( locator.findDrivers( Mockito.isA( Dictionary.class ) ) ) + .thenAnswer( answer ); + + Device device = tstCreateDevice( new String[] + { "org.apache.felix" } ); + + final CountDownLatch latch2 = new CountDownLatch( 1 ); + + Answer answer2 = new Answer() + { + public Object answer(InvocationOnMock invocation) throws Throwable + { + latch2.countDown(); + return null; + } + }; + + Mockito.doAnswer(answer2).when(device).noDriverFound(); + + + m_manager.locatorAdded( locator ); + m_manager.deviceAdded( m_osgi.getReference( device ), device ); + + if ( !latch.await( 5, TimeUnit.SECONDS ) ) + { + Assert.fail( "expected a call to DriverLocator.findDrivers" ); + } + + if ( !latch2.await( 5, TimeUnit.SECONDS ) ) + { + Assert.fail( "expected a call to Driver.noDriverFound" ); + } + + } + + + /** + * This test verified correct behavior when after a driver + * attach led to a referral, this referral leads to an exception. + * + * + * @throws Exception + */ + @Ignore + public void DriverReferral_ReferralFails() throws Exception + { + + final String referredDriver = "org.apache.felix.driver-2.0"; + + String[] driverIds = new String[] + { "org.apache.felix.driver-1.0", "org.apache.felix.driver-1.1" }; + + int[] driverMatches = new int[]{ 1, Device.MATCH_NONE }; + + DriverLocator locator = Mockito.mock( DriverLocator.class, "locator for v1.x" ); + Map drivers = tstExpectDriverLocatorFor( locator, driverIds, driverMatches ); + + + DriverLocator locatorv2 = Mockito.mock( DriverLocator.class, "locator for v2.x (fails always)" ); + Mockito.when( locatorv2.findDrivers( Mockito.isA( Dictionary.class ) ) ) + .thenReturn( null ); + + Mockito.when( locatorv2.loadDriver( Mockito.startsWith( "org.apache.felix.driver-1" ) ) ) + .thenReturn( null ); + + InputStream referredInputStream = Mockito.mock(InputStream.class); + Mockito.when( locatorv2.loadDriver( referredDriver ) ).thenReturn( referredInputStream ); + + + //this is what initial driver referral eventually leads + //to: the loading of a driver bundle + //we fake it, so that it fails + Mockito.when( m_context.installBundle( + Mockito.anyString(), + Mockito.isA( InputStream.class ) ) ) + .thenThrow(new BundleException( "test exception" ) ); + + Driver matched = drivers.get( "org.apache.felix.driver-1.0" ); + + final CountDownLatch latch = new CountDownLatch( 1 ); + + Answer driver10_attach = new Answer() + { + public String answer(InvocationOnMock invocation) throws Throwable + { + System.out.println("driver10_attach()"); + latch.countDown(); + return referredDriver; + } + }; + + Device device = tstCreateDevice( new String[]{ "org.apache.felix" } ); + + + Mockito.when( matched.match( m_osgi.getReference( device ) ) ).thenReturn( 10 ); + + Mockito.when( matched.attach( Mockito.isA( ServiceReference.class ) ) ) + .thenAnswer( driver10_attach ); + +// for ( String driverId : driverIds ) +// { +// Driver driver = drivers.get( driverId ); +// tstExpectBundleUninstall( driver ); +// } + + + //the actual test + + m_manager.locatorAdded( locator ); + m_manager.locatorAdded( locatorv2 ); + + //depman induced callback + m_manager.deviceAdded( m_osgi.getReference( device ), device ); + + + if ( !latch.await( 5, TimeUnit.SECONDS ) ) + { + Assert.fail( "expected an attach to: " + driverIds[0] ); + } + + + Mockito.verify(device).noDriverFound(); + } + + /** + * @author Felix Project Team + */ + private class MatchAnswer implements Answer + { + + private final ServiceReference m_driverRef; + + public MatchAnswer( ServiceReference driverRef ) + { + m_driverRef = driverRef; + } + + + public Integer answer(InvocationOnMock invocation) throws Throwable + { + ServiceReference deviceRef = ( ServiceReference ) invocation.getArguments()[0]; + String[] categories = String[].class.cast( deviceRef.getProperty( Constants.DEVICE_CATEGORY ) ); + String driverId = String.class.cast( m_driverRef.getProperty( Constants.DRIVER_ID ) ); + + for ( String string : categories ) + { + if ( driverId.startsWith( string ) ) + { + Object match = m_driverRef.getProperty( "match" ); + return Integer.valueOf( match.toString() ); + } + } + return Device.MATCH_NONE; + } + + } + + + private class SelectorMatcher implements Answer + { + + private String m_driverId; + + + public SelectorMatcher( String driverId ) + { + m_driverId = driverId; + } + + public Integer answer(InvocationOnMock invocation) throws Throwable + { + int i = 0; + Match[] matches = (Match[])invocation.getArguments()[1]; + + for ( Match match : matches ) + { + if ( match.getDriver().getProperty( Constants.DRIVER_ID ).equals( m_driverId ) ) + { + return i; + } + i++; + } + return DriverSelector.SELECT_NONE; + } + + + } + +} diff --git a/deviceaccess/src/test/java/org/apache/felix/das/DriverAttributesTest.java b/deviceaccess/src/test/java/org/apache/felix/das/DriverAttributesTest.java new file mode 100644 index 00000000000..56cef125d38 --- /dev/null +++ b/deviceaccess/src/test/java/org/apache/felix/das/DriverAttributesTest.java @@ -0,0 +1,174 @@ +/* + * 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.das; + + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.service.device.Device; +import org.osgi.service.device.Driver; + +/** + * + * Some simple tests for the DriverAttributes class. + * + * @author Felix Project Team + * + */ +public class DriverAttributesTest { + + + private DriverAttributes m_attributes; + + @Mock + private ServiceReference m_ref; + + @Mock + private Driver m_driver; + + @Mock + private Bundle m_bundle; + + @Before + public void setUp() throws Exception { + + MockitoAnnotations.initMocks(this); + + Mockito.when(m_ref.getBundle()).thenReturn(m_bundle); + + Mockito.when(m_bundle.getLocation()).thenReturn("_DD_test-driverbundle"); + + m_attributes = new DriverAttributes(m_ref, m_driver); + } + + + @Test + public void VerifyDriverReferenceReturned() throws Exception { + + Assert.assertEquals(m_ref, m_attributes.getReference()); + } + + @Test + public void VerifyDriverInUseByDevice() throws Exception { + + ServiceReference ref = Mockito.mock(ServiceReference.class); + + Mockito.when(ref.getProperty(Constants.OBJECTCLASS)) + .thenReturn(new String[]{Object.class.getName()}); + + Mockito.when(ref.getProperty( + org.osgi.service.device.Constants.DEVICE_CATEGORY)) + .thenReturn(new String[]{"dummy"}); + + Mockito.when(m_bundle.getServicesInUse()).thenReturn(new ServiceReference[]{ref}); + + m_attributes.tryUninstall(); + + Mockito.verify(m_bundle).getLocation(); + Mockito.verify(m_bundle).getServicesInUse(); + Mockito.verifyNoMoreInteractions(m_bundle); + + } + + @Test + public void VerifyDriverInUseByDeviceInstance() throws Exception { + + ServiceReference ref = Mockito.mock(ServiceReference.class); + + Mockito.when(ref.getProperty(Constants.OBJECTCLASS)).thenReturn(new String[]{Device.class.getName()}); + Mockito.when(ref.getProperty( + org.osgi.service.device.Constants.DEVICE_CATEGORY)) + .thenReturn(new String[]{"dummy"}); + + Mockito.when(m_bundle.getServicesInUse()).thenReturn(new ServiceReference[]{ref}); + + m_attributes.tryUninstall(); + + Mockito.verify(m_bundle).getLocation(); + Mockito.verify(m_bundle).getServicesInUse(); + Mockito.verifyNoMoreInteractions(m_bundle); + + } + + + @Test + public void VerifyDriverInUseByNoDevice() throws Exception { + + ServiceReference ref = Mockito.mock(ServiceReference.class); + + Mockito.when(ref.getProperty(Constants.OBJECTCLASS)).thenReturn(new String[]{Object.class.getName()}); + Mockito.when(m_bundle.getServicesInUse()).thenReturn(new ServiceReference[]{ref}); + + m_attributes.tryUninstall(); + + Mockito.verify(m_bundle).getLocation(); + Mockito.verify(m_bundle).getServicesInUse(); + Mockito.verify(m_bundle).uninstall(); + + } + @Test + public void VerifyDriverNotInUseLeadsToUnInstall1() throws Exception { + + Mockito.when(m_bundle.getServicesInUse()).thenReturn(new ServiceReference[0]); + + m_attributes.tryUninstall(); + + Mockito.verify(m_bundle).uninstall(); + + + } + + @Test + public void VerifyDriverNotInUseLeadsToUnInstall2() throws Exception { + + m_attributes.tryUninstall(); + + Mockito.verify(m_bundle).uninstall(); + + } + + @Test + public void VerifyAttachCalledOnDriver() throws Exception { + + + ServiceReference ref = Mockito.mock(ServiceReference.class); + m_attributes.attach(ref); + + Mockito.verify(m_driver).attach(Mockito.eq(ref)); + + } + + @Test + public void VerifyMatchCalledOnDriver() throws Exception { + + + ServiceReference ref = Mockito.mock(ServiceReference.class); + m_attributes.match(ref); + + Mockito.verify(m_driver).match(Mockito.eq(ref)); + + } +} diff --git a/deviceaccess/src/test/java/org/apache/felix/das/OSGiMock.java b/deviceaccess/src/test/java/org/apache/felix/das/OSGiMock.java new file mode 100644 index 00000000000..aaaa5916934 --- /dev/null +++ b/deviceaccess/src/test/java/org/apache/felix/das/OSGiMock.java @@ -0,0 +1,168 @@ +/* + * 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.das; + + +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + + +/** + * + * a very simple mock of an osgi framework. enables the registration of services. + * automatically generates mocked service references for them. + * + * @author Felix Project Team + */ +public class OSGiMock +{ + + @Mock + private BundleContext m_context; + + private Map m_references; + + private Map m_bundles; + + private int m_serviceIndex = 1; + + public OSGiMock() + { + MockitoAnnotations.initMocks(this); + m_references = new HashMap(); + m_bundles = new HashMap(); + } + + public static ServiceReference createReference(final Properties p) + { + ServiceReference ref = Mockito.mock( ServiceReference.class ); + + Mockito.when(ref.getProperty(Mockito.anyString())).thenAnswer(new Answer() { + public Object answer(InvocationOnMock invocation) throws Throwable { + return p.get(invocation.getArguments()[0].toString()); + } + }); + + Mockito.when(ref.getPropertyKeys()).thenAnswer(new Answer() { + public Object answer(InvocationOnMock invocation) throws Throwable { + return p.keySet().toArray(new String[0]); + } + }); + + + return ref; + } + + + + public BundleContext getBundleContext() + { + return m_context; + } + + + @SuppressWarnings("all") + public ServiceReference registerService( String[] ifaces, Object impl, Properties props ) + { + + ServiceReference ref = createReference( ifaces, props ); + + Mockito.when( m_context.registerService( ifaces, impl, props ) ) + .thenReturn( null ); + + Mockito.when( m_context.getService( ref ) ).thenReturn( impl ); + + Mockito.when( ref.getUsingBundles() ).thenReturn( new Bundle[0] ); + + m_references.put( impl, ref ); + + return ref; + } + + + public ServiceReference getReference( Object service ) + { + return m_references.get( service ); + } + + + public Bundle getBundle( ServiceReference ref ) + { + return m_bundles.get( ref ); + } + + + + @SuppressWarnings("all") + public ServiceReference createReference( String[] ifaces, Properties props ) + { + + final ServiceReference ref = Mockito.mock( ServiceReference.class ); + + RefPropAnswer answer = new RefPropAnswer( props, ifaces ); + + Mockito.when( ref.getProperty( Mockito.anyString() ) ) + .thenAnswer( answer ); + + Mockito.when( ref.getPropertyKeys() ) + .thenReturn( props.keySet().toArray( new String[0] ) ); + + Bundle bundle = Mockito.mock( Bundle.class ); + + Mockito.when( ref.getBundle() ).thenReturn( bundle ); + + m_bundles.put( ref, bundle ); + + return ref; + } + + @SuppressWarnings({ "unchecked" }) + private class RefPropAnswer implements Answer + { + private final Dictionary m_p; + + + public RefPropAnswer( Dictionary p, String[] iface ) + { + m_p = p; + m_p.put( Constants.OBJECTCLASS, iface ); + m_p.put( Constants.SERVICE_ID, m_serviceIndex++ ); + } + + + public Object answer(InvocationOnMock invocation) throws Throwable + { + String key = (String)invocation.getArguments()[0]; + return m_p.get( key ); + } + + } +} diff --git a/deviceaccess/src/test/java/org/apache/felix/das/Utils.java b/deviceaccess/src/test/java/org/apache/felix/das/Utils.java new file mode 100644 index 00000000000..6d2d9a3256d --- /dev/null +++ b/deviceaccess/src/test/java/org/apache/felix/das/Utils.java @@ -0,0 +1,78 @@ +/* + * 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.das; + + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + + +/** + * Utility class for injecting objects and invoking + * methods that are normally invoked by the dependency manager. + * + * @author Felix Project Team + * + */ +public class Utils +{ + + public static void invoke( Object target, String method ) + { + try + { + Method m = target.getClass().getDeclaredMethod( method, new Class[0] ); + m.setAccessible( true ); + m.invoke( target, new Object[0] ); + } + catch ( Exception e ) + { + e.printStackTrace(); + junit.framework.Assert.fail( e.getMessage() ); + } + } + + + public static void inject( Object target, Class clazz, Object injectable ) + { + + Field[] fields = target.getClass().getDeclaredFields(); + + for ( Field field : fields ) + { + if ( clazz == field.getType() ) + { + field.setAccessible( true ); + try + { + field.set( target, injectable ); + } + catch ( IllegalArgumentException e ) + { + e.printStackTrace(); + } + catch ( IllegalAccessException e ) + { + e.printStackTrace(); + } + } + } + } + +} diff --git a/deviceaccess/src/test/java/org/apache/felix/das/util/DriverAnalyzerTest.java b/deviceaccess/src/test/java/org/apache/felix/das/util/DriverAnalyzerTest.java new file mode 100644 index 00000000000..88423d9ce38 --- /dev/null +++ b/deviceaccess/src/test/java/org/apache/felix/das/util/DriverAnalyzerTest.java @@ -0,0 +1,131 @@ +/* + * 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.das.util; + + +import java.util.Properties; + +import org.apache.felix.das.OSGiMock; +import org.apache.felix.das.Utils; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.osgi.framework.ServiceReference; +import org.osgi.service.device.Constants; +import org.osgi.service.device.Driver; +import org.osgi.service.log.LogService; + + +/** + * + * Tests the Driver Analyzer. + * + * Nothing fancy is being tested, but if something is changed this + * validates that at least the most basic feedback can be expected. + * + * @author Felix Project Team + * + */ +public class DriverAnalyzerTest { + + + + private DriverAnalyzer m_analyzer; + + + @Mock + private LogService m_log; + + private OSGiMock m_osgi; + + @Before + public void setUp() throws Exception { + + MockitoAnnotations.initMocks(this); + + m_osgi = new OSGiMock(); + m_analyzer = new DriverAnalyzer(); + + Utils.inject(m_analyzer, LogService.class, m_log); + } + + + @Test + public void VerifyCorrectDriverIsIgnored() { + + + Properties p = new Properties(); + p.put(Constants.DRIVER_ID, "a-driver-id"); + + + ServiceReference ref = m_osgi.createReference(new String[]{Driver.class.getName()}, p); + + m_analyzer.driverAdded(ref); + + Mockito.verifyZeroInteractions(m_log); + + } + + @Test + public void VerifyIncorrectDriverNoDriverId() { + + + Properties p = new Properties(); + + ServiceReference ref = m_osgi.createReference(new String[]{Driver.class.getName()}, p); + + m_analyzer.driverAdded(ref); + + Mockito.verify(m_log).log(Mockito.eq(LogService.LOG_ERROR), Mockito.anyString()); + Mockito.verifyNoMoreInteractions(m_log); + + } + + @Test + public void VerifyIncorrectDriverInvalidDriverId() { + + Properties p = new Properties(); + p.put(Constants.DRIVER_ID, new Object()); + + ServiceReference ref = m_osgi.createReference(new String[]{Driver.class.getName()}, p); + + m_analyzer.driverAdded(ref); + + Mockito.verify(m_log).log(Mockito.eq(LogService.LOG_ERROR), Mockito.anyString()); + Mockito.verifyNoMoreInteractions(m_log); + + } + + @Test + public void VerifyIncorrectDriverEmptyDriverId() { + + Properties p = new Properties(); + p.put(Constants.DRIVER_ID, ""); + + ServiceReference ref = m_osgi.createReference(new String[]{Driver.class.getName()}, p); + + m_analyzer.driverAdded(ref); + + Mockito.verify(m_log).log(Mockito.eq(LogService.LOG_ERROR), Mockito.anyString()); + Mockito.verifyNoMoreInteractions(m_log); + + } +} diff --git a/deviceaccess/src/test/java/org/apache/felix/das/util/DriverLoaderTest.java b/deviceaccess/src/test/java/org/apache/felix/das/util/DriverLoaderTest.java new file mode 100644 index 00000000000..d2dca9a0140 --- /dev/null +++ b/deviceaccess/src/test/java/org/apache/felix/das/util/DriverLoaderTest.java @@ -0,0 +1,263 @@ +/* + * 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.das.util; + + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.List; +import java.util.Properties; +import java.util.Set; + +import org.apache.felix.das.DeviceManager; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.device.Constants; +import org.osgi.service.device.DriverLocator; + + +/** + * The Device Manager delegates driver loading to the DriverLoader. + * This JUnit test tests the behavior of that DriverMatcher. + * + * Tests all kinds of driver loading flows. + * all flows pertaining driver loading are grouped in the DriverLoader. + * + * @author Felix Project Team + * + */ +public class DriverLoaderTest +{ + + + private DriverLoader m_loader; + + @Mock + private BundleContext m_context; + + @Mock + private DeviceManager m_log; + + @Before + public void setUp() throws Exception + { + + MockitoAnnotations.initMocks(this); + m_loader = new DriverLoader( m_log, m_context ); + } + + + private DriverLocator tstExpectDriverIdsFor(String[] ids) { + + DriverLocator dl = Mockito.mock(DriverLocator.class ); + Mockito.when( dl.findDrivers( Mockito.isA(Dictionary.class) ) ).thenReturn( ids ); + return dl; + } + + /** + * test whether the driver loader can handle a situation where + * there are no DriverLocators. + * + */ + @Test + public void findDriversNoDriverLocators() + { + + List locators = new ArrayList(); + + + Set driverIds = m_loader.findDrivers( locators, new Properties() ); + Assert.assertTrue( "should be an empty list", driverIds.size() == 0 ); + + } + + + /** + * in this test there is a driver locator. the driver locator is instructed to + * even return some driver ids. + * this test tests whether these driver ids are really returned. + */ + @Test + public void findDriversWithDriverLocator() + { + + List locators = new ArrayList(); + + DriverLocator dl = tstExpectDriverIdsFor( + new String[] { "org.apache.felix.driver-1.0", "org.apache.felix.driver-1.1" } ); + + locators.add( dl ); + + Properties dict = new Properties(); + Set driverIds = m_loader.findDrivers( locators, dict ); + + Assert.assertEquals( "should not be an empty list", 2, driverIds.size()); + + } + + + /** + * in this test there are several driver locators, some of which return + * driver Ids, some don't. we expect an accurate number of driver ids being returned + * from the driverloader. + */ + @Test + public void findDriversWithDriverLocators() + { + + List locators = new ArrayList(); + + DriverLocator dl1 = tstExpectDriverIdsFor( + new String[]{ "org.apache.felix.driver-1.0", "org.apache.felix.driver-1.1" } ); + locators.add( dl1 ); + + DriverLocator dl2 = tstExpectDriverIdsFor( + new String[]{ "org.apache.felix.driver-1.2", "org.apache.felix.driver-1.3" } ); + locators.add( dl2 ); + + DriverLocator dl3 = tstExpectDriverIdsFor( null ); + locators.add( dl3 ); + + + Properties dict = new Properties(); + Set driverIds = m_loader.findDrivers( locators, dict ); + + Assert.assertEquals( "should not be an empty list", 4, driverIds.size() ); + + } + + + @Test + public void findDriversWithDriverLocatorFails() + { + + Properties dict = new Properties(); + List locators = new ArrayList(); + + DriverLocator dl = Mockito.mock( DriverLocator.class, "dl" ); + locators.add( dl ); + + Mockito.when( dl.findDrivers( Mockito.eq( dict ) ) ).thenThrow( new RuntimeException( "test exception" ) ); + + Set driverIds = m_loader.findDrivers( locators, dict ); + + Assert.assertTrue( "should be an empty list", driverIds.size() == 0 ); + + } + + + @Test + public void loadDrivers() throws IOException, BundleException + { + + List locators = new ArrayList(); + + DriverLocator dl = Mockito.mock( DriverLocator.class, "dl" ); + locators.add( dl ); + + String[] driverIds = new String[] + { "org.apache.felix.driver-1.0", "org.apache.felix.driver-1.1", }; + + for ( String string : driverIds ) + { + Mockito.when( dl.loadDriver( Mockito.eq( string ) ) ).thenReturn( null ); + Bundle bundle = Mockito.mock( Bundle.class ); + + Mockito.when( m_context.installBundle( "_DD_" + string, null ) ).thenReturn( bundle ); + bundle.start(); + + ServiceReference ref = Mockito.mock( ServiceReference.class ); + Mockito.when( ref.getProperty( Constants.DRIVER_ID ) ).thenReturn( string ); + Mockito.when( bundle.getRegisteredServices() ).thenReturn( new ServiceReference[] + { ref } ); + } + + List refs = m_loader.loadDrivers( locators, driverIds ); + + Assert.assertEquals( "", 2, refs.size() ); + for ( ServiceReference serviceReference : refs ) + { + String driverId = "" + serviceReference.getProperty( Constants.DRIVER_ID ); + if ( !driverId.equals( driverIds[0] ) && !driverId.equals( driverIds[1] ) ) + { + Assert.fail( "unexpected driverId" ); + } + } + + } + + + @Test + public void loadDrivers_LoadFails() throws IOException, BundleException + { + + List locators = new ArrayList(); + + DriverLocator dl = Mockito.mock( DriverLocator.class, "dl" ); + locators.add( dl ); + + String[] driverIds = new String[] + { "org.apache.felix.driver-1.0", "org.apache.felix.driver-1.1", }; + + for ( String string : driverIds ) + { + Mockito.when( dl.loadDriver( string ) ).thenThrow( new IOException( "test exception" ) ); + } + + List refs = m_loader.loadDrivers( locators, driverIds ); + + Assert.assertEquals( "", 0, refs.size() ); + + } + + + @Test + public void loadDrivers_InstallFails() throws IOException, BundleException + { + + List locators = new ArrayList(); + + DriverLocator dl = Mockito.mock( DriverLocator.class, "dl" ); + locators.add( dl ); + + String[] driverIds = new String[] + { "org.apache.felix.driver-1.0", "org.apache.felix.driver-1.1", }; + + for ( String string : driverIds ) + { + Mockito.when( dl.loadDriver( string ) ).thenReturn( null ); + Mockito.when( m_context.installBundle( DriverLoader.DRIVER_LOCATION_PREFIX + string, null ) ) + .thenThrow(new BundleException( "test exception" ) ); + } + + List refs = m_loader.loadDrivers( locators, driverIds ); + + Assert.assertEquals( "", 0, refs.size() ); + } + +} diff --git a/deviceaccess/src/test/java/org/apache/felix/das/util/DriverMatcherTest.java b/deviceaccess/src/test/java/org/apache/felix/das/util/DriverMatcherTest.java new file mode 100644 index 00000000000..c020d7eff38 --- /dev/null +++ b/deviceaccess/src/test/java/org/apache/felix/das/util/DriverMatcherTest.java @@ -0,0 +1,329 @@ +/* + * 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.das.util; + + + +import org.apache.felix.das.DriverAttributes; +import org.apache.felix.das.DeviceManager; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.service.device.Driver; +import org.osgi.service.device.DriverSelector; +import org.osgi.service.device.Match; + + +/** + * The Device Manager delegates driver matching to the DriverMatcher. + * This JUnit test tests the behavior of that DriverMatcher. + * + * + * @author Felix Project Team + */ +public class DriverMatcherTest +{ + + private DriverMatcher m_matcherImpl; + + private int m_serviceId; + + @Mock + private DeviceManager m_log; + + @Before + public void setUp() throws Exception + { + + m_serviceId = 0; + + MockitoAnnotations.initMocks(this); + + m_matcherImpl = new DriverMatcher( m_log ); + + } + + + private String tstDriverId( Match match ) + { + return ( String ) match.getDriver().getProperty( org.osgi.service.device.Constants.DRIVER_ID ); + } + + + private DriverAttributes tstCreateDriverAttributes( String id, int match, int ranking ) throws Exception + { + + Bundle bundle = Mockito.mock( Bundle.class ); + ServiceReference ref = Mockito.mock( ServiceReference.class ); + + + Mockito.when(ref.getBundle()).thenReturn(bundle); + Mockito.when(bundle.getLocation()).thenReturn(DriverLoader.DRIVER_LOCATION_PREFIX + "-" + id); + + Mockito.when(ref.getProperty(Constants.SERVICE_ID)) + .thenReturn(m_serviceId++); + + Mockito.when(ref.getProperty(org.osgi.service.device.Constants.DRIVER_ID)) + .thenReturn(id); + + + + if ( ranking > 0 ) + { + Mockito.when( ref.getProperty( Constants.SERVICE_RANKING ) ).thenReturn( ranking ); + } + else if ( ranking == 0 ) + { + Mockito.when( ref.getProperty( Constants.SERVICE_RANKING ) ).thenReturn( null ); + } + else + { + //an invalid ranking object + Mockito.when( ref.getProperty( Constants.SERVICE_RANKING ) ).thenReturn( new Object() ); + } + + Driver driver = Mockito.mock( Driver.class ); + Mockito.when( driver.match( Mockito.isA( ServiceReference.class ) ) ).thenReturn( match ); + + return new DriverAttributes( ref, driver ); + + } + + + private void add( String id, int match ) throws Exception + { + add( id, match, 0 ); + } + + + private void add( String id, int match, int ranking ) throws Exception + { + m_matcherImpl.add( match, tstCreateDriverAttributes( id, match, ranking ) ); + } + + + @Test + public void GetBestMatchWithNoDriver() throws Exception + { + + Match match = m_matcherImpl.getBestMatch(); + Assert.assertNull( match ); + + } + + + @Test + public void GetBestMatchWithOneDriver() throws Exception + { + + add( "org.apache.felix.driver-1.0", 1 ); + + Match match = m_matcherImpl.getBestMatch(); + Assert.assertNotNull( match ); + Assert.assertEquals( "org.apache.felix.driver-1.0", tstDriverId( match ) ); + + } + + + @Test + public void GetSelectBestMatchThrowsException() throws Exception + { + + ServiceReference deviceRef = Mockito.mock(ServiceReference.class); + DriverSelector selector = Mockito.mock(DriverSelector.class); + + Mockito.when(selector.select(Mockito.eq(deviceRef), Mockito.isA(Match[].class))) + .thenThrow(new IllegalArgumentException("test")); + + Match match = m_matcherImpl.selectBestMatch(deviceRef, selector); + Assert.assertNull( match ); + + } + + @Test + public void GetBestMatchWithMultipleDrivers() throws Exception + { + + add( "org.apache.felix.driver.a-1.0", 1 ); + add( "org.apache.felix.driver.b-1.0", 1 ); + add( "org.apache.felix.driver.c-1.0", 10 ); + + Match match = m_matcherImpl.getBestMatch(); + Assert.assertNotNull( match ); + Assert.assertEquals( "org.apache.felix.driver.c-1.0", tstDriverId( match ) ); + + } + + + @Test + public void GetBestMatchWithInvalidRanking() throws Exception + { + + add( "org.apache.felix.driver.a-1.0", 1, 0 ); + add( "org.apache.felix.driver.b-1.0", 1, -1 ); + + Match match = m_matcherImpl.getBestMatch(); + Assert.assertNotNull( match ); + Assert.assertEquals( "org.apache.felix.driver.a-1.0", tstDriverId( match ) ); + + } + + + @Test + public void GetBestMatchWithSameRanking() throws Exception + { + + add( "org.apache.felix.driver.a-1.0", 1 ); + add( "org.apache.felix.driver.b-1.0", 1 ); + + Match match = m_matcherImpl.getBestMatch(); + Assert.assertNotNull( match ); + Assert.assertEquals( "org.apache.felix.driver.a-1.0", tstDriverId( match ) ); + Assert.assertEquals( 1, match.getMatchValue() ); + } + + + @Test + public void GetBestMatchWithDifferentRanking() throws Exception + { + + add( "org.apache.felix.driver.a-1.0", 1, 2 ); + add( "org.apache.felix.driver.b-1.0", 1 ); + + Match match = m_matcherImpl.getBestMatch(); + + Assert.assertNotNull( match ); + final String driverId = "org.apache.felix.driver.a-1.0"; + + Assert.assertEquals( driverId, tstDriverId( match ) ); + Assert.assertEquals( 1, match.getMatchValue() ); + } + + + @Test + public void GetBestMatchWithDifferentMatchValue() throws Exception + { + + add( "org.apache.felix.driver.a-1.0", 1 ); + add( "org.apache.felix.driver.b-1.0", 2 ); + add( "org.apache.felix.driver.c-1.0", 1 ); + + Match match = m_matcherImpl.getBestMatch(); + Assert.assertNotNull( match ); + Assert.assertEquals( "org.apache.felix.driver.b-1.0", tstDriverId( match ) ); + + Assert.assertEquals( 2, match.getMatchValue() ); + } + + + @Test + public void selectBestDriver() throws Exception + { + + DriverSelector selector = Mockito.mock( DriverSelector.class ); + ServiceReference deviceRef = Mockito.mock( ServiceReference.class ); + + add( "org.apache.felix.driver-1.0", 1 ); + add( "org.apache.felix.driver-1.1", 1 ); + add( "org.apache.felix.driver-1.2", 1 ); + add( "org.apache.felix.driver-1.3", 1 ); + add( "org.apache.felix.driver-1.4", 1 ); + add( "org.apache.felix.driver-1.5", 1 ); + + + + //this is the actual driverselector implementation + Mockito.when( selector.select( Mockito.isA(ServiceReference.class), Mockito.isA(Match[].class) ) ) + .thenAnswer( new Answer() + { + + public Integer answer(InvocationOnMock invocation) throws Throwable + { + Match[] matches = ( Match[] ) invocation.getArguments()[1]; + int index = 0; + for ( Match m : matches ) + { + if ( tstDriverId( m ).endsWith( "1.3" ) ) + { + return index; + } + index++; + } + Assert.fail( "expected unreachable" ); + return null; + } + } ); + + + Match match = m_matcherImpl.selectBestMatch( deviceRef, selector ); + + Assert.assertNotNull( "no match returned", match ); + String driverId = tstDriverId( match ); + + Assert.assertEquals( "org.apache.felix.driver-1.3", driverId ); + } + + + @Test + public void selectFails() throws Exception + { + + DriverSelector selector = Mockito.mock( DriverSelector.class ); + ServiceReference deviceRef = Mockito.mock( ServiceReference.class ); + + Mockito.when( selector.select( Mockito.eq( deviceRef ), Mockito.isA(Match[].class) ) ) + .thenThrow( new RuntimeException( "test exception" ) ); + + add( "org.apache.felix.driver-1.5", 1 ); + + Match match = m_matcherImpl.selectBestMatch( deviceRef, selector ); + + Assert.assertNull( match ); + + } + + + @Test + public void VerifyMatchToString() throws Exception + { + + DriverSelector selector = Mockito.mock( DriverSelector.class ); + ServiceReference deviceRef = Mockito.mock( ServiceReference.class ); + + Mockito.when( selector.select( Mockito.eq( deviceRef ), Mockito.isA(Match[].class) ) ) + .thenReturn( 0 ); + + add( "org.apache.felix.driver-1.5", 2 ); + + Match match = m_matcherImpl.selectBestMatch( deviceRef, selector ); + + Assert.assertNotNull( match ); + Assert.assertNotNull( match.toString() ); + + } + +} diff --git a/deviceaccess/src/test/java/org/apache/felix/das/util/UtilTest.java b/deviceaccess/src/test/java/org/apache/felix/das/util/UtilTest.java new file mode 100644 index 00000000000..918d32d6f71 --- /dev/null +++ b/deviceaccess/src/test/java/org/apache/felix/das/util/UtilTest.java @@ -0,0 +1,221 @@ +/* + * 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.das.util; + + +import java.util.Properties; + +import junit.framework.Assert; + +import org.apache.felix.das.Utils; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.device.Constants; +import org.osgi.service.device.Device; +import org.osgi.service.log.LogService; + + +/** + * @author Felix Project Team + * + */ +public class UtilTest +{ + + private DeviceAnalyzer m_devA; + + @Mock + private LogService m_log; + + @Mock + private BundleContext m_context; + + + @Before + public void before() throws Exception + { + System.setProperty("org.osgi.vendor.framework", "org.apache.felix.framework"); + + MockitoAnnotations.initMocks(this); + + m_devA = new DeviceAnalyzer( m_context ); + Utils.inject( m_devA, LogService.class, m_log ); + + + String f1 = "(objectClass=org.osgi.service.device.Device)"; + Filter deviceFilter = FrameworkUtil.createFilter(f1); + Mockito.when(m_context.createFilter(Mockito.eq(f1))).thenReturn(deviceFilter); + + String f2 = "(DEVICE_CATEGORY=*)"; + Filter driverFilter = FrameworkUtil.createFilter(f2); + Mockito.when(m_context.createFilter(f2)).thenReturn(driverFilter); + + + Utils.invoke( m_devA, "start" ); + } + + + + private ServiceReference createReference(final Properties p) + { + ServiceReference ref = Mockito.mock( ServiceReference.class ); + + Mockito.when(ref.getProperty(Mockito.anyString())).thenAnswer(new Answer() { + public Object answer(InvocationOnMock invocation) throws Throwable { + return p.get(invocation.getArguments()[0].toString()); + } + }); + + Mockito.when(ref.getPropertyKeys()).thenAnswer(new Answer() { + public Object answer(InvocationOnMock invocation) throws Throwable { + return p.keySet().toArray(new String[0]); + } + }); + + + return ref; + } + + @Test + public void ShowDeviceIfThereIsAnInvalidCategory() throws Exception + { + + Properties p = new Properties(); + p.put( org.osgi.framework.Constants.OBJECTCLASS, new String[]{Object.class.getName()} ); + p.put(Constants.DEVICE_CATEGORY, "dummy"); + + ServiceReference ref = createReference(p); + + m_devA.deviceAdded( ref ); + + Mockito.verify(m_log).log(Mockito.eq(LogService.LOG_ERROR), Mockito.anyString()); + + } + + + @Test + public void ShowDeviceIfThereIsNoCategory() throws Exception + { + + Properties p = new Properties(); + p.put( org.osgi.framework.Constants.OBJECTCLASS, new String[]{Object.class.getName()} ); + + ServiceReference ref = createReference(p); + + m_devA.deviceAdded( ref ); + + Mockito.verify(m_log).log( Mockito.eq( LogService.LOG_ERROR ), Mockito.isA( String.class ) ); + } + + @Test + public void VerifyValidIsDeviceInstanceValidationIfDevice() throws InvalidSyntaxException + { + + Properties p = new Properties(); + p.put( org.osgi.framework.Constants.OBJECTCLASS, new String[]{Device.class.getName()} ); + + ServiceReference ref = createReference(p); + + Assert.assertTrue( "Incorrectly determined as no device", Util.isDeviceInstance(ref) ); + } + + @Test + public void VerifyValidIsDeviceInstanceValidationThrowsException() throws InvalidSyntaxException + { + + Properties p = new Properties(); + p.put( org.osgi.framework.Constants.OBJECTCLASS, new String[]{Device.class.getName()} ); + + ServiceReference ref = createReference(p); + + Assert.assertTrue( "Incorrectly determined as no device", Util.isDeviceInstance(ref) ); + } + + @Test + public void VerifyValidFilterStringCreation() throws InvalidSyntaxException { + + Object[] data = new Object[]{"a","b","c","d"}; + String str = Util.createFilterString("(|(%s=%s)(%s=%s))", data); + + Assert.assertEquals("filter string mismatch","(|(a=b)(c=d))", str); + } + + @Test + public void VerifyValidFilterCreation() throws InvalidSyntaxException { + + Object[] data = new Object[]{Constants.DEVICE_CATEGORY, "dummy"}; + Filter filter = Util.createFilter("(%s=%s)", data); + + + Properties matching = new Properties(); + matching.put(Constants.DEVICE_CATEGORY, new String[]{"dummy", "nonsense"}); + Assert.assertTrue("matching filter does not match", filter.match(matching)); + + Properties notmatching = new Properties(); + notmatching.put(Constants.DEVICE_CATEGORY, new String[]{"lummy", "nonsense"}); + Assert.assertFalse("notmatching filter does match", filter.match(notmatching)); + + + } + + + + + + @Test + public void ShowDeviceIfThereIsAnEmptyCategory() throws Exception + { + + Properties p = new Properties(); + p.put( org.osgi.framework.Constants.OBJECTCLASS, new String[]{Object.class.getName()} ); + p.put( Constants.DEVICE_CATEGORY, new String[0] ); + + + ServiceReference ref = createReference(p); + + m_devA.deviceAdded( ref ); + + Mockito.verify(m_log).log( Mockito.eq( LogService.LOG_ERROR ), Mockito.isA( String.class ) ); + + } + + + @Test + public void NoShowDeviceIfThereIsAValidCategory() throws Exception + { + + Properties p = new Properties(); + p.put( org.osgi.framework.Constants.OBJECTCLASS, new String[]{Device.class.getName()} ); + p.put( Constants.DEVICE_CATEGORY, new String[]{"dummy"} ); + + ServiceReference ref = createReference(p); + + m_devA.deviceAdded( ref ); + } +} diff --git a/doap/doap_Felix.rdf b/doap/doap_Felix.rdf new file mode 100644 index 00000000000..74126a01426 --- /dev/null +++ b/doap/doap_Felix.rdf @@ -0,0 +1,47 @@ + + + + + + 2007-06-27 + + Apache Felix + + + OSGi framework implementation and related technologies. + + + + Java + + + + + + + + + + Felix PMC + + + + + OSGi R6 Specification + OSGi Alliance + OSGi R6 + + + + diff --git a/eventadmin.bridge.configuration/pom.xml b/eventadmin.bridge.configuration/pom.xml deleted file mode 100644 index 98ffc0b6b32..00000000000 --- a/eventadmin.bridge.configuration/pom.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - org.apache.felix - felix - 0.8.0-SNAPSHOT - - 4.0.0 - osgi-bundle - Apache Felix EventAdmin Bridge Configuration - org.apache.felix.eventadmin.bridge.configuration - - - ${pom.groupId} - org.osgi.core - ${pom.version} - provided - - - ${pom.groupId} - org.osgi.compendium - ${pom.version} - provided - - - - - - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} - true - - - EventAdmin Bridge Configuration - Apache Software Foundation - - This bundle provides a bridge between Configuration and EventAdmin events. - - auto-detect - ${pom.artifactId} - - - - - - diff --git a/eventadmin.bridge.configuration/src/main/java/org/apache/felix/eventadmin/bridge/configuration/Activator.java b/eventadmin.bridge.configuration/src/main/java/org/apache/felix/eventadmin/bridge/configuration/Activator.java deleted file mode 100644 index a0055c9256a..00000000000 --- a/eventadmin.bridge.configuration/src/main/java/org/apache/felix/eventadmin/bridge/configuration/Activator.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.bridge.configuration; - -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; - -/** - * The BundleActivator that will register an ConfigurationEventListener service - * with the framework on start. Subsequently, ConfigurationEvents will be - * bridged to available EventAdmin services (as per spec). - * - * @author Felix Project Team - */ -public class Activator implements BundleActivator -{ - /** - * This registers an ConfigurationEventListener service with the framework - * that bridges ConfigurationEvents to the EventAdmin. - * - * @param context - * The context to use - * - * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) - */ - public void start(final BundleContext context) throws Exception - { - new ConfigurationEventToEventAdminBridge(context); - } - - /** - * Stop the bridging of ConfigurationEvents to the EventAdmin. - * - * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) - */ - public void stop(final BundleContext context) throws Exception - { - // Services are unregistered by the framework - } -} diff --git a/eventadmin.bridge.configuration/src/main/java/org/apache/felix/eventadmin/bridge/configuration/ConfigurationEventToEventAdminBridge.java b/eventadmin.bridge.configuration/src/main/java/org/apache/felix/eventadmin/bridge/configuration/ConfigurationEventToEventAdminBridge.java deleted file mode 100644 index 4606bc6e73a..00000000000 --- a/eventadmin.bridge.configuration/src/main/java/org/apache/felix/eventadmin/bridge/configuration/ConfigurationEventToEventAdminBridge.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.bridge.configuration; - -import java.util.Arrays; -import java.util.Hashtable; - -import org.osgi.framework.BundleContext; -import org.osgi.framework.Constants; -import org.osgi.framework.ServiceReference; -import org.osgi.service.cm.ConfigurationAdmin; -import org.osgi.service.cm.ConfigurationEvent; -import org.osgi.service.cm.ConfigurationListener; -import org.osgi.service.event.Event; -import org.osgi.service.event.EventAdmin; -import org.osgi.service.event.EventConstants; - -/** - * @author Felix Project Team - */ -public class ConfigurationEventToEventAdminBridge implements - ConfigurationListener -{ - private final BundleContext m_context; - - public ConfigurationEventToEventAdminBridge(final BundleContext context) - { - m_context = context; - - m_context.registerService(ConfigurationListener.class.getName(), this, - null); - } - - public void configurationEvent(final ConfigurationEvent event) - { - final ServiceReference ref = m_context - .getServiceReference(EventAdmin.class.getName()); - - if(null != ref) - { - final EventAdmin eventAdmin = (EventAdmin) m_context - .getService(ref); - - if(null != eventAdmin) - { - final String topic; - - switch(event.getType()) - { - case ConfigurationEvent.CM_UPDATED: - topic = "org/osgi/service/cm/ConfigurationEvent/CM_UPDATED"; - break; - case ConfigurationEvent.CM_DELETED: - topic = "org/osgi/service/cm/ConfigurationEvent/CM_DELETED"; - break; - default: - m_context.ungetService(ref); - return; - } - - final Hashtable properties = new Hashtable(); - - if(null != event.getFactoryPid()) - { - properties.put("cm.factoryPid", event.getFactoryPid()); - } - - properties.put("cm.pid", event.getPid()); - - final ServiceReference eventRef = event.getReference(); - - if(null == eventRef) - { - throw new IllegalArgumentException( - "ConfigurationEvent.getReference() may not be null"); - } - - properties.put(EventConstants.SERVICE, eventRef); - - properties.put(EventConstants.SERVICE_ID, eventRef.getProperty( - EventConstants.SERVICE_ID)); - - final Object objectClass = eventRef.getProperty( - Constants.OBJECTCLASS); - - if(!(objectClass instanceof String[]) - || !Arrays.asList((String[]) objectClass).contains( - ConfigurationAdmin.class.getName())) - { - throw new IllegalArgumentException( - "Bad objectclass: " + objectClass); - } - - properties.put(EventConstants.SERVICE_OBJECTCLASS, objectClass); - - properties.put(EventConstants.SERVICE_PID, eventRef.getProperty( - EventConstants.SERVICE_PID)); - - eventAdmin.postEvent(new Event(topic, properties)); - - m_context.ungetService(ref); - } - } - } -} diff --git a/eventadmin.bridge.upnp/pom.xml b/eventadmin.bridge.upnp/pom.xml deleted file mode 100644 index 4c0f4d2871d..00000000000 --- a/eventadmin.bridge.upnp/pom.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - org.apache.felix - felix - 0.8.0-SNAPSHOT - - 4.0.0 - osgi-bundle - Apache Felix EventAdmin Bridge UPnP - org.apache.felix.eventadmin.bridge.upnp - - - ${pom.groupId} - org.osgi.core - ${pom.version} - provided - - - ${pom.groupId} - org.osgi.compendium - ${pom.version} - provided - - - - - - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} - true - - - EventAdmin Bridge UPnP - Apache Software Foundation - - This bundle provides a bridge between UPnP and EventAdmin events. - - auto-detect - ${pom.artifactId} - - org.osgi.service.event.EventAdmin, - org.osgi.service.event.EventHandler - - - org.osgi.service.upnp.UPnPEventListener - - - - - - - diff --git a/eventadmin.bridge.upnp/src/main/java/org/apache/felix/eventadmin/bridge/upnp/Activator.java b/eventadmin.bridge.upnp/src/main/java/org/apache/felix/eventadmin/bridge/upnp/Activator.java deleted file mode 100644 index bd36fe7d19a..00000000000 --- a/eventadmin.bridge.upnp/src/main/java/org/apache/felix/eventadmin/bridge/upnp/Activator.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.bridge.upnp; - -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; - -/** - * The BundleActivator that will register an UPnPEventListener service with the - * framework on start. Subsequently, UPnPEvents will be bridged to available - * EventAdmin services (as per spec). - * - * @author Felix Project Team - */ -public class Activator implements BundleActivator -{ - /** - * This registers an UPnPEventListener service with the framework that bridges - * UPnPEvents to the EventAdmin. - * - * @param context The context to use - * - * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) - */ - public void start(final BundleContext context) throws Exception - { - new UPnPEventToEventAdminBridge(context); - } - - /** - * Stop the bridging of UPnPEvents to the EventAdmin. - * - * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) - */ - public void stop(final BundleContext context) throws Exception - { - // Services are unregistered by the framework - } -} diff --git a/eventadmin.bridge.useradmin/pom.xml b/eventadmin.bridge.useradmin/pom.xml deleted file mode 100644 index 60e368ab104..00000000000 --- a/eventadmin.bridge.useradmin/pom.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - org.apache.felix - felix - 0.8.0-SNAPSHOT - - 4.0.0 - osgi-bundle - Apache Felix EventAdmin Bridge UserAdmin - org.apache.felix.eventadmin.bridge.useradmin - - - ${pom.groupId} - org.osgi.core - ${pom.version} - provided - - - ${pom.groupId} - org.osgi.compendium - ${pom.version} - provided - - - - - - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} - true - - - EventAdmin Bridge UserAdmin - Apache Software Foundation - - This bundle provides a bridge between UserAdmin and EventAdmin events. - - auto-detect - ${pom.artifactId} - - - - - - diff --git a/eventadmin.bridge.useradmin/src/main/java/org/apache/felix/eventadmin/bridge/useradmin/Activator.java b/eventadmin.bridge.useradmin/src/main/java/org/apache/felix/eventadmin/bridge/useradmin/Activator.java deleted file mode 100644 index f8116e1e912..00000000000 --- a/eventadmin.bridge.useradmin/src/main/java/org/apache/felix/eventadmin/bridge/useradmin/Activator.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.bridge.useradmin; - -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; - -/** - * The BundleActivator that will register an UserAdminEventListener service with the - * framework on start. Subsequently, UserAdminEvents will be bridged to available - * EventAdmin services (as per spec). - * - * @author Felix Project Team - */ -public class Activator implements BundleActivator -{ - /** - * This registers an UserAdminEventListener service with the framework that bridges - * UserAdminEvents to the EventAdmin. - * - * @param context The context to use - * - * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) - */ - public void start(final BundleContext context) throws Exception - { - new UserAdminEventToEventAdminBridge(context); - } - - /** - * Stop the bridging of UserAdminEvents to the EventAdmin. - * - * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) - */ - public void stop(final BundleContext context) throws Exception - { - // Services are unregistered by the framework - } -} diff --git a/eventadmin.bridge.wireadmin/pom.xml b/eventadmin.bridge.wireadmin/pom.xml deleted file mode 100644 index d35fbe1b51c..00000000000 --- a/eventadmin.bridge.wireadmin/pom.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - org.apache.felix - felix - 0.8.0-SNAPSHOT - - 4.0.0 - osgi-bundle - Apache Felix EventAdmin Bridge WireAdmin - org.apache.felix.eventadmin.bridge.wireadmin - - - ${pom.groupId} - org.osgi.core - ${pom.version} - provided - - - ${pom.groupId} - org.osgi.compendium - ${pom.version} - provided - - - - - - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} - true - - - EventAdmin Bridge WireAdmin - Apache Software Foundation - - This bundle provides a bridge between WireAdmin and EventAdmin events. - - auto-detect - ${pom.artifactId} - - - - - - diff --git a/eventadmin.bridge.wireadmin/src/main/java/org/apache/felix/eventadmin/bridge/wireadmin/Activator.java b/eventadmin.bridge.wireadmin/src/main/java/org/apache/felix/eventadmin/bridge/wireadmin/Activator.java deleted file mode 100644 index ab0245b875a..00000000000 --- a/eventadmin.bridge.wireadmin/src/main/java/org/apache/felix/eventadmin/bridge/wireadmin/Activator.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.bridge.wireadmin; - -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; - -/** - * The BundleActivator that will register an WireAdminEventListener service with the - * framework on start. Subsequently, WireAdminEvents will be bridged to available - * EventAdmin services (as per spec). - * - * @author Felix Project Team - */ -public class Activator implements BundleActivator -{ - /** - * This registers an WireAdminEventListener service with the framework that bridges - * WireAdminEvents to the EventAdmin. - * - * @param context The context to use - * - * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) - */ - public void start(final BundleContext context) throws Exception - { - new WireAdminEventToEventAdminBridge(context); - } - - /** - * Stop the bridging of WireAdminEvents to the EventAdmin. - * - * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) - */ - public void stop(final BundleContext context) throws Exception - { - // Services are unregistered by the framework - } -} diff --git a/eventadmin/bridge.configuration/pom.xml b/eventadmin/bridge.configuration/pom.xml new file mode 100644 index 00000000000..fc89e42a5ba --- /dev/null +++ b/eventadmin/bridge.configuration/pom.xml @@ -0,0 +1,84 @@ + + + + org.apache.felix + felix-parent + 4 + ../../pom/pom.xml + + 4.0.0 + org.apache.felix.eventadmin.bridge.configuration + 0.9.0-SNAPSHOT + Apache Felix EventAdmin Bridge Configuration + + This bundle provides a bridge between Configuration and EventAdmin events. + + bundle + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/eventadmin/bridge.configuration + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/eventadmin/bridge.configuration + http://svn.apache.org/repos/asf/felix/eventadmin/bridge.configuration + + + + 6 + + + + + org.osgi + org.osgi.core + 5.0.0 + provided + + + org.osgi + org.osgi.service.event + 1.3.1 + provided + + + org.osgi + org.osgi.service.cm + 1.5.0 + provided + + + + + + + org.apache.felix + maven-bundle-plugin + 3.2.0 + true + + + ${project.artifactId}.impl.Activator + ${project.artifactId} + The Apache Software Foundation + ${project.artifactId}.impl + + + + + + diff --git a/eventadmin/bridge.configuration/src/main/java/org/apache/felix/eventadmin/bridge/configuration/impl/Activator.java b/eventadmin/bridge.configuration/src/main/java/org/apache/felix/eventadmin/bridge/configuration/impl/Activator.java new file mode 100644 index 00000000000..dac18acb1ae --- /dev/null +++ b/eventadmin/bridge.configuration/src/main/java/org/apache/felix/eventadmin/bridge/configuration/impl/Activator.java @@ -0,0 +1,56 @@ +/* + * 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.eventadmin.bridge.configuration.impl; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +/** + * The BundleActivator that will register an ConfigurationEventListener service + * with the framework on start. Subsequently, ConfigurationEvents will be + * bridged to available EventAdmin services (as per spec). + * + * @author Felix Project Team + */ +public class Activator implements BundleActivator +{ + /** + * This registers an ConfigurationEventListener service with the framework + * that bridges ConfigurationEvents to the EventAdmin. + * + * @param context + * The context to use + * + * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) + */ + public void start(final BundleContext context) throws Exception + { + new ConfigurationEventToEventAdminBridge(context); + } + + /** + * Stop the bridging of ConfigurationEvents to the EventAdmin. + * + * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) + */ + public void stop(final BundleContext context) throws Exception + { + // Services are unregistered by the framework + } +} diff --git a/eventadmin/bridge.configuration/src/main/java/org/apache/felix/eventadmin/bridge/configuration/impl/ConfigurationEventToEventAdminBridge.java b/eventadmin/bridge.configuration/src/main/java/org/apache/felix/eventadmin/bridge/configuration/impl/ConfigurationEventToEventAdminBridge.java new file mode 100644 index 00000000000..4dfb0220712 --- /dev/null +++ b/eventadmin/bridge.configuration/src/main/java/org/apache/felix/eventadmin/bridge/configuration/impl/ConfigurationEventToEventAdminBridge.java @@ -0,0 +1,128 @@ +/* + * 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.eventadmin.bridge.configuration.impl; + +import java.util.Arrays; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.ConfigurationEvent; +import org.osgi.service.cm.ConfigurationListener; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventAdmin; +import org.osgi.service.event.EventConstants; + +/** + * @author Felix Project Team + */ +public class ConfigurationEventToEventAdminBridge implements + ConfigurationListener +{ + private final BundleContext m_context; + + public ConfigurationEventToEventAdminBridge(final BundleContext context) + { + m_context = context; + + m_context.registerService(ConfigurationListener.class, this, + null); + } + + @Override + public void configurationEvent(final ConfigurationEvent event) + { + final ServiceReference ref = m_context.getServiceReference(EventAdmin.class); + + if (null != ref) + { + final EventAdmin eventAdmin = m_context.getService(ref); + + if (null != eventAdmin) + { + try + { + final String topic; + + switch (event.getType()) + { + case ConfigurationEvent.CM_UPDATED: + topic = "org/osgi/service/cm/ConfigurationEvent/CM_UPDATED"; + break; + case ConfigurationEvent.CM_DELETED: + topic = "org/osgi/service/cm/ConfigurationEvent/CM_DELETED"; + break; + case ConfigurationEvent.CM_LOCATION_CHANGED: + topic = "org/osgi/service/cm/ConfigurationEvent/CM_LOCATION_CHANGED"; + break; + default: + return; + } + + final Dictionary properties = new Hashtable(); + + if (null != event.getFactoryPid()) + { + properties.put("cm.factoryPid", event.getFactoryPid()); + } + + properties.put("cm.pid", event.getPid()); + + final ServiceReference eventRef = event.getReference(); + + if (null == eventRef) + { + throw new IllegalArgumentException( + "ConfigurationEvent.getReference() may not be null"); + } + + properties.put(EventConstants.SERVICE, eventRef); + + properties.put(EventConstants.SERVICE_ID, eventRef.getProperty( + EventConstants.SERVICE_ID)); + + final Object objectClass = eventRef.getProperty( + Constants.OBJECTCLASS); + + if(!(objectClass instanceof String[]) + || !Arrays.asList((String[]) objectClass).contains( + ConfigurationAdmin.class.getName())) + { + throw new IllegalArgumentException( + "Bad objectclass: " + objectClass); + } + + properties.put(EventConstants.SERVICE_OBJECTCLASS, objectClass); + + properties.put(EventConstants.SERVICE_PID, eventRef.getProperty( + EventConstants.SERVICE_PID)); + + eventAdmin.postEvent(new Event(topic, properties)); + } + finally + { + m_context.ungetService(ref); + } + } + } + } +} diff --git a/eventadmin/bridge.upnp/pom.xml b/eventadmin/bridge.upnp/pom.xml new file mode 100644 index 00000000000..5f3f8d85428 --- /dev/null +++ b/eventadmin/bridge.upnp/pom.xml @@ -0,0 +1,71 @@ + + + + org.apache.felix + felix + 1.0.4 + ../../pom/pom.xml + + 4.0.0 + bundle + Apache Felix EventAdmin Bridge UPnP + 0.9.0-SNAPSHOT + org.apache.felix.eventadmin.bridge.upnp + + This bundle provides a bridge between UPnP and EventAdmin events. + + + + ${pom.groupId} + org.osgi.core + 1.0.0 + + + ${pom.groupId} + org.osgi.compendium + 1.0.0 + + + + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + ${pom.artifactId}.Activator + ${pom.artifactId} + The Apache Software Foundation + + org.osgi.service.event.EventAdmin, + org.osgi.service.event.EventHandler + + + org.osgi.service.upnp.UPnPEventListener + + ${pom.artifactId}.* + + + + + + diff --git a/eventadmin/bridge.upnp/src/main/java/org/apache/felix/eventadmin/bridge/upnp/Activator.java b/eventadmin/bridge.upnp/src/main/java/org/apache/felix/eventadmin/bridge/upnp/Activator.java new file mode 100644 index 00000000000..a0ce3b8f76a --- /dev/null +++ b/eventadmin/bridge.upnp/src/main/java/org/apache/felix/eventadmin/bridge/upnp/Activator.java @@ -0,0 +1,55 @@ +/* + * 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.eventadmin.bridge.upnp; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +/** + * The BundleActivator that will register an UPnPEventListener service with the + * framework on start. Subsequently, UPnPEvents will be bridged to available + * EventAdmin services (as per spec). + * + * @author Felix Project Team + */ +public class Activator implements BundleActivator +{ + /** + * This registers an UPnPEventListener service with the framework that bridges + * UPnPEvents to the EventAdmin. + * + * @param context The context to use + * + * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) + */ + public void start(final BundleContext context) throws Exception + { + new UPnPEventToEventAdminBridge(context); + } + + /** + * Stop the bridging of UPnPEvents to the EventAdmin. + * + * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) + */ + public void stop(final BundleContext context) throws Exception + { + // Services are unregistered by the framework + } +} diff --git a/eventadmin.bridge.upnp/src/main/java/org/apache/felix/eventadmin/bridge/upnp/UPnPEventToEventAdminBridge.java b/eventadmin/bridge.upnp/src/main/java/org/apache/felix/eventadmin/bridge/upnp/UPnPEventToEventAdminBridge.java similarity index 94% rename from eventadmin.bridge.upnp/src/main/java/org/apache/felix/eventadmin/bridge/upnp/UPnPEventToEventAdminBridge.java rename to eventadmin/bridge.upnp/src/main/java/org/apache/felix/eventadmin/bridge/upnp/UPnPEventToEventAdminBridge.java index 1ddf4881375..1ec3eaa6f4c 100644 --- a/eventadmin.bridge.upnp/src/main/java/org/apache/felix/eventadmin/bridge/upnp/UPnPEventToEventAdminBridge.java +++ b/eventadmin/bridge.upnp/src/main/java/org/apache/felix/eventadmin/bridge/upnp/UPnPEventToEventAdminBridge.java @@ -1,18 +1,20 @@ -/* - * Copyright 2006 The Apache Software Foundation +/* + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.eventadmin.bridge.upnp; @@ -47,7 +49,7 @@ * ServiceListener for EventHandlers in order to determine EventHandler * availability. * - * @author Felix Project Team + * @author Felix Project Team */ public class UPnPEventToEventAdminBridge implements UPnPEventListener { diff --git a/eventadmin/bridge.upnp/src/main/resources/LICENSE b/eventadmin/bridge.upnp/src/main/resources/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/eventadmin/bridge.upnp/src/main/resources/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/eventadmin/bridge.upnp/src/main/resources/NOTICE b/eventadmin/bridge.upnp/src/main/resources/NOTICE new file mode 100644 index 00000000000..0a9e37cdc36 --- /dev/null +++ b/eventadmin/bridge.upnp/src/main/resources/NOTICE @@ -0,0 +1,5 @@ +Apache Felix EventAdmin Bridge UPnP +Copyright 2006 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/eventadmin/bridge.useradmin/pom.xml b/eventadmin/bridge.useradmin/pom.xml new file mode 100644 index 00000000000..6d2446fb45a --- /dev/null +++ b/eventadmin/bridge.useradmin/pom.xml @@ -0,0 +1,64 @@ + + + + org.apache.felix + felix + 1.0.4 + ../../pom/pom.xml + + 4.0.0 + bundle + Apache Felix EventAdmin Bridge UserAdmin + 0.9.0-SNAPSHOT + org.apache.felix.eventadmin.bridge.useradmin + + This bundle provides a bridge between UserAdmin and EventAdmin events. + + + + ${pom.groupId} + org.osgi.core + 1.0.0 + + + ${pom.groupId} + org.osgi.compendium + 1.0.0 + + + + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + ${pom.artifactId}.Activator + ${pom.artifactId} + The Apache Software Foundation + ${pom.artifactId}.* + + + + + + diff --git a/eventadmin/bridge.useradmin/src/main/java/org/apache/felix/eventadmin/bridge/useradmin/Activator.java b/eventadmin/bridge.useradmin/src/main/java/org/apache/felix/eventadmin/bridge/useradmin/Activator.java new file mode 100644 index 00000000000..18231faae1e --- /dev/null +++ b/eventadmin/bridge.useradmin/src/main/java/org/apache/felix/eventadmin/bridge/useradmin/Activator.java @@ -0,0 +1,55 @@ +/* + * 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.eventadmin.bridge.useradmin; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +/** + * The BundleActivator that will register an UserAdminEventListener service with the + * framework on start. Subsequently, UserAdminEvents will be bridged to available + * EventAdmin services (as per spec). + * + * @author Felix Project Team + */ +public class Activator implements BundleActivator +{ + /** + * This registers an UserAdminEventListener service with the framework that bridges + * UserAdminEvents to the EventAdmin. + * + * @param context The context to use + * + * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) + */ + public void start(final BundleContext context) throws Exception + { + new UserAdminEventToEventAdminBridge(context); + } + + /** + * Stop the bridging of UserAdminEvents to the EventAdmin. + * + * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) + */ + public void stop(final BundleContext context) throws Exception + { + // Services are unregistered by the framework + } +} diff --git a/eventadmin.bridge.useradmin/src/main/java/org/apache/felix/eventadmin/bridge/useradmin/UserAdminEventToEventAdminBridge.java b/eventadmin/bridge.useradmin/src/main/java/org/apache/felix/eventadmin/bridge/useradmin/UserAdminEventToEventAdminBridge.java similarity index 80% rename from eventadmin.bridge.useradmin/src/main/java/org/apache/felix/eventadmin/bridge/useradmin/UserAdminEventToEventAdminBridge.java rename to eventadmin/bridge.useradmin/src/main/java/org/apache/felix/eventadmin/bridge/useradmin/UserAdminEventToEventAdminBridge.java index 3a1617d5504..ea23c1918fe 100644 --- a/eventadmin.bridge.useradmin/src/main/java/org/apache/felix/eventadmin/bridge/useradmin/UserAdminEventToEventAdminBridge.java +++ b/eventadmin/bridge.useradmin/src/main/java/org/apache/felix/eventadmin/bridge/useradmin/UserAdminEventToEventAdminBridge.java @@ -1,18 +1,20 @@ -/* - * Copyright 2006 The Apache Software Foundation +/* + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.eventadmin.bridge.useradmin; @@ -30,7 +32,7 @@ import org.osgi.service.useradmin.UserAdminListener; /** - * @author Felix Project Team + * @author Felix Project Team */ public class UserAdminEventToEventAdminBridge implements UserAdminListener { diff --git a/eventadmin/bridge.useradmin/src/main/resources/LICENSE b/eventadmin/bridge.useradmin/src/main/resources/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/eventadmin/bridge.useradmin/src/main/resources/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/eventadmin/bridge.useradmin/src/main/resources/NOTICE b/eventadmin/bridge.useradmin/src/main/resources/NOTICE new file mode 100644 index 00000000000..e768817f8e6 --- /dev/null +++ b/eventadmin/bridge.useradmin/src/main/resources/NOTICE @@ -0,0 +1,5 @@ +Apache Felix EventAdmin Bridge UserAdmin +Copyright 2006 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/eventadmin/bridge.wireadmin/pom.xml b/eventadmin/bridge.wireadmin/pom.xml new file mode 100644 index 00000000000..9e43b040157 --- /dev/null +++ b/eventadmin/bridge.wireadmin/pom.xml @@ -0,0 +1,64 @@ + + + + org.apache.felix + felix + 1.0.4 + ../../pom/pom.xml + + 4.0.0 + bundle + Apache Felix EventAdmin Bridge WireAdmin + 0.9.0-SNAPSHOT + org.apache.felix.eventadmin.bridge.wireadmin + + This bundle provides a bridge between WireAdmin and EventAdmin events. + + + + ${pom.groupId} + org.osgi.core + 1.0.0 + + + ${pom.groupId} + org.osgi.compendium + 1.0.0 + + + + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + The Apache Software Foundation + ${pom.artifactId}.Activator + ${pom.artifactId} + ${pom.artifactId}.* + + + + + + diff --git a/eventadmin/bridge.wireadmin/src/main/java/org/apache/felix/eventadmin/bridge/wireadmin/Activator.java b/eventadmin/bridge.wireadmin/src/main/java/org/apache/felix/eventadmin/bridge/wireadmin/Activator.java new file mode 100644 index 00000000000..f99129aad03 --- /dev/null +++ b/eventadmin/bridge.wireadmin/src/main/java/org/apache/felix/eventadmin/bridge/wireadmin/Activator.java @@ -0,0 +1,55 @@ +/* + * 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.eventadmin.bridge.wireadmin; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +/** + * The BundleActivator that will register an WireAdminEventListener service with the + * framework on start. Subsequently, WireAdminEvents will be bridged to available + * EventAdmin services (as per spec). + * + * @author Felix Project Team + */ +public class Activator implements BundleActivator +{ + /** + * This registers an WireAdminEventListener service with the framework that bridges + * WireAdminEvents to the EventAdmin. + * + * @param context The context to use + * + * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) + */ + public void start(final BundleContext context) throws Exception + { + new WireAdminEventToEventAdminBridge(context); + } + + /** + * Stop the bridging of WireAdminEvents to the EventAdmin. + * + * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) + */ + public void stop(final BundleContext context) throws Exception + { + // Services are unregistered by the framework + } +} diff --git a/eventadmin.bridge.wireadmin/src/main/java/org/apache/felix/eventadmin/bridge/wireadmin/WireAdminEventToEventAdminBridge.java b/eventadmin/bridge.wireadmin/src/main/java/org/apache/felix/eventadmin/bridge/wireadmin/WireAdminEventToEventAdminBridge.java similarity index 86% rename from eventadmin.bridge.wireadmin/src/main/java/org/apache/felix/eventadmin/bridge/wireadmin/WireAdminEventToEventAdminBridge.java rename to eventadmin/bridge.wireadmin/src/main/java/org/apache/felix/eventadmin/bridge/wireadmin/WireAdminEventToEventAdminBridge.java index 006c34abcb4..db33e6a6f0b 100644 --- a/eventadmin.bridge.wireadmin/src/main/java/org/apache/felix/eventadmin/bridge/wireadmin/WireAdminEventToEventAdminBridge.java +++ b/eventadmin/bridge.wireadmin/src/main/java/org/apache/felix/eventadmin/bridge/wireadmin/WireAdminEventToEventAdminBridge.java @@ -1,18 +1,20 @@ -/* - * Copyright 2006 The Apache Software Foundation +/* + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.eventadmin.bridge.wireadmin; @@ -30,7 +32,7 @@ import org.osgi.service.wireadmin.WireAdminListener; /** - * @author Felix Project Team + * @author Felix Project Team */ public class WireAdminEventToEventAdminBridge implements WireAdminListener { diff --git a/eventadmin/bridge.wireadmin/src/main/resources/LICENSE b/eventadmin/bridge.wireadmin/src/main/resources/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/eventadmin/bridge.wireadmin/src/main/resources/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/eventadmin/bridge.wireadmin/src/main/resources/NOTICE b/eventadmin/bridge.wireadmin/src/main/resources/NOTICE new file mode 100644 index 00000000000..6ea3c119748 --- /dev/null +++ b/eventadmin/bridge.wireadmin/src/main/resources/NOTICE @@ -0,0 +1,5 @@ +Apache Felix EventAdmin Bridge WireAdmin +Copyright 2006 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/eventadmin/impl/changelog.txt b/eventadmin/impl/changelog.txt new file mode 100644 index 00000000000..f9241c28395 --- /dev/null +++ b/eventadmin/impl/changelog.txt @@ -0,0 +1,139 @@ +Changes in 1.5.0 +---------------- +** Task + * [FELIX-5773] - Update implementation to EventAdmin 1.4 (R7) +** Improvement + * [FELIX-5813] - EventAdmin async threads should be named +** Bug + * [FELIX-5738] - EventAdmin IgnoreTopic config. property doesn't support wildcards + * [FELIX-5831] - Async/sync Thread Pool Ratio is not changeable at runtime + + +Changes from 1.4.6 to 1.4.8 +--------------------------- +** Improvement + * [FELIX-5323] - EventAdmin should export version 1.3.1 of org.osgi.service.event + + +Changes from 1.4.4 to 1.4.6 +--------------------------- +** Bug + * [FELIX-5006] - EventAdmin threads should be named + * [FELIX-5051] - Memory leak in async delivery + * [FELIX-5107] - NullPointerException in org.apache.felix.eventadmin.impl.adapter.LogEventAdapter if log msg is empty + + +Changes from 1.4.2 to 1.4.4 +--------------------------- +** Improvement + * [FELIX-4623] - Add test for thread based ordering + * [FELIX-4638] - Less locking on event handler timing + * [FELIX-4663] - Potential memory leak in AsyncDeliveryTask +** Bug + * [FELIX-4963] - Eventadmin leaks caller's security context downstream + + +Changes from 1.4.0 to 1.4.2 +--------------------------- +** Improvement + * [FELIX-4623] - Make Async to Sync ThreadPool Ratio Configurable + * [FELIX-4629] - Documentation - Properties and Property Defaults Incorrect + * [FELIX-4630] - Adding PerformanceTestIT to measure difference between send and post events +** Bug + * [FELIX-4627] - Potential Memory Leak in AsyncDeliverTasks + * [FELIX-4617] - Empty configurations for ignore topic and ignore timeout lead to error messages in the log + * [FELIX-4618] - NPE if config value for ignore topic or timeout is empty + + +Changes from 1.3.2 to 1.4.0 +--------------------------- +** Improvement + * [FELIX-4608] - Merge Performance IT into Event Admin + * [FELIX-4604] - Add a configuration to ignore certain events + * [FELIX-3511] - Use java.concurrent from Java 6 + * [FELIX-4316] - Packages imported dynamically should also be imported statically with an optional flag + + +Changes from 1.3.0 to 1.3.2 +--------------------------- +** Bug + * [FELIX-3689] - Event admin requires org.osgi.util.tracker in version 1.5 + + +Changes from 1.2.14 to 1.3.0 +---------------------------- +** Bug + * [FELIX-3121] - Add back the manifest header indicating the provided service + * [FELIX-3451] - EventAdmin ignoring filters for handler which are registered for all (*) topics + +** Improvement + * [FELIX-3319] - Add invalid topics when not accepting EventHandler + * [FELIX-3321] - Improve implementation and reduce load on the service registry + * [FELIX-3518] - Update to EventAdmin Spec 1.3 + + +Changes from 1.2.12 to 1.2.14 +----------------------------- +** Bug + * [FELIX-2997] - java.lang.NullPointerException during shutdown while sending events + * [FELIX-3002] - Embed the OBR specific information for the EventAdmin bundle in the manifest + * [FELIX-3053] - Potential deadlock if event handler throws Throwable and is bypassing timeout handling + * [FELIX-3055] - Event Admin deadlocks when sendEvent is called from within a handleEvent method + +** Improvement + * [FELIX-2156] - Remove Import-Service header in MANIFEST + + +Changes from 1.2.10 to 1.2.12 +----------------------------- +** Bug + * [FELIX-2915] - Potential deadlock on shutdown + + +Changes from 1.2.8 to 1.2.10 +---------------------------- +** Bug + * [FELIX-2836] - Async threads should be daemon threads + +** Improvement + * [FELIX-2861] - Remove unnecessary object creation + + +Changes from 1.2.6 to 1.2.8 +--------------------------- +** Improvement + * [FELIX-2655] - allow event admin log level to be configurable + + +Changes from 1.2.4 to 1.2.6 +--------------------------- +** Bug + * [FELIX-2608] - Threads should be daemon threads + + +Changes from 1.2.2 to 1.2.4 +--------------------------- +** Bug + * [FELIX-2582] - Event admin requires config admin packages + * [FELIX-2431] - EventAdmin service unregistered but not registered again on ConfigAdmin startup + +** Improvement + * [FELIX-2562] - Remove object caches + * [FELIX-2558] - Handle configuration changes without restarting event admin service + + +Changes from 1.0.0 to 1.2.2 +--------------------------- +** Bug + * [FELIX-2089] - IllegalStateException thrown by LogWrapper if logging after bundle stop + +** Improvement + * [FELIX-1875] - Add R4.2 support for Event Admin + * [FELIX-1913] - All events are processed in a queue + * [FELIX-2020] - Make event admin configurable through configuration admin + * [FELIX-1960] - Fine-grained timeout configuration + * [FELIX-664] - Event Admin OBR description + + +Initial Release 1.0.0 +--------------------- diff --git a/eventadmin/impl/pom.xml b/eventadmin/impl/pom.xml new file mode 100644 index 00000000000..566f7cb63dd --- /dev/null +++ b/eventadmin/impl/pom.xml @@ -0,0 +1,244 @@ + + + + org.apache.felix + felix-parent + 6 + ../../pom/pom.xml + + 4.0.0 + bundle + Apache Felix EventAdmin + + This bundle provides an implementation of the OSGi R7 EventAdmin service. + + 1.5.1-SNAPSHOT + org.apache.felix.eventadmin + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/eventadmin/impl + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/eventadmin/impl + http://svn.apache.org/repos/asf/felix/eventadmin/impl + + + + + ${basedir}/target + + + ${bundle.build.name}/${project.build.finalName}.jar + + + NONE + + + + + org.osgi + osgi.core + 6.0.0 + provided + + + org.osgi + org.osgi.service.event + 1.4.0 + provided + + + org.osgi + org.osgi.service.metatype + 1.3.0 + provided + + + org.osgi + org.osgi.service.log + 1.3.0 + provided + + + org.osgi + org.osgi.service.cm + 1.5.0 + provided + + + + junit + junit + 4.12 + test + + + org.jmock + jmock-junit4 + 2.5.1 + test + + + org.slf4j + slf4j-simple + 1.5.2 + test + + + + org.ops4j.pax.exam + pax-exam-container-forked + 4.11.0 + test + + + org.ops4j.pax.exam + pax-exam-junit4 + 4.11.0 + test + + + org.ops4j.pax.exam + pax-exam-link-mvn + 4.11.0 + test + + + org.ops4j.pax.url + pax-url-aether + 2.5.4 + test + + + org.ops4j.pax.url + pax-url-wrap + 2.5.4 + test + + + org.apache.geronimo.specs + geronimo-atinject_1.0_spec + 1.0 + test + + + org.apache.felix + org.apache.felix.framework + 5.6.10 + test + + + + + + ${basedir}/src/main/resources + + + + + org.apache.felix + maven-bundle-plugin + 3.5.0 + true + + + + ${project.artifactId} + + + ${project.artifactId}.impl.Activator + + The Apache Software Foundation + + org.osgi.service.log;version="[1.3,2)" + + + + org.osgi.service.cm;version="[1.2,2)";resolution:=optional, + + + org.osgi.service.metatype;version="[1.1,2)";resolution:=optional, + + + org.osgi.service.log;version="[1.3,2)";resolution:=optional, + + + * + + org.osgi.service.event + org.apache.felix.eventadmin.impl.* + + osgi.implementation;osgi.implementation="osgi.event";uses:="org.osgi.service.event";version:Version="1.4", + osgi.service;objectClass:List<String>="org.osgi.service.event.EventAdmin";uses:="org.osgi.service.event" + + + org.osgi.service.event.EventHandler;availability:=optional;multiple:=true, + org.osgi.service.log.LogService;availability:=optional;multiple:=false, + org.osgi.service.log.LogReaderService;availability:=optional;multiple:=false + + + org.osgi.service.event.EventAdmin + + + + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + + + + project.bundle.file + ${bundle.file.name} + + + + **/ittests/*IT.java + ${additional.ittests} + + + + + + + + perftest + + false + + + **/perftests/*IT.java + + + + diff --git a/eventadmin/impl/src/main/appended-resources/META-INF/DEPENDENCIES b/eventadmin/impl/src/main/appended-resources/META-INF/DEPENDENCIES new file mode 100644 index 00000000000..5bb65dab52a --- /dev/null +++ b/eventadmin/impl/src/main/appended-resources/META-INF/DEPENDENCIES @@ -0,0 +1,16 @@ +I. Included Third-Party Software + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2012). +Licensed under the Apache License 2.0. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2012). +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/Activator.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/Activator.java new file mode 100644 index 00000000000..39378ad6fc0 --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/Activator.java @@ -0,0 +1,90 @@ +/* + * 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.eventadmin.impl; + +import org.apache.felix.eventadmin.impl.util.LogWrapper; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +/** + * The activator of the EventAdmin bundle. This class registers an implementation of + * the OSGi R4 EventAdmin service (see the Compendium 113) with the + * framework. It features timeout-based blacklisting of event-handlers for both, + * asynchronous and synchronous event-dispatching (as a spec conform optional + * extension). + * + * @see Configuration For configuration features of the event admin. + * + * + * @author Felix Project Team + */ +// TODO: Security is in place but untested due to not being implemented by the +// framework. However, it needs to be revisited once security is implemented. +// Two places are affected by this namely, security/* and handler/* +public class Activator implements BundleActivator +{ + private volatile Configuration m_config; + + /** + * Called upon starting of the bundle. Constructs and registers the EventAdmin + * service with the framework. Note that the properties of the service are + * requested from the context in this method hence, the bundle has to be + * restarted in order to take changed properties into account. + * + * @param context The bundle context passed by the framework + * + * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) + */ + public void start(final BundleContext context) + { + // init the LogWrapper. Subsequently, the static methods of the LogWrapper + // can be used to log messages similar to the LogService. The effect of a + // call to any of this methods is either a print to standard out (in case + // no LogService is present) or a call to the respective method of + // available LogServices (the reason is that this way the bundle is + // independent of the org.osgi.service.log package) + LogWrapper.setContext(context); + + // this creates the event admin and starts it + m_config = new Configuration(context); + } + + /** + * Called upon stopping the bundle. This will block until all pending events are + * delivered. An IllegalStateException will be thrown on new events starting with + * the begin of this method. However, it might take some time until we settle + * down which is somewhat cumbersome given that the spec asks for return in + * a timely manner. + * + * @param context The bundle context passed by the framework + * + * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) + */ + public void stop(final BundleContext context) + { + if ( m_config != null ) + { + m_config.destroy(); + } + m_config = null; + + // FELIX-2089: "unset" the bundle context on stop + LogWrapper.setContext(null); + } +} diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/Configuration.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/Configuration.java new file mode 100644 index 00000000000..7ca1dbf923f --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/Configuration.java @@ -0,0 +1,655 @@ +/* + * 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.eventadmin.impl; + +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.StringTokenizer; + +import org.apache.felix.eventadmin.impl.adapter.AbstractAdapter; +import org.apache.felix.eventadmin.impl.adapter.BundleEventAdapter; +import org.apache.felix.eventadmin.impl.adapter.FrameworkEventAdapter; +import org.apache.felix.eventadmin.impl.adapter.LogEventAdapter; +import org.apache.felix.eventadmin.impl.adapter.ServiceEventAdapter; +import org.apache.felix.eventadmin.impl.handler.EventAdminImpl; +import org.apache.felix.eventadmin.impl.security.SecureEventAdminFactory; +import org.apache.felix.eventadmin.impl.tasks.DefaultThreadPool; +import org.apache.felix.eventadmin.impl.util.LogWrapper; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedService; +import org.osgi.service.event.EventAdmin; +import org.osgi.service.metatype.MetaTypeProvider; + + +/** + * The Configuration class encapsules the + * configuration for the event admin. + * + * The service knows about the following properties which are read at bundle startup: + *

      + *

      + * org.apache.felix.eventadmin.ThreadPoolSize - The size of the thread + * pool. + *

      + * The default value is 10. Increase in case of a large amount of synchronous events + * where the EventHandler services in turn send new synchronous events in + * the event dispatching thread or a lot of timeouts are to be expected. A value of + * less then 2 triggers the default value. A value of 2 effectively disables thread + * pooling. + *

      + *

      + *

      + * org.apache.felix.eventadmin.Timeout - The black-listing timeout in + * milliseconds + *

      + * The default value is 5000. Increase or decrease at own discretion. A value of less + * then 100 turns timeouts off. Any other value is the time in milliseconds granted + * to each EventHandler before it gets blacklisted. + *

      + *

      + *

      + * org.apache.felix.eventadmin.RequireTopic - Are EventHandler + * required to be registered with a topic? + *

      + * The default is true. The specification says that EventHandler + * must register with a list of topics they are interested in. Setting this value to + * false will enable that handlers without a topic are receiving all events + * (i.e., they are treated the same as with a topic=*). + *

      + *

      + *

      + * org.apache.felix.eventadmin.IgnoreTimeout - Configure + * EventHandlers to be called without a timeout. + *

      + *

      + * If a timeout is configured by default all event handlers are called using the timeout. + * For performance optimization it is possible to configure event handlers where the + * timeout handling is not used - this reduces the thread usage from the thread pools + * as the timout handling requires an additional thread to call the event handler. + * However, the application should work without this configuration property. It is a + * pure optimization! + *

      + *

      + * The value is a list of strings (separated by comma). If the string ends with a dot, + * all handlers in exactly this package are ignored. If the string ends with a star, + * all handlers in this package and all subpackages are ignored. If the string neither + * ends with a dot nor with a start, this is assumed to define an exact class name. + *

      + *

      + *

      + * org.apache.felix.eventadmin.IgnoreTopic - Configure + * topics to be ignore and not delivered to registered handlers. + *

      + *

      + * For performance optimization it is possible to configure topics which are ignored + * by the event admin implementation. In this case, a event is not delivered to + * registered event handlers. + *

      + *

      + * The value is a list of strings (separated by comma). If a single value ends with a dot, + * all topics in exactly this package are ignored. If a single value ends with a star, + * all topics in this package and all sub packages are ignored. If a single value neither + * ends with a dot nor with a start, this is assumed to define an exact topic. A single + * star can be used to disable delivery completely. + *

      + *

      + *

      + * These properties are read at startup and serve as a default configuration. + * If a configuration admin is configured, the event admin can be configured + * through the config admin. + *

      + * + * @author Felix Project Team + */ +public class Configuration +{ + /** The PID for the event admin. */ + static final String PID = "org.apache.felix.eventadmin.impl.EventAdmin"; + + static final String PROP_THREAD_POOL_SIZE = "org.apache.felix.eventadmin.ThreadPoolSize"; + static final String PROP_ASYNC_TO_SYNC_THREAD_RATIO = "org.apache.felix.eventadmin.AsyncToSyncThreadRatio"; + static final String PROP_TIMEOUT = "org.apache.felix.eventadmin.Timeout"; + static final String PROP_REQUIRE_TOPIC = "org.apache.felix.eventadmin.RequireTopic"; + static final String PROP_IGNORE_TIMEOUT = "org.apache.felix.eventadmin.IgnoreTimeout"; + static final String PROP_IGNORE_TOPIC = "org.apache.felix.eventadmin.IgnoreTopic"; + static final String PROP_LOG_LEVEL = "org.apache.felix.eventadmin.LogLevel"; + + /** The bundle context. */ + private final BundleContext m_bundleContext; + + private int m_threadPoolSize; + + private double m_asyncToSyncThreadRatio; + + private int m_asyncThreadPoolSize; + + private int m_timeout; + + private boolean m_requireTopic; + + private String[] m_ignoreTimeout; + + private String[] m_ignoreTopics; + + private int m_logLevel; + + // The thread pool used - this is a member because we need to close it on stop + private volatile DefaultThreadPool m_sync_pool; + + private volatile DefaultThreadPool m_async_pool; + + // The actual implementation of the service - this is a member because we need to + // close it on stop. Note, security is not part of this implementation but is + // added via a decorator in the start method (this is the wrapped object without + // the wrapper). + private volatile EventAdminImpl m_admin; + + // The registration of the security decorator factory (i.e., the service) + private volatile ServiceRegistration m_registration; + + // all adapters + private AbstractAdapter[] m_adapters; + + private ServiceRegistration m_managedServiceReg; + + // the access control context + private final AccessControlContext acc; + + public Configuration( BundleContext bundleContext ) + { + m_bundleContext = bundleContext; + this.acc = AccessController.getContext(); + + // default configuration + configure( null ); + startOrUpdate(); + + // check for Configuration Admin configuration + try + { + Object service = tryToCreateManagedService(); + if ( service != null ) + { + // add meta type provider if interfaces are available + Object enhancedService = tryToCreateMetaTypeProvider(service); + final String[] interfaceNames; + if ( enhancedService == null ) + { + interfaceNames = new String[] {ManagedService.class.getName()}; + } + else + { + interfaceNames = new String[] {ManagedService.class.getName(), MetaTypeProvider.class.getName()}; + service = enhancedService; + } + Dictionary props = new Hashtable<>(); + props.put( Constants.SERVICE_PID, PID ); + m_managedServiceReg = m_bundleContext.registerService( interfaceNames, service, props ); + } + } + catch ( Throwable t ) + { + // don't care + } + } + + void updateFromConfigAdmin(final Dictionary config) + { + // do this in the background as we don't want to stop + // the config admin + new Thread() + { + + @Override + public void run() + { + if (System.getSecurityManager() != null) + { + AccessController.doPrivileged( + new PrivilegedAction() { + + @Override + public Void run() { + updateFromConfigAdmin0( config ); + return null; + } + + }, + acc + ); + } + else + { + updateFromConfigAdmin0( config ); + } + } + + }.start(); + + } + + void updateFromConfigAdmin0(final Dictionary config) { + synchronized ( Configuration.this ) + { + Configuration.this.configure( config ); + Configuration.this.startOrUpdate(); + } + } + + /** + * Configures this instance. + */ + void configure( Dictionary config ) + { + if ( config == null ) + { + // The size of the internal thread pool. Note that we must execute + // each synchronous event dispatch that happens in the synchronous event + // dispatching thread in a new thread. + // A value of less then 2 triggers the default value. A value of 2 + // effectively disables thread pooling. Furthermore, this will be used by + // a lazy thread pool (i.e., new threads are created when needed). Ones the + // the size is reached and no cached thread is available, the delivery + // is blocked. + m_threadPoolSize = getIntProperty( + PROP_THREAD_POOL_SIZE, m_bundleContext.getProperty(PROP_THREAD_POOL_SIZE), 20, 2); + + // The ratio of asynchronous to synchronous threads in the internal thread + // pool. Ratio must be positive and may be adjusted to represent the + // distribution of post to send operations. Applications with higher number + // of post operations should have a higher ratio. + m_asyncToSyncThreadRatio = getDoubleProperty( + PROP_ASYNC_TO_SYNC_THREAD_RATIO, m_bundleContext.getProperty(PROP_ASYNC_TO_SYNC_THREAD_RATIO), 0.5, 0.0); + + // The timeout in milliseconds - A value of less then 100 turns timeouts off. + // Any other value is the time in milliseconds granted to each EventHandler + // before it gets blacklisted. + m_timeout = getIntProperty(PROP_TIMEOUT, + m_bundleContext.getProperty(PROP_TIMEOUT), 5000, Integer.MIN_VALUE); + + // Are EventHandler required to be registered with a topic? - The default is + // true. The specification says that EventHandler must register with a list + // of topics they are interested in. Setting this value to false will enable + // that handlers without a topic are receiving all events + // (i.e., they are treated the same as with a topic=*). + m_requireTopic = getBooleanProperty( + m_bundleContext.getProperty(PROP_REQUIRE_TOPIC), true); + final String value = m_bundleContext.getProperty(PROP_IGNORE_TIMEOUT); + if ( value == null ) + { + m_ignoreTimeout = null; + } + else + { + final StringTokenizer st = new StringTokenizer(value, ","); + m_ignoreTimeout = new String[st.countTokens()]; + for(int i=0; i 5 ? (int)Math.floor(m_threadPoolSize * m_asyncToSyncThreadRatio) : 2; + } + + private void startOrUpdate() + { + LogWrapper.getLogger().setLogLevel(m_logLevel); + LogWrapper.getLogger().log(LogWrapper.LOG_DEBUG, + PROP_LOG_LEVEL + "=" + m_logLevel); + LogWrapper.getLogger().log(LogWrapper.LOG_DEBUG, + PROP_THREAD_POOL_SIZE + "=" + m_threadPoolSize); + LogWrapper.getLogger().log(LogWrapper.LOG_DEBUG, + PROP_ASYNC_TO_SYNC_THREAD_RATIO + "=" + m_asyncToSyncThreadRatio); + LogWrapper.getLogger().log(LogWrapper.LOG_DEBUG, + "Async Pool Size=" + m_asyncThreadPoolSize); + LogWrapper.getLogger().log(LogWrapper.LOG_DEBUG, + PROP_TIMEOUT + "=" + m_timeout); + LogWrapper.getLogger().log(LogWrapper.LOG_DEBUG, + PROP_REQUIRE_TOPIC + "=" + m_requireTopic); + + // Note that this uses a lazy thread pool that will create new threads on + // demand - in case none of its cached threads is free - until threadPoolSize + // is reached. Subsequently, a threadPoolSize of 2 effectively disables + // caching of threads. + if ( m_sync_pool == null ) + { + m_sync_pool = new DefaultThreadPool(m_threadPoolSize, true); + } + else + { + m_sync_pool.configure(m_threadPoolSize); + } + final int asyncThreadPoolSize = m_asyncThreadPoolSize; + if ( m_async_pool == null ) + { + m_async_pool = new DefaultThreadPool(asyncThreadPoolSize, false); + } + else + { + m_async_pool.configure(asyncThreadPoolSize); + } + + if ( m_admin == null ) + { + m_admin = new EventAdminImpl(m_bundleContext, + m_sync_pool, + m_async_pool, + m_timeout, + m_ignoreTimeout, + m_requireTopic, + m_ignoreTopics); + + // Finally, adapt the outside events to our kind of events as per spec + adaptEvents(m_admin); + + // register the admin wrapped in a service factory (SecureEventAdminFactory) + // that hands-out the m_admin object wrapped in a decorator that checks + // appropriated permissions of each calling bundle + m_registration = m_bundleContext.registerService(EventAdmin.class.getName(), + new SecureEventAdminFactory(m_admin), null); + } + else + { + m_admin.update(m_timeout, m_ignoreTimeout, m_requireTopic, m_ignoreTopics); + } + + } + + /** + * Called upon stopping the bundle. This will block until all pending events are + * delivered. An IllegalStateException will be thrown on new events starting with + * the begin of this method. However, it might take some time until we settle + * down which is somewhat cumbersome given that the spec asks for return in + * a timely manner. + */ + public void destroy() + { + synchronized ( this ) + { + if ( m_adapters != null ) + { + for(int i=0;i properties ) throws ConfigurationException + { + updateFromConfigAdmin(properties); + } + }; + } + catch (Throwable t) + { + // we simply ignore this + } + return null; + } + + /** + * Returns either the parsed int from the value of the property if it is set and + * not less then the min value or the default. Additionally, a warning is + * generated in case the value is erroneous (i.e., can not be parsed as an int or + * is less then the min value). + */ + private int getIntProperty(final String key, final Object value, + final int defaultValue, final int min) + { + if(null != value) + { + final int result; + if ( value instanceof Integer ) + { + result = ((Integer)value).intValue(); + } + else + { + try + { + result = Integer.parseInt(value.toString()); + } + catch (NumberFormatException e) + { + LogWrapper.getLogger().log(LogWrapper.LOG_WARNING, + "Unable to parse property: " + key + " - Using default", e); + return defaultValue; + } + } + if(result >= min) + { + return result; + } + + LogWrapper.getLogger().log(LogWrapper.LOG_WARNING, + "Value for property: " + key + " is to low - Using default"); + } + + return defaultValue; + } + + /** + * Returns either the parsed double from the value of the property if it is set and + * not less then the min value or the default. Additionally, a warning is + * generated in case the value is erroneous (i.e., can not be parsed as an double or + * is less then the min value). + */ + private double getDoubleProperty(final String key, final Object value, + final double defaultValue, final double min) + { + if(null != value) + { + final double result; + if ( value instanceof Double ) + { + result = ((Double)value).doubleValue(); + } + else + { + try + { + result = Double.parseDouble(value.toString()); + } + catch (NumberFormatException e) + { + LogWrapper.getLogger().log(LogWrapper.LOG_WARNING, + "Unable to parse property: " + key + " - Using default", e); + return defaultValue; + } + } + if(result >= min) + { + return result; + } + + LogWrapper.getLogger().log(LogWrapper.LOG_WARNING, + "Value for property: " + key + " is to low - Using default"); + } + + return defaultValue; + } + + /** + * Returns true if the value of the property is set and is either 1, true, or yes + * Returns false if the value of the property is set and is either 0, false, or no + * Returns the defaultValue otherwise + */ + private boolean getBooleanProperty(final Object obj, + final boolean defaultValue) + { + if(null != obj) + { + if ( obj instanceof Boolean ) + { + return ((Boolean)obj).booleanValue(); + } + String value = obj.toString().trim().toLowerCase(); + + if(0 < value.length() && ("0".equals(value) || "false".equals(value) + || "no".equals(value))) + { + return false; + } + + if(0 < value.length() && ("1".equals(value) || "true".equals(value) + || "yes".equals(value))) + { + return true; + } + } + + return defaultValue; + } +} diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/MetaTypeProviderImpl.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/MetaTypeProviderImpl.java new file mode 100644 index 00000000000..46830fe34df --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/MetaTypeProviderImpl.java @@ -0,0 +1,301 @@ +/* + * 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.eventadmin.impl; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Dictionary; + +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedService; +import org.osgi.service.metatype.AttributeDefinition; +import org.osgi.service.metatype.MetaTypeProvider; +import org.osgi.service.metatype.ObjectClassDefinition; + +/** + * The optional meta type provider for the event admin config. + * + * @author Felix Project Team + */ +public class MetaTypeProviderImpl + implements MetaTypeProvider, ManagedService +{ + private final int m_threadPoolSize; + private final int m_timeout; + private final boolean m_requireTopic; + private final String[] m_ignoreTimeout; + private final String[] m_ignoreTopic; + private final double m_asyncThreadPoolRatio; + + private final ManagedService m_delegatee; + + public MetaTypeProviderImpl(final ManagedService delegatee, + final int threadPoolSize, + final int timeout, final boolean requireTopic, + final String[] ignoreTimeout, + final String[] ignoreTopic, + final double asyncThreadPoolRatio) + { + m_threadPoolSize = threadPoolSize; + m_timeout = timeout; + m_requireTopic = requireTopic; + m_delegatee = delegatee; + m_ignoreTimeout = ignoreTimeout; + m_ignoreTopic = ignoreTopic; + m_asyncThreadPoolRatio = asyncThreadPoolRatio; + } + + private ObjectClassDefinition ocd; + + /** + * @see org.osgi.service.cm.ManagedService#updated(java.util.Dictionary) + */ + @Override + public void updated(Dictionary properties) throws ConfigurationException + { + m_delegatee.updated(properties); + } + + /** + * @see org.osgi.service.metatype.MetaTypeProvider#getLocales() + */ + @Override + public String[] getLocales() + { + return null; + } + + /** + * @see org.osgi.service.metatype.MetaTypeProvider#getObjectClassDefinition(java.lang.String, java.lang.String) + */ + @Override + public ObjectClassDefinition getObjectClassDefinition( String id, String locale ) + { + if ( !Configuration.PID.equals( id ) ) + { + return null; + } + + if ( ocd == null ) + { + final ArrayList adList = new ArrayList(); + + adList.add( new AttributeDefinitionImpl( Configuration.PROP_THREAD_POOL_SIZE, "Thread Pool Size", + "The size of the thread pool used for event delivery. The default value is 20. " + + "Increase in case of a large amount of events. A value of " + + "less then 2 triggers the default value. If the pool is exhausted, event delivery " + + "is blocked until a thread becomes available from the pool. Each event is delivered " + + "in a thread from the pool unless the ignore timeouts is configured for the receiving event handler.", + m_threadPoolSize ) ); + adList.add( new AttributeDefinitionImpl( Configuration.PROP_ASYNC_TO_SYNC_THREAD_RATIO, "Async/sync Thread Pool Ratio", + "The ratio of asynchronous to synchronous threads in the internal thread" + + " pool. Ratio must be positive and may be adjusted to represent the " + + "distribution of post to send operations. Applications with higher number " + + "of post operations should have a higher ratio.", + m_asyncThreadPoolRatio)); + + adList.add( new AttributeDefinitionImpl( Configuration.PROP_TIMEOUT, "Timeout", + "The black-listing timeout in milliseconds. The default value is 5000. Increase or decrease " + + "at own discretion. A value of less then 100 turns timeouts off. Any other value is the time " + + "in milliseconds granted to each event handler before it gets blacklisted", + m_timeout ) ); + + adList.add( new AttributeDefinitionImpl( Configuration.PROP_REQUIRE_TOPIC, "Require Topic", + "Are event handlers required to be registered with a topic? " + + "This is enabled by default. The specification says that event handlers " + + "must register with a list of topics they are interested in. Disabling this setting " + + "will enable that handlers without a topic are receiving all events " + + "(i.e., they are treated the same as with a topic=*).", + m_requireTopic ) ); + adList.add( new AttributeDefinitionImpl( Configuration.PROP_IGNORE_TIMEOUT, "Ignore Timeouts", + "Configure event handlers to be called without a timeout. If a timeout is configured by default " + + "all event handlers are called using the timeout. For performance optimization it is possible to " + + "configure event handlers where the timeout handling is not used - this reduces the thread usage " + + "from the thread pools as the timout handling requires an additional thread to call the event " + + "handler. However, the application should work without this configuration property. It is a " + + "pure optimization! The value is a list of strings. If a string ends with a dot, " + + "all handlers in exactly this package are ignored. If the string ends with a star, " + + "all handlers in this package and all subpackages are ignored. If the string neither " + + "ends with a dot nor with a star, this is assumed to define an exact class name.", + AttributeDefinition.STRING, m_ignoreTimeout, Integer.MAX_VALUE, null, null)); + adList.add( new AttributeDefinitionImpl( Configuration.PROP_IGNORE_TOPIC, "Ignore Topics", + "For performance optimization it is possible to configure topics which are ignored " + + "by the event admin implementation. In this case, a event is not delivered to " + + "registered event handlers. The value is a list of strings (separated by comma). " + + "If a single value ends with a dot, all topics in exactly this package are ignored. " + + "If a single value ends with a star, all topics in this package and all sub packages " + + "are ignored. If a single value neither ends with a dot nor with a start, this is assumed " + + "to define an exact topic. A single star can be used to disable delivery completely.", + AttributeDefinition.STRING, m_ignoreTopic, Integer.MAX_VALUE, null, null)); + ocd = new ObjectClassDefinition() + { + + private final AttributeDefinition[] attrs = adList + .toArray( new AttributeDefinition[adList.size()] ); + + + @Override + public String getName() + { + return "Apache Felix Event Admin Implementation"; + } + + + @Override + public InputStream getIcon( int arg0 ) + { + return null; + } + + + @Override + public String getID() + { + return Configuration.PID; + } + + + @Override + public String getDescription() + { + return "Configuration for the Apache Felix Event Admin Implementation." + + " This configuration overwrites configuration defined in framework properties of the same names."; + } + + + @Override + public AttributeDefinition[] getAttributeDefinitions( int filter ) + { + return ( filter == OPTIONAL ) ? null : attrs; + } + }; + } + + return ocd; + } + + class AttributeDefinitionImpl implements AttributeDefinition + { + + private final String id; + private final String name; + private final String description; + private final int type; + private final String[] defaultValues; + private final int cardinality; + private final String[] optionLabels; + private final String[] optionValues; + + + AttributeDefinitionImpl( final String id, final String name, final String description, final boolean defaultValue ) + { + this( id, name, description, BOOLEAN, new String[] + { String.valueOf(defaultValue) }, 0, null, null ); + } + + AttributeDefinitionImpl( final String id, final String name, final String description, final int defaultValue ) + { + this( id, name, description, INTEGER, new String[] + { String.valueOf(defaultValue) }, 0, null, null ); + } + + AttributeDefinitionImpl( final String id, final String name, final String description, final double defaultValue ) + { + this( id, name, description, DOUBLE, new String[] + { String.valueOf(defaultValue) }, 0, null, null ); + } + + AttributeDefinitionImpl( final String id, final String name, final String description, final int type, + final String[] defaultValues, final int cardinality, final String[] optionLabels, + final String[] optionValues ) + { + this.id = id; + this.name = name; + this.description = description; + this.type = type; + this.defaultValues = defaultValues; + this.cardinality = cardinality; + this.optionLabels = optionLabels; + this.optionValues = optionValues; + } + + + @Override + public int getCardinality() + { + return cardinality; + } + + + @Override + public String[] getDefaultValue() + { + return defaultValues; + } + + + @Override + public String getDescription() + { + return description; + } + + + @Override + public String getID() + { + return id; + } + + + @Override + public String getName() + { + return name; + } + + + @Override + public String[] getOptionLabels() + { + return optionLabels; + } + + + @Override + public String[] getOptionValues() + { + return optionValues; + } + + + @Override + public int getType() + { + return type; + } + + + @Override + public String validate( String arg0 ) + { + return null; + } + } +} diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/adapter/AbstractAdapter.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/adapter/AbstractAdapter.java new file mode 100644 index 00000000000..8a687d4876b --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/adapter/AbstractAdapter.java @@ -0,0 +1,55 @@ +/* + * 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.eventadmin.impl.adapter; + +import org.osgi.framework.BundleContext; +import org.osgi.service.event.EventAdmin; + +/** + * Abstract base class for all adapters. + * This class allows to exchange the event admin at runtime + * + * @author Felix Project Team + */ +public abstract class AbstractAdapter +{ + private volatile EventAdmin m_admin; + + /** + * The constructor of the adapter. + * + * @param admin The EventAdmin to use for posting events. + */ + public AbstractAdapter(final EventAdmin admin) + { + if (null == admin) + { + throw new NullPointerException("EventAdmin must not be null"); + } + + m_admin = admin; + } + + protected EventAdmin getEventAdmin() + { + return m_admin; + } + + public abstract void destroy(final BundleContext bundleContext); +} diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/adapter/BundleEventAdapter.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/adapter/BundleEventAdapter.java new file mode 100644 index 00000000000..58106c8f7dc --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/adapter/BundleEventAdapter.java @@ -0,0 +1,120 @@ +/* + * 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.eventadmin.impl.adapter; + +import java.util.Dictionary; +import java.util.Hashtable; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.BundleListener; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventAdmin; +import org.osgi.service.event.EventConstants; + +/** + * This class registers itself as a listener for bundle events and posts them via + * the EventAdmin as specified in 113.6.4 OSGi R4 compendium. + * + * @author Felix Project Team + */ +public class BundleEventAdapter extends AbstractAdapter implements BundleListener +{ + /** + * The constructor of the adapter. This will register the adapter with the given + * context as a BundleListener and subsequently, will post received + * events via the given EventAdmin. + * + * @param context The bundle context with which to register as a listener. + * @param admin The EventAdmin to use for posting events. + */ + public BundleEventAdapter(final BundleContext context, final EventAdmin admin) + { + super(admin); + context.addBundleListener(this); + } + + @Override + public void destroy(BundleContext context) { + context.removeBundleListener(this); + } + + /** + * Once a bundle event is received this method assembles and posts an event via + * the EventAdmin as specified in 113.6.4 OSGi R4 compendium. + * + * @param event The event to adapt. + */ + @Override + public void bundleChanged(final BundleEvent event) + { + final Dictionary properties = new Hashtable(); + + properties.put(EventConstants.EVENT, event); + + properties.put("bundle.id", new Long(event.getBundle() + .getBundleId())); + + final String symbolicName = event.getBundle().getSymbolicName(); + + if (null != symbolicName) + { + properties.put(EventConstants.BUNDLE_SYMBOLICNAME, + symbolicName); + } + + properties.put("bundle", event.getBundle()); + + final StringBuffer topic = new StringBuffer(BundleEvent.class + .getName().replace('.', '/')).append('/'); + + switch (event.getType()) + { + case BundleEvent.INSTALLED: + topic.append("INSTALLED"); + break; + case BundleEvent.STARTED: + topic.append("STARTED"); + break; + case BundleEvent.STOPPED: + topic.append("STOPPED"); + break; + case BundleEvent.UPDATED: + topic.append("UPDATED"); + break; + case BundleEvent.UNINSTALLED: + topic.append("UNINSTALLED"); + break; + case BundleEvent.RESOLVED: + topic.append("RESOLVED"); + break; + case BundleEvent.UNRESOLVED: + topic.append("UNRESOLVED"); + break; + default: + return; // IGNORE EVENT + } + + try { + getEventAdmin().postEvent(new Event(topic.toString(), properties)); + } catch (IllegalStateException e) { + // This is o.k. - indicates that we are stopped. + } + } +} diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/adapter/FrameworkEventAdapter.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/adapter/FrameworkEventAdapter.java new file mode 100644 index 00000000000..a5a55f4e437 --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/adapter/FrameworkEventAdapter.java @@ -0,0 +1,142 @@ +/* + * 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.eventadmin.impl.adapter; + +import java.util.Dictionary; +import java.util.Hashtable; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventAdmin; +import org.osgi.service.event.EventConstants; + +/** + * This class registers itself as a listener for framework events and posts them via + * the EventAdmin as specified in 113.6.3 OSGi R4 compendium. + * + * @author Felix Project Team + */ +public class FrameworkEventAdapter extends AbstractAdapter implements FrameworkListener +{ + /** + * The constructor of the adapter. This will register the adapter with the + * given context as a FrameworkListener and subsequently, will + * post received events via the given EventAdmin. + * + * @param context The bundle context with which to register as a listener. + * @param admin The EventAdmin to use for posting events. + */ + public FrameworkEventAdapter(final BundleContext context, final EventAdmin admin) + { + super(admin); + + context.addFrameworkListener(this); + } + + @Override + public void destroy(BundleContext context) { + context.removeFrameworkListener(this); + } + + /** + * Once a framework event is received this method assembles and posts an event + * via the EventAdmin as specified in 113.6.3 OSGi R4 compendium. + * + * @param event The event to adapt. + */ + @Override + public void frameworkEvent(final FrameworkEvent event) + { + final Dictionary properties = new Hashtable(); + + properties.put(EventConstants.EVENT, event); + + final Bundle bundle = event.getBundle(); + + if (null != bundle) + { + properties.put("bundle.id", new Long(bundle.getBundleId())); + + final String symbolicName = bundle.getSymbolicName(); + + if (null != symbolicName) + { + properties.put(EventConstants.BUNDLE_SYMBOLICNAME, + symbolicName); + } + + properties.put("bundle", bundle); + } + + final Throwable thrown = event.getThrowable(); + + if (null != thrown) + { + properties.put(EventConstants.EXCEPTION_CLASS, + thrown.getClass().getName()); + + final String message = thrown.getMessage(); + + if (null != message) + { + properties.put(EventConstants.EXCEPTION_MESSAGE, + message); + } + + properties.put(EventConstants.EXCEPTION, thrown); + } + + final StringBuffer topic = new StringBuffer( + FrameworkEvent.class.getName().replace('.', '/')) + .append('/'); + + switch (event.getType()) + { + case FrameworkEvent.STARTED: + topic.append("STARTED"); + break; + case FrameworkEvent.ERROR: + topic.append("ERROR"); + break; + case FrameworkEvent.PACKAGES_REFRESHED: + topic.append("PACKAGES_REFRESHED"); + break; + case FrameworkEvent.STARTLEVEL_CHANGED: + topic.append("STARTLEVEL_CHANGED"); + break; + case FrameworkEvent.WARNING: + topic.append("WARNING"); + break; + case FrameworkEvent.INFO: + topic.append("INFO"); + break; + default: + return; // IGNORE EVENT + } + + try { + getEventAdmin().postEvent(new Event(topic.toString(), properties)); + } catch(IllegalStateException e) { + // This is o.k. - indicates that we are stopped. + } + } +} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/adapter/LogEventAdapter.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/adapter/LogEventAdapter.java similarity index 84% rename from eventadmin/src/main/java/org/apache/felix/eventadmin/impl/adapter/LogEventAdapter.java rename to eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/adapter/LogEventAdapter.java index 4d98e8e2e36..7cce20ef9a9 100644 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/adapter/LogEventAdapter.java +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/adapter/LogEventAdapter.java @@ -1,18 +1,20 @@ /* - * Copyright 2005 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.eventadmin.impl.adapter; @@ -34,47 +36,39 @@ /** * This class registers itself as a listener for LogReaderService services * with the framework and subsequently, a LogListener callback with any - * currently available LogReaderService. Any received log event is then + * currently available LogReaderService. Any received log event is then * posted via the EventAdmin as specified in 113.6.6 OSGi R4 compendium. * Note that this class does not create a hard dependency on the org.osgi.service.log - * packages. The adaption only takes place if it is present or once it becomes - * available hence, combined with a DynamicImport-Package no hard dependency is - * needed. - * - * @author Felix Project Team + * packages. The adaption only takes place if it is present or once it becomes + * available hence, combined with a DynamicImport-Package no hard dependency is + * needed. + * + * @author Felix Project Team */ -public class LogEventAdapter implements ServiceListener +public class LogEventAdapter extends AbstractAdapter implements ServiceListener { // The internal lock for this object used instead synchronized(this) private final Object m_lock = new Object(); - + private BundleContext m_context; - + // A singleton instance of the used log listener that is the adapter private Object m_logListener; - - final EventAdmin m_admin; - + /** * The constructor of the adapter. This will register the adapter with the - * given context as a listener for LogReaderService services and + * given context as a listener for LogReaderService services and * subsequently, a LogListener callback with any currently available - * LogReaderService. Any received log event is then posted via the given + * LogReaderService. Any received log event is then posted via the given * EventAdmin. - * + * * @param context The bundle context with which to register as a listener. * @param admin The EventAdmin to use for posting events. */ public LogEventAdapter(final BundleContext context, final EventAdmin admin) { - if(null == admin) - { - throw new NullPointerException("EventAdmin must not be null"); - } - + super(admin); m_context = context; - - m_admin = admin; try { @@ -90,13 +84,13 @@ public LogEventAdapter(final BundleContext context, final EventAdmin admin) { for (int i = 0; i < refs.length; i++) { - final org.osgi.service.log.LogReaderService logReader = + final org.osgi.service.log.LogReaderService logReader = (org.osgi.service.log.LogReaderService) m_context .getService(refs[i]); if (null != logReader) { - logReader.addLogListener((org.osgi.service.log.LogListener) + logReader.addLogListener((org.osgi.service.log.LogListener) getLogListener()); } } @@ -107,32 +101,38 @@ public LogEventAdapter(final BundleContext context, final EventAdmin admin) } } + @Override + public void destroy(BundleContext context) { + context.removeServiceListener(this); + } + /** * Once a LogReaderService register event is received this method - * registers a LogListener with the received service that assembles - * and posts any log event via the EventAdmin as specified in - * 113.6.6 OSGi R4 compendium. - * + * registers a LogListener with the received service that assembles + * and posts any log event via the EventAdmin as specified in + * 113.6.6 OSGi R4 compendium. + * * @param event The event to adapt. */ + @Override public void serviceChanged(final ServiceEvent event) { if (ServiceEvent.REGISTERED == event.getType()) { - final org.osgi.service.log.LogReaderService logReader = + final org.osgi.service.log.LogReaderService logReader = (org.osgi.service.log.LogReaderService) m_context .getService(event.getServiceReference()); if (null != logReader) { - logReader.addLogListener((org.osgi.service.log.LogListener) + logReader.addLogListener((org.osgi.service.log.LogListener) getLogListener()); } } } /* - * Constructs a LogListener that assembles and posts any log event via the + * Constructs a LogListener that assembles and posts any log event via the * EventAdmin as specified in 113.6.6 OSGi R4 compendium. Note that great * care is taken to not create a hard dependency on the org.osgi.service.log * package. @@ -148,13 +148,14 @@ private Object getLogListener() m_logListener = new org.osgi.service.log.LogListener() { + @Override public void logged(final org.osgi.service.log.LogEntry entry) { // This is where the assembly as specified in 133.6.6 OSGi R4 - // compendium is taking place (i.e., the log entry is adapted to + // compendium is taking place (i.e., the log entry is adapted to // an event and posted via the EventAdmin) - - final Dictionary properties = new Hashtable(); + + final Dictionary properties = new Hashtable(); final Bundle bundle = entry.getBundle(); @@ -176,7 +177,8 @@ public void logged(final org.osgi.service.log.LogEntry entry) properties.put("log.level", new Integer(entry.getLevel())); - properties.put(EventConstants.MESSAGE, entry.getMessage()); + properties.put(EventConstants.MESSAGE, + (entry.getMessage()) != null ? entry.getMessage() : "" ); properties.put(EventConstants.TIMESTAMP, new Long( entry.getTime())); @@ -222,7 +224,7 @@ public void logged(final org.osgi.service.log.LogEntry entry) // LOG and IGNORE LogWrapper.getLogger().log( entry.getServiceReference(), - LogWrapper.LOG_WARNING, "Exception parsing " + + LogWrapper.LOG_WARNING, "Exception parsing " + EventConstants.SERVICE_ID + "=" + id, ne); } } @@ -232,7 +234,7 @@ public void logged(final org.osgi.service.log.LogEntry entry) if (null != pid) { - properties.put(EventConstants.SERVICE_PID, + properties.put(EventConstants.SERVICE_PID, pid.toString()); } @@ -280,13 +282,13 @@ public void logged(final org.osgi.service.log.LogEntry entry) } try { - m_admin.postEvent(new Event(topic.toString(), properties)); + getEventAdmin().postEvent(new Event(topic.toString(), properties)); } catch(IllegalStateException e) { // This is o.k. - indicates that we are stopped. } } }; - + return m_logListener; } } diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/adapter/ServiceEventAdapter.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/adapter/ServiceEventAdapter.java new file mode 100644 index 00000000000..cf55b8b97ef --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/adapter/ServiceEventAdapter.java @@ -0,0 +1,144 @@ +/* + * 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.eventadmin.impl.adapter; + +import java.util.Dictionary; +import java.util.Hashtable; + +import org.apache.felix.eventadmin.impl.util.LogWrapper; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventAdmin; +import org.osgi.service.event.EventConstants; + +/** + * This class registers itself as a listener for service events and posts them via + * the EventAdmin as specified in 113.6.5 OSGi R4 compendium. + * + * @author Felix Project Team + */ +public class ServiceEventAdapter extends AbstractAdapter implements ServiceListener +{ + /** + * The constructor of the adapter. This will register the adapter with the + * given context as a ServiceListener and subsequently, will + * post received events via the given EventAdmin. + * + * @param context The bundle context with which to register as a listener. + * @param admin The EventAdmin to use for posting events. + */ + public ServiceEventAdapter(final BundleContext context, final EventAdmin admin) + { + super(admin); + + context.addServiceListener(this); + } + + @Override + public void destroy(BundleContext context) { + context.removeServiceListener(this); + } + + /** + * Once a Service event is received this method assembles and posts an event + * via the EventAdmin as specified in 113.6.5 OSGi R4 compendium. + * + * @param event The event to adapt. + */ + @Override + public void serviceChanged(final ServiceEvent event) + { + final Dictionary properties = new Hashtable(); + + properties.put(EventConstants.EVENT, event); + + properties.put(EventConstants.SERVICE, event + .getServiceReference()); + + final Object id = event.getServiceReference().getProperty( + EventConstants.SERVICE_ID); + + if (null != id) + { + try + { + properties.put(EventConstants.SERVICE_ID, new Long(id + .toString())); + } catch (NumberFormatException ne) + { + // LOG and IGNORE + LogWrapper.getLogger().log(event.getServiceReference(), + LogWrapper.LOG_WARNING, "Exception parsing " + + EventConstants.SERVICE_ID + "=" + id, ne); + } + } + + final Object pid = event.getServiceReference().getProperty( + EventConstants.SERVICE_PID); + + if (null != pid) + { + properties.put(EventConstants.SERVICE_PID, pid.toString()); + } + + final Object objectClass = event.getServiceReference() + .getProperty(Constants.OBJECTCLASS); + + if (null != objectClass) + { + if (objectClass instanceof String[]) + { + properties.put(EventConstants.SERVICE_OBJECTCLASS, + objectClass); + } + else + { + properties.put(EventConstants.SERVICE_OBJECTCLASS, + new String[] { objectClass.toString() }); + } + } + + final StringBuffer topic = new StringBuffer(ServiceEvent.class + .getName().replace('.', '/')).append('/'); + + switch (event.getType()) + { + case ServiceEvent.REGISTERED: + topic.append("REGISTERED"); + break; + case ServiceEvent.MODIFIED: + topic.append("MODIFIED"); + break; + case ServiceEvent.UNREGISTERING: + topic.append("UNREGISTERING"); + break; + default: + return; // IGNORE + } + + try { + getEventAdmin().postEvent(new Event(topic.toString(), properties)); + } catch(IllegalStateException e) { + // This is o.k. - indicates that we are stopped. + } + } +} diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/handler/EventAdminImpl.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/handler/EventAdminImpl.java new file mode 100644 index 00000000000..249bbea2815 --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/handler/EventAdminImpl.java @@ -0,0 +1,187 @@ +/* + * 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.eventadmin.impl.handler; + +import org.apache.felix.eventadmin.impl.tasks.AsyncDeliverTasks; +import org.apache.felix.eventadmin.impl.tasks.DefaultThreadPool; +import org.apache.felix.eventadmin.impl.tasks.SyncDeliverTasks; +import org.apache.felix.eventadmin.impl.util.Matchers; +import org.osgi.framework.BundleContext; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventAdmin; + +/** + * This is the actual implementation of the OSGi R4 Event Admin Service (see the + * Compendium 113 for details). The implementation uses a HandlerTasks + * in order to determine applicable EventHandler for a specific event and + * subsequently dispatches the event to the handlers via DeliverTasks. + * To do this, it uses two different DeliverTasks one for asynchronous and + * one for synchronous event delivery depending on whether its post() or + * its send() method is called. Note that the actual work is done in the + * implementations of the DeliverTasks. Additionally, a stop method is + * provided that prevents subsequent events to be delivered. + * + * @author Felix Project Team + */ +public class EventAdminImpl implements EventAdmin +{ + /** The tracker for the event handlers. */ + private volatile EventHandlerTracker tracker; + + // The asynchronous event dispatcher + private final AsyncDeliverTasks m_postManager; + + // The synchronous event dispatcher + private final SyncDeliverTasks m_sendManager; + + // matchers for ignore topics + private Matchers.Matcher[] m_ignoreTopics; + + /** + * The constructor of the EventAdmin implementation. + * + * @param syncPool The synchronous thread pool + * @param asyncPool The asynchronous thread pool + */ + public EventAdminImpl( + final BundleContext bundleContext, + final DefaultThreadPool syncPool, + final DefaultThreadPool asyncPool, + final int timeout, + final String[] ignoreTimeout, + final boolean requireTopic, + final String[] ignoreTopics) + { + checkNull(syncPool, "syncPool"); + checkNull(asyncPool, "asyncPool"); + + this.tracker = new EventHandlerTracker(bundleContext); + this.tracker.update(ignoreTimeout, requireTopic); + this.tracker.open(); + m_sendManager = new SyncDeliverTasks(syncPool, timeout); + m_postManager = new AsyncDeliverTasks(asyncPool, m_sendManager); + m_ignoreTopics = Matchers.createEventTopicMatchers(ignoreTopics); + } + + /** + * Check if the event admin is active and return the tracker + * @return The tracker + * @throws IllegalArgumentException if the event admin has been stopped + */ + private EventHandlerTracker getTracker() { + final EventHandlerTracker localTracker = tracker; + if ( localTracker == null ) { + throw new IllegalStateException("The EventAdmin is stopped"); + } + return localTracker; + } + + /** + * Check whether the topic should be delivered at all + */ + private boolean checkTopic( final Event event ) + { + boolean result = true; + if ( this.m_ignoreTopics != null ) + { + for(final Matchers.Matcher m : this.m_ignoreTopics) + { + if ( m.match(event.getTopic()) ) + { + result = false; + break; + } + } + } + return result; + } + + /** + * Post an asynchronous event. + * + * @param event The event to be posted by this service + * + * @throws IllegalStateException - In case we are stopped + * + * @see org.osgi.service.event.EventAdmin#postEvent(org.osgi.service.event.Event) + */ + @Override + public void postEvent(final Event event) + { + if ( checkTopic(event) ) + { + m_postManager.execute(this.getTracker().getHandlers(event), event); + } + } + + /** + * Send a synchronous event. + * + * @param event The event to be send by this service + * + * @throws IllegalStateException - In case we are stopped + * + * @see org.osgi.service.event.EventAdmin#sendEvent(org.osgi.service.event.Event) + */ + @Override + public void sendEvent(final Event event) + { + if ( checkTopic(event) ) + { + m_sendManager.execute(this.getTracker().getHandlers(event), event, false); + } + } + + /** + * This method can be used to stop the delivery of events. + */ + public void stop() + { + this.tracker.close(); + this.tracker = null; + } + + /** + * Update the event admin with new configuration. + */ + public void update(final int timeout, + final String[] ignoreTimeout, + final boolean requireTopic, + final String[] ignoreTopics) + { + this.tracker.close(); + this.tracker.update(ignoreTimeout, requireTopic); + this.m_sendManager.update(timeout); + this.tracker.open(); + this.m_ignoreTopics = Matchers.createEventTopicMatchers(ignoreTopics); + } + + /** + * This is a utility method that will throw a NullPointerException + * in case that the given object is null. The message will be of the form + * "${name} + may not be null". + */ + private void checkNull(final Object object, final String name) + { + if (null == object) + { + throw new NullPointerException(name + " may not be null"); + } + } +} diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/handler/EventHandlerProxy.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/handler/EventHandlerProxy.java new file mode 100644 index 00000000000..10adfc2fa3d --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/handler/EventHandlerProxy.java @@ -0,0 +1,445 @@ +/* + * 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.eventadmin.impl.handler; + +import java.util.Collection; +import java.util.Iterator; + +import org.apache.felix.eventadmin.impl.security.PermissionsUtil; +import org.apache.felix.eventadmin.impl.util.LogWrapper; +import org.osgi.framework.Bundle; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventConstants; +import org.osgi.service.event.EventHandler; + +/** + * This is a proxy for event handlers. It gets the real event handler + * on demand and prepares some information for faster processing. + * + * It checks the timeout handling for the implementation as well as + * blacklisting the handler. + * + * @author Felix Project Team + */ +public class EventHandlerProxy { + + /** The service reference for the event handler. */ + private final ServiceReference reference; + + /** The handler context. */ + private final EventHandlerTracker.HandlerContext handlerContext; + + /** The event topics. */ + private volatile String[] topics; + + /** Optional filter. */ + private volatile Filter filter; + + /** Lazy fetched event handler. */ + private volatile EventHandler handler; + + /** Is this handler blacklisted? */ + private volatile boolean blacklisted; + + /** Use timeout. */ + private boolean useTimeout; + + /** Deliver async ordered. */ + private boolean asyncOrderedDelivery; + + /** + * Create an EventHandlerProxy. + * + * @param context The handler context + * @param reference Reference to the EventHandler + */ + public EventHandlerProxy(final EventHandlerTracker.HandlerContext context, + final ServiceReference reference) + { + this.handlerContext = context; + this.reference = reference; + } + + /** + * Update the state with current properties from the service + * @return true if the handler configuration is valid. + */ + public boolean update() + { + this.blacklisted = false; + boolean valid = true; + // First check, topic + final Object topicObj = reference.getProperty(EventConstants.EVENT_TOPIC); + if (topicObj instanceof String) + { + if ( topicObj.toString().equals("*") ) + { + this.topics = null; + } + else + { + this.topics = new String[] {topicObj.toString()}; + } + } + else if (topicObj instanceof String[]) + { + // check if one value matches '*' + final String[] values = (String[])topicObj; + boolean matchAll = false; + for(int i=0;i col = (Collection)topicObj; + final String[] values = new String[col.size()]; + int index = 0; + // check if one value matches '*' + final Iterator i = col.iterator(); + boolean matchAll = false; + while ( i.hasNext() ) + { + final String v = i.next().toString(); + values[index] = v; + index++; + if ( "*".equals(v) ) + { + matchAll = true; + } + } + if ( matchAll ) + { + this.topics = null; + } + else + { + this.topics = values; + } + } + else if ( topicObj == null && !this.handlerContext.requireTopic ) + { + this.topics = null; + } + else + { + final String reason; + if ( topicObj == null ) + { + reason = "Missing"; + } + else + { + reason = "Neither of type String nor String[] : " + topicObj.getClass().getName(); + } + LogWrapper.getLogger().log( + this.reference, + LogWrapper.LOG_WARNING, + "Invalid EVENT_TOPICS : " + reason + " - Ignoring ServiceReference [" + + this.reference + " | Bundle(" + + this.reference.getBundle() + ")]"); + this.topics = null; + valid = false; + } + // Second check filter (but only if topics is valid) + Filter handlerFilter = null; + if ( valid ) + { + final Object filterObj = reference.getProperty(EventConstants.EVENT_FILTER); + if (filterObj instanceof String) + { + try + { + handlerFilter = this.handlerContext.bundleContext.createFilter(filterObj.toString()); + } + catch (final InvalidSyntaxException e) + { + valid = false; + LogWrapper.getLogger().log( + this.reference, + LogWrapper.LOG_WARNING, + "Invalid EVENT_FILTER - Ignoring ServiceReference [" + + this.reference + " | Bundle(" + + this.reference.getBundle() + ")]", e); + } + } + else if ( filterObj != null ) + { + valid = false; + LogWrapper.getLogger().log( + this.reference, + LogWrapper.LOG_WARNING, + "Invalid EVENT_FILTER - Ignoring ServiceReference [" + + this.reference + " | Bundle(" + + this.reference.getBundle() + ")]"); + } + } + this.filter = handlerFilter; + + // new in 1.3 - deliver + this.asyncOrderedDelivery = true; + Object delivery = reference.getProperty(EventConstants.EVENT_DELIVERY); + if ( delivery instanceof Collection ) + { + @SuppressWarnings("unchecked") + final Collection col = (Collection)delivery; + delivery = col.toArray(new String[col.size()]); + } + if ( delivery instanceof String ) + { + this.asyncOrderedDelivery = !(EventConstants.DELIVERY_ASYNC_UNORDERED.equals(delivery.toString())); + } + else if ( delivery instanceof String[] ) + { + final String[] deliveryArray = (String[])delivery; + boolean foundOrdered = false, foundUnordered = false; + for(int i=0; inull is returned + */ + public String[] getTopics() + { + return this.topics; + } + + /** + * Check if this handler is allowed to receive the event + * - blacklisted + * - check filter + * - check permission + */ + public boolean canDeliver(final Event event) + { + if ( this.blacklisted ) + { + return false; + } + final Bundle bundle = reference.getBundle(); + // is service unregistered? + if (bundle == null) + { + return false; + } + + // filter match + final Filter eventFilter = this.filter; + if ( eventFilter != null && !event.matches(eventFilter) ) + { + return false; + } + + // permission check + final Object p = PermissionsUtil.createSubscribePermission(event.getTopic()); + if (p != null && !bundle.hasPermission(p) ) + { + return false; + } + + return true; + } + + /** + * Should a timeout be used for this handler? + */ + public boolean useTimeout() + { + return this.useTimeout; + } + + /** + * Should async events be delivered in order? + */ + public boolean isAsyncOrderedDelivery() + { + return this.asyncOrderedDelivery; + } + + /** + * Check the timeout configuration for this handler. + */ + private void checkTimeout(final String className) + { + if ( this.handlerContext.ignoreTimeoutMatcher != null ) + { + for(int i=0;iFelix Project Team + */ +public class EventHandlerTracker extends ServiceTracker { + + /** The proxies in this list match all events. */ + private final List matchingAllEvents; + + /** This is a map for exact topic matches. The key is the topic, + * the value is a list of proxies. + */ + private final Map> matchingTopic; + + /** This is a map for wildcard topics. The key is the prefix of the topic, + * the value is a list of proxies + */ + private final Map> matchingPrefixTopic; + + + /** The context for the proxies. */ + private HandlerContext handlerContext; + + public EventHandlerTracker(final BundleContext context) { + super(context, EventHandler.class.getName(), null); + + // we start with empty collections + this.matchingAllEvents = new CopyOnWriteArrayList<>(); + this.matchingTopic = new ConcurrentHashMap<>(); + this.matchingPrefixTopic = new ConcurrentHashMap<>(); + } + + /** + * Update the timeout configuration. + * @param ignoreTimeout + */ + public void update(final String[] ignoreTimeout, final boolean requireTopic) { + final Matchers.Matcher[] ignoreTimeoutMatcher = Matchers.createPackageMatchers(ignoreTimeout); + this.handlerContext = new HandlerContext(this.context, ignoreTimeoutMatcher, requireTopic); + } + + /** + * @see org.osgi.util.tracker.ServiceTracker#addingService(org.osgi.framework.ServiceReference) + */ + @Override + public EventHandlerProxy addingService(final ServiceReference reference) { + final EventHandlerProxy proxy = new EventHandlerProxy(this.handlerContext, reference); + if ( proxy.update() ) { + this.put(proxy); + } + return proxy; + } + + /** + * @see org.osgi.util.tracker.ServiceTracker#modifiedService(org.osgi.framework.ServiceReference, java.lang.Object) + */ + @Override + public void modifiedService(final ServiceReference reference, final EventHandlerProxy proxy) { + this.remove(proxy); + if ( proxy.update() ) { + this.put(proxy); + } + } + + /** + * @see org.osgi.util.tracker.ServiceTracker#removedService(org.osgi.framework.ServiceReference, java.lang.Object) + */ + @Override + public void removedService(final ServiceReference reference, final EventHandlerProxy proxy) { + this.remove(proxy); + proxy.dispose(); + } + + private void updateMap(final Map> proxyListMap, final String key, final EventHandlerProxy proxy, final boolean add) { + List proxies = proxyListMap.get(key); + if (proxies == null) + { + if ( !add ) + { + return; + } + proxies = new CopyOnWriteArrayList<>(); + proxyListMap.put(key, proxies); + } + + if ( add ) + { + proxies.add(proxy); + } + else + { + proxies.remove(proxy); + if ( proxies.size() == 0 ) + { + proxyListMap.remove(key); + } + } + } + + /** + * Check the topics of the event handler and put it into the + * corresponding collections. + */ + private synchronized void put(final EventHandlerProxy proxy) { + final String[] topics = proxy.getTopics(); + if ( topics == null ) + { + this.matchingAllEvents.add(proxy); + } + else + { + for(int i = 0; i < topics.length; i++) { + final String topic = topics[i]; + + if ( topic.endsWith("/*") ) + { + // prefix topic: we remove the /* + final String prefix = topic.substring(0, topic.length() - 2); + this.updateMap(this.matchingPrefixTopic, prefix, proxy, true); + } + else + { + // exact match + this.updateMap(this.matchingTopic, topic, proxy, true); + } + } + } + } + + /** + * Check the topics of the event handler and remove it from the + * corresponding collections. + */ + private synchronized void remove(final EventHandlerProxy proxy) { + final String[] topics = proxy.getTopics(); + if ( topics == null ) + { + this.matchingAllEvents.remove(proxy); + } else { + for(int i = 0; i < topics.length; i++) { + final String topic = topics[i]; + + if ( topic.endsWith("/*") ) + { + // prefix topic: we remove the /* + final String prefix = topic.substring(0, topic.length() - 2); + this.updateMap(this.matchingPrefixTopic, prefix, proxy, false); + } + else + { + // exact match + this.updateMap(this.matchingTopic, topic, proxy, false); + } + } + } + } + + /** + * Get all handlers for this event + * + * @param event The event topic + * @return All handlers for the event + */ + public Collection getHandlers(final Event event) { + final String topic = event.getTopic(); + + final Set handlers = new HashSet<>(); + + // Add all handlers matching everything + this.checkHandlerAndAdd(handlers, this.matchingAllEvents, event); + + // Now check for prefix matches + if ( !this.matchingPrefixTopic.isEmpty() ) + { + int pos = topic.lastIndexOf('/'); + while (pos != -1) + { + final String prefix = topic.substring(0, pos); + this.checkHandlerAndAdd(handlers, this.matchingPrefixTopic.get(prefix), event); + + pos = prefix.lastIndexOf('/'); + } + } + + // Add the handlers for matching topic names + this.checkHandlerAndAdd(handlers, this.matchingTopic.get(topic), event); + + return handlers; + } + + /** + * Checks each handler from the proxy list if it can deliver the event + * If the event can be delivered, the proxy is added to the handlers. + */ + private void checkHandlerAndAdd( final Set handlers, + final List proxies, + final Event event) + { + if ( proxies != null ) + { + for(final EventHandlerProxy p : proxies) + { + if ( p.canDeliver(event) ) + { + handlers.add(p); + } + } + } + } + + /** + * The context object passed to the proxies. + */ + static final class HandlerContext + { + /** The bundle context. */ + public final BundleContext bundleContext; + + /** The matchers for ignore timeout handling. */ + public final Matchers.Matcher[] ignoreTimeoutMatcher; + + /** Is a topic required. */ + public final boolean requireTopic; + + public HandlerContext(final BundleContext bundleContext, + final Matchers.Matcher[] ignoreTimeoutMatcher, + final boolean requireTopic) + { + this.bundleContext = bundleContext; + this.ignoreTimeoutMatcher = ignoreTimeoutMatcher; + this.requireTopic = requireTopic; + } + } +} diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/security/EventAdminSecurityDecorator.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/security/EventAdminSecurityDecorator.java new file mode 100644 index 00000000000..a1c0e19fedf --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/security/EventAdminSecurityDecorator.java @@ -0,0 +1,161 @@ +/* + * 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.eventadmin.impl.security; + +import java.security.Permission; + +import org.osgi.framework.Bundle; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventAdmin; + +/** + * This class is a decorator for an EventAdmin service. It secures the + * service by checking any call from a given bundle (i.e., the caller) to the admins + * post or send methods for the appropriate permissions based on a given permission + * factory. This methods then in turn throw a SecurityException in case + * the given bundle doesn't pass the check or delegate the call to decorated service + * instance, respectively. + * + * @author Felix Project Team + */ +public class EventAdminSecurityDecorator implements EventAdmin +{ + // The bundle used to determine appropriate permissions + private final Bundle m_bundle; + + // The decorated service instance + private final EventAdmin m_admin; + + /** + * The constructor of this decorator. The given bundle and permission factory + * will be used to determine appropriate permissions for any call to + * postEvent() or sendEvent(), respectively. This method then + * in turn throw a SecurityException in case the given bundle doesn't + * pass the check. + * + * @param bundle The calling bundle used to determine appropriate permissions + * @param admin The decorated service instance + */ + public EventAdminSecurityDecorator(final Bundle bundle, final EventAdmin admin) + { + checkNull(bundle, "Bundle"); + checkNull(admin, "Admin"); + + m_bundle = bundle; + + m_admin = admin; + } + + /** + * This method checks whether the given (i.e., calling) bundle has + * appropriate permissions to post an event to the targeted topic. A + * SecurityException is thrown in case it has not. Otherwise, the + * event is posted using this decorator's service instance. + * + * @param event The event that should be posted + * + * @see org.osgi.service.event.EventAdmin#postEvent(org.osgi.service.event.Event) + */ + public void postEvent(final Event event) + { + checkPermission(event.getTopic()); + + m_admin.postEvent(event); + } + + /** + * This method checks whether the given (i.e., calling) bundle has + * appropriate permissions to send an event to the targeted topic. A + * SecurityException is thrown in case it has not. Otherwise, + * the event is posted using this decorator's service instance. + * + * @param event The event that should be send + * + * @see org.osgi.service.event.EventAdmin#sendEvent(org.osgi.service.event.Event) + */ + public void sendEvent(final Event event) + { + checkPermission(event.getTopic()); + + m_admin.sendEvent(event); + } + + /** + * Overrides hashCode() and returns the hash code of the decorated + * service instance. + * + * @return The hash code of the decorated service instance + * + * @see java.lang.Object#hashCode() + * @see org.osgi.service.event.EventAdmin + */ + public int hashCode() + { + return m_admin.hashCode(); + } + + /** + * Overrides equals() and delegates the call to the decorated service + * instance. In case that o is an instance of this class it passes o's service + * instance instead of o. + * + * @param o The object to compare with this decorator's service instance + * + * @see java.lang.Object#equals(java.lang.Object) + * @see org.osgi.service.event.EventAdmin + */ + public boolean equals(final Object o) + { + if(o instanceof EventAdminSecurityDecorator) + { + return m_admin.equals(((EventAdminSecurityDecorator) o).m_admin); + } + + return m_admin.equals(o); + } + + /** + * This is a utility method that will throw a SecurityExcepiton in case + * that the given bundle (i.e, the caller) has not appropriate permissions to + * publish to this topic. This method uses Bundle.hasPermission() and the given + * permission factory to determine this. + */ + private void checkPermission(final String topic) + { + final Permission p = PermissionsUtil.createPublishPermission(topic); + if(p != null && !m_bundle.hasPermission(p)) + { + throw new SecurityException("Bundle[" + m_bundle + + "] has no PUBLISH permission for topic [" + topic + "]"); + } + } + + /* + * This is a utility method that will throw a NullPointerException + * in case that the given object is null. The message will be of the form name + + * may not be null. + */ + private void checkNull(final Object object, final String name) + { + if(null == object) + { + throw new NullPointerException(name + " may not be null"); + } + } +} diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/security/PermissionsUtil.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/security/PermissionsUtil.java new file mode 100644 index 00000000000..3bcccd7267f --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/security/PermissionsUtil.java @@ -0,0 +1,89 @@ +/* + * 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.eventadmin.impl.security; + +import java.security.Permission; + +import org.osgi.service.event.TopicPermission; + +/** + * Utility class for permissions. + * + * @author Felix Project Team + */ +public abstract class PermissionsUtil +{ + /** Marker if permission created failed. */ + private static volatile boolean createPermissions = true; + + /** + * Creates a TopicPermission for the given topic and the type PUBLISH + * + * @param topic The target topic + * + * @return The created permission or null in case the + * permission could not be created. + * + * @see org.osgi.service.event.TopicPermission + */ + public static Permission createPublishPermission(final String topic) + { + if ( createPermissions ) + { + try + { + return new org.osgi.service.event.TopicPermission(topic, TopicPermission.PUBLISH); + } + catch (Throwable t) + { + // This might happen in case security is not supported + createPermissions = false; + } + } + return null; + } + + /** + * Creates a TopicPermission for the given topic and the type SUBSCRIBE + * Note that a + * + * @param topic The target topic + * + * @return The created permission or a null in case the + * permission could not be created. + * + * @see org.osgi.service.event.TopicPermission + */ + public static Permission createSubscribePermission(final String topic) + { + if ( createPermissions ) + { + try + { + return new org.osgi.service.event.TopicPermission(topic, TopicPermission.SUBSCRIBE); + } + catch (Throwable t) + { + // This might happen in case security is not supported + createPermissions = false; + } + } + return null; + } +} diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/security/SecureEventAdminFactory.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/security/SecureEventAdminFactory.java new file mode 100644 index 00000000000..e3a8e46b514 --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/security/SecureEventAdminFactory.java @@ -0,0 +1,105 @@ +/* + * 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.eventadmin.impl.security; + +import org.osgi.framework.Bundle; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.event.EventAdmin; + +/** + * This class is a factory that secures a given EventAdmin service by + * wrapping it with a new instance of an EventAdminSecurityDecorator on + * any call to its getService() method. The decorator will determine the + * appropriate permissions by using the given permission factory and the bundle + * parameter passed to the getService() method. + * + * @author Felix Project Team + */ +public class SecureEventAdminFactory implements ServiceFactory +{ + // The EventAdmin to secure + private final EventAdmin m_admin; + + /** + * The constructor of the factory. The factory will use the given event admin and + * permission factory to create a new EventAdminSecurityDecorator + * on any call to getService(). + * + * @param admin The EventAdmin service to secure. + */ + public SecureEventAdminFactory(final EventAdmin admin) + { + checkNull(admin, "Admin"); + + m_admin = admin; + } + + /** + * Returns a new EventAdminSecurityDecorator initialized with the + * given EventAdmin. That in turn will check any call to post or + * send for the appropriate permissions based on the bundle parameter. + * + * @param bundle The bundle used to determine the permissions of the caller + * @param registration The ServiceRegistration that is not used + * + * @return The given service instance wrapped by an EventAdminSecuriryDecorator + * + * @see org.osgi.framework.ServiceFactory#getService(org.osgi.framework.Bundle, + * org.osgi.framework.ServiceRegistration) + */ + @Override + public EventAdmin getService(final Bundle bundle, + final ServiceRegistration registration) + { + // We don't need to cache this objects since the framework already does this. + return new EventAdminSecurityDecorator(bundle, m_admin); + } + + /** + * This method doesn't do anything at the moment. + * + * @param bundle The bundle object that is not used + * @param registration The ServiceRegistration that is not used + * @param service The service object that is not used + * + * @see org.osgi.framework.ServiceFactory#ungetService(org.osgi.framework.Bundle, + * org.osgi.framework.ServiceRegistration, java.lang.Object) + */ + @Override + public void ungetService(final Bundle bundle, + final ServiceRegistration registration, final EventAdmin service) + { + // We don't need to do anything here since we hand-out a new instance with + // any call to getService hence, it is o.k. to just wait for the next gc. + } + + /* + * This is a utility method that will throw a NullPointerException + * in case that the given object is null. The message will be of the form name + + * may not be null. + */ + private void checkNull(final Object object, final String name) + { + if(null == object) + { + throw new NullPointerException(name + " may not be null"); + } + } +} diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/tasks/AsyncDeliverTasks.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/tasks/AsyncDeliverTasks.java new file mode 100644 index 00000000000..651160c1f4e --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/tasks/AsyncDeliverTasks.java @@ -0,0 +1,193 @@ +/* + * 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.eventadmin.impl.tasks; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.felix.eventadmin.impl.handler.EventHandlerProxy; +import org.osgi.service.event.Event; + +/** + * This class does the actual work of the asynchronous event dispatch. + * + * @author Felix Project Team + */ +public class AsyncDeliverTasks +{ + /** The thread pool to use to spin-off new threads. */ + private final DefaultThreadPool m_pool; + + /** The deliver task for actually delivering the events. This + * is the sync deliver tasks as this has all the code for timeout + * handling etc. + */ + private final SyncDeliverTasks m_deliver_task; + + /** A map of running threads currently delivering async events. */ + private final Map m_running_threads = new ConcurrentHashMap(); + + /** + * The constructor of the class that will use the asynchronous. + * + * @param pool The thread pool used to spin-off new asynchronous event + * dispatching threads in case of timeout or that the asynchronous event + * dispatching thread is used to send a synchronous event + * @param deliverTask The deliver tasks for dispatching the event. + */ + public AsyncDeliverTasks(final DefaultThreadPool pool, final SyncDeliverTasks deliverTask) + { + m_pool = pool; + m_deliver_task = deliverTask; + } + + /** + * This does not block an unrelated thread used to send a synchronous event. + * + * @param tasks The event handler dispatch tasks to execute + * + */ + public void execute(final Collection tasks, final Event event) + { + /* + final Iterator i = tasks.iterator(); + boolean hasOrdered = false; + while ( i.hasNext() ) + { + final EventHandlerProxy task = (EventHandlerProxy)i.next(); + if ( !task.isAsyncOrderedDelivery() ) + { + // do somethimg + } + else + { + hasOrdered = true; + } + + } + if ( hasOrdered ) + {*/ + final TaskInfo info = new TaskInfo(tasks, event); + final Long currentThreadId = Thread.currentThread().getId(); + TaskExecuter executer = m_running_threads.get(currentThreadId); + if ( executer == null ) + { + executer = new TaskExecuter(currentThreadId, m_running_threads); + } + synchronized ( executer ) + { + executer.add(info); + if ( !executer.isActive() ) + { + // reactivate thread + executer.setSyncDeliverTasks(m_deliver_task); + if ( !m_pool.executeTask(executer) ) + { + // scheduling failed: last resort, call directly + executer.run(); + } + m_running_threads.put(currentThreadId, executer); + } + } + //} + } + + private final static class TaskInfo { + public final Collection tasks; + public final Event event; + + public TaskInfo next; + + public TaskInfo(final Collection tasks, final Event event) { + this.tasks = tasks; + this.event = event; + } + } + + private final static class TaskExecuter implements Runnable + { + private volatile TaskInfo first; + private volatile TaskInfo last; + + private volatile SyncDeliverTasks m_deliver_task; + + private final Map m_running_threads; + + private final long threadId; + + public TaskExecuter(final long threadId, final Map runningThreads) { + m_running_threads = runningThreads; + this.threadId = threadId; + } + + public boolean isActive() + { + return this.m_deliver_task != null; + } + + public void setSyncDeliverTasks(final SyncDeliverTasks syncDeliverTasks) + { + this.m_deliver_task = syncDeliverTasks; + } + + @Override + public void run() + { + boolean running; + do + { + TaskInfo info = null; + synchronized ( this ) + { + info = first; + first = info.next; + if ( first == null ) + { + last = null; + } + } + m_deliver_task.execute(info.tasks, info.event, true); + synchronized ( this ) + { + running = first != null; + if ( !running ) + { + this.m_deliver_task = null; + this.m_running_threads.remove(threadId); + } + } + } while ( running ); + } + + public void add(final TaskInfo info) + { + if ( first == null ) + { + first = info; + last = info; + } + else + { + last.next = info; + last = info; + } + } + } +} diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/tasks/BlacklistLatch.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/tasks/BlacklistLatch.java new file mode 100644 index 00000000000..2a844ec8a41 --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/tasks/BlacklistLatch.java @@ -0,0 +1,107 @@ +/* + * 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.eventadmin.impl.tasks; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import org.apache.felix.eventadmin.impl.util.LogWrapper; + +/** + * + * A latch that checks handlers for blacklisting on an interval. + * + */ +public class BlacklistLatch { + + private final Semaphore internalSemaphore; + + private final int count; + + private final long timeout; + + private final List handlerTasks; + + /** + * @param count Number of handlers that must call countdown + * @param timeout Timeout in Milliseconds to check for blacklisting handlers + */ + public BlacklistLatch(final int count, final long timeout) + { + this.handlerTasks = new ArrayList(count); + this.count = count; + this.timeout = timeout; + internalSemaphore = new Semaphore(count); + internalSemaphore.drainPermits(); + } + + /** + * + * Count down the number of handlers blocking event completion. + * + */ + public void countDown() + { + internalSemaphore.release(); + } + + /** + * + * Adds a handler task to the timeout based blackout checking. + * + * @param task + */ + public void addToBlacklistCheck(final HandlerTask task) + { + this.handlerTasks.add(task); + } + + /** + * + * Causes current thread to wait until each handler has called countDown. + * Checks on timeout interval to determine if a handler needs blacklisting. + * + */ + public void awaitAndBlacklistCheck() + { + try + { + while(!internalSemaphore.tryAcquire(this.count, this.timeout, TimeUnit.MILLISECONDS)) + { + final Iterator handlerTaskIt = handlerTasks.iterator(); + while(handlerTaskIt.hasNext()) + { + HandlerTask currentTask = handlerTaskIt.next(); + currentTask.checkForBlacklist(); + } + } + } + catch (final InterruptedException e) + { + LogWrapper.getLogger().log( + LogWrapper.LOG_WARNING, + "Event Task Processing Interrupted. Events may not be received in proper order."); + } + } + + +} diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/tasks/DefaultThreadPool.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/tasks/DefaultThreadPool.java new file mode 100644 index 00000000000..b71f51bdc76 --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/tasks/DefaultThreadPool.java @@ -0,0 +1,147 @@ +/* + * 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.eventadmin.impl.tasks; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.felix.eventadmin.impl.util.LogWrapper; + + +/** + * A thread pool that allows to execute tasks using pooled threads in order + * to ease the thread creation overhead. + * + * @author Felix Project Team + */ +public class DefaultThreadPool +{ + + private ExecutorService executor; + + private final ThreadFactory threadFactory; + + private int oldSize = -1; + + private final AtomicLong threadCounter = new AtomicLong(1); + + /** + * Create a new pool. + */ + public DefaultThreadPool(final int poolSize, final boolean syncThreads) + { + if ( syncThreads ) + { + threadFactory = new ThreadFactory() + { + + @Override + public Thread newThread( final Runnable command ) + { + final Thread thread = new SyncThread( command ); + thread.setPriority( Thread.NORM_PRIORITY ); + thread.setDaemon( true ); + + thread.setName("EventAdminThread #" + threadCounter.getAndIncrement()); + return thread; + } + }; + } + else + { + threadFactory = new ThreadFactory() + { + + @Override + public Thread newThread( final Runnable command ) + { + final Thread thread = new Thread( command ); + thread.setPriority( Thread.NORM_PRIORITY ); + thread.setDaemon( true ); + + thread.setName("EventAdminAsyncThread #" + threadCounter.getAndIncrement()); + return thread; + } + }; + } + configure(poolSize); + } + + /** + * Configure a new pool size. + */ + public synchronized void configure(final int poolSize) + { + if ( oldSize != poolSize) + { + oldSize = poolSize; + final ExecutorService oldService = this.executor; + this.executor = Executors.newFixedThreadPool(poolSize, threadFactory); + if ( oldService != null ) + { + oldService.shutdown(); + } + } + } + + /** + * Returns current pool size. + */ + public int getPoolSize() + { + return oldSize; + } + + /** + * Close the pool i.e, stop pooling threads. Note that subsequently, task will + * still be executed but no pooling is taking place anymore. + */ + public void close() + { + this.executor.shutdownNow(); + } + + /** + * Execute the task in a free thread or create a new one. + * @param task The task to execute + * @return {@code true} if the task execution could be scheduled, {@code false} otherwise. + */ + public boolean executeTask(final Runnable task) + { + try + { + this.executor.submit(task); + } + catch ( final RejectedExecutionException ree ) + { + LogWrapper.getLogger().log( + LogWrapper.LOG_WARNING, + "Exception: " + ree, ree); + return false; + } + catch (final Throwable t) + { + LogWrapper.getLogger().log( + LogWrapper.LOG_WARNING, + "Exception: " + t, t); + } + return true; + } +} diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/tasks/HandlerTask.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/tasks/HandlerTask.java new file mode 100644 index 00000000000..db39165b454 --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/tasks/HandlerTask.java @@ -0,0 +1,132 @@ +/* + * 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.eventadmin.impl.tasks; + +import org.apache.felix.eventadmin.impl.handler.EventHandlerProxy; +import org.osgi.service.event.Event; + +/** + * A task that processes an event handler + * + */ +public class HandlerTask implements Runnable +{ + private final EventHandlerProxy task; + + private final Event event; + + private final long timeout; + + private final BlacklistLatch handlerLatch; + + private volatile long startTime; + + private volatile long endTime; + + /** + * + * + * @param task Proxy to the event handler + * @param event The event to send to the handler + * @param timeout Timeout for handler blacklisting + * @param handlerLatch The latch used to ensure events fire in proper order + */ + public HandlerTask(final EventHandlerProxy task, final Event event, final long timeout, final BlacklistLatch handlerLatch) + { + this.task = task; + this.event = event; + this.timeout = timeout; + this.handlerLatch = handlerLatch; + this.startTime = -1l; + this.endTime = -1l; + } + + /** + * Run Hander Event + */ + @Override + public void run() + { + try + { + startTime = System.currentTimeMillis(); + // execute the task + task.sendEvent(event); + endTime = System.currentTimeMillis(); + checkForBlacklist(); + } + finally + { + handlerLatch.countDown(); + } + } + + public void runWithoutBlacklistTiming() + { + task.sendEvent(event); + handlerLatch.countDown(); + } + + /** + * This method defines if a timeout handling should be used for the + * task. + */ + public boolean useTimeout() + { + // we only check the proxy if a timeout is configured + if ( this.timeout > 0) + { + return task.useTimeout(); + } + return false; + } + + /** + * Check to see if we need to blacklist this handler + * + */ + public void checkForBlacklist() + { + if (useTimeout() && getTaskTime() > this.timeout) + { + task.blackListHandler(); + } + } + + /** + * + * Determine the amount of time spent running this task + * + * @return + */ + private long getTaskTime() + { + if (startTime < 0l) + { + return 0l; + } + else if(endTime < 0l) + { + return System.currentTimeMillis() - startTime; + } + return endTime - startTime; + + } + +} diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/tasks/SyncDeliverTasks.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/tasks/SyncDeliverTasks.java new file mode 100644 index 00000000000..120fc2348ec --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/tasks/SyncDeliverTasks.java @@ -0,0 +1,128 @@ +/* + * 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.eventadmin.impl.tasks; + +import java.util.Collection; +import java.util.Iterator; + +import org.apache.felix.eventadmin.impl.handler.EventHandlerProxy; +import org.osgi.service.event.Event; + + +/** + * This class does the actual work of the synchronous event delivery. + * + * This is the heart of the event delivery. If an event is delivered + * without timeout handling, the event is directly delivered using + * the calling thread. + * If timeout handling is enabled, a new thread is taken from the + * thread pool and this thread is used to deliver the event. + * The calling thread is blocked until either the deliver is finished + * or the timeout occurs. + *

      + * Note that in case of a timeout while a task is disabled the thread + * is released and we spin-off a new thread that resumes the disabled + * task hence, this is the only place were we break the semantics of + * the synchronous delivery. While the only one to notice this is the + * timed-out handler - it is the fault of this handler too (i.e., it + * blocked the dispatch for to long) but since it will not receive + * events anymore it will not notice this semantic difference except + * that it might not see events it already sent before. + * + * + * If during an event delivery a new event should be delivered from + * within the event handler, the timeout handler is stopped for the + * delivery time of the inner event! + * + * @author Felix Project Team + */ +public class SyncDeliverTasks +{ + + /** The thread pool used to spin-off new threads. */ + private final DefaultThreadPool pool; + + private long timeout; + + /** + * Construct a new sync deliver tasks. + * @param pool The thread pool used to spin-off new threads. + */ + public SyncDeliverTasks(final DefaultThreadPool pool, final long timeout) + { + this.pool = pool; + this.update(timeout); + } + + /** + * Update the timeout configuration + */ + public void update(final long timeout) + { + this.timeout = timeout; + } + + /** + * This blocks an unrelated thread used to send a synchronous event until the + * event is send (or a timeout occurs). + * + * @param tasks The event handler dispatch tasks to execute + * + */ + public void execute(final Collection tasks, final Event event, final boolean filterAsyncUnordered) + { + final Thread sleepingThread = Thread.currentThread(); + final SyncThread syncThread = sleepingThread instanceof SyncThread ? (SyncThread)sleepingThread : null; + + final Iterator i = tasks.iterator(); + final BlacklistLatch handlerLatch = new BlacklistLatch(tasks.size(), this.timeout/2); + + while ( i.hasNext() ) + { + final EventHandlerProxy task = i.next(); + HandlerTask handlerTask = new HandlerTask(task, event, this.timeout, handlerLatch); +// if ( !filterAsyncUnordered || task.isAsyncOrderedDelivery() ) +// { + if( !handlerTask.useTimeout() ) + { + handlerTask.runWithoutBlacklistTiming(); + } + else if ( syncThread != null ) + { + // if this is a cascaded event, we directly use this thread + // otherwise we could end up in a starvation + handlerTask.run(); + } + else + { + + handlerLatch.addToBlacklistCheck(handlerTask); + if ( !this.pool.executeTask(handlerTask) ) + { + // scheduling failed: last resort, call directly + handlerTask.run(); + } + } + +// } + } + handlerLatch.awaitAndBlacklistCheck(); + + } +} diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/tasks/SyncThread.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/tasks/SyncThread.java new file mode 100644 index 00000000000..3ce23b665cb --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/tasks/SyncThread.java @@ -0,0 +1,38 @@ +/* + * 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.eventadmin.impl.tasks; + +/** + * This thread class is used for sending the events + * synchronously. + * It acts like a marker. + * + * @author Felix Project Team + */ +public class SyncThread extends Thread +{ + + /** + * Constructor used by the thread pool. + */ + public SyncThread(Runnable target) + { + super(target); + } +} diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/util/LogWrapper.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/util/LogWrapper.java new file mode 100644 index 00000000000..1a94ae73e6f --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/util/LogWrapper.java @@ -0,0 +1,509 @@ +/* + * 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.eventadmin.impl.util; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; + +/** + * This class mimics the standard OSGi LogService interface. An + * instance of this class will be used by the EventAdmin for all logging. The + * implementation of this class sends log messages to standard output, if no + * LogService is present; it uses a log service if one is + * installed in the framework. To do that without creating a hard dependency on the + * package it uses fully qualified class names and registers a listener with the + * framework hence, it does not need access to the LogService class but will + * use it if the listener is informed about an available service. By using a + * DynamicImport-Package dependency we don't need the package but + * use it if present. Additionally, all log methods prefix the log message with + * EventAdmin: . + * + * There is one difference in behavior from the standard OSGi LogService. + * This logger has a {@link #m_logLevel} property which decides what messages + * get logged. + * + * @see org.osgi.service.log.LogService + * + * @author Felix Project Team +**/ +// TODO: At the moment we log a message to all currently available LogServices. +// Maybe, we should only log to the one with the highest ranking instead? +// What is the best practice in this case? +public class LogWrapper +{ + /** + * ERROR LEVEL + * + * @see org.osgi.service.log.LogService#LOG_ERROR + */ + public static final int LOG_ERROR = 1; + + /** + * WARNING LEVEL + * + * @see org.osgi.service.log.LogService#LOG_WARNING + */ + public static final int LOG_WARNING = 2; + + /** + * INFO LEVEL + * + * @see org.osgi.service.log.LogService#LOG_INFO + */ + public static final int LOG_INFO = 3; + + /** + * DEBUG LEVEL + * + * @see org.osgi.service.log.LogService#LOG_DEBUG + */ + public static final int LOG_DEBUG = 4; + + // A set containing the currently available LogServices. Furthermore used as lock + private final Set m_loggerRefs = new HashSet(); + + // Only null while not set and m_loggerRefs is empty hence, only needs to be + // checked in case m_loggerRefs is empty otherwise it will not be null. + private BundleContext m_context; + + private ServiceListener m_logServiceListener; + + /** + * Current log level. Message with log level less than or equal to + * current log level will be logged. + * The default value is {@link #LOG_WARNING} + * + * @see #setLogLevel(int) + */ + private int m_logLevel = LOG_WARNING; + /* + * A thread save variant of the double checked locking singleton. + */ + private static class LogWrapperLoader + { + static final LogWrapper m_singleton = new LogWrapper(); + } + + /** + * Returns the singleton instance of this LogWrapper that can be used to send + * log messages to all currently available LogServices or to standard output, + * respectively. + * + * @return the singleton instance of this LogWrapper. + */ + public static LogWrapper getLogger() + { + return LogWrapperLoader.m_singleton; + } + + /** + * Set the BundleContext of the bundle. This method registers a service + * listener for LogServices with the framework that are subsequently used to + * log messages. + *

      + * If the bundle context is null, the service listener is + * unregistered and all remaining references to LogServices dropped before + * internally clearing the bundle context field. + * + * @param context The context of the bundle. + */ + public static void setContext( final BundleContext context ) + { + LogWrapper logWrapper = LogWrapperLoader.m_singleton; + + // context is removed, unregister and drop references + if ( context == null ) + { + if ( logWrapper.m_logServiceListener != null ) + { + logWrapper.m_context.removeServiceListener( logWrapper.m_logServiceListener ); + logWrapper.m_logServiceListener = null; + } + logWrapper.removeLoggerRefs(); + } + + // set field + logWrapper.setBundleContext( context ); + + // context is set, register and get existing services + if ( context != null ) + { + try + { + ServiceListener listener = new ServiceListener() + { + // Add a newly available LogService reference to the singleton. + @Override + public void serviceChanged( final ServiceEvent event ) + { + if ( ServiceEvent.REGISTERED == event.getType() ) + { + LogWrapperLoader.m_singleton.addLoggerRef( event.getServiceReference() ); + } + // unregistered services are handled in the next log operation. + } + + }; + context.addServiceListener( listener, "(" + Constants.OBJECTCLASS + "=org.osgi.service.log.LogService)" ); + logWrapper.m_logServiceListener = listener; + + // Add all available LogService references to the singleton. + final ServiceReference[] refs = context.getServiceReferences( "org.osgi.service.log.LogService", null ); + + if ( null != refs ) + { + for ( int i = 0; i < refs.length; i++ ) + { + logWrapper.addLoggerRef( refs[i] ); + } + } + } + catch ( InvalidSyntaxException e ) + { + // this never happens + } + } + } + + + /* + * The private singleton constructor. + */ + LogWrapper() + { + // Singleton + } + + /* + * Removes all references to LogServices still kept + */ + void removeLoggerRefs() + { + synchronized ( m_loggerRefs ) + { + m_loggerRefs.clear(); + } + } + + /* + * Add a reference to a newly available LogService + */ + void addLoggerRef( final ServiceReference ref ) + { + synchronized (m_loggerRefs) + { + m_loggerRefs.add(ref); + } + } + + /* + * Set the context of the bundle in the singleton implementation. + */ + private void setBundleContext(final BundleContext context) + { + synchronized(m_loggerRefs) + { + m_context = context; + } + } + + /** + * Log a message with the given log level. Note that this will prefix the message + * with EventAdmin: . + * + * @param level The log level with which to log the msg. + * @param msg The message to log. + */ + public void log(final int level, final String msg) + { + // The method will remove any unregistered service reference as well. + synchronized(m_loggerRefs) + { + if (level > m_logLevel) + { + return; // don't log + } + + final String logMsg = "EventAdmin: " + msg; + + if (!m_loggerRefs.isEmpty()) + { + // There is at least one LogService available hence, we can use the + // class as well. + for (Iterator iter = m_loggerRefs.iterator(); iter.hasNext();) + { + final ServiceReference next = iter.next(); + + org.osgi.service.log.LogService logger = + (org.osgi.service.log.LogService) m_context.getService(next); + + if (null != logger) + { + logger.log(level, logMsg); + + m_context.ungetService(next); + } + else + { + // The context returned null for the reference - it follows + // that the service is unregistered and we can remove it + iter.remove(); + } + } + } + else + { + _log(null, level, logMsg, null); + } + } + } + + /** + * Log a message with the given log level and the associated exception. Note that + * this will prefix the message with EventAdmin: . + * + * @param level The log level with which to log the msg. + * @param msg The message to log. + * @param ex The exception associated with the message. + */ + public void log(final int level, final String msg, final Throwable ex) + { + // The method will remove any unregistered service reference as well. + synchronized(m_loggerRefs) + { + if (level > m_logLevel) + { + return; // don't log + } + + final String logMsg = "EventAdmin: " + msg; + + if (!m_loggerRefs.isEmpty()) + { + // There is at least one LogService available hence, we can use the + // class as well. + for (Iterator iter = m_loggerRefs.iterator(); iter.hasNext();) + { + final ServiceReference next = iter.next(); + + org.osgi.service.log.LogService logger = + (org.osgi.service.log.LogService) m_context.getService(next); + + if (null != logger) + { + logger.log(level, logMsg, ex); + + m_context.ungetService(next); + } + else + { + // The context returned null for the reference - it follows + // that the service is unregistered and we can remove it + iter.remove(); + } + } + } + else + { + _log(null, level, logMsg, ex); + } + } + } + + /** + * Log a message with the given log level together with the associated service + * reference. Note that this will prefix the message with EventAdmin: . + * + * @param sr The reference of the service associated with this message. + * @param level The log level with which to log the msg. + * @param msg The message to log. + */ + public void log(final ServiceReference sr, final int level, final String msg) + { + // The method will remove any unregistered service reference as well. + synchronized(m_loggerRefs) + { + if (level > m_logLevel) + { + return; // don't log + } + + final String logMsg = "EventAdmin: " + msg; + + if (!m_loggerRefs.isEmpty()) + { + // There is at least one LogService available hence, we can use the + // class as well. + for (Iterator iter = m_loggerRefs.iterator(); iter.hasNext();) + { + final ServiceReference next = iter.next(); + + org.osgi.service.log.LogService logger = + (org.osgi.service.log.LogService) m_context.getService(next); + + if (null != logger) + { + logger.log(sr, level, logMsg); + + m_context.ungetService(next); + } + else + { + // The context returned null for the reference - it follows + // that the service is unregistered and we can remove it + iter.remove(); + } + } + } + else + { + _log(sr, level, logMsg, null); + } + } + } + + /** + * Log a message with the given log level, the associated service reference and + * exception. Note that this will prefix the message with EventAdmin: . + * + * @param sr The reference of the service associated with this message. + * @param level The log level with which to log the msg. + * @param msg The message to log. + * @param ex The exception associated with the message. + */ + public void log(final ServiceReference sr, final int level, final String msg, + final Throwable ex) + { + // The method will remove any unregistered service reference as well. + synchronized(m_loggerRefs) + { + if (level > m_logLevel) + { + return; // don't log + } + + final String logMsg = "EventAdmin: " + msg; + + if (!m_loggerRefs.isEmpty()) + { + // There is at least one LogService available hence, we can use the + // class as well. + for (Iterator iter = m_loggerRefs.iterator(); iter.hasNext();) + { + final ServiceReference next = iter.next(); + + org.osgi.service.log.LogService logger = + (org.osgi.service.log.LogService) m_context.getService(next); + + if (null != logger) + { + logger.log(sr, level, logMsg, ex); + + m_context.ungetService(next); + } + else + { + // The context returned null for the reference - it follows + // that the service is unregistered and we can remove it + iter.remove(); + } + } + } + else + { + _log(sr, level, logMsg, ex); + } + } + } + + /* + * Log the message to standard output. This appends the level to the message. + * null values are handled appropriate. + */ + private void _log(final ServiceReference sr, final int level, final String msg, + Throwable ex) + { + String s = (sr == null) ? null : "SvcRef " + sr; + s = (s == null) ? msg : s + " " + msg; + s = (ex == null) ? s : s + " (" + ex + ")"; + + switch (level) + { + case LOG_DEBUG: + System.out.println("DEBUG: " + s); + break; + case LOG_ERROR: + System.out.println("ERROR: " + s); + if (ex != null) + { + if ((ex instanceof BundleException) + && (((BundleException) ex).getNestedException() != null)) + { + ex = ((BundleException) ex).getNestedException(); + } + + ex.printStackTrace(); + } + break; + case LOG_INFO: + System.out.println("INFO: " + s); + break; + case LOG_WARNING: + System.out.println("WARNING: " + s); + break; + default: + System.out.println("UNKNOWN[" + level + "]: " + s); + } + } + + /** + * Change the current log level. Log level decides what messages gets + * logged. Any message with a log level higher than the currently set + * log level is not logged. + * + * @param logLevel new log level + */ + public void setLogLevel(int logLevel) + { + synchronized (m_loggerRefs) + { + m_logLevel = logLevel; + } + } + + /** + * @return current log level. + */ + public int getLogLevel() + { + synchronized (m_loggerRefs) + { + return m_logLevel; + } + } +} diff --git a/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/util/Matchers.java b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/util/Matchers.java new file mode 100644 index 00000000000..b6126f84c5b --- /dev/null +++ b/eventadmin/impl/src/main/java/org/apache/felix/eventadmin/impl/util/Matchers.java @@ -0,0 +1,192 @@ +/* + * 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.eventadmin.impl.util; + +import java.util.ArrayList; +import java.util.List; + +public abstract class Matchers { + + private static final char SEP_TOPIC = '/'; + private static final char SEP_PCK = '.'; + + public static Matcher[] createEventTopicMatchers(final String[] config) + { + final Matcher[] matchers; + if ( config == null || config.length == 0 ) + { + matchers = null; + } + else + { + final List list = new ArrayList<>(); + for(int i=0;i 0 ) + { + if ( value.endsWith(".") ) + { + list.add(new PackageMatcher(value.substring(0, value.length() - 1), SEP_TOPIC)); + } + else if ( value.endsWith("*") ) + { + if ( value.equals("*") ) + { + return new Matcher[] {new MatcherAll()}; + } + list.add(new SubPackageMatcher(value.substring(0, value.length() - 1), SEP_TOPIC)); + } + else + { + list.add(new ClassMatcher(value)); + } + } + } + if ( list.size() > 0 ) + { + matchers = list.toArray(new Matcher[list.size()]); + } + else + { + matchers = null; + } + } + return matchers; + } + + public static Matcher[] createPackageMatchers(final String[] ignoreTimeout) + { + final Matchers.Matcher[] ignoreTimeoutMatcher; + if ( ignoreTimeout == null || ignoreTimeout.length == 0 ) + { + ignoreTimeoutMatcher = null; + } + else + { + ignoreTimeoutMatcher = new Matchers.Matcher[ignoreTimeout.length]; + for(int i=0;i 0 ) + { + if ( value.endsWith(".") ) + { + ignoreTimeoutMatcher[i] = new PackageMatcher(value.substring(0, value.length() - 1), SEP_PCK); + } + else if ( value.endsWith("*") ) + { + ignoreTimeoutMatcher[i] = new SubPackageMatcher(value.substring(0, value.length() - 1), SEP_PCK); + } + else + { + ignoreTimeoutMatcher[i] = new ClassMatcher(value); + } + } + } + } + return ignoreTimeoutMatcher; + } + + /** + * The matcher interface for checking if timeout handling + * is disabled for the handler. + * Matching is based on the class name of the event handler. + */ + public interface Matcher + { + boolean match(String className); + } + + /** Match all. */ + private static final class MatcherAll implements Matcher + { + @Override + public boolean match(final String className) + { + return true; + } + } + + /** Match a package. */ + private static final class PackageMatcher implements Matcher + { + private final String packageName; + + private final char sep; + + + public PackageMatcher(final String name, final char sep) + { + this.packageName = name; + this.sep = sep; + } + @Override + public boolean match(final String className) + { + final int pos = className.lastIndexOf(sep); + return pos > -1 && className.substring(0, pos).equals(packageName); + } + } + + /** Match a package or sub package. */ + private static final class SubPackageMatcher implements Matcher + { + private final String packageName; + + private final char sep; + + public SubPackageMatcher(final String name, final char sep) + { + this.packageName = name + sep; + this.sep = sep; + } + + @Override + public boolean match(final String className) + { + final int pos = className.lastIndexOf(sep); + return pos > -1 && className.substring(0, pos + 1).startsWith(packageName); + } + } + + /** Match a class name. */ + private static final class ClassMatcher implements Matcher + { + private final String m_className; + + public ClassMatcher(final String name) + { + m_className = name; + } + @Override + public boolean match(final String className) + { + return m_className.equals(className); + } + } +} diff --git a/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/impl/util/MatchersTest.java b/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/impl/util/MatchersTest.java new file mode 100644 index 00000000000..72825f0f9b6 --- /dev/null +++ b/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/impl/util/MatchersTest.java @@ -0,0 +1,124 @@ +/* + * 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.eventadmin.impl.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class MatchersTest { + + @Test public void testPackageMatchersNoConfig() + { + assertNull(Matchers.createPackageMatchers(null)); + assertNull(Matchers.createPackageMatchers(new String[0])); + } + + @Test public void testPackageMatchersExact() + { + final Matchers.Matcher[] m = Matchers.createPackageMatchers(new String[] {"org.apache.felix.Foo"}); + assertNotNull(m); + assertEquals(1, m.length); + assertNotNull(m[0]); + + assertTrue(m[0].match("org.apache.felix.Foo")); + assertFalse(m[0].match("org.apache.felix.Bar")); + assertFalse(m[0].match("org.apache.felix.Foo$1")); + assertFalse(m[0].match("org.apache.felix.Foo.Test")); + assertFalse(m[0].match("org.apache.felix")); + } + + @Test public void testPackageMatchersPackage() + { + final Matchers.Matcher[] m = Matchers.createPackageMatchers(new String[] {"org.apache.felix."}); + assertNotNull(m); + assertEquals(1, m.length); + assertNotNull(m[0]); + + assertTrue(m[0].match("org.apache.felix.Foo")); + assertTrue(m[0].match("org.apache.felix.Bar")); + assertTrue(m[0].match("org.apache.felix.Foo$1")); + assertFalse(m[0].match("org.apache.felix.Foo.Test")); + assertFalse(m[0].match("org.apache.felix")); + } + + @Test public void testPackageMatchersSubPackage() + { + final Matchers.Matcher[] m = Matchers.createPackageMatchers(new String[] {"org.apache.felix*"}); + assertNotNull(m); + assertEquals(1, m.length); + assertNotNull(m[0]); + + assertTrue(m[0].match("org.apache.felix.Foo")); + assertTrue(m[0].match("org.apache.felix.Bar")); + assertTrue(m[0].match("org.apache.felix.Foo$1")); + assertTrue(m[0].match("org.apache.felix.Foo.Test")); + assertFalse(m[0].match("org.apache.felix")); + } + + @Test public void testTopicMatchersNoConfig() + { + assertNull(Matchers.createEventTopicMatchers(null)); + assertNull(Matchers.createEventTopicMatchers(new String[0])); + } + + @Test public void testTopicMatchersExact() + { + final Matchers.Matcher[] m = Matchers.createEventTopicMatchers(new String[] {"org/apache/felix/Foo"}); + assertNotNull(m); + assertEquals(1, m.length); + assertNotNull(m[0]); + + assertTrue(m[0].match("org/apache/felix/Foo")); + assertFalse(m[0].match("org/apache/felix/Bar")); + assertFalse(m[0].match("org/apache/felix/Foo$1")); + assertFalse(m[0].match("org/apache/felix/Foo/Test")); + assertFalse(m[0].match("org/apache/felix")); + } + + @Test public void testTopicMatchersPackage() + { + final Matchers.Matcher[] m = Matchers.createEventTopicMatchers(new String[] {"org/apache/felix."}); + assertNotNull(m); + assertEquals(1, m.length); + assertNotNull(m[0]); + + assertTrue(m[0].match("org/apache/felix/Foo")); + assertTrue(m[0].match("org/apache/felix/Bar")); + assertTrue(m[0].match("org/apache/felix/Foo$1")); + assertFalse(m[0].match("org/apache/felix/Foo/Test")); + assertFalse(m[0].match("org/apache/felix")); + } + + @Test public void testTopicMatchersSubPackage() + { + final Matchers.Matcher[] m = Matchers.createEventTopicMatchers(new String[] {"org/apache/felix*"}); + assertNotNull(m); + assertEquals(1, m.length); + assertNotNull(m[0]); + + assertTrue(m[0].match("org/apache/felix/Foo")); + assertTrue(m[0].match("org/apache/felix/Bar")); + assertTrue(m[0].match("org/apache/felix/Foo$1")); + assertTrue(m[0].match("org/apache/felix/Foo/Test")); + assertFalse(m[0].match("org/apache/felix")); + } +} diff --git a/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/ittests/AbstractTest.java b/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/ittests/AbstractTest.java new file mode 100644 index 00000000000..a9523620b0b --- /dev/null +++ b/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/ittests/AbstractTest.java @@ -0,0 +1,364 @@ +/* + * 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.eventadmin.ittests; + +import static org.junit.Assert.assertNotNull; +import static org.ops4j.pax.exam.Constants.START_LEVEL_SYSTEM_BUNDLES; +import static org.ops4j.pax.exam.CoreOptions.bundle; +import static org.ops4j.pax.exam.CoreOptions.mavenBundle; +import static org.ops4j.pax.exam.CoreOptions.options; +import static org.ops4j.pax.exam.CoreOptions.provision; +import static org.ops4j.pax.exam.CoreOptions.systemProperty; +import static org.ops4j.pax.exam.CoreOptions.vmOption; + +import java.io.File; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.inject.Inject; + +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.Configuration; +import org.ops4j.pax.exam.CoreOptions; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.options.AbstractDelegateProvisionOption; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventAdmin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +@RunWith(PaxExam.class) +public abstract class AbstractTest implements Runnable { + + // the name of the system property providing the bundle file to be installed and tested + private static final String BUNDLE_JAR_SYS_PROP = "project.bundle.file"; + + /** The logger. */ + protected final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Inject + protected BundleContext bundleContext; + + /** Event admin. */ + private EventAdmin eventAdmin; + + /** Event admin reference. */ + private ServiceReference eventAdminReference; + + private volatile boolean running = false; + + private volatile int nrEvents; + + private volatile int eventCount = 0; + + private volatile int finishEventCount; + + private volatile String prefix; + + private volatile long startTime; + + private final Queue eventRecievedList = new ConcurrentLinkedQueue(); + + /** Wait lock for syncing. */ + private final Object waitLock = new Object(); + + /** + * Handle an event from the listener. + * @param event + * @param payload + */ + public void handleEvent(final Event event, final Object payload) { + if ( this.running ) { + if ( prefix == null || event.getTopic().startsWith(prefix) ) { + incCount(); + eventRecievedList.offer(event); + } + } + } + + /** + * Send an event + * @param topic + * @param properties + * @param sync + */ + protected void send(String topic, Dictionary properties, int index, boolean sync) { + + if(properties == null) + { + properties = new Hashtable(); + } + properties.put("thread", Thread.currentThread().getId()); + properties.put("index", index); + final Event event = new Event(topic, properties); + if ( sync ) { + getEventAdmin().sendEvent(event); + } else { + getEventAdmin().postEvent(event); + } + } + + private synchronized void incCount() { + eventCount++; + if ( eventCount >= finishEventCount) { + final long duration = this.startTime == -1 ? -1 : System.currentTimeMillis() - this.startTime; + logger.info("Finished tests, received {} events in {}ms", eventCount, duration); + } + } + + private synchronized int getCount() { + return eventCount; + } + + protected void start(final String prefix, final int nrThreads, final int nrEvents, final int finishCount) { + logger.info("Starting eventing test {}", this.getClass().getSimpleName()); + + this.startTime = -1; + this.prefix = prefix; + this.nrEvents = nrEvents; + finishEventCount = finishCount; + eventCount = 0; + logger.info("Preparing test with {} threads and {} events per thread.", nrThreads, nrEvents); + logger.info("Expecting {} events.", finishCount); + this.running = true; + final Thread[] threads = new Thread[nrThreads]; + for(int i = 0; i < threads.length; i++) { + threads[i] = new Thread(this); + } + for(int i = 0; i < threads.length; i++) { + threads[i].start(); + } + final Thread infoT = new Thread(new Runnable() { + @Override + public void run() { + while ( running ) { + logger.info("Received {} events so far.", getCount()); + try { + Thread.sleep(1000 * 5); + } catch (final InterruptedException ignore) { + // ignore + } + if ( getCount() >= finishCount) { + running = false; + synchronized ( waitLock ) { + waitLock.notify(); + } + } + } + } + }); + infoT.start(); + logger.info("Started test with {} threads and {} events.", nrThreads, nrEvents); + this.execute(); + } + + private void waitForFinish() { + while ( this.running ) { + synchronized ( this.waitLock ) { + try { + this.waitLock.wait(); + } catch (final InterruptedException e) { + // ignore + } + } + } + } + + protected abstract void sendEvent(final int index); + + /** + * @see java.lang.Runnable#run() + */ + @Override + public void run() { + try { + Thread.sleep(1000 * 10); + } catch (final InterruptedException ignore) { + // ignore + } + synchronized ( this ) { + if ( this.startTime == -1 ) { + this.startTime = System.currentTimeMillis(); + } + } + logger.info("Started thread"); + int index = 0; + while ( this.running && index < nrEvents ) { + this.sendEvent(index); + + index++; + } + logger.info("Send {} events.", index); + } + + private final List listeners = new ArrayList(); + + public void addListener(final String topic, final Object payload) { + this.listeners.add(new Listener(this.bundleContext, this, topic, payload)); + } + + private void stop() { + for(final Listener l : listeners) { + l.dispose(); + } + listeners.clear(); + } + + /** + * Helper method to get a service of the given type + */ + @SuppressWarnings("unchecked") + protected T getService(Class clazz) { + final ServiceReference ref = bundleContext.getServiceReference(clazz.getName()); + assertNotNull("getService(" + clazz.getName() + ") must find ServiceReference", ref); + final T result = (T)(bundleContext.getService(ref)); + assertNotNull("getService(" + clazz.getName() + ") must find service", result); + return result; + } + + protected EventAdmin getEventAdmin() { + if ( eventAdminReference == null || eventAdminReference.getBundle() == null ) { + eventAdmin = null; + eventAdminReference = bundleContext.getServiceReference(EventAdmin.class.getName()); + } + if ( eventAdmin == null && eventAdminReference != null ) { + eventAdmin = (EventAdmin) bundleContext.getService(eventAdminReference); + } + return eventAdmin; + } + + @Configuration + public static Option[] configuration() { + final String bundleFileName = System.getProperty( BUNDLE_JAR_SYS_PROP ); + final File bundleFile = new File( bundleFileName ); + if ( !bundleFile.canRead() ) { + throw new IllegalArgumentException( "Cannot read from bundle file " + bundleFileName + " specified in the " + + BUNDLE_JAR_SYS_PROP + " system property" ); + } + + return options( + provision( + mavenBundle( "org.ops4j.pax.tinybundles", "tinybundles", "1.0.0" ), + mavenBundle("org.apache.sling", "org.apache.sling.commons.log", "2.1.2"), + mavenBundle("org.apache.felix", "org.apache.felix.configadmin", "1.2.8"), + mavenBundle("org.apache.felix", "org.apache.felix.metatype", "1.0.4"), + CoreOptions.bundle( bundleFile.toURI().toString() ), + mavenBundle("org.ops4j.pax.url", "pax-url-mvn", "1.3.5") + ), + // below is instead of normal Pax Exam junitBundles() to deal + // with build server issue + new DirectURLJUnitBundlesOption(), + systemProperty("pax.exam.invoker").value("junit"), + //vmOption("-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005"), + bundle("link:classpath:META-INF/links/org.ops4j.pax.exam.invoker.junit.link") + ); + } + + /** + * Execute a single test. + * @param test + */ + private void execute() { + this.waitForFinish(); + this.stop(); + logger.info("Finished eventing test {}", this.getClass().getSimpleName()); + Event currentEvent = null; + Map orderVerifyMap = new HashMap(); + while((currentEvent =eventRecievedList.poll()) != null) + { + Integer index = (Integer)currentEvent.getProperty("index"); + Long threadId = (Long)currentEvent.getProperty("thread"); + + if(index != null && threadId != null){ + Integer previousIndex = orderVerifyMap.get(threadId); + if(previousIndex == null) + { + if(index != 0) + { + System.out.println("Event " + index + " recieved first for thread " + threadId); + } + orderVerifyMap.put(threadId, index); + } + else + { + if(previousIndex > index) + { + System.out.println("Events for thread " + threadId + " out of order. Event " + previousIndex + " recieved before " + index); + } + else + { + orderVerifyMap.put(threadId, index); + } + } + } + } + try { + Thread.sleep(15 * 1000); + } catch (final InterruptedException ie) { + // ignore + } + } + + + /** + * Clone of Pax Exam's JunitBundlesOption which uses a direct + * URL to the SpringSource JUnit bundle to avoid some weird + * repository issues on the Apache build server. + */ + private static class DirectURLJUnitBundlesOption + extends AbstractDelegateProvisionOption { + + /** + * Constructor. + */ + public DirectURLJUnitBundlesOption(){ + super( + bundle("http://repository.springsource.com/ivy/bundles/external/org.junit/com.springsource.org.junit/4.9.0/com.springsource.org.junit-4.9.0.jar") + ); + noUpdate(); + startLevel(START_LEVEL_SYSTEM_BUNDLES); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return String.format("DirectURLJUnitBundlesOption{url=%s}", getURL()); + } + + /** + * {@inheritDoc} + */ + @Override + protected DirectURLJUnitBundlesOption itself() { + return this; + } + + } +} \ No newline at end of file diff --git a/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/ittests/Listener.java b/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/ittests/Listener.java new file mode 100644 index 00000000000..b06e6e345ee --- /dev/null +++ b/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/ittests/Listener.java @@ -0,0 +1,62 @@ +/* + * 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.eventadmin.ittests; + +import java.util.Dictionary; +import java.util.Hashtable; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventHandler; + +public class Listener implements EventHandler { + + private final ServiceRegistration reg; + + private final Object payload; + + private final AbstractTest test; + + public Listener(final BundleContext ctx, final AbstractTest test, final String topic, final Object payload) { + this(ctx, test, topic != null ? new String[] {topic} : null, payload); + } + + public Listener(final BundleContext ctx, final AbstractTest test, final String[] topics, final Object payload) { + final Dictionary props = new Hashtable(); + if ( topics != null ) { + props.put("event.topics", topics); + } else { + props.put("event.topics", "*"); + } + this.test = test; + this.reg = ctx.registerService(EventHandler.class.getName(), this, props); + this.payload = payload; + } + + public void dispose() { + this.reg.unregister(); + } + + /** + * @see org.osgi.service.event.EventHandler#handleEvent(org.osgi.service.event.Event) + */ + @Override + public void handleEvent(final Event event) { + this.test.handleEvent(event, this.payload); + } +} diff --git a/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/ittests/StressTestIT.java b/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/ittests/StressTestIT.java new file mode 100644 index 00000000000..1e1fb18a761 --- /dev/null +++ b/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/ittests/StressTestIT.java @@ -0,0 +1,56 @@ +/* + * 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.eventadmin.ittests; + +import org.junit.Test; + + +public class StressTestIT extends AbstractTest { + + private static final String PREFIX = "org/apache/felix/eventing/test"; + private static final int THREADS = 15; + private static final int EVENTS_PER_THREAD = 10000; + + @Override + protected void sendEvent(int index) { + final String postFix = String.valueOf(index % 10); + final String topic = PREFIX + '/' + postFix; + this.send(topic, null, index, false); + } + + @Test + public void testEventing() throws Exception { + this.addListener(PREFIX + "/0", null); + this.addListener(PREFIX + "/1", null); + this.addListener(PREFIX + "/2", null); + this.addListener(PREFIX + "/3", null); + this.addListener(PREFIX + "/4", null); + this.addListener(PREFIX + "/5", null); + this.addListener(PREFIX + "/6", null); + this.addListener(PREFIX + "/7", null); + this.addListener(PREFIX + "/8", null); + this.addListener(PREFIX + "/9", null); + this.addListener(PREFIX + "/*", null); + this.addListener("org/apache/felix/eventing/*", null); + this.addListener("org/apache/felix/*", null); + this.addListener("org/apache/*", null); + this.addListener("org/*", null); + this.addListener("*", null); + + this.start(PREFIX, THREADS, EVENTS_PER_THREAD, THREADS * EVENTS_PER_THREAD * (1 + 6)); + } +} diff --git a/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/perftests/PerformanceTestIT.java b/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/perftests/PerformanceTestIT.java new file mode 100644 index 00000000000..7c19b4c3867 --- /dev/null +++ b/eventadmin/impl/src/test/java/org/apache/felix/eventadmin/perftests/PerformanceTestIT.java @@ -0,0 +1,333 @@ +/* + * 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.eventadmin.perftests; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.Configuration; +import org.ops4j.pax.exam.CoreOptions; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.options.AbstractDelegateProvisionOption; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventAdmin; +import org.osgi.service.event.EventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import static org.ops4j.pax.exam.Constants.START_LEVEL_SYSTEM_BUNDLES; +import static org.ops4j.pax.exam.CoreOptions.*; + +@RunWith(PaxExam.class) +public class PerformanceTestIT { + // the name of the system property providing the bundle file to be installed and tested + private static final String BUNDLE_JAR_SYS_PROP = "project.bundle.file"; + + /** The logger. */ + protected static final Logger logger = LoggerFactory.getLogger(PerformanceTestIT.class); + private static final int RUNS = 5; + public static final int BATCH_SIZE = 500000; + + @Inject + protected BundleContext bundleContext; + + /** Event admin reference. */ + private ServiceReference eventAdminReference; + + /** Event admin. */ + private EventAdmin eventAdmin; + + final AtomicLong counter = new AtomicLong(); + + Collection listeners = new ArrayList(); + + @Configuration + public static Option[] configuration() { + final String bundleFileName = System.getProperty( BUNDLE_JAR_SYS_PROP ); + logger.info("Bundle jar at :"+bundleFileName); + final File bundleFile = new File( bundleFileName ); + if ( !bundleFile.canRead() ) { + throw new IllegalArgumentException( "Cannot read from bundle file " + bundleFileName + " specified in the " + + BUNDLE_JAR_SYS_PROP + " system property" ); + } + return options( + vmOption("-Xms1024m"), +// vmOption("-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005"), + provision( + mavenBundle( "org.ops4j.pax.tinybundles", "tinybundles", "1.0.0" ), + mavenBundle("org.apache.sling", "org.apache.sling.commons.log", "2.1.2"), + mavenBundle("org.apache.felix", "org.apache.felix.configadmin", "1.2.8"), + mavenBundle("org.apache.felix", "org.apache.felix.metatype", "1.0.4"), + CoreOptions.bundle(bundleFile.toURI().toString()), + mavenBundle("org.ops4j.pax.url", "pax-url-mvn", "1.3.5") + ), + // below is instead of normal Pax Exam junitBundles() to deal + // with build server issue + new DirectURLJUnitBundlesOption(), + systemProperty("pax.exam.invoker").value("junit"), + bundle("link:classpath:META-INF/links/org.ops4j.pax.exam.invoker.junit.link") + ); + } + + protected EventAdmin loadEventAdmin() { + if ( eventAdminReference == null || eventAdminReference.getBundle() == null ) { + eventAdmin = null; + eventAdminReference = bundleContext.getServiceReference(EventAdmin.class.getName()); + } + if ( eventAdmin == null && eventAdminReference != null ) { + eventAdmin = (EventAdmin) bundleContext.getService(eventAdminReference); + } + return eventAdmin; + } + + public void addListener(Listener listener, String... topics) { + listener.register(bundleContext,topics); + listeners.add(listener); + } + + private void removeListener(Listener listener) { + listener.unregister(); + } + + + protected void send(String topic, Dictionary properties, boolean sync) { + final Event event = new Event(topic, properties); + if ( sync ) { + eventAdmin.sendEvent(event); + } else { + eventAdmin.postEvent(event); + } + } + + + @Test + public void measureThroughputSend() { + loadEventAdmin(); + addListener(new Listener() { + @Override + public void handleEvent(Event event) { + long calledTimes = counter.incrementAndGet(); + if (calledTimes == BATCH_SIZE ) { + synchronized (counter) { + counter.notify(); + } + } + + } + }, "topic"); + + // Warm-up + Hashtable properties = new Hashtable(); + for (int i= 0;i < BATCH_SIZE;i++) { + properties.put("key",i); + send("topic", properties, false); + } + int average =0; + for (int runs = 0; runs < RUNS;runs ++) { + + final CountDownLatch latch = new CountDownLatch(BATCH_SIZE); + addListener(new Listener() { + @Override + public void handleEvent(Event event) { + latch.countDown(); + } + }, "topic" + runs); + + ArrayBlockingQueue workQueue = new ArrayBlockingQueue(BATCH_SIZE+1); + ThreadPoolExecutor executor = new ThreadPoolExecutor(1, + 1, + 1000, + TimeUnit.MILLISECONDS, workQueue); + + + for (int i = 0; i < BATCH_SIZE; i++) { + final String topicString = "topic"+runs; + final Hashtable localProperties = new Hashtable(); + localProperties.put(topicString,i); + workQueue.add(new Runnable() { + @Override + public void run() { + send(topicString, localProperties, true); + } + }); + } + + long startTime = System.nanoTime(); + executor.prestartAllCoreThreads(); + + try { + latch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + long endTime = System.nanoTime(); + long milliseconds = (endTime - startTime) / 1000000; + logger.info("Post Run "+runs+" Elapsed :" + milliseconds); + average += milliseconds; + } + + logger.info("Send Avg: "+average / RUNS); + } + + @Test + public void measureThroughputPost() { + loadEventAdmin(); + addListener(new Listener() { + @Override + public void handleEvent(Event event) { + long calledTimes = counter.incrementAndGet(); + if (calledTimes == BATCH_SIZE ) { + synchronized (counter) { + counter.notify(); + } + } + + } + }, "topic"); + + // Warm-up + Hashtable properties = new Hashtable(); + for (int i= 0;i < BATCH_SIZE;i++) { + properties.put("key",i); + send("topic", properties, false); + } + int average =0; + for (int runs = 0; runs < RUNS;runs ++) { + + final CountDownLatch latch = new CountDownLatch(BATCH_SIZE); + addListener(new Listener() { + @Override + public void handleEvent(Event event) { + latch.countDown(); + } + }, "topic" + runs); + + ArrayBlockingQueue workQueue = new ArrayBlockingQueue(BATCH_SIZE+1); + ThreadPoolExecutor executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), + Runtime.getRuntime().availableProcessors(), + 1000, + TimeUnit.MILLISECONDS, workQueue); + + + for (int i = 0; i < BATCH_SIZE; i++) { + final String topicString = "topic"+runs; + final Hashtable localProperties = new Hashtable(); + localProperties.put(topicString,i); + workQueue.add(new Runnable() { + @Override + public void run() { + send(topicString, localProperties, false); + } + }); + } + + long startTime = System.nanoTime(); + executor.prestartAllCoreThreads(); + + try { + latch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + long endTime = System.nanoTime(); + long milliseconds = (endTime - startTime) / 1000000; + logger.info("Post Run "+runs+" Elapsed :" + milliseconds); + average += milliseconds; + } + + logger.info("Post Avg: "+average / RUNS); + } + + @After + public void tearDown() { + for (Listener listener : listeners) { + removeListener(listener); + } + } + + + + private static abstract class Listener implements EventHandler { + private ServiceRegistration registration; + + protected Listener() { + } + + public void register(BundleContext bundleContext, String...topics) { + final Dictionary props = new Hashtable(); + if ( topics != null ) { + props.put("event.topics", topics); + } else { + props.put("event.topics", "*"); + } + this.registration = bundleContext.registerService(EventHandler.class.getName(), this, props); + } + + public void unregister() { + registration.unregister(); + } + } + + + private static class DirectURLJUnitBundlesOption + extends AbstractDelegateProvisionOption { + + /** + * Constructor. + */ + public DirectURLJUnitBundlesOption(){ + super( + bundle("http://repository.springsource.com/ivy/bundles/external/org.junit/com.springsource.org.junit/4.9.0/com.springsource.org.junit-4.9.0.jar") + ); + noUpdate(); + startLevel(START_LEVEL_SYSTEM_BUNDLES); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return String.format("DirectURLJUnitBundlesOption{url=%s}", getURL()); + } + + /** + * {@inheritDoc} + */ + @Override + protected DirectURLJUnitBundlesOption itself() { + return this; + } + + } +} diff --git a/eventadmin/pom.xml b/eventadmin/pom.xml index c63c9bceb87..22655c65d7e 100644 --- a/eventadmin/pom.xml +++ b/eventadmin/pom.xml @@ -1,60 +1,45 @@ - + + + 4.0.0 + + pom + org.apache.felix felix - 0.8.0-SNAPSHOT + 1.0.4 + ../pom/pom.xml - 4.0.0 - osgi-bundle - Apache Felix EventAdmin - org.apache.felix.eventadmin - - - ${pom.groupId} - org.osgi.core - ${pom.version} - provided - - - ${pom.groupId} - org.osgi.compendium - ${pom.version} - provided - - - - - - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} - true - - - EventAdmin - Apache Software Foundation - - This bundle provides an implementation of the OSGi R4 EventAdmin service. - - ${pom.artifactId} - - org.apache.felix.eventadmin.impl.Activator - - - org.osgi.framework, org.osgi.service.event; version=1.0.1 - - - org.osgi.service.log - - - org.osgi.service.event.EventHandler, org.osgi.service.log.LogService, org.osgi.service.log.LogReaderService - - - org.osgi.service.event.EventAdmin - - - - - - + + Apache Felix EventAdmin Projects + eventadmin-reactor + 1 + + This is just a convenience reactor build. + + + + impl + bridge.upnp + bridge.configuration + bridge.useradmin + bridge.wireadmin + diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/Activator.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/Activator.java deleted file mode 100644 index 702040455a3..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/Activator.java +++ /dev/null @@ -1,422 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl; - -import org.apache.felix.eventadmin.impl.adapter.BundleEventAdapter; -import org.apache.felix.eventadmin.impl.adapter.FrameworkEventAdapter; -import org.apache.felix.eventadmin.impl.adapter.LogEventAdapter; -import org.apache.felix.eventadmin.impl.adapter.ServiceEventAdapter; -import org.apache.felix.eventadmin.impl.dispatch.CacheThreadPool; -import org.apache.felix.eventadmin.impl.dispatch.DelayScheduler; -import org.apache.felix.eventadmin.impl.dispatch.Scheduler; -import org.apache.felix.eventadmin.impl.dispatch.TaskHandler; -import org.apache.felix.eventadmin.impl.dispatch.ThreadPool; -import org.apache.felix.eventadmin.impl.handler.BlacklistingHandlerTasks; -import org.apache.felix.eventadmin.impl.handler.CacheFilters; -import org.apache.felix.eventadmin.impl.handler.CacheTopicHandlerFilters; -import org.apache.felix.eventadmin.impl.handler.CleanBlackList; -import org.apache.felix.eventadmin.impl.handler.Filters; -import org.apache.felix.eventadmin.impl.handler.HandlerTasks; -import org.apache.felix.eventadmin.impl.handler.TopicHandlerFilters; -import org.apache.felix.eventadmin.impl.security.CacheTopicPermissions; -import org.apache.felix.eventadmin.impl.security.SecureEventAdminFactory; -import org.apache.felix.eventadmin.impl.security.TopicPermissions; -import org.apache.felix.eventadmin.impl.tasks.AsyncDeliverTasks; -import org.apache.felix.eventadmin.impl.tasks.BlockTask; -import org.apache.felix.eventadmin.impl.tasks.DeliverTasks; -import org.apache.felix.eventadmin.impl.tasks.DispatchTask; -import org.apache.felix.eventadmin.impl.tasks.SyncDeliverTasks; -import org.apache.felix.eventadmin.impl.util.LeastRecentlyUsedCacheMap; -import org.apache.felix.eventadmin.impl.util.LogWrapper; -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; -import org.osgi.framework.ServiceRegistration; -import org.osgi.service.event.EventAdmin; -import org.osgi.service.event.TopicPermission; - -/** - * The activator of the EventAdmin bundle. This class registers an implementation of - * the OSGi R4 EventAdmin service (see the Compendium 113) with the - * framework. It features timeout-based blacklisting of event-handlers for both, - * asynchronous and synchronous event-dispatching (as a spec conform optional - * extension). - * - * The service knows about the following properties which are read at bundle startup: - * - *

      - *

      - * org.apache.felix.eventadmin.CacheSize - The size of various internal - * caches. - *

      - * The default value is 30. Increase in case of a large number (more then 100) of - * EventHandler services. A value less then 10 triggers the default value. - *

      - *

      - *

      - * org.apache.felix.eventadmin.ThreadPoolSize - The size of the thread - * pool. - *

      - * The default value is 10. Increase in case of a large amount of synchronous events - * where the EventHandler services in turn send new synchronous events in - * the event dispatching thread or a lot of timeouts are to be expected. A value of - * less then 2 triggers the default value. A value of 2 effectively disables thread - * pooling. - *

      - *

      - *

      - * org.apache.felix.eventadmin.Timeout - The black-listing timeout in - * milliseconds - *

      - * The default value is 5000. Increase or decrease at own discretion. A value of less - * then 100 turns timeouts off. Any other value is the time in milliseconds granted - * to each EventHandler before it gets blacklisted. - *

      - *

      - *

      - * org.apache.felix.eventadmin.RequireTopic - Are EventHandler - * required to be registered with a topic? - *

      - * The default is true. The specification says that EventHandler - * must register with a list of topics they are interested in. Setting this value to - * false will enable that handlers without a topic are receiving all events - * (i.e., they are treated the same as with a topic=*). - *

      - * - * @author Felix Project Team - */ -// TODO: Security is in place but untested due to not being implemented by the -// framework. However, it needs to be revisited once security is implemented. -// Two places are affected by this namely, security/* and handler/* -public class Activator implements BundleActivator -{ - // The thread pool used - this is a member because we need to close it on stop - private volatile ThreadPool m_pool; - - // The asynchronous event queue - this is a member because we need to close it on - // stop - private volatile TaskHandler m_asyncQueue; - - // The synchronous event queue - this is a member because we need to close it on - // stop - private volatile TaskHandler m_syncQueue; - - // The actual implementation of the service - this is a member because we need to - // close it on stop. Note, security is not part of this implementation but is - // added via a decorator in the start method (this is the wrapped object without - // the wrapper). - private volatile EventAdminImpl m_admin; - - // The registration of the security decorator factory (i.e., the service) - private volatile ServiceRegistration m_registration; - - /** - * Called upon starting of the bundle. Constructs and registers the EventAdmin - * service with the framework. Note that the properties of the service are - * requested from the context in this method hence, the bundle has to be - * restarted in order to take changed properties into account. - * - * @param context The bundle context passed by the framework - * - * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) - */ - public void start(final BundleContext context) - { - // init the LogWrapper. Subsequently, the static methods of the LogWrapper - // can be used to log messages similar to the LogService. The effect of a - // call to any of this methods is either a print to standard out (in case - // no LogService is present) or a call to the respective method of - // available LogServices (the reason is that this way the bundle is - // independent of the org.osgi.service.log package) - LogWrapper.setContext(context); - - // The size of various internal caches. At the moment there are 4 - // internal caches affected. Each will cache the determined amount of - // small but frequently used objects (i.e., in case of the default value - // we end-up with a total of 120 small objects being cached). A value of less - // then 10 triggers the default value. - final int cacheSize = getIntProperty("org.apache.felix.eventadmin.CacheSize", - context, 30, 10); - - // The size of the internal thread pool. Note that we must execute - // each synchronous event dispatch that happens in the synchronous event - // dispatching thread in a new thread, hence a small thread pool is o.k. - // A value of less then 2 triggers the default value. A value of 2 - // effectively disables thread pooling. Furthermore, this will be used by - // a lazy thread pool (i.e., new threads are created when needed). Ones the - // the size is reached and no cached thread is available new threads will - // be created. - final int threadPoolSize = getIntProperty( - "org.apache.felix.eventadmin.ThreadPoolSize", context, 10, 2); - - // The timeout in milliseconds - A value of less then 100 turns timeouts off. - // Any other value is the time in milliseconds granted to each EventHandler - // before it gets blacklisted. - final int timeout = getIntProperty("org.apache.felix.eventadmin.Timeout", - context, 5000, Integer.MIN_VALUE); - - // Are EventHandler required to be registered with a topic? - The default is - // true. The specification says that EventHandler must register with a list - // of topics they are interested in. Setting this value to false will enable - // that handlers without a topic are receiving all events - // (i.e., they are treated the same as with a topic=*). - final boolean requireTopic = getBooleanProperty( - "org.apache.felix.eventadmin.RequireTopic", context, true); - - LogWrapper.getLogger().log(LogWrapper.LOG_DEBUG, - "org.apache.felix.eventadmin.CacheSize=" + cacheSize); - - LogWrapper.getLogger().log(LogWrapper.LOG_DEBUG, - "org.apache.felix.eventadmin.ThreadPoolSize=" + threadPoolSize); - - LogWrapper.getLogger().log(LogWrapper.LOG_DEBUG, - "org.apache.felix.eventadmin.Timeout=" + timeout); - - LogWrapper.getLogger().log(LogWrapper.LOG_DEBUG, - "org.apache.felix.eventadmin.RequireTopic=" + requireTopic); - - final TopicPermissions publishPermissions = new CacheTopicPermissions( - new LeastRecentlyUsedCacheMap(cacheSize), TopicPermission.PUBLISH); - - final TopicPermissions subscribePermissions = new CacheTopicPermissions( - new LeastRecentlyUsedCacheMap(cacheSize), TopicPermission.SUBSCRIBE); - - final TopicHandlerFilters topicHandlerFilters = - new CacheTopicHandlerFilters(new LeastRecentlyUsedCacheMap(cacheSize), - requireTopic); - - final Filters filters = new CacheFilters( - new LeastRecentlyUsedCacheMap(cacheSize), context); - - // The handlerTasks object is responsible to determine concerned EventHandler - // for a given event. Additionally, it keeps a list of blacklisted handlers. - // Note that blacklisting is deactivated by selecting a different scheduler - // below (and not in this HandlerTasks object!) - final HandlerTasks handlerTasks = new BlacklistingHandlerTasks(context, - new CleanBlackList(), topicHandlerFilters, filters, - subscribePermissions); - - // Either we need a scheduler that will trigger EventHandler blacklisting - // (timeout >= 100) or a null object (timeout < 100) - final Scheduler scheduler = createScheduler(timeout); - - // Note that this uses a lazy thread pool that will create new threads on - // demand - in case none of its cached threads is free - until threadPoolSize - // is reached. Subsequently, a threadPoolSize of 2 effectively disables - // caching of threads. - m_pool = new CacheThreadPool(threadPoolSize); - - m_asyncQueue = new TaskHandler(); - - m_syncQueue = new TaskHandler(); - - m_admin = new EventAdminImpl(handlerTasks, - createAsyncExecuters(m_asyncQueue, m_syncQueue, scheduler, m_pool), - createSyncExecuters(m_syncQueue, scheduler, m_pool)); - - // register the admin wrapped in a service factory (SecureEventAdminFactory) - // that hands-out the m_admin object wrapped in a decorator that checks - // appropriated permissions of each calling bundle - m_registration = context.registerService(EventAdmin.class.getName(), - new SecureEventAdminFactory(m_admin, publishPermissions), null); - - // Finally, adapt the outside events to our kind of events as per spec - adaptEvents(context, m_admin); - } - - /** - * Called upon stopping the bundle. This will block until all pending events are - * delivered. An IllegalStateException will be thrown on new events starting with - * the begin of this method. However, it might take some time until we settle - * down which is somewhat cumbersome given that the spec asks for return in - * a timely manner. - * - * @param context The bundle context passed by the framework - * - * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) - */ - public void stop(final BundleContext context) - { - // We need to unregister manually - m_registration.unregister(); - - m_admin.stop(); - - // This tasks will be unblocked once the queues are empty - final BlockTask asyncShutdownBlock = new BlockTask(); - - final BlockTask syncShutdownBlock = new BlockTask(); - - // Now close the queues. Note that already added tasks will be delivered - // The given shutdownTask will be executed once the queue is empty - m_asyncQueue.close(asyncShutdownBlock); - - m_syncQueue.close(syncShutdownBlock); - - m_admin = null; - - m_asyncQueue = null; - - m_syncQueue = null; - - m_registration = null; - - final DispatchTask task = m_pool.getTask(Thread.currentThread(), null); - - if(null != task) - { - task.handover(); - } - - asyncShutdownBlock.block(); - - syncShutdownBlock.block(); - - m_pool.close(); - - m_pool = null; - } - - /* - * Create an AsyncDeliverTasks object that is used to dispatch asynchronous - * events. Additionally, the asynchronous dispatch queue is initialized and - * activated (i.e., a thread is started via the given ThreadPool). - */ - private DeliverTasks createAsyncExecuters(final TaskHandler handler, - final TaskHandler handoverHandler, final Scheduler scheduler, - final ThreadPool pool) - { - // init the queue - final AsyncDeliverTasks result = new AsyncDeliverTasks(handler, - handoverHandler, pool); - - // set-up the queue for asynchronous event delivery and activate it - // (i.e., a thread is started via the pool) - result.execute(new DispatchTask(handler, scheduler, result)); - - return result; - } - - /* - * Create a SyncDeliverTasks object that is used to dispatch synchronous events. - * Additionally, the synchronous dispatch queue is initialized and activated - * (i.e., a thread is started via the given ThreadPool). - */ - private DeliverTasks createSyncExecuters(final TaskHandler handler, - final Scheduler scheduler, final ThreadPool pool) - { - // init the queue - final SyncDeliverTasks result = new SyncDeliverTasks(handler, pool); - - // set-up the queue for synchronous event delivery and activate it - // (i.e. a thread is started via the pool) - result.execute(new DispatchTask(handler, scheduler, result)); - - return result; - } - - /* - * Returns either a new DelayScheduler with a delay of timeout or the - * Scheduler.NULL_SCHEDULER in case timeout is < 100 in which case timeout and - * subsequently black-listing is disabled. - */ - private Scheduler createScheduler(final int timeout) - { - if(100 > timeout) - { - return Scheduler.NULL_SCHEDULER; - } - - return new DelayScheduler(timeout); - } - - /* - * Init the adapters in org.apache.felix.eventadmin.impl.adapter - */ - private void adaptEvents(final BundleContext context, final EventAdmin admin) - { - new FrameworkEventAdapter(context, admin); - - new BundleEventAdapter(context, admin); - - new ServiceEventAdapter(context, admin); - - new LogEventAdapter(context, admin); - } - - /* - * Returns either the parsed int from the value of the property if it is set and - * not less then the min value or the default. Additionally, a warning is - * generated in case the value is erroneous (i.e., can not be parsed as an int or - * is less then the min value). - */ - private int getIntProperty(final String key, final BundleContext context, - final int defaultValue, final int min) - { - final String value = context.getProperty(key); - - if(null != value) - { - try { - final int result = Integer.parseInt(value); - - if(result >= min) - { - return result; - } - - LogWrapper.getLogger().log(LogWrapper.LOG_WARNING, - "Value for property: " + key + " is to low - Using default"); - } catch (NumberFormatException e) { - LogWrapper.getLogger().log(LogWrapper.LOG_WARNING, - "Unable to parse property: " + key + " - Using default", e); - } - } - - return defaultValue; - } - - - /* - * Returns true if the value of the property is set and is either 1, true, or yes - * Returns false if the value of the property is set and is either 0, false, or no - * Returns the defaultValue otherwise - */ - private boolean getBooleanProperty(final String key, final BundleContext context, - final boolean defaultValue) - { - String value = context.getProperty(key); - - if(null != value) - { - value = value.trim().toLowerCase(); - - if(0 < value.length() && ("0".equals(value) || "false".equals(value) - || "no".equals(value))) - { - return false; - } - - if(0 < value.length() && ("1".equals(value) || "true".equals(value) - || "yes".equals(value))) - { - return true; - } - } - - return defaultValue; - } -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/EventAdminImpl.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/EventAdminImpl.java deleted file mode 100644 index 0e4a23dd58a..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/EventAdminImpl.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl; - -import org.apache.felix.eventadmin.impl.handler.HandlerTasks; -import org.apache.felix.eventadmin.impl.tasks.DeliverTasks; -import org.apache.felix.eventadmin.impl.tasks.HandlerTask; -import org.osgi.service.event.Event; -import org.osgi.service.event.EventAdmin; - -/** - * This is the actual implementation of the OSGi R4 Event Admin Service (see the - * Compendium 113 for details). The implementation uses a HandlerTasks - * in order to determine applicable EventHandler for a specific event and - * subsequently dispatches the event to the handlers via DeliverTasks. - * To do this, it uses two different DeliverTasks one for asynchronous and - * one for synchronous event delivery depending on whether its post() or - * its send() method is called. Note that the actual work is done in the - * implementations of the DeliverTasks. Additionally, a stop method is - * provided that prevents subsequent events to be delivered. - * - * @author Felix Project Team - */ -public class EventAdminImpl implements EventAdmin -{ - // The factory used to determine applicable EventHandlers - this will be replaced - // by a null object in stop() that subsequently throws an IllegalStateException - private volatile HandlerTasks m_managers; - - // The asynchronous event dispatcher - private final DeliverTasks m_postManager; - - // The synchronous event dispatcher - private final DeliverTasks m_sendManager; - - /** - * The constructor of the EventAdmin implementation. The - * HandlerTasks factory is used to determine applicable - * EventHandler for a given event. Additionally, the two - * DeliverTasks are used to dispatch the event. - * - * @param managers The factory used to determine applicable EventHandler - * @param postManager The asynchronous event dispatcher - * @param sendManager The synchronous event dispatcher - */ - public EventAdminImpl(final HandlerTasks managers, - final DeliverTasks postManager, final DeliverTasks sendManager) - { - checkNull(managers, "Managers"); - checkNull(postManager, "PostManager"); - checkNull(sendManager, "SendManager"); - - m_managers = managers; - - m_postManager = postManager; - - m_sendManager = sendManager; - } - - /** - * Post an asynchronous event. - * - * @param event The event to be posted by this service - * - * @throws IllegalStateException - In case we are stopped - * - * @see org.osgi.service.event.EventAdmin#postEvent(org.osgi.service.event.Event) - */ - public void postEvent(final Event event) - { - handleEvent(m_managers.createHandlerTasks(event), m_postManager); - } - - /** - * Send a synchronous event. - * - * @param event The event to be send by this service - * - * @throws IllegalStateException - In case we are stopped - * - * @see org.osgi.service.event.EventAdmin#sendEvent(org.osgi.service.event.Event) - */ - public void sendEvent(final Event event) - { - handleEvent(m_managers.createHandlerTasks(event), m_sendManager); - } - - /** - * This method can be used to stop the delivery of events. The m_managers is - * replaced with a null object that throws an IllegalStateException on a call - * to createHandlerTasks(). - */ - public void stop() - { - // replace the HandlerTasks with a null object that will throw an - // IllegalStateException on a call to createHandlerTasks - m_managers = new HandlerTasks() - { - /** - * This is a null object and this method will throw an - * IllegalStateException due to the bundle being stopped. - * - * @param event An event that is not used. - * - * @return This method does not return normally - * - * @throws IllegalStateException - This is a null object and this method - * will always throw an IllegalStateException - */ - public HandlerTask[] createHandlerTasks(final Event event) - { - throw new IllegalStateException("The EventAdmin is stopped"); - } - }; - } - - /* - * This is a utility method that uses the given DeliverTasks to create a - * dispatch tasks that subsequently is used to dispatch the given HandlerTasks. - */ - private void handleEvent(final HandlerTask[] managers, - final DeliverTasks manager) - { - if (0 < managers.length) - { - // This might throw an IllegalStateException in case that we are stopped - // and the null object for m_managers was not fast enough established - // This is needed in the adapter/* classes due to them sending - // events whenever they receive an event from their source. - // Service importers that call us regardless of the fact that we are - // stopped deserve an exception anyways - manager.createTask().execute(managers); - } - } - - /* - * This is a utility method that will throw a NullPointerException - * in case that the given object is null. The message will be of the form - * "${name} + may not be null". - */ - private void checkNull(final Object object, final String name) - { - if(null == object) - { - throw new NullPointerException(name + " may not be null"); - } - } -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/adapter/BundleEventAdapter.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/adapter/BundleEventAdapter.java deleted file mode 100644 index c40da41ec77..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/adapter/BundleEventAdapter.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.adapter; - -import java.util.Dictionary; -import java.util.Hashtable; - -import org.osgi.framework.BundleContext; -import org.osgi.framework.BundleEvent; -import org.osgi.framework.BundleListener; -import org.osgi.service.event.Event; -import org.osgi.service.event.EventAdmin; -import org.osgi.service.event.EventConstants; - -/** - * This class registers itself as a listener for bundle events and posts them via - * the EventAdmin as specified in 113.6.4 OSGi R4 compendium. - * - * @author Felix Project Team - */ -public class BundleEventAdapter implements BundleListener -{ - private final EventAdmin m_admin; - - /** - * The constructor of the adapter. This will register the adapter with the given - * context as a BundleListener and subsequently, will post received - * events via the given EventAdmin. - * - * @param context The bundle context with which to register as a listener. - * @param admin The EventAdmin to use for posting events. - */ - public BundleEventAdapter(final BundleContext context, final EventAdmin admin) - { - if(null == admin) - { - throw new NullPointerException("EventAdmin must not be null"); - } - - m_admin = admin; - - context.addBundleListener(this); - } - - /** - * Once a bundle event is received this method assembles and posts an event via - * the EventAdmin as specified in 113.6.4 OSGi R4 compendium. - * - * @param event The event to adapt. - */ - public void bundleChanged(final BundleEvent event) - { - final Dictionary properties = new Hashtable(); - - properties.put(EventConstants.EVENT, event); - - properties.put("bundle.id", new Long(event.getBundle() - .getBundleId())); - - final String symbolicName = event.getBundle().getSymbolicName(); - - if (null != symbolicName) - { - properties.put(EventConstants.BUNDLE_SYMBOLICNAME, - symbolicName); - } - - properties.put("bundle", event.getBundle()); - - final StringBuffer topic = new StringBuffer(BundleEvent.class - .getName().replace('.', '/')).append('/'); - - switch (event.getType()) - { - case BundleEvent.INSTALLED: - topic.append("INSTALLED"); - break; - case BundleEvent.STARTED: - topic.append("STARTED"); - break; - case BundleEvent.STOPPED: - topic.append("STOPPED"); - break; - case BundleEvent.UPDATED: - topic.append("UPDATED"); - break; - case BundleEvent.UNINSTALLED: - topic.append("UNINSTALLED"); - break; - case BundleEvent.RESOLVED: - topic.append("RESOLVED"); - break; - case BundleEvent.UNRESOLVED: - topic.append("UNRESOLVED"); - break; - default: - return; // IGNORE EVENT - } - - try { - m_admin.postEvent(new Event(topic.toString(), properties)); - } catch (IllegalStateException e) { - // This is o.k. - indicates that we are stopped. - } - } -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/adapter/FrameworkEventAdapter.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/adapter/FrameworkEventAdapter.java deleted file mode 100644 index 3e0e9266de7..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/adapter/FrameworkEventAdapter.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.adapter; - -import java.util.Dictionary; -import java.util.Hashtable; - -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleContext; -import org.osgi.framework.FrameworkEvent; -import org.osgi.framework.FrameworkListener; -import org.osgi.service.event.Event; -import org.osgi.service.event.EventAdmin; -import org.osgi.service.event.EventConstants; - -/** - * This class registers itself as a listener for framework events and posts them via - * the EventAdmin as specified in 113.6.3 OSGi R4 compendium. - * - * @author Felix Project Team - */ -public class FrameworkEventAdapter implements FrameworkListener -{ - private final EventAdmin m_admin; - - /** - * The constructor of the adapter. This will register the adapter with the - * given context as a FrameworkListener and subsequently, will - * post received events via the given EventAdmin. - * - * @param context The bundle context with which to register as a listener. - * @param admin The EventAdmin to use for posting events. - */ - public FrameworkEventAdapter(final BundleContext context, final EventAdmin admin) - { - if(null == admin) - { - throw new NullPointerException("EventAdmin must not be null"); - } - - m_admin = admin; - - context.addFrameworkListener(this); - } - - /** - * Once a framework event is received this method assembles and posts an event - * via the EventAdmin as specified in 113.6.3 OSGi R4 compendium. - * - * @param event The event to adapt. - */ - public void frameworkEvent(final FrameworkEvent event) - { - final Dictionary properties = new Hashtable(); - - properties.put(EventConstants.EVENT, event); - - final Bundle bundle = event.getBundle(); - - if (null != bundle) - { - properties.put("bundle.id", new Long(bundle.getBundleId())); - - final String symbolicName = bundle.getSymbolicName(); - - if (null != symbolicName) - { - properties.put(EventConstants.BUNDLE_SYMBOLICNAME, - symbolicName); - } - - properties.put("bundle", bundle); - } - - final Throwable thrown = event.getThrowable(); - - if (null != thrown) - { - properties.put(EventConstants.EXCEPTION_CLASS, - thrown.getClass().getName()); - - final String message = thrown.getMessage(); - - if (null != message) - { - properties.put(EventConstants.EXCEPTION_MESSAGE, - message); - } - - properties.put(EventConstants.EXCEPTION, thrown); - } - - final StringBuffer topic = new StringBuffer( - FrameworkEvent.class.getName().replace('.', '/')) - .append('/'); - - switch (event.getType()) - { - case FrameworkEvent.STARTED: - topic.append("STARTED"); - break; - case FrameworkEvent.ERROR: - topic.append("ERROR"); - break; - case FrameworkEvent.PACKAGES_REFRESHED: - topic.append("PACKAGES_REFRESHED"); - break; - case FrameworkEvent.STARTLEVEL_CHANGED: - topic.append("STARTLEVEL_CHANGED"); - break; - case FrameworkEvent.WARNING: - topic.append("WARNING"); - break; - case FrameworkEvent.INFO: - topic.append("INFO"); - break; - default: - return; // IGNORE EVENT - } - - try { - m_admin.postEvent(new Event(topic.toString(), properties)); - } catch(IllegalStateException e) { - // This is o.k. - indicates that we are stopped. - } - } -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/adapter/ServiceEventAdapter.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/adapter/ServiceEventAdapter.java deleted file mode 100644 index bd3336f304e..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/adapter/ServiceEventAdapter.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.adapter; - -import java.util.Dictionary; -import java.util.Hashtable; - -import org.apache.felix.eventadmin.impl.util.LogWrapper; -import org.osgi.framework.BundleContext; -import org.osgi.framework.Constants; -import org.osgi.framework.ServiceEvent; -import org.osgi.framework.ServiceListener; -import org.osgi.service.event.Event; -import org.osgi.service.event.EventAdmin; -import org.osgi.service.event.EventConstants; - -/** - * This class registers itself as a listener for service events and posts them via - * the EventAdmin as specified in 113.6.5 OSGi R4 compendium. - * - * @author Felix Project Team - */ -public class ServiceEventAdapter implements ServiceListener -{ - private final EventAdmin m_admin; - - /** - * The constructor of the adapter. This will register the adapter with the - * given context as a ServiceListener and subsequently, will - * post received events via the given EventAdmin. - * - * @param context The bundle context with which to register as a listener. - * @param admin The EventAdmin to use for posting events. - */ - public ServiceEventAdapter(final BundleContext context, final EventAdmin admin) - { - m_admin = admin; - - context.addServiceListener(this); - } - - /** - * Once a Service event is received this method assembles and posts an event - * via the EventAdmin as specified in 113.6.5 OSGi R4 compendium. - * - * @param event The event to adapt. - */ - public void serviceChanged(final ServiceEvent event) - { - final Dictionary properties = new Hashtable(); - - properties.put(EventConstants.EVENT, event); - - properties.put(EventConstants.SERVICE, event - .getServiceReference()); - - final Object id = event.getServiceReference().getProperty( - EventConstants.SERVICE_ID); - - if (null != id) - { - try - { - properties.put(EventConstants.SERVICE_ID, new Long(id - .toString())); - } catch (NumberFormatException ne) - { - // LOG and IGNORE - LogWrapper.getLogger().log(event.getServiceReference(), - LogWrapper.LOG_WARNING, "Exception parsing " + - EventConstants.SERVICE_ID + "=" + id, ne); - } - } - - final Object pid = event.getServiceReference().getProperty( - EventConstants.SERVICE_PID); - - if (null != pid) - { - properties.put(EventConstants.SERVICE_PID, pid.toString()); - } - - final Object objectClass = event.getServiceReference() - .getProperty(Constants.OBJECTCLASS); - - if (null != objectClass) - { - if (objectClass instanceof String[]) - { - properties.put(EventConstants.SERVICE_OBJECTCLASS, - objectClass); - } - else - { - properties.put(EventConstants.SERVICE_OBJECTCLASS, - new String[] { objectClass.toString() }); - } - } - - final StringBuffer topic = new StringBuffer(ServiceEvent.class - .getName().replace('.', '/')).append('/'); - - switch (event.getType()) - { - case ServiceEvent.REGISTERED: - topic.append("REGISTERED"); - break; - case ServiceEvent.MODIFIED: - topic.append("MODIFIED"); - break; - case ServiceEvent.UNREGISTERING: - topic.append("UNREGISTERING"); - break; - default: - return; // IGNORE - } - - try { - m_admin.postEvent(new Event(topic.toString(), properties)); - } catch(IllegalStateException e) { - // This is o.k. - indicates that we are stopped. - } - } -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/CacheThreadPool.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/CacheThreadPool.java deleted file mode 100644 index fd6e7793756..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/CacheThreadPool.java +++ /dev/null @@ -1,360 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.dispatch; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.felix.eventadmin.impl.tasks.DeliverTask; -import org.apache.felix.eventadmin.impl.tasks.DispatchTask; - -/** - * An implementation of a thread pool that uses a fixed number of cached threads - * but will spin-off new threads as needed. The underlying assumption is that - * threads that have been created more recently will be available sooner then older - * threads hence, once the pool size is reached older threads will be decoupled from - * the pool and the newly created are added to it. - * - * @author Felix Project Team - */ -// TODO: The least recently used method deployed is rather a hack in this case -// it really should be refactored into a plugable strategy. However, I believe -// it to be the best strategy in this case. -public class CacheThreadPool implements ThreadPool -{ - // The internal lock for this object used instead synchronized(this) - // Note that it is used by the pooled threads created by this pool too. This is - // the reason why it is not private. Don't use it from the outside. - final Object m_lock = new Object(); - - // The pooled threads - private final PooledThread[] m_pool; - - // The least recently used index - private final List m_index; - - // Is this pool closed i.e., do we not pool thread anymore? - private boolean m_closed = false; - - /** - * The constructor of the pool. The given size will be used as the max number of - * pooled threads. - * - * @param size The max number of threads pooled at a given time. - */ - public CacheThreadPool(final int size) - { - synchronized (m_lock) - { - m_pool = new PooledThread[size]; - - // We assume that a list is expanded once it reaches half of its capacity - // and it doesn't harm if the assumption is wrong. - m_index = new ArrayList(size + 1 + (size / 2)); - } - } - - /** - * Executes the task in a thread out of the pool or a new thread if no pooled - * thread is available. In case that the max size is reached the least recently - * used (i.e., the longest executing) thread in the pool is decoupled and a new - * one added to the pool that is used to execute the task. - * - * @param task The task to execute - * @param callback The callback associated with the task - * - * @see org.apache.felix.eventadmin.impl.dispatch.ThreadPool#execute(DispatchTask, DeliverTask) - */ - public void execute(final DispatchTask task, final DeliverTask callback) - { - // Note that we associate a callback with a task via the thread used to - // execute the task. In general, a free slot in the pool (i.e., m_pool[i] is - // null) can be used to set-up a new thread. Also note that we need to - // update the LRU index if we change the pool. - synchronized(m_lock) - { - if(m_closed) - { - // We are closed hence, spin-of a new thread for the new task. - final PooledThread result = new PooledThread(); - - // Set-up the thread and associate the task with the callback. - result.reset(task, callback); - - // release the thread immediately since we don't pool anymore. - result.release(); - - return; - } - - // Search in the pool for a free thread. - for (int i = 0; i < m_pool.length; i++) - { - // o.k. we found a free slot now set-up a new thread for it. - if (null == m_pool[i]) - { - m_pool[i] = new PooledThread(); - - m_pool[i].reset(task, callback); - - m_index.add(new Integer(i)); - - return; - } - else if (m_pool[i].available()) - { - // we found a free thread now set it up. - m_pool[i].reset(task, callback); - - final Integer idx = new Integer(i); - - m_index.remove(idx); - - m_index.add(idx); - - return; - } - } - - // The pool is full and no threads are available hence, spin-off a new - // thread and add it to the pool while decoupling the least recently used - // one. This assumes that older threads are likely to take longer to - // become available again then younger ones. - final int pos = ((Integer) m_index.remove(0)).intValue(); - - m_index.add(new Integer(pos)); - - m_pool[pos].release(); - - m_pool[pos] = new PooledThread(); - - m_pool[pos].reset(task, callback); - } - } - - /** - * Look-up the callback associated with the task that the given thread is - * currently executing or return the default value that may be null. - * - * @param thread The thread that is currently executing the task for which to - * return the callback. In case the thread is not created by an instance of - * this class the default value will be returned. - * @param defaultCallback The value to return in case that the thread was not - * created by an instance of this class. May be null - * @return The callback associated with the given thread or the default value. - * - * @see org.apache.felix.eventadmin.impl.dispatch.ThreadPool#getCallback(Thread, DeliverTask) - */ - public DeliverTask getCallback(final Thread thread, final DeliverTask defaultCallback) - { - synchronized (m_lock) - { - if (thread instanceof PooledThread) - { - return ((PooledThread) thread).getCallback(); - } - - return defaultCallback; - } - } - - /** - * Look-up the task that the given thread is currently executing or return the - * default value that may be null in case that the thread has not been - * created by an instance of this class. - * - * @param thread The thread whose currently executed task should be returned. - * @param defaultTask The default value to be returned in case that the thread - * was not created by this instance or doesn't currently has a task. May be - * null - * @return The task the given thread is currently executing or the defaultTask - * - * @see org.apache.felix.eventadmin.impl.dispatch.ThreadPool#getTask(Thread, DispatchTask) - */ - public DispatchTask getTask(Thread thread, DispatchTask defaultTask) - { - synchronized (m_lock) - { - if (thread instanceof PooledThread) - { - return ((PooledThread) thread).getTask(); - } - - return defaultTask; - } - } - - /** - * Close the pool i.e, stop pooling threads. Note that subsequently, task will - * still be executed but no pooling is taking place anymore. - * - * @see org.apache.felix.eventadmin.impl.dispatch.ThreadPool#close() - */ - public void close() - { - synchronized (m_lock) - { - // We are closed hence, decouple all threads from the pool - for (int i = 0; i < m_pool.length; i++) - { - if (null != m_pool[i]) - { - m_pool[i].release(); - - m_pool[i] = null; - } - } - - m_closed = true; - } - } - - /* - * The threads created by this pool. A PooledThread blocks until it gets a new - * task from the pool or is released. Additionally, it is used to associate - * the task it currently runs with its callback. - */ - private class PooledThread extends Thread - { - // The current task or null if none - private DispatchTask m_runnable = null; - - // The callback associated with the current task - private DeliverTask m_callback = null; - - // Is this thread decoupled from the pool (i.e, may cease to exists once its - // current task is finished)? - private boolean m_released = false; - - /* - * This will set-up the thread as a daemon and start it too. No need to call - * its start method explicitly - */ - PooledThread() - { - setDaemon(true); - - start(); - } - - - /** - * Call next() in a loop until next() returns null indicating that we are - * done (i.e., decoupled from the pool) and may cease to exist. - */ - public void run() - { - for (Runnable next = next(); null != next; next = next()) - { - next.run(); - - synchronized (m_lock) - { - m_runnable = null; - } - } - } - - /* - * Block until a new task is available or we are decoupled from the pool. - * This will return the next task or null if we are decoupled from the pool - */ - private DispatchTask next() - { - synchronized (m_lock) - { - while (null == m_runnable) - { - if (m_released) - { - return null; - } - - try - { - m_lock.wait(); - } catch (InterruptedException e) - { - // Not needed - } - } - - return m_runnable; - } - } - - /* - * Set-up the thread for the next task - */ - void reset(final DispatchTask next, final DeliverTask callback) - { - synchronized (m_lock) - { - m_runnable = next; - m_callback = callback; - m_lock.notifyAll(); - } - } - - /* - * Return the callback associated with the current task - */ - DeliverTask getCallback() - { - synchronized (m_lock) - { - return m_callback; - } - } - - /* - * Return whether this thread is available (i.e., has no task and has not - * been released) or not. - */ - boolean available() - { - synchronized (m_lock) - { - return (null == m_runnable) && (!m_released); - } - } - - /* - * Return the current task or null if none - */ - DispatchTask getTask() - { - synchronized (m_lock) - { - return m_runnable; - } - } - - /* - * Decouple this thread from the pool - */ - void release() - { - synchronized (m_lock) - { - m_released = true; - - m_lock.notifyAll(); - } - } - } -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/DelayScheduler.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/DelayScheduler.java deleted file mode 100644 index 866c207f590..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/DelayScheduler.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.dispatch; - -import java.util.Date; -import java.util.Timer; -import java.util.TimerTask; - -/** - * A simple delay scheduler that schedules tasks based on a fixed delay. Possible - * nice values are subtracted from this delay where appropriate. Note that this - * class uses a java.util.Timer internally that is set to be a daemon hence, - * allows to shutdown the vm regardless but can not be stopped. The spec says that - * a java.util.Timer without a reference to itself should go away eventually - * but no guaranties are given. It follows that once the bundle is stopped all - * references to instances of this class should be released and this in turn will - * allow that the timer thread goes away eventually, but this may take an arbitrary - * amount of time. - * - * @see org.apache.felix.eventadmin.impl.dispatch.Scheduler - * @see java.util.Timer - * - * @author Felix Project Team - */ -public class DelayScheduler implements Scheduler -{ - // The timer used for scheduling. Note that it will not be stopped by but - // by the vm once all references to this instance are gone (at least eventually). - private final Timer m_timer = new Timer(true); - - private final int m_delay; - - /** - * The constructor of the scheduler. The scheduler will use the given delay to - * schedule tasks accordingly. - * - * @param delay The delay in milliseconds before a task is executed - */ - public DelayScheduler(final int delay) - { - m_delay = delay; - } - - /** - * Schedule the task to execute after the given delay. - * - * @param task The task to schedule for execution. - * - * @see org.apache.felix.eventadmin.impl.dispatch.Scheduler#schedule(java.lang.Runnable) - */ - public void schedule(final Runnable task) - { - scheduleTaskWithDelay(task, m_delay); - } - - /** - * Schedule the task to execute after the given delay minus the nice. - * - * @param task The task to schedule for execution after delay minus nice - * @param nice The time to subtract from the delay. - * - * @see org.apache.felix.eventadmin.impl.dispatch.Scheduler#schedule(java.lang.Runnable, int) - */ - public void schedule(final Runnable task, int nice) - { - scheduleTaskWithDelay(task, m_delay - nice); - } - - /* - * This method creates a new TimerTask as a wrapper around the given task - * and calls the m_timer.schedule method with it and the current time plus the - * delay. - */ - private void scheduleTaskWithDelay(final Runnable task, final int delay) - { - m_timer.schedule(new TimerTask() - { - public void run() - { - task.run(); - } - }, new Date(System.currentTimeMillis() + delay)); - } -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/Scheduler.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/Scheduler.java deleted file mode 100644 index 8bf918b40c7..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/Scheduler.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.dispatch; - -/** - * A simple scheduler that accepts a task and schedules its for execution at - * its own discretion (i.e., the behavior of the actual implementor). The only - * possible hint is a nice value that should be subtracted from any fixed scheduling - * interval. Additionally, a null object is provided that can be used to disable - * scheduled execution. - * - * @author Felix Project Team - */ -public interface Scheduler -{ - /** - * This is a null object that can be used in case no scheduling is needed. In - * other words tasks given to this scheduler are never executed. - */ - public final Scheduler NULL_SCHEDULER = new Scheduler(){ - /** - * This is a null object hence, this method does nothing. - * - * @param task A task that will never be run. - */ - public void schedule(final Runnable task) - { - // This is a null object hence we don't do nothing. - } - - /** - * This is a null object hence, this method does nothing. - * - * @param task A task that will never be run. - * @param nice A nice value that will never be used. - */ - public void schedule(final Runnable task, final int nice) - { - // This is a null object hence we don't do nothing. - } - }; - - /** - * Schedule the given task for execution at a later time based on the behavior - * of the actual implementor of this interface. Note that this may mean that - * the task is never executed. - * - * @param task The task to schedule for execution. - */ - public void schedule(final Runnable task); - - /** - * Schedule the given task for execution at a later time based on the behavior - * of the actual implementor of this interface. Note that this may mean that - * the task is never executed. The nice value should be subtracted from any fixed - * scheduling interval. - * - * @param task The task to schedule for execution. - * @param nice A value to subtract from any fixed scheduling interval. - */ - public void schedule(final Runnable task, final int nice); -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/TaskHandler.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/TaskHandler.java deleted file mode 100644 index a472e31c879..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/TaskHandler.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.dispatch; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.felix.eventadmin.impl.tasks.HandlerTask; - -/** - * This class implements the TaskQueue and the TaskProducer - * interface. It makes the tasks added via the queue interface available via the - * producer interface until the queue is closed and the producer returns - * null. - * - * @see org.apache.felix.eventadmin.impl.dispatch.TaskQueue - * @see org.apache.felix.eventadmin.impl.dispatch.TaskProducer - * - * @author Felix Project Team - */ -public class TaskHandler implements TaskQueue, TaskProducer -{ - // The queue that is used as a lock as well - private final List m_queue = new ArrayList(); - - // Are we closed? - private boolean m_closed = false; - - /** - * Append the tasks to this queue in one atomic operation while preserving their - * order. - * - * @param tasks The tasks to append to this queue - * - * @throws IllegalStateException in case that this queue is already closed - * - * @see org.apache.felix.eventadmin.impl.dispatch.TaskQueue#append(HandlerTask[]) - */ - public void append(final HandlerTask[] tasks) - { - synchronized (m_queue) - { - if(m_closed) - { - throw new IllegalArgumentException("Queue is closed"); - } - - for (int i = 0; i < tasks.length; i++) - { - m_queue.add(tasks[i]); - } - - if(!m_queue.isEmpty()) - { - m_queue.notifyAll(); - } - } - } - - /** - * Push the tasks to this queue in one atomic operation while preserving their - * order. - * - * @param tasks The tasks to push to the front of this queue. - * - * @throws IllegalStateException in case that this queue is already closed - * - * @see org.apache.felix.eventadmin.impl.dispatch.TaskQueue#push(HandlerTask[]) - */ - public void push(final HandlerTask[] tasks) - { - synchronized (m_queue) - { - if(m_closed) - { - throw new IllegalArgumentException("Queue is closed"); - } - - for (int i = tasks.length -1; i >= 0; i--) - { - m_queue.add(0, tasks[i]); - } - - if(!m_queue.isEmpty()) - { - m_queue.notifyAll(); - } - } - } - - /** - * Close the queue. The given shutdown task will be executed once the queue is - * empty. - * - * @param shutdownTask The task to execute once the queue is empty - * - * @see org.apache.felix.eventadmin.impl.dispatch.TaskQueue#close() - */ - public void close(final HandlerTask shutdownTask) - { - synchronized(m_queue) - { - m_closed = true; - - m_queue.add(shutdownTask); - - m_queue.notifyAll(); - } - } - - /** - * Block until a new task is ready and is returned or no more tasks will be - * returned. - * - * @return The next task or null if no more tasks will be produced - * - * @see org.apache.felix.eventadmin.impl.dispatch.TaskProducer#next() - */ - public HandlerTask next() - { - synchronized (m_queue) - { - while(!m_closed && m_queue.isEmpty()) - { - try - { - m_queue.wait(); - } catch (InterruptedException e) - { - // Not needed - } - } - - if(!m_queue.isEmpty()) - { - return (HandlerTask) m_queue.remove(0); - } - - return null; - } - } -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/TaskProducer.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/TaskProducer.java deleted file mode 100644 index f1c8acb6e3b..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/TaskProducer.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.dispatch; - -import org.apache.felix.eventadmin.impl.tasks.HandlerTask; - -/** - * Instances of this interface will deliver new tasks as soon as they are available - * while blocking in the next() call until then. Unless there won't be any - * more tasks in which case null is returned. - * - * @author Felix Project Team - */ -public interface TaskProducer -{ - /** - * Block until a new task is ready and is returned or no more tasks will be - * returned. - * - * @return The next task or null if no more tasks will be produced - */ - public HandlerTask next(); -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/TaskQueue.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/TaskQueue.java deleted file mode 100644 index 25a0951f80a..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/TaskQueue.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.dispatch; - -import org.apache.felix.eventadmin.impl.tasks.HandlerTask; - -/** - * This is the interface for a simple queue that allows to append or push arrays - * of tasks to it. The elements of such an array are added atomically (i.e, they - * are in the same order one after the other in the queue) either at the end or the - * front of the queue. Additionally, the queue can be closed. - * - * @author Felix Project Team - */ -public interface TaskQueue -{ - /** - * Append the tasks to this queue in one atomic operation while preserving their - * order. - * - * @param tasks The tasks to append to this queue - * - * @throws IllegalStateException in case that this queue is already closed - */ - public void append(HandlerTask[] tasks); - - /** - * Push the tasks to this queue in one atomic operation while preserving their - * order. - * - * @param tasks The tasks to push to the front of this queue. - * - * @throws IllegalStateException in case that this queue is already closed - */ - public void push(HandlerTask[] tasks); - - /** - * Close the queue. The given callback will be executed once the queue is empty. - * - * @param shutdownTask The task to execute once the queue is empty - */ - public void close(final HandlerTask shutdownTask); -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/ThreadPool.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/ThreadPool.java deleted file mode 100644 index b496fbfda05..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/dispatch/ThreadPool.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.dispatch; - -import org.apache.felix.eventadmin.impl.tasks.DeliverTask; -import org.apache.felix.eventadmin.impl.tasks.DispatchTask; - -/** - * A ThreadPool interface that allows to execute tasks using pooled threads in order - * to ease the thread creation overhead and additionally, to associate a callback - * with the thread that executes the task. Subsequently, the callback for a given - * thread can be asked from instances of this class. Finally, the currently executed - * task of a thread created by this pool can be retrieved as well. The look-up - * methods accept plain thread objects and will return given default values in case - * that the specific threads have not been created by this pool. Note that a closed - * pool should still execute new tasks but stop pooling threads. - * - * @author Felix Project Team - */ -public interface ThreadPool -{ - /** - * Execute the task in a free thread or create a new one. The given callback - * will be associated with the executing thread as long as it is executed. - * - * @param task The task to execute - * @param callback The callback that will be associated with the executing thread - * or null if none. - */ - public void execute(final DispatchTask task, final DeliverTask callback); - - /** - * Look-up the callback associated with the task that the given thread is - * currently executing or return the default value that may be null. - * - * @param thread The thread that is currently executing the task for which to - * return the callback. In case the thread is not created by an instance of - * this class the default value will be returned. - * @param defaultCallback The value to return in case that the thread was not - * created by an instance of this class. May be null - * @return The callback associated with the given thread or the default value. - */ - public DeliverTask getCallback(final Thread thread, - final DeliverTask defaultCallback); - - /** - * Look-up the task that the given thread is currently executing or return the - * default value that may be null in case that the thread has not been - * created by an instance of this class. - * - * @param thread The thread whose currently executed task should be returned. - * @param defaultTask The default value to be returned in case that the thread - * was not created by this instance or doesn't currently has a task. May be - * null - * @return The task the given thread is currently executing or the defaultTask - */ - public DispatchTask getTask(final Thread thread, final DispatchTask defaultTask); - - /** - * Close the pool i.e, stop pooling threads. Note that subsequently, task will - * still be executed but no pooling is taking place anymore. - */ - public void close(); -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/BlackList.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/BlackList.java deleted file mode 100644 index 3a11bf70612..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/BlackList.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.handler; - -import org.osgi.framework.ServiceReference; - -/** - * This interface represents a simple set that allows to add service references - * and lookup whether a given reference is in the list. Note that implementations - * of this interface may do additional service reference life-cycle related - * clean-up actions like removing references that point to unregistered services. - * - * @author Felix Project Team - */ -public interface BlackList -{ - /** - * Add a service to this blacklist. - * - * @param ref The reference of the service that is blacklisted - */ - public void add(final ServiceReference ref); - - /** - * Lookup whether a given service is blacklisted. - * - * @param ref The reference of the service - * - * @return true in case that the service reference has been blacklisted, - * false otherwise. - */ - public boolean contains(final ServiceReference ref); -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/BlacklistingHandlerTasks.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/BlacklistingHandlerTasks.java deleted file mode 100644 index ff0b8636530..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/BlacklistingHandlerTasks.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.handler; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.felix.eventadmin.impl.security.TopicPermissions; -import org.apache.felix.eventadmin.impl.tasks.HandlerTask; -import org.apache.felix.eventadmin.impl.tasks.HandlerTaskImpl; -import org.apache.felix.eventadmin.impl.util.LogWrapper; -import org.osgi.framework.BundleContext; -import org.osgi.framework.InvalidSyntaxException; -import org.osgi.framework.ServiceReference; -import org.osgi.service.event.Event; -import org.osgi.service.event.EventConstants; -import org.osgi.service.event.EventHandler; - -/** - * This class is an implementation of the HandlerTasks interface that does provide - * blacklisting of event handlers. Furthermore, handlers are determined from the - * framework on any call to createHandlerTasks() hence, there is no - * book-keeping of EventHandler services while they come and go but a - * query for each sent event. In order to do this, an ldap-filter is created that - * will match applicable EventHandler references. In order to ease some of - * the overhead pains of this approach some light caching is going on. - * - * @author Felix Project Team - */ -public class BlacklistingHandlerTasks implements HandlerTasks -{ - // The blacklist that holds blacklisted event handler service references - private final BlackList m_blackList; - - // The context of the bundle used to get the actual event handler services - private final BundleContext m_context; - - // Used to create the filters that can determine applicable event handlers for - // a given event - private final TopicHandlerFilters m_topicHandlerFilters; - - // Used to create the filters that are used to determine whether an applicable - // event handler is interested in a particular event - private final Filters m_filters; - - // Used to create and possibly cache topic permissions - private final TopicPermissions m_topicPermissions; - - /** - * The constructor of the factory. - * - * @param context The context of the bundle - * @param blackList The set to use for keeping track of blacklisted references - * @param topicHandlerFilters The factory for topic handler filters - * @param filters The factory for Filter objects - * @param topicPermissions The factory for permission objects of type PUBLISH - */ - public BlacklistingHandlerTasks(final BundleContext context, - final BlackList blackList, - final TopicHandlerFilters topicHandlerFilters, final Filters filters, - final TopicPermissions topicPermissions) - { - checkNull(context, "Context"); - checkNull(blackList, "BlackList"); - checkNull(topicHandlerFilters, "TopicHandlerFilters"); - checkNull(filters, "Filters"); - checkNull(topicPermissions, "TopicPermissions"); - - m_context = context; - - m_blackList = blackList; - - m_topicHandlerFilters = topicHandlerFilters; - - m_filters = filters; - - m_topicPermissions = topicPermissions; - } - - /** - * Create the handler tasks for the event. All matching event handlers are - * determined and delivery tasks for them returned. - * - * @param event The event for which' handlers delivery tasks must be created - * - * @return A delivery task for each handler that matches the given event - * - * @see org.apache.felix.eventadmin.impl.handler.HandlerTasks#createHandlerTasks(org.osgi.service.event.Event) - */ - public HandlerTask[] createHandlerTasks(final Event event) - { - final List result = new ArrayList(); - - ServiceReference[] handlerRefs = new ServiceReference[0]; - - try - { - handlerRefs = m_context.getServiceReferences(EventHandler.class - .getName(), m_topicHandlerFilters.createFilterForTopic(event - .getTopic())); - } catch (InvalidSyntaxException e) - { - LogWrapper.getLogger().log(LogWrapper.LOG_WARNING, - "Invalid EVENT_TOPIC [" + event.getTopic() + "]", e); - } - - if(null == handlerRefs) - { - handlerRefs = new ServiceReference[0]; - } - - for (int i = 0; i < handlerRefs.length; i++) - { - if (!m_blackList.contains(handlerRefs[i]) - && handlerRefs[i].getBundle().hasPermission( - m_topicPermissions.createTopicPermission(event.getTopic()))) - { - try - { - if (event.matches(m_filters.createFilter( - (String) handlerRefs[i] - .getProperty(EventConstants.EVENT_FILTER), - Filters.TRUE_FILTER))) - { - result.add(new HandlerTaskImpl(handlerRefs[i], - event, this)); - } - } catch (InvalidSyntaxException e) - { - LogWrapper.getLogger().log( - handlerRefs[i], - LogWrapper.LOG_WARNING, - "Invalid EVENT_FILTER - Blacklisting ServiceReference [" - + handlerRefs[i] + " | Bundle(" - + handlerRefs[i].getBundle() + ")]", e); - - m_blackList.add(handlerRefs[i]); - } - } - } - - return (HandlerTaskImpl[]) result - .toArray(new HandlerTaskImpl[result.size()]); - } - - /** - * Blacklist the given service reference. This is a private method and only - * public due to its usage in a friend class. - * - * @param handlerRef The service reference to blacklist - */ - public void blackList(final ServiceReference handlerRef) - { - m_blackList.add(handlerRef); - - LogWrapper.getLogger().log( - LogWrapper.LOG_WARNING, - "Blacklisting ServiceReference [" + handlerRef + " | Bundle(" - + handlerRef.getBundle() + ")] due to timeout!"); - } - - /** - * Get the real EventHandler service for the handlerRef from the context in case - * the ref is not blacklisted and the service is not unregistered. The - * NullEventHandler object is returned otherwise. This is a private method and - * only public due to its usage in a friend class. - * - * @param handlerRef The service reference for which to get its service - * @return The service of the reference or a null object if the service is - * unregistered - */ - public EventHandler getEventHandler(final ServiceReference handlerRef) - { - final Object result = (m_blackList.contains(handlerRef)) ? null - : m_context.getService(handlerRef); - - return (EventHandler) ((null != result) ? result : m_nullEventHandler); - } - - /** - * Unget the service reference for the given event handler unless it is the - * NullEventHandler. This is a private method and only public due to - * its usage in a friend class. - * - * @param handler The event handler service to unget - * @param handlerRef The service reference to unget - */ - public void ungetEventHandler(final EventHandler handler, - final ServiceReference handlerRef) - { - if(m_nullEventHandler != handler) - { - // Is the handler not unregistered or blacklisted? - if(!m_blackList.contains(handlerRef) && (null != - handlerRef.getBundle())) - { - m_context.ungetService(handlerRef); - } - } - } - - /* - * This is a null object that is supposed to do nothing. This is used once an - * EventHandler is requested for a service reference that is either stale - * (i.e., unregistered) or blacklisted - */ - private final EventHandler m_nullEventHandler = new EventHandler() - { - /** - * This is a null object that is supposed to do nothing at this point. - * - * @param event an event that is not used - */ - public void handleEvent(final Event event) - { - // This is a null object that is supposed to do nothing at this - // point. This is used once a EventHandler is requested for a - // servicereference that is either stale (i.e., unregistered) or - // blacklisted. - } - }; - - /* - * This is a utility method that will throw a NullPointerException - * in case that the given object is null. The message will be of the form name + - * may not be null. - */ - private void checkNull(final Object object, final String name) - { - if(null == object) - { - throw new NullPointerException(name + " may not be null"); - } - } -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/CacheFilters.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/CacheFilters.java deleted file mode 100644 index 883feb25ddf..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/CacheFilters.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.handler; - -import org.apache.felix.eventadmin.impl.util.CacheMap; -import org.osgi.framework.BundleContext; -import org.osgi.framework.Filter; -import org.osgi.framework.InvalidSyntaxException; - -/** - * This is an implementation of the Filters factory that uses a cache in - * order to speed-up filter creation. - * - * @author Felix Project Team - */ -public class CacheFilters implements Filters -{ - // The cache to use - private final CacheMap m_cache; - - // The context of the bundle used to create the Filter objects - private final BundleContext m_context; - - /** - * The constructor of this factory. The cache is used to speed-up filter - * creation. - * - * @param cache The cache to use - * @param context The context of the bundle used to create the Filter - * objects - */ - public CacheFilters(final CacheMap cache, final BundleContext context) - { - if(null == cache) - { - throw new NullPointerException("Cache may not be null"); - } - - if(null == context) - { - throw new NullPointerException("Context may not be null"); - } - - m_cache = cache; - - m_context = context; - } - - /** - * Create a filter for the given filter string or return the nullFilter in case - * the string is null. - * - * @param filter The filter as a string - * @param nullFilter The default value to return if filter is null - * @return The Filter of the filter string or the nullFilter if the - * filter string was null - * @throws InvalidSyntaxException if BundleContext.createFilter() - * throws an InvalidSyntaxException - * - * @see org.apache.felix.eventadmin.impl.handler.Filters#createFilter(java.lang.String, org.osgi.framework.Filter) - */ - public Filter createFilter(String filter, Filter nullFilter) - throws InvalidSyntaxException - { - Filter result = (Filter) ((null != filter) ? m_cache.get(filter) - : nullFilter); - - if (null == result) - { - result = m_context.createFilter(filter); - - m_cache.add(filter, result); - } - - return result; - } - -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/CacheTopicHandlerFilters.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/CacheTopicHandlerFilters.java deleted file mode 100644 index de30ac22009..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/CacheTopicHandlerFilters.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.handler; - -import org.apache.felix.eventadmin.impl.util.CacheMap; -import org.osgi.service.event.EventConstants; - -/** - * The factory for EventHandler filters based on a certain topic. This - * implementation uses a cache to speed-up filter creation. - * - * @author Felix Project Team - */ -public class CacheTopicHandlerFilters implements TopicHandlerFilters -{ - // The cache - private final CacheMap m_cache; - - private final char[] m_keyChars = EventConstants.EVENT_TOPIC.toCharArray(); - - private final char[] m_filterStart; - - /** - * The constructor of the filter factory. - * - * @param cache The cache to use in order to speed-up filter creation. - * - * @param requireTopic Include handlers that do not provide a topic - */ - public CacheTopicHandlerFilters(final CacheMap cache, final boolean requireTopic) - { - if(null == cache) - { - throw new NullPointerException("Cache may not be null"); - } - - m_cache = cache; - - m_filterStart = ("(|" + - ((requireTopic) ? "" : "(!(" + new String(m_keyChars) + "=*))") + - "(" + new String(m_keyChars) + "=\\*)(" + new String(m_keyChars) + - "=").toCharArray(); - } - - /** - * Create a filter that will match all EventHandler services that match - * the given topic. - * - * @param topic The topic to match - * - * @return A filter that will match all EventHandler services for - * the given topic. - * - * @see org.apache.felix.eventadmin.impl.handler.TopicHandlerFilters#createFilterForTopic(java.lang.String) - */ - public String createFilterForTopic(String topic) - { - // build the ldap-query - as a simple example: - // topic=org/apache/felix/TEST - // result = (|(topic=\*)(topic=org/\*)(topic=org/apache/\*) - // (topic=org/apache/felix/\*)(topic=org/apache/felix/TEST)) - String result = (String) m_cache.get(topic); - - if(null == result) - { - char[] topicChars = topic.toCharArray(); - - final StringBuffer filter = new StringBuffer(topicChars.length - * topicChars.length); - - filter.append(m_filterStart); - - for (int i = 0; i < topicChars.length; i++) - { - if ('/' == topicChars[i]) - { - filter.append('/').append('\\').append('*').append(')'); - - filter.append('(').append(m_keyChars).append('=').append( - topicChars, 0, i + 1); - } - else - { - filter.append(topicChars[i]); - } - } - - filter.append(')').append(')'); - - result = filter.toString(); - - m_cache.add(topic, result); - } - - return result; - } -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/CleanBlackList.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/CleanBlackList.java deleted file mode 100644 index 1c103ad617d..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/CleanBlackList.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.handler; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; - -import org.osgi.framework.ServiceReference; - -/** - * This class implements a BlackList that removes references to unregistered - * services automatically. - * - * @see org.apache.felix.eventadmin.impl.handler.BlackList - * - * @author Felix Project Team - */ -public class CleanBlackList implements BlackList -{ - // This set removes stale (i.e., unregistered) references on any call to contains - private final Set m_blackList = Collections.synchronizedSet(new HashSet() - { - public boolean contains(final Object object) - { - for (Iterator iter = super.iterator(); iter.hasNext();) - { - final ServiceReference ref = (ServiceReference) iter.next(); - - if (null == ref.getBundle()) - { - iter.remove(); - } - } - - return super.contains(object); - } - }); - - /** - * Add a service to this blacklist. - * - * @param ref The reference of the service that is blacklisted - * - * @see org.apache.felix.eventadmin.impl.handler.BlackList#add(org.osgi.framework.ServiceReference) - */ - public void add(final ServiceReference ref) - { - m_blackList.add(ref); - } - - /** - * Lookup whether a given service is blacklisted. - * - * @param ref The reference of the service - * - * @return true in case that the service reference has been blacklisted, - * false otherwise. - * - * @see org.apache.felix.eventadmin.impl.handler.BlackList#contains(org.osgi.framework.ServiceReference) - */ - public boolean contains(final ServiceReference ref) - { - return m_blackList.contains(ref); - } - -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/Filters.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/Filters.java deleted file mode 100644 index fea183fb24c..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/Filters.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.handler; - -import java.util.Dictionary; - -import org.osgi.framework.Filter; -import org.osgi.framework.InvalidSyntaxException; -import org.osgi.framework.ServiceReference; - -/** - * The factory for Filter objects. Additionally, two null filter objects - * are provided that either always return true or false, - * respectively. - * - * @author Felix Project Team - */ -public interface Filters -{ - /** - * A null filter object that matches any given service reference. - */ - public static final Filter TRUE_FILTER = new Filter() - { - - /** - * This is a null object that always returns true. - * - * @param reference An unused service reference - * @return true - */ - public boolean match(final ServiceReference reference) - { - return true; - } - - /** - * This is a null object that always returns true. - * - * @param dictionary An unused dictionary - * @return true - */ - public boolean match(final Dictionary dictionary) - { - return true; - } - - /** - * This is a null object that always returns true. - * - * @param dictionary An unused dictionary. - * @return true - */ - public boolean matchCase(final Dictionary dictionary) - { - return true; - } - }; - - /** - * A null filter object that does not match any given service reference. - */ - public static final Filter FALSE_FILTER = new Filter() - { - - /** - * This is a null object that always returns false. - * - * @param reference An unused reference. - * @return false - */ - public boolean match(final ServiceReference reference) - { - return false; - } - - /** - * This is a null object that always returns false. - * - * @param dictionary An unused dictionary - * @return false - */ - public boolean match(final Dictionary dictionary) - { - return false; - } - - /** - * This is a null object that always returns false. - * - * @param dictionary An unused dictionary. - * @return false - */ - public boolean matchCase(final Dictionary dictionary) - { - return false; - } - }; - - /** - * Create a filter for the given filter string or return the nullFilter in case - * the string is null. - * - * @param filter The filter as a string - * @param nullFilter The default value to return if filter is null - * @return The Filter of the filter string or the nullFilter if the - * filter string was null - * @throws InvalidSyntaxException if BundleContext.createFilter() - * throws an InvalidSyntaxException - */ - public Filter createFilter(final String filter, final Filter nullFilter) - throws InvalidSyntaxException; -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/HandlerTasks.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/HandlerTasks.java deleted file mode 100644 index f9e2fd24491..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/HandlerTasks.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.handler; - -import org.apache.felix.eventadmin.impl.tasks.HandlerTask; -import org.osgi.service.event.Event; - -/** - * The factory for event handler tasks. Implementations of this interface can be - * used to create tasks that handle the delivery of events to event handlers. - * - * @author Felix Project Team - */ -public interface HandlerTasks -{ - /** - * Create the handler tasks for the event. All matching event handlers must - * be determined and delivery tasks for them returned. - * - * @param event The event for which' handlers delivery tasks must be created - * - * @return A delivery task for each handler that matches the given event - */ - public HandlerTask[] createHandlerTasks(final Event event); -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/TopicHandlerFilters.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/TopicHandlerFilters.java deleted file mode 100644 index 01771857fdb..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/handler/TopicHandlerFilters.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.handler; - -/** - * The factory for EventHandler filters based on a certain topic. - * - * @author Felix Project Team - */ -public interface TopicHandlerFilters -{ - /** - * Create a filter that will match all EventHandler services that match - * the given topic. - * - * @param topic The topic to match - * - * @return A filter that will match all EventHandler services for - * the given topic. - */ - public String createFilterForTopic(final String topic); -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/security/CacheTopicPermissions.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/security/CacheTopicPermissions.java deleted file mode 100644 index 8e6620db731..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/security/CacheTopicPermissions.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.security; - -import org.apache.felix.eventadmin.impl.util.CacheMap; - -/** - * An implementation of the TopicPermissions factory that uses a given - * cache in order to speed-up topic permission creation. Note that a - * java.lang.Object is returned in case creating a new TopicPermission - * fails. This assumes that Bundle.hasPermission is used in order to evaluate the - * created Permission which in turn will return true if security is not supported - * by the framework. Otherwise, it will return false due to receiving something that - * is not a subclass of java.lang.SecurityPermission hence, this combination - * ensures that access is granted in case a topic permission could not be created due - * to missing security support by the framework. - * - * @see org.apache.felix.eventadmin.impl.security.TopicPermissions - * - * @author Felix Project Team - */ -public class CacheTopicPermissions implements TopicPermissions -{ - // The cache used - private final CacheMap m_cache; - - // The type of the permissions created - private final String m_type; - - /** - * The constructor of this permission factory. The given cache will be used to - * speed-up permission creation and the created permissions will be of the given - * type (i.e., PUBLISH or SUBSCRIBE). - * - * @param cache The cache to be used - * @param type The type that created permissions will be of (i.e, PUBLISH or - * SUBSCRIBE) - * - * @see org.apache.felix.eventadmin.impl.security.TopicPermissions - * @see org.osgi.service.event.TopicPermission#PUBLISH - * @see org.osgi.service.event.TopicPermission#SUBSCRIBE - */ - public CacheTopicPermissions(final CacheMap cache, final String type) - { - checkNull(cache, "CacheMap"); - checkNull(type, "Type"); - - if(!org.osgi.service.event.TopicPermission.PUBLISH.equals(type) && - !org.osgi.service.event.TopicPermission.SUBSCRIBE.equals(type)) - { - throw new IllegalArgumentException( - "Type must be either PUBLISH or SUBSCRIBE"); - } - - m_cache = cache; - - m_type = type; - } - - /** - * Returns the type of the permissions created by this factory. - * - * @return The type of the permissions created by this factory - * - * @see org.apache.felix.eventadmin.impl.security.TopicPermissions#getType() - * @see org.osgi.service.event.TopicPermission#PUBLISH - * @see org.osgi.service.event.TopicPermission#SUBSCRIBE - */ - public String getType() - { - return m_type; - } - - /** - * Creates a TopicPermission for the given topic and the type of this - * factory (i.e., PUBLISH or SUBSCRIBE). Note that a - * java.lang.Object is returned in case creating a new TopicPermission - * fails. This assumes that Bundle.hasPermission is used in order to evaluate the - * created Permission which in turn will return true if security is not supported - * by the framework. Otherwise, it will return false due to receiving something - * that is not a subclass of java.lang.SecurityPermission hence, this - * combination ensures that access is granted in case a topic permission could - * not be created due to missing security support by the framework. - * - * @param topic The target topic - * - * @return The created permission or a java.lang.Object in case the - * permission could not be created. - * - * @see org.apache.felix.eventadmin.impl.security.TopicPermissions#createTopicPermission(String) - * @see org.osgi.service.event.TopicPermission - */ - public Object createTopicPermission(final String topic) - { - Object result = m_cache.get(topic); - - if(null == result) - { - try - { - result = new org.osgi.service.event.TopicPermission(topic, m_type); - } catch (Throwable t) - { - // This might happen in case security is not supported - // Bundle.hasPermission will return true in this case - // hence topicPermission = new Object() is o.k. - - result = new Object(); - } - - m_cache.add(topic, result); - } - - return result; - } - - /* - * This is a utility method that will throw a NullPointerException - * in case that the given object is null. The message will be of the form name + - * may not be null. - */ - private void checkNull(final Object object, final String name) - { - if(null == object) - { - throw new NullPointerException(name + " may not be null"); - } - } -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/security/EventAdminSecurityDecorator.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/security/EventAdminSecurityDecorator.java deleted file mode 100644 index 543f1c81497..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/security/EventAdminSecurityDecorator.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.security; - -import org.osgi.framework.Bundle; -import org.osgi.service.event.Event; -import org.osgi.service.event.EventAdmin; - -/** - * This class is a decorator for an EventAdmin service. It secures the - * service by checking any call from a given bundle (i.e., the caller) to the admins - * post or send methods for the appropriate permissions based on a given permission - * factory. This methods then in turn throw a SecurityException in case - * the given bundle doesn't pass the check or delegate the call to decorated service - * instance, respectively. - * - * @author Felix Project Team - */ -public class EventAdminSecurityDecorator implements EventAdmin -{ - // The bundle used to determine appropriate permissions - private final Bundle m_bundle; - - // The decorated service instance - private final EventAdmin m_admin; - - // The permission factory - private final TopicPermissions m_topicPermissions; - - /** - * The constructor of this decorator. The given bundle and permission factory - * will be used to determine appropriate permissions for any call to - * postEvent() or sendEvent(), respectively. This method then - * in turn throw a SecurityException in case the given bundle doesn't - * pass the check. - * - * @param bundle The calling bundle used to determine appropriate permissions - * @param admin The decorated service instance - * @param topicPermissions The permission factory - */ - public EventAdminSecurityDecorator(final Bundle bundle, final EventAdmin admin, - final TopicPermissions topicPermissions) - { - checkNull(bundle, "Bundle"); - checkNull(admin, "Admin"); - checkNull(topicPermissions, "TopicPermissions"); - - m_bundle = bundle; - - m_admin = admin; - - m_topicPermissions = topicPermissions; - } - - /** - * This method checks whether the given (i.e., calling) bundle has - * appropriate permissions to post an event to the targeted topic. A - * SecurityException is thrown in case it has not. Otherwise, the - * event is posted using this decorator's service instance. - * - * @param event The event that should be posted - * - * @see org.osgi.service.event.EventAdmin#postEvent(org.osgi.service.event.Event) - */ - public void postEvent(final Event event) - { - checkPermission(event.getTopic()); - - m_admin.postEvent(event); - } - - /** - * This method checks whether the given (i.e., calling) bundle has - * appropriate permissions to send an event to the targeted topic. A - * SecurityException is thrown in case it has not. Otherwise, - * the event is posted using this decorator's service instance. - * - * @param event The event that should be send - * - * @see org.osgi.service.event.EventAdmin#sendEvent(org.osgi.service.event.Event) - */ - public void sendEvent(final Event event) - { - checkPermission(event.getTopic()); - - m_admin.sendEvent(event); - } - - /** - * Overrides hashCode() and returns the hash code of the decorated - * service instance. - * - * @return The hash code of the decorated service instance - * - * @see java.lang.Object#hashCode() - * @see org.osgi.service.event.EventAdmin - */ - public int hashCode() - { - return m_admin.hashCode(); - } - - /** - * Overrides equals() and delegates the call to the decorated service - * instance. In case that o is an instance of this class it passes o's service - * instance instead of o. - * - * @param o The object to compare with this decorator's service instance - * - * @see java.lang.Object#equals(java.lang.Object) - * @see org.osgi.service.event.EventAdmin - */ - public boolean equals(final Object o) - { - if(o instanceof EventAdminSecurityDecorator) - { - return m_admin.equals(((EventAdminSecurityDecorator) o).m_admin); - } - - return m_admin.equals(o); - } - - /* - * This is a utility method that will throw a SecurityExcepiton in case - * that the given bundle (i.e, the caller) has not appropriate permissions to - * publish to this topic. This method uses Bundle.hasPermission() and the given - * permission factory to determine this. - */ - private void checkPermission(final String topic) - { - if(!m_bundle.hasPermission(m_topicPermissions.createTopicPermission(topic))) - { - throw new SecurityException("Bundle[" + m_bundle + - "] has no PUBLISH permission for topic [" + topic + "]"); - } - } - - /* - * This is a utility method that will throw a NullPointerException - * in case that the given object is null. The message will be of the form name + - * may not be null. - */ - private void checkNull(final Object object, final String name) - { - if(null == object) - { - throw new NullPointerException(name + " may not be null"); - } - } -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/security/SecureEventAdminFactory.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/security/SecureEventAdminFactory.java deleted file mode 100644 index c8c50e2793b..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/security/SecureEventAdminFactory.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.security; - -import org.osgi.framework.Bundle; -import org.osgi.framework.ServiceFactory; -import org.osgi.framework.ServiceRegistration; -import org.osgi.service.event.EventAdmin; - -/** - * This class is a factory that secures a given EventAdmin service by - * wrapping it with a new instance of an EventAdminSecurityDecorator on - * any call to its getService() method. The decorator will determine the - * appropriate permissions by using the given permission factory and the bundle - * parameter passed to the getService() method. - * - * @author Felix Project Team - */ -public class SecureEventAdminFactory implements ServiceFactory -{ - // The EventAdmin to secure - private EventAdmin m_admin; - - // The permission factory - private final TopicPermissions m_topicPermissions; - - /** - * The constructor of the factory. The factory will use the given event admin and - * permission factory to create a new EventAdminSecurityDecorator - * on any call to getService(). - * - * @param admin The EventAdmin service to secure. - * @param topicPermissions The permission factory to use for permission lookup. - */ - public SecureEventAdminFactory(final EventAdmin admin, final TopicPermissions - topicPermissions) - { - checkNull(admin, "Admin"); - checkNull(topicPermissions, "TopicPermissions"); - - m_admin = admin; - - m_topicPermissions = topicPermissions; - } - - /** - * Returns a new EventAdminSecurityDecorator initialized with the - * given EventAdmin. That in turn will check any call to post or - * send for the appropriate permissions based on the bundle parameter. - * - * @param bundle The bundle used to determine the permissions of the caller - * @param registration The ServiceRegistration that is not used - * - * @return The given service instance wrapped by an EventAdminSecuriryDecorator - * - * @see org.osgi.framework.ServiceFactory#getService(org.osgi.framework.Bundle, - * org.osgi.framework.ServiceRegistration) - */ - public Object getService(final Bundle bundle, - final ServiceRegistration registration) - { - // We don't need to cache this objects since the framework already does this. - return new EventAdminSecurityDecorator(bundle, m_admin, m_topicPermissions); - } - - /** - * This method doesn't do anything at the moment. - * - * @param bundle The bundle object that is not used - * @param registration The ServiceRegistration that is not used - * @param service The service object that is not used - * - * @see org.osgi.framework.ServiceFactory#ungetService(org.osgi.framework.Bundle, - * org.osgi.framework.ServiceRegistration, java.lang.Object) - */ - public void ungetService(final Bundle bundle, - final ServiceRegistration registration, final Object service) - { - // We don't need to do anything here since we hand-out a new instance with - // any call to getService hence, it is o.k. to just wait for the next gc. - } - - /* - * This is a utility method that will throw a NullPointerException - * in case that the given object is null. The message will be of the form name + - * may not be null. - */ - private void checkNull(final Object object, final String name) - { - if(null == object) - { - throw new NullPointerException(name + " may not be null"); - } - } -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/security/TopicPermissions.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/security/TopicPermissions.java deleted file mode 100644 index 381a7e47a4c..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/security/TopicPermissions.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.security; - -/** - * A TopicPermission factory. The factory is bound to a specific type (i.e., - * either PUBLISH or SUBSCRIBE) and subsequently allows to create new permission - * objects by providing the topic. Note that the created permission objects most - * likely will be cached and that in case that a permission can not be created due - * to missing security support by the framework (i.e, security is not supported at - * all) an instance of java.lang.Object will be returned. - * - * @author Felix Project Team - */ -public interface TopicPermissions -{ - /** - * Get the type (i.e., PUBLISH or SUBSCRIBE) of the permission objects that this - * factory will create. - * - * @return The type of the permission objects that this factory will create. - * - * @see org.osgi.service.event.TopicPermission#PUBLISH - * @see org.osgi.service.event.TopicPermission#SUBSCRIBE - */ - public String getType(); - - /** - * This method returns a TopicPermission object for the given topic and - * the type (i.e., PUBLISH or SUBSCRIBE) of this factory. Note that this methods - * returns an instance of java.lang.Object in case that a permission - * could not be created due to missing security support by the framework. - * - * @param topic The targeted topic. - * - * @return A TopicPermission for the given topic and the type of this - * factory or a java.lang.Object in case that the permission could - * not be created due to missing security support by the framework. - * - * @see org.osgi.service.event.TopicPermission - * @see org.osgi.service.event.TopicPermission#PUBLISH - * @see org.osgi.service.event.TopicPermission#SUBSCRIBE - */ - public Object createTopicPermission(final String topic); -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/AsyncDeliverTasks.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/AsyncDeliverTasks.java deleted file mode 100644 index 8e8660d1dcc..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/AsyncDeliverTasks.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.tasks; - -import org.apache.felix.eventadmin.impl.dispatch.TaskQueue; -import org.apache.felix.eventadmin.impl.dispatch.ThreadPool; - -/** - * This class does the actual work of the asynchronous event dispatch. - * - *

      It serves two purposes: first, it will append tasks to its queue hence, - * asynchronous event delivery is executed - second, it will set up a given dispatch - * task with its ThreadPool in a way that it is associated with a - * DeliverTask that will block in case the thread hits the - * SyncDeliverTasks. - *

      - * In other words, if the asynchronous event dispatching thread is used to send a - * synchronous event then it will spin-off a new asynchronous dispatching thread - * while the former waits for the synchronous event to be delivered and then return - * to its ThreadPool. - * - * @author Felix Project Team - */ -public class AsyncDeliverTasks implements DeliverTasks, HandoverTask, DeliverTask -{ - // The asynchronous event delivery queue - private final TaskQueue m_queue; - - // The synchronous event delivery queue needed in case that the asynchronous - // event dispatching thread is used to send a synchronous event. This is a - // private member and only default because it is used in an inner class (for - // performance reasons) - final TaskQueue m_handoverQueue; - - // The thread pool to use to spin-off new threads - private final ThreadPool m_pool; - - /** - * The constructor of the class that will use the asynchronous queue to append - * event dispatch handlers. Furthermore, a second queue is used to append - * the events in case that the asynchronous event dispatching thread is used to - * send a synchronous event - in this case the given ThreadPool is used - * to spin-off a new asynchronous event dispatching thread while the former waits - * for the synchronous event to be delivered. - * - * @param queue The asynchronous event queue - * @param handoverQueue The synchronous event queue, to be used in case that the - * asynchronous event dispatching thread is used to send a synchronous event - * @param pool The thread pool used to spin-off new asynchronous event - * dispatching threads in case of timeout or that the asynchronous event - * dispatching thread is used to send a synchronous event - */ - public AsyncDeliverTasks(final TaskQueue queue, final TaskQueue handoverQueue, - final ThreadPool pool) - { - m_queue = queue; - - m_handoverQueue = handoverQueue; - - m_pool = pool; - } - - /** - * Return a DeliverTask that can be used to execute asynchronous event - * dispatch. - * - * @return A task that can be used to execute asynchronous event dispatch - * - * @see org.apache.felix.eventadmin.impl.tasks.DeliverTasks#createTask() - */ - public DeliverTask createTask() - { - return this; - } - - /** - * Execute asynchronous event dispatch. - * - * @param tasks The event dispatch tasks to execute - * - * @see org.apache.felix.eventadmin.impl.tasks.DeliverTask#execute(org.apache.felix.eventadmin.impl.tasks.HandlerTask[]) - */ - public void execute(final HandlerTask[] tasks) - { - m_queue.append(tasks); - } - - /** - * Execute the handover in case of timeout or that the asynchronous event - * dispatching thread is used to send a synchronous event. - * - * @param task The task to set-up in a new thread - * - * @see org.apache.felix.eventadmin.impl.tasks.HandoverTask#execute(org.apache.felix.eventadmin.impl.tasks.DispatchTask) - */ - public void execute(final DispatchTask task) - { - // This will spin-off a new thread using the thread pool and set it up with - // the given task. Additionally, the thread is associated with a callback - // that will handover (i.e., yet again call this method) and append the - // tasks given to to the m_handoverQueue (i.e., the synchronous queue). This - // will happen in case that the current asynchronous thread is used to - // send a synchronous event. - m_pool.execute(task, new DeliverTask() - { - public void execute(final HandlerTask[] managers) - { - final BlockTask waitManager = new BlockTask(); - - final HandlerTask[] newmanagers = new HandlerTask[managers.length + 1]; - - System.arraycopy(managers, 0, newmanagers, 0, - managers.length); - - newmanagers[managers.length] = waitManager; - - m_handoverQueue.append(newmanagers); - - task.handover(); - - waitManager.block(); - } - }); - } -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/BlockTask.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/BlockTask.java deleted file mode 100644 index b1896a4bd4b..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/BlockTask.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.tasks; - -/** - * This task will can be used to block a thread that subsequently will be unblocked - * once the task is executed. - * - * @author Felix Project Team - */ -public class BlockTask implements HandlerTask -{ - // The internal lock for this object used instead synchronized(this) - private final Object m_lock = new Object(); - - // Has this task not been executed? - private boolean m_blocking = true; - - /** - * Unblock possibly blocking threads. - * - * @see org.apache.felix.eventadmin.impl.tasks.HandlerTask#execute() - */ - public void execute() - { - synchronized (m_lock) - { - m_blocking = false; - m_lock.notifyAll(); - } - } - - /** - * This methods does nothing since we only need this task to block and unblock - * threads. - * - * @see org.apache.felix.eventadmin.impl.tasks.HandlerTask#blackListHandler() - */ - public void blackListHandler() - { - // This method does nothing since we only need this task to block and - // unblock threads - } - - /** - * Block the calling thread until this task is executed. - */ - public void block() - { - synchronized (m_lock) - { - while (m_blocking) - { - try - { - m_lock.wait(); - } catch (InterruptedException e) - { - - } - } - } - } - -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/DeliverTask.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/DeliverTask.java deleted file mode 100644 index 94ed48e731b..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/DeliverTask.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.tasks; - -/** - * Dispatch given event dispatch tasks. - * - * @author Felix Project Team - */ -public interface DeliverTask -{ - /** - * Dispatch the given event dispatch tasks. - * - * @param handlerTasks The event dispatch tasks to execute - */ - public void execute(final HandlerTask[] handlerTasks); -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/DeliverTasks.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/DeliverTasks.java deleted file mode 100644 index 7e8f1be56cb..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/DeliverTasks.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.tasks; - -/** - * A factory that creates DeliverTask objects. - * - * @author Felix Project Team - */ -public interface DeliverTasks -{ - /** - * Create a deliver task. - * - * @return A DeliverTask that can be used to dispatch event handler - * tasks - */ - public DeliverTask createTask(); -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/DispatchTask.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/DispatchTask.java deleted file mode 100644 index 6473abbb83b..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/DispatchTask.java +++ /dev/null @@ -1,324 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.tasks; - -import org.apache.felix.eventadmin.impl.dispatch.Scheduler; -import org.apache.felix.eventadmin.impl.dispatch.TaskProducer; - -/** - * This class is the core of the event dispatching (for both, synchronous and - * asynchronous). It implements handover and timeout capabilities. - * - * @author Felix Project Team - */ -public class DispatchTask implements Runnable -{ - // A null scheduler object that does not schedule given tasks - private static final Scheduler NULL_SCHEDULER = new Scheduler() - { - /** - * This is a null object and will do nothing with the given task - * - * @param task A task that is not used - * - * @see org.apache.felix.eventadmin.impl.dispatch.Scheduler#schedule(java.lang.Runnable) - */ - public void schedule(final Runnable task) - { - // This is a null object and will do nothing with the given task - } - - /** - * This is a null object and will do nothing with the given task - * - * @param task A task that is not used - * @parma nice A value that is not used - * - * @see org.apache.felix.eventadmin.impl.dispatch.Scheduler#schedule(java.lang.Runnable, int) - */ - public void schedule(final Runnable task, final int nice) - { - // This is a null object and will do nothing with the given task - } - }; - - // A null producer object that will return null on any call to next() - private static final TaskProducer NULL_PRODUCER = new TaskProducer() - { - /** - * This is a null object and will return null - * - * @return null - * - * @see org.apache.felix.eventadmin.impl.dispatch.TaskProducer#next() - */ - public HandlerTask next() - { - return null; - } - }; - - // A null handover task that will do nothing on execute - private static final HandoverTask NULL_HANDOVER = new HandoverTask() - { - /** - * This is a null object that will do nothing. - * - * @parma task A task that is not used - * - * @see org.apache.felix.eventadmin.impl.tasks.HandoverTask#execute(org.apache.felix.eventadmin.impl.tasks.DispatchTask) - */ - public void execute(final DispatchTask task) - { - // This is a null object that will do nothing. - } - }; - - // The internal lock for this object used instead synchronized(this) - final Object m_lock = new Object(); - - // The task producer (i.e., the event queue) that will be a null object if not - // needed anymore - private volatile TaskProducer m_producer; - - // The scheduler to use that will be a null object if not needed anymore - private Scheduler m_scheduler; - - // The handover callback that is called on timeouts and handovers and that will - // be a null object if not needed anymore - private HandoverTask m_handover; - - // Used to blacklist on timeout - private BlackListTask m_blackListTask = null; - - // Are we currently blocked (i.e., do not tick the timeout clock down)? - private boolean m_isHolding = false; - - /** - * The constructor of the object. - * - * @param producer The producer (i.e., the event queue) that provides the next - * tasks - * @param scheduler The scheduler to use for timeout actions - * @param handover The callback to use on timeouts and handovers - */ - public DispatchTask(final TaskProducer producer, final Scheduler scheduler, - final HandoverTask handover) - { - m_producer = producer; - - m_scheduler = scheduler; - - m_handover = handover; - } - - /* - * Construct a new object from a old one. - */ - private DispatchTask(final DispatchTask old) - { - this(old.m_producer, old.m_scheduler, old.m_handover); - } - - /** - * This will loop until the producer returns null. Until then the - * returned tasks are executed. - * - * @see java.lang.Runnable#run() - */ - public void run() - { - for (HandlerTask manager = m_producer.next(); null != manager; manager = m_producer - .next()) - { - synchronized (m_lock) - { - // Set-up the timeout - m_blackListTask = new BlackListTask(manager); - - m_scheduler.schedule(m_blackListTask); - } - - // HandlerTask does catch exceptions hence, we don't need to do it. - manager.execute(); - - synchronized (m_lock) - { - // release the timeout - m_blackListTask.cancel(); - } - } - } - - /** - * This method will trigger a callback to the handover callback and stop this - * task. - */ - public void handover() - { - synchronized (m_lock) - { - // release the timeout - m_blackListTask.cancel(); - - // spin-off a new thread - m_handover.execute(new DispatchTask(this)); - - stop(); - } - } - - /** - * This method stops the tasks without a handover - */ - public void stop() - { - synchronized (m_lock) - { - // release the timeout - m_blackListTask.cancel(); - - m_handover = NULL_HANDOVER; - - m_producer = NULL_PRODUCER; - - m_scheduler = NULL_SCHEDULER; - } - } - - /** - * This will pause the task (including its timeout clock) until a call to - * resume() - */ - public void hold() - { - synchronized (m_lock) - { - // release the timeout - m_blackListTask.cancel(); - - // record the time that we already used - int pastTime = (int) (System.currentTimeMillis() - m_blackListTask - .getTime()); - - // spin-off a new thread - m_handover.execute(new DispatchTask(this)); - - // block until a call to resume() - m_isHolding = true; - - while (m_isHolding) - { - try - { - m_lock.wait(); - } catch (InterruptedException e) - { - } - } - - // restore the timeout - m_blackListTask = new BlackListTask(m_blackListTask, - System.currentTimeMillis() - pastTime); - - m_scheduler.schedule(m_blackListTask, pastTime); - } - } - - /** - * This will let the previously hold task resume. - */ - public void resume() - { - synchronized (m_lock) - { - m_isHolding = false; - - m_lock.notifyAll(); - } - } - - /* - * This is the implementation of the timeout. - */ - private class BlackListTask implements Runnable - { - // Are we canceled? - private boolean m_canceled = false; - - // The time we have been started - private final long m_time; - - // The task we will blacklist if we are triggered - private final HandlerTask m_manager; - - BlackListTask(final HandlerTask manager) - { - this(manager, System.currentTimeMillis()); - } - - BlackListTask(final HandlerTask manager, final long time) - { - m_manager = manager; - - m_time = time; - } - - BlackListTask(final BlackListTask old, final long time) - { - this(old.m_manager, time); - } - - /** - * @return The time we have been created. - */ - public long getTime() - { - return m_time; - } - - /** - * We have been triggered hence, blacklist the handler except if we are - * already canceled - * - * @see java.lang.Runnable#run() - */ - public void run() - { - synchronized (m_lock) - { - if (!m_canceled) - { - m_manager.blackListHandler(); - - handover(); - } - } - } - - /** - * Cancel the timeout - */ - public void cancel() - { - synchronized (m_lock) - { - m_canceled = true; - } - } - } -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/HandlerTask.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/HandlerTask.java deleted file mode 100644 index edf00b64576..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/HandlerTask.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.tasks; - -/** - * A task that will deliver its event to its EventHandler when executed - * or blacklist the handler, respectively. - * - * @author Felix Project Team - */ -public interface HandlerTask -{ - /** - * Deliver the event to the handler. - */ - public void execute(); - - /** - * Blacklist the handler. - */ - public void blackListHandler(); -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/HandlerTaskImpl.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/HandlerTaskImpl.java deleted file mode 100644 index b9a243aaa17..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/HandlerTaskImpl.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.tasks; - -import org.apache.felix.eventadmin.impl.handler.BlacklistingHandlerTasks; -import org.apache.felix.eventadmin.impl.util.LogWrapper; -import org.osgi.framework.ServiceReference; -import org.osgi.service.event.Event; -import org.osgi.service.event.EventHandler; - -/** - * An implementation of the HandlerTask interface. - * - * @author Felix Project Team - */ -public class HandlerTaskImpl implements HandlerTask -{ - // The service reference of the handler - private final ServiceReference m_eventHandlerRef; - - // The event to deliver to the handler - private final Event m_event; - - // Used to blacklist the service or get the service object for the reference - private final BlacklistingHandlerTasks m_handlerTasks; - - /** - * Construct a delivery task for the given service and event. - * - * @param eventHandlerRef The servicereference of the handler - * @param event The event to deliver - * @param handlerTasks Used to blacklist the service or get the service object - * for the reference - */ - public HandlerTaskImpl(final ServiceReference eventHandlerRef, - final Event event, final BlacklistingHandlerTasks handlerTasks) - { - m_eventHandlerRef = eventHandlerRef; - - m_event = event; - - m_handlerTasks = handlerTasks; - } - - /** - * @see org.apache.felix.eventadmin.impl.tasks.HandlerTask#execute() - */ - public void execute() - { - // Get the service object - final EventHandler handler = m_handlerTasks - .getEventHandler(m_eventHandlerRef); - - try - { - handler.handleEvent(m_event); - } catch (Exception e) - { - // The spec says that we must catch exceptions and log them: - LogWrapper.getLogger().log( - m_eventHandlerRef, - LogWrapper.LOG_WARNING, - "Exception during event dispatch [" + m_event + " | " - + m_eventHandlerRef + " | Bundle(" - + m_eventHandlerRef.getBundle() + ")]", e); - } - - m_handlerTasks.ungetEventHandler(handler, m_eventHandlerRef); - } - - /** - * @see org.apache.felix.eventadmin.impl.tasks.HandlerTask#blackListHandler() - */ - public void blackListHandler() - { - m_handlerTasks.blackList(m_eventHandlerRef); - } -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/HandoverTask.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/HandoverTask.java deleted file mode 100644 index 5ae14ceb3f1..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/HandoverTask.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.tasks; - -/** - * A task that is used to handover a dispatch thread context to another thread. - * - * @author Felix Project Team - */ -public interface HandoverTask -{ - /** - * Handover the context to another thread. - * - * @param task The context to be executed in another thread. - */ - public void execute(final DispatchTask task); -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/ResumeTask.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/ResumeTask.java deleted file mode 100644 index 82ab8f41c96..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/ResumeTask.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.tasks; - -import org.apache.felix.eventadmin.impl.dispatch.ThreadPool; - -/** - * A task that wakes-up a disabled DispatchTask. Additionally, it will - * stop the currently running task. - * - * @author Felix Project Team - */ -public class ResumeTask implements HandlerTask -{ - // The task to wake-up on execution - private final DispatchTask m_target; - - // The pool used to get the task to stop on execution - private final ThreadPool m_pool; - - /** - * @param target The task to wake-up on execution - * @param pool The pool used to get the task to stop on execution - */ - public ResumeTask(final DispatchTask target, final ThreadPool pool) - { - m_target = target; - - m_pool = pool; - } - - /** - * Stop the current task and wake-up the target. - * - * @see org.apache.felix.eventadmin.impl.tasks.HandlerTask#execute() - */ - public void execute() - { - m_pool.getTask(Thread.currentThread(), null).stop(); - - m_target.resume(); - } - - /** - * This does nothing since this task is only used to wake-up disabled tasks. - * - * @see org.apache.felix.eventadmin.impl.tasks.HandlerTask#blackListHandler() - */ - public void blackListHandler() - { - } -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/SyncDeliverTasks.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/SyncDeliverTasks.java deleted file mode 100644 index 3854436a654..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/tasks/SyncDeliverTasks.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.tasks; - -import org.apache.felix.eventadmin.impl.dispatch.TaskQueue; -import org.apache.felix.eventadmin.impl.dispatch.ThreadPool; - -/** - * This class does the actual work of the synchronous event delivery. - *

      - * It serves two purposes, first it is used to select the appropriate action - * depending on whether the sending thread is the asynchronous, the synchronous, or - * an unrelated thread. Second, it will set up a given dispatch - * task with its ThreadPool in a way that it is associated with a - * DeliverTask that will push given handler tasks to the queue and - * then wait for the tasks to be completed. - *

      - * In other words if an unrelated thread is used to send a synchronous event it is - * blocked until the event is send (or a timeout occurs), if an asynchronous thread - * is used its handover callback is called in order to spin-off a new asynchronous - * delivery thread and the former is blocked until the events are delivered and then - * released (or returned to its thread pool), if a synchronous thread is used its - * task is disabled, the events are pushed to the queue and the threads continuous - * with the delivery of the new events (as per spec). Once the new events are done - * the thread wakes-up the disabled task and resumes to execute it. - *

      - * Note that in case of a timeout while a task is disabled the thread is released and - * we spin-off a new thread that resumes the disabled task hence, this is the only - * place were we break the semantics of the synchronous delivery. While the only one - * to notice this is the timed-out handler - it is the fault of this handler too - * (i.e., it blocked the dispatch for to long) but since it will not receive events - * anymore it will not notice this semantic difference except that it might not see - * events it already sent before. - * - * @author Felix Project Team - */ -public class SyncDeliverTasks implements DeliverTasks, HandoverTask, DeliverTask -{ - // The synchronous event queue - final TaskQueue m_queue; - - // The thread pool used to spin-off new threads and associate callbacks with - // tasks - final ThreadPool m_pool; - - /** - * @param queue The synchronous event queue - * @param pool The thread pool used to spin-off new threads and associate - * callbacks with tasks - */ - public SyncDeliverTasks(final TaskQueue queue, final ThreadPool pool) - { - m_queue = queue; - - m_pool = pool; - } - - /** - * This will select the appropriate action depending on whether the sending - * thread is the asynchronous, the synchronous, or an unrelated thread. - * - * @return The appropriate action - * - * @see org.apache.felix.eventadmin.impl.tasks.DeliverTasks#createTask() - */ - public DeliverTask createTask() - { - return m_pool.getCallback(Thread.currentThread(), this); - } - - /** - * This blocks an unrelated thread used to send a synchronous event until the - * event is send (or a timeout occurs). - * - * @param tasks The event handler dispatch tasks to execute - * - * @see org.apache.felix.eventadmin.impl.tasks.DeliverTask#execute(org.apache.felix.eventadmin.impl.tasks.HandlerTask[]) - */ - public void execute(final HandlerTask[] tasks) - { - final BlockTask waitManager = new BlockTask(); - - final HandlerTask[] newtasks = new HandlerTask[tasks.length + 1]; - - System.arraycopy(tasks, 0, newtasks, 0, tasks.length); - - newtasks[tasks.length] = waitManager; - - m_queue.append(newtasks); - - waitManager.block(); - } - - /** - * Set up a given dispatch task with its ThreadPool in a way that it is - * associated with a DeliverTask that will push given handler tasks to - * the queue and then wait for the tasks to be completed. - * - * @param task The task to set-up - * - * @see org.apache.felix.eventadmin.impl.tasks.HandoverTask#execute(org.apache.felix.eventadmin.impl.tasks.DispatchTask) - */ - public void execute(final DispatchTask task) - { - m_pool.execute(task, new DeliverTask() - { - public void execute(final HandlerTask[] managers) - { - final ResumeTask resumeManager = new ResumeTask( - task, m_pool); - - final HandlerTask[] newmanagers = new HandlerTask[managers.length + 1]; - - System.arraycopy(managers, 0, newmanagers, 0, - managers.length); - - newmanagers[managers.length] = resumeManager; - - m_queue.push(newmanagers); - - task.hold(); - } - }); - } -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/util/CacheMap.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/util/CacheMap.java deleted file mode 100644 index 93e007ff57f..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/util/CacheMap.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.util; - -/** - * This is the interface of a simple cache map. - * - * @author Felix Project Team - */ -public interface CacheMap -{ - /** - * Return the value for the key in case there is one in the cache. - * - * @param key The key to look-up - * - * @return The value for the given key in case there is one in the cache, - * null otherwise - */ - public Object get(final Object key); - - /** - * Add a value for the key to this cache. - * - * @param key The key for the value - * @param value The value to add to the cache - */ - public void add(final Object key, final Object value); - - /** - * Remove a key and its value from the cache. - * - * @param key The key to remove - * - * @return The value of the key in case there is one in the cache, null - * otherwise - */ - public Object remove(final Object key); - - /** - * Returns the number of key-value pairs in this cache. - * - * @return The number of key-value pairs in this cache - */ - public int size(); - - /** - * Remove all entries of the cache. - */ - public void clear(); -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/util/LeastRecentlyUsedCacheMap.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/util/LeastRecentlyUsedCacheMap.java deleted file mode 100644 index 6fd0901282f..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/util/LeastRecentlyUsedCacheMap.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.util; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * This class implements a least recently used cache map. It will hold - * a given size of key-value pairs and drop the least recently used entry once this - * size is reached. This class is thread safe. - * - * @see org.apache.felix.eventadmin.impl.util.CacheMap - * - * @author Felix Project Team - */ -public class LeastRecentlyUsedCacheMap implements CacheMap -{ - // The internal lock for this object used instead synchronized(this) - private final Object m_lock = new Object(); - - // The max number of entries in the cache. Once reached entries are replaced - private final int m_maxSize; - - // The cache - private final Map m_cache; - - // The history used to determine the least recently used entries. The end of the - // list is the most recently used key. In other words m_history.get(0) returns - // the least recently used key. - private final List m_history; - - /** - * The constructor of the cache. The given max size will be used to determine the - * size of the cache that triggers replacing least recently used entries with - * new ones. - * - * @param maxSize The max number of entries in the cache - */ - public LeastRecentlyUsedCacheMap(final int maxSize) - { - if(0 >= maxSize) - { - throw new IllegalArgumentException("Size must be positive"); - } - - m_maxSize = maxSize; - - // We need one more entry then m_maxSize in the cache and a HashMap is - // expanded when it reaches 3/4 of its size hence, the funny numbers. - m_cache = new HashMap(m_maxSize + 1 + ((m_maxSize + 1) * 3) / 4); - - // This is like above but assumes a list is expanded when it reaches 1/2 of - // its size. Not much harm if this is not the case. - m_history = new ArrayList(m_maxSize + 1 + ((m_maxSize + 1) / 2)); - } - - /** - * Returns the value for the key in case there is one. Additionally, the - * LRU counter for the key is updated. - * - * @param key The key for the value to return - * - * @return The value of the key in case there is one, null otherwise - * - * @see org.apache.felix.eventadmin.impl.util.CacheMap#get(java.lang.Object) - */ - public Object get(final Object key) - { - synchronized(m_lock) - { - final Object result = m_cache.get(key); - - if(null != result) - { - m_history.remove(key); - - m_history.add(key); - } - - return result; - } - } - - /** - * Add the key-value pair to the cache. The key will be come the most recently - * used entry. In case max size is (or has been) reached this will remove the - * least recently used entry in the cache. In case that the cache already - * contains this specific key-value pair it LRU counter is updated only. - * - * @param key The key for the value - * @param value The value for the key - * - * @see org.apache.felix.eventadmin.impl.util.CacheMap#add(java.lang.Object, java.lang.Object) - */ - public void add(final Object key, final Object value) - { - synchronized(m_lock) - { - final Object result = m_cache.put(key, value); - - if(null != result) - { - m_history.remove(key); - } - - m_history.add(key); - - if(m_maxSize < m_cache.size()) - { - m_cache.remove(m_history.remove(0)); - } - } - } - - /** - * Remove the entry denoted by key from the cache and return its value. - * - * @param key The key of the entry to be removed - * - * @return The value of the entry removed, null if none - * - * @see org.apache.felix.eventadmin.impl.util.CacheMap#remove(java.lang.Object) - */ - public Object remove(final Object key) - { - synchronized(m_lock) - { - final Object result = m_cache.remove(key); - - if(null != result) - { - m_history.remove(key); - } - - return result; - } - } - - /** - * Return the current size of the cache. - * - * @return The number of entries currently in the cache. - * - * @see org.apache.felix.eventadmin.impl.util.CacheMap#size() - */ - public int size() - { - synchronized (m_lock) - { - return m_cache.size(); - } - } - - /** - * Remove all entries from the cache. - * - * @see org.apache.felix.eventadmin.impl.util.CacheMap#clear() - */ - public void clear() - { - synchronized (m_lock) - { - m_cache.clear(); - - m_history.clear(); - } - } - -} diff --git a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/util/LogWrapper.java b/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/util/LogWrapper.java deleted file mode 100644 index e4a580441e6..00000000000 --- a/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/util/LogWrapper.java +++ /dev/null @@ -1,412 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.eventadmin.impl.util; - -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; - -import org.osgi.framework.BundleContext; -import org.osgi.framework.BundleException; -import org.osgi.framework.Constants; -import org.osgi.framework.InvalidSyntaxException; -import org.osgi.framework.ServiceEvent; -import org.osgi.framework.ServiceListener; -import org.osgi.framework.ServiceReference; - -/** - * This class mimics the standard OSGi LogService interface. An - * instance of this class will be used by the EventAdmin for all logging. The - * implementation of this class sends log messages to standard output, if no - * LogService is present; it uses a log service if one is - * installed in the framework. To do that without creating a hard dependency on the - * package it uses fully qualified class names and registers a listener with the - * framework hence, it does not need access to the LogService class but will - * use it if the listener is informed about an available service. By using a - * DynamicImport-Package dependency we don't need the package but - * use it if present. Additionally, all log methods prefix the log message with - * EventAdmin: . - * - * @see org.osgi.service.log.LogService - * - * @author Felix Project Team -**/ -// TODO: At the moment we log a message to all currently available LogServices. -// Maybe, we should only log to the one with the highest ranking instead? -// What is the best practice in this case? -public class LogWrapper -{ - /** - * ERROR LEVEL - * - * @see org.osgi.service.log.LogService#LOG_ERROR - */ - public static final int LOG_ERROR = 1; - - /** - * WARNING LEVEL - * - * @see org.osgi.service.log.LogService#LOG_WARNING - */ - public static final int LOG_WARNING = 2; - - /** - * INFO LEVEL - * - * @see org.osgi.service.log.LogService#LOG_INFO - */ - public static final int LOG_INFO = 3; - - /** - * DEBUG LEVEL - * - * @see org.osgi.service.log.LogService#LOG_DEBUG - */ - public static final int LOG_DEBUG = 4; - - // A set containing the currently available LogServices. Furthermore used as lock - private final Set m_loggerRefs = new HashSet(); - - // Only null while not set and m_loggerRefs is empty hence, only needs to be - // checked in case m_loggerRefs is empty otherwise it will not be null. - private BundleContext m_context; - - /* - * A thread save variant of the double checked locking singleton. - */ - private static class LogWrapperLoader - { - static final LogWrapper m_singleton = new LogWrapper(); - } - - /** - * Returns the singleton instance of this LogWrapper that can be used to send - * log messages to all currently available LogServices or to standard output, - * respectively. - * - * @return the singleton instance of this LogWrapper. - */ - public static LogWrapper getLogger() - { - return LogWrapperLoader.m_singleton; - } - - /** - * Set the BundleContext of the bundle. This method registers a service - * listener for LogServices with the framework that are subsequently used to - * log messages. - * - * @param context The context of the bundle. - */ - public static void setContext(final BundleContext context) - { - LogWrapperLoader.m_singleton.setBundleContext(context); - - try - { - context.addServiceListener(new ServiceListener() - { - // Add a newly available LogService reference to the singleton. - public void serviceChanged(final ServiceEvent event) - { - if (ServiceEvent.REGISTERED == event.getType()) - { - LogWrapperLoader.m_singleton.addLoggerRef( - event.getServiceReference()); - } - // unregistered services are handled in the next log operation. - } - - }, "(" + Constants.OBJECTCLASS - + "=org.osgi.service.log.LogService)"); - - // Add all available LogService references to the singleton. - final ServiceReference[] refs = context.getServiceReferences( - "org.osgi.service.log.LogService", null); - - if (null != refs) - { - for (int i = 0; i < refs.length; i++) - { - LogWrapperLoader.m_singleton.addLoggerRef(refs[i]); - } - } - } catch (InvalidSyntaxException e) - { - // this never happens - } - } - - /* - * The private singleton constructor. - */ - LogWrapper() - { - // Singleton - } - - /* - * Add a reference to a newly available LogService - */ - void addLoggerRef(final ServiceReference ref) - { - synchronized (m_loggerRefs) - { - m_loggerRefs.add(ref); - } - } - - /* - * Set the context of the bundle in the singleton implementation. - */ - private void setBundleContext(final BundleContext context) - { - synchronized(m_loggerRefs) - { - m_context = context; - } - } - - /** - * Log a message with the given log level. Note that this will prefix the message - * with EventAdmin: . - * - * @param level The log level with which to log the msg. - * @param msg The message to log. - */ - public void log(final int level, final String msg) - { - // The method will remove any unregistered service reference as well. - synchronized(m_loggerRefs) - { - final String logMsg = "EventAdmin: " + msg; - - if (!m_loggerRefs.isEmpty()) - { - // There is at least one LogService available hence, we can use the - // class as well. - for (Iterator iter = m_loggerRefs.iterator(); iter.hasNext();) - { - final ServiceReference next = (ServiceReference) iter.next(); - - org.osgi.service.log.LogService logger = - (org.osgi.service.log.LogService) m_context.getService(next); - - if (null != logger) - { - logger.log(level, logMsg); - - m_context.ungetService(next); - } - else - { - // The context returned null for the reference - it follows - // that the service is unregistered and we can remove it - iter.remove(); - } - } - } - else - { - _log(null, level, logMsg, null); - } - } - } - - /** - * Log a message with the given log level and the associated exception. Note that - * this will prefix the message with EventAdmin: . - * - * @param level The log level with which to log the msg. - * @param msg The message to log. - * @param ex The exception associated with the message. - */ - public void log(final int level, final String msg, final Throwable ex) - { - // The method will remove any unregistered service reference as well. - synchronized(m_loggerRefs) - { - final String logMsg = "EventAdmin: " + msg; - - if (!m_loggerRefs.isEmpty()) - { - // There is at least one LogService available hence, we can use the - // class as well. - for (Iterator iter = m_loggerRefs.iterator(); iter.hasNext();) - { - final ServiceReference next = (ServiceReference) iter.next(); - - org.osgi.service.log.LogService logger = - (org.osgi.service.log.LogService) m_context.getService(next); - - if (null != logger) - { - logger.log(level, logMsg, ex); - - m_context.ungetService(next); - } - else - { - // The context returned null for the reference - it follows - // that the service is unregistered and we can remove it - iter.remove(); - } - } - } - else - { - _log(null, level, logMsg, ex); - } - } - } - - /** - * Log a message with the given log level together with the associated service - * reference. Note that this will prefix the message with EventAdmin: . - * - * @param sr The reference of the service associated with this message. - * @param level The log level with which to log the msg. - * @param msg The message to log. - */ - public void log(final ServiceReference sr, final int level, final String msg) - { - // The method will remove any unregistered service reference as well. - synchronized(m_loggerRefs) - { - final String logMsg = "EventAdmin: " + msg; - - if (!m_loggerRefs.isEmpty()) - { - // There is at least one LogService available hence, we can use the - // class as well. - for (Iterator iter = m_loggerRefs.iterator(); iter.hasNext();) - { - final ServiceReference next = (ServiceReference) iter.next(); - - org.osgi.service.log.LogService logger = - (org.osgi.service.log.LogService) m_context.getService(next); - - if (null != logger) - { - logger.log(sr, level, logMsg); - - m_context.ungetService(next); - } - else - { - // The context returned null for the reference - it follows - // that the service is unregistered and we can remove it - iter.remove(); - } - } - } - else - { - _log(sr, level, logMsg, null); - } - } - } - - /** - * Log a message with the given log level, the associated service reference and - * exception. Note that this will prefix the message with EventAdmin: . - * - * @param sr The reference of the service associated with this message. - * @param level The log level with which to log the msg. - * @param msg The message to log. - * @param ex The exception associated with the message. - */ - public void log(final ServiceReference sr, final int level, final String msg, - final Throwable ex) - { - // The method will remove any unregistered service reference as well. - synchronized(m_loggerRefs) - { - final String logMsg = "EventAdmin: " + msg; - - if (!m_loggerRefs.isEmpty()) - { - // There is at least one LogService available hence, we can use the - // class as well. - for (Iterator iter = m_loggerRefs.iterator(); iter.hasNext();) - { - final ServiceReference next = (ServiceReference) iter.next(); - - org.osgi.service.log.LogService logger = - (org.osgi.service.log.LogService) m_context.getService(next); - - if (null != logger) - { - logger.log(sr, level, logMsg, ex); - - m_context.ungetService(next); - } - else - { - // The context returned null for the reference - it follows - // that the service is unregistered and we can remove it - iter.remove(); - } - } - } - else - { - _log(sr, level, logMsg, ex); - } - } - } - - /* - * Log the message to standard output. This appends the level to the message. - * null values are handled appropriate. - */ - private void _log(final ServiceReference sr, final int level, final String msg, - Throwable ex) - { - String s = (sr == null) ? null : "SvcRef " + sr; - s = (s == null) ? msg : s + " " + msg; - s = (ex == null) ? s : s + " (" + ex + ")"; - - switch (level) - { - case LOG_DEBUG: - System.out.println("DEBUG: " + s); - break; - case LOG_ERROR: - System.out.println("ERROR: " + s); - if (ex != null) - { - if ((ex instanceof BundleException) - && (((BundleException) ex).getNestedException() != null)) - { - ex = ((BundleException) ex).getNestedException(); - } - - ex.printStackTrace(); - } - break; - case LOG_INFO: - System.out.println("INFO: " + s); - break; - case LOG_WARNING: - System.out.println("WARNING: " + s); - break; - default: - System.out.println("UNKNOWN[" + level + "]: " + s); - } - } -} diff --git a/examples/dictionaryclient/pom.xml b/examples/dictionaryclient/pom.xml index 0bbbb81deec..daf301a3fd7 100644 --- a/examples/dictionaryclient/pom.xml +++ b/examples/dictionaryclient/pom.xml @@ -1,48 +1,69 @@ + org.apache.felix felix - 0.8.0-SNAPSHOT + 1.0.4 + ../../pom/pom.xml 4.0.0 - osgi-bundle - Apache Felix Examples: Dictionary Client + bundle + Apache Felix Example Dictionary Client org.apache.felix.examples.dictionaryclient + 0.9.0-SNAPSHOT ${pom.groupId} org.osgi.core - ${pom.version} + 1.0.0 provided ${pom.groupId} org.apache.felix.examples.dictionaryservice - ${pom.version} + 0.9.0-SNAPSHOT provided - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} + org.apache.felix + maven-bundle-plugin + 1.4.0 true - - Dictionary Client Example - Apache Software Foundation - + + ${pom.artifactId} + Example Dictionary Client + The Apache Software Foundation + A bundle using the dictionary service if it finds it at startup. - - + + org.apache.felix.examples.dictionaryclient.Activator - - - org.osgi.framework, org.apache.felix.examples.dictionaryservice - - + + + org.apache.felix.examples.dictionaryclient.* + + diff --git a/examples/dictionaryclient/src/main/java/org/apache/felix/examples/dictionaryclient/Activator.java b/examples/dictionaryclient/src/main/java/org/apache/felix/examples/dictionaryclient/Activator.java index 49e8a0ea702..7026f85b2cb 100644 --- a/examples/dictionaryclient/src/main/java/org/apache/felix/examples/dictionaryclient/Activator.java +++ b/examples/dictionaryclient/src/main/java/org/apache/felix/examples/dictionaryclient/Activator.java @@ -38,7 +38,7 @@ * stop checking words by entering an empty line, but to start checking words * again you must stop and then restart the bundle. * - * @author Felix Project Team + * @author Felix Project Team */ public class Activator implements BundleActivator { diff --git a/examples/dictionaryclient2/pom.xml b/examples/dictionaryclient2/pom.xml index 2c7c5d4fb58..a851160bdf9 100644 --- a/examples/dictionaryclient2/pom.xml +++ b/examples/dictionaryclient2/pom.xml @@ -1,48 +1,69 @@ + org.apache.felix felix - 0.8.0-SNAPSHOT + 1.0.4 + ../../pom/pom.xml 4.0.0 - osgi-bundle - Apache Felix Examples: Dynamic Dictionary Client + bundle + Apache Felix Example Dynamic Dictionary Client org.apache.felix.examples.dictionaryclient2 + 0.9.0-SNAPSHOT ${pom.groupId} org.osgi.core - ${pom.version} + 1.0.0 provided ${pom.groupId} org.apache.felix.examples.dictionaryservice - ${pom.version} + 0.9.0-SNAPSHOT provided - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} + org.apache.felix + maven-bundle-plugin + 1.4.0 true - - Dynamic Dictionary Client Example - Apache Software Foundation - + + ${pom.artifactId} + Example Dynamic Dictionary Client + The Apache Software Foundation + A client bundle with more complex requirements for the dictionary. - - + + org.apache.felix.examples.dictionaryclient2.Activator - - - org.osgi.framework, org.apache.felix.examples.dictionaryservice - - + + + org.apache.felix.examples.dictionaryclient2.* + + diff --git a/examples/dictionaryclient2/src/main/java/org/apache/felix/examples/dictionaryclient2/Activator.java b/examples/dictionaryclient2/src/main/java/org/apache/felix/examples/dictionaryclient2/Activator.java index 3e70082e258..d225fe3c289 100644 --- a/examples/dictionaryclient2/src/main/java/org/apache/felix/examples/dictionaryclient2/Activator.java +++ b/examples/dictionaryclient2/src/main/java/org/apache/felix/examples/dictionaryclient2/Activator.java @@ -42,7 +42,7 @@ * input. You can stop checking words by entering an empty line, but to start * checking words again you must stop and then restart the bundle. * - * @author Felix Project Team + * @author Felix Project Team */ public class Activator implements BundleActivator, ServiceListener { diff --git a/examples/dictionaryservice.itest/pom.xml b/examples/dictionaryservice.itest/pom.xml index 0942b9d90b4..bb081cbc0ff 100644 --- a/examples/dictionaryservice.itest/pom.xml +++ b/examples/dictionaryservice.itest/pom.xml @@ -1,24 +1,44 @@ + org.apache.felix felix - 0.8.0-SNAPSHOT + 1.0.4 + ../../pom/pom.xml 4.0.0 - osgi-bundle - Apache Felix Examples: Dictionary Service Integration Test + bundle + Apache Felix Example Dictionary Service Integration Test org.apache.felix.examples.dictionaryservice.itest + 0.9.0-SNAPSHOT ${pom.groupId} org.osgi.core - ${pom.version} + 1.0.0 provided ${pom.groupId} org.apache.felix.examples.dictionaryservice - ${pom.version} + 0.9.0-SNAPSHOT provided @@ -37,6 +57,7 @@ org.apache.felix.plugins maven-felix-plugin + 0.9.0-SNAPSHOT run @@ -54,24 +75,25 @@ - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} + org.apache.felix + maven-bundle-plugin + 1.4.0 true - - Dictionary Integration Test Example - Apache Software Foundation - + + ${pom.artifactId} + + org.apache.felix.examples.dictionaryservice.itest.* + + Example Dictionary Integration Test + The Apache Software Foundation + A bundle using the dictionary service to test it. - - + + org.apache.felix.examples.dictionaryservice.itest.Activator - - - org.osgi.framework, org.apache.felix.examples.dictionaryservice - - + + diff --git a/examples/dictionaryservice.itest/src/main/java/org/apache/felix/examples/dictionaryservice/itest/Activator.java b/examples/dictionaryservice.itest/src/main/java/org/apache/felix/examples/dictionaryservice/itest/Activator.java index 6aa733c3c6a..75382e00880 100644 --- a/examples/dictionaryservice.itest/src/main/java/org/apache/felix/examples/dictionaryservice/itest/Activator.java +++ b/examples/dictionaryservice.itest/src/main/java/org/apache/felix/examples/dictionaryservice/itest/Activator.java @@ -29,7 +29,7 @@ * correct operation of the dictionary while it is running inside felix. * This is an integration test of the dictionaryservice bundle. * - * @author Felix Project Team + * @author Felix Project Team */ public class Activator implements BundleActivator { diff --git a/examples/dictionaryservice/pom.xml b/examples/dictionaryservice/pom.xml index 1008e1b9403..321293acc6d 100644 --- a/examples/dictionaryservice/pom.xml +++ b/examples/dictionaryservice/pom.xml @@ -1,45 +1,66 @@ + org.apache.felix felix - 0.8.0-SNAPSHOT + 1.0.4 + ../../pom/pom.xml 4.0.0 - osgi-bundle - Apache Felix Examples: English Dictionary Service + bundle + Apache Felix Example English Dictionary Service org.apache.felix.examples.dictionaryservice + 0.9.0-SNAPSHOT ${pom.groupId} org.osgi.core - ${pom.version} + 1.0.0 provided - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} + org.apache.felix + maven-bundle-plugin + 1.4.0 true - - English Dictionary Example - Apache Software Foundation - + + ${pom.artifactId} + Example English Dictionary + The Apache Software Foundation + A bundle that registersan English dictionary service. - - + + org.apache.felix.examples.dictionaryservice.impl.Activator - - + + org.apache.felix.examples.dictionaryservice - - - org.osgi.framework - - + + + org.apache.felix.examples.dictionaryservice.* + + diff --git a/examples/dictionaryservice/src/main/java/org/apache/felix/examples/dictionaryservice/DictionaryService.java b/examples/dictionaryservice/src/main/java/org/apache/felix/examples/dictionaryservice/DictionaryService.java index c38efcf8582..2dbb519c4bc 100644 --- a/examples/dictionaryservice/src/main/java/org/apache/felix/examples/dictionaryservice/DictionaryService.java +++ b/examples/dictionaryservice/src/main/java/org/apache/felix/examples/dictionaryservice/DictionaryService.java @@ -21,7 +21,7 @@ * A simple service interface that defines a dictionary service. A dictionary * service simply verifies the existence of a word. * - * @author Felix Project Team + * @author Felix Project Team */ public interface DictionaryService { diff --git a/examples/dictionaryservice/src/main/java/org/apache/felix/examples/dictionaryservice/impl/Activator.java b/examples/dictionaryservice/src/main/java/org/apache/felix/examples/dictionaryservice/impl/Activator.java index 5da2ded49d1..5934a8a1bb4 100644 --- a/examples/dictionaryservice/src/main/java/org/apache/felix/examples/dictionaryservice/impl/Activator.java +++ b/examples/dictionaryservice/src/main/java/org/apache/felix/examples/dictionaryservice/impl/Activator.java @@ -30,7 +30,7 @@ * dictionary service interface is defined in a separate class file and is * implemented by an inner class. * - * @author Felix Project Team + * @author Felix Project Team */ public class Activator implements BundleActivator { diff --git a/examples/eventlistener/pom.xml b/examples/eventlistener/pom.xml index ba62dafee5e..a86448ec373 100644 --- a/examples/eventlistener/pom.xml +++ b/examples/eventlistener/pom.xml @@ -1,44 +1,63 @@ + org.apache.felix felix - 0.8.0-SNAPSHOT + 1.0.4 + ../../pom/pom.xml 4.0.0 - osgi-bundle - Apache Felix Examples: Service Event Listener + bundle + Apache Felix Example Service Event Listener org.apache.felix.examples.eventlistener + 0.9.0-SNAPSHOT ${pom.groupId} org.osgi.core - ${pom.version} + 1.0.0 provided - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} + org.apache.felix + maven-bundle-plugin + 1.4.0 true - - Service listener example - + + ${pom.artifactId} + Example Service Listener + The Apache Software Foundation + Bundle listening for service events: displays a message on startup and when service events occur. - - + + org.apache.felix.examples.eventlistener.Activator - - - org.apache.felix.examples.eventlistener - - - org.osgi.framework - - + + + org.apache.felix.examples.eventlistener.* + + diff --git a/examples/eventlistener/src/main/java/org/apache/felix/examples/eventlistener/Activator.java b/examples/eventlistener/src/main/java/org/apache/felix/examples/eventlistener/Activator.java index 8740cb2b2a0..dd27674a457 100644 --- a/examples/eventlistener/src/main/java/org/apache/felix/examples/eventlistener/Activator.java +++ b/examples/eventlistener/src/main/java/org/apache/felix/examples/eventlistener/Activator.java @@ -28,7 +28,7 @@ * event mechanism to listen for service events. Upon receiving a service event, * it prints out the event's details. * - * @author Felix Project Team + * @author Felix Project Team */ public class Activator implements BundleActivator, ServiceListener { diff --git a/examples/extenderbased.circle/pom.xml b/examples/extenderbased.circle/pom.xml new file mode 100644 index 00000000000..55d252161e7 --- /dev/null +++ b/examples/extenderbased.circle/pom.xml @@ -0,0 +1,69 @@ + + + + + org.apache.felix + felix + 1.0.4 + ../../pom/pom.xml + + + 4.0.0 + bundle + Apache Felix Circle Extension + A simple extension for drawing circles. + org.apache.felix.example + extenderbased.circle + 1.0.0 + + + org.apache.felix.example + extenderbased.host + 1.0.0 + + + + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + The Apache Software Foundation + Circle + org/apache/felix/example/extenderbased/circle/circle.png + org.apache.felix.example.extenderbased.circle.Circle + org.apache.felix.example.extenderbased.circle.* + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + + diff --git a/examples/extenderbased.circle/src/main/java/org/apache/felix/example/extenderbased/circle/Circle.java b/examples/extenderbased.circle/src/main/java/org/apache/felix/example/extenderbased/circle/Circle.java new file mode 100644 index 00000000000..ee24c61003f --- /dev/null +++ b/examples/extenderbased.circle/src/main/java/org/apache/felix/example/extenderbased/circle/Circle.java @@ -0,0 +1,54 @@ +/* + * 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.example.extenderbased.circle; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.geom.Ellipse2D; +import org.apache.felix.example.extenderbased.host.extension.SimpleShape; + +/** + * This class implements the circle SimpleShape extension. + * It simply provides a draw() that paints a circle. +**/ +public class Circle implements SimpleShape +{ + /** + * Implements the SimpleShape.draw() method for painting + * the shape. + * @param g2 The graphics object used for painting. + * @param p The position to paint the triangle. + **/ + public void draw(Graphics2D g2, Point p) + { + int x = p.x - 25; + int y = p.y - 25; + GradientPaint gradient = new GradientPaint( + x, y, Color.RED, x + 50, y, Color.WHITE); + g2.setPaint(gradient); + g2.fill(new Ellipse2D.Double(x, y, 50, 50)); + BasicStroke wideStroke = new BasicStroke(2.0f); + g2.setColor(Color.black); + g2.setStroke(wideStroke); + g2.draw(new Ellipse2D.Double(x, y, 50, 50)); + } +} \ No newline at end of file diff --git a/examples/extenderbased.circle/src/main/resources/META-INF/LICENSE b/examples/extenderbased.circle/src/main/resources/META-INF/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/examples/extenderbased.circle/src/main/resources/META-INF/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/examples/extenderbased.circle/src/main/resources/META-INF/NOTICE b/examples/extenderbased.circle/src/main/resources/META-INF/NOTICE new file mode 100644 index 00000000000..9c5efcc9e68 --- /dev/null +++ b/examples/extenderbased.circle/src/main/resources/META-INF/NOTICE @@ -0,0 +1,5 @@ +Apache Felix Service-Based Circle Service Example +Copyright 2006 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/examples/extenderbased.circle/src/main/resources/org/apache/felix/example/extenderbased/circle/circle.png b/examples/extenderbased.circle/src/main/resources/org/apache/felix/example/extenderbased/circle/circle.png new file mode 100644 index 00000000000..3d4887eaa2a Binary files /dev/null and b/examples/extenderbased.circle/src/main/resources/org/apache/felix/example/extenderbased/circle/circle.png differ diff --git a/examples/extenderbased.host/pom.xml b/examples/extenderbased.host/pom.xml new file mode 100644 index 00000000000..dd1e3c7802b --- /dev/null +++ b/examples/extenderbased.host/pom.xml @@ -0,0 +1,72 @@ + + + + + org.apache.felix + felix + 1.0.4 + ../../pom/pom.xml + + + 4.0.0 + bundle + Apache Felix Extender-Based Host + A simple application that embeds Felix and uses the extender model for extensibility. + org.apache.felix.example + extenderbased.host + 1.0.0 + + + org.apache.felix + org.apache.felix.framework + 4.0.2 + + + + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + <_donotcopy>(CVS|.svn|config.properties) + org.apache.felix.example.extenderbased.host.launch.Application + !android.*,!dalvik.*,org.osgi.framework,org.osgi.service.packageadmin,org.osgi.service.url,org.osgi.service.startlevel,org.osgi.util.tracker,* + org.apache.felix.example.extenderbased.host.extension.* + org.apache.felix.example.extenderbased.host.* + *;artifactId=org.apache.felix.framework;inline=true + org.apache.felix.example.extenderbased.host.Activator + The Apache Software Foundation + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + diff --git a/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/Activator.java b/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/Activator.java new file mode 100644 index 00000000000..7e771e79016 --- /dev/null +++ b/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/Activator.java @@ -0,0 +1,116 @@ +/* + * 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.example.extenderbased.host; + +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import javax.swing.JFrame; +import javax.swing.SwingUtilities; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; + +/** + * The activator of the host application bundle. The activator creates the + * main application JFrame and starts tracking SimpleShape + * extensions. All activity is performed on the Swing event thread to avoid + * synchronization and repainting issues. Closing the application window + * will result in Bundle.stop() being called on the system bundle, + * which will cause the framework to shutdown and the JVM to exit. +**/ +public class Activator implements BundleActivator +{ + private DrawingFrame m_frame = null; + private ShapeBundleTracker m_shapetracker = null; + + /** + * Displays the applications window and starts extension tracking; + * everything is done on the Swing event thread to avoid synchronization + * and repainting issues. + * @param context The context of the bundle. + **/ + @Override + public void start(final BundleContext context) + { + SwingUtilities.invokeLater(new Runnable() { + // This creates of the application window. + @Override + public void run() + { + m_frame = new DrawingFrame(); + m_frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + m_frame.addWindowListener(new WindowAdapter() + { + @Override + public void windowClosing(WindowEvent evt) + { + try + { + context.getBundle(0).stop(); + } + catch (BundleException ex) + { + ex.printStackTrace(); + } + } + }); + + m_frame.setVisible(true); + + m_shapetracker = new ShapeBundleTracker(context, m_frame); + m_shapetracker.open(); + } + }); + } + + /** + * Stops extension tracking and disposes of the application window. + * @param context The context of the bundle. + **/ + @Override + public void stop(BundleContext context) + { + Runnable runner = new Runnable() { + // This disposes of the application window. + @Override + public void run() + { + m_shapetracker.close(); + m_frame.setVisible(false); + m_frame.dispose(); + } + }; + + if (SwingUtilities.isEventDispatchThread()) + { + runner.run(); + } + else + { + try + { + javax.swing.SwingUtilities.invokeAndWait(runner); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + } +} \ No newline at end of file diff --git a/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/DefaultShape.java b/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/DefaultShape.java new file mode 100644 index 00000000000..4d542c92c1e --- /dev/null +++ b/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/DefaultShape.java @@ -0,0 +1,126 @@ +/* + * 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.example.extenderbased.host; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Point; +import javax.swing.ImageIcon; +import org.apache.felix.example.extenderbased.host.extension.SimpleShape; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; + +/** + * This class is used as a proxy to defer object creation from shape + * provider bundles and also as a placeholder shape when previously + * used shapes are no longer available. These two purposes are + * actually orthogonal, but were combined into a single class to + * reduce the number of classes in the application. The proxy-related + * functionality is introduced as a way to lazily create shape + * objects in an effort to improve performance; this level of + * indirection could be removed if eager creation of objects is not + * a concern. +**/ +class DefaultShape implements SimpleShape +{ + private SimpleShape m_shape; + private ImageIcon m_icon; + private BundleContext m_context; + private long m_bundleId; + private String m_className; + + /** + * This constructs a placeholder shape that draws a default + * icon. It is used when a previously drawn shape is no longer + * available. + **/ + public DefaultShape() + { + // Do nothing. + } + + /** + * This constructs a proxy shape that lazily instantiates the + * shape class from the associated extension bundle. + * @param context The bundle context to use for retrieving the extension bundle. + * @param bundleId The bundle ID of the extension bundle. + * @param className The class name of the shape. + **/ + public DefaultShape(BundleContext context, long bundleId, String className) + { + m_context = context; + m_bundleId = bundleId; + m_className = className; + } + + /** + * Implements the SimpleShape interface method. When acting as + * a proxy, this method lazily loads and instantiates the shape class and + * then uses it to draw the shape. When acting as a placeholder shape, + * this method draws the default icon. + * @param g2 The graphics object used for painting. + * @param p The position to paint the triangle. + **/ + public void draw(Graphics2D g2, Point p) + { + // If this is a proxy shape, instantiate the shape class + // and use it to draw the shape. + if (m_context != null) + { + try + { + if (m_shape == null) + { + // Get the bundle. + Bundle bundle = m_context.getBundle(m_bundleId); + // Load the class and instantiate it. + Class clazz = bundle.loadClass(m_className); + m_shape = (SimpleShape) clazz.newInstance(); + } + // Draw the shape. + m_shape.draw(g2, p); + // If everything was successful, then simply return. + return; + } + catch (Exception ex) + { + // This generally should not happen, but if it does then + // we can just fall through and paint the default icon. + } + } + + // If the proxied shape could not be drawn for any reason or if + // this shape is simply a placeholder, then draw the default icon. + if (m_icon == null) + { + try + { + m_icon = new ImageIcon(this.getClass().getResource("underc.png")); + } + catch (Exception ex) + { + ex.printStackTrace(); + g2.setColor(Color.red); + g2.fillRect(0, 0, 60, 60); + return; + } + } + g2.drawImage(m_icon.getImage(), 0, 0, null); + } +} \ No newline at end of file diff --git a/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/DrawingFrame.java b/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/DrawingFrame.java new file mode 100644 index 00000000000..3b0d7d4da32 --- /dev/null +++ b/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/DrawingFrame.java @@ -0,0 +1,285 @@ +/* + * 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.example.extenderbased.host; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.util.HashMap; +import java.util.Map; +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JToolBar; +import org.apache.felix.example.extenderbased.host.extension.SimpleShape; + +/** + * This class represents the main application class, which is a JFrame subclass + * that manages a toolbar of shapes and a drawing canvas. This class does not + * directly interact with the underlying OSGi framework; instead, it is injected + * with the available SimpleShape instances to eliminate any + * dependencies on the OSGi application programming interfaces. +**/ +public class DrawingFrame extends JFrame + implements MouseListener, MouseMotionListener +{ + private static final long serialVersionUID = 1L; + private static final int BOX = 54; + private final JToolBar m_toolbar; + private String m_selected; + private final JPanel m_panel; + private ShapeComponent m_selectedComponent; + private final Map m_shapes = new HashMap(); + private final SimpleShape m_defaultShape = new DefaultShape(); + private final ActionListener m_reusableActionListener = new ShapeActionListener(); + + /** + * Default constructor that populates the main window. + **/ + public DrawingFrame() + { + super("Extender-Based Host"); + + m_toolbar = new JToolBar("Toolbar"); + m_panel = new JPanel(); + m_panel.setBackground(Color.WHITE); + m_panel.setLayout(null); + m_panel.setMinimumSize(new Dimension(400, 400)); + m_panel.addMouseListener(this); + getContentPane().setLayout(new BorderLayout()); + getContentPane().add(m_toolbar, BorderLayout.NORTH); + getContentPane().add(m_panel, BorderLayout.CENTER); + setSize(400, 400); + } + + /** + * This method sets the currently selected shape to be used for drawing + * on the canvas. + * @param name The name of the shape to use for drawing on the canvas. + **/ + public void selectShape(String name) + { + m_selected = name; + } + + /** + * Retrieves the available SimpleShape associated with the given name. + * @param name The name of the SimpleShape to retrieve. + * @return The corresponding SimpleShape instance if available or + * null. + **/ + public SimpleShape getShape(String name) + { + ShapeInfo info = m_shapes.get(name); + if (info == null) + { + return m_defaultShape; + } + else + { + return info.m_shape; + } + } + + /** + * Injects an available SimpleShape into the drawing frame. + * @param name The name of the injected SimpleShape. + * @param icon The icon associated with the injected SimpleShape. + * @param shape The injected SimpleShape instance. + **/ + public void addShape(String name, Icon icon, SimpleShape shape) + { + m_shapes.put(name, new ShapeInfo(name, icon, shape)); + JButton button = new JButton(icon); + button.setActionCommand(name); + button.addActionListener(m_reusableActionListener); + + if (m_selected == null) + { + button.doClick(); + } + + m_toolbar.add(button); + m_toolbar.validate(); + repaint(); + } + + /** + * Removes a no longer available SimpleShape from the drawing frame. + * @param name The name of the SimpleShape to remove. + **/ + public void removeShape(String name) + { + m_shapes.remove(name); + + if ((m_selected != null) && m_selected.equals(name)) + { + m_selected = null; + } + + for (int i = 0; i < m_toolbar.getComponentCount(); i++) + { + JButton sb = (JButton) m_toolbar.getComponent(i); + if (sb.getActionCommand().equals(name)) + { + m_toolbar.remove(i); + m_toolbar.invalidate(); + validate(); + repaint(); + break; + } + } + + if ((m_selected == null) && (m_toolbar.getComponentCount() > 0)) + { + ((JButton) m_toolbar.getComponent(0)).doClick(); + } + } + + /** + * Implements method for the MouseListener interface to + * draw the selected shape into the drawing canvas. + * @param evt The associated mouse event. + **/ + public void mouseClicked(MouseEvent evt) + { + if (m_selected == null) + { + return; + } + + if (m_panel.contains(evt.getX(), evt.getY())) + { + ShapeComponent sc = new ShapeComponent(this, m_selected); + sc.setBounds(evt.getX() - BOX / 2, evt.getY() - BOX / 2, BOX, BOX); + m_panel.add(sc, 0); + m_panel.validate(); + m_panel.repaint(sc.getBounds()); + } + } + + /** + * Implements an empty method for the MouseListener interface. + * @param evt The associated mouse event. + **/ + public void mouseEntered(MouseEvent evt) + { + } + + /** + * Implements an empty method for the MouseListener interface. + * @param evt The associated mouse event. + **/ + public void mouseExited(MouseEvent evt) + { + } + + /** + * Implements method for the MouseListener interface to initiate + * shape dragging. + * @param evt The associated mouse event. + **/ + public void mousePressed(MouseEvent evt) + { + Component c = m_panel.getComponentAt(evt.getPoint()); + if (c instanceof ShapeComponent) + { + m_selectedComponent = (ShapeComponent) c; + m_panel.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); + m_panel.addMouseMotionListener(this); + m_selectedComponent.repaint(); + } + } + + /** + * Implements method for the MouseListener interface to complete + * shape dragging. + * @param evt The associated mouse event. + **/ + public void mouseReleased(MouseEvent evt) + { + if (m_selectedComponent != null) + { + m_panel.removeMouseMotionListener(this); + m_panel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + m_selectedComponent.setBounds( + evt.getX() - BOX / 2, evt.getY() - BOX / 2, BOX, BOX); + m_selectedComponent.repaint(); + m_selectedComponent = null; + } + } + + /** + * Implements method for the MouseMotionListener interface to + * move a dragged shape. + * @param evt The associated mouse event. + **/ + public void mouseDragged(MouseEvent evt) + { + m_selectedComponent.setBounds( + evt.getX() - BOX / 2, evt.getY() - BOX / 2, BOX, BOX); + } + + /** + * Implements an empty method for the MouseMotionListener + * interface. + * @param evt The associated mouse event. + **/ + public void mouseMoved(MouseEvent evt) + { + } + + /** + * Simple action listener for shape tool bar buttons that sets + * the drawing frame's currently selected shape when receiving + * an action event. + **/ + private class ShapeActionListener implements ActionListener + { + public void actionPerformed(ActionEvent evt) + { + selectShape(evt.getActionCommand()); + } + } + + /** + * This class is used to record the various information pertaining to + * an available shape. + **/ + private static class ShapeInfo + { + public String m_name; + public Icon m_icon; + public SimpleShape m_shape; + public ShapeInfo(String name, Icon icon, SimpleShape shape) + { + m_name = name; + m_icon = icon; + m_shape = shape; + } + } +} \ No newline at end of file diff --git a/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/ShapeBundleTracker.java b/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/ShapeBundleTracker.java new file mode 100644 index 00000000000..93b3a09e8a0 --- /dev/null +++ b/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/ShapeBundleTracker.java @@ -0,0 +1,144 @@ +package org.apache.felix.example.extenderbased.host; + +import java.util.Dictionary; +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.swing.SwingUtilities; +import org.apache.felix.example.extenderbased.host.extension.SimpleShape; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.util.tracker.BundleTracker; + +/** + * This is a simple bundle tracker utility class that tracks active + * bundles. The tracker must be given a bundle context upon creation, + * which it uses to listen for bundle events. The bundle tracker must be + * opened to track objects and closed when it is no longer needed. + * + * @see BundleTracker +**/ +public class ShapeBundleTracker extends BundleTracker +{ + // The application object to notify. + private final DrawingFrame m_frame; + + /** + * Constructs a tracker that uses the specified bundle context to + * track extensions and notifies the specified application object about + * changes. + * @param context The bundle context to be used by the tracker. + * @param frame The application object to notify about extension changes. + **/ + public ShapeBundleTracker(BundleContext context, DrawingFrame frame) + { + // we only want to track active bundles + super(context, Bundle.ACTIVE, null); + this.m_frame = frame; + } + + // Gets called when a bundle in enters the state ACTIVE + @Override + public SimpleShape addingBundle(Bundle bundle, BundleEvent event) + { + // Try to get the name of the extension. + Dictionary dict = bundle.getHeaders(); + String name = dict.get(SimpleShape.NAME_PROPERTY); + + // if the name is not null, bundle is a ShapeBundle + if (name != null) + { + // Get the icon resource of the extension. + String iconPath = dict.get(SimpleShape.ICON_PROPERTY); + Icon icon = new ImageIcon(bundle.getResource(iconPath)); + // Get the class of the extension. + String className = dict.get(SimpleShape.CLASS_PROPERTY); + SimpleShape shape = new DefaultShape(bundle.getBundleContext(), + bundle.getBundleId(), className); + processAdd(name, icon, shape); + return shape; + } + + // bundle is no ShapeBundle, ingore it + return null; + } + + /** + * Util method to process the addition of a new shape. + * + * @param name The name of the new shape + * @param icon The icon of the new shape + * @param shape the shape itself + */ + private void processAdd(final String name, final Icon icon, final SimpleShape shape) + { + try + { + if (SwingUtilities.isEventDispatchThread()) + { + m_frame.addShape(name, icon, shape); + } + else + { + SwingUtilities.invokeAndWait(new Runnable() + { + @Override + public void run() + { + m_frame.addShape(name, icon, shape); + } + }); + } + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + + // Gets called when a bundle leaves the ACTIVE state + @Override + public void removedBundle(Bundle bundle, BundleEvent event, SimpleShape object) + { + // Try to get the name of the extension. + Dictionary dict = bundle.getHeaders(); + String name = dict.get(SimpleShape.NAME_PROPERTY); + + // if the name is not null, bundle is a ShapeBundle + if (name != null) + { + prcoessRemove(name); + } + } + + /** + * Util method to process the removal of a shape. + * + * @param name the name of the shape that is about to be removed. + */ + private void prcoessRemove(final String name) + { + try + { + if (SwingUtilities.isEventDispatchThread()) + { + m_frame.removeShape(name); + } + else + { + SwingUtilities.invokeAndWait(new Runnable() + { + @Override + public void run() + { + m_frame.removeShape(name); + } + }); + } + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/ShapeComponent.java b/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/ShapeComponent.java new file mode 100644 index 00000000000..a3d0f4a0b4a --- /dev/null +++ b/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/ShapeComponent.java @@ -0,0 +1,67 @@ +/* + * 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.example.extenderbased.host; + +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.RenderingHints; +import javax.swing.JComponent; +import org.apache.felix.example.extenderbased.host.extension.SimpleShape; + +/** + * Simple component class used to represent a drawn shape. This component + * uses a shape service to paint its contents. +**/ +public class ShapeComponent extends JComponent +{ + private static final long serialVersionUID = 1L; + private DrawingFrame m_frame; + private String m_shapeName; + + /** + * Construct a component for the specified drawing frame with the specified + * named shape. The component acquires the named shape from the drawing + * frame at the time of painting, which enables it to account for + * service dynamism. + * @param frame The drawing frame associated with the component. + * @param shapeName The name of the shape to draw. + **/ + public ShapeComponent(DrawingFrame frame, String shapeName) + { + m_frame = frame; + m_shapeName = shapeName; + } + + /** + * Paints the contents of the component. The component acquires the named + * shape from the drawing frame at the time of painting, which enables it + * to account for service dynamism. + * @param g The graphics object to use for painting. + **/ + protected void paintComponent(Graphics g) + { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + SimpleShape shape = m_frame.getShape(m_shapeName); + shape.draw(g2, new Point(getWidth()/2, getHeight()/2)); + } +} \ No newline at end of file diff --git a/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/extension/SimpleShape.java b/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/extension/SimpleShape.java new file mode 100644 index 00000000000..ea75de6af53 --- /dev/null +++ b/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/extension/SimpleShape.java @@ -0,0 +1,57 @@ +/* + * 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.example.extenderbased.host.extension; + +import java.awt.Graphics2D; +import java.awt.Point; + +/** + * This interface defines the SimpleShape extension. This extension + * is used to draw shapes. It is defined by three manifest properties: + *

        + *
      • Extension-Name - A String name for the shape. + *
      • + *
      • Extension-Icon - An Icon resource for the shape. + *
      • + *
      • Extension-Class - A Class that implements the shape. + *
      • + *
      +**/ +public interface SimpleShape +{ + /** + * A property for the name of the shape. + **/ + public static final String NAME_PROPERTY = "Extension-Name"; + /** + * A property for the icon of the shape. + **/ + public static final String ICON_PROPERTY = "Extension-Icon"; + /** + * A property for the class of the shape. + **/ + public static final String CLASS_PROPERTY = "Extension-Class"; + + /** + * Method to draw the shape of the extension. + * @param g2 The graphics object used for painting. + * @param p The position to paint the triangle. + **/ + public void draw(Graphics2D g2, Point p); +} \ No newline at end of file diff --git a/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/launch/Application.java b/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/launch/Application.java new file mode 100644 index 00000000000..df52b2fe940 --- /dev/null +++ b/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/launch/Application.java @@ -0,0 +1,127 @@ +/* + * 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.example.extenderbased.host.launch; + +import java.util.Map; +import java.util.ServiceLoader; +import org.apache.felix.example.extenderbased.host.Activator; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.launch.Framework; +import org.osgi.framework.launch.FrameworkFactory; + +/** + * This class provides a static {@code main()} method so that the bundle can be + * run as a stand-alone host application. In such a scenario, the application + * creates its own embedded OSGi framework instance and interacts with the + * internal extensions to providing drawing functionality. To successfully + * launch the stand-alone application, it must be run from this bundle's + * installation directory using "{@code java -jar}". + * The locations of any additional extensions that have to be started, have to + * be passed as command line arguments to this method. + */ +public class Application +{ + private static Framework m_framework = null; + + /** + * Enables the bundle to run as a stand-alone application. When this + * static {@code main()} method is invoked, the application creates + * its own embedded OSGi framework instance and interacts with the + * internal extensions to provide drawing functionality. To successfully + * launch as a stand-alone application, this method should be invoked from + * the bundle's installation directory using "{@code java -jar}". + * The location of any extension that shall be installed can be passed + * as parameters. + *

      + * For example if you build the bundles inside your workspace, maven will + * create a target directory in every project. To start the application + * from within your IDE you should pass: + *

      + *

      +     * {@code file:../extenderbased.circle/target/extenderbased.circle-1.0.0.jar
      +     * file:../extenderbased.square/target/extenderbased.square-1.0.0.jar
      +     * file:../extenderbased.triangle/target/extenderbased.triangle-1.0.0.jar}
      +     * 
      + * + * @param args The locations of additional bundles to start. + **/ + public static void main(String[] args) + { + // args should never be null if the application is run from the command line. Check it anyway. + String[] locations = args != null ? args : new String[0]; + + // Print welcome banner. + System.out.println("\nWelcome to My Launcher"); + System.out.println("======================\n"); + + try + { + Map config = ConfigUtil.createConfig(); + m_framework = createFramework(config); + m_framework.init(); + m_framework.start(); + installAndStartBundles(locations); + m_framework.waitForStop(0); + System.exit(0); + } + catch (Exception ex) + { + System.err.println("Could not create framework: " + ex); + ex.printStackTrace(); + System.exit(-1); + } + } + + /** + * Util method for creating an embedded Framework. Tries to create a {@link FrameworkFactory} + * which is then be used to create the framework. + * + * @param config the configuration to create the framework with + * @return a Framework with the given configuration + */ + private static Framework createFramework(Map config) + { + ServiceLoader factoryLoader = ServiceLoader.load(FrameworkFactory.class); + for(FrameworkFactory factory : factoryLoader){ + return factory.newFramework(config); + } + throw new IllegalStateException("Unable to load FrameworkFactory service."); + } + + /** + * Installs and starts all bundles used by the application. Therefore the host bundle will be started. The locations + * of extensions for the host bundle can be passed in as parameters. + * + * @param bundleLocations the locations where extension for the host bundle are located. Must not be {@code null}! + * @throws BundleException if something went wrong while installing or starting the bundles. + */ + private static void installAndStartBundles(String... bundleLocations) throws BundleException + { + BundleContext bundleContext = m_framework.getBundleContext(); + Activator hostActivator = new Activator(); + hostActivator.start(bundleContext); + for (String location : bundleLocations) + { + Bundle addition = bundleContext.installBundle(location); + addition.start(); + } + } +} \ No newline at end of file diff --git a/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/launch/ConfigUtil.java b/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/launch/ConfigUtil.java new file mode 100644 index 00000000000..dcfdbdad8e9 --- /dev/null +++ b/examples/extenderbased.host/src/main/java/org/apache/felix/example/extenderbased/host/launch/ConfigUtil.java @@ -0,0 +1,116 @@ +/* + * 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.example.extenderbased.host.launch; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.osgi.framework.Constants; + +/** + * Util class for creating the framework configuration + */ +final class ConfigUtil +{ + + /** + * Creates a configuration for the framework. Therefore this method attempts to create + * a temporary cache dir. If creation of the cache dir is successful, it will be added + * to the configuration. + * + * @return + */ + public static Map createConfig() + { + final File cachedir = createCacheDir(); + + Map configMap = new HashMap(); + // Tells the framework to export the extension package, making it accessible + // for the other shape bundels + configMap.put(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, + "org.apache.felix.example.extenderbased.host.extension; version=1.0.0"); + + // if we could create a cache dir, we use it. Otherwise the platform default will be used + if (cachedir != null) + { + configMap.put(Constants.FRAMEWORK_STORAGE, cachedir.getAbsolutePath()); + } + + return configMap; + } + + /** + * Tries to create a temporay cache dir. If creation of the cache dir is successful, + * it will be returned. If creation fails, null will be returned. + * + * @return a {@code File} object representing the cache dir + */ + private static File createCacheDir() + { + final File cachedir; + try + { + cachedir = File.createTempFile("felix.example.extenderbased", null); + cachedir.delete(); + createShutdownHook(cachedir); + return cachedir; + } + catch (IOException e) + { + // temp dir creation failed, return null + return null; + } + } + + /** + * Adds a shutdown hook to the runtime, that will make sure, that the cache dir will + * be deleted after the application has been terminated. + */ + private static void createShutdownHook(final File cachedir) + { + Runtime.getRuntime().addShutdownHook(new Thread() + { + @Override + public void run() + { + deleteFileOrDir(cachedir); + } + }); + } + + + /** + * Utility method used to delete the profile directory when run as + * a stand-alone application. + * @param file The file to recursively delete. + **/ + private static void deleteFileOrDir(File file) + { + if (file.isDirectory()) + { + File[] childs = file.listFiles(); + for (File child : childs) + { + deleteFileOrDir(child); + } + } + file.delete(); + } +} \ No newline at end of file diff --git a/examples/extenderbased.host/src/main/resources/META-INF/LICENSE b/examples/extenderbased.host/src/main/resources/META-INF/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/examples/extenderbased.host/src/main/resources/META-INF/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/examples/extenderbased.host/src/main/resources/META-INF/NOTICE b/examples/extenderbased.host/src/main/resources/META-INF/NOTICE new file mode 100644 index 00000000000..1717c0f6f28 --- /dev/null +++ b/examples/extenderbased.host/src/main/resources/META-INF/NOTICE @@ -0,0 +1,5 @@ +Apache Felix Service-Based Host Example +Copyright 2007 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/examples/extenderbased.host/src/main/resources/org/apache/felix/example/extenderbased/host/underc.png b/examples/extenderbased.host/src/main/resources/org/apache/felix/example/extenderbased/host/underc.png new file mode 100644 index 00000000000..425cdb91f2f Binary files /dev/null and b/examples/extenderbased.host/src/main/resources/org/apache/felix/example/extenderbased/host/underc.png differ diff --git a/examples/extenderbased.square/pom.xml b/examples/extenderbased.square/pom.xml new file mode 100644 index 00000000000..a344355f2c4 --- /dev/null +++ b/examples/extenderbased.square/pom.xml @@ -0,0 +1,69 @@ + + + + + org.apache.felix + felix + 1.0.4 + ../../pom/pom.xml + + + 4.0.0 + bundle + Apache Felix Square Extension + A simple extension for drawing squares. + org.apache.felix.example + extenderbased.square + 1.0.0 + + + org.apache.felix.example + extenderbased.host + 1.0.0 + + + + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + Square + org/apache/felix/example/extenderbased/square/square.png + org.apache.felix.example.extenderbased.square.Square + org.apache.felix.example.extenderbased.square.* + The Apache Software Foundation + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + + diff --git a/examples/extenderbased.square/src/main/java/org/apache/felix/example/extenderbased/square/Square.java b/examples/extenderbased.square/src/main/java/org/apache/felix/example/extenderbased/square/Square.java new file mode 100644 index 00000000000..daa5b995418 --- /dev/null +++ b/examples/extenderbased.square/src/main/java/org/apache/felix/example/extenderbased/square/Square.java @@ -0,0 +1,54 @@ +/* + * 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.example.extenderbased.square; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.geom.Rectangle2D; +import org.apache.felix.example.extenderbased.host.extension.SimpleShape; + +/** + * This class implements the square SimpleShape extension. + * It simply provides a draw() that paints a square. +**/ +public class Square implements SimpleShape +{ + /** + * Implements the SimpleShape.draw() method for painting + * the shape. + * @param g2 The graphics object used for painting. + * @param p The position to paint the triangle. + **/ + public void draw(Graphics2D g2, Point p) + { + int x = p.x - 25; + int y = p.y - 25; + GradientPaint gradient = new GradientPaint( + x, y, Color.BLUE, x + 50, y, Color.WHITE); + g2.setPaint(gradient); + g2.fill(new Rectangle2D.Double(x, y, 50, 50)); + BasicStroke wideStroke = new BasicStroke(2.0f); + g2.setColor(Color.black); + g2.setStroke(wideStroke); + g2.draw(new Rectangle2D.Double(x, y, 50, 50)); + } +} \ No newline at end of file diff --git a/examples/extenderbased.square/src/main/resources/META-INF/LICENSE b/examples/extenderbased.square/src/main/resources/META-INF/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/examples/extenderbased.square/src/main/resources/META-INF/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/examples/extenderbased.square/src/main/resources/META-INF/NOTICE b/examples/extenderbased.square/src/main/resources/META-INF/NOTICE new file mode 100644 index 00000000000..f0fe53c0ceb --- /dev/null +++ b/examples/extenderbased.square/src/main/resources/META-INF/NOTICE @@ -0,0 +1,5 @@ +Apache Felix Service-Based Square Service Example +Copyright 2006 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/examples/extenderbased.square/src/main/resources/org/apache/felix/example/extenderbased/square/square.png b/examples/extenderbased.square/src/main/resources/org/apache/felix/example/extenderbased/square/square.png new file mode 100644 index 00000000000..3f24cfc965c Binary files /dev/null and b/examples/extenderbased.square/src/main/resources/org/apache/felix/example/extenderbased/square/square.png differ diff --git a/examples/extenderbased.triangle/pom.xml b/examples/extenderbased.triangle/pom.xml new file mode 100644 index 00000000000..526f3c38358 --- /dev/null +++ b/examples/extenderbased.triangle/pom.xml @@ -0,0 +1,69 @@ + + + + + org.apache.felix + felix + 1.0.4 + ../../pom/pom.xml + + + 4.0.0 + bundle + Apache Felix Triangle Extension + A simple extension for drawing triangles. + org.apache.felix.example + extenderbased.triangle + 1.0.0 + + + org.apache.felix.example + extenderbased.host + 1.0.0 + + + + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + Triangle + org/apache/felix/example/extenderbased/triangle/triangle.png + org.apache.felix.example.extenderbased.triangle.Triangle + org.apache.felix.example.extenderbased.triangle.* + The Apache Software Foundation + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + + diff --git a/examples/extenderbased.triangle/src/main/java/org/apache/felix/example/extenderbased/triangle/Triangle.java b/examples/extenderbased.triangle/src/main/java/org/apache/felix/example/extenderbased/triangle/Triangle.java new file mode 100644 index 00000000000..c65e94fbfb1 --- /dev/null +++ b/examples/extenderbased.triangle/src/main/java/org/apache/felix/example/extenderbased/triangle/Triangle.java @@ -0,0 +1,63 @@ +/* + * 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.example.extenderbased.triangle; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.geom.GeneralPath; +import org.apache.felix.example.extenderbased.host.extension.SimpleShape; + +/** + * This inner class implements the triangle SimpleShape service. + * It simply provides a draw() that paints a triangle. +**/ +public class Triangle implements SimpleShape +{ + /** + * Implements the SimpleShape.draw() method for painting + * the shape. + * @param g2 The graphics object used for painting. + * @param p The position to paint the triangle. + **/ + public void draw(Graphics2D g2, Point p) + { + int x = p.x - 25; + int y = p.y - 25; + GradientPaint gradient = new GradientPaint( + x, y, Color.GREEN, x + 50, y, Color.WHITE); + g2.setPaint(gradient); + int[] xcoords = { x + 25, x, x + 50 }; + int[] ycoords = { y, y + 50, y + 50 }; + GeneralPath polygon = new GeneralPath(GeneralPath.WIND_EVEN_ODD, xcoords.length); + polygon.moveTo(x + 25, y); + for (int i = 0; i < xcoords.length; i++) + { + polygon.lineTo(xcoords[i], ycoords[i]); + } + polygon.closePath(); + g2.fill(polygon); + BasicStroke wideStroke = new BasicStroke(2.0f); + g2.setColor(Color.black); + g2.setStroke(wideStroke); + g2.draw(polygon); + } +} \ No newline at end of file diff --git a/examples/extenderbased.triangle/src/main/resources/META-INF/LICENSE b/examples/extenderbased.triangle/src/main/resources/META-INF/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/examples/extenderbased.triangle/src/main/resources/META-INF/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/examples/extenderbased.triangle/src/main/resources/META-INF/NOTICE b/examples/extenderbased.triangle/src/main/resources/META-INF/NOTICE new file mode 100644 index 00000000000..6d0c83b51fb --- /dev/null +++ b/examples/extenderbased.triangle/src/main/resources/META-INF/NOTICE @@ -0,0 +1,5 @@ +Apache Felix Service-Based Triangle Service Example +Copyright 2006 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/examples/extenderbased.triangle/src/main/resources/org/apache/felix/example/extenderbased/triangle/triangle.png b/examples/extenderbased.triangle/src/main/resources/org/apache/felix/example/extenderbased/triangle/triangle.png new file mode 100644 index 00000000000..46a52882aef Binary files /dev/null and b/examples/extenderbased.triangle/src/main/resources/org/apache/felix/example/extenderbased/triangle/triangle.png differ diff --git a/examples/frenchdictionary/pom.xml b/examples/frenchdictionary/pom.xml index 13c2fe20d33..ea599e3a7ff 100644 --- a/examples/frenchdictionary/pom.xml +++ b/examples/frenchdictionary/pom.xml @@ -1,48 +1,69 @@ + org.apache.felix felix - 0.8.0-SNAPSHOT + 1.0.4 + ../../pom/pom.xml 4.0.0 - osgi-bundle - Apache Felix Examples: French Dictionary Service + bundle + Apache Felix Example French Dictionary Service org.apache.felix.examples.frenchdictionary + 0.9.0-SNAPSHOT ${pom.groupId} org.osgi.core - ${pom.version} + 1.0.0 provided ${pom.groupId} org.apache.felix.examples.dictionaryservice - ${pom.version} + 0.9.0-SNAPSHOT provided - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} + org.apache.felix + maven-bundle-plugin + 1.4.0 true - - French Dictionary Example - Apache Software Foundation - + + ${pom.artifactId} + Example French Dictionary + The Apache Software Foundation + A bundle that registers a French dictionary service. - - + + org.apache.felix.examples.frenchdictionary.Activator - - - org.osgi.framework, org.apache.felix.examples.dictionaryservice - - + + + org.apache.felix.examples.frenchdictionary.* + + diff --git a/examples/frenchdictionary/src/main/java/org/apache/felix/examples/frenchdictionary/Activator.java b/examples/frenchdictionary/src/main/java/org/apache/felix/examples/frenchdictionary/Activator.java index 56156002844..d0fc23b59e3 100644 --- a/examples/frenchdictionary/src/main/java/org/apache/felix/examples/frenchdictionary/Activator.java +++ b/examples/frenchdictionary/src/main/java/org/apache/felix/examples/frenchdictionary/Activator.java @@ -31,7 +31,7 @@ * implemented by an inner class. This class is identical to the class in * Example 2, except that the dictionary contains French words. * - * @author Felix Project Team + * @author Felix Project Team */ public class Activator implements BundleActivator { diff --git a/examples/jaas/app/pom.xml b/examples/jaas/app/pom.xml new file mode 100644 index 00000000000..89aa747b4ae --- /dev/null +++ b/examples/jaas/app/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + + org.apache.felix + felix-parent + 2.1 + ../../../pom/pom.xml + + + org.apache.felix.example + org.apache.felix.example.jaas.app + 0.0.1-SNAPSHOT + bundle + + JAAS Example - App Module + + + 3.0.3 + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.5 + true + + NONE + + ${project.artifactId} + + org.apache.felix.jaas.boot + + + + + + org.apache.felix + maven-scr-plugin + 1.11.0 + + + generate-scr-scrdescriptor + + scr + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + + + org.osgi + org.osgi.core + 4.2.0 + provided + + + org.osgi + org.osgi.compendium + 4.2.0 + provided + + + org.apache.felix + org.apache.felix.scr.annotations + 1.9.0 + provided + + + javax.servlet + servlet-api + 2.4 + provided + + + org.apache.felix + org.apache.felix.jaas + 0.0.1-SNAPSHOT + provided + + + + \ No newline at end of file diff --git a/examples/jaas/app/src/main/java/org/apache/felix/example/jaas/app/internal/BootClasspathDemoServlet.java b/examples/jaas/app/src/main/java/org/apache/felix/example/jaas/app/internal/BootClasspathDemoServlet.java new file mode 100644 index 00000000000..ef3bb8ee3c6 --- /dev/null +++ b/examples/jaas/app/src/main/java/org/apache/felix/example/jaas/app/internal/BootClasspathDemoServlet.java @@ -0,0 +1,82 @@ +/* + * 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.example.jaas.app.internal; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Property; +import org.apache.felix.scr.annotations.Service; + +@Component +@Service(value = Servlet.class) +@Property(name = "alias",value = "/jaas/boot") +public class BootClasspathDemoServlet extends HttpServlet +{ + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException + { + //Demonstrates the JAAS authentication if the + // 1. Felix JAAS Boot support is used. + // 2. Global configuration policy is used - This needs to be enabled via + // configuration of 'Apache Felix JAAS Configuration SPI' + // + //In that case client code does not have to + //manage Thread Context Classloader or provide explicit configuration + + PrintWriter pw = resp.getWriter(); + CallbackHandler handler = new ServletRequestCallbackHandler(req); + + Subject subject = new Subject(); + try + { + LoginContext lc = new LoginContext("sample", subject, handler); + lc.login(); + + pw.println("Principal authentication successful"); + pw.println(subject); + } + catch (LoginException e) + { + handleAuthenticationFailure(e,pw); + } + + } + + private void handleAuthenticationFailure(LoginException e, PrintWriter pw) + { + pw.println("Authentication Failed"); + pw.println(e); + } + +} diff --git a/examples/jaas/app/src/main/java/org/apache/felix/example/jaas/app/internal/FactoryDemoServlet.java b/examples/jaas/app/src/main/java/org/apache/felix/example/jaas/app/internal/FactoryDemoServlet.java new file mode 100644 index 00000000000..851d5e79884 --- /dev/null +++ b/examples/jaas/app/src/main/java/org/apache/felix/example/jaas/app/internal/FactoryDemoServlet.java @@ -0,0 +1,83 @@ +/* + * 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.example.jaas.app.internal; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.felix.jaas.LoginContextFactory; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Property; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.Service; + +@Component +@Service(value = Servlet.class) +@Property(name = "alias",value = "/jaas/factory") +public class FactoryDemoServlet extends HttpServlet +{ + @Reference + private LoginContextFactory loginContextFactory; + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException + { + + //Demonstrates the JAAS authentication if the + // 1. Felix JAAS LoginContextFactory is used. + //In that case client code does not have to + //manage Thread Context Classloader or provide explicit configuration + + PrintWriter pw = resp.getWriter(); + CallbackHandler handler = new ServletRequestCallbackHandler(req); + Subject subject = new Subject(); + try + { + LoginContext lc = loginContextFactory.createLoginContext("sample",subject,handler); + lc.login(); + + pw.println("Principal authentication successful"); + pw.println(subject); + } + catch (LoginException e) + { + handleAuthenticationFailure(e,pw); + } + + } + + private void handleAuthenticationFailure(LoginException e, PrintWriter pw) + { + pw.println("Authentication Failed"); + pw.println(e); + } + +} diff --git a/examples/jaas/app/src/main/java/org/apache/felix/example/jaas/app/internal/GlobalConfigDemoServlet.java b/examples/jaas/app/src/main/java/org/apache/felix/example/jaas/app/internal/GlobalConfigDemoServlet.java new file mode 100644 index 00000000000..ba874a4c9a0 --- /dev/null +++ b/examples/jaas/app/src/main/java/org/apache/felix/example/jaas/app/internal/GlobalConfigDemoServlet.java @@ -0,0 +1,86 @@ +/* + * 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.example.jaas.app.internal; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Property; +import org.apache.felix.scr.annotations.Service; + +@Component +@Service(value = Servlet.class) +@Property(name = "alias",value = "/jaas/global") +public class GlobalConfigDemoServlet extends HttpServlet +{ + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException + { + + //Demonstrates the JAAS authentication if the + // 1. Replace Global configuration policy is used + // + //This needs to be enabled via configuration of 'Apache Felix JAAS Configuration SPI' + //In that case client code does not have to provide explicit configuration + + PrintWriter pw = resp.getWriter(); + CallbackHandler handler = new ServletRequestCallbackHandler(req); + + Subject subject = new Subject(); + final ClassLoader cl = Thread.currentThread().getContextClassLoader(); + try + { + Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); + LoginContext lc = new LoginContext("sample", subject, handler); + lc.login(); + + pw.println("Principal authentication successful"); + pw.println(subject); + } + catch (LoginException e) + { + handleAuthenticationFailure(e,pw); + } + finally + { + Thread.currentThread().setContextClassLoader(cl); + } + } + + private void handleAuthenticationFailure(LoginException e, PrintWriter pw) + { + pw.println("Authentication Failed"); + pw.println(e); + } + +} diff --git a/examples/jaas/app/src/main/java/org/apache/felix/example/jaas/app/internal/ServletRequestCallbackHandler.java b/examples/jaas/app/src/main/java/org/apache/felix/example/jaas/app/internal/ServletRequestCallbackHandler.java new file mode 100644 index 00000000000..f7920f638fc --- /dev/null +++ b/examples/jaas/app/src/main/java/org/apache/felix/example/jaas/app/internal/ServletRequestCallbackHandler.java @@ -0,0 +1,68 @@ +/* + * 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.example.jaas.app.internal; + +import java.io.IOException; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.servlet.http.HttpServletRequest; + +public class ServletRequestCallbackHandler implements CallbackHandler +{ + + private final HttpServletRequest request; + + public ServletRequestCallbackHandler(HttpServletRequest request) + { + this.request = request; + } + + @Override + public void handle(Callback[] callbacks) throws IOException, + UnsupportedCallbackException + { + for (Callback c : callbacks) + { + if (c instanceof NameCallback) + { + ((NameCallback) c).setName(getParam("j_username")); + } + if (c instanceof PasswordCallback) + { + ((PasswordCallback) c).setPassword(getParam("j_password").toCharArray()); + } + } + } + + private String getParam(String name) + { + String value = request.getParameter(name); + if (value == null) + { + throw new IllegalArgumentException("No parameter with name [" + name + + "] found"); + } + return value.trim(); + } +} diff --git a/examples/jaas/app/src/main/java/org/apache/felix/example/jaas/app/internal/TCCLDemoServlet.java b/examples/jaas/app/src/main/java/org/apache/felix/example/jaas/app/internal/TCCLDemoServlet.java new file mode 100644 index 00000000000..cb8c70fdfa6 --- /dev/null +++ b/examples/jaas/app/src/main/java/org/apache/felix/example/jaas/app/internal/TCCLDemoServlet.java @@ -0,0 +1,100 @@ +/* + * 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.example.jaas.app.internal; + +import java.io.IOException; +import java.io.PrintWriter; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Property; +import org.apache.felix.scr.annotations.Service; + +@Component +@Service(value = Servlet.class) +@Property(name = "alias",value = "/jaas/tccl") +public class TCCLDemoServlet extends HttpServlet +{ + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException + { + + //Demonstrates the JAAS authentication + //In following case the client code would have to + // 1. Manage the thread's context classloader + // 2. Add a DynamicImport for org.apache.felix.jaas.boot + // 3. Fetch the config using the Configuration.getInstance API and pass that on + + PrintWriter pw = resp.getWriter(); + CallbackHandler handler = new ServletRequestCallbackHandler(req); + + Subject subject = new Subject(); + final ClassLoader cl = Thread.currentThread().getContextClassLoader(); + try + { + Configuration config = Configuration.getInstance("JavaLoginConfig", null, + "FelixJaasProvider"); + Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); + LoginContext lc = new LoginContext("sample", subject, handler, config); + lc.login(); + + pw.println("Principal authentication successful"); + pw.println(subject); + } + catch (NoSuchAlgorithmException e) + { + throw new RuntimeException(e); + } + catch (NoSuchProviderException e) + { + throw new RuntimeException(e); + } + catch (LoginException e) + { + handleAuthenticationFailure(e,pw); + } + finally + { + Thread.currentThread().setContextClassLoader(cl); + } + + } + + private void handleAuthenticationFailure(LoginException e, PrintWriter pw) + { + pw.println("Authentication Failed"); + pw.println(e); + } + +} diff --git a/examples/jaas/jdbc-h2/pom.xml b/examples/jaas/jdbc-h2/pom.xml new file mode 100644 index 00000000000..0444809f1ff --- /dev/null +++ b/examples/jaas/jdbc-h2/pom.xml @@ -0,0 +1,124 @@ + + + 4.0.0 + + + org.apache.felix + felix-parent + 2.1 + ../../../pom/pom.xml + + + org.apache.felix.example + org.apache.felix.example.jaas.jdbc-h2 + 0.0.1-SNAPSHOT + bundle + + JAAS Example - H2 Db + + + 3.0.3 + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.5 + true + + NONE + + ${project.artifactId} + org.apache.felix.example.jaas.jdbc.internal.H2Activator + + h2 + + + org.osgi.service.jdbc.*; + org.apache.lucene.*; + javax.transaction.*;resolution:=optional, + * + + + + + + org.apache.felix + maven-scr-plugin + 1.11.0 + + + generate-scr-scrdescriptor + + scr + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + org.apache.rat + apache-rat-plugin + + + src/** + + + src/main/resources/*.csv + + + + + + + + + org.osgi + org.osgi.core + 4.2.0 + provided + + + org.osgi + org.osgi.compendium + 4.2.0 + provided + + + org.apache.felix + org.apache.felix.scr.annotations + 1.9.0 + provided + + + javax.servlet + servlet-api + 2.4 + provided + + + com.h2database + h2 + 1.3.171 + provided + + + org.slf4j + slf4j-api + 1.6.4 + provided + + + + \ No newline at end of file diff --git a/examples/jaas/jdbc-h2/src/main/java/org/apache/felix/example/jaas/jdbc/internal/H2Activator.java b/examples/jaas/jdbc-h2/src/main/java/org/apache/felix/example/jaas/jdbc/internal/H2Activator.java new file mode 100644 index 00000000000..321b16d8d46 --- /dev/null +++ b/examples/jaas/jdbc-h2/src/main/java/org/apache/felix/example/jaas/jdbc/internal/H2Activator.java @@ -0,0 +1,96 @@ +/* + * 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.example.jaas.jdbc.internal; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Dictionary; +import java.util.Hashtable; + +import javax.servlet.Servlet; +import javax.sql.DataSource; + +import org.h2.Driver; +import org.h2.jdbcx.JdbcDataSource; +import org.h2.server.web.WebServlet; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class H2Activator implements BundleActivator +{ + private Logger log = LoggerFactory.getLogger(getClass()); + private JdbcDataSource ds; + private Connection connection; + + @Override + public void start(BundleContext context) throws Exception + { + ds = new JdbcDataSource(); + ds.setURL("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); + + Dictionary props = new Hashtable(); + props.put("dataSourceName", "test"); + context.registerService(DataSource.class.getName(), ds, props); + + loadData(ds); + + //Register the H2 console servlet + Dictionary servletProps = new Hashtable(); + servletProps.put("alias", "/h2"); + servletProps.put("init.webAllowOthers", "true"); + + context.registerService(Servlet.class.getName(), new WebServlet(), servletProps); + } + + private void loadData(JdbcDataSource ds) throws SQLException + { + //Load the default data of user and roles + connection = ds.getConnection(); + Statement stmt = connection.createStatement(); + stmt.execute("CREATE TABLE USERS AS SELECT * FROM CSVREAD('classpath:users.csv',null,'lineComment=#')"); + stmt.execute("CREATE TABLE ROLES AS SELECT * FROM CSVREAD('classpath:roles.csv',null,'lineComment=#')"); + stmt.close(); + log.info("Successfully imported default user and roles"); + } + + @Override + public void stop(BundleContext context) throws Exception + { + if (connection != null) + { + Statement stat = connection.createStatement(); + stat.execute("SHUTDOWN"); + stat.close(); + + try + { + connection.close(); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + Driver.unload(); + } +} diff --git a/examples/jaas/jdbc-h2/src/main/resources/roles.csv b/examples/jaas/jdbc-h2/src/main/resources/roles.csv new file mode 100644 index 00000000000..b073c0973b9 --- /dev/null +++ b/examples/jaas/jdbc-h2/src/main/resources/roles.csv @@ -0,0 +1,5 @@ +#Default role to user mapping +ROLE,USERNAME +ADMIN,foo +ADMIN,admin +USER,bob \ No newline at end of file diff --git a/examples/jaas/jdbc-h2/src/main/resources/users.csv b/examples/jaas/jdbc-h2/src/main/resources/users.csv new file mode 100644 index 00000000000..83f16765ded --- /dev/null +++ b/examples/jaas/jdbc-h2/src/main/resources/users.csv @@ -0,0 +1,4 @@ +USERNAME,PASSWORD +foo,bar +bob,password +admin,password \ No newline at end of file diff --git a/examples/jaas/launcher/pom.xml b/examples/jaas/launcher/pom.xml new file mode 100644 index 00000000000..84c186ea126 --- /dev/null +++ b/examples/jaas/launcher/pom.xml @@ -0,0 +1,112 @@ + + + + + 4.0.0 + + + org.apache.felix + felix-parent + 2.1 + ../../../pom/pom.xml + + + org.apache.felix.example + org.apache.felix.example.jaas.launcher + 0.0.1-SNAPSHOT + jar + + JAAS Example - Launcher Module + + + ${project.version} + + + + + + org.apache.sling + maven-launchpad-plugin + 2.2.0 + + + prepare-package-jar + + prepare-package + + + false + + + + create-bundle-jar + + create-bundle-jar + + + false + + + + ${project.build.directory}/maven-shared-archive-resources + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + create-standalone-jar + + jar + + + standalone + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + true + + + + + + + + + + + + + org.apache.sling + org.apache.sling.launchpad.base + 2.5.0 + app + provided + + + \ No newline at end of file diff --git a/examples/jaas/launcher/src/main/bundles/list.xml b/examples/jaas/launcher/src/main/bundles/list.xml new file mode 100644 index 00000000000..2762b335cdd --- /dev/null +++ b/examples/jaas/launcher/src/main/bundles/list.xml @@ -0,0 +1,167 @@ + + + + + + + + org.apache.felix + org.apache.felix.http.whiteboard + 2.2.0 + + + commons-io + commons-io + 1.4 + + + commons-fileupload + commons-fileupload + 1.2.2 + + + + org.apache.felix + org.apache.felix.jaas + ${jaas.example.version} + + + org.apache.felix.example + org.apache.felix.example.jaas.app + ${jaas.example.version} + + + org.apache.felix.example + org.apache.felix.example.jaas.lm-config + ${jaas.example.version} + + + org.apache.felix.example + org.apache.felix.example.jaas.lm-jdbc + ${jaas.example.version} + + + org.apache.felix.example + org.apache.felix.example.jaas.jdbc-h2 + ${jaas.example.version} + + + + + org.apache.sling + org.apache.sling.settings + 1.2.2 + + + org.apache.sling + org.apache.sling.launchpad.installer + 1.2.0 + + + org.apache.sling + org.apache.sling.installer.api + 1.0.0 + + + org.apache.sling + org.apache.sling.installer.core + 3.4.6 + + + org.apache.sling + org.apache.sling.installer.console + 1.0.0 + + + org.apache.sling + org.apache.sling.installer.factory.configuration + 1.0.10 + + + org.apache.sling + org.apache.sling.installer.provider.file + 1.0.2 + + + + + org.slf4j + slf4j-api + 1.6.4 + + + org.apache.sling + org.apache.sling.commons.log + 3.0.0 + + + org.apache.sling + org.apache.sling.commons.logservice + 1.0.2 + + + org.slf4j + jcl-over-slf4j + 1.6.4 + + + org.slf4j + log4j-over-slf4j + 1.6.4 + + + + + + org.apache.felix + org.apache.felix.webconsole + 4.2.0 + + + org.apache.geronimo.bundles + json + 20090211_1 + + + org.apache.felix + org.apache.felix.webconsole.plugins.ds + 1.0.0 + + + + + + org.apache.felix + org.apache.felix.scr + 1.6.0 + + + org.apache.felix + org.apache.felix.configadmin + 1.6.0 + + + org.apache.felix + org.apache.felix.metatype + 1.0.6 + + + + diff --git a/examples/jaas/launcher/src/main/config/org.apache.felix.example.jaas.jdbc.factory-h2.cfg b/examples/jaas/launcher/src/main/config/org.apache.felix.example.jaas.jdbc.factory-h2.cfg new file mode 100644 index 00000000000..e0e89806e2b --- /dev/null +++ b/examples/jaas/launcher/src/main/config/org.apache.felix.example.jaas.jdbc.factory-h2.cfg @@ -0,0 +1,22 @@ +# +# 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. +# +jaas.realmName=sample +jaas.controlFlag=sufficient + +datasourceName=test diff --git a/examples/jaas/launcher/src/main/config/org.apache.felix.jaas.Configuration.factory-simple.cfg b/examples/jaas/launcher/src/main/config/org.apache.felix.jaas.Configuration.factory-simple.cfg new file mode 100644 index 00000000000..055c554616d --- /dev/null +++ b/examples/jaas/launcher/src/main/config/org.apache.felix.jaas.Configuration.factory-simple.cfg @@ -0,0 +1,23 @@ +# +# 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. +# + +jaas.classname=org.apache.felix.example.jaas.config.internal.SampleConfigLoginModule +jaas.controlFlag=sufficient +jaas.ranking=0 +jaas.realmName=sample diff --git a/examples/jaas/launcher/src/main/sling/common.properties b/examples/jaas/launcher/src/main/sling/common.properties new file mode 100644 index 00000000000..4e2d21226cc --- /dev/null +++ b/examples/jaas/launcher/src/main/sling/common.properties @@ -0,0 +1,22 @@ +# +# 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. +# +# suppress inspection "UnusedProperty" for whole file +sling.bootdelegation.class.org.apache.felix.jaas.boot.ProxyLoginModule = \ + org.apache.felix.jaas.boot +sling.home = jaas-sample diff --git a/examples/jaas/lm-config/pom.xml b/examples/jaas/lm-config/pom.xml new file mode 100644 index 00000000000..80c2cb69614 --- /dev/null +++ b/examples/jaas/lm-config/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + + org.apache.felix + felix-parent + 2.1 + ../../../pom/pom.xml + + + org.apache.felix.example + org.apache.felix.example.jaas.lm-config + 0.0.1-SNAPSHOT + bundle + + JAAS Example - Config based Login Module + + + + + org.apache.felix + maven-bundle-plugin + 2.3.5 + true + + NONE + + ${project.artifactId} + org.apache.felix.example.jaas.config.internal.SampleConfigLoginModule + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.5.1 + + 1.6 + 1.6 + + + + + + + + \ No newline at end of file diff --git a/examples/jaas/lm-config/src/main/java/org/apache/felix/example/jaas/config/internal/SampleConfigLoginModule.java b/examples/jaas/lm-config/src/main/java/org/apache/felix/example/jaas/config/internal/SampleConfigLoginModule.java new file mode 100644 index 00000000000..79c9c082c78 --- /dev/null +++ b/examples/jaas/lm-config/src/main/java/org/apache/felix/example/jaas/config/internal/SampleConfigLoginModule.java @@ -0,0 +1,105 @@ +/* + * 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.example.jaas.config.internal; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +public class SampleConfigLoginModule implements LoginModule +{ + private Subject subject; + private CallbackHandler handler; + private Map options; + private Map sharedState; + private boolean succeeded; + private String name; + + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, + Map sharedState, Map options) + { + this.subject = subject; + this.options = options; + this.handler = callbackHandler; + this.sharedState = sharedState; + } + + @Override + public boolean login() throws LoginException + { + Callback[] callbacks = new Callback[2]; + callbacks[0] = new NameCallback("Name"); + callbacks[1] = new PasswordCallback("Password", false); + + try + { + handler.handle(callbacks); + } + catch (IOException e) + { + throw new LoginException(e.getMessage()); + } + catch (UnsupportedCallbackException e) + { + throw new LoginException(e.getMessage()); + } + + String name = ((NameCallback) callbacks[0]).getName(); + char[] password = ((PasswordCallback) callbacks[1]).getPassword(); + + boolean result = Arrays.equals(name.toCharArray(), password); + succeeded = result; + this.name = name; + return result; + } + + @Override + public boolean commit() throws LoginException + { + if (succeeded) + { + subject.getPrincipals().add(new SamplePrincipal(name, "SampleConfigLoginModule")); + return true; + } + return false; + } + + @Override + public boolean abort() throws LoginException + { + return true; + } + + @Override + public boolean logout() throws LoginException + { + return false; + } +} diff --git a/examples/jaas/lm-config/src/main/java/org/apache/felix/example/jaas/config/internal/SamplePrincipal.java b/examples/jaas/lm-config/src/main/java/org/apache/felix/example/jaas/config/internal/SamplePrincipal.java new file mode 100644 index 00000000000..593236292d0 --- /dev/null +++ b/examples/jaas/lm-config/src/main/java/org/apache/felix/example/jaas/config/internal/SamplePrincipal.java @@ -0,0 +1,68 @@ +/* + * 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.example.jaas.config.internal; + +import java.security.Principal; + +public class SamplePrincipal implements Principal +{ + private final String name; + private final String prefix; + + public SamplePrincipal(String name, String prefix) + { + this.name = name; + this.prefix = prefix; + } + + @Override + public String getName() + { + return name; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + SamplePrincipal that = (SamplePrincipal) o; + + if (!name.equals(that.name)) + return false; + + return true; + } + + @Override + public int hashCode() + { + return name.hashCode(); + } + + @Override + public String toString() + { + return prefix + ":" + name; + } +} diff --git a/examples/jaas/lm-jdbc/pom.xml b/examples/jaas/lm-jdbc/pom.xml new file mode 100644 index 00000000000..9a0d016fddd --- /dev/null +++ b/examples/jaas/lm-jdbc/pom.xml @@ -0,0 +1,104 @@ + + + 4.0.0 + + + org.apache.felix + felix-parent + 2.1 + ../../../pom/pom.xml + + + org.apache.felix.example + org.apache.felix.example.jaas.lm-jdbc + 0.0.1-SNAPSHOT + bundle + + JAAS Example - JDBC based Login Module + + + 3.0.3 + + + + + + org.apache.felix + maven-bundle-plugin + 2.3.5 + true + + + ${project.artifactId} + + org.apache.sling.commons.osgi;inline= + org/apache/sling/commons/osgi/PropertiesUtil*.class + + + + + + org.apache.felix + maven-scr-plugin + 1.11.0 + + + generate-scr-scrdescriptor + + scr + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + + + org.osgi + org.osgi.core + 4.2.0 + provided + + + org.osgi + org.osgi.compendium + 4.2.0 + provided + + + org.apache.felix + org.apache.felix.scr.annotations + 1.9.0 + provided + + + org.apache.felix + org.apache.felix.jaas + 0.0.1-SNAPSHOT + provided + + + org.apache.sling + org.apache.sling.commons.osgi + 2.2.0 + + + org.slf4j + slf4j-api + 1.6.4 + provided + + + + \ No newline at end of file diff --git a/examples/jaas/lm-jdbc/src/main/java/org/apache/felix/example/jaas/jdbc/JdbcLoginModule.java b/examples/jaas/lm-jdbc/src/main/java/org/apache/felix/example/jaas/jdbc/JdbcLoginModule.java new file mode 100644 index 00000000000..39c93bcd151 --- /dev/null +++ b/examples/jaas/lm-jdbc/src/main/java/org/apache/felix/example/jaas/jdbc/JdbcLoginModule.java @@ -0,0 +1,187 @@ +/* + * 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.example.jaas.jdbc; + +import java.io.IOException; +import java.security.Principal; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; +import javax.sql.DataSource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The code is based on org.apache.karaf.jaas.modules.jdbc.JDBCLoginModule + */ +public class JdbcLoginModule implements LoginModule { + private static Logger log = LoggerFactory.getLogger(JdbcLoginModule.class); + private final DataSource dataSource; + private CallbackHandler callbackHandler; + private Set principals; + private boolean detailedLoginExcepion; + private Subject subject; + private final String passwordQuery; + private final String roleQuery; + + public JdbcLoginModule(DataSource dataSource, String passwordQuery, String roleQuery) { + this.dataSource = dataSource; + this.passwordQuery = passwordQuery; + this.roleQuery = roleQuery; + } + + + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, + Map sharedState, Map options) { + this.callbackHandler = callbackHandler; + this.subject = subject; + + } + + @Override + public boolean login() throws LoginException { + Connection connection = null; + + PreparedStatement passwordStatement = null; + PreparedStatement roleStatement = null; + + ResultSet passwordResultSet = null; + ResultSet roleResultSet = null; + + Callback[] callbacks = new Callback[2]; + callbacks[0] = new NameCallback("Username: "); + callbacks[1] = new PasswordCallback("Password: ", false); + + try { + callbackHandler.handle(callbacks); + } catch (IOException ioe) { + throw new LoginException(ioe.getMessage()); + } catch (UnsupportedCallbackException uce) { + throw new LoginException(uce.getMessage() + " not available to obtain information from user"); + } + + String user = ((NameCallback) callbacks[0]).getName(); + + char[] tmpPassword = ((PasswordCallback) callbacks[1]).getPassword(); + if (tmpPassword == null) { + tmpPassword = new char[0]; + } + + String password = new String(tmpPassword); + principals = new HashSet(); + + try { + connection = dataSource.getConnection(); + + //Retrieve user credentials from database. + passwordStatement = connection.prepareStatement(passwordQuery); + passwordStatement.setString(1, user); + passwordResultSet = passwordStatement.executeQuery(); + + if (!passwordResultSet.next()) { + if (!this.detailedLoginExcepion) { + throw new LoginException("login failed"); + } else { + throw new LoginException("Password for " + user + " does not match"); + } + } else { + String storedPassword = passwordResultSet.getString(1); + + if (!checkPassword(password, storedPassword)) { + if (!this.detailedLoginExcepion) { + throw new LoginException("login failed"); + } else { + throw new LoginException("Password for " + user + " does not match"); + } + } + principals.add(new UserPrincipal(user)); + } + + //Retrieve user roles from database + roleStatement = connection.prepareStatement(roleQuery); + roleStatement.setString(1, user); + roleResultSet = roleStatement.executeQuery(); + while (roleResultSet.next()) { + String role = roleResultSet.getString(1); + principals.add(new RolePrincipal(role)); + } + } catch (Exception ex) { + throw new LoginException("Error has occured while retrieving credentials from database:" + ex.getMessage()); + } finally { + try { + if (passwordResultSet != null) { + passwordResultSet.close(); + } + if (passwordStatement != null) { + passwordStatement.close(); + } + if (roleResultSet != null) { + roleResultSet.close(); + } + if (roleStatement != null) { + roleStatement.close(); + } + if (connection != null) { + connection.close(); + } + } catch (SQLException ex) { + log.warn("Failed to clearly close connection to the database:", ex); + } + } + return true; + } + + private boolean checkPassword(String password, String storedPassword) { + return password.equals(storedPassword); + } + + @Override + public boolean commit() throws LoginException { + subject.getPrincipals().addAll(principals); + return true; + } + + @Override + public boolean abort() throws LoginException { + subject.getPrincipals().removeAll(principals); + principals.clear(); + return true; + } + + @Override + public boolean logout() throws LoginException { + return false; + } +} diff --git a/examples/jaas/lm-jdbc/src/main/java/org/apache/felix/example/jaas/jdbc/JdbcLoginModuleFactory.java b/examples/jaas/lm-jdbc/src/main/java/org/apache/felix/example/jaas/jdbc/JdbcLoginModuleFactory.java new file mode 100644 index 00000000000..1cc93e46bb5 --- /dev/null +++ b/examples/jaas/lm-jdbc/src/main/java/org/apache/felix/example/jaas/jdbc/JdbcLoginModuleFactory.java @@ -0,0 +1,139 @@ +/* + * 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.example.jaas.jdbc; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; + +import javax.security.auth.spi.LoginModule; +import javax.sql.DataSource; + +import org.apache.felix.jaas.LoginModuleFactory; +import org.apache.felix.scr.annotations.Activate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.ConfigurationPolicy; +import org.apache.felix.scr.annotations.Deactivate; +import org.apache.felix.scr.annotations.Property; +import org.apache.felix.scr.annotations.PropertyOption; +import org.apache.sling.commons.osgi.PropertiesUtil; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceRegistration; +import org.osgi.util.tracker.ServiceTracker; + +@Component(label = "%jdbc.name", + description = "%jdbc.description", + metatype = true, + name = JdbcLoginModuleFactory.SERVICE_PID, + configurationFactory = true, + specVersion = "1.1", + policy = ConfigurationPolicy.REQUIRE +) +public class JdbcLoginModuleFactory implements LoginModuleFactory +{ + + public static final String SERVICE_PID = " org.apache.felix.example.jaas.jdbc.factory"; + + @Property(value = "required", options = { + @PropertyOption(name = "required", value = "%jaas.flag.required"), + @PropertyOption(name = "requisite", value = "%jaas.flag.requisite"), + @PropertyOption(name = "sufficient", value = "%jaas.flag.sufficient"), + @PropertyOption(name = "optional", value = "%jaas.flag.optional") }) + static final String JAAS_CONTROL_FLAG = "jaas.controlFlag"; + + @Property(intValue = 0) + static final String JAAS_RANKING = "jaas.ranking"; + + @Property + private static final String PROP_REALM = "jaas.realmName"; + + private static final String DEFAULT_PWD_QUERY = "SELECT PASSWORD FROM USERS WHERE USERNAME=?"; + @Property(value = DEFAULT_PWD_QUERY) + private static final String PROP_PWD_QUERY = "query.pwd"; + private String passwordQuery; + + private static final String DEFAULT_ROLE_QUERY = "SELECT ROLE FROM ROLES WHERE USERNAME=?"; + @Property(value = DEFAULT_ROLE_QUERY) + private static final String PROP_ROLE_QUERY = "query.role"; + private String roleQuery; + + private static final String DEFAULT_DS_NAME = "test"; + @Property + private static final String PROP_DS_NAME = "datasourceName"; + private String datasourceName; + private ServiceTracker dataSourceTracker; + + private ServiceRegistration loginModuleFactoryReg; + + @Activate + public void activate(BundleContext context, Map conf) + throws InvalidSyntaxException + { + passwordQuery = PropertiesUtil.toString(conf.get(PROP_PWD_QUERY), + DEFAULT_PWD_QUERY); + roleQuery = PropertiesUtil.toString(conf.get(PROP_ROLE_QUERY), DEFAULT_ROLE_QUERY); + datasourceName = PropertiesUtil.toString(conf.get(PROP_DS_NAME), DEFAULT_DS_NAME); + + Filter filter = context.createFilter("(&(objectClass=javax.sql.DataSource)" + + "(dataSourceName=" + datasourceName + "))"); + dataSourceTracker = new ServiceTracker(context, filter, null); + dataSourceTracker.open(); + registerLoginModuleFactory(context, conf); + } + + @Deactivate + private void deactivate() + { + if (loginModuleFactoryReg != null) + { + loginModuleFactoryReg.unregister(); + } + + if(dataSourceTracker != null) + { + dataSourceTracker.close(); + } + } + + private void registerLoginModuleFactory(BundleContext context, Map config) + { + Dictionary lmProps = new Hashtable(); + + String controlFlag = PropertiesUtil.toString(config.get(JAAS_CONTROL_FLAG), + "required"); + lmProps.put(LoginModuleFactory.JAAS_CONTROL_FLAG,controlFlag); + lmProps.put(LoginModuleFactory.JAAS_REALM_NAME, PropertiesUtil.toString(config.get(PROP_REALM), null)); + lmProps.put(Constants.SERVICE_RANKING, + PropertiesUtil.toInteger(config.get(JAAS_RANKING), 0)); + + loginModuleFactoryReg = context.registerService( + LoginModuleFactory.class.getName(), this, lmProps); + } + + @Override + public LoginModule createLoginModule() + { + return new JdbcLoginModule( + (DataSource) dataSourceTracker.getService(), passwordQuery, roleQuery); + } +} diff --git a/examples/jaas/lm-jdbc/src/main/java/org/apache/felix/example/jaas/jdbc/RolePrincipal.java b/examples/jaas/lm-jdbc/src/main/java/org/apache/felix/example/jaas/jdbc/RolePrincipal.java new file mode 100644 index 00000000000..ddc835ccfe4 --- /dev/null +++ b/examples/jaas/lm-jdbc/src/main/java/org/apache/felix/example/jaas/jdbc/RolePrincipal.java @@ -0,0 +1,58 @@ +/* + * 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.example.jaas.jdbc; + +import java.security.Principal; + +public class RolePrincipal implements Principal { + + private final String name; + + public RolePrincipal(String name) { + assert name != null; + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof RolePrincipal)) return false; + + RolePrincipal that = (RolePrincipal) o; + + if (name != null ? !name.equals(that.name) : that.name != null) return false; + + return true; + } + + @Override + public int hashCode() { + return name != null ? name.hashCode() : 0; + } + + @Override + public String toString() { + return "RolePrincipal[" + name + "]"; + } + +} diff --git a/examples/jaas/lm-jdbc/src/main/java/org/apache/felix/example/jaas/jdbc/UserPrincipal.java b/examples/jaas/lm-jdbc/src/main/java/org/apache/felix/example/jaas/jdbc/UserPrincipal.java new file mode 100644 index 00000000000..55b00518b3d --- /dev/null +++ b/examples/jaas/lm-jdbc/src/main/java/org/apache/felix/example/jaas/jdbc/UserPrincipal.java @@ -0,0 +1,58 @@ +/* + * 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.example.jaas.jdbc; + +import java.security.Principal; + +public class UserPrincipal implements Principal { + + private final String name; + + public UserPrincipal(String name) { + assert name != null; + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof UserPrincipal)) return false; + + UserPrincipal that = (UserPrincipal) o; + + if (name != null ? !name.equals(that.name) : that.name != null) return false; + + return true; + } + + @Override + public int hashCode() { + return name != null ? name.hashCode() : 0; + } + + @Override + public String toString() { + return "UserPrincipal[" + name + "]"; + } + +} diff --git a/examples/jaas/lm-jdbc/src/main/resources/OSGI-INF/metatype/metatype.properties b/examples/jaas/lm-jdbc/src/main/resources/OSGI-INF/metatype/metatype.properties new file mode 100644 index 00000000000..13899d49709 --- /dev/null +++ b/examples/jaas/lm-jdbc/src/main/resources/OSGI-INF/metatype/metatype.properties @@ -0,0 +1,50 @@ +# +# 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. +# +# suppress inspection "UnusedProperty" for whole file + +jdbc.name=Apache Felix JDBC LoginModule Example +jdbc.description=Configuration details for connecting to Database + +#JAAS Stuff +jaas.controlFlag.name=Control Flag +jaas.controlFlag.description=The Flag value controls the overall behavior as authentication proceeds down the stack + +jaas.flag.required=Required +jaas.flag.requisite=Requisite +jaas.flag.sufficient=Sufficient +jaas.flag.optional=Optional + +jaas.ranking.name = Ranking +jaas.ranking.description = The relative ranking of this configuration. + +jaas.options.name = Options +jaas.options.description = Properties in the form of key value pairs that are passed on to the LoginModule(name=value pairs) + +jaas.realmName.name = Realm Name +jaas.realmName.description = Name of the application + +datasourceName.name = Datasource Name +datasourceName.description = Name of the datasource + +query.role.name= Role Query +query.role.description = Query to fetch user roles + +query.pwd.name = Password Query +query.pwd.description = Query to fetch user password + diff --git a/examples/jaas/pom.xml b/examples/jaas/pom.xml new file mode 100644 index 00000000000..843c011e7a8 --- /dev/null +++ b/examples/jaas/pom.xml @@ -0,0 +1,42 @@ + + + + org.apache.felix + felix-parent + 2.1 + ../../pom/pom.xml + + 4.0.0 + pom + Apache Felix Examples: JAAS + org.apache.felix.example + jaas-reactor + 0.0.1-SNAPSHOT + + + + lm-config + lm-jdbc + jdbc-h2 + app + launcher + + + diff --git a/examples/pom.xml b/examples/pom.xml index 1500f9b2d98..fd2d2213bb5 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -1,33 +1,70 @@ + org.apache.felix felix - 0.8.0-SNAPSHOT + 1.0.4 + ../pom/pom.xml 4.0.0 pom Apache Felix Examples: Build build + 0.9.0-SNAPSHOT - - eventlistener - dictionaryservice - dictionaryservice.itest - frenchdictionary - dictionaryclient - dictionaryclient2 - spellcheckservice - spellcheckclient - spellcheckbinder - spellcheckscr - - - - - ${pom.groupId} - org.osgi.core - ${pom.version} - provided - - + + + packaging-osgi-bundle + + + packaging + osgi-bundle + + + + + + + packaging-bundle + + + packaging + bundle + + + + eventlistener + dictionaryservice + + frenchdictionary + dictionaryclient + dictionaryclient2 + spellcheckservice + spellcheckclient + spellcheckbinder + spellcheckscr + servicebased.host + servicebased.circle + servicebased.square + servicebased.triangle + + + diff --git a/examples/servicebased.circle/pom.xml b/examples/servicebased.circle/pom.xml new file mode 100644 index 00000000000..0c00ce691a9 --- /dev/null +++ b/examples/servicebased.circle/pom.xml @@ -0,0 +1,67 @@ + + + + + org.apache.felix + felix + 1.0.4 + ../../pom/pom.xml + + + 4.0.0 + bundle + Apache Felix Circle Service + A simple service for drawing circles. + org.apache.felix.example + servicebased.circle + 1.0.0 + + + org.apache.felix.example + servicebased.host + 1.0.0 + + + + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + org.apache.felix.example.servicebased.circle.* + org.apache.felix.example.servicebased.circle.Activator + The Apache Software Foundation + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + + diff --git a/examples/servicebased.circle/src/main/java/org/apache/felix/example/servicebased/circle/Activator.java b/examples/servicebased.circle/src/main/java/org/apache/felix/example/servicebased/circle/Activator.java new file mode 100644 index 00000000000..f1391e382ad --- /dev/null +++ b/examples/servicebased.circle/src/main/java/org/apache/felix/example/servicebased/circle/Activator.java @@ -0,0 +1,95 @@ +/* + * 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.example.servicebased.circle; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.geom.Ellipse2D; +import java.util.Dictionary; +import java.util.Hashtable; +import javax.swing.ImageIcon; +import org.apache.felix.example.servicebased.host.service.SimpleShape; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +/** + * This class implements a simple bundle activator for the circle + * SimpleShape service. This activator simply creates an instance + * of the circle service object and registers it with the service registry + * along with the service properties indicating the service's name and icon. +**/ +public class Activator implements BundleActivator +{ + private BundleContext m_context = null; + + /** + * Implements the BundleActivator.start() method, which + * registers the circle SimpleShape service. + * @param context The context for the bundle. + **/ + public void start(BundleContext context) + { + m_context = context; + Dictionary dict = new Hashtable(); + dict.put(SimpleShape.NAME_PROPERTY, "Circle"); + dict.put(SimpleShape.ICON_PROPERTY, + new ImageIcon(this.getClass().getResource("circle.png"))); + m_context.registerService( + SimpleShape.class.getName(), new Circle(), dict); + } + + /** + * Implements the BundleActivator.start() method, which + * does nothing. + * @param context The context for the bundle. + **/ + public void stop(BundleContext context) + { + } + + /** + * This inner class implements the circle SimpleShape service. + * It simply provides a draw() that paints a circle. + **/ + public class Circle implements SimpleShape + { + /** + * Implements the SimpleShape.draw() method for painting + * the shape. + * @param g2 The graphics object used for painting. + * @param p The position to paint the triangle. + **/ + public void draw(Graphics2D g2, Point p) + { + int x = p.x - 25; + int y = p.y - 25; + GradientPaint gradient = new GradientPaint( + x, y, Color.RED, x + 50, y, Color.WHITE); + g2.setPaint(gradient); + g2.fill(new Ellipse2D.Double(x, y, 50, 50)); + BasicStroke wideStroke = new BasicStroke(2.0f); + g2.setColor(Color.black); + g2.setStroke(wideStroke); + g2.draw(new Ellipse2D.Double(x, y, 50, 50)); + } + } +} \ No newline at end of file diff --git a/examples/servicebased.circle/src/main/resources/META-INF/LICENSE b/examples/servicebased.circle/src/main/resources/META-INF/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/examples/servicebased.circle/src/main/resources/META-INF/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/examples/servicebased.circle/src/main/resources/META-INF/NOTICE b/examples/servicebased.circle/src/main/resources/META-INF/NOTICE new file mode 100644 index 00000000000..9c5efcc9e68 --- /dev/null +++ b/examples/servicebased.circle/src/main/resources/META-INF/NOTICE @@ -0,0 +1,5 @@ +Apache Felix Service-Based Circle Service Example +Copyright 2006 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/examples/servicebased.circle/src/main/resources/org/apache/felix/example/servicebased/circle/circle.png b/examples/servicebased.circle/src/main/resources/org/apache/felix/example/servicebased/circle/circle.png new file mode 100644 index 00000000000..3d4887eaa2a Binary files /dev/null and b/examples/servicebased.circle/src/main/resources/org/apache/felix/example/servicebased/circle/circle.png differ diff --git a/examples/servicebased.host/pom.xml b/examples/servicebased.host/pom.xml new file mode 100644 index 00000000000..c4c15b0202f --- /dev/null +++ b/examples/servicebased.host/pom.xml @@ -0,0 +1,72 @@ + + + + + org.apache.felix + felix + 1.0.4 + ../../pom/pom.xml + + + 4.0.0 + bundle + Apache Felix Service-Based Host + A simple application that embeds Felix and uses the service model for extensibility. + org.apache.felix.example + servicebased.host + 1.0.0 + + + org.apache.felix + org.apache.felix.framework + 4.0.2 + + + + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + <_donotcopy>(CVS|.svn|config.properties) + org.apache.felix.example.servicebased.host.launch.Application + !android.*,!dalvik.*,org.osgi.framework,org.osgi.service.packageadmin,org.osgi.service.url,org.osgi.service.startlevel,org.osgi.util.tracker,* + org.apache.felix.example.servicebased.host.service.* + org.apache.felix.example.servicebased.host.* + *;artifactId=org.apache.felix.framework;inline=true + org.apache.felix.example.servicebased.host.Activator + The Apache Software Foundation + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + diff --git a/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/Activator.java b/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/Activator.java new file mode 100644 index 00000000000..38b4506dc5a --- /dev/null +++ b/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/Activator.java @@ -0,0 +1,115 @@ +/* + * 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.example.servicebased.host; + +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import javax.swing.JFrame; +import javax.swing.SwingUtilities; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; + +/** + * The activator of the host application bundle. The activator creates the + * main application JFrame and starts tracking SimpleShape + * services. All activity is performed on the Swing event thread to avoid + * synchronization and repainting issues. Closing the application window + * will result in Bundle.stop() being called on the system bundle, + * which will cause the framework to shutdown and the JVM to exit. +**/ +public class Activator implements BundleActivator +{ + private DrawingFrame m_frame = null; + private ShapeTracker m_shapetracker = null; + + /** + * Displays the applications window and starts service tracking; + * everything is done on the Swing event thread to avoid synchronization + * and repainting issues. + * @param context The context of the bundle. + **/ + @Override + public void start(final BundleContext context) + { + SwingUtilities.invokeLater(new Runnable() { + // This creates of the application window. + @Override + public void run() + { + m_frame = new DrawingFrame(); + m_frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + m_frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent evt) + { + try + { + context.getBundle(0).stop(); + } + catch (BundleException ex) + { + ex.printStackTrace(); + } + } + }); + + m_frame.setVisible(true); + + m_shapetracker = new ShapeTracker(context, m_frame); + m_shapetracker.open(); + } + }); + } + + /** + * Stops service tracking and disposes of the application window. + * @param context The context of the bundle. + **/ + @Override + public void stop(BundleContext context) + { + Runnable runner = new Runnable() { + // This disposes of the application window. + @Override + public void run() + { + m_shapetracker.close(); + m_frame.setVisible(false); + m_frame.dispose(); + } + }; + + if (SwingUtilities.isEventDispatchThread()) + { + runner.run(); + } + else + { + try + { + SwingUtilities.invokeAndWait(runner); + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + } +} \ No newline at end of file diff --git a/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/DefaultShape.java b/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/DefaultShape.java new file mode 100644 index 00000000000..f9598d2055a --- /dev/null +++ b/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/DefaultShape.java @@ -0,0 +1,143 @@ +/* + * 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.example.servicebased.host; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Point; +import javax.swing.ImageIcon; +import org.apache.felix.example.servicebased.host.service.SimpleShape; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; + +/** + * This class is used as a proxy to defer object creation from shape + * provider bundles and also as a placeholder shape when previously + * used shapes are no longer available. These two purposes are + * actually orthogonal, but were combined into a single class to + * reduce the number of classes in the application. The proxy-related + * functionality is introduced as a way to lazily create shape + * objects in an effort to improve performance; this level of + * indirection could be removed if eager creation of objects is not + * a concern. Since this application uses the service-based extension + * appraoch, lazy shape creation will only come into effect if + * service providers register service factories instead of directly + * registering SimpleShape or if they use a technology like + * Declarative Services or iPOJO to register services. Since the + * example providers register services instances directly there is + * no laziness in the example, but the proxy approach is still used + * to demonstrate how to make laziness possible and to keep it + * similar to the extender-based approach. +**/ +class DefaultShape implements SimpleShape +{ + private SimpleShape m_shape; + private ImageIcon m_icon; + private BundleContext m_context; + private ServiceReference m_ref; + + /** + * This constructs a placeholder shape that draws a default + * icon. It is used when a previously drawn shape is no longer + * available. + **/ + public DefaultShape() + { + // Do nothing. + } + + /** + * This constructs a proxy shape that lazily gets the shape service. + * @param context The bundle context to use for retrieving the shape service. + * @param ref The service reference of the service. + **/ + public DefaultShape(BundleContext context, ServiceReference ref) + { + m_context = context; + m_ref = ref; + } + + /** + * This method tells the proxy to dispose of its service object; this + * is called when the underlying service goes away. + **/ + public void dispose() + { + if (m_shape != null) + { + m_context.ungetService(m_ref); + m_context = null; + m_ref = null; + m_shape = null; + } + } + + /** + * Implements the SimpleShape interface method. When acting as + * a proxy, this method gets the shape service and then uses it to draw + * the shape. When acting as a placeholder shape, this method draws the + * default icon. + * @param g2 The graphics object used for painting. + * @param p The position to paint the triangle. + **/ + @Override + public void draw(Graphics2D g2, Point p) + { + // If this is a proxy shape, instantiate the shape class + // and use it to draw the shape. + if (m_context != null) + { + try + { + if (m_shape == null) + { + // Get the shape service. + m_shape = m_context.getService(m_ref); + } + // Draw the shape. + m_shape.draw(g2, p); + // If everything was successful, then simply return. + return; + } + catch (Exception ex) + { + // This generally should not happen, but if it does then + // we can just fall through and paint the default icon. + } + } + + // If the proxied shape could not be drawn for any reason or if + // this shape is simply a placeholder, then draw the default icon. + if (m_icon == null) + { + try + { + m_icon = new ImageIcon(this.getClass().getResource("underc.png")); + } + catch (Exception ex) + { + ex.printStackTrace(); + g2.setColor(Color.red); + g2.fillRect(0, 0, 60, 60); + return; + } + } + g2.drawImage(m_icon.getImage(), 0, 0, null); + } +} \ No newline at end of file diff --git a/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/DrawingFrame.java b/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/DrawingFrame.java new file mode 100644 index 00000000000..2046ed7d840 --- /dev/null +++ b/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/DrawingFrame.java @@ -0,0 +1,293 @@ +/* + * 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.example.servicebased.host; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.util.HashMap; +import java.util.Map; +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JToolBar; +import org.apache.felix.example.servicebased.host.service.SimpleShape; + +/** + * This class represents the main application class, which is a JFrame subclass + * that manages a toolbar of shapes and a drawing canvas. This class does not + * directly interact with the underlying OSGi framework; instead, it is injected + * with the available SimpleShape instances to eliminate any + * dependencies on the OSGi application programming interfaces. +**/ +public class DrawingFrame extends JFrame + implements MouseListener, MouseMotionListener +{ + private static final long serialVersionUID = 1L; + private static final int BOX = 54; + private final JToolBar m_toolbar; + private String m_selected; + private final JPanel m_panel; + private ShapeComponent m_selectedComponent; + private final Map m_shapes = new HashMap(); + private final SimpleShape m_defaultShape = new DefaultShape(); + private final ActionListener m_reusableActionListener = new ShapeActionListener(); + + /** + * Default constructor that populates the main window. + **/ + public DrawingFrame() + { + super("Service-Based Host"); + + m_toolbar = new JToolBar("Toolbar"); + m_panel = new JPanel(); + m_panel.setBackground(Color.WHITE); + m_panel.setLayout(null); + m_panel.setMinimumSize(new Dimension(400, 400)); + m_panel.addMouseListener(this); + getContentPane().setLayout(new BorderLayout()); + getContentPane().add(m_toolbar, BorderLayout.NORTH); + getContentPane().add(m_panel, BorderLayout.CENTER); + setSize(400, 400); + } + + /** + * This method sets the currently selected shape to be used for drawing + * on the canvas. + * @param name The name of the shape to use for drawing on the canvas. + **/ + public void selectShape(String name) + { + m_selected = name; + } + + /** + * Retrieves the available SimpleShape associated with the given name. + * @param name The name of the SimpleShape to retrieve. + * @return The corresponding SimpleShape instance if available or + * null. + **/ + public SimpleShape getShape(String name) + { + ShapeInfo info = m_shapes.get(name); + if (info == null) + { + return m_defaultShape; + } + else + { + return info.m_shape; + } + } + + /** + * Injects an available SimpleShape into the drawing frame. + * @param name The name of the injected SimpleShape. + * @param icon The icon associated with the injected SimpleShape. + * @param shape The injected SimpleShape instance. + **/ + public void addShape(String name, Icon icon, SimpleShape shape) + { + m_shapes.put(name, new ShapeInfo(name, icon, shape)); + JButton button = new JButton(icon); + button.setActionCommand(name); + button.addActionListener(m_reusableActionListener); + + if (m_selected == null) + { + button.doClick(); + } + + m_toolbar.add(button); + m_toolbar.validate(); + repaint(); + } + + /** + * Removes a no longer available SimpleShape from the drawing frame. + * @param name The name of the SimpleShape to remove. + **/ + public void removeShape(String name) + { + m_shapes.remove(name); + + if ((m_selected != null) && m_selected.equals(name)) + { + m_selected = null; + } + + for (int i = 0; i < m_toolbar.getComponentCount(); i++) + { + JButton sb = (JButton) m_toolbar.getComponent(i); + if (sb.getActionCommand().equals(name)) + { + m_toolbar.remove(i); + m_toolbar.invalidate(); + validate(); + repaint(); + break; + } + } + + if ((m_selected == null) && (m_toolbar.getComponentCount() > 0)) + { + ((JButton) m_toolbar.getComponent(0)).doClick(); + } + } + + /** + * Implements method for the MouseListener interface to + * draw the selected shape into the drawing canvas. + * @param evt The associated mouse event. + **/ + @Override + public void mouseClicked(MouseEvent evt) + { + if (m_selected == null) + { + return; + } + + if (m_panel.contains(evt.getX(), evt.getY())) + { + ShapeComponent sc = new ShapeComponent(this, m_selected); + sc.setBounds(evt.getX() - BOX / 2, evt.getY() - BOX / 2, BOX, BOX); + m_panel.add(sc, 0); + m_panel.validate(); + m_panel.repaint(sc.getBounds()); + } + } + + /** + * Implements an empty method for the MouseListener interface. + * @param evt The associated mouse event. + **/ + @Override + public void mouseEntered(MouseEvent evt) + { + } + + /** + * Implements an empty method for the MouseListener interface. + * @param evt The associated mouse event. + **/ + @Override + public void mouseExited(MouseEvent evt) + { + } + + /** + * Implements method for the MouseListener interface to initiate + * shape dragging. + * @param evt The associated mouse event. + **/ + @Override + public void mousePressed(MouseEvent evt) + { + Component c = m_panel.getComponentAt(evt.getPoint()); + if (c instanceof ShapeComponent) + { + m_selectedComponent = (ShapeComponent) c; + m_panel.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); + m_panel.addMouseMotionListener(this); + m_selectedComponent.repaint(); + } + } + + /** + * Implements method for the MouseListener interface to complete + * shape dragging. + * @param evt The associated mouse event. + **/ + @Override + public void mouseReleased(MouseEvent evt) + { + if (m_selectedComponent != null) + { + m_panel.removeMouseMotionListener(this); + m_panel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + m_selectedComponent.setBounds( + evt.getX() - BOX / 2, evt.getY() - BOX / 2, BOX, BOX); + m_selectedComponent.repaint(); + m_selectedComponent = null; + } + } + + /** + * Implements method for the MouseMotionListener interface to + * move a dragged shape. + * @param evt The associated mouse event. + **/ + @Override + public void mouseDragged(MouseEvent evt) + { + m_selectedComponent.setBounds( + evt.getX() - BOX / 2, evt.getY() - BOX / 2, BOX, BOX); + } + + /** + * Implements an empty method for the MouseMotionListener + * interface. + * @param evt The associated mouse event. + **/ + @Override + public void mouseMoved(MouseEvent evt) + { + } + + /** + * Simple action listener for shape tool bar buttons that sets + * the drawing frame's currently selected shape when receiving + * an action event. + **/ + private class ShapeActionListener implements ActionListener + { + @Override + public void actionPerformed(ActionEvent evt) + { + selectShape(evt.getActionCommand()); + } + } + + /** + * This class is used to record the various information pertaining to + * an available shape. + **/ + private static class ShapeInfo + { + private final String m_name; + private final Icon m_icon; + private final SimpleShape m_shape; + public ShapeInfo(String name, Icon icon, SimpleShape shape) + { + m_name = name; + m_icon = icon; + m_shape = shape; + } + } +} \ No newline at end of file diff --git a/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/ShapeComponent.java b/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/ShapeComponent.java new file mode 100644 index 00000000000..e7ea1893767 --- /dev/null +++ b/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/ShapeComponent.java @@ -0,0 +1,68 @@ +/* + * 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.example.servicebased.host; + +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.RenderingHints; +import javax.swing.JComponent; +import org.apache.felix.example.servicebased.host.service.SimpleShape; + +/** + * Simple component class used to represent a drawn shape. This component + * uses a shape service to paint its contents. +**/ +public class ShapeComponent extends JComponent +{ + private static final long serialVersionUID = 1L; + private final DrawingFrame m_frame; + private final String m_shapeName; + + /** + * Construct a component for the specified drawing frame with the specified + * named shape. The component acquires the named shape from the drawing + * frame at the time of painting, which enables it to account for + * service dynamism. + * @param frame The drawing frame associated with the component. + * @param shapeName The name of the shape to draw. + **/ + public ShapeComponent(DrawingFrame frame, String shapeName) + { + m_frame = frame; + m_shapeName = shapeName; + } + + /** + * Paints the contents of the component. The component acquires the named + * shape from the drawing frame at the time of painting, which enables it + * to account for service dynamism. + * @param g The graphics object to use for painting. + **/ + @Override + protected void paintComponent(Graphics g) + { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + SimpleShape shape = m_frame.getShape(m_shapeName); + shape.draw(g2, new Point(getWidth()/2, getHeight()/2)); + } +} \ No newline at end of file diff --git a/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/ShapeTracker.java b/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/ShapeTracker.java new file mode 100644 index 00000000000..6ea4bacffb9 --- /dev/null +++ b/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/ShapeTracker.java @@ -0,0 +1,195 @@ +/* + * 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.example.servicebased.host; + +import javax.swing.Icon; +import javax.swing.SwingUtilities; +import org.apache.felix.example.servicebased.host.service.SimpleShape; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; + +/** + * Extends the ServiceTracker to create a tracker for + * SimpleShape services. The tracker is responsible for + * listener for the arrival/departure of SimpleShape + * services and informing the application about the availability + * of shapes. This tracker forces all notifications to be processed + * on the Swing event thread to avoid synchronization and redraw + * issues. +**/ +public class ShapeTracker extends ServiceTracker +{ + // The bundle context used for tracking. + private final BundleContext m_context; + // The application object to notify. + private final DrawingFrame m_frame; + + /** + * Constructs a tracker that uses the specified bundle context to + * track services and notifies the specified application object about + * changes. + * @param context The bundle context to be used by the tracker. + * @param frame The application object to notify about service changes. + **/ + public ShapeTracker(BundleContext context, DrawingFrame frame) + { + super(context, SimpleShape.class.getName(), null); + m_context = context; + m_frame = frame; + } + + /** + * Overrides the ServiceTracker functionality to inform + * the application object about the added service. + * @param ref The service reference of the added service. + * @return The service object to be used by the tracker. + **/ + @Override + public SimpleShape addingService(ServiceReference ref) + { + SimpleShape shape = new DefaultShape(m_context, ref); + processShapeOnEventThread(ShapeEvent.ADDED, ref, shape); + return shape; + } + + /** + * Overrides the ServiceTracker functionality to inform + * the application object about the modified service. + * @param ref The service reference of the modified service. + * @param svc The service object of the modified service. + **/ + @Override + public void modifiedService(ServiceReference ref, SimpleShape svc) + { + processShapeOnEventThread(ShapeEvent.MODIFIED, ref, svc); + } + + /** + * Overrides the ServiceTracker functionality to inform + * the application object about the removed service. + * @param ref The service reference of the removed service. + * @param svc The service object of the removed service. + **/ + @Override + public void removedService(ServiceReference ref, SimpleShape svc) + { + processShapeOnEventThread(ShapeEvent.REMOVED, ref, svc); + ((DefaultShape) svc).dispose(); + } + + /** + * Processes a received service notification from the ServiceTracker, + * forcing the processing of the notification onto the Swing event thread + * if it is not already on it. + * @param event The type of action associated with the notification. + * @param ref The service reference of the corresponding service. + * @param shape The service object of the corresponding service. + **/ + private void processShapeOnEventThread( + ShapeEvent event, ServiceReference ref, SimpleShape shape) + { + try + { + if (SwingUtilities.isEventDispatchThread()) + { + processShape(event, ref, shape); + } + else + { + SwingUtilities.invokeAndWait(new ShapeRunnable(event, ref, shape)); + } + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + + /** + * Actually performs the processing of the service notification. Invokes + * the appropriate callback method on the application object depending on + * the action type of the notification. + * @param event The type of action associated with the notification. + * @param ref The service reference of the corresponding service. + * @param shape The service object of the corresponding service. + **/ + private void processShape(ShapeEvent event, ServiceReference ref, SimpleShape shape) + { + String name = (String) ref.getProperty(SimpleShape.NAME_PROPERTY); + + switch (event) + { + case MODIFIED: + m_frame.removeShape(name); + // Purposely let this fall through to the 'add' case to + // reload the service. + + case ADDED: + Icon icon = (Icon) ref.getProperty(SimpleShape.ICON_PROPERTY); + m_frame.addShape(name, icon, shape); + break; + + case REMOVED: + m_frame.removeShape(name); + break; + } + } + + /** + * Simple class used to process service notification handling on the + * Swing event thread. + **/ + private class ShapeRunnable implements Runnable + { + private final ShapeEvent m_event; + private final ServiceReference m_ref; + private final SimpleShape m_shape; + + /** + * Constructs an object with the specified action, service reference, + * and service object for processing on the Swing event thread. + * @param event The type of action associated with the notification. + * @param ref The service reference of the corresponding service. + * @param shape The service object of the corresponding service. + **/ + public ShapeRunnable(ShapeEvent event, ServiceReference ref, SimpleShape shape) + { + m_event = event; + m_ref = ref; + m_shape = shape; + } + + /** + * Calls the processShape() method. + **/ + @Override + public void run() + { + processShape(m_event, m_ref, m_shape); + } + } + + private static enum ShapeEvent + { + ADDED, + MODIFIED, + REMOVED + } +} \ No newline at end of file diff --git a/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/launch/Application.java b/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/launch/Application.java new file mode 100644 index 00000000000..52d163a42e2 --- /dev/null +++ b/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/launch/Application.java @@ -0,0 +1,131 @@ +/* + * 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.example.servicebased.host.launch; + +import java.util.Map; +import java.util.ServiceLoader; +import org.apache.felix.example.servicebased.host.Activator; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.launch.Framework; +import org.osgi.framework.launch.FrameworkFactory; + +/** + * This class provides a static {@code main()} method so that the bundle can be + * run as a stand-alone host application. In such a scenario, the application + * creates its own embedded OSGi framework instance and interacts with the + * internal extensions to providing drawing functionality. To successfully + * launch the stand-alone application, it must be run from this bundle's + * installation directory using "{@code java -jar}". + * The locations of any additional extensions that have to be started, have to + * be passed as command line arguments to this method. + */ +public class Application +{ + private static Framework m_framework = null; + + /** + * Enables the bundle to run as a stand-alone application. When this + * static {@code main()} method is invoked, the application creates + * its own embedded OSGi framework instance and interacts with the + * internal extensions to provide drawing functionality. To successfully + * launch as a stand-alone application, this method should be invoked from + * the bundle's installation directory using "{@code java -jar}". + * The location of any extension that shall be installed can be passed + * as parameters. + *

      + * For example if you build the bundles inside your workspace, maven will + * create a target directory in every project. To start the application + * from within your IDE you should pass: + *

      + *

      +     * {@code file:../servicebased.circle/target/servicebased.circle-1.0.0.jar
      +     * file:../servicebased.square/target/servicebased.square-1.0.0.jar
      +     * file:../servicebased.triangle/target/servicebased.triangle-1.0.0.jar}
      +     * 
      + * + * @param args The locations of additional bundles to start. + **/ + public static void main(String[] args) + { + // Args should never be null if the application is run from the command line. + // Check it anyway. + String[] locations = args != null ? args : new String[0]; + + // Print welcome banner. + System.out.println("\nWelcome to My Launcher"); + System.out.println("======================\n"); + + try + { + Map config = ConfigUtil.createConfig(); + m_framework = createFramework(config); + m_framework.init(); + m_framework.start(); + installAndStartBundles(locations); + m_framework.waitForStop(0); + System.exit(0); + } + catch (Exception ex) + { + System.err.println("Could not create framework: " + ex); + ex.printStackTrace(); + System.exit(-1); + } + } + + /** + * Util method for creating an embedded Framework. Tries to create a + * {@link FrameworkFactory} which is then be used to create the framework. + * + * @param config the configuration to create the framework with + * @return a Framework with the given configuration + */ + private static Framework createFramework(Map config) + { + ServiceLoader factoryLoader = ServiceLoader.load(FrameworkFactory.class); + for(FrameworkFactory factory : factoryLoader) + { + return factory.newFramework(config); + } + throw new IllegalStateException("Unable to load FrameworkFactory service."); + } + + /** + * Installs and starts all bundles used by the application. Therefore the host bundle + * will be started. The locations of extensions for the host bundle can be passed in + * as parameters. + * + * @param bundleLocations the locations where extension for the host bundle are located. + * Must not be {@code null}! + * @throws BundleException if something went wrong while installing or starting the bundles. + */ + private static void installAndStartBundles(String... bundleLocations) throws BundleException + { + BundleContext bundleContext = m_framework.getBundleContext(); + Activator hostActivator = new Activator(); + hostActivator.start(bundleContext); + for (String location : bundleLocations) + { + Bundle addition = bundleContext.installBundle(location); + addition.start(); + } + } +} \ No newline at end of file diff --git a/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/launch/ConfigUtil.java b/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/launch/ConfigUtil.java new file mode 100644 index 00000000000..415c14029bc --- /dev/null +++ b/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/launch/ConfigUtil.java @@ -0,0 +1,115 @@ +/* + * 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.example.servicebased.host.launch; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.osgi.framework.Constants; + +/** + * Util class for creating the framework configuration + */ +final class ConfigUtil +{ + /** + * Creates a configuration for the framework. Therefore this method attempts to create + * a temporary cache dir. If creation of the cache dir is successful, it will be added + * to the configuration. + * + * @return + */ + public static Map createConfig() + { + final File cachedir = createCacheDir(); + + Map configMap = new HashMap(); + // Tells the framework to export the extension package, making it accessible + // for the other shape bundles + configMap.put(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, + "org.apache.felix.example.servicebased.host.service; version=1.0.0"); + + // if we could create a cache dir, we use it. Otherwise the platform default will be used + if (cachedir != null) + { + configMap.put(Constants.FRAMEWORK_STORAGE, cachedir.getAbsolutePath()); + } + + return configMap; + } + + /** + * Tries to create a temporay cache dir. If creation of the cache dir is successful, + * it will be returned. If creation fails, null will be returned. + * + * @return a {@code File} object representing the cache dir + */ + private static File createCacheDir() + { + final File cachedir; + try + { + cachedir = File.createTempFile("felix.example.extenderbased", null); + cachedir.delete(); + createShutdownHook(cachedir); + return cachedir; + } + catch (IOException e) + { + // temp dir creation failed, return null + return null; + } + } + + /** + * Adds a shutdown hook to the runtime, that will make sure, that the cache dir will + * be deleted after the application has been terminated. + */ + private static void createShutdownHook(final File cachedir) + { + Runtime.getRuntime().addShutdownHook(new Thread() + { + @Override + public void run() + { + deleteFileOrDir(cachedir); + } + }); + } + + + /** + * Utility method used to delete the profile directory when run as + * a stand-alone application. + * @param file The file to recursively delete. + **/ + private static void deleteFileOrDir(File file) + { + if (file.isDirectory()) + { + File[] childs = file.listFiles(); + for (File child : childs) + { + deleteFileOrDir(child); + } + } + file.delete(); + } +} \ No newline at end of file diff --git a/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/service/SimpleShape.java b/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/service/SimpleShape.java new file mode 100644 index 00000000000..72c82af6a66 --- /dev/null +++ b/examples/servicebased.host/src/main/java/org/apache/felix/example/servicebased/host/service/SimpleShape.java @@ -0,0 +1,51 @@ +/* + * 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.example.servicebased.host.service; + +import java.awt.Graphics2D; +import java.awt.Point; + +/** + * This interface defines the SimpleShape service. This service + * is used to draw shapes. It has two service properties: + *
        + *
      • simple.shape.name - A String name for the shape. + *
      • + *
      • simple.shape.icon - An Icon for the shape. + *
      • + *
      +**/ +public interface SimpleShape +{ + /** + * A service property for the name of the shape. + **/ + public static final String NAME_PROPERTY = "simple.shape.name"; + /** + * A service property for the icon of the shape. + **/ + public static final String ICON_PROPERTY = "simple.shape.icon"; + + /** + * Method to draw the shape of the service. + * @param g2 The graphics object used for painting. + * @param p The position to paint the triangle. + **/ + public void draw(Graphics2D g2, Point p); +} diff --git a/examples/servicebased.host/src/main/resources/META-INF/LICENSE b/examples/servicebased.host/src/main/resources/META-INF/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/examples/servicebased.host/src/main/resources/META-INF/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/examples/servicebased.host/src/main/resources/META-INF/NOTICE b/examples/servicebased.host/src/main/resources/META-INF/NOTICE new file mode 100644 index 00000000000..1717c0f6f28 --- /dev/null +++ b/examples/servicebased.host/src/main/resources/META-INF/NOTICE @@ -0,0 +1,5 @@ +Apache Felix Service-Based Host Example +Copyright 2007 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/examples/servicebased.host/src/main/resources/org/apache/felix/example/servicebased/host/underc.png b/examples/servicebased.host/src/main/resources/org/apache/felix/example/servicebased/host/underc.png new file mode 100644 index 00000000000..425cdb91f2f Binary files /dev/null and b/examples/servicebased.host/src/main/resources/org/apache/felix/example/servicebased/host/underc.png differ diff --git a/examples/servicebased.square/pom.xml b/examples/servicebased.square/pom.xml new file mode 100644 index 00000000000..18a7583fc73 --- /dev/null +++ b/examples/servicebased.square/pom.xml @@ -0,0 +1,67 @@ + + + + + org.apache.felix + felix + 1.0.4 + ../../pom/pom.xml + + + 4.0.0 + bundle + Apache Felix Square Service + A simple service for drawing squares. + org.apache.felix.example + servicebased.square + 1.0.0 + + + org.apache.felix.example + servicebased.host + 1.0.0 + + + + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + org.apache.felix.example.servicebased.square.* + org.apache.felix.example.servicebased.square.Activator + The Apache Software Foundation + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + + diff --git a/examples/servicebased.square/src/main/java/org/apache/felix/example/servicebased/square/Activator.java b/examples/servicebased.square/src/main/java/org/apache/felix/example/servicebased/square/Activator.java new file mode 100644 index 00000000000..984d1ebef29 --- /dev/null +++ b/examples/servicebased.square/src/main/java/org/apache/felix/example/servicebased/square/Activator.java @@ -0,0 +1,95 @@ +/* + * 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.example.servicebased.square; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.geom.Rectangle2D; +import java.util.Dictionary; +import java.util.Hashtable; +import javax.swing.ImageIcon; +import org.apache.felix.example.servicebased.host.service.SimpleShape; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +/** + * This class implements a simple bundle activator for the square + * SimpleShape service. This activator simply creates an instance + * of the square service object and registers it with the service registry + * along with the service properties indicating the service's name and icon. +**/ +public class Activator implements BundleActivator +{ + private BundleContext m_context = null; + + /** + * Implements the BundleActivator.start() method, which + * registers the square SimpleShape service. + * @param context The context for the bundle. + **/ + public void start(BundleContext context) + { + m_context = context; + Dictionary dict = new Hashtable(); + dict.put(SimpleShape.NAME_PROPERTY, "Square"); + dict.put(SimpleShape.ICON_PROPERTY, + new ImageIcon(this.getClass().getResource("square.png"))); + m_context.registerService( + SimpleShape.class.getName(), new Square(), dict); + } + + /** + * Implements the BundleActivator.start() method, which + * does nothing. + * @param context The context for the bundle. + **/ + public void stop(BundleContext context) + { + } + + /** + * This inner class implements the square SimpleShape service. + * It simply provides a draw() that paints a square. + **/ + public class Square implements SimpleShape + { + /** + * Implements the SimpleShape.draw() method for painting + * the shape. + * @param g2 The graphics object used for painting. + * @param p The position to paint the triangle. + **/ + public void draw(Graphics2D g2, Point p) + { + int x = p.x - 25; + int y = p.y - 25; + GradientPaint gradient = new GradientPaint( + x, y, Color.BLUE, x + 50, y, Color.WHITE); + g2.setPaint(gradient); + g2.fill(new Rectangle2D.Double(x, y, 50, 50)); + BasicStroke wideStroke = new BasicStroke(2.0f); + g2.setColor(Color.black); + g2.setStroke(wideStroke); + g2.draw(new Rectangle2D.Double(x, y, 50, 50)); + } + } +} \ No newline at end of file diff --git a/examples/servicebased.square/src/main/resources/META-INF/LICENSE b/examples/servicebased.square/src/main/resources/META-INF/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/examples/servicebased.square/src/main/resources/META-INF/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/examples/servicebased.square/src/main/resources/META-INF/NOTICE b/examples/servicebased.square/src/main/resources/META-INF/NOTICE new file mode 100644 index 00000000000..f0fe53c0ceb --- /dev/null +++ b/examples/servicebased.square/src/main/resources/META-INF/NOTICE @@ -0,0 +1,5 @@ +Apache Felix Service-Based Square Service Example +Copyright 2006 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/examples/servicebased.square/src/main/resources/org/apache/felix/example/servicebased/square/square.png b/examples/servicebased.square/src/main/resources/org/apache/felix/example/servicebased/square/square.png new file mode 100644 index 00000000000..3f24cfc965c Binary files /dev/null and b/examples/servicebased.square/src/main/resources/org/apache/felix/example/servicebased/square/square.png differ diff --git a/examples/servicebased.trapezoid/pom.xml b/examples/servicebased.trapezoid/pom.xml new file mode 100644 index 00000000000..7ca86fcb741 --- /dev/null +++ b/examples/servicebased.trapezoid/pom.xml @@ -0,0 +1,65 @@ + + + + + org.apache + apache + 4 + + + 4.0.0 + bundle + Apache Felix Trapezoid Service + A simple service for drawing trapezoids. + org.apache.felix.example + servicebased.trapezoid + 1.0.0 + + + org.apache.felix.example + servicebased.host + 1.0.0 + + + + + + org.apache.felix + maven-bundle-plugin + 1.0.0 + true + + + org.apache.felix.example.servicebased.trapezoid.* + org.apache.felix.example.servicebased.trapezoid.Activator + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + + diff --git a/examples/servicebased.trapezoid/src/main/java/org/apache/felix/example/servicebased/trapezoid/Activator.java b/examples/servicebased.trapezoid/src/main/java/org/apache/felix/example/servicebased/trapezoid/Activator.java new file mode 100644 index 00000000000..23d628f4556 --- /dev/null +++ b/examples/servicebased.trapezoid/src/main/java/org/apache/felix/example/servicebased/trapezoid/Activator.java @@ -0,0 +1,104 @@ +/* + * 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.example.servicebased.trapezoid; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.geom.GeneralPath; +import java.util.Dictionary; +import java.util.Hashtable; +import javax.swing.ImageIcon; +import org.apache.felix.example.servicebased.host.service.SimpleShape; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +/** + * This class implements a simple bundle activator for the trapezoid + * SimpleShape service. This activator simply creates an instance + * of the trapezoid service object and registers it with the service registry + * along with the service properties indicating the service's name and icon. +**/ +public class Activator implements BundleActivator +{ + private BundleContext m_context = null; + + /** + * Implements the BundleActivator.start() method, which + * registers the trapezoid SimpleShape service. + * @param context The context for the bundle. + **/ + public void start(BundleContext context) + { + m_context = context; + Dictionary dict = new Hashtable(); + dict.put(SimpleShape.NAME_PROPERTY, "Trapezoid"); + dict.put(SimpleShape.ICON_PROPERTY, + new ImageIcon(this.getClass().getResource("trapezoid.png"))); + m_context.registerService( + SimpleShape.class.getName(), new Trapezoid(), dict); + } + + /** + * Implements the BundleActivator.start() method, which + * does nothing. + * @param context The context for the bundle. + **/ + public void stop(BundleContext context) + { + } + + /** + * This inner class implements the trapezoid SimpleShape service. + * It simply provides a draw() that paints a trapezoid. + **/ + public class Trapezoid implements SimpleShape + { + /** + * Implements the SimpleShape.draw() method for painting + * the shape. + * @param g2 The graphics object used for painting. + * @param p The position to paint the trapezoid. + **/ + public void draw(Graphics2D g2, Point p) + { + int x = p.x - 25; + int y = p.y - 25; + GradientPaint gradient = new GradientPaint( + x, y, Color.YELLOW, x + 50, y, Color.WHITE); + g2.setPaint(gradient); + int[] xcoords = { x + 10, x, x + 50, x + 40 }; + int[] ycoords = { y, y + 50, y + 50, y }; + GeneralPath polygon = new GeneralPath(GeneralPath.WIND_EVEN_ODD, xcoords.length); + polygon.moveTo(x + 25, y); + for (int i = 0; i < xcoords.length; i++) + { + polygon.lineTo(xcoords[i], ycoords[i]); + } + polygon.closePath(); + g2.fill(polygon); + BasicStroke wideStroke = new BasicStroke(2.0f); + g2.setColor(Color.black); + g2.setStroke(wideStroke); + g2.draw(polygon); + } + } +} diff --git a/examples/servicebased.trapezoid/src/main/resources/META-INF/LICENSE b/examples/servicebased.trapezoid/src/main/resources/META-INF/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/examples/servicebased.trapezoid/src/main/resources/META-INF/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/examples/servicebased.trapezoid/src/main/resources/META-INF/NOTICE b/examples/servicebased.trapezoid/src/main/resources/META-INF/NOTICE new file mode 100644 index 00000000000..6d0c83b51fb --- /dev/null +++ b/examples/servicebased.trapezoid/src/main/resources/META-INF/NOTICE @@ -0,0 +1,5 @@ +Apache Felix Service-Based Triangle Service Example +Copyright 2006 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/examples/servicebased.trapezoid/src/main/resources/org/apache/felix/example/servicebased/trapezoid/trapezoid.png b/examples/servicebased.trapezoid/src/main/resources/org/apache/felix/example/servicebased/trapezoid/trapezoid.png new file mode 100644 index 00000000000..272a5d0ddaf Binary files /dev/null and b/examples/servicebased.trapezoid/src/main/resources/org/apache/felix/example/servicebased/trapezoid/trapezoid.png differ diff --git a/examples/servicebased.triangle/pom.xml b/examples/servicebased.triangle/pom.xml new file mode 100644 index 00000000000..cb3c268c2f7 --- /dev/null +++ b/examples/servicebased.triangle/pom.xml @@ -0,0 +1,67 @@ + + + + + org.apache.felix + felix + 1.0.4 + ../../pom/pom.xml + + + 4.0.0 + bundle + Apache Felix Triangle Service + A simple service for drawing triangles. + org.apache.felix.example + servicebased.triangle + 1.0.0 + + + org.apache.felix.example + servicebased.host + 1.0.0 + + + + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + org.apache.felix.example.servicebased.triangle.* + org.apache.felix.example.servicebased.triangle.Activator + The Apache Software Foundation + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + + diff --git a/examples/servicebased.triangle/src/main/java/org/apache/felix/example/servicebased/triangle/Activator.java b/examples/servicebased.triangle/src/main/java/org/apache/felix/example/servicebased/triangle/Activator.java new file mode 100644 index 00000000000..55efbaa778b --- /dev/null +++ b/examples/servicebased.triangle/src/main/java/org/apache/felix/example/servicebased/triangle/Activator.java @@ -0,0 +1,104 @@ +/* + * 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.example.servicebased.triangle; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.geom.GeneralPath; +import java.util.Dictionary; +import java.util.Hashtable; +import javax.swing.ImageIcon; +import org.apache.felix.example.servicebased.host.service.SimpleShape; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +/** + * This class implements a simple bundle activator for the triangle + * SimpleShape service. This activator simply creates an instance + * of the triangle service object and registers it with the service registry + * along with the service properties indicating the service's name and icon. +**/ +public class Activator implements BundleActivator +{ + private BundleContext m_context = null; + + /** + * Implements the BundleActivator.start() method, which + * registers the triangle SimpleShape service. + * @param context The context for the bundle. + **/ + public void start(BundleContext context) + { + m_context = context; + Dictionary dict = new Hashtable(); + dict.put(SimpleShape.NAME_PROPERTY, "Triangle"); + dict.put(SimpleShape.ICON_PROPERTY, + new ImageIcon(this.getClass().getResource("triangle.png"))); + m_context.registerService( + SimpleShape.class.getName(), new Triangle(), dict); + } + + /** + * Implements the BundleActivator.start() method, which + * does nothing. + * @param context The context for the bundle. + **/ + public void stop(BundleContext context) + { + } + + /** + * This inner class implements the triangle SimpleShape service. + * It simply provides a draw() that paints a triangle. + **/ + public class Triangle implements SimpleShape + { + /** + * Implements the SimpleShape.draw() method for painting + * the shape. + * @param g2 The graphics object used for painting. + * @param p The position to paint the triangle. + **/ + public void draw(Graphics2D g2, Point p) + { + int x = p.x - 25; + int y = p.y - 25; + GradientPaint gradient = new GradientPaint( + x, y, Color.GREEN, x + 50, y, Color.WHITE); + g2.setPaint(gradient); + int[] xcoords = { x + 25, x, x + 50 }; + int[] ycoords = { y, y + 50, y + 50 }; + GeneralPath polygon = new GeneralPath(GeneralPath.WIND_EVEN_ODD, xcoords.length); + polygon.moveTo(x + 25, y); + for (int i = 0; i < xcoords.length; i++) + { + polygon.lineTo(xcoords[i], ycoords[i]); + } + polygon.closePath(); + g2.fill(polygon); + BasicStroke wideStroke = new BasicStroke(2.0f); + g2.setColor(Color.black); + g2.setStroke(wideStroke); + g2.draw(polygon); + } + } +} \ No newline at end of file diff --git a/examples/servicebased.triangle/src/main/resources/META-INF/LICENSE b/examples/servicebased.triangle/src/main/resources/META-INF/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/examples/servicebased.triangle/src/main/resources/META-INF/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/examples/servicebased.triangle/src/main/resources/META-INF/NOTICE b/examples/servicebased.triangle/src/main/resources/META-INF/NOTICE new file mode 100644 index 00000000000..6d0c83b51fb --- /dev/null +++ b/examples/servicebased.triangle/src/main/resources/META-INF/NOTICE @@ -0,0 +1,5 @@ +Apache Felix Service-Based Triangle Service Example +Copyright 2006 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/examples/servicebased.triangle/src/main/resources/org/apache/felix/example/servicebased/triangle/triangle.png b/examples/servicebased.triangle/src/main/resources/org/apache/felix/example/servicebased/triangle/triangle.png new file mode 100644 index 00000000000..46a52882aef Binary files /dev/null and b/examples/servicebased.triangle/src/main/resources/org/apache/felix/example/servicebased/triangle/triangle.png differ diff --git a/examples/simple/LICENSE b/examples/simple/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/examples/simple/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/examples/simple/NOTICE b/examples/simple/NOTICE new file mode 100644 index 00000000000..c80cf6cdf20 --- /dev/null +++ b/examples/simple/NOTICE @@ -0,0 +1,5 @@ +Apache Felix Simple Bundle +Copyright 2006 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/examples/simple/build.xml b/examples/simple/build.xml new file mode 100644 index 00000000000..cd7167a4da1 --- /dev/null +++ b/examples/simple/build.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/simple/doc/index.html b/examples/simple/doc/index.html new file mode 100644 index 00000000000..5015338ea6f --- /dev/null +++ b/examples/simple/doc/index.html @@ -0,0 +1,36 @@ + + + + + + Simple + + + +

      Simple
      +

      +

      Description
      +A very simple bundle that demonstrates four different issues. It +demonstrates the basics for creating a bundle. It also +demonstates how to include an embedded JAR file in a bundle. It +demonstrates how to include a native library in a bundle; currently the +example only supports Linux on a PC, but other platforms will work +similarly. Lastly, it demonstrates dynamic imports.
      +

      +

      Contributor(s)
      +Richard S. Hall (heavy@ungovenred.org)
      +

      +

      License
      +BSD
      +

      +

      Services
      +None

      +

      Properties
      +None

      +

      Requirements
      +
      None

      +
      + + diff --git a/examples/simple/lib/org.osgi.core-0.8.0-SNAPSHOT.jar b/examples/simple/lib/org.osgi.core-0.8.0-SNAPSHOT.jar new file mode 100644 index 00000000000..150cf9ebb52 Binary files /dev/null and b/examples/simple/lib/org.osgi.core-0.8.0-SNAPSHOT.jar differ diff --git a/examples/simple/lib/servlet-incomplete.jar b/examples/simple/lib/servlet-incomplete.jar new file mode 100644 index 00000000000..9bfd8c9e460 Binary files /dev/null and b/examples/simple/lib/servlet-incomplete.jar differ diff --git a/examples/simple/native/build.txt b/examples/simple/native/build.txt new file mode 100644 index 00000000000..d94ebbd223a --- /dev/null +++ b/examples/simple/native/build.txt @@ -0,0 +1,6 @@ +cd ..; ant ; cd native +javah -classpath ../simple.jar -jni org.apache.felix.examples.simple.Activator +gcc -I/usr/java/jdk/include \ + -I/usr/java/jdk/include/linux \ + -c simple_bundle_foo_method.c -o simple_bundle_foo_method.o +ld -G simple_bundle_foo_method.o -o libfoo.so diff --git a/examples/simple/native/libfoo.so b/examples/simple/native/libfoo.so new file mode 100644 index 00000000000..5d246be974e Binary files /dev/null and b/examples/simple/native/libfoo.so differ diff --git a/examples/simple/native/org_apache_felix_examples_simple_Activator.h b/examples/simple/native/org_apache_felix_examples_simple_Activator.h new file mode 100644 index 00000000000..a2a5bea1caa --- /dev/null +++ b/examples/simple/native/org_apache_felix_examples_simple_Activator.h @@ -0,0 +1,21 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class org_apache_felix_examples_simple_Activator */ + +#ifndef _Included_org_apache_felix_examples_simple_Activator +#define _Included_org_apache_felix_examples_simple_Activator +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: org_apache_felix_examples_simple_Activator + * Method: foo + * Signature: ()Ljava/lang/String; + */ +JNIEXPORT jstring JNICALL Java_org_apache_felix_examples_simple_Activator_foo + (JNIEnv *, jobject); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/examples/simple/native/simple_bundle_foo_method.c b/examples/simple/native/simple_bundle_foo_method.c new file mode 100644 index 00000000000..3fd80eeaa79 --- /dev/null +++ b/examples/simple/native/simple_bundle_foo_method.c @@ -0,0 +1,29 @@ +/* + * 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. + */ + +#include "org_apache_felix_examples_simple_Activator.h" + +JNIEXPORT jstring JNICALL + Java_org_apache_felix_examples_simple_Activator_foo + (JNIEnv *env, jobject obj) +{ + char *cstr = "Hello!"; + jstring jstr = (*env)->NewStringUTF(env, cstr); + return jstr; +} diff --git a/examples/simple/src/org/apache/felix/examples/simple/Activator.java b/examples/simple/src/org/apache/felix/examples/simple/Activator.java new file mode 100644 index 00000000000..7a081be9d2d --- /dev/null +++ b/examples/simple/src/org/apache/felix/examples/simple/Activator.java @@ -0,0 +1,86 @@ +/* + * 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.examples.simple; + +import javax.servlet.Servlet; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.apache.felix.examples.simple.embedded.Embedded; + +/** + * A very simple bundle that prints out a message when it is started and + * stopped; it includes an embedded JAR file which is used on its internal + * class path. This bundle is merely a "hello world" example; it does not + * implement any services nor does it offer any configurable properties. +**/ +public class Activator implements BundleActivator +{ + private BundleContext context = null; + + public native String foo(); + + public void start(BundleContext context) throws Exception + { + System.out.println("Simple bundle " + context.getBundle().getBundleId() + + " has started."); + + this.context = context; + + // Get the OS and processor properties. + String os = + context.getProperty("org.osgi.framework.os.name").toLowerCase(); + String processor = + context.getProperty("org.osgi.framework.processor").toLowerCase(); + + // Load library if correct platform. + if (os.equals("linux") && processor.endsWith("86")) + { + try { + System.loadLibrary("foo"); + } catch (Exception ex) { + System.out.println("No library: " + ex); + ex.printStackTrace(); + } + System.out.println("From native: " + foo()); + } + + // Create class from embedded JAR file. + Embedded embedded = new Embedded(); + embedded.sayHello(); + + // Access dynamically imported servlet class. + try { + System.out.println("Class name = " + javax.servlet.http.HttpSession.class); + } catch (Throwable ex) { + System.out.println("The 'javax.servlet.http' package is not available."); + } + try { + System.out.println("Class name = " + Servlet.class); + } catch (Throwable ex) { + System.out.println("The 'javax.servlet' package is not available."); + } + } + + public void stop(BundleContext context) + { + System.out.println("Simple bundle " + context.getBundle().getBundleId() + + " has stopped."); + } +} diff --git a/examples/simple/src/org/apache/felix/examples/simple/embedded/Embedded.java b/examples/simple/src/org/apache/felix/examples/simple/embedded/Embedded.java new file mode 100644 index 00000000000..176379f57fc --- /dev/null +++ b/examples/simple/src/org/apache/felix/examples/simple/embedded/Embedded.java @@ -0,0 +1,27 @@ +/* + * 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.examples.simple.embedded; + +public class Embedded +{ + public void sayHello() + { + System.out.println("From embedded JAR: Hello!"); + } +} diff --git a/examples/simple/src/org/apache/felix/examples/simple/manifest.mf b/examples/simple/src/org/apache/felix/examples/simple/manifest.mf new file mode 100644 index 00000000000..433ff2f9fdb --- /dev/null +++ b/examples/simple/src/org/apache/felix/examples/simple/manifest.mf @@ -0,0 +1,10 @@ +Bundle-ManifestVersion: 2 +Bundle-SymbolicName: org.apache.felix.examples.simple +Bundle-Version: 1.0.0 +Bundle-Name: Simple Bundle +Bundle-Description: Simple bundle that demonstates OSGi features. +Bundle-Activator: org.apache.felix.examples.simple.Activator +Bundle-ClassPath: .,org/apache/felix/examples/simple/embedded.jar +Bundle-NativeCode: /libfoo.so; osname=Linux; processor=x86 +Import-Package: org.osgi.framework +DynamicImport-Package: javax.servlet.* diff --git a/examples/spellcheckbinder/pom.xml b/examples/spellcheckbinder/pom.xml index 03494c7cffd..3744ad475f2 100644 --- a/examples/spellcheckbinder/pom.xml +++ b/examples/spellcheckbinder/pom.xml @@ -1,66 +1,82 @@ + org.apache.felix felix - 0.8.0-SNAPSHOT + 1.0.4 + ../../pom/pom.xml 4.0.0 - osgi-bundle - Apache Felix Examples: Spell Check w/ Service Binder + bundle + Apache Felix Example Spell Check w/ Service Binder org.apache.felix.examples.spellcheckbinder + 0.9.0-SNAPSHOT ${pom.groupId} org.osgi.core - ${pom.version} + 1.0.0 provided ${pom.groupId} org.apache.felix.examples.dictionaryservice - ${pom.version} + 0.9.0-SNAPSHOT provided ${pom.groupId} org.apache.felix.examples.spellcheckservice - ${pom.version} + 0.9.0-SNAPSHOT provided ${pom.groupId} org.apache.felix.servicebinder - ${pom.version} + 0.9.0-SNAPSHOT provided - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} + org.apache.felix + maven-bundle-plugin + 1.4.0 true - - - - Metadata-Location - metadata.xml - - - Spell Check w/ ServiceBinder Example - Apache Software Foundation - + + ${pom.artifactId} + metadata.xml + Example Spell Check w/ Service Binder + The Apache Software Foundation + A bundle that registers a spell checking service based on service binder. - - + + org.apache.felix.examples.spellcheckbinder.Activator - - - org.osgi.framework, org.apache.felix.examples.dictionaryservice, org.apache.felix.servicebinder, org.apache.felix.examples.spellcheckservice - - + + + org.apache.felix.examples.spellcheckbinder.* + + diff --git a/examples/spellcheckbinder/src/main/java/org/apache/felix/examples/spellcheckbinder/Activator.java b/examples/spellcheckbinder/src/main/java/org/apache/felix/examples/spellcheckbinder/Activator.java index e5d448eb55e..7aa1505f863 100644 --- a/examples/spellcheckbinder/src/main/java/org/apache/felix/examples/spellcheckbinder/Activator.java +++ b/examples/spellcheckbinder/src/main/java/org/apache/felix/examples/spellcheckbinder/Activator.java @@ -32,7 +32,7 @@ * file. All application functionality is defined in the * SpellCheckServiceImpl.java file. * - * @author Felix Project Team + * @author Felix Project Team */ public class Activator extends GenericActivator { diff --git a/examples/spellcheckbinder/src/main/java/org/apache/felix/examples/spellcheckbinder/SpellCheckServiceImpl.java b/examples/spellcheckbinder/src/main/java/org/apache/felix/examples/spellcheckbinder/SpellCheckServiceImpl.java index 37497f0b970..095f87511fe 100644 --- a/examples/spellcheckbinder/src/main/java/org/apache/felix/examples/spellcheckbinder/SpellCheckServiceImpl.java +++ b/examples/spellcheckbinder/src/main/java/org/apache/felix/examples/spellcheckbinder/SpellCheckServiceImpl.java @@ -35,7 +35,7 @@ * dependencies to the Service Binder, which automatically manages them and it * also automatically registers the spell check services as appropriate. * - * @author Felix Project Team + * @author Felix Project Team */ public class SpellCheckServiceImpl implements SpellCheckService { diff --git a/examples/spellcheckclient/pom.xml b/examples/spellcheckclient/pom.xml index ed6a6d060be..0b1f57ae051 100644 --- a/examples/spellcheckclient/pom.xml +++ b/examples/spellcheckclient/pom.xml @@ -1,48 +1,69 @@ + org.apache.felix felix - 0.8.0-SNAPSHOT + 1.0.4 + ../../pom/pom.xml 4.0.0 - osgi-bundle - Apache Felix Examples: Spell Check Client + bundle + Apache Felix Example Spell Check Client org.apache.felix.examples.spellcheckclient + 0.9.0-SNAPSHOT ${pom.groupId} org.osgi.core - ${pom.version} + 1.0.0 provided ${pom.groupId} org.apache.felix.examples.spellcheckservice - ${pom.version} + 0.9.0-SNAPSHOT provided - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} + org.apache.felix + maven-bundle-plugin + 1.4.0 true - - Spell Check Client Example - Apache Software Foundation - + + ${pom.artifactId} + Example Spell Check Client + The Apache Software Foundation + A bundle using the spell check service. - - + + org.apache.felix.examples.spellcheckclient.Activator - - - org.osgi.framework, org.apache.felix.examples.spellcheckservice - - + + + org.apache.felix.examples.spellcheckclient.* + + diff --git a/examples/spellcheckclient/src/main/java/org/apache/felix/examples/spellcheckclient/Activator.java b/examples/spellcheckclient/src/main/java/org/apache/felix/examples/spellcheckclient/Activator.java index a8b2000d789..bb558daec64 100644 --- a/examples/spellcheckclient/src/main/java/org/apache/felix/examples/spellcheckclient/Activator.java +++ b/examples/spellcheckclient/src/main/java/org/apache/felix/examples/spellcheckclient/Activator.java @@ -31,7 +31,7 @@ /** * * - * @author Felix Project Team + * @author Felix Project Team */ public class Activator implements BundleActivator, ServiceListener { diff --git a/examples/spellcheckscr/pom.xml b/examples/spellcheckscr/pom.xml index 940bfe2eabb..2494049d358 100644 --- a/examples/spellcheckscr/pom.xml +++ b/examples/spellcheckscr/pom.xml @@ -1,66 +1,74 @@ + org.apache.felix - felix - 0.8.0-SNAPSHOT + felix-parent + 4 + ../../pom/pom.xml + 4.0.0 - osgi-bundle - Apache Felix Examples: Spell Check w/ SCR + bundle + Apache Felix Example Spell Check w/ SCR org.apache.felix.examples.spellcheckscr + 0.9.0-SNAPSHOT + A bundle that registers a spell checking service based on Service Component Runtime. + - ${pom.groupId} + org.osgi org.osgi.core - ${pom.version} + 6.0.0 provided - ${pom.groupId} - org.apache.felix.examples.dictionaryservice - ${pom.version} + org.osgi + org.osgi.service.component.annotations + 1.3.0 provided - ${pom.groupId} - org.apache.felix.examples.spellcheckservice - ${pom.version} + org.apache.felix + org.apache.felix.examples.dictionaryservice + 0.9.0-SNAPSHOT provided - ${pom.groupId} - org.apache.felix.scr - ${pom.version} + org.apache.felix + org.apache.felix.examples.spellcheckservice + 0.9.0-SNAPSHOT provided + - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} + org.apache.felix + maven-bundle-plugin + 3.0.1 true - - - - Service-Component - OSGI-INF/component.xml - - - Spell Check w/ SCR Example - Apache Software Foundation - - A bundle that registers a spell checking service based on Service Component Runtime. - - - org.apache.felix.examples.spellcheckscr.Activator - - - org.osgi.framework, org.apache.felix.examples.dictionaryservice, org.apache.felix.scr, org.apache.felix.examples.spellcheckservice - - + + The Apache Software Foundation + diff --git a/examples/spellcheckscr/src/main/java/org/apache/felix/examples/spellcheckscr/Activator.java b/examples/spellcheckscr/src/main/java/org/apache/felix/examples/spellcheckscr/Activator.java deleted file mode 100644 index 58596bb20a5..00000000000 --- a/examples/spellcheckscr/src/main/java/org/apache/felix/examples/spellcheckscr/Activator.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.examples.spellcheckscr; - - -import org.apache.felix.scr.GenericActivator; - - -/** - * This example re-implements the spell check service of Example 5 using the - * Apache Felix Service Component Runtime. - * The Service Component Runtime greatly simplifies creating OSGi applications - * by essentially eliminating the need to write OSGi-related code; - * instead of writing OSGi code for your bundle, you create a simple XML file to - * describe your bundle's service dependencies. This class extends the generic - * bundle activator; it does not provide any additional functionality. All - * functionality for service-related tasks, such as look-up and binding, is - * handled by the generic activator base class using data from the metadata.xml - * file. All application functionality is defined in the - * SpellCheckServiceImpl.java file. - * - * @author Felix Project Team - */ -public class Activator extends GenericActivator -{ -} diff --git a/examples/spellcheckscr/src/main/java/org/apache/felix/examples/spellcheckscr/SpellCheckServiceImpl.java b/examples/spellcheckscr/src/main/java/org/apache/felix/examples/spellcheckscr/SpellCheckServiceImpl.java index e5e4edd0cd5..42d174a9f83 100644 --- a/examples/spellcheckscr/src/main/java/org/apache/felix/examples/spellcheckscr/SpellCheckServiceImpl.java +++ b/examples/spellcheckscr/src/main/java/org/apache/felix/examples/spellcheckscr/SpellCheckServiceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2006 The Apache Software Foundation + * Copyright 2006-2016 The Apache Software Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,71 +18,49 @@ import java.util.ArrayList; +import java.util.List; import java.util.StringTokenizer; import org.apache.felix.examples.dictionaryservice.DictionaryService; import org.apache.felix.examples.spellcheckservice.SpellCheckService; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; /** - * This class re-implements the spell check service of Example 5. This service - * implementation behaves exactly like the one in Example 5, specifically, it + * This class re-implements the spell check service of Example 6. This service + * implementation behaves exactly like the one in Example 6, specifically, it * aggregates all available dictionary services, monitors their dynamic * availability, and only offers the spell check service if there are dictionary * services available. The service implementation is greatly simplified, though, * by using the Service Component Runtime. Notice that there is no OSGi references in the - * application code; intead, the metadata.xml file describes the service + * application code; instead, the annotations describe the service * dependencies to the Service Component Runtime, which automatically manages them and it * also automatically registers the spell check services as appropriate. - * - * @author Felix Project Team + * + * @author Felix Project Team */ +@Component public class SpellCheckServiceImpl implements SpellCheckService { - // List of service objects. - private ArrayList m_svcObjList = new ArrayList(); - - /** - * This method is used by the Service Component Runtime to add new dictionaries to the - * spell check service. - * - * @param dictionary - * the dictionary to add to the spell check service. + * List of service objects. + * + * This field is managed by the Service Component Runtime and updated + * with the current set of available dictionary services. + * At least one dictionary service is required. */ - public void addDictionary( DictionaryService dictionary ) - { - // Lock list and add service object. - synchronized ( m_svcObjList ) - { - m_svcObjList.add( dictionary ); - } - } - - - /** - * This method is used by the Service Component Runtime to remove dictionaries from the - * spell check service. - * - * @param dictionary - * the dictionary to remove from the spell check service. - */ - public void removeDictionary( DictionaryService dictionary ) - { - // Lock list and remove service object. - synchronized ( m_svcObjList ) - { - m_svcObjList.remove( dictionary ); - } - } - + @Reference(policy=ReferencePolicy.DYNAMIC, cardinality=ReferenceCardinality.AT_LEAST_ONE) + private volatile List m_svcObjList; /** * Checks a given passage for spelling errors. A passage is any number of * words separated by a space and any of the following punctuation marks: * comma (,), period (.), exclamation mark (!), question mark (?), * semi-colon (;), and colon(:). - * + * * @param passage * the passage to spell check. * @return An array of misspelled words or null if no words are misspelled. @@ -95,47 +73,45 @@ public String[] check( String passage ) return null; } - ArrayList errorList = new ArrayList(); + List errorList = new ArrayList(); // Tokenize the passage using spaces and punctionation. StringTokenizer st = new StringTokenizer( passage, " ,.!?;:" ); - // Lock the service list. - synchronized ( m_svcObjList ) + // Put the current set of services in a local field + // the field m_svcObjList might be modified concurrently + final List localServices = m_svcObjList; + + // Loop through each word in the passage. + while ( st.hasMoreTokens() ) { - // Loop through each word in the passage. - while ( st.hasMoreTokens() ) - { - String word = st.nextToken(); - boolean correct = false; + String word = st.nextToken(); + boolean correct = false; - // Check each available dictionary for the current word. - for ( int i = 0; ( !correct ) && ( i < m_svcObjList.size() ); i++ ) + // Check each available dictionary for the current word. + for(final DictionaryService dictionary : localServices) { + if ( dictionary.checkWord( word ) ) { - DictionaryService dictionary = ( DictionaryService ) m_svcObjList.get( i ); - - if ( dictionary.checkWord( word ) ) - { - correct = true; - } + correct = true; + break; } + } - // If the word is not correct, then add it - // to the incorrect word list. - if ( !correct ) - { - errorList.add( word ); - } + // If the word is not correct, then add it + // to the incorrect word list. + if ( !correct ) + { + errorList.add( word ); } } // Return null if no words are incorrect. - if ( errorList.size() == 0 ) + if ( errorList.isEmpty() ) { return null; } // Return the array of incorrect words. - return ( String[] ) errorList.toArray( new String[errorList.size()] ); + return errorList.toArray( new String[errorList.size()] ); } } diff --git a/examples/spellcheckscr/src/main/resources/OSGI-INF/component.properties b/examples/spellcheckscr/src/main/resources/OSGI-INF/component.properties deleted file mode 100644 index 467c98cd475..00000000000 --- a/examples/spellcheckscr/src/main/resources/OSGI-INF/component.properties +++ /dev/null @@ -1,9 +0,0 @@ - -service.pid=org.apache.felix.examples.spellcheckscr -# service.ranking=10 -service.vendor=Apache Software Foundation - -foo=bar -tic=tac toe - - diff --git a/examples/spellcheckscr/src/main/resources/OSGI-INF/component.xml b/examples/spellcheckscr/src/main/resources/OSGI-INF/component.xml deleted file mode 100644 index 624b2a7d9d9..00000000000 --- a/examples/spellcheckscr/src/main/resources/OSGI-INF/component.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/examples/spellcheckservice/pom.xml b/examples/spellcheckservice/pom.xml index a7b8ab6c1e6..d9e26823a26 100644 --- a/examples/spellcheckservice/pom.xml +++ b/examples/spellcheckservice/pom.xml @@ -1,51 +1,69 @@ + org.apache.felix felix - 0.8.0-SNAPSHOT + 1.0.4 + ../../pom/pom.xml 4.0.0 - osgi-bundle - Apache Felix Examples: Spell Check Service + bundle + Apache Felix Example Spell Check Service org.apache.felix.examples.spellcheckservice + 0.9.0-SNAPSHOT ${pom.groupId} org.osgi.core - ${pom.version} + 1.0.0 provided ${pom.groupId} org.apache.felix.examples.dictionaryservice - ${pom.version} + 0.9.0-SNAPSHOT provided - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} + org.apache.felix + maven-bundle-plugin + 1.4.0 true - - Spell Check Service Example - Apache Software Foundation - + + ${pom.artifactId} + Example Spell Check Service + The Apache Software Foundation + A bundle that registers a spell checking service. - - + + org.apache.felix.examples.spellcheckservice.impl.Activator - - - org.osgi.framework, org.apache.felix.examples.dictionaryservice - - - org.apache.felix.examples.spellcheckservice - - + + + org.apache.felix.examples.spellcheckservice.* + + diff --git a/examples/spellcheckservice/src/main/java/org/apache/felix/examples/spellcheckservice/SpellCheckService.java b/examples/spellcheckservice/src/main/java/org/apache/felix/examples/spellcheckservice/SpellCheckService.java index d1c58137066..1800b03f5a8 100644 --- a/examples/spellcheckservice/src/main/java/org/apache/felix/examples/spellcheckservice/SpellCheckService.java +++ b/examples/spellcheckservice/src/main/java/org/apache/felix/examples/spellcheckservice/SpellCheckService.java @@ -23,7 +23,7 @@ * number of words separated by a space character and the following punctuation * marks: comma, period, exclamation mark, question mark, semi-colon, and colon. * - * @author Felix Project Team + * @author Felix Project Team */ public interface SpellCheckService { @@ -37,4 +37,4 @@ public interface SpellCheckService * @return An array of misspelled words or null if no words are misspelled. */ public String[] check( String passage ); -} \ No newline at end of file +} diff --git a/examples/spellcheckservice/src/main/java/org/apache/felix/examples/spellcheckservice/impl/Activator.java b/examples/spellcheckservice/src/main/java/org/apache/felix/examples/spellcheckservice/impl/Activator.java index 5de8ebbaf87..58418199b14 100644 --- a/examples/spellcheckservice/src/main/java/org/apache/felix/examples/spellcheckservice/impl/Activator.java +++ b/examples/spellcheckservice/src/main/java/org/apache/felix/examples/spellcheckservice/impl/Activator.java @@ -42,7 +42,7 @@ * thus the spell check service will appear and disappear as dictionary services * appear and disappear, respectively. * - * @author Felix Project Team + * @author Felix Project Team */ public class Activator implements BundleActivator, ServiceListener { diff --git a/fileinstall-plugins/installer.test/.gitignore b/fileinstall-plugins/installer.test/.gitignore new file mode 100644 index 00000000000..ae3c1726048 --- /dev/null +++ b/fileinstall-plugins/installer.test/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/fileinstall-plugins/installer.test/README.md b/fileinstall-plugins/installer.test/README.md new file mode 100644 index 00000000000..b72e07798a8 --- /dev/null +++ b/fileinstall-plugins/installer.test/README.md @@ -0,0 +1,6 @@ +# Installer Test + +This project tests the BAR Installer in an OSGi container and utilizes the [PaxExam](https://ops4j1.jira.com/wiki/display/PAXEXAM4/Pax+Exam ) framework. Because this test is an integration test rather than a unit test, it must be in a separate project. + +### Dependencies +This project depends on [**org.apache.felix.fileinstall.plugin.installer**](../installer) and [**org.apache.felix.fileinstall.plugin.resolver**](../resolver). \ No newline at end of file diff --git a/fileinstall-plugins/installer.test/pom.xml b/fileinstall-plugins/installer.test/pom.xml new file mode 100644 index 00000000000..e19914f2caa --- /dev/null +++ b/fileinstall-plugins/installer.test/pom.xml @@ -0,0 +1,206 @@ + + + + 4.0.0 + + org.apache.felix + felix-parent + 3 + ../../pom/pom.xml + + + org.apache.felix.fileinstall.plugins.installer.test + 1.0.0-SNAPSHOT + Apache Felix File Install Plugin - Artifact Installer Integration Test + + + 8 + java18 + + + + + + org.apache.felix + org.apache.felix.framework + 5.6.8 + + + + org.apache.felix + org.apache.felix.fileinstall.plugins.installer + ${project.version} + + + + org.apache.felix + org.apache.felix.fileinstall.plugins.resolver + ${project.version} + + + + javax.inject + javax.inject + 1 + + + org.ops4j.pax.exam + pax-exam-junit4 + 4.9.2 + test + + + org.ops4j.pax.exam + pax-exam-container-forked + 4.9.2 + test + + + org.ops4j.pax.exam + pax-exam-link-mvn + 4.9.2 + test + + + + org.apache.felix + org.apache.felix.configadmin + 1.8.12 + test + + + org.apache.felix + org.apache.felix.scr + 2.0.6 + test + + + org.apache.felix + org.apache.felix.log + 1.0.1 + test + + + org.apache.felix + org.apache.felix.resolver + 1.10.1 + test + + + org.apache.felix + org.apache.felix.fileinstall + 3.5.4 + test + + + org.osgi + org.osgi.service.repository + 1.1.0 + test + + + biz.aQute.bnd + biz.aQute.bndlib + 3.4.0 + test + + + biz.aQute.bnd + biz.aQute.repository + 3.4.0 + test + + + + + + + + + org.apache.servicemix.tooling + depends-maven-plugin + 1.2 + + + generate-depends-file + + generate-depends-file + + + + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + + org.apache.servicemix.tooling + + + depends-maven-plugin + + + [1.2,) + + + + generate-depends-file + + + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + [1.8,) + + + + run + + + + + + + + + + + + + + + diff --git a/fileinstall-plugins/installer.test/src/test/java/org/apache/felix/fileinstall/plugins/installer/test/InstallerIntegrationTest.java b/fileinstall-plugins/installer.test/src/test/java/org/apache/felix/fileinstall/plugins/installer/test/InstallerIntegrationTest.java new file mode 100644 index 00000000000..051e0328fa3 --- /dev/null +++ b/fileinstall-plugins/installer.test/src/test/java/org/apache/felix/fileinstall/plugins/installer/test/InstallerIntegrationTest.java @@ -0,0 +1,460 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer.test; + +import static org.junit.Assert.*; +import static org.ops4j.pax.exam.CoreOptions.*; + +import java.io.File; +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.inject.Inject; + +import org.apache.felix.fileinstall.ArtifactInstaller; +import org.apache.felix.fileinstall.plugins.installer.FrameworkInstaller; +import org.apache.felix.fileinstall.plugins.installer.InstallableListener; +import org.apache.felix.fileinstall.plugins.installer.InstallableUnit; +import org.apache.felix.fileinstall.plugins.installer.InstallableUnitEvent; +import org.apache.felix.fileinstall.plugins.installer.State; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.Configuration; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerMethod; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceRegistration; +import org.osgi.util.tracker.ServiceTracker; + +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerMethod.class) +public class InstallerIntegrationTest { + + @Configuration + public Option[] config() { + return options( + // vmOptions("-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000"), + systemProperty("org.apache.felix.fileinstall.plugins.installer.debug").value("true"), + mavenBundle(FELIX_GROUPID, "org.apache.felix.configadmin").versionAsInProject(), + mavenBundle(FELIX_GROUPID, "org.apache.felix.scr").versionAsInProject(), + mavenBundle(FELIX_GROUPID, "org.apache.felix.log").versionAsInProject(), + mavenBundle(FELIX_GROUPID, "org.apache.felix.resolver").versionAsInProject(), + // NB FileInstall included for its API, but NOT started so the directory polling doesn't happen + mavenBundle(FELIX_GROUPID, "org.apache.felix.fileinstall").versionAsInProject().start(false), + mavenBundle("org.osgi", "org.osgi.service.repository").versionAsInProject(), + mavenBundle("biz.aQute.bnd", "biz.aQute.bndlib").versionAsInProject(), + mavenBundle("biz.aQute.bnd", "biz.aQute.repository").versionAsInProject(), + mavenBundle(FELIX_GROUPID, "org.apache.felix.fileinstall.plugins.resolver").versionAsInProject(), + mavenBundle(FELIX_GROUPID, "org.apache.felix.fileinstall.plugins.installer").versionAsInProject(), + junitBundles(), + // javax.xml exports required as version=1.0 for bnd repository + systemPackage("javax.xml.namespace;version=1.0.0"), + systemPackage("javax.xml.stream;version=1.0.0") + ); + } + + private static final String FELIX_GROUPID = "org.apache.felix"; + + @Inject + private BundleContext bundleContext; + + private final File dataDir = new File("./target/test-classes/"); + + private ServiceTracker artifactInstallerTracker; + private ServiceTracker fwkInstallerTracker; + + @Before + public void before() throws Exception { + assertAllBundlesResolved(); + + // Track ArtifactInstaller service ONLY from bundle org.apache.felix.fileinstall.plugins.installer, to avoid potential interference + Bundle installerBundle = findBundle("org.apache.felix.fileinstall.plugins.installer"); + Filter artifactInstallerTrackerFilter = FrameworkUtil.createFilter(String.format("(&(objectClass=%s)(service.bundleid=%d))", ArtifactInstaller.class.getName(), installerBundle.getBundleId())); + this.artifactInstallerTracker = new ServiceTracker<>(this.bundleContext, artifactInstallerTrackerFilter, null); + this.artifactInstallerTracker.open(); + + // Wait up to 5 seconds for ArtifactInstaller to appear + ArtifactInstaller artifactInstaller = this.artifactInstallerTracker.waitForService(5000); + if (artifactInstaller == null) { + fail("ArtifactInstaller service not available within 5 seconds"); + } + + this.fwkInstallerTracker = new ServiceTracker<>(this.bundleContext, FrameworkInstaller.class, null); + this.fwkInstallerTracker.open(); + } + + @After + public void after() { + this.fwkInstallerTracker.close(); + this.artifactInstallerTracker.close(); + } + + @Test + public void testErrorFromInvalidArchive() throws Exception { + Object[] artifactInstallers = this.artifactInstallerTracker.getServices(); + assertNotNull("Should be exactly one ArtifactInstaller service", artifactInstallers); + assertEquals("Should be exactly one ArtifactInstaller service", 1, artifactInstallers.length); + + // Register mock InstallableListener + List installEvents = new LinkedList<>(); + InstallableListener mockInstallListener = new InstallableListener() { + @Override + public void installableUnitsChanged(Collection events) { + for (InstallableUnitEvent event : events) { + String message = String.format("%s %s", event.getNewState(), event.getUnit().getSymbolicName()); + installEvents.add(message); + } + } + }; + + ServiceRegistration mockListenerReg = this.bundleContext.registerService(InstallableListener.class, mockInstallListener, null); + try { + assertEquals("Shouldn't be any install events yet", 0, installEvents.size()); + + // Provide the sample archive + File sampleArchive = new File(this.dataDir, "org.example.invalid-missing-rb.bar"); + ArtifactInstaller installer = this.artifactInstallerTracker.getService(); + assertTrue("installer should handle sample archive", installer.canHandle(sampleArchive)); + installer.install(sampleArchive); + + // Wait for resolve to occur + Thread.sleep(2000); + assertEquals(1, installEvents.size()); + assertEquals("ERROR org.example.invalid-missing-rb", installEvents.get(0)); + } finally { + mockListenerReg.unregister(); + } + } + + private class EventQueue implements InstallableListener { + private final Queue q = new LinkedList<>(); + @Override + public synchronized void installableUnitsChanged(Collection events) { + for (InstallableUnitEvent event : events) { + this.q.add(event); + System.out.printf("!!! added event %s (%s), queue depth is now %d%n ", event.getNewState(), event.getUnit().getSymbolicName(), this.q.size()); + } + } + private synchronized InstallableUnitEvent pop() { + if (this.q.isEmpty()) { + throw new IllegalStateException("Trying to take from empty queue"); + } + InstallableUnitEvent event = this.q.remove(); + System.out.printf("!!! removed event %s (%s), queue depth is now %d%n ", event.getNewState(), event.getUnit().getSymbolicName(), this.q.size()); + return event; + } + private synchronized int depth() { + return this.q.size(); + } + } + + /* + * This is a complex case that tests the interplay of multiple archives with + * overlapping content. + * + * - Archive 1 contains bundles A and B. + * + * - Archive 2 contains bundles A and C. + * + * The process is as follows: + * + * i. Make installer aware of both archives. Both now present as an + * installable unit with 2 artifacts (A+B and A+C). + * + * ii. Install archive 1. + * Archive 2 gets invalidated and re-resolved. It now presents as an + * installable unit with 1 artifact (C) because A is already in the + * framework. + * + * iii. Remove archive 1. Archive 2 gets invalidated and + * re-resolved. It now presents as an installable unit with 2 artifacts + * (A+C) because A is no longer in the framework. + * + * Note that step (iii) is equivalent to deleting the archive from + * FileInstall's load directory. Therefore it doesn't reappear as a RESOLVED + * installable unit. To reinstall this archive, the user would have to copy + * it back into the load directory. + * + */ + @Test + public void testInstallArchives() throws Exception { + // Register InstallableListener + EventQueue eventQueue = new EventQueue(); + ServiceRegistration mockListenerReg = this.bundleContext.registerService(InstallableListener.class, eventQueue, null); + try { + assertEquals("Shouldn't be any install events yet", 0, eventQueue.depth()); + InstallableUnitEvent event; + + // Provide the sample archive + File sampleArchive1 = new File(this.dataDir, "valid1.bar"); + ArtifactInstaller installer = this.artifactInstallerTracker.getService(); + assertNotNull("ArtifactInstaller service should exist", installer); + assertTrue("installer should handle sample archive", installer.canHandle(sampleArchive1)); + installer.install(sampleArchive1); + + // Wait for resolve to occur + Thread.sleep(2000); + assertEquals(1, eventQueue.depth()); + event = eventQueue.pop(); + InstallableUnit unit1 = event.getUnit(); + + assertEquals(State.RESOLVED, event.getNewState()); + assertEquals("samples.valid1", event.getUnit().getSymbolicName()); + assertEquals("samples.valid1 requires 2 bundles to be installed", 2, event.getUnit().getArtifacts().size()); + + // Add a second archive + File sampleArchive2 = new File(this.dataDir, "valid2.bar"); + assertTrue("installer should handle sample archive", installer.canHandle(sampleArchive2)); + installer.install(sampleArchive2); + + // Wait for resolve to occur + Thread.sleep(2000); + assertEquals(1, eventQueue.depth()); + event = eventQueue.pop(); + assertEquals(State.RESOLVED, event.getNewState()); + assertEquals("samples.valid2", event.getUnit().getSymbolicName()); + assertEquals("samples.valid2 requires 2 bundles to be installed", 2, event.getUnit().getArtifacts().size()); + + // Install first archive. + unit1.install().getValue(); + // Check bundles were actually installed! + assertNotNull(findBundle("org.example.a")); + assertNotNull(findBundle("org.example.b")); + + // The installation of first archive should cause invalidation of second archive resolution result, followed by re-resolve. + // First, installing #1 + event = eventQueue.pop(); + assertEquals(State.INSTALLING, event.getNewState()); + assertEquals("samples.valid1", event.getUnit().getSymbolicName()); + + // Next installed #1 + event = eventQueue.pop(); + assertEquals(State.INSTALLED, event.getNewState()); + assertEquals("samples.valid1", event.getUnit().getSymbolicName()); + + // Next removed #2 (due to invalidation) + // Short sleep necessary because the invalidate can happen on another thread (eg framework refresh) + Thread.sleep(500); + event = eventQueue.pop(); + assertEquals(State.REMOVED, event.getNewState()); + assertEquals("samples.valid2", event.getUnit().getSymbolicName()); + + // Next resolved #2 (re-resolve after invalidation) + // Another sleep because the re-resolve happens after at least 1 sec delay + Thread.sleep(2000); + event = eventQueue.pop(); + assertEquals(State.RESOLVED, event.getNewState()); + assertEquals("samples.valid2", event.getUnit().getSymbolicName()); + assertEquals("samples.valid2 now requires 1 bundle to be installed", 1, event.getUnit().getArtifacts().size()); + assertEquals(0, eventQueue.depth()); + + // Now uninstall first archive + installer.uninstall(sampleArchive1); + Thread.sleep(500); + // Check bundles were actually uninstalled! + assertNull(findBundle("org.example.a")); + assertNull(findBundle("org.example.b")); + + // Removed event for #1 + Thread.sleep(500); + event = eventQueue.pop(); + assertEquals(State.REMOVED, event.getNewState()); + assertEquals("samples.valid1", event.getUnit().getSymbolicName()); + + // Removed event for #2 due to invalidation + event = eventQueue.pop(); + assertEquals(State.REMOVED, event.getNewState()); + assertEquals("samples.valid2", event.getUnit().getSymbolicName()); + + // Re-resolution of #2 after a little sleep + Thread.sleep(2000); + event = eventQueue.pop(); + assertEquals(State.RESOLVED, event.getNewState()); + assertEquals("samples.valid2", event.getUnit().getSymbolicName()); + assertEquals("samples.valid2 now requires 2 bundle to be installed", 2, event.getUnit().getArtifacts().size()); + assertEquals(0, eventQueue.depth()); + + } finally { + mockListenerReg.unregister(); + } + } + + @Test + public void testAddRemoveBundles() throws Exception { + // Precondition: no example bundles + assertNull("example bundle already present - broken prior test?", findBundle("org.example.a")); + assertNull("example bundle already present - broken prior test?", findBundle("org.example.b")); + + FrameworkInstaller installer = this.fwkInstallerTracker.getService(); + assertNotNull("installer service not found", installer); + + // Install bundles and check they were installed + File fileA = new File(this.dataDir, "org.example.a.jar"); + File fileB = new File(this.dataDir, "org.example.b.jar"); + List locations = Stream.of(fileA, fileB) + .map(File::toURI) + .map(URI::toString) + .collect(Collectors.toList()); + Object sponsor = new Object(); + List installed = installer.addLocations(sponsor, locations); + assertEquals(2, installed.size()); + assertEquals("org.example.a", installed.get(0).getSymbolicName()); + assertEquals("org.example.b", installed.get(1).getSymbolicName()); + + assertNotNull("example bundle not present after install", findBundle("org.example.a")); + assertNotNull("example bundle not present after install", findBundle("org.example.b")); + + // Uninstall bundles and check they are gone + Set removed = installer.removeSponsor(sponsor).stream().map(Bundle::getSymbolicName).collect(Collectors.toSet()); + assertEquals(2, removed.size()); + assertTrue(removed.contains("org.example.a")); + assertTrue(removed.contains("org.example.b")); + + assertNull("example bundle still present after uninstall", findBundle("org.example.a")); + assertNull("example bundle still present after uninstall", findBundle("org.example.b")); + } + + @Test + public void testAddRemoveOverlappingBundles() throws Exception { + // Precondition: no example bundles + assertNull("example bundle already present - broken prior test?", findBundle("org.example.a")); + assertNull("example bundle already present - broken prior test?", findBundle("org.example.b")); + assertNull("example bundle already present - broken prior test?", findBundle("org.example.c")); + + FrameworkInstaller installer = this.fwkInstallerTracker.getService(); + assertNotNull("installer service not found", installer); + + File fileA = new File(this.dataDir, "org.example.a.jar"); + File fileB = new File(this.dataDir, "org.example.b.jar"); + File fileC = new File(this.dataDir, "org.example.c.jar"); + + // Install first set including A and B + Object sponsor1 = new Object(); + List locations1 = Stream.of(fileA, fileB) + .map(File::toURI) + .map(URI::toString) + .collect(Collectors.toList()); + List installed1 = installer.addLocations(sponsor1, locations1); + assertEquals(2, installed1.size()); + assertEquals("org.example.a", installed1.get(0).getSymbolicName()); + assertEquals("org.example.b", installed1.get(1).getSymbolicName()); + + assertNotNull("example bundle not present after install", findBundle("org.example.a")); + assertNotNull("example bundle not present after install", findBundle("org.example.b")); + assertNull("incorrect bundle installed", findBundle("org.example.c")); + + // Install second set including A and C + Object sponsor2 = new Object(); + List locations2 = Stream.of(fileA, fileC) + .map(File::toURI) + .map(URI::toString) + .collect(Collectors.toList()); + List installed2 = installer.addLocations(sponsor2, locations2); + assertEquals(1, installed2.size()); + + assertEquals("org.example.c", installed2.get(0).getSymbolicName()); + assertNotNull("example bundle not present after install", findBundle("org.example.a")); + assertNotNull("example bundle not present after install", findBundle("org.example.b")); + assertNotNull("example bundle not present after install", findBundle("org.example.c")); + + // Uninstall first set - bundle B should go away but not A + Set removed1 = installer.removeSponsor(sponsor1).stream().map(Bundle::getSymbolicName).collect(Collectors.toSet()); + assertEquals(1, removed1.size()); + assertTrue(removed1.contains("org.example.b")); + + assertNotNull("example bundle should still be present", findBundle("org.example.a")); + assertNull("example bundle still present after uninstall", findBundle("org.example.b")); + assertNotNull("example bundle should still be present", findBundle("org.example.c")); + + // Uninstall second set - all bundles should go away + Set removed2 = installer.removeSponsor(sponsor2).stream().map(Bundle::getSymbolicName).collect(Collectors.toSet()); + assertEquals(2, removed2.size()); + assertTrue(removed2.contains("org.example.a")); + assertTrue(removed2.contains("org.example.c")); + + assertNull("example bundle still present after uninstall", findBundle("org.example.a")); + assertNull("example bundle still present after uninstall", findBundle("org.example.b")); + assertNull("example bundle still present after uninstall", findBundle("org.example.c")); + } + + @Test + public void testDontRemoveExistingBundle() throws Exception { + // Precondition: no example bundles + assertNull("example bundle already present - broken prior test?", findBundle("org.example.a")); + assertNull("example bundle already present - broken prior test?", findBundle("org.example.b")); + + FrameworkInstaller installer = this.fwkInstallerTracker.getService(); + assertNotNull("installer service not found", installer); + + Bundle existingBundle = findBundle("org.apache.felix.scr"); + assertNotNull("SCR bundle has been uninstalled -- broken prior test?", existingBundle); + + String locationA = new File(this.dataDir, "org.example.a.jar").toURI().toString(); + String locationB = new File(this.dataDir, "org.example.b.jar").toURI().toString(); + String existingLocation = existingBundle.getLocation(); + + // Install set including A, B and SCR + Object sponsor = new Object(); + List locations = Arrays.asList(locationA, locationB, existingLocation); + installer.addLocations(sponsor, locations); + assertNotNull("example bundle not present after install", findBundle("org.example.a")); + assertNotNull("example bundle not present after install", findBundle("org.example.b")); + assertNotNull("preexisting bundle not present after install", findBundle("org.apache.felix.scr")); + + // Uninstall set - bundles A and B should go away but not SCR + installer.removeSponsor(sponsor); + assertNull("example bundle still present after uninstall", findBundle("org.example.a")); + assertNull("example bundle still present after uninstall", findBundle("org.example.b")); + assertNotNull("preexisting bundle uninstalled", findBundle("org.apache.felix.scr")); + } + + + private Bundle findBundle(String bsn) { + Bundle found = null; + for (Bundle bundle : this.bundleContext.getBundles()) { + if (bsn.equals(bundle.getSymbolicName()) && bundle.getState() != Bundle.UNINSTALLED) { + if (found != null && bundle.getBundleId() != found.getBundleId()) { + throw new IllegalArgumentException("Ambiguous bundle symbolic name " + bsn); + } + found = bundle; + } + } + return found; + } + + private void assertAllBundlesResolved() { + int mask = Bundle.RESOLVED | Bundle.STARTING | Bundle.STOPPING | Bundle.ACTIVE; + for (Bundle bundle : this.bundleContext.getBundles()) { + assertTrue("Unresolved bundle " + bundle.getSymbolicName(), (bundle.getState() & mask) > 0); + } + } + +} diff --git a/fileinstall-plugins/installer.test/src/test/resources/org.example.a.jar b/fileinstall-plugins/installer.test/src/test/resources/org.example.a.jar new file mode 100644 index 00000000000..74fcf694c54 Binary files /dev/null and b/fileinstall-plugins/installer.test/src/test/resources/org.example.a.jar differ diff --git a/fileinstall-plugins/installer.test/src/test/resources/org.example.b.jar b/fileinstall-plugins/installer.test/src/test/resources/org.example.b.jar new file mode 100644 index 00000000000..5b5accd4a3c Binary files /dev/null and b/fileinstall-plugins/installer.test/src/test/resources/org.example.b.jar differ diff --git a/fileinstall-plugins/installer.test/src/test/resources/org.example.c.jar b/fileinstall-plugins/installer.test/src/test/resources/org.example.c.jar new file mode 100644 index 00000000000..2b61c141100 Binary files /dev/null and b/fileinstall-plugins/installer.test/src/test/resources/org.example.c.jar differ diff --git a/fileinstall-plugins/installer.test/src/test/resources/org.example.invalid-missing-rb.bar b/fileinstall-plugins/installer.test/src/test/resources/org.example.invalid-missing-rb.bar new file mode 100644 index 00000000000..e9fdaf43291 Binary files /dev/null and b/fileinstall-plugins/installer.test/src/test/resources/org.example.invalid-missing-rb.bar differ diff --git a/fileinstall-plugins/installer.test/src/test/resources/valid1.bar b/fileinstall-plugins/installer.test/src/test/resources/valid1.bar new file mode 100644 index 00000000000..0892ccadd53 Binary files /dev/null and b/fileinstall-plugins/installer.test/src/test/resources/valid1.bar differ diff --git a/fileinstall-plugins/installer.test/src/test/resources/valid2.bar b/fileinstall-plugins/installer.test/src/test/resources/valid2.bar new file mode 100644 index 00000000000..054276b59a3 Binary files /dev/null and b/fileinstall-plugins/installer.test/src/test/resources/valid2.bar differ diff --git a/fileinstall-plugins/installer/.gitignore b/fileinstall-plugins/installer/.gitignore new file mode 100644 index 00000000000..ae3c1726048 --- /dev/null +++ b/fileinstall-plugins/installer/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/fileinstall-plugins/installer/README.md b/fileinstall-plugins/installer/README.md new file mode 100644 index 00000000000..7d9fdcc9a41 --- /dev/null +++ b/fileinstall-plugins/installer/README.md @@ -0,0 +1,50 @@ +Bundle Archive Installer Plugin +=============================== + +The purpose of this plugin is to provide an Artifact Installer for Bundle Archive (BAR) files. + + +Bundle Archive (BAR) Files +-------------------------- + +A Bundle Archive is a new file format for installable functionality represented as a collection of one or more OSGi bundles. The purpose is to provide a single file that can be easily deployed onto a server or remote device without conflicting with existing installed bundles. + +The purpose of a BAR is similar to a "Deployment Package" as defined by the OSGi Deployment Admin Specification (see OSGi Release 6 Compendium, Chapter 114). However, two or more Deployment Packages cannot include or reference the same resource, which is problematic because real-world software units often have overlapping requirements, e.g. shared libraries. In contrast multiple BAR files can refer to the same resource, and this resource will be installed only once at runtime. A reference-counting mechanism is used to ensure that shared resources are not uninstalled until the last BAR using that resource is uninstalled. + +A BAR is physically a JAR file with a META-INF/MANIFEST.MF. It must be named with the `.bar` extension and it must contain one or more index files in the format specified by OSGi Release 6 Compendium, Section 132.5 "XML Repository Format". + +The manifest should contain the following headers: + +* `Deployment-SymbolicName` (mandatory): the symbolic name of the deployable unit; +* `Deployment-Version` (optional, defaults to `0.0.0`): the version of the deployable unit; +* `Deployment-Name`: (optional): a human-readable name for the deployable unit; +* `Index-Path`: (optional, defaults to "./index.xml"): a comma-separated list of paths to index files within the BAR. +* `Require-Bundle` **AND/OR** `Require-Capability` (at least one must be present): a root list of requirements which shall be resolved against the indexes listed under `Index-Path`. + + +Resolving Process +----------------- + +A BAR file can be made available for installation by dropping a file with the extension `.bar` into a FileInstall-monitored directory. The BAR installer plugin will then resolve the requirements of the BAR listed under `Require-Bundle` and `Require-Capability` against the indexed listed under `Index-Path`, and using the context of existing installed bundles in the OSGi Framework. + +If resolution fails, e.g. due to missing resources, then an `InstallableUnitEvent` with an `ERROR` state will be sent to all registered `InstallableListener` services. + +If the resolution succeeds then an `InstallableUnitEvent` with a state of `RESOLVED` is sent to all registered `InstallableListener` services. The event shall include an `InstallableUnit` object listing all of the artifacts that would need to be installed into the present OSGi Framework in order to successfully install the BAR. + +Note that the plugin DOES NOT automatically install any resources into the OSGi Framework, it merely makes the unit available to be installed. In order to actually install, an application or management agent is required to implement the `InstallableListener` service and/or interact with the provided `InstallableManager` service. + + +Installation Process +-------------------- + +If installation of an installable unit is requested by the application or management agent, then the list of external bundles shall be installed into the OSGi Framework. Reference counting is used for all bundles installed via this path: if a second BAR requires a bundle that was installed from a previous BAR then we do not attempt to install a second copy, but only increment the reference count for that bundle. When a BAR is uninstalled, the reference count for all bundles it requires is decremented, and if that reference count reaches zero then the bundle is actually uninstalled from the OSGi Framework. In this way we allow multiple BARs to share depedencies, but uninstallation of one BAR does not affect other BARs that depend on the same bundles. + + +Resolve Invalidation +-------------------- + +A resolved BAR may remain in the `RESOLVED` state for some time, depending on the behaviour of the application. If the state of the OSGi Framework later changes, then the resolution result may no longer be valid. Similarly a BAR that is in `ERROR` state because it failed to resolve (e.g. because a dependency was not available) may later succeed if the context changes. + +Therefore the plugin invalidates all BARs in the `RESOLVED` or `ERROR` state in the event of any OSGi bundle being installed, resolved, unresolved, updated or uninstalled. This would include e.g. a refresh operation initiated from the shell. Invalidated BARs are placed back into a queue to be re-resolved in due course. + +Note that the plugin does not attempt to ensure that an installed BAR remains in a consistent state. If the OSGi Framework context is changed outside of our control (e.g. the user uninstalls a bundle manually) then the bundles forming the BAR may be in an invalid state. \ No newline at end of file diff --git a/fileinstall-plugins/installer/bnd.bnd b/fileinstall-plugins/installer/bnd.bnd new file mode 100644 index 00000000000..45d2560a7d1 --- /dev/null +++ b/fileinstall-plugins/installer/bnd.bnd @@ -0,0 +1,6 @@ +-exportcontents: org.apache.felix.fileinstall.plugins.installer +-conditionalpackage:\ + org.apache.felix.utils.manifest,\ + org.jboss.osgi.repository.*,\ + aQute.lib.*,\ + aQute.libg.* diff --git a/fileinstall-plugins/installer/pom.xml b/fileinstall-plugins/installer/pom.xml new file mode 100644 index 00000000000..88ebe53c82e --- /dev/null +++ b/fileinstall-plugins/installer/pom.xml @@ -0,0 +1,137 @@ + + + 4.0.0 + + + org.apache.felix + felix-parent + 3 + ../../pom/pom.xml + + + org.apache.felix.fileinstall.plugins.installer + Apache Felix File Install Installer Plugin + 1.0.0-SNAPSHOT + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/fileinstall-plugins/installer + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/fileinstall-plugins/installer + http://svn.apache.org/repos/asf/felix/fileinstall + + + + 8 + java18 + + + + + ${project.groupId} + org.apache.felix.fileinstall.plugins.resolver + ${project.version} + + + org.osgi + osgi.core + 6.0.0 + provided + + + org.osgi + osgi.cmpn + 6.0.0 + provided + + + org.osgi + osgi.annotation + 6.0.1 + + + org.osgi + org.osgi.service.repository + 1.1.0 + + + org.apache.felix + org.apache.felix.fileinstall + 3.5.0 + compile + + + org.apache.felix + org.apache.felix.utils + 1.8.4 + provided + + + junit + junit + + + + + + + biz.aQute.bnd + bnd-maven-plugin + 3.4.0 + + + default-bnd-process + + bnd-process + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + [1.8,) + + + + run + + + + + + + + + + + + + + + + + diff --git a/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/Artifact.java b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/Artifact.java new file mode 100644 index 00000000000..8b2c1683e7e --- /dev/null +++ b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/Artifact.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer; + +import org.osgi.annotation.versioning.ProviderType; + +@ProviderType +public interface Artifact { + + String getName(); + + String getVersion(); + + /** + * Returns the hash for the artifact content if available, otherwise + * {@code null}. + */ + Hash getHash(); + + /** + * Get the location for the artifact, which may be the location of an + * existing installed bundle or a physical URI from which the artifact may + * be fetched. + */ + String getLocation(); + +} diff --git a/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/FrameworkInstaller.java b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/FrameworkInstaller.java new file mode 100644 index 00000000000..f665e381747 --- /dev/null +++ b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/FrameworkInstaller.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer; + +import java.io.IOException; +import java.util.List; + +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; + +/** + * A management agent responsible for installing and uninstalling bundles into + * the OSGi Framework. It keeps a reference count for bundles that it has + * installed, and is responsible for uninstalling those bundles when the + * reference count drops to zero. + * + * Bundles that were not installed by the provider (e.g. pre-existing bundles) + * should never be uninstalled this provider. + * + * If another management agent modifies the installed bundles then the behaviour + * is undefined. + */ +@ProviderType +public interface FrameworkInstaller { + + /** + * Ensure that bundles exist in the OSGi Framework with the specified + * locations; they will be installed from those locations if they do not + * already exist. Any bundles installed as a result of this method will be + * associated with the given sponsor, and if all sponsors for a bundle are + * later removed then the bundle shall be uninstalled. + * + * NB sponsors should be compared with value equality, i.e. + * {@link Object#equals(Object)}. + * + * @return The list of bundles actually installed by this operation, i.e. + * not including those that were already present. + * + * @throws BundleException + * @throws IOException + */ + List addLocations(Object sponsor, List bundleLocations) throws BundleException, IOException; + + /** + * Remove bundles associated with the specified sponsor object. + * + * @return The list of bundles actually uninstalled by this operation. + */ + List removeSponsor(Object sponsor); + +} diff --git a/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/Hash.java b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/Hash.java new file mode 100644 index 00000000000..6d684c249d1 --- /dev/null +++ b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/Hash.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer; + +import java.util.Arrays; + +public final class Hash { + + private final static char[] hexArray = "0123456789abcdef".toCharArray(); + + private final String algo; + private final byte[] bytes; + + public Hash(String algo, byte[] bytes) { + this.algo = algo; + this.bytes = bytes; + } + + public String getAlgo() { + return this.algo; + } + + public byte[] getBytes() { + return this.bytes; + } + + @Override + public String toString() { + return new StringBuilder() + .append(this.algo) + .append(':') + .append(bytesToHex(this.bytes)) + .toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this.algo == null) ? 0 : this.algo.hashCode()); + result = prime * result + Arrays.hashCode(this.bytes); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Hash other = (Hash) obj; + if (this.algo == null) { + if (other.algo != null) { + return false; + } + } else if (!this.algo.equals(other.algo)) { + return false; + } + if (!Arrays.equals(this.bytes, other.bytes)) { + return false; + } + return true; + } + + private static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } +} diff --git a/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/InstallableListener.java b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/InstallableListener.java new file mode 100644 index 00000000000..8365e719787 --- /dev/null +++ b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/InstallableListener.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer; + +import java.util.Collection; + +import org.osgi.annotation.versioning.ConsumerType; + +/** + * A whiteboard listener for installation units that can be installed into the + * present OSGi Framework. + */ +@ConsumerType +public interface InstallableListener { + + /** + * Notifies that one or more installable units have changed their states. + */ + void installableUnitsChanged(Collection events); + +} diff --git a/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/InstallableManager.java b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/InstallableManager.java new file mode 100644 index 00000000000..ecd71914941 --- /dev/null +++ b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/InstallableManager.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer; + +import java.util.Collection; + +import org.osgi.annotation.versioning.ProviderType; + +@ProviderType +public interface InstallableManager { + + Collection getInstallableUnits(); + +} diff --git a/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/InstallableUnit.java b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/InstallableUnit.java new file mode 100644 index 00000000000..0c77213bcf9 --- /dev/null +++ b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/InstallableUnit.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer; + +import java.io.File; +import java.util.Collection; +import java.util.List; + +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.Bundle; +import org.osgi.util.promise.Promise; + +@ProviderType +public interface InstallableUnit { + + /** + * Get the path to the original archive file from which this installable unit arises. + */ + File getOrigin(); + + String getName(); + + String getSymbolicName(); + + String getVersion(); + + /** + * Get the physical artifacts that must be installed into the OSGi Framework + * in order to fully install this unit. May return {@code null} or empty if + * the state is {@link State#ERROR}. + */ + Collection getArtifacts(); + + /** + * Get the current state of this installable unit. + */ + State getState(); + + /** + * Get any error associated with this installable unit. Likely to be + * {@code null} unless {@link #getState()} returns {@link State#ERROR}. + */ + String getErrorMessage(); + + /** + * Install the unit. + * + * @return A promise of the list of bundles that were actually installed + * into the OSGi Framework as a result of this operation. + */ + Promise> install(); + + /** + * Uninstall the unit. + * + * @return A promise of the list of bundles that were actually uninstalled + * from the OSGi Framework as a result of this operation. + */ + Promise> uninstall(); + +} diff --git a/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/InstallableUnitEvent.java b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/InstallableUnitEvent.java new file mode 100644 index 00000000000..e5984cf74d5 --- /dev/null +++ b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/InstallableUnitEvent.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer; + +public final class InstallableUnitEvent { + + private final State newState; + private final InstallableUnit unit; + private final State oldState; + + public InstallableUnitEvent(State oldState, State newState, InstallableUnit unit) { + this.oldState = oldState; + this.newState = newState; + this.unit = unit; + } + + /** + * Get the state of the unit before this event, which may be null if the unit did not previously exist. + */ + public State getOldState() { + return oldState; + } + + /** + * Get the new state of the unit. + */ + public State getNewState() { + return newState; + } + + public InstallableUnit getUnit() { + return unit; + } + +} diff --git a/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/State.java b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/State.java new file mode 100644 index 00000000000..fc0f4f94e6c --- /dev/null +++ b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/State.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer; + +public enum State { + ERROR, RESOLVED, INSTALLED, INSTALLING, REMOVED +} diff --git a/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/ArtifactImpl.java b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/ArtifactImpl.java new file mode 100644 index 00000000000..2137ed5ff33 --- /dev/null +++ b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/ArtifactImpl.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer.impl; + +import org.apache.felix.fileinstall.plugins.installer.Artifact; +import org.apache.felix.fileinstall.plugins.installer.Hash; + +class ArtifactImpl implements Artifact { + + private final String name; + private final String version; + private final String location; + private final Hash hash; + + ArtifactImpl(String name, String version, String location, Hash hash) { + this.version = version; + if (location == null) { + throw new IllegalArgumentException("Artifact location may not be null"); + } + this.name = name; + this.location = location; + this.hash = hash; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public String getVersion() { + return version; + } + + @Override + public Hash getHash() { + return this.hash; + } + + @Override + public String getLocation() { + return this.location; + } + + @Override + public String toString() { + return new StringBuilder() + .append(this.name) + .append('/').append(this.version) + .append(":").append(this.hash != null ? this.hash : "") + .append("@").append(this.location) + .toString(); + } + +} diff --git a/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/Constants.java b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/Constants.java new file mode 100644 index 00000000000..4c675db00d4 --- /dev/null +++ b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/Constants.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer.impl; + +interface Constants { + + String DEPLOYMENT_NAME = "Deployment-Name"; + + String DEPLOYMENT_SYMBOLIC_NAME = "Deployment-SymbolicName"; + + String DEPLOYMENT_VERSION = "Deployment-Version"; + + String INDEX_FILE = "Index-Path"; + + String DEFAULT_INDEX_FILE = "index.xml"; + + String ARTIFACT_EXTENSION = ".bar"; +} diff --git a/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/DeploymentInstaller.java b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/DeploymentInstaller.java new file mode 100644 index 00000000000..03910f916fe --- /dev/null +++ b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/DeploymentInstaller.java @@ -0,0 +1,715 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer.impl; + +import static org.apache.felix.fileinstall.plugins.installer.impl.Locking.withLock; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Queue; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Supplier; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.stream.Collectors; + +import org.apache.felix.fileinstall.ArtifactInstaller; +import org.apache.felix.fileinstall.plugins.installer.Artifact; +import org.apache.felix.fileinstall.plugins.installer.FrameworkInstaller; +import org.apache.felix.fileinstall.plugins.installer.Hash; +import org.apache.felix.fileinstall.plugins.installer.InstallableListener; +import org.apache.felix.fileinstall.plugins.installer.InstallableManager; +import org.apache.felix.fileinstall.plugins.installer.InstallableUnit; +import org.apache.felix.fileinstall.plugins.installer.InstallableUnitEvent; +import org.apache.felix.fileinstall.plugins.installer.State; +import org.apache.felix.fileinstall.plugins.resolver.PluginResolver; +import org.apache.felix.fileinstall.plugins.resolver.ResolveRequest; +import org.apache.felix.fileinstall.plugins.resolver.ResolveResult; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.BundleException; +import org.osgi.framework.BundleListener; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.Version; +import org.osgi.framework.namespace.IdentityNamespace; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.osgi.service.component.annotations.ReferencePolicyOption; +import org.osgi.service.log.LogService; +import org.osgi.service.repository.ContentNamespace; +import org.osgi.util.promise.Promise; +import org.osgi.util.promise.Promises; + +import aQute.lib.hex.Hex; + +@Component(immediate = true) +public class DeploymentInstaller implements ArtifactInstaller, InstallableManager { + + /** + * Set environment variable "org.apache.felix.fileinstall.plugins.installer.debug" to enable develeroper-level debug output to the console. + */ + public static final boolean DEBUG = Boolean.getBoolean("org.apache.felix.fileinstall.plugins.installer.debug"); + + // If Deployment version is not specified, the version to return + private static final String UNKNOWN_DEPLOYMENT_VERSION = "0.0.0"; + + /** + * OSGi Repository indexes are documented to use SHA-256 for their content hashes. + * @see ContentNamespace#CONTENT_NAMESPACE + */ + private static final String SHA256 = "SHA-256"; + + // Magic number (not officially but good enough for our purposes) of a ZIP file is PK\0x03\0x04 + private static final byte[] ZIP_FILE_MAGIC = new byte[] { 0x50, 0x4b, 0x03, 0x04 }; + + // Queues for pending resolve and install operations. + private final Queue>> pendingInstalls = new LinkedList<>(); + private final Map> pendingResolves = new LinkedHashMap<>(); + + // Currently available Installable Units, and a lock for modifying that map safely. + private final Map units = new HashMap<>(); + private final ReadWriteLock unitsLock = new ReentrantReadWriteLock(); + + // Listeners for install/uninstall events + private final List installListeners = new CopyOnWriteArrayList<>(); + + // The framework listener invalidates any RESOLVED install units whenever the OSGi Framework packages are refreshed. + private final FrameworkListener frameworkListener = new FrameworkListener() { + @Override + public void frameworkEvent(FrameworkEvent event) { + if (event.getType() == FrameworkEvent.PACKAGES_REFRESHED) { + invalidateAllUnits("Packaged Refreshed"); + } + } + }; + // The bundle listener invalidated any RESOLVED install units whenever OSGi Bundles are installed, resolved, unresolved, updated or uninstalled. + private final BundleListener bundleListener = new BundleListener() { + @Override + public void bundleChanged(BundleEvent event) { + int mask = BundleEvent.INSTALLED | BundleEvent.RESOLVED | BundleEvent.UNRESOLVED | BundleEvent.UPDATED | BundleEvent.UNINSTALLED; + if ((mask & event.getType()) > 0) { + invalidateAllUnits(String.format("Bundle %s entered state %s", event.getBundle().getSymbolicName(), bundleEventToString(event.getType()))); + } + } + }; + + // Component instance state. + private BundleContext context; + private Thread processorThread; + + // The PluginResolver service that calculates resolution results for us, but doesn't produce any effects. + @Reference(target = "(!(" + org.osgi.framework.Constants.SERVICE_BUNDLEID + "=0))") + PluginResolver resolver; + + // The FrameworkInstaller service is responsible for ensuring required bundles are present in the OSGi Framework -- installing them when necessary. + @Reference + FrameworkInstaller frameworkInstaller; + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) + void addInstallListener(InstallableListener listener) { + this.installListeners.add(listener); + } + void removeInstallListener(InstallableListener listener) { + this.installListeners.remove(listener); + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policyOption = ReferencePolicyOption.GREEDY) + LogService log; + + @Activate + void activate(BundleContext context) { + this.context = context; + context.addFrameworkListener(this.frameworkListener); + context.addBundleListener(this.bundleListener); + + this.processorThread = new Thread(() -> { + this.log.log(LogService.LOG_INFO, "Deployment Installer thread starting"); + debug("Deployment Install thread starting"); + while (!Thread.interrupted()) { + try { + Runnable job = waitForJob(); + job.run(); + } catch (InterruptedException e) { + debug("Deployment Install thread interrupted"); + this.log.log(LogService.LOG_INFO, "Deployment Installer thread interrupted"); + break; + } catch (Exception e) { + this.log.log(LogService.LOG_ERROR, "Error processing job on Deployment Installer thread", e); + } + } + debug("Deployment Install thread terminated."); + this.log.log(LogService.LOG_INFO, "Deployment Installer thread terminated"); + }); + this.processorThread.start(); + } + + @Deactivate + void deactivate() { + this.processorThread.interrupt(); + this.context.removeBundleListener(this.bundleListener); + this.context.removeFrameworkListener(this.frameworkListener); + } + + /** + * Wait for the next job to do from the two pending queues. Note that we + * always favour the install queue, because performing an install/uninstall + * will invalidate any completed resolves. Therefore the resolves are only + * processed when the install queue is empty. + */ + private Job waitForJob() throws InterruptedException { + synchronized (this.processorThread) { + while (true) { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + + // Try to get an InstallableUnit to install + Job> installJob = this.pendingInstalls.poll(); + if (installJob != null) { + return installJob; + } + + // Try to get a File to resolve. An iterator is needed to remove the first entry from the LinkedHashMap. + Iterator> resolvesIter = this.pendingResolves.values().iterator(); + if (resolvesIter.hasNext()) { + Job resolveJob = resolvesIter.next(); + resolvesIter.remove(); + return resolveJob; + } + + // Wait for a notification + this.processorThread.wait(); + } + } + } + + /** + * Put an install job onto the queue + */ + Promise> putInstallJob(InstallableUnitImpl unit) { + Job> job = new Job<>(() -> installArtifacts(unit)); + synchronized (this.processorThread) { + this.pendingInstalls.add(job); + this.processorThread.notifyAll(); + } + return job.getPromise(); + } + + /** + * Put an uninstall job onto the queue + */ + Promise> putUninstallJob(InstallableUnitImpl unit) { + Job> job = new Job<>(() -> uninstallArtifacts(unit)); + synchronized (this.processorThread) { + this.pendingInstalls.add(job); + this.processorThread.notifyAll(); + } + return job.getPromise(); + } + + private List installArtifacts(InstallableUnitImpl unit) { + Supplier> func = () -> { + State oldState = unit.getState(); + unit.setState(State.INSTALLING); + notifyListeners(Collections.singleton(new InstallableUnitEvent(oldState, State.INSTALLING, unit))); + + try { + // Install Bundles + Collection artifacts = unit.getArtifacts(); + List locations = artifacts.stream().map(Artifact::getLocation).collect(Collectors.toList()); + List installed = this.frameworkInstaller.addLocations(unit, locations); + + // Start bundles + for (Bundle bundle : installed) { + if (!isFragment(bundle)) { + bundle.start(); + } + } + + // Mark unit installed + oldState = unit.getState(); + unit.setState(State.INSTALLED); + notifyListeners(Collections.singleton(new InstallableUnitEvent(oldState, State.INSTALLED, unit))); + + return installed; + } catch (BundleException | IOException e) { + oldState = unit.getState(); + unit.setState(State.ERROR); + unit.setErrorMessage(e.getMessage()); + if (log != null) { + log.log(LogService.LOG_ERROR, "Error installing artifact(s)", e); + } + notifyListeners(Collections.singleton(new InstallableUnitEvent(oldState, State.ERROR, unit))); + return Collections.emptyList(); + } + }; + return withLock(this.unitsLock.writeLock(), func); + } + + private List uninstallArtifacts(InstallableUnitImpl unit) { + List bundles = this.frameworkInstaller.removeSponsor(unit); + + // Mark unit uninstalled + withLock(this.unitsLock.writeLock(), () -> { + State oldState = unit.getState(); + if (unit.setState(State.REMOVED)) { + notifyListeners(Collections.singleton(new InstallableUnitEvent(oldState, State.REMOVED, unit))); + } + }); + + return bundles; + } + + /** + * Put a resolve job onto the queue. The resolve queue is a little + * different: because there is no need to resolve the same file twice, we + * check whether there's already a pending resolve job for the given file. + * If so, just return the same promise. + */ + private Promise putResolveJob(File file) { + Promise promise; + + Job newJob = new Job<>(() -> { + InstallableUnitImpl unit = performResolve(file); + replaceUnit(file, null, unit); + return unit; + }); + synchronized (this.processorThread) { + Job existing = this.pendingResolves.putIfAbsent(file, newJob); + if (existing != null) { + // no need to unblock the processor thread because we haven't added anything + debug("Not adding resolve job for %s as it is already on the queue", file); + promise = existing.getPromise(); + } else { + debug("Added resolve job for %s", file); + promise = newJob.getPromise(); + this.processorThread.notifyAll(); + } + } + return promise; + } + + private InstallableUnitImpl performResolve(File file) { + debug("Starting resolve for file: %s", file); + ResolveRequest request; + try { + request = analyseFile(file); + } catch (Exception e) { + debug("Failed to analyse file %s: %s", file, e.getMessage()); + return newFailedUnit(file, file.getName(), file.getName(), "UNKNOWN_VERSION", String.format("Error reading archive file %s: %s", file.getAbsolutePath(), e.getMessage())); + } + + ResolveResult result; + try { + result = this.resolver.resolve(request); + } catch (Exception e) { + debug("Failed to resolve file %s: %s", file, e.getMessage()); + return newFailedUnit(file, request.getName(), request.getSymbolicName(), request.getVersion(), "Resolution failed: " + e.getMessage()); + + } + + List artifacts = new ArrayList<>(result.getResources().size()); + for (Entry resourceEntry : result.getResources().entrySet()) { + Capability idCap = getIdentityCapability(resourceEntry.getKey()); + ArtifactImpl artifact = new ArtifactImpl(getIdentity(idCap), getVersion(idCap), resourceEntry.getValue(), getContentHash(resourceEntry.getKey())); + artifacts.add(artifact); + } + debug("Sucessful resolve for file %s: Deployment-Name=%s, Deployment-SymbolicName=%s, Deployment-Version= %s", file, request.getName(), request.getSymbolicName(), request.getVersion()); + return newResolvedUnit(file, request.getName(), request.getSymbolicName(), request.getVersion(), artifacts); + + } + + private InstallableUnitImpl newResolvedUnit(File file, String name, String symbolicName, String version, List artifacts) { + InstallableUnitImpl newUnit = new InstallableUnitImpl(this, file, name, symbolicName, version, artifacts); + newUnit.setState(State.RESOLVED); + return newUnit; + } + + private InstallableUnitImpl newFailedUnit(File file, String name, String symbolicName, String version, String message) { + InstallableUnitImpl newUnit = new InstallableUnitImpl(this, file, name, symbolicName, version, Collections.emptyList()); + newUnit.setState(State.ERROR); + newUnit.setErrorMessage(message); + return newUnit; + } + + @Override + public boolean canHandle(File file) { + String fileName = file.getName(); + if (!fileName.toLowerCase().endsWith(Constants.ARTIFACT_EXTENSION.toLowerCase())) { + log(LogService.LOG_DEBUG, null, "Ignoring %s, does not end with '%s' extension", file.getAbsolutePath(), Constants.ARTIFACT_EXTENSION); + return false; + } + + // Check if it's a valid ZIP + try { + if (!isZipFile(file)) { + log(LogService.LOG_WARNING, null, "Not a valid ZIP file, ignoring file: %s", file.getAbsolutePath()); + return false; + } + } catch (IOException e) { + log(LogService.LOG_ERROR, e, "Failed to check ZIP header on %s", file.getAbsolutePath()); + return false; + } + + // Read the requires header and index + try (JarFile jar = new JarFile(file, true)) { + Manifest manifest = jar.getManifest(); + if (manifest == null) { + log(LogService.LOG_WARNING, null, "Not a valid bundle archive: no META-INF/MANIFEST.MF in %s", file.getAbsolutePath()); + return false; + } + String requireBundleStr = manifest.getMainAttributes().getValue(org.osgi.framework.Constants.REQUIRE_BUNDLE); + String requireCapsStr = manifest.getMainAttributes().getValue(org.osgi.framework.Constants.REQUIRE_CAPABILITY); + if (requireBundleStr == null && requireCapsStr == null) { + log(LogService.LOG_WARNING, null, "Not a valid bundle archive: missing %s or %s header in manifest in %s", org.osgi.framework.Constants.REQUIRE_BUNDLE, org.osgi.framework.Constants.REQUIRE_CAPABILITY, file.getAbsolutePath()); + return false; + } + + JarEntry indexEntry = findEntry(jar, Constants.INDEX_FILE, Constants.DEFAULT_INDEX_FILE); + if (indexEntry == null) { + log(LogService.LOG_WARNING, null, "Not a valid bundle archive: no index entry found in %s", file.getAbsolutePath()); + return false; + } + + // Everything seems present and correct + log(LogService.LOG_INFO, null, "Detected valid bundle archive in file %s", file.getAbsolutePath()); + return true; + } catch (IOException e) { + log(LogService.LOG_ERROR, e, "Failed to check PKZIP header for candidate archive file %s", file.getAbsolutePath()); + return false; + } + } + + private ResolveRequest analyseFile(File file) throws IOException { + log(LogService.LOG_INFO, null, "Resolving bundle archive: %s", file.getAbsolutePath()); + + String fileUriStr = file.toURI().toString(); + String indexUriStr; + String name; + String symbolicName; + String version = ""; + + List requirements = new LinkedList<>(); + try (JarFile jar = new JarFile(file)) { + Attributes manifestAttribs = jar.getManifest().getMainAttributes(); + + symbolicName = manifestAttribs.getValue(Constants.DEPLOYMENT_SYMBOLIC_NAME); + if (symbolicName == null) { + symbolicName = file.getName(); + } + + name = manifestAttribs.getValue(Constants.DEPLOYMENT_NAME); + if (name == null) { + name = symbolicName; + } + + version = manifestAttribs.getValue(Constants.DEPLOYMENT_VERSION); + if (version == null) { + version = UNKNOWN_DEPLOYMENT_VERSION; + } + requirements.addAll(RequirementParser.parseRequireBundle(manifestAttribs.getValue(org.osgi.framework.Constants.REQUIRE_BUNDLE))); + requirements.addAll(RequirementParser.parseRequireCapability(manifestAttribs.getValue(org.osgi.framework.Constants.REQUIRE_CAPABILITY))); + if (requirements.isEmpty()) { + throw new IllegalArgumentException(String.format("Missing %s or %s header in manifest in %s", org.osgi.framework.Constants.REQUIRE_BUNDLE, org.osgi.framework.Constants.REQUIRE_CAPABILITY, file.getAbsolutePath())); + } + + JarEntry indexEntry = findEntry(jar, Constants.INDEX_FILE, Constants.DEFAULT_INDEX_FILE); + if (indexEntry == null) { + throw new IllegalArgumentException("Missing index entry in " + file.getAbsolutePath()); + } + indexUriStr = "jar:" + fileUriStr + "!/" + indexEntry.getName(); + } + + try { + ResolveRequest request = new ResolveRequest(name, symbolicName, version, + Collections.singletonList(new URI(indexUriStr)), requirements); + + return request; + } catch (URISyntaxException e) { + throw new IOException("Unable to convert index URI " + indexUriStr, e); + } + } + + @Override + public void install(File file) { + log(LogService.LOG_INFO, null, "Installing bundle archive: %s", file.getAbsolutePath()); + putResolveJob(file); + } + + private static String getIdentity(Capability identityCap) { + Object idObj = identityCap.getAttributes().get(IdentityNamespace.IDENTITY_NAMESPACE); + if (!(idObj instanceof String)) { + throw new IllegalArgumentException("Missing identity capability on resource, or incorrect type"); + } + + return (String) idObj; + } + + private static String getVersion(Capability identityCap) { + Object versionObj = identityCap.getAttributes().get(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE); + if (versionObj == null) { + return Version.emptyVersion.toString(); + } + if (versionObj instanceof Version) { + return ((Version) versionObj).toString(); + } + if (versionObj instanceof String) { + return Version.parseVersion((String) versionObj).toString(); + } + throw new IllegalArgumentException("Incorrect type on identity version"); + } + + private static Capability getIdentityCapability(Resource resource) { + List caps = resource.getCapabilities(IdentityNamespace.IDENTITY_NAMESPACE); + if (caps == null || caps.isEmpty()) { + throw new IllegalArgumentException("Missing identity capability on resource"); + } + return caps.get(0); + } + + @Override + public void uninstall(File file) throws Exception { + log(LogService.LOG_INFO, null, "Uninstalling bundle archive: %s", file.getAbsolutePath()); + unsponsorUnit(file); + } + + @Override + public void update(File file) throws Exception { + log(LogService.LOG_INFO, null, "Uninstalling bundle archive: %s", file.getAbsolutePath()); + unsponsorUnit(file).flatMap(lb -> { + return putResolveJob(file); + }); + } + + @Override + public Collection getInstallableUnits() { + return withLock(this.unitsLock.readLock(), ArrayList::new, this.units.values()); + } + + /** + * Unsponsor the installable unit. Returns a promise which is resolved when the uninstall is completed. + */ + private Promise> unsponsorUnit(File file) { + List events = new LinkedList<>(); + + Promise> bundles = withLock(this.unitsLock.writeLock(), () -> { + Promise> promise = Promises.resolved(Collections.emptyList()); + InstallableUnitImpl existing = this.units.remove(file); + if (existing != null) { + State origState = existing.getState(); + if (origState.equals(State.INSTALLED)) { + promise = putUninstallJob(existing); + } + + if (existing.setState(State.REMOVED)) { + events.add(new InstallableUnitEvent(origState, State.REMOVED, existing)); + } + } + return promise; + }); + + notifyListeners(events); + return bundles; + } + + private void invalidateAllUnits(String reason) { + List affectedFiles = new LinkedList<>(); + List events = new LinkedList<>(); + withLock(this.unitsLock.writeLock(), () -> { + for (Entry entry : this.units.entrySet()) { + InstallableUnitImpl unit = entry.getValue(); + State currentState = unit.getState(); + if (EnumSet.of(State.RESOLVED, State.ERROR).contains(currentState)) { + unit.setState(State.REMOVED); + affectedFiles.add(entry.getKey()); + events.add(new InstallableUnitEvent(currentState, State.REMOVED, unit)); + debug("invalidated %s because: %s%n", entry.getKey(), reason); + } + } + }); + notifyListeners(events); + + // Schedule re-resolution of the affected files. + for (File file : affectedFiles) { + putResolveJob(file); + } + } + + private void replaceUnit(File file, State origState, InstallableUnitImpl newUnit) { + Collection events = new ArrayList<>(2); + events.add(new InstallableUnitEvent(origState, newUnit.getState(), newUnit)); + events.addAll(withLock(this.unitsLock.writeLock(), () -> { + InstallableUnitImpl existing = this.units.put(file, newUnit); + if (existing != null) { + State oldState = existing.getState(); + if (existing.setState(State.REMOVED)) { + return Collections.singleton(new InstallableUnitEvent(oldState, State.REMOVED, existing)); + } + } + return Collections.emptyList(); + })); + notifyListeners(events); + } + + private void notifyListeners(Collection events) { + if (events == null || events.isEmpty()) { + return; + } + for (InstallableListener listener : this.installListeners) { + try { + listener.installableUnitsChanged(events); + } catch (Exception e) { + log(LogService.LOG_ERROR, e, "Error dispatching installable unit event to registered listener"); + } + } + } + + private void log(int level, Throwable t, String format, Object... args) { + if (this.log != null) { + this.log.log(level, String.format(format, args), t); + } + } + + private static boolean isZipFile(File file) throws IOException { + byte[] tmp = new byte[4]; + + try (InputStream in = new FileInputStream(file)) { + int bytesRead = in.read(tmp, 0, 4); + if (bytesRead < 4) { + throw new IOException("Insufficient bytes read for ZIP header test: possibly truncated file?"); + } + } + + return Arrays.equals(tmp, ZIP_FILE_MAGIC); + } + + private static JarEntry findEntry(JarFile jar, String headerName, String defaultPath) throws IOException { + String path = defaultPath; + String headerPath = jar.getManifest().getMainAttributes().getValue(headerName); + if (headerPath != null) { + path = headerPath.trim(); + } + + if (path == null) { + // Could happen if no default and no header + return null; + } + + return jar.getJarEntry(path); + } + + private static boolean isFragment(Bundle bundle) { + return (bundle.adapt(BundleRevision.class).getTypes() & BundleRevision.TYPE_FRAGMENT) > 0; + } + + private static String bundleEventToString(int type) { + String s; + switch (type) { + case BundleEvent.INSTALLED: + s = "INSTALLED"; + break; + case BundleEvent.RESOLVED: + s = "RESOLVED"; + break; + case BundleEvent.STARTING: + s = "STARTING"; + break; + case BundleEvent.STARTED: + s = "STARTED"; + break; + case BundleEvent.STOPPING: + s = "STOPPING"; + break; + case BundleEvent.STOPPED: + s = "STOPPED"; + break; + case BundleEvent.UNRESOLVED: + s = "UNRESOLVED"; + break; + case BundleEvent.UPDATED: + s = "UPDATED"; + break; + case BundleEvent.UNINSTALLED: + s = "UNINSTALLED"; + break; + case BundleEvent.LAZY_ACTIVATION: + s = "LAZY_ACTIVATION"; + break; + default: + s = "UNKNOWN"; + } + return s; + } + + private static Hash getContentHash(Resource resource) throws IllegalArgumentException { + Hash result = null; + + List contentCaps = resource.getCapabilities(ContentNamespace.CONTENT_NAMESPACE); + if (contentCaps != null) { + for (Capability contentCap : contentCaps) { + String hashHexStr = (String) contentCap.getAttributes().get(ContentNamespace.CONTENT_NAMESPACE); + byte[] hashHex = Hex.toByteArray(hashHexStr); + Hash hash = new Hash(SHA256, hashHex); + + if (result != null && !hash.equals(result)) { + throw new IllegalArgumentException("Resource '" + resource + "' has multiple inconsistent content hashes."); + } + result = hash; + } + } + + return result; + } + + private void debug(String format, Object... args) { + if (DEBUG) { + System.out.printf("DEBUG: " + format + "%n", args); + } + } + + +} diff --git a/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/FrameworkInstallerComponent.java b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/FrameworkInstallerComponent.java new file mode 100644 index 00000000000..9a691fc85fd --- /dev/null +++ b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/FrameworkInstallerComponent.java @@ -0,0 +1,144 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.felix.fileinstall.plugins.installer.FrameworkInstaller; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.log.LogService; + +@Component +public class FrameworkInstallerComponent implements FrameworkInstaller { + + private final Map> bundleSponsors = new HashMap<>(); + + @Reference(cardinality = ReferenceCardinality.OPTIONAL) + private LogService log; + + private BundleContext context; + + @Activate + void activate(BundleContext context) { + this.context = context; + } + + @Override + public synchronized List addLocations(Object sponsor, List bundleLocations) throws BundleException, IOException { + List installed = new ArrayList<>(bundleLocations.size()); + + for (String location : bundleLocations) { + // Find an existing bundle with that location. + Bundle existing = null; + for (Bundle bundle : this.context.getBundles()) { + if (bundle.getLocation().equals(location)) { + existing = bundle; + + // If the existing bundle was previously installed by us then add to the sponsors. + Set sponsors = this.bundleSponsors.get(bundle.getBundleId()); + if (sponsors != null) { + sponsors.add(sponsor); + } + break; + } + } + + if (existing == null) { + // No existing bundle with that location. Install it and add the sponsor. + try { + // Ensure that the URLConnection doesn't cache. Mostly useful for JarURLConnection, + // which leaks file handles if this is not done. + URI locationUri = new URI(location); + URLConnection connection = locationUri.toURL().openConnection(); + connection.setUseCaches(false); + try (InputStream stream = connection.getInputStream()) { + Bundle bundle = this.context.installBundle(location, stream); + installed.add(bundle); + + Set sponsors = new HashSet<>(); + sponsors.add(sponsor); + this.bundleSponsors.put(bundle.getBundleId(), sponsors); + } catch (BundleException e) { + if (e.getType() == BundleException.DUPLICATE_BUNDLE_ERROR) { + log.log(LogService.LOG_WARNING, "Duplicate bundle symbolic-name/version in install for location " + location, e); + } else { + throw e; + } + } + } catch (URISyntaxException e) { + throw new IOException("Invalid bundle location URI: " + location, e); + } + } + } + + return installed; + } + + @Override + public synchronized List removeSponsor(Object sponsor) { + List toRemove = new LinkedList<>(); + + for (Iterator>> iter = this.bundleSponsors.entrySet().iterator(); iter.hasNext(); ) { + Entry> entry = iter.next(); + long bundleId = entry.getKey(); + Set sponsors = entry.getValue(); + + if (sponsors.remove(sponsor) && sponsors.isEmpty()) { + // We removed our sponsor and the sponsor set is now empty => this bundle should be removed. + toRemove.add(bundleId); + // Also remove the entry from the sponsor map. + iter.remove(); + } + } + + List removed = new LinkedList<>(); + for (long bundleId : toRemove) { + Bundle bundle = this.context.getBundle(bundleId); + if (bundle != null) { // just in case the bundle was already removed by somebody else + try { + bundle.uninstall(); + removed.add(bundle); + } catch (BundleException e) { + if (this.log != null) { + this.log.log(LogService.LOG_ERROR, "Error uninstalling bundle: " + bundle.getSymbolicName(), e); + } + } + } + } + return removed; + } + +} diff --git a/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/InstallableUnitImpl.java b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/InstallableUnitImpl.java new file mode 100644 index 00000000000..7bca8fb6e3c --- /dev/null +++ b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/InstallableUnitImpl.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer.impl; + +import java.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.apache.felix.fileinstall.plugins.installer.Artifact; +import org.apache.felix.fileinstall.plugins.installer.InstallableUnit; +import org.apache.felix.fileinstall.plugins.installer.State; +import org.osgi.framework.Bundle; +import org.osgi.util.promise.Promise; + +class InstallableUnitImpl implements InstallableUnit { + + private final DeploymentInstaller installer; + private final File originFile; + private final String name; + private final String symbolicName; + private final String version; + private final Collection artifacts; + + private State state; + private String errorMessage = null; + + InstallableUnitImpl(DeploymentInstaller installer, File originFile, String name, String symbolicName, String version, Collection artifacts) { + this.installer = installer; + this.originFile = originFile; + this.name = name; + this.symbolicName = symbolicName; + this.artifacts = artifacts; + this.version = version; + } + + @Override + public File getOrigin() { + return this.originFile; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public String getSymbolicName() { + return this.symbolicName; + } + + @Override + public Collection getArtifacts() { + return Collections.unmodifiableCollection(this.artifacts); + } + + @Override + public State getState() { + return this.state; + } + + @Override + public Promise> install() { + if (!this.state.equals(State.RESOLVED)) { + throw new IllegalStateException(String.format("Cannot install unit %s: not in the %s state", this.symbolicName, State.RESOLVED)); + } + return this.installer.putInstallJob(this); + } + + @Override + public Promise> uninstall() { + return this.installer.putUninstallJob(this); + } + + /** + * Set the state, return true if the state changed. + */ + boolean setState(State state) { + if (state == null) { + throw new IllegalArgumentException("null state not permitted!"); + } + State oldState = this.state; + + this.state = state; + return !this.state.equals(oldState); + } + + void setErrorMessage(String message) { + this.errorMessage = message; + } + + @Override + public String getErrorMessage() { + return this.errorMessage; + } + + @Override + public String getVersion() { + return this.version; + } + +} diff --git a/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/Job.java b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/Job.java new file mode 100644 index 00000000000..b5982c2f28c --- /dev/null +++ b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/Job.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer.impl; + +import java.util.concurrent.Callable; + +import org.osgi.util.promise.Deferred; +import org.osgi.util.promise.Promise; + +class Job implements Runnable { + + private final Deferred deferred = new Deferred<>(); + private final Callable call; + + Job(Callable call) { + this.call = call; + } + + @Override + public void run() { + // Avoid repeating the work + if (this.deferred.getPromise().isDone()) { + return; + } + + try { + T result = this.call.call(); + this.deferred.resolve(result); + } catch (Exception e) { + this.deferred.fail(e); + } + } + + public Promise getPromise() { + return this.deferred.getPromise(); + } + + +} diff --git a/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/Locking.java b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/Locking.java new file mode 100644 index 00000000000..3ef8dad2ff3 --- /dev/null +++ b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/Locking.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer.impl; + +import java.util.concurrent.locks.Lock; +import java.util.function.Function; +import java.util.function.Supplier; + +final class Locking { + + public static void withLock(Lock lock, Runnable cmd) { + lock.lock(); + try { + cmd.run(); + } finally { + lock.unlock(); + } + } + + public static R withLock(Lock lock, Supplier fun) { + lock.lock(); + try { + return fun.get(); + } finally { + lock.unlock(); + } + } + + public static R withLock(Lock lock, Function fun, T in) { + lock.lock(); + try { + return fun.apply(in); + } finally { + lock.unlock(); + } + } + +} diff --git a/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/RequirementParser.java b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/RequirementParser.java new file mode 100644 index 00000000000..0dd6bd96759 --- /dev/null +++ b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/impl/RequirementParser.java @@ -0,0 +1,130 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.felix.utils.manifest.Attribute; +import org.apache.felix.utils.manifest.Clause; +import org.apache.felix.utils.manifest.Directive; +import org.apache.felix.utils.manifest.Parser; +import org.osgi.framework.VersionRange; +import org.osgi.framework.namespace.BundleNamespace; +import org.osgi.resource.Namespace; +import org.osgi.resource.Requirement; + +import aQute.bnd.osgi.resource.RequirementBuilder; + +class RequirementParser { + + static List parseRequireBundle(String header) throws IllegalArgumentException { + if (header == null) { + return Collections.emptyList(); + } + + Clause[] clauses = Parser.parseHeader(header); + List requirements = new ArrayList<>(clauses.length); + for (Clause requireClause : clauses) { + String bsn = requireClause.getName(); + String versionRangeStr = requireClause.getAttribute(org.osgi.framework.Constants.BUNDLE_VERSION_ATTRIBUTE); + + String filter = toBundleFilter(bsn, versionRangeStr); + Requirement requirement = new RequirementBuilder(BundleNamespace.BUNDLE_NAMESPACE) + .addDirective(Namespace.REQUIREMENT_FILTER_DIRECTIVE, filter).buildSyntheticRequirement(); + requirements.add(requirement); + } + return requirements; + } + + static List parseRequireCapability(String header) throws IllegalArgumentException { + if (header == null) { + return Collections.emptyList(); + } + + Clause[] clauses = Parser.parseHeader(header); + List reqs = new ArrayList<>(clauses.length); + for (Clause clause : clauses) { + String namespace = clause.getName(); + + RequirementBuilder reqBuilder = new RequirementBuilder(namespace); + for (Attribute attrib : clause.getAttributes()) { + try { + reqBuilder.addAttribute(attrib.getName(), attrib.getValue()); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid requirement attribute", e); + } + } + for (Directive directive : clause.getDirectives()) { + reqBuilder.addDirective(directive.getName(), directive.getValue()); + } + + reqs.add(reqBuilder.buildSyntheticRequirement()); + } + + return reqs; + } + + private static String toBundleFilter(String bsn, String versionRangeStr) { + final String filterStr; + + String bsnFilter = String.format("(%s=%s)", BundleNamespace.BUNDLE_NAMESPACE, bsn); + + if (versionRangeStr == null) { + filterStr = bsnFilter; + } else { + VersionRange versionRange = new VersionRange(versionRangeStr); + if (versionRange.isExact()) { + String exactVersionFilter = String.format("(%s=%s)", + BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE, versionRange.getLeft()); + filterStr = String.format("(&%s%s)", bsnFilter, exactVersionFilter); + } else if (versionRange.getRight() == null) { + filterStr = String.format("(&%s%s)", bsnFilter, lowerVersionFilter(versionRange)); + } else { + filterStr = String.format("(&%s%s%s)", bsnFilter, lowerVersionFilter(versionRange), + upperVersionFilter(versionRange)); + } + } + return filterStr; + } + + private static String upperVersionFilter(VersionRange versionRange) { + String upperVersionFilter; + if (versionRange.getRightType() == VersionRange.RIGHT_CLOSED) { + upperVersionFilter = String.format("(%s<=%s)", BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE, + versionRange.getRight()); + } else { + upperVersionFilter = String.format("(!(%s>=%s))", BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE, + versionRange.getRight()); + } + return upperVersionFilter; + } + + private static String lowerVersionFilter(VersionRange versionRange) { + String lowerVersionFilter; + if (versionRange.getLeftType() == VersionRange.LEFT_CLOSED) { + lowerVersionFilter = String.format("(%s>=%s)", BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE, + versionRange.getLeft()); + } else { + lowerVersionFilter = String.format("(!(%s<=%s))", BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE, + versionRange.getLeft()); + } + return lowerVersionFilter; + } + +} diff --git a/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/package-info.java b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/package-info.java new file mode 100644 index 00000000000..61960411a18 --- /dev/null +++ b/fileinstall-plugins/installer/src/main/java/org/apache/felix/fileinstall/plugins/installer/package-info.java @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +@org.osgi.annotation.versioning.Version("1.0.0") +package org.apache.felix.fileinstall.plugins.installer; diff --git a/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/CompositeBundleInstallerTest.java b/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/CompositeBundleInstallerTest.java new file mode 100644 index 00000000000..f42205b33c3 --- /dev/null +++ b/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/CompositeBundleInstallerTest.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; + +import org.apache.felix.fileinstall.plugins.installer.impl.DeploymentInstaller; +import org.junit.Before; +import org.junit.Test; + +public class CompositeBundleInstallerTest { + + private TestLogService log; + private DeploymentInstaller installer; + + @Before + public void setup() { + log = new TestLogService(); + installer = new DeploymentInstaller(); + installer.log = log; + } + + @Test + public void testValidArchiveCanBeHandled() { + File jar = new File("target/test-classes/valid1.bar"); + + assertTrue("test archive not found", jar.isFile()); + assertTrue("test archive not handled", installer.canHandle(jar)); + + assertEquals(1, log.logged.size()); + assertTrue("missing info message", log.logged.get(0).startsWith("INFO: Detected valid bundle archive")); + } + + @Test + public void testIndexPathInManifest() { + File jar = new File("target/test-classes/valid2.bar"); + + assertTrue("test archive not found", jar.isFile()); + assertTrue("test archive not handled", installer.canHandle(jar)); + + assertEquals(1, log.logged.size()); + assertTrue("missing info message", log.logged.get(0).startsWith("INFO: Detected valid bundle archive")); + } + + @Test + public void testInvalidExtension() { + File jar = new File("target/test-classes/invalid-wrong-extension.xxx"); + + assertTrue("test archive not found", jar.isFile()); + assertFalse("invalid test archive should not be handled", installer.canHandle(jar)); + + assertEquals(1, log.logged.size()); + assertTrue("missing debug message", log.logged.get(0).startsWith("DEBUG: Ignoring")); + } + + @Test + public void testMissingRequireHeader() { + File jar = new File("target/test-classes/invalid-missing-requires.bar"); + + assertTrue("test archive not found", jar.isFile()); + assertFalse("invalid test archive should not be handled", installer.canHandle(jar)); + + assertEquals(1, log.logged.size()); + assertTrue("missing warning message", log.logged.get(0).startsWith("WARNING: Not a valid bundle archive")); + } + + @Test + public void testNoIndex() { + File jar = new File("target/test-classes/invalid-no-index.bar"); + + assertTrue("test archive not found", jar.isFile()); + assertFalse("invalid test archive should not be handled", installer.canHandle(jar)); + + assertEquals(1, log.logged.size()); + assertTrue("missing warning message", log.logged.get(0).startsWith("WARNING: Not a valid bundle archive")); + } + + @Test + public void testMissingIndexPath() { + File jar = new File("target/test-classes/invalid-missing-index-path.bar"); + + assertTrue("test archive not found", jar.isFile()); + assertFalse("invalid test archive should not be handled", installer.canHandle(jar)); + + assertEquals(1, log.logged.size()); + assertTrue("missing warning message", log.logged.get(0).startsWith("WARNING: Not a valid bundle archive")); + } + +} diff --git a/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/RequirementParserTest.java b/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/RequirementParserTest.java new file mode 100644 index 00000000000..40d59900446 --- /dev/null +++ b/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/RequirementParserTest.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer.impl; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.apache.felix.fileinstall.plugins.installer.impl.RequirementParser; +import org.junit.Test; +import org.osgi.resource.Requirement; + +public class RequirementParserTest { + + @Test + public void testParseRequireBundle() { + List actual = RequirementParser.parseRequireBundle("foo;bundle-version=1.0.0, bar;bundle-version=\"[1.0,1.1)\", baz;bundle-version=\"[2.0,3.0)\", fnarg"); + + assertEquals(4, actual.size()); + assertEquals("(&(osgi.wiring.bundle=foo)(bundle-version>=1.0.0))", actual.get(0).getDirectives().get("filter")); + assertEquals("(&(osgi.wiring.bundle=bar)(bundle-version>=1.0.0)(!(bundle-version>=1.1.0)))", actual.get(1).getDirectives().get("filter")); + assertEquals("(&(osgi.wiring.bundle=baz)(bundle-version>=2.0.0)(!(bundle-version>=3.0.0)))", actual.get(2).getDirectives().get("filter")); + assertEquals("(osgi.wiring.bundle=fnarg)", actual.get(3).getDirectives().get("filter")); + } + + @Test + public void testParseRequireCapability() { + List actual = RequirementParser.parseRequireCapability("osgi.extender; filter:=\"(&(osgi.extender=osgi.ds)(version>=1.0))\"; effective:=active, osgi.service; filter:=\"(objectClass=org.example.Foo)\""); + + assertEquals(2, actual.size()); + assertEquals("(&(osgi.extender=osgi.ds)(version>=1.0))", actual.get(0).getDirectives().get("filter")); + assertEquals("active", actual.get(0).getDirectives().get("effective")); + } + +} diff --git a/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/TestLogService.java b/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/TestLogService.java new file mode 100644 index 00000000000..953f17d1013 --- /dev/null +++ b/fileinstall-plugins/installer/src/test/java/org/apache/felix/fileinstall/plugins/installer/impl/TestLogService.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.installer.impl; + +import java.util.LinkedList; +import java.util.List; + +import org.osgi.framework.ServiceReference; +import org.osgi.service.log.LogService; + +class TestLogService implements LogService { + + List logged = new LinkedList<>(); + + @Override + public void log(int level, String message) { + log(null, level, message, null); + } + + @Override + public void log(int level, String message, Throwable exception) { + log(null, level, message, exception); + } + + @SuppressWarnings("rawtypes") + @Override + public void log(ServiceReference sr, int level, String message) { + log(sr, level, message, null); + } + + @SuppressWarnings("rawtypes") + @Override + public void log(ServiceReference sr, int level, String message, Throwable exception) { + String levelStr; + switch (level) { + case LogService.LOG_DEBUG: + levelStr = "DEBUG"; + break; + case LogService.LOG_INFO: + levelStr = "INFO"; + break; + case LogService.LOG_WARNING: + levelStr = "WARNING"; + break; + case LogService.LOG_ERROR: + levelStr = "ERROR"; + break; + default: + levelStr = "UNKNOWN"; + } + String formatted = String.format("%s: %s %s", levelStr, message, exception != null ? exception : ""); + this.logged.add(formatted); + } + +} diff --git a/fileinstall-plugins/installer/src/test/resources/invalid-missing-index-path.bar b/fileinstall-plugins/installer/src/test/resources/invalid-missing-index-path.bar new file mode 100644 index 00000000000..a9d90fc29f5 Binary files /dev/null and b/fileinstall-plugins/installer/src/test/resources/invalid-missing-index-path.bar differ diff --git a/fileinstall-plugins/installer/src/test/resources/invalid-missing-requires.bar b/fileinstall-plugins/installer/src/test/resources/invalid-missing-requires.bar new file mode 100644 index 00000000000..957fd998787 Binary files /dev/null and b/fileinstall-plugins/installer/src/test/resources/invalid-missing-requires.bar differ diff --git a/fileinstall-plugins/installer/src/test/resources/invalid-no-index.bar b/fileinstall-plugins/installer/src/test/resources/invalid-no-index.bar new file mode 100644 index 00000000000..dcb5f73db85 Binary files /dev/null and b/fileinstall-plugins/installer/src/test/resources/invalid-no-index.bar differ diff --git a/fileinstall-plugins/installer/src/test/resources/invalid-wrong-extension.xxx b/fileinstall-plugins/installer/src/test/resources/invalid-wrong-extension.xxx new file mode 100644 index 00000000000..a3f6127c389 Binary files /dev/null and b/fileinstall-plugins/installer/src/test/resources/invalid-wrong-extension.xxx differ diff --git a/fileinstall-plugins/installer/src/test/resources/valid1.bar b/fileinstall-plugins/installer/src/test/resources/valid1.bar new file mode 100644 index 00000000000..b27790c807f Binary files /dev/null and b/fileinstall-plugins/installer/src/test/resources/valid1.bar differ diff --git a/fileinstall-plugins/installer/src/test/resources/valid2.bar b/fileinstall-plugins/installer/src/test/resources/valid2.bar new file mode 100644 index 00000000000..ad7a94b1b5d Binary files /dev/null and b/fileinstall-plugins/installer/src/test/resources/valid2.bar differ diff --git a/fileinstall-plugins/pom.xml b/fileinstall-plugins/pom.xml new file mode 100644 index 00000000000..664b277ef10 --- /dev/null +++ b/fileinstall-plugins/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + + org.apache.felix + org.apache.felix.fileinstall.plugins.reactor + Apache Felix File Install Plugin Reactor + 1.0.0-SNAPSHOT + pom + + + resolver + installer + installer.test + + + + diff --git a/fileinstall-plugins/resolver/.gitignore b/fileinstall-plugins/resolver/.gitignore new file mode 100644 index 00000000000..ae3c1726048 --- /dev/null +++ b/fileinstall-plugins/resolver/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/fileinstall-plugins/resolver/bnd.bnd b/fileinstall-plugins/resolver/bnd.bnd new file mode 100644 index 00000000000..15c46c59e8b --- /dev/null +++ b/fileinstall-plugins/resolver/bnd.bnd @@ -0,0 +1,4 @@ +-conditionalpackage: net.jcip.annotations + +-exportcontents: ${packages;VERSIONED} + \ No newline at end of file diff --git a/fileinstall-plugins/resolver/pom.xml b/fileinstall-plugins/resolver/pom.xml new file mode 100644 index 00000000000..f6aa5c7cc3a --- /dev/null +++ b/fileinstall-plugins/resolver/pom.xml @@ -0,0 +1,128 @@ + + + 4.0.0 + + + org.apache.felix + felix-parent + 3 + ../../pom/pom.xml + + + org.apache.felix.fileinstall.plugins.resolver + Apache Felix File Install Resolver Plugin + 1.0.0-SNAPSHOT + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/fileinstall-plugins/resolver + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/fileinstall-plugins/resolver + http://svn.apache.org/repos/asf/felix/fileinstall + + + + 7 + + + + + org.osgi + osgi.core + 6.0.0 + provided + + + org.osgi + osgi.cmpn + 6.0.0 + provided + + + org.osgi + osgi.annotation + 6.0.1 + + + biz.aQute.bnd + biz.aQute.bndlib + 3.3.0 + + + net.jcip + jcip-annotations + 1.0 + + + biz.aQute.bnd + biz.aQute.repository + 3.4.0 + + + junit + junit + + + + + + + biz.aQute.bnd + bnd-maven-plugin + 3.4.0 + + + default-bnd-process + + bnd-process + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + [1.8,) + + + + run + + + + + + + + + + + + + + + + diff --git a/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/PluginResolver.java b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/PluginResolver.java new file mode 100644 index 00000000000..9b9259b8cc7 --- /dev/null +++ b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/PluginResolver.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.resolver; + +import org.osgi.annotation.versioning.ProviderType; + +/** + * Service interface for a Plugin Resolver. Takes a request consisting of a list + * of index URIs and returns a resolution result, consisting of a list of + * resources to be installed. + */ +@ProviderType +public interface PluginResolver { + + ResolveResult resolve(ResolveRequest request) throws Exception; + +} diff --git a/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/ResolveRequest.java b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/ResolveRequest.java new file mode 100644 index 00000000000..6a48da66f17 --- /dev/null +++ b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/ResolveRequest.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.resolver; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.osgi.resource.Requirement; + +/** + * Represents a request to resolve certain requirements in the context of the + * current OSGi Framework. + */ +public class ResolveRequest { + + private final String name; + private final String symbolicName; + private final String version; + private final List indexes; + private final Collection requirements; + + /** + * Construct a request. + * + * @param indexes + * The list of repository indexes to search for installable + * resources. + * @param requirements + * The list of requirements to resolve. + */ + public ResolveRequest(String name, String symbolicName, String version, List indexes, Collection requirements) { + this.name = name; + this.symbolicName = symbolicName; + this.version = version; + this.indexes = new ArrayList<>(indexes); + this.requirements = new ArrayList<>(requirements); + } + + public String getName() { + return this.name; + } + + public String getSymbolicName() { + return this.symbolicName; + } + + public String getVersion() { + return this.version; + } + + public List getIndexes() { + return Collections.unmodifiableList(this.indexes); + } + + public Collection getRequirements() { + return Collections.unmodifiableCollection(this.requirements); + } +} diff --git a/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/ResolveResult.java b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/ResolveResult.java new file mode 100644 index 00000000000..906eb298bbc --- /dev/null +++ b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/ResolveResult.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.resolver; + +import java.util.Map; + +import org.osgi.resource.Resource; + +public interface ResolveResult { + + /** + * Return the request that was satisfied by this result. + */ + ResolveRequest getRequest(); + + /** + * Return the resources to install. Each resource is mapped to a location, + * which is either the location field of an existing installed bundle or the + * physical URL of a location from which the bundle can be installed. + */ + Map getResources(); + +} diff --git a/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/BasicRegistry.java b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/BasicRegistry.java new file mode 100644 index 00000000000..03048e918e6 --- /dev/null +++ b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/BasicRegistry.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.resolver.impl; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import aQute.bnd.service.Registry; + +class BasicRegistry implements Registry { + + private final Map, List> plugins = new HashMap<>(); + + synchronized BasicRegistry put(Class clazz, T plugin) { + List list = plugins.get(clazz); + if (list == null) { + list = new LinkedList<>(); + plugins.put(clazz, list); + } + list.add(plugin); + return this; + } + + @Override + @SuppressWarnings("unchecked") + public synchronized List getPlugins(Class clazz) { + List objs = (List) plugins.get(clazz); + return objs != null ? Collections.unmodifiableList(objs) : Collections.emptyList(); + } + + @Override + public T getPlugin(Class clazz) { + List l = getPlugins(clazz); + return (l != null && !l.isEmpty()) ? l.get(0) : null; + } + +} diff --git a/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/CapReqBase.java b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/CapReqBase.java new file mode 100644 index 00000000000..fbbf8945216 --- /dev/null +++ b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/CapReqBase.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.resolver.impl; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.osgi.resource.Resource; + +public class CapReqBase { + + protected final String namespace; + protected final Map directives; + protected final Map attribs; + protected final Resource resource; + + public CapReqBase(String namespace, Map directives, Map attribs, Resource resource) { + this.namespace = namespace; + this.directives = new HashMap<>(directives); + this.attribs = new HashMap<>(attribs); + this.resource = resource; + } + + public String getNamespace() { + return namespace; + } + + public Map getDirectives() { + return Collections.unmodifiableMap(directives); + } + + public Map getAttributes() { + return Collections.unmodifiableMap(attribs); + } + + public Resource getResource() { + return resource; + } + +} diff --git a/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/CapabilityImpl.java b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/CapabilityImpl.java new file mode 100644 index 00000000000..cacc3eaa9e2 --- /dev/null +++ b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/CapabilityImpl.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.resolver.impl; + +import java.util.Map; +import java.util.Map.Entry; + +import org.osgi.resource.Capability; +import org.osgi.resource.Resource; + +public class CapabilityImpl extends CapReqBase implements Capability { + + public static CapabilityImpl copy(Capability cap, Resource resource) { + return new CapabilityImpl(cap.getNamespace(), cap.getDirectives(), cap.getAttributes(), resource); + } + + public CapabilityImpl(String namespace, Map directives, Map attribs, Resource resource) { + super(namespace, directives, attribs, resource); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(this.namespace); + + boolean first = true; + for (Entry directive : this.directives.entrySet()) { + if (first) { + sb.append(":"); + } + first = false; + sb.append(", ").append(directive.getKey()).append(":=").append(directive.getValue()); + } + for (Entry attrib : this.attribs.entrySet()) { + if (first) { + sb.append(":"); + } + first = false; + sb.append(", ").append(attrib.getKey()).append("=").append(attrib.getValue()); + } + + return sb.toString(); + } + +} diff --git a/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/JarURLConnector.java b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/JarURLConnector.java new file mode 100644 index 00000000000..d36f711f343 --- /dev/null +++ b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/JarURLConnector.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.resolver.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; + +import aQute.bnd.service.url.TaggedData; +import aQute.bnd.service.url.URLConnector; + +public class JarURLConnector implements URLConnector { + + @Override + public TaggedData connectTagged(URL url) throws Exception { + URLConnection connection = url.openConnection(); + if (connection instanceof JarURLConnection) { + connection.setUseCaches(false); + } + return new TaggedData(connection, connection.getInputStream()); + } + + @Override + public InputStream connect(URL url) throws IOException, Exception { + return connectTagged(url).getInputStream(); + } + + @Override + public TaggedData connectTagged(URL url, String tag) throws Exception { + return connectTagged(url); + } + +} diff --git a/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/Path.java b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/Path.java new file mode 100644 index 00000000000..a212b794c5e --- /dev/null +++ b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/Path.java @@ -0,0 +1,119 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.resolver.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +final class Path { + + public static final String SLASH = "/"; + public static final String GO_UP = ".."; + + private final List parts; + + private Path(List parts) { + this.parts = Collections.unmodifiableList(parts); + } + + public static Path parse(String string) { + String[] splits = string.split(SLASH); + List parts = new ArrayList(splits.length); + for (String split : splits) { + if (split.length() > 0) { + parts.add(split); + } + } + return new Path(parts); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + boolean first = true; + for (String part : this.parts) { + if (!first) { + builder.append(SLASH); + } + builder.append(part); + first = false; + } + return builder.toString(); + } + + public Path append(String pathString) { + return append(parse(pathString)); + } + + public Path append(Path path) { + List newParts = new ArrayList(this.parts.size() + path.parts.size()); + newParts.addAll(this.parts); + + boolean atRoot = newParts.isEmpty(); + for (String part : path.parts) { + if (GO_UP.equals(part)) { + if (atRoot) { + newParts.add(GO_UP); + } else { + newParts.remove(newParts.size() - 1); + atRoot = newParts.isEmpty(); + } + } else { + newParts.add(part); + } + } + + return new Path(newParts); + } + + public boolean isEmpty() { + return this.parts.isEmpty(); + } + + public int count() { + return this.parts.size(); + } + + public Path parent() { + ArrayList newParts = new ArrayList(this.parts); + if (newParts.isEmpty()) { + newParts.add(GO_UP); + } else { + newParts.remove(newParts.size() - 1); + } + return new Path(newParts); + } + + public String head() { + if (this.parts.isEmpty()) { + throw new IllegalArgumentException("Cannot get head of empty path"); + } + return this.parts.get(0); + } + + public Path tail() { + if (this.parts.isEmpty()) { + throw new IllegalArgumentException("Cannot get tail of empty path"); + } + + ArrayList newParts = new ArrayList(this.parts); + newParts.remove(0); + return new Path(newParts); + } + +} diff --git a/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/PluginResolveContext.java b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/PluginResolveContext.java new file mode 100644 index 00000000000..4c11d7e0c62 --- /dev/null +++ b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/PluginResolveContext.java @@ -0,0 +1,338 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.resolver.impl; + +import java.io.IOException; +import java.net.URI; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.felix.fileinstall.plugins.resolver.ResolveRequest; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.namespace.IdentityNamespace; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.resource.Capability; +import org.osgi.resource.Namespace; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; +import org.osgi.resource.Wiring; +import org.osgi.service.log.LogService; +import org.osgi.service.repository.ContentNamespace; +import org.osgi.service.repository.Repository; +import org.osgi.service.resolver.HostedCapability; +import org.osgi.service.resolver.ResolveContext; + +import aQute.bnd.deployer.repository.FixedIndexedRepo; +import aQute.bnd.service.url.URLConnector; + +class PluginResolveContext extends ResolveContext { + + private static final String INITIAL_RESOURCE_CAPABILITY_NAMESPACE = "__initial"; + private static final String MIME_BUNDLE = "application/vnd.osgi.bundle"; + + private final BundleContext bundleContext; + + // The repositories that will be queries for providers + private final Map repositories = new HashMap<>(); + // A cache of resource->location (URL), generated during resolve and queried + // after resolve in order to fetch the resource. + private final Map resourceLocationMap = new IdentityHashMap<>(); + // A cache of resources to the repositories which own them; used from + // insertHostedCapability method. + private final Map resourceRepositoryMap = new IdentityHashMap<>(); + + private final ResourceImpl initialResource; + private final LogService log; + + PluginResolveContext(BundleContext bundleContext, ResolveRequest request, LogService log) throws IOException { + this.bundleContext = bundleContext; + this.log = log; + + this.initialResource = new ResourceImpl(); + for (Requirement requirement : request.getRequirements()) { + this.initialResource.addRequirement(requirement); + } + this.initialResource.addCapability(createIdentityCap(this.initialResource, request.getName())); + this.initialResource.addCapability(createInitialMarkerCapability(this.initialResource)); + + BasicRegistry registry = new BasicRegistry().put(URLConnector.class, new JarURLConnector()); + + for (URI indexUri : request.getIndexes()) { + // URI cachedIndexUri = getCacheIndexURI(indexUri); + Map repoProps = new HashMap<>(); + repoProps.put("locations", indexUri.toString()); + FixedIndexedRepo repo = new FixedIndexedRepo(); + repo.setRegistry(registry); + repo.setProperties(repoProps); + + this.repositories.put(indexUri, repo); + } + } + + @Override + public Collection getMandatoryResources() { + return Collections.singleton(this.initialResource); + } + + @Override + public List findProviders(Requirement requirement) { + List resultCaps = new LinkedList<>(); + + // Find from installed bundles + Bundle[] bundles = this.bundleContext.getBundles(); + for (Bundle bundle : bundles) { + if (bundle.getState() == Bundle.UNINSTALLED) { + continue; // Skip UNINSTALLED bundles + } + + BundleRevision revision = bundle.adapt(BundleRevision.class); + List bundleCaps = revision.getCapabilities(requirement.getNamespace()); + if (bundleCaps != null) { + for (Capability bundleCap : bundleCaps) { + if (match(requirement, bundleCap, this.log)) { + resultCaps.add(bundleCap); + } + } + } + } + + // Find from repositories + for (Entry repoEntry : this.repositories.entrySet()) { + Repository repository = repoEntry.getValue(); + Map> providers = repository + .findProviders(Collections.singleton(requirement)); + if (providers != null) { + Collection repoCaps = providers.get(requirement); + if (repoCaps != null) { + resultCaps.addAll(repoCaps); + + for (Capability repoCap : repoCaps) { + // Get the list of physical URIs for this resource. + Resource resource = repoCap.getResource(); + // Keep track of which repositories own which resources. + this.resourceRepositoryMap.put(resource, repository); + + // Resolve the Resource's URI relative to the Repository + // Index URI and save for later. + URI repoIndexUri = repoEntry.getKey(); + URI resolvedUri = resolveResourceLocation(resource, repoIndexUri); + if (resolvedUri != null) { + // Cache the resolved URI into the resource URI map, + // which will be used after resolve. + this.resourceLocationMap.put(resource, resolvedUri.toString()); + } + } + } + } + } + return resultCaps; + } + + static boolean match(Requirement requirement, Capability capability) { + return match(requirement, capability, null); + } + + static boolean match(Requirement requirement, Capability capability, LogService log) { + // Namespace MUST match + if (!requirement.getNamespace().equals(capability.getNamespace())) { + return false; + } + + // If capability effective!=resolve then it matches only requirements + // with same effective + String capabilityEffective = capability.getDirectives().get(Namespace.CAPABILITY_EFFECTIVE_DIRECTIVE); + if (capabilityEffective != null) { + String requirementEffective = requirement.getDirectives().get(Namespace.REQUIREMENT_EFFECTIVE_DIRECTIVE); + if (!capabilityEffective.equals(Namespace.EFFECTIVE_RESOLVE) + && !capabilityEffective.equals(requirementEffective)) { + return false; + } + } + + String filterStr = requirement.getDirectives().get(Namespace.REQUIREMENT_FILTER_DIRECTIVE); + if (filterStr == null) { + return true; // no filter, the requirement always matches + } + + try { + Filter filter = FrameworkUtil.createFilter(filterStr); + return filter.matches(capability.getAttributes()); + } catch (InvalidSyntaxException e) { + if (log != null) { + Resource resource = requirement.getResource(); + String id = resource != null ? getIdentity(resource) : ""; + log.log(LogService.LOG_ERROR, + String.format("Invalid filter syntax in requirement from resource %s: %s", id, filterStr), e); + } + return false; + } + } + + /** + * Get the repository index for the specified resource, where 0 indicates an + * existing OSGi bundle in the framework and -1 indicates not found. This + * method is used by + * {@link #insertHostedCapability(List, HostedCapability)}. + */ + private int findResourceRepositoryIndex(Resource resource) { + if (resource instanceof BundleRevision) { + return 0; + } + + int index = 1; + Repository repo = this.resourceRepositoryMap.get(resource); + if (repo == null) { + return -1; + } + for (Repository match : this.repositories.values()) { + if (repo == match) { + return index; + } + index++; + } + // Still not found + return -1; + } + + @Override + public int insertHostedCapability(List capabilities, HostedCapability hc) { + int hostIndex = findResourceRepositoryIndex(hc.getResource()); + if (hostIndex == -1) { + throw new IllegalArgumentException( + "Hosted capability has host resource not found in any known repository."); + } + + for (int pos = 0; pos < capabilities.size(); pos++) { + int resourceIndex = findResourceRepositoryIndex(capabilities.get(pos).getResource()); + if (resourceIndex > hostIndex) { + capabilities.add(pos, hc); + return pos; + } + } + + // The list passed by (some versions of) Felix does not support the + // single-arg add() method... this throws UnsupportedOperationException. + // So we have to call the two-arg add() with an explicit index. + int lastPos = capabilities.size(); + capabilities.add(lastPos, hc); + return lastPos; + } + + @Override + public boolean isEffective(Requirement requirement) { + return true; + } + + @Override + public Map getWirings() { + Map wiringMap = new HashMap<>(); + Bundle[] bundles = this.bundleContext.getBundles(); + for (Bundle bundle : bundles) { + // BundleRevision extends Resource + BundleRevision revision = bundle.adapt(BundleRevision.class); + // BundleWiring extends Wiring + BundleWiring wiring = revision.getWiring(); + if (wiring != null) { + wiringMap.put(revision, wiring); + } + } + return wiringMap; + } + + boolean isInitialResource(Resource resource) { + List markerCaps = resource.getCapabilities(INITIAL_RESOURCE_CAPABILITY_NAMESPACE); + return markerCaps != null && !markerCaps.isEmpty(); + } + + String getLocation(Resource resource) { + String location; + if (resource instanceof BundleRevision) { + location = ((BundleRevision) resource).getBundle().getLocation(); + } else { + location = this.resourceLocationMap.get(resource); + } + return location; + } + + private static CapabilityImpl createInitialMarkerCapability(Resource resource) { + return new CapabilityImpl(INITIAL_RESOURCE_CAPABILITY_NAMESPACE, Collections.emptyMap(), Collections.emptyMap(), + resource); + } + + private static CapabilityImpl createIdentityCap(Resource resource, String identity) { + Map idCapAttrs = new HashMap<>(); + idCapAttrs.put(IdentityNamespace.IDENTITY_NAMESPACE, identity); + CapabilityImpl idCap = new CapabilityImpl(IdentityNamespace.IDENTITY_NAMESPACE, Collections.emptyMap(), + idCapAttrs, resource); + return idCap; + } + + private static URI resolveResourceLocation(Resource resource, URI indexUri) { + List contentCaps = resource.getCapabilities(ContentNamespace.CONTENT_NAMESPACE); + if (contentCaps != null) { + for (Capability contentCap : contentCaps) { + // Ensure this content entry has the correct MIME type for a + // bundle + if (MIME_BUNDLE.equals(contentCap.getAttributes().get(ContentNamespace.CAPABILITY_MIME_ATTRIBUTE))) { + // Get the URI attribute in the index, which is either an + // asbolute URI, or a relative URI to be resolved against + // the index URI. + URI rawUri = null; + Object uriObj = contentCap.getAttributes().get(ContentNamespace.CAPABILITY_URL_ATTRIBUTE); + if (uriObj instanceof URI) { + rawUri = (URI) uriObj; + } else if (uriObj instanceof String) { + rawUri = URI.create((String) uriObj); + } + if (rawUri != null) { + URI resolvedUri = URIUtils.resolve(indexUri, rawUri); + return resolvedUri; + } + } + } + } + + // No content capability was found in the appropriate form + return null; + } + + private static String getIdentity(Resource resource) { + List caps = resource.getCapabilities(IdentityNamespace.IDENTITY_NAMESPACE); + if (caps == null || caps.isEmpty()) { + return ""; + } + + Object idObj = caps.get(0).getAttributes().get(IdentityNamespace.IDENTITY_NAMESPACE); + if (!(idObj instanceof String)) { + return ""; + } + + return (String) idObj; + } + +} diff --git a/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/PluginResolverComponent.java b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/PluginResolverComponent.java new file mode 100644 index 00000000000..b7661054ce8 --- /dev/null +++ b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/PluginResolverComponent.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.resolver.impl; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.felix.fileinstall.plugins.resolver.PluginResolver; +import org.apache.felix.fileinstall.plugins.resolver.ResolveRequest; +import org.apache.felix.fileinstall.plugins.resolver.ResolveResult; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.resource.Resource; +import org.osgi.resource.Wire; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicyOption; +import org.osgi.service.log.LogService; +import org.osgi.service.resolver.Resolver; + +@Component(name = "org.apache.felix.fileinstall.plugins.resolver") +public class PluginResolverComponent implements PluginResolver { + + private BundleContext bundleContext; + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policyOption = ReferencePolicyOption.GREEDY) + LogService log; + + @Reference(target = "(!(" + Constants.SERVICE_BUNDLEID + "=0))") + Resolver frameworkResolver; + + @Activate + void activate(BundleContext bundleContext) { + this.bundleContext = bundleContext; + } + + @Override + public ResolveResult resolve(ResolveRequest request) throws Exception { + PluginResolveContext context = new PluginResolveContext(bundleContext, request, log); + Map> resolved = frameworkResolver.resolve(context); + + ResolveResultImpl result = new ResolveResultImpl(request); + for (Entry> entry : resolved.entrySet()) { + Resource resource = entry.getKey(); + // Skip the synthetic "<>" resource + if (!context.isInitialResource(resource)) { + result.addResource(resource, context.getLocation(resource)); + } + } + + return result; + } +} \ No newline at end of file diff --git a/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/RequirementImpl.java b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/RequirementImpl.java new file mode 100644 index 00000000000..4393bf37dc9 --- /dev/null +++ b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/RequirementImpl.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.resolver.impl; + +import java.util.Map; +import java.util.Map.Entry; + +import org.osgi.resource.Namespace; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; + +public class RequirementImpl extends CapReqBase implements Requirement { + + public static RequirementImpl copy(Requirement req, Resource resource) { + return new RequirementImpl(req.getNamespace(), req.getDirectives(), req.getAttributes(), resource); + } + + public RequirementImpl(String namespace, Map directives, Map attribs, Resource resource) { + super(namespace, directives, attribs, resource); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(this.namespace); + + String filterStr = this.directives.get(Namespace.REQUIREMENT_FILTER_DIRECTIVE); + if (filterStr != null) { + sb.append(":").append(filterStr); + } + + for (Entry directive : this.directives.entrySet()) { + if (!Namespace.REQUIREMENT_FILTER_DIRECTIVE.equals(directive.getKey())) { + sb.append(", ").append(directive.getKey()).append(":=").append(directive.getValue()); + } + } + + for (Entry attrib : this.attribs.entrySet()) { + sb.append(", ").append(attrib.getKey()).append("=").append(attrib.getValue()); + } + + return sb.toString(); + } + +} diff --git a/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/ResolveResultImpl.java b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/ResolveResultImpl.java new file mode 100644 index 00000000000..5de93b8484b --- /dev/null +++ b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/ResolveResultImpl.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.resolver.impl; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Map; + +import org.apache.felix.fileinstall.plugins.resolver.ResolveRequest; +import org.apache.felix.fileinstall.plugins.resolver.ResolveResult; +import org.osgi.resource.Resource; + +public class ResolveResultImpl implements ResolveResult { + + private final ResolveRequest request; + private final Map resourceMap = new IdentityHashMap<>(); + + public ResolveResultImpl(ResolveRequest request) { + this.request = request; + } + + @Override + public ResolveRequest getRequest() { + return request; + } + + @Override + public Map getResources() { + return Collections.unmodifiableMap(resourceMap); + } + + void addResource(Resource resource, String location) { + resourceMap.put(resource, location); + } + + +} \ No newline at end of file diff --git a/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/ResourceImpl.java b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/ResourceImpl.java new file mode 100644 index 00000000000..d30090c63d7 --- /dev/null +++ b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/ResourceImpl.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.resolver.impl; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.osgi.framework.namespace.IdentityNamespace; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; + +public class ResourceImpl implements Resource { + + private final Map> requirements = new HashMap<>(); + private final Map> capabilities = new HashMap<>(); + + void addRequirement(Requirement req) { + if (req.getResource() != this) { + req = RequirementImpl.copy(req, this); + } + + List list = this.requirements.get(req.getNamespace()); + if (list == null) { + list = new LinkedList<>(); + this.requirements.put(req.getNamespace(), list); + } + list.add(req); + } + + void addCapability(Capability cap) { + if (cap.getResource() != this) { + cap = CapabilityImpl.copy(cap, this); + } + + List list = this.capabilities.get(cap.getNamespace()); + if (list == null) { + list = new LinkedList<>(); + this.capabilities.put(cap.getNamespace(), list); + } + list.add(cap); + } + + @Override + public List getCapabilities(String namespace) { + List result; + if (namespace == null) { + result = new LinkedList<>(); + for (List list : this.capabilities.values()) { + result.addAll(list); + } + } else { + result = this.capabilities.get(namespace); + } + return result != null ? result : Collections.emptyList(); + } + + @Override + public List getRequirements(String namespace) { + List result; + if (namespace == null) { + result = new LinkedList<>(); + for (List list : this.requirements.values()) { + result.addAll(list); + } + } else { + result = this.requirements.get(namespace); + } + return result != null ? result : Collections.emptyList(); + } + + @Override + public String toString() { + String name = ""; + List idCaps = getCapabilities(IdentityNamespace.IDENTITY_NAMESPACE); + if (idCaps != null && !idCaps.isEmpty()) { + Object nameObj = idCaps.get(0).getAttributes().get(IdentityNamespace.IDENTITY_NAMESPACE); + if (nameObj instanceof String) { + name = (String) nameObj; + } + } + return name; + } + +} diff --git a/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/URIUtils.java b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/URIUtils.java new file mode 100644 index 00000000000..54f863c9ad4 --- /dev/null +++ b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/impl/URIUtils.java @@ -0,0 +1,166 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.resolver.impl; + +import java.net.URI; + +public final class URIUtils { + + private static final String JAR_URI_SCHEME = "jar"; + + private URIUtils() {} + + /** + * Resolve a URI relative to a base URI. + * + * @param baseUri + * The base URI which must be absolute. + * @param uri + * The relative URI to resolve against the specified base. Note + * that if this URI is absolute, it will be simply returned + * unchanged. + * @return An absolute, resolved URI. + * @throws {@link IllegalArgumentException} If the {@code baseUri} parameter + * is null/relative/opaque or if the URI otherwise cannot be + * resolved. + */ + public static URI resolve(URI baseUri, URI uri) throws IllegalArgumentException { + URI resolved; + if (uri.isAbsolute()) { + resolved = uri; + } else { + URI relative; + String innerPath; + + // Work out if the relative URI contains a bang (!), meaning to navigate into a JAR + String uriPath = uri.getPath(); + int bangIndex = uriPath.indexOf('!'); + if (bangIndex >= 0) { + relative = URI.create(uriPath.substring(0, bangIndex)); + innerPath = uriPath.substring(bangIndex + 1); + } else { + relative = uri; + innerPath = null; + } + + // Do the normal resolution + if (baseUri == null) { + throw new IllegalArgumentException(String.format("Cannot resolve relative URI (%s): base URI is null/unavailable.", uri)); + } + if (!baseUri.isAbsolute()) { + throw new IllegalArgumentException(String.format("Cannot resolve relative URI (%s): base URI is also relative (%s).", uri, baseUri)); + } + if (baseUri.isOpaque()) { + // Handle "jar:" URIs as a special case + if (JAR_URI_SCHEME.equals(baseUri.getScheme())) { + resolved = resolveJarUri(baseUri, relative); + } else { + throw new IllegalArgumentException(String.format("Cannot resolve relative URI (%s): base URI is opaque (%s).", uri, baseUri)); + } + } else { + resolved = baseUri.resolve(relative); + } + + // If an inner path was indicated, create a jar: URI + if (innerPath != null) { + resolved = URI.create("jar:" + resolved.toString() + "!" + innerPath); + } + } + return resolved; + } + + private static URI resolveJarUri(URI baseUri, URI uri) throws IllegalArgumentException { + assert JAR_URI_SCHEME.equals(baseUri.getScheme()) : "not a jar: URI"; + + String ssp = baseUri.getSchemeSpecificPart(); + int bangIndex = ssp.lastIndexOf('!'); + if (bangIndex == -1) { + throw new IllegalArgumentException("Invalid URI in the jar: scheme (missing ! separator): " + baseUri); + } + URI jarUri = URI.create(ssp.substring(0, bangIndex)); + String pathStr = ssp.substring(bangIndex + 1); + + String query = baseUri.getQuery(); + if (query == null) { + int qmIndex = pathStr.lastIndexOf('?'); + if (qmIndex >= 0) { + query = pathStr.substring(qmIndex + 1); + pathStr = pathStr.substring(0, qmIndex); + } else { + query = ""; + } + } + boolean nonavigate = query.indexOf("navigate=false") >= 0; + + Path basePath = Path.parse(pathStr); + Path baseDir = basePath.parent(); + Path resolvedPath = baseDir.append(uri.getPath()); + + URI result; + if (resolvedPath.isEmpty()) { + result = jarUri; + } else if (Path.GO_UP.equals(resolvedPath.head())) { + if (nonavigate) { + throw new IllegalArgumentException("Cannot navigate outside JAR contents."); + } + result = jarUri.resolve(resolvedPath.tail().toString()); + } else { + result = URI.create(JAR_URI_SCHEME + ":" + jarUri + "!/" + resolvedPath); + } + + return result; + } + + /** + * Returns the file name of the resource references by the URI. Works with jar: URIs, which normally return + * {@code null} if the {@link URI#getPath()} call is used. + * @param uri + * @return The filename, if it can be calculated. + * @throws IllegalArgumentException if the filename cannot be calculated (e.g. if the scheme is not understood). + */ + public static String getFileName(URI uri) throws IllegalArgumentException { + String filename; + if (JAR_URI_SCHEME.equals(uri.getScheme())) { + String ssp = uri.getRawSchemeSpecificPart(); + String innerPath; + int bang = ssp.indexOf('!'); + if (bang < 0) { + throw new IllegalArgumentException("Invalid jar content URI; missing '!/' path separator: " + uri); + } else { + innerPath = ssp.substring(bang + 1); + } + filename = getPathLastSegment(innerPath); + } else { + String path = uri.getPath(); + if (path == null) { + throw new IllegalArgumentException("Cannot calculate filename for unknown opaque URI scheme: " + uri); + } + filename = getPathLastSegment(path); + } + return filename; + } + + private static String getPathLastSegment(String path) { + int slash = path.lastIndexOf('/'); + if (slash < 0) { + return path; + } + + return path.substring(slash + 1); + } + +} diff --git a/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/package-info.java b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/package-info.java new file mode 100644 index 00000000000..a8e3e6bf541 --- /dev/null +++ b/fileinstall-plugins/resolver/src/main/java/org/apache/felix/fileinstall/plugins/resolver/package-info.java @@ -0,0 +1,19 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +@org.osgi.annotation.versioning.Version("1.0.0") +package org.apache.felix.fileinstall.plugins.resolver; + diff --git a/fileinstall-plugins/resolver/src/test/java/org/apache/felix/fileinstall/plugins/resolver/impl/PluginResolveContextTest.java b/fileinstall-plugins/resolver/src/test/java/org/apache/felix/fileinstall/plugins/resolver/impl/PluginResolveContextTest.java new file mode 100644 index 00000000000..7749b0e5244 --- /dev/null +++ b/fileinstall-plugins/resolver/src/test/java/org/apache/felix/fileinstall/plugins/resolver/impl/PluginResolveContextTest.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.resolver.impl; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +@SuppressWarnings("serial") +public class PluginResolveContextTest { + + @Test + public void testMatchRequirementsEffectiveResolve() throws Exception { + assertTrue("effective should match: req=default, cap=default", + PluginResolveContext.match(createRequirement("foo", "(foo=bar)", null), + createCapability("foo", null, new HashMap() { + { + put("foo", "bar"); + } + }))); + assertTrue("effective should match: req=default, cap=resolve", + PluginResolveContext.match(createRequirement("foo", "(foo=bar)", null), + createCapability("foo", "resolve", new HashMap() { + { + put("foo", "bar"); + } + }))); + assertTrue("effective should match: req=resolve, cap=default", + PluginResolveContext.match(createRequirement("foo", "(foo=bar)", "resolve"), + createCapability("foo", null, new HashMap() { + { + put("foo", "bar"); + } + }))); + assertTrue("effective should match: req=resolve, cap=resolve", + PluginResolveContext.match(createRequirement("foo", "(foo=bar)", "resolve"), + createCapability("foo", "resolve", new HashMap() { + { + put("foo", "bar"); + } + }))); + } + + @Test + public void testEffectiveResolveCapsMatchAnyRequirement() throws Exception { + // Caps with effective=resolve (or default) match requirements with any + // effective=... + assertTrue("effective should match: req=blah, cap=default", + PluginResolveContext.match(createRequirement("foo", "(foo=bar)", "blah"), + createCapability("foo", null, new HashMap() { + { + put("foo", "bar"); + } + }))); + assertTrue("effective should match: req=blah, cap=resolve", + PluginResolveContext.match(createRequirement("foo", "(foo=bar)", "blah"), + createCapability("foo", "resolve", new HashMap() { + { + put("foo", "bar"); + } + }))); + } + + @Test + public void testEffectiveNonResolveCapsMatchRequirementWithSame() throws Exception { + // Caps with effective=!resolve ONLY match reqs with the same effective + assertTrue("effective should match: req=blah, cap=blah", + PluginResolveContext.match(createRequirement("foo", "(foo=bar)", "blah"), + createCapability("foo", "blah", new HashMap() { + { + put("foo", "bar"); + } + }))); + assertFalse("effective should NOT match: req=default, cap=blah", + PluginResolveContext.match(createRequirement("foo", "(foo=bar)", null), + createCapability("foo", "blah", new HashMap() { + { + put("foo", "bar"); + } + }))); + assertFalse("effective should NOT match: req=default, cap=blah", + PluginResolveContext.match(createRequirement("foo", "(foo=bar)", "resolve"), + createCapability("foo", "blah", new HashMap() { + { + put("foo", "bar"); + } + }))); + assertFalse("effective should NOT match: req=wibble, cap=blah", + PluginResolveContext.match(createRequirement("foo", "(foo=bar)", "wibble"), + createCapability("foo", "blah", new HashMap() { + { + put("foo", "bar"); + } + }))); + } + + static RequirementImpl createRequirement(String ns, String filter, String effective) { + Map directives = new HashMap<>(); + if (filter != null) { + directives.put("filter", "(foo=bar)"); + } + if (effective != null) { + directives.put("effective", effective); + } + + return new RequirementImpl(ns, directives, Collections.emptyMap(), null); + } + + static CapabilityImpl createCapability(String ns, String effective, Map attrs) { + Map directives = new HashMap<>(); + if (effective != null) { + directives.put("effective", effective); + } + + return new CapabilityImpl(ns, directives, attrs, null); + } + +} diff --git a/fileinstall-plugins/resolver/src/test/java/org/apache/felix/fileinstall/plugins/resolver/impl/URIUtilsTest.java b/fileinstall-plugins/resolver/src/test/java/org/apache/felix/fileinstall/plugins/resolver/impl/URIUtilsTest.java new file mode 100644 index 00000000000..951e24d9da3 --- /dev/null +++ b/fileinstall-plugins/resolver/src/test/java/org/apache/felix/fileinstall/plugins/resolver/impl/URIUtilsTest.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * Copyright (c) Intel Corporation + * Copyright (c) 2017 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *******************************************************************************/ +package org.apache.felix.fileinstall.plugins.resolver.impl; + +import java.net.URI; + +import junit.framework.TestCase; + +public class URIUtilsTest extends TestCase { + public void testResolveRelativeHttpUri() { + URI resolved = URIUtils.resolve(URI.create("http://example.org/repository/index.xml"), URI.create("bundle/bundle.jar")); + assertEquals("http://example.org/repository/bundle/bundle.jar", resolved.toString()); + } + + public void testResolveAbsoluteFileBaseUri() { + URI resolved = URIUtils.resolve(URI.create("file:/Users/bob/repository/index.xml"), URI.create("bundle/bundle.jar")); + assertEquals("file:/Users/bob/repository/bundle/bundle.jar", resolved.toString()); + } + + public void testResolveJarUri() throws Exception { + URI base = new URI("jar:http://example.com/systems/example.jar!/sys/system.xml"); + + // Sibling resource within the JAR + assertEquals("jar:http://example.com/systems/example.jar!/sys/repo/index-nim.xml", URIUtils.resolve(base, new URI("repo/index-nim.xml")).toString()); + + // Move up a path within the JAR + assertEquals("jar:http://example.com/systems/example.jar!/repo/index-nim.xml", URIUtils.resolve(base, new URI("../repo/index-nim.xml")).toString()); + + // Here's the tricky bit... move up a path *outside* the JAR + assertEquals("http://example.com/systems/index.xml", URIUtils.resolve(base, new URI("../../index.xml")).toString()); + assertEquals("http://example.com/repos/index.xml", URIUtils.resolve(base, new URI("../../../repos/index.xml")).toString()); + + // Even trickier... move up a path to another JAR, and then into it + assertEquals("jar:http://example.com/systems/other.jar!/index.xml", URIUtils.resolve(base, new URI("../../other.jar!/index.xml")).toString()); + } + + public void testResolveJarUriNoNavigateUp() throws Exception { + URI base = new URI("jar:http://example.com/systems/example.jar!/sys/system.xml?navigate=false"); + + // Sibling resource within the JAR + assertEquals("jar:http://example.com/systems/example.jar!/sys/repo/index-nim.xml", URIUtils.resolve(base, new URI("repo/index-nim.xml")).toString()); + + // Move up a path within the JAR + assertEquals("jar:http://example.com/systems/example.jar!/repo/index-nim.xml", URIUtils.resolve(base, new URI("../repo/index-nim.xml")).toString()); + + // Moving outside the JAR is not allowed + try { + URIUtils.resolve(base, new URI("../../index.xml")); + fail("Should throw IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // expected + } + } + + public void testGetFileName() { + assertEquals("system.xml", URIUtils.getFileName(URI.create("http://www.example.com/foo/bar/system.xml"))); + assertEquals("system.xml", URIUtils.getFileName(URI.create("jar:http://www.example.com/foo/bar/system.jar!/bar/foo/system.xml"))); + + try { + URIUtils.getFileName(URI.create("blah:this/could/be/anything.xml")); + fail("Should throw IllArgExc"); + } catch (IllegalArgumentException e) { + // expected + } + + try { + URIUtils.getFileName(URI.create("jar:http://www.example.com/foo/bar/system.jar")); + fail("Should throw IllArgExc"); + } catch (IllegalArgumentException e) { + // expected + } + } +} diff --git a/fileinstall/doc/changelog.txt b/fileinstall/doc/changelog.txt new file mode 100644 index 00000000000..4d940ac53b8 --- /dev/null +++ b/fileinstall/doc/changelog.txt @@ -0,0 +1,345 @@ +Version 3.6.4 +------------- +** Bug + * [FELIX-4661] - FileInstall: Property "felix.fileinstall.filter" ignored when the service start + * [FELIX-4740] - Bundles don't start if no BundleEvent fired after reaching start level + * [FELIX-4906] - Changes in symlinked bundles not detected with NIO2 WatcherScanner + * [FELIX-5125] - NPE in ConfigInstaller + * [FELIX-5686] - NPE in ConfigInstaller on ConfigDelete + * [FELIX-5691] - Fix whitespace handling in TypedProperties + * [FELIX-5692] - TypedProperties is missing a method to add a raw value + * [FELIX-5723] - ConfigInstaller is not deleting files in the etc directory + +Version 3.6.2 +------------- +** Bug + * [FELIX-4416] - Ability to specify an encoding for config files + * [FELIX-4743] - Have to escape '=' characters in quoted values in *.config files + * [FELIX-5145] - FileInstall writes .config files back with backslashes escaping spaces in config values + * [FELIX-5306] - User friendly syntax for floats and doubles in FileInstall + * [FELIX-5448] - Exceptions from bundle.start() should be logged, not swallowed + * [FELIX-5539] - Directory Watcher leaks ZipInflater + * [FELIX-5689] - FileInstall always expand configuration files + +** Improvement + * [FELIX-5684] - Set multi-location for created configurations + +Version 3.6.0 +------------- +** Improvement + * [FELIX-5609] - Full support for both untyped and typed configurations + * [FELIX-5610] - Files should be deleted if a configuration is deleted + +Version 3.5.8 +------------- +** Bug + * [FELIX-5133] Remove properties that are not present in updated configuration + +Version 3.5.6 +------------- +** Bug + * [FELIX-5133] - Removing an entry from a configuration does not remove it from the monitored file + +** Improvement + * [FELIX-5261] - FileInstall: allow comments in *.config files + +Version 3.5.4 +------------- +** Bug + * [FELIX-4934] - Only log failures for consistently failing bundles + * [FELIX-5209] - [FileInstall] use framework bundle location to get framework bundle + * [FELIX-5217] - Fileinstall is handling removal of many files inefficiently + * [FELIX-5218] - Fileinstall MemoryLeak / Performance degradation in WatcherScanner + +Version 3.5.2 +------------- +** Bug + * [FELIX-4906] - Changes in symlinked bundles not detected with NIO2 WatcherScanner + * [FELIX-4935] - Felix fileinstall project set to incorrect java version + * [FELIX-4975] - fileinstall's ConfigInstaller leaks it's permission checks to caller + * [FELIX-5135] - Felix Fileinstall does not use all properties + * [FELIX-5136] - Felix Fileinstall temp dir generation is inefficient + * [FELIX-5137] - Felix Fileinstall: add support for recursing and skipping subdirectories + +** Improvement + * [FELIX-4714] - Felix should sort bundles by alphabetical name from "deploy" directory + * [FELIX-5167] - Upgrade to felix utils 1.8.2 + +Version 3.5.0 +------------- +** New Feature + * [FELIX-4759] - Support env:XXX subtitution for environment variables + * [FELIX-4760] - Support default/alternate values for variable substitution + +Version 3.4.2 +------------- +** Bug + * [FELIX-4532] - ConcurrentModificationException in fileinstall + * [FELIX-4635] - Avoid a useless change in the bundle location and a persistence the configuration + +Version 3.4.0 +------------- +** Bug + * [FELIX-4436] - DirectoryWatcher should not refresh transformed bundles on every start-up + * [FELIX-4472] - Various concurrency issues in fileinstall + +** Improvement + * [FELIX-2436] - Reduce the number files created by the fileinstall Scanner. + * [FELIX-2702] - File Install should be smarter about starting failed bundles + * [FELIX-3543] - Upgrade to the new osgi wiring api + * [FELIX-4474] - Remove the static fields on FileInstall + * [FELIX-4475] - Improve logging in FileInstall + +Version 3.2.8 +------------- +** Bug + * [FELIX-3686] - FileInstall bundle contains ConfigurationHandler.java source file + * [FELIX-3712] - FileInstall throws Interrupted exception when refreshed + * [FELIX-3714] - Error when when fragment bundle is updated + * [FELIX-3821] - FileInstall does not support correctly ConfigAdmin being shut down + * [FELIX-3822] - FileInstall should unregister its services + * [FELIX-4071] - ConcurrentModificationException in DirectoryWatcher.bundleChanged + * [FELIX-4225] - compile error on FileInstall.java + * [FELIX-4332] - Incorrect evalution of variables + * [FELIX-4338] - Incorrect overwriting of properties that have not been changed + +Version 3.2.6 +------------- +** Bug + * [FELIX-3635] - Unnecessary processing in DirectoryWatcher + +** Improvement + * [FELIX-3597] - Hard to debug fileinstall because shaded classes lack source in -sources.jar + +Version 3.2.4 +------------- +** Bug + * [FELIX-3414] - BundleException: Unable to acquire global lock for resolve + * [FELIX-3416] - ConcurrentModificationException in DirectoryWatcher.findBundlesWithFragmentsToRefresh + * [FELIX-3493] - PackageAdminImpl thorws NPE - File Install does not handle a bundle uninstall gracefully + +** Improvement + * [FELIX-3487] - Variables are not preserved during write-back if they are defined as framework properties + +Version 3.2.2 +------------- +** Bug + * [FELIX-3467] - Configuration is not flush back to the file + +** Improvement + * [FELIX-3398] - Track new versions of artifact listener and optionally update all bundles when a new version is detected + * [FELIX-3448] Small optimization when using file name filters + +Version 3.2.0 +------------- +** Bug + * [FELIX-2066] - File Install fails to move fragment bundle into RESOLVED state + * [FELIX-2763] - [FileInstall] disableConfigSave actually enables configuration save + * [FELIX-2794] - noInitialDelay is not correctly implemented + * [FELIX-2843] - Parsing PID assumes last 4 chars are file extension + * [FELIX-2852] - Infinite reloading of config file ending with .config + * [FELIX-2890] - FileInstall starts non-daemon threads + * [FELIX-2982] - noInitialDelay option does not work + * [FELIX-3047] - Infinite loop in File Install if java.io.tmpdir is non-writable + * [FELIX-3109] - FileInstall sorts artifact url handlers in the wrong order so that the ranking does not work correctly + * [FELIX-3112] - Default for felix.fileinstall.start.level documented incorrectly + * [FELIX-3346] - File Install doesn't scan when the framework start level is lower than felix.fileinstall.start.level setting + * [FELIX-3373] - File install logging doesn't notify user of important errors + * [FELIX-3396] - Fileinstall is not able to handle configuration file if in a path containing braces + +** Improvement + * [FELIX-2787] - [File Install] Do not perform management activities while framework is starting/stopping + * [FELIX-2848] - Saving config file to original location shouldn't add system properties + * [FELIX-3294] - file install dependencies on osgi artifacts should have "provided" scope + * [FELIX-3386] - File install tests use deprecated EasyMock API and fail on BundleContext.getProperty + +Version 3.1.10 +-------------- +** Bug + * [FELIX-2818] - File Install does not support empty configuration when no configuration already exists + * [FELIX-2799] - FileInstall creates multiple configurations for factory configurations on windows + * [FELIX-2798] - ArtifactListener services are not ordered according to the OSGi ranking + +Version 3.1.6 +------------- +** Bug + * [FELIX-2791] - NPE in getStartLevel due to race condition + +Version 3.1.4 +------------- +** Bug + * [FELIX-2756] - NPE when loadiong configurations + * [FELIX-2745] - New configurations throw NPE + +Version 3.1.2 +------------- +** Bug + * [FELIX-2699] - Disable writing configuration back to .cfg files + * [FELIX-2698] - File Install Circular Configuration Update Loop + * [FELIX-2675] - ileinstall always starts bundle eagerly + +Version 3.1.0 +------------- +** Bug + * [FELIX-2318] - Possible NPE for jars with null Manifest + * [FELIX-2531] - Missing documentation for 'felix.fileinstall.noInitialDelay' configuration property + * [FELIX-2556] - FileInstall fails to update fragments as it tries to stop them + +** Improvement + * [FELIX-2021] - Enable FileInstall to set Bundle Start Level + * [FELIX-2571] - Have fileinstall listen for configuration changes and write them back to the config files + * [FELIX-2643] - When using the jardir url handler, fileinstall should not compress the zip stream + * [FELIX-2663] - Allow ConfigInstaller to perform placeholder substitution from framework properties + +** New Feature + * [FELIX-2513] - Support richer format for configurations + * [FELIX-2514] - felix.fileinstall.dir should support more than one directory + +Version 3.0.2 +------------- +** Bug + * [FELIX-2366] - Avoiding property substitution by escaping does not remove escape character + +** Improvement + * [FELIX-1657] - Add option in fileinstall to start bundles transiently (START_TRANSIENT) + +Version 3.0.0 +------------- +** Bug + * [FELIX-1776] - FileInstall starts already installed bundles twice + * [FELIX-2069] - FileInstall creates ./load by default + * [FELIX-2248] - fileinstall does not restart bundles when the underlying file is modified + * [FELIX-2302] - Fileinstall should add entries for directories when creating JARs + * [FELIX-2313] - fileinstall does not recognize jars if it does not have Bundle-SymbolicName and Bundle-Version + +** Improvement + * [FELIX-2249] - fileinstall should log installation/uninstallation/updation/activation details at INFO level + * [FELIX-2307] - There is no way to bypass / escape property substitution + + +Version 2.0.8 +------------- +** Bug + * [FELIX-1775] - fileinstall release archives contains an empty "load" + folder which should be removed + * [FELIX-1789] - FileInstall is too verbose + * [FELIX-1851] - FileInstall watched bundles are restarted twice upon update + * [FELIX-1861] - FileInstall created temp directories are never deleted + * [FELIX-1862] - fileinstall thread name as printed in log messages need + to be less verbose + * [FELIX-1928] - File installer starts bundles too early on restart + +** Task + * [FELIX-1811] - fileinstall should depend on the official osgi artifacts + + +Version 2.0.4 +------------- +** Bug + * [FELIX-1386] - Updating fileinstall bundle in watched directory + causes IllegalsStateException + * [FELIX-1572] - File Install running in an infinite loop while watching + multiple directories, once of which is itself + * [FELIX-1578] - FileInstall issue when updating already installed bundles + at startup time + * [FELIX-1591] - FIleInstall should have an optional / dynamic import on + org.osgi.service.cm instead of exporting it + * [FELIX-1593] - Got an exception when restarting FileInstall bundle + * [FELIX-1628] - Fileinstall should load configurations as soon as config + admin is available + * [FELIX-1715] - osgi:update on xml deployments will cause ZIPExceptions + * [FELIX-1716] - Fileinstall does not recognize changes in lastmodification + date of an uninstalled bundle inside deploy folder + * [FELIX-1736] - After a restart, fileinstall does not always properly + uninstall bundles + * [FELIX-1750] - fileinstall does not work on jdk 1.4 + * [FELIX-1787] - REGRESSION: FileInstall relies on location to be a valid + URL for update to be successful + * [FELIX-1788] - Set felix.fileinstall.tmpdir to ${java.io.tmpdir} by default + * [FELIX-1790] - fileinstal tmp dir needs to be different for each + watched directory + +** Improvement + * [FELIX-1756] - Allow fileinstall to override (i.e. update) bundles that have + not been deployed by fiileinstall initially + +** New Feature + * [FELIX-1537] - File Install should support XML property files + +Version 2.0.0 +------------- +** Bug + * [FELIX-938] - FileInstall starts a stopped bundles even if it is stopped + transiently by user + * [FELIX-1269] - MalformedURLException for bundle locations installed + by FileInstall + * [FELIX-1377] - fileinstall tries to process files which are not fully + copied yet + * [FELIX-1382] - FileInstall attempts to uninstall "System Bundle" + * [FELIX-1481] - When performing variable substitution, fileinstall throws an + exception if there is a start or stop delimiter without the other one + * [FELIX-1540] - [FileInstall] When removing/re-adding a bundle, all the + dependent bundles don't start anymore + +** Improvement + * [FELIX-1301] - Limit FileInstall configuration information to one line in + the output + * [FELIX-1387] - FileInstall unnenessarily computes length of files + * [FELIX-1475] - Add a file filter for a given watched directory + * [FELIX-1476] - Allow system property substitution while loading configurations + from files + * [FELIX-1553] - fileinstall bundle should have an optional import on + org.osgi.service.log instead of exporting it + * [FELIX-1554] - fileinstall should not export org.apache.felix.fileinstall + and org.apache.felix.fileinstall.utils packages + +** New Feature + * [FELIX-922] - File Install bundle should be extensible to support new + artifact type + * [FELIX-1483] - Fileinstall should support exploded artifacts + +Version 1.2.0 +------------- + * [FELIX-1179] - FileInstall should recognize Jar files by content instead + of extension + * [FELIX-1216] - ClassCastException when running FileInstall with + Knopflerfish + * [FELIX-1228] - Felix File Install - Spaces in File Names + * [FELIX-1203] - NPE in fileinstall if a watched bundle is uninstalled by + some other means + * [FELIX-1235] - NullPointerException due to misconfigured watched dir + * [FELIX-1174] - FileInstall bundle does not print stack trace of exceptions + when log service is not used + * [FELIX-1251] - Looping NullPointerException if the polled directory is + removed after File Install registration + * [FELIX-1241] - Log "Bundle Started" + +Version 1.0.0 +------------- + +** Bug + * [FELIX-1036] - FileInstaller spawns multiple watchers for same directory + +** Improvement + * [FELIX-1055] - Making FileInstall bundle CDC-1.1/Foundation 1.1 compatible + +Version 0.9.2 +-------------- + +** Bug + * [FELIX-895] - File Install treats configuration files with identical subnames as the same configuration + * [FELIX-926] - FileInstall does not read all values from Configuration properties + * [FELIX-937] - FileInstall can't handle autostart bundles that are part of watched directory + * [FELIX-938] - FileInstall starts a stopped bundles even if it is stopped transiently by user + +** Improvement + * [FELIX-920] - Add option to only install or "install and start" newly discovered bundles + * [FELIX-939] - Optimize File Install + * [FELIX-942] - Fileinstall unit tests + * [FELIX-983] - Allow property substitution in config file + * [FELIX-998] - Metatype definition for FileInstall + +Version 0.9.0 +-------------- + + * First public release diff --git a/fileinstall/pom.xml b/fileinstall/pom.xml new file mode 100644 index 00000000000..99f7f19e054 --- /dev/null +++ b/fileinstall/pom.xml @@ -0,0 +1,201 @@ + + + + org.apache.felix + felix-parent + 3 + ../pom/pom.xml + + + 4.0.0 + jar + Apache Felix File Install + A utility to automatically install bundles from a directory. + http://felix.apache.org/site/apache-felix-file-install.html + 3.6.5-SNAPSHOT + org.apache.felix.fileinstall + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/fileinstall + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/fileinstall + http://svn.apache.org/repos/asf/felix/fileinstall + + + 7 + + + + org.osgi + org.osgi.core + 4.3.1 + provided + + + org.osgi + org.osgi.compendium + 4.3.1 + provided + + + org.apache.felix + org.apache.felix.configadmin + 1.8.8 + + + org.apache.felix + org.apache.felix.utils + 1.10.5-SNAPSHOT + + + + + + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + org.apache.felix + maven-bundle-plugin + 2.1.0 + true + + + + org.apache.felix.fileinstall;version=${project.version} + + + org.apache.felix.fileinstall.internal, + org.apache.felix.utils.manifest, + org.apache.felix.utils.properties, + org.apache.felix.utils.version, + + + org.osgi.service.log;resolution:=optional, + org.osgi.service.cm;resolution:=optional, + !org.apache.felix.fileinstall, + !org.apache.felix.cm.file, + !org.apache.felix.utils.*, + * + + org.apache.felix.fileinstall.internal.FileInstall + ${pom.artifactId} + The Apache Software Foundation + + http://felix.apache.org/site/apache-felix-file-install.html + + <_versionpolicy>[$(version;==;$(@)),$(version;+;$(@))) + + org.apache.felix.utils;inline="org/apache/felix/utils/collections/DictionaryAsMap*.class" + + + + + + bundle-manifest + process-classes + + manifest + + + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + prepare-package + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.2 + + + package + + shade + + + + + org.apache.felix:org.apache.felix.configadmin + org.apache.felix:org.apache.felix.utils + + + + + org.apache.felix:org.apache.felix.configadmin + + **/ConfigurationHandler.* + + + + org.apache.felix:org.apache.felix.utils + + **/DictionaryAsMap*.* + org/apache/felix/utils/manifest/** + org/apache/felix/utils/properties/** + org/apache/felix/utils/version/** + + + + true + true + true + + + + + + org.codehaus.mojo + rat-maven-plugin + + false + true + true + + doc/* + maven-eclipse.xml + .checkstyle + .externalToolBuilders/* + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + -Xdoclint:none + + + + + diff --git a/fileinstall/src/main/appended-resources/META-INF/DEPENDENCIES b/fileinstall/src/main/appended-resources/META-INF/DEPENDENCIES new file mode 100644 index 00000000000..650265f8c3d --- /dev/null +++ b/fileinstall/src/main/appended-resources/META-INF/DEPENDENCIES @@ -0,0 +1,13 @@ +I. Included Third-Party Software + +n/a + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/ArtifactInstaller.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/ArtifactInstaller.java new file mode 100644 index 00000000000..65305aa21f7 --- /dev/null +++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/ArtifactInstaller.java @@ -0,0 +1,57 @@ +/** + * + * 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.fileinstall; + +import java.io.File; + +/** + * Objects implementing this interface are able to directly + * install and uninstall supported artifacts. Artifacts that + * are transformed into bundles should use the + * {@link ArtifactTransformer} interface instead. + * + * Note that fileinstall does not keep track of those artifacts + * across restarts, so this means that after a restart, existing + * artifacts will be reported as new, while any deleted artifact + * won't be reported as deleted. + */ +public interface ArtifactInstaller extends ArtifactListener +{ + + /** + * Install the artifact + * + * @param artifact the artifact to be installed + */ + void install(File artifact) throws Exception; + + /** + * Update the artifact + * + * @param artifact the artifact to be updated + */ + void update(File artifact) throws Exception; + + /** + * Uninstall the artifact + * + * @param artifact the artifact to be uninstalled + */ + void uninstall(File artifact) throws Exception; + +} diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/ArtifactListener.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/ArtifactListener.java new file mode 100644 index 00000000000..8e29d15ba5a --- /dev/null +++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/ArtifactListener.java @@ -0,0 +1,45 @@ +/** + * + * 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.fileinstall; + +import java.io.File; + +/** + * Interface representing a custom deployment mechanism. + * + * Classes must implement one of its sub-interface, either + * {@link ArtifactTransformer} or + * {@link ArtifactInstaller}. + * + */ +public interface ArtifactListener +{ + + /** + * Returns true if the listener can process the given artifact. + * + * Error occuring when checking the artifact should be catched + * and not be thrown. + * + * @param artifact the artifact to check + * @return true if this listener supports + * the given artifact, false otherwise + */ + boolean canHandle(File artifact); + +} diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/ArtifactTransformer.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/ArtifactTransformer.java new file mode 100644 index 00000000000..a0b11988ab6 --- /dev/null +++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/ArtifactTransformer.java @@ -0,0 +1,36 @@ +/** + * + * 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.fileinstall; + +import java.io.File; + +/** + * Objects implementing this interface are able to convert certain + * kind of artifacts to OSGi bundles. + * + */ +public interface ArtifactTransformer extends ArtifactListener +{ + + /** + * Process the given file (canHandle returned true previously) + * Can return or a pointer to a transformed file. + */ + File transform(File artifact, File tmpDir) throws Exception; + +} diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/ArtifactUrlTransformer.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/ArtifactUrlTransformer.java new file mode 100644 index 00000000000..c860918f6be --- /dev/null +++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/ArtifactUrlTransformer.java @@ -0,0 +1,38 @@ +/** + * + * 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.fileinstall; + +import java.net.URL; + +/** + * Objects implementing this interface are able to convert certain + * kind of artifacts to OSGi bundles on the fly through an URL handler. + * + * This kind of artifact listener should be favored over the {@link ArtifactTransformer} + * because it allows the use of the OSGi update feature on bundles. + */ +public interface ArtifactUrlTransformer extends ArtifactListener +{ + + /** + * Process the given file (canHandle returned true previously) + * Can return or a pointer to a transformed file. + */ + URL transform(URL artifact) throws Exception; + +} \ No newline at end of file diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Artifact.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Artifact.java new file mode 100644 index 00000000000..61af4f45607 --- /dev/null +++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Artifact.java @@ -0,0 +1,120 @@ +/* + * 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.fileinstall.internal; + +import java.io.File; +import java.net.URL; + +import org.apache.felix.fileinstall.ArtifactListener; + +/** + * An artifact that has been dropped into one watched directory. + */ +public class Artifact +{ + + private File path; + private File jaredDirectory; + private URL jaredUrl; + private ArtifactListener listener; + private URL transformedUrl; + private File transformed; + private long bundleId = -1; + private long checksum; + + public File getPath() + { + return path; + } + + public void setPath(File path) + { + this.path = path; + } + + public File getJaredDirectory() + { + return jaredDirectory; + } + + public void setJaredDirectory(File jaredDirectory) + { + this.jaredDirectory = jaredDirectory; + } + + public URL getJaredUrl() + { + return jaredUrl; + } + + public void setJaredUrl(URL jaredUrl) + { + this.jaredUrl = jaredUrl; + } + + public ArtifactListener getListener() + { + return listener; + } + + public void setListener(ArtifactListener listener) + { + this.listener = listener; + } + + public File getTransformed() + { + return transformed; + } + + public void setTransformed(File transformed) + { + this.transformed = transformed; + } + + public URL getTransformedUrl() + { + return transformedUrl; + } + + public void setTransformedUrl(URL transformedUrl) + { + this.transformedUrl = transformedUrl; + } + + public long getBundleId() + { + return bundleId; + } + + public void setBundleId(long bundleId) + { + this.bundleId = bundleId; + } + + public long getChecksum() + { + return checksum; + } + + public void setChecksum(long checksum) + { + this.checksum = checksum; + } +} diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/BundleTransformer.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/BundleTransformer.java new file mode 100644 index 00000000000..f269dea787a --- /dev/null +++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/BundleTransformer.java @@ -0,0 +1,83 @@ +/* + * 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.fileinstall.internal; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import org.apache.felix.fileinstall.ArtifactUrlTransformer; + +/** + * ArtifactUrlTransformer for plain bundles. + */ +public class BundleTransformer implements ArtifactUrlTransformer +{ + public boolean canHandle(File artifact) + { + JarFile jar = null; + try + { + // Handle OSGi bundles with the default deployer + String name = artifact.getName(); + if (!artifact.canRead() + || name.endsWith(".txt") || name.endsWith(".xml") + || name.endsWith(".properties") || name.endsWith(".cfg")) + { + // that's file type which is not supported as bundle and avoid + // exception in the log + return false; + } + jar = new JarFile(artifact); + Manifest m = jar.getManifest(); + if (m != null && m.getMainAttributes().getValue(new Attributes.Name("Bundle-SymbolicName")) != null) + { + return true; + } + } + catch (Exception e) + { + // Ignore + } + finally + { + if (jar != null) + { + try + { + jar.close(); + } + catch (IOException e) + { + // Ignore + } + } + } + return false; + } + + public URL transform(URL artifact) + { + return artifact; + } + +} diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/ConfigInstaller.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/ConfigInstaller.java new file mode 100644 index 00000000000..caee1434384 --- /dev/null +++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/ConfigInstaller.java @@ -0,0 +1,418 @@ +/* + * 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.fileinstall.internal; + +import java.io.*; +import java.net.URI; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.*; + +import org.apache.felix.fileinstall.ArtifactInstaller; +import org.apache.felix.fileinstall.ArtifactListener; +import org.apache.felix.fileinstall.internal.Util.Logger; +import org.apache.felix.utils.collections.DictionaryAsMap; +import org.apache.felix.utils.properties.InterpolationHelper; +import org.apache.felix.utils.properties.TypedProperties; +import org.osgi.framework.*; +import org.osgi.service.cm.*; + +/** + * ArtifactInstaller for configurations. + * TODO: This service lifecycle should be bound to the ConfigurationAdmin service lifecycle. + */ +public class ConfigInstaller implements ArtifactInstaller, ConfigurationListener +{ + private final BundleContext context; + private final ConfigurationAdmin configAdmin; + private final FileInstall fileInstall; + private final Map pidToFile = new HashMap<>(); + private ServiceRegistration registration; + + ConfigInstaller(BundleContext context, ConfigurationAdmin configAdmin, FileInstall fileInstall) + { + this.context = context; + this.configAdmin = configAdmin; + this.fileInstall = fileInstall; + } + + public void init() + { + registration = this.context.registerService( + new String[] { + ConfigurationListener.class.getName(), + ArtifactListener.class.getName(), + ArtifactInstaller.class.getName() + }, + this, null); + try + { + Configuration[] configs = configAdmin.listConfigurations(null); + if (configs != null) + { + for (Configuration config : configs) + { + Dictionary dict = config.getProperties(); + String fileName = dict != null ? (String) dict.get( DirectoryWatcher.FILENAME ) : null; + if (fileName != null) + { + pidToFile.put(config.getPid(), fileName); + } + } + } + } + catch (Exception e) + { + Util.log( context, Logger.LOG_INFO, "Unable to initialize configurations list", e ); + } + } + + public void destroy() + { + registration.unregister(); + } + + public boolean canHandle(File artifact) + { + return artifact.getName().endsWith(".cfg") + || artifact.getName().endsWith(".config"); + } + + public void install(File artifact) throws Exception + { + setConfig(artifact); + } + + public void update(File artifact) throws Exception + { + setConfig(artifact); + } + + public void uninstall(File artifact) throws Exception + { + deleteConfig(artifact); + } + + public void configurationEvent(final ConfigurationEvent configurationEvent) + { + if (System.getSecurityManager() != null) + { + AccessController.doPrivileged( + new PrivilegedAction() + { + public Void run() + { + doConfigurationEvent(configurationEvent); + return null; + } + } + ); + } + else + { + doConfigurationEvent(configurationEvent); + } + } + + public void doConfigurationEvent(ConfigurationEvent configurationEvent) + { + // Check if writing back configurations has been disabled. + { + if (!shouldSaveConfig()) + { + return; + } + } + + if (configurationEvent.getType() == ConfigurationEvent.CM_UPDATED) + { + try + { + Configuration config = getConfigurationAdmin().getConfiguration( + configurationEvent.getPid(), + "?"); + Dictionary dict = config.getProperties(); + String fileName = dict != null ? (String) dict.get( DirectoryWatcher.FILENAME ) : null; + File file = fileName != null ? fromConfigKey(fileName) : null; + if( file != null && file.isFile() && canHandle( file ) ) { + pidToFile.put(config.getPid(), fileName); + TypedProperties props = new TypedProperties( bundleSubstitution() ); + try (Reader r = new InputStreamReader(new FileInputStream(file), encoding())) + { + props.load(r); + } + // remove "removed" properties from the cfg file + List propertiesToRemove = new ArrayList<>(); + for( String key : props.keySet() ) + { + if( dict.get(key) == null + && !Constants.SERVICE_PID.equals(key) + && !ConfigurationAdmin.SERVICE_FACTORYPID.equals(key) + && !DirectoryWatcher.FILENAME.equals(key) ) { + propertiesToRemove.add(key); + } + } + for( Enumeration e = dict.keys(); e.hasMoreElements(); ) + { + String key = e.nextElement().toString(); + if( !Constants.SERVICE_PID.equals(key) + && !ConfigurationAdmin.SERVICE_FACTORYPID.equals(key) + && !DirectoryWatcher.FILENAME.equals(key) ) + { + Object val = dict.get( key ); + props.put( key, val ); + } + } + for( String key : propertiesToRemove ) + { + props.remove(key); + } + try (Writer fw = new OutputStreamWriter(new FileOutputStream(file), encoding())) + { + props.save( fw ); + } + // we're just writing out what's already loaded into ConfigAdmin, so + // update file checksum since lastModified gets updated when writing + fileInstall.updateChecksum(file); + } + } + catch (Exception e) + { + Util.log( context, Logger.LOG_INFO, "Unable to save configuration", e ); + } + } + + if (configurationEvent.getType() == ConfigurationEvent.CM_DELETED) + { + try { + String fileName = pidToFile.remove(configurationEvent.getPid()); + File file = fileName != null ? fromConfigKey(fileName) : null; + if (file != null && file.isFile()) { + if (!file.delete()) { + throw new IOException("Unable to delete file: " + file); + } + } + } + catch (Exception e) + { + Util.log( context, Logger.LOG_INFO, "Unable to delete configuration file", e ); + } + } + } + + boolean shouldSaveConfig() + { + String str = this.context.getProperty( DirectoryWatcher.ENABLE_CONFIG_SAVE ); + if (str == null) + { + str = this.context.getProperty( DirectoryWatcher.DISABLE_CONFIG_SAVE ); + } + if (str != null) + { + return Boolean.valueOf(str); + } + return true; + } + + String encoding() + { + String str = this.context.getProperty( DirectoryWatcher.CONFIG_ENCODING ); + return str != null ? str : "ISO-8859-1"; + } + + ConfigurationAdmin getConfigurationAdmin() + { + return configAdmin; + } + + /** + * Set the configuration based on the config file. + * + * @param f + * Configuration file + * @return true if the configuration has been updated + * @throws Exception + */ + boolean setConfig(final File f) throws Exception + { + final Hashtable ht = new Hashtable<>(); + final InputStream in = new BufferedInputStream(new FileInputStream(f)); + try + { + in.mark(1); + boolean isXml = in.read() == '<'; + in.reset(); + if (isXml) { + final Properties p = new Properties(); + p.loadFromXML(in); + Map strMap = new HashMap<>(); + for (Object k : p.keySet()) { + strMap.put(k.toString(), p.getProperty(k.toString())); + } + InterpolationHelper.performSubstitution(strMap, context); + ht.putAll(strMap); + } else { + TypedProperties p = new TypedProperties( bundleSubstitution() ); + try (Reader r = new InputStreamReader(in, encoding())) + { + p.load(r); + } + for (String k : p.keySet()) { + ht.put(k, p.get(k)); + } + } + } + finally + { + in.close(); + } + + String pid[] = parsePid(f.getName()); + Configuration config = getConfiguration(toConfigKey(f), pid[0], pid[1]); + + Dictionary props = config.getProperties(); + Hashtable old = props != null ? new Hashtable(new DictionaryAsMap<>(props)) : null; + if (old != null) { + old.remove( DirectoryWatcher.FILENAME ); + old.remove( Constants.SERVICE_PID ); + old.remove( ConfigurationAdmin.SERVICE_FACTORYPID ); + } + + if( !ht.equals( old ) ) + { + ht.put(DirectoryWatcher.FILENAME, toConfigKey(f)); + if (old == null) { + Util.log(context, Logger.LOG_INFO, "Creating configuration from " + pid[0] + + (pid[1] == null ? "" : "-" + pid[1]) + ".cfg", null); + } else { + Util.log(context, Logger.LOG_INFO, "Updating configuration from " + pid[0] + + (pid[1] == null ? "" : "-" + pid[1]) + ".cfg", null); + } + config.update(ht); + return true; + } + else + { + return false; + } + } + + /** + * Remove the configuration. + * + * @param f + * File where the configuration in was defined. + * @return true + * @throws Exception + */ + boolean deleteConfig(File f) throws Exception + { + String pid[] = parsePid(f.getName()); + Util.log(context, Logger.LOG_INFO, "Deleting configuration from " + pid[0] + + (pid[1] == null ? "" : "-" + pid[1]) + ".cfg", null); + Configuration config = getConfiguration(toConfigKey(f), pid[0], pid[1]); + config.delete(); + return true; + } + + String toConfigKey(File f) { + return f.getAbsoluteFile().toURI().toString(); + } + + File fromConfigKey(String key) { + return new File(URI.create(key)); + } + + String[] parsePid(String path) + { + String pid = path.substring(0, path.lastIndexOf('.')); + int n = pid.indexOf('-'); + if (n > 0) + { + String factoryPid = pid.substring(n + 1); + pid = pid.substring(0, n); + return new String[] + { + pid, factoryPid + }; + } + else + { + return new String[] + { + pid, null + }; + } + } + + Configuration getConfiguration(String fileName, String pid, String factoryPid) + throws Exception + { + Configuration oldConfiguration = findExistingConfiguration(fileName); + if (oldConfiguration != null) + { + return oldConfiguration; + } + else + { + Configuration newConfiguration; + if (factoryPid != null) + { + newConfiguration = getConfigurationAdmin().createFactoryConfiguration(pid, "?"); + } + else + { + newConfiguration = getConfigurationAdmin().getConfiguration(pid, "?"); + } + return newConfiguration; + } + } + + Configuration findExistingConfiguration(String fileName) throws Exception + { + String filter = "(" + DirectoryWatcher.FILENAME + "=" + escapeFilterValue(fileName) + ")"; + Configuration[] configurations = getConfigurationAdmin().listConfigurations(filter); + if (configurations != null && configurations.length > 0) + { + return configurations[0]; + } + else + { + return null; + } + } + + TypedProperties.SubstitutionCallback bundleSubstitution() { + final InterpolationHelper.SubstitutionCallback cb = new InterpolationHelper.BundleContextSubstitutionCallback(context); + return new TypedProperties.SubstitutionCallback() { + @Override + public String getValue(String name, String key, String value) { + return cb.getValue(value); + } + }; + } + + private String escapeFilterValue(String s) { + return s.replaceAll("[(]", "\\\\("). + replaceAll("[)]", "\\\\)"). + replaceAll("[=]", "\\\\="). + replaceAll("[\\*]", "\\\\*"); + } + +} diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/DirectoryWatcher.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/DirectoryWatcher.java new file mode 100644 index 00000000000..629e48dca6e --- /dev/null +++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/DirectoryWatcher.java @@ -0,0 +1,1498 @@ +/* + * 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.fileinstall.internal; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; +import java.util.regex.Pattern; + +import org.apache.felix.fileinstall.ArtifactInstaller; +import org.apache.felix.fileinstall.ArtifactListener; +import org.apache.felix.fileinstall.ArtifactTransformer; +import org.apache.felix.fileinstall.ArtifactUrlTransformer; +import org.apache.felix.fileinstall.internal.Util.Logger; +import org.apache.felix.utils.manifest.Clause; +import org.apache.felix.utils.manifest.Parser; +import org.apache.felix.utils.version.VersionRange; +import org.apache.felix.utils.version.VersionTable; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.BundleException; +import org.osgi.framework.BundleListener; +import org.osgi.framework.Constants; +import org.osgi.framework.Version; +import org.osgi.framework.startlevel.BundleStartLevel; +import org.osgi.framework.startlevel.FrameworkStartLevel; +import org.osgi.framework.wiring.BundleRevision; + +/** + * -DirectoryWatcher- + * + * This class runs a background task that checks a directory for new files or + * removed files. These files can be configuration files or jars. + * For jar files, its behavior is defined below: + * - If there are new jar files, it installs them and optionally starts them. + * - If it fails to install a jar, it does not try to install it again until + * the jar has been modified. + * - If it fail to start a bundle, it attempts to start it in following + * iterations until it succeeds or the corresponding jar is uninstalled. + * - If some jar files have been deleted, it uninstalls them. + * - If some jar files have been updated, it updates them. + * - If it fails to update a bundle, it tries to update it in following + * iterations until it is successful. + * - If any bundle gets updated or uninstalled, it refreshes the framework + * for the changes to take effect. + * - If it detects any new installations, uninstallations or updations, + * it tries to start all the managed bundle unless it has been configured + * to only install bundles. + * + * @author Felix Project Team + */ +public class DirectoryWatcher extends Thread implements BundleListener +{ + public final static String FILENAME = "felix.fileinstall.filename"; + public final static String POLL = "felix.fileinstall.poll"; + public final static String DIR = "felix.fileinstall.dir"; + public final static String LOG_LEVEL = "felix.fileinstall.log.level"; + public final static String LOG_DEFAULT = "felix.fileinstall.log.default"; + public final static String TMPDIR = "felix.fileinstall.tmpdir"; + public final static String FILTER = "felix.fileinstall.filter"; + public final static String START_NEW_BUNDLES = "felix.fileinstall.bundles.new.start"; + public final static String USE_START_TRANSIENT = "felix.fileinstall.bundles.startTransient"; + public final static String USE_START_ACTIVATION_POLICY = "felix.fileinstall.bundles.startActivationPolicy"; + public final static String NO_INITIAL_DELAY = "felix.fileinstall.noInitialDelay"; + public final static String DISABLE_CONFIG_SAVE = "felix.fileinstall.disableConfigSave"; + public final static String ENABLE_CONFIG_SAVE = "felix.fileinstall.enableConfigSave"; + public final static String CONFIG_ENCODING = "felix.fileinstall.configEncoding"; + public final static String START_LEVEL = "felix.fileinstall.start.level"; + public final static String ACTIVE_LEVEL = "felix.fileinstall.active.level"; + public final static String UPDATE_WITH_LISTENERS = "felix.fileinstall.bundles.updateWithListeners"; + public final static String OPTIONAL_SCOPE = "felix.fileinstall.optionalImportRefreshScope"; + public final static String FRAGMENT_SCOPE = "felix.fileinstall.fragmentRefreshScope"; + public final static String DISABLE_NIO2 = "felix.fileinstall.disableNio2"; + public final static String SUBDIR_MODE = "felix.fileinstall.subdir.mode"; + + public final static String SCOPE_NONE = "none"; + public final static String SCOPE_MANAGED = "managed"; + public final static String SCOPE_ALL = "all"; + + public final static String LOG_STDOUT = "stdout"; + public final static String LOG_JUL = "jul"; + + final FileInstall fileInstall; + + Map properties; + File watchedDirectory; + File tmpDir; + long poll; + int logLevel; + boolean startBundles; + boolean useStartTransient; + boolean useStartActivationPolicy; + String filter; + BundleContext context; + private Bundle systemBundle; + String originatingFileName; + boolean noInitialDelay; + int startLevel; + int activeLevel; + boolean updateWithListeners; + String fragmentScope; + String optionalScope; + boolean disableNio2; + int frameworkStartLevel; + + // Map of all installed artifacts + final Map currentManagedArtifacts = new HashMap(); + + // The scanner to report files changes + Scanner scanner; + + // Represents files that could not be processed because of a missing artifact listener + final Set processingFailures = new HashSet(); + + // Represents installed artifacts which need to be started later because they failed to start + Set delayedStart = new HashSet(); + + // Represents consistently failing bundles + Set consistentlyFailingBundles = new HashSet(); + + // Represents artifacts that could not be installed + final Map installationFailures = new HashMap(); + + // flag (acces to which must be synchronized) that indicates wheter there's a change in state of system, + // which may result in an attempt to start the watched bundles + private AtomicBoolean stateChanged = new AtomicBoolean(); + + public DirectoryWatcher(FileInstall fileInstall, Map properties, BundleContext context) + { + super("fileinstall-" + getThreadName(properties)); + this.fileInstall = fileInstall; + this.properties = properties; + this.context = context; + systemBundle = context.getBundle(Constants.SYSTEM_BUNDLE_LOCATION); + poll = getLong(properties, POLL, 2000); + logLevel = getInt(properties, LOG_LEVEL, Util.getGlobalLogLevel(context)); + originatingFileName = properties.get(FILENAME); + watchedDirectory = getFile(properties, DIR, new File("./load")); + verifyWatchedDir(); + tmpDir = getFile(properties, TMPDIR, null); + prepareTempDir(); + startBundles = getBoolean(properties, START_NEW_BUNDLES, true); // by default, we start bundles. + useStartTransient = getBoolean(properties, USE_START_TRANSIENT, false); // by default, we start bundles persistently. + useStartActivationPolicy = getBoolean(properties, USE_START_ACTIVATION_POLICY, true); // by default, we start bundles using activation policy. + filter = properties.get(FILTER); + noInitialDelay = getBoolean(properties, NO_INITIAL_DELAY, false); + startLevel = getInt(properties, START_LEVEL, 0); // by default, do not touch start level + activeLevel = getInt(properties, ACTIVE_LEVEL, 0); // by default, always scan + updateWithListeners = getBoolean(properties, UPDATE_WITH_LISTENERS, false); // Do not update bundles when listeners are updated + fragmentScope = properties.get(FRAGMENT_SCOPE); + optionalScope = properties.get(OPTIONAL_SCOPE); + disableNio2 = getBoolean(properties, DISABLE_NIO2, false); + this.context.addBundleListener(this); + + if (disableNio2) { + scanner = new Scanner(watchedDirectory, filter, properties.get(SUBDIR_MODE)); + } else { + try { + scanner = new WatcherScanner(context, watchedDirectory, filter, properties.get(SUBDIR_MODE)); + } catch (Throwable t) { + scanner = new Scanner(watchedDirectory, filter, properties.get(SUBDIR_MODE)); + } + } + } + + private void verifyWatchedDir() + { + if (!watchedDirectory.exists()) + { + // Issue #2069: Do not create the directory if it does not exist, + // instead, warn user and continue. We will automatically start + // monitoring the dir when it becomes available. + log(Logger.LOG_WARNING, + watchedDirectory + " does not exist, please create it.", + null); + } + else if (!watchedDirectory.isDirectory()) + { + log(Logger.LOG_ERROR, + "Cannot use " + + watchedDirectory + + " because it's not a directory", null); + throw new RuntimeException( + "File Install can't monitor " + watchedDirectory + " because it is not a directory"); + } + } + + public static String getThreadName(Map properties) + { + return (properties.get(DIR) != null ? properties.get(DIR) : "./load"); + } + + public Map getProperties() + { + return properties; + } + + public void start() + { + if (noInitialDelay) + { + log(Logger.LOG_DEBUG, "Starting initial scan", null); + initializeCurrentManagedBundles(); + Set files = scanner.scan(true); + if (files != null) + { + try + { + process(files); + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + } + } + super.start(); + } + + /** + * Main run loop, will traverse the directory, and then handle the delta + * between installed and newly found/lost bundles and configurations. + * + */ + public void run() + { + // We must wait for FileInstall to complete initialisation + // to avoid race conditions observed in FELIX-2791 + try + { + fileInstall.lock.readLock().lockInterruptibly(); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + log(Logger.LOG_INFO, "Watcher for " + watchedDirectory + " exiting because of interruption.", e); + return; + } + try { + log(Logger.LOG_DEBUG, + "{" + POLL + " (ms) = " + poll + ", " + + DIR + " = " + watchedDirectory.getAbsolutePath() + ", " + + LOG_LEVEL + " = " + logLevel + ", " + + START_NEW_BUNDLES + " = " + startBundles + ", " + + TMPDIR + " = " + tmpDir + ", " + + FILTER + " = " + filter + ", " + + START_LEVEL + " = " + startLevel + "}", null + ); + + if (!noInitialDelay) { + try { + // enforce a delay before the first directory scan + Thread.sleep(poll); + } catch (InterruptedException e) { + log(Logger.LOG_DEBUG, "Watcher for " + watchedDirectory + " was interrupted while waiting " + + poll + " milliseconds for initial directory scan.", e); + return; + } + initializeCurrentManagedBundles(); + } + } + finally + { + fileInstall.lock.readLock().unlock(); + } + + while (!interrupted()) { + try { + FrameworkStartLevel startLevelSvc = systemBundle.adapt(FrameworkStartLevel.class); + // Don't access the disk when the framework is still in a startup phase. + if (startLevelSvc.getStartLevel() >= activeLevel + && systemBundle.getState() == Bundle.ACTIVE) { + Set files = scanner.scan(false); + // Check that there is a result. If not, this means that the directory can not be listed, + // so it's presumably not a valid directory (it may have been deleted by someone). + // In such case, just sleep + if (files != null) { + process(files); + } + } + synchronized (this) { + wait(poll); + } + } catch (InterruptedException e) { + interrupt(); + return; + } catch (Throwable e) { + try { + context.getBundle(); + } catch (IllegalStateException t) { + // FileInstall bundle has been uninstalled, exiting loop + return; + } + log(Logger.LOG_ERROR, "In main loop, we have serious trouble", e); + } + } + } + + public void bundleChanged(BundleEvent bundleEvent) + { + int type = bundleEvent.getType(); + if (type == BundleEvent.UNINSTALLED) + { + for (Iterator it = getArtifacts().iterator(); it.hasNext();) + { + Artifact artifact = (Artifact) it.next(); + if (artifact.getBundleId() == bundleEvent.getBundle().getBundleId()) + { + log(Logger.LOG_DEBUG, "Bundle " + bundleEvent.getBundle().getBundleId() + + " has been uninstalled", null); + it.remove(); + break; + } + } + } + if (type == BundleEvent.INSTALLED || type == BundleEvent.RESOLVED || type == BundleEvent.UNINSTALLED || + type == BundleEvent.UNRESOLVED || type == BundleEvent.UPDATED) { + setStateChanged(true); + } + } + + private void process(Set files) throws InterruptedException + { + fileInstall.lock.readLock().lockInterruptibly(); + try + { + doProcess(files); + } + finally + { + fileInstall.lock.readLock().unlock(); + } + } + + private void doProcess(Set files) throws InterruptedException + { + List listeners = fileInstall.getListeners(); + List deleted = new ArrayList(); + List modified = new ArrayList(); + List created = new ArrayList(); + + // Try to process again files that could not be processed + synchronized (processingFailures) + { + files.addAll(processingFailures); + processingFailures.clear(); + } + + for (File file : files) { + boolean exists = file.exists(); + Artifact artifact = getArtifact(file); + // File has been deleted + if (!exists) { + if (artifact != null) { + deleteJaredDirectory(artifact); + deleteTransformedFile(artifact); + deleted.add(artifact); + } + } + // File exists + else { + File jar = file; + URL jaredUrl = null; + try { + jaredUrl = file.toURI().toURL(); + } catch (MalformedURLException e) { + // Ignore, can't happen + } + // Jar up the directory if needed + if (file.isDirectory()) { + prepareTempDir(); + try { + jar = new File(tmpDir, file.getName() + ".jar"); + Util.jarDir(file, jar); + jaredUrl = new URL(JarDirUrlHandler.PROTOCOL, null, file.getPath()); + + } catch (IOException e) { + // Notify user of problem, won't retry until the dir is updated. + log(Logger.LOG_ERROR, + "Unable to create jar for: " + file.getAbsolutePath(), e); + continue; + } + } + // File has been modified + if (artifact != null) { + artifact.setChecksum(scanner.getChecksum(file)); + // If there's no listener, this is because this artifact has been installed before + // fileinstall has been restarted. In this case, try to find a listener. + if (artifact.getListener() == null) { + ArtifactListener listener = findListener(jar, listeners); + // If no listener can handle this artifact, we need to defer the + // processing for this artifact until one is found + if (listener == null) { + synchronized (processingFailures) { + processingFailures.add(file); + } + continue; + } + artifact.setListener(listener); + } + // If the listener can not handle this file anymore, + // uninstall the artifact and try as if is was new + if (!listeners.contains(artifact.getListener()) || !artifact.getListener().canHandle(jar)) { + deleted.add(artifact); + } + // The listener is still ok + else { + deleteTransformedFile(artifact); + artifact.setJaredDirectory(jar); + artifact.setJaredUrl(jaredUrl); + if (transformArtifact(artifact)) { + modified.add(artifact); + } else { + deleteJaredDirectory(artifact); + deleted.add(artifact); + } + } + } + // File has been added + else { + // Find the listener + ArtifactListener listener = findListener(jar, listeners); + // If no listener can handle this artifact, we need to defer the + // processing for this artifact until one is found + if (listener == null) { + synchronized (processingFailures) { + processingFailures.add(file); + } + continue; + } + // Create the artifact + artifact = new Artifact(); + artifact.setPath(file); + artifact.setJaredDirectory(jar); + artifact.setJaredUrl(jaredUrl); + artifact.setListener(listener); + artifact.setChecksum(scanner.getChecksum(file)); + if (transformArtifact(artifact)) { + created.add(artifact); + } else { + deleteJaredDirectory(artifact); + } + } + } + } + // Handle deleted artifacts + // We do the operations in the following order: + // uninstall, update, install, refresh & start. + Collection uninstalledBundles = uninstall(deleted); + Collection updatedBundles = update(modified); + Collection installedBundles = install(created); + + if (!uninstalledBundles.isEmpty() || !updatedBundles.isEmpty() || !installedBundles.isEmpty()) + { + Set toRefresh = new HashSet(); + toRefresh.addAll(uninstalledBundles); + toRefresh.addAll(updatedBundles); + toRefresh.addAll(installedBundles); + findBundlesWithFragmentsToRefresh(toRefresh); + findBundlesWithOptionalPackagesToRefresh(toRefresh); + if (toRefresh.size() > 0) + { + // Refresh if any bundle got uninstalled or updated. + refresh(toRefresh); + // set the state to reattempt starting managed bundles which aren't already STARTING or ACTIVE + setStateChanged(true); + } + } + + if (startBundles) { + int startLevel = systemBundle.adapt(FrameworkStartLevel.class).getStartLevel(); + boolean doStart = isStateChanged() || startLevel != frameworkStartLevel; + frameworkStartLevel = startLevel; + if (doStart) + { + // Try to start all the bundles that are not persistently stopped + startAllBundles(); + + delayedStart.addAll(installedBundles); + delayedStart.removeAll(uninstalledBundles); + // Try to start newly installed bundles, or bundles which we missed on a previous round + startBundles(delayedStart); + consistentlyFailingBundles.clear(); + consistentlyFailingBundles.addAll(delayedStart); + + // set the state as unchanged to not reattempt starting failed bundles + setStateChanged(false); + } + } + } + + ArtifactListener findListener(File artifact, List listeners) + { + for (ArtifactListener listener : listeners) { + if (listener.canHandle(artifact)) { + return listener; + } + } + return null; + } + + boolean transformArtifact(Artifact artifact) + { + if (artifact.getListener() instanceof ArtifactTransformer) + { + prepareTempDir(); + try + { + File transformed = ((ArtifactTransformer) artifact.getListener()).transform(artifact.getJaredDirectory(), tmpDir); + if (transformed != null) + { + artifact.setTransformed(transformed); + return true; + } + } + catch (Exception e) + { + log(Logger.LOG_WARNING, + "Unable to transform artifact: " + artifact.getPath().getAbsolutePath(), e); + } + return false; + } + else if (artifact.getListener() instanceof ArtifactUrlTransformer) + { + try + { + URL url = artifact.getJaredUrl(); + URL transformed = ((ArtifactUrlTransformer) artifact.getListener()).transform(url); + if (transformed != null) + { + artifact.setTransformedUrl(transformed); + return true; + } + } + catch (Exception e) + { + log(Logger.LOG_WARNING, + "Unable to transform artifact: " + artifact.getPath().getAbsolutePath(), e); + } + return false; + } + return true; + } + + private void deleteTransformedFile(Artifact artifact) + { + if (artifact.getTransformed() != null + && !artifact.getTransformed().equals(artifact.getPath()) + && !artifact.getTransformed().delete()) + { + log(Logger.LOG_WARNING, + "Unable to delete transformed artifact: " + artifact.getTransformed().getAbsolutePath(), null); + } + } + + private void deleteJaredDirectory(Artifact artifact) + { + if (artifact.getJaredDirectory() != null + && !artifact.getJaredDirectory().equals(artifact.getPath()) + && !artifact.getJaredDirectory().delete()) + { + log(Logger.LOG_WARNING, + "Unable to delete jared artifact: " + artifact.getJaredDirectory().getAbsolutePath(), null); + } + } + + + private void prepareTempDir() + { + if (tmpDir == null) + { + File javaIoTmpdir = new File(System.getProperty("java.io.tmpdir")); + if (!javaIoTmpdir.exists() && !javaIoTmpdir.mkdirs()) { + throw new IllegalStateException("Unable to create temporary directory " + javaIoTmpdir); + } + Random random = new Random(); + while (tmpDir == null) + { + File f = new File(javaIoTmpdir, "fileinstall-" + Long.toString(random.nextLong())); + if (!f.exists() && f.mkdirs()) + { + tmpDir = f; + tmpDir.deleteOnExit(); + } + } + } + else + { + prepareDir(tmpDir); + } + } + + /** + * Create the watched directory, if not existing. + * Throws a runtime exception if the directory cannot be created, + * or if the provided File parameter does not refer to a directory. + * + * @param dir + * The directory File Install will monitor + */ + private void prepareDir(File dir) + { + if (!dir.exists() && !dir.mkdirs()) + { + log(Logger.LOG_ERROR, + "Cannot create folder " + + dir + + ". Is the folder write-protected?", null); + throw new RuntimeException("Cannot create folder: " + dir); + } + + if (!dir.isDirectory()) + { + log(Logger.LOG_ERROR, + "Cannot use " + + dir + + " because it's not a directory", null); + throw new RuntimeException( + "Cannot start FileInstall using something that is not a directory"); + } + } + + /** + * Log a message and optional throwable. If there is a log service we use + * it, otherwise we log to the console + * + * @param message + * The message to log + * @param e + * The throwable to log + */ + void log(int msgLevel, String message, Throwable e) + { + Util.log(context, logLevel, msgLevel, message, e); + } + + /** + * Check if a bundle is a fragment. + */ + boolean isFragment(Bundle bundle) + { + BundleRevision rev = bundle.adapt(BundleRevision.class); + return (rev.getTypes() & BundleRevision.TYPE_FRAGMENT) != 0; + } + + /** + * Convenience to refresh the packages + */ + void refresh(Collection bundles) throws InterruptedException + { + FileInstall.refresh(systemBundle, bundles); + } + + /** + * Retrieve a property as a long. + * + * @param properties the properties to retrieve the value from + * @param property the name of the property to retrieve + * @param dflt the default value + * @return the property as a long or the default value + */ + int getInt(Map properties, String property, int dflt) + { + String value = properties.get(property); + if (value != null) + { + try + { + return Integer.parseInt(value); + } + catch (Exception e) + { + log(Logger.LOG_WARNING, property + " set, but not a int: " + value, null); + } + } + return dflt; + } + + /** + * Retrieve a property as a long. + * + * @param properties the properties to retrieve the value from + * @param property the name of the property to retrieve + * @param dflt the default value + * @return the property as a long or the default value + */ + long getLong(Map properties, String property, long dflt) + { + String value = properties.get(property); + if (value != null) + { + try + { + return Long.parseLong(value); + } + catch (Exception e) + { + log(Logger.LOG_WARNING, property + " set, but not a long: " + value, null); + } + } + return dflt; + } + + /** + * Retrieve a property as a File. + * + * @param properties the properties to retrieve the value from + * @param property the name of the property to retrieve + * @param dflt the default value + * @return the property as a File or the default value + */ + File getFile(Map properties, String property, File dflt) + { + String value = properties.get(property); + if (value != null) + { + return new File(value); + } + return dflt; + } + + /** + * Retrieve a property as a boolan. + * + * @param properties the properties to retrieve the value from + * @param property the name of the property to retrieve + * @param dflt the default value + * @return the property as a boolean or the default value + */ + boolean getBoolean(Map properties, String property, boolean dflt) + { + String value = properties.get(property); + if (value != null) + { + return Boolean.valueOf(value); + } + return dflt; + } + + public void close() + { + this.context.removeBundleListener(this); + interrupt(); + for (Artifact artifact : getArtifacts()) { + deleteTransformedFile(artifact); + deleteJaredDirectory(artifact); + } + try + { + scanner.close(); + } + catch (IOException e) + { + // Ignore + } + try + { + join(10000); + } + catch (InterruptedException ie) + { + // Ignore + } + } + + /** + * This method goes through all the currently installed bundles + * and returns information about those bundles whose location + * refers to a file in our {@link #watchedDirectory}. + */ + private void initializeCurrentManagedBundles() + { + Bundle[] bundles = this.context.getBundles(); + String watchedDirPath = watchedDirectory.toURI().normalize().getPath(); + Map checksums = new HashMap(); + Pattern filePattern = filter == null || filter.isEmpty() ? null : Pattern.compile(filter); + for (Bundle bundle : bundles) { + // Convert to a URI because the location of a bundle + // is typically a URI. At least, that's the case for + // autostart bundles and bundles installed by fileinstall. + // Normalisation is needed to ensure that we don't treat (e.g.) + // /tmp/foo and /tmp//foo differently. + String location = bundle.getLocation(); + String path = null; + if (location != null && + !location.equals(Constants.SYSTEM_BUNDLE_LOCATION)) { + URI uri; + try { + uri = new URI(bundle.getLocation()).normalize(); + } catch (URISyntaxException e) { + // Let's try to interpret the location as a file path + uri = new File(location).toURI().normalize(); + } + if (uri.isOpaque() && uri.getSchemeSpecificPart() != null) { + // blueprint:file:/tmp/foo/baa.jar -> file:/tmp/foo/baa.jar + // blueprint:mvn:foo.baa/baa/0.0.1 -> mvn:foo.baa/baa/0.0.1 + // wrap:file:/tmp/foo/baa-1.0.jar$Symbolic-Name=baa&Version=1.0 -> file:/tmp/foo/baa-1.0.jar$Symbolic-Name=baa&Version1.0 + final String schemeSpecificPart = uri.getSchemeSpecificPart(); + // extract content behind the 'file:' protocol of scheme specific path + final int lastIndexOfFileProtocol = schemeSpecificPart.lastIndexOf("file:"); + final int offsetFileProtocol = lastIndexOfFileProtocol >= 0 ? lastIndexOfFileProtocol + "file:".length() : 0; + final int firstIndexOfDollar = schemeSpecificPart.indexOf("$"); + final int endOfPath = firstIndexOfDollar >= 0 ? firstIndexOfDollar : schemeSpecificPart.length(); + // file:/tmp/foo/baa.jar -> /tmp/foo/baa.jar + // mvn:foo.baa/baa/0.0.1 -> mvn:foo.baa/baa/0.0.1 + // file:/tmp/foo/baa-1.0.jar$Symbolic-Name=baa&Version=1.0 -> /tmp/foo/baa-1.0.jar + path = schemeSpecificPart.substring(offsetFileProtocol, endOfPath); + } else { + // file:/tmp/foo/baa.jar -> /tmp/foo/baa.jar + // mnv:foo.baa/baa/0.0.1 -> foo.baa/baa/0.0.1 + path = uri.getPath(); + } + } + if (path == null) { + // jar.getPath is null means we could not parse the location + // as a meaningful URI or file path. + // We can't do any meaningful processing for this bundle. + continue; + } + final int index = path.lastIndexOf('/'); + if (index != -1 && path.startsWith(watchedDirPath)) { + final String fileName = path.substring(index + 1); + if (filePattern == null || filePattern.matcher(fileName).matches()) { + Artifact artifact = new Artifact(); + artifact.setBundleId(bundle.getBundleId()); + artifact.setChecksum(Util.loadChecksum(bundle, context)); + artifact.setListener(null); + artifact.setPath(new File(path)); + setArtifact(new File(path), artifact); + checksums.put(new File(path), artifact.getChecksum()); + } + } + } + scanner.initialize(checksums); + } + + /** + * This method installs a collection of artifacts. + * @param artifacts Collection of {@link Artifact}s to be installed + * @return List of Bundles just installed + */ + private Collection install(Collection artifacts) + { + List bundles = new ArrayList(); + for (Artifact artifact : artifacts) { + Bundle bundle = install(artifact); + if (bundle != null) { + bundles.add(bundle); + } + } + return bundles; + } + + /** + * This method uninstalls a collection of artifacts. + * @param artifacts Collection of {@link Artifact}s to be uninstalled + * @return Collection of Bundles that got uninstalled + */ + private Collection uninstall(Collection artifacts) + { + List bundles = new ArrayList(); + for (Artifact artifact : artifacts) { + Bundle bundle = uninstall(artifact); + if (bundle != null) { + bundles.add(bundle); + } + } + return bundles; + } + + /** + * This method updates a collection of artifacts. + * + * @param artifacts Collection of {@link Artifact}s to be updated. + * @return Collection of bundles that got updated + */ + private Collection update(Collection artifacts) + { + List bundles = new ArrayList(); + for (Artifact artifact : artifacts) { + Bundle bundle = update(artifact); + if (bundle != null) { + bundles.add(bundle); + } + } + return bundles; + } + + /** + * Install an artifact and return the bundle object. + * It uses {@link Artifact#getPath()} as location + * of the new bundle. Before installing a file, + * it sees if the file has been identified as a bad file in + * earlier run. If yes, then it compares to see if the file has changed + * since then. It installs the file if the file has changed. + * If the file has not been identified as a bad file in earlier run, + * then it always installs it. + * + * @param artifact the artifact to be installed + * @return Bundle object that was installed + */ + private Bundle install(Artifact artifact) + { + File path = artifact.getPath(); + Bundle bundle = null; + AtomicBoolean modified = new AtomicBoolean(); + try + { + // If the listener is an installer, ask for an update + if (artifact.getListener() instanceof ArtifactInstaller) + { + ((ArtifactInstaller) artifact.getListener()).install(path); + } + // if the listener is an url transformer + else if (artifact.getListener() instanceof ArtifactUrlTransformer) + { + Artifact badArtifact = installationFailures.get(path); + if (badArtifact != null && badArtifact.getChecksum() == artifact.getChecksum()) + { + return null; // Don't attempt to install it; nothing has changed. + } + URL transformed = artifact.getTransformedUrl(); + String location = transformed.toString(); + BufferedInputStream in = new BufferedInputStream(transformed.openStream()); + bundle = installOrUpdateBundle(location, in, artifact.getChecksum(), modified); + artifact.setBundleId(bundle.getBundleId()); + } + // if the listener is an artifact transformer + else if (artifact.getListener() instanceof ArtifactTransformer) + { + Artifact badArtifact = installationFailures.get(path); + if (badArtifact != null && badArtifact.getChecksum() == artifact.getChecksum()) + { + return null; // Don't attempt to install it; nothing has changed. + } + File transformed = artifact.getTransformed(); + String location = path.toURI().normalize().toString(); + BufferedInputStream in = new BufferedInputStream(new FileInputStream(transformed != null ? transformed : path)); + bundle = installOrUpdateBundle(location, in, artifact.getChecksum(), modified); + artifact.setBundleId(bundle.getBundleId()); + } + installationFailures.remove(path); + setArtifact(path, artifact); + } + catch (Exception e) + { + log(Logger.LOG_ERROR, "Failed to install artifact: " + path, e); + + // Add it our bad jars list, so that we don't + // attempt to install it again and again until the underlying + // jar has been modified. + installationFailures.put(path, artifact); + } + return modified.get() ? bundle : null; + } + + private Bundle installOrUpdateBundle( + String bundleLocation, BufferedInputStream is, long checksum, AtomicBoolean modified) + throws IOException, BundleException + { + JarInputStream jar = null; + try { + is.mark(256 * 1024); + jar = new JarInputStream(is); + Manifest m = jar.getManifest(); + if( m == null ) { + throw new BundleException( + "The bundle " + bundleLocation + " does not have a META-INF/MANIFEST.MF! " + + "Make sure, META-INF and MANIFEST.MF are the first 2 entries in your JAR!"); + } + String sn = m.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME); + String vStr = m.getMainAttributes().getValue(Constants.BUNDLE_VERSION); + Version v = vStr == null ? Version.emptyVersion : Version.parseVersion(vStr); + Bundle[] bundles = context.getBundles(); + for (Bundle b : bundles) { + if (b.getSymbolicName() != null && b.getSymbolicName().equals(sn)) { + vStr = b.getHeaders().get(Constants.BUNDLE_VERSION); + Version bv = vStr == null ? Version.emptyVersion : Version.parseVersion(vStr); + if (v.equals(bv)) { + is.reset(); + if (Util.loadChecksum(b, context) != checksum) { + log(Logger.LOG_WARNING, + "A bundle with the same symbolic name (" + + sn + ") and version (" + vStr + + ") is already installed. Updating this bundle instead.", null + ); + stopTransient(b); + Util.storeChecksum(b, checksum, context); + b.update(is); + modified.set(true); + } + return b; + } + } + } + is.reset(); + Util.log(context, Logger.LOG_INFO, "Installing bundle " + sn + + " / " + v, null); + Bundle b = context.installBundle(bundleLocation, is); + Util.storeChecksum(b, checksum, context); + modified.set(true); + + // Set default start level at install time, the user can override it if he wants + if (startLevel != 0) { + b.adapt(BundleStartLevel.class).setStartLevel(startLevel); + } + + return b; + } + finally + { + if (jar != null) + { + jar.close(); + } + } + } + + /** + * Uninstall a jar file. + */ + private Bundle uninstall(Artifact artifact) + { + Bundle bundle = null; + try + { + File path = artifact.getPath(); + // Find a listener for this artifact if needed + if (artifact.getListener() == null) + { + artifact.setListener(findListener(path, fileInstall.getListeners())); + } + // Forget this artifact + removeArtifact(path); + // Delete transformed file + deleteTransformedFile(artifact); + // if the listener is an installer, uninstall the artifact + if (artifact.getListener() instanceof ArtifactInstaller) + { + ((ArtifactInstaller) artifact.getListener()).uninstall(path); + } + // else we need uninstall the bundle + else if (artifact.getBundleId() != 0) + { + // old can't be null because of the way we calculate deleted list. + bundle = context.getBundle(artifact.getBundleId()); + if (bundle == null) + { + log(Logger.LOG_WARNING, + "Failed to uninstall bundle: " + + path + " with id: " + + artifact.getBundleId() + + ". The bundle has already been uninstalled", null); + return null; + } + log(Logger.LOG_INFO, + "Uninstalling bundle " + + bundle.getBundleId() + " (" + + bundle.getSymbolicName() + ")", null); + bundle.uninstall(); + } + } + catch (Exception e) + { + log(Logger.LOG_WARNING, "Failed to uninstall artifact: " + artifact.getPath(), e); + } + return bundle; + } + + private Bundle update(Artifact artifact) + { + Bundle bundle = null; + try + { + File path = artifact.getPath(); + // If the listener is an installer, ask for an update + if (artifact.getListener() instanceof ArtifactInstaller) + { + ((ArtifactInstaller) artifact.getListener()).update(path); + } + // if the listener is an url transformer + else if (artifact.getListener() instanceof ArtifactUrlTransformer) + { + URL transformed = artifact.getTransformedUrl(); + bundle = context.getBundle(artifact.getBundleId()); + if (bundle == null) + { + log(Logger.LOG_WARNING, + "Failed to update bundle: " + + path + " with ID " + + artifact.getBundleId() + + ". The bundle has been uninstalled", null); + return null; + } + Util.log(context, Logger.LOG_INFO, "Updating bundle " + bundle.getSymbolicName() + + " / " + bundle.getVersion(), null); + stopTransient(bundle); + Util.storeChecksum(bundle, artifact.getChecksum(), context); + InputStream in = (transformed != null) + ? transformed.openStream() + : new FileInputStream(path); + try + { + bundle.update(in); + } + finally + { + in.close(); + } + } + // else we need to ask for an update on the bundle + else if (artifact.getListener() instanceof ArtifactTransformer) + { + File transformed = artifact.getTransformed(); + bundle = context.getBundle(artifact.getBundleId()); + if (bundle == null) + { + log(Logger.LOG_WARNING, + "Failed to update bundle: " + + path + " with ID " + + artifact.getBundleId() + + ". The bundle has been uninstalled", null); + return null; + } + Util.log(context, Logger.LOG_INFO, "Updating bundle " + bundle.getSymbolicName() + + " / " + bundle.getVersion(), null); + stopTransient(bundle); + Util.storeChecksum(bundle, artifact.getChecksum(), context); + InputStream in = new FileInputStream(transformed != null ? transformed : path); + try + { + bundle.update(in); + } + finally + { + in.close(); + } + } + } + catch (Throwable t) + { + log(Logger.LOG_WARNING, "Failed to update artifact " + artifact.getPath(), t); + } + return bundle; + } + + private void stopTransient(Bundle bundle) throws BundleException + { + // Stop the bundle transiently so that it will be restarted when startAllBundles() is called + // but this avoids the need to restart the bundle twice (once for the update and another one + // when refreshing packages). + if (startBundles) + { + if (!isFragment(bundle)) + { + bundle.stop(Bundle.STOP_TRANSIENT); + } + } + } + + /** + * Tries to start all the bundles which somehow got stopped transiently. + * The File Install component will only retry the start When {@link #USE_START_TRANSIENT} + * is set to true or when a bundle is persistently started. Persistently stopped bundles + * are ignored. + */ + private void startAllBundles() + { + FrameworkStartLevel startLevelSvc = systemBundle.adapt(FrameworkStartLevel.class); + Set bundles = new LinkedHashSet<>(); + for (Artifact artifact : getArtifacts()) { + if (artifact.getBundleId() > 0) { + Bundle bundle = context.getBundle(artifact.getBundleId()); + if (bundle != null) { + if (bundle.getState() != Bundle.STARTING && bundle.getState() != Bundle.ACTIVE + && (useStartTransient || bundle.adapt(BundleStartLevel.class).isPersistentlyStarted()) + && startLevelSvc.getStartLevel() >= bundle.adapt(BundleStartLevel.class).getStartLevel()) { + bundles.add(bundle); + } + } + } + } + startBundles(bundles); + } + + /** + * Starts a bundle and removes it from the Collection when successfully started. + */ + private void startBundles(Set bundles) + { + // Check if this is the consistent set of bundles which failed previously. + boolean logFailures = !consistentlyFailingBundles.equals(bundles); + for (Iterator b = bundles.iterator(); b.hasNext(); ) + { + if (startBundle(b.next(), logFailures)) + { + b.remove(); + } + } + } + + /** + * Start a bundle, if the framework's startlevel allows it. + * @param bundle the bundle to start. + * @return whether the bundle was started. + */ + private boolean startBundle(Bundle bundle, boolean logFailures) + { + // Fragments can never be started. + // Bundles can only be started transient when the start level of the framework is high + // enough. Persistent (i.e. non-transient) starts will simply make the framework start the + // bundle when the start level is high enough. + if (startBundles + && bundle.getState() != Bundle.UNINSTALLED + && !isFragment(bundle) + && frameworkStartLevel >= bundle.adapt(BundleStartLevel.class).getStartLevel()) + { + try + { + int options = useStartTransient ? Bundle.START_TRANSIENT : 0; + options |= useStartActivationPolicy ? Bundle.START_ACTIVATION_POLICY : 0; + bundle.start(options); + log(Logger.LOG_INFO, "Started bundle: " + bundle.getLocation(), null); + return true; + } + catch (BundleException e) + { + // Don't log this as an error, instead we start the bundle repeatedly. + if (logFailures) + { + log(Logger.LOG_WARNING, "Error while starting bundle: " + bundle.getLocation(), e); + } + } + } + return false; + } + + protected Set getScopedBundles(String scope) { + // No bundles to check + if (SCOPE_NONE.equals(scope)) { + return new HashSet(); + } + // Go through managed bundles + else if (SCOPE_MANAGED.equals(scope)) { + Set bundles = new HashSet(); + for (Artifact artifact : getArtifacts()) { + if (artifact.getBundleId() > 0) { + Bundle bundle = context.getBundle(artifact.getBundleId()); + if (bundle != null) { + bundles.add(bundle); + } + } + } + return bundles; + // Go through all bundles + } else { + return new HashSet(Arrays.asList(context.getBundles())); + } + } + + protected void findBundlesWithFragmentsToRefresh(Set toRefresh) { + Set fragments = new HashSet(); + Set bundles = getScopedBundles(fragmentScope); + for (Bundle b : toRefresh) { + if (b.getState() != Bundle.UNINSTALLED) { + String hostHeader = b.getHeaders().get(Constants.FRAGMENT_HOST); + if (hostHeader != null) { + Clause[] clauses = Parser.parseHeader(hostHeader); + if (clauses != null && clauses.length > 0) { + Clause path = clauses[0]; + for (Bundle hostBundle : bundles) { + if (hostBundle.getSymbolicName() != null && + hostBundle.getSymbolicName().equals(path.getName())) { + String ver = path.getAttribute(Constants.BUNDLE_VERSION_ATTRIBUTE); + if (ver != null) { + VersionRange v = VersionRange.parseVersionRange(ver); + if (v.contains(VersionTable.getVersion(hostBundle.getHeaders().get(Constants.BUNDLE_VERSION)))) { + fragments.add(hostBundle); + } + } else { + fragments.add(hostBundle); + } + } + } + } + } + } + } + toRefresh.addAll(fragments); + } + + protected void findBundlesWithOptionalPackagesToRefresh(Set toRefresh) { + Set bundles = getScopedBundles(optionalScope); + // First pass: include all bundles contained in these features + bundles.removeAll(toRefresh); + if (bundles.isEmpty()) { + return; + } + // Second pass: for each bundle, check if there is any unresolved optional package that could be resolved + Map> imports = new HashMap>(); + for (Iterator it = bundles.iterator(); it.hasNext(); ) { + Bundle b = it.next(); + String importsStr = b.getHeaders().get(Constants.IMPORT_PACKAGE); + List importsList = getOptionalImports(importsStr); + if (importsList.isEmpty()) { + it.remove(); + } else { + imports.put(b, importsList); + } + } + if (bundles.isEmpty()) { + return; + } + // Third pass: compute a list of packages that are exported by our bundles and see if + // some exported packages can be wired to the optional imports + List exports = new ArrayList(); + for (Bundle b : toRefresh) { + if (b.getState() != Bundle.UNINSTALLED) { + String exportsStr = b.getHeaders().get(Constants.EXPORT_PACKAGE); + if (exportsStr != null) { + Clause[] exportsList = Parser.parseHeader(exportsStr); + exports.addAll(Arrays.asList(exportsList)); + } + } + } + for (Iterator it = bundles.iterator(); it.hasNext(); ) { + Bundle b = it.next(); + List importsList = imports.get(b); + for (Iterator itpi = importsList.iterator(); itpi.hasNext(); ) { + Clause pi = itpi.next(); + boolean matching = false; + for (Clause pe : exports) { + if (pi.getName().equals(pe.getName())) { + String evStr = pe.getAttribute(Constants.VERSION_ATTRIBUTE); + String ivStr = pi.getAttribute(Constants.VERSION_ATTRIBUTE); + Version exported = evStr != null ? Version.parseVersion(evStr) : Version.emptyVersion; + VersionRange imported = ivStr != null ? VersionRange.parseVersionRange(ivStr) : VersionRange.ANY_VERSION; + if (imported.contains(exported)) { + matching = true; + break; + } + } + } + if (!matching) { + itpi.remove(); + } + } + if (importsList.isEmpty()) { + it.remove(); +// } else { +// LOGGER.debug("Refreshing bundle {} ({}) to solve the following optional imports", b.getSymbolicName(), b.getBundleId()); +// for (Clause p : importsList) { +// LOGGER.debug(" {}", p); +// } +// + } + } + toRefresh.addAll(bundles); + } + + protected List getOptionalImports(String importsStr) + { + Clause[] imports = Parser.parseHeader(importsStr); + List result = new LinkedList(); + for (Clause anImport : imports) + { + String resolution = anImport.getDirective(Constants.RESOLUTION_DIRECTIVE); + if (Constants.RESOLUTION_OPTIONAL.equals(resolution)) + { + result.add(anImport); + } + } + return result; + } + + public void addListener(ArtifactListener listener, long stamp) + { + if (updateWithListeners) + { + for (Artifact artifact : getArtifacts()) + { + if (artifact.getListener() == null && artifact.getBundleId() > 0) + { + Bundle bundle = context.getBundle(artifact.getBundleId()); + if (bundle != null && bundle.getLastModified() < stamp) + { + File path = artifact.getPath(); + if (listener.canHandle(path)) + { + synchronized (processingFailures) + { + processingFailures.add(path); + } + } + } + } + } + } + synchronized (this) + { + this.notifyAll(); + } + } + + public void removeListener(ArtifactListener listener) + { + for (Artifact artifact : getArtifacts()) + { + if (artifact.getListener() == listener) + { + artifact.setListener(null); + } + } + synchronized (this) + { + this.notifyAll(); + } + } + + private Artifact getArtifact(File file) + { + synchronized (currentManagedArtifacts) + { + return currentManagedArtifacts.get(file); + } + } + + private List getArtifacts() + { + synchronized (currentManagedArtifacts) + { + return new ArrayList(currentManagedArtifacts.values()); + } + } + + private void setArtifact(File file, Artifact artifact) + { + synchronized (currentManagedArtifacts) + { + currentManagedArtifacts.put(file, artifact); + } + } + + private void removeArtifact(File file) + { + synchronized (currentManagedArtifacts) + { + currentManagedArtifacts.remove(file); + } + } + + private void setStateChanged(boolean changed) { + this.stateChanged.set(changed); + } + + private boolean isStateChanged() { + return stateChanged.get(); + } + +} diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/FileInstall.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/FileInstall.java new file mode 100644 index 00000000000..5ccaecd5dfa --- /dev/null +++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/FileInstall.java @@ -0,0 +1,439 @@ +/* + * 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.fileinstall.internal; + +import java.io.File; +import java.util.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.apache.felix.fileinstall.ArtifactInstaller; +import org.apache.felix.fileinstall.ArtifactListener; +import org.apache.felix.fileinstall.ArtifactTransformer; +import org.apache.felix.fileinstall.ArtifactUrlTransformer; +import org.apache.felix.fileinstall.internal.Util.Logger; +import org.apache.felix.utils.properties.InterpolationHelper; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.wiring.FrameworkWiring; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedServiceFactory; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +/** + * This clever little bundle watches a directory and will install any jar file + * if finds in that directory (as long as it is a valid bundle and not a + * fragment). + * + */ +public class FileInstall implements BundleActivator, ServiceTrackerCustomizer +{ + Runnable cmSupport; + final Map listeners = new TreeMap(); + final BundleTransformer bundleTransformer = new BundleTransformer(); + BundleContext context; + final Map watchers = new HashMap(); + ServiceTracker listenersTracker; + final ReadWriteLock lock = new ReentrantReadWriteLock(); + ServiceRegistration urlHandlerRegistration; + volatile boolean stopped; + + public void start(BundleContext context) throws Exception + { + this.context = context; + lock.writeLock().lock(); + + try + { + Hashtable props = new Hashtable(); + props.put("url.handler.protocol", JarDirUrlHandler.PROTOCOL); + urlHandlerRegistration = context.registerService(org.osgi.service.url.URLStreamHandlerService.class.getName(), new JarDirUrlHandler(), props); + + String flt = "(|(" + Constants.OBJECTCLASS + "=" + ArtifactInstaller.class.getName() + ")" + + "(" + Constants.OBJECTCLASS + "=" + ArtifactTransformer.class.getName() + ")" + + "(" + Constants.OBJECTCLASS + "=" + ArtifactUrlTransformer.class.getName() + "))"; + listenersTracker = new ServiceTracker(context, FrameworkUtil.createFilter(flt), this); + listenersTracker.open(); + + try + { + cmSupport = new ConfigAdminSupport(context, this); + } + catch (NoClassDefFoundError e) + { + Util.log(context, Logger.LOG_DEBUG, + "ConfigAdmin is not available, some features will be disabled", e); + } + + // Created the initial configuration + Hashtable ht = new Hashtable(); + + set(ht, DirectoryWatcher.POLL); + set(ht, DirectoryWatcher.DIR); + set(ht, DirectoryWatcher.LOG_LEVEL); + set(ht, DirectoryWatcher.LOG_DEFAULT); + set(ht, DirectoryWatcher.FILTER); + set(ht, DirectoryWatcher.TMPDIR); + set(ht, DirectoryWatcher.START_NEW_BUNDLES); + set(ht, DirectoryWatcher.USE_START_TRANSIENT); + set(ht, DirectoryWatcher.USE_START_ACTIVATION_POLICY); + set(ht, DirectoryWatcher.NO_INITIAL_DELAY); + set(ht, DirectoryWatcher.DISABLE_CONFIG_SAVE); + set(ht, DirectoryWatcher.ENABLE_CONFIG_SAVE); + set(ht, DirectoryWatcher.CONFIG_ENCODING); + set(ht, DirectoryWatcher.START_LEVEL); + set(ht, DirectoryWatcher.ACTIVE_LEVEL); + set(ht, DirectoryWatcher.UPDATE_WITH_LISTENERS); + set(ht, DirectoryWatcher.OPTIONAL_SCOPE); + set(ht, DirectoryWatcher.FRAGMENT_SCOPE); + set(ht, DirectoryWatcher.DISABLE_NIO2); + set(ht, DirectoryWatcher.SUBDIR_MODE); + + // check if dir is an array of dirs + String dirs = ht.get(DirectoryWatcher.DIR); + if (dirs != null && dirs.indexOf(',') != -1) + { + StringTokenizer st = new StringTokenizer(dirs, ","); + int index = 0; + while (st.hasMoreTokens()) + { + final String dir = st.nextToken().trim(); + ht.put(DirectoryWatcher.DIR, dir); + + String name = "initial"; + if (index > 0) name = name + index; + updated(name, new Hashtable(ht)); + + index++; + } + } + else + { + updated("initial", ht); + } + } + finally + { + // now notify all the directory watchers to proceed + // We need this to avoid race conditions observed in FELIX-2791 + lock.writeLock().unlock(); + } + } + + public Object addingService(ServiceReference serviceReference) + { + ArtifactListener listener = (ArtifactListener) context.getService(serviceReference); + addListener(serviceReference, listener); + return listener; + } + public void modifiedService(ServiceReference reference, Object service) + { + removeListener(reference, (ArtifactListener) service); + addListener(reference, (ArtifactListener) service); + } + public void removedService(ServiceReference serviceReference, Object service) + { + removeListener(serviceReference, (ArtifactListener) service); + } + + // Adapted for FELIX-524 + private void set(Hashtable ht, String key) + { + String o = context.getProperty(key); + if (o == null) + { + o = System.getProperty(key.toUpperCase().replace('.', '_')); + if (o == null) + { + return; + } + } + ht.put(key, o); + } + + public void stop(BundleContext context) throws Exception + { + lock.writeLock().lock(); + try + { + urlHandlerRegistration.unregister(); + List toClose = new ArrayList(); + synchronized (watchers) + { + toClose.addAll(watchers.values()); + watchers.clear(); + } + for (DirectoryWatcher aToClose : toClose) + { + try + { + aToClose.close(); + } + catch (Exception e) + { + // Ignore + } + } + if (listenersTracker != null) + { + listenersTracker.close(); + } + if (cmSupport != null) + { + cmSupport.run(); + } + } + finally + { + stopped = true; + lock.writeLock().unlock(); + } + } + + public void deleted(String pid) + { + DirectoryWatcher watcher; + synchronized (watchers) + { + watcher = watchers.remove(pid); + } + if (watcher != null) + { + watcher.close(); + } + } + + public void updated(String pid, Map properties) + { + InterpolationHelper.performSubstitution(properties, context); + DirectoryWatcher watcher; + synchronized (watchers) + { + watcher = watchers.get(pid); + if (watcher != null && watcher.getProperties().equals(properties)) + { + return; + } + } + if (watcher != null) + { + watcher.close(); + } + watcher = new DirectoryWatcher(this, properties, context); + watcher.setDaemon(true); + synchronized (watchers) + { + watchers.put(pid, watcher); + } + watcher.start(); + } + + public void updateChecksum(File file) + { + List toUpdate = new ArrayList(); + synchronized (watchers) + { + toUpdate.addAll(watchers.values()); + } + for (DirectoryWatcher watcher : toUpdate) + { + watcher.scanner.updateChecksum(file); + } + } + + private void addListener(ServiceReference reference, ArtifactListener listener) + { + synchronized (listeners) + { + listeners.put(reference, listener); + } + + long currentStamp = reference.getBundle().getLastModified(); + + List toNotify = new ArrayList(); + synchronized (watchers) + { + toNotify.addAll(watchers.values()); + } + for (DirectoryWatcher dir : toNotify) + { + dir.addListener(listener, currentStamp); + } + } + + private void removeListener(ServiceReference reference, ArtifactListener listener) + { + synchronized (listeners) + { + listeners.remove(reference); + } + List toNotify = new ArrayList(); + synchronized (watchers) + { + toNotify.addAll(watchers.values()); + } + for (DirectoryWatcher dir : toNotify) + { + dir.removeListener(listener); + } + } + + List getListeners() + { + synchronized (listeners) + { + List l = new ArrayList(listeners.values()); + Collections.reverse(l); + l.add(bundleTransformer); + return l; + } + } + + /** + * Convenience to refresh the packages + */ + static void refresh(Bundle systemBundle, Collection bundles) throws InterruptedException + { + final CountDownLatch latch = new CountDownLatch(1); + FrameworkWiring wiring = systemBundle.adapt(FrameworkWiring.class); + wiring.refreshBundles(bundles, new FrameworkListener() { + public void frameworkEvent(FrameworkEvent event) { + latch.countDown(); + } + }); + latch.await(); + } + + private class ConfigAdminSupport implements Runnable + { + private Tracker tracker; + private ServiceRegistration registration; + + private ConfigAdminSupport(BundleContext context, FileInstall fileInstall) + { + tracker = new Tracker(context, fileInstall); + Hashtable props = new Hashtable(); + props.put(Constants.SERVICE_PID, tracker.getName()); + registration = context.registerService(ManagedServiceFactory.class.getName(), tracker, props); + tracker.open(); + } + + public void run() + { + tracker.close(); + registration.unregister(); + } + + private class Tracker extends ServiceTracker implements ManagedServiceFactory { + + private final FileInstall fileInstall; + private final Set configs = Collections.synchronizedSet(new HashSet()); + private final Map configInstallers = new HashMap(); + + private Tracker(BundleContext bundleContext, FileInstall fileInstall) + { + super(bundleContext, ConfigurationAdmin.class.getName(), null); + this.fileInstall = fileInstall; + } + + public String getName() + { + return "org.apache.felix.fileinstall"; + } + + public void updated(String s, Dictionary dictionary) throws ConfigurationException + { + configs.add(s); + Map props = new HashMap(); + for (Enumeration e = dictionary.keys(); e.hasMoreElements();) { + String k = e.nextElement(); + props.put(k, dictionary.get(k).toString()); + } + fileInstall.updated(s, props); + } + + public void deleted(String s) + { + configs.remove(s); + fileInstall.deleted(s); + } + + public ConfigurationAdmin addingService(ServiceReference serviceReference) + { + lock.writeLock().lock(); + try + { + if (stopped) { + return null; + } + ConfigurationAdmin cm = super.addingService(serviceReference); + long id = (Long) serviceReference.getProperty(Constants.SERVICE_ID); + ConfigInstaller configInstaller = new ConfigInstaller(this.context, cm, fileInstall); + configInstaller.init(); + configInstallers.put(id, configInstaller); + return cm; + } + finally + { + lock.writeLock().unlock(); + } + } + + public void removedService(ServiceReference serviceReference, ConfigurationAdmin o) + { + lock.writeLock().lock(); + try + { + if (stopped) { + return; + } + Iterator iterator = configs.iterator(); + while (iterator.hasNext()) + { + String s = (String) iterator.next(); + fileInstall.deleted(s); + iterator.remove(); + } + long id = (Long) serviceReference.getProperty(Constants.SERVICE_ID); + ConfigInstaller configInstaller = configInstallers.remove(id); + if (configInstaller != null) + { + configInstaller.destroy(); + } + super.removedService(serviceReference, o); + } + finally + { + lock.writeLock().unlock(); + } + } + } + } + +} \ No newline at end of file diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/JarDirUrlHandler.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/JarDirUrlHandler.java new file mode 100644 index 00000000000..67a5e940246 --- /dev/null +++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/JarDirUrlHandler.java @@ -0,0 +1,105 @@ +/** + * + * 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.fileinstall.internal; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; + +import org.osgi.service.url.AbstractURLStreamHandlerService; + +/** + * A URL handler that can jar a directory on the fly + */ +public class JarDirUrlHandler extends AbstractURLStreamHandlerService +{ + + public static final String PROTOCOL = "jardir"; + + private static final String SYNTAX = PROTOCOL + ": file"; + + /** + * Open the connection for the given URL. + * + * @param url the url from which to open a connection. + * @return a connection on the specified URL. + * @throws java.io.IOException if an error occurs or if the URL is malformed. + */ + public URLConnection openConnection(URL url) throws IOException + { + if (url.getPath() == null || url.getPath().trim().length() == 0) + { + throw new MalformedURLException("Path can not be null or empty. Syntax: " + SYNTAX ); + } + return new Connection(url); + } + + public class Connection extends URLConnection + { + + public Connection(URL url) + { + super(url); + } + + public void connect() throws IOException + { + } + + public InputStream getInputStream() throws IOException + { + try + { + final PipedOutputStream pos = new PipedOutputStream(); + final PipedInputStream pis = new PipedInputStream(pos); + new Thread() + { + public void run() + { + try + { + Util.jarDir(new File(getURL().getPath()), pos); + } + catch (IOException e) + { + try + { + pos.close(); + } + catch (IOException e2) + { + // Ignore + } + } + } + }.start(); + return pis; + } + catch (Exception e) + { + throw (IOException) new IOException("Error opening spring xml url").initCause(e); + } + } + } + +} diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Scanner.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Scanner.java new file mode 100644 index 00000000000..5b8631182a7 --- /dev/null +++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Scanner.java @@ -0,0 +1,263 @@ +/* + * 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.fileinstall.internal; + +import java.io.Closeable; +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Pattern; +import java.util.zip.CRC32; + +/** + * A Scanner object is able to detect and report new, modified + * and deleted files. + * + * The scanner use an internal checksum to identify the signature + * of a file or directory. The checksum will change if the file + * or any of the directory's child is modified. + * + * In addition, if the scanner detects a change on a given file, it + * will wait until the checksum does not change anymore before reporting + * the change on this file. This allows to not report the change until + * a big copy if complete for example. + */ +public class Scanner implements Closeable { + + public final static String SUBDIR_MODE_JAR = "jar"; + public final static String SUBDIR_MODE_SKIP = "skip"; + public final static String SUBDIR_MODE_RECURSE = "recurse"; + + final File directory; + final FilenameFilter filter; + final boolean jarSubdir; + final boolean skipSubdir; + final boolean recurseSubdir; + + // Store checksums of files or directories + Map lastChecksums = new HashMap(); + Map storedChecksums = new HashMap(); + + /** + * Create a scanner for the specified directory + * + * @param directory the directory to scan + */ + public Scanner(File directory) + { + this(directory, null, null); + } + + /** + * Create a scanner for the specified directory and file filter + * + * @param directory the directory to scan + * @param filterString a filter for file names + * @param subdirMode to use when scanning + */ + public Scanner(File directory, final String filterString, String subdirMode) + { + this.directory = canon(directory); + if (filterString != null && filterString.length() > 0) + { + this.filter = new FilenameFilter() + { + Pattern pattern = Pattern.compile(filterString); + public boolean accept(File dir, String name) + { + return pattern.matcher(name).matches(); + } + }; + } + else + { + this.filter = null; + } + this.jarSubdir = subdirMode == null || SUBDIR_MODE_JAR.equals(subdirMode); + this.skipSubdir = SUBDIR_MODE_SKIP.equals(subdirMode); + this.recurseSubdir = SUBDIR_MODE_RECURSE.equals(subdirMode); + } + + /** + * Initialize the list of known files. + * This should be called before the first scan to initialize + * the list of known files. The purpose is to be able to detect + * files that have been deleted while the scanner was inactive. + * + * @param checksums a map of checksums + */ + public void initialize(Map checksums) + { + storedChecksums.putAll(checksums); + } + + /** + * Report a set of new, modified or deleted files. + * Modifications are checked against a computed checksum on some file + * attributes to detect any modification. + * Upon restart, such checksums are not known so that all files will + * be reported as modified. + * + * @param reportImmediately report all files immediately without waiting for the checksum to be stable + * @return a list of changes on the files included in the directory + */ + public Set scan(boolean reportImmediately) + { + File[] list = directory.listFiles(filter); + Set files = processFiles(reportImmediately, list); + return new TreeSet<>(files); + } + + private Set processFiles(boolean reportImmediately, File[] list) + { + if (list == null) + { + return new HashSet<>(); + } + Set files = new HashSet(); + Set removed = new HashSet(storedChecksums.keySet()); + for (File file : list) + { + if (file.isDirectory()) + { + if (skipSubdir) + { + continue; + } + else if (recurseSubdir) + { + files.addAll(processFiles(reportImmediately, file.listFiles(filter))); + continue; + } + } + long lastChecksum = lastChecksums.get(file) != null ? (Long) lastChecksums.get(file) : 0; + long storedChecksum = storedChecksums.get(file) != null ? (Long) storedChecksums.get(file) : 0; + long newChecksum = checksum(file); + lastChecksums.put(file, newChecksum); + // Only handle file when it does not change anymore and it has changed + // since last reported + if ((newChecksum == lastChecksum || reportImmediately) && newChecksum != storedChecksum) + { + storedChecksums.put(file, newChecksum); + files.add(file); + } + removed.remove(file); + } + // Make sure we'll handle a file that has been deleted + files.addAll(removed); + for (File file : removed) + { + // Remove no longer used checksums + lastChecksums.remove(file); + storedChecksums.remove(file); + } + return files; + } + + public void close() throws IOException { + } + + private static File canon(File file) + { + try + { + return file.getCanonicalFile(); + } + catch (IOException e) + { + return file; + } + } + + /** + * Retrieve the previously computed checksum for a give file. + * + * @param file the file to retrieve the checksum + * @return the checksum + */ + public long getChecksum(File file) + { + Long c = storedChecksums.get(file); + return c != null ? c : 0; + } + + /** + * Update the checksum of a file if that file is already known locally. + */ + public void updateChecksum(File file) + { + if (file != null && storedChecksums.containsKey(file)) + { + long newChecksum = checksum(file); + storedChecksums.put(file, newChecksum); + } + } + + /** + * Compute a cheksum for the file or directory that consists of the name, length and the last modified date + * for a file and its children in case of a directory + * + * @param file the file or directory + * @return a checksum identifying any change + */ + static long checksum(File file) + { + CRC32 crc = new CRC32(); + checksum(file, crc); + return crc.getValue(); + } + + private static void checksum(File file, CRC32 crc) + { + crc.update(file.getName().getBytes()); + if (file.isFile()) + { + checksum(file.lastModified(), crc); + checksum(file.length(), crc); + } + else if (file.isDirectory()) + { + File[] children = file.listFiles(); + if (children != null) + { + for (File aChildren : children) + { + checksum(aChildren, crc); + } + } + } + } + + private static void checksum(long l, CRC32 crc) + { + for (int i = 0; i < 8; i++) + { + crc.update((int) (l & 0x000000ff)); + l >>= 8; + } + } + +} diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Util.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Util.java new file mode 100644 index 00000000000..db924e5cd2c --- /dev/null +++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Util.java @@ -0,0 +1,385 @@ +/* + * 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.fileinstall.internal; + +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Collections; +import java.util.Set; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.logging.Level; +import java.util.zip.Deflater; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.log.LogService; + +public class Util +{ + private static final String CHECKSUM_SUFFIX = ".checksum"; + + /** + * Returns the log level as defined in the BundleContext or System properties. + * @param context {@link BundleContext} of the FileInstall bundle. + * @return the global log level, or {@link Logger#LOG_ERROR}. + */ + public static int getGlobalLogLevel(BundleContext context) + { + String s = context.getProperty(DirectoryWatcher.LOG_LEVEL); + s = (s == null) + ? System.getProperty(DirectoryWatcher.LOG_LEVEL.toUpperCase().replace('.', '_')) + : s; + s = (s == null) ? "1" : s; + int logLevel = Logger.LOG_ERROR; + try + { + logLevel = Integer.parseInt(s); + } + catch (NumberFormatException ex) + { + // Ignore + } + return logLevel; + } + + /** + * Log a message and optional throwable. If there is a log service we use + * it, otherwise we log to the console + * + * @param message + * The message to log + * @param e + * The throwable to log + */ + public static void log(BundleContext context, int msgLevel, String message, Throwable e) + { + getLogger(context).log(getGlobalLogLevel(context), msgLevel, message, e); + } + + public static void log(BundleContext context, int logLevel, int msgLevel, String message, Throwable e) + { + getLogger(context).log(logLevel, msgLevel, message, e); + } + + private static Logger getLogger(BundleContext context) + { + try + { + context.getBundle(); + if (logger != null && logger.isValidLogger(context)) + { + return logger; + } + logger = new OsgiLogger(context); + } + catch (Throwable t) + { + logger = new DefaultLogger(context); + } + return logger; + } + + private static Logger logger; + + interface Logger + { + static final int LOG_ERROR = 1; + static final int LOG_WARNING = 2; + static final int LOG_INFO = 3; + static final int LOG_DEBUG = 4; + + boolean isValidLogger(BundleContext context); + void log(int logLevel, int msgLevel, String message, Throwable throwable); + } + + static class DefaultLogger implements Logger + { + protected BundleContext context; + private final String logDefault; + + DefaultLogger(BundleContext context) + { + this.context = context; + String s = context.getProperty(DirectoryWatcher.LOG_DEFAULT); + s = (s == null) + ? System.getProperty(DirectoryWatcher.LOG_DEFAULT.toUpperCase().replace('.', '_')) + : s; + logDefault = (s == null) ? DirectoryWatcher.LOG_STDOUT : s; + } + + public boolean isValidLogger(BundleContext context) + { + return true; + } + + public void log(int logLevel, int msgLevel, String message, Throwable throwable) + { + // Only print the message if logging is enabled and + // the message level is less than or equal to the log + // level. + if ((logLevel > 0) && (msgLevel <= logLevel)) + { + if (DirectoryWatcher.LOG_JUL.equals(logDefault)) + { + Level lvl; + switch (msgLevel) + { + case 1: lvl = Level.SEVERE; break; + case 2: lvl = Level.WARNING; break; + case 3: lvl = Level.INFO; break; + case 4: lvl = Level.FINE; break; + default: lvl = Level.FINEST; break; + } + java.util.logging.Logger logger = java.util.logging.Logger.getLogger("fileinstall"); + logger.log(lvl, message, throwable); + } + else + { + System.out.println(message + ((throwable == null) ? "" : ": " + throwable)); + if (throwable != null) + { + throwable.printStackTrace(System.out); + } + } + } + } + } + + static class OsgiLogger extends DefaultLogger + { + OsgiLogger(BundleContext context) + { + super(context); + // Now make sure we can access the LogService class + try + { + getClass().getClassLoader().loadClass(LogService.class.getName()); + } + catch (ClassNotFoundException e) + { + throw new NoClassDefFoundError(e.getMessage()); + } + } + + public boolean isValidLogger(BundleContext context) + { + return context == this.context; + } + + public void log(int logLevel, int msgLevel, String message, Throwable throwable) + { + // Only print the message if logging is enabled and + // the message level is less than or equal to the log + // level. + if ((logLevel > 0) && (msgLevel <= logLevel)) + { + LogService log = getLogService(); + if (log != null) + { + log.log(msgLevel, message, throwable); + } + else + { + super.log(logLevel, msgLevel, message, throwable); + } + } + } + + private LogService getLogService() + { + ServiceReference ref = context.getServiceReference(LogService.class); + if (ref != null) + { + return context.getService(ref); + } + return null; + } + } + + /** + * Jar up a directory + */ + public static void jarDir(File directory, File zipName) throws IOException + { + jarDir(directory, new BufferedOutputStream(new FileOutputStream(zipName))); + } + + public static void jarDir(File directory, OutputStream os) throws IOException + { + // create a ZipOutputStream to zip the data to + JarOutputStream zos = new JarOutputStream(os); + zos.setLevel(Deflater.NO_COMPRESSION); + String path = ""; + File manFile = new File(directory, JarFile.MANIFEST_NAME); + if (manFile.exists()) + { + byte[] readBuffer = new byte[8192]; + FileInputStream fis = new FileInputStream(manFile); + try + { + ZipEntry anEntry = new ZipEntry(JarFile.MANIFEST_NAME); + zos.putNextEntry(anEntry); + int bytesIn = fis.read(readBuffer); + while (bytesIn != -1) + { + zos.write(readBuffer, 0, bytesIn); + bytesIn = fis.read(readBuffer); + } + } + finally + { + fis.close(); + } + zos.closeEntry(); + } + zipDir(directory, zos, path, Collections.singleton(JarFile.MANIFEST_NAME)); + // close the stream + zos.close(); + } + + /** + * Zip up a directory path + */ + public static void zipDir(File directory, ZipOutputStream zos, String path, Set exclusions) throws IOException + { + // get a listing of the directory content + File[] dirList = directory.listFiles(); + byte[] readBuffer = new byte[8192]; + int bytesIn; + // loop through dirList, and zip the files + assert dirList != null; + for (File f : dirList) + { + if (f.isDirectory()) + { + String prefix = path + f.getName() + "/"; + zos.putNextEntry(new ZipEntry(prefix)); + zipDir(f, zos, prefix, exclusions); + continue; + } + String entry = path + f.getName(); + if (!exclusions.contains(entry)) + { + FileInputStream fis = new FileInputStream(f); + try + { + ZipEntry anEntry = new ZipEntry(entry); + zos.putNextEntry(anEntry); + bytesIn = fis.read(readBuffer); + while (bytesIn != -1) + { + zos.write(readBuffer, 0, bytesIn); + bytesIn = fis.read(readBuffer); + } + } + finally + { + fis.close(); + } + } + } + } + + /** + * Stores the checksum into a bundle data file. + * @param b The bundle whose checksum must be stored + * @param checksum the lastModified date to be stored in bc + * @param bc the FileInstall's bundle context where to store the checksum. + */ + public static void storeChecksum( Bundle b, long checksum, BundleContext bc ) + { + String key = getBundleKey(b); + File f = bc.getDataFile( key + CHECKSUM_SUFFIX ); + DataOutputStream dout = null; + try + { + dout = new DataOutputStream( new FileOutputStream( f ) ); + dout.writeLong( checksum ); + } + catch ( Exception e ) + { + e.printStackTrace(); + } + finally + { + if ( dout != null ) + { + try + { + dout.close(); + } + catch ( IOException ignored ) + { + } + } + } + } + + /** + * Returns the stored checksum of the bundle. + * @param b the bundle whose checksum must be returned + * @param bc the FileInstall's bundle context. + * @return the stored checksum of the bundle + */ + public static long loadChecksum( Bundle b, BundleContext bc ) + { + String key = getBundleKey(b); + File f = bc.getDataFile( key + CHECKSUM_SUFFIX ); + DataInputStream in = null; + try + { + in = new DataInputStream( new FileInputStream( f ) ); + return in.readLong(); + } + catch ( Exception e ) + { + return Long.MIN_VALUE; + } + finally + { + if ( in != null ) + { + try + { + in.close(); + } + catch ( IOException e ) + { + // Ignore + } + } + } + } + + private static String getBundleKey(Bundle b) + { + return Long.toString(b.getBundleId()); + } + +} diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Watcher.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Watcher.java new file mode 100644 index 00000000000..b6de977be6f --- /dev/null +++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Watcher.java @@ -0,0 +1,324 @@ +/* + * 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.fileinstall.internal; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitOption; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static java.nio.file.LinkOption.NOFOLLOW_LINKS; +import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; +import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE; +import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; +import static java.nio.file.StandardWatchEventKinds.OVERFLOW; + +/** + * A File watching service + */ +public abstract class Watcher implements Closeable { + + private Path root; + private boolean watch = true; + private WatchService watcher; + private PathMatcher dirMatcher; + private PathMatcher fileMatcher; + private final Map keys = new ConcurrentHashMap(); + private volatile long lastModified; + private final Map processedMap = new ConcurrentHashMap(); + + public void init() throws IOException { + if (root == null) { + Iterable rootDirectories = getFileSystem().getRootDirectories(); + for (Path rootDirectory : rootDirectories) { + if (rootDirectory != null) { + root = rootDirectory; + break; + } + } + } + if (!Files.exists(root)) { + fail("Root path does not exist: " + root); + } else if (!Files.isDirectory(root)) { + fail("Root path is not a directory: " + root); + } + if (watcher == null) { + watcher = watch ? getFileSystem().newWatchService() : null; + } + } + + public void close() throws IOException { + if (watcher != null) { + watcher.close(); + } + } + + public long getLastModified() { + return lastModified; + } + + // Properties + //------------------------------------------------------------------------- + + + public void setRootPath(String rootPath) { + Path path = new File(rootPath).getAbsoluteFile().toPath(); + setRoot(path); + } + + public void setRootDirectory(File directory) { + setRoot(directory.toPath()); + } + + public Path getRoot() { + return root; + } + + public void setRoot(Path root) { + this.root = root; + } + + public boolean isWatch() { + return watch; + } + + public void setWatch(boolean watch) { + this.watch = watch; + } + + public WatchService getWatcher() { + return watcher; + } + + public void setWatcher(WatchService watcher) { + this.watcher = watcher; + } + + public PathMatcher getDirMatcher() { + return dirMatcher; + } + + public void setDirMatcher(PathMatcher dirMatcher) { + this.dirMatcher = dirMatcher; + } + + public PathMatcher getFileMatcher() { + return fileMatcher; + } + + public void setFileMatcher(PathMatcher fileMatcher) { + this.fileMatcher = fileMatcher; + } + + + // Implementation methods + //------------------------------------------------------------------------- + + public void rescan() throws IOException { + for (WatchKey key : keys.keySet()) { + key.cancel(); + } + keys.clear(); + Files.walkFileTree(root, + EnumSet.of(FileVisitOption.FOLLOW_LINKS), + Integer.MAX_VALUE, + new FilteringFileVisitor()); + } + + public void processEvents() { + while (true) { + WatchKey key = watcher.poll(); + if (key == null) { + break; + } + Path dir = keys.get(key); + if (dir == null) { + warn("Could not find key for " + key); + continue; + } + + for (WatchEvent event : key.pollEvents()) { + WatchEvent.Kind kind = event.kind(); + WatchEvent ev = (WatchEvent)event; + + // Context for directory entry event is the file name of entry + Path name = ev.context(); + Path child = dir.resolve(name); + + debug("Processing event {} on path {}", kind, child); + + if (kind == OVERFLOW) { +// rescan(); + continue; + } + + try { + if (kind == ENTRY_CREATE) { + if (Files.isDirectory(child)) { + + // if directory is created, and watching recursively, then + // register it and its sub-directories + Files.walkFileTree(child, new FilteringFileVisitor()); + } else if (Files.isRegularFile(child)) { + scan(child); + } + } else if (kind == ENTRY_MODIFY) { + if (Files.isRegularFile(child)) { + scan(child); + } + } else if (kind == ENTRY_DELETE) { + unscan(child); + } + } catch (IOException x) { + // ignore to keep sample readbale + x.printStackTrace(); + } + } + + // reset key and remove from set if directory no longer accessible + boolean valid = key.reset(); + if (!valid) { + debug("Removing key " + key + " and dir " + dir + " from keys"); + keys.remove(key); + + // all directories are inaccessible + if (keys.isEmpty()) { + break; + } + } + } + } + + private void scan(final Path file) throws IOException { + if (isMatchesFile(file)) { + process(file); + processedMap.put(file, Boolean.TRUE); + } + } + + protected boolean isMatchesFile(Path file) { + boolean matches = true; + if (fileMatcher != null) { + Path rel = root.relativize(file); + matches = fileMatcher.matches(rel); + } + return matches; + } + + private void unscan(final Path file) throws IOException { + if (isMatchesFile(file)) { + onRemove(file); + lastModified = System.currentTimeMillis(); + } else { + // lets find all the files that now no longer exist + List files = new ArrayList(processedMap.keySet()); + for (Path path : files) { + if (!Files.exists(path)) { + debug("File has been deleted: " + path); + processedMap.remove(path); + if (isMatchesFile(path)) { + onRemove(file); + lastModified = System.currentTimeMillis(); + } + } + } + } + } + + private void watch(final Path path) throws IOException { + if (watcher != null) { + WatchKey key = path.register(watcher, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE); + keys.put(key, path); + debug("Watched path " + path + " key " + key); + } else { + warn("No watcher yet for path " + path); + } + } + + protected FileSystem getFileSystem() { + return FileSystems.getDefault(); + } + + public class FilteringFileVisitor implements FileVisitor { + + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + if (Thread.interrupted()) { + throw new InterruptedIOException(); + } + if (dirMatcher != null) { + Path rel = root.relativize(dir); + if (!"".equals(rel.toString()) && !dirMatcher.matches(rel)) { + return FileVisitResult.SKIP_SUBTREE; + } + } + watch(dir); + return FileVisitResult.CONTINUE; + } + + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (Thread.interrupted()) { + throw new InterruptedIOException(); + } + scan(file); + return FileVisitResult.CONTINUE; + } + + public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { + return FileVisitResult.CONTINUE; + } + + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + return FileVisitResult.CONTINUE; + } + } + + + /** + * Throws an invalid argument exception after logging a warning + * just in case the stack trace gets gobbled up by application containers + * like spring or blueprint, at least the error message will be clearly shown in the log + * + */ + public void fail(String message) { + warn(message); + throw new IllegalArgumentException(message); + } + + protected abstract void debug(String message, Object... args); + protected abstract void warn(String message, Object... args); + protected abstract void process(Path path); + protected abstract void onRemove(Path path); +} diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/WatcherScanner.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/WatcherScanner.java new file mode 100644 index 00000000000..7277820627a --- /dev/null +++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/WatcherScanner.java @@ -0,0 +1,165 @@ +/* + * 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.fileinstall.internal; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.osgi.framework.BundleContext; + +public class WatcherScanner extends Scanner { + + BundleContext bundleContext; + PathMatcher fileMatcher; + Watcher watcher; + + Set changed = new HashSet(); + + /** + * Create a scanner for the specified directory and file filter + * + * @param directory the directory to scan + * @param filterString a filter for file names + * @param subdirMode to use when scanning + */ + public WatcherScanner(BundleContext bundleContext, File directory, String filterString, String subdirMode) throws IOException { + super(directory, filterString, subdirMode); + this.bundleContext = bundleContext; + if (filterString != null) { + this.fileMatcher = FileSystems.getDefault().getPathMatcher("regex:" + filterString); + } else { + this.fileMatcher = null; + } + this.watcher = new ScannerWatcher(); + this.watcher.setFileMatcher(fileMatcher); + this.watcher.setRootDirectory(this.directory); + this.watcher.init(); + this.watcher.rescan(); + } + + public Set scan(boolean reportImmediately) { + watcher.processEvents(); + synchronized (changed) { + if (changed.isEmpty()) { + return new HashSet(); + } + Set files = new HashSet(); + Set removed = new HashSet(); + if (reportImmediately) { + removed.addAll(storedChecksums.keySet()); + } + for (Iterator iterator = changed.iterator(); iterator.hasNext(); ) { + File file = iterator.next(); + long lastChecksum = lastChecksums.get(file) != null ? (Long) lastChecksums.get(file) : 0; + long storedChecksum = storedChecksums.get(file) != null ? (Long) storedChecksums.get(file) : 0; + long newChecksum = checksum(file); + lastChecksums.put(file, newChecksum); + if (file.exists()) { + // Only handle file when it does not change anymore and it has changed since last reported + if ((newChecksum == lastChecksum || reportImmediately)) { + if (newChecksum != storedChecksum) { + storedChecksums.put(file, newChecksum); + files.add(file); + } else { + iterator.remove(); + } + if (reportImmediately) { + removed.remove(file); + } + } + } else { + if (!reportImmediately) { + removed.add(file); + } + } + } + for (File file : removed) { + // Make sure we'll handle a file that has been deleted + files.add(file); + // Remove no longer used checksums + lastChecksums.remove(file); + storedChecksums.remove(file); + changed.remove(file); + } + + return files; + } + } + + public void close() throws IOException { + watcher.close(); + } + + class ScannerWatcher extends Watcher { + + @Override + protected void process(Path path) { + File file = path.toFile(); + if (!file.getParentFile().equals(directory)) { + // File is in a sub directory. + if (skipSubdir) { + return; + } + if (jarSubdir) { + // Walk up until the first level sub-directory. + do { + file = file.getParentFile(); + if (file == null) { + // The file was not actually inside the watched directory. + // Should not happen. + return; + } + } while (!file.getParentFile().equals(directory)); + } + // Otherwise we recurse by adding the file as-is. + } + synchronized (changed) { + changed.add(file); + } + } + + @Override + protected void onRemove(Path path) { + process(path); + } + + @Override + protected void debug(String message, Object... args) { + log(Util.Logger.LOG_DEBUG, message, args); + } + + @Override + protected void warn(String message, Object... args) { + log(Util.Logger.LOG_WARNING, message, args); + } + + protected void log(int level, String message, Object... args) { + String msg = String.format(message, args); + Util.log(bundleContext, level, msg, null); + } + + } + +} diff --git a/fileinstall/src/main/resources/OSGI-INF/metatype/metatype.xml b/fileinstall/src/main/resources/OSGI-INF/metatype/metatype.xml new file mode 100644 index 00000000000..58bda2d7a8b --- /dev/null +++ b/fileinstall/src/main/resources/OSGI-INF/metatype/metatype.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/BundleTransformerTest.java b/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/BundleTransformerTest.java new file mode 100644 index 00000000000..65a0765a701 --- /dev/null +++ b/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/BundleTransformerTest.java @@ -0,0 +1,37 @@ +/* + * 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.fileinstall.internal; + +import java.io.File; + +import junit.framework.TestCase; + +/** + * Test for the BundleTransformer + */ +public class BundleTransformerTest extends TestCase +{ + + public void testCanRecognizeInvalidJar() + { + assertFalse(new BundleTransformer().canHandle(new File("src/test/resources/watched/firstjar.jar"))); + assertFalse(new BundleTransformer().canHandle(new File("src/test/resources/watched/notexistentfile.jar"))); + } + +} diff --git a/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/ConfigInstallerTest.java b/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/ConfigInstallerTest.java new file mode 100644 index 00000000000..5d11dde38c9 --- /dev/null +++ b/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/ConfigInstallerTest.java @@ -0,0 +1,415 @@ +/* + * 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.fileinstall.internal; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.concurrent.atomic.AtomicReference; + +import junit.framework.TestCase; +import org.easymock.Capture; +import org.easymock.EasyMock; +import org.easymock.IAnswer; +import org.easymock.IArgumentMatcher; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.ConfigurationEvent; + +/** + * Tests for ConfigInstaller + */ +public class ConfigInstallerTest extends TestCase { + + BundleContext mockBundleContext; + Bundle mockBundle; + ConfigurationAdmin mockConfigurationAdmin; + Configuration mockConfiguration; + + protected void setUp() throws Exception + { + super.setUp(); + mockBundleContext = EasyMock.createMock(BundleContext.class); + mockBundle = EasyMock.createMock(Bundle.class); + mockConfigurationAdmin = EasyMock.createMock(ConfigurationAdmin.class); + mockConfiguration = EasyMock.createMock(Configuration.class); + } + + + public void testParsePidWithoutFactoryPid() + { + ConfigInstaller ci = new ConfigInstaller(null, null, null); + + String path = "pid.cfg"; + assertEquals( "Pid without Factory Pid calculated", "pid", ci.parsePid( path )[0] ); + assertEquals( "Pid without Factory Pid calculated", null, ci.parsePid( path )[1] ); + } + + + public void testParsePidWithFactoryPid() + { + ConfigInstaller ci = new ConfigInstaller(null, null, null); + + String path = "factory-pid.cfg"; + assertEquals( "Pid with Factory Pid calculated", "factory", ci.parsePid( path )[0] ); + assertEquals( "Pid with Factory Pid calculated", "pid", ci.parsePid( path )[1] ); + } + + public void testTypedConfiguration() throws Exception + { + File file = File.createTempFile("test", ".config"); + try (OutputStream os = new FileOutputStream(file)) { + os.write("networkInterface=\"wlp3s0\"\n".getBytes("UTF-8")); + } + String pid = file.getName().substring(0, file.getName().indexOf(".config")); + + Capture> props = new Capture<>(); + EasyMock.expect(mockConfigurationAdmin.listConfigurations((String) EasyMock.anyObject())) + .andReturn(null); + EasyMock.expect(mockConfigurationAdmin.getConfiguration(pid, "?")) + .andReturn(mockConfiguration); + EasyMock.expect(mockConfiguration.getProperties()) + .andReturn(null); + EasyMock.expect(mockBundleContext.getProperty((String) EasyMock.anyObject())) + .andReturn(null) + .anyTimes(); + mockConfiguration.update(EasyMock.capture(props)); + EasyMock.expectLastCall(); + EasyMock.replay(mockConfiguration, mockConfigurationAdmin, mockBundleContext); + + ConfigInstaller ci = new ConfigInstaller( mockBundleContext, mockConfigurationAdmin, new FileInstall() ); + ci.install(file); + + EasyMock.verify(mockConfiguration, mockConfigurationAdmin, mockBundleContext); + Dictionary loaded = props.getValue(); + assertNotNull(loaded); + assertEquals("wlp3s0", loaded.get("networkInterface")); + } + + public void testTypedConfigurationFloat() throws Exception + { + File file = File.createTempFile("test", ".config"); + try (OutputStream os = new FileOutputStream(file)) { + os.write("key=F\"1137191584\"\n".getBytes("UTF-8")); + } + String pid = file.getName().substring(0, file.getName().indexOf(".config")); + + Capture> props = new Capture<>(); + EasyMock.expect(mockConfigurationAdmin.listConfigurations((String) EasyMock.anyObject())) + .andReturn(null); + EasyMock.expect(mockConfigurationAdmin.getConfiguration(pid, "?")) + .andReturn(mockConfiguration); + EasyMock.expect(mockConfiguration.getProperties()) + .andReturn(null); + EasyMock.expect(mockBundleContext.getProperty((String) EasyMock.anyObject())) + .andReturn(null) + .anyTimes(); + mockConfiguration.update(EasyMock.capture(props)); + EasyMock.expectLastCall(); + EasyMock.replay(mockConfiguration, mockConfigurationAdmin, mockBundleContext); + + ConfigInstaller ci = new ConfigInstaller( mockBundleContext, mockConfigurationAdmin, new FileInstall() ); + ci.install(file); + + EasyMock.verify(mockConfiguration, mockConfigurationAdmin, mockBundleContext); + Dictionary loaded = props.getValue(); + assertNotNull(loaded); + assertEquals(400.333F, loaded.get("key")); + } + + public void testGetNewFactoryConfiguration() throws Exception + { + EasyMock.expect(mockConfigurationAdmin.listConfigurations((String) EasyMock.anyObject())) + .andReturn(null); + EasyMock.expect(mockConfigurationAdmin.createFactoryConfiguration( "pid", "?" )) + .andReturn(mockConfiguration); + EasyMock.replay(mockConfiguration, mockConfigurationAdmin, mockBundleContext); + + ConfigInstaller ci = new ConfigInstaller( mockBundleContext, mockConfigurationAdmin, new FileInstall() ); + + assertEquals( "Factory configuration retrieved", mockConfiguration, ci.getConfiguration( "pid-factoryPid.cfg", "pid", "factoryPid" ) ); + + EasyMock.verify(mockConfiguration, mockConfigurationAdmin, mockBundleContext); + } + + + public void testGetExistentFactoryConfiguration() throws Exception + { + EasyMock.expect(mockConfigurationAdmin.listConfigurations((String) EasyMock.anyObject())) + .andReturn(null); + EasyMock.expect(mockConfigurationAdmin.createFactoryConfiguration( "pid", "?" )) + .andReturn(mockConfiguration); + EasyMock.replay(mockConfiguration, mockConfigurationAdmin, mockBundleContext); + + ConfigInstaller ci = new ConfigInstaller( mockBundleContext, mockConfigurationAdmin, new FileInstall() ); + + assertEquals( "Factory configuration retrieved", mockConfiguration, ci.getConfiguration( "pid-factoryPid.cfg","pid", "factoryPid" ) ); + + EasyMock.verify(mockConfiguration, mockConfigurationAdmin, mockBundleContext); + } + + + public void testGetExistentNoFactoryConfiguration() throws Exception + { + EasyMock.expect(mockConfigurationAdmin.listConfigurations((String) EasyMock.anyObject())) + .andReturn(null); + EasyMock.expect(mockConfigurationAdmin.getConfiguration( "pid", "?" )) + .andReturn(mockConfiguration); + EasyMock.replay(mockConfiguration, mockConfigurationAdmin, mockBundleContext); + + ConfigInstaller ci = new ConfigInstaller( mockBundleContext, mockConfigurationAdmin, new FileInstall() ); + + assertEquals( "Factory configuration retrieved", mockConfiguration, ci.getConfiguration( "pid.cfg", "pid", null ) ); + + EasyMock.verify(mockConfiguration, mockConfigurationAdmin, mockBundleContext); + } + + + public void testDeleteConfig() throws Exception + { + mockConfiguration.delete(); + EasyMock.expect(mockBundleContext.getProperty(DirectoryWatcher.LOG_DEFAULT)).andReturn(null); + EasyMock.expect(mockBundleContext.getProperty(DirectoryWatcher.LOG_LEVEL)).andReturn(null); + EasyMock.expect(mockConfigurationAdmin.listConfigurations((String) EasyMock.anyObject())) + .andReturn(null); + EasyMock.expect(mockConfigurationAdmin.getConfiguration("pid", "?" )) + .andReturn(mockConfiguration); + EasyMock.replay(mockConfiguration, mockConfigurationAdmin, mockBundleContext); + + ConfigInstaller ci = new ConfigInstaller( mockBundleContext, mockConfigurationAdmin, new FileInstall() ); + + assertTrue( ci.deleteConfig( new File( "pid.cfg" ) ) ); + + EasyMock.verify(mockConfiguration, mockConfigurationAdmin, mockBundleContext); + } + + public void testCreateConfigAndObserveCMDeleted() throws Exception + { + File file = File.createTempFile("test", ".config"); + try (OutputStream os = new FileOutputStream(file)) { + os.write("key=F\"1137191584\"\n".getBytes("UTF-8")); + } + String pid = file.getName().substring(0, file.getName().indexOf(".config")); + + final Capture> props = new Capture<>(); + + EasyMock.expect(mockConfiguration.getProperties()) + .andReturn(null); + EasyMock.expect(mockBundleContext.getProperty((String) EasyMock.anyObject())) + .andReturn(null) + .anyTimes(); + EasyMock.expect(mockConfigurationAdmin.listConfigurations((String) EasyMock.anyObject())) + .andReturn(null); + EasyMock.expect(mockConfigurationAdmin.getConfiguration(pid, "?")) + .andReturn(mockConfiguration); + + ServiceReference sr = EasyMock.createMock(ServiceReference.class); + mockConfiguration.update(EasyMock.capture(props)); + EasyMock.expectLastCall(); + + EasyMock.expect(mockConfigurationAdmin.getConfiguration(pid, "?")) + .andReturn(mockConfiguration); + EasyMock.expect(mockConfiguration.getProperties()) + .andAnswer(new IAnswer>() { + @Override + public Dictionary answer() throws Throwable { + return props.getValue(); + } + }); + EasyMock.expect(mockConfiguration.getPid()) + .andReturn(pid); + + EasyMock.replay(mockConfiguration, mockConfigurationAdmin, mockBundleContext, sr); + + ConfigInstaller ci = new ConfigInstaller( mockBundleContext, mockConfigurationAdmin, new FileInstall() ); + ci.install(file); + + ci.doConfigurationEvent( new ConfigurationEvent(sr , ConfigurationEvent.CM_UPDATED, null, pid ) ); + + ci.doConfigurationEvent( new ConfigurationEvent(sr , ConfigurationEvent.CM_DELETED, null, pid ) ); + + assertFalse("Configuration file should be deleted", file.isFile()); + } + + public void testUseExistingConfigAndObserveCMDeleted() throws Exception + { + String pid = "test"; + + Capture> props = new Capture<>(); + + EasyMock.expect(mockConfiguration.getProperties()) + .andReturn(null); + EasyMock.expect(mockBundleContext.getProperty((String) EasyMock.anyObject())) + .andReturn(null) + .anyTimes(); + EasyMock.expect(mockConfigurationAdmin.listConfigurations((String) EasyMock.anyObject())) + .andReturn(null); + EasyMock.expect(mockConfigurationAdmin.getConfiguration(pid, "?")) + .andReturn(mockConfiguration); + + ServiceReference sr = EasyMock.createMock(ServiceReference.class); + mockConfiguration.update(EasyMock.capture(props)); + EasyMock.expectLastCall(); + + EasyMock.replay(mockConfiguration, mockConfigurationAdmin, mockBundleContext, sr); + + ConfigInstaller ci = new ConfigInstaller( mockBundleContext, mockConfigurationAdmin, new FileInstall() ); + + ci.doConfigurationEvent( new ConfigurationEvent(sr , ConfigurationEvent.CM_UPDATED, null, pid ) ); + ci.doConfigurationEvent( new ConfigurationEvent(sr , ConfigurationEvent.CM_DELETED, null, pid ) ); + } + + public void testUseExistingConfigWithFileinstallFilenameAndObserveCMDeleted() throws Exception + { + File file = File.createTempFile("test", ".config"); + try (OutputStream os = new FileOutputStream(file)) { + os.write("key=F\"1137191584\"\n".getBytes("UTF-8")); + } + String pid = "test"; + + Capture> propsCapture = new Capture<>(); + Dictionary props = new Hashtable<>(); + props.put(DirectoryWatcher.FILENAME, file.toURI().toString()); + + EasyMock.expect(mockConfiguration.getProperties()) + .andReturn(props); + EasyMock.expect(mockBundleContext.getProperty((String) EasyMock.anyObject())) + .andReturn(null) + .anyTimes(); + EasyMock.expect(mockConfigurationAdmin.listConfigurations((String) EasyMock.anyObject())) + .andReturn(null); + EasyMock.expect(mockConfigurationAdmin.getConfiguration(pid, "?")) + .andReturn(mockConfiguration); + EasyMock.expect(mockConfiguration.getPid()) + .andReturn(pid); + + ServiceReference sr = EasyMock.createMock(ServiceReference.class); + mockConfiguration.update(EasyMock.capture(propsCapture)); + EasyMock.expectLastCall(); + + EasyMock.replay(mockConfiguration, mockConfigurationAdmin, mockBundleContext, sr); + + ConfigInstaller ci = new ConfigInstaller( mockBundleContext, mockConfigurationAdmin, new FileInstall() ); + + ci.doConfigurationEvent( new ConfigurationEvent(sr , ConfigurationEvent.CM_UPDATED, null, pid ) ); + ci.doConfigurationEvent( new ConfigurationEvent(sr , ConfigurationEvent.CM_DELETED, null, pid ) ); + + assertFalse("Configuration file should be deleted", file.isFile()); + } + + public void testSetConfiguration() throws Exception + { + EasyMock.expect(mockBundleContext.getProperty(DirectoryWatcher.CONFIG_ENCODING)).andReturn(null); + EasyMock.expect(mockBundleContext.getProperty(DirectoryWatcher.LOG_DEFAULT)).andReturn(null); + EasyMock.expect(mockBundleContext.getProperty(DirectoryWatcher.LOG_LEVEL)).andReturn(null); + EasyMock.expect(mockConfiguration.getProperties()).andReturn(new Hashtable()); + EasyMock.reportMatcher(new IArgumentMatcher() + { + public boolean matches( Object argument ) + { + return ((Dictionary) argument).get("testkey").equals("testvalue"); + } + + public void appendTo(StringBuffer buffer) + { + buffer.append(""); + } + } ); + mockConfiguration.update(new Hashtable()); + EasyMock.expect(mockConfigurationAdmin.listConfigurations((String) EasyMock.anyObject())) + .andReturn(null); + EasyMock.expect(mockConfigurationAdmin.getConfiguration("firstcfg", "?")) + .andReturn(mockConfiguration); + EasyMock.replay(mockConfiguration, mockConfigurationAdmin, mockBundleContext); + + ConfigInstaller ci = new ConfigInstaller( mockBundleContext, mockConfigurationAdmin, new FileInstall() ); + + assertTrue( ci.setConfig( new File( "src/test/resources/watched/firstcfg.cfg" ) ) ); + + EasyMock.verify(mockConfiguration, mockConfigurationAdmin, mockBundleContext); + } + + public void testShouldSaveConfig() + { + final AtomicReference disable = new AtomicReference(); + final AtomicReference enable = new AtomicReference(); + + EasyMock.expect(mockBundleContext.getProperty(DirectoryWatcher.DISABLE_CONFIG_SAVE)).andAnswer( + new IAnswer() { + public String answer() throws Throwable { + return disable.get() != null ? disable.get().toString() : null; + } + } + ).anyTimes(); + EasyMock.expect(mockBundleContext.getProperty(DirectoryWatcher.ENABLE_CONFIG_SAVE)).andAnswer( + new IAnswer() { + public String answer() throws Throwable { + return enable.get() != null ? enable.get().toString() : null; + } + } + ).anyTimes(); + EasyMock.replay(mockConfiguration, mockConfigurationAdmin, mockBundleContext); + + ConfigInstaller ci = new ConfigInstaller( mockBundleContext, mockConfigurationAdmin, new FileInstall() ); + + disable.set(null); + enable.set(null); + assertTrue( ci.shouldSaveConfig() ); + + disable.set(Boolean.FALSE); + enable.set(null); + assertFalse( ci.shouldSaveConfig() ); + + disable.set(Boolean.TRUE); + enable.set(null); + assertTrue( ci.shouldSaveConfig() ); + + disable.set(null); + enable.set(Boolean.FALSE); + assertFalse( ci.shouldSaveConfig() ); + + disable.set(Boolean.FALSE); + enable.set(Boolean.FALSE); + assertFalse( ci.shouldSaveConfig() ); + + disable.set(Boolean.TRUE); + enable.set(Boolean.FALSE); + assertFalse( ci.shouldSaveConfig() ); + + disable.set(null); + enable.set(Boolean.TRUE); + assertTrue( ci.shouldSaveConfig() ); + + disable.set(Boolean.FALSE); + enable.set(Boolean.TRUE); + assertTrue( ci.shouldSaveConfig() ); + + disable.set(Boolean.TRUE); + enable.set(Boolean.TRUE); + assertTrue( ci.shouldSaveConfig() ); + + EasyMock.verify(mockConfiguration, mockConfigurationAdmin, mockBundleContext); + } + +} diff --git a/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/DirectoryWatcherTest.java b/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/DirectoryWatcherTest.java new file mode 100644 index 00000000000..6769c4d3d5f --- /dev/null +++ b/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/DirectoryWatcherTest.java @@ -0,0 +1,451 @@ +/* + * 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.fileinstall.internal; + + +import java.io.File; +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; +import junit.framework.TestCase; +import org.apache.felix.fileinstall.ArtifactListener; +import org.easymock.EasyMock; +import org.easymock.IMocksControl; +import org.junit.Assert; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleListener; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.startlevel.FrameworkStartLevel; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.service.log.LogService; + + +/** + * Test class for the DirectoryWatcher + */ +public class DirectoryWatcherTest extends TestCase +{ + + private final static String TEST = "test.key"; + Map props = new Hashtable(); + DirectoryWatcher dw; + BundleContext mockBundleContext; + Bundle mockBundle; + Bundle mockSysBundle; + FrameworkStartLevel mockStartLevel; + + + protected void setUp() throws Exception + { + super.setUp(); + IMocksControl ctrl = EasyMock.createControl(); + ctrl.makeThreadSafe(true); + mockBundleContext = ctrl.createMock(BundleContext.class); + mockBundle = EasyMock.createNiceMock(Bundle.class); + mockSysBundle = EasyMock.createNiceMock(Bundle.class); + mockStartLevel = EasyMock.createMock(FrameworkStartLevel.class); + props.put( DirectoryWatcher.DIR, new File( "target/load" ).getAbsolutePath() ); + + // Might get called, but most of the time it doesn't matter whether they do or don't. + EasyMock.expect(mockBundleContext.getProperty(DirectoryWatcher.LOG_DEFAULT)) + .andStubReturn(null); + EasyMock.expect(mockBundleContext.getProperty(DirectoryWatcher.LOG_LEVEL)) + .andStubReturn(null); + EasyMock.expect(mockBundleContext.getServiceReference(LogService.class.getName())) + .andStubReturn(null); + EasyMock.expect(mockBundleContext.getBundle()).andReturn(mockBundle).anyTimes(); + EasyMock.expect(mockBundleContext.getBundle(Constants.SYSTEM_BUNDLE_LOCATION)).andReturn(mockSysBundle).anyTimes(); + EasyMock.expect(mockSysBundle.getState()).andReturn(Bundle.ACTIVE).anyTimes(); + EasyMock.expect(mockSysBundle.adapt(FrameworkStartLevel.class)).andReturn(mockStartLevel).anyTimes(); + EasyMock.expect(mockStartLevel.getStartLevel()).andReturn(50).anyTimes(); + } + + + public void testGetLongWithNonExistentProperty() + { + mockBundleContext.addBundleListener((BundleListener) org.easymock.EasyMock.anyObject()); + EasyMock.replay(mockBundleContext, mockBundle, mockSysBundle, mockStartLevel); + dw = new DirectoryWatcher( new FileInstall(), props, mockBundleContext ); + assertEquals( "getLong gives the default value for non-existing properties", 100, dw.getLong( props, TEST, 100 ) ); + EasyMock.verify(mockBundleContext); + } + + + public void testGetLongWithExistentProperty() + { + props.put( TEST, "33" ); + + mockBundleContext.addBundleListener((BundleListener) org.easymock.EasyMock.anyObject()); + EasyMock.replay(mockBundleContext, mockBundle, mockSysBundle, mockStartLevel); + dw = new DirectoryWatcher( new FileInstall(), props, mockBundleContext ); + assertEquals( "getLong retrieves the right property value", 33, dw.getLong( props, TEST, 100 ) ); + EasyMock.verify(mockBundleContext); + } + + + public void testGetLongWithIncorrectValue() + { + props.put( TEST, "incorrect" ); + + mockBundleContext.addBundleListener((BundleListener) org.easymock.EasyMock.anyObject()); + EasyMock.replay(mockBundleContext, mockBundle, mockSysBundle, mockStartLevel); + dw = new DirectoryWatcher( new FileInstall(), props, mockBundleContext ); + assertEquals( "getLong retrieves the right property value", 100, dw.getLong( props, TEST, 100 ) ); + EasyMock.verify(mockBundleContext); + } + + + public void testGetBooleanWithNonExistentProperty() + { + mockBundleContext.addBundleListener((BundleListener) org.easymock.EasyMock.anyObject()); + EasyMock.replay(mockBundleContext, mockBundle, mockSysBundle, mockStartLevel); + dw = new DirectoryWatcher( new FileInstall(), props, mockBundleContext ); + assertEquals( "getBoolean gives the default value for non-existing properties", true, dw.getBoolean( props, TEST, true ) ); + EasyMock.verify(mockBundleContext); + } + + + public void testGetBooleanWithExistentProperty() + { + props.put( TEST, "true" ); + + mockBundleContext.addBundleListener((BundleListener) org.easymock.EasyMock.anyObject()); + EasyMock.replay(mockBundleContext, mockBundle, mockSysBundle, mockStartLevel); + dw = new DirectoryWatcher( new FileInstall(), props, mockBundleContext ); + assertEquals( "getBoolean retrieves the right property value", true, dw.getBoolean( props, TEST, false ) ); + EasyMock.verify(mockBundleContext); + } + + + public void testGetBooleanWithIncorrectValue() + { + props.put( TEST, "incorrect" ); + + mockBundleContext.addBundleListener((BundleListener) org.easymock.EasyMock.anyObject()); + EasyMock.replay(mockBundleContext, mockBundle, mockSysBundle, mockStartLevel); + dw = new DirectoryWatcher( new FileInstall(), props, mockBundleContext ); + assertEquals( "getBoolean retrieves the right property value", false, dw.getBoolean( props, TEST, true ) ); + EasyMock.verify(mockBundleContext); + } + + + public void testGetFileWithNonExistentProperty() + { + mockBundleContext.addBundleListener((BundleListener) org.easymock.EasyMock.anyObject()); + EasyMock.replay(mockBundleContext, mockBundle, mockSysBundle, mockStartLevel); + dw = new DirectoryWatcher( new FileInstall(), props, mockBundleContext ); + assertEquals( "getFile gives the default value for non-existing properties", new File("tmp"), dw.getFile( props, TEST, new File("tmp") ) ); + EasyMock.verify(mockBundleContext); + } + + + public void testGetFileWithExistentProperty() + { + props.put( TEST, "test" ); + + mockBundleContext.addBundleListener((BundleListener) org.easymock.EasyMock.anyObject()); + EasyMock.replay(mockBundleContext, mockBundle, mockSysBundle, mockStartLevel); + dw = new DirectoryWatcher( new FileInstall(), props, mockBundleContext ); + assertEquals( "getBoolean retrieves the right property value", new File("test"), dw.getFile( props, TEST, new File("tmp") ) ); + EasyMock.verify(mockBundleContext); + } + + + public void testParameterAfterInitialization() + { + props.put( DirectoryWatcher.POLL, "500" ); + props.put( DirectoryWatcher.LOG_LEVEL, "1" ); + props.put( DirectoryWatcher.START_NEW_BUNDLES, "false" ); + props.put( DirectoryWatcher.DIR, new File( "src/test/resources" ).getAbsolutePath() ); + props.put( DirectoryWatcher.TMPDIR, new File( "src/test/resources" ).getAbsolutePath() ); + props.put( DirectoryWatcher.FILTER, ".*\\.cfg" ); + + mockBundleContext.addBundleListener((BundleListener) org.easymock.EasyMock.anyObject()); + EasyMock.replay(mockBundleContext, mockBundle, mockSysBundle, mockStartLevel); + + dw = new DirectoryWatcher( new FileInstall(), props, mockBundleContext ); + + assertEquals( "POLL parameter correctly read", 500l, dw.poll ); + assertEquals( "LOG_LEVEL parameter correctly read", 1, dw.logLevel ); + assertTrue("DIR parameter correctly read", dw.watchedDirectory.getAbsolutePath().endsWith( + "src" + File.separatorChar + "test" + File.separatorChar + "resources")); + assertTrue( "TMPDIR parameter correctly read", dw.tmpDir.getAbsolutePath().endsWith( + "src" + File.separatorChar + "test" + File.separatorChar + "resources" ) ); + assertEquals("START_NEW_BUNDLES parameter correctly read", false, dw.startBundles); + assertEquals( "FILTER parameter correctly read", ".*\\.cfg", dw.filter ); + EasyMock.verify(mockBundleContext); + } + + + public void testDefaultParametersAreSetAfterEmptyInitialization() + { + props.put( DirectoryWatcher.DIR, new File( "src/test/resources" ).getAbsolutePath() ); + + mockBundleContext.addBundleListener((BundleListener) org.easymock.EasyMock.anyObject()); + EasyMock.replay(mockBundleContext, mockBundle, mockSysBundle, mockStartLevel); + + dw = new DirectoryWatcher( new FileInstall(), props, mockBundleContext ); + + assertTrue( "DIR parameter correctly read", dw.watchedDirectory.getAbsolutePath().endsWith( + "src" + File.separatorChar + "test" + File.separatorChar + "resources" ) ); + assertEquals("Default POLL parameter correctly read", 2000l, dw.poll); + assertEquals( "Default LOG_LEVEL parameter correctly read", 1, dw.logLevel ); + assertTrue("Default TMPDIR parameter correctly read", dw.tmpDir.getAbsolutePath().startsWith( + new File(System.getProperty("java.io.tmpdir")).getAbsolutePath())); + assertEquals("Default START_NEW_BUNDLES parameter correctly read", true, dw.startBundles); + assertEquals( "Default FILTER parameter correctly read", null, dw.filter ); + EasyMock.verify(mockBundleContext); + } + + + public void testIsFragment() throws Exception + { + mockBundleContext.addBundleListener((BundleListener) org.easymock.EasyMock.anyObject()); + BundleRevision mockBundleRevision = EasyMock.createNiceMock(BundleRevision.class); + EasyMock.expect(mockBundle.adapt(BundleRevision.class)).andReturn(mockBundleRevision); + EasyMock.expect(mockBundleRevision.getTypes()) + .andReturn(BundleRevision.TYPE_FRAGMENT); + EasyMock.replay(mockBundleContext, mockBundle, mockBundleRevision, mockSysBundle, mockStartLevel); + + dw = new DirectoryWatcher( new FileInstall(), props, mockBundleContext ); + + assertTrue( "Fragment type correctly retrieved from Package Admin service", dw.isFragment( mockBundle ) ); + + EasyMock.verify(mockBundleContext); + } + + public void testInvalidTempDir() throws Exception + { + String oldTmpDir = System.getProperty("java.io.tmpdir"); + + try + { + File parent = new File("target/tmp"); + parent.mkdirs(); + parent.setWritable(false, false); + File tmp = new File(parent, "tmp"); + System.setProperty("java.io.tmpdir", tmp.toString()); + + mockBundleContext.addBundleListener((BundleListener) org.easymock.EasyMock.anyObject()); + EasyMock.expect(mockBundleContext.createFilter((String) EasyMock.anyObject())) + .andReturn(null); + + BundleRevision mockBundleRevision = EasyMock.createNiceMock(BundleRevision.class); + EasyMock.expect(mockBundle.adapt(BundleRevision.class)).andReturn(mockBundleRevision); + EasyMock.expect(mockBundleRevision.getTypes()) + .andReturn(BundleRevision.TYPE_FRAGMENT); + EasyMock.replay(mockBundleContext, mockBundle, mockBundleRevision, mockSysBundle, mockStartLevel); + + try + { + dw = new DirectoryWatcher( new FileInstall(), props, mockBundleContext ); + fail("Expected an IllegalStateException"); + } + catch (IllegalStateException e) + { + // expected + } + } + finally + { + System.setProperty("java.io.tmpdir", oldTmpDir); + } + } + + /** + * Test the {@link DirectoryWatcher#initializeCurrentManagedBundles()} in conjunction with a non opaque Bundle Location. + * Assert that a new created {@link Artifact} will be added into the {@link DirectoryWatcher#currentManagedArtifacts}. + * + * The {@link DirectoryWatcher#process(java.util.Set)} execution will not be called. + * This test breaks the execution in {@link Scanner#initialize(java.util.Map)}. + * @throws URISyntaxException + */ + public void testInitializeCurrentManagedBundlesNonOpaqueURIOnBundleLocation() throws URISyntaxException + { + final RuntimeException expectedException = new RuntimeException("expected exception to break execution on defined point."); + final File watchedDirectoryFile = new File("src/test/resources/watched"); + final String watchedDirectoryPath = watchedDirectoryFile.getAbsolutePath(); + + final String bundleFileName = "firstjar.jar"; + final File bundleFile = new File(watchedDirectoryPath,bundleFileName); + final String bundleLocation = "file:"+watchedDirectoryPath+'/'+bundleFileName; + + // break execution + final Scanner scanner = new Scanner(watchedDirectoryFile) + { + public void initialize(Map checksums) + { + throw expectedException; + } + }; + + mockBundleContext.addBundleListener((BundleListener) org.easymock.EasyMock.anyObject()); + EasyMock.expect(mockBundleContext.getBundles()).andReturn(new Bundle[]{mockBundle}); + EasyMock.expect(mockBundleContext.getDataFile((String) EasyMock.anyObject())).andReturn(null).anyTimes(); + EasyMock.expect(mockBundle.getLocation()).andReturn(bundleLocation).anyTimes(); + + EasyMock.replay(mockBundleContext, mockBundle, mockSysBundle, mockStartLevel); + + props.put(DirectoryWatcher.DIR, watchedDirectoryPath); + + dw = new DirectoryWatcher(new FileInstall(), props, mockBundleContext); + dw.noInitialDelay = true; + dw.scanner = scanner; + try { + dw.start(); + } + catch(RuntimeException e) + { + assertEquals(e, expectedException); + } + assertEquals(1, dw.currentManagedArtifacts.size()); + assertTrue(dw.currentManagedArtifacts.containsKey(bundleFile)); + + EasyMock.verify(mockBundleContext, mockBundle); + } + + /** + * Test the {@link DirectoryWatcher#initializeCurrentManagedBundles()} in conjunction with a opaque Bundle Location. + * Assert that a new created {@link Artifact} will be added into the {@link DirectoryWatcher#currentManagedArtifacts}. + * + * The {@link DirectoryWatcher#process(java.util.Set)} execution will not be called. + * This test breaks the execution in {@link Scanner#initialize(java.util.Map)}. + * @throws URISyntaxException + */ + public void testInitializeCurrentManagedBundlesOpaqueURIOnBundleLocation() throws URISyntaxException + { + final RuntimeException expectedException = new RuntimeException("expected exception to break execution on defined point."); + final File watchedDirectoryFile = new File("src/test/resources/watched"); + final String watchedDirectoryPath = watchedDirectoryFile.getAbsolutePath(); + + final String bundleFileName = "firstjar.jar"; + final File bundleFile = new File(watchedDirectoryPath,bundleFileName); + final String bundleLocation = "blueprint:file:"+watchedDirectoryPath+'/'+bundleFileName+"$Bundle-SymbolicName=foo&Bundle-Version=1.0"; + + // break execution + Scanner scanner = new Scanner(watchedDirectoryFile) + { + public void initialize(Map checksums) + { + throw expectedException; + } + }; + + mockBundleContext.addBundleListener((BundleListener) org.easymock.EasyMock.anyObject()); + EasyMock.expect(mockBundleContext.getBundles()).andReturn(new Bundle[]{mockBundle}); + EasyMock.expect(mockBundleContext.getDataFile((String) EasyMock.anyObject())).andReturn(null).anyTimes(); + EasyMock.expect(mockBundle.getLocation()).andReturn(bundleLocation).anyTimes(); + + EasyMock.replay(mockBundleContext, mockBundle, mockSysBundle, mockStartLevel); + + props.put(DirectoryWatcher.DIR, watchedDirectoryPath); + + dw = new DirectoryWatcher(new FileInstall(), props, mockBundleContext); + dw.noInitialDelay = true; + dw.scanner = scanner; + try { + dw.start(); + } + catch(RuntimeException e) + { + assertEquals(e, expectedException); + } + assertEquals(1, dw.currentManagedArtifacts.size()); + assertTrue(dw.currentManagedArtifacts.containsKey(bundleFile)); + + EasyMock.verify(mockBundleContext, mockBundle); + } + + /** + * Test the {@link DirectoryWatcher#process(java.util.Set) } in conjunction with a opaque Bundle Location. + * Assert that no bundle refresh will be called. + * @throws URISyntaxException + */ + public void testProcessOpaqueURIOnBundleLocation() throws URISyntaxException + { + final RuntimeException expectedException = new RuntimeException("expected exception to break execution on defined point."); + final File watchedDirectoryFile = new File("src/test/resources/watched"); + final String watchedDirectoryPath = watchedDirectoryFile.getAbsolutePath(); + + final String bundleFileName = "firstjar.jar"; + final File bundleFile = new File(watchedDirectoryPath,bundleFileName); + final String bundleLocation = "blueprint:file:"+watchedDirectoryPath+'/'+bundleFileName; + + final Scanner scanner = new Scanner(watchedDirectoryFile) + { + // bypass filesystem scan and return expected bundle file + public Set scan(boolean reportImmediately) + { + Set fileSet = new HashSet(1); + fileSet.add(bundleFile); + return fileSet; + } + }; + + final ArtifactListener mockArtifactListener = EasyMock.createNiceMock(ArtifactListener.class); + EasyMock.expect(mockArtifactListener.canHandle(bundleFile)).andReturn(Boolean.TRUE).anyTimes(); + final ServiceReference mockServiceReference = EasyMock.createNiceMock(ServiceReference.class); + + // simulate known/installed bundles + mockBundleContext.addBundleListener((BundleListener) org.easymock.EasyMock.anyObject()); + EasyMock.expect(mockBundleContext.getBundles()).andReturn(new Bundle[]{mockBundle}); + EasyMock.expect(mockBundleContext.getDataFile((String) EasyMock.anyObject())).andReturn(null).anyTimes(); + EasyMock.expect(mockBundle.getLocation()).andReturn(bundleLocation).anyTimes(); + + EasyMock.replay(mockBundleContext, mockBundle,mockServiceReference, mockArtifactListener, mockSysBundle, mockStartLevel); + + final Artifact artifact = new Artifact(); + artifact.setBundleId(42); + artifact.setChecksum(0); + artifact.setListener(mockArtifactListener); + artifact.setPath(bundleFile); + + FileInstall fileInstall = new FileInstall(); + fileInstall.listeners.put(mockServiceReference, mockArtifactListener); + + props.put(DirectoryWatcher.DIR, watchedDirectoryPath); + + dw = new DirectoryWatcher(fileInstall, props, mockBundleContext) { + + void refresh(Collection bundles) throws InterruptedException { + Assert.fail("bundle refresh called"); + } + + }; + dw.noInitialDelay = true; + // add expected bundle and artifact to the current managed artifacts + dw.currentManagedArtifacts.put(bundleFile, artifact); + dw.scanner = scanner; + try { + dw.start(); + } + catch(RuntimeException e) + { + assertEquals(e, expectedException); + } + + EasyMock.verify(mockBundleContext, mockBundle,mockServiceReference, mockArtifactListener); + } + +} diff --git a/fileinstall/src/test/resources/watched/firstcfg.cfg b/fileinstall/src/test/resources/watched/firstcfg.cfg new file mode 100644 index 00000000000..80fab5942d6 --- /dev/null +++ b/fileinstall/src/test/resources/watched/firstcfg.cfg @@ -0,0 +1,19 @@ +# +# 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. +# +testkey=testvalue diff --git a/fileinstall/src/test/resources/watched/firstjar.jar b/fileinstall/src/test/resources/watched/firstjar.jar new file mode 100644 index 00000000000..2a20387b7eb --- /dev/null +++ b/fileinstall/src/test/resources/watched/firstjar.jar @@ -0,0 +1 @@ +only a fake jar for testing \ No newline at end of file diff --git a/fileinstall/src/test/resources/watched/secondjar.jar b/fileinstall/src/test/resources/watched/secondjar.jar new file mode 100644 index 00000000000..2a20387b7eb --- /dev/null +++ b/fileinstall/src/test/resources/watched/secondjar.jar @@ -0,0 +1 @@ +only a fake jar for testing \ No newline at end of file diff --git a/framework.security/doc/changelog.txt b/framework.security/doc/changelog.txt new file mode 100644 index 00000000000..1f77704c6ec --- /dev/null +++ b/framework.security/doc/changelog.txt @@ -0,0 +1,72 @@ +Changes from 2.6.0 to 2.6.1 +--------------------------- + +** Bug + * [FELIX-5906] - Installing Manifest only bundles causes ArrayIndexOutOfBoundsException + +Changes from 2.4.0 to 2.6.0 +--------------------------- + +** Improvement + * Update to latest framework version 5.6.0 + +Changes from 2.2.0 to 2.4.0 +--------------------------- +** Improvement + * Update to latest framework version 4.4.0 + +Changes form 2.0.1 to 2.2.0 +--------------------------- +** Bug + * [FELIX-3498] - BundleInputStream is not able to handle bundles that only have a MANIFEST.MF entry + * [FELIX-3603] - Resources in META-INF/xxx/ fodlers in a signed bundle should be checked + * [FELIX-3604] - No error log if the certificate is not valid + * [FELIX-3893] - Bundle in cache doesn't pass security check anymore. + +Changes from 2.0.0 to 2.0.1 +--------------------------- + +** Bug + * [FELIX-3196] - Security Problem: Getting full file access within the cache directory from one Bundle + * [FELIX-3221] - NPE when uninstall a bundle on enabled OSGi security + +Changes from 1.4.2 to 2.0.0 +--------------------------- +** Bug + * [FELIX-2648] - Incompatible security provider state when switching from 2.0.4 to 3.0.3 + * [FELIX-2922] - ArrayIndexOutOfBoundsException when specifying no name + * [FELIX-3004] - felix.security does not work with exploded jars + * [FELIX-3101] - ClassCastException in Permissions class + +** Improvement + * Update to latest framework version 4.0.0 + +Changes from 1.4.1 to 1.4.2 +--------------------------- + +** Improvement + * Update to latest framework version 3.2.0 + +Changes from 1.4.0 to 1.4.1 +--------------------------- + +** Bug + * [FELIX-2739] - [Security] Recent framework changes have broken the security provider. + +Changes from 1.2.0 to 1.4.0 +--------------------------- + +** Improvement + * [FELIX-2547] - Implement conditionalpermissioninfo encoding/decoding to match the spec. + + +Changes from 1.0.0 to 1.2.0 +--------------------------- + +** Task + * [FELIX-2294] - Patch Framework Security for compatibility + +1.0.0 +----- + +** Initial Release diff --git a/framework.security/pom.xml b/framework.security/pom.xml new file mode 100644 index 00000000000..134a13b86f2 --- /dev/null +++ b/framework.security/pom.xml @@ -0,0 +1,102 @@ + + + + org.apache.felix + felix-parent + 5 + ../pom/pom.xml + + 4.0.0 + bundle + Apache Felix Security Provider + org.apache.felix.framework.security + 2.7.0-SNAPSHOT + + This bundle provides an implementation of the OSGi security for Apache Felix. + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/framework.security + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/framework.security + http://svn.apache.org/repos/asf/felix/framework.security + + + + org.osgi + org.osgi.core + 5.0.0 + + + ${pom.groupId} + org.apache.felix.framework + 6.1.0-SNAPSHOT + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.3 + 1.3 + + + + org.apache.felix + maven-bundle-plugin + 2.3.5 + true + + + ${pom.artifactId} + The Apache Software Foundation + org.osgi.service.permissionadmin, org.osgi.service.condpermadmin + org.apache.felix.framework.* + !* + system.bundle; extension:=framework + org.apache.felix.framework.SecurityActivator + + + org.apache.felix.framework + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + false + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + + diff --git a/framework.security/src/main/appended-resources/META-INF/DEPENDENCIES b/framework.security/src/main/appended-resources/META-INF/DEPENDENCIES new file mode 100644 index 00000000000..cce7c0b20ef --- /dev/null +++ b/framework.security/src/main/appended-resources/META-INF/DEPENDENCIES @@ -0,0 +1,16 @@ +I. Included Third-Party Software + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 diff --git a/framework.security/src/main/appended-resources/META-INF/LICENSE b/framework.security/src/main/appended-resources/META-INF/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/framework.security/src/main/appended-resources/META-INF/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/framework.security/src/main/appended-resources/META-INF/NOTICE b/framework.security/src/main/appended-resources/META-INF/NOTICE new file mode 100644 index 00000000000..3db2e431ed2 --- /dev/null +++ b/framework.security/src/main/appended-resources/META-INF/NOTICE @@ -0,0 +1,4 @@ +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. diff --git a/framework.security/src/main/java/org/apache/felix/framework/FakeBundle.java b/framework.security/src/main/java/org/apache/felix/framework/FakeBundle.java new file mode 100644 index 00000000000..f7b3d9ba2dc --- /dev/null +++ b/framework.security/src/main/java/org/apache/felix/framework/FakeBundle.java @@ -0,0 +1,207 @@ +/* + * 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.framework; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Map; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.Version; + +/** + * + */ +public class FakeBundle extends BundleImpl implements Bundle +{ + private final Map m_certs; + + public FakeBundle(Map certs) + { + m_certs = Collections.unmodifiableMap(certs); + } + + public Enumeration findEntries(String arg0, String arg1, boolean arg2) + { + return null; + } + + public BundleContext getBundleContext() + { + return null; + } + + public long getBundleId() + { + return -1; + } + + public URL getEntry(String arg0) + { + return null; + } + + public Enumeration getEntryPaths(String arg0) + { + return null; + } + + public Dictionary getHeaders() + { + return new Hashtable(); + } + + public Dictionary getHeaders(String arg0) + { + return new Hashtable(); + } + + public long getLastModified() + { + return 0; + } + + public String getLocation() + { + return ""; + } + + public ServiceReference[] getRegisteredServices() + { + return null; + } + + public URL getResource(String arg0) + { + return null; + } + + public Enumeration getResources(String arg0) throws IOException + { + return null; + } + + public ServiceReference[] getServicesInUse() + { + return null; + } + + public Map getSignerCertificates(int arg0) + { + return m_certs; + } + + public int getState() + { + return Bundle.UNINSTALLED; + } + + public String getSymbolicName() + { + return null; + } + + public Version getVersion() + { + return Version.emptyVersion; + } + + public boolean hasPermission(Object arg0) + { + return false; + } + + public Class loadClass(String arg0) throws ClassNotFoundException + { + return null; + } + + public void start() throws BundleException + { + throw new IllegalStateException(); + } + + public void start(int arg0) throws BundleException + { + throw new IllegalStateException(); + } + + public void stop() throws BundleException + { + throw new IllegalStateException(); + } + + public void stop(int arg0) throws BundleException + { + throw new IllegalStateException(); + } + + public void uninstall() throws BundleException + { + throw new IllegalStateException(); + } + + public void update() throws BundleException + { + throw new IllegalStateException(); + } + + public void update(InputStream arg0) throws BundleException + { + throw new IllegalStateException(); + } + + public boolean equals(Object o) + { + return this == o; + } + + public int hashCode() + { + return System.identityHashCode(this); + } + + public int compareTo(Bundle o) { + // TODO Auto-generated method stub + return 0; + } + + public Object adapt(Class arg0) { + // TODO Auto-generated method stub + return null; + } + + public File getDataFile(String arg0) { + // TODO Auto-generated method stub + return null; + } + + public int compareTo(Object t) + { + throw new UnsupportedOperationException("Not supported yet."); + } +} diff --git a/framework.security/src/main/java/org/apache/felix/framework/SecurityActivator.java b/framework.security/src/main/java/org/apache/felix/framework/SecurityActivator.java new file mode 100644 index 00000000000..1106423cd17 --- /dev/null +++ b/framework.security/src/main/java/org/apache/felix/framework/SecurityActivator.java @@ -0,0 +1,234 @@ +/* + * 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.framework; + +import java.io.File; +import java.io.IOException; +import java.util.StringTokenizer; + +import org.apache.felix.framework.ext.SecurityProvider; +import org.apache.felix.framework.security.SecurityConstants; +import org.apache.felix.framework.security.condpermadmin.ConditionalPermissionAdminImpl; +import org.apache.felix.framework.security.permissionadmin.PermissionAdminImpl; +import org.apache.felix.framework.security.util.Conditions; +import org.apache.felix.framework.security.util.LocalPermissions; +import org.apache.felix.framework.security.util.Permissions; +import org.apache.felix.framework.security.util.PropertiesCache; +import org.apache.felix.framework.util.SecureAction; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.service.condpermadmin.ConditionalPermissionAdmin; +import org.osgi.service.permissionadmin.PermissionAdmin; + +/** + *

      + * This Felix specific activator installs a security provider with the Felix + * framework. The security settings can be changed via the + * {@link PermissionAdmin} and/or the {@link ConditionalPermissionAdmin} + * services that may be published by this class. + *

      + *

      + * Permission informations as well as caching data will be stored in several + * files in a directory called security obtained by a call to + * {@link BundleContext#getDataFile(String))}. + *

      + *

      + * The following properties are recognized: + *

      + * {@link SecurityConstants#ENABLE_PERMISSIONADMIN_PROP} - Whether or not ( + * true|false) to publish a{@link ConditionalPermissionAdmin} + * service. The default is + * {@link SecurityConstants#ENABLE_PERMISSIONADMIN_VALUE}. + *

      + *

      + * {@link SecurityConstants#ENABLE_CONDPERMADMIN_PROP} - Whether or not ( + * true|false) to publish a{@link ConditionalPermissionAdmin} + * service. The default is {@link SecurityConstants#ENABLE_CONDPERMADMIN_VALUE}. + *

      + *

      + * {@link SecurityConstants#KEYSTORE_FILE_PROP} - The keystore URL(s) to use as + * trusted CA stores. The urls must be separated by a guard (i.e., |). + * The default is {@link SecurityConstants#KEYSTORE_FILE_VALUE}. + *

      + *

      + * {@link SecurityConstants#KEYSTORE_PASS_PROP} - The keystore password(s) to + * use for the given keystores. The passwords must be separated by a guard + * (i.e., |).The default is + * {@link SecurityConstants#KEYSTORE_PASS_VALUE}. + *

      + *

      + * {@link SecurityConstants#KEYSTORE_TYPE_PROP} - The keystore type(s) to use + * for the given keystores. The types must be separated by a guard (i.e., + * |).The default is {@link SecurityConstants#KEYSTORE_TYPE_VALUE}. + *

      + *

      + * {@link SecurityConstants#CRL_FILE_PROP} - The CRL URL(s) to use for revoked + * certificates. The urls must be separated by a guard (i.e., |). The + * default is {@link SecurityConstants#CRL_FILE_VALUE}. + *

      + *

      + */ +/* + * TODO: using a string for passwords is bad. We need to investigate + * alternatives. + * + * TODO: we might want to allow for the recognized properties to change without + * a restart. This is trick because we can not publish a managed service due to + * not being able to import as we are an extension bundle. + */ +public final class SecurityActivator implements BundleActivator +{ + public synchronized void start(BundleContext context) throws Exception + { + PermissionAdminImpl pai = null; + + SecureAction action = new SecureAction(); + + Permissions permissions = new Permissions(context, action); + + File tmp = context.getDataFile("security" + File.separator + "tmp"); + if ((tmp == null) || (!tmp.isDirectory() && !tmp.mkdirs())) + { + throw new IOException("Can't create tmp dir."); + } + // TODO: log something if we can not clean-up the tmp dir + File[] old = tmp.listFiles(); + if (old != null) + { + for (int i = 0; i < old.length; i++) + { + old[i].delete(); + } + } + + if ("TRUE".equalsIgnoreCase(getProperty(context, + SecurityConstants.ENABLE_PERMISSIONADMIN_PROP, + SecurityConstants.ENABLE_PERMISSIONADMIN_VALUE))) + { + File cache = context.getDataFile("security" + File.separator + + "pa.txt"); + if ((cache == null) || (!cache.isFile() && !cache.createNewFile())) + { + throw new IOException("Can't create cache file"); + } + pai = new PermissionAdminImpl(permissions, new PropertiesCache( + cache, tmp, action)); + } + + ConditionalPermissionAdminImpl cpai = null; + + if ("TRUE".equalsIgnoreCase(getProperty(context, + SecurityConstants.ENABLE_CONDPERMADMIN_PROP, + SecurityConstants.ENABLE_CONDPERMADMIN_VALUE))) + { + File cpaCache = context.getDataFile("security" + File.separator + + "cpa.txt"); + if ((cpaCache == null) + || (!cpaCache.isFile() && !cpaCache.createNewFile())) + { + throw new IOException("Can't create cache file"); + } + + LocalPermissions localPermissions = new LocalPermissions( + permissions); + + cpai = new ConditionalPermissionAdminImpl(permissions, + new Conditions(action), localPermissions, new PropertiesCache( + cpaCache, tmp, action), pai); + } + + if ((pai != null) || (cpai != null)) + { + String crlList = getProperty(context, + SecurityConstants.CRL_FILE_PROP, + SecurityConstants.CRL_FILE_VALUE); + String storeList = getProperty(context, + SecurityConstants.KEYSTORE_FILE_PROP, + SecurityConstants.KEYSTORE_FILE_VALUE); + String passwdList = getProperty(context, + SecurityConstants.KEYSTORE_PASS_PROP, + SecurityConstants.KEYSTORE_PASS_VALUE); + String typeList = getProperty(context, + SecurityConstants.KEYSTORE_TYPE_PROP, + SecurityConstants.KEYSTORE_TYPE_VALUE); + String osgi_keystores = getProperty(context, + Constants.FRAMEWORK_TRUST_REPOSITORIES, null); + if (osgi_keystores != null) + { + StringTokenizer tok = new StringTokenizer(osgi_keystores, + File.pathSeparator); + + if (storeList.length() == 0) + { + storeList += "file:" + tok.nextToken(); + passwdList += " "; + typeList += "JKS"; + } + while (tok.hasMoreTokens()) + { + storeList += "|file:" + tok.nextToken(); + passwdList += "| "; + typeList += "|JKS"; + } + } + + StringTokenizer storeTok = new StringTokenizer(storeList, "|"); + StringTokenizer passwdTok = new StringTokenizer(passwdList, "|"); + StringTokenizer typeTok = new StringTokenizer(typeList, "|"); + + if ((storeTok.countTokens() != typeTok.countTokens()) + || (passwdTok.countTokens() != storeTok.countTokens())) + { + throw new BundleException( + "Each CACerts keystore must have one type and one passwd entry and vice versa."); + } + + SecurityProvider provider = new SecurityProviderImpl(crlList, + typeList, passwdList, storeList, pai, cpai, action, ((Felix) context.getBundle(0)).getLogger()); + + ((Felix) context.getBundle(0)).setSecurityProvider(provider); + } + + if (pai != null) + { + context.registerService(PermissionAdmin.class.getName(), pai, null); + } + + if (cpai != null) + { + context.registerService(ConditionalPermissionAdmin.class.getName(), + cpai, null); + } + } + + public synchronized void stop(BundleContext context) throws Exception + { + ((Felix) context.getBundle(0)).setSecurityProvider(null); + } + + private String getProperty(BundleContext context, String key, + String defaultValue) + { + String result = context.getProperty(key); + + return (result != null) ? result : defaultValue; + } +} diff --git a/framework.security/src/main/java/org/apache/felix/framework/SecurityProviderImpl.java b/framework.security/src/main/java/org/apache/felix/framework/SecurityProviderImpl.java new file mode 100644 index 00000000000..405f9092f80 --- /dev/null +++ b/framework.security/src/main/java/org/apache/felix/framework/SecurityProviderImpl.java @@ -0,0 +1,135 @@ +/* + * 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.framework; + +import java.security.Permission; +import java.security.ProtectionDomain; + +import org.apache.felix.framework.Logger; +import org.apache.felix.framework.ext.SecurityProvider; +import org.apache.felix.framework.security.condpermadmin.ConditionalPermissionAdminImpl; +import org.apache.felix.framework.security.permissionadmin.PermissionAdminImpl; +import org.apache.felix.framework.security.util.TrustManager; +import org.apache.felix.framework.security.verifier.BundleDNParser; +import org.apache.felix.framework.util.SecureAction; +import org.osgi.framework.Bundle; +import org.osgi.framework.wiring.BundleRevision; + +/** + * This class is the entry point to the security. It is used to determine + * whether a given bundle is signed correctely and has permissions based on + * PermissionAdmin or ConditionalPermissionAdmin. + */ +public final class SecurityProviderImpl implements SecurityProvider +{ + private final BundleDNParser m_parser; + private final PermissionAdminImpl m_pai; + private final ConditionalPermissionAdminImpl m_cpai; + private final SecureAction m_action; + + SecurityProviderImpl(String crlList, String typeList, String passwdList, + String storeList, PermissionAdminImpl pai, + ConditionalPermissionAdminImpl cpai, SecureAction action, Logger logger) + { + m_pai = pai; + m_cpai = cpai; + m_action = action; + m_parser = new BundleDNParser(new TrustManager(crlList, typeList, + passwdList, storeList, m_action), logger); + } + + /** + * If the given bundle is signed but can not be verified (e.g., missing + * files) then throw an exception. + */ + public void checkBundle(Bundle bundle) throws Exception + { + BundleRevisionImpl module = (BundleRevisionImpl) bundle.adapt(BundleRevisionImpl.class); + m_parser.checkDNChains(module, module.getContent(), + Bundle.SIGNERS_TRUSTED); + } + + /** + * Get a signer matcher that can be used to match digital signed bundles. + */ + public Object getSignerMatcher(final Bundle bundle, int signersType) + { + BundleRevisionImpl module = (BundleRevisionImpl) bundle.adapt(BundleRevisionImpl.class); + return m_parser.getDNChains(module, module.getContent(), signersType); + } + + /** + * If we have a permissionadmin then ask that one first and have it decide + * in case there is a location bound. If not then either use its default + * permission in case there is no conditional permission admin or else ask + * that one. + */ + public boolean hasBundlePermission(ProtectionDomain bundleProtectionDomain, + Permission permission, boolean direct) + { + BundleProtectionDomain pd = (BundleProtectionDomain) bundleProtectionDomain; + BundleImpl bundle = pd.getBundle(); + BundleRevisionImpl module = (BundleRevisionImpl) pd.getRevision(); + + if (bundle.getBundleId() == 0) + { + return true; + } + + // System.out.println(info.getBundleId() + " - " + permission); + // TODO: using true, false, or null seems a bit awkward. Improve this. + Boolean result = null; + if (m_pai != null) + { + result = m_pai.hasPermission(bundle._getLocation(), pd.getBundle(), + permission, m_cpai, pd, module.getContent()); + } + + if (result != null) + { + if ((m_cpai != null) && !direct) + { + boolean allow = result.booleanValue(); + if (!allow) + { + m_cpai.clearPD(); + return false; + } + return m_cpai.handlePAHandle(pd); + } + return result.booleanValue(); + } + + if (m_cpai != null) + { + try + { + return m_cpai.hasPermission(module, module.getContent(), pd, + permission, direct, m_pai); + } + catch (Exception e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + return false; + } +} diff --git a/framework.security/src/main/java/org/apache/felix/framework/security/SecurityConstants.java b/framework.security/src/main/java/org/apache/felix/framework/security/SecurityConstants.java new file mode 100644 index 00000000000..790e1852d4e --- /dev/null +++ b/framework.security/src/main/java/org/apache/felix/framework/security/SecurityConstants.java @@ -0,0 +1,46 @@ +/* + * 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.framework.security; + +public interface SecurityConstants +{ + public static final String KEYSTORE_FILE_PROP = "felix.keystore"; + + public static final String KEYSTORE_FILE_VALUE = ""; + + public static final String KEYSTORE_TYPE_PROP = "felix.keystore.type"; + + public static final String KEYSTORE_TYPE_VALUE = ""; + + public static final String KEYSTORE_PASS_PROP = "felix.keystore.pass"; + + public static final String KEYSTORE_PASS_VALUE = ""; + + public static final String CRL_FILE_PROP = "felix.crl"; + + public static final String CRL_FILE_VALUE = ""; + + public static final String ENABLE_CONDPERMADMIN_PROP = "felix.security.conpermadmin"; + + public static final String ENABLE_CONDPERMADMIN_VALUE = "true"; + + public static final String ENABLE_PERMISSIONADMIN_PROP = "felix.security.permissionadmin"; + + public static final String ENABLE_PERMISSIONADMIN_VALUE = "true"; +} diff --git a/framework.security/src/main/java/org/apache/felix/framework/security/condpermadmin/ConditionalPermissionAdminImpl.java b/framework.security/src/main/java/org/apache/felix/framework/security/condpermadmin/ConditionalPermissionAdminImpl.java new file mode 100644 index 00000000000..970942489af --- /dev/null +++ b/framework.security/src/main/java/org/apache/felix/framework/security/condpermadmin/ConditionalPermissionAdminImpl.java @@ -0,0 +1,940 @@ +/* + * 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.framework.security.condpermadmin; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.net.URL; +import java.security.AccessControlContext; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Permission; +import java.security.Principal; +import java.security.ProtectionDomain; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.X509Certificate; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.Map.Entry; + +import org.apache.felix.framework.BundleProtectionDomain; +import org.apache.felix.framework.BundleRevisionImpl; +import org.apache.felix.framework.FakeBundle; +import org.apache.felix.framework.security.permissionadmin.PermissionAdminImpl; +import org.apache.felix.framework.security.util.Conditions; +import org.apache.felix.framework.security.util.LocalPermissions; +import org.apache.felix.framework.security.util.Permissions; +import org.apache.felix.framework.security.util.PropertiesCache; +import org.apache.felix.framework.util.manifestparser.NativeLibrary; + +/* +import org.apache.felix.moduleloader.ICapability; +import org.apache.felix.moduleloader.IContent; +import org.apache.felix.moduleloader.IModule; +import org.apache.felix.moduleloader.IRequirement; +import org.apache.felix.moduleloader.IWire; +*/ +import org.apache.felix.framework.cache.Content; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.Version; +import org.osgi.service.condpermadmin.ConditionInfo; +import org.osgi.service.condpermadmin.ConditionalPermissionAdmin; +import org.osgi.service.condpermadmin.ConditionalPermissionInfo; +import org.osgi.service.condpermadmin.ConditionalPermissionUpdate; +import org.osgi.service.permissionadmin.PermissionInfo; + +/** + * An implementation of the ConditionalPermissionAdmin service that doesn't need + * to have a framework specific security manager set. It use the DomainGripper + * to know what bundleprotectiondomains are expected. + */ +public final class ConditionalPermissionAdminImpl implements + ConditionalPermissionAdmin +{ + private static class OrderedHashMap extends HashMap + { + private final List m_order = new ArrayList(); + + public Object put(Object key, Object value) + { + Object result = super.put(key, value); + if (result != value) + { + m_order.remove(key); + m_order.add(key); + } + return result; + }; + + public void putAll(Map map) + { + for (Iterator iter = map.entrySet().iterator(); iter.hasNext();) + { + Entry entry = (Entry) iter.next(); + put(entry.getKey(), entry.getValue()); + } + }; + + public Set keySet() + { + return new AbstractSet() + { + public Iterator iterator() + { + return m_order.iterator(); + } + + public int size() + { + return m_order.size(); + } + + }; + }; + + public Set entrySet() + { + return new AbstractSet() + { + + public Iterator iterator() + { + return new Iterator() + { + Iterator m_iter = m_order.iterator(); + + public boolean hasNext() + { + return m_iter.hasNext(); + } + + public Object next() + { + final Object key = m_iter.next(); + return new Entry() + { + + public Object getKey() + { + return key; + } + + public Object getValue() + { + return get(key); + } + + public Object setValue(Object arg0) + { + throw new IllegalStateException( + "Not Implemented"); + } + }; + } + + public void remove() + { + throw new IllegalStateException("Not Implemented"); + } + + }; + } + + public int size() + { + return m_order.size(); + } + + }; + }; + + public Collection values() + { + List result = new ArrayList(); + for (Iterator iter = m_order.iterator(); iter.hasNext();) + { + result.add(super.get(iter.next())); + } + return result; + }; + + public Object remove(Object key) + { + Object result = super.remove(key); + if (result != null) + { + m_order.remove(key); + } + return result; + }; + + public void clear() + { + super.clear(); + m_order.clear(); + }; + }; + + private static final ConditionInfo[] EMPTY_CONDITION_INFO = new ConditionInfo[0]; + private static final PermissionInfo[] EMPTY_PERMISSION_INFO = new PermissionInfo[0]; + private final Map m_condPermInfos = new OrderedHashMap(); + private final PropertiesCache m_propertiesCache; + private final Permissions m_permissions; + private final Conditions m_conditions; + private final LocalPermissions m_localPermissions; + private final PermissionAdminImpl m_pai; + + public ConditionalPermissionAdminImpl(Permissions permissions, + Conditions condtions, LocalPermissions localPermissions, + PropertiesCache cache, PermissionAdminImpl pai) throws IOException + { + m_propertiesCache = cache; + m_permissions = permissions; + m_conditions = condtions; + m_localPermissions = localPermissions; + Map old = new OrderedHashMap(); + // Now try to restore the cache. + m_propertiesCache.read(ConditionalPermissionInfoImpl.class, old); + for (Iterator iter = old.entrySet().iterator(); iter.hasNext();) + { + Entry entry = (Entry) iter.next(); + String name = (String) entry.getKey(); + ConditionalPermissionInfoImpl cpi = ((ConditionalPermissionInfoImpl) entry + .getValue()); + m_condPermInfos.put(name, new ConditionalPermissionInfoImpl(name, + cpi._getConditionInfos(), cpi._getPermissionInfos(), this, cpi + .isAllow())); + } + m_pai = pai; + } + + public ConditionalPermissionInfo addConditionalPermissionInfo( + ConditionInfo[] conditions, PermissionInfo[] permissions) + { + Object sm = System.getSecurityManager(); + if (sm != null) + { + ((SecurityManager) sm).checkPermission(Permissions.ALL_PERMISSION); + } + ConditionalPermissionInfoImpl result = new ConditionalPermissionInfoImpl( + notNull(conditions), notNull(permissions), this, true); + + return write(result.getName(), result); + } + + ConditionalPermissionInfoImpl write(String name, + ConditionalPermissionInfoImpl cpi) + { + synchronized (m_propertiesCache) + { + Map tmp = null; + + synchronized (m_condPermInfos) + { + tmp = new OrderedHashMap(); + tmp.putAll(m_condPermInfos); + + if ((name != null) && (cpi != null)) + { + m_condPermInfos.put(name, cpi); + } + else if (name != null) + { + m_condPermInfos.remove(name); + } + else + { + tmp = null; + } + } + + try + { + m_propertiesCache.write(m_condPermInfos); + } + catch (IOException ex) + { + synchronized (m_condPermInfos) + { + if (tmp != null) + { + m_condPermInfos.clear(); + m_condPermInfos.putAll(tmp); + } + } + ex.printStackTrace(); + throw new IllegalStateException(ex.getMessage()); + } + } + synchronized (m_condPermInfos) + { + return (ConditionalPermissionInfoImpl) m_condPermInfos.get(name); + } + } + + private static class FakeCert extends X509Certificate + { + private final Principal m_principal; + + public FakeCert(final String principal) + { + m_principal = new Principal() + { + public String getName() + { + return principal; + } + }; + } + + public void checkValidity() + throws java.security.cert.CertificateExpiredException, + java.security.cert.CertificateNotYetValidException + { + + } + + public void checkValidity(Date date) + throws java.security.cert.CertificateExpiredException, + java.security.cert.CertificateNotYetValidException + { + } + + public int getBasicConstraints() + { + return 0; + } + + public Principal getIssuerDN() + { + return null; + } + + public boolean[] getIssuerUniqueID() + { + return null; + } + + public boolean[] getKeyUsage() + { + return null; + } + + public Date getNotAfter() + { + return null; + } + + public Date getNotBefore() + { + return null; + } + + public BigInteger getSerialNumber() + { + return null; + } + + public String getSigAlgName() + { + return null; + } + + public String getSigAlgOID() + { + return null; + } + + public byte[] getSigAlgParams() + { + return null; + } + + public byte[] getSignature() + { + return null; + } + + public Principal getSubjectDN() + { + return m_principal; + } + + public boolean[] getSubjectUniqueID() + { + return null; + } + + public byte[] getTBSCertificate() + throws java.security.cert.CertificateEncodingException + { + return null; + } + + public int getVersion() + { + return 0; + } + + public byte[] getEncoded() + throws java.security.cert.CertificateEncodingException + { + return null; + } + + public PublicKey getPublicKey() + { + return null; + } + + public String toString() + { + return m_principal.getName(); + } + + public void verify(PublicKey key) + throws java.security.cert.CertificateException, + NoSuchAlgorithmException, InvalidKeyException, + NoSuchProviderException, SignatureException + { + + } + + public void verify(PublicKey key, String sigProvider) + throws java.security.cert.CertificateException, + NoSuchAlgorithmException, InvalidKeyException, + NoSuchProviderException, SignatureException + { + + } + + public Set getCriticalExtensionOIDs() + { + return null; + } + + public byte[] getExtensionValue(String arg0) + { + return null; + } + + public Set getNonCriticalExtensionOIDs() + { + return null; + } + + public boolean hasUnsupportedCriticalExtension() + { + return false; + } + + public boolean equals(Object o) + { + return this == o; + } + + public int hashCode() + { + return System.identityHashCode(this); + } + + } + + public AccessControlContext getAccessControlContext(final String[] signers) + { + Map certificates = new HashMap(); + for (int i = 0; i < signers.length; i++) + { + StringTokenizer tok = new StringTokenizer(signers[i], ";"); + List certsList = new ArrayList(); + while (tok.hasMoreTokens()) + { + certsList.add(tok.nextToken()); + } + String[] certs = (String[]) certsList.toArray(new String[certsList + .size()]); + + X509Certificate key = new FakeCert(certs[0]); + List certList = new ArrayList(); + certificates.put(key, certList); + certList.add(key); + for (int j = 1; j < certs.length; j++) + { + certList.add(new FakeCert(certs[j])); + } + } + final FakeBundle fake = new FakeBundle(certificates); + ProtectionDomain domain = new ProtectionDomain(null, null) + { + public boolean implies(Permission permission) + { + List posts = new ArrayList(); + Boolean result = m_pai.hasPermission("", fake, permission, + ConditionalPermissionAdminImpl.this, this, null); + if (result != null) + { + return result.booleanValue(); + } + if (eval(posts, new BundleRevisionImpl(fake, Long.toString(fake.getBundleId())), permission, m_pai)) + { + if (!posts.isEmpty()) + { + return m_conditions.evalRecursive(posts); + } + return true; + } + return false; + } + }; + return new AccessControlContext(new ProtectionDomain[] { domain }); + } + + public ConditionalPermissionInfo getConditionalPermissionInfo(String name) + { + if (name == null) + { + throw new IllegalArgumentException("Name may not be null"); + } + ConditionalPermissionInfoImpl result = null; + + synchronized (m_condPermInfos) + { + result = (ConditionalPermissionInfoImpl) m_condPermInfos.get(name); + } + + if (result == null) + { + result = new ConditionalPermissionInfoImpl(this, name, true); + + result = write(result.getName(), result); + } + + return result; + } + + public Enumeration getConditionalPermissionInfos() + { + synchronized (m_condPermInfos) + { + return Collections.enumeration(new ArrayList(m_condPermInfos + .values())); + } + } + + public ConditionalPermissionInfo setConditionalPermissionInfo(String name, + ConditionInfo[] conditions, PermissionInfo[] permissions) + { + Object sm = System.getSecurityManager(); + if (sm != null) + { + ((SecurityManager) sm).checkPermission(Permissions.ALL_PERMISSION); + } + + ConditionalPermissionInfoImpl result = null; + conditions = notNull(conditions); + permissions = notNull(permissions); + + if (name != null) + { + synchronized (m_condPermInfos) + { + result = (ConditionalPermissionInfoImpl) m_condPermInfos + .get(name); + + if (result == null) + { + result = new ConditionalPermissionInfoImpl(name, + conditions, permissions, this, true); + } + else + { + result.setConditionsAndPermissions(conditions, permissions); + } + } + } + else + { + result = new ConditionalPermissionInfoImpl(conditions, permissions, + this, true); + } + + return write(result.getName(), result); + } + + private PermissionInfo[] notNull(PermissionInfo[] permissions) + { + if (permissions == null) + { + return ConditionalPermissionInfoImpl.PERMISSION_INFO; + } + return (PermissionInfo[]) notNull((Object[]) permissions).toArray( + EMPTY_PERMISSION_INFO); + } + + private ConditionInfo[] notNull(ConditionInfo[] conditions) + { + if (conditions == null) + { + return ConditionalPermissionInfoImpl.CONDITION_INFO; + } + return (ConditionInfo[]) notNull((Object[]) conditions).toArray( + EMPTY_CONDITION_INFO); + } + + private List notNull(Object[] elements) + { + List result = new ArrayList(); + + for (int i = 0; i < elements.length; i++) + { + if (elements[i] != null) + { + result.add(elements[i]); + } + } + + return result; + } + + // The thread local stack used to keep track of bundle protection domains we + // still expect to see. + private final ThreadLocal m_stack = new ThreadLocal(); + + /** + * This method does the actual permission check. If it is not a direct check + * it will try to determine the other bundle domains that will follow + * automatically in case this is the first check in one permission check. If + * not then it will keep track of which domains we have already see. While + * it keeps track it builds up a list of postponed tuples which it will + * evaluate at the last domain. See the core spec 9.5.1 and following for a + * general description. + * + * @param felixBundle + * the bundle in question. + * @param loader + * the content loader of the bundle to get access to the jar to + * check for local permissions. + * @param root + * the bundle id. + * @param signers + * the signers (this is to support the ACC based on signers) + * @param pd + * the bundle protection domain + * @param permission + * the permission currently checked + * @param direct + * whether this is a direct check or not. direct check will not + * expect any further bundle domains on the stack + * @return true in case the permission is granted or there are postponed + * tuples false if not. Again, see the spec for more explanations. + */ + public boolean hasPermission(BundleRevisionImpl module, Content content, + ProtectionDomain pd, Permission permission, boolean direct, Object admin) + { + // System.out.println(felixBundle + "-" + permission); + List domains = null; + List tuples = null; + Object[] entry = null; + // first see whether this is the normal case (the special case is for + // the ACC based on signers). + // In case of a direct call we don't need to look for other pds + if (direct) + { + domains = new ArrayList(); + tuples = new ArrayList(); + domains.add(pd); + } + else + { + // Get the other pds from the stck + entry = (Object[]) m_stack.get(); + + // if there are none then get them from the gripper + if (entry == null) + { + entry = new Object[] { new ArrayList(DomainGripper.grab()), + new ArrayList() }; + } + else + { + m_stack.set(null); + } + + domains = (List) entry[0]; + tuples = (List) entry[1]; + if (!domains.contains(pd)) + { + // We have been called directly without the direct flag + domains.clear(); + domains.add(pd); + } + } + + // check the local permissions. they need to all the permission if there + // are any + if (!impliesLocal(module.getBundle(), content, permission)) + { + return false; + } + + List posts = new ArrayList(); + + boolean result = eval(posts, module, permission, admin); + + domains.remove(pd); + + // We postponed tuples + if (!posts.isEmpty()) + { + tuples.add(posts); + } + + // Are we at the end or this was a direct call? + if (domains.isEmpty()) + { + m_stack.set(null); + // Now eval the postponed tupels. if the previous eval did return + // false + // tuples will be empty so we don't return from here. + if (!tuples.isEmpty()) + { + return m_conditions.evalRecursive(tuples); + } + } + else + { + // this is to support recursive permission checks. In case we + // trigger + // a permission check while eval the stack is null until this point + m_stack.set(entry); + } + + return result; + } + + public boolean impliesLocal(Bundle felixBundle, Content content, + Permission permission) + { + return m_localPermissions.implies(content, felixBundle, permission); + } + + public boolean isEmpty() + { + synchronized (m_condPermInfos) + { + return m_condPermInfos.isEmpty(); + } + } + + // we need to find all conditions that apply and then check whether they + // de note the permission in question unless the conditions are postponed + // then we make sure their permissions imply the permission and add them + // to the list of posts. Return true in case we pass or have posts + // else falls and clear the posts first. + private boolean eval(List posts, BundleRevisionImpl module, Permission permission, + Object admin) + { + List condPermInfos = null; + + synchronized (m_condPermInfos) + { + if (isEmpty() && (admin == null)) + { + return true; + } + condPermInfos = new ArrayList(m_condPermInfos.values()); + } + + // Check for implicit permissions like access to file area + if (m_permissions.getPermissions( + m_permissions.getImplicit(module.getBundle())).implies(permission, + module.getBundle())) + { + return true; + } + List pls = new ArrayList(); + // now do the real thing + for (Iterator iter = condPermInfos.iterator(); iter.hasNext();) + { + ConditionalPermissionInfoImpl cpi = (ConditionalPermissionInfoImpl) iter + .next(); + + ConditionInfo[] conditions = cpi._getConditionInfos(); + + List currentPosts = new ArrayList(); + + Conditions conds = m_conditions.getConditions(module, conditions); + if (!conds.isSatisfied(currentPosts, m_permissions + .getPermissions(cpi._getPermissionInfos()), permission)) + { + continue; + } + + if (!m_permissions.getPermissions(cpi._getPermissionInfos()) + .implies(permission, null)) + { + continue; + } + + if (currentPosts.isEmpty()) + { + pls.add(new Object[] { cpi, null }); + break; + } + pls.add(new Object[] { cpi, currentPosts, conds }); + } + while (pls.size() > 1) + { + if (!((ConditionalPermissionInfoImpl) ((Object[]) pls.get(pls + .size() - 1))[0]).isAllow()) + { + pls.remove(pls.size() - 1); + } + else + { + break; + } + } + if (pls.size() == 1) + { + if (((Object[]) pls.get(0))[1] != null) + { + posts.add(pls.get(0)); + } + return ((ConditionalPermissionInfoImpl) ((Object[]) pls.get(0))[0]) + .isAllow(); + } + for (Iterator iter = pls.iterator(); iter.hasNext();) + { + posts.add(iter.next()); + } + return !posts.isEmpty(); + } + + public ConditionalPermissionInfo newConditionalPermissionInfo( + String encodedConditionalPermissionInfo) + { + return new ConditionalPermissionInfoImpl( + encodedConditionalPermissionInfo); + } + + public ConditionalPermissionInfo newConditionalPermissionInfo(String name, + ConditionInfo[] conditions, PermissionInfo[] permissions, String access) + { + return new ConditionalPermissionInfoImpl(name, conditions, permissions, + ConditionalPermissionAdminImpl.this, access + .equals(ConditionalPermissionInfo.ALLOW)); + } + + public ConditionalPermissionUpdate newConditionalPermissionUpdate() + { + return new ConditionalPermissionUpdate() + { + List current = null; + List out = null; + { + synchronized (m_condPermInfos) + { + current = new ArrayList(m_condPermInfos.values()); + out = new ArrayList(m_condPermInfos.values()); + } + } + + public boolean commit() + { + synchronized (m_condPermInfos) + { + if (current.equals(new ArrayList(m_condPermInfos.values()))) + { + m_condPermInfos.clear(); + write(null, null); + for (Iterator iter = out.iterator(); iter.hasNext();) + { + ConditionalPermissionInfoImpl cpii = (ConditionalPermissionInfoImpl) iter + .next(); + write(cpii.getName(), cpii); + } + } + else + { + return false; + } + } + return true; + } + + public List getConditionalPermissionInfos() + { + return out; + } + }; + } + + public boolean handlePAHandle(BundleProtectionDomain pd) + { + Object[] entry = (Object[]) m_stack.get(); + + if (entry == null) + { + entry = new Object[] { new ArrayList(DomainGripper.grab()), + new ArrayList() }; + } + + ((List) entry[0]).remove(pd); + if (((List) entry[0]).isEmpty()) + { + m_stack.set(null); + if (!((List) entry[1]).isEmpty()) + { + return m_conditions.evalRecursive(((List) entry[1])); + } + } + else + { + m_stack.set(entry); + } + + return true; + } + + public void clearPD() + { + m_stack.set(null); + } +} diff --git a/framework.security/src/main/java/org/apache/felix/framework/security/condpermadmin/ConditionalPermissionInfoImpl.java b/framework.security/src/main/java/org/apache/felix/framework/security/condpermadmin/ConditionalPermissionInfoImpl.java new file mode 100644 index 00000000000..102ace18a1f --- /dev/null +++ b/framework.security/src/main/java/org/apache/felix/framework/security/condpermadmin/ConditionalPermissionInfoImpl.java @@ -0,0 +1,486 @@ +/* + * 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.framework.security.condpermadmin; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.StringTokenizer; + +import org.apache.felix.framework.security.util.Permissions; +import org.osgi.service.condpermadmin.ConditionInfo; +import org.osgi.service.condpermadmin.ConditionalPermissionInfo; +import org.osgi.service.permissionadmin.PermissionInfo; + +/** + * Simple storage class for condperminfos. Additionally, this class can be used + * to encode and decode infos. + */ +public final class ConditionalPermissionInfoImpl implements + ConditionalPermissionInfo +{ + private static final Random RANDOM = new Random(); + static final ConditionInfo[] CONDITION_INFO = new ConditionInfo[0]; + static final PermissionInfo[] PERMISSION_INFO = new PermissionInfo[0]; + private final Object m_lock = new Object(); + private final String m_name; + private final boolean m_allow; + private volatile ConditionalPermissionAdminImpl m_cpai; + private ConditionInfo[] m_conditions; + private PermissionInfo[] m_permissions; + + private int parseConditionInfo(char[] encoded, int idx, List conditions) { + String type; + String[] args; + try { + int pos = idx; + + /* skip whitespace */ + while (Character.isWhitespace(encoded[pos])) { + pos++; + } + + /* the first character must be '[' */ + if (encoded[pos] != '[') { + throw new IllegalArgumentException("expecting open bracket"); + } + pos++; + + /* skip whitespace */ + while (Character.isWhitespace(encoded[pos])) { + pos++; + } + + /* type is not quoted or encoded */ + int begin = pos; + while (!Character.isWhitespace(encoded[pos]) + && (encoded[pos] != ']')) { + pos++; + } + if (pos == begin || encoded[begin] == '"') { + throw new IllegalArgumentException("expecting type"); + } + type = new String(encoded, begin, pos - begin); + + /* skip whitespace */ + while (Character.isWhitespace(encoded[pos])) { + pos++; + } + + /* type may be followed by args which are quoted and encoded */ + ArrayList argsList = new ArrayList(); + while (encoded[pos] == '"') { + pos++; + begin = pos; + while (encoded[pos] != '"') { + if (encoded[pos] == '\\') { + pos++; + } + pos++; + } + argsList.add(unescapeString(encoded, begin, pos)); + pos++; + + if (Character.isWhitespace(encoded[pos])) { + /* skip whitespace */ + while (Character.isWhitespace(encoded[pos])) { + pos++; + } + } + } + args = (String[]) argsList + .toArray(new String[argsList.size()]); + + /* the final character must be ']' */ + char c = encoded[pos++]; + if (c != ']') { + throw new IllegalArgumentException("expecting close bracket"); + } + conditions.add(new ConditionInfo(type, args)); + return pos; + } + catch (ArrayIndexOutOfBoundsException e) { + throw new IllegalArgumentException("parsing terminated abruptly"); + } + } + + private int parsePermissionInfo(char[] encoded, int idx, List permissions) + { + String parsedType = null; + String parsedName = null; + String parsedActions = null; + try { + int pos = idx; + + /* skip whitespace */ + while (Character.isWhitespace(encoded[pos])) { + pos++; + } + + /* the first character must be '(' */ + if (encoded[pos] != '(') { + throw new IllegalArgumentException("expecting open parenthesis"); + } + pos++; + + /* skip whitespace */ + while (Character.isWhitespace(encoded[pos])) { + pos++; + } + + /* type is not quoted or encoded */ + int begin = pos; + while (!Character.isWhitespace(encoded[pos]) + && (encoded[pos] != ')')) { + pos++; + } + if (pos == begin || encoded[begin] == '"') { + throw new IllegalArgumentException("expecting type"); + } + parsedType = new String(encoded, begin, pos - begin); + + /* skip whitespace */ + while (Character.isWhitespace(encoded[pos])) { + pos++; + } + + /* type may be followed by name which is quoted and encoded */ + if (encoded[pos] == '"') { + pos++; + begin = pos; + while (encoded[pos] != '"') { + if (encoded[pos] == '\\') { + pos++; + } + pos++; + } + parsedName = unescapeString(encoded, begin, pos); + pos++; + + if (Character.isWhitespace(encoded[pos])) { + /* skip whitespace */ + while (Character.isWhitespace(encoded[pos])) { + pos++; + } + + /* + * name may be followed by actions which is quoted and + * encoded + */ + if (encoded[pos] == '"') { + pos++; + begin = pos; + while (encoded[pos] != '"') { + if (encoded[pos] == '\\') { + pos++; + } + pos++; + } + parsedActions = unescapeString(encoded, begin, pos); + pos++; + + /* skip whitespace */ + while (Character.isWhitespace(encoded[pos])) { + pos++; + } + } + } + } + + /* the final character must be ')' */ + char c = encoded[pos++]; + if (c != ')') { + throw new IllegalArgumentException( + "expecting close parenthesis"); + } + permissions.add(new PermissionInfo(parsedType,parsedName, parsedActions)); + return pos; + } + catch (ArrayIndexOutOfBoundsException e) { + throw new IllegalArgumentException("parsing terminated abruptly"); + } + } + /** + * Takes an encoded character array and decodes it into a new String. + */ + private static String unescapeString(char[] str, int begin, int end) { + StringBuffer output = new StringBuffer(end - begin); + for (int i = begin; i < end; i++) { + char c = str[i]; + if (c == '\\') { + i++; + if (i < end) { + c = str[i]; + switch (c) { + case '"' : + case '\\' : + break; + case 'r' : + c = '\r'; + break; + case 'n' : + c = '\n'; + break; + default : + c = '\\'; + i--; + break; + } + } + } + output.append(c); + } + + return output.toString(); + } + + public ConditionalPermissionInfoImpl(String encoded) + { + encoded = encoded.trim(); + String toUpper = encoded.toUpperCase(); + if (!(toUpper.startsWith("ALLOW {") || toUpper.startsWith("DENY {"))) + { + throw new IllegalArgumentException(); + } + m_allow = toUpper.startsWith("ALLOW {"); + m_cpai = null; + List conditions = new ArrayList(); + List permissions = new ArrayList(); + try { + char[] chars = encoded.substring((m_allow ? "ALLOW {".length() : "DENY {".length())).toCharArray(); + int idx = 0; + while (idx < chars.length) + { + if (Character.isWhitespace(chars[idx])) { + idx++; + } + else if (chars[idx] == '[') + { + idx = parseConditionInfo(chars, idx, conditions); + } + else if (chars[idx] == '(') + { + idx = parsePermissionInfo(chars, idx, permissions); + } + else + { + if (chars[idx] != '}') + { + throw new IllegalArgumentException("Expected } but was: " + chars[idx]); + } + idx++; + break; + } + } + while (Character.isWhitespace(chars[idx])) { + idx++; + } + if (chars[idx] == '"') { + idx++; + int begin = idx; + while (chars[idx] != '"') { + if (chars[idx] == '\\') { + idx++; + } + idx++; + } + m_name = unescapeString(chars, begin, idx); + } + else { + m_name = Long.toString(RANDOM.nextLong() ^ System.currentTimeMillis()); + } + } catch (ArrayIndexOutOfBoundsException ex) { + ex.printStackTrace(); + throw new IllegalArgumentException("Unable to parse conditional permission info: " + ex.getMessage()); + } + m_conditions = conditions.isEmpty() ? CONDITION_INFO + : (ConditionInfo[]) conditions.toArray(new ConditionInfo[conditions + .size()]); + m_permissions = permissions.isEmpty() ? PERMISSION_INFO + : (PermissionInfo[]) permissions + .toArray(new PermissionInfo[permissions.size()]); + } + + public ConditionalPermissionInfoImpl(ConditionalPermissionAdminImpl cpai, + String name, boolean access) + { + m_allow = access; + m_name = name; + m_cpai = cpai; + m_conditions = CONDITION_INFO; + m_permissions = PERMISSION_INFO; + } + + public ConditionalPermissionInfoImpl(ConditionInfo[] conditions, + PermissionInfo[] permisions, ConditionalPermissionAdminImpl cpai, + boolean access) + { + m_allow = access; + m_name = Long.toString(RANDOM.nextLong() ^ System.currentTimeMillis()); + m_cpai = cpai; + m_conditions = conditions == null ? CONDITION_INFO : conditions; + m_permissions = permisions == null ? PERMISSION_INFO : permisions; + } + + public ConditionalPermissionInfoImpl(String name, + ConditionInfo[] conditions, PermissionInfo[] permisions, + ConditionalPermissionAdminImpl cpai, boolean access) + { + m_allow = access; + m_name = (name != null) ? name : Long.toString(RANDOM.nextLong() + ^ System.currentTimeMillis()); + m_conditions = conditions == null ? CONDITION_INFO : conditions; + m_permissions = permisions == null ? PERMISSION_INFO : permisions; + m_cpai = cpai; + } + + public void delete() + { + Object sm = System.getSecurityManager(); + if (sm != null) + { + ((SecurityManager) sm).checkPermission(Permissions.ALL_PERMISSION); + } + + synchronized (m_lock) + { + m_cpai.write(m_name, null); + m_conditions = CONDITION_INFO; + m_permissions = PERMISSION_INFO; + } + } + + public ConditionInfo[] getConditionInfos() + { + synchronized (m_lock) + { + return (ConditionInfo[]) m_conditions.clone(); + } + } + + ConditionInfo[] _getConditionInfos() + { + synchronized (m_lock) + { + return m_conditions; + } + } + + void setConditionsAndPermissions(ConditionInfo[] conditions, + PermissionInfo[] permissions) + { + synchronized (m_lock) + { + m_conditions = conditions; + m_permissions = permissions; + } + } + + public String getName() + { + return m_name; + } + + public PermissionInfo[] getPermissionInfos() + { + synchronized (m_lock) + { + return (PermissionInfo[]) m_permissions.clone(); + } + } + + PermissionInfo[] _getPermissionInfos() + { + synchronized (m_lock) + { + return m_permissions; + } + } + + public String getEncoded() + { + StringBuffer buffer = new StringBuffer(); + buffer.append(m_allow ? "ALLOW " : "DENY "); + buffer.append('{'); + buffer.append(' '); + synchronized (m_lock) + { + writeTo(m_conditions, buffer); + writeTo(m_permissions, buffer); + } + buffer.append('}'); + buffer.append(' '); + buffer.append('"'); + escapeString(m_name, buffer); + buffer.append('"'); + return buffer.toString(); + } + + /** + * This escapes the quotes, backslashes, \n, and \r in the string using a + * backslash and appends the newly escaped string to a StringBuffer. + */ + private static void escapeString(String str, StringBuffer output) { + int len = str.length(); + for (int i = 0; i < len; i++) { + char c = str.charAt(i); + switch (c) { + case '"' : + case '\\' : + output.append('\\'); + output.append(c); + break; + case '\r' : + output.append("\\r"); + break; + case '\n' : + output.append("\\n"); + break; + default : + output.append(c); + break; + } + } + } + + private void writeTo(Object[] elements, StringBuffer buffer) + { + for (int i = 0; i < elements.length; i++) + { + buffer.append(elements[i]); + buffer.append(' '); + } + } + + public String toString() + { + return getEncoded(); + } + + public String getAccessDecision() + { + return m_allow ? ConditionalPermissionInfo.ALLOW + : ConditionalPermissionInfo.DENY; + } + + public boolean isAllow() + { + return m_allow; + } +} diff --git a/framework.security/src/main/java/org/apache/felix/framework/security/condpermadmin/DomainGripper.java b/framework.security/src/main/java/org/apache/felix/framework/security/condpermadmin/DomainGripper.java new file mode 100644 index 00000000000..c9c299f7892 --- /dev/null +++ b/framework.security/src/main/java/org/apache/felix/framework/security/condpermadmin/DomainGripper.java @@ -0,0 +1,134 @@ +/* + * 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.framework.security.condpermadmin; + +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.AllPermission; +import java.security.DomainCombiner; +import java.security.Permission; +import java.security.PrivilegedAction; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.List; + +import org.apache.felix.framework.BundleProtectionDomain; + +/** + * This class is a hack to get all BundleProtectionDomains currently on the + * security stack. This way we don't need to have our own security manager set. + */ +final class DomainGripper implements DomainCombiner, PrivilegedAction +{ + private static final ProtectionDomain[] ALL_PERMISSION_PD = new ProtectionDomain[] { new ProtectionDomain( + null, null) + { + public boolean implies(Permission perm) + { + return true; + } + } }; + + // A per thread cache of DomainGripper objects. We might want to wrap them + // in a softreference eventually + private static final ThreadLocal m_cache = new ThreadLocal(); + + private static final Permission ALL_PERMISSION = new AllPermission(); + + private final List m_domains = new ArrayList(); + + private AccessControlContext m_system = null; + + /** + * Get all bundle protection domains and add them to the m_domains. Then + * return the ALL_PERMISSION_PD. + */ + public ProtectionDomain[] combine(ProtectionDomain[] current, + ProtectionDomain[] assigned) + { + filter(current, m_domains); + filter(assigned, m_domains); + + return ALL_PERMISSION_PD; + } + + private void filter(ProtectionDomain[] assigned, List domains) + { + if (assigned != null) + { + for (int i = 0; i < assigned.length; i++) + { + if ((assigned[i].getClass() == BundleProtectionDomain.class) + && !domains.contains(assigned[i])) + { + domains.add(assigned[i]); + } + } + } + } + + /** + * Get the current bundle protection domains on the stack up to the last + * privileged call. + */ + public static List grab() + { + // First try to get a cached version. We cache by thread. + DomainGripper gripper = (DomainGripper) m_cache.get(); + if (gripper == null) + { + // there is none so create one and cache it + gripper = new DomainGripper(); + m_cache.set(gripper); + } + else + { + // This thread has a cached version so prepare it + gripper.m_domains.clear(); + } + + // Get the current context. + gripper.m_system = AccessController.getContext(); + + // and merge it with the current combiner (i.e., gripper) + AccessControlContext context = (AccessControlContext) AccessController + .doPrivileged(gripper); + + gripper.m_system = null; + + // now get the protection domains + AccessController.doPrivileged(gripper, context); + + // and return them + return gripper.m_domains; + } + + public Object run() + { + // this is a call to merge with the current context. + if (m_system != null) + { + return new AccessControlContext(m_system, this); + } + + // this is a call to get the protection domains. + AccessController.checkPermission(ALL_PERMISSION); + return null; + } +} diff --git a/framework.security/src/main/java/org/apache/felix/framework/security/permissionadmin/PermissionAdminImpl.java b/framework.security/src/main/java/org/apache/felix/framework/security/permissionadmin/PermissionAdminImpl.java new file mode 100644 index 00000000000..92617d812f2 --- /dev/null +++ b/framework.security/src/main/java/org/apache/felix/framework/security/permissionadmin/PermissionAdminImpl.java @@ -0,0 +1,296 @@ +/* + * 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.framework.security.permissionadmin; + +import java.io.IOException; +import java.security.AllPermission; +import java.security.Permission; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.felix.framework.security.condpermadmin.ConditionalPermissionAdminImpl; +import org.apache.felix.framework.security.util.Permissions; +import org.apache.felix.framework.security.util.PropertiesCache; + +//import org.apache.felix.moduleloader.IContent; +import org.apache.felix.framework.cache.Content; + +import org.osgi.framework.Bundle; +import org.osgi.service.permissionadmin.PermissionAdmin; +import org.osgi.service.permissionadmin.PermissionInfo; + +/** + * This class is a relatively straight forward implementation of the + * PermissionAdmin service. The only somewhat involved thing is that it respects + * the presents of a conditionalpermissionadmin service as per spec. + */ +// TODO: Do we need this class at all or can we just emulate it using the +// condpermadmin? +public final class PermissionAdminImpl implements PermissionAdmin +{ + private static final PermissionInfo[] ALL_PERMISSION = new PermissionInfo[] { new PermissionInfo( + AllPermission.class.getName(), "", "") }; + + private final Map m_store = new HashMap(); + + private final PropertiesCache m_cache; + + private final Permissions m_permissions; + + private PermissionInfo[] m_default = null; + + public PermissionAdminImpl(Permissions permissions, PropertiesCache cache) + throws IOException + { + m_permissions = permissions; + m_cache = cache; + m_cache.read(PermissionInfo[].class, m_store); + } + + public PermissionInfo[] getDefaultPermissions() + { + synchronized (m_store) + { + if (m_default == null) + { + return null; + } + return (PermissionInfo[]) m_default.clone(); + } + } + + public synchronized String[] getLocations() + { + synchronized (m_store) + { + if (m_store.isEmpty()) + { + return null; + } + + return (String[]) m_store.keySet().toArray( + new String[m_store.size()]); + } + } + + public PermissionInfo[] getPermissions(String location) + { + synchronized (m_store) + { + if (m_store.containsKey(location)) + { + return (PermissionInfo[]) ((PermissionInfo[]) m_store + .get(location)).clone(); + } + return null; + } + } + + /** + * This will do the actual permission check as described in the core spec + * 10.2 It will respect a present condpermadmin service as described in + * 9.10. + * + * @param location + * the location of the bundle. + * @param bundle + * the bundle in question. + * @param permission + * the permission to check. + * @param cpai + * A condpermadmin if one is present else null. + * @param pd + * the protectiondomain + * @return Boolean.TRUE if the location is bound and the permission is + * granted or if there is no cpa and the default permissions imply + * the permission Boolean.FALSE otherwise unless the location is not + * bound and their is a cpa in which case null is returned. + */ + public Boolean hasPermission(String location, Bundle bundle, + Permission permission, ConditionalPermissionAdminImpl cpai, + ProtectionDomain pd, Content content) + { + PermissionInfo[] permissions = null; + PermissionInfo[] defaults = null; + boolean contains = false; + synchronized (m_store) + { + contains = m_store.containsKey(location); + permissions = (PermissionInfo[]) m_store.get(location); + defaults = m_default; + } + if (contains) + { + if (check(permissions, permission, bundle)) + { + return Boolean.TRUE; + } + return check(m_permissions.getImplicit(bundle), permission, bundle) ? Boolean.TRUE + : Boolean.FALSE; + } + else if (cpai == null + || (cpai.isEmpty() && cpai + .impliesLocal(bundle, content, permission))) + { + if (defaults != null) + { + if (check(defaults, permission, null)) + { + return Boolean.TRUE; + } + return check(m_permissions.getImplicit(bundle), permission, + bundle) ? Boolean.TRUE : Boolean.FALSE; + } + else + { + return Boolean.TRUE; + } + } + else + { + return null; + } + } + + private boolean check(PermissionInfo[] permissions, Permission permission, + Bundle bundle) + { + Permissions permissionsObject = m_permissions + .getPermissions(permissions); + + return permissionsObject.implies(permission, bundle); + } + + public void setDefaultPermissions(PermissionInfo[] permissions) + { + Object sm = System.getSecurityManager(); + if (sm != null) + { + ((SecurityManager) sm).checkPermission(Permissions.ALL_PERMISSION); + } + + synchronized (m_cache) + { + PermissionInfo[] def = null; + Map store = null; + synchronized (m_store) + { + def = m_default; + store = new HashMap(m_store); + + m_default = (permissions != null) ? notNull(permissions) : null; + } + + try + { + m_cache.write(setDefaults(store, def)); + } + catch (IOException ex) + { + synchronized (m_store) + { + m_default = def; + } + + ex.printStackTrace(); + // TODO: log this + throw new IllegalStateException(ex.getMessage()); + } + } + } + + public void setPermissions(String location, PermissionInfo[] permissions) + { + Object sm = System.getSecurityManager(); + if (sm != null) + { + ((SecurityManager) sm).checkPermission(Permissions.ALL_PERMISSION); + } + + synchronized (m_cache) + { + if (location != null) + { + Map store = null; + Map storeCopy = null; + PermissionInfo[] def = null; + synchronized (m_store) + { + storeCopy = new HashMap(m_store); + if (permissions != null) + { + m_store.put(location, notNull(permissions)); + } + else + { + m_store.remove(location); + } + store = new HashMap(m_store); + } + try + { + m_cache.write(setDefaults(store, def)); + } + catch (IOException ex) + { + synchronized (m_store) + { + m_store.clear(); + m_store.putAll(storeCopy); + } + + ex.printStackTrace(); + // TODO: log this + throw new IllegalStateException(ex.getMessage()); + } + } + } + } + + private Map setDefaults(Map store, PermissionInfo[] def) + { + if (def != null) + { + store.put("DEFAULT", def); + } + else + { + store.remove("DEFAULT"); + } + return store; + } + + private PermissionInfo[] notNull(PermissionInfo[] permissions) + { + List result = new ArrayList(); + + for (int i = 0; i < permissions.length; i++) + { + if (permissions[i] != null) + { + result.add(permissions[i]); + } + } + return (PermissionInfo[]) result.toArray(new PermissionInfo[result + .size()]); + } +} diff --git a/framework.security/src/main/java/org/apache/felix/framework/security/util/BundleInputStream.java b/framework.security/src/main/java/org/apache/felix/framework/security/util/BundleInputStream.java new file mode 100644 index 00000000000..bb78d662c94 --- /dev/null +++ b/framework.security/src/main/java/org/apache/felix/framework/security/util/BundleInputStream.java @@ -0,0 +1,227 @@ +/* + * 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.framework.security.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +//import org.apache.felix.moduleloader.IContent; +import org.apache.felix.framework.cache.Content; + +/** + * This class makes a given content available as a inputstream with a jar + * content. In other words the stream can be used as input to a JarInputStream. + */ +public final class BundleInputStream extends InputStream +{ + private final Content m_root; + private final Enumeration m_content; + private final OutputStreamBuffer m_outputBuffer = new OutputStreamBuffer(); + + private ByteArrayInputStream m_buffer = null; + private JarOutputStream m_output = null; + + private static final String DUMMY_ENTRY = "__DUMMY-ENTRY__/"; + + public BundleInputStream(Content root) throws IOException + { + m_root = root; + + List entries = new ArrayList(); + + int count = 0; + boolean inMetaInf = true; + String manifest = null; + for (Enumeration e = m_root.getEntries(); e.hasMoreElements();) + { + String entry = (String) e.nextElement(); + if (entry.endsWith("/")) + { + // ignore + } + else if (entry.equalsIgnoreCase("META-INF/MANIFEST.MF")) + { + if (manifest == null) + { + manifest = entry; + } + } + else if (entry.toUpperCase().startsWith("META-INF/") + && entry.indexOf('/', "META-INF/".length()) < 0) + { + entries.add(count++, entry); + } + else + { + entries.add(entry); + } + } + entries.add(count++, DUMMY_ENTRY); + if (manifest == null) + { + manifest = "META-INF/MANIFEST.MF"; + } + m_content = Collections.enumeration(entries); + + try + { + m_output = new JarOutputStream(m_outputBuffer); + readNext(manifest); + m_buffer = new ByteArrayInputStream(m_outputBuffer.m_outBuffer + .toByteArray()); + + m_outputBuffer.m_outBuffer = null; + } + catch (IOException ex) + { + // TODO: figure out what is wrong + ex.printStackTrace(); + throw ex; + } + } + + public int read() throws IOException + { + if ((m_output == null) && (m_buffer == null)) + { + return -1; + } + + if (m_buffer != null) + { + int result = m_buffer.read(); + + if (result == -1) + { + m_buffer = null; + return read(); + } + + return result; + } + + if (m_content.hasMoreElements()) + { + String current = (String) m_content.nextElement(); + + readNext(current); + + if (!m_content.hasMoreElements()) + { + m_output.close(); + m_output = null; + } + + m_buffer = new ByteArrayInputStream(m_outputBuffer.m_outBuffer + .toByteArray()); + + m_outputBuffer.m_outBuffer = null; + } + else + { + m_output.close(); + m_output = null; + } + + return read(); + } + + private void readNext(String path) throws IOException + { + m_outputBuffer.m_outBuffer = new ByteArrayOutputStream(); + + if (path == DUMMY_ENTRY) + { + JarEntry entry = new JarEntry(path); + + m_output.putNextEntry(entry); + } + else + { + InputStream in = null; + try + { + in = m_root.getEntryAsStream(path); + + if (in == null) + { + throw new IOException("Missing entry"); + } + + JarEntry entry = new JarEntry(path); + + m_output.putNextEntry(entry); + + byte[] buffer = new byte[4 * 1024]; + + for (int c = in.read(buffer); c != -1; c = in.read(buffer)) + { + m_output.write(buffer, 0, c); + } + } + finally + { + if (in != null) + { + try + { + in.close(); + } + catch (Exception ex) + { + // Not much we can do + } + } + } + } + + m_output.closeEntry(); + + m_output.flush(); + } + + private static final class OutputStreamBuffer extends OutputStream + { + ByteArrayOutputStream m_outBuffer = null; + + public void write(int b) + { + m_outBuffer.write(b); + } + + public void write(byte[] buffer) throws IOException + { + m_outBuffer.write(buffer); + } + + public void write(byte[] buffer, int offset, int length) + { + m_outBuffer.write(buffer, offset, length); + } + } +} diff --git a/framework.security/src/main/java/org/apache/felix/framework/security/util/Conditions.java b/framework.security/src/main/java/org/apache/felix/framework/security/util/Conditions.java new file mode 100644 index 00000000000..34468ebdbd6 --- /dev/null +++ b/framework.security/src/main/java/org/apache/felix/framework/security/util/Conditions.java @@ -0,0 +1,381 @@ +/* + * 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.framework.security.util; + +import java.security.Permission; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; + +import org.apache.felix.framework.BundleRevisionImpl; +import org.apache.felix.framework.security.condpermadmin.ConditionalPermissionInfoImpl; +import org.apache.felix.framework.util.SecureAction; +import org.osgi.framework.Bundle; +import org.osgi.service.condpermadmin.Condition; +import org.osgi.service.condpermadmin.ConditionInfo; + +/** + * This class caches conditions instances by their infos. Furthermore, it allows + * to eval postponed condition permission tuples as per spec (see 9.45). + */ +// TODO: maybe use bundle events instead of soft/weak references. +public final class Conditions +{ + private static final ThreadLocal m_conditionStack = new ThreadLocal(); + private static final Map m_conditionCache = new WeakHashMap(); + + private final Map m_cache = new WeakHashMap(); + + private final BundleRevisionImpl m_module; + + private final ConditionInfo[] m_conditionInfos; + private final Condition[] m_conditions; + private final SecureAction m_action; + + public Conditions(SecureAction action) + { + this(null, null, action); + } + + private Conditions(BundleRevisionImpl module, ConditionInfo[] conditionInfos, + SecureAction action) + { + m_module = module; + m_conditionInfos = conditionInfos; + if ((module != null) && (conditionInfos != null)) + { + synchronized (m_conditionCache) + { + Map conditionMap = (Map) m_conditionCache.get(module); + if (conditionMap == null) + { + conditionMap = new HashMap(); + conditionMap.put(m_conditionInfos, + new Condition[m_conditionInfos.length]); + m_conditionCache.put(module, conditionMap); + } + Condition[] conditions = (Condition[]) conditionMap + .get(m_conditionInfos); + if (conditions == null) + { + conditions = new Condition[m_conditionInfos.length]; + conditionMap.put(m_conditionInfos, conditions); + } + m_conditions = conditions; + } + } + else + { + m_conditions = null; + } + m_action = action; + } + + public Conditions getConditions(BundleRevisionImpl key, ConditionInfo[] conditions) + { + Conditions result = null; + Map index = null; + synchronized (m_cache) + { + index = (Map) m_cache.get(conditions); + if (index == null) + { + index = new WeakHashMap(); + m_cache.put(conditions, index); + } + } + synchronized (index) + { + if (key != null) + { + result = (Conditions) index.get(key); + } + } + + if (result == null) + { + result = new Conditions(key, conditions, m_action); + synchronized (index) + { + index.put(key, result); + } + } + + return result; + } + + // See whether the given list is satisfied or not + public boolean isSatisfied(List posts, Permissions permissions, + Permission permission) + { + if (m_conditionInfos == null) + { + return true; + } + boolean check = true; + for (int i = 0; i < m_conditionInfos.length; i++) + { + if (m_module == null) + { + // TODO: check whether this is correct! + break; + } + try + { + Condition condition = null; + boolean add = false; + Class clazz = Class.forName(m_conditionInfos[i].getType()); + + synchronized (m_conditions) + { + if (m_conditions[i] == null) + { + m_conditions[i] = createCondition(m_module.getBundle(), + clazz, m_conditionInfos[i]); + } + condition = m_conditions[i]; + } + + Object current = m_conditionStack.get(); + if (current != null) + { + if (current instanceof HashSet) + { + if (((HashSet) current).contains(clazz)) + { + return false; + } + } + else + { + if (current == clazz) + { + return false; + } + } + } + + if (condition.isPostponed()) + { + if (check && !permissions.implies(permission, null)) + { + return false; + } + else + { + check = false; + } + posts.add(new Object[] { condition, new Integer(i) }); + } + else + { + + if (current == null) + { + m_conditionStack.set(clazz); + } + else + { + if (current instanceof HashSet) + { + if (((HashSet) current).contains(clazz)) + { + return false; + } + ((HashSet) current).add(clazz); + } + else + { + if (current == clazz) + { + return false; + } + HashSet frame = new HashSet(); + frame.add(current); + frame.add(clazz); + m_conditionStack.set(frame); + current = frame; + } + } + try + { + boolean mutable = condition.isMutable(); + boolean result = condition.isSatisfied(); + + if (!mutable + && ((condition != Condition.TRUE) && (condition != Condition.FALSE))) + { + synchronized (m_conditions) + { + m_conditions[i] = result ? Condition.TRUE + : Condition.FALSE; + } + } + if (!result) + { + return false; + } + } + finally + { + if (current == null) + { + m_conditionStack.set(null); + } + else + { + ((HashSet) current).remove(clazz); + if (((HashSet) current).isEmpty()) + { + m_conditionStack.set(null); + } + } + } + } + } + catch (Exception e) + { + // TODO: log this as per spec + e.printStackTrace(); + return false; + } + } + return true; + } + + public boolean evalRecursive(List entries) + { + Map contexts = new HashMap(); + outer: for (Iterator iter = entries.iterator(); iter.hasNext();) + { + List tuples = (List) iter.next(); + inner: for (Iterator inner = tuples.iterator(); inner.hasNext();) + { + Object[] entry = (Object[]) inner.next(); + List conditions = (List) entry[1]; + if (conditions == null) + { + if (!((ConditionalPermissionInfoImpl) entry[0]).isAllow()) + { + return false; + } + continue outer; + } + for (Iterator iter2 = conditions.iterator(); iter2.hasNext();) + { + Object[] condEntry = (Object[]) iter2.next(); + Condition cond = (Condition) condEntry[0]; + Dictionary context = (Dictionary) contexts.get(cond + .getClass()); + if (context == null) + { + context = new Hashtable(); + contexts.put(cond.getClass(), context); + } + Object current = m_conditionStack.get(); + if (current == null) + { + m_conditionStack.set(cond.getClass()); + } + else + { + if (current instanceof HashSet) + { + ((HashSet) current).add(cond.getClass()); + } + else + { + HashSet frame = new HashSet(); + frame.add(current); + frame.add(cond.getClass()); + m_conditionStack.set(frame); + current = frame; + } + } + boolean result; + boolean mutable = cond.isMutable(); + try + { + result = cond.isSatisfied(new Condition[] { cond }, + context); + } + finally + { + if (current == null) + { + m_conditionStack.set(null); + } + else + { + ((HashSet) current).remove(cond.getClass()); + if (((HashSet) current).isEmpty()) + { + m_conditionStack.set(null); + } + } + } + if (!mutable && (cond != Condition.TRUE) + && (cond != Condition.FALSE)) + { + synchronized (((Conditions) entry[2]).m_conditions) + { + ((Conditions) entry[2]).m_conditions[((Integer) condEntry[1]) + .intValue()] = result ? Condition.TRUE + : Condition.FALSE; + } + } + if (!result) + { + continue inner; + } + } + if (!((ConditionalPermissionInfoImpl) entry[0]).isAllow()) + { + return false; + } + continue outer; + } + return false; + } + return true; + } + + private Condition createCondition(final Bundle bundle, final Class clazz, + final ConditionInfo info) throws Exception + { + try + { + return (Condition) m_action.getMethod(clazz, "getCondition", + new Class[] { Bundle.class, ConditionInfo.class }).invoke(null, + new Object[] { bundle, info }); + } + catch (Exception ex) + { + ex.printStackTrace(); + return (Condition) m_action.getConstructor(clazz, + new Class[] { Bundle.class, ConditionInfo.class }).newInstance( + new Object[] { bundle, info }); + } + } +} diff --git a/framework.security/src/main/java/org/apache/felix/framework/security/util/LocalPermissions.java b/framework.security/src/main/java/org/apache/felix/framework/security/util/LocalPermissions.java new file mode 100644 index 00000000000..4f921c30d33 --- /dev/null +++ b/framework.security/src/main/java/org/apache/felix/framework/security/util/LocalPermissions.java @@ -0,0 +1,139 @@ +/* + * 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.framework.security.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.security.AllPermission; +import java.security.Permission; +import java.util.ArrayList; +import java.util.Map; +import java.util.WeakHashMap; + +//import org.apache.felix.moduleloader.IContent; +import org.apache.felix.framework.cache.Content; +import org.osgi.framework.Bundle; +import org.osgi.service.permissionadmin.PermissionInfo; + +/** + * A cache for local permissions. Local permissions are read from a given bundle + * and cached for later lookup. See core spec 9.2.1. + */ +// TODO: maybe use bundle events to clean thing up or weak/soft references +public final class LocalPermissions +{ + private static final PermissionInfo[] ALL_PERMISSION = new PermissionInfo[] { new PermissionInfo( + AllPermission.class.getName(), "", "") }; + + private final Map m_cache = new WeakHashMap(); + private final Permissions m_permissions; + + public LocalPermissions(Permissions permissions) throws IOException + { + m_permissions = permissions; + } + + /** + * Return true in case that the given permission is implied by the local + * permissions of the given bundle or if there are none otherwise, false. + * See core spec 9.2.1. + * + * @param root + * the root to use for cacheing as a key + * @param loader + * the loader to get the content of the bundle from + * @param bundle + * the bundle in quesiton + * @param permission + * the permission to check + * @return true if implied by local permissions. + */ + public boolean implies(Content content, Bundle bundle, + Permission permission) + { + PermissionInfo[] permissions = null; + + synchronized (m_cache) + { + if (!m_cache.containsKey(content)) + { + InputStream in = null; + try + { + in = content.getEntryAsStream("OSGI-INF/permissions.perm"); + if (in != null) + { + ArrayList perms = new ArrayList(); + + BufferedReader reader = new BufferedReader( + new InputStreamReader(in, "UTF-8")); + for (String line = reader.readLine(); line != null; line = reader + .readLine()) + { + String trim = line.trim(); + if (trim.startsWith("#") || trim.startsWith("//") + || (trim.length() == 0)) + { + continue; + } + perms.add(new PermissionInfo(line)); + } + + permissions = (PermissionInfo[]) perms + .toArray(new PermissionInfo[perms.size()]); + } + } + catch (Exception ex) + { + } + finally + { + if (in != null) + { + try + { + in.close(); + } + catch (IOException ex) + { + // TODO Auto-generated catch block + ex.printStackTrace(); + } + } + } + + if (permissions == null) + { + permissions = ALL_PERMISSION; + } + + m_cache.put(content, permissions); + } + else + { + permissions = (PermissionInfo[]) m_cache.get(content); + } + } + + return m_permissions.getPermissions(permissions).implies(permission, + bundle); + } +} diff --git a/framework.security/src/main/java/org/apache/felix/framework/security/util/Permissions.java b/framework.security/src/main/java/org/apache/felix/framework/security/util/Permissions.java new file mode 100644 index 00000000000..4df53f2b59b --- /dev/null +++ b/framework.security/src/main/java/org/apache/felix/framework/security/util/Permissions.java @@ -0,0 +1,616 @@ +/* + * 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.framework.security.util; + +import java.io.File; +import java.io.FilePermission; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.security.AccessController; +import java.security.AllPermission; +import java.security.Permission; +import java.security.PermissionCollection; +import java.security.PrivilegedAction; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; + +import org.apache.felix.framework.util.SecureAction; +import org.osgi.framework.AdminPermission; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.packageadmin.ExportedPackage; +import org.osgi.service.packageadmin.PackageAdmin; +import org.osgi.service.permissionadmin.PermissionInfo; + +/** + * A permission cache that uses permisssion infos as keys. Permission are + * created from the parent classloader or any exported package. + */ +// TODO: maybe use bundle events instead of soft/weak references +public final class Permissions +{ + private static final ClassLoader m_classLoader = Permissions.class + .getClassLoader(); + + private static final Map m_permissionCache = new HashMap(); + private static final Map m_permissions = new HashMap(); + private static final ReferenceQueue m_permissionsQueue = new ReferenceQueue(); + + private static final ThreadLocal m_stack = new ThreadLocal(); + + private final Map m_cache; + private final ReferenceQueue m_queue; + private final BundleContext m_context; + private final PermissionInfo[] m_permissionInfos; + private final boolean m_allPermission; + private final SecureAction m_action; + + public static final AllPermission ALL_PERMISSION = new AllPermission(); + + private static final PermissionInfo[] IMPLICIT = new PermissionInfo[] { new PermissionInfo( + FilePermission.class.getName(), "-", "read,write,delete") }; + + Permissions(PermissionInfo[] permissionInfos, BundleContext context, + SecureAction action) + { + m_context = context; + m_permissionInfos = permissionInfos; + m_cache = new HashMap(); + m_queue = new ReferenceQueue(); + m_action = action; + for (int i = 0; i < m_permissionInfos.length; i++) + { + if (m_permissionInfos[i].getType().equals( + AllPermission.class.getName())) + { + m_allPermission = true; + return; + } + } + m_allPermission = false; + } + + public Permissions(BundleContext context, SecureAction action) + { + m_context = context; + m_permissionInfos = null; + m_cache = null; + m_queue = null; + m_allPermission = true; + m_action = action; + } + + public PermissionInfo[] getImplicit(Bundle bundle) + { + return new PermissionInfo[] { + IMPLICIT[0], + new PermissionInfo(AdminPermission.class.getName(), "(id=" + + bundle.getBundleId() + ")", AdminPermission.METADATA), + new PermissionInfo(AdminPermission.class.getName(), "(id=" + + bundle.getBundleId() + ")", AdminPermission.RESOURCE), + new PermissionInfo(AdminPermission.class.getName(), "(id=" + + bundle.getBundleId() + ")", AdminPermission.CONTEXT) }; + } + + public Permissions getPermissions(PermissionInfo[] permissionInfos) + { + cleanUp(m_permissionsQueue, m_permissions); + + Permissions result = null; + synchronized (m_permissions) + { + result = (Permissions) m_permissions.get(new Entry(permissionInfos)); + } + if (result == null) + { + //permissionInfos may not be referenced by the new Permissions, as + //otherwise the reference in m_permissions prevents the key from + //being garbage collectable. + PermissionInfo[] permissionInfosClone = new PermissionInfo[permissionInfos.length]; + System.arraycopy(permissionInfos, 0, permissionInfosClone, 0, permissionInfos.length); + result = new Permissions(permissionInfosClone, m_context, m_action); + synchronized (m_permissions) + { + m_permissions.put( + new Entry(permissionInfos, m_permissionsQueue), result); + } + } + return result; + } + + private static final class Entry extends WeakReference + { + private final int m_hashCode; + + Entry(Object entry, ReferenceQueue queue) + { + super(entry, queue); + m_hashCode = entry.hashCode(); + } + + Entry(Object entry) + { + super(entry); + m_hashCode = entry.hashCode(); + } + + public Object get() + { + return super.get(); + } + + public int hashCode() + { + return m_hashCode; + } + + public boolean equals(Object o) + { + if (o == null) + { + return false; + } + + if (o == this) + { + return true; + } + + Object entry = super.get(); + + if (o instanceof Entry) + { + + Object otherEntry = ((Entry) o).get(); + if (entry == null) + { + return otherEntry == null; + } + if (otherEntry == null) + { + return false; + } + if (!entry.getClass().equals(otherEntry.getClass())) + { + return false; + } + if (entry instanceof Object[]) + { + return Arrays.equals((Object[])entry, (Object[])otherEntry); + } + return entry.equals(((Entry) o).get()); + } + else + { + return false; + } + } + } + + private static final class DefaultPermissionCollection extends + PermissionCollection + { + private final Map m_perms = new HashMap(); + + public void add(Permission perm) + { + synchronized (m_perms) + { + m_perms.put(perm, perm); + } + } + + public Enumeration elements() + { + throw new IllegalStateException("Not implemented"); + } + + public boolean implies(Permission perm) + { + Map perms = null; + + synchronized (m_perms) + { + perms = m_perms; + } + + Permission permission = (Permission) perms.get(perm); + + if ((permission != null) && permission.implies(perm)) + { + return true; + } + + for (Iterator iter = perms.values().iterator(); iter.hasNext();) + { + Permission current = (Permission) iter.next(); + if ((current != null) && (current != permission) + && current.implies(perm)) + { + return true; + } + } + return false; + } + } + + private void cleanUp(ReferenceQueue queue, Map cache) + { + for (Entry entry = (Entry) queue.poll(); entry != null; entry = (Entry) queue + .poll()) + { + synchronized (cache) + { + cache.remove(entry); + } + } + } + + /** + * @param target + * the permission to be implied + * @param bundle + * if not null then allow implicit permissions like file access + * to local data area + * @return true if the permission is implied by this permissions object. + */ + public boolean implies(Permission target, final Bundle bundle) + { + if (m_allPermission) + { + return true; + } + + Class targetClass = target.getClass(); + + cleanUp(m_queue, m_cache); + + if ((bundle != null) && targetClass == FilePermission.class) + { + for (int i = 0; i < m_permissionInfos.length; i++) + { + if (m_permissionInfos[i].getType().equals( + FilePermission.class.getName())) + { + String postfix = ""; + String name = m_permissionInfos[i].getName(); + if (!"<>".equals(name)) + { + if (name.endsWith("*") || name.endsWith("-")) + { + postfix = name.substring(name.length() - 1); + name = name.substring(0, name.length() - 1); + } + if (!(new File(name)).isAbsolute()) + { + BundleContext context = (BundleContext) AccessController + .doPrivileged(new PrivilegedAction() + { + public Object run() + { + return bundle.getBundleContext(); + } + }); + if (context == null) + { + break; + } + name = m_action.getAbsolutePath(new File(context + .getDataFile(""), name)); + } + if (postfix.length() > 0) + { + if ((name.length() > 0) && !name.endsWith("/")) + { + name += "/" + postfix; + } + else + { + name += postfix; + } + } + } + Permission source = createPermission(new PermissionInfo( + FilePermission.class.getName(), name, + m_permissionInfos[i].getActions()), targetClass); + if (source.implies(target)) + { + return true; + } + } + } + return false; + } + + Object current = m_stack.get(); + + if (current == null) + { + m_stack.set(targetClass); + } + else + { + if (current instanceof HashSet) + { + if (((HashSet) current).contains(targetClass)) + { + return false; + } + ((HashSet) current).add(targetClass); + } + else + { + if (current == targetClass) + { + return false; + } + HashSet frame = new HashSet(); + frame.add(current); + frame.add(targetClass); + m_stack.set(frame); + current = frame; + } + } + + try + { + SoftReference collectionEntry = null; + + PermissionCollection collection = null; + + synchronized (m_cache) + { + collectionEntry = (SoftReference) m_cache.get(targetClass); + } + + if (collectionEntry != null) + { + collection = (PermissionCollection) collectionEntry.get(); + } + + if (collection == null) + { + collection = target.newPermissionCollection(); + + if (collection == null) + { + collection = new DefaultPermissionCollection(); + } + + for (int i = 0; i < m_permissionInfos.length; i++) + { + PermissionInfo permissionInfo = m_permissionInfos[i]; + String infoType = permissionInfo.getType(); + String permissionType = targetClass.getName(); + + if (infoType.equals(permissionType)) + { + Permission permission = createPermission( + permissionInfo, targetClass); + + if (permission != null) + { + collection.add(permission); + } + } + } + + synchronized (m_cache) + { + m_cache.put(new Entry(target.getClass(), m_queue), + new SoftReference(collection)); + } + } + + return collection.implies(target); + } + finally + { + if (current == null) + { + m_stack.set(null); + } + else + { + ((HashSet) current).remove(targetClass); + if (((HashSet) current).isEmpty()) + { + m_stack.set(null); + } + } + } + } + + private Permission addToCache(String encoded, Permission permission) + { + if (permission == null) + { + return null; + } + + synchronized (m_permissionCache) + { + Map inner = null; + + SoftReference ref = (SoftReference) m_permissionCache.get(encoded); + if (ref != null) + { + inner = (Map) ref.get(); + } + if (inner == null) + { + inner = new HashMap(); + m_permissionCache.put(encoded, + new SoftReference(inner)); + } + + inner.put(new Entry(permission.getClass()), new Entry(permission)); + } + + return permission; + } + + private Permission getFromCache(String encoded, Class target) + { + synchronized (m_permissionCache) + { + SoftReference ref = (SoftReference) m_permissionCache.get(encoded); + if (ref != null) + { + Map inner = (Map) ref.get(); + if (inner != null) + { + Entry entry = (Entry) inner.get(target); + if (entry != null) + { + Permission result = (Permission) entry.get(); + if (result != null) + { + return result; + } + inner.remove(entry); + } + if (inner.isEmpty()) + { + m_permissionCache.remove(encoded); + } + } + else + { + m_permissionCache.remove(encoded); + } + } + + } + + return null; + } + + private Permission createPermission(final PermissionInfo permissionInfo, + final Class target) + { + return (Permission) AccessController + .doPrivileged(new PrivilegedAction() + { + public Object run() + { + Permission cached = getFromCache(permissionInfo + .getEncoded(), target); + + if (cached != null) + { + return cached; + } + + try + { + if (m_classLoader.loadClass(target.getName()) == target) + { + return addToCache(permissionInfo.getEncoded(), + createPermission(permissionInfo.getName(), + permissionInfo.getActions(), target)); + } + } + catch (ClassNotFoundException e1) + { + } + + ServiceReference[] refs = null; + try + { + refs = m_context.getServiceReferences( + PackageAdmin.class.getName(), null); + } + catch (InvalidSyntaxException e) + { + } + if (refs != null) + { + for (int i = 0; i < refs.length; i++) + { + PackageAdmin admin = (PackageAdmin) m_context + .getService(refs[i]); + + if (admin != null) + { + Permission result = null; + Bundle bundle = admin.getBundle(target); + if (bundle != null) + { + ExportedPackage[] exports = admin + .getExportedPackages(bundle); + if (exports != null) + { + String name = target.getName(); + name = name.substring(0, name + .lastIndexOf('.')); + + for (int j = 0; j < exports.length; j++) + { + if (exports[j].getName().equals( + name)) + { + result = createPermission( + permissionInfo.getName(), + permissionInfo.getActions(), + target); + break; + } + } + } + } + + m_context.ungetService(refs[i]); + + return addToCache(permissionInfo.getEncoded(), + result); + } + } + } + + return null; + } + }); + } + + private Permission createPermission(String name, String action, Class target) + { + // System.out.println("\n\n|" + name + "|\n--\n|" + action + "|\n--\n" + + // target + "\n\n"); + try + { + return (Permission) m_action.getConstructor(target, + new Class[] { String.class, String.class }).newInstance( + new Object[] { name, action }); + } + catch (Exception ex) + { + // TODO: log this or something + } + + return null; + } +} diff --git a/framework.security/src/main/java/org/apache/felix/framework/security/util/PropertiesCache.java b/framework.security/src/main/java/org/apache/felix/framework/security/util/PropertiesCache.java new file mode 100644 index 00000000000..6c76d39c2e5 --- /dev/null +++ b/framework.security/src/main/java/org/apache/felix/framework/security/util/PropertiesCache.java @@ -0,0 +1,242 @@ +/* + * 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.framework.security.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; +import java.util.TreeMap; +import java.util.Map.Entry; + +import org.apache.felix.framework.util.SecureAction; + +public final class PropertiesCache +{ + private final File m_file; + + private final File m_tmp; + + private final SecureAction m_action; + + public PropertiesCache(File store, File tmp, SecureAction action) + { + m_action = action; + m_file = store; + m_tmp = tmp; + } + + public void write(Map data) throws IOException + { + OutputStream out = null; + File tmp = null; + File tmp2 = null; + try + { + tmp = m_action.createTempFile("tmp", null, m_tmp); + tmp2 = m_action.createTempFile("tmp", null, m_tmp); + m_action.deleteFile(tmp2); + Exception org = null; + try + { + out = m_action.getFileOutputStream(tmp); + + Properties store = new Properties(); + + int count = 0; + + for (Iterator iter = data.entrySet().iterator(); iter.hasNext();) + { + Entry entry = (Entry) iter.next(); + store.setProperty(count++ + "-" + (String) entry.getKey(), + getEncoded(entry.getValue())); + } + + store.store(out, null); + } + catch (IOException ex) + { + org = ex; + throw ex; + } + finally + { + if (out != null) + { + try + { + out.close(); + } + catch (IOException ex) + { + if (org == null) + { + throw ex; + } + } + } + } + if ((m_action.fileExists(m_file) && !m_action.renameFile(m_file, + tmp2)) + || !m_action.renameFile(tmp, m_file)) + { + throw new IOException("Unable to write permissions"); + } + } + catch (IOException ex) + { + if (!m_action.fileExists(m_file) && (tmp2 != null) + && m_action.fileExists(tmp2)) + { + m_action.renameFile(tmp2, m_file); + } + throw ex; + } + finally + { + if (tmp != null) + { + m_action.deleteFile(tmp); + } + if (tmp2 != null) + { + m_action.deleteFile(tmp2); + } + } + } + + public void read(Class target, Map map) throws IOException + { + if (!m_file.isFile()) + { + return; + } + InputStream in = null; + Exception other = null; + Map result = new TreeMap(); + try + { + in = m_action.getFileInputStream(m_file); + + Properties store = new Properties(); + store.load(in); + + for (Iterator iter = store.entrySet().iterator(); iter.hasNext();) + { + Entry entry = (Entry) iter.next(); + result.put(entry.getKey(), getUnencoded((String) entry + .getValue(), target)); + } + } + catch (IOException ex) + { + other = ex; + throw ex; + } + finally + { + if (in != null) + { + try + { + in.close(); + } + catch (IOException ex) + { + if (other == null) + { + throw ex; + } + } + } + } + for (Iterator iter = result.entrySet().iterator(); iter.hasNext();) + { + Entry entry = (Entry) iter.next(); + String key = (String) entry.getKey(); + map.put(key.substring(key.indexOf("-")), entry.getValue()); + } + } + + private String getEncoded(Object target) throws IOException + { + Properties props = new Properties(); + if (target.getClass().isArray()) + { + + Object[] array = (Object[]) target; + for (int i = 0; i < array.length; i++) + { + props.setProperty(Integer.toString(i), array[i].toString()); + } + + ByteArrayOutputStream tmp = new ByteArrayOutputStream(); + props.store(tmp, null); + return new String(tmp.toByteArray()); + } + + return target.toString(); + } + + private Object getUnencoded(String encoded, Class target) + throws IOException + { + try + { + if (target.isArray()) + { + Properties props = new Properties(); + props.load(new ByteArrayInputStream(encoded.getBytes())); + Class componentType = target.getComponentType(); + Constructor constructor = m_action.getConstructor( + componentType, new Class[] { String.class }); + Object[] params = new Object[1]; + Object[] result = (Object[]) Array.newInstance(componentType, + props.size()); + + for (Iterator iter = props.entrySet().iterator(); iter + .hasNext();) + { + Entry entry = (Entry) iter.next(); + params[0] = entry.getValue(); + result[Integer.parseInt((String) entry.getKey())] = constructor + .newInstance(params); + } + + return result; + } + + return m_action.invoke(m_action.getConstructor(target, + new Class[] { String.class }), new Object[] { encoded }); + } + catch (Exception ex) + { + ex.printStackTrace(); + + throw new IOException(ex.getMessage()); + } + } +} diff --git a/framework.security/src/main/java/org/apache/felix/framework/security/util/TrustManager.java b/framework.security/src/main/java/org/apache/felix/framework/security/util/TrustManager.java new file mode 100644 index 00000000000..5b2520e08e3 --- /dev/null +++ b/framework.security/src/main/java/org/apache/felix/framework/security/util/TrustManager.java @@ -0,0 +1,193 @@ +/* + * 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.framework.security.util; + +import java.io.File; +import java.io.InputStream; +import java.io.PrintStream; +import java.security.KeyStore; +import java.security.cert.CertificateFactory; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.StringTokenizer; + +import org.apache.felix.framework.util.SecureAction; + +/* + * TODO: the certificate stores as well as the CRLs might change over time + * (added/removed certificates). We need a way to detect that and act on it. + * The problem is to find a good balance between re-checking and caching... + */ +public final class TrustManager +{ + private final SecureAction m_action; + private final String m_crlList; + private final String m_typeList; + private final String m_passwdList; + private final String m_storeList; + private Collection m_caCerts = null; + private Collection m_crls = null; + + public TrustManager(String crlList, String typeList, String passwdList, + String storeList, SecureAction action) + { + m_crlList = crlList; + m_typeList = typeList; + m_passwdList = passwdList; + m_storeList = storeList; + m_action = action; + } + + private synchronized void init() + { + if (m_caCerts == null) + { + try + { + initCRLs(); + initCaCerts(); + } + catch (Exception ex) + { + m_caCerts = new ArrayList(); + m_crls = new ArrayList(); + // TODO: log this + ex.printStackTrace(); + } + } + } + + private void initCRLs() throws Exception + { + final Collection result = new ArrayList(); + + if (m_crlList.trim().length() != 0) + { + CertificateFactory fac = CertificateFactory.getInstance("X509"); + + for (StringTokenizer tok = new StringTokenizer(m_crlList, "|"); tok + .hasMoreElements();) + { + InputStream input = null; + try + { + input = m_action.getURLConnectionInputStream(m_action + .createURL(null, tok.nextToken(), null) + .openConnection()); + result.addAll(fac.generateCRLs(input)); + } + catch (Exception ex) + { + // TODO: log this or something + ex.printStackTrace(); + } + finally + { + if (input != null) + { + try + { + input.close(); + } + catch (Exception ex) + { + // TODO: log this or something + ex.printStackTrace(); + } + } + } + } + } + + m_crls = result; + } + + private void initCaCerts() throws Exception + { + final Collection result = new ArrayList(); + + if (m_storeList.trim().length() != 0) + { + + StringTokenizer storeTok = new StringTokenizer(m_storeList, "|"); + StringTokenizer passwdTok = new StringTokenizer(m_passwdList, "|"); + StringTokenizer typeTok = new StringTokenizer(m_typeList, "|"); + + while (storeTok.hasMoreTokens()) + { + KeyStore ks = KeyStore.getInstance(typeTok.nextToken().trim()); + + InputStream input = null; + try + { + input = m_action.getURLConnectionInputStream(m_action + .createURL(null, storeTok.nextToken().trim(), null) + .openConnection()); + String pass = passwdTok.nextToken().trim(); + + ks.load(input, (pass.length() > 0) ? pass.toCharArray() + : null); + + for (Enumeration e = ks.aliases(); e.hasMoreElements();) + { + String alias = (String) e.nextElement(); + result.add(ks.getCertificate(alias)); + } + } + catch (Exception ex) + { + // TODO: log this or something + ex.printStackTrace(); + } + finally + { + if (input != null) + { + try + { + input.close(); + } + catch (Exception ex) + { + // TODO: log this or something + ex.printStackTrace(); + } + } + } + } + } + + m_caCerts = result; + } + + public Collection getCRLs() + { + init(); + + return m_crls; + } + + public Collection getCaCerts() + { + init(); + + return m_caCerts; + } +} diff --git a/framework.security/src/main/java/org/apache/felix/framework/security/verifier/BundleDNParser.java b/framework.security/src/main/java/org/apache/felix/framework/security/verifier/BundleDNParser.java new file mode 100644 index 00000000000..d24c4d42f38 --- /dev/null +++ b/framework.security/src/main/java/org/apache/felix/framework/security/verifier/BundleDNParser.java @@ -0,0 +1,550 @@ +/* + * 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.framework.security.verifier; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.security.cert.CRL; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; + +import org.apache.felix.framework.BundleRevisionImpl; +import org.apache.felix.framework.Logger; +import org.apache.felix.framework.security.util.BundleInputStream; +import org.apache.felix.framework.security.util.TrustManager; +/* +import org.apache.felix.moduleloader.IContent; +import org.apache.felix.moduleloader.IModule; +*/ +import org.apache.felix.framework.cache.Content; + + +import org.osgi.framework.Bundle; + +public final class BundleDNParser +{ + private static final Method m_getCodeSigners; + private static final Method m_getSignerCertPath; + private static final Method m_getCertificates; + + static + { + Method getCodeSigners = null; + Method getSignerCertPath = null; + Method getCertificates = null; + try + { + getCodeSigners = Class.forName("java.util.jar.JarEntry").getMethod( + "getCodeSigners", null); + getSignerCertPath = Class.forName("java.security.CodeSigner") + .getMethod("getSignerCertPath", null); + getCertificates = Class.forName("java.security.cert.CertPath") + .getMethod("getCertificates", null); + } + catch (Exception ex) + { + ex.printStackTrace(); + getCodeSigners = null; + getSignerCertPath = null; + getCertificates = null; + } + m_getCodeSigners = getCodeSigners; + m_getSignerCertPath = getSignerCertPath; + m_getCertificates = getCertificates; + } + + private final Logger m_logger; + private final Map m_cache = new WeakHashMap(); + private final Map m_allCache = new WeakHashMap(); + + private final TrustManager m_manager; + + public BundleDNParser(TrustManager manager, Logger logger) + { + m_manager = manager; + m_logger = logger; + } + + public Map getCache() + { + synchronized (m_cache) + { + return new HashMap(m_cache); + } + } + + public void put(String root, X509Certificate[] dnChains) + { + synchronized (m_cache) + { + m_cache.put(root, dnChains); + } + } + + public void checkDNChains(BundleRevisionImpl root, Content content, int signersType) + throws Exception + { + if (signersType == Bundle.SIGNERS_TRUSTED) + { + synchronized (m_cache) + { + if (m_cache.containsKey(root)) + { + Map result = (Map) m_cache.get(root); + if ((result != null) && (result.isEmpty())) + { + throw new IOException("Bundle not properly signed"); + } + return; + } + } + } + else + { + synchronized (m_allCache) + { + if (m_allCache.containsKey(root)) + { + Map result = (Map) m_allCache.get(root); + if ((result != null) && (result.isEmpty())) + { + throw new IOException("Bundle not properly signed"); + } + return; + } + } + } + + Map result = null; + Exception org = null; + try + { + result = _getDNChains(content, + signersType == Bundle.SIGNERS_TRUSTED); + } + catch (Exception ex) + { + org = ex; + } + + if (signersType == Bundle.SIGNERS_TRUSTED) + { + synchronized (m_cache) + { + m_cache.put(root, result); + } + } + else + { + synchronized (m_allCache) + { + m_allCache.put(root, result); + } + } + + if (org != null) + { + throw org; + } + } + + public Map getDNChains(BundleRevisionImpl root, Content bundleRevision, + int signersType) + { + if (signersType == Bundle.SIGNERS_TRUSTED) + { + synchronized (m_cache) + { + if (m_cache.containsKey(root)) + { + Map result = (Map) m_cache.get(root); + return (result == null) ? new HashMap() : new HashMap( + result); + } + } + } + else + { + synchronized (m_allCache) + { + if (m_allCache.containsKey(root)) + { + Map result = (Map) m_allCache.get(root); + return (result == null) ? new HashMap() : new HashMap( + result); + } + } + } + + Map result = null; + + try + { + result = _getDNChains(bundleRevision, + signersType == Bundle.SIGNERS_TRUSTED); + } + catch (Exception ex) + { + // Ignore + } + + if (signersType == Bundle.SIGNERS_TRUSTED) + { + synchronized (m_cache) + { + m_cache.put(root, result); + } + } + else + { + synchronized (m_allCache) + { + m_allCache.put(root, result); + } + } + + return (result == null) ? new HashMap() : new HashMap(result); + } + + private Map _getDNChains(Content content, boolean check) + throws IOException + { + X509Certificate[] certificates = null; + + certificates = getCertificates(new BundleInputStream(content), check); + + if (certificates == null) + { + return null; + } + + List rootChains = new ArrayList(); + + getRootChains(certificates, rootChains, check); + + Map result = new HashMap(); + + for (Iterator rootIter = rootChains.iterator(); rootIter.hasNext();) + { + StringBuffer buffer = new StringBuffer(); + + List chain = (List) rootIter.next(); + + Iterator iter = chain.iterator(); + + X509Certificate current = (X509Certificate) iter.next(); + + result.put(current, chain); + } + + if (!result.isEmpty()) + { + return result; + } + + throw new IOException(); + } + + private X509Certificate[] getCertificates(InputStream input, boolean check) + throws IOException + { + JarInputStream bundle = new JarInputStream(input, true); + + if (bundle.getManifest() == null) + { + return null; + } + + List certificateChains = new ArrayList(); + + int count = certificateChains.size(); + + // This is tricky: jdk1.3 doesn't say anything about what is happening + // if a bad sig is detected on an entry - later jdk's do say that they + // will throw a security Exception. The below should cater for both + // behaviors. + for (JarEntry entry = bundle.getNextJarEntry(); entry != null; entry = bundle + .getNextJarEntry()) + { + + if (entry.isDirectory() || + (entry.getName().startsWith("META-INF/") && + (entry.getName().indexOf('/', "META-INF/".length()) < 0))) + { + continue; + } + + for (byte[] tmp = new byte[4096]; bundle.read(tmp, 0, tmp.length) != -1;) + { + } + + Certificate[] certificates = entry.getCertificates(); + + // Workaround stupid bug in the sun jdk 1.5.x - getCertificates() + // returns null there even if there are valid certificates. + // This is a regression bug that has been fixed in 1.6. + // + // We use reflection to see whether we have a SignerCertPath + // for the entry (available >= 1.5) and if so check whether + // there are valid certificates - don't try this at home. + if ((certificates == null) && (m_getCodeSigners != null)) + { + try + { + Object[] signers = (Object[]) m_getCodeSigners.invoke( + entry, null); + + if (signers != null) + { + List certChains = new ArrayList(); + + for (int i = 0; i < signers.length; i++) + { + Object path = m_getSignerCertPath.invoke( + signers[i], null); + + certChains.addAll((List) m_getCertificates.invoke( + path, null)); + } + + certificates = (Certificate[]) certChains + .toArray(new Certificate[certChains.size()]); + } + } + catch (Exception ex) + { + ex.printStackTrace(); + // Not much we can do - probably we are not on >= 1.5 + } + } + + if ((certificates == null) || (certificates.length == 0)) + { + return null; + } + + List chains = new ArrayList(); + + getRootChains(certificates, chains, check); + + if (certificateChains.isEmpty()) + { + certificateChains.addAll(chains); + count = certificateChains.size(); + } + else + { + for (Iterator iter2 = certificateChains.iterator(); iter2 + .hasNext();) + { + X509Certificate cert = (X509Certificate) ((List) iter2 + .next()).get(0); + boolean found = false; + for (Iterator iter3 = chains.iterator(); iter3.hasNext();) + { + X509Certificate cert2 = (X509Certificate) ((List) iter3 + .next()).get(0); + + if (cert.getSubjectDN().equals(cert2.getSubjectDN()) + && cert.equals(cert2)) + { + found = true; + break; + } + } + if (!found) + { + iter2.remove(); + } + } + } + + if (certificateChains.isEmpty()) + { + if (count > 0) + { + throw new IOException("Bad signers"); + } + return null; + } + } + + List result = new ArrayList(); + + for (Iterator iter = certificateChains.iterator(); iter.hasNext();) + { + result.addAll((List) iter.next()); + } + + return (X509Certificate[]) (!result.isEmpty() ? result.toArray(new X509Certificate[result + .size()]) : null); + } + + private boolean isRevoked(Certificate certificate) + { + for (Iterator iter = m_manager.getCRLs().iterator(); iter.hasNext();) + { + if (((CRL) iter.next()).isRevoked(certificate)) + { + return true; + } + } + + return false; + } + + private void getRootChains(Certificate[] certificates, List chains, + boolean check) + { + List chain = new ArrayList(); + + boolean revoked = false; + + for (int i = 0; i < certificates.length - 1; i++) + { + X509Certificate certificate = (X509Certificate) certificates[i]; + + if (!revoked && isRevoked(certificate)) + { + revoked = true; + } + if (!check || !revoked) + { + try + { + if (check) + { + certificate.checkValidity(); + } + + chain.add(certificate); + } + catch (CertificateException ex) + { + m_logger.log(Logger.LOG_WARNING, "Invalid Certificate", ex); + revoked = true; + } + } + + if (!((X509Certificate) certificates[i + 1]).getSubjectDN().equals( + certificate.getIssuerDN())) + { + if (!check || (!revoked && trusted(certificate))) + { + chains.add(chain); + } + + revoked = false; + + if (!chain.isEmpty()) + { + chain = new ArrayList(); + } + } + } + // The final entry in the certs array is always + // a "root" certificate + if (!check || !revoked) + { + chain.add(certificates[certificates.length - 1]); + if (!check + || trusted((X509Certificate) certificates[certificates.length - 1])) + { + chains.add(chain); + } + } + } + + private boolean trusted(X509Certificate cert) + { + if (m_manager.getCaCerts().isEmpty() || isRevoked(cert)) + { + return false; + } + + for (Iterator iter = m_manager.getCaCerts().iterator(); iter.hasNext();) + { + X509Certificate trustedCaCert = (X509Certificate) iter.next(); + + if (isRevoked(trustedCaCert)) + { + continue; + } + + // If the cert has the same SubjectDN + // as a trusted CA, check whether + // the two certs are the same. + if (cert.getSubjectDN().equals(trustedCaCert.getSubjectDN())) + { + if (cert.equals(trustedCaCert)) + { + try + { + cert.checkValidity(); + trustedCaCert.checkValidity(); + return true; + } + catch (CertificateException ex) + { + // Not much we can do + m_logger.log(Logger.LOG_WARNING, "Invalid Certificate", ex); + } + } + } + } + + // cert issued by any of m_trustedCaCerts ? return true : return false + for (Iterator iter = m_manager.getCaCerts().iterator(); iter.hasNext();) + { + X509Certificate trustedCaCert = (X509Certificate) iter.next(); + + if (isRevoked(trustedCaCert)) + { + continue; + } + + if (cert.getIssuerDN().equals(trustedCaCert.getSubjectDN())) + { + try + { + cert.verify(trustedCaCert.getPublicKey()); + cert.checkValidity(); + trustedCaCert.checkValidity(); + return true; + } + catch (Exception ex) + { + m_logger.log(Logger.LOG_WARNING, "Invalid Certificate", ex); + } + } + } + + return false; + } +} diff --git a/framework/doc/README.txt b/framework/doc/README.txt new file mode 100644 index 00000000000..5072242993f --- /dev/null +++ b/framework/doc/README.txt @@ -0,0 +1,6 @@ +For documentation on how to use, launch, and/or embed the Felix framework, +please refer to the documentation accompanying the full Felix installation +or the "main" Felix subproject. Documentation is also available from our +web site at: + + http://cwiki.apache.org/FELIX/documentation.html diff --git a/framework/doc/changelog.txt b/framework/doc/changelog.txt new file mode 100644 index 00000000000..15588778d35 --- /dev/null +++ b/framework/doc/changelog.txt @@ -0,0 +1,1170 @@ +Changes from 6.0.1 to 6.0.2 +--------------------------- + +** Bug + * [FELIX-5917] - [Fwk security] Fix BundlePermission check for fragments + * [FELIX-5942] - Felix Framework freezes when resolving classes in parallel with Java 10 + * [FELIX-5978] - Felix framework unable to retrieve custom URL handlers when security is on + +** Improvement + * [FELIX-5912] - Handle empty package definitions in system package definitions more gracefully + * [FELIX-5914] - Workaround SecurityManager.getClassContext returning null on Android + * [FELIX-6035] - Allow urlhandlers to create urls for jrt protocol without an add-opens + +Changes from 6.0.0 to 6.0.1 +--------------------------- + +** Bug + * [FELIX-5886] - Error logged by org.apache.felix.framework.BundleWiringImpl.getClassByDelegation + * [FELIX-5889] - Extension Bundle capabilities do not index correctly + +Changes from 5.6.10 to 6.0.0 +---------------------------- + +** New Feature + * [FELIX-5791] - Support OSGi R7 framework features + +** Sub-task + * [FELIX-5792] - Implement java.* import and exports + * [FELIX-5796] - Implement multi-release jar support + * [FELIX-5797] - Implement extension bundle imports + * [FELIX-5798] - Implement FrameworkWiringDTO + * [FELIX-5811] - Prevent bundles from providing ee capabilities + +** Bug + * [FELIX-5754] - error in default.properties contained in jar org.apache.felix.framework-5.6.10.jar + * [FELIX-5759] - StackOverflowError thrown during URL construction + * [FELIX-5799] - Fix bugs in Service Factory unregister callback handling + * [FELIX-5807] - Do not hide the cause when a problem occurs in URLHandlersStreamHandlerProxy + * [FELIX-5810] - URLHandlers ContentHandler fails to load sun.net.www.content on Java 9 + * [FELIX-5864] - Missing uses constraints for OSGi spec packages + * [FELIX-5870] - Handle relative path elements in bundle classpath + * [FELIX-5874] - URLHandlersContentHandlerProxy.getBuiltIn always returns null + * [FELIX-5875] - org.osgi.framework.os.name et.al. are not obeyed + +** Improvement + * [FELIX-5800] - Improve calculation of available system bundle exports + * [FELIX-5803] - Auto refresh uninstalled bundle graph if possible + * [FELIX-5804] - Improve framework lookup for bundle protocol urls + * [FELIX-5808] - Clean-up bundlecache manifest parsing and entry reading + * [FELIX-5809] - Capture context for bundle: protocol urls if possible + * [FELIX-5816] - Switch to java.util.Random for startup performance + * [FELIX-5818] - Only resolve fragments that are required on dynamic import + * [FELIX-5823] - Remove support for old multi-file bundle cache format. + * [FELIX-5824] - Use VersionRange from OSGi and remove our own impl. + * [FELIX-5825] - Use ReentrantLock inside WeakZipFileFactory/WeakZipFile and use less synchronized. + * [FELIX-5826] - Remove FelixBundleContext interface + * [FELIX-5827] - Use less synchronized in the BundleWiring and BundleClassloader + * [FELIX-5828] - Remove android support. + * [FELIX-5829] - Delete ImmutableMap/List + * [FELIX-5877] - Add missing org.w3c.dom exports for java < 9 + +Changes from 5.6.8 to 5.6.10 +---------------------------- + +** Bug + * [FELIX-5707] - Bundle-NativeCode header should not require osname and processor parameter to be present to match + * [FELIX-5709] - PackageAdmin getHosts should only return hosts required via HOST_NAMESPACE. + +** Improvement + * [FELIX-5710] - Improve Java9 support + +** Sub-task + * [FELIX-5717] - Define proper package lists for java9 modules + * [FELIX-5718] - Autodetect modules available + * [FELIX-5719] - Extend property substitution to user defined FRAMEWORK_SYSTEMPACKAGES_EXTRA and FRAMEWORK_SYSTEMPACKAGES properties for java9 module expansion. + * [FELIX-5720] - Use PlatformClassLoader as boot classloader on java9 + * [FELIX-5721] - Make javafx packages available by default on java9 if they are there + * [FELIX-5725] - Cache generated reflection classes for jdk.internal.reflect like we do for sun.reflect. + * [FELIX-5727] - Allow extension bundles when loaded with the PlattformClassLoader + * [FELIX-5742] - Provide java9 system bundle exports for jpms jre + +Changes from 5.6.6 to 5.6.8 +--------------------------- + +** Bug + * [FELIX-5672] - Cannot launch Felix on Raspberry Pi: problem with normalizeOSVersion() + * [FELIX-5676] - Uninstall should throw an exception if called while the bundle is starting or stopping + +** Improvement + * [FELIX-5665] - High CPU usage on sun.reflect.Generated* class loads by log4j + +Changes from 5.6.4 to 5.6.6 +--------------------------- + +** Bug + * [FELIX-5646] - Missing EE and system packages for Java 9 + * [FELIX-5649] - Refreshing a fragment causes the framework to be restarted + * [FELIX-5652] - Felix Framework hangs on shutdown + +Changes from 5.6.2 to 5.6.4 +--------------------------- + +** Bug + * [FELIX-4695] - Normalize os.version system property in framework properties + * [FELIX-4837] - Workaround for JNLPClassLoader in ShutdownHook + * [FELIX-5553] - Cannot load native libraries in Windows Server 2016 with the name win32 + * [FELIX-5573] - Don't return null but throw a CNFE from BundleClassloader.loadclass and Bundle.loadClass on recursive class loads. + * [FELIX-5604] - Normalize require capabilites the same way we normalize provide capabilites + * [FELIX-5605] - Update to latest resolver 1.14.0 + +** Improvement + * [FELIX-4696] - Improve native OS version sanitation + * [FELIX-5574] - When detecting an unknow Windows OS name, provides a suitable default value for org.osgi.framework.os.name + * [FELIX-5589] - reduce memory usage of BundleRequirementImpl / BundleCapabilityImpl + * [FELIX-5593] - Specify ARM processor Endianness + +Changes from 5.6.1 to 5.6.2 +--------------------------- + +** Bug + * [FELIX-5184] - Regression: Native JNA bundle cannot be installed on Windows Server 2012 + * [FELIX-5485] - ServiceReference#getUsingBundles() returns usages with 0 count + * [FELIX-5544] - Don't take implicit boot delegation into account on service assignability check + +** Improvement + * [FELIX-5138] - Felix should log underlying exception on failed bundle update + * [FELIX-5513] - Remove code for pre java 5 + * [FELIX-5528] - Improve handing of bundle updates concurrent to other lifecycle changes + * [FELIX-5547] - Implement FelixResolveContext.getSubstitutionWires(Wiring) + * Update to latest resolver 1.12.0 + +** New Feature + * [FELIX-5329] - [Framework] Fix Java 8 packages and add Java 9 packages in default.properties + +Changes from 5.6.0 to 5.6.1 +--------------------------- + +** Bug + * [FELIX-5363] - new URL(string) suffers under concurrency + * [FELIX-5372] - BundleWiring#getClassLoader should return null for fragments + * [FELIX-5384] - EventDispatcher#createWhitelistFromHooks fails under security + +** Improvement + * [FELIX-5247] - Reduce number of threads created by Resolver during a startup of OSGi-based applications + +Changes from 5.4.0 to 5.6.0 +--------------------------- + +** Improvement + * Update to latest resolver 1.10.0 + +** Bug + * [FELIX-4871] - The felix framework logger can't be used with reflection anymore + * [FELIX-5204] - IllegalStateException when using custom URL handlers for bundles + +** Test + * [FELIX-5084] - Add a mock based test for BundleTracker + * [FELIX-5085] - Add a mock based test for ServiceTracker + +Changes from 5.2.0 to 5.4.0 +--------------------------- + +** Bug + * [FELIX-5043] - Potential of waiting forever in ServiceRegistry.getService() + * [FELIX-5061] - Optional resource fragment with requirements that cause class space consistency issues with the host export cause unexpected ResolutionExceptions + * [FELIX-5064] - Framework should provide service capabilities for registered services + * [FELIX-5075] - Framework hooks not re-sorted on service ranking update + +** Improvement + * [FELIX-5014] - Support Windows 10 for Bundle-NativeCode + * [FELIX-5034] - Reduce and correct locking related to the hook registry + * Update to latest resolver 1.8.0 + +Changes from 5.0.1 to 5.2.0 +--------------------------- +** Bug + * [FELIX-4456] - openConnection().getContentLengthLong() always returns -1 for bundle URLs on Java7 + * [FELIX-4468] - EventObject is created with null pointer : "null source" + * [FELIX-4960] - NPE in BundleRevisionImpl.getResourcesLocal() + * [FELIX-4977] - Concurrency issue with factory services + * [FELIX-5010] - NPE when resolving bundle with a header "Bundle-NativeCode = *" + +** Improvement + * [FELIX-4938] - Throw an exception when service use count overflow + +** New Feature + * [FELIX-4942] - Optimise the resolver + +Changes from 5.0.0 to 5.0.1 +--------------------------- +** Bug + * [FELIX-4867] - Stale bundle revisions don't get cleaned up when last using bundle is gone + * [FELIX-4905] - Framework does not export org.osgi.service.resolver + * [FELIX-4914] - Resolution problem with identity requirements on fragments + * [FELIX-4927] - Felix reports itself as an OSGi R4/R5 Framework + * [FELIX-4928] - Singleton getService() sometimes incorrectly returns null + +** Improvement + * [FELIX-4866] - Improve service registry + +Changes from 4.6.1 to 5.0.0 +--------------------------- +** Bug + * [FELIX-4838] - Deadlock in Service Registry + * [FELIX-4850] - org.osgi.framework.version should be 1.8 + * [FELIX-4854] - Unable to create consistent wiring when same package is exported in same version by multiple bundles + +** Improvement + * [FELIX-4821] - Use a faster implementation for StringMap + * [FELIX-4845] - Clean up Logger implementation + * [FELIX-4525] - Refactor the Framework to use the Resolver module + +Changes from 4.6.0 to 4.6.1 +--------------------------- +** Bug + * [FELIX-4806] - Ungetting service through ServiceObjects might throw IllegalArgumentException + +** Improvement + * [FELIX-4810] - Cache WeakZipFile#entries + +Changes from 4.4.1 to 4.6.0 +--------------------------- +** Sub-task + * [FELIX-4503] - [Core R6] Support osgi.native capability + * [FELIX-4504] - [Core R6] Support Framework DTOs + * [FELIX-4505] - [Core R6] Support Prototype Service Factory + * [FELIX-4578] - [Core R6] Support new Framework.init(FrameworkListener ... listeners) override + * [FELIX-4579] - [Core R6] Support Framework Extension Bundle Activators + * [FELIX-4580] - [Core R6] Support service.bundleid Service Registration property + * [FELIX-4581] - [Core R6] Support FrameworkWiring.findProviders(Requirement) + * [FELIX-4582] - [Core R6] Support WovenClassListener + * [FELIX-4583] - [Core R6] Ensure that all OSGi Core R6 CT tests pass + * [FELIX-4590] - [Core R6] Update to R6 API + * [FELIX-4593] - Support the JavaSE/compact profiles for Java 8 + * [FELIX-4726] - [Core R6] Update bundle and service hooks for the system bundle + +** Bug + * [FELIX-3883] - [Framework] Move OS and processor aliases to configuration properties + * [FELIX-4690] - Some bundles containing native code can fail to start on Windows 7+ + * [FELIX-4701] - Properties with surprising spelling in AutoProcessor + * [FELIX-4729] - [Core R6] Support for osgi.native namespace with Loading native code libraries + +** Improvement + * [FELIX-4502] - [Core R6] Provide an OSGi R6 compliant framework implementation + * [FELIX-4658] - URL of the CodeSource of a ProtectionDomain should resolve to the right JAR + * [FELIX-4692] - Improve Service access time + * [FELIX-4757] - Native Capabilities should allow OS and Processor alias to load from default and config properties + +Changes form 4.4.0 to 4.4.1 +--------------------------- +** Bug + * [FELIX-3239] - PackageAdmin#getExportedPackages does not work on packages exported by attached fragments + * [FELIX-3309] - Dashes in qualifier get replaced by periods causing framework not to start up + * [FELIX-3701] - Intermittent CNFE with embedded jars in Fragment Bundles + * [FELIX-4220] - BundleException type is always set to 0 + * [FELIX-4281] - Security Warning: Felix with Java Web Start + * [FELIX-4515] - BundleRevision for system extension has no capabilities + * [FELIX-4523] - Deadlock in URLHandlers when Felix.init and Felix.stop are called concurrently + * [FELIX-4534] - Bundles containing native code fails to start on Windows 7 + +Changes from 4.2.1 to 4.4.0 +--------------------------- +** Bug + * [FELIX-1131] - ServiceReference.isAssignableTo fails when using a factory that can not see the exported class and the bundle exporting the service does not have a direct wire to this class + * [FELIX-3992] - Classloader access outside of a privileged block + * [FELIX-4190] - The framework should not hold any lock while calling ServiceFactory#unget + * [FELIX-4283] - actually throw exception if method not found + * [FELIX-4331] - [Core R5] Support BundleContext and Framework adaptations + * [FELIX-4353] - [Core R5] BundleWiringTests OSGi CT test failures + * [FELIX-4354] - [Core R5] ResolverHookTests OSGi CT test failures + * [FELIX-4355] - [Core R5] org.osgi.test.cases.framework.launch OSGi CT test failure + * [FELIX-4365] - Input stream not properly closed on org.apache.felix.framework.Felix.getFrameworkVersion + * [FELIX-4388] - Initial start level not considered when bundle.start() is called + * [FELIX-4462] - Classloader deadlock in Java6 between two bundles + +** Improvement + * [FELIX-4128] - [Core R5] Provide an OSGi R5 compliant framework implementation + * [FELIX-4284] - remove dead allocation + +** Task + * [FELIX-4080] - [Core R5] Add support for org.osgi.framework.UnfilteredServiceListener + * [FELIX-4082] - [Core R5] Update org.osgi.framework.bsnversion framework property + * [FELIX-4083] - [Core R5] Support for valueOf when evaluating a Filter + * [FELIX-4084] - [Core R5] Enhance Bundle.adapt() to provider AccessControlContext. + * [FELIX-4085] - [Core R5] Implement updates to the Bundle Hook Specification + * [FELIX-4324] - [Core R5] Implement osgi.identity namespace for fragments + +** Wish + * [FELIX-3868] - Adding osgi.identity namespace to bundles (resources) + +Changes from 4.2.0 to 4.2.1 +--------------------------- +** Bug + * [FELIX-3411] - The implementation of org.osgi.service.startlevel.StartLevel#setStartLevel(int) does not follow the spec + * [FELIX-3893] - Bundle in cache doesn't pass security check anymore. + * [FELIX-3907] - NullPointerException in BundleWiringImpl when m_disposed is true. + * [FELIX-3939] - IllegalArgumentException from ClassLoader.definePackage + * [FELIX-3950] - Permission check being done even when security manager is absent + * [FELIX-3957] - Error events can be thrown when refreshing fragment bundles + +** Improvement + * [FELIX-3961] - Add a fallback to the default java security policy if no security provider is present + +Changes from 4.0.3 to 4.2.0 +--------------------------- +** Bug + * [FELIX-2780] - Extension bundle implementation relies on urlhandlers service to start extension bundles + * [FELIX-3160] - NPE in BundleRevisionImpl.close() when uninstalling a bundle + * [FELIX-3242] - Concurrent modification problem in StatefulResolver$ResolverStateImpl.getCandidates + * [FELIX-3273] - Possible exception when accessing headers + * [FELIX-3306] - Lazy activation of bundles is not always working as expected + * [FELIX-3343] - Installing an fragment bundle without Bundle-ManifestVersion: 2 causes NPE in resolver + * [FELIX-3348] - StartLevel thread may terminate on uncaught exception + * [FELIX-3353] - The implementation of org.osgi.service.packageadmin.PackageAdmin#getExportedPackages(Bundle), does not follow the spec. + * [FELIX-3367] - getClassloader permission + * [FELIX-3397] - NPE when trying to resolve invalid fragments + * [FELIX-3411] - The implementation of org.osgi.service.startlevel.StartLevel#setStartLevel(int) does not follow the spec + * [FELIX-3413] - NPE and thread blocked in org.osgi.service.packageadmin.PackageAdmin#refreshPackages(Bundle[]) + * [FELIX-3455] - Framework JARs for JDK 7 + * [FELIX-3465] - Multi root resolve operations may cause duplicate blame chains to be created + * [FELIX-3618] - [Framework] Should not allow bundles to use generic cap/req headers for osgi.wiring.* namespaces + * [FELIX-3626] - Issue of felix on android 4.1 + * [FELIX-3632] - [Framework] Parsing of delimited strings in manifest parser collapses all consecutive escapes + * [FELIX-3670] - PackageAdmin.isBundleType throws NPE for uninstalled bundle + * [FELIX-3713] - Bundle.start() returns without starting the bundle + * [FELIX-3743] - Potential endless loop setting the active framework startlevel + * [FELIX-3753] - Felix crashes when embedded within Felix + * [FELIX-3761] - When a bundle registers a service, the bundle lock is obtained without any real purpose + * [FELIX-3766] - Slightly invalid logic for pre-checking dynamic imports which cause the framework the grab the lock with no real need + * [FELIX-3803] - Bundle#getResource always try to resolve the bundle + * [FELIX-3824] - Possible InvalidStateException thrown while unregistering bundle services + * [FELIX-3840] - problem with URLHandlers when running 2 frameworks in one jvm in separate class loaders + * [FELIX-3844] - Native bundles cannot be installed on Windows 8 and Windows Server 2012 + * [FELIX-3852] - InstallBundle throws ClassCastException: java.util.jar.Attributes$Name cannot be cast to java.lang.String + * [FELIX-3887] - ClassCastException during resolution of Require-Bundle: system.bundle + +** Improvement + * [FELIX-3344] - [Framework] Filter parsing treats ** as invalid syntax + * [FELIX-3372] - Add the ability to handle a blank on the property "org.osgi.framework.system.packages.extra" + * [FELIX-3394] - [Framework] Refactor internal resolver APIs to better align with upcoming OSGi resolver spec + * [FELIX-3447] - Optimize read only collections by using a specific class and not a wrapper which is slower + * [FELIX-3553] - Use of parallel class loading capability of JDK7 + * [FELIX-3609] - Small optimizations + * [FELIX-3611] - Bundle certificates are not added to the CodeSource when building the BundleProtectionDomain + * [FELIX-3807] - Refreshing bundles should first grab all the bundle locks to avoid concurrent modifications of those bundles + +** New Feature + * [FELIX-3504] - [Framework] Move to OSGi R5 packages + +** Task + * [FELIX-3786] - Create system package definintions for Java 8 + + +Changes from 4.0.2 to 4.0.3 +--------------------------- + +** Bug + * [FELIX-3003] - NPE in ResolverImpl.permutateIfNeeded + * [FELIX-3296] - URLHandlers caches null as values for common protocols + * [FELIX-3302] - Adapt the URLHandlers for the 4.0 refactoring + * [FELIX-3363] - Native bundles cannot be installed on Windows Server 2008 r2 with the tag win32 + * [FELIX-3393] - Possible deadlock with reentrant calls + * [FELIX-3572] - [Framework] Resolver is not checking package space consistency for dynamic imports + +** Improvement + * [FELIX-3262] - Startup delay due to URLHandlersBundleStreamHandler + +Changes from 4.0.1 to 4.0.2 +--------------------------- + +** Bug + * [FELIX-3178] - NPE in ResolverImpl + * [FELIX-3194] - [Framework] Manifest parser is not correctly handling escapes + * [FELIX-3205] - Error resolving system.bundle dependencies + * [FELIX-3207] - Improper handling of nulls and substring matching at CapabilitySet/SimpleFilter + * [FELIX-3211] - org.apache.felix.framework.cache.BundleCache.deleteDirectoryTreeRecursive throws NPE + * [FELIX-3220] - Multiple ClassCastException(s) when invoking OSGi Service-Hooks (EventHook, FindHook) + +** Improvement + * [FELIX-3166] - Make Felix compile within Eclipse + +** New Feature + * [FELIX-3156] - Implement Bundle::getDataFile and Bundle::compareTo + +Changes from 4.0.0 to 4.0.1 +--------------------------- + +** Bug + * [FELIX-3137] - [Framework] Capabilities from resolved singleton bundles are not indexed correctly + * [FELIX-3150] - Filter parameter is ignored at getServiceReferences + * [FELIX-3153] - [Framework] refreshPackages() should stop bundles in one pass and refresh them in a second pass + +** Improvement + * [FELIX-3138] - [Framework] Refactor some unnecessary code in the resolver + * [FELIX-3141] - [Framework] Framework should filter removal pending fragments not the resolver + +Changes from 3.2.2 to 4.0.0 +--------------------------- + +** Bug + * [FELIX-2762] - Substring parser incorrectly disallowing use of parentheses characters + * [FELIX-2990] - [Framework] Bug introduced into handling of native libraries during a fresh + * [FELIX-3015] - [Framework] Provide org.osgi.util.tracker version 1.5 + * [FELIX-3033] - [Framework] Service registry hooks are not correctly filtering bundle contexts for event hook + * [FELIX-3038] - Framework does not offer J2SE-1.2 Execution Environment + * [FELIX-3043] - [Framework] Resolver is not correctly resolving fragments in all cases + * [FELIX-3059] - Reexported packages are not calculate correctly leading to resolver and class loading bugs + * [FELIX-3062] - [Framework] Resolver gets into infinite loop in the face of a require-bundle cycle + * [FELIX-3082] - [Framework] Bundles not allowed to add listeners while in STOPPING state + * [FELIX-3085] - Importing/exporting "." should not be allowed + * [FELIX-3096] - Could not add FrameworkListener from ServiceListener + +** Improvement + * [FELIX-2467] - The framework extensions should be returned when invoking the method PackageAdmin#getFragments with the system bundle + * [FELIX-2572] - JRE system packages should include "uses" constraints + * [FELIX-2950] - [Framework] Adopt OSGi R4.3 API as framework internal API + * [FELIX-2998] - [Framework] OSGi R4.3 changed filter handling of exceptions when calling equals()/compare() to return false + * [FELIX-3000] - Move sending service registered event out of bundle lock + * [FELIX-3071] - [Framework] It should be possible to limit number of open JAR files + * [FELIX-3125] - [Framework] Use single file per bundle in bundle cache by default + +** New Feature + * [FELIX-2959] - [Framework] Implement OSGi R4.3 class loader byte-code weaving hook + * [FELIX-2969] - [Framework] Implement OSGi R4.3 framework wiring object + * [FELIX-2973] - [Framework] Implement OSGi R4.3 generic capabilities and requirements + * [FELIX-2975] - [Framework] Implement OSGi R4.3 framework start level object + * [FELIX-2986] - [Framework] Implement OSGi R4.3 resolver hooks + * [FELIX-2999] - [Framework] OSGi R4.3 now specifies that number types should be trimmed when evaluating filters + * [FELIX-3032] - [Framework] Implement OSGi R4.3 bundle hooks + * [FELIX-3052] - [Framework] Implement OSGi R4.3 system bundle generic capabilities + * [FELIX-3056] - [Framework] Implement OSGi R4.3 event listener service registry hook + * [FELIX-3122] - [Framework] Implement OSGi R4.3 framework UUID + * [FELIX-3124] - [Framework] Implement OSGi R4.3 property to allow installing bundles with the same BSN and version + +** Task + * [FELIX-3110] - [Framework] Implement 4.3 security checks + + +Changes from 3.2.1 to 3.2.2 +--------------------------- + +** Bug + * [FELIX-2935] - Bundle.getEntryPaths and findEntries are returning META-INF/ multiple times + * [FELIX-2941] - Felix Framework 3.2.0 and Framework Security 1.4.2 fails OSGI Conditional Permission Admin CT when running on IBM JVM 5 and 6. + * [FELIX-2942] - Bundles with higher start level get activated even before framework reaches that start level + + +Changes from 3.2.0 to 3.2.1 +--------------------------- + +* Bug + * [FELIX-2901] - [Framework] NPE from a host bundle during framework shutdown + * [FELIX-2917] - Constructor of SecurityExceptino() which is incompatible to 1.4 is used + * [FELIX-2924] - bundle stop hangs for http-2.2.0 in felix-3.2.0 + +** Improvement + * [FELIX-2909] - [Framework] Resolver could be more efficient if it detected if fragments were present + + +Changes from 3.0.9 to 3.2.0 +--------------------------- + +* Bug + * [FELIX-1816] - deadlock on SystemBundle.stop() + * [FELIX-2741] - NPE in ResolverImpl.calculatePackageSpaces + * [FELIX-2748] - Possible synchronization issue with BundleContext.getBundle() + * [FELIX-2877] - java6 update 24 breaks felix when used inside webstart + + +** Improvement + * [FELIX-2858] - [Framework] Modify resolver to be self-contained with respect to fragment handling + * [FELIX-2859] - [Framework] Modify resolver to be self-contained with respect to singleton handling + * [FELIX-2864] - [Framework] Add package profile for JDK7 to default properties + +Changes from 3.0.8 to 3.0.9 +--------------------------- + +** Bug + * [FELIX-1581] - Bundle#update, Bundle#uninstall and Bundle#stop should wait for a (configurable) timeout before throwing an exception if the bundle is starting or stopping + * [FELIX-2784] - When the framework try to grab the bundle lock, it ignores thread interruption + * [FELIX-2822] - [Framework] System bundle module's state not reset when framework restarted leading to NPE + * [FELIX-2832] - [Framework] It should not be possible to open an URLConnection to "/" for a bundle URL + * [FELIX-2840] - [Framework] Uninstalling an uninstalled bundle throws NoSuchElementException + * [FELIX-2849] - PackageAdmin is inconsistent between getFragments and getHosts + * [FELIX-2850] - PackageAdmin return fragments / hosts even if the host isn't resolved + * [FELIX-2851] - Resolution problems after a fragment can't be resolved + +** Improvement + * [FELIX-2841] - [Framework] Improve resolve exception messages + +Changes from 3.0.7 to 3.0.8 +--------------------------- + +** Bug + * [FELIX-2749] - Boolean.parseBoolean() newly defined in Java5 should not be used. + * [FELIX-2789] - Native library matching is not correctly checking additional extensions + * [FELIX-2800] - Logging will throw an NPE if no bundle is specified + * [FELIX-2802] - A failed update doesn't rollback properly + * [FELIX-2805] - Cache PackageAdmin.getBundle() result for system bundle classes + +Changes from 3.0.6 to 3.0.7 +--------------------------- + +** Bug + * [FELIX-2456] - Framework no longer fires UNRESOLVED event when a bundle is explicitly refreshed + * [FELIX-2693] - [Framework] Service registry should throw a ServiceException for factories resulting in a cycle + * [FELIX-2700] - Framework uses java.nio package which is not supported by CDC VM + * [FELIX-2710] - [Framework] For exploded bundles Bundle.getEntry("a/b.jar/")==Bundle.getEntry("a/b.jar") + * [FELIX-2717] - [Framework] Resolver treats multiple exports of same package as conflicting for fragment imports + * [FELIX-2725] - [Framework] Resolver is not correctly calculating exported packages for already resolved modules + * [FELIX-2728] - JarRevision does not close URLConnections which are instances of HttpURLConnection + * [FELIX-2736] - [Framework] Resolver is not correctly verifying package space consistency for dynamic imports + * [FELIX-2738] - [Framework] DirectoryContent does not handle security correctly + +** Improvement + * [FELIX-2703] - [Framework] Include OSGi/Minimum EEs in default properties + * [FELIX-2721] - [Framework] Implement custom manifest parser and avoid JarFile + * [FELIX-2737] - [Framework] Optimize resolver algorithm by not re-calculating uses constraints for resolved modules + +Changes from 3.0.5 to 3.0.6 +--------------------------- + +** Bug + * [FELIX-2670] - [Framework] Implicit boot delegation doesn't delegate for external code in all cases + * [FELIX-2683] - [Framework] Bundle last modified time is not persisted on deployment + +** Improvement + * [FELIX-2560] - Bundle URLs do not survive refreshes + +** New Feature + * [FELIX-2646] - [Framework] Locking could be used to prevent concurrent access to a single bundle cache + +Changes from 3.0.4 to 3.0.5 +--------------------------- + +** Bug + * [FELIX-2653] - LinkageError caused by duplicate class definition during implicit boot delegation + +** Improvement + * [FELIX-2645] - Add a (hidden) way to retrieve a local URL for a given bundle URL + * [FELIX-2654] - [Framework] Modify bundle cache to use a single file per bundle state + +Changes from 3.0.3 to 3.0.4 +--------------------------- + +** Bug + * [FELIX-2584] - No FrameworkEvent.ERROR on unchecked exception in event listener + * [FELIX-2629] - [Framework] Module class loader should return an empty enumeration for getResources() + +** Improvement + * [FELIX-2619] - [Framework] Bundle cache is rechecking nonexistent files again and again + * [FELIX-2626] - [Framework] Bundle cache is rewriting some files when restarting bundles + +Changes from 3.0.2 to 3.0.3 +--------------------------- + +** Bug + * [FELIX-2548] - Resolver should use case sensitive indexing for capabilities + * [FELIX-2569] - Felix bundle classloader always delegates to parent loader in getResources() + * [FELIX-2589] - SecurityException "SecurityManager already installed" is thrown when calling Framework.init() multiple times with FRAMEWORK_SECURITY set + * [FELIX-2598] - Hang in Felix: thread owing a bundle lock waits for ever to lock it again + * [FELIX-2599] - When specifying the packages exported by the system bundle, attributes and directives on packages do not show up in the system bundle headers + +** Improvement + * [FELIX-1022] - Classloader Exceptions should be more informative + * [FELIX-2549] - Fix some synchronization issues in content handling + * [FELIX-2555] - Log messages should contain the bundle id, when available + * [FELIX-2597] - Deadlock during delivery of resolved event + +Changes from 3.0.1 to 3.0.2 +--------------------------- + +** Bug + * [FELIX-2421] - Implicit bootdelegation doesn't ignore classloaders if they are inner classes + * [FELIX-2437] - Deadlock on refrsh Import and refresh + * [FELIX-2451] - Felix uses System.out to print debug messages related to resolver... + * [FELIX-2459] - Wrong error message on a missing package + * [FELIX-2466] - Unknown attributes are not stripped from Fragment-Host header + * [FELIX-2473] - Subtring matching is incorreclty matching against an empty string + * [FELIX-2479] - Required bundles not correctly re-exporting their substitutable exports + +** Improvement + * [FELIX-2528] - Potential performance issue in resolver when uses constraint conflict is detected + * [FELIX-2529] - In some scenarios the resolver will not backtrack on imported package decisions + +Changes from 3.0.0 to 3.0.1 +--------------------------- + +** Bug + * [FELIX-2401] - NPE in org.apache.felix.framework.FilterImpl.DictionaryCapability.getAttribute(String) + +Changes from 2.0.5 to 3.0.0 +--------------------------- +** Sub-task + * [FELIX-2036] - Improve resolver's generic capability/requirement model + * [FELIX-2037] - Improve resolver performance by making solution space searching smarter + +** Bug + * [FELIX-995] - JRE packages are exported with wrong version + * [FELIX-1967] - Freeze finding consistent class space + * [FELIX-2080] - Updating bundles when debugging switched on might result in a deadlock with 100% CPU usage + * [FELIX-2150] - URLStreamHandlerProxy.setURL may not set query component correctly + * [FELIX-2172] - Extremely long resolve stage when running CXF-DOSGi system test + * [FELIX-2177] - Fragment bundles not loaded after second start when using autodeploy + * [FELIX-2271] - CLONE -NPE "name can't be null" when trying to install a bundle in Felix 2.0.3 + * [FELIX-2273] - getClassLoader-permission required accessing classes from dynamically loaded class + * [FELIX-2281] - Bundle id order affects fragment resolution success + * [FELIX-2317] - Possible NPE for jars with null Manifest + * [FELIX-2321] - BundleException type should be properly set when installing a duplicate bundle + * [FELIX-2332] - Lots of contention on ExtensionManager.openConnection(URL) + * [FELIX-2335] - Bundle.loadClass() for system bundle doesn't obey boot delegation + * [FELIX-2356] - extension bundle cannot load class from embed jar + * [FELIX-2383] - Bundles are restarted during start level change + * [FELIX-2392] - Felix framework uses a Java5 method + +** Improvement + * [FELIX-1210] - Allow jars with missing intermediate entries to be handled as if they were present in Bundle.getEntryPaths + * [FELIX-1797] - Customizable Framework startup message + * [FELIX-2035] - Reimplement framework resolver + * [FELIX-2039] - Reimplement standard OSGi LDAP filter to use new filter solution for resolver + * [FELIX-2040] - Modify framework service registry to use resolver's new capability/requirement model + * [FELIX-2041] - Look into using generics in framework code + * [FELIX-2042] - Use Gogo as the default shell for the framework distribution + * [FELIX-2324] - Support execution environment so that OBR works properly + * [FELIX-2336] - Variable substitution in configuration files should ignore mismatched delimiters + +Changes from 2.0.4 to 2.0.5 +--------------------------- +** Bug + * [FELIX-1753] - The start level should check that the bundle still exists before starting it to avoid an ugly exception + * [FELIX-2087] - NPE "name can't be null" when trying to install a bundle in Felix 2.0.3 + * [FELIX-2107] - Bundle.findEntries() matches '*' instead of '' + * [FELIX-2195] - Using URLDecoder.decode on locations is wrong + * [FELIX-2222] - Failure to reinstall a cached bundle will corrupt the bundle cache + +Changes from 2.0.3 to 2.0.4 +--------------------------- + +** Bug + * [FELIX-2056] - URLHandlersStreamHandler not getting handlers from frameworks that are inside a different classloader + * [FELIX-2067] - Fragment bundle ignored silently when the host Bundle-SymbolicName equals import package name + * [FELIX-2073] - The System Bundle is not providing WS Addressing + +** Improvement + * [FELIX-2071] - Missing checks inside ModuleImpl (in Framework) which causes exceptions + +Changes from 2.0.2 to 2.0.3 +--------------------------- + +** Bug + * [FELIX-1838] - PackageAdmin.getExportedPackages() duplicates output for packages with different version + * [FELIX-1867] - ModuleImpl diagnoseClassLoadError throw NullPointerException for empty package name in debug mode + * [FELIX-1917] - A few minor bugs in the framework found while embedding Felix + * [FELIX-1919] - Fragment bundle cannot be linked to its host + * [FELIX-1920] - RequiredBundle.getRequiringBundles() incorrectly calculates result + * [FELIX-1929] - getStartLevel() always reports requested start level, not active start level + * [FELIX-1982] - Documented but uninterpreted felix.cache.* properties + * [FELIX-1998] - Use UTF-8 when decoding reference location URLs + * [FELIX-2002] - Uninstalled fragments are not properly detached + +** Improvement + * [FELIX-37] - Implement security for bundle resource URLs + * [FELIX-325] - Factor out security checks from the framework/system bundle code + * [FELIX-1973] - Implement all required security checks + +** New Feature + * [FELIX-30] - Implement extension bundles + * [FELIX-1991] - Allow boot delegation class loader to be configurable per bundle + + +Changes from 2.0.1 to 2.0.2 +--------------------------- + +** Bug + * [FELIX-1754] - Usage of BundleContext.getServiceReferences results in failure to activate components + * [FELIX-1782] - Errors during start-up on gnu/classpath based VMs (jamvm, kaffe, cacao) and mika + * [FELIX-1795] - Bundle version ignored in Fragment-Host header + * [FELIX-1834] - java.io.IOException: No framework context found when embedding felix frameworks as bundles + +** Improvement + * [FELIX-1534] - Improve fragment merging + * [FELIX-1781] - Try to reduce object allocations/usage in resolver algorithm + * [FELIX-1783] - Eliminate contention on ServiceRegistry.getServiceReferences(String, Filter) + +Changes from 2.0.0 to 2.0.1 +--------------------------- + +** Bug + * [FELIX-1565] - Deadlock UrlHandlers + * [FELIX-1573] - Occasional NPE in URLHandlersBundleStreamHandler + * [FELIX-1580] - Regression with native library handling + * [FELIX-1586] - Framework reports org.osgi.framework.version as 1.3 + * [FELIX-1600] - ServiceReference.isAssignableTo() always returns true if requesting bundle has no wire + * [FELIX-1631] - Implicit bootdelegation causes hang on android + * [FELIX-1710] - Resolver still does not discard all partial results when a cyclical dependency fails + * [FELIX-1721] - Framework boot delegation has a bug due to extraneous code + * [FELIX-1731] - Native library extraction could be improved to help cases where there are dependencies among libraries + +** Improvement + * [FELIX-1619] - Change the default auto-deploy actions to be install and start only + * [FELIX-1625] - Refactor bundle cache to simplify management + * [FELIX-1679] - VersionRange class should use finals to be thread safe. + * [FELIX-1724] - Various module metadata should be cached + +** Task + * [FELIX-1617] - Modify framework, main, shell, shell.tui, and obr to depend on official OSGi JAR files + +Changes from 1.8.1 to 2.0.0 +--------------------------- + +** Bug + * [FELIX-893] - Felix fails to start using J9 JVM + * [FELIX-905] - Felix needs an RFC 126 FindHook + * [FELIX-906] - Felix needs an RFC 126 EventHook + * [FELIX-1122] - Extension bundles are not being removed from the bundle list when uninstalled + * [FELIX-1123] - System bundle does still not correctly handle (export) package attributes + * [FELIX-1124] - ResourceNotFoundException too verbose + * [FELIX-1130] - Bundle.getHeaders() returns a Dictionary + * [FELIX-1138] - URL Handlers performance regression due to service lookups + * [FELIX-1170] - MemoryLeak when stopping and restarting Felix + * [FELIX-1187] - BundleContext.ungetService() should return false only if the usage count is zero when the method is invoked + * [FELIX-1197] - Bundle Fragments not resolved correctly + * [FELIX-1198] - config.properties still refers to old org.osgi.framework.startlevel property + * [FELIX-1247] - BundleEvent.UNRESOLVED should be fired during update/uninstall not refresh + * [FELIX-1249] - Bundle.findEntries() should search fragments as well as the bundle itself + * [FELIX-1254] - Bundle#findEntries does not return resources from fragments + * [FELIX-1271] - Improve manifest localization to handle special cases + * [FELIX-1272] - Need to special case getResource()/getResources()/loadClass() for fragment bundles + * [FELIX-1273] - Bundle.getResources() should return null for a non-existent resource + * [FELIX-1277] - Fix Service Hooks Tests failures in RFC 126 TCK + * [FELIX-1279] - Framework.waitForStop() does not obey timeout + * [FELIX-1280] - Package Admin - getExportedPackages must return null instead of an empty array + * [FELIX-1285] - SecureAction captures the calling context incorrectly + * [FELIX-1286] - Module class loader should use secure action instead of a privileged block + * [FELIX-1287] - System bundle operations from RFC-132 (e.g., init(), start(), stop(), waitForStop()) are using wrong lock object + * [FELIX-1288] - System bundle context should be null after stopping the framework + * [FELIX-1292] - PackageAdmin.getBundle(Class) should return null if the bundle associated with the passed in class is from another framework + * [FELIX-1293] - StringMap used for case insensitive properties does not respect ordering if case sensitivity is changed + * [FELIX-1295] - ServiceRegistry increments/decrements service use count after/before getService/ungetService() is called on ServiceFactory + * [FELIX-1371] - Automatic parent class loader delegation does not correctly filter calls to Bundle.loadClass() + * [FELIX-1397] - Required execution environment verification should happen at resolve time, not install time + * [FELIX-1400] - bootdelegation and dynamic-import-packages are accepting invalid patterns + * [FELIX-1401] - Manifest localization with fragments not handled correctly + * [FELIX-1422] - Resolver does not always discard partial results when a cyclically dependency fails + * [FELIX-1435] - Resolver does not always resolve a dynamic import to a fragment export + * [FELIX-1527] - R4.2 spec errata now specifies uninstalling a bundle must transition through INSTALLED on its way to UNINSTALLED + * [FELIX-1551] - Start level service must synchronously persist bundle start level changes + * [FELIX-1556] - Bundle.getResource/s is not able to find resources if the package is not alraedy wired when I use DynamicImport-Package: * + +** Improvement + * [FELIX-712] - Ability to disable automatic parent classloader delegation + * [FELIX-883] - JarContent logs and then swallows exceptions when reading from JAR file, should probably throw an exception + * [FELIX-1120] - Enable BundleCache customization + * [FELIX-1134] - Add support for native libraries in fragments + * [FELIX-1189] - Improve error message in main when there is an error processing auto-install/start bundles + * [FELIX-1246] - PackageAdmin.getBundle() is not implemented efficiently + * [FELIX-1260] - Make Bundle.findEntries() and Bundle.getEntryPaths() more thread safe + * [FELIX-1291] - Implement support for proper return type from Framework.waitForStop() + * [FELIX-1300] - Remove legacy bundle cache support when extracting embedded JAR files + * [FELIX-1360] - Improve native library matching algorithm (part 2) + * [FELIX-1404] - Use provided classes from OSGi R4.2 companion code + * [FELIX-1432] - Manifest parser doesn't return import package declarations in order of manifest + * [FELIX-1462] - Felix framework launcher should only use standard launching API + +** New Feature + * [FELIX-33] - Implement system bundle update + * [FELIX-749] - Add support for lazy activation of bundles + * [FELIX-1193] - Implement org.osgi.framework.bundle.parent from RFC 132 + * [FELIX-1205] - Update to the latest OSGi R4.2 API + * [FELIX-1244] - Add support for ServiceEvent.MODIFIED_ENDMATCH + * [FELIX-1250] - Support service exceptions for service factories + * [FELIX-1289] - Support for FrameworkUtil.getBundle() + * [FELIX-1297] - Implement support for new org.osgi.framework.command.execpermission configuration property + * [FELIX-1298] - Implement support for new org.osgi.framework.library.extensions configuration property + * [FELIX-1446] - Framework launcher should automatically deploy bundles in bundle directory + * [FELIX-1478] - Add shutdown hook to launcher to cleanly shutdown the framework if the process is killed + + + + +** Task + * [FELIX-1144] - The NOTICE file for Main subproject is not correctly copied into the source JAR + + +** Test + * [FELIX-1208] - Need migrate the EventDispatcherTest to newer version of EasyMock + +Changes form 1.6.1 to 1.8.0 +--------------------------- +** Bug + * [FELIX-1034] - bootdelegation property seems to be matching more packages than desired + * [FELIX-1059] - DynamicImport-Package matches more packages than desired + +** Improvement + * [FELIX-1060] - URLHandlers doesn't support URLStreamHandler.openConnection(proxy,url) method + +** New Feature + * [FELIX-29] - Implement bundle fragments + +Changes from 1.6.0 to 1.6.1 +--------------------------- +** Bug + * [FELIX-1027] - deadlock with felix 1.6.0 ? + * [FELIX-1028] - NPE in configuration view when running webconsole with Equinox + * [FELIX-1033] - Exceptions when Felix is started with security manager + * [FELIX-1035] - deadlock observed while using fileinstall to monitor multiple directories + * [FELIX-1045] - Felix 1.6.0 fails with ClassCircularityError + +Changes from 1.4.1 to 1.6.0 +--------------------------- + +** Bug + * [FELIX-737] - Resolver does not correctly discard export when module imports the same package (part 2) + * [FELIX-852] - Fragment support is currently limited to directly resolved hosts + * [FELIX-869] - SCR throws exception on invalidating a component with a reference to a service that was already unregistered + * [FELIX-891] - Bundle lock acquisition should not record lock until it verifies the bundle is lockable + * [FELIX-892] - ServiceReferenceImpl improper implementation of equals and hashcode + * [FELIX-893] - Felix fails to start using J9 JVM + * [FELIX-897] - Empty system package is exported when a semicolon is present in "extra" configuration + * [FELIX-902] - Empty bundle.state file produces NPE + * [FELIX-910] - Framework may ignore framework startlevel on startup + * [FELIX-911] - Potential deadlock between Bundle.stop() and BundleContext.registerService() + * [FELIX-915] - PackageAdmin returns null on getBundle(...) with system classes + * [FELIX-934] - Bootdelegation bug + * [FELIX-947] - Behaviour of startlevel comman doesn't adhere to spec + * [FELIX-948] - ServiceReferenceImpl.compareTo should handle other types than integer for service ranking + * [FELIX-951] - Deadlock in iPojo when stopping Felix + * [FELIX-952] - Exception thrown when unregistering services because the bundle is stopped + * [FELIX-953] - Bundle#loadClass sometimes return null instead of throwing a CNFE + * [FELIX-961] - 100% CPU looping inside uses calculation + * [FELIX-962] - Erroneous class loading delegation to the application launcher classloader in some cases + * [FELIX-969] - system bundle does not correctly handle (export) package attributes + * [FELIX-971] - Exception thrown in ServiceTracker at shutdown + * [FELIX-978] - Resolver does not clean up properly on a failed recursive attempt to resolve + * [FELIX-1004] - Extensionmanager openConnection(URL) method should be public + * [FELIX-1005] - Strange list of imported packages returned by the package admin + +** Improvement + * [FELIX-681] - Modify daemon threads to catch all exceptions and log them to avoid premature thread death + * [FELIX-851] - Refactor the module abstraction layer to align more closely to OSGi concepts + * [FELIX-872] - JarContent swallows exception when opening manifest, it should log it + * [FELIX-883] - JarContent logs and then swallows exceptions when reading from JAR file, should probably throw an exception + * [FELIX-894] - Loosen locking when starting/stopping/uninstalling a bundle by firing event without holding a lock + * [FELIX-908] - Unsynchronize access to bundle state inside BundleInfo by making the variable volatile + +Changes from 1.4.0 to 1.4.1 +--------------------------- + +* [2008-12-19] Refactor the URLHandlers implementation to fix a possible + Linkage-Error when running embedded inside tomcat. Furthermore, make it possible + to dynamically set a SecurityManager, change the url we add to the framework + classloader to fix issues with rmi and make sure we restore the previous URLStreamHandlerFactory + after stopping. (FELIX-842, FELIX-837, FELIX-844, FELIX-827) +* [2008-12-19] Add missing javax.security.cert in JRE 1.4 and 1.5. (FELIX-854) +* [2008-12-19] Improve framework internal locking protocol. +* [2008-12-18] Fixed an issue where bundles with a non-existing native library + would not be removed correctly. (FELIX-835) +* [2008-11-21] Throw an exception when there is an attempt to start or stop a + fragment, as per the spec. (FELIX-820) +* [2008-11-20] Fixed a bug during shutdown where uninstalled fragments are not + properly closed. (FELIX-819) +* [2008-11-14] Added partial support for new service registry hooks as proposed + for OSGi R4.2; currently, only listener hooks are supported. (FELIX-804) +* [2008-11-08] Fixed Felix' delegation hack so that it correctly delegates to + the parent class loader for resources as appropriate; previously it was always + delegating for class loading, which was incorrect. (FELIX-808) + +Changes from 1.2.2 to 1.4.0 +--------------------------- + +* [2008-11-05] URLStreamHandlerService and ContentHandlerService override + built-in handlers and make it possible to use URLHandlers when extending the + Felix class. (FELIX-756, FELIX-800) +* [2008-11-04] Implement ServiceReference.compareTo() method. (FELIX-799) +* [2008-10-31] Fix some visibility issues in the LDAP operators which could + result in incorrect toString values. (FELIX-765) +* [2008-10-31] Fixed spec compliance issues around getting a service reference + from an invalid registration and throwing an exception when there are + duplicate property keys. (FELIX-798) +* [2008-10-21] Fixed an issue with extension bundles which would be installed + as fragments and fail to install extension bundles if they have incomplete + metadata. (FELIX-770) +* [2008-10-21] Fix a possible deadlock in URLHandlers. (FELIX-748) +* [2008-10-16] Modified framework to have default values for the system + packages property, which also required modifying main to no longer expect to + set it. Now it is possible to start Felix with no configuration properties. + (FELIX-753) +* [2008-10-16] Update felix to work with the 1.0.0-rc1 version of the android + sdk. +* [2008-10-15] Implemented remaining PackageAdmin methods from R4. (FELIX-35) +* [2008-10-15] Modified resolver to correctly mark fragment modules as resolved. + (FELIX-777) +* [2008-10-15] Modified the resolver to longer consider uninstalled fragments + and hosts when resolving dependencies; this required a new "stale" flag on + modules that gets set when their associated bundle is uninstalled. (FELIX-776) +* [2008-10-15] Modified the resolver to sort candidates when resolving + Require-Bundle dependencies. (FELIX-775) +* [2008-10-14] Modified ExportedPackage.getImportingBundles() to include + bundles requiring the exporting bundle, in addition to those bundles + importing the package. Also tried to simplify dependency management code + by separating it out. (FELIX-764) +* [2008-10-14] Fix a NullPointerException in SecureAction when a + SecurityManager is used by keeping pointers on the local stack. (FELIX-766) +* [2008-10-13] Use System.gc to allow to update and uninstall bundles with + native libs on Windows. (FELIX-733) +* [2008-10-10] Set the activator to null on Bundle.stop() to allow for earlier + garbage collection. (FELIX-762) +* [2008-10-10] Implements support for flushing the cache on framework + initialization. (FELIX-755) +* [2008-10-10] Improve exception messages. (FELIX-750) +* [2008-10-09] Modified the bundle cache to no longer have profiles. (FELIX-754) +* [2008-10-08] Modified the Felix API to aligned with the proposed standard + OSGi framework API. The framework instance can now be stopped and restarted. + (FELIX-753) +* [2008-09-29] Support transiently starting/stopping bundles. (FELIX-713) +* [2008-09-25] Correctly discard exported packages when a bundle is chosen to + import a package it exports. (FELIX-736) +* [2008-09-24] Clearly distinguish between "resolved" and "unresolved" + capabilities, which is necessary since "resolved" capabilities have higher + priority than "unresolved" ones. (FELIX-730) +* [2008-09-24] Do not bootdelegate in the case where Bundle.loadClass() has + been used. (FELIX-712) +* [2008-09-23] Improve decoding of reference URLs. (FELIX-731) +* [2008-09-23] For completeness, added symbolic names to framework and main. +* [2008-09-17] Improve gc by fixing an issue with StartLevel and PackageAdmin. + (FELIX-728) +* [2008-09-15] Fix an issue with Require-Bundle not aggregating packages + correctly. (FELIX-722) +* [2008-09-15] Fixed a bug where class loader delegation for dynamic imports + was happening when it shouldn't. (FELIX-724) +* [2008-09-12] Added a configuration property to determine whether installing a + fragment that uses unimplemented features throws an exception or logs a + warning. (FELIX-725) +* [2008-09-12] Removed some experimental code for "implicit requirements", + since it was no longer needed. +* [2008-09-12] Fix some visibility issues within the LDAP filter. (FELIX-721) + +Changes from 1.2.1 to 1.2.2 +--------------------------- + +* [2008-10-14] Fixed a NullPointer exception that could happen when + running with a SecurityManager as a backport of FELIX-766. + +Changes from 1.2.0 to 1.2.1 +--------------------------- + +* [2008-09-05] Fixed a performance regression that was caused by uncached + access to the bundle manifest headers. (FELIX-711) +* [2008-09-03] Fixed a bug in calculating the system bundle's exported + packages which added a null export package. + +Changes from 1.0.4 to 1.2.0 +--------------------------- + +* [2008-08-29] Support JAR-wide manifest package metadata. (FELIX-682) +* [2008-08-29] Throw an exception when installing a fragment that uses + features that we do not support. +* [2008-08-20] Improve error reporting when loading bundle classes using + Bundle.loadClass. +* [2008-08-16] Fix attribute checking when resolving dynamic imports. + (FELIX-676) +* [2008-08-08] Implement singleton bundle support. (FELIX-102) +* [2008-08-06] Default to current security policy if no security extension is + installed in order to make Subject.doAs work. (FELIX-654) +* [2008-08-04] Return Bundle.getLastModified() for bundle URLs + lastModified(). (FELIX-582) +* [2008-08-04] Improve handling of improper resource URLs. (FELIX-577) +* [2008-08-01] Add necessary stub methods to implement new R4.1 methods. + (FELIX-595) +* [2008-08-01] Applied patch to properly return symbolic name when it has + parameters. (FELIX-650) +* [2008-07-25] Fix some rollback issues after a bundle update throws an + exception. (FELIX-647) +* [2008-07-23] Modified manifest parser to be more lenient. (FELIX-641) +* [2008-07-07] Add support for the standard symbolic name for the system + bundle. (FELIX-602) +* [2008-06-01] Add initial fragment support for extending the host bundle + class path. (FELIX-29, FELIX-656) + +Changes from 1.0.3 to 1.0.4 +___________________________ + +* [2008-04-18] Check that the returned object from a service factory is an + instance of all classes named when the service was registered. (FELIX-540) +* [2008-04-13] Fix an issue when logging messages while holding framework + internal locks that could lead to a deadlock. For now we just disable + logging to log services inside the framework. (FELIX-536) +* [2008-04-13] Implemented various small performance improvments e.g., + we now cache filters and create an index for objectclass to improve + service lookup performance and don't use the BundleInfo to get the + bundle id but the BundleImpl directly. Furthermore, we added an index to + get bundles by id. +* [2008-04-04] Fix an NoClassDefFoundError when trying to query the + framework classloader for whether a class is available or not. +* [2008-03-19] Don't try to resolve extension bundles and fix a + classcastexception that could happen if more then one framework is around + and both have extensions installed. Furthermore, make extension bundles + use the system bundle context. +* [2008-03-07] Bundle.getResource does now check for AdminPermission. + (FELIX-150) +* [2008-03-06] Make urlhandlers work when a handler factory is already set. + Furthermore, it is now possible to have more then one framework running + in more then one classloader. (FELIX-38). +* [2008-03-05] Make LDAP filter reentrant. +* [2008-02-29] Refactored the IContent abstraction from the module loader + layer to provide more generic access to the content of the module. This + makes it possible that a bundle's class path could span multiple module's, + which will be necessary for fragments. (FELIX-29) +* [2008-02-27] Applied a patch to disable the class loading diagnostic message + when the logger is not at the DEBUG level. (FELIX-500) +* [2008-02-15] Modified the service registry to use more fine-grained locking + to avoid callbacks to service factories while holding locks. (FELIX-489) + +Changes from 1.0.1 to 1.0.3 +--------------------------- + +* [2008-01-27] Moved framework docs to main, since the launcher examples now + depend on main for the auto-property handling. +* [2008-01-25] Applied patches to improve how Felix finds resources when + getResources() is called. (FELIX-466, FELIX-467) +* [2008-01-25] Make FilterImpl.toString() add escape characters where needed. + (FELIX-471) +* [2008-01-16] Fix a NPE on framework restart when extension bundles are + installed. +* [2008-01-15] Try to fix a potential visibility issue on loading a class + form a bundle where it seems to be possible that we create two classloaders + instead of one. +* [2008-01-15] Modified boot delegation to not terminate when delegating to + parent class loader for non-java.* packages. (FELIX-463) +* [2008-01-04] Fix some issues related to directories on the bundle class + path. Specifically, leading slashes created an issue and are now stripped + and entries were not being properly filtered when enumerating the contents + of a class path directory. (FELIX-426) +* [2007-12-20] Modified logger to have all of its public methods be final to + avoid it from being extended in undesired ways via the constructor supplied + logger. (FELIX-428) +* [2007-12-20] The TCK has changed to verify that bundles do not depend on + themselves, so we filter that case now. +* [2007-12-20] Applied patch from Guillaume Nodet to properly fire a framework + error event only when a bundle cannot be resolved. (FELIX-441) +* [2007-12-19] Modified framework to accept a Logger instance so that host + applications can do custom logging until the log service arrives. (FELIX-428) +* [2007-12-19] Add support for loading bundles on Google Android (FELIX-440). +* [2007-12-18] Improve native code loading - bundle installation will now + fail in case a native library is not in the jar and we only use the first + library of a given name. Furthermore, we now support .dylib extensions on + the mac as well as others; should make it possible to use .netmodules as + well. (FELIX-439) +* [2007-12-17] Make the win32 alias match any version of windows for native + clauses. (FELIX-438) +* [2007-12-14] Removed auto-property processing out of the framework and + into the default launcher, i.e, main. (FELIX-393) +* [2007-12-13] Fix a StackOverflowError in URLHandlers.createStreamHandler() + when creating URL on jamvm and Mika. This patch resolves an unfortunate + interaction between our ExtensionManager and the URLHandlers by making the + URLHandlers aware of the extension protocol. Pretty much like we do already + for the bundle protocol. (FELIX-435) +* [2007-11-26] Fix a small oversight in the extension manager that could lead + to a null pointer exception and save some memory by creating less objects. +* [2007-11-26] Degrate to version 0.0.0 if we can not find the Felix.properties + for some reason and avoid a null pointer in this case. +* [2007-11-08] Reorganized usage count methods to better handle null + reference checking in response to Karl Pauls seeing an NPE when trying to + get a service that was already unregistered while shutting down the + framework. +* [2007-11-06] Added a simple check to detect and work around a bug in J9. + (FELIX-416) +* [2007-10-30] Change how the extension manager url stream handler handles + request to the root path in order to make some tomcat issue go away. + (FELIX-414) +* [2007-10-26] Added support for "/" bundle resources as requested. (FELIX-383) +* [2007-10-24] Use system bundle when firing a framework error event when an + install error occurs. +* [2007-10-22] Fix a NPE when getEntryPaths is called on the system bundle. + (FELIX-394) +* [2007-10-16] Modifies bundle resource URL handling such that if a resource + URL points to a resource that does not exist, a bundle class path search + for the resource will be instigated and if any matching resource is found, + that one will be used instead. (FELIX-383) +* [2007-10-10] Auto-property handling now installs bundles into the default + bundle start level if a start level is not specified. (FELIX-359) +* [2007-10-08] Overwrite the URLStreamHandler.getHostAddress(URL) in the + ExtensionManager to immediately return null to prevent DNS lookup. + (FELIX-388) +* [2007-09-30] Resolved a concurrency issue that could result in the same + bundle being resolved more than once; also tried to simplify locking in + the core search policy implementation. (FELIX-381) + +Changes from 1.0.0 to 1.0.1 +--------------------------- + +* [2007-07-23] Fixed a bug in the framework shutdown process which was + causing threads calling Felix.stopAndWait() to not get notified that + the framework had shutdown. (FELIX-329) +* [2007-08-15] Fixed a bug in the LDAP filter which was not thread safe + on execution. (FELIX-338) +* [2007-08-17] Added support for persistent last used bundle ID to avoid + re-use of bundle identifiers. (FELIX-339) +* [2007-08-23] Modified BundleImpl.getHeaders(Locale) to accept a null + locale. (FELIX-346) +* [2007-09-11] Added support to handle external termination of Felix + which was not handled or detected and prevented a restart of future + Felix instances in the same VM. (FELIX-363) +* [2007-09-12] Add support for Collection values in the LDAP filter to + match the 4.1 spec. +* [2007-09-12] Fixed a bug in the LDAP filter where attributes where + limited to [a-zA-Z ] so didn't allow for e.g., numbers. (FELIX-361) +* [2007-09-13] Enable support for exporting the same package more than + once (FELIX-101). +* [2007-09-13] Returns the system bundle from a call to + PackageAdmin.getBundle(Class) in case the class comes from the + classloader (or one of its parents) that loaded Felix and the system + bundle exports the package. +* [2007-09-16] Fixed a bug in class space filtering in the service registry + that could result in class cast exceptions for service clients. +* [2007-09-16] Fixed a bug that prevented extension bundle exports to be + usable. + +Changes from 0.8.0-incubator to 1.0.0 +------------------------------------- + +* [2007-01-18] Added support for bundle header localization. (FELIX-27) +* [2007-01-22] Modified framework resolver to support a generic + capability/requirement model. +* [2007-01-22] Reorganized and centralized manifest parsing code and added + support for resolver's generic capability/requirement model. (FELIX-98) +* [2007-01-22] Improved native library naming normalization. (FELIX-26) +* [2007-01-23] No longer eagerly resolving classes loaded from modules + since this was causing verification errors with IBM J9. +* [2007-01-25] Added some support for execution environment checking. + (FELIX-23) +* [2007-01-29] Added support for getAllServiceReferences(). (FELIX-32) +* [2007-01-31] Added Require-Bundle support to resolve using the generic + capability/requirement model of the resolver. (FELIX-28) +* [2007-02-05] Fixed a bug in processor type normalization for x86-64 + processors. +* [2007-02-09] The resolver previously ignored packages that were pending + removal when resolving new bundles, now it does not. +* [2007-02-09] Bundles are automatically refreshed when updated/uninstalled + if none of their exported packages are in use. +* [2007-02-13] Added support for extension bundles. (FELIX-30) +* [2007-03-02] Added a Bundle.getBundleContext() method until actual + support for OSGi R4.1. +* [2007-04-26] Modified Bundle.findEntries() to return URLs to directory + entries as well as file entries. +* [2007-05-06] Modified LDAP evaluator to special case the fact that + BigDecimal is not available in Foundation profile. +* [2007-05-21] Made some performance improvements in LDAP evaluation. +* [2007-05-22] Modified JAR file to include Service Tracker package. +* [2007-05-22] Improved concurrency handling around checking for already + loaded classes and defining classes. +* [2007-06-05] Modified resource URLs to use port number rather than + prepend information to the resource path. +* [2007-06-13] Improved dynamic imports to cycle through all available + candidates when checking for class space consistency. +* [2007-06-18] Improved service registry filtering based on class versions + to allow a bundle to register a service for a different version of class + that it can access. +* [2007-06-21] Modified our "last ditch effort" to guess when to delegate + to the system bundle to make it a little more robust. +* [2007-06-29] Fixed a bug in EventDispatcher that was causing asynchronous + events to not be fired after stopping the framework instance and creating + a new instance. (FELIX-314) +* [2007-07-03] Fixed a bug in EventDispatcher that would not correctly + update a listener when it implemented multiple listener interfaces. +* [2007-07-04] Modified Felix framework class to implement the Bundle + interface to improve the startup/shutdown sequence and to provide a + simplified API for creating framework instances. +* [2007-07-11] Removed the PropertyResolver-related classes and now only + use Maps for configuration properties. (FELIX-324) diff --git a/framework/etc/all.policy b/framework/etc/all.policy deleted file mode 100644 index dc85222db29..00000000000 --- a/framework/etc/all.policy +++ /dev/null @@ -1,3 +0,0 @@ -grant { - permission java.security.AllPermission; -}; diff --git a/framework/etc/config.properties.gui b/framework/etc/config.properties.gui deleted file mode 100644 index fb62d12d977..00000000000 --- a/framework/etc/config.properties.gui +++ /dev/null @@ -1,4 +0,0 @@ -felix.auto.start.1=file:bundle/shell.jar file:bundle/tablelayout.jar \ - file:bundle/shellgui.jar file:bundle/shellplugin.jar \ - file:bundle/bundlerepository.jar - diff --git a/framework/etc/config.properties.text b/framework/etc/config.properties.text deleted file mode 100644 index 0486a4d5dcd..00000000000 --- a/framework/etc/config.properties.text +++ /dev/null @@ -1,3 +0,0 @@ -felix.auto.start.1=file:bundle/shell.jar file:bundle/shelltui.jar \ - file:bundle/bundlerepository.jar - diff --git a/framework/etc/example.policy b/framework/etc/example.policy deleted file mode 100644 index 961318a9713..00000000000 --- a/framework/etc/example.policy +++ /dev/null @@ -1,40 +0,0 @@ -grant codeBase "file:bundle/shellgui.jar" { - permission org.osgi.framework.ServicePermission "org.ungoverned.osgi.bundle.shellgui.Plugin", "get"; -}; - -grant codeBase "file:bundle/shelltui.jar" { - permission org.osgi.framework.ServicePermission "org.ungoverned.osgi.service.shell.ShellService", "get"; -}; - -grant codeBase "file:bundle/shellplugin.jar" { - permission java.security.AllPermission; -}; - -grant codeBase "file:bundle/bundlerepository.jar" { - permission java.security.AllPermission; -}; - -grant codeBase "file:bundle/permissionmanager.jar" { - permission java.security.AllPermission; -}; - -grant codeBase "file:bundle/shell.jar" { - permission java.security.AllPermission; -}; - -grant codeBase "file:${lib.dir}/felix.jar" { - permission java.security.AllPermission; -}; - -grant codeBase "file:${lib.dir}/osgi.jar" { - permission java.security.AllPermission; -}; - -grant codeBase "file:${lib.dir}/moduleloader.jar" { - permission java.security.AllPermission; -}; - -grant { - permission java.io.FilePermission "${user.home}${file.separator}.felix${file.separator}-", "read, write, delete"; - permission org.osgi.framework.PackagePermission "*", "EXPORT"; -}; diff --git a/framework/etc/felix.bat b/framework/etc/felix.bat deleted file mode 100644 index ed8250304a8..00000000000 --- a/framework/etc/felix.bat +++ /dev/null @@ -1,16 +0,0 @@ -@echo off - -rem -rem The following 'set' command should be automatically -rem assigned during install, if not, edit it to reflect -rem your Java installation. -rem - -set JAVA_HOME="%%Java directory%%" - -rem -rem You do not need to edit the following. -rem - -%JAVA_HOME%\bin\java -jar lib\felix.jar - diff --git a/framework/etc/felix.sh b/framework/etc/felix.sh deleted file mode 100644 index 9ec3e09a028..00000000000 --- a/framework/etc/felix.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh - -# -# The following variable should be automatically -# assigned during install, if not, edit it to reflect -# your Java installation. -# - -java_home=%%Java directory%% - -# -# You don't need to edit the following line -# - -exec ${java_home}/bin/java -jar lib/felix.jar diff --git a/framework/pom.xml b/framework/pom.xml index 268f5fd07da..f04490cab72 100644 --- a/framework/pom.xml +++ b/framework/pom.xml @@ -1,31 +1,169 @@ - + + org.apache.felix - felix - 0.8.0-SNAPSHOT + felix-parent + 6 + ../../pom/pom.xml 4.0.0 - jar - Apache Felix OSGi Framework Implementation + bundle + Apache Felix Framework org.apache.felix.framework - - - ${pom.groupId} - org.apache.felix.shell - ${pom.version} - - - ${pom.groupId} - org.osgi.core - ${pom.version} - - + 6.1.0-SNAPSHOT + + $ + 6 + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/framework + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/framework + http://svn.apache.org/repos/asf/felix/framework + + + + + org.apache.felix + maven-bundle-plugin + 4.1.0 + true + + + org.apache.felix.framework + Apache Felix Framework + OSGi R7 framework implementation. + The Apache Software Foundation + + org.osgi.framework.*;-split-package:=first, + org.osgi.resource;-split-package:=first, + org.osgi.resource.dto;-split-package:=first, + org.osgi.service.packageadmin;-split-package:=first, + org.osgi.service.startlevel;-split-package:=first, + org.osgi.service.url;-split-package:=first, + org.osgi.service.resolver, + org.osgi.util.tracker;-split-package:=first, + org.osgi.dto;-split-package:=first + + org.apache.felix.framework.*, org.apache.felix.resolver.* + !* + + + + + org.apache.rat + apache-rat-plugin + + + verify + + check + + + + + + src/** + + + src/main/appended-resources/** + src/**/packageinfo + src/main/resources/META-INF/services/org.osgi.framework.launch.FrameworkFactory + src/main/resources/org/apache/felix/framework/Felix.properties + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + false + + + src/main/resources true - + + + + org.osgi + org.osgi.annotation + 6.0.0 + provided + + + org.apache.felix + org.apache.felix.resolver + 2.1.0-SNAPSHOT + provided + + + org.osgi + org.osgi.core + + + + + org.ow2.asm + asm-all + 4.2 + test + + + org.easymock + easymock + 2.5.2 + test + + + org.mockito + mockito-all + 1.10.19 + test + + + junit + junit + 4.12 + test + + + org.codehaus.mojo + animal-sniffer-annotations + 1.9 + + diff --git a/framework/src/main/appended-resources/META-INF/DEPENDENCIES b/framework/src/main/appended-resources/META-INF/DEPENDENCIES new file mode 100644 index 00000000000..984b61d7361 --- /dev/null +++ b/framework/src/main/appended-resources/META-INF/DEPENDENCIES @@ -0,0 +1,22 @@ +Copyright 2015 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2014). +Licensed under the Apache License 2.0. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2014). +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 diff --git a/framework/src/main/appended-resources/META-INF/LICENSE b/framework/src/main/appended-resources/META-INF/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/framework/src/main/appended-resources/META-INF/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/framework/src/main/appended-resources/META-INF/NOTICE b/framework/src/main/appended-resources/META-INF/NOTICE new file mode 100644 index 00000000000..d4d1ad82269 --- /dev/null +++ b/framework/src/main/appended-resources/META-INF/NOTICE @@ -0,0 +1,4 @@ +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2015). +Licensed under the Apache License 2.0. diff --git a/framework/src/main/java/org/apache/felix/framework/BundleContextImpl.java b/framework/src/main/java/org/apache/felix/framework/BundleContextImpl.java index d97a1178567..3b79f1a9337 100644 --- a/framework/src/main/java/org/apache/felix/framework/BundleContextImpl.java +++ b/framework/src/main/java/org/apache/felix/framework/BundleContextImpl.java @@ -1,38 +1,57 @@ /* - * Copyright 2005 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework; import java.io.File; import java.io.InputStream; -import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.Dictionary; -import java.util.List; - -import org.apache.felix.framework.ext.FelixBundleContext; -import org.osgi.framework.*; -class BundleContextImpl implements FelixBundleContext +import org.osgi.framework.AdminPermission; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.BundleListener; +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceObjects; +import org.osgi.framework.ServicePermission; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.SynchronousBundleListener; + +class BundleContextImpl implements BundleContext { + private Logger m_logger = null; private Felix m_felix = null; private BundleImpl m_bundle = null; private boolean m_valid = true; - protected BundleContextImpl(Felix felix, BundleImpl bundle) + protected BundleContextImpl(Logger logger, Felix felix, BundleImpl bundle) { + m_logger = logger; m_felix = felix; m_bundle = bundle; } @@ -42,32 +61,17 @@ protected void invalidate() m_valid = false; } - public void addImportPackage() throws BundleException - { - throw new BundleException("Not implemented yet."); - } - - public void removeImportPackage() throws BundleException - { - throw new BundleException("Not implemented yet."); - } - - public void addExportPackage() throws BundleException - { - throw new BundleException("Not implemented yet."); - } - - public void removeExportPackage() throws BundleException - { - throw new BundleException("Not implemented yet."); - } - public String getProperty(String name) { checkValidity(); - + + // CONCURRENCY NOTE: This is a check-then-act situation, + // but we ignore it since the time window is small and + // the result is the same as if the calling thread had + // won the race condition. + Object sm = System.getSecurityManager(); - + if (sm != null) { if (!(Constants.FRAMEWORK_VERSION.equals(name) || @@ -81,14 +85,19 @@ public String getProperty(String name) new java.util.PropertyPermission(name, "read")); } } - + return m_felix.getProperty(name); } public Bundle getBundle() { checkValidity(); - + + // CONCURRENCY NOTE: This is a check-then-act situation, + // but we ignore it since the time window is small and + // the result is the same as if the calling thread had + // won the race condition. + return m_bundle; } @@ -96,8 +105,13 @@ public Filter createFilter(String expr) throws InvalidSyntaxException { checkValidity(); - - return new FilterImpl(m_felix.getLogger(), expr); + + // CONCURRENCY NOTE: This is a check-then-act situation, + // but we ignore it since the time window is small and + // the result is the same as if the calling thread had + // won the race condition. + + return new FilterImpl(expr); } public Bundle installBundle(String location) @@ -110,14 +124,19 @@ public Bundle installBundle(String location, InputStream is) throws BundleException { checkValidity(); - + + // CONCURRENCY NOTE: This is a check-then-act situation, + // but we ignore it since the time window is small and + // the result is the same as if the calling thread had + // won the race condition. + Bundle result = null; - + Object sm = System.getSecurityManager(); - + if (sm != null) { - result = m_felix.installBundle(location, is); + result = m_felix.installBundle(m_bundle, location, is); // Do check the bundle again in case that is was installed // already. ((SecurityManager) sm).checkPermission( @@ -125,59 +144,92 @@ public Bundle installBundle(String location, InputStream is) } else { - result = m_felix.installBundle(location, is); + result = m_felix.installBundle(m_bundle, location, is); } - + return result; } public Bundle getBundle(long id) { checkValidity(); - - return m_felix.getBundle(id); + + // CONCURRENCY NOTE: This is a check-then-act situation, + // but we ignore it since the time window is small and + // the result is the same as if the calling thread had + // won the race condition. + + return m_felix.getBundle(this, id); + } + + public Bundle getBundle(String location) + { + checkValidity(); + + // CONCURRENCY NOTE: This is a check-then-act situation, + // but we ignore it since the time window is small and + // the result is the same as if the calling thread had + // won the race condition. + + return m_felix.getBundle(location); } public Bundle[] getBundles() { checkValidity(); - - return m_felix.getBundles(); + + // CONCURRENCY NOTE: This is a check-then-act situation, + // but we ignore it since the time window is small and + // the result is the same as if the calling thread had + // won the race condition. + + return m_felix.getBundles(this); } public void addBundleListener(BundleListener l) { checkValidity(); - + + // CONCURRENCY NOTE: This is a check-then-act situation, but + // internally the event dispatcher double checks whether or not + // the bundle context is valid before adding the service listener + // while holding the event queue lock, so it will either succeed + // or fail. + Object sm = System.getSecurityManager(); - + if (sm != null) { if (l instanceof SynchronousBundleListener) { - ((SecurityManager) sm).checkPermission(new AdminPermission(m_bundle, + ((SecurityManager) sm).checkPermission(new AdminPermission(m_bundle, AdminPermission.LISTENER)); } } - + m_felix.addBundleListener(m_bundle, l); } public void removeBundleListener(BundleListener l) { checkValidity(); - + + // CONCURRENCY NOTE: This is a check-then-act situation, + // but we ignore it since the time window is small and + // the result is the same as if the calling thread had + // won the race condition. + Object sm = System.getSecurityManager(); - + if (sm != null) { - if(l instanceof SynchronousBundleListener) + if (l instanceof SynchronousBundleListener) { - ((SecurityManager) sm).checkPermission(new AdminPermission(m_bundle, + ((SecurityManager) sm).checkPermission(new AdminPermission(m_bundle, AdminPermission.LISTENER)); } } - + m_felix.removeBundleListener(m_bundle, l); } @@ -197,44 +249,70 @@ public void addServiceListener(ServiceListener l, String s) throws InvalidSyntaxException { checkValidity(); - + + // CONCURRENCY NOTE: This is a check-then-act situation, but + // internally the event dispatcher double checks whether or not + // the bundle context is valid before adding the service listener + // while holding the event queue lock, so it will either succeed + // or fail. + m_felix.addServiceListener(m_bundle, l, s); } public void removeServiceListener(ServiceListener l) { checkValidity(); - + + // CONCURRENCY NOTE: This is a check-then-act situation, + // but we ignore it since the time window is small and + // the result is the same as if the calling thread had + // won the race condition. + m_felix.removeServiceListener(m_bundle, l); } public void addFrameworkListener(FrameworkListener l) { checkValidity(); - + + // CONCURRENCY NOTE: This is a check-then-act situation, but + // internally the event dispatcher double checks whether or not + // the bundle context is valid before adding the service listener + // while holding the event queue lock, so it will either succeed + // or fail. + m_felix.addFrameworkListener(m_bundle, l); } public void removeFrameworkListener(FrameworkListener l) { checkValidity(); - + + // CONCURRENCY NOTE: This is a check-then-act situation, + // but we ignore it since the time window is small and + // the result is the same as if the calling thread had + // won the race condition. + m_felix.removeFrameworkListener(m_bundle, l); } - public ServiceRegistration registerService( - String clazz, Object svcObj, Dictionary dict) + public ServiceRegistration registerService( + String clazz, Object svcObj, Dictionary dict) { return registerService(new String[] { clazz }, svcObj, dict); } - public ServiceRegistration registerService( - String[] clazzes, Object svcObj, Dictionary dict) + public ServiceRegistration registerService( + String[] clazzes, Object svcObj, Dictionary dict) { checkValidity(); - + + // CONCURRENCY NOTE: This is a NOT a check-then-act situation, + // because internally the framework acquires the bundle state + // lock to ensure state consistency. + Object sm = System.getSecurityManager(); - + if (sm != null) { if (clazzes != null) @@ -242,18 +320,30 @@ public ServiceRegistration registerService( for (int i = 0;i < clazzes.length;i++) { ((SecurityManager) sm).checkPermission( - new ServicePermission(clazzes[i], ServicePermission.REGISTER)); + new ServicePermission(clazzes[i], ServicePermission.REGISTER)); } } } - - return m_felix.registerService(m_bundle, clazzes, svcObj, dict); + + return m_felix.registerService(this, clazzes, svcObj, dict); + } + + public ServiceRegistration registerService( + Class clazz, S svcObj, Dictionary dict) + { + return (ServiceRegistration) + registerService(new String[] { clazz.getName() }, svcObj, dict); } - public ServiceReference getServiceReference(String clazz) + public ServiceReference getServiceReference(String clazz) { checkValidity(); - + + // CONCURRENCY NOTE: This is a check-then-act situation, + // but we ignore it since the time window is small and + // the result is the same as if the calling thread had + // won the race condition. + try { ServiceReference[] refs = getServiceReferences(clazz, null); @@ -261,11 +351,16 @@ public ServiceReference getServiceReference(String clazz) } catch (InvalidSyntaxException ex) { - m_felix.getLogger().log(Logger.LOG_ERROR, "BundleContextImpl: " + ex); + m_logger.log(m_bundle, Logger.LOG_ERROR, "BundleContextImpl: " + ex); } return null; } + public ServiceReference getServiceReference(Class clazz) + { + return (ServiceReference) getServiceReference(clazz.getName()); + } + private ServiceReference getBestServiceReference(ServiceReference[] refs) { if (refs == null) @@ -280,257 +375,103 @@ private ServiceReference getBestServiceReference(ServiceReference[] refs) // Loop through all service references and return // the "best" one according to its rank and ID. - ServiceReference bestRef = null; - Integer bestRank = null; - Long bestId = null; - for (int i = 0; i < refs.length; i++) + ServiceReference bestRef = refs[0]; + for (int i = 1; i < refs.length; i++) { - ServiceReference ref = refs[i]; - - // The first time through the loop just - // assume that the first reference is best. - if (bestRef == null) + if (bestRef.compareTo(refs[i]) < 0) { - bestRef = ref; - bestRank = (Integer) bestRef.getProperty("service.ranking"); - // The spec says no ranking defaults to zero. - if (bestRank == null) - { - bestRank = new Integer(0); - } - bestId = (Long) bestRef.getProperty("service.id"); - } - - // Compare current and best references to see if - // the current reference is a better choice. - Integer rank = (Integer) ref.getProperty("service.ranking"); - - // The spec says no ranking defaults to zero. - if (rank == null) - { - rank = new Integer(0); - } - - // If the current reference ranking is greater than the - // best ranking, then keep the current reference. - if (bestRank.compareTo(rank) < 0) - { - bestRef = ref; - bestRank = rank; - bestId = (Long) bestRef.getProperty("service.id"); - } - // If rankings are equal, then compare IDs and - // keep the smallest. - else if (bestRank.compareTo(rank) == 0) - { - Long id = (Long) ref.getProperty("service.id"); - // If either reference has a null ID, then keep - // the one with a non-null ID. - if ((bestId == null) || (id == null)) - { - bestRef = (bestId == null) ? ref : bestRef; - // bestRank = bestRank; // No need to update since they are equal. - bestId = (Long) bestRef.getProperty("service.id"); - } - // Otherwise compare IDs. - else - { - // If the current reference ID is less than the - // best ID, then keep the current reference. - if (bestId.compareTo(id) > 0) - { - bestRef = ref; - // bestRank = bestRank; // No need to update since they are equal. - bestId = (Long) bestRef.getProperty("service.id"); - } - } + bestRef = refs[i]; } } return bestRef; } - public ServiceReference[] getAllServiceReferences(String clazz, String filter) throws InvalidSyntaxException + public ServiceReference[] getAllServiceReferences(String clazz, String filter) + throws InvalidSyntaxException { checkValidity(); - - // TODO: Implement BundleContext.getAllServiceReferences() - Object sm = System.getSecurityManager(); - - if (sm != null) - { - ServiceReference[] refs = null; - - if (refs == null) - { - return refs; - } - - List result = new ArrayList(); - - for (int i = 0;i < refs.length;i++) - { - String[] objectClass = (String[]) refs[i].getProperty( - Constants.OBJECTCLASS); - - if (objectClass == null) - { - continue; - } - - for (int j = 0;j < objectClass.length;j++) - { - try - { - ((SecurityManager) sm).checkPermission(new ServicePermission( - objectClass[j], ServicePermission.GET)); - - result.add(refs[i]); - - break; - } - catch (Exception e) - { - - } - } - } - - if (result.isEmpty()) - { - return null; - } - - return (ServiceReference[]) result.toArray(new ServiceReference[result.size()]); - } - else - { - return null; - } + + // CONCURRENCY NOTE: This is a check-then-act situation, + // but we ignore it since the time window is small and + // the result is the same as if the calling thread had + // won the race condition. + + return m_felix.getAllowedServiceReferences(m_bundle, clazz, filter, false); + } - public ServiceReference[] getServiceReferences(String clazz, String filter) + public ServiceReference[] getServiceReferences(String clazz, String filter) throws InvalidSyntaxException { checkValidity(); - - Object sm = System.getSecurityManager(); - - if (sm != null) - { - ServiceReference[] refs = m_felix.getServiceReferences(m_bundle, clazz, filter); - - if (refs == null) - { - return refs; - } - - List result = new ArrayList(); - - for (int i = 0;i < refs.length;i++) - { - String[] objectClass = (String[]) refs[i].getProperty( - Constants.OBJECTCLASS); - - if (objectClass == null) - { - continue; - } - - for (int j = 0;j < objectClass.length;j++) - { - try - { - ((SecurityManager) sm).checkPermission(new ServicePermission( - objectClass[j], ServicePermission.GET)); - - result.add(refs[i]); - - break; - } - catch (Exception e) - { - - } - } - } - - if (result.isEmpty()) - { - return null; - } - - return (ServiceReference[]) result.toArray(new ServiceReference[result.size()]); - } - else - { - return m_felix.getServiceReferences(m_bundle, clazz, filter); - } + + // CONCURRENCY NOTE: This is a check-then-act situation, + // but we ignore it since the time window is small and + // the result is the same as if the calling thread had + // won the race condition. + + return m_felix.getAllowedServiceReferences(m_bundle, clazz, filter, true); + } - public Object getService(ServiceReference ref) + public Collection> getServiceReferences( + Class clazz, String filter) + throws InvalidSyntaxException + { + ServiceReference[] refs = + (ServiceReference[]) getServiceReferences(clazz.getName(), filter); + return (refs == null) + ? Collections.EMPTY_LIST + : (Collection>) Arrays.asList(refs); + } + + public S getService(ServiceReference ref) { checkValidity(); - + + // CONCURRENCY NOTE: This is a check-then-act situation, + // but we ignore it since the time window is small and + // the result is the same as if the calling thread had + // won the race condition. + if (ref == null) { throw new NullPointerException("Specified service reference cannot be null."); } - + Object sm = System.getSecurityManager(); - + if (sm != null) { - String[] objectClass = (String[]) ref.getProperty(Constants.OBJECTCLASS); - - if (objectClass == null) - { - return null; - } - - boolean hasPermission = false; - - for (int i = 0;(i < objectClass.length) && !hasPermission;i++) - { - try - { - ((SecurityManager) sm).checkPermission( - new ServicePermission(objectClass[i], ServicePermission.GET)); - - hasPermission = true; - } - catch (Exception ex) - { - - } - } - - if (!hasPermission) - { - throw new SecurityException("No permission"); - } + ((SecurityManager) sm).checkPermission(new ServicePermission(ref, ServicePermission.GET)); } - - return m_felix.getService(m_bundle, ref); + + return m_felix.getService(m_bundle, ref, false); } - public boolean ungetService(ServiceReference ref) + public boolean ungetService(ServiceReference ref) { checkValidity(); - + if (ref == null) { throw new NullPointerException("Specified service reference cannot be null."); } // Unget the specified service. - return m_felix.ungetService(m_bundle, ref); + return m_felix.ungetService(m_bundle, ref, null); } public File getDataFile(String s) { checkValidity(); - + + // CONCURRENCY NOTE: This is a check-then-act situation, + // but we ignore it since the time window is small and + // the result is the same as if the calling thread had + // won the race condition. + return m_felix.getDataFile(m_bundle, s); } @@ -538,7 +479,7 @@ private void checkValidity() { if (m_valid) { - switch (m_bundle.getInfo().getState()) + switch (m_bundle.getState()) { case Bundle.ACTIVE: case Bundle.STARTING: @@ -546,7 +487,88 @@ private void checkValidity() return; } } - + throw new IllegalStateException("Invalid BundleContext."); } + + /** + * @see org.osgi.framework.BundleContext#registerService(java.lang.Class, org.osgi.framework.ServiceFactory, java.util.Dictionary) + */ + public ServiceRegistration registerService(Class clazz, + ServiceFactory factory, Dictionary properties) + { + return (ServiceRegistration) + registerService(new String[] { clazz.getName() }, factory, properties); + } + + /** + * @see org.osgi.framework.BundleContext#getServiceObjects(org.osgi.framework.ServiceReference) + */ + public ServiceObjects getServiceObjects(final ServiceReference ref) + { + checkValidity(); + + Object sm = System.getSecurityManager(); + + if (sm != null) + { + ((SecurityManager) sm).checkPermission(new ServicePermission(ref, ServicePermission.GET)); + } + + ServiceRegistrationImpl reg = + ((ServiceRegistrationImpl.ServiceReferenceImpl) ref).getRegistration(); + if ( reg.isValid() ) + { + return new ServiceObjectsImpl(ref); + } + return null; + } + + // + // ServiceObjects implementation + // + class ServiceObjectsImpl implements ServiceObjects + { + private final ServiceReference m_ref; + + public ServiceObjectsImpl(final ServiceReference ref) + { + this.m_ref = ref; + } + + public S getService() { + checkValidity(); + + // CONCURRENCY NOTE: This is a check-then-act situation, + // but we ignore it since the time window is small and + // the result is the same as if the calling thread had + // won the race condition. + + final Object sm = System.getSecurityManager(); + + if (sm != null) + { + ((SecurityManager) sm).checkPermission(new ServicePermission(m_ref, ServicePermission.GET)); + } + + return m_felix.getService(m_bundle, m_ref, true); + } + + public void ungetService(final S srvObj) + { + checkValidity(); + + // Unget the specified service. + if ( !m_felix.ungetService(m_bundle, m_ref, srvObj) ) + { + throw new IllegalArgumentException(); + } + } + + public ServiceReference getServiceReference() + { + return m_ref; + } + } + } \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/BundleImpl.java b/framework/src/main/java/org/apache/felix/framework/BundleImpl.java index 77fffc9c3c6..d3b6724702b 100644 --- a/framework/src/main/java/org/apache/felix/framework/BundleImpl.java +++ b/framework/src/main/java/org/apache/felix/framework/BundleImpl.java @@ -1,64 +1,276 @@ /* - * Copyright 2005 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework; +import org.apache.felix.framework.cache.BundleArchive; +import org.apache.felix.framework.util.SecurityManagerEx; +import org.apache.felix.framework.util.ShrinkableCollection; +import org.apache.felix.framework.util.StringMap; +import org.apache.felix.framework.util.Util; +import org.osgi.dto.DTO; +import org.osgi.framework.AdaptPermission; +import org.osgi.framework.AdminPermission; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.ServicePermission; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.Version; +import org.osgi.framework.hooks.bundle.CollisionHook; +import org.osgi.framework.startlevel.BundleStartLevel; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleRevisions; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; + +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; -import java.util.*; +import java.security.AccessControlContext; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.StringTokenizer; + +class BundleImpl implements Bundle, BundleRevisions +{ + // No one should use this field directly, use getFramework() instead. + private final Felix __m_felix; + + private final BundleArchive m_archive; + private final List m_revisions = new ArrayList(0); + private volatile int m_state; + private boolean m_useDeclaredActivationPolicy; + private BundleActivator m_activator = null; + private volatile BundleContext m_context = null; + private final Map m_cachedHeaders = new HashMap(); + private Map m_uninstalledHeaders = null; + private long m_cachedHeadersTimestamp; + private final Bundle m_installingBundle; + + // Indicates whether the bundle is stale, meaning that it has + // been refreshed and completely removed from the framework. + private boolean m_stale = false; + // Used for bundle locking. + private int m_lockCount = 0; + private Thread m_lockThread = null; + + /** + * This constructor is used by the system bundle (i.e., the framework), + * since it needs a constructor that does not throw an exception. + **/ + BundleImpl() + { + __m_felix = null; + m_archive = null; + m_state = Bundle.INSTALLED; + m_useDeclaredActivationPolicy = false; + m_stale = false; + m_activator = null; + m_context = null; + m_installingBundle = null; + } -import org.osgi.framework.*; + BundleImpl(Felix felix, Bundle installingBundle, BundleArchive archive) throws Exception + { + __m_felix = felix; + m_archive = archive; + m_state = Bundle.INSTALLED; + m_useDeclaredActivationPolicy = false; + m_stale = false; + m_activator = null; + m_context = null; + m_installingBundle = installingBundle; + + BundleRevisionImpl revision = createRevision(false); + addRevision(revision); + } -class BundleImpl implements Bundle -{ - private Felix m_felix = null; - private BundleInfo m_info = null; + // This method exists because the system bundle extends BundleImpl + // and cannot pass itself into the BundleImpl constructor. All methods + // in BundleImpl should use this method to get the framework and should + // not access the field directly. + Felix getFramework() + { + return __m_felix; + } + + BundleArchive getArchive() + { + return m_archive; + } + +// Only called when the framework is stopping. Don't need to clean up dependencies. + synchronized void close() + { + closeRevisions(); + try + { + m_archive.close(); + } + catch (Exception ex) + { + getFramework().getLogger().log( + this, + Logger.LOG_ERROR, + "Unable to close archive revisions.", ex); + } + } + +// Called when install fails, when stopping framework with uninstalled bundles, +// and when refreshing an uninstalled bundle. Only need to clear up dependencies +// for last case. + synchronized void closeAndDelete() throws Exception + { + if (!m_stale) + { + // Mark the bundle as stale, since it is being deleted. + m_stale = true; + // Close all revisions. + closeRevisions(); + // Delete bundle archive, which will close revisions. + m_archive.closeAndDelete(); + } + } + +// Called from BundleImpl.close(), BundleImpl.closeAndDelete(), and BundleImpl.refresh() + private void closeRevisions() + { + // Remove the bundle's associated revisions from the resolver state + // and close them. + for (BundleRevisionImpl br : m_revisions) + { + // Remove the revision from the resolver state. + getFramework().getResolver().removeRevision(br); + + // Close the revision's content. + br.close(); + } + } - protected BundleImpl(Felix felix, BundleInfo info) +// Called when refreshing a bundle. Must clean up dependencies beforehand. + synchronized void refresh() throws Exception { - m_felix = felix; - m_info = info; + if (isExtension() && (getFramework().getState() != Bundle.STOPPING)) + { + getFramework().getLogger().log(this, Logger.LOG_WARNING, + "Framework restart on extension bundle refresh not implemented."); + } + else + { + // Get current revision, since we can reuse it. + BundleRevisionImpl current = adapt(BundleRevisionImpl.class); + // Close all existing revisions. + closeRevisions(); + // Clear all revisions. + m_revisions.clear(); + + // Purge all old archive revisions, only keeping the newest one. + m_archive.purge(); + + // Reset the content of the current bundle revision. + current.resetContent(m_archive.getCurrentRevision().getContent()); + // Re-add the revision to the bundle. + addRevision(current); + + // Reset the bundle state. + m_state = Bundle.INSTALLED; + m_stale = false; + + synchronized (m_cachedHeaders) + { + m_cachedHeaders.clear(); + m_cachedHeadersTimestamp = 0; + } + } } - Felix getFelix() // package protected + synchronized boolean isDeclaredActivationPolicyUsed() { - return m_felix; + return m_useDeclaredActivationPolicy; } - BundleInfo getInfo() // package protected + synchronized void setDeclaredActivationPolicyUsed(boolean b) { - return m_info; + m_useDeclaredActivationPolicy = b; } - void setInfo(BundleInfo info) // package protected + synchronized BundleActivator getActivator() { - m_info = info; + return m_activator; } - private BundleContext getContext() + synchronized void setActivator(BundleActivator activator) { - return m_info.getContext(); + m_activator = activator; } + @Override + public BundleContext getBundleContext() + { + Object sm = System.getSecurityManager(); + + if (sm != null) + { + ((SecurityManager) sm).checkPermission( + new AdminPermission(this, AdminPermission.CONTEXT)); + } + + return m_context; + } + + void setBundleContext(BundleContext context) + { + m_context = context; + } + + @Override public long getBundleId() { - return m_info.getBundleId(); + try + { + return m_archive.getId(); + } + catch (Exception ex) + { + getFramework().getLogger().log( + this, + Logger.LOG_ERROR, + "Error getting the identifier from bundle archive.", + ex); + return -1; + } } + @Override public URL getEntry(String name) { Object sm = System.getSecurityManager(); @@ -76,9 +288,10 @@ public URL getEntry(String name) } } - return m_felix.getBundleEntry(this, name); + return getFramework().getBundleEntry(this, name); } + @Override public Enumeration getEntryPaths(String path) { Object sm = System.getSecurityManager(); @@ -96,9 +309,10 @@ public Enumeration getEntryPaths(String path) } } - return m_felix.getBundleEntryPaths(this, path); + return getFramework().getBundleEntryPaths(this, path); } + @Override public Enumeration findEntries(String path, String filePattern, boolean recurse) { Object sm = System.getSecurityManager(); @@ -116,10 +330,18 @@ public Enumeration findEntries(String path, String filePattern, boolean recurse) } } - return m_felix.findBundleEntries(this, path, filePattern, recurse); + return getFramework().findBundleEntries( + this, path, filePattern, recurse); } + @Override public Dictionary getHeaders() + { + return getHeaders(Locale.getDefault().toString()); + } + + @Override + public Dictionary getHeaders(String locale) { Object sm = System.getSecurityManager(); @@ -128,14 +350,267 @@ public Dictionary getHeaders() ((SecurityManager) sm).checkPermission(new AdminPermission(this, AdminPermission.METADATA)); } - return m_felix.getBundleHeaders(this); + + if (locale == null) + { + locale = Locale.getDefault().toString(); + } + + return getFramework().getBundleHeaders(this, locale); } + Map getCurrentLocalizedHeader(String locale) + { + Map result = null; + + // Spec says empty local returns raw headers. + if (locale.length() == 0) + { + result = new StringMap(adapt(BundleRevisionImpl.class).getHeaders()); + } + + // If we have no result, try to get it from the cached headers. + if (result == null) + { + synchronized (m_cachedHeaders) + { + // If the bundle is uninstalled, then we should always return + // the uninstalled headers, which are the default locale as per + // the spec. + if (m_uninstalledHeaders != null) + { + result = m_uninstalledHeaders; + } + // If the bundle has been updated, clear the cached headers. + else if (getLastModified() > m_cachedHeadersTimestamp) + { + m_cachedHeaders.clear(); + } + // Otherwise, returned the cached headers if they exist. + else + { + // Check if headers for this locale have already been resolved + if (m_cachedHeaders.containsKey(locale)) + { + result = (Map) m_cachedHeaders.get(locale); + } + } + } + } + + // If the requested locale is not cached, then try to create it. + if (result == null) + { + // Get a modifiable copy of the raw headers. + Map headers = new StringMap(adapt(BundleRevisionImpl.class).getHeaders()); + // Assume for now that this will be the result. + result = headers; + + // Check to see if we actually need to localize anything + boolean localize = false; + for (Iterator it = headers.values().iterator(); !localize && it.hasNext(); ) + { + if (((String) it.next()).startsWith("%")) + { + localize = true; + } + } + + if (!localize) + { + // If localization is not needed, just cache the headers and return + // them as-is. Not sure if this is useful + updateHeaderCache(locale, headers); + } + else + { + // Do localization here and return the localized headers + String basename = (String) headers.get(Constants.BUNDLE_LOCALIZATION); + if (basename == null) + { + basename = Constants.BUNDLE_LOCALIZATION_DEFAULT_BASENAME; + } + + // Create ordered list of revisions to search for localization + // property resources. + List revisionList = createLocalizationRevisionList( + adapt(BundleRevisionImpl.class)); + + // Create ordered list of files to load properties from + List resourceList = createLocalizationResourceList(basename, locale); + + // Create a merged props file with all available props for this locale + boolean found = false; + Properties mergedProperties = new Properties(); + for (BundleRevision br : revisionList) + { + for (String res : resourceList) + { + URL temp = ((BundleRevisionImpl) br).getEntry(res + ".properties"); + if (temp != null) + { + found = true; + try + { + mergedProperties.load( + temp.openConnection().getInputStream()); + } + catch (IOException ex) + { + // File doesn't exist, just continue loop + } + } + } + } + + // If the specified locale was not found, then the spec says we should + // return the default localization. + if (!found && !locale.equals(Locale.getDefault().toString())) + { + result = getCurrentLocalizedHeader(Locale.getDefault().toString()); + } + // Otherwise, perform the localization based on the discovered + // properties and cache the result. + else + { + // Resolve all localized header entries + for (Iterator it = headers.entrySet().iterator(); it.hasNext(); ) + { + Map.Entry entry = (Map.Entry) it.next(); + String value = (String) entry.getValue(); + if (value.startsWith("%")) + { + String newvalue; + String key = value.substring(value.indexOf("%") + 1); + newvalue = mergedProperties.getProperty(key); + if (newvalue==null) + { + newvalue = key; + } + entry.setValue(newvalue); + } + } + + updateHeaderCache(locale, headers); + } + } + } + + return result; + } + + private void updateHeaderCache(String locale, Map localizedHeaders) + { + synchronized (m_cachedHeaders) + { + if (m_uninstalledHeaders == null) + { + m_cachedHeaders.put(locale, localizedHeaders); + m_cachedHeadersTimestamp = System.currentTimeMillis(); + } + } + } + + private static List createLocalizationRevisionList(BundleRevision br) + { + // If the revision is a fragment, then we actually need + // to search its host and associated fragments for its + // localization information. So, check to see if there + // are any hosts and then use the one with the highest + // version instead of the fragment itself. If there are + // no hosts, but the revision is a fragment, then just + // search the revision itself. + if (Util.isFragment(br)) + { + if (br.getWiring() != null) + { + List hostWires = br.getWiring().getRequiredWires(null); + if ((hostWires != null) && (hostWires.size() > 0)) + { + br = hostWires.get(0).getProviderWiring().getRevision(); + for (int hostIdx = 1; hostIdx < hostWires.size(); hostIdx++) + { + if (br.getVersion().compareTo( + hostWires.get(hostIdx).getProviderWiring().getRevision().getVersion()) < 0) + { + br = hostWires.get(hostIdx).getProviderWiring().getRevision(); + } + } + } + } + } + + // Create a list of the revision and any attached fragment revisions. + List result = new ArrayList(); + result.add(br); + BundleWiring wiring = br.getWiring(); + if (wiring != null) + { + List fragments = Util.getFragments(wiring); + if (fragments != null) + { + result.addAll(fragments); + } + } + return result; + } + + private static List createLocalizationResourceList(String basename, String locale) + { + List result = new ArrayList(4); + + StringTokenizer tokens; + StringBuilder tempLocale = new StringBuilder(basename); + + result.add(tempLocale.toString()); + + if (locale.length() > 0) + { + tokens = new StringTokenizer(locale, "_"); + while (tokens.hasMoreTokens()) + { + tempLocale.append("_").append(tokens.nextToken()); + result.add(tempLocale.toString()); + } + } + return result; + } + + @Override public long getLastModified() { - return m_info.getLastModified(); + try + { + return m_archive.getLastModified(); + } + catch (Exception ex) + { + getFramework().getLogger().log( + this, + Logger.LOG_ERROR, + "Error reading last modification time from bundle archive.", + ex); + return 0; + } } + void setLastModified(long l) + { + try + { + m_archive.setLastModified(l); + } + catch (Exception ex) + { + getFramework().getLogger().log( + this, + Logger.LOG_ERROR, + "Error writing last modification time to bundle archive.", + ex); + } + } + + @Override public String getLocation() { Object sm = System.getSecurityManager(); @@ -145,7 +620,24 @@ public String getLocation() ((SecurityManager) sm).checkPermission(new AdminPermission(this, AdminPermission.METADATA)); } - return m_felix.getBundleLocation(this); + return _getLocation(); + } + + String _getLocation() + { + try + { + return m_archive.getLocation(); + } + catch (Exception ex) + { + getFramework().getLogger().log( + this, + Logger.LOG_ERROR, + "Error getting location from bundle archive.", + ex); + return null; + } } /** @@ -153,9 +645,49 @@ public String getLocation() * * @return a URL to named resource, or null if not found. **/ + @Override public URL getResource(String name) { - return m_felix.getBundleResource(this, name); + Object sm = System.getSecurityManager(); + + if (sm != null) + { + try + { + ((SecurityManager) sm).checkPermission( + new AdminPermission(this, AdminPermission.RESOURCE)); + } + catch (Exception e) + { + return null; // No permission + } + } + + return getFramework().getBundleResource(this, name); + } + + @Override + public Enumeration getResources(String name) throws IOException + { + Object sm = System.getSecurityManager(); + + if (sm != null) + { + try + { + ((SecurityManager) sm).checkPermission( + new AdminPermission(this, AdminPermission.RESOURCE)); + } + catch (Exception e) + { + return null; // No permission + } + } + + // Spec says we should return null when resources not found, + // even though ClassLoader.getResources() returns empty enumeration. + Enumeration e = getFramework().getBundleResources(this, name); + return ((e == null) || !e.hasMoreElements()) ? null : e; } /** @@ -164,13 +696,14 @@ public URL getResource(String name) * * @return an array of service references or null. **/ + @Override public ServiceReference[] getRegisteredServices() { Object sm = System.getSecurityManager(); if (sm != null) { - ServiceReference[] refs = m_felix.getBundleRegisteredServices(this); + ServiceReference[] refs = getFramework().getBundleRegisteredServices(this); if (refs == null) { @@ -179,31 +712,18 @@ public ServiceReference[] getRegisteredServices() List result = new ArrayList(); - for (int i = 0;i < refs.length;i++) + for (int i = 0; i < refs.length; i++) { - String[] objectClass = (String[]) refs[i].getProperty( - Constants.OBJECTCLASS); - - if (objectClass == null) + try { - continue; - } + ((SecurityManager) sm).checkPermission(new ServicePermission( + refs[i], ServicePermission.GET)); - for (int j = 0;j < objectClass.length;j++) + result.add(refs[i]); + } + catch (Exception ex) { - try - { - ((SecurityManager) sm).checkPermission(new ServicePermission( - objectClass[j], ServicePermission.GET)); - - result.add(refs[i]); - - break; - } - catch (Exception e) - { - - } + // Silently ignore. } } @@ -216,17 +736,18 @@ public ServiceReference[] getRegisteredServices() } else { - return m_felix.getBundleRegisteredServices(this); + return getFramework().getBundleRegisteredServices(this); } } + @Override public ServiceReference[] getServicesInUse() { Object sm = System.getSecurityManager(); if (sm != null) { - ServiceReference[] refs = m_felix.getBundleServicesInUse(this); + ServiceReference[] refs = getFramework().getBundleServicesInUse(this); if (refs == null) { @@ -235,31 +756,18 @@ public ServiceReference[] getServicesInUse() List result = new ArrayList(); - for (int i = 0;i < refs.length;i++) + for (int i = 0; i < refs.length; i++) { - String[] objectClass = (String[]) refs[i].getProperty( - Constants.OBJECTCLASS); - - if (objectClass == null) + try { - continue; - } + ((SecurityManager) sm).checkPermission( + new ServicePermission(refs[i], ServicePermission.GET)); - for (int j = 0;j < objectClass.length;j++) + result.add(refs[i]); + } + catch (Exception ex) { - try - { - ((SecurityManager) sm).checkPermission(new ServicePermission( - objectClass[j], ServicePermission.GET)); - - result.add(refs[i]); - - break; - } - catch (Exception e) - { - - } + // Silently ignore. } } @@ -271,27 +779,187 @@ public ServiceReference[] getServicesInUse() return (ServiceReference[]) result.toArray(new ServiceReference[result.size()]); } - return m_felix.getBundleServicesInUse(this); + return getFramework().getBundleServicesInUse(this); } + @Override public int getState() { - return m_info.getState(); + return m_state; } + // This method should not be called directly. + void __setState(int i) + { + m_state = i; + } + + int getPersistentState() + { + try + { + return m_archive.getPersistentState(); + } + catch (Exception ex) + { + getFramework().getLogger().log( + this, + Logger.LOG_ERROR, + "Error reading persistent state from bundle archive.", + ex); + return Bundle.INSTALLED; + } + } + + void setPersistentStateInactive() + { + try + { + m_archive.setPersistentState(Bundle.INSTALLED); + } + catch (Exception ex) + { + getFramework().getLogger().log(this, Logger.LOG_ERROR, + "Error writing persistent state to bundle archive.", + ex); + } + } + + void setPersistentStateActive() + { + try + { + m_archive.setPersistentState(Bundle.ACTIVE); + } + catch (Exception ex) + { + getFramework().getLogger().log( + this, + Logger.LOG_ERROR, + "Error writing persistent state to bundle archive.", + ex); + } + } + + void setPersistentStateStarting() + { + try + { + m_archive.setPersistentState(Bundle.STARTING); + } + catch (Exception ex) + { + getFramework().getLogger().log( + this, + Logger.LOG_ERROR, + "Error writing persistent state to bundle archive.", + ex); + } + } + void setPersistentStateUninstalled() + { + try + { + m_archive.setPersistentState(Bundle.UNINSTALLED); + } + catch (Exception ex) + { + getFramework().getLogger().log( + this, + Logger.LOG_ERROR, + "Error writing persistent state to bundle archive.", + ex); + } + } + + int getStartLevel(int defaultLevel) + { + try + { + int level = m_archive.getStartLevel(); + if ( level == -1 ) + { + level = defaultLevel; + } + return level; + } + catch (Exception ex) + { + getFramework().getLogger().log( + this, + Logger.LOG_ERROR, + "Error reading start level from bundle archive.", + ex); + return defaultLevel; + } + } + + void setStartLevel(int i) + { + try + { + m_archive.setStartLevel(i); + } + catch (Exception ex) + { + getFramework().getLogger().log( + this, + Logger.LOG_ERROR, + "Error writing start level to bundle archive.", + ex); + } + } + + synchronized boolean isStale() + { + return m_stale; + } + + synchronized boolean isExtension() + { + for (BundleRevisionImpl revision : m_revisions) + { + if (revision.isExtension()) + { + return true; + } + } + return false; + } + + @Override public String getSymbolicName() { - return (String) m_felix.getBundleHeaders(this).get( - Constants.BUNDLE_SYMBOLICNAME); + return adapt(BundleRevisionImpl.class).getSymbolicName(); } + @Override + public Version getVersion() + { + return adapt(BundleRevisionImpl.class).getVersion(); + } + + @Override public boolean hasPermission(Object obj) { - return m_felix.bundleHasPermission(this, obj); + return getFramework().bundleHasPermission(this, obj); } + @Override + public Map getSignerCertificates(int signersType) + { + // TODO: SECURITY - This needs to be adapted to our security mechanisms. + return (Map) getFramework().getSignerMatcher(this, signersType); + } + + @Override public Class loadClass(String name) throws ClassNotFoundException { + if (isExtension()) + { + throw new ClassNotFoundException("Extension bundles cannot load classes."); + } + Object sm = System.getSecurityManager(); if (sm != null) @@ -301,16 +969,23 @@ public Class loadClass(String name) throws ClassNotFoundException ((SecurityManager) sm).checkPermission(new AdminPermission(this, AdminPermission.CLASS)); } - catch (Exception e) + catch (Exception ex) { - throw new ClassNotFoundException("No permission.", e); + throw new ClassNotFoundException("No permission.", ex); } } - return m_felix.loadBundleClass(this, name); + return getFramework().loadBundleClass(this, name); } + @Override public void start() throws BundleException + { + start(0); + } + + @Override + public void start(int options) throws BundleException { Object sm = System.getSecurityManager(); @@ -320,14 +995,16 @@ public void start() throws BundleException AdminPermission.EXECUTE)); } - m_felix.startBundle(this, true); + getFramework().startBundle(this, options); } + @Override public void update() throws BundleException { update(null); } + @Override public void update(InputStream is) throws BundleException { Object sm = System.getSecurityManager(); @@ -338,10 +1015,17 @@ public void update(InputStream is) throws BundleException AdminPermission.LIFECYCLE)); } - m_felix.updateBundle(this, is); + getFramework().updateBundle(this, is); } + @Override public void stop() throws BundleException + { + stop(0); + } + + @Override + public void stop(int options) throws BundleException { Object sm = System.getSecurityManager(); @@ -351,9 +1035,10 @@ public void stop() throws BundleException AdminPermission.EXECUTE)); } - m_felix.stopBundle(this, true); + getFramework().stopBundle(this, ((options & Bundle.STOP_TRANSIENT) == 0)); } + @Override public void uninstall() throws BundleException { Object sm = System.getSecurityManager(); @@ -364,69 +1049,355 @@ public void uninstall() throws BundleException AdminPermission.LIFECYCLE)); } - m_felix.uninstallBundle(this); + Map headers = getCurrentLocalizedHeader(Locale.getDefault().toString()); + + // Uninstall the bundle. + getFramework().uninstallBundle(this); + + // After a bundle is uninstalled, the spec says getHeaders() should + // return the localized headers for the default locale at the time of + // of uninstall. So, let's clear the existing header cache to throw + // away any other locales and populate it with the headers for the + // default locale. We only do this if there are multiple cached locales + // and if the default locale is not cached. If this method is called + // more than once, then subsequent calls will do nothing here since + // only the default locale will be left in the header cache. + synchronized (m_cachedHeaders) + { + if (m_uninstalledHeaders == null) + { + m_uninstalledHeaders = headers; + m_cachedHeaders.clear(); + } + } + } + + private static final SecurityManagerEx m_smEx = new SecurityManagerEx(); + private static final ClassLoader m_classloader = Felix.class.getClassLoader(); + + void checkAdapt(Class type) + { + Object sm = System.getSecurityManager(); + if ((sm != null) && (getFramework().getSecurityProvider() != null)) + { + Class[] classes = m_smEx.getClassContext(); + if (classes.length < 3 || ((Felix.m_secureAction.getClassLoader(classes[3]) != m_classloader) || + !classes[3].getName().startsWith("org.apache.felix.framework."))) + { + ((SecurityManager) sm).checkPermission( + new AdaptPermission(type.getName(), this, AdaptPermission.ADAPT)); + } + } + } + + @Override + public synchronized A adapt(Class type) + { + checkAdapt(type); + if (type == BundleContext.class) + { + return (A) m_context; + } + else if (type == BundleStartLevel.class) + { + return (A) getFramework().adapt(FrameworkStartLevelImpl.class) + .createBundleStartLevel(this); + } + else if (type == BundleRevision.class) + { + if (m_state == Bundle.UNINSTALLED) + { + return null; + } + return (A) m_revisions.get(0); + } + // We need some way to get the current revision even if + // the associated bundle is uninstalled, so we use the + // impl revision class for this purpose. + else if (type == BundleRevisionImpl.class) + { + return (A) m_revisions.get(0); + } + else if (type == BundleRevisions.class) + { + return (A) this; + } + else if (type == BundleWiring.class) + { + if (m_state == Bundle.UNINSTALLED) + { + return null; + } + return (A) m_revisions.get(0).getWiring(); + } + else if ( type == AccessControlContext.class) + { + if (m_state == Bundle.UNINSTALLED) + { + return null; + } + final ProtectionDomain pd = this.getProtectionDomain(); + if (pd == null) + { + return null; + } + return (A) new AccessControlContext(new ProtectionDomain[] {pd}); + + } + else if (DTO.class.isAssignableFrom(type) || + DTO[].class.isAssignableFrom(type)) + { + return DTOFactory.createDTO(this, type); + } + return null; + } + + @Override + public File getDataFile(String filename) + { + return getFramework().getDataFile(this, filename); + } + + @Override + public int compareTo(Bundle t) + { + long thisBundleId = this.getBundleId(); + long thatBundleId = t.getBundleId(); + return (thisBundleId < thatBundleId ? -1 : (thisBundleId == thatBundleId ? 0 : 1)); } + @Override public String toString() { + String sym = getSymbolicName(); + if (sym != null) + { + return sym + " [" + getBundleId() +"]"; + } return "[" + getBundleId() +"]"; } + synchronized boolean isRemovalPending() + { + return (m_state == Bundle.UNINSTALLED) || (m_revisions.size() > 1) || m_stale; + } + // - // PLACE FOLLOWING METHODS INTO PROPER LOCATION ONCE IMPLEMENTED. + // Revision management. // - public Dictionary getHeaders(String locale) + @Override + public Bundle getBundle() { - // TODO: Implement Bundle.getHeaders(String locale) - // Should be done after [#FELIX-27] resolution - Object sm = System.getSecurityManager(); + return this; + } - if (sm != null) + @Override + public synchronized List getRevisions() + { + return new ArrayList(m_revisions); + } + + /** + * Determines if the specified module is associated with this bundle. + * @param revision the module to determine if it is associate with this bundle. + * @return true if the specified module is in the array of modules + * associated with this bundle, false otherwise. + **/ + synchronized boolean hasRevision(BundleRevision revision) + { + return m_revisions.contains(revision); + } + + synchronized void revise(String location, InputStream is) + throws Exception + { + // This operation will increase the revision count for the bundle. + m_archive.revise(location, is); + try { - ((SecurityManager) sm).checkPermission(new AdminPermission(this, - AdminPermission.METADATA)); + BundleRevisionImpl revision = createRevision(true); + addRevision(revision); } + catch (Exception ex) + { + m_archive.rollbackRevise(); + throw ex; + } + } - return null; + synchronized boolean rollbackRevise() throws Exception + { + BundleRevision br = m_revisions.remove(0); + // Since revising a bundle adds a revision to the global + // state, we must remove it from the global state on rollback. + getFramework().getResolver().removeRevision(br); + + return m_archive.rollbackRevise(); } - public Enumeration getResources(String name) throws IOException + // This method should be private, but is visible because the + // system bundle needs to add its revision directly to the bundle, + // since it doesn't have an archive from which it will be created, + // which is the normal case. + synchronized void addRevision(BundleRevisionImpl revision) throws Exception { - // TODO: Implement Bundle.getResources() - Object sm = System.getSecurityManager(); + m_revisions.add(0, revision); - if (sm != null) + try { - try + getFramework().setBundleProtectionDomain(revision); + } + catch (Exception ex) + { + m_revisions.remove(0); + throw ex; + } + + /// Now that the revision is added to the bundle, we can update + // the resolver's state to be aware of any new capabilities. + getFramework().getResolver().addRevision(revision); + } + + private BundleRevisionImpl createRevision(boolean isUpdate) throws Exception + { + // Get and parse the manifest from the most recent revision and + // create an associated revision object for it. + Map headerMap = Util.getMultiReleaseAwareManifestHeaders( + getFramework()._getProperty("java.specification.version"), m_archive.getCurrentRevision()); + + // Create the bundle revision instance. + BundleRevisionImpl revision = new BundleRevisionImpl( + this, + Long.toString(getBundleId()) + + "." + m_archive.getCurrentRevisionNumber().toString(), + headerMap, + m_archive.getCurrentRevision().getContent()); + + // For R4 bundles, verify that the bundle symbolic name + version + // is unique unless this check has been disabled. + String allowMultiple = + (String) getFramework().getConfig().get(Constants.FRAMEWORK_BSNVERSION); + allowMultiple = (allowMultiple == null) + ? Constants.FRAMEWORK_BSNVERSION_MANAGED + : allowMultiple; + if (revision.getManifestVersion().equals("2") + && !allowMultiple.equals(Constants.FRAMEWORK_BSNVERSION_MULTIPLE)) + { + Version bundleVersion = revision.getVersion(); + bundleVersion = (bundleVersion == null) ? Version.emptyVersion : bundleVersion; + String symName = revision.getSymbolicName(); + + List collisionCanditates = new ArrayList(); + Bundle[] bundles = getFramework().getBundles(); + for (int i = 0; (bundles != null) && (i < bundles.length); i++) { - ((SecurityManager) sm).checkPermission(new AdminPermission(this, - AdminPermission.RESOURCE)); + long id = ((BundleImpl) bundles[i]).getBundleId(); + if (id != getBundleId()) + { + if (symName.equals(bundles[i].getSymbolicName()) + && bundleVersion.equals(bundles[i].getVersion())) + { + collisionCanditates.add(bundles[i]); + } + } } - catch (Exception e) + if (!collisionCanditates.isEmpty() && allowMultiple.equals(Constants.FRAMEWORK_BSNVERSION_MANAGED)) { - return null; // No permission + Set> hooks = getFramework().getHookRegistry().getHooks(CollisionHook.class); + if (!hooks.isEmpty()) + { + Collection shrinkableCollisionCandidates = new ShrinkableCollection(collisionCanditates); + for (ServiceReference hook : hooks) + { + CollisionHook ch = getFramework().getService(getFramework(), hook, false); + if (ch != null) + { + int operationType; + Bundle target; + if (isUpdate) + { + operationType = CollisionHook.UPDATING; + target = this; + } + else + { + operationType = CollisionHook.INSTALLING; + target = m_installingBundle == null ? this : m_installingBundle; + } + + Felix.m_secureAction.invokeBundleCollisionHook(ch, operationType, target, + shrinkableCollisionCandidates); + } + } + } + } + if (!collisionCanditates.isEmpty()) + { + throw new BundleException( + "Bundle symbolic name and version are not unique: " + + symName + ':' + bundleVersion, BundleException.DUPLICATE_BUNDLE_ERROR); } } - return null; + return revision; } - public boolean equals(Object obj) + synchronized ProtectionDomain getProtectionDomain() { - if (obj instanceof BundleImpl) + ProtectionDomain pd = null; + + for (int i = m_revisions.size() - 1; (i >= 0) && (pd == null); i--) { - return (((BundleImpl) obj).getInfo().getBundleId() == getInfo().getBundleId()); + pd = m_revisions.get(i).getProtectionDomain(); + } + + return pd; + } + + // + // Locking related methods. + // + + synchronized boolean isLockable() + { + return (m_lockCount == 0) || (m_lockThread == Thread.currentThread()); + } + + synchronized Thread getLockingThread() + { + return m_lockThread; + } + + synchronized void lock() + { + if ((m_lockCount > 0) && (m_lockThread != Thread.currentThread())) + { + throw new IllegalStateException("Bundle is locked by another thread."); + } + m_lockCount++; + m_lockThread = Thread.currentThread(); + } + + synchronized void unlock() + { + if (m_lockCount == 0) + { + throw new IllegalStateException("Bundle is not locked."); + } + if ((m_lockCount > 0) && (m_lockThread != Thread.currentThread())) + { + throw new IllegalStateException("Bundle is locked by another thread."); + } + m_lockCount--; + if (m_lockCount == 0) + { + m_lockThread = null; } - return false; } - /* - * This is a hack to get access to the subject-dns of the current revision - * from inside the AdminPermission. - */ - String[] getSubjectDNs() + BundleContext _getBundleContext() { - return m_info.getArchive().getDNChains(); + return m_context; } -} \ No newline at end of file +} diff --git a/framework/src/main/java/org/apache/felix/framework/BundleInfo.java b/framework/src/main/java/org/apache/felix/framework/BundleInfo.java deleted file mode 100644 index 0bbd8ed65e1..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/BundleInfo.java +++ /dev/null @@ -1,357 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework; - -import java.util.Map; - -import org.apache.felix.framework.cache.BundleArchive; -import org.apache.felix.framework.searchpolicy.R4SearchPolicy; -import org.apache.felix.moduleloader.IModule; -import org.osgi.framework.*; - -class BundleInfo -{ - private Logger m_logger = null; - private BundleArchive m_archive = null; - private IModule[] m_modules = null; - private int m_state = 0; - private long m_modified = 0; - private BundleActivator m_activator = null; - private BundleContext m_context = null; - // Indicates whether the bundle is stale, meaning that it has - // been refreshed and completely removed from the framework. - private boolean m_stale = false; - - // Used for bundle locking. - private int m_lockCount = 0; - private Thread m_lockThread = null; - - protected BundleInfo(Logger logger, BundleArchive archive, IModule module) - throws Exception - { - m_logger = logger; - m_archive = archive; - m_modules = (module == null) ? new IModule[0] : new IModule[] { module }; - - m_state = Bundle.INSTALLED; - m_stale = false; - m_activator = null; - m_context = null; - } - - /** - * Returns the bundle archive associated with this bundle. - * @return the bundle archive associated with this bundle. - **/ - public BundleArchive getArchive() - { - return m_archive; - } - - /** - * Returns an array of all modules associated with the bundle represented by - * this BundleInfo object. A module in the array corresponds to a - * revision of the bundle's JAR file and is ordered from oldest to newest. - * Multiple revisions of a bundle JAR file might exist if a bundle is - * updated, without refreshing the framework. In this case, exports from - * the prior revisions of the bundle JAR file are still offered; the - * current revision will be bound to packages from the prior revision, - * unless the packages were not offered by the prior revision. There is - * no limit on the potential number of bundle JAR file revisions. - * @return array of modules corresponding to the bundle JAR file revisions. - **/ - public IModule[] getModules() - { - return m_modules; - } - - /** - * Determines if the specified module is associated with this bundle. - * @param module the module to determine if it is associate with this bundle. - * @return true if the specified module is in the array of modules - * associated with this bundle, false otherwise. - **/ - public boolean hasModule(IModule module) - { - for (int i = 0; i < m_modules.length; i++) - { - if (m_modules[i] == module) - { - return true; - } - } - return false; - } - - /** - * Returns the newest module, which corresponds to the last module - * in the module array. - * @return the newest module. - **/ - public IModule getCurrentModule() - { - return m_modules[m_modules.length - 1]; - } - - /** - * Add a module that corresponds to a new bundle JAR file revision for - * the bundle associated with this BundleInfo object. - * @param module the module to add. - **/ - public void addModule(IModule module) - { - IModule[] dest = new IModule[m_modules.length + 1]; - System.arraycopy(m_modules, 0, dest, 0, m_modules.length); - dest[m_modules.length] = module; - m_modules = dest; - } - - public long getBundleId() - { - try - { - return m_archive.getId(); - } - catch (Exception ex) - { - m_logger.log( - Logger.LOG_ERROR, - "Error getting the identifier from bundle archive.", - ex); - return -1; - } - } - - public String getLocation() - { - try - { - return m_archive.getLocation(); - } - catch (Exception ex) - { - m_logger.log( - Logger.LOG_ERROR, - "Error getting location from bundle archive.", - ex); - return null; - } - } - - public int getStartLevel(int defaultLevel) - { - try - { - return m_archive.getStartLevel(); - } - catch (Exception ex) - { - m_logger.log( - Logger.LOG_ERROR, - "Error reading start level from bundle archive.", - ex); - return defaultLevel; - } - } - - public void setStartLevel(int i) - { - try - { - m_archive.setStartLevel(i); - } - catch (Exception ex) - { - m_logger.log( - Logger.LOG_ERROR, - "Error writing start level to bundle archive.", - ex); - } - } - - public Map getCurrentHeader() - { - try - { - // Return the header for the most recent bundle revision only, - // since we shouldn't ever need access to older revisions. - return m_archive.getRevision(m_archive.getRevisionCount() - 1).getManifestHeader(); - } - catch (Exception ex) - { - m_logger.log( - Logger.LOG_ERROR, - "Error reading manifest from bundle archive.", - ex); - return null; - } - } - - public int getState() - { - return m_state; - } - - public void setState(int i) - { - m_state = i; - } - - public long getLastModified() - { - return m_modified; - } - - public void setLastModified(long l) - { - m_modified = l; - } - - public int getPersistentState() - { - try - { - return m_archive.getPersistentState(); - } - catch (Exception ex) - { - m_logger.log( - Logger.LOG_ERROR, - "Error reading persistent state from bundle archive.", - ex); - return Bundle.INSTALLED; - } - } - - public void setPersistentStateInactive() - { - try - { - m_archive.setPersistentState(Bundle.INSTALLED); - } - catch (Exception ex) - { - m_logger.log(Logger.LOG_ERROR, - "Error writing persistent state to bundle archive.", - ex); - } - } - - public void setPersistentStateActive() - { - try - { - m_archive.setPersistentState(Bundle.ACTIVE); - } - catch (Exception ex) - { - m_logger.log( - Logger.LOG_ERROR, - "Error writing persistent state to bundle archive.", - ex); - } - } - - public void setPersistentStateUninstalled() - { - try - { - m_archive.setPersistentState(Bundle.UNINSTALLED); - } - catch (Exception ex) - { - m_logger.log( - Logger.LOG_ERROR, - "Error writing persistent state to bundle archive.", - ex); - } - } - - public BundleContext getContext() - { - return m_context; - } - - public void setContext(BundleContext context) - { - m_context = context; - } - - public BundleActivator getActivator() - { - return m_activator; - } - - public void setActivator(BundleActivator activator) - { - m_activator = activator; - } - - public boolean isStale() - { - return m_stale; - } - - public void setStale() - { - m_stale = true; - } - - // - // Locking related methods. - // NOTE: These methods are not synchronized because it is assumed they - // will only ever be called when the caller is in a synchronized block. - // - - public boolean isLockable() - { - return (m_lockCount == 0) || (m_lockThread == Thread.currentThread()); - } - - public void lock() - { - if ((m_lockCount > 0) && (m_lockThread != Thread.currentThread())) - { - throw new IllegalStateException("Bundle is locked by another thread."); - } - m_lockCount++; - m_lockThread = Thread.currentThread(); - } - - public void unlock() - { - if (m_lockCount == 0) - { - throw new IllegalStateException("Bundle is not locked."); - } - if ((m_lockCount > 0) && (m_lockThread != Thread.currentThread())) - { - throw new IllegalStateException("Bundle is locked by another thread."); - } - m_lockCount--; - if (m_lockCount == 0) - { - m_lockThread = null; - } - } - - public void syncLock(BundleInfo info) - { - m_lockCount = info.m_lockCount; - m_lockThread = info.m_lockThread; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/BundleProtectionDomain.java b/framework/src/main/java/org/apache/felix/framework/BundleProtectionDomain.java new file mode 100644 index 00000000000..ccd48079ddf --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/BundleProtectionDomain.java @@ -0,0 +1,484 @@ +/* + * 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.framework; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.ref.WeakReference; +import java.lang.reflect.InvocationTargetException; +import java.net.JarURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.security.AccessController; +import java.security.CodeSource; +import java.security.Permission; +import java.security.PermissionCollection; +import java.security.Permissions; +import java.security.PrivilegedAction; +import java.security.ProtectionDomain; +import java.security.cert.Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import org.apache.felix.framework.cache.Content; +import org.apache.felix.framework.cache.JarContent; +import org.apache.felix.framework.util.FelixConstants; +import org.osgi.framework.Bundle; +import org.osgi.framework.PackagePermission; + +import org.osgi.framework.wiring.BundleRevision; + +public class BundleProtectionDomain extends ProtectionDomain +{ + private static final class BundleInputStream extends InputStream + { + private final Content m_root; + private final Enumeration m_content; + private final OutputStreamBuffer m_outputBuffer = new OutputStreamBuffer(); + + private ByteArrayInputStream m_buffer = null; + private JarOutputStream m_output = null; + + private static final String DUMMY_ENTRY = "__DUMMY-ENTRY__/"; + + public BundleInputStream(Content root) throws IOException + { + m_root = root; + + List entries = new ArrayList(); + + int count = 0; + String manifest = null; + for (Enumeration e = m_root.getEntries(); e.hasMoreElements();) + { + String entry = (String) e.nextElement(); + if (entry.endsWith("/")) + { + // ignore + } + else if (entry.equalsIgnoreCase("META-INF/MANIFEST.MF")) + { + if (manifest == null) + { + manifest = entry; + } + } + else if (entry.toUpperCase().startsWith("META-INF/") + && entry.indexOf('/', "META-INF/".length()) < 0) + { + entries.add(count++, entry); + } + else + { + entries.add(entry); + } + } + entries.add(count++, DUMMY_ENTRY); + if (manifest == null) + { + manifest = "META-INF/MANIFEST.MF"; + } + m_content = Collections.enumeration(entries); + + m_output = new JarOutputStream(m_outputBuffer); + readNext(manifest); + m_buffer = new ByteArrayInputStream(m_outputBuffer.m_outBuffer + .toByteArray()); + + m_outputBuffer.m_outBuffer = null; + } + + public int read() throws IOException + { + if ((m_output == null) && (m_buffer == null)) + { + return -1; + } + + if (m_buffer != null) + { + int result = m_buffer.read(); + + if (result == -1) + { + m_buffer = null; + return read(); + } + + return result; + } + + if (m_content.hasMoreElements()) + { + String current = (String) m_content.nextElement(); + + readNext(current); + + if (!m_content.hasMoreElements()) + { + m_output.close(); + m_output = null; + } + + m_buffer = new ByteArrayInputStream(m_outputBuffer.m_outBuffer + .toByteArray()); + + m_outputBuffer.m_outBuffer = null; + } + else + { + m_output.close(); + m_output = null; + } + + return read(); + } + + private void readNext(String path) throws IOException + { + m_outputBuffer.m_outBuffer = new ByteArrayOutputStream(); + + if (path == DUMMY_ENTRY) + { + JarEntry entry = new JarEntry(path); + + m_output.putNextEntry(entry); + } + else + { + InputStream in = null; + try + { + in = m_root.getEntryAsStream(path); + + if (in == null) + { + throw new IOException("Missing entry"); + } + + JarEntry entry = new JarEntry(path); + + m_output.putNextEntry(entry); + + byte[] buffer = new byte[4 * 1024]; + + for (int c = in.read(buffer); c != -1; c = in.read(buffer)) + { + m_output.write(buffer, 0, c); + } + } + finally + { + if (in != null) + { + try + { + in.close(); + } + catch (Exception ex) + { + // Not much we can do + } + } + } + } + + m_output.closeEntry(); + } + } + + private static final class OutputStreamBuffer extends OutputStream + { + ByteArrayOutputStream m_outBuffer = null; + + public void write(int b) + { + m_outBuffer.write(b); + } + + public void write(byte[] buffer) throws IOException + { + m_outBuffer.write(buffer); + } + + public void write(byte[] buffer, int offset, int length) + { + m_outBuffer.write(buffer, offset, length); + } + } + + private static final class RevisionAsJarURL extends URLStreamHandler + { + private final WeakReference m_revision; + private volatile URL url; + + private RevisionAsJarURL(BundleRevisionImpl revision) + { + m_revision = new WeakReference(revision); + } + + @Override + protected URLConnection openConnection(URL u) throws IOException + { + if (url != null) { + return url.openConnection(); + } + BundleRevisionImpl revision = (BundleRevisionImpl) m_revision.get(); + + if (revision != null) + { + File target; + Content content = revision.getContent(); + if (content instanceof JarContent) + { + target = ((JarContent) content).getFile(); + } + else + { + target = Felix.m_secureAction.createTempFile("jar", null, null); + Felix.m_secureAction.deleteFileOnExit(target); + FileOutputStream output = null; + InputStream input = null; + IOException rethrow = null; + try + { + output = new FileOutputStream(target); + input = new BundleInputStream(content); + byte[] buffer = new byte[64 * 1024]; + for (int i = input.read(buffer);i != -1; i = input.read(buffer)) + { + output.write(buffer,0, i); + } + } + catch (IOException ex) + { + rethrow = ex; + } + finally + { + if (output != null) + { + try + { + output.close(); + } + catch (IOException ex) + { + if (rethrow == null) + { + rethrow = ex; + } + } + } + + if (input != null) + { + try + { + input.close(); + } + catch (IOException ex) + { + if (rethrow == null) + { + rethrow = ex; + } + } + } + + if (rethrow != null) + { + throw rethrow; + } + } + } + return (url = new URL("jar:" + target.toURI().toURL() + "!/")).openConnection(); + } + throw new IOException("Unable to access bundle revision."); + } + + private static boolean getUseCachedURL(final BundleRevisionImpl revision) + { + String property; + + if (System.getSecurityManager() != null) + { + property = (String) AccessController.doPrivileged(new PrivilegedAction(){ + @Override + public String run() + { + return getUseCachedURLProperty(revision); + } + }); + } + else + { + property = getUseCachedURLProperty(revision); + } + + return Boolean.parseBoolean(property); + } + + private static String getUseCachedURLProperty(BundleRevisionImpl revision) + { + return revision.getBundle().getFramework().getProperty(FelixConstants.USE_CACHEDURLS_PROPS); + } + + private static URL create(BundleRevisionImpl revision) throws MalformedURLException + { + RevisionAsJarURL handler = new RevisionAsJarURL(revision); + + boolean useCachedUrlForCodeSource = getUseCachedURL(revision); + + if (useCachedUrlForCodeSource) + { + String location = "jar:" + revision.getEntry("/") + "!/"; + return Felix.m_secureAction.createURL( + Felix.m_secureAction.createURL(null, "jar:", handler), + location, + handler + ); + } + + String location = revision.getBundle()._getLocation(); + if (location.startsWith("reference:")) + { + location = location.substring("reference:".length()); + } + + try + { + return Felix.m_secureAction.createURL( + Felix.m_secureAction.createURL(null, "jar:", handler), + location, + handler + ); + } + catch (MalformedURLException ex) + { + location = "jar:" + revision.getEntry("/") + "!/"; + + return Felix.m_secureAction.createURL( + Felix.m_secureAction.createURL(null, "jar:", handler), + location, + handler + ); + } + } + } + + private final WeakReference m_revision; + private final int m_hashCode; + private final String m_toString; + private volatile PermissionCollection m_woven; + + BundleProtectionDomain(BundleRevisionImpl revision, Object certificates) + throws MalformedURLException + { + super( + new CodeSource( + RevisionAsJarURL.create(revision), + (Certificate[]) certificates), + null, null, null); + m_revision = new WeakReference(revision); + m_hashCode = revision.hashCode(); + m_toString = "[" + revision + "]"; + } + + BundleRevisionImpl getRevision() + { + return m_revision.get(); + } + + public boolean implies(Permission permission) + { + Felix felix = getFramework(); + return felix != null && felix.impliesBundlePermission(this, permission, false); + } + + boolean superImplies(Permission permission) + { + return super.implies(permission); + } + + public boolean impliesDirect(Permission permission) + { + Felix felix = getFramework(); + return felix != null && felix.impliesBundlePermission(this, permission, true); + } + + boolean impliesWoven(Permission permission) + { + return m_woven != null && m_woven.implies(permission); + } + + synchronized void addWoven(String s) + { + if (m_woven == null) + { + m_woven = new Permissions(); + } + m_woven.add(new PackagePermission(s, PackagePermission.IMPORT)); + } + + BundleImpl getBundle() + { + BundleRevisionImpl revision = m_revision.get(); + return revision != null ? revision.getBundle() : null; + } + + Felix getFramework() { + BundleRevisionImpl revision = m_revision.get(); + return revision != null ? revision.getBundle().getFramework() : null; + } + + public int hashCode() + { + return m_hashCode; + } + + public boolean equals(Object other) + { + if ((other == null) || (other.getClass() != BundleProtectionDomain.class)) + { + return false; + } + if (m_hashCode != other.hashCode()) + { + return false; + } + return m_revision.get() == ((BundleProtectionDomain) other).m_revision.get(); + } + + public String toString() + { + return m_toString; + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/BundleRevisionDependencies.java b/framework/src/main/java/org/apache/felix/framework/BundleRevisionDependencies.java new file mode 100644 index 00000000000..74a66afc4de --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/BundleRevisionDependencies.java @@ -0,0 +1,328 @@ +/* + * 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.framework; + +import org.apache.felix.framework.util.Util; +import org.osgi.framework.Bundle; +import org.osgi.framework.namespace.HostNamespace; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleRevisions; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +class BundleRevisionDependencies +{ + private final Map>> + m_dependentsMap = new HashMap>>(); + + public synchronized void addDependent(BundleWire bw) + { + BundleRevision provider = bw.getProvider(); + Map> caps = + m_dependentsMap.get(provider); + if (caps == null) + { + caps = new HashMap>(); + m_dependentsMap.put(provider, caps); + } + Set dependents = caps.get(bw.getCapability()); + if (dependents == null) + { + dependents = new HashSet(); + caps.put(bw.getCapability(), dependents); + } + dependents.add(bw); + } + +/* + public synchronized void removeDependent( + BundleRevision provider, BundleCapability cap, BundleRevision requirer) + { + Map> caps = m_dependentsMap.get(provider); + if (caps != null) + { + Set dependents = caps.get(cap); + if (dependents == null) + { + dependents.remove(requirer); + if (dependents.isEmpty()) + { + caps.remove(cap); + if (caps.isEmpty()) + { + m_dependentsMap.remove(provider); + } + } + } + } + } +*/ + public synchronized void removeDependents(BundleRevision provider) + { + m_dependentsMap.remove(provider); + } + + public synchronized Map> + getDependents(BundleRevision provider) + { + return m_dependentsMap.get(provider); + } + + public synchronized boolean hasDependents(BundleRevision revision) + { + // We have to special case fragments, since their dependencies + // are actually reversed (i.e., they require a host, but then + // the host ends up dependent on them at run time). + if (Util.isFragment(revision) + && (revision.getWiring() != null)) + { + for (BundleWire bw : revision.getWiring().getRequiredWires(null)) + { + if (HostNamespace.HOST_NAMESPACE.equals(bw.getCapability().getNamespace())) + { + return true; + } + } + } + else if (m_dependentsMap.containsKey(revision)) + { + return true; + } + return false; + } + + public synchronized boolean hasDependents(Bundle bundle) + { + List revisions = bundle.adapt(BundleRevisions.class).getRevisions(); + for (BundleRevision revision : revisions) + { + if (hasDependents(revision)) + { + return true; + } + } + return false; + } + + public synchronized List getProvidedWires( + BundleRevision revision, String namespace) + { + List providedWires = new ArrayList(); + + Map> providedCaps = + m_dependentsMap.get(revision); + if (providedCaps != null) + { + // The wires are supposed to be in declared capability order, so + // get the capability list from the revision's wiring, which is + // in declared order (including fragments), and use it to create + // the provided wire list in declared order. + BundleWiring wiring = revision.getWiring(); + if (wiring != null) + { + List resolvedCaps = wiring.getCapabilities(namespace); + for (BundleCapability resolvedCap : resolvedCaps) + { + Set dependentWires = providedCaps.get(resolvedCap); + if (dependentWires != null) + { + providedWires.addAll(dependentWires); + } + } + } + } + + return providedWires; + } + + public synchronized Set getDependentBundles(Bundle bundle) + { + Set result = new HashSet(); + + List revisions = bundle.adapt(BundleRevisions.class).getRevisions(); + for (BundleRevision revision : revisions) + { + // We need to special case fragments, + // since their dependents are their hosts. + if (Util.isFragment(revision)) + { + BundleWiring wiring = revision.getWiring(); + if (wiring != null) + { + for (BundleWire bw : wiring.getRequiredWires(null)) + { + if (HostNamespace.HOST_NAMESPACE.equals(bw.getCapability().getNamespace())) + { + result.add(bw.getProvider().getBundle()); + } + } + } + } + else + { + Map> caps = + m_dependentsMap.get(revision); + if (caps != null) + { + for (Entry> entry : caps.entrySet()) + { + for (BundleWire dependentWire : entry.getValue()) + { + result.add(dependentWire.getRequirer().getBundle()); + } + } + } + } + } + + return result; + } + + public synchronized Set getImportingBundles( + Bundle exporter, BundleCapability exportCap) + { + // Create set for storing importing bundles. + Set result = new HashSet(); + + // Get exported package name. + String pkgName = (String) + exportCap.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE); + + // Get all importers and requirers for all revisions of the bundle. + // The spec says that require-bundle should be returned with importers. + for (BundleRevision revision : exporter.adapt(BundleRevisions.class).getRevisions()) + { + Map> + caps = m_dependentsMap.get(revision); + if (caps != null) + { + for (Entry> entry : caps.entrySet()) + { + BundleCapability cap = entry.getKey(); + if ((cap.getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE) + && cap.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE) + .equals(pkgName)) + || cap.getNamespace().equals(BundleRevision.BUNDLE_NAMESPACE)) + { + for (BundleWire dependentWire : entry.getValue()) + { + result.add(dependentWire.getRequirer().getBundle()); + } + } + } + } + } + + // Return the results. + return result; + } + + public synchronized Set getRequiringBundles(Bundle bundle) + { + // Create set for storing requiring bundles. + Set result = new HashSet(); + + // Get all requirers for all revisions of the bundle. + for (BundleRevision revision : bundle.adapt(BundleRevisions.class).getRevisions()) + { + Map> + caps = m_dependentsMap.get(revision); + if (caps != null) + { + for (Entry> entry : caps.entrySet()) + { + if (entry.getKey().getNamespace() + .equals(BundleRevision.BUNDLE_NAMESPACE)) + { + for (BundleWire dependentWire : entry.getValue()) + { + result.add(dependentWire.getRequirer().getBundle()); + } + } + } + } + } + + // Return the results. + return result; + } + + public synchronized void removeDependencies(Bundle bundle) + { + List revs = bundle.adapt(BundleRevisions.class).getRevisions(); + for (BundleRevision rev : revs) + { + BundleWiring wiring = rev.getWiring(); + if (wiring != null) + { + for (BundleWire bw : wiring.getRequiredWires(null)) + { + Map> caps = + m_dependentsMap.get(bw.getProvider()); + if (caps != null) + { + List gc = new ArrayList(); + for (Entry> entry + : caps.entrySet()) + { + entry.getValue().remove(bw); + if (entry.getValue().isEmpty()) + { + gc.add(entry.getKey()); + } + } + for (BundleCapability cap : gc) + { + caps.remove(cap); + } + if (caps.isEmpty()) + { + m_dependentsMap.remove(bw.getProvider()); + } + } + } + } + } + } + + public synchronized void dump() + { +/* +System.out.println("DEPENDENTS:"); + for (Entry>> entry + : m_dependentsMap.entrySet()) + { + System.out.println("Revision " + entry.getKey() + " DEPS:"); + for (Entry> capEntry : entry.getValue().entrySet()) + { + System.out.println(" " + capEntry.getKey() + " <-- " + capEntry.getValue()); + } + } +*/ + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/BundleRevisionImpl.java b/framework/src/main/java/org/apache/felix/framework/BundleRevisionImpl.java new file mode 100644 index 00000000000..a6d47ab2c53 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/BundleRevisionImpl.java @@ -0,0 +1,686 @@ +/* + * 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.framework; + +import org.apache.felix.framework.cache.Content; +import org.apache.felix.framework.util.FelixConstants; +import org.apache.felix.framework.util.MultiReleaseContent; +import org.apache.felix.framework.util.SecureAction; +import org.apache.felix.framework.util.Util; +import org.apache.felix.framework.util.manifestparser.ManifestParser; +import org.apache.felix.framework.util.manifestparser.NativeLibrary; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.Version; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; + +public class BundleRevisionImpl implements BundleRevision, Resource +{ + public final static int EAGER_ACTIVATION = 0; + public final static int LAZY_ACTIVATION = 1; + + private final String m_id; + private final Map m_headerMap; + + private final String m_manifestVersion; + private final boolean m_isExtension; + private final boolean m_isFragment; + private final String m_symbolicName; + private final Version m_version; + + private final List m_declaredCaps; + private final List m_declaredReqs; + private final List m_declaredNativeLibs; + private final int m_declaredActivationPolicy; + private final List m_activationIncludes; + private final List m_activationExcludes; + + private final BundleImpl m_bundle; + + private volatile Content m_content; + private volatile List m_contentPath; + private volatile ProtectionDomain m_protectionDomain = null; + private final static SecureAction m_secureAction = new SecureAction(); + + // Bundle wiring when resolved. + private volatile BundleWiringImpl m_wiring = null; + + /** + * This constructor is used by the extension manager, since it needs + * a constructor that does not throw an exception. + * @param bundle + * @param id + * @throws org.osgi.framework.BundleException + */ + public BundleRevisionImpl(BundleImpl bundle, String id) + { + m_bundle = bundle; + m_id = id; + m_headerMap = null; + m_content = null; + m_manifestVersion = ""; + m_symbolicName = null; + m_isExtension = false; + m_isFragment = false; + m_version = null; + m_declaredCaps = Collections.emptyList(); + m_declaredReqs = Collections.emptyList(); + m_declaredNativeLibs = null; + m_declaredActivationPolicy = EAGER_ACTIVATION; + m_activationExcludes = null; + m_activationIncludes = null; + } + + BundleRevisionImpl( + BundleImpl bundle, String id, Map headerMap, Content content) + throws BundleException + { + m_bundle = bundle; + m_id = id; + m_headerMap = headerMap; + m_content = content; + + ManifestParser mp = new ManifestParser( + bundle.getFramework().getLogger(), + bundle.getFramework().getConfig(), + this, + m_headerMap); + + // Record some of the parsed metadata. Note, if this is an extension + // bundle it's exports are removed, since they will be added to the + // system bundle directly later on. + + m_isExtension = mp.isExtension(); + m_manifestVersion = mp.getManifestVersion(); + m_version = mp.getBundleVersion(); + m_declaredCaps = mp.getCapabilities(); + m_declaredReqs = mp.getRequirements(); + m_declaredNativeLibs = mp.getLibraries(); + m_declaredActivationPolicy = mp.getActivationPolicy(); + m_activationExcludes = (mp.getActivationExcludeDirective() == null) + ? null + : ManifestParser.parseDelimitedString(mp.getActivationExcludeDirective(), ","); + m_activationIncludes = (mp.getActivationIncludeDirective() == null) + ? null + : ManifestParser.parseDelimitedString(mp.getActivationIncludeDirective(), ","); + m_symbolicName = mp.getSymbolicName(); + m_isFragment = m_headerMap.containsKey(Constants.FRAGMENT_HOST); + } + + static SecureAction getSecureAction() + { + return m_secureAction; + } + + int getDeclaredActivationPolicy() + { + return m_declaredActivationPolicy; + } + + boolean isActivationTrigger(String pkgName) + { + if ((m_activationIncludes == null) && (m_activationExcludes == null)) + { + return true; + } + + // If there are no include filters then all classes are included + // by default, otherwise try to find one match. + boolean included = (m_activationIncludes == null); + for (int i = 0; + (!included) && (m_activationIncludes != null) && (i < m_activationIncludes.size()); + i++) + { + included = m_activationIncludes.get(i).equals(pkgName); + } + + // If there are no exclude filters then no classes are excluded + // by default, otherwise try to find one match. + boolean excluded = false; + for (int i = 0; + (!excluded) && (m_activationExcludes != null) && (i < m_activationExcludes.size()); + i++) + { + excluded = m_activationExcludes.get(i).equals(pkgName); + } + return included && !excluded; + } + + // + // BundleRevision methods. + // + + public String getSymbolicName() + { + return m_symbolicName; + } + + public Version getVersion() + { + return m_version; + } + + public List getCapabilities(String namespace) + { + return asCapabilityList(getDeclaredCapabilities(namespace)); + } + + static List asCapabilityList(List reqs) + { + return reqs; + } + + public List getDeclaredCapabilities(String namespace) + { + List result = m_declaredCaps; + if (namespace != null) + { + result = new ArrayList(); + for (BundleCapability cap : m_declaredCaps) + { + if (cap.getNamespace().equals(namespace)) + { + result.add(cap); + } + } + } + return result; + } + + public List getRequirements(String namespace) + { + return asRequirementList(getDeclaredRequirements(namespace)); + } + + static List asRequirementList(List reqs) + { + return reqs; + } + + public List getDeclaredRequirements(String namespace) + { + List result = m_declaredReqs; + if (namespace != null) + { + result = new ArrayList(); + for (BundleRequirement req : m_declaredReqs) + { + if (req.getNamespace().equals(namespace)) + { + result.add(req); + } + } + } + return result; + } + + public int getTypes() + { + return (getManifestVersion().equals("2") && m_isFragment) ? BundleRevision.TYPE_FRAGMENT : 0; + } + + public BundleWiring getWiring() + { + return m_wiring; + } + + public BundleImpl getBundle() + { + return m_bundle; + } + + // + // Implementating details. + // + + public Map getHeaders() + { + return m_headerMap; + } + + public boolean isExtension() + { + return m_isExtension; + } + + public String getManifestVersion() + { + return m_manifestVersion; + } + + public List getDeclaredNativeLibraries() + { + return m_declaredNativeLibs; + } + + public String getId() + { + return m_id; + } + + public synchronized void resolve(BundleWiringImpl wiring) + { + if (m_wiring != null) + { + m_wiring.dispose(); + m_wiring = null; + } + + if (wiring != null) + { + // If the wiring has fragments, then close the old content path, + // since it'll need to be recalculated to include fragments. + if (!Util.getFragments(wiring).isEmpty()) + { + for (int i = 0; (m_contentPath != null) && (i < m_contentPath.size()); i++) + { + // Don't close this module's content, if it is on the content path. + if (m_content != m_contentPath.get(i)) + { + m_contentPath.get(i).close(); + } + } + m_contentPath = null; + } + + m_wiring = wiring; + } + } + + public void setProtectionDomain(ProtectionDomain pd) + { + m_protectionDomain = pd; + } + + public ProtectionDomain getProtectionDomain() + { + return m_protectionDomain; + } + + // + // Content access methods. + // + + public Content getContent() + { + return m_content; + } + + void resetContent(Content content) + { + m_content = content; + } + + List getContentPath() + { + if (m_contentPath == null) + { + try + { + m_contentPath = initializeContentPath(); + } + catch (Exception ex) + { + m_bundle.getFramework().getLogger().log( + m_bundle, Logger.LOG_ERROR, "Unable to get module class path.", ex); + } + } + return m_contentPath; + } + + private synchronized List initializeContentPath() throws Exception + { + if (m_contentPath != null) + { + return m_contentPath; + } + List contentList = new ArrayList(); + calculateContentPath(this, getContent(), contentList, true); + + List fragments = null; + List fragmentContents = null; + if (m_wiring != null) + { + // Get fragments and their contents from the wiring. + // Note that we don't use Util.getFragments() here because + // the wiring returns parallel arrays and the utility method + // doesn't necessarily return the correct order. + fragments = m_wiring.getFragments(); + fragmentContents = m_wiring.getFragmentContents(); + } + if (fragments != null) + { + for (int i = 0; i < fragments.size(); i++) + { + calculateContentPath( + fragments.get(i), fragmentContents.get(i), contentList, false); + } + } + return contentList; + } + + private List calculateContentPath( + BundleRevision revision, Content content, List contentList, + boolean searchFragments) + { + // Creating the content path entails examining the bundle's + // class path to determine whether the bundle JAR file itself + // is on the bundle's class path and then creating content + // objects for everything on the class path. + + // Create a list to contain the content path for the specified content. + List localContentList = new ArrayList(); + + // Find class path meta-data. + String classPath = (String) ((BundleRevisionImpl) revision) + .getHeaders().get(FelixConstants.BUNDLE_CLASSPATH); + // Parse the class path into strings. + List classPathStrings = ManifestParser.parseDelimitedString( + classPath, FelixConstants.CLASS_PATH_SEPARATOR); + + if (classPathStrings == null) + { + classPathStrings = new ArrayList(0); + } + + // Create the bundles class path. + for (int i = 0; i < classPathStrings.size(); i++) + { + // Remove any leading slash, since all bundle class path + // entries are relative to the root of the bundle. + classPathStrings.set(i, (classPathStrings.get(i).startsWith("/")) + ? classPathStrings.get(i).substring(1) + : classPathStrings.get(i)); + + // Check for the bundle itself on the class path. + if (classPathStrings.get(i).equals(FelixConstants.CLASS_PATH_DOT)) + { + localContentList.add(MultiReleaseContent.wrap( + getBundle().getFramework()._getProperty("java.specification.version"), content)); + } + else + { + // Try to find the embedded class path entry in the current + // content. + Content embeddedContent = content.getEntryAsContent(classPathStrings.get(i)); + // If the embedded class path entry was not found, it might be + // in one of the fragments if the current content is the bundle, + // so try to search the fragments if necessary. + List fragmentContents = (m_wiring == null) + ? null : m_wiring.getFragmentContents(); + for (int fragIdx = 0; + searchFragments && (embeddedContent == null) + && (fragmentContents != null) && (fragIdx < fragmentContents.size()); + fragIdx++) + { + embeddedContent = + fragmentContents.get(fragIdx).getEntryAsContent(classPathStrings.get(i)); + } + // If we found the embedded content, then add it to the + // class path content list. + if (embeddedContent != null) + { + localContentList.add(MultiReleaseContent.wrap( + getBundle().getFramework()._getProperty("java.specification.version"),embeddedContent)); + } + else + { +// TODO: FRAMEWORK - Per the spec, this should fire a FrameworkEvent.INFO event; +// need to create an "Eventer" class like "Logger" perhaps. + ((BundleImpl) m_bundle).getFramework().getLogger().log( + getBundle(), Logger.LOG_INFO, + "Class path entry not found: " + + classPathStrings.get(i)); + } + } + } + + // If there is nothing on the class path, then include + // "." by default, as per the spec. + if (localContentList.isEmpty()) + { + localContentList.add(MultiReleaseContent.wrap( + getBundle().getFramework()._getProperty("java.specification.version"),content)); + } + + // Now add the local contents to the global content list and return it. + contentList.addAll(localContentList); + return contentList; + } + + URL getResourceLocal(String name) + { + URL url = null; + + // Remove leading slash, if present, but special case + // "/" so that it returns a root URL...this isn't very + // clean or meaningful, but the Spring guys want it. + if (name.equals("/")) + { + // Just pick a class path index since it doesn't really matter. + url = createURL(1, name); + } + else if (name.startsWith("/")) + { + name = name.substring(1); + } + + // Check the module class path. + List contentPath = getContentPath(); + for (int i = 0; + (url == null) && + (i < contentPath.size()); i++) + { + if (contentPath.get(i).hasEntry(name)) + { + url = createURL(i + 1, name); + } + } + + return url; + } + + Enumeration getResourcesLocal(String name) + { + List l = new ArrayList(); + + // Special case "/" so that it returns a root URLs for + // each bundle class path entry...this isn't very + // clean or meaningful, but the Spring guys want it. + final List contentPath = getContentPath(); + if (contentPath == null) + return Collections.enumeration(Collections.emptyList()); + + if (name.equals("/")) + { + for (int i = 0; i < contentPath.size(); i++) + { + l.add(createURL(i + 1, name)); + } + } + else + { + // Remove leading slash, if present. + if (name.startsWith("/")) + { + name = name.substring(1); + } + + // Check the module class path. + for (int i = 0; i < contentPath.size(); i++) + { + if (contentPath.get(i).hasEntry(name)) + { + // Use the class path index + 1 for creating the path so + // that we can differentiate between module content URLs + // (where the path will start with 0) and module class + // path URLs. + l.add(createURL(i + 1, name)); + } + } + } + + return Collections.enumeration(l); + } + + // TODO: API: Investigate how to handle this better, perhaps we need + // multiple URL policies, one for content -- one for class path. + public URL getEntry(String name) + { + URL url = null; + + // Check for the special case of "/", which represents + // the root of the bundle according to the spec. + if (name.equals("/")) + { + url = createURL(0, "/"); + } + + if (url == null) + { + // Remove leading slash, if present. + if (name.startsWith("/")) + { + name = name.substring(1); + } + + // Check the module content. + if (getContent().hasEntry(name)) + { + // Module content URLs start with 0, whereas module + // class path URLs start with the index into the class + // path + 1. + url = createURL(0, name); + } + } + + return url; + } + + public boolean hasInputStream(int index, String urlPath) + { + if (urlPath.startsWith("/")) + { + urlPath = urlPath.substring(1); + } + if (index == 0) + { + return getContent().hasEntry(urlPath); + } + return getContentPath().get(index - 1).hasEntry(urlPath); + } + + public InputStream getInputStream(int index, String urlPath) + throws IOException + { + if (urlPath.startsWith("/")) + { + urlPath = urlPath.substring(1); + } + if (index == 0) + { + return getContent().getEntryAsStream(urlPath); + } + return getContentPath().get(index - 1).getEntryAsStream(urlPath); + } + + public URL getLocalURL(int index, String urlPath) + { + if (urlPath.startsWith("/")) + { + urlPath = urlPath.substring(1); + } + if (index == 0) + { + return getContent().getEntryAsURL(urlPath); + } + return getContentPath().get(index - 1).getEntryAsURL(urlPath); + } + + private URL createURL(int port, String path) + { + // Add a slash if there is one already, otherwise + // the is no slash separating the host from the file + // in the resulting URL. + if (!path.startsWith("/")) + { + path = "/" + path; + } + + try + { + return m_secureAction.createURL(null, + FelixConstants.BUNDLE_URL_PROTOCOL + "://" + + m_bundle.getFramework()._getProperty(Constants.FRAMEWORK_UUID) + "_" + m_id + ":" + port + path, + getBundle().getFramework().getBundleStreamHandler()); + } + catch (MalformedURLException ex) + { + m_bundle.getFramework().getLogger().log( + m_bundle, + Logger.LOG_ERROR, + "Unable to create resource URL.", + ex); + } + return null; + } + + synchronized void close() + { + try + { + resolve(null); + } + catch (Exception ex) + { + ((BundleImpl) m_bundle).getFramework().getLogger().log( + Logger.LOG_ERROR, "Error releasing revision: " + ex.getMessage(), ex); + } + m_content.close(); + m_content = null; + for (int i = 0; (m_contentPath != null) && (i < m_contentPath.size()); i++) + { + m_contentPath.get(i).close(); + } + m_contentPath = null; + } + + @Override + public String toString() + { + return m_bundle.toString() + "(R " + m_id + ")"; + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/BundleWiringImpl.java b/framework/src/main/java/org/apache/felix/framework/BundleWiringImpl.java new file mode 100644 index 00000000000..fa1f7299ec7 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/BundleWiringImpl.java @@ -0,0 +1,2895 @@ +/* + * 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.framework; + +import org.apache.felix.framework.cache.Content; +import org.apache.felix.framework.capabilityset.SimpleFilter; +import org.apache.felix.framework.resolver.ResourceNotFoundException; +import org.apache.felix.framework.util.CompoundEnumeration; +import org.apache.felix.framework.util.FelixConstants; +import org.apache.felix.framework.util.SecurityManagerEx; +import org.apache.felix.framework.util.Util; +import org.apache.felix.framework.util.manifestparser.ManifestParser; +import org.apache.felix.framework.util.manifestparser.NativeLibrary; +import org.apache.felix.framework.wiring.BundleRequirementImpl; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; +import org.osgi.framework.BundleReference; +import org.osgi.framework.CapabilityPermission; +import org.osgi.framework.Constants; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.PackagePermission; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.hooks.weaving.WeavingException; +import org.osgi.framework.hooks.weaving.WeavingHook; +import org.osgi.framework.hooks.weaving.WovenClass; +import org.osgi.framework.hooks.weaving.WovenClassListener; +import org.osgi.framework.namespace.IdentityNamespace; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Wire; +import org.osgi.service.resolver.ResolutionException; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.security.SecureClassLoader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; + +public class BundleWiringImpl implements BundleWiring +{ + public final static int LISTRESOURCES_DEBUG = 1048576; + + public final static int EAGER_ACTIVATION = 0; + public final static int LAZY_ACTIVATION = 1; + + public static final ClassLoader CNFE_CLASS_LOADER = new ClassLoader() + { + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException + { + throw new ClassNotFoundException("Unable to load class '" + name + "'"); + } + }; + + private final Logger m_logger; + private final Map m_configMap; + private final StatefulResolver m_resolver; + private final BundleRevisionImpl m_revision; + private final List m_fragments; + // Wire list is copy-on-write since it may change due to + // dynamic imports. + private volatile List m_wires; + // Imported package map is copy-on-write since it may change + // due to dynamic imports. + private volatile Map m_importedPkgs; + private final Map> m_requiredPkgs; + private final List m_resolvedCaps; + private final Map>> m_includedPkgFilters; + private final Map>> m_excludedPkgFilters; + private final List m_resolvedReqs; + private final List m_resolvedNativeLibs; + private final List m_fragmentContents; + + private volatile List m_wovenReqs = null; + + private volatile BundleClassLoader m_classLoader; + + // Bundle-specific class loader for boot delegation. + private final ClassLoader m_bootClassLoader; + // Default class loader for boot delegation. + private final static ClassLoader m_defBootClassLoader; + + // Statically define the default class loader for boot delegation. + static + { + ClassLoader cl = null; + try + { + cl = (ClassLoader) BundleRevisionImpl.getSecureAction().invokeDirect( + BundleRevisionImpl.getSecureAction().getMethod(ClassLoader.class, "getPlatformClassLoader", null) + ,null, null); + } + catch (Throwable t) + { + // Not on Java9 + try + { + Constructor ctor = BundleRevisionImpl.getSecureAction().getDeclaredConstructor( + SecureClassLoader.class, new Class[]{ClassLoader.class}); + BundleRevisionImpl.getSecureAction().setAccesssible(ctor); + cl = (ClassLoader) BundleRevisionImpl.getSecureAction().invoke( + ctor, new Object[]{null}); + } + catch (Throwable ex) + { + // On Android we get an exception if we set the parent class loader + // to null, so we will work around that case by setting the parent + // class loader to the system class loader in getClassLoader() below. + cl = null; + System.err.println("Problem creating boot delegation class loader: " + ex); + } + } + m_defBootClassLoader = cl; + } + + // Boolean flag to enable/disable implicit boot delegation. + private final boolean m_implicitBootDelegation; + // Boolean flag to enable/disable local URLs. + private final boolean m_useLocalURLs; + + // Re-usable security manager for accessing class context. + private static SecurityManagerEx m_sm = new SecurityManagerEx(); + + // Thread local to detect class loading cycles. + private final ThreadLocal m_cycleCheck = new ThreadLocal(); + + // Thread local to keep track of deferred activation. + private static final ThreadLocal m_deferredActivation = new ThreadLocal(); + + // Flag indicating whether this wiring has been disposed. + private volatile boolean m_isDisposed = false; + + private volatile ConcurrentHashMap m_accessorLookupCache; + + BundleWiringImpl( + Logger logger, Map configMap, StatefulResolver resolver, + BundleRevisionImpl revision, List fragments, + List wires, + Map importedPkgs, + Map> requiredPkgs) throws Exception + { + m_logger = logger; + m_configMap = configMap; + m_resolver = resolver; + m_revision = revision; + m_importedPkgs = importedPkgs; + m_requiredPkgs = requiredPkgs; + m_wires = Util.newImmutableList(wires); + + // We need to sort the fragments and add ourself as a dependent of each one. + // We also need to create an array of fragment contents to attach to our + // content path. + List fragmentContents = null; + if (fragments != null) + { + // Sort fragments according to ID order, if necessary. + // Note that this sort order isn't 100% correct since + // it uses a string, but it is likely close enough and + // avoids having to create more objects. + if (fragments.size() > 1) + { + SortedMap sorted = new TreeMap(); + for (BundleRevision f : fragments) + { + sorted.put(((BundleRevisionImpl) f).getId(), f); + } + fragments = new ArrayList(sorted.values()); + } + fragmentContents = new ArrayList(fragments.size()); + for (int i = 0; (fragments != null) && (i < fragments.size()); i++) + { + fragmentContents.add( + ((BundleRevisionImpl) fragments.get(i)).getContent() + .getEntryAsContent(FelixConstants.CLASS_PATH_DOT)); + } + } + m_fragments = fragments; + m_fragmentContents = fragmentContents; + + // Calculate resolved list of requirements, which includes: + // 1. All requirements for which we have a wire. + // 2. All dynamic imports from the host and any fragments. + // Also create set of imported packages so we can eliminate any + // substituted exports from our resolved capabilities. + Set imports = new HashSet(); + List reqList = new ArrayList(); + // First add resolved requirements from wires. + for (BundleWire bw : wires) + { + // Fragments may have multiple wires for the same requirement, so we + // need to check for and avoid duplicates in that case. + if (!bw.getRequirement().getNamespace().equals(BundleRevision.HOST_NAMESPACE) + || !reqList.contains(bw.getRequirement())) + { + reqList.add(bw.getRequirement()); + if (bw.getRequirement().getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE)) + { + imports.add((String) + bw.getCapability().getAttributes().get(BundleRevision.PACKAGE_NAMESPACE)); + } + } + } + // Next add dynamic requirements from host. + for (BundleRequirement req : m_revision.getDeclaredRequirements(null)) + { + if (req.getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE)) + { + String resolution = req.getDirectives().get(Constants.RESOLUTION_DIRECTIVE); + if ((resolution != null) && (resolution.equals("dynamic"))) + { + reqList.add(req); + } + } + } + // Finally, add dynamic requirements from fragments. + if (m_fragments != null) + { + for (BundleRevision fragment : m_fragments) + { + for (BundleRequirement req : fragment.getDeclaredRequirements(null)) + { + if (req.getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE)) + { + String resolution = req.getDirectives().get(Constants.RESOLUTION_DIRECTIVE); + if ((resolution != null) && (resolution.equals("dynamic"))) + { + reqList.add(req); + } + } + } + } + } + m_resolvedReqs = Util.newImmutableList(reqList); + + // Calculate resolved list of capabilities, which includes: + // 1. All capabilities from host and any fragments except for exported + // packages that we have an import (i.e., the export was substituted). + // 2. For fragments the identity capability only. + // And nothing else at this time. + boolean isFragment = Util.isFragment(revision); + List capList = new ArrayList(); + // Also keep track of whether any resolved package capabilities are filtered. + Map>> includedPkgFilters = + new HashMap>>(); + Map>> excludedPkgFilters = + new HashMap>>(); + + if (isFragment) + { + // This is a fragment, add its identity capability + for (BundleCapability cap : m_revision.getDeclaredCapabilities(null)) + { + if (IdentityNamespace.IDENTITY_NAMESPACE.equals(cap.getNamespace())) + { + String effective = cap.getDirectives().get(Constants.EFFECTIVE_DIRECTIVE); + if ((effective == null) || (effective.equals(Constants.EFFECTIVE_RESOLVE))) + { + capList.add(cap); + } + } + } + } + else + { + for (BundleCapability cap : m_revision.getDeclaredCapabilities(null)) + { + if (!cap.getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE) + || (cap.getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE) + && !imports.contains(cap.getAttributes() + .get(BundleRevision.PACKAGE_NAMESPACE).toString()))) + { + // TODO: OSGi R4.4 - We may need to make this more flexible since in the future it may + // be possible to consider other effective values via OBR's Environment.isEffective(). + String effective = cap.getDirectives().get(Constants.EFFECTIVE_DIRECTIVE); + if ((effective == null) || (effective.equals(Constants.EFFECTIVE_RESOLVE))) + { + capList.add(cap); + if (cap.getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE)) + { + List> filters = + parsePkgFilters(cap, Constants.INCLUDE_DIRECTIVE); + if (filters != null) + { + includedPkgFilters.put((String) + cap.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE), + filters); + } + filters = parsePkgFilters(cap, Constants.EXCLUDE_DIRECTIVE); + if (filters != null) + { + excludedPkgFilters.put((String) + cap.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE), + filters); + } + } + } + } + } + if (m_fragments != null) + { + for (BundleRevision fragment : m_fragments) + { + for (BundleCapability cap : fragment.getDeclaredCapabilities(null)) + { + if (IdentityNamespace.IDENTITY_NAMESPACE.equals(cap.getNamespace())) { + // The identity capability is not transferred from the fragment to the bundle + continue; + } + + if (!cap.getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE) + || (cap.getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE) + && !imports.contains(cap.getAttributes() + .get(BundleRevision.PACKAGE_NAMESPACE).toString()))) + { + // TODO: OSGi R4.4 - We may need to make this more flexible since in the future it may + // be possible to consider other effective values via OBR's Environment.isEffective(). + String effective = cap.getDirectives().get(Constants.EFFECTIVE_DIRECTIVE); + if ((effective == null) || (effective.equals(Constants.EFFECTIVE_RESOLVE))) + { + capList.add(cap); + if (cap.getNamespace().equals( + BundleRevision.PACKAGE_NAMESPACE)) + { + List> filters = + parsePkgFilters( + cap, Constants.INCLUDE_DIRECTIVE); + if (filters != null) + { + includedPkgFilters.put((String) + cap.getAttributes() + .get(BundleRevision.PACKAGE_NAMESPACE), + filters); + } + filters = parsePkgFilters(cap, Constants.EXCLUDE_DIRECTIVE); + if (filters != null) + { + excludedPkgFilters.put((String) + cap.getAttributes() + .get(BundleRevision.PACKAGE_NAMESPACE), + filters); + } + } + } + } + } + } + } + } + + if (System.getSecurityManager() != null) + { + for (Iterator iter = capList.iterator();iter.hasNext();) + { + BundleCapability cap = iter.next(); + if (cap.getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE)) + { + if (!((BundleProtectionDomain) ((BundleRevisionImpl) cap.getRevision()).getProtectionDomain()).impliesDirect( + new PackagePermission((String) cap.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE), PackagePermission.EXPORTONLY))) + { + iter.remove(); + } + } + else if (!cap.getNamespace().equals(BundleRevision.HOST_NAMESPACE) && !cap.getNamespace().equals(BundleRevision.BUNDLE_NAMESPACE) && + !cap.getNamespace().equals("osgi.ee")) + { + if (!((BundleProtectionDomain) ((BundleRevisionImpl) cap.getRevision()).getProtectionDomain()).impliesDirect( + new CapabilityPermission(cap.getNamespace(), CapabilityPermission.PROVIDE))) + { + iter.remove(); + } + } + } + } + + m_resolvedCaps = Util.newImmutableList(capList); + m_includedPkgFilters = (includedPkgFilters.isEmpty()) + ? Collections.EMPTY_MAP : includedPkgFilters; + m_excludedPkgFilters = (excludedPkgFilters.isEmpty()) + ? Collections.EMPTY_MAP : excludedPkgFilters; + + List libList = (m_revision.getDeclaredNativeLibraries() == null) + ? new ArrayList() + : new ArrayList(m_revision.getDeclaredNativeLibraries()); + for (int fragIdx = 0; + (m_fragments != null) && (fragIdx < m_fragments.size()); + fragIdx++) + { + List libs = + ((BundleRevisionImpl) m_fragments.get(fragIdx)) + .getDeclaredNativeLibraries(); + for (int reqIdx = 0; + (libs != null) && (reqIdx < libs.size()); + reqIdx++) + { + libList.add(libs.get(reqIdx)); + } + } + // We need to return null here if we don't have any libraries, since a + // zero-length array is used to indicate that matching native libraries + // could not be found when resolving the bundle. + m_resolvedNativeLibs = (libList.isEmpty()) ? null : Util.newImmutableList(libList); + + ClassLoader bootLoader = m_defBootClassLoader; + if (revision.getBundle().getBundleId() != 0) + { + Object map = m_configMap.get(FelixConstants.BOOT_CLASSLOADERS_PROP); + if (map instanceof Map) + { + Object l = ((Map) map).get(m_revision.getBundle()); + if (l instanceof ClassLoader) + { + bootLoader = (ClassLoader) l; + } + } + } + m_bootClassLoader = bootLoader; + + m_implicitBootDelegation = + (m_configMap.get(FelixConstants.IMPLICIT_BOOT_DELEGATION_PROP) == null) + || Boolean.valueOf( + (String) m_configMap.get( + FelixConstants.IMPLICIT_BOOT_DELEGATION_PROP)); + + m_useLocalURLs = + m_configMap.get(FelixConstants.USE_LOCALURLS_PROP) != null; + } + + private static List> parsePkgFilters(BundleCapability cap, String filtername) + { + List> filters = null; + String include = cap.getDirectives().get(filtername); + if (include != null) + { + List filterStrings = ManifestParser.parseDelimitedString(include, ","); + filters = new ArrayList>(filterStrings.size()); + + for (int filterIdx = 0; filterIdx < filterStrings.size(); filterIdx++) + { + List substrings = + SimpleFilter.parseSubstring(filterStrings.get(filterIdx)); + filters.add(substrings); + } + } + return filters; + } + + @Override + public String toString() + { + return m_revision.getBundle().toString(); + } + + public synchronized void dispose() + { + if (m_fragmentContents != null) + { + for (Content content : m_fragmentContents) + { + content.close(); + } + } + m_classLoader = null; + m_isDisposed = true; + m_accessorLookupCache = null; + } + + // TODO: OSGi R4.3 - This really shouldn't be public, but it is needed by the + // resolver to determine if a bundle can dynamically import. + public boolean hasPackageSource(String pkgName) + { + return (m_importedPkgs.containsKey(pkgName) || m_requiredPkgs.containsKey(pkgName)); + } + + // TODO: OSGi R4.3 - This really shouldn't be public, but it is needed by the + // to implement dynamic imports. + public BundleRevision getImportedPackageSource(String pkgName) + { + return m_importedPkgs.get(pkgName); + } + + List getFragments() + { + return m_fragments; + } + + List getFragmentContents() + { + return m_fragmentContents; + } + + @Override + public boolean isCurrent() + { + BundleRevision current = getBundle().adapt(BundleRevision.class); + return (current != null) && (current.getWiring() == this); + } + + @Override + public boolean isInUse() + { + return !m_isDisposed; + } + + @Override + public List getResourceCapabilities(String namespace) + { + return BundleRevisionImpl.asCapabilityList(getCapabilities(namespace)); + } + + @Override + public List getCapabilities(String namespace) + { + if (isInUse()) + { + List result = m_resolvedCaps; + if (namespace != null) + { + result = new ArrayList(); + for (BundleCapability cap : m_resolvedCaps) + { + if (cap.getNamespace().equals(namespace)) + { + result.add(cap); + } + } + } + return result; + } + return null; + } + + @Override + public List getResourceRequirements(String namespace) + { + return BundleRevisionImpl.asRequirementList(getRequirements(namespace)); + } + + @Override + public List getRequirements(String namespace) + { + if (isInUse()) + { + List searchReqs = m_resolvedReqs; + List wovenReqs = m_wovenReqs; + List result = m_resolvedReqs; + + if (wovenReqs != null) + { + searchReqs = new ArrayList(m_resolvedReqs); + searchReqs.addAll(wovenReqs); + result = searchReqs; + } + + if (namespace != null) + { + result = new ArrayList(); + for (BundleRequirement req : searchReqs) + { + if (req.getNamespace().equals(namespace)) + { + result.add(req); + } + } + } + return result; + } + return null; + } + + public List getNativeLibraries() + { + return m_resolvedNativeLibs; + } + + private static List asWireList(List wires) + { + return wires; + } + + @Override + public List getProvidedResourceWires(String namespace) + { + return asWireList(getProvidedWires(namespace)); + } + + @Override + public List getProvidedWires(String namespace) + { + if (isInUse()) + { + return m_revision.getBundle() + .getFramework().getDependencies().getProvidedWires(m_revision, namespace); + } + return null; + } + + @Override + public List getRequiredResourceWires(String namespace) + { + return asWireList(getRequiredWires(namespace)); + } + + @Override + public List getRequiredWires(String namespace) + { + if (isInUse()) + { + List result = m_wires; + if (namespace != null) + { + result = new ArrayList(); + for (BundleWire bw : m_wires) + { + if (bw.getRequirement().getNamespace().equals(namespace)) + { + result.add(bw); + } + } + } + return result; + } + return null; + } + + public synchronized void addDynamicWire(BundleWire wire) + { + // Make new wires list. + List wires = new ArrayList(m_wires); + wires.add(wire); + if (wire.getCapability().getAttributes().get(BundleRevision.PACKAGE_NAMESPACE) != null) + { + // Make new imported package map. + Map importedPkgs = + new HashMap(m_importedPkgs); + importedPkgs.put( + (String) wire.getCapability().getAttributes().get(BundleRevision.PACKAGE_NAMESPACE), + wire.getProviderWiring().getRevision()); + + m_importedPkgs = importedPkgs; + } + // Update associated member values. + // Technically, there is a window here where readers won't see + // both values updates at the same time, but it seems unlikely + // to cause any issues. + m_wires = Util.newImmutableList(wires); + } + + @Override + public BundleRevision getResource() + { + return m_revision; + } + + @Override + public BundleRevision getRevision() + { + return m_revision; + } + + @Override + public ClassLoader getClassLoader() + { + if (m_isDisposed || Util.isFragment(m_revision)) + { + return null; + } + + return getClassLoaderInternal(); + } + + private ClassLoader getClassLoaderInternal() + { + ClassLoader classLoader = m_classLoader; + if (m_classLoader != null) + { + return classLoader; + } + else + { + return _getClassLoaderInternal(); + } + } + + private synchronized ClassLoader _getClassLoaderInternal() + { + // Only try to create the class loader if the bundle + // is not disposed. + if (!m_isDisposed && (m_classLoader == null)) + { + m_classLoader = BundleRevisionImpl.getSecureAction().run( + new PrivilegedAction() + { + @Override + public BundleClassLoader run() + { + return new BundleClassLoader(BundleWiringImpl.this, determineParentClassLoader(), m_logger); + } + } + ); + } + return m_classLoader; + } + + @Override + public List findEntries(String path, String filePattern, int options) + { + if (isInUse()) + { + if (!Util.isFragment(m_revision)) + { + Enumeration e = + m_revision.getBundle().getFramework() + .findBundleEntries(m_revision, path, filePattern, + (options & BundleWiring.FINDENTRIES_RECURSE) > 0); + List entries = new ArrayList(); + while ((e != null) && e.hasMoreElements()) + { + entries.add(e.nextElement()); + } + return Util.newImmutableList(entries); + } + return Collections.EMPTY_LIST; + } + return null; + } + + // Thread local to detect class loading cycles. + private final ThreadLocal m_listResourcesCycleCheck = new ThreadLocal(); + + @Override + public Collection listResources( + String path, String filePattern, int options) + { + // Implementation note: If you enable the DEBUG option for + // listResources() to print from where each resource comes, + // it will not give 100% accurate answers in the face of + // Require-Bundle cycles with overlapping content since + // the actual source will depend on who does the class load + // first. Further, normal class loaders cache class load + // results so it is always the same subsequently, but we + // don't do that here so it will always return a different + // result depending upon who is asking. Moral to the story: + // don't do cycles and certainly don't do them with + // overlapping content. + + Collection resources = null; + + // Normalize path. + if ((path.length() > 0) && (path.charAt(0) == '/')) + { + path = path.substring(1); + } + if ((path.length() > 0) && (path.charAt(path.length() - 1) != '/')) + { + path = path + '/'; + } + + // Parse the file filter. + filePattern = (filePattern == null) ? "*" : filePattern; + List pattern = SimpleFilter.parseSubstring(filePattern); + + // We build an internal collection of ResourceSources, since this + // allows us to print out additional debug information. + Collection sources = listResourcesInternal(path, pattern, options); + if (sources != null) + { + boolean debug = (options & LISTRESOURCES_DEBUG) > 0; + resources = new TreeSet(); + for (ResourceSource source : sources) + { + if (debug) + { + resources.add(source.toString()); + } + else + { + resources.add(source.m_resource); + } + } + } + return resources; + } + + private synchronized Collection listResourcesInternal( + String path, List pattern, int options) + { + if (isInUse()) + { + boolean recurse = (options & BundleWiring.LISTRESOURCES_RECURSE) > 0; + boolean localOnly = (options & BundleWiring.LISTRESOURCES_LOCAL) > 0; + + // Check for cycles, which can happen with Require-Bundle. + Set cycles = (Set) m_listResourcesCycleCheck.get(); + if (cycles == null) + { + cycles = new HashSet(); + m_listResourcesCycleCheck.set(cycles); + } + if (cycles.contains(path)) + { + return Collections.EMPTY_LIST; + } + cycles.add(path); + + try + { + // Calculate set of remote resources (i.e., those either + // imported or required). + Collection remoteResources = new TreeSet(); + // Imported packages cannot have merged content, so we need to + // keep track of these packages. + Set noMerging = new HashSet(); + // Loop through wires to compute remote resources. + for (BundleWire bw : m_wires) + { + if (bw.getCapability().getNamespace() + .equals(BundleRevision.PACKAGE_NAMESPACE)) + { + // For imported packages, we only need to calculate + // the remote resources of the specific imported package. + remoteResources.addAll( + calculateRemotePackageResources( + bw, bw.getCapability(), recurse, + path, pattern, noMerging)); + } + else if (bw.getCapability().getNamespace() + .equals(BundleRevision.BUNDLE_NAMESPACE)) + { + // For required bundles, all declared package capabilities + // from the required bundle will be available to requirers, + // so get the target required bundle's declared packages + // and handle them in a similar fashion to a normal import + // except that their content can be merged with local + // packages. + List exports = + bw.getProviderWiring().getRevision() + .getDeclaredCapabilities(BundleRevision.PACKAGE_NAMESPACE); + for (BundleCapability export : exports) + { + remoteResources.addAll( + calculateRemotePackageResources( + bw, export, recurse, path, pattern, null)); + } + + // Since required bundle may reexport bundles it requires, + // check its wires for this case. + List requiredBundles = + bw.getProviderWiring().getRequiredWires( + BundleRevision.BUNDLE_NAMESPACE); + for (BundleWire rbWire : requiredBundles) + { + String visibility = + rbWire.getRequirement().getDirectives() + .get(Constants.VISIBILITY_DIRECTIVE); + if ((visibility != null) + && (visibility.equals(Constants.VISIBILITY_REEXPORT))) + { + // For each reexported required bundle, treat them + // in a similar fashion as a normal required bundle + // by including all of their declared package + // capabilities in the requiring bundle's class + // space. + List reexports = + rbWire.getProviderWiring().getRevision() + .getDeclaredCapabilities(BundleRevision.PACKAGE_NAMESPACE); + for (BundleCapability reexport : reexports) + { + remoteResources.addAll( + calculateRemotePackageResources( + bw, reexport, recurse, path, pattern, null)); + } + } + } + } + } + + // Calculate set of local resources (i.e., those contained + // in the revision or its fragments). + Collection localResources = new TreeSet(); + // Get the revision's content path, which includes contents + // from fragments. + List contentPath = m_revision.getContentPath(); + for (Content content : contentPath) + { + Enumeration e = content.getEntries(); + if (e != null) + { + while (e.hasMoreElements()) + { + String resource = e.nextElement(); + String resourcePath = getTrailingPath(resource); + if (!noMerging.contains(resourcePath)) + { + if ((!recurse && resourcePath.equals(path)) + || (recurse && resourcePath.startsWith(path))) + { + if (matchesPattern(pattern, getPathHead(resource))) + { + localResources.add( + new ResourceSource(resource, m_revision)); + } + } + } + } + } + } + + if (localOnly) + { + return localResources; + } + else + { + remoteResources.addAll(localResources); + return remoteResources; + } + } + finally + { + cycles.remove(path); + if (cycles.isEmpty()) + { + m_listResourcesCycleCheck.set(null); + } + } + } + return null; + } + + private Collection calculateRemotePackageResources( + BundleWire bw, BundleCapability cap, boolean recurse, + String path, List pattern, Set noMerging) + { + Collection resources = Collections.EMPTY_SET; + + // Convert package name to a path. + String subpath = (String) cap.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE); + subpath = subpath.replace('.', '/') + '/'; + // If necessary, record that this package should not be merged + // with local content. + if (noMerging != null) + { + noMerging.add(subpath); + } + + // If we are not recuring, check for path equality or if + // we are recursing, check that the subpath starts with + // the target path. + if ((!recurse && subpath.equals(path)) + || (recurse && subpath.startsWith(path))) + { + // Delegate to the original provider wiring to have it calculate + // the list of resources in the package. In this case, we don't + // want to recurse since we want the precise package. + resources = + ((BundleWiringImpl) bw.getProviderWiring()).listResourcesInternal( + subpath, pattern, 0); + + // The delegatedResources result will include subpackages + // which need to be filtered out, since imported packages + // do not give access to subpackages. If a subpackage is + // imported, it will be added by its own wire. + for (Iterator it = resources.iterator(); + it.hasNext(); ) + { + ResourceSource reqResource = it.next(); + if (reqResource.m_resource.charAt( + reqResource.m_resource.length() - 1) == '/') + { + it.remove(); + } + } + } + // If we are not recursing, but the required package + // is a child of the desired path, then include its + // immediate child package. We do this so that it is + // possible to use listResources() to walk the resource + // tree similar to doing a directory walk one level + // at a time. + else if (!recurse && subpath.startsWith(path)) + { + int idx = subpath.indexOf('/', path.length()); + if (idx >= 0) + { + subpath = subpath.substring(0, idx + 1); + } + if (matchesPattern(pattern, getPathHead(subpath))) + { + resources = Collections.singleton( + new ResourceSource(subpath, bw.getProviderWiring().getRevision())); + } + } + + return resources; + } + + private static String getPathHead(String resource) + { + if (resource.length() == 0) + { + return resource; + } + int idx = (resource.charAt(resource.length() - 1) == '/') + ? resource.lastIndexOf('/', resource.length() - 2) + : resource.lastIndexOf('/'); + if (idx < 0) + { + return resource; + } + return resource.substring(idx + 1); + } + + private static String getTrailingPath(String resource) + { + if (resource.length() == 0) + { + return null; + } + int idx = (resource.charAt(resource.length() - 1) == '/') + ? resource.lastIndexOf('/', resource.length() - 2) + : resource.lastIndexOf('/'); + if (idx < 0) + { + return ""; + } + return resource.substring(0, idx + 1); + } + + private static boolean matchesPattern(List pattern, String resource) + { + if (resource.charAt(resource.length() - 1) == '/') + { + resource = resource.substring(0, resource.length() - 1); + } + return SimpleFilter.compareSubstring(pattern, resource); + } + + @Override + public BundleImpl getBundle() + { + return m_revision.getBundle(); + } + + // + // Class loader implementation methods. + // + + private URL createURL(int port, String path) + { + // Add a slash if there is one already, otherwise + // the is no slash separating the host from the file + // in the resulting URL. + if (!path.startsWith("/")) + { + path = "/" + path; + } + + try + { + return BundleRevisionImpl.getSecureAction().createURL(null, + FelixConstants.BUNDLE_URL_PROTOCOL + "://" + + getBundle().getFramework()._getProperty(Constants.FRAMEWORK_UUID) + "_" + m_revision.getId() + ":" + port + path, + getBundle().getFramework().getBundleStreamHandler()); + } + catch (MalformedURLException ex) + { + m_logger.log(m_revision.getBundle(), + Logger.LOG_ERROR, + "Unable to create resource URL.", + ex); + } + return null; + } + + public Enumeration getResourcesByDelegation(String name) + { + Set requestSet = (Set) m_cycleCheck.get(); + if (requestSet == null) + { + requestSet = new HashSet(); + m_cycleCheck.set(requestSet); + } + if (!requestSet.contains(name)) + { + requestSet.add(name); + try + { + return findResourcesByDelegation(name); + } + finally + { + requestSet.remove(name); + } + } + + return null; + } + + private Enumeration findResourcesByDelegation(String name) + { + Enumeration urls = null; + List completeUrlList = new ArrayList(); + + // Get the package of the target class/resource. + String pkgName = Util.getResourcePackage(name); + + // Delegate any packages listed in the boot delegation + // property to the parent class loader. + if (shouldBootDelegate(pkgName)) + { + try + { + // Get the appropriate class loader for delegation. + ClassLoader bdcl = getBootDelegationClassLoader(); + urls = bdcl.getResources(name); + } + catch (IOException ex) + { + // This shouldn't happen and even if it does, there + // is nothing we can do, so just ignore it. + } + // If this is a java.* package, then always terminate the + // search; otherwise, continue to look locally. + if (pkgName.startsWith("java.")) + { + return urls; + } + + completeUrlList.add(urls); + } + + // Look in the revisions's imported packages. If the package is + // imported, then we stop searching no matter the result since + // imported packages cannot be split. + BundleRevision provider = m_importedPkgs.get(pkgName); + if (provider != null) + { + // Delegate to the provider revision. + urls = ((BundleWiringImpl) provider.getWiring()).getResourcesByDelegation(name); + + // If we find any resources, then add them. + if ((urls != null) && (urls.hasMoreElements())) + { + completeUrlList.add(urls); + } + + // Always return here since imported packages cannot be split + // across required bundles or the revision's content. + return new CompoundEnumeration((Enumeration[]) + completeUrlList.toArray(new Enumeration[completeUrlList.size()])); + } + + // See whether we can get the resource from the required bundles and + // regardless of whether or not this is the case continue to the next + // step potentially passing on the result of this search (if any). + List providers = m_requiredPkgs.get(pkgName); + if (providers != null) + { + for (BundleRevision p : providers) + { + // Delegate to the provider revision. + urls = ((BundleWiringImpl) p.getWiring()).getResourcesByDelegation(name); + + // If we find any resources, then add them. + if ((urls != null) && (urls.hasMoreElements())) + { + completeUrlList.add(urls); + } + + // Do not return here, since required packages can be split + // across the revision's content. + } + } + + // Try the module's own class path. If we can find the resource then + // return it together with the results from the other searches else + // try to look into the dynamic imports. + urls = m_revision.getResourcesLocal(name); + if ((urls != null) && (urls.hasMoreElements())) + { + completeUrlList.add(urls); + } + else + { + // If not found, then try the module's dynamic imports. + // At this point, the module's imports were searched and so was the + // the module's content. Now we make an attempt to load the + // class/resource via a dynamic import, if possible. + try + { + provider = m_resolver.resolve(m_revision, pkgName); + } + catch (ResolutionException ex) + { + // Ignore this since it is likely normal. + } + catch (BundleException ex) + { + // Ignore this since it is likely the result of a resolver hook. + } + if (provider != null) + { + // Delegate to the provider revision. + urls = ((BundleWiringImpl) provider.getWiring()).getResourcesByDelegation(name); + + // If we find any resources, then add them. + if ((urls != null) && (urls.hasMoreElements())) + { + completeUrlList.add(urls); + } + } + } + + return new CompoundEnumeration((Enumeration[]) + completeUrlList.toArray(new Enumeration[completeUrlList.size()])); + } + + private ClassLoader determineParentClassLoader() + { + // Determine the class loader's parent based on the + // configuration property; use boot class loader by + // default. + String cfg = (String) m_configMap.get(Constants.FRAMEWORK_BUNDLE_PARENT); + cfg = (cfg == null) ? Constants.FRAMEWORK_BUNDLE_PARENT_BOOT : cfg; + final ClassLoader parent; + if (cfg.equalsIgnoreCase(Constants.FRAMEWORK_BUNDLE_PARENT_APP)) + { + parent = BundleRevisionImpl.getSecureAction().getSystemClassLoader(); + } + else if (cfg.equalsIgnoreCase(Constants.FRAMEWORK_BUNDLE_PARENT_EXT)) + { + parent = BundleRevisionImpl.getSecureAction().getParentClassLoader( + BundleRevisionImpl.getSecureAction().getSystemClassLoader()); + } + else if (cfg.equalsIgnoreCase(Constants.FRAMEWORK_BUNDLE_PARENT_FRAMEWORK)) + { + parent = BundleRevisionImpl.getSecureAction() + .getClassLoader(BundleRevisionImpl.class); + } + // On Android we cannot set the parent class loader to be null, so + // we special case that situation here and set it to the system + // class loader by default instead, which is not really spec. + else if (m_bootClassLoader == null) + { + parent = BundleRevisionImpl.getSecureAction().getSystemClassLoader(); + } + else + { + parent = null; + } + return parent; + } + + boolean shouldBootDelegate(String pkgName) + { + // Always boot delegate if the bundle has a configured + // boot class loader. + if (m_bootClassLoader != m_defBootClassLoader) + { + return true; + } + + boolean result = false; + + // Only consider delegation if we have a package name, since + // we don't want to promote the default package. The spec does + // not take a stand on this issue. + if (pkgName.length() > 0) + { + for (int i = 0; + !result + && (i < getBundle() + .getFramework().getBootPackages().length); + i++) + { + // Check if the boot package is wildcarded. + // A wildcarded boot package will be in the form "foo.", + // so a matching subpackage will start with "foo.", e.g., + // "foo.bar". + if (getBundle().getFramework().getBootPackageWildcards()[i] + && pkgName.startsWith( + getBundle().getFramework().getBootPackages()[i])) + { + return true; + } + // If not wildcarded, then check for an exact match. + else if (getBundle() + .getFramework().getBootPackages()[i].equals(pkgName)) + { + return true; + } + } + } + + return result; + } + + ClassLoader getBootDelegationClassLoader() + { + ClassLoader loader = m_classLoader; + // Get the appropriate class loader for delegation. + ClassLoader parent = (loader == null) ? + determineParentClassLoader() : + BundleRevisionImpl.getSecureAction().getParentClassLoader(loader); + + return (parent == null) ? m_bootClassLoader : parent; + } + + public Class getClassByDelegation(String name) throws ClassNotFoundException + { + // We do not call getClassLoader().loadClass() for arrays because + // it does not correctly handle array types, which is necessary in + // cases like deserialization using a wrapper class loader. + if ((name != null) && (name.length() > 0) && (name.charAt(0) == '[')) + { + return Class.forName(name, false, getClassLoader()); + } + + // Check to see if the requested class is filtered. + if (isFiltered(name)) + { + throw new ClassNotFoundException(name); + } + + ClassLoader cl = getClassLoaderInternal(); + if (cl == null) + { + throw new ClassNotFoundException( + "Unable to load class '" + + name + + "' because the bundle wiring for " + + m_revision.getSymbolicName() + + " is no longer valid."); + } + return cl.loadClass(name); + } + + private boolean isFiltered(String name) + { + String pkgName = Util.getClassPackage(name); + List> includeFilters = m_includedPkgFilters.get(pkgName); + List> excludeFilters = m_excludedPkgFilters.get(pkgName); + + if ((includeFilters == null) && (excludeFilters == null)) + { + return false; + } + + // Get the class name portion of the target class. + String className = Util.getClassName(name); + + // If there are no include filters then all classes are included + // by default, otherwise try to find one match. + boolean included = (includeFilters == null); + for (int i = 0; + (!included) && (includeFilters != null) && (i < includeFilters.size()); + i++) + { + included = SimpleFilter.compareSubstring(includeFilters.get(i), className); + } + + // If there are no exclude filters then no classes are excluded + // by default, otherwise try to find one match. + boolean excluded = false; + for (int i = 0; + (!excluded) && (excludeFilters != null) && (i < excludeFilters.size()); + i++) + { + excluded = SimpleFilter.compareSubstring(excludeFilters.get(i), className); + } + return !included || excluded; + } + + public URL getResourceByDelegation(String name) + { + try + { + return (URL) findClassOrResourceByDelegation(name, false); + } + catch (ClassNotFoundException ex) + { + // This should never be thrown because we are loading resources. + } + catch (ResourceNotFoundException ex) + { + m_logger.log(m_revision.getBundle(), + Logger.LOG_DEBUG, + ex.getMessage()); + } + return null; + } + + private Object findClassOrResourceByDelegation(String name, boolean isClass) + throws ClassNotFoundException, ResourceNotFoundException + { + Object result = null; + + Set requestSet = (Set) m_cycleCheck.get(); + if (requestSet == null) + { + requestSet = new HashSet(); + m_cycleCheck.set(requestSet); + } + if (requestSet.add(name)) + { + try + { + // Get the package of the target class/resource. + String pkgName = (isClass) ? Util.getClassPackage(name) : Util.getResourcePackage(name); + + boolean accessor = name.startsWith("sun.reflect.Generated") || name.startsWith("jdk.internal.reflect."); + + if (accessor) + { + if (m_accessorLookupCache == null) + { + m_accessorLookupCache = new ConcurrentHashMap(); + } + + ClassLoader loader = m_accessorLookupCache.get(name); + if (loader != null) + { + return loader.loadClass(name); + } + } + + // Delegate any packages listed in the boot delegation + // property to the parent class loader. + if (shouldBootDelegate(pkgName)) + { + try + { + // Get the appropriate class loader for delegation. + ClassLoader bdcl = getBootDelegationClassLoader(); + result = (isClass) ? (Object) bdcl.loadClass(name) : (Object) bdcl.getResource(name); + + // If this is a java.* package, then always terminate the + // search; otherwise, continue to look locally if not found. + if (pkgName.startsWith("java.") || (result != null)) + { + if (accessor) + { + m_accessorLookupCache.put(name, bdcl); + } + return result; + } + } + catch (ClassNotFoundException ex) + { + // If this is a java.* package, then always terminate the + // search; otherwise, continue to look locally if not found. + if (pkgName.startsWith("java.")) + { + throw ex; + } + } + } + + if (accessor) + { + List> allRevisions = new ArrayList>( 1 + m_requiredPkgs.size()); + allRevisions.add(m_importedPkgs.values()); + allRevisions.addAll(m_requiredPkgs.values()); + + for (Collection revisions : allRevisions) + { + for (BundleRevision revision : revisions) + { + ClassLoader loader = revision.getWiring().getClassLoader(); + if (loader != null && loader instanceof BundleClassLoader) + { + BundleClassLoader bundleClassLoader = (BundleClassLoader) loader; + result = bundleClassLoader.findLoadedClassInternal(name); + if (result != null) + { + m_accessorLookupCache.put(name, bundleClassLoader); + return result; + } + } + } + } + + try + { + result = tryImplicitBootDelegation(name, isClass); + } + catch (Exception ex) + { + // Ignore, will throw using CNFE_CLASS_LOADER + } + + if (result != null) + { + m_accessorLookupCache.put(name, BundleRevisionImpl.getSecureAction() + .getClassLoader(this.getClass())); + return result; + } + else + { + m_accessorLookupCache.put(name, CNFE_CLASS_LOADER); + CNFE_CLASS_LOADER.loadClass(name); + } + } + + // Look in the revision's imports. Note that the search may + // be aborted if this method throws an exception, otherwise + // it continues if a null is returned. + result = searchImports(pkgName, name, isClass); + + // If not found, try the revision's own class path. + if (result == null) + { + if (isClass) + { + ClassLoader cl = getClassLoaderInternal(); + if (cl == null) + { + throw new ClassNotFoundException( + "Unable to load class '" + + name + + "' because the bundle wiring for " + + m_revision.getSymbolicName() + + " is no longer valid."); + } + result = ((BundleClassLoader) cl).findClass(name); + } + else + { + result = m_revision.getResourceLocal(name); + } + + // If still not found, then try the revision's dynamic imports. + if (result == null) + { + result = searchDynamicImports(pkgName, name, isClass); + } + } + } + finally + { + requestSet.remove(name); + } + } + else + { + // If a cycle is detected, we should return null to break the + // cycle. This should only ever be return to internal class + // loading code and not to the actual instigator of the class load. + return null; + } + + if (result == null) + { + if (isClass) + { + throw new ClassNotFoundException( + name + " not found by " + this.getBundle()); + } + else + { + throw new ResourceNotFoundException( + name + " not found by " + this.getBundle()); + } + } + + return result; + } + + private Object searchImports(String pkgName, String name, boolean isClass) + throws ClassNotFoundException, ResourceNotFoundException + { + // Check if the package is imported. + BundleRevision provider = m_importedPkgs.get(pkgName); + if (provider != null) + { + // If we find the class or resource, then return it. + Object result = (isClass) + ? (Object) ((BundleWiringImpl) provider.getWiring()).getClassByDelegation(name) + : (Object) ((BundleWiringImpl) provider.getWiring()).getResourceByDelegation(name); + if (result != null) + { + return result; + } + + // If no class or resource was found, then we must throw an exception + // since the provider of this package did not contain the + // requested class and imported packages are atomic. + if (isClass) + { + throw new ClassNotFoundException(name); + } + throw new ResourceNotFoundException(name); + } + + // Check if the package is required. + List providers = m_requiredPkgs.get(pkgName); + if (providers != null) + { + for (BundleRevision p : providers) + { + // If we find the class or resource, then return it. + try + { + Object result = (isClass) + ? (Object) ((BundleWiringImpl) p.getWiring()).getClassByDelegation(name) + : (Object) ((BundleWiringImpl) p.getWiring()).getResourceByDelegation(name); + if (result != null) + { + return result; + } + } + catch (ClassNotFoundException ex) + { + // Since required packages can be split, don't throw an + // exception here if it is not found. Instead, we'll just + // continue searching other required bundles and the + // revision's local content. + } + } + } + + return null; + } + + private Object searchDynamicImports( + final String pkgName, final String name, final boolean isClass) + throws ClassNotFoundException, ResourceNotFoundException + { + // At this point, the module's imports were searched and so was the + // the module's content. Now we make an attempt to load the + // class/resource via a dynamic import, if possible. + BundleRevision provider = null; + try + { + provider = m_resolver.resolve(m_revision, pkgName); + } + catch (ResolutionException ex) + { + // Ignore this since it is likely normal. + } + catch (BundleException ex) + { + // Ignore this since it is likely the result of a resolver hook. + } + + // If the dynamic import was successful, then this initial + // time we must directly return the result from dynamically + // created package sources, but subsequent requests for + // classes/resources in the associated package will be + // processed as part of normal static imports. + if (provider != null) + { + // Return the class or resource. + return (isClass) + ? (Object) ((BundleWiringImpl) provider.getWiring()).getClassByDelegation(name) + : (Object) ((BundleWiringImpl) provider.getWiring()).getResourceByDelegation(name); + } + + return tryImplicitBootDelegation(name, isClass); + } + + private Object tryImplicitBootDelegation(final String name, final boolean isClass) + throws ClassNotFoundException, ResourceNotFoundException + { + // If implicit boot delegation is enabled, then try to guess whether + // we should boot delegate. + if (m_implicitBootDelegation) + { + // At this point, the class/resource could not be found by the bundle's + // static or dynamic imports, nor its own content. Before we throw + // an exception, we will try to determine if the instigator of the + // class/resource load was a class from a bundle or not. This is necessary + // because the specification mandates that classes on the class path + // should be hidden (except for java.*), but it does allow for these + // classes/resources to be exposed by the system bundle as an export. + // However, in some situations classes on the class path make the faulty + // assumption that they can access everything on the class path from + // every other class loader that they come in contact with. This is + // not true if the class loader in question is from a bundle. Thus, + // this code tries to detect that situation. If the class instigating + // the load request was NOT from a bundle, then we will make the + // assumption that the caller actually wanted to use the parent class + // loader and we will delegate to it. If the class was + // from a bundle, then we will enforce strict class loading rules + // for the bundle and throw an exception. + + // Get the class context to see the classes on the stack. + final Class[] classes = m_sm.getClassContext(); + try + { + if (System.getSecurityManager() != null) + { + return AccessController + .doPrivileged(new PrivilegedExceptionAction() + { + @Override + public Object run() throws Exception + { + return doImplicitBootDelegation(classes, name, + isClass); + } + }); + } + else + { + return doImplicitBootDelegation(classes, name, isClass); + } + } + catch (PrivilegedActionException ex) + { + Exception cause = ex.getException(); + if (cause instanceof ClassNotFoundException) + { + throw (ClassNotFoundException) cause; + } + else + { + throw (ResourceNotFoundException) cause; + } + } + } + return null; + } + + private Object doImplicitBootDelegation(Class[] classes, String name, boolean isClass) + throws ClassNotFoundException, ResourceNotFoundException + { + // Start from 1 to skip security manager class. + for (int i = 1; i < classes.length; i++) + { + // Find the first class on the call stack that is not from + // the class loader that loaded the Felix classes or is not + // a class loader or class itself, because we want to ignore + // calls to ClassLoader.loadClass() and Class.forName() since + // we are trying to find out who instigated the class load. + // Also ignore inner classes of class loaders, since we can + // assume they are a class loader too. + + // TODO: FRAMEWORK - This check is a hack and we should see if we can think + // of another way to do it, since it won't necessarily work in all situations. + // Since Felix uses threads for changing the start level + // and refreshing packages, it is possible that there are no + // bundle classes on the call stack; therefore, as soon as we + // see Thread on the call stack we exit this loop. Other cases + // where bundles actually use threads are not an issue because + // the bundle classes will be on the call stack before the + // Thread class. + if (Thread.class.equals(classes[i])) + { + break; + } + // Break if the current class came from a bundle, since we should + // not implicitly boot delegate in that case. + else if (isClassLoadedFromBundleRevision(classes[i])) + { + break; + } + // Break if this goes through BundleImpl because it must be a call + // to Bundle.loadClass() which should not implicitly boot delegate. + else if (BundleImpl.class.equals(classes[i])) + { + break; + } + // Break if this goes through ServiceRegistrationImpl.ServiceReferenceImpl + // because it must be a assignability check which should not implicitly boot delegate + else if (ServiceRegistrationImpl.ServiceReferenceImpl.class.equals(classes[i])) + { + break; + } + else if (isClassExternal(classes[i])) + { + try + { + // Return the class or resource from the parent class loader. + return (isClass) + ? (Object) BundleRevisionImpl.getSecureAction() + .getClassLoader(this.getClass()).loadClass(name) + : (Object) BundleRevisionImpl.getSecureAction() + .getClassLoader(this.getClass()).getResource(name); + } + catch (NoClassDefFoundError ex) + { + // Ignore, will return null + } + break; + } + } + + return null; + } + + private boolean isClassLoadedFromBundleRevision(Class clazz) + { + // The target class is loaded by a bundle class loader, + // then return true. + if (BundleClassLoader.class.isInstance( + BundleRevisionImpl.getSecureAction().getClassLoader(clazz))) + { + return true; + } + + // If the target class was loaded from a class loader that + // came from a bundle, then return true. + ClassLoader last = null; + for (ClassLoader cl = BundleRevisionImpl.getSecureAction().getClassLoader(clazz); + (cl != null) && (last != cl); + cl = BundleRevisionImpl.getSecureAction().getClassLoader(cl.getClass())) + { + last = cl; + if (BundleClassLoader.class.isInstance(cl)) + { + return true; + } + } + + return false; + } + + /** + * Tries to determine whether the given class is part of the framework or not. + * Framework classes include everything in org.apache.felix.framework.* and + * org.osgi.framework.*. We also consider ClassLoader and Class to be internal + * classes, because they are inserted into the stack trace as a result of + * method overloading. Typically, ClassLoader or Class will be mixed in + * between framework classes or will be at the point where the class loading + * request enters the framework class loading mechanism, which will then be + * followed by either bundle or external code, which will then exit our + * attempt to determine if we should boot delegate or not. Other standard + * class loaders, like URLClassLoader, are considered external classes and + * should trigger boot delegation. This means that bundles can create standard + * class loaders to get access to boot packages, but this is the standard + * behavior of class loaders. + * @param clazz the class to determine if it is external or not. + * @return true if the class is external, otherwise false. + */ + private boolean isClassExternal(Class clazz) + { + if (clazz.getName().startsWith("org.apache.felix.framework.")) + { + return false; + } + else if (clazz.getName().startsWith("org.osgi.framework.")) + { + return false; + } + else if (ClassLoader.class.equals(clazz)) + { + return false; + } + else if (Class.class.equals(clazz)) + { + return false; + } + return true; + } + + static class ToLocalUrlEnumeration implements Enumeration + { + final Enumeration m_enumeration; + + ToLocalUrlEnumeration(Enumeration enumeration) + { + m_enumeration = enumeration; + } + + @Override + public boolean hasMoreElements() + { + return m_enumeration.hasMoreElements(); + } + + @Override + public Object nextElement() + { + return convertToLocalUrl((URL) m_enumeration.nextElement()); + } + } + + public static class BundleClassLoader extends SecureClassLoader implements BundleReference + { + static final boolean m_isParallel; + + static + { + m_isParallel = registerAsParallel(); + } + + @IgnoreJRERequirement + private static boolean registerAsParallel() + { + boolean registered = false; + try + { + registered = ClassLoader.registerAsParallelCapable(); + } + catch (Throwable th) + { + // This is OK on older java versions + } + return registered; + } + + // Flag used to determine if a class has been loaded from this class + // loader or not. + private volatile boolean m_isActivationTriggered = false; + + private Object[][] m_cachedLibs = new Object[0][]; + private static final int LIBNAME_IDX = 0; + private static final int LIBPATH_IDX = 1; + private final ConcurrentHashMap m_classLocks = new ConcurrentHashMap(); + private final BundleWiringImpl m_wiring; + private final Logger m_logger; + + public BundleClassLoader(BundleWiringImpl wiring, ClassLoader parent, Logger logger) + { + super(parent); + m_wiring = wiring; + m_logger = logger; + } + + public boolean isActivationTriggered() + { + return m_isActivationTriggered; + } + + @Override + public BundleImpl getBundle() + { + return m_wiring.getBundle(); + } + + @Override + protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException + { + Class clazz = findLoadedClass(name); + + if (clazz == null) + { + try + { + clazz = (Class) m_wiring.findClassOrResourceByDelegation(name, true); + } + catch (ResourceNotFoundException ex) + { + // This should never happen since we are asking for a class, + // so just ignore it. + } + catch (ClassNotFoundException cnfe) + { + ClassNotFoundException ex = cnfe; + if (m_logger.getLogLevel() >= Logger.LOG_DEBUG) + { + String msg = diagnoseClassLoadError(m_wiring.m_resolver, m_wiring.m_revision, name); + ex = (msg != null) + ? new ClassNotFoundException(msg, cnfe) + : ex; + } + throw ex; + } + if (clazz == null) + { + // We detected a cycle + throw new ClassNotFoundException("Cycle detected while trying to load class: " + name); + } + } + + // Resolve the class and return it. + if (resolve) + { + resolveClass(clazz); + } + return clazz; + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException + { + Class clazz = findLoadedClass(name); + + // Search for class in bundle revision. + if (clazz == null) + { + // Do a quick check to try to avoid searching for classes on a + // disposed class loader, which will avoid some odd exception. + // This won't prevent all weird exception, since the wiring could + // still get disposed of after this check, but it will prevent + // some, perhaps. + if (m_wiring.m_isDisposed) + { + throw new ClassNotFoundException( + "Unable to load class '" + + name + + "' because the bundle wiring for " + + m_wiring.m_revision.getSymbolicName() + + " is no longer valid."); + } + + String actual = name.replace('.', '/') + ".class"; + + byte[] bytes = null; + + // Check the bundle class path. + List contentPath = m_wiring.m_revision.getContentPath(); + Content content = null; + for (int i = 0; + (bytes == null) && + (i < contentPath.size()); i++) + { + bytes = contentPath.get(i).getEntryAsBytes(actual); + content = contentPath.get(i); + } + + if (bytes != null) + { + // Get package name. + String pkgName = Util.getClassPackage(name); + + // Get weaving hooks and invoke them to give them a + // chance to weave the class' byte code before we + // define it. + // NOTE: We don't try to dynamically track hook addition + // or removal, we just get a snapshot and leave any changes + // as a race condition, doing any necessary clean up in + // the error handling. + Felix felix = m_wiring.m_revision.getBundle().getFramework(); + + Set> hooks = + felix.getHookRegistry().getHooks(WeavingHook.class); + + Set> wovenClassListeners = + felix.getHookRegistry().getHooks(WovenClassListener.class); + + WovenClassImpl wci = null; + if (!hooks.isEmpty()) + { + // Create woven class to be used for hooks. + wci = new WovenClassImpl(name, m_wiring, bytes); + try + { + transformClass(felix, wci, hooks, wovenClassListeners, + name, bytes); + } + catch (Error e) + { + // Mark the woven class as incomplete. + wci.complete(null, null, null); + wci.setState(WovenClass.TRANSFORMING_FAILED); + callWovenClassListeners(felix, wovenClassListeners, wci); + throw e; + } + } + + try + { + clazz = isParallel() ? defineClassParallel(name, felix, wovenClassListeners, wci, bytes, content, pkgName) : + defineClassNotParallel(name, felix, wovenClassListeners, wci, bytes, content, pkgName); + } + catch (ClassFormatError e) + { + if (wci != null) + { + wci.setState(WovenClass.DEFINE_FAILED); + callWovenClassListeners(felix, wovenClassListeners, wci); + } + throw e; + } + + // Perform deferred activation without holding the class loader lock, + // if the class we are returning is the instigating class. + List deferredList = (List) m_deferredActivation.get(); + if ((deferredList != null) + && (deferredList.size() > 0) + && ((Object[]) deferredList.get(0))[0].equals(name)) + { + // Null the deferred list. + m_deferredActivation.set(null); + while (!deferredList.isEmpty()) + { + // Lazy bundles should be activated in the reverse order + // of when they were added to the deferred list, so grab + // them from the end of the deferred list. + Object[] lazy = (Object[]) deferredList.remove(deferredList.size() - 1); + try + { + felix.getFramework().activateBundle((BundleImpl) (lazy)[1], true); + } + catch (Throwable ex) + { + m_logger.log((BundleImpl) (lazy)[1], + Logger.LOG_WARNING, + "Unable to lazily start bundle.", + ex); + } + } + } + } + } + + return clazz; + } + + Class defineClassParallel(String name, Felix felix, Set> wovenClassListeners, WovenClassImpl wci, byte[] bytes, + Content content, String pkgName) throws ClassFormatError + { + Class clazz = null; + + Thread me = Thread.currentThread(); + + while (clazz == null && m_classLocks.putIfAbsent(name, me) != me) + { + clazz = findLoadedClass(name); + } + + if (clazz == null) + { + try + { + clazz = findLoadedClass(name); + if (clazz == null) + { + clazz = defineClass(felix, wovenClassListeners, wci, name, + bytes, content, pkgName); + } + } + finally + { + m_classLocks.remove(name); + } + } + return clazz; + } + + Class defineClassNotParallel(String name, Felix felix, Set> wovenClassListeners, WovenClassImpl wci, byte[] bytes, + Content content, String pkgName) throws ClassFormatError + { + Class clazz = findLoadedClass(name); + + if (clazz == null) + { + synchronized (m_classLocks) + { + clazz = findLoadedClass(name); + if (clazz == null) + { + clazz = defineClass(felix, wovenClassListeners, wci, name, + bytes, content, pkgName); + } + } + } + return clazz; + } + + Class defineClass(Felix felix, + Set> wovenClassListeners, + WovenClassImpl wci, String name, byte[] bytes, Content content, String pkgName) + throws ClassFormatError + { + // If we have a woven class then get the class bytes from + // it since they may have changed. + // NOTE: We are taking a snapshot of these values and + // are not preventing a malbehaving weaving hook from + // modifying them after the fact. The price of preventing + // this isn't worth it, since they can already wreck + // havoc via weaving anyway. However, we do pass the + // snapshot values into the woven class when we mark it + // as complete so that it will refect the actual values + // we used to define the class. + if (wci != null) + { + bytes = wci._getBytes(); + List wovenImports = wci.getDynamicImportsInternal(); + + // Try to add any woven dynamic imports, since they + // could potentially be needed when defining the class. + List allWovenReqs = + new ArrayList(); + for (String s : wovenImports) + { + try + { + List wovenReqs = + ManifestParser.parseDynamicImportHeader( + m_logger, m_wiring.m_revision, s); + allWovenReqs.addAll(wovenReqs); + } + catch (BundleException ex) + { + // There should be no exception here + // since we checked syntax before adding + // dynamic import strings to list. + } + } + // Add the dynamic requirements. + if (!allWovenReqs.isEmpty()) + { + // Check for duplicate woven imports. + // First grab existing woven imports, if any. + Set filters = new HashSet(); + if (m_wiring.m_wovenReqs != null) + { + for (BundleRequirement req : m_wiring.m_wovenReqs) + { + filters.add( + ((BundleRequirementImpl) req) + .getFilter().toString()); + } + } + // Then check new woven imports for duplicates + // against existing and self. + int idx = allWovenReqs.size(); + while (idx < allWovenReqs.size()) + { + BundleRequirement wovenReq = allWovenReqs.get(idx); + String filter = ((BundleRequirementImpl) + wovenReq).getFilter().toString(); + if (!filters.contains(filter)) + { + filters.add(filter); + idx++; + } + else + { + allWovenReqs.remove(idx); + } + } + // Merge existing with new imports, if any. + if (!allWovenReqs.isEmpty()) + { + if (m_wiring.m_wovenReqs != null) + { + allWovenReqs.addAll(0, m_wiring.m_wovenReqs); + } + m_wiring.m_wovenReqs = allWovenReqs; + } + } + } + + int activationPolicy = + getBundle().isDeclaredActivationPolicyUsed() + ? getBundle() + .adapt(BundleRevisionImpl.class).getDeclaredActivationPolicy() + : EAGER_ACTIVATION; + + // If the revision is using deferred activation, then if + // we load this class from this revision we need to activate + // the bundle before returning the class. We will short + // circuit the trigger matching if the trigger is already + // tripped. + boolean isTriggerClass = m_isActivationTriggered + ? false : m_wiring.m_revision.isActivationTrigger(pkgName); + + if (!m_isActivationTriggered + && isTriggerClass + && (activationPolicy == BundleRevisionImpl.LAZY_ACTIVATION) + && (getBundle().getState() == Bundle.STARTING)) + { + List deferredList = (List) m_deferredActivation.get(); + if (deferredList == null) + { + deferredList = new ArrayList(); + m_deferredActivation.set(deferredList); + } + deferredList.add(new Object[]{name, getBundle()}); + } + // We need to try to define a Package object for the class + // before we call defineClass() if we haven't already + // created it. + if (pkgName.length() > 0) + { + if (getPackage(pkgName) == null) + { + Object[] params = definePackage(pkgName); + + // This is a harmless check-then-act situation, + // where threads might be racing to create different + // classes in the same package, so catch and ignore + // any IAEs that may occur. + try + { + definePackage( + pkgName, + (String) params[0], + (String) params[1], + (String) params[2], + (String) params[3], + (String) params[4], + (String) params[5], + null); + } + catch (IllegalArgumentException ex) + { + // Ignore. + } + } + } + + Class clazz = null; + // If we have a security context, then use it to + // define the class with it for security purposes, + // otherwise define the class without a protection domain. + if (m_wiring.m_revision.getProtectionDomain() != null) + { + clazz = defineClass(name, bytes, 0, bytes.length, + m_wiring.m_revision.getProtectionDomain()); + } + else + { + clazz = defineClass(name, bytes, 0, bytes.length); + } + if (wci != null) + { + wci.completeDefine(clazz); + wci.setState(WovenClass.DEFINED); + callWovenClassListeners(felix, wovenClassListeners, wci); + } + + // At this point if we have a trigger class, then the deferred + // activation trigger has tripped. + if (!m_isActivationTriggered && isTriggerClass && (clazz != null)) + { + m_isActivationTriggered = true; + } + + return clazz; + } + + void transformClass(Felix felix, WovenClassImpl wci, + Set> hooks, + Set> wovenClassListeners, + String name, byte[] bytes) throws Error { + + // Loop through hooks in service ranking order. + for (ServiceReference sr : hooks) + { + // Only use the hook if it is not black listed. + if (!felix.getHookRegistry().isHookBlackListed(sr)) + { + // Get the hook service object. + // Note that we don't use the bundle context + // to get the service object since that would + // perform sercurity checks. + WeavingHook wh = felix.getService(felix, sr, false); + if (wh != null) + { + try + { + BundleRevisionImpl.getSecureAction() + .invokeWeavingHook(wh, wci); + } + catch (Throwable th) + { + if (!(th instanceof WeavingException)) + { + felix.getHookRegistry().blackListHook(sr); + } + felix.fireFrameworkEvent( + FrameworkEvent.ERROR, + sr.getBundle(), + th); + + // Throw class format exception per spec. + Error error = new ClassFormatError("Weaving hook failed."); + error.initCause(th); + throw error; + } + finally + { + felix.ungetService(felix, sr, null); + } + } + } + } + wci.setState(WovenClass.TRANSFORMED); + callWovenClassListeners(felix, wovenClassListeners, wci); + } + + protected void callWovenClassListeners(Felix felix, Set> wovenClassListeners, WovenClass wovenClass) + { + if(wovenClassListeners != null) + { + for(ServiceReference currentWovenClassListenerRef : wovenClassListeners) + { + WovenClassListener currentWovenClassListner = felix.getService(felix, currentWovenClassListenerRef, false); + try + { + BundleRevisionImpl.getSecureAction().invokeWovenClassListener(currentWovenClassListner, wovenClass); + } + catch (Exception e) + { + m_logger.log(Logger.LOG_ERROR, "Woven Class Listner failed.", e); + } + finally + { + felix.ungetService(felix, currentWovenClassListenerRef, null); + } + } + } + } + + private Object[] definePackage(String pkgName) + { + String spectitle = (String) m_wiring.m_revision.getHeaders().get("Specification-Title"); + String specversion = (String) m_wiring.m_revision.getHeaders().get("Specification-Version"); + String specvendor = (String) m_wiring.m_revision.getHeaders().get("Specification-Vendor"); + String impltitle = (String) m_wiring.m_revision.getHeaders().get("Implementation-Title"); + String implversion = (String) m_wiring.m_revision.getHeaders().get("Implementation-Version"); + String implvendor = (String) m_wiring.m_revision.getHeaders().get("Implementation-Vendor"); + if ((spectitle != null) + || (specversion != null) + || (specvendor != null) + || (impltitle != null) + || (implversion != null) + || (implvendor != null)) + { + return new Object[] { + spectitle, specversion, specvendor, impltitle, implversion, implvendor + }; + } + return new Object[] {null, null, null, null, null, null}; + } + + @Override + public URL getResource(String name) + { + URL url = m_wiring.getResourceByDelegation(name); + if (m_wiring.m_useLocalURLs) + { + url = convertToLocalUrl(url); + } + return url; + } + + @Override + protected URL findResource(String name) + { + return m_wiring.m_revision.getResourceLocal(name); + } + + @Override + protected Enumeration findResources(String name) + { + return m_wiring.m_revision.getResourcesLocal(name); + } + + @Override + protected String findLibrary(String name) + { + // Remove leading slash, if present. + if (name.startsWith("/")) + { + name = name.substring(1); + } + + String result = null; + // CONCURRENCY: In the long run, we might want to break this + // sync block in two to avoid manipulating the cache while + // holding the lock, but for now we will do it the simple way. + synchronized (this) + { + // Check to make sure we haven't already found this library. + for (int i = 0; (result == null) && (i < m_cachedLibs.length); i++) + { + if (m_cachedLibs[i][LIBNAME_IDX].equals(name)) + { + result = (String) m_cachedLibs[i][LIBPATH_IDX]; + } + } + + // If we don't have a cached result, see if we have a matching + // native library. + if (result == null) + { + List libs = m_wiring.getNativeLibraries(); + for (int libIdx = 0; (libs != null) && (libIdx < libs.size()); libIdx++) + { + if (libs.get(libIdx).match(m_wiring.m_configMap, name)) + { + // Search bundle content first for native library. + result = m_wiring.m_revision.getContent().getEntryAsNativeLibrary( + libs.get(libIdx).getEntryName()); + // If not found, then search fragments in order. + for (int i = 0; + (result == null) && (m_wiring.m_fragmentContents != null) + && (i < m_wiring.m_fragmentContents.size()); + i++) + { + result = m_wiring.m_fragmentContents.get(i).getEntryAsNativeLibrary( + libs.get(libIdx).getEntryName()); + } + } + } + + // Remember the result for future requests. + if (result != null) + { + Object[][] tmp = new Object[m_cachedLibs.length + 1][]; + System.arraycopy(m_cachedLibs, 0, tmp, 0, m_cachedLibs.length); + tmp[m_cachedLibs.length] = new Object[] { name, result }; + m_cachedLibs = tmp; + } + } + } + + return result; + } + + protected boolean isParallel() + { + return m_isParallel; + } + + @Override + public Enumeration getResources(String name) + { + Enumeration urls = m_wiring.getResourcesByDelegation(name); + if (m_wiring.m_useLocalURLs) + { + urls = new ToLocalUrlEnumeration(urls); + } + return urls; + } + + @Override + public String toString() + { + return m_wiring.toString(); + } + + Class findLoadedClassInternal(String name) + { + return super.findLoadedClass(name); + } + } + + static URL convertToLocalUrl(URL url) + { + if (url.getProtocol().equals("bundle")) + { + try + { + url = ((URLHandlersBundleURLConnection) + url.openConnection()).getLocalURL(); + } + catch (IOException ex) + { + // Ignore and add original url. + } + } + return url; + } + + private static class ResourceSource implements Comparable + { + public final String m_resource; + public final BundleRevision m_revision; + + public ResourceSource(String resource, BundleRevision revision) + { + m_resource = resource; + m_revision = revision; + } + + @Override + public boolean equals(Object o) + { + if (o instanceof ResourceSource) + { + return m_resource.equals(((ResourceSource) o).m_resource); + } + return false; + } + + @Override + public int hashCode() + { + return m_resource.hashCode(); + } + + @Override + public int compareTo(ResourceSource t) + { + return m_resource.compareTo(t.m_resource); + } + + @Override + public String toString() + { + return m_resource + + " -> " + + m_revision.getSymbolicName() + + " [" + m_revision + "]"; + } + } + + private static String diagnoseClassLoadError( + StatefulResolver resolver, BundleRevision revision, String name) + { + // We will try to do some diagnostics here to help the developer + // deal with this exception. + + // Get package name. + String pkgName = Util.getClassPackage(name); + if (pkgName.length() == 0) + { + return null; + } + + // First, get the bundle string of the revision doing the class loader. + String importer = revision.getBundle().toString(); + + // Next, check to see if the revision imports the package. + List wires = (revision.getWiring() == null) + ? null : revision.getWiring().getProvidedWires(null); + for (int i = 0; (wires != null) && (i < wires.size()); i++) + { + if (wires.get(i).getCapability().getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE) && + wires.get(i).getCapability().getAttributes().get(BundleRevision.PACKAGE_NAMESPACE).equals(pkgName)) + { + String exporter = wires.get(i).getProviderWiring().getBundle().toString(); + + StringBuilder sb = new StringBuilder("*** Package '"); + sb.append(pkgName); + sb.append("' is imported by bundle "); + sb.append(importer); + sb.append(" from bundle "); + sb.append(exporter); + sb.append(", but the exported package from bundle "); + sb.append(exporter); + sb.append(" does not contain the requested class '"); + sb.append(name); + sb.append("'. Please verify that the class name is correct in the importing bundle "); + sb.append(importer); + sb.append(" and/or that the exported package is correctly bundled in "); + sb.append(exporter); + sb.append(". ***"); + + return sb.toString(); + } + } + + // Next, check to see if the package was optionally imported and + // whether or not there is an exporter available. + List reqs = revision.getWiring().getRequirements(null); + /* + * TODO: RB - Fix diagnostic message for optional imports. + for (int i = 0; (reqs != null) && (i < reqs.length); i++) + { + if (reqs[i].getName().equals(pkgName) && reqs[i].isOptional()) + { + // Try to see if there is an exporter available. + IModule[] exporters = getResolvedExporters(reqs[i], true); + exporters = (exporters.length == 0) + ? getUnresolvedExporters(reqs[i], true) : exporters; + + // An exporter might be available, but it may have attributes + // that do not match the importer's required attributes, so + // check that case by simply looking for an exporter of the + // desired package without any attributes. + if (exporters.length == 0) + { + IRequirement pkgReq = new Requirement( + ICapability.PACKAGE_NAMESPACE, "(package=" + pkgName + ")"); + exporters = getResolvedExporters(pkgReq, true); + exporters = (exporters.length == 0) + ? getUnresolvedExporters(pkgReq, true) : exporters; + } + + long expId = (exporters.length == 0) + ? -1 : Util.getBundleIdFromModuleId(exporters[0].getId()); + + StringBuilder sb = new StringBuilder("*** Class '"); + sb.append(name); + sb.append("' was not found, but this is likely normal since package '"); + sb.append(pkgName); + sb.append("' is optionally imported by bundle "); + sb.append(impId); + sb.append("."); + if (exporters.length > 0) + { + sb.append(" However, bundle "); + sb.append(expId); + if (reqs[i].isSatisfied( + Util.getExportPackage(exporters[0], reqs[i].getName()))) + { + sb.append(" does export this package. Bundle "); + sb.append(expId); + sb.append(" must be installed before bundle "); + sb.append(impId); + sb.append(" is resolved or else the optional import will be ignored."); + } + else + { + sb.append(" does export this package with attributes that do not match."); + } + } + sb.append(" ***"); + + return sb.toString(); + } + } + */ + // Next, check to see if the package is dynamically imported by the revision. + if (resolver.isAllowedDynamicImport(revision, pkgName)) + { + // Try to see if there is an exporter available. + Map dirs = Collections.EMPTY_MAP; + Map attrs = Collections.singletonMap( + BundleRevision.PACKAGE_NAMESPACE, (Object) pkgName); + BundleRequirementImpl req = new BundleRequirementImpl( + revision, BundleRevision.PACKAGE_NAMESPACE, dirs, attrs); + List exporters = resolver.findProviders(req, false); + + BundleRevision provider = null; + try + { + provider = resolver.resolve(revision, pkgName); + } + catch (Exception ex) + { + provider = null; + } + + String exporter = (exporters.isEmpty()) + ? null : exporters.iterator().next().toString(); + + StringBuilder sb = new StringBuilder("*** Class '"); + sb.append(name); + sb.append("' was not found, but this is likely normal since package '"); + sb.append(pkgName); + sb.append("' is dynamically imported by bundle "); + sb.append(importer); + sb.append("."); + if ((exporters.size() > 0) && (provider == null)) + { + sb.append(" However, bundle "); + sb.append(exporter); + sb.append(" does export this package with attributes that do not match."); + } + sb.append(" ***"); + + return sb.toString(); + } + + // Next, check to see if there are any exporters for the package at all. + Map dirs = Collections.EMPTY_MAP; + Map attrs = Collections.singletonMap( + BundleRevision.PACKAGE_NAMESPACE, (Object) pkgName); + BundleRequirementImpl req = new BundleRequirementImpl( + revision, BundleRevision.PACKAGE_NAMESPACE, dirs, attrs); + List exports = resolver.findProviders(req, false); + if (exports.size() > 0) + { + boolean classpath = false; + try + { + BundleRevisionImpl.getSecureAction() + .getClassLoader(BundleClassLoader.class).loadClass(name); + classpath = true; + } + catch (NoClassDefFoundError err) + { + // Ignore + } + catch (Exception ex) + { + // Ignore + } + + String exporter = exports.iterator().next().toString(); + + StringBuilder sb = new StringBuilder("*** Class '"); + sb.append(name); + sb.append("' was not found because bundle "); + sb.append(importer); + sb.append(" does not import '"); + sb.append(pkgName); + sb.append("' even though bundle "); + sb.append(exporter); + sb.append(" does export it."); + if (classpath) + { + sb.append(" Additionally, the class is also available from the system class loader. There are two fixes: 1) Add an import for '"); + sb.append(pkgName); + sb.append("' to bundle "); + sb.append(importer); + sb.append("; imports are necessary for each class directly touched by bundle code or indirectly touched, such as super classes if their methods are used. "); + sb.append("2) Add package '"); + sb.append(pkgName); + sb.append("' to the '"); + sb.append(Constants.FRAMEWORK_BOOTDELEGATION); + sb.append("' property; a library or VM bug can cause classes to be loaded by the wrong class loader. The first approach is preferable for preserving modularity."); + } + else + { + sb.append(" To resolve this issue, add an import for '"); + sb.append(pkgName); + sb.append("' to bundle "); + sb.append(importer); + sb.append("."); + } + sb.append(" ***"); + + return sb.toString(); + } + + // Next, try to see if the class is available from the system + // class loader. + try + { + BundleRevisionImpl.getSecureAction() + .getClassLoader(BundleClassLoader.class).loadClass(name); + + StringBuilder sb = new StringBuilder("*** Package '"); + sb.append(pkgName); + sb.append("' is not imported by bundle "); + sb.append(importer); + sb.append(", nor is there any bundle that exports package '"); + sb.append(pkgName); + sb.append("'. However, the class '"); + sb.append(name); + sb.append("' is available from the system class loader. There are two fixes: 1) Add package '"); + sb.append(pkgName); + sb.append("' to the '"); + sb.append(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA); + sb.append("' property and modify bundle "); + sb.append(importer); + sb.append(" to import this package; this causes the system bundle to export class path packages. 2) Add package '"); + sb.append(pkgName); + sb.append("' to the '"); + sb.append(Constants.FRAMEWORK_BOOTDELEGATION); + sb.append("' property; a library or VM bug can cause classes to be loaded by the wrong class loader. The first approach is preferable for preserving modularity."); + sb.append(" ***"); + + return sb.toString(); + } + catch (Exception ex2) + { + } + + // Finally, if there are no imports or exports for the package + // and it is not available on the system class path, simply + // log a message saying so. + StringBuilder sb = new StringBuilder("*** Class '"); + sb.append(name); + sb.append("' was not found. Bundle "); + sb.append(importer); + sb.append(" does not import package '"); + sb.append(pkgName); + sb.append("', nor is the package exported by any other bundle or available from the system class loader."); + sb.append(" ***"); + + return sb.toString(); + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/DTOFactory.java b/framework/src/main/java/org/apache/felix/framework/DTOFactory.java new file mode 100644 index 00000000000..0abcf9c2a79 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/DTOFactory.java @@ -0,0 +1,573 @@ +/* + * 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.framework; + +import org.osgi.dto.DTO; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.Version; +import org.osgi.framework.dto.BundleDTO; +import org.osgi.framework.dto.FrameworkDTO; +import org.osgi.framework.dto.ServiceReferenceDTO; +import org.osgi.framework.launch.Framework; +import org.osgi.framework.startlevel.BundleStartLevel; +import org.osgi.framework.startlevel.FrameworkStartLevel; +import org.osgi.framework.startlevel.dto.BundleStartLevelDTO; +import org.osgi.framework.startlevel.dto.FrameworkStartLevelDTO; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleRevisions; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.framework.wiring.dto.BundleRevisionDTO; +import org.osgi.framework.wiring.dto.BundleWireDTO; +import org.osgi.framework.wiring.dto.BundleWiringDTO; +import org.osgi.framework.wiring.dto.BundleWiringDTO.NodeDTO; +import org.osgi.framework.wiring.dto.FrameworkWiringDTO; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; +import org.osgi.resource.Wire; +import org.osgi.resource.Wiring; +import org.osgi.resource.dto.CapabilityDTO; +import org.osgi.resource.dto.CapabilityRefDTO; +import org.osgi.resource.dto.RequirementDTO; +import org.osgi.resource.dto.RequirementRefDTO; +import org.osgi.resource.dto.WireDTO; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Creates various DTOs provided by the core framework. + */ +public class DTOFactory +{ + private DTOFactory() + { + // Only static methods + } + + static T createDTO(Bundle bundle, Class type) + { + if (Bundle.UNINSTALLED == bundle.getState()) + return null; + + if (type == BundleDTO.class) + { + return type.cast(createBundleDTO(bundle)); + } + else if (type == BundleStartLevelDTO.class) + { + return type.cast(createBundleStartLevelDTO(bundle)); + } + else if (type == BundleRevisionDTO.class) + { + return type.cast(createBundleRevisionDTO(bundle)); + } + else if (type == BundleRevisionDTO[].class) + { + return type.cast(createBundleRevisionDTOArray(bundle)); + } + else if (type == BundleWiringDTO.class) + { + return type.cast(createBundleWiringDTO(bundle)); + } + else if (type == BundleWiringDTO[].class) + { + return type.cast(createBundleWiringDTOArray(bundle)); + } + else if (type == ServiceReferenceDTO[].class) + { + return type.cast(createServiceReferenceDTOArray(bundle)); + } + else if (type == FrameworkDTO.class && bundle instanceof Felix) + { + return type.cast(createFrameworkDTO((Felix) bundle)); + } + else if (type == FrameworkStartLevelDTO.class && bundle instanceof Framework) + { + return type.cast(createFrameworkStartLevelDTO((Framework) bundle)); + } + else if (type == FrameworkWiringDTO.class && bundle instanceof Felix) + { + return type.cast(createFrameworkWiringDTO((Felix) bundle)); + } + return null; + } + + private static BundleDTO createBundleDTO(Bundle bundle) + { + BundleDTO dto = new BundleDTO(); + dto.id = bundle.getBundleId(); + dto.lastModified = bundle.getLastModified(); + dto.state = bundle.getState(); + dto.symbolicName = bundle.getSymbolicName(); + dto.version = "" + bundle.getVersion(); + return dto; + } + + private static BundleRevisionDTO createBundleRevisionDTO(Bundle bundle) + { + BundleRevision br = bundle.adapt(BundleRevision.class); + if (!(br instanceof BundleRevisionImpl)) + return null; + + return createBundleRevisionDTO(bundle, (BundleRevisionImpl) br, new HashSet()); + } + + private static BundleRevisionDTO[] createBundleRevisionDTOArray(Bundle bundle) + { + BundleRevisions brs = bundle.adapt(BundleRevisions.class); + if (brs == null || brs.getRevisions() == null) + return null; + + List revisions = brs.getRevisions(); + BundleRevisionDTO[] dtos = new BundleRevisionDTO[revisions.size()]; + for (int i=0; i < revisions.size(); i++) + { + if (revisions.get(i) instanceof BundleRevisionImpl) + dtos[i] = createBundleRevisionDTO(bundle, (BundleRevisionImpl) revisions.get(i), new HashSet()); + } + return dtos; + } + + private static BundleRevisionDTO createBundleRevisionDTO(BundleRevision revision, Set resources) + { + if (revision instanceof BundleRevisionImpl) + return createBundleRevisionDTO(revision.getBundle(), (BundleRevisionImpl) revision, resources); + else + return null; + } + + private static BundleRevisionDTO createBundleRevisionDTO(Bundle bundle, BundleRevisionImpl revision, Set resources) + { + BundleRevisionDTO dto = new BundleRevisionDTO(); + dto.id = getRevisionID(revision); + addBundleRevisionDTO(dto, resources); + + dto.bundle = bundle.getBundleId(); + dto.symbolicName = revision.getSymbolicName(); + dto.type = revision.getTypes(); + dto.version = revision.getVersion().toString(); + + dto.capabilities = new ArrayList(); + for (Capability cap : revision.getCapabilities(null)) + { + CapabilityDTO cdto = new CapabilityDTO(); + cdto.id = getCapabilityID(cap); + cdto.namespace = cap.getNamespace(); + cdto.attributes = convertAttrsToDTO(cap.getAttributes()); + cdto.directives = new HashMap(cap.getDirectives()); + cdto.resource = getResourceIDAndAdd(cap.getResource(), resources); + + dto.capabilities.add(cdto); + } + + dto.requirements = new ArrayList(); + for (Requirement req : revision.getRequirements(null)) + { + RequirementDTO rdto = new RequirementDTO(); + rdto.id = getRequirementID(req); + rdto.namespace = req.getNamespace(); + rdto.attributes = convertAttrsToDTO(req.getAttributes()); + rdto.directives = new HashMap(req.getDirectives()); + rdto.resource = getResourceIDAndAdd(req.getResource(), resources); + + dto.requirements.add(rdto); + } + return dto; + } + + private static BundleWiringDTO createBundleWiringDTO(Bundle bundle) + { + BundleWiring bw = bundle.adapt(BundleWiring.class); + return bw != null ? createBundleWiringDTO(bw) : null; + } + + private static BundleWiringDTO createBundleWiringDTO(BundleWiring wiring) + { + BundleWiringDTO dto = new BundleWiringDTO(); + dto.bundle = wiring.getBundle().getBundleId(); + dto.root = getWiringID(wiring); + dto.nodes = new HashSet(); + dto.resources = new HashSet(); + + createBundleRevisionDTO(wiring.getRevision(), dto.resources); + createBundleWiringNodeDTO(wiring, dto.resources, dto.nodes); + + return dto; + } + + private static BundleWiringDTO[] createBundleWiringDTOArray(Bundle bundle) + { + BundleRevisions brs = bundle.adapt(BundleRevisions.class); + if (brs == null || brs.getRevisions() == null) + return null; + + List revisions = brs.getRevisions(); + BundleWiringDTO[] dtos = new BundleWiringDTO[revisions.size()]; + for (int i=0; i < revisions.size(); i++) + { + BundleWiring wiring = revisions.get(i).getWiring(); + dtos[i] = createBundleWiringDTO(wiring); + } + return dtos; + } + + private static void createBundleWiringNodeDTO(BundleWiring bw, Set resources, Set nodes) + { + NodeDTO node = new BundleWiringDTO.NodeDTO(); + node.id = getWiringID(bw); + + addNodeDTO(node, nodes); + + node.current = bw.isCurrent(); + node.inUse = bw.isInUse(); + node.resource = getResourceIDAndAdd(bw.getResource(), resources); + + node.capabilities = new ArrayList(); + for (Capability cap : bw.getCapabilities(null)) + { + CapabilityRefDTO cdto = new CapabilityRefDTO(); + cdto.capability = getCapabilityID(cap); + cdto.resource = getResourceIDAndAdd(cap.getResource(), resources); + node.capabilities.add(cdto); + } + + node.requirements = new ArrayList(); + for (Requirement req : bw.getRequirements(null)) + { + RequirementRefDTO rdto = new RequirementRefDTO(); + rdto.requirement = getRequirementID(req); + rdto.resource = getResourceIDAndAdd(req.getResource(), resources); + node.requirements.add(rdto); + } + + node.providedWires = new ArrayList(); + for (Wire pw : bw.getProvidedWires(null)) + { + node.providedWires.add(createBundleWireDTO(pw, resources, nodes)); + } + + node.requiredWires = new ArrayList(); + for (Wire rw : bw.getRequiredWires(null)) + { + node.requiredWires.add(createBundleWireDTO(rw, resources, nodes)); + } + } + + private static BundleWireDTO createBundleWireDTO(Wire wire, Set resources, Set nodes) + { + BundleWireDTO wdto = new BundleWireDTO(); + if (wire instanceof BundleWire) + { + BundleWire w = (BundleWire) wire; + + BundleWiring pw = w.getProviderWiring(); + addWiringNodeIfNotPresent(pw, resources, nodes); + wdto.providerWiring = getWiringID(pw); + + BundleWiring rw = w.getRequirerWiring(); + addWiringNodeIfNotPresent(rw, resources, nodes); + wdto.requirerWiring = getWiringID(rw); + } + wdto.provider = getResourceIDAndAdd(wire.getProvider(), resources); + wdto.requirer = getResourceIDAndAdd(wire.getRequirer(), resources); + wdto.capability = new CapabilityRefDTO(); + wdto.capability.capability = getCapabilityID(wire.getCapability()); + wdto.capability.resource = getResourceIDAndAdd(wire.getCapability().getResource(), resources); + wdto.requirement = new RequirementRefDTO(); + wdto.requirement.requirement = getRequirementID(wire.getRequirement()); + wdto.requirement.resource = getResourceIDAndAdd(wire.getRequirement().getResource(), resources); + return wdto; + } + + private static BundleStartLevelDTO createBundleStartLevelDTO(Bundle bundle) + { + BundleStartLevelDTO dto = new BundleStartLevelDTO(); + dto.bundle = bundle.getBundleId(); + + BundleStartLevel sl = bundle.adapt(BundleStartLevel.class); + dto.activationPolicyUsed = sl.isActivationPolicyUsed(); + dto.persistentlyStarted = sl.isPersistentlyStarted(); + dto.startLevel = sl.getStartLevel(); + + return dto; + } + + private static ServiceReferenceDTO[] createServiceReferenceDTOArray(Bundle bundle) + { + BundleContext ctx = ((BundleImpl) bundle)._getBundleContext(); + if (ctx == null) + return null; + + ServiceReference[] svcs = bundle.getRegisteredServices(); + if (svcs == null) + return new ServiceReferenceDTO[0]; + + ServiceReferenceDTO[] dtos = new ServiceReferenceDTO[svcs.length]; + for (int i=0; i < svcs.length; i++) + { + dtos[i] = createServiceReferenceDTO(svcs[i]); + } + return dtos; + } + + private static ServiceReferenceDTO createServiceReferenceDTO(ServiceReference svc) + { + ServiceReferenceDTO dto = new ServiceReferenceDTO(); + dto.bundle = svc.getBundle().getBundleId(); + dto.id = (Long) svc.getProperty(Constants.SERVICE_ID); + Map props = new HashMap(); + for (String key : svc.getPropertyKeys()) + { + props.put(key, svc.getProperty(key)); + } + dto.properties = new HashMap(props); + + Bundle[] ubs = svc.getUsingBundles(); + if (ubs == null) + { + dto.usingBundles = new long[0]; + } + else + { + dto.usingBundles = new long[ubs.length]; + for (int j=0; j < ubs.length; j++) + { + dto.usingBundles[j] = ubs[j].getBundleId(); + } + } + return dto; + } + + @SuppressWarnings("unchecked") + private static FrameworkDTO createFrameworkDTO(Felix framework) + { + FrameworkDTO dto = new FrameworkDTO(); + dto.properties = convertAttrsToDTO(framework.getConfig()); + + dto.bundles = new ArrayList(); + for (Bundle b : framework._getBundleContext().getBundles()) + { + dto.bundles.add(DTOFactory.createDTO(b, BundleDTO.class)); + } + + dto.services = new ArrayList(); + + ServiceReference[] refs = null; + try + { + refs = framework._getBundleContext().getAllServiceReferences(null, null); + } + catch (InvalidSyntaxException e) + { + // No filter, should never happen + } + + for (ServiceReference sr : refs) + { + dto.services.add(createServiceReferenceDTO(sr)); + } + + return dto; + } + + private static FrameworkStartLevelDTO createFrameworkStartLevelDTO(Framework framework) + { + FrameworkStartLevel fsl = framework.adapt(FrameworkStartLevel.class); + + FrameworkStartLevelDTO dto = new FrameworkStartLevelDTO(); + dto.initialBundleStartLevel = fsl.getInitialBundleStartLevel(); + dto.startLevel = fsl.getStartLevel(); + + return dto; + } + + private static FrameworkWiringDTO createFrameworkWiringDTO(Felix framework) + { + FrameworkWiringDTO dto = new FrameworkWiringDTO(); + + dto.resources = new HashSet(); + dto.wirings = new HashSet(); + + Set bundles = new LinkedHashSet(Arrays.asList(framework.getBundles())); + bundles.addAll(framework.getRemovalPendingBundles()); + + for (Bundle bundle : bundles) + { + addBundleWiring(bundle, dto.resources, dto.wirings); + } + + return dto; + } + + private static void addBundleWiring(Bundle bundle, Set resources, Set wirings) + { + BundleRevisions brs = bundle.adapt(BundleRevisions.class); + + for (BundleRevision revision : brs.getRevisions()) + { + BundleWiring wiring = revision.getWiring(); + if (wiring != null) + { + createBundleWiringNodeDTO(wiring, resources, wirings); + } + } + } + + private static void addBundleRevisionDTO(BundleRevisionDTO dto, Set resources) + { + for (BundleRevisionDTO r : resources) + { + if (r.id == dto.id) + return; + } + resources.add(dto); + } + + private static void addNodeDTO(NodeDTO dto, Set nodes) + { + for (NodeDTO nodeDTO : nodes) + { + if (nodeDTO.id == dto.id) + return; + } + nodes.add(dto); + } + + private static void addWiringNodeIfNotPresent(BundleWiring bw, Set resources, Set nodes) + { + int wiringID = getWiringID(bw); + for (NodeDTO n : nodes) + { + if (n.id == wiringID) + return; + } + createBundleWiringNodeDTO(bw, resources, nodes); + } + + // Attributes contain Version values which are not supported for DTOs, so if + // these are found they need to be converted to String values. + private static Map convertAttrsToDTO(Map map) + { + Map m = new HashMap(); + for (Map.Entry entry : map.entrySet()) + { + Object value = convertAttrToDTO(entry.getValue()); + if (value != null) + { + m.put(entry.getKey(), value); + } + } + return m; + } + + private static Object convertAttrToDTO(Object value) + { + if (value instanceof Version) + { + return value.toString(); + } + else if (isPermissibleAttribute(value.getClass()) + || (value.getClass().isArray() + && isPermissibleAttribute(value.getClass().getComponentType()))) + { + return value; + } + else if (value instanceof List) + { + List result = new ArrayList(); + for (Object v : ((List) value)) + { + Object vv = convertAttrToDTO(v); + if (vv != null) + { + result.add(vv); + } + } + return result.isEmpty() ? null : result; + } + else + { + return null; + } + } + + private static boolean isPermissibleAttribute(Class clazz) + { + return clazz == Boolean.class || clazz == String.class + || DTO.class.isAssignableFrom(clazz) || Number.class.isAssignableFrom(clazz); + } + + private static int getWiringID(Wiring bw) + { + Resource res = bw.getResource(); + if (res != null) + { + return getResourceIDAndAdd(res, null); + } + return bw.hashCode(); + } + + private static int getCapabilityID(Capability capability) + { + return capability.hashCode(); + } + + private static int getRequirementID(Requirement requirement) + { + return requirement.hashCode(); + } + + private static int getResourceIDAndAdd(Resource res, Set resources) + { + if (res instanceof BundleRevisionImpl) + { + BundleRevisionImpl bres = (BundleRevisionImpl) res; + int id = bres.getId().hashCode(); + + if (resources == null) + return id; + + for (BundleRevisionDTO rdto : resources) + { + if (rdto.id == id) + return id; + } + createBundleRevisionDTO(bres, resources); + return id; + } + return res.hashCode(); + } + + private static int getRevisionID(BundleRevisionImpl revision) + { + return revision.getId().hashCode(); + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/EntryFilterEnumeration.java b/framework/src/main/java/org/apache/felix/framework/EntryFilterEnumeration.java new file mode 100644 index 00000000000..290de06f976 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/EntryFilterEnumeration.java @@ -0,0 +1,245 @@ +/* + * 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.framework; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.*; +import org.apache.felix.framework.capabilityset.SimpleFilter; +import org.apache.felix.framework.util.Util; +import org.osgi.framework.wiring.BundleRevision; + +class EntryFilterEnumeration implements Enumeration +{ + private final BundleRevision m_revision; + private final List m_enumerations; + private final List m_revisions; + private int m_revisionIndex = 0; + private final String m_path; + private final List m_filePattern; + private final boolean m_recurse; + private final boolean m_isURLValues; + private final Set m_dirEntries = new HashSet(); + private final List m_nextEntries = new ArrayList(2); + + public EntryFilterEnumeration( + BundleRevision revision, boolean includeFragments, String path, + String filePattern, boolean recurse, boolean isURLValues) + { + m_revision = revision; + List fragments = Util.getFragments(revision.getWiring()); + if (includeFragments && !fragments.isEmpty()) + { + m_revisions = fragments; + } + else + { + m_revisions = new ArrayList(1); + } + m_revisions.add(0, m_revision); + m_enumerations = new ArrayList(m_revisions.size()); + for (int i = 0; i < m_revisions.size(); i++) + { + m_enumerations.add(((BundleRevisionImpl) m_revisions.get(i)).getContent() != null ? + ((BundleRevisionImpl) m_revisions.get(i)).getContent().getEntries() : null); + } + m_recurse = recurse; + m_isURLValues = isURLValues; + + // Sanity check the parameters. + if (path == null) + { + throw new IllegalArgumentException("The path for findEntries() cannot be null."); + } + // Strip leading '/' if present. + if ((path.length() > 0) && (path.charAt(0) == '/')) + { + path = path.substring(1); + } + // Add a '/' to the end if not present. + if ((path.length() > 0) && (path.charAt(path.length() - 1) != '/')) + { + path = path + "/"; + } + m_path = path; + + // File pattern defaults to "*" if not specified. + filePattern = (filePattern == null) ? "*" : filePattern; + + m_filePattern = SimpleFilter.parseSubstring(filePattern); + + findNext(); + } + + public synchronized boolean hasMoreElements() + { + return !m_nextEntries.isEmpty(); + } + + public synchronized Object nextElement() + { + if (m_nextEntries.isEmpty()) + { + throw new NoSuchElementException("No more entries."); + } + Object last = m_nextEntries.remove(0); + findNext(); + return last; + } + + private void findNext() + { + // This method filters the content entry enumeration, such that + // it only displays the contents of the directory specified by + // the path argument either recursively or not; much like using + // "ls -R" or "ls" to list the contents of a directory, respectively. + if (m_enumerations == null) + { + return; + } + while ((m_revisionIndex < m_enumerations.size()) && m_nextEntries.isEmpty()) + { + while (m_enumerations.get(m_revisionIndex) != null + && m_enumerations.get(m_revisionIndex).hasMoreElements() + && m_nextEntries.isEmpty()) + { + // Get the current entry to determine if it should be filtered or not. + String entryName = (String) m_enumerations.get(m_revisionIndex).nextElement(); + // Check to see if the current entry is a descendent of the specified path. + if (!entryName.equals(m_path) && entryName.startsWith(m_path)) + { + // Cached entry URL. If we are returning URLs, we use this + // cached URL to avoid doing multiple URL lookups from a revision + // when synthesizing directory URLs. + URL entryURL = null; + + // If the current entry is in a subdirectory of the specified path, + // get the index of the slash character. + int dirSlashIdx = entryName.indexOf('/', m_path.length()); + + // JAR files are supposed to contain entries for directories, + // but not all do. So determine the directory for this entry + // and see if we've already seen an entry for for it. If not, + // synthesize an entry for it. If we are doing a recursive + // match, we need to synthesize each matching subdirectory + // of the entry. + if (dirSlashIdx >= 0) + { + // Start synthesizing directories for the current entry + // at the subdirectory after the initial path. + int subDirSlashIdx = dirSlashIdx; + String dir; + do + { + // Calculate the subdirectory name. + dir = entryName.substring(0, subDirSlashIdx + 1); + // If we have not seen this directory before, then record + // it and add it to the list of available next entries. If + // the original entry is actually a directory, then it will + // be added to next entries like normal if it matches, but if + // it is not a directory, its parent directory entries may be + // synthesized depending on the matching filter and recursion. + if (!m_dirEntries.contains(dir)) + { + // Record the directory entry. + m_dirEntries.add(dir); + // Add the directory to the list of + if (SimpleFilter.compareSubstring( + m_filePattern, getLastPathElement(dir))) + { + // Add synthesized directory entry to the next + // entries list in the correct form. + if (m_isURLValues) + { + entryURL = (entryURL == null) + ? ((BundleRevisionImpl) m_revisions. + get(m_revisionIndex)).getEntry(entryName) + : entryURL; + try + { + m_nextEntries.add(new URL(entryURL, "/" + dir)); + } + catch (MalformedURLException ex) + { + } + } + else + { + m_nextEntries.add(dir); + } + } + } + // Now prepare to synthesize the next subdirectory + // if we are matching recursively. + subDirSlashIdx = entryName.indexOf('/', dir.length()); + } + while (m_recurse && (subDirSlashIdx >= 0)); + } + + // Now we actually need to check if the current entry itself should + // be filtered or not. If we are recursive or the current entry + // is a child (not a grandchild) of the initial path, then we need + // to check if it matches the file pattern. If we've already added + // or synthesized the directory entry, then we can ignore it. + if (!m_dirEntries.contains(entryName) + && (m_recurse || (dirSlashIdx < 0) + || (dirSlashIdx == entryName.length() - 1))) + { + // See if the file pattern matches the last element of the path. + if (SimpleFilter.compareSubstring( + m_filePattern, getLastPathElement(entryName))) + { + if (m_isURLValues) + { + entryURL = (entryURL == null) + ? ((BundleRevisionImpl) + m_revisions.get(m_revisionIndex)).getEntry(entryName) + : entryURL; + m_nextEntries.add(entryURL); + } + else + { + m_nextEntries.add(entryName); + } + } + } + } + } + if (m_nextEntries.isEmpty()) + { + m_revisionIndex++; + // Reset directory entries, since fragments may + // have overlapping directory entries that need + // to be returned. + m_dirEntries.clear(); + } + } + } + + private static String getLastPathElement(String entryName) + { + int endIdx = (entryName.charAt(entryName.length() - 1) == '/') + ? entryName.length() - 1 + : entryName.length(); + int startIdx = (entryName.charAt(entryName.length() - 1) == '/') + ? entryName.lastIndexOf('/', endIdx - 1) + 1 + : entryName.lastIndexOf('/', endIdx) + 1; + return entryName.substring(startIdx, endIdx); + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/EventDispatcher.java b/framework/src/main/java/org/apache/felix/framework/EventDispatcher.java new file mode 100644 index 00000000000..a58af941326 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/EventDispatcher.java @@ -0,0 +1,1174 @@ +/* + * 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.framework; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.EventListener; +import java.util.EventObject; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.felix.framework.util.*; +import org.osgi.framework.AllServiceListener; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.BundleListener; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServicePermission; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.SynchronousBundleListener; +import org.osgi.framework.UnfilteredServiceListener; +import org.osgi.framework.hooks.service.ListenerHook; +import org.osgi.framework.launch.Framework; + +public class EventDispatcher +{ + private final Logger m_logger; + private final ServiceRegistry m_registry; + + private Map> + m_fwkListeners = Collections.EMPTY_MAP; + private Map> + m_bndlListeners = Collections.EMPTY_MAP; + private Map> + m_syncBndlListeners = Collections.EMPTY_MAP; + private Map> + m_svcListeners = Collections.EMPTY_MAP; + + // A single thread is used to deliver events for all dispatchers. + private static Thread m_thread = null; + private final static String m_threadLock = new String("thread lock"); + private static int m_references = 0; + private static volatile boolean m_stopping = false; + + // List of requests. + private static final List m_requestList = new ArrayList(); + // Pooled requests to avoid memory allocation. + private static final List m_requestPool = new ArrayList(); + + private static final SecureAction m_secureAction = new SecureAction(); + + public EventDispatcher(Logger logger, ServiceRegistry registry) + { + m_logger = logger; + m_registry = registry; + } + + public void startDispatching() + { + synchronized (m_threadLock) + { + // Start event dispatching thread if necessary. + if (m_thread == null || !m_thread.isAlive()) + { + m_stopping = false; + + m_thread = new Thread(new Runnable() { + @Override + public void run() + { + try + { + EventDispatcher.run(); + } + finally + { + // Ensure we update state even if stopped by external cause + // e.g. an Applet VM forceably killing threads + synchronized (m_threadLock) + { + m_thread = null; + m_stopping = false; + m_references = 0; + m_threadLock.notifyAll(); + } + } + } + }, "FelixDispatchQueue"); + m_thread.start(); + } + + // reference counting and flags + m_references++; + } + } + + public void stopDispatching() + { + synchronized (m_threadLock) + { + // Return if already dead or stopping. + if (m_thread == null || m_stopping) + { + return; + } + + // decrement use counter, don't continue if there are users + m_references--; + if (m_references > 0) + { + return; + } + + m_stopping = true; + } + + // Signal dispatch thread. + synchronized (m_requestList) + { + m_requestList.notify(); + } + + // Use separate lock for shutdown to prevent any chance of nested lock deadlock + synchronized (m_threadLock) + { + while (m_thread != null) + { + try + { + m_threadLock.wait(); + } + catch (InterruptedException ex) + { + } + } + } + } + + public Filter addListener(BundleContext bc, Class clazz, EventListener l, Filter filter) + { + // Verify the listener. + if (l == null) + { + throw new IllegalArgumentException("Listener is null"); + } + else if (!clazz.isInstance(l)) + { + throw new IllegalArgumentException( + "Listener not of type " + clazz.getName()); + } + + // See if we can simply update the listener, if so then + // return immediately. + Filter oldFilter = updateListener(bc, clazz, l, filter); + if (oldFilter != null) + { + return oldFilter; + } + + // Lock the object to add the listener. + synchronized (this) + { + // Verify that the bundle context is still valid. + try + { + bc.getBundle(); + } + catch (IllegalStateException ex) + { + // Bundle context is no longer valid, so just return. + return null; + } + + Map> listeners = null; + Object acc = null; + + if (clazz == FrameworkListener.class) + { + listeners = m_fwkListeners; + } + else if (clazz == BundleListener.class) + { + if (SynchronousBundleListener.class.isInstance(l)) + { + listeners = m_syncBndlListeners; + } + else + { + listeners = m_bndlListeners; + } + } + else if (clazz == ServiceListener.class) + { + // Remember security context for filtering service events. + Object sm = System.getSecurityManager(); + if (sm != null) + { + acc = ((SecurityManager) sm).getSecurityContext(); + } + // We need to create a Set for keeping track of matching service + // registrations so we can fire ServiceEvent.MODIFIED_ENDMATCH + // events. We need a Set even if filter is null, since the + // listener can be updated and have a filter added later. + listeners = m_svcListeners; + } + else + { + throw new IllegalArgumentException("Unknown listener: " + l.getClass()); + } + + // Add listener. + ListenerInfo info = + new ListenerInfo(bc.getBundle(), bc, clazz, l, filter, acc, false); + listeners = addListenerInfo(listeners, info); + + if (clazz == FrameworkListener.class) + { + m_fwkListeners = listeners; + } + else if (clazz == BundleListener.class) + { + if (SynchronousBundleListener.class.isInstance(l)) + { + m_syncBndlListeners = listeners; + } + else + { + m_bndlListeners = listeners; + } + } + else if (clazz == ServiceListener.class) + { + m_svcListeners = listeners; + } + } + return null; + } + + public ListenerHook.ListenerInfo removeListener( + BundleContext bc, Class clazz, EventListener l) + { + ListenerHook.ListenerInfo returnInfo = null; + + // Verify listener. + if (l == null) + { + throw new IllegalArgumentException("Listener is null"); + } + else if (!clazz.isInstance(l)) + { + throw new IllegalArgumentException( + "Listener not of type " + clazz.getName()); + } + + // Lock the object to remove the listener. + synchronized (this) + { + Map> listeners = null; + + if (clazz == FrameworkListener.class) + { + listeners = m_fwkListeners; + } + else if (clazz == BundleListener.class) + { + if (SynchronousBundleListener.class.isInstance(l)) + { + listeners = m_syncBndlListeners; + } + else + { + listeners = m_bndlListeners; + } + } + else if (clazz == ServiceListener.class) + { + listeners = m_svcListeners; + } + else + { + throw new IllegalArgumentException("Unknown listener: " + l.getClass()); + } + + // Try to find the instance in our list. + int idx = -1; + for (Entry> entry : listeners.entrySet()) + { + List infos = entry.getValue(); + for (int i = 0; i < infos.size(); i++) + { + ListenerInfo info = infos.get(i); + if (info.getBundleContext().equals(bc) && + (info.getListenerClass() == clazz) && + (info.getListener() == l)) + { + // For service listeners, we must return some info about + // the listener for the ListenerHook callback. + if (ServiceListener.class == clazz) + { + returnInfo = new ListenerInfo(infos.get(i), true); + } + idx = i; + break; + } + } + } + + // If we have the instance, then remove it. + if (idx >= 0) + { + listeners = removeListenerInfo(listeners, bc, idx); + } + + if (clazz == FrameworkListener.class) + { + m_fwkListeners = listeners; + } + else if (clazz == BundleListener.class) + { + if (SynchronousBundleListener.class.isInstance(l)) + { + m_syncBndlListeners = listeners; + } + else + { + m_bndlListeners = listeners; + } + } + else if (clazz == ServiceListener.class) + { + m_svcListeners = listeners; + } + } + + // Return information about the listener; this is null + // for everything but service listeners. + return returnInfo; + } + + public void removeListeners(BundleContext bc) + { + if (bc == null) + { + return; + } + + synchronized (this) + { + // Remove all framework listeners associated with the specified bundle. + m_fwkListeners = removeListenerInfos(m_fwkListeners, bc); + + // Remove all bundle listeners associated with the specified bundle. + m_bndlListeners = removeListenerInfos(m_bndlListeners, bc); + + // Remove all synchronous bundle listeners associated with + // the specified bundle. + m_syncBndlListeners = removeListenerInfos(m_syncBndlListeners, bc); + + // Remove all service listeners associated with the specified bundle. + m_svcListeners = removeListenerInfos(m_svcListeners, bc); + } + } + + public Filter updateListener(BundleContext bc, Class clazz, EventListener l, Filter filter) + { + if (clazz == ServiceListener.class) + { + synchronized (this) + { + // Verify that the bundle context is still valid. + try + { + bc.getBundle(); + } + catch (IllegalStateException ex) + { + // Bundle context is no longer valid, so just return. + } + + // See if the service listener is already registered; if so then + // update its filter per the spec. + List infos = m_svcListeners.get(bc); + for (int i = 0; (infos != null) && (i < infos.size()); i++) + { + ListenerInfo info = infos.get(i); + if (info.getBundleContext().equals(bc) && + (info.getListenerClass() == clazz) && + (info.getListener() == l)) + { + // The spec says to update the filter in this case. + Filter oldFilter = info.getParsedFilter(); + ListenerInfo newInfo = new ListenerInfo( + info.getBundle(), + info.getBundleContext(), + info.getListenerClass(), + info.getListener(), + filter, + info.getSecurityContext(), + info.isRemoved()); + m_svcListeners = updateListenerInfo(m_svcListeners, i, newInfo); + return oldFilter; + } + } + } + } + + return null; + } + + /** + * Returns all existing service listener information into a collection of + * ListenerHook.ListenerInfo objects. This is used the first time a listener + * hook is registered to synchronize it with the existing set of listeners. + * @return Returns all existing service listener information into a collection of + * ListenerHook.ListenerInfo objects + **/ + public Collection getAllServiceListeners() + { + List listeners = new ArrayList(); + synchronized (this) + { + for (Entry> entry : m_svcListeners.entrySet()) + { + listeners.addAll(entry.getValue()); + } + } + return listeners; + } + + public void fireFrameworkEvent(FrameworkEvent event) + { + // Take a snapshot of the listener array. + Map> listeners = null; + synchronized (this) + { + listeners = m_fwkListeners; + } + + // Fire all framework listeners on a separate thread. + fireEventAsynchronously(this, Request.FRAMEWORK_EVENT, listeners, event); + } + + public void fireBundleEvent(BundleEvent event, Felix felix) + { + // Take a snapshot of the listener array. + Map> listeners = null; + Map> syncListeners = null; + synchronized (this) + { + listeners = m_bndlListeners; + syncListeners = m_syncBndlListeners; + } + + // Create a whitelist of bundle context for bundle listeners, + // if we have hooks. + Set whitelist = createWhitelistFromHooks(event, felix, + listeners, syncListeners, org.osgi.framework.hooks.bundle.EventHook.class); + + // If we have a whitelist, then create copies of only the whitelisted + // listeners. + if (whitelist != null) + { + Map> copy = + new HashMap>(); + for (BundleContext bc : whitelist) + { + List infos = listeners.get(bc); + if (infos != null) + { + copy.put(bc, infos); + } + } + listeners = copy; + copy = new HashMap>(); + for (BundleContext bc : whitelist) + { + List infos = syncListeners.get(bc); + if (infos != null) + { + copy.put(bc, infos); + } + } + syncListeners = copy; + } + + // Fire synchronous bundle listeners immediately on the calling thread. + fireEventImmediately( + this, Request.BUNDLE_EVENT, syncListeners, event, null); + + // The spec says that asynchronous bundle listeners do not get events + // of types STARTING, STOPPING, or LAZY_ACTIVATION. + if ((event.getType() != BundleEvent.STARTING) && + (event.getType() != BundleEvent.STOPPING) && + (event.getType() != BundleEvent.LAZY_ACTIVATION)) + { + // Fire asynchronous bundle listeners on a separate thread. + fireEventAsynchronously( + this, Request.BUNDLE_EVENT, listeners, event); + } + } + + public void fireServiceEvent( + final ServiceEvent event, final Dictionary oldProps, final Felix felix) + { + // Take a snapshot of the listener array. + Map> listeners = null; + synchronized (this) + { + listeners = m_svcListeners; + } + + // Use service registry hooks to filter target listeners. + listeners = filterListenersUsingHooks(event, felix, listeners); + + // Fire all service events immediately on the calling thread. + fireEventImmediately( + this, Request.SERVICE_EVENT, listeners, event, oldProps); + } + +// TODO: OSGi R4.3 - This is ugly and inefficient. + private Map> filterListenersUsingHooks( + ServiceEvent event, Felix felix, Map> listeners) + { + Set> ehs = + m_registry.getHookRegistry().getHooks(org.osgi.framework.hooks.service.EventHook.class); + if (!ehs.isEmpty()) + { + // Create a whitelist of bundle context for bundle listeners, + // if we have hooks. + Set whitelist = createWhitelistFromHooks(event, felix, + listeners, null, org.osgi.framework.hooks.service.EventHook.class); + + // If we have a whitelist, then create copies of only the whitelisted + // listeners. + if (whitelist != null) + { + Map> copy = + new HashMap>(); + for (BundleContext bc : whitelist) + { + copy.put(bc, listeners.get(bc)); + } + listeners = copy; + } + } + + Set> elhs = + m_registry.getHookRegistry().getHooks(org.osgi.framework.hooks.service.EventListenerHook.class); + if (!elhs.isEmpty()) + { + List systemBundleListeners = null; + + // The mutable map is used to keep the lists that underpin the shrinkable collections. + Map> mutableMap = + new HashMap>(); + // Create map with shrinkable collections. + Map> shrinkableMap = + new HashMap>(); + for (Entry> entry : listeners.entrySet()) + { + BundleContext bc = entry.getKey(); + ArrayList mutableList = new ArrayList(entry.getValue()); + mutableMap.put(bc, mutableList); + + // We want to pass the list as a generic collection to Shrinkable Collection. + // Need to convert to raw type before we can do this... + ArrayList ml = mutableList; + + Collection shrinkableCollection = + new ShrinkableCollection(ml); + shrinkableMap.put(bc, shrinkableCollection); + + // Keep a copy of the System Bundle Listeners, as they might be removed by the hooks, but we + // actually need to keep them in the end. + if (bc == felix._getBundleContext()) + systemBundleListeners = new ArrayList(entry.getValue()); + } + shrinkableMap = + new ShrinkableMap> + (shrinkableMap); + + for (ServiceReference sr : elhs) + { + if (felix != null) + { + org.osgi.framework.hooks.service.EventListenerHook elh = null; + try + { + elh = m_registry.getService(felix, sr, false); + } + catch (Exception ex) + { + // If we can't get the hook, then ignore it. + } + if (elh != null) + { + try + { + m_secureAction.invokeServiceEventListenerHook( + elh, event, shrinkableMap); + } + catch (Throwable th) + { + m_logger.log(sr, Logger.LOG_WARNING, + "Problem invoking event hook", th); + } + finally + { + m_registry.ungetService(felix, sr, null); + } + } + } + } + +// TODO: OSGi R4.3 - Should check and only do this if there was a change. + + // the listeners map should only contain List values, and not Shrinkable + // Collections. Therefore we create a new map from the lists that are + // the delegates for the Shrinkable Collections. Any changes made by the + // hooks will have propagated to these maps. + Map> newMap = + new HashMap>(); + for (Map.Entry> entry : shrinkableMap.entrySet()) + { + if (!entry.getValue().isEmpty()) + { + newMap.put(entry.getKey(), mutableMap.get(entry.getKey())); + } + } + + // Put the system bundle listeners back, because they really need to be called + // regardless whether they were removed by the hooks or not. + if (systemBundleListeners != null) + newMap.put(felix._getBundleContext(), systemBundleListeners); + + listeners = newMap; + } + + return listeners; + } + + private Set createWhitelistFromHooks( + EventObject event, Felix felix, + Map> listeners1, + Map> listeners2, + Class hookClass) + { + // Create a whitelist of bundle context, if we have hooks. + Set whitelist = null; + Set> hooks = m_registry.getHookRegistry().getHooks(hookClass); + if (!hooks.isEmpty()) + { + boolean systemBundleListener = false; + BundleContext systemBundleContext = felix._getBundleContext(); + + whitelist = new HashSet(); + for (Entry> entry : listeners1.entrySet()) + { + whitelist.add(entry.getKey()); + if (entry.getKey() == systemBundleContext) + systemBundleListener = true; + } + if (listeners2 != null) + { + for (Entry> entry : listeners2.entrySet()) + { + whitelist.add(entry.getKey()); + if (entry.getKey() == systemBundleContext) + systemBundleListener = true; + } + } + + int originalSize = whitelist.size(); + ShrinkableCollection shrinkable = + new ShrinkableCollection(whitelist); + for (ServiceReference sr : hooks) + { + if (felix != null) + { + T eh = null; + try + { + eh = m_registry.getService(felix, sr, false); + } + catch (Exception ex) + { + // If we can't get the hook, then ignore it. + } + if (eh != null) + { + try + { + if (eh instanceof org.osgi.framework.hooks.service.EventHook) + { + m_secureAction.invokeServiceEventHook( + (org.osgi.framework.hooks.service.EventHook) eh, + (ServiceEvent) event, shrinkable); + } + else if (eh instanceof org.osgi.framework.hooks.bundle.EventHook) + { + m_secureAction.invokeBundleEventHook( + (org.osgi.framework.hooks.bundle.EventHook) eh, + (BundleEvent) event, shrinkable); + } + } + catch (Throwable th) + { + m_logger.log(sr, Logger.LOG_WARNING, + "Problem invoking event hook", th); + } + finally + { + m_registry.ungetService(felix, sr, null); + } + } + } + } + + if (systemBundleListener && !whitelist.contains(systemBundleContext)) + { + // The system bundle cannot be removed from the listeners, so if it was + // removed, add it back in. Note that this cannot be prevented by the shrinkable + // since the effect of removing the system bundle from the listeners must be + // visible between the event hooks. + whitelist.add(systemBundleContext); + } + + // If the whitelist hasn't changed, then null it to avoid having + // to do whitelist lookups during event delivery. + if (originalSize == whitelist.size()) + { + whitelist = null; + } + } + return whitelist; + } + + private static void fireEventAsynchronously( + EventDispatcher dispatcher, int type, + Map> listeners, + EventObject event) + { + //TODO: should possibly check this within thread lock, seems to be ok though without + // If dispatch thread is stopped, then ignore dispatch request. + if (m_stopping || m_thread == null) + { + return; + } + + // First get a request from the pool or create one if necessary. + Request req = null; + synchronized (m_requestPool) + { + if (m_requestPool.size() > 0) + { + req = m_requestPool.remove(0); + } + else + { + req = new Request(); + } + } + + // Initialize dispatch request. + req.m_dispatcher = dispatcher; + req.m_type = type; + req.m_listeners = listeners; + req.m_event = event; + + // Lock the request list. + synchronized (m_requestList) + { + // Add our request to the list. + m_requestList.add(req); + // Notify the dispatch thread that there is work to do. + m_requestList.notify(); + } + } + + private static void fireEventImmediately( + EventDispatcher dispatcher, int type, + Map> listeners, + EventObject event, Dictionary oldProps) + { + if (!listeners.isEmpty()) + { + // Notify appropriate listeners. + for (Entry> entry : listeners.entrySet()) + { + for (ListenerInfo info : entry.getValue()) + { + Bundle bundle = info.getBundle(); + EventListener l = info.getListener(); + Filter filter = info.getParsedFilter(); + Object acc = info.getSecurityContext(); + + try + { + if (type == Request.FRAMEWORK_EVENT) + { + invokeFrameworkListenerCallback(bundle, l, event); + } + else if (type == Request.BUNDLE_EVENT) + { + invokeBundleListenerCallback(bundle, l, event); + } + else if (type == Request.SERVICE_EVENT) + { + invokeServiceListenerCallback( + bundle, l, filter, acc, event, oldProps); + } + } + catch (Throwable th) + { + if ((type != Request.FRAMEWORK_EVENT) + || (((FrameworkEvent) event).getType() != FrameworkEvent.ERROR)) + { + dispatcher.m_logger.log(bundle, + Logger.LOG_ERROR, + "EventDispatcher: Error during dispatch.", th); + dispatcher.fireFrameworkEvent( + new FrameworkEvent(FrameworkEvent.ERROR, bundle, th)); + } + } + } + } + } + } + + private static void invokeFrameworkListenerCallback( + Bundle bundle, final EventListener l, final EventObject event) + { + // The spec says only active bundles receive asynchronous events, + // but we will include starting bundles too otherwise + // it is impossible to see everything. + if ((bundle.getState() == Bundle.STARTING) || + (bundle.getState() == Bundle.ACTIVE)) + { + if (System.getSecurityManager() != null) + { + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Object run() + { + ((FrameworkListener) l).frameworkEvent((FrameworkEvent) event); + return null; + } + }); + } + else + { + ((FrameworkListener) l).frameworkEvent((FrameworkEvent) event); + } + } + } + + private static void invokeBundleListenerCallback( + Bundle bundle, final EventListener l, final EventObject event) + { + // A bundle listener is either synchronous or asynchronous. + // If the bundle listener is synchronous, then deliver the + // event to bundles with a state of STARTING, STOPPING, or + // ACTIVE. If the listener is asynchronous, then deliver the + // event only to bundles that are STARTING or ACTIVE. + if (((SynchronousBundleListener.class.isAssignableFrom(l.getClass())) && + ((bundle.getState() == Bundle.STARTING) || + (bundle.getState() == Bundle.STOPPING) || + (bundle.getState() == Bundle.ACTIVE))) + || + ((bundle.getState() == Bundle.STARTING) || + (bundle.getState() == Bundle.ACTIVE))) + { + if (System.getSecurityManager() != null) + { + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Object run() + { + ((BundleListener) l).bundleChanged((BundleEvent) event); + return null; + } + }); + } + else + { + ((BundleListener) l).bundleChanged((BundleEvent) event); + } + } + } + + private static void invokeServiceListenerCallback( + Bundle bundle, final EventListener l, Filter filter, Object acc, + final EventObject event, final Dictionary oldProps) + { + // Service events should be delivered to STARTING, + // STOPPING, and ACTIVE bundles. + if ((bundle.getState() != Bundle.STARTING) && + (bundle.getState() != Bundle.STOPPING) && + (bundle.getState() != Bundle.ACTIVE)) + { + return; + } + + // Check that the bundle has permission to get at least + // one of the service interfaces; the objectClass property + // of the service stores its service interfaces. + ServiceReference ref = ((ServiceEvent) event).getServiceReference(); + + boolean hasPermission = true; + Object sm = System.getSecurityManager(); + if ((acc != null) && (sm != null)) + { + try + { + ServicePermission perm = + new ServicePermission( + ref, ServicePermission.GET); + ((SecurityManager) sm).checkPermission(perm, acc); + } + catch (Exception ex) + { + hasPermission = false; + } + } + + if (hasPermission) + { + // Dispatch according to the filter. + boolean matched; + if (l instanceof UnfilteredServiceListener) + { + // An UnfilteredServiceListener always matches, regardless of the filter. + // The filter is still passed on to the Service Registry Hooks. + matched = true; + } + else + { + matched = (filter == null) + || filter.match(((ServiceEvent) event).getServiceReference()); + } + + if (matched) + { + if ((l instanceof AllServiceListener) || + Util.isServiceAssignable(bundle, ((ServiceEvent) event).getServiceReference())) + { + if (System.getSecurityManager() != null) + { + AccessController.doPrivileged(new PrivilegedAction() + { + @Override + public Object run() + { + ((ServiceListener) l).serviceChanged((ServiceEvent) event); + return null; + } + }); + } + else + { + ((ServiceListener) l).serviceChanged((ServiceEvent) event); + } + } + } + // We need to send an MODIFIED_ENDMATCH event if the listener + // matched previously. + else if (((ServiceEvent) event).getType() == ServiceEvent.MODIFIED) + { + if (filter.match(oldProps)) + { + final ServiceEvent se = new ServiceEvent( + ServiceEvent.MODIFIED_ENDMATCH, + ((ServiceEvent) event).getServiceReference()); + if (System.getSecurityManager() != null) + { + AccessController.doPrivileged(new PrivilegedAction() + { + @Override + public Object run() + { + ((ServiceListener) l).serviceChanged(se); + return null; + } + }); + } + else + { + ((ServiceListener) l).serviceChanged(se); + } + } + } + } + } + + private static Map> addListenerInfo( + Map> listeners, ListenerInfo info) + { + // Make a copy of the map, since we will be mutating it. + Map> copy = + new HashMap>(listeners); + // Remove the affected entry and make a copy so we can modify it. + List infos = copy.remove(info.getBundleContext()); + if (infos == null) + { + infos = new ArrayList(); + } + else + { + infos = new ArrayList(infos); + } + // Add the new listener info. + infos.add(info); + // Put the listeners back into the copy of the map and return it. + copy.put(info.getBundleContext(), infos); + return copy; + } + + private static Map> updateListenerInfo( + Map> listeners, int idx, + ListenerInfo info) + { + // Make a copy of the map, since we will be mutating it. + Map> copy = + new HashMap>(listeners); + // Remove the affected entry and make a copy so we can modify it. + List infos = copy.remove(info.getBundleContext()); + if (infos != null) + { + infos = new ArrayList(infos); + // Update the new listener info. + infos.set(idx, info); + // Put the listeners back into the copy of the map and return it. + copy.put(info.getBundleContext(), infos); + return copy; + } + return listeners; + } + + private static Map> removeListenerInfo( + Map> listeners, BundleContext bc, int idx) + { + // Make a copy of the map, since we will be mutating it. + Map> copy = + new HashMap>(listeners); + // Remove the affected entry and make a copy so we can modify it. + List infos = copy.remove(bc); + if (infos != null) + { + infos = new ArrayList(infos); + // Remove the listener info. + infos.remove(idx); + if (!infos.isEmpty()) + { + // Put the listeners back into the copy of the map and return it. + copy.put(bc, infos); + } + return copy; + } + return listeners; + } + + private static Map> removeListenerInfos( + Map> listeners, BundleContext bc) + { + // Make a copy of the map, since we will be mutating it. + Map> copy = + new HashMap>(listeners); + // Remove the affected entry and return the copy. + copy.remove(bc); + return copy; + } + + /** + * This is the dispatching thread's main loop. + **/ + private static void run() + { + Request req = null; + while (true) + { + // Lock the request list so we can try to get a + // dispatch request from it. + synchronized (m_requestList) + { + // Wait while there are no requests to dispatch. If the + // dispatcher thread is supposed to stop, then let the + // dispatcher thread exit the loop and stop. + while (m_requestList.isEmpty() && !m_stopping) + { + // Wait until some signals us for work. + try + { + m_requestList.wait(); + } + catch (InterruptedException ex) + { + // Not much we can do here except for keep waiting. + } + } + + // If there are no events to dispatch and shutdown + // has been called then exit, otherwise dispatch event. + if (m_requestList.isEmpty() && m_stopping) + { + return; + } + + // Get the dispatch request. + req = m_requestList.remove(0); + } + + // Deliver event outside of synchronized block + // so that we don't block other requests from being + // queued during event processing. + // NOTE: We don't catch any exceptions here, because + // the invoked method shields us from exceptions by + // catching Throwables when it invokes callbacks. + fireEventImmediately( + req.m_dispatcher, req.m_type, req.m_listeners, + req.m_event, null); + + // Put dispatch request in cache. + synchronized (m_requestPool) + { + req.m_dispatcher = null; + req.m_type = -1; + req.m_listeners = null; + req.m_event = null; + m_requestPool.add(req); + } + } + } + + private static class Request + { + public static final int FRAMEWORK_EVENT = 0; + public static final int BUNDLE_EVENT = 1; + public static final int SERVICE_EVENT = 2; + + public EventDispatcher m_dispatcher = null; + public int m_type = -1; + public Map> m_listeners = null; + public EventObject m_event = null; + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/ExportedPackageImpl.java b/framework/src/main/java/org/apache/felix/framework/ExportedPackageImpl.java index c7a692041c5..be2b1dbe0c0 100644 --- a/framework/src/main/java/org/apache/felix/framework/ExportedPackageImpl.java +++ b/framework/src/main/java/org/apache/felix/framework/ExportedPackageImpl.java @@ -1,49 +1,57 @@ /* - * Copyright 2006 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework; -import org.apache.felix.framework.searchpolicy.R4Export; -import org.apache.felix.moduleloader.IModule; +import java.util.Set; +import org.apache.felix.framework.wiring.BundleCapabilityImpl; import org.osgi.framework.Bundle; import org.osgi.framework.Version; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRevision; import org.osgi.service.packageadmin.ExportedPackage; class ExportedPackageImpl implements ExportedPackage { - private Felix m_felix = null; - private BundleImpl m_exportingBundle = null; - private IModule m_exportingModule = null; - private R4Export m_export = null; - private String m_toString = null; - private String m_versionString = null; + private final Felix m_felix; + private final BundleImpl m_exportingBundle; + private final BundleRevision m_exportingRevision; + private final BundleCapability m_export; + private final String m_pkgName; + private final Version m_version; public ExportedPackageImpl( - Felix felix, BundleImpl exporter, IModule module, R4Export export) + Felix felix, BundleImpl exporter, BundleRevision revision, BundleCapability export) { m_felix = felix; m_exportingBundle = exporter; - m_exportingModule = module; + m_exportingRevision = revision; m_export = export; + m_pkgName = (String) m_export.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE); + m_version = (!m_export.getAttributes().containsKey(BundleCapabilityImpl.VERSION_ATTR)) + ? Version.emptyVersion + : (Version) m_export.getAttributes().get(BundleCapabilityImpl.VERSION_ATTR); } public Bundle getExportingBundle() { // If the package is stale, then return null per the spec. - if (m_exportingBundle.getInfo().isStale()) + if (m_exportingBundle.isStale()) { return null; } @@ -53,48 +61,36 @@ public Bundle getExportingBundle() public Bundle[] getImportingBundles() { // If the package is stale, then return null per the spec. - if (m_exportingBundle.getInfo().isStale()) + if (m_exportingBundle.isStale()) { return null; } - return m_felix.getImportingBundles(this); + Set set = m_felix.getImportingBundles(m_exportingBundle, m_export); + return set.toArray(new Bundle[set.size()]); } public String getName() { - return m_export.getName(); + return m_pkgName; } public String getSpecificationVersion() { - if (m_versionString == null) - { - m_versionString = (m_export.getVersion() == null) - ? Version.emptyVersion.toString() - : m_export.getVersion().toString(); - } - return m_versionString; + return m_version.toString(); } public Version getVersion() { - return (m_export.getVersion() == null) - ? Version.emptyVersion - : m_export.getVersion(); + return m_version; } public boolean isRemovalPending() { - return m_exportingModule.isRemovalPending(); + return m_exportingBundle.isRemovalPending(); } public String toString() { - if (m_toString == null) - { - m_toString = m_export.getName() - + "; version=" + getSpecificationVersion(); - } - return m_toString; + return m_pkgName + "; version=" + m_version; } } \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/ExtensionManager.java b/framework/src/main/java/org/apache/felix/framework/ExtensionManager.java new file mode 100644 index 00000000000..a5c8664c786 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/ExtensionManager.java @@ -0,0 +1,1207 @@ +/* + * 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.framework; + +import org.apache.felix.framework.cache.Content; +import org.apache.felix.framework.cache.DirectoryContent; +import org.apache.felix.framework.cache.JarContent; +import org.apache.felix.framework.ext.ClassPathExtenderFactory; +import org.apache.felix.framework.util.ClassParser; +import org.apache.felix.framework.util.FelixConstants; +import org.apache.felix.framework.util.StringMap; +import org.apache.felix.framework.util.Util; +import org.apache.felix.framework.util.manifestparser.ManifestParser; +import org.apache.felix.framework.util.manifestparser.NativeLibrary; +import org.apache.felix.framework.util.manifestparser.NativeLibraryClause; +import org.apache.felix.framework.wiring.BundleCapabilityImpl; +import org.apache.felix.framework.wiring.BundleRequirementImpl; +import org.apache.felix.framework.wiring.BundleWireImpl; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; +import org.osgi.framework.AdminPermission; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.Version; +import org.osgi.framework.namespace.BundleNamespace; +import org.osgi.framework.namespace.ExecutionEnvironmentNamespace; +import org.osgi.framework.namespace.HostNamespace; +import org.osgi.framework.namespace.IdentityNamespace; +import org.osgi.framework.namespace.NativeNamespace; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.URI; +import java.net.URL; +import java.security.AccessController; +import java.security.AllPermission; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.Properties; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * The ExtensionManager class is used as content loader of the systembundle. Added extension + * bundles exports will be available via this loader. + */ +class ExtensionManager implements Content +{ + static final ClassPathExtenderFactory.ClassPathExtender m_extenderFramework; + static final ClassPathExtenderFactory.ClassPathExtender m_extenderBoot; + private static final Set IDENTITY = new HashSet(Arrays.asList( + BundleNamespace.BUNDLE_NAMESPACE, + HostNamespace.HOST_NAMESPACE, + IdentityNamespace.IDENTITY_NAMESPACE)); + + static + { + ClassPathExtenderFactory.ClassPathExtender extenderFramework = null; + ClassPathExtenderFactory.ClassPathExtender extenderBoot = null; + + if (!"true".equalsIgnoreCase(Felix.m_secureAction.getSystemProperty(FelixConstants.FELIX_EXTENSIONS_DISABLE, "false"))) + { + ServiceLoader loader = ServiceLoader.load(ClassPathExtenderFactory.class, + ExtensionManager.class.getClassLoader()); + + + for (Iterator iter = loader.iterator(); + iter.hasNext() && (extenderFramework == null || extenderBoot == null); ) + { + try + { + ClassPathExtenderFactory factory = iter.next(); + + if (extenderFramework == null) + { + try + { + extenderFramework = factory.getExtender(ExtensionManager.class.getClassLoader()); + } + catch (Throwable t) + { + // Ignore + } + } + if (extenderBoot == null) + { + try + { + extenderBoot = factory.getExtender(null); + } + catch (Throwable t) + { + // Ignore + } + } + } + catch (Throwable t) + { + // Ignore + } + } + + try + { + if (extenderFramework == null) + { + extenderFramework = new ClassPathExtenderFactory.DefaultClassLoaderExtender() + .getExtender(ExtensionManager.class.getClassLoader()); + } + } + catch (Throwable t) { + // Ignore + } + } + + m_extenderFramework = extenderFramework; + m_extenderBoot = extenderBoot; + } + + private final Logger m_logger; + private volatile ExtensionManagerRevision m_systemBundleRevision; + + private final List m_extensionTuples = Collections.synchronizedList(new ArrayList()); + + private final List m_resolvedExtensions = new CopyOnWriteArrayList(); + private final List m_unresolvedExtensions = new CopyOnWriteArrayList(); + private final List m_failedExtensions = new CopyOnWriteArrayList(); + + private static class ExtensionTuple + { + private final BundleActivator m_activator; + private final Bundle m_bundle; + private volatile boolean m_failed; + private volatile boolean m_started; + + public ExtensionTuple(BundleActivator activator, Bundle bundle) + { + m_activator = activator; + m_bundle = bundle; + } + } + + /** + * This constructor is used to create one instance per framework instance. + * The general approach is to have one private static instance that we register + * with the parent classloader and one instance per framework instance that + * keeps track of extension bundles and systembundle exports for that framework + * instance. + * + * @param logger the logger to use. + */ + ExtensionManager(Logger logger, Map configMap, Felix felix) + { + m_logger = logger; + + m_systemBundleRevision = new ExtensionManagerRevision(configMap, felix); + } + + protected BundleCapability buildNativeCapabilites(BundleRevisionImpl revision, Map configMap) { + String osArchitecture = (String) configMap.get(FelixConstants.FRAMEWORK_PROCESSOR); + String osName = (String) configMap.get(FelixConstants.FRAMEWORK_OS_NAME); + String osVersion = (String) configMap.get(FelixConstants.FRAMEWORK_OS_VERSION); + String userLang = (String) configMap.get(FelixConstants.FRAMEWORK_LANGUAGE); + Map attributes = new HashMap(); + + //Add all startup properties so we can match selection-filters + attributes.putAll(configMap); + + if( osArchitecture != null ) + { + attributes.put(NativeNamespace.CAPABILITY_PROCESSOR_ATTRIBUTE, NativeLibraryClause.getProcessorWithAliases(osArchitecture)); + } + + if( osName != null) + { + attributes.put(NativeNamespace.CAPABILITY_OSNAME_ATTRIBUTE, NativeLibraryClause.getOsNameWithAliases(osName)); + } + + if( osVersion != null) + { + attributes.put(NativeNamespace.CAPABILITY_OSVERSION_ATTRIBUTE, new Version(NativeLibraryClause.normalizeOSVersion(osVersion))); + } + + if( userLang != null) + { + attributes.put(NativeNamespace.CAPABILITY_LANGUAGE_ATTRIBUTE, userLang); + } + + return new BundleCapabilityImpl(revision, NativeNamespace.NATIVE_NAMESPACE, Collections. emptyMap(), attributes); + } + + @IgnoreJRERequirement + void updateRevision(Felix felix, Map configMap) + { + Map config = new HashMap(configMap); + Properties defaultProperties = Util.loadDefaultProperties(m_logger); + + Util.initializeJPMSEE(felix._getProperty("java.specification.version"), defaultProperties, m_logger); + + String sysprops = felix._getProperty(Constants.FRAMEWORK_SYSTEMPACKAGES); + + final Map> exports = Util.initializeJPMS(defaultProperties); + + if (exports != null && (sysprops == null || "true".equalsIgnoreCase(felix._getProperty(FelixConstants.USE_PROPERTY_SUBSTITUTION_IN_SYSTEMPACKAGES)))) + { + java.nio.file.FileSystem fs = java.nio.file.FileSystems.getFileSystem(URI.create("jrt:/")); + final ClassParser classParser = new ClassParser(); + final Set imports = new HashSet(); + for (Set moduleImport : exports.values()) + { + for (String pkg : moduleImport) + { + if (!pkg.startsWith("java.")) + { + imports.add(pkg); + } + } + } + for (final String moduleKey : exports.keySet()) + { + int idx = moduleKey.indexOf("@"); + String module = idx == -1 ? moduleKey : moduleKey.substring(0, idx); + if (felix._getProperty(module) == null && !exports.get(moduleKey).isEmpty() && defaultProperties.getProperty(module) == null) + { + final SortedMap> referred = new TreeMap>(); + if ("true".equalsIgnoreCase(felix._getProperty(FelixConstants.CALCULATE_SYSTEMPACKAGES_USES))) + { + try + { + Properties cachedProps = new Properties(); + File modulesDir = felix.getDataFile(felix, "modules"); + modulesDir.mkdirs(); + File cached = new File(modulesDir, moduleKey + ".properties"); + if (cached.isFile()) + { + FileInputStream input = new FileInputStream(cached); + cachedProps.load(new InputStreamReader(input, "UTF-8")); + input.close(); + for (Enumeration keys = cachedProps.propertyNames(); keys.hasMoreElements();) + { + String pkg = (String) keys.nextElement(); + referred.put(pkg, new TreeSet(Arrays.asList(cachedProps.getProperty(pkg).split(",")))); + } + } + else + { + java.nio.file.Path path = fs.getPath("modules", module.substring("felix.jpms.".length())); + java.nio.file.Files.walkFileTree(path, (java.nio.file.FileVisitor) Felix.class.getClassLoader().loadClass("org.apache.felix.framework.util.ClassFileVisitor") + .getConstructor(Set.class, Set.class, ClassParser.class, SortedMap.class).newInstance(imports, exports.get(moduleKey), classParser, referred)); + for (String pkg : referred.keySet()) + { + SortedSet uses = referred.get(pkg); + if (uses != null && !uses.isEmpty()) + { + cachedProps.setProperty(pkg, String.join(",", uses)); + } + } + OutputStream output = new FileOutputStream(cached); + cachedProps.store(new OutputStreamWriter(output, "UTF-8"), null); + output.close(); + } + } + catch (Throwable e) + { + m_logger.log(Logger.LOG_WARNING, "Exception calculating JPMS module exports", e); + } + } + + String pkgs = ""; + + for (String pkg : exports.get(moduleKey)) + { + pkgs += "," + pkg; + SortedSet uses = referred.get(pkg); + if (uses != null && !uses.isEmpty()) + { + pkgs += ";uses:=\""; + String sep = ""; + for (String u : uses) + { + pkgs += sep + u; + sep = ","; + } + pkgs += "\""; + } + pkgs += ";version=\"" + defaultProperties.getProperty("felix.detect.java.version") + "\""; + } + defaultProperties.put(module, pkgs); + } + } + } + + for (Map.Entry entry : defaultProperties.entrySet()) + { + if (!config.containsKey(entry.getKey())) + { + config.put(entry.getKey(), entry.getValue()); + } + } + + if(sysprops != null && "true".equalsIgnoreCase(felix._getProperty(FelixConstants.USE_PROPERTY_SUBSTITUTION_IN_SYSTEMPACKAGES))) + { + config.put(Constants.FRAMEWORK_SYSTEMPACKAGES, Util.getPropertyWithSubs(Util.toProperties(config), Constants.FRAMEWORK_SYSTEMPACKAGES)); + } + else if (sysprops == null) + { + config.put(Constants.FRAMEWORK_SYSTEMPACKAGES, Util.getPropertyWithSubs(Util.toProperties(config), Constants.FRAMEWORK_SYSTEMPACKAGES)); + } + + String syspropsExtra = felix._getProperty(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA); + if (syspropsExtra != null && "true".equalsIgnoreCase(felix._getProperty(FelixConstants.USE_PROPERTY_SUBSTITUTION_IN_SYSTEMPACKAGES))) + { + config.put(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, Util.getPropertyWithSubs(Util.toProperties(config), Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA)); + } + + String syscaps = felix._getProperty(Constants.FRAMEWORK_SYSTEMCAPABILITIES); + if(syscaps != null && "true".equalsIgnoreCase(felix._getProperty(FelixConstants.USE_PROPERTY_SUBSTITUTION_IN_SYSTEMPACKAGES))) + { + config.put(Constants.FRAMEWORK_SYSTEMCAPABILITIES, Util.getPropertyWithSubs(Util.toProperties(config), Constants.FRAMEWORK_SYSTEMCAPABILITIES)); + } + else if(syscaps == null) + { + config.put(Constants.FRAMEWORK_SYSTEMCAPABILITIES, Util.getPropertyWithSubs(Util.toProperties(config), Constants.FRAMEWORK_SYSTEMCAPABILITIES)); + } + + String syscapsExtra = felix._getProperty(Constants.FRAMEWORK_SYSTEMCAPABILITIES_EXTRA); + if (syscapsExtra != null && "true".equalsIgnoreCase(felix._getProperty(FelixConstants.USE_PROPERTY_SUBSTITUTION_IN_SYSTEMPACKAGES))) + { + config.put(Constants.FRAMEWORK_SYSTEMCAPABILITIES_EXTRA, Util.getPropertyWithSubs(Util.toProperties(config), Constants.FRAMEWORK_SYSTEMCAPABILITIES_EXTRA)); + } + + m_systemBundleRevision.update(config); + } + + public BundleRevisionImpl getRevision() + { + return m_systemBundleRevision; + } + + /** + * Add an extension bundle. The bundle will be added to the parent classloader + * and it's exported packages will be added to the module definition + * exports of this instance. Subsequently, they are available form the + * instance in it's role as content loader. + * + * @param bundle the extension bundle to add. + * @throws BundleException if extension bundles are not supported or this is + * not a framework extension. + * @throws SecurityException if the caller does not have the needed + * AdminPermission.EXTENSIONLIFECYCLE and security is enabled. + * @throws Exception in case something goes wrong. + */ + void addExtensionBundle(BundleImpl bundle) throws Exception + { + Object sm = System.getSecurityManager(); + if (sm != null) + { + ((SecurityManager) sm).checkPermission( + new AdminPermission(bundle, AdminPermission.EXTENSIONLIFECYCLE)); + + if (!((BundleProtectionDomain) bundle.getProtectionDomain()).impliesDirect(new AllPermission())) + { + throw new SecurityException("Extension Bundles must have AllPermission"); + } + } + + String directive = ManifestParser.parseExtensionBundleHeader((String) + ((BundleRevisionImpl) bundle.adapt(BundleRevision.class)) + .getHeaders().get(Constants.FRAGMENT_HOST)); + + if (!Constants.EXTENSION_FRAMEWORK.equals(directive)) + { + throw new BundleException("Unsupported Extension Bundle type: " + + directive, new UnsupportedOperationException( + "Unsupported Extension Bundle type!")); + } + else if (m_extenderFramework == null) + { + // We don't support extensions + m_logger.log(bundle, Logger.LOG_WARNING, + "Unable to add extension bundle - Maybe ClassLoader is not supported " + + "(on java9, try --add-opens=java.base/jdk.internal.loader=ALL-UNNAMED)?"); + + throw new UnsupportedOperationException( + "Unable to add extension bundle."); + } + + Content content = bundle.adapt(BundleRevisionImpl.class).getContent(); + final File file; + if (content instanceof JarContent) + { + file = ((JarContent) content).getFile(); + } + else if (content instanceof DirectoryContent) + { + file = ((DirectoryContent) content).getFile(); + } + else + { + file = null; + } + if (file == null) + { + // We don't support revision type for extension + m_logger.log(bundle, Logger.LOG_WARNING, + "Unable to add extension bundle - wrong revision type?"); + + throw new UnsupportedOperationException( + "Unable to add extension bundle."); + } + + BundleRevisionImpl bri = bundle.adapt(BundleRevisionImpl.class); + + bri.resolve(null); + + // we have to try again for all previously failed extensions because maybe they can now resolve. + m_unresolvedExtensions.addAll(m_failedExtensions); + m_failedExtensions.clear(); + m_unresolvedExtensions.add(bri); + } + + public synchronized List resolveExtensionBundles(Felix felix) + { + if (m_unresolvedExtensions.isEmpty()) + { + return Collections.emptyList(); + } + + // Collect the highest version of unresolved that are not already resolved by bsn + List extensions = new ArrayList(); + // Collect the unresolved that where filtered out as alternatives in case the highest version doesn't resolve + List alt = new ArrayList(); + + outer : for (BundleRevisionImpl revision : m_unresolvedExtensions) + { + // Already resolved by bsn? + for (BundleRevisionImpl existing : m_resolvedExtensions) + { + if (existing.getSymbolicName().equals(revision.getSymbolicName())) + { + // Then ignore it + continue outer; + } + } + // Otherwise, does a higher version exist by bsn? + for (BundleRevisionImpl other : m_unresolvedExtensions) + { + if ((revision != other) && (revision.getSymbolicName().equals(other.getSymbolicName())) && + revision.getVersion().compareTo(other.getVersion()) < 0) + { + // Add this one to alternatives and filter it + alt.add(revision); + continue outer; + } + } + + // no higher version and not resolved yet by bsn - try to resolve it + extensions.add(revision); + } + + // This will return all resolvable revisions with the wires they need + Map> wirings = findResolvableExtensions(extensions, alt); + + List result = new ArrayList(); + + for (Map.Entry> entry : wirings.entrySet()) + { + BundleRevisionImpl revision = entry.getKey(); + + // move this revision from unresolved to resolved + m_unresolvedExtensions.remove(revision); + m_resolvedExtensions.add(revision); + + BundleWire wire = new BundleWireImpl(revision, + revision.getDeclaredRequirements(BundleRevision.HOST_NAMESPACE).get(0), + m_systemBundleRevision, m_systemBundleRevision.getWiring().getCapabilities(BundleRevision.HOST_NAMESPACE).get(0)); + + try + { + revision.resolve(new BundleWiringImpl(m_logger, m_systemBundleRevision.m_configMap, null, revision, null, + Collections.singletonList(wire), Collections.EMPTY_MAP, Collections.EMPTY_MAP)); + } + catch (Exception ex) + { + m_logger.log(revision.getBundle(), Logger.LOG_ERROR, + "Error resolving extension bundle : " + revision.getBundle(), ex); + } + + felix.getDependencies().addDependent(wire); + + List caps = new ArrayList(); + for (BundleCapability cap : entry.getKey().getDeclaredCapabilities(null)) + { + if (!IDENTITY.contains(cap.getNamespace())) + { + caps.add(cap); + } + } + m_systemBundleRevision.appendCapabilities(caps); + for (BundleWire w : entry.getValue()) + { + if (!w.getRequirement().getNamespace().equals(BundleRevision.HOST_NAMESPACE) && + !w.getRequirement().getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE)) + { + ((BundleWiringImpl) w.getRequirer().getWiring()).addDynamicWire(w); + felix.getDependencies().addDependent(w); + } + } + + final File f; + Content revisionContent = revision.getContent(); + if (revisionContent instanceof JarContent) + { + f = ((JarContent) revisionContent).getFile(); + } + else + { + f = ((DirectoryContent) revisionContent).getFile(); + } + try + { + AccessController.doPrivileged(new PrivilegedExceptionAction() + { + @Override + public Void run() throws Exception { + m_extenderFramework.add(f); + return null; + } + }); + } + catch (Exception ex) + { + m_logger.log(revision.getBundle(), Logger.LOG_ERROR, + "Error adding extension bundle to framework classloader: " + revision.getBundle(), ex); + } + + felix.setBundleStateAndNotify(revision.getBundle(), Bundle.RESOLVED); + result.add(revision.getBundle()); + } + + // at this point, all revisions left in unresolved are not resolvable + m_failedExtensions.addAll(m_unresolvedExtensions); + m_unresolvedExtensions.clear(); + + return result; + } + + /** + * Start extension bundle if it has an activator + * + * @param felix the framework instance the extension bundle is installed in. + * @param bundle the extension bundle to start if it has a an extension bundle activator. + */ + void startExtensionBundle(Felix felix, BundleImpl bundle) + { + Map headers = bundle.adapt(BundleRevisionImpl.class).getHeaders(); + String activatorClass = (String) headers.get(Constants.EXTENSION_BUNDLE_ACTIVATOR); + boolean felixExtension = false; + if (activatorClass == null) + { + felixExtension = true; + activatorClass = (String) headers.get(FelixConstants.FELIX_EXTENSION_ACTIVATOR); + } + + if (activatorClass != null) + { + ExtensionTuple tuple = null; + try + { +// TODO: SECURITY - Should this consider security? + BundleActivator activator = (BundleActivator) + Felix.m_secureAction.getClassLoader(felix.getClass()).loadClass( + activatorClass.trim()).newInstance(); + + BundleContext context = felix._getBundleContext(); + + bundle.setBundleContext(context); + +// TODO: EXTENSIONMANAGER - This is kind of hacky, can we improve it? + if (!felixExtension) + { + tuple = new ExtensionTuple(activator, bundle); + m_extensionTuples.add(tuple); + } + else + { + felix.m_activatorList.add(activator); + } + + if ((felix.getState() == Bundle.ACTIVE) || (felix.getState() == Bundle.STARTING)) + { + if (tuple != null) + { + tuple.m_started = true; + } + Felix.m_secureAction.startActivator(activator, context); + } + } + catch (Throwable ex) + { + if (tuple != null) + { + tuple.m_failed = true; + } + felix.fireFrameworkEvent(FrameworkEvent.ERROR, bundle, + new BundleException("Unable to start Bundle", ex)); + + m_logger.log(bundle, Logger.LOG_WARNING, + "Unable to start Extension Activator", ex); + } + } + } + + void startPendingExtensionBundles(Felix felix) + { + for (int i = 0;i < m_extensionTuples.size();i++) + { + if (!m_extensionTuples.get(i).m_started) + { + m_extensionTuples.get(i).m_started = true; + try + { + Felix.m_secureAction.startActivator(m_extensionTuples.get(i).m_activator, felix._getBundleContext()); + } + catch (Throwable ex) + { + m_extensionTuples.get(i).m_failed = true; + + felix.fireFrameworkEvent(FrameworkEvent.ERROR, m_extensionTuples.get(i).m_bundle, + new BundleException("Unable to start Bundle", BundleException.ACTIVATOR_ERROR, ex)); + + m_logger.log(m_extensionTuples.get(i).m_bundle, Logger.LOG_WARNING, + "Unable to start Extension Activator", ex); + } + } + } + } + + void stopExtensionBundles(Felix felix) + { + for (int i = m_extensionTuples.size() - 1; i >= 0;i--) + { + if (m_extensionTuples.get(i).m_started && !m_extensionTuples.get(i).m_failed) + { + try + { + Felix.m_secureAction.stopActivator(m_extensionTuples.get(i).m_activator, felix._getBundleContext()); + } + catch (Throwable ex) + { + felix.fireFrameworkEvent(FrameworkEvent.ERROR, m_extensionTuples.get(i).m_bundle, + new BundleException("Unable to stop Bundle", BundleException.ACTIVATOR_ERROR, ex)); + + m_logger.log(m_extensionTuples.get(i).m_bundle, Logger.LOG_WARNING, + "Unable to stop Extension Activator", ex); + } + } + } + m_extensionTuples.clear(); + } + + public synchronized void removeExtensionBundles() + { + m_resolvedExtensions.clear(); + m_unresolvedExtensions.clear(); + m_failedExtensions.clear(); + } + + private Map> findResolvableExtensions(List extensions, List alt) + { + // The idea is to loop through the extensions and try to resolve all unresolved extension. If we can't resolve + // a given extension, we will call the method again with the extension in question removed or replaced if there + // is a replacement for it in alt. + // This resolve doesn't take into account that maybe a revision could be resolved with a revision from alt but + // not with the current extensions. In that case, it will be removed (assuming the current extension can be resolved) + // in other words, it will prefer to resolve the highest version of each extension over install order + Map> wires = new LinkedHashMap>(); + + for (BundleRevisionImpl bri : extensions) + { + List wi = new ArrayList(); + boolean resolved = true; + outer: for (BundleRequirement req : bri.getDeclaredRequirements(null)) + { + // first see if we can resolve from the system bundle + for (BundleCapability cap : m_systemBundleRevision.getWiring().getCapabilities(req.getNamespace())) + { + if (req.matches(cap)) + { + // we can, create the wire but in the case of an ee requirement, make it from the extension + wi.add(new BundleWireImpl( + req.getNamespace().equals(ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE) ? + bri : m_systemBundleRevision, req, m_systemBundleRevision, cap)); + + continue outer; + } + } + + // now loop through the resolved extensions + for (BundleRevisionImpl extension : m_resolvedExtensions) + { + // and check the caps that will not be lifted (i.e., identity) + for (BundleCapability cap : extension.getDeclaredCapabilities(req.getNamespace())) + { + if (req.matches(cap)) + { + // it was identity - hence, use the extension itself as provider + wi.add(new BundleWireImpl(m_systemBundleRevision, req, extension, cap)); + continue outer; + } + } + } + // now loop through the other extensions + for (BundleRevisionImpl extension : extensions) + { + for (BundleCapability cap : extension.getDeclaredCapabilities(req.getNamespace())) + { + if (req.matches(cap)) + { + // we can use a yet unresolved extension (resolved one are implicitly checked by the + // system bundle loop above as they would be attached. + wi.add(new BundleWireImpl(m_systemBundleRevision, req, + // lift identity + IDENTITY.contains(cap.getNamespace()) ? + extension : m_systemBundleRevision, cap)); + continue outer; + } + } + // and check the caps that will not be lifted (i.e., identity) + for (BundleCapability cap : extension.getDeclaredCapabilities(req.getNamespace())) + { + if (req.matches(cap)) + { + // it was identity - hence, use the extension itself as provider + wi.add(new BundleWireImpl(m_systemBundleRevision, req, extension, cap)); + continue outer; + } + } + } + // we couldn't find a provider - was it optional? + if (!((BundleRequirementImpl)req).isOptional()) + { + resolved = false; + break; + } + } + if(resolved) + { + wires.put(bri, wi); + } + else + { + // we failed to resolve this extension - try again without it. Yes, this throws away the work done + // up to this point + List next = new ArrayList(extensions); + List nextAlt = new ArrayList(); + + outer : for (BundleRevisionImpl replacement : alt) + { + if (bri.getSymbolicName().equals(replacement.getSymbolicName())) + { + for (BundleRevisionImpl other : alt) + { + if ((replacement != other) && (replacement.getSymbolicName().equals(other.getSymbolicName())) && + replacement.getVersion().compareTo(other.getVersion()) < 0) + { + nextAlt.add(replacement); + continue outer; + } + } + next.set(next.indexOf(bri), replacement); + break; + } + nextAlt.add(replacement); + } + + next.remove(bri); + + return next.isEmpty() ? Collections.EMPTY_MAP : findResolvableExtensions(next, nextAlt); + } + } + return wires; + } + + public void close() + { + // Do nothing on close, since we have nothing open. + } + + public Enumeration getEntries() + { + return new Enumeration() + { + public boolean hasMoreElements() + { + return false; + } + + public Object nextElement() throws NoSuchElementException + { + throw new NoSuchElementException(); + } + }; + } + + public boolean hasEntry(String name) { + return false; + } + + public byte[] getEntryAsBytes(String name) + { + return null; + } + + public InputStream getEntryAsStream(String name) throws IOException + { + return null; + } + + public Content getEntryAsContent(String name) + { + return null; + } + + public String getEntryAsNativeLibrary(String name) + { + return null; + } + + public URL getEntryAsURL(String name) + { + return null; + } + + // + // Utility methods. + // + + class ExtensionManagerRevision extends BundleRevisionImpl + { + private volatile Map m_configMap; + private final Map m_headerMap = new StringMap(); + private volatile List m_capabilities = Collections.EMPTY_LIST; + private volatile Version m_version; + private volatile BundleWiring m_wiring; + + ExtensionManagerRevision(Map configMap, Felix felix) + { + super(felix, "0"); + + m_configMap = configMap; + +// TODO: FRAMEWORK - Not all of this stuff really belongs here + // Populate system bundle header map. + m_headerMap.put(FelixConstants.BUNDLE_VERSION, + m_configMap.get(FelixConstants.FELIX_VERSION_PROPERTY)); + m_headerMap.put(FelixConstants.BUNDLE_SYMBOLICNAME, + FelixConstants.SYSTEM_BUNDLE_SYMBOLICNAME); + m_headerMap.put(FelixConstants.BUNDLE_NAME, "System Bundle"); + m_headerMap.put(FelixConstants.BUNDLE_DESCRIPTION, + "This bundle is system specific; it implements various system services."); + m_headerMap.put(FelixConstants.EXPORT_SERVICE, + "org.osgi.service.packageadmin.PackageAdmin," + + "org.osgi.service.startlevel.StartLevel," + + "org.osgi.service.url.URLHandlers"); + + + m_headerMap.put(FelixConstants.BUNDLE_MANIFESTVERSION, "2"); + + + m_version = new Version((String) m_configMap.get(FelixConstants.FELIX_VERSION_PROPERTY)); + + try + { + ManifestParser mp = new ManifestParser( + m_logger, m_configMap, this, m_headerMap); + List caps = ManifestParser.aliasSymbolicName(mp.getCapabilities(), this); + caps.add(buildNativeCapabilites(this, m_configMap)); + appendCapabilities(caps); + m_headerMap.put(Constants.EXPORT_PACKAGE, convertCapabilitiesToHeaders(caps)); + } + catch (Exception ex) + { + m_capabilities = Collections.EMPTY_LIST; + m_logger.log( + Logger.LOG_ERROR, + "Error parsing system bundle statement", ex); + } + } + + private void update(Map configMap) + { + Properties configProps = Util.toProperties(configMap); + // The system bundle exports framework packages as well as + // arbitrary user-defined packages from the system class path. + // We must construct the system bundle's export metadata. + // Get configuration property that specifies which class path + // packages should be exported by the system bundle. + String syspkgs = configProps.getProperty(FelixConstants.FRAMEWORK_SYSTEMPACKAGES); + + syspkgs = (syspkgs == null) ? "" : syspkgs; + + // If any extra packages are specified, then append them. + String pkgextra = configProps.getProperty(FelixConstants.FRAMEWORK_SYSTEMPACKAGES_EXTRA); + + syspkgs = ((pkgextra == null) || (pkgextra.trim().length() == 0)) + ? syspkgs : syspkgs + (pkgextra.trim().startsWith(",") ? pkgextra : "," + pkgextra); + + if (syspkgs.startsWith(",")) + { + syspkgs = syspkgs.substring(1); + } + + m_headerMap.put(FelixConstants.EXPORT_PACKAGE, syspkgs); + + // The system bundle alsp provides framework generic capabilities + // as well as arbitrary user-defined generic capabilities. We must + // construct the system bundle's capabilities metadata. Get the + // configuration property that specifies which capabilities should + // be provided by the system bundle. + String syscaps = configProps.getProperty(Constants.FRAMEWORK_SYSTEMCAPABILITIES); + + syscaps = (syscaps == null) ? "" : syscaps; + + // If any extra capabilities are specified, then append them. + String capextra = configProps.getProperty(Constants.FRAMEWORK_SYSTEMCAPABILITIES_EXTRA); + + syscaps = ((capextra == null) || (capextra.trim().length() == 0)) + ? syscaps : syscaps + (capextra.trim().startsWith(",") ? capextra : "," + capextra); + + m_headerMap.put(FelixConstants.PROVIDE_CAPABILITY, syscaps); + + try + { + ManifestParser mp = new ManifestParser( + m_logger, m_configMap, this, m_headerMap); + List caps = ManifestParser.aliasSymbolicName(mp.getCapabilities(), this); + caps.add(buildNativeCapabilites(this, m_configMap)); + m_capabilities = Collections.EMPTY_LIST; + appendCapabilities(caps); + m_headerMap.put(Constants.EXPORT_PACKAGE, convertCapabilitiesToHeaders(caps)); + } + catch (Exception ex) + { + m_capabilities = Collections.EMPTY_LIST; + m_logger.log( + Logger.LOG_ERROR, + "Error parsing system bundle statement.", ex); + } + } + + private void appendCapabilities(List caps) + { + List newCaps = new ArrayList(m_capabilities.size() + caps.size()); + newCaps.addAll(m_capabilities); + newCaps.addAll(caps); + m_capabilities = Util.newImmutableList(newCaps); + } + + private String convertCapabilitiesToHeaders(List caps) + { + StringBuilder exportSB = new StringBuilder(); + + for (BundleCapability cap : caps) + { + if (cap.getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE)) + { + // Add a comma separate if there is an existing package. + if (exportSB.length() > 0) + { + exportSB.append(", "); + } + + // Append exported package information. + exportSB.append(cap.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE)); + for (Entry entry : cap.getDirectives().entrySet()) + { + exportSB.append("; "); + exportSB.append(entry.getKey()); + exportSB.append(":=\""); + exportSB.append(entry.getValue()); + exportSB.append("\""); + } + for (Entry entry : cap.getAttributes().entrySet()) + { + if (!entry.getKey().equals(BundleRevision.PACKAGE_NAMESPACE) + && !entry.getKey().equals(Constants.BUNDLE_SYMBOLICNAME_ATTRIBUTE) + && !entry.getKey().equals(Constants.BUNDLE_VERSION_ATTRIBUTE)) + { + exportSB.append("; "); + exportSB.append(entry.getKey()); + exportSB.append("=\""); + exportSB.append(entry.getValue()); + exportSB.append("\""); + } + } + } + } + + return exportSB.toString(); + } + + @Override + public Map getHeaders() + { + return Util.newImmutableMap(m_headerMap); + } + + @Override + public List getDeclaredCapabilities(String namespace) + { + List caps = m_capabilities; + List result; + if (namespace != null) + { + result = new ArrayList(); + for (BundleCapability cap : caps) + { + if (cap.getNamespace().equals(namespace)) + { + result.add(cap); + } + } + } + else + { + result = new ArrayList(m_capabilities); + } + return result; + } + + @Override + public String getSymbolicName() + { + return FelixConstants.SYSTEM_BUNDLE_SYMBOLICNAME; + } + + @Override + public Version getVersion() + { + return m_version; + } + + @Override + public void close() + { + // Nothing needed here. + } + + @Override + public Content getContent() + { + return ExtensionManager.this; + } + + @Override + public URL getEntry(String name) + { + // There is no content for the system bundle, so return null. + return null; + } + + @Override + public boolean hasInputStream(int index, String urlPath) + { + return (getClass().getClassLoader().getResource(urlPath) != null); + } + + @Override + public InputStream getInputStream(int index, String urlPath) + { + return getClass().getClassLoader().getResourceAsStream(urlPath); + } + + @Override + public URL getLocalURL(int index, String urlPath) + { + return getClass().getClassLoader().getResource(urlPath); + } + + @Override + public void resolve(BundleWiringImpl wire) + { + try + { + m_wiring = new ExtensionManagerWiring( + m_logger, m_configMap, this); + } + catch (Exception ex) + { + // This should never happen. + } + } + + @Override + public BundleWiring getWiring() + { + return m_wiring; + } + } + + class ExtensionManagerWiring extends BundleWiringImpl + { + ExtensionManagerWiring( + Logger logger, Map configMap, BundleRevisionImpl revision) + throws Exception + { + super(logger, configMap, null, revision, + null, Collections.EMPTY_LIST, null, null); + } + + @Override + public ClassLoader getClassLoader() + { + return getClass().getClassLoader(); + } + + @Override + public List getCapabilities(String namespace) + { + return m_systemBundleRevision.getDeclaredCapabilities(namespace); + } + + @Override + public List getNativeLibraries() + { + return Collections.EMPTY_LIST; + } + + @Override + public Class getClassByDelegation(String name) throws ClassNotFoundException + { + return getClass().getClassLoader().loadClass(name); + } + + @Override + public URL getResourceByDelegation(String name) + { + return getClass().getClassLoader().getResource(name); + } + + @Override + public Enumeration getResourcesByDelegation(String name) + { + try + { + return getClass().getClassLoader().getResources(name); + } + catch (IOException ex) + { + return null; + } + } + + @Override + public void dispose() + { + // Nothing needed here. + } + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/FakeURLStreamHandler.java b/framework/src/main/java/org/apache/felix/framework/FakeURLStreamHandler.java index 8d9047e9251..4f368afb109 100644 --- a/framework/src/main/java/org/apache/felix/framework/FakeURLStreamHandler.java +++ b/framework/src/main/java/org/apache/felix/framework/FakeURLStreamHandler.java @@ -1,18 +1,20 @@ -/* - * Copyright 2005 The Apache Software Foundation +/* + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework; @@ -34,6 +36,6 @@ class FakeURLStreamHandler extends URLStreamHandler { protected URLConnection openConnection(URL url) throws IOException { - return null; + throw new IOException("FakeURLStreamHandler can not be used!"); } } diff --git a/framework/src/main/java/org/apache/felix/framework/Felix.java b/framework/src/main/java/org/apache/felix/framework/Felix.java index 1349530b1ef..46792c51f6f 100644 --- a/framework/src/main/java/org/apache/felix/framework/Felix.java +++ b/framework/src/main/java/org/apache/felix/framework/Felix.java @@ -1,187 +1,338 @@ /* - * Copyright 2006 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework; -import java.io.*; +import org.apache.felix.framework.BundleWiringImpl.BundleClassLoader; +import org.apache.felix.framework.ServiceRegistry.ServiceRegistryCallbacks; +import org.apache.felix.framework.cache.BundleArchive; +import org.apache.felix.framework.cache.BundleCache; +import org.apache.felix.framework.capabilityset.CapabilitySet; +import org.apache.felix.framework.capabilityset.SimpleFilter; +import org.apache.felix.framework.ext.SecurityProvider; +import org.apache.felix.framework.util.FelixConstants; +import org.apache.felix.framework.util.ListenerInfo; +import org.apache.felix.framework.util.MapToDictionary; +import org.apache.felix.framework.util.SecureAction; +import org.apache.felix.framework.util.ShrinkableCollection; +import org.apache.felix.framework.util.StringMap; +import org.apache.felix.framework.util.ThreadGate; +import org.apache.felix.framework.util.Util; +import org.apache.felix.framework.util.manifestparser.NativeLibraryClause; +import org.apache.felix.framework.wiring.BundleRequirementImpl; +import org.osgi.framework.AdminPermission; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.BundleException; +import org.osgi.framework.BundleListener; +import org.osgi.framework.BundleReference; +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.PackagePermission; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceException; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServicePermission; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.Version; +import org.osgi.framework.launch.Framework; +import org.osgi.framework.namespace.HostNamespace; +import org.osgi.framework.startlevel.FrameworkStartLevel; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleRevisions; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.framework.wiring.FrameworkWiring; +import org.osgi.resource.Requirement; +import org.osgi.service.packageadmin.ExportedPackage; +import org.osgi.service.resolver.ResolutionException; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.MalformedURLException; import java.net.URL; import java.net.URLStreamHandler; -import java.security.CodeSource; -import java.security.ProtectionDomain; -import java.util.*; - -import org.apache.felix.framework.cache.*; -import org.apache.felix.framework.searchpolicy.*; -import org.apache.felix.framework.util.*; -import org.apache.felix.moduleloader.*; -import org.osgi.framework.*; -import org.osgi.service.packageadmin.ExportedPackage; -import org.osgi.service.startlevel.StartLevel; - -public class Felix +import java.security.AccessControlException; +import java.security.Permission; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.SortedSet; +import java.util.StringTokenizer; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.WeakHashMap; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +public class Felix extends BundleImpl implements Framework { - // Logging related member variables. - private Logger m_logger = new Logger(); - // Config properties. - private PropertyResolver m_config = new ConfigImpl(); - // Configuration properties passed into constructor. - private MutablePropertyResolver m_configMutable = null; + // The secure action used to do privileged calls + static final SecureAction m_secureAction = new SecureAction(); + + // The extension manager to handle extension bundles + private final ExtensionManager m_extensionManager; - // MODULE FACTORY. - private IModuleFactory m_factory = null; - private R4SearchPolicyCore m_policyCore = null; + // Framework wiring object. + private final FrameworkWiringImpl m_fwkWiring; + private final FrameworkStartLevelImpl m_fwkStartLevel; - // Object used as a lock when calculating which bundles - // when performing an operation on one or more bundles. - private Object[] m_bundleLock = new Object[0]; + // Logging related member variables. + private final Logger m_logger; + // Immutable config properties. + private final Map m_configMap; + // Mutable configuration properties passed into constructor. + private final Map m_configMutableMap; + + // Resolver and resolver state. + private final StatefulResolver m_resolver; + + // Lock object used to determine if an individual bundle + // lock or the global lock can be acquired. + private final ReentrantLock m_bundleLock = new ReentrantLock(true); + private final Condition m_bundleLockCondition = m_bundleLock.newCondition(); + + // Keeps track of threads wanting to acquire the global lock. + private final List m_globalLockWaitersList = new ArrayList(); + // The thread currently holding the global lock. + private Thread m_globalLockThread = null; + // How many times the global lock was acquired by the thread holding + // the global lock; if this value is zero, then it means the global + // lock is free. + private int m_globalLockCount = 0; // Maps a bundle location to a bundle location; // used to reserve a location when installing a bundle. - private Map m_installRequestMap = null; + private final Map m_installRequestMap = new HashMap(); // This lock must be acquired to modify m_installRequestMap; // to help avoid deadlock this lock as priority 1 and should // be acquired before locks with lower priority. - private Object[] m_installRequestLock_Priority1 = new Object[0]; - - // Maps a bundle location to a bundle. - private HashMap m_installedBundleMap = null; - // This lock must be acquired to modify m_installedBundleMap; - // to help avoid deadlock this lock as priority 2 and should - // be acquired before locks with lower priority. - private Object[] m_installedBundleLock_Priority2 = new Object[0]; - + private final Object[] m_installRequestLock_Priority1 = new Object[0]; + + // Contains two maps, one mapping a String bundle location to a bundle + // and the other mapping a Long bundle identifier to a bundle. + // CONCURRENCY: Access guarded by the global lock for writes, + // but no lock for reads since it is copy on write. + private volatile Map[] m_installedBundles; + private static final int LOCATION_MAP_IDX = 0; + private static final int IDENTIFIER_MAP_IDX = 1; // An array of uninstalled bundles before a refresh occurs. - private BundleImpl[] m_uninstalledBundles = null; - // This lock must be acquired to modify m_uninstalledBundles; - // to help avoid deadlock this lock as priority 3 and should - // be acquired before locks with lower priority. - private Object[] m_uninstalledBundlesLock_Priority3 = new Object[0]; - - // Status flag for framework. - public static final int INITIAL_STATUS = -1; - public static final int RUNNING_STATUS = 0; - public static final int STARTING_STATUS = 1; - public static final int STOPPING_STATUS = 2; - private int m_frameworkStatus = INITIAL_STATUS; + // CONCURRENCY: Access guarded by the global lock for writes, + // but no lock for reads since it is copy on write. + private volatile List m_uninstalledBundles; + // Object to keep track of dependencies among bundle revisions. + private final BundleRevisionDependencies m_dependencies = + new BundleRevisionDependencies(); // Framework's active start level. - private int m_activeStartLevel = - FelixConstants.FRAMEWORK_INACTIVE_STARTLEVEL; - - // Local file system cache. + private volatile int m_activeStartLevel = FelixConstants.FRAMEWORK_INACTIVE_STARTLEVEL; + // Framework's target start level. + // Normally the target start will equal the active start level, except + // when the start level is changing, in which case the target start level + // will report the new start level while the active start level will report + // the old start level. Once the start level change is complete, the two + // will become equal again. + private volatile int m_targetStartLevel = FelixConstants.FRAMEWORK_INACTIVE_STARTLEVEL; + // Keep track of bundles currently being processed by start level thread. + private final SortedSet m_startLevelBundles = + new TreeSet(); + + // Local bundle cache. private BundleCache m_cache = null; + // System bundle activator list. + List m_activatorList = null; + // Next available bundle identifier. private long m_nextId = 1L; - private Object m_nextIdLock = new Object[0]; - - // List of event listeners. - private EventDispatcher m_dispatcher = null; + private final Object m_nextIdLock = new Object[0]; // Service registry. - private ServiceRegistry m_registry = null; + private final ServiceRegistry m_registry; + + // List of event listeners. + private final EventDispatcher m_dispatcher; // Reusable bundle URL stream handler. - private URLStreamHandler m_bundleStreamHandler = null; + private final URLStreamHandler m_bundleStreamHandler; - // The secure action used to do privileged calls - private SecureAction m_secureAction = new SecureAction(); + // Boot package delegation. + private final String[] m_bootPkgs; + private final boolean[] m_bootPkgWildcards; + + // Shutdown gate. + private volatile ThreadGate m_shutdownGate = null; - private Collection m_trustedCaCerts = null; + // Security Manager created by the framework + private SecurityManager m_securityManager = null; + + // Do we need to consult the default java security policy if no security provider is present? + private volatile boolean m_securityDefaultPolicy; /** *

      - * This method starts the framework instance; instances of the framework - * are dormant until this method is called. The caller may also provide - * MutablePropertyResolver implementations that the instance will - * use to obtain configuration or framework properties. Configuration - * properties are used internally by the framework and its extensions to alter - * its default behavior. Framework properties are used by bundles - * and are accessible from BundleContext.getProperty(). + * This constructor creates a framework instance with a specified Map + * of configuration properties. Configuration properties are used internally + * by the framework to alter its default behavior. The configuration properties + * should have a String key and an Object value. The passed + * in Map is copied by the framework and all keys are converted to + * Strings. *

      *

      - * Configuration properties are the sole means to configure the framework's - * default behavior; the framework does not refer to any system properties for - * configuration information. If a MutablePropertyResolver is - * supplied to this method for configuration properties, then the framework will - * consult the MutablePropertyResolver instance for any and all - * configuration properties. It is possible to specify a null - * configuration property resolver, in which case the framework will use its - * default behavior in all cases. However, if the - * DefaulBundleCache - * is used, then at a minimum a profile name or profile directory must - * be specified. + * Configuration properties are generally the sole means to configure the + * framework's default behavior; the framework does not typically refer to + * any system properties for configuration information. If a Map is + * supplied to this method for configuration properties, then the framework + * will consult the Map instance for any and all configuration + * properties. It is possible to specify a null for the configuration + * property map, in which case the framework will use its default behavior + * in all cases. *

      *

      - * The following configuration properties can be specified: + * The following configuration properties can be specified (properties starting + * with "felix" are specific to Felix, while those starting with + * "org.osgi" are standard OSGi properties): *

      *
        - *
      • felix.auto.install.<n> - Space-delimited list of - * bundles to automatically install into start level n when - * the framework is started. Append a specific start level to this - * property name to assign the bundles' start level - * (e.g., felix.auto.install.2). + *
      • org.osgi.framework.storage - Sets the directory to use as + * the bundle cache; by default bundle cache directory is + * felix-cache in the current working directory. The value + * should be a valid directory name. The directory name can be either absolute + * or relative. Relative directory names are relative to the current working + * directory. The specified directory will be created if it does + * not exist. + *
      • + *
      • org.osgi.framework.storage.clean - Determines whether the + * bundle cache is flushed. The value can either be "none" + * or "onFirstInit", where "none" does not flush + * the bundle cache and "onFirstInit" flushes the bundle + * cache when the framework instance is first initialized. The default + * value is "none". + *
      • + *
      • felix.cache.rootdir - Sets the root directory to use to + * calculate the bundle cache directory for relative directory names. If + * org.osgi.framework.storage is set to a relative name, by + * default it is relative to the current working directory. If this + * property is set, then it will be calculated as being relative to + * the specified root directory. + *
      • + *
      • felix.cache.filelimit - The integer value of this string + * sets an upper limit on how many files the cache will open. The default + * value is zero, which means there is no limit. + *
      • + *
      • felix.cache.locking - Enables or disables bundle cache locking, + * which is used to prevent concurrent access to the bundle cache. This is + * enabled by default, but on older/smaller JVMs file channel locking is + * not available; set this property to false to disable it. + *
      • + *
      • felix.cache.bufsize - Sets the buffer size to be used by + * the cache; the default value is 4096. The integer value of this + * string provides control over the size of the internal buffer of the + * disk cache for performance reasons. + *
      • + *
      • org.osgi.framework.system.packages - Specifies a + * comma-delimited list of packages that should be exported via the + * System Bundle from the parent class loader. The framework will set + * this to a reasonable default. If the value is specified, it + * replaces any default value. + *
      • + *
      • org.osgi.framework.system.packages.extra - Specifies a + * comma-delimited list of packages that should be exported via the + * System Bundle from the parent class loader in addition to the + * packages in org.osgi.framework.system.packages. The default + * value is empty. If a value is specified, it is appended to the list + * of default or specified packages in + * org.osgi.framework.system.packages. + *
      • + *
      • org.osgi.framework.bootdelegation - Specifies a + * comma-delimited list of packages that should be made implicitly + * available to all bundles from the parent class loader. It is + * recommended not to use this property since it breaks modularity. + * The default value is empty. + *
      • + *
      • felix.systembundle.activators - A List of + * BundleActivator instances that are started/stopped when + * the System Bundle is started/stopped. The specified instances will + * receive the System Bundle's BundleContext when invoked. *
      • - *
      • felix.auto.start.<n> - Space-delimited list of - * bundles to automatically install and start into start level - * n when the framework is started. Append a - * specific start level to this property name to assign the - * bundles' start level(e.g., felix.auto.start.2). + *
      • felix.log.logger - An instance of Logger that the + * framework uses as its default logger. *
      • - *
      • felix.startlevel.framework - The initial start level - * of the framework once it starts execution; the default + *
      • felix.log.level - An integer value indicating the degree + * of logging reported by the framework; the higher the value the more + * logging is reported. If zero ('0') is specified, then logging is + * turned off completely. The log levels match those specified in the + * OSGi Log Service (i.e., 1 = error, 2 = warning, 3 = information, + * and 4 = debug). The default value is 1. + *
      • + *
      • org.osgi.framework.startlevel.beginning - The initial + * start level of the framework once it starts execution; the default * value is 1. *
      • *
      • felix.startlevel.bundle - The default start level for * newly installed bundles; the default value is 1. *
      • - *
      • framework.service.urlhandlers - Flag to indicate whether + *
      • felix.service.urlhandlers - Flag to indicate whether * to activate the URL Handlers service for the framework instance; * the default value is "true". Activating the URL Handlers * service will result in the URL.setURLStreamHandlerFactory() * and URLConnection.setContentHandlerFactory() being called. *
      • - *
      • felix.embedded.execution - Flag to indicate whether - * the framework is embedded into a host application; the default value is - * "false". If this flag is "true" then the framework - * will not called System.exit() upon termination. + *
      • felix.fragment.validation - Determines if installing + * unsupported fragment bundles throws an exception or logs a warning. + * Possible values are "exception" or "warning". The + * default value is "exception". *
      • - *
      • felix.strict.osgi - Flag to indicate whether the framework is - * running in strict OSGi mode; the default value is "true". - * If this flag is "false" it enables a non-OSGi-compliant - * feature by persisting BundleActivators that implement - * Serializable. This feature is not recommended since - * it is non-compliant. + *
      • felix.security.defaultpolicy - Flag to indicate whether + * to consult the default java securtiy policy if no security extension + * is present. The default value is "false". *
      • *
      *

      - * Besides the above framework configuration properties, it is also - * possible to specify properties for the bundle cache. The available - * bundle cache properties depend on the cache implementation - * being used. For the properties of the default bundle cache, refer to the - * DefaulBundleCache - * API documentation. - *

      - *

      - * Framework properties are somewhat misnamed, since they are not used by - * the framework, but by bundles via BundleContext.getProperty(). - * Please refer to bundle documentation of your specific bundle for any - * available properties. - *

      - *

      * The Main class implements some * functionality for default property file handling, which makes it * possible to specify configuration properties and framework properties @@ -190,620 +341,1269 @@ public class Felix * able to take advantage of the features it provides; refer to its * class documentation for more information. *

      + *

      + * The framework is not actually started until the start() method + * is called. + *

      * - * @param configMutable An object for obtaining configuration properties, - * may be null. - * @param frameworkProps An object for obtaining framework properties, + * @param configMap A map for obtaining configuration properties, * may be null. - * @param activatorList A list of System Bundle activators. **/ - public synchronized void start(MutablePropertyResolver configMutable, - List activatorList) + public Felix(Map configMap) { - start(configMutable, activatorList, (Collection) null); - } - - public synchronized void start( - MutablePropertyResolver configMutable, - List activatorList, Collection trustedCaCerts) - { - if (m_frameworkStatus != INITIAL_STATUS) + super(); + // Copy the configuration properties; convert keys to strings. + m_configMutableMap = new StringMap(); + if (configMap != null) { - throw new IllegalStateException("Invalid framework status: " + m_frameworkStatus); + for (Iterator i = configMap.entrySet().iterator(); i.hasNext(); ) + { + Map.Entry entry = (Map.Entry) i.next(); + m_configMutableMap.put(entry.getKey().toString(), entry.getValue()); + } } - m_trustedCaCerts = trustedCaCerts; - - // The framework is now in its startup sequence. - m_frameworkStatus = STARTING_STATUS; - // Initialize member variables. - m_factory = null; - m_configMutable = (configMutable == null) - ? new MutablePropertyResolverImpl(new StringMap(false)) : configMutable; - m_activeStartLevel = FelixConstants.FRAMEWORK_INACTIVE_STARTLEVEL; - m_installRequestMap = new HashMap(); - m_installedBundleMap = new HashMap(); - m_uninstalledBundles = null; - m_cache = null; - m_nextId = 1L; - m_dispatcher = null; - m_bundleStreamHandler = new URLHandlersBundleStreamHandler(this); - m_registry = new ServiceRegistry(m_logger); + // Get any system bundle activators. + m_activatorList = (List) m_configMutableMap.remove(FelixConstants.SYSTEMBUNDLE_ACTIVATORS_PROP); + m_activatorList = (m_activatorList == null) ? new ArrayList() : new ArrayList(m_activatorList); - // Add a listener to the service registry; this is - // used to distribute service registry events to - // service listeners. - m_registry.addServiceListener(new ServiceListener() { - public void serviceChanged(ServiceEvent event) - { - fireServiceEvent(event); - } - }); + m_configMap = createUnmodifiableMap(m_configMutableMap); + // Create logger with appropriate log level. Even though the + // logger needs the system bundle's context for tracking log + // services, it is created now because it is needed before + // the system bundle is activated. The system bundle's context + // will be set in the init() method after the system bundle + // is activated. + if (m_configMutableMap.get(FelixConstants.LOG_LOGGER_PROP) != null) + { + m_logger = (Logger) m_configMutableMap.get(FelixConstants.LOG_LOGGER_PROP); + } + else + { + m_logger = new Logger(); + } try { - m_cache = new BundleCache(m_config, m_logger, m_trustedCaCerts); + m_logger.setLogLevel( + Integer.parseInt( + (String) m_configMutableMap.get(FelixConstants.LOG_LEVEL_PROP))); } - catch (Exception ex) + catch (NumberFormatException ex) { - System.err.println("Error creating bundle cache:"); - ex.printStackTrace(); + // Ignore and just use the default logging level. + } - // Only shutdown the JVM if the framework is running stand-alone. - String embedded = m_config.get( - FelixConstants.EMBEDDED_EXECUTION_PROP); - boolean isEmbedded = (embedded == null) - ? false : embedded.equals("true"); - if (!isEmbedded) - { - m_secureAction.exit(-1); - } - else + // Initialize framework properties. + initializeFrameworkProperties(); + + // Read the boot delegation property and parse it. + String s = (m_configMap == null) + ? null + : (String) m_configMap.get(Constants.FRAMEWORK_BOOTDELEGATION); + s = (s == null) ? "java.*" : s + ",java.*"; + StringTokenizer st = new StringTokenizer(s, " ,"); + m_bootPkgs = new String[st.countTokens()]; + m_bootPkgWildcards = new boolean[m_bootPkgs.length]; + for (int i = 0; i < m_bootPkgs.length; i++) + { + s = st.nextToken(); + if (s.equals("*") || s.endsWith(".*")) { - throw new RuntimeException(ex.toString()); + m_bootPkgWildcards[i] = true; + s = s.substring(0, s.length() - 1); } + m_bootPkgs[i] = s; } - // Create search policy for module loader. - m_policyCore = new R4SearchPolicyCore(m_logger, m_config); + //Initialize Native Library Aliases + NativeLibraryClause.initializeNativeAliases(m_configMap); - // Add a resolver listener to the search policy - // so that we will be notified when modules are resolved - // in order to update the bundle state. - m_policyCore.addResolverListener(new ResolveListener() { - public void moduleResolved(ModuleEvent event) - { - BundleImpl bundle = null; - try - { - long id = Util.getBundleIdFromModuleId( - event.getModule().getId()); - if (id >= 0) - { - // Update the bundle's state to resolved when the - // current module is resolved; just ignore resolve - // events for older revisions since this only occurs - // when an update is done on an unresolved bundle - // and there was no refresh performed. - bundle = (BundleImpl) getBundle(id); + // Read the security default policy property + m_securityDefaultPolicy = "true".equals(getProperty(FelixConstants.SECURITY_DEFAULT_POLICY)); - // Lock the bundle first. - try - { - acquireBundleLock(bundle); - if (bundle.getInfo().getCurrentModule() == event.getModule()) - { - bundle.getInfo().setState(Bundle.RESOLVED); - fireBundleEvent(BundleEvent.RESOLVED, bundle); - } - } - finally - { - releaseBundleLock(bundle); - } - } - } - catch (NumberFormatException ex) - { - // Ignore. - } - } + // Create default bundle stream handler. + m_bundleStreamHandler = new URLHandlersBundleStreamHandler(this, m_secureAction); - public void moduleUnresolved(ModuleEvent event) + // Create service registry. + m_registry = new ServiceRegistry(m_logger, new ServiceRegistryCallbacks() { + @Override + public void serviceChanged(ServiceEvent event, Dictionary oldProps) { - // We can ignore this, because the only time it - // should happen is when a refresh occurs. The - // refresh operation resets the bundle's state - // by calling BundleInfo.reset(), thus it is not - // necessary for us to reset the bundle's state - // here. + fireServiceEvent(event, oldProps); } }); - m_factory = new ModuleFactoryImpl(m_logger); - m_policyCore.setModuleFactory(m_factory); + // Create a resolver and its state. + m_resolver = new StatefulResolver(this, m_registry); - // Initialize event dispatcher. - m_dispatcher = new EventDispatcher(m_logger); - - // Initialize framework properties. - initializeFrameworkProperties(); + // Create the extension manager, which we will use as the + // revision for the system bundle. + m_extensionManager = new ExtensionManager(m_logger, m_configMap, this); - // Before we reload any cached bundles, let's create a system - // bundle that is responsible for providing specific container - // related services. - SystemBundle systembundle = null; try { - // Create a simple bundle info for the system bundle. - BundleInfo info = new BundleInfo( - m_logger, new SystemBundleArchive(), null); - systembundle = new SystemBundle(this, info, activatorList, - m_secureAction); - // Create a module for the system bundle. - IModuleDefinition md = new ModuleDefinition( - systembundle.getExports(), null, null, null); - systembundle.getInfo().addModule(m_factory.createModule("0", md)); - systembundle.getContentLoader().setSearchPolicy( - new R4SearchPolicy( - m_policyCore, systembundle.getInfo().getCurrentModule())); - m_factory.setContentLoader( - systembundle.getInfo().getCurrentModule(), - systembundle.getContentLoader()); - m_factory.setSecurityContext( - systembundle.getInfo().getCurrentModule(), - systembundle.getClass().getProtectionDomain()); - - m_installedBundleMap.put( - systembundle.getInfo().getLocation(), systembundle); - - // Manually resolve the System Bundle, which will cause its - // state to be set to RESOLVED. - try - { - m_policyCore.resolve(systembundle.getInfo().getCurrentModule()); - } - catch (ResolveException ex) - { - // This should never happen. - throw new BundleException( - "Unresolved package in System Bundle:" - + ex.getPackage()); - } - - // Start the system bundle; this will set its state - // to STARTING, we must set its state to ACTIVE after - // all bundles are restarted below according to the spec. - systembundle.start(); + addRevision(m_extensionManager.getRevision()); } catch (Exception ex) { - m_factory = null; - EventDispatcher.shutdown(); - m_logger.log(Logger.LOG_ERROR, "Unable to start system bundle.", ex); - throw new RuntimeException("Unable to start system bundle."); + // This should not throw an exception, but if so, lets convert it to + // a runtime exception. + throw new RuntimeException("Exception creating system bundle revision", ex); } - // Reload and cached bundles. - BundleArchive[] archives = null; + // Create event dispatcher. + m_dispatcher = new EventDispatcher(m_logger, m_registry); - // First get cached bundle identifiers. - try - { - archives = m_cache.getArchives(); - } - catch (Exception ex) - { - m_logger.log( - Logger.LOG_ERROR, - "Unable to list saved bundles: " + ex, ex); - archives = null; - } + // Create framework wiring object. + m_fwkWiring = new FrameworkWiringImpl(this, m_registry); + // Create framework start level object. + m_fwkStartLevel = new FrameworkStartLevelImpl(this, m_registry); + } - BundleImpl bundle = null; + Logger getLogger() + { + return m_logger; + } - // Now install all cached bundles. - for (int i = 0; (archives != null) && (i < archives.length); i++) - { - try - { - // Make sure our id generator is not going to overlap. - // TODO: This is not correct since it may lead to re-used - // ids, which is not okay according to OSGi. - m_nextId = Math.max(m_nextId, archives[i].getId() + 1); + Map getConfig() + { + return m_configMap; + } - // It is possible that a bundle in the cache was previously - // uninstalled, but not completely deleted (perhaps because - // of a crash or a locked file), so if we see an archive - // with an UNINSTALLED persistent state, then try to remove - // it now. - if (archives[i].getPersistentState() == Bundle.UNINSTALLED) - { - m_cache.remove(archives[i]); - } - // Otherwise re-install the cached bundle. - else - { - // Install the cached bundle. - bundle = (BundleImpl) installBundle( - archives[i].getId(), archives[i].getLocation(), null); - } - } - catch (Exception ex) - { - fireFrameworkEvent(FrameworkEvent.ERROR, bundle, ex); - try - { - m_logger.log( - Logger.LOG_ERROR, - "Unable to re-install " + archives[i].getLocation(), - ex); - } - catch (Exception ex2) - { - m_logger.log( - Logger.LOG_ERROR, - "Unable to re-install cached bundle.", - ex); - } - // TODO: Perhaps we should remove the cached bundle? - } - } + StatefulResolver getResolver() + { + return m_resolver; + } - // Get the framework's default start level. - int startLevel = FelixConstants.FRAMEWORK_DEFAULT_STARTLEVEL; - String s = m_config.get(FelixConstants.FRAMEWORK_STARTLEVEL_PROP); - if (s != null) - { - try - { - startLevel = Integer.parseInt(s); - } - catch (NumberFormatException ex) - { - startLevel = FelixConstants.FRAMEWORK_DEFAULT_STARTLEVEL; - } - } + BundleRevisionDependencies getDependencies() + { + return m_dependencies; + } + + URLStreamHandler getBundleStreamHandler() + { + return m_bundleStreamHandler; + } + + String[] getBootPackages() + { + return m_bootPkgs; + } - // Load bundles from auto-install and auto-start properties; - processAutoProperties(); + boolean[] getBootPackageWildcards() + { + return m_bootPkgWildcards; + } + + private Map createUnmodifiableMap(Map mutableMap) + { + Map result = Collections.unmodifiableMap(mutableMap); - // Set the start level using the start level service; - // this ensures that all start level requests are - // serialized. - // NOTE: There is potentially a specification compliance - // issue here, since the start level request is asynchronous; - // this means that the framework will fire its STARTED event - // before all bundles have officially been restarted and it - // is not clear if this is really an issue or not. + // Work around a bug in certain version of J9 where a call to + // Collections.unmodifiableMap().keySet().iterator() throws + // a NoClassDefFoundError. We try to detect this and return + // the given mutableMap instead. try { - StartLevel sl = (StartLevel) getService( - getBundle(0), - getServiceReferences((BundleImpl) getBundle(0), StartLevel.class.getName(), null)[0]); - sl.setStartLevel(startLevel); + result.keySet().iterator(); } - catch (InvalidSyntaxException ex) + catch (NoClassDefFoundError ex) { - // Should never happen. + return mutableMap; } - // The framework is now running. - m_frameworkStatus = RUNNING_STATUS; - - // Set the system bundle state to ACTIVE. - systembundle.getInfo().setState(Bundle.ACTIVE); + return result; + } - // Fire started event for system bundle. - fireBundleEvent(BundleEvent.STARTED, systembundle); + // This overrides BundleImpl.close() which avoids removing the + // system bundle module from the resolver state. + @Override + void close() + { + } - // Send a framework event to indicate the framework has started. - fireFrameworkEvent(FrameworkEvent.STARTED, getBundle(0), null); + // This overrides the default behavior of BundleImpl.getFramework() + // to return "this", since the system bundle is the framework. + @Override + Felix getFramework() + { + return this; } - /** - * This method cleanly shuts down the framework, it must be called at the - * end of a session in order to shutdown all active bundles. - **/ - public synchronized void shutdown() + @Override + public A adapt(Class type) { - // Change framework status from running to stopping. - // If framework is not running, then just return. - if (m_frameworkStatus != RUNNING_STATUS) + checkAdapt(type); + if ((type == Framework.class) + || (type == Felix.class)) { - return; + return (A) this; } - - // The framework is now in its shutdown sequence. - m_frameworkStatus = STOPPING_STATUS; - - // Use the start level service to set the start level to zero - // in order to stop all bundles in the framework. Since framework - // shutdown happens on its own thread, we can wait for the start - // level service to finish before proceeding by calling the - // non-spec setStartLevelAndWait() method. - try + else if ((type == FrameworkWiring.class) + || (type == FrameworkWiringImpl.class)) { - StartLevelImpl sl = (StartLevelImpl) getService( - getBundle(0), - getServiceReferences((BundleImpl) getBundle(0), StartLevel.class.getName(), null)[0]); - sl.setStartLevelAndWait(0); + return (A) m_fwkWiring; } - catch (InvalidSyntaxException ex) + else if ((type == FrameworkStartLevel.class) + || (type == FrameworkStartLevelImpl.class)) { - // Should never happen. + return (A) m_fwkStartLevel; } + return super.adapt(type); + } - // Just like initialize() called the system bundle's start() - // method, we must call its stop() method here so that it - // can perform any necessary clean up. - try - { - getBundle(0).stop(); - } - catch (Exception ex) - { - fireFrameworkEvent(FrameworkEvent.ERROR, getBundle(0), ex); - m_logger.log(Logger.LOG_ERROR, "Error stopping system bundle.", ex); - } + @Override + public long getBundleId() + { + return 0; + } - // Since they may be updated and uninstalled bundles that - // have not been refreshed, we will take care of refreshing - // them during shutdown. + @Override + public long getLastModified() + { + return 0; + } - // First loop through all bundled and purge old revisions - // from updated bundles. - Bundle[] bundles = getBundles(); - for (int i = 0; i < bundles.length; i++) - { - BundleImpl bundle = (BundleImpl) bundles[i]; - if (bundle.getInfo().getArchive().getRevisionCount() > 1) - { - try - { - purgeBundle(bundle); - } - catch (Exception ex) - { - fireFrameworkEvent(FrameworkEvent.ERROR, bundle, ex); - m_logger.log(Logger.LOG_ERROR, "Unable to purge bundle " - + bundle.getInfo().getLocation(), ex); - } - } - } + @Override + void setLastModified(long l) + { + // Ignore. + } - // Next garbage collection any uninstalled bundles. - for (int i = 0; - (m_uninstalledBundles != null) && (i < m_uninstalledBundles.length); - i++) - { - try - { - garbageCollectBundle(m_uninstalledBundles[i]); - } - catch (Exception ex) - { - m_logger.log( - Logger.LOG_ERROR, - "Unable to remove " - + m_uninstalledBundles[i].getInfo().getLocation(), ex); - } - } + @Override + String _getLocation() + { + return Constants.SYSTEM_BUNDLE_LOCATION; + } - // Shutdown event dispatching queue. - EventDispatcher.shutdown(); + @Override + public int getPersistentState() + { + return Bundle.ACTIVE; + } - // The framework is no longer in a usable state. - m_frameworkStatus = INITIAL_STATUS; + @Override + public void setPersistentStateInactive() + { + // Ignore. + } - // Remove all bundles from the module factory so that any - // open resources will be closed. - bundles = getBundles(); - for (int i = 0; i < bundles.length; i++) - { - BundleImpl bundle = (BundleImpl) bundles[i]; - try - { - IModule[] modules = bundle.getInfo().getModules(); - for (int j = 0; j < modules.length; j++) - { - m_factory.removeModule(modules[j]); - } - } - catch (Exception ex) - { - m_logger.log(Logger.LOG_ERROR, - "Unable to clean up " + bundle.getInfo().getLocation(), ex); - } - } + @Override + public void setPersistentStateActive() + { + // Ignore. } - public int getStatus() + @Override + public void setPersistentStateUninstalled() { - return m_frameworkStatus; + // Ignore. } /** - * Returns the active start level of the framework; this method - * implements functionality for the Start Level service. - * @return The active start level of the framework. + * Overrides standard BundleImpl.getStartLevel() behavior to + * always return zero for the system bundle. + * @param defaultLevel This parameter is ignored by the system bundle. + * @return Always returns zero. **/ - protected int getStartLevel() + @Override + int getStartLevel(int defaultLevel) { - return m_activeStartLevel; + return 0; } /** - * Implements the functionality of the setStartLevel() - * method for the StartLevel service, but does not do the security or - * parameter check. The security and parameter check are done in the - * StartLevel service implementation because this method is called on - * a separate thread and the caller's thread would already be gone if - * we did the checks in this method. This method should not be called - * directly. - * @param requestedLevel The new start level of the framework. + * Overrides standard BundleImpl.setStartLevel() behavior to + * always throw an exception since the system bundle's start level cannot + * be changed. + * @param level This parameter is ignored by the system bundle. + * @throws IllegalArgumentException Always throws exception since system + * bundle's start level cannot be changed. **/ - protected void setFrameworkStartLevel(int requestedLevel) - { - Bundle[] bundles = null; - - // Synchronization for changing the start level is rather loose. - // The install lock is grabbed initially to atomically change the - // framework's start level and to grab a sorted snapshot of the - // currently installed bundles, but then this lock is freed immediately. - // No locks are held while processing the currently installed bundles - // for starting/stopping based on the new start level. The only locking - // that occurs is for individual bundles when startBundle()/stopBundle() - // is called, but this locking is done in the respective method. - // - // This approach does mean that it is possible for a for individual - // bundle states to change during this operation. For example, bundle - // start levels can be changed or bundles can be uninstalled. If a - // bundle's start level changes, then it is possible for it to be - // processed out of order. Uninstalled bundles are just logged and - // ignored. I had a bit of discussion with Peter Kriens about these - // issues and he felt they were consistent with the spec, which - // intended Start Level to have some leeway. - // - // Calls to this method are only made by the start level thread, which - // serializes framework start level changes. Thus, it is not possible - // for two requests to change the framework's start level to interfere - // with each other. - - synchronized (m_installedBundleLock_Priority2) - { - // Determine if we are lowering or raising the - // active start level. - boolean lowering = (requestedLevel < m_activeStartLevel); + @Override + void setStartLevel(int level) + { + throw new IllegalArgumentException("Cannot set the system bundle's start level."); + } - // Record new start level. - m_activeStartLevel = requestedLevel; + @Override + public boolean hasPermission(Object obj) + { + return true; + } - // Get a snapshot of all installed bundles. - bundles = getBundles(); - // Sort bundle array by start level either ascending or - // descending depending on whether the start level is being - // lowered or raised to that the bundles can be efficiently - // processed in order. Within a start level sort by bundle ID. - Comparator comparator = null; - if (lowering) - { - // Sort descending to stop highest start level first. - comparator = new Comparator() { - public int compare(Object o1, Object o2) - { - BundleImpl b1 = (BundleImpl) o1; - BundleImpl b2 = (BundleImpl) o2; - if (b1.getInfo().getStartLevel(getInitialBundleStartLevel()) - < b2.getInfo().getStartLevel(getInitialBundleStartLevel())) + @Override + public void init() throws BundleException + { + init((FrameworkListener[]) null); + } + /** + * @see org.osgi.framework.launch.Framework#init(org.osgi.framework.FrameworkListener[]) + */ + @Override + public void init(final FrameworkListener... listeners) throws BundleException + { + // The system bundle can only be initialized if it currently isn't started. + acquireBundleLock(this, + Bundle.INSTALLED | Bundle.RESOLVED | Bundle.STARTING | Bundle.ACTIVE); + try + { + if ((getState() == Bundle.INSTALLED) || (getState() == Bundle.RESOLVED)) + { + String security = (String) m_configMap.get(Constants.FRAMEWORK_SECURITY); + if (security != null) + { + if (System.getSecurityManager() != null) + { + throw new SecurityException("SecurityManager already installed"); + } + security = security.trim(); + if (Constants.FRAMEWORK_SECURITY_OSGI.equalsIgnoreCase(security) || (security.length() == 0)) + { + System.setSecurityManager(m_securityManager = new SecurityManager()); + } + else + { + try { - return 1; + System.setSecurityManager(m_securityManager = + (SecurityManager) Class.forName(security).newInstance()); } - else if (b1.getInfo().getStartLevel(getInitialBundleStartLevel()) - > b2.getInfo().getStartLevel(getInitialBundleStartLevel())) + catch (Throwable t) { - return -1; + SecurityException se = + new SecurityException( + "Unable to install custom SecurityManager: " + security); + se.initCause(t); + throw se; } - else if (b1.getInfo().getBundleId() < b2.getInfo().getBundleId()) - { - return 1; + } + } + + // Generate a framework UUID. + // Spec says we get a new UUID for each invocation of init(). + m_configMutableMap.put( + FelixConstants.FRAMEWORK_UUID, + Util.randomUUID("true".equalsIgnoreCase(_getProperty(FelixConstants.FRAMEWORK_UUID_SECURE)))); + + // Initialize event dispatcher. + m_dispatcher.startDispatching(); + + // Create the bundle cache, if necessary, so that we can reload any + // installed bundles. + m_cache = (BundleCache) + m_configMutableMap.get(FelixConstants.FRAMEWORK_BUNDLECACHE_IMPL); + if (m_cache == null) + { + try + { + m_cache = new BundleCache(m_logger, m_configMap); + } + catch (Exception ex) + { + m_logger.log(Logger.LOG_ERROR, "Error creating bundle cache.", ex); + throw new BundleException("Error creating bundle cache.", ex); + } + } + + // If this is the first time init is called, check to see if + // we need to flush the bundle cache. + if (getState() == Bundle.INSTALLED) + { + String clean = (String) m_configMap.get(Constants.FRAMEWORK_STORAGE_CLEAN); + if ((clean != null) + && clean.equalsIgnoreCase(Constants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT)) + { + try + { + m_cache.delete(); + } + catch (Exception ex) + { + throw new BundleException("Unable to flush bundle cache.", ex); } - return -1; } + } + + // Initialize installed bundle data structures. + Map[] maps = new Map[] { + new HashMap(1), + new TreeMap() }; - } - else - { - // Sort ascending to start lowest start level first. - comparator = new Comparator() { - public int compare(Object o1, Object o2) + m_uninstalledBundles = new ArrayList(0); + + // Add the system bundle to the set of installed bundles. + maps[LOCATION_MAP_IDX].put(_getLocation(), this); + maps[IDENTIFIER_MAP_IDX].put(new Long(0), this); + m_installedBundles = maps; + + + try + { + getResolver().removeRevision(m_extensionManager.getRevision()); + m_extensionManager.removeExtensionBundles(); + m_extensionManager.updateRevision(this, m_configMap); + if (!m_configMutableMap.containsKey(Constants.FRAMEWORK_SYSTEMPACKAGES)) { - BundleImpl b1 = (BundleImpl) o1; - BundleImpl b2 = (BundleImpl) o2; - if (b1.getInfo().getStartLevel(getInitialBundleStartLevel()) - > b2.getInfo().getStartLevel(getInitialBundleStartLevel())) + m_configMutableMap.put(Constants.FRAMEWORK_SYSTEMPACKAGES, m_extensionManager.getRevision().getHeaders().get(Constants.EXPORT_PACKAGE)); + } + getResolver().addRevision(m_extensionManager.getRevision()); + } + catch (Exception ex) + { + // This should not throw an exception, but if so, lets convert it to + // a runtime exception. + throw new BundleException("Exception creating system bundle revision", ex); + } + + // Manually resolve the system bundle, which will cause its + // state to be set to RESOLVED. + try + { + m_resolver.resolve( + Collections.singleton(adapt(BundleRevision.class)), + Collections.EMPTY_SET); + } + catch (ResolutionException ex) + { + // This should never happen. + throw new BundleException( + "Unresolved constraint in System Bundle:" + + ex.getUnresolvedRequirements()); + } + + // Reload the cached bundles before creating and starting the + // system bundle, since we want all cached bundles to be reloaded + // when we activate the system bundle and any subsequent system + // bundle activators passed into the framework constructor. + BundleArchive[] archives = null; + + // First get cached bundle identifiers. + try + { + archives = m_cache.getArchives(); + } + catch (Exception ex) + { + m_logger.log(Logger.LOG_ERROR, "Unable to list saved bundles.", ex); + archives = null; + } + + // Create system bundle activator and bundle context so we can activate it. + setActivator(new SystemBundleActivator()); + setBundleContext(new BundleContextImpl(m_logger, this, this)); + + boolean javaVersionChanged = handleJavaVersionChange(); + + // Now load all cached bundles. + for (int i = 0; (archives != null) && (i < archives.length); i++) + { + try + { + // Keep track of the max bundle ID currently in use since we + // will need to use this as our next bundle ID value if the + // persisted value cannot be read. + m_nextId = Math.max(m_nextId, archives[i].getId() + 1); + + // It is possible that a bundle in the cache was previously + // uninstalled, but not completely deleted (perhaps because + // of a crash or a locked file), so if we see an archive + // with an UNINSTALLED persistent state, then try to remove + // it now. + if (archives[i].getPersistentState() == Bundle.UNINSTALLED) + { + archives[i].closeAndDelete(); + } + // Otherwise re-install the cached bundle. + else { - return 1; + // Install the cached bundle. + reloadBundle(archives[i], javaVersionChanged); } - else if (b1.getInfo().getStartLevel(getInitialBundleStartLevel()) - < b2.getInfo().getStartLevel(getInitialBundleStartLevel())) + } + catch (Exception ex) + { + fireFrameworkEvent(FrameworkEvent.ERROR, this, ex); + try { - return -1; + m_logger.log( + Logger.LOG_ERROR, + "Unable to re-install " + archives[i].getLocation(), + ex); } - else if (b1.getInfo().getBundleId() > b2.getInfo().getBundleId()) + catch (Exception ex2) { - return 1; + m_logger.log( + Logger.LOG_ERROR, + "Unable to re-install cached bundle.", + ex); } - return -1; + // TODO: FRAMEWORK - Perhaps we should remove the cached bundle? } - }; + } + + for (Bundle extension : m_extensionManager.resolveExtensionBundles(this)) + { + m_extensionManager.startExtensionBundle(this, (BundleImpl) extension); + } + + // Now that we have loaded all cached bundles and have determined the + // max bundle ID of cached bundles, we need to try to load the next + // bundle ID from persistent storage. In case of failure, we should + // keep the max value. + m_nextId = Math.max(m_nextId, loadNextId()); + + // The framework is now in its startup sequence. + setBundleStateAndNotify(this, Bundle.STARTING); + + // Now it is possible for threads to wait for the framework to stop, + // so create a gate for that purpose. + m_shutdownGate = new ThreadGate(); + + // add framework listeners + if ( listeners != null ) + { + for(final FrameworkListener fl : listeners) + { + addFrameworkListener(this, fl); + } + } + + // Start services + m_resolver.start(); + m_fwkWiring.start(); + m_fwkStartLevel.start(); + + try + { + Felix.m_secureAction.startActivator( + getActivator(), _getBundleContext()); + } + catch (Throwable ex) + { + m_dispatcher.stopDispatching(); + m_logger.log(Logger.LOG_ERROR, "Unable to start system bundle.", ex); + throw new RuntimeException("Unable to start system bundle."); + } + + // We have to check with the security provider (if there is one). + // This is to avoid having bundles in the cache that have been tampered with + SecurityProvider sp = getFramework().getSecurityProvider(); + if ((sp != null) && (System.getSecurityManager() != null)) + { + boolean locked = acquireGlobalLock(); + if (!locked) + { + throw new BundleException( + "Unable to acquire the global lock to check the bundle."); + } + try + { + for (Object bundle : m_installedBundles[IDENTIFIER_MAP_IDX].values()) + { + try + { + if (bundle != this) + { + setBundleProtectionDomain(((BundleImpl) bundle).adapt(BundleRevisionImpl.class)); + } + } + catch (Exception ex) + { + ((BundleImpl) bundle).close(); + maps = new Map[] { + new HashMap(m_installedBundles[LOCATION_MAP_IDX]), + new TreeMap(m_installedBundles[IDENTIFIER_MAP_IDX]) + }; + maps[LOCATION_MAP_IDX].remove(((BundleImpl) bundle)._getLocation()); + maps[IDENTIFIER_MAP_IDX].remove(new Long(((BundleImpl) bundle).getBundleId())); + m_installedBundles = maps; + + m_logger.log( + Logger.LOG_ERROR, + "Bundle in cache doesn't pass security check anymore.", + ex); + } + } + } + finally + { + // Always release the global lock. + releaseGlobalLock(); + } + } + + m_extensionManager.startPendingExtensionBundles(Felix.this); + m_fwkWiring.refreshBundles(null); + + // Clear the cache of classes coming from the system bundle. + // This is only used for Felix.getBundle(Class clazz) to speed + // up class lookup for the system bundle. + synchronized (m_systemBundleClassCache) + { + m_systemBundleClassCache.clear(); + } + } + } + finally + { + releaseBundleLock(this); + + if ( listeners != null ) + { + for(final FrameworkListener fl : listeners) + { + removeFrameworkListener(this, fl); + } + } + } + } + + private boolean handleJavaVersionChange() + { + File dataFile = getDataFile(this, "last.java.version"); + + int currentVersion = 8; + try + { + currentVersion = Version.parseVersion(_getProperty("java.specification.version")).getMajor(); + } + catch (Exception ignore) + { + getLogger().log(this, Logger.LOG_WARNING, "Unable to parse current java version", ignore); + } + + if (currentVersion < 8) + { + currentVersion = 8; + } + + int lastVersion = 8; + if (dataFile.isFile()) + { + BufferedReader input = null; + try + { + input = new BufferedReader(new InputStreamReader(new FileInputStream(dataFile), "UTF-8")); + lastVersion = Version.parseVersion(input.readLine()).getMajor(); + } + catch (Exception ignore) + { + getLogger().log(this, Logger.LOG_WARNING, "Unable to parse last java version", ignore); } + finally + { + if (input != null) + { + try + { + input.close(); + } + catch (Exception ignore) + { - Arrays.sort(bundles, comparator); + } + } + } } - // Stop or start the bundles according to the start level. - for (int i = 0; (bundles != null) && (i < bundles.length); i++) + if (lastVersion < 8) { - BundleImpl impl = (BundleImpl) bundles[i]; + lastVersion = 8; + } + + PrintWriter output = null; - // Ignore the system bundle, since its start() and - // stop() methods get called explicitly in Felix.start() - // and Felix.shutdown(), respectively. - if (impl.getInfo().getBundleId() == 0) + try + { + output = new PrintWriter(new OutputStreamWriter( + new FileOutputStream(getDataFile(this, "last.java.version")), "UTF-8")); + output.println(Integer.toString(currentVersion)); + output.flush(); + } + catch (Exception ignore) + { + getLogger().log(this, Logger.LOG_WARNING, "Unable to persist current java version", ignore); + } + finally + { + if (output != null) { - continue; + try + { + output.close(); + } + catch (Exception ignore) + { + + } } + } + return currentVersion != lastVersion; + } - // Lock the current bundle. - acquireBundleLock(impl); + void setBundleProtectionDomain(BundleRevisionImpl revisionImpl) throws Exception + { + Object certificates = null; + SecurityProvider sp = getFramework().getSecurityProvider(); + if ((sp != null) && (System.getSecurityManager() != null)) + { + BundleImpl bundleImpl = revisionImpl.getBundle(); + sp.checkBundle(bundleImpl); + Map signers = (Map) sp.getSignerMatcher(bundleImpl, Bundle.SIGNERS_TRUSTED); + certificates = signers.keySet().toArray(new java.security.cert.Certificate[signers.size()]); + } + revisionImpl.setProtectionDomain( + new BundleProtectionDomain(revisionImpl, certificates)); + } - try + /** + * This method starts the framework instance, which will transition the + * framework from start level 0 to its active start level as specified in + * its configuration properties (1 by default). If the init() was + * not explicitly invoked before calling this method, then it will be + * implicitly invoked before starting the framework. + * + * @throws org.osgi.framework.BundleException if any error occurs. + **/ + @Override + public void start() throws BundleException + { + int startLevel = FelixConstants.FRAMEWORK_DEFAULT_STARTLEVEL; + + acquireBundleLock(this, + Bundle.INSTALLED | Bundle.RESOLVED | Bundle.STARTING | Bundle.ACTIVE); + try + { + // Initialize if necessary. + if ((getState() == Bundle.INSTALLED) || (getState() == Bundle.RESOLVED)) { - // Start the bundle if necessary. - if ((impl.getInfo().getPersistentState() == Bundle.ACTIVE) && - (impl.getInfo().getStartLevel(getInitialBundleStartLevel()) - <= m_activeStartLevel)) + init(); + } + + // If the current state is STARTING, then the system bundle can be started. + if (getState() == Bundle.STARTING) + { + // Get the framework's default start level. + String s = (String) m_configMap.get(Constants.FRAMEWORK_BEGINNING_STARTLEVEL); + if (s != null) { try { - startBundle(impl, false); + startLevel = Integer.parseInt(s); } - catch (Throwable th) + catch (NumberFormatException ex) { - fireFrameworkEvent(FrameworkEvent.ERROR, impl, th); - m_logger.log( - Logger.LOG_ERROR, - "Error starting " + impl.getInfo().getLocation(), th); + startLevel = FelixConstants.FRAMEWORK_DEFAULT_STARTLEVEL; } } - // Stop the bundle if necessary. - else if (impl.getInfo().getStartLevel(getInitialBundleStartLevel()) - > m_activeStartLevel) + + m_fwkStartLevel.setStartLevelAndWait(startLevel); + + // The framework is now running. + setBundleStateAndNotify(this, Bundle.ACTIVE); + } + } + finally + { + releaseBundleLock(this); + } + + // Fire started event for system bundle. + fireBundleEvent(BundleEvent.STARTED, this); + + // Send a framework event to indicate the framework has started. + fireFrameworkEvent(FrameworkEvent.STARTED, this, null); + } + + @Override + public void start(int options) throws BundleException + { + start(); + } + + /** + * This method asynchronously shuts down the framework, it must be called at the + * end of a session in order to shutdown all active bundles. + **/ + @Override + public void stop() throws BundleException + { + Object sm = System.getSecurityManager(); + + if (sm != null) + { + ((SecurityManager) sm).checkPermission(new AdminPermission(this, + AdminPermission.EXECUTE)); + } + + if ((getState() & (Bundle.INSTALLED | Bundle.RESOLVED)) == 0) + { + // Spec says stop() on SystemBundle should return immediately and + // shutdown framework on another thread. + (new Thread("FelixShutdown") + { + @Override + public void run() { try { - stopBundle(impl, false); + stopBundle(Felix.this, true); } - catch (Throwable th) + catch (BundleException ex) { - fireFrameworkEvent(FrameworkEvent.ERROR, impl, th); m_logger.log( Logger.LOG_ERROR, - "Error stopping " + impl.getInfo().getLocation(), th); + "Exception trying to stop framework.", + ex); + } + } + }).start(); + } + } + + @Override + public void stop(int options) throws BundleException + { + stop(); + } + + /** + * This method will cause the calling thread to block until the framework + * shuts down. + * @param timeout A timeout value. + * @throws java.lang.InterruptedException If the thread was interrupted. + **/ + @Override + public FrameworkEvent waitForStop(long timeout) throws InterruptedException + { + // Throw exception if timeout is negative. + if (timeout < 0) + { + throw new IllegalArgumentException("Timeout cannot be negative."); + } + + // If there is a gate, wait on it; otherwise, return immediately. + // Grab a copy of the gate, since it is volatile. + ThreadGate gate = m_shutdownGate; + boolean open = false; + if (gate != null) + { + open = gate.await(timeout); + } + + FrameworkEvent event; + if (open && (gate.getMessage() != null)) + { + event = (FrameworkEvent) gate.getMessage(); + } + else if (!open && (gate != null)) + { + event = new FrameworkEvent(FrameworkEvent.WAIT_TIMEDOUT, this, null); + } + else + { + event = new FrameworkEvent(FrameworkEvent.STOPPED, this, null); + } + return event; + } + + @Override + public void uninstall() throws BundleException + { + throw new BundleException("Cannot uninstall the system bundle."); + } + + @Override + public void update() throws BundleException + { + update(null); + } + + @Override + public void update(InputStream is) throws BundleException + { + Object sm = System.getSecurityManager(); + + if (sm != null) + { + ((SecurityManager) sm).checkPermission(new AdminPermission(this, + AdminPermission.EXECUTE)); + } + + // Spec says to close input stream first. + try + { + if (is != null) is.close(); + } + catch (IOException ex) + { + m_logger.log(Logger.LOG_WARNING, "Exception closing input stream.", ex); + } + + // Then to stop and restart the framework on a separate thread. + new Thread(new Runnable() { + @Override + public void run() + { + try + { + // First acquire the system bundle lock to verify the state. + acquireBundleLock(Felix.this, Bundle.STARTING | Bundle.ACTIVE); + // Set the reason for the shutdown. + m_shutdownGate.setMessage( + new FrameworkEvent(FrameworkEvent.STOPPED_UPDATE, Felix.this, null)); + // Record the state and stop the system bundle. + int oldState = Felix.this.getState(); + try + { + stop(); + } + catch (BundleException ex) + { + m_logger.log(Logger.LOG_WARNING, "Exception stopping framework.", ex); + } + finally + { + releaseBundleLock(Felix.this); + } + + // Make sure the framework is stopped. + try + { + waitForStop(0); + } + catch (InterruptedException ex) + { + m_logger.log(Logger.LOG_WARNING, "Did not wait for framework to stop.", ex); + } + + // Depending on the old state, restart the framework. + try + { + switch (oldState) + { + case Bundle.STARTING: + init(); + break; + case Bundle.ACTIVE: + start(); + break; + } + } + catch (BundleException ex) + { + m_logger.log(Logger.LOG_WARNING, "Exception restarting framework.", ex); } } + catch (Exception ex) + { + m_logger.log(Logger.LOG_WARNING, "Cannot update an inactive framework."); + } + } + }).start(); + } + + private void stopRefresh() throws BundleException + { + Object sm = System.getSecurityManager(); + + if (sm != null) + { + ((SecurityManager) sm).checkPermission(new AdminPermission(this, + AdminPermission.EXECUTE)); + } + + + // Stop the framework on a separate thread. + new Thread(new Runnable() { + @Override + public void run() + { + try + { + // First acquire the system bundle lock to verify the state. + acquireBundleLock(Felix.this, Bundle.STARTING | Bundle.ACTIVE); + // Set the reason for the shutdown. + m_shutdownGate.setMessage( + new FrameworkEvent(FrameworkEvent.STOPPED_SYSTEM_REFRESHED, Felix.this, null)); + // Record the state and stop the system bundle. + int oldState = Felix.this.getState(); + try + { + stop(); + } + catch (BundleException ex) + { + m_logger.log(Logger.LOG_WARNING, "Exception stopping framework.", ex); + } + finally + { + releaseBundleLock(Felix.this); + } + } + catch (Exception ex) + { + m_logger.log(Logger.LOG_WARNING, "Cannot update an inactive framework."); + } + } + }).start(); + } + + @Override + public String toString() + { + return getSymbolicName() + " [" + getBundleId() +"]"; + } + + /** + * Returns the active start level of the framework; this method + * implements functionality for the Start Level service. + * @return The active start level of the framework. + **/ + int getActiveStartLevel() + { + return m_activeStartLevel; + } + + /** + * Implements the functionality of the setStartLevel() + * method for the StartLevel service, but does not do the security or + * parameter check. The security and parameter check are done in the + * StartLevel service implementation because this method is called on + * a separate thread and the caller's thread would already be gone if + * we did the checks in this method. This method should not be called + * directly. + * @param requestedLevel The new start level of the framework. + **/ + void setActiveStartLevel(int requestedLevel, FrameworkListener[] listeners) + { + Bundle[] bundles; + + // Record the target start level immediately and use this for + // comparisons for starting/stopping bundles to avoid race + // conditions to restart stopped bundles. + m_targetStartLevel = requestedLevel; + + // Do nothing if the requested start level is the same as the + // active start level. + if (m_targetStartLevel != m_activeStartLevel) + { + // Synchronization for changing the start level is rather loose. + // The framework's active start level is volatile, so no lock is + // needed to access it. No locks are held while processing the + // currently installed bundles for starting/stopping based on the new + // active start level. The only locking that occurs is for individual + // bundles when startBundle()/stopBundle() is called, but this locking + // is done in the respective method. + // + // This approach does mean that it is possible for a for individual + // bundle states to change during this operation. If a bundle's start + // level changes, then it is possible for it to be processed out of + // order. Uninstalled bundles are just logged and ignored. + // + // Calls to this method are only made by the start level thread, which + // serializes framework start level changes. Thus, it is not possible + // for two requests to change the framework's start level to interfere + // with each other. + + // Acquire global lock. + boolean locked = acquireGlobalLock(); + if (!locked) + { + // If the calling thread holds bundle locks, then we might not + // be able to get the global lock. + throw new IllegalStateException( + "Unable to acquire global lock to create bundle snapshot."); + } + + boolean bundlesRemaining; + try + { + synchronized (m_startLevelBundles) + { + // Get a sorted snapshot of all installed bundles + // to be processed during the start level change. + // We also snapshot the start level here, since it + // may change and we don't want to consider any + // changes since they will be queued for the start + // level thread. + bundles = getBundles(); + for (Bundle b : bundles) + { + m_startLevelBundles.add( + new StartLevelTuple( + (BundleImpl) b, + ((BundleImpl) b).getStartLevel( + getInitialBundleStartLevel()))); + } + bundlesRemaining = !m_startLevelBundles.isEmpty(); + } } finally { - // Always release bundle lock. - releaseBundleLock(impl); + releaseGlobalLock(); + } + + // Determine if we are lowering or raising the + // active start level. + boolean isLowering = (m_targetStartLevel < m_activeStartLevel); + // Determine the range of start levels to process. + int low = (isLowering) ? m_targetStartLevel + 1 : m_activeStartLevel + 1; + int high = (isLowering) ? m_activeStartLevel : m_targetStartLevel; + m_activeStartLevel = (isLowering) ? high : low; + + // Process bundles and stop or start them accordingly. + while (bundlesRemaining) + { + StartLevelTuple tuple; + + // Remove our tuple to be processed while holding the queue lock + // and update the active start level accordingly, which allows + // us to determine in startBundle() if concurrent requests to + // start a bundle should be handled synchronously or just added + // to the queue and handled asynchronously. + synchronized (m_startLevelBundles) + { + if (isLowering) + { + tuple = m_startLevelBundles.last(); + } + else + { + tuple = m_startLevelBundles.first(); + } + + if ((tuple.m_level >= low) && (tuple.m_level <= high)) + { + m_activeStartLevel = tuple.m_level; + } + } + + // Ignore the system bundle, since its start() and + // stop() methods get called explicitly in Felix.start() + // and Felix.stop(), respectively. + if (tuple.m_bundle.getBundleId() != 0) + { + // Lock the current bundle. + try + { + acquireBundleLock(tuple.m_bundle, + Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE + | Bundle.STARTING | Bundle.STOPPING); + } + catch (IllegalStateException ex) + { + // Ignore if the bundle has been uninstalled. + if (tuple.m_bundle.getState() != Bundle.UNINSTALLED) + { + fireFrameworkEvent(FrameworkEvent.ERROR, tuple.m_bundle, ex); + m_logger.log(tuple.m_bundle, + Logger.LOG_ERROR, + "Error locking " + tuple.m_bundle._getLocation(), ex); + } + else + { + synchronized (m_startLevelBundles) + { + m_startLevelBundles.remove(tuple); + bundlesRemaining = !m_startLevelBundles.isEmpty(); + } + } + continue; + } + + try + { + // Start the bundle if necessary. + // Note that we only attempt to start the bundle if + // its start level is equal to the active start level, + // which means we assume lower bundles are in the state + // they should be in (i.e., we won't attempt to restart + // them if they previously failed to start). + if (!isLowering + && (((tuple.m_bundle.getPersistentState() == Bundle.ACTIVE) + || (tuple.m_bundle.getPersistentState() == Bundle.STARTING)) + && (tuple.m_level == m_activeStartLevel))) + { + try + { +// TODO: LAZY - Not sure if this is the best way... + int options = Bundle.START_TRANSIENT; + options = (tuple.m_bundle.getPersistentState() == Bundle.STARTING) + ? options | Bundle.START_ACTIVATION_POLICY + : options; + startBundle(tuple.m_bundle, options); + } + catch (Throwable th) + { + fireFrameworkEvent(FrameworkEvent.ERROR, tuple.m_bundle, th); + m_logger.log(tuple.m_bundle, + Logger.LOG_ERROR, + "Error starting " + tuple.m_bundle._getLocation(), th); + } + } + // Stop the bundle if necessary. + else if (isLowering + && (((tuple.m_bundle.getState() == Bundle.ACTIVE) + || (tuple.m_bundle.getState() == Bundle.STARTING)) + && (tuple.m_level == m_activeStartLevel))) + { + try + { + stopBundle(tuple.m_bundle, false); + } + catch (Throwable th) + { + fireFrameworkEvent(FrameworkEvent.ERROR, tuple.m_bundle, th); + m_logger.log(tuple.m_bundle, + Logger.LOG_ERROR, + "Error stopping " + tuple.m_bundle._getLocation(), th); + } + } + } + finally + { + // Always release bundle lock. + releaseBundleLock(tuple.m_bundle); + } + } + + synchronized (m_startLevelBundles) + { + m_startLevelBundles.remove(tuple); + bundlesRemaining = !m_startLevelBundles.isEmpty(); + } } + + m_activeStartLevel = m_targetStartLevel; } - fireFrameworkEvent(FrameworkEvent.STARTLEVEL_CHANGED, getBundle(0), null); + if (getState() == Bundle.ACTIVE) + { + fireFrameworkEvent(FrameworkEvent.STARTLEVEL_CHANGED, this, null); + + if (listeners != null) + { + FrameworkEvent event = new FrameworkEvent( + FrameworkEvent.STARTLEVEL_CHANGED, this, null); + for (FrameworkListener l : listeners) + { + try + { + l.frameworkEvent(event); + } + catch (Throwable th) + { + m_logger.log(Logger.LOG_ERROR, + "Framework listener delivery error.", th); + } + } + } + } } /** @@ -812,9 +1612,9 @@ else if (impl.getInfo().getStartLevel(getInitialBundleStartLevel()) * the Start Level service. * @return The default start level for newly installed bundles. **/ - protected int getInitialBundleStartLevel() + int getInitialBundleStartLevel() { - String s = m_config.get(FelixConstants.BUNDLE_STARTLEVEL_PROP); + String s = (String) m_configMap.get(FelixConstants.BUNDLE_STARTLEVEL_PROP); if (s != null) { @@ -839,10 +1639,8 @@ protected int getInitialBundleStartLevel() * bundles. * @throws java.lang.IllegalArgumentException If the specified start * level is not greater than zero. - * @throws java.security.SecurityException If the caller does not - * have AdminPermission. **/ - protected void setInitialBundleStartLevel(int startLevel) + void setInitialBundleStartLevel(int startLevel) { if (startLevel <= 0) { @@ -850,7 +1648,7 @@ protected void setInitialBundleStartLevel(int startLevel) "Initial start level must be greater than zero."); } - m_configMutable.put( + m_configMutableMap.put( FelixConstants.BUNDLE_STARTLEVEL_PROP, Integer.toString(startLevel)); } @@ -862,14 +1660,14 @@ protected void setInitialBundleStartLevel(int startLevel) * @throws java.lang.IllegalArgumentException If the specified * bundle has been uninstalled. **/ - protected int getBundleStartLevel(Bundle bundle) + int getBundleStartLevel(Bundle bundle) { if (bundle.getState() == Bundle.UNINSTALLED) { throw new IllegalArgumentException("Bundle is uninstalled."); } - return ((BundleImpl) bundle).getInfo().getStartLevel(getInitialBundleStartLevel()); + return ((BundleImpl) bundle).getStartLevel(getInitialBundleStartLevel()); } /** @@ -880,40 +1678,62 @@ protected int getBundleStartLevel(Bundle bundle) * @throws java.lang.IllegalArgumentException If the specified * bundle is the system bundle or if the bundle has been * uninstalled. - * @throws java.security.SecurityException If the caller does not - * have AdminPermission. **/ - protected void setBundleStartLevel(Bundle bundle, int startLevel) + void setBundleStartLevel(Bundle bundle, int startLevel) { // Acquire bundle lock. - acquireBundleLock((BundleImpl) bundle); + BundleImpl impl = (BundleImpl) bundle; + try + { + acquireBundleLock(impl, + Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE + | Bundle.STARTING | Bundle.STOPPING); + } + catch (IllegalStateException ex) + { + fireFrameworkEvent(FrameworkEvent.ERROR, impl, ex); + m_logger.log(impl, + Logger.LOG_ERROR, + "Error locking " + impl._getLocation(), ex); + return; + } Throwable rethrow = null; try { - if (bundle.getState() == Bundle.UNINSTALLED) - { - throw new IllegalArgumentException("Bundle is uninstalled."); - } - if (startLevel >= 1) { - BundleImpl impl = (BundleImpl) bundle; - impl.getInfo().setStartLevel(startLevel); + // NOTE: The start level was persistently recorded inside + // the start level impl because the spec requires it to be + // done synchronously. try { // Start the bundle if necessary. - if ((impl.getInfo().getPersistentState() == Bundle.ACTIVE) && - (impl.getInfo().getStartLevel(getInitialBundleStartLevel()) - <= m_activeStartLevel)) + // We don't need to be concerned about the target + // start level here, since this method is only executed + // by the start level thread, so no start level change + // can be occurring concurrently. + if (((impl.getPersistentState() == Bundle.ACTIVE) + || (impl.getPersistentState() == Bundle.STARTING)) + && (startLevel <= m_activeStartLevel)) { - startBundle(impl, false); +// TODO: LAZY - Not sure if this is the best way... + int options = Bundle.START_TRANSIENT; + options = (impl.getPersistentState() == Bundle.STARTING) + ? options | Bundle.START_ACTIVATION_POLICY + : options; + startBundle(impl, options); } // Stop the bundle if necessary. - else if (impl.getInfo().getStartLevel(getInitialBundleStartLevel()) - > m_activeStartLevel) + // We don't need to be concerned about the target + // start level here, since this method is only executed + // by the start level thread, so no start level change + // can be occurring concurrently. + else if (((impl.getState() == Bundle.ACTIVE) + || (impl.getState() == Bundle.STARTING)) + && (startLevel > m_activeStartLevel)) { stopBundle(impl, false); } @@ -921,18 +1741,22 @@ else if (impl.getInfo().getStartLevel(getInitialBundleStartLevel()) catch (Throwable th) { rethrow = th; - m_logger.log(Logger.LOG_ERROR, "Error starting/stopping bundle.", th); + m_logger.log( + impl, + Logger.LOG_ERROR, "Error starting/stopping bundle.", th); } } else { - m_logger.log(Logger.LOG_WARNING, "Bundle start level must be greater than zero."); + m_logger.log( + impl, + Logger.LOG_WARNING, "Bundle start level must be greater than zero."); } } finally { // Always release bundle lock. - releaseBundleLock((BundleImpl) bundle); + releaseBundleLock(impl); } if (rethrow != null) @@ -950,14 +1774,34 @@ else if (impl.getInfo().getStartLevel(getInitialBundleStartLevel()) * @throws java.lang.IllegalArgumentException If the specified * bundle has been uninstalled. **/ - protected boolean isBundlePersistentlyStarted(Bundle bundle) + boolean isBundlePersistentlyStarted(Bundle bundle) { if (bundle.getState() == Bundle.UNINSTALLED) { throw new IllegalArgumentException("Bundle is uninstalled."); } - return (((BundleImpl) bundle).getInfo().getPersistentState() == Bundle.ACTIVE); + return (((BundleImpl) bundle).getPersistentState() == Bundle.ACTIVE) + || (((BundleImpl) bundle).getPersistentState() == Bundle.STARTING); + } + + /** + * Returns whether the bundle is using its declared activation policy; + * this is an method implementation for the Start Level service. + * @param bundle The bundle to examine. + * @return true if the bundle is using its declared activation + * policy, false otherwise. + * @throws java.lang.IllegalArgumentException If the specified + * bundle has been uninstalled. + **/ + boolean isBundleActivationPolicyUsed(Bundle bundle) + { + if (bundle.getState() == Bundle.UNINSTALLED) + { + throw new IllegalArgumentException("Bundle is uninstalled."); + } + + return ((BundleImpl) bundle).isDeclaredActivationPolicyUsed(); } // @@ -965,85 +1809,195 @@ protected boolean isBundlePersistentlyStarted(Bundle bundle) // /** - * Implementation for Bundle.getHeaders(). + * Get bundle headers and resolve any localized strings from resource bundles. + * @param bundle + * @param locale + * @return localized bundle headers dictionary. **/ - protected Dictionary getBundleHeaders(BundleImpl bundle) + Dictionary getBundleHeaders(BundleImpl bundle, String locale) { - return new MapToDictionary(bundle.getInfo().getCurrentHeader()); + return new MapToDictionary(bundle.getCurrentLocalizedHeader(locale)); } /** - * Implementation for Bundle.getLocation(). + * Implementation for Bundle.getResource(). **/ - protected String getBundleLocation(BundleImpl bundle) + URL getBundleResource(BundleImpl bundle, String name) { - return bundle.getInfo().getLocation(); + if (bundle.getState() == Bundle.UNINSTALLED) + { + throw new IllegalStateException("The bundle is uninstalled."); + } + else if (Util.isFragment(bundle.adapt(BundleRevision.class))) + { + return null; + } + else if (bundle.getState() == Bundle.INSTALLED) + { + try + { + resolveBundleRevision(bundle.adapt(BundleRevision.class)); + } + catch (Exception ex) + { + // Ignore. + } + } + + // If the bundle revision isn't resolved, then just search + // locally, otherwise delegate. + if (bundle.adapt(BundleRevision.class).getWiring() == null) + { + return ((BundleRevisionImpl) bundle.adapt(BundleRevision.class)) + .getResourceLocal(name); + } + else + { + return ((BundleWiringImpl) bundle.adapt(BundleRevision.class).getWiring()) + .getResourceByDelegation(name); + } } /** - * Implementation for Bundle.getResource(). + * Implementation for Bundle.getResources(). **/ - protected URL getBundleResource(BundleImpl bundle, String name) + Enumeration getBundleResources(BundleImpl bundle, String name) { - if (bundle.getInfo().getState() == Bundle.UNINSTALLED) + if (bundle.getState() == Bundle.UNINSTALLED) { throw new IllegalStateException("The bundle is uninstalled."); } - return bundle.getInfo().getCurrentModule().getResource(name); + else if (Util.isFragment(bundle.adapt(BundleRevision.class))) + { + return null; + } + else if (bundle.getState() == Bundle.INSTALLED) + { + try + { + resolveBundleRevision(bundle.adapt(BundleRevision.class)); + } + catch (Exception ex) + { + // Ignore. + } + } + + if (bundle.adapt(BundleRevision.class).getWiring() == null) + { + return ((BundleRevisionImpl) bundle.adapt(BundleRevision.class)) + .getResourcesLocal(name); + } + else + { + return ((BundleWiringImpl) bundle.adapt(BundleRevision.class).getWiring()) + .getResourcesByDelegation(name); + } } /** * Implementation for Bundle.getEntry(). **/ - protected URL getBundleEntry(BundleImpl bundle, String name) + URL getBundleEntry(BundleImpl bundle, String name) { - if (bundle.getInfo().getState() == Bundle.UNINSTALLED) + if (bundle.getState() == Bundle.UNINSTALLED) { throw new IllegalStateException("The bundle is uninstalled."); } - return ((ContentLoaderImpl) bundle.getInfo().getCurrentModule() - .getContentLoader()).getResourceFromContent(name); + + URL url = ((BundleRevisionImpl) bundle.adapt(BundleRevision.class)).getEntry(name); + + // Some JAR files do not contain directory entries, so if + // the entry wasn't found and is a directory, scan the entries + // to see if we should synthesize an entry for it. + if ((url == null) && name.endsWith("/") && !name.equals("/")) + { + // Use the entry filter enumeration to search the bundle content + // recursively for matching entries and return URLs to them. + Enumeration enumeration = + new EntryFilterEnumeration( + bundle.adapt(BundleRevision.class), false, name, "*", true, true); + // If the enumeration has elements, then that means we need + // to synthesize the directory entry. + if (enumeration.hasMoreElements()) + { + URL entryURL = (URL) enumeration.nextElement(); + try + { + url = new URL(entryURL, ((name.charAt(0) == '/') ? name : "/" + name)); + } + catch (MalformedURLException ex) + { + url = null; + } + } + } + return url; } /** * Implementation for Bundle.getEntryPaths(). **/ - protected Enumeration getBundleEntryPaths(BundleImpl bundle, String path) + Enumeration getBundleEntryPaths(BundleImpl bundle, String path) { - if (bundle.getInfo().getState() == Bundle.UNINSTALLED) + if (bundle.getState() == Bundle.UNINSTALLED) { throw new IllegalStateException("The bundle is uninstalled."); } - // Get the entry enumeration from the module content and + // Get the entry enumeration from the revision content and // create a wrapper enumeration to filter it. - Enumeration enumeration = new GetEntryPathsEnumeration(bundle, path); + Enumeration enumeration = + new EntryFilterEnumeration( + bundle.adapt(BundleRevision.class), false, path, "*", false, false); // Return the enumeration if it has elements. return (!enumeration.hasMoreElements()) ? null : enumeration; } /** - * Implementation for findEntries(). + * Implementation for Bundle.findEntries(). **/ - public Enumeration findBundleEntries( - BundleImpl bundle, String path, String filePattern, boolean recurse) + Enumeration findBundleEntries( + BundleImpl bundle, String path, String filePattern, boolean recurse) { - // Try to resolve the bundle per the spec. - resolveBundles(new Bundle[] { bundle }); + if (bundle.getState() == Bundle.UNINSTALLED) + { + throw new IllegalStateException("The bundle is uninstalled."); + } + else if (!Util.isFragment(bundle.adapt(BundleRevision.class)) && bundle.getState() == Bundle.INSTALLED) + { + try + { + resolveBundleRevision(bundle.adapt(BundleRevision.class)); + } + catch (Exception ex) + { + // Ignore. + } + } + return findBundleEntries( + bundle.adapt(BundleRevision.class), path, filePattern, recurse); + } - // Get the entry enumeration from the module content and + /** + * Implementation for BundleWiring.findEntries(). + **/ + Enumeration findBundleEntries( + BundleRevision revision, String path, String filePattern, boolean recurse) + { + // Get the entry enumeration from the revision content and // create a wrapper enumeration to filter it. Enumeration enumeration = - new FindEntriesEnumeration(bundle, path, filePattern, recurse); + new EntryFilterEnumeration(revision, true, path, filePattern, recurse, true); // Return the enumeration if it has elements. return (!enumeration.hasMoreElements()) ? null : enumeration; } - protected ServiceReference[] getBundleRegisteredServices(BundleImpl bundle) + ServiceReference[] getBundleRegisteredServices(BundleImpl bundle) { - if (bundle.getInfo().getState() == Bundle.UNINSTALLED) + if (bundle.getState() == Bundle.UNINSTALLED) { throw new IllegalStateException("The bundle is uninstalled."); } @@ -1054,7 +2008,7 @@ protected ServiceReference[] getBundleRegisteredServices(BundleImpl bundle) return refs; } - protected ServiceReference[] getBundleServicesInUse(Bundle bundle) + ServiceReference[] getBundleServicesInUse(Bundle bundle) { // Filter list of "in use" service references. ServiceReference[] refs = m_registry.getServicesInUse(bundle); @@ -1062,9 +2016,9 @@ protected ServiceReference[] getBundleServicesInUse(Bundle bundle) return refs; } - protected boolean bundleHasPermission(BundleImpl bundle, Object obj) + boolean bundleHasPermission(BundleImpl bundle, Object obj) { - if (bundle.getInfo().getState() == Bundle.UNINSTALLED) + if (bundle.getState() == Bundle.UNINSTALLED) { throw new IllegalStateException("The bundle is uninstalled."); } @@ -1074,14 +2028,16 @@ protected boolean bundleHasPermission(BundleImpl bundle, Object obj) try { return (obj instanceof java.security.Permission) - ? ((ProtectionDomain) - bundle.getInfo().getCurrentModule().getSecurityContext()) - .implies((java.security.Permission) obj) + ? impliesBundlePermission( + (BundleProtectionDomain) + bundle.getProtectionDomain(), + (java.security.Permission) obj, true) : false; } catch (Exception ex) { m_logger.log( + bundle, Logger.LOG_WARNING, "Exception while evaluating the permission.", ex); @@ -1095,564 +2051,1027 @@ protected boolean bundleHasPermission(BundleImpl bundle, Object obj) /** * Implementation for Bundle.loadClass(). **/ - protected Class loadBundleClass(BundleImpl bundle, String name) throws ClassNotFoundException + Class loadBundleClass(BundleImpl bundle, String name) throws ClassNotFoundException { - Class clazz = bundle.getInfo().getCurrentModule().getClass(name); - if (clazz == null) + if (bundle.getState() == Bundle.UNINSTALLED) { - // Throw exception. - ClassNotFoundException ex = new ClassNotFoundException(name); - - // The spec says we must fire a framework error. - fireFrameworkEvent( - FrameworkEvent.ERROR, bundle, - new BundleException(ex.getMessage())); + throw new IllegalStateException("Bundle is uninstalled"); + } + else if (Util.isFragment(bundle.adapt(BundleRevision.class))) + { + throw new ClassNotFoundException("Fragments cannot load classes."); + } + else if (bundle.getState() == Bundle.INSTALLED) + { + try + { + resolveBundleRevision(bundle.adapt(BundleRevision.class)); + } + catch (BundleException ex) + { + // The spec says we must fire a framework error. + fireFrameworkEvent(FrameworkEvent.ERROR, bundle, ex); + // Then throw a class not found exception. + throw new ClassNotFoundException(name, ex); + } + } - throw ex; + // We do not call getClassLoader().loadClass() for arrays because + // it does not correctly handle array types, which is necessary in + // cases like deserialization using a wrapper class loader. + if ((name != null) && (name.length() > 0) && (name.charAt(0) == '[')) + { + return Class.forName(name, false, + ((BundleWiringImpl) bundle.adapt(BundleWiring.class)).getClassLoader()); } - return clazz; + + return ((BundleWiringImpl) + bundle.adapt(BundleWiring.class)).getClassLoader().loadClass(name); } /** * Implementation for Bundle.start(). **/ - protected void startBundle(BundleImpl bundle, boolean record) - throws BundleException + void startBundle(BundleImpl bundle, int options) throws BundleException { // CONCURRENCY NOTE: - // Starting a bundle may actually impact many bundles, since - // the bundle being started my need to be resolved, which in - // turn may need to resolve other bundles. Despite this fact, - // we only acquire the lock for the bundle being started, because - // when resolve is called on this bundle, it will eventually - // call resolve on the module loader search policy, which does - // its own locking on the module factory instance. Since the - // resolve algorithm is locking the module factory instance, it - // is not possible for other bundles to be installed or removed, - // so we don't have to worry about these possibilities. - // - // Further, if other bundles are started during this operation, - // then either they will resolve first because they got the lock - // on the module factory or we will resolve first since we got - // the lock on the module factory, so there should be no interference. - // If other bundles are stopped or uninstalled, this should pose - // no problems, since this does not impact their resolved state. - // If a refresh occurs, then the refresh algorithm ulimately has - // to acquire the module factory instance lock too before it can - // completely purge old modules, so it should also complete either - // before or after this bundle is started. At least that's the - // theory. + // We will first acquire the bundle lock for the specific bundle + // as long as the bundle is INSTALLED, RESOLVED, or ACTIVE. If this + // bundle is not yet resolved, then it will be resolved too. In + // that case, the global lock will be acquired to make sure no + // bundles can be installed or uninstalled during the resolve. + + int eventType; + boolean isTransient = (options & Bundle.START_TRANSIENT) != 0; // Acquire bundle lock. - acquireBundleLock(bundle); + try + { + acquireBundleLock(bundle, + Bundle.INSTALLED | Bundle.RESOLVED | Bundle.STARTING | Bundle.ACTIVE | Bundle.STOPPING); + } + catch (IllegalStateException ex) + { + if (bundle.getState() == Bundle.UNINSTALLED) + { + throw new IllegalStateException("Cannot start an uninstalled bundle."); + } + else + { + throw new BundleException( + "Bundle " + bundle + + " cannot be started: " + ex.getMessage()); + } + } + + // Record whether the bundle is using its declared activation policy. + boolean wasDeferred = bundle.isDeclaredActivationPolicyUsed() + && (((BundleRevisionImpl) bundle.adapt(BundleRevision.class)) + .getDeclaredActivationPolicy() == BundleRevisionImpl.LAZY_ACTIVATION); + bundle.setDeclaredActivationPolicyUsed( + (options & Bundle.START_ACTIVATION_POLICY) != 0); + BundleException rethrow = null; try { - _startBundle(bundle, record); + // The spec doesn't say whether it is possible to start an extension + // We just do nothing + if (bundle.isExtension()) + { + return; + } + + // As per the OSGi spec, fragment bundles can not be started and must + // throw a BundleException when there is an attempt to start one. + if (Util.isFragment(bundle.adapt(BundleRevision.class))) + { + throw new BundleException("Fragment bundles can not be started."); + } + + // Set and save the bundle's persistent state to active + // if we are supposed to record state change. + if (!isTransient) + { + if ((options & Bundle.START_ACTIVATION_POLICY) != 0) + { + bundle.setPersistentStateStarting(); + } + else + { + bundle.setPersistentStateActive(); + } + } + + // Check to see if the bundle's start level is greater than the + // the framework's target start level and if so just return. For + // transient starts we compare their start level to the current + // active start level, since we never process transient starts + // asynchronously (i.e., they are either satisfied and process + // synchronously here or they throw an exception). + int bundleLevel = bundle.getStartLevel(getInitialBundleStartLevel()); + if (isTransient && (bundleLevel > m_activeStartLevel)) + { + // Throw an exception for transient starts. + throw new BundleException( + "Cannot start bundle " + bundle + " because its start level is " + + bundleLevel + + ", which is greater than the framework's start level of " + + m_activeStartLevel + ".", BundleException.START_TRANSIENT_ERROR); + } + else if (bundleLevel > m_targetStartLevel) + { + // Ignore persistent starts that are not satisfied. + return; + } + + // Check to see if there is a start level change in progress and if + // so queue this bundle to the start level bundle queue for the start + // level thread and return, except for transient starts which are + // queued but processed synchronously. + // Note: Don't queue starts from the start level thread, otherwise + // we'd never get anything started. + if (!Thread.currentThread().getName().equals(FrameworkStartLevelImpl.THREAD_NAME)) + { + synchronized (m_startLevelBundles) + { + // Since we have the start level queue lock, we know the + // active start level cannot change. For transient starts + // we now need to double check and make sure we can still + // start them since technically the active start level could + // have changed. + if (isTransient && (bundleLevel > m_activeStartLevel)) + { + throw new BundleException( + "Cannot start bundle " + bundle + " because its start level is " + + bundleLevel + + ", which is greater than the framework's start level of " + + m_activeStartLevel + ".", BundleException.START_TRANSIENT_ERROR); + } + + // If the start level bundle queue is not empty, then we know + // there is a start level operation ongoing. We know that the + // bundle being started is satisfied by the target start level + // otherwise we wouldn't be here. In most cases we simply want + // to queue the bundle; however, if the bundle level is lower + // than the current active start level, then we want to handle + // it synchronously. This is because the start level thread + // ignores bundles that it should have already processed. + // So queue the bundle if its bundle start level is greater + // or equal to the active start level and then simply return + // since it will be processed asynchronously. + if (!m_startLevelBundles.isEmpty() + && (bundleLevel >= m_activeStartLevel)) + { + // Only add the bundle to the start level bundles + // being process if it is not already there. + boolean found = false; + for (StartLevelTuple tuple : m_startLevelBundles) + { + if (tuple.m_bundle == bundle) + { + found = true; + } + } + + if (!found) + { + m_startLevelBundles.add(new StartLevelTuple(bundle, bundleLevel)); + } + + // Note that although we queued the transiently started + // bundle, we don't return here because we handle all + // transient bundles synchronously. The reason why we + // queue it anyway is for the case where the start level + // is lowering, since the transiently started bundle may + // have already been processed by the start level thread + // and we will start it briefly here synchronously, but + // we want the start level thread to process it again + // so it gets another chance to stop it. This is not an + // issue when the start level is raising because the + // start level thread ignores non-persistently started + // bundles or if it is also persistently started it will + // be a no-op. + if (!isTransient) + { + return; + } + } + } + } + + switch (bundle.getState()) + { + case Bundle.UNINSTALLED: + throw new IllegalStateException("Cannot start an uninstalled bundle."); + case Bundle.STARTING: + if (!wasDeferred) + { + throw new BundleException( + "Bundle " + bundle + + " cannot be started, since it is starting."); + } + break; + case Bundle.STOPPING: + throw new BundleException( + "Bundle " + bundle + + " cannot be started, since it is stopping."); + case Bundle.ACTIVE: + return; + case Bundle.INSTALLED: + resolveBundleRevision(bundle.adapt(BundleRevision.class)); + // No break. + case Bundle.RESOLVED: + // Set the bundle's context. + bundle.setBundleContext(new BundleContextImpl(m_logger, this, bundle)); + // At this point, no matter if the bundle's activation policy is + // eager or deferred, we need to set the bundle's state to STARTING. + // We don't fire a BundleEvent here for this state change, since + // STARTING events are only fired if we are invoking the activator, + // which we may not do if activation is deferred. + setBundleStateAndNotify(bundle, Bundle.STARTING); + break; + } + + // If the bundle's activation policy is eager or activation has already + // been triggered, then activate the bundle immediately. + if (!bundle.isDeclaredActivationPolicyUsed() + || (((BundleRevisionImpl) bundle.adapt(BundleRevision.class)) + .getDeclaredActivationPolicy() != BundleRevisionImpl.LAZY_ACTIVATION) + || ((BundleClassLoader) bundle.adapt(BundleWiring.class).getClassLoader()) + .isActivationTriggered()) + { + // Record the event type for the final event and activate. + eventType = BundleEvent.STARTED; + // Note that the STARTING event is thrown in the activateBundle() method. + try + { + activateBundle(bundle, false); + } + catch (BundleException ex) + { + rethrow = ex; + } + } + // Otherwise, defer bundle activation. + else + { + // Record the event type for the final event. + eventType = BundleEvent.LAZY_ACTIVATION; + } + + // We still need to fire the STARTED event, but we will do + // it later so we can release the bundle lock. } finally { // Release bundle lock. releaseBundleLock(bundle); } - } - private void _startBundle(BundleImpl bundle, boolean record) - throws BundleException - { - // Set and save the bundle's persistent state to active - // if we are supposed to record state change. - if (record) + // If there was no exception, then we should fire the STARTED + // or LAZY_ACTIVATION event here without holding the lock. Otherwise, + // fire STOPPED and rethrow exception. + if (rethrow == null) + { + fireBundleEvent(eventType, bundle); + } + else { - bundle.getInfo().setPersistentStateActive(); + fireBundleEvent(BundleEvent.STOPPED, bundle); + throw rethrow; } + } - // Try to start the bundle. - BundleInfo info = bundle.getInfo(); + void activateBundle(BundleImpl bundle, boolean fireEvent) throws BundleException + { + // CONCURRENCY NOTE: + // We will first acquire the bundle lock for the specific bundle + // as long as the bundle is STARTING or ACTIVE, which is necessary + // because we may change the bundle state. - // Ignore bundles whose persistent state is not active - // or whose start level is greater than the framework's. - if ((info.getPersistentState() != Bundle.ACTIVE) - || (info.getStartLevel(getInitialBundleStartLevel()) > getStartLevel())) + // Acquire bundle lock. + try { - return; + acquireBundleLock(bundle, Bundle.STARTING | Bundle.ACTIVE); } - - switch (info.getState()) + catch (IllegalStateException ex) { - case Bundle.UNINSTALLED: - throw new IllegalStateException("Cannot start an uninstalled bundle."); - case Bundle.STARTING: - case Bundle.STOPPING: - throw new BundleException("Starting a bundle that is starting or stopping is currently not supported."); - case Bundle.ACTIVE: - return; - case Bundle.INSTALLED: - _resolveBundle(bundle); - // No break. - case Bundle.RESOLVED: - info.setState(Bundle.STARTING); - fireBundleEvent(BundleEvent.STARTING, bundle); - break; + throw new IllegalStateException( + "Activation only occurs for bundles in STARTING state."); } try { - // Set the bundle's context. - info.setContext(new BundleContextImpl(this, bundle)); + // If the bundle is already active or its start level is not met, + // simply return. Generally, the bundle start level should always + // be less than or equal to the active start level since the bundle + // must be in the STARTING state to activate it. One potential corner + // case is if the bundle is being lazily activated at the same time + // there is a start level change going on to lower the start level. + // In that case, we test here and avoid activating the bundle since + // it will be stopped by the start level thread. + if ((bundle.getState() == Bundle.ACTIVE) || + (bundle.getStartLevel(getInitialBundleStartLevel()) > m_targetStartLevel)) + { + return; + } + + Throwable rethrow = null; + try + { + // Set the bundle's activator. + bundle.setActivator(createBundleActivator(bundle)); + } + catch (Throwable th) + { + rethrow = th; + } + + try + { + // Fire STARTING event to signify call to bundle activator. + fireBundleEvent(BundleEvent.STARTING, bundle); + + if (rethrow != null) + { + throw rethrow; + } + // Activate the bundle if it has an activator. + if (bundle.getActivator() != null) + { + m_secureAction.startActivator( + bundle.getActivator(), bundle._getBundleContext()); + } - // Set the bundle's activator. - info.setActivator(createBundleActivator(bundle.getInfo())); + setBundleStateAndNotify(bundle, Bundle.ACTIVE); - // Activate the bundle if it has an activator. - if (bundle.getInfo().getActivator() != null) + // We still need to fire the STARTED event, but we will do + // it later so we can release the bundle lock. + } + catch (Throwable th) { - m_secureAction.startActivator(info.getActivator(), - info.getContext()); + // Spec says we must fire STOPPING event. + fireBundleEvent(BundleEvent.STOPPING, bundle); + + // If there was an error starting the bundle, + // then reset its state to RESOLVED. + setBundleStateAndNotify(bundle, Bundle.RESOLVED); + + // Clean up the bundle activator + bundle.setActivator(null); + + // Clean up the bundle context. + // We invalidate this first to make sure it cannot be used + // after stopping the activator. + BundleContextImpl bci = (BundleContextImpl) bundle._getBundleContext(); + bci.invalidate(); + bundle.setBundleContext(null); + + // Unregister any services offered by this bundle. + m_registry.unregisterServices(bundle); + + // Release any services being used by this bundle. + m_registry.ungetServices(bundle); + + // Remove any listeners registered by this bundle. + m_dispatcher.removeListeners(bci); + + // The spec says to expect BundleException or + // SecurityException, so rethrow these exceptions. + if (th instanceof BundleException) + { + throw (BundleException) th; + } + else if ((System.getSecurityManager() != null) && + (th instanceof java.security.PrivilegedActionException)) + { + th = ((java.security.PrivilegedActionException) th).getException(); + } + + // Rethrow all other exceptions as a BundleException. + throw new BundleException( + "Activator start error in bundle " + bundle + ".", + BundleException.ACTIVATOR_ERROR, th); } + } + finally + { + // Release bundle lock. + releaseBundleLock(bundle); + } - // TODO: CONCURRENCY - Reconsider firing event outside of the - // bundle lock. - info.setState(Bundle.ACTIVE); + // If there was no exception, then we should fire the STARTED + // event here without holding the lock if specified. + // TODO: LAZY - It would be nice to figure out how to do this without + // duplicating code; this method is called from two different + // places -- one fires the event itself the other one needs it. + if (fireEvent) + { fireBundleEvent(BundleEvent.STARTED, bundle); } - catch (Throwable th) + } + + void updateBundle(BundleImpl bundle, InputStream is) + throws BundleException + { + // Acquire bundle lock. + try + { + acquireBundleLock(bundle, Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE | Bundle.STARTING | Bundle.STOPPING); + } + catch (IllegalStateException ex) { - // If there was an error starting the bundle, - // then reset its state to RESOLVED. - info.setState(Bundle.RESOLVED); + if (bundle.getState() == Bundle.UNINSTALLED) + { + throw new IllegalStateException("Cannot update an uninstalled bundle."); + } + else + { + throw new BundleException( + "Bundle " + bundle + + " cannot be update: " + ex.getMessage()); + } + } - // Clean up the bundle context. - ((BundleContextImpl) info.getContext()).invalidate(); - info.setContext(null); + // We must release the lock and close the input stream, so do both + // in a finally block. + try + { + // Check if the bundle is not currently STARTING or STOPPING because if it is + // we are in a loop where the bundle being started or stopped triggered an update + // of itself (either directly or indirectly) which we can not handle. + if ((bundle.getState() == Bundle.STARTING && (!bundle.isDeclaredActivationPolicyUsed() + || ((BundleRevisionImpl) bundle.adapt(BundleRevision.class)) + .getDeclaredActivationPolicy() != BundleRevisionImpl.LAZY_ACTIVATION )) || + bundle.getState() == Bundle.STOPPING) + { + throw new IllegalStateException("Bundle " + bundle + + " cannot be update, since it is either STARTING or STOPPING."); + } + + // Variable to indicate whether bundle is active or not. + Throwable rethrow = null; - // Unregister any services offered by this bundle. - m_registry.unregisterServices(bundle); + final int oldState = bundle.getState(); - // Release any services being used by this bundle. - m_registry.ungetServices(bundle); + // First get the update-URL from our header. + String updateLocation = (String) + ((BundleRevisionImpl) bundle.adapt(BundleRevision.class)) + .getHeaders().get(Constants.BUNDLE_UPDATELOCATION); - // Remove any listeners registered by this bundle. - m_dispatcher.removeListeners(bundle); + // If no update location specified, use original location. + if (updateLocation == null) + { + updateLocation = bundle._getLocation(); + } - // The spec says to expect BundleException or - // SecurityException, so rethrow these exceptions. - if (th instanceof BundleException) + // Stop the bundle if it is active, but do not change its + // persistent state. + if (oldState == Bundle.ACTIVE) { - throw (BundleException) th; + stopBundle(bundle, false); } - else if (th instanceof SecurityException) + + try { - throw (SecurityException) th; + // Revising the bundle creates a new revision, which modifies + // the global state, so we need to acquire the global lock + // before revising. + boolean locked = acquireGlobalLock(); + if (!locked) + { + throw new BundleException( + "Cannot acquire global lock to update the bundle."); + } + try + { + // Try to revise. + boolean wasExtension = bundle.isExtension(); + bundle.revise(updateLocation, is); + + // Verify bundle revision. + try + { + Object sm = System.getSecurityManager(); + + if (sm != null) + { + ((SecurityManager) sm).checkPermission( + new AdminPermission(bundle, AdminPermission.LIFECYCLE)); + } + + // If this is an update from a normal to an extension bundle + // then attach the extension + if (!wasExtension && bundle.isExtension()) + { + m_extensionManager.addExtensionBundle(bundle); + } + else if (wasExtension) + { + setBundleStateAndNotify(bundle, Bundle.INSTALLED); + } + } + catch (Throwable ex) + { + try + { + bundle.rollbackRevise(); + } + catch (Exception busted) + { + m_logger.log( + bundle, Logger.LOG_ERROR, "Unable to rollback.", busted); + } + + throw ex; + } + } + finally + { + // Always release the global lock. + releaseGlobalLock(); + } } - else if ((System.getSecurityManager() != null) && - (th instanceof java.security.PrivilegedActionException)) + catch (Throwable ex) { - th = ((java.security.PrivilegedActionException) th).getException(); + m_logger.log( + bundle, Logger.LOG_ERROR, "Unable to update the bundle.", ex); + rethrow = ex; } - // Rethrow all other exceptions as a BundleException. - throw new BundleException("Activator start error.", th); - } - } + // Set new state, mark as needing a refresh, and fire updated event + // if successful. + if (rethrow == null) + { + bundle.setLastModified(System.currentTimeMillis()); - protected void _resolveBundle(BundleImpl bundle) - throws BundleException - { - // If a security manager is installed, then check for permission - // to import the necessary packages. - if (System.getSecurityManager() != null) - { + if (!bundle.isExtension()) + { + setBundleStateAndNotify(bundle, Bundle.INSTALLED); + } - ProtectionDomain pd = (ProtectionDomain) - bundle.getInfo().getCurrentModule().getSecurityContext(); + for (Bundle extension : m_extensionManager.resolveExtensionBundles(this)) + { + m_extensionManager.startExtensionBundle(this, (BundleImpl) extension); + } - R4Import[] imports = - bundle.getInfo().getCurrentModule().getDefinition().getImports(); + fireBundleEvent(BundleEvent.UNRESOLVED, bundle); - for (int i = 0;i < imports.length; i++) - { - PackagePermission perm = new PackagePermission(imports[i].getName(), - PackagePermission.IMPORT); + fireBundleEvent(BundleEvent.UPDATED, bundle); - if (!pd.implies(perm)) + // Acquire global lock to check if we should auto-refresh. + boolean locked = acquireGlobalLock(); + // If we did not get the global lock, then do not try to + // auto-refresh. + if (locked) { - throw new java.security.AccessControlException( - "PackagePermission.IMPORT denied for import: " + - imports[i].getName(), perm); + try + { + if (!m_dependencies.hasDependents(bundle) + && !bundle.isExtension()) + { + try + { + List list = new ArrayList(1); + list.add(bundle); + refreshPackages(list, null); + } + catch (Exception ex) + { + m_logger.log(bundle, + Logger.LOG_ERROR, + "Unable to immediately purge the bundle revisions.", ex); + } + } + } + finally + { + // Always release the global lock. + releaseGlobalLock(); + } } } - // Check export permission for all exports of the current module. - R4Export[] implicitImports = - bundle.getInfo().getCurrentModule().getDefinition().getExports(); - for (int i = 0;i < implicitImports.length; i++) + // If the old state was active, but the new revision is a fragment, + // then mark the persistent state to inactive. + if ((oldState == Bundle.ACTIVE) + && Util.isFragment(bundle.adapt(BundleRevision.class))) { - PackagePermission perm = new PackagePermission( - implicitImports[i].getName(), PackagePermission.EXPORT); + bundle.setPersistentStateInactive(); + m_logger.log(bundle, Logger.LOG_WARNING, + "Previously active bundle was updated to a fragment, resetting state to inactive: " + + bundle); + } + // Otherwise, restart the bundle if it was previously active, + // but do not change its persistent state. + else if (oldState == Bundle.ACTIVE) + { + startBundle(bundle, Bundle.START_TRANSIENT); + } - if (!pd.implies(perm)) + // If update failed, rethrow exception. + if (rethrow != null) + { + if (rethrow instanceof AccessControlException) { - throw new java.security.AccessControlException( - "PackagePermission.EXPORT denied for implicit export: " + - implicitImports[i].getName(), perm); + throw (AccessControlException) rethrow; } - } - } - - IModule module = bundle.getInfo().getCurrentModule(); - try - { - m_policyCore.resolve(module); + else if (rethrow instanceof BundleException) + { + throw (BundleException) rethrow; + } + else + { + throw new BundleException( + "Update of bundle " + bundle + " failed.", rethrow); + } + } } - catch (ResolveException ex) + finally { - if (ex.getModule() != null) + // Close the input stream. + try { - throw new BundleException( - "Unresolved package in bundle " - + Util.getBundleIdFromModuleId(ex.getModule().getId()) - + ": " + ex.getPackage()); + if (is != null) is.close(); } - else + catch (Exception ex) { - throw new BundleException(ex.getMessage()); + m_logger.log(bundle, Logger.LOG_ERROR, "Unable to close input stream.", ex); } + + // Release bundle lock. + releaseBundleLock(bundle); } } - protected void updateBundle(BundleImpl bundle, InputStream is) + void stopBundle(BundleImpl bundle, boolean record) throws BundleException { // Acquire bundle lock. - acquireBundleLock(bundle); - try { - _updateBundle(bundle, is); + acquireBundleLock(bundle, + Bundle.INSTALLED | Bundle.RESOLVED | Bundle.STARTING | Bundle.ACTIVE | Bundle.STOPPING); } - finally + catch (IllegalStateException ex) { - // Release bundle lock. - releaseBundleLock(bundle); + if (bundle.getState() == Bundle.UNINSTALLED) + { + throw new IllegalStateException("Cannot stop an uninstalled bundle."); + } + else + { + throw new BundleException( + "Bundle " + bundle + + " cannot be stopped: " + ex.getMessage()); + } } - } - - protected void _updateBundle(BundleImpl bundle, InputStream is) - throws BundleException - { - // We guarantee to close the input stream, so put it in a - // finally clause. try { - // Variable to indicate whether bundle is active or not. - Exception rethrow = null; + Throwable rethrow = null; - // Cannot update an uninstalled bundle. - BundleInfo info = bundle.getInfo(); - if (info.getState() == Bundle.UNINSTALLED) + // Set the bundle's persistent state to inactive if necessary. + if (record) { - throw new IllegalStateException("The bundle is uninstalled."); + bundle.setPersistentStateInactive(); } - // First get the update-URL from our header. - String updateLocation = (String) - info.getCurrentHeader().get(Constants.BUNDLE_UPDATELOCATION); + // If the bundle is not persistently started, then we + // need to reset the activation policy flag, since it + // does not persist across persistent stops or transient + // stops. + if (!isBundlePersistentlyStarted(bundle)) + { + bundle.setDeclaredActivationPolicyUsed(false); + } - // If no update location specified, use original location. - if (updateLocation == null) + // As per the OSGi spec, fragment bundles can not be stopped and must + // throw a BundleException when there is an attempt to stop one. + if (Util.isFragment(bundle.adapt(BundleRevision.class))) + { + throw new BundleException("Fragment bundles can not be stopped: " + bundle); + } + + boolean wasActive = false; + switch (bundle.getState()) { - updateLocation = info.getLocation(); + case Bundle.UNINSTALLED: + throw new IllegalStateException("Cannot stop an uninstalled bundle."); + case Bundle.STARTING: + if (bundle.isDeclaredActivationPolicyUsed() + && ((BundleRevisionImpl) bundle.adapt(BundleRevision.class)) + .getDeclaredActivationPolicy() != BundleRevisionImpl.LAZY_ACTIVATION) + { + throw new BundleException( + "Stopping a starting or stopping bundle is currently not supported."); + } + break; + case Bundle.STOPPING: + throw new BundleException( + "Stopping a starting or stopping bundle is currently not supported."); + case Bundle.INSTALLED: + case Bundle.RESOLVED: + return; + case Bundle.ACTIVE: + wasActive = true; + break; } - // Stop the bundle, but do not change the persistent state. - stopBundle(bundle, false); + // At this point, no matter if the bundle's activation policy is + // eager or deferred, we need to set the bundle's state to STOPPING + // and fire the STOPPING event. + setBundleStateAndNotify(bundle, Bundle.STOPPING); + fireBundleEvent(BundleEvent.STOPPING, bundle); - try + // If the bundle was active, then invoke the activator stop() method + // or if we are stopping the system bundle. + if ((wasActive) || (bundle.getBundleId() == 0)) { - // Get the bundle's archive. - BundleArchive archive = m_cache.getArchive(info.getBundleId()); - // Update the bundle; this operation will increase - // the revision count for the bundle. - archive.revise(updateLocation, is); - // Create a module for the new revision; the revision is - // base zero, so subtract one from the revision count to - // get the revision of the new update. try { - IModule module = createModule( - info.getBundleId(), - archive.getRevisionCount() - 1, - info.getCurrentHeader()); - - Object sm = System.getSecurityManager(); - - if (sm != null) + if (bundle.getActivator() != null) { - ((SecurityManager) sm).checkPermission( - new AdminPermission(bundle, AdminPermission.LIFECYCLE)); + m_secureAction.stopActivator(bundle.getActivator(), bundle._getBundleContext()); } - - // Add module to bundle info. - info.addModule(module); } - catch (Exception ex) + catch (Throwable th) { - try - { - archive.undoRevise(); - } - catch (Exception busted) - { - m_logger.log(Logger.LOG_ERROR, "Unable to rollback.", busted); - } - - throw ex; + m_logger.log(bundle, Logger.LOG_ERROR, "Error stopping bundle.", th); + rethrow = th; } } - catch (Exception ex) - { - m_logger.log(Logger.LOG_ERROR, "Unable to update the bundle.", ex); - rethrow = ex; - } - // Set new state, mark as needing a refresh, and fire updated event - // if successful. - if (rethrow == null) + // Do not clean up after the system bundle since it will + // clean up after itself. + if (bundle.getBundleId() != 0) { - info.setLastModified(System.currentTimeMillis()); - info.setState(Bundle.INSTALLED); - fireBundleEvent(BundleEvent.UNRESOLVED, bundle); + // Clean up the bundle activator. + bundle.setActivator(null); - // Mark previous the bundle's old module for removal since - // it can no longer be used to resolve other modules per the spec. - ((ModuleImpl) info.getModules()[info.getModules().length - 2]) - .setRemovalPending(true); + // Clean up the bundle context. + // We invalidate this first to make sure it cannot be used + // after stopping the activator. + BundleContextImpl bci = (BundleContextImpl) bundle._getBundleContext(); + bci.invalidate(); + bundle.setBundleContext(null); - fireBundleEvent(BundleEvent.UPDATED, bundle); - } + // Unregister any services offered by this bundle. + m_registry.unregisterServices(bundle); - // Restart bundle, but do not change the persistent state. - // This will not start the bundle if it was not previously - // active. - startBundle(bundle, false); + // Release any services being used by this bundle. + m_registry.ungetServices(bundle); - // If update failed, rethrow exception. + // The spec says that we must remove all event + // listeners for a bundle when it is stopped. + m_dispatcher.removeListeners(bci); + + setBundleStateAndNotify(bundle, Bundle.RESOLVED); + + // We still need to fire the STOPPED event, but we will do + // it later so we can release the bundle lock. + } + + // Throw activator error if there was one. if (rethrow != null) { - if ((System.getSecurityManager() != null) && - (rethrow instanceof SecurityException)) + // The spec says to expect BundleException or + // SecurityException, so rethrow these exceptions. + if (rethrow instanceof BundleException) + { + throw (BundleException) rethrow; + } + else if ((System.getSecurityManager() != null) && + (rethrow instanceof java.security.PrivilegedActionException)) { - throw (SecurityException) rethrow; + rethrow = ((java.security.PrivilegedActionException) rethrow).getException(); } - throw new BundleException("Update failed.", rethrow); - } - } - finally - { - try - { - if (is != null) is.close(); - } - catch (IOException ex) - { - m_logger.log(Logger.LOG_ERROR, "Unable to close input stream.", ex); + // Rethrow all other exceptions as a BundleException. + throw new BundleException( + "Activator stop error in bundle " + bundle + ".", rethrow); } } - } - - protected void stopBundle(BundleImpl bundle, boolean record) - throws BundleException - { - // Acquire bundle lock. - acquireBundleLock(bundle); - - try - { - _stopBundle(bundle, record); - } finally { // Always release bundle lock. releaseBundleLock(bundle); } + + // If there was no exception, then we should fire the STOPPED event + // here without holding the lock. + fireBundleEvent(BundleEvent.STOPPED, bundle); } - private void _stopBundle(BundleImpl bundle, boolean record) - throws BundleException + void uninstallBundle(BundleImpl bundle) throws BundleException { - Throwable rethrow = null; - - // Set the bundle's persistent state to inactive if necessary. - if (record) + // Acquire bundle lock. + try { - bundle.getInfo().setPersistentStateInactive(); + acquireBundleLock(bundle, + Bundle.INSTALLED | Bundle.RESOLVED | Bundle.STARTING | Bundle.ACTIVE | Bundle.STOPPING); } - - BundleInfo info = bundle.getInfo(); - - switch (info.getState()) + catch (IllegalStateException ex) { - case Bundle.UNINSTALLED: - throw new IllegalStateException("Cannot stop an uninstalled bundle."); - case Bundle.STARTING: - case Bundle.STOPPING: - throw new BundleException("Stopping a bundle that is starting or stopping is currently not supported."); - case Bundle.INSTALLED: - case Bundle.RESOLVED: - return; - case Bundle.ACTIVE: - // Set bundle state.. - info.setState(Bundle.STOPPING); - fireBundleEvent(BundleEvent.STOPPING, bundle); - break; + if (bundle.getState() == Bundle.UNINSTALLED) + { + throw new IllegalStateException("Cannot uninstall an uninstalled bundle."); + } + else + { + throw new BundleException( + "Bundle " + bundle + + " cannot be uninstalled: " + ex.getMessage()); + } } try { - if (bundle.getInfo().getActivator() != null) + // Check if the bundle is not currently STARTING or STOPPING because if it is + // we are in a loop where the bundle being started or stopped triggered a state change + // of itself (either directly or indirectly) which we can not handle. + if ((bundle.getState() == Bundle.STARTING && (!bundle.isDeclaredActivationPolicyUsed() + || ((BundleRevisionImpl) bundle.adapt(BundleRevision.class)) + .getDeclaredActivationPolicy() != BundleRevisionImpl.LAZY_ACTIVATION )) || + bundle.getState() == Bundle.STOPPING) { - m_secureAction.stopActivator(info.getActivator(), - info.getContext()); + throw new IllegalStateException("Bundle " + bundle + + " cannot be uninstalled, since it is either STARTING or STOPPING."); } - // Try to save the activator in the cache. - // NOTE: This is non-standard OSGi behavior and only - // occurs if strictness is disabled. - String strict = m_config.get(FelixConstants.STRICT_OSGI_PROP); - boolean isStrict = (strict == null) ? true : strict.equals("true"); - if (!isStrict) + // The spec says that uninstall should always succeed, so + // catch an exception here if stop() doesn't succeed and + // rethrow it at the end. + if (!bundle.isExtension() && (bundle.getState() == Bundle.ACTIVE)) { try { - m_cache.getArchive(info.getBundleId()) - .setActivator(info.getActivator()); + stopBundle(bundle, true); } - catch (Exception ex) + catch (BundleException ex) { - // Problem saving activator, so ignore it. - // TODO: Perhaps we should handle this some other way? + fireFrameworkEvent(FrameworkEvent.ERROR, bundle, ex); } } - } - catch (Throwable th) - { - m_logger.log(Logger.LOG_ERROR, "Error stopping bundle.", th); - rethrow = th; - } - - // Clean up the bundle context. - ((BundleContextImpl) info.getContext()).invalidate(); - info.setContext(null); - - // Unregister any services offered by this bundle. - m_registry.unregisterServices(bundle); - // Release any services being used by this bundle. - m_registry.ungetServices(bundle); - - // The spec says that we must remove all event - // listeners for a bundle when it is stopped. - m_dispatcher.removeListeners(bundle); + // Remove the bundle from the installed map. + BundleImpl target = null; + // Acquire global lock. + boolean locked = acquireGlobalLock(); + if (!locked) + { + // If the calling thread holds bundle locks, then we might not + // be able to get the global lock. + throw new IllegalStateException( + "Unable to acquire global lock to remove bundle."); + } + try + { + // Use a copy-on-write approach to remove the bundle + // from the installed maps. + Map[] maps = new Map[] { + new HashMap(m_installedBundles[LOCATION_MAP_IDX]), + new TreeMap(m_installedBundles[IDENTIFIER_MAP_IDX]) + }; + target = (BundleImpl) maps[LOCATION_MAP_IDX].remove(bundle._getLocation()); + if (target != null) + { + maps[IDENTIFIER_MAP_IDX].remove(new Long(target.getBundleId())); + m_installedBundles = maps; - info.setState(Bundle.RESOLVED); - fireBundleEvent(BundleEvent.STOPPED, bundle); + // Set the bundle's persistent state to uninstalled. + bundle.setPersistentStateUninstalled(); - // Throw activator error if there was one. - if (rethrow != null) - { - // The spec says to expect BundleException or - // SecurityException, so rethrow these exceptions. - if (rethrow instanceof BundleException) - { - throw (BundleException) rethrow; + // Put bundle in uninstalled bundle array. + rememberUninstalledBundle(bundle); + } } - else if (rethrow instanceof SecurityException) + finally { - throw (SecurityException) rethrow; + releaseGlobalLock(); } - else if ((System.getSecurityManager() != null) && - (rethrow instanceof java.security.PrivilegedActionException)) + + if (target == null) { - rethrow = ((java.security.PrivilegedActionException) rethrow).getException(); + m_logger.log(bundle, + Logger.LOG_ERROR, "Unable to remove bundle from installed map!"); } - // Rethrow all other exceptions as a BundleException. - throw new BundleException("Activator stop error.", rethrow); - } - } + setBundleStateAndNotify(bundle, Bundle.INSTALLED); - protected void uninstallBundle(BundleImpl bundle) throws BundleException - { - // Acquire bundle lock. - acquireBundleLock(bundle); + // Unfortunately, fire UNRESOLVED event while holding the lock, + // since we still need to change the bundle state. + fireBundleEvent(BundleEvent.UNRESOLVED, bundle); - try - { - _uninstallBundle(bundle); + // Set state to uninstalled. + setBundleStateAndNotify(bundle, Bundle.UNINSTALLED); + bundle.setLastModified(System.currentTimeMillis()); } finally { // Always release bundle lock. releaseBundleLock(bundle); } - } - private void _uninstallBundle(BundleImpl bundle) throws BundleException - { - BundleInfo info = bundle.getInfo(); - if (info.getState() == Bundle.UNINSTALLED) - { - throw new IllegalStateException("The bundle is uninstalled."); - } + // Fire UNINSTALLED event without holding the lock. + fireBundleEvent(BundleEvent.UNINSTALLED, bundle); - // The spec says that uninstall should always succeed, so - // catch an exception here if stop() doesn't succeed and - // rethrow it at the end. - try + // Acquire global lock to check if we should auto-refresh. + boolean locked = acquireGlobalLock(); + if (locked) { - stopBundle(bundle, true); - } - catch (BundleException ex) - { - fireFrameworkEvent(FrameworkEvent.ERROR, bundle, ex); - } + // Populate a set of refresh candidates. This also includes any bundles that this bundle + // is wired to but have previously been uninstalled. + Set refreshCandidates = addUninstalled(bundle, new LinkedHashSet()); - // Remove the bundle from the installed map. - BundleImpl target = null; - synchronized (m_installedBundleLock_Priority2) - { - target = (BundleImpl) m_installedBundleMap.remove(info.getLocation()); + try + { + // First see if we can throw away the complete graph + Set dependent = new HashSet(); + for (Bundle b : refreshCandidates) + { + populateDependentGraph(b, dependent); + } + + if (refreshCandidates.containsAll(dependent)) + { + try + { + refreshPackages(refreshCandidates, null); + } + catch (Exception ex) + { + m_logger.log(this, Logger.LOG_ERROR, "Unable to immediately garbage collect bundles.", ex); + } + } + // Otherwise, try to remove one candidate at a time + else + { + boolean progress; + do + { + // The idea is to keep trying as long as we make progress (ie., managed to gc a bundle) + progress = false; + for (Iterator iter = refreshCandidates.iterator(); iter.hasNext();) + { + Bundle b = iter.next(); + // If the bundle is not used by anyone, then garbage + // collect it now. + if (!m_dependencies.hasDependents(b)) + { + iter.remove(); + try + { + List list = Collections.singletonList(b); + refreshPackages(list, null); + progress = true; + } + catch (Exception ex) + { + m_logger.log(b, Logger.LOG_ERROR, "Unable to immediately garbage collect the bundle.", ex); + } + } + } + } while (progress); + } + } + finally + { + // Always release the global lock. + releaseGlobalLock(); + } } + } - // Finally, put the uninstalled bundle into the - // uninstalled list for subsequent refreshing. - if (target != null) + private Set addUninstalled(Bundle bundle, Set refreshCandidates) + { + refreshCandidates.add(bundle); // Add this bundle first, so that it gets refreshed first + BundleRevisions bundleRevisions = bundle.adapt(BundleRevisions.class); + if (bundleRevisions != null) { - // Set the bundle's persistent state to uninstalled. - target.getInfo().setPersistentStateUninstalled(); - - // Mark current module for removal since it can no longer - // be used to resolve other modules per the spec. - ((ModuleImpl) target.getInfo().getCurrentModule()).setRemovalPending(true); - - // Put bundle in uninstalled bundle array. - rememberUninstalledBundle(bundle); + for (BundleRevision br : bundleRevisions.getRevisions()) + { + BundleWiring bw = br.getWiring(); + if (bw != null) + { + for (BundleWire wire : bw.getRequiredWires(null)) + { + Bundle b = wire.getProvider().getBundle(); + if (b.getState() == Bundle.UNINSTALLED && !refreshCandidates.contains(b)) + refreshCandidates = addUninstalled(b, refreshCandidates); + } + } + } } - else + Set dependent = populateDependentGraph(bundle, new HashSet()); + for (Bundle b : dependent) { - m_logger.log( - Logger.LOG_ERROR, "Unable to remove bundle from installed map!"); + if (b.getState() == Bundle.UNINSTALLED && !refreshCandidates.contains(b)) + { + refreshCandidates = addUninstalled(b, refreshCandidates); + } } - - // Set state to uninstalled. - info.setState(Bundle.UNINSTALLED); - info.setLastModified(System.currentTimeMillis()); - - // Fire bundle event. - fireBundleEvent(BundleEvent.UNINSTALLED, bundle); + return refreshCandidates; } // @@ -1666,24 +3085,121 @@ private void _uninstallBundle(BundleImpl bundle) throws BundleException * @param key The name of the property to retrieve. * @return The value of the specified property or null. **/ - protected String getProperty(String key) + String getProperty(String key) + { + // First, check the config properties. + Object val = m_configMap.get(key); + // If not found, then try the system properties. + return !(val instanceof String) ? System.getProperty(key) : (String) val; + } + + String _getProperty(String key) { // First, check the config properties. - String val = (String) m_config.get(key); + Object val = m_configMap.get(key); // If not found, then try the system properties. - return (val == null) ? System.getProperty(key) : val; + return !(val instanceof String) ? m_secureAction.getSystemProperty(key, null) : (String) val; } - protected Bundle installBundle(String location, InputStream is) - throws BundleException - { - return installBundle(-1, location, is); + private Bundle reloadBundle(BundleArchive ba, boolean updateMulti) + throws BundleException + { + BundleImpl bundle = null; + + // Try to purge old revisions before installing; + // this is done just in case a "refresh" didn't + // occur last session...this would only be due to + // an error or system crash. + try + { + if (ba.isRemovalPending()) + { + ba.purge(); + } + } + catch (Exception ex) + { + m_logger.log( + Logger.LOG_ERROR, + "Could not purge bundle.", ex); + } + + try + { + // Acquire the global lock to create the bundle, + // since this impacts the global state. + boolean locked = acquireGlobalLock(); + if (!locked) + { + throw new BundleException( + "Unable to acquire the global lock to install the bundle."); + } + try + { + bundle = new BundleImpl(this, null, ba); + + if (updateMulti) + { + try + { + if ("true".equals(bundle.adapt(BundleRevisionImpl.class).getHeaders().get("Multi-Release"))) + { + ba.setLastModified(System.currentTimeMillis()); + } + } + catch (Exception ex) + { + getLogger().log(this, Logger.LOG_WARNING, "Unable to update multi-release bundle last modified", ex); + } + } + + // Extensions are handled as a special case. + if (bundle.isExtension()) + { + m_extensionManager.addExtensionBundle(bundle); + } + + // Use a copy-on-write approach to add the bundle + // to the installed maps. + Map[] maps = new Map[] { + new HashMap(m_installedBundles[LOCATION_MAP_IDX]), + new TreeMap(m_installedBundles[IDENTIFIER_MAP_IDX]) + }; + maps[LOCATION_MAP_IDX].put(bundle._getLocation(), bundle); + maps[IDENTIFIER_MAP_IDX].put(new Long(bundle.getBundleId()), bundle); + m_installedBundles = maps; + } + finally + { + // Always release the global lock. + releaseGlobalLock(); + } + } + catch (Throwable ex) + { + if (ex instanceof BundleException) + { + throw (BundleException) ex; + } + else if (ex instanceof AccessControlException) + { + throw (AccessControlException) ex; + } + else + { + throw new BundleException("Could not create bundle object.", ex); + } + } + + return bundle; } - private Bundle installBundle(long id, String location, InputStream is) + Bundle installBundle( + Bundle origin, String location, InputStream is) throws BundleException { - BundleImpl bundle = null; + BundleArchive ba = null; + BundleImpl existing, bundle = null; // Acquire an install lock. acquireInstallLock(location); @@ -1691,33 +3207,24 @@ private Bundle installBundle(long id, String location, InputStream is) try { // Check to see if the framework is still running; - if ((getStatus() == Felix.STOPPING_STATUS) || - (getStatus() == Felix.INITIAL_STATUS)) + if ((getState() == Bundle.STOPPING) || + (getState() == Bundle.UNINSTALLED)) { throw new BundleException("The framework has been shutdown."); } // If bundle location is already installed, then // return it as required by the OSGi specification. - bundle = (BundleImpl) getBundle(location); - if (bundle != null) - { - return bundle; - } - - // Determine if this is a new or existing bundle. - boolean isNew = (id < 0); - - // If the bundle is new we must cache its JAR file. - if (isNew) + existing = (BundleImpl) getBundle(location); + if (existing == null) { // First generate an identifier for it. - id = getNextId(); + long id = getNextId(); try { // Add the bundle to the cache. - m_cache.create(id, location, is); + ba = m_cache.create(id, getInitialBundleStartLevel(), location, is); } catch (Exception ex) { @@ -1737,81 +3244,105 @@ private Bundle installBundle(long id, String location, InputStream is) "Unable to close input stream.", ex); } } - } - else - { - // If the bundle we are installing is not new, - // then try to purge old revisions before installing - // it; this is done just in case a "refresh" - // didn't occur last session...this would only be - // due to an error or system crash. + try { - if (m_cache.getArchive(id).getRevisionCount() > 1) + // Acquire the global lock to create the bundle, + // since this impacts the global state. + boolean locked = acquireGlobalLock(); + if (!locked) { - m_cache.getArchive(id).purge(); + throw new BundleException( + "Unable to acquire the global lock to install the bundle."); + } + try + { + bundle = new BundleImpl(this, origin, ba); + } + finally + { + // Always release the global lock. + releaseGlobalLock(); } - } - catch (Exception ex) - { - m_logger.log( - Logger.LOG_ERROR, - "Could not purge bundle.", ex); - } - } - - try - { - BundleArchive archive = m_cache.getArchive(id); - bundle = new BundleImpl(this, createBundleInfo(archive)); - - Object sm = System.getSecurityManager(); - if (sm != null) - { - ((SecurityManager) sm).checkPermission( - new AdminPermission(bundle, AdminPermission.LIFECYCLE)); + if (!bundle.isExtension()) + { + Object sm = System.getSecurityManager(); + if (sm != null) + { + ((SecurityManager) sm).checkPermission( + new AdminPermission(bundle, AdminPermission.LIFECYCLE)); + } + } + else + { + m_extensionManager.addExtensionBundle(bundle); + } } - } - catch (Exception ex) - { - // If the bundle is new, then remove it from the cache. - // TODO: Perhaps it should be removed if it is not new too. - if (isNew) + catch (Throwable ex) { + // Remove bundle from the cache. try { - m_cache.remove(m_cache.getArchive(id)); + if (bundle != null) + { + bundle.closeAndDelete(); + } + else if (ba != null) + { + ba.closeAndDelete(); + } } catch (Exception ex1) { - m_logger.log( + m_logger.log(bundle, Logger.LOG_ERROR, "Could not remove from cache.", ex1); } + if (ex instanceof BundleException) + { + throw (BundleException) ex; + } + else if (ex instanceof AccessControlException) + { + throw (AccessControlException) ex; + } + else + { + throw new BundleException("Could not create bundle object.", ex); + } } - if ((System.getSecurityManager() != null) && - (ex instanceof SecurityException)) + // Acquire global lock. + boolean locked = acquireGlobalLock(); + if (!locked) { - throw (SecurityException) ex; + // If the calling thread holds bundle locks, then we might not + // be able to get the global lock. + throw new IllegalStateException( + "Unable to acquire global lock to add bundle."); + } + try + { + // Use a copy-on-write approach to add the bundle + // to the installed maps. + Map[] maps = new Map[] { + new HashMap(m_installedBundles[LOCATION_MAP_IDX]), + new TreeMap(m_installedBundles[IDENTIFIER_MAP_IDX]) + }; + maps[LOCATION_MAP_IDX].put(location, bundle); + maps[IDENTIFIER_MAP_IDX].put(new Long(bundle.getBundleId()), bundle); + m_installedBundles = maps; + } + finally + { + releaseGlobalLock(); } - throw new BundleException("Could not create bundle object.", ex); - } - - // If the bundle is new, then set its start level; existing - // bundles already have their start level set. - if (isNew) - { - // This will persistently set the bundle's start level. - bundle.getInfo().setStartLevel(getInitialBundleStartLevel()); - bundle.getInfo().setLastModified(System.currentTimeMillis()); - } - - synchronized (m_installedBundleLock_Priority2) - { - m_installedBundleMap.put(location, bundle); + for (Bundle extension : m_extensionManager.resolveExtensionBundles(this)) + { + m_extensionManager.startExtensionBundle(this, (BundleImpl) extension); + } } } finally @@ -1826,18 +3357,67 @@ private Bundle installBundle(long id, String location, InputStream is) } catch (IOException ex) { - m_logger.log( + m_logger.log(bundle, Logger.LOG_ERROR, "Unable to close input stream.", ex); // Not much else we can do. } } - // Fire bundle event. - fireBundleEvent(BundleEvent.INSTALLED, bundle); + if (existing != null) + { + Set> hooks = + getHookRegistry().getHooks(org.osgi.framework.hooks.bundle.FindHook.class); + if (!hooks.isEmpty()) + { + Collection bundles = new ArrayList(1); + bundles.add(existing); + bundles = new ShrinkableCollection(bundles); + for (ServiceReference hook : hooks) + { + org.osgi.framework.hooks.bundle.FindHook fh = getService(this, hook, false); + if (fh != null) + { + try + { + m_secureAction.invokeBundleFindHook( + fh, ((BundleImpl) origin)._getBundleContext(), bundles); + } + catch (Throwable th) + { + m_logger.doLog( + hook.getBundle(), + hook, + Logger.LOG_WARNING, + "Problem invoking bundle hook.", + th); + } + } + } + + if (origin != this) + { + // If the origin was something else than the system bundle, reject this action if + // the bundle has been removed by the hooks. However, if it is the system bundle, + // the install action should always succeed, regardless of whether the hooks are + // trying to prevent it. + if (bundles.isEmpty()) + { + throw new BundleException( + "Bundle installation rejected by hook.", + BundleException.REJECTED_BY_HOOK); + } + } + } + } + else + { + // Fire bundle event. + fireBundleEvent(BundleEvent.INSTALLED, bundle, origin); + } // Return new bundle. - return bundle; + return (existing != null) ? existing : bundle; } /** @@ -1847,12 +3427,9 @@ private Bundle installBundle(long id, String location, InputStream is) * @return The bundle associated with the location or null if there * is no bundle associated with the location. **/ - protected Bundle getBundle(String location) + Bundle getBundle(String location) { - synchronized (m_installedBundleLock_Priority2) - { - return (Bundle) m_installedBundleMap.get(location); - } + return (Bundle) m_installedBundles[LOCATION_MAP_IDX].get(location); } /** @@ -1863,96 +3440,161 @@ protected Bundle getBundle(String location) * @return The bundle associated with the identifier or null if there * is no bundle associated with the identifier. **/ - protected Bundle getBundle(long id) + Bundle getBundle(BundleContext bc, long id) { - synchronized (m_installedBundleLock_Priority2) + BundleImpl bundle = (BundleImpl) + m_installedBundles[IDENTIFIER_MAP_IDX].get(new Long(id)); + if (bundle != null) { - BundleImpl bundle = null; - - for (Iterator i = m_installedBundleMap.values().iterator(); i.hasNext(); ) + List uninstalledBundles = m_uninstalledBundles; + for (int i = 0; + (bundle == null) + && (uninstalledBundles != null) + && (i < uninstalledBundles.size()); + i++) { - bundle = (BundleImpl) i.next(); - if (bundle.getInfo().getBundleId() == id) + if (uninstalledBundles.get(i).getBundleId() == id) { - return bundle; + bundle = uninstalledBundles.get(i); } } } - synchronized (m_uninstalledBundlesLock_Priority3) + Set> hooks = + getHookRegistry().getHooks(org.osgi.framework.hooks.bundle.FindHook.class); + if (!hooks.isEmpty() && (bundle != null)) { - for (int i = 0; - (m_uninstalledBundles != null) && (i < m_uninstalledBundles.length); - i++) + Collection bundles = new ArrayList(1); + bundles.add(bundle); + bundles = new ShrinkableCollection(bundles); + for (ServiceReference hook : hooks) { - if (m_uninstalledBundles[i].getInfo().getBundleId() == id) + org.osgi.framework.hooks.bundle.FindHook fh = getService(this, hook, false); + if (fh != null) { - return m_uninstalledBundles[i]; + try + { + m_secureAction.invokeBundleFindHook(fh, bc, bundles); + } + catch (Throwable th) + { + m_logger.doLog( + hook.getBundle(), + hook, + Logger.LOG_WARNING, + "Problem invoking bundle hook.", + th); + } } } - } - return null; + if (bc.getBundle() != this) + { + // If the requesting bundle is not the system bundle, return the filtered result + bundle = (bundles.isEmpty()) ? null : bundle; + } + } + return bundle; } - // Private member for method below. - private Comparator m_comparator = null; - /** - * Implementation for BundleContext.getBundles(). Retrieves - * all installed bundles. + * Retrieves a bundle by its identifier and avoids bundles hooks. * - * @return An array containing all installed bundles or null if - * there are no installed bundles. + * @return The bundle associated with the identifier or null. **/ - protected Bundle[] getBundles() + Bundle getBundle(long id) { - if (m_comparator == null) + BundleImpl bundle = (BundleImpl) + m_installedBundles[IDENTIFIER_MAP_IDX].get(new Long(id)); + if (bundle != null) { - m_comparator = new Comparator() { - public int compare(Object o1, Object o2) - { - Bundle b1 = (Bundle) o1; - Bundle b2 = (Bundle) o2; - if (b1.getBundleId() > b2.getBundleId()) - return 1; - else if (b1.getBundleId() < b2.getBundleId()) - return -1; - return 0; - } - }; + return bundle; } - Bundle[] bundles = null; - - synchronized (m_installedBundleLock_Priority2) + List uninstalledBundles = m_uninstalledBundles; + for (int i = 0; + (uninstalledBundles != null) && (i < uninstalledBundles.size()); + i++) { - if (m_installedBundleMap.size() == 0) + if (uninstalledBundles.get(i).getBundleId() == id) { - return null; + return uninstalledBundles.get(i); } + } + + return null; + } - bundles = new Bundle[m_installedBundleMap.size()]; - int counter = 0; - for (Iterator i = m_installedBundleMap.values().iterator(); i.hasNext(); ) + /** + * Implementation for BundleContext.getBundles(). Retrieves + * all installed bundles. + * + * @return An array containing all installed bundles or an empty + * array if there are no installed bundles. + **/ + Bundle[] getBundles(BundleContext bc) + { + Collection bundles = m_installedBundles[IDENTIFIER_MAP_IDX].values(); + if ( !bundles.isEmpty() ) + { + Set> hooks = + getHookRegistry().getHooks(org.osgi.framework.hooks.bundle.FindHook.class); + if (!hooks.isEmpty()) { - bundles[counter++] = (Bundle) i.next(); + Collection shrunkBundles = new ShrinkableCollection(new ArrayList(bundles)); + for (ServiceReference hook : hooks) + { + org.osgi.framework.hooks.bundle.FindHook fh = getService(this, hook, false); + if (fh != null) + { + try + { + m_secureAction.invokeBundleFindHook(fh, bc, shrunkBundles); + } + catch (Throwable th) + { + m_logger.doLog( + hook.getBundle(), + hook, + Logger.LOG_WARNING, + "Problem invoking bundle hook.", + th); + } + } + } + if ( bc.getBundle() != this ) { + // If the requesting bundle is something other than the system bundle, return the shrunk + // collection of bundles. If it *is* the system bundle, it should receive the unfiltered bundles. + bundles = shrunkBundles; + } } } - Arrays.sort(bundles, m_comparator); + return bundles.toArray(new Bundle[bundles.size()]); + } - return bundles; + /** + * Retrieves all installed bundles and avoids bundles hooks. + * + * @return An array containing all installed bundles or null if + * there are no installed bundles. + **/ + Bundle[] getBundles() + { + Collection bundles = m_installedBundles[IDENTIFIER_MAP_IDX].values(); + return bundles.toArray(new Bundle[bundles.size()]); } - protected void addBundleListener(Bundle bundle, BundleListener l) + void addBundleListener(BundleImpl bundle, BundleListener l) { - m_dispatcher.addListener(bundle, BundleListener.class, l, null); + m_dispatcher.addListener( + bundle._getBundleContext(), BundleListener.class, l, null); } - protected void removeBundleListener(Bundle bundle, BundleListener l) + void removeBundleListener(BundleImpl bundle, BundleListener l) { - m_dispatcher.removeListener(bundle, BundleListener.class, l); + m_dispatcher.removeListener( + bundle._getBundleContext(), BundleListener.class, l); } /** @@ -1964,11 +3606,69 @@ protected void removeBundleListener(Bundle bundle, BundleListener l) * @param l The service listener to add to the listener list. * @param f The filter for the listener; may be null. **/ - protected void addServiceListener(Bundle bundle, ServiceListener l, String f) + void addServiceListener(BundleImpl bundle, ServiceListener l, String f) throws InvalidSyntaxException { - m_dispatcher.addListener( - bundle, ServiceListener.class, l, (f == null) ? null : new FilterImpl(m_logger, f)); + Filter oldFilter; + Filter newFilter = (f == null) ? null : FrameworkUtil.createFilter(f); + + oldFilter = m_dispatcher.addListener( + bundle._getBundleContext(), ServiceListener.class, l, newFilter); + + // Invoke ListenerHook.removed() if filter updated. + Set> listenerHooks = + getHookRegistry().getHooks(org.osgi.framework.hooks.service.ListenerHook.class); + if (oldFilter != null) + { + final Collection removed = Collections.singleton( + new ListenerInfo(bundle, bundle._getBundleContext(), + ServiceListener.class, l, oldFilter, null, true)); + for (ServiceReference sr : listenerHooks) + { + org.osgi.framework.hooks.service.ListenerHook lh = getService(this, sr, false); + if (lh != null) + { + try + { + m_secureAction.invokeServiceListenerHookRemoved(lh, removed); + } + catch (Throwable th) + { + m_logger.log(sr, Logger.LOG_WARNING, + "Problem invoking service registry hook", th); + } + finally + { + m_registry.ungetService(this, sr, null); + } + } + } + } + + // Invoke the ListenerHook.added() on all hooks. + final Collection added = Collections.singleton( + new ListenerInfo(bundle, bundle._getBundleContext(), + ServiceListener.class, l, newFilter, null, false)); + for (ServiceReference sr : listenerHooks) + { + org.osgi.framework.hooks.service.ListenerHook lh = getService(this, sr, false); + if (lh != null) + { + try + { + m_secureAction.invokeServiceListenerHookAdded(lh, added); + } + catch (Throwable th) + { + m_logger.log(sr, Logger.LOG_WARNING, + "Problem invoking service registry hook", th); + } + finally + { + m_registry.ungetService(this, sr, null); + } + } + } } /** @@ -1978,19 +3678,51 @@ protected void addServiceListener(Bundle bundle, ServiceListener l, String f) * @param bundle The context bundle of the listener * @param l The service listener to remove from the listener list. **/ - protected void removeServiceListener(Bundle bundle, ServiceListener l) + void removeServiceListener(BundleImpl bundle, ServiceListener l) { - m_dispatcher.removeListener(bundle, ServiceListener.class, l); + org.osgi.framework.hooks.service.ListenerHook.ListenerInfo listener = + m_dispatcher.removeListener( + bundle._getBundleContext(), ServiceListener.class, l); + + if (listener != null) + { + // Invoke the ListenerHook.removed() on all hooks. + Set> listenerHooks = + getHookRegistry().getHooks(org.osgi.framework.hooks.service.ListenerHook.class); + Collection removed = Collections.singleton(listener); + for (ServiceReference sr : listenerHooks) + { + org.osgi.framework.hooks.service.ListenerHook lh = getService(this, sr, false); + if (lh != null) + { + try + { + m_secureAction.invokeServiceListenerHookRemoved(lh, removed); + } + catch (Throwable th) + { + m_logger.log(sr, Logger.LOG_WARNING, + "Problem invoking service registry hook", th); + } + finally + { + m_registry.ungetService(this, sr, null); + } + } + } + } } - protected void addFrameworkListener(Bundle bundle, FrameworkListener l) + void addFrameworkListener(BundleImpl bundle, FrameworkListener l) { - m_dispatcher.addListener(bundle, FrameworkListener.class, l, null); + m_dispatcher.addListener( + bundle._getBundleContext(), FrameworkListener.class, l, null); } - protected void removeFrameworkListener(Bundle bundle, FrameworkListener l) + void removeFrameworkListener(BundleImpl bundle, FrameworkListener l) { - m_dispatcher.removeListener(bundle, FrameworkListener.class, l); + m_dispatcher.removeListener( + bundle._getBundleContext(), FrameworkListener.class, l); } /** @@ -2004,8 +3736,8 @@ protected void removeFrameworkListener(Bundle bundle, FrameworkListener l) * service or null. * @return A ServiceRegistration object or null. **/ - protected ServiceRegistration registerService( - BundleImpl bundle, String[] classNames, Object svcObj, Dictionary dict) + ServiceRegistration registerService( + BundleContextImpl context, String[] classNames, Object svcObj, Dictionary dict) { if (classNames == null) { @@ -2016,182 +3748,266 @@ else if (svcObj == null) throw new IllegalArgumentException("Service object cannot be null."); } - // Acquire bundle lock. - acquireBundleLock(bundle); - ServiceRegistration reg = null; - try + // Check to make sure that the service object is + // an instance of all service classes; ignore if + // service object is a service factory. + if (!(svcObj instanceof ServiceFactory)) { - BundleInfo info = bundle.getInfo(); + for (int i = 0; i < classNames.length; i++) + { + Class clazz = Util.loadClassUsingClass(svcObj.getClass(), classNames[i], m_secureAction); + if (clazz == null) + { + throw new IllegalArgumentException( + "Cannot cast service: " + classNames[i]); + } + else if (!clazz.isAssignableFrom(svcObj.getClass())) + { + throw new IllegalArgumentException( + "Service object is not an instance of \"" + + classNames[i] + "\"."); + } + } + } - // Can only register services if starting or active. - if ((info.getState() & (Bundle.STARTING | Bundle.ACTIVE)) == 0) + reg = m_registry.registerService(context.getBundle(), classNames, svcObj, dict); + + // Check to see if this a listener hook; if so, then we need + // to invoke the callback with all existing service listeners. + if (HookRegistry.isHook( + classNames, org.osgi.framework.hooks.service.ListenerHook.class, svcObj)) + { + org.osgi.framework.hooks.service.ListenerHook lh = + (org.osgi.framework.hooks.service.ListenerHook) + getService(this, reg.getReference(), false); + if (lh != null) { - throw new IllegalStateException( - "Can only register services while bundle is active or activating."); + try + { + m_secureAction.invokeServiceListenerHookAdded( + lh, m_dispatcher.getAllServiceListeners()); + } + catch (Throwable th) + { + m_logger.log(reg.getReference(), Logger.LOG_WARNING, + "Problem invoking service registry hook", th); + } + finally + { + this.ungetService(this, reg.getReference(), null); + } + } + } + + this.fireServiceEvent(new ServiceEvent(ServiceEvent.REGISTERED, reg.getReference()), null); + + return reg; + } + + /** + * Retrieves an array of {@link ServiceReference} objects based on calling bundle, + * service class name, and filter expression. Optionally checks for isAssignable to + * make sure that the service can be cast to the + * @param bundle Calling Bundle + * @param className Service Classname or null for all + * @param expr Filter Criteria or null + * @return Array of ServiceReference objects that meet the criteria + * @throws InvalidSyntaxException + */ + ServiceReference[] getServiceReferences( + final BundleImpl bundle, final String className, + final String expr, final boolean checkAssignable) + throws InvalidSyntaxException + { + // Define filter if expression is not null. + SimpleFilter filter = null; + if (expr != null) + { + try + { + filter = SimpleFilter.parse(expr); + } + catch (Exception ex) + { + throw new InvalidSyntaxException(ex.getMessage(), expr); + } + } + + // Ask the service registry for all matching service references. + final Collection refList = m_registry.getServiceReferences(className, filter); + + // Filter on assignable references + if (checkAssignable) + { + for (Iterator refIter = refList.iterator(); refIter.hasNext();) + { + // Get the current service reference. + ServiceReference ref = (ServiceReference) refIter.next(); + + // Now check for castability. + if (!Util.isServiceAssignable(bundle, ref)) + { + refIter.remove(); + } } + } + + // If the requesting bundle is the system bundle, ignore the effects of the findhooks + Collection resRefList = (bundle == this ? new ArrayList(refList) : refList); - // Check to make sure that the service object is - // an instance of all service classes; ignore if - // service object is a service factory. - if (!(svcObj instanceof ServiceFactory)) + // activate findhooks + Set> findHooks = + getHookRegistry().getHooks(org.osgi.framework.hooks.service.FindHook.class); + for (ServiceReference sr : findHooks) + { + org.osgi.framework.hooks.service.FindHook fh = getService(this, sr, false); + if (fh != null) { - for (int i = 0; i < classNames.length; i++) + try + { + m_secureAction.invokeServiceFindHook( + fh, + bundle._getBundleContext(), + className, + expr, + !checkAssignable, + new ShrinkableCollection(refList)); + } + catch (Throwable th) + { + m_logger.log(sr, Logger.LOG_WARNING, + "Problem invoking service registry hook", th); + } + finally { - Class clazz = Util.loadClassUsingClass(svcObj.getClass(), classNames[i]); - if (clazz == null) - { - throw new IllegalArgumentException( - "Cannot cast service: " + classNames[i]); - } - else if (!clazz.isAssignableFrom(svcObj.getClass())) - { - throw new IllegalArgumentException( - "Service object is not an instance of \"" - + classNames[i] + "\"."); - } + m_registry.ungetService(this, sr, null); } } - - reg = m_registry.registerService(bundle, classNames, svcObj, dict); } - finally + + // We return resRefList which is normally the same as refList and therefore any modifications + // to refList are also visible to resRefList. However in the case of the system bundle being + // the requestor, resRefList is a copy of the original list before the hooks were invoked. + if (resRefList.size() > 0) { - // Always release bundle lock. - releaseBundleLock(bundle); + return (ServiceReference[]) resRefList.toArray(new ServiceReference[resRefList.size()]); } - // TODO: CONCURRENCY - Reconsider firing event here, outside of the - // bundle lock. - - // NOTE: The service registered event is fired from the service - // registry to the framework, where it is then redistributed to - // interested service event listeners. - - return reg; + return null; } - protected ServiceReference[] getServiceReferences( - BundleImpl bundle, String className, String expr) + /** + * Retrieves Array of {@link ServiceReference} objects based on calling bundle, service class name, + * optional filter expression, and optionally filters further on the version. + * If running under a {@link SecurityManager}, checks that the calling bundle has permissions to + * see the service references and removes references that aren't. + * @param bundle Calling Bundle + * @param className Service Classname or null for all + * @param expr Filter Criteria or null + * @param checkAssignable true to check for isAssignable, false to return all versions + * @return Array of ServiceReference objects that meet the criteria + * @throws InvalidSyntaxException + */ + ServiceReference[] getAllowedServiceReferences( + BundleImpl bundle, String className, String expr, boolean checkAssignable) throws InvalidSyntaxException { - // Define filter if expression is not null. - Filter filter = null; - if (expr != null) + ServiceReference[] refs = getServiceReferences(bundle, className, expr, checkAssignable); + + Object sm = System.getSecurityManager(); + + if ((sm == null) || (refs == null)) { - filter = new FilterImpl(m_logger, expr); + return refs; } - // Ask the service registry for all matching service references. - List refList = m_registry.getServiceReferences(className, filter); + List result = new ArrayList(); - // The returned reference list must be filtered for two cases: - // 1) The requesting bundle may not be wired to the same class - // as the providing bundle (i.e, different versions), so filter - // any services for which the requesting bundle might get a - // class cast exception. - // 2) Security is enabled and the requesting bundle does not have - // permission access the service. - for (int refIdx = 0; (refList != null) && (refIdx < refList.size()); refIdx++) + for (int i = 0; i < refs.length; i++) { - // Get the current service reference. - ServiceReference ref = (ServiceReference) refList.get(refIdx); - - // Now check for castability. - if (!Felix.isServiceAssignable(bundle, ref)) + try + { + ((SecurityManager) sm).checkPermission(new ServicePermission(refs[i], ServicePermission.GET)); + result.add(refs[i]); + } + catch (Exception ex) { - refList.remove(refIdx); - refIdx--; + // Ignore, since we are just testing permission. } } - if (refList.size() > 0) + if (result.isEmpty()) { - return (ServiceReference[]) refList.toArray(new ServiceReference[refList.size()]); + return null; } - return null; + return (ServiceReference[]) result.toArray(new ServiceReference[result.size()]); + } - /** - * This method determines if the requesting bundle is able to cast - * the specified service reference based on class visibility rules - * of the underlying modules. - * @param requester The bundle requesting the service. - * @param ref The service in question. - * @return true if the requesting bundle is able to case - * the service object to a known type. - **/ - public static boolean isServiceAssignable(Bundle requester, ServiceReference ref) + S getService(Bundle bundle, ServiceReference ref, boolean isServiceObjetcs) { - // Boolean flag. - boolean allow = true; - // Get the service's objectClass property. - String[] objectClass = (String[]) ref.getProperty(FelixConstants.OBJECTCLASS); - - // The the service reference is not assignable when the requesting - // bundle is wired to a different version of the service object. - // NOTE: We are pessimistic here, if any class in the service's - // objectClass is not usable by the requesting bundle, then we - // disallow the service reference. - for (int classIdx = 0; (allow) && (classIdx < objectClass.length); classIdx++) + try { - if (!ref.isAssignableTo(requester, objectClass[classIdx])) - { - allow = false; - } + return m_registry.getService(bundle, ref, isServiceObjetcs); } - return allow; - } - - protected Object getService(Bundle bundle, ServiceReference ref) - { - // Check that the bundle has permission to get at least - // one of the service interfaces; the objectClass property - // of the service stores its service interfaces. - String[] objectClass = (String[]) - ref.getProperty(Constants.OBJECTCLASS); - if (objectClass == null) + catch (ServiceException ex) { - return null; + fireFrameworkEvent(FrameworkEvent.ERROR, bundle, ex); } - return m_registry.getService(bundle, ref); + return null; } - protected boolean ungetService(Bundle bundle, ServiceReference ref) + boolean ungetService(Bundle bundle, ServiceReference ref, Object srvObj) { - return m_registry.ungetService(bundle, ref); + return m_registry.ungetService(bundle, ref, srvObj); } - protected File getDataFile(BundleImpl bundle, String s) + File getDataFile(BundleImpl bundle, String s) { - // The spec says to throw an error if the bundle - // is stopped, which I assume means not active, - // starting, or stopping. - if ((bundle.getInfo().getState() != Bundle.ACTIVE) && - (bundle.getInfo().getState() != Bundle.STARTING) && - (bundle.getInfo().getState() != Bundle.STOPPING)) + if (bundle.getState() == Bundle.UNINSTALLED) { - throw new IllegalStateException("Only active bundles can create files."); + throw new IllegalStateException("Bundle has been uninstalled"); + } + else if (Util.isFragment(adapt(BundleRevision.class))) + { + return null; } try { - return m_cache.getArchive( - bundle.getInfo().getBundleId()).getDataFile(s); + if (bundle == this) + { + return m_cache.getSystemBundleDataFile(s); + } + + return bundle.getArchive().getDataFile(s); } catch (Exception ex) { - m_logger.log(Logger.LOG_ERROR, ex.getMessage()); + m_logger.log(bundle, Logger.LOG_ERROR, ex.getMessage()); return null; } } + // + // Hook service management methods. + // + + HookRegistry getHookRegistry() + { + return m_registry.getHookRegistry(); + } + // // PackageAdmin related methods. // + private final Map m_systemBundleClassCache = + new WeakHashMap(); + /** * This method returns the bundle associated with the specified class if * the class was loaded from a bundle from this framework instance. If the @@ -2203,21 +4019,55 @@ protected File getDataFile(BundleImpl bundle, String s) * if the class was not loaded by a bundle or its associated * bundle belongs to a different framework instance. **/ - protected Bundle getBundle(Class clazz) + Bundle getBundle(Class clazz) { - if (clazz.getClassLoader() instanceof ContentClassLoader) - { - IContentLoader contentLoader = - ((ContentClassLoader) clazz.getClassLoader()).getContentLoader(); - IModule[] modules = m_factory.getModules(); - for (int i = 0; i < modules.length; i++) + // If the class comes from bundle class loader, then return + // associated bundle if it is from this framework instance. + ClassLoader classLoader = m_secureAction.getClassLoader(clazz); + + if (classLoader instanceof BundleReference) + { + // Only return the bundle if it is from this framework. + BundleReference br = (BundleReference) classLoader; + return ((br.getBundle() instanceof BundleImpl) + && (((BundleImpl) br.getBundle()).getFramework() == this)) + ? br.getBundle() : null; + } + // Otherwise check if the class conceptually comes from the + // system bundle. Ignore implicit boot delegation. + if (!clazz.getName().startsWith("java.")) + { + Boolean fromSystemBundle; + synchronized (m_systemBundleClassCache) + { + fromSystemBundle = m_systemBundleClassCache.get(clazz); + } + if (fromSystemBundle == null) { - if (modules[i].getContentLoader() == contentLoader) + Class sbClass = null; + try + { + sbClass = ((BundleWiringImpl) m_extensionManager + .getRevision().getWiring()).getClassByDelegation(clazz.getName()); + } + catch (ClassNotFoundException ex) + { + // Ignore, treat as false. + } + synchronized (m_systemBundleClassCache) { - long id = Util.getBundleIdFromModuleId(modules[i].getId()); - return getBundle(id); + if (sbClass == clazz) + { + fromSystemBundle = Boolean.TRUE; + } + else + { + fromSystemBundle = Boolean.FALSE; + } + m_systemBundleClassCache.put(clazz, fromSystemBundle); } } + return fromSystemBundle.booleanValue() ? this : null; } return null; } @@ -2227,48 +4077,93 @@ protected Bundle getBundle(Class clazz) * package name. This is used by the PackageAdmin service * implementation. * - * @param name The name of the exported package to find. + * @param pkgName The name of the exported package to find. * @return The exported package or null if no matching package was found. **/ - protected ExportedPackage[] getExportedPackages(String name) + ExportedPackage[] getExportedPackages(String pkgName) { // First, get all exporters of the package. - ExportedPackage[] pkgs = null; - IModule[] exporters = m_policyCore.getInUseExporters(new R4Import(name, null, null), true); - if (exporters != null) + Map attrs = Collections.singletonMap( + BundleRevision.PACKAGE_NAMESPACE, (Object) pkgName); + BundleRequirementImpl req = new BundleRequirementImpl( + null, + BundleRevision.PACKAGE_NAMESPACE, + Collections.EMPTY_MAP, + attrs); + List exports = m_resolver.findProviders(req, false); + + // We only want resolved capabilities. + for (Iterator it = exports.iterator(); it.hasNext(); ) + { + if (it.next().getRevision().getWiring() == null) + { + it.remove(); + } + } + + if (exports != null) { - pkgs = new ExportedPackage[exporters.length]; - for (int pkgIdx = 0; pkgIdx < pkgs.length; pkgIdx++) + List pkgs = new ArrayList(); + + for (Iterator it = exports.iterator(); it.hasNext(); ) { - // Get the bundle associated with the current exporting module. - BundleImpl bundle = (BundleImpl) getBundle( - Util.getBundleIdFromModuleId(exporters[pkgIdx].getId())); + // Get the bundle associated with the current exporting revision. + Bundle bundle = it.next().getRevision().getBundle(); // We need to find the version of the exported package, but this // is tricky since there may be multiple versions of the package // offered by a given bundle, since multiple revisions of the // bundle JAR file may exist if the bundle was updated without // refreshing the framework. In this case, each revision of the - // bundle JAR file is represented as a module in the BundleInfo - // module array, which is ordered from oldest to newest. We assume - // that the first module found to be exporting the package is the - // provider of the package, which makes sense since it must have - // been resolved first. - IModule[] modules = bundle.getInfo().getModules(); - for (int modIdx = 0; modIdx < modules.length; modIdx++) + // bundle JAR file is represented as a revision ordered from + // newest to oldest. We assume that the first revision found to + // be exporting the package is the provider of the package, + // which makes sense since it must have been resolved first. + List originRevisions = + bundle.adapt(BundleRevisions.class).getRevisions(); + for (int i = originRevisions.size() - 1; i >= 0; i--) { - R4Export export = Util.getExportPackage(modules[modIdx], name); - if (export != null) + BundleRevision originBr = originRevisions.get(i); + List revisions = Collections.singletonList(originBr); + + if ((originBr.getTypes() & BundleRevision.TYPE_FRAGMENT) != 0) { - pkgs[pkgIdx] = - new ExportedPackageImpl( - this, bundle, modules[modIdx], export); + // If it's a fragment, find the revisions of the attached + // bundle(s) and work with those instead. Note that fragments + // can be attached to multiple hosts... + revisions = new ArrayList(); + + for (BundleWire bw : originBr.getWiring().getRequiredWires(HostNamespace.HOST_NAMESPACE)) + { + revisions.add(bw.getProviderWiring().getRevision()); + } + } + + for (BundleRevision br : revisions) + { + List caps = (br.getWiring() == null) + ? br.getDeclaredCapabilities(null) + : br.getWiring().getCapabilities(null); + for (BundleCapability cap : caps) + { + if (cap.getNamespace().equals(req.getNamespace()) + && CapabilitySet.matches(cap, req.getFilter())) + { + pkgs.add( + new ExportedPackageImpl( + this, (BundleImpl) br.getBundle(), br, cap)); + } + } } } } + + return (pkgs.isEmpty()) + ? null + : (ExportedPackage[]) pkgs.toArray(new ExportedPackage[pkgs.size()]); } - return pkgs; + return null; } /** @@ -2281,7 +4176,7 @@ protected ExportedPackage[] getExportedPackages(String name) * to be retrieved. * @return An array of exported packages. **/ - protected ExportedPackage[] getExportedPackages(Bundle b) + ExportedPackage[] getExportedPackages(Bundle b) { List list = new ArrayList(); @@ -2296,22 +4191,24 @@ protected ExportedPackage[] getExportedPackages(Bundle b) else { // To create a list of all exported packages, we must look - // in the installed and uninstalled sets of bundles. To - // ensure a somewhat consistent view, we will gather all - // of this information from within the installed bundle - // lock. - synchronized (m_installedBundleLock_Priority2) + // in the installed and uninstalled sets of bundles. + boolean locked = acquireGlobalLock(); + if (!locked) + { + // If the calling thread holds bundle locks, then we might not + // be able to get the global lock. + throw new IllegalStateException( + "Unable to acquire global lock to retrieve exported packages."); + } + try { // First get exported packages from uninstalled bundles. - synchronized (m_uninstalledBundlesLock_Priority3) + for (int bundleIdx = 0; + (m_uninstalledBundles != null) && (bundleIdx < m_uninstalledBundles.size()); + bundleIdx++) { - for (int bundleIdx = 0; - (m_uninstalledBundles != null) && (bundleIdx < m_uninstalledBundles.length); - bundleIdx++) - { - BundleImpl bundle = m_uninstalledBundles[bundleIdx]; - getExportedPackages(bundle, list); - } + BundleImpl bundle = m_uninstalledBundles.get(bundleIdx); + getExportedPackages(bundle, list); } // Now get exported packages from installed bundles. @@ -2322,9 +4219,15 @@ protected ExportedPackage[] getExportedPackages(Bundle b) getExportedPackages(bundle, list); } } + finally + { + releaseGlobalLock(); + } } - return (ExportedPackage[]) list.toArray(new ExportedPackage[list.size()]); + return (list.isEmpty()) + ? null + : (ExportedPackage[]) list.toArray(new ExportedPackage[list.size()]); } /** @@ -2333,416 +4236,555 @@ protected ExportedPackage[] getExportedPackages(Bundle b) * @param bundle The bundle from which to retrieve exported packages. * @param list The list to which the exported packages are added **/ - private void getExportedPackages(BundleImpl bundle, List list) + private void getExportedPackages(Bundle bundle, List list) { - // Since a bundle may have many modules associated with it, - // one for each revision in the cache, search each module - // for each revision to get all exports. - IModule[] modules = bundle.getInfo().getModules(); - for (int modIdx = 0; modIdx < modules.length; modIdx++) - { - R4Export[] exports = modules[modIdx].getDefinition().getExports(); - if ((exports != null) && (exports.length > 0)) + // Since a bundle may have many revisions associated with it, + // one for each revision in the cache, search each revision + // to get all exports. + for (BundleRevision br : bundle.adapt(BundleRevisions.class).getRevisions()) + { + List caps = (br.getWiring() == null) + ? br.getDeclaredCapabilities(null) + : br.getWiring().getCapabilities(null); + if ((caps != null) && (caps.size() > 0)) { - for (int expIdx = 0; expIdx < exports.length; expIdx++) + for (BundleCapability cap : caps) { - // See if the target bundle's module is one of the - // "in use" exporters of the package. - IModule[] inUseModules = - m_policyCore.getInUseExporters( - new R4Import(exports[expIdx].getName(), null, null), true); - // Search through the current providers to find the target - // module. - for (int i = 0; (inUseModules != null) && (i < inUseModules.length); i++) + if (cap.getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE)) { - if (inUseModules[i] == modules[modIdx]) - { - list.add(new ExportedPackageImpl( - this, bundle, modules[modIdx], exports[expIdx])); - } + String pkgName = (String) + cap.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE); + list.add(new ExportedPackageImpl( + this, (BundleImpl) bundle, br, cap)); } } } } } - protected Bundle[] getImportingBundles(ExportedPackage ep) + // Needed by ExportedPackageImpl. + Set getImportingBundles(BundleImpl exporter, BundleCapability cap) { - // Get exporting bundle. - BundleImpl exporter = (BundleImpl) - ((ExportedPackage) ep).getExportingBundle(); - BundleInfo exporterInfo = exporter.getInfo(); + return m_dependencies.getImportingBundles(exporter, cap); + } - // Create list for storing importing bundles. - List list = new ArrayList(); - Bundle[] bundles = getBundles(); + // Needed by RequiredBundleImpl. + Set getRequiringBundles(BundleImpl bundle) + { + return m_dependencies.getRequiringBundles(bundle); + } - // Check all bundles to see who imports the package. - for (int bundleIdx = 0; bundleIdx < bundles.length; bundleIdx++) + boolean resolveBundles(Collection targets) + { + // Acquire global lock. + boolean locked = acquireGlobalLock(); + if (!locked) { - BundleImpl importer = (BundleImpl) bundles[bundleIdx]; - - // Check the import wires of all modules for all bundles. - IModule[] modules = importer.getInfo().getModules(); - for (int modIdx = 0; modIdx < modules.length; modIdx++) - { - IWire wire = Util.getWire(modules[modIdx], ep.getName()); - - // If the resolving module is associated with the - // exporting bundle, then add current bundle to - // import list. - if ((wire != null) && exporterInfo.hasModule(wire.getExporter())) - { - // Add the bundle to the list of importers. - list.add(bundles[bundleIdx]); - break; - } - } + m_logger.log( + Logger.LOG_WARNING, + "Unable to acquire global lock to perform resolve.", + null); + return false; } - // Return the results. - if (list.size() > 0) + try { - return (Bundle[]) list.toArray(new Bundle[list.size()]); - } + // Remember original targets. + Collection originalTargets = targets; - return null; - } - - protected boolean resolveBundles(Bundle[] targets) - { - // Acquire locks for all bundles to be resolved. - BundleImpl[] bundles = acquireBundleResolveLocks(targets); + // Determine set of bundles to be resolved, which is either the + // specified bundles or all bundles if null. + if (targets == null) + { + // Add all bundles to the list. + targets = m_installedBundles[LOCATION_MAP_IDX].values(); + } - try - { + // Now resolve each target bundle. boolean result = true; // If there are targets, then resolve each one. - if (bundles != null) + if (!targets.isEmpty()) { - for (int i = 0; i < bundles.length; i++) + // Get bundle revisions for bundles in INSTALLED state. + Set revisions = + new HashSet(targets.size()); + for (Bundle b : targets) { - try + if (b.getState() != Bundle.UNINSTALLED) { - _resolveBundle(bundles[i]); + revisions.add(b.adapt(BundleRevision.class)); } - catch (BundleException ex) + } + // If we had to filter any of the original targets, then + // the return result will be false regardless. + if ((originalTargets != null) && (originalTargets.size() != revisions.size())) + { + result = false; + } + try + { + m_resolver.resolve(Collections.EMPTY_SET, revisions); + if (result) { - result = false; - m_logger.log( - Logger.LOG_WARNING, - "Unable to resolve bundle " + bundles[i].getBundleId(), - ex); + for (BundleRevision br : revisions) + { + if (br.getWiring() == null) + { + result = false; + break; + } + } } } + catch (ResolutionException ex) + { + result = false; + } + catch (BundleException ex) + { + result = false; + } } return result; } finally { - // Always release all bundle locks. - releaseBundleLocks(bundles); + // Always release the global lock. + releaseGlobalLock(); } } - protected void refreshPackages(Bundle[] targets) + private void resolveBundleRevision(BundleRevision revision) throws BundleException { - // Acquire locks for all impacted bundles. - BundleImpl[] bundles = acquireBundleRefreshLocks(targets); + try + { + m_resolver.resolve(Collections.singleton(revision), Collections.EMPTY_SET); + } + catch (ResolutionException ex) + { + throw new BundleException(ex.getMessage() + + " Unresolved requirements: " + ex.getUnresolvedRequirements(), + BundleException.RESOLVE_ERROR); + } + } - // Remove any targeted bundles from the uninstalled bundles - // array, since they will be removed from the system after - // the refresh. - for (int i = 0; (bundles != null) && (i < bundles.length); i++) + void refreshPackages(Collection targets, FrameworkListener[] listeners) + { + // Acquire global lock. + boolean locked = acquireGlobalLock(); + if (!locked) { - forgetUninstalledBundle(bundles[i]); + // If the thread calling holds bundle locks, then we might not + // be able to get the global lock. However, in practice this + // should not happen since the calls to this method have either + // already acquired the global lock or it is PackageAdmin which + // doesn't hold bundle locks. + throw new IllegalStateException( + "Unable to acquire global lock for refresh."); } - try + // Determine set of bundles to refresh, which is all transitive + // dependencies of specified set or all transitive dependencies + // of all bundles if null is specified. + Collection newTargets = targets; + if (newTargets == null) { - // If there are targets, then refresh each one. - if (bundles != null) + List list = new ArrayList(); + + // First add all uninstalled bundles. + for (int i = 0; + (m_uninstalledBundles != null) && (i < m_uninstalledBundles.size()); + i++) + { + list.add(m_uninstalledBundles.get(i)); + } + + // Then add all updated bundles. + Iterator iter = m_installedBundles[LOCATION_MAP_IDX].values().iterator(); + while (iter.hasNext()) + { + BundleImpl bundle = (BundleImpl) iter.next(); + if (bundle.isRemovalPending()) + { + list.add(bundle); + } + } + + if (!list.isEmpty()) { - // At this point the map contains every bundle that has been - // updated and/or removed as well as all bundles that import - // packages from these bundles. + newTargets = list; + } + } - // Create refresh helpers for each bundle. - RefreshHelper[] helpers = new RefreshHelper[bundles.length]; - for (int i = 0; i < bundles.length; i++) + // If there are targets, then find all dependencies for each one. + Set bundles = null; + if (newTargets != null) + { + // Create map of bundles that import the packages + // from the target bundles. + bundles = new HashSet(); + for (Bundle target : newTargets) + { + // If anyone passes in a null bundle, then just + // ignore it. + if (target != null) { - helpers[i] = new RefreshHelper(bundles[i]); + // Add the current target bundle to the map of + // bundles to be refreshed. + bundles.add(target); + // Add all importing bundles to map. + populateDependentGraph((BundleImpl) target, bundles); } + } + } + + // Now refresh each bundle. + try + { + boolean restart = false; + boolean extensionBundle = false; + + Bundle systemBundle = this; - // Stop, purge or remove, and reinitialize all bundles first. - for (int i = 0; i < helpers.length; i++) + // We need to restart the framework if either an extension bundle is + // refreshed or the system bundle is refreshed and any extension bundle + // has been updated or uninstalled. + if (bundles != null) + { + for (Bundle b : bundles) { - helpers[i].stop(); - helpers[i].purgeOrRemove(); - helpers[i].reinitialize(); + if (systemBundle == b) + { + restart = true; + } + else if (((BundleImpl) b).isExtension()) + { + restart = true; + extensionBundle = true; + break; + } } - // Then restart all bundles that were previously running. - for (int i = 0; i < helpers.length; i++) + // If we need to restart the framework, then no reason to + // do a refresh. + if (!restart) + { + // Now we actually need to refresh the affected bundles. + // At this point the collection contains every bundle that has + // been updated and/or removed as well as all bundles that import + // packages from these bundles. + + // Create refresh helpers for each bundle. + List helpers = new ArrayList(bundles.size()); + for (Bundle b : bundles) + { + // Remove any targeted bundles from the uninstalled bundles + // array, since they will be removed from the system after + // the refresh. + // TODO: FRAMEWORK - Is this correct? + forgetUninstalledBundle((BundleImpl) b); + + // Create refresh helper for bundle. + helpers.add(new RefreshHelper(b)); + } + + // Stop all refreshing bundles. + for (RefreshHelper helper : helpers) + { + if (helper != null) + { + helper.stop(); + } + } + + // Refresh or remove all refreshing bundles first. + for (RefreshHelper helper : helpers) + { + if (helper != null) + { + helper.refreshOrRemove(); + } + } + + // Restart all refreshed bundles that were previously running. + for (RefreshHelper helper : helpers) + { + if (helper != null) + { + helper.restart(); + } + } + } + else { - helpers[i].restart(); + if (!extensionBundle) + { + try + { + update(); + } + catch (BundleException ex) { + m_logger.log(Logger.LOG_ERROR, "Framework restart error.", ex); + } + } + else + { + try + { + stopRefresh(); + } + catch (BundleException ex) { + m_logger.log(Logger.LOG_ERROR, "Framework stop error.", ex); + } + } } } } finally { - // Always release all bundle locks. - releaseBundleLocks(bundles); + // Always release the global lock. + releaseGlobalLock(); } - fireFrameworkEvent(FrameworkEvent.PACKAGES_REFRESHED, getBundle(0), null); - } - - private void populateImportGraph(BundleImpl target, Map map) - { - // Get the exported packages for the specified bundle. - ExportedPackage[] pkgs = getExportedPackages(target); + fireFrameworkEvent(FrameworkEvent.PACKAGES_REFRESHED, this, null); - for (int pkgIdx = 0; (pkgs != null) && (pkgIdx < pkgs.length); pkgIdx++) + if (listeners != null) { - // Get all imports of this package. - Bundle[] importers = getImportingBundles(pkgs[pkgIdx]); - - for (int impIdx = 0; - (importers != null) && (impIdx < importers.length); - impIdx++) + FrameworkEvent event = new FrameworkEvent( + FrameworkEvent.PACKAGES_REFRESHED, this, null); + for (FrameworkListener l : listeners) { - // Avoid cycles if the bundle is already in map. - if (!map.containsKey(importers[impIdx])) + try + { + l.frameworkEvent(event); + } + catch (Throwable th) { - // Add each importing bundle to map. - map.put(importers[impIdx], importers[impIdx]); - // Now recurse into each bundle to get its importers. - populateImportGraph( - (BundleImpl) importers[impIdx], map); + m_logger.log(Logger.LOG_ERROR, + "Framework listener delivery error.", th); } } } } - // - // Miscellaneous private methods. - // - - private BundleInfo createBundleInfo(BundleArchive archive) - throws Exception + Collection getDependencyClosure(Collection targets) { - // Get the bundle manifest. - Map headerMap = null; - try + // Acquire global lock. + boolean locked = acquireGlobalLock(); + if (!locked) { - // Although there should only ever be one revision at this - // point, get the header for the current revision to be safe. - headerMap = archive.getRevision(archive.getRevisionCount() - 1).getManifestHeader(); + // If the thread calling holds bundle locks, then we might not + // be able to get the global lock. However, in practice this + // should not happen since the calls to this method have either + // already acquired the global lock or it is PackageAdmin which + // doesn't hold bundle locks. + throw new IllegalStateException( + "Unable to acquire global lock for refresh."); } - catch (Exception ex) + + try { - throw new BundleException("Unable to read JAR manifest.", ex); + // If there are targets, then find all dependencies for each one. + Set bundles = Collections.EMPTY_SET; + if (targets != null) + { + // Create map of bundles that import the packages + // from the target bundles. + bundles = new HashSet(); + for (Bundle target : targets) + { + // Add the current target bundle to the map of + // bundles to be refreshed. + bundles.add(target); + // Add all importing bundles to map. + populateDependentGraph((BundleImpl) target, bundles); + } + } + return bundles; } - - // We can't do anything without the manifest header. - if (headerMap == null) + finally { - throw new BundleException("Unable to read JAR manifest header."); + // Always release the global lock. + releaseGlobalLock(); } + } - // Create the module for the bundle; although there should only - // ever be one revision at this point, create the module for - // the current revision to be safe. - IModule module = createModule( - archive.getId(), archive.getRevisionCount() - 1, headerMap); + // Calls to this method must have the global lock. + private Set populateDependentGraph(Bundle exporter, Set set) + { + // Get all dependent bundles of this bundle. + Set dependents = m_dependencies.getDependentBundles(exporter); - // Finally, create an return the bundle info. - return new BundleInfo(m_logger, archive, module); + if (dependents != null) + { + for (Bundle b : dependents) + { + // Avoid cycles if the bundle is already in set. + if (!set.contains(b)) + { + // Add each dependent bundle to set. + set.add(b); + // Now recurse into each bundle to get its dependents. + set = populateDependentGraph((BundleImpl) b, set); + } + } + } + return set; } - /** - * Creates a module for a given bundle by reading the bundle's - * manifest meta-data and converting it to work with the underlying - * import/export search policy of the module loader. - * @param targetId The identifier of the bundle for which the module should - * be created. - * @param headerMap The headers map associated with the bundle. - * @return The initialized and/or newly created module. - **/ - private IModule createModule(long targetId, int revision, Map headerMap) - throws Exception + Collection getRemovalPendingBundles() { - ManifestParser mp = new ManifestParser(m_logger, m_config, headerMap); - - // Verify that the bundle symbolic name and version is unique. - if (mp.getVersion().equals("2")) + // Acquire global lock. + boolean locked = acquireGlobalLock(); + if (!locked) { - String bundleVersion = mp.get(FelixConstants.BUNDLE_VERSION); - bundleVersion = (bundleVersion == null) ? "0.0.0" : bundleVersion; - String symName = (String) mp.get(FelixConstants.BUNDLE_SYMBOLICNAME); + // If the thread calling holds bundle locks, then we might not + // be able to get the global lock. However, in practice this + // should not happen since the calls to this method have either + // already acquired the global lock or it is PackageAdmin which + // doesn't hold bundle locks. + throw new IllegalStateException( + "Unable to acquire global lock for refresh."); + } - Bundle[] bundles = getBundles(); - for (int i = 0; (bundles != null) && (i < bundles.length); i++) + try + { + List bundles = new ArrayList(); + if (m_uninstalledBundles != null) + { + for (Bundle b : m_uninstalledBundles) + { + bundles.add(b); + } + } + for (Bundle b : getBundles()) { - long id = ((BundleImpl) bundles[i]).getInfo().getBundleId(); - String sym = (String) ((BundleImpl) bundles[i]) - .getInfo().getCurrentHeader().get(Constants.BUNDLE_SYMBOLICNAME); - String ver = (String) ((BundleImpl) bundles[i]) - .getInfo().getCurrentHeader().get(Constants.BUNDLE_VERSION); - ver = (ver == null) ? "0.0.0" : ver; - if (symName.equals(sym) && bundleVersion.equals(ver) && (targetId != id)) + if (((BundleImpl) b).isRemovalPending()) { - throw new BundleException("Bundle symbolic name and version are not unique."); + bundles.add(b); } } + return bundles; + } + finally + { + // Always release the global lock. + releaseGlobalLock(); } + } - // Now that we have all of the metadata associated with the - // module, we need to create the module itself. This is somewhat - // complicated because a module is constructed out of several - // interrelated pieces (e.g., module definition, content loader, - // search policy, url policy). We need to create all of these - // pieces and bind them together. + // + // Miscellaneous private methods. + // - // Create the module definition for the new module. - IModuleDefinition md = new ModuleDefinition( - mp.getExports(), - mp.getImports(), - mp.getDynamicImports(), - mp.getLibraries(m_cache.getArchive(targetId).getRevision(revision))); + private volatile SecurityProvider m_securityProvider; - // Create the module using the module definition. - IModule module = m_factory.createModule( - Long.toString(targetId) + "." + Integer.toString(revision), md); + SecurityProvider getSecurityProvider() + { + return m_securityProvider; + } - ProtectionDomain pd = null; + void setSecurityProvider(SecurityProvider securityProvider) + { + m_securityProvider = securityProvider; + } - if (System.getSecurityManager() != null) + Object getSignerMatcher(BundleImpl bundle, int signersType) + { + if ((bundle != this) && (m_securityProvider != null)) { - String location = m_cache.getArchive(targetId).getLocation(); + return m_securityProvider.getSignerMatcher(bundle, signersType); + } + return new HashMap(); + } - if (location.startsWith("reference:")) + boolean impliesBundlePermission(BundleProtectionDomain bundleProtectionDomain, Permission permission, boolean direct) + { + if (direct && permission instanceof PackagePermission) + { + if (bundleProtectionDomain.impliesWoven(permission)) { - location = location.substring("reference:".length()); + return true; } - - CodeSource codesource = new CodeSource( - new URL(location), - m_cache.getArchive(targetId).getCertificates()); - - pd = new ProtectionDomain(codesource, - m_secureAction.getPolicy().getPermissions(codesource)); } + if (m_securityProvider != null) + { + return m_securityProvider.hasBundlePermission(bundleProtectionDomain, permission, direct); + } + else + { + Bundle source = bundleProtectionDomain.getBundle(); - m_factory.setSecurityContext(module, pd); - - // Create the content loader from the module archive. - IContentLoader contentLoader = new ContentLoaderImpl( - m_logger, - m_cache.getArchive(targetId).getRevision(revision).getContent(), - m_cache.getArchive(targetId).getRevision(revision).getContentPath(), - pd); - // Set the content loader's search policy. - contentLoader.setSearchPolicy( - new R4SearchPolicy(m_policyCore, module)); - // Set the content loader's URL policy. - contentLoader.setURLPolicy( -// TODO: ML - SUCKS NEEDING URL POLICY PER MODULE. - new URLPolicyImpl( - m_logger, m_bundleStreamHandler, module)); - - // Set the module's content loader to the created content loader. - m_factory.setContentLoader(module, contentLoader); - - // Done, so return the module. - return module; + return (m_securityDefaultPolicy && (source == null || source.getBundleId() != 0)) ? + bundleProtectionDomain.superImplies(permission) : true; + } } - private BundleActivator createBundleActivator(BundleInfo info) + private BundleActivator createBundleActivator(Bundle impl) throws Exception { // CONCURRENCY NOTE: // This method is called indirectly from startBundle() (via _startBundle()), - // which has the exclusion lock, so there is no need to do any locking here. + // which has the bundle lock, so there is no need to do any locking here. + // Get the activator class from the header map. BundleActivator activator = null; - - String strict = m_config.get(FelixConstants.STRICT_OSGI_PROP); - boolean isStrict = (strict == null) ? true : strict.equals("true"); - if (!isStrict) + Map headerMap = ((BundleRevisionImpl) impl.adapt(BundleRevision.class)).getHeaders(); + String className = (String) headerMap.get(Constants.BUNDLE_ACTIVATOR); + // Try to instantiate activator class if present. + if (className != null) { + className = className.trim(); + Class clazz; try { - activator = - m_cache.getArchive(info.getBundleId()) - .getActivator(info.getCurrentModule()); - } - catch (Exception ex) - { - activator = null; + clazz = ((BundleWiringImpl) + impl.adapt(BundleRevision.class).getWiring()).getClassByDelegation(className); } - } - - // If there was no cached activator, then get the activator - // class from the bundle manifest. - if (activator == null) - { - // Get the associated bundle archive. - BundleArchive ba = m_cache.getArchive(info.getBundleId()); - // Get the manifest from the current revision; revision is - // base zero so subtract one from the count to get the - // current revision. - Map headerMap = ba.getRevision(ba.getRevisionCount() - 1).getManifestHeader(); - // Get the activator class attribute. - String className = (String) headerMap.get(Constants.BUNDLE_ACTIVATOR); - // Try to instantiate activator class if present. - if (className != null) + catch (ClassNotFoundException ex) { - className = className.trim(); - Class clazz = info.getCurrentModule().getClass(className); - if (clazz == null) - { - throw new BundleException("Not found: " - + className, new ClassNotFoundException(className)); - } - activator = (BundleActivator) clazz.newInstance(); + throw new BundleException("Not found: " + className, ex); } + activator = (BundleActivator) clazz.newInstance(); } return activator; } - private void purgeBundle(BundleImpl bundle) throws Exception + private void refreshBundle(BundleImpl bundle) throws Exception { // Acquire bundle lock. - acquireBundleLock(bundle); - try { - BundleInfo info = bundle.getInfo(); + acquireBundleLock(bundle, Bundle.INSTALLED | Bundle.RESOLVED); + } + catch (IllegalStateException ex) + { + throw new BundleException( + "Bundle state has changed unexpectedly during refresh."); + } - // In case of a refresh, then we want to physically - // remove the bundle's modules from the module manager. - // This is necessary for two reasons: 1) because - // under Windows we won't be able to delete the bundle - // because files might be left open in the resource - // sources of its modules and 2) we want to make sure - // that no references to old modules exist since they - // will all be stale after the refresh. The only other - // way to do this is to remove the bundle, but that - // would be incorrect, because this is a refresh operation - // and should not trigger bundle REMOVE events. - IModule[] modules = info.getModules(); - for (int i = 0; i < modules.length; i++) + try + { + // See if we need to fire UNRESOLVED event. + boolean fire = (bundle.getState() != Bundle.INSTALLED); + // Remove dependencies. + m_dependencies.removeDependencies(bundle); + // Reset the bundle object. + bundle.refresh(); + // Fire UNRESOLVED event if necessary + // and notify state change.. + if (fire) { - m_factory.removeModule(modules[i]); + setBundleStateAndNotify(bundle, Bundle.INSTALLED); + fireBundleEvent(BundleEvent.UNRESOLVED, bundle); } - - // Purge all bundle revisions, but the current one. - m_cache.getArchive(info.getBundleId()).purge(); + } + catch (Exception ex) + { + fireFrameworkEvent(FrameworkEvent.ERROR, bundle, ex); } finally { @@ -2751,24 +4793,6 @@ private void purgeBundle(BundleImpl bundle) throws Exception } } - private void garbageCollectBundle(BundleImpl bundle) throws Exception - { - // CONCURRENCY NOTE: There is no reason to lock this bundle, - // because this method is only called during shutdown or a - // refresh operation and these are already guarded by locks. - - // Remove the bundle's associated modules from - // the module manager. - IModule[] modules = bundle.getInfo().getModules(); - for (int i = 0; i < modules.length; i++) - { - m_factory.removeModule(modules[i]); - } - - // Remove the bundle from the cache. - m_cache.remove(m_cache.getArchive(bundle.getInfo().getBundleId())); - } - // // Event-related methods. // @@ -2776,7 +4800,7 @@ private void garbageCollectBundle(BundleImpl bundle) throws Exception /** * Fires bundle events. **/ - private void fireFrameworkEvent( + void fireFrameworkEvent( int type, Bundle bundle, Throwable throwable) { m_dispatcher.fireFrameworkEvent(new FrameworkEvent(type, bundle, throwable)); @@ -2788,20 +4812,25 @@ private void fireFrameworkEvent( * @param type The type of bundle event to fire. * @param bundle The bundle associated with the event. **/ - private void fireBundleEvent(int type, Bundle bundle) + void fireBundleEvent(int type, Bundle bundle) + { + m_dispatcher.fireBundleEvent(new BundleEvent(type, bundle), this); + } + + void fireBundleEvent(int type, Bundle bundle, Bundle origin) { - m_dispatcher.fireBundleEvent(new BundleEvent(type, bundle)); + m_dispatcher.fireBundleEvent(new BundleEvent(type, bundle, origin), this); } /** * Fires service events. * - * @param type The type of service event to fire. - * @param ref The service reference associated with the event. + * @param event The service event to fire. + * @param oldProps The old props of the service. **/ - private void fireServiceEvent(ServiceEvent event) + private void fireServiceEvent(ServiceEvent event, Dictionary oldProps) { - m_dispatcher.fireServiceEvent(event); + m_dispatcher.fireServiceEvent(event, oldProps, this); } // @@ -2811,26 +4840,89 @@ private void fireServiceEvent(ServiceEvent event) private void initializeFrameworkProperties() { // Standard OSGi properties. - m_configMutable.put( + m_configMutableMap.put( FelixConstants.FRAMEWORK_VERSION, FelixConstants.FRAMEWORK_VERSION_VALUE); - m_configMutable.put( + m_configMutableMap.put( FelixConstants.FRAMEWORK_VENDOR, FelixConstants.FRAMEWORK_VENDOR_VALUE); - m_configMutable.put( - FelixConstants.FRAMEWORK_LANGUAGE, - System.getProperty("user.language")); - m_configMutable.put( - FelixConstants.FRAMEWORK_OS_VERSION, - System.getProperty("os.version")); + m_configMutableMap.put( + FelixConstants.SUPPORTS_FRAMEWORK_EXTENSION, + ExtensionManager.m_extenderFramework != null ? "true" : "false"); + m_configMutableMap.put( + FelixConstants.SUPPORTS_FRAMEWORK_FRAGMENT, + "true"); + m_configMutableMap.put( + FelixConstants.SUPPORTS_FRAMEWORK_REQUIREBUNDLE, + "true"); + m_configMutableMap.put( + FelixConstants.SUPPORTS_BOOTCLASSPATH_EXTENSION, + ExtensionManager.m_extenderBoot != null ? "true" : "false"); String s = null; - s = R4LibraryClause.normalizeOSName(System.getProperty("os.name")); - m_configMutable.put(FelixConstants.FRAMEWORK_OS_NAME, s); - s = R4LibraryClause.normalizeProcessor(System.getProperty("os.arch")); - m_configMutable.put(FelixConstants.FRAMEWORK_PROCESSOR, s); - m_configMutable.put( + if (!m_configMutableMap.containsKey(FelixConstants.FRAMEWORK_OS_NAME)) + { + s = NativeLibraryClause.normalizeOSName(System.getProperty("os.name")); + m_configMutableMap.put(FelixConstants.FRAMEWORK_OS_NAME, s); + } + + if ( !m_configMutableMap.containsKey(FelixConstants.FRAMEWORK_PROCESSOR)) + { + s = NativeLibraryClause.normalizeProcessor(System.getProperty("os.arch")); + m_configMutableMap.put(FelixConstants.FRAMEWORK_PROCESSOR, s); + } + + if ( !m_configMutableMap.containsKey(FelixConstants.FRAMEWORK_OS_VERSION)) + { + m_configMutableMap.put(FelixConstants.FRAMEWORK_OS_VERSION, + NativeLibraryClause.normalizeOSVersion(System.getProperty("os.version"))); + } + if (!m_configMutableMap.containsKey(FelixConstants.FRAMEWORK_LANGUAGE)) + { + m_configMutableMap.put(FelixConstants.FRAMEWORK_LANGUAGE, + System.getProperty("user.language")); + } + m_configMutableMap.put( FelixConstants.FELIX_VERSION_PROPERTY, getFrameworkVersion()); + + Properties defaultProperties = Util.loadDefaultProperties(m_logger); + + Util.initializeJPMSEE(_getProperty("java.specification.version"), defaultProperties, m_logger); + + // Set supported execution environments to default value, + // if not explicitly configured. + loadFromDefaultIfNotDefined(defaultProperties, Constants.FRAMEWORK_EXECUTIONENVIRONMENT); + + // Set supported native capabilities to default value, + // if not explicitly configured. + loadPrefixFromDefaultIfNotDefined(m_configMutableMap, defaultProperties, FelixConstants.NATIVE_OS_NAME_ALIAS_PREFIX); + loadPrefixFromDefaultIfNotDefined(m_configMutableMap, defaultProperties, FelixConstants.NATIVE_PROC_NAME_ALIAS_PREFIX); + } + + private void loadFromDefaultIfNotDefined(Properties defaultProperties, String propertyName) + { + String s; + if (!getConfig().containsKey(propertyName)) + { + s = Util.getPropertyWithSubs(defaultProperties, propertyName); + if (s != null) + { + m_configMutableMap.put(propertyName, s); + } + } + } + + private void loadPrefixFromDefaultIfNotDefined(Map configMap, Properties defaultProperties, String prefix) + { + Map defaultPropsWithPrefix = Util.getPropertiesWithPrefix(defaultProperties, prefix); + + for(String currentDefaultProperty: defaultPropsWithPrefix.keySet()) + { + if(!configMap.containsKey(currentDefaultProperty)) + { + configMap.put(currentDefaultProperty, defaultPropsWithPrefix.get(currentDefaultProperty)); + } + } } /** @@ -2842,256 +4934,337 @@ private static String getFrameworkVersion() // The framework version property. Properties props = new Properties(); InputStream in = Felix.class.getResourceAsStream("Felix.properties"); - try + if (in != null) { - props.load(in); - } - catch (IOException ex) - { - ex.printStackTrace(); + try + { + props.load(in); + } + catch (IOException ex) + { + ex.printStackTrace(); + } + finally + { + try + { + in.close(); + } + catch (IOException ex) + { + // Not much we can do. + } + } } // Maven uses a '-' to separate the version qualifier, // while OSGi uses a '.', so we need to convert to a '.' - StringBuffer sb = - new StringBuffer( + StringBuilder sb = + new StringBuilder( props.getProperty( - FelixConstants.FELIX_VERSION_PROPERTY, "unknown")); - if (sb.toString().indexOf("-") >= 0) + FelixConstants.FELIX_VERSION_PROPERTY, "0.0.0")); + String toRet = cleanMavenVersion(sb); + if (toRet.indexOf("${pom") >= 0) { - sb.setCharAt(sb.toString().indexOf("-"), '.'); + return "0.0.0"; + } + else + { + return toRet; } - return sb.toString(); } - private void processAutoProperties() + /** + * The main purpose of this method is to turn a.b.c-SNAPSHOT into a.b.c.SNAPSHOT + * it can also deal with a.b-SNAPSHOT and turns it into a.b.0.SNAPSHOT and + * will leave the dash in a.b.c.something-else, as it's valid in that last example. + * In short this method attempts to map a Maven version to an OSGi version as well + * as possible. + * @param sb The version to be cleaned + * @return The cleaned version + */ + private static String cleanMavenVersion(StringBuilder sb) { - // The auto-install property specifies a space-delimited list of - // bundle URLs to be automatically installed into each new profile; - // the start level to which the bundles are assigned is specified by - // appending a ".n" to the auto-install property name, where "n" is - // the desired start level for the list of bundles. - String[] keys = m_config.getKeys(); - for (int i = 0; (keys != null) && (i < keys.length); i++) + int dots = 0; + for (int i = 0; i < sb.length(); i++) { - if (keys[i].startsWith(FelixConstants.AUTO_INSTALL_PROP)) + switch (sb.charAt(i)) { - int startLevel = 1; - try - { - startLevel = Integer.parseInt(keys[i].substring(keys[i].lastIndexOf('.') + 1)); - } - catch (NumberFormatException ex) - { - m_logger.log(Logger.LOG_ERROR, "Invalid property: " + keys[i]); - } - StringTokenizer st = new StringTokenizer(m_config.get(keys[i]), "\" ",true); - if (st.countTokens() > 0) - { - String location = null; - do + case '.': + dots++; + break; + case '-': + if (dots < 3) { - location = nextLocation(st); - if (location != null) + sb.setCharAt(i, '.'); + for (int j = dots; j < 2; j++) { - try - { - BundleImpl b = (BundleImpl) installBundle(location, null); - b.getInfo().setStartLevel(startLevel); - } - catch (Exception ex) - { - m_logger.log( - Logger.LOG_ERROR, "Auto-properties install.", ex); - } + sb.insert(i, ".0"); } } - while (location != null); - } + break; } } + return sb.toString(); + } + + // + // Private utility methods. + // - // The auto-start property specifies a space-delimited list of - // bundle URLs to be automatically installed and started into each - // new profile; the start level to which the bundles are assigned - // is specified by appending a ".n" to the auto-start property name, - // where "n" is the desired start level for the list of bundles. - // The following code starts bundles in two passes, first it installs - // them, then it starts them. - for (int i = 0; (keys != null) && (i < keys.length); i++) + /** + * Generated the next valid bundle identifier. + **/ + private long loadNextId() + { + synchronized (m_nextIdLock) { - if (keys[i].startsWith(FelixConstants.AUTO_START_PROP)) + // Read persisted next bundle identifier. + InputStream is = null; + BufferedReader br = null; + try + { + File file = m_cache.getSystemBundleDataFile("bundle.id"); + is = m_secureAction.getFileInputStream(file); + br = new BufferedReader(new InputStreamReader(is)); + return Long.parseLong(br.readLine()); + } + catch (FileNotFoundException ex) + { + // Ignore this case because we assume that this is the + // initial startup of the framework and therefore the + // file does not exist yet. + } + catch (Exception ex) + { + m_logger.log( + Logger.LOG_WARNING, + "Unable to initialize next bundle identifier from persistent storage.", + ex); + } + finally { - int startLevel = 1; try { - startLevel = Integer.parseInt(keys[i].substring(keys[i].lastIndexOf('.') + 1)); - } - catch (NumberFormatException ex) - { - m_logger.log(Logger.LOG_ERROR, "Invalid property: " + keys[i]); + if (br != null) br.close(); + if (is != null) is.close(); } - StringTokenizer st = new StringTokenizer(m_config.get(keys[i]), "\" ",true); - if (st.countTokens() > 0) + catch (Exception ex) { - String location = null; - do - { - location = nextLocation(st); - if (location != null) - { - try - { - BundleImpl b = (BundleImpl) installBundle(location, null); - b.getInfo().setStartLevel(startLevel); - } - catch (Exception ex) - { - m_logger.log(Logger.LOG_ERROR, "Auto-properties install.", ex); - } - } - } - while (location != null); + m_logger.log( + Logger.LOG_WARNING, + "Unable to close next bundle identifier file.", + ex); } } } - // Now loop through and start the installed bundles. - for (int i = 0; (keys != null) && (i < keys.length); i++) - { - if (keys[i].startsWith(FelixConstants.AUTO_START_PROP)) - { - StringTokenizer st = new StringTokenizer(m_config.get(keys[i]), "\" ",true); - if (st.countTokens() > 0) - { - String location = null; - do - { - location = nextLocation(st); - if (location != null) - { - // Installing twice just returns the same bundle. - try - { - BundleImpl bundle = (BundleImpl) installBundle(location, null); - if (bundle != null) - { - startBundle(bundle, true); - } - } - catch (Exception ex) - { - m_logger.log( - Logger.LOG_ERROR, "Auto-properties start.", ex); - } - } - } - while (location != null); - } - } - } + return -1; } - private String nextLocation(StringTokenizer st) + private long getNextId() { - String retVal = null; - - if (st.countTokens() > 0) + synchronized (m_nextIdLock) { - String tokenList = "\" "; - StringBuffer tokBuf = new StringBuffer(10); - String tok = null; - boolean inQuote = false; - boolean tokStarted = false; - boolean exit = false; - while ((st.hasMoreTokens()) && (!exit)) - { - tok = st.nextToken(tokenList); - if (tok.equals("\"")) - { - inQuote = ! inQuote; - if (inQuote) - { - tokenList = "\""; - } - else - { - tokenList = "\" "; - } + // Save the current id. + long id = m_nextId; - } - else if (tok.equals(" ")) + // Increment the next id. + m_nextId++; + + // Write the bundle state. + OutputStream os = null; + BufferedWriter bw = null; + try + { + File file = m_cache.getSystemBundleDataFile("bundle.id"); + os = m_secureAction.getFileOutputStream(file); + bw = new BufferedWriter(new OutputStreamWriter(os)); + String s = Long.toString(m_nextId); + bw.write(s, 0, s.length()); + } + catch (Exception ex) + { + m_logger.log( + Logger.LOG_WARNING, + "Unable to save next bundle identifier to persistent storage.", + ex); + } + finally + { + try { - if (tokStarted) - { - retVal = tokBuf.toString(); - tokStarted=false; - tokBuf = new StringBuffer(10); - exit = true; - } + if (bw != null) bw.close(); + if (os != null) os.close(); } - else + catch (Exception ex) { - tokStarted = true; - tokBuf.append(tok.trim()); + m_logger.log( + Logger.LOG_WARNING, + "Unable to close next bundle identifier file.", + ex); } } - // Handle case where end of token stream and - // still got data - if ((!exit) && (tokStarted)) - { - retVal = tokBuf.toString(); - } + return id; } - - return retVal; } // - // Private utility methods. + // Miscellaneous inner classes. // - /** - * Generated the next valid bundle identifier. - **/ - private long getNextId() + class SystemBundleActivator implements BundleActivator { - synchronized (m_nextIdLock) + @Override + public void start(BundleContext context) throws Exception { - return m_nextId++; + // Add the bundle activator for the url handler service. + m_activatorList.add(0, new URLHandlersActivator(m_configMap, Felix.this)); + + // Start all activators. + for (Iterator iter = m_activatorList.iterator(); iter.hasNext(); ) + { + try + { + Felix.m_secureAction.startActivator( + iter.next(), context); + } + catch (Throwable throwable) + { + iter.remove(); + fireFrameworkEvent(FrameworkEvent.ERROR, context.getBundle(), + new BundleException("Unable to start Bundle", throwable)); + m_logger.log( + Logger.LOG_WARNING, + "Exception starting a system bundle activator.", + throwable); + } + } } - } - // - // Configuration methods and inner classes. - // + @Override + public void stop(BundleContext context) + { + // The state of the framework should be STOPPING, so + // acquire the bundle lock to verify it. + acquireBundleLock(Felix.this, Bundle.STOPPING); + releaseBundleLock(Felix.this); + + // Use the start level service to set the start level to zero + // in order to stop all bundles in the framework. Since framework + // shutdown happens on its own thread, we can wait for the start + // level service to finish before proceeding by calling the + // non-spec setStartLevelAndWait() method. + m_fwkStartLevel.setStartLevelAndWait(0); + + // Stop framework wiring thread. + m_fwkWiring.stop(); + // Stop framework start level thread. + m_fwkStartLevel.stop(); + + // Shutdown event dispatching queue. + m_dispatcher.stopDispatching(); + + // Since there may be updated and uninstalled bundles that + // have not been refreshed, we will take care of refreshing + // them during shutdown. + + // Refresh all updated bundles. + Bundle[] bundles = getBundles(); + for (int i = 0; i < bundles.length; i++) + { + BundleImpl bundle = (BundleImpl) bundles[i]; + if (bundle.isRemovalPending()) + { + try + { + refreshBundle(bundle); + } + catch (Exception ex) + { + fireFrameworkEvent(FrameworkEvent.ERROR, bundle, ex); + m_logger.log(bundle, Logger.LOG_ERROR, "Unable to purge bundle " + + bundle._getLocation(), ex); + } + } + } + + // Delete uninstalled bundles. + for (int i = 0; + (m_uninstalledBundles != null) && (i < m_uninstalledBundles.size()); + i++) + { + try + { + m_uninstalledBundles.get(i).closeAndDelete(); + } + catch (Exception ex) + { + m_logger.log(m_uninstalledBundles.get(i), + Logger.LOG_ERROR, + "Unable to remove " + + m_uninstalledBundles.get(i)._getLocation(), ex); + } + } - public PropertyResolver getConfig() - { - return m_config; - } + // Dispose of the bundles to close their associated contents. + bundles = getBundles(); + for (int i = 0; i < bundles.length; i++) + { + ((BundleImpl) bundles[i]).close(); + } - private class ConfigImpl implements PropertyResolver - { - public String get(String key) - { - return (m_configMutable == null) ? null : m_configMutable.get(key); - } + m_extensionManager.stopExtensionBundles(Felix.this); + // Stop all system bundle activators. + for (int i = 0; i < m_activatorList.size(); i++) + { + try + { + Felix.m_secureAction.stopActivator((BundleActivator) + m_activatorList.get(i), _getBundleContext()); + } + catch (Throwable throwable) + { + fireFrameworkEvent(FrameworkEvent.ERROR, context.getBundle(), + new BundleException("Unable to stop Bundle", throwable)); + m_logger.log( + Logger.LOG_WARNING, + "Exception stopping a system bundle activator.", + throwable); + } + } + if (m_securityManager != null) + { + System.setSecurityManager(null); + m_securityManager = null; + } - public String[] getKeys() - { - return m_configMutable.getKeys(); - } - } + m_dependencies.removeDependents(adapt(BundleRevision.class)); - // - // Logging methods and inner classes. - // + // Dispose of the bundle cache. + m_cache.release(); + m_cache = null; - public Logger getLogger() - { - return m_logger; + // Set the framework state to resolved. + acquireBundleLock(Felix.this, Bundle.STOPPING); + try + { + // Clean up the bundle context. + ((BundleContextImpl) _getBundleContext()).invalidate(); + setBundleContext(null); + + // Set the framework state to resolved and open + // the shutdown gate. + setBundleStateAndNotify(Felix.this, Bundle.RESOLVED); + m_shutdownGate.open(); + m_shutdownGate = null; + } + finally + { + releaseBundleLock(Felix.this); + } + } } /** @@ -3102,6 +5275,7 @@ public Logger getLogger() private class RefreshHelper { private BundleImpl m_bundle = null; + private int m_oldState = Bundle.INSTALLED; public RefreshHelper(Bundle bundle) { @@ -3110,83 +5284,114 @@ public RefreshHelper(Bundle bundle) public void stop() { - if (m_bundle.getInfo().getState() == Bundle.ACTIVE) + acquireBundleLock(m_bundle, + Bundle.INSTALLED | Bundle.RESOLVED | Bundle.STARTING | + Bundle.ACTIVE | Bundle.STOPPING | Bundle.UNINSTALLED); + try { - try - { - stopBundle(m_bundle, false); - } - catch (BundleException ex) + m_oldState = m_bundle.getState(); + if (m_oldState != Bundle.UNINSTALLED) { - fireFrameworkEvent(FrameworkEvent.ERROR, m_bundle, ex); + if (!Util.isFragment(m_bundle.adapt(BundleRevision.class))) + { + stopBundle(m_bundle, false); + } } } + catch (Throwable ex) + { + fireFrameworkEvent(FrameworkEvent.ERROR, m_bundle, ex); + } + finally + { + releaseBundleLock(m_bundle); + } } - public void purgeOrRemove() + public void refreshOrRemove() { try { - BundleInfo info = m_bundle.getInfo(); - - // Mark the bundle as stale. - info.setStale(); - - // Remove or purge the bundle depending on its + // Delete or refresh the bundle depending on its // current state. - if (info.getState() == Bundle.UNINSTALLED) + if (m_bundle.getState() == Bundle.UNINSTALLED) { - // This physically removes the bundle from memory - // as well as the bundle cache. - garbageCollectBundle(m_bundle); + // Remove dependencies. + m_dependencies.removeDependencies(m_bundle); + m_bundle.closeAndDelete(); m_bundle = null; } else { - // This physically removes all old revisions of the - // bundle from memory and only maintains the newest - // version in the bundle cache. - purgeBundle(m_bundle); + // This removes all old bundle revisions from memory and + // from disk. It only maintains the newest revision in the + // bundle cache. + refreshBundle(m_bundle); } } - catch (Exception ex) + catch (Throwable ex) { fireFrameworkEvent(FrameworkEvent.ERROR, m_bundle, ex); } } - public void reinitialize() + public void restart() { - if (m_bundle != null) + if ((m_bundle != null) && (m_oldState == Bundle.ACTIVE)) { try { - BundleInfo info = m_bundle.getInfo(); - BundleInfo newInfo = createBundleInfo(info.getArchive()); - newInfo.syncLock(info); - m_bundle.setInfo(newInfo); - fireBundleEvent(BundleEvent.UNRESOLVED, m_bundle); +// TODO: LAZY - Not sure if this is the best way... + int options = Bundle.START_TRANSIENT; + options = (m_bundle.getPersistentState() == Bundle.STARTING) + ? options | Bundle.START_ACTIVATION_POLICY + : options; + startBundle(m_bundle, options); } - catch (Exception ex) + catch (Throwable ex) { fireFrameworkEvent(FrameworkEvent.ERROR, m_bundle, ex); } } } + } - public void restart() + // Compares bundles by start level. Within a start level, + // bundles are sorted by bundle ID. + private static class StartLevelTuple implements Comparable + { + private final BundleImpl m_bundle; + private int m_level; + + StartLevelTuple(BundleImpl bundle, int level) { - if (m_bundle != null) + m_bundle = bundle; + m_level = level; + } + + @Override + public int compareTo(StartLevelTuple t) + { + int result = 1; + + if (m_level < t.m_level) { - try - { - startBundle(m_bundle, false); - } - catch (BundleException ex) - { - fireFrameworkEvent(FrameworkEvent.ERROR, m_bundle, ex); - } + result = -1; + } + else if (m_level > t.m_level) + { + result = 1; + } + else if (m_bundle.getBundleId() < t.m_bundle.getBundleId()) + { + result = -1; + } + else if (m_bundle.getBundleId() == t.m_bundle.getBundleId()) + { + result = 0; } + + return result; } } @@ -3196,80 +5401,69 @@ public void restart() private void rememberUninstalledBundle(BundleImpl bundle) { - synchronized (m_uninstalledBundlesLock_Priority3) + boolean locked = acquireGlobalLock(); + if (!locked) + { + // If the calling thread holds bundle locks, then we might not + // be able to get the global lock. + throw new IllegalStateException( + "Unable to acquire global lock to record uninstalled bundle."); + } + try { // Verify that the bundle is not already in the array. for (int i = 0; - (m_uninstalledBundles != null) && (i < m_uninstalledBundles.length); + (m_uninstalledBundles != null) && (i < m_uninstalledBundles.size()); i++) { - if (m_uninstalledBundles[i] == bundle) + if (m_uninstalledBundles.get(i) == bundle) { return; } } - if (m_uninstalledBundles != null) - { - BundleImpl[] newBundles = - new BundleImpl[m_uninstalledBundles.length + 1]; - System.arraycopy(m_uninstalledBundles, 0, - newBundles, 0, m_uninstalledBundles.length); - newBundles[m_uninstalledBundles.length] = bundle; - m_uninstalledBundles = newBundles; - } - else - { - m_uninstalledBundles = new BundleImpl[] { bundle }; - } + // Use a copy-on-write approach to add the bundle + // to the uninstalled list. + List uninstalledBundles = new ArrayList(m_uninstalledBundles); + uninstalledBundles.add(bundle); + m_uninstalledBundles = uninstalledBundles; + } + finally + { + releaseGlobalLock(); } } private void forgetUninstalledBundle(BundleImpl bundle) { - synchronized (m_uninstalledBundlesLock_Priority3) + boolean locked = acquireGlobalLock(); + if (!locked) + { + // If the calling thread holds bundle locks, then we might not + // be able to get the global lock. + throw new IllegalStateException( + "Unable to acquire global lock to release uninstalled bundle."); + } + try { if (m_uninstalledBundles == null) { return; } - int idx = -1; - for (int i = 0; i < m_uninstalledBundles.length; i++) - { - if (m_uninstalledBundles[i] == bundle) - { - idx = i; - break; - } - } - - if (idx >= 0) - { - // If this is the only bundle, then point to empty list. - if ((m_uninstalledBundles.length - 1) == 0) - { - m_uninstalledBundles = new BundleImpl[0]; - } - // Otherwise, we need to do some array copying. - else - { - BundleImpl[] newBundles = - new BundleImpl[m_uninstalledBundles.length - 1]; - System.arraycopy(m_uninstalledBundles, 0, newBundles, 0, idx); - if (idx < newBundles.length) - { - System.arraycopy( - m_uninstalledBundles, idx + 1, - newBundles, idx, newBundles.length - idx); - } - m_uninstalledBundles = newBundles; - } - } + // Use a copy-on-write approach to remove the bundle + // from the uninstalled list. + List uninstalledBundles = new ArrayList(m_uninstalledBundles); + uninstalledBundles.remove(bundle); + m_uninstalledBundles = uninstalledBundles; + } + finally + { + releaseGlobalLock(); } } - protected void acquireInstallLock(String location) + void acquireInstallLock(String location) throws BundleException { synchronized (m_installRequestLock_Priority1) @@ -3290,7 +5484,7 @@ protected void acquireInstallLock(String location) } } - protected void releaseInstallLock(String location) + void releaseInstallLock(String location) { synchronized (m_installRequestLock_Priority1) { @@ -3299,256 +5493,229 @@ protected void releaseInstallLock(String location) } } - protected void acquireBundleLock(BundleImpl bundle) + void setBundleStateAndNotify(BundleImpl bundle, int state) { - synchronized (m_bundleLock) + m_bundleLock.lock(); + try + { + bundle.__setState(state); + m_bundleLockCondition.signalAll(); + } + finally + { + m_bundleLock.unlock(); + } + } + + /** + * This method acquires the lock for the specified bundle as long as the + * bundle is in one of the specified states. If it is not, an exception + * is thrown. Bundle state changes will be monitored to avoid deadlocks. + * @param bundle The bundle to lock. + * @param desiredStates Logically OR'ed desired bundle states. + * @throws java.lang.IllegalStateException If the bundle is not in one of the + * specified desired states. + **/ + void acquireBundleLock(BundleImpl bundle, int desiredStates) + throws IllegalStateException + { + m_bundleLock.lock(); + try { - while (!bundle.getInfo().isLockable()) + // Wait if the desired bundle is already locked by someone else + // or if any thread has the global lock, unless the current thread + // holds the global lock or the bundle lock already. + while (!bundle.isLockable() || + ((m_globalLockThread != null) + && (m_globalLockThread != Thread.currentThread()) + && (bundle.getLockingThread() != Thread.currentThread()))) { + // Check to make sure the bundle is in a desired state. + // If so, keep waiting. If not, throw an exception. + if ((desiredStates & bundle.getState()) == 0) + { + throw new IllegalStateException("Bundle in unexpected state."); + } + // If the calling thread already owns the global lock, then make + // sure no other thread is trying to promote a bundle lock to a + // global lock. If so, interrupt the other thread to avoid deadlock. + else if (m_globalLockThread == Thread.currentThread() + && (bundle.getLockingThread() != null) + && m_globalLockWaitersList.contains(bundle.getLockingThread())) + { + bundle.getLockingThread().interrupt(); + } + try { - m_bundleLock.wait(); + m_bundleLockCondition.await(); } catch (InterruptedException ex) { - // Ignore and just keep waiting. + throw new IllegalStateException("Unable to acquire bundle lock, thread interrupted."); } } - bundle.getInfo().lock(); - } - } - protected boolean acquireBundleLockOrFail(BundleImpl bundle) - { - synchronized (m_bundleLock) - { - if (!bundle.getInfo().isLockable()) + // Now that we can acquire the bundle lock, let's check to make sure + // it is in a desired state; if not, throw an exception and do not + // lock it. + if ((desiredStates & bundle.getState()) == 0) { - return false; + throw new IllegalStateException("Bundle in unexpected state."); } - bundle.getInfo().lock(); - return true; - } - } - protected void releaseBundleLock(BundleImpl bundle) - { - synchronized (m_bundleLock) + // Acquire the bundle lock. + bundle.lock(); + } + finally { - bundle.getInfo().unlock(); - m_bundleLock.notifyAll(); + m_bundleLock.unlock(); } } - protected BundleImpl[] acquireBundleResolveLocks(Bundle[] targets) + /** + * Releases the bundle's lock. + * @param bundle The bundle whose lock is to be released. + * @throws java.lang.IllegalStateException If the calling thread does not + * own the bundle lock. + **/ + void releaseBundleLock(BundleImpl bundle) { - // Hold bundles to be locked. - BundleImpl[] bundles = null; - // Convert existing target bundle array to bundle impl array. - if (targets != null) + m_bundleLock.lock(); + try { - bundles = new BundleImpl[targets.length]; - for (int i = 0; i < targets.length; i++) + // Unlock the bundle. + bundle.unlock(); + // If the thread no longer holds the bundle lock, + // then remove it from the held lock map. + if (bundle.getLockingThread() == null) { - bundles[i] = (BundleImpl) targets[i]; + m_bundleLockCondition.signalAll(); } } + finally + { + m_bundleLock.unlock(); + } + } - synchronized (m_bundleLock) + /** + * Attempts to acquire the global lock. Will also promote a bundle lock + * to the global lock, if the calling thread already holds a bundle lock. + * Since it is possible to deadlock when trying to acquire the global lock + * while holding a bundle lock, this method may fail if a potential deadlock + * is detected. If the calling thread does not hold a bundle lock, then it + * will wait indefinitely to acquire the global. + * @return true if the global lock was successfully acquired, + * false otherwise. + **/ + boolean acquireGlobalLock() + { + m_bundleLock.lock(); + try { - boolean success = false; - while (!success) + // Wait as long as some other thread holds the global lock + // and the current thread is not interrupted. + boolean interrupted = false; + while (!interrupted + && (m_globalLockThread != null) + && (m_globalLockThread != Thread.currentThread())) { - // If targets is null, then resolve all unresolved bundles. - if (targets == null) - { - List list = new ArrayList(); - - // Add all unresolved bundles to the list. - synchronized (m_installedBundleLock_Priority2) - { - Iterator iter = m_installedBundleMap.values().iterator(); - while (iter.hasNext()) - { - BundleImpl bundle = (BundleImpl) iter.next(); - if (bundle.getInfo().getState() == Bundle.INSTALLED) - { - list.add(bundle); - } - } - } - - // Create an array. - if (list.size() > 0) - { - bundles = (BundleImpl[]) list.toArray(new BundleImpl[list.size()]); - } - } - - // Check if all unresolved bundles can be locked. - boolean lockable = true; - if (bundles != null) + // Add calling thread to global lock waiters list. + m_globalLockWaitersList.add(Thread.currentThread()); + // We need to wake up all waiting threads so we can + // recheck for potential deadlock in acquireBundleLock() + // if this thread was holding a bundle lock and is now + // trying to promote it to a global lock. + m_bundleLockCondition.signalAll(); + // Now wait for the global lock. + try { - for (int i = 0; lockable && (i < bundles.length); i++) - { - lockable = bundles[i].getInfo().isLockable(); - } - - // If we can lock all bundles, then lock them. - if (lockable) - { - for (int i = 0; i < bundles.length; i++) - { - bundles[i].getInfo().lock(); - } - success = true; - } - // Otherwise, wait and try again. - else - { - try - { - m_bundleLock.wait(); - } - catch (InterruptedException ex) - { - // Ignore and just keep waiting. - } - } + m_bundleLockCondition.await(); } - else + catch (InterruptedException ex) { - // If there were no bundles to lock, then we can just - // exit the lock loop. - success = true; + interrupted = true; } + // At this point we are either interrupted or will get the + // global lock, so remove the thread from the waiters list. + m_globalLockWaitersList.remove(Thread.currentThread()); + } + + // Check to see if we were interrupted, which means someone + // with the global lock wants our bundle lock, so we should + // fail gracefully. + if (!interrupted) + { + // Increment the current thread's global lock count. + m_globalLockCount++; + m_globalLockThread = Thread.currentThread(); } - } - return bundles; + // Note: If the thread was interrupted, there is no reason to notify + // anyone, since the thread was likely interrupted to force it to give + // up a bundle lock it is holding. When it does give up the bundle + // lock, it will do a notifyAll() in there. + + return !interrupted; + } + finally + { + m_bundleLock.unlock(); + } } - protected BundleImpl[] acquireBundleRefreshLocks(Bundle[] targets) + /** + * Releases the global lock. + * @throws java.lang.IllegalStateException If the calling thread does not + * own the global lock. + **/ + void releaseGlobalLock() { - // Hold bundles to be locked. - BundleImpl[] bundles = null; - - synchronized (m_bundleLock) + m_bundleLock.lock(); + try { - boolean success = false; - while (!success) + // Decrement the current thread's global lock count; + if (m_globalLockThread == Thread.currentThread()) { - // If targets is null, then refresh all pending bundles. - Bundle[] newTargets = targets; - if (newTargets == null) + m_globalLockCount--; + if (m_globalLockCount == 0) { - List list = new ArrayList(); - - // First add all uninstalled bundles. - synchronized (m_uninstalledBundlesLock_Priority3) - { - for (int i = 0; - (m_uninstalledBundles != null) && (i < m_uninstalledBundles.length); - i++) - { - list.add(m_uninstalledBundles[i]); - } - } - - // Then add all updated bundles. - synchronized (m_installedBundleLock_Priority2) - { - Iterator iter = m_installedBundleMap.values().iterator(); - while (iter.hasNext()) - { - BundleImpl bundle = (BundleImpl) iter.next(); - if (bundle.getInfo().getArchive().getRevisionCount() > 1) - { - list.add(bundle); - } - } - } - - // Create an array. - if (list.size() > 0) - { - newTargets = (Bundle[]) list.toArray(new Bundle[list.size()]); - } + m_globalLockThread = null; + m_bundleLockCondition.signalAll(); } + } + else + { + throw new IllegalStateException( + "The current thread doesn't own the global lock."); + } + } + finally + { + m_bundleLock.unlock(); + } + } - // If there are targets, then find all dependencies - // for each one. - if (newTargets != null) - { - // Create map of bundles that import the packages - // from the target bundles. - Map map = new HashMap(); - for (int targetIdx = 0; targetIdx < newTargets.length; targetIdx++) - { - // Add the current target bundle to the map of - // bundles to be refreshed. - BundleImpl target = (BundleImpl) newTargets[targetIdx]; - map.put(target, target); - // Add all importing bundles to map. - populateImportGraph(target, map); - } - - bundles = (BundleImpl[]) map.values().toArray(new BundleImpl[map.size()]); - } + private volatile URLHandlersActivator m_urlHandlersActivator; - // Check if all corresponding bundles can be locked. - boolean lockable = true; - if (bundles != null) - { - for (int i = 0; lockable && (i < bundles.length); i++) - { - lockable = bundles[i].getInfo().isLockable(); - } + void setURLHandlersActivator(URLHandlersActivator urlHandlersActivator) + { + m_urlHandlersActivator = urlHandlersActivator; + } - // If we can lock all bundles, then lock them. - if (lockable) - { - for (int i = 0; i < bundles.length; i++) - { - bundles[i].getInfo().lock(); - } - success = true; - } - // Otherwise, wait and try again. - else - { - try - { - m_bundleLock.wait(); - } - catch (InterruptedException ex) - { - // Ignore and just keep waiting. - } - } - } - else - { - // If there were no bundles to lock, then we can just - // exit the lock loop. - success = true; - } - } - } + Object getStreamHandlerService(String protocol) + { + return m_urlHandlersActivator.getStreamHandlerService(protocol); + } - return bundles; + Object getContentHandlerService(String mimeType) + { + return m_urlHandlersActivator.getContentHandlerService(mimeType); } - protected void releaseBundleLocks(BundleImpl[] bundles) + Collection findProviders(final Requirement requirement) { - // Always unlock any locked bundles. - synchronized (m_bundleLock) - { - for (int i = 0; (bundles != null) && (i < bundles.length); i++) - { - bundles[i].getInfo().unlock(); - } - m_bundleLock.notifyAll(); - } + return m_resolver.findProvidersInternal(null, requirement, true, false); } } diff --git a/framework/src/main/java/org/apache/felix/framework/FilterImpl.java b/framework/src/main/java/org/apache/felix/framework/FilterImpl.java index e9f478d0475..666533f719b 100644 --- a/framework/src/main/java/org/apache/felix/framework/FilterImpl.java +++ b/framework/src/main/java/org/apache/felix/framework/FilterImpl.java @@ -1,270 +1,258 @@ /* - * Copyright 2005 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework; -import java.io.CharArrayReader; -import java.io.IOException; -import java.util.*; - +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import org.apache.felix.framework.ServiceRegistrationImpl.ServiceReferenceImpl; +import org.apache.felix.framework.capabilityset.CapabilitySet; +import org.apache.felix.framework.capabilityset.SimpleFilter; import org.apache.felix.framework.util.StringMap; -import org.apache.felix.framework.util.ldap.*; -import org.osgi.framework.*; - -/** - * This class implements an RFC 1960-based filter. The syntax of the - * filter string is the string representation of LDAP search filters - * as defined in RFC 1960. These filters are used to search for services - * and to track services using ServiceTracker objects. -**/ +import org.apache.felix.framework.wiring.BundleCapabilityImpl; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.wiring.BundleRevision; + public class FilterImpl implements Filter { - private Logger m_logger = null; - private String m_toString = null; - private Evaluator m_evaluator = null; - private SimpleMapper m_mapper = null; - -// TODO: FilterImpl needs a logger, this is a hack for FrameworkUtil. - public FilterImpl(String expr) throws InvalidSyntaxException - { - this(null, expr); - } + private final SimpleFilter m_filter; - /** - * Construct a filter for a given filter expression string. - * @param expr the filter expression string for the filter. - **/ - public FilterImpl(Logger logger, String expr) throws InvalidSyntaxException + public FilterImpl(String filterStr) throws InvalidSyntaxException { - m_logger = logger; - if (expr == null) + try { - throw new InvalidSyntaxException("Filter cannot be null", null); + m_filter = SimpleFilter.parse(filterStr); } - - if (expr != null) + catch (Throwable th) { - CharArrayReader car = new CharArrayReader(expr.toCharArray()); - LdapLexer lexer = new LdapLexer(car); - Parser parser = new Parser(lexer); - try - { - if (!parser.start()) - { - throw new InvalidSyntaxException( - "Failed to parse LDAP query.", expr); - } - } - catch (ParseException ex) - { - throw new InvalidSyntaxException( - ex.getMessage(), expr); - } - catch (IOException ex) - { - throw new InvalidSyntaxException( - ex.getMessage(), expr); - } - m_evaluator = new Evaluator(parser.getProgram()); - m_mapper = new SimpleMapper(); + throw new InvalidSyntaxException(th.getMessage(), filterStr); } } - /** - * Compares the Filter object to another. - * @param o the object to compare this Filter against. - * @return If the other object is a Filter object, it - * returns this.toString().equals(obj.toString()); - * false otherwise. - **/ - public boolean equals(Object o) + public boolean match(ServiceReference sr) { - if (o == null) + if (sr instanceof ServiceReferenceImpl) { - return false; + return CapabilitySet.matches((ServiceReferenceImpl) sr, m_filter); } - else if (o instanceof Filter) + else { - return toString().equals(o.toString()); + return CapabilitySet.matches(new WrapperCapability(sr), m_filter); } - return false; } - /** - * Returns the hash code for the Filter object. - * @return The value this.toString().hashCode(). - **/ + public boolean match(Dictionary dctnr) + { + return CapabilitySet.matches(new WrapperCapability(dctnr, false), m_filter); + } + + public boolean matchCase(Dictionary dctnr) + { + return CapabilitySet.matches(new WrapperCapability(dctnr, true), m_filter); + } + + public boolean matches(Map map) + { + return CapabilitySet.matches(new WrapperCapability(map), m_filter); + } + + public boolean equals(Object o) + { + return toString().equals(o.toString()); + } + public int hashCode() { return toString().hashCode(); } - /** - * Filter using a Dictionary object. The Filter - * is executed using the Dictionary object's keys and values. - * @param dict the Dictionary object whose keys and values - * are used to determine a match. - * @return true if the Dictionary object's keys - * and values match this filter; false otherwise. - * @throws IllegalArgumentException if the dictionary contains case - * variants of the same key name. - **/ - public boolean match(Dictionary dict) - throws IllegalArgumentException + public String toString() { - try + return m_filter.toString(); + } + + static class WrapperCapability extends BundleCapabilityImpl + { + private final Map m_map; + + public WrapperCapability(Map map) { - m_mapper.setSource(dict, false); - return m_evaluator.evaluate(m_mapper); + super(null, null, Collections.EMPTY_MAP, Collections.EMPTY_MAP); + m_map = (map == null) ? Collections.EMPTY_MAP : map; } - catch (AttributeNotFoundException ex) + + public WrapperCapability(Dictionary dict, boolean caseSensitive) { - log(Logger.LOG_DEBUG, "FilterImpl: Attribute not found.", ex); + super(null, null, Collections.EMPTY_MAP, Collections.EMPTY_MAP); + m_map = new DictionaryToMap(dict, caseSensitive); } - catch (EvaluationException ex) + + public WrapperCapability(ServiceReference sr) { - log(Logger.LOG_ERROR, "FilterImpl: " + toString(), ex); + super(null, null, Collections.EMPTY_MAP, Collections.EMPTY_MAP); + m_map = new StringMap(); + for (String key : sr.getPropertyKeys()) + { + m_map.put(key, sr.getProperty(key)); + } } - return false; - } - /** - * Filter using a service's properties. The Filter - * is executed using the properties of the referenced service. - * @param ref A reference to the service whose properties - * are used to determine a match. - * @return true if the service's properties match this - * filter; false otherwise. - **/ - public boolean match(ServiceReference ref) - { - try + @Override + public BundleRevision getRevision() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String getNamespace() { - m_mapper.setSource(ref); - return m_evaluator.evaluate(m_mapper); + throw new UnsupportedOperationException("Not supported yet."); } - catch (AttributeNotFoundException ex) + + @Override + public Map getDirectives() { - log(Logger.LOG_DEBUG, "FilterImpl: Attribute not found.", ex); + throw new UnsupportedOperationException("Not supported yet."); } - catch (EvaluationException ex) + + @Override + public Map getAttributes() { - log(Logger.LOG_ERROR, "FilterImpl: " + toString(), ex); + return m_map; + } + + @Override + public List getUses() + { + throw new UnsupportedOperationException("Not supported yet."); } - return false; } - public boolean matchCase(Dictionary dictionary) + private static class DictionaryToMap implements Map { - try + private final Map m_map; + private final Dictionary m_dict; + + public DictionaryToMap(Dictionary dict, boolean caseSensitive) { - m_mapper.setSource(dictionary, true); - return m_evaluator.evaluate(m_mapper); + if (!caseSensitive) + { + m_dict = null; + m_map = new StringMap(); + if (dict != null) + { + Enumeration keys = dict.keys(); + while (keys.hasMoreElements()) + { + Object key = keys.nextElement(); + if (m_map.get(key) == null) + { + m_map.put(key, dict.get(key)); + } + else + { + throw new IllegalArgumentException( + "Duplicate attribute: " + key.toString()); + } + } + } + } + else + { + m_dict = dict; + m_map = null; + } } - catch (AttributeNotFoundException ex) + + public int size() { - log(Logger.LOG_DEBUG, "FilterImpl: Attribute not found.", ex); + throw new UnsupportedOperationException("Not supported yet."); } - catch (EvaluationException ex) + + public boolean isEmpty() { - log(Logger.LOG_ERROR, "FilterImpl: " + toString(), ex); + throw new UnsupportedOperationException("Not supported yet."); } - return false; - } - /** - * Returns the Filter object's filter string. - * @return Filter string. - **/ - public String toString() - { - if (m_toString == null) + public boolean containsKey(Object o) { - m_toString = m_evaluator.toStringInfix(); + throw new UnsupportedOperationException("Not supported yet."); } - return m_toString; - } - - static class SimpleMapper implements Mapper - { - private ServiceReference m_ref = null; - private StringMap m_map = null; - public void setSource(ServiceReference ref) + public boolean containsValue(Object o) { - m_ref = ref; - m_map = null; + throw new UnsupportedOperationException("Not supported yet."); } - public void setSource(Dictionary dict, boolean caseSensitive) + public Object get(Object o) { - // Create a map if we don't have one. - - if (m_map == null) + if (m_dict != null) { - m_map = new StringMap(); + return m_dict.get(o); } - else + else if (m_map != null) { - m_map.clear(); + return m_map.get(o); } + return null; + } - // Set case comparison accordingly. - m_map.setCaseSensitive(caseSensitive); + public Object put(Object k, Object v) + { + throw new UnsupportedOperationException("Not supported yet."); + } - // Put all dictionary entries into the map. - if (dict != null) - { - Enumeration keys = dict.keys(); - while (keys.hasMoreElements()) - { - Object key = keys.nextElement(); - if (m_map.get(key) == null) - { - m_map.put(key, dict.get(key)); - } - else - { - throw new IllegalArgumentException( - "Duplicate attribute: " + key.toString()); - } - } - } - m_ref = null; + public Object remove(Object o) + { + throw new UnsupportedOperationException("Not supported yet."); } - public Object lookup(String name) + public void putAll(Map map) { - if (m_map == null) - { - return m_ref.getProperty(name); - } - return m_map.get(name); + throw new UnsupportedOperationException("Not supported yet."); } - } - private void log(int flag, String msg, Throwable th) - { - if (m_logger == null) + public void clear() { - System.out.println(msg + ": " + th); + throw new UnsupportedOperationException("Not supported yet."); } - else + + public Set keySet() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public Collection values() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public Set> entrySet() { - m_logger.log(flag, msg, th); + return Collections.EMPTY_SET; } } } \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/FindEntriesEnumeration.java b/framework/src/main/java/org/apache/felix/framework/FindEntriesEnumeration.java deleted file mode 100644 index 61c125a9d39..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/FindEntriesEnumeration.java +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework; - -import java.util.*; - -import org.apache.felix.framework.searchpolicy.ContentLoaderImpl; - -class FindEntriesEnumeration implements Enumeration -{ - private BundleImpl m_bundle = null; - private Enumeration m_enumeration = null; - private String m_path = null; - private String[] m_filePattern = null; - private boolean m_recurse = false; - private Object m_next = null; - - public FindEntriesEnumeration( - BundleImpl bundle, String path, String filePattern, boolean recurse) - { - m_bundle = bundle; - m_path = path; - m_enumeration = m_bundle.getInfo().getCurrentModule() - .getContentLoader().getContent().getEntries(); - m_recurse = recurse; - - // Sanity check the parameters. - if (m_path == null) - { - throw new IllegalArgumentException("The path for findEntries() cannot be null."); - } - // Strip leading '/' if present. - if ((m_path.length() > 0) && (m_path.charAt(0) == '/')) - { - m_path = m_path.substring(1); - } - // Add a '/' to the end if not present. - if ((m_path.length() > 0) && (m_path.charAt(path.length() - 1) != '/')) - { - m_path = m_path + "/"; - } - - // File pattern defaults to "*" if not specified. - filePattern = (filePattern == null) ? "*" : filePattern; - - m_filePattern = parseSubstring(filePattern); - - m_next = findNext(); - } - - public boolean hasMoreElements() - { - return (m_next != null); - } - - public Object nextElement() - { - if (m_next == null) - { - throw new NoSuchElementException("No more entry paths."); - } - Object last = m_next; - m_next = findNext(); - return last; - } - - private Object findNext() - { - // This method filters the content entry enumeration, such that - // it only displays the contents of the directory specified by - // the path argument either recursively or not; much like using - // "ls -R" or "ls" to list the contents of a directory, respectively. - while (m_enumeration.hasMoreElements()) - { - // Get the next entry name. - String entryName = (String) m_enumeration.nextElement(); - // Check to see if it is a descendent of the specified path. - if (!entryName.equals(m_path) && entryName.startsWith(m_path)) - { - // NOTE: We assume here that directories are not returned, - // unlike getEntryPaths() above, where directories are returned; - // this may or may not be the correct spec interpretation. - - // If this is recursive, then simply verify that the - // entry is not a directory my making sure it does not - // end with '/'. If this is not recursive, then verify - // that the entry is a child of the path and not a - // grandchild by examining its remaining path length. - // This code uses the knowledge that content entries - // corresponding to directories end in '/'. - int idx = entryName.indexOf('/', m_path.length()); - if ((m_recurse && (entryName.charAt(entryName.length() - 1) != '/')) - || (idx < 0)) - { - // Get the last element of the path. - idx = entryName.lastIndexOf('/'); - String lastElement = entryName; - if (idx >= 0) - { - lastElement = entryName.substring(idx + 1); - } - // See if the file pattern matches the last element of the path. - if (checkSubstring(m_filePattern, lastElement)) - { - // Convert entry name into an entry URL. - return ((ContentLoaderImpl) m_bundle.getInfo() - .getCurrentModule().getContentLoader()) - .getResourceFromContent(entryName); - } - } - } - } - - return null; - } - - // - // The following substring-related code was lifted and modified - // from the LDAP parser code. - // - - private static String[] parseSubstring(String target) - { - List pieces = new ArrayList(); - StringBuffer ss = new StringBuffer(); - // int kind = SIMPLE; // assume until proven otherwise - boolean wasStar = false; // indicates last piece was a star - boolean leftstar = false; // track if the initial piece is a star - boolean rightstar = false; // track if the final piece is a star - - int idx = 0; - - // We assume (sub)strings can contain leading and trailing blanks - for (;;) - { - if (idx >= target.length()) - { - if (wasStar) - { - // insert last piece as "" to handle trailing star - rightstar = true; - } - else - { - pieces.add(ss.toString()); - // accumulate the last piece - // note that in the case of - // (cn=); this might be - // the string "" (!=null) - } - ss.setLength(0); - break; - } - - char c = target.charAt(idx++); - if (c == '*') - { - if (wasStar) - { - // encountered two successive stars; - // I assume this is illegal - throw new IllegalArgumentException("Invalid filter string: " + target); - } - if (ss.length() > 0) - { - pieces.add(ss.toString()); // accumulate the pieces - // between '*' occurrences - } - ss.setLength(0); - // if this is a leading star, then track it - if (pieces.size() == 0) - { - leftstar = true; - } - ss.setLength(0); - wasStar = true; - } - else - { - wasStar = false; - ss.append(c); - } - } - if (leftstar || rightstar || pieces.size() > 1) - { - // insert leading and/or trailing "" to anchor ends - if (rightstar) - { - pieces.add(""); - } - if (leftstar) - { - pieces.add(0, ""); - } - } - - return (String[]) pieces.toArray(new String[pieces.size()]); - } - - private static boolean checkSubstring(String[] pieces, String s) - { - // Walk the pieces to match the string - // There are implicit stars between each piece, - // and the first and last pieces might be "" to anchor the match. - // assert (pieces.length > 1) - // minimal case is * - - boolean result = false; - int len = pieces.length; - int index = 0; - - for (int i = 0; i < len; i++) - { - String piece = (String) pieces[i]; - if (i == len - 1) - { - // this is the last piece - if (s.endsWith(piece)) - { - result = true; - } - else - { - result = false; - } - break; - } - // initial non-star; assert index == 0 - else if (i == 0) - { - if (!s.startsWith(piece)) - { - result = false; - break; - } - } - // assert i > 0 && i < len-1 - else - { - // Sure wish stringbuffer supported e.g. indexOf - index = s.indexOf(piece, index); - if (index < 0) - { - result = false; - break; - } - } - // start beyond the matching piece - index += piece.length(); - } - - return result; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/FrameworkFactory.java b/framework/src/main/java/org/apache/felix/framework/FrameworkFactory.java new file mode 100644 index 00000000000..ce377ad2601 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/FrameworkFactory.java @@ -0,0 +1,30 @@ +/* + * 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.framework; + +import java.util.Map; +import org.osgi.framework.launch.Framework; + +public class FrameworkFactory implements org.osgi.framework.launch.FrameworkFactory +{ + public Framework newFramework(Map configuration) + { + return new Felix(configuration); + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/FrameworkStartLevelImpl.java b/framework/src/main/java/org/apache/felix/framework/FrameworkStartLevelImpl.java new file mode 100644 index 00000000000..db4a611ddee --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/FrameworkStartLevelImpl.java @@ -0,0 +1,348 @@ +/* + * 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.framework; + +import java.util.ArrayList; +import java.util.List; + +import org.osgi.framework.AdminPermission; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.startlevel.BundleStartLevel; +import org.osgi.framework.startlevel.FrameworkStartLevel; +import org.osgi.service.startlevel.StartLevel; + +class FrameworkStartLevelImpl implements FrameworkStartLevel, Runnable +{ + static final String THREAD_NAME = "FelixStartLevel"; + + private static final int BUNDLE_IDX = 0; + private static final int STARTLEVEL_IDX = 1; + + private final Felix m_felix; + private final ServiceRegistry m_registry; + private final List m_requests = new ArrayList(); + private final List m_requestListeners + = new ArrayList(); + private ServiceRegistration m_slReg; + private Thread m_thread = null; + + FrameworkStartLevelImpl(Felix felix, ServiceRegistry registry) + { + m_felix = felix; + m_registry = registry; + } + + @SuppressWarnings("unchecked") + void start() + { + m_slReg = (ServiceRegistration) m_registry.registerService(m_felix, + new String[] { StartLevel.class.getName() }, + new StartLevelImpl(m_felix), + null); + } + + // Should only be called hold requestList lock. + private void startThread() + { + // Start a thread to perform asynchronous package refreshes. + if (m_thread == null) + { + m_thread = new Thread(this, THREAD_NAME); + m_thread.setDaemon(true); + m_thread.start(); + } + } + + /** + * Stops the FelixStartLevel thread on system shutdown. Shutting down the + * thread explicitly is required in the embedded case, where Felix may be + * stopped without the Java VM being stopped. In this case the + * FelixStartLevel thread must be stopped explicitly. + *

      + * This method is called by the + * {@link StartLevelActivator#stop(BundleContext)} method. + */ + void stop() + { + synchronized (m_requests) + { + if (m_thread != null) + { + // Null thread variable to signal to the thread that + // we want it to exit. + m_thread = null; + + // Wake up the thread, if it is currently in the wait() state + // for more work. + m_requests.notifyAll(); + } + } + } + + public Bundle getBundle() + { + return m_felix; + } + + public int getStartLevel() + { + return m_felix.getActiveStartLevel(); + } + + public void setStartLevel(int startlevel, FrameworkListener... listeners) + { + Object sm = System.getSecurityManager(); + + if (sm != null) + { + ((SecurityManager) sm).checkPermission( + new AdminPermission(m_felix, AdminPermission.STARTLEVEL)); + } + + if (startlevel <= 0) + { + throw new IllegalArgumentException( + "Start level must be greater than zero."); + } + + synchronized (m_requests) + { + if (m_thread == null) + { + throw new IllegalStateException("No inital startlevel yet"); + } + // Queue request. + m_requestListeners.add(listeners); + m_requests.add(new Integer(startlevel)); + m_requests.notifyAll(); + } + } + + /** + * This method is currently only called by the by the thread that calls + * the Felix.start() method and the shutdown thread when the + * framework is shutting down. + * @param startlevel + **/ + /* package */ void setStartLevelAndWait(int startlevel) + { + Object request = new Integer(startlevel); + synchronized (request) + { + synchronized (m_requests) + { + // Start thread if necessary. + startThread(); + // Queue request. + m_requestListeners.add(null); + m_requests.add(request); + m_requests.notifyAll(); + } + + try + { + request.wait(); + } + catch (InterruptedException ex) + { + // Log it and ignore since it won't cause much of an issue. + m_felix.getLogger().log( + Logger.LOG_WARNING, + "Wait for start level change during shutdown interrupted.", + ex); + } + } + } + + public int getInitialBundleStartLevel() + { + return m_felix.getInitialBundleStartLevel(); + } + + public void setInitialBundleStartLevel(int startlevel) + { + Object sm = System.getSecurityManager(); + + if (sm != null) + { + ((SecurityManager) sm).checkPermission( + new AdminPermission(m_felix, AdminPermission.STARTLEVEL)); + } + m_felix.setInitialBundleStartLevel(startlevel); + } + + BundleStartLevel createBundleStartLevel(BundleImpl bundle) + { + return new BundleStartLevelImpl(bundle); + } + + class BundleStartLevelImpl implements BundleStartLevel + { + private BundleImpl m_bundle; + + private BundleStartLevelImpl(BundleImpl bundle) + { + m_bundle = bundle; + } + + public Bundle getBundle() + { + return m_bundle; + } + + public int getStartLevel() + { + return m_felix.getBundleStartLevel(m_bundle); + } + + public void setStartLevel(int startlevel) + { + Object sm = System.getSecurityManager(); + + if (sm != null) + { + ((SecurityManager) sm).checkPermission( + new AdminPermission(m_bundle, AdminPermission.EXECUTE)); + } + + if (m_bundle.getBundleId() == 0) + { + throw new IllegalArgumentException( + "Cannot change system bundle start level."); + } + else if (startlevel <= 0) + { + throw new IllegalArgumentException( + "Start level must be greater than zero."); + } + synchronized (m_requests) + { + // Start thread if necessary. + startThread(); + // Synchronously persists the start level. + m_bundle.setStartLevel(startlevel); + // Queue request. + m_requestListeners.add(null); + m_requests.add(new Object[] { m_bundle, new Integer(startlevel) }); + m_requests.notifyAll(); + } + } + + public boolean isPersistentlyStarted() + { + return m_felix.isBundlePersistentlyStarted(m_bundle); + } + + public boolean isActivationPolicyUsed() + { + return m_felix.isBundleActivationPolicyUsed(m_bundle); + } + } + + public void run() + { + // This thread loops forever, thus it should + // be a daemon thread. + Object previousRequest = null; + while (true) + { + Object request = null; + FrameworkListener[] listeners = null; + synchronized (m_requests) + { + // Wait for a request. + while (m_requests.isEmpty()) + { + // Terminate the thread if requested to do so (see stop()). + if (m_thread == null) + { + return; + } + + try + { + m_requests.wait(); + } + catch (InterruptedException ex) + { + // Ignore. + } + } + + // Get the requested start level. + request = m_requests.remove(0); + listeners = m_requestListeners.remove(0); + } + + // If the request object is an Integer, then the request + // is to set the framework start level. If the request is + // an Object array, then the request is to set the start + // level for a bundle. + // NOTE: We don't catch any exceptions here, because + // the invoked methods shield us from exceptions by + // catching Throwables when they invoke callbacks. + if (request instanceof Integer) + { + // Set the new framework start level. + try + { + m_felix.setActiveStartLevel(((Integer) request).intValue(), listeners); + } + catch (IllegalStateException ise) + { + // Thrown if global lock cannot be acquired, in which case + // just retry (unless we already did) + if (previousRequest == request) + { + m_felix.getLogger().log(Logger.LOG_ERROR, + "Unexpected problem setting active start level to " + request, ise); + } + else + { + synchronized (m_requests) + { + m_requests.add(0, request); + previousRequest = request; + } + } + } + catch (Exception ex) + { + m_felix.getLogger().log(Logger.LOG_ERROR, + "Unexpected problem setting active start level to " + request, ex); + } + } + else + { + Bundle bundle = (Bundle) ((Object[]) request)[BUNDLE_IDX]; + int startlevel = ((Integer) ((Object[]) request)[STARTLEVEL_IDX]).intValue(); + m_felix.setBundleStartLevel(bundle, startlevel); + } + + // Notify any waiting thread that this request is done. + synchronized (request) + { + request.notifyAll(); + } + } + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/FrameworkUtil.java b/framework/src/main/java/org/apache/felix/framework/FrameworkUtil.java deleted file mode 100644 index 6d31e4b8067..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/FrameworkUtil.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework; - -import org.osgi.framework.Filter; -import org.osgi.framework.InvalidSyntaxException; - -public class FrameworkUtil -{ - public static Filter createFilter(String filter) - throws InvalidSyntaxException - { - return new FilterImpl(filter); - } -} diff --git a/framework/src/main/java/org/apache/felix/framework/FrameworkWiringImpl.java b/framework/src/main/java/org/apache/felix/framework/FrameworkWiringImpl.java new file mode 100644 index 00000000000..7bc42ce438d --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/FrameworkWiringImpl.java @@ -0,0 +1,206 @@ +/* + * 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.framework; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.osgi.framework.AdminPermission; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.FrameworkWiring; +import org.osgi.resource.Requirement; +import org.osgi.service.packageadmin.PackageAdmin; + +class FrameworkWiringImpl implements FrameworkWiring, Runnable +{ + private final Felix m_felix; + private final ServiceRegistry m_registry; + private final List> m_requests = new ArrayList(); + private final List m_requestListeners + = new ArrayList(); + private ServiceRegistration m_paReg; + private Thread m_thread = null; + + + public FrameworkWiringImpl(Felix felix, ServiceRegistry registry) + { + m_felix = felix; + m_registry = registry; + } + + @SuppressWarnings("unchecked") + void start() + { + m_paReg = (ServiceRegistration) m_registry.registerService(m_felix, + new String[] { PackageAdmin.class.getName() }, + new PackageAdminImpl(m_felix), + null); + } + + /** + * Stops the FelixFrameworkWiring thread on system shutdown. Shutting down the + * thread explicitly is required in the embedded case, where Felix may be + * stopped without the Java VM being stopped. In this case the + * FelixFrameworkWiring thread must be stopped explicitly. + *

      + * This method is called by the + * {@link PackageAdminActivator#stop(BundleContext)} method. + */ + void stop() + { + synchronized (m_requests) + { + if (m_thread != null) + { + // Null thread variable to signal to the thread that + // we want it to exit. + m_thread = null; + + // Wake up the thread, if it is currently in the wait() state + // for more work. + m_requests.notifyAll(); + } + } + } + + public Bundle getBundle() + { + return m_felix; + } + + public void refreshBundles(Collection bundles, FrameworkListener... listeners) + { + Object sm = System.getSecurityManager(); + + if (sm != null) + { + ((SecurityManager) sm).checkPermission( + new AdminPermission(m_felix, AdminPermission.RESOLVE)); + } + synchronized (m_requests) + { + // Start a thread to perform asynchronous package refreshes. + if (m_thread == null) + { + m_thread = new Thread(this, "FelixFrameworkWiring"); + m_thread.setDaemon(true); + m_thread.start(); + } + + // Queue request and notify thread. + m_requests.add(bundles); + m_requestListeners.add(listeners); + m_requests.notifyAll(); + } + } + + public boolean resolveBundles(Collection bundles) + { + Object sm = System.getSecurityManager(); + + if (sm != null) + { + ((SecurityManager) sm).checkPermission( + new AdminPermission(m_felix, AdminPermission.RESOLVE)); + } + + if (m_thread == null) + { + return false; + } + + return m_felix.resolveBundles(bundles); + } + + public Collection getRemovalPendingBundles() + { + return m_felix.getRemovalPendingBundles(); + } + + public Collection getDependencyClosure(Collection targets) + { + return m_felix.getDependencyClosure(targets); + } + + /** + * The OSGi specification states that package refreshes happen + * asynchronously; this is the run() method for the package + * refreshing thread. + **/ + public void run() + { + // This thread loops forever, thus it should + // be a daemon thread. + while (true) + { + Collection bundles = null; + FrameworkListener[] listeners = null; + synchronized (m_requests) + { + // Wait for a refresh request. + while (m_requests.isEmpty()) + { + // Terminate the thread if requested to do so (see stop()). + if (m_thread == null) + { + return; + } + + try + { + m_requests.wait(); + } + catch (InterruptedException ex) + { + } + } + + // Get the bundles parameter for the current refresh request. + bundles = m_requests.get(0); + listeners = m_requestListeners.get(0); + } + + // Perform refresh. + // NOTE: We don't catch any exceptions here, because + // the invoked method shields us from exceptions by + // catching Throwables when its invokes callbacks. + m_felix.refreshPackages(bundles, listeners); + + // Remove the first request since it is now completed. + synchronized (m_requests) + { + m_requests.remove(0); + m_requestListeners.remove(0); + } + } + } + + /** + * @see org.osgi.framework.wiring.FrameworkWiring#findProviders(org.osgi.resource.Requirement) + */ + public Collection findProviders(final Requirement requirement) + { + return m_felix.findProviders(requirement); + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/GetEntryPathsEnumeration.java b/framework/src/main/java/org/apache/felix/framework/GetEntryPathsEnumeration.java deleted file mode 100644 index 0d7529aec98..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/GetEntryPathsEnumeration.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework; - -import java.util.Enumeration; -import java.util.NoSuchElementException; - -class GetEntryPathsEnumeration implements Enumeration -{ - private BundleImpl m_bundle = null; - private Enumeration m_enumeration = null; - private String m_path = null; - private Object m_next = null; - - public GetEntryPathsEnumeration(BundleImpl bundle, String path) - { - m_bundle = bundle; - m_path = path; - m_enumeration = m_bundle.getInfo().getCurrentModule() - .getContentLoader().getContent().getEntries(); - - // Sanity check the parameters. - if (m_path == null) - { - throw new IllegalArgumentException("The path for findEntries() cannot be null."); - } - // Strip leading '/' if present. - if ((m_path.length() > 0) && (m_path.charAt(0) == '/')) - { - m_path = m_path.substring(1); - } - // Add a '/' to the end if not present. - if ((m_path.length() > 0) && (m_path.charAt(m_path.length() - 1) != '/')) - { - m_path = m_path + "/"; - } - - m_next = findNext(); - } - - public boolean hasMoreElements() - { - return (m_next != null); - } - - public Object nextElement() - { - if (m_next == null) - { - throw new NoSuchElementException("No more entry paths."); - } - Object last = m_next; - m_next = findNext(); - return last; - } - - private Object findNext() - { - // This method filters the content entry enumeration, such that - // it only displays the contents of the directory specified by - // the path argument; much like using "ls" to list the contents - // of a directory. - while (m_enumeration.hasMoreElements()) - { - // Get the next entry name. - String entryName = (String) m_enumeration.nextElement(); - // Check to see if it is a descendent of the specified path. - if (!entryName.equals(m_path) && entryName.startsWith(m_path)) - { - // Verify that it is a child of the path and not a - // grandchild by examining its remaining path length. - // This code uses the knowledge that content entries - // corresponding to directories end in '/'. It checks - // to see if the next occurrence of '/' is also the - // end of the string, which means that this entry - // represents a child directory of the path. - int idx = entryName.indexOf('/', m_path.length()); - if ((idx < 0) || (idx == (entryName.length() - 1))) - { - return entryName; - } - } - } - return null; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/HookRegistry.java b/framework/src/main/java/org/apache/felix/framework/HookRegistry.java new file mode 100644 index 00000000000..1d8ff8254c9 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/HookRegistry.java @@ -0,0 +1,237 @@ +/* + * 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.framework; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; + +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceReference; + +/** + * This registry holds all services implementing one of the hook services + */ +public class HookRegistry +{ + /** no need to use a sync'ed structure as this is read only. */ + private final static Map> HOOK_CLASSES = new HashMap>(); + + static + { + addHookClass(org.osgi.framework.hooks.bundle.CollisionHook.class); + addHookClass(org.osgi.framework.hooks.bundle.FindHook.class); + addHookClass(org.osgi.framework.hooks.bundle.EventHook.class); + addHookClass(org.osgi.framework.hooks.service.EventHook.class); + addHookClass(org.osgi.framework.hooks.service.EventListenerHook.class); + addHookClass(org.osgi.framework.hooks.service.FindHook.class); + addHookClass(org.osgi.framework.hooks.service.ListenerHook.class); + addHookClass(org.osgi.framework.hooks.weaving.WeavingHook.class); + addHookClass(org.osgi.framework.hooks.weaving.WovenClassListener.class); + addHookClass(org.osgi.framework.hooks.resolver.ResolverHookFactory.class); + addHookClass(org.osgi.service.url.URLStreamHandlerService.class); + addHookClass(java.net.ContentHandler.class); + }; + + private static void addHookClass(final Class c) { + HOOK_CLASSES.put(c.getName(), c); + } + + private final Map>> m_allHooks = + new ConcurrentHashMap>>(); + + private final WeakHashMap, ServiceReference> m_blackList = + new WeakHashMap, ServiceReference>(); + + + static boolean isHook(final String[] classNames, final Class hookClass, final Object svcObj) + { + for (final String serviceName : classNames) + { + if (serviceName.equals(hookClass.getName())) + { + // For a service factory, we can only match names. + if (svcObj instanceof ServiceFactory) + { + return true; + } + // For a service object, check if its class matches. + if (hookClass.isAssignableFrom(svcObj.getClass())) + { + return true; + } + } + } + + return false; + } + + private boolean isHook(final String serviceName, final Object svcObj) + { + final Class hookClass = HOOK_CLASSES.get(serviceName); + if ( hookClass != null ) + { + // For a service factory, we can only match names. + if (svcObj instanceof ServiceFactory) + { + return true; + } + // For a service object, check if its class matches. + if (hookClass.isAssignableFrom(svcObj.getClass())) + { + return true; + } + } + + return false; + } + + /** + * Check and add the service to the set of hooks + * @param classNames The service names + * @param svcObj The service object + * @param ref The service reference + */ + public void addHooks(final String[] classNames, final Object svcObj, final ServiceReference ref) + { + for(final String serviceName : classNames) + { + if (isHook(serviceName, svcObj)) + { + synchronized (m_allHooks) // we need to sync as we replace the value + { + SortedSet> hooks = m_allHooks.get(serviceName); + if (hooks == null) + { + hooks = new TreeSet>(Collections.reverseOrder()); + } + else + { + hooks = new TreeSet>(hooks); + } + hooks.add(ref); + m_allHooks.put(serviceName, hooks); + } + } + } + } + + /** + * Update the service ranking for a hook + * @param ref The service reference + */ + public void updateHooks(final ServiceReference ref) + { + // We maintain the hooks sorted, so if ranking has changed for example, + // we need to ensure the order remains correct by resorting the hooks. + final Object svcObj = ((ServiceRegistrationImpl.ServiceReferenceImpl) ref) + .getRegistration().getService(); + final String [] classNames = (String[]) ref.getProperty(Constants.OBJECTCLASS); + + for(final String serviceName : classNames) + { + if (isHook(serviceName, svcObj)) + { + synchronized (m_allHooks) // we need to sync as we replace the value + { + SortedSet> hooks = m_allHooks.get(serviceName); + if (hooks != null) + { + TreeSet> newHooks = new TreeSet>(Collections.reverseOrder()); + for (ServiceReference hook : hooks) { + newHooks.add(hook); // copy constructor / addAll() does not re-sort + } + + m_allHooks.put(serviceName, newHooks); + } + } + } + } + } + + /** + * Remove the service hooks + * @param ref The service reference + */ + public void removeHooks(final ServiceReference ref) + { + final Object svcObj = ((ServiceRegistrationImpl.ServiceReferenceImpl) ref) + .getRegistration().getService(); + final String [] classNames = (String[]) ref.getProperty(Constants.OBJECTCLASS); + + for(final String serviceName : classNames) + { + if (isHook(serviceName, svcObj)) + { + synchronized (m_allHooks) // we need to sync as we replace the value + { + SortedSet> hooks = m_allHooks.get(serviceName); + if (hooks != null) + { + hooks = new TreeSet>(hooks); + hooks.remove(ref); + m_allHooks.put(serviceName, hooks); + } + } + } + } + synchronized ( m_blackList ) + { + m_blackList.remove(ref); + } + } + + /** + * Return the sorted set of hooks + * @param hookClass The hook class + * @return The sorted set - the set might be empty + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Set> getHooks(final Class hookClass) + { + final Set> hooks = m_allHooks.get(hookClass.getName()); + if (hooks != null) + { + return (Set)hooks; + } + return Collections.emptySet(); + } + + public boolean isHookBlackListed(final ServiceReference sr) + { + synchronized ( m_blackList ) + { + return m_blackList.containsKey(sr); + } + } + + public void blackListHook(final ServiceReference sr) + { + synchronized ( m_blackList ) + { + m_blackList.put(sr, sr); + } + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/Logger.java b/framework/src/main/java/org/apache/felix/framework/Logger.java index 6266c10d4b0..944590b1549 100644 --- a/framework/src/main/java/org/apache/felix/framework/Logger.java +++ b/framework/src/main/java/org/apache/felix/framework/Logger.java @@ -1,118 +1,143 @@ /* - * Copyright 2005 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework; +import java.lang.reflect.Method; + +import org.osgi.framework.Bundle; import org.osgi.framework.BundleException; import org.osgi.framework.ServiceReference; /** *

      * This class mimics the standard OSGi LogService interface. An - * instance of this class will be used by the framework for all logging. Currently, - * the implementation of this class just sends log messages to standard output, - * but in the future it will be modified to use a log service if one is - * installed in the framework. To do so, it will need to use reflection to - * call the log service methods, since it will not have access to the - * LogService class. + * instance of this class is used by the framework for all logging. By default + * this class logs messages to standard out. The log level can be set to + * control the amount of logging performed, where a higher number results in + * more logging. A log level of zero turns off logging completely. + *

      + *

      + * The log levels match those specified in the OSGi Log Service (i.e., 1 = error, + * 2 = warning, 3 = information, and 4 = debug). The default value is 1. *

      **/ -// TODO: Modify LogWrapper to get LogService service object and invoke with reflection. -public class Logger +public class Logger extends org.apache.felix.resolver.Logger { - public static final int LOG_ERROR = 1; - public static final int LOG_WARNING = 2; - public static final int LOG_INFO = 3; - public static final int LOG_DEBUG = 4; - - private Object m_logObj = null; + private Object[] m_logger; public Logger() { + super(LOG_ERROR); } - public void log(int level, String msg) + public void setLogger(Object logger) { - synchronized (this) + if (logger == null) { - if (m_logObj != null) + m_logger = null; + } + else + { + try { -// Will use reflection. -// m_logObj.log(level, msg); + Method mth = logger.getClass().getMethod("log", + Integer.TYPE, String.class, Throwable.class); + mth.setAccessible(true); + m_logger = new Object[] { logger, mth }; } - else + catch (NoSuchMethodException ex) { - _log(null, level, msg, null); + System.err.println("Logger: " + ex); + m_logger = null; } } } - public void log(int level, String msg, Throwable ex) + public final void log(ServiceReference sr, int level, String msg) + { + _log(null, sr, level, msg, null); + } + + public final void log(ServiceReference sr, int level, String msg, Throwable throwable) + { + _log(null, sr, level, msg, throwable); + } + + public final void log(Bundle bundle, int level, String msg) + { + _log(bundle, null, level, msg, null); + } + + public final void log(Bundle bundle, int level, String msg, Throwable throwable) + { + _log(bundle, null, level, msg, throwable); + } + + protected void _log( + Bundle bundle, ServiceReference sr, int level, + String msg, Throwable throwable) { - synchronized (this) + if (getLogLevel() >= level) { - if (m_logObj != null) - { -// Will use reflection. -// m_logObj.log(level, msg); - } - else - { - _log(null, level, msg, ex); - } + // Default logging action. + doLog(bundle, sr, level, msg, throwable); } } - public void log(ServiceReference sr, int level, String msg) + protected void doLog( + Bundle bundle, ServiceReference sr, int level, + String msg, Throwable throwable) { - synchronized (this) + StringBuilder s = new StringBuilder(); + if (sr != null) { - if (m_logObj != null) - { -// Will use reflection. -// m_logObj.log(level, msg); - } - else - { - _log(sr, level, msg, null); - } + s.append("SvcRef ").append(sr).append(" ").append(msg); + } + else if (bundle != null) + { + s.append("Bundle ").append(bundle.toString()).append(" ").append(msg); } + else + { + s.append(msg); + } + if (throwable != null) + { + s.append(" (").append(throwable).append(")"); + } + doLog(level, s.toString(), throwable); } - public void log(ServiceReference sr, int level, String msg, Throwable ex) + protected void doLog(int level, String msg, Throwable throwable) { - synchronized (this) + if (m_logger != null) { - if (m_logObj != null) - { -// Will use reflection. -// m_logObj.log(level, msg); - } - else - { - _log(sr, level, msg, ex); - } + doLogReflectively(level, msg, throwable); + } + else + { + doLogOut(level, msg, throwable); } } - - private void _log(ServiceReference sr, int level, String msg, Throwable ex) + + protected void doLogOut(int level, String s, Throwable throwable) { - String s = (sr == null) ? null : "SvcRef " + sr; - s = (s == null) ? msg : s + " " + msg; - s = (ex == null) ? s : s + " (" + ex + ")"; switch (level) { case LOG_DEBUG: @@ -120,14 +145,14 @@ private void _log(ServiceReference sr, int level, String msg, Throwable ex) break; case LOG_ERROR: System.out.println("ERROR: " + s); - if (ex != null) + if (throwable != null) { - if ((ex instanceof BundleException) && - (((BundleException) ex).getNestedException() != null)) + if ((throwable instanceof BundleException) && + (((BundleException) throwable).getNestedException() != null)) { - ex = ((BundleException) ex).getNestedException(); + throwable = ((BundleException) throwable).getNestedException(); } - ex.printStackTrace(); + throwable.printStackTrace(); } break; case LOG_INFO: @@ -140,4 +165,21 @@ private void _log(ServiceReference sr, int level, String msg, Throwable ex) System.out.println("UNKNOWN[" + level + "]: " + s); } } -} \ No newline at end of file + + protected void doLogReflectively(int level, String msg, Throwable throwable) + { + try + { + ((Method) m_logger[1]).invoke( + m_logger[0], + level, + msg, + throwable + ); + } + catch (Exception ex) + { + System.err.println("Logger: " + ex); + } + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/PackageAdminActivator.java b/framework/src/main/java/org/apache/felix/framework/PackageAdminActivator.java deleted file mode 100644 index 3cf1543addd..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/PackageAdminActivator.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework; - -import org.osgi.framework.*; - -class PackageAdminActivator implements BundleActivator -{ - private Felix m_felix = null; - private ServiceRegistration m_reg = null; - - public PackageAdminActivator(Felix felix) - { - m_felix = felix; - } - - public void start(BundleContext context) throws Exception - { - m_reg = context.registerService( - org.osgi.service.packageadmin.PackageAdmin.class.getName(), - new PackageAdminImpl(m_felix), null); - } - - public void stop(BundleContext context) throws Exception - { - m_reg.unregister(); - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/PackageAdminImpl.java b/framework/src/main/java/org/apache/felix/framework/PackageAdminImpl.java index f7c80138c42..20a340dded9 100644 --- a/framework/src/main/java/org/apache/felix/framework/PackageAdminImpl.java +++ b/framework/src/main/java/org/apache/felix/framework/PackageAdminImpl.java @@ -1,48 +1,60 @@ /* - * Copyright 2006 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework; import java.util.*; +import org.apache.felix.framework.util.Util; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.Version; +import org.osgi.framework.VersionRange; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleRevisions; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.FrameworkWiring; +import org.osgi.service.packageadmin.ExportedPackage; +import org.osgi.service.packageadmin.PackageAdmin; +import org.osgi.service.packageadmin.RequiredBundle; -import org.apache.felix.framework.searchpolicy.VersionRange; -import org.osgi.framework.*; -import org.osgi.service.packageadmin.*; -class PackageAdminImpl implements PackageAdmin, Runnable +public class PackageAdminImpl implements PackageAdmin { + private static final Comparator COMPARATOR = new Comparator() { + public int compare(Object o1, Object o2) + { + // Reverse arguments to sort in descending order. + return ((ExportedPackage) o2).getVersion().compareTo( + ((ExportedPackage) o1).getVersion()); + } + }; + private Felix m_felix = null; - private Bundle[][] m_reqBundles = null; - private Bundle m_systemBundle = null; - public PackageAdminImpl(Felix felix) + PackageAdminImpl(Felix felix) { m_felix = felix; - m_systemBundle = m_felix.getBundle(0); - - // Start a thread to perform asynchronous package refreshes. - Thread t = new Thread(this, "FelixPackageAdmin"); - t.setDaemon(true); - t.start(); } /** * Returns the bundle associated with this class if the class was * loaded from a bundle, otherwise returns null. - * + * * @param clazz the class for which to determine its associated bundle. * @return the bundle associated with the specified class, otherwise null. **/ @@ -56,47 +68,38 @@ public Bundle getBundle(Class clazz) * version is in the specified version range. If no version range is * specified, then all bundles with the specified symbolic name are * returned. The array is sorted in descending version order. - * + * * @param symbolicName the target symbolic name. * @param versionRange the target version range. * @return an array of matching bundles sorted in descending version order. **/ public Bundle[] getBundles(String symbolicName, String versionRange) { -// TODO: PACKAGEADMIN - This could be made more efficient by reducing object creation. - VersionRange vr = (versionRange == null) - ? null : VersionRange.parse(versionRange); + VersionRange vr = (versionRange == null) ? null : new VersionRange(versionRange); Bundle[] bundles = m_felix.getBundles(); List list = new ArrayList(); for (int i = 0; (bundles != null) && (i < bundles.length); i++) { - String sym = (String) ((BundleImpl) bundles[i]) - .getInfo().getCurrentHeader().get(Constants.BUNDLE_SYMBOLICNAME); + String sym = bundles[i].getSymbolicName(); if ((sym != null) && sym.equals(symbolicName)) { - String s = (String) ((BundleImpl) bundles[i]) - .getInfo().getCurrentHeader().get(Constants.BUNDLE_VERSION); - Version v = (s == null) ? new Version("0.0.0") : new Version(s); - if ((vr == null) || vr.isInRange(v)) + Version v = bundles[i].adapt(BundleRevision.class).getVersion(); + if ((vr == null) || vr.includes(v)) { list.add(bundles[i]); } } } - if (list.size() == 0) + if (list.isEmpty()) { return null; } bundles = (Bundle[]) list.toArray(new Bundle[list.size()]); - Arrays.sort(bundles, new Comparator() { + Arrays.sort(bundles,new Comparator() { public int compare(Object o1, Object o2) { - String s1 = (String) ((BundleImpl) o1) - .getInfo().getCurrentHeader().get(Constants.BUNDLE_VERSION); - String s2 = (String) ((BundleImpl) o2) - .getInfo().getCurrentHeader().get(Constants.BUNDLE_VERSION); - Version v1 = (s1 == null) ? new Version("0.0.0") : new Version(s1); - Version v2 = (s2 == null) ? new Version("0.0.0") : new Version(s2); + Version v1 = ((Bundle) o1).adapt(BundleRevision.class).getVersion(); + Version v2 = ((Bundle) o2).adapt(BundleRevision.class).getVersion(); // Compare in reverse order to get descending sort. return v2.compareTo(v1); } @@ -106,6 +109,12 @@ public int compare(Object o1, Object o2) public int getBundleType(Bundle bundle) { + Map headerMap = ((BundleRevisionImpl) + bundle.adapt(BundleRevisionImpl.class)).getHeaders(); + if (headerMap.containsKey(Constants.FRAGMENT_HOST)) + { + return PackageAdmin.BUNDLE_TYPE_FRAGMENT; + } return 0; } @@ -127,14 +136,7 @@ public ExportedPackage getExportedPackage(String name) return null; } // Sort the exported versions. - Arrays.sort(pkgs, new Comparator() { - public int compare(Object o1, Object o2) - { - // Reverse arguments to sort in descending order. - return ((ExportedPackage) o2).getVersion().compareTo( - ((ExportedPackage) o1).getVersion()); - } - }); + Arrays.sort(pkgs, COMPARATOR); // Return the highest version. return pkgs[0]; } @@ -152,124 +154,116 @@ public ExportedPackage[] getExportedPackages(String name) * @return an array of packages exported by the bundle or null if the * bundle does not export any packages. **/ - public ExportedPackage[] getExportedPackages(Bundle b) + public ExportedPackage[] getExportedPackages(Bundle bundle) { - return m_felix.getExportedPackages(b); - } - - /** - * The OSGi specification states that refreshing packages is - * asynchronous; this method simply notifies the package admin - * thread to do a refresh. - * @param bundles array of bundles to refresh or null to refresh - * any bundles in need of refreshing. - **/ - public synchronized void refreshPackages(Bundle[] bundles) - throws SecurityException - { - Object sm = System.getSecurityManager(); - - if (sm != null) - { - ((SecurityManager) sm).checkPermission( - new AdminPermission(m_systemBundle, AdminPermission.RESOLVE)); - } - - // Save our request parameters and notify all. - if (m_reqBundles == null) - { - m_reqBundles = new Bundle[][] { bundles }; - } - else - { - Bundle[][] newReqBundles = new Bundle[m_reqBundles.length + 1][]; - System.arraycopy(m_reqBundles, 0, - newReqBundles, 0, m_reqBundles.length); - newReqBundles[m_reqBundles.length] = bundles; - m_reqBundles = newReqBundles; - } - notifyAll(); + ExportedPackage[] pkgs = m_felix.getExportedPackages(bundle); + return ((pkgs == null) || pkgs.length == 0) ? null : pkgs; } - /** - * The OSGi specification states that package refreshes happen - * asynchronously; this is the run() method for the package - * refreshing thread. - **/ - public void run() + public Bundle[] getFragments(Bundle bundle) { - // This thread loops forever, thus it should - // be a daemon thread. - Bundle[] bundles = null; - while (true) + // If the bundle is not a fragment, then return its fragments. + if ((getBundleType(bundle) & BUNDLE_TYPE_FRAGMENT) == 0) { - synchronized (this) + List list = new ArrayList(); + // Iterate through revisions + for (BundleRevision revision : bundle.adapt(BundleRevisions.class).getRevisions()) { - // Wait for a refresh request. - while (m_reqBundles == null) + // Get attached fragments. + if (revision.getWiring() != null) { - try - { - wait(); - } - catch (InterruptedException ex) + List fragments = + Util.getFragments(revision.getWiring()); + for (int i = 0; i < fragments.size(); i++) { + Bundle b = fragments.get(i).getBundle(); + if (b != null) + { + list.add(b); + } } } - - // Get the bundles parameter for the current refresh request. - bundles = m_reqBundles[0]; } + // Convert list to an array. + return (list.isEmpty()) + ? null + : (Bundle[]) list.toArray(new Bundle[list.size()]); + } + return null; + } - // Perform refresh. - m_felix.refreshPackages(bundles); - - // Remove the first request since it is now completed. - synchronized (this) + public Bundle[] getHosts(Bundle bundle) + { + // If the bundle is a fragment, return its hosts + if ((getBundleType(bundle) & BUNDLE_TYPE_FRAGMENT) != 0) + { + List list = new ArrayList(); + // Iterate through revisions + for (BundleRevision revision : bundle.adapt(BundleRevisions.class).getRevisions()) { - if (m_reqBundles.length == 1) - { - m_reqBundles = null; - } - else + // Get hosts + if (revision.getWiring() != null) { - Bundle[][] newReqBundles = new Bundle[m_reqBundles.length - 1][]; - System.arraycopy(m_reqBundles, 1, - newReqBundles, 0, m_reqBundles.length - 1); - m_reqBundles = newReqBundles; + List hostWires = revision.getWiring().getRequiredWires(null); + for (int i = 0; (hostWires != null) && (i < hostWires.size()); i++) + { + BundleWire wire = hostWires.get(i); + if (wire.getCapability().getNamespace().equals(BundleRevision.HOST_NAMESPACE)) + { + Bundle b = wire.getProviderWiring().getBundle(); + if (b != null) + { + list.add(b); + } + } + } } } + // Convert list to an array. + return (list.isEmpty()) + ? null + : (Bundle[]) list.toArray(new Bundle[list.size()]); } + return null; } - public boolean resolveBundles(Bundle[] bundles) + public RequiredBundle[] getRequiredBundles(String symbolicName) { - Object sm = System.getSecurityManager(); - - if (sm != null) + List list = new ArrayList(); + for (Bundle bundle : m_felix.getBundles()) { - ((SecurityManager) sm).checkPermission( - new AdminPermission(m_systemBundle, AdminPermission.RESOLVE)); + if ((symbolicName == null) + || (symbolicName.equals(bundle.getSymbolicName()))) + { + list.add(new RequiredBundleImpl(m_felix, (BundleImpl) bundle)); + } } - - return m_felix.resolveBundles(bundles); + return (list.isEmpty()) + ? null + : (RequiredBundle[]) list.toArray(new RequiredBundle[list.size()]); } - public RequiredBundle[] getRequiredBundles(String symbolicName) - { - // TODO: Implement PackageAdmin.getRequiredBundles() - return null; - } - - public Bundle[] getFragments(Bundle bundle) + /** + * The OSGi specification states that refreshing packages is + * asynchronous; this method simply notifies the package admin + * thread to do a refresh. + * @param bundles array of bundles to refresh or null to refresh + * any bundles in need of refreshing. + **/ + public void refreshPackages(Bundle[] bundles) + throws SecurityException { - // TODO: Implement PackageAdmin.getFragments() - return null; + List list = (bundles == null) + ? null + : Arrays.asList(bundles); + m_felix.adapt(FrameworkWiring.class).refreshBundles(list); } - public Bundle[] getHosts(Bundle bundle) + public boolean resolveBundles(Bundle[] bundles) { - // TODO: Implement PackageAdmin.getHosts() - return null; + List list = (bundles == null) + ? null + : Arrays.asList(bundles); + return m_felix.adapt(FrameworkWiring.class).resolveBundles(list); } } \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/RequiredBundleImpl.java b/framework/src/main/java/org/apache/felix/framework/RequiredBundleImpl.java new file mode 100644 index 00000000000..855223b07b1 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/RequiredBundleImpl.java @@ -0,0 +1,79 @@ +/* + * 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.framework; + +import java.util.Set; +import org.osgi.framework.Bundle; +import org.osgi.framework.Version; +import org.osgi.service.packageadmin.RequiredBundle; + +class RequiredBundleImpl implements RequiredBundle +{ + private final Felix m_felix; + private final BundleImpl m_bundle; + private volatile String m_toString = null; + private volatile String m_versionString = null; + + public RequiredBundleImpl(Felix felix, BundleImpl bundle) + { + m_felix = felix; + m_bundle = bundle; + } + + public String getSymbolicName() + { + return m_bundle.getSymbolicName(); + } + + public Bundle getBundle() + { + return m_bundle; + } + + public Bundle[] getRequiringBundles() + { + // If the package is stale, then return null per the spec. + if (m_bundle.isStale()) + { + return null; + } + Set set = m_felix.getRequiringBundles(m_bundle); + return set.toArray(new Bundle[set.size()]); + } + + public Version getVersion() + { + return m_bundle.getVersion(); + } + + public boolean isRemovalPending() + { + return m_bundle.isRemovalPending(); + } + + public String toString() + { + if (m_toString == null) + { + m_toString = m_bundle.getSymbolicName() + + "; version=" + m_bundle.getVersion(); + } + return m_toString; + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/ResolveContextImpl.java b/framework/src/main/java/org/apache/felix/framework/ResolveContextImpl.java new file mode 100644 index 00000000000..a1865ee6865 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/ResolveContextImpl.java @@ -0,0 +1,199 @@ +/* + * 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.framework; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.felix.framework.StatefulResolver.ResolverHookRecord; +import org.apache.felix.framework.resolver.CandidateComparator; +import org.apache.felix.framework.util.Util; +import org.osgi.framework.namespace.HostNamespace; +import org.osgi.framework.namespace.PackageNamespace; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; +import org.osgi.resource.Wire; +import org.osgi.resource.Wiring; +import org.osgi.service.resolver.HostedCapability; +import org.osgi.service.resolver.ResolveContext; + +/** + * + */ +public class ResolveContextImpl extends ResolveContext +{ + private final StatefulResolver m_state; + private final Map m_wirings; + private final ResolverHookRecord m_resolverHookrecord; + private final Collection m_mandatory; + private final Collection m_optional; + private final Collection m_ondemand; + + ResolveContextImpl( + StatefulResolver state, Map wirings, + ResolverHookRecord resolverHookRecord, Collection mandatory, + Collection optional, Collection ondemand) + { + m_state = state; + m_wirings = wirings; + m_resolverHookrecord = resolverHookRecord; + m_mandatory = mandatory; + m_optional = optional; + m_ondemand = ondemand; + } + + @Override + public Collection getMandatoryResources() + { + return new ArrayList(m_mandatory); + } + + @Override + public Collection getOptionalResources() + { + return new ArrayList(m_optional); + } + + public Collection getOndemandResources(Resource host) + { + List result = new ArrayList(); + for (BundleRevision revision : m_ondemand) + { + for (BundleRequirement req : revision.getDeclaredRequirements(null)) + { + if (req.getNamespace().equals(BundleRevision.HOST_NAMESPACE)) + { + for (Capability cap : host.getCapabilities(null)) + { + if (cap.getNamespace().equals(BundleRevision.HOST_NAMESPACE)) + { + if (req.matches((BundleCapability) cap)) + { + result.add(revision); + break; + } + } + } + } + } + } + return result; + } + + @Override + public List findProviders(Requirement br) + { + if (!(br instanceof BundleRequirement)) + throw new IllegalStateException("Expected a BundleRequirement"); + + List result = m_state.findProvidersInternal( + m_resolverHookrecord, br, true, true); + + // Casting the result to a List of Capability. + // TODO Can we do this without the strange double-cast? + @SuppressWarnings("unchecked") + List caps = + (List) (List) result; + return caps; + } + + @Override + public int insertHostedCapability(List caps, HostedCapability hc) + { + int idx = Collections.binarySearch(caps, hc, new CandidateComparator()); + if (idx < 0) + { + idx = Math.abs(idx + 1); + } + caps.add(idx, hc); + return idx; + } + + @Override + public boolean isEffective(Requirement br) + { + return m_state.isEffective(br); + } + + @Override + public Map getWirings() + { + return m_wirings; + } + + @Override + public List getSubstitutionWires(Wiring wiring) { + // TODO: this is calculating information that probably has been calculated + // already or at least could be calculated quicker taking into account the + // current state. We need to revisit this. + Set exportNames = new HashSet(); + for (Capability cap : wiring.getResource().getCapabilities(null)) + { + if (cap.getNamespace().equals(PackageNamespace.PACKAGE_NAMESPACE)) + { + exportNames.add( + (String) cap.getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE)); + } + } + // Add fragment exports + for (Wire wire : wiring.getProvidedResourceWires(HostNamespace.HOST_NAMESPACE)) + { + for (Capability cap : wire.getRequirement().getResource().getCapabilities(null)) + { + if (cap.getNamespace().equals(PackageNamespace.PACKAGE_NAMESPACE)) + { + exportNames.add((String) cap.getAttributes().get( + PackageNamespace.PACKAGE_NAMESPACE)); + } + } + } + List substitutionWires = new ArrayList(); + for (Wire wire : wiring.getRequiredResourceWires(null)) + { + if (wire.getCapability().getNamespace().equals(PackageNamespace.PACKAGE_NAMESPACE)) + { + if (exportNames.contains(wire.getCapability().getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE))) + { + substitutionWires.add(wire); + } + } + } + return substitutionWires; + } + + @Override + public Collection findRelatedResources(Resource resource) { + return !Util.isFragment(resource) ? getOndemandResources(resource) : Collections.emptyList(); + } + + @Override + public void onCancel(Runnable callback) { + // TODO: implement session cancel + super.onCancel(callback); + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/ServiceReferenceImpl.java b/framework/src/main/java/org/apache/felix/framework/ServiceReferenceImpl.java deleted file mode 100644 index c0249518833..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/ServiceReferenceImpl.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework; - -import org.apache.felix.framework.util.Util; -import org.apache.felix.moduleloader.IWire; -import org.osgi.framework.Bundle; -import org.osgi.framework.ServiceReference; - -class ServiceReferenceImpl implements ServiceReference -{ - private ServiceRegistrationImpl m_registration = null; - private Bundle m_bundle = null; - - public ServiceReferenceImpl(ServiceRegistrationImpl reg, Bundle bundle) - { - m_registration = reg; - m_bundle = bundle; - } - - protected ServiceRegistrationImpl getServiceRegistration() - { - return m_registration; - } - - public Object getProperty(String s) - { - return m_registration.getProperty(s); - } - - public String[] getPropertyKeys() - { - return m_registration.getPropertyKeys(); - } - - public Bundle getBundle() - { - // The spec says that this should return null if - // the service is unregistered. - return (m_registration.isValid()) ? m_bundle : null; - } - - public Bundle[] getUsingBundles() - { - return m_registration.getUsingBundles(); - } - - public boolean equals(Object obj) - { - try - { - ServiceReferenceImpl ref = (ServiceReferenceImpl) obj; - return ref.m_registration == m_registration; - } - catch (ClassCastException ex) - { - // Ignore and return false. - } - catch (NullPointerException ex) - { - // Ignore and return false. - } - - return false; - } - - public int hashCode() - { - if (m_registration.getReference() != null) - { - if (m_registration.getReference() != this) - { - return m_registration.getReference().hashCode(); - } - return super.hashCode(); - } - return 0; - } - - public String toString() - { - String[] ocs = (String[]) getProperty("objectClass"); - String oc = "["; - for(int i = 0; i < ocs.length; i++) - { - oc = oc + ocs[i]; - if (i < ocs.length - 1) - oc = oc + ", "; - } - oc = oc + "]"; - return oc; - } - - public boolean isAssignableTo(Bundle requester, String className) - { - // Always return true if the requester is the same as the provider. - if (requester == m_bundle) - { - return true; - } - - // Boolean flag. - boolean allow = true; - // Get the package. - String pkgName = - Util.getClassPackage(className); - // Get package wiring from service requester. - IWire requesterWire = Util.getWire( - ((BundleImpl) requester).getInfo().getCurrentModule(), pkgName); - // Get package wiring from service provider. - IWire providerWire = Util.getWire( - ((BundleImpl) m_bundle).getInfo().getCurrentModule(), pkgName); - - // There are three situations that may occur here: - // 1. The requester does not have a wire for the package. - // 2. The provider does not have a wire for the package. - // 3. Both have a wire for the package. - // For case 1, we do not filter the service reference since we - // assume that the bundle is using reflection or that it won't - // use that class at all since it does not import it. For - // case 2, we have to try to load the class from the class - // loader of the service object and then compare the class - // loaders to determine if we should filter the service - // refernce. In case 3, we simply compare the exporting - // modules from the package wiring to determine if we need - // to filter the service reference. - - // Case 1: Always include service reference. - if (requesterWire == null) - { - // This is an intentional no-op. - } - // Case 2: Only include service reference if the service - // object uses the same class as the requester. - else if (providerWire == null) - { - try - { - // Load the class from the requesting bundle. - Class requestClass = - ((BundleImpl) requester).getInfo().getCurrentModule().getClass(className); - // Get the service registration and ask it to check - // if the service object is assignable to the requesting - // bundle's class. - allow = getServiceRegistration().isClassAccessible(requestClass); - } - catch (Exception ex) - { - // This should not happen, filter to be safe. - allow = false; - } - } - // Case 3: Include service reference if the wires have the - // same source module. - else - { - allow = providerWire.getExporter().equals(requesterWire.getExporter()); - } - - return allow; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/ServiceRegistrationImpl.java b/framework/src/main/java/org/apache/felix/framework/ServiceRegistrationImpl.java index 7f0cb2e716c..91699000395 100644 --- a/framework/src/main/java/org/apache/felix/framework/ServiceRegistrationImpl.java +++ b/framework/src/main/java/org/apache/felix/framework/ServiceRegistrationImpl.java @@ -1,47 +1,75 @@ /* - * Copyright 2005 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework; import java.security.AccessController; +import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.felix.framework.util.MapToDictionary; import org.apache.felix.framework.util.StringMap; import org.apache.felix.framework.util.Util; -import org.osgi.framework.*; +import org.apache.felix.framework.wiring.BundleCapabilityImpl; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleReference; +import org.osgi.framework.Constants; +import org.osgi.framework.PrototypeServiceFactory; +import org.osgi.framework.ServiceException; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleWire; class ServiceRegistrationImpl implements ServiceRegistration { // Service registry. - private ServiceRegistry m_registry = null; - // Bundle implementing the service. - private Bundle m_bundle = null; + private final ServiceRegistry m_registry; + // Bundle providing the service. + private final Bundle m_bundle; // Interfaces associated with the service object. - private String[] m_classes = null; + private final String[] m_classes; // Service Id associated with the service object. - private Long m_serviceId = null; + private final Long m_serviceId; // Service object. - private Object m_svcObj = null; + private volatile Object m_svcObj; // Service factory interface. - private ServiceFactory m_factory = null; + private volatile ServiceFactory m_factory; // Associated property dictionary. - private Map m_propMap = null; + private volatile Map m_propMap = new StringMap(); // Re-usable service reference. - private ServiceReferenceImpl m_ref = null; + private final ServiceReferenceImpl m_ref; + // Flag indicating that we are unregistering. + private volatile boolean m_isUnregistering = false; + // This threadlocal is used to detect cycles. + private final ThreadLocal m_threadLoopDetection = new ThreadLocal(); + + private final Object syncObject = new Object(); public ServiceRegistrationImpl( ServiceRegistry registry, Bundle bundle, @@ -60,10 +88,7 @@ public ServiceRegistrationImpl( // This reference is the "standard" reference for this // service and will always be returned by getReference(). - // Since all reference to this service are supposed to - // be equal, we use the hashcode of this reference for - // a references to this service in ServiceReference. - m_ref = new ServiceReferenceImpl(this, m_bundle); + m_ref = new ServiceReferenceImpl(); } protected boolean isValid() @@ -71,12 +96,12 @@ protected boolean isValid() return (m_svcObj != null); } - public ServiceReference getReference() + protected synchronized void invalidate() { - return m_ref; + m_svcObj = null; } - public void setProperties(Dictionary dict) + public synchronized ServiceReference getReference() { // Make sure registration is valid. if (!isValid()) @@ -84,23 +109,44 @@ public void setProperties(Dictionary dict) throw new IllegalStateException( "The service registration is no longer valid."); } - // Set the properties. - initializeProperties(dict); + return m_ref; + } + + public void setProperties(Dictionary dict) + { + Map oldProps; + synchronized (this) + { + // Make sure registration is valid. + if (!isValid()) + { + throw new IllegalStateException( + "The service registration is no longer valid."); + } + // Remember old properties. + oldProps = m_propMap; + // Set the properties. + initializeProperties(dict); + } // Tell registry about it. - m_registry.servicePropertiesModified(this); + m_registry.servicePropertiesModified(this, new MapToDictionary(oldProps)); } public void unregister() { - if (m_svcObj != null) + synchronized (this) { - m_registry.unregisterService(m_bundle, this); - m_svcObj = null; - m_factory = null; + if (!isValid() || m_isUnregistering) + { + throw new IllegalStateException("Service already unregistered."); + } + m_isUnregistering = true; } - else + m_registry.unregisterService(m_bundle, this); + synchronized (this) { - throw new IllegalStateException("Service already unregistered."); + m_svcObj = null; + m_factory = null; } } @@ -115,84 +161,107 @@ public void unregister() * @return true if the specified class is reachable from the * service object's class loader, false otherwise. **/ - protected boolean isClassAccessible(Class clazz) + private boolean isClassAccessible(Class clazz) { - try + // We need to see if the class loader of the service object has + // access to the specified class; however, we may not have a service + // object. If we only have service factory, then we will assume two + // different scenarios: + // 1. The service factory is provided by the bundle providing the + // service. + // 2. The service factory is NOT provided by the bundle providing + // the service. + // For case 1, we will use the class loader of the service factory + // to find the class. For case 2, we will assume we have an extender + // at work here and always return true, since we have no real way of + // knowing the wiring of the provider unless we actually get the + // service object, which defeats the lazy aspect of service factories. + + // Case 2. + if ((m_factory != null) + && (Felix.m_secureAction.getClassLoader(m_factory.getClass()) instanceof BundleReference) + && !((BundleReference) Felix.m_secureAction.getClassLoader(m_factory.getClass())).getBundle().equals(m_bundle)) { - // First, try to load the class from the bundle that registered - // the service. - Class targetClass = ((BundleImpl) m_bundle) - .getInfo().getCurrentModule().getClass(clazz.getName()); - if (targetClass != null) + try { - return (targetClass == clazz); + Class providedClazz = m_bundle.loadClass(clazz.getName()); + if (providedClazz != null) + { + return providedClazz == clazz; + } } - - // If it cannot be found from the registering bundle, then try to load - // from the service object or service factory class. - Class sourceClass = (m_factory != null) - ? m_factory.getClass() : m_svcObj.getClass(); - targetClass = Util.loadClassUsingClass(sourceClass, clazz.getName()); - return (targetClass == clazz); - } - catch (Exception ex) - { - // Ignore this and return false. + catch (ClassNotFoundException ex) + { + // Ignore and try interface class loaders. + } + return true; } - return false; + + // Case 1. + Class sourceClass = (m_factory != null) ? m_factory.getClass() : m_svcObj.getClass(); + return Util.loadClassUsingClass(sourceClass, clazz.getName(), Felix.m_secureAction) == clazz; } - protected Object getProperty(String key) + Object getProperty(String key) { return m_propMap.get(key); } - private transient ArrayList m_list = new ArrayList(); - - protected String[] getPropertyKeys() + private String[] getPropertyKeys() { - synchronized (m_list) - { - m_list.clear(); - Iterator i = m_propMap.entrySet().iterator(); - while (i.hasNext()) - { - Map.Entry entry = (Map.Entry) i.next(); - m_list.add(entry.getKey()); - } - return (String[]) m_list.toArray(new String[m_list.size()]); - } + Set s = m_propMap.keySet(); + return (String[]) s.toArray(new String[s.size()]); } - protected Bundle[] getUsingBundles() + private Bundle[] getUsingBundles() { return m_registry.getUsingBundles(m_ref); } - protected Object getService(Bundle acqBundle) + /** + * This method provides direct access to the associated service object; + * it generally should not be used by anyone other than the service registry + * itself. + * @return The service object associated with the registration. + **/ + Object getService() + { + return m_svcObj; + } + + Object getService(Bundle acqBundle) { // If the service object is a service factory, then // let it create the service object. if (m_factory != null) { + Object svcObj = null; try { if (System.getSecurityManager() != null) { - return AccessController.doPrivileged( + svcObj = AccessController.doPrivileged( new ServiceFactoryPrivileged(acqBundle, null)); } else { - return getFactoryUnchecked(acqBundle); + svcObj = getFactoryUnchecked(acqBundle); } } - catch (Exception ex) + catch (PrivilegedActionException ex) { - m_registry.getLogger().log( - Logger.LOG_ERROR, "ServiceRegistrationImpl: Error getting service.", ex); - return null; + if (ex.getException() instanceof ServiceException) + { + throw (ServiceException) ex.getException(); + } + else + { + throw new ServiceException( + "Service factory exception: " + ex.getException().getMessage(), + ServiceException.FACTORY_EXCEPTION, ex.getException()); + } } + return svcObj; } else { @@ -200,10 +269,10 @@ protected Object getService(Bundle acqBundle) } } - protected void ungetService(Bundle relBundle, Object svcObj) + void ungetService(Bundle relBundle, Object svcObj) { // If the service object is a service factory, then - // let is release the service object. + // let it release the service object. if (m_factory != null) { try @@ -218,44 +287,101 @@ protected void ungetService(Bundle relBundle, Object svcObj) ungetFactoryUnchecked(relBundle, svcObj); } } - catch (Exception ex) + catch (Throwable ex) { m_registry.getLogger().log( - Logger.LOG_ERROR, "ServiceRegistrationImpl: Error ungetting service.", ex); + m_bundle, + Logger.LOG_ERROR, + "ServiceRegistrationImpl: Error ungetting service.", + ex); } } } - private void initializeProperties(Dictionary dict) + private void initializeProperties(Dictionary dict) { - // Create a case insensitive map. - if (m_propMap == null) - { - m_propMap = new StringMap(false); - } - else - { - m_propMap.clear(); - } + // Create a case-insensitive map for the properties. + Map props = new StringMap(); if (dict != null) { - Enumeration keys = dict.keys(); + // Make sure there are no duplicate keys. + Enumeration keys = dict.keys(); while (keys.hasMoreElements()) { - Object key = keys.nextElement(); - m_propMap.put(key, dict.get(key)); + String key = keys.nextElement(); + if (props.get(key) == null) + { + props.put(key, dict.get(key)); + } + else + { + throw new IllegalArgumentException("Duplicate service property: " + key); + } } } // Add the framework assigned properties. - m_propMap.put(Constants.OBJECTCLASS, m_classes); - m_propMap.put(Constants.SERVICE_ID, m_serviceId); + props.put(Constants.OBJECTCLASS, m_classes); + props.put(Constants.SERVICE_ID, m_serviceId); + props.put(Constants.SERVICE_BUNDLEID, m_bundle.getBundleId()); + if ( m_factory != null ) + { + props.put(Constants.SERVICE_SCOPE, + (m_factory instanceof PrototypeServiceFactory + ? Constants.SCOPE_PROTOTYPE : Constants.SCOPE_BUNDLE)); + } + else + { + props.put(Constants.SERVICE_SCOPE, Constants.SCOPE_SINGLETON); + } + + // Update the service property map. + m_propMap = props; } private Object getFactoryUnchecked(Bundle bundle) { - return m_factory.getService(bundle, this); + Object svcObj = null; + try + { + svcObj = m_factory.getService(bundle, this); + } + catch (Throwable th) + { + throw new ServiceException( + "Service factory exception: " + th.getMessage(), + ServiceException.FACTORY_EXCEPTION, th); + } + if (svcObj != null) + { + for (int i = 0; i < m_classes.length; i++) + { + Class clazz = Util.loadClassUsingClass( + svcObj.getClass(), m_classes[i], Felix.m_secureAction); + if ((clazz == null) || !clazz.isAssignableFrom(svcObj.getClass())) + { + if (clazz == null) + { + throw new ServiceException( + "Service cannot be cast due to missing class: " + m_classes[i], + ServiceException.FACTORY_ERROR); + } + else + { + throw new ServiceException( + "Service cannot be cast: " + m_classes[i], + ServiceException.FACTORY_ERROR); + } + } + } + } + else + { + throw new ServiceException( + "Service factory returned null. (" + m_factory + ")", ServiceException.FACTORY_ERROR); + } + return svcObj; } private void ungetFactoryUnchecked(Bundle bundle, Object svcObj) @@ -292,4 +418,364 @@ public Object run() throws Exception return null; } } + + // + // ServiceReference implementation + // + + class ServiceReferenceImpl extends BundleCapabilityImpl implements ServiceReference + { + private final ServiceReferenceMap m_map; + + private ServiceReferenceImpl() + { + super(null, null, Collections.EMPTY_MAP, Collections.EMPTY_MAP); + m_map = new ServiceReferenceMap(); + } + + ServiceRegistrationImpl getRegistration() + { + return ServiceRegistrationImpl.this; + } + + // + // Capability methods. + // + + @Override + public BundleRevision getRevision() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String getNamespace() + { + return "service-reference"; + } + + @Override + public Map getDirectives() + { + return Collections.EMPTY_MAP; + } + + @Override + public Map getAttributes() + { + return m_map; + } + + @Override + public List getUses() + { + return Collections.EMPTY_LIST; + } + + // + // ServiceReference methods. + // + + public Object getProperty(String s) + { + return ServiceRegistrationImpl.this.getProperty(s); + } + + public String[] getPropertyKeys() + { + return ServiceRegistrationImpl.this.getPropertyKeys(); + } + + public Bundle getBundle() + { + // The spec says that this should return null if + // the service is unregistered. + return (isValid()) ? m_bundle : null; + } + + public Bundle[] getUsingBundles() + { + return ServiceRegistrationImpl.this.getUsingBundles(); + } + + @Override + public String toString() + { + String[] ocs = (String[]) getProperty("objectClass"); + String oc = "["; + for(int i = 0; i < ocs.length; i++) + { + oc = oc + ocs[i]; + if (i < ocs.length - 1) + oc = oc + ", "; + } + oc = oc + "]"; + return oc; + } + + public boolean isAssignableTo(Bundle requester, String className) + { + // Always return true if the requester is the same as the provider. + if (requester == m_bundle) + { + return true; + } + + // Boolean flag. + boolean allow = true; + // Get the package. + String pkgName = + Util.getClassPackage(className); + // Get package wiring from service requester. + BundleRevision requesterRevision = requester.adapt(BundleRevision.class); + BundleWire requesterWire = Util.getWire(requesterRevision, pkgName); + BundleCapability requesterCap = Util.getPackageCapability(requesterRevision, pkgName); + // Get package wiring from service provider. + BundleRevision providerRevision = m_bundle.adapt(BundleRevision.class); + BundleWire providerWire = Util.getWire(providerRevision, pkgName); + BundleCapability providerCap = Util.getPackageCapability(providerRevision, pkgName); + + // There are four situations that may occur here: + // 1. Neither the requester, nor provider have wires for the package. + // 2. The requester does not have a wire for the package. + // 3. The provider does not have a wire for the package. + // 4. Both the requester and provider have a wire for the package. + // For case 1, if the requester does not have access to the class at + // all, we assume it is using reflection and do not filter. If the + // requester does have access to the class, then we make sure it is + // the same class as the service. For case 2, we do not filter if the + // requester is the exporter of the package to which the provider of + // the service is wired. Otherwise, as in case 1, if the requester + // does not have access to the class at all, we do not filter, but if + // it does have access we check if it is the same class accessible to + // the providing revision. For case 3, the provider will not have a wire + // if it is exporting the package, so we determine if the requester + // is wired to it or somehow using the same class. For case 4, we + // simply compare the exporting revisions from the package wiring to + // determine if we need to filter the service reference. + + // Case 1: Both requester and provider have no wire. + if ((requesterWire == null) && (providerWire == null)) + { + // If requester has no access then true, otherwise service + // registration must have same class as requester. + try + { + Class requestClass = + ((BundleWiringImpl) requesterRevision.getWiring()) + .getClassByDelegation(className); + allow = getRegistration().isClassAccessible(requestClass); + } + catch (Exception ex) + { + // Requester has no access to the class, so allow it, since + // we assume the requester is using reflection. + allow = true; + } + } + // Case 2: Requester has no wire, but provider does. + else if ((requesterWire == null) && (providerWire != null)) + { + // If the requester exports the package, then the provider must + // be wired to it. + if (requesterCap != null) + { + allow = providerWire.getProviderWiring().getRevision().equals(requesterRevision); + } + // Otherwise, check if the requester has access to the class and, + // if so, if it is the same class as the provider. + else + { + try + { + // Try to load class from requester. + Class requestClass =((BundleWiringImpl) + requesterRevision.getWiring()).getClassByDelegation(className); + try + { + // If requester has access to the class, verify it is the + // same class as the provider. + allow = (((BundleWiringImpl) + providerRevision.getWiring()) + .getClassByDelegation(className) == requestClass); + } + catch (Exception ex) + { + allow = false; + } + } + catch (Exception ex) + { + // Requester has no access to the class, so allow it, since + // we assume the requester is using reflection. + allow = true; + } + } + } + // Case 3: Requester has a wire, but provider does not. + else if ((requesterWire != null) && (providerWire == null)) + { + // If the provider exports the package, then the requester must + // be wired to it. + if (providerCap != null) + { + allow = requesterWire.getProviderWiring().getRevision().equals(providerRevision); + } + // If the provider is not the exporter of the requester's package, + // then try to use the service registration to see if the requester's + // class is accessible. + else + { + try + { + // Load the class from the requesting bundle. + Class requestClass = ((BundleWiringImpl) + requesterRevision.getWiring()) + .getClassByDelegation(className); + // Get the service registration and ask it to check + // if the service object is assignable to the requesting + // bundle's class. + allow = getRegistration().isClassAccessible(requestClass); + } + catch (Exception ex) + { + // Filter to be safe. + allow = false; + } + } + } + // Case 4: Both requester and provider have a wire. + else + { + // Include service reference if the wires have the + // same source revision. + allow = providerWire.getProviderWiring().getRevision() + .equals(requesterWire.getProviderWiring().getRevision()); + } + + return allow; + } + + public int compareTo(Object reference) + { + ServiceReference other = (ServiceReference) reference; + + Long id = (Long) getProperty(Constants.SERVICE_ID); + Long otherId = (Long) other.getProperty(Constants.SERVICE_ID); + + if (id.equals(otherId)) + { + return 0; // same service + } + + Object rankObj = getProperty(Constants.SERVICE_RANKING); + Object otherRankObj = other.getProperty(Constants.SERVICE_RANKING); + + // If no rank, then spec says it defaults to zero. + rankObj = (rankObj == null) ? new Integer(0) : rankObj; + otherRankObj = (otherRankObj == null) ? new Integer(0) : otherRankObj; + + // If rank is not Integer, then spec says it defaults to zero. + Integer rank = (rankObj instanceof Integer) + ? (Integer) rankObj : new Integer(0); + Integer otherRank = (otherRankObj instanceof Integer) + ? (Integer) otherRankObj : new Integer(0); + + // Sort by rank in ascending order. + if (rank.compareTo(otherRank) < 0) + { + return -1; // lower rank + } + else if (rank.compareTo(otherRank) > 0) + { + return 1; // higher rank + } + + // If ranks are equal, then sort by service id in descending order. + return (id.compareTo(otherId) < 0) ? 1 : -1; + } + + @Override + public Dictionary getProperties() { + return new Hashtable(ServiceRegistrationImpl.this.m_propMap); + } + } + + private class ServiceReferenceMap implements Map + { + public int size() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public boolean isEmpty() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public boolean containsKey(Object o) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public boolean containsValue(Object o) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public Object get(Object o) + { + return ServiceRegistrationImpl.this.getProperty((String) o); + } + + public Object put(Object k, Object v) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public Object remove(Object o) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void putAll(Map map) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void clear() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public Set keySet() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public Collection values() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public Set> entrySet() + { + return Collections.EMPTY_SET; + } + } + + boolean currentThreadMarked() + { + return m_threadLoopDetection.get() != null; + } + + void markCurrentThread() + { + m_threadLoopDetection.set(Boolean.TRUE); + } + + void unmarkCurrentThread() + { + m_threadLoopDetection.set(null); + } } \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/ServiceRegistry.java b/framework/src/main/java/org/apache/felix/framework/ServiceRegistry.java index e60e9ada0cf..8c94ca8eeba 100644 --- a/framework/src/main/java/org/apache/felix/framework/ServiceRegistry.java +++ b/framework/src/main/java/org/apache/felix/framework/ServiceRegistry.java @@ -1,96 +1,206 @@ /* - * Copyright 2005 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework; -import java.util.*; - -import org.apache.felix.framework.util.FelixConstants; -import org.osgi.framework.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.felix.framework.capabilityset.CapabilitySet; +import org.apache.felix.framework.capabilityset.SimpleFilter; +import org.apache.felix.framework.wiring.BundleCapabilityImpl; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceException; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.resource.Capability; public class ServiceRegistry { - private Logger m_logger = null; - private long m_currentServiceId = 1L; + private final Logger m_logger; + + /** Counter for the service id */ + private final AtomicLong m_currentServiceId = new AtomicLong(1); + // Maps bundle to an array of service registrations. - private Map m_serviceRegsMap = new HashMap(); + private final ConcurrentMap>> m_regsMap = new ConcurrentHashMap>>(); + + // Capability set for all service registrations. + private final CapabilitySet m_regCapSet = new CapabilitySet(Collections.singletonList(Constants.OBJECTCLASS), false); + // Maps bundle to an array of usage counts. - private Map m_inUseMap = new HashMap(); + private final ConcurrentMap m_inUseMap = new ConcurrentHashMap(); - private ServiceListener m_serviceListener = null; + private final ServiceRegistryCallbacks m_callbacks; - public ServiceRegistry(Logger logger) + private final HookRegistry hookRegistry = new HookRegistry(); + + public ServiceRegistry(final Logger logger, final ServiceRegistryCallbacks callbacks) { m_logger = logger; + m_callbacks = callbacks; } - public synchronized ServiceReference[] getRegisteredServices(Bundle bundle) + /** + * Get all service references for a bundle + * @param bundle + * @return List with all valid service references or {@code null}. + */ + public ServiceReference[] getRegisteredServices(final Bundle bundle) { - ServiceRegistration[] regs = (ServiceRegistration[]) m_serviceRegsMap.get(bundle); + final List> regs = m_regsMap.get(bundle); if (regs != null) { - ServiceReference[] refs = new ServiceReference[regs.length]; - for (int i = 0; i < refs.length; i++) + final List> refs = new ArrayList>(regs.size()); + // this is a per bundle list, therefore synchronizing this should be fine + synchronized ( regs ) { - refs[i] = regs[i].getReference(); + for (final ServiceRegistration reg : regs) + { + try + { + refs.add(reg.getReference()); + } + catch (final IllegalStateException ex) + { + // Don't include the reference as it is not valid anymore + } + } } - return refs; + return refs.toArray(new ServiceReference[refs.size()]); } return null; } - public ServiceRegistration registerService( - Bundle bundle, String[] classNames, Object svcObj, Dictionary dict) + /** + * Register a new service + * + * Caller must fire service event as this method is not doing it! + * + * @param bundle The bundle registering the service + * @param classNames The service class names + * @param svcObj The service object + * @param dict Optional service properties + * @return Service registration + */ + public ServiceRegistration registerService( + final Bundle bundle, + final String[] classNames, + final Object svcObj, + final Dictionary dict) { - ServiceRegistration reg = null; + // Create the service registration. + final ServiceRegistrationImpl reg = new ServiceRegistrationImpl( + this, bundle, classNames, m_currentServiceId.getAndIncrement(), svcObj, dict); - synchronized (this) + // Keep track of registered hooks. + this.hookRegistry.addHooks(classNames, svcObj, reg.getReference()); + + // Get the bundles current registered services. + final List> newRegs = new ArrayList>(); + List> regs = m_regsMap.putIfAbsent(bundle, newRegs); + if (regs == null) { - // Create the service registration. - reg = new ServiceRegistrationImpl( - this, bundle, classNames, new Long(m_currentServiceId++), svcObj, dict); - // Get the bundles current registered services. - ServiceRegistration[] regs = (ServiceRegistration[]) m_serviceRegsMap.get(bundle); - m_serviceRegsMap.put(bundle, addServiceRegistration(regs, reg)); + regs = newRegs; } - fireServiceChanged(new ServiceEvent(ServiceEvent.REGISTERED, reg.getReference())); + // this is a per bundle list, therefore synchronizing this should be fine + synchronized ( regs ) + { + regs.add(reg); + } + m_regCapSet.addCapability((BundleCapabilityImpl) reg.getReference()); + return reg; } - public void unregisterService(Bundle bundle, ServiceRegistration reg) + /** + * Unregister a service + * @param bundle The bundle unregistering the service + * @param reg The service registration + */ + public void unregisterService( + final Bundle bundle, + final ServiceRegistration reg) { - // First remove the registered service. - synchronized (this) + // If this is a hook, it should be removed. + this.hookRegistry.removeHooks(reg.getReference()); + + // Now remove the registered service. + final List> regs = m_regsMap.get(bundle); + if (regs != null) { - ServiceRegistration[] regs = (ServiceRegistration[]) m_serviceRegsMap.get(bundle); - m_serviceRegsMap.put(bundle, removeServiceRegistration(regs, reg)); + // this is a per bundle list, therefore synchronizing this should be fine + synchronized ( regs ) + { + regs.remove(reg); + } } + m_regCapSet.removeCapability((BundleCapabilityImpl) reg.getReference()); - // Fire the service event which gives all client bundles the - // opportunity to unget their service object. - fireServiceChanged(new ServiceEvent(ServiceEvent.UNREGISTERING, reg.getReference())); + // Notify callback objects about unregistering service. + if (m_callbacks != null) + { + m_callbacks.serviceChanged( + new ServiceEvent(ServiceEvent.UNREGISTERING, reg.getReference()), null); + } // Now forcibly unget the service object for all stubborn clients. - synchronized (this) + final ServiceReference ref = reg.getReference(); + ungetServices(ref); + + // Invalidate registration + ((ServiceRegistrationImpl) reg).invalidate(); + + // Bundles are allowed to get a reference while unregistering + // get fresh set of bundles (should be empty, but this is a sanity check) + ungetServices(ref); + + // Now flush all usage counts as the registration is invalid + for (Bundle usingBundle : m_inUseMap.keySet()) { - Bundle[] clients = getUsingBundles(reg.getReference()); - for (int i = 0; (clients != null) && (i < clients.length); i++) + flushUsageCount(usingBundle, ref, null); + } + } + + private void ungetServices(final ServiceReference ref) + { + final Bundle[] clients = getUsingBundles(ref); + for (int i = 0; (clients != null) && (i < clients.length); i++) + { + final UsageCount[] usages = m_inUseMap.get(clients[i]); + for (int x = 0; (usages != null) && (x < usages.length); x++) { - while (ungetService(clients[i], reg.getReference())) - ; // Keep removing until it is no longer possible + if (usages[x].m_ref.equals(ref)) + { + ungetService(clients[i], ref, (usages[x].m_prototype ? usages[x].getService() : null)); + } } } } @@ -101,89 +211,75 @@ public void unregisterService(Bundle bundle, ServiceRegistration reg) * one. This method is only called be the framework to clean up after * a stopped bundle. * @param bundle the bundle whose services should be unregistered. - **/ - public void unregisterServices(Bundle bundle) + **/ + public void unregisterServices(final Bundle bundle) { // Simply remove all service registrations for the bundle. - ServiceRegistration[] regs = null; - synchronized (this) - { - regs = (ServiceRegistration[]) m_serviceRegsMap.get(bundle); - } + final List> regs = m_regsMap.remove(bundle); - // Unregister each service. - for (int i = 0; (regs != null) && (i < regs.length); i++) - { - regs[i].unregister(); - } + // Note, there is no race condition here with respect to the + // bundle registering more services, because its bundle context + // has already been invalidated by this point, so it would not + // be able to register more services. - // Now remove the bundle itself. - m_serviceRegsMap.remove(bundle); - } - - public synchronized List getServiceReferences(String className, Filter filter) - throws InvalidSyntaxException - { - // Create a filtered list of service references. - List list = new ArrayList(); - - // Iterator over all service registrations. - for (Iterator i = m_serviceRegsMap.entrySet().iterator(); i.hasNext(); ) + // Unregister each service. + if (regs != null) { - Map.Entry entry = (Map.Entry) i.next(); - ServiceRegistration[] regs = (ServiceRegistration[]) entry.getValue(); - - for (int regIdx = 0; - (regs != null) && (regIdx < regs.length); - regIdx++) + final List> copyRefs; + // there shouldn't be a need to sync, but just to be safe + // we create a copy array and use that for iterating + synchronized ( regs ) { - // Determine if the registered services matches - // the search criteria. - boolean matched = false; - - // If className is null, then look at filter only. - if ((className == null) && - ((filter == null) || filter.match(regs[regIdx].getReference()))) - { - matched = true; - } - // If className is not null, then first match the - // objectClass property before looking at the - // filter. - else if (className != null) + copyRefs = new ArrayList>(regs); + } + for (final ServiceRegistration reg : copyRefs) + { + if (((ServiceRegistrationImpl) reg).isValid()) { - String[] objectClass = (String[]) - ((ServiceRegistrationImpl) regs[regIdx]).getProperty(FelixConstants.OBJECTCLASS); - for (int classIdx = 0; - classIdx < objectClass.length; - classIdx++) + try { - if (objectClass[classIdx].equals(className) && - ((filter == null) || filter.match(regs[regIdx].getReference()))) - { - matched = true; - break; - } + reg.unregister(); + } + catch (final IllegalStateException e) + { + // Ignore exception if the service has already been unregistered } - } - - // Add reference if it was a match. - if (matched) - { - list.add(regs[regIdx].getReference()); } } } + } - return list; + public Collection getServiceReferences(final String className, SimpleFilter filter) + { + if ((className == null) && (filter == null)) + { + // Return all services. + filter = new SimpleFilter(null, null, SimpleFilter.MATCH_ALL); + } + else if ((className != null) && (filter == null)) + { + // Return services matching the class name. + filter = new SimpleFilter(Constants.OBJECTCLASS, className, SimpleFilter.EQ); + } + else if ((className != null) && (filter != null)) + { + // Return services matching the class name and filter. + final List filters = new ArrayList(2); + filters.add(new SimpleFilter(Constants.OBJECTCLASS, className, SimpleFilter.EQ)); + filters.add(filter); + filter = new SimpleFilter(null, filters, SimpleFilter.AND); + } + // else just use the specified filter. + + return m_regCapSet.match(filter, false); } - public synchronized ServiceReference[] getServicesInUse(Bundle bundle) + public ServiceReference[] getServicesInUse(final Bundle bundle) { - UsageCount[] usages = (UsageCount[]) m_inUseMap.get(bundle); + final UsageCount[] usages = m_inUseMap.get(bundle); if (usages != null) { - ServiceReference[] refs = new ServiceReference[usages.length]; + final ServiceReference[] refs = new ServiceReference[usages.length]; for (int i = 0; i < refs.length; i++) { refs[i] = usages[i].m_ref; @@ -193,101 +289,255 @@ public synchronized ServiceReference[] getServicesInUse(Bundle bundle) return null; } - public synchronized Object getService(Bundle bundle, ServiceReference ref) + @SuppressWarnings("unchecked") + public S getService(final Bundle bundle, final ServiceReference ref, final boolean isServiceObjects) { - // Get usage counts for specified bundle. - UsageCount[] usages = (UsageCount[]) m_inUseMap.get(bundle); + // prototype scope is only possible if called from ServiceObjects + final boolean isPrototype = isServiceObjects && ref.getProperty(Constants.SERVICE_SCOPE) == Constants.SCOPE_PROTOTYPE; + UsageCount usage = null; + Object svcObj = null; + + // Get the service registration. + final ServiceRegistrationImpl reg = + ((ServiceRegistrationImpl.ServiceReferenceImpl) ref).getRegistration(); - // Make sure the service registration is still valid. - if (!((ServiceReferenceImpl) ref).getServiceRegistration().isValid()) + // We don't allow cycles when we call out to the service factory. + if ( reg.currentThreadMarked() ) { - // If the service registration is not valid, then this means - // that the service provider unregistered the service. The spec - // says that calls to get an unregistered service should always - // return null (assumption: even if it is currently cached - // by the bundle). So in this case, flush the service reference - // from the cache and return null. - m_inUseMap.put(bundle, removeUsageCount(usages, ref)); - - // It is not necessary to unget the service object from - // the providing bundle, since the associated service is - // unregistered and hence not in the list of registered services - // of the providing bundle. This is precisely why the service - // registration was not found above in the first place. - return null; + throw new ServiceException( + "ServiceFactory.getService() resulted in a cycle.", + ServiceException.FACTORY_ERROR, + null); } - // Get the service registration. - ServiceRegistrationImpl reg = ((ServiceReferenceImpl) ref).getServiceRegistration(); - - // Get the usage count, if any. - UsageCount usage = getUsageCount(usages, ref); - - // If the service object is cached, then increase the usage - // count and return the cached service object. - Object svcObj = null; - if (usage != null) + try { - usage.m_count++; - svcObj = usage.m_svcObj; + reg.markCurrentThread(); + + // Make sure the service registration is still valid. + if (reg.isValid()) + { + // Get the usage count, or create a new one. If this is a + // prototype, the we'll alway create a new one. + usage = obtainUsageCount(bundle, ref, null, isPrototype); + + // Increment the usage count and grab the already retrieved + // service object, if one exists. + incrementToPositiveValue(usage.m_count); + svcObj = usage.getService(); + + if ( isServiceObjects ) + { + incrementToPositiveValue(usage.m_serviceObjectsCount); + } + + // If we have a usage count, but no service object, then we haven't + // cached the service object yet, so we need to create one. + if (usage != null) + { + ServiceHolder holder = null; + + // There is a possibility that the holder is unset between the compareAndSet() and the get() + // below. If that happens get() returns null and we may have to set a new holder. This is + // why the below section is in a loop. + while (holder == null) + { + ServiceHolder h = new ServiceHolder(); + if (usage.m_svcHolderRef.compareAndSet(null, h)) + { + holder = h; + try { + svcObj = reg.getService(bundle); + holder.m_service = svcObj; + } finally { + holder.m_latch.countDown(); + } + } + else + { + holder = usage.m_svcHolderRef.get(); + if (holder != null) + { + boolean interrupted = false; + do + { + try + { + // Need to ensure that the other thread has obtained + // the service. + holder.m_latch.await(); + if (interrupted) + { + Thread.currentThread().interrupt(); + } + interrupted = false; + } + catch (InterruptedException e) + { + interrupted = true; + Thread.interrupted(); + } + } + while (interrupted); + svcObj = holder.m_service; + } + } + + // if someone concurrently changed the holder, loop again + if (holder != usage.m_svcHolderRef.get()) + holder = null; + } + if (svcObj != null && isPrototype) + { + UsageCount existingUsage = obtainUsageCount(bundle, ref, svcObj, null); + if (existingUsage != null && existingUsage != usage) + { + flushUsageCount(bundle, ref, usage); + usage = existingUsage; + incrementToPositiveValue(usage.m_count); + if ( isServiceObjects ) + { + incrementToPositiveValue(usage.m_serviceObjectsCount); + } + } + } + } + } } - else + finally { - // Get service object from service registration. - svcObj = reg.getService(bundle); + reg.unmarkCurrentThread(); - // Cache the service object. - if (svcObj != null) + if (!reg.isValid() || (svcObj == null)) { - m_inUseMap.put(bundle, addUsageCount(usages, ref, svcObj)); + flushUsageCount(bundle, ref, usage); } } - return svcObj; + return (S) svcObj; } - public synchronized boolean ungetService(Bundle bundle, ServiceReference ref) + // Increment the Atomic Long by 1, and ensure the result is at least 1. + // This method uses a loop, optimistic algorithm to do this in a threadsafe + // way without locks. + private void incrementToPositiveValue(AtomicLong al) { - // Get usage count. - UsageCount[] usages = (UsageCount[]) m_inUseMap.get(bundle); - UsageCount usage = getUsageCount(usages, ref); + boolean success = false; - // If no usage count, then return. - if (usage == null) + while (!success) { - return false; + long oldVal = al.get(); + long newVal = Math.max(oldVal + 1L, 1L); + checkCountOverflow(newVal); + + success = al.compareAndSet(oldVal, newVal); } + } - // Make sure the service registration is still valid. - if (!((ServiceReferenceImpl) ref).getServiceRegistration().isValid()) + private void checkCountOverflow(long c) + { + if (c == Long.MAX_VALUE) { - // If the service registration is not valid, then this means - // that the service provider unregistered the service. The spec - // says that calls to get an unregistered service should always - // return null (assumption: even if it is currently cached - // by the bundle). So in this case, flush the service reference - // from the cache and return null. - m_inUseMap.put(bundle, removeUsageCount(usages, ref)); - return false; + throw new ServiceException( + "The use count for the service overflowed.", + ServiceException.UNSPECIFIED, + null); } + } - // Decrement usage count. - usage.m_count--; + public boolean ungetService(final Bundle bundle, final ServiceReference ref, final Object svcObj) + { + final ServiceRegistrationImpl reg = + ((ServiceRegistrationImpl.ServiceReferenceImpl) ref).getRegistration(); - // Remove reference when usage count goes to zero - // and unget the service object from the exporting - // bundle. - if (usage.m_count == 0) + if ( reg.currentThreadMarked() ) { - m_inUseMap.put(bundle, removeUsageCount(usages, ref)); - ServiceRegistrationImpl reg = - ((ServiceReferenceImpl) ref).getServiceRegistration(); - reg.ungetService(bundle, usage.m_svcObj); - usage.m_svcObj = null; + throw new IllegalStateException( + "ServiceFactory.ungetService() resulted in a cycle."); } - // Return true if the usage count is greater than zero. - return (usage.m_count > 0); + try + { + // Mark the current thread to avoid cycles + reg.markCurrentThread(); + + // Get the usage count. + UsageCount usage = obtainUsageCount(bundle, ref, svcObj, null); + // If there are no cached services, then just return immediately. + if (usage == null) + { + return false; + } + // if this is a call from service objects and the service was not fetched from + // there, return false + if ( svcObj != null ) + { + if (usage.m_serviceObjectsCount.decrementAndGet() < 0) + { + return false; + } + } + + // If usage count will go to zero, then unget the service + // from the registration. + long count = usage.m_count.decrementAndGet(); + try + { + if (count <= 0) + { + ServiceHolder holder = usage.m_svcHolderRef.get(); + Object svc = holder != null ? holder.m_service : null; + + if (svc != null) + { + // Check the count again to ensure that nobody else has just + // obtained the service again + if (usage.m_count.get() <= 0) + { + if (usage.m_svcHolderRef.compareAndSet(holder, null)) + { + // Temporarily increase the usage again so that the + // service factory still sees the usage in the unget + usage.m_count.incrementAndGet(); + try + { + // Remove reference from usages array. + reg.ungetService(bundle, svc); + } + finally + { + // now we can decrease the usage again + usage.m_count.decrementAndGet(); + } + + } + } + } + } + + return count >= 0; + } + finally + { + if (!reg.isValid()) + { + usage.m_svcHolderRef.set(null); + } + + // If the registration is invalid or the usage count for a prototype + // reached zero, then flush it. Non-prototype services are not flushed + // on ungetService() when they reach 0 as this introduces a race + // condition of concurrently the same service is obtained via getService() + if (!reg.isValid() || (count <= 0 && svcObj != null)) + { + flushUsageCount(bundle, ref, usage); + } + } + } + finally + { + reg.unmarkCurrentThread(); + } } @@ -296,37 +546,45 @@ public synchronized boolean ungetService(Bundle bundle, ServiceReference ref) * used by the specified bundle. * @param bundle the bundle whose services are to be released. **/ - public synchronized void ungetServices(Bundle bundle) + public void ungetServices(final Bundle bundle) { - UsageCount[] usages = (UsageCount[]) m_inUseMap.get(bundle); + UsageCount[] usages = m_inUseMap.get(bundle); if (usages == null) { return; } + // Note, there is no race condition here with respect to the + // bundle using more services, because its bundle context + // has already been invalidated by this point, so it would not + // be able to look up more services. + // Remove each service object from the // service cache. for (int i = 0; i < usages.length; i++) { + if (usages[i].m_svcHolderRef.get() == null) + continue; + // Keep ungetting until all usage count is zero. - while (ungetService(bundle, usages[i].m_ref)) + while (ungetService(bundle, usages[i].m_ref, usages[i].m_prototype ? usages[i].getService() : null)) { // Empty loop body. } } } - public synchronized Bundle[] getUsingBundles(ServiceReference ref) + public Bundle[] getUsingBundles(ServiceReference ref) { Bundle[] bundles = null; - for (Iterator iter = m_inUseMap.entrySet().iterator(); iter.hasNext(); ) + for (Iterator> iter = m_inUseMap.entrySet().iterator(); iter.hasNext(); ) { - Map.Entry entry = (Map.Entry) iter.next(); - Bundle bundle = (Bundle) entry.getKey(); - UsageCount[] usages = (UsageCount[]) entry.getValue(); + Map.Entry entry = iter.next(); + Bundle bundle = entry.getKey(); + UsageCount[] usages = entry.getValue(); for (int useIdx = 0; useIdx < usages.length; useIdx++) { - if (usages[useIdx].m_ref.equals(ref)) + if (usages[useIdx].m_ref.equals(ref) && usages[useIdx].m_count.get() > 0) { // Add the bundle to the array to be returned. if (bundles == null) @@ -346,9 +604,14 @@ public synchronized Bundle[] getUsingBundles(ServiceReference ref) return bundles; } - public void servicePropertiesModified(ServiceRegistration reg) + void servicePropertiesModified(ServiceRegistration reg, Dictionary oldProps) { - fireServiceChanged(new ServiceEvent(ServiceEvent.MODIFIED, reg.getReference())); + this.hookRegistry.updateHooks(reg.getReference()); + if (m_callbacks != null) + { + m_callbacks.serviceChanged( + new ServiceEvent(ServiceEvent.MODIFIED, reg.getReference()), oldProps); + } } public Logger getLogger() @@ -356,190 +619,165 @@ public Logger getLogger() return m_logger; } - private static ServiceRegistration[] addServiceRegistration( - ServiceRegistration[] regs, ServiceRegistration reg) + /** + * Obtain a UsageCount object, by looking for an existing one or creating a new one (if possible). + * This method tries to find a UsageCount object in the {@code m_inUseMap}. If one is found then + * this is returned, otherwise a UsageCount object will be created, but this can only be done if + * the {@code isPrototype} parameter is not {@code null}. If {@code isPrototype} is {@code TRUE} + * then a new UsageCount object will always be created. + * @param bundle The bundle using the service. + * @param ref The Service Reference. + * @param svcObj A Service Object, if applicable. + * @param isPrototype {@code TRUE} if we know that this is a prototype, {@ FALSE} if we know that + * it isn't. There are cases where we don't know (the pure lookup case), in that case use {@code null}. + * @return The UsageCount object if it could be obtained, or {@code null} otherwise. + */ + UsageCount obtainUsageCount(Bundle bundle, ServiceReference ref, Object svcObj, Boolean isPrototype) { - if (regs == null) - { - regs = new ServiceRegistration[] { reg }; - } - else - { - ServiceRegistration[] newRegs = new ServiceRegistration[regs.length + 1]; - System.arraycopy(regs, 0, newRegs, 0, regs.length); - newRegs[regs.length] = reg; - regs = newRegs; - } - return regs; - } + UsageCount usage = null; - private static ServiceRegistration[] removeServiceRegistration( - ServiceRegistration[] regs, ServiceRegistration reg) - { - for (int i = 0; (regs != null) && (i < regs.length); i++) + // This method uses an optimistic concurrency mechanism with a conditional put/replace + // on the m_inUseMap. If this fails (because another thread made changes) this thread + // retries the operation. This is the purpose of the while loop. + boolean success = false; + while (!success) { - if (regs[i].equals(reg)) + UsageCount[] usages = m_inUseMap.get(bundle); + + // If we know it's a prototype, then we always need to create a new usage count + if (!Boolean.TRUE.equals(isPrototype)) { - // If this is the only usage, then point to empty list. - if ((regs.length - 1) == 0) + for (int i = 0; (usages != null) && (i < usages.length); i++) { - regs = new ServiceRegistration[0]; - } - // Otherwise, we need to do some array copying. - else - { - ServiceRegistration[] newRegs = new ServiceRegistration[regs.length - 1]; - System.arraycopy(regs, 0, newRegs, 0, i); - if (i < newRegs.length) + if (usages[i].m_ref.equals(ref) + && ((svcObj == null && !usages[i].m_prototype) || usages[i].getService() == svcObj)) { - System.arraycopy( - regs, i + 1, newRegs, i, newRegs.length - i); + return usages[i]; } - regs = newRegs; } } - } - return regs; - } - - public synchronized void addServiceListener(ServiceListener l) - { - m_serviceListener = ServiceListenerMulticaster.add(m_serviceListener, l); - } - - public synchronized void removeServiceListener(ServiceListener l) - { - m_serviceListener = ServiceListenerMulticaster.remove(m_serviceListener, l); - } - - protected void fireServiceChanged(ServiceEvent event) - { - // Grab a copy of the listener list. - ServiceListener listener = m_serviceListener; - // If not null, then dispatch event. - if (listener != null) - { - m_serviceListener.serviceChanged(event); - } - } - private static class ServiceListenerMulticaster implements ServiceListener - { - protected ServiceListener m_a = null, m_b = null; - - protected ServiceListenerMulticaster(ServiceListener a, ServiceListener b) - { - m_a = a; - m_b = b; - } - - public void serviceChanged(ServiceEvent e) - { - m_a.serviceChanged(e); - m_b.serviceChanged(e); - } - - public static ServiceListener add(ServiceListener a, ServiceListener b) - { - if (a == null) + // We haven't found an existing usage count object so we need to create on. For this we need to + // know whether this is a prototype or not. + if (isPrototype == null) { - return b; + // If this parameter isn't passed in we can't create a usage count. + return null; } - else if (b == null) + + // Add a new Usage Count. + usage = new UsageCount(ref, isPrototype); + if (usages == null) { - return a; + UsageCount[] newUsages = new UsageCount[] { usage }; + success = m_inUseMap.putIfAbsent(bundle, newUsages) == null; } else { - return new ServiceListenerMulticaster(a, b); + UsageCount[] newUsages = new UsageCount[usages.length + 1]; + System.arraycopy(usages, 0, newUsages, 0, usages.length); + newUsages[usages.length] = usage; + success = m_inUseMap.replace(bundle, usages, newUsages); } } - - public static ServiceListener remove(ServiceListener a, ServiceListener b) + return usage; + } + + /** + * Utility method to flush the specified bundle's usage count for the + * specified service reference. This should be called to completely + * remove the associated usage count object for the specified service + * reference. If the goal is to simply decrement the usage, then get + * the usage count and decrement its counter. This method will also + * remove the specified bundle from the "in use" map if it has no more + * usage counts after removing the usage count for the specified service + * reference. + * @param bundle The bundle whose usage count should be removed. + * @param ref The service reference whose usage count should be removed. + **/ + void flushUsageCount(Bundle bundle, ServiceReference ref, UsageCount uc) + { + // This method uses an optimistic concurrency mechanism with conditional modifications + // on the m_inUseMap. If this fails (because another thread made changes) this thread + // retries the operation. This is the purpose of the while loop. + boolean success = false; + while (!success) { - if ((a == null) || (a == b)) - { - return null; - } - else if (a instanceof ServiceListenerMulticaster) + UsageCount[] usages = m_inUseMap.get(bundle); + final UsageCount[] orgUsages = usages; + for (int i = 0; (usages != null) && (i < usages.length); i++) { - return add( - remove(((ServiceListenerMulticaster) a).m_a, b), - remove(((ServiceListenerMulticaster) a).m_b, b)); + if ((uc == null && usages[i].m_ref.equals(ref)) || (uc == usages[i])) + { + // If this is the only usage, then point to empty list. + if ((usages.length - 1) == 0) + { + usages = null; + } + // Otherwise, we need to do some array copying. + else + { + UsageCount[] newUsages = new UsageCount[usages.length - 1]; + System.arraycopy(usages, 0, newUsages, 0, i); + if (i < newUsages.length) + { + System.arraycopy( + usages, i + 1, newUsages, i, newUsages.length - i); + } + usages = newUsages; + i--; + } + } } - else + + if (usages == orgUsages) + return; // no change in map + + if (orgUsages != null) { - return a; + if (usages != null) + success = m_inUseMap.replace(bundle, orgUsages, usages); + else + success = m_inUseMap.remove(bundle, orgUsages); } } } - private static UsageCount getUsageCount(UsageCount[] usages, ServiceReference ref) + public HookRegistry getHookRegistry() { - for (int i = 0; (usages != null) && (i < usages.length); i++) - { - if (usages[i].m_ref.equals(ref)) - { - return usages[i]; - } - } - return null; + return this.hookRegistry; } - private static UsageCount[] addUsageCount(UsageCount[] usages, ServiceReference ref, Object svcObj) + static class UsageCount { - UsageCount usage = new UsageCount(); - usage.m_ref = ref; - usage.m_svcObj = svcObj; - usage.m_count++; + final ServiceReference m_ref; + final boolean m_prototype; - if (usages == null) + final AtomicLong m_count = new AtomicLong(); + final AtomicLong m_serviceObjectsCount = new AtomicLong(); + final AtomicReference m_svcHolderRef = new AtomicReference(); + + UsageCount(final ServiceReference ref, final boolean isPrototype) { - usages = new UsageCount[] { usage }; + m_ref = ref; + m_prototype = isPrototype; } - else + + Object getService() { - UsageCount[] newUsages = new UsageCount[usages.length + 1]; - System.arraycopy(usages, 0, newUsages, 0, usages.length); - newUsages[usages.length] = usage; - usages = newUsages; + ServiceHolder sh = m_svcHolderRef.get(); + return sh == null ? null : sh.m_service; } - return usages; } - private static UsageCount[] removeUsageCount(UsageCount[] usages, ServiceReference ref) + static class ServiceHolder { - for (int i = 0; (usages != null) && (i < usages.length); i++) - { - if (usages[i].m_ref.equals(ref)) - { - // If this is the only usage, then point to empty list. - if ((usages.length - 1) == 0) - { - usages = new UsageCount[0]; - } - // Otherwise, we need to do some array copying. - else - { - UsageCount[] newUsages= new UsageCount[usages.length - 1]; - System.arraycopy(usages, 0, newUsages, 0, i); - if (i < newUsages.length) - { - System.arraycopy( - usages, i + 1, newUsages, i, newUsages.length - i); - } - usages = newUsages; - } - } - } - - return usages; + final CountDownLatch m_latch = new CountDownLatch(1); + volatile Object m_service; } - private static class UsageCount + public interface ServiceRegistryCallbacks { - public int m_count = 0; - public ServiceReference m_ref = null; - public Object m_svcObj = null; + void serviceChanged(ServiceEvent event, Dictionary oldProps); } -} \ No newline at end of file +} diff --git a/framework/src/main/java/org/apache/felix/framework/StartLevelActivator.java b/framework/src/main/java/org/apache/felix/framework/StartLevelActivator.java deleted file mode 100644 index c903f6f6c8d..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/StartLevelActivator.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework; - -import org.osgi.framework.*; - -class StartLevelActivator implements BundleActivator -{ - private Felix m_felix = null; - private ServiceRegistration m_reg = null; - - public StartLevelActivator(Felix felix) - { - m_felix = felix; - } - - public void start(BundleContext context) throws Exception - { - m_reg = context.registerService( - org.osgi.service.startlevel.StartLevel.class.getName(), - new StartLevelImpl(m_felix), null); - } - - public void stop(BundleContext context) throws Exception - { - m_reg.unregister(); - } -} diff --git a/framework/src/main/java/org/apache/felix/framework/StartLevelImpl.java b/framework/src/main/java/org/apache/felix/framework/StartLevelImpl.java index 9785dfdf065..274ef7e8250 100644 --- a/framework/src/main/java/org/apache/felix/framework/StartLevelImpl.java +++ b/framework/src/main/java/org/apache/felix/framework/StartLevelImpl.java @@ -1,60 +1,48 @@ /* - * Copyright 2005 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework; -import java.util.ArrayList; -import java.util.List; -import org.osgi.framework.AdminPermission; import org.osgi.framework.Bundle; +import org.osgi.framework.startlevel.BundleStartLevel; +import org.osgi.framework.startlevel.FrameworkStartLevel; import org.osgi.service.startlevel.StartLevel; /** - * @author rickhall - * - * To change the template for this generated type comment go to - * Window>Preferences>Java>Code Generation>Code and Comments + * StartLevel service implementation. + * @author Felix Project Team **/ -public class StartLevelImpl implements StartLevel, Runnable +public class StartLevelImpl implements StartLevel { - private static final int BUNDLE_IDX = 0; - private static final int STARTLEVEL_IDX = 1; + private final Felix m_felix; - private Felix m_felix = null; - private List m_requestList = null; - private Bundle m_systemBundle = null; - - public StartLevelImpl(Felix felix) + StartLevelImpl(Felix felix) { m_felix = felix; - m_requestList = new ArrayList(); - m_systemBundle = m_felix.getBundle(0); - // Start a thread to perform asynchronous package refreshes. - Thread t = new Thread(this, "FelixStartLevel"); - t.setDaemon(true); - t.start(); } - + /* (non-Javadoc) * @see org.osgi.service.startlevel.StartLevel#getStartLevel() **/ public int getStartLevel() { - return m_felix.getStartLevel(); + return m_felix.adapt(FrameworkStartLevel.class).getStartLevel(); } /* (non-Javadoc) @@ -62,56 +50,7 @@ public int getStartLevel() **/ public void setStartLevel(int startlevel) { - Object sm = System.getSecurityManager(); - - if (sm != null) - { - ((SecurityManager) sm).checkPermission( - new AdminPermission(m_systemBundle, AdminPermission.STARTLEVEL)); - } - - if (startlevel <= 0) - { - throw new IllegalArgumentException( - "Start level must be greater than zero."); - } - - synchronized (m_requestList) - { - m_requestList.add(new Integer(startlevel)); - m_requestList.notifyAll(); - } - } - - /** - * This method is currently only called by the shutdown thread when the - * framework is shutting down. - * @param startlevel - **/ - /* package */ void setStartLevelAndWait(int startlevel) - { - Object request = new Integer(startlevel); - synchronized (request) - { - synchronized (m_requestList) - { - m_requestList.add(request); - m_requestList.notifyAll(); - } - - try - { - request.wait(); - } - catch (InterruptedException ex) - { - // Log it and ignore since it won't cause much of an issue. - m_felix.getLogger().log( - Logger.LOG_WARNING, - "Wait for start level change during shutdown interrupted.", - ex); - } - } + m_felix.adapt(FrameworkStartLevel.class).setStartLevel(startlevel); } /* (non-Javadoc) @@ -119,7 +58,7 @@ public void setStartLevel(int startlevel) **/ public int getBundleStartLevel(Bundle bundle) { - return m_felix.getBundleStartLevel(bundle); + return bundle.adapt(BundleStartLevel.class).getStartLevel(); } /* (non-Javadoc) @@ -127,29 +66,7 @@ public int getBundleStartLevel(Bundle bundle) **/ public void setBundleStartLevel(Bundle bundle, int startlevel) { - Object sm = System.getSecurityManager(); - - if (sm != null) - { - ((SecurityManager) sm).checkPermission( - new AdminPermission(bundle, AdminPermission.STARTLEVEL)); - } - - if (bundle.getBundleId() == 0) - { - throw new IllegalArgumentException( - "Cannot change system bundle start level."); - } - else if (startlevel <= 0) - { - throw new IllegalArgumentException( - "Start level must be greater than zero."); - } - synchronized (m_requestList) - { - m_requestList.add(new Object[] { bundle, new Integer(startlevel) }); - m_requestList.notifyAll(); - } + bundle.adapt(BundleStartLevel.class).setStartLevel(startlevel); } /* (non-Javadoc) @@ -157,7 +74,7 @@ else if (startlevel <= 0) **/ public int getInitialBundleStartLevel() { - return m_felix.getInitialBundleStartLevel(); + return m_felix.adapt(FrameworkStartLevel.class).getInitialBundleStartLevel(); } /* (non-Javadoc) @@ -165,14 +82,7 @@ public int getInitialBundleStartLevel() **/ public void setInitialBundleStartLevel(int startlevel) { - Object sm = System.getSecurityManager(); - - if (sm != null) - { - ((SecurityManager) sm).checkPermission( - new AdminPermission(m_systemBundle, AdminPermission.STARTLEVEL)); - } - m_felix.setInitialBundleStartLevel(startlevel); + m_felix.adapt(FrameworkStartLevel.class).setInitialBundleStartLevel(startlevel); } /* (non-Javadoc) @@ -180,57 +90,14 @@ public void setInitialBundleStartLevel(int startlevel) **/ public boolean isBundlePersistentlyStarted(Bundle bundle) { - return m_felix.isBundlePersistentlyStarted(bundle); + return bundle.adapt(BundleStartLevel.class).isPersistentlyStarted(); } - public void run() + /* (non-Javadoc) + * @see org.osgi.service.startlevel.StartLevel#isBundleActivationPolicyUsed(org.osgi.framework.Bundle) + **/ + public boolean isBundleActivationPolicyUsed(Bundle bundle) { - Object request = null; - - // This thread loops forever, thus it should - // be a daemon thread. - while (true) - { - synchronized (m_requestList) - { - // Wait for a request. - while (m_requestList.size() == 0) - { - try - { - m_requestList.wait(); - } - catch (InterruptedException ex) - { - // Ignore. - } - } - - // Get the requested start level. - request = m_requestList.remove(0); - } - - // If the request object is an Integer, then the request - // is to set the framework start level. If the request is - // an Object array, then the request is to set the start - // level for a bundle. - if (request instanceof Integer) - { - // Set the new framework start level. - m_felix.setFrameworkStartLevel(((Integer) request).intValue()); - } - else - { - Bundle bundle = (Bundle) ((Object[]) request)[BUNDLE_IDX]; - int startlevel = ((Integer) ((Object[]) request)[STARTLEVEL_IDX]).intValue(); - m_felix.setBundleStartLevel(bundle, startlevel); - } - - // Notify any waiting thread that this request is done. - synchronized (request) - { - request.notifyAll(); - } - } + return bundle.adapt(BundleStartLevel.class).isActivationPolicyUsed(); } } \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/StatefulResolver.java b/framework/src/main/java/org/apache/felix/framework/StatefulResolver.java new file mode 100644 index 00000000000..2d71eae5565 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/StatefulResolver.java @@ -0,0 +1,1777 @@ +/* + * 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.framework; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.felix.framework.capabilityset.CapabilitySet; +import org.apache.felix.framework.capabilityset.SimpleFilter; +import org.apache.felix.framework.resolver.CandidateComparator; +import org.apache.felix.framework.resolver.ResolveException; +import org.apache.felix.framework.util.FelixConstants; +import org.apache.felix.framework.util.ShrinkableCollection; +import org.apache.felix.framework.util.Util; +import org.apache.felix.framework.util.manifestparser.NativeLibrary; +import org.apache.felix.framework.wiring.BundleRequirementImpl; +import org.apache.felix.framework.wiring.BundleWireImpl; +import org.apache.felix.resolver.ResolverImpl; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.BundleException; +import org.osgi.framework.BundlePermission; +import org.osgi.framework.CapabilityPermission; +import org.osgi.framework.Constants; +import org.osgi.framework.PackagePermission; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.hooks.resolver.ResolverHook; +import org.osgi.framework.hooks.resolver.ResolverHookFactory; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; +import org.osgi.resource.Wire; +import org.osgi.resource.Wiring; +import org.osgi.service.resolver.ResolutionException; +import org.osgi.service.resolver.Resolver; + +class StatefulResolver +{ + private final Logger m_logger; + private final Felix m_felix; + private final ServiceRegistry m_registry; + private final Executor m_executor; + private final ResolverImpl m_resolver; + private boolean m_isResolving = false; + + // Set of all revisions. + private final Set m_revisions; + // Set of all fragments. + private final Set m_fragments; + // Capability sets. + private final Map m_capSets; + // Maps singleton symbolic names to list of bundle revisions sorted by version. + private final Map> m_singletons; + // Selected singleton bundle revisions. + private final Set m_selectedSingletons; + + StatefulResolver(Felix felix, ServiceRegistry registry) + { + m_felix = felix; + m_registry = registry; + m_logger = m_felix.getLogger(); + m_executor = getExecutor(); + m_resolver = new ResolverImpl(m_logger, m_executor); + + m_revisions = new HashSet(); + m_fragments = new HashSet(); + m_capSets = new HashMap(); + m_singletons = new HashMap>(); + m_selectedSingletons = new HashSet(); + + List indices = new ArrayList(); + indices.add(BundleRevision.BUNDLE_NAMESPACE); + m_capSets.put(BundleRevision.BUNDLE_NAMESPACE, new CapabilitySet(indices, true)); + + indices = new ArrayList(); + indices.add(BundleRevision.PACKAGE_NAMESPACE); + m_capSets.put(BundleRevision.PACKAGE_NAMESPACE, new CapabilitySet(indices, true)); + + indices = new ArrayList(); + indices.add(BundleRevision.HOST_NAMESPACE); + m_capSets.put(BundleRevision.HOST_NAMESPACE, new CapabilitySet(indices, true)); + } + + private Executor getExecutor() + { + String str = m_felix.getProperty(FelixConstants.RESOLVER_PARALLELISM); + int parallelism = Runtime.getRuntime().availableProcessors(); + if (str != null) + { + try + { + parallelism = Integer.parseInt(str); + } + catch (NumberFormatException e) + { + // Ignore + } + } + if (parallelism <= 1) + { + return new Executor() + { + public void execute(Runnable command) + { + command.run(); + } + }; + } + else + { + ThreadPoolExecutor executor = new ThreadPoolExecutor( + parallelism, parallelism, + 60, TimeUnit.SECONDS, + new LinkedBlockingQueue(), + new ThreadFactory() + { + final AtomicInteger counter = new AtomicInteger(); + @Override + public Thread newThread(Runnable r) + { + Thread thread = new Thread(r, "FelixResolver-" + counter.incrementAndGet()); + thread.setDaemon(true); + return thread; + } + }); + executor.allowCoreThreadTimeOut(true); + return executor; + } + } + + void start() + { + m_registry.registerService(m_felix, + new String[] { Resolver.class.getName() }, + new ResolverImpl(m_logger, 1), + null); + } + + synchronized void addRevision(BundleRevision br) + { + // Always attempt to remove the revision, since + // this method can be used for re-indexing a revision + // after it has been resolved. + removeRevision(br); + + m_revisions.add(br); + + // Add singletons to the singleton map. + boolean isSingleton = Util.isSingleton(br); + if (isSingleton) + { + // Index the new singleton. + addToSingletonMap(m_singletons, br); + } + + // We always need to index non-singleton bundle capabilities, but + // singleton bundles only need to be index if they are resolved. + // Unresolved singleton capabilities are only indexed before a + // resolve operation when singleton selection is performed. + if (!isSingleton || (br.getWiring() != null)) + { + if (Util.isFragment(br)) + { + m_fragments.add(br); + } + indexCapabilities(br); + } + } + + synchronized void removeRevision(BundleRevision br) + { + if (m_revisions.remove(br)) + { + m_fragments.remove(br); + deindexCapabilities(br); + + // If this module is a singleton, then remove it from the + // singleton map. + List revisions = m_singletons.get(br.getSymbolicName()); + if (revisions != null) + { + revisions.remove(br); + if (revisions.isEmpty()) + { + m_singletons.remove(br.getSymbolicName()); + } + } + } + } + + boolean isEffective(Requirement req) + { + String effective = req.getDirectives().get(Constants.EFFECTIVE_DIRECTIVE); + return ((effective == null) || effective.equals(Constants.EFFECTIVE_RESOLVE)); + } + + synchronized List findProviders( + BundleRequirement req, boolean obeyMandatory) + { + ResolverHookRecord record = new ResolverHookRecord( + Collections., ResolverHook>emptyMap(), null); + return findProvidersInternal(record, req, obeyMandatory, true); + } + + synchronized List findProvidersInternal( + final ResolverHookRecord record, + final Requirement req, + final boolean obeyMandatory, + final boolean invokeHooksAndSecurity) + { + List result = new ArrayList(); + + CapabilitySet capSet = m_capSets.get(req.getNamespace()); + if (capSet != null) + { + // Get the requirement's filter; if this is our own impl we + // have a shortcut to get the already parsed filter, otherwise + // we must parse it from the directive. + SimpleFilter sf; + if (req instanceof BundleRequirementImpl) + { + sf = ((BundleRequirementImpl) req).getFilter(); + } + else + { + String filter = req.getDirectives().get(Constants.FILTER_DIRECTIVE); + if (filter == null) + { + sf = new SimpleFilter(null, null, SimpleFilter.MATCH_ALL); + } + else + { + sf = SimpleFilter.parse(filter); + } + } + + // Find the matching candidates. + Set matches = capSet.match(sf, obeyMandatory); + // Filter matching candidates. + for (Capability cap : matches) + { + if (!(cap instanceof BundleCapability)) + continue; + + BundleCapability bcap = (BundleCapability) cap; + + // Filter according to security. + if (invokeHooksAndSecurity && filteredBySecurity((BundleRequirement)req, bcap)) + { + continue; + } + // Filter already resolved hosts, since we don't support + // dynamic attachment of fragments. + if (req.getNamespace().equals(BundleRevision.HOST_NAMESPACE) + && (bcap.getRevision().getWiring() != null)) + { + continue; + } + + result.add(bcap); + } + } + + if ( invokeHooksAndSecurity ) + { + // If we have resolver hooks, then we may need to filter our results + // based on a whitelist and/or fine-grained candidate filtering. + if (!result.isEmpty() && !record.getResolverHookRefs().isEmpty()) + { + // It we have a whitelist, then first filter out candidates + // from disallowed revisions. + if (record.getBundleRevisionWhitelist() != null) + { + for (Iterator it = result.iterator(); it.hasNext(); ) + { + if (!record.getBundleRevisionWhitelist().contains(it.next().getRevision())) + { + it.remove(); + } + } + } + + // Now give the hooks a chance to do fine-grained filtering. + ShrinkableCollection shrinkable = + new ShrinkableCollection(result); + for (ResolverHook hook : record.getResolverHooks()) + { + try + { + Felix.m_secureAction + .invokeResolverHookMatches(hook, (BundleRequirement)req, shrinkable); + } + catch (Throwable th) + { + m_logger.log(Logger.LOG_WARNING, "Resolver hook exception.", th); + } + } + } + } + + Collections.sort(result, new CandidateComparator()); + + return result; + } + + private boolean filteredBySecurity(BundleRequirement req, BundleCapability cap) + { + if (System.getSecurityManager() != null) + { + BundleRevisionImpl reqRevision = (BundleRevisionImpl) req.getRevision(); + + if (req.getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE)) + { + if (!((BundleProtectionDomain) ((BundleRevisionImpl) cap.getRevision()).getProtectionDomain()).impliesDirect( + new PackagePermission((String) cap.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE), + PackagePermission.EXPORTONLY)) || + !((reqRevision == null) || + ((BundleProtectionDomain) reqRevision.getProtectionDomain()).impliesDirect( + new PackagePermission((String) cap.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE), + cap.getRevision().getBundle(),PackagePermission.IMPORT)) + )) + { + if (reqRevision != cap.getRevision()) + { + return true; + } + } + } + else if (req.getNamespace().equals(BundleRevision.BUNDLE_NAMESPACE)) + { if (!((BundleProtectionDomain) ((BundleRevisionImpl) cap.getRevision()).getProtectionDomain()).impliesDirect( + new BundlePermission(cap.getRevision().getSymbolicName(), BundlePermission.PROVIDE)) || + !((reqRevision == null) || + ((BundleProtectionDomain) reqRevision.getProtectionDomain()).impliesDirect( + new BundlePermission(cap.getRevision().getSymbolicName(), BundlePermission.REQUIRE)) + )) + { + return true; + } + } + else if (req.getNamespace().equals(BundleRevision.HOST_NAMESPACE)) + { + if (!((BundleProtectionDomain) reqRevision.getProtectionDomain()) + .impliesDirect(new BundlePermission( + cap.getRevision().getSymbolicName(), + BundlePermission.FRAGMENT)) + || !((BundleProtectionDomain) ((BundleRevisionImpl) cap.getRevision()).getProtectionDomain()) + .impliesDirect(new BundlePermission( + cap.getRevision().getSymbolicName(), + BundlePermission.HOST))) + { + return true; + } + } + else if (!req.getNamespace().equals("osgi.ee")) + { + if (!((BundleProtectionDomain) ((BundleRevisionImpl) cap.getRevision()).getProtectionDomain()).impliesDirect( + new CapabilityPermission(req.getNamespace(), CapabilityPermission.PROVIDE)) + || + !((reqRevision == null) || ((BundleProtectionDomain) reqRevision.getProtectionDomain()).impliesDirect( + new CapabilityPermission(req.getNamespace(), cap.getAttributes(), cap.getRevision().getBundle(), CapabilityPermission.REQUIRE)))) + { + return true; + } + } + } + return false; + } + + void resolve( + Set mandatory, + Set optional) + throws ResolutionException, BundleException + { + // Acquire global lock. + boolean locked = m_felix.acquireGlobalLock(); + if (!locked) + { + throw new ResolveException( + "Unable to acquire global lock for resolve.", null, null); + } + + // Make sure we are not already resolving, which can be + // the case if a resolver hook does something bad. + if (m_isResolving) + { + m_felix.releaseGlobalLock(); + throw new IllegalStateException("Nested resolve operations not allowed."); + } + m_isResolving = true; + + Map> wireMap = null; + try + { + // Make our own copy of revisions. + mandatory = (mandatory.isEmpty()) + ? mandatory : new HashSet(mandatory); + optional = (optional.isEmpty()) + ? optional : new HashSet(optional); + + // Prepare resolver hooks, if any. + ResolverHookRecord record = prepareResolverHooks(mandatory, optional); + + // Select any singletons in the resolver state. + selectSingletons(record); + + // Extensions are resolved differently. + for (Iterator it = mandatory.iterator(); it.hasNext(); ) + { + BundleRevision br = it.next(); + BundleImpl bundle = (BundleImpl) br.getBundle(); + if (bundle.isExtension()) + { + it.remove(); + } + else if (Util.isSingleton(br) && !isSelectedSingleton(br)) + { + throw new ResolveException("Singleton conflict.", br, null); + } + } + for (Iterator it = optional.iterator(); it.hasNext(); ) + { + BundleRevision br = it.next(); + BundleImpl bundle = (BundleImpl) br.getBundle(); + if (bundle.isExtension()) + { + it.remove(); + } + else if (Util.isSingleton(br) && !isSelectedSingleton(br)) + { + it.remove(); + } + } + + // Catch any resolve exception to rethrow later because + // we may need to call end() on resolver hooks. + ResolutionException rethrow = null; + try + { + // Resolve the revision. + wireMap = m_resolver.resolve( + new ResolveContextImpl( + this, + getWirings(), + record, + mandatory, + optional, + getFragments())); + } + catch (ResolutionException ex) + { + rethrow = ex; + } + + // Release resolver hooks, if any. + releaseResolverHooks(record); + + // If the resolve failed, rethrow the exception. + if (rethrow != null) + { + throw rethrow; + } + + // Otherwise, mark all revisions as resolved. + markResolvedRevisions(wireMap); + } + finally + { + // Clear resolving flag. + m_isResolving = false; + // Always release the global lock. + m_felix.releaseGlobalLock(); + } + + fireResolvedEvents(wireMap); + } + + BundleRevision resolve(BundleRevision revision, String pkgName) + throws ResolutionException, BundleException + { + BundleRevision provider = null; + + // We cannot dynamically import if the revision is not already resolved + // or if it is not allowed, so check that first. Note: We check if the + // dynamic import is allowed without holding any locks, but this is + // okay since the resolver will double check later after we have + // acquired the global lock below. + if ((revision.getWiring() != null) && isAllowedDynamicImport(revision, pkgName)) + { + // Acquire global lock. + boolean locked = m_felix.acquireGlobalLock(); + if (!locked) + { + throw new ResolveException( + "Unable to acquire global lock for resolve.", revision, null); + } + + // Make sure we are not already resolving, which can be + // the case if a resolver hook does something bad. + if (m_isResolving) + { + m_felix.releaseGlobalLock(); + throw new IllegalStateException("Nested resolve operations not allowed."); + } + m_isResolving = true; + + Map> wireMap = null; + try + { + // Double check to make sure that someone hasn't beaten us to + // dynamically importing the package, which can happen if two + // threads are racing to do so. If we have an existing wire, + // then just return it instead. + provider = ((BundleWiringImpl) revision.getWiring()) + .getImportedPackageSource(pkgName); + if (provider == null) + { + // Prepare resolver hooks, if any. + ResolverHookRecord record = + prepareResolverHooks( + Collections.singleton(revision), Collections.EMPTY_SET); + + // Select any singletons in the resolver state. + selectSingletons(record); + + // Catch any resolve exception to rethrow later because + // we may need to call end() on resolver hooks. + ResolutionException rethrow = null; + try + { + List dynamics = + Util.getDynamicRequirements(revision.getWiring().getRequirements(null)); + + // Loop through the importer's dynamic requirements to determine if + // there is a matching one for the package from which we want to + // load a class. + Map attrs = Collections.singletonMap( + BundleRevision.PACKAGE_NAMESPACE, (Object) pkgName); + BundleRequirementImpl req = new BundleRequirementImpl( + revision, + BundleRevision.PACKAGE_NAMESPACE, + Collections.EMPTY_MAP, + attrs); + final List candidates = findProvidersInternal(record, req, false, true); + + // Try to find a dynamic requirement that matches the capabilities. + final BundleRequirementImpl dynReq = findDynamicRequirement(dynamics, candidates); + + // If we found a matching dynamic requirement, then filter out + // any candidates that do not match it. + if (dynReq != null) + { + for (Iterator itCand = candidates.iterator(); + itCand.hasNext(); ) + { + Capability cap = itCand.next(); + if (!CapabilitySet.matches( + cap, dynReq.getFilter())) + { + itCand.remove(); + } + } + } + else + { + candidates.clear(); + } + + Map wirings = getWirings(); + + wireMap = dynReq != null && wirings.containsKey(revision) ? m_resolver.resolveDynamic( + new ResolveContextImpl( + this, + wirings, + record, + Collections.emptyList(), + Collections.emptyList(), + getFragments()) + { + @Override + public List findProviders(Requirement br) + { + return (List) (br == dynReq ? candidates : super.findProviders(br)); + } + }, + revision.getWiring(), dynReq) : Collections.>emptyMap(); + } + catch (ResolutionException ex) + { + rethrow = ex; + } + + // Release resolver hooks, if any. + releaseResolverHooks(record); + + // If the resolve failed, rethrow the exception. + if (rethrow != null) + { + throw rethrow; + } + + if ((wireMap != null) && wireMap.containsKey(revision)) + { + List dynamicWires = wireMap.remove(revision); + Wire dynamicWire = dynamicWires.get(0); + + // Mark all revisions as resolved. + markResolvedRevisions(wireMap); + + // Dynamically add new wire to importing revision. + if (dynamicWire != null) + { + // TODO is a rw already a BundleWire? + // TODO can we optimize this? + if (dynamicWire.getRequirer() instanceof BundleRevision && + dynamicWire.getRequirement() instanceof BundleRequirement && + dynamicWire.getProvider() instanceof BundleRevision && + dynamicWire.getCapability() instanceof BundleCapability) + { + BundleRevision dwRequirer = (BundleRevision) dynamicWire.getRequirer(); + BundleRequirement dwRequirement = (BundleRequirement) dynamicWire.getRequirement(); + BundleRevision dwProvider = (BundleRevision) dynamicWire.getProvider(); + BundleCapability dwCapability = (BundleCapability) dynamicWire.getCapability(); + + BundleWire bw = new BundleWireImpl( + dwRequirer, + dwRequirement, + dwProvider, + dwCapability); + + m_felix.getDependencies().addDependent(bw); + + ((BundleWiringImpl) revision.getWiring()).addDynamicWire(bw); + + m_felix.getLogger().log( + Logger.LOG_DEBUG, + "DYNAMIC WIRE: " + dynamicWire); + + provider = ((BundleWiringImpl) revision.getWiring()) + .getImportedPackageSource(pkgName); + } + } + } + } + } + finally + { + // Clear resolving flag. + m_isResolving = false; + // Always release the global lock. + m_felix.releaseGlobalLock(); + } + + fireResolvedEvents(wireMap); + } + + return provider; + } + + private BundleRequirementImpl findDynamicRequirement(List dynamics, List candidates) + { + for (int dynIdx = 0; (candidates.size() > 0) && (dynIdx < dynamics.size()); dynIdx++) + { + for (Iterator itCand = candidates.iterator(); itCand.hasNext(); ) + { + Capability cap = itCand.next(); + if (CapabilitySet.matches( + cap, + ((BundleRequirementImpl) dynamics.get(dynIdx)).getFilter())) + { + return (BundleRequirementImpl) dynamics.get(dynIdx); + } + } + } + return null; + } + + private ResolverHookRecord prepareResolverHooks( + Set mandatory, Set optional) + throws BundleException, ResolutionException + { + // This map maps the hook factory service to the actual hook objects. It + // needs to be a map that preserves insertion order to ensure that we call + // hooks in the correct order. + // The hooks are added in the order that m_felix.getHooks() returns them which + // is also the order in which they should be called. + Map, ResolverHook> hookMap = + new LinkedHashMap, ResolverHook>(); + + // Get resolver hook factories. + Set> hookRefs = + m_felix.getHookRegistry().getHooks(ResolverHookFactory.class); + Collection whitelist; + + if (!hookRefs.isEmpty()) + { + // Create triggers list. + Set triggers; + if (!mandatory.isEmpty() && !optional.isEmpty()) + { + triggers = new HashSet(mandatory); + triggers.addAll(optional); + } + else + { + triggers = (mandatory.isEmpty()) + ? optional : mandatory; + } + triggers = Collections.unmodifiableSet(triggers); + + BundleException rethrow = null; + + // Create resolver hook objects by calling begin() on factory. + for (ServiceReference ref : hookRefs) + { + try + { + ResolverHookFactory rhf = m_felix.getService(m_felix, ref, false); + if (rhf != null) + { + ResolverHook hook = + Felix.m_secureAction + .invokeResolverHookFactory(rhf, triggers); + if (hook != null) + { + hookMap.put(ref, hook); + } + } + } + catch (Throwable ex) + { + rethrow = new BundleException( + "Resolver hook exception: " + ex.getMessage(), + BundleException.REJECTED_BY_HOOK, + ex); + // Resolver hook spec: if there is an exception during the resolve operation; abort. + // So we break here to make sure that no further resolver hooks are created. + break; + } + } + + if (rethrow != null) + { + for (ResolverHook hook : hookMap.values()) + { + try + { + Felix.m_secureAction.invokeResolverHookEnd(hook); + } + catch (Exception ex) + { + rethrow = new BundleException( + "Resolver hook exception: " + ex.getMessage(), + BundleException.REJECTED_BY_HOOK, + ex); + } + } + + throw rethrow; + } + + // Ask hooks to indicate which revisions should not be resolved. + whitelist = new ShrinkableCollection(getUnresolvedRevisions()); + int originalSize = whitelist.size(); + for (ResolverHook hook : hookMap.values()) + { + try + { + Felix.m_secureAction + .invokeResolverHookResolvable(hook, whitelist); + } + catch (Throwable ex) + { + rethrow = new BundleException( + "Resolver hook exception: " + ex.getMessage(), + BundleException.REJECTED_BY_HOOK, + ex); + // Resolver hook spec: if there is an exception during the resolve operation; abort. + // So we break here to make sure that no further resolver operations are executed. + break; + } + } + + if (rethrow != null) + { + for (ResolverHook hook : hookMap.values()) + { + try + { + Felix.m_secureAction.invokeResolverHookEnd(hook); + } + catch (Exception ex) + { + rethrow = new BundleException( + "Resolver hook exception: " + ex.getMessage(), + BundleException.REJECTED_BY_HOOK, + ex); + } + } + + throw rethrow; + } + + // If nothing was removed, then just null the whitelist + // as an optimization. + if (whitelist.size() == originalSize) + { + whitelist = null; + } + + // Check to make sure the target revisions are allowed to resolve. + if (whitelist != null) + { + // We only need to check this for the non-dynamic import + // case. The dynamic import case will only have one resolved + // trigger revision in the mandatory set, so ignore that case. + if (mandatory.isEmpty() + || !optional.isEmpty() + || (mandatory.iterator().next().getWiring() == null)) + { + mandatory.retainAll(whitelist); + optional.retainAll(whitelist); + if (mandatory.isEmpty() && optional.isEmpty()) + { + throw new ResolveException( + "Resolver hook prevented resolution.", null, null); + } + } + } + } + else + { + whitelist = null; + } + + return new ResolverHookRecord(hookMap, whitelist); + } + + private void releaseResolverHooks(ResolverHookRecord record) + throws BundleException + { + // If we have resolver hooks, we must call end() on them. + if (!record.getResolverHookRefs().isEmpty()) + { + // Verify that all resolver hook service references are still valid + // Call end() on resolver hooks. + for (ResolverHook hook : record.getResolverHooks()) + { + try + { + Felix.m_secureAction.invokeResolverHookEnd(hook); + } + catch (Throwable th) + { + m_logger.log( + Logger.LOG_WARNING, "Resolver hook exception.", th); + } + } + // Verify that all hook service references are still valid + // and unget all resolver hook factories. + boolean invalid = false; + for (ServiceReference ref : record.getResolverHookRefs()) + { + if (ref.getBundle() == null) + { + invalid = true; + } + m_felix.ungetService(m_felix, ref, null); + } + if (invalid) + { + throw new BundleException( + "Resolver hook service unregistered during resolve.", + BundleException.REJECTED_BY_HOOK); + } + } + } + + // This method duplicates a lot of logic from: + // ResolverImpl.getDynamicImportCandidates() + // This is only a rough check since it doesn't include resolver hooks. + boolean isAllowedDynamicImport(BundleRevision revision, String pkgName) + { + // Unresolved revisions cannot dynamically import, nor can the default + // package be dynamically imported. + if ((revision.getWiring() == null) || pkgName.length() == 0) + { + return false; + } + + // If the revision doesn't have dynamic imports, then just return + // immediately. + List dynamics = + Util.getDynamicRequirements(revision.getWiring().getRequirements(null)); + if ((dynamics == null) || dynamics.isEmpty()) + { + return false; + } + + // If the revision exports this package, then we cannot + // attempt to dynamically import it. + for (BundleCapability cap : revision.getWiring().getCapabilities(null)) + { + if (cap.getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE) + && cap.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE).equals(pkgName)) + { + return false; + } + } + + // If this revision already imports or requires this package, then + // we cannot dynamically import it. + if (((BundleWiringImpl) revision.getWiring()).hasPackageSource(pkgName)) + { + return false; + } + + // Loop through the importer's dynamic requirements to determine if + // there is a matching one for the package from which we want to + // load a class. + Map attrs = Collections.singletonMap( + BundleRevision.PACKAGE_NAMESPACE, (Object) pkgName); + BundleRequirementImpl req = new BundleRequirementImpl( + revision, + BundleRevision.PACKAGE_NAMESPACE, + Collections.EMPTY_MAP, + attrs); + List candidates = findProviders(req, false); + + // Try to find a dynamic requirement that matches the capabilities. + BundleRequirementImpl dynReq = null; + for (int dynIdx = 0; + (candidates.size() > 0) && (dynReq == null) && (dynIdx < dynamics.size()); + dynIdx++) + { + for (Iterator itCand = candidates.iterator(); + (dynReq == null) && itCand.hasNext(); ) + { + Capability cap = itCand.next(); + if (CapabilitySet.matches( + cap, + ((BundleRequirementImpl) dynamics.get(dynIdx)).getFilter())) + { + dynReq = (BundleRequirementImpl) dynamics.get(dynIdx); + } + } + } + + // If we found a matching dynamic requirement, then filter out + // any candidates that do not match it. + if (dynReq != null) + { + for (Iterator itCand = candidates.iterator(); + itCand.hasNext(); ) + { + Capability cap = itCand.next(); + if (!CapabilitySet.matches( + cap, dynReq.getFilter())) + { + itCand.remove(); + } + } + } + else + { + candidates.clear(); + } + + return !candidates.isEmpty(); + } + + private void markResolvedRevisions(Map> wireMap) + throws ResolveException + { + boolean debugLog = m_felix.getLogger().getLogLevel() >= Logger.LOG_DEBUG; + + // DO THIS IN THREE PASSES: + // 1. Aggregate fragments per host. + // 2. Attach wires and fragments to hosts. + // -> If fragments fail to attach, then undo. + // 3. Mark hosts and fragments as resolved. + + // First pass. + if (wireMap != null) + { + // First pass: Loop through the wire map to find the host wires + // for any fragments and map a host to all of its fragments. + Map> hosts = + new HashMap>(); + for (Entry> entry : wireMap.entrySet()) + { + Resource revision = entry.getKey(); + List wires = entry.getValue(); + + if (Util.isFragment(revision)) + { + for (Wire w : wires) + { + List fragments = hosts.get(w.getProvider()); + if (fragments == null) + { + fragments = new ArrayList(); + hosts.put(w.getProvider(), fragments); + } + + if (w.getRequirer() instanceof BundleRevision) + fragments.add((BundleRevision) w.getRequirer()); + } + } + } + + // Second pass: Loop through the wire map to do three things: + // 1) convert resolver wires to bundle wires 2) create wiring + // objects for revisions and 3) record dependencies among + // revisions. We don't actually set the wirings here because + // that indicates that a revision is resolved and we don't want + // to mark anything as resolved unless we succussfully create + // all wirings. + Map wirings = + new HashMap(wireMap.size()); + for (Entry> entry : wireMap.entrySet()) + { + Resource resource = entry.getKey(); + if (!(resource instanceof BundleRevision)) + continue; + + BundleRevision revision = (BundleRevision) resource; + List resolverWires = entry.getValue(); + + List bundleWires = + new ArrayList(resolverWires.size()); + + // Need to special case fragments since they may already have + // wires if they are already attached to another host; if that + // is the case, then we want to merge the old host wires with + // the new ones. + if ((revision.getWiring() != null) && Util.isFragment(revision)) + { + // Fragments only have host wires, so just add them all. + bundleWires.addAll(revision.getWiring().getRequiredWires(null)); + } + + // Loop through resolver wires to calculate the package + // space implied by the wires as well as to record the + // dependencies. + Map importedPkgs = + new HashMap(); + Map> requiredPkgs = + new HashMap>(); + for (Wire rw : resolverWires) + { + // TODO is a rw already a BundleWire? + // TODO can we optimize this? + if (!(rw.getRequirer() instanceof BundleRevision)) + continue; + BundleRevision requirer = (BundleRevision) rw.getRequirer(); + if (!(rw.getRequirement() instanceof BundleRequirement)) + continue; + BundleRequirement requirement = (BundleRequirement) rw.getRequirement(); + if (!(rw.getProvider() instanceof BundleRevision)) + continue; + BundleRevision provider = (BundleRevision) rw.getProvider(); + if (!(rw.getCapability() instanceof BundleCapability)) + continue; + BundleCapability capability = (BundleCapability) rw.getCapability(); + + BundleWire bw = new BundleWireImpl( + requirer, + requirement, + provider, + capability); + bundleWires.add(bw); + + if (Util.isFragment(revision)) + { + if (debugLog) + { + m_felix.getLogger().log( + Logger.LOG_DEBUG, + "FRAGMENT WIRE: " + rw.toString()); + } + } + else + { + if (debugLog) + { + m_felix.getLogger().log(Logger.LOG_DEBUG, "WIRE: " + rw.toString()); + } + + if (capability.getNamespace() + .equals(BundleRevision.PACKAGE_NAMESPACE)) + { + importedPkgs.put( + (String) capability.getAttributes() + .get(BundleRevision.PACKAGE_NAMESPACE), + provider); + } + else if (capability.getNamespace() + .equals(BundleRevision.BUNDLE_NAMESPACE)) + { + Set pkgs = calculateExportedAndReexportedPackages( + provider, + wireMap, + new HashSet(), + new HashSet()); + for (String pkg : pkgs) + { + List revs = requiredPkgs.get(pkg); + if (revs == null) + { + revs = new ArrayList(); + requiredPkgs.put(pkg, revs); + } + revs.add(provider); + } + } + } + } + + List fragments = hosts.get(revision); + try + { + wirings.put( + revision, + new BundleWiringImpl( + m_felix.getLogger(), + m_felix.getConfig(), + this, + (BundleRevisionImpl) revision, + fragments, + bundleWires, + importedPkgs, + requiredPkgs)); + } + catch (Exception ex) + { + // This is a fatal error, so undo everything and + // throw an exception. + for (Entry wiringEntry + : wirings.entrySet()) + { + // Dispose of wiring. + try + { + wiringEntry.getValue().dispose(); + } + catch (Exception ex2) + { + // We are in big trouble. + RuntimeException rte = new RuntimeException( + "Unable to clean up resolver failure.", ex2); + m_felix.getLogger().log( + Logger.LOG_ERROR, + rte.getMessage(), ex2); + throw rte; + } + } + + ResolveException re = new ResolveException( + "Unable to resolve " + revision, + revision, null); + re.initCause(ex); + m_felix.getLogger().log( + Logger.LOG_ERROR, + re.getMessage(), ex); + throw re; + } + } + + // Third pass: Loop through the wire map to mark revision as resolved + // and update the resolver state. + for (Entry entry : wirings.entrySet()) + { + BundleRevisionImpl revision = (BundleRevisionImpl) entry.getKey(); + + // Mark revision as resolved. + BundleWiring wiring = entry.getValue(); + revision.resolve(entry.getValue()); + + // Record dependencies. + for (BundleWire bw : wiring.getRequiredWires(null)) + { + m_felix.getDependencies().addDependent(bw); + } + + // Reindex the revision's capabilities since its resolved + // capabilities could be different than its declared ones + // (e.g., due to substitutable exports). + addRevision(revision); + + // Update the state of the revision's bundle to resolved as well. + markBundleResolved(revision); + } + } + } + + private void markBundleResolved(BundleRevision revision) + { + // Update the bundle's state to resolved when the + // current revision is resolved; just ignore resolve + // events for older revisions since this only occurs + // when an update is done on an unresolved bundle + // and there was no refresh performed. + BundleImpl bundle = (BundleImpl) revision.getBundle(); + + // Lock the bundle first. + try + { + // Acquire bundle lock. + try + { + m_felix.acquireBundleLock( + bundle, Bundle.INSTALLED | Bundle.RESOLVED | Bundle.ACTIVE); + } + catch (IllegalStateException ex) + { + // There is nothing we can do. + } + if (bundle.adapt(BundleRevision.class) == revision) + { + if (bundle.getState() != Bundle.INSTALLED) + { + m_felix.getLogger().log(bundle, + Logger.LOG_WARNING, + "Received a resolve event for a bundle that has already been resolved."); + } + else + { + m_felix.setBundleStateAndNotify(bundle, Bundle.RESOLVED); + } + } + } + finally + { + m_felix.releaseBundleLock(bundle); + } + } + + private void fireResolvedEvents(Map> wireMap) + { + if (wireMap != null) + { + Iterator>> iter = + wireMap.entrySet().iterator(); + // Iterate over the map to fire necessary RESOLVED events. + while (iter.hasNext()) + { + Entry> entry = iter.next(); + Resource resource = entry.getKey(); + if (!(resource instanceof BundleRevision)) + continue; + + BundleRevision revision = (BundleRevision) resource; + + // Fire RESOLVED events for all fragments. + List fragments = + Util.getFragments(revision.getWiring()); + for (int i = 0; i < fragments.size(); i++) + { + m_felix.fireBundleEvent( + BundleEvent.RESOLVED, fragments.get(i).getBundle()); + } + m_felix.fireBundleEvent(BundleEvent.RESOLVED, revision.getBundle()); + } + } + } + + private static Set calculateExportedAndReexportedPackages( + BundleRevision br, + Map> wireMap, + Set pkgs, + Set cycles) + { + if (!cycles.contains(br)) + { + cycles.add(br); + + // Add all exported packages. + for (BundleCapability cap : br.getDeclaredCapabilities(null)) + { + if (cap.getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE)) + { + pkgs.add((String) + cap.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE)); + } + } + + // Now check to see if any required bundles are required with reexport + // visibility, since we need to include those packages too. + if (br.getWiring() == null) + { + for (Wire rw : wireMap.get(br)) + { + if (rw.getCapability().getNamespace().equals( + BundleRevision.BUNDLE_NAMESPACE)) + { + String dir = rw.getRequirement() + .getDirectives().get(Constants.VISIBILITY_DIRECTIVE); + if ((dir != null) && (dir.equals(Constants.VISIBILITY_REEXPORT))) + { + calculateExportedAndReexportedPackages( + // TODO need to fix the cast + (BundleRevision) rw.getProvider(), + wireMap, + pkgs, + cycles); + } + } + } + } + else + { + for (BundleWire bw : br.getWiring().getRequiredWires(null)) + { + if (bw.getCapability().getNamespace().equals( + BundleRevision.BUNDLE_NAMESPACE)) + { + String dir = bw.getRequirement() + .getDirectives().get(Constants.VISIBILITY_DIRECTIVE); + if ((dir != null) && (dir.equals(Constants.VISIBILITY_REEXPORT))) + { + calculateExportedAndReexportedPackages( + bw.getProviderWiring().getRevision(), + wireMap, + pkgs, + cycles); + } + } + } + } + } + + return pkgs; + } + + private synchronized void indexCapabilities(BundleRevision br) + { + List caps = + (Util.isFragment(br) || (br.getWiring() == null)) + ? br.getDeclaredCapabilities(null) + : br.getWiring().getCapabilities(null); + if (caps != null) + { + for (BundleCapability cap : caps) + { + // If the capability is from a different revision, then + // don't index it since it is a capability from a fragment. + // In that case, the fragment capability is still indexed. + // It will be the resolver's responsibility to find all + // attached hosts for fragments. + if (cap.getRevision() == br) + { + CapabilitySet capSet = m_capSets.get(cap.getNamespace()); + if (capSet == null) + { + capSet = new CapabilitySet(null, true); + m_capSets.put(cap.getNamespace(), capSet); + } + capSet.addCapability(cap); + } + } + } + } + + private synchronized void deindexCapabilities(BundleRevision br) + { + // We only need be concerned with declared capabilities here, + // because resolved capabilities will be a subset, since fragment + // capabilities are not considered to be part of the host. + List caps = br.getDeclaredCapabilities(null); + if (caps != null) + { + for (BundleCapability cap : caps) + { + CapabilitySet capSet = m_capSets.get(cap.getNamespace()); + if (capSet != null) + { + capSet.removeCapability(cap); + } + } + } + } + + private synchronized boolean isSelectedSingleton(BundleRevision br) + { + return m_selectedSingletons.contains(br); + } + + private synchronized void selectSingletons(ResolverHookRecord record) + throws BundleException + { + // First deindex any unresolved singletons to make sure + // there aren't any available from previous resolves. + // Also remove them from the fragment list, for the same + // reason. + m_selectedSingletons.clear(); + for (Entry> entry : m_singletons.entrySet()) + { + for (BundleRevision singleton : entry.getValue()) + { + if (singleton.getWiring() == null) + { + deindexCapabilities(singleton); + m_fragments.remove(singleton); + } + } + } + + // If no resolver hooks, then use default singleton selection + // algorithm, otherwise defer to the resolver hooks. + if (record.getResolverHookRefs().isEmpty()) + { + selectDefaultSingletons(record); + } + else + { + selectSingletonsUsingHooks(record); + } + } + + /* + * Selects the singleton with the highest version from groupings + * based on the symbolic name. No selection is made if the group + * already has a resolved singleton. + */ + private void selectDefaultSingletons(ResolverHookRecord record) + { + // Now select the singletons available for this resolve operation. + for (Entry> entry : m_singletons.entrySet()) + { + selectSingleton(record, entry.getValue()); + } + } + + /* + * Groups singletons based on resolver hook filtering and then selects + * the singleton from each group with the highest version that is in + * the resolver hook whitelist. No selection is made if a group already + * has a resolved singleton in it. + */ + private void selectSingletonsUsingHooks(ResolverHookRecord record) + throws BundleException + { + // Convert singleton bundle revision map into a map using + // bundle capabilities instead, since this is what the resolver + // hooks require. + Map> allCollisions + = new HashMap>(); + for (Entry> entry : m_singletons.entrySet()) + { + Collection bundleCaps = + new ArrayList(); + for (BundleRevision br : entry.getValue()) + { + List caps = + br.getDeclaredCapabilities(BundleRevision.BUNDLE_NAMESPACE); + if (!caps.isEmpty()) + { + bundleCaps.add(caps.get(0)); + } + } + + for (BundleCapability bc : bundleCaps) + { + Collection capCopy = + new ShrinkableCollection( + new ArrayList(bundleCaps)); + capCopy.remove(bc); + allCollisions.put(bc, capCopy); + } + } + + // Invoke hooks to allow them to filter singleton collisions. + for (ResolverHook hook : record.getResolverHooks()) + { + for (Entry> entry + : allCollisions.entrySet()) + { + try + { + Felix.m_secureAction + .invokeResolverHookSingleton(hook, entry.getKey(), entry.getValue()); + } + catch (Throwable ex) + { + throw new BundleException( + "Resolver hook exception: " + ex.getMessage(), + BundleException.REJECTED_BY_HOOK, + ex); + } + } + } + + // Create groups according to how the resolver hooks filtered the + // collisions. + List> groups = new ArrayList>(); + while (!allCollisions.isEmpty()) + { + BundleCapability target = allCollisions.entrySet().iterator().next().getKey(); + groups.add(groupSingletons(allCollisions, target, new ArrayList())); + } + + // Now select the singletons available for this resolve operation. + for (List group : groups) + { + selectSingleton(record, group); + } + } + + private List groupSingletons( + Map> allCollisions, + BundleCapability target, List group) + { + if (!group.contains(target.getRevision())) + { + // Add the target since it is implicitly part of the group. + group.add(target.getRevision()); + + // Recursively add the revisions of any singleton's in the + // target's collisions. + Collection collisions = allCollisions.remove(target); + for (BundleCapability collision : collisions) + { + groupSingletons(allCollisions, collision, group); + } + + // Need to check the values of other collisions for this target + // and add those to the target's group too, since collisions are + // treated as two-way relationships. Repeat until there are no + // collision groups left that contain the target capability. + boolean repeat; + do + { + repeat = false; + for (Entry> entry: + allCollisions.entrySet()) + { + if (entry.getValue().contains(target)) + { + repeat = true; + groupSingletons(allCollisions, entry.getKey(), group); + break; + } + } + } + while (repeat); + } + return group; + } + + /* + * Selects the highest bundle revision from the group that is + * in the resolver hook whitelist (if there are hooks). No + * selection is made if there is an already resolved singleton + * in the group, since it is already indexed. + */ + private void selectSingleton(ResolverHookRecord record, List singletons) + { + BundleRevision selected = null; + for (BundleRevision singleton : singletons) + { + // If a singleton is already resolved, + // then there is nothing to do. + if (singleton.getWiring() != null) + { + selected = null; + break; + } + // If this singleton is not in the whitelist, then it cannot + // be selected. If it is, in can only be selected if it has + // a higher version than the currently selected singleton, if + // there is one. + if (((record.getBundleRevisionWhitelist() == null) || record.getBundleRevisionWhitelist().contains(singleton)) + && ((selected == null) + || (selected.getVersion().compareTo(singleton.getVersion()) > 0))) + { + selected = singleton; + } + } + if (selected != null) + { + // Record the selected singleton. + m_selectedSingletons.add(selected); + // Index its capabilities. + indexCapabilities(selected); + // If the selected singleton is a fragment, then + // add it to the list of fragments. + if (Util.isFragment(selected)) + { + m_fragments.add(selected); + } + } + } + + private synchronized Set getFragments() + { + Set fragments = new HashSet(m_fragments); + // Filter out any fragments that are not the current revision. + for (Iterator it = fragments.iterator(); it.hasNext(); ) + { + BundleRevision fragment = it.next(); + BundleRevision currentFragmentRevision = + fragment.getBundle().adapt(BundleRevision.class); + if (fragment != currentFragmentRevision) + { + it.remove(); + } + } + return fragments; + } + + private synchronized Set getUnresolvedRevisions() + { + Set unresolved = new HashSet(); + for (BundleRevision revision : m_revisions) + { + if (revision.getWiring() == null) + { + unresolved.add(revision); + } + } + return unresolved; + } + + private synchronized Map getWirings() + { + Map wirings = new HashMap(); + + for (BundleRevision revision : m_revisions) + { + if (revision.getWiring() != null) + { + wirings.put(revision, revision.getWiring()); + } + } + return wirings; + } + + private static void addToSingletonMap( + Map> singletons, BundleRevision br) + { + List revisions = singletons.get(br.getSymbolicName()); + if (revisions == null) + { + revisions = new ArrayList(); + } + revisions.add(br); + singletons.put(br.getSymbolicName(), revisions); + } + + static class ResolverHookRecord + { + final Map, ResolverHook> m_resolveHookMap; + final Collection m_brWhitelist; + + /** The map passed in must be of an ordered type, so that the iteration order over the values + * is predictable. + */ + ResolverHookRecord(Map, ResolverHook> resolveHookMap, + Collection brWhiteList) + { + m_resolveHookMap = resolveHookMap; + m_brWhitelist = brWhiteList; + } + + Collection getBundleRevisionWhitelist() + { + return m_brWhitelist; + } + + Set> getResolverHookRefs() + { + return m_resolveHookMap.keySet(); + } + + // This slightly over the top implementation to obtain the hooks is to ensure that at the point that + // the actual hook is obtained, the service is still registered. There are CT tests that unregister + // the hook service while iterating over the hooks and expect that the unregistered hook is not called + // in that case. + Iterable getResolverHooks() + { + return new Iterable() + { + @Override + public Iterator iterator() + { + return new Iterator() + { + private final Iterator, ResolverHook>> it = + m_resolveHookMap.entrySet().iterator(); + private Entry, ResolverHook> next = null; + + @Override + public boolean hasNext() + { + if (next == null) + findNext(); + + return next != null; + } + + @Override + public ResolverHook next() + { + if (next == null) + findNext(); + + if (next == null) + throw new NoSuchElementException(); + + ResolverHook hook = next.getValue(); + next = null; + return hook; + } + + // Find the next hook on the iterator, but only if the service is still registered. + // If the service has since been unregistered, skip the hook. + private void findNext() + { + while (it.hasNext()) + { + next = it.next(); + if (next.getKey().getBundle() != null) + return; + else + next = null; + } + } + + @Override + public void remove() + { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/SystemBundle.java b/framework/src/main/java/org/apache/felix/framework/SystemBundle.java deleted file mode 100644 index e19f07ebda9..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/SystemBundle.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework; - -import java.io.InputStream; -import java.util.*; - -import org.apache.felix.framework.cache.SystemBundleArchive; -import org.apache.felix.framework.searchpolicy.R4Export; -import org.apache.felix.framework.searchpolicy.R4Package; -import org.apache.felix.framework.util.FelixConstants; -import org.apache.felix.framework.util.SecureAction; -import org.apache.felix.framework.util.StringMap; -import org.apache.felix.moduleloader.IContentLoader; -import org.osgi.framework.*; - -class SystemBundle extends BundleImpl -{ - private List m_activatorList = null; - private BundleActivator m_activator = null; - private Thread m_shutdownThread = null; - private R4Export[] m_exports = null; - private IContentLoader m_contentLoader = null; - private SecureAction m_secureAction = null; - - protected SystemBundle(Felix felix, BundleInfo info, List activatorList, - SecureAction secureAction) throws BundleException - { - super(felix, info); - - m_secureAction = secureAction; - - // Create an activator list if necessary. - if (activatorList == null) - { - activatorList = new ArrayList(); - } - - // Add the bundle activator for the package admin service. - activatorList.add(new PackageAdminActivator(felix)); - - // Add the bundle activator for the start level service. - activatorList.add(new StartLevelActivator(felix)); - - // Add the bundle activator for the URL Handlers service. - activatorList.add(new URLHandlersActivator(felix)); - - m_activatorList = activatorList; - - // The system bundle exports framework packages as well as - // arbitrary user-defined packages from the system class path. - // We must construct the system bundle's export metadata. - - // Get system property that specifies which class path - // packages should be exported by the system bundle. - R4Package[] classPathPkgs = null; - try - { - classPathPkgs = R4Package.parseImportOrExportHeader( - getFelix().getConfig().get(Constants.FRAMEWORK_SYSTEMPACKAGES)); - } - catch (Exception ex) - { - classPathPkgs = new R4Package[0]; - getFelix().getLogger().log( - Logger.LOG_ERROR, - "Error parsing system bundle export statement.", ex); - } - - // Now, create the list of standard framework exports for - // the system bundle. - m_exports = new R4Export[classPathPkgs.length]; - - // Copy the class path exported packages. - for (int i = 0; i < classPathPkgs.length; i++) - { - m_exports[i] = new R4Export(classPathPkgs[i]); - } - - m_contentLoader = new SystemBundleContentLoader(getFelix().getLogger()); - - StringBuffer exportSB = new StringBuffer(""); - for (int i = 0; i < m_exports.length; i++) - { - if (i > 0) - { - exportSB.append(", "); - } - - exportSB.append(m_exports[i].getName()); - exportSB.append("; version=\""); - exportSB.append(m_exports[i].getVersion().toString()); - exportSB.append("\""); - } - - // Initialize header map as a case insensitive map. - Map map = new StringMap(false); - map.put(FelixConstants.BUNDLE_VERSION, - getFelix().getConfig().get(FelixConstants.FELIX_VERSION_PROPERTY)); - map.put(FelixConstants.BUNDLE_SYMBOLICNAME, - FelixConstants.SYSTEM_BUNDLE_SYMBOLICNAME); - map.put(FelixConstants.BUNDLE_NAME, "System Bundle"); - map.put(FelixConstants.BUNDLE_DESCRIPTION, - "This bundle is system specific; it implements various system services."); - map.put(FelixConstants.EXPORT_PACKAGE, exportSB.toString()); - map.put(FelixConstants.EXPORT_SERVICE, "org.osgi.service.packageadmin.PackageAdmin,org.osgi.service.startlevel.StartLevel"); - ((SystemBundleArchive) getInfo().getArchive()).setManifestHeader(map); - - // TODO: FRAMEWORK - We need some systematic way for framework services - // to add packages and services to the system bundle's headers, something - // that will allow for them to be independently configured. - } - - public R4Export[] getExports() - { - return m_exports; - } - - public IContentLoader getContentLoader() - { - return m_contentLoader; - } - - public synchronized void start() throws BundleException - { - // The system bundle is only started once and it - // is started by the framework. - if (getState() == Bundle.ACTIVE) - { - return; - } - - getInfo().setState(Bundle.STARTING); - - try - { - getInfo().setContext(new BundleContextImpl(getFelix(), this)); - getActivator().start(getInfo().getContext()); - } - catch (Throwable throwable) - { - throw new BundleException( - "Unable to start system bundle.", throwable); - } - - // Do NOT set the system bundle state to active yet, this - // must be done after all other bundles have been restarted. - // This will be done after the framework is initialized. - } - - public synchronized void stop() throws BundleException - { - Object sm = System.getSecurityManager(); - - if (sm != null) - { - ((SecurityManager) sm).checkPermission(new AdminPermission(this, - AdminPermission.EXECUTE)); - } - - // Spec says stop() on SystemBundle should return immediately and - // shutdown framework on another thread. - if (getFelix().getStatus() == Felix.RUNNING_STATUS) - { - // Initial call of stop, so kick off shutdown. - m_shutdownThread = new Thread("FelixShutdown") { - public void run() - { - try - { - getFelix().shutdown(); - } - catch (Exception ex) - { - getFelix().getLogger().log( - Logger.LOG_ERROR, - "SystemBundle: Error while shutting down.", ex); - } - - // Only shutdown the JVM if the framework is running stand-alone. - String embedded = getFelix().getConfig() - .get(FelixConstants.EMBEDDED_EXECUTION_PROP); - boolean isEmbedded = (embedded == null) - ? false : embedded.equals("true"); - if (!isEmbedded) - { - m_secureAction.exit(0); - } - } - }; - getInfo().setState(Bundle.STOPPING); - m_shutdownThread.start(); - } - else if ((getFelix().getStatus() == Felix.STOPPING_STATUS) && - (Thread.currentThread() == m_shutdownThread)) - { - // Callback from shutdown thread, so do our own stop. - try - { - getActivator().stop(getInfo().getContext()); - } - catch (Throwable throwable) - { - throw new BundleException( - "Unable to stop system bundle.", throwable); - } - } - } - - public synchronized void uninstall() throws BundleException - { - throw new BundleException("Cannot uninstall the system bundle."); - } - - public synchronized void update() throws BundleException - { - update(null); - } - - public synchronized void update(InputStream is) throws BundleException - { - Object sm = System.getSecurityManager(); - - if (sm != null) - { - ((SecurityManager) sm).checkPermission(new AdminPermission(this, - AdminPermission.EXECUTE)); - } - - // TODO: This is supposed to stop and then restart the framework. - throw new BundleException("System bundle update not implemented yet."); - } - - protected BundleActivator getActivator() - { - if (m_activator == null) - { - m_activator = new SystemBundleActivator(getFelix(), m_activatorList); - } - return m_activator; - } -} diff --git a/framework/src/main/java/org/apache/felix/framework/SystemBundleActivator.java b/framework/src/main/java/org/apache/felix/framework/SystemBundleActivator.java deleted file mode 100644 index aa4ac1e33a6..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/SystemBundleActivator.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework; - -import java.util.List; - -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; - -class SystemBundleActivator implements BundleActivator -{ - private Felix m_felix = null; - private List m_activatorList = null; - private BundleContext m_context = null; - - SystemBundleActivator(Felix felix, List activatorList) - { - this.m_felix = felix; - this.m_activatorList = activatorList; - } - - public void start(BundleContext context) throws Exception - { - this.m_context = context; - - // Start all activators. - if (m_activatorList != null) - { - for (int i = 0; i < m_activatorList.size(); i++) - { - ((BundleActivator) m_activatorList.get(i)).start(context); - } - } - } - - public void stop(BundleContext context) throws Exception - { - if (m_activatorList != null) - { - // Stop all activators. - for (int i = 0; i < m_activatorList.size(); i++) - { - ((BundleActivator) m_activatorList.get(i)).stop(context); - } - } - } - - public BundleContext getBundleContext() - { - return m_context; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/SystemBundleContentLoader.java b/framework/src/main/java/org/apache/felix/framework/SystemBundleContentLoader.java deleted file mode 100644 index 70a684fe185..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/SystemBundleContentLoader.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; - -import org.apache.felix.moduleloader.*; - -public class SystemBundleContentLoader implements IContentLoader -{ - private Logger m_logger = null; - private ISearchPolicy m_searchPolicy = null; - private IURLPolicy m_urlPolicy = null; - - public SystemBundleContentLoader(Logger logger) - { - m_logger = logger; - } - - public void open() - { - // Nothing needed here. - } - - public void close() - { - // Nothing needed here. - } - - public IContent getContent() - { - return null; - } - - public ISearchPolicy getSearchPolicy() - { - return m_searchPolicy; - } - - public void setSearchPolicy(ISearchPolicy searchPolicy) - { - m_searchPolicy = searchPolicy; - } - - public IURLPolicy getURLPolicy() - { - return m_urlPolicy; - } - - public void setURLPolicy(IURLPolicy urlPolicy) - { - m_urlPolicy = urlPolicy; - } - - public Class getClass(String name) - { - try - { - return getClass().getClassLoader().loadClass(name); - } - catch (ClassNotFoundException ex) - { - m_logger.log( - Logger.LOG_WARNING, - ex.getMessage(), - ex); - } - return null; - } - - public URL getResource(String name) - { - return getClass().getClassLoader().getResource(name); - } - - public boolean hasInputStream(String urlPath) throws IOException - { - return (getClass().getClassLoader().getResource(urlPath) != null); - } - - public InputStream getInputStream(String urlPath) throws IOException - { - return getClass().getClassLoader().getResourceAsStream(urlPath); - } - - public String findLibrary(String name) - { - // No native libs associated with the system bundle. - return null; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/URLHandlers.java b/framework/src/main/java/org/apache/felix/framework/URLHandlers.java index 8fd43f6ba2f..79b13cee35d 100644 --- a/framework/src/main/java/org/apache/felix/framework/URLHandlers.java +++ b/framework/src/main/java/org/apache/felix/framework/URLHandlers.java @@ -1,27 +1,42 @@ /* - * Copyright 2005 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework; -import java.net.*; -import java.util.*; +import java.lang.reflect.Method; +import java.net.ContentHandler; +import java.net.ContentHandlerFactory; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; +import java.util.List; +import java.util.StringTokenizer; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +import static org.apache.felix.framework.util.Util.putIfAbsentAndReturn; -import org.apache.felix.framework.searchpolicy.ContentClassLoader; -import org.apache.felix.framework.util.*; -import org.osgi.framework.BundleContext; +import org.apache.felix.framework.util.FelixConstants; +import org.apache.felix.framework.util.SecureAction; +import org.apache.felix.framework.util.SecurityManagerEx; +import org.osgi.framework.Constants; +import org.osgi.service.url.URLStreamHandlerService; /** *

      @@ -64,188 +79,480 @@ **/ class URLHandlers implements URLStreamHandlerFactory, ContentHandlerFactory { + private static final Class[] CLASS_TYPE = new Class[]{Class.class}; + + private static final Class URLHANDLERS_CLASS = URLHandlers.class; + + private static final SecureAction m_secureAction = new SecureAction(); + + private static volatile SecurityManagerEx m_sm = null; + private static volatile URLHandlers m_handler = null; + + // This maps classloaders of URLHandlers in other classloaders to lists of + // their frameworks. + private final static ConcurrentHashMap> m_classloaderToFrameworkLists = new ConcurrentHashMap>(); + + // The list to hold all enabled frameworks registered with this handlers + private static final CopyOnWriteArrayList m_frameworks = new CopyOnWriteArrayList(); + private static volatile int m_counter = 0; + + private static final ConcurrentHashMap m_contentHandlerCache = new ConcurrentHashMap(); + private static final ConcurrentHashMap m_streamHandlerCache = new ConcurrentHashMap(); + private static final ConcurrentHashMap m_protocolToURL = new ConcurrentHashMap(); + + private static volatile URLStreamHandlerFactory m_streamHandlerFactory; + private static volatile ContentHandlerFactory m_contentHandlerFactory; private static final String STREAM_HANDLER_PACKAGE_PROP = "java.protocol.handler.pkgs"; - private static final String CONTENT_HANDLER_PACKAGE_PROP = "java.content.handler.pkgs"; - private static final String DEFAULT_STREAM_HANDLER_PACKAGE = "sun.net.www.protocol"; - - private static final String DEFAULT_CONTENT_HANDLER_PACKAGE = "sun.net.www.content"; - private static String m_lock = new String("string-lock"); - private static SecurityManagerEx m_sm = null; - private static URLHandlers m_handler = null; - private static int m_frameworkCount = 0; - private static List m_frameworkList = null; - private static Map m_streamHandlerCache = null; - private static Map m_contentHandlerCache = null; - - private final static SecureAction m_secureAction = new SecureAction(); + private static final String DEFAULT_STREAM_HANDLER_PACKAGE = "sun.net.www.protocol|com.ibm.oti.net.www.protocol|gnu.java.net.protocol|wonka.net|com.acunia.wonka.net|org.apache.harmony.luni.internal.net.www.protocol|weblogic.utils|weblogic.net|javax.net.ssl|COM.newmonics.www.protocols"; + private static volatile Object m_rootURLHandlers; - /** - *

      - * Only one instance of this class is created in a static initializer - * and that one instance is registered as the stream and content handler - * factories for the JVM. - *

      - **/ - private URLHandlers() + private static final String m_streamPkgs; + private static final ConcurrentHashMap m_builtIn = new ConcurrentHashMap(); + private static final boolean m_loaded; + + static + { + String pkgs = new SecureAction().getSystemProperty(STREAM_HANDLER_PACKAGE_PROP, ""); + m_streamPkgs = (pkgs.equals("")) + ? DEFAULT_STREAM_HANDLER_PACKAGE + : pkgs + "|" + DEFAULT_STREAM_HANDLER_PACKAGE; + m_loaded = (null != URLHandlersStreamHandlerProxy.class) && + (null != URLHandlersContentHandlerProxy.class) && (null != URLStreamHandlerService.class); + } + + private void init(String protocol, URLStreamHandlerFactory factory) { - // No one can create an instance, but we need an instance - // so we can set this as the stream and content handler factory. - URL.setURLStreamHandlerFactory(this); - URLConnection.setContentHandlerFactory(this); + try + { + URLStreamHandler handler = getBuiltInStreamHandler(protocol, factory); + if (handler != null) + { + URL url = new URL(protocol, null, -1, "", handler); + addToCache(m_protocolToURL, protocol, url); + } + } + catch (Throwable ex) + { + // Ignore, this is a best effort (maybe log it or something). + } } /** *

      - * This is a method implementation for the URLStreamHandlerFactory - * interface. It simply creates a stream handler proxy object for the - * specified protocol. It caches the returned proxy; therefore, subsequent - * requests for the same protocol will receive the same handler proxy. + * Only one instance of this class is created per classloader + * and that one instance is registered as the stream and content handler + * factories for the JVM. Unless, we already register one from a different + * classloader. In this case we attach to this root. *

      - * @param protocol the protocol for which a stream handler should be returned. - * @return a stream handler proxy for the specified protocol. **/ - public URLStreamHandler createURLStreamHandler(String protocol) + private URLHandlers() { - synchronized (this) + m_sm = new SecurityManagerEx(); + synchronized (URL.class) { - // See if there is a cached stream handler. - // IMPLEMENTATION NOTE: Caching is not strictly necessary for - // stream handlers since the Java runtime caches them. Caching is - // performed for code consistency between stream and content - // handlers and also because caching behavior may not be guaranteed - // across different JRE implementations. - URLStreamHandler handler = (m_streamHandlerCache == null) - ? null - : (URLStreamHandler) m_streamHandlerCache.get(protocol); - - // If this is the framework's "bundle:" protocol, then return - // a handler for that immediately, since no one else can be - // allowed to deal with it. - if (protocol.equals(FelixConstants.BUNDLE_URL_PROTOCOL)) + URLStreamHandlerFactory currentFactory = null; + try { - handler = new URLHandlersBundleStreamHandler(null); - if (m_streamHandlerCache == null) + currentFactory = (URLStreamHandlerFactory) m_secureAction.swapStaticFieldIfNotClass(URL.class, + URLStreamHandlerFactory.class, URLHANDLERS_CLASS, "streamHandlerLock"); + } + catch (Throwable ex) + { + // Ignore, this is a best effort (maybe log it or something) + } + + init("file", currentFactory); + init("ftp", currentFactory); + init("http", currentFactory); + init("https", currentFactory); + try + { + getBuiltInStreamHandler("jar", currentFactory); + } + catch (Throwable ex) + { + // Ignore, this is a best effort (maybe log it or something) + } + + // Try to preload the jrt handler as we need it from the jvm on java > 8 + if (getFromCache(m_builtIn, "jrt") == null) + { + try + { + // Try to get it directly from the URL class to if possible + Method getURLStreamHandler = m_secureAction.getDeclaredMethod(URL.class,"getURLStreamHandler", new Class[]{String.class}); + URLStreamHandler handler = (URLStreamHandler) m_secureAction.invoke(getURLStreamHandler, null, new Object[]{"jrt"}); + addToCache(m_builtIn, "jrt", handler); + } + catch (Throwable ex) + { + // Ignore, this is a best effort and try to load the normal way + try + { + getBuiltInStreamHandler("jrt", currentFactory); + } + catch (Throwable ex2) + { + // Ignore, this is a best efforts + } + } + } + + if (currentFactory != null) + { + try { - m_streamHandlerCache = new HashMap(); + URL.setURLStreamHandlerFactory(currentFactory); + } + catch (Throwable ex) + { + // Ignore, this is a best effort (maybe log it or something) } - m_streamHandlerCache.put(protocol, handler); - return handler; } - // If there is not cached handler, then search for built-in - // handler or create a new handler proxy. - if (handler == null) + try { - // Check for built-in handlers for the protocol. - String pkgs = m_secureAction.getSystemProperty(STREAM_HANDLER_PACKAGE_PROP, ""); - pkgs = (pkgs.equals("")) - ? DEFAULT_STREAM_HANDLER_PACKAGE - : pkgs + "|" + DEFAULT_STREAM_HANDLER_PACKAGE; - - // Iterate over built-in packages. - StringTokenizer pkgTok = new StringTokenizer(pkgs, "| "); - while (pkgTok.hasMoreTokens()) + URL.setURLStreamHandlerFactory(this); + m_streamHandlerFactory = this; + m_rootURLHandlers = this; + // try to flush the cache (gnu/classpath doesn't do it itself) + try { - String pkg = pkgTok.nextToken().trim(); - String className = pkg + "." + protocol + ".Handler"; - try + m_secureAction.flush(URL.class, URL.class); + } + catch (Throwable t) + { + // Not much we can do + } + } + catch (Error err) + { + try + { + // there already is a factory set so try to swap it with ours. + m_streamHandlerFactory = (URLStreamHandlerFactory) + m_secureAction.swapStaticFieldIfNotClass(URL.class, + URLStreamHandlerFactory.class, URLHANDLERS_CLASS, "streamHandlerLock"); + + if (m_streamHandlerFactory == null) + { + throw err; + } + if (!m_streamHandlerFactory.getClass().getName().equals(URLHANDLERS_CLASS.getName())) { - // If a built-in handler is found then let the - // JRE handle it. - if (m_secureAction.forName(className) != null) + URL.setURLStreamHandlerFactory(this); + m_rootURLHandlers = this; + } + else if (URLHANDLERS_CLASS != m_streamHandlerFactory.getClass()) + { + try { - return null; + m_secureAction.invoke( + m_secureAction.getDeclaredMethod(m_streamHandlerFactory.getClass(), + "registerFrameworkListsForContextSearch", + new Class[]{ClassLoader.class, List.class}), + m_streamHandlerFactory, new Object[]{ URLHANDLERS_CLASS.getClassLoader(), + m_frameworks }); + m_rootURLHandlers = m_streamHandlerFactory; + } + catch (Exception ex) + { + throw new RuntimeException(ex.getMessage()); } } - catch (Exception ex) + } + catch (Exception e) + { + throw err; + } + } + + try + { + URLConnection.setContentHandlerFactory(this); + m_contentHandlerFactory = this; + // try to flush the cache (gnu/classpath doesn't do it itself) + try + { + m_secureAction.flush(URLConnection.class, URLConnection.class); + } + catch (Throwable t) + { + // Not much we can do + } + } + catch (Error err) + { + // there already is a factory set so try to swap it with ours. + try + { + m_contentHandlerFactory = (ContentHandlerFactory) + m_secureAction.swapStaticFieldIfNotClass( + URLConnection.class, ContentHandlerFactory.class, + URLHANDLERS_CLASS, null); + if (m_contentHandlerFactory == null) + { + throw err; + } + if (!m_contentHandlerFactory.getClass().getName().equals( + URLHANDLERS_CLASS.getName())) { - // This could be a class not found exception or an - // instantiation exception, not much we can do in either - // case other than ignore it. + URLConnection.setContentHandlerFactory(this); } } - - // If no cached or built-in content handler, then create a - // proxy handler and cache it. - handler = new URLHandlersStreamHandlerProxy(protocol); - if (m_streamHandlerCache == null) + catch (Exception ex) { - m_streamHandlerCache = new HashMap(); + throw err; } - m_streamHandlerCache.put(protocol, handler); } + } + // are we not the new root? + if (!((m_streamHandlerFactory == this) || !URLHANDLERS_CLASS.getName().equals( + m_streamHandlerFactory.getClass().getName()))) + { + m_sm = null; + m_protocolToURL.clear(); + m_builtIn.clear(); + } + } - return handler; + static void registerFrameworkListsForContextSearch(ClassLoader index, + List frameworkLists) + { + synchronized (URL.class) + { + synchronized (m_classloaderToFrameworkLists) + { + m_classloaderToFrameworkLists.put(index, frameworkLists); + } } } - /** - *

      - * This is a method implementation for the ContentHandlerFactory - * interface. It simply creates a content handler proxy object for the - * specified mime type. It caches the returned proxy; therefore, subsequent - * requests for the same content type will receive the same handler proxy. - *

      - * @param mimeType the mime type for which a content handler should be returned. - * @return a content handler proxy for the specified mime type. - **/ - public ContentHandler createContentHandler(String mimeType) + static void unregisterFrameworkListsForContextSearch(ClassLoader index) { - synchronized (this) + synchronized (URL.class) { - // See if there is a cached content handler. - ContentHandler handler = (m_contentHandlerCache == null) - ? null - : (ContentHandler) m_contentHandlerCache.get(mimeType); - - // If there is not cached handler, then search for built-in - // handler or create a new handler proxy. - if (handler == null) + synchronized (m_classloaderToFrameworkLists) { - // Check for built-in handlers for the mime type. - String pkgs = m_secureAction.getSystemProperty(CONTENT_HANDLER_PACKAGE_PROP, ""); - pkgs = (pkgs.equals("")) - ? DEFAULT_CONTENT_HANDLER_PACKAGE - : pkgs + "|" + DEFAULT_CONTENT_HANDLER_PACKAGE; - - // Remove periods, slashes, and dashes from mime type. - String fixedType = mimeType.replace('.', '_').replace('/', '.').replace('-', '_'); - - // Iterate over built-in packages. - StringTokenizer pkgTok = new StringTokenizer(pkgs, "| "); - while (pkgTok.hasMoreTokens()) + m_classloaderToFrameworkLists.remove(index); + if (m_classloaderToFrameworkLists.isEmpty() ) { - String pkg = pkgTok.nextToken().trim(); - String className = pkg + "." + fixedType; - try + synchronized (m_frameworks) { - // If a built-in handler is found then let the - // JRE handle it. - if (m_secureAction.forName(className) != null) + if (m_frameworks.isEmpty()) { - return null; + try + { + m_secureAction.swapStaticFieldIfNotClass(URL.class, + URLStreamHandlerFactory.class, null, "streamHandlerLock"); + } + catch (Exception ex) + { + // TODO log this + ex.printStackTrace(); + } + + if (m_streamHandlerFactory.getClass() != URLHANDLERS_CLASS) + { + URL.setURLStreamHandlerFactory(m_streamHandlerFactory); + } + try + { + m_secureAction.swapStaticFieldIfNotClass( + URLConnection.class, ContentHandlerFactory.class, + null, null); + } + catch (Exception ex) + { + // TODO log this + ex.printStackTrace(); + } + + if (m_contentHandlerFactory.getClass() != URLHANDLERS_CLASS) + { + URLConnection.setContentHandlerFactory(m_contentHandlerFactory); + } } } - catch (Exception ex) - { - // This could be a class not found exception or an - // instantiation exception, not much we can do in either - // case other than ignore it. - } } + } + } + } + + private URLStreamHandler getBuiltInStreamHandler(String protocol, URLStreamHandlerFactory factory) + { + URLStreamHandler handler = getFromCache(m_builtIn, protocol); - // If no cached or built-in content handler, then create a - // proxy handler and cache it. - handler = new URLHandlersContentHandlerProxy(mimeType); - if (m_contentHandlerCache == null) + if (handler != null) + { + return handler; + } + + if (factory != null) + { + handler = factory.createURLStreamHandler(protocol); + } + + if (handler == null) + { + // Check for built-in handlers for the mime type. + // Iterate over built-in packages. + handler = loadBuiltInStreamHandler(protocol, null); + } + + if (handler == null) + { + handler = loadBuiltInStreamHandler(protocol, ClassLoader.getSystemClassLoader()); + } + + return addToCache(m_builtIn, protocol, handler); + } + + private URLStreamHandler loadBuiltInStreamHandler(String protocol, ClassLoader classLoader) { + StringTokenizer pkgTok = new StringTokenizer(m_streamPkgs, "| "); + while (pkgTok.hasMoreTokens()) + { + String pkg = pkgTok.nextToken().trim(); + String className = pkg + "." + protocol + ".Handler"; + try + { + // If a built-in handler is found then cache and return it + Class handler = m_secureAction.forName(className, classLoader); + if (handler != null) + { + return (URLStreamHandler) handler.newInstance(); + } + } + catch (Throwable ex) + { + // This could be a class not found exception or an + // instantiation exception, not much we can do in either + // case other than ignore it. + } + } + // This is a workaround for android - Starting with 4.1 the built-in core handler + // are not following the normal naming nore package schema :-( + String androidHandler = null; + if ("file".equalsIgnoreCase(protocol)) + { + androidHandler = "libcore.net.url.FileHandler"; + } + else if ("ftp".equalsIgnoreCase(protocol)) + { + androidHandler = "libcore.net.url.FtpHandler"; + } + else if ("http".equalsIgnoreCase(protocol)) + { + androidHandler = "libcore.net.http.HttpHandler"; + } + else if ("https".equalsIgnoreCase(protocol)) + { + androidHandler = "libcore.net.http.HttpsHandler"; + } + else if ("jar".equalsIgnoreCase(protocol)) + { + androidHandler = "libcore.net.url.JarHandler"; + } + if (androidHandler != null) + { + try + { + // If a built-in handler is found then cache and return it + Class handler = m_secureAction.forName(androidHandler, classLoader); + if (handler != null) { - m_contentHandlerCache = new HashMap(); + return (URLStreamHandler) handler.newInstance(); } - m_contentHandlerCache.put(mimeType, handler); } + catch (Throwable ex) + { + // This could be a class not found exception or an + // instantiation exception, not much we can do in either + // case other than ignore it. + } + } + return null; + } + /** + *

      + * This is a method implementation for the URLStreamHandlerFactory + * interface. It simply creates a stream handler proxy object for the + * specified protocol. It caches the returned proxy; therefore, subsequent + * requests for the same protocol will receive the same handler proxy. + *

      + * @param protocol the protocol for which a stream handler should be returned. + * @return a stream handler proxy for the specified protocol. + **/ + public URLStreamHandler createURLStreamHandler(String protocol) + { + // See if there is a cached stream handler. + // IMPLEMENTATION NOTE: Caching is not strictly necessary for + // stream handlers since the Java runtime caches them. Caching is + // performed for code consistency between stream and content + // handlers and also because caching behavior may not be guaranteed + // across different JRE implementations. + URLStreamHandler handler = getFromCache(m_streamHandlerCache, protocol); + + if (handler != null) + { return handler; } + // If this is the framework's "bundle:" protocol, then return + // a handler for that immediately, since no one else can be + // allowed to deal with it. + if (protocol.equals(FelixConstants.BUNDLE_URL_PROTOCOL)) + { + return new URLHandlersBundleStreamHandler(getFrameworkFromContext(), m_secureAction); + } + + handler = getBuiltInStreamHandler(protocol, + (m_streamHandlerFactory != this) ? m_streamHandlerFactory : null); + + // If built-in content handler, then create a proxy handler. + return addToCache(m_streamHandlerCache, protocol, + new URLHandlersStreamHandlerProxy(protocol, m_secureAction, + handler, getFromCache(m_protocolToURL, protocol))); + } + + /** + *

      + * This is a method implementation for the ContentHandlerFactory + * interface. It simply creates a content handler proxy object for the + * specified mime type. It caches the returned proxy; therefore, subsequent + * requests for the same content type will receive the same handler proxy. + *

      + * @param mimeType the mime type for which a content handler should be returned. + * @return a content handler proxy for the specified mime type. + **/ + public ContentHandler createContentHandler(String mimeType) + { + // See if there is a cached stream handler. + // IMPLEMENTATION NOTE: Caching is not strictly necessary for + // stream handlers since the Java runtime caches them. Caching is + // performed for code consistency between stream and content + // handlers and also because caching behavior may not be guaranteed + // across different JRE implementations. + ContentHandler handler = getFromCache(m_contentHandlerCache, mimeType); + + if (handler != null) + { + return handler; + } + + return addToCache(m_contentHandlerCache, mimeType, + new URLHandlersContentHandlerProxy(mimeType, m_secureAction, + (m_contentHandlerFactory != this) ? m_contentHandlerFactory : null)); + } + + private static V addToCache(ConcurrentHashMap cache, K key, V value) + { + return key != null && value != null ? putIfAbsentAndReturn(cache, key, value) : null; + } + + private static V getFromCache(ConcurrentHashMap cache, K key) + { + return key != null ? cache.get(key) : null; } /** @@ -255,19 +562,14 @@ public ContentHandler createContentHandler(String mimeType) *

      * @param framework the framework instance to be added to the instance * registry. - * @param context the system bundle context associated with the framework - * instance. * @param enable a flag indicating whether or not the framework wants to * enable the URL Handlers service. **/ - public static void registerInstance( - Felix framework, BundleContext context, boolean enable) + public static void registerFrameworkInstance(Felix framework, boolean enable) { - synchronized (m_lock) + boolean register = false; + synchronized (m_frameworks) { - // Increment framework instance count. - m_frameworkCount++; - // If the URL Handlers service is not going to be enabled, // then return immediately. if (enable) @@ -275,19 +577,37 @@ public static void registerInstance( // We need to create an instance if this is the first // time this method is called, which will set the handler // factories. - if (m_handler == null) + if (m_handler == null ) { - m_sm = new SecurityManagerEx(); - m_handler = new URLHandlers(); + register = true; } - - // Create the framework list, if necessary, and add the - // new framework instance to it. - if (m_frameworkList == null) + else { - m_frameworkList = new ArrayList(); + m_frameworks.add(framework); + m_counter++; + } + } + else + { + m_counter++; + } + } + if (register) + { + synchronized (URL.class) + { + synchronized (m_classloaderToFrameworkLists) + { + synchronized (m_frameworks) + { + if (m_handler == null ) + { + m_handler = new URLHandlers(); + } + m_frameworks.add(framework); + m_counter++; + } } - m_frameworkList.add(framework); } } } @@ -300,14 +620,59 @@ public static void registerInstance( * @param framework the framework instance to be removed from the instance * registry. **/ - public static void unregisterInstance(Felix framework) + public static void unregisterFrameworkInstance(Object framework) { - synchronized (m_lock) + boolean unregister = false; + synchronized (m_frameworks) + { + if (m_frameworks.contains(framework)) + { + if (m_frameworks.size() == 1 && m_handler != null) + { + unregister = true; + } + else + { + m_frameworks.remove(framework); + m_counter--; + } + } + else + { + m_counter--; + } + } + if (unregister) { - m_frameworkCount--; - if (m_frameworkList != null) + synchronized (URL.class) { - m_frameworkList.remove(framework); + synchronized (m_classloaderToFrameworkLists) + { + synchronized (m_frameworks) + { + m_frameworks.remove(framework); + m_counter--; + if (m_frameworks.isEmpty() && m_handler != null) + { + + m_handler = null; + try + { + m_secureAction.invoke(m_secureAction.getDeclaredMethod( + m_rootURLHandlers.getClass(), + "unregisterFrameworkListsForContextSearch", + new Class[]{ ClassLoader.class}), + m_rootURLHandlers, + new Object[] {URLHANDLERS_CLASS.getClassLoader()}); + } + catch (Exception e) + { + // This should not happen + e.printStackTrace(); + } + } + } + } } } } @@ -323,50 +688,122 @@ public static void unregisterInstance(Felix framework) * @return the system bundle context associated with the caller or * null if no associated framework was found. **/ - public static Felix getFrameworkFromContext() + public static Object getFrameworkFromContext() { - synchronized (m_lock) + // This is a hack. The idea is to return the only registered framework quickly + int attempts = 0; + while (m_classloaderToFrameworkLists.isEmpty() && (m_counter == 1) && (m_frameworks.size() == 1)) { - if (m_frameworkList != null) + Object framework = m_frameworks.get(0); + + if (framework != null) { - // First, perform a simple short cut, if there is only - // one framework instance registered, assume that this - // is the bundle context to be returned and just return - // it immediately. - if ((m_frameworkList.size() == 1) && (m_frameworkCount == 1)) + return framework; + } + else if (attempts++ > 3) + { + break; + } + } + + // get the current class call stack. + Class[] stack = m_sm.getClassContext(); + // Find the first class that is loaded from a bundle. + Class targetClass = null; + ClassLoader targetClassLoader = null; + for (int i = 0; i < stack.length; i++) + { + ClassLoader classLoader = m_secureAction.getClassLoader(stack[i]); + if (classLoader != null) + { + String name = classLoader.getClass().getName(); + if (name.startsWith("org.apache.felix.framework.ModuleImpl$ModuleClassLoader") + || name.equals("org.apache.felix.framework.searchpolicy.ContentClassLoader") + || name.startsWith("org.apache.felix.framework.BundleWiringImpl$BundleClassLoader")) { - return (Felix) m_frameworkList.get(0); + targetClass = stack[i]; + targetClassLoader = classLoader; + break; } - - // If there is more than one registered framework instance, - // then get the current class call stack. - Class[] stack = m_sm.getClassContext(); - // Find the first class that is loaded from a bundle. - Class targetClass = null; - for (int i = 0; i < stack.length; i++) + } + } + + // If we found a class loaded from a bundle, then iterate + // over the framework instances and see which framework owns + // the bundle that loaded the class. + if (targetClass != null) + { + ClassLoader index = m_secureAction.getClassLoader(targetClassLoader.getClass()); + + List frameworks = (List) m_classloaderToFrameworkLists.get(index); + + if ((frameworks == null) && (index == URLHANDLERS_CLASS.getClassLoader())) + { + frameworks = m_frameworks; + } + if (frameworks != null) + { + // Check the registry of framework instances + for (Object framework : frameworks) { - if (stack[i].getClassLoader() instanceof ContentClassLoader) + try { - targetClass = stack[i]; - break; + if (m_secureAction.invoke( + m_secureAction.getDeclaredMethod(framework.getClass(), + "getBundle", CLASS_TYPE), + framework, new Object[]{targetClass}) != null) + { + return framework; + } } + catch (Exception ex) + { + // This should not happen but if it does there is + // not much we can do other then ignore it. + // Maybe log this or something. + ex.printStackTrace(); + } + } + } + } + return null; + } + + public static Object getFrameworkFromContext(String uuid) + { + if (uuid != null) + { + for (Felix framework : m_frameworks) + { + if (uuid.equals(framework._getProperty(Constants.FRAMEWORK_UUID))) + { + return framework; } - // If we found a class loaded from a bundle, then iterate - // over the framework instances and see which framework owns - // the bundle that loaded the class. - if (targetClass != null) + } + for (List frameworks : m_classloaderToFrameworkLists.values()) + { + for (Object framework : frameworks) { - // Check the registry of framework instances - for (int i = 0; i < m_frameworkList.size(); i++) + try { - if (((Felix) m_frameworkList.get(i)).getBundle(targetClass) != null) + if (uuid.equals( + m_secureAction.invoke( + m_secureAction.getDeclaredMethod(framework.getClass(),"getProperty", new Class[]{String.class}), + framework, new Object[]{Constants.FRAMEWORK_UUID}))) { - return (Felix) m_frameworkList.get(i); + return framework; } } + catch (Exception ex) + { + // This should not happen but if it does there is + // not much we can do other then ignore it. + // Maybe log this or something. + ex.printStackTrace(); + } } } - return null; } + return getFrameworkFromContext(); } -} \ No newline at end of file +} diff --git a/framework/src/main/java/org/apache/felix/framework/URLHandlersActivator.java b/framework/src/main/java/org/apache/felix/framework/URLHandlersActivator.java index 8851a6acded..63101aad2e8 100644 --- a/framework/src/main/java/org/apache/felix/framework/URLHandlersActivator.java +++ b/framework/src/main/java/org/apache/felix/framework/URLHandlersActivator.java @@ -1,24 +1,32 @@ /* - * Copyright 2005 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework; +import java.net.ContentHandler; +import java.util.Map; +import java.util.Set; + import org.apache.felix.framework.util.FelixConstants; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.url.URLStreamHandlerService; /** *

      @@ -31,11 +39,12 @@ **/ class URLHandlersActivator implements BundleActivator { - private Felix m_framework = null; - private BundleContext m_context = null; + private final Map m_configMap; + private final Felix m_framework; - public URLHandlersActivator(Felix framework) + public URLHandlersActivator(Map configMap, Felix framework) { + m_configMap = configMap; m_framework = framework; } @@ -43,20 +52,71 @@ public URLHandlersActivator(Felix framework) // Bundle activator methods. // + @Override public void start(BundleContext context) { - m_context = context; // Only register the framework with the URL Handlers service // if the service is enabled. - boolean enable = (m_framework.getConfig().get( + boolean enable = (m_configMap.get( FelixConstants.SERVICE_URLHANDLERS_PROP) == null) ? true - : !m_framework.getConfig().get(FelixConstants.SERVICE_URLHANDLERS_PROP).equals("false"); - URLHandlers.registerInstance(m_framework, m_context, enable); + : !m_configMap.get(FelixConstants.SERVICE_URLHANDLERS_PROP).equals("false"); + + if (enable) + { + m_framework.setURLHandlersActivator(this); + } + URLHandlers.registerFrameworkInstance(m_framework, enable); } + @Override public void stop(BundleContext context) { - URLHandlers.unregisterInstance(m_framework); + URLHandlers.unregisterFrameworkInstance(m_framework); + m_framework.setURLHandlersActivator(null); + } + + protected Object getStreamHandlerService(String protocol) + { + return get( + m_framework.getHookRegistry().getHooks(URLStreamHandlerService.class), + "url.handler.protocol", protocol); + } + + protected Object getContentHandlerService(String mimeType) + { + return get( + m_framework.getHookRegistry().getHooks(ContentHandler.class), + "url.content.mimetype", mimeType); + } + + private S get(Set> hooks, String key, String value) + { + Object service = null; + if (!hooks.isEmpty()) + { + for (ServiceReference ref : hooks) + { + Object values = ref.getProperty(key); + if (values instanceof String[]) + { + for (int valueIdx = 0; + (valueIdx < ((String[]) values).length) && (service == null); + valueIdx++) + { + if (value.equals(((String[]) values)[valueIdx])) + { + return m_framework.getService(m_framework, ref, false); + } + } + } + else if (value.equals(values)) + { + return m_framework.getService(m_framework, ref, false); + } + } + } + + return null; } } \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/URLHandlersBundleStreamHandler.java b/framework/src/main/java/org/apache/felix/framework/URLHandlersBundleStreamHandler.java index 19f017a5fca..b8801b8fc1f 100644 --- a/framework/src/main/java/org/apache/felix/framework/URLHandlersBundleStreamHandler.java +++ b/framework/src/main/java/org/apache/felix/framework/URLHandlersBundleStreamHandler.java @@ -1,35 +1,186 @@ /* - * Copyright 2005 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework; import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.net.*; +import java.security.Permission; + +import org.apache.felix.framework.util.SecureAction; +import org.apache.felix.framework.util.Util; +import org.osgi.framework.AdminPermission; +import org.osgi.framework.Bundle; class URLHandlersBundleStreamHandler extends URLStreamHandler { - private Felix m_framework = null; + private final Object m_framework; + private final SecureAction m_action; - public URLHandlersBundleStreamHandler(Felix framework) + public URLHandlersBundleStreamHandler(Object framework, SecureAction action) { m_framework = framework; + m_action = action; + } + + public URLHandlersBundleStreamHandler(SecureAction action) + { + m_framework = null; + m_action = action; + } + + protected URLConnection openConnection(URL url) throws IOException + { + if (!"felix".equals(url.getAuthority())) + { + checkPermission(url); + } + Object framework = m_framework; + + if (framework == null) + { + framework = URLHandlers.getFrameworkFromContext(Util.getFrameworkUUIDFromURL(url.getHost())); + } + + if (framework != null) + { + if (framework instanceof Felix) + { + return new URLHandlersBundleURLConnection(url, (Felix) framework); + } + try + { + ClassLoader loader = m_action.getClassLoader(framework.getClass()); + + Class targetClass = loader.loadClass( + URLHandlersBundleURLConnection.class.getName()); + + Constructor constructor = m_action.getConstructor(targetClass, + new Class[]{URL.class, loader.loadClass( + Felix.class.getName())}); + m_action.setAccesssible(constructor); + return (URLConnection) m_action.invoke(constructor, new Object[]{url, framework}); + } + catch (Exception ex) + { + throw new IOException(ex.getMessage()); + } + } + throw new IOException("No framework context found"); } - protected synchronized URLConnection openConnection(URL url) throws IOException + protected void parseURL(URL u, String spec, int start, int limit) { - return new URLHandlersBundleURLConnection(url, m_framework); + super.parseURL(u, spec, start, limit); + + if (checkPermission(u)) + { + super.setURL(u, u.getProtocol(), u.getHost(), u.getPort(), "felix", u.getUserInfo(), u.getPath(), u.getQuery(), u.getRef()); + } + } + + protected String toExternalForm(URL u) + { + StringBuilder result = new StringBuilder(); + result.append(u.getProtocol()); + result.append("://"); + result.append(u.getHost()); + result.append(':'); + result.append(u.getPort()); + if (u.getPath() != null) + { + result.append(u.getPath()); + } + if (u.getQuery() != null) + { + result.append('?'); + result.append(u.getQuery()); + } + if (u.getRef() != null) + { + result.append("#"); + result.append(u.getRef()); + } + return result.toString(); + } + + protected java.net.InetAddress getHostAddress(URL u) + { + return null; + } + + private boolean checkPermission(URL u) + { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) + { + Object framework = m_framework; + if (framework == null) + { + framework = URLHandlers.getFrameworkFromContext(Util.getFrameworkUUIDFromURL(u.getHost())); + } + try { + long bundleId = Util.getBundleIdFromRevisionId(Util.getRevisionIdFromURL(u.getHost())); + + if (framework instanceof Felix) + { + Bundle bundle = ((Felix) framework).getBundle(bundleId); + if (bundle != null) + { + sm.checkPermission(new AdminPermission(bundle, AdminPermission.RESOURCE)); + return true; + } + } + else if (framework != null) + { + Method method = m_action.getDeclaredMethod(framework.getClass(), "getBundle", new Class[]{long.class}); + m_action.setAccesssible(method); + Object bundle = method.invoke(framework, bundleId); + if (bundle != null) + { + ClassLoader loader = m_action.getClassLoader(framework.getClass()); + + sm.checkPermission((Permission) m_action.getConstructor( + loader.loadClass(AdminPermission.class.getName()), + new Class[] {loader.loadClass(Bundle.class.getName()), String.class}).newInstance(bundle, AdminPermission.RESOURCE)); + return true; + } + } + else + { + throw new IOException("No framework context found"); + } + } + catch (SecurityException ex) + { + throw ex; + } + catch (Exception ex) + { + throw new SecurityException(ex); + } + } + else + { + return true; + } + return false; } -} \ No newline at end of file +} diff --git a/framework/src/main/java/org/apache/felix/framework/URLHandlersBundleURLConnection.java b/framework/src/main/java/org/apache/felix/framework/URLHandlersBundleURLConnection.java index c8a95ce0572..2cb7ee739a8 100644 --- a/framework/src/main/java/org/apache/felix/framework/URLHandlersBundleURLConnection.java +++ b/framework/src/main/java/org/apache/felix/framework/URLHandlersBundleURLConnection.java @@ -1,18 +1,20 @@ /* - * Copyright 2006 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework; @@ -22,13 +24,17 @@ import java.net.URLConnection; import java.security.Permission; -import org.apache.felix.framework.searchpolicy.ContentLoaderImpl; import org.apache.felix.framework.util.Util; -import org.apache.felix.moduleloader.IModule; +import org.osgi.framework.Bundle; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleRevisions; +import org.osgi.framework.wiring.BundleWiring; class URLHandlersBundleURLConnection extends URLConnection { private Felix m_framework; + private BundleRevision m_targetRevision; + private int m_classPathIdx = -1; private int m_contentLength; private long m_contentTime; private String m_contentType; @@ -38,13 +44,28 @@ public URLHandlersBundleURLConnection(URL url, Felix framework) throws IOException { super(url); + + // If this is an attempt to create a connection to the root of + // the bundle, then throw an exception since this isn't possible. + // We only allow "/" as a valid URL so it can be used as context + // for creating other URLs. + String path = url.getPath(); + if ((path == null) || (path.length() == 0) || path.equals("/")) + { + throw new IOException("Resource does not exist: " + url); + } + m_framework = framework; // If we don't have a framework instance, try to find // one from the call context. if (m_framework == null) { - m_framework = URLHandlers.getFrameworkFromContext(); + Object tmp = URLHandlers.getFrameworkFromContext(Util.getFrameworkUUIDFromURL(url.getHost())); + if (tmp instanceof Felix) + { + m_framework = (Felix) tmp; + } } // If there is still no framework, then error. @@ -52,47 +73,78 @@ public URLHandlersBundleURLConnection(URL url, Felix framework) { throw new IOException("Unable to find framework for URL: " + url); } - // Verify that the resource pointed to be the URL exists. + // Verify that the resource pointed to by the URL exists. // The URL is constructed like this: - // bundle:/// - // Where = . - long bundleId = Util.getBundleIdFromModuleId(url.getHost()); - BundleImpl bundle = (BundleImpl) m_framework.getBundle(bundleId); + // bundle://:/ + // Where = . + long bundleId = Util.getBundleIdFromRevisionId(Util.getRevisionIdFromURL(url.getHost())); + Bundle bundle = m_framework.getBundle(bundleId); if (bundle == null) { throw new IOException("No bundle associated with resource: " + url); } - int revision = Util.getModuleRevisionFromModuleId(url.getHost()); - IModule[] modules = bundle.getInfo().getModules(); - if ((modules == null) || (revision < 0) || (revision >= modules.length) || - !modules[revision].getContentLoader().hasInputStream(url.getPath())) + m_contentTime = bundle.getLastModified(); + + // Get the bundle's revisions to find the target revision. + BundleRevisions revisions = bundle.adapt(BundleRevisions.class); + if ((revisions == null) || revisions.getRevisions().isEmpty()) { throw new IOException("Resource does not exist: " + url); } + + // Search for matching revision name. + for (BundleRevision br : revisions.getRevisions()) + { + if (((BundleRevisionImpl) br).getId().equals(url.getHost())) + { + m_targetRevision = br; + break; + } + } + + // If not found, assume the current revision. + if (m_targetRevision == null) + { + m_targetRevision = revisions.getRevisions().get(0); + } + + // If the resource cannot be found at the current class path index, + // then search them all in order to see if it can be found. This is + // necessary since the user might create a resource URL from another + // resource URL and not realize they have the wrong class path entry. + // Of course, this approach won't work in cases where there are multiple + // resources with the same path, since it will always find the first + // one on the class path. + m_classPathIdx = url.getPort(); + if (m_classPathIdx < 0) + { + m_classPathIdx = 0; + } + if (!((BundleRevisionImpl) m_targetRevision) + .hasInputStream(m_classPathIdx, url.getPath())) + { + BundleWiring wiring = m_targetRevision.getWiring(); + ClassLoader cl = (wiring != null) ? wiring.getClassLoader() : null; + URL newurl = (cl != null) ? cl.getResource(url.getPath()) : null; + if (newurl == null) + { + throw new IOException("Resource does not exist: " + url); + } + m_classPathIdx = newurl.getPort(); + } } - public void connect() throws IOException + public synchronized void connect() throws IOException { if (!connected) { - // The URL is constructed like this: - // bundle:/// - // Where = . - long bundleId = Util.getBundleIdFromModuleId(url.getHost()); - BundleImpl bundle = (BundleImpl) m_framework.getBundle(bundleId); - if (bundle == null) - { - throw new IOException("No bundle associated with resource: " + url); - } - int revision = Util.getModuleRevisionFromModuleId(url.getHost()); - IModule[] modules = bundle.getInfo().getModules(); - if ((modules == null) || (revision < 0) || (revision >= modules.length)) + if ((m_targetRevision == null) || (m_classPathIdx < 0)) { throw new IOException("Resource does not exist: " + url); } - m_is = bundle.getInfo().getModules()[revision].getContentLoader().getInputStream(url.getPath()); + m_is = ((BundleRevisionImpl) + m_targetRevision).getInputStream(m_classPathIdx, url.getPath()); m_contentLength = (m_is == null) ? 0 : m_is.available(); - m_contentTime = 0L; m_contentType = URLConnection.guessContentTypeFromName(url.getFile()); connected = true; } @@ -101,42 +153,41 @@ public void connect() throws IOException public InputStream getInputStream() throws IOException { - if (!connected) - { - connect(); - } + connect(); + return m_is; } public int getContentLength() { - if (!connected) + try { - try - { - connect(); - } - catch(IOException ex) - { - return -1; - } + connect(); } + catch(IOException ex) + { + return -1; + } + return m_contentLength; } + public long getContentLengthLong() + { + return getContentLength(); + } + public long getLastModified() { - if (!connected) + try { - try - { - connect(); - } - catch(IOException ex) - { - return 0; - } + connect(); } + catch(IOException ex) + { + return 0; + } + if (m_contentTime != -1L) { return m_contentTime; @@ -149,27 +200,40 @@ public long getLastModified() public String getContentType() { - if (!connected) + try { - try - { - connect(); - } - catch (IOException ex) - { - return null; - } + connect(); } + catch (IOException ex) + { + return null; + } + return m_contentType; } public Permission getPermission() { - // TODO: This should probably return a FilePermission + // TODO: SECURITY - This should probably return a FilePermission // to access the bundle JAR file, but we don't have the // necessary information here to construct the absolute // path of the JAR file...so it would take some // re-arranging to get this to work. return null; } + + /** + * Retrieve the entry as a URL using standard protocols such as file: and jar: + * + * @return the local URL + */ + URL getLocalURL() + { + if ((m_targetRevision == null) || (m_classPathIdx < 0)) + { + return url; + } + return ((BundleRevisionImpl) + m_targetRevision).getLocalURL(m_classPathIdx, url.getPath()); + } } \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/URLHandlersContentHandlerProxy.java b/framework/src/main/java/org/apache/felix/framework/URLHandlersContentHandlerProxy.java index e17093b1ff6..da8c4a5c4b8 100644 --- a/framework/src/main/java/org/apache/felix/framework/URLHandlersContentHandlerProxy.java +++ b/framework/src/main/java/org/apache/felix/framework/URLHandlersContentHandlerProxy.java @@ -1,31 +1,33 @@ /* - * Copyright 2005 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework; +import org.apache.felix.framework.util.SecureAction; + import java.io.IOException; import java.net.ContentHandler; +import java.net.ContentHandlerFactory; import java.net.URLConnection; -import java.util.HashMap; -import java.util.Map; - -import org.osgi.framework.BundleContext; -import org.osgi.service.url.URLConstants; +import java.util.StringTokenizer; +import java.util.concurrent.ConcurrentHashMap; -import sun.security.action.GetIntegerAction; +import static org.apache.felix.framework.util.Util.putIfAbsentAndReturn; /** *

      @@ -51,19 +53,40 @@ **/ class URLHandlersContentHandlerProxy extends ContentHandler { - private Map m_trackerMap = new HashMap(); - private String m_mimeType = null; + private static final Class[] STRING_TYPES = new Class[]{String.class}; - public URLHandlersContentHandlerProxy(String mimeType) + private static final String CONTENT_HANDLER_PACKAGE_PROP = "java.content.handler.pkgs"; + private static final String DEFAULT_CONTENT_HANDLER_PACKAGE = "sun.net.www.content|sun.awt.www.content|com.ibm.oti.net.www.content|gnu.java.net.content|org.apache.harmony.luni.internal.net.www.content|COM.newmonics.www.content"; + + private static final ConcurrentHashMap m_builtIn = new ConcurrentHashMap(); + private static final String m_pkgs; + + static + { + String pkgs = new SecureAction().getSystemProperty(CONTENT_HANDLER_PACKAGE_PROP, ""); + m_pkgs = (pkgs.equals("")) + ? DEFAULT_CONTENT_HANDLER_PACKAGE + : pkgs + "|" + DEFAULT_CONTENT_HANDLER_PACKAGE; + } + + private final ContentHandlerFactory m_factory; + + private final String m_mimeType; + private final SecureAction m_action; + + public URLHandlersContentHandlerProxy(String mimeType, SecureAction action, + ContentHandlerFactory factory) { m_mimeType = mimeType; + m_action = action; + m_factory = factory; } // // ContentHandler interface method. // - public synchronized Object getContent(URLConnection urlc) throws IOException + public Object getContent(URLConnection urlc) throws IOException { ContentHandler svc = getContentHandlerService(); if (svc == null) @@ -87,39 +110,80 @@ public synchronized Object getContent(URLConnection urlc) throws IOException private ContentHandler getContentHandlerService() { // Get the framework instance associated with call stack. - Felix framework = URLHandlers.getFrameworkFromContext(); + Object framework = URLHandlers.getFrameworkFromContext(); - // If the framework has disabled the URL Handlers service, - // then it will not be found so just return null. if (framework == null) { + return getBuiltIn(); + } + try + { + ContentHandler service; + if (framework instanceof Felix) + { + service = (ContentHandler) ((Felix) framework).getContentHandlerService(m_mimeType); + } + else + { + service = (ContentHandler) m_action.invoke( + m_action.getDeclaredMethod(framework.getClass(), "getContentHandlerService", STRING_TYPES), + framework, new Object[]{m_mimeType}); + } + + return (service == null) ? getBuiltIn() : service; + } + catch (Exception ex) + { + // TODO: log this or something + ex.printStackTrace(); return null; } + } + + private ContentHandler getBuiltIn() + { + ContentHandler result = m_builtIn.get(m_mimeType); + + if (result != null) + { + return result; + } + + if (m_factory != null) + { + result = m_factory.createContentHandler(m_mimeType); + if (result != null) + { + return putIfAbsentAndReturn(m_builtIn, m_mimeType, result); + } + } + // Check for built-in handlers for the mime type. + // Remove periods, slashes, and dashes from mime type. + String fixedType = m_mimeType.replace('.', '_').replace('/', '.').replace('-', '_'); - // Get the service tracker for the framework instance or create one. - URLHandlersServiceTracker tracker = - (URLHandlersServiceTracker) m_trackerMap.get(framework); - if (tracker == null) + // Iterate over built-in packages. + StringTokenizer pkgTok = new StringTokenizer(m_pkgs, "| "); + while (pkgTok.hasMoreTokens()) { - // Get the framework's system bundle context. - BundleContext context = - ((SystemBundleActivator) - ((SystemBundle) framework.getBundle(0)).getActivator()) - .getBundleContext(); - // Create a filter for the mime type. - String filter = - "(&(objectClass=" - + ContentHandler.class.getName() - + ")(" - + URLConstants.URL_CONTENT_MIMETYPE - + "=" - + m_mimeType - + "))"; - // Create a simple service tracker for the framework. - tracker = new URLHandlersServiceTracker(context, filter); - // Cache the simple service tracker. - m_trackerMap.put(framework, tracker); + String pkg = pkgTok.nextToken().trim(); + String className = pkg + "." + fixedType; + try + { + // If a built-in handler is found then cache and return it + Class handler = m_action.forName(className, null); + if (handler != null) + { + return putIfAbsentAndReturn(m_builtIn, m_mimeType, + (ContentHandler) handler.newInstance()); + } + } + catch (Exception ex) + { + // This could be a class not found exception or an + // instantiation exception, not much we can do in either + // case other than ignore it. + } } - return (ContentHandler) tracker.getService(); + return null; } } \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/URLHandlersServiceTracker.java b/framework/src/main/java/org/apache/felix/framework/URLHandlersServiceTracker.java deleted file mode 100644 index 1f6dc73b60d..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/URLHandlersServiceTracker.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework; - -import org.apache.felix.framework.util.FelixConstants; -import org.osgi.framework.*; - -/** - *

      - * This class implements a simple service tracker that maintains a - * service object reference to the "best" service available at any - * given time that matches the filter associated with the tracker. - * The best service is the one with the one with the highest ranking - * and lowest service identifier. - *

      -**/ -class URLHandlersServiceTracker -{ - private BundleContext m_context = null; - private String m_filter = null; - private ServiceReference m_ref = null; - private Object m_svcObj = null; - private long m_id = -1; - private int m_rank = -1; - - /** - *

      - * Creates a simple service tracker associated with the specified bundle - * context for services matching the specified filter. - *

      - * @param context the bundle context used for tracking services. - * @param filter the filter used for matching services. - **/ - public URLHandlersServiceTracker(BundleContext context, String filter) - { - m_context = context; - m_filter = filter; - - synchronized (this) - { - // Add a service listener to track service changes - // for services matching the specified filter. - ServiceListener sl = new ServiceListener() { - public void serviceChanged(ServiceEvent event) - { - ServiceReference eventRef = event.getServiceReference(); - if ((event.getType() == ServiceEvent.REGISTERED) || - (event.getType() == ServiceEvent.MODIFIED)) - { - synchronized (URLHandlersServiceTracker.this) - { - Long idObj = (Long) eventRef.getProperty(FelixConstants.SERVICE_ID); - Integer rankObj = (Integer) eventRef.getProperty(FelixConstants.SERVICE_RANKING); - int rank = (rankObj == null) ? 0 : rankObj.intValue(); - if ((rank > m_rank) || - ((rank == m_rank) && (idObj.longValue() < m_id))) - { - if (m_ref != null) - { - m_context.ungetService(m_ref); - } - m_ref = eventRef; - m_rank = rank; - m_id = idObj.longValue(); - m_svcObj = m_context.getService(m_ref); - } - } - } - else if (event.getType() == ServiceEvent.UNREGISTERING) - { - synchronized (URLHandlersServiceTracker.this) - { - if (eventRef == m_ref) - { - selectBestService(); - } - } - } - } - }; - try - { - m_context.addServiceListener(sl, m_filter); - } - catch (InvalidSyntaxException ex) - { - System.out.println("Cannot add service listener." + ex); - } - - // Select the best service object. - selectBestService(); - - } // End of synchronized block. - } - - public Object getService() - { - return m_svcObj; - } - - /** - *

      - * This method selects the highest ranking service object with the - * lowest service identifier out of the services selected by the - * service filter associated with this proxy. This method is called - * to initialize the proxy and any time when the service object - * being used is unregistered. If there is an existing service - * selected when this method is called, it will unget the existing - * service before selecting the best available service. - *

      - **/ - private void selectBestService() - { - // If there is an existing service, then unget it. - if (m_ref != null) - { - m_context.ungetService(m_ref); - m_ref = null; - m_svcObj = null; - m_id = -1; - m_rank = -1; - } - - try - { - // Get all service references matching the service filter - // associated with this proxy. - ServiceReference[] refs = m_context.getServiceReferences(null, m_filter); - // Loop through all service references and select the reference - // with the highest ranking and lower service identifier. - for (int i = 0; (refs != null) && (i < refs.length); i++) - { - Long idObj = (Long) refs[i].getProperty(FelixConstants.SERVICE_ID); - Integer rankObj = (Integer) refs[i].getProperty(FelixConstants.SERVICE_RANKING); - // Ranking value defaults to zero. - int rank = (rankObj == null) ? 0 : rankObj.intValue(); - if ((rank > m_rank) || - ((rank == m_rank) && (idObj.longValue() < m_id))) - { - m_ref = refs[i]; - m_rank = rank; - m_id = idObj.longValue(); - } - } - - // If a service reference was selected, then - // get its service object. - if (m_ref != null) - { - m_svcObj = m_context.getService(m_ref); - } - } - catch (InvalidSyntaxException ex) - { -//TODO: LOGGER. - System.err.println("URLHandlersServiceTracker: " + ex); - } - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/URLHandlersStreamHandlerProxy.java b/framework/src/main/java/org/apache/felix/framework/URLHandlersStreamHandlerProxy.java index ed2e8bb2f22..ef577c677f3 100644 --- a/framework/src/main/java/org/apache/felix/framework/URLHandlersStreamHandlerProxy.java +++ b/framework/src/main/java/org/apache/felix/framework/URLHandlersStreamHandlerProxy.java @@ -1,28 +1,37 @@ /* - * Copyright 2005 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework; import java.io.IOException; -import java.net.*; -import java.util.HashMap; -import java.util.Map; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; -import org.osgi.framework.BundleContext; -import org.osgi.service.url.*; +import org.apache.felix.framework.util.SecureAction; +import org.osgi.service.url.URLStreamHandlerService; +import org.osgi.service.url.URLStreamHandlerSetter; /** *

      @@ -47,104 +56,404 @@ *

      **/ public class URLHandlersStreamHandlerProxy extends URLStreamHandler - implements URLStreamHandlerSetter + implements URLStreamHandlerSetter, InvocationHandler { - private Map m_trackerMap = new HashMap(); - private String m_protocol = null; + private static final Class[] URL_PROXY_CLASS; + private static final Class[] STRING_TYPES = new Class[]{String.class}; + private static final Method EQUALS; + private static final Method GET_DEFAULT_PORT; + private static final Method GET_HOST_ADDRESS; + private static final Method HASH_CODE; + private static final Method HOSTS_EQUAL; + private static final Method OPEN_CONNECTION; + private static final Method OPEN_CONNECTION_PROXY; + private static final Method SAME_FILE; + private static final Method TO_EXTERNAL_FORM; + + static { + SecureAction action = new SecureAction(); + try + { + EQUALS = URLStreamHandler.class.getDeclaredMethod("equals", + new Class[]{URL.class, URL.class}); + action.setAccesssible(EQUALS); + GET_DEFAULT_PORT = URLStreamHandler.class.getDeclaredMethod("getDefaultPort", + (Class[]) null); + action.setAccesssible(GET_DEFAULT_PORT); + GET_HOST_ADDRESS = URLStreamHandler.class.getDeclaredMethod( + "getHostAddress", new Class[]{URL.class}); + action.setAccesssible(GET_HOST_ADDRESS); + HASH_CODE = URLStreamHandler.class.getDeclaredMethod( + "hashCode", new Class[]{URL.class}); + action.setAccesssible(HASH_CODE); + HOSTS_EQUAL = URLStreamHandler.class.getDeclaredMethod( + "hostsEqual", new Class[]{URL.class, URL.class}); + action.setAccesssible(HOSTS_EQUAL); + OPEN_CONNECTION = URLStreamHandler.class.getDeclaredMethod( + "openConnection", new Class[]{URL.class}); + action.setAccesssible(OPEN_CONNECTION); + SAME_FILE = URLStreamHandler.class.getDeclaredMethod( + "sameFile", new Class[]{URL.class, URL.class}); + action.setAccesssible(SAME_FILE); + TO_EXTERNAL_FORM = URLStreamHandler.class.getDeclaredMethod( + "toExternalForm", new Class[]{URL.class}); + action.setAccesssible(TO_EXTERNAL_FORM); + } + catch (Exception ex) + { + throw new RuntimeException(ex.getMessage(), ex); + } - public URLHandlersStreamHandlerProxy(String protocol) + Method open_connection_proxy = null; + Class[] url_proxy_class = null; + try + { + url_proxy_class = new Class[]{URL.class, java.net.Proxy.class}; + open_connection_proxy = URLStreamHandler.class.getDeclaredMethod( + "openConnection", url_proxy_class); + action.setAccesssible(open_connection_proxy); + } + catch (Throwable ex) + { + open_connection_proxy = null; + url_proxy_class = null; + } + OPEN_CONNECTION_PROXY = open_connection_proxy; + URL_PROXY_CLASS = url_proxy_class; + } + + private final Object m_service; + private final SecureAction m_action; + private final URLStreamHandler m_builtIn; + private final URL m_builtInURL; + private final String m_protocol; + + public URLHandlersStreamHandlerProxy(String protocol, + SecureAction action, URLStreamHandler builtIn, URL builtInURL) { m_protocol = protocol; + m_service = null; + m_action = action; + m_builtIn = builtIn; + m_builtInURL = builtInURL; + } + + private URLHandlersStreamHandlerProxy(Object service, SecureAction action) + { + m_protocol = null; + m_service = service; + m_action = action; + m_builtIn = null; + m_builtInURL = null; } // // URLStreamHandler interface methods. // - - protected synchronized boolean equals(URL url1, URL url2) + protected boolean equals(URL url1, URL url2) { - URLStreamHandlerService svc = getStreamHandlerService(); + Object svc = getStreamHandlerService(); if (svc == null) { throw new IllegalStateException( "Unknown protocol: " + url1.getProtocol()); } - return svc.equals(url1, url2); + if (svc instanceof URLStreamHandlerService) + { + return ((URLStreamHandlerService) svc).equals(url1, url2); + } + try + { + return ((Boolean) EQUALS.invoke(svc, new Object[]{url1, url2})).booleanValue(); + } + catch (Exception ex) + { + throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage(), ex); + } } - protected synchronized int getDefaultPort() + protected int getDefaultPort() { - URLStreamHandlerService svc = getStreamHandlerService(); + Object svc = getStreamHandlerService(); if (svc == null) { throw new IllegalStateException("Stream handler unavailable."); } - return svc.getDefaultPort(); + if (svc instanceof URLStreamHandlerService) + { + return ((URLStreamHandlerService) svc).getDefaultPort(); + } + try + { + return ((Integer) GET_DEFAULT_PORT.invoke(svc, null)).intValue(); + } + catch (Exception ex) + { + throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage(), ex); + } } - protected synchronized InetAddress getHostAddress(URL url) + protected InetAddress getHostAddress(URL url) { - URLStreamHandlerService svc = getStreamHandlerService(); + Object svc = getStreamHandlerService(); if (svc == null) { throw new IllegalStateException( "Unknown protocol: " + url.getProtocol()); } - return svc.getHostAddress(url); + if (svc instanceof URLStreamHandlerService) + { + return ((URLStreamHandlerService) svc).getHostAddress(url); + } + try + { + return (InetAddress) GET_HOST_ADDRESS.invoke(svc, new Object[]{url}); + } + catch (Exception ex) + { + throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage(), ex); + } } - protected synchronized int hashCode(URL url) + protected int hashCode(URL url) { - URLStreamHandlerService svc = getStreamHandlerService(); + Object svc = getStreamHandlerService(); if (svc == null) { throw new IllegalStateException( "Unknown protocol: " + url.getProtocol()); } - return svc.hashCode(url); + if (svc instanceof URLStreamHandlerService) + { + return ((URLStreamHandlerService) svc).hashCode(url); + } + try + { + return ((Integer) HASH_CODE.invoke(svc, new Object[]{url})).intValue(); + } + catch (Exception ex) + { + throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage(), ex); + } } - protected synchronized boolean hostsEqual(URL url1, URL url2) + protected boolean hostsEqual(URL url1, URL url2) { - URLStreamHandlerService svc = getStreamHandlerService(); + Object svc = getStreamHandlerService(); if (svc == null) { throw new IllegalStateException( "Unknown protocol: " + url1.getProtocol()); } - return svc.hostsEqual(url1, url2); + if (svc instanceof URLStreamHandlerService) + { + return ((URLStreamHandlerService) svc).hostsEqual(url1, url2); + } + try + { + return ((Boolean) HOSTS_EQUAL.invoke(svc, new Object[]{url1, url2})).booleanValue(); + } + catch (Exception ex) + { + throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage(), ex); + } + } + + protected URLConnection openConnection(URL url) throws IOException + { + Object svc = getStreamHandlerService(); + if (svc == null) + { + throw new MalformedURLException("Unknown protocol: " + url.getProtocol()); + } + if (svc instanceof URLStreamHandlerService) + { + return ((URLStreamHandlerService) svc).openConnection(url); + } + try + { + if ("http".equals(url.getProtocol()) && + "felix.extensions".equals(url.getHost()) && + 9 == url.getPort()) + { + try + { + Object handler = m_action.getDeclaredField( + ExtensionManager.class, "m_extensionManager", null); + + if (handler != null) + { + return (URLConnection) m_action.invoke( + m_action.getMethod(handler.getClass(), + "openConnection", new Class[]{URL.class}), handler, + new Object[]{url}); + } + + throw new IOException("Extensions not supported or ambiguous context."); + } + catch (IOException ex) + { + throw ex; + } + catch (Exception ex) + { + throw new IOException(ex.getMessage()); + } + } + return (URLConnection) OPEN_CONNECTION.invoke(svc, new Object[]{url}); + } + catch (IOException ex) + { + throw ex; + } + catch (Exception ex) + { + throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage(), ex); + } } - protected synchronized URLConnection openConnection(URL url) throws IOException + protected URLConnection openConnection(URL url, java.net.Proxy proxy) throws IOException { - URLStreamHandlerService svc = getStreamHandlerService(); + Object svc = getStreamHandlerService(); if (svc == null) { - throw new MalformedURLException("Unknown protocol: " + url.toString()); + throw new MalformedURLException("Unknown protocol: " + url.getProtocol()); + } + if (svc instanceof URLStreamHandlerService) + { + Method method; + try + { + method = svc.getClass().getMethod("openConnection", URL_PROXY_CLASS); + } + catch (NoSuchMethodException e) + { + RuntimeException rte = new UnsupportedOperationException(e.getMessage()); + rte.initCause(e); + throw rte; + } + try + { + m_action.setAccesssible(method); + return (URLConnection) method.invoke(svc, new Object[]{url, proxy}); + } + catch (Exception e) + { + if (e instanceof IOException) + { + throw (IOException) e; + } + throw new IOException(e.getMessage(), e); + } + } + try + { + return (URLConnection) OPEN_CONNECTION_PROXY.invoke(svc, new Object[]{url, proxy}); + } + catch (Exception ex) + { + if (ex instanceof IOException) + { + throw (IOException) ex; + } + throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage(), ex); } - return svc.openConnection(url); } - protected synchronized void parseURL(URL url, String spec, int start, int limit) + // We use this thread local to detect whether we have a reentrant entry to the parseURL + // method. This can happen do to some difference between gnu/classpath and sun jvms + // For more see inside the method. + private static final ThreadLocal m_loopCheck = new ThreadLocal(); + protected void parseURL(URL url, String spec, int start, int limit) { - URLStreamHandlerService svc = getStreamHandlerService(); + Object svc = getStreamHandlerService(); if (svc == null) { throw new IllegalStateException( "Unknown protocol: " + url.getProtocol()); } - svc.parseURL(this, url, spec, start, limit); + if (svc instanceof URLStreamHandlerService) + { + ((URLStreamHandlerService) svc).parseURL(this, url, spec, start, limit); + } + else + { + try + { + URL test = null; + // In order to cater for built-in urls being over-writable we need to use a + // somewhat strange hack. We use a hidden feature inside the jdk which passes + // the handler of the url given as a context to a new URL to that URL as its + // handler. This way, we can create a new URL which will use the given built-in + // handler to parse the url. Subsequently, we can use the information from that + // URL to call set with the correct values. + if (m_builtInURL != null) + { + // However, if we are on gnu/classpath we have to pass the handler directly + // because the hidden feature is not there. Funnily, the workaround to pass + // pass the handler directly doesn't work on sun as their handler detects + // that it is not the same as the one inside the url and throws an exception + // Luckily it doesn't do that on gnu/classpath. We detect that we need to + // pass the handler directly by using the m_loopCheck thread local to detect + // that we parseURL has been called inside a call to parseURL. + if (m_loopCheck.get() != null) + { + test = new URL(new URL(m_builtInURL, url.toExternalForm()), spec, (URLStreamHandler) svc); + } + else + { + // Set-up the thread local as we don't expect to be called again until we are + // done. Otherwise, we are on gnu/classpath + m_loopCheck.set(Thread.currentThread()); + try + { + test = new URL(new URL(m_builtInURL, url.toExternalForm()), spec); + } + finally + { + m_loopCheck.set(null); + } + } + } + else + { + // We don't have a url with a built-in handler for this but still want to create + // the url with the buil-in handler as we could find one now. This might not + // work for all handlers on sun but it is better then doing nothing. + test = m_action.createURL(url, spec, (URLStreamHandler) svc); + } + + super.setURL(url, test.getProtocol(), test.getHost(), test.getPort(),test.getAuthority(), + test.getUserInfo(), test.getPath(), test.getQuery(), test.getRef()); + } + catch (Exception ex) + { + throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage(), ex); + } + } } - protected synchronized boolean sameFile(URL url1, URL url2) + protected boolean sameFile(URL url1, URL url2) { - URLStreamHandlerService svc = getStreamHandlerService(); + Object svc = getStreamHandlerService(); if (svc == null) { throw new IllegalStateException( "Unknown protocol: " + url1.getProtocol()); } - return svc.sameFile(url1, url2); + if (svc instanceof URLStreamHandlerService) + { + return ((URLStreamHandlerService) svc).sameFile(url1, url2); + } + try + { + return ((Boolean) SAME_FILE.invoke( + svc, new Object[]{url1, url2})).booleanValue(); + } + catch (Exception ex) + { + throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage(), ex); + } } public void setURL( @@ -157,18 +466,93 @@ public void setURL( public void setURL( URL url, String protocol, String host, int port, String file, String ref) { - super.setURL(url, protocol, host, port, null, null, file, null, ref); + super.setURL(url, protocol, host, port, file, ref); } - protected synchronized String toExternalForm(URL url) + protected String toExternalForm(URL url) + { + return toExternalForm(url, getStreamHandlerService()); + } + + private String toExternalForm(URL url, Object svc) { - URLStreamHandlerService svc = getStreamHandlerService(); if (svc == null) { throw new IllegalStateException( "Unknown protocol: " + url.getProtocol()); } - return svc.toExternalForm(url); + if (svc instanceof URLStreamHandlerService) + { + return ((URLStreamHandlerService) svc).toExternalForm(url); + } + try + { + try + { + String result = (String) TO_EXTERNAL_FORM.invoke( + svc, new Object[]{url}); + + // mika does return an invalid format if we have a url with the + // protocol only (://null) - we catch this case now + if ((result != null) && (result.equals(url.getProtocol() + "://null"))) + { + result = url.getProtocol() + ":"; + } + + return result; + } + catch (InvocationTargetException ex) + { + Throwable t = ex.getTargetException(); + if (t instanceof Exception) + { + throw (Exception) t; + } + else if (t instanceof Error) + { + throw (Error) t; + } + else + { + throw new IllegalStateException("Unknown throwable: " + t, t); + } + } + } + catch (NullPointerException ex) + { + // workaround for harmony and possibly J9. The issue is that + // their implementation of URLStreamHandler.toExternalForm() + // assumes that URL.getFile() doesn't return null but in our + // case it can -- hence, we catch the NPE and do the work + // ourselvs. The only difference is that we check whether the + // URL.getFile() is null or not. + StringBuilder answer = new StringBuilder(); + answer.append(url.getProtocol()); + answer.append(':'); + String authority = url.getAuthority(); + if ((authority != null) && (authority.length() > 0)) + { + answer.append("//"); //$NON-NLS-1$ + answer.append(url.getAuthority()); + } + + String file = url.getFile(); + String ref = url.getRef(); + if (file != null) + { + answer.append(file); + } + if (ref != null) + { + answer.append('#'); + answer.append(ref); + } + return answer.toString(); + } + catch (Exception ex) + { + throw new IllegalStateException("Stream handler unavailable due to: " + ex.getMessage(), ex); + } } /** @@ -182,42 +566,78 @@ protected synchronized String toExternalForm(URL url) * associated with the current call stack or null * is no service is available. **/ - private URLStreamHandlerService getStreamHandlerService() - { - // Get the framework instance associated with call stack. - Felix framework = URLHandlers.getFrameworkFromContext(); - - // If the framework has disabled the URL Handlers service, - // then it will not be found so just return null. - if (framework == null) - { - return null; - } - - // Get the service tracker for the framework instance or create one. - URLHandlersServiceTracker tracker = - (URLHandlersServiceTracker) m_trackerMap.get(framework); - if (tracker == null) - { - // Get the framework's system bundle context. - BundleContext context = - ((SystemBundleActivator) - ((SystemBundle) framework.getBundle(0)).getActivator()) - .getBundleContext(); - // Create a filter for the protocol. - String filter = - "(&(objectClass=" - + URLStreamHandlerService.class.getName() - + ")(" - + URLConstants.URL_HANDLER_PROTOCOL - + "=" - + m_protocol - + "))"; - // Create a simple service tracker for the framework. - tracker = new URLHandlersServiceTracker(context, filter); - // Cache the simple service tracker. - m_trackerMap.put(framework, tracker); - } - return (URLStreamHandlerService) tracker.getService(); - } -} \ No newline at end of file + private Object getStreamHandlerService() + { + try + { + // Get the framework instance associated with call stack. + Object framework = URLHandlers.getFrameworkFromContext(); + + if (framework == null) + { + return m_builtIn; + } + + + Object service; + if (framework instanceof Felix) + { + service = ((Felix) framework).getStreamHandlerService(m_protocol); + } + else + { + service = m_action.invoke( + m_action.getDeclaredMethod(framework.getClass(), "getStreamHandlerService", STRING_TYPES), + framework, new Object[]{m_protocol}); + } + + if (service == null) + { + return m_builtIn; + } + if (service instanceof URLStreamHandlerService) + { + return (URLStreamHandlerService) service; + } + + return m_action.createProxy( + m_action.getClassLoader(URLStreamHandlerService.class), + new Class[]{URLStreamHandlerService.class}, + new URLHandlersStreamHandlerProxy(service, m_action)); + } + catch (ThreadDeath td) + { + throw td; + } + catch (Throwable t) + { + // In case that we are inside tomcat - the problem is that the webapp classloader + // creates a new url to load a class. This gets us to this method. Now, if we + // trigger a classload while executing, tomcat is creating a new url and we end-up with + // a loop which is cut short after two iterations (because of a circularclassload). + // We catch this exception (and all others) and just return the built-in handler + // (if we have any) as this way we at least eventually get started (this just means + // that we don't use the potentially provided built-in handler overwrite). + return m_builtIn; + } + } + + public Object invoke(Object obj, Method method, Object[] params) + throws Throwable + { + Class[] types = method.getParameterTypes(); + if (m_service == null) + { + return m_action.invoke(m_action.getMethod(this.getClass(), method.getName(), types), this, params); + } + if ("parseURL".equals(method.getName())) + { + ClassLoader loader = m_action.getClassLoader(m_service.getClass()); + types[0] = loader.loadClass(URLStreamHandlerSetter.class.getName()); + params[0] = m_action.createProxy(loader, new Class[]{types[0]}, + (URLHandlersStreamHandlerProxy) params[0]); + } + return m_action.invokeDirect(m_action.getDeclaredMethod(m_service.getClass(), + method.getName(), types), m_service, params); + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/WovenClassImpl.java b/framework/src/main/java/org/apache/felix/framework/WovenClassImpl.java new file mode 100644 index 00000000000..9459f2fd521 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/WovenClassImpl.java @@ -0,0 +1,454 @@ +/* + * 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.framework; + +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import org.apache.felix.framework.util.Util; +import org.apache.felix.framework.util.manifestparser.ManifestParser; +import org.osgi.framework.AdminPermission; +import org.osgi.framework.PackagePermission; +import org.osgi.framework.hooks.weaving.WovenClass; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleWiring; + +class WovenClassImpl implements WovenClass, List +{ + private final String m_className; + private final BundleWiring m_wiring; + private byte[] m_bytes; + private List m_imports = new ArrayList(); + private Class m_definedClass = null; + private boolean m_isComplete = false; + private int m_state; + + /* package */WovenClassImpl(String className, BundleWiring wiring, + byte[] bytes) + { + m_className = className; + m_wiring = wiring; + m_bytes = bytes; + m_state = TRANSFORMING; + } + + synchronized void complete(Class definedClass, byte[] bytes, + List imports) + { + completeDefine(definedClass); + m_bytes = (bytes == null) ? m_bytes : bytes; + completeImports(imports); + } + + synchronized void completeImports(List imports) + { + m_imports = (imports == null) ? Util.newImmutableList(m_imports) + : Util.newImmutableList(imports); + } + + synchronized void completeDefine(Class definedClass) + { + m_definedClass = definedClass; + } + + public synchronized byte[] getBytes() + { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) + { + sm.checkPermission(new AdminPermission(m_wiring.getBundle(), + AdminPermission.WEAVE)); + } + byte[] bytes = m_bytes; + if (m_isComplete) + { + bytes = new byte[m_bytes.length]; + System.arraycopy(m_bytes, 0, bytes, 0, m_bytes.length); + } + return bytes; + } + + public synchronized void setBytes(byte[] bytes) + { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) + { + sm.checkPermission(new AdminPermission(m_wiring.getBundle(), + AdminPermission.WEAVE)); + } + if (m_state >= TRANSFORMED) + { + throw new IllegalStateException( + "Cannot change bytes after class weaving is completed."); + } else + { + m_bytes = bytes; + } + } + + synchronized List getDynamicImportsInternal() + { + return m_imports; + } + + public synchronized List getDynamicImports() + { + return this; + } + + public synchronized boolean isWeavingComplete() + { + return m_isComplete; + } + + public String getClassName() + { + return m_className; + } + + public ProtectionDomain getProtectionDomain() + { + return ((BundleImpl) m_wiring.getRevision().getBundle()) + .getProtectionDomain(); + } + + public synchronized Class getDefinedClass() + { + return m_definedClass; + } + + public BundleWiring getBundleWiring() + { + return m_wiring; + } + + // + // List implementation for dynamic imports. + // + // Design-wise this could be separated out into a separate type, + // but since it will only ever be used for this purpose it didn't + // appear to make much sense to introduce another type for it. + + public synchronized int size() + { + return m_imports.size(); + } + + public synchronized boolean isEmpty() + { + return m_imports.isEmpty(); + } + + public synchronized boolean contains(Object o) + { + return m_imports.contains(o); + } + + public synchronized Iterator iterator() + { + return m_imports.iterator(); + } + + public synchronized Object[] toArray() + { + return m_imports.toArray(); + } + + public synchronized T[] toArray(T[] ts) + { + return m_imports.toArray(ts); + } + + public synchronized boolean add(String s) + { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) + { + sm.checkPermission(new AdminPermission(m_wiring.getBundle(), + AdminPermission.WEAVE)); + } + if (s != null) + { + try + { + List reqs = ManifestParser + .parseDynamicImportHeader(null, null, s); + } catch (Exception ex) + { + RuntimeException re = new IllegalArgumentException( + "Unable to parse dynamic import."); + re.initCause(ex); + throw re; + } + checkImport(s); + return m_imports.add(s); + } + return false; + } + + private void checkImport(String s) + { + SecurityManager sm = System.getSecurityManager(); + + if (sm != null) + { + sm.checkPermission(new PackagePermission(s, PackagePermission.IMPORT)); + } + } + + public synchronized boolean remove(Object o) + { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) + { + sm.checkPermission(new AdminPermission(m_wiring.getBundle(), + AdminPermission.WEAVE)); + } + return m_imports.remove(o); + } + + public synchronized boolean containsAll(Collection collection) + { + return m_imports.containsAll(collection); + } + + public synchronized boolean addAll(Collection collection) + { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) + { + sm.checkPermission(new AdminPermission(m_wiring.getBundle(), + AdminPermission.WEAVE)); + } + for (String s : collection) + { + try + { + List reqs = ManifestParser + .parseDynamicImportHeader(null, null, s); + } catch (Exception ex) + { + RuntimeException re = new IllegalArgumentException( + "Unable to parse dynamic import."); + re.initCause(ex); + throw re; + } + checkImport(s); + } + return m_imports.addAll(collection); + } + + public synchronized boolean addAll(int i, + Collection collection) + { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) + { + sm.checkPermission(new AdminPermission(m_wiring.getBundle(), + AdminPermission.WEAVE)); + } + for (String s : collection) + { + try + { + List reqs = ManifestParser + .parseDynamicImportHeader(null, null, s); + } catch (Exception ex) + { + RuntimeException re = new IllegalArgumentException( + "Unable to parse dynamic import."); + re.initCause(ex); + throw re; + } + checkImport(s); + } + return m_imports.addAll(i, collection); + } + + public synchronized boolean removeAll(Collection collection) + { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) + { + sm.checkPermission(new AdminPermission(m_wiring.getBundle(), + AdminPermission.WEAVE)); + } + return m_imports.removeAll(collection); + } + + public synchronized boolean retainAll(Collection collection) + { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) + { + sm.checkPermission(new AdminPermission(m_wiring.getBundle(), + AdminPermission.WEAVE)); + } + return m_imports.retainAll(collection); + } + + public synchronized void clear() + { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) + { + sm.checkPermission(new AdminPermission(m_wiring.getBundle(), + AdminPermission.WEAVE)); + } + m_imports.clear(); + } + + public synchronized String get(int i) + { + return m_imports.get(i); + } + + public synchronized String set(int i, String s) + { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) + { + sm.checkPermission(new AdminPermission(m_wiring.getBundle(), + AdminPermission.WEAVE)); + } + try + { + List reqs = ManifestParser + .parseDynamicImportHeader(null, null, s); + } catch (Exception ex) + { + RuntimeException re = new IllegalArgumentException( + "Unable to parse dynamic import."); + re.initCause(ex); + throw re; + } + checkImport(s); + return m_imports.set(i, s); + } + + public synchronized void add(int i, String s) + { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) + { + sm.checkPermission(new AdminPermission(m_wiring.getBundle(), + AdminPermission.WEAVE)); + } + try + { + List reqs = ManifestParser + .parseDynamicImportHeader(null, null, s); + } catch (Exception ex) + { + RuntimeException re = new IllegalArgumentException( + "Unable to parse dynamic import."); + re.initCause(ex); + throw re; + } + checkImport(s); + m_imports.add(i, s); + } + + public synchronized String remove(int i) + { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) + { + sm.checkPermission(new AdminPermission(m_wiring.getBundle(), + AdminPermission.WEAVE)); + } + return m_imports.remove(i); + } + + public synchronized int indexOf(Object o) + { + return m_imports.indexOf(o); + } + + public synchronized int lastIndexOf(Object o) + { + return m_imports.lastIndexOf(o); + } + + public synchronized ListIterator listIterator() + { + return m_imports.listIterator(); + } + + public synchronized ListIterator listIterator(int i) + { + return m_imports.listIterator(i); + } + + public synchronized List subList(int i, int i1) + { + return m_imports.subList(i, i1); + } + + byte[] _getBytes() + { + byte[] bytes = m_bytes; + if (m_isComplete) + { + bytes = new byte[m_bytes.length]; + System.arraycopy(m_bytes, 0, bytes, 0, m_bytes.length); + } + return bytes; + } + + /* + * (non-Javadoc) + * + * @see org.osgi.framework.hooks.weaving.WovenClass#getState() + */ + public synchronized int getState() + { + return m_state; + } + + public synchronized void setState(int state) + { + // Per 56.6.4.13 Weaving complete if state is DEFINED, DEFINE_FAILED, or + // TRANSFORMING_FAILED + if (!m_isComplete + && (state == DEFINED || state == DEFINE_FAILED || state == TRANSFORMING_FAILED)) + { + m_isComplete = true; + if (state == DEFINED || state == DEFINE_FAILED) + { + BundleProtectionDomain pd = (BundleProtectionDomain) + ((BundleRevisionImpl) m_wiring.getRevision()).getProtectionDomain(); + for (String s : m_imports) + { + pd.addWoven(s); + } + } + } + if(state == TRANSFORMED) + { + completeImports(null); + } + m_state = state; + } + +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/cache/BundleArchive.java b/framework/src/main/java/org/apache/felix/framework/cache/BundleArchive.java index 514fdc4bd16..fc527bc7b1b 100644 --- a/framework/src/main/java/org/apache/felix/framework/cache/BundleArchive.java +++ b/framework/src/main/java/org/apache/felix/framework/cache/BundleArchive.java @@ -1,29 +1,32 @@ /* - * Copyright 2006 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework.cache; import java.io.*; -import java.util.Collection; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; import org.apache.felix.framework.Logger; -import org.apache.felix.framework.util.ObjectInputStreamX; -import org.apache.felix.moduleloader.IModule; +import org.apache.felix.framework.util.WeakZipFileFactory; import org.osgi.framework.Bundle; -import org.osgi.framework.BundleActivator; +import org.osgi.framework.Constants; /** *

      @@ -60,7 +63,6 @@ * will be copied. Currently, reference URLs can only refer to "file:" targets. *

      * @see org.apache.felix.framework.cache.BundleCache - * @see org.apache.felix.framework.cache.BundleRevision **/ public class BundleArchive { @@ -68,41 +70,39 @@ public class BundleArchive public static final transient String REFERENCE_PROTOCOL = "reference:"; public static final transient String INPUTSTREAM_PROTOCOL = "inputstream:"; - private static final transient String BUNDLE_ID_FILE = "bundle.id"; - private static final transient String BUNDLE_LOCATION_FILE = "bundle.location"; - private static final transient String CURRENT_LOCATION_FILE = "current.location"; + private static final transient String BUNDLE_INFO_FILE = "bundle.info"; private static final transient String REVISION_LOCATION_FILE = "revision.location"; - private static final transient String BUNDLE_STATE_FILE = "bundle.state"; - private static final transient String BUNDLE_START_LEVEL_FILE = "bundle.startlevel"; - private static final transient String REFRESH_COUNTER_FILE = "refresh.counter"; - private static final transient String BUNDLE_ACTIVATOR_FILE = "bundle.activator"; private static final transient String REVISION_DIRECTORY = "version"; private static final transient String DATA_DIRECTORY = "data"; - private static final transient String ACTIVE_STATE = "active"; - private static final transient String INSTALLED_STATE = "installed"; - private static final transient String UNINSTALLED_STATE = "uninstalled"; - private Logger m_logger = null; + private final Logger m_logger; + private final Map m_configMap; + private final WeakZipFileFactory m_zipFactory; + private final File m_archiveRootDir; + private long m_id = -1; - private File m_archiveRootDir = null; private String m_originalLocation = null; - private String m_currentLocation = null; private int m_persistentState = -1; private int m_startLevel = -1; - private BundleRevision[] m_revisions = null; - private Collection m_trustedCaCerts = null; - - private long m_refreshCount = -1; + private long m_lastModified = -1; /** - *

      - * This constructor is only used by the system bundle archive implementation - * because it is special an is not really an archive. - *

      + * The refresh count field is used when generating the bundle revision + * directory name where native libraries are extracted. This is necessary + * because Sun's JVM requires a one-to-one mapping between native libraries + * and class loaders where the native library is uniquely identified by its + * absolute path in the file system. This constraint creates a problem when + * a bundle is refreshed, because it gets a new class loader. Using the + * refresh counter to generate the name of the bundle revision directory + * resolves this problem because each time bundle is refresh, the native + * library will have a unique name. As a result of the unique name, the JVM + * will then reload the native library without a problem. **/ - BundleArchive() - { - } + private long m_refreshCount = -1; + + // Maps a Long revision number to a BundleRevision. + private final SortedMap m_revisions + = new TreeMap(); /** *

      @@ -121,12 +121,13 @@ public class BundleArchive * @param is input stream from which to read the bundle content. * @throws Exception if any error occurs. **/ - public BundleArchive( - Logger logger, File archiveRootDir, long id, String location, InputStream is, - Collection trustedCaCerts) + public BundleArchive(Logger logger, Map configMap, WeakZipFileFactory zipFactory, + File archiveRootDir, long id, int startLevel, String location, InputStream is) throws Exception { m_logger = logger; + m_configMap = configMap; + m_zipFactory = zipFactory; m_archiveRootDir = archiveRootDir; m_id = id; if (m_id <= 0) @@ -135,13 +136,16 @@ public BundleArchive( "Bundle ID cannot be less than or equal to zero."); } m_originalLocation = location; - m_trustedCaCerts = trustedCaCerts; + m_persistentState = Bundle.INSTALLED; + m_startLevel = startLevel; + m_lastModified = System.currentTimeMillis(); + m_refreshCount = 0; // Save state. initialize(); // Add a revision for the content. - revise(m_originalLocation, is); + reviseInternal(false, new Long(0), m_originalLocation, is); } /** @@ -153,65 +157,62 @@ public BundleArchive( *

      * @param logger the logger to be used by the archive. * @param archiveRootDir the archive root directory for storing state. - * @param id the bundle identifier associated with the archive. + * @param configMap configMap for BundleArchive * @throws Exception if any error occurs. **/ - public BundleArchive(Logger logger, File archiveRootDir, - Collection trustedCaCerts) + public BundleArchive(Logger logger, Map configMap, WeakZipFileFactory zipFactory, + File archiveRootDir) throws Exception { m_logger = logger; + m_configMap = configMap; + m_zipFactory = zipFactory; m_archiveRootDir = archiveRootDir; - m_trustedCaCerts = trustedCaCerts; - - // Add a revision for each one that already exists in the file - // system. The file system might contain more than one revision - // if the bundle was updated in a previous session, but the - // framework was not refreshed; this might happen if the framework - // did not exit cleanly. We must create the existing revisions so - // that they can be properly purged. - int revisionCount = 0; - while (true) + + readBundleInfo(); + + // Add a revision number for each revision that exists in the file + // system. The file system might contain more than one revision if + // the bundle was updated in a previous session, but the framework + // was not refreshed; this might happen if the framework did not + // exit cleanly. We must add the existing revisions so that + // they can be properly purged. + + // Find the existing revision directories, which will be named like: + // "${REVISION_DIRECTORY)${refresh-count}.${revision-number}" + File[] children = m_archiveRootDir.listFiles(); + for (File child : children) { - // Count the number of existing revision directories, which - // will be in a directory named like: - // "${REVISION_DIRECTORY)${refresh-count}.${revision-count}" - File revisionRootDir = new File(m_archiveRootDir, - REVISION_DIRECTORY + getRefreshCount() + "." + revisionCount); - if (!BundleCache.getSecureAction().fileExists(revisionRootDir)) + if (child.getName().startsWith(REVISION_DIRECTORY) + && child.isDirectory()) { - break; + // Determine the revision number and add it to the revision map. + int idx = child.getName().lastIndexOf('.'); + if (idx > 0) + { + Long revNum = Long.decode(child.getName().substring(idx + 1)); + m_revisions.put(revNum, null); + } } - - // Increment the revision count. - revisionCount++; } - // If there are multiple revisions in the file system, then create - // an array that is big enough to hold all revisions minus one; the - // call below to revise() will add the most recent revision. NOTE: We - // do not actually need to add a real revision object for the older - // revisions since they will be purged immediately on framework startup. - if (revisionCount > 1) + if (m_revisions.isEmpty()) { - m_revisions = new BundleRevision[revisionCount - 1]; + throw new Exception( + "No valid revisions in bundle archive directory: " + + archiveRootDir); } - // Add the revision object for the most recent revision. We first try to read - // the location from the current revision - if that fails we likely have - // an old bundle cache and read the location the old way. The next - // revision will update the bundle cache. - // TODO: FRAMEWORK - This try catch block can eventually be deleted when we decide to remove - // support for the old way, then we only need the first call to revise(). - try - { - revise(getRevisionLocation(revisionCount - 1), null); - } - catch (Exception ex) - { - m_logger.log(Logger.LOG_WARNING, getClass().getName() + ": Updating old bundle cache format."); - revise(getCurrentLocation(), null); - } + // Remove the last revision number since the call to reviseInternal() + // will properly add the most recent bundle revision. + // NOTE: We do not actually need to add a real revision object for the + // older revisions since they will be purged immediately on framework + // startup. + Long currentRevNum = m_revisions.lastKey(); + m_revisions.remove(currentRevNum); + + // Add the revision object for the most recent revision. + reviseInternal(true, currentRevNum, getRevisionLocation(currentRevNum), null); } /** @@ -223,38 +224,6 @@ public BundleArchive(Logger logger, File archiveRootDir, **/ public synchronized long getId() throws Exception { - if (m_id > 0) - { - return m_id; - } - - // Read bundle location. - InputStream is = null; - BufferedReader br = null; - try - { - is = BundleCache.getSecureAction() - .getFileInputStream(new File(m_archiveRootDir, BUNDLE_ID_FILE)); - br = new BufferedReader(new InputStreamReader(is)); - m_id = Long.parseLong(br.readLine()); - } - catch (FileNotFoundException ex) - { - // HACK: Get the bundle identifier from the archive root directory - // name, which is of the form "bundle" where is the bundle - // identifier numbers. This is a hack to deal with old archives that - // did not save their bundle identifier, but instead had it passed - // into them. Eventually, this can be removed. - m_id = Long.parseLong( - m_archiveRootDir.getName().substring( - BundleCache.BUNDLE_DIR_PREFIX.length())); - } - finally - { - if (br != null) br.close(); - if (is != null) is.close(); - } - return m_id; } @@ -267,27 +236,7 @@ public synchronized long getId() throws Exception **/ public synchronized String getLocation() throws Exception { - if (m_originalLocation != null) - { - return m_originalLocation; - } - - // Read bundle location. - InputStream is = null; - BufferedReader br = null; - try - { - is = BundleCache.getSecureAction() - .getFileInputStream(new File(m_archiveRootDir, BUNDLE_LOCATION_FILE)); - br = new BufferedReader(new InputStreamReader(is)); - m_originalLocation = br.readLine(); - return m_originalLocation; - } - finally - { - if (br != null) br.close(); - if (is != null) is.close(); - } + return m_originalLocation; } /** @@ -301,49 +250,7 @@ public synchronized String getLocation() throws Exception **/ public synchronized int getPersistentState() throws Exception { - if (m_persistentState >= 0) - { - return m_persistentState; - } - - // Get bundle state file. - File stateFile = new File(m_archiveRootDir, BUNDLE_STATE_FILE); - - // If the state file doesn't exist, then - // assume the bundle was installed. - if (!BundleCache.getSecureAction().fileExists(stateFile)) - { - return Bundle.INSTALLED; - } - - // Read the bundle state. - InputStream is = null; - BufferedReader br = null; - try - { - is = BundleCache.getSecureAction() - .getFileInputStream(stateFile); - br = new BufferedReader(new InputStreamReader(is)); - String s = br.readLine(); - if (s.equals(ACTIVE_STATE)) - { - m_persistentState = Bundle.ACTIVE; - } - else if (s.equals(UNINSTALLED_STATE)) - { - m_persistentState = Bundle.UNINSTALLED; - } - else - { - m_persistentState = Bundle.INSTALLED; - } - return m_persistentState; - } - finally - { - if (br != null) br.close(); - if (is != null) is.close(); - } + return m_persistentState; } /** @@ -357,41 +264,10 @@ else if (s.equals(UNINSTALLED_STATE)) **/ public synchronized void setPersistentState(int state) throws Exception { - // Write the bundle state. - OutputStream os = null; - BufferedWriter bw= null; - try + if (m_persistentState != state) { - os = BundleCache.getSecureAction() - .getFileOutputStream(new File(m_archiveRootDir, BUNDLE_STATE_FILE)); - bw = new BufferedWriter(new OutputStreamWriter(os)); - String s = null; - switch (state) - { - case Bundle.ACTIVE: - s = ACTIVE_STATE; - break; - case Bundle.UNINSTALLED: - s = UNINSTALLED_STATE; - break; - default: - s = INSTALLED_STATE; - break; - } - bw.write(s, 0, s.length()); m_persistentState = state; - } - catch (IOException ex) - { - m_logger.log( - Logger.LOG_ERROR, - getClass().getName() + ": Unable to record state - " + ex); - throw ex; - } - finally - { - if (bw != null) bw.close(); - if (os != null) os.close(); + writeBundleInfo(); } } @@ -404,37 +280,7 @@ public synchronized void setPersistentState(int state) throws Exception **/ public synchronized int getStartLevel() throws Exception { - if (m_startLevel >= 0) - { - return m_startLevel; - } - - // Get bundle start level file. - File levelFile = new File(m_archiveRootDir, BUNDLE_START_LEVEL_FILE); - - // If the start level file doesn't exist, then - // return an error. - if (!BundleCache.getSecureAction().fileExists(levelFile)) - { - return -1; - } - - // Read the bundle start level. - InputStream is = null; - BufferedReader br= null; - try - { - is = BundleCache.getSecureAction() - .getFileInputStream(levelFile); - br = new BufferedReader(new InputStreamReader(is)); - m_startLevel = Integer.parseInt(br.readLine()); - return m_startLevel; - } - finally - { - if (br != null) br.close(); - if (is != null) is.close(); - } + return m_startLevel; } /** @@ -446,183 +292,180 @@ public synchronized int getStartLevel() throws Exception **/ public synchronized void setStartLevel(int level) throws Exception { - // Write the bundle start level. - OutputStream os = null; - BufferedWriter bw = null; - try + if (m_startLevel != level) { - os = BundleCache.getSecureAction() - .getFileOutputStream(new File(m_archiveRootDir, BUNDLE_START_LEVEL_FILE)); - bw = new BufferedWriter(new OutputStreamWriter(os)); - String s = Integer.toString(level); - bw.write(s, 0, s.length()); m_startLevel = level; - } - catch (IOException ex) - { - m_logger.log( - Logger.LOG_ERROR, - getClass().getName() + ": Unable to record start level - " + ex); - throw ex; - } - finally - { - if (bw != null) bw.close(); - if (os != null) os.close(); + writeBundleInfo(); } } /** *

      - * Returns a File object corresponding to the data file - * of the relative path of the specified string. + * Returns the last modification time of this archive. *

      - * @return a File object corresponding to the specified file name. + * @return the last modification time of this archive. * @throws Exception if any error occurs. **/ - public synchronized File getDataFile(String fileName) throws Exception + public synchronized long getLastModified() throws Exception { - // Do some sanity checking. - if ((fileName.length() > 0) && (fileName.charAt(0) == File.separatorChar)) - throw new IllegalArgumentException("The data file path must be relative, not absolute."); - else if (fileName.indexOf("..") >= 0) - throw new IllegalArgumentException("The data file path cannot contain a reference to the \"..\" directory."); - - // Get bundle data directory. - File dataDir = new File(m_archiveRootDir, DATA_DIRECTORY); - // Create the data directory if necessary. - if (!BundleCache.getSecureAction().fileExists(dataDir)) - { - if (!BundleCache.getSecureAction().mkdir(dataDir)) - { - throw new IOException("Unable to create bundle data directory."); - } - } - - // Return the data file. - return new File(dataDir, fileName); + return m_lastModified; } /** *

      - * Returns the serialized activator for this archive. This is an - * extension to the OSGi specification. + * Sets the the last modification time of this archive. *

      - * @return the serialized activator for this archive. + * @param lastModified The time of the last modification to set for + * this archive. According to the OSGi specification this time is + * set each time a bundle is installed, updated or uninstalled. + * * @throws Exception if any error occurs. **/ - public synchronized BundleActivator getActivator(IModule module) - throws Exception + public synchronized void setLastModified(long lastModified) throws Exception { - // Get bundle activator file. - File activatorFile = new File(m_archiveRootDir, BUNDLE_ACTIVATOR_FILE); - // If the activator file doesn't exist, then - // assume there isn't one. - if (!BundleCache.getSecureAction().fileExists(activatorFile)) + if (m_lastModified != lastModified) { - return null; + m_lastModified = lastModified; + writeBundleInfo(); } + } - // Deserialize the activator object. - InputStream is = null; - ObjectInputStreamX ois = null; - try - { - is = BundleCache.getSecureAction() - .getFileInputStream(activatorFile); - ois = new ObjectInputStreamX(is, module); - Object o = ois.readObject(); - return (BundleActivator) o; - } - catch (Exception ex) - { - m_logger.log( - Logger.LOG_ERROR, - getClass().getName() + ": Trying to deserialize - " + ex); - } - finally + /** + * This utility method is used to retrieve the current refresh + * counter value for the bundle. This value is used when generating + * the bundle revision directory name where native libraries are extracted. + * This is necessary because Sun's JVM requires a one-to-one mapping + * between native libraries and class loaders where the native library + * is uniquely identified by its absolute path in the file system. This + * constraint creates a problem when a bundle is refreshed, because it + * gets a new class loader. Using the refresh counter to generate the name + * of the bundle revision directory resolves this problem because each time + * bundle is refresh, the native library will have a unique name. + * As a result of the unique name, the JVM will then reload the + * native library without a problem. + **/ + private long getRefreshCount() throws Exception + { + return m_refreshCount; + } + + /** + * This utility method is used to retrieve the current refresh + * counter value for the bundle. This value is used when generating + * the bundle revision directory name where native libraries are extracted. + * This is necessary because Sun's JVM requires a one-to-one mapping + * between native libraries and class loaders where the native library + * is uniquely identified by its absolute path in the file system. This + * constraint creates a problem when a bundle is refreshed, because it + * gets a new class loader. Using the refresh counter to generate the name + * of the bundle revision directory resolves this problem because each time + * bundle is refresh, the native library will have a unique name. + * As a result of the unique name, the JVM will then reload the + * native library without a problem. + **/ + private void setRefreshCount(long count) + throws Exception + { + if (m_refreshCount != count) { - if (ois != null) ois.close(); - if (is != null) is.close(); + m_refreshCount = count; + writeBundleInfo(); } - - return null; } /** *

      - * Serializes the activator for this archive. + * Returns a File object corresponding to the data file + * of the relative path of the specified string. *

      - * @param obj the activator to serialize. + * @return a File object corresponding to the specified file name. * @throws Exception if any error occurs. **/ - public synchronized void setActivator(Object obj) throws Exception + public File getDataFile(String fileName) throws Exception { - if (!(obj instanceof Serializable)) - { - return; - } + // Get bundle data directory. + File dataDir = new File(m_archiveRootDir, DATA_DIRECTORY); - // Serialize the activator object. - OutputStream os = null; - ObjectOutputStream oos = null; - try - { - os = BundleCache.getSecureAction() - .getFileOutputStream(new File(m_archiveRootDir, BUNDLE_ACTIVATOR_FILE)); - oos = new ObjectOutputStream(os); - oos.writeObject(obj); - } - catch (IOException ex) + // Create the data directory if necessary. + if (!BundleCache.getSecureAction().fileExists(dataDir) && !BundleCache.getSecureAction().mkdirs(dataDir) && !BundleCache.getSecureAction().fileExists(dataDir)) { - m_logger.log( - Logger.LOG_ERROR, - getClass().getName() + ": Unable to serialize activator - " + ex); - throw ex; + throw new IOException("Unable to create bundle data directory."); } - finally + + File dataFile = new File(dataDir, fileName); + + String dataFilePath = BundleCache.getSecureAction().getCanonicalPath(dataFile); + String dataDirPath = BundleCache.getSecureAction().getCanonicalPath(dataDir); + if (!dataFilePath.equals(dataDirPath) && !dataFilePath.startsWith(dataDirPath + File.separatorChar)) { - if (oos != null) oos.close(); - if (os != null) os.close(); + throw new IllegalArgumentException("The data file must be inside the data dir."); } + + // Return the data file. + return dataFile; } /** *

      - * Returns the number of revisions available for this archive. + * Returns the current revision object for the archive. *

      - * @return tthe number of revisions available for this archive. + * @return the current revision object for the archive. **/ - public synchronized int getRevisionCount() + public synchronized Long getCurrentRevisionNumber() { - return (m_revisions == null) ? 0 : m_revisions.length; + return (m_revisions.isEmpty()) ? null : m_revisions.lastKey(); } /** *

      - * Returns the revision object for the specified revision. + * Returns the current revision object for the archive. *

      - * @return the revision object for the specified revision. + * @return the current revision object for the archive. **/ - public synchronized BundleRevision getRevision(int i) + public synchronized BundleArchiveRevision getCurrentRevision() { - if ((i >= 0) && (i < getRevisionCount())) - { - return m_revisions[i]; - } - return null; + return (m_revisions.isEmpty()) ? null : m_revisions.get(m_revisions.lastKey()); + } + + public synchronized boolean isRemovalPending() + { + return (m_revisions.size() > 1); } /** *

      - * This method adds a revision to the archive. The revision is created - * based on the specified location and/or input stream. + * This method adds a revision to the archive using the associated + * location and input stream. If the input stream is null, then the + * location is used a URL to obtain an input stream. *

      * @param location the location string associated with the revision. + * @param is the input stream from which to read the revision. * @throws Exception if any error occurs. **/ public synchronized void revise(String location, InputStream is) throws Exception + { + Long revNum = (m_revisions.isEmpty()) + ? new Long(0) + : new Long(m_revisions.lastKey().longValue() + 1); + + reviseInternal(false, revNum, location, is); + } + + /** + * Actually adds a revision to the bundle archive. This method is also + * used to reload cached bundles too. The revision is given the specified + * revision number and is read from the input stream if supplied or from + * the location URL if not. + * @param isReload if the bundle is being reloaded or not. + * @param revNum the revision number of the revision. + * @param location the location associated with the revision. + * @param is the input stream from which to read the revision. + * @throws Exception if any error occurs. + */ + private void reviseInternal( + boolean isReload, Long revNum, String location, InputStream is) + throws Exception { // If we have an input stream, then we have to use it // no matter what the update location is, so just ignore @@ -632,31 +475,21 @@ public synchronized void revise(String location, InputStream is) { location = "inputstream:"; } - BundleRevision revision = createRevisionFromLocation(location, is); + + // Create a bundle revision for revision number. + BundleArchiveRevision revision = createRevisionFromLocation(location, is, revNum); if (revision == null) { throw new Exception("Unable to revise archive."); } - // Set the current revision location to match. - // TODO: FRAMEWORK - This can eventually be deleted when we removed - // support for the old way of doing things. - setCurrentLocation(location); - - setRevisionLocation(location, (m_revisions == null) ? 0 : m_revisions.length); - - // Add new revision to revision array. - if (m_revisions == null) - { - m_revisions = new BundleRevision[] { revision }; - } - else + if (!isReload) { - BundleRevision[] tmp = new BundleRevision[m_revisions.length + 1]; - System.arraycopy(m_revisions, 0, tmp, 0, m_revisions.length); - tmp[m_revisions.length] = revision; - m_revisions = tmp; + setRevisionLocation(location, revNum); } + + // Add new revision to revision map. + m_revisions.put(revNum, revision); } /** @@ -671,23 +504,20 @@ public synchronized void revise(String location, InputStream is) * @return true if the undo was a success false if there is no previous revision * @throws Exception if any error occurs. */ - public synchronized boolean undoRevise() throws Exception + public synchronized boolean rollbackRevise() throws Exception { // Can only undo the revision if there is more than one. - if (getRevisionCount() <= 1) + if (m_revisions.size() <= 1) { return false; } - String location = getRevisionLocation(m_revisions.length - 2); - - // TODO: FRAMEWORK - This can eventually be deleted when we removed - // support for the old way of doing things. - setCurrentLocation(location); + Long revNum = m_revisions.lastKey(); + BundleArchiveRevision revision = m_revisions.remove(revNum); try { - m_revisions[m_revisions.length - 1].dispose(); + revision.close(); } catch(Exception ex) { @@ -696,30 +526,17 @@ public synchronized boolean undoRevise() throws Exception } File revisionDir = new File(m_archiveRootDir, REVISION_DIRECTORY + - getRefreshCount() + "." + (m_revisions.length - 1)); + getRefreshCount() + "." + revNum.toString()); if (BundleCache.getSecureAction().fileExists(revisionDir)) { BundleCache.deleteDirectoryTree(revisionDir); } - BundleRevision[] tmp = new BundleRevision[m_revisions.length - 1]; - System.arraycopy(m_revisions, 0, tmp, 0, m_revisions.length - 1); - return true; } - public synchronized java.security.cert.Certificate[] getCertificates() - { - return m_revisions[m_revisions.length -1].getCertificates(); - } - - public synchronized String[] getDNChains() - { - return m_revisions[m_revisions.length -1].getDNChains(); - } - - private synchronized String getRevisionLocation(int revision) throws Exception + private synchronized String getRevisionLocation(Long revNum) throws Exception { InputStream is = null; BufferedReader br = null; @@ -727,7 +544,7 @@ private synchronized String getRevisionLocation(int revision) throws Exception { is = BundleCache.getSecureAction().getFileInputStream(new File( new File(m_archiveRootDir, REVISION_DIRECTORY + - getRefreshCount() + "." + revision), REVISION_LOCATION_FILE)); + getRefreshCount() + "." + revNum.toString()), REVISION_LOCATION_FILE)); br = new BufferedReader(new InputStreamReader(is)); return br.readLine(); @@ -739,7 +556,8 @@ private synchronized String getRevisionLocation(int revision) throws Exception } } - private synchronized void setRevisionLocation(String location, int revision) throws Exception + private synchronized void setRevisionLocation(String location, Long revNum) + throws Exception { // Save current revision location. OutputStream os = null; @@ -749,7 +567,7 @@ private synchronized void setRevisionLocation(String location, int revision) thr os = BundleCache.getSecureAction() .getFileOutputStream(new File( new File(m_archiveRootDir, REVISION_DIRECTORY + - getRefreshCount() + "." + revision), REVISION_LOCATION_FILE)); + getRefreshCount() + "." + revNum.toString()), REVISION_LOCATION_FILE)); bw = new BufferedWriter(new OutputStreamWriter(os)); bw.write(location, 0, location.length()); } @@ -760,83 +578,116 @@ private synchronized void setRevisionLocation(String location, int revision) thr } } - /** - *

      - * This method removes all old revisions associated with the archive - * and keeps only the current revision. - *

      - * @throws Exception if any error occurs. - **/ - public synchronized void purge() throws Exception + public synchronized void close() { - // Get the current refresh count. - long refreshCount = getRefreshCount(); // Get the current revision count. - int count = getRevisionCount(); - - // Dispose and delete all but the current revision. - File revisionDir = null; - for (int i = 0; i < count - 1; i++) + for (BundleArchiveRevision revision : m_revisions.values()) { // Dispose of the revision, but this might be null in certain // circumstances, such as if this bundle archive was created // for an existing bundle that was updated, but not refreshed // due to a system crash; see the constructor code for details. - if (m_revisions[i] != null) - { - m_revisions[i].dispose(); - } - revisionDir = new File(m_archiveRootDir, REVISION_DIRECTORY + refreshCount + "." + i); - if (BundleCache.getSecureAction().fileExists(revisionDir)) + if (revision != null) { - BundleCache.deleteDirectoryTree(revisionDir); + try + { + revision.close(); + } + catch (Exception ex) + { + m_logger.log( + Logger.LOG_ERROR, + "Unable to close revision - " + + revision.getRevisionRootDir(), ex); + } } } - - // We still need to dispose the current revision, but we - // don't want to delete it, because we want to rename it - // to the new refresh level. - m_revisions[count - 1].dispose(); - - // Save the current revision location for use later when - // we recreate the revision. - String location = getRevisionLocation(count -1); - - // Increment the refresh count. - setRefreshCount(refreshCount + 1); - - // Rename the current revision directory to be the zero revision - // of the new refresh level. - File currentDir = new File(m_archiveRootDir, REVISION_DIRECTORY + (refreshCount + 1) + ".0"); - revisionDir = new File(m_archiveRootDir, REVISION_DIRECTORY + refreshCount + "." + (count - 1)); - BundleCache.getSecureAction().renameFile(revisionDir, currentDir); - - // Null the revision array since they are all invalid now. - m_revisions = null; - // Finally, recreate the revision for the current location. - BundleRevision revision = createRevisionFromLocation(location, null); - // Create new revision array. - m_revisions = new BundleRevision[] { revision }; } /** *

      - * This method disposes removes the bundle archive directory. + * This method closes any revisions and deletes the bundle archive directory. *

      * @throws Exception if any error occurs. **/ - /* package */ void dispose() throws Exception + public synchronized void closeAndDelete() { + // Close the revisions and delete the archive directory. + close(); if (!BundleCache.deleteDirectoryTree(m_archiveRootDir)) { m_logger.log( Logger.LOG_ERROR, - getClass().getName() - + ": Unable to delete archive directory - " - + m_archiveRootDir); + "Unable to delete archive directory - " + m_archiveRootDir); } } + /** + *

      + * This method removes all old revisions associated with the archive + * and keeps only the current revision. + *

      + * @throws Exception if any error occurs. + **/ + public synchronized void purge() throws Exception + { + // Remember current revision number. + Long currentRevNum = getCurrentRevisionNumber(); + + // Record whether the current revision has native libraries, which + // we'll use later to determine if we need to rename its directory. + Map headers = getCurrentRevision().getManifestHeader(); + + boolean hasNativeLibs = headers != null && getCurrentRevision().getManifestHeader() + .containsKey(Constants.BUNDLE_NATIVECODE); + + // Close all revisions and then delete all but the current revision. + // We don't delete it the current revision, because we want to rename it + // to the new refresh level. + close(); + + // Delete all old revisions. + long refreshCount = getRefreshCount(); + for (Long revNum : m_revisions.keySet()) + { + if (!revNum.equals(currentRevNum)) + { + File revisionDir = new File( + m_archiveRootDir, + REVISION_DIRECTORY + refreshCount + "." + revNum.toString()); + if (BundleCache.getSecureAction().fileExists(revisionDir)) + { + BundleCache.deleteDirectoryTree(revisionDir); + } + } + } + + // If the revision has native libraries, then rename its directory + // to avoid the issue of being unable to load the same native library + // into two different class loaders. + if (hasNativeLibs) + { + // Increment the refresh count. + setRefreshCount(refreshCount + 1); + + // Rename the current revision directory to the new refresh level. + File currentDir = new File(m_archiveRootDir, + REVISION_DIRECTORY + (refreshCount + 1) + "." + currentRevNum.toString()); + File revisionDir = new File(m_archiveRootDir, + REVISION_DIRECTORY + refreshCount + "." + currentRevNum.toString()); + BundleCache.getSecureAction().renameFile(revisionDir, currentDir); + } + + // Clear the revision map since they are all invalid now. + m_revisions.clear(); + + // Recreate the revision for the current location. + BundleArchiveRevision revision = createRevisionFromLocation( + getRevisionLocation(currentRevNum), null, currentRevNum); + // Add new revision to the revision map. + m_revisions.put(currentRevNum, revision); + } + /** *

      * Initializes the bundle archive object by creating the archive @@ -867,89 +718,7 @@ private void initialize() throws Exception throw new IOException("Unable to create archive directory."); } - // Save id. - os = BundleCache.getSecureAction() - .getFileOutputStream(new File(m_archiveRootDir, BUNDLE_ID_FILE)); - bw = new BufferedWriter(new OutputStreamWriter(os)); - bw.write(Long.toString(m_id), 0, Long.toString(m_id).length()); - bw.close(); - os.close(); - - // Save location string. - os = BundleCache.getSecureAction() - .getFileOutputStream(new File(m_archiveRootDir, BUNDLE_LOCATION_FILE)); - bw = new BufferedWriter(new OutputStreamWriter(os)); - bw.write(m_originalLocation, 0, m_originalLocation.length()); - } - finally - { - if (bw != null) bw.close(); - if (os != null) os.close(); - } - } - - /** - *

      - * Returns the current location associated with the bundle archive, - * which is the last location from which the bundle was updated. It is - * necessary to keep track of this so it is possible to determine what - * kind of revision needs to be created when recreating revisions when - * the framework restarts. - *

      - * @return the last update location. - * @throws Exception if any error occurs. - **/ - private String getCurrentLocation() throws Exception - { - if (m_currentLocation != null) - { - return m_currentLocation; - } - - // Read current location. - InputStream is = null; - BufferedReader br = null; - try - { - is = BundleCache.getSecureAction() - .getFileInputStream(new File(m_archiveRootDir, CURRENT_LOCATION_FILE)); - br = new BufferedReader(new InputStreamReader(is)); - m_currentLocation = br.readLine(); - return m_currentLocation; - } - catch (FileNotFoundException ex) - { - return getLocation(); - } - finally - { - if (br != null) br.close(); - if (is != null) is.close(); - } - } - - /** - *

      - * Set the current location associated with the bundle archive, - * which is the last location from which the bundle was updated. It is - * necessary to keep track of this so it is possible to determine what - * kind of revision needs to be created when recreating revisions when - * the framework restarts. - *

      - * @throws Exception if any error occurs. - **/ - private void setCurrentLocation(String location) throws Exception - { - // Save current location. - OutputStream os = null; - BufferedWriter bw = null; - try - { - os = BundleCache.getSecureAction() - .getFileOutputStream(new File(m_archiveRootDir, CURRENT_LOCATION_FILE)); - bw = new BufferedWriter(new OutputStreamWriter(os)); - bw.write(location, 0, location.length()); - m_currentLocation = location; + writeBundleInfo(); } finally { @@ -964,21 +733,23 @@ private void setCurrentLocation(String location) throws Exception *

      * @return the location string associated with this archive. **/ - private BundleRevision createRevisionFromLocation(String location, InputStream is) + private BundleArchiveRevision createRevisionFromLocation( + String location, InputStream is, Long revNum) throws Exception { // The revision directory is named using the refresh count and - // the revision count. The revision count is obvious, but the - // refresh count is less obvious. This is necessary due to how - // native libraries are handled in Java; needless to say, every - // time a bundle is refreshed we must change the name of its - // native libraries so that we can reload them. Thus, we use the - // refresh counter as a way to change the name of the revision - // directory to give native libraries new absolute names. + // the revision number. The revision number is an increasing + // counter of the number of times the bundle was revised. + // The refresh count is necessary due to how native libraries + // are handled in Java; needless to say, every time a bundle is + // refreshed we must change the name of its native libraries so + // that we can reload them. Thus, we use the refresh counter as + // a way to change the name of the revision directory to give + // native libraries new absolute names. File revisionRootDir = new File(m_archiveRootDir, - REVISION_DIRECTORY + getRefreshCount() + "." + getRevisionCount()); + REVISION_DIRECTORY + getRefreshCount() + "." + revNum.toString()); - BundleRevision result = null; + BundleArchiveRevision result = null; try { @@ -992,6 +763,9 @@ private BundleRevision createRevisionFromLocation(String location, InputStream i throw new IOException("Reference URLs can only be files: " + location); } + // Decode any URL escaped sequences. + location = decode(location); + // Make sure the referenced file exists. File file = new File(location.substring(FILE_PROTOCOL.length())); if (!BundleCache.getSecureAction().fileExists(file)) @@ -1004,22 +778,26 @@ private BundleRevision createRevisionFromLocation(String location, InputStream i // flag set to true. if (BundleCache.getSecureAction().isFileDirectory(file)) { - result = new DirectoryRevision(m_logger, revisionRootDir, location); + result = new DirectoryRevision(m_logger, m_configMap, + m_zipFactory, revisionRootDir, location); } else { - result = new JarRevision(m_logger, revisionRootDir, location, true); + result = new JarRevision(m_logger, m_configMap, + m_zipFactory, revisionRootDir, location, true, null); } } else if (location.startsWith(INPUTSTREAM_PROTOCOL)) { // Assume all input streams point to JAR files. - result = new JarRevision(m_logger, revisionRootDir, location, false, is); + result = new JarRevision(m_logger, m_configMap, + m_zipFactory, revisionRootDir, location, false, is); } else { // Anything else is assumed to be a URL to a JAR file. - result = new JarRevision(m_logger, revisionRootDir, location, false); + result = new JarRevision(m_logger, m_configMap, + m_zipFactory, revisionRootDir, location, false, null); } } catch (Exception ex) @@ -1038,54 +816,74 @@ else if (location.startsWith(INPUTSTREAM_PROTOCOL)) throw ex; } - result.setTrustedCaCerts(m_trustedCaCerts); - return result; } - /** - * This utility method is used to retrieve the current refresh - * counter value for the bundle. This value is used when generating - * the bundle revision directory name where native libraries are extracted. - * This is necessary because Sun's JVM requires a one-to-one mapping - * between native libraries and class loaders where the native library - * is uniquely identified by its absolute path in the file system. This - * constraint creates a problem when a bundle is refreshed, because it - * gets a new class loader. Using the refresh counter to generate the name - * of the bundle revision directory resolves this problem because each time - * bundle is refresh, the native library will have a unique name. - * As a result of the unique name, the JVM will then reload the - * native library without a problem. - **/ - private long getRefreshCount() throws Exception + // Method from Harmony java.net.URIEncoderDecoder (luni subproject) + // used by URI to decode uri components. + private static String decode(String s) throws UnsupportedEncodingException { - // If we have already read the refresh counter file, - // then just return the result. - if (m_refreshCount >= 0) + StringBuilder result = new StringBuilder(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + for (int i = 0; i < s.length();) { - return m_refreshCount; + char c = s.charAt(i); + if (c == '%') + { + out.reset(); + do + { + if ((i + 2) >= s.length()) + { + throw new IllegalArgumentException( + "Incomplete % sequence at: " + i); + } + int d1 = Character.digit(s.charAt(i + 1), 16); + int d2 = Character.digit(s.charAt(i + 2), 16); + if ((d1 == -1) || (d2 == -1)) + { + throw new IllegalArgumentException("Invalid % sequence (" + + s.substring(i, i + 3) + + ") at: " + String.valueOf(i)); + } + out.write((byte) ((d1 << 4) + d2)); + i += 3; + } + while ((i < s.length()) && (s.charAt(i) == '%')); + result.append(out.toString("UTF-8")); + continue; + } + result.append(c); + i++; } + return result.toString(); + } - // Get refresh counter file. - File counterFile = new File(m_archiveRootDir, REFRESH_COUNTER_FILE); - - // If the refresh counter file doesn't exist, then - // assume the counter is at zero. - if (!BundleCache.getSecureAction().fileExists(counterFile)) - { - return 0; - } + private void readBundleInfo() throws Exception + { + File infoFile = new File(m_archiveRootDir, BUNDLE_INFO_FILE); - // Read the bundle refresh counter. + // Read the bundle start level. InputStream is = null; - BufferedReader br = null; + BufferedReader br= null; try { is = BundleCache.getSecureAction() - .getFileInputStream(counterFile); + .getFileInputStream(infoFile); br = new BufferedReader(new InputStreamReader(is)); - long counter = Long.parseLong(br.readLine()); - return counter; + + // Read id. + m_id = Long.parseLong(br.readLine()); + // Read location. + m_originalLocation = br.readLine(); + // Read state. + m_persistentState = Integer.parseInt(br.readLine()); + // Read start level. + m_startLevel = Integer.parseInt(br.readLine()); + // Read last modified. + m_lastModified = Long.parseLong(br.readLine()); + // Read refresh count. + m_refreshCount = Long.parseLong(br.readLine()); } finally { @@ -1094,43 +892,47 @@ private long getRefreshCount() throws Exception } } - /** - * This utility method is used to retrieve the current refresh - * counter value for the bundle. This value is used when generating - * the bundle revision directory name where native libraries are extracted. - * This is necessary because Sun's JVM requires a one-to-one mapping - * between native libraries and class loaders where the native library - * is uniquely identified by its absolute path in the file system. This - * constraint creates a problem when a bundle is refreshed, because it - * gets a new class loader. Using the refresh counter to generate the name - * of the bundle revision directory resolves this problem because each time - * bundle is refresh, the native library will have a unique name. - * As a result of the unique name, the JVM will then reload the - * native library without a problem. - **/ - private void setRefreshCount(long counter) - throws Exception + private void writeBundleInfo() throws Exception { - // Get refresh counter file. - File counterFile = new File(m_archiveRootDir, REFRESH_COUNTER_FILE); - - // Write the refresh counter. + // Write the bundle start level. OutputStream os = null; BufferedWriter bw = null; try { os = BundleCache.getSecureAction() - .getFileOutputStream(counterFile); + .getFileOutputStream(new File(m_archiveRootDir, BUNDLE_INFO_FILE)); bw = new BufferedWriter(new OutputStreamWriter(os)); - String s = Long.toString(counter); + + // Write id. + String s = Long.toString(m_id); + bw.write(s, 0, s.length()); + bw.newLine(); + // Write location. + s = (m_originalLocation == null) ? "" : m_originalLocation; + bw.write(s, 0, s.length()); + bw.newLine(); + // Write state. + s = Integer.toString(m_persistentState); + bw.write(s, 0, s.length()); + bw.newLine(); + // Write start level. + s = Integer.toString(m_startLevel); + bw.write(s, 0, s.length()); + bw.newLine(); + // Write last modified. + s = Long.toString(m_lastModified); + bw.write(s, 0, s.length()); + bw.newLine(); + // Write refresh count. + s = Long.toString(m_refreshCount); bw.write(s, 0, s.length()); - m_refreshCount = counter; + bw.newLine(); } catch (IOException ex) { m_logger.log( Logger.LOG_ERROR, - getClass().getName() + ": Unable to write refresh counter: " + ex); + getClass().getName() + ": Unable to cache bundle info - " + ex); throw ex; } finally @@ -1139,4 +941,4 @@ private void setRefreshCount(long counter) if (os != null) os.close(); } } -} \ No newline at end of file +} diff --git a/framework/src/main/java/org/apache/felix/framework/cache/BundleArchiveRevision.java b/framework/src/main/java/org/apache/felix/framework/cache/BundleArchiveRevision.java new file mode 100644 index 00000000000..ba67ea6aaf7 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/cache/BundleArchiveRevision.java @@ -0,0 +1,141 @@ +/* + * 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.framework.cache; + +import java.io.File; +import java.util.Map; + +import org.apache.felix.framework.Logger; + +/** + *

      + * This class implements an abstract revision of a bundle archive. A revision + * is an abstraction of a bundle's actual content and is associated with a + * parent bundle archive. A bundle archive may have multiple revisions assocaited + * with it at one time, since updating a bundle results in a new version of the + * bundle's content until the bundle is refreshed. Upon a refresh, then old + * revisions are then purged. This abstract class is the base class for all + * concrete types of revisions, such as ones for a JAR file or directories. All + * revisions are assigned a root directory into which all of their state should + * be stored, if necessary. Clean up of this directory is the responsibility + * of the parent bundle archive and not of the revision itself. + *

      + * @see org.apache.felix.framework.cache.BundleCache + * @see org.apache.felix.framework.cache.BundleArchive +**/ +public abstract class BundleArchiveRevision +{ + private final Logger m_logger; + private final Map m_configMap; + private final File m_revisionRootDir; + private final String m_location; + + /** + *

      + * This class is abstract and cannot be created. It represents a revision + * of a bundle, i.e., its content. A revision is associated with a particular + * location string, which is typically in URL format. Subclasses of this + * class provide particular functionality, such as a revision in the form + * of a JAR file or a directory. Each revision subclass is expected to use + * the root directory associated with the abstract revision instance to + * store any state; this will ensure that resources used by the revision are + * properly freed when the revision is no longer needed. + *

      + * @param logger a logger for use by the revision. + * @param revisionRootDir the root directory to be used by the revision + * subclass for storing any state. + * @param location the location string associated with the revision. + * @throws Exception if any errors occur. + **/ + public BundleArchiveRevision(Logger logger, Map configMap, File revisionRootDir, String location) + throws Exception + { + m_logger = logger; + m_configMap = configMap; + m_revisionRootDir = revisionRootDir; + m_location = location; + } + + /** + *

      + * Returns the logger for this revision. + *

      + * @return the logger instance for this revision. + **/ + public Logger getLogger() + { + return m_logger; + } + + /** + *

      + * Returns the configuration map for this revision. + *

      + * @return the configuration map for this revision. + **/ + public Map getConfig() + { + return m_configMap; + } + + /** + *

      + * Returns the root directory for this revision. + *

      + * @return the root directory for this revision. + **/ + public File getRevisionRootDir() + { + return m_revisionRootDir; + } + + /** + *

      + * Returns the location string this revision. + *

      + * @return the location string for this revision. + **/ + public String getLocation() + { + return m_location; + } + + /** + *

      + * Returns the main attributes of the JAR file manifest header of the + * revision. The returned map is case insensitive. + *

      + * @return the case-insensitive JAR file manifest header of the revision. + * @throws java.lang.Exception if any error occurs. + **/ + public abstract Map getManifestHeader() throws Exception; + + public abstract Content getContent() throws Exception; + + /** + *

      + * This method is called when the revision is no longer needed. The directory + * associated with the revision will automatically be removed for each + * revision, so this method only needs to be concerned with other issues, + * such as open files. + *

      + * @throws Exception if any error occurs. + **/ + protected abstract void close() throws Exception; +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/cache/BundleCache.java b/framework/src/main/java/org/apache/felix/framework/cache/BundleCache.java index edcf5bdad19..8e63af981f0 100644 --- a/framework/src/main/java/org/apache/felix/framework/cache/BundleCache.java +++ b/framework/src/main/java/org/apache/felix/framework/cache/BundleCache.java @@ -1,27 +1,39 @@ /* - * Copyright 2006 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework.cache; -import java.io.*; -import java.util.*; - import org.apache.felix.framework.Logger; -import org.apache.felix.framework.util.PropertyResolver; import org.apache.felix.framework.util.SecureAction; +import org.apache.felix.framework.util.WeakZipFileFactory; +import org.osgi.framework.Constants; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.ref.SoftReference; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** *

      @@ -29,72 +41,349 @@ * BundleRevision subclasses, implement the Felix bundle cache. * It is possible to configure the default behavior of this class by * passing properties into Felix' constructor. The configuration properties - * for this class are: + * for this class are (properties starting with "felix" are specific + * to Felix, while those starting with "org.osgi" are standard OSGi + * properties): *

      *
        - *
      • felix.cache.bufsize - Sets the buffer size to be used by - * the cache; the default value is 4096. The integer - * value of this string provides control over the size of the - * internal buffer of the disk cache for performance reasons. + *
      • felix.cache.filelimit - The integer value of this string + * sets an upper limit on how many files the cache will open. The default + * value is zero, which means there is no limit. + *
      • + *
      • org.osgi.framework.storage - Sets the directory to use as + * the bundle cache; by default bundle cache directory is + * felix-cache in the current working directory. The value + * should be a valid directory name. The directory name can be either absolute + * or relative. Relative directory names are relative to the current working + * directory. The specified directory will be created if it does + * not exist. *
      • - *
      • felix.cache.dir - Sets the directory to be used by the - * cache as its cache directory. The cache directory is where all - * profile directories are stored and a profile directory is where a - * set of installed bundles are stored. By default, the cache - * directory is .felix in the user's home directory. If - * this property is specified, then its value will be used as the cache - * directory instead of .felix. This directory will be created - * if it does not exist. + *
      • felix.cache.rootdir - Sets the root directory to use to + * calculate the bundle cache directory for relative directory names. If + * org.osgi.framework.storage is set to a relative name, by + * default it is relative to the current working directory. If this + * property is set, then it will be calculated as being relative to + * the specified root directory. *
      • - *
      • felix.cache.profile - Sets the profile name that will be - * used to create a profile directory inside of the cache directory. - * The created directory will contained all installed bundles associated - * with the profile. + *
      • felix.cache.locking - Enables or disables bundle cache locking, + * which is used to prevent concurrent access to the bundle cache. This is + * enabled by default, but on older/smaller JVMs file channel locking is + * not available; set this property to false to disable it. *
      • - *
      • felix.cache.profiledir - Sets the directory to use as the - * profile directory for the bundle cache; by default the profile - * name is used to create a directory in the .felix cache - * directory. If this property is specified, then the cache directory - * and profile name properties are ignored. The specified value of this - * property is used directly as the directory to contain all cached - * bundles. If this property is set, it is not necessary to set the - * cache directory or profile name properties. This directory will be - * created if it does not exist. + *
      • felix.cache.bufsize - Sets the buffer size to be used by + * the cache; the default value is 4096. The integer value of this + * string provides control over the size of the internal buffer of the + * disk cache for performance reasons. *
      • - *
      *

      - * For specific information on how to configure Felix using system properties, - * refer to the Felix usage documentation. + * For specific information on how to configure the Felix framework, refer + * to the Felix framework usage documentation. *

      - * @see org.apache.felix.framework.util.BundleArchive + * @see org.apache.felix.framework.cache.BundleArchive **/ public class BundleCache { public static final String CACHE_BUFSIZE_PROP = "felix.cache.bufsize"; - public static final String CACHE_DIR_PROP = "felix.cache.dir"; - public static final String CACHE_PROFILE_DIR_PROP = "felix.cache.profiledir"; - public static final String CACHE_PROFILE_PROP = "felix.cache.profile"; + public static final String CACHE_ROOTDIR_PROP = "felix.cache.rootdir"; + public static final String CACHE_LOCKING_PROP = "felix.cache.locking"; + public static final String CACHE_FILELIMIT_PROP = "felix.cache.filelimit"; + private static final ThreadLocal m_defaultBuffer = new ThreadLocal(); + private static volatile int DEFAULT_BUFFER = 1024 * 64; - protected static transient int BUFSIZE = 4096; - protected static transient final String CACHE_DIR_NAME = ".felix"; - protected static transient final String BUNDLE_DIR_PREFIX = "bundle"; + private static transient final String CACHE_DIR_NAME = "felix-cache"; + private static transient final String CACHE_ROOTDIR_DEFAULT = "."; + private static transient final String CACHE_LOCK_NAME = "cache.lock"; + static transient final String BUNDLE_DIR_PREFIX = "bundle"; - private PropertyResolver m_cfg = null; - private Logger m_logger = null; - private File m_profileDir = null; - private BundleArchive[] m_archives = null; - private Collection m_trustedCaCerts = null; + private static final SecureAction m_secureAction = new SecureAction(); - private static SecureAction m_secureAction = new SecureAction(); + private final Logger m_logger; + private final Map m_configMap; + private final WeakZipFileFactory m_zipFactory; + private final Object m_lock; - public BundleCache(PropertyResolver cfg, Logger logger, - Collection trustedCaCerts) + public BundleCache(Logger logger, Map configMap) throws Exception { - m_cfg = cfg; m_logger = logger; - m_trustedCaCerts = trustedCaCerts; - initialize(); + m_configMap = configMap; + + int limit = 0; + String limitStr = (String) m_configMap.get(CACHE_FILELIMIT_PROP); + if (limitStr != null) + { + try + { + limit = Integer.parseInt(limitStr); + } + catch (NumberFormatException ex) + { + limit = 0; + } + } + m_zipFactory = new WeakZipFileFactory(limit); + + // Create the cache directory, if it does not exist. + File cacheDir = determineCacheDir(m_configMap); + if (!getSecureAction().fileExists(cacheDir)) + { + if (!getSecureAction().mkdirs(cacheDir)) + { + m_logger.log( + Logger.LOG_ERROR, + "Unable to create cache directory: " + cacheDir); + throw new RuntimeException("Unable to create cache directory."); + } + } + + Object locking = m_configMap.get(CACHE_LOCKING_PROP); + locking = (locking == null) + ? Boolean.TRUE.toString() + : locking.toString().toLowerCase(); + if (locking.equals(Boolean.TRUE.toString())) + { + File lockFile = new File(cacheDir, CACHE_LOCK_NAME); + FileChannel fc = null; + FileOutputStream fos = null; + try + { + if (!getSecureAction().fileExists(lockFile)) + { + fos = getSecureAction().getFileOutputStream(lockFile); + fc = fos.getChannel(); + } + else + { + fos = getSecureAction().getFileOutputStream(lockFile); + fc = fos.getChannel(); + } + } + catch (Exception ex) + { + try + { + if (fos != null) fos.close(); + if (fc != null) fc.close(); + } + catch (Exception ex2) + { + // Ignore. + } + throw new Exception("Unable to create bundle cache lock file: " + ex); + } + try + { + m_lock = fc.tryLock(); + } + catch (Exception ex) + { + throw new Exception("Unable to lock bundle cache: " + ex); + } + } + else + { + m_lock = null; + } + } + + // Parse the main attributes of the manifest of the given jarfile. + // The idea is to not open the jar file as a java.util.jarfile but + // read the mainfest from the zipfile directly and parse it manually + // to use less memory and be faster. + // + // @return the given map for convenience + public static Map getMainAttributes(Map headers, InputStream inputStream, long size) throws Exception + { + if (size > 0) + { + return getMainAttributes(headers, inputStream, (int) (size < Integer.MAX_VALUE ? size : Integer.MAX_VALUE)); + } + else + { + return headers; + } + } + + static byte[] read(InputStream input, long size) throws Exception + { + return read(input, size <= Integer.MAX_VALUE ? (int) size : Integer.MAX_VALUE); + } + + static byte[] read(InputStream input, int size) throws Exception + { + if (size <= 0) + { + return new byte[0]; + } + + byte[] result = new byte[size]; + + Exception exception = null; + try + { + for (int i = input.read(result, 0, size); i != -1 && i < size; i += input.read(result, i, size - i)) + { + + } + } + catch (Exception ex) + { + exception = ex; + } + finally + { + try + { + input.close(); + } + catch (Exception ex) + { + throw exception != null ? exception : ex; + } + } + + return result; + } + + public static Map getMainAttributes(Map headers, InputStream inputStream, int size) throws Exception + { + if (size <= 0) + { + inputStream.close(); + return headers; + } + + // Get the buffer for this thread if there is one already otherwise, + // create one of size DEFAULT_BUFFER (64K) if the manifest is less + // than 64k or of the size of the manifest. + SoftReference ref = (SoftReference) m_defaultBuffer.get(); + byte[] bytes = null; + if (ref != null) + { + bytes = (byte[]) ref.get(); + } + + if (bytes == null) + { + bytes = new byte[size + 1 > DEFAULT_BUFFER ? size + 1 : DEFAULT_BUFFER]; + m_defaultBuffer.set(new SoftReference(bytes)); + } + else if (size + 1 > bytes.length) + { + bytes = new byte[size + 1]; + m_defaultBuffer.set(new SoftReference(bytes)); + } + + // Now read in the manifest in one go into the bytes array. + // The InputStream should be already buffered and can handle up to 64K buffers in one go. + try + { + int i = inputStream.read(bytes); + while (i < size) + { + i += inputStream.read(bytes, i, bytes.length - i); + } + } + finally + { + inputStream.close(); + } + + // Force a new line at the end of the manifest to deal with broken manifest without any line-ending + bytes[size++] = '\n'; + + // Now parse the main attributes. The idea is to do that + // without creating new byte arrays. Therefore, we read through + // the manifest bytes inside the bytes array and write them back into + // the same array unless we don't need them (e.g., \r\n and \n are skipped). + // That allows us to create the strings from the bytes array without the skipped + // chars. We stop as soon as we see a blank line as that denotes that the main + // attributes part is finished. + String key = null; + int last = 0; + int current = 0; + for (int i = 0; i < size; i++) + { + // skip \r and \n if it is followed by another \n + // (we catch the blank line case in the next iteration) + if (bytes[i] == '\r') + { + if ((i + 1 < size) && (bytes[i + 1] == '\n')) + { + continue; + } + } + if (bytes[i] == '\n') + { + if ((i + 1 < size) && (bytes[i + 1] == ' ')) + { + i++; + continue; + } + } + // If we don't have a key yet and see the first : we parse it as the key + // and skip the : that follows it. + if ((key == null) && (bytes[i] == ':')) + { + key = new String(bytes, last, (current - last), "UTF-8"); + if ((i + 1 < size) && (bytes[i + 1] == ' ')) + { + last = current + 1; + continue; + } + else + { + throw new Exception("Manifest error: Missing space separator - " + key); + } + } + // if we are at the end of a line + if (bytes[i] == '\n') + { + // and it is a blank line stop parsing (main attributes are done) + if ((last == current) && (key == null)) + { + break; + } + // Otherwise, parse the value and add it to the map (we throw an + // exception if we don't have a key or the key already exist. + String value = new String(bytes, last, (current - last), "UTF-8"); + if (key == null) + { + throw new Exception("Manifest error: Missing attribute name - " + value); + } + else if (headers.put(key.intern(), value) != null) + { + throw new Exception("Manifest error: Duplicate attribute name - " + key); + } + last = current; + key = null; + } + else + { + // write back the byte if it needs to be included in the key or the value. + bytes[current++] = bytes[i]; + } + } + return headers; + } + + public synchronized void release() + { + if (m_lock != null) + { + try + { + ((FileLock) m_lock).release(); + ((FileLock) m_lock).channel().close(); + } + catch (Exception ex) + { + // Not much we can do here, just log it. + m_logger.log( + Logger.LOG_WARNING, + "Exception releasing bundle cache.", ex); + } + } } /* package */ static SecureAction getSecureAction() @@ -102,55 +391,78 @@ public BundleCache(PropertyResolver cfg, Logger logger, return m_secureAction; } - public synchronized BundleArchive[] getArchives() - throws Exception + public synchronized void delete() throws Exception { - return m_archives; + // Delete the cache directory. + File cacheDir = determineCacheDir(m_configMap); + deleteDirectoryTree(cacheDir); } - public synchronized BundleArchive getArchive(long id) + public BundleArchive[] getArchives() throws Exception { - for (int i = 0; i < m_archives.length; i++) + // Get buffer size value. + try { - if (m_archives[i].getId() == id) + String sBufSize = (String) m_configMap.get(CACHE_BUFSIZE_PROP); + if (sBufSize != null) { - return m_archives[i]; + DEFAULT_BUFFER = Integer.parseInt(sBufSize); } } - return null; - } + catch (NumberFormatException ne) + { + // Use the default value. + } - public synchronized int getArchiveIndex(BundleArchive ba) - { - for (int i = 0; i < m_archives.length; i++) + // Create the existing bundle archives in the directory, if any exist. + File cacheDir = determineCacheDir(m_configMap); + List archiveList = new ArrayList(); + File[] children = getSecureAction().listDirectory(cacheDir); + for (int i = 0; (children != null) && (i < children.length); i++) { - if (m_archives[i] == ba) + // Ignore directories that aren't bundle directories or + // is the system bundle directory. + if (children[i].getName().startsWith(BUNDLE_DIR_PREFIX) && + !children[i].getName().equals(BUNDLE_DIR_PREFIX + Long.toString(0))) { - return i; + // Recreate the bundle archive. + try + { + archiveList.add( + new BundleArchive( + m_logger, m_configMap, m_zipFactory, children[i])); + } + catch (Exception ex) + { + // Log exception and remove bundle archive directory. + m_logger.log(Logger.LOG_ERROR, + "Error reloading cached bundle, removing it: " + children[i], ex); + deleteDirectoryTree(children[i]); + } } } - return -1; + + return (BundleArchive[]) + archiveList.toArray(new BundleArchive[archiveList.size()]); } - public synchronized BundleArchive create( - long id, String location, InputStream is) + public BundleArchive create(long id, int startLevel, String location, InputStream is) throws Exception { + File cacheDir = determineCacheDir(m_configMap); + // Construct archive root directory. File archiveRootDir = - new File(m_profileDir, BUNDLE_DIR_PREFIX + Long.toString(id)); + new File(cacheDir, BUNDLE_DIR_PREFIX + Long.toString(id)); try { // Create the archive and add it to the list of archives. BundleArchive ba = - new BundleArchive(m_logger, archiveRootDir, id, location, is, - m_trustedCaCerts); - BundleArchive[] tmp = new BundleArchive[m_archives.length + 1]; - System.arraycopy(m_archives, 0, tmp, 0, m_archives.length); - tmp[m_archives.length] = ba; - m_archives = tmp; + new BundleArchive( + m_logger, m_configMap, m_zipFactory, archiveRootDir, + id, startLevel, location, is); return ba; } catch (Exception ex) @@ -161,8 +473,7 @@ public synchronized BundleArchive create( { m_logger.log( Logger.LOG_ERROR, - getClass().getName() - + ": Unable to delete the archive directory - " + "Unable to delete the archive directory: " + archiveRootDir); } } @@ -170,28 +481,40 @@ public synchronized BundleArchive create( } } - public synchronized void remove(BundleArchive ba) + /** + * Provides the system bundle access to its private storage area; this + * special case is necessary since the system bundle is not really a + * bundle and therefore must be treated in a special way. + * @param fileName the name of the file in the system bundle's private area. + * @return a File object corresponding to the specified file name. + * @throws Exception if any error occurs. + **/ + public File getSystemBundleDataFile(String fileName) throws Exception { - if (ba != null) + // Make sure system bundle directory exists. + File sbDir = new File(determineCacheDir(m_configMap), BUNDLE_DIR_PREFIX + Long.toString(0)); + + // If the system bundle directory exists, then we don't + // need to initialize since it has already been done. + if (!getSecureAction().fileExists(sbDir) && !getSecureAction().mkdirs(sbDir) && !getSecureAction().fileExists(sbDir)) { - // Remove the archive. - ba.dispose(); - // Remove the archive from the cache. - int idx = getArchiveIndex(ba); - if (idx >= 0) - { - BundleArchive[] tmp = - new BundleArchive[m_archives.length - 1]; - System.arraycopy(m_archives, 0, tmp, 0, idx); - if (idx < tmp.length) - { - System.arraycopy(m_archives, idx + 1, tmp, idx, - tmp.length - idx); - } - m_archives = tmp; - } + m_logger.log( + Logger.LOG_ERROR, + "Unable to create system bundle directory."); + throw new IOException("Unable to create system bundle directory."); } + + File dataFile = new File(sbDir, fileName); + + String dataFilePath = BundleCache.getSecureAction().getCanonicalPath(dataFile); + String dataDirPath = BundleCache.getSecureAction().getCanonicalPath(sbDir); + if (!dataFilePath.equals(dataDirPath) && !dataFilePath.startsWith(dataDirPath + File.separatorChar)) + { + throw new IllegalArgumentException("The data file must be inside the data dir."); + } + + return dataFile; } // @@ -203,148 +526,116 @@ public synchronized void remove(BundleArchive ba) * @param is the input stream to copy. * @param outputFile the file to which the input stream should be copied. **/ - protected static void copyStreamToFile(InputStream is, File outputFile) + static void copyStreamToFile(InputStream is, File outputFile) throws IOException { + // Get the buffer for this thread if there is one already otherwise, + // create one of size DEFAULT_BUFFER + SoftReference ref = (SoftReference) m_defaultBuffer.get(); + byte[] bytes = null; + if (ref != null) + { + bytes = (byte[]) ref.get(); + } + + if (bytes == null) + { + bytes = new byte[DEFAULT_BUFFER]; + m_defaultBuffer.set(new SoftReference(bytes)); + } + OutputStream os = null; try { os = getSecureAction().getFileOutputStream(outputFile); - os = new BufferedOutputStream(os, BUFSIZE); - byte[] b = new byte[BUFSIZE]; - int len = 0; - while ((len = is.read(b)) != -1) + for (int i = is.read(bytes);i != -1; i = is.read(bytes)) { - os.write(b, 0, len); + os.write(bytes, 0, i); } } finally { - if (is != null) is.close(); - if (os != null) os.close(); + try + { + if (is != null) is.close(); + } + finally + { + if (os != null) os.close(); + } } } - protected static boolean deleteDirectoryTree(File target) + static boolean deleteDirectoryTree(File target) { - if (!getSecureAction().fileExists(target)) + if (!deleteDirectoryTreeRecursive(target)) { - return true; - } - - if (getSecureAction().isFileDirectory(target)) - { - File[] files = getSecureAction().listDirectory(target); - for (int i = 0; i < files.length; i++) - { - deleteDirectoryTree(files[i]); - } + // We might be talking windows and native libs -- hence, + // try to trigger a gc and try again. The hope is that + // this releases the classloader that loaded the native + // lib and allows us to delete it because it then + // would not be used anymore. + System.gc(); + System.gc(); + return deleteDirectoryTreeRecursive(target); } - - return getSecureAction().deleteFile(target); + return true; } // // Private methods. // - private void initialize() throws Exception + private static File determineCacheDir(Map configMap) { - // Get buffer size value. - try + File cacheDir; + + // Check to see if the cache directory is specified in the storage + // configuration property. + String cacheDirStr = (String) configMap.get(Constants.FRAMEWORK_STORAGE); + // Get the cache root directory for relative paths; the default is ".". + String rootDirStr = (String) configMap.get(CACHE_ROOTDIR_PROP); + rootDirStr = (rootDirStr == null) ? CACHE_ROOTDIR_DEFAULT : rootDirStr; + if (cacheDirStr != null) { - String sBufSize = m_cfg.get(CACHE_BUFSIZE_PROP); - if (sBufSize != null) + // If the specified cache directory is relative, then use the + // root directory to calculate the absolute path. + cacheDir = new File(cacheDirStr); + if (!cacheDir.isAbsolute()) { - BUFSIZE = Integer.parseInt(sBufSize); + cacheDir = new File(rootDirStr, cacheDirStr); } } - catch (NumberFormatException ne) - { - // Use the default value. - } - - // See if the profile directory is specified. - String profileDirStr = m_cfg.get(CACHE_PROFILE_DIR_PROP); - if (profileDirStr != null) - { - m_profileDir = new File(profileDirStr); - } else { - // Since no profile directory was specified, then the profile - // directory will be a directory in the cache directory named - // after the profile. - - // First, determine the location of the cache directory; it - // can either be specified or in the default location. - String cacheDirStr = m_cfg.get(CACHE_DIR_PROP); - if (cacheDirStr == null) - { - // Since no cache directory was specified, put it - // ".felix" in the user's home by default. - cacheDirStr = System.getProperty("user.home"); - cacheDirStr = cacheDirStr.endsWith(File.separator) - ? cacheDirStr : cacheDirStr + File.separator; - cacheDirStr = cacheDirStr + CACHE_DIR_NAME; - } - - // Now, get the profile name. - String profileName = m_cfg.get(CACHE_PROFILE_PROP); - if (profileName == null) - { - throw new IllegalArgumentException( - "No profile name or directory has been specified."); - } - // Profile name cannot contain the File.separator char. - else if (profileName.indexOf(File.separator) >= 0) - { - throw new IllegalArgumentException( - "The profile name cannot contain the file separator character."); - } - - m_profileDir = new File(cacheDirStr, profileName); + // If no cache directory was specified, then use the default name + // in the root directory. + cacheDir = new File(rootDirStr, CACHE_DIR_NAME); } - // Create profile directory, if it does not exist. - if (!getSecureAction().fileExists(m_profileDir)) + return cacheDir; + } + + private static boolean deleteDirectoryTreeRecursive(File target) + { + if (!getSecureAction().fileExists(target)) { - if (!getSecureAction().mkdirs(m_profileDir)) - { - m_logger.log( - Logger.LOG_ERROR, - getClass().getName() + ": Unable to create directory: " - + m_profileDir); - throw new RuntimeException("Unable to create profile directory."); - } + return true; } - // Create the existing bundle archives in the profile directory, - // if any exist. - List archiveList = new ArrayList(); - File[] children = getSecureAction().listDirectory(m_profileDir); - for (int i = 0; (children != null) && (i < children.length); i++) + if (getSecureAction().isFileDirectory(target)) { - // Ignore directories that aren't bundle directories. - if (children[i].getName().startsWith(BUNDLE_DIR_PREFIX)) + File[] files = getSecureAction().listDirectory(target); + if (files != null) { - // Recreate the bundle archive. - try + for (int i = 0; i < files.length; i++) { - archiveList.add( - new BundleArchive(m_logger, children[i], m_trustedCaCerts)); - } - catch (Exception ex) - { - // Log and ignore. - m_logger.log(Logger.LOG_ERROR, - getClass().getName() + ": Error creating archive.", ex); + deleteDirectoryTreeRecursive(files[i]); } } } - m_archives = (BundleArchive[]) - archiveList.toArray(new BundleArchive[archiveList.size()]); + return getSecureAction().deleteFile(target); } -} +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/cache/BundleRevision.java b/framework/src/main/java/org/apache/felix/framework/cache/BundleRevision.java deleted file mode 100644 index 55b47b4aaa6..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/cache/BundleRevision.java +++ /dev/null @@ -1,879 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.cache; - -import java.io.File; -import java.io.InputStream; -import java.security.cert.*; -import java.util.*; -import java.util.jar.*; - -import org.apache.felix.framework.Logger; -import org.apache.felix.moduleloader.IContent; - -/** - *

      - * This class implements an abstract revision of a bundle archive. A revision - * is an abstraction of a bundle's actual content and is associated with a - * parent bundle archive. A bundle archive may have multiple revisions assocaited - * with it at one time, since updating a bundle results in a new version of the - * bundle's content until the bundle is refreshed. Upon a refresh, then old - * revisions are then purged. This abstract class is the base class for all - * concrete types of revisions, such as ones for a JAR file or directories. All - * revisions are assigned a root directory into which all of their state should - * be stored, if necessary. Clean up of this directory is the responsibility - * of the parent bundle archive and not of the revision itself. - *

      - * @see org.apache.felix.framework.cache.BundleCache - * @see org.apache.felix.framework.cache.BundleArchive -**/ -public abstract class BundleRevision -{ - private Logger m_logger; - private File m_revisionRootDir = null; - private String m_location = null; - private Collection m_trustedCaCerts = null; - private X509Certificate[] m_certificates = null; - private String[] m_subjectDNChain = null; - private boolean m_certInitDone = (System.getSecurityManager() == null); - private boolean m_subjectDNInitDone = (System.getSecurityManager() == null); - - /** - *

      - * This constructor is only used by the system bundle archive. - *

      - **/ - BundleRevision() - { - } - - /** - *

      - * This class is abstract and cannot be created. It represents a revision - * of a bundle, i.e., its content. A revision is associated with a particular - * location string, which is typically in URL format. Subclasses of this - * class provide particular functionality, such as a revision in the form - * of a JAR file or a directory. Each revision subclass is expected to use - * the root directory associated with the abstract revision instance to - * store any state; this will ensure that resources used by the revision are - * properly freed when the revision is no longer needed. - *

      - * @param logger a logger for use by the revision. - * @param revisionRootDir the root directory to be used by the revision - * subclass for storing any state. - * @param location the location string associated with the revision. - * @param trustedCaCerts the trusted CA certificates if any. - * @throws Exception if any errors occur. - **/ - public BundleRevision(Logger logger, File revisionRootDir, String location) - throws Exception - { - m_logger = logger; - m_revisionRootDir = revisionRootDir; - m_location = location; - } - - - /** - *

      - * Returns the logger for this revision. - *

      - * @return the logger instance for this revision. - **/ - public Logger getLogger() - { - return m_logger; - } - - /** - *

      - * Returns the root directory for this revision. - *

      - * @return the root directory for this revision. - **/ - public File getRevisionRootDir() - { - return m_revisionRootDir; - } - - /** - *

      - * Returns the location string this revision. - *

      - * @return the location string for this revision. - **/ - public String getLocation() - { - return m_location; - } - - /** - *

      - * Returns the main attributes of the JAR file manifest header of the - * revision. The returned map is case insensitive. - *

      - * @return the case-insensitive JAR file manifest header of the revision. - * @throws java.lang.Exception if any error occurs. - **/ - public abstract Map getManifestHeader() throws Exception; - - /** - *

      - * Returns a content object that is associated with the revision. - *

      - * @return a content object that is associated with the revision. - * @throws java.lang.Exception if any error occurs. - **/ - public abstract IContent getContent() throws Exception; - - /** - *

      - * Returns an array of content objects that are associated with the - * specified revision's bundle class path. - *

      - * @return an array of content objects for the revision's bundle class path. - * @throws java.lang.Exception if any error occurs. - **/ - public abstract IContent[] getContentPath() throws Exception; - - /** - *

      - * Returns the absolute file path for the specified native library of the - * revision. - *

      - * @param libName the name of the library. - * @return a String that contains the absolute path name to - * the requested native library of the revision. - * @throws java.lang.Exception if any error occurs. - **/ - public abstract String findLibrary(String libName) throws Exception; - - /** - *

      - * This method is called when the revision is no longer needed. The directory - * associated with the revision will automatically be removed for each - * revision, so this method only needs to be concerned with other issues, - * such as open files. - *

      - * @throws Exception if any error occurs. - **/ - public abstract void dispose() throws Exception; - - protected void setTrustedCaCerts(Collection trustedCaCerts) - { - m_trustedCaCerts = trustedCaCerts; - } - - public X509Certificate[] getCertificates() - { - if (m_certInitDone) - { - return m_certificates; - } - - if (m_trustedCaCerts == null) - { - return null; - } - - try - { - m_certificates = getRevisionCertificates(); - } - catch (Exception ex) - { - ex.printStackTrace(); - // TODO: log this or something - } - finally - { - m_certInitDone = true; - } - - return m_certificates; - } - - protected abstract X509Certificate[] getRevisionCertificates() throws Exception; - - public String[] getDNChains() - { - if (m_subjectDNInitDone) - { - return m_subjectDNChain; - } - - try - { - X509Certificate[] certificates = getCertificates(); - - if (certificates == null) - { - return null; - } - - List rootChains = new ArrayList(); - - getRootChains(certificates, rootChains); - - List result = new ArrayList(); - - for (Iterator rootIter = rootChains.iterator();rootIter.hasNext();) - { - StringBuffer buffer = new StringBuffer(); - - List chain = (List) rootIter.next(); - - Iterator iter = chain.iterator(); - - X509Certificate current = (X509Certificate) iter.next(); - - try - { - buffer.append(parseSubjectDN(current.getTBSCertificate())); - - while (iter.hasNext()) - { - buffer.append(';'); - - current = (X509Certificate) iter.next(); - - buffer.append(parseSubjectDN(current.getTBSCertificate())); - } - - result.add(buffer.toString()); - - } - catch (Exception ex) - { - // something went wrong during parsing - - // it might be that the cert contained an unsupported OID - ex.printStackTrace(); - // TODO: log this or something - } - } - - if (!result.isEmpty()) - { - m_subjectDNChain = (String[]) result.toArray(new String[result.size()]); - } - } - finally - { - m_subjectDNInitDone = true; - } - - return m_subjectDNChain; - } - - protected X509Certificate[] getCertificatesForJar(JarFile bundle) - throws Exception - { - if (bundle.getManifest() == null) - { - return null; - } - - List bundleEntries = new ArrayList(); - - Enumeration entries = bundle.entries(); - - while (entries.hasMoreElements()) - { - JarEntry entry = (JarEntry) entries.nextElement(); - bundleEntries.add(entry); - InputStream is = bundle.getInputStream(entry); - byte[] read = new byte[4096]; - while (is.read(read) != -1) - { - // read the entry - } - is.close(); - } - bundle.close(); - - List certificateChains = new ArrayList(); - - for (Iterator iter = bundleEntries.iterator();iter.hasNext();) - { - JarEntry entry = (JarEntry) iter.next(); - - if (entry.isDirectory() || entry.getName().startsWith("META-INF")) - { - continue; - } - - Certificate[] certificates = entry.getCertificates(); - - if ((certificates == null) || (certificates.length == 0)) - { - return null; - } - - List chains = new ArrayList(); - - getRootChains(certificates, chains); - - if (certificateChains.isEmpty()) - { - certificateChains.addAll(chains); - } - else - { - for (Iterator iter2 = certificateChains.iterator();iter2.hasNext();) - { - X509Certificate cert = (X509Certificate) ((List) iter2.next()).get(0); - boolean found = false; - for (Iterator iter3 = chains.iterator();iter3.hasNext();) - { - X509Certificate cert2 = (X509Certificate) ((List) iter3.next()).get(0); - - if (cert.getSubjectDN().equals(cert2.getSubjectDN()) && cert.equals(cert2)) - { - found = true; - break; - } - } - if (!found) - { - iter2.remove(); - } - } - } - - if (certificateChains.isEmpty()) - { - return null; - } - } - - List result = new ArrayList(); - - for (Iterator iter = certificateChains.iterator();iter.hasNext();) - { - result.addAll((List) iter.next()); - } - - return (X509Certificate[]) result.toArray(new X509Certificate[result.size()]); - } - - protected void getRootChains(Certificate[] certificates, List chains) - { - List chain = new ArrayList(); - - for (int i = 0; i < certificates.length - 1; i++) - { - chain.add(certificates[i]); - if (!((X509Certificate)certificates[i + 1]).getSubjectDN().equals( - ((X509Certificate)certificates[i]).getIssuerDN())) - { - - if (trusted((X509Certificate) certificates[i])) - { - chains.add(chain); - } - - chain = new ArrayList(); - } - } - // The final entry in the certs array is always - // a "root" certificate - chain.add(certificates[certificates.length - 1]); - - if (trusted((X509Certificate) certificates[certificates.length - 1])) - { - chains.add(chain); - } - } - - // @return true if - // m_trustedCaCerts.contains(cert) || cert issued by any of m_trustedCaCerts - protected boolean trusted(X509Certificate cert) - { - if (m_trustedCaCerts == null) - { - return false; - } - - // m_trustedCaCerts.contains(cert) ? return true - for (Iterator iter = m_trustedCaCerts.iterator();iter.hasNext();) - { - X509Certificate trustedCaCert = (X509Certificate) iter.next(); - - // If the cert has the same SubjectDN - // as a trusted CA, check whether - // the two certs are the same. - if (cert.getSubjectDN().equals(trustedCaCert.getSubjectDN())) - { - if (cert.equals(trustedCaCert)) - { - try - { - cert.checkValidity(); - trustedCaCert.checkValidity(); - return true; - } - catch (CertificateException ex) - { - System.err.println("WARNING: Invalid certificate [" + ex + "]"); - } - } - } - } - - // cert issued by any of m_trustedCaCerts ? return true : return false - for (Iterator iter = m_trustedCaCerts.iterator();iter.hasNext();) - { - X509Certificate trustedCaCert = (X509Certificate) iter.next(); - - if (cert.getIssuerDN().equals(trustedCaCert.getSubjectDN())) - { - try - { - cert.verify(trustedCaCert.getPublicKey()); - cert.checkValidity(); - trustedCaCert.checkValidity(); - return true; - } - catch (Exception ex) - { - System.err.println("WARNING: Invalid certificate [" + ex + "]"); - } - } - } - - return false; - } - - /* - * This is deep magiK, bare with me. The problem is that we don't get - * access to the original subject dn in a certificate without resorting to - * sun.* classes or running on something > OSGi-minimum/jdk1.3. Furthermore, - * we need access to it because there is no other way to escape it properly. - * Note, this is due to missing of a public X500Name in OSGI-minimum/jdk1.3 - * a.k.a foundation. - * - * The solution is to get the DER encoded TBS certificate bytes via the - * available java methods and parse-out the subject dn in canonical form by - * hand. This is possible without deploying a full-blown BER encoder/decoder - * due to java already having done all the cumbersome verification and - * normalization work. - * - * The following skips through the TBS certificate bytes until it reaches and - * subsequently parses the subject dn. If the below makes immediate sense to - * you - you either are a X509/X501/DER expert or quite possibly mad. In any - * case, please seek medical care immediately. - */ - protected String parseSubjectDN(byte[] tbsCertEncoded) throws Exception - { - // init - tbs_buffer = tbsCertEncoded; - tbs_offset = 0; - - try // this is a finally block that resets the tbs_buffer to null after we're done - { - // TBSCertificate ::= SEQUENCE { - // version [0] EXPLICIT Version DEFAULT v1, - // serialNumber CertificateSerialNumber, - // signature AlgorithmIdentifier, - // issuer Name, - // validity Validity, - // subject Name, - // - // WE CAN STOP! - // - // subjectPublicKeyInfo SubjectPublicKeyInfo, - // issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, - // -- If present, version must be v2 or v3 - // subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, - // -- If present, version must be v2 or v3 - // extensions [3] EXPLICIT Extensions OPTIONAL - // -- If present, version must be v3 - // } - - next(); - next(); - // if a version is present skip it - if (tbs_tag == 0) - { - next(); - tbs_offset += tbs_length; - } - tbs_offset += tbs_length; - // skip the serialNumber - next(); - next(); - tbs_offset += tbs_length; - // skip the signature - next(); - tbs_offset += tbs_length; - // skip the issuer - // The issuer is a sequence of sets of issuer dns like the subject later on - - // we just skip it. - next(); - int endOffset = tbs_offset + tbs_length; - - int seqTagOffset = tbs_tagOffset; - - // skip the sequence - while (endOffset > tbs_offset) - { - next(); - - int endOffset2 = tbs_offset + tbs_length; - - int seqTagOffset2 = tbs_tagOffset; - - // skip each set - while (endOffset2 > tbs_offset) - { - next(); - next(); - tbs_offset += tbs_length; - next(); - tbs_offset += tbs_length; - } - - tbs_tagOffset = seqTagOffset2; - } - - tbs_tagOffset = seqTagOffset; - // skip the validity which contains two dates to be skiped - next(); - next(); - tbs_offset += tbs_length; - next(); - tbs_offset += tbs_length; - next(); - // Now extract the subject dns and add them to attributes - List attributes = new ArrayList(); - - endOffset = tbs_offset + tbs_length; - - seqTagOffset = tbs_tagOffset; - - // for each set of rdns - while (endOffset > tbs_offset) - { - next(); - int endOffset2 = tbs_offset + tbs_length; - - // store tag offset - int seqTagOffset2 = tbs_tagOffset; - - List rdn = new ArrayList(); - - // for each rdn in the set - while (endOffset2 > tbs_offset) - { - next(); - next(); - tbs_offset += tbs_length; - // parse the oid of the rdn - int oidElement = 1; - for (int i = 0; i < tbs_length; i++, ++oidElement) - { - while ((tbs_buffer[tbs_contentOffset + i] & 0x80) == 0x80) - { - i++; - } - } - int[] oid = new int[oidElement]; - for (int id = 1, i = 0; id < oid.length; id++, i++) - { - int octet = tbs_buffer[tbs_contentOffset + i]; - oidElement = octet & 0x7F; - while ((octet & 0x80) != 0) - { - i++; - octet = tbs_buffer[tbs_contentOffset + i]; - oidElement = oidElement << 7 | (octet & 0x7f); - } - oid[id] = oidElement; - } - // The first OID is special - if (oid[1] > 79) - { - oid[0] = 2; - oid[1] = oid[1] - 80; - } - else - { - oid[0] = oid[1] / 40; - oid[1] = oid[1] % 40; - } - // Now parse the value of the rdn - next(); - String str = null; - int tagTmp = tbs_tag; - tbs_offset += tbs_length; - switch(tagTmp) - { - case 30: // BMPSTRING - case 22: // IA5STRING - case 27: // GENERALSTRING - case 19: // PRINTABLESTRING - case 20: // TELETEXSTRING && T61STRING - case 28: // UNIVERSALSTRING - str = new String(tbs_buffer, tbs_contentOffset, - tbs_length); - break; - case 12: // UTF8_STRING - str = new String(tbs_buffer, tbs_contentOffset, - tbs_length, "UTF-8"); - break; - default: // OCTET - byte[] encoded = new byte[tbs_offset - tbs_tagOffset]; - System.arraycopy(tbs_buffer, tbs_tagOffset, encoded, - 0, encoded.length); - // Note, I'm not sure this is allowed by the spec - // i.e., whether OCTET subjects are allowed at all - // but it shouldn't harm doing it anyways (we just - // convert it into a hex string prefixed with \#). - str = toHexString(encoded); - break; - } - - rdn.add(new Object[]{mapOID(oid), makeCanonical(str)}); - } - - attributes.add(rdn); - tbs_tagOffset = seqTagOffset2; - } - - tbs_tagOffset = seqTagOffset; - - StringBuffer result = new StringBuffer(); - - for (int i = attributes.size() - 1; i >= 0; i--) - { - List rdn = (List) attributes.get(i); - Collections.sort(rdn, new Comparator() - { - public int compare(Object obj1, Object obj2) - { - return ((String) ((Object[]) obj1)[0]).compareTo( - ((String) ((Object[])obj2)[0])); - } - }); - - for (Iterator iter = rdn.iterator();iter.hasNext();) - { - Object[] att = (Object[]) iter.next(); - result.append((String) att[0]); - result.append('='); - result.append((String) att[1]); - - if (iter.hasNext()) - { - // multi-valued RDN - result.append('+'); - } - } - - if (i != 0) - { - result.append(','); - } - } - - // the spec says: - // return result.toString().toUpperCase(Locale.US).toLowerCase(Locale.US); - // but that doesn't make no sense to me whatsoever hence, - return result.toString().toLowerCase(Locale.US); - } - finally - { - tbs_buffer = null; - } - } - - private byte[] tbs_buffer = null; - private int tbs_offset = 0; - private int tbs_tagOffset = 0; - private int tbs_tag = -1; - private int tbs_length = -1; - private int tbs_contentOffset = -1; - - // Determine the type of the current sequence (tbs_tab), and the length and - // offset of it (tbs_length and tbs_tagOffset) plus increment the global - // offset (tbs_offset) accordingly. Note, we don't need to check for - // the indefinite length because this is supposed to be DER not BER (and - // we implicitly assume that java only gives us valid DER). - private void next() - { - tbs_tagOffset = tbs_offset; - tbs_tag = tbs_buffer[tbs_offset++] & 0xFF; - tbs_length = tbs_buffer[tbs_offset++] & 0xFF; - // There are two kinds of length forms - make sure we use the right one - if ((tbs_length & 0x80) != 0) - { - // its the long kind - int numOctets = tbs_length & 0x7F; - // hence, convert it - tbs_length = tbs_buffer[tbs_offset++] & 0xFF; - for (int i = 1; i < numOctets; i++) - { - int ch = tbs_buffer[tbs_offset++] & 0xFF; - tbs_length = (tbs_length << 8) + ch; - } - } - tbs_contentOffset = tbs_offset; - } - - private String makeCanonical(String value) - { - int len = value.length(); - - if (len == 0) - { - return value; - } - - StringBuffer result = new StringBuffer(len); - - int i = 0; - if (value.charAt(0) == '#') - { - result.append('\\'); - result.append('#'); - i++; - } - for (;i < len; i++) - { - char c = value.charAt(i); - - switch (c) - { - case ' ': - int pos = result.length(); - // remove leading spaces and - // remove all spaces except one in any sequence of spaces - if ((pos == 0) || (result.charAt(pos - 1) == ' ')) - { - break; - } - result.append(' '); - break; - case '"': - case '\\': - case ',': - case '+': - case '<': - case '>': - case ';': - result.append('\\'); - default: - result.append(c); - } - } - - // count down until first none space to remove trailing spaces - i = result.length() - 1; - while ((i > -1) && (result.charAt(i) == ' ')) - { - i--; - } - - result.setLength(i + 1); - - return result.toString(); - } - - private String toHexString(byte[] encoded) - { - StringBuffer result = new StringBuffer(); - - result.append('#'); - - for (int i = 0; i < encoded.length; i++) - { - int c = (encoded[i] >> 4) & 0x0F; - if (c < 10) - { - result.append((char) (c + 48)); - } - else - { - result.append((char) (c + 87)); - } - - c = encoded[i] & 0x0F; - - if (c < 10) - { - result.append((char) (c + 48)); - } - else - { - result.append((char) (c + 87)); - } - } - - return result.toString(); - } - - private static final Map OID2NAME = new HashMap(); - - static - { - // see core-spec 2.3.5 - OID2NAME.put("2.5.4.3", "cn"); - OID2NAME.put("2.5.4.4", "sn"); - OID2NAME.put("2.5.4.6", "c"); - OID2NAME.put("2.5.4.7", "l"); - OID2NAME.put("2.5.4.8", "st"); - OID2NAME.put("2.5.4.10", "o"); - OID2NAME.put("2.5.4.11", "ou"); - OID2NAME.put("2.5.4.12", "title"); - OID2NAME.put("2.5.4.42", "givenname"); - OID2NAME.put("2.5.4.43", "initials"); - OID2NAME.put("2.5.4.44", "generationqualifier"); - OID2NAME.put("2.5.4.46", "dnqualifier"); - OID2NAME.put("2.5.4.9", "street"); - OID2NAME.put("0.9.2342.19200300.100.1.25", "dc"); - OID2NAME.put("0.9.2342.19200300.100.1.1", "uid"); - OID2NAME.put("1.2.840.113549.1.9.1", "emailaddress"); - OID2NAME.put("2.5.4.5", "serialnumber"); - // p.s.: it sucks that the spec doesn't list some of the oids used - // p.p.s: it sucks that the spec doesn't list the short form for all names - // In summary, there is a certain amount of guess-work involved but I'm - // fairly certain I've got it right. - } - - // This just creates a string of the oid and looks for its name in the - // known names map OID2NAME. There might be faster implementations :-) - private String mapOID(int[] oid) - { - StringBuffer oidString = new StringBuffer(); - - oidString.append(oid[0]); - for (int i = 1;i < oid.length;i++) - { - oidString.append('.'); - oidString.append(oid[i]); - } - - String result = (String) OID2NAME.get(oidString.toString()); - - if (result == null) - { - throw new IllegalArgumentException("Unknown oid: " + oidString.toString()); - } - - return result; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/cache/Content.java b/framework/src/main/java/org/apache/felix/framework/cache/Content.java new file mode 100644 index 00000000000..09ace44de19 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/cache/Content.java @@ -0,0 +1,124 @@ +/* + * 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.framework.cache; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Enumeration; + +public interface Content +{ + /** + *

      + * This method must be called when the content is no longer needed so + * that any resourses being used (e.g., open files) can be closed. Once + * this method is called, the content is no longer usable. If the content + * is already closed, then calls on this method should have no effect. + *

      + **/ + void close(); + + /** + *

      + * This method determines if the specified named entry is contained in + * the associated content. The entry name is a relative path with '/' + * separators. + *

      + * @param name The name of the entry to find. + * @return true if a corresponding entry was found, false + * otherwise. + **/ + boolean hasEntry(String name); + + /** + *

      + * Returns an enumeration of entry names as String objects. + * An entry name is a path constructed with '/' as path element + * separators and is relative to the root of the content. Entry names + * for entries that represent directories should end with the '/' + * character. + *

      + * @returns An enumeration of entry names or null. + **/ + Enumeration getEntries(); + + /** + *

      + * This method returns the named entry as an array of bytes. + *

      + * @param name The name of the entry to retrieve as a byte array. + * @return An array of bytes if the corresponding entry was found, null + * otherwise. + **/ + byte[] getEntryAsBytes(String name); + + /** + *

      + * This method returns the named entry as an input stream. + *

      + * @param name The name of the entry to retrieve as an input stream. + * @return An input stream if the corresponding entry was found, null + * otherwise. + * @throws java.io.IOException if any error occurs. + **/ + InputStream getEntryAsStream(String name) throws IOException; + + /** + *

      + * This method returns the named entry as an IContent Typically, + * this method only makes sense for entries that correspond to some form + * of aggregated resource (e.g., an embedded JAR file or directory), but + * implementations are free to interpret this however makes sense. This method + * should return a new IContent instance for every invocation and + * the caller is responsible for opening and closing the returned content + * object. + *

      + * @param name The name of the entry to retrieve as an IContent. + * @return An IContent instance if a corresponding entry was found, + * null otherwise. + **/ + Content getEntryAsContent(String name); + + /** + *

      + * This method returns the named entry as a file in the file system for + * use as a native library. It may not be possible for all content + * implementations (e.g., memory only) to implement this method, in which + * case it is acceptable to return null. Since native libraries + * can only be associated with a single class loader, this method should + * return a unique file per request. + *

      + * @param name The name of the entry to retrieve as a file. + * @return A string corresponding to the absolute path of the file if a + * corresponding entry was found, null otherwise. + **/ + String getEntryAsNativeLibrary(String name); + + /** + *

      + * This method allows retrieving an entry as a local URL. + *

      + * + * @param name The name of the entry to retrieve as a URL + * @return A URL using a local protocol such as file, jar + * or null if not possible. + */ + URL getEntryAsURL(String name); +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/cache/ContentDirectoryContent.java b/framework/src/main/java/org/apache/felix/framework/cache/ContentDirectoryContent.java new file mode 100644 index 00000000000..d268f32ee65 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/cache/ContentDirectoryContent.java @@ -0,0 +1,159 @@ +/* + * 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.framework.cache; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Enumeration; +import java.util.NoSuchElementException; + +public class ContentDirectoryContent implements Content +{ + private final Content m_content; + private final String m_rootPath; + + public ContentDirectoryContent(Content content, String path) + { + m_content = content; + // Add a '/' to the end if not present. + m_rootPath = (path.length() > 0) && (path.charAt(path.length() - 1) != '/') + ? path + "/" : path; + } + + public void close() + { + // We do not actually close the associated content + // from which we are filtering our directory because + // we assume that this will be close manually by + // the owner of that content. + } + + public boolean hasEntry(String name) throws IllegalStateException + { + if ((name.length() > 0) && (name.charAt(0) == '/')) + { + name = name.substring(1); + } + + return m_content.hasEntry(m_rootPath + name); + } + + public Enumeration getEntries() + { + Enumeration result = new EntriesEnumeration(m_content.getEntries(), m_rootPath); + return result.hasMoreElements() ? result : null; + } + + public byte[] getEntryAsBytes(String name) throws IllegalStateException + { + if ((name.length() > 0) && (name.charAt(0) == '/')) + { + name = name.substring(1); + } + + return m_content.getEntryAsBytes(m_rootPath + name); + } + + public InputStream getEntryAsStream(String name) + throws IllegalStateException, IOException + { + if ((name.length() > 0) && (name.charAt(0) == '/')) + { + name = name.substring(1); + } + + return m_content.getEntryAsStream(m_rootPath + name); + } + + public URL getEntryAsURL(String name) + { + return m_content.getEntryAsURL(m_rootPath + name); + } + + public Content getEntryAsContent(String name) + { + if ((name.length() > 0) && (name.charAt(0) == '/')) + { + name = name.substring(1); + } + + return m_content.getEntryAsContent(m_rootPath + name); + } + + public String getEntryAsNativeLibrary(String name) + { + if ((name.length() > 0) && (name.charAt(0) == '/')) + { + name = name.substring(1); + } + + return m_content.getEntryAsNativeLibrary(m_rootPath + name); + } + + public String toString() + { + return "CONTENT DIR " + m_rootPath + " (" + m_content + ")"; + } + + private static class EntriesEnumeration implements Enumeration + { + private final Enumeration m_enumeration; + private final String m_rootPath; + private String m_nextEntry = null; + + public EntriesEnumeration(Enumeration enumeration, String rootPath) + { + m_enumeration = enumeration; + m_rootPath = rootPath; + m_nextEntry = findNextEntry(); + } + + public synchronized boolean hasMoreElements() + { + return (m_nextEntry != null); + } + + public synchronized Object nextElement() + { + if (m_nextEntry == null) + { + throw new NoSuchElementException("No more elements."); + } + String currentEntry = m_nextEntry; + m_nextEntry = findNextEntry(); + return currentEntry; + } + + private String findNextEntry() + { + // Find next entry that is inside the root directory. + while (m_enumeration.hasMoreElements()) + { + String next = (String) m_enumeration.nextElement(); + if (next.startsWith(m_rootPath) && !next.equals(m_rootPath)) + { + // Strip off the root directory. + return next.substring(m_rootPath.length()); + } + } + return null; + } + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/cache/DirectoryContent.java b/framework/src/main/java/org/apache/felix/framework/cache/DirectoryContent.java new file mode 100644 index 00000000000..50da934ec27 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/cache/DirectoryContent.java @@ -0,0 +1,392 @@ +/* + * 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.framework.cache; + +import org.apache.felix.framework.Logger; +import org.apache.felix.framework.util.FelixConstants; +import org.apache.felix.framework.util.Util; +import org.apache.felix.framework.util.WeakZipFileFactory; +import org.osgi.framework.Constants; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Properties; + +public class DirectoryContent implements Content +{ + private static final int BUFSIZE = 4096; + private static final transient String EMBEDDED_DIRECTORY = "-embedded"; + private static final transient String LIBRARY_DIRECTORY = "-lib"; + + private final Logger m_logger; + private final Map m_configMap; + private final WeakZipFileFactory m_zipFactory; + private final Object m_revisionLock; + private final File m_rootDir; + private final File m_dir; + private Map m_nativeLibMap; + + public DirectoryContent(Logger logger, Map configMap, + WeakZipFileFactory zipFactory, Object revisionLock, File rootDir, File dir) + { + m_logger = logger; + m_configMap = configMap; + m_zipFactory = zipFactory; + m_revisionLock = revisionLock; + m_rootDir = rootDir; + m_dir = dir; + } + + public File getFile() + { + return m_dir; + } + + public void close() + { + // Nothing to clean up. + } + + public boolean hasEntry(String name) throws IllegalStateException + { + if ((name.length() > 0) && (name.charAt(0) == '/')) + { + name = name.substring(1); + } + + // Return true if the file associated with the entry exists, + // unless the entry name ends with "/", in which case only + // return true if the file is really a directory. + File file = new File(m_dir, name); + return BundleCache.getSecureAction().fileExists(file) + && (name.endsWith("/") + ? BundleCache.getSecureAction().isFileDirectory(file) : true); + } + + public Enumeration getEntries() + { + // Wrap entries enumeration to filter non-matching entries. + Enumeration e = new EntriesEnumeration(m_dir); + + // Spec says to return null if there are no entries. + return (e.hasMoreElements()) ? e : null; + } + + public byte[] getEntryAsBytes(String name) throws IllegalStateException + { + if ((name.length() > 0) && (name.charAt(0) == '/')) + { + name = name.substring(1); + } + + // Get the embedded resource. + + File file = new File(m_dir, name); + try + { + + return BundleCache.getSecureAction().fileExists(file) ? BundleCache.read(BundleCache.getSecureAction().getFileInputStream(file), file.length()) : null; + } + catch (Exception ex) + { + m_logger.log( + Logger.LOG_ERROR, + "DirectoryContent: Unable to read bytes for file " + name + " from file " + file.getAbsolutePath(), ex); + return null; + } + } + + public InputStream getEntryAsStream(String name) + throws IllegalStateException, IOException + { + if ((name.length() > 0) && (name.charAt(0) == '/')) + { + name = name.substring(1); + } + + File file = new File(m_dir, name); + try + { + return BundleCache.getSecureAction().fileExists(file) ? BundleCache.getSecureAction().getFileInputStream(file) : null; + } + catch (Exception ex) + { + m_logger.log( + Logger.LOG_ERROR, + "DirectoryContent: Unable to create inputstream for file " + name + " from file " + file.getAbsolutePath(), ex); + return null; + } + } + + public URL getEntryAsURL(String name) + { + if ((name.length() > 0) && (name.charAt(0) == '/')) + { + name = name.substring(1); + } + + if (hasEntry(name)) + { + try + { + return BundleCache.getSecureAction().toURI(new File(m_dir, name)).toURL(); + } + catch (MalformedURLException e) + { + return null; + } + } + else + { + return null; + } + } + + public Content getEntryAsContent(String entryName) + { + // If the entry name refers to the content itself, then + // just return it immediately. + if (entryName.equals(FelixConstants.CLASS_PATH_DOT)) + { + return new DirectoryContent( + m_logger, m_configMap, m_zipFactory, m_revisionLock, m_rootDir, m_dir); + } + + // Remove any leading slash, since all bundle class path + // entries are relative to the root of the bundle. + entryName = (entryName.startsWith("/")) ? entryName.substring(1) : entryName; + + if (entryName.trim().startsWith(".." + File.separatorChar) || + entryName.contains(File.separator + ".." + File.separatorChar) || + entryName.trim().endsWith(File.separator + "..") || + entryName.trim().equals("..")) + { + return null; + } + + // Any embedded JAR files will be extracted to the embedded directory. + File embedDir = new File(m_rootDir, m_dir.getName() + EMBEDDED_DIRECTORY); + + // Determine if the entry is an emdedded JAR file or + // directory in the bundle JAR file. Ignore any entries + // that do not exist per the spec. + File file = new File(m_dir, entryName); + if (BundleCache.getSecureAction().isFileDirectory(file)) + { + return new DirectoryContent( + m_logger, m_configMap, m_zipFactory, m_revisionLock, m_rootDir, file); + } + else if (BundleCache.getSecureAction().fileExists(file) + && entryName.endsWith(".jar")) + { + File extractDir = new File(embedDir, + (entryName.lastIndexOf('/') >= 0) + ? entryName.substring(0, entryName.lastIndexOf('/')) + : entryName); + + return new JarContent( + m_logger, m_configMap, m_zipFactory, m_revisionLock, + extractDir, file, null); + } + + // The entry could not be found, so return null. + return null; + } + +// TODO: SECURITY - This will need to consider security. + public String getEntryAsNativeLibrary(String entryName) + { + // Return result. + String result = null; + + // Remove any leading slash, since all bundle class path + // entries are relative to the root of the bundle. + entryName = (entryName.startsWith("/")) ? entryName.substring(1) : entryName; + + if (entryName.trim().startsWith(".." + File.separatorChar) || + entryName.contains(File.separator + ".." + File.separatorChar) || + entryName.trim().endsWith(File.separator + "..") || + entryName.trim().equals("..")) + { + return null; + } + + // Any embedded native library files will be extracted to the lib directory. + File libDir = new File(m_rootDir, m_dir.getName() + LIBRARY_DIRECTORY); + + // The entry must exist and refer to a file, not a directory, + // since we are expecting it to be a native library. + File entryFile = new File(m_dir, entryName); + if (BundleCache.getSecureAction().fileExists(entryFile) + && !BundleCache.getSecureAction().isFileDirectory(entryFile)) + { + // Extracting the embedded native library file impacts all other + // existing contents for this revision, so we have to grab the + // revision lock first before trying to extract the embedded JAR + // file to avoid a race condition. + synchronized (m_revisionLock) + { + // Since native libraries cannot be shared, we must extract a + // separate copy per request, so use the request library counter + // as part of the extracted path. + if (m_nativeLibMap == null) + { + m_nativeLibMap = new HashMap(); + } + Integer libCount = (Integer) m_nativeLibMap.get(entryName); + // Either set or increment the library count. + libCount = (libCount == null) ? new Integer(0) : new Integer(libCount.intValue() + 1); + m_nativeLibMap.put(entryName, libCount); + File libFile = new File( + libDir, libCount.toString() + File.separatorChar + entryName); + + if (!BundleCache.getSecureAction().fileExists(libFile)) + { + if (!BundleCache.getSecureAction().fileExists(libFile.getParentFile()) + && !BundleCache.getSecureAction().mkdirs(libFile.getParentFile())) + { + m_logger.log( + Logger.LOG_ERROR, + "Unable to create library directory."); + } + else + { + InputStream is = null; + + try + { + is = BundleCache.getSecureAction().getFileInputStream(entryFile); + + // Create the file. + BundleCache.copyStreamToFile(is, libFile); + + // Perform exec permission command on extracted library + // if one is configured. + String command = (String) m_configMap.get( + Constants.FRAMEWORK_EXECPERMISSION); + if (command != null) + { + Properties props = new Properties(); + props.setProperty("abspath", libFile.toString()); + command = Util.substVars(command, "command", null, props); + Process p = BundleCache.getSecureAction().exec(command); + p.waitFor(); + } + + // Return the path to the extracted native library. + result = BundleCache.getSecureAction().getAbsolutePath(libFile); + } + catch (Exception ex) + { + m_logger.log( + Logger.LOG_ERROR, + "Extracting native library.", ex); + } + } + } + else + { + // Return the path to the extracted native library. + result = BundleCache.getSecureAction().getAbsolutePath(libFile); + } + } + } + + return result; + } + + public String toString() + { + return "DIRECTORY " + m_dir; + } + + private static class EntriesEnumeration implements Enumeration + { + private final File m_dir; + private final File[] m_children; + private int m_counter = 0; + + public EntriesEnumeration(File dir) + { + m_dir = dir; + m_children = listFilesRecursive(m_dir); + } + + public synchronized boolean hasMoreElements() + { + return (m_children != null) && (m_counter < m_children.length); + } + + public synchronized Object nextElement() + { + if ((m_children == null) || (m_counter >= m_children.length)) + { + throw new NoSuchElementException("No more entry paths."); + } + + // Convert the file separator character to slashes. + String abs = BundleCache.getSecureAction() + .getAbsolutePath(m_children[m_counter]).replace(File.separatorChar, '/'); + + // Remove the leading path of the reference directory, since the + // entry paths are supposed to be relative to the root. + StringBuilder sb = new StringBuilder(abs); + sb.delete(0, BundleCache.getSecureAction().getAbsolutePath(m_dir).length() + 1); + // Add a '/' to the end of directory entries. + if (BundleCache.getSecureAction().isFileDirectory(m_children[m_counter])) + { + sb.append('/'); + } + m_counter++; + return sb.toString(); + } + + private File[] listFilesRecursive(File dir) + { + File[] children = BundleCache.getSecureAction().listDirectory(dir); + File[] combined = children; + if (children != null) + { + for (int i = 0; i < children.length; i++) + { + if (BundleCache.getSecureAction().isFileDirectory(children[i])) + { + File[] grandchildren = listFilesRecursive(children[i]); + if (grandchildren != null && grandchildren.length > 0) + { + File[] tmp = new File[combined.length + grandchildren.length]; + System.arraycopy(combined, 0, tmp, 0, combined.length); + System.arraycopy( + grandchildren, 0, tmp, combined.length, grandchildren.length); + combined = tmp; + } + } + } + } + return combined; + } + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/cache/DirectoryRevision.java b/framework/src/main/java/org/apache/felix/framework/cache/DirectoryRevision.java index e2ffde59886..a1cf6080c1b 100644 --- a/framework/src/main/java/org/apache/felix/framework/cache/DirectoryRevision.java +++ b/framework/src/main/java/org/apache/felix/framework/cache/DirectoryRevision.java @@ -1,29 +1,30 @@ /* - * Copyright 2006 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework.cache; -import java.io.*; -import java.security.cert.X509Certificate; -import java.util.Map; -import java.util.jar.*; - import org.apache.felix.framework.Logger; -import org.apache.felix.framework.util.*; -import org.apache.felix.moduleloader.*; +import org.apache.felix.framework.util.StringMap; +import org.apache.felix.framework.util.WeakZipFileFactory; + +import java.io.File; +import java.io.IOException; +import java.util.Map; /** *

      @@ -32,17 +33,17 @@ * execute the bundle and does not copy the bundle content at all. *

      **/ -class DirectoryRevision extends BundleRevision +class DirectoryRevision extends BundleArchiveRevision { - private static final transient String BUNDLE_JAR_FILE = "bundle.jar"; - - private File m_refDir = null; - private Map m_header = null; + private final WeakZipFileFactory m_zipFactory; + private final File m_refDir; public DirectoryRevision( - Logger logger, File revisionRootDir, String location) throws Exception + Logger logger, Map configMap, WeakZipFileFactory zipFactory, + File revisionRootDir, String location) throws Exception { - super(logger, revisionRootDir, location); + super(logger, configMap, revisionRootDir, location); + m_zipFactory = zipFactory; m_refDir = new File(location.substring( location.indexOf(BundleArchive.FILE_PROTOCOL) + BundleArchive.FILE_PROTOCOL.length())); @@ -66,289 +67,23 @@ public DirectoryRevision( } } - public synchronized Map getManifestHeader() + public Map getManifestHeader() throws Exception { - if (m_header != null) - { - return m_header; - } - - // Read the header file from the reference directory. - InputStream is = null; - - try - { - // Open manifest file. - is = BundleCache.getSecureAction() - .getFileInputStream(new File(m_refDir, "META-INF/MANIFEST.MF")); - // Error if no jar file. - if (is == null) - { - throw new IOException("No manifest file found."); - } - - // Get manifest. - Manifest mf = new Manifest(is); - // Create a case insensitive map of manifest attributes. - m_header = new StringMap(mf.getMainAttributes(), false); - return m_header; - } - finally - { - if (is != null) is.close(); - } + File manifest = new File(m_refDir, "META-INF/MANIFEST.MF"); + return manifest.isFile() ? BundleCache.getMainAttributes(new StringMap(), BundleCache.getSecureAction().getFileInputStream(manifest), manifest.length()) : null; } - public IContent getContent() throws Exception + public Content getContent() throws Exception { - return new DirectoryContent(m_refDir); + return new DirectoryContent(getLogger(), getConfig(), m_zipFactory, + this, getRevisionRootDir(), m_refDir); } - public synchronized IContent[] getContentPath() throws Exception + protected void close() throws Exception { - // Creating the content path entails examining the bundle's - // class path to determine whether the bundle JAR file itself - // is on the bundle's class path and then creating content - // objects for everything on the class path. - - // Get the bundle's manifest header. - Map map = getManifestHeader(); - - // Find class path meta-data. - String classPath = (map == null) - ? null : (String) map.get(FelixConstants.BUNDLE_CLASSPATH); - - // Parse the class path into strings. - String[] classPathStrings = Util.parseDelimitedString( - classPath, FelixConstants.CLASS_PATH_SEPARATOR); - - if (classPathStrings == null) - { - classPathStrings = new String[0]; - } - - // Create the bundles class path. - IContent self = new DirectoryContent(m_refDir); - IContent[] contentPath = new IContent[classPathStrings.length]; - for (int i = 0; i < classPathStrings.length; i++) - { - if (classPathStrings[i].equals(FelixConstants.CLASS_PATH_DOT)) - { - contentPath[i] = self; - } - else - { - // Determine if the class path entry is a file or directory. - File file = new File(m_refDir, classPathStrings[i]); - if (BundleCache.getSecureAction().isFileDirectory(file)) - { - contentPath[i] = new DirectoryContent(file); - } - else - { - contentPath[i] = new JarContent(file); - } - } - } - - // If there is nothing on the class path, then include - // "." by default, as per the spec. - if (contentPath.length == 0) - { - contentPath = new IContent[] { self }; - } - - return contentPath; - } - -// TODO: This will need to consider security. - public String findLibrary(String libName) throws Exception - { - return BundleCache.getSecureAction().getAbsolutePath(new File(m_refDir, libName)); - } - - public void dispose() throws Exception - { - // Nothing to dispose of, since we don't maintain any state outside + // Nothing to close since we don't maintain any state outside // of the revision directory, which will be automatically deleted // by the parent bundle archive. } - - protected X509Certificate[] getRevisionCertificates() throws Exception - { - File tmp = new File(getRevisionRootDir(), BUNDLE_JAR_FILE); - - if (BundleCache.getSecureAction().fileExists(tmp)) - { - BundleCache.getSecureAction().deleteFile(tmp); - } - - try - { - BundleCache.copyStreamToFile(new RevisionToInputStream(m_refDir), - tmp); - - JarFile bundle = BundleCache.getSecureAction().openJAR(tmp); - - return getCertificatesForJar(bundle); - } - finally - { - try - { - if (BundleCache.getSecureAction().fileExists(tmp)) - { - BundleCache.getSecureAction().deleteFile(tmp); - } - } - catch (Exception e) - { - // Not much we can do - } - } - } - - private class RevisionToInputStream extends InputStream - { - class OutputStreamBuffer extends OutputStream - { - ByteArrayOutputStream outBuffer = null; - - public void write(int b) - { - outBuffer.write(b); - } - } - - private File m_revisionDir = null; - private File[] m_content = null; - private File m_manifest = null; - private ByteArrayInputStream m_buffer = null; - private int m_current = 0; - private OutputStreamBuffer m_outputBuffer = new OutputStreamBuffer(); - private JarOutputStream m_output = null; - - RevisionToInputStream(File revisionDir) throws IOException - { - m_revisionDir = revisionDir; - - m_outputBuffer.outBuffer = new ByteArrayOutputStream(); - - m_manifest = new File(m_revisionDir, "META-INF/MANIFEST.MF"); - - m_output = new JarOutputStream(m_outputBuffer); - - readNext(m_manifest, false); - - m_content = listFilesRecursive(revisionDir); - } - - private File[] listFilesRecursive(File dir) - { - File[] children = BundleCache.getSecureAction().listDirectory(dir); - File[] combined = children; - for (int i = 0; i < children.length; i++) - { - if (BundleCache.getSecureAction().isFileDirectory(children[i])) - { - File[] grandchildren = listFilesRecursive(children[i]); - if (grandchildren.length > 0) - { - File[] tmp = new File[combined.length + grandchildren.length]; - System.arraycopy(combined, 0, tmp, 0, combined.length); - System.arraycopy(grandchildren, 0, tmp, combined.length, grandchildren.length); - combined = tmp; - } - } - } - return combined; - } - - private boolean readNext(File file, boolean close) throws IOException - { - if (BundleCache.getSecureAction().isFileDirectory(file)) - { - return false; - } - - m_outputBuffer.outBuffer = new ByteArrayOutputStream(); - - InputStream in = null; - try - { - in = BundleCache.getSecureAction().getFileInputStream(file); - - JarEntry entry = new JarEntry( - file.getPath().substring(m_revisionDir.getPath().length() + 1)); - - - m_output.putNextEntry(entry); - - int c = -1; - - while ((c = in.read()) != -1) - { - m_output.write(c); - } - } - finally - { - if (in != null) - { - in.close(); - } - } - - m_output.closeEntry(); - - m_output.flush(); - - if (close) - { - m_output.close(); - m_output = null; - } - - m_buffer = new ByteArrayInputStream(m_outputBuffer.outBuffer.toByteArray()); - - m_outputBuffer.outBuffer = null; - - return true; - } - - public int read() throws IOException - { - if ((m_output == null) && (m_buffer == null)) - { - return -1; - } - - if (m_buffer != null) - { - int result = m_buffer.read(); - - if (result == -1) - { - m_buffer = null; - return read(); - } - else - { - return result; - } - } - - while ((m_current < m_content.length) && - (m_content[m_current].equals(m_manifest) || - !readNext(m_content[m_current], (m_current + 1) == m_content.length))) - { - m_current++; - } - - m_current++; - - return read(); - } - } } \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/cache/JarContent.java b/framework/src/main/java/org/apache/felix/framework/cache/JarContent.java new file mode 100644 index 00000000000..35afc9cb62e --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/cache/JarContent.java @@ -0,0 +1,429 @@ +/* + * 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.framework.cache; + +import org.apache.felix.framework.Logger; +import org.apache.felix.framework.util.FelixConstants; +import org.apache.felix.framework.util.Util; +import org.apache.felix.framework.util.WeakZipFileFactory; +import org.apache.felix.framework.util.WeakZipFileFactory.WeakZipFile; +import org.osgi.framework.Constants; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.zip.ZipEntry; + +public class JarContent implements Content +{ + private static final transient String EMBEDDED_DIRECTORY = "-embedded"; + private static final transient String LIBRARY_DIRECTORY = "-lib"; + + private final Logger m_logger; + private final Map m_configMap; + private final WeakZipFileFactory m_zipFactory; + private final Object m_revisionLock; + private final File m_rootDir; + private final File m_file; + private final WeakZipFile m_zipFile; + private final boolean m_isZipFileOwner; + private Map m_nativeLibMap; + + public JarContent(Logger logger, Map configMap, WeakZipFileFactory zipFactory, + Object revisionLock, File rootDir, File file, WeakZipFile zipFile) + { + m_logger = logger; + m_configMap = configMap; + m_zipFactory = zipFactory; + m_revisionLock = revisionLock; + m_rootDir = rootDir; + m_file = file; + if (zipFile == null) + { + try + { + m_zipFile = m_zipFactory.create(m_file); + } + catch (IOException ex) + { + throw new RuntimeException( + "Unable to open JAR file, probably deleted: " + ex.getMessage()); + } + } + else + { + m_zipFile = zipFile; + } + m_isZipFileOwner = (zipFile == null); + } + + protected void finalize() + { + close(); + } + + public void close() + { + try + { + if (m_isZipFileOwner) + { + m_zipFile.close(); + } + } + catch (Exception ex) + { + m_logger.log( + Logger.LOG_ERROR, + "JarContent: Unable to close JAR file.", ex); + } + } + + public boolean hasEntry(String name) throws IllegalStateException + { + try + { + ZipEntry ze = m_zipFile.getEntry(name); + return ze != null; + } + catch (Exception ex) + { + return false; + } + finally + { + } + } + + public Enumeration getEntries() + { + // Wrap entries enumeration to filter non-matching entries. + Enumeration e = m_zipFile.names(); + + // Spec says to return null if there are no entries. + return (e.hasMoreElements()) ? e : null; + } + + public byte[] getEntryAsBytes(String name) throws IllegalStateException + { + // Get the embedded resource. + try + { + ZipEntry ze = m_zipFile.getEntry(name); + if (ze == null) + { + return null; + } + + return BundleCache.read(m_zipFile.getInputStream(ze), ze.getSize()); + + } + catch (Exception ex) + { + m_logger.log( + Logger.LOG_ERROR, + "JarContent: Unable to read bytes for file " + name + " in ZIP file " + m_file.getAbsolutePath(), ex); + return null; + } + } + + public InputStream getEntryAsStream(String name) + throws IllegalStateException, IOException + { + // Get the embedded resource. + InputStream is = null; + + try + { + ZipEntry ze = m_zipFile.getEntry(name); + if (ze == null) + { + return null; + } + is = m_zipFile.getInputStream(ze); + if (is == null) + { + return null; + } + } + catch (Exception ex) + { + return null; + } + + return is; + } + + public URL getEntryAsURL(String name) + { + if (hasEntry(name)) + { + try + { + return new URL("jar:" + m_file.toURI().toURL().toExternalForm() + "!/" + name); + } + catch (MalformedURLException e) + { + return null; + } + } + else + { + return null; + } + } + + public Content getEntryAsContent(String entryName) + { + // If the entry name refers to the content itself, then + // just return it immediately. + if (entryName.equals(FelixConstants.CLASS_PATH_DOT)) + { + return new JarContent(m_logger, m_configMap, m_zipFactory, m_revisionLock, + m_rootDir, m_file, m_zipFile); + } + + // Remove any leading slash. + entryName = (entryName.startsWith("/")) ? entryName.substring(1) : entryName; + + if (entryName.trim().startsWith(".." + File.separatorChar) || + entryName.contains(File.separator + ".." + File.separatorChar) || + entryName.trim().endsWith(File.separator + "..") || + entryName.trim().equals("..")) + { + return null; + } + // Any embedded JAR files will be extracted to the embedded directory. + // Since embedded JAR file names may clash when extracting from multiple + // embedded JAR files, the embedded directory is per embedded JAR file. + File embedDir = new File(m_rootDir, m_file.getName() + EMBEDDED_DIRECTORY); + + // Find the entry in the JAR file and create the + // appropriate content type for it. + + // Determine if the entry is an emdedded JAR file or + // directory in the bundle JAR file. Ignore any entries + // that do not exist per the spec. + ZipEntry ze = m_zipFile.getEntry(entryName); + + if ((ze != null) && ze.isDirectory()) + { + return new ContentDirectoryContent(this, entryName); + } + else if ((ze != null) && ze.getName().endsWith(".jar")) + { + File extractJar = new File(embedDir, entryName); + + try + { + if (!BundleCache.getSecureAction().fileExists(extractJar)) + { + // Extracting the embedded JAR file impacts all other existing + // contents for this revision, so we have to grab the revision + // lock first before trying to extract the embedded JAR file + // to avoid a race condition. + synchronized (m_revisionLock) + { + if (!BundleCache.getSecureAction().fileExists(extractJar)) + { + // Make sure that the embedded JAR's parent directory exists; + // it may be in a sub-directory. + File jarDir = extractJar.getParentFile(); + if (!BundleCache.getSecureAction().fileExists(jarDir) && !BundleCache.getSecureAction().mkdirs(jarDir)) + { + throw new IOException("Unable to create embedded JAR directory."); + } + + // Extract embedded JAR into its directory. + BundleCache.copyStreamToFile(m_zipFile.getInputStream(ze), extractJar); + } + } + } + return new JarContent( + m_logger, m_configMap, m_zipFactory, m_revisionLock, + extractJar.getParentFile(), extractJar, null); + } + catch (Exception ex) + { + m_logger.log( + Logger.LOG_ERROR, + "Unable to extract embedded JAR file.", ex); + } + } + + // The entry could not be found, so return null. + return null; + } + +// TODO: SECURITY - This will need to consider security. + public String getEntryAsNativeLibrary(String entryName) + { + // Return result. + String result = null; + + // Remove any leading slash. + entryName = (entryName.startsWith("/")) ? entryName.substring(1) : entryName; + + if (entryName.trim().startsWith(".." + File.separatorChar) || + entryName.contains(File.separator + ".." + File.separatorChar) || + entryName.trim().endsWith(File.separator + "..") || + entryName.trim().equals("..")) + { + return null; + } + + // Any embedded native libraries will be extracted to the lib directory. + // Since embedded library file names may clash when extracting from multiple + // embedded JAR files, the embedded lib directory is per embedded JAR file. + File libDir = new File(m_rootDir, m_file.getName() + LIBRARY_DIRECTORY); + + // The entry name must refer to a file type, since it is + // a native library, not a directory. + ZipEntry ze = m_zipFile.getEntry(entryName); + if ((ze != null) && !ze.isDirectory()) + { + // Extracting the embedded native library file impacts all other + // existing contents for this revision, so we have to grab the + // revision lock first before trying to extract the embedded JAR + // file to avoid a race condition. + synchronized (m_revisionLock) + { + // Since native libraries cannot be shared, we must extract a + // separate copy per request, so use the request library counter + // as part of the extracted path. + if (m_nativeLibMap == null) + { + m_nativeLibMap = new HashMap(); + } + Integer libCount = (Integer) m_nativeLibMap.get(entryName); + // Either set or increment the library count. + libCount = (libCount == null) ? new Integer(0) : new Integer(libCount.intValue() + 1); + m_nativeLibMap.put(entryName, libCount); + File libFile = new File( + libDir, libCount.toString() + File.separatorChar + entryName); + + if (!BundleCache.getSecureAction().fileExists(libFile)) + { + if (!BundleCache.getSecureAction().fileExists(libFile.getParentFile()) + && !BundleCache.getSecureAction().mkdirs(libFile.getParentFile())) + { + m_logger.log( + Logger.LOG_ERROR, + "Unable to create library directory."); + } + else + { + try + { + // Create the file. + BundleCache.copyStreamToFile(m_zipFile.getInputStream(ze), libFile); + + // Perform exec permission command on extracted library + // if one is configured. + String command = (String) m_configMap.get( + Constants.FRAMEWORK_EXECPERMISSION); + if (command != null) + { + Properties props = new Properties(); + props.setProperty("abspath", libFile.toString()); + command = Util.substVars(command, "command", null, props); + Process p = BundleCache.getSecureAction().exec(command); + // We have to make sure we read stdout and stderr because + // otherwise we will block on certain unbuffered os's + // (like eg. windows) + Thread stdOut = new Thread( + new DevNullRunnable(p.getInputStream())); + Thread stdErr = new Thread( + new DevNullRunnable(p.getErrorStream())); + stdOut.setDaemon(true); + stdErr.setDaemon(true); + stdOut.start(); + stdErr.start(); + p.waitFor(); + stdOut.join(); + stdErr.join(); + } + + // Return the path to the extracted native library. + result = BundleCache.getSecureAction().getAbsolutePath(libFile); + } + catch (Exception ex) + { + m_logger.log( + Logger.LOG_ERROR, + "Extracting native library.", ex); + } + } + } + else + { + // Return the path to the extracted native library. + result = BundleCache.getSecureAction().getAbsolutePath(libFile); + } + } + } + + return result; + } + + public String toString() + { + return "JAR " + m_file.getPath(); + } + + public File getFile() + { + return m_file; + } + + private static class DevNullRunnable implements Runnable + { + private final InputStream m_in; + + public DevNullRunnable(InputStream in) + { + m_in = in; + } + + public void run() + { + try + { + try + { + while (m_in.read() != -1){} + } + finally + { + m_in.close(); + } + } + catch (Exception ex) + { + // Not much we can do - maybe we should log it? + } + } + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/cache/JarRevision.java b/framework/src/main/java/org/apache/felix/framework/cache/JarRevision.java index 23d6f9ae0c2..ac5c1e8ac94 100644 --- a/framework/src/main/java/org/apache/felix/framework/cache/JarRevision.java +++ b/framework/src/main/java/org/apache/felix/framework/cache/JarRevision.java @@ -1,34 +1,38 @@ /* - * Copyright 2006 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework.cache; -import java.io.*; +import org.apache.felix.framework.Logger; +import org.apache.felix.framework.util.StringMap; +import org.apache.felix.framework.util.Util; +import org.apache.felix.framework.util.WeakZipFileFactory; +import org.apache.felix.framework.util.WeakZipFileFactory.WeakZipFile; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; -import java.security.PrivilegedActionException; -import java.security.cert.X509Certificate; -import java.util.*; -import java.util.jar.*; +import java.util.Map; import java.util.zip.ZipEntry; -import org.apache.felix.framework.Logger; -import org.apache.felix.framework.util.*; -import org.apache.felix.moduleloader.*; - /** *

      * This class implements a bundle archive revision for a standard bundle @@ -40,28 +44,22 @@ * directory, such as embedded JAR files and native libraries. *

      **/ -class JarRevision extends BundleRevision +class JarRevision extends BundleArchiveRevision { private static final transient String BUNDLE_JAR_FILE = "bundle.jar"; - private static final transient String EMBEDDED_DIRECTORY = "embedded"; - private static final transient String LIBRARY_DIRECTORY = "lib"; - private File m_bundleFile = null; - private Map m_header = null; + private final WeakZipFileFactory m_zipFactory; + private final File m_bundleFile; + private final WeakZipFile m_zipFile; public JarRevision( - Logger logger, File revisionRootDir, String location, boolean byReference) + Logger logger, Map configMap, WeakZipFileFactory zipFactory, + File revisionRootDir, String location, boolean byReference, InputStream is) throws Exception { - this(logger, revisionRootDir, location, byReference, null); - } + super(logger, configMap, revisionRootDir, location); - public JarRevision( - Logger logger, File revisionRootDir, String location, - boolean byReference, InputStream is) - throws Exception - { - super(logger, revisionRootDir, location); + m_zipFactory = zipFactory; if (byReference) { @@ -76,171 +74,46 @@ public JarRevision( // Save and process the bundle JAR. initialize(byReference, is); - } - - public synchronized Map getManifestHeader() throws Exception - { - if (m_header != null) - { - return m_header; - } - - // Get the embedded resource. - JarFile jarFile = null; + // Open shared copy of the JAR file. + WeakZipFile zipFile = null; try { // Open bundle JAR file. - jarFile = BundleCache.getSecureAction().openJAR(m_bundleFile); + zipFile = m_zipFactory.create(m_bundleFile); // Error if no jar file. - if (jarFile == null) + if (zipFile == null) { throw new IOException("No JAR file found."); } - // Get manifest. - Manifest mf = jarFile.getManifest(); - // Create a case insensitive map of manifest attributes. - m_header = new StringMap(mf.getMainAttributes(), false); - return m_header; - + m_zipFile = zipFile; } - finally + catch (Exception ex) { - if (jarFile != null) jarFile.close(); + if (zipFile != null) zipFile.close(); + throw ex; } } - public IContent getContent() throws Exception + public Map getManifestHeader() throws Exception { - return new JarContent(m_bundleFile); - } - - public synchronized IContent[] getContentPath() throws Exception - { - // Creating the content path entails examining the bundle's - // class path to determine whether the bundle JAR file itself - // is on the bundle's class path and then creating content - // objects for everything on the class path. - - File embedDir = new File(getRevisionRootDir(), EMBEDDED_DIRECTORY); - - // Get the bundle's manifest header. - Map map = getManifestHeader(); + // Read and parse headers into a case insensitive map of manifest attributes and return it. + ZipEntry manifestEntry = m_zipFile.getEntry("META-INF/MANIFEST.MF"); - // Find class path meta-data. - String classPath = (map == null) - ? null : (String) map.get(FelixConstants.BUNDLE_CLASSPATH); - - // Parse the class path into strings. - String[] classPathStrings = Util.parseDelimitedString( - classPath, FelixConstants.CLASS_PATH_SEPARATOR); - - if (classPathStrings == null) - { - classPathStrings = new String[0]; - } + Map manifest = manifestEntry != null ? BundleCache.getMainAttributes(new StringMap(), m_zipFile.getInputStream(manifestEntry), manifestEntry.getSize()) : null; - // Create the bundles class path. - JarFile bundleJar = null; - try - { - bundleJar = BundleCache.getSecureAction().openJAR(m_bundleFile); - IContent self = new JarContent(m_bundleFile); - IContent[] contentPath = new IContent[classPathStrings.length]; - for (int i = 0; i < classPathStrings.length; i++) - { - if (classPathStrings[i].equals(FelixConstants.CLASS_PATH_DOT)) - { - contentPath[i] = self; - } - else - { - // Determine if the class path entry is a file or directory - // in the bundle JAR file. - ZipEntry entry = bundleJar.getEntry(classPathStrings[i]); - if ((entry != null) && entry.isDirectory()) - { - contentPath[i] = new ContentDirectoryContent(self, classPathStrings[i]); - } - else - { - contentPath[i] = new JarContent(new File(embedDir, classPathStrings[i])); - } - } - } - - // If there is nothing on the class path, then include - // "." by default, as per the spec. - if (contentPath.length == 0) - { - contentPath = new IContent[] { self }; - } - - return contentPath; - } - finally - { - if (bundleJar != null) bundleJar.close(); - } + return manifest; } -// TODO: This will need to consider security. - public synchronized String findLibrary(String libName) throws Exception + public Content getContent() throws Exception { - // Get bundle lib directory. - File libDir = new File(getRevisionRootDir(), LIBRARY_DIRECTORY); - // Get lib file. - File libFile = new File(libDir, File.separatorChar + libName); - // Make sure that the library's parent directory exists; - // it may be in a sub-directory. - libDir = libFile.getParentFile(); - if (!BundleCache.getSecureAction().fileExists(libDir)) - { - if (!BundleCache.getSecureAction().mkdirs(libDir)) - { - throw new IOException("Unable to create library directory."); - } - } - // Extract the library from the JAR file if it does not - // already exist. - if (!BundleCache.getSecureAction().fileExists(libFile)) - { - JarFile bundleJar = null; - InputStream is = null; - - try - { - bundleJar = BundleCache.getSecureAction().openJAR(m_bundleFile); - ZipEntry ze = bundleJar.getEntry(libName); - if (ze == null) - { - throw new IOException("No JAR entry: " + libName); - } - is = new BufferedInputStream( - bundleJar.getInputStream(ze), BundleCache.BUFSIZE); - if (is == null) - { - throw new IOException("No input stream: " + libName); - } - - // Create the file. - BundleCache.copyStreamToFile(is, libFile); - } - finally - { - if (bundleJar != null) bundleJar.close(); - if (is != null) is.close(); - } - } - - return BundleCache.getSecureAction().getAbsolutePath(libFile); + return new JarContent(getLogger(), getConfig(), m_zipFactory, + this, getRevisionRootDir(), m_bundleFile, m_zipFile); } - public void dispose() throws Exception + protected void close() throws Exception { - // Nothing to dispose of, since we don't maintain any state outside - // of the revision directory, which will be automatically deleted - // by the parent bundle archive. + m_zipFile.close(); } // @@ -252,196 +125,65 @@ private void initialize(boolean byReference, InputStream is) { try { - // If the revision directory exists, then we don't - // need to initialize since it has already been done. - if (BundleCache.getSecureAction().fileExists(getRevisionRootDir())) + // If the revision directory does not exist, then create it. + if (!BundleCache.getSecureAction().fileExists(getRevisionRootDir())) { - return; - } - - // Create revision directory. - if (!BundleCache.getSecureAction().mkdir(getRevisionRootDir())) - { - getLogger().log( - Logger.LOG_ERROR, - getClass().getName() + ": Unable to create revision directory."); - throw new IOException("Unable to create archive directory."); - } + if (!BundleCache.getSecureAction().mkdir(getRevisionRootDir())) + { + getLogger().log( + Logger.LOG_ERROR, + getClass().getName() + ": Unable to create revision directory."); + throw new IOException("Unable to create archive directory."); + } - if (!byReference) - { - if (is == null) + if (!byReference) { - // Do it the manual way to have a chance to - // set request properties such as proxy auth. - URL url = new URL(getLocation()); - URLConnection conn = url.openConnection(); + URLConnection conn = null; + try + { + if (is == null) + { + // Do it the manual way to have a chance to + // set request properties such as proxy auth. + URL url = BundleCache.getSecureAction().createURL( + null, getLocation(), null); + conn = url.openConnection(); + + // Support for http proxy authentication. + String auth = BundleCache.getSecureAction() + .getSystemProperty("http.proxyAuth", null); + if ((auth != null) && (auth.length() > 0)) + { + if ("http".equals(url.getProtocol()) || + "https".equals(url.getProtocol())) + { + String base64 = Util.base64Encode(auth); + conn.setRequestProperty( + "Proxy-Authorization", "Basic " + base64); + } + } + is = BundleCache.getSecureAction() + .getURLConnectionInputStream(conn); + } - // Support for http proxy authentication. - String auth = BundleCache.getSecureAction() - .getSystemProperty("http.proxyAuth", null); - if ((auth != null) && (auth.length() > 0)) + // Save the bundle jar file. + BundleCache.copyStreamToFile(is, m_bundleFile); + } + finally { - if ("http".equals(url.getProtocol()) || - "https".equals(url.getProtocol())) + // This is a hack to fix an issue on Android, where + // HttpURLConnections are not properly closed. (FELIX-2728) + if ((conn != null) && (conn instanceof HttpURLConnection)) { - String base64 = Util.base64Encode(auth); - conn.setRequestProperty( - "Proxy-Authorization", "Basic " + base64); + ((HttpURLConnection) conn).disconnect(); } } - is = BundleCache.getSecureAction().getURLConnectionInputStream(conn); } - - // Save the bundle jar file. - BundleCache.copyStreamToFile(is, m_bundleFile); } - - preprocessBundleJar(); } finally { if (is != null) is.close(); } } - - /** - * This method pre-processes a bundle JAR file making it ready - * for use. This entails extracting all embedded JAR files and - * all native libraries. - * @throws java.lang.Exception if any error occurs while processing JAR file. - **/ - private void preprocessBundleJar() throws Exception - { - // - // Create special directories so that we can avoid checking - // for their existence all the time. - // - - File embedDir = new File(getRevisionRootDir(), EMBEDDED_DIRECTORY); - if (!BundleCache.getSecureAction().fileExists(embedDir)) - { - if (!BundleCache.getSecureAction().mkdir(embedDir)) - { - throw new IOException("Could not create embedded JAR directory."); - } - } - - File libDir = new File(getRevisionRootDir(), LIBRARY_DIRECTORY); - if (!BundleCache.getSecureAction().fileExists(libDir)) - { - if (!BundleCache.getSecureAction().mkdir(libDir)) - { - throw new IOException("Unable to create native library directory."); - } - } - - // - // This block extracts all embedded JAR files. - // - - try - { - // Get the bundle's manifest header. - Map map = getManifestHeader(); - - // Find class path meta-data. - String classPath = (map == null) - ? null : (String) map.get(FelixConstants.BUNDLE_CLASSPATH); - - // Parse the class path into strings. - String[] classPathStrings = Util.parseDelimitedString( - classPath, FelixConstants.CLASS_PATH_SEPARATOR); - - if (classPathStrings == null) - { - classPathStrings = new String[0]; - } - - for (int i = 0; i < classPathStrings.length; i++) - { - if (!classPathStrings[i].equals(FelixConstants.CLASS_PATH_DOT)) - { - extractEmbeddedJar(classPathStrings[i]); - } - } - - } - catch (PrivilegedActionException ex) - { - throw ((PrivilegedActionException) ex).getException(); - } - } - - /** - * This method extracts an embedded JAR file from the bundle's - * JAR file. - * @param id the identifier of the bundle that owns the embedded JAR file. - * @param jarPath the path to the embedded JAR file inside the bundle JAR file. - **/ - private void extractEmbeddedJar(String jarPath) - throws Exception - { - // Remove leading slash if present. - jarPath = (jarPath.length() > 0) && (jarPath.charAt(0) == '/') - ? jarPath.substring(1) : jarPath; - - // If JAR is already extracted, then don't re-extract it... - File jarFile = new File( - getRevisionRootDir(), EMBEDDED_DIRECTORY + File.separatorChar + jarPath); - - if (!BundleCache.getSecureAction().fileExists(jarFile)) - { - JarFile bundleJar = null; - InputStream is = null; - try - { - // Make sure class path entry is a JAR file. - bundleJar = BundleCache.getSecureAction().openJAR(m_bundleFile); - ZipEntry ze = bundleJar.getEntry(jarPath); - if (ze == null) - { -// TODO: FRAMEWORK - Per the spec, this should fire a FrameworkEvent.INFO event; -// need to create an "Eventer" class like "Logger" perhaps. - getLogger().log(Logger.LOG_INFO, "Class path entry not found: " + jarPath); - return; - } - // If the zip entry is a directory, then ignore it since - // we don't need to extact it; otherwise, it points to an - // embedded JAR file, so extract it. - else if (!ze.isDirectory()) - { - // Make sure that the embedded JAR's parent directory exists; - // it may be in a sub-directory. - File jarDir = jarFile.getParentFile(); - if (!BundleCache.getSecureAction().fileExists(jarDir)) - { - if (!BundleCache.getSecureAction().mkdirs(jarDir)) - { - throw new IOException("Unable to create embedded JAR directory."); - } - } - - // Extract embedded JAR into its directory. - is = new BufferedInputStream(bundleJar.getInputStream(ze), BundleCache.BUFSIZE); - if (is == null) - { - throw new IOException("No input stream: " + jarPath); - } - // Copy the file. - BundleCache.copyStreamToFile(is, jarFile); - } - } - finally - { - if (bundleJar != null) bundleJar.close(); - if (is != null) is.close(); - } - } - } - - protected X509Certificate[] getRevisionCertificates() throws Exception - { - return getCertificatesForJar(BundleCache.getSecureAction().openJAR(m_bundleFile, true)); - } -} +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/cache/SystemBundleArchive.java b/framework/src/main/java/org/apache/felix/framework/cache/SystemBundleArchive.java deleted file mode 100644 index 99bd5b03401..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/cache/SystemBundleArchive.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.cache; - -import java.io.File; -import java.io.InputStream; -import java.security.cert.X509Certificate; -import java.util.Map; - -import org.apache.felix.framework.util.FelixConstants; -import org.apache.felix.moduleloader.IContent; -import org.apache.felix.moduleloader.IModule; -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleActivator; - -/** - *

      - * This class represents the bundle archive of the system bundle. It is a - * special case that is mostly just an empty implementation, since the system - * bundle is not a real archive. - *

      -**/ -public class SystemBundleArchive extends BundleArchive -{ - private Map m_headerMap = null; - private BundleRevision m_revision = null; - - public SystemBundleArchive() - { - m_revision = new BundleRevision() { - - public Map getManifestHeader() throws Exception - { - return m_headerMap; - } - - public IContent getContent() throws Exception - { - return null; - } - - public IContent[] getContentPath() throws Exception - { - return null; - } - - public String findLibrary(String libName) throws Exception - { - return null; - } - - public void dispose() throws Exception - { - } - - protected X509Certificate[] getRevisionCertificates() - { - return null; - } - }; - } - - public long getId() - { - return 0; - } - - public String getLocation() throws Exception - { - return FelixConstants.SYSTEM_BUNDLE_LOCATION; - } - - public String getCurrentLocation() throws Exception - { - return null; - } - - public void setCurrentLocation(String location) throws Exception - { - } - - public int getPersistentState() throws Exception - { - return Bundle.ACTIVE; - } - - public void setPersistentState(int state) throws Exception - { - } - - public int getStartLevel() throws Exception - { - return FelixConstants.SYSTEMBUNDLE_DEFAULT_STARTLEVEL; - } - - public void setStartLevel(int level) throws Exception - { - } - - public File getDataFile(String fileName) throws Exception - { - return null; - } - - public BundleActivator getActivator(IModule module) - throws Exception - { - return null; - } - - public void setActivator(Object obj) throws Exception - { - } - - public int getRevisionCount() - { - return 1; - } - - public BundleRevision getRevision(int i) - { - return m_revision; - } - - public void revise(String location, InputStream is) - throws Exception - { - } - - public void purge() throws Exception - { - } - - public void dispose() throws Exception - { - } - - public Map getManifestHeader(int revision) - throws Exception - { - return m_headerMap; - } - - public void setManifestHeader(Map headerMap) - { - m_headerMap = headerMap; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/capabilityset/CapabilitySet.java b/framework/src/main/java/org/apache/felix/framework/capabilityset/CapabilitySet.java new file mode 100644 index 00000000000..71167516fa8 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/capabilityset/CapabilitySet.java @@ -0,0 +1,663 @@ +/* + * 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.framework.capabilityset; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentSkipListMap; + +import org.apache.felix.framework.util.SecureAction; +import org.apache.felix.framework.util.StringComparator; +import org.apache.felix.framework.wiring.BundleCapabilityImpl; +import org.osgi.framework.Version; +import org.osgi.framework.VersionRange; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.resource.Capability; + +public class CapabilitySet +{ + private final SortedMap>> m_indices; // Should also be concurrent! + private final Set m_capSet = Collections.newSetFromMap(new ConcurrentHashMap()); + private final static SecureAction m_secureAction = new SecureAction(); + + public void dump() + { + for (Entry>> entry : m_indices.entrySet()) + { + boolean header1 = false; + for (Entry> entry2 : entry.getValue().entrySet()) + { + boolean header2 = false; + for (BundleCapability cap : entry2.getValue()) + { + if (cap.getRevision().getBundle().getBundleId() != 0) + { + if (!header1) + { + System.out.println(entry.getKey() + ":"); + header1 = true; + } + if (!header2) + { + System.out.println(" " + entry2.getKey()); + header2 = true; + } + System.out.println(" " + cap); + } + } + } + } + } + + public CapabilitySet(final List indexProps, final boolean caseSensitive) + { + m_indices = (caseSensitive) + ? new ConcurrentSkipListMap>>() + : new ConcurrentSkipListMap>>( + StringComparator.COMPARATOR); + for (int i = 0; (indexProps != null) && (i < indexProps.size()); i++) + { + m_indices.put( + indexProps.get(i), new ConcurrentHashMap>()); + } + } + + public void addCapability(final BundleCapability cap) + { + m_capSet.add(cap); + + // Index capability. + for (Entry>> entry : m_indices.entrySet()) + { + Object value = cap.getAttributes().get(entry.getKey()); + if (value != null) + { + if (value.getClass().isArray()) + { + value = convertArrayToList(value); + } + + ConcurrentMap> index = + (ConcurrentMap>) entry.getValue(); + + if (value instanceof Collection) + { + Collection c = (Collection) value; + for (Object o : c) + { + indexCapability(index, cap, o); + } + } + else + { + indexCapability(index, cap, value); + } + } + } + } + + private void indexCapability( + ConcurrentMap> index, BundleCapability cap, Object capValue) + { + Set caps = Collections.newSetFromMap(new ConcurrentHashMap()); + Set prevval = index.putIfAbsent(capValue, caps); + if (prevval != null) + caps = prevval; + caps.add(cap); + } + + public void removeCapability(final BundleCapability cap) + { + if (m_capSet.remove(cap)) + { + for (Entry>> entry : m_indices.entrySet()) + { + Object value = cap.getAttributes().get(entry.getKey()); + if (value != null) + { + if (value.getClass().isArray()) + { + value = convertArrayToList(value); + } + + Map> index = entry.getValue(); + + if (value instanceof Collection) + { + Collection c = (Collection) value; + for (Object o : c) + { + deindexCapability(index, cap, o); + } + } + else + { + deindexCapability(index, cap, value); + } + } + } + } + } + + private void deindexCapability( + Map> index, BundleCapability cap, Object value) + { + Set caps = index.get(value); + if (caps != null) + { + caps.remove(cap); + if (caps.isEmpty()) + { + index.remove(value); + } + } + } + + public Set match(final SimpleFilter sf, final boolean obeyMandatory) + { + final Set matches = match(m_capSet, sf); + return (obeyMandatory) + ? matchMandatory(matches, sf) + : matches; + } + + private Set match(Set caps, final SimpleFilter sf) + { + Set matches = Collections.newSetFromMap(new ConcurrentHashMap()); + + if (sf.getOperation() == SimpleFilter.MATCH_ALL) + { + matches.addAll(caps); + } + else if (sf.getOperation() == SimpleFilter.AND) + { + // Evaluate each subfilter against the remaining capabilities. + // For AND we calculate the intersection of each subfilter. + // We can short-circuit the AND operation if there are no + // remaining capabilities. + final List sfs = (List) sf.getValue(); + for (int i = 0; (caps.size() > 0) && (i < sfs.size()); i++) + { + matches = match(caps, sfs.get(i)); + caps = matches; + } + } + else if (sf.getOperation() == SimpleFilter.OR) + { + // Evaluate each subfilter against the remaining capabilities. + // For OR we calculate the union of each subfilter. + List sfs = (List) sf.getValue(); + for (int i = 0; i < sfs.size(); i++) + { + matches.addAll(match(caps, sfs.get(i))); + } + } + else if (sf.getOperation() == SimpleFilter.NOT) + { + // Evaluate each subfilter against the remaining capabilities. + // For OR we calculate the union of each subfilter. + matches.addAll(caps); + List sfs = (List) sf.getValue(); + for (int i = 0; i < sfs.size(); i++) + { + matches.removeAll(match(caps, sfs.get(i))); + } + } + else + { + Map> index = m_indices.get(sf.getName()); + if ((sf.getOperation() == SimpleFilter.EQ) && (index != null)) + { + Set existingCaps = index.get(sf.getValue()); + if (existingCaps != null) + { + matches.addAll(existingCaps); + if (caps != m_capSet) + { + matches.retainAll(caps); + } + } + } + else + { + for (Iterator it = caps.iterator(); it.hasNext(); ) + { + Capability cap = it.next(); + Object lhs = cap.getAttributes().get(sf.getName()); + if (lhs != null) + { + if (compare(lhs, sf.getValue(), sf.getOperation())) + { + matches.add(cap); + } + } + } + } + } + + return matches; + } + + public static boolean matches(Capability cap, SimpleFilter sf) + { + return matchesInternal(cap, sf) && matchMandatory(cap, sf); + } + + private static boolean matchesInternal(Capability cap, SimpleFilter sf) + { + boolean matched = true; + + if (sf.getOperation() == SimpleFilter.MATCH_ALL) + { + matched = true; + } + else if (sf.getOperation() == SimpleFilter.AND) + { + // Evaluate each subfilter against the remaining capabilities. + // For AND we calculate the intersection of each subfilter. + // We can short-circuit the AND operation if there are no + // remaining capabilities. + List sfs = (List) sf.getValue(); + for (int i = 0; matched && (i < sfs.size()); i++) + { + matched = matchesInternal(cap, sfs.get(i)); + } + } + else if (sf.getOperation() == SimpleFilter.OR) + { + // Evaluate each subfilter against the remaining capabilities. + // For OR we calculate the union of each subfilter. + matched = false; + List sfs = (List) sf.getValue(); + for (int i = 0; !matched && (i < sfs.size()); i++) + { + matched = matchesInternal(cap, sfs.get(i)); + } + } + else if (sf.getOperation() == SimpleFilter.NOT) + { + // Evaluate each subfilter against the remaining capabilities. + // For OR we calculate the union of each subfilter. + List sfs = (List) sf.getValue(); + for (int i = 0; i < sfs.size(); i++) + { + matched = !(matchesInternal(cap, sfs.get(i))); + } + } + else + { + matched = false; + Object lhs = cap.getAttributes().get(sf.getName()); + if (lhs != null) + { + matched = compare(lhs, sf.getValue(), sf.getOperation()); + } + } + + return matched; + } + + private static Set matchMandatory( + Set caps, SimpleFilter sf) + { + for (Iterator it = caps.iterator(); it.hasNext(); ) + { + Capability cap = it.next(); + if (!matchMandatory(cap, sf)) + { + it.remove(); + } + } + return caps; + } + + private static boolean matchMandatory(Capability cap, SimpleFilter sf) + { + Map attrs = cap.getAttributes(); + for (Entry entry : attrs.entrySet()) + { + if (((BundleCapabilityImpl) cap).isAttributeMandatory(entry.getKey()) + && !matchMandatoryAttribute(entry.getKey(), sf)) + { + return false; + } + } + return true; + } + + private static boolean matchMandatoryAttribute(String attrName, SimpleFilter sf) + { + if ((sf.getName() != null) && sf.getName().equals(attrName)) + { + return true; + } + else if (sf.getOperation() == SimpleFilter.AND) + { + List list = (List) sf.getValue(); + for (int i = 0; i < list.size(); i++) + { + SimpleFilter sf2 = (SimpleFilter) list.get(i); + if ((sf2.getName() != null) + && sf2.getName().equals(attrName)) + { + return true; + } + } + } + return false; + } + + private static final Class[] STRING_CLASS = new Class[] { String.class }; + private static final String VALUE_OF_METHOD_NAME = "valueOf"; + + private static boolean compare(Object lhs, Object rhsUnknown, int op) + { + if (lhs == null) + { + return false; + } + + // If this is a PRESENT operation, then just return true immediately + // since we wouldn't be here if the attribute wasn't present. + if (op == SimpleFilter.PRESENT) + { + return true; + } + + //Need a special case here when lhs is a Version and rhs is a VersionRange + //Version is comparable so we need to check this first + if(lhs instanceof Version && op == SimpleFilter.EQ) + { + Object rhs = null; + try + { + rhs = coerceType(lhs, (String) rhsUnknown); + } + catch (Exception ex) + { + //Do nothing will check later if rhs is null + } + + if(rhs != null && rhs instanceof VersionRange) + { + return ((VersionRange)rhs).includes((Version)lhs); + } + } + + // If the type is comparable, then we can just return the + // result immediately. + if (lhs instanceof Comparable) + { + // Spec says SUBSTRING is false for all types other than string. + if ((op == SimpleFilter.SUBSTRING) && !(lhs instanceof String)) + { + return false; + } + + Object rhs; + if (op == SimpleFilter.SUBSTRING) + { + rhs = rhsUnknown; + } + else + { + try + { + rhs = coerceType(lhs, (String) rhsUnknown); + } + catch (Exception ex) + { + return false; + } + } + + switch (op) + { + case SimpleFilter.EQ : + try + { + return (((Comparable) lhs).compareTo(rhs) == 0); + } + catch (Exception ex) + { + return false; + } + case SimpleFilter.GTE : + try + { + return (((Comparable) lhs).compareTo(rhs) >= 0); + } + catch (Exception ex) + { + return false; + } + case SimpleFilter.LTE : + try + { + return (((Comparable) lhs).compareTo(rhs) <= 0); + } + catch (Exception ex) + { + return false; + } + case SimpleFilter.APPROX : + return compareApproximate(lhs, rhs); + case SimpleFilter.SUBSTRING : + return SimpleFilter.compareSubstring((List) rhs, (String) lhs); + default: + throw new RuntimeException( + "Unknown comparison operator: " + op); + } + } + // Booleans do not implement comparable, so special case them. + else if (lhs instanceof Boolean) + { + Object rhs; + try + { + rhs = coerceType(lhs, (String) rhsUnknown); + } + catch (Exception ex) + { + return false; + } + + switch (op) + { + case SimpleFilter.EQ : + case SimpleFilter.GTE : + case SimpleFilter.LTE : + case SimpleFilter.APPROX : + return (lhs.equals(rhs)); + default: + throw new RuntimeException( + "Unknown comparison operator: " + op); + } + } + + // If the LHS is not a comparable or boolean, check if it is an + // array. If so, convert it to a list so we can treat it as a + // collection. + if (lhs.getClass().isArray()) + { + lhs = convertArrayToList(lhs); + } + + // If LHS is a collection, then call compare() on each element + // of the collection until a match is found. + if (lhs instanceof Collection) + { + for (Iterator iter = ((Collection) lhs).iterator(); iter.hasNext(); ) + { + if (compare(iter.next(), rhsUnknown, op)) + { + return true; + } + } + + return false; + } + + // Spec says SUBSTRING is false for all types other than string. + if ((op == SimpleFilter.SUBSTRING) && !(lhs instanceof String)) + { + return false; + } + + // Since we cannot identify the LHS type, then we can only perform + // equality comparison. + try + { + return lhs.equals(coerceType(lhs, (String) rhsUnknown)); + } + catch (Exception ex) + { + return false; + } + } + + private static boolean compareApproximate(Object lhs, Object rhs) + { + if (rhs instanceof String) + { + return removeWhitespace((String) lhs) + .equalsIgnoreCase(removeWhitespace((String) rhs)); + } + else if (rhs instanceof Character) + { + return Character.toLowerCase(((Character) lhs)) + == Character.toLowerCase(((Character) rhs)); + } + return lhs.equals(rhs); + } + + private static String removeWhitespace(String s) + { + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) + { + if (!Character.isWhitespace(s.charAt(i))) + { + sb.append(s.charAt(i)); + } + } + return sb.toString(); + } + + private static Object coerceType(Object lhs, String rhsString) throws Exception + { + // If the LHS expects a string, then we can just return + // the RHS since it is a string. + if (lhs.getClass() == rhsString.getClass()) + { + return rhsString; + } + + // Try to convert the RHS type to the LHS type by using + // the string constructor of the LHS class, if it has one. + Object rhs = null; + try + { + // The Character class is a special case, since its constructor + // does not take a string, so handle it separately. + if (lhs instanceof Character) + { + rhs = new Character(rhsString.charAt(0)); + } + else if(lhs instanceof Version && rhsString.indexOf(',') >= 0) + { + rhs = new VersionRange(rhsString); + } + else + { + // Spec says we should trim number types. + if ((lhs instanceof Number) || (lhs instanceof Boolean)) + { + rhsString = rhsString.trim(); + } + + try + { + // Try to find a suitable static valueOf method + Method valueOfMethod = m_secureAction.getDeclaredMethod( + lhs.getClass(), VALUE_OF_METHOD_NAME, STRING_CLASS); + if (valueOfMethod.getReturnType().isAssignableFrom(lhs.getClass()) + && ((valueOfMethod.getModifiers() & Modifier.STATIC) > 0)) + { + m_secureAction.setAccesssible(valueOfMethod); + rhs = valueOfMethod.invoke(null, new Object[] { rhsString }); + } + } + catch (Exception ex) + { + // Static valueOf fails, try the next conversion mechanism + } + + if (rhs == null) + { + Constructor ctor = m_secureAction.getConstructor(lhs.getClass(), STRING_CLASS); + m_secureAction.setAccesssible(ctor); + rhs = ctor.newInstance(new Object[] { rhsString }); + } + } + } + catch (Exception ex) + { + throw new Exception( + "Could not instantiate class " + + lhs.getClass().getName() + + " from string constructor with argument '" + + rhsString + "' because " + ex); + } + + return rhs; + } + + /** + * This is an ugly utility method to convert an array of primitives + * to an array of primitive wrapper objects. This method simplifies + * processing LDAP filters since the special case of primitive arrays + * can be ignored. + * @param array An array of primitive types. + * @return An corresponding array using pritive wrapper objects. + **/ + private static List convertArrayToList(Object array) + { + int len = Array.getLength(array); + List list = new ArrayList(len); + for (int i = 0; i < len; i++) + { + list.add(Array.get(array, i)); + } + return list; + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/capabilityset/SimpleFilter.java b/framework/src/main/java/org/apache/felix/framework/capabilityset/SimpleFilter.java new file mode 100644 index 00000000000..253c70d99da --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/capabilityset/SimpleFilter.java @@ -0,0 +1,652 @@ +/* + * 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.framework.capabilityset; + +import org.osgi.framework.Version; +import org.osgi.framework.VersionRange; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +public class SimpleFilter +{ + public static final int MATCH_ALL = 0; + public static final int AND = 1; + public static final int OR = 2; + public static final int NOT = 3; + public static final int EQ = 4; + public static final int LTE = 5; + public static final int GTE = 6; + public static final int SUBSTRING = 7; + public static final int PRESENT = 8; + public static final int APPROX = 9; + + private final String m_name; + private final Object m_value; + private final int m_op; + + public SimpleFilter(String attr, Object value, int op) + { + m_name = attr; + m_value = value; + m_op = op; + } + + public String getName() + { + return m_name; + } + + public Object getValue() + { + return m_value; + } + + public int getOperation() + { + return m_op; + } + + public String toString() + { + String s = null; + switch (m_op) + { + case AND: + s = "(&" + toString((List) m_value) + ")"; + break; + case OR: + s = "(|" + toString((List) m_value) + ")"; + break; + case NOT: + s = "(!" + toString((List) m_value) + ")"; + break; + case EQ: + s = "(" + m_name + "=" + toEncodedString(m_value) + ")"; + break; + case LTE: + s = "(" + m_name + "<=" + toEncodedString(m_value) + ")"; + break; + case GTE: + s = "(" + m_name + ">=" + toEncodedString(m_value) + ")"; + break; + case SUBSTRING: + s = "(" + m_name + "=" + unparseSubstring((List) m_value) + ")"; + break; + case PRESENT: + s = "(" + m_name + "=*)"; + break; + case APPROX: + s = "(" + m_name + "~=" + toEncodedString(m_value) + ")"; + break; + case MATCH_ALL: + s = "(*)"; + break; + } + return s; + } + + private static String toString(List list) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < list.size(); i++) + { + sb.append(list.get(i).toString()); + } + return sb.toString(); + } + + private static String toDecodedString(String s, int startIdx, int endIdx) + { + StringBuilder sb = new StringBuilder(endIdx - startIdx); + boolean escaped = false; + for (int i = 0; i < (endIdx - startIdx); i++) + { + char c = s.charAt(startIdx + i); + if (!escaped && (c == '\\')) + { + escaped = true; + } + else + { + escaped = false; + sb.append(c); + } + } + + return sb.toString(); + } + + private static String toEncodedString(Object o) + { + if (o instanceof String) + { + String s = (String) o; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) + { + char c = s.charAt(i); + if ((c == '\\') || (c == '(') || (c == ')') || (c == '*')) + { + sb.append('\\'); + } + sb.append(c); + } + + o = sb.toString(); + } + + return o.toString(); + } + + public static SimpleFilter parse(String filter) + { + int idx = skipWhitespace(filter, 0); + + if ((filter == null) || (filter.length() == 0) || (idx >= filter.length())) + { + throw new IllegalArgumentException("Null or empty filter."); + } + else if (filter.charAt(idx) != '(') + { + throw new IllegalArgumentException("Missing opening parenthesis: " + filter); + } + + SimpleFilter sf = null; + List stack = new ArrayList(); + boolean isEscaped = false; + while (idx < filter.length()) + { + if (sf != null) + { + throw new IllegalArgumentException( + "Only one top-level operation allowed: " + filter); + } + + if (!isEscaped && (filter.charAt(idx) == '(')) + { + // Skip paren and following whitespace. + idx = skipWhitespace(filter, idx + 1); + + if (filter.charAt(idx) == '&') + { + int peek = skipWhitespace(filter, idx + 1); + if (filter.charAt(peek) == '(') + { + idx = peek - 1; + stack.add(0, new SimpleFilter(null, new ArrayList(), SimpleFilter.AND)); + } + else + { + stack.add(0, new Integer(idx)); + } + } + else if (filter.charAt(idx) == '|') + { + int peek = skipWhitespace(filter, idx + 1); + if (filter.charAt(peek) == '(') + { + idx = peek - 1; + stack.add(0, new SimpleFilter(null, new ArrayList(), SimpleFilter.OR)); + } + else + { + stack.add(0, new Integer(idx)); + } + } + else if (filter.charAt(idx) == '!') + { + int peek = skipWhitespace(filter, idx + 1); + if (filter.charAt(peek) == '(') + { + idx = peek - 1; + stack.add(0, new SimpleFilter(null, new ArrayList(), SimpleFilter.NOT)); + } + else + { + stack.add(0, new Integer(idx)); + } + } + else + { + stack.add(0, new Integer(idx)); + } + } + else if (!isEscaped && (filter.charAt(idx) == ')')) + { + Object top = stack.remove(0); + if (top instanceof SimpleFilter) + { + if (!stack.isEmpty() && (stack.get(0) instanceof SimpleFilter)) + { + ((List) ((SimpleFilter) stack.get(0)).m_value).add(top); + } + else + { + sf = (SimpleFilter) top; + } + } + else if (!stack.isEmpty() && (stack.get(0) instanceof SimpleFilter)) + { + ((List) ((SimpleFilter) stack.get(0)).m_value).add( + SimpleFilter.subfilter(filter, ((Integer) top).intValue(), idx)); + } + else + { + sf = SimpleFilter.subfilter(filter, ((Integer) top).intValue(), idx); + } + } + else if (!isEscaped && (filter.charAt(idx) == '\\')) + { + isEscaped = true; + } + else + { + isEscaped = false; + } + + idx = skipWhitespace(filter, idx + 1); + } + + if (sf == null) + { + throw new IllegalArgumentException("Missing closing parenthesis: " + filter); + } + + return sf; + } + + private static SimpleFilter subfilter(String filter, int startIdx, int endIdx) + { + final String opChars = "=<>~"; + + // Determine the ending index of the attribute name. + int attrEndIdx = startIdx; + for (int i = 0; i < (endIdx - startIdx); i++) + { + char c = filter.charAt(startIdx + i); + if (opChars.indexOf(c) >= 0) + { + break; + } + else if (!Character.isWhitespace(c)) + { + attrEndIdx = startIdx + i + 1; + } + } + if (attrEndIdx == startIdx) + { + throw new IllegalArgumentException( + "Missing attribute name: " + filter.substring(startIdx, endIdx)); + } + String attr = filter.substring(startIdx, attrEndIdx); + + // Skip the attribute name and any following whitespace. + startIdx = skipWhitespace(filter, attrEndIdx); + + // Determine the operator type. + int op = -1; + switch (filter.charAt(startIdx)) + { + case '=': + op = EQ; + startIdx++; + break; + case '<': + if (filter.charAt(startIdx + 1) != '=') + { + throw new IllegalArgumentException( + "Unknown operator: " + filter.substring(startIdx, endIdx)); + } + op = LTE; + startIdx += 2; + break; + case '>': + if (filter.charAt(startIdx + 1) != '=') + { + throw new IllegalArgumentException( + "Unknown operator: " + filter.substring(startIdx, endIdx)); + } + op = GTE; + startIdx += 2; + break; + case '~': + if (filter.charAt(startIdx + 1) != '=') + { + throw new IllegalArgumentException( + "Unknown operator: " + filter.substring(startIdx, endIdx)); + } + op = APPROX; + startIdx += 2; + break; + default: + throw new IllegalArgumentException( + "Unknown operator: " + filter.substring(startIdx, endIdx)); + } + + // Parse value. + Object value = toDecodedString(filter, startIdx, endIdx); + + // Check if the equality comparison is actually a substring + // or present operation. + if (op == EQ) + { + String valueStr = filter.substring(startIdx, endIdx); + List values = parseSubstring(valueStr); + if ((values.size() == 2) + && (values.get(0).length() == 0) + && (values.get(1).length() == 0)) + { + op = PRESENT; + } + else if (values.size() > 1) + { + op = SUBSTRING; + value = values; + } + } + + return new SimpleFilter(attr, value, op); + } + + public static List parseSubstring(String value) + { + List pieces = new ArrayList(); + StringBuilder ss = new StringBuilder(); + // int kind = SIMPLE; // assume until proven otherwise + boolean wasStar = false; // indicates last piece was a star + boolean leftstar = false; // track if the initial piece is a star + boolean rightstar = false; // track if the final piece is a star + + int idx = 0; + + // We assume (sub)strings can contain leading and trailing blanks + boolean escaped = false; +loop: for (;;) + { + if (idx >= value.length()) + { + if (wasStar) + { + // insert last piece as "" to handle trailing star + rightstar = true; + } + else + { + pieces.add(ss.toString()); + // accumulate the last piece + // note that in the case of + // (cn=); this might be + // the string "" (!=null) + } + ss.setLength(0); + break loop; + } + + // Read the next character and account for escapes. + char c = value.charAt(idx++); + if (!escaped && (c == '*')) + { + // If we have successive '*' characters, then we can + // effectively collapse them by ignoring succeeding ones. + if (!wasStar) + { + if (ss.length() > 0) + { + pieces.add(ss.toString()); // accumulate the pieces + // between '*' occurrences + } + ss.setLength(0); + // if this is a leading star, then track it + if (pieces.isEmpty()) + { + leftstar = true; + } + wasStar = true; + } + } + else if (!escaped && (c == '\\')) + { + escaped = true; + } + else + { + escaped = false; + wasStar = false; + ss.append(c); + } + } + if (leftstar || rightstar || pieces.size() > 1) + { + // insert leading and/or trailing "" to anchor ends + if (rightstar) + { + pieces.add(""); + } + if (leftstar) + { + pieces.add(0, ""); + } + } + return pieces; + } + + public static String unparseSubstring(List pieces) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < pieces.size(); i++) + { + if (i > 0) + { + sb.append("*"); + } + sb.append(toEncodedString(pieces.get(i))); + } + return sb.toString(); + } + + public static boolean compareSubstring(List pieces, String s) + { + // Walk the pieces to match the string + // There are implicit stars between each piece, + // and the first and last pieces might be "" to anchor the match. + // assert (pieces.length > 1) + // minimal case is * + + boolean result = true; + int len = pieces.size(); + + // Special case, if there is only one piece, then + // we must perform an equality test. + if (len == 1) + { + return s.equals(pieces.get(0)); + } + + // Otherwise, check whether the pieces match + // the specified string. + + int index = 0; + +loop: for (int i = 0; i < len; i++) + { + String piece = pieces.get(i); + + // If this is the first piece, then make sure the + // string starts with it. + if (i == 0) + { + if (!s.startsWith(piece)) + { + result = false; + break loop; + } + } + + // If this is the last piece, then make sure the + // string ends with it. + if (i == (len - 1)) + { + if (s.endsWith(piece) && (s.length() >= (index + piece.length()))) + { + result = true; + } + else + { + result = false; + } + break loop; + } + + // If this is neither the first or last piece, then + // make sure the string contains it. + if ((i > 0) && (i < (len - 1))) + { + index = s.indexOf(piece, index); + if (index < 0) + { + result = false; + break loop; + } + } + + // Move string index beyond the matching piece. + index += piece.length(); + } + + return result; + } + + private static int skipWhitespace(String s, int startIdx) + { + int len = s.length(); + while ((startIdx < len) && Character.isWhitespace(s.charAt(startIdx))) + { + startIdx++; + } + return startIdx; + } + + /** + * Converts a attribute map to a filter. The filter is created by iterating + * over the map's entry set. If ordering of attributes is important (e.g., + * for hitting attribute indices), then the map's entry set should iterate + * in the desired order. Equality testing is assumed for all attribute types + * other than version ranges, which are handled appropriated. If the attribute + * map is empty, then a filter that matches anything is returned. + * @param attrs Map of attributes to convert to a filter. + * @return A filter corresponding to the attributes. + */ + public static SimpleFilter convert(Map attrs) + { + // Rather than building a filter string to be parsed into a SimpleFilter, + // we will just create the parsed SimpleFilter directly. + + List filters = new ArrayList(); + + for (Entry entry : attrs.entrySet()) + { + if (entry.getValue() instanceof VersionRange) + { + VersionRange vr = (VersionRange) entry.getValue(); + if (vr.getLeftType() == VersionRange.LEFT_CLOSED) + { + filters.add( + new SimpleFilter( + entry.getKey(), + vr.getLeft().toString(), + SimpleFilter.GTE)); + } + else + { + SimpleFilter not = + new SimpleFilter(null, new ArrayList(), SimpleFilter.NOT); + ((List) not.getValue()).add( + new SimpleFilter( + entry.getKey(), + vr.getLeft().toString(), + SimpleFilter.LTE)); + filters.add(not); + } + + if (vr.getRight() != null) + { + if (vr.getRightType() == VersionRange.RIGHT_CLOSED) + { + filters.add( + new SimpleFilter( + entry.getKey(), + vr.getRight().toString(), + SimpleFilter.LTE)); + } + else + { + SimpleFilter not = + new SimpleFilter(null, new ArrayList(), SimpleFilter.NOT); + ((List) not.getValue()).add( + new SimpleFilter( + entry.getKey(), + vr.getRight().toString(), + SimpleFilter.GTE)); + filters.add(not); + } + } + } + else + { + List values = SimpleFilter.parseSubstring(entry.getValue().toString()); + if (values.size() > 1) + { + filters.add( + new SimpleFilter( + entry.getKey(), + values, + SimpleFilter.SUBSTRING)); + } + else + { + filters.add( + new SimpleFilter( + entry.getKey(), + values.get(0), + SimpleFilter.EQ)); + } + } + } + + SimpleFilter sf = null; + + if (filters.size() == 1) + { + sf = filters.get(0); + } + else if (attrs.size() > 1) + { + sf = new SimpleFilter(null, filters, SimpleFilter.AND); + } + else if (filters.isEmpty()) + { + sf = new SimpleFilter(null, null, SimpleFilter.MATCH_ALL); + } + + return sf; + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/ext/ClassPathExtenderFactory.java b/framework/src/main/java/org/apache/felix/framework/ext/ClassPathExtenderFactory.java new file mode 100644 index 00000000000..26103b42678 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/ext/ClassPathExtenderFactory.java @@ -0,0 +1,153 @@ +/* + * 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.framework.ext; + +import java.io.File; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; + +public interface ClassPathExtenderFactory +{ + interface ClassPathExtender + { + void add(File file) throws Exception; + } + + ClassPathExtender getExtender(ClassLoader loader); + + final class DefaultClassLoaderExtender implements ClassPathExtenderFactory, ClassPathExtenderFactory.ClassPathExtender + { + private static final Method m_append; + private static final ClassLoader m_app; + private static final Method m_addURL; + private final ClassLoader m_loader; + + static + { + ClassLoader app = ClassLoader.getSystemClassLoader(); + + Method append = null; + + while (app != null) + { + try + { + append = app.getClass().getDeclaredMethod("appendToClassPathForInstrumentation", String.class); + append.setAccessible(true); + break; + } + catch (Exception e) + { + append = null; + try + { + app = app.getParent(); + } + catch (Exception ex) + { + app = null; + } + } + } + m_append = append; + m_app = app; + + Method addURL; + + try + { + addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + addURL.setAccessible(true); + } + catch (Exception e) + { + addURL = null; + } + + m_addURL = addURL; + } + + public DefaultClassLoaderExtender() + { + this(null); + } + + private DefaultClassLoaderExtender(ClassLoader loader) + { + m_loader = loader; + } + + @Override + public ClassPathExtender getExtender(ClassLoader loader) + { + if (m_append != null) + { + ClassLoader current = ClassLoader.getSystemClassLoader(); + + while (current != null) + { + if (loader == current) + { + return this; + } + current = current.getParent(); + } + } + if (m_addURL != null && loader instanceof URLClassLoader) + { + return new DefaultClassLoaderExtender(loader); + } + return null; + } + + @Override + public void add(final File file) throws Exception + { + ClassLoader loader; + if (m_loader != null) + { + loader = m_loader; + synchronized (m_loader) + { + m_addURL.invoke(m_loader, file.getCanonicalFile().toURI().toURL()); + } + } + else + { + loader = m_app; + synchronized (m_app) + { + m_append.invoke(m_app, file.getCanonicalFile().getPath()); + } + } + + try + { + for (int i = 0; i < 1000; i++) + { + loader.loadClass("flushFelixExtensionSubsystem" + i + ".class"); + } + } + catch (Exception ex) { + // This is expected, we need to init the url subsystem + } + } + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/ext/FelixBundleContext.java b/framework/src/main/java/org/apache/felix/framework/ext/FelixBundleContext.java deleted file mode 100644 index 8c46c3428ae..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/ext/FelixBundleContext.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.ext; - -import org.osgi.framework.BundleContext; -import org.osgi.framework.BundleException; - -public interface FelixBundleContext extends BundleContext -{ - public void addImportPackage() throws BundleException; - public void removeImportPackage() throws BundleException; - public void addExportPackage() throws BundleException; - public void removeExportPackage() throws BundleException; -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/ext/SecurityProvider.java b/framework/src/main/java/org/apache/felix/framework/ext/SecurityProvider.java new file mode 100644 index 00000000000..2ae3c456cfa --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/ext/SecurityProvider.java @@ -0,0 +1,33 @@ +/* + * 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.framework.ext; + +import java.security.Permission; +import java.security.ProtectionDomain; + +import org.osgi.framework.Bundle; + +public interface SecurityProvider +{ + boolean hasBundlePermission(ProtectionDomain pd, Permission p, boolean direct); + + Object getSignerMatcher(Bundle bundle, int signersType); + + void checkBundle(Bundle bundle) throws Exception; +} diff --git a/framework/src/main/java/org/apache/felix/framework/resolver/CandidateComparator.java b/framework/src/main/java/org/apache/felix/framework/resolver/CandidateComparator.java new file mode 100644 index 00000000000..a6117f1727d --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/resolver/CandidateComparator.java @@ -0,0 +1,114 @@ +/* + * 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.framework.resolver; + +import java.util.Comparator; + +import org.apache.felix.framework.wiring.BundleCapabilityImpl; +import org.osgi.framework.Constants; +import org.osgi.framework.Version; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.resource.Capability; + +public class CandidateComparator implements Comparator +{ + public int compare(Capability cap1, Capability cap2) + { + // First check resolved state, since resolved capabilities have priority + // over unresolved ones. Compare in reverse order since we want to sort + // in descending order. + int c = 0; + + BundleCapability bcap1 = null; + BundleCapability bcap2 = null; + + if (cap1 instanceof BundleCapability && + cap2 instanceof BundleCapability) + { + bcap1 = (BundleCapability) cap1; + bcap2 = (BundleCapability) cap2; + + if ((bcap1.getRevision().getWiring() != null) + && (bcap2.getRevision().getWiring() == null)) + { + c = -1; + } + else if ((bcap1.getRevision().getWiring() == null) + && (bcap2.getRevision().getWiring() != null)) + { + c = 1; + } + } + + // Compare revision capabilities. + if ((c == 0) && cap1.getNamespace().equals(BundleRevision.BUNDLE_NAMESPACE)) + { + c = ((Comparable) cap1.getAttributes().get(BundleRevision.BUNDLE_NAMESPACE)) + .compareTo(cap2.getAttributes().get(BundleRevision.BUNDLE_NAMESPACE)); + if (c == 0) + { + Version v1 = (!cap1.getAttributes().containsKey(Constants.BUNDLE_VERSION_ATTRIBUTE)) + ? Version.emptyVersion + : (Version) cap1.getAttributes().get(Constants.BUNDLE_VERSION_ATTRIBUTE); + Version v2 = (!cap2.getAttributes().containsKey(Constants.BUNDLE_VERSION_ATTRIBUTE)) + ? Version.emptyVersion + : (Version) cap2.getAttributes().get(Constants.BUNDLE_VERSION_ATTRIBUTE); + // Compare these in reverse order, since we want + // highest version to have priority. + c = v2.compareTo(v1); + } + } + // Compare package capabilities. + else if ((c == 0) && cap1.getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE)) + { + c = ((Comparable) cap1.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE)) + .compareTo(cap2.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE)); + if (c == 0) + { + Version v1 = (!cap1.getAttributes().containsKey(BundleCapabilityImpl.VERSION_ATTR)) + ? Version.emptyVersion + : (Version) cap1.getAttributes().get(BundleCapabilityImpl.VERSION_ATTR); + Version v2 = (!cap2.getAttributes().containsKey(BundleCapabilityImpl.VERSION_ATTR)) + ? Version.emptyVersion + : (Version) cap2.getAttributes().get(BundleCapabilityImpl.VERSION_ATTR); + // Compare these in reverse order, since we want + // highest version to have priority. + c = v2.compareTo(v1); + } + } + + // Finally, compare bundle identity. + if (c == 0 && bcap1 != null && bcap2 != null) + { + if (bcap1.getRevision().getBundle().getBundleId() < + bcap2.getRevision().getBundle().getBundleId()) + { + c = -1; + } + else if (bcap1.getRevision().getBundle().getBundleId() > + bcap2.getRevision().getBundle().getBundleId()) + { + c = 1; + } + } + + return c; + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/resolver/ResolveException.java b/framework/src/main/java/org/apache/felix/framework/resolver/ResolveException.java new file mode 100755 index 00000000000..9d9a232b6a0 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/resolver/ResolveException.java @@ -0,0 +1,50 @@ +/* + * 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.framework.resolver; + +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.service.resolver.ResolutionException; + +public class ResolveException extends ResolutionException +{ + private final BundleRevision m_revision; + private final BundleRequirement m_req; + + /** + * Constructs an instance of ResolveException with the specified detail message. + * @param msg the detail message. + */ + public ResolveException(String msg, BundleRevision revision, BundleRequirement req) + { + super(msg); + m_revision = revision; + m_req = req; + } + + public BundleRevision getRevision() + { + return m_revision; + } + + public BundleRequirement getRequirement() + { + return m_req; + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/resolver/ResourceNotFoundException.java b/framework/src/main/java/org/apache/felix/framework/resolver/ResourceNotFoundException.java new file mode 100644 index 00000000000..b1903fb607c --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/resolver/ResourceNotFoundException.java @@ -0,0 +1,32 @@ +/* + * 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.framework.resolver; + +public class ResourceNotFoundException extends Exception +{ + public ResourceNotFoundException(String msg) + { + super(msg); + } + + public ResourceNotFoundException(String msg, Throwable th) + { + super(msg, th); + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/searchpolicy/ContentClassLoader.java b/framework/src/main/java/org/apache/felix/framework/searchpolicy/ContentClassLoader.java deleted file mode 100644 index 22fa24a8c75..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/searchpolicy/ContentClassLoader.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.searchpolicy; - -import java.net.URL; -import java.security.ProtectionDomain; -import java.security.SecureClassLoader; -import java.util.Enumeration; - -import org.apache.felix.framework.util.Util; -import org.apache.felix.moduleloader.IContentLoader; -import org.apache.felix.moduleloader.ResourceNotFoundException; - -public class ContentClassLoader extends SecureClassLoader -{ - private ContentLoaderImpl m_contentLoader = null; - private ProtectionDomain m_protectionDomain = null; - - public ContentClassLoader(ContentLoaderImpl contentLoader, - ProtectionDomain protectionDomain) - { - m_contentLoader = contentLoader; - m_protectionDomain = protectionDomain; - } - - public IContentLoader getContentLoader() - { - return m_contentLoader; - } - - protected Class loadClassFromModule(String name) - throws ClassNotFoundException - { - // Ask the search policy for the clas before consulting the module. - Class clazz = findClass(name); - - // If not found, then throw an exception. - if (clazz == null) - { - throw new ClassNotFoundException(name); - } - resolveClass(clazz); - return clazz; - } - - protected Class loadClass(String name, boolean resolve) - throws ClassNotFoundException - { - // Make sure the class was not already loaded. - Class clazz = findLoadedClass(name); - if (clazz == null) - { - // Ask the search policy for the class. - clazz = m_contentLoader.getSearchPolicy().findClass(name); - } - - // If still not found, then throw an exception. - if (clazz == null) - { - throw new ClassNotFoundException(name); - } - // Otherwise resolve the class. - if (resolve) - { - resolveClass(clazz); - } - return clazz; - } - - protected Class findClass(String name) throws ClassNotFoundException - { -// TODO: ML - We probably need to do this check closer to defineClass() and -// protect it with a synchronized block. - Class clazz = findLoadedClass(name); - - // Search for class in module. - if (clazz == null) - { - String actual = name.replace('.', '/') + ".class"; - byte[] bytes = null; - - // Check the module class path. - for (int i = 0; - (bytes == null) && - (i < m_contentLoader.getClassPath().length); i++) - { - bytes = m_contentLoader.getClassPath()[i].getEntry(actual); - } - - if (bytes != null) - { - // We need to try to define a Package object for the class - // before we call defineClass(). Get the package name and - // see if we have already created the package. - String pkgName = Util.getClassPackage(name); - if (pkgName.length() > 0) - { - if (getPackage(pkgName) == null) - { - Object[] params = - m_contentLoader.getSearchPolicy() - .definePackage(pkgName); - if (params != null) - { - definePackage( - pkgName, - (String) params[0], - (String) params[1], - (String) params[2], - (String) params[3], - (String) params[4], - (String) params[5], - null); - } - else - { - definePackage(pkgName, null, null, - null, null, null, null, null); - } - } - } - - // If we have a security context, then use it to - // define the class with it for security purposes, - // otherwise define the class without a protection domain. - if (m_protectionDomain != null) - { - clazz = defineClass(name, bytes, 0, bytes.length, - m_protectionDomain); - } - else - { - clazz = defineClass(name, bytes, 0, bytes.length); - } - } - } - - return clazz; - } - - public URL getResourceFromModule(String name) - { - try - { - return findResource(name); - } - catch (Throwable th) - { - // Ignore and just return null. - } - return null; - } - - public URL getResource(String name) - { - // Ask the search policy for the class before consulting the module. - try - { - return m_contentLoader.getSearchPolicy().findResource(name); - } - catch (ResourceNotFoundException ex) - { - } - return null; - } - - protected URL findResource(String name) - { - return m_contentLoader.getResource(name); - } - - protected Enumeration findResources(String name) - { - return m_contentLoader.getResources(name); - } - - protected String findLibrary(String name) - { - return m_contentLoader.getSearchPolicy().findLibrary(name); - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/searchpolicy/ContentLoaderImpl.java b/framework/src/main/java/org/apache/felix/framework/searchpolicy/ContentLoaderImpl.java deleted file mode 100644 index 1c96ea0c01c..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/searchpolicy/ContentLoaderImpl.java +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.searchpolicy; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.security.ProtectionDomain; -import java.util.Enumeration; -import java.util.Vector; - -import org.apache.felix.framework.Logger; -import org.apache.felix.framework.util.SecureAction; -import org.apache.felix.moduleloader.*; - -public class ContentLoaderImpl implements IContentLoader -{ - private Logger m_logger = null; - private IContent m_content = null; - private IContent[] m_contentPath = null; - private ISearchPolicy m_searchPolicy = null; - private IURLPolicy m_urlPolicy = null; - private ContentClassLoader m_classLoader = null; - private ProtectionDomain m_protectionDomain = null; - private static SecureAction m_secureAction = new SecureAction(); - - public ContentLoaderImpl(Logger logger, IContent content, - IContent[] contentPath) - { - this(logger, content, contentPath, null); - } - - public ContentLoaderImpl(Logger logger, IContent content, - IContent[] contentPath, ProtectionDomain protectionDomain) - { - m_logger = logger; - m_content = content; - m_contentPath = contentPath; - m_protectionDomain = protectionDomain; - } - - public Logger getLogger() - { - return m_logger; - } - - public void open() - { - m_content.open(); - for (int i = 0; (m_contentPath != null) && (i < m_contentPath.length); i++) - { - m_contentPath[i].open(); - } - } - - public void close() - { - m_content.close(); - for (int i = 0; (m_contentPath != null) && (i < m_contentPath.length); i++) - { - m_contentPath[i].close(); - } - } - - public IContent getContent() - { - return m_content; - } - - public IContent[] getClassPath() - { - return m_contentPath; - } - - public void setSearchPolicy(ISearchPolicy searchPolicy) - { - m_searchPolicy = searchPolicy; - } - - public ISearchPolicy getSearchPolicy() - { - return m_searchPolicy; - } - - public void setURLPolicy(IURLPolicy urlPolicy) - { - m_urlPolicy = urlPolicy; - } - - public IURLPolicy getURLPolicy() - { - return m_urlPolicy; - } - - public Class getClass(String name) - { - if (m_classLoader == null) - { - m_classLoader = m_secureAction.createContentClassLoader(this, - m_protectionDomain); - } - - try - { - return m_classLoader.loadClassFromModule(name); - } - catch (ClassNotFoundException ex) - { - return null; - } - } - - public URL getResource(String name) - { - URL url = null; - - // Remove leading slash, if present. - if (name.startsWith("/")) - { - name = name.substring(1); - } - - // Check the module class path. - for (int i = 0; - (url == null) && - (i < getClassPath().length); i++) - { - if (getClassPath()[i].hasEntry(name)) - { - url = getURLPolicy().createURL((i + 1) + "/" + name); - } - } - - return url; - } - - protected Enumeration getResources(String name) - { - Vector v = new Vector(); - - // Remove leading slash, if present. - if (name.startsWith("/")) - { - name = name.substring(1); - } - - // Check the module class path. - for (int i = 0; i < getClassPath().length; i++) - { - if (getClassPath()[i].hasEntry(name)) - { - // Use the class path index + 1 for creating the path so - // that we can differentiate between module content URLs - // (where the path will start with 0) and module class - // path URLs. - v.addElement(getURLPolicy().createURL((i + 1) + "/" + name)); - } - } - - return v.elements(); - } - - // TODO: API: Investigate making this an API call. - public URL getResourceFromContent(String name) - { - URL url = null; - - // Check for the special case of "/", which represents - // the root of the bundle according to the spec. - if (name.equals("/")) - { - url = getURLPolicy().createURL("0/"); - } - - if (url == null) - { - // Remove leading slash, if present. - if (name.startsWith("/")) - { - name = name.substring(1); - } - - // Check the module content. - if (getContent().hasEntry(name)) - { - // Module content URLs start with 0, whereas module - // class path URLs start with the index into the class - // path + 1. - url = getURLPolicy().createURL("0/" + name); - } - } - - return url; - } - - public boolean hasInputStream(String urlPath) - { - if (urlPath.startsWith("/")) - { - urlPath = urlPath.substring(1); - } - // The urlPath is the path portion of a resource URL - // that is contructed above in getResouce() like this: - // / - // where == 0 is the module content - // and > 0 is the index into the class - // path - 1. - int idx = Integer.parseInt(urlPath.substring(0, urlPath.indexOf('/'))); - urlPath = urlPath.substring(urlPath.indexOf('/') + 1); - if (idx == 0) - { - return m_content.hasEntry(urlPath); - } - return m_contentPath[idx - 1].hasEntry(urlPath); - } - - public InputStream getInputStream(String urlPath) - throws IOException - { - if (urlPath.startsWith("/")) - { - urlPath = urlPath.substring(1); - } - // The urlPath is the path portion of a resource URL - // that is contructed above in getResouce() like this: - // / - // where == 0 is the module content - // and > 0 is the index into the class - // path - 1. - int idx = Integer.parseInt(urlPath.substring(0, urlPath.indexOf('/'))); - urlPath = urlPath.substring(urlPath.indexOf('/') + 1); - if (idx == 0) - { - return m_content.getEntryAsStream(urlPath); - } - return m_contentPath[idx - 1].getEntryAsStream(urlPath); - } - - public String toString() - { - return m_searchPolicy.toString(); - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/searchpolicy/ModuleDefinition.java b/framework/src/main/java/org/apache/felix/framework/searchpolicy/ModuleDefinition.java deleted file mode 100644 index c5cc11ea148..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/searchpolicy/ModuleDefinition.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.searchpolicy; - -import org.apache.felix.moduleloader.IModuleDefinition; - -public class ModuleDefinition implements IModuleDefinition -{ - private R4Export[] m_exports = null; - private R4Import[] m_imports = null; - private R4Import[] m_dynamicImports = null; - private R4Library[] m_libraries = null; - - public ModuleDefinition( - R4Export[] exports, R4Import[] imports, - R4Import[] dynamicImports, R4Library[] libraries) - { - m_exports = exports; - m_imports = imports; - m_dynamicImports = dynamicImports; - m_libraries = libraries; - } - - public R4Export[] getExports() - { -// TODO: ML - These should probably all return copies of the array. - return m_exports; - } - - public R4Import[] getImports() - { - return m_imports; - } - - public R4Import[] getDynamicImports() - { - return m_dynamicImports; - } - - public R4Library[] getLibraries() - { - return m_libraries; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Attribute.java b/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Attribute.java deleted file mode 100644 index a8658c36e66..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Attribute.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.searchpolicy; - -public class R4Attribute -{ - private String m_name = ""; - private Object m_value = null; - private boolean m_isMandatory = false; - - public R4Attribute(String name, Object value, boolean isMandatory) - { - m_name = name; - m_value = value; - m_isMandatory = isMandatory; - } - - public String getName() - { - return m_name; - } - - public Object getValue() - { - return m_value; - } - - public boolean isMandatory() - { - return m_isMandatory; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Directive.java b/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Directive.java deleted file mode 100644 index 2d81fa3dfe4..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Directive.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.searchpolicy; - -public class R4Directive -{ - private String m_name = ""; - private String m_value = ""; - - public R4Directive(String name, String value) - { - m_name = name; - m_value = value; - } - - public String getName() - { - return m_name; - } - - public String getValue() - { - return m_value; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Export.java b/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Export.java deleted file mode 100644 index e78260b29f3..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Export.java +++ /dev/null @@ -1,303 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.searchpolicy; - -import java.util.*; - -import org.apache.felix.framework.util.Util; -import org.osgi.framework.Constants; -import org.osgi.framework.Version; - -public class R4Export extends R4Package -{ - private String[] m_uses = null; - private String[][] m_includeFilter = null; - private String[][] m_excludeFilter = null; - - public R4Export(R4Package pkg) - { - this(pkg.getName(), pkg.getDirectives(), pkg.getAttributes()); - } - - public R4Export(String name, R4Directive[] directives, R4Attribute[] attrs) - { - super(name, directives, attrs); - - // Find all export directives: uses, mandatory, include, and exclude. - String mandatory = "", uses = ""; - for (int i = 0; i < m_directives.length; i++) - { - if (m_directives[i].getName().equals(Constants.USES_DIRECTIVE)) - { - uses = m_directives[i].getValue(); - } - else if (m_directives[i].getName().equals(Constants.MANDATORY_DIRECTIVE)) - { - mandatory = m_directives[i].getValue(); - } - else if (m_directives[i].getName().equals(Constants.INCLUDE_DIRECTIVE)) - { - String[] ss = Util.parseDelimitedString(m_directives[i].getValue(), ","); - m_includeFilter = new String[ss.length][]; - for (int filterIdx = 0; filterIdx < ss.length; filterIdx++) - { - m_includeFilter[filterIdx] = parseSubstring(ss[filterIdx]); - } - } - else if (m_directives[i].getName().equals(Constants.EXCLUDE_DIRECTIVE)) - { - String[] ss = Util.parseDelimitedString(m_directives[i].getValue(), ","); - m_excludeFilter = new String[ss.length][]; - for (int filterIdx = 0; filterIdx < ss.length; filterIdx++) - { - m_excludeFilter[filterIdx] = parseSubstring(ss[filterIdx]); - } - } - } - - // Parse these uses directive. - StringTokenizer tok = new StringTokenizer(uses, ","); - m_uses = new String[tok.countTokens()]; - for (int i = 0; i < m_uses.length; i++) - { - m_uses[i] = tok.nextToken().trim(); - } - - // Parse mandatory directive and mark specified - // attributes as mandatory. - tok = new StringTokenizer(mandatory, ","); - while (tok.hasMoreTokens()) - { - // Get attribute name. - String attrName = tok.nextToken().trim(); - // Find attribute and mark it as mandatory. - boolean found = false; - for (int i = 0; (!found) && (i < m_attrs.length); i++) - { - if (m_attrs[i].getName().equals(attrName)) - { - m_attrs[i] = new R4Attribute( - m_attrs[i].getName(), - m_attrs[i].getValue(), true); - found = true; - } - } - // If a specified mandatory attribute was not found, - // then error. - if (!found) - { - throw new IllegalArgumentException( - "Mandatory attribute '" + attrName + "' does not exist."); - } - } - - // Find the version, if present, and convert to Version. - // The version attribute value may be a String or a Version, - // since the value may be coming from an R4Export that already - // converted it to Version. - m_version = Version.emptyVersion; - for (int i = 0; i < m_attrs.length; i++) - { - if (m_attrs[i].getName().equals(Constants.VERSION_ATTRIBUTE)) - { - String versionStr = (m_attrs[i].getValue() instanceof Version) - ? ((Version) m_attrs[i].getValue()).toString() - : (String) m_attrs[i].getValue(); - m_version = Version.parseVersion(versionStr); - m_attrs[i] = new R4Attribute( - m_attrs[i].getName(), - m_version, - m_attrs[i].isMandatory()); - break; - } - } - } - - public String[] getUses() - { - return m_uses; - } - - public boolean isIncluded(String name) - { - if ((m_includeFilter == null) && (m_excludeFilter == null)) - { - return true; - } - - // Get the class name portion of the target class. - String className = Util.getClassName(name); - - // If there are no include filters then all classes are included - // by default, otherwise try to find one match. - boolean included = (m_includeFilter == null); - for (int i = 0; - (!included) && (m_includeFilter != null) && (i < m_includeFilter.length); - i++) - { - included = checkSubstring(m_includeFilter[i], className); - } - - // If there are no exclude filters then no classes are excluded - // by default, otherwise try to find one match. - boolean excluded = false; - for (int i = 0; - (!excluded) && (m_excludeFilter != null) && (i < m_excludeFilter.length); - i++) - { - excluded = checkSubstring(m_excludeFilter[i], className); - } - return included && !excluded; - } - - // - // The following substring-related code was lifted and modified - // from the LDAP parser code. - // - - private static String[] parseSubstring(String target) - { - List pieces = new ArrayList(); - StringBuffer ss = new StringBuffer(); - // int kind = SIMPLE; // assume until proven otherwise - boolean wasStar = false; // indicates last piece was a star - boolean leftstar = false; // track if the initial piece is a star - boolean rightstar = false; // track if the final piece is a star - - int idx = 0; - - // We assume (sub)strings can contain leading and trailing blanks - for (;;) - { - if (idx >= target.length()) - { - if (wasStar) - { - // insert last piece as "" to handle trailing star - rightstar = true; - } - else - { - pieces.add(ss.toString()); - // accumulate the last piece - // note that in the case of - // (cn=); this might be - // the string "" (!=null) - } - ss.setLength(0); - break; - } - - char c = target.charAt(idx++); - if (c == '*') - { - if (wasStar) - { - // encountered two successive stars; - // I assume this is illegal - throw new IllegalArgumentException("Invalid filter string: " + target); - } - if (ss.length() > 0) - { - pieces.add(ss.toString()); // accumulate the pieces - // between '*' occurrences - } - ss.setLength(0); - // if this is a leading star, then track it - if (pieces.size() == 0) - { - leftstar = true; - } - ss.setLength(0); - wasStar = true; - } - else - { - wasStar = false; - ss.append(c); - } - } - if (leftstar || rightstar || pieces.size() > 1) - { - // insert leading and/or trailing "" to anchor ends - if (rightstar) - { - pieces.add(""); - } - if (leftstar) - { - pieces.add(0, ""); - } - } - return (String[]) pieces.toArray(new String[pieces.size()]); - } - - private static boolean checkSubstring(String[] pieces, String s) - { - // Walk the pieces to match the string - // There are implicit stars between each piece, - // and the first and last pieces might be "" to anchor the match. - // assert (pieces.length > 1) - // minimal case is * - - boolean result = false; - int len = pieces.length; - int index = 0; - - for (int i = 0; i < len; i++) - { - String piece = (String) pieces[i]; - if (i == len - 1) - { - // this is the last piece - if (s.endsWith(piece)) - { - result = true; - } - else - { - result = false; - } - break; - } - // initial non-star; assert index == 0 - else if (i == 0) - { - if (!s.startsWith(piece)) - { - result = false; - break; - } - } - // assert i > 0 && i < len-1 - else - { - // Sure wish stringbuffer supported e.g. indexOf - index = s.indexOf(piece, index); - if (index < 0) - { - result = false; - break; - } - } - // start beyond the matching piece - index += piece.length(); - } - - return result; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Import.java b/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Import.java deleted file mode 100644 index 5293368c364..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Import.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.searchpolicy; - -import org.osgi.framework.Constants; -import org.osgi.framework.Version; - -public class R4Import extends R4Package -{ - private VersionRange m_versionRange = null; - private boolean m_isOptional = false; - - public R4Import(R4Package pkg) - { - this(pkg.getName(), pkg.getDirectives(), pkg.getAttributes()); - } - - public R4Import(String name, R4Directive[] directives, R4Attribute[] attrs) - { - super(name, directives, attrs); - - // Find all import directives: resolution. - for (int i = 0; i < m_directives.length; i++) - { - if (m_directives[i].getName().equals(Constants.RESOLUTION_DIRECTIVE)) - { - m_isOptional = m_directives[i].getValue().equals(Constants.RESOLUTION_OPTIONAL); - } - } - - // Convert version and bundle version attributes to VersionRange. - // The attribute value may be a String or a Version, since the - // value may be coming from an R4Export that already converted - // it to Version. - m_versionRange = VersionRange.parse(Version.emptyVersion.toString()); - m_version = m_versionRange.getLow(); - for (int i = 0; i < m_attrs.length; i++) - { - if (m_attrs[i].getName().equals(Constants.VERSION_ATTRIBUTE)) - { - String versionStr = (m_attrs[i].getValue() instanceof Version) - ? ((Version) m_attrs[i].getValue()).toString() - : (String) m_attrs[i].getValue(); - m_versionRange = VersionRange.parse(versionStr); - m_version = m_versionRange.getLow(); - m_attrs[i] = new R4Attribute( - m_attrs[i].getName(), - m_versionRange, - m_attrs[i].isMandatory()); - } - else if (m_attrs[i].getName().equals(Constants.BUNDLE_VERSION_ATTRIBUTE)) - { - String versionStr = (m_attrs[i].getValue() instanceof Version) - ? ((Version) m_attrs[i].getValue()).toString() - : (String) m_attrs[i].getValue(); - m_attrs[i] = new R4Attribute( - m_attrs[i].getName(), - VersionRange.parse(versionStr), - m_attrs[i].isMandatory()); - } - } - } - - public Version getVersionHigh() - { - return m_versionRange.getHigh(); - } - - public boolean isLowInclusive() - { - return m_versionRange.isLowInclusive(); - } - - public boolean isHighInclusive() - { - return m_versionRange.isHighInclusive(); - } - - public boolean isOptional() - { - return m_isOptional; - } - - public boolean isSatisfied(R4Export export) - { - // For packages to be compatible, they must have the - // same name. - if (!getName().equals(export.getName())) - { - return false; - } - - return m_versionRange.isInRange(export.getVersion()) - && doAttributesMatch(export); - } - - private boolean doAttributesMatch(R4Export export) - { - // Cycle through all attributes of this import package - // and make sure its values match the attribute values - // of the specified export package. - for (int impAttrIdx = 0; impAttrIdx < getAttributes().length; impAttrIdx++) - { - // Get current attribute from this import package. - R4Attribute impAttr = getAttributes()[impAttrIdx]; - - // Ignore version attribute, since it is a special case that - // has already been compared using isVersionInRange() before - // the call to this method was made. - if (impAttr.getName().equals(Constants.VERSION_ATTRIBUTE)) - { - continue; - } - - // Check if the export package has the same attribute. - boolean found = false; - for (int expAttrIdx = 0; - (!found) && (expAttrIdx < export.getAttributes().length); - expAttrIdx++) - { - // Get current attribute for the export package. - R4Attribute expAttr = export.getAttributes()[expAttrIdx]; - // Check if the attribute names are equal. - if (impAttr.getName().equals(expAttr.getName())) - { - // We only recognize version types. If the value of the - // attribute is a version/version range, then we use the - // "in range" comparison, otherwise we simply use equals(). - if (expAttr.getValue() instanceof Version) - { - if (!((VersionRange) impAttr.getValue()).isInRange((Version) expAttr.getValue())) - { - return false; - } - } - else if (!impAttr.getValue().equals(expAttr.getValue())) - { - return false; - } - found = true; - } - } - // If the attribute was not found, then return false. - if (!found) - { - return false; - } - } - - // Now, cycle through all attributes of the export package and verify that - // all mandatory attributes are present in this import package. - for (int expAttrIdx = 0; expAttrIdx < export.getAttributes().length; expAttrIdx++) - { - // Get current attribute for this package. - R4Attribute expAttr = export.getAttributes()[expAttrIdx]; - - // If the export attribute is mandatory, then make sure - // this import package has the attribute. - if (expAttr.isMandatory()) - { - boolean found = false; - for (int impAttrIdx = 0; - (!found) && (impAttrIdx < getAttributes().length); - impAttrIdx++) - { - // Get current attribute from specified package. - R4Attribute impAttr = getAttributes()[impAttrIdx]; - - // Check if the attribute names are equal - // and set found flag. - if (expAttr.getName().equals(impAttr.getName())) - { - found = true; - } - } - // If not found, then return false. - if (!found) - { - return false; - } - } - } - - return true; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Library.java b/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Library.java deleted file mode 100644 index 89aa66c85cd..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Library.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.searchpolicy; - -import org.apache.felix.framework.Logger; -import org.apache.felix.framework.cache.BundleRevision; -import org.osgi.framework.BundleException; -import org.osgi.framework.Constants; - -public class R4Library -{ - private Logger m_logger = null; - private BundleRevision m_revision = null; - private String m_libraryFile = null; - private String[] m_osnames = null; - private String[] m_processors = null; - private String[] m_osversions = null; - private String[] m_languages = null; - private String m_selectionFilter = null; - - public R4Library(Logger logger, BundleRevision revision, - String libraryFile, String[] osnames, String[] processors, String[] osversions, - String[] languages, String selectionFilter) - { - m_logger = logger; - m_revision = revision; - m_libraryFile = libraryFile; - m_osnames = osnames; - m_processors = processors; - m_osversions = osversions; - m_languages = languages; - m_selectionFilter = selectionFilter; - } - - public String[] getOSNames() - { - return m_osnames; - } - - public String[] getProcessors() - { - return m_processors; - } - - public String[] getOSVersions() - { - return m_osversions; - } - - public String[] getLanguages() - { - return m_languages; - } - - public String getSelectionFilter() - { - return m_selectionFilter; - } - - /** - *

      - * Returns a file system path to the specified library. - *

      - * - * @param name the name of the library that is being requested. - * @return a file system path to the specified library. - */ - public String getPath(String name) - { - String libname = System.mapLibraryName(name); - if (m_libraryFile.indexOf(libname) >= 0) - { - try - { - return m_revision.findLibrary(m_libraryFile); - } - catch (Exception ex) - { - m_logger.log(Logger.LOG_ERROR, "R4Library: Finding library '" - + name + "'.", new BundleException( - "Unable to find native library '" + name + "'.")); - } - } - return null; - } - - public String toString() - { - if (m_libraryFile != null) - { - StringBuffer sb = new StringBuffer(); - sb.append(m_libraryFile); - sb.append(';'); - for (int i = 0; (m_osnames != null) && (i < m_osnames.length); i++) - { - sb.append(Constants.BUNDLE_NATIVECODE_OSNAME); - sb.append('='); - sb.append(m_osnames[i]); - sb.append(';'); - } - for (int i = 0; (m_processors != null) && (i < m_processors.length); i++) - { - sb.append(Constants.BUNDLE_NATIVECODE_PROCESSOR); - sb.append(m_processors[i]); - sb.append(';'); - } - for (int i = 0; (m_osversions != null) && (i < m_osversions.length); i++) - { - sb.append(Constants.BUNDLE_NATIVECODE_OSVERSION); - sb.append(m_osversions[i]); - sb.append(';'); - } - for (int i = 0; (m_languages != null) && (i < m_languages.length); i++) - { - sb.append(Constants.BUNDLE_NATIVECODE_LANGUAGE); - sb.append(m_languages[i]); - sb.append(';'); - } - sb.append(Constants.SELECTION_FILTER_ATTRIBUTE); - sb.append('='); - sb.append('\''); - sb.append(m_selectionFilter); - sb.append('\''); - - return sb.toString(); - } - return "*"; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4LibraryClause.java b/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4LibraryClause.java deleted file mode 100644 index cbb2c50d917..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4LibraryClause.java +++ /dev/null @@ -1,521 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.searchpolicy; - -import java.util.*; - -import org.apache.felix.framework.FilterImpl; -import org.apache.felix.framework.Logger; -import org.apache.felix.framework.util.FelixConstants; -import org.apache.felix.framework.util.PropertyResolver; -import org.osgi.framework.*; - -public class R4LibraryClause -{ - private String[] m_libraryFiles = null; - private String[] m_osnames = null; - private String[] m_processors = null; - private String[] m_osversions = null; - private String[] m_languages = null; - private String m_selectionFilter = null; - - public R4LibraryClause(String[] libraryFiles, String[] osnames, - String[] processors, String[] osversions, String[] languages, - String selectionFilter) - { - m_libraryFiles = libraryFiles; - m_osnames = osnames; - m_processors = processors; - m_osversions = osversions; - m_languages = languages; - m_selectionFilter = selectionFilter; - } - - public R4LibraryClause(R4LibraryClause library) - { - m_libraryFiles = library.m_libraryFiles; - m_osnames = library.m_osnames; - m_osversions = library.m_osversions; - m_processors = library.m_processors; - m_languages = library.m_languages; - m_selectionFilter = library.m_selectionFilter; - } - - public String[] getLibraryFiles() - { - return m_libraryFiles; - } - - public String[] getOSNames() - { - return m_osnames; - } - - public String[] getProcessors() - { - return m_processors; - } - - public String[] getOSVersions() - { - return m_osversions; - } - - public String[] getLanguages() - { - return m_languages; - } - - public String getSelectionFilter() - { - return m_selectionFilter; - } - - public boolean match(PropertyResolver config) throws BundleException - { - String normal_osname = normalizeOSName(config.get(Constants.FRAMEWORK_OS_NAME)); - String normal_processor = normalizeProcessor(config.get(Constants.FRAMEWORK_PROCESSOR)); - String normal_osversion = normalizeOSVersion(config.get(Constants.FRAMEWORK_OS_VERSION)); - String normal_language = config.get(Constants.FRAMEWORK_LANGUAGE); - - // Check library's osname. - if (!checkOSNames(normal_osname, getOSNames())) - { - return false; - } - - // Check library's processor. - if (!checkProcessors(normal_processor, getProcessors())) - { - return false; - } - - // Check library's osversion if specified. - if ((getOSVersions() != null) && - (getOSVersions().length > 0) && - !checkOSVersions(normal_osversion, getOSVersions())) - { - return false; - } - - // Check library's language if specified. - if ((getLanguages() != null) && - (getLanguages().length > 0) && - !checkLanguages(normal_language, getLanguages())) - { - return false; - } - - // Check library's selection-filter if specified. - if ((getSelectionFilter() != null) && - (getSelectionFilter().length() >= 0) && - !checkSelectionFilter(config, getSelectionFilter())) - { - return false; - } - - return true; - } - - private boolean checkOSNames(String currentOSName, String[] osnames) - { - for (int i = 0; (osnames != null) && (i < osnames.length); i++) - { - if (osnames[i].equals(currentOSName)) - { - return true; - } - } - return false; - } - - private boolean checkProcessors(String currentProcessor, String[] processors) - { - for (int i = 0; (processors != null) && (i < processors.length); i++) - { - if (processors[i].equals(currentProcessor)) - { - return true; - } - } - return false; - } - - private boolean checkOSVersions(String currentOSVersion, String[] osversions) - throws BundleException - { - for (int i = 0; (osversions != null) && (i < osversions.length); i++) - { - try - { - VersionRange range = VersionRange.parse(osversions[i]); - if (range.isInRange(new Version(currentOSVersion))) - { - return true; - } - } - catch (Exception ex) - { - throw new BundleException( - "Error evaluating osversion: " + osversions[i], ex); - } - } - return false; - } - - private boolean checkLanguages(String currentLanguage, String[] languages) - { - for (int i = 0; (languages != null) && (i < languages.length); i++) - { - if (languages[i].equals(currentLanguage)) - { - return true; - } - } - return false; - } - - private boolean checkSelectionFilter(PropertyResolver config, String expr) - throws BundleException - { - // Get all framework properties - Dictionary dict = new Hashtable(); - String[] keys = config.getKeys(); - for (int i = 0; i < keys.length; i++) - { - dict.put(keys[i], config.get(keys[i])); - } - // Compute expression - try - { - FilterImpl filter = new FilterImpl(expr); - return filter.match(dict); - } - catch (Exception ex) - { - throw new BundleException( - "Error evaluating filter expression: " + expr, ex); - } - } - - public static R4LibraryClause parse(Logger logger, String s) - { - try - { - if ((s == null) || (s.length() == 0)) - { - return null; - } - - if (s.equals(FelixConstants.BUNDLE_NATIVECODE_OPTIONAL)) - { - return new R4LibraryClause(null, null, null, null, null, null); - } - - // The tokens are separated by semicolons and may include - // any number of libraries along with one set of associated - // properties. - StringTokenizer st = new StringTokenizer(s, ";"); - String[] libFiles = new String[st.countTokens()]; - List osNameList = new ArrayList(); - List osVersionList = new ArrayList(); - List processorList = new ArrayList(); - List languageList = new ArrayList(); - String selectionFilter = null; - int libCount = 0; - while (st.hasMoreTokens()) - { - String token = st.nextToken().trim(); - if (token.indexOf('=') < 0) - { - // Remove the slash, if necessary. - libFiles[libCount] = (token.charAt(0) == '/') - ? token.substring(1) - : token; - libCount++; - } - else - { - // Check for valid native library properties; defined as - // a property name, an equal sign, and a value. - // NOTE: StringTokenizer can not be used here because - // a value can contain one or more "=" too, e.g., - // selection-filter="(org.osgi.framework.windowing.system=gtk)" - String property = null; - String value = null; - if (!(token.indexOf("=") > 1)) - { - throw new IllegalArgumentException( - "Bundle manifest native library entry malformed: " + token); - } - else - { - property = (token.substring(0, token.indexOf("="))) - .trim().toLowerCase(); - value = (token.substring(token.indexOf("=") + 1, token - .length())).trim(); - } - - // Values may be quoted, so remove quotes if present. - if (value.charAt(0) == '"') - { - // This should always be true, otherwise the - // value wouldn't be properly quoted, but we - // will check for safety. - if (value.charAt(value.length() - 1) == '"') - { - value = value.substring(1, value.length() - 1); - } - else - { - value = value.substring(1); - } - } - // Add the value to its corresponding property list. - if (property.equals(Constants.BUNDLE_NATIVECODE_OSNAME)) - { - osNameList.add(value); - } - else if (property.equals(Constants.BUNDLE_NATIVECODE_OSVERSION)) - { - osVersionList.add(value); - } - else if (property.equals(Constants.BUNDLE_NATIVECODE_PROCESSOR)) - { - processorList.add(value); - } - else if (property.equals(Constants.BUNDLE_NATIVECODE_LANGUAGE)) - { - languageList.add(value); - } - else if (property.equals(Constants.SELECTION_FILTER_ATTRIBUTE)) - { -// TODO: NATIVE - I believe we can have multiple selection filters too. - selectionFilter = value; - } - } - } - - if (libCount == 0) - { - return null; - } - - // Shrink lib file array. - String[] actualLibFiles = new String[libCount]; - System.arraycopy(libFiles, 0, actualLibFiles, 0, libCount); - return new R4LibraryClause( - actualLibFiles, - (String[]) osNameList.toArray(new String[0]), - (String[]) processorList.toArray(new String[0]), - (String[]) osVersionList.toArray(new String[0]), - (String[]) languageList.toArray(new String[0]), - selectionFilter); - } - catch (RuntimeException ex) - { - logger.log(Logger.LOG_ERROR, - "Error parsing native library header.", ex); - throw ex; - } - } - - public static String normalizeOSName(String value) - { - if (value.startsWith("win")) - { - String os = "win"; - if (value.indexOf("32") >= 0 || value.indexOf("*") >= 0) - { - os = "win32"; - } - else if (value.indexOf("95") >= 0) - { - os = "windows95"; - } - else if (value.indexOf("98") >= 0) - { - os = "windows98"; - } - else if (value.indexOf("nt") >= 0) - { - os = "windowsnt"; - } - else if (value.indexOf("2000") >= 0) - { - os = "windows2000"; - } - else if (value.indexOf("xp") >= 0) - { - os = "windowsxp"; - } - else if (value.indexOf("ce") >= 0) - { - os = "windowsce"; - } - return os; - } - else if (value.startsWith("linux")) - { - return "linux"; - } - else if (value.startsWith("aix")) - { - return "aix"; - } - else if (value.startsWith("digitalunix")) - { - return "digitalunix"; - } - else if (value.startsWith("hpux")) - { - return "hpux"; - } - else if (value.startsWith("irix")) - { - return "irix"; - } - else if (value.startsWith("macos")) - { - return "macos"; - } - else if (value.startsWith("netware")) - { - return "netware"; - } - else if (value.startsWith("openbsd")) - { - return "openbsd"; - } - else if (value.startsWith("netbsd")) - { - return "netbsd"; - } - else if (value.startsWith("os2") || value.startsWith("os/2")) - { - return "os2"; - } - else if (value.startsWith("qnx") || value.startsWith("procnto")) - { - return "qnx"; - } - else if (value.startsWith("solaris")) - { - return "solaris"; - } - else if (value.startsWith("sunos")) - { - return "sunos"; - } - else if (value.startsWith("vxworks")) - { - return "vxworks"; - } - return value; - } - - public static String normalizeProcessor(String value) - { - if (value.startsWith("x86") || value.startsWith("pentium") - || value.startsWith("i386") || value.startsWith("i486") - || value.startsWith("i586") || value.startsWith("i686")) - { - return "x86"; - } - else if (value.startsWith("x86-64") || value.startsWith("amd64")) - { - return "x86-64"; - } - else if (value.startsWith("68k")) - { - return "68k"; - } - else if (value.startsWith("arm")) - { - return "arm"; - } - else if (value.startsWith("alpha")) - { - return "alpha"; - } - else if (value.startsWith("ignite") || value.startsWith("psc1k")) - { - return "ignite"; - } - else if (value.startsWith("mips")) - { - return "mips"; - } - else if (value.startsWith("parisc")) - { - return "parisc"; - } - else if (value.startsWith("powerpc") || value.startsWith("power") - || value.startsWith("ppc")) - { - return "powerpc"; - } - else if (value.startsWith("sparc")) - { - return "sparc"; - } - return value; - } - - public static String normalizeOSVersion(String value) - { - // Header: 'Bundle-NativeCode', Parameter: 'osversion' - // Standardized 'osversion': major.minor.micro, only digits - String VERSION_DELIM = "."; - String QUALIFIER_DELIM = "-"; - int major = 0; - int minor = 0; - int micro = 0; - try - { - StringTokenizer st = new StringTokenizer(value, VERSION_DELIM, true); - major = Integer.parseInt(st.nextToken()); - - if (st.hasMoreTokens()) - { - st.nextToken(); // consume delimiter - minor = Integer.parseInt(st.nextToken()); - - if (st.hasMoreTokens()) - { - st.nextToken(); // consume delimiter - String microStr = st.nextToken(); - if (microStr.indexOf(QUALIFIER_DELIM) < 0) - { - micro = Integer.parseInt(microStr); - } - else - { - micro = Integer.parseInt(microStr.substring(0, microStr - .indexOf(QUALIFIER_DELIM))); - } - } - } - } - catch (Exception ex) - { - return Version.emptyVersion.toString(); - } - - return major + "." + minor + "." + micro; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Package.java b/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Package.java deleted file mode 100755 index f0d5a8c9194..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Package.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.searchpolicy; - -import java.util.*; - -import org.apache.felix.framework.util.FelixConstants; -import org.apache.felix.framework.util.Util; -import org.osgi.framework.Constants; -import org.osgi.framework.Version; - -public class R4Package -{ - private String m_name = ""; - protected R4Directive[] m_directives = null; - protected R4Attribute[] m_attrs = null; - protected Version m_version = new Version("0.0.0"); - - public R4Package(String name, R4Directive[] directives, R4Attribute[] attrs) - { - m_name = name; - m_directives = (directives == null) - ? new R4Directive[0] : (R4Directive[]) directives.clone(); - m_attrs = (attrs == null) - ? new R4Attribute[0] : (R4Attribute[]) attrs.clone(); - } - - public String getName() - { - return m_name; - } - - public R4Directive[] getDirectives() - { - return m_directives; - } - - public R4Attribute[] getAttributes() - { - return m_attrs; - } - - public Version getVersion() - { - return m_version; - } - - - public String toString() - { - String msg = getName(); - for (int i = 0; (m_directives != null) && (i < m_directives.length); i++) - { - msg = msg + " [" + m_directives[i].getName() + ":="+ m_directives[i].getValue() + "]"; - } - for (int i = 0; (m_attrs != null) && (i < m_attrs.length); i++) - { - msg = msg + " [" + m_attrs[i].getName() + "="+ m_attrs[i].getValue() + "]"; - } - return msg; - } - - // Like this: pkg1; pkg2; dir1:=dirval1; dir2:=dirval2; attr1=attrval1; attr2=attrval2, - // pkg1; pkg2; dir1:=dirval1; dir2:=dirval2; attr1=attrval1; attr2=attrval2 - public static R4Package[] parseImportOrExportHeader(String s) - { - R4Package[] pkgs = null; - if (s != null) - { - if (s.length() == 0) - { - throw new IllegalArgumentException( - "The import and export headers cannot be an empty string."); - } - String[] ss = Util.parseDelimitedString( - s, FelixConstants.CLASS_PATH_SEPARATOR); - pkgs = parsePackageStrings(ss); - } - return (pkgs == null) ? new R4Package[0] : pkgs; - } - - // Like this: pkg1; pkg2; dir1:=dirval1; dir2:=dirval2; attr1=attrval1; attr2=attrval2 - public static R4Package[] parsePackageStrings(String[] ss) - throws IllegalArgumentException - { - if (ss == null) - { - return null; - } - - List completeList = new ArrayList(); - for (int ssIdx = 0; ssIdx < ss.length; ssIdx++) - { - // Break string into semi-colon delimited pieces. - String[] pieces = Util.parseDelimitedString( - ss[ssIdx], FelixConstants.PACKAGE_SEPARATOR); - - // Count the number of different packages; packages - // will not have an '=' in their string. This assumes - // that packages come first, before directives and - // attributes. - int pkgCount = 0; - for (int pieceIdx = 0; pieceIdx < pieces.length; pieceIdx++) - { - if (pieces[pieceIdx].indexOf('=') >= 0) - { - break; - } - pkgCount++; - } - - // Error if no packages were specified. - if (pkgCount == 0) - { - throw new IllegalArgumentException( - "No packages specified on import: " + ss[ssIdx]); - } - - // Parse the directives/attributes. - Map dirsMap = new HashMap(); - Map attrsMap = new HashMap(); - int idx = -1; - String sep = null; - for (int pieceIdx = pkgCount; pieceIdx < pieces.length; pieceIdx++) - { - // Check if it is a directive. - if ((idx = pieces[pieceIdx].indexOf(FelixConstants.DIRECTIVE_SEPARATOR)) >= 0) - { - sep = FelixConstants.DIRECTIVE_SEPARATOR; - } - // Check if it is an attribute. - else if ((idx = pieces[pieceIdx].indexOf(FelixConstants.ATTRIBUTE_SEPARATOR)) >= 0) - { - sep = FelixConstants.ATTRIBUTE_SEPARATOR; - } - // It is an error. - else - { - throw new IllegalArgumentException( - "Not a directive/attribute: " + ss[ssIdx]); - } - - String key = pieces[pieceIdx].substring(0, idx).trim(); - String value = pieces[pieceIdx].substring(idx + sep.length()).trim(); - - // Remove quotes, if value is quoted. - if (value.startsWith("\"") && value.endsWith("\"")) - { - value = value.substring(1, value.length() - 1); - } - - // Save the directive/attribute in the appropriate array. - if (sep.equals(FelixConstants.DIRECTIVE_SEPARATOR)) - { - // Check for duplicates. - if (dirsMap.get(key) != null) - { - throw new IllegalArgumentException( - "Duplicate directive: " + key); - } - dirsMap.put(key, new R4Directive(key, value)); - } - else - { - // Check for duplicates. - if (attrsMap.get(key) != null) - { - throw new IllegalArgumentException( - "Duplicate attribute: " + key); - } - attrsMap.put(key, new R4Attribute(key, value, false)); - } - } - - // Check for "version" and "specification-version" attributes - // and verify they are the same if both are specified. - R4Attribute v = (R4Attribute) attrsMap.get(Constants.VERSION_ATTRIBUTE); - R4Attribute sv = (R4Attribute) attrsMap.get(Constants.PACKAGE_SPECIFICATION_VERSION); - if ((v != null) && (sv != null)) - { - // Verify they are equal. - if (!((String) v.getValue()).trim().equals(((String) sv.getValue()).trim())) - { - throw new IllegalArgumentException( - "Both version and specificat-version are specified, but they are not equal."); - } - } - - // Ensure that only the "version" attribute is used - if (sv != null) - { - attrsMap.remove(Constants.PACKAGE_SPECIFICATION_VERSION); - if (v == null) - { - attrsMap.put(Constants.VERSION_ATTRIBUTE, - new R4Attribute( - Constants.VERSION_ATTRIBUTE, - sv.getValue(), - sv.isMandatory())); - } - } - - // Create directive array. - R4Directive[] dirs = (R4Directive[]) - dirsMap.values().toArray(new R4Directive[dirsMap.size()]); - - // Create attribute array. - R4Attribute[] attrs = (R4Attribute[]) - attrsMap.values().toArray(new R4Attribute[attrsMap.size()]); - - // Create package attributes for each package and - // set directives/attributes. Add each package to - // completel list of packages. - R4Package[] pkgs = new R4Package[pkgCount]; - for (int pkgIdx = 0; pkgIdx < pkgCount; pkgIdx++) - { - pkgs[pkgIdx] = new R4Package(pieces[pkgIdx], dirs, attrs); - completeList.add(pkgs[pkgIdx]); - } - } - - R4Package[] pkgs = (R4Package[]) - completeList.toArray(new R4Package[completeList.size()]); - return pkgs; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4SearchPolicy.java b/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4SearchPolicy.java deleted file mode 100644 index ad777609b92..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4SearchPolicy.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.searchpolicy; - -import java.net.URL; - -import org.apache.felix.moduleloader.*; - -public class R4SearchPolicy implements ISearchPolicy -{ - private R4SearchPolicyCore m_policyCore = null; - private IModule m_module = null; - - public R4SearchPolicy(R4SearchPolicyCore policyCore, IModule module) - { - m_policyCore = policyCore; - m_module = module; - } - - public Object[] definePackage(String name) - { - return m_policyCore.definePackage(m_module, name); - } - - public Class findClass(String name) - throws ClassNotFoundException - { - return m_policyCore.findClass(m_module, name); - } - - public URL findResource(String name) - throws ResourceNotFoundException - { - return m_policyCore.findResource(m_module, name); - } - - public String findLibrary(String name) - { - return m_policyCore.findLibrary(m_module, name); - } - - public String toString() - { - return m_module.toString(); - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4SearchPolicyCore.java b/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4SearchPolicyCore.java deleted file mode 100755 index 904ab71743a..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4SearchPolicyCore.java +++ /dev/null @@ -1,1785 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.searchpolicy; - -import java.net.URL; -import java.security.ProtectionDomain; -import java.util.*; - -import org.apache.felix.framework.Logger; -import org.apache.felix.framework.util.*; -import org.apache.felix.moduleloader.*; -import org.osgi.framework.*; - -public class R4SearchPolicyCore implements ModuleListener -{ - private Logger m_logger = null; - private PropertyResolver m_config = null; - private IModuleFactory m_factory = null; - private Map m_availPkgMap = new HashMap(); - private Map m_inUsePkgMap = new HashMap(); - private Map m_moduleDataMap = new HashMap(); - - // Boot delegation packages. - private String[] m_bootPkgs = null; - private boolean[] m_bootPkgWildcards = null; - - // Listener-related instance variables. - private static final ResolveListener[] m_emptyListeners = new ResolveListener[0]; - private ResolveListener[] m_listeners = m_emptyListeners; - - // Reusable empty array. - public static final IModule[] m_emptyModules = new IModule[0]; - - // Re-usable security manager for accessing class context. - private static SecurityManagerEx m_sm = new SecurityManagerEx(); - - public R4SearchPolicyCore(Logger logger, PropertyResolver config) - { - m_logger = logger; - m_config = config; - - // Read the boot delegation property and parse it. - String s = m_config.get(Constants.FRAMEWORK_BOOTDELEGATION); - s = (s == null) ? "java.*" : s + ",java.*"; - StringTokenizer st = new StringTokenizer(s, " ,"); - m_bootPkgs = new String[st.countTokens()]; - m_bootPkgWildcards = new boolean[m_bootPkgs.length]; - for (int i = 0; i < m_bootPkgs.length; i++) - { - s = st.nextToken(); - if (s.endsWith("*")) - { - m_bootPkgWildcards[i] = true; - s = s.substring(0, s.length() - 1); - } - m_bootPkgs[i] = s; - } - } - - public IModuleFactory getModuleFactory() - { - return m_factory; - } - - public void setModuleFactory(IModuleFactory factory) - throws IllegalStateException - { - if (m_factory == null) - { - m_factory = factory; - m_factory.addModuleListener(this); - } - else - { - throw new IllegalStateException("Module manager is already initialized"); - } - } - - protected synchronized boolean isResolved(IModule module) - { - ModuleData data = (ModuleData) m_moduleDataMap.get(module); - return (data == null) ? false : data.m_resolved; - } - - protected synchronized void setResolved(IModule module, boolean resolved) - { - ModuleData data = (ModuleData) m_moduleDataMap.get(module); - if (data == null) - { - data = new ModuleData(module); - m_moduleDataMap.put(module, data); - } - data.m_resolved = resolved; - } - - public Object[] definePackage(IModule module, String pkgName) - { - R4Package pkg = Util.getExportPackage(module, pkgName); - if (pkg != null) - { - return new Object[] { - pkgName, // Spec title. - pkg.getVersion().toString(), // Spec version. - "", // Spec vendor. - "", // Impl title. - "", // Impl version. - "" // Impl vendor. - }; - } - return null; - } - - public Class findClass(IModule module, String name) - throws ClassNotFoundException - { - try - { - return (Class) findClassOrResource(module, name, true); - } - catch (ResourceNotFoundException ex) - { - // This should never happen, so just ignore it. - } - catch (ClassNotFoundException ex) - { - String msg = diagnoseClassLoadError(module, name); - throw new ClassNotFoundException(msg, ex); - } - - // We should never reach this point. - return null; - } - - public URL findResource(IModule module, String name) - throws ResourceNotFoundException - { - try - { - return (URL) findClassOrResource(module, name, false); - } - catch (ClassNotFoundException ex) - { - // This should never happen, so just ignore it. - } - catch (ResourceNotFoundException ex) - { - throw ex; - } - - // We should never reach this point. - return null; - } - - private Object findClassOrResource(IModule module, String name, boolean isClass) - throws ClassNotFoundException, ResourceNotFoundException - { - // First, try to resolve the originating module. -// TODO: Consider opimizing this call to resolve, since it is called -// for each class load. - try - { - resolve(module); - } - catch (ResolveException ex) - { - if (isClass) - { - // We do not use the resolve exception as the - // cause of the exception, since this would - // potentially leak internal module information. - throw new ClassNotFoundException( - name + ": cannot resolve package " - + ex.getPackage()); - } - else - { - // The spec states that if the bundle cannot be resolved, then - // only the local bundle's resources should be searched. So we - // will ask the module's own class path. - URL url = module.getContentLoader().getResource(name); - if (url != null) - { - return url; - } - - // We need to throw a resource not found exception. - throw new ResourceNotFoundException( - name + ": cannot resolve package " - + ex.getPackage()); - } - } - - // Get the package of the target class/resource. - String pkgName = (isClass) - ? Util.getClassPackage(name) - : Util.getResourcePackage(name); - - // Delegate any packages listed in the boot delegation - // property to the parent class loader. - for (int i = 0; i < m_bootPkgs.length; i++) - { - // A wildcarded boot delegation package will be in the form of "foo.", - // so if the package is wildcarded do a startsWith() or a regionMatches() - // to ignore the trailing "." to determine if the request should be - // delegated to the parent class loader. If the package is not wildcarded, - // then simply do an equals() test to see if the request should be - // delegated to the parent class loader. - if (pkgName.length() > 0) - { - // Only consider delegation if we have a package name, since - // we don't want to promote the default package. The spec does - // not take a stand on this issue. - if ((m_bootPkgWildcards[i] && - (pkgName.startsWith(m_bootPkgs[i]) || - m_bootPkgs[i].regionMatches(0, pkgName, 0, pkgName.length()))) - || (!m_bootPkgWildcards[i] && m_bootPkgs[i].equals(pkgName))) - { - return (isClass) - ? (Object) getClass().getClassLoader().loadClass(name) - : (Object) getClass().getClassLoader().getResource(name); - } - } - } - - // Look in the module's imports. Note that the search may - // be aborted if this method throws an exception, otherwise - // it continues if a null is returned. - Object result = searchImports(module, name, isClass); - - // If not found, try the module's own class path. - if (result == null) - { - result = (isClass) - ? (Object) module.getContentLoader().getClass(name) - : (Object) module.getContentLoader().getResource(name); - - // If still not found, then try the module's dynamic imports. - if (result == null) - { - result = searchDynamicImports(module, name, pkgName, isClass); - } - } - - if (result == null) - { - if (isClass) - { - throw new ClassNotFoundException(name); - } - else - { - throw new ResourceNotFoundException(name); - } - } - - return result; - } - - private Object searchImports(IModule module, String name, boolean isClass) - throws ClassNotFoundException, ResourceNotFoundException - { - // We delegate to the module's wires to find the class or resource. - IWire[] wires = module.getWires(); - for (int i = 0; (wires != null) && (i < wires.length); i++) - { - // If we find the class or resource, then return it. - Object result = (isClass) - ? (Object) wires[i].getClass(name) - : (Object) wires[i].getResource(name); - if (result != null) - { - return result; - } - } - - return null; - } - - private Object searchDynamicImports( - IModule module, String name, String pkgName, boolean isClass) - throws ClassNotFoundException, ResourceNotFoundException - { - // At this point, the module's imports were searched and so was the - // the module's content. Now we make an attempt to load the - // class/resource via a dynamic import, if possible. - IWire wire = attemptDynamicImport(module, pkgName); - - // If the dynamic import was successful, then this initial - // time we must directly return the result from dynamically - // created wire, but subsequent requests for classes/resources - // in the associated package will be processed as part of - // normal static imports. - if (wire != null) - { - // Return the class or resource. - return (isClass) - ? (Object) wire.getClass(name) - : (Object) wire.getResource(name); - } - - // At this point, the class/resource could not be found by the bundle's - // static or dynamic imports, nor its own content. Before we throw - // an exception, we will try to determine if the instigator of the - // class/resource load was a class from a bundle or not. This is necessary - // because the specification mandates that classes on the class path - // should be hidden (except for java.*), but it does allow for these - // classes/resources to be exposed by the system bundle as an export. - // However, in some situations classes on the class path make the faulty - // assumption that they can access everything on the class path from - // every other class loader that they come in contact with. This is - // not true if the class loader in question is from a bundle. Thus, - // this code tries to detect that situation. If the class - // instigating the load request was NOT from a bundle, then we will - // make the assumption that the caller actually wanted to use the - // parent class loader and we will delegate to it. If the class was - // from a bundle, then we will enforce strict class loading rules - // for the bundle and throw an exception. - - // Get the class context to see the classes on the stack. - Class[] classes = m_sm.getClassContext(); - // Start from 1 to skip security manager class. - for (int i = 1; i < classes.length; i++) - { - // Find the first class on the call stack that is not one - // of the R4 search policy classes, nor a class loader or - // class itself, because we want to ignore the calls to - // ClassLoader.loadClass() and Class.forName(). - if (!R4SearchPolicyCore.class.equals(classes[i]) - && !R4SearchPolicy.class.equals(classes[i]) - && !ClassLoader.class.isAssignableFrom(classes[i]) - && !Class.class.isAssignableFrom(classes[i])) - { - // If the instigating class was not from a bundle, then - // delegate to the parent class loader. Otherwise, break - // out of loop and return null. - if (!ContentClassLoader.class.isInstance(classes[i].getClassLoader())) - { - return this.getClass().getClassLoader().loadClass(name); - } - break; - } - } - - return null; - } - - private IWire attemptDynamicImport(IModule module, String pkgName) - { - R4Wire wire = null; - IModule candidate = null; - - // There is an overriding assumption here that a package is - // never split across bundles. If a package can be split - // across bundles, then this will fail. - - try - { - // Get the matching dynamic import, if any. - R4Import impMatch = createDynamicImportTarget(module, pkgName); - - // If the target package does not match any dynamically imported - // packages or if the module is already wired for the target package, - // then just return null. The module may be already wired to the target - // package if the class being searched for does not actually exist. - if ((impMatch == null) || (Util.getWire(module, impMatch.getName()) != null)) - { - return null; - } - - // At this point, the target package has matched a dynamically - // imported package spec. Now we must try to find a candidate - // exporter for target package and add it to the module's set - // of wires. - - // Lock module manager instance to ensure that nothing changes. - synchronized (m_factory) - { - // Try to add a new entry to the module's import attribute. - // Select the first candidate that successfully resolves. - - // First check already resolved exports for a match. - IModule[] candidates = getInUseExporters(impMatch, false); - // If there is an "in use" candidate, just take the first one. - if (candidates.length > 0) - { - candidate = candidates[0]; - } - - // If there were no "in use" candidates, then try "available" - // candidates and take the first one that can resolve. - if (candidate == null) - { - candidates = getAvailableExporters(impMatch, false); - for (int candIdx = 0; - (candidate == null) && (candIdx < candidates.length); - candIdx++) - { - try - { - resolve(candidates[candIdx]); - candidate = candidates[candIdx]; - } - catch (ResolveException ex) - { - } - } - } - - // If we found a candidate, then add it to the module's - // wiring attribute. - if (candidate != null) - { - IWire[] wires = module.getWires(); - R4Wire[] newWires = null; - if (wires == null) - { - newWires = new R4Wire[1]; - } - else - { - newWires = new R4Wire[wires.length + 1]; - System.arraycopy(wires, 0, newWires, 0, wires.length); - } - // Find the candidate's export package object and - // use that for creating the wire; this is necessary - // since it contains "uses" dependency information. - wire = new R4Wire( - module, candidate, - Util.getExportPackage(candidate, impMatch.getName())); - newWires[newWires.length - 1] = wire; - ((ModuleImpl) module).setWires(newWires); -m_logger.log(Logger.LOG_DEBUG, "WIRE: " + newWires[newWires.length - 1]); - } - } - } - catch (Exception ex) - { - m_logger.log(Logger.LOG_ERROR, "Unable to dynamically import package.", ex); - } - - return wire; - } - - private R4Import createDynamicImportTarget(IModule module, String pkgName) - { - // Check the dynamic import specs for a match of - // the target package. - R4Import[] dynamics = module.getDefinition().getDynamicImports(); - R4Import impMatch = null; - for (int i = 0; (impMatch == null) && (dynamics != null) - && (i < dynamics.length); i++) - { - // Star matches everything. - if (dynamics[i].getName().equals("*")) - { - // Create a package instance without wildcard. - impMatch = new R4Import(pkgName, dynamics[i] - .getDirectives(), dynamics[i].getAttributes()); - } - // Packages ending in ".*" must match starting strings. - else if (dynamics[i].getName().endsWith(".*")) - { - if (pkgName.regionMatches(0, dynamics[i].getName(), 0, - dynamics[i].getName().length() - 2)) - { - // Create a package instance without wildcard. - impMatch = new R4Import(pkgName, dynamics[i] - .getDirectives(), dynamics[i].getAttributes()); - } - } - // Or we can have a precise match. - else - { - if (pkgName.equals(dynamics[i].getName())) - { - impMatch = dynamics[i]; - } - } - } - - return impMatch; - } - - public String findLibrary(IModule module, String name) - { - // Remove leading slash, if present. - if (name.startsWith("/")) - { - name = name.substring(1); - } - - R4Library[] libs = module.getDefinition().getLibraries(); - for (int i = 0; (libs != null) && (i < libs.length); i++) - { - String lib = libs[i].getPath(name); - if (lib != null) - { - return lib; - } - } - return null; - } - - public IModule[] getAvailableExporters(R4Import pkg, boolean includeRemovalPending) - { - // Synchronized on the module manager to make sure that no - // modules are added, removed, or resolved. - synchronized (m_factory) - { - IModule[] exporters = getCompatibleExporters( - (IModule[]) m_availPkgMap.get(pkg.getName()), pkg, includeRemovalPending); - - if ((exporters != null) && (System.getSecurityManager() != null)) - { - PackagePermission perm = new PackagePermission(pkg.getName(), - PackagePermission.EXPORT); - - for (int i = 0;i < exporters.length;i++) - { - if (exporters[i] != null) - { - if (!((ProtectionDomain) exporters[i].getSecurityContext()).implies(perm)) - { - m_logger.log(Logger.LOG_DEBUG, - "PackagePermission.EXPORT denied for " + pkg + - "from " + exporters[i].getId()); - - exporters[i] = null; - } - } - } - - exporters = shrinkModuleArray(exporters); - } - - return exporters; - } - } - - public IModule[] getInUseExporters(R4Import pkg, boolean includeRemovalPending) - { - // Synchronized on the module manager to make sure that no - // modules are added, removed, or resolved. - synchronized (m_factory) - { - return getCompatibleExporters( - (IModule[]) m_inUsePkgMap.get(pkg.getName()), pkg, includeRemovalPending); - } - } - - public void resolve(IModule rootModule) - throws ResolveException - { - // If the module is already resolved, then we can just return. - if (isResolved(rootModule)) - { - return; - } - - // This variable maps an unresolved module to a list of resolver - // nodes, where there is one resolver node for each import that - // must be resolved. A resolver node contains the potential - // candidates to resolve the import and the current selected - // candidate index. - Map resolverMap = new HashMap(); - - // This map will be used to hold the final wires for all - // resolved modules, which can then be used to fire resolved - // events outside of the synchronized block. - Map resolvedModuleWireMap = null; - - // Synchronize on the module manager, because we don't want - // any modules being added or removed while we are in the - // middle of this operation. - synchronized (m_factory) - { - // The first step is to populate the resolver map. This - // will use the target module to populate the resolver map - // with all potential modules that need to be resolved as a - // result of resolving the target module. The key of the - // map is a potential module to be resolved and the value is - // a list of resolver nodes, one for each of the module's - // imports, where each resolver node contains the potential - // candidates for resolving the import. Not all modules in - // this map will be resolved, only the target module and - // any candidates selected to resolve its imports and the - // transitive imports this implies. - populateResolverMap(resolverMap, rootModule); - - // The next step is to use the resolver map to determine if - // the class space for the root module is consistent. This - // is an iterative process that transitively walks the "uses" - // relationships of all currently selected potential candidates - // for resolving import packages checking for conflicts. If a - // conflict is found, it "increments" the configuration of - // currently selected potential candidates and tests them again. - // If this method returns, then it has found a consistent set - // of candidates; otherwise, a resolve exception is thrown if - // it exhausts all possible combinations and could not find a - // consistent class space. - findConsistentClassSpace(resolverMap, rootModule); - - // The final step is to create the wires for the root module and - // transitively all modules that are to be resolved from the - // selected candidates for resolving the root module's imports. - // When this call returns, each module's wiring and resolved - // attributes are set. The resulting wiring map is used below - // to fire resolved events outside of the synchronized block. - // The resolved module wire map maps a module to its array of - // wires. - resolvedModuleWireMap = createWires(resolverMap, rootModule); - -//dumpAvailablePackages(); -//dumpUsedPackages(); - } // End of synchronized block on module manager. - - // Fire resolved events for all resolved modules; - // the resolved modules array will only be set if the resolve - // was successful after the root module was resolved. - if (resolvedModuleWireMap != null) - { - Iterator iter = resolvedModuleWireMap.entrySet().iterator(); - while (iter.hasNext()) - { - fireModuleResolved((IModule) ((Map.Entry) iter.next()).getKey()); - } - } - } - - private void populateResolverMap(Map resolverMap, IModule module) - throws ResolveException - { - // Detect cycles. - if (resolverMap.get(module) != null) - { - return; - } - // Map to hold the module's import packages - // and their respective resolving candidates. - List nodeList = new ArrayList(); - - // Even though the node list is currently empty, we - // record it in the resolver map early so we can use - // it to detect cycles. - resolverMap.put(module, nodeList); - - // Loop through each import and calculate its resolving - // set of candidates. - R4Import[] imports = module.getDefinition().getImports(); - for (int impIdx = 0; (imports != null) && (impIdx < imports.length); impIdx++) - { - // Get the candidates from the "in use" and "available" - // package maps. Candidates "in use" have higher priority - // than "available" ones, so put the "in use" candidates - // at the front of the list of candidates. - IModule[] inuse = getInUseExporters(imports[impIdx], false); - IModule[] available = getAvailableExporters(imports[impIdx], false); - IModule[] candidates = new IModule[inuse.length + available.length]; - System.arraycopy(inuse, 0, candidates, 0, inuse.length); - System.arraycopy(available, 0, candidates, inuse.length, available.length); - - // If we have candidates, then we need to recursively populate - // the resolver map with each of them. - ResolveException rethrow = null; - if (candidates.length > 0) - { - for (int candIdx = 0; candIdx < candidates.length; candIdx++) - { - try - { - // Only populate the resolver map with modules that - // are not already resolved. - if (!isResolved(candidates[candIdx])) - { - populateResolverMap(resolverMap, candidates[candIdx]); - } - } - catch (ResolveException ex) - { - // If we received a resolve exception, then the - // current candidate is not resolvable for some - // reason and should be removed from the list of - // candidates. For now, just null it. - candidates[candIdx] = null; - rethrow = ex; - } - } - - // Remove any nulled candidates to create the final list - // of available candidates. - candidates = shrinkModuleArray(candidates); - } - - // If no candidates exist at this point, then throw a - // resolve exception unless the import is optional. - if ((candidates.length == 0) && !imports[impIdx].isOptional()) - { - // If we have received an exception while trying to populate - // the resolver map, rethrow that exception since it might - // be useful. NOTE: This is not necessarily the "only" - // correct exception, since it is possible that multiple - // candidates were not resolvable, but it is better than - // nothing. - if (rethrow != null) - { - throw rethrow; - } - else - { - throw new ResolveException( - "Unable to resolve.", module, imports[impIdx]); - } - } - else if (candidates.length > 0) - { - nodeList.add( - new ResolverNode(module, imports[impIdx], candidates)); - } - } - } - -// TODO: REMOVE THESE DEBUG METHODS. - private void dumpAvailablePackages() - { - synchronized (this) - { - System.out.println("AVAILABLE PACKAGES:"); - for (Iterator i = m_availPkgMap.entrySet().iterator(); i.hasNext(); ) - { - Map.Entry entry = (Map.Entry) i.next(); - IModule[] modules = (IModule[]) entry.getValue(); - if ((modules != null) && (modules.length > 0)) - { - System.out.println(" " + entry.getKey()); - for (int j = 0; j < modules.length; j++) - { - System.out.println(" " + modules[j]); - } - } - } - } - } - - private void dumpUsedPackages() - { - synchronized (this) - { - System.out.println("USED PACKAGES:"); - for (Iterator i = m_inUsePkgMap.entrySet().iterator(); i.hasNext(); ) - { - Map.Entry entry = (Map.Entry) i.next(); - IModule[] modules = (IModule[]) entry.getValue(); - if ((modules != null) && (modules.length > 0)) - { - System.out.println(" " + entry.getKey()); - for (int j = 0; j < modules.length; j++) - { - System.out.println(" " + modules[j]); - } - } - } - } - } - - private IModule[] getCompatibleExporters( - IModule[] modules, R4Import target, boolean includeRemovalPending) - { - // Create list of compatible exporters. - IModule[] candidates = null; - for (int modIdx = 0; (modules != null) && (modIdx < modules.length); modIdx++) - { - // The spec says that we cannot consider modules that - // are pending removal, so ignore them. - if (includeRemovalPending || !modules[modIdx].isRemovalPending()) - { - // Get the modules export package for the target package. - R4Export export = Util.getExportPackage( - modules[modIdx], target.getName()); - // If compatible, then add the candidate to the list. - if ((export != null) && (target.isSatisfied(export))) - { - candidates = addModuleToArray(candidates, modules[modIdx]); - } - } - } - if (candidates == null) - { - return m_emptyModules; - } - return candidates; - } - - private void findConsistentClassSpace(Map resolverMap, IModule rootModule) - throws ResolveException - { - List resolverList = null; - - // Test the current set of candidates to determine if they - // are consistent. Keep looping until we find a consistent - // set or an exception is thrown. - Map cycleMap = new HashMap(); - while (!isClassSpaceConsistent(resolverMap, rootModule, cycleMap)) - { -m_logger.log( - Logger.LOG_DEBUG, - "Constraint violation detected, will try to repair."); - - // The incrementCandidateConfiguration() method requires an - // ordered access to the resolver map, so we will create - // a reusable list once right here. - if (resolverList == null) - { - resolverList = new ArrayList(); - for (Iterator iter = resolverMap.entrySet().iterator(); - iter.hasNext(); ) - { - resolverList.add((List) ((Map.Entry) iter.next()).getValue()); - } - } - - // Increment the candidate configuration so we can test again. - incrementCandidateConfiguration(resolverList); - - // Clear the cycle map. - cycleMap.clear(); - } - } - - private boolean isClassSpaceConsistent( - Map resolverMap, IModule rootModule, Map cycleMap) - { - // We do not need to verify that already resolved modules - // have consistent class spaces because they should be - // consistent by definition. Also, if the root module is - // part of a cycle, then just assume it is true. - if (isResolved(rootModule) || (cycleMap.get(rootModule) != null)) - { - return true; - } - - // Add to cycle map for future reference. - cycleMap.put(rootModule, rootModule); - - // Create an implicit "uses" constraint for every exported package - // of the root module that is not also imported; uses constraints - // for exported packages that are also imported will be taken - // care of as part of normal import package processing. - R4Export[] exports = rootModule.getDefinition().getExports(); - Map usesMap = new HashMap(); - for (int i = 0; (exports != null) && (i < exports.length); i++) - { - // Ignore exports that are also imported, since they - // will be taken care of when verifying import constraints. - if (Util.getImportPackage(rootModule, exports[i].getName()) == null) - { - usesMap.put(exports[i].getName(), rootModule); - } - } - - // Loop through the current candidates for the module's imports - // (available in the resolver node list of the resolver map) and - // calculate the uses constraints for each of the currently - // selected candidates for resolving the imports. Compare each - // candidate's constraints to the existing constraints to check - // for conflicts. - List nodeList = (List) resolverMap.get(rootModule); - for (int nodeIdx = 0; nodeIdx < nodeList.size(); nodeIdx++) - { - // Verify that the current candidate does not violate - // any "uses" constraints of existing candidates by - // calculating the candidate's transitive "uses" constraints - // for the provided package and testing whether they - // overlap with existing constraints. - - // First, get the resolver node. - ResolverNode node = (ResolverNode) nodeList.get(nodeIdx); - - // Verify that the current candidate itself has a consistent - // class space. - if (!isClassSpaceConsistent( - resolverMap, node.m_candidates[node.m_idx], cycleMap)) - { - return false; - } - - // Get the exported package from the current candidate that - // will be used to resolve the root module's import. - R4Export candidatePkg = Util.getExportPackage( - node.m_candidates[node.m_idx], node.m_import.getName()); - - // Calculate the "uses" dependencies implied by the candidate's - // exported package with respect to the currently selected - // candidates in the resolver map. - Map candUsesMap = calculateUsesDependencies( - resolverMap, - node.m_candidates[node.m_idx], - candidatePkg, - new HashMap()); - - // Iterate through the root module's current set of transitive - // "uses" constraints and compare them with the candidate's - // transitive set of constraints. - Iterator usesIter = candUsesMap.entrySet().iterator(); - while (usesIter.hasNext()) - { - // If the candidate's uses constraints overlap with - // the existing uses constraints, but refer to a - // different provider, then the class space is not - // consistent; thus, return false. - Map.Entry entry = (Map.Entry) usesIter.next(); - if ((usesMap.get(entry.getKey()) != null) && - (usesMap.get(entry.getKey()) != entry.getValue())) - { - return false; - } - } - - // Since the current candidate's uses constraints did not - // conflict with existing constraints, merge all constraints - // and keep testing the remaining candidates for the other - // imports of the root module. - usesMap.putAll(candUsesMap); - } - - return true; - } - - private Map calculateUsesDependencies( - Map resolverMap, IModule module, R4Export export, Map usesMap) - { -// TODO: CAN THIS BE OPTIMIZED? -// TODO: IS THIS CYCLE CHECK CORRECT?? -// TODO: WHAT HAPPENS IF THERE ARE OVERLAPS WHEN CALCULATING USES?? -// MAKE AN EXAMPLE WHERE TWO DEPENDENCIES PROVIDE SAME PACKAGE. - // Make sure we are not in a cycle. - if (usesMap.get(export.getName()) != null) - { - return usesMap; - } - - // The target package at least uses itself, - // so add it to the uses map. - usesMap.put(export.getName(), module); - - // Get the "uses" constraints for the target export - // package and calculate the transitive uses constraints - // of any used packages. - String[] uses = export.getUses(); - List nodeList = (List) resolverMap.get(module); - - // We need to walk the transitive closure of "uses" relationships - // for the current export package to calculate the entire set of - // "uses" constraints. - for (int usesIdx = 0; usesIdx < uses.length; usesIdx++) - { - // There are two possibilities at this point: 1) we are dealing - // with an already resolved bundle or 2) we are dealing with a - // bundle that has not yet been resolved. In case 1, there will - // be no resolver node in the resolver map, so we just need to - // examine the bundle directly to determine its exact constraints. - // In case 2, there will be a resolver node in the resolver map, - // so we will use that to determine the potential constraints of - // potential candidate for resolving the import. - - // This is case 1, described in the comment above. - if (nodeList == null) - { - // Get the actual exporter from the wire or if there - // is no wire, then get the export is from the module - // itself. - IWire wire = Util.getWire(module, uses[usesIdx]); - if (wire != null) - { - usesMap = calculateUsesDependencies( - resolverMap, wire.getExporter(), wire.getExport(), usesMap); - } - else - { - export = Util.getExportPackage(module, uses[usesIdx]); - if (export != null) - { - usesMap = calculateUsesDependencies( - resolverMap, module, export, usesMap); - } - } - } - // This is case 2, described in the comment above. - else - { - // First, get the resolver node for the "used" package. - ResolverNode node = null; - for (int nodeIdx = 0; - (node == null) && (nodeIdx < nodeList.size()); - nodeIdx++) - { - node = (ResolverNode) nodeList.get(nodeIdx); - if (!node.m_import.getName().equals(uses[usesIdx])) - { - node = null; - } - } - - // If there is a resolver node for the "used" package, - // then this means that the module imports the package - // and we need to recursively add the constraints of - // the potential exporting module. - if (node != null) - { - usesMap = calculateUsesDependencies( - resolverMap, - node.m_candidates[node.m_idx], - Util.getExportPackage(node.m_candidates[node.m_idx], node.m_import.getName()), - usesMap); - } - // If there was no resolver node for the "used" package, - // then this means that the module exports the package - // and we need to recursively add the constraints of this - // other exported package of this module. - else if (Util.getExportPackage(module, uses[usesIdx]) != null) - { - usesMap = calculateUsesDependencies( - resolverMap, - module, - Util.getExportPackage(module, uses[usesIdx]), - usesMap); - } - } - } - - return usesMap; - } - - private void incrementCandidateConfiguration(List resolverList) - throws ResolveException - { - for (int i = 0; i < resolverList.size(); i++) - { - List nodeList = (List) resolverList.get(i); - for (int j = 0; j < nodeList.size(); j++) - { - ResolverNode node = (ResolverNode) nodeList.get(j); - // See if we can increment the node, without overflowing - // the candidate array bounds. - if ((node.m_idx + 1) < node.m_candidates.length) - { - node.m_idx++; - return; - } - // If the index will overflow the candidate array bounds, - // then set the index back to zero and try to increment - // the next candidate. - else - { - node.m_idx = 0; - } - } - } - throw new ResolveException( - "Unable to resolve due to constraint violation.", null, null); - } - - private Map createWires(Map resolverMap, IModule rootModule) - { - Map resolvedModuleWireMap = - populateWireMap(resolverMap, rootModule, new HashMap()); - Iterator iter = resolvedModuleWireMap.entrySet().iterator(); - while (iter.hasNext()) - { - Map.Entry entry = (Map.Entry) iter.next(); - IModule module = (IModule) entry.getKey(); - IWire[] wires = (IWire[]) entry.getValue(); - - // Set the module's resolved and wiring attribute. - setResolved(module, true); - // Only add wires attribute if some exist; export - // only modules may not have wires. - if (wires.length > 0) - { - ((ModuleImpl) module).setWires(wires); - } - - // Remove the wire's exporting module from the "available" - // package map and put it into the "in use" package map; - // these steps may be a no-op. - for (int wireIdx = 0; - (wires != null) && (wireIdx < wires.length); - wireIdx++) - { -m_logger.log(Logger.LOG_DEBUG, "WIRE: " + wires[wireIdx]); - // First remove the wire module from "available" package map. - IModule[] modules = (IModule[]) m_availPkgMap.get(wires[wireIdx].getExport().getName()); - modules = removeModuleFromArray(modules, wires[wireIdx].getExporter()); - m_availPkgMap.put(wires[wireIdx].getExport().getName(), modules); - - // Also remove any exported packages from the "available" - // package map that are from the module associated with - // the current wires where the exported packages were not - // actually exported; an export may not be exported if - // the module also imports the same package and was wired - // to a different module. If the exported package is not - // actually exported, then we just want to remove it - // completely, since it cannot be used. - if (wires[wireIdx].getExporter() != module) - { - modules = (IModule[]) m_availPkgMap.get(wires[wireIdx].getExport().getName()); - modules = removeModuleFromArray(modules, module); - m_availPkgMap.put(wires[wireIdx].getExport().getName(), modules); - } - - // Add the module of the wire to the "in use" package map. - modules = (IModule[]) m_inUsePkgMap.get(wires[wireIdx].getExport().getName()); - modules = addModuleToArray(modules, wires[wireIdx].getExporter()); - m_inUsePkgMap.put(wires[wireIdx].getExport().getName(), modules); - } - } - return resolvedModuleWireMap; - } - - private Map populateWireMap(Map resolverMap, IModule module, Map wireMap) - { - // If the module is already resolved or it is part of - // a cycle, then just return the wire map. - if (isResolved(module) || (wireMap.get(module) != null)) - { - return wireMap; - } - - List nodeList = (List) resolverMap.get(module); - IWire[] wires = new IWire[nodeList.size()]; - - // Put the module in the wireMap with an empty wire array; - // we do this early so we can use it to detect cycles. - wireMap.put(module, wires); - - // Loop through each resolver node and create a wire - // for the selected candidate for the associated import. - for (int nodeIdx = 0; nodeIdx < nodeList.size(); nodeIdx++) - { - // Get the import's associated resolver node. - ResolverNode node = (ResolverNode) nodeList.get(nodeIdx); - - // Add the candidate to the list of wires. - R4Export export = - Util.getExportPackage(node.m_candidates[node.m_idx], node.m_import.getName()); - wires[nodeIdx] = new R4Wire(module, node.m_candidates[node.m_idx], export); - - // Create the wires for the selected candidate module. - wireMap = populateWireMap(resolverMap, node.m_candidates[node.m_idx], wireMap); - } - - return wireMap; - } - - // - // Event handling methods for validation events. - // - - /** - * Adds a resolver listener to the search policy. Resolver - * listeners are notified when a module is resolve and/or unresolved - * by the search policy. - * @param l the resolver listener to add. - **/ - public void addResolverListener(ResolveListener l) - { - // Verify listener. - if (l == null) - { - throw new IllegalArgumentException("Listener is null"); - } - - // Use the m_noListeners object as a lock. - synchronized (m_emptyListeners) - { - // If we have no listeners, then just add the new listener. - if (m_listeners == m_emptyListeners) - { - m_listeners = new ResolveListener[] { l }; - } - // Otherwise, we need to do some array copying. - // Notice, the old array is always valid, so if - // the dispatch thread is in the middle of a dispatch, - // then it has a reference to the old listener array - // and is not affected by the new value. - else - { - ResolveListener[] newList = new ResolveListener[m_listeners.length + 1]; - System.arraycopy(m_listeners, 0, newList, 0, m_listeners.length); - newList[m_listeners.length] = l; - m_listeners = newList; - } - } - } - - /** - * Removes a resolver listener to this search policy. - * @param l the resolver listener to remove. - **/ - public void removeResolverListener(ResolveListener l) - { - // Verify listener. - if (l == null) - { - throw new IllegalArgumentException("Listener is null"); - } - - // Use the m_emptyListeners object as a lock. - synchronized (m_emptyListeners) - { - // Try to find the instance in our list. - int idx = -1; - for (int i = 0; i < m_listeners.length; i++) - { - if (m_listeners[i].equals(l)) - { - idx = i; - break; - } - } - - // If we have the instance, then remove it. - if (idx >= 0) - { - // If this is the last listener, then point to empty list. - if (m_listeners.length == 1) - { - m_listeners = m_emptyListeners; - } - // Otherwise, we need to do some array copying. - // Notice, the old array is always valid, so if - // the dispatch thread is in the middle of a dispatch, - // then it has a reference to the old listener array - // and is not affected by the new value. - else - { - ResolveListener[] newList = new ResolveListener[m_listeners.length - 1]; - System.arraycopy(m_listeners, 0, newList, 0, idx); - if (idx < newList.length) - { - System.arraycopy(m_listeners, idx + 1, newList, idx, - newList.length - idx); - } - m_listeners = newList; - } - } - } - } - - /** - * Fires a validation event for the specified module. - * @param module the module that was resolved. - **/ - private void fireModuleResolved(IModule module) - { - // Event holder. - ModuleEvent event = null; - - // Get a copy of the listener array, which is guaranteed - // to not be null. - ResolveListener[] listeners = m_listeners; - - // Loop through listeners and fire events. - for (int i = 0; i < listeners.length; i++) - { - // Lazily create event. - if (event == null) - { - event = new ModuleEvent(m_factory, module); - } - listeners[i].moduleResolved(event); - } - } - - /** - * Fires an unresolved event for the specified module. - * @param module the module that was unresolved. - **/ - private void fireModuleUnresolved(IModule module) - { -// TODO: Call this method where appropriate. - // Event holder. - ModuleEvent event = null; - - // Get a copy of the listener array, which is guaranteed - // to not be null. - ResolveListener[] listeners = m_listeners; - - // Loop through listeners and fire events. - for (int i = 0; i < listeners.length; i++) - { - // Lazily create event. - if (event == null) - { - event = new ModuleEvent(m_factory, module); - } - listeners[i].moduleUnresolved(event); - } - } - - // - // ModuleListener callback methods. - // - - public void moduleAdded(ModuleEvent event) - { - synchronized (m_factory) - { - // When a module is added, create an aggregated list of available - // exports to simplify later processing when resolving bundles. - IModule module = event.getModule(); - R4Export[] exports = module.getDefinition().getExports(); - - // Add exports to available package map. - for (int i = 0; (exports != null) && (i < exports.length); i++) - { - IModule[] modules = (IModule[]) m_availPkgMap.get(exports[i].getName()); - - // We want to add the module into the list of available - // exporters in sorted order (descending version and - // ascending bundle identifier). Insert using a simple - // binary search algorithm. - if (modules == null) - { - modules = new IModule[] { module }; - } - else - { - int top = 0, bottom = modules.length - 1, middle = 0; - Version middleVersion = null; - while (top <= bottom) - { - middle = (bottom - top) / 2 + top; - middleVersion = Util.getExportPackage( - modules[middle], exports[i].getName()).getVersion(); - // Sort in reverse version order. - int cmp = middleVersion.compareTo(exports[i].getVersion()); - if (cmp < 0) - { - bottom = middle - 1; - } - else if (cmp == 0) - { - // Sort further by ascending bundle ID. - long middleId = Util.getBundleIdFromModuleId(modules[middle].getId()); - long exportId = Util.getBundleIdFromModuleId(module.getId()); - if (middleId < exportId) - { - top = middle + 1; - } - else - { - bottom = middle - 1; - } - } - else - { - top = middle + 1; - } - } - - IModule[] newMods = new IModule[modules.length + 1]; - System.arraycopy(modules, 0, newMods, 0, top); - System.arraycopy(modules, top, newMods, top + 1, modules.length - top); - newMods[top] = module; - modules = newMods; - } - - m_availPkgMap.put(exports[i].getName(), modules); - } - } - } - - public void moduleRemoved(ModuleEvent event) - { - // When a module is removed from the system, we need remove - // its exports from the "in use" and "available" package maps - // as well as remove the module from the module data map. - - // Synchronize on the module manager, since we don't want any - // bundles to be installed or removed. - synchronized (m_factory) - { - // Remove exports from package maps. - R4Export[] exports = event.getModule().getDefinition().getExports(); - for (int i = 0; (exports != null) && (i < exports.length); i++) - { - // Remove from "available" package map. - IModule[] modules = (IModule[]) m_availPkgMap.get(exports[i].getName()); - if (modules != null) - { - modules = removeModuleFromArray(modules, event.getModule()); - m_availPkgMap.put(exports[i].getName(), modules); - } - // Remove from "in use" package map. - modules = (IModule[]) m_inUsePkgMap.get(exports[i].getName()); - if (modules != null) - { - modules = removeModuleFromArray(modules, event.getModule()); - m_inUsePkgMap.put(exports[i].getName(), modules); - } - } - - // Finally, remove module data. - m_moduleDataMap.remove(event.getModule()); - } - } - - // - // Simple utility methods. - // - - private static IModule[] addModuleToArray(IModule[] modules, IModule m) - { - // Verify that the module is not already in the array. - for (int i = 0; (modules != null) && (i < modules.length); i++) - { - if (modules[i] == m) - { - return modules; - } - } - - if (modules != null) - { - IModule[] newModules = new IModule[modules.length + 1]; - System.arraycopy(modules, 0, newModules, 0, modules.length); - newModules[modules.length] = m; - modules = newModules; - } - else - { - modules = new IModule[] { m }; - } - - return modules; - } - - private static IModule[] removeModuleFromArray(IModule[] modules, IModule m) - { - if (modules == null) - { - return m_emptyModules; - } - - int idx = -1; - for (int i = 0; i < modules.length; i++) - { - if (modules[i] == m) - { - idx = i; - break; - } - } - - if (idx >= 0) - { - // If this is the module, then point to empty list. - if ((modules.length - 1) == 0) - { - modules = m_emptyModules; - } - // Otherwise, we need to do some array copying. - else - { - IModule[] newModules = new IModule[modules.length - 1]; - System.arraycopy(modules, 0, newModules, 0, idx); - if (idx < newModules.length) - { - System.arraycopy( - modules, idx + 1, newModules, idx, newModules.length - idx); - } - modules = newModules; - } - } - return modules; - } - - private static IModule[] shrinkModuleArray(IModule[] modules) - { - if (modules == null) - { - return m_emptyModules; - } - - // Move all non-null values to one end of the array. - int lower = 0; - for (int i = 0; i < modules.length; i++) - { - if (modules[i] != null) - { - modules[lower++] = modules[i]; - } - } - - if (lower == 0) - { - return m_emptyModules; - } - - // Copy non-null values into a new array and return. - IModule[] newModules = new IModule[lower]; - System.arraycopy(modules, 0, newModules, 0, lower); - return newModules; - } - - // - // Simple utility classes. - // - - private static class ModuleData - { - public IModule m_module = null; - public boolean m_resolved = false; - public ModuleData(IModule module) - { - m_module = module; - } - } - - private class ResolverNode - { - public IModule m_module = null; - public R4Import m_import = null; - public IModule[] m_candidates = null; - public int m_idx = 0; - public boolean m_visited = false; - public ResolverNode(IModule module, R4Import imp, IModule[] candidates) - { - m_module = module; - m_import = imp; - m_candidates = candidates; - if (isResolved(m_module)) - { - m_visited = true; - } - } - } - - private String diagnoseClassLoadError(IModule module, String name) - { - // We will try to do some diagnostics here to help the developer - // deal with this exception. - - // Get package name. - String pkgName = Util.getClassPackage(name); - - // First, get the bundle ID of the module doing the class loader. - long impId = Util.getBundleIdFromModuleId(module.getId()); - - // Next, check to see if the module imports the package. - IWire[] wires = module.getWires(); - for (int i = 0; (wires != null) && (i < wires.length); i++) - { - if (wires[i].getExport().getName().equals(pkgName)) - { - long expId = Util.getBundleIdFromModuleId( - wires[i].getExporter().getId()); - - StringBuffer sb = new StringBuffer("*** Package '"); - sb.append(pkgName); - sb.append("' is imported by bundle "); - sb.append(impId); - sb.append(" from bundle "); - sb.append(expId); - sb.append(", but the exported package from bundle "); - sb.append(expId); - sb.append(" does not contain the requested class '"); - sb.append(name); - sb.append("'. Please verify that the class name is correct in the importing bundle "); - sb.append(impId); - sb.append(" and/or that the exported package is correctly bundled in "); - sb.append(expId); - sb.append(". ***"); - - return sb.toString(); - } - } - - // Next, check to see if the package was optionally imported and - // whether or not there is an exporter available. - R4Import[] imports = module.getDefinition().getImports(); - for (int i = 0; (imports != null) && (i < imports.length); i++) - { - if (imports[i].getName().equals(pkgName) && imports[i].isOptional()) - { - // Try to see if there is an exporter available. It may be - // the case that the package is exported, but the attributes - // do not match, so check that case too. - IModule[] exporters = getInUseExporters(imports[i], true); - exporters = (exporters.length == 0) - ? getAvailableExporters(imports[i], true) : exporters; - exporters = (exporters.length == 0) - ? getInUseExporters(new R4Import(pkgName, null, null), true) : exporters; - exporters = (exporters.length == 0) - ? getAvailableExporters(new R4Import(pkgName, null, null), true) : exporters; - long expId = (exporters.length == 0) - ? -1 : Util.getBundleIdFromModuleId(exporters[0].getId()); - - StringBuffer sb = new StringBuffer("*** Class '"); - sb.append(name); - sb.append("' was not found, but this is likely normal since package '"); - sb.append(pkgName); - sb.append("' is optionally imported by bundle "); - sb.append(impId); - sb.append("."); - if (exporters.length > 0) - { - sb.append(" However, bundle "); - sb.append(expId); - if (imports[i].isSatisfied( - Util.getExportPackage(exporters[0], imports[i].getName()))) - { - sb.append(" does export this package. Bundle "); - sb.append(expId); - sb.append(" must be installed before bundle "); - sb.append(impId); - sb.append(" is resolved or else the optional import will be ignored."); - } - else - { - sb.append(" does export this package with attributes that do not match."); - } - } - sb.append(" ***"); - - return sb.toString(); - } - } - - // Next, check to see if the package is dynamically imported by the module. - R4Import imp = createDynamicImportTarget(module, pkgName); - if (imp != null) - { - // Try to see if there is an exporter available. It may be - // the case that the package is exported, but the attributes - // do not match, so check that case too. - IModule[] exporters = getInUseExporters(imp, true); - exporters = (exporters.length == 0) - ? getAvailableExporters(imp, true) : exporters; - exporters = (exporters.length == 0) - ? getInUseExporters(new R4Import(pkgName, null, null), true) : exporters; - exporters = (exporters.length == 0) - ? getAvailableExporters(new R4Import(pkgName, null, null), true) : exporters; - long expId = (exporters.length == 0) - ? -1 : Util.getBundleIdFromModuleId(exporters[0].getId()); - - StringBuffer sb = new StringBuffer("*** Class '"); - sb.append(name); - sb.append("' was not found, but this is likely normal since package '"); - sb.append(pkgName); - sb.append("' is dynamically imported by bundle "); - sb.append(impId); - sb.append("."); - if (exporters.length > 0) - { - if (!imp.isSatisfied( - Util.getExportPackage(exporters[0], imp.getName()))) - { - sb.append(" However, bundle "); - sb.append(expId); - sb.append(" does export this package with attributes that do not match."); - } - } - sb.append(" ***"); - - return sb.toString(); - } - - // Next, if the package is not imported by the module, check to - // see if there is an exporter for the package. - IModule[] exporters = getInUseExporters(new R4Import(pkgName, null, null), true); - exporters = (exporters.length == 0) - ? getAvailableExporters(new R4Import(pkgName, null, null), true) : exporters; - if (exporters.length > 0) - { - boolean classpath = false; - try - { - getClass().getClassLoader().loadClass(name); - classpath = true; - } - catch (Exception ex) - { - // Ignore - } - - long expId = Util.getBundleIdFromModuleId(exporters[0].getId()); - - StringBuffer sb = new StringBuffer("*** Class '"); - sb.append(name); - sb.append("' was not found because bundle "); - sb.append(impId); - sb.append(" does not import '"); - sb.append(pkgName); - sb.append("' even though bundle "); - sb.append(expId); - sb.append(" does export it."); - if (classpath) - { - sb.append(" Additionally, the class is also available from the system class loader. There are two fixes: 1) Add an import for '"); - sb.append(pkgName); - sb.append("' to bundle "); - sb.append(impId); - sb.append("; imports are necessary for each class directly touched by bundle code or indirectly touched, such as super classes if their methods are used. "); - sb.append("2) Add package '"); - sb.append(pkgName); - sb.append("' to the '"); - sb.append(Constants.FRAMEWORK_BOOTDELEGATION); - sb.append("' property; a library or VM bug can cause classes to be loaded by the wrong class loader. The first approach is preferable for preserving modularity."); - } - else - { - sb.append(" To resolve this issue, add an import for '"); - sb.append(pkgName); - sb.append("' to bundle "); - sb.append(impId); - sb.append("."); - } - sb.append(" ***"); - - return sb.toString(); - } - - // Next, try to see if the class is available from the system - // class loader. - try - { - getClass().getClassLoader().loadClass(name); - - StringBuffer sb = new StringBuffer("*** Package '"); - sb.append(pkgName); - sb.append("' is not imported by bundle "); - sb.append(impId); - sb.append(", nor is there any bundle that exports package '"); - sb.append(pkgName); - sb.append("'. However, the class '"); - sb.append(name); - sb.append("' is available from the system class loader. There are two fixes: 1) Add package '"); - sb.append(pkgName); - sb.append("' to the '"); - sb.append(Constants.FRAMEWORK_SYSTEMPACKAGES); - sb.append("' property and modify bundle "); - sb.append(impId); - sb.append(" to import this package; this causes the system bundle to export class path packages. 2) Add package '"); - sb.append(pkgName); - sb.append("' to the '"); - sb.append(Constants.FRAMEWORK_BOOTDELEGATION); - sb.append("' property; a library or VM bug can cause classes to be loaded by the wrong class loader. The first approach is preferable for preserving modularity."); - sb.append(" ***"); - - return sb.toString(); - } - catch (Exception ex2) - { - } - - // Finally, if there are no imports or exports for the package - // and it is not available on the system class path, simply - // log a message saying so. - StringBuffer sb = new StringBuffer("*** Class '"); - sb.append(name); - sb.append("' was not found. Bundle "); - sb.append(impId); - sb.append(" does not import package '"); - sb.append(pkgName); - sb.append("', nor is the package exported by any other bundle or available from the system class loader."); - sb.append(" ***"); - - return sb.toString(); - } -} diff --git a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Wire.java b/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Wire.java deleted file mode 100755 index c630036c4af..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/searchpolicy/R4Wire.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.searchpolicy; - -import java.net.URL; - -import org.apache.felix.framework.util.Util; -import org.apache.felix.moduleloader.*; - -public class R4Wire implements IWire -{ - private IModule m_importer = null; - private IModule m_exporter = null; - private R4Export m_export= null; - - public R4Wire(IModule importer, IModule exporter, R4Export export) - { - m_importer = importer; - m_exporter = exporter; - m_export = export; - } - - /* (non-Javadoc) - * @see org.apache.felix.framework.searchpolicy.IWire#getImportingModule() - */ - public IModule getImporter() - { - return m_importer; - } - - /* (non-Javadoc) - * @see org.apache.felix.framework.searchpolicy.IWire#getExportingModule() - */ - public IModule getExporter() - { - return m_exporter; - } - - /* (non-Javadoc) - * @see org.apache.felix.framework.searchpolicy.IWire#getExport() - */ - public R4Export getExport() - { - return m_export; - } - - /* (non-Javadoc) - * @see org.apache.felix.framework.searchpolicy.IWire#getClass(java.lang.String) - */ - public Class getClass(String name) throws ClassNotFoundException - { - Class clazz = null; - - // Get the package of the target class. - String pkgName = Util.getClassPackage(name); - - // Only check when the package of the target class is - // the same as the package for the wire. - if (m_export.getName().equals(pkgName)) - { - // Before delegating to the exporting module to satisfy - // the class load, we must check the include/exclude filters - // from the target package to make sure that the class is - // actually visible. However, if the exporting module is the - // same as the requesting module, then filtering is not - // performed since a module has complete access to itself. - if ((m_exporter == m_importer) || m_export.isIncluded(name)) - { - clazz = m_exporter.getContentLoader().getClass(name); - } - - // If no class was found, then we must throw an exception - // since the exporter for this package did not contain the - // requested class. - if (clazz == null) - { - throw new ClassNotFoundException(name); - } - } - - return clazz; - } - - /* (non-Javadoc) - * @see org.apache.felix.framework.searchpolicy.IWire#getResource(java.lang.String) - */ - public URL getResource(String name) throws ResourceNotFoundException - { - URL url = null; - - // Get the package of the target class. - String pkgName = Util.getResourcePackage(name); - - // Only check when the package of the target resource is - // the same as the package for the wire. - if (m_export.getName().equals(pkgName)) - { - url = m_exporter.getContentLoader().getResource(name); - - // If no resource was found, then we must throw an exception - // since the exporter for this package did not contain the - // requested class. - if (url == null) - { - throw new ResourceNotFoundException(name); - } - } - - return url; - } - - public String toString() - { - return m_importer + " -> " + m_export.getName() + " -> " + m_exporter; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/searchpolicy/ResolveException.java b/framework/src/main/java/org/apache/felix/framework/searchpolicy/ResolveException.java deleted file mode 100755 index b1632da591c..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/searchpolicy/ResolveException.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.searchpolicy; - -import org.apache.felix.moduleloader.IModule; - -/** - *

      - * This exception is thrown if a module cannot be resolved. The module - * that failed to be resolved is recorded, along with the failed import target - * identifier and version number. If the error was a result of a propagation - * conflict, then the propagation error flag is set. - *

      - * @see org.apache.felix.moduleloader.search.ImportSearchPolicy#validate(org.apache.felix.moduleloader.Module) -**/ -public class ResolveException extends Exception -{ - private IModule m_module = null; - private R4Package m_pkg = null; - - /** - * Constructs an exception with the specified message, module, - * import identifier, import version number, and propagation flag. - **/ - public ResolveException(String msg, IModule module, R4Package pkg) - { - super(msg); - m_module = module; - m_pkg = pkg; - } - - /** - * Returns the module that was being resolved. - * @return the module that was being resolved. - **/ - public IModule getModule() - { - return m_module; - } - - public R4Package getPackage() - { - return m_pkg; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/searchpolicy/ResolveListener.java b/framework/src/main/java/org/apache/felix/framework/searchpolicy/ResolveListener.java deleted file mode 100755 index 2ed2a623d58..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/searchpolicy/ResolveListener.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.searchpolicy; - -import java.util.EventListener; - -import org.apache.felix.moduleloader.ModuleEvent; - -/** - *

      - * This is an event listener interface for listening to resolution - * events that are generated by the R4SearchPolicy. Events - * are fired when a module is resolved and when it is unresolved. - *

      - * @see org.apache.felix.framework.searchpolicy.R4SearchPolicyCore -**/ -public interface ResolveListener extends EventListener -{ - /** - * This is an event callback method that indicates that - * a module was resolved. - * @param event the module event containing the event data. - **/ - public void moduleResolved(ModuleEvent event); - - /** - * This is an event callback method that indicates that - * a module was unresolved. - * @param event the module event containing the event data. - **/ - public void moduleUnresolved(ModuleEvent event); -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/searchpolicy/URLPolicyImpl.java b/framework/src/main/java/org/apache/felix/framework/searchpolicy/URLPolicyImpl.java deleted file mode 100644 index f2a69f15b69..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/searchpolicy/URLPolicyImpl.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.searchpolicy; - -import java.net.*; - -import org.apache.felix.framework.Logger; -import org.apache.felix.framework.util.FelixConstants; -import org.apache.felix.framework.util.SecureAction; -import org.apache.felix.moduleloader.IModule; -import org.apache.felix.moduleloader.IURLPolicy; - -public class URLPolicyImpl implements IURLPolicy -{ - private Logger m_logger = null; - private URLStreamHandler m_streamHandler = null; - private IModule m_module = null; - private static SecureAction m_secureAction = new SecureAction(); - -// TODO: ML - IT SUCKS HAVING A URL POLICY OBJECT PER MODULE! - public URLPolicyImpl(Logger logger, URLStreamHandler streamHandler, IModule module) - { - m_logger = logger; - m_streamHandler = streamHandler; - m_module = module; - } - - public URL createURL(String path) - { - // Add a slash if there is one already, otherwise - // the is no slash separating the host from the file - // in the resulting URL. - if (!path.startsWith("/")) - { - path = "/" + path; - } - - try - { - return m_secureAction.createURL( - FelixConstants.BUNDLE_URL_PROTOCOL, - m_module.getId(), -1, path, m_streamHandler); - } - catch (MalformedURLException ex) - { - m_logger.log( - Logger.LOG_ERROR, - "Unable to create resource URL.", - ex); - } - return null; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/searchpolicy/VersionRange.java b/framework/src/main/java/org/apache/felix/framework/searchpolicy/VersionRange.java deleted file mode 100644 index 6faf192c5ad..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/searchpolicy/VersionRange.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.searchpolicy; - -import org.osgi.framework.Version; - -public class VersionRange -{ - private Version m_low = null; - private boolean m_isLowInclusive = false; - private Version m_high = null; - private boolean m_isHighInclusive = false; - - public VersionRange(Version low, boolean isLowInclusive, - Version high, boolean isHighInclusive) - { - m_low = low; - m_isLowInclusive = isLowInclusive; - m_high = high; - m_isHighInclusive = isHighInclusive; - } - - public Version getLow() - { - return m_low; - } - - public boolean isLowInclusive() - { - return m_isLowInclusive; - } - - public Version getHigh() - { - return m_high; - } - - public boolean isHighInclusive() - { - return m_isHighInclusive; - } - - public boolean isInRange(Version version) - { - // We might not have an upper end to the range. - if (m_high == null) - { - return (version.compareTo(m_low) >= 0); - } - else if (isLowInclusive() && isHighInclusive()) - { - return (version.compareTo(m_low) >= 0) && (version.compareTo(m_high) <= 0); - } - else if (isHighInclusive()) - { - return (version.compareTo(m_low) > 0) && (version.compareTo(m_high) <= 0); - } - else if (isLowInclusive()) - { - return (version.compareTo(m_low) >= 0) && (version.compareTo(m_high) < 0); - } - return (version.compareTo(m_low) > 0) && (version.compareTo(m_high) < 0); - } - - public static VersionRange parse(String range) - { - // Check if the version is an interval. - if (range.indexOf(',') >= 0) - { - String s = range.substring(1, range.length() - 1); - String vlo = s.substring(0, s.indexOf(',')).trim(); - String vhi = s.substring(s.indexOf(',') + 1, s.length()).trim(); - return new VersionRange ( - new Version(vlo), (range.charAt(0) == '['), - new Version(vhi), (range.charAt(range.length() - 1) == ']')); - } - else - { - return new VersionRange(new Version(range), true, null, false); - } - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/ClassFileVisitor.java b/framework/src/main/java/org/apache/felix/framework/util/ClassFileVisitor.java new file mode 100644 index 00000000000..6722dac275d --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/util/ClassFileVisitor.java @@ -0,0 +1,85 @@ +/* + * 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.framework.util; + +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; + +import java.io.IOException; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeSet; + +@IgnoreJRERequirement +public class ClassFileVisitor extends java.nio.file.SimpleFileVisitor +{ + private final Set m_imports; + private final Set m_exports; + private final ClassParser m_classParser; + private final SortedMap> m_result; + + public ClassFileVisitor(Set imports, Set exports, ClassParser classParser, SortedMap> result) + { + m_imports = imports; + m_exports = exports; + m_classParser = classParser; + m_result = result; + } + + @Override + public java.nio.file.FileVisitResult visitFile(java.nio.file.Path file, java.nio.file.attribute.BasicFileAttributes attrs) throws IOException + { + if (file.getNameCount() > 3) + { + String name = file.subpath(2, file.getNameCount() - 1).toString().replace("/", "."); + if (m_exports.contains(name) && file.toString().endsWith(".class")) + { + SortedSet strings = m_result.get(name); + + if (!name.startsWith("java.")) + { + try + { + Set refs = m_classParser.parseClassFileUses(file.toString(), java.nio.file.Files.newInputStream(file)); + refs.retainAll(m_imports); + refs.remove(name); + if (strings == null) + { + strings = new TreeSet(refs); + m_result.put(name, strings); + } + else + { + strings.addAll(refs); + } + } + catch (Exception ex) + { + ex.printStackTrace(); + } + } + else if (strings == null) + { + m_result.put(name, new TreeSet()); + } + } + } + return java.nio.file.FileVisitResult.CONTINUE; + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/ClassParser.java b/framework/src/main/java/org/apache/felix/framework/util/ClassParser.java new file mode 100644 index 00000000000..cb08530bca9 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/util/ClassParser.java @@ -0,0 +1,2808 @@ +/* + * 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.framework.util; + +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.ElementType; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Modifier; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * This class is based on code developed at https://github.com/bndtools/bnd + */ +public class ClassParser +{ + Map typeRefCache = new HashMap(); + Map descriptorCache = new HashMap(); + Map packageCache = new HashMap(); + + // MUST BE BEFORE PRIMITIVES, THEY USE THE DEFAULT PACKAGE!! + final static PackageRef DEFAULT_PACKAGE = new PackageRef(); + final static PackageRef PRIMITIVE_PACKAGE = new PackageRef(); + + final static TypeRef VOID = new ConcreteRef("V", "void", PRIMITIVE_PACKAGE); + final static TypeRef BOOLEAN = new ConcreteRef("Z", "boolean", PRIMITIVE_PACKAGE); + final static TypeRef BYTE = new ConcreteRef("B", "byte", PRIMITIVE_PACKAGE); + final static TypeRef CHAR = new ConcreteRef("C", "char", PRIMITIVE_PACKAGE); + final static TypeRef SHORT = new ConcreteRef("S", "short", PRIMITIVE_PACKAGE); + final static TypeRef INTEGER = new ConcreteRef("I", "int", PRIMITIVE_PACKAGE); + final static TypeRef LONG = new ConcreteRef("J", "long", PRIMITIVE_PACKAGE); + final static TypeRef DOUBLE = new ConcreteRef("D", "double", PRIMITIVE_PACKAGE); + final static TypeRef FLOAT = new ConcreteRef("F", "float", PRIMITIVE_PACKAGE); + + + { + packageCache.put("", DEFAULT_PACKAGE); + } + + private interface TypeRef extends Comparable + { + String getBinary(); + + String getFQN(); + + String getPath(); + + boolean isPrimitive(); + + TypeRef getClassRef(); + + PackageRef getPackageRef(); + + String getShortName(); + + String getSourcePath(); + + String getDottedOnly(); + + } + + private static class PackageRef implements Comparable + { + final String binaryName; + final String fqn; + + PackageRef(String binaryName) + { + this.binaryName = fqnToBinary(binaryName); + this.fqn = binaryToFQN(binaryName); + } + + PackageRef() + { + this.binaryName = ""; + this.fqn = "."; + } + + public String getFQN() + { + return fqn; + } + + @Override + public String toString() + { + return fqn; + } + + boolean isPrimitivePackage() + { + return this == PRIMITIVE_PACKAGE; + } + + @Override + public int compareTo(PackageRef other) + { + return fqn.compareTo(other.fqn); + } + + @Override + public boolean equals(Object o) + { + assert o instanceof PackageRef; + return o == this; + } + + @Override + public int hashCode() + { + return super.hashCode(); + } + + + } + + // We "intern" the + private static class ConcreteRef implements TypeRef + { + final String binaryName; + final String fqn; + final boolean primitive; + final PackageRef packageRef; + + ConcreteRef(PackageRef packageRef, String binaryName) + { + this.binaryName = binaryName; + this.fqn = binaryToFQN(binaryName); + this.primitive = false; + this.packageRef = packageRef; + } + + ConcreteRef(String binaryName, String fqn, PackageRef pref) + { + this.binaryName = binaryName; + this.fqn = fqn; + this.primitive = true; + this.packageRef = pref; + } + + @Override + public String getBinary() + { + return binaryName; + } + + @Override + public String getPath() + { + return binaryName + ".class"; + } + + @Override + public String getSourcePath() + { + return binaryName + ".java"; + } + + @Override + public String getFQN() + { + return fqn; + } + + @Override + public String getDottedOnly() + { + return fqn.replace('$', '.'); + } + + @Override + public boolean isPrimitive() + { + return primitive; + } + + @Override + public TypeRef getClassRef() + { + return this; + } + + @Override + public PackageRef getPackageRef() + { + return packageRef; + } + + @Override + public String getShortName() + { + int n = binaryName.lastIndexOf('/'); + return binaryName.substring(n + 1); + } + + @Override + public String toString() + { + return fqn; + } + + @Override + public boolean equals(Object other) + { + assert other instanceof TypeRef; + return this == other; + } + + @Override + public int compareTo(TypeRef other) + { + if (this == other) + { + return 0; + } + return fqn.compareTo(other.getFQN()); + } + + @Override + public int hashCode() + { + return super.hashCode(); + } + + } + + private static class ArrayRef implements TypeRef + { + final TypeRef component; + + ArrayRef(TypeRef component) + { + this.component = component; + } + + @Override + public String getBinary() + { + return "[" + component.getBinary(); + } + + @Override + public String getFQN() + { + return component.getFQN() + "[]"; + } + + @Override + public String getPath() + { + return component.getPath(); + } + + @Override + public String getSourcePath() + { + return component.getSourcePath(); + } + + @Override + public boolean isPrimitive() + { + return false; + } + + @Override + public TypeRef getClassRef() + { + return component.getClassRef(); + } + + @Override + public boolean equals(Object other) + { + if (other == null || other.getClass() != getClass()) + { + return false; + } + + return component.equals(((ArrayRef) other).component); + } + + @Override + public PackageRef getPackageRef() + { + return component.getPackageRef(); + } + + @Override + public String getShortName() + { + return component.getShortName() + "[]"; + } + + @Override + public String toString() + { + return component.toString() + "[]"; + } + + @Override + public String getDottedOnly() + { + return component.getDottedOnly(); + } + + @Override + public int compareTo(TypeRef other) + { + if (this == other) + { + return 0; + } + + return getFQN().compareTo(other.getFQN()); + } + + @Override + public int hashCode() + { + return super.hashCode(); + } + } + + private TypeRef getTypeRef(String binaryClassName) + { + TypeRef ref = typeRefCache.get(binaryClassName); + if (ref != null) + { + return ref; + } + + if (binaryClassName.startsWith("[")) + { + ref = getTypeRef(binaryClassName.substring(1)); + ref = new ArrayRef(ref); + } + else + { + if (binaryClassName.length() == 1) + { + switch (binaryClassName.charAt(0)) + { + case 'V': + return VOID; + case 'B': + return BYTE; + case 'C': + return CHAR; + case 'I': + return INTEGER; + case 'S': + return SHORT; + case 'D': + return DOUBLE; + case 'F': + return FLOAT; + case 'J': + return LONG; + case 'Z': + return BOOLEAN; + } + // falls trough for other 1 letter class names + } + if (binaryClassName.startsWith("L") && binaryClassName.endsWith(";")) + { + binaryClassName = binaryClassName.substring(1, binaryClassName.length() - 1); + } + ref = typeRefCache.get(binaryClassName); + if (ref != null) + { + return ref; + } + + PackageRef pref; + int n = binaryClassName.lastIndexOf('/'); + if (n < 0) + { + pref = DEFAULT_PACKAGE; + } + else + { + pref = getPackageRef(binaryClassName.substring(0, n)); + } + + ref = new ConcreteRef(pref, binaryClassName); + } + + typeRefCache.put(binaryClassName, ref); + return ref; + } + + private PackageRef getPackageRef(String binaryPackName) + { + if (binaryPackName.indexOf('.') >= 0) + { + binaryPackName = binaryPackName.replace('.', '/'); + } + PackageRef ref = packageCache.get(binaryPackName); + if (ref != null) + { + return ref; + } + + ref = new PackageRef(binaryPackName); + packageCache.put(binaryPackName, ref); + return ref; + } + + private Descriptor getDescriptor(String descriptor) + { + Descriptor d = descriptorCache.get(descriptor); + if (d != null) + { + return d; + } + d = new Descriptor(descriptor); + descriptorCache.put(descriptor, d); + return d; + } + + private class Descriptor + { + final TypeRef type; + final TypeRef[] prototype; + final String descriptor; + + Descriptor(String descriptor) + { + this.descriptor = descriptor; + int index = 0; + List types = new ArrayList(); + if (descriptor.charAt(index) == '(') + { + index++; + while (descriptor.charAt(index) != ')') + { + index = parse(types, descriptor, index); + } + index++; // skip ) + prototype = types.toArray(new TypeRef[0]); + types.clear(); + } + else + { + prototype = null; + } + + index = parse(types, descriptor, index); + type = types.get(0); + } + + int parse(List types, String descriptor, int index) + { + char c; + StringBuilder sb = new StringBuilder(); + while ((c = descriptor.charAt(index++)) == '[') + { + sb.append('['); + } + + switch (c) + { + case 'L': + while ((c = descriptor.charAt(index++)) != ';') + { + // TODO + sb.append(c); + } + break; + + case 'V': + case 'B': + case 'C': + case 'I': + case 'S': + case 'D': + case 'F': + case 'J': + case 'Z': + sb.append(c); + break; + + default: + throw new IllegalArgumentException( + "Invalid type in descriptor: " + c + " from " + descriptor + "[" + index + "]"); + } + types.add(getTypeRef(sb.toString())); + return index; + } + + @Override + public boolean equals(Object other) + { + if (other == null || other.getClass() != getClass()) + { + return false; + } + + return Arrays.equals(prototype, ((Descriptor) other).prototype) && type == ((Descriptor) other).type; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = prime + type.hashCode(); + result = prime * result + ((prototype == null) ? 0 : Arrays.hashCode(prototype)); + return result; + } + + @Override + public String toString() + { + return descriptor; + } + } + + private static String binaryToFQN(String binary) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0, l = binary.length(); i < l; i++) + { + char c = binary.charAt(i); + + if (c == '/') + { + sb.append('.'); + } + else + { + sb.append(c); + } + } + String result = sb.toString(); + assert result.length() > 0; + return result; + } + + private static String fqnToBinary(String binary) + { + return binary.replace('.', '/'); + } + + + TypeRef getTypeRefFromFQN(String fqn) + { + if (fqn.equals("boolean")) + { + return BOOLEAN; + } + + if (fqn.equals("byte")) + { + return BOOLEAN; + } + + if (fqn.equals("char")) + { + return CHAR; + } + + if (fqn.equals("short")) + { + return SHORT; + } + + if (fqn.equals("int")) + { + return INTEGER; + } + + if (fqn.equals("long")) + { + return LONG; + } + + if (fqn.equals("float")) + { + return FLOAT; + } + + if (fqn.equals("double")) + { + return DOUBLE; + } + + return getTypeRef(fqnToBinary(fqn)); + } + + + public Set parseClassFileUses(String path, InputStream in) throws Exception + { + DataInputStream din = new DataInputStream(in); + try + { + return new Clazz(this, path).parseClassFileData(din); + } + finally + { + din.close(); + } + } + + private static class Clazz + { + + class ClassConstant + { + int cname; + boolean referred; + + ClassConstant(int class_index) + { + this.cname = class_index; + } + + public String getName() + { + return (String) pool[cname]; + } + + @Override + public String toString() + { + return "ClassConstant[" + getName() + "]"; + } + } + + + enum CONSTANT + { + Zero(0), + Utf8, + Two, + Integer(4), + Float(4), + Long(8), + Double(8), + Class(2), + String(2), + Fieldref(4), + Methodref(4), + InterfaceMethodref(4), + NameAndType(4), + Thirteen, + Fourteen, + MethodHandle(3), + MethodType(2), + Seventeen, + InvokeDynamic(4), + Module(2), + Package(2); + private final int skip; + + CONSTANT(int skip) + { + this.skip = skip; + } + + CONSTANT() + { + this.skip = -1; + } + + int skip() + { + return skip; + } + } + + final static int ACC_MODULE = 0x8000; + + static protected class Assoc + { + Assoc(CONSTANT tag, int a, int b) + { + this.tag = tag; + this.a = a; + this.b = b; + } + + CONSTANT tag; + int a; + int b; + + @Override + public String toString() + { + return "Assoc[" + tag + ", " + a + "," + b + "]"; + } + } + + public abstract class Def + { + + final int access; + + public Def(int access) + { + this.access = access; + } + + } + + public class FieldDef extends Def + { + final String name; + final Descriptor descriptor; + String signature; + Object constant; + + + public FieldDef(int access, String name, String descriptor) + { + super(access); + this.name = name; + this.descriptor = Clazz.this.classParser.getDescriptor(descriptor); + } + + + @Override + public String toString() + { + return name; + } + } + + public class MethodDef extends FieldDef + { + public MethodDef(int access, String method, String descriptor) + { + super(access, method, descriptor); + } + } + + boolean hasDefaultConstructor; + + int depth = 0; + + TypeRef className; + Object pool[]; + int intPool[]; + Set imports = new HashSet(); + String path; + int minor = 0; + int major = 0; + int accessx = 0; + int forName = 0; + int class$ = 0; + TypeRef[] interfaces; + TypeRef zuper; + FieldDef last = null; + final ClassParser classParser; + String classSignature; + + private boolean detectLdc; + + public Clazz(ClassParser classParser, String path) + { + this.path = path; + this.classParser = classParser; + } + + Set parseClassFileData(DataInput in) throws Exception + { + + ++depth; + + boolean crawl = false; // Crawl the byte code if we have a + // collector + int magic = in.readInt(); + if (magic != 0xCAFEBABE) + { + throw new IOException("Not a valid class file (no CAFEBABE header)"); + } + + minor = in.readUnsignedShort(); // minor version + major = in.readUnsignedShort(); // major version + int count = in.readUnsignedShort(); + pool = new Object[count]; + intPool = new int[count]; + + CONSTANT[] tags = CONSTANT.values(); + process: + for (int poolIndex = 1; poolIndex < count; poolIndex++) + { + int tagValue = in.readUnsignedByte(); + if (tagValue >= tags.length) + { + throw new IOException("Unrecognized constant pool tag value " + tagValue); + } + CONSTANT tag = tags[tagValue]; + switch (tag) + { + case Zero: + break process; + case Utf8: + constantUtf8(in, poolIndex); + break; + case Integer: + constantInteger(in, poolIndex); + break; + case Float: + constantFloat(in, poolIndex); + break; + // For some insane optimization reason, + // the long and double entries take two slots in the + // constant pool. See 4.4.5 + case Long: + constantLong(in, poolIndex); + poolIndex++; + break; + case Double: + constantDouble(in, poolIndex); + poolIndex++; + break; + case Class: + constantClass(in, poolIndex); + break; + case String: + constantString(in, poolIndex); + break; + case Fieldref: + case Methodref: + case InterfaceMethodref: + ref(in, poolIndex); + break; + case NameAndType: + nameAndType(in, poolIndex, tag); + break; + case MethodHandle: + methodHandle(in, poolIndex, tag); + break; + case MethodType: + methodType(in, poolIndex, tag); + break; + case InvokeDynamic: + invokeDynamic(in, poolIndex, tag); + break; + default: + int skip = tag.skip(); + if (skip == -1) + { + throw new IOException("Invalid tag " + tag); + } + in.skipBytes(skip); + break; + } + } + + pool(pool, intPool); + + // All name& type and class constant records contain classParser we must + // treat + // as references, though not API + for (Object o : pool) + { + if (o == null) + { + continue; + } + + if (o instanceof Assoc) + { + Assoc assoc = (Assoc) o; + switch (assoc.tag) + { + case Fieldref: + case Methodref: + case InterfaceMethodref: + classConstRef(assoc.a); + break; + + case NameAndType: + case MethodType: + referTo(assoc.b, 0); // Descriptor + break; + default: + break; + } + } + } + + // + // There is a bug in J8 compiler that leaves an + // orphan class constant. So when we have a CC that + // is not referenced by fieldrefs, method refs, or other + // refs then we need to crawl the byte code. + // + for (Object o : pool) + { + if (o instanceof ClassConstant) + { + ClassConstant cc = (ClassConstant) o; + if (cc.referred == false) + { + detectLdc = true; + } + } + } + + /* + * Parse after the constant pool, code thanks to Hans Christian + * Falkenberg + */ + + accessx = in.readUnsignedShort(); // access + + int this_class = in.readUnsignedShort(); + className = classParser.getTypeRef((String) pool[intPool[this_class]]); + if (!isModule()) + { + referTo(className, Modifier.PUBLIC); + } + + int super_class = in.readUnsignedShort(); + String superName = (String) pool[intPool[super_class]]; + if (superName != null) + { + zuper = classParser.getTypeRef(superName); + } + + if (zuper != null) + { + referTo(zuper, accessx); + } + + int interfacesCount = in.readUnsignedShort(); + if (interfacesCount > 0) + { + interfaces = new TypeRef[interfacesCount]; + for (int i = 0; i < interfacesCount; i++) + { + interfaces[i] = classParser.getTypeRef((String) pool[intPool[in.readUnsignedShort()]]); + referTo(interfaces[i], accessx); + } + } + + int fieldsCount = in.readUnsignedShort(); + for (int i = 0; i < fieldsCount; i++) + { + int access_flags = in.readUnsignedShort(); // skip access flags + int name_index = in.readUnsignedShort(); + int descriptor_index = in.readUnsignedShort(); + + // Java prior to 1.5 used a weird + // static variable to hold the com.X.class + // result construct. If it did not find it + // it would create a variable class$com$X + // that would be used to hold the class + // object gotten with Class.forName ... + // Stupidly, they did not actively use the + // class name for the field type, so bnd + // would not see a reference. We detect + // this case and add an artificial descriptor + String name = pool[name_index].toString(); // name_index + if (name.startsWith("class$") || name.startsWith("$class$")) + { + crawl = true; + } + + referTo(descriptor_index, access_flags); + doAttributes(in, ElementType.FIELD, false, access_flags); + } + + // + // Check if we have to crawl the code to find + // the ldc(_w) invokestatic Class.forName + // if so, calculate the method ref index so we + // can do this efficiently + // + if (crawl) + { + forName = findMethodReference("java/lang/Class", "forName", "(Ljava/lang/String;)Ljava/lang/Class;"); + class$ = findMethodReference(className.getBinary(), "class$", "(Ljava/lang/String;)Ljava/lang/Class;"); + } + else if (major == 48) + { + forName = findMethodReference("java/lang/Class", "forName", "(Ljava/lang/String;)Ljava/lang/Class;"); + if (forName > 0) + { + crawl = true; + class$ = findMethodReference(className.getBinary(), "class$", + "(Ljava/lang/String;)Ljava/lang/Class;"); + } + } + + // There are some serious changes in the + // class file format. So we do not do any crawling + // it has also become less important + // however, jDK8 has a bug that leaves an orphan ClassConstnat + // so if we have those, we need to also crawl the byte codes. + // if (major >= JAVA.OpenJDK7.major) + + crawl |= detectLdc; + + // + // Handle the methods + // + int methodCount = in.readUnsignedShort(); + for (int i = 0; i < methodCount; i++) + { + int access_flags = in.readUnsignedShort(); + int name_index = in.readUnsignedShort(); + int descriptor_index = in.readUnsignedShort(); + String name = pool[name_index].toString(); + String descriptor = pool[descriptor_index].toString(); + MethodDef mdef = null; + referTo(descriptor_index, access_flags); + + if ("".equals(name)) + { + if (Modifier.isPublic(access_flags) && "()V".equals(descriptor)) + { + hasDefaultConstructor = true; + } + doAttributes(in, ElementType.CONSTRUCTOR, crawl, access_flags); + } + else + { + doAttributes(in, ElementType.METHOD, crawl, access_flags); + } + } + last = null; + + doAttributes(in, ElementType.TYPE, false, accessx); + + // + // Parse all the classParser we found + // + reset(); + return imports; + } + + private void constantFloat(DataInput in, int poolIndex) throws IOException + { + in.skipBytes(4); + } + + private void constantInteger(DataInput in, int poolIndex) throws IOException + { + intPool[poolIndex] = in.readInt(); + pool[poolIndex] = intPool[poolIndex]; + } + + private void pool(@SuppressWarnings("unused") Object[] pool, @SuppressWarnings("unused") int[] intPool) + { + } + + private void nameAndType(DataInput in, int poolIndex, CONSTANT tag) throws IOException + { + int name_index = in.readUnsignedShort(); + int descriptor_index = in.readUnsignedShort(); + pool[poolIndex] = new Assoc(tag, name_index, descriptor_index); + } + + private void methodType(DataInput in, int poolIndex, CONSTANT tag) throws IOException + { + int descriptor_index = in.readUnsignedShort(); + pool[poolIndex] = new Assoc(tag, 0, descriptor_index); + } + + private void methodHandle(DataInput in, int poolIndex, CONSTANT tag) throws IOException + { + int reference_kind = in.readUnsignedByte(); + int reference_index = in.readUnsignedShort(); + pool[poolIndex] = new Assoc(tag, reference_kind, reference_index); + } + + private void invokeDynamic(DataInput in, int poolIndex, CONSTANT tag) throws IOException + { + int bootstrap_method_attr_index = in.readUnsignedShort(); + int name_and_type_index = in.readUnsignedShort(); + pool[poolIndex] = new Assoc(tag, bootstrap_method_attr_index, name_and_type_index); + } + + private void ref(DataInput in, int poolIndex) throws IOException + { + int class_index = in.readUnsignedShort(); + int name_and_type_index = in.readUnsignedShort(); + pool[poolIndex] = new Assoc(Clazz.CONSTANT.Methodref, class_index, name_and_type_index); + } + + private void constantString(DataInput in, int poolIndex) throws IOException + { + int string_index = in.readUnsignedShort(); + intPool[poolIndex] = string_index; + } + + private void constantClass(DataInput in, int poolIndex) throws IOException + { + int class_index = in.readUnsignedShort(); + intPool[poolIndex] = class_index; + ClassConstant c = new ClassConstant(class_index); + pool[poolIndex] = c; + } + + private void constantDouble(DataInput in, int poolIndex) throws IOException + { + in.skipBytes(8); + } + + private void constantLong(DataInput in, int poolIndex) throws IOException + { + in.skipBytes(8); + } + + private void constantUtf8(DataInput in, int poolIndex) throws IOException + { + // CONSTANT_Utf8 + + String name = in.readUTF(); + pool[poolIndex] = name; + } + + private int findMethodReference(String clazz, String methodname, String descriptor) + { + for (int i = 1; i < pool.length; i++) + { + if (pool[i] instanceof Assoc) + { + Assoc methodref = (Assoc) pool[i]; + if (methodref.tag == CONSTANT.Methodref) + { + // Method ref + int class_index = methodref.a; + int class_name_index = intPool[class_index]; + if (clazz.equals(pool[class_name_index])) + { + int name_and_type_index = methodref.b; + Assoc name_and_type = (Assoc) pool[name_and_type_index]; + if (name_and_type.tag == CONSTANT.NameAndType) + { + // Name and Type + int name_index = name_and_type.a; + int type_index = name_and_type.b; + if (methodname.equals(pool[name_index])) + { + if (descriptor.equals(pool[type_index])) + { + return i; + } + } + } + } + } + } + } + return -1; + } + + private void doAttributes(DataInput in, ElementType member, boolean crawl, int access_flags) throws Exception + { + int attributesCount = in.readUnsignedShort(); + for (int j = 0; j < attributesCount; j++) + { + // skip name CONSTANT_Utf8 pointer + doAttribute(in, member, crawl, access_flags); + } + } + + private static long getUnsignedInt(int x) + { + return x & 0x00000000ffffffffL; + } + + private static int getUnsingedByte(byte b) + { + return b & 0xFF; + } + + private static int getUnsingedShort(short s) + { + return s & 0xFFFF; + } + + private void doAttribute(DataInput in, ElementType member, boolean crawl, int access_flags) throws Exception + { + final int attribute_name_index = in.readUnsignedShort(); + final String attributeName = (String) pool[attribute_name_index]; + final long attribute_length = getUnsignedInt(in.readInt()); + if (attributeName.equals("Deprecated")) + { + } + else if (attributeName.equals("RuntimeVisibleAnnotations")) + { + doAnnotations(in, member, RetentionPolicy.RUNTIME, access_flags); + + } + else if (attributeName.equals("RuntimeInvisibleAnnotations")) + { + doAnnotations(in, member, RetentionPolicy.CLASS, access_flags); + + } + else if (attributeName.equals("RuntimeVisibleParameterAnnotations")) + { + doParameterAnnotations(in, member, RetentionPolicy.RUNTIME, access_flags); + + } + else if (attributeName.equals("RuntimeInvisibleParameterAnnotations")) + { + doParameterAnnotations(in, member, RetentionPolicy.CLASS, access_flags); + + } + else if (attributeName.equals("RuntimeVisibleTypeAnnotations")) + { + doTypeAnnotations(in, member, RetentionPolicy.RUNTIME, access_flags); + + } + else if (attributeName.equals("RuntimeInvisibleTypeAnnotations")) + { + doTypeAnnotations(in, member, RetentionPolicy.CLASS, access_flags); + + } + else if (attributeName.equals("InnerClasses")) + { + doInnerClasses(in); + + } + else if (attributeName.equals("EnclosingMethod")) + { + doEnclosingMethod(in); + + } + else if (attributeName.equals("SourceFile")) + { + doSourceFile(in); + + } + else if (attributeName.equals("Code")) + { + doCode(in, crawl); + + } + else if (attributeName.equals("Signature")) + { + doSignature(in, member, access_flags); + + } + else if (attributeName.equals("ConstantValue")) + { + doConstantValue(in); + + } + else if (attributeName.equals("AnnotationDefault")) + { + doElementValue(in, member, RetentionPolicy.RUNTIME, access_flags); + } + else if (attributeName.equals("Exceptions")) + { + doExceptions(in, access_flags); + + } + else if (attributeName.equals("BootstrapMethods")) + { + doBootstrapMethods(in); + + } + else if (attributeName.equals("StackMapTable")) + { + doStackMapTable(in); + + } + else + { + if (attribute_length > 0x7FFFFFFF) + { + throw new IllegalArgumentException("Attribute > 2Gb"); + } + in.skipBytes((int) attribute_length); + + } + } + + private void doEnclosingMethod(DataInput in) throws IOException + { + int cIndex = in.readUnsignedShort(); + int mIndex = in.readUnsignedShort(); + classConstRef(cIndex); + } + + private void doInnerClasses(DataInput in) throws Exception + { + int number_of_classes = in.readUnsignedShort(); + for (int i = 0; i < number_of_classes; i++) + { + int inner_class_info_index = in.readUnsignedShort(); + int outer_class_info_index = in.readUnsignedShort(); + int inner_name_index = in.readUnsignedShort(); + int inner_class_access_flags = in.readUnsignedShort(); + } + } + + void doSignature(DataInput in, ElementType member, int access_flags) throws IOException + { + int signature_index = in.readUnsignedShort(); + String signature = (String) pool[signature_index]; + try + { + + parseDescriptor(signature, access_flags); + if (last != null) + { + last.signature = signature; + } + + if (member == ElementType.TYPE) + { + classSignature = signature; + } + + } + catch (Exception e) + { + throw new RuntimeException("Signature failed for " + signature, e); + } + } + + void doConstantValue(DataInput in) throws IOException + { + int constantValue_index = in.readUnsignedShort(); + } + + void doExceptions(DataInput in, int access_flags) throws IOException + { + int exception_count = in.readUnsignedShort(); + for (int i = 0; i < exception_count; i++) + { + int index = in.readUnsignedShort(); + ClassConstant cc = (ClassConstant) pool[index]; + TypeRef clazz = classParser.getTypeRef(cc.getName()); + referTo(clazz, access_flags); + } + } + + private void doCode(DataInput in, boolean crawl) throws Exception + { + /* int max_stack = */ + in.readUnsignedShort(); + /* int max_locals = */ + in.readUnsignedShort(); + int code_length = in.readInt(); + byte code[] = new byte[code_length]; + in.readFully(code, 0, code_length); + if (crawl) + { + crawl(code); + } + int exception_table_length = in.readUnsignedShort(); + for (int i = 0; i < exception_table_length; i++) + { + int start_pc = in.readUnsignedShort(); + int end_pc = in.readUnsignedShort(); + int handler_pc = in.readUnsignedShort(); + int catch_type = in.readUnsignedShort(); + classConstRef(catch_type); + } + doAttributes(in, ElementType.METHOD, false, 0); + } + + private void crawl(byte[] code) + { + ByteBuffer bb = ByteBuffer.wrap(code); + int lastReference = -1; + + while (bb.remaining() > 0) + { + int instruction = getUnsingedByte(bb.get()); + switch (instruction) + { + case ldc: + lastReference = getUnsingedByte(bb.get()); + classConstRef(lastReference); + break; + + case ldc_w: + lastReference = getUnsingedShort(bb.getShort()); + classConstRef(lastReference); + break; + + case anewarray: + case checkcast: + case instanceof_: + case new_: + { + int cref = getUnsingedShort(bb.getShort()); + classConstRef(cref); + lastReference = -1; + break; + } + + case multianewarray: + { + int cref = getUnsingedShort(bb.getShort()); + classConstRef(cref); + bb.get(); + lastReference = -1; + break; + } + + case invokespecial: + { + int mref = getUnsingedShort(bb.getShort()); + break; + } + + case invokevirtual: + { + int mref = getUnsingedShort(bb.getShort()); + break; + } + + case invokeinterface: + { + int mref = getUnsingedShort(bb.getShort()); + bb.get(); // read past the 'count' operand + bb.get(); // read past the reserved space for future operand + break; + } + + case invokestatic: + { + int methodref = getUnsingedShort(bb.getShort()); + + if ((methodref == forName || methodref == class$) && lastReference != -1 + && pool[intPool[lastReference]] instanceof String) + { + String fqn = (String) pool[intPool[lastReference]]; + if (!fqn.equals("class") && fqn.indexOf('.') > 0) + { + TypeRef clazz = classParser.getTypeRefFromFQN(fqn); + referTo(clazz, 0); + } + lastReference = -1; + } + break; + } + + /* + * 3/5: opcode, indexbyte1, indexbyte2 or iinc, indexbyte1, + * indexbyte2, countbyte1, countbyte2 + */ + case wide: + int opcode = getUnsingedByte(bb.get()); + bb.getShort(); // at least 3 bytes + if (opcode == iinc) + { + bb.getShort(); + } + break; + + case tableswitch: + // Skip to place divisible by 4 + while ((bb.position() & 0x3) != 0) + { + bb.get(); + } + /* int deflt = */ + bb.getInt(); + int low = bb.getInt(); + int high = bb.getInt(); + bb.position(bb.position() + (high - low + 1) * 4); + lastReference = -1; + break; + + case lookupswitch: + // Skip to place divisible by 4 + while ((bb.position() & 0x3) != 0) + { + int n = bb.get(); + assert n == 0; // x + } + /* deflt = */ + int deflt = bb.getInt(); + int npairs = bb.getInt(); + bb.position(bb.position() + npairs * 8); + lastReference = -1; + break; + + default: + lastReference = -1; + bb.position(bb.position() + OFFSETS[instruction]); + } + } + } + + private void doSourceFile(DataInput in) throws IOException + { + int sourcefile_index = in.readUnsignedShort(); + } + + private void doParameterAnnotations(DataInput in, ElementType member, RetentionPolicy policy, int access_flags) + throws Exception + { + int num_parameters = in.readUnsignedByte(); + for (int p = 0; p < num_parameters; p++) + { + doAnnotations(in, member, policy, access_flags); + } + } + + private void doTypeAnnotations(DataInput in, ElementType member, RetentionPolicy policy, int access_flags) + throws Exception + { + int num_annotations = in.readUnsignedShort(); + for (int p = 0; p < num_annotations; p++) + { + + // type_annotation { + // u1 target_type; + // union { + // type_parameter_target; + // supertype_target; + // type_parameter_bound_target; + // empty_target; + // method_formal_parameter_target; + // throws_target; + // localvar_target; + // catch_target; + // offset_target; + // type_argument_target; + // } target_info; + // type_path target_path; + // u2 type_index; + // u2 num_element_value_pairs; + // { u2 element_name_index; + // element_value value; + // } element_value_pairs[num_element_value_pairs]; + // } + + // Table 4.7.20-A. Interpretation of target_type values (Part 1) + + int target_type = in.readUnsignedByte(); + switch (target_type) + { + case 0x00: // type parameter declaration of generic class or + // interface + case 0x01: // type parameter declaration of generic method or + // constructor + // + // type_parameter_target { + // u1 type_parameter_index; + // } + in.skipBytes(1); + break; + + case 0x10: // type in extends clause of class or interface + // declaration (including the direct superclass of + // an anonymous class declaration), or in implements + // clause of interface declaration + // supertype_target { + // u2 supertype_index; + // } + + in.skipBytes(2); + break; + + case 0x11: // type in bound of type parameter declaration of + // generic class or interface + case 0x12: // type in bound of type parameter declaration of + // generic method or constructor + // type_parameter_bound_target { + // u1 type_parameter_index; + // u1 bound_index; + // } + in.skipBytes(2); + break; + + case 0x13: // type in field declaration + case 0x14: // return type of method, or type of newly + // constructed object + case 0x15: // receiver type of method or constructor + break; + + case 0x16: // type in formal parameter declaration of method, + // constructor, or lambda expression + // formal_parameter_target { + // u1 formal_parameter_index; + // } + in.skipBytes(1); + break; + + case 0x17: // type in throws clause of method or constructor + // throws_target { + // u2 throws_type_index; + // } + in.skipBytes(2); + break; + + case 0x40: // type in local variable declaration + case 0x41: // type in resource variable declaration + // localvar_target { + // u2 table_length; + // { u2 start_pc; + // u2 length; + // u2 index; + // } table[table_length]; + // } + int table_length = in.readUnsignedShort(); + in.skipBytes(table_length * 6); + break; + + case 0x42: // type in exception parameter declaration + // catch_target { + // u2 exception_table_index; + // } + in.skipBytes(2); + break; + + case 0x43: // type in instanceof expression + case 0x44: // type in new expression + case 0x45: // type in method reference expression using ::new + case 0x46: // type in method reference expression using + // ::Identifier + // offset_target { + // u2 offset; + // } + in.skipBytes(2); + break; + + case 0x47: // type in cast expression + case 0x48: // type argument for generic constructor in new + // expression or explicit constructor invocation + // statement + + case 0x49: // type argument for generic method in method + // invocation expression + case 0x4A: // type argument for generic constructor in method + // reference expression using ::new + case 0x4B: // type argument for generic method in method + // reference expression using ::Identifier + // type_argument_target { + // u2 offset; + // u1 type_argument_index; + // } + in.skipBytes(3); + break; + + } + + // The value of the target_path item denotes precisely which part of + // the type indicated by target_info is annotated. The format of the + // type_path structure is specified in §4.7.20.2. + // + // type_path { + // u1 path_length; + // { u1 type_path_kind; + // u1 type_argument_index; + // } path[path_length]; + // } + + int path_length = in.readUnsignedByte(); + in.skipBytes(path_length * 2); + + // + // Rest is identical to the normal annotations + doAnnotation(in, member, policy, access_flags); + } + } + + private void doAnnotations(DataInput in, ElementType member, RetentionPolicy policy, int access_flags) + throws Exception + { + int num_annotations = in.readUnsignedShort(); // # of annotations + for (int a = 0; a < num_annotations; a++) + { + doAnnotation(in, member, policy, access_flags); + } + } + + // annotation { + // u2 type_index; + // u2 num_element_value_pairs; { + // u2 element_name_index; + // element_value value; + // } + // element_value_pairs[num_element_value_pairs]; + // } + + private void doAnnotation(DataInput in, ElementType member, RetentionPolicy policy, int access_flags) throws IOException + { + int type_index = in.readUnsignedShort(); + + String typeName = (String) pool[type_index]; + if (typeName != null) + { + if (policy == RetentionPolicy.RUNTIME) + { + referTo(type_index, 0); + } + } + int num_element_value_pairs = in.readUnsignedShort(); + + for (int v = 0; v < num_element_value_pairs; v++) + { + in.readUnsignedShort(); + doElementValue(in, member, policy, access_flags); + } + } + + private Object doElementValue(DataInput in, ElementType member, RetentionPolicy policy, int access_flags) throws IOException + { + char tag = (char) in.readUnsignedByte(); + switch (tag) + { + case 'B': // Byte + case 'C': // Character + case 'I': // Integer + case 'S': // Short + int const_value_index = in.readUnsignedShort(); + return intPool[const_value_index]; + + case 'D': // Double + case 'F': // Float + case 's': // String + case 'J': // Long + const_value_index = in.readUnsignedShort(); + return pool[const_value_index]; + + case 'Z': // Boolean + const_value_index = in.readUnsignedShort(); + return pool[const_value_index] == null || pool[const_value_index].equals(0) ? false : true; + + case 'e': // enum constant + int type_name_index = in.readUnsignedShort(); + if (policy == RetentionPolicy.RUNTIME) + { + referTo(type_name_index, 0); + } + int const_name_index = in.readUnsignedShort(); + return pool[const_name_index]; + + case 'c': // Class + int class_info_index = in.readUnsignedShort(); + TypeRef name = classParser.getTypeRef((String) pool[class_info_index]); + if (policy == RetentionPolicy.RUNTIME) + { + referTo(class_info_index, 0); + } + return name; + + case '@': // Annotation type + doAnnotation(in, member, policy, access_flags); + + case '[': // Array + int num_values = in.readUnsignedShort(); + Object[] result = new Object[num_values]; + for (int i = 0; i < num_values; i++) + { + result[i] = doElementValue(in, member, policy, access_flags); + } + return result; + + default: + throw new IllegalArgumentException("Invalid value for Annotation ElementValue tag " + tag); + } + } + + /* + * We don't currently process BootstrapMethods. We walk the data structure + * to consume the attribute. + */ + private void doBootstrapMethods(DataInput in) throws IOException + { + final int num_bootstrap_methods = in.readUnsignedShort(); + for (int v = 0; v < num_bootstrap_methods; v++) + { + final int bootstrap_method_ref = in.readUnsignedShort(); + final int num_bootstrap_arguments = in.readUnsignedShort(); + for (int a = 0; a < num_bootstrap_arguments; a++) + { + final int bootstrap_argument = in.readUnsignedShort(); + } + } + } + + /* + * The verifier can require access to types only referenced in StackMapTable + * attributes. + */ + private void doStackMapTable(DataInput in) throws IOException + { + final int number_of_entries = in.readUnsignedShort(); + for (int v = 0; v < number_of_entries; v++) + { + final int frame_type = in.readUnsignedByte(); + if (frame_type <= 63) + { // same_frame + // nothing else to do + } + else if (frame_type <= 127) + { // same_locals_1_stack_item_frame + verification_type_info(in); + } + else if (frame_type <= 246) + { // RESERVED + // nothing else to do + } + else if (frame_type <= 247) + { // same_locals_1_stack_item_frame_extended + final int offset_delta = in.readUnsignedShort(); + verification_type_info(in); + } + else if (frame_type <= 250) + { // chop_frame + final int offset_delta = in.readUnsignedShort(); + } + else if (frame_type <= 251) + { // same_frame_extended + final int offset_delta = in.readUnsignedShort(); + } + else if (frame_type <= 254) + { // append_frame + final int offset_delta = in.readUnsignedShort(); + final int number_of_locals = frame_type - 251; + for (int n = 0; n < number_of_locals; n++) + { + verification_type_info(in); + } + } + else if (frame_type <= 255) + { // full_frame + final int offset_delta = in.readUnsignedShort(); + final int number_of_locals = in.readUnsignedShort(); + for (int n = 0; n < number_of_locals; n++) + { + verification_type_info(in); + } + final int number_of_stack_items = in.readUnsignedShort(); + for (int n = 0; n < number_of_stack_items; n++) + { + verification_type_info(in); + } + } + } + } + + private void verification_type_info(DataInput in) throws IOException + { + final int tag = in.readUnsignedByte(); + switch (tag) + { + case 7:// Object_variable_info + final int cpool_index = in.readUnsignedShort(); + classConstRef(cpool_index); + break; + case 8:// ITEM_Uninitialized + final int offset = in.readUnsignedShort(); + break; + } + } + + void referTo(TypeRef typeRef, int modifiers) + { + if (typeRef.isPrimitive()) + { + return; + } + + PackageRef packageRef = typeRef.getPackageRef(); + if (packageRef.isPrimitivePackage()) + { + return; + } + + imports.add(packageRef.getFQN()); + } + + void referTo(int index, int modifiers) + { + String descriptor = (String) pool[index]; + parseDescriptor(descriptor, modifiers); + } + + /* + * This method parses a descriptor and adds the package of the descriptor to + * the referenced packages. The syntax of the descriptor is: + * + *
      +         * descriptor ::= ( '(' reference * ')' )? reference reference ::= 'L'
      +         * classname ( '<' references '>' )? ';' | 'B' | 'Z' | ... | '+' | '-'
      +         * | '['
      +         * 
      + * + * This methods uses heavy recursion to parse the descriptor and a roving + * pointer to limit the creation of string objects. + * + * @param descriptor The to be parsed descriptor + * @param modifiers + */ + + public void parseDescriptor(String descriptor, int modifiers) + { + // Some classParser are weird, they start with a generic + // declaration that contains ':', not sure what they mean ... + int rover = 0; + if (descriptor.charAt(0) == '<') + { + rover = parseFormalTypeParameters(descriptor, rover, modifiers); + } + + if (descriptor.charAt(rover) == '(') + { + rover = parseReferences(descriptor, rover + 1, ')', modifiers); + rover++; + } + parseReferences(descriptor, rover, (char) 0, modifiers); + } + + /* + * Parse a sequence of references. A sequence ends with a given character or + * when the string ends. + * + * @param descriptor The whole descriptor. + * @param rover The index in the descriptor + * @param delimiter The end character or 0 + * @return the last index processed, one character after the delimeter + */ + int parseReferences(String descriptor, int rover, char delimiter, int modifiers) + { + int r = rover; + while (r < descriptor.length() && descriptor.charAt(r) != delimiter) + { + r = parseReference(descriptor, r, modifiers); + } + return r; + } + + /* + * Parse a single reference. This can be a single character or an object + * reference when it starts with 'L'. + * + * @param descriptor The descriptor + * @param rover The place to start + * @return The return index after the reference + */ + int parseReference(String descriptor, int rover, int modifiers) + { + int r = rover; + char c = descriptor.charAt(r); + while (c == '[') + { + c = descriptor.charAt(++r); + } + + if (c == '<') + { + r = parseReferences(descriptor, r + 1, '>', modifiers); + } + else if (c == 'T') + { + // Type variable name + r++; + while (descriptor.charAt(r) != ';') + { + r++; + } + } + else if (c == 'L') + { + StringBuilder sb = new StringBuilder(); + r++; + while ((c = descriptor.charAt(r)) != ';') + { + if (c == '<') + { + r = parseReferences(descriptor, r + 1, '>', modifiers); + } + else + { + sb.append(c); + } + r++; + } + TypeRef ref = classParser.getTypeRef(sb.toString()); + + referTo(ref, modifiers); + } + else + { + if ("+-*BCDFIJSZV".indexOf(c) < 0) + { + ;// System.err.println("Should not skip: " + c); + } + } + + // this skips a lot of characters + // [, *, +, -, B, etc. + + return r + 1; + } + + /* + * FormalTypeParameters + * + * @param descriptor + * @param index + */ + private int parseFormalTypeParameters(String descriptor, int index, int modifiers) + { + index++; + while (descriptor.charAt(index) != '>') + { + // Skip IDENTIFIER + index = descriptor.indexOf(':', index) + 1; + if (index == 0) + { + throw new IllegalArgumentException("Expected ClassBound or InterfaceBounds: " + descriptor); + } + + // ClassBound? InterfaceBounds + char c = descriptor.charAt(index); + + if (c != ':') + { + // ClassBound? + index = parseReference(descriptor, index, modifiers); + c = descriptor.charAt(index); + } + + // InterfaceBounds* + while (c == ':') + { + index++; + index = parseReference(descriptor, index, modifiers); + c = descriptor.charAt(index); + } // for each interface + + } // for each formal parameter + return index + 1; // skip > + } + + public Set getReferred() + { + return imports; + } + + /* + * .class construct for different compilers sun 1.1 Detect static variable + * class$com$acme$MyClass 1.2 " 1.3 " 1.4 " 1.5 ldc_w (class) 1.6 " eclipse + * 1.1 class$0, ldc (string), invokestatic Class.forName 1.2 " 1.3 " 1.5 ldc + * (class) 1.6 " 1.5 and later is not an issue, sun pre 1.5 is easy to + * detect the static variable that decodes the class name. For eclipse, the + * class$0 gives away we have a reference encoded in a string. + * compilerversions/compilerversions.jar contains test versions of all + * versions/compilers. + */ + + public void reset() + { + if (--depth == 0) + { + pool = null; + intPool = null; + } + } + + @Override + public String toString() + { + if (className != null) + { + return className.getFQN(); + } + return super.toString(); + } + + + public boolean isModule() + { + return (ACC_MODULE & accessx) != 0; + } + + private void classConstRef(int lastReference) + { + Object o = pool[lastReference]; + if (o == null) + { + return; + } + + if (o instanceof ClassConstant) + { + ClassConstant cc = (ClassConstant) o; + if (cc.referred) + { + return; + } + cc.referred = true; + String name = cc.getName(); + if (name != null) + { + TypeRef tr = classParser.getTypeRef(name); + referTo(tr, 0); + } + } + + } + + + // the stack + final static short bipush = 0x10; // byte ? value + // pushes a + // byte + // onto the stack as an integer + // value + final static short sipush = 0x11; // byte1, byte2 ? + // value + // pushes a + // signed integer (byte1 << 8 + + // byte2) onto the stack + final static short ldc = 0x12; // index ? value + // pushes + // a + // constant #index from a + // constant pool (String, int, + // float or class type) onto the + // stack + final static short ldc_w = 0x13; // indexbyte1, + // indexbyte2 ? + // value pushes a constant + // #index from a constant pool + // (String, int, float or class + // type) onto the stack (wide + // index is constructed as + // indexbyte1 << 8 + indexbyte2) + final static short ldc2_w = 0x14; // indexbyte1, + // indexbyte2 ? + // value pushes a constant + // #index from a constant pool + // (double or long) onto the + // stack (wide index is + // constructed as indexbyte1 << + // 8 + indexbyte2) + final static short iload = 0x15; // index ? value + // loads + // an int + // value from a variable #index + final static short lload = 0x16; // index ? value + // load a + // long + // value from a local variable + // #index + final static short fload = 0x17; // index ? value + // loads a + // float + // value from a local variable + // #index + final static short dload = 0x18; // index ? value + // loads a + // double + // value from a local variable + // #index + final static short aload = 0x19; // index ? objectref + // loads a + // reference onto the stack from + // short from array + final static short istore = 0x36; // index value ? + // store + // int value + // into variable #index + final static short lstore = 0x37; // index value ? + // store a + // long + // value in a local variable + // #index + final static short fstore = 0x38; // index value ? + // stores + // a float + // value into a local variable + // #index + final static short dstore = 0x39; // index value ? + // stores + // a double + // longs + final static short iinc = 0x84; // index, const [No + // change] + // increment local variable + // compares two doubles + final static short ifeq = 0x99; // branchbyte1, + // branchbyte2 + // a long from an array + final static short astore = 0x3a; // index objectref ? + // stores a + // reference into a local + // double to a long + final static short ifne = 0x9a; // branchbyte1, + // branchbyte2 + // value ? if value is not 0, + // branch to instruction at + // branchoffset (signed short + // constructed from unsigned + // bytes branchbyte1 << 8 + + // branchbyte2) + final static short iflt = 0x9b; // branchbyte1, + // branchbyte2 + // value ? if value is less than + // 0, branch to instruction at + // branchoffset (signed short + // constructed from unsigned + // bytes branchbyte1 << 8 + + // branchbyte2) + final static short ifge = 0x9c; // branchbyte1, + // branchbyte2 + // value ? if value is greater + // than or equal to 0, branch to + // instruction at branchoffset + // (signed short constructed + // from unsigned bytes + // branchbyte1 << 8 + + // branchbyte2) + final static short ifgt = 0x9d; // branchbyte1, + // branchbyte2 + // value ? if value is greater + // than 0, branch to instruction + // at branchoffset (signed short + // constructed from unsigned + // bytes branchbyte1 << 8 + + // branchbyte2) + final static short ifle = 0x9e; // branchbyte1, + // branchbyte2 + // value ? if value is less than + // or equal to 0, branch to + // instruction at branchoffset + // (signed short constructed + // from unsigned bytes + // branchbyte1 << 8 + + // branchbyte2) + final static short if_icmpeq = 0x9f; // branchbyte1, + // branchbyte2 + // value1, value2 ? if ints are + // equal, branch to instruction + // at branchoffset (signed short + // constructed from unsigned + // bytes branchbyte1 << 8 + + // branchbyte2) + final static short if_icmpne = 0xa0; // branchbyte1, + // branchbyte2 + // value1, value2 ? if ints are + // not equal, branch to + // instruction at branchoffset + // (signed short constructed + // from unsigned bytes + // branchbyte1 << 8 + + // branchbyte2) + final static short if_icmplt = 0xa1; // branchbyte1, + // branchbyte2 + // value1, value2 ? if value1 is + // less than value2, branch to + // instruction at branchoffset + // (signed short constructed + // from unsigned bytes + // branchbyte1 << 8 + + // branchbyte2) + final static short if_icmpge = 0xa2; // branchbyte1, + // branchbyte2 + // value1, value2 ? if value1 is + // greater than or equal to + // value2, branch to instruction + // at branchoffset (signed short + // constructed from unsigned + // bytes branchbyte1 << 8 + + // branchbyte2) + final static short if_icmpgt = 0xa3; // branchbyte1, + // branchbyte2 + // value1, value2 ? if value1 is + // greater than value2, branch + // to instruction at + // branchoffset (signed short + // constructed from unsigned + // bytes branchbyte1 << 8 + + // branchbyte2) + final static short if_icmple = 0xa4; // branchbyte1, + // branchbyte2 + // value1, value2 ? if value1 is + // less than or equal to value2, + // branch to instruction at + // branchoffset (signed short + // constructed from unsigned + // bytes branchbyte1 << 8 + + // branchbyte2) + final static short if_acmpeq = 0xa5; // branchbyte1, + // branchbyte2 + // value1, value2 ? if + // references are equal, branch + // to instruction at + // branchoffset (signed short + // constructed from unsigned + // bytes branchbyte1 << 8 + + // branchbyte2) + final static short if_acmpne = 0xa6; // branchbyte1, + // branchbyte2 + // value1, value2 ? if + // references are not equal, + // branch to instruction at + // branchoffset (signed short + // constructed from unsigned + // bytes branchbyte1 << 8 + + // branchbyte2) + final static short goto_ = 0xa7; // branchbyte1, + // branchbyte2 [no + // change] goes to another + // instruction at branchoffset + // (signed short constructed + // from unsigned bytes + // branchbyte1 << 8 + + // branchbyte2) + final static short jsr = 0xa8; // branchbyte1, + // branchbyte2 ? + // address jump to subroutine at + // branchoffset (signed short + // constructed from unsigned + // bytes branchbyte1 << 8 + + // branchbyte2) and place the + // return address on the stack + final static short ret = 0xa9; // index [No change] + // continue + // execution from address taken + // from a local variable #index + // (the asymmetry with jsr is + // intentional) + final static short tableswitch = 0xaa; // [0-3 bytes + // padding], + // defaultbyte1, defaultbyte2, + // defaultbyte3, defaultbyte4, + // lowbyte1, lowbyte2, lowbyte3, + // lowbyte4, highbyte1, + // highbyte2, highbyte3, + // highbyte4, jump offsets... + // index ? continue execution + // from an address in the table + // at offset index + final static short lookupswitch = 0xab; // <0-3 bytes + // padding>, + // defaultbyte1, defaultbyte2, + // from + // method + final static short getstatic = 0xb2; // index1, index2 ? + // value gets a + // static field value of a + // class, where the field is + // identified by field reference + // in the constant pool index + // (index1 << 8 + index2) + final static short putstatic = 0xb3; // indexbyte1, + // indexbyte2 value + // ? set static field to value + // in a class, where the field + // is identified by a field + // reference index in constant + // pool (indexbyte1 << 8 + + // indexbyte2) + final static short getfield = 0xb4; // index1, index2 + // objectref ? + // value gets a field value of + // an object objectref, where + // the field is identified by + // field reference in the + // constant pool index (index1 + // << 8 + index2) + final static short putfield = 0xb5; // indexbyte1, + // indexbyte2 + // objectref, value ? set field + // to value in an object + // objectref, where the field is + // identified by a field + // reference index in constant + // pool (indexbyte1 << 8 + + // indexbyte2) + final static short invokevirtual = 0xb6; // indexbyte1, + // indexbyte2 + // objectref, [arg1, arg2, ...] + // ? invoke virtual method on + // object objectref, where the + // method is identified by + // method reference index in + // constant pool (indexbyte1 << + // 8 + indexbyte2) + final static short invokespecial = 0xb7; // indexbyte1, + // indexbyte2 + // objectref, [arg1, arg2, ...] + // ? invoke instance method on + // object objectref, where the + // method is identified by + // method reference index in + // constant pool (indexbyte1 << + // 8 + indexbyte2) + final static short invokestatic = 0xb8; // indexbyte1, + // indexbyte2 [arg1, + // arg2, ...] ? invoke a static + // method, where the method is + // identified by method + // reference index in constant + // pool (indexbyte1 << 8 + + // indexbyte2) + final static short invokeinterface = 0xb9; // indexbyte1, + // indexbyte2, + // count, 0 objectref, [arg1, + // arg2, ...] ? invokes an + // interface method on object + // objectref, where the + // interface method is + // identified by method + // reference index in constant + // pool (indexbyte1 << 8 + + // indexbyte2) + final static short invokedynamic = 0xba; // introduced in J7 + + final static short new_ = 0xbb; // indexbyte1, + // indexbyte2 ? + // objectref creates new object + // of type identified by class + // reference in constant pool + // index (indexbyte1 << 8 + + // indexbyte2) + final static short newarray = 0xbc; // atype count ? + // arrayref + // creates new array with count + // elements of primitive type + // identified by atype + final static short anewarray = 0xbd; // indexbyte1, + // indexbyte2 count + // objectref throws an error or + // exception (notice that the + // rest of the stack is cleared, + // leaving only a reference to + // the Throwable) + final static short checkcast = 0xc0; // indexbyte1, + // indexbyte2 + // objectref ? objectref checks + // whether an objectref is of a + // certain type, the class + // reference of which is in the + // constant pool at index + // (indexbyte1 << 8 + + // indexbyte2) + final static short instanceof_ = 0xc1; // indexbyte1, + // indexbyte2 + // object ("release the lock" - + // end of synchronized() + // section) + final static short wide = 0xc4; // opcode, + // indexbyte1, + // indexbyte2 + final static short multianewarray = 0xc5; // indexbyte1, + // indexbyte2, + // dimensions count1, + // [count2,...] ? arrayref + // create a new array of + // dimensions dimensions with + // elements of type identified + // by class reference in + // constant pool index + // (indexbyte1 << 8 + + // indexbyte2); the sizes of + // each dimension is identified + // by count1, [count2, etc] + final static short ifnull = 0xc6; // branchbyte1, + // branchbyte2 + // value ? if value is null, + // branch to instruction at + // branchoffset (signed short + // constructed from unsigned + // bytes branchbyte1 << 8 + + // branchbyte2) + final static short ifnonnull = 0xc7; // branchbyte1, + // branchbyte2 + // value ? if value is not null, + // branch to instruction at + // branchoffset (signed short + // constructed from unsigned + // bytes branchbyte1 << 8 + + // branchbyte2) + final static short goto_w = 0xc8; // branchbyte1, + // branchbyte2, + // branchbyte3, branchbyte4 [no + // change] goes to another + // instruction at branchoffset + // (signed int constructed from + // unsigned bytes branchbyte1 << + // 24 + branchbyte2 << 16 + + // branchbyte3 << 8 + + // branchbyte4) + final static short jsr_w = 0xc9; // branchbyte1, + // branchbyte2, + + + final static byte OFFSETS[] = new byte[256]; + + static + { + OFFSETS[bipush] = 1; // byte ? value pushes a byte onto the + // stack as an integer value + OFFSETS[sipush] = 2; // byte1, byte2 ? value pushes a signed + // integer (byte1 << 8 + byte2) onto the + // stack + OFFSETS[ldc] = 1; // index ? value pushes a constant + // #index from a constant pool (String, + // int, float or class type) onto the + // stack + OFFSETS[ldc_w] = 2; // indexbyte1, indexbyte2 ? value pushes + // a constant #index from a constant + // pool (String, int, float or class + // type) onto the stack (wide index is + // constructed as indexbyte1 << 8 + + // indexbyte2) + OFFSETS[ldc2_w] = 2; // indexbyte1, indexbyte2 ? value pushes + // a constant #index from a constant + // pool (double or long) onto the stack + // (wide index is constructed as + // indexbyte1 << 8 + indexbyte2) + OFFSETS[iload] = 1; // index ? value loads an int value from + // a variable #index + OFFSETS[lload] = 1; // index ? value load a long value from + // a local variable #index + OFFSETS[fload] = 1; // index ? value loads a float value + // from a local variable #index + OFFSETS[dload] = 1; // index ? value loads a double value + // from a local variable #index + OFFSETS[aload] = 1; // index ? objectref loads a reference + // onto the stack from a local variable + // #index + OFFSETS[istore] = 1; // index value ? store int value into + // variable #index + OFFSETS[lstore] = 1; // index value ? store a long value in a + // local variable #index + OFFSETS[fstore] = 1; // index value ? stores a float value + // into a local variable #index + OFFSETS[dstore] = 1; // index value ? stores a double value + // into a local variable #index + OFFSETS[iinc] = 2; // index, const [No change] increment + // local variable #index by signed byte + // const + OFFSETS[ifeq] = 2; // branchbyte1, branchbyte2 value ? if + // value is 0, branch to instruction at + // branchoffset (signed short + // constructed from unsigned bytes + // branchbyte1 << 8 + branchbyte2) + OFFSETS[astore] = 1; // index objectref ? stores a reference + // into a local variable #index + OFFSETS[ifne] = 2; // branchbyte1, branchbyte2 value ? if + // value is not 0, branch to instruction + // at branchoffset (signed short + // constructed from unsigned bytes + // branchbyte1 << 8 + branchbyte2) + OFFSETS[iflt] = 2; // branchbyte1, branchbyte2 value ? if + // value is less than 0, branch to + // instruction at branchoffset (signed + // short constructed from unsigned bytes + // branchbyte1 << 8 + branchbyte2) + OFFSETS[ifge] = 2; // branchbyte1, branchbyte2 value ? if + // value is greater than or equal to 0, + // branch to instruction at branchoffset + // (signed short constructed from + // unsigned bytes branchbyte1 << 8 + + // branchbyte2) + OFFSETS[ifgt] = 2; // branchbyte1, branchbyte2 value ? if + // value is greater than 0, branch to + // instruction at branchoffset (signed + // short constructed from unsigned bytes + // branchbyte1 << 8 + branchbyte2) + OFFSETS[ifle] = 2; // branchbyte1, branchbyte2 value ? if + // value is less than or equal to 0, + // branch to instruction at branchoffset + // (signed short constructed from + // unsigned bytes branchbyte1 << 8 + + // branchbyte2) + OFFSETS[if_icmpeq] = 2; // branchbyte1, branchbyte2 value1, + // value2 ? if ints are equal, + // branch to instruction at + // branchoffset (signed short + // constructed from unsigned bytes + // branchbyte1 << 8 + branchbyte2) + OFFSETS[if_icmpne] = 2; // branchbyte1, branchbyte2 value1, + // value2 ? if ints are not equal, + // branch to instruction at + // branchoffset (signed short + // constructed from unsigned bytes + // branchbyte1 << 8 + branchbyte2) + OFFSETS[if_icmplt] = 2; // branchbyte1, branchbyte2 value1, + // value2 ? if value1 is less than + // value2, branch to instruction at + // branchoffset (signed short + // constructed from unsigned bytes + // branchbyte1 << 8 + branchbyte2) + OFFSETS[if_icmpge] = 2; // branchbyte1, branchbyte2 value1, + // value2 ? if value1 is greater + // than or equal to value2, branch + // to instruction at branchoffset + // (signed short constructed from + // unsigned bytes branchbyte1 << 8 + + // branchbyte2) + OFFSETS[if_icmpgt] = 2; // branchbyte1, branchbyte2 value1, + // value2 ? if value1 is greater + // than value2, branch to + // instruction at branchoffset + // (signed short constructed from + // unsigned bytes branchbyte1 << 8 + + // branchbyte2) + OFFSETS[if_icmple] = 2; // branchbyte1, branchbyte2 value1, + // value2 ? if value1 is less than + // or equal to value2, branch to + // instruction at branchoffset + // (signed short constructed from + // unsigned bytes branchbyte1 << 8 + + // branchbyte2) + OFFSETS[if_acmpeq] = 2; // branchbyte1, branchbyte2 value1, + // value2 ? if references are equal, + // branch to instruction at + // branchoffset (signed short + // constructed from unsigned bytes + // branchbyte1 << 8 + branchbyte2) + OFFSETS[if_acmpne] = 2; // branchbyte1, branchbyte2 value1, + // value2 ? if references are not + // equal, branch to instruction at + // branchoffset (signed short + // constructed from unsigned bytes + // branchbyte1 << 8 + branchbyte2) + OFFSETS[goto_] = 2; // branchbyte1, branchbyte2 [no change] + // goes to another instruction at + // branchoffset (signed short + // constructed from unsigned bytes + // branchbyte1 << 8 + branchbyte2) + OFFSETS[jsr] = 2; // branchbyte1, branchbyte2 ? address + // jump to subroutine at branchoffset + // (signed short constructed from + // unsigned bytes branchbyte1 << 8 + + // branchbyte2) and place the return + // address on the stack + OFFSETS[ret] = 1; // index [No change] continue execution + // from address taken from a local + // variable #index (the asymmetry with + // jsr is intentional) + OFFSETS[tableswitch] = -1; // [0-3 bytes padding], + // defaultbyte1, defaultbyte2, + // defaultbyte3, defaultbyte4, + // lowbyte1, lowbyte2, lowbyte3, + // lowbyte4, highbyte1, + // highbyte2, highbyte3, + // highbyte4, jump offsets... + // index ? continue execution + // from an address in the table + // at offset index + OFFSETS[lookupswitch] = -1; // <0-3 bytes padding>, + // defaultbyte1, defaultbyte2, + // defaultbyte3, defaultbyte4, + // npairs1, npairs2, npairs3, + // npairs4, match-offset + // pairs... key ? a target + // address is looked up from a + // table using a key and + // execution continues from the + // instruction at that address + OFFSETS[getstatic] = 2; // index1, index2 ? value gets a + // static field value of a class, + // where the field is identified by + // field reference in the constant + // pool index (index1 << 8 + index2) + OFFSETS[putstatic] = 2; // indexbyte1, indexbyte2 value ? + // set static field to value in a + // class, where the field is + // identified by a field reference + // index in constant pool + // (indexbyte1 << 8 + indexbyte2) + OFFSETS[getfield] = 2; // index1, index2 objectref ? value + // gets a field value of an object + // objectref, where the field is + // identified by field reference in + // the constant pool index (index1 + // << 8 + index2) + OFFSETS[putfield] = 2; // indexbyte1, indexbyte2 objectref, + // value ? set field to value in an + // object objectref, where the field + // is identified by a field + // reference index in constant pool + // (indexbyte1 << 8 + indexbyte2) + OFFSETS[invokevirtual] = 2; // indexbyte1, indexbyte2 + // objectref, [arg1, arg2, ...] + // ? invoke virtual method on + // object objectref, where the + // method is identified by + // method reference index in + // constant pool (indexbyte1 << + // 8 + indexbyte2) + OFFSETS[invokespecial] = 2; // indexbyte1, indexbyte2 + // objectref, [arg1, arg2, ...] + // ? invoke instance method on + // object objectref, where the + // method is identified by + // method reference index in + // constant pool (indexbyte1 << + // 8 + indexbyte2) + OFFSETS[invokestatic] = 2; // indexbyte1, indexbyte2 [arg1, + // arg2, ...] ? invoke a static + // method, where the method is + // identified by method + // reference index in constant + // pool (indexbyte1 << 8 + + // indexbyte2) + OFFSETS[invokeinterface] = 2; // indexbyte1, indexbyte2, + // count, 0 objectref, + // [arg1, arg2, ...] ? + // invokes an interface + // method on object + // objectref, where the + // interface method is + // identified by method + // reference index in + // constant pool (indexbyte1 + // << 8 + indexbyte2) + + OFFSETS[invokedynamic] = 4; // 4: indexbyte1, indexbyte2, 0, 0 + + OFFSETS[new_] = 2; // indexbyte1, indexbyte2 ? objectref + // creates new object of type identified + // by class reference in constant pool + // index (indexbyte1 << 8 + indexbyte2) + OFFSETS[newarray] = 1; // atype count ? arrayref creates + // new array with count elements of + // primitive type identified by + // atype + OFFSETS[anewarray] = 2; // indexbyte1, indexbyte2 count ? + // arrayref creates a new array of + // references of length count and + // component type identified by the + // class reference index (indexbyte1 + // << 8 + indexbyte2) in the + // constant pool + OFFSETS[checkcast] = 2; // indexbyte1, indexbyte2 objectref + // ? objectref checks whether an + // objectref is of a certain type, + // the class reference of which is + // in the constant pool at index + // (indexbyte1 << 8 + indexbyte2) + OFFSETS[instanceof_] = 2; // indexbyte1, indexbyte2 objectref + // ? result determines if an object + // objectref is of a given type, + // identified by class reference + // index in constant pool + // (indexbyte1 << 8 + indexbyte2) + OFFSETS[wide] = 3; // opcode, indexbyte1, indexbyte2 + OFFSETS[multianewarray] = 3; // indexbyte1, indexbyte2, + // dimensions count1, + // [count2,...] ? arrayref + // create a new array of + // dimensions dimensions with + // elements of type identified + // by class reference in + // constant pool index + // (indexbyte1 << 8 + + // indexbyte2); the sizes of + // each dimension is identified + // by count1, [count2, etc] + OFFSETS[ifnull] = 2; // branchbyte1, branchbyte2 value ? if + // value is null, branch to instruction + // at branchoffset (signed short + // constructed from unsigned bytes + // branchbyte1 << 8 + branchbyte2) + OFFSETS[ifnonnull] = 2; // branchbyte1, branchbyte2 value ? + // if value is not null, branch to + // instruction at branchoffset + // (signed short constructed from + // unsigned bytes branchbyte1 << 8 + + // branchbyte2) + OFFSETS[goto_w] = 4; // branchbyte1, branchbyte2, + // branchbyte3, branchbyte4 [no change] + // goes to another instruction at + // branchoffset (signed int constructed + // from unsigned bytes branchbyte1 << 24 + // + branchbyte2 << 16 + branchbyte3 << + // 8 + branchbyte4) + OFFSETS[jsr_w] = 4; // branchbyte1, branchbyte2, + // branchbyte3, branchbyte4 ? address + // jump to subroutine at branchoffset + // (signed int constructed from unsigned + // bytes branchbyte1 << 24 + branchbyte2 + // << 16 + branchbyte3 << 8 + + // branchbyte4) and place the return + // address on the stack + } + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/util/CompoundEnumeration.java b/framework/src/main/java/org/apache/felix/framework/util/CompoundEnumeration.java new file mode 100644 index 00000000000..8ea8ffba8d9 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/util/CompoundEnumeration.java @@ -0,0 +1,109 @@ +/* + * 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.framework.util; + +import java.util.Enumeration; +import java.util.NoSuchElementException; + +public class CompoundEnumeration implements Enumeration +{ + private Enumeration[] m_enums = null; + private int index = 0; + + public CompoundEnumeration(Enumeration[] enums) + { + m_enums = enums; + } + + public boolean hasMoreElements() + { + // if the current enum is null that means this enum is finished + if (currentEnumeration() == null) + { + // No next enum + return false; + } + // If the current enum has more elements, lets go + return currentEnumeration().hasMoreElements(); + } + + private Enumeration findNextEnumeration(boolean moveCursor) + { + return findNextEnumeration(index, moveCursor); + } + + private Enumeration findNextEnumeration(int cursor, boolean moveCursor) + { + // next place in the array + int next = cursor + 1; + // If the cursor is still in the array + if (next < m_enums.length) + { + + // If there is something in that place + // AND the enum is not empty + if (m_enums[next] != null && + m_enums[next].hasMoreElements()) + { + // OK + if (moveCursor) + { + index = next; + } + return m_enums[next]; + } + // Try next element + return findNextEnumeration(next, moveCursor); + } + // No more elements available + return null; + } + + public Object nextElement() + { + // ask for the next element of the current enum. + if (currentEnumeration() != null) + { + return currentEnumeration().nextElement(); + } + + // no more elements in this Enum + // We must throw a NoSuchElementException + throw new NoSuchElementException("No more elements"); + } + + private Enumeration currentEnumeration() + { + if (m_enums != null) + { + if (index < m_enums.length) + { + Enumeration e = m_enums[index]; + if (e == null || !e.hasMoreElements()) + { + // the current enum is null or empty + // we probably want to switch to the next one + e = findNextEnumeration(true); + } + return e; + } + } + return null; + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/EventDispatcher.java b/framework/src/main/java/org/apache/felix/framework/util/EventDispatcher.java deleted file mode 100644 index 62902fdd2e2..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/util/EventDispatcher.java +++ /dev/null @@ -1,791 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.util; - -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.util.*; - -import org.apache.felix.framework.Felix; -import org.apache.felix.framework.Logger; -import org.osgi.framework.*; - -public class EventDispatcher -{ - private static final int LISTENER_BUNDLE_OFFSET = 0; - private static final int LISTENER_CLASS_OFFSET = 1; - private static final int LISTENER_OBJECT_OFFSET = 2; - private static final int LISTENER_FILTER_OFFSET = 3; - private static final int LISTENER_SECURITY_OFFSET = 4; - private static final int LISTENER_ARRAY_INCREMENT = 5; - - // Framework instance for this dispatcher. - private Logger m_logger = null; - - // Representation of an empty listener list. - private static final Object[] m_emptyList = new Object[0]; - - private Object[] m_frameworkListeners = m_emptyList; - private Object[] m_bundleListeners = m_emptyList; - private Object[] m_syncBundleListeners = m_emptyList; - private Object[] m_serviceListeners = m_emptyList; - - // A single thread is used to deliver events for all dispatchers. - private static Thread m_thread = null; - private static String m_threadLock = "thread lock"; - private static boolean m_stopping = false; - private static boolean m_stopped = false; - // List of requests. - private static final ArrayList m_requestList = new ArrayList(); - // Pooled requests to avoid memory allocation. - private static final ArrayList m_requestPool = new ArrayList(); - - public EventDispatcher(Logger logger) - { - m_logger = logger; - - synchronized (m_threadLock) - { - // Start event dispatching thread if necessary. - if (m_thread == null) - { - m_thread = new Thread(new Runnable() { - public void run() - { - EventDispatcher.run(); - } - }, "FelixDispatchQueue"); - m_thread.start(); - } - } - } - - public static void shutdown() - { - synchronized (m_threadLock) - { - // Return if already stopped. - if (m_stopped) - { - return; - } - - // Signal dispatch thread. - m_stopping = true; - synchronized (m_requestList) - { - m_requestList.notify(); - } - - // Wait for dispatch thread to stop. - while (!m_stopped) - { - try - { - m_threadLock.wait(); - } - catch (InterruptedException ex) - { - } - } - } - } - - public void addListener(Bundle bundle, Class clazz, EventListener l, Filter filter) - { - // Verify the listener. - if (l == null) - { - throw new IllegalArgumentException("Listener is null"); - } - else if (!clazz.isInstance(l)) - { - throw new IllegalArgumentException( - "Listener not of type " + clazz.getName()); - } - - // See if we can simply update the listener, if so then - // return immediately. - if (updateListener(bundle, clazz, l, filter)) - { - return; - } - - // Lock the object to add the listener. - synchronized (this) - { - Object[] listeners = null; - Object acc = null; - - if (clazz == FrameworkListener.class) - { - listeners = m_frameworkListeners; - } - else if (clazz == BundleListener.class) - { - if (SynchronousBundleListener.class.isInstance(l)) - { - listeners = m_syncBundleListeners; - } - else - { - listeners = m_bundleListeners; - } - } - else if (clazz == ServiceListener.class) - { - // Remember security context for filtering service events. - Object sm = System.getSecurityManager(); - if (sm != null) - { - acc = ((SecurityManager) sm).getSecurityContext(); - } - - listeners = m_serviceListeners; - } - else - { - throw new IllegalArgumentException("Unknown listener: " + l.getClass()); - } - - // If we have no listeners, then just add the new listener. - if (listeners == m_emptyList) - { - listeners = new Object[LISTENER_ARRAY_INCREMENT]; - listeners[LISTENER_BUNDLE_OFFSET] = bundle; - listeners[LISTENER_CLASS_OFFSET] = clazz; - listeners[LISTENER_OBJECT_OFFSET] = l; - listeners[LISTENER_FILTER_OFFSET] = filter; - listeners[LISTENER_SECURITY_OFFSET] = acc; - } - // Otherwise, we need to do some array copying. - // Notice, the old array is always valid, so if - // the dispatch thread is in the middle of a dispatch, - // then it has a reference to the old listener array - // and is not affected by the new value. - else - { - Object[] newList = new Object[listeners.length + LISTENER_ARRAY_INCREMENT]; - System.arraycopy(listeners, 0, newList, 0, listeners.length); - newList[listeners.length + LISTENER_BUNDLE_OFFSET] = bundle; - newList[listeners.length + LISTENER_CLASS_OFFSET] = clazz; - newList[listeners.length + LISTENER_OBJECT_OFFSET] = l; - newList[listeners.length + LISTENER_FILTER_OFFSET] = filter; - newList[listeners.length + LISTENER_SECURITY_OFFSET] = acc; - listeners = newList; - } - - if (clazz == FrameworkListener.class) - { - m_frameworkListeners = listeners; - } - else if (clazz == BundleListener.class) - { - if (SynchronousBundleListener.class.isInstance(l)) - { - m_syncBundleListeners = listeners; - } - else - { - m_bundleListeners = listeners; - } - } - else if (clazz == ServiceListener.class) - { - m_serviceListeners = listeners; - } - } - } - - public void removeListener(Bundle bundle, Class clazz, EventListener l) - { - // Verify listener. - if (l == null) - { - throw new IllegalArgumentException("Listener is null"); - } - else if (!clazz.isInstance(l)) - { - throw new IllegalArgumentException( - "Listener not of type " + clazz.getName()); - } - - // Lock the object to remove the listener. - synchronized (this) - { - Object[] listeners = null; - - if (clazz == FrameworkListener.class) - { - listeners = m_frameworkListeners; - } - else if (clazz == BundleListener.class) - { - if (SynchronousBundleListener.class.isInstance(l)) - { - listeners = m_syncBundleListeners; - } - else - { - listeners = m_bundleListeners; - } - } - else if (clazz == ServiceListener.class) - { - listeners = m_serviceListeners; - } - else - { - throw new IllegalArgumentException("Unknown listener: " + l.getClass()); - } - - // Try to find the instance in our list. - int idx = -1; - for (int i = 0; i < listeners.length; i += LISTENER_ARRAY_INCREMENT) - { - if (listeners[i + LISTENER_BUNDLE_OFFSET].equals(bundle) && - (listeners[i + LISTENER_CLASS_OFFSET] == clazz) && - (listeners[i + LISTENER_OBJECT_OFFSET] == l)) - { - idx = i; - break; - } - } - - // If we have the instance, then remove it. - if (idx >= 0) - { - // If this is the last listener, then point to empty list. - if ((listeners.length - LISTENER_ARRAY_INCREMENT) == 0) - { - listeners = m_emptyList; - } - // Otherwise, we need to do some array copying. - // Notice, the old array is always valid, so if - // the dispatch thread is in the middle of a dispatch, - // then it has a reference to the old listener array - // and is not affected by the new value. - else - { - Object[] newList = new Object[listeners.length - LISTENER_ARRAY_INCREMENT]; - System.arraycopy(listeners, 0, newList, 0, idx); - if (idx < newList.length) - { - System.arraycopy( - listeners, idx + LISTENER_ARRAY_INCREMENT, - newList, idx, newList.length - idx); - } - listeners = newList; - } - } - - if (clazz == FrameworkListener.class) - { - m_frameworkListeners = listeners; - } - else if (clazz == BundleListener.class) - { - if (SynchronousBundleListener.class.isInstance(l)) - { - m_syncBundleListeners = listeners; - } - else - { - m_bundleListeners = listeners; - } - } - else if (clazz == ServiceListener.class) - { - m_serviceListeners = listeners; - } - } - } - - public void removeListeners(Bundle bundle) - { - if (bundle == null) - { - return; - } - - synchronized (this) - { - // Remove all framework listeners associated with the specified bundle. - Object[] listeners = m_frameworkListeners; - for (int i = listeners.length - LISTENER_ARRAY_INCREMENT; - i >= 0; - i -= LISTENER_ARRAY_INCREMENT) - { - // Check if the bundle associated with the current listener - // is the same as the specified bundle, if so remove the listener. - Bundle registeredBundle = (Bundle) listeners[LISTENER_BUNDLE_OFFSET]; - if (bundle.equals(registeredBundle)) - { - Class clazz = (Class) listeners[LISTENER_CLASS_OFFSET]; - EventListener l = (EventListener) listeners[LISTENER_OBJECT_OFFSET]; - removeListener(bundle, clazz, l); - } - } - - // Remove all bundle listeners associated with the specified bundle. - listeners = m_bundleListeners; - for (int i = listeners.length - LISTENER_ARRAY_INCREMENT; - i >= 0; - i -= LISTENER_ARRAY_INCREMENT) - { - // Check if the bundle associated with the current listener - // is the same as the specified bundle, if so remove the listener. - Bundle registeredBundle = (Bundle) listeners[LISTENER_BUNDLE_OFFSET]; - if (bundle.equals(registeredBundle)) - { - Class clazz = (Class) listeners[LISTENER_CLASS_OFFSET]; - EventListener l = (EventListener) listeners[LISTENER_OBJECT_OFFSET]; - removeListener(bundle, clazz, l); - } - } - - // Remove all synchronous bundle listeners associated with - // the specified bundle. - listeners = m_syncBundleListeners; - for (int i = listeners.length - LISTENER_ARRAY_INCREMENT; - i >= 0; - i -= LISTENER_ARRAY_INCREMENT) - { - // Check if the bundle associated with the current listener - // is the same as the specified bundle, if so remove the listener. - Bundle registeredBundle = (Bundle) listeners[LISTENER_BUNDLE_OFFSET]; - if (bundle.equals(registeredBundle)) - { - Class clazz = (Class) listeners[LISTENER_CLASS_OFFSET]; - EventListener l = (EventListener) listeners[LISTENER_OBJECT_OFFSET]; - removeListener(bundle, clazz, l); - } - } - - // Remove all service listeners associated with the specified bundle. - listeners = m_serviceListeners; - for (int i = listeners.length - LISTENER_ARRAY_INCREMENT; - i >= 0; - i -= LISTENER_ARRAY_INCREMENT) - { - // Check if the bundle associated with the current listener - // is the same as the specified bundle, if so remove the listener. - Bundle registeredBundle = (Bundle) listeners[LISTENER_BUNDLE_OFFSET]; - if (bundle.equals(registeredBundle)) - { - Class clazz = (Class) listeners[LISTENER_CLASS_OFFSET]; - EventListener l = (EventListener) listeners[LISTENER_OBJECT_OFFSET]; - removeListener(bundle, clazz, l); - } - } - } - } - - public boolean updateListener(Bundle bundle, Class clazz, EventListener l, Filter filter) - { - synchronized (this) - { - Object[] listeners = null; - - if (clazz == FrameworkListener.class) - { - listeners = m_frameworkListeners; - } - else if (clazz == BundleListener.class) - { - if (SynchronousBundleListener.class.isInstance(l)) - { - listeners = m_syncBundleListeners; - } - else - { - listeners = m_bundleListeners; - } - } - else if (clazz == ServiceListener.class) - { - listeners = m_serviceListeners; - } - - // See if the listener is already registered, if so then - // handle it according to the spec. - for (int i = 0; i < listeners.length; i += LISTENER_ARRAY_INCREMENT) - { - if (listeners[i + LISTENER_BUNDLE_OFFSET].equals(bundle) && - (listeners[i + LISTENER_CLASS_OFFSET] == clazz) && - (listeners[i + LISTENER_OBJECT_OFFSET] == l)) - { - if (l instanceof FrameworkListener) - { - // The spec says to ignore this case. - } - else if (l instanceof BundleListener) - { - // The spec says to ignore this case. - } - else if (l instanceof ServiceListener) - { - // The spec says to update the filter in this case. - listeners[i + LISTENER_FILTER_OFFSET] = filter; - } - return true; - } - } - } - - return false; - } - - public void fireFrameworkEvent(FrameworkEvent event) - { - // Take a snapshot of the listener array. - Object[] listeners = null; - synchronized (this) - { - listeners = m_frameworkListeners; - } - - // Fire all framework listeners on a separate thread. - fireEventAsynchronously(m_logger, Request.FRAMEWORK_EVENT, listeners, event); - } - - public void fireBundleEvent(BundleEvent event) - { - // Take a snapshot of the listener array. - Object[] listeners = null; - Object[] syncListeners = null; - synchronized (this) - { - listeners = m_bundleListeners; - syncListeners = m_syncBundleListeners; - } - - // Fire synchronous bundle listeners immediately on the calling thread. - fireEventImmediately(m_logger, Request.BUNDLE_EVENT, syncListeners, event); - - // The spec says that asynchronous bundle listeners do not get events - // of types STARTING or STOPPING. - if ((event.getType() != BundleEvent.STARTING) && - (event.getType() != BundleEvent.STOPPING)) - { - // Fire asynchronous bundle listeners on a separate thread. - fireEventAsynchronously(m_logger, Request.BUNDLE_EVENT, listeners, event); - } - } - - public void fireServiceEvent(ServiceEvent event) - { - // Take a snapshot of the listener array. - Object[] listeners = null; - synchronized (this) - { - listeners = m_serviceListeners; - } - - // Fire all service events immediately on the calling thread. - fireEventImmediately(m_logger, Request.SERVICE_EVENT, listeners, event); - } - - private void fireEventAsynchronously( - Logger logger, int type, Object[] listeners, EventObject event) - { - // If dispatch thread is stopped, then ignore dispatch request. - if (m_stopped || m_stopping) - { - return; - } - - // First get a request from the pool or create one if necessary. - Request req = null; - synchronized (m_requestPool) - { - if (m_requestPool.size() > 0) - { - req = (Request) m_requestPool.remove(0); - } - else - { - req = new Request(); - } - } - - // Initialize dispatch request. - req.m_logger = logger; - req.m_type = type; - req.m_listeners = listeners; - req.m_event = event; - - // Lock the request list. - synchronized (m_requestList) - { - // Add our request to the list. - m_requestList.add(req); - // Notify the dispatch thread that there is work to do. - m_requestList.notify(); - } - } - - private static void fireEventImmediately( - Logger logger, int type, Object[] listeners, EventObject event) - { - if (listeners.length > 0) - { - // Notify appropriate listeners. - for (int i = listeners.length - LISTENER_ARRAY_INCREMENT; - i >= 0; - i -= LISTENER_ARRAY_INCREMENT) - { - Bundle bundle = (Bundle) listeners[i + LISTENER_BUNDLE_OFFSET]; - EventListener l = (EventListener) listeners[i + LISTENER_OBJECT_OFFSET]; - Filter filter = (Filter) listeners[i + LISTENER_FILTER_OFFSET]; - Object acc = listeners[i + LISTENER_SECURITY_OFFSET]; - try - { - if (type == Request.FRAMEWORK_EVENT) - { - invokeFrameworkListenerCallback(bundle, l, event); - } - else if (type == Request.BUNDLE_EVENT) - { - invokeBundleListenerCallback(bundle, l, event); - } - else if (type == Request.SERVICE_EVENT) - { - invokeServiceListenerCallback(bundle, l, filter, acc, event); - } - } - catch (Throwable th) - { - logger.log( - Logger.LOG_ERROR, - "EventDispatcher: Error during dispatch.", th); - } - } - } - } - - private static void invokeFrameworkListenerCallback( - Bundle bundle, final EventListener l, final EventObject event) - { - // The spec says only active bundles receive asynchronous events, - // but we will include starting bundles too otherwise - // it is impossible to see everything. - if ((bundle.getState() == Bundle.STARTING) || - (bundle.getState() == Bundle.ACTIVE)) - { - if (System.getSecurityManager() != null) - { - AccessController.doPrivileged(new PrivilegedAction() { - public Object run() - { - ((FrameworkListener) l).frameworkEvent((FrameworkEvent) event); - return null; - } - }); - } - else - { - ((FrameworkListener) l).frameworkEvent((FrameworkEvent) event); - } - } - } - - private static void invokeBundleListenerCallback( - Bundle bundle, final EventListener l, final EventObject event) - { - // A bundle listener is either synchronous or asynchronous. - // If the bundle listener is synchronous, then deliver the - // event to bundles with a state of STARTING, STOPPING, or - // ACTIVE. If the listener is asynchronous, then deliver the - // event only to bundles that are STARTING or ACTIVE. - if (((SynchronousBundleListener.class.isAssignableFrom(l.getClass())) && - ((bundle.getState() == Bundle.STARTING) || - (bundle.getState() == Bundle.STOPPING) || - (bundle.getState() == Bundle.ACTIVE))) - || - ((bundle.getState() == Bundle.STARTING) || - (bundle.getState() == Bundle.ACTIVE))) - { - if (System.getSecurityManager() != null) - { - AccessController.doPrivileged(new PrivilegedAction() { - public Object run() - { - ((BundleListener) l).bundleChanged((BundleEvent) event); - return null; - } - }); - } - else - { - ((BundleListener) l).bundleChanged((BundleEvent) event); - } - } - } - - private static void invokeServiceListenerCallback( - Bundle bundle, final EventListener l, Filter filter, Object acc, final EventObject event) - { - // Service events should be delivered to STARTING, - // STOPPING, and ACTIVE bundles. - if ((bundle.getState() != Bundle.STARTING) && - (bundle.getState() != Bundle.STOPPING) && - (bundle.getState() != Bundle.ACTIVE)) - { - return; - } - - // Check that the bundle has permission to get at least - // one of the service interfaces; the objectClass property - // of the service stores its service interfaces. - ServiceReference ref = ((ServiceEvent) event).getServiceReference(); - String[] objectClass = (String[]) ref.getProperty(Constants.OBJECTCLASS); - - // On the safe side, if there is no objectClass property - // then ignore event altogether. - if (objectClass != null) - { - boolean hasPermission = false; - - Object sm = System.getSecurityManager(); - if ((acc != null) && (sm != null)) - { - for (int i = 0; - !hasPermission && (i < objectClass.length); - i++) - { - try - { - ServicePermission perm = - new ServicePermission( - objectClass[i], ServicePermission.GET); - ((SecurityManager) sm).checkPermission(perm, acc); - hasPermission = true; - } - catch (Exception ex) - { - } - } - } - else - { - hasPermission = true; - } - - if (hasPermission) - { - // Dispatch according to the filter. - if ((filter == null) || filter.match(((ServiceEvent) event).getServiceReference())) - { - if ((l instanceof AllServiceListener) || - Felix.isServiceAssignable(bundle, ((ServiceEvent) event).getServiceReference())) - { - if (System.getSecurityManager() != null) - { - AccessController.doPrivileged(new PrivilegedAction() { - public Object run() - { - ((ServiceListener) l).serviceChanged((ServiceEvent) event); - return null; - } - }); - } - else - { - { - ((ServiceListener) l).serviceChanged((ServiceEvent) event); - } - } - } - } - } - } - } - - /** - * This is the dispatching thread's main loop. - **/ - private static void run() - { - Request req = null; - while (true) - { - // Lock the request list so we can try to get a - // dispatch request from it. - synchronized (m_requestList) - { - // Wait while there are no requests to dispatch. If the - // dispatcher thread is supposed to stop, then let the - // dispatcher thread exit the loop and stop. - while ((m_requestList.size() == 0) && !m_stopping) - { - // Wait until some signals us for work. - try - { - m_requestList.wait(); - } - catch (InterruptedException ex) - { - // Not much we can do here except for keep waiting. - } - } - - // If there are no events to dispatch and shutdown - // has been called then exit, otherwise dispatch event. - if ((m_requestList.size() == 0) && (m_stopping)) - { - synchronized (m_threadLock) - { - m_stopped = true; - m_threadLock.notifyAll(); - } - return; - } - - // Get the dispatch request. - req = (Request) m_requestList.remove(0); - } - - // Deliver event outside of synchronized block - // so that we don't block other requests from being - // queued during event processing. - fireEventImmediately(req.m_logger, req.m_type, req.m_listeners, req.m_event); - - // Put dispatch request in cache. - synchronized (m_requestPool) - { - req.m_logger = null; - req.m_type = -1; - req.m_listeners = null; - req.m_event = null; - m_requestPool.add(req); - } - } - } - - private static class Request - { - public static final int FRAMEWORK_EVENT = 0; - public static final int BUNDLE_EVENT = 1; - public static final int SERVICE_EVENT = 2; - - public Logger m_logger = null; - public int m_type = -1; - public Object[] m_listeners = null; - public EventObject m_event = null; - } -} diff --git a/framework/src/main/java/org/apache/felix/framework/util/FelixConstants.java b/framework/src/main/java/org/apache/felix/framework/util/FelixConstants.java index d71cb233b64..71a1f75151f 100644 --- a/framework/src/main/java/org/apache/felix/framework/util/FelixConstants.java +++ b/framework/src/main/java/org/apache/felix/framework/util/FelixConstants.java @@ -1,60 +1,76 @@ /* - * Copyright 2005 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework.util; public interface FelixConstants extends org.osgi.framework.Constants { + String SYSTEM_BUNDLE_SYMBOLICNAME = "org.apache.felix.framework"; // Framework constants and values. - public static final String FRAMEWORK_VERSION_VALUE = "1.3"; - public static final String FRAMEWORK_VENDOR_VALUE = "Apache Software Foundation"; + String FRAMEWORK_VERSION_VALUE = "1.9"; + String FRAMEWORK_VENDOR_VALUE = "Apache Software Foundation"; // Framework constants and values. - public static final String FELIX_VERSION_PROPERTY = "felix.version"; - + String FELIX_VERSION_PROPERTY = "felix.version"; + // Miscellaneous manifest constants. - public static final String DIRECTIVE_SEPARATOR = ":="; - public static final String ATTRIBUTE_SEPARATOR = "="; - public static final String CLASS_PATH_SEPARATOR = ","; - public static final String CLASS_PATH_DOT = "."; - public static final String PACKAGE_SEPARATOR = ";"; - public static final String VERSION_SEGMENT_SEPARATOR = "."; - public static final int VERSION_SEGMENT_COUNT = 3; - public static final String BUNDLE_NATIVECODE_OPTIONAL = "*"; + String DIRECTIVE_SEPARATOR = ":="; + String ATTRIBUTE_SEPARATOR = "="; + String CLASS_PATH_SEPARATOR = ","; + String CLASS_PATH_DOT = "."; + String PACKAGE_SEPARATOR = ";"; + String VERSION_SEGMENT_SEPARATOR = "."; + int VERSION_SEGMENT_COUNT = 3; + String BUNDLE_NATIVECODE_OPTIONAL = "*"; // Miscellaneous OSGi constants. - public static final String BUNDLE_URL_PROTOCOL = "bundle"; + String BUNDLE_URL_PROTOCOL = "bundle"; // Miscellaneous framework configuration property names. - public static final String AUTO_INSTALL_PROP = "felix.auto.install"; - public static final String AUTO_START_PROP = "felix.auto.start"; - public static final String EMBEDDED_EXECUTION_PROP = "felix.embedded.execution"; - public static final String STRICT_OSGI_PROP = "felix.strict.osgi"; - public static final String FRAMEWORK_STARTLEVEL_PROP - = "felix.startlevel.framework"; - public static final String BUNDLE_STARTLEVEL_PROP - = "felix.startlevel.bundle"; - public static final String SERVICE_URLHANDLERS_PROP = "felix.service.urlhandlers"; + String FRAMEWORK_BUNDLECACHE_IMPL = "felix.bundlecache.impl"; + String LOG_LEVEL_PROP = "felix.log.level"; + String LOG_LOGGER_PROP = "felix.log.logger"; + String SYSTEMBUNDLE_ACTIVATORS_PROP = "felix.systembundle.activators"; + String BUNDLE_STARTLEVEL_PROP = "felix.startlevel.bundle"; + String SERVICE_URLHANDLERS_PROP = "felix.service.urlhandlers"; + String IMPLICIT_BOOT_DELEGATION_PROP = "felix.bootdelegation.implicit"; + String BOOT_CLASSLOADERS_PROP = "felix.bootdelegation.classloaders"; + String USE_LOCALURLS_PROP = "felix.jarurls"; + String NATIVE_OS_NAME_ALIAS_PREFIX = "felix.native.osname.alias"; + String NATIVE_PROC_NAME_ALIAS_PREFIX = "felix.native.processor.alias"; + String USE_CACHEDURLS_PROPS = "felix.bundlecodesource.usecachedurls"; + String RESOLVER_PARALLELISM = "felix.resolver.parallelism"; + String USE_PROPERTY_SUBSTITUTION_IN_SYSTEMPACKAGES = "felix.systempackages.substitution"; + + // Missing OSGi constant for resolution directive. + String RESOLUTION_DYNAMIC = "dynamic"; // Start level-related constants. - public static final int FRAMEWORK_INACTIVE_STARTLEVEL = 0; - public static final int FRAMEWORK_DEFAULT_STARTLEVEL = 1; - public static final int SYSTEMBUNDLE_DEFAULT_STARTLEVEL = 0; - public static final int BUNDLE_DEFAULT_STARTLEVEL = 1; + int FRAMEWORK_INACTIVE_STARTLEVEL = 0; + int FRAMEWORK_DEFAULT_STARTLEVEL = 1; + int SYSTEMBUNDLE_DEFAULT_STARTLEVEL = 0; + int BUNDLE_DEFAULT_STARTLEVEL = 1; // Miscellaneous properties values. - public static final String FAKE_URL_PROTOCOL_VALUE = "location:"; -} \ No newline at end of file + String FAKE_URL_PROTOCOL_VALUE = "location:"; + String FELIX_EXTENSION_ACTIVATOR = "Felix-Activator"; + String SECURITY_DEFAULT_POLICY = "felix.security.defaultpolicy"; + String FELIX_EXTENSIONS_DISABLE = "felix.extensions.disable"; + String FRAMEWORK_UUID_SECURE = "felix.uuid.secure"; + String CALCULATE_SYSTEMPACKAGES_USES = "felix.systempackages.calculate.uses"; +} diff --git a/framework/src/main/java/org/apache/felix/framework/util/IteratorToEnumeration.java b/framework/src/main/java/org/apache/felix/framework/util/IteratorToEnumeration.java deleted file mode 100644 index adf8b19a5e9..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/util/IteratorToEnumeration.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.util; - -import java.util.Enumeration; -import java.util.Iterator; - -public class IteratorToEnumeration implements Enumeration -{ - private Iterator m_iter = null; - - public IteratorToEnumeration(Iterator iter) - { - m_iter = iter; - } - - public boolean hasMoreElements() - { - if (m_iter == null) - return false; - return m_iter.hasNext(); - } - - public Object nextElement() - { - if (m_iter == null) - return null; - return m_iter.next(); - } -} diff --git a/framework/src/main/java/org/apache/felix/framework/util/ListenerInfo.java b/framework/src/main/java/org/apache/felix/framework/util/ListenerInfo.java new file mode 100644 index 00000000000..b7ad8aece91 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/util/ListenerInfo.java @@ -0,0 +1,141 @@ +/* + * 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.framework.util; + +import java.util.EventListener; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Filter; +import org.osgi.framework.hooks.service.ListenerHook; + + +public class ListenerInfo implements ListenerHook.ListenerInfo +{ + private final Bundle m_bundle; + private final BundleContext m_context; + private final Class m_listenerClass; + private final EventListener m_listener; + private final Filter m_filter; + private final Object m_acc; + private final boolean m_removed; + + public ListenerInfo( + Bundle bundle, BundleContext context, Class listenerClass, EventListener listener, + Filter filter, Object acc, boolean removed) + { + // Technically, we could get the bundle from the bundle context, but + // there are some corner cases where the bundle context might become + // invalid and we still need the bundle. + m_bundle = bundle; + m_context = context; + m_listenerClass = listenerClass; + m_listener = listener; + m_filter = filter; + m_acc = acc; + m_removed = removed; + } + + public ListenerInfo(ListenerInfo info, boolean removed) + { + m_bundle = info.m_bundle; + m_context = info.m_context; + m_listenerClass = info.m_listenerClass; + m_listener = info.m_listener; + m_filter = info.m_filter; + m_acc = info.m_acc; + m_removed = removed; + } + + public Bundle getBundle() + { + return m_bundle; + } + + public BundleContext getBundleContext() + { + return m_context; + } + + public Class getListenerClass() + { + return m_listenerClass; + } + + public EventListener getListener() + { + return m_listener; + } + + public Filter getParsedFilter() + { + return m_filter; + } + + public String getFilter() + { + if (m_filter != null) + { + return m_filter.toString(); + } + return null; + } + + public Object getSecurityContext() + { + return m_acc; + } + + public boolean isRemoved() + { + return m_removed; + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) + { + return true; + } + + if (!(obj instanceof ListenerInfo)) + { + return false; + } + + ListenerInfo other = (ListenerInfo) obj; + return (other.m_bundle == m_bundle) + && (other.m_context == m_context) + && (other.m_listenerClass == m_listenerClass) + && (other.m_listener == m_listener) + && (m_filter == null ? other.m_filter == null : m_filter.equals(other.m_filter)); + } + + @Override + public int hashCode() + { + int hash = 7; + hash = 59 * hash + (this.m_bundle != null ? this.m_bundle.hashCode() : 0); + hash = 59 * hash + (this.m_context != null ? this.m_context.hashCode() : 0); + hash = 59 * hash + (this.m_listenerClass != null ? this.m_listenerClass.hashCode() : 0); + hash = 59 * hash + (this.m_listener != null ? this.m_listener.hashCode() : 0); + hash = 59 * hash + (this.m_filter != null ? this.m_filter.hashCode() : 0); + return hash; + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/ManifestParser.java b/framework/src/main/java/org/apache/felix/framework/util/ManifestParser.java deleted file mode 100644 index 91c9bb2cc72..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/util/ManifestParser.java +++ /dev/null @@ -1,528 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.util; - -import java.util.*; - -import org.apache.felix.framework.Logger; -import org.apache.felix.framework.cache.BundleRevision; -import org.apache.felix.framework.searchpolicy.*; -import org.osgi.framework.*; - -public class ManifestParser -{ - private Logger m_logger = null; - private PropertyResolver m_config = null; - private Map m_headerMap = null; - private R4Export[] m_exports = null; - private R4Import[] m_imports = null; - private R4Import[] m_dynamics = null; - private R4LibraryClause[] m_libraryHeaders = null; - private boolean m_libraryHeadersOptional = false; - - public ManifestParser(Logger logger, PropertyResolver config, Map headerMap) - throws BundleException - { - m_logger = logger; - m_config = config; - m_headerMap = headerMap; - - // Verify that only manifest version 2 is specified. - String manifestVersion = get(Constants.BUNDLE_MANIFESTVERSION); - if ((manifestVersion != null) && !manifestVersion.equals("2")) - { - throw new BundleException( - "Unknown 'Bundle-ManifestVersion' value: " + manifestVersion); - } - - // Verify bundle version syntax. - if (get(Constants.BUNDLE_VERSION) != null) - { - try - { - Version.parseVersion(get(Constants.BUNDLE_VERSION)); - } - catch (RuntimeException ex) - { - // R4 bundle versions must parse, R3 bundle version may not. - if (getVersion().equals("2")) - { - throw ex; - } - } - } - - // Create map to check for duplicate imports/exports. - Map dupeMap = new HashMap(); - - // - // Parse Export-Package. - // - - // Get export packages from bundle manifest. - R4Package[] pkgs = R4Package.parseImportOrExportHeader( - (String) headerMap.get(Constants.EXPORT_PACKAGE)); - - // Create non-duplicated export array. - dupeMap.clear(); - for (int i = 0; i < pkgs.length; i++) - { - if (dupeMap.get(pkgs[i].getName()) == null) - { - // Verify that java.* packages are not exported. - if (pkgs[i].getName().startsWith("java.")) - { - throw new BundleException( - "Exporting java.* packages not allowed: " + pkgs[i].getName()); - } - dupeMap.put(pkgs[i].getName(), new R4Export(pkgs[i])); - } - else - { - // TODO: FRAMEWORK - Exports can be duplicated, so fix this. - m_logger.log(Logger.LOG_WARNING, - "Duplicate export - " + pkgs[i].getName()); - } - } - m_exports = (R4Export[]) dupeMap.values().toArray(new R4Export[dupeMap.size()]); - - // - // Parse Import-Package. - // - - // Get import packages from bundle manifest. - pkgs = R4Package.parseImportOrExportHeader( - (String) headerMap.get(Constants.IMPORT_PACKAGE)); - - // Create non-duplicated import array. - dupeMap.clear(); - for (int i = 0; i < pkgs.length; i++) - { - if (dupeMap.get(pkgs[i].getName()) == null) - { - // Verify that java.* packages are not imported. - if (pkgs[i].getName().startsWith("java.")) - { - throw new BundleException( - "Importing java.* packages not allowed: " + pkgs[i].getName()); - } - dupeMap.put(pkgs[i].getName(), new R4Import(pkgs[i])); - } - else - { - throw new BundleException( - "Duplicate import - " + pkgs[i].getName()); - } - } - m_imports = (R4Import[]) dupeMap.values().toArray(new R4Import[dupeMap.size()]); - - // - // Parse DynamicImport-Package. - // - - // Get dynamic import packages from bundle manifest. - pkgs = R4Package.parseImportOrExportHeader( - (String) headerMap.get(Constants.DYNAMICIMPORT_PACKAGE)); - - // Dynamic imports can have duplicates, so just create an array. - m_dynamics = new R4Import[pkgs.length]; - for (int i = 0; i < pkgs.length; i++) - { - m_dynamics[i] = new R4Import(pkgs[i]); - } - - // - // Parse Bundle-NativeCode. - // - - // Get native library entry names for module library sources. - m_libraryHeaders = - Util.parseLibraryStrings( - m_logger, - Util.parseDelimitedString(get(Constants.BUNDLE_NATIVECODE), ",")); - - // Check to see if there was an optional native library clause, which is - // represented by a null library header; if so, record it and remove it. - if ((m_libraryHeaders.length > 0) && - (m_libraryHeaders[m_libraryHeaders.length - 1].getLibraryFiles() == null)) - { - m_libraryHeadersOptional = true; - R4LibraryClause[] tmp = new R4LibraryClause[m_libraryHeaders.length - 1]; - System.arraycopy(m_libraryHeaders, 0, tmp, 0, m_libraryHeaders.length - 1); - m_libraryHeaders = tmp; - } - - // Do final checks and normalization of manifest. - if (getVersion().equals("2")) - { - checkAndNormalizeR4(); - } - else - { - checkAndNormalizeR3(); - } - } - - public String get(String key) - { - return (String) m_headerMap.get(key); - } - - public String getVersion() - { - String manifestVersion = get(Constants.BUNDLE_MANIFESTVERSION); - return (manifestVersion == null) ? "1" : manifestVersion; - } - - public R4Export[] getExports() - { - return m_exports; - } - - public R4Import[] getImports() - { - return m_imports; - } - - public R4Import[] getDynamicImports() - { - return m_dynamics; - } - - public R4LibraryClause[] getLibraryClauses() - { - return m_libraryHeaders; - } - - /** - *

      - * This method returns the selected native library metadata from - * the manifest. The information is not the raw metadata from the - * manifest, but is native library metadata clause selected according - * to the OSGi native library clause selection policy. The metadata - * returned by this method will be attached directly to a module and - * used for finding its native libraries at run time. To inspect the - * raw native library metadata refer to getLibraryClauses(). - *

      - * @param revision the bundle revision for the module. - * @return an array of selected library metadata objects from the manifest. - * @throws BundleException if any problems arise. - */ - public R4Library[] getLibraries(BundleRevision revision) throws BundleException - { - R4LibraryClause clause = getSelectedLibraryClause(); - - if (clause != null) - { - R4Library[] libraries = new R4Library[clause.getLibraryFiles().length]; - for (int i = 0; i < libraries.length; i++) - { - libraries[i] = new R4Library( - m_logger, revision, clause.getLibraryFiles()[i], - clause.getOSNames(), clause.getProcessors(), clause.getOSVersions(), - clause.getLanguages(), clause.getSelectionFilter()); - } - return libraries; - } - return null; - } - - private R4LibraryClause getSelectedLibraryClause() throws BundleException - { - if ((m_libraryHeaders != null) && (m_libraryHeaders.length > 0)) - { - List clauseList = new ArrayList(); - - // Search for matching native clauses. - for (int i = 0; i < m_libraryHeaders.length; i++) - { - if (m_libraryHeaders[i].match(m_config)) - { - clauseList.add(m_libraryHeaders[i]); - } - } - - // Select the matching native clause. - int selected = 0; - if (clauseList.size() == 0) - { - // If optional clause exists, no error thrown. - if (m_libraryHeadersOptional) - { - return null; - } - else - { - throw new BundleException("Unable to select a native library clause."); - } - } - else if (clauseList.size() == 1) - { - selected = 0; - } - else if (clauseList.size() > 1) - { - selected = firstSortedClause(clauseList); - } - return ((R4LibraryClause) clauseList.get(selected)); - } - - return null; - } - - private int firstSortedClause(List clauseList) - { - ArrayList indexList = new ArrayList(); - ArrayList selection = new ArrayList(); - - // Init index list - for (int i = 0; i < clauseList.size(); i++) - { - indexList.add("" + i); - } - - // Select clause with 'osversion' range declared - // and get back the max floor of 'osversion' ranges. - Version osVersionRangeMaxFloor = new Version(0, 0, 0); - for (int i = 0; i < indexList.size(); i++) - { - int index = Integer.parseInt(indexList.get(i).toString()); - String[] osversions = ((R4LibraryClause) clauseList.get(index)).getOSVersions(); - if (osversions != null) - { - selection.add("" + indexList.get(i)); - } - for (int k = 0; (osversions != null) && (k < osversions.length); k++) - { - VersionRange range = VersionRange.parse(osversions[k]); - if ((range.getLow()).compareTo(osVersionRangeMaxFloor) >= 0) - { - osVersionRangeMaxFloor = range.getLow(); - } - } - } - - if (selection.size() == 1) - { - return Integer.parseInt(selection.get(0).toString()); - } - else if (selection.size() > 1) - { - // Keep only selected clauses with an 'osversion' - // equal to the max floor of 'osversion' ranges. - indexList = selection; - selection = new ArrayList(); - for (int i = 0; i < indexList.size(); i++) - { - int index = Integer.parseInt(indexList.get(i).toString()); - String[] osversions = ((R4LibraryClause) clauseList.get(index)).getOSVersions(); - for (int k = 0; k < osversions.length; k++) - { - VersionRange range = VersionRange.parse(osversions[k]); - if ((range.getLow()).compareTo(osVersionRangeMaxFloor) >= 0) - { - selection.add("" + indexList.get(i)); - } - } - } - } - - if (selection.size() == 0) - { - // Re-init index list. - selection.clear(); - indexList.clear(); - for (int i = 0; i < clauseList.size(); i++) - { - indexList.add("" + i); - } - } - else if (selection.size() == 1) - { - return Integer.parseInt(selection.get(0).toString()); - } - else - { - indexList = selection; - selection.clear(); - } - - // Keep only clauses with 'language' declared. - for (int i = 0; i < indexList.size(); i++) - { - int index = Integer.parseInt(indexList.get(i).toString()); - if (((R4LibraryClause) clauseList.get(index)).getLanguages() != null) - { - selection.add("" + indexList.get(i)); - } - } - - // Return the first sorted clause - if (selection.size() == 0) - { - return 0; - } - else - { - return Integer.parseInt(selection.get(0).toString()); - } - } - - private void checkAndNormalizeR3() throws BundleException - { - // Check to make sure that R3 bundles have only specified - // the 'specification-version' attribute and no directives - // on their exports. - for (int i = 0; (m_exports != null) && (i < m_exports.length); i++) - { - if (m_exports[i].getDirectives().length != 0) - { - throw new BundleException("R3 exports cannot contain directives."); - } - // NOTE: This is checking for "version" rather than "specification-version" - // because the package class normalizes to "version" to avoid having - // future special cases. This could be changed if more strict behavior - // is required. - if ((m_exports[i].getAttributes().length > 1) || - ((m_exports[i].getAttributes().length == 1) && - (!m_exports[i].getAttributes()[0].getName().equals(Constants.VERSION_ATTRIBUTE)))) - { - throw new BundleException( - "R3 export syntax does not support attributes: " + m_exports[i]); - } - } - - // Check to make sure that R3 bundles have only specified - // the 'specification-version' attribute and no directives - // on their imports. - for (int i = 0; (m_imports != null) && (i < m_imports.length); i++) - { - if (m_imports[i].getDirectives().length != 0) - { - throw new BundleException("R3 imports cannot contain directives."); - } - // NOTE: This is checking for "version" rather than "specification-version" - // because the package class normalizes to "version" to avoid having - // future special cases. This could be changed if more strict behavior - // is required. - if ((m_imports[i].getVersionHigh() != null) || - (m_imports[i].getAttributes().length > 1) || - ((m_imports[i].getAttributes().length == 1) && - (!m_imports[i].getAttributes()[0].getName().equals(Constants.VERSION_ATTRIBUTE)))) - { - throw new BundleException( - "R3 import syntax does not support attributes: " + m_imports[i]); - } - } - - // Since all R3 exports imply an import, add a corresponding - // import for each existing export. Create non-duplicated import array. - Map map = new HashMap(); - // Add existing imports. - for (int i = 0; i < m_imports.length; i++) - { - map.put(m_imports[i].getName(), m_imports[i]); - } - // Add import for each export. - for (int i = 0; i < m_exports.length; i++) - { - if (map.get(m_exports[i].getName()) == null) - { - map.put(m_exports[i].getName(), new R4Import(m_exports[i])); - } - } - m_imports = - (R4Import[]) map.values().toArray(new R4Import[map.size()]); - - // Add a "uses" directive onto each export of R3 bundles - // that references every other import (which will include - // exports, since export implies import); this is - // necessary since R3 bundles assumed a single class space, - // but R4 allows for multiple class spaces. - String usesValue = ""; - for (int i = 0; (m_imports != null) && (i < m_imports.length); i++) - { - usesValue = usesValue - + ((usesValue.length() > 0) ? "," : "") - + m_imports[i].getName(); - } - R4Directive uses = new R4Directive( - Constants.USES_DIRECTIVE, usesValue); - for (int i = 0; (m_exports != null) && (i < m_exports.length); i++) - { - m_exports[i] = new R4Export( - m_exports[i].getName(), - new R4Directive[] { uses }, - m_exports[i].getAttributes()); - } - - // Check to make sure that R3 bundles have no attributes or - // directives on their dynamic imports. - for (int i = 0; (m_dynamics != null) && (i < m_dynamics.length); i++) - { - if (m_dynamics[i].getDirectives().length != 0) - { - throw new BundleException("R3 dynamic imports cannot contain directives."); - } - if (m_dynamics[i].getAttributes().length != 0) - { - throw new BundleException("R3 dynamic imports cannot contain attributes."); - } - } - } - - private void checkAndNormalizeR4() throws BundleException - { - // Verify that bundle symbolic name is specified. - String symName = get(Constants.BUNDLE_SYMBOLICNAME); - if (symName == null) - { - throw new BundleException("R4 bundle manifests must include bundle symbolic name."); - } - - // Verify that the exports do not specify bundle symbolic name - // or bundle version. - for (int i = 0; (m_exports != null) && (i < m_exports.length); i++) - { - String targetVer = get(Constants.BUNDLE_VERSION); - targetVer = (targetVer == null) ? "0.0.0" : targetVer; - - R4Attribute[] attrs = m_exports[i].getAttributes(); - for (int attrIdx = 0; attrIdx < attrs.length; attrIdx++) - { - // Find symbolic name and version attribute, if present. - if (attrs[attrIdx].getName().equals(Constants.BUNDLE_VERSION_ATTRIBUTE) || - attrs[attrIdx].getName().equals(Constants.BUNDLE_SYMBOLICNAME_ATTRIBUTE)) - { - throw new BundleException( - "Exports must not specify bundle symbolic name or bundle version."); - } - } - - // Now that we know that there are no bundle symbolic name and version - // attributes, add them since the spec says they are there implicitly. - R4Attribute[] newAttrs = new R4Attribute[attrs.length + 2]; - System.arraycopy(attrs, 0, newAttrs, 0, attrs.length); - newAttrs[attrs.length] = new R4Attribute( - Constants.BUNDLE_SYMBOLICNAME_ATTRIBUTE, symName, false); - newAttrs[attrs.length + 1] = new R4Attribute( - Constants.BUNDLE_VERSION_ATTRIBUTE, Version.parseVersion(targetVer), false); - m_exports[i] = new R4Export( - m_exports[i].getName(), m_exports[i].getDirectives(), newAttrs); - } - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/MapToDictionary.java b/framework/src/main/java/org/apache/felix/framework/util/MapToDictionary.java index 7149adf8b22..7b955c0f6e3 100644 --- a/framework/src/main/java/org/apache/felix/framework/util/MapToDictionary.java +++ b/framework/src/main/java/org/apache/felix/framework/util/MapToDictionary.java @@ -1,18 +1,20 @@ -/* - * Copyright 2005 The Apache Software Foundation +/* + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework.util; @@ -21,7 +23,7 @@ /** * This is a simple class that implements a Dictionary - * from a Map. The resulting dictionary is immutatable. + * from a Map. The resulting dictionary is immutable. **/ public class MapToDictionary extends Dictionary { @@ -32,43 +34,31 @@ public class MapToDictionary extends Dictionary public MapToDictionary(Map map) { + if (map == null) + { + throw new IllegalArgumentException("Source map cannot be null."); + } m_map = map; } public Enumeration elements() { - if (m_map == null) - { - return null; - } - return new IteratorToEnumeration(m_map.values().iterator()); + return Collections.enumeration(m_map.values()); } public Object get(Object key) { - if (m_map == null) - { - return null; - } return m_map.get(key); } public boolean isEmpty() { - if (m_map == null) - { - return true; - } return m_map.isEmpty(); } public Enumeration keys() { - if (m_map == null) - { - return null; - } - return new IteratorToEnumeration(m_map.keySet().iterator()); + return Collections.enumeration(m_map.keySet()); } public Object put(Object key, Object value) @@ -83,10 +73,11 @@ public Object remove(Object key) public int size() { - if (m_map == null) - { - return 0; - } return m_map.size(); } + + public String toString() + { + return m_map.toString(); + } } \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/MultiReleaseContent.java b/framework/src/main/java/org/apache/felix/framework/util/MultiReleaseContent.java new file mode 100644 index 00000000000..00e84d9120b --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/util/MultiReleaseContent.java @@ -0,0 +1,186 @@ +/* + * 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.framework.util; + +import org.apache.felix.framework.cache.Content; +import org.osgi.framework.Version; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +public class MultiReleaseContent implements Content +{ + private final Content m_content; + private final int m_javaVersion; + + MultiReleaseContent(int javaVersion, Content content) + { + m_javaVersion = javaVersion; + m_content = content; + } + + public static Content wrap(String javaVersionString, Content content) + { + int javaVersion = 8; + + try + { + javaVersion = Version.parseVersion(javaVersionString).getMajor(); + } + catch (Exception ex) + { + + } + if (javaVersion > 8) + { + try + { + InputStream manifestStream = content.getEntryAsStream("META-INF/MANIFEST.MF"); + if (manifestStream != null) + { + if ("true".equals(new Manifest(manifestStream).getMainAttributes().getValue("Multi-Release"))) + { + content = new MultiReleaseContent(javaVersion, content); + } + } + } + catch (Exception ex) + { + } + } + return content; + } + + @Override + public void close() + { + m_content.close(); + } + + @Override + public boolean hasEntry(String name) + { + return m_content.hasEntry(findPath(name)); + } + + @Override + public Enumeration getEntries() + { + Enumeration entries = m_content.getEntries(); + if (entries != null) + { + Set result = new LinkedHashSet(); + while (entries.hasMoreElements()) + { + String path = entries.nextElement(); + result.add(path); + String internalPath = path; + while (internalPath.startsWith("/")) + { + internalPath = internalPath.substring(1); + } + if (internalPath.startsWith("META-INF/versions/") ) + { + int idx = internalPath.indexOf('/', "META-INF/versions/".length()); + if ((idx != -1) && (idx + 1) < internalPath.length()) + { + int version = Version.parseVersion(internalPath.substring("META-INF/versions/".length(), idx)).getMajor(); + if ((version > 8) && (version <= m_javaVersion)) + { + internalPath = internalPath.substring(idx + 1); + if (!internalPath.startsWith("META-INF/")) + { + result.add(internalPath); + } + } + } + } + } + return Collections.enumeration(result); + } + else + { + return entries; + } + } + + @Override + public byte[] getEntryAsBytes(String name) + { + return m_content.getEntryAsBytes(findPath(name)); + } + + @Override + public InputStream getEntryAsStream(String name) throws IOException + { + return m_content.getEntryAsStream(findPath(name)); + } + + @Override + public Content getEntryAsContent(String name) + { + return m_content.getEntryAsContent(findPath(name)); + } + + @Override + public String getEntryAsNativeLibrary(String name) + { + return m_content.getEntryAsNativeLibrary(findPath(name)); + } + + @Override + public URL getEntryAsURL(String name) + { + return m_content.getEntryAsURL(findPath(name)); + } + + private String findPath(String path) + { + String internalPath = path; + while (internalPath.startsWith("/")) + { + internalPath = internalPath.substring(1); + } + if (!internalPath.startsWith("META-INF/")) + { + int version = m_javaVersion; + while (version >= 9) + { + String versionPath = "META-INF/versions/" + version-- + "/" + internalPath; + if (m_content.hasEntry(versionPath)) + { + return versionPath; + } + } + } + return path; + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/util/MutablePropertyResolver.java b/framework/src/main/java/org/apache/felix/framework/util/MutablePropertyResolver.java deleted file mode 100644 index da9aa4b0949..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/util/MutablePropertyResolver.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.util; - -public interface MutablePropertyResolver extends PropertyResolver -{ - public String put(String key, String value); -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/MutablePropertyResolverImpl.java b/framework/src/main/java/org/apache/felix/framework/util/MutablePropertyResolverImpl.java deleted file mode 100644 index 5d01319cb3a..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/util/MutablePropertyResolverImpl.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.util; - -import java.util.Map; - -public class MutablePropertyResolverImpl implements MutablePropertyResolver -{ - private Map m_props = null; - - public MutablePropertyResolverImpl(Map props) - { - m_props = props; - } - - public synchronized String put(String key, String value) - { - return (String) m_props.put(key, value); - } - - public synchronized String get(String key) - { - return (String) m_props.get(key); - } - - public synchronized String[] getKeys() - { - return (String[]) m_props.keySet().toArray(new String[0]); - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/ObjectInputStreamX.java b/framework/src/main/java/org/apache/felix/framework/util/ObjectInputStreamX.java deleted file mode 100644 index b5a6f44263d..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/util/ObjectInputStreamX.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.util; - -import java.io.*; - -import org.apache.felix.moduleloader.IModule; - -/** - * The ObjectInputStreamX class is a simple extension to the ObjectInputStream - * class. The purpose of ObjectInputStreamX is to allow objects to be deserialized - * when their classes are not in the CLASSPATH (e.g., in a JAR file). - */ -public class ObjectInputStreamX extends ObjectInputStream -{ - private IModule m_module = null; - - /** - * Construct an ObjectInputStreamX for the specified InputStream and the specified - * ClassLoader. - * @param in the input stream to read. - * @param loader the class loader used to resolve classes. - */ - public ObjectInputStreamX(InputStream in, IModule module) - throws IOException, StreamCorruptedException - { - super(in); - m_module = module; - } - - /** - * By overloading this method, the ObjectInputStreamX can resolve a class - * from the class loader that was passed into it at construction time. - */ - protected Class resolveClass(ObjectStreamClass v) - throws IOException, ClassNotFoundException - { - return m_module.getClass(v.getName()); - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/PropertyResolver.java b/framework/src/main/java/org/apache/felix/framework/util/PropertyResolver.java deleted file mode 100644 index e7825ce3e1e..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/util/PropertyResolver.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.util; - -public interface PropertyResolver -{ - public String get(String key); - public String[] getKeys(); -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java b/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java index bdfcba79c7e..4a202a747ff 100644 --- a/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java +++ b/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java @@ -1,31 +1,46 @@ /* - * Copyright 2005 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework.util; import java.io.*; +import java.lang.reflect.*; +import java.lang.reflect.Proxy; import java.net.*; import java.security.*; +import java.util.Collection; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; import java.util.jar.JarFile; +import java.util.zip.ZipFile; -import org.apache.felix.framework.searchpolicy.ContentClassLoader; -import org.apache.felix.framework.searchpolicy.ContentLoaderImpl; -import org.apache.felix.moduleloader.JarFileX; +import org.osgi.framework.Bundle; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.hooks.resolver.ResolverHook; +import org.osgi.framework.hooks.service.ListenerHook; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; /** *

      @@ -55,7 +70,23 @@ public Object initialValue() public SecureAction() { - m_acc = AccessController.getContext(); + if (System.getSecurityManager() != null) + { + try + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.INITIALIZE_CONTEXT_ACTION, null); + m_acc = (AccessControlContext) AccessController.doPrivileged(actions); + } + catch (PrivilegedActionException ex) + { + throw (RuntimeException) ex.getException(); + } + } + else + { + m_acc = AccessController.getContext(); + } } public String getSystemProperty(String name, String def) @@ -79,14 +110,77 @@ public String getSystemProperty(String name, String def) } } - public Class forName(String name) throws ClassNotFoundException + public ClassLoader getParentClassLoader(ClassLoader loader) + { + if (System.getSecurityManager() != null) + { + try + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.GET_PARENT_CLASS_LOADER_ACTION, loader); + return (ClassLoader) AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException ex) + { + throw (RuntimeException) ex.getException(); + } + } + else + { + return loader.getParent(); + } + } + + public ClassLoader getSystemClassLoader() + { + if (System.getSecurityManager() != null) + { + try + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.GET_SYSTEM_CLASS_LOADER_ACTION); + return (ClassLoader) AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException ex) + { + throw (RuntimeException) ex.getException(); + } + } + else + { + return ClassLoader.getSystemClassLoader(); + } + } + + public ClassLoader getClassLoader(Class clazz) + { + if (System.getSecurityManager() != null) + { + try + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.GET_CLASS_LOADER_ACTION, clazz); + return (ClassLoader) AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException ex) + { + throw (RuntimeException) ex.getException(); + } + } + else + { + return clazz.getClassLoader(); + } + } + + public Class forName(String name, ClassLoader classloader) throws ClassNotFoundException { if (System.getSecurityManager() != null) { try { Actions actions = (Actions) m_actions.get(); - actions.set(Actions.FOR_NAME_ACTION, name); + actions.set(Actions.FOR_NAME_ACTION, name, classloader); return (Class) AccessController.doPrivileged(actions, m_acc); } catch (PrivilegedActionException ex) @@ -98,6 +192,10 @@ public Class forName(String name) throws ClassNotFoundException throw (RuntimeException) ex.getException(); } } + else if (classloader != null) + { + return Class.forName(name, true, classloader); + } else { return Class.forName(name); @@ -113,8 +211,8 @@ public URL createURL(String protocol, String host, try { Actions actions = (Actions) m_actions.get(); - actions.set(Actions.CREATE_URL_ACTION, protocol, host, port, - path, handler); + actions.set(Actions.CREATE_URL_ACTION, protocol, host, + new Integer(port), path, handler); return (URL) AccessController.doPrivileged(actions, m_acc); } catch (PrivilegedActionException ex) @@ -132,6 +230,54 @@ public URL createURL(String protocol, String host, } } + public URL createURL(URL context, String spec, URLStreamHandler handler) + throws MalformedURLException + { + if (System.getSecurityManager() != null) + { + try + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.CREATE_URL_WITH_CONTEXT_ACTION, context, + spec, handler); + return (URL) AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException ex) + { + if (ex.getException() instanceof MalformedURLException) + { + throw (MalformedURLException) ex.getException(); + } + throw (RuntimeException) ex.getException(); + } + } + else + { + return new URL(context, spec, handler); + } + } + + public Process exec(String command) throws IOException + { + if (System.getSecurityManager() != null) + { + try + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.EXEC_ACTION, command); + return (Process) AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException ex) + { + throw (RuntimeException) ex.getException(); + } + } + else + { + return Runtime.getRuntime().exec(command); + } + } + public String getAbsolutePath(File file) { if (System.getSecurityManager() != null) @@ -284,7 +430,7 @@ public boolean renameFile(File oldFile, File newFile) } } - public InputStream getFileInputStream(File file) throws IOException + public FileInputStream getFileInputStream(File file) throws IOException { if (System.getSecurityManager() != null) { @@ -292,7 +438,7 @@ public InputStream getFileInputStream(File file) throws IOException { Actions actions = (Actions) m_actions.get(); actions.set(Actions.GET_FILE_INPUT_ACTION, file); - return (InputStream) AccessController.doPrivileged(actions, m_acc); + return (FileInputStream) AccessController.doPrivileged(actions, m_acc); } catch (PrivilegedActionException ex) { @@ -309,7 +455,7 @@ public InputStream getFileInputStream(File file) throws IOException } } - public OutputStream getFileOutputStream(File file) throws IOException + public FileOutputStream getFileOutputStream(File file) throws IOException { if (System.getSecurityManager() != null) { @@ -317,7 +463,7 @@ public OutputStream getFileOutputStream(File file) throws IOException { Actions actions = (Actions) m_actions.get(); actions.set(Actions.GET_FILE_OUTPUT_ACTION, file); - return (OutputStream) AccessController.doPrivileged(actions, m_acc); + return (FileOutputStream) AccessController.doPrivileged(actions, m_acc); } catch (PrivilegedActionException ex) { @@ -334,6 +480,27 @@ public OutputStream getFileOutputStream(File file) throws IOException } } + public URI toURI(File file) + { + if (System.getSecurityManager() != null) + { + try + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.TO_URI_ACTION, file); + return (URI) AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException ex) + { + throw (RuntimeException) ex.getException(); + } + } + else + { + return file.toURI(); + } + } + public InputStream getURLConnectionInputStream(URLConnection conn) throws IOException { @@ -382,15 +549,42 @@ public boolean deleteFile(File target) } } - public JarFileX openJAR(File file) throws IOException + public File createTempFile(String prefix, String suffix, File dir) + throws IOException + { + if (System.getSecurityManager() != null) + { + try + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.CREATE_TMPFILE_ACTION, prefix, suffix, dir); + return (File) AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException ex) + { + if (ex.getException() instanceof IOException) + { + throw (IOException) ex.getException(); + } + throw (RuntimeException) ex.getException(); + } + } + else + { + return File.createTempFile(prefix, suffix, dir); + } + } + + public void deleteFileOnExit(File file) + throws IOException { if (System.getSecurityManager() != null) { try { Actions actions = (Actions) m_actions.get(); - actions.set(Actions.OPEN_JARX_ACTION, file); - return (JarFileX) AccessController.doPrivileged(actions, m_acc); + actions.set(Actions.DELETE_FILEONEXIT_ACTION, file); + AccessController.doPrivileged(actions, m_acc); } catch (PrivilegedActionException ex) { @@ -403,19 +597,20 @@ public JarFileX openJAR(File file) throws IOException } else { - return new JarFileX(file); + file.deleteOnExit(); } } - public JarFile openJAR(File file, boolean verify) throws IOException + public URLConnection openURLConnection(URL url) throws IOException { if (System.getSecurityManager() != null) { try { Actions actions = (Actions) m_actions.get(); - actions.set(Actions.OPEN_JAR_ACTION, file, (verify ? Boolean.TRUE : Boolean.FALSE)); - return (JarFile) AccessController.doPrivileged(actions, m_acc); + actions.set(Actions.OPEN_URLCONNECTION_ACTION, url); + return (URLConnection) AccessController.doPrivileged(actions, + m_acc); } catch (PrivilegedActionException ex) { @@ -428,34 +623,57 @@ public JarFile openJAR(File file, boolean verify) throws IOException } else { - return new JarFileX(file); + return url.openConnection(); } } - public ContentClassLoader createContentClassLoader(ContentLoaderImpl impl) + public ZipFile openZipFile(File file) throws IOException { - return createContentClassLoader(impl, null); + if (System.getSecurityManager() != null) + { + try + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.OPEN_ZIPFILE_ACTION, file); + return (ZipFile) AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException ex) + { + if (ex.getException() instanceof IOException) + { + throw (IOException) ex.getException(); + } + throw (RuntimeException) ex.getException(); + } + } + else + { + return new ZipFile(file); + } } - public ContentClassLoader createContentClassLoader(ContentLoaderImpl impl, - ProtectionDomain protectionDomain) + public JarFile openJarFile(File file) throws IOException { if (System.getSecurityManager() != null) { try { Actions actions = (Actions) m_actions.get(); - actions.set(Actions.CREATE_CONTENTCLASSLOADER_ACTION, impl, protectionDomain); - return (ContentClassLoader) AccessController.doPrivileged(actions, m_acc); + actions.set(Actions.OPEN_JARFILE_ACTION, file); + return (JarFile) AccessController.doPrivileged(actions, m_acc); } catch (PrivilegedActionException ex) { + if (ex.getException() instanceof IOException) + { + throw (IOException) ex.getException(); + } throw (RuntimeException) ex.getException(); } } else { - return new ContentClassLoader(impl, protectionDomain); + return new JarFile(file); } } @@ -503,232 +721,1160 @@ public void stopActivator(BundleActivator activator, BundleContext context) } } - public void exit(int code) + public Policy getPolicy() { if (System.getSecurityManager() != null) { try { Actions actions = (Actions) m_actions.get(); - actions.set(Actions.SYSTEM_EXIT_ACTION, new Integer(code)); - AccessController.doPrivileged(actions, m_acc); + actions.set(Actions.GET_POLICY_ACTION, null); + return (Policy) AccessController.doPrivileged(actions, m_acc); } catch (PrivilegedActionException ex) { - // We don't need to rethrow anything since System.exit throws - // runtime exceptions only + throw (RuntimeException) ex.getException(); } } else { - System.exit(code); + return Policy.getPolicy(); } } - public Policy getPolicy() + public void addURLToURLClassLoader(URL extension, ClassLoader loader) throws Exception { if (System.getSecurityManager() != null) { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.ADD_EXTENSION_URL_ACTION, extension, loader); try { - Actions actions = (Actions) m_actions.get(); - actions.set(Actions.GET_POLICY_ACTION, null); - return (Policy) AccessController.doPrivileged(actions, m_acc); + AccessController.doPrivileged(actions, m_acc); } - catch (PrivilegedActionException ex) + catch (PrivilegedActionException e) { - throw (RuntimeException) ex.getException(); + throw e.getException(); } } else { - return Policy.getPolicy(); + Method addURL = + URLClassLoader.class.getDeclaredMethod("addURL", + new Class[] {URL.class}); + addURL.setAccessible(true); + addURL.invoke(loader, new Object[]{extension}); } } - private static class Actions implements PrivilegedExceptionAction + public Constructor getConstructor(Class target, Class[] types) throws Exception { - public static final int GET_PROPERTY_ACTION = 0; - public static final int FOR_NAME_ACTION = 1; - public static final int CREATE_URL_ACTION = 2; - public static final int GET_ABSOLUTE_PATH_ACTION = 3; - public static final int FILE_EXISTS_ACTION = 4; - public static final int FILE_IS_DIRECTORY_ACTION = 5; - public static final int MAKE_DIRECTORY_ACTION = 6; - public static final int MAKE_DIRECTORIES_ACTION = 7; - public static final int LIST_DIRECTORY_ACTION = 8; - public static final int RENAME_FILE_ACTION = 9; - public static final int GET_FILE_INPUT_ACTION = 10; - public static final int GET_FILE_OUTPUT_ACTION = 11; - public static final int DELETE_FILE_ACTION = 12; - public static final int OPEN_JARX_ACTION = 13; - public static final int GET_URL_INPUT_ACTION = 14; - public static final int CREATE_CONTENTCLASSLOADER_ACTION = 15; - public static final int START_ACTIVATOR_ACTION = 16; - public static final int STOP_ACTIVATOR_ACTION = 17; - public static final int SYSTEM_EXIT_ACTION = 18; - public static final int OPEN_JAR_ACTION=19; - public static final int GET_POLICY_ACTION = 20; - - private int m_action = -1; - private Object m_arg1 = null; - private Object m_arg2 = null; - - private String m_protocol = null; - private String m_host = null; - private int m_port = -1; - private String m_path = null; - private URLStreamHandler m_handler = null; - - public void set(int action, Object arg1) + if (System.getSecurityManager() != null) { - m_action = action; - m_arg1 = arg1; - - m_arg2 = null; - m_protocol = null; - m_host = null; - m_port = -1; - m_path = null; - m_handler = null; + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.GET_CONSTRUCTOR_ACTION, target, types); + try + { + return (Constructor) AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } } - - public void set(int action, Object arg1, Object arg2) + else { - m_action = action; - m_arg1 = arg1; - m_arg2 = arg2; - - m_protocol = null; - m_host = null; - m_port = -1; - m_path = null; - m_handler = null; + return target.getConstructor(types); } + } - public void set(int action, String protocol, String host, - int port, String path, URLStreamHandler handler) + public Constructor getDeclaredConstructor(Class target, Class[] types) throws Exception + { + if (System.getSecurityManager() != null) { - m_action = action; - m_protocol = protocol; - m_host = host; - m_port = port; - m_path = path; - m_handler = handler; - - m_arg1 = null; - m_arg2 = null; + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.GET_DECLARED_CONSTRUCTOR_ACTION, target, types); + try + { + return (Constructor) AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + return target.getDeclaredConstructor(types); } + } - private void unset() + public Method getMethod(Class target, String method, Class[] types) throws Exception + { + if (System.getSecurityManager() != null) { - m_action = -1; - m_arg1 = null; - m_arg2 = null; - m_protocol = null; - m_host = null; - m_port = -1; - m_path = null; - m_handler = null; + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.GET_METHOD_ACTION, target, method, types); + try + { + return (Method) AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + return target.getMethod(method, types); } + } - public Object run() throws Exception + public Method getDeclaredMethod(Class target, String method, Class[] types) throws Exception + { + if (System.getSecurityManager() != null) { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.GET_DECLARED_METHOD_ACTION, target, method, types); try { - if (m_action == GET_PROPERTY_ACTION) - { - return System.getProperty((String) m_arg1, (String) m_arg2); - } - else if (m_action == FOR_NAME_ACTION) - { - return Class.forName((String) m_arg1); - } - else if (m_action == CREATE_URL_ACTION) - { - return new URL(m_protocol, m_host, m_port, m_path, m_handler); - } - else if (m_action == GET_ABSOLUTE_PATH_ACTION) - { - return ((File) m_arg1).getAbsolutePath(); - } - else if (m_action == FILE_EXISTS_ACTION) - { - return ((File) m_arg1).exists() ? Boolean.TRUE : Boolean.FALSE; - } - else if (m_action == FILE_IS_DIRECTORY_ACTION) - { - return ((File) m_arg1).isDirectory() ? Boolean.TRUE : Boolean.FALSE; - } - else if (m_action == MAKE_DIRECTORY_ACTION) - { - return ((File) m_arg1).mkdir() ? Boolean.TRUE : Boolean.FALSE; - } - else if (m_action == MAKE_DIRECTORIES_ACTION) - { - return ((File) m_arg1).mkdirs() ? Boolean.TRUE : Boolean.FALSE; - } - else if (m_action == LIST_DIRECTORY_ACTION) - { - return ((File) m_arg1).listFiles(); - } - else if (m_action == RENAME_FILE_ACTION) - { - return ((File) m_arg1).renameTo((File) m_arg2) ? Boolean.TRUE : Boolean.FALSE; - } - else if (m_action == GET_FILE_INPUT_ACTION) - { - return new FileInputStream((File) m_arg1); - } - else if (m_action == GET_FILE_OUTPUT_ACTION) - { - return new FileOutputStream((File) m_arg1); - } - else if (m_action == DELETE_FILE_ACTION) - { - return ((File) m_arg1).delete() ? Boolean.TRUE : Boolean.FALSE; - } - else if (m_action == OPEN_JARX_ACTION) - { - return new JarFileX((File) m_arg1); - } - else if (m_action == OPEN_JAR_ACTION) - { - return new JarFile((File) m_arg1, ((Boolean) m_arg2).booleanValue()); - } - else if (m_action == GET_URL_INPUT_ACTION) - { - return ((URLConnection) m_arg1).getInputStream(); - } - else if (m_action == CREATE_CONTENTCLASSLOADER_ACTION) - { - return new ContentClassLoader((ContentLoaderImpl) m_arg1, - (ProtectionDomain) m_arg2); - } - else if (m_action == START_ACTIVATOR_ACTION) - { - ((BundleActivator) m_arg1).start((BundleContext) m_arg2); - return null; - } - else if (m_action == STOP_ACTIVATOR_ACTION) + return (Method) AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + return target.getDeclaredMethod(method, types); + } + } + + public void setAccesssible(AccessibleObject ao) + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.SET_ACCESSIBLE_ACTION, ao); + try + { + AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw (RuntimeException) e.getException(); + } + } + else + { + ao.setAccessible(true); + } + } + + public Object invoke(Method method, Object target, Object[] params) throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.INVOKE_METHOD_ACTION, method, target, params); + try + { + return AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + method.setAccessible(true); + return method.invoke(target, params); + } + } + + public Object invokeDirect(Method method, Object target, Object[] params) throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.INVOKE_DIRECTMETHOD_ACTION, method, target, params); + try + { + return AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + return method.invoke(target, params); + } + } + + public Object invoke(Constructor constructor, Object[] params) throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.INVOKE_CONSTRUCTOR_ACTION, constructor, params); + try + { + return AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + return constructor.newInstance(params); + } + } + + public Object getDeclaredField(Class targetClass, String name, Object target) + throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.GET_FIELD_ACTION, targetClass, name, target); + try + { + return AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + Field field = targetClass.getDeclaredField(name); + field.setAccessible(true); + + return field.get(target); + } + } + + public Object swapStaticFieldIfNotClass(Class targetClazz, + Class targetType, Class condition, String lockName) throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.SWAP_FIELD_ACTION, targetClazz, targetType, + condition, lockName); + try + { + return AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + return _swapStaticFieldIfNotClass(targetClazz, targetType, + condition, lockName); + } + } + + private static Object _swapStaticFieldIfNotClass(Class targetClazz, + Class targetType, Class condition, String lockName) throws Exception + { + Object lock = null; + if (lockName != null) + { + try + { + Field lockField = + targetClazz.getDeclaredField(lockName); + lockField.setAccessible(true); + lock = lockField.get(null); + } + catch (NoSuchFieldException ex) + { + } + } + if (lock == null) + { + lock = targetClazz; + } + synchronized (lock) + { + Field[] fields = targetClazz.getDeclaredFields(); + + Object result = null; + for (int i = 0; (i < fields.length) && (result == null); i++) + { + if (Modifier.isStatic(fields[i].getModifiers()) && + (fields[i].getType() == targetType)) { - ((BundleActivator) m_arg1).stop((BundleContext) m_arg2); - return null; + fields[i].setAccessible(true); + + result = fields[i].get(null); + + if (result != null) + { + if ((condition == null) || + !result.getClass().getName().equals(condition.getName())) + { + fields[i].set(null, null); + } + } } - else if (m_action == SYSTEM_EXIT_ACTION) + } + if (result != null) + { + if ((condition == null) || !result.getClass().getName().equals(condition.getName())) { - System.exit(((Integer) m_arg1).intValue()); + // reset cache + for (int i = 0; i < fields.length; i++) + { + if (Modifier.isStatic(fields[i].getModifiers()) && + (fields[i].getType() == Hashtable.class)) + { + fields[i].setAccessible(true); + Hashtable cache = (Hashtable) fields[i].get(null); + if (cache != null) + { + cache.clear(); + } + } + } } - else if (m_action == GET_POLICY_ACTION) + return result; + } + } + return null; + } + + public void flush(Class targetClazz, Object lock) throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.FLUSH_FIELD_ACTION, targetClazz, lock); + try + { + AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + _flush(targetClazz, lock); + } + } + + private static void _flush(Class targetClazz, Object lock) throws Exception + { + synchronized (lock) + { + Field[] fields = targetClazz.getDeclaredFields(); + // reset cache + for (int i = 0; i < fields.length; i++) + { + if (Modifier.isStatic(fields[i].getModifiers()) && + ((fields[i].getType() == Hashtable.class) || (fields[i].getType() == HashMap.class))) { - return Policy.getPolicy(); + fields[i].setAccessible(true); + if (fields[i].getType() == Hashtable.class) + { + Hashtable cache = (Hashtable) fields[i].get(null); + if (cache != null) + { + cache.clear(); + } + } + else + { + HashMap cache = (HashMap) fields[i].get(null); + if (cache != null) + { + cache.clear(); + } + } } - return null; } - finally + } + } + + public void invokeBundleCollisionHook( + org.osgi.framework.hooks.bundle.CollisionHook ch, int operationType, + Bundle targetBundle, Collection collisionCandidates) + throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.INVOKE_BUNDLE_COLLISION_HOOK, ch, operationType, targetBundle, collisionCandidates); + try + { + AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + ch.filterCollisions(operationType, targetBundle, collisionCandidates); + } + } + + public void invokeBundleFindHook( + org.osgi.framework.hooks.bundle.FindHook fh, + BundleContext bc, Collection bundles) + throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.INVOKE_BUNDLE_FIND_HOOK, fh, bc, bundles); + try + { + AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + fh.find(bc, bundles); + } + } + + public void invokeBundleEventHook( + org.osgi.framework.hooks.bundle.EventHook eh, + BundleEvent event, Collection contexts) + throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.INVOKE_BUNDLE_EVENT_HOOK, eh, event, contexts); + try + { + AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + eh.event(event, contexts); + } + } + + public void invokeWeavingHook( + org.osgi.framework.hooks.weaving.WeavingHook wh, + org.osgi.framework.hooks.weaving.WovenClass wc) + throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.INVOKE_WEAVING_HOOK, wh, wc); + try + { + AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + wh.weave(wc); + } + } + + public void invokeServiceEventHook( + org.osgi.framework.hooks.service.EventHook eh, + ServiceEvent event, Collection contexts) + throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.INVOKE_SERVICE_EVENT_HOOK, eh, event, contexts); + try + { + AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + eh.event(event, contexts); + } + } + + public void invokeServiceFindHook( + org.osgi.framework.hooks.service.FindHook fh, + BundleContext context, String name, String filter, + boolean allServices, Collection> references) + throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set( + Actions.INVOKE_SERVICE_FIND_HOOK, fh, context, name, filter, + (allServices) ? Boolean.TRUE : Boolean.FALSE, references); + try { - unset(); + AccessController.doPrivileged(actions, m_acc); } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + fh.find(context, name, filter, allServices, references); + } + } + + public void invokeServiceListenerHookAdded( + org.osgi.framework.hooks.service.ListenerHook lh, + Collection listeners) + throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.INVOKE_SERVICE_LISTENER_HOOK_ADDED, lh, listeners); + try + { + AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + lh.added(listeners); + } + } + + public void invokeServiceListenerHookRemoved( + org.osgi.framework.hooks.service.ListenerHook lh, + Collection listeners) + throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.INVOKE_SERVICE_LISTENER_HOOK_REMOVED, lh, listeners); + try + { + AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + lh.removed(listeners); + } + } + + public void invokeServiceEventListenerHook( + org.osgi.framework.hooks.service.EventListenerHook elh, + ServiceEvent event, + Map> listeners) + throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.INVOKE_SERVICE_EVENT_LISTENER_HOOK, elh, event, listeners); + try + { + AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + elh.event(event, listeners); + } + } + + public ResolverHook invokeResolverHookFactory( + org.osgi.framework.hooks.resolver.ResolverHookFactory rhf, + Collection triggers) + throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.INVOKE_RESOLVER_HOOK_FACTORY, rhf, triggers); + try + { + return (ResolverHook) AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + return rhf.begin(triggers); + } + } + + public void invokeResolverHookResolvable( + org.osgi.framework.hooks.resolver.ResolverHook rh, + Collection candidates) + throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.INVOKE_RESOLVER_HOOK_RESOLVABLE, rh, candidates); + try + { + AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + rh.filterResolvable(candidates); + } + } + + public void invokeResolverHookSingleton( + org.osgi.framework.hooks.resolver.ResolverHook rh, + BundleCapability singleton, + Collection collisions) + throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.INVOKE_RESOLVER_HOOK_SINGLETON, rh, singleton, collisions); + try + { + AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + rh.filterSingletonCollisions(singleton, collisions); + } + } + + public void invokeResolverHookMatches( + org.osgi.framework.hooks.resolver.ResolverHook rh, + BundleRequirement req, + Collection candidates) + throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.INVOKE_RESOLVER_HOOK_MATCHES, rh, req, candidates); + try + { + AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + rh.filterMatches(req, candidates); + } + } + + public void invokeResolverHookEnd( + org.osgi.framework.hooks.resolver.ResolverHook rh) + throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.INVOKE_RESOLVER_HOOK_END, rh); + try + { + AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + rh.end(); + } + } + + public void invokeWovenClassListener( + org.osgi.framework.hooks.weaving.WovenClassListener wcl, + org.osgi.framework.hooks.weaving.WovenClass wc) + throws Exception + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.INVOKE_WOVEN_CLASS_LISTENER, wcl, wc); + try + { + AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + wcl.modified(wc); + } + } + + public T run(PrivilegedAction action) + { + if (System.getSecurityManager() != null) + { + return AccessController.doPrivileged(action); + } + else + { + return action.run(); + } + } + + public T run(PrivilegedExceptionAction action) throws Exception + { + if (System.getSecurityManager() != null) + { + try + { + return AccessController.doPrivileged(action); + } + catch (PrivilegedActionException e) + { + throw e.getException(); + } + } + else + { + return action.run(); + } + } + + public String getCanonicalPath(File dataFile) throws IOException + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.GET_CANONICAL_PATH, dataFile); + try + { + return (String) AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw (IOException) e.getException(); + } + } + else + { + return dataFile.getCanonicalPath(); + } + } + + public Object createProxy(ClassLoader classLoader, + Class[] interfaces, InvocationHandler handler) + { + if (System.getSecurityManager() != null) + { + Actions actions = (Actions) m_actions.get(); + actions.set(Actions.CREATE_PROXY, classLoader, interfaces, handler); + try + { + return AccessController.doPrivileged(actions, m_acc); + } + catch (PrivilegedActionException e) + { + throw (RuntimeException) e.getException(); + } + } + else + { + return Proxy.newProxyInstance(classLoader, interfaces, handler); + } + } + + private static class Actions implements PrivilegedExceptionAction + { + public static final int INITIALIZE_CONTEXT_ACTION = 0; + public static final int ADD_EXTENSION_URL_ACTION = 1; + public static final int CREATE_TMPFILE_ACTION = 2; + public static final int CREATE_URL_ACTION = 3; + public static final int CREATE_URL_WITH_CONTEXT_ACTION = 4; + public static final int DELETE_FILE_ACTION = 5; + public static final int EXEC_ACTION = 6; + public static final int FILE_EXISTS_ACTION = 7; + public static final int FILE_IS_DIRECTORY_ACTION = 8; + public static final int FOR_NAME_ACTION = 9; + public static final int GET_ABSOLUTE_PATH_ACTION = 10; + public static final int GET_CONSTRUCTOR_ACTION = 11; + public static final int GET_DECLARED_CONSTRUCTOR_ACTION = 12; + public static final int GET_DECLARED_METHOD_ACTION = 13; + public static final int GET_FIELD_ACTION = 14; + public static final int GET_FILE_INPUT_ACTION = 15; + public static final int GET_FILE_OUTPUT_ACTION = 16; + public static final int TO_URI_ACTION = 17; + public static final int GET_METHOD_ACTION = 18; + public static final int GET_POLICY_ACTION = 19; + public static final int GET_PROPERTY_ACTION = 20; + public static final int GET_PARENT_CLASS_LOADER_ACTION = 21; + public static final int GET_SYSTEM_CLASS_LOADER_ACTION = 22; + public static final int GET_URL_INPUT_ACTION = 23; + public static final int INVOKE_CONSTRUCTOR_ACTION = 24; + public static final int INVOKE_DIRECTMETHOD_ACTION = 25; + public static final int INVOKE_METHOD_ACTION = 26; + public static final int LIST_DIRECTORY_ACTION = 27; + public static final int MAKE_DIRECTORIES_ACTION = 28; + public static final int MAKE_DIRECTORY_ACTION = 29; + public static final int OPEN_ZIPFILE_ACTION = 30; + public static final int OPEN_URLCONNECTION_ACTION = 31; + public static final int RENAME_FILE_ACTION = 32; + public static final int SET_ACCESSIBLE_ACTION = 33; + public static final int START_ACTIVATOR_ACTION = 34; + public static final int STOP_ACTIVATOR_ACTION = 35; + public static final int SWAP_FIELD_ACTION = 36; + public static final int SYSTEM_EXIT_ACTION = 37; + public static final int FLUSH_FIELD_ACTION = 38; + public static final int GET_CLASS_LOADER_ACTION = 39; + public static final int INVOKE_BUNDLE_FIND_HOOK = 40; + public static final int INVOKE_BUNDLE_EVENT_HOOK = 41; + public static final int INVOKE_WEAVING_HOOK = 42; + public static final int INVOKE_SERVICE_EVENT_HOOK = 43; + public static final int INVOKE_SERVICE_FIND_HOOK = 44; + public static final int INVOKE_SERVICE_LISTENER_HOOK_ADDED = 45; + public static final int INVOKE_SERVICE_LISTENER_HOOK_REMOVED = 46; + public static final int INVOKE_SERVICE_EVENT_LISTENER_HOOK = 47; + public static final int INVOKE_RESOLVER_HOOK_FACTORY = 48; + public static final int INVOKE_RESOLVER_HOOK_RESOLVABLE = 49; + public static final int INVOKE_RESOLVER_HOOK_SINGLETON = 50; + public static final int INVOKE_RESOLVER_HOOK_MATCHES = 51; + public static final int INVOKE_RESOLVER_HOOK_END = 52; + public static final int INVOKE_BUNDLE_COLLISION_HOOK = 53; + public static final int OPEN_JARFILE_ACTION = 54; + public static final int DELETE_FILEONEXIT_ACTION = 55; + public static final int INVOKE_WOVEN_CLASS_LISTENER = 56; + public static final int GET_CANONICAL_PATH = 57; + public static final int CREATE_PROXY = 58; + + private int m_action = -1; + private Object m_arg1 = null; + private Object m_arg2 = null; + private Object m_arg3 = null; + private Object m_arg4 = null; + private Object m_arg5 = null; + private Object m_arg6 = null; + + public void set(int action) + { + m_action = action; + } + + public void set(int action, Object arg1) + { + m_action = action; + m_arg1 = arg1; + } + + public void set(int action, Object arg1, Object arg2) + { + m_action = action; + m_arg1 = arg1; + m_arg2 = arg2; + } + + public void set(int action, Object arg1, Object arg2, Object arg3) + { + m_action = action; + m_arg1 = arg1; + m_arg2 = arg2; + m_arg3 = arg3; + } + + public void set(int action, Object arg1, Object arg2, Object arg3, + Object arg4) + { + m_action = action; + m_arg1 = arg1; + m_arg2 = arg2; + m_arg3 = arg3; + m_arg4 = arg4; + } + + public void set(int action, Object arg1, Object arg2, Object arg3, + Object arg4, Object arg5) + { + m_action = action; + m_arg1 = arg1; + m_arg2 = arg2; + m_arg3 = arg3; + m_arg4 = arg4; + m_arg5 = arg5; + } + + public void set(int action, Object arg1, Object arg2, Object arg3, + Object arg4, Object arg5, Object arg6) + { + m_action = action; + m_arg1 = arg1; + m_arg2 = arg2; + m_arg3 = arg3; + m_arg4 = arg4; + m_arg5 = arg5; + m_arg6 = arg6; + } + + private void unset() + { + m_action = -1; + m_arg1 = null; + m_arg2 = null; + m_arg3 = null; + m_arg4 = null; + m_arg5 = null; + m_arg6 = null; + } + + public Object run() throws Exception + { + int action = m_action; + Object arg1 = m_arg1; + Object arg2 = m_arg2; + Object arg3 = m_arg3; + Object arg4 = m_arg4; + Object arg5 = m_arg5; + Object arg6 = m_arg6; + + unset(); + + switch (action) + { + case INITIALIZE_CONTEXT_ACTION: + return AccessController.getContext(); + case ADD_EXTENSION_URL_ACTION: + Method addURL = + URLClassLoader.class.getDeclaredMethod("addURL", + new Class[] {URL.class}); + addURL.setAccessible(true); + addURL.invoke(arg2, new Object[]{arg1}); + return null; + case CREATE_TMPFILE_ACTION: + return File.createTempFile((String) arg1, (String) arg2, (File) arg3); + case CREATE_URL_ACTION: + return new URL((String) arg1, (String) arg2, + ((Integer) arg3).intValue(), (String) arg4, + (URLStreamHandler) arg5); + case CREATE_URL_WITH_CONTEXT_ACTION: + return new URL((URL) arg1, (String) arg2, (URLStreamHandler) arg3); + case DELETE_FILE_ACTION: + return ((File) arg1).delete() ? Boolean.TRUE : Boolean.FALSE; + case EXEC_ACTION: + return Runtime.getRuntime().exec((String) arg1); + case FILE_EXISTS_ACTION: + return ((File) arg1).exists() ? Boolean.TRUE : Boolean.FALSE; + case FILE_IS_DIRECTORY_ACTION: + return ((File) arg1).isDirectory() ? Boolean.TRUE : Boolean.FALSE; + case FOR_NAME_ACTION: + return (arg2 == null) ? Class.forName((String) arg1) : Class.forName((String) arg1, true, + (ClassLoader) arg2); + case GET_ABSOLUTE_PATH_ACTION: + return ((File) arg1).getAbsolutePath(); + case GET_CONSTRUCTOR_ACTION: + return ((Class) arg1).getConstructor((Class[]) arg2); + case GET_DECLARED_CONSTRUCTOR_ACTION: + return ((Class) arg1).getDeclaredConstructor((Class[]) arg2); + case GET_DECLARED_METHOD_ACTION: + return ((Class) arg1).getDeclaredMethod((String) arg2, (Class[]) arg3); + case GET_FIELD_ACTION: + Field field = ((Class) arg1).getDeclaredField((String) arg2); + field.setAccessible(true); + return field.get(arg3); + case GET_FILE_INPUT_ACTION: + return new FileInputStream((File) arg1); + case GET_FILE_OUTPUT_ACTION: + return new FileOutputStream((File) arg1); + case TO_URI_ACTION: + return ((File) arg1).toURI(); + case GET_METHOD_ACTION: + return ((Class) arg1).getMethod((String) arg2, (Class[]) arg3); + case GET_POLICY_ACTION: + return Policy.getPolicy(); + case GET_PROPERTY_ACTION: + return System.getProperty((String) arg1, (String) arg2); + case GET_PARENT_CLASS_LOADER_ACTION: + return ((ClassLoader) arg1).getParent(); + case GET_SYSTEM_CLASS_LOADER_ACTION: + return ClassLoader.getSystemClassLoader(); + case GET_URL_INPUT_ACTION: + return ((URLConnection) arg1).getInputStream(); + case INVOKE_CONSTRUCTOR_ACTION: + return ((Constructor) arg1).newInstance((Object[]) arg2); + case INVOKE_DIRECTMETHOD_ACTION: + return ((Method) arg1).invoke(arg2, (Object[]) arg3); + case INVOKE_METHOD_ACTION: + ((Method) arg1).setAccessible(true); + return ((Method) arg1).invoke(arg2, (Object[]) arg3); + case LIST_DIRECTORY_ACTION: + return ((File) arg1).listFiles(); + case MAKE_DIRECTORIES_ACTION: + return ((File) arg1).mkdirs() ? Boolean.TRUE : Boolean.FALSE; + case MAKE_DIRECTORY_ACTION: + return ((File) arg1).mkdir() ? Boolean.TRUE : Boolean.FALSE; + case OPEN_ZIPFILE_ACTION: + return new ZipFile((File) arg1); + case OPEN_URLCONNECTION_ACTION: + return ((URL) arg1).openConnection(); + case RENAME_FILE_ACTION: + return ((File) arg1).renameTo((File) arg2) ? Boolean.TRUE : Boolean.FALSE; + case SET_ACCESSIBLE_ACTION: + ((AccessibleObject) arg1).setAccessible(true); + return null; + case START_ACTIVATOR_ACTION: + ((BundleActivator) arg1).start((BundleContext) arg2); + return null; + case STOP_ACTIVATOR_ACTION: + ((BundleActivator) arg1).stop((BundleContext) arg2); + return null; + case SWAP_FIELD_ACTION: + return _swapStaticFieldIfNotClass((Class) arg1, + (Class) arg2, (Class) arg3, (String) arg4); + case SYSTEM_EXIT_ACTION: + System.exit(((Integer) arg1).intValue()); + case FLUSH_FIELD_ACTION: + _flush(((Class) arg1), arg2); + return null; + case GET_CLASS_LOADER_ACTION: + return ((Class) arg1).getClassLoader(); + case INVOKE_BUNDLE_FIND_HOOK: + ((org.osgi.framework.hooks.bundle.FindHook) arg1).find( + (BundleContext) arg2, (Collection) arg3); + return null; + case INVOKE_BUNDLE_EVENT_HOOK: + ((org.osgi.framework.hooks.bundle.EventHook) arg1).event( + (BundleEvent) arg2, (Collection) arg3); + return null; + case INVOKE_WEAVING_HOOK: + ((org.osgi.framework.hooks.weaving.WeavingHook) arg1).weave( + (org.osgi.framework.hooks.weaving.WovenClass) arg2); + return null; + case INVOKE_SERVICE_EVENT_HOOK: + ((org.osgi.framework.hooks.service.EventHook) arg1).event( + (ServiceEvent) arg2, (Collection) arg3); + return null; + case INVOKE_SERVICE_FIND_HOOK: + ((org.osgi.framework.hooks.service.FindHook) arg1).find( + (BundleContext) arg2, (String) arg3, (String) arg4, + ((Boolean) arg5).booleanValue(), + (Collection>) arg6); + return null; + case INVOKE_SERVICE_LISTENER_HOOK_ADDED: + ((org.osgi.framework.hooks.service.ListenerHook) arg1).added( + (Collection) arg2); + return null; + case INVOKE_SERVICE_LISTENER_HOOK_REMOVED: + ((org.osgi.framework.hooks.service.ListenerHook) arg1).removed( + (Collection) arg2); + return null; + case INVOKE_SERVICE_EVENT_LISTENER_HOOK: + ((org.osgi.framework.hooks.service.EventListenerHook) arg1).event( + (ServiceEvent) arg2, + (Map>) arg3); + return null; + case INVOKE_RESOLVER_HOOK_FACTORY: + return ((org.osgi.framework.hooks.resolver.ResolverHookFactory) arg1).begin( + (Collection) arg2); + case INVOKE_RESOLVER_HOOK_RESOLVABLE: + ((org.osgi.framework.hooks.resolver.ResolverHook) arg1).filterResolvable( + (Collection) arg2); + return null; + case INVOKE_RESOLVER_HOOK_SINGLETON: + ((org.osgi.framework.hooks.resolver.ResolverHook) arg1) + .filterSingletonCollisions( + (BundleCapability) arg2, + (Collection) arg3); + return null; + case INVOKE_RESOLVER_HOOK_MATCHES: + ((org.osgi.framework.hooks.resolver.ResolverHook) arg1).filterMatches( + (BundleRequirement) arg2, + (Collection) arg3); + return null; + case INVOKE_RESOLVER_HOOK_END: + ((org.osgi.framework.hooks.resolver.ResolverHook) arg1).end(); + return null; + case INVOKE_BUNDLE_COLLISION_HOOK: + ((org.osgi.framework.hooks.bundle.CollisionHook) arg1).filterCollisions((Integer) arg2, + (Bundle) arg3, (Collection) arg4); + return null; + case OPEN_JARFILE_ACTION: + return new JarFile((File) arg1); + case DELETE_FILEONEXIT_ACTION: + ((File) arg1).deleteOnExit(); + return null; + case INVOKE_WOVEN_CLASS_LISTENER: + ((org.osgi.framework.hooks.weaving.WovenClassListener) arg1).modified( + (org.osgi.framework.hooks.weaving.WovenClass) arg2); + return null; + case GET_CANONICAL_PATH: + return ((File) arg1).getCanonicalPath(); + case CREATE_PROXY: + return Proxy.newProxyInstance((ClassLoader)arg1, (Class[])arg2, + (InvocationHandler) arg3); + } + + return null; } } } diff --git a/framework/src/main/java/org/apache/felix/framework/util/SecurityManagerEx.java b/framework/src/main/java/org/apache/felix/framework/util/SecurityManagerEx.java index 6b60e80adb7..77264719a70 100644 --- a/framework/src/main/java/org/apache/felix/framework/util/SecurityManagerEx.java +++ b/framework/src/main/java/org/apache/felix/framework/util/SecurityManagerEx.java @@ -1,18 +1,20 @@ -/* - * Copyright 2005 The Apache Software Foundation +/* + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework.util; @@ -24,8 +26,12 @@ **/ public class SecurityManagerEx extends SecurityManager { + // In Android apparently getClassContext returns null - we work around this by returning an empty array in that case. + private static final Class[] EMPTY_CLASSES = new Class[0]; + public Class[] getClassContext() { - return super.getClassContext(); + Class[] result = super.getClassContext(); + return result != null ? result : EMPTY_CLASSES; } } \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/ShrinkableCollection.java b/framework/src/main/java/org/apache/felix/framework/util/ShrinkableCollection.java new file mode 100644 index 00000000000..6a6ad6b9a7a --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/util/ShrinkableCollection.java @@ -0,0 +1,125 @@ +/* + * 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.framework.util; + +import java.util.Collection; +import java.util.Iterator; + +/** + * A collection wrapper that only permits clients to shrink the collection. +**/ +public class ShrinkableCollection implements Collection +{ + private final Collection m_delegate; + + public ShrinkableCollection(Collection delegate) + { + m_delegate = delegate; + } + + @Override + public boolean add(T o) + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection c) + { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() + { + m_delegate.clear(); + } + + @Override + public boolean contains(Object o) + { + return m_delegate.contains(o); + } + + @Override + public boolean containsAll(Collection c) + { + return m_delegate.containsAll(c); + } + + @Override + public boolean equals(Object o) + { + return m_delegate.equals(o); + } + + @Override + public int hashCode() + { + return m_delegate.hashCode(); + } + + @Override + public boolean isEmpty() + { + return m_delegate.isEmpty(); + } + + @Override + public Iterator iterator() + { + return m_delegate.iterator(); + } + + @Override + public boolean remove(Object o) + { + return m_delegate.remove(o); + } + + @Override + public boolean removeAll(Collection c) + { + return m_delegate.removeAll(c); + } + + @Override + public boolean retainAll(Collection c) + { + return m_delegate.retainAll(c); + } + + @Override + public int size() + { + return m_delegate.size(); + } + + @Override + public Object[] toArray() + { + return m_delegate.toArray(); + } + + @Override + public A[] toArray(A[] a) + { + return m_delegate.toArray(a); + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/ShrinkableMap.java b/framework/src/main/java/org/apache/felix/framework/util/ShrinkableMap.java new file mode 100644 index 00000000000..0923f1d77c4 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/util/ShrinkableMap.java @@ -0,0 +1,94 @@ +/* + * 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.framework.util; + +import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class ShrinkableMap implements Map +{ + private final Map m_delegate; + + public ShrinkableMap(Map delegate) + { + m_delegate = delegate; + } + + public int size() + { + return m_delegate.size(); + } + + public boolean isEmpty() + { + return m_delegate.isEmpty(); + } + + public boolean containsKey(Object o) + { + return m_delegate.containsKey(o); + } + + public boolean containsValue(Object o) + { + return m_delegate.containsValue(o); + } + + public V get(Object o) + { + return m_delegate.get(o); + } + + public V put(K k, V v) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public V remove(Object o) + { + return m_delegate.remove(o); + } + + public void putAll(Map map) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void clear() + { + m_delegate.clear(); + } + + public Set keySet() + { + return m_delegate.keySet(); + } + + public Collection values() + { + return m_delegate.values(); + } + + public Set> entrySet() + { + return m_delegate.entrySet(); + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/StringComparator.java b/framework/src/main/java/org/apache/felix/framework/util/StringComparator.java new file mode 100644 index 00000000000..fb2f6e1f0e1 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/util/StringComparator.java @@ -0,0 +1,73 @@ +/* + * 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.framework.util; + +import java.util.Comparator; + +public class StringComparator implements Comparator +{ + + public static final StringComparator COMPARATOR = new StringComparator(); + + public int compare(String s1, String s2) + { + int n1 = s1.length(); + int n2 = s2.length(); + int min = n1 < n2 ? n1 : n2; + for ( int i = 0; i < min; i++ ) + { + char c1 = s1.charAt( i ); + char c2 = s2.charAt( i ); + if ( c1 != c2 ) + { + // Fast check for simple ascii codes + if ( c1 <= 128 && c2 <= 128 ) + { + c1 = toLowerCaseFast(c1); + c2 = toLowerCaseFast(c2); + if ( c1 != c2 ) + { + return c1 - c2; + } + } + else + { + c1 = Character.toUpperCase( c1 ); + c2 = Character.toUpperCase( c2 ); + if ( c1 != c2 ) + { + c1 = Character.toLowerCase( c1 ); + c2 = Character.toLowerCase( c2 ); + if ( c1 != c2 ) + { + // No overflow because of numeric promotion + return c1 - c2; + } + } + } + } + } + return n1 - n2; + } + + private static char toLowerCaseFast( char ch ) + { + return ( ch >= 'A' && ch <= 'Z' ) ? ( char ) ( ch + 'a' - 'A' ) : ch; + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/StringMap.java b/framework/src/main/java/org/apache/felix/framework/util/StringMap.java index ae1aa90e1ef..45435146694 100644 --- a/framework/src/main/java/org/apache/felix/framework/util/StringMap.java +++ b/framework/src/main/java/org/apache/felix/framework/util/StringMap.java @@ -1,93 +1,49 @@ /* - * Copyright 2005 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework.util; -import java.util.*; +import java.util.Map; +import java.util.TreeMap; /** - * Simple utility class that creates a map for string-based keys by - * extending TreeMap. This map can be set to use case-sensitive - * or case-insensitive comparison when searching for the key. - * Any keys put into this map will be converted to - * a String using the toString() method, - * since it is only intended to compare strings. -**/ -public class StringMap extends TreeMap + * Simple utility class that creates a map for string-based keys. + * This map can be set to use case-sensitive or case-insensitive + * comparison when searching for the key. Any keys put into this + * map will be converted to a String using the + * toString() method, since it is only intended to + * compare strings. + **/ +public class StringMap extends TreeMap { - public StringMap() - { - this(true); - } - - public StringMap(boolean caseSensitive) - { - super(new StringComparator(caseSensitive)); - } - - public StringMap(Map map, boolean caseSensitive) - { - this(caseSensitive); - putAll(map); - } - - public Object put(Object key, Object value) - { - return super.put(key.toString(), value); - } - - public boolean isCaseSensitive() - { - return ((StringComparator) comparator()).isCaseSensitive(); - } - public void setCaseSensitive(boolean b) + public StringMap() { - ((StringComparator) comparator()).setCaseSensitive(b); + super(StringComparator.COMPARATOR); } - private static class StringComparator implements Comparator + public StringMap(Map map) { - private boolean m_isCaseSensitive = true; - - public StringComparator(boolean b) - { - m_isCaseSensitive = b; - } - - public int compare(Object o1, Object o2) - { - if (m_isCaseSensitive) - { - return o1.toString().compareTo(o2.toString()); - } - else - { - return o1.toString().compareToIgnoreCase(o2.toString()); - } - } - - public boolean isCaseSensitive() - { - return m_isCaseSensitive; - } - - public void setCaseSensitive(boolean b) + this(); + for (Map.Entry e : map.entrySet()) { - m_isCaseSensitive = b; + put(e.getKey().toString(), e.getValue()); } } -} \ No newline at end of file + +} diff --git a/framework/src/main/java/org/apache/felix/framework/util/ThreadGate.java b/framework/src/main/java/org/apache/felix/framework/util/ThreadGate.java new file mode 100644 index 00000000000..f9900a391e3 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/util/ThreadGate.java @@ -0,0 +1,91 @@ +/* + * 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.framework.util; + +/** + * This class implements a simple one-shot gate for threads. The gate + * starts closed and will block any threads that try to wait on it. Once + * opened, all waiting threads will be released. The gate cannot be reused. +**/ +public class ThreadGate +{ + private boolean m_open = false; + private Object m_msg = null; + private boolean m_initialized = false; + + /** + * Open the gate and release any waiting threads. + **/ + public synchronized void open() + { + m_open = true; + notifyAll(); + } + + /** + * Returns the message object associated with the gate; the + * message is just an arbitrary object used to pass information + * to the waiting threads. + * @return the message object associated with the gate. + **/ + public synchronized Object getMessage() + { + return m_msg; + } + + /** + * Sets the message object associated with the gate. The message + * object can only be set once, subsequent calls to this method + * are ignored. + * @param msg the message object to associate with this gate. + **/ + public synchronized void setMessage(Object msg) + { + if (!m_initialized) + { + m_msg = msg; + m_initialized = true; + } + } + + /** + * Wait for the gate to open. + * @return true if the gate was opened or false if the timeout expired. + * @throws java.lang.InterruptedException If the calling thread is interrupted; + * the gate still remains closed until opened. + **/ + public synchronized boolean await(long timeout) throws InterruptedException + { + long start = System.currentTimeMillis(); + long remaining = timeout; + while (!m_open) + { + wait(remaining); + if (timeout > 0) + { + remaining = timeout - (System.currentTimeMillis() - start); + if (remaining <= 0) + { + break; + } + } + } + return m_open; + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/Util.java b/framework/src/main/java/org/apache/felix/framework/util/Util.java index 4d840b58eea..f6acd36312a 100644 --- a/framework/src/main/java/org/apache/felix/framework/util/Util.java +++ b/framework/src/main/java/org/apache/felix/framework/util/Util.java @@ -1,38 +1,274 @@ /* - * Copyright 2006 The Apache Software Foundation + * 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 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * 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.framework.util; -import java.io.*; +import org.apache.felix.framework.Felix; +import org.apache.felix.framework.Logger; +import org.apache.felix.framework.cache.BundleArchiveRevision; +import org.apache.felix.framework.cache.BundleCache; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.Version; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.resource.Resource; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Method; +import java.net.URL; +import java.security.SecureRandom; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; - -import org.apache.felix.framework.Logger; -import org.apache.felix.framework.searchpolicy.*; -import org.apache.felix.moduleloader.IModule; -import org.apache.felix.moduleloader.IWire; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Random; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; public class Util { /** - * Converts a module identifier to a bundle identifier. Module IDs + * The default name used for the default configuration properties file. + **/ + private static final String DEFAULT_PROPERTIES_FILE = "default.properties"; + + public static Properties loadDefaultProperties(Logger logger) + { + Properties defaultProperties = new Properties(); + URL propURL = Util.class.getClassLoader().getResource(DEFAULT_PROPERTIES_FILE); + if (propURL != null) + { + InputStream is = null; + try + { + // Load properties from URL. + is = propURL.openConnection().getInputStream(); + defaultProperties.load(is); + is.close(); + } + catch (Exception ex) + { + // Try to close input stream if we have one. + try + { + if (is != null) is.close(); + } + catch (IOException ex2) + { + // Nothing we can do. + } + + logger.log( + Logger.LOG_ERROR, "Unable to load any configuration properties.", ex); + } + } + return defaultProperties; + } + + public static void initializeJPMSEE(String javaVersion, Properties properties, Logger logger) + { + try + { + Version version = new Version(javaVersion); + + if (version.getMajor() >= 9) + { + StringBuilder eecap = new StringBuilder(", osgi.ee; osgi.ee=\"OSGi/Minimum\"; version:List=\"1.0,1.1,1.2\",osgi.ee; osgi.ee=\"JavaSE\"; version:List=\"1.0,1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8,9"); + for (int i = 10; i <= version.getMajor();i++) + { + eecap.append(',').append(Integer.toString(i)); + } + eecap.append("\",osgi.ee; osgi.ee=\"JavaSE/compact1\"; version:List=\"1.8,9"); + for (int i = 10; i <= version.getMajor();i++) + { + eecap.append(',').append(Integer.toString(i)); + } + eecap.append("\",osgi.ee; osgi.ee=\"JavaSE/compact2\"; version:List=\"1.8,9"); + for (int i = 10; i <= version.getMajor();i++) + { + eecap.append(',').append(Integer.toString(i)); + } + eecap.append("\",osgi.ee; osgi.ee=\"JavaSE/compact3\"; version:List=\"1.8,9"); + for (int i = 10; i <= version.getMajor();i++) + { + eecap.append(',').append(Integer.toString(i)); + } + eecap.append("\""); + + StringBuilder ee = new StringBuilder(); + + for (int i = version.getMajor(); i > 9;i--) + { + ee.append("JavaSE-").append(Integer.toString(i)).append(','); + } + + ee.append("JavaSE-9,JavaSE-1.8,JavaSE-1.7,JavaSE-1.6,J2SE-1.5,J2SE-1.4,J2SE-1.3,J2SE-1.2,JRE-1.1,JRE-1.0,OSGi/Minimum-1.2,OSGi/Minimum-1.1,OSGi/Minimum-1.0"); + + properties.put("ee-jpms", ee.toString()); + + properties.put("eecap-jpms", eecap.toString()); + } + + properties.put("felix.detect.java.specification.version", version.getMajor() < 9 ? ("1." + (version.getMinor() > 6 ? version.getMinor() : 6)) : Integer.toString(version.getMajor())); + + if (version.getMajor() < 9) + { + properties.put("felix.detect.java.version", String.format("0.0.0.JavaSE_001_%03d", version.getMinor() > 6 ? version.getMinor() : 6)); + } + else + { + properties.put("felix.detect.java.version", String.format("0.0.0.JavaSE_%03d", version.getMajor())); + } + } + catch (Exception ex) + { + logger.log(Logger.LOG_ERROR, "Exception parsing java version", ex); + } + } + + public static Map> initializeJPMS(Properties properties) + { + Map> exports = null; + try + { + Class c_ModuleLayer = Felix.class.getClassLoader().loadClass("java.lang.ModuleLayer"); + Class c_Module = Felix.class.getClassLoader().loadClass("java.lang.Module"); + Class c_Descriptor = Felix.class.getClassLoader().loadClass("java.lang.module.ModuleDescriptor"); + Class c_Exports = Felix.class.getClassLoader().loadClass("java.lang.module.ModuleDescriptor$Exports"); + Method m_getLayer = c_Module.getMethod("getLayer"); + Method m_getModule = Class.class.getMethod("getModule"); + Method m_canRead = c_Module.getMethod("canRead", c_Module); + Method m_getName = c_Module.getMethod("getName"); + + Object self = m_getModule.invoke(Felix.class); + Object moduleLayer = m_getLayer.invoke(self); + + if (moduleLayer == null) + { + moduleLayer = c_ModuleLayer.getMethod("boot").invoke(null); + } + + Set modules = new TreeSet(); + exports = new HashMap>(); + for (Object module : ((Iterable) c_ModuleLayer.getMethod("modules").invoke(moduleLayer))) + { + if ((Boolean) m_canRead.invoke(self, module)) + { + Object name = m_getName.invoke(module); + properties.put("felix.detect.jpms." + name, name); + modules.add("felix.jpms." + name); + Set pkgs = new HashSet(); + + Object descriptor = c_Module.getMethod("getDescriptor").invoke(module); + + for (Object export :((Set) c_Descriptor.getMethod("exports").invoke(descriptor))) + { + if (((Set) c_Exports.getMethod("targets").invoke(export)).isEmpty()) + { + pkgs.add((String) c_Exports.getMethod("source").invoke(export)); + } + } + if (!pkgs.isEmpty()) + { + exports.put("felix.jpms." + c_Descriptor.getMethod("toNameAndVersion").invoke(descriptor), pkgs); + } + } + } + + properties.put("felix.detect.jpms", "jpms"); + String modulesString = ""; + for (String module : modules) { + modulesString += "${" + module + "}"; + } + properties.put("jre-jpms", modulesString); + } + catch (Exception ex) + { + // Not much we can do - probably not on java9 + } + return exports; + } + + public static String getPropertyWithSubs(Properties props, String name) + { + // Perform variable substitution for property. + String value = props.getProperty(name); + value = (value != null) + ? Util.substVars(value, name, null, props): null; + return value; + } + + public static Map getPropertiesWithPrefix(Properties props, String prefix) + { + Map result = new HashMap(); + + Set propertySet = props.stringPropertyNames(); + + for(String currentPropertyKey: propertySet) + { + if(currentPropertyKey.startsWith(prefix)) + { + String value = props.getProperty(currentPropertyKey); + // Perform variable substitution for property. + value = (value != null) + ? Util.substVars(value, currentPropertyKey, null, props): null; + result.put(currentPropertyKey, value); + } + } + return result; + } + + public static Properties toProperties(Map map) + { + Properties result = new Properties(); + for (Iterator iter = map.entrySet().iterator(); iter.hasNext();) + { + Entry entry = (Entry) iter.next(); + if (entry.getKey() != null && entry.getValue() != null) + { + result.setProperty(entry.getKey().toString(), entry.getValue().toString()); + } + } + return result; + } + + /** + * Converts a revision identifier to a bundle identifier. Revision IDs * are typically <bundle-id>.<revision>; this * method returns only the portion corresponding to the bundle ID. **/ - public static long getBundleIdFromModuleId(String id) + public static long getBundleIdFromRevisionId(String id) { try { @@ -55,14 +291,16 @@ public static int getModuleRevisionFromModuleId(String id) { try { - String rev = (id.indexOf('.') >= 0) - ? id.substring(id.indexOf('.') + 1) : id; - return Integer.parseInt(rev); + int index = id.indexOf('.'); + if (index >= 0) + { + return Integer.parseInt(id.substring(index + 1)); + } } catch (NumberFormatException ex) { - return -1; } + return -1; } public static String getClassName(String className) @@ -144,21 +382,21 @@ public static String getResourcePackage(String resource) * loaders of any interfaces it implements and the class loaders of * all super classes. *

      - * @param svcObj the class that is the root of the search. + * @param clazz the class that is the root of the search. * @param name the name of the class to load. * @return the loaded class or null if it could not be * loaded. **/ - public static Class loadClassUsingClass(Class clazz, String name) + public static Class loadClassUsingClass(Class clazz, String name, SecureAction action) { Class loadedClass = null; - + while (clazz != null) { // Get the class loader of the current class object. - ClassLoader loader = clazz.getClassLoader(); + ClassLoader loader = action.getClassLoader(clazz); // A null class loader represents the system class loader. - loader = (loader == null) ? ClassLoader.getSystemClassLoader() : loader; + loader = (loader == null) ? action.getSystemClassLoader() : loader; try { return loader.loadClass(name); @@ -167,159 +405,121 @@ public static Class loadClassUsingClass(Class clazz, String name) { // Ignore and try interface class loaders. } - + // Try to see if we can load the class from // one of the class's implemented interface // class loaders. Class[] ifcs = clazz.getInterfaces(); for (int i = 0; i < ifcs.length; i++) { - loadedClass = loadClassUsingClass(ifcs[i], name); + loadedClass = loadClassUsingClass(ifcs[i], name, action); if (loadedClass != null) { return loadedClass; } } - + // Try to see if we can load the class from // the super class class loader. clazz = clazz.getSuperclass(); } - + return null; } - public static R4Export getExportPackage(IModule m, String name) + /** + * This method determines if the requesting bundle is able to cast + * the specified service reference based on class visibility rules + * of the underlying modules. + * @param requester The bundle requesting the service. + * @param ref The service in question. + * @return true if the requesting bundle is able to case + * the service object to a known type. + **/ + public static boolean isServiceAssignable(Bundle requester, ServiceReference ref) { - R4Export[] pkgs = m.getDefinition().getExports(); - for (int i = 0; (pkgs != null) && (i < pkgs.length); i++) + // Boolean flag. + boolean allow = true; + // Get the service's objectClass property. + String[] objectClass = (String[]) ref.getProperty(FelixConstants.OBJECTCLASS); + + // The the service reference is not assignable when the requesting + // bundle is wired to a different version of the service object. + // NOTE: We are pessimistic here, if any class in the service's + // objectClass is not usable by the requesting bundle, then we + // disallow the service reference. + for (int classIdx = 0; (allow) && (classIdx < objectClass.length); classIdx++) { - if (pkgs[i].getName().equals(name)) + if (!ref.isAssignableTo(requester, objectClass[classIdx])) { - return pkgs[i]; + allow = false; } } - return null; + return allow; } - public static R4Import getImportPackage(IModule m, String name) + public static List getDynamicRequirements( + List reqs) { - R4Import[] pkgs = m.getDefinition().getImports(); - for (int i = 0; (pkgs != null) && (i < pkgs.length); i++) + List result = null; + if (reqs != null) { - if (pkgs[i].getName().equals(name)) + for (BundleRequirement req : reqs) { - return pkgs[i]; + String resolution = req.getDirectives().get(Constants.RESOLUTION_DIRECTIVE); + if ((resolution != null) && resolution.equals("dynamic")) + { + if (result == null) + { + result = new ArrayList(); + } + result.add(req); + } } } - return null; + return result; } - public static IWire getWire(IModule m, String name) + public static BundleWire getWire(BundleRevision br, String name) { - IWire[] wires = m.getWires(); - for (int i = 0; (wires != null) && (i < wires.length); i++) + if (br.getWiring() != null) { - if (wires[i].getExport().getName().equals(name)) + List wires = br.getWiring().getRequiredWires(null); + if (wires != null) { - return wires[i]; + for (BundleWire w : wires) + { + if (w.getCapability().getNamespace() + .equals(BundleRevision.PACKAGE_NAMESPACE) && + w.getCapability().getAttributes() + .get(BundleRevision.PACKAGE_NAMESPACE).equals(name)) + { + return w; + } + } } } return null; } - /** - * Parses delimited string and returns an array containing the tokens. This - * parser obeys quotes, so the delimiter character will be ignored if it is - * inside of a quote. This method assumes that the quote character is not - * included in the set of delimiter characters. - * @param value the delimited string to parse. - * @param delim the characters delimiting the tokens. - * @return an array of string tokens or null if there were no tokens. - **/ - public static String[] parseDelimitedString(String value, String delim) + public static BundleCapability getPackageCapability(BundleRevision br, String name) { - if (value == null) - { - value = ""; - } - - List list = new ArrayList(); - - int CHAR = 1; - int DELIMITER = 2; - int STARTQUOTE = 4; - int ENDQUOTE = 8; - - StringBuffer sb = new StringBuffer(); - - int expecting = (CHAR | DELIMITER | STARTQUOTE); - - for (int i = 0; i < value.length(); i++) + if (br.getWiring() != null) { - char c = value.charAt(i); - - boolean isDelimiter = (delim.indexOf(c) >= 0); - boolean isQuote = (c == '"'); - - if (isDelimiter && ((expecting & DELIMITER) > 0)) - { - list.add(sb.toString().trim()); - sb.delete(0, sb.length()); - expecting = (CHAR | DELIMITER | STARTQUOTE); - } - else if (isQuote && ((expecting & STARTQUOTE) > 0)) + List capabilities = br.getWiring().getCapabilities(null); + if (capabilities != null) { - sb.append(c); - expecting = CHAR | ENDQUOTE; - } - else if (isQuote && ((expecting & ENDQUOTE) > 0)) - { - sb.append(c); - expecting = (CHAR | STARTQUOTE | DELIMITER); - } - else if ((expecting & CHAR) > 0) - { - sb.append(c); - } - else - { - throw new IllegalArgumentException("Invalid delimited string: " + value); + for (BundleCapability c : capabilities) + { + if (c.getNamespace().equals(BundleRevision.PACKAGE_NAMESPACE) + && c.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE).equals(name)) + { + return c; + } + } } } - - if (sb.length() > 0) - { - list.add(sb.toString().trim()); - } - - return (String[]) list.toArray(new String[list.size()]); - } - - /** - * Parses native code manifest headers. - * @param libStrs an array of native library manifest header - * strings from the bundle manifest. - * @return an array of LibraryInfo objects for the - * passed in strings. - **/ - public static R4LibraryClause[] parseLibraryStrings(Logger logger, String[] libStrs) - throws IllegalArgumentException - { - if (libStrs == null) - { - return new R4LibraryClause[0]; - } - - List libList = new ArrayList(); - - for (int i = 0; i < libStrs.length; i++) - { - R4LibraryClause clause = R4LibraryClause.parse(logger, libStrs[i]); - libList.add(clause); - } - - return (R4LibraryClause[]) libList.toArray(new R4LibraryClause[libList.size()]); + return null; } private static final byte encTab[] = { 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, @@ -345,7 +545,7 @@ public static String base64Encode(String s) throws IOException /** * Encode a raw byte array to a Base64 String. - * + * * @param in Byte array to encode. * @param len Length of Base64 lines. 0 means no line breaks. **/ @@ -433,4 +633,405 @@ public static void encode(InputStream in, OutputStream out, int len) out.write(0x0a); } } + + + private static final String DELIM_START = "${"; + private static final String DELIM_STOP = "}"; + + /** + *

      + * This method performs property variable substitution on the + * specified value. If the specified value contains the syntax + * ${<prop-name>}, where <prop-name> + * refers to either a configuration property or a system property, + * then the corresponding property value is substituted for the variable + * placeholder. Multiple variable placeholders may exist in the + * specified value as well as nested variable placeholders, which + * are substituted from inner most to outer most. Configuration + * properties override system properties. + *

      + * @param val The string on which to perform property substitution. + * @param currentKey The key of the property being evaluated used to + * detect cycles. + * @param cycleMap Map of variable references used to detect nested cycles. + * @param configProps Set of configuration properties. + * @return The value of the specified string after system property substitution. + * @throws IllegalArgumentException If there was a syntax error in the + * property placeholder syntax or a recursive variable reference. + **/ + public static String substVars(String val, String currentKey, + Map cycleMap, Properties configProps) + throws IllegalArgumentException + { + // If there is currently no cycle map, then create + // one for detecting cycles for this invocation. + if (cycleMap == null) + { + cycleMap = new HashMap(); + } + + // Put the current key in the cycle map. + cycleMap.put(currentKey, currentKey); + + // Assume we have a value that is something like: + // "leading ${foo.${bar}} middle ${baz} trailing" + + // Find the first ending '}' variable delimiter, which + // will correspond to the first deepest nested variable + // placeholder. + int stopDelim = -1; + int startDelim = -1; + + do + { + stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1); + // If there is no stopping delimiter, then just return + // the value since there is no variable declared. + if (stopDelim < 0) + { + return val; + } + // Try to find the matching start delimiter by + // looping until we find a start delimiter that is + // greater than the stop delimiter we have found. + startDelim = val.indexOf(DELIM_START); + // If there is no starting delimiter, then just return + // the value since there is no variable declared. + if (startDelim < 0) + { + return val; + } + while (stopDelim >= 0) + { + int idx = val.indexOf(DELIM_START, startDelim + DELIM_START.length()); + if ((idx < 0) || (idx > stopDelim)) + { + break; + } + else if (idx < stopDelim) + { + startDelim = idx; + } + } + } + while ((startDelim > stopDelim) && (stopDelim >= 0)); + + // At this point, we have found a variable placeholder so + // we must perform a variable substitution on it. + // Using the start and stop delimiter indices, extract + // the first, deepest nested variable placeholder. + String variable = + val.substring(startDelim + DELIM_START.length(), stopDelim); + + // Verify that this is not a recursive variable reference. + if (cycleMap.get(variable) != null) + { + throw new IllegalArgumentException( + "recursive variable reference: " + variable); + } + + // Get the value of the deepest nested variable placeholder. + // Try to configuration properties first. + String substValue = (configProps != null) + ? configProps.getProperty(variable, null) + : null; + if (substValue == null) + { + // Ignore unknown property values. + substValue = System.getProperty(variable, ""); + } + + // Remove the found variable from the cycle map, since + // it may appear more than once in the value and we don't + // want such situations to appear as a recursive reference. + cycleMap.remove(variable); + + // Append the leading characters, the substituted value of + // the variable, and the trailing characters to get the new + // value. + val = val.substring(0, startDelim) + + substValue + + val.substring(stopDelim + DELIM_STOP.length(), val.length()); + + // Now perform substitution again, since there could still + // be substitutions to make. + val = substVars(val, currentKey, cycleMap, configProps); + + // Return the value. + return val; + } + + /** + * Returns true if the specified bundle revision is a singleton + * (i.e., directive singleton:=true in Bundle-SymbolicName). + * + * @param revision the revision to check for singleton status. + * @return true if the revision is a singleton, false otherwise. + **/ + public static boolean isSingleton(BundleRevision revision) + { + final List caps = revision.getDeclaredCapabilities(null); + for (BundleCapability cap : caps) + { + // Find the bundle capability and check its directives. + if (cap.getNamespace().equals(BundleRevision.BUNDLE_NAMESPACE)) + { + for (Entry entry : cap.getDirectives().entrySet()) + { + if (entry.getKey().equalsIgnoreCase(Constants.SINGLETON_DIRECTIVE)) + { + return Boolean.valueOf(entry.getValue()); + } + } + // Can only have one bundle capability, so break. + break; + } + } + return false; + } + + /** + * Checks if the provided module definition declares a fragment host. + * + * @param revision the module to check + * @return true if the module declares a fragment host, false + * otherwise. + */ + public static boolean isFragment(BundleRevision revision) + { + return ((revision.getTypes() & BundleRevision.TYPE_FRAGMENT) > 0); + } + + public static boolean isFragment(Resource resource) + { + if (resource instanceof BundleRevision) + return isFragment((BundleRevision) resource); + else + return false; + } + + public static List getFragments(BundleWiring wiring) + { + List fragments = Collections.EMPTY_LIST; + if (wiring != null) + { + List wires = wiring.getProvidedWires(null); + if (wires != null) + { + for (BundleWire w : wires) + { + if (w.getCapability().getNamespace() + .equals(BundleRevision.HOST_NAMESPACE)) + { + // Create array list if needed. + if (fragments.isEmpty()) + { + fragments = new ArrayList(); + } + fragments.add(w.getRequirerWiring().getRevision()); + } + } + } + } + return fragments; + } + + // + // UUID code copied from Apache Harmony java.util.UUID + // + + /** + *

      + * Generates a variant 2, version 4 (randomly generated number) UUID as per + * RFC 4122. + * + * @return an UUID instance. + */ + public static String randomUUID(boolean secure) { + byte[] data = new byte[16]; + if (secure) + { + SecureRandom rng = new SecureRandom(); + rng.nextBytes(data); + } + else + { + Random rng = new Random(); + rng.nextBytes(data); + } + long mostSigBits = (data[0] & 0xFFL) << 56; + mostSigBits |= (data[1] & 0xFFL) << 48; + mostSigBits |= (data[2] & 0xFFL) << 40; + mostSigBits |= (data[3] & 0xFFL) << 32; + mostSigBits |= (data[4] & 0xFFL) << 24; + mostSigBits |= (data[5] & 0xFFL) << 16; + mostSigBits |= (data[6] & 0x0FL) << 8; + mostSigBits |= (0x4L << 12); // set the version to 4 + mostSigBits |= (data[7] & 0xFFL); + + long leastSigBits = (data[8] & 0x3FL) << 56; + leastSigBits |= (0x2L << 62); // set the variant to bits 01 + leastSigBits |= (data[9] & 0xFFL) << 48; + leastSigBits |= (data[10] & 0xFFL) << 40; + leastSigBits |= (data[11] & 0xFFL) << 32; + leastSigBits |= (data[12] & 0xFFL) << 24; + leastSigBits |= (data[13] & 0xFFL) << 16; + leastSigBits |= (data[14] & 0xFFL) << 8; + leastSigBits |= (data[15] & 0xFFL); + + // + // UUID.init() + // + + int variant; + int version; + long timestamp; + int clockSequence; + long node; + int hash; + + // setup hash field + int msbHash = (int) (mostSigBits ^ (mostSigBits >>> 32)); + int lsbHash = (int) (leastSigBits ^ (leastSigBits >>> 32)); + hash = msbHash ^ lsbHash; + + // setup variant field + if ((leastSigBits & 0x8000000000000000L) == 0) { + // MSB0 not set, NCS backwards compatibility variant + variant = 0; + } else if ((leastSigBits & 0x4000000000000000L) != 0) { + // MSB1 set, either MS reserved or future reserved + variant = (int) ((leastSigBits & 0xE000000000000000L) >>> 61); + } else { + // MSB1 not set, RFC 4122 variant + variant = 2; + } + + // setup version field + version = (int) ((mostSigBits & 0x000000000000F000) >>> 12); + + if (!(variant != 2 && version != 1)) { + // setup timestamp field + long timeLow = (mostSigBits & 0xFFFFFFFF00000000L) >>> 32; + long timeMid = (mostSigBits & 0x00000000FFFF0000L) << 16; + long timeHigh = (mostSigBits & 0x0000000000000FFFL) << 48; + timestamp = timeLow | timeMid | timeHigh; + + // setup clock sequence field + clockSequence = (int) ((leastSigBits & 0x3FFF000000000000L) >>> 48); + + // setup node field + node = (leastSigBits & 0x0000FFFFFFFFFFFFL); + } + + // + // UUID.toString() + // + + StringBuilder builder = new StringBuilder(36); + String msbStr = Long.toHexString(mostSigBits); + if (msbStr.length() < 16) { + int diff = 16 - msbStr.length(); + for (int i = 0; i < diff; i++) { + builder.append('0'); + } + } + builder.append(msbStr); + builder.insert(8, '-'); + builder.insert(13, '-'); + builder.append('-'); + String lsbStr = Long.toHexString(leastSigBits); + if (lsbStr.length() < 16) { + int diff = 16 - lsbStr.length(); + for (int i = 0; i < diff; i++) { + builder.append('0'); + } + } + builder.append(lsbStr); + builder.insert(23, '-'); + return builder.toString(); + } + + public static V putIfAbsentAndReturn(ConcurrentHashMap map, K key, V value) + { + V result = map.putIfAbsent(key, value); + return result != null ? result : value; + } + + public static String getFrameworkUUIDFromURL(String host) + { + if (host != null) + { + int idx = host.indexOf('_'); + if (idx > 0) + { + return host.substring(0, idx); + } + } + return null; + } + + public static String getRevisionIdFromURL(String host) + { + if (host != null) + { + int idx = host.indexOf('_'); + if (idx > 0 && idx < host.length()) + { + return host.substring(idx + 1); + } + else + { + return host; + } + } + return null; + } + + public static Map getMultiReleaseAwareManifestHeaders(String version, BundleArchiveRevision revision) throws Exception + { + Map manifest = revision.getManifestHeader(); + if (manifest == null) + { + throw new FileNotFoundException("META-INF/MANIFEST.MF"); + } + if ("true".equals(manifest.get("Multi-Release"))) + { + for (int major = Version.parseVersion(version).getMajor(); major >= 9; major--) + { + byte[] versionManifestInput = revision.getContent() + .getEntryAsBytes("META-INF/versions/" + major + "/OSGI-INF/MANIFEST.MF"); + + if (versionManifestInput != null) + { + Map versionManifest = BundleCache.getMainAttributes( + new StringMap(), new ByteArrayInputStream(versionManifestInput), versionManifestInput.length); + + if (versionManifest.get(Constants.IMPORT_PACKAGE) != null) + { + manifest.put(Constants.IMPORT_PACKAGE, versionManifest.get(Constants.IMPORT_PACKAGE)); + } + if (versionManifest.get(Constants.REQUIRE_CAPABILITY) != null) + { + manifest.put(Constants.REQUIRE_CAPABILITY, versionManifest.get(Constants.REQUIRE_CAPABILITY)); + } + break; + } + } + } + return manifest; + } + + private static final List EMPTY_LIST = Collections.unmodifiableList(Collections.EMPTY_LIST); + private static final Map EMPTY_MAP = Collections.unmodifiableMap(Collections.EMPTY_MAP); + + public static List newImmutableList(List list) + { + return list == null || list.isEmpty() ? EMPTY_LIST : Collections.unmodifiableList(list); + } + + public static Map newImmutableMap(Map map) + { + return map == null || map.isEmpty() ? EMPTY_MAP : Collections.unmodifiableMap(map); + } } \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/WeakZipFileFactory.java b/framework/src/main/java/org/apache/felix/framework/util/WeakZipFileFactory.java new file mode 100644 index 00000000000..3e13c3e7e78 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/util/WeakZipFileFactory.java @@ -0,0 +1,737 @@ +/* + * 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.framework.util; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Semaphore; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * This class implements a factory for creating weak zip files, which behave + * mostly like a ZipFile, but can be weakly closed to limit the number of + * open files. + */ +public class WeakZipFileFactory +{ + private static final int WEAKLY_CLOSED = 0; + private static final int OPEN = 1; + private static final int CLOSED = 2; + + private static final SecureAction m_secureAction = new SecureAction(); + + private final List m_zipFiles = new ArrayList(); + private final List m_openFiles = new ArrayList(); + private final Lock m_globalMutex = new ReentrantLock(); + private final int m_limit; + + /** + * Constructs a weak zip file factory with the specified file limit. A limit + * of zero signifies no limit. + * @param limit maximum number of open zip files at any given time. + */ + public WeakZipFileFactory(int limit) + { + if (limit < 0) + { + throw new IllegalArgumentException("Limit must be non-negative."); + } + m_limit = limit; + } + + /** + * Factory method used to create weak zip files. + * @param file the target zip file. + * @return the created weak zip file. + * @throws IOException if the zip file could not be opened. + */ + public WeakZipFile create(File file) throws IOException + { + WeakZipFile wzf = new WeakZipFile(file); + + if (m_limit > 0) + { + m_globalMutex.lock(); + + try + { + m_zipFiles.add(wzf); + m_openFiles.add(wzf); + if (m_openFiles.size() > m_limit) + { + WeakZipFile candidate = m_openFiles.get(0); + for (WeakZipFile tmp : m_openFiles) + { + if (candidate.m_timestamp > tmp.m_timestamp) + { + candidate = tmp; + } + } + candidate._closeWeakly(); + } + } + finally + { + m_globalMutex.unlock(); + } + } + + return wzf; + } + + /** + * Only used for testing. + * @return unclosed weak zip files. + **/ + List getZipZiles() + { + m_globalMutex.lock(); + + try + { + return m_zipFiles; + } + finally + { + m_globalMutex.unlock(); + } + } + + /** + * Only used for testing. + * @return open weak zip files. + **/ + List getOpenZipZiles() + { + m_globalMutex.lock(); + + try + { + return m_openFiles; + } + finally + { + m_globalMutex.unlock(); + } + } + + /** + * This class wraps a ZipFile to making it possible to weakly close it; + * this means the underlying zip file will be automatically reopened on demand + * if anyone tries to use it. + */ + public class WeakZipFile + { + private final File m_file; + private final Lock m_localMutex = new ReentrantLock(false); + private volatile ZipFile m_zipFile; + private volatile int m_status = OPEN; + private volatile long m_timestamp; + private volatile SoftReference> m_entries; + + /** + * Constructor is private since instances need to be centrally + * managed. + * @param file the target zip file. + * @throws IOException if the zip file could not be opened. + */ + private WeakZipFile(File file) throws IOException + { + m_file = file; + m_zipFile = m_secureAction.openZipFile(m_file); + m_timestamp = System.currentTimeMillis(); + } + + /** + * Returns the specified entry from the zip file. + * @param name the name of the entry to return. + * @return the zip entry associated with the specified name or null + * if it does not exist. + */ + public ZipEntry getEntry(String name) + { + ensureZipFileIsOpen(); + + try + { + LinkedHashMap entries = getEntries(false); + ZipEntry ze; + if (entries != null) + { + ze = entries.get(name); + if (ze == null) + { + ze = entries.get(name + "/"); + } + } + else + { + ze = m_zipFile.getEntry(name); + } + + if ((ze != null) && (ze.getSize() == 0) && !ze.isDirectory()) + { + //The attempts to fix an apparent bug in the JVM in versions + // 1.4.2 and lower where directory entries in ZIP/JAR files are + // not correctly identified. + ZipEntry dirEntry = m_zipFile.getEntry(name + '/'); + if (dirEntry != null) + { + ze = dirEntry; + } + } + return ze; + } + finally + { + if (m_limit > 0) + { + m_localMutex.unlock(); + } + } + } + + /** + * Returns an enumeration of zip entries from the zip file. + * @return an enumeration of zip entries. + */ + public Enumeration entries() + { + ensureZipFileIsOpen(); + + try + { + LinkedHashMap entries = getEntries(true); + return Collections.enumeration(entries.values()); + } + finally + { + if (m_limit > 0) + { + m_localMutex.unlock(); + } + } + } + + public Enumeration names() + { + ensureZipFileIsOpen(); + + try + { + LinkedHashMap entries = getEntries(true); + return Collections.enumeration(entries.keySet()); + } + finally + { + if (m_limit > 0) + { + m_localMutex.unlock(); + } + } + } + + private LinkedHashMap getEntries(boolean create) + { + LinkedHashMap entries = null; + if (m_entries != null) + { + entries = m_entries.get(); + } + if (entries == null && create) + { + synchronized (m_zipFile) + { + if (m_entries != null) + { + entries = m_entries.get(); + } + if (entries == null) + { + // We need to suck in all of the entries since the zip + // file may get weakly closed during iteration. Technically, + // this may not be 100% correct either since if the zip file + // gets weakly closed and reopened, then the zip entries + // will be from a different zip file. It is not clear if this + // will cause any issues. + Enumeration e = m_zipFile.entries(); + entries = new LinkedHashMap(); + while (e.hasMoreElements()) + { + ZipEntry entry = e.nextElement(); + entries.put(entry.getName(), entry); + } + m_entries = new SoftReference>(entries); + } + } + } + return entries; + } + + /** + * Returns an input stream for the specified zip entry. + * @param ze the zip entry whose input stream is to be retrieved. + * @return an input stream to the zip entry. + * @throws IOException if the input stream cannot be opened. + */ + public InputStream getInputStream(ZipEntry ze) throws IOException + { + ensureZipFileIsOpen(); + + try + { + InputStream is = m_zipFile.getInputStream(ze); + return m_limit == 0 ? is : new WeakZipInputStream(ze.getName(), is); + } + finally + { + if (m_limit > 0) + { + m_localMutex.unlock(); + } + } + } + + /** + * Weakly closes the zip file, which means that it will be reopened + * if anyone tries to use it again. + */ + void closeWeakly() + { + m_globalMutex.lock(); + + try + { + _closeWeakly(); + } + finally + { + m_globalMutex.unlock(); + } + } + + /** + * This method is used internally to weakly close a zip file. It should + * only be called when already holding the global lock, otherwise use + * closeWeakly(). + */ + private void _closeWeakly() + { + m_localMutex.lock(); + + try + { + if (m_status == OPEN) + { + try + { + m_status = WEAKLY_CLOSED; + if (m_zipFile != null) + { + m_zipFile.close(); + m_zipFile = null; + } + m_openFiles.remove(this); + } + catch (IOException ex) + { + __close(); + } + } + } + finally + { + m_localMutex.unlock(); + } + } + + /** + * This method permanently closes the zip file. + * @throws IOException if any error occurs while trying to close the + * zip file. + */ + public void close() throws IOException + { + if (m_limit > 0) + { + m_globalMutex.lock(); + m_localMutex.lock(); + } + + try + { + ZipFile tmp = m_zipFile; + __close(); + if (tmp != null) + { + tmp.close(); + } + } + finally + { + if (m_limit > 0) + { + m_localMutex.unlock(); + m_globalMutex.unlock(); + } + } + } + + /** + * This internal method is used to clear the zip file from the data + * structures and reset its state. It should only be called when + * holding the global and local mutexes. + */ + private void __close() + { + m_status = CLOSED; + m_zipFile = null; + m_zipFiles.remove(this); + m_openFiles.remove(this); + } + + /** + * This method ensures that the zip file associated with this + * weak zip file instance is actually open and acquires the + * local weak zip file mutex. If the underlying zip file is closed, + * then the local mutex is released and an IllegalStateException is + * thrown. If the zip file is weakly closed, then it is reopened. + * If the zip file is already opened, then no additional action is + * necessary. If this method does not throw an exception, then + * the end result is the zip file member field is non-null and the + * local mutex has been acquired. + */ + private void ensureZipFileIsOpen() + { + if (m_limit == 0) + { + return; + } + + // Get mutex for zip file. + m_localMutex.lock(); + + // If zip file is closed, then just return null. + if (m_status == CLOSED) + { + m_localMutex.unlock(); + throw new IllegalStateException("Zip file is closed: " + m_file); + } + + // If zip file is weakly closed, we need to reopen it, + // but we have to release the zip mutex to acquire the + // global mutex, then reacquire the zip mutex. This + // ensures that the global mutex is always acquired + // before any local mutex to avoid deadlocks. + IOException cause = null; + if (m_status == WEAKLY_CLOSED) + { + m_localMutex.unlock(); + + m_globalMutex.lock(); + + m_localMutex.lock(); + + // Double check status since it may have changed. + if (m_status == CLOSED) + { + m_localMutex.unlock(); + m_globalMutex.unlock(); + throw new IllegalStateException("Zip file is closed: " + m_file); + } + else if (m_status == WEAKLY_CLOSED) + { + try + { + __reopenZipFile(); + } + catch (IOException ex) + { + cause = ex; + } + } + + // Release the global mutex, since it should no longer be necessary. + m_globalMutex.unlock(); + } + + // It is possible that reopening the zip file failed, so we check + // for that case and throw an exception. + if (m_zipFile == null) + { + m_localMutex.unlock(); + IllegalStateException ise = + new IllegalStateException("Zip file is closed: " + m_file); + if (cause != null) + { + ise.initCause(cause); + } + throw ise; + } + } + + /** + * Thie internal method is used to reopen a weakly closed zip file. + * It makes a best effort, but may fail and leave the zip file member + * field null. Any failure reopening a zip file results in it being + * permanently closed. This method should only be invoked when holding + * the global and local mutexes. + */ + private void __reopenZipFile() throws IOException + { + if (m_status == WEAKLY_CLOSED) + { + try + { + m_zipFile = m_secureAction.openZipFile(m_file); + m_status = OPEN; + m_timestamp = System.currentTimeMillis(); + } + catch (IOException ex) + { + __close(); + throw ex; + } + + if (m_zipFile != null) + { + m_openFiles.add(this); + if (m_openFiles.size() > m_limit) + { + WeakZipFile candidate = m_openFiles.get(0); + for (WeakZipFile tmp : m_openFiles) + { + if (candidate.m_timestamp > tmp.m_timestamp) + { + candidate = tmp; + } + } + candidate._closeWeakly(); + } + } + } + } + + /** + * This is an InputStream wrapper that will properly reopen the underlying + * zip file if it is weakly closed and create the underlying input stream. + */ + class WeakZipInputStream extends InputStream + { + private final String m_entryName; + private volatile InputStream m_is; + private volatile int m_currentPos = 0; + private volatile ZipFile m_zipFileSnapshot; + + WeakZipInputStream(String entryName, InputStream is) + { + m_entryName = entryName; + m_is = is; + m_zipFileSnapshot = m_zipFile; + } + + /** + * This internal method ensures that the zip file is open and that + * the underlying input stream is valid. Upon successful completion, + * the underlying input stream will be valid and the local mutex + * will be held. + * @throws IOException if the was an error handling the input stream. + */ + private void ensureInputStreamIsValid() throws IOException + { + if (m_limit == 0) + { + return; + } + + ensureZipFileIsOpen(); + + // If the underlying zip file changed, then we need + // to get the input stream again. + if (m_zipFileSnapshot != m_zipFile) + { + m_zipFileSnapshot = m_zipFile; + + if (m_is != null) + { + try + { + m_is.close(); + } + catch (Exception ex) + { + // Not much we can do. + } + } + try + { + m_is = m_zipFile.getInputStream(m_zipFile.getEntry(m_entryName)); + m_is.skip(m_currentPos); + } + catch (IOException ex) + { + if (m_limit > 0) + { + m_localMutex.unlock(); + } + throw ex; + } + } + } + + @Override + public int available() throws IOException + { + ensureInputStreamIsValid(); + try + { + return m_is.available(); + } + finally + { + if (m_limit > 0) + { + m_localMutex.unlock(); + } + } + } + + @Override + public void close() throws IOException + { + ensureInputStreamIsValid(); + try + { + InputStream is = m_is; + m_is = null; + if (is != null) + { + is.close(); + } + } + finally + { + if (m_limit > 0) + { + m_localMutex.unlock(); + } + } + } + + public int read() throws IOException + { + ensureInputStreamIsValid(); + try + { + int len = m_is.read(); + if (len > 0) + { + m_currentPos++; + } + return len; + } + finally + { + if (m_limit > 0) + { + m_localMutex.unlock(); + } + } + } + + @Override + public int read(byte[] bytes) throws IOException + { + ensureInputStreamIsValid(); + try + { + int len = m_is.read(bytes); + if (len > 0) + { + m_currentPos += len; + } + return len; + } + finally + { + if (m_limit > 0) + { + m_localMutex.unlock(); + } + } + } + + @Override + public int read(byte[] bytes, int i, int i1) throws IOException + { + ensureInputStreamIsValid(); + try + { + int len = m_is.read(bytes, i, i1); + if (len > 0) + { + m_currentPos += len; + } + return len; + } + finally + { + if (m_limit > 0) + { + m_localMutex.unlock(); + } + } + } + + @Override + public long skip(long l) throws IOException + { + ensureInputStreamIsValid(); + try + { + long len = m_is.skip(l); + if (len > 0) + { + m_currentPos += len; + } + return len; + } + finally + { + if (m_limit > 0) + { + m_localMutex.unlock(); + } + } + } + } + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/util/ldap/AttributeNotFoundException.java b/framework/src/main/java/org/apache/felix/framework/util/ldap/AttributeNotFoundException.java deleted file mode 100644 index 40d044d1f46..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/util/ldap/AttributeNotFoundException.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.util.ldap; - -public class AttributeNotFoundException extends EvaluationException -{ - public AttributeNotFoundException(String msg) - { - super(msg); - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/ldap/Driver.java b/framework/src/main/java/org/apache/felix/framework/util/ldap/Driver.java deleted file mode 100644 index 0690187217c..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/util/ldap/Driver.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.util.ldap; - -import java.io.*; -import java.util.*; - -public class Driver { - - public static void main(String[] argv) - { - Mapper mapper = new DriverMapper(); - - if(argv== null || argv.length == 0) { - System.err.println("usage: Driver "); - return; - } - LdapLexer lexer = new LdapLexer(); - FileReader fr = null; - char[] line = null; - Evaluator engine = new Evaluator(); - - Parser parser = new Parser(); -// parser.setDebug(System.out); - - try { - File spec = new File(argv[0]); - fr = new FileReader(spec); - - // The basic operation of the driver is: - // 1. read a line from the file - // 2. parse that line - // 3. print the resulting program - // 4. repeat 1 until eof - - for(;;) { - line = getLine(fr); - if(line == null) break; - System.out.println("Driver: filter: "+new String(line)); - CharArrayReader car = new CharArrayReader(line); - lexer.setReader(car); - parser.reset(lexer); - boolean status = false; - try { - status = parser.start(); - if(!status) { - System.err.println("parse failed"); - printErrorLocation(line,lexer.charno()); - } - } catch (ParseException pe) { - System.err.println(pe.toString()); - printErrorLocation(line,lexer.charno()); - } - if(status) { - try { - engine.reset(parser.getProgram()); -// System.out.println("Driver: program: "+engine.toString()); - System.out.println("Driver: program: "+engine.toStringInfix()); - System.out.println("Eval = " + engine.evaluate(mapper)); - } catch (EvaluationException ee) { - System.err.print("Driver: "); - printEvaluationStack(engine.getOperands()); - System.err.println(ee.toString()); - } - } - } - } catch (Exception e) { - System.err.println(e.toString()); - printErrorLocation(line,lexer.charno()); - e.printStackTrace(); - } - } - - // Get a line of input at a time and return a char[] buffer - // containing the line - - static char[] getLine(Reader reader) throws IOException - { - StringBuffer buf = new StringBuffer(); - for(;;) { - int c = reader.read(); - if(c == '\r') continue; - if(c < 0) { - if(buf.length() == 0) return null; // no more lines - break; - } - if(c == '\n') break; - buf.append((char)c); - } - - char[] cbuf = new char[buf.length()]; - buf.getChars(0,buf.length(),cbuf,0); - return cbuf; - } - - - static void printErrorLocation(char[] line, int charno) - { - System.err.print("|"); - if(line != null) System.err.print(new String(line)); - System.err.println("|"); - for(int i=0;i ArrayList is using jdk1.2 or later - public Operator[] children = null; -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/ldap/ParseException.java b/framework/src/main/java/org/apache/felix/framework/util/ldap/ParseException.java deleted file mode 100644 index 3d683649c92..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/util/ldap/ParseException.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.util.ldap; - -public class ParseException extends Exception { - public ParseException() {super();} - public ParseException(String msg) {super(msg);} -} diff --git a/framework/src/main/java/org/apache/felix/framework/util/ldap/Parser.java b/framework/src/main/java/org/apache/felix/framework/util/ldap/Parser.java deleted file mode 100644 index ed781ca7d29..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/util/ldap/Parser.java +++ /dev/null @@ -1,1695 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.util.ldap; - -import java.io.IOException; -import java.io.PrintStream; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.*; - -public class Parser -{ - // - // Parser contants. - // - - // End of file. - public static final int EOF = -1; - - // Special characters in parse - public static final char LPAREN = '('; - public static final char RPAREN = ')'; - public static final char STAR = '*'; - - // Define the list of legal leading and trailing - // characters in an attribute name. - public static final String ATTRIBUTECHARS0 = - ".abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"; - // Define the list of legal internal characters in an attribute name. - public static final String ATTRIBUTECHARS1 = - ".abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_ "; - - // Define an enum for substring procedure - public static final int SIMPLE = 0; - public static final int PRESENT = 1; - public static final int SUBSTRING = 2; - - // different from =|>|<|~ - public static final int NOOP = 0; - - // Comparison operators. - public static final int EQUAL = 0; - public static final int GREATER_EQUAL = 1; - public static final int LESS_EQUAL = 2; - public static final int APPROX = 3; - - // Criteria in % to accept something as approximate. - public static final int APPROX_CRITERIA = 10; - - // Flag indicating presense of BigInteger/Decimal. - private static boolean m_hasBigNumbers = false; - - static - { - try - { - Class.forName("java.math.BigDecimal"); - m_hasBigNumbers = true; - } - catch (Exception ex) - { - // Ignore. - } - } - // - // Instance variables. - // - - private LdapLexer lexer = null; - private List program; - - public Parser() - { - reset(); - } - - public Parser(LdapLexer l) - { - reset(l); - } - - public void reset() - { - lexer = null; - if (program == null) - { - program = new ArrayList(); - } - program.clear(); - } - - public void reset(LdapLexer l) - { - reset(); - lexer = l; - } - - public Object[] getProgram() - { - return program.toArray(new Object[program.size()]); - } - - // Define the recursive descent procedures - - /* - ::= - */ - public boolean start() throws ParseException, IOException - { - boolean ok = filter(); - if (!ok) - { - return ok; - } - int ch = lexer.get(); - if (ch != EOF) - { - throw new ParseException( - "expected ; found '" + ((char) ch) + "'"); - } - return ok; - } - - /* - ::= '(' ')' - */ - boolean filter() throws ParseException, IOException - { - debug("filter"); - if (lexer.peeknw() != LPAREN) - { - return false; - } - lexer.get(); - if (!filtercomp()) - { - throw new ParseException("expected filtercomp"); - } - if (lexer.getnw() != RPAREN) - { - throw new ParseException("expected )"); - } - return true; - } - - /* - ::= | | | - ::= '&' - ::= '|' - ::= '!' - */ - boolean filtercomp() throws ParseException, IOException - { - debug("filtercomp"); - int c = lexer.peeknw(); - switch (c) - { - case '&' : - case '|' : - lexer.get(); - int cnt = filterlist(); - if (cnt == 0) - { - return false; - } - // Code: [And|Or](cnt) - program.add( - c == '&' - ? (Operator) new AndOperator(cnt) - : (Operator) new OrOperator(cnt)); - return true; - case '!' : - lexer.get(); - if (!filter()) - { - return false; - } - // Code: Not() - program.add(new NotOperator()); - return true; - case EOF : - return false; - default : - // check for key - if (ATTRIBUTECHARS0.indexOf(c) <= 0) - { - return false; - } - boolean b = item(); - return b; - } - } - - /* - ::= | - */ - int filterlist() throws ParseException, IOException - { - debug("filterlist"); - int cnt = 0; - if (filter()) - { - do - { - cnt++; - } - while (filter()); - } - return (cnt); - } - - /* - ::= | | - ::= - ::= | | | - ::= '=*' - ::= '=' - */ - boolean item() throws ParseException, IOException - { - debug("item"); - - StringBuffer attr = new StringBuffer(); - if (!attribute(attr)) - { - return false; - } - - lexer.skipwhitespace(); // assume allowable before equal operator - // note: I treat the =* case as = followed by a special substring - int op = equalop(); - if (op == NOOP) - { - String oplist = "=|~=|>=|<="; - throw new ParseException("expected " + oplist); - } - ArrayList pieces = new ArrayList(); - int kind = substring(pieces); - // Get some of the illegal cases out of the way - if (op != '=' && kind != SIMPLE) - { - // We assume that only the = operator can work - // with right sides containing stars. If not correct - // then this code must change. - throw new ParseException("expected value|substring|*"); - } - - switch (kind) - { - case SIMPLE : - // Code: Push(attr); Constant(pieces.get(0)); (); - program.add(new PushOperator(attr.toString())); - program.add(new ConstOperator(pieces.get(0))); - switch (op) - { - case '<' : - program.add(new LessEqualOperator()); - break; - case '>' : - program.add(new GreaterEqualOperator()); - break; - case '~' : - program.add(new ApproxOperator()); - break; - case '=' : - default : - program.add(new EqualOperator()); - } - break; - case PRESENT : - // Code: Present(attr); - program.add(new PresentOperator(attr.toString())); - break; - case SUBSTRING : - generateSubStringCode(attr.toString(), pieces); - break; - default : - throw new ParseException("expected value|substring|*"); - } - return true; - } - - // Generating code for substring right side is mildly - // complicated. - - void generateSubStringCode(String attr, ArrayList pieces) - { - // Code: Push(attr) - program.add(new PushOperator(attr.toString())); - - // Convert the pieces arraylist to a String[] - String[] list = - (String[]) pieces.toArray(new String[pieces.size()]); - - // Code: SubString(list) - program.add(new SubStringOperator(list)); - } - - /* - is a string representing an attributte, - or key, in the properties - objects of the registered services. Attribute names are not case - sensitive; that is cn and CN both refer to the same attribute. - Attribute names may have embedded spaces, but not leading or - trailing spaces. - */ - boolean attribute(StringBuffer buf) throws ParseException, IOException - { - debug("attribute"); - lexer.skipwhitespace(); - buf.setLength(0); - int c = lexer.peek(); // need to make sure there - // is at least one KEYCHAR - if (c == EOF) - { - return false; - } - if (ATTRIBUTECHARS0.indexOf(c) < 0) - { - return false; - } - - do - { - buf.append((char) lexer.get()); - } - while (ATTRIBUTECHARS1.indexOf(lexer.peek()) >= 0); - - // The above may have accumulated trailing blanks that must be removed - int i = buf.length() - 1; - while (i > 0 && buf.charAt(i) == ' ') - { - i--; - } - buf.setLength(i + 1); - return true; - } - - /* - ::= '=' - ::= '~=' - ::= '>=' - ::= '<=' - ::= '=*' - */ - int equalop() throws ParseException, IOException - { - debug("equalop"); - lexer.skipwhitespace(); - int op = lexer.peek(); - switch (op) - { - case '=' : - lexer.get(); - break; - case '~' : - case '<' : - case '>' : - // skip main operator char - int c = lexer.get(); - // make sure that the next char is '=' - c = lexer.get(); - if (c != '=') - { - throw new ParseException("expected ~=|>=|<="); - } - break; - default : - op = NOOP; - } - return op; - } - - /* - ::= '=' - ::= NULL | - ::= '*' - ::= NULL | '*' - ::= NULL | - ::= ... - */ - /* - This procedure handles all cases on right side of an item - */ - int substring(ArrayList pieces) throws ParseException, IOException - { - debug("substring"); - - pieces.clear(); - StringBuffer ss = new StringBuffer(); - // int kind = SIMPLE; // assume until proven otherwise - boolean wasStar = false; // indicates last piece was a star - boolean leftstar = false; // track if the initial piece is a star - boolean rightstar = false; // track if the final piece is a star - - // We assume (sub)strings can contain leading and trailing blanks -loop: for (;;) - { - int c = lexer.peek(); - switch (c) - { - case RPAREN : - if (wasStar) - { - // insert last piece as "" to handle trailing star - rightstar = true; - } - else - { - pieces.add(ss.toString()); - // accumulate the last piece - // note that in the case of - // (cn=); this might be - // the string "" (!=null) - } - ss.setLength(0); - break loop; - case '\\' : - wasStar = false; - lexer.get(); - c = lexer.get(); - if (c == EOF) - { - throw new ParseException("unexpected EOF"); - } - ss.append((char) c); - break; - case EOF : - if (pieces.size() > 0) - { - throw new ParseException("expected ')'"); - } - else - { - throw new ParseException("expected value|substring"); - } - case '*' : - if (wasStar) - { - // encountered two successive stars; - // I assume this is illegal - throw new ParseException("unexpected '**'"); - } - lexer.get(); - if (ss.length() > 0) - { - pieces.add(ss.toString()); // accumulate the pieces - // between '*' occurrences - } - ss.setLength(0); - // if this is a leading star, then track it - if (pieces.size() == 0) - { - leftstar = true; - } - ss.setLength(0); - wasStar = true; - break; - default : - wasStar = false; - ss.append((char) lexer.get()); - } - } - if (pieces.size() == 0) - { - return PRESENT; - } - if (leftstar || rightstar || pieces.size() > 1) - { - // insert leading and/or trailing "" to anchor ends - if (rightstar) - { - pieces.add(""); - } - if (leftstar) - { - pieces.add(0, ""); - } - return SUBSTRING; - } - // assert !leftstar && !rightstar && pieces.size == 1 - return SIMPLE; - } - - // Debug stuff - - static boolean debug = false; - - PrintStream dbgout = null; - - public void setDebug(PrintStream out) - { - debug = true; - dbgout = out; - } - - void debug(String proc) - { - if (!debug || dbgout == null) - { - return; - } - dbgout.println("parsing " + proc + ":" + lexer.charno()); - dbgout.flush(); - } - - // Exclusive inner classes - - private static class AndOperator extends Operator - { - private int operandCount; - - public AndOperator(int opcnt) - { - operandCount = opcnt; - } - - public void execute(Stack operands, Mapper mapper) - throws EvaluationException - { - // Determine result using short-circuit evaluation. - boolean result = true; - for (int i = 0; i < operandCount; i++) - { - if (operands.empty()) - { - fewOperands("AND"); - } - - // For short-circuited evaluation, once the AND - // becomes false, we can ignore the remaining - // expressions, but we must still pop them off. - if (!result) - { - operands.pop(); - } - else - { - result = ((Boolean) operands.pop()).booleanValue(); - } - } - operands.push(new Boolean(result)); - } - - public String toString() - { - return "&(" + operandCount + ")"; - } - - public void buildTree(Stack operands) - { - children = new Operator[operandCount]; - // need to preserve stack order - for (int i = 0; i < operandCount; i++) - { - children[(operandCount - 1) - i] = - (Operator) operands.pop(); - } - operands.push(this); - } - - public void toStringInfix(StringBuffer b) - { - b.append("(&"); - for (int i = 0; i < children.length; i++) - { - Operator o = (Operator) children[i]; - o.toStringInfix(b); - } - b.append(")"); - } - } - - private static class OrOperator extends Operator - { - private int operandCount; - - public OrOperator(int opcnt) - { - operandCount = opcnt; - } - - public void execute(Stack operands, Mapper mapper) - throws EvaluationException - { - // Determine result using short-circuit evaluation. - boolean result = false; - for (int i = 0; i < operandCount; i++) - { - if (operands.empty()) - { - fewOperands("OR"); - } - - // For short-circuited evaluation, once the OR - // becomes true, we can ignore the remaining - // expressions, but we must still pop them off. - if (result) - { - operands.pop(); - } - else - { - result = ((Boolean) operands.pop()).booleanValue(); - } - } - operands.push(new Boolean(result)); - } - - public String toString() - { - return "|(" + operandCount + ")"; - } - - public void buildTree(Stack operands) - { - children = new Operator[operandCount]; - // need to preserve stack order - for (int i = 0; i < operandCount; i++) - { - children[(operandCount - 1) - i] = - (Operator) operands.pop(); - } - operands.push(this); - } - - public void toStringInfix(StringBuffer b) - { - b.append("(|"); - for (int i = 0; i < children.length; i++) - { - Operator o = (Operator) children[i]; - o.toStringInfix(b); - } - b.append(")"); - } - } - - private static class NotOperator extends Operator - { - public NotOperator() - { - } - - public void execute(Stack operands, Mapper mapper) - throws EvaluationException - { - if (operands.empty()) - { - fewOperands("NOT"); - } - boolean result = !((Boolean) operands.pop()).booleanValue(); - operands.push(new Boolean(result)); - } - - public String toString() - { - return "!()"; - } - - public void buildTree(Stack operands) - { - children = new Operator[1]; - children[0] = (Operator) operands.pop(); - operands.push(this); - } - - public void toStringInfix(StringBuffer b) - { - b.append("(!"); - for (int i = 0; i < children.length; i++) - { - Operator o = (Operator) children[i]; - o.toStringInfix(b); - } - b.append(")"); - } - } - - private static class EqualOperator extends Operator - { - public EqualOperator() - { - } - - public void execute(Stack operands, Mapper mapper) - throws EvaluationException - { - if (operands.empty()) - { - fewOperands("="); - } - - // We cheat and use the knowledge that top (right) operand - // will always be a string because of the way code was generated - String rhs = (String) operands.pop(); - if (operands.empty()) - { - fewOperands("="); - } - - Object lhs = operands.pop(); - - operands.push(new Boolean(compare(lhs, rhs, EQUAL))); - } - - public String toString() - { - return "=()"; - } - - public void buildTree(Stack operands) - { - children = new Operator[2]; - // need to preserve stack order - for (int i = 0; i < 2; i++) - { - Operator o = (Operator) operands.pop(); - children[1 - i] = o; - } - operands.push(this); - } - - public void toStringInfix(StringBuffer b) - { - b.append("("); - for (int i = 0; i < children.length; i++) - { - Operator o = (Operator) children[i]; - if (i > 0) - { - b.append("="); - } - o.toStringInfix(b); - } - b.append(")"); - } - } - - private static class GreaterEqualOperator extends Operator - { - public GreaterEqualOperator() - { - } - - public void execute(Stack operands, Mapper mapper) - throws EvaluationException - { - if (operands.empty()) - { - fewOperands(">="); - } - // We cheat and use the knowledge that top (right) operand - // will always be a string because of the way code was generated - String rhs = (String) operands.pop(); - if (operands.empty()) - { - fewOperands(">="); - } - Object lhs = operands.pop(); - - operands.push(new Boolean(compare(lhs, rhs, GREATER_EQUAL))); - } - - public String toString() - { - return ">=()"; - } - - public void buildTree(Stack operands) - { - children = new Operator[2]; - // need to preserve stack order - for (int i = 0; i < 2; i++) - { - children[1 - i] = (Operator) operands.pop(); - } - operands.push(this); - } - - public void toStringInfix(StringBuffer b) - { - b.append("("); - for (int i = 0; i < children.length; i++) - { - Operator o = (Operator) children[i]; - if (i > 0) - { - b.append(">="); - } - o.toStringInfix(b); - } - b.append(")"); - } - } - - private static class LessEqualOperator extends Operator - { - public LessEqualOperator() - { - } - - public void execute(Stack operands, Mapper mapper) - throws EvaluationException - { - if (operands.empty()) - { - fewOperands("<="); - } - // We cheat and use the knowledge that top (right) operand - // will always be a string because of the way code was generated - String rhs = (String) operands.pop(); - if (operands.empty()) - { - fewOperands("<="); - } - Object lhs = (Object) operands.pop(); - operands.push(new Boolean(compare(lhs, rhs, LESS_EQUAL))); - } - - public String toString() - { - return "<=()"; - } - - public void buildTree(Stack operands) - { - children = new Operator[2]; - // need to preserve stack order - for (int i = 0; i < 2; i++) - { - children[1 - i] = (Operator) operands.pop(); - } - operands.push(this); - } - - public void toStringInfix(StringBuffer b) - { - b.append("("); - for (int i = 0; i < children.length; i++) - { - Operator o = (Operator) children[i]; - if (i > 0) - { - b.append("<="); - } - o.toStringInfix(b); - } - b.append(")"); - } - } - - private static class ApproxOperator extends Operator - { - public ApproxOperator() - { - } - - public void execute(Stack operands, Mapper mapper) - throws EvaluationException - { - if (operands.empty()) - { - fewOperands("~="); - } - // We cheat and use the knowledge that top (right) operand - // will always be a string because of the way code was generated - String rhs = (String) operands.pop(); - if (operands.empty()) - { - fewOperands("~="); - } - Object lhs = operands.pop(); - operands.push(new Boolean(compare(lhs, rhs, APPROX))); - } - - public String toString() - { - return "~=()"; - } - - public void buildTree(Stack operands) - { - children = new Operator[2]; - // need to preserve stack order - for (int i = 0; i < 2; i++) - { - children[1 - i] = (Operator) operands.pop(); - } - operands.push(this); - } - - public void toStringInfix(StringBuffer b) - { - b.append("("); - for (int i = 0; i < children.length; i++) - { - Operator o = (Operator) children[i]; - if (i > 0) - { - b.append("~="); - } - o.toStringInfix(b); - } - b.append(")"); - } - } - - private static class PresentOperator extends Operator - { - String attribute; - - public PresentOperator(String attribute) - { - this.attribute = attribute; - } - - public void execute(Stack operands, Mapper mapper) - throws EvaluationException - { - Object value = mapper.lookup(attribute); - operands.push(new Boolean(value != null)); - } - - public String toString() - { - return attribute + "=*"; - } - - public void buildTree(Stack operands) - { - operands.push(this); - } - - public void toStringInfix(StringBuffer b) - { - b.append("("); - b.append(attribute + "=*"); - b.append(")"); - } - } - - private static class PushOperator extends Operator - { - String attribute; - - public PushOperator(String attribute) - { - this.attribute = attribute; - } - - public void execute(Stack operands, Mapper mapper) - throws EvaluationException - { - // find and push the value of a given attribute - Object value = mapper.lookup(attribute); - if (value == null) - { - throw new AttributeNotFoundException( - "attribute " + attribute + " not found"); - } - operands.push(value); - } - - public String toString() - { - return "push(" + attribute + ")"; - } - - public String toStringInfix() - { - return attribute; - } - - public void buildTree(Stack operands) - { - operands.push(this); - } - - public void toStringInfix(StringBuffer b) - { - b.append(attribute); - } - } - - private static class ConstOperator extends Operator - { - Object val; - - public ConstOperator(Object val) - { - this.val = val; - } - - public void execute(Stack operands, Mapper mapper) - throws EvaluationException - { - operands.push(val); - } - - public String toString() - { - return "const(" + val + ")"; - } - - public String toStringInfix() - { - return val.toString(); - } - - public void buildTree(Stack operands) - { - operands.push(this); - } - - public void toStringInfix(StringBuffer b) - { - b.append(val.toString()); - } - } - - private static class SubStringOperator extends Operator - implements OperatorConstants - { - String[] pieces; - - public SubStringOperator(String[] pieces) - { - this.pieces = pieces; - } - - public void execute(Stack operands, Mapper mapper) - throws EvaluationException - { - if (operands.empty()) - { - fewOperands("SUBSTRING"); - } - - Object op = operands.pop(); - - // The operand can either be a string or an array of strings. - if (op instanceof String) - { - operands.push(check((String) op)); - } - else if (op instanceof String[]) - { - // If one element of the array matches, then push true. - String[] ops = (String[]) op; - boolean result = false; - for (int i = 0; !result && (i < ops.length); i++) - { - if (check(ops[i]) == Boolean.TRUE) - { - result = true; - } - } - - operands.push((result) ? Boolean.TRUE : Boolean.FALSE); - } - else - { - unsupportedType("SUBSTRING", op.getClass()); - } - } - - private Boolean check(String s) - { - // Walk the pieces to match the string - // There are implicit stars between each piece, - // and the first and last pieces might be "" to anchor the match. - // assert (pieces.length > 1) - // minimal case is * - - Boolean result = Boolean.FALSE; - int len = pieces.length; - - int index = 0; - for (int i = 0; i < len; i++) - { - String piece = (String) pieces[i]; - - if (i == len - 1) - { - // this is the last piece - if (s.endsWith(piece)) - { - result = Boolean.TRUE; - } - else - { - result = Boolean.FALSE; - } - break; - } - // initial non-star; assert index == 0 - else if (i == 0) - { - if (!s.startsWith(piece)) - { - result = Boolean.FALSE; - break; - } - } - // assert i > 0 && i < len-1 - else - { - // Sure wish stringbuffer supported e.g. indexOf - index = s.indexOf(piece, index); - if (index < 0) - { - result = Boolean.FALSE; - break; - } - } - // start beyond the matching piece - index += piece.length(); - } - - return result; - } - - public String toString() - { - StringBuffer b = new StringBuffer(); - b.append("substring("); - for (int i = 0; i < pieces.length; i++) - { - String piece = pieces[i]; - if (i > 0) - { - b.append("*"); - } - b.append(escape(piece)); - } - b.append(")"); - return b.toString(); - } - - public String escape(String s) - { - int len = s.length(); - StringBuffer buf = new StringBuffer(len); - for (int i = 0; i < len; i++) - { - char c = s.charAt(i); - if (c == ')' || c == '*') - buf.append('\\'); - buf.append(c); - } - return buf.toString(); - } - - public void buildTree(Stack operands) - { - children = new Operator[1]; - children[0] = (Operator) operands.pop(); - operands.push(this); - } - - public void toStringInfix(StringBuffer b) - { - b.append("("); - children[0].toStringInfix(b); // dump attribute - b.append("="); - for (int i = 0; i < pieces.length; i++) - { - String piece = (String) pieces[i]; - if (i > 0) - { - b.append("*"); - } - b.append(piece); - } - b.append(")"); - } - } - - // Utility classes and Interfaces - - private interface OperatorConstants - { - static final int SSINIT = 0; - static final int SSFINAL = 1; - static final int SSMIDDLE = 2; - static final int SSANY = 3; - } - - /** - * Compare two operands in an expression with respect - * to the following operators =, <=, >= and ~= - * - * Example: value=100 - * - * @param lhs an object that implements comparable or an array of - * objects that implement comparable. - * @param rhs a string representing the right operand. - * @param operator an integer that represents the operator. - * @return true or false according to the evaluation. - * @throws EvaluationException if it is not possible to do the comparison. - **/ - public static boolean compare(Object lhs, String rhs, int operator) - throws EvaluationException - { - // Determine class of LHS. - Class lhsClass = null; - - // If LHS is an array, then call compare() on each element - // of the array until a match is found. - if (lhs.getClass().isArray()) - { - // First, if this is an array of primitives, then convert - // the entire array to an array of the associated - // primitive wrapper class instances. - if (lhs.getClass().getComponentType().isPrimitive()) - { - lhs = convertPrimitiveArray(lhs); - } - - // Now call compare on each element of array. - Object[] array = (Object[]) lhs; - for (int i = 0; i < array.length; i++) - { - if (compare(array[i], rhs, operator)) - { - return true; - } - } - } - // If LHS is a vector, then call compare() on each element - // of the vector until a match is found. - else if (lhs instanceof Vector) - { - for (Enumeration e = ((Vector) lhs).elements(); e.hasMoreElements();) - { - if (compare(e.nextElement(), rhs, operator)) - { - return true; - } - } - } - else - { - // Get the class of LHS. - lhsClass = lhs.getClass(); - - // At this point we are expecting the LHS to be a comparable, - // but Boolean is a special case since it is the only primitive - // wrapper class that does not implement comparable; deal with - // Boolean separately. - if (lhsClass == Boolean.class) - { - return compareBoolean(lhs, rhs, operator); - } - - // If the LHS is not a comparable, then try to use simple - // equals() comparison. If that fails, return false. - if (!(Comparable.class.isAssignableFrom(lhsClass))) - { - try - { - Object rhsObject = lhsClass - .getConstructor(new Class[] { String.class }) - .newInstance(new Object[] { rhs }); - return lhs.equals(rhsObject); - } - catch (Exception ex) - { - // Always return false. - } - - return false; - } - - // Here we know that the LHS is a comparable object, so - // try to create an object for the RHS by using a constructor - // that will take the RHS string as a parameter. - Comparable rhsComparable = null; - try - { - // We are expecting to be able to construct a comparable - // instance from the RHS string by passing it into the - // constructor of the corresponing comparable class. The - // Character class is a special case, since its constructor - // does not take a string, so handle it separately. - if (lhsClass == Character.class) - { - rhsComparable = new Character(rhs.charAt(0)); - } - else - { - rhsComparable = (Comparable) lhsClass - .getConstructor(new Class[] { String.class }) - .newInstance(new Object[] { rhs }); - } - } - catch (Exception ex) - { - throw new EvaluationException( - "Could not instantiate class " - + lhsClass.getName() - + " with constructor String parameter " - + rhs + " " + ex); - } - - Comparable lhsComparable = (Comparable) lhs; - - switch (operator) - { - case EQUAL : - return (lhsComparable.compareTo(rhsComparable) == 0); - case GREATER_EQUAL : - return (lhsComparable.compareTo(rhsComparable) >= 0); - case LESS_EQUAL : - return (lhsComparable.compareTo(rhsComparable) <= 0); - case APPROX: - return compareToApprox(lhsComparable, rhsComparable); - default: - throw new EvaluationException("Unknown comparison operator..." - + operator); - } - } - - return false; - } - - /** - * This is an ugly utility method to convert an array of primitives - * to an array of primitive wrapper objects. This method simplifies - * processing LDAP filters since the special case of primitive arrays - * can be ignored. - * @param array - * @return - **/ - private static Object[] convertPrimitiveArray(Object array) - { - Class clazz = array.getClass().getComponentType(); - - if (clazz == Boolean.TYPE) - { - boolean[] src = (boolean[]) array; - array = new Boolean[src.length]; - for (int i = 0; i < src.length; i++) - { - ((Object[]) array)[i] = new Boolean(src[i]); - } - } - else if (clazz == Character.TYPE) - { - char[] src = (char[]) array; - array = new Character[src.length]; - for (int i = 0; i < src.length; i++) - { - ((Object[]) array)[i] = new Character(src[i]); - } - } - else if (clazz == Byte.TYPE) - { - byte[] src = (byte[]) array; - array = new Byte[src.length]; - for (int i = 0; i < src.length; i++) - { - ((Object[]) array)[i] = new Byte(src[i]); - } - } - else if (clazz == Short.TYPE) - { - byte[] src = (byte[]) array; - array = new Byte[src.length]; - for (int i = 0; i < src.length; i++) - { - ((Object[]) array)[i] = new Byte(src[i]); - } - } - else if (clazz == Integer.TYPE) - { - int[] src = (int[]) array; - array = new Integer[src.length]; - for (int i = 0; i < src.length; i++) - { - ((Object[]) array)[i] = new Integer(src[i]); - } - } - else if (clazz == Long.TYPE) - { - long[] src = (long[]) array; - array = new Long[src.length]; - for (int i = 0; i < src.length; i++) - { - ((Object[]) array)[i] = new Long(src[i]); - } - } - else if (clazz == Float.TYPE) - { - float[] src = (float[]) array; - array = new Float[src.length]; - for (int i = 0; i < src.length; i++) - { - ((Object[]) array)[i] = new Float(src[i]); - } - } - else if (clazz == Double.TYPE) - { - double[] src = (double[]) array; - array = new Double[src.length]; - for (int i = 0; i < src.length; i++) - { - ((Object[]) array)[i] = new Double(src[i]); - } - } - - return (Object[]) array; - } - - private static boolean compareBoolean(Object lhs, String rhs, int operator) - throws EvaluationException - { - Boolean rhsBoolean = new Boolean(rhs); - if (lhs.getClass().isArray()) - { - Object[] objs = (Object[]) lhs; - for (int i = 0; i < objs.length; i++) - { - switch (operator) - { - case EQUAL : - case GREATER_EQUAL : - case LESS_EQUAL : - case APPROX: - if (objs[i].equals(rhsBoolean)) - { - return true; - } - break; - default: - throw new EvaluationException( - "Unknown comparison operator: " + operator); - } - } - return false; - } - else - { - switch (operator) - { - case EQUAL : - case GREATER_EQUAL : - case LESS_EQUAL : - case APPROX: - return (lhs.equals(rhsBoolean)); - default: - throw new EvaluationException("Unknown comparison operator..." - + operator); - } - } - } - - /** - * Test if two objects are approximate. The two objects that are passed must - * have the same type. - * - * Approximate for numerical values involves a difference of less than APPROX_CRITERIA - * Approximate for string values is calculated by using the Levenshtein distance - * between strings and is case insensitive. Less than APPROX_CRITERIA of - * difference is considered as approximate. - * - * Supported types only include the following subclasses of Number: - * - Byte - * - Double - * - Float - * - Int - * - Long - * - Short - * - BigInteger - * - BigDecimal - * As subclasses of Number must provide methods to convert the represented numeric value - * to byte, double, float, int, long, and short. (see API) - * - * @param obj1 - * @param obj2 - * @return true if they are approximate - * @throws EvaluationException if it the two objects cannot be approximated - **/ - private static boolean compareToApprox(Object obj1, Object obj2) throws EvaluationException - { - if (obj1 instanceof Byte) - { - byte value1 = ((Byte)obj1).byteValue(); - byte value2 = ((Byte)obj2).byteValue(); - return (value2 >= (value1-((Math.abs(value1)*(byte)APPROX_CRITERIA)/(byte)100)) - && value2 <= (value1+((Math.abs(value1)*(byte)APPROX_CRITERIA)/(byte)100))); - } - else if (obj1 instanceof Character) - { - char value1 = ((Character)obj1).charValue(); - char value2 = ((Character)obj2).charValue(); - return (value2 >= (value1-((Math.abs(value1)*(char)APPROX_CRITERIA)/(char)100)) - && value2 <= (value1+((Math.abs(value1)*(char)APPROX_CRITERIA)/(char)100))); - } - else if (obj1 instanceof Double) - { - double value1 = ((Double)obj1).doubleValue(); - double value2 = ((Double)obj2).doubleValue(); - return (value2 >= (value1-((Math.abs(value1)*(double)APPROX_CRITERIA)/(double)100)) - && value2 <= (value1+((Math.abs(value1)*(double)APPROX_CRITERIA)/(double)100))); - } - else if (obj1 instanceof Float) - { - float value1 = ((Float)obj1).floatValue(); - float value2 = ((Float)obj2).floatValue(); - return (value2 >= (value1-((Math.abs(value1)*(float)APPROX_CRITERIA)/(float)100)) - && value2 <= (value1+((Math.abs(value1)*(float)APPROX_CRITERIA)/(float)100))); - } - else if (obj1 instanceof Integer) - { - int value1 = ((Integer)obj1).intValue(); - int value2 = ((Integer)obj2).intValue(); - return (value2 >= (value1-((Math.abs(value1)*(int)APPROX_CRITERIA)/(int)100)) - && value2 <= (value1+((Math.abs(value1)*(int)APPROX_CRITERIA)/(int)100))); - } - else if (obj1 instanceof Long) - { - long value1 = ((Long)obj1).longValue(); - long value2 = ((Long)obj2).longValue(); - return (value2 >= (value1-((Math.abs(value1)*(long)APPROX_CRITERIA)/(long)100)) - && value2 <= (value1+((Math.abs(value1)*(long)APPROX_CRITERIA)/(long)100))); - } - else if (obj1 instanceof Short) - { - short value1 = ((Short)obj1).shortValue(); - short value2 = ((Short)obj2).shortValue(); - return (value2 >= (value1-((Math.abs(value1)*(short)APPROX_CRITERIA)/(short)100)) - && value2 <= (value1+((Math.abs(value1)*(short)APPROX_CRITERIA)/(short)100))); - } - else if (obj1 instanceof String) - { - int distance = getDistance( - obj1.toString().toLowerCase(), obj2.toString().toLowerCase()); - int size = ((String)obj1).length(); - return (distance <= ((size*APPROX_CRITERIA)/100)); - } - else if (m_hasBigNumbers && (obj1 instanceof BigInteger)) - { - BigInteger value1 = (BigInteger)obj1; - BigInteger value2 = (BigInteger)obj2; - BigInteger delta = value1.abs().multiply( - BigInteger.valueOf(APPROX_CRITERIA) - .divide(BigInteger.valueOf(100))); - BigInteger low = value1.subtract(delta); - BigInteger high = value1.add(delta); - return (value2.compareTo(low) >= 0) && (value2.compareTo(high) <= 0); - } - else if (m_hasBigNumbers && (obj1 instanceof BigDecimal)) - { - BigDecimal value1 = (BigDecimal)obj1; - BigDecimal value2 = (BigDecimal)obj2; - BigDecimal delta = value1.abs().multiply( - BigDecimal.valueOf(APPROX_CRITERIA) - .divide(BigDecimal.valueOf(100), BigDecimal.ROUND_HALF_DOWN)); - BigDecimal low = value1.subtract(delta); - BigDecimal high = value1.add(delta); - return (value2.compareTo(low) >= 0) && (value2.compareTo(high) <= 0); - } - throw new EvaluationException( - "Approximate operator not supported for type " - + obj1.getClass().getName()); - } - - /** - * Calculate the Levenshtein distance (LD) between two strings. - * The Levenshteing distance is a measure of the similarity between - * two strings, which we will refer to as the source string (s) and - * the target string (t). The distance is the number of deletions, - * insertions, or substitutions required to transform s into t. - * - * Algorithm from: http://www.merriampark.com/ld.htm - * - * @param s the first string - * @param t the second string - * @return - */ - private static int getDistance(String s, String t) - { - int d[][]; // matrix - int n; // length of s - int m; // length of t - int i; // iterates through s - int j; // iterates through t - char s_i; // ith character of s - char t_j; // jth character of t - int cost; // cost - - // Step 1 - n = s.length(); - m = t.length(); - if (n == 0) - { - return m; - } - if (m == 0) - { - return n; - } - d = new int[n + 1][m + 1]; - - // Step 2 - for (i = 0; i <= n; i++) - { - d[i][0] = i; - } - - for (j = 0; j <= m; j++) - { - d[0][j] = j; - } - - // Step 3 - for (i = 1; i <= n; i++) - { - s_i = s.charAt(i - 1); - // Step 4 - for (j = 1; j <= m; j++) - { - t_j = t.charAt(j - 1); - // Step 5 - if (s_i == t_j) - { - cost = 0; - } - else - { - cost = 1; - } - // Step 6 - d[i][j] = - Minimum( - d[i - 1][j] + 1, - d[i][j - 1] + 1, - d[i - 1][j - 1] + cost); - } - } - // Step 7 - return d[n][m]; - } - - /** - * Calculate the minimum between three values - * - * @param a - * @param b - * @param c - * @return - */ - private static int Minimum(int a, int b, int c) - { - int mi; - mi = a; - if (b < mi) - { - mi = b; - } - if (c < mi) - { - mi = c; - } - return mi; - } - - private static void fewOperands(String op) throws EvaluationException - { - throw new EvaluationException(op + ": too few operands"); - } - - private static void unsupportedType(String opStr, Class clazz) - throws EvaluationException - { - throw new EvaluationException( - opStr + ": unsupported type " + clazz.getName(), clazz); - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/ldap/Unknown.java b/framework/src/main/java/org/apache/felix/framework/util/ldap/Unknown.java deleted file mode 100644 index 34e7ca88e85..00000000000 --- a/framework/src/main/java/org/apache/felix/framework/util/ldap/Unknown.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.framework.util.ldap; - -/** - * This class is used to create simple marker instances that are inserted - * into the evaluation stack of a LDAP filter expression when an attribute - * is referenced that has no defined value. These invalid marker instances - * force the operators to throw an "unsupported type" exception, which the - * evaluator catches and then converts the entire subexpression containing - * the non-existent attribute to false. -**/ -class Unknown -{ -} diff --git a/framework/src/main/java/org/apache/felix/framework/util/manifestparser/ManifestParser.java b/framework/src/main/java/org/apache/felix/framework/util/manifestparser/ManifestParser.java new file mode 100644 index 00000000000..9158c6a4d13 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/util/manifestparser/ManifestParser.java @@ -0,0 +1,2192 @@ +/* + * 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.framework.util.manifestparser; + +import org.apache.felix.framework.BundleRevisionImpl; +import org.apache.felix.framework.Logger; +import org.apache.felix.framework.capabilityset.SimpleFilter; +import org.apache.felix.framework.util.FelixConstants; +import org.apache.felix.framework.wiring.BundleCapabilityImpl; +import org.apache.felix.framework.wiring.BundleRequirementImpl; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.Version; +import org.osgi.framework.VersionRange; +import org.osgi.framework.namespace.BundleNamespace; +import org.osgi.framework.namespace.ExecutionEnvironmentNamespace; +import org.osgi.framework.namespace.IdentityNamespace; +import org.osgi.framework.namespace.NativeNamespace; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class ManifestParser +{ + private static final String BUNDLE_LICENSE_HEADER = "Bundle-License"; // No constant defined by OSGi... + + private final Logger m_logger; + private final Map m_configMap; + private final Map m_headerMap; + private volatile int m_activationPolicy = BundleRevisionImpl.EAGER_ACTIVATION; + private volatile String m_activationIncludeDir; + private volatile String m_activationExcludeDir; + private volatile boolean m_isExtension = false; + private volatile String m_bundleSymbolicName; + private volatile Version m_bundleVersion; + private volatile List m_capabilities; + private volatile List m_requirements; + private volatile List m_libraryClauses; + private volatile boolean m_libraryHeadersOptional = false; + + public ManifestParser(Logger logger, Map configMap, BundleRevision owner, Map headerMap) + throws BundleException + { + m_logger = logger; + m_configMap = configMap; + m_headerMap = headerMap; + + // Verify that only manifest version 2 is specified. + String manifestVersion = getManifestVersion(m_headerMap); + if ((manifestVersion != null) && !manifestVersion.equals("2")) + { + throw new BundleException( + "Unknown 'Bundle-ManifestVersion' value: " + manifestVersion); + } + + // Create lists to hold capabilities and requirements. + List capList = new ArrayList(); + + // + // Parse bundle version. + // + + m_bundleVersion = Version.emptyVersion; + if (headerMap.get(Constants.BUNDLE_VERSION) != null) + { + try + { + m_bundleVersion = Version.parseVersion( + (String) headerMap.get(Constants.BUNDLE_VERSION)); + } + catch (RuntimeException ex) + { + // R4 bundle versions must parse, R3 bundle version may not. + if (getManifestVersion().equals("2")) + { + throw ex; + } + m_bundleVersion = Version.emptyVersion; + } + } + + // + // Parse bundle symbolic name. + // + + BundleCapabilityImpl bundleCap = parseBundleSymbolicName(owner, m_headerMap); + if (bundleCap != null) + { + m_bundleSymbolicName = (String) + bundleCap.getAttributes().get(BundleRevision.BUNDLE_NAMESPACE); + + // Add a bundle capability and a host capability to all + // non-fragment bundles. A host capability is the same + // as a require capability, but with a different capability + // namespace. Bundle capabilities resolve required-bundle + // dependencies, while host capabilities resolve fragment-host + // dependencies. + if (headerMap.get(Constants.FRAGMENT_HOST) == null) + { + // All non-fragment bundles have host capabilities. + capList.add(bundleCap); + // A non-fragment bundle can choose to not have a host capability. + String attachment = + bundleCap.getDirectives().get(Constants.FRAGMENT_ATTACHMENT_DIRECTIVE); + attachment = (attachment == null) + ? Constants.FRAGMENT_ATTACHMENT_RESOLVETIME + : attachment; + if (!attachment.equalsIgnoreCase(Constants.FRAGMENT_ATTACHMENT_NEVER)) + { + Map hostAttrs = + new HashMap(bundleCap.getAttributes()); + Object value = hostAttrs.remove(BundleRevision.BUNDLE_NAMESPACE); + hostAttrs.put(BundleRevision.HOST_NAMESPACE, value); + capList.add(new BundleCapabilityImpl( + owner, BundleRevision.HOST_NAMESPACE, + bundleCap.getDirectives(), + hostAttrs)); + } + } + + // + // Add the osgi.identity capability. + // + capList.add(addIdentityCapability(owner, headerMap, bundleCap)); + } + + // Verify that bundle symbolic name is specified. + if (getManifestVersion().equals("2") && (m_bundleSymbolicName == null)) + { + throw new BundleException( + "R4 bundle manifests must include bundle symbolic name."); + } + + m_isExtension = checkExtensionBundle(headerMap); + + // + // Parse Fragment-Host. + // + + List hostReqs = parseFragmentHost(m_logger, owner, m_headerMap); + + // + // Parse Require-Bundle + // + + List rbClauses = + parseStandardHeader((String) headerMap.get(Constants.REQUIRE_BUNDLE)); + rbClauses = normalizeRequireClauses(m_logger, rbClauses, getManifestVersion()); + List rbReqs = convertRequires(rbClauses, owner); + + // + // Parse Import-Package. + // + + List importClauses = + parseStandardHeader((String) headerMap.get(Constants.IMPORT_PACKAGE)); + importClauses = normalizeImportClauses(m_logger, importClauses, getManifestVersion()); + List importReqs = convertImports(importClauses, owner); + + // + // Parse DynamicImport-Package. + // + + List dynamicClauses = + parseStandardHeader((String) headerMap.get(Constants.DYNAMICIMPORT_PACKAGE)); + dynamicClauses = normalizeDynamicImportClauses(m_logger, dynamicClauses, getManifestVersion()); + List dynamicReqs = convertImports(dynamicClauses, owner); + + // + // Parse Require-Capability. + // + + List requireClauses = + parseStandardHeader((String) headerMap.get(Constants.REQUIRE_CAPABILITY)); + importClauses = normalizeCapabilityClauses( + m_logger, requireClauses, getManifestVersion()); + List requireReqs = convertRequireCapabilities(importClauses, owner); + + // + // Parse Bundle-RequiredExecutionEnvironment. + // + List breeReqs = + parseBreeHeader((String) headerMap.get(Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT), owner); + + // + // Parse Export-Package. + // + + List exportClauses = + parseStandardHeader((String) headerMap.get(Constants.EXPORT_PACKAGE)); + exportClauses = normalizeExportClauses(logger, exportClauses, + getManifestVersion(), m_bundleSymbolicName, m_bundleVersion); + List exportCaps = convertExports(exportClauses, owner); + + // + // Parse Provide-Capability. + // + + List provideClauses = + parseStandardHeader((String) headerMap.get(Constants.PROVIDE_CAPABILITY)); + provideClauses = normalizeCapabilityClauses( + logger, provideClauses, getManifestVersion()); + List provideCaps = convertProvideCapabilities(provideClauses, owner); + + // + // Calculate implicit imports. + // + + if (!getManifestVersion().equals("2")) + { + List implicitClauses = + calculateImplicitImports(exportCaps, importClauses); + importReqs.addAll(convertImports(implicitClauses, owner)); + + List allImportClauses = + new ArrayList(implicitClauses.size() + importClauses.size()); + allImportClauses.addAll(importClauses); + allImportClauses.addAll(implicitClauses); + + exportCaps = calculateImplicitUses(exportCaps, allImportClauses); + } + + // + // Parse Bundle-NativeCode. + // + + // Parse native library clauses. + m_libraryClauses = + parseLibraryStrings( + m_logger, + parseDelimitedString((String) m_headerMap.get(Constants.BUNDLE_NATIVECODE), ",")); + + // Check to see if there was an optional native library clause, which is + // represented by a null library header; if so, record it and remove it. + if (!m_libraryClauses.isEmpty() && + (m_libraryClauses.get(m_libraryClauses.size() - 1).getLibraryEntries() == null)) + { + m_libraryHeadersOptional = true; + m_libraryClauses.remove(m_libraryClauses.size() - 1); + } + + List nativeCodeReqs = convertNativeCode(owner, m_libraryClauses, m_libraryHeadersOptional); + + // Combine all requirements. + m_requirements = new ArrayList( + hostReqs.size() + importReqs.size() + rbReqs.size() + + requireReqs.size() + dynamicReqs.size() + breeReqs.size()); + m_requirements.addAll(hostReqs); + m_requirements.addAll(importReqs); + m_requirements.addAll(rbReqs); + m_requirements.addAll(requireReqs); + m_requirements.addAll(dynamicReqs); + m_requirements.addAll(breeReqs); + m_requirements.addAll(nativeCodeReqs); + + // Combine all capabilities. + m_capabilities = new ArrayList( + capList.size() + exportCaps.size() + provideCaps.size()); + m_capabilities.addAll(capList); + m_capabilities.addAll(exportCaps); + m_capabilities.addAll(provideCaps); + + // + // Parse activation policy. + // + + // This sets m_activationPolicy, m_includedPolicyClasses, and + // m_excludedPolicyClasses. + parseActivationPolicy(headerMap); + } + + private static List normalizeImportClauses( + Logger logger, List clauses, String mv) + throws BundleException + { + // Verify that the values are equals if the package specifies + // both version and specification-version attributes. + Set dupeSet = new HashSet(); + for (ParsedHeaderClause clause : clauses) + { + // Check for "version" and "specification-version" attributes + // and verify they are the same if both are specified. + Object v = clause.m_attrs.get(Constants.VERSION_ATTRIBUTE); + Object sv = clause.m_attrs.get(Constants.PACKAGE_SPECIFICATION_VERSION); + if ((v != null) && (sv != null)) + { + // Verify they are equal. + if (!((String) v).trim().equals(((String) sv).trim())) + { + throw new IllegalArgumentException( + "Both version and specification-version are specified, but they are not equal."); + } + } + + // Ensure that only the "version" attribute is used and convert + // it to the VersionRange type. + if ((v != null) || (sv != null)) + { + clause.m_attrs.remove(Constants.PACKAGE_SPECIFICATION_VERSION); + v = (v == null) ? sv : v; + clause.m_attrs.put( + Constants.VERSION_ATTRIBUTE, + new VersionRange(v.toString())); + } + + // If bundle version is specified, then convert its type to VersionRange. + v = clause.m_attrs.get(Constants.BUNDLE_VERSION_ATTRIBUTE); + if (v != null) + { + clause.m_attrs.put( + Constants.BUNDLE_VERSION_ATTRIBUTE, + new VersionRange(v.toString())); + } + + // Verify no duplicate imports. + for (String pkgName : clause.m_paths) + { + if (!dupeSet.contains(pkgName)) + { + + // The character "." has no meaning in the OSGi spec except + // when placed on the bundle class path. Some people, however, + // mistakenly think it means the default package when imported + // or exported. This is not correct. It is invalid. + if (pkgName.equals(".")) + { + throw new BundleException("Imporing '.' is invalid."); + } + // Make sure a package name was specified. + else if (pkgName.length() == 0) + { + throw new BundleException( + "Imported package names cannot be zero length."); + } + dupeSet.add(pkgName); + } + else + { + throw new BundleException("Duplicate import: " + pkgName); + } + } + + if (!mv.equals("2")) + { + // R3 bundles cannot have directives on their imports. + if (!clause.m_dirs.isEmpty()) + { + throw new BundleException("R3 imports cannot contain directives."); + } + + // Remove and ignore all attributes other than version. + // NOTE: This is checking for "version" rather than "specification-version" + // because the package class normalizes to "version" to avoid having + // future special cases. This could be changed if more strict behavior + // is required. + if (!clause.m_attrs.isEmpty()) + { + // R3 package requirements should only have version attributes. + Object pkgVersion = clause.m_attrs.get(BundleCapabilityImpl.VERSION_ATTR); + pkgVersion = (pkgVersion == null) + ? new VersionRange(VersionRange.LEFT_CLOSED, Version.emptyVersion, null, VersionRange.RIGHT_CLOSED) + : pkgVersion; + for (Entry entry : clause.m_attrs.entrySet()) + { + if (!entry.getKey().equals(BundleCapabilityImpl.VERSION_ATTR)) + { + logger.log(Logger.LOG_WARNING, + "Unknown R3 import attribute: " + + entry.getKey()); + } + } + + // Remove all other attributes except package version. + clause.m_attrs.clear(); + clause.m_attrs.put(BundleCapabilityImpl.VERSION_ATTR, pkgVersion); + } + } + } + + return clauses; + } + + public static List parseDynamicImportHeader( + Logger logger, BundleRevision owner, String header) + throws BundleException + { + + List importClauses = parseStandardHeader(header); + importClauses = normalizeDynamicImportClauses(logger, importClauses, "2"); + List reqs = convertImports(importClauses, owner); + return reqs; + } + + private static List convertImports( + List clauses, BundleRevision owner) + { + // Now convert generic header clauses into requirements. + List reqList = new ArrayList(); + for (ParsedHeaderClause clause : clauses) + { + for (String path : clause.m_paths) + { + // Prepend the package name to the array of attributes. + Map attrs = clause.m_attrs; + // Note that we use a linked hash map here to ensure the + // package attribute is first, which will make indexing + // more efficient. +// TODO: OSGi R4.3 - This is ordering is kind of hacky. + // Prepend the package name to the array of attributes. + Map newAttrs = new LinkedHashMap(attrs.size() + 1); + // We want this first from an indexing perspective. + newAttrs.put( + BundleRevision.PACKAGE_NAMESPACE, + path); + newAttrs.putAll(attrs); + // But we need to put it again to make sure it wasn't overwritten. + newAttrs.put( + BundleRevision.PACKAGE_NAMESPACE, + path); + + // Create filter now so we can inject filter directive. + SimpleFilter sf = SimpleFilter.convert(newAttrs); + + // Inject filter directive. +// TODO: OSGi R4.3 - Can we insert this on demand somehow? + Map dirs = clause.m_dirs; + Map newDirs = new HashMap(dirs.size() + 1); + newDirs.putAll(dirs); + newDirs.put( + Constants.FILTER_DIRECTIVE, + sf.toString()); + + // Create package requirement and add to requirement list. + reqList.add( + new BundleRequirementImpl( + owner, + BundleRevision.PACKAGE_NAMESPACE, + newDirs, + Collections.EMPTY_MAP, + sf)); + } + } + + return reqList; + } + + private static List normalizeDynamicImportClauses( + Logger logger, List clauses, String mv) + throws BundleException + { + // Verify that the values are equals if the package specifies + // both version and specification-version attributes. + for (ParsedHeaderClause clause : clauses) + { + if (!mv.equals("2")) + { + // R3 bundles cannot have directives on their imports. + if (!clause.m_dirs.isEmpty()) + { + throw new BundleException("R3 imports cannot contain directives."); + } + } + + // Add the resolution directive to indicate that these are + // dynamic imports. + clause.m_dirs.put(Constants.RESOLUTION_DIRECTIVE, + FelixConstants.RESOLUTION_DYNAMIC); + + // Check for "version" and "specification-version" attributes + // and verify they are the same if both are specified. + Object v = clause.m_attrs.get(Constants.VERSION_ATTRIBUTE); + Object sv = clause.m_attrs.get(Constants.PACKAGE_SPECIFICATION_VERSION); + if ((v != null) && (sv != null)) + { + // Verify they are equal. + if (!((String) v).trim().equals(((String) sv).trim())) + { + throw new IllegalArgumentException( + "Both version and specification-version are specified, but they are not equal."); + } + } + + // Ensure that only the "version" attribute is used and convert + // it to the VersionRange type. + if ((v != null) || (sv != null)) + { + clause.m_attrs.remove(Constants.PACKAGE_SPECIFICATION_VERSION); + v = (v == null) ? sv : v; + clause.m_attrs.put( + Constants.VERSION_ATTRIBUTE, + new VersionRange(v.toString())); + } + + // If bundle version is specified, then convert its type to VersionRange. + v = clause.m_attrs.get(Constants.BUNDLE_VERSION_ATTRIBUTE); + if (v != null) + { + clause.m_attrs.put( + Constants.BUNDLE_VERSION_ATTRIBUTE, + new VersionRange(v.toString())); + } + + // Dynamic imports can have duplicates, verify that no partial package name wild carding is used + for (String pkgName : clause.m_paths) + { + if (!pkgName.equals("*") && pkgName.endsWith("*") && !pkgName.endsWith(".*")) + { + throw new BundleException( + "Partial package name wild carding is not allowed: " + pkgName); + } + } + } + + return clauses; + } + + private static List convertRequireCapabilities( + List clauses, BundleRevision owner) + throws BundleException + { + // Now convert generic header clauses into requirements. + List reqList = new ArrayList(); + for (ParsedHeaderClause clause : clauses) + { + try + { + String filterStr = clause.m_dirs.get(Constants.FILTER_DIRECTIVE); + SimpleFilter sf = (filterStr != null) + ? SimpleFilter.parse(filterStr) + : new SimpleFilter(null, null, SimpleFilter.MATCH_ALL); + for (String path : clause.m_paths) + { + if (path.startsWith("osgi.wiring.")) + { + throw new BundleException("Manifest cannot use Require-Capability for '" + + path + + "' namespace."); + } + + // Create requirement and add to requirement list. + reqList.add( + new BundleRequirementImpl( + owner, + path, + clause.m_dirs, + clause.m_attrs, + sf)); + } + } + catch (Exception ex) + { + throw new BundleException("Error creating requirement: " + ex); + } + } + + return reqList; + } + + static List convertNativeCode(BundleRevision owner, List nativeLibraryClauses, boolean hasOptionalLibraryDirective) + { + List result = new ArrayList(); + + List nativeFilterClauseList = new ArrayList(); + + if(nativeLibraryClauses != null && !nativeLibraryClauses.isEmpty()) + { + for(NativeLibraryClause clause: nativeLibraryClauses) + { + String[] osNameArray = clause.getOSNames(); + String[] osVersionArray = clause.getOSVersions(); + String[] processorArray = clause.getProcessors(); + String[] languageArray = clause.getLanguages(); + + String currentSelectionFilter = clause.getSelectionFilter(); + + List nativeFilterList = new ArrayList(); + if(osNameArray != null && osNameArray.length > 0) + { + nativeFilterList.add(buildFilterFromArray(NativeNamespace.CAPABILITY_OSNAME_ATTRIBUTE, osNameArray, SimpleFilter.APPROX)); + } + + if(osVersionArray != null && osVersionArray.length > 0) + { + nativeFilterList.add(buildFilterFromArray(NativeNamespace.CAPABILITY_OSVERSION_ATTRIBUTE, osVersionArray, SimpleFilter.EQ)); + } + + if(processorArray != null && processorArray.length > 0) + { + nativeFilterList.add(buildFilterFromArray(NativeNamespace.CAPABILITY_PROCESSOR_ATTRIBUTE, processorArray, SimpleFilter.APPROX)); + } + + if(languageArray != null && languageArray.length > 0) + { + nativeFilterList.add(buildFilterFromArray(NativeNamespace.CAPABILITY_LANGUAGE_ATTRIBUTE, languageArray, SimpleFilter.APPROX)); + } + + if(currentSelectionFilter != null) + { + nativeFilterList.add(SimpleFilter.parse(currentSelectionFilter)); + } + + if(!nativeFilterList.isEmpty()) + { + SimpleFilter nativeClauseFilter = new SimpleFilter(null, nativeFilterList, SimpleFilter.AND); + nativeFilterClauseList.add(nativeClauseFilter); + } + } + + Map requirementDirectives = new HashMap(); + + SimpleFilter consolidatedNativeFilter = null; + + if(hasOptionalLibraryDirective) + { + requirementDirectives.put(NativeNamespace.REQUIREMENT_RESOLUTION_DIRECTIVE, NativeNamespace.RESOLUTION_OPTIONAL); + } + + if(nativeFilterClauseList.size() > 1) + { + consolidatedNativeFilter = new SimpleFilter(null, nativeFilterClauseList, SimpleFilter.OR); + + requirementDirectives.put(NativeNamespace.REQUIREMENT_FILTER_DIRECTIVE, consolidatedNativeFilter.toString()); + } + else if(nativeFilterClauseList.size() == 1) + { + consolidatedNativeFilter = nativeFilterClauseList.get(0); + + requirementDirectives.put(NativeNamespace.REQUIREMENT_FILTER_DIRECTIVE, consolidatedNativeFilter.toString()); + } + + if(requirementDirectives.size() > 0) + { + result.add(new BundleRequirementImpl(owner, NativeNamespace.NATIVE_NAMESPACE, requirementDirectives, + Collections.emptyMap(), + consolidatedNativeFilter)); + } + + } + + return result; + } + + private static SimpleFilter buildFilterFromArray(String attributeName, String[] stringArray, int operation) + { + SimpleFilter result = null; + List filterSet = new ArrayList(); + + if(stringArray != null) + { + for(String currentValue : stringArray) + { + filterSet.add(new SimpleFilter(attributeName, currentValue.toLowerCase(), operation)); + } + + if(filterSet.size() == 1) + { + result = filterSet.get(0); + } + else + { + result = new SimpleFilter(null, filterSet, SimpleFilter.OR); + } + } + + return result; + } + + private static List normalizeCapabilityClauses( + Logger logger, List clauses, String mv) + throws BundleException + { + + if (!mv.equals("2") && !clauses.isEmpty()) + { + // Should we error here if we are not an R4 bundle? + } + + // Convert attributes into specified types. + for (ParsedHeaderClause clause : clauses) + { + for (Entry entry : clause.m_types.entrySet()) + { + String type = entry.getValue(); + if (!type.equals("String")) + { + if (type.equals("Double")) + { + clause.m_attrs.put( + entry.getKey(), + new Double(clause.m_attrs.get(entry.getKey()).toString().trim())); + } + else if (type.equals("Version")) + { + clause.m_attrs.put( + entry.getKey(), + new Version(clause.m_attrs.get(entry.getKey()).toString().trim())); + } + else if (type.equals("Long")) + { + clause.m_attrs.put( + entry.getKey(), + new Long(clause.m_attrs.get(entry.getKey()).toString().trim())); + } + else if (type.startsWith("List")) + { + int startIdx = type.indexOf('<'); + int endIdx = type.indexOf('>'); + if (((startIdx > 0) && (endIdx <= startIdx)) + || ((startIdx < 0) && (endIdx > 0))) + { + throw new BundleException( + "Invalid Provide-Capability attribute list type for '" + + entry.getKey() + + "' : " + + type); + } + + String listType = "String"; + if (endIdx > startIdx) + { + listType = type.substring(startIdx + 1, endIdx).trim(); + } + + List tokens = parseDelimitedString( + clause.m_attrs.get(entry.getKey()).toString(), ",", false); + List values = new ArrayList(tokens.size()); + for (String token : tokens) + { + if (listType.equals("String")) + { + values.add(token); + } + else if (listType.equals("Double")) + { + values.add(new Double(token.trim())); + } + else if (listType.equals("Version")) + { + values.add(new Version(token.trim())); + } + else if (listType.equals("Long")) + { + values.add(new Long(token.trim())); + } + else + { + throw new BundleException( + "Unknown Provide-Capability attribute list type for '" + + entry.getKey() + + "' : " + + type); + } + } + clause.m_attrs.put( + entry.getKey(), + values); + } + else + { + throw new BundleException( + "Unknown Provide-Capability attribute type for '" + + entry.getKey() + + "' : " + + type); + } + } + } + } + + return clauses; + } + + private static List convertProvideCapabilities( + List clauses, BundleRevision owner) + throws BundleException + { + List capList = new ArrayList(); + for (ParsedHeaderClause clause : clauses) + { + for (String path : clause.m_paths) + { + if (path.startsWith("osgi.wiring.")) + { + throw new BundleException("Manifest cannot use Provide-Capability for '" + + path + + "' namespace."); + } + + if((path.startsWith(ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE) || + path.startsWith(NativeNamespace.NATIVE_NAMESPACE)) && (owner == null || + !FelixConstants.SYSTEM_BUNDLE_SYMBOLICNAME.equals(owner.getSymbolicName()))) + { + throw new BundleException("Only System Bundle can use Provide-Capability for '" + + path + + "' namespace.", BundleException.MANIFEST_ERROR); + } + + // Create package capability and add to capability list. + capList.add( + new BundleCapabilityImpl( + owner, + path, + clause.m_dirs, + clause.m_attrs)); + } + } + + return capList; + } + + private static List normalizeExportClauses( + Logger logger, List clauses, + String mv, String bsn, Version bv) + throws BundleException + { + for (ParsedHeaderClause clause : clauses) + { + // Verify that the named package has not already been declared. + for (String pkgName : clause.m_paths) + { + // Verify that java.* packages are not exported (except from the system bundle). + if (!FelixConstants.SYSTEM_BUNDLE_SYMBOLICNAME.equals(bsn) && pkgName.startsWith("java.")) + { + throw new BundleException( + "Exporting java.* packages not allowed: " + + pkgName, BundleException.MANIFEST_ERROR); + } + // The character "." has no meaning in the OSGi spec except + // when placed on the bundle class path. Some people, however, + // mistakenly think it means the default package when imported + // or exported. This is not correct. It is invalid. + else if (pkgName.equals(".")) + { + throw new BundleException("Exporing '.' is invalid."); + } + // Make sure a package name was specified. + else if (pkgName.length() == 0) + { + throw new BundleException( + "Exported package names cannot be zero length."); + } + } + + // Check for "version" and "specification-version" attributes + // and verify they are the same if both are specified. + Object v = clause.m_attrs.get(Constants.VERSION_ATTRIBUTE); + Object sv = clause.m_attrs.get(Constants.PACKAGE_SPECIFICATION_VERSION); + if ((v != null) && (sv != null)) + { + // Verify they are equal. + if (!((String) v).trim().equals(((String) sv).trim())) + { + throw new IllegalArgumentException( + "Both version and specification-version are specified, but they are not equal."); + } + } + + // Always add the default version if not specified. + if ((v == null) && (sv == null)) + { + v = Version.emptyVersion; + } + + // Ensure that only the "version" attribute is used and convert + // it to the appropriate type. + if ((v != null) || (sv != null)) + { + // Convert version attribute to type Version. + clause.m_attrs.remove(Constants.PACKAGE_SPECIFICATION_VERSION); + v = (v == null) ? sv : v; + clause.m_attrs.put( + Constants.VERSION_ATTRIBUTE, + Version.parseVersion(v.toString())); + } + + // If this is an R4 bundle, then make sure it doesn't specify + // bundle symbolic name or bundle version attributes. + if (mv.equals("2")) + { + // Find symbolic name and version attribute, if present. + if (clause.m_attrs.containsKey(Constants.BUNDLE_VERSION_ATTRIBUTE) + || clause.m_attrs.containsKey(Constants.BUNDLE_SYMBOLICNAME_ATTRIBUTE)) + { + throw new BundleException( + "Exports must not specify bundle symbolic name or bundle version."); + } + + // Now that we know that there are no bundle symbolic name and version + // attributes, add them since the spec says they are there implicitly. + clause.m_attrs.put(Constants.BUNDLE_SYMBOLICNAME_ATTRIBUTE, bsn); + clause.m_attrs.put(Constants.BUNDLE_VERSION_ATTRIBUTE, bv); + } + else if (!mv.equals("2")) + { + // R3 bundles cannot have directives on their exports. + if (!clause.m_dirs.isEmpty()) + { + throw new BundleException("R3 exports cannot contain directives."); + } + + // Remove and ignore all attributes other than version. + // NOTE: This is checking for "version" rather than "specification-version" + // because the package class normalizes to "version" to avoid having + // future special cases. This could be changed if more strict behavior + // is required. + if (!clause.m_attrs.isEmpty()) + { + // R3 package capabilities should only have a version attribute. + Object pkgVersion = clause.m_attrs.get(BundleCapabilityImpl.VERSION_ATTR); + pkgVersion = (pkgVersion == null) + ? Version.emptyVersion + : pkgVersion; + for (Entry entry : clause.m_attrs.entrySet()) + { + if (!entry.getKey().equals(BundleCapabilityImpl.VERSION_ATTR)) + { + logger.log( + Logger.LOG_WARNING, + "Unknown R3 export attribute: " + + entry.getKey()); + } + } + + // Remove all other attributes except package version. + clause.m_attrs.clear(); + clause.m_attrs.put(BundleCapabilityImpl.VERSION_ATTR, pkgVersion); + } + } + } + + return clauses; + } + + private static List convertExports( + List clauses, BundleRevision owner) + { + List capList = new ArrayList(); + for (ParsedHeaderClause clause : clauses) + { + for (String pkgName : clause.m_paths) + { + // Prepend the package name to the array of attributes. + Map attrs = clause.m_attrs; + Map newAttrs = new HashMap(attrs.size() + 1); + newAttrs.putAll(attrs); + newAttrs.put( + BundleRevision.PACKAGE_NAMESPACE, + pkgName); + + // Create package capability and add to capability list. + capList.add( + new BundleCapabilityImpl( + owner, + BundleRevision.PACKAGE_NAMESPACE, + clause.m_dirs, + newAttrs)); + } + } + + return capList; + } + + public String getManifestVersion() + { + String manifestVersion = getManifestVersion(m_headerMap); + return (manifestVersion == null) ? "1" : manifestVersion; + } + + private static String getManifestVersion(Map headerMap) + { + String manifestVersion = (String) headerMap.get(Constants.BUNDLE_MANIFESTVERSION); + return (manifestVersion == null) ? null : manifestVersion.trim(); + } + + public int getActivationPolicy() + { + return m_activationPolicy; + } + + public String getActivationIncludeDirective() + { + return m_activationIncludeDir; + } + + public String getActivationExcludeDirective() + { + return m_activationExcludeDir; + } + + public boolean isExtension() + { + return m_isExtension; + } + + public String getSymbolicName() + { + return m_bundleSymbolicName; + } + + public Version getBundleVersion() + { + return m_bundleVersion; + } + + public List getCapabilities() + { + return m_capabilities; + } + + public List getRequirements() + { + return m_requirements; + } + + /** + *

      + * This method returns the selected native library metadata from + * the manifest. The information is not the raw metadata from the + * manifest, but is the native library clause selected according + * to the OSGi native library clause selection policy. The metadata + * returned by this method will be attached directly to a module and + * used for finding its native libraries at run time. To inspect the + * raw native library metadata refer to getLibraryClauses(). + *

      + *

      + * This method returns one of three values: + *

      + *
        + *
      • null - if the are no native libraries for this module; + * this may also indicate the native libraries are optional and + * did not match the current platform.
      • + *
      • Zero-length NativeLibrary array - if no matching native library + * clause was found; this bundle should not resolve.
      • + *
      • Nonzero-length NativeLibrary array - the native libraries + * associated with the matching native library clause.
      • + *
      + * + * @return null if there are no native libraries, a zero-length + * array if no libraries matched, or an array of selected libraries. + **/ + public List getLibraries() + { + ArrayList libs = null; + try + { + NativeLibraryClause clause = getSelectedLibraryClause(); + if (clause != null) + { + String[] entries = clause.getLibraryEntries(); + libs = new ArrayList(entries.length); + int current = 0; + for (int i = 0; i < entries.length; i++) + { + String name = getName(entries[i]); + boolean found = false; + for (int j = 0; !found && (j < current); j++) + { + found = getName(entries[j]).equals(name); + } + if (!found) + { + libs.add(new NativeLibrary( + clause.getLibraryEntries()[i], + clause.getOSNames(), clause.getProcessors(), clause.getOSVersions(), + clause.getLanguages(), clause.getSelectionFilter())); + } + } + libs.trimToSize(); + } + } + catch (Exception ex) + { + libs = new ArrayList(0); + } + return libs; + } + + private String getName(String path) + { + int idx = path.lastIndexOf('/'); + if (idx > -1) + { + return path.substring(idx); + } + return path; + } + + private NativeLibraryClause getSelectedLibraryClause() throws BundleException + { + if ((m_libraryClauses != null) && (m_libraryClauses.size() > 0)) + { + List clauseList = new ArrayList(); + + // Search for matching native clauses. + for (NativeLibraryClause libraryClause : m_libraryClauses) + { + if (libraryClause.match(m_configMap)) + { + clauseList.add(libraryClause); + } + } + + // Select the matching native clause. + int selected = 0; + if (clauseList.isEmpty()) + { + // If optional clause exists, no error thrown. + if (m_libraryHeadersOptional) + { + return null; + } + else + { + throw new BundleException("Unable to select a native library clause."); + } + } + else if (clauseList.size() == 1) + { + selected = 0; + } + else if (clauseList.size() > 1) + { + selected = firstSortedClause(clauseList); + } + return ((NativeLibraryClause) clauseList.get(selected)); + } + + return null; + } + + private int firstSortedClause(List clauseList) + { + ArrayList indexList = new ArrayList(); + ArrayList selection = new ArrayList(); + + // Init index list + for (int i = 0; i < clauseList.size(); i++) + { + indexList.add("" + i); + } + + // Select clause with 'osversion' range declared + // and get back the max floor of 'osversion' ranges. + Version osVersionRangeMaxFloor = new Version(0, 0, 0); + for (int i = 0; i < indexList.size(); i++) + { + int index = Integer.parseInt(indexList.get(i).toString()); + String[] osversions = ((NativeLibraryClause) clauseList.get(index)).getOSVersions(); + if (osversions != null) + { + selection.add("" + indexList.get(i)); + } + for (int k = 0; (osversions != null) && (k < osversions.length); k++) + { + VersionRange range = new VersionRange(osversions[k]); + if ((range.getLeft()).compareTo(osVersionRangeMaxFloor) >= 0) + { + osVersionRangeMaxFloor = range.getLeft(); + } + } + } + + if (selection.size() == 1) + { + return Integer.parseInt(selection.get(0).toString()); + } + else if (selection.size() > 1) + { + // Keep only selected clauses with an 'osversion' + // equal to the max floor of 'osversion' ranges. + indexList = selection; + selection = new ArrayList(); + for (int i = 0; i < indexList.size(); i++) + { + int index = Integer.parseInt(indexList.get(i).toString()); + String[] osversions = ((NativeLibraryClause) clauseList.get(index)).getOSVersions(); + for (int k = 0; k < osversions.length; k++) + { + VersionRange range = new VersionRange(osversions[k]); + if ((range.getLeft()).compareTo(osVersionRangeMaxFloor) >= 0) + { + selection.add("" + indexList.get(i)); + } + } + } + } + + if (selection.isEmpty()) + { + // Re-init index list. + selection.clear(); + indexList.clear(); + for (int i = 0; i < clauseList.size(); i++) + { + indexList.add("" + i); + } + } + else if (selection.size() == 1) + { + return Integer.parseInt(selection.get(0).toString()); + } + else + { + indexList = selection; + selection.clear(); + } + + // Keep only clauses with 'language' declared. + for (int i = 0; i < indexList.size(); i++) + { + int index = Integer.parseInt(indexList.get(i).toString()); + if (((NativeLibraryClause) clauseList.get(index)).getLanguages() != null) + { + selection.add("" + indexList.get(i)); + } + } + + // Return the first sorted clause + if (selection.isEmpty()) + { + return 0; + } + else + { + return Integer.parseInt(selection.get(0).toString()); + } + } + + private static List calculateImplicitImports( + List exports, List imports) + throws BundleException + { + List clauseList = new ArrayList(); + + // Since all R3 exports imply an import, add a corresponding + // requirement for each existing export capability. Do not + // duplicate imports. + Map map = new HashMap(); + // Add existing imports. + for (int impIdx = 0; impIdx < imports.size(); impIdx++) + { + for (int pathIdx = 0; pathIdx < imports.get(impIdx).m_paths.size(); pathIdx++) + { + map.put( + imports.get(impIdx).m_paths.get(pathIdx), + imports.get(impIdx).m_paths.get(pathIdx)); + } + } + // Add import requirement for each export capability. + for (int i = 0; i < exports.size(); i++) + { + if (map.get(exports.get(i).getAttributes() + .get(BundleRevision.PACKAGE_NAMESPACE)) == null) + { + // Convert Version to VersionRange. + Map attrs = new HashMap(); + Object version = exports.get(i).getAttributes().get(Constants.VERSION_ATTRIBUTE); + if (version != null) + { + attrs.put( + Constants.VERSION_ATTRIBUTE, + new VersionRange(version.toString())); + } + + List paths = new ArrayList(); + paths.add((String) + exports.get(i).getAttributes().get(BundleRevision.PACKAGE_NAMESPACE)); + clauseList.add( + new ParsedHeaderClause( + paths, Collections.EMPTY_MAP, attrs, Collections.EMPTY_MAP)); + } + } + + return clauseList; + } + + private static List calculateImplicitUses( + List exports, List imports) + throws BundleException + { + // Add a "uses" directive onto each export of R3 bundles + // that references every other import (which will include + // exports, since export implies import); this is + // necessary since R3 bundles assumed a single class space, + // but R4 allows for multiple class spaces. + String usesValue = ""; + for (int i = 0; i < imports.size(); i++) + { + for (int pathIdx = 0; pathIdx < imports.get(i).m_paths.size(); pathIdx++) + { + usesValue = usesValue + + ((usesValue.length() > 0) ? "," : "") + + imports.get(i).m_paths.get(pathIdx); + } + } + for (int i = 0; i < exports.size(); i++) + { + Map dirs = new HashMap(1); + dirs.put(Constants.USES_DIRECTIVE, usesValue); + exports.set(i, new BundleCapabilityImpl( + exports.get(i).getRevision(), + BundleRevision.PACKAGE_NAMESPACE, + dirs, + exports.get(i).getAttributes())); + } + + return exports; + } + + private static boolean checkExtensionBundle(Map headerMap) throws BundleException + { + Object extension = parseExtensionBundleHeader( + (String) headerMap.get(Constants.FRAGMENT_HOST)); + + if (extension != null) + { + if (!(Constants.EXTENSION_FRAMEWORK.equals(extension) || + Constants.EXTENSION_BOOTCLASSPATH.equals(extension))) + { + throw new BundleException( + "Extension bundle must have either 'extension:=framework' or 'extension:=bootclasspath'"); + } + if (headerMap.containsKey(Constants.REQUIRE_BUNDLE) || + headerMap.containsKey(Constants.BUNDLE_NATIVECODE) || + headerMap.containsKey(Constants.DYNAMICIMPORT_PACKAGE) || + headerMap.containsKey(Constants.BUNDLE_ACTIVATOR)) + { + throw new BundleException("Invalid extension bundle manifest"); + } + return true; + } + return false; + } + + private static BundleCapabilityImpl parseBundleSymbolicName( + BundleRevision owner, Map headerMap) + throws BundleException + { + List clauses = parseStandardHeader( + (String) headerMap.get(Constants.BUNDLE_SYMBOLICNAME)); + if (clauses.size() > 0) + { + if (clauses.size() > 1) + { + throw new BundleException( + "Cannot have multiple symbolic names: " + + headerMap.get(Constants.BUNDLE_SYMBOLICNAME)); + } + else if (clauses.get(0).m_paths.size() > 1) + { + throw new BundleException( + "Cannot have multiple symbolic names: " + + headerMap.get(Constants.BUNDLE_SYMBOLICNAME)); + } + + // Get bundle version. + Version bundleVersion = Version.emptyVersion; + if (headerMap.get(Constants.BUNDLE_VERSION) != null) + { + try + { + bundleVersion = Version.parseVersion( + (String) headerMap.get(Constants.BUNDLE_VERSION)); + } + catch (RuntimeException ex) + { + // R4 bundle versions must parse, R3 bundle version may not. + String mv = getManifestVersion(headerMap); + if (mv != null) + { + throw ex; + } + bundleVersion = Version.emptyVersion; + } + } + + // Create a require capability and return it. + String symName = (String) clauses.get(0).m_paths.get(0); + clauses.get(0).m_attrs.put(BundleRevision.BUNDLE_NAMESPACE, symName); + clauses.get(0).m_attrs.put(Constants.BUNDLE_VERSION_ATTRIBUTE, bundleVersion); + return new BundleCapabilityImpl( + owner, + BundleRevision.BUNDLE_NAMESPACE, + clauses.get(0).m_dirs, + clauses.get(0).m_attrs); + } + + return null; + } + + private static BundleCapabilityImpl addIdentityCapability(BundleRevision owner, + Map headerMap, BundleCapabilityImpl bundleCap) + { + Map attrs = new HashMap(); + + attrs.put(IdentityNamespace.IDENTITY_NAMESPACE, + bundleCap.getAttributes().get(BundleNamespace.BUNDLE_NAMESPACE)); + attrs.put(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE, + headerMap.get(Constants.FRAGMENT_HOST) == null + ? IdentityNamespace.TYPE_BUNDLE + : IdentityNamespace.TYPE_FRAGMENT); + attrs.put(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE, + bundleCap.getAttributes().get(Constants.BUNDLE_VERSION_ATTRIBUTE)); + + if (headerMap.get(Constants.BUNDLE_COPYRIGHT) != null) + { + attrs.put(IdentityNamespace.CAPABILITY_COPYRIGHT_ATTRIBUTE, + headerMap.get(Constants.BUNDLE_COPYRIGHT)); + } + + if (headerMap.get(Constants.BUNDLE_DESCRIPTION) != null) + { + attrs.put(IdentityNamespace.CAPABILITY_DESCRIPTION_ATTRIBUTE, + headerMap.get(Constants.BUNDLE_DESCRIPTION)); + } + if (headerMap.get(Constants.BUNDLE_DOCURL) != null) + { + attrs.put(IdentityNamespace.CAPABILITY_DOCUMENTATION_ATTRIBUTE, + headerMap.get(Constants.BUNDLE_DOCURL)); + } + if (headerMap.get(BUNDLE_LICENSE_HEADER) != null) + { + attrs.put(IdentityNamespace.CAPABILITY_LICENSE_ATTRIBUTE, + headerMap.get(BUNDLE_LICENSE_HEADER)); + } + + Map dirs; + if (bundleCap.getDirectives().get(Constants.SINGLETON_DIRECTIVE) != null) + { + dirs = Collections.singletonMap(IdentityNamespace.CAPABILITY_SINGLETON_DIRECTIVE, + bundleCap.getDirectives().get(Constants.SINGLETON_DIRECTIVE)); + } + else + { + dirs = Collections.emptyMap(); + } + return new BundleCapabilityImpl(owner, IdentityNamespace.IDENTITY_NAMESPACE, dirs, attrs); + } + + private static List parseFragmentHost( + Logger logger, BundleRevision owner, Map headerMap) + throws BundleException + { + List reqs = new ArrayList(); + + String mv = getManifestVersion(headerMap); + if ((mv != null) && mv.equals("2")) + { + List clauses = parseStandardHeader( + (String) headerMap.get(Constants.FRAGMENT_HOST)); + if (clauses.size() > 0) + { + // Make sure that only one fragment host symbolic name is specified. + if (clauses.size() > 1) + { + throw new BundleException( + "Fragments cannot have multiple hosts: " + + headerMap.get(Constants.FRAGMENT_HOST)); + } + else if (clauses.get(0).m_paths.size() > 1) + { + throw new BundleException( + "Fragments cannot have multiple hosts: " + + headerMap.get(Constants.FRAGMENT_HOST)); + } + + // If the bundle-version attribute is specified, then convert + // it to the proper type. + Object value = clauses.get(0).m_attrs.get(Constants.BUNDLE_VERSION_ATTRIBUTE); + value = (value == null) ? "0.0.0" : value; + if (value != null) + { + clauses.get(0).m_attrs.put( + Constants.BUNDLE_VERSION_ATTRIBUTE, + new VersionRange(value.toString())); + } + + // Note that we use a linked hash map here to ensure the + // host symbolic name is first, which will make indexing + // more efficient. +// TODO: OSGi R4.3 - This is ordering is kind of hacky. + // Prepend the host symbolic name to the map of attributes. + Map attrs = clauses.get(0).m_attrs; + Map newAttrs = new LinkedHashMap(attrs.size() + 1); + // We want this first from an indexing perspective. + newAttrs.put( + BundleRevision.HOST_NAMESPACE, + clauses.get(0).m_paths.get(0)); + newAttrs.putAll(attrs); + // But we need to put it again to make sure it wasn't overwritten. + newAttrs.put( + BundleRevision.HOST_NAMESPACE, + clauses.get(0).m_paths.get(0)); + + // Create filter now so we can inject filter directive. + SimpleFilter sf = SimpleFilter.convert(newAttrs); + + // Inject filter directive. +// TODO: OSGi R4.3 - Can we insert this on demand somehow? + Map dirs = clauses.get(0).m_dirs; + Map newDirs = new HashMap(dirs.size() + 1); + newDirs.putAll(dirs); + newDirs.put( + Constants.FILTER_DIRECTIVE, + sf.toString()); + + reqs.add(new BundleRequirementImpl( + owner, BundleRevision.HOST_NAMESPACE, + newDirs, + newAttrs)); + } + } + else if (headerMap.get(Constants.FRAGMENT_HOST) != null) + { + String s = (String) headerMap.get(Constants.BUNDLE_SYMBOLICNAME); + s = (s == null) ? (String) headerMap.get(Constants.BUNDLE_NAME) : s; + s = (s == null) ? headerMap.toString() : s; + logger.log( + Logger.LOG_WARNING, + "Only R4 bundles can be fragments: " + s); + } + + return reqs; + } + + private static List parseBreeHeader(String header, BundleRevision owner) + { + List filters = new ArrayList(); + for (String entry : parseDelimitedString(header, ",")) + { + List names = parseDelimitedString(entry, "/"); + List left = parseDelimitedString(names.get(0), "-"); + + String lName = left.get(0); + Version lVer; + try + { + lVer = Version.parseVersion(left.get(1)); + } + catch (Exception ex) + { + // Version doesn't parse. Make it part of the name. + lName = names.get(0); + lVer = null; + } + + String rName = null; + Version rVer = null; + if (names.size() > 1) + { + List right = parseDelimitedString(names.get(1), "-"); + rName = right.get(0); + try + { + rVer = Version.parseVersion(right.get(1)); + } + catch (Exception ex) + { + rName = names.get(1); + rVer = null; + } + } + + String versionClause; + if (lVer != null) + { + if ((rVer != null) && (!rVer.equals(lVer))) + { + // Both versions are defined, but different. Make each of them part of the name + lName = names.get(0); + rName = names.get(1); + versionClause = null; + } + else + { + versionClause = getBreeVersionClause(lVer); + } + } + else + { + versionClause = getBreeVersionClause(rVer); + } + + if ("J2SE".equals(lName)) + { + // J2SE is not used in the Capability variant of BREE, use JavaSE here + // This can only happen with the lName part... + lName = "JavaSE"; + } + + String nameClause; + if (rName != null) + nameClause = "(" + ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE + "=" + lName + "/" + rName + ")"; + else + nameClause = "(" + ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE + "=" + lName + ")"; + + String filter; + if (versionClause != null) + filter = "(&" + nameClause + versionClause + ")"; + else + filter = nameClause; + + filters.add(filter); + } + + if (filters.size() == 0) + { + return Collections.emptyList(); + } + else + { + String reqFilter; + if (filters.size() == 1) + { + reqFilter = filters.get(0); + } + else + { + // If there are more BREE filters, we need to or them together + StringBuilder sb = new StringBuilder("(|"); + for (String f : filters) + { + sb.append(f); + } + sb.append(")"); + reqFilter = sb.toString(); + } + + SimpleFilter sf = SimpleFilter.parse(reqFilter); + return Collections.singletonList(new BundleRequirementImpl( + owner, + ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE, + Collections.singletonMap(ExecutionEnvironmentNamespace.REQUIREMENT_FILTER_DIRECTIVE, reqFilter), + Collections.emptyMap(), + sf)); + } + } + + private static String getBreeVersionClause(Version ver) + { + if (ver == null) + return null; + + return "(" + ExecutionEnvironmentNamespace.CAPABILITY_VERSION_ATTRIBUTE + "=" + ver + ")"; + } + + private static List normalizeRequireClauses( + Logger logger, List clauses, String mv) + { + // R3 bundles cannot require other bundles. + if (!mv.equals("2")) + { + clauses.clear(); + } + else + { + // Convert bundle version attribute to VersionRange type. + for (ParsedHeaderClause clause : clauses) + { + Object value = clause.m_attrs.get(Constants.BUNDLE_VERSION_ATTRIBUTE); + if (value != null) + { + clause.m_attrs.put( + Constants.BUNDLE_VERSION_ATTRIBUTE, + new VersionRange(value.toString())); + } + } + } + + return clauses; + } + + private static List convertRequires( + List clauses, BundleRevision owner) + { + List reqList = new ArrayList(); + for (ParsedHeaderClause clause : clauses) + { + for (String path : clause.m_paths) + { + // Prepend the bundle symbolic name to the array of attributes. + Map attrs = clause.m_attrs; + // Note that we use a linked hash map here to ensure the + // symbolic name attribute is first, which will make indexing + // more efficient. +// TODO: OSGi R4.3 - This is ordering is kind of hacky. + // Prepend the symbolic name to the array of attributes. + Map newAttrs = new LinkedHashMap(attrs.size() + 1); + // We want this first from an indexing perspective. + newAttrs.put( + BundleRevision.BUNDLE_NAMESPACE, + path); + newAttrs.putAll(attrs); + // But we need to put it again to make sure it wasn't overwritten. + newAttrs.put( + BundleRevision.BUNDLE_NAMESPACE, + path); + + // Create filter now so we can inject filter directive. + SimpleFilter sf = SimpleFilter.convert(newAttrs); + + // Inject filter directive. +// TODO: OSGi R4.3 - Can we insert this on demand somehow? + Map dirs = clause.m_dirs; + Map newDirs = new HashMap(dirs.size() + 1); + newDirs.putAll(dirs); + newDirs.put( + Constants.FILTER_DIRECTIVE, + sf.toString()); + + // Create package requirement and add to requirement list. + reqList.add( + new BundleRequirementImpl( + owner, + BundleRevision.BUNDLE_NAMESPACE, + newDirs, + newAttrs)); + } + } + + return reqList; + } + + public static String parseExtensionBundleHeader(String header) + throws BundleException + { + List clauses = parseStandardHeader(header); + + String result = null; + + if (clauses.size() == 1) + { + for (Entry entry : clauses.get(0).m_dirs.entrySet()) + { + if (Constants.EXTENSION_DIRECTIVE.equals(entry.getKey())) + { + result = entry.getValue(); + } + } + + if (FelixConstants.SYSTEM_BUNDLE_SYMBOLICNAME.equals(clauses.get(0).m_paths.get(0)) || + Constants.SYSTEM_BUNDLE_SYMBOLICNAME.equals(clauses.get(0).m_paths.get(0))) + { + result = (result == null) ? Constants.EXTENSION_FRAMEWORK : result; + } + else if (result != null) + { + throw new BundleException( + "Only the system bundle can have extension bundles."); + } + } + + return result; + } + + private void parseActivationPolicy(Map headerMap) + { + m_activationPolicy = BundleRevisionImpl.EAGER_ACTIVATION; + + List clauses = parseStandardHeader( + (String) headerMap.get(Constants.BUNDLE_ACTIVATIONPOLICY)); + + if (clauses.size() > 0) + { + // Just look for a "path" matching the lazy policy, ignore + // everything else. + for (String path : clauses.get(0).m_paths) + { + if (path.equals(Constants.ACTIVATION_LAZY)) + { + m_activationPolicy = BundleRevisionImpl.LAZY_ACTIVATION; + for (Entry entry : clauses.get(0).m_dirs.entrySet()) + { + if (entry.getKey().equalsIgnoreCase(Constants.INCLUDE_DIRECTIVE)) + { + m_activationIncludeDir = entry.getValue(); + } + else if (entry.getKey().equalsIgnoreCase(Constants.EXCLUDE_DIRECTIVE)) + { + m_activationExcludeDir = entry.getValue(); + } + } + break; + } + } + } + } + + // Like this: path; path; dir1:=dirval1; dir2:=dirval2; attr1=attrval1; attr2=attrval2, + // path; path; dir1:=dirval1; dir2:=dirval2; attr1=attrval1; attr2=attrval2 + public static void main(String[] headers) + { + String header = headers[0]; + if (header != null) + { + if (header.length() == 0) + { + throw new IllegalArgumentException( + "A header cannot be an empty string."); + } + List clauses = parseStandardHeader(header); + + for (ParsedHeaderClause clause : clauses) + { + System.out.println("PATHS " + clause.m_paths); + System.out.println(" DIRS " + clause.m_dirs); + System.out.println(" ATTRS " + clause.m_attrs); + System.out.println(" TYPES " + clause.m_types); + } + + } + } + + private static final char EOF = (char) -1; + + private static char charAt(int pos, String headers, int length) + { + if (pos >= length) + { + return EOF; + } + return headers.charAt(pos); + } + + private static final int CLAUSE_START = 0; + private static final int PARAMETER_START = 1; + private static final int KEY = 2; + private static final int DIRECTIVE_OR_TYPEDATTRIBUTE = 4; + private static final int ARGUMENT = 8; + private static final int VALUE = 16; + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static List parseStandardHeader(String header) + { + List clauses = new ArrayList(); + if (header == null) + { + return clauses; + } + ParsedHeaderClause clause = null; + String key = null; + Map targetMap = null; + int state = CLAUSE_START; + int currentPosition = 0; + int startPosition = 0; + int length = header.length(); + boolean quoted = false; + boolean escaped = false; + + char currentChar = EOF; + do + { + currentChar = charAt(currentPosition, header, length); + switch (state) + { + case CLAUSE_START: + clause = new ParsedHeaderClause( + new ArrayList(), + new HashMap(), + new HashMap(), + new HashMap()); + clauses.add(clause); + state = PARAMETER_START; + case PARAMETER_START: + startPosition = currentPosition; + state = KEY; + case KEY: + switch (currentChar) + { + case ':': + case '=': + key = header.substring(startPosition, currentPosition).trim(); + startPosition = currentPosition + 1; + targetMap = clause.m_attrs; + state = currentChar == ':' ? DIRECTIVE_OR_TYPEDATTRIBUTE : ARGUMENT; + break; + case EOF: + case ',': + case ';': + clause.m_paths.add(header.substring(startPosition, currentPosition).trim()); + state = currentChar == ',' ? CLAUSE_START : PARAMETER_START; + break; + default: + break; + } + currentPosition++; + break; + case DIRECTIVE_OR_TYPEDATTRIBUTE: + switch(currentChar) + { + case '=': + if (startPosition != currentPosition) + { + clause.m_types.put(key, header.substring(startPosition, currentPosition).trim()); + } + else + { + targetMap = clause.m_dirs; + } + state = ARGUMENT; + startPosition = currentPosition + 1; + break; + default: + break; + } + currentPosition++; + break; + case ARGUMENT: + if (currentChar == '\"') + { + quoted = true; + currentPosition++; + } + else + { + quoted = false; + } + if (!Character.isWhitespace(currentChar)) { + state = VALUE; + } + else { + currentPosition++; + } + break; + case VALUE: + if (escaped) + { + escaped = false; + } + else + { + if (currentChar == '\\' ) + { + escaped = true; + } + else if (quoted && currentChar == '\"') + { + quoted = false; + } + else if (!quoted) + { + String value = null; + switch(currentChar) + { + case EOF: + case ';': + case ',': + value = header.substring(startPosition, currentPosition).trim(); + if (value.startsWith("\"") && value.endsWith("\"")) + { + value = value.substring(1, value.length() - 1); + } + if (targetMap.put(key, value) != null) + { + throw new IllegalArgumentException( + "Duplicate '" + key + "' in: " + header); + } + state = currentChar == ';' ? PARAMETER_START : CLAUSE_START; + break; + default: + break; + } + } + } + currentPosition++; + break; + default: + break; + } + } while ( currentChar != EOF); + + if (state > PARAMETER_START) + { + throw new IllegalArgumentException("Unable to parse header: " + header); + } + return clauses; + } + + public static List parseDelimitedString(String value, String delim) + { + return parseDelimitedString(value, delim, true); + } + + /** + * Parses delimited string and returns an array containing the tokens. This + * parser obeys quotes, so the delimiter character will be ignored if it is + * inside of a quote. This method assumes that the quote character is not + * included in the set of delimiter characters. + * @param value the delimited string to parse. + * @param delim the characters delimiting the tokens. + * @return a list of string or an empty list if there are none. + **/ + public static List parseDelimitedString(String value, String delim, boolean trim) + { + if (value == null) + { + value = ""; + } + + List list = new ArrayList(); + + int CHAR = 1; + int DELIMITER = 2; + int STARTQUOTE = 4; + int ENDQUOTE = 8; + + StringBuilder sb = new StringBuilder(); + + int expecting = (CHAR | DELIMITER | STARTQUOTE); + + boolean isEscaped = false; + for (int i = 0; i < value.length(); i++) + { + char c = value.charAt(i); + + boolean isDelimiter = (delim.indexOf(c) >= 0); + + if (!isEscaped && (c == '\\')) + { + isEscaped = true; + continue; + } + + if (isEscaped) + { + sb.append(c); + } + else if (isDelimiter && ((expecting & DELIMITER) > 0)) + { + if (trim) + { + list.add(sb.toString().trim()); + } + else + { + list.add(sb.toString()); + } + sb.delete(0, sb.length()); + expecting = (CHAR | DELIMITER | STARTQUOTE); + } + else if ((c == '"') && ((expecting & STARTQUOTE) > 0)) + { + sb.append(c); + expecting = CHAR | ENDQUOTE; + } + else if ((c == '"') && ((expecting & ENDQUOTE) > 0)) + { + sb.append(c); + expecting = (CHAR | STARTQUOTE | DELIMITER); + } + else if ((expecting & CHAR) > 0) + { + sb.append(c); + } + else + { + throw new IllegalArgumentException("Invalid delimited string: " + value); + } + + isEscaped = false; + } + + if (sb.length() > 0) + { + if (trim) + { + list.add(sb.toString().trim()); + } + else + { + list.add(sb.toString()); + } + } + + return list; + } + + /** + * Parses native code manifest headers. + * @param libStrs an array of native library manifest header + * strings from the bundle manifest. + * @return an array of LibraryInfo objects for the + * passed in strings. + **/ + private static List parseLibraryStrings( + Logger logger, List libStrs) + throws IllegalArgumentException + { + if (libStrs == null) + { + return new ArrayList(0); + } + + List libList = new ArrayList(libStrs.size()); + + for (int i = 0; i < libStrs.size(); i++) + { + NativeLibraryClause clause = NativeLibraryClause.parse(logger, libStrs.get(i)); + libList.add(clause); + } + + return libList; + } + + public static List aliasSymbolicName(List caps, BundleRevision owner) + { + if (caps == null) + { + return new ArrayList(0); + } + + List aliasCaps = new ArrayList(caps); + + String[] aliases = { + FelixConstants.SYSTEM_BUNDLE_SYMBOLICNAME, + Constants.SYSTEM_BUNDLE_SYMBOLICNAME }; + + for (int capIdx = 0; capIdx < aliasCaps.size(); capIdx++) + { + BundleCapability cap = aliasCaps.get(capIdx); + + // Need to alias bundle and host capabilities. + if (cap.getNamespace().equals(BundleRevision.BUNDLE_NAMESPACE) + || cap.getNamespace().equals(BundleRevision.HOST_NAMESPACE)) + { + // Make a copy of the attribute array. + Map aliasAttrs = + new HashMap(cap.getAttributes()); + // Add the aliased value. + aliasAttrs.put(cap.getNamespace(), aliases); + // Create the aliased capability to replace the old capability. + cap = new BundleCapabilityImpl( + owner, + cap.getNamespace(), + cap.getDirectives(), + aliasAttrs); + aliasCaps.set(capIdx, cap); + } + + // Further, search attributes for bundle symbolic name and alias it too. + for (Entry entry : cap.getAttributes().entrySet()) + { + // If there is a bundle symbolic name attribute, add the + // standard alias as a value. + if (entry.getKey().equalsIgnoreCase(Constants.BUNDLE_SYMBOLICNAME_ATTRIBUTE)) + { + // Make a copy of the attribute array. + Map aliasAttrs = + new HashMap(cap.getAttributes()); + // Add the aliased value. + aliasAttrs.put(Constants.BUNDLE_SYMBOLICNAME_ATTRIBUTE, aliases); + // Create the aliased capability to replace the old capability. + aliasCaps.set(capIdx, new BundleCapabilityImpl( + owner, + cap.getNamespace(), + cap.getDirectives(), + aliasAttrs)); + // Continue with the next capability. + break; + } + } + } + + return aliasCaps; + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/util/manifestparser/NativeLibrary.java b/framework/src/main/java/org/apache/felix/framework/util/manifestparser/NativeLibrary.java new file mode 100644 index 00000000000..35ecbca1990 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/util/manifestparser/NativeLibrary.java @@ -0,0 +1,187 @@ +/* + * 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.framework.util.manifestparser; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.osgi.framework.Constants; + +public class NativeLibrary +{ + private String m_libraryFile; + private String[] m_osnames; + private String[] m_processors; + private String[] m_osversions; + private String[] m_languages; + private String m_selectionFilter; + + public NativeLibrary( + String libraryFile, String[] osnames, String[] processors, String[] osversions, + String[] languages, String selectionFilter) throws Exception + { + m_libraryFile = libraryFile; + m_osnames = osnames; + m_processors = processors; + m_osversions = osversions; + m_languages = languages; + m_selectionFilter = selectionFilter; + } + + public String getEntryName() + { + return m_libraryFile; + } + + public String[] getOSNames() + { + return m_osnames; + } + + public String[] getProcessors() + { + return m_processors; + } + + public String[] getOSVersions() + { + return m_osversions; + } + + public String[] getLanguages() + { + return m_languages; + } + + public String getSelectionFilter() + { + return m_selectionFilter; + } + + /** + *

      + * Determines if the specified native library name matches this native + * library definition. + *

      + * @param name the native library name to try to match. + * @return true if this native library name matches this native + * library definition; false otherwise. + **/ + public boolean match(Map configMap, String name) + { + // First, check for an exact match. + boolean matched = false; + if (m_libraryFile.equals(name) || m_libraryFile.endsWith("/" + name)) + { + matched = true; + } + + // Then check the mapped name. + String libname = System.mapLibraryName(name); + // As well as any additional library file extensions. + List exts = ManifestParser.parseDelimitedString( + (String) configMap.get(Constants.FRAMEWORK_LIBRARY_EXTENSIONS), ","); + if (exts == null) + { + exts = new ArrayList(); + } + // For Mac OSX, try dylib too. + if (libname.endsWith(".jnilib") && m_libraryFile.endsWith(".dylib")) + { + exts.add("dylib"); + } + if (libname.endsWith(".dylib") && m_libraryFile.endsWith(".jnilib")) + { + exts.add("jnilib"); + } + // Loop until we find a match or not. + int extIdx = -1; + while (!matched && (extIdx < exts.size())) + { + // Check if the current name matches. + if (m_libraryFile.equals(libname) || m_libraryFile.endsWith("/" + libname)) + { + matched = true; + } + + // Increment extension index. + extIdx++; + + // If we have other native library extensions to try, then + // calculate the new native library name. + if (!matched && (extIdx < exts.size())) + { + int idx = libname.lastIndexOf("."); + libname = (idx < 0) + ? libname + "." + exts.get(extIdx) + : libname.substring(0, idx + 1) + exts.get(extIdx); + } + } + + return matched; + } + + public String toString() + { + if (m_libraryFile != null) + { + StringBuilder sb = new StringBuilder(); + sb.append(m_libraryFile); + for (int i = 0; (m_osnames != null) && (i < m_osnames.length); i++) + { + sb.append(';'); + sb.append(Constants.BUNDLE_NATIVECODE_OSNAME); + sb.append('='); + sb.append(m_osnames[i]); + } + for (int i = 0; (m_processors != null) && (i < m_processors.length); i++) + { + sb.append(';'); + sb.append(Constants.BUNDLE_NATIVECODE_PROCESSOR); + sb.append('='); + sb.append(m_processors[i]); + } + for (int i = 0; (m_osversions != null) && (i < m_osversions.length); i++) + { + sb.append(';'); + sb.append(Constants.BUNDLE_NATIVECODE_OSVERSION); + sb.append('='); + sb.append(m_osversions[i]); + } + for (int i = 0; (m_languages != null) && (i < m_languages.length); i++) + { + sb.append(';'); + sb.append(Constants.BUNDLE_NATIVECODE_LANGUAGE); + sb.append('='); + sb.append(m_languages[i]); + } + if (m_selectionFilter != null) + { + sb.append(';'); + sb.append(Constants.SELECTION_FILTER_ATTRIBUTE); + sb.append('='); + sb.append('\''); + sb.append(m_selectionFilter); + } + + return sb.toString(); + } + return "*"; + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/util/manifestparser/NativeLibraryClause.java b/framework/src/main/java/org/apache/felix/framework/util/manifestparser/NativeLibraryClause.java new file mode 100644 index 00000000000..69e71552433 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/util/manifestparser/NativeLibraryClause.java @@ -0,0 +1,850 @@ +/* + * 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.framework.util.manifestparser; + +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.felix.framework.Logger; +import org.apache.felix.framework.util.FelixConstants; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.Version; +import org.osgi.framework.VersionRange; + +public class NativeLibraryClause +{ + private static final String OS_AIX = "aix"; + private static final String OS_DIGITALUNIX = "digitalunix"; + private static final String OS_EPOC = "epoc32"; + private static final String OS_HPUX = "hpux"; + private static final String OS_IRIX = "irix"; + private static final String OS_LINUX = "linux"; + private static final String OS_MACOS = "macos"; + private static final String OS_MACOSX = "macosx"; + private static final String OS_NETBSD = "netbsd"; + private static final String OS_NETWARE = "netware"; + private static final String OS_OPENBSD = "openbsd"; + private static final String OS_OS2 = "os2"; + private static final String OS_QNX = "qnx"; + private static final String OS_SOLARIS = "solaris"; + private static final String OS_SUNOS = "sunos"; + private static final String OS_VXWORKS = "vxworks"; + private static final String OS_WINDOWS_2000 = "windows2000"; + private static final String OS_WINDOWS_2003 = "windows2003"; + private static final String OS_WINDOWS_7 = "windows7"; + private static final String OS_WINDOWS_8 = "windows8"; + private static final String OS_WINDOWS_9 = "windows9"; + private static final String OS_WINDOWS_10 = "windows10"; + private static final String OS_WINDOWS_95 = "windows95"; + private static final String OS_WINDOWS_98 = "windows98"; + private static final String OS_WINDOWS_CE = "windowsce"; + private static final String OS_WINDOWS_NT = "windowsnt"; + private static final String OS_WINDOWS_SERVER_2008 = "windowsserver2008"; + private static final String OS_WINDOWS_SERVER_2012 = "windowsserver2012"; + private static final String OS_WINDOWS_SERVER_2016 = "windowsserver2016"; + private static final String OS_WINDOWS_VISTA = "windowsvista"; + private static final String OS_WINDOWS_XP = "windowsxp"; + private static final String OS_WIN_32 = "win32"; + + private static final String PROC_X86_64 = "x86-64"; + private static final String PROC_X86 = "x86"; + private static final String PROC_68K = "68k"; + private static final String PROC_ARM_LE = "arm_le"; + private static final String PROC_ARM_BE = "arm_be"; + private static final String PROC_ARM = "arm"; + private static final String PROC_ALPHA = "alpha"; + private static final String PROC_IGNITE = "ignite"; + private static final String PROC_MIPS = "mips"; + private static final String PROC_PARISC = "parisc"; + private static final String PROC_POWER_PC = "powerpc"; + private static final String PROC_SPARC = "sparc"; + + + private static final Map> OS_ALIASES = new HashMap>(); + + private static final Map> PROC_ALIASES = new HashMap>(); + + private final String[] m_libraryEntries; + private final String[] m_osnames; + private final String[] m_processors; + private final String[] m_osversions; + private final String[] m_languages; + private final String m_selectionFilter; + + public NativeLibraryClause(String[] libraryEntries, String[] osnames, + String[] processors, String[] osversions, String[] languages, + String selectionFilter) + { + m_libraryEntries = libraryEntries; + m_osnames = osnames; + m_processors = processors; + m_osversions = osversions; + m_languages = languages; + m_selectionFilter = selectionFilter; + } + + public NativeLibraryClause(NativeLibraryClause library) + { + this(library.m_libraryEntries, library.m_osnames, library.m_osversions, + library.m_processors, library.m_languages, + library.m_selectionFilter); + } + + /** + * Initialize the processor and os name aliases from Felix Config. + * + * @param configMap + */ + public static synchronized void initializeNativeAliases(Map configMap) + { + Map osNameKeyMap = getAllKeysWithPrefix(FelixConstants.NATIVE_OS_NAME_ALIAS_PREFIX, configMap); + + Map processorKeyMap = getAllKeysWithPrefix(FelixConstants.NATIVE_PROC_NAME_ALIAS_PREFIX, configMap); + + parseNativeAliases(osNameKeyMap, OS_ALIASES); + parseNativeAliases(processorKeyMap, PROC_ALIASES); + } + + private static void parseNativeAliases(Map aliasStringMap, Map> aliasMap) + { + for(Map.Entry aliasEntryString: aliasStringMap.entrySet()) + { + String currentAliasKey = aliasEntryString.getKey(); + + String currentNormalizedName = currentAliasKey.substring(currentAliasKey.lastIndexOf(".")+1); + + String currentAliasesString = aliasEntryString.getValue(); + + if(currentAliasesString != null) + { + String[] aliases = currentAliasesString.split(","); + List fullAliasList = new ArrayList(); + //normalized name is always first. + fullAliasList.add(currentNormalizedName); + fullAliasList.addAll(Arrays.asList(aliases)); + aliasMap.put(currentNormalizedName, fullAliasList); + for(String currentAlias: aliases) + { + List aliasList = aliasMap.get(currentAlias); + if(aliasList == null) + { + aliasMap.put(currentAlias, fullAliasList); + } + else + { + for(String newAliases: aliases) + { + if(!aliasList.contains(newAliases)) + { + aliasList.add(newAliases); + } + } + } + } + } + else + { + List aliasList = aliasMap.get(currentNormalizedName); + if(aliasList == null) + { + aliasMap.put(currentNormalizedName, new ArrayList(Collections.singletonList(currentNormalizedName))); + } + else + { + //if the alias is also a normalized name make sure it's first + aliasList.add(0, currentNormalizedName); + } + } + } + } + + private static Map getAllKeysWithPrefix(String prefix, Map configMap) + { + Map keysWithPrefix = new HashMap(); + for(Map.Entry currentEntry: configMap.entrySet()) + { + if(currentEntry.getKey().startsWith(prefix)) + { + keysWithPrefix.put(currentEntry.getKey(), currentEntry.getValue()); + } + } + return keysWithPrefix; + } + + public String[] getLibraryEntries() + { + return m_libraryEntries; + } + + public String[] getOSNames() + { + return m_osnames; + } + + public String[] getProcessors() + { + return m_processors; + } + + public String[] getOSVersions() + { + return m_osversions; + } + + public String[] getLanguages() + { + return m_languages; + } + + public String getSelectionFilter() + { + return m_selectionFilter; + } + + public boolean match(Map configMap) throws BundleException + { + String osName = (String) configMap.get(FelixConstants.FRAMEWORK_OS_NAME); + String processorName = (String) configMap.get(FelixConstants.FRAMEWORK_PROCESSOR); + String osVersion = (String) configMap.get(FelixConstants.FRAMEWORK_OS_VERSION); + String language = (String) configMap.get(FelixConstants.FRAMEWORK_LANGUAGE); + + // Check library's osname. + if ((getOSNames() != null) && + (getOSNames().length > 0) && + !checkOSNames(osName, getOSNames())) + { + return false; + } + + // Check library's processor. + if ((getProcessors() != null) && + (getProcessors().length > 0) && + !checkProcessors(processorName, getProcessors())) + { + return false; + } + + // Check library's osversion if specified. + if ((getOSVersions() != null) && + (getOSVersions().length > 0) && + !checkOSVersions(osVersion, getOSVersions())) + { + return false; + } + + // Check library's language if specified. + if ((getLanguages() != null) && + (getLanguages().length > 0) && + !checkLanguages(language, getLanguages())) + { + return false; + } + + // Check library's selection-filter if specified. + if ((getSelectionFilter() != null) && + !checkSelectionFilter(configMap, getSelectionFilter())) + { + return false; + } + + return true; + } + + private boolean checkOSNames(String osName, String[] osnames) + { + List capabilityOsNames = getOsNameWithAliases(osName); + if (capabilityOsNames != null && osnames != null) + { + for (String curOsName : osnames) + { + if (capabilityOsNames.contains(curOsName)) + { + return true; + } + + } + } + return false; + } + + private boolean checkProcessors(String processorName, String[] processors) + { + List capabilitiesProcessors = getProcessorWithAliases(processorName); + if (capabilitiesProcessors != null && processors != null) + { + for (String currentProcessor : processors) + { + if (capabilitiesProcessors.contains(currentProcessor)) + { + return true; + } + } + } + return false; + } + + private boolean checkOSVersions(String osVersion, String[] osversions) + throws BundleException + { + Version currentOSVersion = Version.parseVersion(normalizeOSVersion(osVersion)); + for (int i = 0; (osversions != null) && (i < osversions.length); i++) + { + try + { + VersionRange range = new VersionRange(osversions[i]); + if (range.includes(currentOSVersion)) + { + return true; + } + } + catch (Exception ex) + { + throw new BundleException( + "Error evaluating osversion: " + osversions[i], ex); + } + } + return false; + } + + private boolean checkLanguages(String currentLanguage, String[] languages) + { + for (int i = 0; (languages != null) && (i < languages.length); i++) + { + if (languages[i].equals(currentLanguage)) + { + return true; + } + } + return false; + } + + private boolean checkSelectionFilter(Map configMap, String expr) + throws BundleException + { + // Get all framework properties + Dictionary dict = new Hashtable(); + for (Iterator i = configMap.keySet().iterator(); i.hasNext(); ) + { + Object key = i.next(); + dict.put(key, configMap.get(key)); + } + // Compute expression + try + { + Filter filter = FrameworkUtil.createFilter(expr); + return filter.match(dict); + } + catch (Exception ex) + { + throw new BundleException( + "Error evaluating filter expression: " + expr, ex); + } + } + + public static NativeLibraryClause parse(Logger logger, String s) + { + try + { + if ((s == null) || (s.length() == 0)) + { + return null; + } + + s = s.trim(); + if (s.equals(FelixConstants.BUNDLE_NATIVECODE_OPTIONAL)) + { + return new NativeLibraryClause(null, null, null, null, null, null); + } + + // The tokens are separated by semicolons and may include + // any number of libraries along with one set of associated + // properties. + StringTokenizer st = new StringTokenizer(s, ";"); + String[] libEntries = new String[st.countTokens()]; + List osNameList = new ArrayList(); + List osVersionList = new ArrayList(); + List processorList = new ArrayList(); + List languageList = new ArrayList(); + String selectionFilter = null; + int libCount = 0; + while (st.hasMoreTokens()) + { + String token = st.nextToken().trim(); + if (token.indexOf('=') < 0) + { + // Remove the slash, if necessary. + libEntries[libCount] = (token.charAt(0) == '/') + ? token.substring(1) + : token; + libCount++; + } + else + { + // Check for valid native library properties; defined as + // a property name, an equal sign, and a value. + // NOTE: StringTokenizer can not be used here because + // a value can contain one or more "=" too, e.g., + // selection-filter="(org.osgi.framework.windowing.system=gtk)" + String property = null; + String value = null; + if (!(token.indexOf("=") > 1)) + { + throw new IllegalArgumentException( + "Bundle manifest native library entry malformed: " + token); + } + else + { + property = (token.substring(0, token.indexOf("="))) + .trim().toLowerCase(); + value = (token.substring(token.indexOf("=") + 1, token + .length())).trim(); + } + + // Values may be quoted, so remove quotes if present. + if (value.charAt(0) == '"') + { + // This should always be true, otherwise the + // value wouldn't be properly quoted, but we + // will check for safety. + if (value.charAt(value.length() - 1) == '"') + { + value = value.substring(1, value.length() - 1); + } + else + { + value = value.substring(1); + } + } + + if (value != null) + { + value = value.toLowerCase(); + } + + // Add the value to its corresponding property list. + if (property.equals(Constants.BUNDLE_NATIVECODE_OSNAME)) + { + osNameList.add(value); + } + else if (property.equals(Constants.BUNDLE_NATIVECODE_OSVERSION)) + { + osVersionList.add(normalizeOSVersionRange(value)); + } + else if (property.equals(Constants.BUNDLE_NATIVECODE_PROCESSOR)) + { + processorList.add(value); + } + else if (property.equals(Constants.BUNDLE_NATIVECODE_LANGUAGE)) + { + languageList.add(value); + } + else if (property.equals(Constants.SELECTION_FILTER_ATTRIBUTE)) + { + // TODO: NATIVE - I believe we can have multiple selection filters too. + selectionFilter = value; + } + } + } + + if (libCount == 0) + { + return null; + } + + // Shrink lib file array. + String[] actualLibEntries = new String[libCount]; + System.arraycopy(libEntries, 0, actualLibEntries, 0, libCount); + return new NativeLibraryClause( + actualLibEntries, + (String[]) osNameList.toArray(new String[osNameList.size()]), + (String[]) processorList.toArray(new String[processorList.size()]), + (String[]) osVersionList.toArray(new String[osVersionList.size()]), + (String[]) languageList.toArray(new String[languageList.size()]), + selectionFilter); + } + catch (RuntimeException ex) + { + logger.log(Logger.LOG_ERROR, + "Error parsing native library header.", ex); + throw ex; + } + } + + public static List getOsNameWithAliases(String osName) + { + //Can't assume this has been normalized + osName = normalizeOSName(osName); + + List result = OS_ALIASES.get(osName); + + if(result == null) + { + result = Collections.singletonList(osName); + } + + return Collections.unmodifiableList(result); + } + + public static List getProcessorWithAliases(String processor) + { + //Can't assume this has been normalized + processor = normalizeProcessor(processor); + + List result = PROC_ALIASES.get(processor); + + if(result == null) + { + result = Collections.singletonList(processor); + } + return Collections.unmodifiableList(result); + } + + public static String normalizeOSName(String value) + { + value = value.toLowerCase(); + + if (OS_ALIASES.containsKey(value)) + { + // we found an alias match return the first value which is the normalized name + return OS_ALIASES.get(value).get(0); + } + + //If we don't find a match do it the old way for compatibility + if (value.startsWith("win")) + { + String os = OS_WIN_32; + if (value.indexOf("32") >= 0 || value.indexOf("*") >= 0) + { + os = OS_WIN_32; + } + else if (value.indexOf("95") >= 0) + { + os = OS_WINDOWS_95; + } + else if (value.indexOf("98") >= 0) + { + os = OS_WINDOWS_98; + } + else if (value.indexOf("nt") >= 0) + { + os = OS_WINDOWS_NT; + } + else if (value.indexOf("2000") >= 0) + { + os = OS_WINDOWS_2000; + } + else if (value.indexOf("2003") >= 0) + { + os = OS_WINDOWS_2003; + } + else if (value.indexOf("2008") >= 0) + { + os = OS_WINDOWS_SERVER_2008; + } + else if (value.indexOf("2012") >= 0) + { + os = OS_WINDOWS_SERVER_2012; + } + else if (value.indexOf("2016") >= 0) + { + os = OS_WINDOWS_SERVER_2016; + } + else if (value.indexOf("xp") >= 0) + { + os = OS_WINDOWS_XP; + } + else if (value.indexOf("ce") >= 0) + { + os = OS_WINDOWS_CE; + } + else if (value.indexOf("vista") >= 0) + { + os = OS_WINDOWS_VISTA; + } + else if ((value.indexOf(" 7") >= 0) || value.startsWith(OS_WINDOWS_7) + || value.equals("win7")) + { + os = OS_WINDOWS_7; + } + else if ((value.indexOf(" 8") >= 0) || value.startsWith(OS_WINDOWS_8) + || value.equals("win8")) + { + os = OS_WINDOWS_8; + } + else if ((value.indexOf(" 9") >= 0) || value.startsWith(OS_WINDOWS_9) + || value.equals("win9")) + { + os = OS_WINDOWS_9; + } + else if ((value.indexOf(" 10") >= 0) || value.startsWith(OS_WINDOWS_10) + || value.equals("win10")) + { + os = OS_WINDOWS_10; + } + + return os; + } + else if (value.startsWith(OS_LINUX)) + { + return OS_LINUX; + } + else if (value.startsWith(OS_AIX)) + { + return OS_AIX; + } + else if (value.startsWith(OS_DIGITALUNIX)) + { + return OS_DIGITALUNIX; + } + else if (value.startsWith(OS_HPUX)) + { + return OS_HPUX; + } + else if (value.startsWith(OS_IRIX)) + { + return OS_IRIX; + } + else if (value.startsWith(OS_MACOSX) || value.startsWith("mac os x")) + { + return OS_MACOSX; + } + else if (value.startsWith(OS_MACOS) || value.startsWith("mac os")) + { + return OS_MACOS; + } + else if (value.startsWith(OS_NETWARE)) + { + return OS_NETWARE; + } + else if (value.startsWith(OS_OPENBSD)) + { + return OS_OPENBSD; + } + else if (value.startsWith(OS_NETBSD)) + { + return OS_NETBSD; + } + else if (value.startsWith(OS_OS2) || value.startsWith("os/2")) + { + return OS_OS2; + } + else if (value.startsWith(OS_QNX) || value.startsWith("procnto")) + { + return OS_QNX; + } + else if (value.startsWith(OS_SOLARIS)) + { + return OS_SOLARIS; + } + else if (value.startsWith(OS_SUNOS)) + { + return OS_SUNOS; + } + else if (value.startsWith(OS_VXWORKS)) + { + return OS_VXWORKS; + } + else if (value.startsWith(OS_EPOC)) + { + return OS_EPOC; + } + return value; + } + + public static String normalizeProcessor(String value) + { + value = value.toLowerCase(); + + if(PROC_ALIASES.containsKey(value)) + { + return PROC_ALIASES.get(value).get(0); + } + + if (value.startsWith(PROC_X86_64) || value.startsWith("amd64") || + value.startsWith("em64") || value.startsWith("x86_64")) + { + return PROC_X86_64; + } + else if (value.startsWith(PROC_X86) || value.startsWith("pentium") + || value.startsWith("i386") || value.startsWith("i486") + || value.startsWith("i586") || value.startsWith("i686")) + { + return PROC_X86; + } + else if (value.startsWith(PROC_68K)) + { + return PROC_68K; + } + else if (value.startsWith(PROC_ARM_LE)) + { + return PROC_ARM_LE; + } + else if (value.startsWith(PROC_ARM_BE)) + { + return PROC_ARM_BE; + } + else if (value.startsWith(PROC_ARM)) + { + return ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN ? PROC_ARM_BE : PROC_ARM_LE; + } + else if (value.startsWith(PROC_ALPHA)) + { + return PROC_ALPHA; + } + else if (value.startsWith(PROC_IGNITE) || value.startsWith("psc1k")) + { + return PROC_IGNITE; + } + else if (value.startsWith(PROC_MIPS)) + { + return PROC_MIPS; + } + else if (value.startsWith(PROC_PARISC)) + { + return PROC_PARISC; + } + else if (value.startsWith(PROC_POWER_PC) || value.startsWith("power") + || value.startsWith("ppc")) + { + return PROC_POWER_PC; + } + else if (value.startsWith(PROC_SPARC)) + { + return PROC_SPARC; + } + + return value; + } + + public static String normalizeOSVersionRange(String value) + { + if (value.indexOf(',') >= 0) + { + try + { + String s = value.substring(1, value.length() - 1); + String vlo = s.substring(0, s.indexOf(',')).trim(); + String vhi = s.substring(s.indexOf(',') + 1, s.length()).trim(); + return new VersionRange(value.charAt(0), new Version(cleanupVersion(vlo)), new Version( + cleanupVersion(vhi)), value.charAt(value.length() - 1)).toString(); + } + + catch (Exception ex) + { + return Version.emptyVersion.toString(); + } + } + + return normalizeOSVersion(value); + } + + public static String normalizeOSVersion(String value) + { + return new Version(cleanupVersion(value)).toString(); + } + + private static final Pattern FUZZY_VERSION = Pattern.compile( "(\\d+)(\\.(\\d+)(\\.(\\d+))?)?([^a-zA-Z0-9](.*))?", + Pattern.DOTALL ); + + private static String cleanupVersion( String version ) + { + StringBuilder result = new StringBuilder(); + Matcher m = FUZZY_VERSION.matcher( version ); + if ( m.matches() ) + { + String major = m.group( 1 ); + String minor = m.group( 3 ); + String micro = m.group( 5 ); + String qualifier = m.group( 7 ); + + if ( major != null ) + { + result.append( major ); + if ( minor != null ) + { + result.append( "." ); + result.append( minor ); + if ( micro != null ) + { + result.append( "." ); + result.append( micro ); + if ( qualifier != null && qualifier.length() > 0 ) + { + result.append( "." ); + cleanupModifier( result, qualifier ); + } + } + else if ( qualifier != null && qualifier.length() > 0) + { + result.append( ".0." ); + cleanupModifier( result, qualifier ); + } + else + { + result.append( ".0" ); + } + } + else if ( qualifier != null && qualifier.length() > 0 ) + { + result.append( ".0.0." ); + cleanupModifier( result, qualifier ); + } + else + { + result.append( ".0.0" ); + } + } + } + else + { + result.append( "0.0.0." ); + cleanupModifier( result, version ); + } + return result.toString(); + } + + + private static void cleanupModifier( StringBuilder result, String modifier ) + { + for ( int i = 0; i < modifier.length(); i++ ) + { + char c = modifier.charAt( i ); + if ( ( c >= '0' && c <= '9' ) || ( c >= 'a' && c <= 'z' ) || ( c >= 'A' && c <= 'Z' ) || c == '_' + || c == '-' ) + result.append( c ); + else + result.append( '_' ); + } + } + +} diff --git a/framework/src/main/java/org/apache/felix/framework/util/manifestparser/ParsedHeaderClause.java b/framework/src/main/java/org/apache/felix/framework/util/manifestparser/ParsedHeaderClause.java new file mode 100644 index 00000000000..7c393f2a2a2 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/util/manifestparser/ParsedHeaderClause.java @@ -0,0 +1,40 @@ +/* + * 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.framework.util.manifestparser; + +import java.util.List; +import java.util.Map; + +public class ParsedHeaderClause +{ + public final List m_paths; + public final Map m_dirs; + public final Map m_attrs; + public final Map m_types; + + public ParsedHeaderClause( + List paths, Map dirs, Map attrs, + Map types) + { + m_paths = paths; + m_dirs = dirs; + m_attrs = attrs; + m_types = types; + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/wiring/BundleCapabilityImpl.java b/framework/src/main/java/org/apache/felix/framework/wiring/BundleCapabilityImpl.java new file mode 100644 index 00000000000..9fa3806e818 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/wiring/BundleCapabilityImpl.java @@ -0,0 +1,204 @@ +/* + * 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.framework.wiring; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; +import org.apache.felix.framework.capabilityset.SimpleFilter; +import org.apache.felix.framework.util.Util; +import org.apache.felix.framework.util.manifestparser.ManifestParser; +import org.osgi.framework.Constants; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRevision; + +public class BundleCapabilityImpl implements BundleCapability +{ + public static final String VERSION_ATTR = "version"; + + private final BundleRevision m_revision; + private final String m_namespace; + private final Map m_dirs; + private final Map m_attrs; + private final List m_uses; + private final List> m_includeFilter; + private final List> m_excludeFilter; + private final Set m_mandatory; + + public BundleCapabilityImpl(BundleRevision revision, String namespace, + Map dirs, Map attrs) + { + m_namespace = namespace; + m_revision = revision; + m_dirs = Util.newImmutableMap(dirs); + m_attrs = Util.newImmutableMap(attrs); + + // Find all export directives: uses, mandatory, include, and exclude. + + List uses = Collections.EMPTY_LIST; + String value = m_dirs.get(Constants.USES_DIRECTIVE); + if (value != null) + { + // Parse these uses directive. + StringTokenizer tok = new StringTokenizer(value, ","); + uses = new ArrayList(tok.countTokens()); + while (tok.hasMoreTokens()) + { + uses.add(tok.nextToken().trim()); + } + } + m_uses = uses; + + value = m_dirs.get(Constants.INCLUDE_DIRECTIVE); + if (value != null) + { + List filters = ManifestParser.parseDelimitedString(value, ","); + m_includeFilter = new ArrayList>(filters.size()); + for (int filterIdx = 0; filterIdx < filters.size(); filterIdx++) + { + List substrings = SimpleFilter.parseSubstring(filters.get(filterIdx)); + m_includeFilter.add(substrings); + } + } + else + { + m_includeFilter = null; + } + + value = m_dirs.get(Constants.EXCLUDE_DIRECTIVE); + if (value != null) + { + List filters = ManifestParser.parseDelimitedString(value, ","); + m_excludeFilter = new ArrayList>(filters.size()); + for (int filterIdx = 0; filterIdx < filters.size(); filterIdx++) + { + List substrings = SimpleFilter.parseSubstring(filters.get(filterIdx)); + m_excludeFilter.add(substrings); + } + } + else + { + m_excludeFilter = null; + } + + Set mandatory = Collections.EMPTY_SET; + value = m_dirs.get(Constants.MANDATORY_DIRECTIVE); + if (value != null) + { + List names = ManifestParser.parseDelimitedString(value, ","); + mandatory = new HashSet(names.size()); + for (String name : names) + { + // If attribute exists, then record it as mandatory. + if (m_attrs.containsKey(name)) + { + mandatory.add(name); + } + // Otherwise, report an error. + else + { + throw new IllegalArgumentException( + "Mandatory attribute '" + name + "' does not exist."); + } + } + } + m_mandatory = mandatory; + } + + public BundleRevision getResource() + { + return m_revision; + } + + public BundleRevision getRevision() + { + return m_revision; + } + + public String getNamespace() + { + return m_namespace; + } + + public Map getDirectives() + { + return m_dirs; + } + + public Map getAttributes() + { + return m_attrs; + } + + public boolean isAttributeMandatory(String name) + { + return !m_mandatory.isEmpty() && m_mandatory.contains(name); + } + + public List getUses() + { + return m_uses; + } + + public boolean isIncluded(String name) + { + if ((m_includeFilter == null) && (m_excludeFilter == null)) + { + return true; + } + + // Get the class name portion of the target class. + String className = Util.getClassName(name); + + // If there are no include filters then all classes are included + // by default, otherwise try to find one match. + boolean included = (m_includeFilter == null); + for (int i = 0; + (!included) && (m_includeFilter != null) && (i < m_includeFilter.size()); + i++) + { + included = SimpleFilter.compareSubstring(m_includeFilter.get(i), className); + } + + // If there are no exclude filters then no classes are excluded + // by default, otherwise try to find one match. + boolean excluded = false; + for (int i = 0; + (!excluded) && (m_excludeFilter != null) && (i < m_excludeFilter.size()); + i++) + { + excluded = SimpleFilter.compareSubstring(m_excludeFilter.get(i), className); + } + return included && !excluded; + } + + @Override + public String toString() + { + if (m_revision == null) + { + return m_attrs.toString(); + } + return "[" + m_revision + "] " + m_namespace + "; " + m_attrs; + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/wiring/BundleRequirementImpl.java b/framework/src/main/java/org/apache/felix/framework/wiring/BundleRequirementImpl.java new file mode 100644 index 00000000000..3e3b68511f2 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/wiring/BundleRequirementImpl.java @@ -0,0 +1,112 @@ +/* + * 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.framework.wiring; + +import java.util.Collections; +import java.util.Map; +import org.apache.felix.framework.capabilityset.CapabilitySet; +import org.apache.felix.framework.capabilityset.SimpleFilter; +import org.apache.felix.framework.util.Util; +import org.osgi.framework.Constants; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; + +public class BundleRequirementImpl implements BundleRequirement +{ + private final BundleRevision m_revision; + private final String m_namespace; + private final SimpleFilter m_filter; + private final boolean m_optional; + private final Map m_dirs; + private final Map m_attrs; + + public BundleRequirementImpl( + BundleRevision revision, String namespace, + Map dirs, Map attrs, SimpleFilter filter) + { + m_revision = revision; + m_namespace = namespace; + m_dirs = Util.newImmutableMap(dirs); + m_attrs = Util.newImmutableMap(attrs); + m_filter = filter; + + // Find resolution import directives. + boolean optional = false; + if (m_dirs.containsKey(Constants.RESOLUTION_DIRECTIVE) + && m_dirs.get(Constants.RESOLUTION_DIRECTIVE).equals(Constants.RESOLUTION_OPTIONAL)) + { + optional = true; + } + m_optional = optional; + } + + public BundleRequirementImpl( + BundleRevision revision, String namespace, + Map dirs, Map attrs) + { + this(revision, namespace, dirs, Collections.EMPTY_MAP, SimpleFilter.convert(attrs)); + } + + public String getNamespace() + { + return m_namespace; + } + + public Map getDirectives() + { + return m_dirs; + } + + public Map getAttributes() + { + return m_attrs; + } + + public BundleRevision getResource() + { + return m_revision; + } + + public BundleRevision getRevision() + { + return m_revision; + } + + public boolean matches(BundleCapability cap) + { + return CapabilitySet.matches((BundleCapabilityImpl) cap, getFilter()); + } + + public boolean isOptional() + { + return m_optional; + } + + public SimpleFilter getFilter() + { + return m_filter; + } + + @Override + public String toString() + { + return "[" + m_revision + "] " + m_namespace + "; " + getFilter().toString(); + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/framework/wiring/BundleWireImpl.java b/framework/src/main/java/org/apache/felix/framework/wiring/BundleWireImpl.java new file mode 100644 index 00000000000..89259432d2d --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/wiring/BundleWireImpl.java @@ -0,0 +1,79 @@ +/* + * 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.framework.wiring; + +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; + +public class BundleWireImpl implements BundleWire +{ + private final BundleRevision m_requirer; + private final BundleRequirement m_req; + private final BundleRevision m_provider; + private final BundleCapability m_cap; + + public BundleWireImpl(BundleRevision requirer, BundleRequirement req, + BundleRevision provider, BundleCapability cap) + { + m_requirer = requirer; + m_req = req; + m_provider = provider; + m_cap = cap; + } + + public BundleRevision getRequirer() + { + return m_requirer; + } + + public BundleWiring getRequirerWiring() + { + return m_requirer.getWiring(); + } + + public BundleRequirement getRequirement() + { + return m_req; + } + + public BundleRevision getProvider() + { + return m_provider; + } + + public BundleWiring getProviderWiring() + { + return m_provider.getWiring(); + } + + public BundleCapability getCapability() + { + return m_cap; + } + + public String toString() + { + return m_req + + " -> " + + "[" + m_provider + "]"; + } +} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/moduleloader/ContentDirectoryContent.java b/framework/src/main/java/org/apache/felix/moduleloader/ContentDirectoryContent.java deleted file mode 100644 index 44ac066a96d..00000000000 --- a/framework/src/main/java/org/apache/felix/moduleloader/ContentDirectoryContent.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.moduleloader; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Enumeration; - -public class ContentDirectoryContent implements IContent -{ - private IContent m_content = null; - private String m_rootPath = null; - private boolean m_opened = false; - - public ContentDirectoryContent(IContent content, String path) - { - m_content = content; - // Add a '/' to the end if not present. - m_rootPath = (path.length() > 0) && (path.charAt(path.length() - 1) != '/') - ? path + "/" : path; - } - - protected void finalize() - { - if (m_content != null) - { - m_content.close(); - } - } - - public void open() - { - m_content.open(); - m_opened = true; - } - - public synchronized void close() - { - try - { - if (m_content != null) - { - m_content.close(); - } - } - catch (Exception ex) - { - System.err.println("JarContent: " + ex); - } - - m_content = null; - m_opened = false; - } - - public synchronized boolean hasEntry(String name) throws IllegalStateException - { - if (!m_opened) - { - throw new IllegalStateException("ContentDirectoryContent is not open"); - } - - if ((name.length() > 0) && (name.charAt(0) == '/')) - { - name = name.substring(1); - } - - return m_content.hasEntry(m_rootPath + name); - } - - public synchronized byte[] getEntry(String name) throws IllegalStateException - { - if (!m_opened) - { - throw new IllegalStateException("ContentDirectoryContent is not open"); - } - - if ((name.length() > 0) && (name.charAt(0) == '/')) - { - name = name.substring(1); - } - - return m_content.getEntry(m_rootPath + name); - } - - public synchronized InputStream getEntryAsStream(String name) - throws IllegalStateException, IOException - { - if (!m_opened) - { - throw new IllegalStateException("ContentDirectoryContent is not open"); - } - - if ((name.length() > 0) && (name.charAt(0) == '/')) - { - name = name.substring(1); - } - - return m_content.getEntryAsStream(m_rootPath + name); - } - - public synchronized Enumeration getEntries() - { - if (!m_opened) - { - throw new IllegalStateException("ContentDirectoryContent is not open"); - } - - return new EntriesEnumeration(m_content.getEntries(), m_rootPath); - } - - public String toString() - { - return "CONTENT DIR " + m_rootPath + " (" + m_content + ")"; - } - - private static class EntriesEnumeration implements Enumeration - { - private Enumeration m_enumeration = null; - private String m_rootPath = null; - - public EntriesEnumeration(Enumeration enumeration, String rootPath) - { - m_enumeration = enumeration; - m_rootPath = rootPath; - } - - public boolean hasMoreElements() - { - return m_enumeration.hasMoreElements(); - } - - public Object nextElement() - { - // We need to treat the specified path as the root of the - // content, so we need to strip off the leading path from - // each entry because it is automatically added back on - // in the other calls above. - return ((String) m_enumeration.nextElement()).substring(m_rootPath.length()); - } - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/moduleloader/DirectoryContent.java b/framework/src/main/java/org/apache/felix/moduleloader/DirectoryContent.java deleted file mode 100644 index 4404e45fff6..00000000000 --- a/framework/src/main/java/org/apache/felix/moduleloader/DirectoryContent.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.moduleloader; - -import java.io.*; -import java.util.*; - -public class DirectoryContent implements IContent -{ - private static final int BUFSIZE = 4096; - - private File m_dir = null; - private boolean m_opened = false; - - public DirectoryContent(File dir) - { - m_dir = dir; - } - - protected void finalize() - { - } - - public void open() - { - m_opened = true; - } - - public synchronized void close() - { - m_opened = false; - } - - public synchronized boolean hasEntry(String name) throws IllegalStateException - { - if (!m_opened) - { - throw new IllegalStateException("DirectoryContent is not open"); - } - - if ((name.length() > 0) && (name.charAt(0) == '/')) - { - name = name.substring(1); - } - - return new File(m_dir, name).exists(); - } - - public synchronized byte[] getEntry(String name) throws IllegalStateException - { - if (!m_opened) - { - throw new IllegalStateException("DirectoryContent is not open"); - } - - if ((name.length() > 0) && (name.charAt(0) == '/')) - { - name = name.substring(1); - } - - // Get the embedded resource. - InputStream is = null; - ByteArrayOutputStream baos = null; - - try - { - is = new BufferedInputStream(new FileInputStream(new File(m_dir, name))); - baos = new ByteArrayOutputStream(BUFSIZE); - byte[] buf = new byte[BUFSIZE]; - int n = 0; - while ((n = is.read(buf, 0, buf.length)) >= 0) - { - baos.write(buf, 0, n); - } - return baos.toByteArray(); - - } - catch (Exception ex) - { - return null; - } - finally - { - try - { - if (baos != null) baos.close(); - } - catch (Exception ex) - { - } - try - { - if (is != null) is.close(); - } - catch (Exception ex) - { - } - } - } - - public synchronized InputStream getEntryAsStream(String name) - throws IllegalStateException, IOException - { - if (!m_opened) - { - throw new IllegalStateException("DirectoryContent is not open"); - } - - if ((name.length() > 0) && (name.charAt(0) == '/')) - { - name = name.substring(1); - } - - return new FileInputStream(new File(m_dir, name)); - } - - public synchronized Enumeration getEntries() - { - if (!m_opened) - { - throw new IllegalStateException("JarContent is not open"); - } - - // Wrap entries enumeration to filter non-matching entries. - Enumeration e = new EntriesEnumeration(m_dir); - - // Spec says to return null if there are no entries. - return (e.hasMoreElements()) ? e : null; - } - - public String toString() - { - return "DIRECTORY " + m_dir; - } - - private static class EntriesEnumeration implements Enumeration - { - private File m_dir = null; - private File[] m_children = null; - private int m_counter = 0; - - public EntriesEnumeration(File dir) - { - m_dir = dir; - m_children = listFilesRecursive(m_dir); - } - - public boolean hasMoreElements() - { - return (m_children != null) && (m_counter < m_children.length); - } - - public Object nextElement() - { - if ((m_children == null) || (m_counter >= m_children.length)) - { - throw new NoSuchElementException("No more entry paths."); - } - - // Convert the file separator character to slashes. - String abs = m_children[m_counter].getAbsolutePath() - .replace(File.separatorChar, '/'); - - // Remove the leading path of the reference directory, since the - // entry paths are supposed to be relative to the root. - StringBuffer sb = new StringBuffer(abs); - sb.delete(0, m_dir.getAbsolutePath().length() + 1); - // Add a '/' to the end of directory entries. - if (m_children[m_counter].isDirectory()) - { - sb.append('/'); - } - m_counter++; - return sb.toString(); - } - - public File[] listFilesRecursive(File dir) - { - File[] children = dir.listFiles(); - File[] combined = children; - for (int i = 0; i < children.length; i++) - { - if (children[i].isDirectory()) - { - File[] grandchildren = listFilesRecursive(children[i]); - if (grandchildren.length > 0) - { - File[] tmp = new File[combined.length + grandchildren.length]; - System.arraycopy(combined, 0, tmp, 0, combined.length); - System.arraycopy(grandchildren, 0, tmp, combined.length, grandchildren.length); - combined = tmp; - } - } - } - return combined; - } - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/moduleloader/IContent.java b/framework/src/main/java/org/apache/felix/moduleloader/IContent.java deleted file mode 100644 index 60965ce335e..00000000000 --- a/framework/src/main/java/org/apache/felix/moduleloader/IContent.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.moduleloader; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Enumeration; - -public interface IContent -{ - public void open(); - public void close(); - public boolean hasEntry(String name); - public byte[] getEntry(String name); - public InputStream getEntryAsStream(String name) - throws IOException; - - /** - *

      - * Returns an enumeration of entry names as String objects. - * An entry name is a path constructed with '/' as path element - * separators and is relative to the root of the content. Entry names - * for entries that represent directories should end with the '/' - * character. - *

      - * @ returns An enumeration of entry names or null. - **/ - public Enumeration getEntries(); -} diff --git a/framework/src/main/java/org/apache/felix/moduleloader/IContentLoader.java b/framework/src/main/java/org/apache/felix/moduleloader/IContentLoader.java deleted file mode 100644 index 7aac6b08a7f..00000000000 --- a/framework/src/main/java/org/apache/felix/moduleloader/IContentLoader.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.moduleloader; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; - -public interface IContentLoader -{ - public void open(); - public void close(); - - public IContent getContent(); - - public void setSearchPolicy(ISearchPolicy searchPolicy); - public ISearchPolicy getSearchPolicy(); - - public void setURLPolicy(IURLPolicy urlPolicy); - public IURLPolicy getURLPolicy(); - - public Class getClass(String name); - public URL getResource(String name); - - public boolean hasInputStream(String urlPath) - throws IOException; - public InputStream getInputStream(String urlPath) - throws IOException; -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/moduleloader/IModule.java b/framework/src/main/java/org/apache/felix/moduleloader/IModule.java deleted file mode 100644 index 371832414b6..00000000000 --- a/framework/src/main/java/org/apache/felix/moduleloader/IModule.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.moduleloader; - -import java.net.URL; - -public interface IModule -{ - public String getId(); - public IModuleDefinition getDefinition(); - public IContentLoader getContentLoader(); - public IWire[] getWires(); - - public boolean isRemovalPending(); - - public Class getClass(String name); - public URL getResource(String name); - - public Object getSecurityContext(); -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/moduleloader/IModuleDefinition.java b/framework/src/main/java/org/apache/felix/moduleloader/IModuleDefinition.java deleted file mode 100644 index b1b0c06802e..00000000000 --- a/framework/src/main/java/org/apache/felix/moduleloader/IModuleDefinition.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.moduleloader; - -import org.apache.felix.framework.searchpolicy.*; - -public interface IModuleDefinition -{ - public R4Export[] getExports(); - public R4Import[] getImports(); - public R4Import[] getDynamicImports(); - public R4Library[] getLibraries(); -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/moduleloader/IModuleFactory.java b/framework/src/main/java/org/apache/felix/moduleloader/IModuleFactory.java deleted file mode 100644 index 10029f4b119..00000000000 --- a/framework/src/main/java/org/apache/felix/moduleloader/IModuleFactory.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.moduleloader; - -public interface IModuleFactory -{ - public IModule[] getModules(); - public IModule getModule(String id); - - public IModule createModule(String id, IModuleDefinition md); - public void removeModule(IModule module); - - public void setContentLoader(IModule module, IContentLoader contentLoader); - - public void addModuleListener(ModuleListener l); - public void removeModuleListener(ModuleListener l); - - public void setSecurityContext(IModule module, Object securityContext); -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/moduleloader/ISearchPolicy.java b/framework/src/main/java/org/apache/felix/moduleloader/ISearchPolicy.java deleted file mode 100644 index d3dd0d145b7..00000000000 --- a/framework/src/main/java/org/apache/felix/moduleloader/ISearchPolicy.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.moduleloader; - -import java.net.URL; - -import org.apache.felix.moduleloader.ResourceNotFoundException; - -public interface ISearchPolicy -{ - public Object[] definePackage(String name); - public Class findClass(String name) throws ClassNotFoundException; - public URL findResource(String name) throws ResourceNotFoundException; - public String findLibrary(String name); -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/moduleloader/IURLPolicy.java b/framework/src/main/java/org/apache/felix/moduleloader/IURLPolicy.java deleted file mode 100644 index 4579b4d67fc..00000000000 --- a/framework/src/main/java/org/apache/felix/moduleloader/IURLPolicy.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.moduleloader; - -import java.net.URL; - -public interface IURLPolicy -{ - public URL createURL(String path); -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/moduleloader/IWire.java b/framework/src/main/java/org/apache/felix/moduleloader/IWire.java deleted file mode 100644 index f0429465af8..00000000000 --- a/framework/src/main/java/org/apache/felix/moduleloader/IWire.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.moduleloader; - -import java.net.URL; - -import org.apache.felix.framework.searchpolicy.R4Export; - -public interface IWire -{ - public IModule getImporter(); - public IModule getExporter(); - public R4Export getExport(); - public Class getClass(String name) throws ClassNotFoundException; - public URL getResource(String name) throws ResourceNotFoundException; -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/moduleloader/JarContent.java b/framework/src/main/java/org/apache/felix/moduleloader/JarContent.java deleted file mode 100644 index 4df78b11d66..00000000000 --- a/framework/src/main/java/org/apache/felix/moduleloader/JarContent.java +++ /dev/null @@ -1,292 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.moduleloader; - -import java.io.*; -import java.util.*; -import java.util.Enumeration; -import java.util.NoSuchElementException; -import java.util.zip.ZipEntry; - -import org.apache.felix.framework.util.SecureAction; - -public class JarContent implements IContent -{ - private static final int BUFSIZE = 4096; - - private File m_file = null; - private JarFileX m_jarFile = null; - private boolean m_opened = false; - - private static SecureAction m_secureAction = new SecureAction(); - - public JarContent(File file) - { - m_file = file; - } - - protected void finalize() - { - if (m_jarFile != null) - { - try - { - m_jarFile.close(); - } - catch (IOException ex) - { - // Not much we can do, so ignore it. - } - } - } - - public void open() - { - m_opened = true; - } - - public synchronized void close() - { - try - { - if (m_jarFile != null) - { - m_jarFile.close(); - } - } - catch (Exception ex) - { - System.err.println("JarContent: " + ex); - } - - m_jarFile = null; - m_opened = false; - } - - public synchronized boolean hasEntry(String name) throws IllegalStateException - { - if (!m_opened) - { - throw new IllegalStateException("JarContent is not open"); - } - - // Open JAR file if not already opened. - if (m_jarFile == null) - { - try - { - openJarFile(); - } - catch (IOException ex) - { - System.err.println("JarContent: " + ex); - return false; - } - } - - try - { - ZipEntry ze = m_jarFile.getEntry(name); - return ze != null; - } - catch (Exception ex) - { - return false; - } - finally - { - } - } - - public synchronized byte[] getEntry(String name) throws IllegalStateException - { - if (!m_opened) - { - throw new IllegalStateException("JarContent is not open"); - } - - // Open JAR file if not already opened. - if (m_jarFile == null) - { - try - { - openJarFile(); - } - catch (IOException ex) - { - System.err.println("JarContent: " + ex); - return null; - } - } - - // Get the embedded resource. - InputStream is = null; - ByteArrayOutputStream baos = null; - - try - { - ZipEntry ze = m_jarFile.getEntry(name); - if (ze == null) - { - return null; - } - is = m_jarFile.getInputStream(ze); - if (is == null) - { - return null; - } - baos = new ByteArrayOutputStream(BUFSIZE); - byte[] buf = new byte[BUFSIZE]; - int n = 0; - while ((n = is.read(buf, 0, buf.length)) >= 0) - { - baos.write(buf, 0, n); - } - return baos.toByteArray(); - - } - catch (Exception ex) - { - return null; - } - finally - { - try - { - if (baos != null) baos.close(); - } - catch (Exception ex) - { - } - try - { - if (is != null) is.close(); - } - catch (Exception ex) - { - } - } - } - - public synchronized InputStream getEntryAsStream(String name) - throws IllegalStateException, IOException - { - if (!m_opened) - { - throw new IllegalStateException("JarContent is not open"); - } - - // Open JAR file if not already opened. - if (m_jarFile == null) - { - try - { - openJarFile(); - } - catch (IOException ex) - { - System.err.println("JarContent: " + ex); - return null; - } - } - - // Get the embedded resource. - InputStream is = null; - - try - { - ZipEntry ze = m_jarFile.getEntry(name); - if (ze == null) - { - return null; - } - is = m_jarFile.getInputStream(ze); - if (is == null) - { - return null; - } - } - catch (Exception ex) - { - return null; - } - - return is; - } - - public synchronized Enumeration getEntries() - { - if (!m_opened) - { - throw new IllegalStateException("JarContent is not open"); - } - - // Open JAR file if not already opened. - if (m_jarFile == null) - { - try - { - openJarFile(); - } - catch (IOException ex) - { - System.err.println("JarContent: " + ex); - return null; - } - } - - // Wrap entries enumeration to filter non-matching entries. - Enumeration e = new EntriesEnumeration(m_jarFile.entries()); - - // Spec says to return null if there are no entries. - return (e.hasMoreElements()) ? e : null; - } - - private void openJarFile() throws IOException - { - if (m_jarFile == null) - { - m_jarFile = m_secureAction.openJAR(m_file); - } - } - - public String toString() - { - return "JAR " + m_file.getPath(); - } - - private static class EntriesEnumeration implements Enumeration - { - private Enumeration m_enumeration = null; - private Object m_next = null; - - public EntriesEnumeration(Enumeration enumeration) - { - m_enumeration = enumeration; - } - - public boolean hasMoreElements() - { - return m_enumeration.hasMoreElements(); - } - - public Object nextElement() - { - return ((ZipEntry) m_enumeration.nextElement()).getName(); - } - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/moduleloader/JarFileX.java b/framework/src/main/java/org/apache/felix/moduleloader/JarFileX.java deleted file mode 100644 index d64796fbf68..00000000000 --- a/framework/src/main/java/org/apache/felix/moduleloader/JarFileX.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.moduleloader; - -import java.io.File; -import java.io.IOException; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.zip.ZipEntry; - -/** - * The purpose of this class is to fix an apparent bug in the JVM in versions - * 1.4.2 and lower where directory entries in ZIP/JAR files are not correctly - * identified. -**/ -public class JarFileX extends JarFile -{ - public JarFileX(File file) throws IOException - { - super(file); - } - - public JarFileX(File file, boolean verify) throws IOException - { - super(file, verify); - } - - public JarFileX(File file, boolean verify, int mode) throws IOException - { - super(file, verify, mode); - } - - public JarFileX(String name) throws IOException - { - super(name); - } - - public JarFileX(String name, boolean verify) throws IOException - { - super(name, verify); - } - - public ZipEntry getEntry(String name) - { - ZipEntry entry = super.getEntry(name); - if ((entry != null) && (entry.getSize() == 0) && !entry.isDirectory()) - { - ZipEntry dirEntry = super.getEntry(name + '/'); - if (dirEntry != null) - { - entry = dirEntry; - } - } - return entry; - } - - public JarEntry getJarEntry(String name) - { - JarEntry entry = super.getJarEntry(name); - if ((entry != null) && (entry.getSize() == 0) && !entry.isDirectory()) - { - JarEntry dirEntry = super.getJarEntry(name + '/'); - if (dirEntry != null) - { - entry = dirEntry; - } - } - return entry; - } - - -} diff --git a/framework/src/main/java/org/apache/felix/moduleloader/ModuleEvent.java b/framework/src/main/java/org/apache/felix/moduleloader/ModuleEvent.java deleted file mode 100644 index 8a00403467d..00000000000 --- a/framework/src/main/java/org/apache/felix/moduleloader/ModuleEvent.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.moduleloader; - -import java.util.EventObject; - -/** - *

      - * This is an event class that is used by the ModuleManager to - * indicate when modules are added, removed, or reset. To receive these - * events, a ModuleListener must be added to the ModuleManager - * instance. - *

      - * @see org.apache.felix.moduleloader.ModuleFactoryImpl - * @see org.apache.felix.moduleloader.ModuleImpl - * @see org.apache.felix.moduleloader.ModuleListener -**/ -public class ModuleEvent extends EventObject -{ - private IModule m_module = null; - - /** - *

      - * Constructs a module event with the specified ModuleManager - * as the event source and the specified module as the subject of - * the event. - *

      - * @param mgr the source of the event. - * @param module the subject of the event. - **/ - public ModuleEvent(IModuleFactory factory, IModule module) - { - super(factory); - m_module = module; - } - - /** - *

      - * Returns the module that is the subject of the event. - *

      - * @return the module that is the subject of the event. - **/ - public IModule getModule() - { - return m_module; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/moduleloader/ModuleFactoryImpl.java b/framework/src/main/java/org/apache/felix/moduleloader/ModuleFactoryImpl.java deleted file mode 100644 index d6a2b38eed6..00000000000 --- a/framework/src/main/java/org/apache/felix/moduleloader/ModuleFactoryImpl.java +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.moduleloader; - -import java.util.HashMap; -import java.util.Map; - -import org.apache.felix.framework.Logger; - -public class ModuleFactoryImpl implements IModuleFactory -{ - private Logger m_logger = null; - private Map m_moduleMap = new HashMap(); - - private ModuleListener[] m_listeners = null; - private static final ModuleListener[] m_noListeners = new ModuleListener[0]; - - public ModuleFactoryImpl(Logger logger) - { - m_logger = logger; - m_listeners = m_noListeners; - } - - public synchronized IModule[] getModules() - { - return (IModule[]) m_moduleMap.values().toArray(new IModule[m_moduleMap.size()]); - } - - public synchronized IModule getModule(String id) - { - return (IModule) m_moduleMap.get(id); - } - - public IModule createModule(String id, IModuleDefinition md) - { - IModule module = null; - - // Use a synchronized block instead of synchronizing the - // method, so we can fire our event outside of the block. - synchronized (this) - { - if (m_moduleMap.get(id) == null) - { - module = new ModuleImpl(m_logger, id, md); - m_moduleMap.put(id, module); - } - else - { - throw new IllegalArgumentException("Module ID must be unique."); - } - } - - // Fire event here instead of inside synchronized block. - fireModuleAdded(module); - - return module; - } - - public void removeModule(IModule module) - { - // Use a synchronized block instead of synchronizing the - // method, so we can fire our event outside of the block. - synchronized (this) - { - if (m_moduleMap.get(module.getId()) != null) - { - // Remove from data structures. - m_moduleMap.remove(module.getId()); - // Close the module's content loader to deinitialize it. - // TODO: This is not really the best place for this, but at - // least it is centralized with the call to IContentLoader.open() - // when a module's content loader is set below. - ((ModuleImpl) module).getContentLoader().close(); - } - else - { - // Don't fire event. - return; - } - } - - // Fire event here instead of inside synchronized block. - fireModuleRemoved(module); - } - - public void setContentLoader(IModule module, IContentLoader contentLoader) - { - synchronized (this) - { - ((ModuleImpl) module).setContentLoader(contentLoader); - // Open the module's content loader to initialize it. - // TODO: This is not really the best place for this, but at - // least it is centralized with the call to IContentLoader.close() - // when the module is removed above. - contentLoader.open(); - } - } - - public void setSecurityContext(IModule module, Object securityContext) - { - synchronized (this) - { - ((ModuleImpl) module).setSecurityContext(securityContext); - } - } - - /** - *

      - * Adds a listener to the IModuleFactory to listen for - * module added and removed events. - *

      - * @param l the ModuleListener to add. - **/ - public void addModuleListener(ModuleListener l) - { - // Verify listener. - if (l == null) - { - throw new IllegalArgumentException("Listener is null"); - } - - // Use the m_noListeners object as a lock. - synchronized (m_noListeners) - { - // If we have no listeners, then just add the new listener. - if (m_listeners == m_noListeners) - { - m_listeners = new ModuleListener[] { l }; - } - // Otherwise, we need to do some array copying. - // Notice, the old array is always valid, so if - // the dispatch thread is in the middle of a dispatch, - // then it has a reference to the old listener array - // and is not affected by the new value. - else - { - ModuleListener[] newList = new ModuleListener[m_listeners.length + 1]; - System.arraycopy(m_listeners, 0, newList, 0, m_listeners.length); - newList[m_listeners.length] = l; - m_listeners = newList; - } - } - } - - /** - *

      - * Removes a listener from the IModuleFactory. - *

      - * @param l the ModuleListener to remove. - **/ - public void removeModuleListener(ModuleListener l) - { - // Verify listener. - if (l == null) - { - throw new IllegalArgumentException("Listener is null"); - } - - // Use the m_noListeners object as a lock. - synchronized (m_noListeners) - { - // Try to find the instance in our list. - int idx = -1; - for (int i = 0; i < m_listeners.length; i++) - { - if (m_listeners[i].equals(l)) - { - idx = i; - break; - } - } - - // If we have the instance, then remove it. - if (idx >= 0) - { - // If this is the last listener, then point to empty list. - if (m_listeners.length == 1) - { - m_listeners = m_noListeners; - } - // Otherwise, we need to do some array copying. - // Notice, the old array is always valid, so if - // the dispatch thread is in the middle of a dispatch, - // then it has a reference to the old listener array - // and is not affected by the new value. - else - { - ModuleListener[] newList = new ModuleListener[m_listeners.length - 1]; - System.arraycopy(m_listeners, 0, newList, 0, idx); - if (idx < newList.length) - { - System.arraycopy(m_listeners, idx + 1, newList, idx, - newList.length - idx); - } - m_listeners = newList; - } - } - } - } - - /** - *

      - * Fires an event indicating that the specified module was added - * to the IModuleFactory. - *

      - * @param module the module that was added. - **/ - protected void fireModuleAdded(IModule module) - { - // Event holder. - ModuleEvent event = null; - - // Get a copy of the listener array, which is guaranteed - // to not be null. - ModuleListener[] listeners = m_listeners; - - // Loop through listeners and fire events. - for (int i = 0; i < listeners.length; i++) - { - // Lazily create event. - if (event == null) - { - event = new ModuleEvent(this, module); - } - listeners[i].moduleAdded(event); - } - } - - /** - *

      - * Fires an event indicating that the specified module was removed - * from the IModuleFactory. - *

      - * @param module the module that was removed. - **/ - protected void fireModuleRemoved(IModule module) - { - // Event holder. - ModuleEvent event = null; - - // Get a copy of the listener array, which is guaranteed - // to not be null. - ModuleListener[] listeners = m_listeners; - - // Loop through listeners and fire events. - for (int i = 0; i < listeners.length; i++) - { - // Lazily create event. - if (event == null) - { - event = new ModuleEvent(this, module); - } - listeners[i].moduleRemoved(event); - } - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/moduleloader/ModuleImpl.java b/framework/src/main/java/org/apache/felix/moduleloader/ModuleImpl.java deleted file mode 100644 index 4b47de312ef..00000000000 --- a/framework/src/main/java/org/apache/felix/moduleloader/ModuleImpl.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.moduleloader; - -import java.net.URL; - -import org.apache.felix.framework.Logger; - -public class ModuleImpl implements IModule -{ - private Logger m_logger = null; - private String m_id = null; - private boolean m_removalPending = false; - private IModuleDefinition m_md = null; - private IContentLoader m_contentLoader = null; - private IWire[] m_wires = null; - private Object m_securityContext = null; - - ModuleImpl(Logger logger, String id, IModuleDefinition md) - { - m_logger = logger; - m_id = id; - m_md = md; - } - - public String getId() - { - return m_id; - } - - public IModuleDefinition getDefinition() - { - return m_md; - } - - public IContentLoader getContentLoader() - { - return m_contentLoader; - } - - protected void setContentLoader(IContentLoader contentLoader) - { - m_contentLoader = contentLoader; - } - - protected void setSecurityContext(Object securityContext) - { - m_securityContext = securityContext; - } - - public IWire[] getWires() - { - return m_wires; - } - - public void setWires(IWire[] wires) - { - m_wires = wires; - } - - public boolean isRemovalPending() - { - return m_removalPending; - } - - public void setRemovalPending(boolean removalPending) - { - m_removalPending = removalPending; - } - - public Class getClass(String name) - { - try - { - return m_contentLoader.getSearchPolicy().findClass(name); - } - catch (ClassNotFoundException ex) - { - m_logger.log( - Logger.LOG_WARNING, - ex.getMessage(), - ex); - } - return null; - } - - public URL getResource(String name) - { - try - { - return m_contentLoader.getSearchPolicy().findResource(name); - } - catch (ResourceNotFoundException ex) - { - m_logger.log( - Logger.LOG_WARNING, - ex.getMessage(), - ex); - } - return null; - } - - public String toString() - { - return m_id; - } - - public Object getSecurityContext() - { - return m_securityContext; - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/moduleloader/ModuleListener.java b/framework/src/main/java/org/apache/felix/moduleloader/ModuleListener.java deleted file mode 100644 index 813187a1151..00000000000 --- a/framework/src/main/java/org/apache/felix/moduleloader/ModuleListener.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.moduleloader; - -import java.util.EventListener; - -/** - *

      - * This interface is an event listener for ModuleEvent events. - * To receive events, an implementation of this listener must be added - * to the IModuleFactory instance. - *

      - * @see org.apache.felix.moduleloader.IModuleFactory - * @see org.apache.felix.moduleloader.ModuleEvent -**/ -public interface ModuleListener extends EventListener -{ - /** - *

      - * This method is called after a module is added to the - * IModuleFactory. - *

      - * @param event the event object containing the event details. - **/ - public void moduleAdded(ModuleEvent event); - - /** - *

      - * This method is called after a module is remove from the - * IModuleFactory. - *

      - * @param event the event object containing the event details. - **/ - public void moduleRemoved(ModuleEvent event); -} \ No newline at end of file diff --git a/framework/src/main/java/org/apache/felix/moduleloader/ResourceNotFoundException.java b/framework/src/main/java/org/apache/felix/moduleloader/ResourceNotFoundException.java deleted file mode 100644 index 23be3477e25..00000000000 --- a/framework/src/main/java/org/apache/felix/moduleloader/ResourceNotFoundException.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.moduleloader; - -public class ResourceNotFoundException extends Exception -{ - public ResourceNotFoundException(String msg) - { - super(msg); - } -} \ No newline at end of file diff --git a/framework/src/main/java/org/osgi/dto/DTO.java b/framework/src/main/java/org/osgi/dto/DTO.java new file mode 100644 index 00000000000..53074a69bfb --- /dev/null +++ b/framework/src/main/java/org/osgi/dto/DTO.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.dto; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Super type for Data Transfer Objects. + *

      + * A Data Transfer Object (DTO) is easily serializable having only public fields + * of primitive types and their wrapper classes, Strings, enums, Version, and + * DTOs. List, Set, Map, and array aggregates may also be used. The aggregates + * must only hold objects of the listed types or aggregates. + *

      + * The object graph from a Data Transfer Object must be a tree to simplify + * serialization and deserialization. + * + * @author $Id$ + * @NotThreadSafe + */ +public abstract class DTO { + + /** + * Return a string representation of this DTO suitable for use when + * debugging. + * + *

      + * The format of the string representation is not specified and subject to + * change. + * + * @return A string representation of this DTO suitable for use when + * debugging. + */ + @Override + public String toString() { + return appendValue(new StringBuilder(), new IdentityHashMap(), "#", this).toString(); + } + + /** + * Append the specified DTO's string representation to the specified + * StringBuilder. + * + * @param result StringBuilder to which the string representation is + * appended. + * @param objectRefs References to "seen" objects. + * @param refpath The reference path of the specified DTO. + * @param dto The DTO whose string representation is to be appended. + * @return The specified StringBuilder. + */ + private static StringBuilder appendDTO(final StringBuilder result, final Map objectRefs, final String refpath, final DTO dto) { + result.append("{"); + String delim = ""; + for (Field field : dto.getClass().getFields()) { + if (Modifier.isStatic(field.getModifiers())) { + continue; + } + result.append(delim); + final String name = field.getName(); + appendString(result, name); + result.append(":"); + Object value = null; + try { + value = field.get(dto); + } catch (IllegalAccessException e) { + // use null value; + } + appendValue(result, objectRefs, refpath + "/" + name, value); + delim = ", "; + } + result.append("}"); + return result; + } + + /** + * Append the specified value's string representation to the specified + * StringBuilder. + * + *

      + * This method handles cycles in the object graph, using path-based + * references, even though the specification requires the object graph from + * a DTO to be a tree. + * + * @param result StringBuilder to which the string representation is + * appended. + * @param objectRefs References to "seen" objects. + * @param refpath The reference path of the specified value. + * @param value The object whose string representation is to be appended. + * @return The specified StringBuilder. + */ + private static StringBuilder appendValue(final StringBuilder result, final Map objectRefs, final String refpath, final Object value) { + if (value == null) { + return result.append("null"); + } + // Simple Java types + if (value instanceof String || value instanceof Character) { + return appendString(result, compress(value.toString())); + } + if (value instanceof Number || value instanceof Boolean) { + return result.append(value.toString()); + } + if (value instanceof Enum) { + return appendString(result, ((Enum< ? >) value).name()); + } + if ("org.osgi.framework.Version".equals(value.getClass().getName())) { + return appendString(result, value.toString()); + } + + // Complex types + final String path = objectRefs.get(value); + if (path != null) { + result.append("{\"$ref\":"); + appendString(result, path); + result.append("}"); + return result; + } + objectRefs.put(value, refpath); + + if (value instanceof DTO) { + return appendDTO(result, objectRefs, refpath, (DTO) value); + } + if (value instanceof Map) { + return appendMap(result, objectRefs, refpath, (Map) value); + } + if (value instanceof List || value instanceof Set) { + return appendIterable(result, objectRefs, refpath, (Iterable) value); + } + if (value.getClass().isArray()) { + return appendArray(result, objectRefs, refpath, value); + } + return appendString(result, compress(value.toString())); + } + + /** + * Append the specified array's string representation to the specified + * StringBuilder. + * + * @param result StringBuilder to which the string representation is + * appended. + * @param objectRefs References to "seen" objects. + * @param refpath The reference path of the specified array. + * @param array The array whose string representation is to be appended. + * @return The specified StringBuilder. + */ + private static StringBuilder appendArray(final StringBuilder result, final Map objectRefs, final String refpath, final Object array) { + result.append("["); + final int length = Array.getLength(array); + for (int i = 0; i < length; i++) { + if (i > 0) { + result.append(","); + } + appendValue(result, objectRefs, refpath + "/" + i, Array.get(array, i)); + } + result.append("]"); + return result; + } + + /** + * Append the specified iterable's string representation to the specified + * StringBuilder. + * + * @param result StringBuilder to which the string representation is + * appended. + * @param objectRefs References to "seen" objects. + * @param refpath The reference path of the specified list. + * @param iterable The iterable whose string representation is to be + * appended. + * @return The specified StringBuilder. + */ + private static StringBuilder appendIterable(final StringBuilder result, final Map objectRefs, final String refpath, final Iterable iterable) { + result.append("["); + int i = 0; + for (Object item : iterable) { + if (i > 0) { + result.append(","); + } + appendValue(result, objectRefs, refpath + "/" + i, item); + i++; + } + result.append("]"); + return result; + } + + /** + * Append the specified map's string representation to the specified + * StringBuilder. + * + * @param result StringBuilder to which the string representation is + * appended. + * @param objectRefs References to "seen" objects. + * @param refpath The reference path of the specified map. + * @param map The map whose string representation is to be appended. + * @return The specified StringBuilder. + */ + private static StringBuilder appendMap(final StringBuilder result, final Map objectRefs, final String refpath, final Map map) { + result.append("{"); + String delim = ""; + for (Map.Entry entry : map.entrySet()) { + result.append(delim); + final String name = String.valueOf(entry.getKey()); + appendString(result, name); + result.append(":"); + final Object value = entry.getValue(); + appendValue(result, objectRefs, refpath + "/" + name, value); + delim = ", "; + } + result.append("}"); + return result; + } + + /** + * Append the specified string to the specified StringBuilder. + * + * @param result StringBuilder to which the string is appended. + * @param string The string to be appended. + * @return The specified StringBuilder. + */ + private static StringBuilder appendString(final StringBuilder result, final CharSequence string) { + result.append("\""); + int i = result.length(); + result.append(string); + while (i < result.length()) { // escape if necessary + char c = result.charAt(i); + if ((c == '"') || (c == '\\')) { + result.insert(i, '\\'); + i = i + 2; + continue; + } + if (c < 0x20) { + result.insert(i + 1, Integer.toHexString(c | 0x10000)); + result.replace(i, i + 2, "\\u"); + i = i + 6; + continue; + } + i++; + } + result.append("\""); + return result; + } + + /** + * Compress, in length, the specified string. + * + * @param in The string to potentially compress. + * @return The string compressed, if necessary. + */ + private static CharSequence compress(final CharSequence in) { + final int length = in.length(); + if (length <= 21) { + return in; + } + StringBuilder result = new StringBuilder(21); + result.append(in, 0, 9); + result.append("..."); + result.append(in, length - 9, length); + return result; + } +} diff --git a/framework/src/main/java/org/osgi/dto/package-info.java b/framework/src/main/java/org/osgi/dto/package-info.java new file mode 100644 index 00000000000..d0d0ad127b5 --- /dev/null +++ b/framework/src/main/java/org/osgi/dto/package-info.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * OSGi Data Transfer Object Package Version 1.1. + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. This package has two types of + * users: the consumers that use the API in this package and the providers that + * implement the API in this package. + *

      + * Example import for consumers using the API in this package: + *

      + * {@code Import-Package: org.osgi.dto; version="[1.1,2.0)"} + *

      + * Example import for providers implementing the API in this package: + *

      + * {@code Import-Package: org.osgi.dto; version="[1.1,1.2)"} + * + * @author $Id$ + */ + +@Version("1.1") +package org.osgi.dto; + +import org.osgi.annotation.versioning.Version; + diff --git a/framework/src/main/java/org/osgi/framework/AdaptPermission.java b/framework/src/main/java/org/osgi/framework/AdaptPermission.java new file mode 100644 index 00000000000..c3a3d60ada9 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/AdaptPermission.java @@ -0,0 +1,621 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import java.io.IOException; +import java.io.NotSerializableException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamField; +import java.security.AccessController; +import java.security.BasicPermission; +import java.security.Permission; +import java.security.PermissionCollection; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A bundle's authority to adapt an object to a type. + * + *

      + * {@code AdaptPermission} has one action: {@code adapt}. + * + * @ThreadSafe + * @author $Id: a2bc7aac583601ace28b252fdf4ae9a53ce32a9a $ + */ +public final class AdaptPermission extends BasicPermission { + + private static final long serialVersionUID = 1L; + + /** + * The action string {@code initiate}. + */ + public final static String ADAPT = "adapt"; + + private final static int ACTION_ADAPT = 0x00000001; + private final static int ACTION_ALL = ACTION_ADAPT; + final static int ACTION_NONE = 0; + + /** + * The actions mask. + */ + transient int action_mask; + + /** + * The actions in canonical form. + * + * @serial + */ + private volatile String actions = null; + + /** + * The bundle used by this AdaptPermission. + */ + transient final Bundle bundle; + + /** + * This holds a Filter matching object used to evaluate the filter in + * implies. + */ + transient Filter filter; + + /** + * This map holds the properties of the permission, used to match a filter + * in implies. This is not initialized until necessary, and then cached in + * this object. + */ + private transient volatile Map properties; + + /** + * Creates a new granted {@code AdaptPermission} object. + * + * This constructor must only be used to create a permission that is going + * to be checked. + *

      + * Examples: + * + *

      +	 * (adaptClass=com.acme.*)
      +	 * (&(signer=\*,o=ACME,c=US)(adaptClass=com.acme.*))
      +	 * (signer=\*,o=ACME,c=US)
      +	 * 
      + * + *

      + * When a signer key is used within the filter expression the signer value + * must escape the special filter chars ('*', '(', ')'). + *

      + * The name is specified as a filter expression. The filter gives access to + * the following attributes: + *

        + *
      • signer - A Distinguished Name chain used to sign the exporting + * bundle. Wildcards in a DN are not matched according to the filter string + * rules, but according to the rules defined for a DN chain.
      • + *
      • location - The location of the exporting bundle.
      • + *
      • id - The bundle ID of the exporting bundle.
      • + *
      • name - The symbolic name of the exporting bundle.
      • + *
      • adaptClass - The name of the type to which an object can be adapted.
      • + *
      + * Filter attribute names are processed in a case sensitive manner. + * + * @param filter A filter expression. Filter attribute names are processed + * in a case sensitive manner. A special value of {@code "*"} can be + * used to match all adaptations. + * @param actions {@code adapt}. + * @throws IllegalArgumentException If the filter has an invalid syntax. + */ + public AdaptPermission(String filter, String actions) { + this(parseFilter(filter), parseActions(actions)); + } + + /** + * Creates a new requested {@code AdaptPermission} object to be used by the + * code that must perform {@code checkPermission}. {@code AdaptPermission} + * objects created with this constructor cannot be added to an + * {@code AdaptPermission} permission collection. + * + * @param adaptClass The name of the type to which an object can be adapted. + * @param adaptableBundle The bundle associated with the object being + * adapted. + * @param actions {@code adapt}. + */ + public AdaptPermission(String adaptClass, Bundle adaptableBundle, String actions) { + super(adaptClass); + setTransients(null, parseActions(actions)); + this.bundle = adaptableBundle; + if (adaptClass == null) { + throw new NullPointerException("adaptClass must not be null"); + } + if (adaptableBundle == null) { + throw new NullPointerException("adaptableBundle must not be null"); + } + } + + /** + * Package private constructor used by AdaptPermissionCollection. + * + * @param filter name filter + * @param mask action mask + */ + AdaptPermission(Filter filter, int mask) { + super((filter == null) ? "*" : filter.toString()); + setTransients(filter, mask); + this.bundle = null; + } + + /** + * Called by constructors and when deserialized. + * + * @param filter Permission's filter or {@code null} for wildcard. + * @param mask action mask + */ + private void setTransients(Filter filter, int mask) { + this.filter = filter; + if ((mask == ACTION_NONE) || ((mask & ACTION_ALL) != mask)) { + throw new IllegalArgumentException("invalid action string"); + } + this.action_mask = mask; + } + + /** + * Parse action string into action mask. + * + * @param actions Action string. + * @return action mask. + */ + private static int parseActions(String actions) { + boolean seencomma = false; + + int mask = ACTION_NONE; + + if (actions == null) { + return mask; + } + + char[] a = actions.toCharArray(); + + int i = a.length - 1; + if (i < 0) + return mask; + + while (i != -1) { + char c; + + // skip whitespace + while ((i != -1) && ((c = a[i]) == ' ' || c == '\r' || c == '\n' || c == '\f' || c == '\t')) + i--; + + // check for the known strings + int matchlen; + + if (i >= 4 && (a[i - 4] == 'a' || a[i - 4] == 'A') + && (a[i - 3] == 'd' || a[i - 3] == 'D') + && (a[i - 2] == 'a' || a[i - 2] == 'A') + && (a[i - 1] == 'p' || a[i - 1] == 'P') + && (a[i] == 't' || a[i] == 'T')) { + matchlen = 5; + mask |= ACTION_ADAPT; + + } else { + // parse error + throw new IllegalArgumentException("invalid actions: " + actions); + } + + // make sure we didn't just match the tail of a word + // like "ackbarfadapt". Also, skip to the comma. + seencomma = false; + while (i >= matchlen && !seencomma) { + switch (a[i - matchlen]) { + case ',' : + seencomma = true; + /* FALLTHROUGH */ + case ' ' : + case '\r' : + case '\n' : + case '\f' : + case '\t' : + break; + default : + throw new IllegalArgumentException("invalid permission: " + actions); + } + i--; + } + + // point i at the location of the comma minus one (or -1). + i -= matchlen; + } + + if (seencomma) { + throw new IllegalArgumentException("invalid actions: " + actions); + } + + return mask; + } + + /** + * Parse filter string into a Filter object. + * + * @param filterString The filter string to parse. + * @return a Filter for this bundle. + * @throws IllegalArgumentException If the filter syntax is invalid. + */ + private static Filter parseFilter(String filterString) { + filterString = filterString.trim(); + if (filterString.equals("*")) { + return null; + } + try { + return FrameworkUtil.createFilter(filterString); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException("invalid filter", e); + } + } + + /** + * Determines if the specified permission is implied by this object. + * + *

      + * This method checks that the filter of the target is implied by the adapt + * class name of this object. The list of {@code AdaptPermission} actions + * must either match or allow for the list of the target object to imply the + * target {@code AdaptPermission} action. + *

      + * + * @param p The requested permission. + * @return {@code true} if the specified permission is implied by this + * object; {@code false} otherwise. + */ + @Override + public boolean implies(Permission p) { + if (!(p instanceof AdaptPermission)) { + return false; + } + AdaptPermission requested = (AdaptPermission) p; + if (bundle != null) { + return false; + } + // if requested permission has a filter, then it is an invalid argument + if (requested.filter != null) { + return false; + } + return implies0(requested, ACTION_NONE); + } + + /** + * Internal implies method. Used by the implies and the permission + * collection implies methods. + * + * @param requested The requested AdaptPermission which has already be + * validated as a proper argument. The requested AdaptPermission must + * not have a filter expression. + * @param effective The effective actions with which to start. + * @return {@code true} if the specified permission is implied by this + * object; {@code false} otherwise. + */ + boolean implies0(AdaptPermission requested, int effective) { + /* check actions first - much faster */ + effective |= action_mask; + final int desired = requested.action_mask; + if ((effective & desired) != desired) { + return false; + } + /* Get filter */ + Filter f = filter; + if (f == null) { + // it's "*" + return true; + } + return f.matches(requested.getProperties()); + } + + /** + * Returns the canonical string representation of the + * {@code AdaptPermission} actions. + * + *

      + * Always returns present {@code AdaptPermission} actions in the following + * order: {@code adapt}. + * + * @return Canonical string representation of the {@code AdaptPermission} + * actions. + */ + @Override + public String getActions() { + String result = actions; + if (result == null) { + actions = result = ADAPT; + } + return result; + } + + /** + * Returns a new {@code PermissionCollection} object suitable for storing + * {@code AdaptPermission} objects. + * + * @return A new {@code PermissionCollection} object. + */ + @Override + public PermissionCollection newPermissionCollection() { + return new AdaptPermissionCollection(); + } + + /** + * Determines the equality of two {@code AdaptPermission} objects. + * + * This method checks that specified permission has the same name and + * {@code AdaptPermission} actions as this {@code AdaptPermission} object. + * + * @param obj The object to test for equality with this + * {@code AdaptPermission} object. + * @return {@code true} if {@code obj} is a {@code AdaptPermission}, and has + * the same name and actions as this {@code AdaptPermission} object; + * {@code false} otherwise. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (!(obj instanceof AdaptPermission)) { + return false; + } + + AdaptPermission cp = (AdaptPermission) obj; + + return (action_mask == cp.action_mask) && getName().equals(cp.getName()) && ((bundle == cp.bundle) || ((bundle != null) && bundle.equals(cp.bundle))); + } + + /** + * Returns the hash code value for this object. + * + * @return A hash code value for this object. + */ + @Override + public int hashCode() { + int h = 31 * 17 + getName().hashCode(); + h = 31 * h + getActions().hashCode(); + if (bundle != null) { + h = 31 * h + bundle.hashCode(); + } + return h; + } + + /** + * WriteObject is called to save the state of this permission object to a + * stream. The actions are serialized, and the superclass takes care of the + * name. + */ + private synchronized void writeObject(java.io.ObjectOutputStream s) throws IOException { + if (bundle != null) { + throw new NotSerializableException("cannot serialize"); + } + // Write out the actions. The superclass takes care of the name + // call getActions to make sure actions field is initialized + if (actions == null) + getActions(); + s.defaultWriteObject(); + } + + /** + * readObject is called to restore the state of this permission from a + * stream. + */ + private synchronized void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { + // Read in the action, then initialize the rest + s.defaultReadObject(); + setTransients(parseFilter(getName()), parseActions(actions)); + } + + /** + * Called by {@link AdaptPermission#implies(Permission)}. This method is + * only called on a requested permission which cannot have a filter set. + * + * @return a map of properties for this permission. + */ + private Map getProperties() { + Map result = properties; + if (result != null) { + return result; + } + final Map map = new HashMap(5); + map.put("adaptClass", getName()); + if (bundle != null) { + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Void run() { + map.put("id", Long.valueOf(bundle.getBundleId())); + map.put("location", bundle.getLocation()); + String name = bundle.getSymbolicName(); + if (name != null) { + map.put("name", name); + } + SignerProperty signer = new SignerProperty(bundle); + if (signer.isBundleSigned()) { + map.put("signer", signer); + } + return null; + } + }); + } + return properties = map; + } +} + +/** + * Stores a set of {@code AdaptPermission} permissions. + * + * @see java.security.Permission + * @see java.security.Permissions + * @see java.security.PermissionCollection + */ + +final class AdaptPermissionCollection extends PermissionCollection { + static final long serialVersionUID = -3350758995234427603L; + /** + * Collection of permissions. + * + * @serial + * @GuardedBy this + */ + private Map permissions; + + /** + * Boolean saying if "*" is in the collection. + * + * @serial + * @GuardedBy this + */ + private boolean all_allowed; + + /** + * Create an empty AdaptPermissions object. + */ + public AdaptPermissionCollection() { + permissions = new HashMap(); + all_allowed = false; + } + + /** + * Adds a permission to this permission collection. + * + * @param permission The {@code AdaptPermission} object to add. + * @throws IllegalArgumentException If the specified permission is not a + * {@code AdaptPermission} instance or was constructed with a Bundle + * object. + * @throws SecurityException If this {@code AdaptPermissionCollection} + * object has been marked read-only. + */ + @Override + public void add(final Permission permission) { + if (!(permission instanceof AdaptPermission)) { + throw new IllegalArgumentException("invalid permission: " + permission); + } + if (isReadOnly()) { + throw new SecurityException("attempt to add a Permission to a " + "readonly PermissionCollection"); + } + + final AdaptPermission ap = (AdaptPermission) permission; + if (ap.bundle != null) { + throw new IllegalArgumentException("cannot add to collection: " + ap); + } + + final String name = ap.getName(); + synchronized (this) { + Map pc = permissions; + final AdaptPermission existing = pc.get(name); + if (existing != null) { + final int oldMask = existing.action_mask; + final int newMask = ap.action_mask; + if (oldMask != newMask) { + pc.put(name, new AdaptPermission(existing.filter, oldMask | newMask)); + + } + } else { + pc.put(name, ap); + } + + if (!all_allowed) { + if (name.equals("*")) { + all_allowed = true; + } + } + } + } + + /** + * Determines if the specified permissions implies the permissions expressed + * in {@code permission}. + * + * @param permission The Permission object to compare with this + * {@code AdaptPermission} object. + * @return {@code true} if {@code permission} is a proper subset of a + * permission in the set; {@code false} otherwise. + */ + @Override + public boolean implies(final Permission permission) { + if (!(permission instanceof AdaptPermission)) { + return false; + } + final AdaptPermission requested = (AdaptPermission) permission; + /* if requested permission has a filter, then it is an invalid argument */ + if (requested.filter != null) { + return false; + } + + int effective = AdaptPermission.ACTION_NONE; + + Collection perms; + synchronized (this) { + Map pc = permissions; + /* short circuit if the "*" Permission was added */ + if (all_allowed) { + AdaptPermission ap = pc.get("*"); + if (ap != null) { + effective |= ap.action_mask; + final int desired = requested.action_mask; + if ((effective & desired) == desired) { + return true; + } + } + } + perms = pc.values(); + } + /* iterate one by one over filteredPermissions */ + for (AdaptPermission perm : perms) { + if (perm.implies0(requested, effective)) { + return true; + } + } + return false; + } + + /** + * Returns an enumeration of all {@code AdaptPermission} objects in the + * container. + * + * @return Enumeration of all {@code AdaptPermission} objects. + */ + @Override + public synchronized Enumeration elements() { + List all = new ArrayList(permissions.values()); + return Collections.enumeration(all); + } + + /* serialization logic */ + private static final ObjectStreamField[] serialPersistentFields = {new ObjectStreamField("permissions", HashMap.class), new ObjectStreamField("all_allowed", Boolean.TYPE)}; + + private synchronized void writeObject(ObjectOutputStream out) throws IOException { + ObjectOutputStream.PutField pfields = out.putFields(); + pfields.put("permissions", permissions); + pfields.put("all_allowed", all_allowed); + out.writeFields(); + } + + private synchronized void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { + ObjectInputStream.GetField gfields = in.readFields(); + @SuppressWarnings("unchecked") + HashMap p = (HashMap) gfields.get("permissions", null); + permissions = p; + all_allowed = gfields.get("all_allowed", false); + } +} diff --git a/framework/src/main/java/org/osgi/framework/AdminPermission.java b/framework/src/main/java/org/osgi/framework/AdminPermission.java index f46cf54ebc3..0f8775e2db3 100644 --- a/framework/src/main/java/org/osgi/framework/AdminPermission.java +++ b/framework/src/main/java/org/osgi/framework/AdminPermission.java @@ -1,873 +1,1016 @@ /* - * Copyright 2006 The Apache Software Foundation + * Copyright (c) OSGi Alliance (2000, 2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * http://www.apache.org/licenses/LICENSE-2.0 * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -package org.osgi.framework; -import java.lang.reflect.Method; -import java.security.*; -import java.util.*; +package org.osgi.framework; -import org.apache.felix.framework.FilterImpl; +import java.io.IOException; +import java.io.NotSerializableException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamField; +import java.security.AccessController; +import java.security.BasicPermission; +import java.security.Permission; +import java.security.PermissionCollection; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; /** + * A bundle's authority to perform specific privileged administrative operations + * on or to get sensitive information about a bundle. The actions for this + * permission are: + * + *

      + * Action             Methods
      + * class              Bundle.loadClass
      + * execute            Bundle.start
      + *                    Bundle.stop
      + *                    BundleStartLevel.setStartLevel
      + * extensionLifecycle BundleContext.installBundle for extension bundles
      + *                    Bundle.update for extension bundles
      + *                    Bundle.uninstall for extension bundles
      + * lifecycle          BundleContext.installBundle
      + *                    Bundle.update
      + *                    Bundle.uninstall
      + * listener           BundleContext.addBundleListener for
      + *                      SynchronousBundleListener
      + *                    BundleContext.removeBundleListener for
      + *                      SynchronousBundleListener
      + * metadata           Bundle.getHeaders
      + *                    Bundle.getLocation
      + * resolve            FrameworkWiring.refreshBundles
      + *                    FrameworkWiring.resolveBundles
      + * resource           Bundle.getResource
      + *                    Bundle.getResources
      + *                    Bundle.getEntry
      + *                    Bundle.getEntryPaths
      + *                    Bundle.findEntries
      + *                    Bundle resource/entry URL creation
      + * startlevel         FrameworkStartLevel.setStartLevel
      + *                    FrameworkStartLevel.setInitialBundleStartLevel
      + * context            Bundle.getBundleContext
      + * weave              WovenClass.getBytes
      + *                    WovenClass.setBytes
      + *                    WovenClass.getDynamicImports for modification
      + * 
      + * + *

      + * The special action "*" will represent all actions. The + * {@code resolve} action is implied by the {@code class}, {@code execute} and + * {@code resource} actions. *

      - * This class is a replacement for the version that ships with the - * standard OSGi JAR file. - *

      -**/ -public final class AdminPermission extends BasicPermission -{ - static final long serialVersionUID = 307051004521261705L; - - public static final String CLASS = "class"; - public static final String EXECUTE = "execute"; - public static final String EXTENSIONLIFECYCLE = "extensionLifecycle"; - public static final String LIFECYCLE = "lifecycle"; - public static final String LISTENER = "listener"; - public static final String METADATA = "metadata"; - public static final String RESOLVE = "resolve"; - public static final String RESOURCE = "resource"; - public static final String STARTLEVEL = "startlevel"; - - private static final int CLASS_MASK = 1; - private static final int EXECUTE_MASK = 2; - private static final int EXTENSIONLIFECYCLE_MASK = 4; - private static final int LIFECYCLE_MASK = 8; - private static final int LISTENER_MASK = 16; - private static final int METADATA_MASK = 32; - private static final int RESOLVE_MASK = 64; - private static final int RESOURCE_MASK = 128; - private static final int STARTLEVEL_MASK = 256; - private static final int ALL_MASK = - CLASS_MASK | EXECUTE_MASK | EXTENSIONLIFECYCLE_MASK | - LIFECYCLE_MASK | LISTENER_MASK | METADATA_MASK | - RESOLVE_MASK | RESOURCE_MASK | STARTLEVEL_MASK; - - private String m_actions = null; - int m_actionMask = 0; - - // Cached filter for permissions created with a filter when - // granting admin permissions. - private FilterImpl m_filterImpl = null; - - // Bundle associated with the permission when checking - // admin permissions. - private Bundle m_bundle = null; - // Cached bundle property dictionary when checking - // admin permissions. - private Dictionary m_bundleDict = null; - - // This constructor is only used when granting an admin permission. - public AdminPermission() - { - this("*", ALL_MASK); + * The name of this permission is a filter expression. The filter gives access + * to the following attributes: + *
        + *
      • signer - A Distinguished Name chain used to sign a bundle. Wildcards in a + * DN are not matched according to the filter string rules, but according to the + * rules defined for a DN chain.
      • + *
      • location - The location of a bundle.
      • + *
      • id - The bundle ID of the designated bundle.
      • + *
      • name - The symbolic name of a bundle.
      • + *
      + * Filter attribute names are processed in a case sensitive manner. + * + * @ThreadSafe + * @author $Id: 7906054ba14028f4c0dc21610dfd8b86ae3ffa00 $ + */ + +public final class AdminPermission extends BasicPermission { + static final long serialVersionUID = 307051004521261705L; + + /** + * The action string {@code class}. The {@code class} action implies the + * {@code resolve} action. + * + * @since 1.3 + */ + public final static String CLASS = "class"; + /** + * The action string {@code execute}. The {@code execute} action implies the + * {@code resolve} action. + * + * @since 1.3 + */ + public final static String EXECUTE = "execute"; + /** + * The action string {@code extensionLifecycle}. + * + * @since 1.3 + */ + public final static String EXTENSIONLIFECYCLE = "extensionLifecycle"; + /** + * The action string {@code lifecycle}. + * + * @since 1.3 + */ + public final static String LIFECYCLE = "lifecycle"; + /** + * The action string {@code listener}. + * + * @since 1.3 + */ + public final static String LISTENER = "listener"; + /** + * The action string {@code metadata}. + * + * @since 1.3 + */ + public final static String METADATA = "metadata"; + /** + * The action string {@code resolve}. The {@code resolve} action is implied + * by the {@code class}, {@code execute} and {@code resource} actions. + * + * @since 1.3 + */ + public final static String RESOLVE = "resolve"; + /** + * The action string {@code resource}. The {@code resource} action implies + * the {@code resolve} action. + * + * @since 1.3 + */ + public final static String RESOURCE = "resource"; + /** + * The action string {@code startlevel}. + * + * @since 1.3 + */ + public final static String STARTLEVEL = "startlevel"; + + /** + * The action string {@code context}. + * + * @since 1.4 + */ + public final static String CONTEXT = "context"; + + /** + * The action string {@code weave}. + * + * @since 1.6 + */ + public final static String WEAVE = "weave"; + + private final static int ACTION_CLASS = 0x00000001; + private final static int ACTION_EXECUTE = 0x00000002; + private final static int ACTION_LIFECYCLE = 0x00000004; + private final static int ACTION_LISTENER = 0x00000008; + private final static int ACTION_METADATA = 0x00000010; + private final static int ACTION_RESOLVE = 0x00000040; + private final static int ACTION_RESOURCE = 0x00000080; + private final static int ACTION_STARTLEVEL = 0x00000100; + private final static int ACTION_EXTENSIONLIFECYCLE = 0x00000200; + private final static int ACTION_CONTEXT = 0x00000400; + private final static int ACTION_WEAVE = 0x00000800; + private final static int ACTION_ALL = ACTION_CLASS | ACTION_EXECUTE | ACTION_LIFECYCLE | ACTION_LISTENER | ACTION_METADATA | ACTION_RESOLVE + | ACTION_RESOURCE | ACTION_STARTLEVEL | ACTION_EXTENSIONLIFECYCLE | ACTION_CONTEXT | ACTION_WEAVE; + final static int ACTION_NONE = 0; + + /** + * The actions in canonical form. + * + * @serial + */ + private volatile String actions = null; + + /** + * The actions mask. + */ + transient int action_mask; + + /** + * If this AdminPermission was constructed with a filter, this holds a + * Filter matching object used to evaluate the filter in implies. + */ + transient Filter filter; + + /** + * The bundle governed by this AdminPermission - only used if filter == null + */ + transient final Bundle bundle; + + /** + * This map holds the properties of the permission, used to match a filter + * in implies. This is not initialized until necessary, and then cached in + * this object. + */ + private transient volatile Map properties; + + /** + * ThreadLocal used to determine if we have recursively called + * getProperties. + */ + private static final ThreadLocal recurse = new ThreadLocal(); + + /** + * Creates a new {@code AdminPermission} object that matches all bundles and + * has all actions. Equivalent to AdminPermission("*","*"); + */ + public AdminPermission() { + this(null, ACTION_ALL); } - // This constructor is only used when checking a granted admin permission. - public AdminPermission(Bundle bundle, String actions) - { - this(createName(bundle), actions); - m_bundle = bundle; - } - - // This constructor is only used when granting an admin permission. - public AdminPermission(String filter, String actions) - { - super((filter == null) || (filter.equals("*")) ? "(id=*)" : filter); - m_actionMask = parseActions(actions); - } - - // This constructor is only used by the admin permission collection - // when combining admin permissions or by the default constructor when granting - // an admin permission - AdminPermission(String filter, int actionMask) - { - super((filter == null) || (filter.equals("*")) ? "(id=*)" : filter); - m_actionMask = actionMask; - } - - public boolean equals(Object obj) - { - if (obj == this) - { - return true; + /** + * Create a new AdminPermission. + * + * This constructor must only be used to create a permission that is going + * to be checked. + *

      + * Examples: + * + *

      +	 * (signer=\*,o=ACME,c=US)
      +	 * (&(signer=\*,o=ACME,c=US)(name=com.acme.*)
      +	 *   (location=http://www.acme.com/bundles/*))
      +	 * (id>=1)
      +	 * 
      + * + *

      + * When a signer key is used within the filter expression the signer value + * must escape the special filter chars ('*', '(', ')'). + *

      + * Null arguments are equivalent to "*". + * + * @param filter A filter expression that can use signer, location, id, and + * name keys. A value of "*" or {@code null} matches all + * bundle. Filter attribute names are processed in a case sensitive + * manner. + * @param actions {@code class}, {@code execute}, {@code extensionLifecycle} + * , {@code lifecycle}, {@code listener}, {@code metadata}, + * {@code resolve} , {@code resource}, {@code startlevel}, + * {@code context} or {@code weave}. A value of "*" or {@code null} + * indicates all actions. + * @throws IllegalArgumentException If the filter has an invalid syntax. + */ + public AdminPermission(String filter, String actions) { + // arguments will be null if called from a PermissionInfo defined with + // no args + this(parseFilter(filter), parseActions(actions)); + } + + /** + * Creates a new requested {@code AdminPermission} object to be used by the + * code that must perform {@code checkPermission}. {@code AdminPermission} + * objects created with this constructor cannot be added to an + * {@code AdminPermission} permission collection. + * + * @param bundle A bundle. + * @param actions {@code class}, {@code execute}, {@code extensionLifecycle} + * , {@code lifecycle}, {@code listener}, {@code metadata}, + * {@code resolve} , {@code resource}, {@code startlevel}, + * {@code context}, {@code weave}. A value of "*" or {@code null} + * indicates all actions. + * @since 1.3 + */ + public AdminPermission(Bundle bundle, String actions) { + super(createName(bundle)); + setTransients(null, parseActions(actions)); + this.bundle = bundle; + } + + /** + * Create a permission name from a Bundle + * + * @param bundle Bundle to use to create permission name. + * @return permission name. + */ + private static String createName(Bundle bundle) { + if (bundle == null) { + throw new IllegalArgumentException("bundle must not be null"); } + StringBuilder sb = new StringBuilder("(id="); + sb.append(bundle.getBundleId()); + sb.append(")"); + return sb.toString(); + } - if (!(obj instanceof AdminPermission)) - { - return false; + /** + * Package private constructor used by AdminPermissionCollection. + * + * @param filter name filter or {@code null} for wildcard. + * @param mask action mask + */ + AdminPermission(Filter filter, int mask) { + super((filter == null) ? "*" : filter.toString()); + setTransients(filter, mask); + this.bundle = null; + } + + /** + * Called by constructors and when deserialized. + * + * @param filter Permission's filter or {@code null} for wildcard. + * @param mask action mask + */ + private void setTransients(Filter filter, int mask) { + this.filter = filter; + if ((mask == ACTION_NONE) || ((mask & ACTION_ALL) != mask)) { + throw new IllegalArgumentException("invalid action string"); } + this.action_mask = mask; + } - AdminPermission p = (AdminPermission) obj; + /** + * Parse action string into action mask. + * + * @param actions Action string. + * @return action mask. + */ + private static int parseActions(String actions) { + if ((actions == null) || actions.equals("*")) { + return ACTION_ALL; + } + + boolean seencomma = false; + + int mask = ACTION_NONE; + + char[] a = actions.toCharArray(); + + int i = a.length - 1; + if (i < 0) + return mask; + + while (i != -1) { + char c; + + // skip whitespace + while ((i != -1) && ((c = a[i]) == ' ' || c == '\r' || c == '\n' || c == '\f' || c == '\t')) + i--; + + // check for the known strings + int matchlen; + + if (i >= 4 && (a[i - 4] == 'c' || a[i - 4] == 'C') + && (a[i - 3] == 'l' || a[i - 3] == 'L') + && (a[i - 2] == 'a' || a[i - 2] == 'A') + && (a[i - 1] == 's' || a[i - 1] == 'S') + && (a[i] == 's' || a[i] == 'S')) { + matchlen = 5; + mask |= ACTION_CLASS | ACTION_RESOLVE; + + } else + if (i >= 6 && (a[i - 6] == 'e' || a[i - 6] == 'E') + && (a[i - 5] == 'x' || a[i - 5] == 'X') + && (a[i - 4] == 'e' || a[i - 4] == 'E') + && (a[i - 3] == 'c' || a[i - 3] == 'C') + && (a[i - 2] == 'u' || a[i - 2] == 'U') + && (a[i - 1] == 't' || a[i - 1] == 'T') + && (a[i] == 'e' || a[i] == 'E')) { + matchlen = 7; + mask |= ACTION_EXECUTE | ACTION_RESOLVE; + + } else + if (i >= 17 && (a[i - 17] == 'e' || a[i - 17] == 'E') + && (a[i - 16] == 'x' || a[i - 16] == 'X') + && (a[i - 15] == 't' || a[i - 15] == 'T') + && (a[i - 14] == 'e' || a[i - 14] == 'E') + && (a[i - 13] == 'n' || a[i - 13] == 'N') + && (a[i - 12] == 's' || a[i - 12] == 'S') + && (a[i - 11] == 'i' || a[i - 11] == 'I') + && (a[i - 10] == 'o' || a[i - 10] == 'O') + && (a[i - 9] == 'n' || a[i - 9] == 'N') + && (a[i - 8] == 'l' || a[i - 8] == 'L') + && (a[i - 7] == 'i' || a[i - 7] == 'I') + && (a[i - 6] == 'f' || a[i - 6] == 'F') + && (a[i - 5] == 'e' || a[i - 5] == 'E') + && (a[i - 4] == 'c' || a[i - 4] == 'C') + && (a[i - 3] == 'y' || a[i - 3] == 'Y') + && (a[i - 2] == 'c' || a[i - 2] == 'C') + && (a[i - 1] == 'l' || a[i - 1] == 'L') + && (a[i] == 'e' || a[i] == 'E')) { + matchlen = 18; + mask |= ACTION_EXTENSIONLIFECYCLE; + + } else + if (i >= 8 && (a[i - 8] == 'l' || a[i - 8] == 'L') + && (a[i - 7] == 'i' || a[i - 7] == 'I') + && (a[i - 6] == 'f' || a[i - 6] == 'F') + && (a[i - 5] == 'e' || a[i - 5] == 'E') + && (a[i - 4] == 'c' || a[i - 4] == 'C') + && (a[i - 3] == 'y' || a[i - 3] == 'Y') + && (a[i - 2] == 'c' || a[i - 2] == 'C') + && (a[i - 1] == 'l' || a[i - 1] == 'L') + && (a[i] == 'e' || a[i] == 'E')) { + matchlen = 9; + mask |= ACTION_LIFECYCLE; + + } else + if (i >= 7 && (a[i - 7] == 'l' || a[i - 7] == 'L') + && (a[i - 6] == 'i' || a[i - 6] == 'I') + && (a[i - 5] == 's' || a[i - 5] == 'S') + && (a[i - 4] == 't' || a[i - 4] == 'T') + && (a[i - 3] == 'e' || a[i - 3] == 'E') + && (a[i - 2] == 'n' || a[i - 2] == 'N') + && (a[i - 1] == 'e' || a[i - 1] == 'E') + && (a[i] == 'r' || a[i] == 'R')) { + matchlen = 8; + mask |= ACTION_LISTENER; + + } else + if (i >= 7 + && (a[i - 7] == 'm' || a[i - 7] == 'M') + && (a[i - 6] == 'e' || a[i - 6] == 'E') + && (a[i - 5] == 't' || a[i - 5] == 'T') + && (a[i - 4] == 'a' || a[i - 4] == 'A') + && (a[i - 3] == 'd' || a[i - 3] == 'D') + && (a[i - 2] == 'a' || a[i - 2] == 'A') + && (a[i - 1] == 't' || a[i - 1] == 'T') + && (a[i] == 'a' || a[i] == 'A')) { + matchlen = 8; + mask |= ACTION_METADATA; + + } else + if (i >= 6 + && (a[i - 6] == 'r' || a[i - 6] == 'R') + && (a[i - 5] == 'e' || a[i - 5] == 'E') + && (a[i - 4] == 's' || a[i - 4] == 'S') + && (a[i - 3] == 'o' || a[i - 3] == 'O') + && (a[i - 2] == 'l' || a[i - 2] == 'L') + && (a[i - 1] == 'v' || a[i - 1] == 'V') + && (a[i] == 'e' || a[i] == 'E')) { + matchlen = 7; + mask |= ACTION_RESOLVE; + + } else + if (i >= 7 + && (a[i - 7] == 'r' || a[i - 7] == 'R') + && (a[i - 6] == 'e' || a[i - 6] == 'E') + && (a[i - 5] == 's' || a[i - 5] == 'S') + && (a[i - 4] == 'o' || a[i - 4] == 'O') + && (a[i - 3] == 'u' || a[i - 3] == 'U') + && (a[i - 2] == 'r' || a[i - 2] == 'R') + && (a[i - 1] == 'c' || a[i - 1] == 'C') + && (a[i] == 'e' || a[i] == 'E')) { + matchlen = 8; + mask |= ACTION_RESOURCE + | ACTION_RESOLVE; + + } else + if (i >= 9 + && (a[i - 9] == 's' || a[i - 9] == 'S') + && (a[i - 8] == 't' || a[i - 8] == 'T') + && (a[i - 7] == 'a' || a[i - 7] == 'A') + && (a[i - 6] == 'r' || a[i - 6] == 'R') + && (a[i - 5] == 't' || a[i - 5] == 'T') + && (a[i - 4] == 'l' || a[i - 4] == 'L') + && (a[i - 3] == 'e' || a[i - 3] == 'E') + && (a[i - 2] == 'v' || a[i - 2] == 'V') + && (a[i - 1] == 'e' || a[i - 1] == 'E') + && (a[i] == 'l' || a[i] == 'L')) { + matchlen = 10; + mask |= ACTION_STARTLEVEL; + + } else + if (i >= 6 + && (a[i - 6] == 'c' || a[i - 6] == 'C') + && (a[i - 5] == 'o' || a[i - 5] == 'O') + && (a[i - 4] == 'n' || a[i - 4] == 'N') + && (a[i - 3] == 't' || a[i - 3] == 'T') + && (a[i - 2] == 'e' || a[i - 2] == 'E') + && (a[i - 1] == 'x' || a[i - 1] == 'X') + && (a[i] == 't' || a[i] == 'T')) { + matchlen = 7; + mask |= ACTION_CONTEXT; + + } else + if (i >= 4 + && (a[i - 4] == 'w' || a[i - 4] == 'W') + && (a[i - 3] == 'e' || a[i - 3] == 'E') + && (a[i - 2] == 'a' || a[i - 2] == 'A') + && (a[i - 1] == 'v' || a[i - 1] == 'V') + && (a[i] == 'e' || a[i] == 'E')) { + matchlen = 5; + mask |= ACTION_WEAVE; + + } else + if (i >= 0 && (a[i] == '*')) { + matchlen = 1; + mask |= ACTION_ALL; + + } else { + // parse error + throw new IllegalArgumentException("invalid permission: " + actions); + } + + // make sure we didn't just match the tail of a word + // like "ackbarfstartlevel". Also, skip to the comma. + seencomma = false; + while (i >= matchlen && !seencomma) { + switch (a[i - matchlen]) { + case ',' : + seencomma = true; + /* FALLTHROUGH */ + case ' ' : + case '\r' : + case '\n' : + case '\f' : + case '\t' : + break; + default : + throw new IllegalArgumentException("invalid permission: " + actions); + } + i--; + } + + // point i at the location of the comma minus one (or -1). + i -= matchlen; + } + + if (seencomma) { + throw new IllegalArgumentException("invalid permission: " + actions); + } - return getName().equals(p.getName()) && (m_actionMask == p.m_actionMask); + return mask; } - public int hashCode() - { - return getName().hashCode() ^ getActions().hashCode(); + /** + * Parse filter string into a Filter object. + * + * @param filterString The filter string to parse. + * @return a Filter for this bundle. If the specified filterString is + * {@code null} or equals "*", then {@code null} is returned to + * indicate a wildcard. + * @throws IllegalArgumentException If the filter syntax is invalid. + */ + private static Filter parseFilter(String filterString) { + if (filterString == null) { + return null; + } + filterString = filterString.trim(); + if (filterString.equals("*")) { + return null; + } + + try { + return FrameworkUtil.createFilter(filterString); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException("invalid filter", e); + } } - public String getActions() - { - if (m_actions == null) - { - m_actions = createActionString(m_actionMask); - } - return m_actions; + /** + * Determines if the specified permission is implied by this object. This + * method throws an exception if the specified permission was not + * constructed with a bundle. + * + *

      + * This method returns {@code true} if the specified permission is an + * AdminPermission AND + *

        + *
      • this object's filter matches the specified permission's bundle ID, + * bundle symbolic name, bundle location and bundle signer distinguished + * name chain OR
      • + *
      • this object's filter is "*"
      • + *
      + * AND this object's actions include all of the specified permission's + * actions. + *

      + * Special case: if the specified permission was constructed with "*" + * filter, then this method returns {@code true} if this object's filter is + * "*" and this object's actions include all of the specified permission's + * actions + * + * @param p The requested permission. + * @return {@code true} if the specified permission is implied by this + * object; {@code false} otherwise. + */ + @Override + public boolean implies(Permission p) { + if (!(p instanceof AdminPermission)) { + return false; + } + AdminPermission requested = (AdminPermission) p; + if (bundle != null) { + return false; + } + // if requested permission has a filter, then it is an invalid argument + if (requested.filter != null) { + return false; + } + return implies0(requested, ACTION_NONE); } - public boolean implies(Permission p) - { - if (!(p instanceof AdminPermission)) - { + /** + * Internal implies method. Used by the implies and the permission + * collection implies methods. + * + * @param requested The requested AdminPermision which has already be + * validated as a proper argument. The requested AdminPermission must + * not have a filter expression. + * @param effective The effective actions with which to start. + * @return {@code true} if the specified permission is implied by this + * object; {@code false} otherwise. + */ + boolean implies0(AdminPermission requested, int effective) { + /* check actions first - much faster */ + effective |= action_mask; + final int desired = requested.action_mask; + if ((effective & desired) != desired) { + return false; + } + + /* Get our filter */ + Filter f = filter; + if (f == null) { + // it's "*" + return true; + } + /* is requested a wildcard filter? */ + if (requested.bundle == null) { return false; } + Map requestedProperties = requested.getProperties(); + if (requestedProperties == null) { + /* + * If the requested properties are null, then we have detected a + * recursion getting the bundle location. So we return true to + * permit the bundle location request in the AdminPermission check + * up the stack to succeed. + */ + return true; + } + return f.matches(requestedProperties); + } - AdminPermission admin = (AdminPermission) p; - - // Make sure that the permission was create with a bundle or a "*". - // Otherwise, throw an Exception - as per spec. - if ((admin.m_bundle == null) && !(admin.getName().equals("(id=*)"))) - { - throw new RuntimeException( - "The specified permission was not constructed with a bundle or *!"); - } - - // Make sure the action mask is a subset. - if ((m_actionMask & admin.m_actionMask) != admin.m_actionMask) - { - return false; - } - - // Special case: if the specified permission was constructed with "*" - // filter, then this method returns true if this object's - // filter is "*". - if (admin.getName().equals("(id=*)")) - { - return getName().equals("(id=*)"); - } - - // Next, if this object was create with a "*" we can return true - // (This way we avoid creating and matching a filter). - if (getName().equals("(id=*)")) - { - return true; - } - - // Otherwise, see if this permission's filter matches the - // dictionary of the passed in permission. - if (m_filterImpl == null) - { - try - { - m_filterImpl = new FilterImpl(getName()); - } - catch (InvalidSyntaxException ex) - { - return false; - } - } - - return m_filterImpl.match(admin.getBundleDictionary()); + /** + * Returns the canonical string representation of the + * {@code AdminPermission} actions. + * + *

      + * Always returns present {@code AdminPermission} actions in the following + * order: {@code class}, {@code execute}, {@code extensionLifecycle}, + * {@code lifecycle}, {@code listener}, {@code metadata}, {@code resolve}, + * {@code resource}, {@code startlevel}, {@code context}, {@code weave}. + * + * @return Canonical string representation of the {@code AdminPermission} + * actions. + */ + @Override + public String getActions() { + String result = actions; + if (result == null) { + StringBuilder sb = new StringBuilder(); + + int mask = action_mask; + if ((mask & ACTION_CLASS) == ACTION_CLASS) { + sb.append(CLASS); + sb.append(','); + } + + if ((mask & ACTION_EXECUTE) == ACTION_EXECUTE) { + sb.append(EXECUTE); + sb.append(','); + } + + if ((mask & ACTION_EXTENSIONLIFECYCLE) == ACTION_EXTENSIONLIFECYCLE) { + sb.append(EXTENSIONLIFECYCLE); + sb.append(','); + } + + if ((mask & ACTION_LIFECYCLE) == ACTION_LIFECYCLE) { + sb.append(LIFECYCLE); + sb.append(','); + } + + if ((mask & ACTION_LISTENER) == ACTION_LISTENER) { + sb.append(LISTENER); + sb.append(','); + } + + if ((mask & ACTION_METADATA) == ACTION_METADATA) { + sb.append(METADATA); + sb.append(','); + } + + if ((mask & ACTION_RESOLVE) == ACTION_RESOLVE) { + sb.append(RESOLVE); + sb.append(','); + } + + if ((mask & ACTION_RESOURCE) == ACTION_RESOURCE) { + sb.append(RESOURCE); + sb.append(','); + } + + if ((mask & ACTION_STARTLEVEL) == ACTION_STARTLEVEL) { + sb.append(STARTLEVEL); + sb.append(','); + } + + if ((mask & ACTION_CONTEXT) == ACTION_CONTEXT) { + sb.append(CONTEXT); + sb.append(','); + } + + if ((mask & ACTION_WEAVE) == ACTION_WEAVE) { + sb.append(WEAVE); + sb.append(','); + } + + // remove trailing comma + if (sb.length() > 0) { + sb.setLength(sb.length() - 1); + } + + actions = result = sb.toString(); + } + return result; } - public PermissionCollection newPermissionCollection() - { + /** + * Returns a new {@code PermissionCollection} object suitable for storing + * {@code AdminPermission}s. + * + * @return A new {@code PermissionCollection} object. + */ + @Override + public PermissionCollection newPermissionCollection() { return new AdminPermissionCollection(); } - private Dictionary getBundleDictionary() - { - if (m_bundleDict == null) - { - // Add bundle properties to dictionary. - m_bundleDict = new Hashtable(); - m_bundleDict.put("id", new Long(m_bundle.getBundleId())); - - String symbolicName = m_bundle.getSymbolicName(); - if (symbolicName != null) - { - m_bundleDict.put("name", symbolicName); - } - // Add location in privileged block since it performs a security check. - if (System.getSecurityManager() != null) - { - AccessController.doPrivileged(new PrivilegedAction() - { - public Object run() - { - m_bundleDict.put("location", m_bundle.getLocation()); - return null; - } - }); - } - else - { - m_bundleDict.put("location", m_bundle.getLocation()); - } - - m_bundleDict.put("signer", new Signer(m_bundle)); - } - return m_bundleDict; - } - - private static int parseActions(String actions) - { - if (actions == null) - { - return ALL_MASK; - } - - int mask = 0; - - StringTokenizer st = new StringTokenizer(actions, ", "); - while (st.hasMoreTokens()) - { - String s = st.nextToken(); - if (s.equals("*")) - { - mask = ALL_MASK; - break; - } - else if (s.equalsIgnoreCase(CLASS)) - { - mask |= CLASS_MASK; - } - else if (s.equalsIgnoreCase(EXECUTE)) - { - mask |= EXECUTE_MASK; - } - else if (s.equalsIgnoreCase(EXTENSIONLIFECYCLE)) - { - mask |= EXTENSIONLIFECYCLE_MASK; - } - else if (s.equalsIgnoreCase(LIFECYCLE)) - { - mask |= LIFECYCLE_MASK; - } - else if (s.equalsIgnoreCase(LISTENER)) - { - mask |= LISTENER_MASK; - } - else if (s.equalsIgnoreCase(METADATA)) - { - mask |= METADATA_MASK; - } - else if (s.equalsIgnoreCase(RESOLVE)) - { - mask |= RESOLVE_MASK; - } - else if (s.equalsIgnoreCase(RESOURCE)) - { - mask |= RESOURCE_MASK; - } - else if (s.equalsIgnoreCase(STARTLEVEL)) - { - mask |= STARTLEVEL_MASK; - } - } - - return mask; - } - - private static String createActionString(int mask) - { - StringBuffer sb = new StringBuffer(); - - if ((mask & CLASS_MASK) > 0) - { - sb.append(CLASS); - sb.append(","); - } - - if ((mask & EXECUTE_MASK) > 0) - { - sb.append(EXECUTE); - sb.append(","); - } - - if ((mask & EXTENSIONLIFECYCLE_MASK) > 0) - { - sb.append(EXTENSIONLIFECYCLE); - sb.append(","); - } - - if ((mask & LIFECYCLE_MASK) > 0) - { - sb.append(LIFECYCLE); - sb.append(","); - } - - if ((mask & LISTENER_MASK) > 0) - { - sb.append(LISTENER); - sb.append(","); - } - - if ((mask & METADATA_MASK) > 0) - { - sb.append(METADATA); - sb.append(","); - } - - if ((mask & RESOLVE_MASK) > 0) - { - sb.append(RESOLVE); - sb.append(","); - } - - if ((mask & RESOURCE_MASK) > 0) - { - sb.append(RESOURCE); - sb.append(","); - } - - if ((mask & STARTLEVEL_MASK) > 0) - { - sb.append(STARTLEVEL); - sb.append(","); - } - - // Remove trailing comma. - if (sb.length() > 0) - { - sb.setLength(sb.length() - 1); - } - - return sb.toString(); - } - - private static String createName(Bundle bundle) - { - StringBuffer sb = new StringBuffer(); - sb.append("(id="); - sb.append(bundle.getBundleId()); - sb.append(")"); - return sb.toString(); - } - - public static final class Signer implements PrivilegedAction - { - private Bundle m_bundleImpl = null; - private String m_filter = null; - private Method m_getSubjectDNs = null; - private boolean m_init = false; - - public Signer(String filter) - { - m_filter = filter; - } - - Signer(Bundle bundle) - { - m_bundleImpl = bundle; - } - - public boolean equals(Object o) - { - if (!(o instanceof Signer)) - { - return false; - } - - String pattern = ((Signer) o).m_filter; - - if (pattern == null) - { - return true; - } - - String[] dns = getSubjectDNs(); - - if (dns == null) - { - return pattern.trim().equals("\\*"); - } - - for (int i = 0;i < dns.length;i++) - { - if (match(pattern, dns[i])) - { - return true; - } - } - - return false; - } - - private String[] getSubjectDNs() - { - if (System.getSecurityManager() != null) - { - return (String[]) AccessController.doPrivileged(this); - } - else - { - return null; - } - } - - public Object run() - { - try - { - if (!m_init) - { - m_getSubjectDNs = - m_bundleImpl.getClass().getDeclaredMethod("getSubjectDNs", null); - - m_init = true; - } - - m_getSubjectDNs.setAccessible(true); - - return m_getSubjectDNs.invoke(m_bundleImpl, null); - - } - catch (Exception ex) - { - // TODO: log this or something - ex.printStackTrace(); - } - - return null; - } - - private static boolean match(String pattern, String dn) - { - try - { - return ((pattern != null) && (dn != null)) ? - matchDN(pattern.toCharArray(), 0, dn.toCharArray(), 0) : false; - } - catch (Exception ex) - { - // TODO: log this or something - ex.printStackTrace(); - } - - return false; - } - - private static boolean matchDN(char[] pattern, int pPos, char[] dn, int dPos) - { - pPos = skip(pattern, pPos, ' '); - - if (pPos >= pattern.length) - { - return true; - } - - int befor = pPos; - - if ((pPos < pattern.length -1) && (pattern[pPos] == '\\') && (pattern[pPos + 1] == '*')) - { - pPos = pPos + 1; - } - - switch (pattern[pPos++]) - { - case '*': - pPos = skip(pattern, pPos, ' '); - if ((pPos < pattern.length) && (pattern[pPos] == ';')) - { - if (matchDN(pattern, ++pPos, dn, dPos)) - { - return true; - } - return matchDN(pattern, pPos, dn, skipEscapedUntil(dn, dPos, ';') + 1); - } - if (pPos >= pattern.length) - { - return true; - } - return matchRDN(pattern, befor, dn, dPos); - case '-': - pPos = skip(pattern, pPos, ' '); - if ((pPos < pattern.length) && (pattern[pPos] == ';')) - { - int next = dPos; - pPos++; - do - { - if (matchDN(pattern, pPos, dn, next)) - { - return true; - } - next = skipEscapedUntil(dn, next, ';') + 1; - } while (next < dn.length); - - return false; - } - if (pPos >= pattern.length) - { - return true; - } - throw new IllegalArgumentException("[" + pPos + "]" + new String(pattern)); - default: - break; - } - - return matchRDN(pattern, befor, dn, dPos); - } - - private static boolean matchRDN(char[] pattern, int pPos, char[] dn, int dPos) - { - pPos = skip(pattern, pPos, ' '); - - if (pPos >= pattern.length) - { - return true; - } - - if ((pPos < pattern.length -1) && (pattern[pPos] == '\\') && (pattern[pPos + 1] == '*')) - { - pPos = pPos + 1; - } - - switch (pattern[pPos++]) - { - case '*': - pPos = skip(pattern, pPos, ' '); - if ((pPos < pattern.length) && (pattern[pPos] == ',')) - { - pPos++; - do - { - if (matchKV(pattern, pPos, dn, dPos)) - { - return true; - } - - int comma = skipEscapedUntil(dn, dPos, ','); - int colon = skipEscapedUntil(dn, dPos, ';'); - - dPos = (comma > colon) ? colon : comma; - } while ((dPos < dn.length) && (dn[dPos++] == ',')); - return false; - } - throw new IllegalArgumentException("[" + pPos + "]" + new String(pattern)); - default: - break; - } - - return matchKV(pattern, pPos - 1, dn, dPos); - } - - private static boolean matchKV(char[] pattern, int pPos, char[] dn, int dPos) - { - pPos = skip(pattern, pPos, ' '); - - if (pPos >= pattern.length) - { - return false; - } - - int equals = skipEscapedUntil(pattern, pPos, '='); - int comma = skipEscapedUntil(pattern, pPos, ','); - int colon = skipEscapedUntil(pattern, pPos, ';'); - if (((colon < pattern.length) && (colon < equals)) || - ((comma < pattern.length) && (comma < equals)) || - (equals >= pattern.length)) - { - return false; - } - - String key = (String) KEY2OIDSTRING.get( - new String(pattern, pPos, equals - pPos).toLowerCase(Locale.US).trim()); - - if (key == null) - { - throw new IllegalArgumentException("Bad key [" + - new String(pattern, pPos, equals - pPos) + "] in [" + - new String(pattern) + "]"); - } - - pPos = equals + 1; - int keylength = key.length(); - for (int i = 0;i < keylength;i++) - { - if ((dPos >= dn.length) || (key.charAt(i) != dn[dPos++])) - { - return false; - } - } - - if ((dPos >= dn.length) || (dn[dPos++] != '=')) - { - return false; - } - - pPos = skip(pattern, pPos, ' '); - if ((pPos < pattern.length -1) && (pattern[pPos] == '\\') && (pattern[pPos + 1] == '*')) - { - pPos = skip(pattern, pPos + 2, ' '); - if (pPos >= pattern.length) - { - return true; - } - comma = skipEscapedUntil(dn, dPos, ','); - colon = skipEscapedUntil(dn, dPos, ';'); - if ((pattern[pPos] == ',') && (colon > comma)) - { - return matchKV(pattern, ++pPos, dn, comma + 1); - } - - if (pattern[pPos] == ';' ) - { - return matchDN(pattern, ++pPos, dn, colon + 1); - } - - return false; - } - boolean escaped = false; - while ((pPos < pattern.length) && (dPos < dn.length)) - { - switch (Character.toLowerCase(pattern[pPos++])) - { - case ' ': - if ((pattern[pPos - 2] != ' ') && ((dn[dPos++] != ' ') && - (dn[--dPos] != ';') && (dn[dPos] != ','))) - { - return false; - } - break; - case '\\': - escaped = !escaped; - break; - - case '(': - case ')': - if (escaped) - { - if (dn[dPos++] != pattern[pPos - 1]) - { - return false; - } - escaped = false; - break; - } - return false; - case ';': - if (!escaped) - { - if ((dPos < dn.length) && ((dn[dPos] == ',') || (dn[dPos] == ';'))) - { - return matchDN(pattern, pPos, dn, skipEscapedUntil(dn, dPos, ';') + 1); - } - return false; - } - case ',': - if (!escaped) - { - if ((dPos < dn.length) && (dn[dPos] == ',')) - { - return matchKV(pattern, pPos, dn, dPos + 1); - } - return false; - } - default: - if (escaped) - { - if (dn[dPos++] != '\\') - { - return false; - } - escaped = false; - } - if (dn[dPos++] != Character.toLowerCase(pattern[pPos - 1])) - { - return false; - } - break; - } - } - - pPos = skip(pattern, pPos, ' '); - if (pPos >= pattern.length) - { - if ((dPos >= dn.length) || (dn[dPos] == ',') || (dn[dPos] == ';')) - { - return true; - } - } - else - { - switch (pattern[pPos++]) - { - case ',': - return matchKV(pattern, pPos, dn, dPos); - case ';': - return matchDN(pattern, pPos, dn, dPos); - default: - break; - } - } - - return false; - } - - private static final Map KEY2OIDSTRING = new HashMap(); - - static { - KEY2OIDSTRING.put("2.5.4.3", "cn"); - KEY2OIDSTRING.put("cn", "cn"); - KEY2OIDSTRING.put("commonname", "cn"); - KEY2OIDSTRING.put("2.5.4.4", "sn"); - KEY2OIDSTRING.put("sn", "sn"); - KEY2OIDSTRING.put("surname", "sn"); - KEY2OIDSTRING.put("2.5.4.6", "c"); - KEY2OIDSTRING.put("c", "c"); - KEY2OIDSTRING.put("countryname", "c"); - KEY2OIDSTRING.put("2.5.4.7", "l"); - KEY2OIDSTRING.put("l", "l"); - KEY2OIDSTRING.put("localityname", "l"); - KEY2OIDSTRING.put("2.5.4.8", "st"); - KEY2OIDSTRING.put("st", "st"); - KEY2OIDSTRING.put("stateorprovincename", "st"); - KEY2OIDSTRING.put("2.5.4.10", "o"); - KEY2OIDSTRING.put("o", "o"); - KEY2OIDSTRING.put("organizationname", "o"); - KEY2OIDSTRING.put("2.5.4.11", "ou"); - KEY2OIDSTRING.put("ou", "ou"); - KEY2OIDSTRING.put("organizationalunitname", "ou"); - KEY2OIDSTRING.put("2.5.4.12", "title"); - KEY2OIDSTRING.put("t", "title"); - KEY2OIDSTRING.put("title", "title"); - KEY2OIDSTRING.put("2.5.4.42", "givenname"); - KEY2OIDSTRING.put("givenname", "givenname"); - KEY2OIDSTRING.put("2.5.4.43", "initials"); - KEY2OIDSTRING.put("initials", "initials"); - KEY2OIDSTRING.put("2.5.4.44", "generationqualifier"); - KEY2OIDSTRING.put("generationqualifier", "generationqualifier"); - KEY2OIDSTRING.put("2.5.4.46", "dnqualifier"); - KEY2OIDSTRING.put("dnqualifier", "dnqualifier"); - KEY2OIDSTRING.put("2.5.4.9", "street"); - KEY2OIDSTRING.put("street", "street"); - KEY2OIDSTRING.put("streetaddress", "street"); - KEY2OIDSTRING.put("0.9.2342.19200300.100.1.25", "dc"); - KEY2OIDSTRING.put("dc", "dc"); - KEY2OIDSTRING.put("domaincomponent", "dc"); - KEY2OIDSTRING.put("0.9.2342.19200300.100.1.1", "uid"); - KEY2OIDSTRING.put("uid", "uid"); - KEY2OIDSTRING.put("userid", "uid"); - KEY2OIDSTRING.put("1.2.840.113549.1.9.1", "emailaddress"); - KEY2OIDSTRING.put("emailaddress", "emailaddress"); - KEY2OIDSTRING.put("2.5.4.5", "serialnumber"); - KEY2OIDSTRING.put("serialnumber", "serialnumber"); - } - - private static int skipEscapedUntil(char[] string, int pos, char value) - { - boolean escaped = false; - - while (pos < string.length) - { - switch (string[pos++]) - { - case '\\': - escaped = true; - break; - default: - if (!escaped) - { - if (string[pos - 1] == value) - { - return pos - 1; - } - } - escaped = false; - break; - } - } - - return pos; - } - - private static int skip(char[] string, int pos, char value) - { - while (pos < string.length) - { - if (string[pos] != value) - { - break; - } - pos++; - } - - return pos; - } - } + /** + * Determines the equality of two {@code AdminPermission} objects. + * + * @param obj The object being compared for equality with this object. + * @return {@code true} if {@code obj} is equivalent to this + * {@code AdminPermission}; {@code false} otherwise. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (!(obj instanceof AdminPermission)) { + return false; + } + + AdminPermission ap = (AdminPermission) obj; + + return (action_mask == ap.action_mask) && ((bundle == ap.bundle) || ((bundle != null) && bundle.equals(ap.bundle))) && (filter == null ? ap.filter == null : filter.equals(ap.filter)); + } + + /** + * Returns the hash code value for this object. + * + * @return Hash code value for this object. + */ + @Override + public int hashCode() { + int h = 31 * 17 + getName().hashCode(); + h = 31 * h + getActions().hashCode(); + if (bundle != null) { + h = 31 * h + bundle.hashCode(); + } + return h; + } + + /** + * WriteObject is called to save the state of this permission object to a + * stream. The actions are serialized, and the superclass takes care of the + * name. + */ + private synchronized void writeObject(java.io.ObjectOutputStream s) throws IOException { + if (bundle != null) { + throw new NotSerializableException("cannot serialize"); + } + // Write out the actions. The superclass takes care of the name + // call getActions to make sure actions field is initialized + if (actions == null) + getActions(); + s.defaultWriteObject(); + } + + /** + * readObject is called to restore the state of this permission from a + * stream. + */ + private synchronized void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { + // Read in the data, then initialize the transients + s.defaultReadObject(); + setTransients(parseFilter(getName()), parseActions(actions)); + } + + /** + * Called by {@code implies0} on an AdminPermission which was constructed + * with a Bundle. This method loads a map with the filter-matchable + * properties of this bundle. The map is cached so this lookup only happens + * once. + * + * This method should only be called on an AdminPermission which was + * constructed with a bundle + * + * @return a map of properties for this bundle + */ + private Map getProperties() { + Map result = properties; + if (result != null) { + return result; + } + /* + * We may have recursed here due to the Bundle.getLocation call in the + * doPrivileged below. If this is the case, return null to allow implies + * to return true. + */ + final Object mark = recurse.get(); + if (mark == bundle) { + return null; + } + recurse.set(bundle); + try { + final Map map = new HashMap(4); + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Void run() { + map.put("id", Long.valueOf(bundle.getBundleId())); + map.put("location", bundle.getLocation()); + String name = bundle.getSymbolicName(); + if (name != null) { + map.put("name", name); + } + SignerProperty signer = new SignerProperty(bundle); + if (signer.isBundleSigned()) { + map.put("signer", signer); + } + return null; + } + }); + return properties = map; + } finally { + recurse.set(null); + } + } } -final class AdminPermissionCollection extends PermissionCollection -{ - private static final long serialVersionUID = 3747361397420496672L; - private HashMap m_map = new HashMap(); - - public void add(Permission permission) - { - if (!(permission instanceof AdminPermission)) - { - throw new IllegalArgumentException("Invalid permission: " + permission); - } - else if (isReadOnly()) - { - throw new SecurityException( - "Cannot add to read-only permission collection."); - } - - AdminPermission admin = (AdminPermission) permission; - AdminPermission current = (AdminPermission) m_map.get(admin.getName()); - if (current != null) - { - if (admin.m_actionMask != current.m_actionMask) - { - m_map.put(admin.getName(), - new AdminPermission(admin.getName(), - admin.m_actionMask | current.m_actionMask)); - } - } - else - { - m_map.put(admin.getName(), admin); - } - } - - public boolean implies(Permission permission) - { - if (!(permission instanceof AdminPermission)) - { - return false; - } - - for (Iterator iter = m_map.values().iterator(); iter.hasNext(); ) - { - if (((AdminPermission) iter.next()).implies(permission)) - { - return true; - } - } - - return false; - } - - public Enumeration elements() - { - return Collections.enumeration(m_map.values()); - } +/** + * Stores a collection of {@code AdminPermission}s. + */ +final class AdminPermissionCollection extends PermissionCollection { + private static final long serialVersionUID = 3906372644575328048L; + /** + * Collection of permissions. + * + * @GuardedBy this + */ + private transient Map permissions; + + /** + * Boolean saying if "*" is in the collection. + * + * @serial + * @GuardedBy this + */ + private boolean all_allowed; + + /** + * Create an empty AdminPermissions object. + * + */ + public AdminPermissionCollection() { + permissions = new HashMap(); + } + + /** + * Adds a permission to this permission collection. + * + * @param permission The {@code AdminPermission} object to add. + * @throws IllegalArgumentException If the specified permission is not an + * {@code AdminPermission} instance or was constructed with a Bundle + * object. + * @throws SecurityException If this {@code AdminPermissionCollection} + * object has been marked read-only. + */ + @Override + public void add(Permission permission) { + if (!(permission instanceof AdminPermission)) { + throw new IllegalArgumentException("invalid permission: " + permission); + } + if (isReadOnly()) { + throw new SecurityException("attempt to add a Permission to a " + "readonly PermissionCollection"); + } + final AdminPermission ap = (AdminPermission) permission; + if (ap.bundle != null) { + throw new IllegalArgumentException("cannot add to collection: " + ap); + } + final String name = ap.getName(); + synchronized (this) { + Map pc = permissions; + AdminPermission existing = pc.get(name); + if (existing != null) { + int oldMask = existing.action_mask; + int newMask = ap.action_mask; + + if (oldMask != newMask) { + pc.put(name, new AdminPermission(existing.filter, oldMask | newMask)); + } + } else { + pc.put(name, ap); + } + if (!all_allowed) { + if (name.equals("*")) { + all_allowed = true; + } + } + } + } + + /** + * Determines if the specified permissions implies the permissions expressed + * in {@code permission}. + * + * @param permission The Permission object to compare with the + * {@code AdminPermission} objects in this collection. + * @return {@code true} if {@code permission} is implied by an + * {@code AdminPermission} in this collection, {@code false} + * otherwise. + */ + @Override + public boolean implies(Permission permission) { + if (!(permission instanceof AdminPermission)) { + return false; + } + + AdminPermission requested = (AdminPermission) permission; + // if requested permission has a filter, then it is an invalid argument + if (requested.filter != null) { + return false; + } + int effective = AdminPermission.ACTION_NONE; + Collection perms; + synchronized (this) { + Map pc = permissions; + // short circuit if the "*" Permission was added + if (all_allowed) { + AdminPermission ap = pc.get("*"); + if (ap != null) { + effective |= ap.action_mask; + final int desired = requested.action_mask; + if ((effective & desired) == desired) { + return true; + } + } + } + perms = pc.values(); + } + + // just iterate one by one + for (AdminPermission perm : perms) { + if (perm.implies0(requested, effective)) { + return true; + } + } + return false; + } + + /** + * Returns an enumeration of all {@code AdminPermission} objects in the + * container. + * + * @return Enumeration of all {@code AdminPermission} objects. + */ + @Override + public synchronized Enumeration elements() { + List all = new ArrayList(permissions.values()); + return Collections.enumeration(all); + } + + /* serialization logic */ + private static final ObjectStreamField[] serialPersistentFields = {new ObjectStreamField("permissions", Hashtable.class), new ObjectStreamField("all_allowed", Boolean.TYPE)}; + + private synchronized void writeObject(ObjectOutputStream out) throws IOException { + Hashtable hashtable = new Hashtable(permissions); + ObjectOutputStream.PutField pfields = out.putFields(); + pfields.put("permissions", hashtable); + pfields.put("all_allowed", all_allowed); + out.writeFields(); + } + + private synchronized void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { + ObjectInputStream.GetField gfields = in.readFields(); + @SuppressWarnings("unchecked") + Hashtable hashtable = (Hashtable) gfields.get("permissions", null); + permissions = new HashMap(hashtable); + all_allowed = gfields.get("all_allowed", false); + } } diff --git a/framework/src/main/java/org/osgi/framework/AllServiceListener.java b/framework/src/main/java/org/osgi/framework/AllServiceListener.java new file mode 100644 index 00000000000..5ae5bd5e242 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/AllServiceListener.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) OSGi Alliance (2005, 2015). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import org.osgi.annotation.versioning.ConsumerType; + +/** + * A {@code ServiceEvent} listener that does not filter based upon package + * wiring. {@code AllServiceListener} is a listener interface that may be + * implemented by a bundle developer. When a {@code ServiceEvent} is fired, it + * is synchronously delivered to an {@code AllServiceListener}. The Framework + * may deliver {@code ServiceEvent} objects to an {@code AllServiceListener} out + * of order and may concurrently call and/or reenter an + * {@code AllServiceListener}. + *

      + * An {@code AllServiceListener} object is registered with the Framework using + * the {@code BundleContext.addServiceListener} method. + * {@code AllServiceListener} objects are called with a {@code ServiceEvent} + * object when a service is registered, modified, or is in the process of + * unregistering. + * + *

      + * {@code ServiceEvent} object delivery to {@code AllServiceListener} objects is + * filtered by the filter specified when the listener was registered. If the + * Java Runtime Environment supports permissions, then additional filtering is + * done. {@code ServiceEvent} objects are only delivered to the listener if the + * bundle which defines the listener object's class has the appropriate + * {@code ServicePermission} to get the service using at least one of the named + * classes under which the service was registered. + * + *

      + * Unlike normal {@code ServiceListener} objects, {@code AllServiceListener} + * objects receive all {@code ServiceEvent} objects regardless of whether the + * package source of the listening bundle is equal to the package source of the + * bundle that registered the service. This means that the listener may not be + * able to cast the service object to any of its corresponding service + * interfaces if the service object is retrieved. + * + * @see ServiceEvent + * @see ServicePermission + * @ThreadSafe + * @since 1.3 + * @author $Id: 7eba8b2b69fd8e68b793fd09611b6efdc6fdd73c $ + */ +@ConsumerType +@FunctionalInterface +public interface AllServiceListener extends ServiceListener { + // This is a marker interface +} diff --git a/framework/src/main/java/org/osgi/framework/Bundle.java b/framework/src/main/java/org/osgi/framework/Bundle.java new file mode 100644 index 00000000000..2ba3a33f2d6 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/Bundle.java @@ -0,0 +1,1214 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2018). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.security.cert.X509Certificate; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.wiring.FrameworkWiring; + +/** + * An installed bundle in the Framework. + * + *

      + * A {@code Bundle} object is the access point to define the lifecycle of an + * installed bundle. Each bundle installed in the OSGi environment must have an + * associated {@code Bundle} object. + * + *

      + * A bundle must have a unique identity, a {@code long}, chosen by the + * Framework. This identity must not change during the lifecycle of a bundle, + * even when the bundle is updated. Uninstalling and then reinstalling the + * bundle must create a new unique identity. + * + *

      + * A bundle can be in one of six states: + *

        + *
      • {@link #UNINSTALLED}
      • + *
      • {@link #INSTALLED}
      • + *
      • {@link #RESOLVED}
      • + *
      • {@link #STARTING}
      • + *
      • {@link #STOPPING}
      • + *
      • {@link #ACTIVE}
      • + *
      + *

      + * Values assigned to these states have no specified ordering; they represent + * bit values that may be ORed together to determine if a bundle is in one of + * the valid states. + * + *

      + * A bundle should only have active threads of execution when its state is one + * of {@code STARTING},{@code ACTIVE}, or {@code STOPPING}. An + * {@code UNINSTALLED} bundle can not be set to another state; it is a zombie + * and can only be reached because references are kept somewhere. + * + *

      + * The Framework is the only entity that is allowed to create {@code Bundle} + * objects, and these objects are only valid within the Framework that created + * them. + * + *

      + * Bundles have a natural ordering such that if two {@code Bundle}s have the + * same {@link #getBundleId() bundle id} they are equal. A {@code Bundle} is + * less than another {@code Bundle} if it has a lower {@link #getBundleId() + * bundle id} and is greater if it has a higher bundle id. + * + * @ThreadSafe + * @author $Id: 545299bc454bb01ef73a14693ffec76a13430eea $ + */ +@ProviderType +public interface Bundle extends Comparable { + /** + * The bundle is uninstalled and may not be used. + * + *

      + * The {@code UNINSTALLED} state is only visible after a bundle is + * uninstalled; the bundle is in an unusable state but references to the + * {@code Bundle} object may still be available and used for introspection. + *

      + * The value of {@code UNINSTALLED} is 0x00000001. + */ + int UNINSTALLED = 0x00000001; + + /** + * The bundle is installed but not yet resolved. + * + *

      + * A bundle is in the {@code INSTALLED} state when it has been installed in + * the Framework but is not or cannot be resolved. + *

      + * This state is visible if the bundle's code dependencies are not resolved. + * The Framework may attempt to resolve an {@code INSTALLED} bundle's code + * dependencies and move the bundle to the {@code RESOLVED} state. + *

      + * The value of {@code INSTALLED} is 0x00000002. + */ + int INSTALLED = 0x00000002; + + /** + * The bundle is resolved and is able to be started. + * + *

      + * A bundle is in the {@code RESOLVED} state when the Framework has + * successfully resolved the bundle's code dependencies. These dependencies + * include: + *

        + *
      • The bundle's class path from its {@link Constants#BUNDLE_CLASSPATH} + * Manifest header.
      • + *
      • The bundle's package dependencies from its + * {@link Constants#EXPORT_PACKAGE} and {@link Constants#IMPORT_PACKAGE} + * Manifest headers.
      • + *
      • The bundle's required bundle dependencies from its + * {@link Constants#REQUIRE_BUNDLE} Manifest header.
      • + *
      • A fragment bundle's host dependency from its + * {@link Constants#FRAGMENT_HOST} Manifest header.
      • + *
      + *

      + * Note that the bundle is not active yet. A bundle must be put in the + * {@code RESOLVED} state before it can be started. The Framework may + * attempt to resolve a bundle at any time. + *

      + * The value of {@code RESOLVED} is 0x00000004. + */ + int RESOLVED = 0x00000004; + + /** + * The bundle is in the process of starting. + * + *

      + * A bundle is in the {@code STARTING} state when its {@link #start(int) + * start} method is active. A bundle must be in this state when the bundle's + * {@link BundleActivator#start(BundleContext)} is called. If the + * {@code BundleActivator.start} method completes without exception, then + * the bundle has successfully started and must move to the {@code ACTIVE} + * state. + *

      + * If the bundle has a {@link Constants#ACTIVATION_LAZY lazy activation + * policy}, then the bundle may remain in this state for some time until the + * activation is triggered. + *

      + * The value of {@code STARTING} is 0x00000008. + */ + int STARTING = 0x00000008; + + /** + * The bundle is in the process of stopping. + * + *

      + * A bundle is in the {@code STOPPING} state when its {@link #stop(int) + * stop} method is active. A bundle must be in this state when the bundle's + * {@link BundleActivator#stop(BundleContext)} method is called. When the + * {@code BundleActivator.stop} method completes the bundle is stopped and + * must move to the {@code RESOLVED} state. + *

      + * The value of {@code STOPPING} is 0x00000010. + */ + int STOPPING = 0x00000010; + + /** + * The bundle is now running. + * + *

      + * A bundle is in the {@code ACTIVE} state when it has been successfully + * started and activated. + *

      + * The value of {@code ACTIVE} is 0x00000020. + */ + int ACTIVE = 0x00000020; + + /** + * The bundle start operation is transient and the persistent autostart + * setting of the bundle is not modified. + * + *

      + * This bit may be set when calling {@link #start(int)} to notify the + * framework that the autostart setting of the bundle must not be modified. + * If this bit is not set, then the autostart setting of the bundle is + * modified. + * + * @since 1.4 + * @see #start(int) + */ + int START_TRANSIENT = 0x00000001; + + /** + * The bundle start operation must activate the bundle according to the + * bundle's declared {@link Constants#BUNDLE_ACTIVATIONPOLICY activation + * policy}. + * + *

      + * This bit may be set when calling {@link #start(int)} to notify the + * framework that the bundle must be activated using the bundle's declared + * activation policy. + * + * @since 1.4 + * @see Constants#BUNDLE_ACTIVATIONPOLICY + * @see #start(int) + */ + int START_ACTIVATION_POLICY = 0x00000002; + + /** + * The bundle stop is transient and the persistent autostart setting of the + * bundle is not modified. + * + *

      + * This bit may be set when calling {@link #stop(int)} to notify the + * framework that the autostart setting of the bundle must not be modified. + * If this bit is not set, then the autostart setting of the bundle is + * modified. + * + * @since 1.4 + * @see #stop(int) + */ + int STOP_TRANSIENT = 0x00000001; + + /** + * Request that all certificates used to sign the bundle be returned. + * + * @since 1.5 + * @see #getSignerCertificates(int) + */ + int SIGNERS_ALL = 1; + + /** + * Request that only certificates used to sign the bundle that are trusted + * by the framework be returned. + * + * @since 1.5 + * @see #getSignerCertificates(int) + */ + int SIGNERS_TRUSTED = 2; + + /** + * Returns this bundle's current state. + * + *

      + * A bundle can be in only one state at any time. + * + * @return An element of {@code UNINSTALLED},{@code INSTALLED}, + * {@code RESOLVED}, {@code STARTING}, {@code STOPPING}, + * {@code ACTIVE}. + */ + int getState(); + + /** + * Starts this bundle. + * + *

      + * If this bundle's state is {@code UNINSTALLED} then an + * {@code IllegalStateException} is thrown. + *

      + * If the current start level is less than this bundle's start level: + *

        + *
      • If the {@link #START_TRANSIENT} option is set, then a + * {@code BundleException} is thrown indicating this bundle cannot be + * started due to the Framework's current start level.
      • + *
      • Otherwise, the Framework must set this bundle's persistent autostart + * setting to Started with declared activation if the + * {@link #START_ACTIVATION_POLICY} option is set or + * Started with eager activation if not set.
      • + *
      + *

      + * When the Framework's current start level becomes equal to or more than + * this bundle's start level, this bundle will be started. + *

      + * Otherwise, the following steps are required to start this bundle: + *

        + *
      1. If this bundle is in the process of being activated or deactivated + * then this method must wait for activation or deactivation to complete + * before continuing. If this does not occur in a reasonable time, a + * {@code BundleException} is thrown to indicate this bundle was unable to + * be started.
      2. + *
      3. If the {@link #START_TRANSIENT} option is not set then set this + * bundle's autostart setting to Started with declared activation + * if the {@link #START_ACTIVATION_POLICY} option is set or + * Started with eager activation if not set. When the Framework is + * restarted and this bundle's autostart setting is not Stopped, + * this bundle must be automatically started.
      4. + *
      5. If this bundle's state is {@code ACTIVE} then this method returns + * immediately.
      6. + *
      7. If this bundle's state is not {@code RESOLVED}, an attempt is made to + * resolve this bundle. If the Framework cannot resolve this bundle, a + * {@code BundleException} is thrown.
      8. + *
      9. If the {@link #START_ACTIVATION_POLICY} option is set and this + * bundle's declared activation policy is {@link Constants#ACTIVATION_LAZY + * lazy} then: + *
          + *
        • If this bundle's state is {@code STARTING} then this method returns + * immediately.
        • + *
        • This bundle's state is set to {@code STARTING}.
        • + *
        • A bundle event of type {@link BundleEvent#LAZY_ACTIVATION} is fired.
        • + *
        • This method returns immediately and the remaining steps will be + * followed when this bundle's activation is later triggered.
        • + *
        + *
      10. + *
      11. This bundle's state is set to {@code STARTING}.
      12. + *
      13. A bundle event of type {@link BundleEvent#STARTING} is fired.
      14. + *
      15. The {@link BundleActivator#start(BundleContext)} method of this + * bundle's {@code BundleActivator}, if one is specified, is called. If the + * {@code BundleActivator} is invalid or throws an exception then: + *
          + *
        • This bundle's state is set to {@code STOPPING}.
        • + *
        • A bundle event of type {@link BundleEvent#STOPPING} is fired.
        • + *
        • Any services registered by this bundle must be unregistered.
        • + *
        • Any services used by this bundle must be released.
        • + *
        • Any listeners registered by this bundle must be removed.
        • + *
        • This bundle's state is set to {@code RESOLVED}.
        • + *
        • A bundle event of type {@link BundleEvent#STOPPED} is fired.
        • + *
        • A {@code BundleException} is then thrown.
        • + *
        + *
      16. + *
      17. This bundle's state is set to {@code ACTIVE}.
      18. + *
      19. A bundle event of type {@link BundleEvent#STARTED} is fired.
      20. + *
      + * + * Preconditions + *
        + *
      • {@code getState()} in { {@code INSTALLED}, {@code RESOLVED} + * } or { {@code INSTALLED}, {@code RESOLVED}, + * {@code STARTING} } if this bundle has a lazy activation policy.
      • + *
      + * Postconditions, no exceptions thrown + *
        + *
      • Bundle autostart setting is modified unless the + * {@link #START_TRANSIENT} option was set.
      • + *
      • {@code getState()} in { {@code ACTIVE} } unless the + * lazy activation policy was used.
      • + *
      • {@code BundleActivator.start()} has been called and did not throw an + * exception unless the lazy activation policy was used.
      • + *
      + * Postconditions, when an exception is thrown + *
        + *
      • Depending on when the exception occurred, bundle autostart setting is + * modified unless the {@link #START_TRANSIENT} option was set.
      • + *
      • {@code getState()} not in { {@code STARTING}, {@code ACTIVE} + * }.
      • + *
      + * + * @param options The options for starting this bundle. See + * {@link #START_TRANSIENT} and {@link #START_ACTIVATION_POLICY}. The + * Framework must ignore unrecognized options. + * @throws BundleException If this bundle could not be started. + * BundleException types thrown by this method include: + * {@link BundleException#START_TRANSIENT_ERROR}, + * {@link BundleException#NATIVECODE_ERROR}, + * {@link BundleException#RESOLVE_ERROR}, + * {@link BundleException#STATECHANGE_ERROR}, and + * {@link BundleException#ACTIVATOR_ERROR}. + * @throws IllegalStateException If this bundle has been uninstalled or this + * bundle tries to change its own state. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[this,EXECUTE]}, and the Java Runtime + * Environment supports permissions. + * @since 1.4 + */ + void start(int options) throws BundleException; + + /** + * Starts this bundle with no options. + * + *

      + * This method performs the same function as calling {@code start(0)}. + * + * @throws BundleException If this bundle could not be started. + * BundleException types thrown by this method include: + * {@link BundleException#NATIVECODE_ERROR}, + * {@link BundleException#RESOLVE_ERROR}, + * {@link BundleException#STATECHANGE_ERROR}, and + * {@link BundleException#ACTIVATOR_ERROR}. + * @throws IllegalStateException If this bundle has been uninstalled or this + * bundle tries to change its own state. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[this,EXECUTE]}, and the Java Runtime + * Environment supports permissions. + * @see #start(int) + */ + void start() throws BundleException; + + /** + * Stops this bundle. + * + *

      + * The following steps are required to stop a bundle: + *

        + *
      1. If this bundle's state is {@code UNINSTALLED} then an + * {@code IllegalStateException} is thrown.
      2. + *
      3. If this bundle is in the process of being activated or deactivated + * then this method must wait for activation or deactivation to complete + * before continuing. If this does not occur in a reasonable time, a + * {@code BundleException} is thrown to indicate this bundle was unable to + * be stopped.
      4. + *
      5. If the {@link #STOP_TRANSIENT} option is not set then set this + * bundle's persistent autostart setting to Stopped. When the + * Framework is restarted and this bundle's autostart setting is + * Stopped, this bundle must not be automatically started.
      6. + *
      7. If this bundle's state is not {@code STARTING} or {@code ACTIVE} then + * this method returns immediately.
      8. + *
      9. This bundle's state is set to {@code STOPPING}.
      10. + *
      11. A bundle event of type {@link BundleEvent#STOPPING} is fired.
      12. + *
      13. If this bundle's state was {@code ACTIVE} prior to setting the state + * to {@code STOPPING}, the {@link BundleActivator#stop(BundleContext)} + * method of this bundle's {@code BundleActivator}, if one is specified, is + * called. If that method throws an exception, this method must continue to + * stop this bundle and a {@code BundleException} must be thrown after + * completion of the remaining steps.
      14. + *
      15. Any services registered by this bundle must be unregistered.
      16. + *
      17. Any services used by this bundle must be released.
      18. + *
      19. Any listeners registered by this bundle must be removed.
      20. + *
      21. If this bundle's state is {@code UNINSTALLED}, because this bundle + * was uninstalled while the {@code BundleActivator.stop} method was + * running, a {@code BundleException} must be thrown.
      22. + *
      23. This bundle's state is set to {@code RESOLVED}.
      24. + *
      25. A bundle event of type {@link BundleEvent#STOPPED} is fired.
      26. + *
      + * + * Preconditions + *
        + *
      • {@code getState()} in { {@code ACTIVE} }.
      • + *
      + * Postconditions, no exceptions thrown + *
        + *
      • Bundle autostart setting is modified unless the + * {@link #STOP_TRANSIENT} option was set.
      • + *
      • {@code getState()} not in { {@code ACTIVE}, {@code STOPPING} + * }.
      • + *
      • {@code BundleActivator.stop} has been called and did not throw an + * exception.
      • + *
      + * Postconditions, when an exception is thrown + *
        + *
      • Bundle autostart setting is modified unless the + * {@link #STOP_TRANSIENT} option was set.
      • + *
      + * + * @param options The options for stopping this bundle. See + * {@link #STOP_TRANSIENT}. The Framework must ignore unrecognized + * options. + * @throws BundleException BundleException types thrown by this method + * include: {@link BundleException#STATECHANGE_ERROR} and + * {@link BundleException#ACTIVATOR_ERROR}. + * @throws IllegalStateException If this bundle has been uninstalled or this + * bundle tries to change its own state. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[this,EXECUTE]}, and the Java Runtime + * Environment supports permissions. + * @since 1.4 + */ + void stop(int options) throws BundleException; + + /** + * Stops this bundle with no options. + * + *

      + * This method performs the same function as calling {@code stop(0)}. + * + * @throws BundleException BundleException types thrown by this method + * include: {@link BundleException#STATECHANGE_ERROR} and + * {@link BundleException#ACTIVATOR_ERROR}. + * @throws IllegalStateException If this bundle has been uninstalled or this + * bundle tries to change its own state. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[this,EXECUTE]}, and the Java Runtime + * Environment supports permissions. + * @see #start(int) + */ + void stop() throws BundleException; + + /** + * Updates this bundle from an {@code InputStream}. + * + *

      + * If the specified {@code InputStream} is {@code null}, the Framework must + * create the {@code InputStream} from which to read the updated bundle by + * interpreting, in an implementation dependent manner, this bundle's + * {@link Constants#BUNDLE_UPDATELOCATION Bundle-UpdateLocation} Manifest + * header, if present, or this bundle's original location. + * + *

      + * If this bundle's state is {@code ACTIVE}, it must be stopped before the + * update and started after the update successfully completes. + * + *

      + * If this bundle has exported any packages that are imported by another + * bundle, these packages must remain exported until the + * {@link FrameworkWiring#refreshBundles(java.util.Collection, FrameworkListener...) + * FrameworkWiring.refreshBundles} method has been has been called or the + * Framework is relaunched. + * + *

      + * The following steps are required to update a bundle: + *

        + *
      1. If this bundle's state is {@code UNINSTALLED} then an + * {@code IllegalStateException} is thrown.
      2. + *
      3. If this bundle's state is {@code ACTIVE}, {@code STARTING} or + * {@code STOPPING}, this bundle is stopped as described in the + * {@code Bundle.stop} method. If {@code Bundle.stop} throws an exception, + * the exception is rethrown terminating the update.
      4. + *
      5. The updated version of this bundle is read from the input stream and + * installed. If the Framework is unable to install the updated version of + * this bundle, the original version of this bundle must be restored and a + * {@code BundleException} must be thrown after completion of the remaining + * steps.
      6. + *
      7. This bundle's state is set to {@code INSTALLED}.
      8. + *
      9. If the updated version of this bundle was successfully installed, a + * bundle event of type {@link BundleEvent#UPDATED} is fired.
      10. + *
      11. If this bundle's state was originally {@code ACTIVE}, the updated + * bundle is started as described in the {@code Bundle.start} method. If + * {@code Bundle.start} throws an exception, a Framework event of type + * {@link FrameworkEvent#ERROR} is fired containing the exception.
      12. + *
      + * + * Preconditions + *
        + *
      • {@code getState()} not in { {@code UNINSTALLED} }.
      • + *
      + * Postconditions, no exceptions thrown + *
        + *
      • {@code getState()} in { {@code INSTALLED}, {@code RESOLVED}, + * {@code ACTIVE} }.
      • + *
      • This bundle has been updated.
      • + *
      + * Postconditions, when an exception is thrown + *
        + *
      • {@code getState()} in { {@code INSTALLED}, {@code RESOLVED}, + * {@code ACTIVE} }.
      • + *
      • Original bundle is still used; no update occurred.
      • + *
      + * + * @param input The {@code InputStream} from which to read the new bundle or + * {@code null} to indicate the Framework must create the input + * stream from this bundle's {@link Constants#BUNDLE_UPDATELOCATION + * Bundle-UpdateLocation} Manifest header, if present, or this + * bundle's original location. The input stream must always be closed + * when this method completes, even if an exception is thrown. + * @throws BundleException If this bundle could not be updated. + * BundleException types thrown by this method include: + * {@link BundleException#READ_ERROR}, + * {@link BundleException#DUPLICATE_BUNDLE_ERROR}, + * {@link BundleException#MANIFEST_ERROR}, + * {@link BundleException#NATIVECODE_ERROR}, + * {@link BundleException#RESOLVE_ERROR}, + * {@link BundleException#STATECHANGE_ERROR}, and + * {@link BundleException#ACTIVATOR_ERROR}. + * @throws IllegalStateException If this bundle has been uninstalled or this + * bundle tries to change its own state. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[this,LIFECYCLE]} for both the current + * bundle and the updated bundle, and the Java Runtime Environment + * supports permissions. + * @see #stop() + * @see #start() + */ + void update(InputStream input) throws BundleException; + + /** + * Updates this bundle. + * + *

      + * This method performs the same function as calling + * {@link #update(InputStream)} with a {@code null} InputStream. + * + * @throws BundleException If this bundle could not be updated. + * BundleException types thrown by this method include: + * {@link BundleException#READ_ERROR}, + * {@link BundleException#DUPLICATE_BUNDLE_ERROR}, + * {@link BundleException#MANIFEST_ERROR}, + * {@link BundleException#NATIVECODE_ERROR}, + * {@link BundleException#RESOLVE_ERROR}, + * {@link BundleException#STATECHANGE_ERROR}, and + * {@link BundleException#ACTIVATOR_ERROR}. + * @throws IllegalStateException If this bundle has been uninstalled or this + * bundle tries to change its own state. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[this,LIFECYCLE]} for both the current + * bundle and the updated bundle, and the Java Runtime Environment + * supports permissions. + * @see #update(InputStream) + */ + void update() throws BundleException; + + /** + * Uninstalls this bundle. + * + *

      + * This method causes the Framework to notify other bundles that this bundle + * is being uninstalled, and then puts this bundle into the + * {@code UNINSTALLED} state. The Framework must remove any resources + * related to this bundle that it is able to remove. + * + *

      + * If this bundle has exported any packages, the Framework must continue to + * make these packages available to their importing bundles until the + * {@link FrameworkWiring#refreshBundles(java.util.Collection, FrameworkListener...) + * FrameworkWiring.refreshBundles} method has been called or the Framework + * is relaunched. + * + *

      + * The following steps are required to uninstall a bundle: + *

        + *
      1. If this bundle's state is {@code UNINSTALLED} then an + * {@code IllegalStateException} is thrown.
      2. + *
      3. If this bundle's state is {@code ACTIVE}, {@code STARTING} or + * {@code STOPPING}, this bundle is stopped as described in the + * {@code Bundle.stop} method. If {@code Bundle.stop} throws an exception, a + * Framework event of type {@link FrameworkEvent#ERROR} is fired containing + * the exception.
      4. + *
      5. This bundle's state is set to {@code UNINSTALLED}.
      6. + *
      7. A bundle event of type {@link BundleEvent#UNINSTALLED} is fired.
      8. + *
      9. This bundle and any persistent storage area provided for this bundle + * by the Framework are removed.
      10. + *
      + * + * Preconditions + *
        + *
      • {@code getState()} not in { {@code UNINSTALLED} }.
      • + *
      + * Postconditions, no exceptions thrown + *
        + *
      • {@code getState()} in { {@code UNINSTALLED} }.
      • + *
      • This bundle has been uninstalled.
      • + *
      + * Postconditions, when an exception is thrown + *
        + *
      • {@code getState()} not in { {@code UNINSTALLED} }.
      • + *
      • This Bundle has not been uninstalled.
      • + *
      + * + * @throws BundleException If the uninstall failed. This can occur if + * another thread is attempting to change this bundle's state and + * does not complete in a timely manner. BundleException types + * thrown by this method include: + * {@link BundleException#STATECHANGE_ERROR} + * @throws IllegalStateException If this bundle has been uninstalled or this + * bundle tries to change its own state. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[this,LIFECYCLE]}, and the Java Runtime + * Environment supports permissions. + * @see #stop() + */ + void uninstall() throws BundleException; + + /** + * Returns this bundle's Manifest headers and values. This method returns + * all the Manifest headers and values from the main section of this + * bundle's Manifest file; that is, all lines prior to the first blank line. + * + *

      + * Manifest header names are case-insensitive. The methods of the returned + * {@code Dictionary} object must operate on header names in a + * case-insensitive manner. + * + * If a Manifest header value starts with "%", it must be + * localized according to the default locale. If no localization is found + * for a header value, the header value without the leading "%" is + * returned. + * + *

      + * For example, the following Manifest headers and values are included if + * they are present in the Manifest file: + * + *

      +	 *     Bundle-Name
      +	 *     Bundle-Vendor
      +	 *     Bundle-Version
      +	 *     Bundle-Description
      +	 *     Bundle-DocURL
      +	 *     Bundle-ContactAddress
      +	 * 
      + * + *

      + * This method must continue to return Manifest header information while + * this bundle is in the {@code UNINSTALLED} state. + * + * @return An unmodifiable {@code Dictionary} object containing this + * bundle's Manifest headers and values. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[this,METADATA]}, and the Java Runtime + * Environment supports permissions. + * @see Constants#BUNDLE_LOCALIZATION + */ + Dictionary getHeaders(); + + /** + * Returns this bundle's unique identifier. This bundle is assigned a unique + * identifier by the Framework when it was installed in the OSGi + * environment. + * + *

      + * A bundle's unique identifier has the following attributes: + *

        + *
      • Is unique and persistent.
      • + *
      • Is a {@code long}.
      • + *
      • Its value is not reused for another bundle, even after a bundle is + * uninstalled.
      • + *
      • Does not change while a bundle remains installed.
      • + *
      • Does not change when a bundle is updated.
      • + *
      + * + *

      + * This method must continue to return this bundle's unique identifier while + * this bundle is in the {@code UNINSTALLED} state. + * + * @return The unique identifier of this bundle. + */ + long getBundleId(); + + /** + * Returns this bundle's location identifier. + * + *

      + * The location identifier is the location passed to + * {@code BundleContext.installBundle} when a bundle is installed. The + * location identifier does not change while this bundle remains installed, + * even if this bundle is updated. + * + *

      + * This method must continue to return this bundle's location identifier + * while this bundle is in the {@code UNINSTALLED} state. + * + * @return The string representation of this bundle's location identifier. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[this,METADATA]}, and the Java Runtime + * Environment supports permissions. + */ + String getLocation(); + + /** + * Returns this bundle's {@code ServiceReference} list for all services it + * has registered or {@code null} if this bundle has no registered services. + * + *

      + * If the Java runtime supports permissions, a {@code ServiceReference} + * object to a service is included in the returned list only if the caller + * has the {@code ServicePermission} to get the service using at least one + * of the named classes the service was registered under. + * + *

      + * The list is valid at the time of the call to this method, however, as the + * Framework is a very dynamic environment, services can be modified or + * unregistered at anytime. + * + * @return An array of {@code ServiceReference} objects or {@code null}. + * @throws IllegalStateException If this bundle has been uninstalled. + * @see ServiceRegistration + * @see ServiceReference + * @see ServicePermission + */ + ServiceReference[] getRegisteredServices(); + + /** + * Returns this bundle's {@code ServiceReference} list for all services it + * is using or returns {@code null} if this bundle is not using any + * services. A bundle is considered to be using a service if it has any + * unreleased service objects. + * + *

      + * If the Java Runtime Environment supports permissions, a + * {@code ServiceReference} object to a service is included in the returned + * list only if the caller has the {@code ServicePermission} to get the + * service using at least one of the named classes the service was + * registered under. + *

      + * The list is valid at the time of the call to this method, however, as the + * Framework is a very dynamic environment, services can be modified or + * unregistered at anytime. + * + * @return An array of {@code ServiceReference} objects or {@code null}. + * @throws IllegalStateException If this bundle has been uninstalled. + * @see ServiceReference + * @see ServicePermission + */ + ServiceReference[] getServicesInUse(); + + /** + * Determines if this bundle has the specified permissions. + * + *

      + * If the Java Runtime Environment does not support permissions, this method + * always returns {@code true}. + *

      + * {@code permission} is of type {@code Object} to avoid referencing the + * {@code java.security.Permission} class directly. This is to allow the + * Framework to be implemented in Java environments which do not support + * permissions. + * + *

      + * If the Java Runtime Environment does support permissions, this bundle and + * all its resources including embedded JAR files, belong to the same + * {@code java.security.ProtectionDomain}; that is, they must share the same + * set of permissions. + * + * @param permission The permission to verify. + * @return {@code true} if this bundle has the specified permission or the + * permissions possessed by this bundle imply the specified + * permission; {@code false} if this bundle does not have the + * specified permission or {@code permission} is not an + * {@code instanceof} {@code java.security.Permission}. + * @throws IllegalStateException If this bundle has been uninstalled. + */ + boolean hasPermission(Object permission); + + /** + * Find the specified resource from this bundle's class loader. + * + * This bundle's class loader is called to search for the specified + * resource. If this bundle's state is {@code INSTALLED}, this method must + * attempt to resolve this bundle before attempting to get the specified + * resource. If this bundle cannot be resolved, then only this bundle must + * be searched for the specified resource. Imported packages cannot be + * searched when this bundle has not been resolved. If this bundle is a + * fragment bundle then {@code null} is returned. + *

      + * Note: Jar and zip files are not required to include directory entries. + * URLs to directory entries will not be returned if the bundle contents do + * not contain directory entries. + * + * @param name The name of the resource. See {@code ClassLoader.getResource} + * for a description of the format of a resource name. + * @return A URL to the named resource, or {@code null} if the resource + * could not be found or if this bundle is a fragment bundle or if + * the caller does not have the appropriate + * {@code AdminPermission[this,RESOURCE]}, and the Java Runtime + * Environment supports permissions. + * @throws IllegalStateException If this bundle has been uninstalled. + * @see #getEntry(String) + * @see #findEntries(String, String, boolean) + * @since 1.1 + */ + URL getResource(String name); + + /** + * Returns this bundle's Manifest headers and values localized to the + * specified locale. + * + *

      + * This method performs the same function as {@code Bundle.getHeaders()} + * except the manifest header values are localized to the specified locale. + * + *

      + * If a Manifest header value starts with "%", it must be + * localized according to the specified locale. If a locale is specified and + * cannot be found, then the header values must be returned using the + * default locale. Localizations are searched for in the following order: + * + *

      +	 *   bn + "_" + Ls + "_" + Cs + "_" + Vs
      +	 *   bn + "_" + Ls + "_" + Cs
      +	 *   bn + "_" + Ls
      +	 *   bn + "_" + Ld + "_" + Cd + "_" + Vd
      +	 *   bn + "_" + Ld + "_" + Cd
      +	 *   bn + "_" + Ld
      +	 *   bn
      +	 * 
      + * + * Where {@code bn} is this bundle's localization basename, {@code Ls}, + * {@code Cs} and {@code Vs} are the specified locale (language, country, + * variant) and {@code Ld}, {@code Cd} and {@code Vd} are the default locale + * (language, country, variant). + * + * If {@code null} is specified as the locale string, the header values must + * be localized using the default locale. If the empty string ("") + * is specified as the locale string, the header values must not be + * localized and the raw (unlocalized) header values, including any leading + * "%", must be returned. If no localization is found for a header + * value, the header value without the leading "%" is returned. + * + *

      + * This method must continue to return Manifest header information while + * this bundle is in the {@code UNINSTALLED} state, however the header + * values must only be available in the raw and default locale values. + * + * @param locale The locale name into which the header values are to be + * localized. If the specified locale is {@code null} then the locale + * returned by {@code java.util.Locale.getDefault} is used. If the + * specified locale is the empty string, this method will return the + * raw (unlocalized) manifest headers including any leading + * "%". + * @return An unmodifiable {@code Dictionary} object containing this + * bundle's Manifest headers and values. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[this,METADATA]}, and the Java Runtime + * Environment supports permissions. + * @see #getHeaders() + * @see Constants#BUNDLE_LOCALIZATION + * @since 1.3 + */ + Dictionary getHeaders(String locale); + + /** + * Returns the symbolic name of this bundle as specified by its + * {@code Bundle-SymbolicName} manifest header. The bundle symbolic name + * should be based on the reverse domain name naming convention like that + * used for java packages. + * + *

      + * This method must continue to return this bundle's symbolic name while + * this bundle is in the {@code UNINSTALLED} state. + * + * @return The symbolic name of this bundle or {@code null} if this bundle + * does not have a symbolic name. + * @since 1.3 + */ + String getSymbolicName(); + + /** + * Loads the specified class using this bundle's class loader. + * + *

      + * If this bundle is a fragment bundle then this method must throw a + * {@code ClassNotFoundException}. + * + *

      + * If this bundle's state is {@code INSTALLED}, this method must attempt to + * resolve this bundle before attempting to load the class. + * + *

      + * If this bundle cannot be resolved, a Framework event of type + * {@link FrameworkEvent#ERROR} is fired containing a + * {@code BundleException} with details of the reason this bundle could not + * be resolved. This method must then throw a {@code ClassNotFoundException}. + * + *

      + * If this bundle's state is {@code UNINSTALLED}, then an + * {@code IllegalStateException} is thrown. + * + * @param name The name of the class to load. + * @return The Class object for the requested class. + * @throws ClassNotFoundException If no such class can be found or if this + * bundle is a fragment bundle or if the caller does not have the + * appropriate {@code AdminPermission[this,CLASS]}, and the Java + * Runtime Environment supports permissions. + * @throws IllegalStateException If this bundle has been uninstalled. + * @since 1.3 + */ + Class loadClass(String name) throws ClassNotFoundException; + + /** + * Find the specified resources from this bundle's class loader. + * + * This bundle's class loader is called to search for the specified + * resources. If this bundle's state is {@code INSTALLED}, this method must + * attempt to resolve this bundle before attempting to get the specified + * resources. If this bundle cannot be resolved, then only this bundle must + * be searched for the specified resources. Imported packages cannot be + * searched when a bundle has not been resolved. If this bundle is a + * fragment bundle then {@code null} is returned. + *

      + * Note: Jar and zip files are not required to include directory entries. + * URLs to directory entries will not be returned if the bundle contents do + * not contain directory entries. + * + * @param name The name of the resource. See + * {@code ClassLoader.getResources} for a description of the format + * of a resource name. + * @return An enumeration of URLs to the named resources, or {@code null} if + * the resource could not be found or if this bundle is a fragment + * bundle or if the caller does not have the appropriate + * {@code AdminPermission[this,RESOURCE]}, and the Java Runtime + * Environment supports permissions. + * @throws IllegalStateException If this bundle has been uninstalled. + * @throws IOException If there is an I/O error. + * @since 1.3 + */ + Enumeration getResources(String name) throws IOException; + + /** + * Returns an Enumeration of all the paths ({@code String} objects) to + * entries within this bundle whose longest sub-path matches the specified + * path. This bundle's class loader is not used to search for entries. Only + * the contents of this bundle are searched. + *

      + * The specified path is always relative to the root of this bundle and may + * begin with a "/". A path value of "/" indicates the + * root of this bundle. + *

      + * Returned paths indicating subdirectory paths end with a "/". + * The returned paths are all relative to the root of this bundle and must + * not begin with "/". + *

      + * Note: Jar and zip files are not required to include directory entries. + * Paths to directory entries will not be returned if the bundle contents do + * not contain directory entries. + * + * @param path The path name for which to return entry paths. + * @return An Enumeration of the entry paths ({@code String} objects) or + * {@code null} if no entry could be found or if the caller does not + * have the appropriate {@code AdminPermission[this,RESOURCE]} and + * the Java Runtime Environment supports permissions. + * @throws IllegalStateException If this bundle has been uninstalled. + * @since 1.3 + */ + Enumeration getEntryPaths(String path); + + /** + * Returns a URL to the entry at the specified path in this bundle. This + * bundle's class loader is not used to search for the entry. Only the + * contents of this bundle are searched for the entry. + *

      + * The specified path is always relative to the root of this bundle and may + * begin with "/". A path value of "/" indicates the + * root of this bundle. + *

      + * Note: Jar and zip files are not required to include directory entries. + * URLs to directory entries will not be returned if the bundle contents do + * not contain directory entries. + * + * @param path The path name of the entry. + * @return A URL to the entry, or {@code null} if no entry could be found or + * if the caller does not have the appropriate + * {@code AdminPermission[this,RESOURCE]} and the Java Runtime + * Environment supports permissions. + * @throws IllegalStateException If this bundle has been uninstalled. + * @since 1.3 + */ + URL getEntry(String path); + + /** + * Returns the time when this bundle was last modified. A bundle is + * considered to be modified when it is installed, updated or uninstalled. + * + *

      + * The time value is the number of milliseconds since January 1, 1970, + * 00:00:00 UTC. + * + * @return The time when this bundle was last modified. + * @since 1.3 + */ + long getLastModified(); + + /** + * Returns entries in this bundle and its attached fragments. This bundle's + * class loader is not used to search for entries. Only the contents of this + * bundle and its attached fragments are searched for the specified entries. + * + * If this bundle's state is {@code INSTALLED}, this method must attempt to + * resolve this bundle before attempting to find entries. + * + *

      + * This method is intended to be used to obtain configuration, setup, + * localization and other information from this bundle. This method takes + * into account that the "contents" of this bundle can be extended + * with fragments. This "bundle space" is not a namespace with + * unique members; the same entry name can be present multiple times. This + * method therefore returns an enumeration of URL objects. These URLs can + * come from different JARs but have the same path name. This method can + * either return only entries in the specified path or recurse into + * subdirectories returning entries in the directory tree beginning at the + * specified path. Fragments can be attached after this bundle is resolved, + * possibly changing the set of URLs returned by this method. If this bundle + * is not resolved, only the entries in the JAR file of this bundle are + * returned. + *

      + * Examples: + * + *

      +	 * // List all XML files in the OSGI-INF directory and below
      +	 * Enumeration e = b.findEntries("OSGI-INF", "*.xml", true);
      +	 * 
      +	 * // Find a specific localization file
      +	 * Enumeration e = b.findEntries("OSGI-INF/l10n",
      +	 *     "bundle_nl_DU.properties", false);
      +	 * if (e.hasMoreElements())
      +	 *     return (URL) e.nextElement();
      +	 * 
      + * + *

      + * URLs for directory entries must have their path end with "/". + *

      + * Note: Jar and zip files are not required to include directory entries. + * URLs to directory entries will not be returned if the bundle contents do + * not contain directory entries. + * + * @param path The path name in which to look. The path is always relative + * to the root of this bundle and may begin with "/". A + * path value of "/" indicates the root of this bundle. + * @param filePattern The file name pattern for selecting entries in the + * specified path. The pattern is only matched against the last + * element of the entry path. If the entry is a directory then the + * trailing "/" is not used for pattern matching. Substring + * matching is supported, as specified in the Filter specification, + * using the wildcard character ("*"). If null is + * specified, this is equivalent to "*" and matches all + * files. + * @param recurse If {@code true}, recurse into subdirectories. Otherwise + * only return entries from the specified path. + * @return An enumeration of URL objects for each matching entry, or + * {@code null} if no matching entry could be found or if the caller + * does not have the appropriate + * {@code AdminPermission[this,RESOURCE]}, and the Java Runtime + * Environment supports permissions. The URLs are sorted such that + * entries from this bundle are returned first followed by the + * entries from attached fragments in attachment order. If this + * bundle is a fragment, then only matching entries in this fragment + * are returned. + * @throws IllegalStateException If this bundle has been uninstalled. + * @since 1.3 + */ + Enumeration findEntries(String path, String filePattern, boolean recurse); + + /** + * Returns this bundle's {@link BundleContext}. The returned + * {@code BundleContext} can be used by the caller to act on behalf of this + * bundle. + * + *

      + * If this bundle is not in the {@link #STARTING}, {@link #ACTIVE}, or + * {@link #STOPPING} states or this bundle is a fragment bundle, then this + * bundle has no valid {@code BundleContext}. This method will return + * {@code null} if this bundle has no valid {@code BundleContext}. + * + * @return A {@code BundleContext} for this bundle or {@code null} if this + * bundle has no valid {@code BundleContext}. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[this,CONTEXT]}, and the Java Runtime + * Environment supports permissions. + * @since 1.4 + */ + BundleContext getBundleContext(); + + /** + * Return the certificates for the signers of this bundle and the + * certificate chains for those signers. + * + * @param signersType If {@link #SIGNERS_ALL} is specified, then information + * on all signers of this bundle is returned. If + * {@link #SIGNERS_TRUSTED} is specified, then only information on + * the signers of this bundle trusted by the framework is returned. + * @return The {@code X509Certificate}s for the signers of this bundle and + * the {@code X509Certificate} chains for those signers. The keys of + * the {@code Map} are the {@code X509Certificate}s of the signers + * of this bundle. The value for a key is a {@code List} containing + * the {@code X509Certificate} chain for the signer. The first item + * in the {@code List} is the signer's {@code X509Certificate} which + * is then followed by the rest of the {@code X509Certificate} + * chain. The returned {@code Map} will be empty if there are no + * signers. The returned {@code Map} is the property of the caller + * who is free to modify it. + * @throws IllegalArgumentException If the specified {@code signersType} is + * not {@link #SIGNERS_ALL} or {@link #SIGNERS_TRUSTED}. + * @since 1.5 + */ + Map> getSignerCertificates(int signersType); + + /** + * Returns the version of this bundle as specified by its + * {@code Bundle-Version} manifest header. If this bundle does not have a + * specified version then {@link Version#emptyVersion} is returned. + * + *

      + * This method must continue to return this bundle's version while this + * bundle is in the {@code UNINSTALLED} state. + * + * @return The version of this bundle. + * @since 1.5 + */ + Version getVersion(); + + /** + * Adapt this bundle to the specified type. + * + *

      + * Adapting this bundle to the specified type may require certain checks, + * including security checks, to succeed. If a check does not succeed, then + * this bundle cannot be adapted and {@code null} is returned. + * + * @param The type to which this bundle is to be adapted. + * @param type Class object for the type to which this bundle is to be + * adapted. + * @return The object, of the specified type, to which this bundle has been + * adapted or {@code null} if this bundle cannot be adapted to the + * specified type. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdaptPermission[type,this,ADAPT]}, and the Java Runtime + * Environment supports permissions. + * @since 1.6 + */ + A adapt(Class type); + + /** + * Creates a {@code File} object for a file in the persistent storage area + * provided for this bundle by the Framework. This method will return + * {@code null} if the platform does not have file system support or this + * bundle is a fragment bundle. + * + *

      + * A {@code File} object for the base directory of the persistent storage + * area provided for this bundle by the Framework can be obtained by calling + * this method with an empty string as {@code filename}. + * + *

      + * If the Java Runtime Environment supports permissions, the Framework will + * ensure that this bundle has the {@code java.io.FilePermission} with + * actions {@code read},{@code write},{@code delete} for all files + * (recursively) in the persistent storage area provided for this bundle. + * + * @param filename A relative name to the file to be accessed. + * @return A {@code File} object that represents the requested file or + * {@code null} if the platform does not have file system support or + * this bundle is a fragment bundle. + * @throws IllegalStateException If this bundle has been uninstalled. + * @since 1.6 + */ + File getDataFile(String filename); +} diff --git a/framework/src/main/java/org/osgi/framework/BundleActivator.java b/framework/src/main/java/org/osgi/framework/BundleActivator.java new file mode 100644 index 00000000000..ab80909d0be --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/BundleActivator.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import org.osgi.annotation.versioning.ConsumerType; + +/** + * Customizes the starting and stopping of a bundle. + *

      + * {@code BundleActivator} is an interface that may be implemented when a bundle + * is started or stopped. The Framework can create instances of a bundle's + * {@code BundleActivator} as required. If an instance's + * {@code BundleActivator.start} method executes successfully, it is guaranteed + * that the same instance's {@code BundleActivator.stop} method will be called + * when the bundle is to be stopped. The Framework must not concurrently call a + * {@code BundleActivator} object. + * + *

      + * {@code BundleActivator} is specified through the {@code Bundle-Activator} + * Manifest header. A bundle can only specify a single {@code BundleActivator} + * in the Manifest file. Fragment bundles must not have a + * {@code BundleActivator}. The form of the Manifest header is: + * + *

      + * {@code Bundle-Activator:} class-name + * + *

      + * where class-name is a fully qualified Java classname. + *

      + * The specified {@code BundleActivator} class must have a public constructor + * that takes no parameters so that a {@code BundleActivator} object can be + * created by {@code Class.newInstance()}. + * + * @NotThreadSafe + * @author $Id: a9d91a8ae13157f49a6a55b0c7f25b63b6bd00bd $ + */ +@ConsumerType +public interface BundleActivator { + /** + * Called when this bundle is started so the Framework can perform the + * bundle-specific activities necessary to start this bundle. This method + * can be used to register services or to allocate any resources that this + * bundle needs. + * + *

      + * This method must complete and return to its caller in a timely manner. + * + * @param context The execution context of the bundle being started. + * @throws Exception If this method throws an exception, this bundle is + * marked as stopped and the Framework will remove this bundle's + * listeners, unregister all services registered by this bundle, and + * release all services used by this bundle. + */ + public void start(BundleContext context) throws Exception; + + /** + * Called when this bundle is stopped so the Framework can perform the + * bundle-specific activities necessary to stop the bundle. In general, this + * method should undo the work that the {@code BundleActivator.start} method + * started. There should be no active threads that were started by this + * bundle when this bundle returns. A stopped bundle must not call any + * Framework objects. + * + *

      + * This method must complete and return to its caller in a timely manner. + * + * @param context The execution context of the bundle being stopped. + * @throws Exception If this method throws an exception, the bundle is still + * marked as stopped, and the Framework will remove the bundle's + * listeners, unregister all services registered by the bundle, and + * release all services used by the bundle. + */ + public void stop(BundleContext context) throws Exception; +} diff --git a/framework/src/main/java/org/osgi/framework/BundleContext.java b/framework/src/main/java/org/osgi/framework/BundleContext.java new file mode 100644 index 00000000000..6ee6a980041 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/BundleContext.java @@ -0,0 +1,945 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2018). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import java.io.File; +import java.io.InputStream; +import java.util.Collection; +import java.util.Dictionary; +import org.osgi.annotation.versioning.ProviderType; + +/** + * A bundle's execution context within the Framework. The context is used to + * grant access to other methods so that this bundle can interact with the + * Framework. + * + *

      + * {@code BundleContext} methods allow a bundle to: + *

      + * + *

      + * A {@code BundleContext} object will be created for a bundle when the bundle + * is started. The {@code Bundle} object associated with a {@code BundleContext} + * object is called the context bundle. + * + *

      + * The {@code BundleContext} object will be passed to the + * {@link BundleActivator#start(BundleContext)} method during activation of the + * context bundle. The same {@code BundleContext} object will be passed to the + * {@link BundleActivator#stop(BundleContext)} method when the context bundle is + * stopped. A {@code BundleContext} object is generally for the private use of + * its associated bundle and is not meant to be shared with other bundles in the + * OSGi environment. + * + *

      + * The {@code BundleContext} object is only valid during the execution of its + * context bundle; that is, during the period from when the context bundle is in + * the {@code STARTING}, {@code STOPPING}, and {@code ACTIVE} bundle states. + * However, the {@code BundleContext} object becomes invalid after + * {@link BundleActivator#stop(BundleContext)} returns (if the bundle has a + * Bundle Activator). The {@code BundleContext} object becomes invalid before + * disposing of any remaining registered services and releasing any remaining + * services in use. Since those activities can result in other bundles being + * called (for example, {@link ServiceListener}s for + * {@link ServiceEvent#UNREGISTERING} events and {@link ServiceFactory}s for + * unget operations), those other bundles can observe the stopping bundle in the + * {@code STOPPING} state but with an invalid {@code BundleContext} object. If + * the {@code BundleContext} object is used after it has become invalid, an + * {@code IllegalStateException} must be thrown. The {@code BundleContext} + * object must never be reused after its context bundle is stopped. + * + *

      + * Two {@code BundleContext} objects are equal if they both refer to the same + * execution context of a bundle. The Framework is the only entity that can + * create {@code BundleContext} objects and they are only valid within the + * Framework that created them. + * + *

      + * A {@link Bundle} can be {@link Bundle#adapt(Class) adapted} to its + * {@code BundleContext}. In order for this to succeed, the caller must have the + * appropriate {@code AdminPermission[bundle,CONTEXT]} if the Java Runtime + * Environment supports permissions. + * + * @ThreadSafe + * @author $Id: 6c43d322b8ea2137c094ce10e1f33e9c54519dd6 $ + */ +@ProviderType +public interface BundleContext extends BundleReference { + + /** + * Returns the value of the specified property. If the key is not found in + * the Framework properties, the system properties are then searched. The + * method returns {@code null} if the property is not found. + * + *

      + * All bundles must have permission to read properties whose names start + * with "org.osgi.". + * + * @param key The name of the requested property. + * @return The value of the requested property, or {@code null} if the + * property is undefined. + * @throws SecurityException If the caller does not have the appropriate + * {@code PropertyPermission} to read the property, and the Java + * Runtime Environment supports permissions. + */ + String getProperty(String key); + + /** + * Returns the {@code Bundle} object associated with this + * {@code BundleContext}. This bundle is called the context bundle. + * + * @return The {@code Bundle} object associated with this + * {@code BundleContext}. + * @throws IllegalStateException If this BundleContext is no longer valid. + */ + @Override + Bundle getBundle(); + + /** + * Installs a bundle from the specified {@code InputStream} object. + * + *

      + * If the specified {@code InputStream} is {@code null}, the Framework must + * create the {@code InputStream} from which to read the bundle by + * interpreting, in an implementation dependent manner, the specified + * {@code location}. + * + *

      + * The specified {@code location} identifier will be used as the identity of + * the bundle. Every installed bundle is uniquely identified by its location + * identifier which is typically in the form of a URL. + * + *

      + * The following steps are required to install a bundle: + *

        + *
      1. If a bundle containing the same location identifier is already + * installed, the {@code Bundle} object for that bundle is returned.
      2. + *
      3. The bundle's content is read from the input stream. If this fails, a + * {@link BundleException} is thrown.
      4. + *
      5. The bundle's associated resources are allocated. The associated + * resources minimally consist of a unique identifier and a persistent + * storage area if the platform has file system support. If this step fails, + * a {@code BundleException} is thrown.
      6. + *
      7. The bundle's state is set to {@code INSTALLED}.
      8. + *
      9. A bundle event of type {@link BundleEvent#INSTALLED} is fired.
      10. + *
      11. The {@code Bundle} object for the newly or previously installed + * bundle is returned.
      12. + *
      + * + * Postconditions, no exceptions thrown + *
        + *
      • {@code getState()} in { {@code INSTALLED}, {@code RESOLVED} + * }.
      • + *
      • Bundle has a unique ID.
      • + *
      + * Postconditions, when an exception is thrown + *
        + *
      • Bundle is not installed. If there was an existing bundle for the + * specified location, then that bundle must still be in the state it was + * prior to calling this method.
      • + *
      + * + * @param location The location identifier of the bundle to install. + * @param input The {@code InputStream} object from which this bundle will + * be read or {@code null} to indicate the Framework must create the + * input stream from the specified location identifier. The input + * stream must always be closed when this method completes, even if + * an exception is thrown. + * @return The {@code Bundle} object of the installed bundle. + * @throws BundleException If the installation failed. BundleException types + * thrown by this method include: {@link BundleException#READ_ERROR} + * , {@link BundleException#DUPLICATE_BUNDLE_ERROR}, + * {@link BundleException#MANIFEST_ERROR}, and + * {@link BundleException#REJECTED_BY_HOOK}. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[installed bundle,LIFECYCLE]}, and the Java + * Runtime Environment supports permissions. + * @throws IllegalStateException If this BundleContext is no longer valid. + */ + Bundle installBundle(String location, InputStream input) throws BundleException; + + /** + * Installs a bundle from the specified {@code location} identifier. + * + *

      + * This method performs the same function as calling + * {@link #installBundle(String,InputStream)} with the specified + * {@code location} identifier and a {@code null} InputStream. + * + * @param location The location identifier of the bundle to install. + * @return The {@code Bundle} object of the installed bundle. + * @throws BundleException If the installation failed. BundleException types + * thrown by this method include: {@link BundleException#READ_ERROR} + * , {@link BundleException#DUPLICATE_BUNDLE_ERROR}, + * {@link BundleException#MANIFEST_ERROR}, and + * {@link BundleException#REJECTED_BY_HOOK}. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[installed bundle,LIFECYCLE]}, and the Java + * Runtime Environment supports permissions. + * @throws IllegalStateException If this BundleContext is no longer valid. + * @see #installBundle(String, InputStream) + */ + Bundle installBundle(String location) throws BundleException; + + /** + * Returns the bundle with the specified identifier. + * + * @param id The identifier of the bundle to retrieve. + * @return A {@code Bundle} object or {@code null} if the identifier does + * not match any installed bundle. + */ + Bundle getBundle(long id); + + /** + * Returns a list of all installed bundles. + *

      + * This method returns a list of all bundles installed in the OSGi + * environment at the time of the call to this method. However, since the + * Framework is a very dynamic environment, bundles can be installed or + * uninstalled at anytime. + * + * @return An array of {@code Bundle} objects, one object per installed + * bundle. + */ + Bundle[] getBundles(); + + /** + * Adds the specified {@code ServiceListener} object with the specified + * {@code filter} to the context bundle's list of listeners. See + * {@link Filter} for a description of the filter syntax. + * {@code ServiceListener} objects are notified when a service has a + * lifecycle state change. + * + *

      + * If the context bundle's list of listeners already contains a listener + * {@code l} such that {@code (l==listener)}, then this method replaces that + * listener's filter (which may be {@code null}) with the specified one + * (which may be {@code null}). + * + *

      + * The listener is called if the filter criteria is met. To filter based + * upon the class of the service, the filter should reference the + * {@link Constants#OBJECTCLASS} property. If {@code filter} is {@code null} + * , all services are considered to match the filter. + * + *

      + * When using a {@code filter}, it is possible that the {@code ServiceEvent} + * s for the complete lifecycle of a service will not be delivered to the + * listener. For example, if the {@code filter} only matches when the + * property {@code x} has the value {@code 1}, the listener will not be + * called if the service is registered with the property {@code x} not set + * to the value {@code 1}. Subsequently, when the service is modified + * setting property {@code x} to the value {@code 1}, the filter will match + * and the listener will be called with a {@code ServiceEvent} of type + * {@code MODIFIED}. Thus, the listener will not be called with a + * {@code ServiceEvent} of type {@code REGISTERED}. + * + *

      + * If the Java Runtime Environment supports permissions, the + * {@code ServiceListener} object will be notified of a service event only + * if the bundle that is registering it has the {@code ServicePermission} to + * get the service using at least one of the named classes the service was + * registered under. + * + * @param listener The {@code ServiceListener} object to be added. + * @param filter The filter criteria. + * @throws InvalidSyntaxException If {@code filter} contains an invalid + * filter string that cannot be parsed. + * @throws IllegalStateException If this BundleContext is no longer valid. + * @see ServiceEvent + * @see ServiceListener + * @see ServicePermission + */ + void addServiceListener(ServiceListener listener, String filter) throws InvalidSyntaxException; + + /** + * Adds the specified {@code ServiceListener} object to the context bundle's + * list of listeners. + * + *

      + * This method is the same as calling + * {@code BundleContext.addServiceListener(ServiceListener listener, + * String filter)} with {@code filter} set to {@code null}. + * + * @param listener The {@code ServiceListener} object to be added. + * @throws IllegalStateException If this BundleContext is no longer valid. + * @see #addServiceListener(ServiceListener, String) + */ + void addServiceListener(ServiceListener listener); + + /** + * Removes the specified {@code ServiceListener} object from the context + * bundle's list of listeners. + * + *

      + * If {@code listener} is not contained in this context bundle's list of + * listeners, this method does nothing. + * + * @param listener The {@code ServiceListener} to be removed. + * @throws IllegalStateException If this BundleContext is no longer valid. + */ + void removeServiceListener(ServiceListener listener); + + /** + * Adds the specified {@code BundleListener} object to the context bundle's + * list of listeners if not already present. BundleListener objects are + * notified when a bundle has a lifecycle state change. + * + *

      + * If the context bundle's list of listeners already contains a listener + * {@code l} such that {@code (l==listener)}, this method does nothing. + * + * @param listener The {@code BundleListener} to be added. + * @throws IllegalStateException If this BundleContext is no longer valid. + * @throws SecurityException If listener is a + * {@code SynchronousBundleListener} and the caller does not have + * the appropriate {@code AdminPermission[context bundle,LISTENER]}, + * and the Java Runtime Environment supports permissions. + * @see BundleEvent + * @see BundleListener + */ + void addBundleListener(BundleListener listener); + + /** + * Removes the specified {@code BundleListener} object from the context + * bundle's list of listeners. + * + *

      + * If {@code listener} is not contained in the context bundle's list of + * listeners, this method does nothing. + * + * @param listener The {@code BundleListener} object to be removed. + * @throws IllegalStateException If this BundleContext is no longer valid. + * @throws SecurityException If listener is a + * {@code SynchronousBundleListener} and the caller does not have + * the appropriate {@code AdminPermission[context bundle,LISTENER]}, + * and the Java Runtime Environment supports permissions. + */ + void removeBundleListener(BundleListener listener); + + /** + * Adds the specified {@code FrameworkListener} object to the context + * bundle's list of listeners if not already present. FrameworkListeners are + * notified of general Framework events. + * + *

      + * If the context bundle's list of listeners already contains a listener + * {@code l} such that {@code (l==listener)}, this method does nothing. + * + * @param listener The {@code FrameworkListener} object to be added. + * @throws IllegalStateException If this BundleContext is no longer valid. + * @see FrameworkEvent + * @see FrameworkListener + */ + void addFrameworkListener(FrameworkListener listener); + + /** + * Removes the specified {@code FrameworkListener} object from the context + * bundle's list of listeners. + * + *

      + * If {@code listener} is not contained in the context bundle's list of + * listeners, this method does nothing. + * + * @param listener The {@code FrameworkListener} object to be removed. + * @throws IllegalStateException If this BundleContext is no longer valid. + */ + void removeFrameworkListener(FrameworkListener listener); + + /** + * Registers the specified service object with the specified properties + * under the specified class names into the Framework. A + * {@code ServiceRegistration} object is returned. The + * {@code ServiceRegistration} object is for the private use of the bundle + * registering the service and should not be shared with other bundles. The + * registering bundle is defined to be the context bundle. Other bundles can + * locate the service by using one of the + * {@link #getServiceReferences(Class, String)}, + * {@link #getServiceReferences(String, String)}, + * {@link #getServiceReference(Class)} or + * {@link #getServiceReference(String)} methods. + * + *

      + * A bundle can register a service object that implements the + * {@link ServiceFactory} interface to have more flexibility in providing + * service objects to other bundles. + * + *

      + * The following steps are required to register a service: + *

        + *
      1. If {@code service} does not implement {@code ServiceFactory}, an + * {@code IllegalArgumentException} is thrown if {@code service} is not an + * {@code instanceof} all the specified class names.
      2. + *
      3. The Framework adds the following service properties to the service + * properties from the specified {@code Dictionary} (which may be + * {@code null}): + *
          + *
        • A property named {@link Constants#SERVICE_ID} identifying the + * registration number of the service
        • + *
        • A property named {@link Constants#OBJECTCLASS} containing all the + * specified classes.
        • + *
        • A property named {@link Constants#SERVICE_SCOPE} identifying the + * scope of the service.
        • + *
        • A property named {@link Constants#SERVICE_BUNDLEID} identifying the + * context bundle.
        • + *
        + * Properties with these names in the specified {@code Dictionary} will be + * ignored.
      4. + *
      5. The service is added to the Framework service registry and may now be + * used by other bundles.
      6. + *
      7. A service event of type {@link ServiceEvent#REGISTERED} is fired.
      8. + *
      9. A {@code ServiceRegistration} object for this registration is + * returned.
      10. + *
      + * + * @param clazzes The class names under which the service can be located. + * The class names in this array will be stored in the service's + * properties under the key {@link Constants#OBJECTCLASS}. + * @param service The service object or an object implementing + * {@code ServiceFactory}. + * @param properties The properties for this service. The keys in the + * properties object must all be {@code String} objects. See + * {@link Constants} for a list of standard service property keys. + * Changes should not be made to this object after calling this + * method. To update the service's properties the + * {@link ServiceRegistration#setProperties(Dictionary)} method must + * be called. The set of properties may be {@code null} if the + * service has no properties. + * @return A {@code ServiceRegistration} object for use by the bundle + * registering the service to update the service's properties or to + * unregister the service. + * @throws IllegalArgumentException If one of the following is true: + *
        + *
      • {@code service} is {@code null}.
      • {@code service} does + * not implement {@code ServiceFactory} and is not an instance of + * all the specified classes.
      • {@code properties} contains + * case variants of the same key name.
      • + *
      + * @throws SecurityException If the caller does not have the + * {@code ServicePermission} to register the service for all the + * named classes and the Java Runtime Environment supports + * permissions. + * @throws IllegalStateException If this BundleContext is no longer valid. + * @see ServiceRegistration + * @see PrototypeServiceFactory + * @see ServiceFactory + */ + ServiceRegistration registerService(String[] clazzes, Object service, Dictionary properties); + + /** + * Registers the specified service object with the specified properties + * under the specified class name with the Framework. + * + *

      + * This method is otherwise identical to + * {@link #registerService(String[], Object, Dictionary)} and is provided as + * a convenience when {@code service} will only be registered under a single + * class name. Note that even in this case the value of the service's + * {@link Constants#OBJECTCLASS} property will be an array of string, rather + * than just a single string. + * + * @param clazz The class name under which the service can be located. + * @param service The service object or an object implementing + * {@code ServiceFactory}. + * @param properties The properties for this service. + * @return A {@code ServiceRegistration} object for use by the bundle + * registering the service to update the service's properties or to + * unregister the service. + * @throws IllegalStateException If this BundleContext is no longer valid. + * @see #registerService(String[], Object, Dictionary) + */ + ServiceRegistration registerService(String clazz, Object service, Dictionary properties); + + /** + * Registers the specified service object with the specified properties + * under the name of the specified class with the Framework. + * + *

      + * This method is otherwise identical to + * {@link #registerService(String, Object, Dictionary)} and is provided to + * return a type safe {@code ServiceRegistration}. + * + * @param Type of Service. + * @param clazz The class under whose name the service can be located. + * @param service The service object or an object implementing + * {@code ServiceFactory}. + * @param properties The properties for this service. + * @return A {@code ServiceRegistration} object for use by the bundle + * registering the service to update the service's properties or to + * unregister the service. + * @throws IllegalStateException If this BundleContext is no longer valid. + * @see #registerService(String, Object, Dictionary) + * @since 1.6 + */ + ServiceRegistration registerService(Class clazz, S service, Dictionary properties); + + /** + * Registers the specified service factory object with the specified + * properties under the name of the specified class with the Framework. + * + *

      + * This method is otherwise identical to + * {@link #registerService(Class, Object, Dictionary)} and is provided to + * return a type safe {@code ServiceRegistration} when registering a + * {@link ServiceFactory}. + * + * @param Type of Service. + * @param clazz The class under whose name the service can be located. + * @param factory The {@code ServiceFactory} object. + * @param properties The properties for this service. + * @return A {@code ServiceRegistration} object for use by the bundle + * registering the service to update the service's properties or to + * unregister the service. + * @throws IllegalStateException If this BundleContext is no longer valid. + * @see #registerService(Class, Object, Dictionary) + * @since 1.8 + */ + ServiceRegistration registerService(Class clazz, ServiceFactory factory, Dictionary properties); + + /** + * Returns an array of {@code ServiceReference} objects. The returned array + * of {@code ServiceReference} objects contains services that were + * registered under the specified class, match the specified filter + * expression, and the packages for the class names under which the services + * were registered match the context bundle's packages as defined in + * {@link ServiceReference#isAssignableTo(Bundle, String)}. + * + *

      + * The list is valid at the time of the call to this method. However since + * the Framework is a very dynamic environment, services can be modified or + * unregistered at any time. + * + *

      + * The specified {@code filter} expression is used to select the registered + * services whose service properties contain keys and values which satisfy + * the filter expression. See {@link Filter} for a description of the filter + * syntax. If the specified {@code filter} is {@code null}, all registered + * services are considered to match the filter. If the specified + * {@code filter} expression cannot be parsed, an + * {@link InvalidSyntaxException} will be thrown with a human readable + * message where the filter became unparsable. + * + *

      + * The result is an array of {@code ServiceReference} objects for all + * services that meet all of the following conditions: + *

        + *
      • If the specified class name, {@code clazz}, is not {@code null}, the + * service must have been registered with the specified class name. The + * complete list of class names with which a service was registered is + * available from the service's {@link Constants#OBJECTCLASS objectClass} + * property.
      • + *
      • If the specified {@code filter} is not {@code null}, the filter + * expression must match the service.
      • + *
      • If the Java Runtime Environment supports permissions, the caller must + * have {@code ServicePermission} with the {@code GET} action for at least + * one of the class names under which the service was registered.
      • + *
      • For each class name with which the service was registered, calling + * {@link ServiceReference#isAssignableTo(Bundle, String)} with the context + * bundle and the class name on the service's {@code ServiceReference} + * object must return {@code true}
      • + *
      + * + * @param clazz The class name with which the service was registered or + * {@code null} for all services. + * @param filter The filter expression or {@code null} for all services. + * @return An array of {@code ServiceReference} objects or {@code null} if + * no services are registered which satisfy the search. + * @throws InvalidSyntaxException If the specified {@code filter} contains + * an invalid filter expression that cannot be parsed. + * @throws IllegalStateException If this BundleContext is no longer valid. + */ + ServiceReference[] getServiceReferences(String clazz, String filter) throws InvalidSyntaxException; + + /** + * Returns an array of {@code ServiceReference} objects. The returned array + * of {@code ServiceReference} objects contains services that were + * registered under the specified class and match the specified filter + * expression. + * + *

      + * The list is valid at the time of the call to this method. However since + * the Framework is a very dynamic environment, services can be modified or + * unregistered at any time. + * + *

      + * The specified {@code filter} expression is used to select the registered + * services whose service properties contain keys and values which satisfy + * the filter expression. See {@link Filter} for a description of the filter + * syntax. If the specified {@code filter} is {@code null}, all registered + * services are considered to match the filter. If the specified + * {@code filter} expression cannot be parsed, an + * {@link InvalidSyntaxException} will be thrown with a human readable + * message where the filter became unparsable. + * + *

      + * The result is an array of {@code ServiceReference} objects for all + * services that meet all of the following conditions: + *

        + *
      • If the specified class name, {@code clazz}, is not {@code null}, the + * service must have been registered with the specified class name. The + * complete list of class names with which a service was registered is + * available from the service's {@link Constants#OBJECTCLASS objectClass} + * property.
      • + *
      • If the specified {@code filter} is not {@code null}, the filter + * expression must match the service.
      • + *
      • If the Java Runtime Environment supports permissions, the caller must + * have {@code ServicePermission} with the {@code GET} action for at least + * one of the class names under which the service was registered.
      • + *
      + * + * @param clazz The class name with which the service was registered or + * {@code null} for all services. + * @param filter The filter expression or {@code null} for all services. + * @return An array of {@code ServiceReference} objects or {@code null} if + * no services are registered which satisfy the search. + * @throws InvalidSyntaxException If the specified {@code filter} contains + * an invalid filter expression that cannot be parsed. + * @throws IllegalStateException If this BundleContext is no longer valid. + * @since 1.3 + */ + ServiceReference[] getAllServiceReferences(String clazz, String filter) throws InvalidSyntaxException; + + /** + * Returns a {@code ServiceReference} object for a service that implements + * and was registered under the specified class. + * + *

      + * The returned {@code ServiceReference} object is valid at the time of the + * call to this method. However as the Framework is a very dynamic + * environment, services can be modified or unregistered at any time. + * + *

      + * This method is the same as calling + * {@link #getServiceReferences(String, String)} with a {@code null} filter + * expression and then finding the reference with the highest priority. It + * is provided as a convenience for when the caller is interested in any + * service that implements the specified class. + *

      + * If multiple such services exist, the service with the highest priority is + * selected. This priority is defined as the service reference with the + * highest ranking (as specified in its {@link Constants#SERVICE_RANKING} + * property) is returned. + *

      + * If there is a tie in ranking, the service with the lowest service id (as + * specified in its {@link Constants#SERVICE_ID} property); that is, the + * service that was registered first is returned. + * + * @param clazz The class name with which the service was registered. + * @return A {@code ServiceReference} object, or {@code null} if no services + * are registered which implement the named class. + * @throws IllegalStateException If this BundleContext is no longer valid. + * @see #getServiceReferences(String, String) + */ + ServiceReference getServiceReference(String clazz); + + /** + * Returns a {@code ServiceReference} object for a service that implements + * and was registered under the name of the specified class. + * + *

      + * The returned {@code ServiceReference} object is valid at the time of the + * call to this method. However as the Framework is a very dynamic + * environment, services can be modified or unregistered at any time. + * + *

      + * This method is the same as calling + * {@link #getServiceReferences(Class, String)} with a {@code null} filter + * expression. It is provided as a convenience for when the caller is + * interested in any service that implements the specified class. + *

      + * If multiple such services exist, the service with the highest ranking (as + * specified in its {@link Constants#SERVICE_RANKING} property) is returned. + *

      + * If there is a tie in ranking, the service with the lowest service id (as + * specified in its {@link Constants#SERVICE_ID} property); that is, the + * service that was registered first is returned. + * + * @param Type of Service. + * @param clazz The class under whose name the service was registered. Must + * not be {@code null}. + * @return A {@code ServiceReference} object, or {@code null} if no services + * are registered which implement the specified class. + * @throws IllegalStateException If this BundleContext is no longer valid. + * @see #getServiceReferences(Class, String) + * @since 1.6 + */ + ServiceReference getServiceReference(Class clazz); + + /** + * Returns a collection of {@code ServiceReference} objects. The returned + * collection of {@code ServiceReference} objects contains services that + * were registered under the name of the specified class, match the + * specified filter expression, and the packages for the class names under + * which the services were registered match the context bundle's packages as + * defined in {@link ServiceReference#isAssignableTo(Bundle, String)}. + * + *

      + * The collection is valid at the time of the call to this method. However + * since the Framework is a very dynamic environment, services can be + * modified or unregistered at any time. + * + *

      + * The specified {@code filter} expression is used to select the registered + * services whose service properties contain keys and values which satisfy + * the filter expression. See {@link Filter} for a description of the filter + * syntax. If the specified {@code filter} is {@code null}, all registered + * services are considered to match the filter. If the specified + * {@code filter} expression cannot be parsed, an + * {@link InvalidSyntaxException} will be thrown with a human readable + * message where the filter became unparsable. + * + *

      + * The result is a collection of {@code ServiceReference} objects for all + * services that meet all of the following conditions: + *

        + *
      • The service must have been registered with the name of the specified + * class. The complete list of class names with which a service was + * registered is available from the service's {@link Constants#OBJECTCLASS + * objectClass} property.
      • + *
      • If the specified {@code filter} is not {@code null}, the filter + * expression must match the service.
      • + *
      • If the Java Runtime Environment supports permissions, the caller must + * have {@code ServicePermission} with the {@code GET} action for at least + * one of the class names under which the service was registered.
      • + *
      • For each class name with which the service was registered, calling + * {@link ServiceReference#isAssignableTo(Bundle, String)} with the context + * bundle and the class name on the service's {@code ServiceReference} + * object must return {@code true}
      • + *
      + * + * @param Type of Service + * @param clazz The class under whose name the service was registered. Must + * not be {@code null}. + * @param filter The filter expression or {@code null} for all services. + * @return A collection of {@code ServiceReference} objects. May be empty if + * no services are registered which satisfy the search. + * @throws InvalidSyntaxException If the specified {@code filter} contains + * an invalid filter expression that cannot be parsed. + * @throws IllegalStateException If this BundleContext is no longer valid. + * @since 1.6 + */ + Collection> getServiceReferences(Class clazz, String filter) throws InvalidSyntaxException; + + /** + * Returns the service object for the service referenced by the specified + * {@code ServiceReference} object. + * + *

      + * A bundle's use of a service object obtained from this method is tracked + * by the bundle's use count of that service. Each time the service object + * is returned by {@link #getService(ServiceReference)} the context bundle's + * use count for the service is incremented by one. Each time the service + * object is released by {@link #ungetService(ServiceReference)} the context + * bundle's use count for the service is decremented by one. + * + *

      + * When a bundle's use count for the service drops to zero, the bundle + * should no longer use the service object. + * + *

      + * This method will always return {@code null} when the service associated + * with the specified {@code reference} has been unregistered. + * + *

      + * The following steps are required to get the service object: + *

        + *
      1. If the service has been unregistered, {@code null} is returned.
      2. + *
      3. If the context bundle's use count for the service is currently zero + * and the service has {@link Constants#SCOPE_BUNDLE bundle} or + * {@link Constants#SCOPE_PROTOTYPE prototype} scope, the + * {@link ServiceFactory#getService(Bundle, ServiceRegistration)} method is + * called to supply the service object for the context bundle. If the + * service object returned by the {@code ServiceFactory} object is + * {@code null}, not an {@code instanceof} all the classes named when the + * service was registered or the {@code ServiceFactory} object throws an + * exception or will be recursively called for the context bundle, + * {@code null} is returned and a Framework event of type + * {@link FrameworkEvent#ERROR} containing a {@link ServiceException} + * describing the error is fired. The supplied service object is cached by + * the Framework. While the context bundle's use count for the service is + * greater than zero, subsequent calls to get the service object for the + * context bundle will return the cached service object.
      4. + *
      5. The context bundle's use count for the service is incremented by one. + *
      6. + *
      7. The service object for the service is returned.
      8. + *
      + * + * @param Type of Service. + * @param reference A reference to the service. + * @return A service object for the service associated with + * {@code reference} or {@code null} if the service is not + * registered, the service object returned by a + * {@code ServiceFactory} does not implement the classes under which + * it was registered or the {@code ServiceFactory} threw an + * exception. + * @throws SecurityException If the caller does not have the + * {@code ServicePermission} to get the service using at least one + * of the named classes the service was registered under and the + * Java Runtime Environment supports permissions. + * @throws IllegalStateException If this BundleContext is no longer valid. + * @throws IllegalArgumentException If the specified + * {@code ServiceReference} was not created by the same framework + * instance as this {@code BundleContext}. + * @see #ungetService(ServiceReference) + * @see ServiceFactory + */ + S getService(ServiceReference reference); + + /** + * Releases the service object for the service referenced by the specified + * {@code ServiceReference} object. If the context bundle's use count for + * the service is zero, this method returns {@code false}. Otherwise, the + * context bundle's use count for the service is decremented by one. + * + *

      + * The service object must no longer be used and all references to it should + * be destroyed when a bundle's use count for the service drops to zero. + * + *

      + * The following steps are required to release the service object: + *

        + *
      1. If the context bundle's use count for the service is zero or the + * service has been unregistered, {@code false} is returned.
      2. + *
      3. The context bundle's use count for the service is decremented by one. + *
      4. + *
      5. If the context bundle's use count for the service is now zero and the + * service has {@link Constants#SCOPE_BUNDLE bundle} or + * {@link Constants#SCOPE_PROTOTYPE prototype} scope, the + * {@link ServiceFactory#ungetService(Bundle, ServiceRegistration, Object)} + * method is called to release the service object for the context bundle.
      6. + *
      7. {@code true} is returned.
      8. + *
      + * + * @param reference A reference to the service to be released. + * @return {@code false} if the context bundle's use count for the service + * is zero or if the service has been unregistered; {@code true} + * otherwise. + * @throws IllegalStateException If this BundleContext is no longer valid. + * @throws IllegalArgumentException If the specified + * {@code ServiceReference} was not created by the same framework + * instance as this {@code BundleContext}. + * @see #getService(ServiceReference) + * @see ServiceFactory + */ + boolean ungetService(ServiceReference reference); + + /** + * Returns the {@link ServiceObjects} object for the service referenced by + * the specified {@code ServiceReference} object. + * + *

      + * The {@link ServiceObjects} object can be used to obtain multiple service + * objects for services with {@link Constants#SCOPE_PROTOTYPE prototype} + * scope. + * + *

      + * For services with {@link Constants#SCOPE_SINGLETON singleton} or + * {@link Constants#SCOPE_BUNDLE bundle} scope, the + * {@link ServiceObjects#getService()} method behaves the same as the + * {@link #getService(ServiceReference)} method and the + * {@link ServiceObjects#ungetService(Object)} method behaves the same as + * the {@link #ungetService(ServiceReference)} method. That is, only one, + * use-counted service object is available from the {@link ServiceObjects} + * object. + * + *

      + * This method will always return {@code null} when the service associated + * with the specified {@code reference} has been unregistered. + * + * @param Type of Service. + * @param reference A reference to the service. + * @return A {@link ServiceObjects} object for the service associated with + * the specified {@code reference} or {@code null} if the service is + * not registered. + * @throws SecurityException If the caller does not have the + * {@code ServicePermission} to get the service using at least one + * of the named classes the service was registered under and the + * Java Runtime Environment supports permissions. + * @throws IllegalStateException If this BundleContext is no longer valid. + * @throws IllegalArgumentException If the specified + * {@code ServiceReference} was not created by the same framework + * instance as this {@code BundleContext}. + * @see PrototypeServiceFactory + * @since 1.8 + */ + ServiceObjects getServiceObjects(ServiceReference reference); + + /** + * Creates a {@code File} object for a file in the persistent storage area + * provided for the bundle by the Framework. This method will return + * {@code null} if the platform does not have file system support. + * + *

      + * A {@code File} object for the base directory of the persistent storage + * area provided for the context bundle by the Framework can be obtained by + * calling this method with an empty string as {@code filename}. + * + *

      + * If the Java Runtime Environment supports permissions, the Framework will + * ensure that the bundle has the {@code java.io.FilePermission} with + * actions {@code read},{@code write},{@code delete} for all files + * (recursively) in the persistent storage area provided for the context + * bundle. + * + * @param filename A relative name to the file to be accessed. + * @return A {@code File} object that represents the requested file or + * {@code null} if the platform does not have file system support. + * @throws IllegalStateException If this BundleContext is no longer valid. + */ + File getDataFile(String filename); + + /** + * Creates a {@code Filter} object. This {@code Filter} object may be used + * to match a {@code ServiceReference} object or a {@code Dictionary} + * object. + * + *

      + * If the filter cannot be parsed, an {@link InvalidSyntaxException} will be + * thrown with a human readable message where the filter became unparsable. + * + * @param filter The filter string. + * @return A {@code Filter} object encapsulating the filter string. + * @throws InvalidSyntaxException If {@code filter} contains an invalid + * filter string that cannot be parsed. + * @throws NullPointerException If {@code filter} is null. + * @throws IllegalStateException If this BundleContext is no longer valid. + * @see "Framework specification for a description of the filter string syntax." + * @see FrameworkUtil#createFilter(String) + * @since 1.1 + */ + Filter createFilter(String filter) throws InvalidSyntaxException; + + /** + * Returns the bundle with the specified location. + * + * @param location The location of the bundle to retrieve. + * @return A {@code Bundle} object or {@code null} if the location does not + * match any installed bundle. + * @since 1.6 + */ + Bundle getBundle(String location); +} diff --git a/framework/src/main/java/org/osgi/framework/BundleEvent.java b/framework/src/main/java/org/osgi/framework/BundleEvent.java new file mode 100644 index 00000000000..5bc861480e1 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/BundleEvent.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import java.util.EventObject; + +/** + * An event from the Framework describing a bundle lifecycle change. + *

      + * {@code BundleEvent} objects are delivered to + * {@code SynchronousBundleListener}s and {@code BundleListener}s when a change + * occurs in a bundle's lifecycle. A type code is used to identify the event + * type for future extendability. + * + *

      + * OSGi Alliance reserves the right to extend the set of types. + * + * @Immutable + * @see BundleListener + * @see SynchronousBundleListener + * @author $Id: cd49848213816c4f76541b70be12ac3af7900972 $ + */ + +public class BundleEvent extends EventObject { + static final long serialVersionUID = 4080640865971756012L; + /** + * Bundle that had a change occur in its lifecycle. + */ + private final Bundle bundle; + + /** + * Type of bundle lifecycle change. + */ + private final int type; + + /** + * The bundle has been installed. + * + * @see BundleContext#installBundle(String) + */ + public final static int INSTALLED = 0x00000001; + + /** + * The bundle has been started. + *

      + * The bundle's {@link BundleActivator#start(BundleContext) BundleActivator + * start} method has been executed if the bundle has a bundle activator + * class. + * + * @see Bundle#start() + */ + public final static int STARTED = 0x00000002; + + /** + * The bundle has been stopped. + *

      + * The bundle's {@link BundleActivator#stop(BundleContext) BundleActivator + * stop} method has been executed if the bundle has a bundle activator + * class. + * + * @see Bundle#stop() + */ + public final static int STOPPED = 0x00000004; + + /** + * The bundle has been updated. + * + * @see Bundle#update() + */ + public final static int UPDATED = 0x00000008; + + /** + * The bundle has been uninstalled. + * + * @see Bundle#uninstall() + */ + public final static int UNINSTALLED = 0x00000010; + + /** + * The bundle has been resolved. + * + * @see Bundle#RESOLVED + * @since 1.3 + */ + public final static int RESOLVED = 0x00000020; + + /** + * The bundle has been unresolved. + * + * @see Bundle#INSTALLED + * @since 1.3 + */ + public final static int UNRESOLVED = 0x00000040; + + /** + * The bundle is about to be activated. + *

      + * The bundle's {@link BundleActivator#start(BundleContext) BundleActivator + * start} method is about to be called if the bundle has a bundle activator + * class. This event is only delivered to {@link SynchronousBundleListener} + * s. It is not delivered to {@code BundleListener}s. + * + * @see Bundle#start() + * @since 1.3 + */ + public final static int STARTING = 0x00000080; + + /** + * The bundle is about to deactivated. + *

      + * The bundle's {@link BundleActivator#stop(BundleContext) BundleActivator + * stop} method is about to be called if the bundle has a bundle activator + * class. This event is only delivered to {@link SynchronousBundleListener} + * s. It is not delivered to {@code BundleListener}s. + * + * @see Bundle#stop() + * @since 1.3 + */ + public final static int STOPPING = 0x00000100; + + /** + * The bundle will be lazily activated. + *

      + * The bundle has a {@link Constants#ACTIVATION_LAZY lazy activation policy} + * and is waiting to be activated. It is now in the {@link Bundle#STARTING + * STARTING} state and has a valid {@code BundleContext}. This event is only + * delivered to {@link SynchronousBundleListener}s. It is not delivered to + * {@code BundleListener}s. + * + * @since 1.4 + */ + public final static int LAZY_ACTIVATION = 0x00000200; + + /** + * Bundle that was the origin of the event. For install event type, this is + * the bundle whose context was used to install the bundle. Otherwise it is + * the bundle itself. + * + * @since 1.6 + */ + private final Bundle origin; + + /** + * Creates a bundle event of the specified type. + * + * @param type The event type. + * @param bundle The bundle which had a lifecycle change. + * @param origin The bundle which is the origin of the event. For the event + * type {@link #INSTALLED}, this is the bundle whose context was used + * to install the bundle. Otherwise it is the bundle itself. + * @since 1.6 + */ + public BundleEvent(int type, Bundle bundle, Bundle origin) { + super(bundle); + if (origin == null) { + throw new IllegalArgumentException("null origin"); + } + this.bundle = bundle; + this.type = type; + this.origin = origin; + } + + /** + * Creates a bundle event of the specified type. + * + * @param type The event type. + * @param bundle The bundle which had a lifecycle change. This bundle is + * used as the origin of the event. + */ + public BundleEvent(int type, Bundle bundle) { + super(bundle); + this.bundle = bundle; + this.type = type; + this.origin = bundle; + } + + /** + * Returns the bundle which had a lifecycle change. This bundle is the + * source of the event. + * + * @return The bundle that had a change occur in its lifecycle. + */ + public Bundle getBundle() { + return bundle; + } + + /** + * Returns the type of lifecyle event. The type values are: + *

        + *
      • {@link #INSTALLED}
      • + *
      • {@link #RESOLVED}
      • + *
      • {@link #LAZY_ACTIVATION}
      • + *
      • {@link #STARTING}
      • + *
      • {@link #STARTED}
      • + *
      • {@link #STOPPING}
      • + *
      • {@link #STOPPED}
      • + *
      • {@link #UPDATED}
      • + *
      • {@link #UNRESOLVED}
      • + *
      • {@link #UNINSTALLED}
      • + *
      + * + * @return The type of lifecycle event. + */ + public int getType() { + return type; + } + + /** + * Returns the bundle that was the origin of the event. + * + *

      + * For the event type {@link #INSTALLED}, this is the bundle whose context + * was used to install the bundle. Otherwise it is the bundle itself. + * + * @return The bundle that was the origin of the event. + * @since 1.6 + */ + public Bundle getOrigin() { + return origin; + } +} diff --git a/framework/src/main/java/org/osgi/framework/BundleException.java b/framework/src/main/java/org/osgi/framework/BundleException.java new file mode 100644 index 00000000000..dfed44274ee --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/BundleException.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +/** + * A Framework exception used to indicate that a bundle lifecycle problem + * occurred. + * + *

      + * A {@code BundleException} object is created by the Framework to denote an + * exception condition in the lifecycle of a bundle. {@code BundleException}s + * should not be created by bundle developers. A type code is used to identify + * the exception type for future extendability. + * + *

      + * OSGi Alliance reserves the right to extend the set of types. + * + *

      + * This exception conforms to the general purpose exception chaining mechanism. + * + * @author $Id: a0d23c4cb73b0d6386bcd6b9eebed29fdd9a4deb $ + */ + +public class BundleException extends Exception { + static final long serialVersionUID = 3571095144220455665L; + /** + * Type of bundle exception. + * + * @since 1.5 + */ + private final int type; + + /** + * No exception type is specified. + * + * @since 1.5 + */ + public static final int UNSPECIFIED = 0; + /** + * The operation was unsupported. This type can be used anywhere a + * BundleException can be thrown. + * + * @since 1.5 + */ + public static final int UNSUPPORTED_OPERATION = 1; + /** + * The operation was invalid. + * + * @since 1.5 + */ + public static final int INVALID_OPERATION = 2; + /** + * The bundle manifest was in error. + * + * @since 1.5 + */ + public static final int MANIFEST_ERROR = 3; + /** + * The bundle was not resolved. + * + * @since 1.5 + */ + public static final int RESOLVE_ERROR = 4; + /** + * The bundle activator was in error. + * + * @since 1.5 + */ + public static final int ACTIVATOR_ERROR = 5; + /** + * The operation failed due to insufficient permissions. + * + * @since 1.5 + */ + public static final int SECURITY_ERROR = 6; + /** + * The operation failed to complete the requested lifecycle state change. + * + * @since 1.5 + */ + public static final int STATECHANGE_ERROR = 7; + + /** + * The bundle could not be resolved due to an error with the + * Bundle-NativeCode header. + * + * @since 1.5 + */ + public static final int NATIVECODE_ERROR = 8; + + /** + * The install or update operation failed because another already installed + * bundle has the same symbolic name and version. This exception type will + * only occur if the framework is configured to only allow a single bundle + * to be installed for a given symbolic name and version. + * + * @see Constants#FRAMEWORK_BSNVERSION + * @since 1.5 + */ + public static final int DUPLICATE_BUNDLE_ERROR = 9; + + /** + * The start transient operation failed because the start level of the + * bundle is greater than the current framework start level + * + * @since 1.5 + */ + public static final int START_TRANSIENT_ERROR = 10; + + /** + * The framework received an error while reading the input stream for a + * bundle. + * + * @since 1.6 + */ + public static final int READ_ERROR = 11; + + /** + * A framework hook rejected the operation. + * + * @since 1.6 + */ + public static final int REJECTED_BY_HOOK = 12; + + /** + * Creates a {@code BundleException} with the specified message and + * exception cause. + * + * @param msg The associated message. + * @param cause The cause of this exception. + */ + public BundleException(String msg, Throwable cause) { + this(msg, UNSPECIFIED, cause); + } + + /** + * Creates a {@code BundleException} with the specified message. + * + * @param msg The message. + */ + public BundleException(String msg) { + this(msg, UNSPECIFIED); + } + + /** + * Creates a {@code BundleException} with the specified message, type and + * exception cause. + * + * @param msg The associated message. + * @param type The type for this exception. + * @param cause The cause of this exception. + * @since 1.5 + */ + public BundleException(String msg, int type, Throwable cause) { + super(msg, cause); + this.type = type; + } + + /** + * Creates a {@code BundleException} with the specified message and type. + * + * @param msg The message. + * @param type The type for this exception. + * @since 1.5 + */ + public BundleException(String msg, int type) { + super(msg); + this.type = type; + } + + /** + * Returns the cause of this exception or {@code null} if no cause was + * specified when this exception was created. + * + *

      + * This method predates the general purpose exception chaining mechanism. + * The {@code getCause()} method is now the preferred means of obtaining + * this information. + * + * @return The result of calling {@code getCause()}. + */ + public Throwable getNestedException() { + return getCause(); + } + + /** + * Returns the cause of this exception or {@code null} if no cause was set. + * + * @return The cause of this exception or {@code null} if no cause was set. + * @since 1.3 + */ + @Override + public Throwable getCause() { + return super.getCause(); + } + + /** + * Initializes the cause of this exception to the specified value. + * + * @param cause The cause of this exception. + * @return This exception. + * @throws IllegalArgumentException If the specified cause is this + * exception. + * @throws IllegalStateException If the cause of this exception has already + * been set. + * @since 1.3 + */ + @Override + public Throwable initCause(Throwable cause) { + return super.initCause(cause); + } + + /** + * Returns the type for this exception or {@code UNSPECIFIED} if the type + * was unspecified or unknown. + * + * @return The type of this exception. + * @since 1.5 + */ + public int getType() { + return type; + } +} diff --git a/framework/src/main/java/org/osgi/framework/BundleListener.java b/framework/src/main/java/org/osgi/framework/BundleListener.java new file mode 100644 index 00000000000..567491b44f0 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/BundleListener.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2015). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import java.util.EventListener; +import org.osgi.annotation.versioning.ConsumerType; + +/** + * A {@code BundleEvent} listener. {@code BundleListener} is a listener + * interface that may be implemented by a bundle developer. When a + * {@code BundleEvent} is fired, it is asynchronously delivered to a + * {@code BundleListener}. The Framework delivers {@code BundleEvent} objects to + * a {@code BundleListener} in order and must not concurrently call a + * {@code BundleListener}. + *

      + * A {@code BundleListener} object is registered with the Framework using the + * {@link BundleContext#addBundleListener(BundleListener)} method. + * {@code BundleListener}s are called with a {@code BundleEvent} object when a + * bundle has been installed, resolved, started, stopped, updated, unresolved, + * or uninstalled. + * + * @see BundleEvent + * @NotThreadSafe + * @author $Id: 50c74faa9062af826e76064737568302e6993366 $ + */ +@ConsumerType +@FunctionalInterface +public interface BundleListener extends EventListener { + /** + * Receives notification that a bundle has had a lifecycle change. + * + * @param event The {@code BundleEvent}. + */ + public void bundleChanged(BundleEvent event); +} diff --git a/framework/src/main/java/org/osgi/framework/BundlePermission.java b/framework/src/main/java/org/osgi/framework/BundlePermission.java new file mode 100644 index 00000000000..237159944ee --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/BundlePermission.java @@ -0,0 +1,593 @@ +/* + * Copyright (c) OSGi Alliance (2004, 2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamField; +import java.security.BasicPermission; +import java.security.Permission; +import java.security.PermissionCollection; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +/** + * A bundle's authority to require or provide a bundle or to receive or attach + * fragments. + * + *

      + * A bundle symbolic name defines a unique fully qualified name. Wildcards may + * be used. + * + *

      + * name ::= <symbolic name> | <symbolic name ending in ".*"> | *
      + * 
      + * + * Examples: + * + *
      + * org.osgi.example.bundle
      + * org.osgi.example.*
      + * *
      + * 
      + * + *

      + * {@code BundlePermission} has four actions: {@code provide}, {@code require}, + * {@code host}, and {@code fragment}. The {@code provide} action implies the + * {@code require} action. + * + * @since 1.3 + * @ThreadSafe + * @author $Id: 7b0816059dc9b3e37f0375039bebbe5f0b18d998 $ + */ + +public final class BundlePermission extends BasicPermission { + + private static final long serialVersionUID = 3257846601685873716L; + + /** + * The action string {@code provide}. The {@code provide} action implies the + * {@code require} action. + */ + public final static String PROVIDE = "provide"; + + /** + * The action string {@code require}. The {@code require} action is implied + * by the {@code provide} action. + */ + public final static String REQUIRE = "require"; + + /** + * The action string {@code host}. + */ + public final static String HOST = "host"; + + /** + * The action string {@code fragment}. + */ + public final static String FRAGMENT = "fragment"; + + private final static int ACTION_PROVIDE = 0x00000001; + private final static int ACTION_REQUIRE = 0x00000002; + private final static int ACTION_HOST = 0x00000004; + private final static int ACTION_FRAGMENT = 0x00000008; + private final static int ACTION_ALL = ACTION_PROVIDE | ACTION_REQUIRE | ACTION_HOST | ACTION_FRAGMENT; + final static int ACTION_NONE = 0; + /** + * The actions mask. + */ + private transient int action_mask; + + /** + * The actions in canonical form. + * + * @serial + */ + private volatile String actions = null; + + /** + * Defines the authority to provide and/or require and or specify a host + * fragment symbolic name within the OSGi environment. + *

      + * Bundle Permissions are granted over all possible versions of a bundle. + * + * A bundle that needs to provide a bundle must have the appropriate + * {@code BundlePermission} for the symbolic name; a bundle that requires a + * bundle must have the appropriate {@code BundlePermssion} for that + * symbolic name; a bundle that specifies a fragment host must have the + * appropriate {@code BundlePermission} for that symbolic name. + * + * @param symbolicName The bundle symbolic name. + * @param actions {@code provide},{@code require}, {@code host}, + * {@code fragment} (canonical order). + */ + public BundlePermission(String symbolicName, String actions) { + this(symbolicName, parseActions(actions)); + } + + /** + * Package private constructor used by BundlePermissionCollection. + * + * @param symbolicName the bundle symbolic name + * @param mask the action mask + */ + BundlePermission(String symbolicName, int mask) { + super(symbolicName); + setTransients(mask); + } + + /** + * Called by constructors and when deserialized. + * + * @param mask + */ + private synchronized void setTransients(int mask) { + if ((mask == ACTION_NONE) || ((mask & ACTION_ALL) != mask)) { + throw new IllegalArgumentException("invalid action string"); + } + + action_mask = mask; + } + + /** + * Returns the current action mask. + *

      + * Used by the BundlePermissionCollection class. + * + * @return Current action mask. + */ + synchronized int getActionsMask() { + return action_mask; + } + + /** + * Parse action string into action mask. + * + * @param actions Action string. + * @return action mask. + */ + private static int parseActions(String actions) { + boolean seencomma = false; + + int mask = ACTION_NONE; + + if (actions == null) { + return mask; + } + + char[] a = actions.toCharArray(); + + int i = a.length - 1; + if (i < 0) + return mask; + + while (i != -1) { + char c; + + // skip whitespace + while ((i != -1) && ((c = a[i]) == ' ' || c == '\r' || c == '\n' || c == '\f' || c == '\t')) + i--; + + // check for the known strings + int matchlen; + + if (i >= 6 && (a[i - 6] == 'p' || a[i - 6] == 'P') + && (a[i - 5] == 'r' || a[i - 5] == 'R') + && (a[i - 4] == 'o' || a[i - 4] == 'O') + && (a[i - 3] == 'v' || a[i - 3] == 'V') + && (a[i - 2] == 'i' || a[i - 2] == 'I') + && (a[i - 1] == 'd' || a[i - 1] == 'D') + && (a[i] == 'e' || a[i] == 'E')) { + matchlen = 7; + mask |= ACTION_PROVIDE | ACTION_REQUIRE; + } else + if (i >= 6 && (a[i - 6] == 'r' || a[i - 6] == 'R') + && (a[i - 5] == 'e' || a[i - 5] == 'E') + && (a[i - 4] == 'q' || a[i - 4] == 'Q') + && (a[i - 3] == 'u' || a[i - 3] == 'U') + && (a[i - 2] == 'i' || a[i - 2] == 'I') + && (a[i - 1] == 'r' || a[i - 1] == 'R') + && (a[i] == 'e' || a[i] == 'E')) { + matchlen = 7; + mask |= ACTION_REQUIRE; + } else + if (i >= 3 && (a[i - 3] == 'h' || a[i - 3] == 'H') + && (a[i - 2] == 'o' || a[i - 2] == 'O') + && (a[i - 1] == 's' || a[i - 1] == 'S') + && (a[i] == 't' || a[i] == 'T')) { + matchlen = 4; + mask |= ACTION_HOST; + } else + if (i >= 7 && (a[i - 7] == 'f' || a[i - 7] == 'F') + && (a[i - 6] == 'r' || a[i - 6] == 'R') + && (a[i - 5] == 'a' || a[i - 5] == 'A') + && (a[i - 4] == 'g' || a[i - 4] == 'G') + && (a[i - 3] == 'm' || a[i - 3] == 'M') + && (a[i - 2] == 'e' || a[i - 2] == 'E') + && (a[i - 1] == 'n' || a[i - 1] == 'N') + && (a[i] == 't' || a[i] == 'T')) { + matchlen = 8; + mask |= ACTION_FRAGMENT; + } else { + // parse error + throw new IllegalArgumentException("invalid permission: " + actions); + } + + // make sure we didn't just match the tail of a word + // like "ackbarfrequire". Also, skip to the comma. + seencomma = false; + while (i >= matchlen && !seencomma) { + switch (a[i - matchlen]) { + case ',' : + seencomma = true; + /* FALLTHROUGH */ + case ' ' : + case '\r' : + case '\n' : + case '\f' : + case '\t' : + break; + default : + throw new IllegalArgumentException("invalid permission: " + actions); + } + i--; + } + + // point i at the location of the comma minus one (or -1). + i -= matchlen; + } + + if (seencomma) { + throw new IllegalArgumentException("invalid permission: " + actions); + } + + return mask; + } + + /** + * Determines if the specified permission is implied by this object. + * + *

      + * This method checks that the symbolic name of the target is implied by the + * symbolic name of this object. The list of {@code BundlePermission} + * actions must either match or allow for the list of the target object to + * imply the target {@code BundlePermission} action. + *

      + * The permission to provide a bundle implies the permission to require the + * named symbolic name. + * + *

      +	 *       x.y.*,"provide" -> x.y.z,"provide" is true
      +	 *       *,"require" -> x.y, "require"      is true
      +	 *       *,"provide" -> x.y, "require"      is true
      +	 *       x.y,"provide" -> x.y.z, "provide"  is false
      +	 * 
      + * + * @param p The requested permission. + * @return {@code true} if the specified {@code BundlePermission} action is + * implied by this object; {@code false} otherwise. + */ + @Override + public boolean implies(Permission p) { + if (!(p instanceof BundlePermission)) { + return false; + } + BundlePermission requested = (BundlePermission) p; + + final int effective = getActionsMask(); + final int desired = requested.getActionsMask(); + return ((effective & desired) == desired) && super.implies(requested); + } + + /** + * Returns the canonical string representation of the + * {@code BundlePermission} actions. + * + *

      + * Always returns present {@code BundlePermission} actions in the following + * order: {@code provide}, {@code require}, {@code host}, {@code fragment}. + * + * @return Canonical string representation of the {@code BundlePermission + * } actions. + */ + @Override + public String getActions() { + String result = actions; + if (result == null) { + StringBuilder sb = new StringBuilder(); + boolean comma = false; + + if ((action_mask & ACTION_PROVIDE) == ACTION_PROVIDE) { + sb.append(PROVIDE); + comma = true; + } + + if ((action_mask & ACTION_REQUIRE) == ACTION_REQUIRE) { + if (comma) + sb.append(','); + sb.append(REQUIRE); + comma = true; + } + + if ((action_mask & ACTION_HOST) == ACTION_HOST) { + if (comma) + sb.append(','); + sb.append(HOST); + comma = true; + } + + if ((action_mask & ACTION_FRAGMENT) == ACTION_FRAGMENT) { + if (comma) + sb.append(','); + sb.append(FRAGMENT); + } + + actions = result = sb.toString(); + } + return result; + } + + /** + * Returns a new {@code PermissionCollection} object suitable for storing + * {@code BundlePermission} objects. + * + * @return A new {@code PermissionCollection} object. + */ + @Override + public PermissionCollection newPermissionCollection() { + return new BundlePermissionCollection(); + } + + /** + * Determines the equality of two {@code BundlePermission} objects. + * + * This method checks that specified bundle has the same bundle symbolic + * name and {@code BundlePermission} actions as this + * {@code BundlePermission} object. + * + * @param obj The object to test for equality with this + * {@code BundlePermission} object. + * @return {@code true} if {@code obj} is a {@code BundlePermission}, and + * has the same bundle symbolic name and actions as this + * {@code BundlePermission} object; {@code false} otherwise. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (!(obj instanceof BundlePermission)) { + return false; + } + + BundlePermission bp = (BundlePermission) obj; + + return (getActionsMask() == bp.getActionsMask()) && getName().equals(bp.getName()); + } + + /** + * Returns the hash code value for this object. + * + * @return A hash code value for this object. + */ + @Override + public int hashCode() { + int h = 31 * 17 + getName().hashCode(); + h = 31 * h + getActions().hashCode(); + return h; + } + + /** + * WriteObject is called to save the state of the {@code BundlePermission} + * object to a stream. The actions are serialized, and the superclass takes + * care of the name. + */ + private synchronized void writeObject(java.io.ObjectOutputStream s) throws IOException { + // Write out the actions. The superclass takes care of the name + // call getActions to make sure actions field is initialized + if (actions == null) + getActions(); + s.defaultWriteObject(); + } + + /** + * readObject is called to restore the state of the BundlePermission from a + * stream. + */ + private synchronized void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { + // Read in the action, then initialize the rest + s.defaultReadObject(); + setTransients(parseActions(actions)); + } +} + +/** + * Stores a set of {@code BundlePermission} permissions. + * + * @see java.security.Permission + * @see java.security.Permissions + * @see java.security.PermissionCollection + */ + +final class BundlePermissionCollection extends PermissionCollection { + private static final long serialVersionUID = 3258407326846433079L; + + /** + * Table of permissions. + * + * @GuardedBy this + */ + private transient Map permissions; + + /** + * Boolean saying if "*" is in the collection. + * + * @serial + * @GuardedBy this + */ + private boolean all_allowed; + + /** + * Create an empty BundlePermissions object. + * + */ + public BundlePermissionCollection() { + permissions = new HashMap(); + all_allowed = false; + } + + /** + * Add a permission to this permission collection. + * + * @param permission The {@code BundlePermission} object to add. + * @throws IllegalArgumentException If the permission is not a + * {@code BundlePermission} instance. + * @throws SecurityException If this {@code BundlePermissionCollection} + * object has been marked read-only. + */ + @Override + public void add(final Permission permission) { + if (!(permission instanceof BundlePermission)) { + throw new IllegalArgumentException("invalid permission: " + permission); + } + if (isReadOnly()) { + throw new SecurityException("attempt to add a Permission to a " + "readonly PermissionCollection"); + } + final BundlePermission bp = (BundlePermission) permission; + final String name = bp.getName(); + synchronized (this) { + Map pc = permissions; + BundlePermission existing = pc.get(name); + if (existing != null) { + final int oldMask = existing.getActionsMask(); + final int newMask = bp.getActionsMask(); + if (oldMask != newMask) { + pc.put(name, new BundlePermission(name, oldMask | newMask)); + + } + } else { + pc.put(name, bp); + } + + if (!all_allowed) { + if (name.equals("*")) + all_allowed = true; + } + } + } + + /** + * Determines if the specified permissions implies the permissions expressed + * in {@code permission}. + * + * @param permission The Permission object to compare with this + * {@code BundlePermission} object. + * @return {@code true} if {@code permission} is a proper subset of a + * permission in the set; {@code false} otherwise. + */ + @Override + public boolean implies(final Permission permission) { + if (!(permission instanceof BundlePermission)) { + return false; + } + BundlePermission requested = (BundlePermission) permission; + String requestedName = requested.getName(); + final int desired = requested.getActionsMask(); + int effective = BundlePermission.ACTION_NONE; + BundlePermission bp; + + synchronized (this) { + Map pc = permissions; + /* short circuit if the "*" Permission was added */ + if (all_allowed) { + bp = pc.get("*"); + if (bp != null) { + effective |= bp.getActionsMask(); + if ((effective & desired) == desired) { + return true; + } + } + } + bp = pc.get(requestedName); + // strategy: + // Check for full match first. Then work our way up the + // name looking for matches on a.b.* + if (bp != null) { + // we have a direct hit! + effective |= bp.getActionsMask(); + if ((effective & desired) == desired) { + return true; + } + } + // work our way up the tree... + int last; + int offset = requestedName.length() - 1; + while ((last = requestedName.lastIndexOf(".", offset)) != -1) { + requestedName = requestedName.substring(0, last + 1) + "*"; + bp = pc.get(requestedName); + if (bp != null) { + effective |= bp.getActionsMask(); + if ((effective & desired) == desired) { + return true; + } + } + offset = last - 1; + } + // we don't have to check for "*" as it was already checked + // at the top (all_allowed), so we just return false + return false; + } + } + + /** + * Returns an enumeration of all {@code BundlePermission} objects in the + * container. + * + * @return Enumeration of all {@code BundlePermission} objects. + */ + @Override + public synchronized Enumeration elements() { + List all = new ArrayList(permissions.values()); + return Collections.enumeration(all); + } + + /* serialization logic */ + private static final ObjectStreamField[] serialPersistentFields = {new ObjectStreamField("permissions", Hashtable.class), new ObjectStreamField("all_allowed", Boolean.TYPE)}; + + private synchronized void writeObject(ObjectOutputStream out) throws IOException { + Hashtable hashtable = new Hashtable(permissions); + ObjectOutputStream.PutField pfields = out.putFields(); + pfields.put("permissions", hashtable); + pfields.put("all_allowed", all_allowed); + out.writeFields(); + } + + private synchronized void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { + ObjectInputStream.GetField gfields = in.readFields(); + @SuppressWarnings("unchecked") + Hashtable hashtable = (Hashtable) gfields.get("permissions", null); + permissions = new HashMap(hashtable); + all_allowed = gfields.get("all_allowed", false); + } +} diff --git a/framework/src/main/java/org/osgi/framework/BundleReference.java b/framework/src/main/java/org/osgi/framework/BundleReference.java new file mode 100644 index 00000000000..8cd2df59df0 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/BundleReference.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) OSGi Alliance (2009, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import org.osgi.annotation.versioning.ProviderType; + +/** + * A reference to a Bundle. + * + * @since 1.5 + * @ThreadSafe + * @author $Id: ad4e0b99177540205a1a8f37f9075989434cc59f $ + */ +@ProviderType +public interface BundleReference { + /** + * Returns the {@code Bundle} object associated with this + * {@code BundleReference}. + * + * @return The {@code Bundle} object associated with this + * {@code BundleReference}. + */ + public Bundle getBundle(); +} diff --git a/framework/src/main/java/org/osgi/framework/CapabilityPermission.java b/framework/src/main/java/org/osgi/framework/CapabilityPermission.java new file mode 100644 index 00000000000..177ff97a88f --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/CapabilityPermission.java @@ -0,0 +1,785 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import java.io.IOException; +import java.io.NotSerializableException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamField; +import java.security.AccessController; +import java.security.BasicPermission; +import java.security.Permission; +import java.security.PermissionCollection; +import java.security.PrivilegedAction; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A bundle's authority to provide or require a capability. + *

        + *
      • The {@code provide} action allows a bundle to provide a capability + * matching the specified filter.
      • + *
      • The {@code require} action allows a bundle to require a capability + * matching the specified filter.
      • + *
      + * + * @ThreadSafe + * @author $Id: 8a38df04e56e9dcab7ea413ba69d4c4f05487c25 $ + * @since 1.6 + */ + +public final class CapabilityPermission extends BasicPermission { + static final long serialVersionUID = -7662148639076511574L; + /** + * The action string {@code require}. + */ + public final static String REQUIRE = "require"; + /** + * The action string {@code provide}. + */ + public final static String PROVIDE = "provide"; + + private final static int ACTION_REQUIRE = 0x00000001; + private final static int ACTION_PROVIDE = 0x00000002; + private final static int ACTION_ALL = ACTION_REQUIRE | ACTION_PROVIDE; + final static int ACTION_NONE = 0; + + /** + * The actions mask. + */ + transient int action_mask; + + /** + * The actions in canonical form. + * + * @serial + */ + private volatile String actions = null; + + /** + * The attributes of the requested capability. Must be null if not + * constructed with attributes. + */ + transient final Map attributes; + + /** + * The bundle of the requested capability. Must be null if not constructed + * with bundle. + */ + transient final Bundle bundle; + + /** + * If this CapabilityPermission was constructed with a filter, this holds a + * Filter matching object used to evaluate the filter in implies. + */ + transient Filter filter; + + /** + * This map holds the properties of the permission, used to match a filter + * in implies. This is not initialized until necessary, and then cached in + * this object. + */ + private transient volatile Map properties; + + /** + * Create a new CapabilityPermission. + * + *

      + * The name is specified as a dot-separated string. Wildcards may be used. + * + *

      +	 * name ::= <namespace> | <namespace ending in ".*"> | *
      +	 * 
      + * + * Examples: + * + *
      +	 * com.acme.capability.*
      +	 * org.foo.capability
      +	 * *
      +	 * 
      + * + * For the {@code require} action, the name can also be a filter expression. + * The filter gives access to the capability attributes as well as the + * following attributes: + *
        + *
      • signer - A Distinguished Name chain used to sign the bundle providing + * the capability. Wildcards in a DN are not matched according to the filter + * string rules, but according to the rules defined for a DN chain.
      • + *
      • location - The location of the bundle providing the capability.
      • + *
      • id - The bundle ID of the bundle providing the capability.
      • + *
      • name - The symbolic name of the bundle providing the capability.
      • + *
      • capability.namespace - The namespace of the required capability.
      • + *
      + * Since the above attribute names may conflict with attribute names of a + * capability, you can prefix an attribute name with '@' in the filter + * expression to match against the capability attributes and not one of the + * above attributes. Filter attribute names are processed in a case + * sensitive manner. + * + *

      + * There are two possible actions: {@code require} and {@code provide}. The + * {@code require} permission allows the owner of this permission to require + * a capability matching the attributes. The {@code provide} permission + * allows the bundle to provide a capability in the specified capability + * namespace. + * + * @param name The capability namespace or a filter over the attributes. + * @param actions {@code require},{@code provide} (canonical order) + * @throws IllegalArgumentException If the specified name is a filter + * expression and either the specified action is not {@code require} + * or the filter has an invalid syntax. + */ + public CapabilityPermission(String name, String actions) { + this(name, parseActions(actions)); + if ((this.filter != null) && ((action_mask & ACTION_ALL) != ACTION_REQUIRE)) { + throw new IllegalArgumentException("invalid action string for filter expression"); + } + } + + /** + * Creates a new requested {@code CapabilityPermission} object to be used by + * code that must perform {@code checkPermission} for the {@code require} + * action. {@code CapabilityPermission} objects created with this + * constructor cannot be added to a {@code CapabilityPermission} permission + * collection. + * + * @param namespace The requested capability namespace. + * @param attributes The requested capability attributes. + * @param providingBundle The bundle providing the requested capability. + * @param actions The action {@code require}. + * @throws IllegalArgumentException If the specified action is not + * {@code require} or attributes or providingBundle are {@code null} + * . + */ + public CapabilityPermission(String namespace, Map attributes, Bundle providingBundle, String actions) { + super(namespace); + setTransients(namespace, parseActions(actions)); + if (attributes == null) { + throw new IllegalArgumentException("attributes must not be null"); + } + if (providingBundle == null) { + throw new IllegalArgumentException("bundle must not be null"); + } + this.attributes = new HashMap(attributes); + this.bundle = providingBundle; + if ((action_mask & ACTION_ALL) != ACTION_REQUIRE) { + throw new IllegalArgumentException("invalid action string"); + } + } + + /** + * Package private constructor used by CapabilityPermissionCollection. + * + * @param name class name + * @param mask action mask + */ + CapabilityPermission(String name, int mask) { + super(name); + setTransients(name, mask); + this.attributes = null; + this.bundle = null; + } + + /** + * Called by constructors and when deserialized. + * + * @param mask action mask + */ + private void setTransients(String name, int mask) { + if ((mask == ACTION_NONE) || ((mask & ACTION_ALL) != mask)) { + throw new IllegalArgumentException("invalid action string"); + } + action_mask = mask; + filter = parseFilter(name); + } + + /** + * Parse action string into action mask. + * + * @param actions Action string. + * @return action mask. + */ + private static int parseActions(String actions) { + boolean seencomma = false; + + int mask = ACTION_NONE; + + if (actions == null) { + return mask; + } + + char[] a = actions.toCharArray(); + + int i = a.length - 1; + if (i < 0) + return mask; + + while (i != -1) { + char c; + + // skip whitespace + while ((i != -1) && ((c = a[i]) == ' ' || c == '\r' || c == '\n' || c == '\f' || c == '\t')) + i--; + + // check for the known strings + int matchlen; + + if (i >= 6 && (a[i - 6] == 'r' || a[i - 6] == 'R') + && (a[i - 5] == 'e' || a[i - 5] == 'E') + && (a[i - 4] == 'q' || a[i - 4] == 'Q') + && (a[i - 3] == 'u' || a[i - 3] == 'U') + && (a[i - 2] == 'i' || a[i - 2] == 'I') + && (a[i - 1] == 'r' || a[i - 1] == 'R') + && (a[i] == 'e' || a[i] == 'E')) { + matchlen = 7; + mask |= ACTION_REQUIRE; + } else + if (i >= 6 && (a[i - 6] == 'p' || a[i - 6] == 'P') + && (a[i - 5] == 'r' || a[i - 5] == 'R') + && (a[i - 4] == 'o' || a[i - 4] == 'O') + && (a[i - 3] == 'v' || a[i - 3] == 'V') + && (a[i - 2] == 'i' || a[i - 2] == 'I') + && (a[i - 1] == 'd' || a[i - 1] == 'D') + && (a[i] == 'e' || a[i] == 'E')) { + matchlen = 7; + mask |= ACTION_PROVIDE; + } else { + // parse error + throw new IllegalArgumentException("invalid permission: " + actions); + } + + // make sure we didn't just match the tail of a word + // like "ackbarfprovide". Also, skip to the comma. + seencomma = false; + while (i >= matchlen && !seencomma) { + switch (a[i - matchlen]) { + case ',' : + seencomma = true; + /* FALLTHROUGH */ + case ' ' : + case '\r' : + case '\n' : + case '\f' : + case '\t' : + break; + default : + throw new IllegalArgumentException("invalid permission: " + actions); + } + i--; + } + + // point i at the location of the comma minus one (or -1). + i -= matchlen; + } + + if (seencomma) { + throw new IllegalArgumentException("invalid permission: " + actions); + } + + return mask; + } + + /** + * Parse filter string into a Filter object. + * + * @param filterString The filter string to parse. + * @return a Filter for this bundle. If the specified filterString is not a + * filter expression, then {@code null} is returned. + * @throws IllegalArgumentException If the filter syntax is invalid. + */ + private static Filter parseFilter(String filterString) { + filterString = filterString.trim(); + if (filterString.charAt(0) != '(') { + return null; + } + + try { + return FrameworkUtil.createFilter(filterString); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException("invalid filter", e); + } + } + + /** + * Determines if a {@code CapabilityPermission} object "implies" the + * specified permission. + * + * @param p The target permission to check. + * @return {@code true} if the specified permission is implied by this + * object; {@code false} otherwise. + */ + @Override + public boolean implies(Permission p) { + if (!(p instanceof CapabilityPermission)) { + return false; + } + CapabilityPermission requested = (CapabilityPermission) p; + if (bundle != null) { + return false; + } + // if requested permission has a filter, then it is an invalid argument + if (requested.filter != null) { + return false; + } + return implies0(requested, ACTION_NONE); + } + + /** + * Internal implies method. Used by the implies and the permission + * collection implies methods. + * + * @param requested The requested CapabilityPermission which has already be + * validated as a proper argument. The requested CapabilityPermission + * must not have a filter expression. + * @param effective The effective actions with which to start. + * @return {@code true} if the specified permission is implied by this + * object; {@code false} otherwise. + */ + boolean implies0(CapabilityPermission requested, int effective) { + /* check actions first - much faster */ + effective |= action_mask; + final int desired = requested.action_mask; + if ((effective & desired) != desired) { + return false; + } + /* Get filter if any */ + Filter f = filter; + if (f == null) { + return super.implies(requested); + } + return f.matches(requested.getProperties()); + } + + /** + * Returns the canonical string representation of the actions. Always + * returns present actions in the following order: {@code require}, + * {@code provide}. + * + * @return The canonical string representation of the actions. + */ + @Override + public String getActions() { + String result = actions; + if (result == null) { + StringBuilder sb = new StringBuilder(); + boolean comma = false; + + int mask = action_mask; + if ((mask & ACTION_REQUIRE) == ACTION_REQUIRE) { + sb.append(REQUIRE); + comma = true; + } + + if ((mask & ACTION_PROVIDE) == ACTION_PROVIDE) { + if (comma) + sb.append(','); + sb.append(PROVIDE); + } + + actions = result = sb.toString(); + } + + return result; + } + + /** + * Returns a new {@code PermissionCollection} object for storing + * {@code CapabilityPermission} objects. + * + * @return A new {@code PermissionCollection} object suitable for storing + * {@code CapabilityPermission} objects. + */ + @Override + public PermissionCollection newPermissionCollection() { + return new CapabilityPermissionCollection(); + } + + /** + * Determines the equality of two CapabilityPermission objects. + * + * Checks that specified object has the same name and action as this + * {@code CapabilityPermission}. + * + * @param obj The object to test for equality. + * @return true if obj is a {@code CapabilityPermission}, and has the same + * name and actions as this {@code CapabilityPermission} object; + * {@code false} otherwise. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (!(obj instanceof CapabilityPermission)) { + return false; + } + + CapabilityPermission cp = (CapabilityPermission) obj; + + return (action_mask == cp.action_mask) && getName().equals(cp.getName()) && ((attributes == cp.attributes) || ((attributes != null) && (attributes.equals(cp.attributes)))) + && ((bundle == cp.bundle) || ((bundle != null) && bundle.equals(cp.bundle))); + } + + /** + * Returns the hash code value for this object. + * + * @return Hash code value for this object. + */ + @Override + public int hashCode() { + int h = 31 * 17 + getName().hashCode(); + h = 31 * h + getActions().hashCode(); + if (attributes != null) { + h = 31 * h + attributes.hashCode(); + } + if (bundle != null) { + h = 31 * h + bundle.hashCode(); + } + return h; + } + + /** + * WriteObject is called to save the state of this permission to a stream. + * The actions are serialized, and the superclass takes care of the name. + */ + private synchronized void writeObject(java.io.ObjectOutputStream s) throws IOException { + if (bundle != null) { + throw new NotSerializableException("cannot serialize"); + } + // Write out the actions. The superclass takes care of the name + // call getActions to make sure actions field is initialized + if (actions == null) + getActions(); + s.defaultWriteObject(); + } + + /** + * readObject is called to restore the state of this permission from a + * stream. + */ + private synchronized void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { + // Read in the action, then initialize the rest + s.defaultReadObject(); + setTransients(getName(), parseActions(actions)); + } + + /** + * Called by {@link CapabilityPermission#implies(Permission)}. This method + * is only called on a requested permission which cannot have a filter set. + * + * @return a map of properties for this permission. + */ + private Map getProperties() { + Map result = properties; + if (result != null) { + return result; + } + final Map props = new HashMap(5); + props.put("capability.namespace", getName()); + if (bundle == null) { + return properties = props; + } + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Void run() { + props.put("id", Long.valueOf(bundle.getBundleId())); + props.put("location", bundle.getLocation()); + String name = bundle.getSymbolicName(); + if (name != null) { + props.put("name", name); + } + SignerProperty signer = new SignerProperty(bundle); + if (signer.isBundleSigned()) { + props.put("signer", signer); + } + return null; + } + }); + return properties = new Properties(props, attributes); + } + + static private final class Properties extends AbstractMap { + private final Map properties; + private final Map attributes; + private transient volatile Set> entries; + + Properties(Map properties, Map attributes) { + this.properties = properties; + this.attributes = attributes; + entries = null; + } + + @Override + public Object get(Object k) { + if (!(k instanceof String)) { + return null; + } + String key = (String) k; + if (key.charAt(0) == '@') { + return attributes.get(key.substring(1)); + } + Object value = properties.get(key); + if (value != null) { // fall back to service properties + return value; + } + return attributes.get(key); + } + + @Override + public Set> entrySet() { + if (entries != null) { + return entries; + } + Set> all = new HashSet>(attributes.size() + properties.size()); + all.addAll(attributes.entrySet()); + all.addAll(properties.entrySet()); + return entries = Collections.unmodifiableSet(all); + } + } +} + +/** + * Stores a set of CapabilityPermission permissions. + * + * @see java.security.Permission + * @see java.security.Permissions + * @see java.security.PermissionCollection + */ +final class CapabilityPermissionCollection extends PermissionCollection { + static final long serialVersionUID = -615322242639008920L; + + /** + * Table of permissions. + * + * @serial + * @GuardedBy this + */ + private Map permissions; + + /** + * Boolean saying if "*" is in the collection. + * + * @serial + * @GuardedBy this + */ + private boolean all_allowed; + + /** + * Table of permissions with filter expressions. + * + * @serial + * @GuardedBy this + */ + private Map filterPermissions; + + /** + * Creates an empty CapabilityPermissionCollection object. + */ + public CapabilityPermissionCollection() { + permissions = new HashMap(); + all_allowed = false; + } + + /** + * Adds a permission to this permission collection. + * + * @param permission The Permission object to add. + * @throws IllegalArgumentException If the specified permission is not a + * CapabilityPermission object. + * @throws SecurityException If this {@code CapabilityPermissionCollection} + * object has been marked read-only. + */ + @Override + public void add(final Permission permission) { + if (!(permission instanceof CapabilityPermission)) { + throw new IllegalArgumentException("invalid permission: " + permission); + } + if (isReadOnly()) { + throw new SecurityException("attempt to add a Permission to a " + "readonly PermissionCollection"); + } + + final CapabilityPermission cp = (CapabilityPermission) permission; + if (cp.bundle != null) { + throw new IllegalArgumentException("cannot add to collection: " + cp); + } + + final String name = cp.getName(); + final Filter f = cp.filter; + synchronized (this) { + /* select the bucket for the permission */ + Map pc; + if (f != null) { + pc = filterPermissions; + if (pc == null) { + filterPermissions = pc = new HashMap(); + } + } else { + pc = permissions; + } + final CapabilityPermission existing = pc.get(name); + + if (existing != null) { + final int oldMask = existing.action_mask; + final int newMask = cp.action_mask; + if (oldMask != newMask) { + pc.put(name, new CapabilityPermission(name, oldMask | newMask)); + } + } else { + pc.put(name, cp); + } + + if (!all_allowed) { + if (name.equals("*")) { + all_allowed = true; + } + } + } + } + + /** + * Determines if a set of permissions implies the permissions expressed in + * {@code permission}. + * + * @param permission The Permission object to compare. + * @return {@code true} if {@code permission} is a proper subset of a + * permission in the set; {@code false} otherwise. + */ + @Override + public boolean implies(final Permission permission) { + if (!(permission instanceof CapabilityPermission)) { + return false; + } + final CapabilityPermission requested = (CapabilityPermission) permission; + /* if requested permission has a filter, then it is an invalid argument */ + if (requested.filter != null) { + return false; + } + + String requestedName = requested.getName(); + final int desired = requested.action_mask; + int effective = CapabilityPermission.ACTION_NONE; + + Collection perms; + synchronized (this) { + Map pc = permissions; + CapabilityPermission cp; + /* short circuit if the "*" Permission was added */ + if (all_allowed) { + cp = pc.get("*"); + if (cp != null) { + effective |= cp.action_mask; + if ((effective & desired) == desired) { + return true; + } + } + } + + /* + * strategy: Check for full match first. Then work our way up the + * name looking for matches on a.b.* + */ + cp = pc.get(requestedName); + if (cp != null) { + /* we have a direct hit! */ + effective |= cp.action_mask; + if ((effective & desired) == desired) { + return true; + } + } + /* work our way up the tree... */ + int last; + int offset = requestedName.length() - 1; + while ((last = requestedName.lastIndexOf(".", offset)) != -1) { + requestedName = requestedName.substring(0, last + 1) + "*"; + cp = pc.get(requestedName); + if (cp != null) { + effective |= cp.action_mask; + if ((effective & desired) == desired) { + return true; + } + } + offset = last - 1; + } + /* + * we don't have to check for "*" as it was already checked before + * we were called. + */ + pc = filterPermissions; + if (pc == null) { + return false; + } + perms = pc.values(); + } + /* iterate one by one over filteredPermissions */ + for (CapabilityPermission perm : perms) { + if (perm.implies0(requested, effective)) { + return true; + } + } + return false; + } + + /** + * Returns an enumeration of all the {@code CapabilityPermission} objects in + * the container. + * + * @return Enumeration of all the CapabilityPermission objects. + */ + @Override + public synchronized Enumeration elements() { + List all = new ArrayList(permissions.values()); + Map pc = filterPermissions; + if (pc != null) { + all.addAll(pc.values()); + } + return Collections.enumeration(all); + } + + /* serialization logic */ + private static final ObjectStreamField[] serialPersistentFields = {new ObjectStreamField("permissions", HashMap.class), new ObjectStreamField("all_allowed", Boolean.TYPE), + new ObjectStreamField("filterPermissions", HashMap.class) }; + + private synchronized void writeObject(ObjectOutputStream out) throws IOException { + ObjectOutputStream.PutField pfields = out.putFields(); + pfields.put("permissions", permissions); + pfields.put("all_allowed", all_allowed); + pfields.put("filterPermissions", filterPermissions); + out.writeFields(); + } + + private synchronized void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { + ObjectInputStream.GetField gfields = in.readFields(); + @SuppressWarnings("unchecked") + HashMap p = (HashMap) gfields.get("permissions", null); + permissions = p; + all_allowed = gfields.get("all_allowed", false); + @SuppressWarnings("unchecked") + HashMap fp = (HashMap) gfields.get("filterPermissions", null); + filterPermissions = fp; + } +} diff --git a/framework/src/main/java/org/osgi/framework/Configurable.java b/framework/src/main/java/org/osgi/framework/Configurable.java new file mode 100644 index 00000000000..62abd45fe58 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/Configurable.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +/** + * Supports a configuration object. + * + *

      + * {@code Configurable} is an interface that should be used by a bundle + * developer in support of a configurable service. Bundles that need to + * configure a service may test to determine if the service object is an + * {@code instanceof Configurable}. + * + * @deprecated As of 1.2. Please use Configuration Admin service. + * @author $Id: dce95833dd076e870099385a306989959a84aafd $ + */ +public interface Configurable { + /** + * Returns this service's configuration object. + * + *

      + * Services implementing {@code Configurable} should take care when + * returning a service configuration object since this object is probably + * sensitive. + *

      + * If the Java Runtime Environment supports permissions, it is recommended + * that the caller is checked for some appropriate permission before + * returning the configuration object. + * + * @return The configuration object for this service. + * @throws SecurityException If the caller does not have an appropriate + * permission and the Java Runtime Environment supports permissions. + * @deprecated As of 1.2. Please use Configuration Admin service. + */ + public Object getConfigurationObject(); +} diff --git a/framework/src/main/java/org/osgi/framework/Constants.java b/framework/src/main/java/org/osgi/framework/Constants.java new file mode 100644 index 00000000000..68dfb532de0 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/Constants.java @@ -0,0 +1,1882 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2018). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.hooks.bundle.CollisionHook; +import org.osgi.framework.launch.Framework; + +/** + * Defines standard names for the OSGi environment system properties, service + * properties, and Manifest header attribute keys. + * + *

      + * The values associated with these keys are of type {@code String}, unless + * otherwise indicated. + * + * @since 1.1 + * @author $Id: 41e648afb56767a610f279a9c7effef47dfcbf2e $ + */ +@ProviderType +public interface Constants { + /** + * Location identifier of the OSGi system bundle , which is defined + * to be "System Bundle". + */ + String SYSTEM_BUNDLE_LOCATION = "System Bundle"; + + /** + * Alias for the symbolic name of the OSGi system bundle . It is + * defined to be "system.bundle". + * + * @since 1.3 + */ + String SYSTEM_BUNDLE_SYMBOLICNAME = "system.bundle"; + + /** + * Identifier of the OSGi system bundle , which is defined to be + * {@code 0}. + * + * @since 1.8 + */ + long SYSTEM_BUNDLE_ID = 0L; + + /** + * Manifest header identifying the bundle's category. + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + */ + String BUNDLE_CATEGORY = "Bundle-Category"; + + /** + * Manifest header identifying a list of directories and embedded JAR files, + * which are bundle resources used to extend the bundle's classpath. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + */ + String BUNDLE_CLASSPATH = "Bundle-ClassPath"; + + /** + * Manifest header identifying the bundle's copyright information. + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + */ + String BUNDLE_COPYRIGHT = "Bundle-Copyright"; + + /** + * Manifest header containing a brief description of the bundle's + * functionality. + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + */ + String BUNDLE_DESCRIPTION = "Bundle-Description"; + + /** + * Manifest header identifying the bundle's name. + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + */ + String BUNDLE_NAME = "Bundle-Name"; + + /** + * Manifest header identifying a number of hardware environments and the + * native language code libraries that the bundle is carrying for each of + * these environments. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + */ + String BUNDLE_NATIVECODE = "Bundle-NativeCode"; + + /** + * Manifest header identifying the packages that the bundle offers to the + * Framework for export. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + */ + String EXPORT_PACKAGE = "Export-Package"; + + /** + * Manifest header identifying the fully qualified class names of the + * services that the bundle may register (used for informational purposes + * only). + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + * + * @deprecated As of 1.2. + */ + String EXPORT_SERVICE = "Export-Service"; + + /** + * Manifest header identifying the packages on which the bundle depends. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + */ + String IMPORT_PACKAGE = "Import-Package"; + + /** + * Manifest header identifying the packages that the bundle may dynamically + * import during execution. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + * + * @since 1.2 + */ + String DYNAMICIMPORT_PACKAGE = "DynamicImport-Package"; + + /** + * Manifest header identifying the fully qualified class names of the + * services that the bundle requires (used for informational purposes only). + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + * + * @deprecated As of 1.2. + */ + String IMPORT_SERVICE = "Import-Service"; + + /** + * Manifest header identifying the bundle's vendor. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + */ + String BUNDLE_VENDOR = "Bundle-Vendor"; + + /** + * Manifest header identifying the bundle's version. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + */ + String BUNDLE_VERSION = "Bundle-Version"; + + /** + * Manifest header identifying the bundle's documentation URL, from which + * further information about the bundle may be obtained. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + */ + String BUNDLE_DOCURL = "Bundle-DocURL"; + + /** + * Manifest header identifying the contact address where problems with the + * bundle may be reported; for example, an email address. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + */ + String BUNDLE_CONTACTADDRESS = "Bundle-ContactAddress"; + + /** + * Manifest header identifying the bundle's activator class. + * + *

      + * If present, this header specifies the name of the bundle resource class + * that implements the {@code BundleActivator} interface and whose + * {@code start} and {@code stop} methods are called by the Framework when + * the bundle is started and stopped, respectively. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + */ + String BUNDLE_ACTIVATOR = "Bundle-Activator"; + + /** + * Manifest header identifying the extension bundle's activator class. + * + *

      + * If present, this header specifies the name of the extension bundle + * resource class that implements the {@code BundleActivator} interface and + * whose {@code start} and {@code stop} methods are called by the Framework + * when the Framework is initialized and shutdown, respectively. + * + * @since 1.8 + */ + String EXTENSION_BUNDLE_ACTIVATOR = "ExtensionBundle-Activator"; + + /** + * Manifest header identifying the location from which a new bundle version + * is obtained during a bundle update operation. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + */ + String BUNDLE_UPDATELOCATION = "Bundle-UpdateLocation"; + + /** + * Manifest header attribute identifying the version of a package specified + * in the Export-Package or Import-Package manifest header. + * + * @deprecated As of 1.3. This has been replaced by + * {@link #VERSION_ATTRIBUTE}. + */ + String PACKAGE_SPECIFICATION_VERSION = "specification-version"; + + /** + * Manifest header attribute identifying the processor required to run + * native bundle code specified in the Bundle-NativeCode manifest header). + * + *

      + * The attribute value is encoded in the Bundle-NativeCode manifest header + * like: + * + *

      +	 *     Bundle-NativeCode: http.so ; processor=x86 ...
      +	 * 
      + * + * @see #BUNDLE_NATIVECODE + */ + String BUNDLE_NATIVECODE_PROCESSOR = "processor"; + + /** + * Manifest header attribute identifying the operating system required to + * run native bundle code specified in the Bundle-NativeCode manifest + * header). + *

      + * The attribute value is encoded in the Bundle-NativeCode manifest header + * like: + * + *

      +	 *     Bundle-NativeCode: http.so ; osname=Linux ...
      +	 * 
      + * + * @see #BUNDLE_NATIVECODE + */ + String BUNDLE_NATIVECODE_OSNAME = "osname"; + + /** + * Manifest header attribute identifying the operating system version + * required to run native bundle code specified in the Bundle-NativeCode + * manifest header). + *

      + * The attribute value is encoded in the Bundle-NativeCode manifest header + * like: + * + *

      +	 *     Bundle-NativeCode: http.so ; osversion="2.34" ...
      +	 * 
      + * + * @see #BUNDLE_NATIVECODE + */ + String BUNDLE_NATIVECODE_OSVERSION = "osversion"; + + /** + * Manifest header attribute identifying the language in which the native + * bundle code is written specified in the Bundle-NativeCode manifest + * header. See ISO 639 for possible values. + *

      + * The attribute value is encoded in the Bundle-NativeCode manifest header + * like: + * + *

      +	 *     Bundle-NativeCode: http.so ; language=nl_be ...
      +	 * 
      + * + * @see #BUNDLE_NATIVECODE + */ + String BUNDLE_NATIVECODE_LANGUAGE = "language"; + + /** + * Manifest header identifying the required execution environment for the + * bundle. The service platform may run this bundle if any of the execution + * environments named in this header matches one of the execution + * environments it implements. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + * + * @since 1.2 + * @deprecated As of 1.6. Replaced by the {@code osgi.ee} capability. + */ + String BUNDLE_REQUIREDEXECUTIONENVIRONMENT = "Bundle-RequiredExecutionEnvironment"; + + /** + * Manifest header identifying the bundle's symbolic name. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + * + * @since 1.3 + */ + String BUNDLE_SYMBOLICNAME = "Bundle-SymbolicName"; + + /** + * Manifest header directive identifying whether a bundle is a singleton. + * The default value is {@code false}. + * + *

      + * The directive value is encoded in the Bundle-SymbolicName manifest header + * like: + * + *

      +	 *     Bundle-SymbolicName: com.acme.module.test; singleton:=true
      +	 * 
      + * + * @see #BUNDLE_SYMBOLICNAME + * @since 1.3 + */ + String SINGLETON_DIRECTIVE = "singleton"; + + /** + * Manifest header directive identifying if and when a fragment may attach + * to a host bundle. The default value is + * {@link #FRAGMENT_ATTACHMENT_ALWAYS always}. + * + *

      + * The directive value is encoded in the Bundle-SymbolicName manifest header + * like: + * + *

      +	 *     Bundle-SymbolicName: com.acme.module.test; fragment-attachment:="never"
      +	 * 
      + * + * @see #BUNDLE_SYMBOLICNAME + * @see #FRAGMENT_ATTACHMENT_ALWAYS + * @see #FRAGMENT_ATTACHMENT_RESOLVETIME + * @see #FRAGMENT_ATTACHMENT_NEVER + * @since 1.3 + */ + String FRAGMENT_ATTACHMENT_DIRECTIVE = "fragment-attachment"; + + /** + * Manifest header directive value identifying a fragment attachment type of + * always. A fragment attachment type of always indicates that fragments are + * allowed to attach to the host bundle at any time (while the host is + * resolved or during the process of resolving the host bundle). + * + *

      + * The directive value is encoded in the Bundle-SymbolicName manifest header + * like: + * + *

      +	 *     Bundle-SymbolicName: com.acme.module.test; fragment-attachment:="always"
      +	 * 
      + * + * @see #FRAGMENT_ATTACHMENT_DIRECTIVE + * @since 1.3 + */ + String FRAGMENT_ATTACHMENT_ALWAYS = "always"; + + /** + * Manifest header directive value identifying a fragment attachment type of + * resolve-time. A fragment attachment type of resolve-time indicates that + * fragments are allowed to attach to the host bundle only during the + * process of resolving the host bundle. + * + *

      + * The directive value is encoded in the Bundle-SymbolicName manifest header + * like: + * + *

      +	 *     Bundle-SymbolicName: com.acme.module.test;
      +	 *       fragment-attachment:="resolve-time"
      +	 * 
      + * + * @see #FRAGMENT_ATTACHMENT_DIRECTIVE + * @since 1.3 + */ + String FRAGMENT_ATTACHMENT_RESOLVETIME = "resolve-time"; + + /** + * Manifest header directive value identifying a fragment attachment type of + * never. A fragment attachment type of never indicates that no fragments + * are allowed to attach to the host bundle at any time. + * + *

      + * The directive value is encoded in the Bundle-SymbolicName manifest header + * like: + * + *

      +	 *     Bundle-SymbolicName: com.acme.module.test; fragment-attachment:="never"
      +	 * 
      + * + * @see #FRAGMENT_ATTACHMENT_DIRECTIVE + * @since 1.3 + */ + String FRAGMENT_ATTACHMENT_NEVER = "never"; + + /** + * Manifest header identifying the base name of the bundle's localization + * entries. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + * + * @see #BUNDLE_LOCALIZATION_DEFAULT_BASENAME + * @since 1.3 + */ + String BUNDLE_LOCALIZATION = "Bundle-Localization"; + + /** + * Default value for the {@code Bundle-Localization} manifest header. + * + * @see #BUNDLE_LOCALIZATION + * @since 1.3 + */ + String BUNDLE_LOCALIZATION_DEFAULT_BASENAME = "OSGI-INF/l10n/bundle"; + + /** + * Manifest header identifying the symbolic names of other bundles required + * by the bundle. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + * + * @since 1.3 + */ + String REQUIRE_BUNDLE = "Require-Bundle"; + + /** + * Manifest header attribute identifying a range of versions for a bundle + * specified in the {@code Require-Bundle} or {@code Fragment-Host} manifest + * headers. The default value is {@code 0.0.0}. + * + *

      + * The attribute value is encoded in the Require-Bundle manifest header + * like: + * + *

      +	 *     Require-Bundle: com.acme.module.test; bundle-version="1.1"
      +	 *     Require-Bundle: com.acme.module.test; bundle-version="[1.0,2.0)"
      +	 * 
      + * + *

      + * The bundle-version attribute value uses a mathematical interval notation + * to specify a range of bundle versions. A bundle-version attribute value + * specified as a single version means a version range that includes any + * bundle version greater than or equal to the specified version. + * + * @see #REQUIRE_BUNDLE + * @since 1.3 + */ + String BUNDLE_VERSION_ATTRIBUTE = "bundle-version"; + + /** + * Manifest header identifying the symbolic name of another bundle for which + * that the bundle is a fragment. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + * + * @since 1.3 + */ + String FRAGMENT_HOST = "Fragment-Host"; + + /** + * Manifest header attribute is used for selection by filtering based upon + * system properties. + * + *

      + * The attribute value is encoded in manifest headers like: + * + *

      +	 *     Bundle-NativeCode: libgtk.so; selection-filter="(ws=gtk)"; ...
      +	 * 
      + * + * @see #BUNDLE_NATIVECODE + * @since 1.3 + */ + String SELECTION_FILTER_ATTRIBUTE = "selection-filter"; + + /** + * Manifest header identifying the bundle manifest version. A bundle + * manifest may express the version of the syntax in which it is written by + * specifying a bundle manifest version. Bundles exploiting OSGi Release 4, + * or later, syntax must specify a bundle manifest version. + *

      + * The bundle manifest version defined by OSGi Release 4 or, more + * specifically, by version 1.3 of the OSGi Core Specification is "2". + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + * + * @since 1.3 + */ + String BUNDLE_MANIFESTVERSION = "Bundle-ManifestVersion"; + + /** + * Manifest header attribute identifying the version of a package specified + * in the Export-Package or Import-Package manifest header. + * + *

      + * The attribute value is encoded in the Export-Package or Import-Package + * manifest header like: + * + *

      +	 *     Export-Package: org.osgi.framework; version="1.1"
      +	 * 
      + * + * @see #EXPORT_PACKAGE + * @see #IMPORT_PACKAGE + * @since 1.3 + */ + String VERSION_ATTRIBUTE = "version"; + + /** + * Manifest header attribute identifying the symbolic name of a bundle that + * exports a package specified in the Import-Package manifest header. + * + *

      + * The attribute value is encoded in the Import-Package manifest header + * like: + * + *

      +	 *     Import-Package: org.osgi.framework;
      +	 *       bundle-symbolic-name="com.acme.module.test"
      +	 * 
      + * + * @see #IMPORT_PACKAGE + * @since 1.3 + */ + String BUNDLE_SYMBOLICNAME_ATTRIBUTE = "bundle-symbolic-name"; + + /** + * Manifest header directive identifying the resolution type in the + * Import-Package, Require-Bundle or Require-Capability manifest header. The + * default value is {@link #RESOLUTION_MANDATORY mandatory}. + * + *

      + * The directive value is encoded in the Import-Package, Require-Bundle or + * Require-Capability manifest header like: + * + *

      +	 *     Import-Package: org.osgi.framework; resolution:="optional"
      +	 *     Require-Bundle: com.acme.module.test; resolution:="optional"
      +	 *     Require-Capability: com.acme.capability; resolution:="optional"
      +	 * 
      + * + * @see #IMPORT_PACKAGE + * @see #REQUIRE_BUNDLE + * @see #REQUIRE_CAPABILITY + * @see #RESOLUTION_MANDATORY + * @see #RESOLUTION_OPTIONAL + * @since 1.3 + */ + String RESOLUTION_DIRECTIVE = "resolution"; + + /** + * Manifest header directive value identifying a mandatory resolution type. + * A mandatory resolution type indicates that the import package, require + * bundle or require capability must be resolved when the bundle is + * resolved. If such an import, require bundle or require capability cannot + * be resolved, the module fails to resolve. + * + *

      + * The directive value is encoded in the Import-Package, Require-Bundle or + * Require-Capability manifest header like: + * + *

      +	 *     Import-Package: org.osgi.framework; resolution:="mandatory"
      +	 *     Require-Bundle: com.acme.module.test; resolution:="mandatory"
      +	 *     Require-Capability: com.acme.capability; resolution:="mandatory"
      +	 * 
      + * + * @see #RESOLUTION_DIRECTIVE + * @since 1.3 + */ + String RESOLUTION_MANDATORY = "mandatory"; + + /** + * Manifest header directive value identifying an optional resolution type. + * An optional resolution type indicates that the import, require bundle or + * require capability is optional and the bundle may be resolved without the + * import, require bundle or require capability being resolved. If the + * import, require bundle or require capability is not resolved when the + * bundle is resolved, the import, require bundle or require capability may + * not be resolved until the bundle is refreshed. + * + *

      + * The directive value is encoded in the Import-Package, Require-Bundle or + * Require-Capability manifest header like: + * + *

      +	 *     Import-Package: org.osgi.framework; resolution:="optional"
      +	 *     Require-Bundle: com.acme.module.test; resolution:="optional"
      +	 *     Require-Capability: com.acme.capability; resolution:="optional"
      +	 * 
      + * + * @see #RESOLUTION_DIRECTIVE + * @since 1.3 + */ + String RESOLUTION_OPTIONAL = "optional"; + + /** + * Manifest header directive identifying a list of packages that an exported + * package or provided capability uses. + * + *

      + * The directive value is encoded in the Export-Package or + * Provide-Capability manifest header like: + * + *

      +	 *     Export-Package: org.osgi.util.tracker; uses:="org.osgi.framework"
      +	 *     Provide-Capability: com.acme.capability; uses:="com.acme.service"
      +	 * 
      + * + * @see #EXPORT_PACKAGE + * @see #PROVIDE_CAPABILITY + * @since 1.3 + */ + String USES_DIRECTIVE = "uses"; + + /** + * Manifest header directive identifying a list of classes to include in the + * exported package. + * + *

      + * This directive is used by the Export-Package manifest header to identify + * a list of classes of the specified package which must be allowed to be + * exported. The directive value is encoded in the Export-Package manifest + * header like: + * + *

      +	 *     Export-Package: org.osgi.framework; include:="MyClass*"
      +	 * 
      + * + *

      + * This directive is also used by the Bundle-ActivationPolicy manifest + * header to identify the packages from which class loads will trigger lazy + * activation. The directive value is encoded in the Bundle-ActivationPolicy + * manifest header like: + * + *

      +	 *     Bundle-ActivationPolicy: lazy; include:="org.osgi.framework"
      +	 * 
      + * + * @see #EXPORT_PACKAGE + * @see #BUNDLE_ACTIVATIONPOLICY + * @since 1.3 + */ + String INCLUDE_DIRECTIVE = "include"; + + /** + * Manifest header directive identifying a list of classes to exclude in the + * exported package.. + *

      + * This directive is used by the Export-Package manifest header to identify + * a list of classes of the specified package which must not be allowed to + * be exported. The directive value is encoded in the Export-Package + * manifest header like: + * + *

      +	 *     Export-Package: org.osgi.framework; exclude:="*Impl"
      +	 * 
      + * + *

      + * This directive is also used by the Bundle-ActivationPolicy manifest + * header to identify the packages from which class loads will not trigger + * lazy activation. The directive value is encoded in the + * Bundle-ActivationPolicy manifest header like: + * + *

      +	 *     Bundle-ActivationPolicy: lazy; exclude:="org.osgi.framework"
      +	 * 
      + * + * @see #EXPORT_PACKAGE + * @see #BUNDLE_ACTIVATIONPOLICY + * @since 1.3 + */ + String EXCLUDE_DIRECTIVE = "exclude"; + + /** + * Manifest header directive identifying names of matching attributes which + * must be specified by matching Import-Package statements in the + * Export-Package manifest header. + * + *

      + * The directive value is encoded in the Export-Package manifest header + * like: + * + *

      +	 *     Export-Package: org.osgi.framework; mandatory:="bundle-symbolic-name"
      +	 * 
      + * + * @see #EXPORT_PACKAGE + * @since 1.3 + */ + String MANDATORY_DIRECTIVE = "mandatory"; + + /** + * Manifest header directive identifying the visibility of a required bundle + * in the Require-Bundle manifest header. The default value is + * {@link #VISIBILITY_PRIVATE private}. + * + *

      + * The directive value is encoded in the Require-Bundle manifest header + * like: + * + *

      +	 *     Require-Bundle: com.acme.module.test; visibility:="reexport"
      +	 * 
      + * + * @see #REQUIRE_BUNDLE + * @see #VISIBILITY_PRIVATE + * @see #VISIBILITY_REEXPORT + * @since 1.3 + */ + String VISIBILITY_DIRECTIVE = "visibility"; + + /** + * Manifest header directive value identifying a private visibility type. A + * private visibility type indicates that any packages that are exported by + * the required bundle are not made visible on the export signature of the + * requiring bundle. + * + *

      + * The directive value is encoded in the Require-Bundle manifest header + * like: + * + *

      +	 *     Require-Bundle: com.acme.module.test; visibility:="private"
      +	 * 
      + * + * @see #VISIBILITY_DIRECTIVE + * @since 1.3 + */ + String VISIBILITY_PRIVATE = "private"; + + /** + * Manifest header directive value identifying a reexport visibility type. A + * reexport visibility type indicates any packages that are exported by the + * required bundle are re-exported by the requiring bundle. Any arbitrary + * matching attributes with which they were exported by the required bundle + * are deleted. + *

      + * The directive value is encoded in the Require-Bundle manifest header + * like: + * + *

      +	 *     Require-Bundle: com.acme.module.test; visibility:="reexport"
      +	 * 
      + * + * @see #VISIBILITY_DIRECTIVE + * @since 1.3 + */ + String VISIBILITY_REEXPORT = "reexport"; + + /** + * Manifest header directive identifying the type of the extension fragment. + * + *

      + * The directive value is encoded in the Fragment-Host manifest header like: + * + *

      +	 *     Fragment-Host: system.bundle; extension:="framework"
      +	 * 
      + * + *

      + * The default value is {@link #EXTENSION_FRAMEWORK framework}. + * + * @see #FRAGMENT_HOST + * @see #EXTENSION_FRAMEWORK + * @since 1.3 + */ + String EXTENSION_DIRECTIVE = "extension"; + + /** + * Manifest header directive value identifying the type of extension + * fragment. An extension fragment type of framework indicates that the + * extension fragment is to be loaded by the framework's class loader. + * + *

      + * The directive value is encoded in the Fragment-Host manifest header like: + * + *

      +	 *     Fragment-Host: system.bundle; extension:="framework"
      +	 * 
      + * + * @see #EXTENSION_DIRECTIVE + * @since 1.3 + */ + String EXTENSION_FRAMEWORK = "framework"; + + /** + * Manifest header directive value identifying the type of extension + * fragment. An extension fragment type of bootclasspath indicates that the + * extension fragment is to be loaded by the boot class loader. + *

      + * The directive value is encoded in the Fragment-Host manifest header like: + * + *

      +	 *     Fragment-Host: system.bundle; extension:="bootclasspath"
      +	 * 
      + * + * @see #EXTENSION_DIRECTIVE + * @since 1.3 + * @deprecated As of 1.9. + */ + String EXTENSION_BOOTCLASSPATH = "bootclasspath"; + + /** + * Manifest header identifying the bundle's activation policy. + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + * + * @since 1.4 + * @see #ACTIVATION_LAZY + * @see #INCLUDE_DIRECTIVE + * @see #EXCLUDE_DIRECTIVE + */ + String BUNDLE_ACTIVATIONPOLICY = "Bundle-ActivationPolicy"; + + /** + * Bundle activation policy declaring the bundle must be activated when the + * first class load is made from the bundle. + *

      + * A bundle with the lazy activation policy that is started with the + * {@link Bundle#START_ACTIVATION_POLICY START_ACTIVATION_POLICY} option + * will wait in the {@link Bundle#STARTING STARTING} state until the first + * class load from the bundle occurs. The bundle will then be activated + * before the class is returned to the requester. + *

      + * The activation policy value is specified as in the + * Bundle-ActivationPolicy manifest header like: + * + *

      +	 *       Bundle-ActivationPolicy: lazy
      +	 * 
      + * + * @see #BUNDLE_ACTIVATIONPOLICY + * @see Bundle#start(int) + * @see Bundle#START_ACTIVATION_POLICY + * @since 1.4 + */ + String ACTIVATION_LAZY = "lazy"; + + /** + * Framework environment property identifying the Framework version. + * + *

      + * The value of this property may be retrieved by calling the + * {@code BundleContext.getProperty} method. + */ + String FRAMEWORK_VERSION = "org.osgi.framework.version"; + + /** + * Framework environment property identifying the Framework implementation + * vendor. + * + *

      + * The value of this property may be retrieved by calling the + * {@code BundleContext.getProperty} method. + */ + String FRAMEWORK_VENDOR = "org.osgi.framework.vendor"; + + /** + * Framework launching property identifying the Framework implementation + * language (see ISO 639 for possible values). + * + *

      + * The value of this property may be retrieved by calling the + * {@code BundleContext.getProperty} method. + */ + String FRAMEWORK_LANGUAGE = "org.osgi.framework.language"; + + /** + * Framework launching property identifying the Framework host-computer's + * operating system. + * + *

      + * The value of this property may be retrieved by calling the + * {@code BundleContext.getProperty} method. + */ + String FRAMEWORK_OS_NAME = "org.osgi.framework.os.name"; + + /** + * Framework launching property identifying the Framework host-computer's + * operating system version number. + * + *

      + * The value of this property may be retrieved by calling the + * {@code BundleContext.getProperty} method. + */ + String FRAMEWORK_OS_VERSION = "org.osgi.framework.os.version"; + + /** + * Framework launching property identifying the Framework host-computer's + * processor name. + * + *

      + * The value of this property may be retrieved by calling the + * {@code BundleContext.getProperty} method. + */ + String FRAMEWORK_PROCESSOR = "org.osgi.framework.processor"; + + /** + * Framework launching property identifying execution environments provided + * by the Framework. + * + *

      + * The value of this property may be retrieved by calling the + * {@code BundleContext.getProperty} method. + * + * @since 1.2 + * @deprecated As of 1.6. Replaced by the {@code osgi.ee} capability. + */ + String FRAMEWORK_EXECUTIONENVIRONMENT = "org.osgi.framework.executionenvironment"; + + /** + * Framework launching property identifying packages for which the Framework + * must delegate class loading to the parent class loader of the bundle. + * + *

      + * The value of this property may be retrieved by calling the + * {@code BundleContext.getProperty} method. + * + * @see #FRAMEWORK_BUNDLE_PARENT + * @since 1.3 + */ + String FRAMEWORK_BOOTDELEGATION = "org.osgi.framework.bootdelegation"; + + /** + * Framework launching property identifying packages which the system bundle + * must export. + * + *

      + * If this property is not specified then the framework must calculate a + * reasonable default value for the current execution environment. + * + *

      + * The value of this property may be retrieved by calling the + * {@code BundleContext.getProperty} method. + * + * @since 1.3 + */ + String FRAMEWORK_SYSTEMPACKAGES = "org.osgi.framework.system.packages"; + + /** + * Framework launching property identifying extra packages which the system + * bundle must export from the current execution environment. + * + *

      + * This property is useful for configuring extra system packages in addition + * to the system packages calculated by the framework. + * + *

      + * The value of this property may be retrieved by calling the + * {@code BundleContext.getProperty} method. + * + * @see #FRAMEWORK_SYSTEMPACKAGES + * @since 1.5 + */ + String FRAMEWORK_SYSTEMPACKAGES_EXTRA = "org.osgi.framework.system.packages.extra"; + + /** + * Framework environment property identifying whether the Framework supports + * framework extension bundles. + * + *

      + * As of version 1.4, the value of this property must be {@code true}. The + * Framework must support framework extension bundles. + * + *

      + * The value of this property may be retrieved by calling the + * {@code BundleContext.getProperty} method. + * + * @since 1.3 + */ + String SUPPORTS_FRAMEWORK_EXTENSION = "org.osgi.supports.framework.extension"; + + /** + * Framework environment property identifying whether the Framework supports + * bootclasspath extension bundles. + * + *

      + * If the value of this property is {@code true}, then the Framework + * supports bootclasspath extension bundles. The default value is + * {@code false}. + *

      + * The value of this property may be retrieved by calling the + * {@code BundleContext.getProperty} method. + * + * @since 1.3 + */ + String SUPPORTS_BOOTCLASSPATH_EXTENSION = "org.osgi.supports.bootclasspath.extension"; + + /** + * Framework environment property identifying whether the Framework supports + * fragment bundles. + * + *

      + * As of version 1.4, the value of this property must be {@code true}. The + * Framework must support fragment bundles. + *

      + * The value of this property may be retrieved by calling the + * {@code BundleContext.getProperty} method. + * + * @since 1.3 + */ + String SUPPORTS_FRAMEWORK_FRAGMENT = "org.osgi.supports.framework.fragment"; + + /** + * Framework environment property identifying whether the Framework supports + * the {@link #REQUIRE_BUNDLE Require-Bundle} manifest header. + * + *

      + * As of version 1.4, the value of this property must be {@code true}. The + * Framework must support the {@code Require-Bundle} manifest header. + *

      + * The value of this property may be retrieved by calling the + * {@code BundleContext.getProperty} method. + * + * @since 1.3 + */ + String SUPPORTS_FRAMEWORK_REQUIREBUNDLE = "org.osgi.supports.framework.requirebundle"; + + /** + * Framework launching property specifying the type of security manager the + * framework must use. If not specified then the framework will not set the + * VM security manager. + * + * @see #FRAMEWORK_SECURITY_OSGI + * @since 1.5 + */ + String FRAMEWORK_SECURITY = "org.osgi.framework.security"; + + /** + * Specifies that a security manager that supports all security aspects of + * the OSGi core specification including postponed conditions must be + * installed. + * + *

      + * If this value is specified and there is a security manager already + * installed, then a {@code SecurityException} must be thrown when the + * Framework is initialized. + * + * @see #FRAMEWORK_SECURITY + * @since 1.5 + */ + String FRAMEWORK_SECURITY_OSGI = "osgi"; + + /** + * Framework launching property specifying the persistent storage area used + * by the framework. The value of this property must be a valid file path in + * the file system to a directory. If the specified directory does not exist + * then the framework will create the directory. If the specified path + * exists but is not a directory or if the framework fails to create the + * storage directory, then framework initialization must fail. The framework + * is free to use this directory as it sees fit. This area can not be shared + * with anything else. + *

      + * If this property is not set, the framework should use a reasonable + * platform default for the persistent storage area. + * + * @since 1.5 + */ + String FRAMEWORK_STORAGE = "org.osgi.framework.storage"; + + /** + * Framework launching property specifying if and when the persistent + * storage area for the framework should be cleaned. If this property is not + * set, then the framework storage area must not be cleaned. + * + * @see #FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT + * @since 1.5 + */ + String FRAMEWORK_STORAGE_CLEAN = "org.osgi.framework.storage.clean"; + + /** + * Specifies that the framework storage area must be cleaned before the + * framework is initialized for the first time. Subsequent inits, starts or + * updates of the framework will not result in cleaning the framework + * storage area. + * + * @since 1.5 + */ + String FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT = "onFirstInit"; + + /** + * Framework launching property specifying a comma separated list of + * additional library file extensions that must be used when a bundle's + * class loader is searching for native libraries. If this property is not + * set, then only the library name returned by + * {@code System.mapLibraryName(String)} will be used to search. This is + * needed for certain operating systems which allow more than one extension + * for a library. For example, AIX allows library extensions of {@code .a} + * and {@code .so}, but {@code System.mapLibraryName(String)} will only + * return names with the {@code .a} extension. + * + * @since 1.5 + */ + String FRAMEWORK_LIBRARY_EXTENSIONS = "org.osgi.framework.library.extensions"; + + /** + * Framework launching property specifying an optional OS specific command + * to set file permissions on extracted native code. On some operating + * systems, it is required that native libraries be set to executable. This + * optional property allows you to specify the command. For example, on a + * UNIX style OS, this property could have the following value. + * + *

      +	 * chmod +rx ${abspath}
      +	 * 
      + * + * The ${abspath} is used by the framework to substitute the + * actual absolute file path. + * + * @since 1.5 + */ + String FRAMEWORK_EXECPERMISSION = "org.osgi.framework.command.execpermission"; + + /** + * Specified the substitution string for the absolute path of a file. + * + * @see #FRAMEWORK_EXECPERMISSION + * @since 1.6 + */ + String FRAMEWORK_COMMAND_ABSPATH = "abspath"; + + /** + * Framework launching property specifying the trust repositories used by + * the framework. The value is a {@code java.io.File.pathSeparator} + * separated list of valid file paths to files that contain key stores. Key + * stores of type {@code JKS} must be supported and other key store types + * may be supported. The framework will use the key stores as trust + * repositories to authenticate certificates of trusted signers. The key + * stores are only used as read-only trust repositories to access public + * keys. No passwords are required to access the key stores' public keys. + *

      + * Note that framework implementations are allowed to use other trust + * repositories in addition to the trust repositories specified by this + * property. How these other trust repositories are configured and populated + * is implementation specific. + * + * @since 1.5 + */ + String FRAMEWORK_TRUST_REPOSITORIES = "org.osgi.framework.trust.repositories"; + + /** + * Framework launching property specifying the current windowing system. The + * framework should provide a reasonable default if this is not set. + * + * @since 1.5 + */ + String FRAMEWORK_WINDOWSYSTEM = "org.osgi.framework.windowsystem"; + + /** + * Framework launching property specifying the beginning start level of the + * framework. + * + * @see "Core Specification, Starting the Framework." + * @since 1.5 + */ + String FRAMEWORK_BEGINNING_STARTLEVEL = "org.osgi.framework.startlevel.beginning"; + + /** + * Framework launching property specifying the parent class loader type for + * all bundle class loaders. Default value is + * {@link #FRAMEWORK_BUNDLE_PARENT_BOOT boot}. + * + * @see #FRAMEWORK_BUNDLE_PARENT_BOOT + * @see #FRAMEWORK_BUNDLE_PARENT_EXT + * @see #FRAMEWORK_BUNDLE_PARENT_APP + * @see #FRAMEWORK_BUNDLE_PARENT_FRAMEWORK + * @since 1.5 + */ + String FRAMEWORK_BUNDLE_PARENT = "org.osgi.framework.bundle.parent"; + + /** + * Specifies to use of the boot class loader as the parent class loader for + * all bundle class loaders. + * + * @since 1.5 + * @see #FRAMEWORK_BUNDLE_PARENT + */ + String FRAMEWORK_BUNDLE_PARENT_BOOT = "boot"; + + /** + * Specifies to use the extension class loader as the parent class loader + * for all bundle class loaders. + * + * @since 1.5 + * @see #FRAMEWORK_BUNDLE_PARENT + */ + String FRAMEWORK_BUNDLE_PARENT_EXT = "ext"; + + /** + * Specifies to use the application class loader as the parent class loader + * for all bundle class loaders. Depending on how the framework is launched, + * this may refer to the same class loader as + * {@link #FRAMEWORK_BUNDLE_PARENT_FRAMEWORK}. + * + * @since 1.5 + * @see #FRAMEWORK_BUNDLE_PARENT + */ + String FRAMEWORK_BUNDLE_PARENT_APP = "app"; + + /** + * Specifies to use the framework class loader as the parent class loader + * for all bundle class loaders. The framework class loader is the class + * loader used to load the framework implementation. Depending on how the + * framework is launched, this may refer to the same class loader as + * {@link #FRAMEWORK_BUNDLE_PARENT_APP}. + * + * @since 1.5 + * @see #FRAMEWORK_BUNDLE_PARENT + */ + String FRAMEWORK_BUNDLE_PARENT_FRAMEWORK = "framework"; + + /* + * Service properties. + */ + + /** + * Service property identifying all of the class names under which a service + * was registered in the Framework. The value of this property must be of + * type {@code String[]}. + * + *

      + * This property is set by the Framework when a service is registered. + */ + String OBJECTCLASS = "objectClass"; + + /** + * Service property identifying a service's registration number. The value + * of this property must be of type {@code Long}. + * + *

      + * The value of this property is assigned by the Framework when a service is + * registered. The Framework assigns a unique, non-negative value that is + * larger than all previously assigned values since the Framework was + * started. These values are NOT persistent across restarts of the + * Framework. + */ + String SERVICE_ID = "service.id"; + + /** + * Service property identifying a service's persistent identifier. + * + *

      + * This property may be supplied in the {@code properties} + * {@code Dictionary} object passed to the + * {@code BundleContext.registerService} method. The value of this property + * must be of type {@code String}, {@code String[]}, or {@code Collection} + * of {@code String}. + * + *

      + * A service's persistent identifier uniquely identifies the service and + * persists across multiple Framework invocations. + * + *

      + * By convention, every bundle has its own unique namespace, starting with + * the bundle's identifier (see {@link Bundle#getBundleId()}) and followed + * by a dot (.). A bundle may use this as the prefix of the persistent + * identifiers for the services it registers. + */ + String SERVICE_PID = "service.pid"; + + /** + * Service property identifying a service's ranking number. + * + *

      + * This property may be supplied in the {@code properties + * Dictionary} object passed to the {@code BundleContext.registerService} + * method. The value of this property must be of type {@code Integer}. + * + *

      + * The service ranking is used by the Framework to determine the natural + * order of services, see {@link ServiceReference#compareTo(Object)}, + * and the default service to be returned from a call to the + * {@link BundleContext#getServiceReference(Class)} or + * {@link BundleContext#getServiceReference(String)} method. + * + *

      + * The default ranking is zero (0). A service with a ranking of + * {@code Integer.MAX_VALUE} is very likely to be returned as the default + * service, whereas a service with a ranking of {@code Integer.MIN_VALUE} is + * very unlikely to be returned. + * + *

      + * If the supplied property value is not of type {@code Integer}, it is + * deemed to have a ranking value of zero. + */ + String SERVICE_RANKING = "service.ranking"; + + /** + * Service property identifying a service's vendor. + * + *

      + * This property may be supplied in the properties {@code Dictionary} object + * passed to the {@code BundleContext.registerService} method. + */ + String SERVICE_VENDOR = "service.vendor"; + + /** + * Service property identifying a service's description. + * + *

      + * This property may be supplied in the properties {@code Dictionary} object + * passed to the {@code BundleContext.registerService} method. + */ + String SERVICE_DESCRIPTION = "service.description"; + + /** + * Service property identifying the {@link Bundle#getBundleId() bundle id} + * of the {@link ServiceReference#getBundle() bundle registering the + * service}. + * + *

      + * This property is set by the Framework when a service is registered. The + * value of this property must be of type {@code Long}. + * + * @since 1.8 + */ + String SERVICE_BUNDLEID = "service.bundleid"; + + /** + * Service property identifying a service's scope. + * + *

      + * This property is set by the Framework when a service is registered. If + * the registered object implements {@link PrototypeServiceFactory}, then + * the value of this service property will be {@link #SCOPE_PROTOTYPE}. + * Otherwise, if the registered object implements {@link ServiceFactory}, + * then the value of this service property will be {@link #SCOPE_BUNDLE}. + * Otherwise, the value of this service property will be + * {@link #SCOPE_SINGLETON}. + * + * @since 1.8 + * @see #SCOPE_SINGLETON + * @see #SCOPE_BUNDLE + * @see #SCOPE_PROTOTYPE + */ + String SERVICE_SCOPE = "service.scope"; + + /** + * Service scope is singleton. All bundles using the service receive the + * same service object. + * + * @since 1.8 + * @see #SERVICE_SCOPE + */ + String SCOPE_SINGLETON = "singleton"; + + /** + * Service scope is bundle. Each bundle using the service receives a + * customized service object. + * + * @since 1.8 + * @see #SERVICE_SCOPE + */ + String SCOPE_BUNDLE = "bundle"; + + /** + * Service scope is prototype. Each bundle using the service receives either + * a customized service object or can request multiple customized service + * objects via {@link ServiceObjects}. + * + * @since 1.8 + * @see #SERVICE_SCOPE + */ + String SCOPE_PROTOTYPE = "prototype"; + + /** + * Framework environment property identifying the Framework's universally + * unique identifier (UUID). A UUID represents a 128-bit value. A new UUID + * is generated by the {@link Framework#init()} method each time a framework + * is initialized. The value of this property must conform to the UUID + * string representation specified in RFC 4122. + * + *

      + * The value of this property may be retrieved by calling the + * {@code BundleContext.getProperty} method. + * + * @since 1.6 + */ + String FRAMEWORK_UUID = "org.osgi.framework.uuid"; + + /** + * Service property identifying the configuration types supported by a + * distribution provider. Registered by the distribution provider on one of + * its services to indicate the supported configuration types. + * + *

      + * The value of this property must be of type {@code String}, + * {@code String[]}, or {@code Collection} of {@code String}. + * + * @since 1.6 + * @see "Remote Services Specification" + */ + String REMOTE_CONFIGS_SUPPORTED = "remote.configs.supported"; + + /** + * Service property identifying the intents supported by a distribution + * provider. Registered by the distribution provider on one of its services + * to indicate the vocabulary of implemented intents. + * + *

      + * The value of this property must be of type {@code String}, + * {@code String[]}, or {@code Collection} of {@code String}. + * + * @since 1.6 + * @see "Remote Services Specification" + */ + String REMOTE_INTENTS_SUPPORTED = "remote.intents.supported"; + + /** + * Service property identifying the configuration types that should be used + * to export the service. Each configuration type represents the + * configuration parameters for an endpoint. A distribution provider should + * create an endpoint for each configuration type that it supports. + * + *

      + * This property may be supplied in the {@code properties} + * {@code Dictionary} object passed to the + * {@code BundleContext.registerService} method. The value of this property + * must be of type {@code String}, {@code String[]}, or {@code Collection} + * of {@code String}. + * + * @since 1.6 + * @see "Remote Services Specification" + */ + String SERVICE_EXPORTED_CONFIGS = "service.exported.configs"; + + /** + * Service property identifying the intents that the distribution provider + * must implement to distribute the service. Intents listed in this property + * are reserved for intents that are critical for the code to function + * correctly, for example, ordering of messages. These intents should not be + * configurable. + * + *

      + * This property may be supplied in the {@code properties} + * {@code Dictionary} object passed to the + * {@code BundleContext.registerService} method. The value of this property + * must be of type {@code String}, {@code String[]}, or {@code Collection} + * of {@code String}. + * + * @since 1.6 + * @see "Remote Services Specification" + */ + String SERVICE_EXPORTED_INTENTS = "service.exported.intents"; + + /** + * Service property identifying the extra intents that the distribution + * provider must implement to distribute the service. This property is + * merged with the {@code service.exported.intents} property before the + * distribution provider interprets the listed intents; it has therefore the + * same semantics but the property should be configurable so the + * administrator can choose the intents based on the topology. Bundles + * should therefore make this property configurable, for example through the + * Configuration Admin service. + * + *

      + * This property may be supplied in the {@code properties} + * {@code Dictionary} object passed to the + * {@code BundleContext.registerService} method. The value of this property + * must be of type {@code String}, {@code String[]}, or {@code Collection} + * of {@code String}. + * + * @since 1.6 + * @see "Remote Services Specification" + */ + String SERVICE_EXPORTED_INTENTS_EXTRA = "service.exported.intents.extra"; + + /** + * Service property marking the service for export. It defines the + * interfaces under which this service can be exported. This list must be a + * subset of the types under which the service was registered. The single + * value of an asterisk ({@code '*'} \u002A) indicates all the interface + * types under which the service was registered excluding the non-interface + * types. It is strongly recommended to only export interface types and not + * concrete classes due to the complexity of creating proxies for some type + * of concrete classes. + * + *

      + * This property may be supplied in the {@code properties} + * {@code Dictionary} object passed to the + * {@code BundleContext.registerService} method. The value of this property + * must be of type {@code String}, {@code String[]}, or {@code Collection} + * of {@code String}. + * + * @since 1.6 + * @see "Remote Services Specification" + */ + String SERVICE_EXPORTED_INTERFACES = "service.exported.interfaces"; + + /** + * Service property identifying the service as imported. This service + * property must be set by a distribution provider to any value when it + * registers the endpoint proxy as an imported service. A bundle can use + * this property to filter out imported services. + * + *

      + * The value of this property may be of any type. + * + * @since 1.6 + * @see "Remote Services Specification" + */ + String SERVICE_IMPORTED = "service.imported"; + + /** + * Service property identifying the configuration types used to import the + * service. Any associated properties for this configuration types must be + * properly mapped to the importing system. For example, a URL in these + * properties must point to a valid resource when used in the importing + * framework. If multiple configuration types are listed in this property, + * then they must be synonyms for exactly the same remote endpoint that is + * used to export this service. + * + *

      + * The value of this property must be of type {@code String}, + * {@code String[]}, or {@code Collection} of {@code String}. + * + * @since 1.6 + * @see "Remote Services Specification" + * @see #SERVICE_EXPORTED_CONFIGS + */ + String SERVICE_IMPORTED_CONFIGS = "service.imported.configs"; + + /** + * Service property identifying the intents that this service implement. + * This property has a dual purpose: + *

        + *
      • A bundle can use this service property to notify the distribution + * provider that these intents are already implemented by the exported + * service object.
      • + *
      • A distribution provider must use this property to convey the combined + * intents of: the exporting service, the intents that the exporting + * distribution provider adds, and the intents that the importing + * distribution provider adds.
      • + *
      + * + * To export a service, a distribution provider must expand any qualified + * intents. Both the exporting and importing distribution providers must + * recognize all intents before a service can be distributed. + * + *

      + * The value of this property must be of type {@code String}, + * {@code String[]}, or {@code Collection} of {@code String}. + * + * @since 1.6 + * @see "Remote Services Specification" + */ + String SERVICE_INTENTS = "service.intents"; + + /** + * Manifest header identifying the capabilities that the bundle offers to + * provide to other bundles. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + * + * @since 1.6 + */ + String PROVIDE_CAPABILITY = "Provide-Capability"; + + /** + * Manifest header identifying the capabilities on which the bundle depends. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + * + * @since 1.6 + */ + String REQUIRE_CAPABILITY = "Require-Capability"; + + /** + * Manifest header directive identifying the effective time of the provided + * capability. The default value is {@link #EFFECTIVE_RESOLVE resolve}. + * + *

      + * The directive value is encoded in the Provide-Capability manifest header + * like: + * + *

      +	 *     Provide-Capability: com.acme.capability; effective:="resolve"
      +	 * 
      + * + * @see #PROVIDE_CAPABILITY + * @see #EFFECTIVE_RESOLVE + * @see #EFFECTIVE_ACTIVE + * @since 1.6 + */ + String EFFECTIVE_DIRECTIVE = "effective"; + + /** + * Manifest header directive value identifying a capability that is + * effective at resolve time. Capabilities with an effective time of resolve + * are the only capabilities which are processed by the resolver. + * + *

      + * The directive value is encoded in the Provide-Capability manifest header + * like: + * + *

      +	 *     Provide-Capability: com.acme.capability; effective:="resolve"
      +	 * 
      + * + * @see #EFFECTIVE_DIRECTIVE + * @since 1.6 + */ + String EFFECTIVE_RESOLVE = "resolve"; + + /** + * Manifest header directive value identifying a capability that is + * effective at active time. Capabilities with an effective time of active + * are ignored by the resolver. + * + *

      + * The directive value is encoded in the Provide-Capability manifest header + * like: + * + *

      +	 *     Provide-Capability: com.acme.capability; effective:="active"
      +	 * 
      + * + * @see #EFFECTIVE_DIRECTIVE + * @since 1.6 + */ + String EFFECTIVE_ACTIVE = "active"; + + /** + * Manifest header directive identifying the capability filter specified in + * the Require-Capability manifest header. + * + *

      + * The directive value is encoded in the Require-Capability manifest header + * like: + * + *

      +	 *     Require-Capability: com.acme.capability; filter:="(someattr=somevalue)"
      +	 * 
      + * + * @see #REQUIRE_CAPABILITY + * @since 1.6 + */ + String FILTER_DIRECTIVE = "filter"; + + /** + * Framework launching property identifying capabilities which the system + * bundle must provide. + * + *

      + * If this property is not specified then the framework must calculate a + * reasonable default value for the current execution environment. + * + *

      + * The value of this property may be retrieved by calling the + * {@code BundleContext.getProperty} method. + * + * @since 1.6 + */ + String FRAMEWORK_SYSTEMCAPABILITIES = "org.osgi.framework.system.capabilities"; + + /** + * Framework launching property identifying extra capabilities which the + * system bundle must additionally provide. + * + *

      + * This property is useful for configuring extra system capabilities in + * addition to the system capabilities calculated by the framework. + * + *

      + * The value of this property may be retrieved by calling the + * {@code BundleContext.getProperty} method. + * + * @see #FRAMEWORK_SYSTEMCAPABILITIES + * @since 1.6 + */ + String FRAMEWORK_SYSTEMCAPABILITIES_EXTRA = "org.osgi.framework.system.capabilities.extra"; + + /** + * Framework launching property specifying whether multiple bundles having + * the same {@link #BUNDLE_SYMBOLICNAME symbolic name} and + * {@link #BUNDLE_VERSION version} may be installed. + * + *

      + * Default value is {@link #FRAMEWORK_BSNVERSION_MANAGED managed} in this + * release of the specification. This default may change in a future + * specification release. Therefore, code must not assume the default + * behavior is {@code managed} and should interrogate the value of this + * property to determine the behavior. + * + *

      + * The value of this property may be retrieved by calling the + * {@code BundleContext.getProperty} method. + * + * @see #FRAMEWORK_BSNVERSION_MULTIPLE + * @see #FRAMEWORK_BSNVERSION_SINGLE + * @see #FRAMEWORK_BSNVERSION_MANAGED + * @since 1.6 + */ + String FRAMEWORK_BSNVERSION = "org.osgi.framework.bsnversion"; + + /** + * Specifies the framework will allow multiple bundles to be installed + * having the same symbolic name and version. + * + * @since 1.6 + * @see #FRAMEWORK_BSNVERSION + */ + String FRAMEWORK_BSNVERSION_MULTIPLE = "multiple"; + + /** + * Specifies the framework will only allow a single bundle to be installed + * for a given symbolic name and version. It will be an error to install a + * bundle or update a bundle to have the same symbolic name and version as + * another installed bundle. + * + * @since 1.6 + * @see #FRAMEWORK_BSNVERSION + * @see BundleException#DUPLICATE_BUNDLE_ERROR + */ + String FRAMEWORK_BSNVERSION_SINGLE = "single"; + + /** + * Specifies the framework must consult the {@link CollisionHook bundle + * collision hook} services to determine if it will be an error to install a + * bundle or update a bundle to have the same symbolic name and version as + * another installed bundle. If no bundle collision hook services are + * registered, then it will be an error to install a bundle or update a + * bundle to have the same symbolic name and version as another installed + * bundle. + * + * @since 1.7 + * @see #FRAMEWORK_BSNVERSION + * @see BundleException#DUPLICATE_BUNDLE_ERROR + */ + String FRAMEWORK_BSNVERSION_MANAGED = "managed"; + + /** + * Manifest header identifying the bundle's icon URLs. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + * + * @since 1.8 + */ + String BUNDLE_ICON = "Bundle-Icon"; + + /** + * Manifest header identifying the bundle's license information. + * + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + * + * @since 1.8 + */ + String BUNDLE_LICENSE = "Bundle-License"; + + /** + * Manifest header identifying the bundle's developers. + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + * + * @since 1.9 + */ + String BUNDLE_DEVELOPERS = "Bundle-Developers"; + + /** + * Manifest header identifying the bundle's software configuration + * management system. + *

      + * The header value may be retrieved from the {@code Dictionary} object + * returned by the {@code Bundle.getHeaders} method. + * + * @since 1.9 + */ + String BUNDLE_SCM = "Bundle-SCM"; + + /** + * Service property identifying the monotonically increasing change count of + * a service. + *

      + * A service may optional provide this property to indicate there has been a + * change in some data provided by the service. The change count must be + * incremented with a positive value every time the data provided by the + * service is changed. The service announces the modified change count by + * updating its service properties with the new value for this service + * property. + *

      + * The value of this property must be of type {@code Long}. + * + * @since 1.9 + */ + String SERVICE_CHANGECOUNT = "service.changecount"; + + /** + * Intent supported by Remote Services implementations that support Basic + * Remote Services as defined for the {@code osgi.basic} intent. + * + * @since 1.9 + */ + String INTENT_BASIC = "osgi.basic"; + + /** + * Intent supported by Remote Service implementations that support + * Asynchronous Remote Services as defined for the {@code osgi.async} + * intent. + * + * @since 1.9 + */ + String INTENT_ASYNC = "osgi.async"; + + /** + * Intent supported by Remote Service implementation that provide + * confidential communications as defined for the {@code osgi.confidential} + * intent. + * + * @since 1.9 + */ + String INTENT_CONFIDENTIAL = "osgi.confidential"; + + /** + * Intent supported by Remote Service implementations that provide private + * communications as defined for the {@code osgi.private} intent. + * + * @since 1.9 + */ + String INTENT_PRIVATE = "osgi.private"; +} diff --git a/framework/src/main/java/org/osgi/framework/Filter.java b/framework/src/main/java/org/osgi/framework/Filter.java new file mode 100644 index 00000000000..6c0580953f8 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/Filter.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2015). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import java.util.Dictionary; +import java.util.Map; +import org.osgi.annotation.versioning.ProviderType; + +/** + * An RFC 1960-based Filter. + *

      + * {@code Filter}s can be created by calling + * {@link BundleContext#createFilter(String)} or + * {@link FrameworkUtil#createFilter(String)} with a filter string. + *

      + * A {@code Filter} can be used numerous times to determine if the match + * argument matches the filter string that was used to create the {@code Filter}. + *

      + * Some examples of LDAP filters are: + * + *

      + *  "(cn=Babs Jensen)"
      + *  "(!(cn=Tim Howes))"
      + *  "(&(" + Constants.OBJECTCLASS + "=Person)(|(sn=Jensen)(cn=Babs J*)))"
      + *  "(o=univ*of*mich*)"
      + * 
      + * + * @since 1.1 + * @see "Core Specification, Filters, for a description of the filter string syntax." + * @ThreadSafe + * @author $Id: b8bf786f2bde901997f24c6f1bcff61320a533fe $ + */ +@ProviderType +public interface Filter { + /** + * Filter using a service's properties. + *

      + * This {@code Filter} is executed using the keys and values of the + * referenced service's properties. The keys are looked up in a case + * insensitive manner. + * + * @param reference The reference to the service whose properties are used + * in the match. + * @return {@code true} if the service's properties match this + * {@code Filter}; {@code false} otherwise. + */ + boolean match(ServiceReference reference); + + /** + * Filter using a {@code Dictionary} with case insensitive key lookup. This + * {@code Filter} is executed using the specified {@code Dictionary}'s keys + * and values. The keys are looked up in a case insensitive manner. + * + * @param dictionary The {@code Dictionary} whose key/value pairs are used + * in the match. + * @return {@code true} if the {@code Dictionary}'s values match this + * filter; {@code false} otherwise. + * @throws IllegalArgumentException If {@code dictionary} contains case + * variants of the same key name. + */ + boolean match(Dictionary dictionary); + + /** + * Returns this {@code Filter}'s filter string. + *

      + * The filter string is normalized by removing whitespace which does not + * affect the meaning of the filter. + * + * @return This {@code Filter}'s filter string. + */ + @Override + String toString(); + + /** + * Compares this {@code Filter} to another {@code Filter}. + * + *

      + * This implementation returns the result of calling + * {@code this.toString().equals(obj.toString())}. + * + * @param obj The object to compare against this {@code Filter}. + * @return If the other object is a {@code Filter} object, then returns the + * result of calling {@code this.toString().equals(obj.toString())}; + * {@code false} otherwise. + */ + @Override + boolean equals(Object obj); + + /** + * Returns the hashCode for this {@code Filter}. + * + *

      + * This implementation returns the result of calling + * {@code this.toString().hashCode()}. + * + * @return The hashCode of this {@code Filter}. + */ + @Override + int hashCode(); + + /** + * Filter using a {@code Dictionary}. This {@code Filter} is executed using + * the specified {@code Dictionary}'s keys and values. The keys are looked + * up in a normal manner respecting case. + * + * @param dictionary The {@code Dictionary} whose key/value pairs are used + * in the match. + * @return {@code true} if the {@code Dictionary}'s values match this + * filter; {@code false} otherwise. + * @since 1.3 + */ + boolean matchCase(Dictionary dictionary); + + /** + * Filter using a {@code Map}. This {@code Filter} is executed using the + * specified {@code Map}'s keys and values. The keys are looked up in a + * normal manner respecting case. + * + * @param map The {@code Map} whose key/value pairs are used in the match. + * Maps with {@code null} key or values are not supported. A + * {@code null} value is considered not present to the filter. + * @return {@code true} if the {@code Map}'s values match this filter; + * {@code false} otherwise. + * @since 1.6 + */ + boolean matches(Map map); +} diff --git a/framework/src/main/java/org/osgi/framework/FrameworkEvent.java b/framework/src/main/java/org/osgi/framework/FrameworkEvent.java new file mode 100644 index 00000000000..84c63f1b7c5 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/FrameworkEvent.java @@ -0,0 +1,258 @@ +/* + * Copyright (c) OSGi Alliance (2004, 2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import java.util.EventObject; + +import org.osgi.framework.startlevel.FrameworkStartLevel; +import org.osgi.framework.wiring.FrameworkWiring; + +/** + * A general event from the Framework. + * + *

      + * {@code FrameworkEvent} objects are delivered to {@code FrameworkListener}s + * when a general event occurs within the OSGi environment. A type code is used + * to identify the event type for future extendability. + * + *

      + * OSGi Alliance reserves the right to extend the set of event types. + * + * @Immutable + * @see FrameworkListener + * @author $Id: b3072b2d058e70389a52e342ed5f8647b930b8f1 $ + */ + +public class FrameworkEvent extends EventObject { + static final long serialVersionUID = 207051004521261705L; + /** + * Bundle related to the event. + */ + private final Bundle bundle; + + /** + * Exception related to the event. + */ + private final Throwable throwable; + + /** + * Type of event. + */ + private final int type; + + /** + * The Framework has started. + * + *

      + * This event is fired when the Framework has started after all installed + * bundles that are marked to be started have been started and the Framework + * has reached the initial start level. The source of this event is the + * System Bundle. + * + * @see "The Start Level Specification" + */ + public final static int STARTED = 0x00000001; + + /** + * An error has occurred. + * + *

      + * There was an error associated with a bundle. + */ + public final static int ERROR = 0x00000002; + + /** + * A FrameworkWiring.refreshBundles operation has completed. + * + *

      + * This event is fired when the Framework has completed the refresh bundles + * operation initiated by a call to the FrameworkWiring.refreshBundles + * method. The source of this event is the System Bundle. + * + * @since 1.2 + * @see FrameworkWiring#refreshBundles(java.util.Collection, + * FrameworkListener...) + */ + public final static int PACKAGES_REFRESHED = 0x00000004; + + /** + * A FrameworkStartLevel.setStartLevel operation has completed. + * + *

      + * This event is fired when the Framework has completed changing the active + * start level initiated by a call to the StartLevel.setStartLevel method. + * The source of this event is the System Bundle. + * + * @since 1.2 + * @see FrameworkStartLevel#setStartLevel(int, FrameworkListener...) + */ + public final static int STARTLEVEL_CHANGED = 0x00000008; + + /** + * A warning has occurred. + * + *

      + * There was a warning associated with a bundle. + * + * @since 1.3 + */ + public final static int WARNING = 0x00000010; + + /** + * An informational event has occurred. + * + *

      + * There was an informational event associated with a bundle. + * + * @since 1.3 + */ + public final static int INFO = 0x00000020; + + /** + * The Framework has stopped. + * + *

      + * This event is fired when the Framework has been stopped because of a stop + * operation on the system bundle. The source of this event is the System + * Bundle. + * + * @since 1.5 + */ + public final static int STOPPED = 0x00000040; + + /** + * The Framework has stopped during update. + * + *

      + * This event is fired when the Framework has been stopped because of an + * update operation on the system bundle. The Framework will be restarted + * after this event is fired. The source of this event is the System Bundle. + * + * @since 1.5 + */ + public final static int STOPPED_UPDATE = 0x00000080; + + /** + * The Framework has stopped and the boot class path has changed. + * + *

      + * This event is fired when the Framework has been stopped because of a stop + * operation on the system bundle and a bootclasspath extension bundle has + * been installed or updated. The source of this event is the System Bundle. + * + * @since 1.5 + */ + public final static int STOPPED_BOOTCLASSPATH_MODIFIED = 0x00000100; + + /** + * The Framework did not stop before the wait timeout expired. + * + *

      + * This event is fired when the Framework did not stop before the wait + * timeout expired. The source of this event is the System Bundle. + * + * @since 1.5 + */ + public final static int WAIT_TIMEDOUT = 0x00000200; + + /** + * The Framework has stopped and the framework requires a new class loader + * to restart. + *

      + * This event is fired when the Framework has been stopped because of a stop + * operation on the system bundle and the framework requires a new class + * loader to be used to restart. For example, if a framework extension + * bundle has been refreshed. The source of this event is the System Bundle. + * + * @since 1.9 + */ + public final static int STOPPED_SYSTEM_REFRESHED = 0x00000400; + + /** + * Creates a Framework event. + * + * @param type The event type. + * @param source The event source object. This may not be {@code null}. + * @deprecated As of 1.2. This constructor is deprecated in favor of using + * the other constructor with the System Bundle as the event + * source. + */ + public FrameworkEvent(int type, Object source) { + super(source); + this.type = type; + this.bundle = null; + this.throwable = null; + } + + /** + * Creates a Framework event regarding the specified bundle. + * + * @param type The event type. + * @param bundle The event source. + * @param throwable The related exception. This argument may be {@code null} + * if there is no related exception. + */ + public FrameworkEvent(int type, Bundle bundle, Throwable throwable) { + super(bundle); + this.type = type; + this.bundle = bundle; + this.throwable = throwable; + } + + /** + * Returns the exception related to this event. + * + * @return The related exception or {@code null} if none. + */ + public Throwable getThrowable() { + return throwable; + } + + /** + * Returns the bundle associated with the event. This bundle is also the + * source of the event. + * + * @return The bundle associated with the event. + */ + public Bundle getBundle() { + return bundle; + } + + /** + * Returns the type of framework event. + *

      + * The type values are: + *

        + *
      • {@link #STARTED}
      • + *
      • {@link #ERROR}
      • + *
      • {@link #WARNING}
      • + *
      • {@link #INFO}
      • + *
      • {@link #PACKAGES_REFRESHED}
      • + *
      • {@link #STARTLEVEL_CHANGED}
      • + *
      • {@link #STOPPED}
      • + *
      • {@link #STOPPED_BOOTCLASSPATH_MODIFIED}
      • + *
      • {@link #STOPPED_UPDATE}
      • + *
      • {@link #WAIT_TIMEDOUT}
      • + *
      + * + * @return The type of state change. + */ + + public int getType() { + return type; + } +} diff --git a/framework/src/main/java/org/osgi/framework/FrameworkListener.java b/framework/src/main/java/org/osgi/framework/FrameworkListener.java new file mode 100644 index 00000000000..e4c8a91b6e8 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/FrameworkListener.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2015). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import java.util.EventListener; +import org.osgi.annotation.versioning.ConsumerType; + +/** + * A {@code FrameworkEvent} listener. {@code FrameworkListener} is a listener + * interface that may be implemented by a bundle developer. When a + * {@code FrameworkEvent} is fired, it is asynchronously delivered to a + * {@code FrameworkListener}. The Framework delivers {@code FrameworkEvent} + * objects to a {@code FrameworkListener} in order and must not concurrently + * call a {@code FrameworkListener}. + * + *

      + * A {@code FrameworkListener} object is registered with the Framework using the + * {@link BundleContext#addFrameworkListener(FrameworkListener)} method. + * {@code FrameworkListener} objects are called with a {@code FrameworkEvent} + * objects when the Framework starts and when asynchronous errors occur. + * + * @see FrameworkEvent + * @NotThreadSafe + * @author $Id: a8e5255b2b18c9ba60cf82d6e16a296667017399 $ + */ +@ConsumerType +@FunctionalInterface +public interface FrameworkListener extends EventListener { + + /** + * Receives notification of a general {@code FrameworkEvent} object. + * + * @param event The {@code FrameworkEvent} object. + */ + public void frameworkEvent(FrameworkEvent event); +} diff --git a/framework/src/main/java/org/osgi/framework/FrameworkUtil.java b/framework/src/main/java/org/osgi/framework/FrameworkUtil.java index 501d6b6f498..9f720927b16 100644 --- a/framework/src/main/java/org/osgi/framework/FrameworkUtil.java +++ b/framework/src/main/java/org/osgi/framework/FrameworkUtil.java @@ -1,39 +1,2158 @@ /* - * Copyright 2006 The Apache Software Foundation + * Copyright (c) OSGi Alliance (2005, 2016). All Rights Reserved. * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * http://www.apache.org/licenses/LICENSE-2.0 * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ + package org.osgi.framework; -import org.apache.felix.framework.FilterImpl; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.security.auth.x500.X500Principal; /** + * Framework Utility class. + * *

      - * Framework utility class that currently only provides public access to - * Filter instance creation. This class is a replacement for the - * version that ships with the standard OSGi JAR file. - *

      -**/ -public class FrameworkUtil -{ - private FrameworkUtil() - { - } - - public static Filter createFilter(String filter) - throws InvalidSyntaxException - { - return new FilterImpl(null, filter); - } -} \ No newline at end of file + * This class contains utility methods which access Framework functions that may + * be useful to bundles. + * + * @since 1.3 + * @ThreadSafe + * @author $Id: 90d50e4d3f69b659bed23beedab6e54b31b96d76 $ + */ +public class FrameworkUtil { + /** + * FrameworkUtil objects may not be constructed. + */ + private FrameworkUtil() { + // private empty constructor to prevent construction + } + + /** + * Creates a {@code Filter} object. This {@code Filter} object may be used + * to match a {@code ServiceReference} object or a {@code Dictionary} + * object. + * + *

      + * If the filter cannot be parsed, an {@link InvalidSyntaxException} will be + * thrown with a human readable message where the filter became unparsable. + * + *

      + * This method returns a Filter implementation which may not perform as well + * as the framework implementation-specific Filter implementation returned + * by {@link BundleContext#createFilter(String)}. + * + * @param filter The filter string. + * @return A {@code Filter} object encapsulating the filter string. + * @throws InvalidSyntaxException If {@code filter} contains an invalid + * filter string that cannot be parsed. + * @throws NullPointerException If {@code filter} is null. + * + * @see Filter + */ + public static Filter createFilter(String filter) throws InvalidSyntaxException { + return FilterImpl.newInstance(filter); + } + + /** + * Match a Distinguished Name (DN) chain against a pattern. DNs can be + * matched using wildcards. A wildcard ({@code '*'} \u002A) replaces all + * possible values. Due to the structure of the DN, the comparison is more + * complicated than string-based wildcard matching. + *

      + * A wildcard can stand for zero or more DNs in a chain, a number of + * relative distinguished names (RDNs) within a DN, or the value of a single + * RDN. The DNs in the chain and the matching pattern are canonicalized + * before processing. This means, among other things, that spaces must be + * ignored, except in values. + *

      + * The format of a wildcard match pattern is: + * + *

      +	 * matchPattern ::= dn-match ( ';' dn-match ) *
      +	 * dn-match     ::= ( '*' | rdn-match ) ( ',' rdn-match ) * | '-'
      +	 * rdn-match    ::= name '=' value-match
      +	 * value-match  ::= '*' | value-star
      +	 * value-star   ::= < value, requires escaped '*' and '-' >
      +	 * 
      + *

      + * The most simple case is a single wildcard; it must match any DN. A + * wildcard can also replace the first list of RDNs of a DN. The first RDNs + * are the least significant. Such lists of matched RDNs can be empty. + *

      + * For example, a match pattern with a wildcard that matches all DNs that + * end with RDNs of o=ACME and c=US would look like this: + * + *

      +	 * *, o=ACME, c=US
      +	 * 
      + * + * This match pattern would match the following DNs: + * + *
      +	 * cn = Bugs Bunny, o = ACME, c = US
      +	 * ou = Carrots, cn=Daffy Duck, o=ACME, c=US
      +	 * street = 9C\, Avenue St. Drézéry, o=ACME, c=US
      +	 * dc=www, dc=acme, dc=com, o=ACME, c=US
      +	 * o=ACME, c=US
      +	 * 
      + * + * The following DNs would not match: + * + *
      +	 * street = 9C\, Avenue St. Drézéry, o=ACME, c=FR
      +	 * dc=www, dc=acme, dc=com, c=US
      +	 * 
      + * + * If a wildcard is used for a value of an RDN, the value must be exactly *. + * The wildcard must match any value, and no substring matching must be + * done. For example: + * + *
      +	 * cn=*,o=ACME,c=*
      +	 * 
      + * + * This match pattern with wildcard must match the following DNs: + * + *
      +	 * cn=Bugs Bunny,o=ACME,c=US
      +	 * cn = Daffy Duck , o = ACME , c = US
      +	 * cn=Road Runner, o=ACME, c=NL
      +	 * 
      + * + * But not: + * + *
      +	 * o=ACME, c=NL
      +	 * dc=acme.com, cn=Bugs Bunny, o=ACME, c=US
      +	 * 
      + * + *

      + * A match pattern may contain a chain of DN match patterns. The semicolon( + * {@code ';'} \u003B) must be used to separate DN match patterns in a + * chain. Wildcards can also be used to match against a complete DN within a + * chain. + *

      + * The following example matches a certificate signed by Tweety Inc. in the + * US. + *

      + * + *
      +	 * * ; ou=S & V, o=Tweety Inc., c=US
      +	 * 
      + *

      + * The wildcard ('*') matches zero or one DN in the chain, however, + * sometimes it is necessary to match a longer chain. The minus sign ( + * {@code '-'} \u002D) represents zero or more DNs, whereas the asterisk + * only represents a single DN. For example, to match a DN where the Tweety + * Inc. is in the DN chain, use the following expression: + *

      + * + *
      +	 * - ; *, o=Tweety Inc., c=US
      +	 * 
      + * + * @param matchPattern The pattern against which to match the DN chain. + * @param dnChain The DN chain to match against the specified pattern. Each + * element of the chain must be of type {@code String} and use the + * format defined in RFC 2253. + * @return {@code true} If the pattern matches the DN chain; otherwise + * {@code false} is returned. + * @throws IllegalArgumentException If the specified match pattern or DN + * chain is invalid. + * @since 1.5 + */ + public static boolean matchDistinguishedNameChain(String matchPattern, List dnChain) { + return DNChainMatching.match(matchPattern, dnChain); + } + + /** + * Return a {@code Bundle} for the specified bundle class. The returned + * {@code Bundle} is the bundle associated with the bundle class loader + * which defined the specified class. + * + * @param classFromBundle A class defined by a bundle class loader. + * @return A {@code Bundle} for the specified bundle class or {@code null} + * if the specified class was not defined by a bundle class loader. + * @since 1.5 + */ + public static Bundle getBundle(final Class classFromBundle) { + // We use doPriv since the caller may not have permission + // to call getClassLoader. + Object cl = AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Object run() { + return classFromBundle.getClassLoader(); + } + }); + + if (cl instanceof BundleReference) { + return ((BundleReference) cl).getBundle(); + } + return null; + } + + /** + * RFC 1960-based Filter. Filter objects can be created by calling the + * constructor with the desired filter string. A Filter object can be called + * numerous times to determine if the match argument matches the filter + * string that was used to create the Filter object. + * + *

      + * The syntax of a filter string is the string representation of LDAP search + * filters as defined in RFC 1960: A String Representation of LDAP Search + * Filters (available at http://www.ietf.org/rfc/rfc1960.txt). It should + * be noted that RFC 2254: A String Representation of LDAP Search + * Filters (available at http://www.ietf.org/rfc/rfc2254.txt) supersedes + * RFC 1960 but only adds extensible matching and is not applicable for this + * API. + * + *

      + * The string representation of an LDAP search filter is defined by the + * following grammar. It uses a prefix format. + * + *

      +	 *   <filter> ::= '(' <filtercomp> ')'
      +	 *   <filtercomp> ::= <and> | <or> | <not> | <item>
      +	 *   <and> ::= '&' <filterlist>
      +	 *   <or> ::= '|' <filterlist>
      +	 *   <not> ::= '!' <filter>
      +	 *   <filterlist> ::= <filter> | <filter> <filterlist>
      +	 *   <item> ::= <simple> | <present> | <substring>
      +	 *   <simple> ::= <attr> <filtertype> <value>
      +	 *   <filtertype> ::= <equal> | <approx> | <greater> | <less>
      +	 *   <equal> ::= '='
      +	 *   <approx> ::= '˜='
      +	 *   <greater> ::= '>='
      +	 *   <less> ::= '<='
      +	 *   <present> ::= <attr> '=*'
      +	 *   <substring> ::= <attr> '=' <initial> <any> <final>
      +	 *   <initial> ::= NULL | <value>
      +	 *   <any> ::= '*' <starval>
      +	 *   <starval> ::= NULL | <value> '*' <starval>
      +	 *   <final> ::= NULL | <value>
      +	 * 
      + * + * {@code <attr>} is a string representing an attribute, or key, in + * the properties objects of the registered services. Attribute names are + * not case sensitive; that is cn and CN both refer to the same attribute. + * {@code <value>} is a string representing the value, or part of one, + * of a key in the properties objects of the registered services. If a + * {@code <value>} must contain one of the characters ' {@code *}' or + * '{@code (}' or '{@code )}', these characters should be escaped by + * preceding them with the backslash '{@code \}' character. Note that + * although both the {@code <substring>} and {@code <present>} + * productions can produce the {@code 'attr=*'} construct, this construct is + * used only to denote a presence filter. + * + *

      + * Examples of LDAP filters are: + * + *

      +	 *   "(cn=Babs Jensen)"
      +	 *   "(!(cn=Tim Howes))"
      +	 *   "(&(" + Constants.OBJECTCLASS + "=Person)(|(sn=Jensen)(cn=Babs J*)))"
      +	 *   "(o=univ*of*mich*)"
      +	 * 
      + * + *

      + * The approximate match ({@code ~=}) is implementation specific but should + * at least ignore case and white space differences. Optional are codes like + * soundex or other smart "closeness" comparisons. + * + *

      + * Comparison of values is not straightforward. Strings are compared + * differently than numbers and it is possible for a key to have multiple + * values. Note that that keys in the match argument must always be strings. + * The comparison is defined by the object type of the key's value. The + * following rules apply for comparison: + * + *

      + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
      Property Value Type Comparison Type
      StringString comparison
      Integer, Long, Float, Double, Byte, Short, BigInteger, BigDecimalnumerical comparison
      Charactercharacter comparison
      Booleanequality comparisons only
      [] (array)recursively applied to values
      Collectionrecursively applied to values
      + * Note: arrays of primitives are also supported.
      + * + * A filter matches a key that has multiple values if it matches at least + * one of those values. For example, + * + *
      +	 * Dictionary d = new Hashtable();
      +	 * d.put("cn", new String[] {"a", "b", "c"});
      +	 * 
      + * + * d will match {@code (cn=a)} and also {@code (cn=b)} + * + *

      + * A filter component that references a key having an unrecognizable data + * type will evaluate to {@code false} . + */ + static private final class FilterImpl implements Filter { + /* filter operators */ + private static final int EQUAL = 1; + private static final int APPROX = 2; + private static final int GREATER = 3; + private static final int LESS = 4; + private static final int PRESENT = 5; + private static final int SUBSTRING = 6; + private static final int AND = 7; + private static final int OR = 8; + private static final int NOT = 9; + + /** filter operation */ + private final int op; + /** filter attribute or null if operation AND, OR or NOT */ + private final String attr; + /** filter operands */ + private final Object value; + + /* normalized filter string for Filter object */ + private transient String filterString; + + /** + * Constructs a {@link FilterImpl} object. This filter object may be + * used to match a {@link ServiceReference} or a Dictionary. + * + *

      + * If the filter cannot be parsed, an {@link InvalidSyntaxException} + * will be thrown with a human readable message where the filter became + * unparsable. + * + * @param filterString the filter string. + * @throws InvalidSyntaxException If the filter parameter contains an + * invalid filter string that cannot be parsed. + */ + static FilterImpl newInstance(String filterString) throws InvalidSyntaxException { + return new Parser(filterString).parse(); + } + + FilterImpl(int operation, String attr, Object value) { + this.op = operation; + this.attr = attr; + this.value = value; + filterString = null; + } + + /** + * Filter using a service's properties. + *

      + * This {@code Filter} is executed using the keys and values of the + * referenced service's properties. The keys are looked up in a case + * insensitive manner. + * + * @param reference The reference to the service whose properties are + * used in the match. + * @return {@code true} if the service's properties match this + * {@code Filter}; {@code false} otherwise. + */ + @Override + public boolean match(ServiceReference reference) { + return matches(new ServiceReferenceMap(reference)); + } + + /** + * Filter using a {@code Dictionary} with case insensitive key lookup. + * This {@code Filter} is executed using the specified + * {@code Dictionary}'s keys and values. The keys are looked up in a + * case insensitive manner. + * + * @param dictionary The {@code Dictionary} whose key/value pairs are + * used in the match. + * @return {@code true} if the {@code Dictionary}'s values match this + * filter; {@code false} otherwise. + * @throws IllegalArgumentException If {@code dictionary} contains case + * variants of the same key name. + */ + @Override + public boolean match(Dictionary dictionary) { + return matches(new CaseInsensitiveMap(dictionary)); + } + + /** + * Filter using a {@code Dictionary}. This {@code Filter} is executed + * using the specified {@code Dictionary}'s keys and values. The keys + * are looked up in a normal manner respecting case. + * + * @param dictionary The {@code Dictionary} whose key/value pairs are + * used in the match. + * @return {@code true} if the {@code Dictionary}'s values match this + * filter; {@code false} otherwise. + * @since 1.3 + */ + @Override + public boolean matchCase(Dictionary dictionary) { + switch (op) { + case AND : { + FilterImpl[] filters = (FilterImpl[]) value; + for (FilterImpl f : filters) { + if (!f.matchCase(dictionary)) { + return false; + } + } + return true; + } + + case OR : { + FilterImpl[] filters = (FilterImpl[]) value; + for (FilterImpl f : filters) { + if (f.matchCase(dictionary)) { + return true; + } + } + return false; + } + + case NOT : { + FilterImpl filter = (FilterImpl) value; + return !filter.matchCase(dictionary); + } + + case SUBSTRING : + case EQUAL : + case GREATER : + case LESS : + case APPROX : { + Object prop = (dictionary == null) ? null : dictionary.get(attr); + return compare(op, prop, value); + } + + case PRESENT : { + Object prop = (dictionary == null) ? null : dictionary.get(attr); + return prop != null; + } + } + + return false; + } + + /** + * Filter using a {@code Map}. This {@code Filter} is executed using the + * specified {@code Map}'s keys and values. The keys are looked up in a + * normal manner respecting case. + * + * @param map The {@code Map} whose key/value pairs are used in the + * match. Maps with {@code null} key or values are not supported. + * A {@code null} value is considered not present to the filter. + * @return {@code true} if the {@code Map}'s values match this filter; + * {@code false} otherwise. + * @since 1.6 + */ + @Override + public boolean matches(Map map) { + switch (op) { + case AND : { + FilterImpl[] filters = (FilterImpl[]) value; + for (FilterImpl f : filters) { + if (!f.matches(map)) { + return false; + } + } + return true; + } + + case OR : { + FilterImpl[] filters = (FilterImpl[]) value; + for (FilterImpl f : filters) { + if (f.matches(map)) { + return true; + } + } + return false; + } + + case NOT : { + FilterImpl filter = (FilterImpl) value; + return !filter.matches(map); + } + + case SUBSTRING : + case EQUAL : + case GREATER : + case LESS : + case APPROX : { + Object prop = (map == null) ? null : map.get(attr); + return compare(op, prop, value); + } + + case PRESENT : { + Object prop = (map == null) ? null : map.get(attr); + return prop != null; + } + } + + return false; + } + + /** + * Returns this {@code Filter}'s filter string. + *

      + * The filter string is normalized by removing whitespace which does not + * affect the meaning of the filter. + * + * @return This {@code Filter}'s filter string. + */ + @Override + public String toString() { + String result = filterString; + if (result == null) { + filterString = result = normalize().toString(); + } + return result; + } + + /** + * Returns this {@code Filter}'s normalized filter string. + *

      + * The filter string is normalized by removing whitespace which does not + * affect the meaning of the filter. + * + * @return This {@code Filter}'s filter string. + */ + private StringBuilder normalize() { + StringBuilder sb = new StringBuilder(); + sb.append('('); + + switch (op) { + case AND : { + sb.append('&'); + + FilterImpl[] filters = (FilterImpl[]) value; + for (FilterImpl f : filters) { + sb.append(f.normalize()); + } + + break; + } + + case OR : { + sb.append('|'); + + FilterImpl[] filters = (FilterImpl[]) value; + for (FilterImpl f : filters) { + sb.append(f.normalize()); + } + + break; + } + + case NOT : { + sb.append('!'); + FilterImpl filter = (FilterImpl) value; + sb.append(filter.normalize()); + + break; + } + + case SUBSTRING : { + sb.append(attr); + sb.append('='); + + String[] substrings = (String[]) value; + + for (String substr : substrings) { + if (substr == null) /* * */{ + sb.append('*'); + } else /* xxx */{ + sb.append(encodeValue(substr)); + } + } + + break; + } + case EQUAL : { + sb.append(attr); + sb.append('='); + sb.append(encodeValue((String) value)); + + break; + } + case GREATER : { + sb.append(attr); + sb.append(">="); + sb.append(encodeValue((String) value)); + + break; + } + case LESS : { + sb.append(attr); + sb.append("<="); + sb.append(encodeValue((String) value)); + + break; + } + case APPROX : { + sb.append(attr); + sb.append("~="); + sb.append(encodeValue(approxString((String) value))); + + break; + } + + case PRESENT : { + sb.append(attr); + sb.append("=*"); + + break; + } + } + + sb.append(')'); + + return sb; + } + + /** + * Compares this {@code Filter} to another {@code Filter}. + * + *

      + * This implementation returns the result of calling + * {@code this.toString().equals(obj.toString()}. + * + * @param obj The object to compare against this {@code Filter}. + * @return If the other object is a {@code Filter} object, then returns + * the result of calling + * {@code this.toString().equals(obj.toString()}; {@code false} + * otherwise. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (!(obj instanceof Filter)) { + return false; + } + + return this.toString().equals(obj.toString()); + } + + /** + * Returns the hashCode for this {@code Filter}. + * + *

      + * This implementation returns the result of calling + * {@code this.toString().hashCode()}. + * + * @return The hashCode of this {@code Filter}. + */ + @Override + public int hashCode() { + return this.toString().hashCode(); + } + + /** + * Encode the value string such that '(', '*', ')' and '\' are escaped. + * + * @param value unencoded value string. + * @return encoded value string. + */ + private static String encodeValue(String value) { + boolean encoded = false; + int inlen = value.length(); + int outlen = inlen << 1; /* inlen 2 */ + + char[] output = new char[outlen]; + value.getChars(0, inlen, output, inlen); + + int cursor = 0; + for (int i = inlen; i < outlen; i++) { + char c = output[i]; + + switch (c) { + case '(' : + case '*' : + case ')' : + case '\\' : { + output[cursor] = '\\'; + cursor++; + encoded = true; + + break; + } + } + + output[cursor] = c; + cursor++; + } + + return encoded ? new String(output, 0, cursor) : value; + } + + private boolean compare(int operation, Object value1, Object value2) { + if (value1 == null) { + return false; + } + if (value1 instanceof String) { + return compare_String(operation, (String) value1, value2); + } + if (value1 instanceof Version) { + return compare_Version(operation, (Version) value1, value2); + } + + Class clazz = value1.getClass(); + if (clazz.isArray()) { + Class type = clazz.getComponentType(); + if (type.isPrimitive()) { + return compare_PrimitiveArray(operation, type, value1, value2); + } + return compare_ObjectArray(operation, (Object[]) value1, value2); + } + if (value1 instanceof Collection) { + return compare_Collection(operation, (Collection) value1, value2); + } + if (value1 instanceof Integer) { + return compare_Integer(operation, ((Integer) value1).intValue(), value2); + } + if (value1 instanceof Long) { + return compare_Long(operation, ((Long) value1).longValue(), value2); + } + if (value1 instanceof Byte) { + return compare_Byte(operation, ((Byte) value1).byteValue(), value2); + } + if (value1 instanceof Short) { + return compare_Short(operation, ((Short) value1).shortValue(), value2); + } + if (value1 instanceof Character) { + return compare_Character(operation, ((Character) value1).charValue(), value2); + } + if (value1 instanceof Float) { + return compare_Float(operation, ((Float) value1).floatValue(), value2); + } + if (value1 instanceof Double) { + return compare_Double(operation, ((Double) value1).doubleValue(), value2); + } + if (value1 instanceof Boolean) { + return compare_Boolean(operation, ((Boolean) value1).booleanValue(), value2); + } + if (value1 instanceof Comparable) { + @SuppressWarnings("unchecked") + Comparable comparable = (Comparable) value1; + return compare_Comparable(operation, comparable, value2); + } + return compare_Unknown(operation, value1, value2); + } + + private boolean compare_Collection(int operation, Collection collection, Object value2) { + for (Object value1 : collection) { + if (compare(operation, value1, value2)) { + return true; + } + } + return false; + } + + private boolean compare_ObjectArray(int operation, Object[] array, Object value2) { + for (Object value1 : array) { + if (compare(operation, value1, value2)) { + return true; + } + } + return false; + } + + private boolean compare_PrimitiveArray(int operation, Class type, Object primarray, Object value2) { + if (Integer.TYPE.isAssignableFrom(type)) { + int[] array = (int[]) primarray; + for (int value1 : array) { + if (compare_Integer(operation, value1, value2)) { + return true; + } + } + return false; + } + if (Long.TYPE.isAssignableFrom(type)) { + long[] array = (long[]) primarray; + for (long value1 : array) { + if (compare_Long(operation, value1, value2)) { + return true; + } + } + return false; + } + if (Byte.TYPE.isAssignableFrom(type)) { + byte[] array = (byte[]) primarray; + for (byte value1 : array) { + if (compare_Byte(operation, value1, value2)) { + return true; + } + } + return false; + } + if (Short.TYPE.isAssignableFrom(type)) { + short[] array = (short[]) primarray; + for (short value1 : array) { + if (compare_Short(operation, value1, value2)) { + return true; + } + } + return false; + } + if (Character.TYPE.isAssignableFrom(type)) { + char[] array = (char[]) primarray; + for (char value1 : array) { + if (compare_Character(operation, value1, value2)) { + return true; + } + } + return false; + } + if (Float.TYPE.isAssignableFrom(type)) { + float[] array = (float[]) primarray; + for (float value1 : array) { + if (compare_Float(operation, value1, value2)) { + return true; + } + } + return false; + } + if (Double.TYPE.isAssignableFrom(type)) { + double[] array = (double[]) primarray; + for (double value1 : array) { + if (compare_Double(operation, value1, value2)) { + return true; + } + } + return false; + } + if (Boolean.TYPE.isAssignableFrom(type)) { + boolean[] array = (boolean[]) primarray; + for (boolean value1 : array) { + if (compare_Boolean(operation, value1, value2)) { + return true; + } + } + return false; + } + return false; + } + + private boolean compare_String(int operation, String string, Object value2) { + switch (operation) { + case SUBSTRING : { + String[] substrings = (String[]) value2; + int pos = 0; + for (int i = 0, size = substrings.length; i < size; i++) { + String substr = substrings[i]; + + if (i + 1 < size) /* if this is not that last substr */{ + if (substr == null) /* * */{ + String substr2 = substrings[i + 1]; + + if (substr2 == null) /* ** */ + continue; /* ignore first star */ + /* xxx */ + int index = string.indexOf(substr2, pos); + if (index == -1) { + return false; + } + + pos = index + substr2.length(); + if (i + 2 < size) // if there are more + // substrings, increment + // over the string we just + // matched; otherwise need + // to do the last substr + // check + i++; + } else /* xxx */{ + int len = substr.length(); + if (string.regionMatches(pos, substr, 0, len)) { + pos += len; + } else { + return false; + } + } + } else /* last substr */{ + if (substr == null) /* * */{ + return true; + } + /* xxx */ + return string.endsWith(substr); + } + } + + return true; + } + case EQUAL : { + return string.equals(value2); + } + case APPROX : { + string = approxString(string); + String string2 = approxString((String) value2); + + return string.equalsIgnoreCase(string2); + } + case GREATER : { + return string.compareTo((String) value2) >= 0; + } + case LESS : { + return string.compareTo((String) value2) <= 0; + } + } + return false; + } + + private boolean compare_Integer(int operation, int intval, Object value2) { + if (operation == SUBSTRING) { + return false; + } + int intval2; + try { + intval2 = Integer.parseInt(((String) value2).trim()); + } catch (IllegalArgumentException e) { + return false; + } + switch (operation) { + case APPROX : + case EQUAL : { + return intval == intval2; + } + case GREATER : { + return intval >= intval2; + } + case LESS : { + return intval <= intval2; + } + } + return false; + } + + private boolean compare_Long(int operation, long longval, Object value2) { + if (operation == SUBSTRING) { + return false; + } + long longval2; + try { + longval2 = Long.parseLong(((String) value2).trim()); + } catch (IllegalArgumentException e) { + return false; + } + + switch (operation) { + case APPROX : + case EQUAL : { + return longval == longval2; + } + case GREATER : { + return longval >= longval2; + } + case LESS : { + return longval <= longval2; + } + } + return false; + } + + private boolean compare_Byte(int operation, byte byteval, Object value2) { + if (operation == SUBSTRING) { + return false; + } + byte byteval2; + try { + byteval2 = Byte.parseByte(((String) value2).trim()); + } catch (IllegalArgumentException e) { + return false; + } + + switch (operation) { + case APPROX : + case EQUAL : { + return byteval == byteval2; + } + case GREATER : { + return byteval >= byteval2; + } + case LESS : { + return byteval <= byteval2; + } + } + return false; + } + + private boolean compare_Short(int operation, short shortval, Object value2) { + if (operation == SUBSTRING) { + return false; + } + short shortval2; + try { + shortval2 = Short.parseShort(((String) value2).trim()); + } catch (IllegalArgumentException e) { + return false; + } + + switch (operation) { + case APPROX : + case EQUAL : { + return shortval == shortval2; + } + case GREATER : { + return shortval >= shortval2; + } + case LESS : { + return shortval <= shortval2; + } + } + return false; + } + + private boolean compare_Character(int operation, char charval, Object value2) { + if (operation == SUBSTRING) { + return false; + } + char charval2; + try { + charval2 = ((String) value2).charAt(0); + } catch (IndexOutOfBoundsException e) { + return false; + } + + switch (operation) { + case EQUAL : { + return charval == charval2; + } + case APPROX : { + return (charval == charval2) || (Character.toUpperCase(charval) == Character.toUpperCase(charval2)) || (Character.toLowerCase(charval) == Character.toLowerCase(charval2)); + } + case GREATER : { + return charval >= charval2; + } + case LESS : { + return charval <= charval2; + } + } + return false; + } + + private boolean compare_Boolean(int operation, boolean boolval, Object value2) { + if (operation == SUBSTRING) { + return false; + } + boolean boolval2 = Boolean.valueOf(((String) value2).trim()).booleanValue(); + switch (operation) { + case APPROX : + case EQUAL : + case GREATER : + case LESS : { + return boolval == boolval2; + } + } + return false; + } + + private boolean compare_Float(int operation, float floatval, Object value2) { + if (operation == SUBSTRING) { + return false; + } + float floatval2; + try { + floatval2 = Float.parseFloat(((String) value2).trim()); + } catch (IllegalArgumentException e) { + return false; + } + + switch (operation) { + case APPROX : + case EQUAL : { + return Float.compare(floatval, floatval2) == 0; + } + case GREATER : { + return Float.compare(floatval, floatval2) >= 0; + } + case LESS : { + return Float.compare(floatval, floatval2) <= 0; + } + } + return false; + } + + private boolean compare_Double(int operation, double doubleval, Object value2) { + if (operation == SUBSTRING) { + return false; + } + double doubleval2; + try { + doubleval2 = Double.parseDouble(((String) value2).trim()); + } catch (IllegalArgumentException e) { + return false; + } + + switch (operation) { + case APPROX : + case EQUAL : { + return Double.compare(doubleval, doubleval2) == 0; + } + case GREATER : { + return Double.compare(doubleval, doubleval2) >= 0; + } + case LESS : { + return Double.compare(doubleval, doubleval2) <= 0; + } + } + return false; + } + + private static Object valueOf(Class target, String value2) { + do { + Method method; + try { + method = target.getMethod("valueOf", String.class); + } catch (NoSuchMethodException e) { + break; + } + if (Modifier.isStatic(method.getModifiers()) && target.isAssignableFrom(method.getReturnType())) { + setAccessible(method); + try { + return method.invoke(null, value2.trim()); + } catch (IllegalAccessException e) { + return null; + } catch (InvocationTargetException e) { + return null; + } + } + } while (false); + + do { + Constructor constructor; + try { + constructor = target.getConstructor(String.class); + } catch (NoSuchMethodException e) { + break; + } + setAccessible(constructor); + try { + return constructor.newInstance(value2.trim()); + } catch (IllegalAccessException e) { + return null; + } catch (InvocationTargetException e) { + return null; + } catch (InstantiationException e) { + return null; + } + } while (false); + + return null; + } + + private static void setAccessible(AccessibleObject accessible) { + if (!accessible.isAccessible()) { + AccessController.doPrivileged(new SetAccessibleAction(accessible)); + } + } + + private boolean compare_Comparable(int operation, Comparable value1, Object value2) { + if (operation == SUBSTRING) { + return false; + } + value2 = valueOf(value1.getClass(), (String) value2); + if (value2 == null) { + return false; + } + try { + switch (operation) { + case APPROX : + case EQUAL : { + return value1.compareTo(value2) == 0; + } + case GREATER : { + return value1.compareTo(value2) >= 0; + } + case LESS : { + return value1.compareTo(value2) <= 0; + } + } + } catch (Exception e) { + // if the compareTo method throws an exception; return false + return false; + } + return false; + } + + private boolean compare_Version(int operation, Version value1, Object value2) { + if (operation == SUBSTRING) { + return false; + } + try { + Version version2 = Version.valueOf((String) value2); + switch (operation) { + case APPROX : + case EQUAL : { + return value1.compareTo(version2) == 0; + } + case GREATER : { + return value1.compareTo(version2) >= 0; + } + case LESS : { + return value1.compareTo(version2) <= 0; + } + } + } catch (Exception e) { + // if the valueOf or compareTo method throws an exception + return false; + } + return false; + } + + private boolean compare_Unknown(int operation, Object value1, Object value2) { + if (operation == SUBSTRING) { + return false; + } + value2 = valueOf(value1.getClass(), (String) value2); + if (value2 == null) { + return false; + } + try { + switch (operation) { + case APPROX : + case EQUAL : + case GREATER : + case LESS : { + return value1.equals(value2); + } + } + } catch (Exception e) { + // if the equals method throws an exception; return false + return false; + } + return false; + } + + /** + * Map a string for an APPROX (~=) comparison. + * + * This implementation removes white spaces. This is the minimum + * implementation allowed by the OSGi spec. + * + * @param input Input string. + * @return String ready for APPROX comparison. + */ + private static String approxString(String input) { + boolean changed = false; + char[] output = input.toCharArray(); + int cursor = 0; + for (char c : output) { + if (Character.isWhitespace(c)) { + changed = true; + continue; + } + + output[cursor] = c; + cursor++; + } + + return changed ? new String(output, 0, cursor) : input; + } + + /** + * Parser class for OSGi filter strings. This class parses the complete + * filter string and builds a tree of Filter objects rooted at the + * parent. + */ + static private final class Parser { + private final String filterstring; + private final char[] filterChars; + private int pos; + + Parser(String filterstring) { + this.filterstring = filterstring; + filterChars = filterstring.toCharArray(); + pos = 0; + } + + FilterImpl parse() throws InvalidSyntaxException { + FilterImpl filter; + try { + filter = parse_filter(); + } catch (ArrayIndexOutOfBoundsException e) { + throw new InvalidSyntaxException("Filter ended abruptly", filterstring, e); + } + + if (pos != filterChars.length) { + throw new InvalidSyntaxException("Extraneous trailing characters: " + filterstring.substring(pos), filterstring); + } + return filter; + } + + private FilterImpl parse_filter() throws InvalidSyntaxException { + FilterImpl filter; + skipWhiteSpace(); + + if (filterChars[pos] != '(') { + throw new InvalidSyntaxException("Missing '(': " + filterstring.substring(pos), filterstring); + } + + pos++; + + filter = parse_filtercomp(); + + skipWhiteSpace(); + + if (filterChars[pos] != ')') { + throw new InvalidSyntaxException("Missing ')': " + filterstring.substring(pos), filterstring); + } + + pos++; + + skipWhiteSpace(); + + return filter; + } + + private FilterImpl parse_filtercomp() throws InvalidSyntaxException { + skipWhiteSpace(); + + char c = filterChars[pos]; + + switch (c) { + case '&' : { + pos++; + return parse_and(); + } + case '|' : { + pos++; + return parse_or(); + } + case '!' : { + pos++; + return parse_not(); + } + } + return parse_item(); + } + + private FilterImpl parse_and() throws InvalidSyntaxException { + int lookahead = pos; + skipWhiteSpace(); + + if (filterChars[pos] != '(') { + pos = lookahead - 1; + return parse_item(); + } + + List operands = new ArrayList(10); + + while (filterChars[pos] == '(') { + FilterImpl child = parse_filter(); + operands.add(child); + } + + return new FilterImpl(FilterImpl.AND, null, + operands.toArray(new FilterImpl[0])); + } + + private FilterImpl parse_or() throws InvalidSyntaxException { + int lookahead = pos; + skipWhiteSpace(); + + if (filterChars[pos] != '(') { + pos = lookahead - 1; + return parse_item(); + } + + List operands = new ArrayList(10); + + while (filterChars[pos] == '(') { + FilterImpl child = parse_filter(); + operands.add(child); + } + + return new FilterImpl(FilterImpl.OR, null, + operands.toArray(new FilterImpl[0])); + } + + private FilterImpl parse_not() throws InvalidSyntaxException { + int lookahead = pos; + skipWhiteSpace(); + + if (filterChars[pos] != '(') { + pos = lookahead - 1; + return parse_item(); + } + + FilterImpl child = parse_filter(); + + return new FilterImpl(FilterImpl.NOT, null, child); + } + + private FilterImpl parse_item() throws InvalidSyntaxException { + String attr = parse_attr(); + + skipWhiteSpace(); + + switch (filterChars[pos]) { + case '~' : { + if (filterChars[pos + 1] == '=') { + pos += 2; + return new FilterImpl(FilterImpl.APPROX, attr, parse_value()); + } + break; + } + case '>' : { + if (filterChars[pos + 1] == '=') { + pos += 2; + return new FilterImpl(FilterImpl.GREATER, attr, parse_value()); + } + break; + } + case '<' : { + if (filterChars[pos + 1] == '=') { + pos += 2; + return new FilterImpl(FilterImpl.LESS, attr, parse_value()); + } + break; + } + case '=' : { + if (filterChars[pos + 1] == '*') { + int oldpos = pos; + pos += 2; + skipWhiteSpace(); + if (filterChars[pos] == ')') { + return new FilterImpl(FilterImpl.PRESENT, attr, null); + } + pos = oldpos; + } + + pos++; + Object string = parse_substring(); + + if (string instanceof String) { + return new FilterImpl(FilterImpl.EQUAL, attr, string); + } + return new FilterImpl(FilterImpl.SUBSTRING, attr, string); + } + } + + throw new InvalidSyntaxException("Invalid operator: " + filterstring.substring(pos), filterstring); + } + + private String parse_attr() throws InvalidSyntaxException { + skipWhiteSpace(); + + int begin = pos; + int end = pos; + + char c = filterChars[pos]; + + while (c != '~' && c != '<' && c != '>' && c != '=' && c != '(' && c != ')') { + pos++; + + if (!Character.isWhitespace(c)) { + end = pos; + } + + c = filterChars[pos]; + } + + int length = end - begin; + + if (length == 0) { + throw new InvalidSyntaxException("Missing attr: " + filterstring.substring(pos), filterstring); + } + + return new String(filterChars, begin, length); + } + + private String parse_value() throws InvalidSyntaxException { + StringBuilder sb = new StringBuilder(filterChars.length - pos); + + parseloop: while (true) { + char c = filterChars[pos]; + + switch (c) { + case ')' : { + break parseloop; + } + + case '(' : { + throw new InvalidSyntaxException("Invalid value: " + filterstring.substring(pos), filterstring); + } + + case '\\' : { + pos++; + c = filterChars[pos]; + /* fall through into default */ + } + + default : { + sb.append(c); + pos++; + break; + } + } + } + + if (sb.length() == 0) { + throw new InvalidSyntaxException("Missing value: " + filterstring.substring(pos), filterstring); + } + + return sb.toString(); + } + + private Object parse_substring() throws InvalidSyntaxException { + StringBuilder sb = new StringBuilder(filterChars.length - pos); + + List operands = new ArrayList(10); + + parseloop: while (true) { + char c = filterChars[pos]; + + switch (c) { + case ')' : { + if (sb.length() > 0) { + operands.add(sb.toString()); + } + + break parseloop; + } + + case '(' : { + throw new InvalidSyntaxException("Invalid value: " + filterstring.substring(pos), filterstring); + } + + case '*' : { + if (sb.length() > 0) { + operands.add(sb.toString()); + } + + sb.setLength(0); + + operands.add(null); + pos++; + + break; + } + + case '\\' : { + pos++; + c = filterChars[pos]; + /* fall through into default */ + } + + default : { + sb.append(c); + pos++; + break; + } + } + } + + int size = operands.size(); + + if (size == 0) { + return ""; + } + + if (size == 1) { + Object single = operands.get(0); + + if (single != null) { + return single; + } + } + + return operands.toArray(new String[0]); + } + + private void skipWhiteSpace() { + for (int length = filterChars.length; (pos < length) && Character.isWhitespace(filterChars[pos]);) { + pos++; + } + } + } + } + + /** + * This Map is used for case-insensitive key lookup during filter + * evaluation. This Map implementation only supports the get operation using + * a String key as no other operations are used by the Filter + * implementation. + */ + static private final class CaseInsensitiveMap extends AbstractMap implements Map { + private final Dictionary dictionary; + private final String[] keys; + + /** + * Create a case insensitive map from the specified dictionary. + * + * @param dictionary + * @throws IllegalArgumentException If {@code dictionary} contains case + * variants of the same key name. + */ + CaseInsensitiveMap(Dictionary dictionary) { + if (dictionary == null) { + this.dictionary = null; + this.keys = new String[0]; + return; + } + this.dictionary = dictionary; + List keyList = new ArrayList(dictionary.size()); + for (Enumeration e = dictionary.keys(); e.hasMoreElements();) { + Object k = e.nextElement(); + if (k instanceof String) { + String key = (String) k; + for (String i : keyList) { + if (key.equalsIgnoreCase(i)) { + throw new IllegalArgumentException(); + } + } + keyList.add(key); + } + } + this.keys = keyList.toArray(new String[0]); + } + + @Override + public Object get(Object o) { + String k = (String) o; + for (String key : keys) { + if (key.equalsIgnoreCase(k)) { + return dictionary.get(key); + } + } + return null; + } + + @Override + public Set> entrySet() { + throw new UnsupportedOperationException(); + } + } + + /** + * This Map is used for key lookup from a ServiceReference during filter + * evaluation. This Map implementation only supports the get operation using + * a String key as no other operations are used by the Filter + * implementation. + */ + static private final class ServiceReferenceMap extends AbstractMap implements Map { + private final ServiceReference reference; + + ServiceReferenceMap(ServiceReference reference) { + this.reference = reference; + } + + @Override + public Object get(Object key) { + if (reference == null) { + return null; + } + return reference.getProperty((String) key); + } + + @Override + public Set> entrySet() { + throw new UnsupportedOperationException(); + } + } + + static private final class SetAccessibleAction implements PrivilegedAction { + private final AccessibleObject accessible; + + SetAccessibleAction(AccessibleObject accessible) { + this.accessible = accessible; + } + + @Override + public Void run() { + accessible.setAccessible(true); + return null; + } + } + + /** + * This class contains a method to match a distinguished name (DN) chain + * against and DN chain pattern. + *

      + * The format of DNs are given in RFC 2253. We represent a signature chain + * for an X.509 certificate as a semicolon separated list of DNs. This is + * what we refer to as the DN chain. Each DN is made up of relative + * distinguished names (RDN) which in turn are made up of key value pairs. + * For example: + * + *

      +	 *   cn=ben+ou=research,o=ACME,c=us;ou=Super CA,c=CA
      +	 * 
      + * + * is made up of two DNs: "{@code cn=ben+ou=research,o=ACME,c=us} " and " + * {@code ou=Super CA,c=CA} ". The first DN is made of of three RDNs: " + * {@code cn=ben+ou=research}" and "{@code o=ACME}" and " {@code c=us} + * ". The first RDN has two name value pairs: " {@code cn=ben}" and " + * {@code ou=research}". + *

      + * A chain pattern makes use of wildcards ('*' or '-') to match against DNs, + * and wildcards ('*') to match againts DN prefixes, and value. If a DN in a + * match pattern chain is made up of a wildcard ("*"), that wildcard will + * match zero or one DNs in the chain. If a DN in a match pattern chain is + * made up of a wildcard ("-"), that wildcard will match zero or more DNs in + * the chain. If the first RDN of a DN is the wildcard ("*"), that DN will + * match any other DN with the same suffix (the DN with the wildcard RDN + * removed). If a value of a name/value pair is a wildcard ("*"), the value + * will match any value for that name. + */ + static private final class DNChainMatching { + private static final String MINUS_WILDCARD = "-"; + private static final String STAR_WILDCARD = "*"; + + /** + * Check the name/value pairs of the rdn against the pattern. + * + * @param rdn List of name value pairs for a given RDN. + * @param rdnPattern List of name value pattern pairs. + * @return true if the list of name value pairs match the pattern. + */ + private static boolean rdnmatch(List rdn, List rdnPattern) { + if (rdn.size() != rdnPattern.size()) { + return false; + } + for (int i = 0; i < rdn.size(); i++) { + String rdnNameValue = (String) rdn.get(i); + String patNameValue = (String) rdnPattern.get(i); + int rdnNameEnd = rdnNameValue.indexOf('='); + int patNameEnd = patNameValue.indexOf('='); + if (rdnNameEnd != patNameEnd || !rdnNameValue.regionMatches(0, patNameValue, 0, rdnNameEnd)) { + return false; + } + String patValue = patNameValue.substring(patNameEnd); + String rdnValue = rdnNameValue.substring(rdnNameEnd); + if (!rdnValue.equals(patValue) && !patValue.equals("=*") && !patValue.equals("=#16012a")) { + return false; + } + } + return true; + } + + private static boolean dnmatch(List dn, List dnPattern) { + int dnStart = 0; + int patStart = 0; + int patLen = dnPattern.size(); + if (patLen == 0) { + return false; + } + if (dnPattern.get(0).equals(STAR_WILDCARD)) { + patStart = 1; + patLen--; + } + if (dn.size() < patLen) { + return false; + } else { + if (dn.size() > patLen) { + if (!dnPattern.get(0).equals(STAR_WILDCARD)) { + // If the number of rdns do not match we must have a + // prefix map + return false; + } + // The rdnPattern and rdn must have the same number of + // elements + dnStart = dn.size() - patLen; + } + } + for (int i = 0; i < patLen; i++) { + if (!rdnmatch((List) dn.get(i + dnStart), (List) dnPattern.get(i + patStart))) { + return false; + } + } + return true; + } + + /** + * Parses a distinguished name chain pattern and returns a List where + * each element represents a distinguished name (DN) in the chain of + * DNs. Each element will be either a String, if the element represents + * a wildcard ("*" or "-"), or a List representing an RDN. Each element + * in the RDN List will be a String, if the element represents a + * wildcard ("*"), or a List of Strings, each String representing a + * name/value pair in the RDN. + * + * @param pattern + * @return a list of DNs. + * @throws IllegalArgumentException + */ + private static List parseDNchainPattern(String pattern) { + if (pattern == null) { + throw new IllegalArgumentException("The pattern must not be null."); + } + List parsed = new ArrayList(); + final int length = pattern.length(); + char c = ';'; // start with semi-colon to detect empty pattern + for (int startIndex = skipSpaces(pattern, 0); startIndex < length;) { + int cursor = startIndex; + int endIndex = startIndex; + out: for (boolean inQuote = false; cursor < length; cursor++) { + c = pattern.charAt(cursor); + switch (c) { + case '"' : + inQuote = !inQuote; + break; + case '\\' : + cursor++; // skip the escaped char + if (cursor == length) { + throw new IllegalArgumentException("unterminated escape"); + } + break; + case ';' : + if (!inQuote) { + break out; // end of pattern + } + break; + } + if (c != ' ') { // ignore trailing whitespace + endIndex = cursor + 1; + } + } + parsed.add(pattern.substring(startIndex, endIndex)); + startIndex = skipSpaces(pattern, cursor + 1); + } + if (c == ';') { // last non-whitespace character was a semi-colon + throw new IllegalArgumentException("empty pattern"); + } + + // Now we have parsed into a list of strings, lets make List of rdn + // out of them + for (int i = 0; i < parsed.size(); i++) { + String dn = (String) parsed.get(i); + if (dn.equals(STAR_WILDCARD) || dn.equals(MINUS_WILDCARD)) { + continue; + } + List rdns = new ArrayList(); + if (dn.charAt(0) == '*') { + int index = skipSpaces(dn, 1); + if (dn.charAt(index) != ',') { + throw new IllegalArgumentException("invalid wildcard prefix"); + } + rdns.add(STAR_WILDCARD); + dn = new X500Principal(dn.substring(index + 1)).getName(X500Principal.CANONICAL); + } else { + dn = new X500Principal(dn).getName(X500Principal.CANONICAL); + } + // Now dn is a nice CANONICAL DN + parseDN(dn, rdns); + parsed.set(i, rdns); + } + return parsed; + } + + private static List parseDNchain(List chain) { + if (chain == null) { + throw new IllegalArgumentException("DN chain must not be null."); + } + List result = new ArrayList(chain.size()); + // Now we parse is a list of strings, lets make List of rdn out + // of them + for (String dn : chain) { + dn = new X500Principal(dn).getName(X500Principal.CANONICAL); + // Now dn is a nice CANONICAL DN + List rdns = new ArrayList(); + parseDN(dn, rdns); + result.add(rdns); + } + if (result.size() == 0) { + throw new IllegalArgumentException("empty DN chain"); + } + return result; + } + + /** + * Increment startIndex until the end of dnChain is hit or until it is + * the index of a non-space character. + */ + private static int skipSpaces(String dnChain, int startIndex) { + while (startIndex < dnChain.length() && dnChain.charAt(startIndex) == ' ') { + startIndex++; + } + return startIndex; + } + + /** + * Takes a distinguished name in canonical form and fills in the + * rdnArray with the extracted RDNs. + * + * @param dn the distinguished name in canonical form. + * @param rdn the list to fill in with RDNs extracted from the dn + * @throws IllegalArgumentException if a formatting error is found. + */ + private static void parseDN(String dn, List rdn) { + int startIndex = 0; + char c = '\0'; + List nameValues = new ArrayList(); + while (startIndex < dn.length()) { + int endIndex; + for (endIndex = startIndex; endIndex < dn.length(); endIndex++) { + c = dn.charAt(endIndex); + if (c == ',' || c == '+') { + break; + } + if (c == '\\') { + endIndex++; // skip the escaped char + } + } + if (endIndex > dn.length()) { + throw new IllegalArgumentException("unterminated escape " + dn); + } + nameValues.add(dn.substring(startIndex, endIndex)); + if (c != '+') { + rdn.add(nameValues); + if (endIndex != dn.length()) { + nameValues = new ArrayList(); + } else { + nameValues = null; + } + } + startIndex = endIndex + 1; + } + if (nameValues != null) { + throw new IllegalArgumentException("improperly terminated DN " + dn); + } + } + + /** + * This method will return an 'index' which points to a non-wildcard DN + * or the end-of-list. + */ + private static int skipWildCards(List dnChainPattern, int dnChainPatternIndex) { + int i; + for (i = dnChainPatternIndex; i < dnChainPattern.size(); i++) { + Object dnPattern = dnChainPattern.get(i); + if (dnPattern instanceof String) { + if (!dnPattern.equals(STAR_WILDCARD) && !dnPattern.equals(MINUS_WILDCARD)) { + throw new IllegalArgumentException("expected wildcard in DN pattern"); + } + // otherwise continue skipping over wild cards + } else { + if (dnPattern instanceof List) { + // if its a list then we have our 'non-wildcard' DN + break; + } else { + // unknown member of the DNChainPattern + throw new IllegalArgumentException("expected String or List in DN Pattern"); + } + } + } + // i either points to end-of-list, or to the first + // non-wildcard pattern after dnChainPatternIndex + return i; + } + + /** + * recursively attempt to match the DNChain, and the DNChainPattern + * where DNChain is of the format: "DN;DN;DN;" and DNChainPattern is of + * the format: "DNPattern;*;DNPattern" (or combinations of this) + */ + private static boolean dnChainMatch(List dnChain, int dnChainIndex, List dnChainPattern, int dnChainPatternIndex) throws IllegalArgumentException { + if (dnChainIndex >= dnChain.size()) { + return false; + } + if (dnChainPatternIndex >= dnChainPattern.size()) { + return false; + } + // check to see what the pattern starts with + Object dnPattern = dnChainPattern.get(dnChainPatternIndex); + if (dnPattern instanceof String) { + if (!dnPattern.equals(STAR_WILDCARD) && !dnPattern.equals(MINUS_WILDCARD)) { + throw new IllegalArgumentException("expected wildcard in DN pattern"); + } + // here we are processing a wild card as the first DN + // skip all wildcard DN's + if (dnPattern.equals(MINUS_WILDCARD)) { + dnChainPatternIndex = skipWildCards(dnChainPattern, dnChainPatternIndex); + } else { + dnChainPatternIndex++; // only skip the '*' wildcard + } + if (dnChainPatternIndex >= dnChainPattern.size()) { + // return true iff the wild card is '-' or if we are at the + // end of the chain + return dnPattern.equals(MINUS_WILDCARD) ? true : dnChain.size() - 1 == dnChainIndex; + } + // + // we will now recursively call to see if the rest of the + // DNChainPattern matches increasingly smaller portions of the + // rest of the DNChain + // + if (dnPattern.equals(STAR_WILDCARD)) { + // '*' option: only wildcard on 0 or 1 + return dnChainMatch(dnChain, dnChainIndex, dnChainPattern, dnChainPatternIndex) || dnChainMatch(dnChain, dnChainIndex + 1, dnChainPattern, dnChainPatternIndex); + } + for (int i = dnChainIndex; i < dnChain.size(); i++) { + // '-' option: wildcard 0 or more + if (dnChainMatch(dnChain, i, dnChainPattern, dnChainPatternIndex)) { + return true; + } + } + // if we are here, then we didn't find a match.. fall through to + // failure + } else { + if (dnPattern instanceof List) { + // here we have to do a deeper check for each DN in the + // pattern until we hit a wild card + do { + if (!dnmatch((List) dnChain.get(dnChainIndex), (List) dnPattern)) { + return false; + } + // go to the next set of DN's in both chains + dnChainIndex++; + dnChainPatternIndex++; + // if we finished the pattern then it all matched + if ((dnChainIndex >= dnChain.size()) && (dnChainPatternIndex >= dnChainPattern.size())) { + return true; + } + // if the DN Chain is finished, but the pattern isn't + // finished then if the rest of the pattern is not + // wildcard then we are done + if (dnChainIndex >= dnChain.size()) { + dnChainPatternIndex = skipWildCards(dnChainPattern, dnChainPatternIndex); + // return TRUE iff the pattern index moved past the + // list-size (implying that the rest of the pattern + // is all wildcards) + return dnChainPatternIndex >= dnChainPattern.size(); + } + // if the pattern finished, but the chain continues then + // we have a mis-match + if (dnChainPatternIndex >= dnChainPattern.size()) { + return false; + } + // get the next DN Pattern + dnPattern = dnChainPattern.get(dnChainPatternIndex); + if (dnPattern instanceof String) { + if (!dnPattern.equals(STAR_WILDCARD) && !dnPattern.equals(MINUS_WILDCARD)) { + throw new IllegalArgumentException("expected wildcard in DN pattern"); + } + // if the next DN is a 'wildcard', then we will + // recurse + return dnChainMatch(dnChain, dnChainIndex, dnChainPattern, dnChainPatternIndex); + } else { + if (!(dnPattern instanceof List)) { + throw new IllegalArgumentException("expected String or List in DN Pattern"); + } + } + // if we are here, then we will just continue to the + // match the next set of DN's from the DNChain, and the + // DNChainPattern since both are lists + } while (true); + // should never reach here? + } else { + throw new IllegalArgumentException("expected String or List in DN Pattern"); + } + } + // if we get here, the the default return is 'mis-match' + return false; + } + + /** + * Matches a distinguished name chain against a pattern of a + * distinguished name chain. + * + * @param dnChain + * @param pattern the pattern of distinguished name (DN) chains to match + * against the dnChain. Wildcards ("*" or "-") can be used in + * three cases: + *
        + *
      1. As a DN. In this case, the DN will consist of just the "*" + * or "-". When "*" is used it will match zero or one DNs. When + * "-" is used it will match zero or more DNs. For example, + * "cn=me,c=US;*;cn=you" will match + * "cn=me,c=US";cn=you" and "cn=me,c=US;cn=her;cn=you". The + * pattern "cn=me,c=US;-;cn=you" will match "cn=me,c=US";cn=you" + * and "cn=me,c=US;cn=her;cn=him;cn=you".
      2. + *
      3. As a DN prefix. In this case, the DN must start with "*,". + * The wild card will match zero or more RDNs at the start of a + * DN. For example, "*,cn=me,c=US;cn=you" will match + * "cn=me,c=US";cn=you" and + * "ou=my org unit,o=my org,cn=me,c=US;cn=you"
      4. + *
      5. As a value. In this case the value of a name value pair in + * an RDN will be a "*". The wildcard will match any value for + * the given name. For example, "cn=*,c=US;cn=you" will match + * "cn=me,c=US";cn=you" and "cn=her,c=US;cn=you", but it will not + * match "ou=my org unit,c=US;cn=you". If the wildcard does not + * occur by itself in the value, it will not be used as a + * wildcard. In other words, "cn=m*,c=US;cn=you" represents the + * common name of "m*" not any common name starting with "m".
      6. + *
      + * @return true if dnChain matches the pattern. + * @throws IllegalArgumentException + */ + static boolean match(String pattern, List dnChain) { + List parsedDNChain; + List parsedDNPattern; + try { + parsedDNChain = parseDNchain(dnChain); + } catch (RuntimeException e) { + throw new IllegalArgumentException( + "Invalid DN chain: " + toString(dnChain), e); + } + try { + parsedDNPattern = parseDNchainPattern(pattern); + } catch (RuntimeException e) { + throw new IllegalArgumentException( + "Invalid match pattern: " + pattern, e); + } + return dnChainMatch(parsedDNChain, 0, parsedDNPattern, 0); + } + + private static String toString(List dnChain) { + if (dnChain == null) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (Iterator iChain = dnChain.iterator(); iChain.hasNext();) { + sb.append(iChain.next()); + if (iChain.hasNext()) { + sb.append("; "); + } + } + return sb.toString(); + } + } +} diff --git a/framework/src/main/java/org/osgi/framework/InvalidSyntaxException.java b/framework/src/main/java/org/osgi/framework/InvalidSyntaxException.java new file mode 100644 index 00000000000..03f6f2b5364 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/InvalidSyntaxException.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +/** + * A Framework exception used to indicate that a filter string has an invalid + * syntax. + * + *

      + * An {@code InvalidSyntaxException} object indicates that a filter string + * parameter has an invalid syntax and cannot be parsed. See {@link Filter} for + * a description of the filter string syntax. + * + *

      + * This exception conforms to the general purpose exception chaining mechanism. + * + * @author $Id: 59696296eff1574fa844e3a0a975ba6c83ec59af $ + */ + +public class InvalidSyntaxException extends Exception { + static final long serialVersionUID = -4295194420816491875L; + /** + * The invalid filter string. + */ + private final String filter; + + /** + * Creates an exception of type {@code InvalidSyntaxException}. + * + *

      + * This method creates an {@code InvalidSyntaxException} object with the + * specified message and the filter string which generated the exception. + * + * @param msg The message. + * @param filter The invalid filter string. + */ + public InvalidSyntaxException(String msg, String filter) { + super(message(msg, filter)); + this.filter = filter; + } + + /** + * Creates an exception of type {@code InvalidSyntaxException}. + * + *

      + * This method creates an {@code InvalidSyntaxException} object with the + * specified message and the filter string which generated the exception. + * + * @param msg The message. + * @param filter The invalid filter string. + * @param cause The cause of this exception. + * @since 1.3 + */ + public InvalidSyntaxException(String msg, String filter, Throwable cause) { + super(message(msg, filter), cause); + this.filter = filter; + } + + /** + * Return message string for super constructor. + */ + private static String message(String msg, String filter) { + if ((msg == null) || (filter == null) || msg.indexOf(filter) >= 0) { + return msg; + } + return msg + ": " + filter; + } + + /** + * Returns the filter string that generated the + * {@code InvalidSyntaxException} object. + * + * @return The invalid filter string. + * @see BundleContext#getServiceReferences(Class, String) + * @see BundleContext#getServiceReferences(String, String) + * @see BundleContext#addServiceListener(ServiceListener,String) + */ + public String getFilter() { + return filter; + } + + /** + * Returns the cause of this exception or {@code null} if no cause was set. + * + * @return The cause of this exception or {@code null} if no cause was set. + * @since 1.3 + */ + @Override + public Throwable getCause() { + return super.getCause(); + } + + /** + * Initializes the cause of this exception to the specified value. + * + * @param cause The cause of this exception. + * @return This exception. + * @throws IllegalArgumentException If the specified cause is this + * exception. + * @throws IllegalStateException If the cause of this exception has already + * been set. + * @since 1.3 + */ + @Override + public Throwable initCause(Throwable cause) { + return super.initCause(cause); + } +} diff --git a/framework/src/main/java/org/osgi/framework/PackagePermission.java b/framework/src/main/java/org/osgi/framework/PackagePermission.java new file mode 100644 index 00000000000..1338daa0882 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/PackagePermission.java @@ -0,0 +1,785 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import java.io.IOException; +import java.io.NotSerializableException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamField; +import java.security.AccessController; +import java.security.BasicPermission; +import java.security.Permission; +import java.security.PermissionCollection; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +/** + * A bundle's authority to import or export a package. + * + *

      + * A package is a dot-separated string that defines a fully qualified Java + * package. + *

      + * For example: + * + *

      + * org.osgi.service.http
      + * 
      + * + *

      + * {@code PackagePermission} has three actions: {@code exportonly}, + * {@code import} and {@code export}. The {@code export} action, which is + * deprecated, implies the {@code import} action. + * + * @ThreadSafe + * @author $Id: 264ccd683465cbe22d571b0cb7d0b19352d582f7 $ + */ + +public final class PackagePermission extends BasicPermission { + static final long serialVersionUID = -5107705877071099135L; + + /** + * The action string {@code export}. The {@code export} action implies the + * {@code import} action. + * + * @deprecated As of 1.5. Use {@code exportonly} instead. + */ + public final static String EXPORT = "export"; + + /** + * The action string {@code exportonly}. The {@code exportonly} action does + * not imply the {@code import} action. + * + * @since 1.5 + */ + public final static String EXPORTONLY = "exportonly"; + + /** + * The action string {@code import}. + */ + public final static String IMPORT = "import"; + + private final static int ACTION_EXPORT = 0x00000001; + private final static int ACTION_IMPORT = 0x00000002; + private final static int ACTION_ALL = ACTION_EXPORT | ACTION_IMPORT; + final static int ACTION_NONE = 0; + + /** + * The actions mask. + */ + transient int action_mask; + + /** + * The actions in canonical form. + * + * @serial + */ + private volatile String actions = null; + + /** + * The bundle used by this PackagePermission. + */ + transient final Bundle bundle; + + /** + * If this PackagePermission was constructed with a filter, this holds a + * Filter matching object used to evaluate the filter in implies. + */ + transient Filter filter; + + /** + * This map holds the properties of the permission, used to match a filter + * in implies. This is not initialized until necessary, and then cached in + * this object. + */ + private transient volatile Map properties; + + /** + * Creates a new {@code PackagePermission} object. + * + *

      + * The name is specified as a normal Java package name: a dot-separated + * string. Wildcards may be used. + * + *

      +	 * name ::= <package name> | <package name ending in ".*"> | *
      +	 * 
      + * + * Examples: + * + *
      +	 * org.osgi.service.http
      +	 * javax.servlet.*
      +	 * *
      +	 * 
      + * + * For the {@code import} action, the name can also be a filter expression. + * The filter gives access to the following attributes: + *
        + *
      • signer - A Distinguished Name chain used to sign the exporting + * bundle. Wildcards in a DN are not matched according to the filter string + * rules, but according to the rules defined for a DN chain.
      • + *
      • location - The location of the exporting bundle.
      • + *
      • id - The bundle ID of the exporting bundle.
      • + *
      • name - The symbolic name of the exporting bundle.
      • + *
      • package.name - The name of the requested package.
      • + *
      + * Filter attribute names are processed in a case sensitive manner. + * + *

      + * Package Permissions are granted over all possible versions of a package. + * + * A bundle that needs to export a package must have the appropriate + * {@code PackagePermission} for that package; similarly, a bundle that + * needs to import a package must have the appropriate + * {@code PackagePermssion} for that package. + *

      + * Permission is granted for both classes and resources. + * + * @param name Package name or filter expression. A filter expression can + * only be specified if the specified action is {@code import}. + * @param actions {@code exportonly},{@code import} (canonical order). + * @throws IllegalArgumentException If the specified name is a filter + * expression and either the specified action is not {@code import} + * or the filter has an invalid syntax. + */ + public PackagePermission(String name, String actions) { + this(name, parseActions(actions)); + if ((filter != null) && ((action_mask & ACTION_ALL) != ACTION_IMPORT)) { + throw new IllegalArgumentException("invalid action string for filter expression"); + } + } + + /** + * Creates a new requested {@code PackagePermission} object to be used by + * code that must perform {@code checkPermission} for the {@code import} + * action. {@code PackagePermission} objects created with this constructor + * cannot be added to a {@code PackagePermission} permission collection. + * + * @param name The name of the requested package to import. + * @param exportingBundle The bundle exporting the requested package. + * @param actions The action {@code import}. + * @throws IllegalArgumentException If the specified action is not + * {@code import} or the name is a filter expression. + * @since 1.5 + */ + public PackagePermission(String name, Bundle exportingBundle, String actions) { + super(name); + setTransients(name, parseActions(actions)); + this.bundle = exportingBundle; + if (exportingBundle == null) { + throw new IllegalArgumentException("bundle must not be null"); + } + if (filter != null) { + throw new IllegalArgumentException("invalid name"); + } + if ((action_mask & ACTION_ALL) != ACTION_IMPORT) { + throw new IllegalArgumentException("invalid action string"); + } + } + + /** + * Package private constructor used by PackagePermissionCollection. + * + * @param name package name + * @param mask action mask + */ + PackagePermission(String name, int mask) { + super(name); + setTransients(name, mask); + this.bundle = null; + } + + /** + * Called by constructors and when deserialized. + * + * @param mask action mask + */ + private void setTransients(String name, int mask) { + if ((mask == ACTION_NONE) || ((mask & ACTION_ALL) != mask)) { + throw new IllegalArgumentException("invalid action string"); + } + action_mask = mask; + filter = parseFilter(name); + } + + /** + * Parse action string into action mask. + * + * @param actions Action string. + * @return action mask. + */ + private static int parseActions(String actions) { + boolean seencomma = false; + + int mask = ACTION_NONE; + + if (actions == null) { + return mask; + } + + char[] a = actions.toCharArray(); + + int i = a.length - 1; + if (i < 0) + return mask; + + while (i != -1) { + char c; + + // skip whitespace + while ((i != -1) && ((c = a[i]) == ' ' || c == '\r' || c == '\n' || c == '\f' || c == '\t')) + i--; + + // check for the known strings + int matchlen; + + if (i >= 5 && (a[i - 5] == 'i' || a[i - 5] == 'I') + && (a[i - 4] == 'm' || a[i - 4] == 'M') + && (a[i - 3] == 'p' || a[i - 3] == 'P') + && (a[i - 2] == 'o' || a[i - 2] == 'O') + && (a[i - 1] == 'r' || a[i - 1] == 'R') + && (a[i] == 't' || a[i] == 'T')) { + matchlen = 6; + mask |= ACTION_IMPORT; + + } else + if (i >= 5 && (a[i - 5] == 'e' || a[i - 5] == 'E') + && (a[i - 4] == 'x' || a[i - 4] == 'X') + && (a[i - 3] == 'p' || a[i - 3] == 'P') + && (a[i - 2] == 'o' || a[i - 2] == 'O') + && (a[i - 1] == 'r' || a[i - 1] == 'R') + && (a[i] == 't' || a[i] == 'T')) { + matchlen = 6; + mask |= ACTION_EXPORT | ACTION_IMPORT; + + } else { + if (i >= 9 && (a[i - 9] == 'e' || a[i - 9] == 'E') + && (a[i - 8] == 'x' || a[i - 8] == 'X') + && (a[i - 7] == 'p' || a[i - 7] == 'P') + && (a[i - 6] == 'o' || a[i - 6] == 'O') + && (a[i - 5] == 'r' || a[i - 5] == 'R') + && (a[i - 4] == 't' || a[i - 4] == 'T') + && (a[i - 3] == 'o' || a[i - 3] == 'O') + && (a[i - 2] == 'n' || a[i - 2] == 'N') + && (a[i - 1] == 'l' || a[i - 1] == 'L') + && (a[i] == 'y' || a[i] == 'Y')) { + matchlen = 10; + mask |= ACTION_EXPORT; + + } else { + // parse error + throw new IllegalArgumentException("invalid permission: " + actions); + } + } + + // make sure we didn't just match the tail of a word + // like "ackbarfimport". Also, skip to the comma. + seencomma = false; + while (i >= matchlen && !seencomma) { + switch (a[i - matchlen]) { + case ',' : + seencomma = true; + /* FALLTHROUGH */ + case ' ' : + case '\r' : + case '\n' : + case '\f' : + case '\t' : + break; + default : + throw new IllegalArgumentException("invalid permission: " + actions); + } + i--; + } + + // point i at the location of the comma minus one (or -1). + i -= matchlen; + } + + if (seencomma) { + throw new IllegalArgumentException("invalid permission: " + actions); + } + + return mask; + } + + /** + * Parse filter string into a Filter object. + * + * @param filterString The filter string to parse. + * @return a Filter for this bundle. If the specified filterString is not a + * filter expression, then {@code null} is returned. + * @throws IllegalArgumentException If the filter syntax is invalid. + */ + private static Filter parseFilter(String filterString) { + filterString = filterString.trim(); + if (filterString.charAt(0) != '(') { + return null; + } + + try { + return FrameworkUtil.createFilter(filterString); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException("invalid filter", e); + } + } + + /** + * Determines if the specified permission is implied by this object. + * + *

      + * This method checks that the package name of the target is implied by the + * package name of this object. The list of {@code PackagePermission} + * actions must either match or allow for the list of the target object to + * imply the target {@code PackagePermission} action. + *

      + * The permission to export a package implies the permission to import the + * named package. + * + *

      +	 * x.y.*,"export" -> x.y.z,"export" is true
      +	 * *,"import" -> x.y, "import"      is true
      +	 * *,"export" -> x.y, "import"      is true
      +	 * x.y,"export" -> x.y.z, "export"  is false
      +	 * 
      + * + * @param p The requested permission. + * @return {@code true} if the specified permission is implied by this + * object; {@code false} otherwise. + */ + @Override + public boolean implies(Permission p) { + if (!(p instanceof PackagePermission)) { + return false; + } + PackagePermission requested = (PackagePermission) p; + if (bundle != null) { + return false; + } + // if requested permission has a filter, then it is an invalid argument + if (requested.filter != null) { + return false; + } + return implies0(requested, ACTION_NONE); + } + + /** + * Internal implies method. Used by the implies and the permission + * collection implies methods. + * + * @param requested The requested PackagePermission which has already be + * validated as a proper argument. The requested PackagePermission + * must not have a filter expression. + * @param effective The effective actions with which to start. + * @return {@code true} if the specified permission is implied by this + * object; {@code false} otherwise. + */ + boolean implies0(PackagePermission requested, int effective) { + /* check actions first - much faster */ + effective |= action_mask; + final int desired = requested.action_mask; + if ((effective & desired) != desired) { + return false; + } + /* Get filter if any */ + Filter f = filter; + if (f == null) { + return super.implies(requested); + } + return f.matches(requested.getProperties()); + } + + /** + * Returns the canonical string representation of the + * {@code PackagePermission} actions. + * + *

      + * Always returns present {@code PackagePermission} actions in the following + * order: {@code EXPORTONLY},{@code IMPORT}. + * + * @return Canonical string representation of the {@code PackagePermission} + * actions. + */ + @Override + public String getActions() { + String result = actions; + if (result == null) { + StringBuilder sb = new StringBuilder(); + boolean comma = false; + + int mask = action_mask; + if ((mask & ACTION_EXPORT) == ACTION_EXPORT) { + sb.append(EXPORTONLY); + comma = true; + } + + if ((mask & ACTION_IMPORT) == ACTION_IMPORT) { + if (comma) + sb.append(','); + sb.append(IMPORT); + } + + actions = result = sb.toString(); + } + return result; + } + + /** + * Returns a new {@code PermissionCollection} object suitable for storing + * {@code PackagePermission} objects. + * + * @return A new {@code PermissionCollection} object. + */ + @Override + public PermissionCollection newPermissionCollection() { + return new PackagePermissionCollection(); + } + + /** + * Determines the equality of two {@code PackagePermission} objects. + * + * This method checks that specified package has the same package name and + * {@code PackagePermission} actions as this {@code PackagePermission} + * object. + * + * @param obj The object to test for equality with this + * {@code PackagePermission} object. + * @return {@code true} if {@code obj} is a {@code PackagePermission}, and + * has the same package name and actions as this + * {@code PackagePermission} object; {@code false} otherwise. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (!(obj instanceof PackagePermission)) { + return false; + } + + PackagePermission pp = (PackagePermission) obj; + + return (action_mask == pp.action_mask) && getName().equals(pp.getName()) && ((bundle == pp.bundle) || ((bundle != null) && bundle.equals(pp.bundle))); + } + + /** + * Returns the hash code value for this object. + * + * @return A hash code value for this object. + */ + @Override + public int hashCode() { + int h = 31 * 17 + getName().hashCode(); + h = 31 * h + getActions().hashCode(); + if (bundle != null) { + h = 31 * h + bundle.hashCode(); + } + return h; + } + + /** + * WriteObject is called to save the state of this permission object to a + * stream. The actions are serialized, and the superclass takes care of the + * name. + */ + private synchronized void writeObject(java.io.ObjectOutputStream s) throws IOException { + if (bundle != null) { + throw new NotSerializableException("cannot serialize"); + } + // Write out the actions. The superclass takes care of the name + // call getActions to make sure actions field is initialized + if (actions == null) + getActions(); + s.defaultWriteObject(); + } + + /** + * readObject is called to restore the state of this permission from a + * stream. + */ + private synchronized void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { + // Read in the action, then initialize the rest + s.defaultReadObject(); + setTransients(getName(), parseActions(actions)); + } + + /** + * Called by {@link PackagePermission#implies(Permission)}. This method is + * only called on a requested permission which cannot have a filter set. + * + * @return a map of properties for this permission. + */ + private Map getProperties() { + Map result = properties; + if (result != null) { + return result; + } + final Map map = new HashMap(5); + map.put("package.name", getName()); + if (bundle != null) { + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Void run() { + map.put("id", Long.valueOf(bundle.getBundleId())); + map.put("location", bundle.getLocation()); + String name = bundle.getSymbolicName(); + if (name != null) { + map.put("name", name); + } + SignerProperty signer = new SignerProperty(bundle); + if (signer.isBundleSigned()) { + map.put("signer", signer); + } + return null; + } + }); + } + return properties = map; + } +} + +/** + * Stores a set of {@code PackagePermission} permissions. + * + * @see java.security.Permission + * @see java.security.Permissions + * @see java.security.PermissionCollection + */ + +final class PackagePermissionCollection extends PermissionCollection { + static final long serialVersionUID = -3350758995234427603L; + /** + * Table of permissions with names. + * + * @GuardedBy this + */ + private transient Map permissions; + + /** + * Boolean saying if "*" is in the collection. + * + * @serial + * @GuardedBy this + */ + private boolean all_allowed; + + /** + * Table of permissions with filter expressions. + * + * @serial + * @GuardedBy this + */ + private Map filterPermissions; + + /** + * Create an empty PackagePermissions object. + */ + public PackagePermissionCollection() { + permissions = new HashMap(); + all_allowed = false; + } + + /** + * Adds a permission to this permission collection. + * + * @param permission The {@code PackagePermission} object to add. + * @throws IllegalArgumentException If the specified permission is not a + * {@code PackagePermission} instance or was constructed with a + * Bundle object. + * @throws SecurityException If this {@code PackagePermissionCollection} + * object has been marked read-only. + */ + @Override + public void add(final Permission permission) { + if (!(permission instanceof PackagePermission)) { + throw new IllegalArgumentException("invalid permission: " + permission); + } + if (isReadOnly()) { + throw new SecurityException("attempt to add a Permission to a " + "readonly PermissionCollection"); + } + + final PackagePermission pp = (PackagePermission) permission; + if (pp.bundle != null) { + throw new IllegalArgumentException("cannot add to collection: " + pp); + } + + final String name = pp.getName(); + final Filter f = pp.filter; + synchronized (this) { + /* select the bucket for the permission */ + Map pc; + if (f != null) { + pc = filterPermissions; + if (pc == null) { + filterPermissions = pc = new HashMap(); + } + } else { + pc = permissions; + } + + final PackagePermission existing = pc.get(name); + if (existing != null) { + final int oldMask = existing.action_mask; + final int newMask = pp.action_mask; + if (oldMask != newMask) { + pc.put(name, new PackagePermission(name, oldMask | newMask)); + + } + } else { + pc.put(name, pp); + } + + if (!all_allowed) { + if (name.equals("*")) { + all_allowed = true; + } + } + } + } + + /** + * Determines if the specified permissions implies the permissions expressed + * in {@code permission}. + * + * @param permission The Permission object to compare with this + * {@code PackagePermission} object. + * @return {@code true} if {@code permission} is a proper subset of a + * permission in the set; {@code false} otherwise. + */ + @Override + public boolean implies(final Permission permission) { + if (!(permission instanceof PackagePermission)) { + return false; + } + final PackagePermission requested = (PackagePermission) permission; + /* if requested permission has a filter, then it is an invalid argument */ + if (requested.filter != null) { + return false; + } + String requestedName = requested.getName(); + final int desired = requested.action_mask; + int effective = PackagePermission.ACTION_NONE; + + Collection perms; + synchronized (this) { + Map pc = permissions; + PackagePermission pp; + /* short circuit if the "*" Permission was added */ + if (all_allowed) { + pp = pc.get("*"); + if (pp != null) { + effective |= pp.action_mask; + if ((effective & desired) == desired) { + return true; + } + } + } + /* + * strategy: Check for full match first. Then work our way up the + * name looking for matches on a.b.* + */ + pp = pc.get(requestedName); + if (pp != null) { + /* we have a direct hit! */ + effective |= pp.action_mask; + if ((effective & desired) == desired) { + return true; + } + } + /* work our way up the tree... */ + int last; + int offset = requestedName.length() - 1; + while ((last = requestedName.lastIndexOf(".", offset)) != -1) { + requestedName = requestedName.substring(0, last + 1) + "*"; + pp = pc.get(requestedName); + if (pp != null) { + effective |= pp.action_mask; + if ((effective & desired) == desired) { + return true; + } + } + offset = last - 1; + } + /* + * we don't have to check for "*" as it was already checked before + * we were called. + */ + pc = filterPermissions; + if (pc == null) { + return false; + } + perms = pc.values(); + } + /* iterate one by one over filteredPermissions */ + for (PackagePermission perm : perms) { + if (perm.implies0(requested, effective)) { + return true; + } + } + return false; + } + + /** + * Returns an enumeration of all {@code PackagePermission} objects in the + * container. + * + * @return Enumeration of all {@code PackagePermission} objects. + */ + @Override + public synchronized Enumeration elements() { + List all = new ArrayList(permissions.values()); + Map pc = filterPermissions; + if (pc != null) { + all.addAll(pc.values()); + } + return Collections.enumeration(all); + } + + /* serialization logic */ + private static final ObjectStreamField[] serialPersistentFields = {new ObjectStreamField("permissions", Hashtable.class), new ObjectStreamField("all_allowed", Boolean.TYPE), + new ObjectStreamField("filterPermissions", HashMap.class) }; + + private synchronized void writeObject(ObjectOutputStream out) throws IOException { + Hashtable hashtable = new Hashtable(permissions); + ObjectOutputStream.PutField pfields = out.putFields(); + pfields.put("permissions", hashtable); + pfields.put("all_allowed", all_allowed); + pfields.put("filterPermissions", filterPermissions); + out.writeFields(); + } + + private synchronized void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { + ObjectInputStream.GetField gfields = in.readFields(); + @SuppressWarnings("unchecked") + Hashtable hashtable = (Hashtable) gfields.get("permissions", null); + permissions = new HashMap(hashtable); + all_allowed = gfields.get("all_allowed", false); + @SuppressWarnings("unchecked") + HashMap fp = (HashMap) gfields.get("filterPermissions", null); + filterPermissions = fp; + } +} diff --git a/framework/src/main/java/org/osgi/framework/PrototypeServiceFactory.java b/framework/src/main/java/org/osgi/framework/PrototypeServiceFactory.java new file mode 100644 index 00000000000..864506fa156 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/PrototypeServiceFactory.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2015). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import org.osgi.annotation.versioning.ConsumerType; + +/** + * A factory for {@link Constants#SCOPE_PROTOTYPE prototype scope} services. The + * factory can provide multiple, customized service objects in the OSGi + * environment. + * + *

      + * When registering a service, a {@code PrototypeServiceFactory} object can be + * used instead of a service object, so that the bundle developer can create a + * customized service object for each caller that is using the service. + * + *

      + * When a caller uses a {@link ServiceObjects} to + * {@link ServiceObjects#getService() request} a service object, the framework + * calls the {@link #getService(Bundle, ServiceRegistration) getService} method + * to return a service object customized for the requesting caller. The caller + * can {@link ServiceObjects#ungetService(Object) release} the returned service + * object and the framework will call the + * {@link #ungetService(Bundle, ServiceRegistration, Object) ungetService} + * method with the service object. + * + *

      + * When a bundle uses the {@link BundleContext#getService(ServiceReference)} + * method to obtain a service object, the framework must act as if the service + * has {@link Constants#SCOPE_BUNDLE bundle scope}. That is, the framework will + * call the {@link #getService(Bundle, ServiceRegistration) getService} method + * to obtain a bundle-scoped service object which will be cached and have a use + * count. See {@link ServiceFactory}. + * + *

      + * A bundle can use both {@link ServiceObjects} and + * {@link BundleContext#getService(ServiceReference)} to obtain a service object + * for a service. {@link ServiceObjects#getService()} will always return a + * service object provided by a call to + * {@link #getService(Bundle, ServiceRegistration)} and + * {@link BundleContext#getService(ServiceReference)} will always return the + * bundle-scoped service object. + * + *

      + * {@code PrototypeServiceFactory} objects are only used by the Framework and + * are not made available to other bundles in the OSGi environment. The + * Framework may concurrently call a {@code PrototypeServiceFactory}. + * + * @param Type of Service + * @see BundleContext#getServiceObjects(ServiceReference) + * @see ServiceObjects + * @ThreadSafe + * @since 1.8 + * @author $Id$ + */ +@ConsumerType +public interface PrototypeServiceFactory extends ServiceFactory { + /** + * Returns a service object for a caller. + * + *

      + * The Framework invokes this method for each caller requesting a service + * object using {@link ServiceObjects#getService()}. The factory can then + * return a customized service object for the caller. + * + *

      + * The Framework must check that the returned service object is valid. If + * the returned service object is {@code null} or is not an + * {@code instanceof} all the classes named when the service was registered, + * a framework event of type {@link FrameworkEvent#ERROR} is fired + * containing a service exception of type + * {@link ServiceException#FACTORY_ERROR} and {@code null} is returned to + * the caller. If this method throws an exception, a framework event of type + * {@link FrameworkEvent#ERROR} is fired containing a service exception of + * type {@link ServiceException#FACTORY_EXCEPTION} with the thrown exception + * as the cause and {@code null} is returned to the caller. + * + * @param bundle The bundle requesting the service. + * @param registration The {@code ServiceRegistration} object for the + * requested service. + * @return A service object that must be an instance of all + * the classes named when the service was registered. + * @see ServiceObjects#getService() + */ + @Override + public S getService(Bundle bundle, ServiceRegistration registration); + + /** + * Releases a service object customized for a caller. + * + *

      + * The Framework invokes this method when a service has been released by a + * bundle such as by calling {@link ServiceObjects#ungetService(Object)}. + * The service object may then be destroyed. + * + *

      + * If this method throws an exception, a framework event of type + * {@link FrameworkEvent#ERROR} is fired containing a service exception of + * type {@link ServiceException#FACTORY_EXCEPTION} with the thrown exception + * as the cause. + * + * @param bundle The bundle releasing the service. + * @param registration The {@code ServiceRegistration} object for the + * service being released. + * @param service The service object returned by a previous call to the + * {@link #getService(Bundle, ServiceRegistration) getService} + * method. + * @see ServiceObjects#ungetService(Object) + */ + @Override + public void ungetService(Bundle bundle, ServiceRegistration registration, S service); +} diff --git a/framework/src/main/java/org/osgi/framework/ServiceEvent.java b/framework/src/main/java/org/osgi/framework/ServiceEvent.java new file mode 100644 index 00000000000..06eae880109 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/ServiceEvent.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import java.util.Dictionary; +import java.util.EventObject; + +/** + * An event from the Framework describing a service lifecycle change. + *

      + * {@code ServiceEvent} objects are delivered to {@code ServiceListener}s and + * {@code AllServiceListener}s when a change occurs in this service's lifecycle. + * A type code is used to identify the event type for future extendability. + * + *

      + * OSGi Alliance reserves the right to extend the set of types. + * + * @Immutable + * @see ServiceListener + * @see AllServiceListener + * @author $Id: b27a941cb68e6416825b1b717090a2eb098733f3 $ + */ + +public class ServiceEvent extends EventObject { + static final long serialVersionUID = 8792901483909409299L; + /** + * Reference to the service that had a change occur in its lifecycle. + */ + private final ServiceReference reference; + + /** + * Type of service lifecycle change. + */ + private final int type; + + /** + * This service has been registered. + *

      + * This event is synchronously delivered after the service + * has been registered with the Framework. + * + * @see BundleContext#registerService(String[],Object,Dictionary) + */ + public final static int REGISTERED = 0x00000001; + + /** + * The properties of a registered service have been modified. + *

      + * This event is synchronously delivered after the service + * properties have been modified. + * + * @see ServiceRegistration#setProperties(Dictionary) + */ + public final static int MODIFIED = 0x00000002; + + /** + * This service is in the process of being unregistered. + *

      + * This event is synchronously delivered before the service + * has completed unregistering. + * + *

      + * If a bundle is using a service that is {@code UNREGISTERING}, the bundle + * should release its use of the service when it receives this event. If the + * bundle does not release its use of the service when it receives this + * event, the Framework will automatically release the bundle's use of the + * service while completing the service unregistration operation. + * + * @see ServiceRegistration#unregister() + * @see BundleContext#ungetService(ServiceReference) + */ + public final static int UNREGISTERING = 0x00000004; + + /** + * The properties of a registered service have been modified and the new + * properties no longer match the listener's filter. + *

      + * This event is synchronously delivered after the service + * properties have been modified. This event is only delivered to listeners + * which were added with a non-{@code null} filter where the filter matched + * the service properties prior to the modification but the filter does not + * match the modified service properties. + * + * @see ServiceRegistration#setProperties(Dictionary) + * @since 1.5 + */ + public final static int MODIFIED_ENDMATCH = 0x00000008; + + /** + * Creates a new service event object. + * + * @param type The event type. + * @param reference A {@code ServiceReference} object to the service that + * had a lifecycle change. + */ + public ServiceEvent(int type, ServiceReference reference) { + super(reference); + this.reference = reference; + this.type = type; + } + + /** + * Returns a reference to the service that had a change occur in its + * lifecycle. + *

      + * This reference is the source of the event. + * + * @return Reference to the service that had a lifecycle change. + */ + public ServiceReference getServiceReference() { + return reference; + } + + /** + * Returns the type of event. The event type values are: + *

        + *
      • {@link #REGISTERED}
      • + *
      • {@link #MODIFIED}
      • + *
      • {@link #MODIFIED_ENDMATCH}
      • + *
      • {@link #UNREGISTERING}
      • + *
      + * + * @return Type of service lifecycle change. + */ + + public int getType() { + return type; + } +} diff --git a/framework/src/main/java/org/osgi/framework/ServiceException.java b/framework/src/main/java/org/osgi/framework/ServiceException.java new file mode 100644 index 00000000000..e0a120c295c --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/ServiceException.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) OSGi Alliance (2007, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +/** + * A service exception used to indicate that a service problem occurred. + * + *

      + * A {@code ServiceException} object is created by the Framework or service + * implementation to denote an exception condition in the service. A type code + * is used to identify the exception type for future extendability. Service + * implementations may also create subclasses of {@code ServiceException}. When + * subclassing, the subclass should set the type to {@link #SUBCLASSED} to + * indicate that {@code ServiceException} has been subclassed. + * + *

      + * This exception conforms to the general purpose exception chaining mechanism. + * + * @author $Id: 32a515460813c702b127f65dda91cd23781b2a98 $ + * @since 1.5 + */ + +public class ServiceException extends RuntimeException { + static final long serialVersionUID = 3038963223712959631L; + + /** + * Type of service exception. + */ + private final int type; + + /** + * No exception type is unspecified. + */ + public static final int UNSPECIFIED = 0; + /** + * The service has been unregistered. + */ + public static final int UNREGISTERED = 1; + /** + * The service factory produced an invalid service object. + */ + public static final int FACTORY_ERROR = 2; + /** + * The service factory threw an exception. + */ + public static final int FACTORY_EXCEPTION = 3; + /** + * The exception is a subclass of ServiceException. The subclass should be + * examined for the type of the exception. + */ + public static final int SUBCLASSED = 4; + /** + * An error occurred invoking a remote service. + */ + public static final int REMOTE = 5; + /** + * The service factory resulted in a recursive call to itself for the + * requesting bundle. + * + * @since 1.6 + */ + public static final int FACTORY_RECURSION = 6; + /** + * An asynchronous operation was unable to obtain the service. + * + * @since 1.8 + */ + public static final int ASYNC_ERROR = 7; + + /** + * Creates a {@code ServiceException} with the specified message and + * exception cause. + * + * @param msg The associated message. + * @param cause The cause of this exception. + */ + public ServiceException(String msg, Throwable cause) { + this(msg, UNSPECIFIED, cause); + } + + /** + * Creates a {@code ServiceException} with the specified message. + * + * @param msg The message. + */ + public ServiceException(String msg) { + this(msg, UNSPECIFIED); + } + + /** + * Creates a {@code ServiceException} with the specified message, type and + * exception cause. + * + * @param msg The associated message. + * @param type The type for this exception. + * @param cause The cause of this exception. + */ + public ServiceException(String msg, int type, Throwable cause) { + super(msg, cause); + this.type = type; + } + + /** + * Creates a {@code ServiceException} with the specified message and type. + * + * @param msg The message. + * @param type The type for this exception. + */ + public ServiceException(String msg, int type) { + super(msg); + this.type = type; + } + + /** + * Returns the type for this exception or {@code UNSPECIFIED} if the type + * was unspecified or unknown. + * + * @return The type of this exception. + */ + public int getType() { + return type; + } +} diff --git a/framework/src/main/java/org/osgi/framework/ServiceFactory.java b/framework/src/main/java/org/osgi/framework/ServiceFactory.java new file mode 100644 index 00000000000..14479581109 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/ServiceFactory.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import org.osgi.annotation.versioning.ConsumerType; + +/** + * A factory for {@link Constants#SCOPE_BUNDLE bundle scope} services. The + * factory can provide service objects customized for each bundle in the OSGi + * environment. + * + *

      + * When registering a service, a {@code ServiceFactory} object can be used + * instead of a service object, so that the bundle developer can create a + * customized service object for each bundle that is using the service. + * + *

      + * When a bundle {@link BundleContext#getService(ServiceReference) requests} the + * service object, the framework calls the + * {@link #getService(Bundle, ServiceRegistration) getService} method to return + * a service object customized for the requesting bundle. The returned service + * object is cached by the Framework for subsequent calls to + * {@link BundleContext#getService(ServiceReference)} until the bundle releases + * its use of the service. + * + *

      + * When the bundle's use count for the service is + * {@link BundleContext#ungetService(ServiceReference) decremented} to zero + * (including the bundle stopping or the service being unregistered), the + * framework will call the + * {@link #ungetService(Bundle, ServiceRegistration, Object) ungetService} + * method. + * + *

      + * {@code ServiceFactory} objects are only used by the Framework and are not + * made available to other bundles in the OSGi environment. The Framework may + * concurrently call a {@code ServiceFactory}. + * + * @param Type of Service + * @see BundleContext#getService(ServiceReference) + * @ThreadSafe + * @author $Id: f11fc6bee18315fb659c7987d1b66f1c9c95548a $ + */ +@ConsumerType +public interface ServiceFactory { + /** + * Returns a service object for a bundle. + * + *

      + * The Framework invokes this method the first time the specified + * {@code bundle} requests a service object using the + * {@link BundleContext#getService(ServiceReference)} method. The factory + * can then return a customized service object for each bundle. + * + *

      + * The Framework must check that the returned service object is valid. If + * the returned service object is {@code null} or is not an + * {@code instanceof} all the classes named when the service was registered, + * a framework event of type {@link FrameworkEvent#ERROR} is fired + * containing a service exception of type + * {@link ServiceException#FACTORY_ERROR} and {@code null} is returned to + * the bundle. If this method throws an exception, a framework event of type + * {@link FrameworkEvent#ERROR} is fired containing a service exception of + * type {@link ServiceException#FACTORY_EXCEPTION} with the thrown exception + * as the cause and {@code null} is returned to the bundle. If this method + * is recursively called for the specified bundle, a framework event of type + * {@link FrameworkEvent#ERROR} is fired containing a service exception of + * type {@link ServiceException#FACTORY_RECURSION} and {@code null} is + * returned to the bundle. + * + *

      + * The Framework caches the valid service object and will return the same + * service object on any future call to + * {@link BundleContext#getService(ServiceReference)} for the specified + * bundle. This means the Framework must not allow this method to be + * concurrently called for the specified bundle. + * + * @param bundle The bundle requesting the service. + * @param registration The {@code ServiceRegistration} object for the + * requested service. + * @return A service object that must be an instance of all + * the classes named when the service was registered. + * @see BundleContext#getService(ServiceReference) + */ + public S getService(Bundle bundle, ServiceRegistration registration); + + /** + * Releases a service object customized for a bundle. + * + *

      + * The Framework invokes this method when a service has been released by a + * bundle. The service object may then be destroyed. + * + *

      + * If this method throws an exception, a framework event of type + * {@link FrameworkEvent#ERROR} is fired containing a service exception of + * type {@link ServiceException#FACTORY_EXCEPTION} with the thrown exception + * as the cause. + * + * @param bundle The bundle releasing the service. + * @param registration The {@code ServiceRegistration} object for the + * service being released. + * @param service The service object returned by a previous call to the + * {@link #getService(Bundle, ServiceRegistration) getService} + * method. + * @see BundleContext#ungetService(ServiceReference) + */ + public void ungetService(Bundle bundle, ServiceRegistration registration, S service); +} diff --git a/framework/src/main/java/org/osgi/framework/ServiceListener.java b/framework/src/main/java/org/osgi/framework/ServiceListener.java new file mode 100644 index 00000000000..6d89ded3d6e --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/ServiceListener.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2015). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import java.util.EventListener; +import org.osgi.annotation.versioning.ConsumerType; + +/** + * A {@code ServiceEvent} listener. {@code ServiceListener} is a listener + * interface that may be implemented by a bundle developer. When a + * {@code ServiceEvent} is fired, it is synchronously delivered to a + * {@code ServiceListener}. The Framework may deliver {@code ServiceEvent} + * objects to a {@code ServiceListener} out of order and may concurrently call + * and/or reenter a {@code ServiceListener}. + * + *

      + * A {@code ServiceListener} object is registered with the Framework using the + * {@code BundleContext.addServiceListener} method. {@code ServiceListener} + * objects are called with a {@code ServiceEvent} object when a service is + * registered, modified, or is in the process of unregistering. + * + *

      + * {@code ServiceEvent} object delivery to {@code ServiceListener} objects is + * filtered by the filter specified when the listener was registered. If the + * Java Runtime Environment supports permissions, then additional filtering is + * done. {@code ServiceEvent} objects are only delivered to the listener if the + * bundle which defines the listener object's class has the appropriate + * {@code ServicePermission} to get the service using at least one of the named + * classes under which the service was registered. + * + *

      + * {@code ServiceEvent} object delivery to {@code ServiceListener} objects is + * further filtered according to package sources as defined in + * {@link ServiceReference#isAssignableTo(Bundle, String)}. + * + * @see ServiceEvent + * @see ServicePermission + * @ThreadSafe + * @author $Id: e061a4c69c017c04dafe285926424bd1a2132b51 $ + */ +@ConsumerType +@FunctionalInterface +public interface ServiceListener extends EventListener { + /** + * Receives notification that a service has had a lifecycle change. + * + * @param event The {@code ServiceEvent} object. + */ + public void serviceChanged(ServiceEvent event); +} diff --git a/framework/src/main/java/org/osgi/framework/ServiceObjects.java b/framework/src/main/java/org/osgi/framework/ServiceObjects.java new file mode 100644 index 00000000000..84901895b76 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/ServiceObjects.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import org.osgi.annotation.versioning.ProviderType; + +/** + * Allows multiple service objects for a service to be obtained. + *

      + * For services with {@link Constants#SCOPE_PROTOTYPE prototype} scope, multiple + * service objects for the service can be obtained. Since implementations of + * {@link PrototypeServiceFactory} can return the same service object + * repeatedly, the framework must use count the returned service objects to + * release the service object only when its use count returns to zero. + *

      + * For services with {@link Constants#SCOPE_SINGLETON singleton} or + * {@link Constants#SCOPE_BUNDLE bundle} scope, only one, use-counted service + * object is available to a requesting bundle. + *

      + * Any unreleased service objects obtained from this {@code ServiceObjects} + * object are automatically released by the framework when the bundle associated + * with the BundleContext used to create this {@code ServiceObjects} object is + * stopped. + * + * @param Type of Service + * @see BundleContext#getServiceObjects(ServiceReference) + * @see PrototypeServiceFactory + * @ThreadSafe + * @since 1.8 + * @author $Id$ + */ +@ProviderType +public interface ServiceObjects { + /** + * Returns a service object for the {@link #getServiceReference() + * associated} service. + *

      + * This {@code ServiceObjects} object can be used to obtain multiple service + * objects for the associated service if the service has + * {@link Constants#SCOPE_PROTOTYPE prototype} scope. + *

      + * If the associated service has {@link Constants#SCOPE_SINGLETON singleton} + * or {@link Constants#SCOPE_BUNDLE bundle} scope, this method behaves the + * same as calling the {@link BundleContext#getService(ServiceReference)} + * method for the associated service. That is, only one, use-counted service + * object is available from this {@link ServiceObjects} object. + *

      + * This method will always return {@code null} when the associated service + * has been unregistered. + *

      + * For a prototype scope service, the following steps are required to obtain + * a service object: + *

        + *
      1. If the associated service has been unregistered, {@code null} is + * returned.
      2. + *
      3. The + * {@link PrototypeServiceFactory#getService(Bundle, ServiceRegistration)} + * method is called to supply a customized service object for the caller. + *
      4. + *
      5. If the service object returned by the {@code PrototypeServiceFactory} + * object is {@code null}, not an {@code instanceof} all the classes named + * when the service was registered or the {@code PrototypeServiceFactory} + * object throws an exception, {@code null} is returned and a Framework + * event of type {@link FrameworkEvent#ERROR} containing a + * {@link ServiceException} describing the error is fired.
      6. + *
      7. The use count for the customized service object is incremented by + * one.
      8. + *
      9. The customized service object is returned.
      10. + *
      + * + * @return A service object for the associated service or {@code null} if + * the service is not registered, the customized service object + * returned by a {@code ServiceFactory} does not implement the + * classes under which it was registered or the + * {@code ServiceFactory} threw an exception. + * @throws IllegalStateException If the BundleContext used to create this + * {@code ServiceObjects} object is no longer valid. + * @see #ungetService(Object) + */ + public S getService(); + + /** + * Releases a service object for the {@link #getServiceReference() + * associated} service. + *

      + * This {@code ServiceObjects} object can be used to obtain multiple service + * objects for the associated service if the service has + * {@link Constants#SCOPE_PROTOTYPE prototype} scope. If the associated + * service has {@link Constants#SCOPE_SINGLETON singleton} or + * {@link Constants#SCOPE_BUNDLE bundle} scope, this method behaves the same + * as calling the {@link BundleContext#ungetService(ServiceReference)} + * method for the associated service. That is, only one, use-counted service + * object is available from this {@link ServiceObjects} object. + *

      + * For a prototype scope service, the following steps are required to + * release a service object: + *

        + *
      1. If the associated service has been unregistered, this method returns + * without doing anything.
      2. + *
      3. The use count for the specified service object is decremented by one. + *
      4. + *
      5. If the use count for the specified service object is now zero, the + * {@link PrototypeServiceFactory#ungetService(Bundle, ServiceRegistration, Object)} + * method is called to release the specified service object.
      6. + *
      + *

      + * The specified service object must no longer be used and all references to + * it should be destroyed after calling this method when the use count has + * returned to zero. + * + * @param service A service object previously provided by this + * {@code ServiceObjects} object. + * @throws IllegalStateException If the BundleContext used to create this + * {@code ServiceObjects} object is no longer valid. + * @throws IllegalArgumentException If the specified service object is + * {@code null} or was not provided by a {@code ServiceObjects} + * object for the associated service. + * @see #getService() + */ + public void ungetService(S service); + + /** + * Returns the {@link ServiceReference} for the service associated with this + * {@code ServiceObjects} object. + * + * @return The {@link ServiceReference} for the service associated with this + * {@code ServiceObjects} object. + */ + public ServiceReference getServiceReference(); +} diff --git a/framework/src/main/java/org/osgi/framework/ServicePermission.java b/framework/src/main/java/org/osgi/framework/ServicePermission.java new file mode 100644 index 00000000000..e7d6c6f881c --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/ServicePermission.java @@ -0,0 +1,926 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import java.io.IOException; +import java.io.NotSerializableException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamField; +import java.security.AccessController; +import java.security.BasicPermission; +import java.security.Permission; +import java.security.PermissionCollection; +import java.security.PrivilegedAction; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A bundle's authority to register or get a service. + *

        + *
      • The {@code register} action allows a bundle to register a service on the + * specified names.
      • + *
      • The {@code get} action allows a bundle to detect a service and get it.
      • + *
      + * Permission to get a service is required in order to detect events regarding + * the service. Untrusted bundles should not be able to detect the presence of + * certain services unless they have the appropriate {@code ServicePermission} + * to get the specific service. + * + * @ThreadSafe + * @author $Id: 8db61d0b1cadd57ab173cba677b6bfb353680800 $ + */ + +public final class ServicePermission extends BasicPermission { + static final long serialVersionUID = -7662148639076511574L; + /** + * The action string {@code get}. + */ + public final static String GET = "get"; + /** + * The action string {@code register}. + */ + public final static String REGISTER = "register"; + + private final static int ACTION_GET = 0x00000001; + private final static int ACTION_REGISTER = 0x00000002; + private final static int ACTION_ALL = ACTION_GET | ACTION_REGISTER; + final static int ACTION_NONE = 0; + + /** + * The actions mask. + */ + transient int action_mask; + + /** + * The actions in canonical form. + * + * @serial + */ + private volatile String actions = null; + + /** + * The service used by this ServicePermission. Must be null if not + * constructed with a service. + */ + transient final ServiceReference service; + + /** + * The object classes for this ServicePermission. Must be null if not + * constructed with a service. + */ + transient final String[] objectClass; + + /** + * If this ServicePermission was constructed with a filter, this holds a + * Filter matching object used to evaluate the filter in implies. + */ + transient Filter filter; + + /** + * This map holds the properties of the permission, used to match a filter + * in implies. This is not initialized until necessary, and then cached in + * this object. + */ + private transient volatile Map properties; + + /** + * True if constructed with a name and the name is "*" or ends with ".*". + */ + private transient boolean wildcard; + + /** + * If constructed with a name and the name ends with ".*", this contains the + * name without the final "*". + */ + private transient String prefix; + + /** + * Create a new ServicePermission. + * + *

      + * The name of the service is specified as a fully qualified class name. + * Wildcards may be used. + * + *

      +	 * name ::= <class name> | <class name ending in ".*"> | *
      +	 * 
      + * + * Examples: + * + *
      +	 * org.osgi.service.http.HttpService
      +	 * org.osgi.service.http.*
      +	 * *
      +	 * 
      + * + * For the {@code get} action, the name can also be a filter expression. The + * filter gives access to the service properties as well as the following + * attributes: + *
        + *
      • signer - A Distinguished Name chain used to sign the bundle + * publishing the service. Wildcards in a DN are not matched according to + * the filter string rules, but according to the rules defined for a DN + * chain.
      • + *
      • location - The location of the bundle publishing the service.
      • + *
      • id - The bundle ID of the bundle publishing the service.
      • + *
      • name - The symbolic name of the bundle publishing the service.
      • + *
      + * Since the above attribute names may conflict with service property names + * used by a service, you can prefix an attribute name with '@' in the + * filter expression to match against the service property and not one of + * the above attributes. Filter attribute names are processed in a case + * sensitive manner unless the attribute references a service property. + * Service properties names are case insensitive. + * + *

      + * There are two possible actions: {@code get} and {@code register}. The + * {@code get} permission allows the owner of this permission to obtain a + * service with this name. The {@code register} permission allows the bundle + * to register a service under that name. + * + * @param name The service class name + * @param actions {@code get},{@code register} (canonical order) + * @throws IllegalArgumentException If the specified name is a filter + * expression and either the specified action is not {@code get} or + * the filter has an invalid syntax. + */ + public ServicePermission(String name, String actions) { + this(name, parseActions(actions)); + if ((filter != null) && ((action_mask & ACTION_ALL) != ACTION_GET)) { + throw new IllegalArgumentException("invalid action string for filter expression"); + } + } + + /** + * Creates a new requested {@code ServicePermission} object to be used by + * code that must perform {@code checkPermission} for the {@code get} + * action. {@code ServicePermission} objects created with this constructor + * cannot be added to a {@code ServicePermission} permission collection. + * + * @param reference The requested service. + * @param actions The action {@code get}. + * @throws IllegalArgumentException If the specified action is not + * {@code get} or reference is {@code null}. + * @since 1.5 + */ + public ServicePermission(ServiceReference reference, String actions) { + super(createName(reference)); + setTransients(null, parseActions(actions)); + this.service = reference; + this.objectClass = (String[]) reference.getProperty(Constants.OBJECTCLASS); + if ((action_mask & ACTION_ALL) != ACTION_GET) { + throw new IllegalArgumentException("invalid action string"); + } + } + + /** + * Create a permission name from a ServiceReference + * + * @param reference ServiceReference to use to create permission name. + * @return permission name. + */ + private static String createName(ServiceReference reference) { + if (reference == null) { + throw new IllegalArgumentException("reference must not be null"); + } + StringBuilder sb = new StringBuilder("(" + Constants.SERVICE_ID + "="); + sb.append(reference.getProperty(Constants.SERVICE_ID)); + sb.append(")"); + return sb.toString(); + } + + /** + * Package private constructor used by ServicePermissionCollection. + * + * @param name class name + * @param mask action mask + */ + ServicePermission(String name, int mask) { + super(name); + setTransients(parseFilter(name), mask); + this.service = null; + this.objectClass = null; + } + + /** + * Called by constructors and when deserialized. + * + * @param mask action mask + */ + private void setTransients(Filter f, int mask) { + if ((mask == ACTION_NONE) || ((mask & ACTION_ALL) != mask)) { + throw new IllegalArgumentException("invalid action string"); + } + action_mask = mask; + filter = f; + if (f == null) { + String name = getName(); + int l = name.length(); + /* if "*" or endsWith ".*" */ + wildcard = ((name.charAt(l - 1) == '*') && ((l == 1) || (name.charAt(l - 2) == '.'))); + if (wildcard && (l > 1)) { + prefix = name.substring(0, l - 1); + } + } + } + + /** + * Parse action string into action mask. + * + * @param actions Action string. + * @return action mask. + */ + private static int parseActions(String actions) { + boolean seencomma = false; + + int mask = ACTION_NONE; + + if (actions == null) { + return mask; + } + + char[] a = actions.toCharArray(); + + int i = a.length - 1; + if (i < 0) + return mask; + + while (i != -1) { + char c; + + // skip whitespace + while ((i != -1) && ((c = a[i]) == ' ' || c == '\r' || c == '\n' || c == '\f' || c == '\t')) + i--; + + // check for the known strings + int matchlen; + + if (i >= 2 && (a[i - 2] == 'g' || a[i - 2] == 'G') + && (a[i - 1] == 'e' || a[i - 1] == 'E') + && (a[i] == 't' || a[i] == 'T')) { + matchlen = 3; + mask |= ACTION_GET; + + } else + if (i >= 7 && (a[i - 7] == 'r' || a[i - 7] == 'R') + && (a[i - 6] == 'e' || a[i - 6] == 'E') + && (a[i - 5] == 'g' || a[i - 5] == 'G') + && (a[i - 4] == 'i' || a[i - 4] == 'I') + && (a[i - 3] == 's' || a[i - 3] == 'S') + && (a[i - 2] == 't' || a[i - 2] == 'T') + && (a[i - 1] == 'e' || a[i - 1] == 'E') + && (a[i] == 'r' || a[i] == 'R')) { + matchlen = 8; + mask |= ACTION_REGISTER; + + } else { + // parse error + throw new IllegalArgumentException("invalid permission: " + actions); + } + + // make sure we didn't just match the tail of a word + // like "ackbarfregister". Also, skip to the comma. + seencomma = false; + while (i >= matchlen && !seencomma) { + switch (a[i - matchlen]) { + case ',' : + seencomma = true; + /* FALLTHROUGH */ + case ' ' : + case '\r' : + case '\n' : + case '\f' : + case '\t' : + break; + default : + throw new IllegalArgumentException("invalid permission: " + actions); + } + i--; + } + + // point i at the location of the comma minus one (or -1). + i -= matchlen; + } + + if (seencomma) { + throw new IllegalArgumentException("invalid permission: " + actions); + } + + return mask; + } + + /** + * Parse filter string into a Filter object. + * + * @param filterString The filter string to parse. + * @return a Filter for this bundle. If the specified filterString is not a + * filter expression, then {@code null} is returned. + * @throws IllegalArgumentException If the filter syntax is invalid. + */ + private static Filter parseFilter(String filterString) { + filterString = filterString.trim(); + if (filterString.charAt(0) != '(') { + return null; + } + + try { + return FrameworkUtil.createFilter(filterString); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException("invalid filter", e); + } + } + + /** + * Determines if a {@code ServicePermission} object "implies" the specified + * permission. + * + * @param p The target permission to check. + * @return {@code true} if the specified permission is implied by this + * object; {@code false} otherwise. + */ + @Override + public boolean implies(Permission p) { + if (!(p instanceof ServicePermission)) { + return false; + } + ServicePermission requested = (ServicePermission) p; + if (service != null) { + return false; + } + // if requested permission has a filter, then it is an invalid argument + if (requested.filter != null) { + return false; + } + return implies0(requested, ACTION_NONE); + } + + /** + * Internal implies method. Used by the implies and the permission + * collection implies methods. + * + * @param requested The requested ServicePermission which has already be + * validated as a proper argument. The requested ServicePermission + * must not have a filter expression. + * @param effective The effective actions with which to start. + * @return {@code true} if the specified permission is implied by this + * object; {@code false} otherwise. + */ + boolean implies0(ServicePermission requested, int effective) { + /* check actions first - much faster */ + effective |= action_mask; + final int desired = requested.action_mask; + if ((effective & desired) != desired) { + return false; + } + /* we have name of "*" */ + if (wildcard && (prefix == null)) { + return true; + } + /* if we have a filter */ + Filter f = filter; + if (f != null) { + return f.matches(requested.getProperties()); + } + /* if requested permission not created with ServiceReference */ + String[] requestedNames = requested.objectClass; + if (requestedNames == null) { + return super.implies(requested); + } + /* requested permission created with ServiceReference */ + if (wildcard) { + int pl = prefix.length(); + for (int i = 0, l = requestedNames.length; i < l; i++) { + String requestedName = requestedNames[i]; + if ((requestedName.length() > pl) && requestedName.startsWith(prefix)) { + return true; + } + } + } else { + String name = getName(); + for (int i = 0, l = requestedNames.length; i < l; i++) { + if (requestedNames[i].equals(name)) { + return true; + } + } + } + return false; + } + + /** + * Returns the canonical string representation of the actions. Always + * returns present actions in the following order: {@code get}, + * {@code register}. + * + * @return The canonical string representation of the actions. + */ + @Override + public String getActions() { + String result = actions; + if (result == null) { + StringBuilder sb = new StringBuilder(); + boolean comma = false; + + int mask = action_mask; + if ((mask & ACTION_GET) == ACTION_GET) { + sb.append(GET); + comma = true; + } + + if ((mask & ACTION_REGISTER) == ACTION_REGISTER) { + if (comma) + sb.append(','); + sb.append(REGISTER); + } + + actions = result = sb.toString(); + } + + return result; + } + + /** + * Returns a new {@code PermissionCollection} object for storing + * {@code ServicePermission} objects. + * + * @return A new {@code PermissionCollection} object suitable for storing + * {@code ServicePermission} objects. + */ + @Override + public PermissionCollection newPermissionCollection() { + return new ServicePermissionCollection(); + } + + /** + * Determines the equality of two ServicePermission objects. + * + * Checks that specified object has the same class name and action as this + * {@code ServicePermission}. + * + * @param obj The object to test for equality. + * @return true if obj is a {@code ServicePermission}, and has the same + * class name and actions as this {@code ServicePermission} object; + * {@code false} otherwise. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (!(obj instanceof ServicePermission)) { + return false; + } + + ServicePermission sp = (ServicePermission) obj; + + return (action_mask == sp.action_mask) && getName().equals(sp.getName()) && ((service == sp.service) || ((service != null) && (service.compareTo(sp.service) == 0))); + } + + /** + * Returns the hash code value for this object. + * + * @return Hash code value for this object. + */ + @Override + public int hashCode() { + int h = 31 * 17 + getName().hashCode(); + h = 31 * h + getActions().hashCode(); + if (service != null) { + h = 31 * h + service.hashCode(); + } + return h; + } + + /** + * WriteObject is called to save the state of this permission to a stream. + * The actions are serialized, and the superclass takes care of the name. + */ + private synchronized void writeObject(java.io.ObjectOutputStream s) throws IOException { + if (service != null) { + throw new NotSerializableException("cannot serialize"); + } + // Write out the actions. The superclass takes care of the name + // call getActions to make sure actions field is initialized + if (actions == null) + getActions(); + s.defaultWriteObject(); + } + + /** + * readObject is called to restore the state of this permission from a + * stream. + */ + private synchronized void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { + // Read in the action, then initialize the rest + s.defaultReadObject(); + setTransients(parseFilter(getName()), parseActions(actions)); + } + + /** + * Called by {@link ServicePermission#implies(Permission)}. This method is + * only called on a requested permission which cannot have a filter set. + * + * @return a map of properties for this permission. + */ + private Map getProperties() { + Map result = properties; + if (result != null) { + return result; + } + if (service == null) { + result = new HashMap(1); + result.put(Constants.OBJECTCLASS, new String[] {getName()}); + return properties = result; + } + final Map props = new HashMap(4); + final Bundle bundle = service.getBundle(); + if (bundle != null) { + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Void run() { + props.put("id", Long.valueOf(bundle.getBundleId())); + props.put("location", bundle.getLocation()); + String name = bundle.getSymbolicName(); + if (name != null) { + props.put("name", name); + } + SignerProperty signer = new SignerProperty(bundle); + if (signer.isBundleSigned()) { + props.put("signer", signer); + } + return null; + } + }); + } + return properties = new Properties(props, service); + } + + static private final class Properties extends AbstractMap { + private final Map properties; + private final ServiceReference service; + private transient volatile Set> entries; + + Properties(Map properties, ServiceReference service) { + this.properties = properties; + this.service = service; + entries = null; + } + + @Override + public Object get(Object k) { + if (!(k instanceof String)) { + return null; + } + String key = (String) k; + if (key.charAt(0) == '@') { + return service.getProperty(key.substring(1)); + } + Object value = properties.get(key); + if (value != null) { // fall back to service properties + return value; + } + return service.getProperty(key); + } + + @Override + public Set> entrySet() { + if (entries != null) { + return entries; + } + Set> all = new HashSet>(properties.entrySet()); + add: for (String key : service.getPropertyKeys()) { + for (String k : properties.keySet()) { + if (key.equalsIgnoreCase(k)) { + continue add; + } + } + all.add(new Entry(key, service.getProperty(key))); + } + return entries = Collections.unmodifiableSet(all); + } + + static private final class Entry implements Map.Entry { + private final String k; + private final Object v; + + Entry(String key, Object value) { + this.k = key; + this.v = value; + } + + @Override + public String getKey() { + return k; + } + + @Override + public Object getValue() { + return v; + } + + @Override + public Object setValue(Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() { + return k + "=" + v; + } + + @Override + public int hashCode() { + return ((k == null) ? 0 : k.hashCode()) ^ ((v == null) ? 0 : v.hashCode()); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Map.Entry)) { + return false; + } + Map.Entry e = (Map.Entry) obj; + final Object key = e.getKey(); + if ((k == key) || ((k != null) && k.equals(key))) { + final Object value = e.getValue(); + if ((v == value) || ((v != null) && v.equals(value))) { + return true; + } + } + return false; + } + } + } +} + +/** + * Stores a set of ServicePermission permissions. + * + * @see java.security.Permission + * @see java.security.Permissions + * @see java.security.PermissionCollection + */ +final class ServicePermissionCollection extends PermissionCollection { + static final long serialVersionUID = 662615640374640621L; + /** + * Table of permissions. + * + * @GuardedBy this + */ + private transient Map permissions; + + /** + * Boolean saying if "*" is in the collection. + * + * @serial + * @GuardedBy this + */ + private boolean all_allowed; + + /** + * Table of permissions with filter expressions. + * + * @serial + * @GuardedBy this + */ + private Map filterPermissions; + + /** + * Creates an empty ServicePermissions object. + */ + public ServicePermissionCollection() { + permissions = new HashMap(); + all_allowed = false; + } + + /** + * Adds a permission to this permission collection. + * + * @param permission The Permission object to add. + * @throws IllegalArgumentException If the specified permission is not a + * ServicePermission object. + * @throws SecurityException If this {@code ServicePermissionCollection} + * object has been marked read-only. + */ + @Override + public void add(final Permission permission) { + if (!(permission instanceof ServicePermission)) { + throw new IllegalArgumentException("invalid permission: " + permission); + } + if (isReadOnly()) { + throw new SecurityException("attempt to add a Permission to a " + "readonly PermissionCollection"); + } + + final ServicePermission sp = (ServicePermission) permission; + if (sp.service != null) { + throw new IllegalArgumentException("cannot add to collection: " + sp); + } + + final String name = sp.getName(); + final Filter f = sp.filter; + synchronized (this) { + /* select the bucket for the permission */ + Map pc; + if (f != null) { + pc = filterPermissions; + if (pc == null) { + filterPermissions = pc = new HashMap(); + } + } else { + pc = permissions; + } + final ServicePermission existing = pc.get(name); + + if (existing != null) { + final int oldMask = existing.action_mask; + final int newMask = sp.action_mask; + if (oldMask != newMask) { + pc.put(name, new ServicePermission(name, oldMask | newMask)); + } + } else { + pc.put(name, sp); + } + + if (!all_allowed) { + if (name.equals("*")) { + all_allowed = true; + } + } + } + } + + /** + * Determines if a set of permissions implies the permissions expressed in + * {@code permission}. + * + * @param permission The Permission object to compare. + * @return {@code true} if {@code permission} is a proper subset of a + * permission in the set; {@code false} otherwise. + */ + @Override + public boolean implies(final Permission permission) { + if (!(permission instanceof ServicePermission)) { + return false; + } + final ServicePermission requested = (ServicePermission) permission; + /* if requested permission has a filter, then it is an invalid argument */ + if (requested.filter != null) { + return false; + } + + int effective = ServicePermission.ACTION_NONE; + Collection perms; + synchronized (this) { + final int desired = requested.action_mask; + /* short circuit if the "*" Permission was added */ + if (all_allowed) { + ServicePermission sp = permissions.get("*"); + if (sp != null) { + effective |= sp.action_mask; + if ((effective & desired) == desired) { + return true; + } + } + } + + String[] requestedNames = requested.objectClass; + /* if requested permission not created with ServiceReference */ + if (requestedNames == null) { + effective |= effective(requested.getName(), desired, effective); + if ((effective & desired) == desired) { + return true; + } + } + /* requested permission created with ServiceReference */ + else { + for (int i = 0, l = requestedNames.length; i < l; i++) { + if ((effective(requestedNames[i], desired, effective) & desired) == desired) { + return true; + } + } + } + Map pc = filterPermissions; + if (pc == null) { + return false; + } + perms = pc.values(); + } + + /* iterate one by one over filteredPermissions */ + for (ServicePermission perm : perms) { + if (perm.implies0(requested, effective)) { + return true; + } + } + return false; + } + + /** + * Consult permissions map to compute the effective permission for the + * requested permission name. + * + * @param requestedName The requested service name. + * @param desired The desired actions. + * @param effective The effective actions. + * @return The new effective actions. + */ + private int effective(String requestedName, final int desired, int effective) { + final Map pc = permissions; + ServicePermission sp = pc.get(requestedName); + // strategy: + // Check for full match first. Then work our way up the + // name looking for matches on a.b.* + if (sp != null) { + // we have a direct hit! + effective |= sp.action_mask; + if ((effective & desired) == desired) { + return effective; + } + } + // work our way up the tree... + int last; + int offset = requestedName.length() - 1; + while ((last = requestedName.lastIndexOf(".", offset)) != -1) { + requestedName = requestedName.substring(0, last + 1) + "*"; + sp = pc.get(requestedName); + if (sp != null) { + effective |= sp.action_mask; + if ((effective & desired) == desired) { + return effective; + } + } + offset = last - 1; + } + /* + * we don't have to check for "*" as it was already checked before we + * were called. + */ + return effective; + } + + /** + * Returns an enumeration of all the {@code ServicePermission} objects in + * the container. + * + * @return Enumeration of all the ServicePermission objects. + */ + @Override + public synchronized Enumeration elements() { + List all = new ArrayList(permissions.values()); + Map pc = filterPermissions; + if (pc != null) { + all.addAll(pc.values()); + } + return Collections.enumeration(all); + } + + /* serialization logic */ + private static final ObjectStreamField[] serialPersistentFields = {new ObjectStreamField("permissions", Hashtable.class), new ObjectStreamField("all_allowed", Boolean.TYPE), + new ObjectStreamField("filterPermissions", HashMap.class) }; + + private synchronized void writeObject(ObjectOutputStream out) throws IOException { + Hashtable hashtable = new Hashtable(permissions); + ObjectOutputStream.PutField pfields = out.putFields(); + pfields.put("permissions", hashtable); + pfields.put("all_allowed", all_allowed); + pfields.put("filterPermissions", filterPermissions); + out.writeFields(); + } + + private synchronized void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { + ObjectInputStream.GetField gfields = in.readFields(); + @SuppressWarnings("unchecked") + Hashtable hashtable = (Hashtable) gfields.get("permissions", null); + permissions = new HashMap(hashtable); + all_allowed = gfields.get("all_allowed", false); + @SuppressWarnings("unchecked") + HashMap fp = (HashMap) gfields.get("filterPermissions", null); + filterPermissions = fp; + } +} diff --git a/framework/src/main/java/org/osgi/framework/ServiceReference.java b/framework/src/main/java/org/osgi/framework/ServiceReference.java new file mode 100644 index 00000000000..5d1b1756608 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/ServiceReference.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import java.util.Dictionary; + +import org.osgi.annotation.versioning.ProviderType; + +/** + * A reference to a service. + * + *

      + * The Framework returns {@code ServiceReference} objects from the + * {@code BundleContext.getServiceReference} and + * {@code BundleContext.getServiceReferences} methods. + *

      + * A {@code ServiceReference} object may be shared between bundles and can be + * used to examine the properties of the service and to get the service object. + *

      + * Every service registered in the Framework has a unique + * {@code ServiceRegistration} object and may have multiple, distinct + * {@code ServiceReference} objects referring to it. {@code ServiceReference} + * objects associated with a {@code ServiceRegistration} object have the same + * {@code hashCode} and are considered equal (more specifically, their + * {@code equals()} method will return {@code true} when compared). + *

      + * If the same service object is registered multiple times, + * {@code ServiceReference} objects associated with different + * {@code ServiceRegistration} objects are not equal. + * + * @param Type of Service. + * @see BundleContext#getServiceReference(Class) + * @see BundleContext#getServiceReference(String) + * @see BundleContext#getServiceReferences(Class, String) + * @see BundleContext#getServiceReferences(String, String) + * @see BundleContext#getService(ServiceReference) + * @see BundleContext#getServiceObjects(ServiceReference) + * @ThreadSafe + * @author $Id: 1454244c30992b7a52ac3838b03bc584c3495816 $ + */ +@ProviderType +public interface ServiceReference extends Comparable { + /** + * Returns the property value to which the specified property key is mapped + * in the properties {@code Dictionary} object of the service referenced by + * this {@code ServiceReference} object. + * + *

      + * Property keys are case-insensitive. + * + *

      + * This method must continue to return property values after the service has + * been unregistered. This is so references to unregistered services (for + * example, {@code ServiceReference} objects stored in the log) can still be + * interrogated. + * + * @param key The property key. + * @return The property value to which the key is mapped; {@code null} if + * there is no property named after the key. + */ + public Object getProperty(String key); + + /** + * Returns an array of the keys in the properties {@code Dictionary} object + * of the service referenced by this {@code ServiceReference} object. + * + *

      + * This method will continue to return the keys after the service has been + * unregistered. This is so references to unregistered services (for + * example, {@code ServiceReference} objects stored in the log) can still be + * interrogated. + * + *

      + * This method is case-preserving ; this means that every key in the + * returned array must have the same case as the corresponding key in the + * properties {@code Dictionary} that was passed to the + * {@link BundleContext#registerService(String[],Object,Dictionary)} or + * {@link ServiceRegistration#setProperties(Dictionary)} methods. + * + * @return An array of property keys. + */ + public String[] getPropertyKeys(); + + /** + * Returns the bundle that registered the service referenced by this + * {@code ServiceReference} object. + * + *

      + * This method must return {@code null} when the service has been + * unregistered. This can be used to determine if the service has been + * unregistered. + * + * @return The bundle that registered the service referenced by this + * {@code ServiceReference} object; {@code null} if that service has + * already been unregistered. + * @see BundleContext#registerService(String[],Object,Dictionary) + */ + public Bundle getBundle(); + + /** + * Returns the bundles that are using the service referenced by this + * {@code ServiceReference} object. Specifically, this method returns the + * bundles whose usage count for that service is greater than zero. + * + * @return An array of bundles whose usage count for the service referenced + * by this {@code ServiceReference} object is greater than zero; + * {@code null} if no bundles are currently using that service. + * + * @since 1.1 + */ + public Bundle[] getUsingBundles(); + + /** + * Tests if the bundle that registered the service referenced by this + * {@code ServiceReference} and the specified bundle use the same source for + * the package of the specified class name. + *

      + * This method performs the following checks: + *

        + *
      1. Get the package name from the specified class name.
      2. + *
      3. For the bundle that registered the service referenced by this + * {@code ServiceReference} (registrant bundle); find the source for the + * package. If no source is found then return {@code true} if the registrant + * bundle is equal to the specified bundle; otherwise return {@code false}.
      4. + *
      5. If the package source of the registrant bundle is equal to the + * package source of the specified bundle then return {@code true}; + * otherwise return {@code false}.
      6. + *
      + * + * @param bundle The {@code Bundle} object to check. + * @param className The class name to check. + * @return {@code true} if the bundle which registered the service + * referenced by this {@code ServiceReference} and the specified + * bundle use the same source for the package of the specified class + * name. Otherwise {@code false} is returned. + * @throws IllegalArgumentException If the specified {@code Bundle} was not + * created by the same framework instance as this + * {@code ServiceReference}. + * @since 1.3 + */ + public boolean isAssignableTo(Bundle bundle, String className); + + /** + * Compares this {@code ServiceReference} with the specified + * {@code ServiceReference} for order. + * + *

      + * If this {@code ServiceReference} and the specified + * {@code ServiceReference} have the same {@link Constants#SERVICE_ID + * service id} they are equal. This {@code ServiceReference} is less than + * the specified {@code ServiceReference} if it has a lower + * {@link Constants#SERVICE_RANKING service ranking} and greater if it has a + * higher service ranking. Otherwise, if this {@code ServiceReference} and + * the specified {@code ServiceReference} have the same + * {@link Constants#SERVICE_RANKING service ranking}, this + * {@code ServiceReference} is less than the specified + * {@code ServiceReference} if it has a higher {@link Constants#SERVICE_ID + * service id} and greater if it has a lower service id. + * + * @param reference The {@code ServiceReference} to be compared. + * @return Returns a negative integer, zero, or a positive integer if this + * {@code ServiceReference} is less than, equal to, or greater than + * the specified {@code ServiceReference}. + * @throws IllegalArgumentException If the specified + * {@code ServiceReference} was not created by the same framework + * instance as this {@code ServiceReference}. + * @since 1.4 + */ + @Override + public int compareTo(Object reference); + + /** + * Returns a copy of the properties of the service referenced by this + * {@code ServiceReference} object. + *

      + * This method will continue to return the properties after the service has + * been unregistered. This is so references to unregistered services (for + * example, {@code ServiceReference} objects stored in the log) can still be + * interrogated. + *

      + * The returned {@code Dictionary} object: + *

        + *
      • Must map property values by using property keys in a + * case-insensitive manner.
      • + *
      • Must return property keys is a case-preserving manner. This + * means that the keys must have the same case as the corresponding key in + * the properties {@code Dictionary} that was passed to the + * {@link BundleContext#registerService(String[],Object,Dictionary)} or + * {@link ServiceRegistration#setProperties(Dictionary)} methods.
      • + *
      • Is the property of the caller and can be modified by the caller but + * any changes are not reflected in the properties of the service. + * {@link ServiceRegistration#setProperties(Dictionary)} must be called to + * modify the properties of the service.
      • + *
      + * + * @return A copy of the properties of the service referenced by this + * {@code ServiceReference} object + * @since 1.9 + */ + public Dictionary getProperties(); +} diff --git a/framework/src/main/java/org/osgi/framework/ServiceRegistration.java b/framework/src/main/java/org/osgi/framework/ServiceRegistration.java new file mode 100644 index 00000000000..b50e6cbb621 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/ServiceRegistration.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import java.util.Dictionary; +import org.osgi.annotation.versioning.ProviderType; + +/** + * A registered service. + * + *

      + * The Framework returns a {@code ServiceRegistration} object when a + * {@code BundleContext.registerService} method invocation is successful. The + * {@code ServiceRegistration} object is for the private use of the registering + * bundle and should not be shared with other bundles. + *

      + * The {@code ServiceRegistration} object may be used to update the properties + * of the service or to unregister the service. + * + * @param Type of Service. + * @see BundleContext#registerService(String[],Object,Dictionary) + * @ThreadSafe + * @author $Id: 0bc5bfa68ae7cb4a409c066585d3ab4077d80eeb $ + */ +@ProviderType +public interface ServiceRegistration { + /** + * Returns a {@code ServiceReference} object for a service being registered. + *

      + * The {@code ServiceReference} object may be shared with other bundles. + * + * @throws IllegalStateException If this {@code ServiceRegistration} object + * has already been unregistered. + * @return {@code ServiceReference} object. + */ + public ServiceReference getReference(); + + /** + * Updates the properties associated with a service. + * + *

      + * The {@link Constants#OBJECTCLASS}, {@link Constants#SERVICE_BUNDLEID}, + * {@link Constants#SERVICE_ID} and {@link Constants#SERVICE_SCOPE} keys + * cannot be modified by this method. These values are set by the Framework + * when the service is registered in the OSGi environment. + * + *

      + * The following steps are required to modify service properties: + *

        + *
      1. The service's properties are replaced with the provided properties.
      2. + *
      3. A service event of type {@link ServiceEvent#MODIFIED} is fired.
      4. + *
      + * + * @param properties The properties for this service. See {@link Constants} + * for a list of standard service property keys. Changes should not + * be made to this object after calling this method. To update the + * service's properties this method should be called again. + * + * @throws IllegalStateException If this {@code ServiceRegistration} object + * has already been unregistered. + * @throws IllegalArgumentException If {@code properties} contains case + * variants of the same key name. + */ + public void setProperties(Dictionary properties); + + /** + * Unregisters a service. Remove a {@code ServiceRegistration} object from + * the Framework service registry. All {@code ServiceReference} objects + * associated with this {@code ServiceRegistration} object can no longer be + * used to interact with the service once unregistration is complete. + * + *

      + * The following steps are required to unregister a service: + *

        + *
      1. The service is removed from the Framework service registry so that it + * can no longer be obtained.
      2. + *
      3. A service event of type {@link ServiceEvent#UNREGISTERING} is fired + * so that bundles using this service can release their use of the service. + * Once delivery of the service event is complete, the + * {@code ServiceReference} objects for the service may no longer be used to + * get a service object for the service.
      4. + *
      5. For each bundle whose use count for this service is greater than + * zero: + *
          + *
        • The bundle's use count for this service is set to zero.
        • + *
        • If the service was registered with a {@link ServiceFactory} object, + * the {@code ServiceFactory.ungetService} method is called to release the + * service object for the bundle.
        • + *
        + *
      6. + *
      + * + * @throws IllegalStateException If this {@code ServiceRegistration} object + * has already been unregistered. + * @see BundleContext#ungetService(ServiceReference) + * @see ServiceFactory#ungetService(Bundle, ServiceRegistration, Object) + */ + public void unregister(); +} diff --git a/framework/src/main/java/org/osgi/framework/SignerProperty.java b/framework/src/main/java/org/osgi/framework/SignerProperty.java new file mode 100644 index 00000000000..0dbb6caeabb --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/SignerProperty.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) OSGi Alliance (2009, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Package private class used by permissions for filter matching on signer key + * during filter expression evaluation in the permission implies method. + * + * @Immutable + * @author $Id: 53dec4a366c1c419baeb1e3b7d6b2b3bf172ad93 $ + */ +final class SignerProperty { + private final Bundle bundle; + private final String pattern; + + /** + * String constructor used by the filter matching algorithm to construct a + * SignerProperty from the attribute value in a filter expression. + * + * @param pattern Attribute value in the filter expression. + */ + public SignerProperty(String pattern) { + this.pattern = pattern; + this.bundle = null; + } + + /** + * Used by the permission implies method to build the properties for a + * filter match. + * + * @param bundle The bundle whose signers are to be matched. + */ + SignerProperty(Bundle bundle) { + this.bundle = bundle; + this.pattern = null; + } + + /** + * Used by the filter matching algorithm. This methods does NOT satisfy the + * normal equals contract. Since the class is only used in filter expression + * evaluations, it only needs to support comparing an instance created with + * a Bundle to an instance created with a pattern string from the filter + * expression. + * + * @param o SignerProperty to compare against. + * @return true if the DN name chain matches the pattern. + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof SignerProperty)) + return false; + SignerProperty other = (SignerProperty) o; + Bundle matchBundle = bundle != null ? bundle : other.bundle; + String matchPattern = bundle != null ? other.pattern : pattern; + Map> signers = matchBundle.getSignerCertificates(Bundle.SIGNERS_TRUSTED); + for (List signerCerts : signers.values()) { + List dnChain = new ArrayList(signerCerts.size()); + for (X509Certificate signerCert : signerCerts) { + dnChain.add(signerCert.getSubjectDN().getName()); + } + try { + if (FrameworkUtil.matchDistinguishedNameChain(matchPattern, dnChain)) { + return true; + } + } catch (IllegalArgumentException e) { + continue; // bad pattern + } + } + return false; + } + + /** + * Since the equals method does not obey the general equals contract, this + * method cannot generate hash codes which obey the equals contract. + */ + @Override + public int hashCode() { + return 31; + } + + /** + * Check if the bundle is signed. + * + * @return true if constructed with a bundle that is signed. + */ + boolean isBundleSigned() { + if (bundle == null) { + return false; + } + Map> signers = bundle.getSignerCertificates(Bundle.SIGNERS_TRUSTED); + return !signers.isEmpty(); + } +} diff --git a/framework/src/main/java/org/osgi/framework/SynchronousBundleListener.java b/framework/src/main/java/org/osgi/framework/SynchronousBundleListener.java new file mode 100644 index 00000000000..b65290e0da9 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/SynchronousBundleListener.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) OSGi Alliance (2001, 2015). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import org.osgi.annotation.versioning.ConsumerType; + +/** + * A synchronous {@code BundleEvent} listener. {@code SynchronousBundleListener} + * is a listener interface that may be implemented by a bundle developer. When a + * {@code BundleEvent} is fired, it is synchronously delivered to a + * {@code SynchronousBundleListener}. The Framework may deliver + * {@code BundleEvent} objects to a {@code SynchronousBundleListener} out of + * order and may concurrently call and/or reenter a + * {@code SynchronousBundleListener}. + * + *

      + * For {@code BundleEvent} types {@link BundleEvent#STARTED STARTED} and + * {@link BundleEvent#LAZY_ACTIVATION LAZY_ACTIVATION}, the Framework must not + * hold the referenced bundle's "state change" lock when the + * {@code BundleEvent} is delivered to a {@code SynchronousBundleListener}. For + * the other {@code BundleEvent} types, the Framework must hold the referenced + * bundle's "state change" lock when the {@code BundleEvent} is + * delivered to a {@code SynchronousBundleListener}. A + * {@code SynchronousBundleListener} cannot directly call life cycle methods on + * the referenced bundle when the Framework is holding the referenced bundle's + * "state change" lock. + * + *

      + * A {@code SynchronousBundleListener} object is registered with the Framework + * using the {@link BundleContext#addBundleListener(BundleListener)} method. + * {@code SynchronousBundleListener} objects are called with a + * {@code BundleEvent} object when a bundle has been installed, resolved, + * starting, started, stopping, stopped, updated, unresolved, or uninstalled. + *

      + * Unlike normal {@code BundleListener} objects, + * {@code SynchronousBundleListener}s are synchronously called during bundle + * lifecycle processing. The bundle lifecycle processing will not proceed until + * all {@code SynchronousBundleListener}s have completed. + * {@code SynchronousBundleListener} objects will be called prior to + * {@code BundleListener} objects. + *

      + * {@code AdminPermission[bundle,LISTENER]} is required to add or remove a + * {@code SynchronousBundleListener} object. + * + * @since 1.1 + * @see BundleEvent + * @ThreadSafe + * @author $Id: e1555f69b0cba5d9d8b6ad51add3d9e02827ef66 $ + */ +@ConsumerType +@FunctionalInterface +public interface SynchronousBundleListener extends BundleListener { + // This is a marker interface +} diff --git a/framework/src/main/java/org/osgi/framework/UnfilteredServiceListener.java b/framework/src/main/java/org/osgi/framework/UnfilteredServiceListener.java new file mode 100644 index 00000000000..8b6c88241e0 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/UnfilteredServiceListener.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) OSGi Alliance (2011, 2015). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import org.osgi.annotation.versioning.ConsumerType; +import org.osgi.framework.hooks.service.ListenerHook; + +/** + * A {@code ServiceEvent} listener that does not filter based upon any + * filter string specified to + * {@link BundleContext#addServiceListener(ServiceListener, String)}. Using an + * {@code UnfilteredServiceListener} and specifying a filter string to + * {@link BundleContext#addServiceListener(ServiceListener, String)} allows the + * listener to receive all {@code ServiceEvent} objects while still advising + * {@link ListenerHook} implementation of the service interests in the filter + * string. + * + * For example, an implementation of Declarative Services would add an + * {@code UnfilteredServiceListener} with a filter string listing all the + * services referenced by all the service components. The Declarative Services + * implementation would receive all {@code ServiceEvent} objects for internal + * processing and a Remote Services discovery service implementation can observe + * the service interests of the service components using a {@link ListenerHook}. + * When the set of service components being processed changes, the Declarative + * Services implementation would re-add the {@code UnfilteredServiceListener} + * with an updated filter string. + * + *

      + * When a {@code ServiceEvent} is fired, it is synchronously delivered to an + * {@code UnfilteredServiceListener}. The Framework may deliver + * {@code ServiceEvent} objects to an {@code UnfilteredServiceListener} out of + * order and may concurrently call and/or reenter an + * {@code UnfilteredServiceListener}. + * + *

      + * An {@code UnfilteredServiceListener} object is registered with the Framework + * using the {@code BundleContext.addServiceListener} method. + * {@code UnfilteredServiceListener} objects are called with a + * {@code ServiceEvent} object when a service is registered, modified, or is in + * the process of unregistering. + * + *

      + * {@code ServiceEvent} object delivery to {@code UnfilteredServiceListener} + * objects are not filtered by the filter specified when the listener was + * registered. If the Java Runtime Environment supports permissions, then some + * filtering is done. {@code ServiceEvent} objects are only delivered to the + * listener if the bundle which defines the listener object's class has the + * appropriate {@code ServicePermission} to get the service using at least one + * of the named classes under which the service was registered. + * + * @see ServiceEvent + * @see ServicePermission + * @ThreadSafe + * @since 1.7 + * @author $Id: ed5080773ff2a67f5f2ebf98628b7f14be115f73 $ + */ +@ConsumerType +@FunctionalInterface +public interface UnfilteredServiceListener extends ServiceListener { + // This is a marker interface +} diff --git a/framework/src/main/java/org/osgi/framework/Version.java b/framework/src/main/java/org/osgi/framework/Version.java new file mode 100644 index 00000000000..1110fe05230 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/Version.java @@ -0,0 +1,426 @@ +/* + * Copyright (c) OSGi Alliance (2004, 2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import java.util.NoSuchElementException; +import java.util.StringTokenizer; + +/** + * Version identifier for capabilities such as bundles and packages. + * + *

      + * Version identifiers have four components. + *

        + *
      1. Major version. A non-negative integer.
      2. + *
      3. Minor version. A non-negative integer.
      4. + *
      5. Micro version. A non-negative integer.
      6. + *
      7. Qualifier. A text string. See {@code Version(String)} for the format of + * the qualifier string.
      8. + *
      + * + *

      + * {@code Version} objects are immutable. + * + * @since 1.3 + * @Immutable + * @author $Id: 2a5e4b8c63928ffda304dfe523fc06df49c68eae $ + */ + +public class Version implements Comparable { + private final int major; + private final int minor; + private final int micro; + private final String qualifier; + private static final String SEPARATOR = "."; + private transient String versionString /* default to null */; + private transient int hash /* default to 0 */; + + /** + * The empty version "0.0.0". + */ + public static final Version emptyVersion = new Version(0, 0, 0); + + /** + * Creates a version identifier from the specified numerical components. + * + *

      + * The qualifier is set to the empty string. + * + * @param major Major component of the version identifier. + * @param minor Minor component of the version identifier. + * @param micro Micro component of the version identifier. + * @throws IllegalArgumentException If the numerical components are + * negative. + */ + public Version(int major, int minor, int micro) { + this(major, minor, micro, null); + } + + /** + * Creates a version identifier from the specified components. + * + * @param major Major component of the version identifier. + * @param minor Minor component of the version identifier. + * @param micro Micro component of the version identifier. + * @param qualifier Qualifier component of the version identifier. If + * {@code null} is specified, then the qualifier will be set to the + * empty string. + * @throws IllegalArgumentException If the numerical components are negative + * or the qualifier string is invalid. + */ + public Version(int major, int minor, int micro, String qualifier) { + if (qualifier == null) { + qualifier = ""; + } + + this.major = major; + this.minor = minor; + this.micro = micro; + this.qualifier = qualifier; + validate(); + } + + /** + * Creates a version identifier from the specified string. + * + *

      + * Version string grammar: + * + *

      +	 * version ::= major('.'minor('.'micro('.'qualifier)?)?)?
      +	 * major ::= digit+
      +	 * minor ::= digit+
      +	 * micro ::= digit+
      +	 * qualifier ::= (alpha|digit|'_'|'-')+
      +	 * digit ::= [0..9]
      +	 * alpha ::= [a..zA..Z]
      +	 * 
      + * + * @param version String representation of the version identifier. There + * must be no whitespace in the argument. + * @throws IllegalArgumentException If {@code version} is improperly + * formatted. + */ + public Version(String version) { + int maj = 0; + int min = 0; + int mic = 0; + String qual = ""; + + try { + StringTokenizer st = new StringTokenizer(version, SEPARATOR, true); + maj = parseInt(st.nextToken(), version); + + if (st.hasMoreTokens()) { // minor + st.nextToken(); // consume delimiter + min = parseInt(st.nextToken(), version); + + if (st.hasMoreTokens()) { // micro + st.nextToken(); // consume delimiter + mic = parseInt(st.nextToken(), version); + + if (st.hasMoreTokens()) { // qualifier separator + st.nextToken(); // consume delimiter + qual = st.nextToken(""); // remaining string + + if (st.hasMoreTokens()) { // fail safe + throw new IllegalArgumentException("invalid version \"" + version + "\": invalid format"); + } + } + } + } + } catch (NoSuchElementException e) { + throw new IllegalArgumentException( + "invalid version \"" + version + "\": invalid format", e); + } + + major = maj; + minor = min; + micro = mic; + qualifier = qual; + validate(); + } + + /** + * Parse numeric component into an int. + * + * @param value Numeric component + * @param version Complete version string for exception message, if any + * @return int value of numeric component + */ + private static int parseInt(String value, String version) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("invalid version \"" + version + + "\": non-numeric \"" + value + "\"", e); + } + } + + /** + * Called by the Version constructors to validate the version components. + * + * @throws IllegalArgumentException If the numerical components are negative + * or the qualifier string is invalid. + */ + private void validate() { + if (major < 0) { + throw new IllegalArgumentException("invalid version \"" + toString0() + "\": negative number \"" + major + "\""); + } + if (minor < 0) { + throw new IllegalArgumentException("invalid version \"" + toString0() + "\": negative number \"" + minor + "\""); + } + if (micro < 0) { + throw new IllegalArgumentException("invalid version \"" + toString0() + "\": negative number \"" + micro + "\""); + } + for (char ch : qualifier.toCharArray()) { + if (('A' <= ch) && (ch <= 'Z')) { + continue; + } + if (('a' <= ch) && (ch <= 'z')) { + continue; + } + if (('0' <= ch) && (ch <= '9')) { + continue; + } + if ((ch == '_') || (ch == '-')) { + continue; + } + throw new IllegalArgumentException("invalid version \"" + toString0() + "\": invalid qualifier \"" + qualifier + "\""); + } + } + + /** + * Parses a version identifier from the specified string. + * + *

      + * See {@link #Version(String)} for the format of the version string. + * + * @param version String representation of the version identifier. Leading + * and trailing whitespace will be ignored. + * @return A {@code Version} object representing the version identifier. If + * {@code version} is {@code null} or the empty string then + * {@link #emptyVersion} will be returned. + * @throws IllegalArgumentException If {@code version} is improperly + * formatted. + */ + public static Version parseVersion(String version) { + if (version == null) { + return emptyVersion; + } + + return valueOf(version); + } + + /** + * Returns a {@code Version} object holding the version identifier in the + * specified {@code String}. + * + *

      + * See {@link #Version(String)} for the format of the version string. + * + *

      + * This method performs a similar function as {@link #parseVersion(String)} + * but has the static factory {@code valueOf(String)} method signature. + * + * @param version String representation of the version identifier. Leading + * and trailing whitespace will be ignored. Must not be {@code null}. + * @return A {@code Version} object representing the version identifier. If + * {@code version} is the empty string then {@link #emptyVersion} + * will be returned. + * @throws IllegalArgumentException If {@code version} is improperly + * formatted. + * @since 1.8 + */ + public static Version valueOf(String version) { + version = version.trim(); + if (version.length() == 0) { + return emptyVersion; + } + + return new Version(version); + } + + /** + * Returns the major component of this version identifier. + * + * @return The major component. + */ + public int getMajor() { + return major; + } + + /** + * Returns the minor component of this version identifier. + * + * @return The minor component. + */ + public int getMinor() { + return minor; + } + + /** + * Returns the micro component of this version identifier. + * + * @return The micro component. + */ + public int getMicro() { + return micro; + } + + /** + * Returns the qualifier component of this version identifier. + * + * @return The qualifier component. + */ + public String getQualifier() { + return qualifier; + } + + /** + * Returns the string representation of this version identifier. + * + *

      + * The format of the version string will be {@code major.minor.micro} if + * qualifier is the empty string or {@code major.minor.micro.qualifier} + * otherwise. + * + * @return The string representation of this version identifier. + */ + @Override + public String toString() { + return toString0(); + } + + /** + * Internal toString behavior + * + * @return The string representation of this version identifier. + */ + String toString0() { + String s = versionString; + if (s != null) { + return s; + } + int q = qualifier.length(); + StringBuilder result = new StringBuilder(20 + q); + result.append(major); + result.append(SEPARATOR); + result.append(minor); + result.append(SEPARATOR); + result.append(micro); + if (q > 0) { + result.append(SEPARATOR); + result.append(qualifier); + } + return versionString = result.toString(); + } + + /** + * Returns a hash code value for the object. + * + * @return An integer which is a hash code value for this object. + */ + @Override + public int hashCode() { + int h = hash; + if (h != 0) { + return h; + } + h = 31 * 17; + h = 31 * h + major; + h = 31 * h + minor; + h = 31 * h + micro; + h = 31 * h + qualifier.hashCode(); + return hash = h; + } + + /** + * Compares this {@code Version} object to another object. + * + *

      + * A version is considered to be equal to another version if the + * major, minor and micro components are equal and the qualifier component + * is equal (using {@code String.equals}). + * + * @param object The {@code Version} object to be compared. + * @return {@code true} if {@code object} is a {@code Version} and is equal + * to this object; {@code false} otherwise. + */ + @Override + public boolean equals(Object object) { + if (object == this) { // quicktest + return true; + } + + if (!(object instanceof Version)) { + return false; + } + + Version other = (Version) object; + return (major == other.major) && (minor == other.minor) && (micro == other.micro) && qualifier.equals(other.qualifier); + } + + /** + * Compares this {@code Version} object to another {@code Version}. + * + *

      + * A version is considered to be less than another version if its + * major component is less than the other version's major component, or the + * major components are equal and its minor component is less than the other + * version's minor component, or the major and minor components are equal + * and its micro component is less than the other version's micro component, + * or the major, minor and micro components are equal and it's qualifier + * component is less than the other version's qualifier component (using + * {@code String.compareTo}). + * + *

      + * A version is considered to be equal to another version if the + * major, minor and micro components are equal and the qualifier component + * is equal (using {@code String.compareTo}). + * + * @param other The {@code Version} object to be compared. + * @return A negative integer, zero, or a positive integer if this version + * is less than, equal to, or greater than the specified + * {@code Version} object. + * @throws ClassCastException If the specified object is not a + * {@code Version} object. + */ + @Override + public int compareTo(Version other) { + if (other == this) { // quicktest + return 0; + } + + int result = major - other.major; + if (result != 0) { + return result; + } + + result = minor - other.minor; + if (result != 0) { + return result; + } + + result = micro - other.micro; + if (result != 0) { + return result; + } + + return qualifier.compareTo(other.qualifier); + } +} diff --git a/framework/src/main/java/org/osgi/framework/VersionRange.java b/framework/src/main/java/org/osgi/framework/VersionRange.java new file mode 100644 index 00000000000..abe8d312388 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/VersionRange.java @@ -0,0 +1,542 @@ +/* + * Copyright (c) OSGi Alliance (2011, 2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework; + +import java.util.NoSuchElementException; +import java.util.StringTokenizer; + +/** + * Version range. A version range is an interval describing a set of + * {@link Version versions}. + * + *

      + * A range has a left (lower) endpoint and a right (upper) endpoint. Each + * endpoint can be open (excluded from the set) or closed (included in the set). + * + *

      + * {@code VersionRange} objects are immutable. + * + * @since 1.7 + * @Immutable + * @author $Id: cc407ff6fb1d5252b61a033924e63b751880f580 $ + */ + +public class VersionRange { + /** + * The left endpoint is open and is excluded from the range. + *

      + * The value of {@code LEFT_OPEN} is {@code '('}. + */ + public static final char LEFT_OPEN = '('; + /** + * The left endpoint is closed and is included in the range. + *

      + * The value of {@code LEFT_CLOSED} is {@code '['}. + */ + public static final char LEFT_CLOSED = '['; + /** + * The right endpoint is open and is excluded from the range. + *

      + * The value of {@code RIGHT_OPEN} is {@code ')'}. + */ + public static final char RIGHT_OPEN = ')'; + /** + * The right endpoint is closed and is included in the range. + *

      + * The value of {@code RIGHT_CLOSED} is {@code ']'}. + */ + public static final char RIGHT_CLOSED = ']'; + + private final boolean leftClosed; + private final Version left; + private final Version right; + private final boolean rightClosed; + private final boolean empty; + + private transient String versionRangeString /* default to null */; + private transient int hash /* default to 0 */; + + private static final String LEFT_OPEN_DELIMITER = "("; + private static final String LEFT_CLOSED_DELIMITER = "["; + private static final String LEFT_DELIMITERS = LEFT_CLOSED_DELIMITER + LEFT_OPEN_DELIMITER; + private static final String RIGHT_OPEN_DELIMITER = ")"; + private static final String RIGHT_CLOSED_DELIMITER = "]"; + private static final String RIGHT_DELIMITERS = RIGHT_OPEN_DELIMITER + RIGHT_CLOSED_DELIMITER; + private static final String ENDPOINT_DELIMITER = ","; + + /** + * Creates a version range from the specified versions. + * + * @param leftType Must be either {@link #LEFT_CLOSED} or {@link #LEFT_OPEN} + * . + * @param leftEndpoint Left endpoint of range. Must not be {@code null}. + * @param rightEndpoint Right endpoint of range. May be {@code null} to + * indicate the right endpoint is Infinity. + * @param rightType Must be either {@link #RIGHT_CLOSED} or + * {@link #RIGHT_OPEN}. + * @throws IllegalArgumentException If the arguments are invalid. + */ + public VersionRange(char leftType, Version leftEndpoint, Version rightEndpoint, char rightType) { + if ((leftType != LEFT_CLOSED) && (leftType != LEFT_OPEN)) { + throw new IllegalArgumentException("invalid leftType \"" + leftType + "\""); + } + if ((rightType != RIGHT_OPEN) && (rightType != RIGHT_CLOSED)) { + throw new IllegalArgumentException("invalid rightType \"" + rightType + "\""); + } + if (leftEndpoint == null) { + throw new IllegalArgumentException("null leftEndpoint argument"); + } + leftClosed = leftType == LEFT_CLOSED; + rightClosed = rightType == RIGHT_CLOSED; + left = leftEndpoint; + right = rightEndpoint; + empty = isEmpty0(); + } + + /** + * Creates a version range from the specified string. + * + *

      + * Version range string grammar: + * + *

      +	 * range ::= interval | atleast
      +	 * interval ::= ( '[' | '(' ) left ',' right ( ']' | ')' )
      +	 * left ::= version
      +	 * right ::= version
      +	 * atleast ::= version
      +	 * 
      + * + * @param range String representation of the version range. The versions in + * the range must contain no whitespace. Other whitespace in the + * range string is ignored. Must not be {@code null}. + * @throws IllegalArgumentException If {@code range} is improperly + * formatted. + */ + public VersionRange(String range) { + boolean closedLeft; + boolean closedRight; + Version endpointLeft; + Version endpointRight; + + try { + StringTokenizer st = new StringTokenizer(range, LEFT_DELIMITERS, true); + String token = st.nextToken().trim(); // whitespace or left delim + if (token.length() == 0) { // leading whitespace + token = st.nextToken(); // left delim + } + closedLeft = LEFT_CLOSED_DELIMITER.equals(token); + if (!closedLeft && !LEFT_OPEN_DELIMITER.equals(token)) { + // first token is not a delimiter, so it must be "atleast" + if (st.hasMoreTokens()) { // there must be no more tokens + throw new IllegalArgumentException("invalid range \"" + range + "\": invalid format"); + } + leftClosed = true; + rightClosed = false; + left = parseVersion(token, range); + right = null; + empty = false; + return; + } + String version = st.nextToken(ENDPOINT_DELIMITER); + endpointLeft = parseVersion(version, range); + token = st.nextToken(); // consume comma + version = st.nextToken(RIGHT_DELIMITERS); + token = st.nextToken(); // right delim + closedRight = RIGHT_CLOSED_DELIMITER.equals(token); + if (!closedRight && !RIGHT_OPEN_DELIMITER.equals(token)) { + throw new IllegalArgumentException("invalid range \"" + range + "\": invalid format"); + } + endpointRight = parseVersion(version, range); + + if (st.hasMoreTokens()) { // any more tokens have to be whitespace + token = st.nextToken("").trim(); + if (token.length() != 0) { // trailing whitespace + throw new IllegalArgumentException("invalid range \"" + range + "\": invalid format"); + } + } + } catch (NoSuchElementException e) { + throw new IllegalArgumentException( + "invalid range \"" + range + "\": invalid format", e); + } + + leftClosed = closedLeft; + rightClosed = closedRight; + left = endpointLeft; + right = endpointRight; + empty = isEmpty0(); + } + + /** + * Parse version component into a Version. + * + * @param version version component string + * @param range Complete range string for exception message, if any + * @return Version + */ + private static Version parseVersion(String version, String range) { + try { + return Version.valueOf(version); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "invalid range \"" + range + "\": " + e.getMessage(), e); + } + } + + /** + * Returns the left endpoint of this version range. + * + * @return The left endpoint. + */ + public Version getLeft() { + return left; + } + + /** + * Returns the right endpoint of this version range. + * + * @return The right endpoint. May be {@code null} which indicates the right + * endpoint is Infinity. + */ + public Version getRight() { + return right; + } + + /** + * Returns the type of the left endpoint of this version range. + * + * @return {@link #LEFT_CLOSED} if the left endpoint is closed or + * {@link #LEFT_OPEN} if the left endpoint is open. + */ + public char getLeftType() { + return leftClosed ? LEFT_CLOSED : LEFT_OPEN; + } + + /** + * Returns the type of the right endpoint of this version range. + * + * @return {@link #RIGHT_CLOSED} if the right endpoint is closed or + * {@link #RIGHT_OPEN} if the right endpoint is open. + */ + public char getRightType() { + return rightClosed ? RIGHT_CLOSED : RIGHT_OPEN; + } + + /** + * Returns whether this version range includes the specified version. + * + * @param version The version to test for inclusion in this version range. + * @return {@code true} if the specified version is included in this version + * range; {@code false} otherwise. + */ + public boolean includes(Version version) { + if (empty) { + return false; + } + if (left.compareTo(version) >= (leftClosed ? 1 : 0)) { + return false; + } + if (right == null) { + return true; + } + return right.compareTo(version) >= (rightClosed ? 0 : 1); + } + + /** + * Returns the intersection of this version range with the specified version + * ranges. + * + * @param ranges The version ranges to intersect with this version range. + * @return A version range representing the intersection of this version + * range and the specified version ranges. If no version ranges are + * specified, then this version range is returned. + */ + public VersionRange intersection(VersionRange... ranges) { + if ((ranges == null) || (ranges.length == 0)) { + return this; + } + // prime with data from this version range + boolean closedLeft = leftClosed; + boolean closedRight = rightClosed; + Version endpointLeft = left; + Version endpointRight = right; + + for (VersionRange range : ranges) { + int comparison = endpointLeft.compareTo(range.left); + if (comparison == 0) { + closedLeft = closedLeft && range.leftClosed; + } else { + if (comparison < 0) { // move endpointLeft to the right + endpointLeft = range.left; + closedLeft = range.leftClosed; + } + } + if (range.right != null) { + if (endpointRight == null) { + endpointRight = range.right; + closedRight = range.rightClosed; + } else { + comparison = endpointRight.compareTo(range.right); + if (comparison == 0) { + closedRight = closedRight && range.rightClosed; + } else { + if (comparison > 0) { // move endpointRight to the left + endpointRight = range.right; + closedRight = range.rightClosed; + } + } + } + } + } + + return new VersionRange(closedLeft ? LEFT_CLOSED : LEFT_OPEN, endpointLeft, endpointRight, closedRight ? RIGHT_CLOSED : RIGHT_OPEN); + } + + /** + * Returns whether this version range is empty. A version range is empty if + * the set of versions defined by the interval is empty. + * + * @return {@code true} if this version range is empty; {@code false} + * otherwise. + */ + public boolean isEmpty() { + return empty; + } + + /** + * Internal isEmpty behavior. + * + * @return {@code true} if this version range is empty; {@code false} + * otherwise. + */ + private boolean isEmpty0() { + if (right == null) { // infinity + return false; + } + int comparison = left.compareTo(right); + if (comparison == 0) { // endpoints equal + return !leftClosed || !rightClosed; + } + return comparison > 0; // true if left > right + } + + /** + * Returns whether this version range contains only a single version. + * + * @return {@code true} if this version range contains only a single + * version; {@code false} otherwise. + */ + public boolean isExact() { + if (empty || (right == null)) { + return false; + } + if (leftClosed) { + if (rightClosed) { + // [l,r]: exact if l == r + return left.equals(right); + } else { + // [l,r): exact if l++ >= r + Version adjacent1 = new Version(left.getMajor(), left.getMinor(), left.getMicro(), left.getQualifier() + "-"); + return adjacent1.compareTo(right) >= 0; + } + } else { + if (rightClosed) { + // (l,r] is equivalent to [l++,r]: exact if l++ == r + Version adjacent1 = new Version(left.getMajor(), left.getMinor(), left.getMicro(), left.getQualifier() + "-"); + return adjacent1.equals(right); + } else { + // (l,r) is equivalent to [l++,r): exact if (l++)++ >=r + Version adjacent2 = new Version(left.getMajor(), left.getMinor(), left.getMicro(), left.getQualifier() + "--"); + return adjacent2.compareTo(right) >= 0; + } + } + } + + /** + * Returns the string representation of this version range. + * + *

      + * The format of the version range string will be a version string if the + * right end point is Infinity ({@code null}) or an interval string. + * + * @return The string representation of this version range. + */ + @Override + public String toString() { + String s = versionRangeString; + if (s != null) { + return s; + } + String leftVersion = left.toString(); + if (right == null) { + StringBuilder result = new StringBuilder(leftVersion.length() + 1); + result.append(left.toString0()); + return versionRangeString = result.toString(); + } + String rightVerion = right.toString(); + StringBuilder result = new StringBuilder( + leftVersion.length() + rightVerion.length() + 5); + result.append(leftClosed ? LEFT_CLOSED : LEFT_OPEN); + result.append(left.toString0()); + result.append(ENDPOINT_DELIMITER); + result.append(right.toString0()); + result.append(rightClosed ? RIGHT_CLOSED : RIGHT_OPEN); + return versionRangeString = result.toString(); + } + + /** + * Returns a hash code value for the object. + * + * @return An integer which is a hash code value for this object. + */ + @Override + public int hashCode() { + int h = hash; + if (h != 0) { + return h; + } + if (empty) { + return hash = 31; + } + h = 31 + (leftClosed ? 7 : 5); + h = 31 * h + left.hashCode(); + if (right != null) { + h = 31 * h + right.hashCode(); + h = 31 * h + (rightClosed ? 7 : 5); + } + return hash = h; + } + + /** + * Compares this {@code VersionRange} object to another object. + * + *

      + * A version range is considered to be equal to another version + * range if both the endpoints and their types are equal or if both version + * ranges are {@link #isEmpty() empty}. + * + * @param object The {@code VersionRange} object to be compared. + * @return {@code true} if {@code object} is a {@code VersionRange} and is + * equal to this object; {@code false} otherwise. + */ + @Override + public boolean equals(Object object) { + if (object == this) { // quicktest + return true; + } + if (!(object instanceof VersionRange)) { + return false; + } + VersionRange other = (VersionRange) object; + if (empty && other.empty) { + return true; + } + if (right == null) { + return (leftClosed == other.leftClosed) && (other.right == null) && left.equals(other.left); + } + return (leftClosed == other.leftClosed) && (rightClosed == other.rightClosed) && left.equals(other.left) && right.equals(other.right); + } + + /** + * Returns the filter string for this version range using the specified + * attribute name. + * + * @param attributeName The attribute name to use in the returned filter + * string. + * @return A filter string for this version range using the specified + * attribute name. + * @throws IllegalArgumentException If the specified attribute name is not a + * valid attribute name. + * + * @see "Core Specification, Filters, for a description of the filter string syntax." + */ + public String toFilterString(String attributeName) { + if (attributeName.length() == 0) { + throw new IllegalArgumentException("invalid attributeName \"" + attributeName + "\""); + } + for (char ch : attributeName.toCharArray()) { + if ((ch == '=') || (ch == '>') || (ch == '<') || (ch == '~') || (ch == '(') || (ch == ')')) { + throw new IllegalArgumentException("invalid attributeName \"" + attributeName + "\""); + } + } + + StringBuilder result = new StringBuilder(128); + final boolean needPresence = !leftClosed && ((right == null) || !rightClosed); + final boolean multipleTerms = needPresence || (right != null); + if (multipleTerms) { + result.append("(&"); + } + if (needPresence) { + result.append('('); + result.append(attributeName); + result.append("=*)"); + } + if (leftClosed) { + result.append('('); + result.append(attributeName); + result.append(">="); + result.append(left.toString0()); + result.append(')'); + } else { + result.append("(!("); + result.append(attributeName); + result.append("<="); + result.append(left.toString0()); + result.append("))"); + } + if (right != null) { + if (rightClosed) { + result.append('('); + result.append(attributeName); + result.append("<="); + result.append(right.toString0()); + result.append(')'); + } else { + result.append("(!("); + result.append(attributeName); + result.append(">="); + result.append(right.toString0()); + result.append("))"); + } + } + if (multipleTerms) { + result.append(')'); + } + + return result.toString(); + } + + /** + * Returns a {@code VersionRange} object holding the version range in the + * specified {@code String}. + * + *

      + * See {@link #VersionRange(String)} for the format of the version range + * string. + * + * @param range String representation of the version range. The versions in + * the range must contain no whitespace. Other whitespace in the + * range string is ignored. Must not be {@code null}. + * @return A {@code VersionRange} object representing the version range. + * @throws IllegalArgumentException If {@code range} is improperly + * formatted. + * @since 1.8 + */ + public static VersionRange valueOf(String range) { + return new VersionRange(range); + } +} diff --git a/framework/src/main/java/org/osgi/framework/dto/BundleDTO.java b/framework/src/main/java/org/osgi/framework/dto/BundleDTO.java new file mode 100644 index 00000000000..aa30709351d --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/dto/BundleDTO.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.dto; + +import org.osgi.dto.DTO; +import org.osgi.framework.Bundle; + +/** + * Data Transfer Object for a Bundle. + * + *

      + * A Bundle can be adapted to provide a {@code BundleDTO} for the Bundle. + * + * @author $Id$ + * @NotThreadSafe + */ +public class BundleDTO extends DTO { + /** + * The bundle's unique identifier. + * + * @see Bundle#getBundleId() + */ + public long id; + + /** + * The time when the bundle was last modified. + * + * @see Bundle#getLastModified() + */ + public long lastModified; + + /** + * The bundle's state. + * + * @see Bundle#getState() + */ + public int state; + + /** + * The bundle's symbolic name. + * + * @see Bundle#getSymbolicName() + */ + public String symbolicName; + + /** + * The bundle's version. + * + * @see Bundle#getVersion() + */ + public String version; +} diff --git a/framework/src/main/java/org/osgi/framework/dto/FrameworkDTO.java b/framework/src/main/java/org/osgi/framework/dto/FrameworkDTO.java new file mode 100644 index 00000000000..7c525727cbe --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/dto/FrameworkDTO.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.dto; + +import java.util.List; +import java.util.Map; +import org.osgi.dto.DTO; +import org.osgi.framework.BundleContext; + +/** + * Data Transfer Object for a Framework. + * + *

      + * The System Bundle can be adapted to provide a {@code FrameworkDTO} for the + * framework of the system bundle. A {@code FrameworkDTO} obtained from a + * framework will contain only the launch properties of the framework. These + * properties will not include the System properties. + * + * @author $Id$ + * @NotThreadSafe + */ +public class FrameworkDTO extends DTO { + /** + * The bundles that are installed in the framework. + * + * @see BundleContext#getBundles() + */ + public List bundles; + + /** + * The launch properties of the framework. + * + * The value type must be a numerical type, Boolean, String, DTO or an array + * of any of the former. + * + * @see BundleContext#getProperty(String) + */ + public Map properties; + + /** + * The services that are registered in the framework. + * + * @see BundleContext#getServiceReferences(String, String) + */ + public List services; +} diff --git a/framework/src/main/java/org/osgi/framework/dto/ServiceReferenceDTO.java b/framework/src/main/java/org/osgi/framework/dto/ServiceReferenceDTO.java new file mode 100644 index 00000000000..87265ded2fc --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/dto/ServiceReferenceDTO.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.dto; + +import java.util.Map; +import org.osgi.dto.DTO; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + +/** + * Data Transfer Object for a ServiceReference. + * + *

      + * {@code ServiceReferenceDTO}s for all registered services can be obtained from + * a {@link FrameworkDTO}. A started Bundle can be adapted to provide a + * {@code ServiceReferenceDTO[]} of the services registered by the Bundle. A + * {@code ServiceReferenceDTO} obtained from a framework must convert service + * property values which are not valid value types for DTOs to type + * {@code String} using {@code String.valueOf(Object)}. + * + * @author $Id$ + * @NotThreadSafe + */ +public class ServiceReferenceDTO extends DTO { + /** + * The id of the service. + * + * @see Constants#SERVICE_ID + */ + public long id; + + /** + * The id of the bundle that registered the service. + * + * @see ServiceReference#getBundle() + */ + public long bundle; + + /** + * The properties for the service. + * + * The value type must be a numerical type, Boolean, String, DTO or an array + * of any of the former. + * + * @see ServiceReference#getProperty(String) + */ + public Map properties; + + /** + * The ids of the bundles that are using the service. + * + * @see ServiceReference#getUsingBundles() + */ + public long[] usingBundles; +} diff --git a/framework/src/main/java/org/osgi/framework/dto/package-info.java b/framework/src/main/java/org/osgi/framework/dto/package-info.java new file mode 100644 index 00000000000..2acfb6f1633 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/dto/package-info.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * OSGi Data Transfer Object Framework Package Version 1.8. + * + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. This package has two types of + * users: the consumers that use the API in this package and the providers that + * implement the API in this package. + * + *

      + * Example import for consumers using the API in this package: + *

      + * {@code Import-Package: org.osgi.framework.dto; version="[1.8,2.0)"} + *

      + * Example import for providers implementing the API in this package: + *

      + * {@code Import-Package: org.osgi.framework.dto; version="[1.8,1.9)"} + * + * @author $Id$ + */ + +@Version("1.8") +package org.osgi.framework.dto; + +import org.osgi.annotation.versioning.Version; + diff --git a/framework/src/main/java/org/osgi/framework/hooks/bundle/CollisionHook.java b/framework/src/main/java/org/osgi/framework/hooks/bundle/CollisionHook.java new file mode 100644 index 00000000000..cb3eb2d23ec --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/hooks/bundle/CollisionHook.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) OSGi Alliance (2011, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.hooks.bundle; + +import java.util.Collection; +import org.osgi.annotation.versioning.ConsumerType; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; + +/** + * OSGi Framework Bundle Collision Hook Service. + * + *

      + * If the framework was launched with the {@link Constants#FRAMEWORK_BSNVERSION + * org.osgi.framework.bsnversion} framework launching property set to + * {@link Constants#FRAMEWORK_BSNVERSION_MANAGED managed}, then all registered + * collision hook services will be called during framework bundle install and + * update operations to determine if an install or update operation will result + * in a bundle symbolic name and version collision. + * + * @ThreadSafe + * @author $Id: b05e0c3819aff6df35c2ec034242596d04f53408 $ + */ +@ConsumerType +public interface CollisionHook { + + /** + * Specifies a bundle install operation is being performed. + */ + int INSTALLING = 1; + + /** + * Specifies a bundle update operation is being performed. + */ + int UPDATING = 2; + + /** + * Filter bundle collisions hook method. This method is called during the + * install or update operation. The operation type will be + * {@link #INSTALLING installing} or {@link #UPDATING updating}. Depending + * on the operation type the target bundle and the collision candidate + * collection are the following: + *

        + *
      • {@link #INSTALLING installing} - The target is the bundle associated + * with the {@link BundleContext} used to call one of the + * {@link BundleContext#installBundle(String) install} methods. The + * collision candidate collection contains the existing bundles installed + * which have the same symbolic name and version as the bundle being + * installed.
      • + *
      • {@link #UPDATING updating} - The target is the bundle used to call + * one of the {@link Bundle#update() update} methods. The collision + * candidate collection contains the existing bundles installed which have + * the same symbolic name and version as the content the target bundle is + * being updated to.
      • + *
      + * This method can filter the collection of collision candidates by removing + * potential collisions. For the specified operation to succeed, the + * collection of collision candidates must be empty after all registered + * collision hook services have been called. + * + * @param operationType The operation type. Must be the value of + * {@link #INSTALLING installing} or {@link #UPDATING updating}. + * @param target The target bundle used to determine what collision + * candidates to filter. + * @param collisionCandidates The collection of collision candidates. The + * collection supports all the optional {@code Collection} operations + * except {@code add} and {@code addAll}. Attempting to add to the + * collection will result in an {@code UnsupportedOperationException} + * . The collection is not synchronized. + */ + void filterCollisions(int operationType, Bundle target, Collection collisionCandidates); +} diff --git a/framework/src/main/java/org/osgi/framework/hooks/bundle/EventHook.java b/framework/src/main/java/org/osgi/framework/hooks/bundle/EventHook.java new file mode 100644 index 00000000000..4bd7657d96d --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/hooks/bundle/EventHook.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.hooks.bundle; + +import java.util.Collection; +import org.osgi.annotation.versioning.ConsumerType; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; + +/** + * OSGi Framework Bundle Event Hook Service. + * + *

      + * Bundles registering this service will be called during framework lifecycle + * (install, start, stop, update, and uninstall bundle) operations. + * + * @ThreadSafe + * @author $Id: 0b986db904e41d5a5bfb5d28fc849ee967decf0d $ + */ +@ConsumerType +public interface EventHook { + + /** + * Bundle event hook method. This method is called prior to bundle event + * delivery when a bundle is installed, resolved, started, stopped, + * unresolved, or uninstalled. This method can filter the bundles which + * receive the event. + *

      + * This method must be called by the framework one and only one time for + * each bundle event generated, this included bundle events which are + * generated when there are no bundle listeners registered. This method must + * be called on the same thread that is performing the action which + * generated the specified event. The specified collection includes bundle + * contexts with synchronous and asynchronous bundle listeners registered + * with them. + * + * @param event The bundle event to be delivered + * @param contexts A collection of Bundle Contexts for bundles which have + * listeners to which the specified event will be delivered. The + * implementation of this method may remove bundle contexts from the + * collection to prevent the event from being delivered to the + * associated bundles. The collection supports all the optional + * {@code Collection} operations except {@code add} and + * {@code addAll}. Attempting to add to the collection will result in + * an {@code UnsupportedOperationException}. The collection is not + * synchronized. + */ + void event(BundleEvent event, Collection contexts); +} diff --git a/framework/src/main/java/org/osgi/framework/hooks/bundle/FindHook.java b/framework/src/main/java/org/osgi/framework/hooks/bundle/FindHook.java new file mode 100644 index 00000000000..9c8ab231b20 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/hooks/bundle/FindHook.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) OSGi Alliance (2011, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.hooks.bundle; + +import java.util.Collection; +import org.osgi.annotation.versioning.ConsumerType; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; + +/** + * OSGi Framework Bundle Context Hook Service. + * + *

      + * Bundles registering this service will be called during framework bundle find + * (get bundles) operations. + * + * @ThreadSafe + * @author $Id: 1029e10212f150304095fc99433197083cc00e9e $ + */ +@ConsumerType +public interface FindHook { + /** + * Find hook method. This method is called for the following: + *

        + *
      • Bundle find operations using {@link BundleContext#getBundle(long)} + * and {@link BundleContext#getBundles()} methods. The find method can + * filter the result of the find operation. Note that a find operation using + * the {@link BundleContext#getBundle(String)} method does not cause the + * find method to be called.
      • + *
      • Bundle install operations when an existing bundle is already + * installed at a given location. In this case, the find method is called to + * determine if the context performing the install operation is able to find + * the bundle. If the context cannot find the existing bundle then the + * install operation must fail with a + * {@link BundleException#REJECTED_BY_HOOK} exception.
      • + *
      + * + * @param context The bundle context of the bundle performing the find + * operation. + * @param bundles A collection of Bundles to be returned as a result of the + * find operation. The implementation of this method may remove + * bundles from the collection to prevent the bundles from being + * returned to the bundle performing the find operation. The + * collection supports all the optional {@code Collection} operations + * except {@code add} and {@code addAll}. Attempting to add to the + * collection will result in an {@code UnsupportedOperationException} + * . The collection is not synchronized. + */ + void find(BundleContext context, Collection bundles); +} diff --git a/framework/src/main/java/org/osgi/framework/hooks/bundle/package-info.java b/framework/src/main/java/org/osgi/framework/hooks/bundle/package-info.java new file mode 100644 index 00000000000..08c20cab669 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/hooks/bundle/package-info.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Framework Bundle Hooks Package Version 1.1. + * + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. + * + *

      + * Example import for consumers using the API in this package: + *

      + * {@code Import-Package: org.osgi.framework.hooks.bundle; version="[1.1,2.0)"} + * + * @author $Id$ + */ + +@Version("1.1") +package org.osgi.framework.hooks.bundle; + +import org.osgi.annotation.versioning.Version; + diff --git a/framework/src/main/java/org/osgi/framework/hooks/resolver/ResolverHook.java b/framework/src/main/java/org/osgi/framework/hooks/resolver/ResolverHook.java new file mode 100644 index 00000000000..024abd00972 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/hooks/resolver/ResolverHook.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2018). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.hooks.resolver; + +import java.util.Collection; +import org.osgi.annotation.versioning.ConsumerType; +import org.osgi.framework.Bundle; +import org.osgi.framework.namespace.BundleNamespace; +import org.osgi.framework.namespace.IdentityNamespace; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.FrameworkWiring; + +/** + * OSGi Framework Resolver Hook instances are obtained from the OSGi + * {@link ResolverHookFactory Framework Resolver Hook Factory} service. + * + *

      + * A Resolver Hook instance is called by the framework during a resolve process. + * A resolver hook may influence the outcome of a resolve process by removing + * entries from shrinkable collections that are passed to the hook during a + * resolve process. A shrinkable collection is a {@code Collection} that + * supports all remove operations. Any other attempts to modify a shrinkable + * collection will result in an {@code UnsupportedOperationException} being + * thrown. + * + *

      + * The following steps outline the way a framework uses the resolver hooks + * during a resolve process. + *

        + *
      1. Collect a snapshot of registered resolver hook factories that will be + * called during the current resolve process. Any hook factories registered + * after the snapshot is taken must not be called during the current resolve + * process. A resolver hook factory contained in the snapshot may become + * unregistered during the resolve process. The framework should handle this and + * stop calling the resolver hook instance provided by the unregistered hook + * factory and the current resolve process must fail. If possible, an exception + * must be thrown to the caller of the API which triggered the resolve process. + * In cases where the caller is not available a framework event of type error + * should be fired.
      2. + *
      3. For each registered hook factory call the + * {@link ResolverHookFactory#begin(Collection)} method to inform the hooks + * about a resolve process beginning and to obtain a Resolver Hook instance that + * will be used for the duration of the resolve process.
      4. + * + *
      5. Determine the collection of unresolved bundle revisions that may be + * considered for resolution during the current resolution process and place + * each of the bundle revisions in a shrinkable collection {@code Resolvable}. + * For each resolver hook call the {@link #filterResolvable(Collection)} method + * with the shrinkable collection {@code Resolvable}.
      6. + *
      7. The shrinkable collection {@code Resolvable} now contains all the + * unresolved bundle revisions that may end up as resolved at the end of the + * current resolve process. Any other bundle revisions that got removed from the + * shrinkable collection {@code Resolvable} must not end up as resolved at the + * end of the current resolve process.
      8. + *
      9. For each bundle revision {@code B} left in the shrinkable collection + * {@code Resolvable} and any bundle revision {@code B} which is currently + * resolved that represents a singleton bundle do the following: + *
          + *
        • Determine the collection of available capabilities that have a namespace + * of {@link IdentityNamespace osgi.identity}, are singletons, and have the same + * symbolic name as the singleton bundle revision {@code B} and place each of + * the matching capabilities into a shrinkable collection {@code Collisions}.
        • + *
        • Remove the {@link IdentityNamespace osgi.identity} capability provided by + * bundle revision {@code B} from shrinkable collection {@code Collisions}. A + * singleton bundle cannot collide with itself.
        • + *
        • For each resolver hook call the + * {@link #filterSingletonCollisions(BundleCapability, Collection)} with the + * {@link IdentityNamespace osgi.identity} capability provided by bundle + * revision {@code B} and the shrinkable collection {@code Collisions}
        • + *
        • The shrinkable collection {@code Collisions} now contains all singleton + * {@link IdentityNamespace osgi.identity} capabilities that can influence the + * ability of bundle revision {@code B} to resolve.
        • + *
        • If the bundle revision {@code B} is already resolved then any resolvable + * bundle revision contained in the collection {@code Collisions} is not allowed + * to resolve.
        • + *
        + *
      10. + *
      11. During a resolve process a framework is free to attempt to resolve any or + * all bundles contained in shrinkable collection {@code Resolvable}. For each + * bundle revision {@code B} left in the shrinkable collection + * {@code Resolvable} which the framework attempts to resolve the following + * steps must be followed: + *
          + *
        • For each requirement {@code R} specified by bundle revision {@code B} + * determine the collection of capabilities that satisfy (or match) the + * requirement and place each matching capability into a shrinkable collection + * {@code Candidates}. A capability is considered to match a particular + * requirement if its attributes satisfy a specified requirement and the + * requirer bundle has permission to access the capability.
        • + *
        • For each resolver hook call the + * {@link #filterMatches(BundleRequirement, Collection)} with the requirement + * {@code R} and the shrinkable collection {@code Candidates}.
        • + *
        • The shrinkable collection {@code Candidates} now contains all the + * capabilities that may be used to satisfy the requirement {@code R}. Any other + * capabilities that got removed from the shrinkable collection + * {@code Candidates} must not be used to satisfy requirement {@code R}.
        • + *
        + *
      12. + *
      13. For each resolver hook call the {@link #end()} method to inform the hooks + * about a resolve process ending.
      14. + *
      + * In all cases, the order in which the resolver hooks are called is the reverse + * compareTo ordering of their Service References. That is, the service with the + * highest ranking number must be called first. In cases where a shrinkable + * collection becomes empty the framework is required to call the remaining + * registered hooks. + *

      + * Resolver hooks are low level. Implementations of the resolver hook must be + * careful not to create an unresolvable state which is very hard for a + * developer or a provisioner to diagnose. Resolver hooks also must not be + * allowed to start another synchronous resolve process (e.g. by calling + * {@link Bundle#start()} or {@link FrameworkWiring#resolveBundles(Collection)} + * ). The framework must detect this and throw an {@link IllegalStateException}. + * + * @see ResolverHookFactory + * @NotThreadSafe + * @author $Id: 6c1f3732d22b088e9f8a80c17e70e61862abf1c4 $ + */ +@ConsumerType +public interface ResolverHook { + /** + * Filter resolvable candidates hook method. This method may be called + * multiple times during a single resolve process. This method can filter + * the collection of candidates by removing potential candidates. Removing a + * candidate will prevent the candidate from resolving during the current + * resolve process. + * + * @param candidates the collection of resolvable candidates available + * during a resolve process. + */ + void filterResolvable(Collection candidates); + + /** + * Filter singleton collisions hook method. This method is called during the + * resolve process for the specified singleton. The specified singleton + * represents a singleton capability and the specified collection represent + * a collection of singleton capabilities which are considered collision + * candidates. The singleton capability and the collection of collision + * candidates must all use the same namespace. + *

      + * Currently only capabilities with the namespace of {@link BundleNamespace + * osgi.wiring.bundle} and {@link IdentityNamespace osgi.identity} can be + * singletons. The collision candidates will all have the same namespace, be + * singletons, and have the same symbolic name as the specified singleton + * capability. + *

      + * In the future, capabilities in other namespaces may support the singleton + * concept. Hook implementations should be prepared to receive calls to this + * method for capabilities in namespaces other than {@link BundleNamespace + * osgi.wiring.bundle} or {@link IdentityNamespace osgi.identity}. + *

      + * This method can filter the list of collision candidates by removing + * potential collisions. Removing a collision candidate will allow the + * specified singleton to resolve regardless of the resolution state of the + * removed collision candidate. + * + * @param singleton the singleton involved in a resolve process + * @param collisionCandidates a collection of singleton collision candidates + */ + void filterSingletonCollisions(BundleCapability singleton, Collection collisionCandidates); + + /** + * Filter matches hook method. This method is called during the resolve + * process for the specified requirement. The collection of candidates match + * the specified requirement. This method can filter the collection of + * matching candidates by removing candidates from the collection. Removing + * a candidate will prevent the resolve process from choosing the removed + * candidate to satisfy the requirement. + *

      + * All of the candidates will have the same namespace and will match the + * specified requirement. + *

      + * If the Java Runtime Environment supports permissions then the collection + * of candidates will only contain candidates for which the requirer has + * permission to access. + * + * @param requirement the requirement to filter candidates for + * @param candidates a collection of candidates that match the requirement + */ + void filterMatches(BundleRequirement requirement, Collection candidates); + + /** + * This method is called once at the end of the resolve process. After the + * end method is called the resolve process has ended. The framework must + * not hold onto this resolver hook instance after end has been called. + */ + void end(); +} diff --git a/framework/src/main/java/org/osgi/framework/hooks/resolver/ResolverHookFactory.java b/framework/src/main/java/org/osgi/framework/hooks/resolver/ResolverHookFactory.java new file mode 100644 index 00000000000..10f6e97faa1 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/hooks/resolver/ResolverHookFactory.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) OSGi Alliance (2011, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.hooks.resolver; + +import java.util.Collection; +import org.osgi.annotation.versioning.ConsumerType; +import org.osgi.framework.Bundle; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.FrameworkWiring; + +/** + * OSGi Framework Resolver Hook Factory Service. + * + *

      + * Bundles registering this service will be called by the framework during a + * bundle resolver process to obtain a {@link ResolverHook resolver hook} + * instance which will be used for the duration of a resolve process. + * + * @ThreadSafe + * @see ResolverHook + * @author $Id: d073890a9814291f258c9326f720323d7f4d7d90 $ + */ +@ConsumerType +public interface ResolverHookFactory { + /** + * This method is called by the framework each time a resolve process begins + * to obtain a {@link ResolverHook resolver hook} instance. This resolver + * hook instance will be used for the duration of the resolve process. At + * the end of the resolve process the method {@link ResolverHook#end()} must + * be called by the framework and the framework must not hold any references + * of the resolver hook instance. + *

      + * The triggers represent the collection of bundles which triggered the + * resolve process. This collection may be empty if the triggers cannot be + * determined by the framework. In most cases the triggers can easily be + * determined. Calling certain methods on {@link Bundle bundle} when a + * bundle is in the {@link Bundle#INSTALLED INSTALLED} state will cause the + * framework to begin a resolve process in order to resolve the bundle. The + * following methods will start a resolve process in this case: + *

        + *
      • {@link Bundle#start() start}
      • + *
      • {@link Bundle#loadClass(String) loadClass}
      • + *
      • {@link Bundle#findEntries(String, String, boolean) findEntries}
      • + *
      • {@link Bundle#getResource(String) getResource}
      • + *
      • {@link Bundle#getResources(String) getResources}
      • + *
      + * In such cases the collection will contain the single bundle which the + * framework is trying to resolve. Other cases will cause multiple bundles + * to be included in the trigger bundles collection. When + * {@link FrameworkWiring#resolveBundles(Collection) resolveBundles} is + * called the collection of triggers must include all the current bundle + * revisions for bundles passed to resolveBundles which are in the + * {@link Bundle#INSTALLED INSTALLED} state. + *

      + * When + * {@link FrameworkWiring#refreshBundles(Collection, org.osgi.framework.FrameworkListener...)} + * is called the collection of triggers is determined with the following + * steps: + *

        + *
      • If the collection of bundles passed is null then + * {@link FrameworkWiring#getRemovalPendingBundles()} is called to get the + * initial collection of bundles.
      • + *
      • The equivalent of calling + * {@link FrameworkWiring#getDependencyClosure(Collection)} is called with + * the initial collection of bundles to get the dependency closure + * collection of the bundles being refreshed.
      • + *
      • Remove any non-active bundles from the dependency closure collection. + *
      • + *
      • For each bundle remaining in the dependency closure collection get + * the current bundle revision and add it to the collection of triggers.
      • + *
      + *

      + * As described above, a resolve process is typically initiated as a result + * of calling API that causes the framework to attempt to resolve one or + * more bundles. The framework is free to start a resolve process at any + * time for reasons other than calls to framework API. For example, a + * resolve process may be used by the framework for diagnostic purposes and + * result in no bundles actually becoming resolved at the end of the + * process. Resolver hook implementations must be prepared for resolve + * processes that are initiated for other reasons besides calls to framework + * API. + * + * @param triggers an unmodifiable collection of bundles which triggered the + * resolve process. This collection may be empty if the collection of + * trigger bundles cannot be determined. + * @return a resolver hook instance to be used for the duration of the + * resolve process. A {@code null} value may be returned which + * indicates this resolver hook factory abstains from the resolve + * process. + */ + ResolverHook begin(Collection triggers); +} diff --git a/framework/src/main/java/org/osgi/framework/hooks/resolver/package-info.java b/framework/src/main/java/org/osgi/framework/hooks/resolver/package-info.java new file mode 100644 index 00000000000..de050037c6b --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/hooks/resolver/package-info.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Framework Resolver Hooks Package Version 1.0. + * + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. + * + *

      + * Example import for consumers using the API in this package: + *

      + * {@code Import-Package: org.osgi.framework.hooks.resolver; version="[1.0,2.0)"} + * + * @author $Id$ + */ + +@Version("1.0") +package org.osgi.framework.hooks.resolver; + +import org.osgi.annotation.versioning.Version; + diff --git a/framework/src/main/java/org/osgi/framework/hooks/service/EventHook.java b/framework/src/main/java/org/osgi/framework/hooks/service/EventHook.java new file mode 100644 index 00000000000..3603e77d30a --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/hooks/service/EventHook.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) OSGi Alliance (2008, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.hooks.service; + +import java.util.Collection; +import org.osgi.annotation.versioning.ConsumerType; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceEvent; + +/** + * OSGi Framework Service Event Hook Service. + * + *

      + * Bundles registering this service will be called during framework service + * (register, modify, and unregister service) operations. + * + * @ThreadSafe + * @deprecated As of 1.1. Replaced by {@link EventListenerHook}. + * @author $Id: b8c500c6f09cc25e3a6294489b7e6a1d458d5e6f $ + */ +@ConsumerType +public interface EventHook { + /** + * Event hook method. This method is called prior to service event delivery + * when a publishing bundle registers, modifies or unregisters a service. + * This method can filter the bundles which receive the event. + * + * @param event The service event to be delivered. + * @param contexts A collection of Bundle Contexts for bundles which have + * listeners to which the specified event will be delivered. The + * implementation of this method may remove bundle contexts from the + * collection to prevent the event from being delivered to the + * associated bundles. The collection supports all the optional + * {@code Collection} operations except {@code add} and + * {@code addAll}. Attempting to add to the collection will result in + * an {@code UnsupportedOperationException}. The collection is not + * synchronized. + */ + void event(ServiceEvent event, Collection contexts); +} diff --git a/framework/src/main/java/org/osgi/framework/hooks/service/EventListenerHook.java b/framework/src/main/java/org/osgi/framework/hooks/service/EventListenerHook.java new file mode 100644 index 00000000000..3230742a73b --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/hooks/service/EventListenerHook.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.hooks.service; + +import java.util.Collection; +import java.util.Map; +import org.osgi.annotation.versioning.ConsumerType; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.hooks.service.ListenerHook.ListenerInfo; + +/** + * OSGi Framework Service Event Listener Hook Service. + * + *

      + * Bundles registering this service will be called during framework service + * (register, modify, and unregister service) operations. + * + * @ThreadSafe + * @since 1.1 + * @author $Id: 2b80241ca24005be3a9a3550138e1ba9a3a9ad6e $ + */ +@ConsumerType +public interface EventListenerHook { + /** + * Event listener hook method. This method is called prior to service event + * delivery when a publishing bundle registers, modifies or unregisters a + * service. This method can filter the listeners which receive the event. + * + * @param event The service event to be delivered. + * @param listeners A map of Bundle Contexts to a collection of Listener + * Infos for the bundle's listeners to which the specified event will + * be delivered. The implementation of this method may remove bundle + * contexts from the map and listener infos from the collection + * values to prevent the event from being delivered to the associated + * listeners. The map supports all the optional {@code Map} + * operations except {@code put} and {@code putAll}. Attempting to + * add to the map will result in an + * {@code UnsupportedOperationException}. The collection values in + * the map supports all the optional {@code Collection} operations + * except {@code add} and {@code addAll}. Attempting to add to a + * collection will result in an {@code UnsupportedOperationException} + * . The map and the collections are not synchronized. + */ + void event(ServiceEvent event, Map> listeners); +} diff --git a/framework/src/main/java/org/osgi/framework/hooks/service/FindHook.java b/framework/src/main/java/org/osgi/framework/hooks/service/FindHook.java new file mode 100644 index 00000000000..745a6b4f687 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/hooks/service/FindHook.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) OSGi Alliance (2008, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.hooks.service; + +import java.util.Collection; +import org.osgi.annotation.versioning.ConsumerType; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; + +/** + * OSGi Framework Service Find Hook Service. + * + *

      + * Bundles registering this service will be called during framework service find + * (get service references) operations. + * + * @ThreadSafe + * @author $Id: 4325a49f8c29ac9e7f32e8450db26148bbda5642 $ + */ +@ConsumerType +public interface FindHook { + /** + * Find hook method. This method is called during the service find operation + * (for example, {@link BundleContext#getServiceReferences(String, String)} + * ). This method can filter the result of the find operation. + * + * @param context The bundle context of the bundle performing the find + * operation. + * @param name The class name of the services to find or {@code null} to + * find all services. + * @param filter The filter criteria of the services to find or {@code null} + * for no filter criteria. + * @param allServices {@code true} if the find operation is the result of a + * call to + * {@link BundleContext#getAllServiceReferences(String, String)} + * @param references A collection of Service References to be returned as a + * result of the find operation. The implementation of this method + * may remove service references from the collection to prevent the + * references from being returned to the bundle performing the find + * operation. The collection supports all the optional + * {@code Collection} operations except {@code add} and + * {@code addAll}. Attempting to add to the collection will result in + * an {@code UnsupportedOperationException}. The collection is not + * synchronized. + */ + void find(BundleContext context, String name, String filter, boolean allServices, Collection> references); +} diff --git a/framework/src/main/java/org/osgi/framework/hooks/service/ListenerHook.java b/framework/src/main/java/org/osgi/framework/hooks/service/ListenerHook.java new file mode 100644 index 00000000000..7584728d843 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/hooks/service/ListenerHook.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) OSGi Alliance (2008, 2018). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.hooks.service; + +import java.util.Collection; +import org.osgi.annotation.versioning.ConsumerType; +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.BundleContext; + +/** + * OSGi Framework Service Listener Hook Service. + * + *

      + * Bundles registering this service will be called during service listener + * addition and removal. + * + * @ThreadSafe + * @author $Id: b17e35a5cab84256d78ed9b749f9829d690a752d $ + */ +@ConsumerType +public interface ListenerHook { + /** + * Added listeners hook method. This method is called to provide the hook + * implementation with information on newly added service listeners. This + * method will be called as service listeners are added while this hook is + * registered. Also, immediately after registration of this hook, this + * method will be called to provide the current collection of service + * listeners which had been added prior to the hook being registered. + * + * @param listeners A collection of {@link ListenerInfo}s for newly added + * service listeners which are now listening to service events. + * Attempting to add to or remove from the collection will result in + * an {@code UnsupportedOperationException}. The collection is not + * synchronized. + */ + void added(Collection listeners); + + /** + * Removed listeners hook method. This method is called to provide the hook + * implementation with information on newly removed service listeners. This + * method will be called as service listeners are removed while this hook is + * registered. + * + * @param listeners A collection of {@link ListenerInfo}s for newly removed + * service listeners which are no longer listening to service events. + * Attempting to add to or remove from the collection will result in + * an {@code UnsupportedOperationException}. The collection is not + * synchronized. + */ + void removed(Collection listeners); + + /** + * Information about a Service Listener. This interface describes the bundle + * which added the Service Listener and the filter with which it was added. + * + * @ThreadSafe + */ + @ProviderType + public interface ListenerInfo { + /** + * Return the context of the bundle which added the listener. + * + * @return The context of the bundle which added the listener. + */ + BundleContext getBundleContext(); + + /** + * Return the filter string with which the listener was added. + * + * @return The filter string with which the listener was added. This may + * be {@code null} if the listener was added without a filter. + */ + String getFilter(); + + /** + * Return the state of the listener for this addition and removal life + * cycle. Initially this method will return {@code false} indicating the + * listener has been added but has not been removed. After the listener + * has been removed, this method must always return {@code true}. + * + *

      + * There is an extremely rare case in which removed notification to + * {@link ListenerHook}s can be made before added notification if two + * threads are racing to add and remove the same service listener. + * Because {@link ListenerHook}s are called synchronously during service + * listener addition and removal, the Framework cannot guarantee + * in-order delivery of added and removed notification for a given + * service listener. This method can be used to detect this rare + * occurrence. + * + * @return {@code false} if the listener has not been removed, + * {@code true} otherwise. + */ + boolean isRemoved(); + + /** + * Compares this {@code ListenerInfo} to another {@code ListenerInfo}. + * Two {@code ListenerInfo}s are equals if they refer to the same + * listener for a given addition and removal life cycle. If the same + * listener is added again, it must have a different + * {@code ListenerInfo} which is not equal to this {@code ListenerInfo}. + * + * @param obj The object to compare against this {@code ListenerInfo}. + * @return {@code true} if the other object is a {@code ListenerInfo} + * object and both objects refer to the same listener for a + * given addition and removal life cycle. + */ + @Override + boolean equals(Object obj); + + /** + * Returns the hash code for this {@code ListenerInfo}. + * + * @return The hash code of this {@code ListenerInfo}. + */ + @Override + int hashCode(); + } +} diff --git a/framework/src/main/java/org/osgi/framework/hooks/service/package-info.java b/framework/src/main/java/org/osgi/framework/hooks/service/package-info.java new file mode 100644 index 00000000000..74f0c41d5eb --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/hooks/service/package-info.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Framework Service Hooks Package Version 1.1. + * + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. + * + *

      + * Example import for consumers using the API in this package: + *

      + * {@code Import-Package: org.osgi.framework.hooks.service; version="[1.1,2.0)"} + * + * @author $Id$ + */ + +@Version("1.1") +package org.osgi.framework.hooks.service; + +import org.osgi.annotation.versioning.Version; + diff --git a/framework/src/main/java/org/osgi/framework/hooks/weaving/WeavingException.java b/framework/src/main/java/org/osgi/framework/hooks/weaving/WeavingException.java new file mode 100644 index 00000000000..254dd9bf2ea --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/hooks/weaving/WeavingException.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.hooks.weaving; + +/** + * A weaving exception used to indicate that the class load should be failed but + * the weaving hook must not be blacklisted by the framework. + * + *

      + * This exception conforms to the general purpose exception chaining mechanism. + * + * @author $Id: 7575fc1b015fea7c77397391df6c8d2085513e76 $ + */ + +public class WeavingException extends RuntimeException { + private static final long serialVersionUID = 1L; + + /** + * Creates a {@code WeavingException} with the specified message and + * exception cause. + * + * @param msg The associated message. + * @param cause The cause of this exception. + */ + public WeavingException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Creates a {@code WeavingException} with the specified message. + * + * @param msg The message. + */ + public WeavingException(String msg) { + super(msg); + } +} diff --git a/framework/src/main/java/org/osgi/framework/hooks/weaving/WeavingHook.java b/framework/src/main/java/org/osgi/framework/hooks/weaving/WeavingHook.java new file mode 100644 index 00000000000..760b00c5fa1 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/hooks/weaving/WeavingHook.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.hooks.weaving; + +import org.osgi.annotation.versioning.ConsumerType; + +/** + * OSGi Framework Weaving Hook Service. + * + *

      + * Bundles registering this service will be called during framework class + * loading operations. Weaving hook services are called when a class is being + * loaded by the framework and have an opportunity to transform the class file + * bytes that represents the class being loaded. Weaving hooks may also ask the + * framework to wire in additional dynamic imports to the bundle. + * + *

      + * When a class is being loaded, the framework will create a {@link WovenClass} + * object for the class and pass it to each registered weaving hook service for + * possible modification. The first weaving hook called will see the original + * class file bytes. Subsequently called weaving hooks will see the class file + * bytes as modified by previously called weaving hooks. + * + * @ThreadSafe + * @author $Id: 8d99df5b0f3e7ffa9573695923afe86de9835fde $ + */ +@ConsumerType +public interface WeavingHook { + /** + * Weaving hook method. + * + * This method can modify the specified woven class object to weave the + * class being defined. + * + *

      + * If this method throws any exception, the framework must log the exception + * and fail the class load in progress. This weaving hook service must be + * blacklisted by the framework and must not be called again. The + * blacklisting of this weaving hook service must expire when this weaving + * hook service is unregistered. However, this method can throw a + * {@link WeavingException} to deliberately fail the class load in progress + * without being blacklisted by the framework. + * + * @param wovenClass The {@link WovenClass} object that represents the data + * that will be used to define the class. + * @throws WeavingException If this weaving hook wants to deliberately fail + * the class load in progress without being blacklisted by the + * framework + */ + public void weave(WovenClass wovenClass); +} diff --git a/framework/src/main/java/org/osgi/framework/hooks/weaving/WovenClass.java b/framework/src/main/java/org/osgi/framework/hooks/weaving/WovenClass.java new file mode 100644 index 00000000000..a7e131c9851 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/hooks/weaving/WovenClass.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.hooks.weaving; + +import java.security.ProtectionDomain; +import java.util.List; +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.wiring.BundleWiring; + +/** + * A class being woven. + * + * This object represents a class being woven and is passed to each + * {@link WeavingHook} for possible modification. It allows access to the most + * recently transformed class file bytes and to any additional packages that + * should be added to the bundle as dynamic imports. + * + *

      + * Upon entering one of the terminal states, this object becomes effectively + * immutable. + * + * @NotThreadSafe + * @author $Id: b86db7713c738ae7147fe86f754302e2e324676b $ + */ +@ProviderType +public interface WovenClass { + /** + * The woven class is being transformed. + * + *

      + * The woven class is in this state while {@link WeavingHook weaving hooks} + * are being called. The woven class is mutable so the {@link #getBytes() + * class bytes} may be {@link #setBytes(byte[]) modified} and + * {@link #getDynamicImports() dynamic imports} may be added. If a weaving + * hook throws an exception the state transitions to + * {@link #TRANSFORMING_FAILED}. Otherwise, after the last weaving hook has + * been successfully called, the state transitions to {@link #TRANSFORMED}. + * + * @since 1.1 + */ + int TRANSFORMING = 0x00000001; + + /** + * The woven class has been transformed. + * + *

      + * The woven class is in this state after {@link WeavingHook weaving hooks} + * have been called and before the class is defined. The woven class cannot + * be further transformed. The woven class is in this state while defining + * the class. If a failure occurs while defining the class, the state + * transitions to {@link #DEFINE_FAILED}. Otherwise, after the class has + * been defined, the state transitions to {@link #DEFINED}. + * + * @since 1.1 + */ + int TRANSFORMED = 0x00000002; + + /** + * The woven class has been defined. + *

      + * The woven class is in this state after the class is defined. The woven + * class cannot be further transformed. This is a terminal state. Upon + * entering this state, this object is effectively immutable, the + * {@link #getBundleWiring() bundle wiring} has been updated with the + * {@link #getDynamicImports() dynamic import requirements} and the class + * has been {@link #getDefinedClass() defined}. + * + * @since 1.1 + */ + int DEFINED = 0x00000004; + + /** + * The woven class failed to transform. + *

      + * The woven class is in this state if a {@link WeavingHook weaving hook} + * threw an exception. The woven class cannot be further transformed or + * defined. This is a terminal state. Upon entering this state, this object + * is effectively immutable. + * + * @since 1.1 + */ + int TRANSFORMING_FAILED = 0x00000008; + + /** + * The woven class failed to define. + *

      + * The woven class is in this state when a failure occurs while defining the + * class. The woven class cannot be further transformed or defined. This is + * a terminal state. Upon entering this state, this object is effectively + * immutable. + * + * @since 1.1 + */ + int DEFINE_FAILED = 0x00000010; + + /** + * Returns the class file bytes to be used to define the + * {@link WovenClass#getClassName() named} class. + * + *

      + * While in the {@link #TRANSFORMING} state, this method returns a reference + * to the class files byte array contained in this object. After leaving the + * {@link #TRANSFORMING} state, this woven class can no longer be + * transformed and a copy of the class file byte array is returned. + * + * @return The bytes to be used to define the + * {@link WovenClass#getClassName() named} class. + * @throws SecurityException If the caller does not have + * {@code AdminPermission[bundle,WEAVE]} and the Java runtime + * environment supports permissions. + */ + public byte[] getBytes(); + + /** + * Set the class file bytes to be used to define the + * {@link WovenClass#getClassName() named} class. This method must not be + * called outside invocations of the {@link WeavingHook#weave(WovenClass) + * weave} method by the framework. + * + *

      + * While in the {@link #TRANSFORMING} state, this method replaces the + * reference to the array contained in this object with the specified array. + * After leaving the {@link #TRANSFORMING} state, this woven class can no + * longer be transformed and this method will throw an + * {@link IllegalStateException}. + * + * @param newBytes The new classfile that will be used to define the + * {@link WovenClass#getClassName() named} class. The specified array + * is retained by this object and the caller must not modify the + * specified array. + * @throws NullPointerException If newBytes is {@code null}. + * @throws IllegalStateException If state is {@link #TRANSFORMED}, + * {@link #DEFINED}, {@link #TRANSFORMING_FAILED} or + * {@link #DEFINE_FAILED}. + * @throws SecurityException If the caller does not have + * {@code AdminPermission[bundle,WEAVE]} and the Java runtime + * environment supports permissions. + */ + public void setBytes(byte[] newBytes); + + /** + * Returns the list of dynamic import package descriptions to add to the + * {@link #getBundleWiring() bundle wiring} for this woven class. Changes + * made to the returned list will be visible to later {@link WeavingHook + * weaving hooks} called with this object. The returned list must not be + * modified outside invocations of the {@link WeavingHook#weave(WovenClass) + * weave} method by the framework. + * + *

      + * After leaving the {@link #TRANSFORMING} state, this woven class can no + * longer be transformed and the returned list will be unmodifiable. + * + *

      + * If the Java runtime environment supports permissions, any modification to + * the returned list requires {@code AdminPermission[bundle,WEAVE]}. + * Additionally, any add or set modification requires + * {@code PackagePermission[package,IMPORT]}. + * + * @return A list containing zero or more dynamic import package + * descriptions to add to the bundle wiring for this woven class. + * This list must throw {@code IllegalArgumentException} if a + * malformed dynamic import package description is added. + * @see "Core Specification, Dynamic Import Package, for the syntax of a dynamic import package description." + */ + public List getDynamicImports(); + + /** + * Returns whether weaving is complete in this woven class. Weaving is + * complete after the class is defined. + * + * @return {@code true} if {@link #getState() state} is {@link #DEFINED}, + * {@link #TRANSFORMING_FAILED} or {@link #DEFINE_FAILED}; + * {@code false} otherwise. + */ + public boolean isWeavingComplete(); + + /** + * Returns the fully qualified name of the class being woven. + * + * @return The fully qualified name of the class being woven. + */ + public String getClassName(); + + /** + * Returns the protection domain to which the woven class will be assigned + * when it is defined. + * + * @return The protection domain to which the woven class will be assigned + * when it is defined, or {@code null} if no protection domain will + * be assigned. + */ + public ProtectionDomain getProtectionDomain(); + + /** + * Returns the class defined by this woven class. During weaving, this + * method will return {@code null}. Once weaving is + * {@link #isWeavingComplete() complete}, this method will return the class + * object if this woven class was used to define the class. + * + * @return The class associated with this woven class, or {@code null} if + * weaving is not complete, the class definition failed or this + * woven class was not used to define the class. + */ + public Class getDefinedClass(); + + /** + * Returns the bundle wiring whose class loader will define the woven class. + * + * @return The bundle wiring whose class loader will define the woven class. + */ + public BundleWiring getBundleWiring(); + + /** + * Returns the current state of this woven class. + *

      + * A woven class can be in only one state at any time. + * + * @return Either {@link #TRANSFORMING}, {@link #TRANSFORMED}, + * {@link #DEFINED}, {@link #TRANSFORMING_FAILED} or + * {@link #DEFINE_FAILED}. + * @since 1.1 + */ + public int getState(); +} diff --git a/framework/src/main/java/org/osgi/framework/hooks/weaving/WovenClassListener.java b/framework/src/main/java/org/osgi/framework/hooks/weaving/WovenClassListener.java new file mode 100644 index 00000000000..4a7a69943bf --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/hooks/weaving/WovenClassListener.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.hooks.weaving; + +import org.osgi.annotation.versioning.ConsumerType; + +/** + * Woven Class Listener Service. + * + *

      + * Bundles registering this service will receive notifications whenever a + * {@link WovenClass woven class} completes a {@link WovenClass#getState() + * state} transition. Woven Class Listeners are not able to modify the woven + * class in contrast with {@link WeavingHook weaving hooks}. + * + *

      + * Receiving a woven class in the {@link WovenClass#TRANSFORMED TRANSFORMED} + * state allows listeners to observe the modified {@link WovenClass#getBytes() + * byte codes} before the class has been {@link WovenClass#DEFINED DEFINED} as + * well as the additional {@link WovenClass#getDynamicImports() dynamic imports} + * before the {@link WovenClass#getBundleWiring() bundle wiring} has been + * updated. + * + *

      + * Woven class listeners are synchronously {@link #modified(WovenClass) called} + * when a woven class completes a state transition. The woven class processing + * will not proceed until all woven class listeners are done. + * + *

      + * If the Java runtime environment supports permissions, the caller must have + * {@code ServicePermission[WovenClassListener,REGISTER]} in order to register a + * listener. + * + * @ThreadSafe + * @since 1.1 + * @author $Id$ + */ +@ConsumerType +public interface WovenClassListener { + /** + * Receives notification that a {@link WovenClass woven class} has completed + * a state transition. + * + *

      + * The listener will be notified when a woven class has entered the + * {@link WovenClass#TRANSFORMED TRANSFORMED}, {@link WovenClass#DEFINED + * DEFINED}, {@link WovenClass#TRANSFORMING_FAILED TRANSFORMING_FAILED} and + * {@link WovenClass#DEFINE_FAILED DEFINE_FAILED} states. + * + *

      + * If this method throws any exception, the Framework must log the exception + * but otherwise ignore it. + * + * @param wovenClass The woven class that completed a state transition. + */ + public void modified(WovenClass wovenClass); +} diff --git a/framework/src/main/java/org/osgi/framework/hooks/weaving/package-info.java b/framework/src/main/java/org/osgi/framework/hooks/weaving/package-info.java new file mode 100644 index 00000000000..5b48f041a17 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/hooks/weaving/package-info.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Framework Weaving Hooks Package Version 1.1. + * + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. + *

      + * + *

      + * Example import for consumers using the API in this package: + *

      + *

      + * {@code Import-Package: org.osgi.framework.hooks.weaving; version="[1.1,2.0)"} + *

      + * @author $Id$ + */ + +@Version("1.1") +package org.osgi.framework.hooks.weaving; + +import org.osgi.annotation.versioning.Version; + diff --git a/framework/src/main/java/org/osgi/framework/launch/Framework.java b/framework/src/main/java/org/osgi/framework/launch/Framework.java new file mode 100644 index 00000000000..37a519dae9b --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/launch/Framework.java @@ -0,0 +1,421 @@ +/* + * Copyright (c) OSGi Alliance (2008, 2018). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.launch; + +import java.io.InputStream; +import java.net.URL; +import java.util.Enumeration; +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; + +/** + * A Framework instance. A Framework is also known as a System Bundle. + * + *

      + * Framework instances are created using a {@link FrameworkFactory}. The methods + * of this interface can be used to manage and control the created framework + * instance. + * + * @ThreadSafe + * @author $Id: 7fa67978e59a43dfedd6e755ddfa5b1aa6ea9141 $ + */ +@ProviderType +public interface Framework extends Bundle { + + /** + * Initialize this Framework. + *

      + * This method performs the same function as calling + * {@link #init(FrameworkListener...)} with no framework listeners. + * + * @throws BundleException If this Framework could not be initialized. + * @throws SecurityException If the Java Runtime Environment supports + * permissions and the caller does not have the appropriate + * {@code AdminPermission[this,EXECUTE]} or if there is a security + * manager already installed and the + * {@link Constants#FRAMEWORK_SECURITY} configuration property is + * set. + * @see #init(FrameworkListener...) + */ + void init() throws BundleException; + + /** + * Initialize this Framework. After calling this method, this Framework + * must: + *

        + *
      • Have generated a new {@link Constants#FRAMEWORK_UUID framework UUID}. + *
      • + *
      • Be in the {@link #STARTING} state.
      • + *
      • Have a valid Bundle Context.
      • + *
      • Be at start level 0.
      • + *
      • Have event handling enabled.
      • + *
      • Have reified Bundle objects for all installed bundles.
      • + *
      • Have registered any framework services. For example, + * {@code ConditionalPermissionAdmin}.
      • + *
      • Be {@link #adapt(Class) adaptable} to the OSGi defined types to which + * a system bundle can be adapted.
      • + *
      • Have called the {@code start} method of the extension bundle + * activator for all resolved extension bundles.
      • + *
      + * + *

      + * This Framework will not actually be started until {@link #start() start} + * is called. + * + *

      + * This method does nothing if called when this Framework is in the + * {@link #STARTING}, {@link #ACTIVE} or {@link #STOPPING} states. + * + *

      + * All framework events fired by this method are also delivered to the + * specified FrameworkListeners in the order they are specified before + * returning from this method. After returning from this method the + * specified listeners are no longer notified of framework events. + * + * @param listeners Zero or more listeners to be notified when framework + * events occur while initializing the framework. The specified + * listeners do not need to be otherwise registered with the + * framework. If a specified listener is registered with the + * framework, it will be notified twice for each framework event. + * @throws BundleException If this Framework could not be initialized. + * @throws SecurityException If the Java Runtime Environment supports + * permissions and the caller does not have the appropriate + * {@code AdminPermission[this,EXECUTE]} or if there is a security + * manager already installed and the + * {@link Constants#FRAMEWORK_SECURITY} configuration property is + * set. + * @since 1.2 + */ + void init(FrameworkListener... listeners) throws BundleException; + + /** + * Wait until this Framework has completely stopped. The {@code stop} and + * {@code update} methods on a Framework performs an asynchronous stop of + * the Framework. This method can be used to wait until the asynchronous + * stop of this Framework has completed. This method will only wait if + * called when this Framework is in the {@link #STARTING}, {@link #ACTIVE}, + * or {@link #STOPPING} states. Otherwise it will return immediately. + *

      + * A Framework Event is returned to indicate why this Framework has stopped. + * + * @param timeout Maximum number of milliseconds to wait until this + * Framework has completely stopped. A value of zero will wait + * indefinitely. + * @return A Framework Event indicating the reason this method returned. The + * following {@code FrameworkEvent} types may be returned by this + * method. + *

        + *
      • {@link FrameworkEvent#STOPPED STOPPED} - This Framework has + * been stopped.
      • + * + *
      • {@link FrameworkEvent#STOPPED_UPDATE STOPPED_UPDATE} - This + * Framework has been updated which has shutdown and will now + * restart.
      • + * + *
      • {@link FrameworkEvent#STOPPED_BOOTCLASSPATH_MODIFIED + * STOPPED_BOOTCLASSPATH_MODIFIED} - This Framework has been stopped + * and a bootclasspath extension bundle has been installed or + * updated. The VM must be restarted in order for the changed boot + * class path to take effect.
      • + * + *
      • {@link FrameworkEvent#ERROR ERROR} - The Framework + * encountered an error while shutting down or an error has occurred + * which forced the framework to shutdown.
      • + * + *
      • {@link FrameworkEvent#WAIT_TIMEDOUT WAIT_TIMEDOUT} - This + * method has timed out and returned before this Framework has + * stopped.
      • + *
      + * @throws InterruptedException If another thread interrupted the current + * thread before or while the current thread was waiting for this + * Framework to completely stop. The interrupted status of + * the current thread is cleared when this exception is thrown. + * @throws IllegalArgumentException If the value of timeout is negative. + */ + FrameworkEvent waitForStop(long timeout) throws InterruptedException; + + /** + * Start this Framework. + * + *

      + * The following steps are taken to start this Framework: + *

        + *
      1. If this Framework is not in the {@link #STARTING} state, + * {@link #init() initialize} this Framework.
      2. + *
      3. All installed bundles must be started in accordance with each + * bundle's persistent autostart setting. This means some bundles + * will not be started, some will be started with eager activation + * and some will be started with their declared activation policy. + * The start level of this Framework is moved to the start level specified + * by the {@link Constants#FRAMEWORK_BEGINNING_STARTLEVEL beginning start + * level} framework property, as described in the Start Level + * Specification. If this framework property is not specified, then the + * start level of this Framework is moved to start level one (1). Any + * exceptions that occur during bundle starting must be wrapped in a + * {@link BundleException} and then published as a framework event of type + * {@link FrameworkEvent#ERROR}
      4. + *
      5. This Framework's state is set to {@link #ACTIVE}.
      6. + *
      7. A framework event of type {@link FrameworkEvent#STARTED} is fired
      8. + *
      + * + * @throws BundleException If this Framework could not be started. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[this,EXECUTE]}, and the Java Runtime + * Environment supports permissions. + * @see "Start Level Specification" + */ + @Override + void start() throws BundleException; + + /** + * Start this Framework. + * + *

      + * Calling this method is the same as calling {@link #start()}. There are no + * start options for the Framework. + * + * @param options Ignored. There are no start options for the Framework. + * @throws BundleException If this Framework could not be started. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[this,EXECUTE]}, and the Java Runtime + * Environment supports permissions. + * @see #start() + */ + @Override + void start(int options) throws BundleException; + + /** + * Stop this Framework. + * + *

      + * The method returns immediately to the caller after initiating the + * following steps to be taken on another thread. + *

        + *
      1. This Framework's state is set to {@link #STOPPING}.
      2. + *
      3. All installed bundles must be stopped without changing each bundle's + * persistent autostart setting. The start level of this Framework is + * moved to start level zero (0), as described in the Start Level + * Specification. Any exceptions that occur during bundle stopping must + * be wrapped in a {@link BundleException} and then published as a framework + * event of type {@link FrameworkEvent#ERROR}
      4. + *
      5. Unregister all services registered by this Framework.
      6. + *
      7. Event handling is disabled.
      8. + *
      9. This Framework's state is set to {@link #RESOLVED}.
      10. + *
      11. All resources held by this Framework are released. This includes + * threads, bundle class loaders, open files, etc.
      12. + *
      13. Notify all threads that are waiting at {@link #waitForStop(long) + * waitForStop} that the stop operation has completed.
      14. + *
      + *

      + * After being stopped, this Framework may be discarded, initialized or + * started. + * + * @throws BundleException If stopping this Framework could not be + * initiated. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[this,EXECUTE]}, and the Java Runtime + * Environment supports permissions. + * @see "Start Level Specification" + */ + @Override + void stop() throws BundleException; + + /** + * Stop this Framework. + * + *

      + * Calling this method is the same as calling {@link #stop()}. There are no + * stop options for the Framework. + * + * @param options Ignored. There are no stop options for the Framework. + * @throws BundleException If stopping this Framework could not be + * initiated. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[this,EXECUTE]}, and the Java Runtime + * Environment supports permissions. + * @see #stop() + */ + @Override + void stop(int options) throws BundleException; + + /** + * The Framework cannot be uninstalled. + * + *

      + * This method always throws a BundleException. + * + * @throws BundleException This Framework cannot be uninstalled. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[this,LIFECYCLE]}, and the Java Runtime + * Environment supports permissions. + */ + @Override + void uninstall() throws BundleException; + + /** + * Stop and restart this Framework. + * + *

      + * The method returns immediately to the caller after initiating the + * following steps to be taken on another thread. + *

        + *
      1. Perform the steps in the {@link #stop()} method to stop this + * Framework.
      2. + *
      3. Perform the steps in the {@link #start()} method to start this + * Framework.
      4. + *
      + * + * @throws BundleException If stopping and restarting this Framework could + * not be initiated. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[this,LIFECYCLE]}, and the Java Runtime + * Environment supports permissions. + */ + @Override + void update() throws BundleException; + + /** + * Stop and restart this Framework. + * + *

      + * Calling this method is the same as calling {@link #update()} except that + * any provided InputStream is immediately closed. + * + * @param in Any provided InputStream is immediately closed before returning + * from this method and otherwise ignored. + * @throws BundleException If stopping and restarting this Framework could + * not be initiated. + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[this,LIFECYCLE]}, and the Java Runtime + * Environment supports permissions. + */ + @Override + void update(InputStream in) throws BundleException; + + /** + * Returns the Framework unique identifier. This Framework is assigned the + * unique identifier zero (0) since this Framework is also a System Bundle. + * + * @return 0. + * @see Bundle#getBundleId() + */ + @Override + long getBundleId(); + + /** + * Returns the Framework location identifier. This Framework is assigned the + * unique location "{@code System Bundle}" since this Framework is + * also a System Bundle. + * + * @return The string "{@code System Bundle}". + * @throws SecurityException If the caller does not have the appropriate + * {@code AdminPermission[this,METADATA]}, and the Java Runtime + * Environment supports permissions. + * @see Bundle#getLocation() + * @see Constants#SYSTEM_BUNDLE_LOCATION + */ + @Override + String getLocation(); + + /** + * Returns the symbolic name of this Framework. The symbolic name is unique + * for the implementation of the framework. However, the symbolic name + * "{@code system.bundle}" must be recognized as an alias to the + * implementation-defined symbolic name since this Framework is also a + * System Bundle. + * + * @return The symbolic name of this Framework. + * @see Bundle#getSymbolicName() + * @see Constants#SYSTEM_BUNDLE_SYMBOLICNAME + */ + @Override + String getSymbolicName(); + + /** + * Returns {@code null} as a framework implementation does not have a proper + * bundle from which to return entry paths. + * + * @param path Ignored. + * @return {@code null} as a framework implementation does not have a proper + * bundle from which to return entry paths. + */ + @Override + Enumeration getEntryPaths(String path); + + /** + * Returns {@code null} as a framework implementation does not have a proper + * bundle from which to return an entry. + * + * @param path Ignored. + * @return {@code null} as a framework implementation does not have a proper + * bundle from which to return an entry. + */ + @Override + URL getEntry(String path); + + /** + * Returns the time when the set of bundles in this framework was last + * modified. The set of bundles is considered to be modified when a bundle + * is installed, updated or uninstalled. + * + *

      + * The time value is the number of milliseconds since January 1, 1970, + * 00:00:00 UTC. + * + * @return The time when the set of bundles in this framework was last + * modified. + */ + @Override + long getLastModified(); + + /** + * Returns {@code null} as a framework implementation does not have a proper + * bundle from which to return entries. + * + * @param path Ignored. + * @param filePattern Ignored. + * @param recurse Ignored. + * @return {@code null} as a framework implementation does not have a proper + * bundle from which to return entries. + */ + @Override + Enumeration findEntries(String path, String filePattern, boolean recurse); + + /** + * Adapt this Framework to the specified type. + * + *

      + * Adapting this Framework to the specified type may require certain checks, + * including security checks, to succeed. If a check does not succeed, then + * this Framework cannot be adapted and {@code null} is returned. If this + * Framework is not {@link #init() initialized}, then {@code null} is + * returned if the specified type is one of the OSGi defined types to which + * a system bundle can be adapted. + * + * @param The type to which this Framework is to be adapted. + * @param type Class object for the type to which this Framework is to be + * adapted. + * @return The object, of the specified type, to which this Framework has + * been adapted or {@code null} if this Framework cannot be adapted + */ + @Override + A adapt(Class type); +} diff --git a/framework/src/main/java/org/osgi/framework/launch/FrameworkFactory.java b/framework/src/main/java/org/osgi/framework/launch/FrameworkFactory.java new file mode 100644 index 00000000000..6d656d34e64 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/launch/FrameworkFactory.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) OSGi Alliance (2009, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.launch; + +import java.util.Map; +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.Bundle; + +/** + * A factory for creating {@link Framework} instances. + * + *

      + * A framework implementation jar must contain the following resource: + * + *

      + * /META-INF/services/org.osgi.framework.launch.FrameworkFactory
      + * 
      + * + * This UTF-8 encoded resource must contain the name of the framework + * implementation's FrameworkFactory implementation class. Space and tab + * characters, including blank lines, in the resource must be ignored. The + * number sign ({@code '#'} \u0023) and all characters following it on each + * line are a comment and must be ignored. + * + *

      + * Launchers can find the name of the FrameworkFactory implementation class in + * the resource and then load and construct a FrameworkFactory object for the + * framework implementation. The FrameworkFactory implementation class must have + * a public, no-argument constructor. Java™ SE 6 introduced the + * {@code ServiceLoader} class which can create a FrameworkFactory instance from + * the resource. + * + * @ThreadSafe + * @author $Id: c1647bcb8416b6dfa9e37c6cc146bb54c7173526 $ + */ +@ProviderType +public interface FrameworkFactory { + + /** + * Create a new {@link Framework} instance. + * + * @param configuration The framework properties to configure the new + * framework instance. If framework properties are not provided by + * the configuration argument, the created framework instance must + * use some reasonable default configuration appropriate for the + * current VM. For example, the system packages for the current + * execution environment should be properly exported. The specified + * configuration argument may be {@code null}. The created framework + * instance must copy any information needed from the specified + * configuration argument since the configuration argument can be + * changed after the framework instance has been created. + * @return A new, configured {@link Framework} instance. The framework + * instance must be in the {@link Bundle#INSTALLED} state. + * @throws SecurityException If the caller does not have + * {@code AllPermission}, and the Java Runtime Environment supports + * permissions. + */ + Framework newFramework(Map configuration); +} diff --git a/framework/src/main/java/org/osgi/framework/launch/package-info.java b/framework/src/main/java/org/osgi/framework/launch/package-info.java new file mode 100644 index 00000000000..db5e926caf1 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/launch/package-info.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Framework Launch Package Version 1.2. + * + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. + * + *

      + * Example import for consumers using the API in this package: + *

      + * {@code Import-Package: org.osgi.framework.launch; version="[1.2,2.0)"} + * + * @author $Id$ + */ + +@Version("1.2") +package org.osgi.framework.launch; + +import org.osgi.annotation.versioning.Version; + diff --git a/framework/src/main/java/org/osgi/framework/namespace/AbstractWiringNamespace.java b/framework/src/main/java/org/osgi/framework/namespace/AbstractWiringNamespace.java new file mode 100644 index 00000000000..c5186a1b21a --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/namespace/AbstractWiringNamespace.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.namespace; + +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.resource.Namespace; + +/** + * Wiring Capability and Requirement Namespaces base class. + * + *

      + * This class is the common class shared by all OSGi defined wiring namespaces. + * It defines the names for the common attributes and directives for the OSGi + * specified wiring namespaces. + * + *

      + * The values associated with these keys are of type {@code String}, unless + * otherwise indicated. + * + * @Immutable + * @author $Id: 6bbd0ddefc452b0ace2f43ec3aa67a687adcf03c $ + */ +@ProviderType +public abstract class AbstractWiringNamespace extends Namespace { + + /** + * The capability directive used to specify the comma separated list of + * mandatory attributes which must be specified in the + * {@link Namespace#REQUIREMENT_FILTER_DIRECTIVE filter} of a requirement in + * order for the capability to match the requirement. + */ + public final static String CAPABILITY_MANDATORY_DIRECTIVE = "mandatory"; + + /** + * The capability attribute contains the {@code Version} of the resource + * providing the capability if one is specified or {@code 0.0.0} if not + * specified. The value of this attribute must be of type {@code Version}. + */ + public static final String CAPABILITY_BUNDLE_VERSION_ATTRIBUTE = "bundle-version"; + + AbstractWiringNamespace() { + // empty + } +} diff --git a/framework/src/main/java/org/osgi/framework/namespace/BundleNamespace.java b/framework/src/main/java/org/osgi/framework/namespace/BundleNamespace.java new file mode 100644 index 00000000000..59e36cbd8d4 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/namespace/BundleNamespace.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.namespace; + +import org.osgi.resource.Namespace; + +/** + * Bundle Capability and Requirement Namespace. + * + *

      + * This class defines the names for the attributes and directives for this + * namespace. All unspecified capability attributes are of type {@code String} + * and are used as arbitrary matching attributes for the capability. The values + * associated with the specified directive and attribute keys are of type + * {@code String}, unless otherwise indicated. + * + *

      + * Unless otherwise noted, all directives specified on the + * {@code Bundle-SymbolicName} header are visible in the capability and all + * directives specified on the {@code Require-Bundle} header are visible in the + * requirement. + * + *

        + *
      • The {@link Namespace#CAPABILITY_USES_DIRECTIVE uses} directive must be + * ignored. A {@code uses} directive specified on the + * {@code Bundle-SymbolicName} header must be ignored. A {@code uses} directive + * must not be present in the capability.
      • + *
      • The {@link Namespace#CAPABILITY_EFFECTIVE_DIRECTIVE effective} + * {@link Namespace#REQUIREMENT_EFFECTIVE_DIRECTIVE directives} must be ignored. + * This namespace is only effective at {@link Namespace#EFFECTIVE_RESOLVE + * resolve} time. An {@code effective} directive specified on the + * {@code Bundle-SymbolicName} or {@code Require-Bundle} headers must be + * ignored. An {@code effective} directive must not be present in a capability + * or requirement.
      • + *
      • The {@link Namespace#REQUIREMENT_CARDINALITY_DIRECTIVE cardinality} + * directive must be ignored. A {@code cardinality} directive specified on the + * {@code Require-Bundle} header must be ignored. A {@code cardinality} + * directive must not be present in a requirement.
      • + *
      + * + *

      + * A non-fragment resource with the {@link IdentityNamespace#TYPE_BUNDLE + * osgi.bundle} type {@link IdentityNamespace#CAPABILITY_TYPE_ATTRIBUTE + * identity} provides exactly one bundle capability (that is, + * the bundle can be required by another bundle). A fragment resource with the + * {@link IdentityNamespace#TYPE_FRAGMENT osgi.fragment} type + * {@link IdentityNamespace#CAPABILITY_TYPE_ATTRIBUTE identity} must not declare + * a bundle capability. A resource requires zero or more bundle requirements + * (that is, required bundles). + *

      + * † A resource with no symbolic name must not provide a bundle + * capability. + * + * @Immutable + * @author $Id: 2672d40cf3705b2cf21d01530e4bdfa2cdc61764 $ + */ +public final class BundleNamespace extends AbstractWiringNamespace { + + /** + * Namespace name for bundle capabilities and requirements. + * + *

      + * Also, the capability attribute used to specify the symbolic name of the + * bundle. + */ + public static final String BUNDLE_NAMESPACE = "osgi.wiring.bundle"; + + /** + * The capability directive identifying if the resource is a singleton. A + * {@code String} value of "{@code true}" indicates the resource + * is a singleton; any other value or {@code null} indicates the resource is + * not a singleton. + * + *

      + * This directive should be examined using the {@link IdentityNamespace + * identity} namespace. + * + * @see IdentityNamespace#CAPABILITY_SINGLETON_DIRECTIVE + */ + public static final String CAPABILITY_SINGLETON_DIRECTIVE = "singleton"; + + /** + * The capability directive identifying if and when a fragment may attach to + * a host bundle. + * + *

      + * This directive should be examined using the {@link HostNamespace host} + * namespace. + * + * @see HostNamespace#CAPABILITY_FRAGMENT_ATTACHMENT_DIRECTIVE + */ + public static final String CAPABILITY_FRAGMENT_ATTACHMENT_DIRECTIVE = "fragment-attachment"; + + /** + * The requirement directive used to specify the type of the extension + * fragment. + * + *

      + * This directive should be examined using the {@link HostNamespace host} + * namespace. + * + * @see HostNamespace#REQUIREMENT_EXTENSION_DIRECTIVE + */ + public final static String REQUIREMENT_EXTENSION_DIRECTIVE = "extension"; + + /** + * The requirement directive used to specify the visibility type for a + * requirement. The default value is {@link #VISIBILITY_PRIVATE private}. + * + * @see #VISIBILITY_PRIVATE private + * @see #VISIBILITY_REEXPORT reexport + */ + public final static String REQUIREMENT_VISIBILITY_DIRECTIVE = "visibility"; + + /** + * The directive value identifying a private + * {@link #REQUIREMENT_VISIBILITY_DIRECTIVE visibility} type. A private + * visibility type indicates that any {@link PackageNamespace packages} that + * are exported by the required bundle are not made visible on the export + * signature of the requiring bundle. . + * + * @see #REQUIREMENT_VISIBILITY_DIRECTIVE + */ + public final static String VISIBILITY_PRIVATE = "private"; + + /** + * The directive value identifying a reexport + * {@link #REQUIREMENT_VISIBILITY_DIRECTIVE visibility} type. A reexport + * visibility type indicates any {@link PackageNamespace packages} that are + * exported by the required bundle are re-exported by the requiring bundle. + * + * @see #REQUIREMENT_VISIBILITY_DIRECTIVE + */ + public final static String VISIBILITY_REEXPORT = "reexport"; + + private BundleNamespace() { + // empty + } +} diff --git a/framework/src/main/java/org/osgi/framework/namespace/ExecutionEnvironmentNamespace.java b/framework/src/main/java/org/osgi/framework/namespace/ExecutionEnvironmentNamespace.java new file mode 100644 index 00000000000..a3f79a09cd0 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/namespace/ExecutionEnvironmentNamespace.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.namespace; + +import org.osgi.resource.Namespace; + +/** + * Execution Environment Capability and Requirement Namespace. + * + *

      + * This class defines the names for the attributes and directives for this + * namespace. All unspecified capability attributes are of type {@code String} + * and are used as arbitrary matching attributes for the capability. The values + * associated with the specified directive and attribute keys are of type + * {@code String}, unless otherwise indicated. + * + * @Immutable + * @author $Id: 45a233050d35a5debc8a7d1be1f7e81178916984 $ + */ +public final class ExecutionEnvironmentNamespace extends Namespace { + + /** + * Namespace name for execution environment capabilities and requirements. + * + *

      + * Also, the capability attribute used to specify the name of the execution + * environment. + */ + public static final String EXECUTION_ENVIRONMENT_NAMESPACE = "osgi.ee"; + + /** + * The capability attribute contains the versions of the execution + * environment. The value of this attribute must be of type + * {@code List}. + */ + public final static String CAPABILITY_VERSION_ATTRIBUTE = "version"; + + private ExecutionEnvironmentNamespace() { + // empty + } +} diff --git a/framework/src/main/java/org/osgi/framework/namespace/HostNamespace.java b/framework/src/main/java/org/osgi/framework/namespace/HostNamespace.java new file mode 100644 index 00000000000..2f843a86a43 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/namespace/HostNamespace.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.namespace; + +import org.osgi.resource.Namespace; + +/** + * Host Capability and Requirement Namespace. + * + *

      + * This class defines the names for the attributes and directives for this + * namespace. All unspecified capability attributes are of type {@code String} + * and are used as arbitrary matching attributes for the capability. The values + * associated with the specified directive and attribute keys are of type + * {@code String}, unless otherwise indicated. + * + *

      + * Unless otherwise noted, all directives specified on the + * {@code Bundle-SymbolicName} header are visible in the capability and all + * directives specified on the {@code Fragment-Host} header are visible in the + * requirement. + * + *

        + *
      • The {@link Namespace#CAPABILITY_USES_DIRECTIVE uses} directive must be + * ignored. A {@code uses} directive specified on the + * {@code Bundle-SymbolicName} header must be ignored. A {@code uses} directive + * must not be present in the capability.
      • + *
      • The {@link Namespace#CAPABILITY_EFFECTIVE_DIRECTIVE effective} + * {@link Namespace#REQUIREMENT_EFFECTIVE_DIRECTIVE directives} must be ignored. + * This namespace is only effective at {@link Namespace#EFFECTIVE_RESOLVE + * resolve} time. An {@code effective} directive specified on the + * {@code Bundle-SymbolicName} or {@code Fragment-Host} headers must be ignored. + * An {@code effective} directive must not be present in a capability or + * requirement.
      • + *
      • The {@link Namespace#REQUIREMENT_CARDINALITY_DIRECTIVE cardinality} + * directive has limited applicability to this namespace. A {@code cardinality} + * directive specified on the {@code Fragment-Host} header must be ignored. All + * requirements must have the {@code cardinality} directive set to + * {@link Namespace#CARDINALITY_MULTIPLE multiple}.
      • + *
      + * + *

      + * A non-fragment resource with the with the + * {@link IdentityNamespace#TYPE_BUNDLE osgi.bundle} type + * {@link IdentityNamespace#CAPABILITY_TYPE_ATTRIBUTE identity} provides zero or + * one host capabilities. A fragment resource with the + * {@link IdentityNamespace#TYPE_FRAGMENT osgi.fragment} type + * {@link IdentityNamespace#CAPABILITY_TYPE_ATTRIBUTE identity} must not declare + * a host capability and must declare exactly one host requirement. + *

      + * † A resource with no bundle symbolic name must not provide a host + * capability. + * + * @Immutable + * @author $Id: 9f789ca25dafcf9d5e9a4f45d377f943d62b134a $ + */ +public final class HostNamespace extends AbstractWiringNamespace { + + /** + * Namespace name for host capabilities and requirements. + * + *

      + * Also, the capability attribute used to specify the symbolic name of the + * host. + * + */ + public static final String HOST_NAMESPACE = "osgi.wiring.host"; + + /** + * The capability directive identifying if the resource is a singleton. A + * {@code String} value of "{@code true}" indicates the resource + * is a singleton; any other value or {@code null} indicates the resource is + * not a singleton. + * + *

      + * This directive should be examined using the {@link IdentityNamespace + * identity} namespace. + * + * @see IdentityNamespace#CAPABILITY_SINGLETON_DIRECTIVE + */ + public static final String CAPABILITY_SINGLETON_DIRECTIVE = "singleton"; + + /** + * The capability directive identifying if and when a fragment may attach to + * a host bundle. The default value is {@link #FRAGMENT_ATTACHMENT_ALWAYS + * always}. + * + * @see #FRAGMENT_ATTACHMENT_ALWAYS + * @see #FRAGMENT_ATTACHMENT_RESOLVETIME + * @see #FRAGMENT_ATTACHMENT_NEVER + */ + public static final String CAPABILITY_FRAGMENT_ATTACHMENT_DIRECTIVE = "fragment-attachment"; + + /** + * The directive value indicating that fragments are allowed to attach to + * the host bundle at any time (while the host is resolved or during the + * process of resolving the host bundle). + * + * @see #CAPABILITY_FRAGMENT_ATTACHMENT_DIRECTIVE + */ + public static final String FRAGMENT_ATTACHMENT_ALWAYS = "always"; + + /** + * The directive value indicating that fragments are allowed to attach to + * the host bundle only during the process of resolving the host bundle. + * + * @see #CAPABILITY_FRAGMENT_ATTACHMENT_DIRECTIVE + */ + public static final String FRAGMENT_ATTACHMENT_RESOLVETIME = "resolve-time"; + + /** + * The directive value indicating that no fragments are allowed to attach to + * the host bundle at any time. + * + * @see #CAPABILITY_FRAGMENT_ATTACHMENT_DIRECTIVE + */ + public static final String FRAGMENT_ATTACHMENT_NEVER = "never"; + + /** + * The requirement directive used to specify the type of the extension + * fragment. The default value is {@link #EXTENSION_FRAMEWORK framework}. + * + * @see #EXTENSION_FRAMEWORK + * @see #EXTENSION_BOOTCLASSPATH + */ + public final static String REQUIREMENT_EXTENSION_DIRECTIVE = "extension"; + + /** + * The directive value indicating that the extension fragment is to be + * loaded by the framework's class loader. + * + * + * @see #REQUIREMENT_EXTENSION_DIRECTIVE + */ + public final static String EXTENSION_FRAMEWORK = "framework"; + + /** + * The directive value indicating that the extension fragment is to be + * loaded by the boot class loader. + * + * @see #REQUIREMENT_EXTENSION_DIRECTIVE + */ + public final static String EXTENSION_BOOTCLASSPATH = "bootclasspath"; + + /** + * The requirement directive used to specify the visibility type for a + * requirement. + * + *

      + * This directive should be examined using the {@link BundleNamespace + * bundle} namespace. + * + * @see BundleNamespace#REQUIREMENT_VISIBILITY_DIRECTIVE + */ + public final static String REQUIREMENT_VISIBILITY_DIRECTIVE = "visibility"; + + private HostNamespace() { + // empty + } +} diff --git a/framework/src/main/java/org/osgi/framework/namespace/IdentityNamespace.java b/framework/src/main/java/org/osgi/framework/namespace/IdentityNamespace.java new file mode 100644 index 00000000000..cfbb843a970 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/namespace/IdentityNamespace.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.namespace; + +import org.osgi.resource.Namespace; + +/** + * Identity Capability and Requirement Namespace. + * + *

      + * This class defines the names for the attributes and directives for this + * namespace. All unspecified capability attributes are of type {@code String} + * and are used as arbitrary matching attributes for the capability. The values + * associated with the specified directive and attribute keys are of type + * {@code String}, unless otherwise indicated. + * + *

      + * Each resource provides exactly one identity capability that + * can be used to identify the resource. + * + *

      + * The bundle wiring for the bundle revision provides exactly + * one identity capability. + * + *

      + * † A resource with no symbolic name must not provide an identity + * capability. + * + * @Immutable + * @author $Id: 7bc7a11c45b30538ffbb7572c4539f6160557684 $ + */ +public final class IdentityNamespace extends Namespace { + + /** + * Namespace name for identity capabilities and requirements. + * + *

      + * Also, the capability attribute used to specify the symbolic name of the + * resource. + */ + public static final String IDENTITY_NAMESPACE = "osgi.identity"; + + /** + * The capability directive identifying if the resource is a singleton. A + * {@code String} value of "true" indicates the resource is a + * singleton; any other value or {@code null} indicates the resource is not + * a singleton. + */ + public static final String CAPABILITY_SINGLETON_DIRECTIVE = "singleton"; + + /** + * The capability attribute identifying the {@code Version} of the resource + * if one is specified or {@code 0.0.0} if not specified. The value of this + * attribute must be of type {@code Version}. + */ + public static final String CAPABILITY_VERSION_ATTRIBUTE = "version"; + + /** + * The capability attribute identifying the resource type. If the resource + * has no type then the value {@link #TYPE_UNKNOWN unknown} must be used for + * the attribute. + * + * @see #TYPE_BUNDLE + * @see #TYPE_FRAGMENT + * @see #TYPE_UNKNOWN + */ + public static final String CAPABILITY_TYPE_ATTRIBUTE = "type"; + + /** + * The attribute value identifying the resource + * {@link #CAPABILITY_TYPE_ATTRIBUTE type} as an OSGi bundle. + * + * @see #CAPABILITY_TYPE_ATTRIBUTE + */ + public static final String TYPE_BUNDLE = "osgi.bundle"; + + /** + * The attribute value identifying the resource + * {@link #CAPABILITY_TYPE_ATTRIBUTE type} as an OSGi fragment. + * + * @see #CAPABILITY_TYPE_ATTRIBUTE + */ + public static final String TYPE_FRAGMENT = "osgi.fragment"; + + /** + * The attribute value identifying the resource + * {@link #CAPABILITY_TYPE_ATTRIBUTE type} as unknown. + * + * @see #CAPABILITY_TYPE_ATTRIBUTE + */ + public static final String TYPE_UNKNOWN = "unknown"; + + /** + * The capability attribute that contains a human readable copyright notice + * for the resource. See the {@code Bundle-Copyright} manifest header. + */ + public static final String CAPABILITY_COPYRIGHT_ATTRIBUTE = "copyright"; + + /** + * The capability attribute that contains a human readable description for + * the resource. See the {@code Bundle-Description} manifest header. + */ + public static final String CAPABILITY_DESCRIPTION_ATTRIBUTE = "description"; + + /** + * The capability attribute that contains the URL to documentation for the + * resource. See the {@code Bundle-DocURL} manifest header. + */ + public static final String CAPABILITY_DOCUMENTATION_ATTRIBUTE = "documentation"; + + /** + * The capability attribute that contains the URL to the license for the + * resource. See the {@code name} portion of the {@code Bundle-License} + * manifest header. + */ + public static final String CAPABILITY_LICENSE_ATTRIBUTE = "license"; + + /** + * The requirement directive that classifies the relationship with another + * resource. + * + * @see #CLASSIFIER_SOURCES + * @see #CLASSIFIER_JAVADOC + */ + public static final String REQUIREMENT_CLASSIFIER_DIRECTIVE = "classifier"; + + /** + * The attribute value identifying the resource + * {@link #REQUIREMENT_CLASSIFIER_DIRECTIVE classifier} as an archive + * containing the source code in the same directory layout as the resource. + * + * @see #REQUIREMENT_CLASSIFIER_DIRECTIVE + */ + + public static final String CLASSIFIER_SOURCES = "sources"; + /** + * The attribute value identifying the resource + * {@link #REQUIREMENT_CLASSIFIER_DIRECTIVE classifier} as an archive + * containing the javadoc in the same directory layout as the resource. + * + * @see #REQUIREMENT_CLASSIFIER_DIRECTIVE + */ + public static final String CLASSIFIER_JAVADOC = "javadoc"; + + private IdentityNamespace() { + // empty + } +} diff --git a/framework/src/main/java/org/osgi/framework/namespace/NativeNamespace.java b/framework/src/main/java/org/osgi/framework/namespace/NativeNamespace.java new file mode 100644 index 00000000000..fc914204c1c --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/namespace/NativeNamespace.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.namespace; + +import org.osgi.framework.Constants; +import org.osgi.resource.Namespace; + +/** + * Native Capability and Requirement Namespace. + * + *

      + * This class defines the names for the attributes and directives for this + * namespace. All unspecified capability attributes are of type {@code String} + * and are used as arbitrary matching attributes for the capability. The values + * associated with the specified directive and attribute keys are of type + * {@code String}, unless otherwise indicated. + * + * @Immutable + * @since 1.1 + * @author $Id$ + */ +public final class NativeNamespace extends Namespace { + + /** + * Namespace name for native capabilities and requirements. + */ + public static final String NATIVE_NAMESPACE = "osgi.native"; + + /** + * The capability attribute contains alias values of the + * {@link Constants#FRAMEWORK_OS_NAME org.osgi.framework.os.name} launching + * property value according to the + * OSGi + * Specification References. The value of this attribute must be of type + * {@code List}. + */ + public final static String CAPABILITY_OSNAME_ATTRIBUTE = NATIVE_NAMESPACE + ".osname"; + + /** + * The capability attribute contains a {@code Version} parsed from the + * {@link Constants#FRAMEWORK_OS_VERSION org.osgi.framework.os.version} + * launching property value. The value of this attribute must be of type + * {@code Version}. + */ + public final static String CAPABILITY_OSVERSION_ATTRIBUTE = NATIVE_NAMESPACE + ".osversion"; + + /** + * The capability attribute contains alias values of the + * {@link Constants#FRAMEWORK_PROCESSOR org.osgi.framework.processor} + * launching property value according to the + * OSGi + * Specification References. The value of this attribute must be of type + * {@code List}. + */ + public final static String CAPABILITY_PROCESSOR_ATTRIBUTE = NATIVE_NAMESPACE + ".processor"; + + /** + * The capability attribute contains the + * {@link Constants#FRAMEWORK_LANGUAGE org.osgi.framework.language} + * launching property value. The value of this attribute must be of type + * {@code String}. + */ + public final static String CAPABILITY_LANGUAGE_ATTRIBUTE = NATIVE_NAMESPACE + ".language"; + + private NativeNamespace() { + // empty + } +} diff --git a/framework/src/main/java/org/osgi/framework/namespace/PackageNamespace.java b/framework/src/main/java/org/osgi/framework/namespace/PackageNamespace.java new file mode 100644 index 00000000000..9f7114cac2b --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/namespace/PackageNamespace.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.namespace; + +import org.osgi.resource.Namespace; + +/** + * Package Capability and Requirement Namespace. + * + *

      + * A resource provides zero or more package capabilities (this is, exported + * packages) and requires zero or more package requirements (that is, imported + * packages). + * + *

      + * This class defines the names for the attributes and directives for this + * namespace. All unspecified capability attributes are of type {@code String} + * and are used as arbitrary matching attributes for the capability. The values + * associated with the specified directive and attribute keys are of type + * {@code String}, unless otherwise indicated. + * + *

      + * Unless otherwise noted, all directives specified on the + * {@code Export-Package} header are visible in the capability and all + * directives specified on the {@code Import-Package} and + * {@code DynamicImport-Package} headers are visible in the requirement. + * + *

        + *
      • The {@link Namespace#CAPABILITY_EFFECTIVE_DIRECTIVE effective} + * {@link Namespace#REQUIREMENT_EFFECTIVE_DIRECTIVE directives} must be ignored. + * This namespace is only effective at {@link Namespace#EFFECTIVE_RESOLVE + * resolve} time. An {@code effective} directive specified on the + * {@code Export-Package}, {@code Import-Package} or + * {@code DynamicImport-Package} headers must be ignored. An {@code effective} + * directive must not be present in a capability or requirement.
      • + *
      • The {@link Namespace#REQUIREMENT_CARDINALITY_DIRECTIVE cardinality} + * directive has limited applicability to this namespace. A {@code cardinality} + * directive specified on the {@code Import-Package} or + * {@code DynamicImport-Package} headers must be ignored. Only requirements with + * {@link Namespace#REQUIREMENT_RESOLUTION_DIRECTIVE resolution} set to + * {@link #RESOLUTION_DYNAMIC dynamic} and the package name contains a wildcard + * must have the {@code cardinality} directive set to + * {@link Namespace#CARDINALITY_MULTIPLE multiple}. Otherwise, a + * {@code cardinality} directive must not be present in a requirement.
      • + *
      + * + * @Immutable + * @author $Id: 5f241fb0804d477ab8f2d33f247d45e62caf72df $ + */ +public final class PackageNamespace extends AbstractWiringNamespace { + + /** + * Namespace name for package capabilities and requirements. + * + *

      + * Also, the capability attribute used to specify the name of the package. + */ + public static final String PACKAGE_NAMESPACE = "osgi.wiring.package"; + + /** + * The capability directive used to specify the comma separated list of + * classes which must be allowed to be exported. + */ + public final static String CAPABILITY_INCLUDE_DIRECTIVE = "include"; + + /** + * The capability directive used to specify the comma separated list of + * classes which must not be allowed to be exported. + */ + public final static String CAPABILITY_EXCLUDE_DIRECTIVE = "exclude"; + + /** + * The capability attribute contains the {@code Version} of the package if + * one is specified or {@code 0.0.0} if not specified. The value of this + * attribute must be of type {@code Version}. + */ + public final static String CAPABILITY_VERSION_ATTRIBUTE = "version"; + + /** + * The capability attribute contains the symbolic name of the resource + * providing the package. + */ + public final static String CAPABILITY_BUNDLE_SYMBOLICNAME_ATTRIBUTE = "bundle-symbolic-name"; + + /** + * The directive value identifying a dynamic requirement resolution type. A + * dynamic resolution type indicates that the requirement is resolved + * dynamically at runtime (such as a dynamically imported package) and the + * resource will be resolved without the requirement being resolved. + * + * @see Namespace#REQUIREMENT_RESOLUTION_DIRECTIVE + */ + public final static String RESOLUTION_DYNAMIC = "dynamic"; + + private PackageNamespace() { + // empty + } +} diff --git a/framework/src/main/java/org/osgi/framework/namespace/package-info.java b/framework/src/main/java/org/osgi/framework/namespace/package-info.java new file mode 100644 index 00000000000..e89f34a4fc6 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/namespace/package-info.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Namespace Package Version 1.1. + * + *

      + * Bundles should not need to import this package at runtime since all + * the types in this package just contain constants for capability and + * requirement namespaces specified by the OSGi Alliance. + * + * @author $Id$ + */ + +@Version("1.1") +package org.osgi.framework.namespace; + +import org.osgi.annotation.versioning.Version; + diff --git a/framework/src/main/java/org/osgi/framework/package-info.java b/framework/src/main/java/org/osgi/framework/package-info.java new file mode 100644 index 00000000000..818d4aed968 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/package-info.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Framework Package Version 1.9. + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. + *

      + * Example import for consumers using the API in this package: + *

      + * {@code Import-Package: org.osgi.framework; version="[1.9,2.0)"} + * + * @author $Id$ + */ + +@Version("1.9") +package org.osgi.framework; + +import org.osgi.annotation.versioning.Version; + diff --git a/framework/src/main/java/org/osgi/framework/startlevel/BundleStartLevel.java b/framework/src/main/java/org/osgi/framework/startlevel/BundleStartLevel.java new file mode 100644 index 00000000000..44e3c45bc03 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/startlevel/BundleStartLevel.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.startlevel; + +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleReference; + +/** + * Query and modify the start level information for a bundle. The start level + * object for a bundle can be obtained by calling {@link Bundle#adapt(Class) + * bundle.adapt(BundleStartLevel.class)} on the bundle. + * + *

      + * The bundle associated with this BundleStartLevel object can be obtained by + * calling {@link BundleReference#getBundle()}. + * + * @ThreadSafe + * @author $Id: 421ffd6e9c48cda1bcd28c62e9ace1c05852f112 $ + */ +@ProviderType +public interface BundleStartLevel extends BundleReference { + /** + * Return the assigned start level value for the bundle. + * + * @return The start level value of the bundle. + * @see #setStartLevel(int) + * @throws IllegalStateException If the bundle has been uninstalled. + */ + int getStartLevel(); + + /** + * Assign a start level value to the bundle. + * + *

      + * The bundle will be assigned the specified start level. The start level + * value assigned to the bundle will be persistently recorded by the + * Framework. + *

      + * If the new start level for the bundle is lower than or equal to the + * active start level of the Framework and the bundle's autostart setting + * indicates this bundle must be started, the Framework will start the + * bundle as described in the {@link Bundle#start(int)} method using the + * {@link Bundle#START_TRANSIENT} option. The + * {@link Bundle#START_ACTIVATION_POLICY} option must also be used if + * {@link #isActivationPolicyUsed()} returns {@code true}. The actual + * starting of the bundle must occur asynchronously. + *

      + * If the new start level for the bundle is higher than the active start + * level of the Framework, the Framework will stop the bundle as described + * in the {@link Bundle#stop(int)} method using the + * {@link Bundle#STOP_TRANSIENT} option. The actual stopping of the bundle + * must occur asynchronously. + * + * @param startlevel The new start level for the bundle. + * @throws IllegalArgumentException If the specified start level is less + * than or equal to zero, or if the bundle is the system bundle. + * @throws IllegalStateException If the bundle has been uninstalled. + * @throws SecurityException If the caller does not have + * {@code AdminPermission[bundle,EXECUTE]} and the Java runtime + * environment supports permissions. + */ + void setStartLevel(int startlevel); + + /** + * Returns whether the bundle's autostart setting indicates it must be + * started. + *

      + * The autostart setting of a bundle indicates whether the bundle is to be + * started when its start level is reached. + * + * @return {@code true} if the autostart setting of the bundle indicates it + * is to be started. {@code false} otherwise. + * @throws IllegalStateException If this bundle has been uninstalled. + * @see Bundle#START_TRANSIENT + */ + boolean isPersistentlyStarted(); + + /** + * Returns whether the bundle's autostart setting indicates that the + * activation policy declared in the bundle manifest must be used. + *

      + * The autostart setting of a bundle indicates whether the bundle's declared + * activation policy is to be used when the bundle is started. + * + * @return {@code true} if the bundle's autostart setting indicates the + * activation policy declared in the manifest must be used. + * {@code false} if the bundle must be eagerly activated. + * @throws IllegalStateException If the bundle has been uninstalled. + * @see Bundle#START_ACTIVATION_POLICY + */ + boolean isActivationPolicyUsed(); +} diff --git a/framework/src/main/java/org/osgi/framework/startlevel/FrameworkStartLevel.java b/framework/src/main/java/org/osgi/framework/startlevel/FrameworkStartLevel.java new file mode 100644 index 00000000000..632c94c7642 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/startlevel/FrameworkStartLevel.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) OSGi Alliance (2002, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.startlevel; + +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleReference; +import org.osgi.framework.FrameworkListener; + +/** + * Query and modify the start level information for the framework. The start + * level object for the framework can be obtained by calling + * {@link Bundle#adapt(Class) bundle.adapt(FrameworkStartLevel.class)} on the + * system bundle. Only the system bundle can be adapted to a FrameworkStartLevel + * object. + * + *

      + * The system bundle associated with this FrameworkStartLevel object can be + * obtained by calling {@link BundleReference#getBundle()}. + * + * @ThreadSafe + * @author $Id: de8d10036e85359428ca3e14bbe9b2c6d448fb93 $ + */ +@ProviderType +public interface FrameworkStartLevel extends BundleReference { + /** + * Return the active start level value of the Framework. + * + * If the Framework is in the process of changing the start level this + * method must return the active start level if this differs from the + * requested start level. + * + * @return The active start level value of the Framework. + */ + int getStartLevel(); + + /** + * Modify the active start level of the Framework and notify when complete. + * + *

      + * The Framework will move to the requested start level. This method will + * return immediately to the caller and the start level change will occur + * asynchronously on another thread. The specified {@code FrameworkListener} + * s are notified, in the order specified, when the start level change is + * complete. When the start level change completes normally, each specified + * {@code FrameworkListener} will be called with a Framework event of type + * {@code FrameworkEvent.STARTLEVEL_CHANGED}. If the start level change does + * not complete normally, each specified {@code FrameworkListener} will be + * called with a Framework event of type {@code FrameworkEvent.ERROR}. + * + *

      + * If the specified start level is higher than the active start level, the + * Framework will continue to increase the start level until the Framework + * has reached the specified start level. + * + * At each intermediate start level value on the way to and including the + * target start level, the Framework must: + *

        + *
      1. Change the active start level to the intermediate start level value.
      2. + *
      3. Start bundles at the intermediate start level whose autostart setting + * indicate they must be started. They are started as described in the + * {@link Bundle#start(int)} method using the {@link Bundle#START_TRANSIENT} + * option. The {@link Bundle#START_ACTIVATION_POLICY} option must also be + * used if {@link BundleStartLevel#isActivationPolicyUsed()} returns + * {@code true} for the bundle.
      4. + *
      + * When this process completes after the specified start level is reached, + * the Framework will fire a Framework event of type + * {@code FrameworkEvent.STARTLEVEL_CHANGED} to announce it has moved to the + * specified start level. + * + *

      + * If the specified start level is lower than the active start level, the + * Framework will continue to decrease the start level until the Framework + * has reached the specified start level. + * + * At each intermediate start level value on the way to and including the + * specified start level, the framework must: + *

        + *
      1. Stop bundles at the intermediate start level as described in the + * {@link Bundle#stop(int)} method using the {@link Bundle#STOP_TRANSIENT} + * option.
      2. + *
      3. Change the active start level to the intermediate start level value.
      4. + *
      + * When this process completes after the specified start level is reached, + * the Framework will fire a Framework event of type + * {@code FrameworkEvent.STARTLEVEL_CHANGED} to announce it has moved to the + * specified start level. + * + *

      + * If the specified start level is equal to the active start level, then no + * bundles are started or stopped, however, the Framework must fire a + * Framework event of type {@code FrameworkEvent.STARTLEVEL_CHANGED} to + * announce it has finished moving to the specified start level. This event + * may arrive before this method returns. + * + * @param startlevel The requested start level for the Framework. + * @param listeners Zero or more listeners to be notified when the start + * level change has been completed. The specified listeners do not + * need to be otherwise registered with the framework. If a specified + * listener is already registered with the framework, it will be + * notified twice. + * @throws IllegalArgumentException If the specified start level is less + * than or equal to zero. + * @throws SecurityException If the caller does not have + * {@code AdminPermission[System Bundle,STARTLEVEL]} and the Java + * runtime environment supports permissions. + */ + void setStartLevel(int startlevel, FrameworkListener... listeners); + + /** + * Return the initial start level value that is assigned to a Bundle when it + * is first installed. + * + * @return The initial start level value for Bundles. + * @see #setInitialBundleStartLevel(int) + */ + int getInitialBundleStartLevel(); + + /** + * Set the initial start level value that is assigned to a Bundle when it is + * first installed. + * + *

      + * The initial bundle start level will be set to the specified start level. + * The initial bundle start level value will be persistently recorded by the + * Framework. + * + *

      + * When a Bundle is installed via {@code BundleContext.installBundle}, it is + * assigned the initial bundle start level value. + * + *

      + * The default initial bundle start level value is 1 unless this method has + * been called to assign a different initial bundle start level value. + * + *

      + * This method does not change the start level values of installed bundles. + * + * @param startlevel The initial start level for newly installed bundles. + * @throws IllegalArgumentException If the specified start level is less + * than or equal to zero. + * @throws SecurityException If the caller does not have + * {@code AdminPermission[System Bundle,STARTLEVEL]} and the Java + * runtime environment supports permissions. + */ + void setInitialBundleStartLevel(int startlevel); +} diff --git a/framework/src/main/java/org/osgi/framework/startlevel/dto/BundleStartLevelDTO.java b/framework/src/main/java/org/osgi/framework/startlevel/dto/BundleStartLevelDTO.java new file mode 100644 index 00000000000..81430e24483 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/startlevel/dto/BundleStartLevelDTO.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.startlevel.dto; + +import org.osgi.dto.DTO; +import org.osgi.framework.startlevel.BundleStartLevel; + +/** + * Data Transfer Object for a BundleStartLevel. + * + *

      + * An installed Bundle can be adapted to provide a {@code BundleStartLevelDTO} + * for the Bundle. + * + * @author $Id$ + * @NotThreadSafe + */ +public class BundleStartLevelDTO extends DTO { + /** + * The id of the bundle associated with this start level. + * + * @see BundleStartLevel#getBundle() + */ + public long bundle; + + /** + * The assigned start level value for the bundle. + * + * @see BundleStartLevel#getStartLevel() + */ + public int startLevel; + + /** + * The bundle's autostart setting indicates that the activation policy + * declared in the bundle manifest must be used. + * + * @see BundleStartLevel#isActivationPolicyUsed() + */ + public boolean activationPolicyUsed; + + /** + * The bundle's autostart setting indicates it must be started. + * + * @see BundleStartLevel#isPersistentlyStarted() + */ + public boolean persistentlyStarted; +} diff --git a/framework/src/main/java/org/osgi/framework/startlevel/dto/FrameworkStartLevelDTO.java b/framework/src/main/java/org/osgi/framework/startlevel/dto/FrameworkStartLevelDTO.java new file mode 100644 index 00000000000..2d1de40ba2b --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/startlevel/dto/FrameworkStartLevelDTO.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.startlevel.dto; + +import org.osgi.dto.DTO; +import org.osgi.framework.startlevel.FrameworkStartLevel; + +/** + * Data Transfer Object for a FrameworkStartLevel. + * + *

      + * The System Bundle can be adapted to provide a {@code FrameworkStartLevelDTO} + * for the framework of the Bundle. + * + * @author $Id$ + * @NotThreadSafe + */ +public class FrameworkStartLevelDTO extends DTO { + /** + * The active start level value for the framework. + * + * @see FrameworkStartLevel#getStartLevel() + */ + public int startLevel; + + /** + * The initial start level value that is assigned to a bundle when it is + * first installed. + * + * @see FrameworkStartLevel#getInitialBundleStartLevel() + */ + public int initialBundleStartLevel; +} diff --git a/framework/src/main/java/org/osgi/framework/startlevel/dto/package-info.java b/framework/src/main/java/org/osgi/framework/startlevel/dto/package-info.java new file mode 100644 index 00000000000..62262c52e64 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/startlevel/dto/package-info.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * OSGi Data Transfer Object Framework Start Level Package Version 1.0. + * + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. This package has two types of + * users: the consumers that use the API in this package and the providers that + * implement the API in this package. + * + *

      + * Example import for consumers using the API in this package: + *

      + * {@code Import-Package: org.osgi.framework.startlevel.dto; version="[1.0,2.0)"} + *

      + * Example import for providers implementing the API in this package: + *

      + * {@code Import-Package: org.osgi.framework.startlevel.dto; version="[1.0,1.1)"} + * + * @author $Id$ + */ + +@Version("1.0") +package org.osgi.framework.startlevel.dto; + +import org.osgi.annotation.versioning.Version; + diff --git a/framework/src/main/java/org/osgi/framework/startlevel/package-info.java b/framework/src/main/java/org/osgi/framework/startlevel/package-info.java new file mode 100644 index 00000000000..27d775bbe53 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/startlevel/package-info.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Framework Start Level Package Version 1.0. + * + *

      + * The Framework Start Level package allows management agents to manage a start + * level assigned to each bundle and the active start level of the Framework. + * This package is a replacement for the now deprecated + * {@code org.osgi.service.startlevel} package. + * + *

      + * A start level is defined to be a state of execution in which the Framework + * exists. Start level values are defined as unsigned integers with 0 (zero) + * being the state where the Framework is not launched. Progressively higher + * integral values represent progressively higher start levels. For example, 2 + * is a higher start level than 1. + * + *

      + * {@code AdminPermission} is required to modify start level information. + * + *

      + * Start Level support in the Framework includes the ability to modify the + * active start level of the Framework and to assign a specific start level to a + * bundle. The beginning start level of a Framework is specified via the + * {@link org.osgi.framework.Constants#FRAMEWORK_BEGINNING_STARTLEVEL} framework + * property when configuring a framework. + * + *

      + * When the Framework is first started it must be at start level zero. In this + * state, no bundles are running. This is the initial state of the Framework + * before it is launched. When the Framework is launched, the Framework will + * enter start level one and all bundles which are assigned to start level one + * and whose autostart setting indicates the bundle should be started are + * started as described in the {@link org.osgi.framework.Bundle#start(int)} + * method. The Framework will continue to increase the start level, starting + * bundles at each start level, until the Framework has reached a beginning + * start level. At this point the Framework has completed starting bundles and + * will then fire a Framework event of type + * {@link org.osgi.framework.FrameworkEvent#STARTED} to announce it has + * completed its launch. + * + *

      + * Within a start level, bundles may be started in an order defined by the + * Framework implementation. This may be something like ascending + * {@link org.osgi.framework.Bundle#getBundleId()} order or an order based upon + * dependencies between bundles. A similar but reversed order may be used when + * stopping bundles within a start level. + * + *

      + * The Framework Start Level package can be used by management bundles to alter + * the active start level of the framework. + * + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. For example: + * + *

      + * Import-Package: org.osgi.framework.startlevel; version="[1.0,2.0)"
      + * 
      + * + * @author $Id$ + */ + +@Version("1.0") +package org.osgi.framework.startlevel; + +import org.osgi.annotation.versioning.Version; + diff --git a/framework/src/main/java/org/osgi/framework/wiring/BundleCapability.java b/framework/src/main/java/org/osgi/framework/wiring/BundleCapability.java new file mode 100644 index 00000000000..3d12c757234 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/wiring/BundleCapability.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2015). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.wiring; + +import java.util.Map; +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.namespace.AbstractWiringNamespace; +import org.osgi.resource.Capability; + +/** + * A capability that has been declared from a {@link BundleRevision bundle + * revision}. + * + * @ThreadSafe + * @author $Id: b4eb0df09a8354afdb46f013926f81f70f817f43 $ + */ +@ProviderType +public interface BundleCapability extends Capability { + + /** + * Returns the bundle revision declaring this capability. + * + * @return The bundle revision declaring this capability. + */ + BundleRevision getRevision(); + + /** + * Returns the namespace of this capability. + * + * @return The namespace of this capability. + */ + @Override + String getNamespace(); + + /** + * Returns the directives of this capability. + * + *

      + * All capability directives not specified by the + * {@link AbstractWiringNamespace wiring namespaces} have no specified + * semantics and are considered extra user defined information. + * + * @return An unmodifiable map of directive names to directive values for + * this capability, or an empty map if this capability has no + * directives. + */ + @Override + Map getDirectives(); + + /** + * Returns the attributes of this capability. + * + * @return An unmodifiable map of attribute names to attribute values for + * this capability, or an empty map if this capability has no + * attributes. + */ + @Override + Map getAttributes(); + + /** + * Returns the resource declaring this capability. + * + *

      + * This method returns the same value as {@link #getRevision()}. + * + * @return The resource declaring this capability. + * @since 1.1 + */ + @Override + BundleRevision getResource(); +} diff --git a/framework/src/main/java/org/osgi/framework/wiring/BundleRequirement.java b/framework/src/main/java/org/osgi/framework/wiring/BundleRequirement.java new file mode 100644 index 00000000000..ace284f51ae --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/wiring/BundleRequirement.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2015). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.wiring; + +import java.util.Map; +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.namespace.AbstractWiringNamespace; +import org.osgi.resource.Requirement; + +/** + * A requirement that has been declared from a {@link BundleRevision bundle + * revision}. + * + * @ThreadSafe + * @author $Id: f7e73311cfc384905cb65d9d24b12304291be2d8 $ + */ +@ProviderType +public interface BundleRequirement extends Requirement { + /** + * Returns the bundle revision declaring this requirement. + * + * @return The bundle revision declaring this requirement. + */ + BundleRevision getRevision(); + + /** + * Returns whether the specified capability matches this requirement. + * + * @param capability The capability to match to this requirement. + * @return {@code true} if the specified capability has the same + * {@link #getNamespace() namespace} as this requirement and the + * filter for this requirement matches the + * {@link BundleCapability#getAttributes() attributes of the + * specified capability}; {@code false} otherwise. + */ + boolean matches(BundleCapability capability); + + /** + * Returns the namespace of this requirement. + * + * @return The namespace of this requirement. + */ + @Override + String getNamespace(); + + /** + * Returns the directives of this requirement. + * + *

      + * All requirement directives not specified by the + * {@link AbstractWiringNamespace wiring namespaces} have no specified + * semantics and are considered extra user defined information. + * + * @return An unmodifiable map of directive names to directive values for + * this requirement, or an empty map if this requirement has no + * directives. + */ + @Override + Map getDirectives(); + + /** + * Returns the attributes of this requirement. + * + *

      + * Requirement attributes have no specified semantics and are considered + * extra user defined information. + * + * @return An unmodifiable map of attribute names to attribute values for + * this requirement, or an empty map if this requirement has no + * attributes. + */ + @Override + Map getAttributes(); + + /** + * Returns the resource declaring this requirement. + * + *

      + * This method returns the same value as {@link #getRevision()}. + * + * @return The resource declaring this requirement. This can be {@code null} + * if this requirement is synthesized. + * @since 1.1 + */ + @Override + BundleRevision getResource(); +} diff --git a/framework/src/main/java/org/osgi/framework/wiring/BundleRevision.java b/framework/src/main/java/org/osgi/framework/wiring/BundleRevision.java new file mode 100644 index 00000000000..93b650acf89 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/wiring/BundleRevision.java @@ -0,0 +1,307 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2018). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.wiring; + +import java.util.List; +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleReference; +import org.osgi.framework.Constants; +import org.osgi.framework.Version; +import org.osgi.framework.namespace.BundleNamespace; +import org.osgi.framework.namespace.HostNamespace; +import org.osgi.framework.namespace.PackageNamespace; +import org.osgi.resource.Capability; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; + +/** + * Bundle Revision. When a bundle is installed and each time a bundle is + * updated, a new bundle revision of the bundle is created. Since a bundle + * update can change the entries in a bundle, different bundle wirings for the + * same bundle can be associated with different bundle revisions. + * + *

      + * For a bundle that has not been uninstalled, the most recent bundle revision + * is defined to be the current bundle revision. A bundle in the UNINSTALLED + * state does not have a current revision. The current bundle revision for a + * bundle can be obtained by calling {@link Bundle#adapt(Class) bundle.adapt} + * (BundleRevision.class). Since a bundle in the UNINSTALLED state does not have + * a current revision, adapting such a bundle returns {@code null}. + * + *

      + * The framework defines namespaces for {@link PackageNamespace package}, + * {@link BundleNamespace bundle} and {@link HostNamespace host} capabilities + * and requirements. These namespaces are defined only to express wiring + * information by the framework. They must not be used in + * {@link Constants#PROVIDE_CAPABILITY Provide-Capability} and + * {@link Constants#REQUIRE_CAPABILITY Require-Capability} manifest headers. + * + * @ThreadSafe + * @author $Id: 0d0ae508d172eefe49c9c338e4d29ffd13500f0b $ + */ +@ProviderType +public interface BundleRevision extends BundleReference, Resource { + /** + * Returns the symbolic name for this bundle revision. + * + * @return The symbolic name for this bundle revision. + * @see Bundle#getSymbolicName() + */ + String getSymbolicName(); + + /** + * Returns the version for this bundle revision. + * + * @return The version for this bundle revision, or + * {@link Version#emptyVersion} if this bundle revision has no + * version information. + * @see Bundle#getVersion() + */ + Version getVersion(); + + /** + * Returns the capabilities declared by this bundle revision. + * + * @param namespace The namespace of the declared capabilities to return or + * {@code null} to return the declared capabilities from all + * namespaces. + * @return An unmodifiable list containing the declared + * {@link BundleCapability}s from the specified namespace. The + * returned list will be empty if this bundle revision declares no + * capabilities in the specified namespace. The list contains the + * declared capabilities in the order they are specified in the + * manifest. + */ + List getDeclaredCapabilities(String namespace); + + /** + * Returns the requirements declared by this bundle revision. + * + * @param namespace The namespace of the declared requirements to return or + * {@code null} to return the declared requirements from all + * namespaces. + * @return An unmodifiable list containing the declared + * {@link BundleRequirement}s from the specified namespace. The + * returned list will be empty if this bundle revision declares no + * requirements in the specified namespace. The list contains the + * declared requirements in the order they are specified in the + * manifest. + */ + List getDeclaredRequirements(String namespace); + + /** + * Namespace for package capabilities and requirements. + * + *

      + * The name of the package is stored in the capability attribute of the same + * name as this namespace (osgi.wiring.package). The other directives and + * attributes of the package, from the {@link Constants#EXPORT_PACKAGE + * Export-Package} manifest header, can be found in the capability's + * {@link BundleCapability#getDirectives() directives} and + * {@link BundleCapability#getAttributes() attributes}. The + * {@link Constants#VERSION_ATTRIBUTE version} capability attribute must + * contain the {@link Version} of the package if one is specified or + * {@link Version#emptyVersion} if not specified. The + * {@link Constants#BUNDLE_SYMBOLICNAME_ATTRIBUTE bundle-symbolic-name} + * capability attribute must contain the + * {@link BundleRevision#getSymbolicName() symbolic name} of the provider if + * one is specified. The {@link Constants#BUNDLE_VERSION_ATTRIBUTE + * bundle-version} capability attribute must contain the + * {@link BundleRevision#getVersion() version} of the provider if one is + * specified or {@link Version#emptyVersion} if not specified. + * + *

      + * The package capabilities provided by the system bundle, that is the + * bundle with id zero, must include the package specified by the + * {@link Constants#FRAMEWORK_SYSTEMPACKAGES} and + * {@link Constants#FRAMEWORK_SYSTEMPACKAGES_EXTRA} framework properties as + * well as any other package exported by the framework implementation. + * + *

      + * A bundle revision {@link BundleRevision#getDeclaredCapabilities(String) + * declares} zero or more package capabilities (this is, exported packages) + * and {@link BundleRevision#getDeclaredRequirements(String) declares} zero + * or more package requirements. + *

      + * A bundle wiring {@link BundleWiring#getCapabilities(String) provides} + * zero or more resolved package capabilities (that is, exported packages) + * and {@link BundleWiring#getRequiredWires(String) requires} zero or more + * resolved package requirements (that is, imported packages). The number of + * package wires required by a bundle wiring may change as the bundle wiring + * may dynamically import additional packages. + * + * @see PackageNamespace + */ + String PACKAGE_NAMESPACE = PackageNamespace.PACKAGE_NAMESPACE; + + /** + * Namespace for bundle capabilities and requirements. + * + *

      + * The bundle symbolic name of the bundle is stored in the capability + * attribute of the same name as this namespace (osgi.wiring.bundle). The + * other directives and attributes of the bundle, from the + * {@link Constants#BUNDLE_SYMBOLICNAME Bundle-SymbolicName} manifest + * header, can be found in the capability's + * {@link BundleCapability#getDirectives() directives} and + * {@link BundleCapability#getAttributes() attributes}. The + * {@link Constants#BUNDLE_VERSION_ATTRIBUTE bundle-version} capability + * attribute must contain the {@link Version} of the bundle from the + * {@link Constants#BUNDLE_VERSION Bundle-Version} manifest header if one is + * specified or {@link Version#emptyVersion} if not specified. + * + *

      + * A non-fragment revision + * {@link BundleRevision#getDeclaredCapabilities(String) declares} exactly + * one bundle capability (that is, the bundle can be + * required by another bundle). A fragment revision must not declare a + * bundle capability. + * + *

      + * A bundle wiring for a non-fragment revision + * {@link BundleWiring#getCapabilities(String) provides} exactly + * one bundle capability (that is, the bundle can be + * required by another bundle) and + * {@link BundleWiring#getRequiredWires(String) requires} zero or more + * bundle capabilities (that is, requires other bundles). + * + *

      + * † A bundle with no bundle symbolic name (that is, a bundle with + * {@link Constants#BUNDLE_MANIFESTVERSION Bundle-ManifestVersion} + * {@literal <} 2) must not provide a bundle capability. + * + * @see BundleNamespace + */ + String BUNDLE_NAMESPACE = BundleNamespace.BUNDLE_NAMESPACE; + + /** + * Namespace for host capabilities and requirements. + * + *

      + * The bundle symbolic name of the bundle is stored in the capability + * attribute of the same name as this namespace (osgi.wiring.host). The + * other directives and attributes of the bundle, from the + * {@link Constants#BUNDLE_SYMBOLICNAME Bundle-SymbolicName} manifest + * header, can be found in the capability's + * {@link BundleCapability#getDirectives() directives} and + * {@link BundleCapability#getAttributes() attributes}. The + * {@link Constants#BUNDLE_VERSION_ATTRIBUTE bundle-version} capability + * attribute must contain the {@link Version} of the bundle from the + * {@link Constants#BUNDLE_VERSION Bundle-Version} manifest header if one is + * specified or {@link Version#emptyVersion} if not specified. + * + *

      + * A non-fragment revision + * {@link BundleRevision#getDeclaredCapabilities(String) declares} zero or + * one host capability if the bundle + * {@link Constants#FRAGMENT_ATTACHMENT_DIRECTIVE allows fragments to be + * attached}. A fragment revision must + * {@link BundleRevision#getDeclaredRequirements(String) declare} exactly + * one host requirement. + * + *

      + * A bundle wiring for a non-fragment revision + * {@link BundleWiring#getCapabilities(String) provides} zero or + * one host capability if the bundle + * {@link Constants#FRAGMENT_ATTACHMENT_DIRECTIVE allows fragments to be + * attached}. A bundle wiring for a fragment revision + * {@link BundleWiring#getRequiredWires(String) requires} a host capability + * for each host to which it is attached. + * + *

      + * † A bundle with no bundle symbolic name (that is, a bundle with + * {@link Constants#BUNDLE_MANIFESTVERSION Bundle-ManifestVersion} + * {@literal <} 2) must not provide a host capability. + * + * @see HostNamespace + */ + String HOST_NAMESPACE = HostNamespace.HOST_NAMESPACE; + + /** + * Returns the special types of this bundle revision. The bundle revision + * type values are: + *

        + *
      • {@link #TYPE_FRAGMENT}
      • + *
      + * + * A bundle revision may be more than one type at a time. A type code is + * used to identify the bundle revision type for future extendability. + * + *

      + * If this bundle revision is not one or more of the defined types then 0 is + * returned. + * + * @return The special types of this bundle revision. The type values are + * ORed together. + */ + int getTypes(); + + /** + * Bundle revision type indicating the bundle revision is a fragment. + * + * @see #getTypes() + */ + int TYPE_FRAGMENT = 0x00000001; + + /** + * Returns the bundle wiring which is using this bundle revision. + * + * @return The bundle wiring which is using this bundle revision or + * {@code null} if no bundle wiring is using this bundle revision. + * @see BundleWiring#getRevision() + */ + BundleWiring getWiring(); + + /** + * Returns the capabilities declared by this resource. + * + *

      + * This method returns the same value as + * {@link #getDeclaredCapabilities(String)}. + * + * @param namespace The namespace of the declared capabilities to return or + * {@code null} to return the declared capabilities from all + * namespaces. + * @return An unmodifiable list containing the declared {@link Capability}s + * from the specified namespace. The returned list will be empty if + * this resource declares no capabilities in the specified + * namespace. + * @since 1.1 + */ + @Override + List getCapabilities(String namespace); + + /** + * Returns the requirements declared by this bundle resource. + * + *

      + * This method returns the same value as + * {@link #getDeclaredRequirements(String)}. + * + * @param namespace The namespace of the declared requirements to return or + * {@code null} to return the declared requirements from all + * namespaces. + * @return An unmodifiable list containing the declared {@link Requirement} + * s from the specified namespace. The returned list will be empty + * if this resource declares no requirements in the specified + * namespace. + * @since 1.1 + */ + @Override + List getRequirements(String namespace); +} diff --git a/framework/src/main/java/org/osgi/framework/wiring/BundleRevisions.java b/framework/src/main/java/org/osgi/framework/wiring/BundleRevisions.java new file mode 100644 index 00000000000..f9f7871cd9d --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/wiring/BundleRevisions.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) OSGi Alliance (2011, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.wiring; + +import java.util.List; +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleReference; + +/** + * The {@link BundleRevision bundle revisions} of a bundle. When a bundle is + * installed and each time a bundle is updated, a new bundle revision of the + * bundle is created. For a bundle that has not been uninstalled, the most + * recent bundle revision is defined to be the current bundle revision. A bundle + * in the UNINSTALLED state does not have a current revision. An in use bundle + * revision is associated with an {@link BundleWiring#isInUse() in use} + * {@link BundleWiring}. The current bundle revision, if there is one, and all + * in use bundle revisions are returned. + * + *

      + * The bundle revisions for a bundle can be obtained by calling + * {@link Bundle#adapt(Class) bundle.adapt}({@link BundleRevisions}.class). + * {@link #getRevisions()} on the bundle. + * + * @ThreadSafe + * @author $Id: 83e7bf03af2150a54af13a319325856e532cefde $ + */ +@ProviderType +public interface BundleRevisions extends BundleReference { + /** + * Return the bundle revisions for the {@link BundleReference#getBundle() + * referenced} bundle. + * + *

      + * The result is a list containing the current bundle revision, if there is + * one, and all in use bundle revisions. The list may also contain + * intermediate bundle revisions which are not in use. + * + *

      + * The list is ordered in reverse chronological order such that the first + * item is the most recent bundle revision and last item is the oldest + * bundle revision. + * + *

      + * Generally the list will have at least one bundle revision for the bundle: + * the current bundle revision. However, for an uninstalled bundle with no + * in use bundle revisions, the list may be empty. + * + * @return A list containing a snapshot of the {@link BundleRevision}s for + * the referenced bundle. + */ + List getRevisions(); +} diff --git a/framework/src/main/java/org/osgi/framework/wiring/BundleWire.java b/framework/src/main/java/org/osgi/framework/wiring/BundleWire.java new file mode 100644 index 00000000000..3eb7fdc9102 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/wiring/BundleWire.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) OSGi Alliance (2011, 2015). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.wiring; + +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.resource.Wire; + +/** + * A wire connecting a {@link BundleCapability} to a {@link BundleRequirement}. + * + * @ThreadSafe + * @author $Id: 9877055d8ebbe52b82ec16b876d4491a47a106ef $ + */ +@ProviderType +public interface BundleWire extends Wire { + /** + * Returns the {@link BundleCapability} for this wire. + * + * @return The {@link BundleCapability} for this wire. + */ + @Override + BundleCapability getCapability(); + + /** + * Return the {@link BundleRequirement} for this wire. + * + * @return The {@link BundleRequirement} for this wire. + */ + @Override + BundleRequirement getRequirement(); + + /** + * Returns the bundle wiring {@link BundleWiring#getProvidedWires(String) + * providing} the {@link #getCapability() capability}. + * + *

      + * The bundle revision referenced by the returned bundle wiring may differ + * from the bundle revision referenced by the {@link #getCapability() + * capability}. + * + * @return The bundle wiring providing the capability. If the bundle wiring + * providing the capability is not {@link BundleWiring#isInUse() in + * use}, {@code null} will be returned. + */ + BundleWiring getProviderWiring(); + + /** + * Returns the bundle wiring who + * {@link BundleWiring#getRequiredWires(String) requires} the + * {@link #getCapability() capability}. + * + *

      + * The bundle revision referenced by the returned bundle wiring may differ + * from the bundle revision referenced by the {@link #getRequirement() + * requirement}. + * + * @return The bundle wiring whose requirement is wired to the capability. + * If the bundle wiring requiring the capability is not + * {@link BundleWiring#isInUse() in use}, {@code null} will be + * returned. + */ + BundleWiring getRequirerWiring(); + + /** + * Returns the resource providing the {@link #getCapability() capability}. + * + *

      + * The returned resource may differ from the resource referenced by the + * {@link #getCapability() capability}. + * + *

      + * This method returns the same value as {@link #getProviderWiring()}. + * {@link BundleWiring#getRevision() getRevision()}. + * + * @return The resource providing the capability. + * @since 1.1 + */ + @Override + BundleRevision getProvider(); + + /** + * Returns the resource who {@link #getRequirement() requires} the + * {@link #getCapability() capability}. + * + *

      + * The returned resource may differ from the resource referenced by the + * {@link #getRequirement() requirement}. + * + *

      + * This method returns the same value as {@link #getRequirerWiring()}. + * {@link BundleWiring#getRevision() getRevision()}. + * + * @return The resource who requires the capability. + * @since 1.1 + */ + @Override + BundleRevision getRequirer(); +} diff --git a/framework/src/main/java/org/osgi/framework/wiring/BundleWiring.java b/framework/src/main/java/org/osgi/framework/wiring/BundleWiring.java new file mode 100644 index 00000000000..2b81de1585c --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/wiring/BundleWiring.java @@ -0,0 +1,509 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.wiring; + +import java.net.URL; +import java.util.Collection; +import java.util.List; + +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleReference; +import org.osgi.framework.namespace.IdentityNamespace; +import org.osgi.framework.namespace.PackageNamespace; +import org.osgi.resource.Capability; +import org.osgi.resource.Namespace; +import org.osgi.resource.Requirement; +import org.osgi.resource.Wire; +import org.osgi.resource.Wiring; + +/** + * A wiring for a bundle. Each time a bundle is resolved, a new bundle wiring + * for the bundle is created. A bundle wiring is associated with a bundle + * revision and represents the dependencies with other bundle wirings. + * + *

      + * The bundle wiring for a bundle is the {@link #isCurrent() current} bundle + * wiring if it is the most recent bundle wiring for the current bundle + * revision. A bundle wiring is {@link #isInUse() in use} if it is the current + * bundle wiring or if some other in use bundle wiring is dependent upon it. For + * example, another bundle wiring is wired to a capability provided by the + * bundle wiring. An in use bundle wiring for a non-fragment bundle has a class + * loader. All bundles with non-current, in use bundle wirings are considered + * removal pending. Once a bundle wiring is no longer in use, it is considered + * stale and is discarded by the framework. + * + *

      + * The current bundle wiring for a bundle can be obtained by calling + * {@link Bundle#adapt(Class) bundle.adapt}(BundleWiring.class). A bundle in the + * INSTALLED or UNINSTALLED state does not have a current wiring, adapting such + * a bundle returns {@code null}. + * + * @ThreadSafe + * @author $Id: ddad9df28bbd85405057e1f1237f404f83ac8758 $ + */ +@ProviderType +public interface BundleWiring extends BundleReference, Wiring { + /** + * Returns {@code true} if this bundle wiring is the current bundle wiring. + * The bundle wiring for a bundle is the current bundle wiring if it is the + * most recent bundle wiring for the current bundle revision. All bundles + * with non-current, in use bundle wirings are considered + * {@link FrameworkWiring#getRemovalPendingBundles() removal pending}. + * + * @return {@code true} if this bundle wiring is the current bundle wiring; + * {@code false} otherwise. + */ + boolean isCurrent(); + + /** + * Returns {@code true} if this bundle wiring is in use. A bundle wiring is + * in use if it is the {@link #isCurrent() current} wiring or if some other + * in use bundle wiring is dependent upon it. Once a bundle wiring is no + * longer in use, it is considered stale and is discarded by the framework. + * + * @return {@code true} if this bundle wiring is in use; {@code false} + * otherwise. + */ + boolean isInUse(); + + /** + * Returns the capabilities provided by this bundle wiring. + * + *

      + * Only capabilities considered by the resolver are returned. For example, + * capabilities with {@link Namespace#CAPABILITY_EFFECTIVE_DIRECTIVE + * effective} directive not equal to {@link Namespace#EFFECTIVE_RESOLVE + * resolve} are not returned. + * + *

      + * A capability may not be required by any bundle wiring and thus there may + * be no {@link #getProvidedWires(String) wires} for the capability. + * + *

      + * A bundle wiring for a non-fragment revision provides a subset of the + * declared capabilities from the bundle revision and all attached fragment + * revisions. Not all declared capabilities may be + * provided since some may be discarded. For example, if a package is + * declared to be both exported and imported, only one is selected and the + * other is discarded. + *

      + * A bundle wiring for a fragment revision with a symbolic name must provide + * exactly one {@link IdentityNamespace identity} capability. + *

      + * † The {@link IdentityNamespace identity} capability provided by + * attached fragment revisions must not be included in the capabilities of + * the host bundle wiring. + * + * @param namespace The namespace of the capabilities to return or + * {@code null} to return the capabilities from all namespaces. + * @return A list containing a snapshot of the {@link BundleCapability}s, or + * an empty list if this bundle wiring provides no capabilities in + * the specified namespace. If this bundle wiring is not + * {@link #isInUse() in use}, {@code null} will be returned. For a + * given namespace, the list contains the capabilities in the order + * the capabilities were specified in the manifests of the + * {@link #getRevision() bundle revision} and the attached + * fragments of this bundle wiring. There is no + * ordering defined between capabilities in different namespaces. + */ + List getCapabilities(String namespace); + + /** + * Returns the requirements of this bundle wiring. + *

      + * Only requirements considered by the resolver are returned. For example, + * requirements with {@link Namespace#REQUIREMENT_EFFECTIVE_DIRECTIVE + * effective} directive not equal to {@link Namespace#EFFECTIVE_RESOLVE + * resolve} are not returned. + *

      + * A bundle wiring for a non-fragment revision has a subset of the declared + * requirements from the bundle revision and all attached fragment + * revisions. Not all declared requirements may be present since some may be + * discarded. For example, if a package is declared to be both exported and + * imported, only one is selected and the other is discarded. + * + * @param namespace The namespace of the requirements to return or + * {@code null} to return the requirements from all namespaces. + * @return A list containing a snapshot of the {@link BundleRequirement}s, + * or an empty list if this bundle wiring uses no requirements in + * the specified namespace. If this bundle wiring is not + * {@link #isInUse() in use}, {@code null} will be returned. For a + * given namespace, the list contains the requirements in the order + * the requirements were specified in the manifests of the + * {@link #getRevision() bundle revision} and the attached fragments + * of this bundle wiring. There is no ordering defined between + * requirements in different namespaces. + */ + List getRequirements(String namespace); + + /** + * Returns the {@link BundleWire}s to the provided {@link BundleCapability + * capabilities} of this bundle wiring. + * + * @param namespace The namespace of the capabilities for which to return + * wires or {@code null} to return the wires for the capabilities in + * all namespaces. + * @return A list containing a snapshot of the {@link BundleWire}s for the + * {@link BundleCapability capabilities} of this bundle wiring, or + * an empty list if this bundle wiring has no capabilities in the + * specified namespace. If this bundle wiring is not + * {@link #isInUse() in use}, {@code null} will be returned. For a + * given namespace, the list contains the wires in the order the + * capabilities were specified in the manifests of the + * {@link #getRevision() bundle revision} and the attached fragments + * of this bundle wiring. There is no ordering defined between + * capabilities in different namespaces. + */ + List getProvidedWires(String namespace); + + /** + * Returns the {@link BundleWire}s to the {@link BundleRequirement + * requirements} in use by this bundle wiring. + * + *

      + * This method may return different results if this bundle wiring + * establishes additional wires to more requirements. For example, + * dynamically importing a package will establish a new wire to the + * dynamically imported package. + * + * @param namespace The namespace of the requirements for which to return + * wires or {@code null} to return the wires for the requirements in + * all namespaces. + * @return A list containing a snapshot of the {@link BundleWire}s for the + * {@link BundleRequirement requirements} of this bundle wiring, or + * an empty list if this bundle wiring has no requirements in the + * specified namespace. If this bundle wiring is not + * {@link #isInUse() in use}, {@code null} will be returned. For a + * given namespace, the list contains the wires in the order the + * requirements were specified in the manifests of the + * {@link #getRevision() bundle revision} and the attached fragments + * of this bundle wiring followed by dynamically established wires, + * if any, in the order they were established. There is no ordering + * defined between requirements in different namespaces. + */ + List getRequiredWires(String namespace); + + /** + * Returns the bundle revision for the bundle in this bundle wiring. Since a + * bundle update can change the entries in a bundle, different bundle + * wirings for the same bundle can have different bundle revisions. + * + *

      + * The bundle object {@link BundleReference#getBundle() referenced} by the + * returned {@code BundleRevision} may return different information than the + * returned {@code BundleRevision} since the returned {@code BundleRevision} + * may refer to an older revision of the bundle. + * + * @return The bundle revision for this bundle wiring. + * @see BundleRevision#getWiring() + */ + BundleRevision getRevision(); + + /** + * Returns the class loader for this bundle wiring. Since a bundle refresh + * creates a new bundle wiring for a bundle, different bundle wirings for + * the same bundle will have different class loaders. + * + * @return The class loader for this bundle wiring. If this bundle wiring is + * not {@link #isInUse() in use} or this bundle wiring is for a + * fragment revision, {@code null} will be returned. + * @throws SecurityException If the caller does not have the appropriate + * {@code RuntimePermission("getClassLoader")}, and the Java Runtime + * Environment supports permissions. + */ + ClassLoader getClassLoader(); + + /** + * Returns entries in this bundle wiring's {@link #getRevision() bundle + * revision} and its attached fragment revisions. This bundle wiring's class + * loader is not used to search for entries. Only the contents of this + * bundle wiring's bundle revision and its attached fragment revisions are + * searched for the specified entries. + * + *

      + * This method takes into account that the "contents" of this + * bundle wiring can have attached fragments. This "bundle space" + * is not a namespace with unique members; the same entry name can be + * present multiple times. This method therefore returns a list of URL + * objects. These URLs can come from different JARs but have the same path + * name. This method can either return only entries in the specified path or + * recurse into subdirectories returning entries in the directory tree + * beginning at the specified path. + * + *

      + * URLs for directory entries must have their path end with "/". + *

      + * Note: Jar and zip files are not required to include directory entries. + * URLs to directory entries will not be returned if the bundle contents do + * not contain directory entries. + * + * @param path The path name in which to look. The path is always relative + * to the root of this bundle wiring and may begin with + * "/". A path value of "/" indicates the root of + * this bundle wiring. + * @param filePattern The file name pattern for selecting entries in the + * specified path. The pattern is only matched against the last + * element of the entry path. If the entry is a directory then the + * trailing "/" is not used for pattern matching. Substring + * matching is supported, as specified in the Filter specification, + * using the wildcard character ("*"). If {@code null} is + * specified, this is equivalent to "*" and matches all + * files. + * @param options The options for listing resource names. See + * {@link #FINDENTRIES_RECURSE}. The method must ignore unrecognized + * options. + * @return An unmodifiable list of URL objects for each matching entry, or + * an empty list if no matching entry could be found, if this bundle + * wiring is for a fragment revision or if the caller does not have + * the appropriate {@code AdminPermission[bundle,RESOURCE]} and the + * Java Runtime Environment supports permissions. The list is + * ordered such that entries from the {@link #getRevision() bundle + * revision} are returned first followed by the entries from + * attached fragment revisions in attachment order. If this bundle + * wiring is not {@link #isInUse() in use}, {@code null} must be + * returned. + * @see Bundle#findEntries(String, String, boolean) + */ + List findEntries(String path, String filePattern, int options); + + /** + * The find entries operation must recurse into subdirectories. + * + *

      + * This bit may be set when calling + * {@link #findEntries(String, String, int)} to specify the result must + * include the matching entries from the specified path and its + * subdirectories. If this bit is not set, then the result must only include + * matching entries from the specified path. + * + * @see #findEntries(String, String, int) + */ + int FINDENTRIES_RECURSE = 0x00000001; + + /** + * Returns the names of resources visible to this bundle wiring's + * {@link #getClassLoader() class loader}. The returned names can be used to + * access the resources via this bundle wiring's class loader. + * + *

        + *
      • Only the resource names for resources in bundle wirings will be + * returned. The names of resources visible to a bundle wiring's parent + * class loader, such as the bootstrap class loader, must not be included in + * the result.
      • + *
      • Only established wires will be examined for resources. This method + * must not cause new wires for dynamic imports to be established.
      • + *
      + * + * @param path The path name in which to look. The path is always relative + * to the root of this bundle wiring's class loader and may begin + * with "/". A path value of "/" indicates the + * root of this bundle wiring's class loader. + * @param filePattern The file name pattern for selecting resource names in + * the specified path. The pattern is only matched against the last + * element of the resource path. If the resource is a directory then + * the trailing "/" is not used for pattern matching. + * Substring matching is supported, as specified in the Filter + * specification, using the wildcard character ("*"). If + * {@code null} is specified, this is equivalent to "*" and + * matches all files. + * @param options The options for listing resource names. See + * {@link #LISTRESOURCES_LOCAL} and {@link #LISTRESOURCES_RECURSE}. + * This method must ignore unrecognized options. + * @return An unmodifiable collection of resource names for each matching + * resource, or an empty collection if no matching resource could be + * found, if this bundle wiring is for a fragment revision or if the + * caller does not have the appropriate + * {@code AdminPermission[bundle,RESOURCE]} and the Java Runtime + * Environment supports permissions. The collection is unordered and + * must contain no duplicate resource names. If this bundle wiring + * is not {@link #isInUse() in use}, {@code null} must be returned. + */ + Collection listResources(String path, String filePattern, int options); + + /** + * The list resource names operation must recurse into subdirectories. + * + *

      + * This bit may be set when calling + * {@link #listResources(String, String, int)} to specify the result must + * include the names of matching resources from the specified path and its + * subdirectories. If this bit is not set, then the result must only include + * names of matching resources from the specified path. + * + * @see #listResources(String, String, int) + */ + int LISTRESOURCES_RECURSE = 0x00000001; + + /** + * The list resource names operation must limit the result to the names of + * matching resources contained in this bundle wiring's + * {@link #getRevision() bundle revision} and its attached fragment + * revisions. The result must not include resource names for resources in + * {@link PackageNamespace package} names which are + * {@link #getRequiredWires(String) imported} by this wiring. + * + *

      + * This bit may be set when calling + * {@link #listResources(String, String, int)} to specify the result must + * only include the names of matching resources contained in this bundle + * wiring's bundle revision and its attached fragment revisions. If this bit + * is not set, then the result must include the names of matching resources + * reachable from this bundle wiring's class loader which may include the + * names of matching resources contained in imported packages and required + * bundles. + * + * @see #listResources(String, String, int) + */ + int LISTRESOURCES_LOCAL = 0x00000002; + + /** + * Returns the capabilities provided by this wiring. + * + *

      + * Only capabilities considered by the resolver are returned. For example, + * capabilities with {@link Namespace#CAPABILITY_EFFECTIVE_DIRECTIVE + * effective} directive not equal to {@link Namespace#EFFECTIVE_RESOLVE + * resolve} are not returned. + * + *

      + * A capability may not be required by any wiring and thus there may be no + * {@link #getProvidedResourceWires(String) wires} for the capability. + * + *

      + * A wiring for a non-fragment resource provides a subset of the declared + * capabilities from the resource and all attached fragment + * resources. Not all declared capabilities may be + * provided since some may be discarded. For example, if a package is + * declared to be both exported and imported, only one is selected and the + * other is discarded. + *

      + * A wiring for a fragment resource with a symbolic name must provide + * exactly one {@code osgi.identity} capability. + *

      + * † The {@code osgi.identity} capability provided by attached + * fragment resource must not be included in the capabilities of the host + * wiring. + * + *

      + * This method returns the same value as {@link #getCapabilities(String)}. + * + * @param namespace The namespace of the capabilities to return or + * {@code null} to return the capabilities from all namespaces. + * @return A list containing a snapshot of the {@link Capability}s, or an + * empty list if this wiring provides no capabilities in the + * specified namespace. For a given namespace, the list contains the + * capabilities in the order the capabilities were specified in the + * manifests of the {@link #getResource() resource} and the attached + * fragment resources of this wiring. There is no + * ordering defined between capabilities in different namespaces. + * @since 1.1 + */ + @Override + List getResourceCapabilities(String namespace); + + /** + * Returns the requirements of this wiring. + * + *

      + * Only requirements considered by the resolver are returned. For example, + * requirements with {@link Namespace#REQUIREMENT_EFFECTIVE_DIRECTIVE + * effective} directive not equal to {@link Namespace#EFFECTIVE_RESOLVE + * resolve} are not returned. + * + *

      + * A wiring for a non-fragment resource has a subset of the declared + * requirements from the resource and all attached fragment resources. Not + * all declared requirements may be present since some may be discarded. For + * example, if a package is declared to be optionally imported and is not + * actually imported, the requirement must be discarded. + * + *

      + * This method returns the same value as {@link #getRequirements(String)}. + * + * @param namespace The namespace of the requirements to return or + * {@code null} to return the requirements from all namespaces. + * @return A list containing a snapshot of the {@link Requirement}s, or an + * empty list if this wiring uses no requirements in the specified + * namespace. For a given namespace, the list contains the + * requirements in the order the requirements were specified in the + * manifests of the {@link #getResource() resource} and the attached + * fragment resources of this wiring. There is no ordering defined + * between requirements in different namespaces. + * @since 1.1 + */ + @Override + List getResourceRequirements(String namespace); + + /** + * Returns the {@link Wire}s to the provided {@link Capability capabilities} + * of this wiring. + * + *

      + * This method returns the same value as {@link #getProvidedWires(String)}. + * + * @param namespace The namespace of the capabilities for which to return + * wires or {@code null} to return the wires for the capabilities in + * all namespaces. + * @return A list containing a snapshot of the {@link Wire}s for the + * {@link Capability capabilities} of this wiring, or an empty list + * if this wiring has no capabilities in the specified namespace. + * For a given namespace, the list contains the wires in the order + * the capabilities were specified in the manifests of the + * {@link #getResource() resource} and the attached fragment + * resources of this wiring. There is no ordering defined between + * capabilities in different namespaces. + * @since 1.1 + */ + @Override + List getProvidedResourceWires(String namespace); + + /** + * Returns the {@link Wire}s to the {@link Requirement requirements} in use + * by this wiring. + * + *

      + * This method returns the same value as {@link #getRequiredWires(String)}. + * + * @param namespace The namespace of the requirements for which to return + * wires or {@code null} to return the wires for the requirements in + * all namespaces. + * @return A list containing a snapshot of the {@link Wire}s for the + * {@link Requirement requirements} of this wiring, or an empty list + * if this wiring has no requirements in the specified namespace. + * For a given namespace, the list contains the wires in the order + * the requirements were specified in the manifests of the + * {@link #getResource() resource} and the attached fragment + * resources of this wiring. There is no ordering defined between + * requirements in different namespaces. + * @since 1.1 + */ + @Override + List getRequiredResourceWires(String namespace); + + /** + * Returns the resource associated with this wiring. + * + *

      + * This method returns the same value as {@link #getRevision()}. + * + * @return The resource associated with this wiring. + * @since 1.1 + */ + @Override + BundleRevision getResource(); +} diff --git a/framework/src/main/java/org/osgi/framework/wiring/FrameworkWiring.java b/framework/src/main/java/org/osgi/framework/wiring/FrameworkWiring.java new file mode 100644 index 00000000000..ee3c548ae4d --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/wiring/FrameworkWiring.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) OSGi Alliance (2001, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.wiring; + +import java.util.Collection; +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleReference; +import org.osgi.framework.FrameworkListener; +import org.osgi.resource.Requirement; + +/** + * Query and modify wiring information for the framework. The framework wiring + * object for the framework can be obtained by calling + * {@link Bundle#adapt(Class) bundle.adapt(FrameworkWiring.class)} on the system + * bundle. Only the system bundle can be adapted to a FrameworkWiring object. + * + *

      + * The system bundle associated with this FrameworkWiring object can be obtained + * by calling {@link BundleReference#getBundle()}. + * + * @ThreadSafe + * @author $Id: 1ab9112badc94f802ccda966f7b73584f2a5c412 $ + */ +@ProviderType +public interface FrameworkWiring extends BundleReference { + /** + * Refreshes the specified bundles. This forces the update (replacement) or + * removal of packages exported by the specified bundles. + * + *

      + * The technique by which the framework refreshes bundles may vary among + * different framework implementations. A permissible implementation is to + * stop and restart the framework. + * + *

      + * This method returns to the caller immediately and then performs the + * following steps on a separate thread: + * + *

        + *
      1. Compute the {@link #getDependencyClosure(Collection) dependency + * closure} of the specified bundles. If no bundles are specified, compute + * the dependency closure of the {@link #getRemovalPendingBundles() removal + * pending} bundles.
      2. + *
      3. Each bundle in the dependency closure that is in the {@code ACTIVE} + * state will be stopped as described in the {@code Bundle.stop} method.
      4. + *
      5. Each bundle in the dependency closure that is in the {@code RESOLVED} + * state is unresolved and thus moved to the {@code INSTALLED} state. The + * effect of this step is that bundles in the dependency closure are no + * longer {@code RESOLVED}.
      6. + *
      7. Each bundle in the dependency closure that is in the + * {@code UNINSTALLED} state is removed from the dependency closure and is + * now completely removed from the Framework.
      8. + *
      9. Each bundle in the dependency closure that was in the {@code ACTIVE} + * state prior to Step 2 is started as described in the {@code Bundle.start} + * method, causing all bundles required for the restart to be resolved. It + * is possible that, as a result of the previous steps, packages that were + * previously exported no longer are. Therefore, some bundles may be + * unresolvable until bundles satisfying the dependencies have been + * installed in the Framework.
      10. + *
      + * + *

      + * For any exceptions that are thrown during any of these steps, a framework + * event of type {@code FrameworkEvent.ERROR} is fired containing the + * exception. The source bundle for these events should be the specific + * bundle to which the exception is related. If no specific bundle can be + * associated with the exception then the System Bundle must be used as the + * source bundle for the event. All framework events fired by this method + * are also delivered to the specified {@code FrameworkListener}s in the + * order they are specified. + * + *

      + * When this process completes after the bundles are refreshed, the + * Framework will fire a Framework event of type + * {@code FrameworkEvent.PACKAGES_REFRESHED} to announce it has completed + * the bundle refresh. The specified {@code FrameworkListener}s are notified + * in the order specified. Each specified {@code FrameworkListener} will be + * called with a Framework event of type + * {@code FrameworkEvent.PACKAGES_REFRESHED}. + * + * @param bundles The bundles to be refreshed, or {@code null} to refresh + * the {@link #getRemovalPendingBundles() removal pending} bundles. + * @param listeners Zero or more listeners to be notified when the bundle + * refresh has been completed. The specified listeners do not need to + * be otherwise registered with the framework. If a specified + * listener is already registered with the framework, it will be + * notified twice. + * @throws IllegalArgumentException If the specified {@code Bundle}s were + * not created by the same framework instance associated with this + * FrameworkWiring. + * @throws SecurityException If the caller does not have + * {@code AdminPermission[System Bundle,RESOLVE]} and the Java + * runtime environment supports permissions. + */ + void refreshBundles(Collection bundles, FrameworkListener... listeners); + + /** + * Resolves the specified bundles. The Framework must attempt to resolve the + * specified bundles that are unresolved. Additional bundles that are not + * included in the specified bundles may be resolved as a result of calling + * this method. A permissible implementation of this method is to attempt to + * resolve all unresolved bundles installed in the framework. + * + *

      + * If no bundles are specified, then the Framework will attempt to resolve + * all unresolved bundles. This method must not cause any bundle to be + * refreshed, stopped, or started. This method will not return until the + * operation has completed. + * + * @param bundles The bundles to resolve or {@code null} to resolve all + * unresolved bundles installed in the Framework. + * @return {@code true} if all specified bundles are resolved; {@code false} + * otherwise. + * @throws IllegalArgumentException If the specified {@code Bundle}s were + * not created by the same framework instance associated with this + * FrameworkWiring. + * @throws SecurityException If the caller does not have + * {@code AdminPermission[System Bundle,RESOLVE]} and the Java + * runtime environment supports permissions. + */ + boolean resolveBundles(Collection bundles); + + /** + * Returns the bundles that have {@link BundleWiring#isCurrent() + * non-current}, {@link BundleWiring#isInUse() in use} bundle wirings. This + * is typically the bundles which have been updated or uninstalled since the + * last call to {@link #refreshBundles(Collection, FrameworkListener...)}. + * + * @return A collection containing a snapshot of the {@code Bundle}s which + * have non-current, in use {@code BundleWiring}s, or an empty + * collection if there are no such bundles. + */ + Collection getRemovalPendingBundles(); + + /** + * Returns the dependency closure for the specified bundles. + * + *

      + * A graph of bundles is computed starting with the specified bundles. The + * graph is expanded by adding any bundle that is either wired to a package + * that is currently exported by a bundle in the graph or requires a bundle + * in the graph. The graph is fully constructed when there is no bundle + * outside the graph that is wired to a bundle in the graph. The graph may + * contain {@code UNINSTALLED} bundles that are + * {@link #getRemovalPendingBundles() removal pending}. + * + * @param bundles The initial bundles for which to generate the dependency + * closure. + * @return A collection containing a snapshot of the dependency closure of + * the specified bundles, or an empty collection if there were no + * specified bundles. + * @throws IllegalArgumentException If the specified {@code Bundle}s were + * not created by the same framework instance associated with this + * FrameworkWiring. + */ + Collection getDependencyClosure(Collection bundles); + + /** + * Find bundle capabilities that match the given requirement. + * + *

      + * The returned collection contains {@link BundleCapability} objects where + * the revision must be the {@link BundleCapability#getRevision() declaring + * revision} of the capability and the revision must either be the current + * bundle revision or an {@link BundleWiring#isInUse() in use} bundle + * revision. + * + *

      + * Each returned capability must match the given requirement. This means + * that the filter in the requirement must match as well as any namespace + * specific directives. For example, the mandatory attributes for the + * osgi.wiring.package namespace. + * + *

      + * The returned collection has not been filtered to remove capabilities that + * are non-effective, substituted or for which the providing bundle does not + * have permission to provide. No resolve hooks are called to filter + * matching capabilities. + * + * @param requirement The requirement to find matching bundle capabilities. + * Must not be {@code null}. + * @return A collection of {@link BundleCapability} objects that match the + * specified requirement. + * @since 1.2 + */ + Collection findProviders(Requirement requirement); +} diff --git a/framework/src/main/java/org/osgi/framework/wiring/dto/BundleRevisionDTO.java b/framework/src/main/java/org/osgi/framework/wiring/dto/BundleRevisionDTO.java new file mode 100644 index 00000000000..43d74167b8b --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/wiring/dto/BundleRevisionDTO.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.wiring.dto; + +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.resource.dto.ResourceDTO; + +/** + * Data Transfer Object for a BundleRevision. + * + *

      + * An installed Bundle can be adapted to provide a {@code BundleRevisionDTO} for + * the current revision of the Bundle. {@code BundleRevisionDTO} objects for all + * in use revisions of the Bundle can be obtained by adapting the bundle to + * {@code BundleRevisionDTO[]}. + * + * @author $Id$ + * @NotThreadSafe + */ +public class BundleRevisionDTO extends ResourceDTO { + /** + * The symbolic name of the bundle revision. + * + * @see BundleRevision#getSymbolicName() + */ + public String symbolicName; + + /** + * The type of the bundle revision. + * + * @see BundleRevision#getTypes() + */ + public int type; + + /** + * The version of the bundle revision. + * + * @see BundleRevision#getVersion() + */ + public String version; + + /** + * The id of the bundle associated with the bundle revision. + * + * @see BundleRevision#getBundle() + */ + public long bundle; +} diff --git a/framework/src/main/java/org/osgi/framework/wiring/dto/BundleWireDTO.java b/framework/src/main/java/org/osgi/framework/wiring/dto/BundleWireDTO.java new file mode 100644 index 00000000000..ef923b41460 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/wiring/dto/BundleWireDTO.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.wiring.dto; + +import org.osgi.framework.wiring.BundleWire; +import org.osgi.resource.dto.WireDTO; +import org.osgi.resource.dto.WiringDTO; + +/** + * Data Transfer Object for a BundleWire. + * + *

      + * {@code BundleWireDTO}s are referenced {@link BundleWiringDTO.NodeDTO}s. + * + * @author $Id$ + * @NotThreadSafe + */ +public class BundleWireDTO extends WireDTO { + /** + * The identifier of the provider wiring for the bundle wire. + * + * @see WiringDTO#id + * @see BundleWire#getProviderWiring() + */ + public int providerWiring; + + /** + * The identifier of the requiring wiring for the bundle wire. + * + * @see WiringDTO#id + * @see BundleWire#getRequirerWiring() + */ + public int requirerWiring; +} diff --git a/framework/src/main/java/org/osgi/framework/wiring/dto/BundleWiringDTO.java b/framework/src/main/java/org/osgi/framework/wiring/dto/BundleWiringDTO.java new file mode 100644 index 00000000000..e23e80fcf8a --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/wiring/dto/BundleWiringDTO.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.wiring.dto; + +import java.util.Set; +import org.osgi.dto.DTO; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.resource.dto.WiringDTO; + +/** + * Data Transfer Object for a BundleWiring graph. + * + *

      + * An installed Bundle can be adapted to provide a {@code BundleWiringDTO} for + * the current wiring Bundle. {@code BundleWiringDTO} objects for all in use + * wirings of the Bundle can be obtained by adapting the bundle to + * {@code BundleWiringDTO[]}. + * + * @author $Id$ + * @NotThreadSafe + */ +public class BundleWiringDTO extends DTO { + /** + * The id of the bundle associated with the bundle wiring graph. + * + * @see BundleWiring#getBundle() + */ + public long bundle; + + /** + * The identifier of the root wiring node of the bundle wiring graph. + * + * @see WiringDTO#id + */ + public int root; + + /** + * The set of wiring nodes referenced by the wiring graph. + * + *

      + * All wiring nodes referenced by wiring node identifiers in the wiring + * graph are contained in this set. + */ + public Set nodes; + + /** + * The set of resources referenced by the wiring graph. + * + *

      + * All resources referenced by resource identifiers in the wiring graph are + * contained in this set. + */ + public Set resources; + + /** + * Data Transfer Object for a BundleWiring node. + * + *

      + * The {@link WiringDTO#providedWires providedWires} field must contain an + * array of {@link BundleWireDTO}s. The {@link WiringDTO#requiredWires + * requiredWires} field must contain an array of {@link BundleWireDTO}s. + * + * @NotThreadSafe + */ + public static class NodeDTO extends WiringDTO { + /** + * The bundle wiring's in use setting indicates that the bundle wiring + * is in use. + * + * @see BundleWiring#isInUse() + */ + public boolean inUse; + + /** + * The current state of the bundle wiring. The bundle wiring's current + * setting indicates that the bundle wiring is the current bundle wiring + * for the bundle. + * + * @see BundleWiring#isCurrent() + */ + public boolean current; + } +} diff --git a/framework/src/main/java/org/osgi/framework/wiring/dto/FrameworkWiringDTO.java b/framework/src/main/java/org/osgi/framework/wiring/dto/FrameworkWiringDTO.java new file mode 100755 index 00000000000..2019947fc93 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/wiring/dto/FrameworkWiringDTO.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.framework.wiring.dto; + +import java.util.Set; + +import org.osgi.dto.DTO; + +/** + * Data Transfer Object for the wiring graph of the framework. + *

      + * The system bundle can be adapted to provide the {@code FrameworkWiringDTO}. + * Only the system bundle can be adapted to a {@code FrameworkWiringDTO} object + * + * @author $Id: 1786218bfe09a4d46d9041d96d01b2a71612eb0d $ + * @NotThreadSafe + * @since 1.3 + */ +public class FrameworkWiringDTO extends DTO { + /** + * The set of wiring nodes referenced by the wiring graph of the framework. + *

      + * All wiring nodes referenced by wiring node identifiers in the wiring + * graph are contained in this set. + */ + public Set wirings; + /** + * The set of resources referenced by the wiring graph of the framework. + *

      + * All resources referenced by resource identifiers in the wiring graph are + * contained in this set. + */ + public Set resources; +} diff --git a/framework/src/main/java/org/osgi/framework/wiring/dto/package-info.java b/framework/src/main/java/org/osgi/framework/wiring/dto/package-info.java new file mode 100644 index 00000000000..47eb1eed801 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/wiring/dto/package-info.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * OSGi Data Transfer Object Framework Wiring Package Version 1.3. + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. This package has two types of + * users: the consumers that use the API in this package and the providers that + * implement the API in this package. + *

      + * Example import for consumers using the API in this package: + *

      + * {@code Import-Package: org.osgi.framework.wiring.dto; version="[1.3,2.0)"} + *

      + * Example import for providers implementing the API in this package: + *

      + * {@code Import-Package: org.osgi.framework.wiring.dto; version="[1.3,1.4)"} + * + * @author $Id$ + */ + +@Version("1.3") +package org.osgi.framework.wiring.dto; + +import org.osgi.annotation.versioning.Version; + diff --git a/framework/src/main/java/org/osgi/framework/wiring/package-info.java b/framework/src/main/java/org/osgi/framework/wiring/package-info.java new file mode 100644 index 00000000000..6fb7232ca6c --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/wiring/package-info.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Framework Wiring Package Version 1.2. + * + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. For example: + * + *

      + * Import-Package: org.osgi.framework.wiring; version="[1.2,2.0)"
      + * 
      + * + * @author $Id$ + */ + +@Version("1.2") +package org.osgi.framework.wiring; + +import org.osgi.annotation.versioning.Version; + diff --git a/framework/src/main/java/org/osgi/resource/Capability.java b/framework/src/main/java/org/osgi/resource/Capability.java new file mode 100644 index 00000000000..44eff0a754c --- /dev/null +++ b/framework/src/main/java/org/osgi/resource/Capability.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) OSGi Alliance (2011, 2015). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.resource; + +import java.util.Map; +import org.osgi.annotation.versioning.ConsumerType; + +/** + * A capability that has been declared from a {@link Resource}. + * + *

      + * Instances of this type must be effectively immutable. That is, for a + * given instance of this interface, the methods defined by this interface must + * always return the same result. + * + * @ThreadSafe + * @author $Id: 4c1f7f4309b23e7cf345c784ed096a2d9bd80467 $ + */ +@ConsumerType +public interface Capability { + + /** + * Returns the namespace of this capability. + * + * @return The namespace of this capability. + */ + String getNamespace(); + + /** + * Returns the directives of this capability. + * + * @return An unmodifiable map of directive names to directive values for + * this capability, or an empty map if this capability has no + * directives. + */ + Map getDirectives(); + + /** + * Returns the attributes of this capability. + * + * @return An unmodifiable map of attribute names to attribute values for + * this capability, or an empty map if this capability has no + * attributes. + */ + Map getAttributes(); + + /** + * Returns the resource declaring this capability. + * + * @return The resource declaring this capability. + */ + Resource getResource(); + + /** + * Compares this {@code Capability} to another {@code Capability}. + * + *

      + * This {@code Capability} is equal to another {@code Capability} if they + * have the same namespace, directives and attributes and are declared by + * the same resource. + * + * @param obj The object to compare against this {@code Capability}. + * @return {@code true} if this {@code Capability} is equal to the other + * object; {@code false} otherwise. + */ + @Override + boolean equals(Object obj); + + /** + * Returns the hashCode of this {@code Capability}. + * + * @return The hashCode of this {@code Capability}. + */ + @Override + int hashCode(); +} diff --git a/framework/src/main/java/org/osgi/resource/Namespace.java b/framework/src/main/java/org/osgi/resource/Namespace.java new file mode 100644 index 00000000000..a8223b1920d --- /dev/null +++ b/framework/src/main/java/org/osgi/resource/Namespace.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.resource; + +import org.osgi.annotation.versioning.ConsumerType; + +/** + * Capability and Requirement Namespaces base class. + * + *

      + * This class is the common class shared by all OSGi defined namespaces. It + * defines the names for the common attributes and directives for the OSGi + * specified namespaces. + * + *

      + * The OSGi Alliance reserves the right to extend the set of directives and + * attributes which have specified semantics for all of the specified + * namespaces. + * + *

      + * The values associated with these keys are of type {@code String}, unless + * otherwise indicated. + * + * @Immutable + * @author $Id: 95a67250528646012b39e8e5a92775fbb635a8c0 $ + */ +@ConsumerType +public abstract class Namespace { + + /** + * The capability directive used to specify the comma separated list of + * package names used by a capability. + */ + public final static String CAPABILITY_USES_DIRECTIVE = "uses"; + + /** + * The capability directive used to specify the effective time for the + * capability. The default value is {@link #EFFECTIVE_RESOLVE resolve}. + * + * @see #EFFECTIVE_RESOLVE resolve + * @see #EFFECTIVE_ACTIVE active + */ + public final static String CAPABILITY_EFFECTIVE_DIRECTIVE = "effective"; + + /** + * The requirement directive used to specify a capability filter. This + * filter is used to match against a capability's attributes. + */ + public final static String REQUIREMENT_FILTER_DIRECTIVE = "filter"; + + /** + * The requirement directive used to specify the resolution type for a + * requirement. The default value is {@link #RESOLUTION_MANDATORY mandatory} + * . + * + * @see #RESOLUTION_MANDATORY mandatory + * @see #RESOLUTION_OPTIONAL optional + */ + public final static String REQUIREMENT_RESOLUTION_DIRECTIVE = "resolution"; + + /** + * The directive value identifying a mandatory requirement resolution type. + * A mandatory resolution type indicates that the requirement must be + * resolved when the resource is resolved. If such a requirement cannot be + * resolved, the resource fails to resolve. + * + * @see #REQUIREMENT_RESOLUTION_DIRECTIVE + */ + public final static String RESOLUTION_MANDATORY = "mandatory"; + + /** + * The directive value identifying an optional requirement resolution type. + * An optional resolution type indicates that the requirement is optional + * and the resource may be resolved without the requirement being resolved. + * + * @see #REQUIREMENT_RESOLUTION_DIRECTIVE + */ + public final static String RESOLUTION_OPTIONAL = "optional"; + + /** + * The requirement directive used to specify the effective time for the + * requirement. The default value is {@link #EFFECTIVE_RESOLVE resolve}. + * + * @see #EFFECTIVE_RESOLVE resolve + * @see #EFFECTIVE_ACTIVE active + */ + public final static String REQUIREMENT_EFFECTIVE_DIRECTIVE = "effective"; + + /** + * The directive value identifying a {@link #CAPABILITY_EFFECTIVE_DIRECTIVE + * capability} or {@link #REQUIREMENT_EFFECTIVE_DIRECTIVE requirement} that + * is effective at resolve time. Capabilities and requirements with an + * effective time of resolve are the only capabilities which are processed + * while resolving a resource. + * + * @see #REQUIREMENT_EFFECTIVE_DIRECTIVE + * @see #CAPABILITY_EFFECTIVE_DIRECTIVE + */ + public final static String EFFECTIVE_RESOLVE = "resolve"; + + /** + * The directive value identifying a {@link #CAPABILITY_EFFECTIVE_DIRECTIVE + * capability} or {@link #REQUIREMENT_EFFECTIVE_DIRECTIVE requirement} that + * is effective at active time. Capabilities and requirements with an + * effective time of active are ignored while resolving a resource. + * + * @see #REQUIREMENT_EFFECTIVE_DIRECTIVE + * @see #CAPABILITY_EFFECTIVE_DIRECTIVE + */ + public final static String EFFECTIVE_ACTIVE = "active"; + + /** + * The requirement directive used to specify the cardinality for a + * requirement. The default value is {@link #CARDINALITY_SINGLE single}. + * + * @see #CARDINALITY_MULTIPLE multiple + * @see #CARDINALITY_SINGLE single + */ + public final static String REQUIREMENT_CARDINALITY_DIRECTIVE = "cardinality"; + + /** + * The directive value identifying a multiple + * {@link #REQUIREMENT_CARDINALITY_DIRECTIVE cardinality} type. + * + * @see #REQUIREMENT_CARDINALITY_DIRECTIVE + */ + public final static String CARDINALITY_MULTIPLE = "multiple"; + + /** + * The directive value identifying a + * {@link #REQUIREMENT_CARDINALITY_DIRECTIVE cardinality} type of single. + * + * @see #REQUIREMENT_CARDINALITY_DIRECTIVE + */ + public final static String CARDINALITY_SINGLE = "single"; + + /** + * Protected constructor for Namespace sub-types. + */ + protected Namespace() { + // empty + } +} diff --git a/framework/src/main/java/org/osgi/resource/Requirement.java b/framework/src/main/java/org/osgi/resource/Requirement.java new file mode 100644 index 00000000000..01b17971b6d --- /dev/null +++ b/framework/src/main/java/org/osgi/resource/Requirement.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) OSGi Alliance (2011, 2015). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.resource; + +import java.util.Map; +import org.osgi.annotation.versioning.ConsumerType; + +/** + * A requirement that has been declared from a {@link Resource} . + * + *

      + * Instances of this type must be effectively immutable. That is, for a + * given instance of this interface, the methods defined by this interface must + * always return the same result. + * + * @ThreadSafe + * @author $Id: 210e427134eaf0404c4f374f619af972fb570572 $ + */ +@ConsumerType +public interface Requirement { + /** + * Returns the namespace of this requirement. + * + * @return The namespace of this requirement. + */ + String getNamespace(); + + /** + * Returns the directives of this requirement. + * + * @return An unmodifiable map of directive names to directive values for + * this requirement, or an empty map if this requirement has no + * directives. + */ + Map getDirectives(); + + /** + * Returns the attributes of this requirement. + * + *

      + * Requirement attributes have no specified semantics and are considered + * extra user defined information. + * + * @return An unmodifiable map of attribute names to attribute values for + * this requirement, or an empty map if this requirement has no + * attributes. + */ + Map getAttributes(); + + /** + * Returns the resource declaring this requirement. + * + * @return The resource declaring this requirement. This can be {@code null} + * if this requirement is synthesized. + */ + Resource getResource(); + + /** + * Compares this {@code Requirement} to another {@code Requirement}. + * + *

      + * This {@code Requirement} is equal to another {@code Requirement} if they + * have the same namespace, directives and attributes and are declared by + * the same resource. + * + * @param obj The object to compare against this {@code Requirement}. + * @return {@code true} if this {@code Requirement} is equal to the other + * object; {@code false} otherwise. + */ + @Override + boolean equals(Object obj); + + /** + * Returns the hashCode of this {@code Requirement}. + * + * @return The hashCode of this {@code Requirement}. + */ + @Override + int hashCode(); +} diff --git a/framework/src/main/java/org/osgi/resource/Resource.java b/framework/src/main/java/org/osgi/resource/Resource.java new file mode 100644 index 00000000000..f76f5873f57 --- /dev/null +++ b/framework/src/main/java/org/osgi/resource/Resource.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) OSGi Alliance (2011, 2015). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.resource; + +import java.util.List; +import org.osgi.annotation.versioning.ConsumerType; + +/** + * A resource is the representation of a uniquely identified and typed data. A + * resource declares requirements that need to be satisfied by capabilities + * before it can provide its capabilities. + * + *

      + * Instances of this type must be effectively immutable. That is, for a + * given instance of this interface, the methods defined by this interface must + * always return the same result. + * + * @ThreadSafe + * @author $Id: 6f13a1b536b116eebb255285b958d889db733939 $ + */ +@ConsumerType +public interface Resource { + /** + * Returns the capabilities declared by this resource. + * + * @param namespace The namespace of the declared capabilities to return or + * {@code null} to return the declared capabilities from all + * namespaces. + * @return An unmodifiable list containing the declared {@link Capability}s + * from the specified namespace. The returned list will be empty if + * this resource declares no capabilities in the specified + * namespace. + */ + List getCapabilities(String namespace); + + /** + * Returns the requirements declared by this bundle resource. + * + * @param namespace The namespace of the declared requirements to return or + * {@code null} to return the declared requirements from all + * namespaces. + * @return An unmodifiable list containing the declared {@link Requirement} + * s from the specified namespace. The returned list will be empty + * if this resource declares no requirements in the specified + * namespace. + */ + List getRequirements(String namespace); + + /** + * Compares this {@code Resource} to another {@code Resource}. + * + *

      + * This {@code Resource} is equal to another {@code Resource} if both have + * the same content and come from the same location. Location may be defined + * as the bundle location if the resource is an installed bundle or the + * repository location if the resource is in a repository. + * + * @param obj The object to compare against this {@code Resource}. + * @return {@code true} if this {@code Resource} is equal to the other + * object; {@code false} otherwise. + */ + @Override + boolean equals(Object obj); + + /** + * Returns the hashCode of this {@code Resource}. + * + * @return The hashCode of this {@code Resource}. + */ + @Override + int hashCode(); +} diff --git a/framework/src/main/java/org/osgi/resource/Wire.java b/framework/src/main/java/org/osgi/resource/Wire.java new file mode 100644 index 00000000000..f0466d0dfd8 --- /dev/null +++ b/framework/src/main/java/org/osgi/resource/Wire.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) OSGi Alliance (2011, 2015). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.resource; + +import org.osgi.annotation.versioning.ConsumerType; + +/** + * A wire connecting a {@link Capability} to a {@link Requirement}. + * + *

      + * Instances of this type must be effectively immutable. That is, for a + * given instance of this interface, the methods defined by this interface must + * always return the same result. + * + * @ThreadSafe + * @author $Id: a0d1c39e16788708ffab9bb00707554f97f09998 $ + */ +@ConsumerType +public interface Wire { + /** + * Returns the {@link Capability} for this wire. + * + * @return The {@link Capability} for this wire. + */ + Capability getCapability(); + + /** + * Returns the {@link Requirement} for this wire. + * + * @return The {@link Requirement} for this wire. + */ + Requirement getRequirement(); + + /** + * Returns the resource providing the {@link #getCapability() capability}. + * + *

      + * The returned resource may differ from the resource referenced by the + * {@link #getCapability() capability}. + * + * @return The resource providing the capability. + */ + Resource getProvider(); + + /** + * Returns the resource who {@link #getRequirement() requires} the + * {@link #getCapability() capability}. + * + *

      + * The returned resource may differ from the resource referenced by the + * {@link #getRequirement() requirement}. + * + * @return The resource who requires the capability. + */ + Resource getRequirer(); + + /** + * Compares this {@code Wire} to another {@code Wire}. + * + *

      + * This {@code Wire} is equal to another {@code Wire} if they have the same + * capability, requirement, provider and requirer. + * + * @param obj The object to compare against this {@code Wire}. + * @return {@code true} if this {@code Wire} is equal to the other object; + * {@code false} otherwise. + */ + @Override + boolean equals(Object obj); + + /** + * Returns the hashCode of this {@code Wire}. + * + * @return The hashCode of this {@code Wire}. + */ + @Override + int hashCode(); +} diff --git a/framework/src/main/java/org/osgi/resource/Wiring.java b/framework/src/main/java/org/osgi/resource/Wiring.java new file mode 100644 index 00000000000..8f3de79f3f6 --- /dev/null +++ b/framework/src/main/java/org/osgi/resource/Wiring.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) OSGi Alliance (2011, 2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.resource; + +import java.util.List; + +import org.osgi.annotation.versioning.ConsumerType; + +/** + * A wiring for a resource. A wiring is associated with a resource and + * represents the dependencies with other wirings. + * + *

      + * Instances of this type must be effectively immutable. That is, for a + * given instance of this interface, the methods defined by this interface must + * always return the same result. + * + * @ThreadSafe + * @author $Id: 7cb1384266be355d3e727b55e789d36a88299cf6 $ + */ +@ConsumerType +public interface Wiring { + /** + * Returns the capabilities provided by this wiring. + * + *

      + * Only capabilities considered by the resolver are returned. For example, + * capabilities with {@link Namespace#CAPABILITY_EFFECTIVE_DIRECTIVE + * effective} directive not equal to {@link Namespace#EFFECTIVE_RESOLVE + * resolve} are not returned. + * + *

      + * A capability may not be required by any wiring and thus there may be no + * {@link #getProvidedResourceWires(String) wires} for the capability. + * + *

      + * A wiring for a non-fragment resource provides a subset of the declared + * capabilities from the resource and all attached fragment + * resources. Not all declared capabilities may be + * provided since some may be discarded. For example, if a package is + * declared to be both exported and imported, only one is selected and the + * other is discarded. + *

      + * A wiring for a fragment resource with a symbolic name must provide + * exactly one {@code osgi.identity} capability. + *

      + * † The {@code osgi.identity} capability provided by attached + * fragment resource must not be included in the capabilities of the host + * wiring. + * + * @param namespace The namespace of the capabilities to return or + * {@code null} to return the capabilities from all namespaces. + * @return A list containing a snapshot of the {@link Capability}s, or an + * empty list if this wiring provides no capabilities in the + * specified namespace. For a given namespace, the list contains the + * capabilities in the order the capabilities were specified in the + * manifests of the {@link #getResource() resource} and the attached + * fragment resources of this wiring. There is no + * ordering defined between capabilities in different namespaces. + */ + List getResourceCapabilities(String namespace); + + /** + * Returns the requirements of this wiring. + *

      + * Only requirements considered by the resolver are returned. For example, + * requirements with {@link Namespace#REQUIREMENT_EFFECTIVE_DIRECTIVE + * effective} directive not equal to {@link Namespace#EFFECTIVE_RESOLVE + * resolve} are not returned. + *

      + * A wiring for a non-fragment resource has a subset of the declared + * requirements from the resource and all attached fragment resources. Not + * all declared requirements may be present since some may be discarded. For + * example, if a package is declared to be both exported and imported, only + * one is selected and the other is discarded. + * + * @param namespace The namespace of the requirements to return or + * {@code null} to return the requirements from all namespaces. + * @return A list containing a snapshot of the {@link Requirement}s, or an + * empty list if this wiring uses no requirements in the specified + * namespace. For a given namespace, the list contains the + * requirements in the order the requirements were specified in the + * manifests of the {@link #getResource() resource} and the attached + * fragment resources of this wiring. There is no ordering defined + * between requirements in different namespaces. + */ + List getResourceRequirements(String namespace); + + /** + * Returns the {@link Wire}s to the provided {@link Capability capabilities} + * of this wiring. + * + * @param namespace The namespace of the capabilities for which to return + * wires or {@code null} to return the wires for the capabilities in + * all namespaces. + * @return A list containing a snapshot of the {@link Wire}s for the + * {@link Capability capabilities} of this wiring, or an empty list + * if this wiring has no capabilities in the specified namespace. + * For a given namespace, the list contains the wires in the order + * the capabilities were specified in the manifests of the + * {@link #getResource() resource} and the attached fragment + * resources of this wiring. There is no ordering defined between + * capabilities in different namespaces. + */ + List getProvidedResourceWires(String namespace); + + /** + * Returns the {@link Wire}s to the {@link Requirement requirements} in use + * by this wiring. + * + * @param namespace The namespace of the requirements for which to return + * wires or {@code null} to return the wires for the requirements in + * all namespaces. + * @return A list containing a snapshot of the {@link Wire}s for the + * {@link Requirement requirements} of this wiring, or an empty list + * if this wiring has no requirements in the specified namespace. + * For a given namespace, the list contains the wires in the order + * the requirements were specified in the manifests of the + * {@link #getResource() resource} and the attached fragment + * resources of this wiring. There is no ordering defined between + * requirements in different namespaces. + */ + List getRequiredResourceWires(String namespace); + + /** + * Returns the resource associated with this wiring. + * + * @return The resource associated with this wiring. + */ + Resource getResource(); +} diff --git a/framework/src/main/java/org/osgi/resource/dto/CapabilityDTO.java b/framework/src/main/java/org/osgi/resource/dto/CapabilityDTO.java new file mode 100644 index 00000000000..1f7fad1bf59 --- /dev/null +++ b/framework/src/main/java/org/osgi/resource/dto/CapabilityDTO.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.resource.dto; + +import java.util.Map; +import org.osgi.dto.DTO; +import org.osgi.resource.Capability; + +/** + * Data Transfer Object for a Capability. + * + * @author $Id$ + * @NotThreadSafe + */ +public class CapabilityDTO extends DTO { + /** + * The unique identifier of the capability. + * + *

      + * This identifier is transiently assigned and may vary across restarts. + */ + public int id; + + /** + * The namespace for the capability. + * + * @see Capability#getNamespace() + */ + public String namespace; + + /** + * The directives for the capability. + * + * @see Capability#getDirectives() + */ + public Map directives; + + /** + * The attributes for the capability. + * + *

      + * The value type must be a numerical type, Boolean, String, DTO or an array + * of any of the former. + * + * @see Capability#getAttributes() + */ + public Map attributes; + + /** + * The identifier of the resource declaring the capability. + * + * @see ResourceDTO#id + * @see Capability#getResource() + */ + public int resource; +} diff --git a/framework/src/main/java/org/osgi/resource/dto/CapabilityRefDTO.java b/framework/src/main/java/org/osgi/resource/dto/CapabilityRefDTO.java new file mode 100644 index 00000000000..81d5b85fdd9 --- /dev/null +++ b/framework/src/main/java/org/osgi/resource/dto/CapabilityRefDTO.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) OSGi Alliance (2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.resource.dto; + +import org.osgi.dto.DTO; + +/** + * Data Transfer Object for a reference to a Capability. + * + * @author $Id$ + * @NotThreadSafe + */ +public class CapabilityRefDTO extends DTO { + /** + * The identifier of the capability in the resource. + * + * @see CapabilityDTO#id + */ + public int capability; + + /** + * The identifier of the resource declaring the capability. + * + * @see ResourceDTO#id + */ + public int resource; +} diff --git a/framework/src/main/java/org/osgi/resource/dto/RequirementDTO.java b/framework/src/main/java/org/osgi/resource/dto/RequirementDTO.java new file mode 100644 index 00000000000..dfa21db37ca --- /dev/null +++ b/framework/src/main/java/org/osgi/resource/dto/RequirementDTO.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.resource.dto; + +import java.util.Map; +import org.osgi.dto.DTO; +import org.osgi.resource.Requirement; + +/** + * Data Transfer Object for a Requirement. + * + * @author $Id$ + * @NotThreadSafe + */ +public class RequirementDTO extends DTO { + /** + * The unique identifier of the requirement. + * + *

      + * This identifier is transiently assigned and may vary across restarts. + */ + public int id; + + /** + * The namespace for the requirement. + * + * @see Requirement#getNamespace() + */ + public String namespace; + + /** + * The directives for the requirement. + * + * @see Requirement#getDirectives() + */ + public Map directives; + + /** + * The attributes for the requirement. + * + *

      + * The value type must be a numerical type, Boolean, String, DTO or an array + * of any of the former. + * + * @see Requirement#getAttributes() + */ + public Map attributes; + + /** + * The identifier of the resource declaring the requirement. + * + * @see ResourceDTO#id + * @see Requirement#getResource() + */ + public int resource; +} diff --git a/framework/src/main/java/org/osgi/resource/dto/RequirementRefDTO.java b/framework/src/main/java/org/osgi/resource/dto/RequirementRefDTO.java new file mode 100644 index 00000000000..8f913a72d9d --- /dev/null +++ b/framework/src/main/java/org/osgi/resource/dto/RequirementRefDTO.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) OSGi Alliance (2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.resource.dto; + +import org.osgi.dto.DTO; + +/** + * Data Transfer Object for a reference to a Requirement. + * + * @author $Id$ + * @NotThreadSafe + */ +public class RequirementRefDTO extends DTO { + /** + * The identifier of the requirement in the resource. + * + * @see RequirementDTO#id + */ + public int requirement; + + /** + * The identifier of the resource declaring the requirement. + * + * @see ResourceDTO#id + */ + public int resource; +} diff --git a/framework/src/main/java/org/osgi/resource/dto/ResourceDTO.java b/framework/src/main/java/org/osgi/resource/dto/ResourceDTO.java new file mode 100644 index 00000000000..377b7aff2a6 --- /dev/null +++ b/framework/src/main/java/org/osgi/resource/dto/ResourceDTO.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.resource.dto; + +import java.util.List; +import org.osgi.dto.DTO; +import org.osgi.resource.Resource; + +/** + * Data Transfer Object for a Resource. + * + * @author $Id$ + * @NotThreadSafe + */ +public class ResourceDTO extends DTO { + /** + * The unique identifier of the resource. + * + *

      + * This identifier is transiently assigned and may vary across restarts. + */ + public int id; + + /** + * The capabilities of the resource. + * + * @see Resource#getCapabilities(String) + */ + public List capabilities; + + /** + * The requirements of the resource. + * + * @see Resource#getRequirements(String) + */ + public List requirements; +} diff --git a/framework/src/main/java/org/osgi/resource/dto/WireDTO.java b/framework/src/main/java/org/osgi/resource/dto/WireDTO.java new file mode 100644 index 00000000000..017ea7cc366 --- /dev/null +++ b/framework/src/main/java/org/osgi/resource/dto/WireDTO.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.resource.dto; + +import org.osgi.dto.DTO; +import org.osgi.resource.Wire; + +/** + * Data Transfer Object for a Wire. + * + * @author $Id$ + * @NotThreadSafe + */ +public class WireDTO extends DTO { + /** + * Reference to the Capability for the wire. + * + * @see Wire#getCapability() + */ + public CapabilityRefDTO capability; + + /** + * Reference to the Requirement for the wire. + * + * @see Wire#getRequirement() + */ + public RequirementRefDTO requirement; + + /** + * The identifier of the provider resource for the wire. + * + * @see ResourceDTO#id + * @see Wire#getProvider() + */ + public int provider; + + /** + * The identifier of the requiring resource for the wire. + * + * @see ResourceDTO#id + * @see Wire#getRequirer() + */ + public int requirer; +} diff --git a/framework/src/main/java/org/osgi/resource/dto/WiringDTO.java b/framework/src/main/java/org/osgi/resource/dto/WiringDTO.java new file mode 100644 index 00000000000..8dc3844e57e --- /dev/null +++ b/framework/src/main/java/org/osgi/resource/dto/WiringDTO.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.resource.dto; + +import java.util.List; +import org.osgi.dto.DTO; +import org.osgi.resource.Wiring; + +/** + * Data Transfer Object for a Wiring node. + * + * @author $Id$ + * @NotThreadSafe + */ +public class WiringDTO extends DTO { + /** + * The unique identifier of the wiring node. + * + *

      + * This identifier is transiently assigned and may vary across restarts. + */ + public int id; + + /** + * The references to the capabilities for the wiring node. + * + * @see Wiring#getResourceCapabilities(String) + */ + public List capabilities; + + /** + * The references to the requirements for the wiring node. + * + * @see Wiring#getResourceRequirements(String) + */ + public List requirements; + + /** + * The provided wires for the wiring node. + * + * @see Wiring#getProvidedResourceWires(String) + */ + public List providedWires; + + /** + * The required wires for the wiring node. + * + * @see Wiring#getRequiredResourceWires(String) + */ + public List requiredWires; + + /** + * The identifier of the resource associated with the wiring node. + * + * @see ResourceDTO#id + * @see Wiring#getResource() + */ + public int resource; +} diff --git a/framework/src/main/java/org/osgi/resource/dto/package-info.java b/framework/src/main/java/org/osgi/resource/dto/package-info.java new file mode 100644 index 00000000000..6e316cddacc --- /dev/null +++ b/framework/src/main/java/org/osgi/resource/dto/package-info.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) OSGi Alliance (2012, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * OSGi Data Transfer Object Resource Package Version 1.0. + * + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. This package has two types of + * users: the consumers that use the API in this package and the providers that + * implement the API in this package. + * + *

      + * Example import for consumers using the API in this package: + *

      + * {@code Import-Package: org.osgi.resource.dto; version="[1.0,2.0)"} + *

      + * Example import for providers implementing the API in this package: + *

      + * {@code Import-Package: org.osgi.resource.dto; version="[1.0,1.1)"} + * + * @author $Id$ + */ + +@Version("1.0") +package org.osgi.resource.dto; + +import org.osgi.annotation.versioning.Version; + diff --git a/framework/src/main/java/org/osgi/resource/package-info.java b/framework/src/main/java/org/osgi/resource/package-info.java new file mode 100644 index 00000000000..c537ca9b2fa --- /dev/null +++ b/framework/src/main/java/org/osgi/resource/package-info.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) OSGi Alliance (2011, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Resource Package Version 1.0. + * + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. For example: + * + *

      + * Import-Package: org.osgi.resource; version="[1.0,2.0)"
      + * 
      + * + * @author $Id$ + */ + +@Version("1.0") +package org.osgi.resource; + +import org.osgi.annotation.versioning.Version; + diff --git a/framework/src/main/java/org/osgi/service/packageadmin/ExportedPackage.java b/framework/src/main/java/org/osgi/service/packageadmin/ExportedPackage.java new file mode 100644 index 00000000000..0a1236dd8cb --- /dev/null +++ b/framework/src/main/java/org/osgi/service/packageadmin/ExportedPackage.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) OSGi Alliance (2001, 2014). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.service.packageadmin; + +import org.osgi.framework.Bundle; +import org.osgi.framework.Version; + +/** + * An exported package. + * + * Objects implementing this interface are created by the Package Admin service. + * + *

      + * The term exported package refers to a package that has been exported + * from a resolved bundle. This package may or may not be currently wired to + * other bundles. + * + *

      + * The information about an exported package provided by this object may change. + * An {@code ExportedPackage} object becomes stale if the package it + * references has been updated or removed as a result of calling + * {@code PackageAdmin.refreshPackages()}. + * + * If this object becomes stale, its {@code getName()} and + * {@code getVersion()} methods continue to return their original values, + * {@code isRemovalPending()} returns {@code true}, and + * {@code getExportingBundle()} and {@code getImportingBundles()} + * return {@code null}. + * + * @ThreadSafe + * @noimplement + * @deprecated The PackageAdmin service has been replaced by the + * org.osgi.framework.wiring package. + * @author $Id: f4cdb9e84ce788c16d5304166a2b9eeecb5fabf3 $ + */ +public interface ExportedPackage { + /** + * Returns the name of the package associated with this exported package. + * + * @return The name of this exported package. + */ + public String getName(); + + /** + * Returns the bundle exporting the package associated with this exported + * package. + * + * @return The exporting bundle, or {@code null} if this + * {@code ExportedPackage} object has become stale. + */ + public Bundle getExportingBundle(); + + /** + * Returns the resolved bundles that are currently wired to this exported + * package. + * + *

      + * Bundles which require the exporting bundle associated with this exported + * package are considered to be wired to this exported package are included + * in the returned array. See {@link RequiredBundle#getRequiringBundles()}. + * + * @return The array of resolved bundles currently wired to this exported + * package, or {@code null} if this + * {@code ExportedPackage} object has become stale. The array + * will be empty if no bundles are wired to this exported package. + */ + public Bundle[] getImportingBundles(); + + /** + * Returns the version of this exported package. + * + * @return The version of this exported package, or {@code null} if no + * version information is available. + * @deprecated As of 1.2. Replaced by {@link #getVersion()}. + */ + public String getSpecificationVersion(); + + /** + * Returns the version of this exported package. + * + * @return The version of this exported package, or + * {@link Version#emptyVersion} if no version information is + * available. + * @since 1.2 + */ + public Version getVersion(); + + /** + * Returns {@code true} if the package associated with this + * {@code ExportedPackage} object has been exported by a bundle that + * has been updated or uninstalled. + * + * @return {@code true} if the associated package is being exported + * by a bundle that has been updated or uninstalled, or if this + * {@code ExportedPackage} object has become stale; + * {@code false} otherwise. + */ + public boolean isRemovalPending(); +} diff --git a/framework/src/main/java/org/osgi/service/packageadmin/PackageAdmin.java b/framework/src/main/java/org/osgi/service/packageadmin/PackageAdmin.java new file mode 100644 index 00000000000..26bdfb56bc6 --- /dev/null +++ b/framework/src/main/java/org/osgi/service/packageadmin/PackageAdmin.java @@ -0,0 +1,300 @@ +/* + * Copyright (c) OSGi Alliance (2001, 2015). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.service.packageadmin; + +import org.osgi.framework.Bundle; + +/** + * Framework service which allows bundle programmers to inspect the package + * wiring state of bundles in the Framework as well as other functions related + * to the class loader network among bundles. + * + *

      + * If present, there will only be a single instance of this service registered + * with the Framework. + * + * @ThreadSafe + * @noimplement + * @author $Id: 1de8679fcf7df57b019a93219f6b82222eb1525e $ + * @deprecated This service has been replaced by the + * org.osgi.framework.wiring package. + * @see org.osgi.service.packageadmin.ExportedPackage + * @see org.osgi.service.packageadmin.RequiredBundle + */ +public interface PackageAdmin { + /** + * Gets the exported packages for the specified bundle. + * + * @param bundle The bundle whose exported packages are to be returned, or + * {@code null} if all exported packages are to be returned. If + * the specified bundle is the system bundle (that is, the bundle + * with id zero), this method returns all the packages known to be + * exported by the system bundle. This will include the package + * specified by the {@code org.osgi.framework.system.packages} + * system property as well as any other package exported by the + * framework implementation. + * + * @return An array of exported packages, or {@code null} if the + * specified bundle has no exported packages. + * @throws IllegalArgumentException If the specified {@code Bundle} was + * not created by the same framework instance that registered this + * {@code PackageAdmin} service. + * @deprecated + */ + public ExportedPackage[] getExportedPackages(Bundle bundle); + + /** + * Gets the exported packages for the specified package name. + * + * @param name The name of the exported packages to be returned. + * + * @return An array of the exported packages, or {@code null} if no + * exported packages with the specified name exists. + * @since 1.2 + * @deprecated + */ + public ExportedPackage[] getExportedPackages(String name); + + /** + * Gets the exported package for the specified package name. + * + *

      + * If there are multiple exported packages with specified name, the exported + * package with the highest version will be returned. + * + * @param name The name of the exported package to be returned. + * + * @return The exported package, or {@code null} if no exported + * package with the specified name exists. + * @see #getExportedPackages(String) + * @deprecated + */ + public ExportedPackage getExportedPackage(String name); + + /** + * Forces the update (replacement) or removal of packages exported by the + * specified bundles. + * + *

      + * If no bundles are specified, this method will update or remove any + * packages exported by any bundles that were previously updated or + * uninstalled since the last call to this method. The technique by which + * this is accomplished may vary among different Framework implementations. + * One permissible implementation is to stop and restart the Framework. + * + *

      + * This method returns to the caller immediately and then performs the + * following steps on a separate thread: + * + *

        + *
      1. Compute a graph of bundles starting with the specified bundles. If no + * bundles are specified, compute a graph of bundles starting with bundle + * updated or uninstalled since the last call to this method. Add to the + * graph any bundle that is wired to a package that is currently exported by + * a bundle in the graph. The graph is fully constructed when there is no + * bundle outside the graph that is wired to a bundle in the graph. The + * graph may contain {@code UNINSTALLED} bundles that are currently still + * exporting packages.
      2. + * + *
      3. Each bundle in the graph that is in the {@code ACTIVE} state will be + * stopped as described in the {@code Bundle.stop} method.
      4. + * + *
      5. Each bundle in the graph that is in the {@code RESOLVED} state is + * unresolved and thus moved to the {@code INSTALLED} state. The effect of + * this step is that bundles in the graph are no longer {@code RESOLVED}.
      6. + * + *
      7. Each bundle in the graph that is in the {@code UNINSTALLED} state is + * removed from the graph and is now completely removed from the Framework.
      8. + * + *
      9. Each bundle in the graph that was in the {@code ACTIVE} state prior + * to Step 2 is started as described in the {@code Bundle.start} method, + * causing all bundles required for the restart to be resolved. It is + * possible that, as a result of the previous steps, packages that were + * previously exported no longer are. Therefore, some bundles may be + * unresolvable until another bundle offering a compatible package for + * export has been installed in the Framework.
      10. + *
      11. A framework event of type {@code FrameworkEvent.PACKAGES_REFRESHED} + * is fired.
      12. + *
      + * + *

      + * For any exceptions that are thrown during any of these steps, a + * {@code FrameworkEvent} of type {@code ERROR} is fired + * containing the exception. The source bundle for these events should be + * the specific bundle to which the exception is related. If no specific + * bundle can be associated with the exception then the System Bundle must + * be used as the source bundle for the event. + * + * @param bundles The bundles whose exported packages are to be updated or + * removed, or {@code null} for all bundles updated or + * uninstalled since the last call to this method. + * @throws SecurityException If the caller does not have + * {@code AdminPermission[System Bundle,RESOLVE]} and the Java + * runtime environment supports permissions. + * @throws IllegalArgumentException If the specified {@code Bundle}s + * were not created by the same framework instance that registered + * this {@code PackageAdmin} service. + */ + public void refreshPackages(Bundle[] bundles); + + /** + * Resolve the specified bundles. The Framework must attempt to resolve the + * specified bundles that are unresolved. Additional bundles that are not + * included in the specified bundles may be resolved as a result of calling + * this method. A permissible implementation of this method is to attempt to + * resolve all unresolved bundles installed in the framework. + * + *

      + * If {@code null} is specified then the Framework will attempt to + * resolve all unresolved bundles. This method must not cause any bundle to + * be refreshed, stopped, or started. This method will not return until the + * operation has completed. + * + * @param bundles The bundles to resolve or {@code null} to resolve all + * unresolved bundles installed in the Framework. + * @return {@code true} if all specified bundles are resolved; + * @throws SecurityException If the caller does not have + * {@code AdminPermission[System Bundle,RESOLVE]} and the Java + * runtime environment supports permissions. + * @throws IllegalArgumentException If the specified {@code Bundle}s + * were not created by the same framework instance that registered + * this {@code PackageAdmin} service. + * @since 1.2 + */ + public boolean resolveBundles(Bundle[] bundles); + + /** + * Returns an array of required bundles having the specified symbolic name. + * + *

      + * If {@code null} is specified, then all required bundles will be + * returned. + * + * @param symbolicName The bundle symbolic name or {@code null} for + * all required bundles. + * @return An array of required bundles or {@code null} if no + * required bundles exist for the specified symbolic name. + * @since 1.2 + * @deprecated + */ + public RequiredBundle[] getRequiredBundles(String symbolicName); + + /** + * Returns the bundles with the specified symbolic name whose bundle version + * is within the specified version range. If no bundles are installed that + * have the specified symbolic name, then {@code null} is returned. + * If a version range is specified, then only the bundles that have the + * specified symbolic name and whose bundle versions belong to the specified + * version range are returned. The returned bundles are ordered by version + * in descending version order so that the first element of the array + * contains the bundle with the highest version. + * + * @see org.osgi.framework.Constants#BUNDLE_VERSION_ATTRIBUTE + * @param symbolicName The symbolic name of the desired bundles. + * @param versionRange The version range of the desired bundles, or + * {@code null} if all versions are desired. + * @return An array of bundles with the specified name belonging to the + * specified version range ordered in descending version order, or + * {@code null} if no bundles are found. + * @since 1.2 + */ + public Bundle[] getBundles(String symbolicName, String versionRange); + + /** + * Returns an array of attached fragment bundles for the specified bundle. + * If the specified bundle is a fragment then {@code null} is returned. + * If no fragments are attached to the specified bundle then + * {@code null} is returned. + *

      + * This method does not attempt to resolve the specified bundle. If the + * specified bundle is not resolved then {@code null} is returned. + * + * @param bundle The bundle whose attached fragment bundles are to be + * returned. + * @return An array of fragment bundles or {@code null} if the bundle + * does not have any attached fragment bundles or the bundle is not + * resolved. + * @throws IllegalArgumentException If the specified {@code Bundle} was + * not created by the same framework instance that registered this + * {@code PackageAdmin} service. + * @since 1.2 + */ + public Bundle[] getFragments(Bundle bundle); + + /** + * Returns the host bundles to which the specified fragment bundle is + * attached. + * + * @param bundle The fragment bundle whose host bundles are to be returned. + * @return An array containing the host bundles to which the specified + * fragment is attached or {@code null} if the specified bundle + * is not a fragment or is not attached to any host bundles. + * @throws IllegalArgumentException If the specified {@code Bundle} was + * not created by the same framework instance that registered this + * {@code PackageAdmin} service. + * @since 1.2 + */ + public Bundle[] getHosts(Bundle bundle); + + /** + * Returns the bundle from which the specified class is loaded. The class + * loader of the returned bundle must have been used to load the specified + * class. If the class was not loaded by a bundle class loader then + * {@code null} is returned. + * + * @param clazz The class object from which to locate the bundle. + * @return The bundle from which the specified class is loaded or + * {@code null} if the class was not loaded by a bundle class + * loader created by the same framework instance that registered + * this {@code PackageAdmin} service. + * @since 1.2 + */ + public Bundle getBundle(Class clazz); + + /** + * Bundle type indicating the bundle is a fragment bundle. + * + *

      + * The value of {@code BUNDLE_TYPE_FRAGMENT} is 0x00000001. + * + * @since 1.2 + */ + public static final int BUNDLE_TYPE_FRAGMENT = 0x00000001; + + /** + * Returns the special type of the specified bundle. The bundle type values + * are: + *

        + *
      • {@link #BUNDLE_TYPE_FRAGMENT}
      • + *
      + * + * A bundle may be more than one type at a time. A type code is used to + * identify the bundle type for future extendability. + * + *

      + * If a bundle is not one or more of the defined types then 0x00000000 is + * returned. + * + * @param bundle The bundle for which to return the special type. + * @return The special type of the bundle. + * @throws IllegalArgumentException If the specified {@code Bundle} was + * not created by the same framework instance that registered this + * {@code PackageAdmin} service. + * @since 1.2 + */ + public int getBundleType(Bundle bundle); +} diff --git a/framework/src/main/java/org/osgi/service/packageadmin/RequiredBundle.java b/framework/src/main/java/org/osgi/service/packageadmin/RequiredBundle.java new file mode 100644 index 00000000000..2434dcec569 --- /dev/null +++ b/framework/src/main/java/org/osgi/service/packageadmin/RequiredBundle.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) OSGi Alliance (2004, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.service.packageadmin; + +import org.osgi.framework.Bundle; +import org.osgi.framework.Version; + +/** + * A required bundle. + * + * Objects implementing this interface are created by the Package Admin service. + * + *

      + * The term required bundle refers to a resolved bundle that has a bundle + * symbolic name and is not a fragment. That is, a bundle that may be required + * by other bundles. This bundle may or may not be currently required by other + * bundles. + * + *

      + * The information about a required bundle provided by this object may change. A + * {@code RequiredBundle} object becomes stale if an exported package of + * the bundle it references has been updated or removed as a result of calling + * {@code PackageAdmin.refreshPackages()}). + * + * If this object becomes stale, its {@code getSymbolicName()} and + * {@code getVersion()} methods continue to return their original values, + * {@code isRemovalPending()} returns true, and {@code getBundle()} + * and {@code getRequiringBundles()} return {@code null}. + * + * @since 1.2 + * @ThreadSafe + * @noimplement + * @deprecated The PackageAdmin service has been replaced by the + * org.osgi.framework.wiring package. + * @author $Id: 08ab9c1a6f4a9af2060293d2c2972e4e07e2a238 $ + */ +public interface RequiredBundle { + /** + * Returns the symbolic name of this required bundle. + * + * @return The symbolic name of this required bundle. + */ + public String getSymbolicName(); + + /** + * Returns the bundle associated with this required bundle. + * + * @return The bundle, or {@code null} if this + * {@code RequiredBundle} object has become stale. + */ + public Bundle getBundle(); + + /** + * Returns the bundles that currently require this required bundle. + * + *

      + * If this required bundle is required and then re-exported by another + * bundle then all the requiring bundles of the re-exporting bundle are + * included in the returned array. + * + * @return An array of bundles currently requiring this required bundle, or + * {@code null} if this {@code RequiredBundle} object + * has become stale. The array will be empty if no bundles require + * this required package. + */ + public Bundle[] getRequiringBundles(); + + /** + * Returns the version of this required bundle. + * + * @return The version of this required bundle, or + * {@link Version#emptyVersion} if no version information is + * available. + */ + public Version getVersion(); + + /** + * Returns {@code true} if the bundle associated with this + * {@code RequiredBundle} object has been updated or uninstalled. + * + * @return {@code true} if the required bundle has been updated or + * uninstalled, or if the {@code RequiredBundle} object has + * become stale; {@code false} otherwise. + */ + public boolean isRemovalPending(); +} diff --git a/framework/src/main/java/org/osgi/service/packageadmin/package-info.java b/framework/src/main/java/org/osgi/service/packageadmin/package-info.java new file mode 100644 index 00000000000..41bb8d38ec3 --- /dev/null +++ b/framework/src/main/java/org/osgi/service/packageadmin/package-info.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Package Admin Package Version 1.2. + * + *

      + * Deprecated. + * This package is deprecated and has been replaced by the + * {@code org.osgi.framework.wiring} package. + * + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. + * + *

      + * Example import for consumers using the API in this package: + *

      + * {@code Import-Package: org.osgi.service.packageadmin; version="[1.2,2.0)"} + * + * @author $Id$ + */ + +@Version("1.2") +package org.osgi.service.packageadmin; + +import org.osgi.annotation.versioning.Version; diff --git a/framework/src/main/java/org/osgi/service/startlevel/StartLevel.java b/framework/src/main/java/org/osgi/service/startlevel/StartLevel.java new file mode 100644 index 00000000000..2e299ed27eb --- /dev/null +++ b/framework/src/main/java/org/osgi/service/startlevel/StartLevel.java @@ -0,0 +1,275 @@ +/* + * Copyright (c) OSGi Alliance (2002, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.service.startlevel; + +import org.osgi.framework.Bundle; + +/** + * The StartLevel service allows management agents to manage a start level + * assigned to each bundle and the active start level of the Framework. There is + * at most one StartLevel service present in the OSGi environment. + * + *

      + * A start level is defined to be a state of execution in which the Framework + * exists. StartLevel values are defined as unsigned integers with 0 (zero) + * being the state where the Framework is not launched. Progressively higher + * integral values represent progressively higher start levels. e.g. 2 is a + * higher start level than 1. + *

      + * Access to the StartLevel service is protected by corresponding + * {@code ServicePermission}. In addition {@code AdminPermission} + * is required to actually modify start level information. + *

      + * Start Level support in the Framework includes the ability to control the + * beginning start level of the Framework, to modify the active start level of + * the Framework and to assign a specific start level to a bundle. How the + * beginning start level of a Framework is specified is implementation + * dependent. It may be a command line argument when invoking the Framework + * implementation. + *

      + * When the Framework is first started it must be at start level zero. In this + * state, no bundles are running. This is the initial state of the Framework + * before it is launched. + * + * When the Framework is launched, the Framework will enter start level one and + * all bundles which are assigned to start level one and whose autostart setting + * indicates the bundle should be started are started as described in the + * {@code Bundle.start} method. The Framework will continue to increase + * the start level, starting bundles at each start level, until the Framework + * has reached a beginning start level. At this point the Framework has + * completed starting bundles and will then fire a Framework event of type + * {@code FrameworkEvent.STARTED} to announce it has completed its + * launch. + * + *

      + * Within a start level, bundles may be started in an order defined by the + * Framework implementation. This may be something like ascending + * {@code Bundle.getBundleId} order or an order based upon dependencies + * between bundles. A similar but reversed order may be used when stopping + * bundles within a start level. + * + *

      + * The StartLevel service can be used by management bundles to alter the active + * start level of the framework. + * + * @ThreadSafe + * @noimplement + * @author $Id: 42f3c6bbf682a69ea3914c737d5b0001694383db $ + * @deprecated This service has been replaced by the + * org.osgi.framework.startlevel package. + */ +public interface StartLevel { + /** + * Return the active start level value of the Framework. + * + * If the Framework is in the process of changing the start level this + * method must return the active start level if this differs from the + * requested start level. + * + * @return The active start level value of the Framework. + */ + public int getStartLevel(); + + /** + * Modify the active start level of the Framework. + * + *

      + * The Framework will move to the requested start level. This method will + * return immediately to the caller and the start level change will occur + * asynchronously on another thread. + * + *

      + * If the specified start level is higher than the active start level, the + * Framework will continue to increase the start level until the Framework + * has reached the specified start level. + * + * At each intermediate start level value on the way to and including the + * target start level, the Framework must: + *

        + *
      1. Change the active start level to the intermediate start level value.
      2. + *
      3. Start bundles at the intermediate start level whose autostart setting + * indicate they must be started. They are started as described in the + * {@link Bundle#start(int)} method using the {@link Bundle#START_TRANSIENT} + * option. The {@link Bundle#START_ACTIVATION_POLICY} option must also be + * used if {@link #isBundleActivationPolicyUsed(Bundle)} returns + * {@code true} for the bundle.
      4. + *
      + * When this process completes after the specified start level is reached, + * the Framework will fire a Framework event of type + * {@code FrameworkEvent.STARTLEVEL_CHANGED} to announce it has moved + * to the specified start level. + * + *

      + * If the specified start level is lower than the active start level, the + * Framework will continue to decrease the start level until the Framework + * has reached the specified start level. + * + * At each intermediate start level value on the way to and including the + * specified start level, the framework must: + *

        + *
      1. Stop bundles at the intermediate start level as described in the + * {@link Bundle#stop(int)} method using the {@link Bundle#STOP_TRANSIENT} + * option.
      2. + *
      3. Change the active start level to the intermediate start level value.
      4. + *
      + * When this process completes after the specified start level is reached, + * the Framework will fire a Framework event of type + * {@code FrameworkEvent.STARTLEVEL_CHANGED} to announce it has moved + * to the specified start level. + * + *

      + * If the specified start level is equal to the active start level, then no + * bundles are started or stopped, however, the Framework must fire a + * Framework event of type {@code FrameworkEvent.STARTLEVEL_CHANGED} + * to announce it has finished moving to the specified start level. This + * event may arrive before this method return. + * + * @param startlevel The requested start level for the Framework. + * @throws IllegalArgumentException If the specified start level is less + * than or equal to zero. + * @throws SecurityException If the caller does not have + * {@code AdminPermission[System Bundle,STARTLEVEL]} and the + * Java runtime environment supports permissions. + */ + public void setStartLevel(int startlevel); + + /** + * Return the assigned start level value for the specified Bundle. + * + * @param bundle The target bundle. + * @return The start level value of the specified Bundle. + * @throws java.lang.IllegalArgumentException If the specified bundle has + * been uninstalled or if the specified bundle was not created by + * the same framework instance that registered this + * {@code StartLevel} service. + */ + public int getBundleStartLevel(Bundle bundle); + + /** + * Assign a start level value to the specified Bundle. + * + *

      + * The specified bundle will be assigned the specified start level. The + * start level value assigned to the bundle will be persistently recorded by + * the Framework. + *

      + * If the new start level for the bundle is lower than or equal to the + * active start level of the Framework and the bundle's autostart setting + * indicates the bundle must be started, the Framework will start the + * specified bundle as described in the {@link Bundle#start(int)} method + * using the {@link Bundle#START_TRANSIENT} option. The + * {@link Bundle#START_ACTIVATION_POLICY} option must also be used if + * {@link #isBundleActivationPolicyUsed(Bundle)} returns {@code true} + * for the bundle. The actual starting of this bundle must occur + * asynchronously. + *

      + * If the new start level for the bundle is higher than the active start + * level of the Framework, the Framework will stop the specified bundle as + * described in the {@link Bundle#stop(int)} method using the + * {@link Bundle#STOP_TRANSIENT} option. The actual stopping of this bundle + * must occur asynchronously. + * + * @param bundle The target bundle. + * @param startlevel The new start level for the specified Bundle. + * @throws IllegalArgumentException If the specified bundle has been + * uninstalled, or if the specified start level is less than or + * equal to zero, or if the specified bundle is the system bundle, + * or if the specified bundle was not created by the same framework + * instance that registered this {@code StartLevel} service. + * @throws SecurityException If the caller does not have + * {@code AdminPermission[bundle,EXECUTE]} and the Java runtime + * environment supports permissions. + */ + public void setBundleStartLevel(Bundle bundle, int startlevel); + + /** + * Return the initial start level value that is assigned to a Bundle when it + * is first installed. + * + * @return The initial start level value for Bundles. + * @see #setInitialBundleStartLevel(int) + */ + public int getInitialBundleStartLevel(); + + /** + * Set the initial start level value that is assigned to a Bundle when it is + * first installed. + * + *

      + * The initial bundle start level will be set to the specified start level. + * The initial bundle start level value will be persistently recorded by the + * Framework. + * + *

      + * When a Bundle is installed via {@code BundleContext.installBundle}, + * it is assigned the initial bundle start level value. + * + *

      + * The default initial bundle start level value is 1 unless this method has + * been called to assign a different initial bundle start level value. + * + *

      + * This method does not change the start level values of installed bundles. + * + * @param startlevel The initial start level for newly installed bundles. + * @throws IllegalArgumentException If the specified start level is less + * than or equal to zero. + * @throws SecurityException If the caller does not have + * {@code AdminPermission[System Bundle,STARTLEVEL]} and the + * Java runtime environment supports permissions. + */ + public void setInitialBundleStartLevel(int startlevel); + + /** + * Returns whether the specified bundle's autostart setting indicates the + * bundle must be started. + *

      + * The autostart setting of a bundle indicates whether the bundle is to be + * started when its start level is reached. + * + * @param bundle The bundle whose autostart setting is to be examined. + * @return {@code true} if the autostart setting of the bundle + * indicates the bundle is to be started. {@code false} + * otherwise. + * @throws java.lang.IllegalArgumentException If the specified bundle has + * been uninstalled or if the specified bundle was not created by + * the same framework instance that registered this + * {@code StartLevel} service. + * @see Bundle#START_TRANSIENT + */ + public boolean isBundlePersistentlyStarted(Bundle bundle); + + /** + * Returns whether the specified bundle's autostart setting indicates that + * the activation policy declared in the bundle's manifest must be used. + *

      + * The autostart setting of a bundle indicates whether the bundle's declared + * activation policy is to be used when the bundle is started. + * + * @param bundle The bundle whose autostart setting is to be examined. + * @return {@code true} if the bundle's autostart setting indicates the + * activation policy declared in the manifest must be used. + * {@code false} if the bundle must be eagerly activated. + * @throws java.lang.IllegalArgumentException If the specified bundle has + * been uninstalled or if the specified bundle was not created by + * the same framework instance that registered this + * {@code StartLevel} service. + * @since 1.1 + * @see Bundle#START_ACTIVATION_POLICY + */ + public boolean isBundleActivationPolicyUsed(Bundle bundle); +} diff --git a/framework/src/main/java/org/osgi/service/startlevel/package-info.java b/framework/src/main/java/org/osgi/service/startlevel/package-info.java new file mode 100644 index 00000000000..70b62bd0b80 --- /dev/null +++ b/framework/src/main/java/org/osgi/service/startlevel/package-info.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Start Level Package Version 1.1. + * + *

      + * Deprecated. + * This package is deprecated and has been replaced by the + * {@code org.osgi.framework.startlevel} package. + * + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. + * + *

      + * Example import for consumers using the API in this package: + *

      + * {@code Import-Package: org.osgi.service.startlevel; version="[1.1,2.0)"} + * + * @author $Id$ + */ + +@Version("1.1") +package org.osgi.service.startlevel; + +import org.osgi.annotation.versioning.Version; diff --git a/framework/src/main/java/org/osgi/service/url/AbstractURLStreamHandlerService.java b/framework/src/main/java/org/osgi/service/url/AbstractURLStreamHandlerService.java new file mode 100644 index 00000000000..da154ddbf72 --- /dev/null +++ b/framework/src/main/java/org/osgi/service/url/AbstractURLStreamHandlerService.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) OSGi Alliance (2002, 2015). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.service.url; + +import java.net.InetAddress; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import org.osgi.annotation.versioning.ConsumerType; + +/** + * Abstract implementation of the {@code URLStreamHandlerService} interface. All + * the methods simply invoke the corresponding methods on + * {@code java.net.URLStreamHandler} except for {@code parseURL} and + * {@code setURL}, which use the {@code URLStreamHandlerSetter} parameter. + * Subclasses of this abstract class should not need to override the + * {@code setURL} and {@code parseURL(URLStreamHandlerSetter,...)} methods. + * + * @ThreadSafe + * @author $Id: 71ce6a3846e0b82b2e096ae7617c2af7a1297f0e $ + */ +@ConsumerType +public abstract class AbstractURLStreamHandlerService extends URLStreamHandler implements URLStreamHandlerService { + /** + * @see "java.net.URLStreamHandler.openConnection" + */ + @Override + public abstract URLConnection openConnection(URL u) throws java.io.IOException; + + /** + * The {@code URLStreamHandlerSetter} object passed to the parseURL method. + */ + protected volatile URLStreamHandlerSetter realHandler; + + /** + * Parse a URL using the {@code URLStreamHandlerSetter} object. This method + * sets the {@code realHandler} field with the specified + * {@code URLStreamHandlerSetter} object and then calls + * {@code parseURL(URL,String,int,int)}. + * + * @param realHandler The object on which the {@code setURL} method must be + * invoked for the specified URL. + * @see "java.net.URLStreamHandler.parseURL" + */ + @Override + public void parseURL(@SuppressWarnings("hiding") URLStreamHandlerSetter realHandler, URL u, String spec, int start, int limit) { + this.realHandler = realHandler; + parseURL(u, spec, start, limit); + } + + /** + * This method calls {@code super.toExternalForm}. + * + * @see "java.net.URLStreamHandler.toExternalForm" + */ + @Override + public String toExternalForm(URL u) { + return super.toExternalForm(u); + } + + /** + * This method calls {@code super.equals(URL,URL)}. + * + * @see "java.net.URLStreamHandler.equals(URL,URL)" + */ + @Override + public boolean equals(URL u1, URL u2) { + return super.equals(u1, u2); + } + + /** + * This method calls {@code super.getDefaultPort}. + * + * @see "java.net.URLStreamHandler.getDefaultPort" + */ + @Override + public int getDefaultPort() { + return super.getDefaultPort(); + } + + /** + * This method calls {@code super.getHostAddress}. + * + * @see "java.net.URLStreamHandler.getHostAddress" + */ + @Override + public InetAddress getHostAddress(URL u) { + return super.getHostAddress(u); + } + + /** + * This method calls {@code super.hashCode(URL)}. + * + * @see "java.net.URLStreamHandler.hashCode(URL)" + */ + @Override + public int hashCode(URL u) { + return super.hashCode(u); + } + + /** + * This method calls {@code super.hostsEqual}. + * + * @see "java.net.URLStreamHandler.hostsEqual" + */ + @Override + public boolean hostsEqual(URL u1, URL u2) { + return super.hostsEqual(u1, u2); + } + + /** + * This method calls {@code super.sameFile}. + * + * @see "java.net.URLStreamHandler.sameFile" + */ + @Override + public boolean sameFile(URL u1, URL u2) { + return super.sameFile(u1, u2); + } + + /** + * This method calls + * {@code realHandler.setURL(URL,String,String,int,String,String)}. + * + * @see "java.net.URLStreamHandler.setURL(URL,String,String,int,String,String)" + * @deprecated This method is only for compatibility with handlers written + * for JDK 1.1. + */ + @Override + protected void setURL(URL u, String proto, String host, int port, String file, String ref) { + realHandler.setURL(u, proto, host, port, file, ref); + } + + /** + * This method calls + * {@code realHandler.setURL(URL,String,String,int,String,String,String,String)} + * . + * + * @see "java.net.URLStreamHandler.setURL(URL,String,String,int,String,String,String,String)" + */ + @Override + protected void setURL(URL u, String proto, String host, int port, String auth, String user, String path, String query, String ref) { + realHandler.setURL(u, proto, host, port, auth, user, path, query, ref); + } +} diff --git a/framework/src/main/java/org/osgi/service/url/URLConstants.java b/framework/src/main/java/org/osgi/service/url/URLConstants.java new file mode 100644 index 00000000000..522c1b89d2b --- /dev/null +++ b/framework/src/main/java/org/osgi/service/url/URLConstants.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) OSGi Alliance (2002, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.service.url; + +import org.osgi.annotation.versioning.ProviderType; + +/** + * Defines standard names for property keys associated with + * {@link URLStreamHandlerService} and {@code java.net.ContentHandler} services. + * + *

      + * The values associated with these keys are of type {@code java.lang.String[]} + * or {@code java.lang.String}, unless otherwise indicated. + * + * @author $Id: 490baaad326523bcb3915ef04572d3c28560db0b $ + */ +@ProviderType +public interface URLConstants { + /** + * Service property naming the protocols serviced by a + * URLStreamHandlerService. The property's value is a protocol name or an + * array of protocol names. + */ + public static final String URL_HANDLER_PROTOCOL = "url.handler.protocol"; + /** + * Service property naming the MIME types serviced by a + * java.net.ContentHandler. The property's value is a MIME type or an array + * of MIME types. + */ + public static final String URL_CONTENT_MIMETYPE = "url.content.mimetype"; +} diff --git a/framework/src/main/java/org/osgi/service/url/URLStreamHandlerService.java b/framework/src/main/java/org/osgi/service/url/URLStreamHandlerService.java new file mode 100644 index 00000000000..5ba4f7bd4ee --- /dev/null +++ b/framework/src/main/java/org/osgi/service/url/URLStreamHandlerService.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) OSGi Alliance (2002, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.service.url; + +import java.net.InetAddress; +import java.net.URL; +import java.net.URLConnection; +import org.osgi.annotation.versioning.ConsumerType; + +/** + * Service interface with public versions of the protected + * {@code java.net.URLStreamHandler} methods. + *

      + * The important differences between this interface and the + * {@code URLStreamHandler} class are that the {@code setURL} method is absent + * and the {@code parseURL} method takes a {@link URLStreamHandlerSetter} object + * as the first argument. Classes implementing this interface must call the + * {@code setURL} method on the {@code URLStreamHandlerSetter} object received + * in the {@code parseURL} method instead of {@code URLStreamHandler.setURL} to + * avoid a {@code SecurityException}. + * + * @see AbstractURLStreamHandlerService + * + * @ThreadSafe + * @author $Id: 810d8718f5ad689981fbb2c22886ad2695f17297 $ + */ +@ConsumerType +public interface URLStreamHandlerService { + /** + * @see "java.net.URLStreamHandler.openConnection" + */ + @SuppressWarnings("javadoc") + public URLConnection openConnection(URL u) throws java.io.IOException; + + /** + * Parse a URL. This method is called by the {@code URLStreamHandler} proxy, + * instead of {@code java.net.URLStreamHandler.parseURL}, passing a + * {@code URLStreamHandlerSetter} object. + * + * @param realHandler The object on which {@code setURL} must be invoked for + * this URL. + * @see "java.net.URLStreamHandler.parseURL" + */ + @SuppressWarnings("javadoc") + public void parseURL(URLStreamHandlerSetter realHandler, URL u, String spec, int start, int limit); + + /** + * @see "java.net.URLStreamHandler.toExternalForm" + */ + @SuppressWarnings("javadoc") + public String toExternalForm(URL u); + + /** + * @see "java.net.URLStreamHandler.equals(URL, URL)" + */ + @SuppressWarnings("javadoc") + public boolean equals(URL u1, URL u2); + + /** + * @see "java.net.URLStreamHandler.getDefaultPort" + */ + @SuppressWarnings("javadoc") + public int getDefaultPort(); + + /** + * @see "java.net.URLStreamHandler.getHostAddress" + */ + @SuppressWarnings("javadoc") + public InetAddress getHostAddress(URL u); + + /** + * @see "java.net.URLStreamHandler.hashCode(URL)" + */ + @SuppressWarnings("javadoc") + public int hashCode(URL u); + + /** + * @see "java.net.URLStreamHandler.hostsEqual" + */ + @SuppressWarnings("javadoc") + public boolean hostsEqual(URL u1, URL u2); + + /** + * @see "java.net.URLStreamHandler.sameFile" + */ + @SuppressWarnings("javadoc") + public boolean sameFile(URL u1, URL u2); +} diff --git a/framework/src/main/java/org/osgi/service/url/URLStreamHandlerSetter.java b/framework/src/main/java/org/osgi/service/url/URLStreamHandlerSetter.java new file mode 100644 index 00000000000..d7e222a351d --- /dev/null +++ b/framework/src/main/java/org/osgi/service/url/URLStreamHandlerSetter.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) OSGi Alliance (2002, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.service.url; + +import java.net.URL; +import org.osgi.annotation.versioning.ConsumerType; + +/** + * Interface used by {@code URLStreamHandlerService} objects to call the + * {@code setURL} method on the proxy {@code URLStreamHandler} object. + * + *

      + * Objects of this type are passed to the + * {@link URLStreamHandlerService#parseURL(URLStreamHandlerSetter, URL, String, int, int)} + * method. Invoking the {@code setURL} method on the + * {@code URLStreamHandlerSetter} object will invoke the {@code setURL} method + * on the proxy {@code URLStreamHandler} object that is actually registered with + * {@code java.net.URL} for the protocol. + * + * @ThreadSafe + * @author $Id: 96648b48a7ce8fb4baf50c7b118a0339f4efcf35 $ + */ +@ConsumerType +public interface URLStreamHandlerSetter { + /** + * @see "java.net.URLStreamHandler.setURL(URL,String,String,int,String,String)" + * + * @deprecated This method is only for compatibility with handlers written + * for JDK 1.1. + */ + @SuppressWarnings("javadoc") + public void setURL(URL u, String protocol, String host, int port, String file, String ref); + + /** + * @see "java.net.URLStreamHandler.setURL(URL,String,String,int,String,String,String,String)" + */ + @SuppressWarnings("javadoc") + public void setURL(URL u, String protocol, String host, int port, String authority, String userInfo, String path, String query, String ref); +} diff --git a/framework/src/main/java/org/osgi/service/url/package-info.java b/framework/src/main/java/org/osgi/service/url/package-info.java new file mode 100644 index 00000000000..7053546e11b --- /dev/null +++ b/framework/src/main/java/org/osgi/service/url/package-info.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * URL Stream and Content Handlers Package Version 1.0. + * + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. + * + *

      + * Example import for consumers using the API in this package: + *

      + * {@code Import-Package: org.osgi.service.url; version="[1.0,2.0)"} + * + * @author $Id$ + */ + +@Version("1.0") +package org.osgi.service.url; + +import org.osgi.annotation.versioning.Version; + diff --git a/framework/src/main/java/org/osgi/util/tracker/AbstractTracked.java b/framework/src/main/java/org/osgi/util/tracker/AbstractTracked.java new file mode 100644 index 00000000000..9aa0bd47e7b --- /dev/null +++ b/framework/src/main/java/org/osgi/util/tracker/AbstractTracked.java @@ -0,0 +1,466 @@ +/* + * Copyright (c) OSGi Alliance (2007, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.tracker; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * Abstract class to track items. If a Tracker is reused (closed then reopened), + * then a new AbstractTracked object is used. This class acts a map of tracked + * item -> customized object. Subclasses of this class will act as the listener + * object for the tracker. This class is used to synchronize access to the + * tracked items. This is not a public class. It is only for use by the + * implementation of the Tracker class. + * + * @param The tracked item. It is the key. + * @param The value mapped to the tracked item. + * @param The reason the tracked item is being tracked or untracked. + * @ThreadSafe + * @author $Id: 5988d793936c25139421a95bad2d3cd96e2ab355 $ + * @since 1.4 + */ +abstract class AbstractTracked { + /* set this to true to compile in debug messages */ + static final boolean DEBUG = false; + + /** + * Map of tracked items to customized objects. + * + * @GuardedBy this + */ + private final Map tracked; + + /** + * Modification count. This field is initialized to zero and incremented by + * modified. + * + * @GuardedBy this + */ + private int trackingCount; + + /** + * List of items in the process of being added. This is used to deal with + * nesting of events. Since events may be synchronously delivered, events + * can be nested. For example, when processing the adding of a service and + * the customizer causes the service to be unregistered, notification to the + * nested call to untrack that the service was unregistered can be made to + * the track method. + * + * Since the ArrayList implementation is not synchronized, all access to + * this list must be protected by the same synchronized object for + * thread-safety. + * + * @GuardedBy this + */ + private final List adding; + + /** + * true if the tracked object is closed. + * + * This field is volatile because it is set by one thread and read by + * another. + */ + volatile boolean closed; + + /** + * Initial list of items for the tracker. This is used to correctly process + * the initial items which could be modified before they are tracked. This + * is necessary since the initial set of tracked items are not "announced" + * by events and therefore the event which makes the item untracked could be + * delivered before we track the item. + * + * An item must not be in both the initial and adding lists at the same + * time. An item must be moved from the initial list to the adding list + * "atomically" before we begin tracking it. + * + * Since the LinkedList implementation is not synchronized, all access to + * this list must be protected by the same synchronized object for + * thread-safety. + * + * @GuardedBy this + */ + private final LinkedList initial; + + /** + * AbstractTracked constructor. + */ + AbstractTracked() { + tracked = new HashMap(); + trackingCount = 0; + adding = new ArrayList(6); + initial = new LinkedList(); + closed = false; + } + + /** + * Set initial list of items into tracker before events begin to be + * received. + * + * This method must be called from Tracker's open method while synchronized + * on this object in the same synchronized block as the add listener call. + * + * @param list The initial list of items to be tracked. {@code null} entries + * in the list are ignored. + * @GuardedBy this + */ + void setInitial(S[] list) { + if (list == null) { + return; + } + for (S item : list) { + if (item == null) { + continue; + } + if (DEBUG) { + System.out.println("AbstractTracked.setInitial: " + item); //$NON-NLS-1$ + } + initial.add(item); + } + } + + /** + * Track the initial list of items. This is called after events can begin to + * be received. + * + * This method must be called from Tracker's open method while not + * synchronized on this object after the add listener call. + * + */ + void trackInitial() { + while (true) { + S item; + synchronized (this) { + if (closed || (initial.size() == 0)) { + /* + * if there are no more initial items + */ + return; /* we are done */ + } + /* + * move the first item from the initial list to the adding list + * within this synchronized block. + */ + item = initial.removeFirst(); + if (tracked.get(item) != null) { + /* if we are already tracking this item */ + if (DEBUG) { + System.out.println("AbstractTracked.trackInitial[already tracked]: " + item); //$NON-NLS-1$ + } + continue; /* skip this item */ + } + if (adding.contains(item)) { + /* + * if this item is already in the process of being added. + */ + if (DEBUG) { + System.out.println("AbstractTracked.trackInitial[already adding]: " + item); //$NON-NLS-1$ + } + continue; /* skip this item */ + } + adding.add(item); + } + if (DEBUG) { + System.out.println("AbstractTracked.trackInitial: " + item); //$NON-NLS-1$ + } + trackAdding(item, null); /* + * Begin tracking it. We call trackAdding + * since we have already put the item in the + * adding list. + */ + } + } + + /** + * Called by the owning Tracker object when it is closed. + */ + void close() { + closed = true; + } + + /** + * Begin to track an item. + * + * @param item Item to be tracked. + * @param related Action related object. + */ + void track(final S item, final R related) { + final T object; + synchronized (this) { + if (closed) { + return; + } + object = tracked.get(item); + if (object == null) { /* we are not tracking the item */ + if (adding.contains(item)) { + /* if this item is already in the process of being added. */ + if (DEBUG) { + System.out.println("AbstractTracked.track[already adding]: " + item); //$NON-NLS-1$ + } + return; + } + adding.add(item); /* mark this item is being added */ + } else { /* we are currently tracking this item */ + if (DEBUG) { + System.out.println("AbstractTracked.track[modified]: " + item); //$NON-NLS-1$ + } + modified(); /* increment modification count */ + } + } + + if (object == null) { /* we are not tracking the item */ + trackAdding(item, related); + } else { + /* Call customizer outside of synchronized region */ + customizerModified(item, related, object); + /* + * If the customizer throws an unchecked exception, it is safe to + * let it propagate + */ + } + } + + /** + * Common logic to add an item to the tracker used by track and + * trackInitial. The specified item must have been placed in the adding list + * before calling this method. + * + * @param item Item to be tracked. + * @param related Action related object. + */ + private void trackAdding(final S item, final R related) { + if (DEBUG) { + System.out.println("AbstractTracked.trackAdding: " + item); //$NON-NLS-1$ + } + T object = null; + boolean becameUntracked = false; + /* Call customizer outside of synchronized region */ + try { + object = customizerAdding(item, related); + /* + * If the customizer throws an unchecked exception, it will + * propagate after the finally + */ + } finally { + synchronized (this) { + if (adding.remove(item) && !closed) { + /* + * if the item was not untracked during the customizer + * callback + */ + if (object != null) { + tracked.put(item, object); + modified(); /* increment modification count */ + notifyAll(); /* notify any waiters */ + } + } else { + becameUntracked = true; + } + } + } + /* + * The item became untracked during the customizer callback. + */ + if (becameUntracked && (object != null)) { + if (DEBUG) { + System.out.println("AbstractTracked.trackAdding[removed]: " + item); //$NON-NLS-1$ + } + /* Call customizer outside of synchronized region */ + customizerRemoved(item, related, object); + /* + * If the customizer throws an unchecked exception, it is safe to + * let it propagate + */ + } + } + + /** + * Discontinue tracking the item. + * + * @param item Item to be untracked. + * @param related Action related object. + */ + void untrack(final S item, final R related) { + final T object; + synchronized (this) { + if (initial.remove(item)) { /* + * if this item is already in the list + * of initial references to process + */ + if (DEBUG) { + System.out.println("AbstractTracked.untrack[removed from initial]: " + item); //$NON-NLS-1$ + } + return; /* + * we have removed it from the list and it will not be + * processed + */ + } + + if (adding.remove(item)) { /* + * if the item is in the process of + * being added + */ + if (DEBUG) { + System.out.println("AbstractTracked.untrack[being added]: " + item); //$NON-NLS-1$ + } + return; /* + * in case the item is untracked while in the process of + * adding + */ + } + object = tracked.remove(item); /* + * must remove from tracker before + * calling customizer callback + */ + if (object == null) { /* are we actually tracking the item */ + return; + } + modified(); /* increment modification count */ + } + if (DEBUG) { + System.out.println("AbstractTracked.untrack[removed]: " + item); //$NON-NLS-1$ + } + /* Call customizer outside of synchronized region */ + customizerRemoved(item, related, object); + /* + * If the customizer throws an unchecked exception, it is safe to let it + * propagate + */ + } + + /** + * Returns the number of tracked items. + * + * @return The number of tracked items. + * + * @GuardedBy this + */ + int size() { + return tracked.size(); + } + + /** + * Returns if the tracker is empty. + * + * @return Whether the tracker is empty. + * + * @GuardedBy this + * @since 1.5 + */ + boolean isEmpty() { + return tracked.isEmpty(); + } + + /** + * Return the customized object for the specified item + * + * @param item The item to lookup in the map + * @return The customized object for the specified item. + * + * @GuardedBy this + */ + T getCustomizedObject(final S item) { + return tracked.get(item); + } + + /** + * Copy the tracked items into an array. + * + * @param list An array to contain the tracked items. + * @return The specified list if it is large enough to hold the tracked + * items or a new array large enough to hold the tracked items. + * @GuardedBy this + */ + S[] copyKeys(final S[] list) { + return tracked.keySet().toArray(list); + } + + /** + * Increment the modification count. If this method is overridden, the + * overriding method MUST call this method to increment the tracking count. + * + * @GuardedBy this + */ + void modified() { + trackingCount++; + } + + /** + * Returns the tracking count for this {@code ServiceTracker} object. + * + * The tracking count is initialized to 0 when this object is opened. Every + * time an item is added, modified or removed from this object the tracking + * count is incremented. + * + * @GuardedBy this + * @return The tracking count for this object. + */ + int getTrackingCount() { + return trackingCount; + } + + /** + * Copy the tracked items and associated values into the specified map. + * + * @param Type of {@code Map} to hold the tracked items and associated + * values. + * @param map The map into which to copy the tracked items and associated + * values. This map must not be a user provided map so that user code + * is not executed while synchronized on this. + * @return The specified map. + * @GuardedBy this + * @since 1.5 + */ + > M copyEntries(final M map) { + map.putAll(tracked); + return map; + } + + /** + * Call the specific customizer adding method. This method must not be + * called while synchronized on this object. + * + * @param item Item to be tracked. + * @param related Action related object. + * @return Customized object for the tracked item or {@code null} if the + * item is not to be tracked. + */ + abstract T customizerAdding(final S item, final R related); + + /** + * Call the specific customizer modified method. This method must not be + * called while synchronized on this object. + * + * @param item Tracked item. + * @param related Action related object. + * @param object Customized object for the tracked item. + */ + abstract void customizerModified(final S item, final R related, final T object); + + /** + * Call the specific customizer removed method. This method must not be + * called while synchronized on this object. + * + * @param item Tracked item. + * @param related Action related object. + * @param object Customized object for the tracked item. + */ + abstract void customizerRemoved(final S item, final R related, final T object); +} diff --git a/framework/src/main/java/org/osgi/util/tracker/BundleTracker.java b/framework/src/main/java/org/osgi/util/tracker/BundleTracker.java new file mode 100644 index 00000000000..d8422ad5584 --- /dev/null +++ b/framework/src/main/java/org/osgi/util/tracker/BundleTracker.java @@ -0,0 +1,504 @@ +/* + * Copyright (c) OSGi Alliance (2007, 2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.tracker; + +import java.util.HashMap; +import java.util.Map; + +import org.osgi.annotation.versioning.ConsumerType; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.SynchronousBundleListener; + +/** + * The {@code BundleTracker} class simplifies tracking bundles much like the + * {@code ServiceTracker} simplifies tracking services. + *

      + * A {@code BundleTracker} is constructed with state criteria and a + * {@code BundleTrackerCustomizer} object. A {@code BundleTracker} can use the + * {@code BundleTrackerCustomizer} to select which bundles are tracked and to + * create a customized object to be tracked with the bundle. The + * {@code BundleTracker} can then be opened to begin tracking all bundles whose + * state matches the specified state criteria. + *

      + * The {@code getBundles} method can be called to get the {@code Bundle} objects + * of the bundles being tracked. The {@code getObject} method can be called to + * get the customized object for a tracked bundle. + *

      + * The {@code BundleTracker} class is thread-safe. It does not call a + * {@code BundleTrackerCustomizer} while holding any locks. + * {@code BundleTrackerCustomizer} implementations must also be thread-safe. + * + * @param The type of the tracked object. + * @ThreadSafe + * @author $Id: ac16c218a0df059303f253a33523f753000623c4 $ + * @since 1.4 + */ +@ConsumerType +public class BundleTracker implements BundleTrackerCustomizer { + /* set this to true to compile in debug messages */ + static final boolean DEBUG = false; + + /** + * The Bundle Context used by this {@code BundleTracker}. + */ + protected final BundleContext context; + + /** + * The {@code BundleTrackerCustomizer} object for this tracker. + */ + final BundleTrackerCustomizer customizer; + + /** + * Tracked bundles: {@code Bundle} object -> customized Object and + * {@code BundleListener} object + */ + private volatile Tracked tracked; + + /** + * Accessor method for the current Tracked object. This method is only + * intended to be used by the unsynchronized methods which do not modify the + * tracked field. + * + * @return The current Tracked object. + */ + private Tracked tracked() { + return tracked; + } + + /** + * State mask for bundles being tracked. This field contains the ORed values + * of the bundle states being tracked. + */ + final int mask; + + /** + * Create a {@code BundleTracker} for bundles whose state is present in the + * specified state mask. + * + *

      + * Bundles whose state is present on the specified state mask will be + * tracked by this {@code BundleTracker}. + * + * @param context The {@code BundleContext} against which the tracking is + * done. + * @param stateMask The bit mask of the {@code OR}ing of the bundle states + * to be tracked. + * @param customizer The customizer object to call when bundles are added, + * modified, or removed in this {@code BundleTracker}. If customizer + * is {@code null}, then this {@code BundleTracker} will be used as + * the {@code BundleTrackerCustomizer} and this {@code BundleTracker} + * will call the {@code BundleTrackerCustomizer} methods on itself. + * @see Bundle#getState() + */ + public BundleTracker(BundleContext context, int stateMask, BundleTrackerCustomizer customizer) { + this.context = context; + this.mask = stateMask; + this.customizer = (customizer == null) ? this : customizer; + } + + /** + * Open this {@code BundleTracker} and begin tracking bundles. + * + *

      + * Bundle which match the state criteria specified when this + * {@code BundleTracker} was created are now tracked by this + * {@code BundleTracker}. + * + * @throws java.lang.IllegalStateException If the {@code BundleContext} with + * which this {@code BundleTracker} was created is no longer valid. + * @throws java.lang.SecurityException If the caller and this class do not + * have the appropriate + * {@code AdminPermission[context bundle,LISTENER]}, and the Java + * Runtime Environment supports permissions. + */ + public void open() { + final Tracked t; + synchronized (this) { + if (tracked != null) { + return; + } + if (DEBUG) { + System.out.println("BundleTracker.open"); //$NON-NLS-1$ + } + t = new Tracked(); + synchronized (t) { + context.addBundleListener(t); + Bundle[] bundles = context.getBundles(); + if (bundles != null) { + int length = bundles.length; + for (int i = 0; i < length; i++) { + int state = bundles[i].getState(); + if ((state & mask) == 0) { + /* null out bundles whose states are not interesting */ + bundles[i] = null; + } + } + /* set tracked with the initial bundles */ + t.setInitial(bundles); + } + } + tracked = t; + } + /* Call tracked outside of synchronized region */ + t.trackInitial(); /* process the initial references */ + } + + /** + * Close this {@code BundleTracker}. + * + *

      + * This method should be called when this {@code BundleTracker} should end + * the tracking of bundles. + * + *

      + * This implementation calls {@link #getBundles()} to get the list of + * tracked bundles to remove. + */ + public void close() { + final Bundle[] bundles; + final Tracked outgoing; + synchronized (this) { + outgoing = tracked; + if (outgoing == null) { + return; + } + if (DEBUG) { + System.out.println("BundleTracker.close"); //$NON-NLS-1$ + } + outgoing.close(); + bundles = getBundles(); + tracked = null; + try { + context.removeBundleListener(outgoing); + } catch (IllegalStateException e) { + /* In case the context was stopped. */ + } + } + if (bundles != null) { + for (int i = 0; i < bundles.length; i++) { + outgoing.untrack(bundles[i], null); + } + } + } + + /** + * Default implementation of the + * {@code BundleTrackerCustomizer.addingBundle} method. + * + *

      + * This method is only called when this {@code BundleTracker} has been + * constructed with a {@code null BundleTrackerCustomizer} argument. + * + *

      + * This implementation simply returns the specified {@code Bundle}. + * + *

      + * This method can be overridden in a subclass to customize the object to be + * tracked for the bundle being added. + * + * @param bundle The {@code Bundle} being added to this + * {@code BundleTracker} object. + * @param event The bundle event which caused this customizer method to be + * called or {@code null} if there is no bundle event associated with + * the call to this method. + * @return The specified bundle. + * @see BundleTrackerCustomizer#addingBundle(Bundle, BundleEvent) + */ + @Override + public T addingBundle(Bundle bundle, BundleEvent event) { + @SuppressWarnings("unchecked") + T result = (T) bundle; + return result; + } + + /** + * Default implementation of the + * {@code BundleTrackerCustomizer.modifiedBundle} method. + * + *

      + * This method is only called when this {@code BundleTracker} has been + * constructed with a {@code null BundleTrackerCustomizer} argument. + * + *

      + * This implementation does nothing. + * + * @param bundle The {@code Bundle} whose state has been modified. + * @param event The bundle event which caused this customizer method to be + * called or {@code null} if there is no bundle event associated with + * the call to this method. + * @param object The customized object for the specified Bundle. + * @see BundleTrackerCustomizer#modifiedBundle(Bundle, BundleEvent, Object) + */ + @Override + public void modifiedBundle(Bundle bundle, BundleEvent event, T object) { + /* do nothing */ + } + + /** + * Default implementation of the + * {@code BundleTrackerCustomizer.removedBundle} method. + * + *

      + * This method is only called when this {@code BundleTracker} has been + * constructed with a {@code null BundleTrackerCustomizer} argument. + * + *

      + * This implementation does nothing. + * + * @param bundle The {@code Bundle} being removed. + * @param event The bundle event which caused this customizer method to be + * called or {@code null} if there is no bundle event associated with + * the call to this method. + * @param object The customized object for the specified bundle. + * @see BundleTrackerCustomizer#removedBundle(Bundle, BundleEvent, Object) + */ + @Override + public void removedBundle(Bundle bundle, BundleEvent event, T object) { + /* do nothing */ + } + + /** + * Return an array of {@code Bundle}s for all bundles being tracked by this + * {@code BundleTracker}. + * + * @return An array of {@code Bundle}s or {@code null} if no bundles are + * being tracked. + */ + public Bundle[] getBundles() { + final Tracked t = tracked(); + if (t == null) { /* if BundleTracker is not open */ + return null; + } + synchronized (t) { + if (t.isEmpty()) { + return null; + } + return t.copyKeys(new Bundle[0]); + } + } + + /** + * Returns the customized object for the specified {@code Bundle} if the + * specified bundle is being tracked by this {@code BundleTracker}. + * + * @param bundle The {@code Bundle} being tracked. + * @return The customized object for the specified {@code Bundle} or + * {@code null} if the specified {@code Bundle} is not being + * tracked. + */ + public T getObject(Bundle bundle) { + final Tracked t = tracked(); + if (t == null) { /* if BundleTracker is not open */ + return null; + } + synchronized (t) { + return t.getCustomizedObject(bundle); + } + } + + /** + * Remove a bundle from this {@code BundleTracker}. + * + * The specified bundle will be removed from this {@code BundleTracker} . If + * the specified bundle was being tracked then the + * {@code BundleTrackerCustomizer.removedBundle} method will be called for + * that bundle. + * + * @param bundle The {@code Bundle} to be removed. + */ + public void remove(Bundle bundle) { + final Tracked t = tracked(); + if (t == null) { /* if BundleTracker is not open */ + return; + } + t.untrack(bundle, null); + } + + /** + * Return the number of bundles being tracked by this {@code BundleTracker}. + * + * @return The number of bundles being tracked. + */ + public int size() { + final Tracked t = tracked(); + if (t == null) { /* if BundleTracker is not open */ + return 0; + } + synchronized (t) { + return t.size(); + } + } + + /** + * Returns the tracking count for this {@code BundleTracker}. + * + * The tracking count is initialized to 0 when this {@code BundleTracker} is + * opened. Every time a bundle is added, modified or removed from this + * {@code BundleTracker} the tracking count is incremented. + * + *

      + * The tracking count can be used to determine if this {@code BundleTracker} + * has added, modified or removed a bundle by comparing a tracking count + * value previously collected with the current tracking count value. If the + * value has not changed, then no bundle has been added, modified or removed + * from this {@code BundleTracker} since the previous tracking count was + * collected. + * + * @return The tracking count for this {@code BundleTracker} or -1 if this + * {@code BundleTracker} is not open. + */ + public int getTrackingCount() { + final Tracked t = tracked(); + if (t == null) { /* if BundleTracker is not open */ + return -1; + } + synchronized (t) { + return t.getTrackingCount(); + } + } + + /** + * Return a {@code Map} with the {@code Bundle}s and customized objects for + * all bundles being tracked by this {@code BundleTracker}. + * + * @return A {@code Map} with the {@code Bundle}s and customized objects for + * all services being tracked by this {@code BundleTracker}. If no + * bundles are being tracked, then the returned map is empty. + * @since 1.5 + */ + public Map getTracked() { + Map map = new HashMap(); + final Tracked t = tracked(); + if (t == null) { /* if BundleTracker is not open */ + return map; + } + synchronized (t) { + return t.copyEntries(map); + } + } + + /** + * Return if this {@code BundleTracker} is empty. + * + * @return {@code true} if this {@code BundleTracker} is not tracking any + * bundles. + * @since 1.5 + */ + public boolean isEmpty() { + final Tracked t = tracked(); + if (t == null) { /* if BundleTracker is not open */ + return true; + } + synchronized (t) { + return t.isEmpty(); + } + } + + /** + * Inner class which subclasses AbstractTracked. This class is the + * {@code SynchronousBundleListener} object for the tracker. + * + * @ThreadSafe + * @since 1.4 + */ + private final class Tracked extends AbstractTracked implements SynchronousBundleListener { + /** + * Tracked constructor. + */ + Tracked() { + super(); + } + + /** + * {@code BundleListener} method for the {@code BundleTracker} class. + * This method must NOT be synchronized to avoid deadlock potential. + * + * @param event {@code BundleEvent} object from the framework. + */ + @Override + public void bundleChanged(final BundleEvent event) { + /* + * Check if we had a delayed call (which could happen when we + * close). + */ + if (closed) { + return; + } + final Bundle bundle = event.getBundle(); + final int state = bundle.getState(); + if (DEBUG) { + System.out.println("BundleTracker.Tracked.bundleChanged[" + state + "]: " + bundle); //$NON-NLS-1$ //$NON-NLS-2$ + } + + if ((state & mask) != 0) { + track(bundle, event); + /* + * If the customizer throws an unchecked exception, it is safe + * to let it propagate + */ + } else { + untrack(bundle, event); + /* + * If the customizer throws an unchecked exception, it is safe + * to let it propagate + */ + } + } + + /** + * Call the specific customizer adding method. This method must not be + * called while synchronized on this object. + * + * @param item Item to be tracked. + * @param related Action related object. + * @return Customized object for the tracked item or {@code null} if the + * item is not to be tracked. + */ + @Override + T customizerAdding(final Bundle item, final BundleEvent related) { + return customizer.addingBundle(item, related); + } + + /** + * Call the specific customizer modified method. This method must not be + * called while synchronized on this object. + * + * @param item Tracked item. + * @param related Action related object. + * @param object Customized object for the tracked item. + */ + @Override + void customizerModified(final Bundle item, final BundleEvent related, final T object) { + customizer.modifiedBundle(item, related, object); + } + + /** + * Call the specific customizer removed method. This method must not be + * called while synchronized on this object. + * + * @param item Tracked item. + * @param related Action related object. + * @param object Customized object for the tracked item. + */ + @Override + void customizerRemoved(final Bundle item, final BundleEvent related, final T object) { + customizer.removedBundle(item, related, object); + } + } +} diff --git a/framework/src/main/java/org/osgi/util/tracker/BundleTrackerCustomizer.java b/framework/src/main/java/org/osgi/util/tracker/BundleTrackerCustomizer.java new file mode 100644 index 00000000000..8a62f2c82ec --- /dev/null +++ b/framework/src/main/java/org/osgi/util/tracker/BundleTrackerCustomizer.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) OSGi Alliance (2007, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.tracker; + +import org.osgi.annotation.versioning.ConsumerType; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleEvent; + +/** + * The {@code BundleTrackerCustomizer} interface allows a {@code BundleTracker} + * to customize the {@code Bundle}s that are tracked. A + * {@code BundleTrackerCustomizer} is called when a bundle is being added to a + * {@code BundleTracker}. The {@code BundleTrackerCustomizer} can then return an + * object for the tracked bundle. A {@code BundleTrackerCustomizer} is also + * called when a tracked bundle is modified or has been removed from a + * {@code BundleTracker}. + * + *

      + * The methods in this interface may be called as the result of a + * {@code BundleEvent} being received by a {@code BundleTracker}. Since + * {@code BundleEvent}s are received synchronously by the {@code BundleTracker}, + * it is highly recommended that implementations of these methods do not alter + * bundle states while being synchronized on any object. + * + *

      + * The {@code BundleTracker} class is thread-safe. It does not call a + * {@code BundleTrackerCustomizer} while holding any locks. + * {@code BundleTrackerCustomizer} implementations must also be thread-safe. + * + * @param The type of the tracked object. + * @ThreadSafe + * @author $Id: 031b2979522768150d23ee70dfe62528432c19f7 $ + * @since 1.4 + */ +@ConsumerType +public interface BundleTrackerCustomizer { + /** + * A bundle is being added to the {@code BundleTracker}. + * + *

      + * This method is called before a bundle which matched the search parameters + * of the {@code BundleTracker} is added to the {@code BundleTracker}. This + * method should return the object to be tracked for the specified + * {@code Bundle}. The returned object is stored in the + * {@code BundleTracker} and is available from the + * {@link BundleTracker#getObject(Bundle) getObject} method. + * + * @param bundle The {@code Bundle} being added to the {@code BundleTracker} + * . + * @param event The bundle event which caused this customizer method to be + * called or {@code null} if there is no bundle event associated with + * the call to this method. + * @return The object to be tracked for the specified {@code Bundle} object + * or {@code null} if the specified {@code Bundle} object should not + * be tracked. + */ + public T addingBundle(Bundle bundle, BundleEvent event); + + /** + * A bundle tracked by the {@code BundleTracker} has been modified. + * + *

      + * This method is called when a bundle being tracked by the + * {@code BundleTracker} has had its state modified. + * + * @param bundle The {@code Bundle} whose state has been modified. + * @param event The bundle event which caused this customizer method to be + * called or {@code null} if there is no bundle event associated with + * the call to this method. + * @param object The tracked object for the specified bundle. + */ + public void modifiedBundle(Bundle bundle, BundleEvent event, T object); + + /** + * A bundle tracked by the {@code BundleTracker} has been removed. + * + *

      + * This method is called after a bundle is no longer being tracked by the + * {@code BundleTracker}. + * + * @param bundle The {@code Bundle} that has been removed. + * @param event The bundle event which caused this customizer method to be + * called or {@code null} if there is no bundle event associated with + * the call to this method. + * @param object The tracked object for the specified bundle. + */ + public void removedBundle(Bundle bundle, BundleEvent event, T object); +} diff --git a/framework/src/main/java/org/osgi/util/tracker/ServiceTracker.java b/framework/src/main/java/org/osgi/util/tracker/ServiceTracker.java new file mode 100644 index 00000000000..42c176c90f8 --- /dev/null +++ b/framework/src/main/java/org/osgi/util/tracker/ServiceTracker.java @@ -0,0 +1,988 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2017). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.tracker; + +import java.lang.reflect.Array; +import java.util.Collections; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.osgi.annotation.versioning.ConsumerType; +import org.osgi.framework.AllServiceListener; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; + +/** + * The {@code ServiceTracker} class simplifies using services from the + * Framework's service registry. + *

      + * A {@code ServiceTracker} object is constructed with search criteria and a + * {@code ServiceTrackerCustomizer} object. A {@code ServiceTracker} can use a + * {@code ServiceTrackerCustomizer} to customize the service objects to be + * tracked. The {@code ServiceTracker} can then be opened to begin tracking all + * services in the Framework's service registry that match the specified search + * criteria. The {@code ServiceTracker} correctly handles all of the details of + * listening to {@code ServiceEvent}s and getting and ungetting services. + *

      + * The {@code getServiceReferences} method can be called to get references to + * the services being tracked. The {@code getService} and {@code getServices} + * methods can be called to get the service objects for the tracked service. + *

      + * The {@code ServiceTracker} class is thread-safe. It does not call a + * {@code ServiceTrackerCustomizer} while holding any locks. + * {@code ServiceTrackerCustomizer} implementations must also be thread-safe. + * + * @param The type of the service being tracked. + * @param The type of the tracked object. + * @ThreadSafe + * @author $Id: 3c9016c43c6289259f97470eff4c9986b6fb887a $ + */ +@ConsumerType +public class ServiceTracker implements ServiceTrackerCustomizer { + /* set this to true to compile in debug messages */ + static final boolean DEBUG = false; + /** + * The Bundle Context used by this {@code ServiceTracker}. + */ + protected final BundleContext context; + /** + * The Filter used by this {@code ServiceTracker} which specifies the search + * criteria for the services to track. + * + * @since 1.1 + */ + protected final Filter filter; + /** + * The {@code ServiceTrackerCustomizer} for this tracker. + */ + final ServiceTrackerCustomizer customizer; + /** + * Filter string for use when adding the ServiceListener. If this field is + * set, then certain optimizations can be taken since we don't have a user + * supplied filter. + */ + final String listenerFilter; + /** + * Class name to be tracked. If this field is set, then we are tracking by + * class name. + */ + private final String trackClass; + /** + * Reference to be tracked. If this field is set, then we are tracking a + * single ServiceReference. + */ + private final ServiceReference trackReference; + /** + * Tracked services: {@code ServiceReference} -> customized Object and + * {@code ServiceListener} object + */ + private volatile Tracked tracked; + + /** + * Accessor method for the current Tracked object. This method is only + * intended to be used by the unsynchronized methods which do not modify the + * tracked field. + * + * @return The current Tracked object. + */ + private Tracked tracked() { + return tracked; + } + + /** + * Cached ServiceReference for getServiceReference. + * + * This field is volatile since it is accessed by multiple threads. + */ + private volatile ServiceReference cachedReference; + /** + * Cached service object for getService. + * + * This field is volatile since it is accessed by multiple threads. + */ + private volatile T cachedService; + + /** + * Create a {@code ServiceTracker} on the specified {@code ServiceReference} + * . + * + *

      + * The service referenced by the specified {@code ServiceReference} will be + * tracked by this {@code ServiceTracker}. + * + * @param context The {@code BundleContext} against which the tracking is + * done. + * @param reference The {@code ServiceReference} for the service to be + * tracked. + * @param customizer The customizer object to call when services are added, + * modified, or removed in this {@code ServiceTracker}. If customizer + * is {@code null}, then this {@code ServiceTracker} will be used as + * the {@code ServiceTrackerCustomizer} and this + * {@code ServiceTracker} will call the + * {@code ServiceTrackerCustomizer} methods on itself. + */ + public ServiceTracker(final BundleContext context, final ServiceReference reference, final ServiceTrackerCustomizer customizer) { + this.context = context; + this.trackReference = reference; + this.trackClass = null; + this.customizer = (customizer == null) ? this : customizer; + this.listenerFilter = "(" + Constants.SERVICE_ID + "=" + reference.getProperty(Constants.SERVICE_ID).toString() + ")"; + try { + this.filter = context.createFilter(listenerFilter); + } catch (InvalidSyntaxException e) { + /* + * we could only get this exception if the ServiceReference was + * invalid + */ + IllegalArgumentException iae = new IllegalArgumentException("unexpected InvalidSyntaxException: " + e.getMessage()); + iae.initCause(e); + throw iae; + } + } + + /** + * Create a {@code ServiceTracker} on the specified class name. + * + *

      + * Services registered under the specified class name will be tracked by + * this {@code ServiceTracker}. + * + * @param context The {@code BundleContext} against which the tracking is + * done. + * @param clazz The class name of the services to be tracked. + * @param customizer The customizer object to call when services are added, + * modified, or removed in this {@code ServiceTracker}. If customizer + * is {@code null}, then this {@code ServiceTracker} will be used as + * the {@code ServiceTrackerCustomizer} and this + * {@code ServiceTracker} will call the + * {@code ServiceTrackerCustomizer} methods on itself. + */ + public ServiceTracker(final BundleContext context, final String clazz, final ServiceTrackerCustomizer customizer) { + this.context = context; + this.trackReference = null; + this.trackClass = clazz; + this.customizer = (customizer == null) ? this : customizer; + // we call clazz.toString to verify clazz is non-null! + this.listenerFilter = "(" + Constants.OBJECTCLASS + "=" + clazz.toString() + ")"; + try { + this.filter = context.createFilter(listenerFilter); + } catch (InvalidSyntaxException e) { + /* + * we could only get this exception if the clazz argument was + * malformed + */ + IllegalArgumentException iae = new IllegalArgumentException("unexpected InvalidSyntaxException: " + e.getMessage()); + iae.initCause(e); + throw iae; + } + } + + /** + * Create a {@code ServiceTracker} on the specified {@code Filter} object. + * + *

      + * Services which match the specified {@code Filter} object will be tracked + * by this {@code ServiceTracker}. + * + * @param context The {@code BundleContext} against which the tracking is + * done. + * @param filter The {@code Filter} to select the services to be tracked. + * @param customizer The customizer object to call when services are added, + * modified, or removed in this {@code ServiceTracker}. If customizer + * is null, then this {@code ServiceTracker} will be used as the + * {@code ServiceTrackerCustomizer} and this {@code ServiceTracker} + * will call the {@code ServiceTrackerCustomizer} methods on itself. + * @since 1.1 + */ + public ServiceTracker(final BundleContext context, final Filter filter, final ServiceTrackerCustomizer customizer) { + this.context = context; + this.trackReference = null; + this.trackClass = null; + this.listenerFilter = filter.toString(); + this.filter = filter; + this.customizer = (customizer == null) ? this : customizer; + if ((context == null) || (filter == null)) { + /* + * we throw a NPE here to be consistent with the other constructors + */ + throw new NullPointerException(); + } + } + + /** + * Create a {@code ServiceTracker} on the specified class. + * + *

      + * Services registered under the name of the specified class will be tracked + * by this {@code ServiceTracker}. + * + * @param context The {@code BundleContext} against which the tracking is + * done. + * @param clazz The class of the services to be tracked. + * @param customizer The customizer object to call when services are added, + * modified, or removed in this {@code ServiceTracker}. If customizer + * is {@code null}, then this {@code ServiceTracker} will be used as + * the {@code ServiceTrackerCustomizer} and this + * {@code ServiceTracker} will call the + * {@code ServiceTrackerCustomizer} methods on itself. + * @since 1.5 + */ + public ServiceTracker(final BundleContext context, final Class clazz, final ServiceTrackerCustomizer customizer) { + this(context, clazz.getName(), customizer); + } + + /** + * Open this {@code ServiceTracker} and begin tracking services. + * + *

      + * This implementation calls {@code open(false)}. + * + * @throws java.lang.IllegalStateException If the {@code BundleContext} with + * which this {@code ServiceTracker} was created is no longer valid. + * @see #open(boolean) + */ + public void open() { + open(false); + } + + /** + * Open this {@code ServiceTracker} and begin tracking services. + * + *

      + * Services which match the search criteria specified when this + * {@code ServiceTracker} was created are now tracked by this + * {@code ServiceTracker}. + * + * @param trackAllServices If {@code true}, then this {@code ServiceTracker} + * will track all matching services regardless of class loader + * accessibility. If {@code false}, then this {@code ServiceTracker} + * will only track matching services which are class loader + * accessible to the bundle whose {@code BundleContext} is used by + * this {@code ServiceTracker}. + * @throws java.lang.IllegalStateException If the {@code BundleContext} with + * which this {@code ServiceTracker} was created is no longer valid. + * @since 1.3 + */ + public void open(boolean trackAllServices) { + final Tracked t; + synchronized (this) { + if (tracked != null) { + return; + } + if (DEBUG) { + System.out.println("ServiceTracker.open: " + filter); + } + t = trackAllServices ? new AllTracked() : new Tracked(); + synchronized (t) { + try { + context.addServiceListener(t, listenerFilter); + ServiceReference[] references = null; + if (trackClass != null) { + references = getInitialReferences(trackAllServices, trackClass, null); + } else { + if (trackReference != null) { + if (trackReference.getBundle() != null) { + @SuppressWarnings("unchecked") + ServiceReference[] single = new ServiceReference[] {trackReference}; + references = single; + } + } else { /* user supplied filter */ + references = getInitialReferences(trackAllServices, null, listenerFilter); + } + } + /* set tracked with the initial references */ + t.setInitial(references); + } catch (InvalidSyntaxException e) { + throw new RuntimeException("unexpected InvalidSyntaxException: " + e.getMessage(), e); + } + } + tracked = t; + } + /* Call tracked outside of synchronized region */ + t.trackInitial(); /* process the initial references */ + } + + /** + * Returns the list of initial {@code ServiceReference}s that will be + * tracked by this {@code ServiceTracker}. + * + * @param trackAllServices If {@code true}, use + * {@code getAllServiceReferences}. + * @param className The class name with which the service was registered, or + * {@code null} for all services. + * @param filterString The filter criteria or {@code null} for all services. + * @return The list of initial {@code ServiceReference}s. + * @throws InvalidSyntaxException If the specified filterString has an + * invalid syntax. + */ + private ServiceReference[] getInitialReferences(boolean trackAllServices, String className, String filterString) throws InvalidSyntaxException { + @SuppressWarnings("unchecked") + ServiceReference[] result = (ServiceReference[]) ((trackAllServices) ? context.getAllServiceReferences(className, filterString) : context.getServiceReferences(className, filterString)); + return result; + } + + /** + * Close this {@code ServiceTracker}. + * + *

      + * This method should be called when this {@code ServiceTracker} should end + * the tracking of services. + * + *

      + * This implementation calls {@link #getServiceReferences()} to get the list + * of tracked services to remove. + */ + public void close() { + final Tracked outgoing; + final ServiceReference[] references; + synchronized (this) { + outgoing = tracked; + if (outgoing == null) { + return; + } + if (DEBUG) { + System.out.println("ServiceTracker.close: " + filter); + } + outgoing.close(); + references = getServiceReferences(); + tracked = null; + try { + context.removeServiceListener(outgoing); + } catch (IllegalStateException e) { + /* In case the context was stopped. */ + } + } + modified(); /* clear the cache */ + synchronized (outgoing) { + outgoing.notifyAll(); /* wake up any waiters */ + } + if (references != null) { + for (int i = 0; i < references.length; i++) { + outgoing.untrack(references[i], null); + } + } + if (DEBUG) { + if ((cachedReference == null) && (cachedService == null)) { + System.out.println("ServiceTracker.close[cached cleared]: " + filter); + } + } + } + + /** + * Default implementation of the + * {@code ServiceTrackerCustomizer.addingService} method. + *

      + * This method is only called when this {@code ServiceTracker} has been + * constructed with a {@code null ServiceTrackerCustomizer} argument. + *

      + * This implementation returns the result of calling {@code getService}, on + * the {@code BundleContext} with which this {@code ServiceTracker} was + * created, passing the specified {@code ServiceReference}. + *

      + * This method can be overridden in a subclass to customize the service + * object to be tracked for the service being added. In that case, take care + * not to rely on the default implementation of + * {@link #removedService(ServiceReference, Object) removedService} to unget + * the service. + * + * @param reference The reference to the service being added to this + * {@code ServiceTracker}. + * @return The service object to be tracked for the service added to this + * {@code ServiceTracker}. + * @see ServiceTrackerCustomizer#addingService(ServiceReference) + */ + @Override + public T addingService(ServiceReference reference) { + @SuppressWarnings("unchecked") + T result = (T) context.getService(reference); + return result; + } + + /** + * Default implementation of the + * {@code ServiceTrackerCustomizer.modifiedService} method. + * + *

      + * This method is only called when this {@code ServiceTracker} has been + * constructed with a {@code null ServiceTrackerCustomizer} argument. + * + *

      + * This implementation does nothing. + * + * @param reference The reference to modified service. + * @param service The service object for the modified service. + * @see ServiceTrackerCustomizer#modifiedService(ServiceReference, Object) + */ + @Override + public void modifiedService(ServiceReference reference, T service) { + /* do nothing */ + } + + /** + * Default implementation of the + * {@code ServiceTrackerCustomizer.removedService} method. + *

      + * This method is only called when this {@code ServiceTracker} has been + * constructed with a {@code null ServiceTrackerCustomizer} argument. + *

      + * This implementation calls {@code ungetService}, on the + * {@code BundleContext} with which this {@code ServiceTracker} was created, + * passing the specified {@code ServiceReference}. + *

      + * This method can be overridden in a subclass. If the default + * implementation of {@link #addingService(ServiceReference) addingService} + * method was used, this method must unget the service. + * + * @param reference The reference to removed service. + * @param service The service object for the removed service. + * @see ServiceTrackerCustomizer#removedService(ServiceReference, Object) + */ + @Override + public void removedService(ServiceReference reference, T service) { + context.ungetService(reference); + } + + /** + * Wait for at least one service to be tracked by this + * {@code ServiceTracker}. This method will also return when this + * {@code ServiceTracker} is closed. + * + *

      + * It is strongly recommended that {@code waitForService} is not used during + * the calling of the {@code BundleActivator} methods. + * {@code BundleActivator} methods are expected to complete in a short + * period of time. + * + *

      + * This implementation calls {@link #getService()} to determine if a service + * is being tracked. + * + * @param timeout The time interval in milliseconds to wait. If zero, the + * method will wait indefinitely. + * @return Returns the result of {@link #getService()}. + * @throws InterruptedException If another thread has interrupted the + * current thread. + * @throws IllegalArgumentException If the value of timeout is negative. + */ + public T waitForService(long timeout) throws InterruptedException { + if (timeout < 0) { + throw new IllegalArgumentException("timeout value is negative"); + } + + T object = getService(); + if (object != null) { + return object; + } + + final long endTime = (timeout == 0) ? 0 : (System.currentTimeMillis() + timeout); + do { + final Tracked t = tracked(); + if (t == null) { /* if ServiceTracker is not open */ + return null; + } + synchronized (t) { + if (t.size() == 0) { + t.wait(timeout); + } + } + object = getService(); + if (endTime > 0) { // if we have a timeout + timeout = endTime - System.currentTimeMillis(); + if (timeout <= 0) { // that has expired + break; + } + } + } while (object == null); + return object; + } + + /** + * Return an array of {@code ServiceReference}s for all services being + * tracked by this {@code ServiceTracker}. + * + * @return Array of {@code ServiceReference}s or {@code null} if no services + * are being tracked. + */ + public ServiceReference[] getServiceReferences() { + final Tracked t = tracked(); + if (t == null) { /* if ServiceTracker is not open */ + return null; + } + synchronized (t) { + if (t.isEmpty()) { + return null; + } + @SuppressWarnings("unchecked") + ServiceReference[] result = new ServiceReference[0]; + return t.copyKeys(result); + } + } + + /** + * Returns a {@code ServiceReference} for one of the services being tracked + * by this {@code ServiceTracker}. + * + *

      + * If multiple services are being tracked, the service with the highest + * ranking (as specified in its {@code service.ranking} property) is + * returned. If there is a tie in ranking, the service with the lowest + * service id (as specified in its {@code service.id} property); that is, + * the service that was registered first is returned. This is the same + * algorithm used by {@code BundleContext.getServiceReference}. + * + *

      + * This implementation calls {@link #getServiceReferences()} to get the list + * of references for the tracked services. + * + * @return A {@code ServiceReference} or {@code null} if no services are + * being tracked. + * @since 1.1 + */ + public ServiceReference getServiceReference() { + ServiceReference reference = cachedReference; + if (reference != null) { + if (DEBUG) { + System.out.println("ServiceTracker.getServiceReference[cached]: " + filter); + } + return reference; + } + if (DEBUG) { + System.out.println("ServiceTracker.getServiceReference: " + filter); + } + ServiceReference[] references = getServiceReferences(); + int length = (references == null) ? 0 : references.length; + if (length == 0) { /* if no service is being tracked */ + return null; + } + int index = 0; + if (length > 1) { /* if more than one service, select highest ranking */ + int rankings[] = new int[length]; + int count = 0; + int maxRanking = Integer.MIN_VALUE; + for (int i = 0; i < length; i++) { + Object property = references[i].getProperty(Constants.SERVICE_RANKING); + int ranking = (property instanceof Integer) ? ((Integer) property).intValue() : 0; + rankings[i] = ranking; + if (ranking > maxRanking) { + index = i; + maxRanking = ranking; + count = 1; + } else { + if (ranking == maxRanking) { + count++; + } + } + } + if (count > 1) { /* if still more than one service, select lowest id */ + long minId = Long.MAX_VALUE; + for (int i = 0; i < length; i++) { + if (rankings[i] == maxRanking) { + long id = ((Long) (references[i].getProperty(Constants.SERVICE_ID))).longValue(); + if (id < minId) { + index = i; + minId = id; + } + } + } + } + } + return cachedReference = references[index]; + } + + /** + * Returns the service object for the specified {@code ServiceReference} if + * the specified referenced service is being tracked by this + * {@code ServiceTracker}. + * + * @param reference The reference to the desired service. + * @return A service object or {@code null} if the service referenced by the + * specified {@code ServiceReference} is not being tracked. + */ + public T getService(ServiceReference reference) { + final Tracked t = tracked(); + if (t == null) { /* if ServiceTracker is not open */ + return null; + } + synchronized (t) { + return t.getCustomizedObject(reference); + } + } + + /** + * Return an array of service objects for all services being tracked by this + * {@code ServiceTracker}. + * + *

      + * This implementation calls {@link #getServiceReferences()} to get the list + * of references for the tracked services and then calls + * {@link #getService(ServiceReference)} for each reference to get the + * tracked service object. + * + * @return An array of service objects or {@code null} if no services are + * being tracked. + */ + public Object[] getServices() { + final Tracked t = tracked(); + if (t == null) { /* if ServiceTracker is not open */ + return null; + } + synchronized (t) { + ServiceReference[] references = getServiceReferences(); + int length = (references == null) ? 0 : references.length; + if (length == 0) { + return null; + } + Object[] objects = new Object[length]; + for (int i = 0; i < length; i++) { + objects[i] = getService(references[i]); + } + return objects; + } + } + + /** + * Returns a service object for one of the services being tracked by this + * {@code ServiceTracker}. + * + *

      + * If any services are being tracked, this implementation returns the result + * of calling {@code getService(getServiceReference())}. + * + * @return A service object or {@code null} if no services are being + * tracked. + */ + public T getService() { + T service = cachedService; + if (service != null) { + if (DEBUG) { + System.out.println("ServiceTracker.getService[cached]: " + filter); + } + return service; + } + if (DEBUG) { + System.out.println("ServiceTracker.getService: " + filter); + } + ServiceReference reference = getServiceReference(); + if (reference == null) { + return null; + } + return cachedService = getService(reference); + } + + /** + * Remove a service from this {@code ServiceTracker}. + * + * The specified service will be removed from this {@code ServiceTracker}. + * If the specified service was being tracked then the + * {@code ServiceTrackerCustomizer.removedService} method will be called for + * that service. + * + * @param reference The reference to the service to be removed. + */ + public void remove(ServiceReference reference) { + final Tracked t = tracked(); + if (t == null) { /* if ServiceTracker is not open */ + return; + } + t.untrack(reference, null); + } + + /** + * Return the number of services being tracked by this + * {@code ServiceTracker}. + * + * @return The number of services being tracked. + */ + public int size() { + final Tracked t = tracked(); + if (t == null) { /* if ServiceTracker is not open */ + return 0; + } + synchronized (t) { + return t.size(); + } + } + + /** + * Returns the tracking count for this {@code ServiceTracker}. + * + * The tracking count is initialized to 0 when this {@code ServiceTracker} + * is opened. Every time a service is added, modified or removed from this + * {@code ServiceTracker}, the tracking count is incremented. + * + *

      + * The tracking count can be used to determine if this + * {@code ServiceTracker} has added, modified or removed a service by + * comparing a tracking count value previously collected with the current + * tracking count value. If the value has not changed, then no service has + * been added, modified or removed from this {@code ServiceTracker} since + * the previous tracking count was collected. + * + * @since 1.2 + * @return The tracking count for this {@code ServiceTracker} or -1 if this + * {@code ServiceTracker} is not open. + */ + public int getTrackingCount() { + final Tracked t = tracked(); + if (t == null) { /* if ServiceTracker is not open */ + return -1; + } + synchronized (t) { + return t.getTrackingCount(); + } + } + + /** + * Called by the Tracked object whenever the set of tracked services is + * modified. Clears the cache. + */ + /* + * This method must not be synchronized since it is called by Tracked while + * Tracked is synchronized. We don't want synchronization interactions + * between the listener thread and the user thread. + */ + void modified() { + cachedReference = null; /* clear cached value */ + cachedService = null; /* clear cached value */ + if (DEBUG) { + System.out.println("ServiceTracker.modified: " + filter); + } + } + + /** + * Return a {@code SortedMap} of the {@code ServiceReference}s and service + * objects for all services being tracked by this {@code ServiceTracker}. + * The map is sorted in reverse natural order of {@code ServiceReference}. + * That is, the first entry is the service with the highest ranking and the + * lowest service id. + * + * @return A {@code SortedMap} with the {@code ServiceReference}s and + * service objects for all services being tracked by this + * {@code ServiceTracker}. If no services are being tracked, then + * the returned map is empty. + * @since 1.5 + */ + public SortedMap, T> getTracked() { + SortedMap, T> map = new TreeMap, T>(Collections.reverseOrder()); + final Tracked t = tracked(); + if (t == null) { /* if ServiceTracker is not open */ + return map; + } + synchronized (t) { + return t.copyEntries(map); + } + } + + /** + * Return if this {@code ServiceTracker} is empty. + * + * @return {@code true} if this {@code ServiceTracker} is not tracking any + * services. + * @since 1.5 + */ + public boolean isEmpty() { + final Tracked t = tracked(); + if (t == null) { /* if ServiceTracker is not open */ + return true; + } + synchronized (t) { + return t.isEmpty(); + } + } + + /** + * Return an array of service objects for all services being tracked by this + * {@code ServiceTracker}. The runtime type of the returned array is that of + * the specified array. + * + *

      + * This implementation calls {@link #getServiceReferences()} to get the list + * of references for the tracked services and then calls + * {@link #getService(ServiceReference)} for each reference to get the + * tracked service object. + * + * @param array An array into which the tracked service objects will be + * stored, if the array is large enough. + * @return An array of service objects being tracked. If the specified array + * is large enough to hold the result, then the specified array is + * returned. If the specified array is longer then necessary to hold + * the result, the array element after the last service object is + * set to {@code null}. If the specified array is not large enough + * to hold the result, a new array is created and returned. + * @since 1.5 + */ + public T[] getServices(T[] array) { + final Tracked t = tracked(); + if (t == null) { /* if ServiceTracker is not open */ + if (array.length > 0) { + array[0] = null; + } + return array; + } + synchronized (t) { + ServiceReference[] references = getServiceReferences(); + int length = (references == null) ? 0 : references.length; + if (length == 0) { + if (array.length > 0) { + array[0] = null; + } + return array; + } + if (length > array.length) { + @SuppressWarnings("unchecked") + T[] newInstance = (T[]) Array.newInstance(array.getClass().getComponentType(), length); + array = newInstance; + } + for (int i = 0; i < length; i++) { + array[i] = getService(references[i]); + } + if (array.length > length) { + array[length] = null; + } + return array; + } + } + + /** + * Inner class which subclasses AbstractTracked. This class is the + * {@code ServiceListener} object for the tracker. + * + * @ThreadSafe + */ + private class Tracked extends AbstractTracked, T, ServiceEvent> implements ServiceListener { + /** + * Tracked constructor. + */ + Tracked() { + super(); + } + + /** + * {@code ServiceListener} method for the {@code ServiceTracker} class. + * This method must NOT be synchronized to avoid deadlock potential. + * + * @param event {@code ServiceEvent} object from the framework. + */ + @Override + final public void serviceChanged(final ServiceEvent event) { + /* + * Check if we had a delayed call (which could happen when we + * close). + */ + if (closed) { + return; + } + @SuppressWarnings("unchecked") + final ServiceReference reference = (ServiceReference) event.getServiceReference(); + if (DEBUG) { + System.out.println("ServiceTracker.Tracked.serviceChanged[" + event.getType() + "]: " + reference); + } + + switch (event.getType()) { + case ServiceEvent.REGISTERED : + case ServiceEvent.MODIFIED : + track(reference, event); + /* + * If the customizer throws an unchecked exception, it is + * safe to let it propagate + */ + break; + case ServiceEvent.MODIFIED_ENDMATCH : + case ServiceEvent.UNREGISTERING : + untrack(reference, event); + /* + * If the customizer throws an unchecked exception, it is + * safe to let it propagate + */ + break; + } + } + + /** + * Increment the tracking count and tell the tracker there was a + * modification. + * + * @GuardedBy this + */ + @Override + final void modified() { + super.modified(); /* increment the modification count */ + ServiceTracker.this.modified(); + } + + /** + * Call the specific customizer adding method. This method must not be + * called while synchronized on this object. + * + * @param item Item to be tracked. + * @param related Action related object. + * @return Customized object for the tracked item or {@code null} if the + * item is not to be tracked. + */ + @Override + final T customizerAdding(final ServiceReference item, final ServiceEvent related) { + return customizer.addingService(item); + } + + /** + * Call the specific customizer modified method. This method must not be + * called while synchronized on this object. + * + * @param item Tracked item. + * @param related Action related object. + * @param object Customized object for the tracked item. + */ + @Override + final void customizerModified(final ServiceReference item, final ServiceEvent related, final T object) { + customizer.modifiedService(item, object); + } + + /** + * Call the specific customizer removed method. This method must not be + * called while synchronized on this object. + * + * @param item Tracked item. + * @param related Action related object. + * @param object Customized object for the tracked item. + */ + @Override + final void customizerRemoved(final ServiceReference item, final ServiceEvent related, final T object) { + customizer.removedService(item, object); + } + } + + /** + * Subclass of Tracked which implements the AllServiceListener interface. + * This class is used by the ServiceTracker if open is called with true. + * + * @since 1.3 + * @ThreadSafe + */ + private class AllTracked extends Tracked implements AllServiceListener { + /** + * AllTracked constructor. + */ + AllTracked() { + super(); + } + } +} diff --git a/framework/src/main/java/org/osgi/util/tracker/ServiceTrackerCustomizer.java b/framework/src/main/java/org/osgi/util/tracker/ServiceTrackerCustomizer.java new file mode 100644 index 00000000000..6aa95cb5820 --- /dev/null +++ b/framework/src/main/java/org/osgi/util/tracker/ServiceTrackerCustomizer.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) OSGi Alliance (2000, 2013). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.osgi.util.tracker; + +import org.osgi.annotation.versioning.ConsumerType; +import org.osgi.framework.ServiceReference; + +/** + * The {@code ServiceTrackerCustomizer} interface allows a + * {@code ServiceTracker} to customize the service objects that are tracked. A + * {@code ServiceTrackerCustomizer} is called when a service is being added to a + * {@code ServiceTracker}. The {@code ServiceTrackerCustomizer} can then return + * an object for the tracked service. A {@code ServiceTrackerCustomizer} is also + * called when a tracked service is modified or has been removed from a + * {@code ServiceTracker}. + * + *

      + * The methods in this interface may be called as the result of a + * {@code ServiceEvent} being received by a {@code ServiceTracker}. Since + * {@code ServiceEvent}s are synchronously delivered by the Framework, it is + * highly recommended that implementations of these methods do not register ( + * {@code BundleContext.registerService}), modify ( + * {@code ServiceRegistration.setProperties}) or unregister ( + * {@code ServiceRegistration.unregister}) a service while being synchronized on + * any object. + * + *

      + * The {@code ServiceTracker} class is thread-safe. It does not call a + * {@code ServiceTrackerCustomizer} while holding any locks. + * {@code ServiceTrackerCustomizer} implementations must also be thread-safe. + * + * @param The type of the service being tracked. + * @param The type of the tracked object. + * @ThreadSafe + * @author $Id: 0c3333455f7d80a7793c77ac9671baa4a02a89b9 $ + */ +@ConsumerType +public interface ServiceTrackerCustomizer { + /** + * A service is being added to the {@code ServiceTracker}. + * + *

      + * This method is called before a service which matched the search + * parameters of the {@code ServiceTracker} is added to the + * {@code ServiceTracker}. This method should return the service object to + * be tracked for the specified {@code ServiceReference}. The returned + * service object is stored in the {@code ServiceTracker} and is available + * from the {@code getService} and {@code getServices} methods. + * + * @param reference The reference to the service being added to the + * {@code ServiceTracker}. + * @return The service object to be tracked for the specified referenced + * service or {@code null} if the specified referenced service + * should not be tracked. + */ + public T addingService(ServiceReference reference); + + /** + * A service tracked by the {@code ServiceTracker} has been modified. + * + *

      + * This method is called when a service being tracked by the + * {@code ServiceTracker} has had it properties modified. + * + * @param reference The reference to the service that has been modified. + * @param service The service object for the specified referenced service. + */ + public void modifiedService(ServiceReference reference, T service); + + /** + * A service tracked by the {@code ServiceTracker} has been removed. + * + *

      + * This method is called after a service is no longer being tracked by the + * {@code ServiceTracker}. + * + * @param reference The reference to the service that has been removed. + * @param service The service object for the specified referenced service. + */ + public void removedService(ServiceReference reference, T service); +} diff --git a/framework/src/main/java/org/osgi/util/tracker/package-info.java b/framework/src/main/java/org/osgi/util/tracker/package-info.java new file mode 100644 index 00000000000..b72e066d2d6 --- /dev/null +++ b/framework/src/main/java/org/osgi/util/tracker/package-info.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) OSGi Alliance (2010, 2016). All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tracker Package Version 1.5. + * + *

      + * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. + * + *

      + * Example import for consumers using the API in this package: + *

      + * {@code Import-Package: org.osgi.util.tracker; version="[1.5,2.0)"} + * + * @author $Id$ + */ + +@Version("1.5.2") +package org.osgi.util.tracker; + +import org.osgi.annotation.versioning.Version; + diff --git a/framework/src/main/resources/META-INF/services/org.osgi.framework.launch.FrameworkFactory b/framework/src/main/resources/META-INF/services/org.osgi.framework.launch.FrameworkFactory new file mode 100644 index 00000000000..425043bad1e --- /dev/null +++ b/framework/src/main/resources/META-INF/services/org.osgi.framework.launch.FrameworkFactory @@ -0,0 +1 @@ +org.apache.felix.framework.FrameworkFactory diff --git a/framework/src/main/resources/default.properties b/framework/src/main/resources/default.properties new file mode 100644 index 00000000000..f6b4821caaf --- /dev/null +++ b/framework/src/main/resources/default.properties @@ -0,0 +1,377 @@ +# 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. + +# +# Framework config properties. +# + +# New-style generic execution environment capabilities. +org.osgi.framework.system.capabilities= \ + ${dollar}{felix.service.caps} ${dollar}{eecap-${dollar}{java.specification.version}} ${dollar}{eecap-${dollar}{felix.detect.jpms}} + +# Native Processor Aliases. Format is felix.native.processor.alias.=alias1,alias2 +felix.native.processor.alias.68k= +felix.native.processor.alias.arm= +felix.native.processor.alias.arm_le= +felix.native.processor.alias.arm_be= +felix.native.processor.alias.alpha= +felix.native.processor.alias.ignite=psc1k +felix.native.processor.alias.mips= +felix.native.processor.alias.parisc= +felix.native.processor.alias.powerpc=power,ppc +felix.native.processor.alias.x86=pentium,i386,i486,i586,i686 +felix.native.processor.alias.x86-64=amd64,em64t,x86_64 +felix.native.processor.alias.sparc= + +# Native Operating System Name Aliases. Format is felix.native.osname.alias.=alias1,alias2 +felix.native.osname.alias.aix= +felix.native.osname.alias.digitalunix= +felix.native.osname.alias.epoc32=symbianos +felix.native.osname.alias.hpux=hp-ux +felix.native.osname.alias.irix= +felix.native.osname.alias.linux= +felix.native.osname.alias.macos=mac os +felix.native.osname.alias.macosx=mac os x +felix.native.osname.alias.netbsd= +felix.native.osname.alias.netware= +felix.native.osname.alias.openbsd= +felix.native.osname.alias.os2=os/2 +felix.native.osname.alias.qnx=procnto +felix.native.osname.alias.solaris= +felix.native.osname.alias.sunos= +felix.native.osname.alias.vxworks= +felix.native.osname.alias.windows95=win95,windows 95,win32 +felix.native.osname.alias.windows98=win98,windows 98,win32 +felix.native.osname.alias.windowsnt=winnt,windows nt,win32 +felix.native.osname.alias.windowsce=wince,windows ce +felix.native.osname.alias.windows2000=win2000,windows 2000,win32 +felix.native.osname.alias.windows2003=win2003,windows 2003,win32,windows server 2003 +felix.native.osname.alias.windowsxp=winxp,windows xp,win32 +felix.native.osname.alias.windowsvista=winvista,windows vista,win32 +felix.native.osname.alias.windows7=windows 7,win32 +felix.native.osname.alias.windows8=windows 8,win32 +felix.native.osname.alias.windows9=windows 9,win32 +felix.native.osname.alias.windows10=windows 10,win32 +felix.native.osname.alias.windowsserver2008=windows server 2008,win32 +felix.native.osname.alias.windowsserver2012=windows server 2012,win32 +felix.native.osname.alias.windowsserver2016=windows server 2016,win32 +felix.native.osname.alias.win32= + +felix.service.caps=osgi.service; objectClass:List=org.osgi.service.resolver.Resolver; uses:=org.osgi.service.resolver, \ + osgi.service; objectClass:List=org.osgi.service.startlevel.StartLevel; uses:=org.osgi.service.startlevel, \ + osgi.service; objectClass:List=org.osgi.service.packageadmin.PackageAdmin; uses:=org.osgi.service.packageadmin + +eecap-1.8=, osgi.ee; osgi.ee="OSGi/Minimum"; version:List="1.0,1.1,1.2", \ + osgi.ee; osgi.ee="JavaSE"; version:List="1.0,1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8", \ + osgi.ee; osgi.ee="JavaSE/compact1"; version:List="1.8", \ + osgi.ee; osgi.ee="JavaSE/compact2"; version:List="1.8", \ + osgi.ee; osgi.ee="JavaSE/compact3"; version:List="1.8" +eecap-1.7=, osgi.ee; osgi.ee="OSGi/Minimum"; version:List="1.0,1.1,1.2", \ + osgi.ee; osgi.ee="JavaSE"; version:List="1.0,1.1,1.2,1.3,1.4,1.5,1.6,1.7" +eecap-1.6=, osgi.ee; osgi.ee="OSGi/Minimum"; version:List="1.0,1.1,1.2", \ + osgi.ee; osgi.ee="JavaSE"; version:List="1.0,1.1,1.2,1.3,1.4,1.5,1.6" + +# Deprecated old-style execution environment properties. +org.osgi.framework.executionenvironment= \ + ${dollar}{ee-${dollar}{java.specification.version}} ${dollar}{ee-${dollar}{felix.detect.jpms}} + +ee-1.8=JavaSE-1.8,JavaSE-1.7,JavaSE-1.6,J2SE-1.5,J2SE-1.4,J2SE-1.3, \ + J2SE-1.2,JRE-1.1,JRE-1.0,OSGi/Minimum-1.2,OSGi/Minimum-1.1, \ + OSGi/Minimum-1.0 +ee-1.7=JavaSE-1.7,JavaSE-1.6,J2SE-1.5,J2SE-1.4,J2SE-1.3, \ + J2SE-1.2,JRE-1.1,JRE-1.0,OSGi/Minimum-1.2,OSGi/Minimum-1.1, \ + OSGi/Minimum-1.0 +ee-1.6=JavaSE-1.6,J2SE-1.5,J2SE-1.4,J2SE-1.3,J2SE-1.2, \ + JRE-1.1,JRE-1.0,OSGi/Minimum-1.2,OSGi/Minimum-1.1,OSGi/Minimum-1.0 + +# Default packages exported by system bundle. +org.osgi.framework.system.packages=\ + org.osgi.framework;version="1.9", \ + org.osgi.framework.dto;version="1.8";uses:="org.osgi.dto", \ + org.osgi.framework.hooks.bundle;version="1.1";uses:="org.osgi.framework", \ + org.osgi.framework.hooks.resolver;version="1.0";uses:="org.osgi.framework.wiring", \ + org.osgi.framework.hooks.service;version="1.1";uses:="org.osgi.framework", \ + org.osgi.framework.hooks.weaving;version="1.1";uses:="org.osgi.framework.wiring", \ + org.osgi.framework.launch;version="1.2";uses:="org.osgi.framework", \ + org.osgi.framework.namespace;version="1.1";uses:="org.osgi.resource", \ + org.osgi.framework.startlevel;version="1.0";uses:="org.osgi.framework", \ + org.osgi.framework.startlevel.dto;version="1.0";uses:="org.osgi.dto", \ + org.osgi.framework.wiring;version="1.2";uses:="org.osgi.framework,org.osgi.resource", \ + org.osgi.framework.wiring.dto;version="1.3";uses:="org.osgi.dto,org.osgi.resource.dto", \ + org.osgi.resource;version="1.0", \ + org.osgi.resource.dto;version="1.0";uses:="org.osgi.dto", \ + org.osgi.service.packageadmin;version="1.2";uses:="org.osgi.framework", \ + org.osgi.service.startlevel;version="1.1";uses:="org.osgi.framework", \ + org.osgi.service.url;version="1.0", \ + org.osgi.service.resolver;version="1.1";uses:="org.osgi.resource", \ + org.osgi.util.tracker;version="1.5.2";uses:="org.osgi.framework", \ + org.osgi.dto;version="1.1" \ + ${dollar}{jre-${dollar}{felix.detect.java.specification.version}} \ + ${dollar}{jre-${dollar}{felix.detect.jpms}} + +# +# Java platform package export properties. +# +jre-base=, \ + java.applet;version="${dollar}{felix.detect.java.version}", \ + java.awt;version="${dollar}{felix.detect.java.version}", \ + java.awt.color;version="${dollar}{felix.detect.java.version}", \ + java.awt.datatransfer;version="${dollar}{felix.detect.java.version}", \ + java.awt.dnd;version="${dollar}{felix.detect.java.version}", \ + java.awt.event;version="${dollar}{felix.detect.java.version}", \ + java.awt.font;version="${dollar}{felix.detect.java.version}", \ + java.awt.geom;version="${dollar}{felix.detect.java.version}", \ + java.awt.im;version="${dollar}{felix.detect.java.version}", \ + java.awt.im.spi;version="${dollar}{felix.detect.java.version}", \ + java.awt.image;version="${dollar}{felix.detect.java.version}", \ + java.awt.image.renderable;version="${dollar}{felix.detect.java.version}", \ + java.awt.print;version="${dollar}{felix.detect.java.version}", \ + java.beans;version="${dollar}{felix.detect.java.version}", \ + java.beans.beancontext;version="${dollar}{felix.detect.java.version}", \ + java.io;version="${dollar}{felix.detect.java.version}", \ + java.lang;version="${dollar}{felix.detect.java.version}", \ + java.lang.annotation;version="${dollar}{felix.detect.java.version}", \ + java.lang.instrument;version="${dollar}{felix.detect.java.version}", \ + java.lang.management;version="${dollar}{felix.detect.java.version}", \ + java.lang.ref;version="${dollar}{felix.detect.java.version}", \ + java.lang.reflect;version="${dollar}{felix.detect.java.version}", \ + java.math;version="${dollar}{felix.detect.java.version}", \ + java.net;version="${dollar}{felix.detect.java.version}", \ + java.nio;version="${dollar}{felix.detect.java.version}", \ + java.nio.channels;version="${dollar}{felix.detect.java.version}", \ + java.nio.channels.spi;version="${dollar}{felix.detect.java.version}", \ + java.nio.charset;version="${dollar}{felix.detect.java.version}", \ + java.nio.charset.spi;version="${dollar}{felix.detect.java.version}", \ + java.rmi;version="${dollar}{felix.detect.java.version}", \ + java.rmi.activation;version="${dollar}{felix.detect.java.version}", \ + java.rmi.dgc;version="${dollar}{felix.detect.java.version}", \ + java.rmi.registry;version="${dollar}{felix.detect.java.version}", \ + java.rmi.server;version="${dollar}{felix.detect.java.version}", \ + java.security;version="${dollar}{felix.detect.java.version}", \ + java.security.acl;version="${dollar}{felix.detect.java.version}", \ + java.security.cert;version="${dollar}{felix.detect.java.version}", \ + java.security.interfaces;version="${dollar}{felix.detect.java.version}", \ + java.security.spec;version="${dollar}{felix.detect.java.version}", \ + java.sql;version="${dollar}{felix.detect.java.version}", \ + java.text;version="${dollar}{felix.detect.java.version}", \ + java.text.spi;version="${dollar}{felix.detect.java.version}", \ + java.util;version="${dollar}{felix.detect.java.version}", \ + java.util.concurrent;version="${dollar}{felix.detect.java.version}", \ + java.util.concurrent.atomic;version="${dollar}{felix.detect.java.version}", \ + java.util.concurrent.locks;version="${dollar}{felix.detect.java.version}", \ + java.util.jar;version="${dollar}{felix.detect.java.version}", \ + java.util.logging;version="${dollar}{felix.detect.java.version}", \ + java.util.prefs;version="${dollar}{felix.detect.java.version}", \ + java.util.regex;version="${dollar}{felix.detect.java.version}", \ + java.util.spi;version="${dollar}{felix.detect.java.version}", \ + java.util.zip;version="${dollar}{felix.detect.java.version}", \ + javax.accessibility;uses:="javax.swing.text";version="${dollar}{felix.detect.java.version}", \ + javax.activation;version="${dollar}{felix.detect.java.version}", \ + javax.activity;version="${dollar}{felix.detect.java.version}", \ + javax.annotation;version="${dollar}{felix.detect.java.version}", \ + javax.annotation.processing;uses:="javax.lang.model,javax.lang.model.element,javax.lang.model.util,javax.tools";version="${dollar}{felix.detect.java.version}", \ + javax.crypto.interfaces;uses:="javax.crypto,javax.crypto.spec";version="${dollar}{felix.detect.java.version}", \ + javax.crypto.spec;uses:="javax.crypto";version="${dollar}{felix.detect.java.version}", \ + javax.imageio;uses:="javax.imageio.event,javax.imageio.metadata,javax.imageio.spi,javax.imageio.stream";version="${dollar}{felix.detect.java.version}", \ + javax.imageio.event;uses:="javax.imageio";version="${dollar}{felix.detect.java.version}", \ + javax.imageio.metadata;uses:="javax.imageio,org.w3c.dom";version="${dollar}{felix.detect.java.version}", \ + javax.imageio.plugins.bmp;uses:="javax.imageio";version="${dollar}{felix.detect.java.version}", \ + javax.imageio.plugins.jpeg;uses:="javax.imageio";version="${dollar}{felix.detect.java.version}", \ + javax.imageio.spi;uses:="javax.imageio,javax.imageio.metadata,javax.imageio.stream";version="${dollar}{felix.detect.java.version}", \ + javax.imageio.stream;uses:="javax.imageio";version="${dollar}{felix.detect.java.version}", \ + javax.jws;version="${dollar}{felix.detect.java.version}", \ + javax.jws.soap;version="${dollar}{felix.detect.java.version}", \ + javax.lang.model.util;uses:="javax.annotation.processing,javax.lang.model,javax.lang.model.element,javax.lang.model.type";version="${dollar}{felix.detect.java.version}", \ + javax.management;uses:="javax.management.loading,javax.management.openmbean";version="${dollar}{felix.detect.java.version}", \ + javax.management.loading;uses:="javax.management";version="${dollar}{felix.detect.java.version}", \ + javax.management.modelmbean;uses:="javax.management,javax.management.loading";version="${dollar}{felix.detect.java.version}", \ + javax.management.openmbean;uses:="javax.management";version="${dollar}{felix.detect.java.version}", \ + javax.management.relation;uses:="javax.management";version="${dollar}{felix.detect.java.version}", \ + javax.management.remote;uses:="javax.management,javax.security.auth";version="${dollar}{felix.detect.java.version}", \ + javax.management.remote.rmi;uses:="javax.management,javax.management.loading,javax.management.remote,javax.naming,javax.rmi,javax.rmi.CORBA,javax.rmi.ssl,javax.security.auth,org.omg.CORBA,org.omg.CORBA.portable,org.omg.CORBA_2_3.portable";version="${dollar}{felix.detect.java.version}", \ + javax.management.timer;uses:="javax.management";version="${dollar}{felix.detect.java.version}", \ + javax.naming;uses:="javax.naming.spi";version="${dollar}{felix.detect.java.version}", \ + javax.naming.directory;uses:="javax.naming";version="${dollar}{felix.detect.java.version}", \ + javax.naming.event;uses:="javax.naming,javax.naming.directory";version="${dollar}{felix.detect.java.version}", \ + javax.naming.ldap;uses:="javax.naming,javax.naming.directory,javax.naming.event,javax.net.ssl";version="${dollar}{felix.detect.java.version}", \ + javax.naming.spi;uses:="javax.naming,javax.naming.directory";version="${dollar}{felix.detect.java.version}", \ + javax.net;version="${dollar}{felix.detect.java.version}", \ + javax.net.ssl;uses:="javax.net,javax.security.auth.x500,javax.security.cert";version="${dollar}{felix.detect.java.version}", \ + javax.print;uses:="javax.print.attribute,javax.print.attribute.standard,javax.print.event";version="${dollar}{felix.detect.java.version}", \ + javax.print.attribute;version="${dollar}{felix.detect.java.version}", \ + javax.print.attribute.standard;uses:="javax.print.attribute";version="${dollar}{felix.detect.java.version}", \ + javax.print.event;uses:="javax.print,javax.print.attribute";version="${dollar}{felix.detect.java.version}", \ + javax.rmi;uses:="javax.rmi.CORBA,org.omg.CORBA";version="${dollar}{felix.detect.java.version}", \ + javax.rmi.CORBA;uses:="org.omg.CORBA,org.omg.CORBA.portable,org.omg.CORBA_2_3.portable,org.omg.SendingContext";version="${dollar}{felix.detect.java.version}", \ + javax.rmi.ssl;uses:="javax.net,javax.net.ssl";version="${dollar}{felix.detect.java.version}", \ + javax.script;version="${dollar}{felix.detect.java.version}", \ + javax.security.auth;version="${dollar}{felix.detect.java.version}", \ + javax.security.auth.callback;version="${dollar}{felix.detect.java.version}", \ + javax.security.auth.kerberos;uses:="javax.crypto,javax.security.auth";version="${dollar}{felix.detect.java.version}", \ + javax.security.auth.login;uses:="javax.security.auth,javax.security.auth.callback";version="${dollar}{felix.detect.java.version}", \ + javax.security.auth.spi;uses:="javax.security.auth,javax.security.auth.callback,javax.security.auth.login";version="${dollar}{felix.detect.java.version}", \ + javax.security.auth.x500;uses:="javax.security.auth";version="${dollar}{felix.detect.java.version}", \ + javax.security.cert;version="${dollar}{felix.detect.java.version}", \ + javax.security.sasl;uses:="javax.security.auth.callback";version="${dollar}{felix.detect.java.version}", \ + javax.sound.midi;uses:="javax.sound.midi.spi";version="${dollar}{felix.detect.java.version}", \ + javax.sound.midi.spi;uses:="javax.sound.midi";version="${dollar}{felix.detect.java.version}", \ + javax.sound.sampled;uses:="javax.sound.sampled.spi";version="${dollar}{felix.detect.java.version}", \ + javax.sound.sampled.spi;uses:="javax.sound.sampled";version="${dollar}{felix.detect.java.version}", \ + javax.sql;uses:="javax.transaction.xa";version="${dollar}{felix.detect.java.version}", \ + javax.sql.rowset;uses:="javax.sql,javax.sql.rowset.serial,javax.sql.rowset.spi";version="${dollar}{felix.detect.java.version}", \ + javax.sql.rowset.serial;uses:="javax.sql.rowset";version="${dollar}{felix.detect.java.version}", \ + javax.sql.rowset.spi;uses:="javax.naming,javax.sql,javax.sql.rowset";version="${dollar}{felix.detect.java.version}", \ + javax.swing;uses:="javax.accessibility,javax.print,javax.print.attribute,javax.swing.border,javax.swing.colorchooser,javax.swing.event,javax.swing.filechooser,javax.swing.plaf,javax.swing.plaf.basic,javax.swing.plaf.metal,javax.swing.table,javax.swing.text,javax.swing.text.html,javax.swing.tree";version="${dollar}{felix.detect.java.version}", \ + javax.swing.border;uses:="javax.swing";version="${dollar}{felix.detect.java.version}", \ + javax.swing.event;uses:="javax.swing,javax.swing.table,javax.swing.text,javax.swing.tree,javax.swing.undo";version="${dollar}{felix.detect.java.version}", \ + javax.swing.filechooser;uses:="javax.swing";version="${dollar}{felix.detect.java.version}", \ + javax.swing.plaf;uses:="javax.accessibility,javax.swing,javax.swing.border,javax.swing.filechooser,javax.swing.text,javax.swing.tree";version="${dollar}{felix.detect.java.version}", \ + javax.swing.plaf.metal;uses:="javax.swing,javax.swing.border,javax.swing.event,javax.swing.filechooser,javax.swing.plaf,javax.swing.plaf.basic,javax.swing.text,javax.swing.tree";version="${dollar}{felix.detect.java.version}", \ + javax.swing.plaf.multi;uses:="javax.accessibility,javax.swing,javax.swing.filechooser,javax.swing.plaf,javax.swing.text,javax.swing.tree";version="${dollar}{felix.detect.java.version}", \ + javax.swing.table;uses:="javax.accessibility,javax.swing,javax.swing.border,javax.swing.event,javax.swing.plaf";version="${dollar}{felix.detect.java.version}", \ + javax.swing.text;uses:="javax.accessibility,javax.print,javax.print.attribute,javax.swing,javax.swing.event,javax.swing.plaf,javax.swing.plaf.basic,javax.swing.text.html,javax.swing.tree,javax.swing.undo";version="${dollar}{felix.detect.java.version}", \ + javax.swing.text.html;uses:="javax.accessibility,javax.swing,javax.swing.border,javax.swing.event,javax.swing.plaf,javax.swing.text,javax.swing.undo";version="${dollar}{felix.detect.java.version}", \ + javax.swing.text.html.parser;uses:="javax.swing.text,javax.swing.text.html";version="${dollar}{felix.detect.java.version}", \ + javax.swing.text.rtf;uses:="javax.swing.text";version="${dollar}{felix.detect.java.version}", \ + javax.swing.tree;uses:="javax.swing,javax.swing.border,javax.swing.event,javax.swing.plaf,javax.swing.plaf.basic";version="${dollar}{felix.detect.java.version}", \ + javax.swing.undo;uses:="javax.swing,javax.swing.event";version="${dollar}{felix.detect.java.version}", \ + javax.tools;uses:="javax.annotation.processing,javax.lang.model,javax.lang.model.element";version="${dollar}{felix.detect.java.version}", \ + javax.transaction;version="${dollar}{felix.detect.java.version}", \ + javax.transaction.xa;version="${dollar}{felix.detect.java.version}", \ + javax.xml;version="${dollar}{felix.detect.java.version}", \ + javax.xml.bind;uses:="javax.xml.bind.annotation,javax.xml.bind.annotation.adapters,javax.xml.bind.attachment,javax.xml.datatype,javax.xml.namespace,javax.xml.stream,javax.xml.transform,javax.xml.transform.stream,javax.xml.validation,org.w3c.dom,org.xml.sax";version="${dollar}{felix.detect.java.version}", \ + javax.xml.bind.annotation;uses:="javax.xml.bind,javax.xml.parsers,javax.xml.transform,javax.xml.transform.dom,org.w3c.dom";version="${dollar}{felix.detect.java.version}", \ + javax.xml.bind.annotation.adapters;uses:="javax.xml.bind";version="${dollar}{felix.detect.java.version}", \ + javax.xml.bind.attachment;uses:="javax.activation";version="${dollar}{felix.detect.java.version}", \ + javax.xml.bind.helpers;uses:="javax.xml.bind,javax.xml.bind.annotation.adapters,javax.xml.bind.attachment,javax.xml.parsers,javax.xml.stream,javax.xml.transform,javax.xml.transform.dom,javax.xml.transform.sax,javax.xml.transform.stream,javax.xml.validation,org.w3c.dom,org.xml.sax";version="${dollar}{felix.detect.java.version}", \ + javax.xml.bind.util;uses:="javax.xml.bind,javax.xml.transform.sax,org.xml.sax,org.xml.sax.ext,org.xml.sax.helpers";version="${dollar}{felix.detect.java.version}", \ + javax.xml.crypto;uses:="javax.xml.crypto.dsig.keyinfo";version="${dollar}{felix.detect.java.version}", \ + javax.xml.crypto.dom;uses:="javax.xml.crypto,org.w3c.dom";version="${dollar}{felix.detect.java.version}", \ + javax.xml.crypto.dsig;uses:="javax.xml.crypto,javax.xml.crypto.dsig.keyinfo,javax.xml.crypto.dsig.spec";version="${dollar}{felix.detect.java.version}", \ + javax.xml.crypto.dsig.dom;uses:="javax.xml.crypto,javax.xml.crypto.dom,javax.xml.crypto.dsig,org.w3c.dom";version="${dollar}{felix.detect.java.version}", \ + javax.xml.crypto.dsig.keyinfo;uses:="javax.xml.crypto";version="${dollar}{felix.detect.java.version}", \ + javax.xml.crypto.dsig.spec;uses:="javax.xml.crypto";version="${dollar}{felix.detect.java.version}", \ + javax.xml.datatype;uses:="javax.xml.namespace";version="${dollar}{felix.detect.java.version}", \ + javax.xml.namespace;version="${dollar}{felix.detect.java.version}", \ + javax.xml.parsers;uses:="javax.xml.validation,org.w3c.dom,org.xml.sax,org.xml.sax.helpers";version="${dollar}{felix.detect.java.version}", \ + javax.xml.soap;uses:="javax.activation,javax.xml.namespace,javax.xml.transform,javax.xml.transform.dom,org.w3c.dom";version="${dollar}{felix.detect.java.version}", \ + javax.xml.stream;uses:="javax.xml.namespace,javax.xml.stream.events,javax.xml.stream.util,javax.xml.transform";version="${dollar}{felix.detect.java.version}", \ + javax.xml.stream.events;uses:="javax.xml.namespace,javax.xml.stream";version="${dollar}{felix.detect.java.version}", \ + javax.xml.stream.util;uses:="javax.xml.namespace,javax.xml.stream,javax.xml.stream.events";version="${dollar}{felix.detect.java.version}", \ + javax.xml.transform;version="${dollar}{felix.detect.java.version}", \ + javax.xml.transform.dom;uses:="javax.xml.transform,org.w3c.dom";version="${dollar}{felix.detect.java.version}", \ + javax.xml.transform.sax;uses:="javax.xml.transform,javax.xml.transform.stream,org.xml.sax,org.xml.sax.ext";version="${dollar}{felix.detect.java.version}", \ + javax.xml.transform.stax;uses:="javax.xml.stream,javax.xml.stream.events,javax.xml.transform";version="${dollar}{felix.detect.java.version}", \ + javax.xml.transform.stream;uses:="javax.xml.transform";version="${dollar}{felix.detect.java.version}", \ + javax.xml.validation;uses:="javax.xml.transform,javax.xml.transform.stream,org.w3c.dom,org.w3c.dom.ls,org.xml.sax";version="${dollar}{felix.detect.java.version}", \ + javax.xml.ws.handler;uses:="javax.xml.namespace,javax.xml.ws";version="${dollar}{felix.detect.java.version}", \ + javax.xml.ws.handler.soap;uses:="javax.xml.bind,javax.xml.namespace,javax.xml.soap,javax.xml.ws.handler";version="${dollar}{felix.detect.java.version}", \ + javax.xml.ws.http;uses:="javax.xml.ws";version="${dollar}{felix.detect.java.version}", \ + javax.xml.ws.soap;uses:="javax.xml.soap,javax.xml.ws,javax.xml.ws.spi";version="${dollar}{felix.detect.java.version}", \ + javax.xml.ws.spi;uses:="javax.xml.bind,javax.xml.namespace,javax.xml.transform,javax.xml.ws,javax.xml.ws.handler,javax.xml.ws.wsaddressing,org.w3c.dom";version="${dollar}{felix.detect.java.version}", \ + javax.xml.ws.wsaddressing;uses:="javax.xml.bind,javax.xml.bind.annotation,javax.xml.namespace,javax.xml.transform,javax.xml.ws,javax.xml.ws.spi,org.w3c.dom";version="${dollar}{felix.detect.java.version}", \ + javax.xml.xpath;uses:="javax.xml.namespace,org.xml.sax";version="${dollar}{felix.detect.java.version}", \ + org.ietf.jgss;version="${dollar}{felix.detect.java.version}", \ + org.omg.CORBA;uses:="org.omg.CORBA.DynAnyPackage,org.omg.CORBA.ORBPackage,org.omg.CORBA.TypeCodePackage,org.omg.CORBA.portable,org.omg.CORBA_2_3.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.CORBA.DynAnyPackage;uses:="org.omg.CORBA";version="${dollar}{felix.detect.java.version}", \ + org.omg.CORBA.ORBPackage;uses:="org.omg.CORBA,org.omg.CORBA.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.CORBA.TypeCodePackage;uses:="org.omg.CORBA,org.omg.CORBA.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.CORBA.portable;uses:="org.omg.CORBA,org.omg.CORBA_2_3.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.CORBA_2_3;uses:="org.omg.CORBA,org.omg.CORBA.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.CORBA_2_3.portable;uses:="org.omg.CORBA,org.omg.CORBA.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.CosNaming;uses:="org.omg.CORBA,org.omg.CORBA.portable,org.omg.CosNaming.NamingContextExtPackage,org.omg.CosNaming.NamingContextPackage,org.omg.PortableServer";version="${dollar}{felix.detect.java.version}", \ + org.omg.CosNaming.NamingContextExtPackage;uses:="org.omg.CORBA,org.omg.CORBA.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.CosNaming.NamingContextPackage;uses:="org.omg.CORBA,org.omg.CORBA.portable,org.omg.CosNaming";version="${dollar}{felix.detect.java.version}", \ + org.omg.Dynamic;uses:="org.omg.CORBA,org.omg.CORBA.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.DynamicAny;uses:="org.omg.CORBA,org.omg.CORBA.portable,org.omg.DynamicAny.DynAnyFactoryPackage,org.omg.DynamicAny.DynAnyPackage";version="${dollar}{felix.detect.java.version}", \ + org.omg.DynamicAny.DynAnyFactoryPackage;uses:="org.omg.CORBA,org.omg.CORBA.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.DynamicAny.DynAnyPackage;uses:="org.omg.CORBA,org.omg.CORBA.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.IOP;uses:="org.omg.CORBA,org.omg.CORBA.portable,org.omg.IOP.CodecFactoryPackage,org.omg.IOP.CodecPackage";version="${dollar}{felix.detect.java.version}", \ + org.omg.IOP.CodecFactoryPackage;uses:="org.omg.CORBA,org.omg.CORBA.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.IOP.CodecPackage;uses:="org.omg.CORBA,org.omg.CORBA.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.Messaging;uses:="org.omg.CORBA,org.omg.CORBA.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.PortableInterceptor;uses:="org.omg.CORBA,org.omg.CORBA.portable,org.omg.CORBA_2_3.portable,org.omg.Dynamic,org.omg.IOP,org.omg.PortableInterceptor.ORBInitInfoPackage";version="${dollar}{felix.detect.java.version}", \ + org.omg.PortableInterceptor.ORBInitInfoPackage;uses:="org.omg.CORBA,org.omg.CORBA.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.PortableServer;uses:="org.omg.CORBA,org.omg.CORBA.portable,org.omg.CORBA_2_3,org.omg.PortableServer.CurrentPackage,org.omg.PortableServer.POAManagerPackage,org.omg.PortableServer.POAPackage,org.omg.PortableServer.ServantLocatorPackage,org.omg.PortableServer.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.PortableServer.CurrentPackage;uses:="org.omg.CORBA,org.omg.CORBA.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.PortableServer.POAManagerPackage;uses:="org.omg.CORBA,org.omg.CORBA.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.PortableServer.POAPackage;uses:="org.omg.CORBA,org.omg.CORBA.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.PortableServer.ServantLocatorPackage;uses:="org.omg.CORBA,org.omg.CORBA.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.PortableServer.portable;uses:="org.omg.CORBA,org.omg.PortableServer";version="${dollar}{felix.detect.java.version}", \ + org.omg.SendingContext;uses:="org.omg.CORBA,org.omg.CORBA.portable";version="${dollar}{felix.detect.java.version}", \ + org.omg.stub.java.rmi;uses:="javax.rmi.CORBA";version="${dollar}{felix.detect.java.version}", \ + org.w3c.dom;version="${dollar}{felix.detect.java.version}", \ + org.w3c.dom.bootstrap;uses:="org.w3c.dom";version="${dollar}{felix.detect.java.version}", \ + org.w3c.dom.ls;uses:="org.w3c.dom,org.w3c.dom.events";version="${dollar}{felix.detect.java.version}", \ + org.w3c.dom.events;uses:="org.w3c.dom,org.w3c.dom.views";version="${dollar}{felix.detect.java.version}", \ + org.w3c.dom.views;version="${dollar}{felix.detect.java.version}", \ + org.w3c.dom.traversal;uses:="org.w3c.dom";version="${dollar}{felix.detect.java.version}", \ + org.w3c.dom.ranges;uses:="org.w3c.dom";version="${dollar}{felix.detect.java.version}", \ + org.w3c.dom.css;uses:="org.w3c.dom,org.w3c.dom.stylesheets,org.w3c.dom.views";version="${dollar}{felix.detect.java.version}", \ + org.w3c.dom.html;uses:="org.w3c.dom";version="${dollar}{felix.detect.java.version}", \ + org.w3c.dom.stylesheets;uses:="org.w3c.dom";version="${dollar}{felix.detect.java.version}", \ + org.w3c.dom.xpath;uses:="org.w3c.dom";version="${dollar}{felix.detect.java.version}", \ + org.xml.sax;version="${dollar}{felix.detect.java.version}", \ + org.xml.sax.ext;uses:="org.xml.sax,org.xml.sax.helpers";version="${dollar}{felix.detect.java.version}", \ + org.xml.sax.helpers;uses:="org.xml.sax";version="${dollar}{felix.detect.java.version}" + +jre-base-1.6= \ + ${jre-base}, \ + javax.crypto;uses:="javax.crypto.spec";version="${dollar}{felix.detect.java.version}", \ + javax.lang.model;version="${dollar}{felix.detect.java.version}" + +jre-1.6= \ + ${jre-base-1.6}, \ + javax.lang.model.element;uses:="javax.lang.model.type";version="${dollar}{felix.detect.java.version}", \ + javax.lang.model.type;uses:="javax.lang.model.element";version="${dollar}{felix.detect.java.version}", \ + javax.management.monitor;uses:="javax.management,javax.management.openmbean";version="${dollar}{felix.detect.java.version}", \ + javax.swing.colorchooser;uses:="javax.swing,javax.swing.border,javax.swing.event";version="${dollar}{felix.detect.java.version}", \ + javax.swing.plaf.basic;uses:="javax.accessibility,javax.sound.sampled,javax.swing,javax.swing.border,javax.swing.colorchooser,javax.swing.event,javax.swing.filechooser,javax.swing.plaf,javax.swing.table,javax.swing.text,javax.swing.text.html,javax.swing.tree";version="${dollar}{felix.detect.java.version}", \ + javax.swing.plaf.synth;uses:="javax.swing,javax.swing.border,javax.swing.colorchooser,javax.swing.event,javax.swing.plaf,javax.swing.plaf.basic,javax.swing.table,javax.swing.text,javax.swing.tree,javax.xml.parsers,org.xml.sax";version="${dollar}{felix.detect.java.version}", \ + javax.xml.ws;uses:="javax.xml.bind,javax.xml.bind.annotation,javax.xml.namespace,javax.xml.transform,javax.xml.transform.stream,javax.xml.ws.handler,javax.xml.ws.spi,org.w3c.dom";version="${dollar}{felix.detect.java.version}" + +jre-base-1.7=, \ + java.lang.invoke;version="${dollar}{felix.detect.java.version}", \ + java.nio.file;version="${dollar}{felix.detect.java.version}", \ + java.nio.file.attribute;version="${dollar}{felix.detect.java.version}", \ + java.nio.file.spi;version="${dollar}{felix.detect.java.version}", \ + javax.lang.model.element;uses:="javax.lang.model,javax.lang.model.type";version="${dollar}{felix.detect.java.version}", \ + javax.lang.model.type;uses:="javax.lang.model,javax.lang.model.element";version="${dollar}{felix.detect.java.version}", \ + javax.management.monitor;uses:="javax.management";version="${dollar}{felix.detect.java.version}", \ + javax.swing.colorchooser;uses:="javax.accessibility,javax.swing,javax.swing.border,javax.swing.event,javax.swing.text";version="${dollar}{felix.detect.java.version}", \ + javax.swing.plaf.basic;uses:="javax.accessibility,javax.sound.sampled,javax.swing,javax.swing.border,javax.swing.colorchooser,javax.swing.event,javax.swing.filechooser,javax.swing.plaf,javax.swing.plaf.synth,javax.swing.table,javax.swing.text,javax.swing.text.html,javax.swing.tree";version="${dollar}{felix.detect.java.version}", \ + javax.swing.plaf.nimbus;uses:="javax.swing,javax.swing.border,javax.swing.plaf,javax.swing.plaf.synth";version="${dollar}{felix.detect.java.version}", \ + javax.swing.plaf.synth;uses:="javax.swing,javax.swing.border,javax.swing.colorchooser,javax.swing.event,javax.swing.plaf,javax.swing.plaf.basic,javax.swing.table,javax.swing.text,javax.swing.tree,javax.xml.parsers,org.xml.sax,org.xml.sax.helpers";version="${dollar}{felix.detect.java.version}", \ + javax.xml.ws;uses:="javax.xml.bind,javax.xml.bind.annotation,javax.xml.namespace,javax.xml.transform,javax.xml.transform.stream,javax.xml.ws.handler,javax.xml.ws.spi,javax.xml.ws.spi.http,org.w3c.dom";version="${dollar}{felix.detect.java.version}", \ + javax.xml.ws.spi.http;version="${dollar}{felix.detect.java.version}" + +jre-1.7= \ + ${jre-base-1.6} \ + ${jre-base-1.7} + +jre-1.8= \ + ${jre-base} \ + ${jre-base-1.7}, \ + java.time;version="${dollar}{felix.detect.java.version}", \ + java.time.chrono;version="${dollar}{felix.detect.java.version}", \ + java.time.format;version="${dollar}{felix.detect.java.version}", \ + java.time.temporal;version="${dollar}{felix.detect.java.version}", \ + java.time.zone;version="${dollar}{felix.detect.java.version}", \ + java.util.function;version="${dollar}{felix.detect.java.version}", \ + java.util.stream;version="${dollar}{felix.detect.java.version}", \ + javax.crypto;uses:="javax.crypto.spec,javax.security.auth";version="${dollar}{felix.detect.java.version}", \ + javax.lang.model;uses:="javax.lang.model.element";version="${dollar}{felix.detect.java.version}" \ No newline at end of file diff --git a/framework/src/test/java/org/apache/felix/framework/BootLoaderTest.java b/framework/src/test/java/org/apache/felix/framework/BootLoaderTest.java new file mode 100644 index 00000000000..e392a642171 --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/BootLoaderTest.java @@ -0,0 +1,119 @@ +/* + * 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.framework; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import junit.framework.TestCase; +import org.apache.felix.framework.util.FelixConstants; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.launch.Framework; + +/** + * Test to ensure behaviour of "felix.bootdelegation.classloaders" contract. + * + */ +public class BootLoaderTest extends TestCase +{ + private File cacheDir; + + public void testCanProvideOwnClassLoader() throws Exception + { + final ClassLoader myClassloader = new CL(); + final List queriedFor = new ArrayList(); + + Map bundle2Classloader = new HashMap() + { + public Object get(Object o) + { + queriedFor.add(o); + return myClassloader; + } + }; + + Map params = new HashMap(); + params.put(Constants.FRAMEWORK_SYSTEMPACKAGES, + "org.osgi.framework; version=1.4.0," + + "org.osgi.service.packageadmin; version=1.2.0," + + "org.osgi.service.startlevel; version=1.1.0," + + "org.osgi.util.tracker; version=1.3.3," + + "org.osgi.service.url; version=1.0.0"); + params.put(FelixConstants.BOOT_CLASSLOADERS_PROP, bundle2Classloader); + cacheDir = File.createTempFile("felix-cache", ".dir"); + cacheDir.delete(); + cacheDir.mkdirs(); + String cache = cacheDir.getPath(); + params.put("felix.cache.profiledir", cache); + params.put("felix.cache.dir", cache); + params.put(Constants.FRAMEWORK_STORAGE, cache); + + String mf = "Bundle-SymbolicName: boot.test\n" + + "Bundle-Version: 1.1.0\n" + + "Bundle-ManifestVersion: 2\n" + + "Export-Package: boot.test"; + File bundle = createBundle(mf); + + Framework f = new Felix(params); + f.init(); + f.getBundleContext().installBundle(bundle.toURI().toString()); + f.start(); + + Bundle[] arr = f.getBundleContext().getBundles(); + assertEquals("Two, system and mine: " + Arrays.toString(arr), 2, arr.length); + Class c = arr[1].loadClass("boot.test.Test"); + assertNotNull("Class loaded", c); + assertEquals("One query", 1, queriedFor.size()); + assertEquals("Queried for my bundle", arr[1], queriedFor.get(0)); + } + + public static final class CL extends ClassLoader + { + protected Class findClass(String name) throws ClassNotFoundException + { + if (name.equals("boot.test.Test")) + { + return String.class; + } + throw new ClassNotFoundException(); + } + } + + private static File createBundle(String manifest) throws IOException + { + File f = File.createTempFile("felix-bundle", ".jar"); + f.deleteOnExit(); + + Manifest mf = new Manifest(new ByteArrayInputStream(manifest.getBytes("utf-8"))); + mf.getMainAttributes().putValue("Manifest-Version", "1.0"); + JarOutputStream os = new JarOutputStream(new FileOutputStream(f), mf); + os.close(); + + return f; + } +} \ No newline at end of file diff --git a/framework/src/test/java/org/apache/felix/framework/BundleRevisionImplTest.java b/framework/src/test/java/org/apache/felix/framework/BundleRevisionImplTest.java new file mode 100644 index 00000000000..685af10a72a --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/BundleRevisionImplTest.java @@ -0,0 +1,42 @@ +/* + * 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.framework; + +import java.util.Enumeration; +import java.util.List; + +import junit.framework.TestCase; + +import org.apache.felix.framework.cache.Content; + +public class BundleRevisionImplTest extends TestCase +{ + public void testGetResourcesLocalNullContentPath() + { + BundleRevisionImpl bri = new BundleRevisionImpl(null, null) { + @Override + synchronized List getContentPath() + { + return null; + } + }; + Enumeration en = bri.getResourcesLocal("foo"); + assertFalse(en.hasMoreElements()); + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/BundleWiringImplTest.java b/framework/src/test/java/org/apache/felix/framework/BundleWiringImplTest.java new file mode 100644 index 00000000000..cee0d2135f0 --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/BundleWiringImplTest.java @@ -0,0 +1,1213 @@ +/* + * 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.framework; + +import org.apache.felix.framework.BundleWiringImpl.BundleClassLoader; +import org.apache.felix.framework.cache.Content; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.hooks.weaving.WeavingException; +import org.osgi.framework.hooks.weaving.WeavingHook; +import org.osgi.framework.hooks.weaving.WovenClass; +import org.osgi.framework.hooks.weaving.WovenClassListener; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class BundleWiringImplTest +{ + + private BundleWiringImpl bundleWiring; + + private StatefulResolver mockResolver; + + private BundleRevisionImpl mockRevisionImpl; + + private BundleImpl mockBundle; + + @SuppressWarnings("rawtypes") + public void initializeSimpleBundleWiring() throws Exception + { + + mockResolver = mock(StatefulResolver.class); + mockRevisionImpl = mock(BundleRevisionImpl.class); + mockBundle = mock(BundleImpl.class); + + Logger logger = new Logger(); + Map configMap = new HashMap(); + List fragments = new ArrayList(); + List wires = new ArrayList(); + Map importedPkgs = new HashMap(); + Map> requiredPkgs = new HashMap>(); + + when(mockRevisionImpl.getBundle()).thenReturn(mockBundle); + when(mockBundle.getBundleId()).thenReturn(Long.valueOf(1)); + + bundleWiring = new BundleWiringImpl(logger, configMap, mockResolver, + mockRevisionImpl, fragments, wires, importedPkgs, requiredPkgs); + } + + @Test + public void testBundleClassLoader() throws Exception + { + bundleWiring = mock(BundleWiringImpl.class); + BundleClassLoader bundleClassLoader = createBundleClassLoader( + BundleClassLoader.class, bundleWiring); + assertNotNull(bundleClassLoader); + } + + @SuppressWarnings("rawtypes") + @Test + public void testFindClassNonExistant() throws Exception + { + initializeSimpleBundleWiring(); + + BundleClassLoader bundleClassLoader = createBundleClassLoader( + BundleClassLoader.class, bundleWiring); + assertNotNull(bundleClassLoader); + Class foundClass = null; + try + { + foundClass = bundleClassLoader + .findClass("org.apache.felix.test.NonExistant"); + } catch (ClassNotFoundException e) + { + fail("Class should not throw exception"); + } + assertNull("Nonexistant Class Should be null", foundClass); + } + + @SuppressWarnings("rawtypes") + @Test + public void testFindClassExistant() throws Exception + { + Felix mockFramework = mock(Felix.class); + HookRegistry hReg = mock(HookRegistry.class); + Mockito.when(mockFramework.getHookRegistry()).thenReturn(hReg); + Content mockContent = mock(Content.class); + Class testClass = TestClass.class; + String testClassName = testClass.getName(); + String testClassAsPath = testClassName.replace('.', '/') + ".class"; + byte[] testClassBytes = createTestClassBytes(testClass, testClassAsPath); + + List contentPath = new ArrayList(); + contentPath.add(mockContent); + initializeSimpleBundleWiring(); + + when(mockBundle.getFramework()).thenReturn(mockFramework); + when(mockFramework.getBootPackages()).thenReturn(new String[0]); + + when(mockRevisionImpl.getContentPath()).thenReturn(contentPath); + when(mockContent.getEntryAsBytes(testClassAsPath)).thenReturn( + testClassBytes); + + BundleClassLoader bundleClassLoader = createBundleClassLoader( + BundleClassLoader.class, bundleWiring); + assertNotNull(bundleClassLoader); + Class foundClass = null; + try + { + + foundClass = bundleClassLoader.findClass(TestClass.class.getName()); + } catch (ClassNotFoundException e) + { + fail("Class should not throw exception"); + } + assertNotNull("Class Should be found in this classloader", foundClass); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void testFindClassWeave() throws Exception + { + Felix mockFramework = mock(Felix.class); + Content mockContent = mock(Content.class); + ServiceReference mockServiceReferenceWeavingHook = mock(ServiceReference.class); + ServiceReference mockServiceReferenceWovenClassListener = mock(ServiceReference.class); + + Set> hooks = new HashSet>(); + hooks.add(mockServiceReferenceWeavingHook); + + DummyWovenClassListener dummyWovenClassListener = new DummyWovenClassListener(); + + Set> listeners = new HashSet>(); + listeners.add(mockServiceReferenceWovenClassListener); + + Class testClass = TestClass.class; + String testClassName = testClass.getName(); + String testClassAsPath = testClassName.replace('.', '/') + ".class"; + byte[] testClassBytes = createTestClassBytes(testClass, testClassAsPath); + + List contentPath = new ArrayList(); + contentPath.add(mockContent); + initializeSimpleBundleWiring(); + + when(mockBundle.getFramework()).thenReturn(mockFramework); + when(mockFramework.getBootPackages()).thenReturn(new String[0]); + + when(mockRevisionImpl.getContentPath()).thenReturn(contentPath); + when(mockContent.getEntryAsBytes(testClassAsPath)).thenReturn( + testClassBytes); + + HookRegistry hReg = mock(HookRegistry.class); + when(hReg.getHooks(WeavingHook.class)).thenReturn(hooks); + when(mockFramework.getHookRegistry()).thenReturn(hReg); + when( + mockFramework.getService(mockFramework, + mockServiceReferenceWeavingHook, false)).thenReturn( + new GoodDummyWovenHook()); + + when(hReg.getHooks(WovenClassListener.class)).thenReturn( + listeners); + when( + mockFramework.getService(mockFramework, + mockServiceReferenceWovenClassListener, false)) + .thenReturn(dummyWovenClassListener); + + BundleClassLoader bundleClassLoader = createBundleClassLoader( + BundleClassLoader.class, bundleWiring); + assertNotNull(bundleClassLoader); + Class foundClass = null; + try + { + + foundClass = bundleClassLoader.findClass(TestClass.class.getName()); + } catch (ClassNotFoundException e) + { + fail("Class should not throw exception"); + } + assertNotNull("Class Should be found in this classloader", foundClass); + assertEquals("Weaving should have added a field", 1, + foundClass.getFields().length); + assertEquals("There should be 2 state changes fired by the weaving", 2, + dummyWovenClassListener.stateList.size()); + assertEquals("The first state change should transform the class", + (Object)WovenClass.TRANSFORMED, + dummyWovenClassListener.stateList.get(0)); + assertEquals("The second state change should define the class", + (Object)WovenClass.DEFINED, dummyWovenClassListener.stateList.get(1)); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void testFindClassBadWeave() throws Exception + { + Felix mockFramework = mock(Felix.class); + Content mockContent = mock(Content.class); + ServiceReference mockServiceReferenceWeavingHook = mock(ServiceReference.class); + ServiceReference mockServiceReferenceWovenClassListener = mock(ServiceReference.class); + + Set> hooks = new HashSet>(); + hooks.add(mockServiceReferenceWeavingHook); + + DummyWovenClassListener dummyWovenClassListener = new DummyWovenClassListener(); + + Set> listeners = new HashSet>(); + listeners.add(mockServiceReferenceWovenClassListener); + + Class testClass = TestClass.class; + String testClassName = testClass.getName(); + String testClassAsPath = testClassName.replace('.', '/') + ".class"; + byte[] testClassBytes = createTestClassBytes(testClass, testClassAsPath); + + List contentPath = new ArrayList(); + contentPath.add(mockContent); + initializeSimpleBundleWiring(); + + when(mockBundle.getFramework()).thenReturn(mockFramework); + when(mockFramework.getBootPackages()).thenReturn(new String[0]); + + when(mockRevisionImpl.getContentPath()).thenReturn(contentPath); + when(mockContent.getEntryAsBytes(testClassAsPath)).thenReturn( + testClassBytes); + + HookRegistry hReg = mock(HookRegistry.class); + when(hReg.getHooks(WeavingHook.class)).thenReturn(hooks); + when(mockFramework.getHookRegistry()).thenReturn(hReg); + when( + mockFramework.getService(mockFramework, + mockServiceReferenceWeavingHook, false)).thenReturn( + new BadDummyWovenHook()); + + when(hReg.getHooks(WovenClassListener.class)).thenReturn( + listeners); + when( + mockFramework.getService(mockFramework, + mockServiceReferenceWovenClassListener, false)) + .thenReturn(dummyWovenClassListener); + + BundleClassLoader bundleClassLoader = createBundleClassLoader( + BundleClassLoader.class, bundleWiring); + assertNotNull(bundleClassLoader); + + try + { + + bundleClassLoader.findClass(TestClass.class.getName()); + fail("Class should throw exception"); + } catch (Error e) + { + // This is expected + } + + assertEquals("There should be 1 state changes fired by the weaving", 1, + dummyWovenClassListener.stateList.size()); + assertEquals( + "The only state change should be a failed transform on the class", + (Object)WovenClass.TRANSFORMING_FAILED, + dummyWovenClassListener.stateList.get(0)); + + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void testFindClassWeaveDefineError() throws Exception + { + Felix mockFramework = mock(Felix.class); + Content mockContent = mock(Content.class); + ServiceReference mockServiceReferenceWeavingHook = mock(ServiceReference.class); + ServiceReference mockServiceReferenceWovenClassListener = mock(ServiceReference.class); + + Set> hooks = new HashSet>(); + hooks.add(mockServiceReferenceWeavingHook); + + DummyWovenClassListener dummyWovenClassListener = new DummyWovenClassListener(); + + Set> listeners = new HashSet>(); + listeners.add(mockServiceReferenceWovenClassListener); + + Class testClass = TestClass.class; + String testClassName = testClass.getName(); + String testClassAsPath = testClassName.replace('.', '/') + ".class"; + byte[] testClassBytes = createTestClassBytes(testClass, testClassAsPath); + + List contentPath = new ArrayList(); + contentPath.add(mockContent); + initializeSimpleBundleWiring(); + + when(mockBundle.getFramework()).thenReturn(mockFramework); + when(mockFramework.getBootPackages()).thenReturn(new String[0]); + + when(mockRevisionImpl.getContentPath()).thenReturn(contentPath); + when(mockContent.getEntryAsBytes(testClassAsPath)).thenReturn( + testClassBytes); + + HookRegistry hReg = mock(HookRegistry.class); + when(hReg.getHooks(WeavingHook.class)).thenReturn(hooks); + when(mockFramework.getHookRegistry()).thenReturn(hReg); + when( + mockFramework.getService(mockFramework, + mockServiceReferenceWeavingHook, false)).thenReturn( + new BadDefineWovenHook()); + + when(hReg.getHooks(WovenClassListener.class)).thenReturn( + listeners); + when( + mockFramework.getService(mockFramework, + mockServiceReferenceWovenClassListener, false)) + .thenReturn(dummyWovenClassListener); + + BundleClassLoader bundleClassLoader = createBundleClassLoader( + BundleClassLoader.class, bundleWiring); + assertNotNull(bundleClassLoader); + try + { + + bundleClassLoader.findClass(TestClass.class.getName()); + fail("Class should throw exception"); + } catch (Throwable e) + { + + } + assertEquals("There should be 2 state changes fired by the weaving", 2, + dummyWovenClassListener.stateList.size()); + assertEquals("The first state change should transform the class", + (Object)WovenClass.TRANSFORMED, + dummyWovenClassListener.stateList.get(0)); + assertEquals("The second state change failed the define on the class", + (Object)WovenClass.DEFINE_FAILED, + dummyWovenClassListener.stateList.get(1)); + } + + private ConcurrentHashMap getAccessorCache(BundleWiringImpl wiring) throws NoSuchFieldException, IllegalAccessException { + Field m_accessorLookupCache = BundleWiringImpl.class.getDeclaredField("m_accessorLookupCache"); + m_accessorLookupCache.setAccessible(true); + return (ConcurrentHashMap) m_accessorLookupCache.get(wiring); + } + + @Test + public void testFirstGeneratedAccessorSkipClassloading() throws Exception + { + + String classToBeLoaded = "sun.reflect.GeneratedMethodAccessor21"; + + Felix mockFramework = mock(Felix.class); + when(mockFramework.getBootPackages()).thenReturn(new String[0]); + + initializeSimpleBundleWiring(); + + when(bundleWiring.getBundle().getFramework()).thenReturn(mockFramework); + + BundleClassLoader bundleClassLoader = createBundleClassLoader( + BundleClassLoader.class, bundleWiring); + assertNotNull(bundleClassLoader); + + try { + bundleClassLoader.loadClass(classToBeLoaded, true); + fail(); + } catch (ClassNotFoundException cnf) { + //this is expected + + //make sure boot delegation was done before CNF was thrown + verify(mockFramework).getBootPackages(); + + //make sure the class is added to the skip class cache + assertEquals(getAccessorCache(bundleWiring).get(classToBeLoaded), BundleWiringImpl.CNFE_CLASS_LOADER); + } catch (Exception e) { + e.printStackTrace(); + fail(); + } + } + + @SuppressWarnings("rawtypes") + public void initializeBundleWiringWithImportsAndRequired(Map importedPkgs, Map> requiredPkgs) throws Exception + { + + mockResolver = mock(StatefulResolver.class); + mockRevisionImpl = mock(BundleRevisionImpl.class); + mockBundle = mock(BundleImpl.class); + + Logger logger = new Logger(); + Map configMap = new HashMap(); + List fragments = new ArrayList(); + List wires = new ArrayList(); + + when(mockRevisionImpl.getBundle()).thenReturn(mockBundle); + when(mockBundle.getBundleId()).thenReturn(Long.valueOf(1)); + + bundleWiring = new BundleWiringImpl(logger, configMap, mockResolver, + mockRevisionImpl, fragments, wires, importedPkgs, requiredPkgs); + } + + @Test + public void testAccessorFirstLoadFailed() throws Exception + { + + String classToBeLoaded = "sun.reflect.GeneratedMethodAccessor21"; + + Felix mockFramework = mock(Felix.class); + when(mockFramework.getBootPackages()).thenReturn(new String[0]); + + Map importedPkgs = mock(Map.class); + Map> requiredPkgs = mock(Map.class); + + initializeBundleWiringWithImportsAndRequired(importedPkgs, requiredPkgs); + + when(bundleWiring.getBundle().getFramework()).thenReturn(mockFramework); + + BundleClassLoader bundleClassLoader = createBundleClassLoader( + BundleClassLoader.class, bundleWiring); + assertNotNull(bundleClassLoader); + + try { + bundleClassLoader.loadClass(classToBeLoaded, true); + fail(); + } catch (ClassNotFoundException cnf) { + //this is expected + + //make sure boot delegation was done before CNF was thrown + verify(mockFramework).getBootPackages(); + + //make sure imported and required pkgs are searched + verify(importedPkgs).values(); + verify(requiredPkgs).values(); + + //make sure the class is added to the skip class cache + assertEquals(getAccessorCache(bundleWiring).get(classToBeLoaded), BundleWiringImpl.CNFE_CLASS_LOADER); + } catch (Exception e) { + e.printStackTrace(); + fail(); + } + } + + @Test + public void testAccessorSubsequentLoadFailed() throws Exception + { + + String classToBeLoaded = "sun.reflect.GeneratedMethodAccessor21"; + + Felix mockFramework = mock(Felix.class); + when(mockFramework.getBootPackages()).thenReturn(new String[0]); + + Map importedPkgs = mock(Map.class); + Map> requiredPkgs = mock(Map.class); + + initializeBundleWiringWithImportsAndRequired(importedPkgs, requiredPkgs); + + when(bundleWiring.getBundle().getFramework()).thenReturn(mockFramework); + + BundleClassLoader bundleClassLoader = createBundleClassLoader( + BundleClassLoader.class, bundleWiring); + assertNotNull(bundleClassLoader); + + //first attempt to populate the cache + try { + bundleClassLoader.loadClass(classToBeLoaded, true); + fail(); + } catch (ClassNotFoundException cnf) { + //this is expected + } + + //now test that the subsequent class load throws CNF with out boot delegation and import/required packages + try { + + importedPkgs = mock(Map.class); + requiredPkgs = mock(Map.class); + initializeBundleWiringWithImportsAndRequired(importedPkgs, requiredPkgs); + mockFramework = mock(Felix.class); + when(mockFramework.getBootPackages()).thenReturn(new String[0]); + when(bundleWiring.getBundle().getFramework()).thenReturn(mockFramework); + bundleClassLoader.loadClass(classToBeLoaded, true); + fail(); + } catch (ClassNotFoundException cnf) { + //this is expected + + //make sure boot delegation was not used + verify(mockFramework, never()).getBootPackages(); + + //make sure boot import and required packages were not searched + verify(importedPkgs, never()).values(); + verify(requiredPkgs, never()).values(); + + } catch (Exception e) { + e.printStackTrace(); + fail(); + } + } + + private BundleRevision getBundleRevision(String classToBeLoaded, BundleClassLoader pkgBundleClassLoader, Object value) throws ClassNotFoundException { + BundleRevision bundleRevision = mock(BundleRevision.class); + BundleWiring pkgBundleWiring = mock(BundleWiring.class); + when(pkgBundleClassLoader.findLoadedClassInternal(classToBeLoaded)).thenAnswer(createAnswer(value)); + when(pkgBundleClassLoader.loadClass(classToBeLoaded)).thenAnswer(createAnswer(value)); + + when(pkgBundleWiring.getClassLoader()).thenReturn(pkgBundleClassLoader); + when(bundleRevision.getWiring()).thenReturn(pkgBundleWiring); + return bundleRevision; + } + + @Test + public void testAccessorLoadImportPackage() throws Exception + { + + String classToBeLoaded = "sun.reflect.GeneratedMethodAccessor21"; + + Felix mockFramework = mock(Felix.class); + when(mockFramework.getBootPackages()).thenReturn(new String[0]); + + Map importedPkgs = mock(Map.class); + BundleClassLoader foundClassLoader = mock(BundleClassLoader.class); + BundleClassLoader notFoundClassLoader = mock(BundleClassLoader.class); + BundleRevision bundleRevision1 = getBundleRevision(classToBeLoaded, foundClassLoader, String.class); + BundleRevision bundleRevision2 = getBundleRevision(classToBeLoaded, notFoundClassLoader, null); + Map importedPkgsActual = new LinkedHashMap(); + importedPkgsActual.put("sun.reflect1", bundleRevision1); + importedPkgsActual.put("sun.reflect2", bundleRevision2); + when(importedPkgs.values()).thenReturn(importedPkgsActual.values()); + Map> requiredPkgs = mock(Map.class); + + initializeBundleWiringWithImportsAndRequired(importedPkgs, requiredPkgs); + + when(bundleWiring.getBundle().getFramework()).thenReturn(mockFramework); + + BundleClassLoader bundleClassLoader = createBundleClassLoader( + BundleClassLoader.class, bundleWiring); + assertNotNull(bundleClassLoader); + + //call class load to populate the cache + try { + Object result = bundleClassLoader.loadClass(classToBeLoaded, true); + assertNotNull(result); + assertTrue(getAccessorCache(bundleWiring).containsKey(classToBeLoaded)); + assertEquals(getAccessorCache(bundleWiring).get(classToBeLoaded), foundClassLoader); + verify(foundClassLoader, times(1)).findLoadedClassInternal(classToBeLoaded); + verify(notFoundClassLoader, never()).findLoadedClassInternal(classToBeLoaded); + } catch (Exception e) { + fail(); + } + + //now make sure subsequent class load happens from cached revision + Object result = bundleClassLoader.loadClass(classToBeLoaded, true); + assertNotNull(result); + //makes sure the look up cache is accessed and the class is loaded from cached revision + verify(foundClassLoader, times(1)).findLoadedClassInternal(classToBeLoaded); + verify(foundClassLoader, times(1)).loadClass(classToBeLoaded); + verify(notFoundClassLoader, never()).findLoadedClassInternal(classToBeLoaded); + } + + private static Answer createAnswer(final T value) { + Answer dummy = new Answer() { + @Override + public T answer(InvocationOnMock invocation) throws Throwable { + return value; + } + }; + return dummy; + } + + @Test + public void testAccessorBootDelegate() throws Exception + { + + String classToBeLoaded = "sun.reflect.GeneratedMethodAccessor21"; + + Felix mockFramework = mock(Felix.class); + when(mockFramework.getBootPackages()).thenReturn(new String[0]); + + Map importedPkgs = mock(Map.class); + BundleRevision bundleRevision1 = mock(BundleRevision.class); + Map importedPkgsActual = new HashMap(); + importedPkgsActual.put("sun.reflect1", bundleRevision1); + when(importedPkgs.values()).thenReturn(importedPkgsActual.values()); + Map> requiredPkgs = mock(Map.class); + + ClassLoader bootDelegateClassLoader = mock(ClassLoader.class); + + when(bootDelegateClassLoader.loadClass(classToBeLoaded)).thenAnswer(createAnswer(String.class)); + + initializeBundleWiringWithImportsAndRequired(importedPkgs, requiredPkgs); + + when(bundleWiring.getBundle().getFramework()).thenReturn(mockFramework); + + Field field = bundleWiring.getClass().getDeclaredField("m_bootClassLoader"); + field.setAccessible(true); + field.set(bundleWiring, bootDelegateClassLoader); + + BundleClassLoader bundleClassLoader = createBundleClassLoader( + BundleClassLoader.class, bundleWiring); + assertNotNull(bundleClassLoader); + + try { + Object result = bundleClassLoader.loadClass(classToBeLoaded, true); + assertNotNull(result); + verify(importedPkgs, never()).values(); + verify(requiredPkgs, never()).values(); + assertTrue(getAccessorCache(bundleWiring).containsKey(classToBeLoaded)); + assertTrue(getAccessorCache(bundleWiring).get(classToBeLoaded) == bootDelegateClassLoader); + } catch (Exception e) { + fail(); + } + + //now make sure subsequent class loading happens from boot delegation + Object result = bundleClassLoader.loadClass(classToBeLoaded, true); + assertNotNull(result); + //makes sure the look up cache is accessed and the class is loaded via boot delegation + verify(importedPkgs, never()).values(); + verify(requiredPkgs, never()).values(); + } + + @Test + public void testParallelClassload() throws Exception + { + + + Felix mockFramework = mock(Felix.class); + HookRegistry hReg = mock(HookRegistry.class); + Mockito.when(mockFramework.getHookRegistry()).thenReturn(hReg); + Content mockContent = mock(Content.class); + final Class testClass = TestClassSuper.class; + final String testClassName = testClass.getName(); + final String testClassAsPath = testClassName.replace('.', '/') + ".class"; + byte[] testClassBytes = createTestClassBytes(testClass, testClassAsPath); + + final Class testClass2 = TestClassChild.class; + final String testClassName2 = testClass2.getName(); + final String testClassAsPath2 = testClassName2.replace('.', '/') + ".class"; + byte[] testClassBytes2 = createTestClassBytes(testClass2, testClassAsPath2); + + final Class testClass3 = TestClass.class; + final String testClassName3 = testClass3.getName(); + final String testClassAsPath3 = testClassName3.replace('.', '/') + ".class"; + byte[] testClassBytes3 = createTestClassBytes(testClass3, testClassAsPath3); + + List contentPath = new ArrayList(); + contentPath.add(mockContent); + BundleWiringImpl bundleWiring; + + StatefulResolver mockResolver; + + BundleRevisionImpl mockRevisionImpl; + + BundleImpl mockBundle; + + mockResolver = mock(StatefulResolver.class); + mockRevisionImpl = mock(BundleRevisionImpl.class); + mockBundle = mock(BundleImpl.class); + + Logger logger = new Logger(); + Map configMap = new HashMap(); + List fragments = new ArrayList(); + List wires = new ArrayList(); + Map importedPkgs = new HashMap(); + Map> requiredPkgs = new HashMap>(); + + when(mockRevisionImpl.getBundle()).thenReturn(mockBundle); + when(mockBundle.getBundleId()).thenReturn(Long.valueOf(1)); + + bundleWiring = new BundleWiringImpl(logger, configMap, mockResolver, + mockRevisionImpl, fragments, wires, importedPkgs, requiredPkgs); + + when(mockBundle.getFramework()).thenReturn(mockFramework); + when(mockFramework.getBootPackages()).thenReturn(new String[0]); + + when(mockRevisionImpl.getContentPath()).thenReturn(contentPath); + when(mockContent.getEntryAsBytes(testClassAsPath)).thenReturn( + testClassBytes); + when(mockContent.getEntryAsBytes(testClassAsPath2)).thenReturn( + testClassBytes2); + when(mockContent.getEntryAsBytes(testClassAsPath3)).thenReturn( + testClassBytes3); + + + final TestBundleClassLoader bundleClassLoader = createBundleClassLoader( + TestBundleClassLoader.class, bundleWiring); + assertNotNull(bundleClassLoader); + + Field m_classLoader = bundleWiring.getClass().getDeclaredField("m_classLoader"); + m_classLoader.setAccessible(true); + m_classLoader.set(bundleWiring, bundleClassLoader); + + assertTrue(bundleClassLoader.isParallel()); + + final AtomicInteger loaded = new AtomicInteger(); + new Thread() { + public void run() { + try + { + loaded.set(bundleClassLoader.findClass(testClassName2) != null ? 1 : 2); + } + catch (Exception e) + { + e.printStackTrace(); + loaded.set(3); + } + } + }.start(); + + while (bundleClassLoader.m_gate.getQueueLength() == 0) + { + Thread.sleep(1); + } + + final AtomicInteger loaded2 = new AtomicInteger(); + new Thread() { + public void run() { + try + { + loaded2.set(bundleClassLoader.findClass(testClassName3) != null ? 1 : 2); + } + catch (ClassNotFoundException e) + { + e.printStackTrace(); + loaded2.set(3); + } + } + }.start(); + + while (loaded2.get() == 0) + { + Thread.sleep(1); + } + + assertEquals(0, loaded.get()); + assertEquals(1, bundleClassLoader.m_gate.getQueueLength()); + + loaded2.set(0); + Thread tester = new Thread() { + public void run() { + try + { + loaded2.set(bundleClassLoader.findClass(testClassName2) != null ? 1 : 2); + } + catch (ClassNotFoundException e) + { + e.printStackTrace(); + loaded2.set(3); + } + } + }; + tester.start(); + + Thread.sleep(100); + + assertEquals(0, loaded2.get()); + assertEquals(1, bundleClassLoader.m_gate.getQueueLength()); + + bundleClassLoader.m_gate.release(); + + + while (loaded.get() == 0) + { + Thread.sleep(1); + } + + assertEquals(1, loaded.get()); + + while (loaded2.get() == 0) + { + Thread.sleep(1); + } + assertEquals(1, loaded2.get()); + } + + @Test + public void testClassloadStress() throws Exception + { + ExecutorService executors = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 4); + final List exceptionsNP = Collections.synchronizedList(new ArrayList()); + final List exceptionsP = Collections.synchronizedList(new ArrayList()); + + for (int i = 0; i < 100; i++) { + executors.submit(i % 2 == 0 ? new Runnable() + { + @Override + public void run() + { + try + { + testNotParallelClassload(); + } + catch (Throwable e) + { + exceptionsNP.add(e); + } + } + } : new Runnable() + { + @Override + public void run() + { + try + { + testParallelClassload(); + } + catch (Throwable e) + { + exceptionsP.add(e); + } + } + }); + } + executors.shutdown(); + executors.awaitTermination(10, TimeUnit.MINUTES); + assertTrue(exceptionsNP.toString(), exceptionsNP.isEmpty()); + assertTrue(exceptionsP.toString(), exceptionsP.isEmpty()); + } + + @Test + public void testNotParallelClassload() throws Exception + { + + Felix mockFramework = mock(Felix.class); + HookRegistry hReg = mock(HookRegistry.class); + Mockito.when(mockFramework.getHookRegistry()).thenReturn(hReg); + Content mockContent = mock(Content.class); + final Class testClass = TestClassSuper.class; + final String testClassName = testClass.getName(); + final String testClassAsPath = testClassName.replace('.', '/') + ".class"; + byte[] testClassBytes = createTestClassBytes(testClass, testClassAsPath); + + final Class testClass2 = TestClassChild.class; + final String testClassName2 = testClass2.getName(); + final String testClassAsPath2 = testClassName2.replace('.', '/') + ".class"; + byte[] testClassBytes2 = createTestClassBytes(testClass2, testClassAsPath2); + + final Class testClass3 = TestClass.class; + final String testClassName3 = testClass3.getName(); + final String testClassAsPath3 = testClassName3.replace('.', '/') + ".class"; + byte[] testClassBytes3 = createTestClassBytes(testClass3, testClassAsPath3); + + List contentPath = new ArrayList(); + contentPath.add(mockContent); + BundleWiringImpl bundleWiring; + + StatefulResolver mockResolver; + + BundleRevisionImpl mockRevisionImpl; + + BundleImpl mockBundle; + + mockResolver = mock(StatefulResolver.class); + mockRevisionImpl = mock(BundleRevisionImpl.class); + mockBundle = mock(BundleImpl.class); + + Logger logger = new Logger(); + Map configMap = new HashMap(); + List fragments = new ArrayList(); + List wires = new ArrayList(); + Map importedPkgs = new HashMap(); + Map> requiredPkgs = new HashMap>(); + + when(mockRevisionImpl.getBundle()).thenReturn(mockBundle); + when(mockBundle.getBundleId()).thenReturn(Long.valueOf(1)); + + bundleWiring = new BundleWiringImpl(logger, configMap, mockResolver, + mockRevisionImpl, fragments, wires, importedPkgs, requiredPkgs); + + when(mockBundle.getFramework()).thenReturn(mockFramework); + when(mockFramework.getBootPackages()).thenReturn(new String[0]); + + when(mockRevisionImpl.getContentPath()).thenReturn(contentPath); + when(mockContent.getEntryAsBytes(testClassAsPath)).thenReturn( + testClassBytes); + when(mockContent.getEntryAsBytes(testClassAsPath2)).thenReturn( + testClassBytes2); + when(mockContent.getEntryAsBytes(testClassAsPath3)).thenReturn( + testClassBytes3); + + + final TestBundleClassLoader2 bundleClassLoader = createBundleClassLoader( + TestBundleClassLoader2.class, bundleWiring); + assertNotNull(bundleClassLoader); + + Field m_classLoader = bundleWiring.getClass().getDeclaredField("m_classLoader"); + m_classLoader.setAccessible(true); + m_classLoader.set(bundleWiring, bundleClassLoader); + + assertFalse(bundleClassLoader.isParallel()); + + final AtomicInteger loaded = new AtomicInteger(); + new Thread() { + public void run() { + try + { + loaded.set(bundleClassLoader.findClass(testClassName2) != null ? 1 : 2); + } + catch (Exception e) + { + e.printStackTrace(); + loaded.set(3); + } + } + }.start(); + + while (bundleClassLoader.m_gate.getQueueLength() == 0) + { + Thread.sleep(1); + } + + final AtomicInteger loaded2 = new AtomicInteger(); + new Thread() { + public void run() { + try + { + loaded2.set(bundleClassLoader.findClass(testClassName3) != null ? 1 : 2); + } + catch (ClassNotFoundException e) + { + e.printStackTrace(); + loaded2.set(3); + } + } + }.start(); + + Thread.sleep(100); + + assertEquals(0, loaded.get()); + assertEquals(0, loaded2.get()); + assertEquals(1, bundleClassLoader.m_gate.getQueueLength()); + + final AtomicInteger loaded3 = new AtomicInteger(); + Thread tester = new Thread() { + public void run() { + try + { + loaded3.set(bundleClassLoader.findClass(testClassName2) != null ? 1 : 2); + } + catch (ClassNotFoundException e) + { + e.printStackTrace(); + loaded3.set(3); + } + } + }; + tester.start(); + + Thread.sleep(100); + + assertEquals(0, loaded3.get()); + assertEquals(0, loaded2.get()); + + assertEquals(0, loaded.get()); + assertEquals(1, bundleClassLoader.m_gate.getQueueLength()); + + bundleClassLoader.m_gate.release(); + + + while (loaded.get() == 0) + { + Thread.sleep(1); + } + + assertEquals(1, loaded.get()); + + while (loaded2.get() == 0) + { + Thread.sleep(1); + } + assertEquals(1, loaded2.get()); + + while (loaded3.get() == 0) + { + Thread.sleep(1); + } + assertEquals(1, loaded3.get()); + } + + private static class TestBundleClassLoader extends BundleClassLoader + { + static { + ClassLoader.registerAsParallelCapable(); + } + + Semaphore m_gate = new Semaphore(0); + public TestBundleClassLoader(BundleWiringImpl wiring, ClassLoader parent, Logger logger) + { + super(wiring, parent, logger); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException + { + if (name.startsWith("java")) + { + return getClass().getClassLoader().loadClass(name); + } + return super.loadClass(name, resolve); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException + { + if (name.startsWith("java")) + { + return getClass().getClassLoader().loadClass(name); + } + if (name.equals(TestClassSuper.class.getName())) + { + m_gate.acquireUninterruptibly(); + } + return super.findClass(name); + } + } + + private static class TestBundleClassLoader2 extends BundleClassLoader + { + Semaphore m_gate = new Semaphore(0); + public TestBundleClassLoader2(BundleWiringImpl wiring, ClassLoader parent, Logger logger) + { + super(wiring, parent, logger); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException + { + if (name.startsWith("java")) + { + return getClass().getClassLoader().loadClass(name); + } + return super.loadClass(name, resolve); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException + { + if (name.startsWith("java")) + { + return getClass().getClassLoader().loadClass(name); + } + if (name.equals(TestClassSuper.class.getName())) + { + m_gate.acquireUninterruptibly(); + } + return super.findClass(name); + } + + @Override + protected boolean isParallel() + { + return false; + } + } + + @SuppressWarnings("rawtypes") + private byte[] createTestClassBytes(Class testClass, String testClassAsPath) + throws IOException + { + InputStream testClassResourceStream = testClass.getClassLoader() + .getResourceAsStream(testClassAsPath); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int curByte; + while ((curByte = testClassResourceStream.read()) != -1) + { + baos.write(curByte); + } + byte[] testClassBytes = baos.toByteArray(); + return testClassBytes; + } + + @SuppressWarnings("rawtypes") + private T createBundleClassLoader( + Class bundleClassLoaderClass, BundleWiringImpl bundleWiring) + throws Exception + { + Logger logger = new Logger(); + Constructor ctor = BundleRevisionImpl.getSecureAction().getConstructor( + bundleClassLoaderClass, + new Class[] { BundleWiringImpl.class, ClassLoader.class, + Logger.class }); + BundleRevisionImpl.getSecureAction().setAccesssible(ctor); + T bundleClassLoader = (T) BundleRevisionImpl + .getSecureAction().invoke( + ctor, + new Object[] { bundleWiring, + this.getClass().getClassLoader(), logger }); + return bundleClassLoader; + } + + class TestClass + { + // An empty test class to weave. + } + + class TestClassSuper + { + // An empty test class to weave. + } + + class TestClassChild extends TestClassSuper + { + + } + + class GoodDummyWovenHook implements WeavingHook + { + // Adds the awesomePublicField to a class + @Override + @SuppressWarnings("unchecked") + public void weave(WovenClass wovenClass) + { + byte[] wovenClassBytes = wovenClass.getBytes(); + ClassNode classNode = new ClassNode(); + ClassReader reader = new ClassReader(wovenClassBytes); + reader.accept(classNode, 0); + classNode.fields.add(new FieldNode(Opcodes.ACC_PUBLIC, + "awesomePublicField", "Ljava/lang/String;", null, null)); + ClassWriter writer = new ClassWriter(reader, Opcodes.ASM4); + classNode.accept(writer); + wovenClass.setBytes(writer.toByteArray()); + } + } + + class BadDefineWovenHook implements WeavingHook + { + // Adds the awesomePublicField twice to the class. This is bad java. + @Override + @SuppressWarnings("unchecked") + public void weave(WovenClass wovenClass) + { + byte[] wovenClassBytes = wovenClass.getBytes(); + ClassNode classNode = new ClassNode(); + ClassReader reader = new ClassReader(wovenClassBytes); + reader.accept(classNode, 0); + classNode.fields.add(new FieldNode(Opcodes.ACC_PUBLIC, + "awesomePublicField", "Ljava/lang/String;", null, null)); + classNode.fields.add(new FieldNode(Opcodes.ACC_PUBLIC, + "awesomePublicField", "Ljava/lang/String;", null, null)); + ClassWriter writer = new ClassWriter(reader, Opcodes.ASM4); + classNode.accept(writer); + wovenClass.setBytes(writer.toByteArray()); + } + } + + class BadDummyWovenHook implements WeavingHook + { + // Just Blow up + @Override + public void weave(WovenClass wovenClass) + { + throw new WeavingException("Bad Weaver!"); + } + } + + class DummyWovenClassListener implements WovenClassListener + { + public List stateList = new ArrayList(); + + @Override + public void modified(WovenClass wovenClass) + { + stateList.add(wovenClass.getState()); + } + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/CollisionHookTest.java b/framework/src/test/java/org/apache/felix/framework/CollisionHookTest.java new file mode 100644 index 00000000000..a04fe4a5992 --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/CollisionHookTest.java @@ -0,0 +1,311 @@ +/* + * 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.framework; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.framework.cache.BundleArchive; +import org.apache.felix.framework.cache.BundleArchiveRevision; +import org.mockito.Mockito; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.Version; +import org.osgi.framework.hooks.bundle.CollisionHook; + +import junit.framework.TestCase; + +public class CollisionHookTest extends TestCase +{ + public void testCollisionHookInstall() throws Exception + { + BundleImpl identicalBundle = mockBundleImpl(1L, "foo", "1.2.1.a"); + BundleImpl differentBundle = mockBundleImpl(2L, "bar", "1.2.1.a"); + BundleImpl originatingBundle = mockBundleImpl(4L, "xyz", "1.0.0"); + + CollisionHook testCollisionHook = new CollisionHook() + { + @Override + public void filterCollisions(int operationType, Bundle target, Collection collisionCandidates) + { + if ((target.getBundleId() == 4L) && (operationType == CollisionHook.INSTALLING)) + { + collisionCandidates.clear(); + } + } + }; + + @SuppressWarnings("unchecked") + ServiceReference chRef = Mockito.mock(ServiceReference.class); + + // Mock the framework + StatefulResolver mockResolver = Mockito.mock(StatefulResolver.class); + Felix felixMock = Mockito.mock(Felix.class); + HookRegistry hReg = mock(HookRegistry.class); + when(hReg.getHooks(CollisionHook.class)).thenReturn(Collections.singleton(chRef)); + when(felixMock.getHookRegistry()).thenReturn(hReg); + Mockito.when(felixMock.getResolver()).thenReturn(mockResolver); + Mockito.when(felixMock.getBundles()).thenReturn(new Bundle[] + { + differentBundle, identicalBundle + }); + Mockito.when(felixMock.getService(felixMock, chRef, false)).thenReturn(testCollisionHook); + + // Mock the archive of the bundle being installed + Map headerMap = new HashMap(); + headerMap.put(Constants.BUNDLE_SYMBOLICNAME, "foo"); + headerMap.put(Constants.BUNDLE_VERSION, "1.2.1.a"); + headerMap.put(Constants.BUNDLE_MANIFESTVERSION, "2"); + + BundleArchiveRevision archiveRevision = Mockito.mock(BundleArchiveRevision.class); + Mockito.when(archiveRevision.getManifestHeader()).thenReturn(headerMap); + + BundleArchive archive = Mockito.mock(BundleArchive.class); + Mockito.when(archive.getCurrentRevision()).thenReturn(archiveRevision); + Mockito.when(archive.getId()).thenReturn(3L); + + BundleImpl bi = new BundleImpl(felixMock, originatingBundle, archive); + assertEquals(3L, bi.getBundleId()); + + // Do the revise operation. + try + { + bi.revise(null, null); + fail("Should have thrown a BundleException because the installed bundle is not unique"); + } + catch (BundleException be) + { + // good + assertTrue(be.getMessage().contains("not unique")); + } + } + + public void testCollisionHookUpdate() throws Exception + { + BundleImpl identicalBundle = mockBundleImpl(1L, "foo", "1.2.1.a"); + BundleImpl differentBundle = mockBundleImpl(2L, "foo", "1.2.1"); + + CollisionHook testCollisionHook = new CollisionHook() + { + @Override + public void filterCollisions(int operationType, Bundle target, Collection collisionCandidates) + { + if ((target.getBundleId() == 3L) && (operationType == CollisionHook.UPDATING)) + { + collisionCandidates.clear(); + } + } + }; + + @SuppressWarnings("unchecked") + ServiceReference chRef = Mockito.mock(ServiceReference.class); + + Map config = new HashMap(); + config.put(Constants.FRAMEWORK_BSNVERSION, Constants.FRAMEWORK_BSNVERSION_MANAGED); + + // Mock the framework + StatefulResolver mockResolver = Mockito.mock(StatefulResolver.class); + Felix felixMock = Mockito.mock(Felix.class); + Mockito.when(felixMock.getConfig()).thenReturn(config); + HookRegistry hReg = mock(HookRegistry.class); + when(hReg.getHooks(CollisionHook.class)).thenReturn(Collections.singleton(chRef)); + when(felixMock.getHookRegistry()).thenReturn(hReg); + Mockito.when(felixMock.getResolver()).thenReturn(mockResolver); + Mockito.when(felixMock.getBundles()).thenReturn(new Bundle[] + { + differentBundle, identicalBundle + }); + Mockito.when(felixMock.getService(felixMock, chRef, false)).thenReturn(testCollisionHook); + + // Mock the archive of the bundle being installed + Map headerMap = new HashMap(); + headerMap.put(Constants.BUNDLE_SYMBOLICNAME, "zar"); + headerMap.put(Constants.BUNDLE_VERSION, "1.2.1.a"); + headerMap.put(Constants.BUNDLE_MANIFESTVERSION, "2"); + + BundleArchiveRevision archiveRevision = Mockito.mock(BundleArchiveRevision.class); + Mockito.when(archiveRevision.getManifestHeader()).thenReturn(headerMap); + + BundleArchive archive = Mockito.mock(BundleArchive.class); + Mockito.when(archive.getCurrentRevision()).thenReturn(archiveRevision); + Mockito.when(archive.getId()).thenReturn(3L); + + BundleImpl bi = new BundleImpl(felixMock, null, archive); + assertEquals("zar", bi.getSymbolicName()); + + // Do the revise operation, change the bsn to foo + headerMap.put(Constants.BUNDLE_SYMBOLICNAME, "foo"); + bi.revise(null, null); + assertEquals("foo", bi.getSymbolicName()); + } + + public void testCollisionNotEnabled() throws Exception + { + BundleImpl identicalBundle = mockBundleImpl(1L, "foo", "1.2.1.a"); + BundleImpl differentBundle = mockBundleImpl(2L, "bar", "1.2.1.a"); + + CollisionHook testCollisionHook = new CollisionHook() + { + @Override + public void filterCollisions(int operationType, Bundle target, Collection collisionCandidates) + { + if ((target.getBundleId() == 3L) && (operationType == CollisionHook.INSTALLING)) + { + collisionCandidates.clear(); + } + } + }; + + @SuppressWarnings("unchecked") + ServiceReference chRef = Mockito.mock(ServiceReference.class); + + Map config = new HashMap(); + config.put(Constants.FRAMEWORK_BSNVERSION, Constants.FRAMEWORK_BSNVERSION_SINGLE); + + // Mock the framework + StatefulResolver mockResolver = Mockito.mock(StatefulResolver.class); + Felix felixMock = Mockito.mock(Felix.class); + Mockito.when(felixMock.getConfig()).thenReturn(config); + HookRegistry hReg = mock(HookRegistry.class); + when(hReg.getHooks(CollisionHook.class)).thenReturn(Collections.singleton(chRef)); + when(felixMock.getHookRegistry()).thenReturn(hReg); + Mockito.when(felixMock.getResolver()).thenReturn(mockResolver); + Mockito.when(felixMock.getBundles()).thenReturn(new Bundle[] + { + differentBundle, identicalBundle + }); + Mockito.when(felixMock.getService(felixMock, chRef, false)).thenReturn(testCollisionHook); + + // Mock the archive of the bundle being installed + Map headerMap = new HashMap(); + headerMap.put(Constants.BUNDLE_SYMBOLICNAME, "foo"); + headerMap.put(Constants.BUNDLE_VERSION, "1.2.1.a"); + headerMap.put(Constants.BUNDLE_MANIFESTVERSION, "2"); + + BundleArchiveRevision archiveRevision = Mockito.mock(BundleArchiveRevision.class); + Mockito.when(archiveRevision.getManifestHeader()).thenReturn(headerMap); + + BundleArchive archive = Mockito.mock(BundleArchive.class); + Mockito.when(archive.getCurrentRevision()).thenReturn(archiveRevision); + Mockito.when(archive.getId()).thenReturn(3L); + + try + { + new BundleImpl(felixMock, null, archive); + fail("Should have thrown a BundleException because the collision hook is not enabled"); + } + catch (BundleException be) + { + // good + assertTrue(be.getMessage().contains("not unique")); + } + } + + public void testAllowMultiple() throws Exception + { + BundleImpl identicalBundle = mockBundleImpl(1L, "foo", "1.2.1.a"); + BundleImpl differentBundle = mockBundleImpl(2L, "bar", "1.2.1.a"); + + Map config = new HashMap(); + config.put(Constants.FRAMEWORK_BSNVERSION, Constants.FRAMEWORK_BSNVERSION_MULTIPLE); + + // Mock the framework + StatefulResolver mockResolver = Mockito.mock(StatefulResolver.class); + Felix felixMock = Mockito.mock(Felix.class); + Mockito.when(felixMock.getConfig()).thenReturn(config); + Mockito.when(felixMock.getResolver()).thenReturn(mockResolver); + Mockito.when(felixMock.getBundles()).thenReturn(new Bundle[] + { + differentBundle, identicalBundle + }); + + // Mock the archive of the bundle being installed + Map headerMap = new HashMap(); + headerMap.put(Constants.BUNDLE_SYMBOLICNAME, "foo"); + headerMap.put(Constants.BUNDLE_VERSION, "1.2.1.a"); + headerMap.put(Constants.BUNDLE_MANIFESTVERSION, "2"); + + BundleArchiveRevision archiveRevision = Mockito.mock(BundleArchiveRevision.class); + Mockito.when(archiveRevision.getManifestHeader()).thenReturn(headerMap); + + BundleArchive archive = Mockito.mock(BundleArchive.class); + Mockito.when(archive.getCurrentRevision()).thenReturn(archiveRevision); + Mockito.when(archive.getId()).thenReturn(3L); + + BundleImpl bi = new BundleImpl(felixMock, null, archive); + assertEquals(3L, bi.getBundleId()); + } + + public void testNoCollisionHook() throws Exception + { + BundleImpl identicalBundle = mockBundleImpl(1L, "foo", "1.2.1.a"); + BundleImpl differentBundle = mockBundleImpl(2L, "bar", "1.2.1.a"); + + // Mock the framework + StatefulResolver mockResolver = Mockito.mock(StatefulResolver.class); + Felix felixMock = Mockito.mock(Felix.class); + HookRegistry hReg = Mockito.mock(HookRegistry.class); + Mockito.when(felixMock.getHookRegistry()).thenReturn(hReg); + Mockito.when(felixMock.getResolver()).thenReturn(mockResolver); + Mockito.when(felixMock.getBundles()).thenReturn(new Bundle[] + { + differentBundle, identicalBundle + }); + + // Mock the archive of the bundle being installed + Map headerMap = new HashMap(); + headerMap.put(Constants.BUNDLE_SYMBOLICNAME, "foo"); + headerMap.put(Constants.BUNDLE_VERSION, "1.2.1.a"); + headerMap.put(Constants.BUNDLE_MANIFESTVERSION, "2"); + + BundleArchiveRevision archiveRevision = Mockito.mock(BundleArchiveRevision.class); + Mockito.when(archiveRevision.getManifestHeader()).thenReturn(headerMap); + + BundleArchive archive = Mockito.mock(BundleArchive.class); + Mockito.when(archive.getCurrentRevision()).thenReturn(archiveRevision); + Mockito.when(archive.getId()).thenReturn(3L); + + try + { + new BundleImpl(felixMock, null, archive); + fail("Should have thrown a BundleException because the installed bundle is not unique"); + } + catch (BundleException be) + { + // good + assertTrue(be.getMessage().contains("not unique")); + } + } + + private BundleImpl mockBundleImpl(long id, String bsn, String version) + { + BundleImpl identicalBundle = Mockito.mock(BundleImpl.class); + Mockito.when(identicalBundle.getSymbolicName()).thenReturn(bsn); + Mockito.when(identicalBundle.getVersion()).thenReturn(Version.parseVersion(version)); + Mockito.when(identicalBundle.getBundleId()).thenReturn(id); + return identicalBundle; + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/ConcurrencyTest.java b/framework/src/test/java/org/apache/felix/framework/ConcurrencyTest.java new file mode 100644 index 00000000000..fef1e6e587b --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/ConcurrencyTest.java @@ -0,0 +1,350 @@ +/* + * 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.framework; + +import static java.lang.System.err; +import static java.lang.System.out; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.launch.Framework; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +import junit.framework.Assert; +import junit.framework.TestCase; + +/** + * This test performs concurrent registration of service components that have dependencies between each other. + * There are 10 components. The first one has an optional dependency on the second one, the second on the third , etc ... The last component + * does not have any dependencies. + * At the beginning of a concurrent test, the nth component creates a service tracker on the nth+1 component and registers in the registry + * (and components and their associated Service Trackers are registered/opened concurrently). + * At the end of an iteration test, we check that all nth component are properly injected (satisfied) with the nth+1 component (except the + * last one which has no dependency). + */ +public class ConcurrencyTest extends TestCase +{ + public static final int DELAY = 1000; + final static int NPROCS = Runtime.getRuntime().availableProcessors(); + final static int COMPONENTS = (NPROCS == 1) ? 4 : NPROCS; + final static int ITERATIONS = 50000; + + /** + * Threadpool which can be optionally used by parallel scenarios. + */ + private final static Executor TPOOL = Executors.newFixedThreadPool(COMPONENTS); + + /** + * Latch used to ensure that the test has completed propertly. + */ + private final CountDownLatch m_testDone = new CountDownLatch(1); + + /** + * Starts a concurrent test. + */ + public void testConcurrentComponents() throws Exception + { + Map params = new HashMap(); + params.put(Constants.FRAMEWORK_SYSTEMPACKAGES, + "org.osgi.framework; version=1.4.0," + "org.osgi.service.packageadmin; version=1.2.0," + + "org.osgi.service.startlevel; version=1.1.0," + "org.osgi.util.tracker; version=1.3.3," + + "org.osgi.service.url; version=1.0.0"); + File cacheDir = File.createTempFile("felix-cache", ".dir"); + cacheDir.delete(); + cacheDir.mkdirs(); + String cache = cacheDir.getPath(); + params.put("felix.cache.profiledir", cache); + params.put("felix.cache.dir", cache); + params.put(Constants.FRAMEWORK_STORAGE, cache); + + Framework f = new Felix(params); + f.init(); + f.start(); + + try + { + out.println("Starting load test."); + Loader loader = new Loader(f.getBundleContext()); + loader.start(); + + Assert.assertTrue(m_testDone.await(60, TimeUnit.SECONDS)); + loader.stop(); + } + finally + { + f.stop(); + Thread.sleep(DELAY); + deleteDir(cacheDir); + } + } + + private static void deleteDir(File root) throws IOException + { + if (root.isDirectory()) + { + for (File file : root.listFiles()) + { + deleteDir(file); + } + } + assertTrue(root.delete()); + } + + /** + * A simple component, that creates a service tracker on another component and registers in the osgi service registry. + */ + public class Component implements ServiceTrackerCustomizer + { + private final BundleContext m_ctx; + private final int m_id; + private volatile int m_dependsOnId = -1; + private volatile ServiceTracker m_tracker; + private volatile boolean m_satisfied; + private volatile ServiceRegistration m_registration; + + public Component(BundleContext ctx, int id) + { + m_ctx = ctx; + m_id = id; + } + + public void dependsOn(int componentId) + { + m_dependsOnId = componentId; + } + + public void start() + { + // if we depends on another component, add the dependency. + if (m_dependsOnId != -1) + { + Filter filter; + try + { + filter = m_ctx.createFilter( + "(&(objectClass=" + Component.class.getName() + ")(id=" + m_dependsOnId + "))"); + } + catch (InvalidSyntaxException e) + { + e.printStackTrace(); + return; + } + m_tracker = new ServiceTracker(m_ctx, filter, this); + m_tracker.open(); + } + else + { + // We don't depend on anything, mark our satisfied flag to true + m_satisfied = true; + } + register(); + } + + public boolean isSatisfied() + { + return m_satisfied; + } + + public void stop() + { + if (m_tracker != null) + { + m_tracker.close(); + } + m_registration.unregister(); + } + + public Component addingService(ServiceReference reference) + { + Component service = m_ctx.getService(reference); + String id = (String) reference.getProperty("id"); + if (String.valueOf(m_dependsOnId).equals(id)) + { + m_satisfied = true; + } + else + { + System.err.println("Component#" + m_id + " received wrong dependency #" + id); + } + + return service; + } + + public void modifiedService(ServiceReference reference, Component service) + { + } + + public void removedService(ServiceReference reference, Component service) + { + try + { + m_ctx.ungetService(reference); + } + catch (IllegalStateException e) + { + e.printStackTrace(); + } + } + + private void register() { + Hashtable properties = new Hashtable(); + properties.put("id", String.valueOf(m_id)); + m_registration = m_ctx.registerService(Component.class.getName(), this, properties); + } + } + + public class Loader implements Runnable + { + final BundleContext m_ctx; + private Thread m_thread; + + Loader(BundleContext ctx) + { + m_ctx = ctx; + } + + public void start() + { + m_thread = new Thread(this); + m_thread.start(); + } + + public void stop() + { + m_thread.interrupt(); + } + + public void run() + { + // Creates all components. Each nth components will depends on the + // nth+1 component. The last component does not depend on another one. + + out.println("Starting Concurrency test ..."); + for (int i = 0; i < ITERATIONS; i++) + { + try + { + if (i % 1000 == 0) + { + out.println("."); + } + createComponentsConcurrently(i); + } + catch (Throwable error) + { + err.println("Test failed"); + error.printStackTrace(err); + return; + } + } + + out.println("\nTest successful."); + m_testDone.countDown(); + } + + private void createComponentsConcurrently(int iteration) throws Exception + { + // Create components. + final List components = new ArrayList(); + for (int i = 0; i < COMPONENTS; i++) + { + components.add(createComponents(iteration, i)); + } + + // Start all components asynchronously. + final CountDownLatch latch = new CountDownLatch(components.size()); + for (int i = 0; i < COMPONENTS; i++) + { + final Component component = components.get(i); + TPOOL.execute(new Runnable() + { + public void run() + { + try + { + component.start(); + } + finally + { + latch.countDown(); + } + + } + }); + } + + if (!latch.await(5, TimeUnit.SECONDS)) + { + System.err.println("ThreadPool did not complete timely."); + return; + } + + // Count the number of satisfied components. + long satisfied = 0; + for (int i = 0; i < COMPONENTS; i++) + { + satisfied += (components.get(i).isSatisfied()) ? 1 : 0; + } + + // Report an error if we don't have expected satisfied components. + if (satisfied != COMPONENTS) { + for (int i = 0; i < COMPONENTS; i ++) { + if (! components.get(i).isSatisfied()) { + out.println("Component #" + i + " unsatisfied."); + } + } + Assert.fail("Found unsatisfied components: " + String.valueOf(COMPONENTS - satisfied)); + return; + } + + // Stop all components + for (int i = 0; i < COMPONENTS; i++) + { + components.get(i).stop(); + } + } + + private Component createComponents(int iteration, int i) + { + Component component = new Component(m_ctx, iteration + i); + if (i < (COMPONENTS - 1)) + { + component.dependsOn(iteration + i + 1); + } + return component; + } + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/ConcurrentBundleUpdateTest.java b/framework/src/test/java/org/apache/felix/framework/ConcurrentBundleUpdateTest.java new file mode 100644 index 00000000000..4b1618ff13a --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/ConcurrentBundleUpdateTest.java @@ -0,0 +1,412 @@ +/* + * 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.framework; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.Proxy.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.SynchronousBundleListener; + +import junit.framework.TestCase; + +public class ConcurrentBundleUpdateTest extends TestCase +{ + public void testConcurrentBundleUpdate() throws Exception + { + Map params = new HashMap(); + params.put(Constants.FRAMEWORK_SYSTEMPACKAGES, + "org.osgi.framework; version=1.4.0," + + "org.osgi.service.packageadmin; version=1.2.0," + + "org.osgi.service.startlevel; version=1.1.0," + + "org.osgi.util.tracker; version=1.3.3," + + "org.osgi.service.url; version=1.0.0"); + File cacheDir = File.createTempFile("felix-cache", ".dir"); + cacheDir.delete(); + cacheDir.mkdirs(); + String cache = cacheDir.getPath(); + params.put("felix.cache.profiledir", cache); + params.put("felix.cache.dir", cache); + params.put(Constants.FRAMEWORK_STORAGE, cache); + + + + try + { + Felix felix = new Felix(params); + try + { + felix.init(); + felix.start(); + + String mf = "Bundle-SymbolicName: test.concurrent.bundleupdate\n" + + "Bundle-Version: 1.0.0\n" + + "Bundle-ManifestVersion: 2\n" + + "Import-Package: org.osgi.framework\n" + + "Manifest-Version: 1.0\n" + + "Bundle-Activator: " + ConcurrentBundleUpdaterActivator.class.getName() + "\n\n"; + + final BundleImpl updater = (BundleImpl) felix.getBundleContext().installBundle(createBundle(mf, ConcurrentBundleUpdaterActivator.class).toURI().toURL().toString()); + + final Semaphore step = new Semaphore(0); + SynchronousBundleListener listenerStarting = new SynchronousBundleListener() + { + + @Override + public void bundleChanged(BundleEvent event) + { + if (event.getBundle().equals(updater) && event.getType() == BundleEvent.STARTING) + { + step.release(); + } + } + }; + felix.getBundleContext().addBundleListener(listenerStarting); + new Thread() + { + public void run() + { + try + { + updater.start(); + } + catch (Exception ex) + { + + } + } + }.start(); + + assertTrue(step.tryAcquire(1, TimeUnit.SECONDS)); + + felix.getBundleContext().removeBundleListener(listenerStarting); + + assertEquals(Bundle.STARTING, updater.getState()); + assertEquals(0, step.availablePermits()); + + new Thread() + { + public void run() + { + try + { + step.release(); + updater.update(); + step.release(); + } + catch (Exception ex) + { + } + } + }.start(); + assertTrue(step.tryAcquire(1, TimeUnit.SECONDS)); + SynchronousBundleListener listenerStarted = new SynchronousBundleListener() + { + @Override + public void bundleChanged(BundleEvent event) + { + if (event.getBundle().equals(updater) && event.getType() == BundleEvent.STARTED) + { + step.release(); + } + if (event.getBundle().equals(updater) && event.getType() == BundleEvent.STOPPING) + { + step.release(); + } + } + }; + felix.getBundleContext().addBundleListener(listenerStarted); + + ((Runnable) updater.getActivator()).run(); + + assertTrue(step.tryAcquire(2, 1, TimeUnit.SECONDS)); + + felix.getBundleContext().removeBundleListener(listenerStarted); + + assertEquals(0, step.availablePermits()); + + assertEquals(Bundle.STOPPING, updater.getState()); + + felix.getBundleContext().addBundleListener(listenerStarting); + + ((Runnable) updater.getActivator()).run(); + + assertTrue(step.tryAcquire(1, TimeUnit.SECONDS)); + + felix.getBundleContext().removeBundleListener(listenerStarting); + + assertEquals(Bundle.STARTING, updater.getState()); + + ((Runnable) updater.getActivator()).run(); + + assertTrue(step.tryAcquire(1, TimeUnit.SECONDS)); + + assertEquals(Bundle.ACTIVE, updater.getState()); + + ((Runnable) updater.getActivator()).run(); + + updater.uninstall(); + + assertEquals(Bundle.UNINSTALLED, updater.getState()); + + try + { + updater.update(); + fail("Expected exception on update of uninstalled bundle"); + } + catch (IllegalStateException expected) { + + } + } + finally + { + felix.stop(); + felix.waitForStop(1000); + } + } + finally + { + delete(cacheDir); + } + } + + public void testConcurrentBundleCycleUpdate() throws Exception + { + Map params = new HashMap(); + params.put(Constants.FRAMEWORK_SYSTEMPACKAGES, + "org.osgi.framework; version=1.4.0," + + "org.osgi.service.packageadmin; version=1.2.0," + + "org.osgi.service.startlevel; version=1.1.0," + + "org.osgi.util.tracker; version=1.3.3," + + "org.osgi.service.url; version=1.0.0"); + File cacheDir = File.createTempFile("felix-cache", ".dir"); + cacheDir.delete(); + cacheDir.mkdirs(); + String cache = cacheDir.getPath(); + params.put("felix.cache.profiledir", cache); + params.put("felix.cache.dir", cache); + params.put(Constants.FRAMEWORK_STORAGE, cache); + + + + try + { + Felix felix = new Felix(params); + try + { + felix.init(); + felix.start(); + + String mf = "Bundle-SymbolicName: test.concurrent.bundleupdate\n" + + "Bundle-Version: 1.0.0\n" + + "Bundle-ManifestVersion: 2\n" + + "Import-Package: org.osgi.framework\n" + + "Manifest-Version: 1.0\n" + + "Bundle-Activator: " + ConcurrentBundleUpdaterCycleActivator.class.getName() + "\n\n"; + + final BundleImpl updater = (BundleImpl) felix.getBundleContext().installBundle(createBundle(mf, ConcurrentBundleUpdaterCycleActivator.class).toURI().toURL().toString()); + + final Semaphore step = new Semaphore(0); + SynchronousBundleListener listenerStarting = new SynchronousBundleListener() + { + @Override + public void bundleChanged(BundleEvent event) + { + if (event.getBundle().equals(updater) && event.getType() == BundleEvent.STARTING) + { + step.release(); + } + } + }; + felix.getBundleContext().addBundleListener(listenerStarting); + new Thread() + { + public void run() + { + try + { + updater.start(); + } + catch (Exception ex) + { + step.release(); + } + } + }.start(); + + assertTrue(step.tryAcquire(1, TimeUnit.SECONDS)); + + felix.getBundleContext().removeBundleListener(listenerStarting); + + assertEquals(Bundle.STARTING, updater.getState()); + assertEquals(0, step.availablePermits()); + + ((Runnable) updater.getActivator()).run(); + + assertTrue(step.tryAcquire(1, TimeUnit.SECONDS)); + assertEquals(Bundle.RESOLVED, updater.getState()); + + updater.uninstall(); + + assertEquals(Bundle.UNINSTALLED, updater.getState()); + + try + { + updater.update(); + fail("Expected exception on update of uninstalled bundle"); + } + catch (IllegalStateException expected) { + + } + } + finally + { + felix.stop(); + felix.waitForStop(1000); + } + } + finally + { + delete(cacheDir); + } + } + + public static final class ConcurrentBundleUpdaterActivator implements BundleActivator, Runnable + { + Semaphore semaphore = new Semaphore(0); + + private BundleContext context; + + @Override + public void start(BundleContext context) throws Exception + { + this.context = context; + if (!semaphore.tryAcquire(1, TimeUnit.SECONDS)) + { + throw new BundleException("Timeout"); + } + } + + @Override + public void stop(BundleContext context) throws Exception + { + this.context = context; + if (!semaphore.tryAcquire(1, TimeUnit.SECONDS)) + { + throw new BundleException("Timeout"); + } + } + + @Override + public void run() + { + semaphore.release(); + } + + } + + public static final class ConcurrentBundleUpdaterCycleActivator implements BundleActivator, Runnable + { + Semaphore semaphore = new Semaphore(0); + + private BundleContext context; + + @Override + public void start(BundleContext context) throws Exception + { + this.context = context; + if (!semaphore.tryAcquire(1, TimeUnit.SECONDS)) + { + throw new BundleException("Timeout"); + } + context.getBundle().update(); + } + + @Override + public void stop(BundleContext context) throws Exception { + this.context = context; + if (!semaphore.tryAcquire(1, TimeUnit.SECONDS)) + { + throw new BundleException("Timeout"); + } + } + + @Override + public void run() + { + semaphore.release(); + } + + } + + private static File createBundle(String manifest, Class... classes) throws IOException + { + File f = File.createTempFile("felix-bundle", ".jar"); + f.deleteOnExit(); + + Manifest mf = new Manifest(new ByteArrayInputStream(manifest.getBytes("utf-8"))); + JarOutputStream os = new JarOutputStream(new FileOutputStream(f), mf); + + for (Class clazz : classes) + { + String path = clazz.getName().replace('.', '/') + ".class"; + os.putNextEntry(new ZipEntry(path)); + + InputStream is = clazz.getClassLoader().getResourceAsStream(path); + byte[] buffer = new byte[8 * 1024]; + for (int i = is.read(buffer); i != -1; i = is.read(buffer)) + { + os.write(buffer, 0, i); + } + is.close(); + os.closeEntry(); + } + os.close(); + return f; + } + + private static void delete(File file) throws IOException + { + if (file.isDirectory()) + { + for (File child : file.listFiles()) + { + delete(child); + } + } + file.delete(); + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/ConcurrentClassLoaderTest.java b/framework/src/test/java/org/apache/felix/framework/ConcurrentClassLoaderTest.java new file mode 100644 index 00000000000..b57c962932b --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/ConcurrentClassLoaderTest.java @@ -0,0 +1,152 @@ +/* + * 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.framework; + +import junit.framework.TestCase; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.launch.Framework; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +public class ConcurrentClassLoaderTest extends TestCase +{ + private static final int CONCURRENCY_LEVEL = 1000; + + private File m_cacheDir; + private Framework m_felix; + + public void testCanResolveClassInParallel() throws Exception + { + m_cacheDir = createCacheDir(); + m_felix = createFramework(m_cacheDir); + m_felix.init(); + m_felix.getBundleContext().installBundle(createBundleWithDynamicImportPackage()); + m_felix.start(); + + Bundle[] bundles = m_felix.getBundleContext().getBundles(); + assertEquals("Two, system and mine: " + Arrays.toString(bundles), 2, bundles.length); + final Bundle bundle = bundles[1]; + + // This latch ensures that all threads start at the same time + final CountDownLatch latch = new CountDownLatch(CONCURRENCY_LEVEL); + final AtomicInteger doneCount = new AtomicInteger(); + for (int i = 0; i < CONCURRENCY_LEVEL; i++) { + new Thread() + { + public void run() + { + try { + latch.countDown(); + latch.await(); + bundle.loadClass("com.sun.this.class.does.not.exist.but.asking.for.it.must.not.block"); + } catch (Exception e) { + // ignore + } finally { + doneCount.incrementAndGet(); + } + } + }.start(); + } + + // Wait for 1 minute for all threads to catch up + long timeout = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(1); + while (System.currentTimeMillis() < timeout && doneCount.get() != CONCURRENCY_LEVEL) { + Thread.sleep(50); + } + + assertEquals("Class resolution all started", 0, latch.getCount()); + assertEquals("Class resolution all finished", CONCURRENCY_LEVEL, doneCount.get()); + } + + @Override + protected void tearDown() throws Exception + { + super.tearDown(); + + m_felix.stop(); + m_felix.waitForStop(1000); + m_felix = null; + + delete(m_cacheDir); + } + + private static File createCacheDir() throws IOException + { + File cacheDir = File.createTempFile("felix-cache", ".dir"); + cacheDir.delete(); + cacheDir.mkdirs(); + return cacheDir; + } + + private static Framework createFramework(File cacheDir) + { + Map params = new HashMap(); + params.put(Constants.FRAMEWORK_SYSTEMPACKAGES, "org.osgi.framework; version=1.4.0," + + "org.osgi.service.packageadmin; version=1.2.0," + + "org.osgi.service.startlevel; version=1.1.0," + + "org.osgi.util.tracker; version=1.3.3," + + "org.osgi.service.url; version=1.0.0"); + + params.put("felix.cache.profiledir", cacheDir.getPath()); + params.put("felix.cache.dir", cacheDir.getPath()); + params.put(Constants.FRAMEWORK_STORAGE, cacheDir.getPath()); + + return new Felix(params); + } + + + private static String createBundleWithDynamicImportPackage() throws IOException + { + String manifest = "Bundle-SymbolicName: boot.test\n" + "Bundle-Version: 1.1.0\n" + "Bundle-ManifestVersion: 2\n" + "DynamicImport-Package: com.sun.*"; + + File f = File.createTempFile("felix-bundle", ".jar"); + f.deleteOnExit(); + + Manifest mf = new Manifest(new ByteArrayInputStream(manifest.getBytes("utf-8"))); + mf.getMainAttributes().putValue("Manifest-Version", "1.0"); + JarOutputStream os = new JarOutputStream(new FileOutputStream(f), mf); + os.close(); + + return f.toURI().toString(); + } + + private static void delete(File file) throws IOException { + if (file.isDirectory()) { + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + delete(child); + } + } + } + file.delete(); + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/CycleDetectionWithWovenClassTest.java b/framework/src/test/java/org/apache/felix/framework/CycleDetectionWithWovenClassTest.java new file mode 100644 index 00000000000..704c99dc3da --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/CycleDetectionWithWovenClassTest.java @@ -0,0 +1,267 @@ +/* + * 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.framework; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +import org.junit.Assert; +import org.junit.Assume; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleReference; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.hooks.weaving.WeavingHook; +import org.osgi.framework.hooks.weaving.WovenClass; + +import junit.framework.TestCase; + +public class CycleDetectionWithWovenClassTest extends TestCase { + + public void testDoesBootdelegateForClassloaderClassload() throws Exception{ + withFelixDo(new ThrowingConsumer() { + @Override + public void accept(Felix felix) throws Exception { + BundleImpl bundle = (BundleImpl) felix.getBundleContext().installBundle(createBundle( + WeavingActivator.class, CycleDetectionWithWovenClassTest.class, Hook.class, getClass().getClassLoader().loadClass("org.apache.felix.framework.CycleDetectionWithWovenClassTest$WeavingActivator$1") +).toURI().toURL().toString()); + + bundle.start(); + + Runnable testClass = felix.getBundleContext().getService( + felix.getBundleContext().getServiceReference(Runnable.class)); + + testClass.run(); + } + }); + } + + + public static class WeavingActivator implements BundleActivator, Runnable { + private volatile BundleContext context; + private volatile boolean wasNull = false; + @Override + public void start(final BundleContext context) throws Exception { + this.context = context; + context.registerService(WeavingHook.class.getName(), new ServiceFactory() + { + + @Override + public WeavingHook getService(Bundle bundle, ServiceRegistration registration) + { + // TODO Auto-generated method stub + try + { + Class hook = getClass().getClassLoader().loadClass("org.apache.felix.framework.CycleDetectionWithWovenClassTest$Hook"); + if (hook == null) + { + wasNull = true; + return null; + } + else { + return (WeavingHook) hook.newInstance(); + } + + } + catch (ClassNotFoundException e) + { + // This is what we expect. + } + catch (Exception ex) { + wasNull = true; + } + return null; + } + + @Override + public void ungetService(Bundle bundle, ServiceRegistration registration, + WeavingHook service) + { + // TODO Auto-generated method stub + + } + }, null); + + context.registerService(Runnable.class, this, null); + } + + @Override + public void stop(BundleContext context) throws Exception { + // TODO Auto-generated method stub + + } + + public void run() { + try + { + ((Callable) context.getBundle().loadClass("org.apache.felix.framework.CycleDetectionWithWovenClassTest$Hook").newInstance()).call(); + + if (wasNull) + { + throw new IllegalStateException("Returned null from nested classload"); + } + } catch (Exception e) + { + throw new RuntimeException(e); + } + } + + } + public static class Hook implements WeavingHook, Callable { + + private static boolean woven = false; + @Override + public void weave(WovenClass wovenClass) + { + woven = true; + } + + @Override + public Boolean call() throws Exception + { + return woven; + } + + } + + private void withFelixDo(ThrowingConsumer consumer) throws Exception { + File cacheDir = File.createTempFile("felix-cache", ".dir"); + try + { + Felix felix = getFramework(cacheDir); + try + { + felix.init(); + felix.start(); + consumer.accept(felix); + } + finally + { + felix.stop(); + felix.waitForStop(1000); + } + } + finally + { + delete(cacheDir); + } + } + + @FunctionalInterface + private static interface ThrowingConsumer { + public void accept(T t) throws Exception; + } + + + private Felix getFramework(File cacheDir) { + Map params = new HashMap(); + params.put(Constants.FRAMEWORK_SYSTEMPACKAGES, + "org.osgi.framework; version=1.4.0," + + "org.osgi.service.packageadmin; version=1.2.0," + + "org.osgi.service.startlevel; version=1.1.0," + + "org.osgi.util.tracker; version=1.3.3," + + "org.osgi.service.url; version=1.0.0," + + "org.osgi.framework.hooks.weaving"); + cacheDir.delete(); + cacheDir.mkdirs(); + String cache = cacheDir.getPath(); + params.put("felix.cache.profiledir", cache); + params.put("felix.cache.dir", cache); + params.put(Constants.FRAMEWORK_STORAGE, cache); + + return new Felix(params); + } + + private static File createBundle(Class activator, Class...classes) throws IOException + { + String mf = "Bundle-SymbolicName: " + activator.getName() +"\n" + + "Bundle-Version: 1.0.0\n" + + "Bundle-ManifestVersion: 2\n" + + "Import-Package: org.osgi.framework,org.osgi.framework.hooks.weaving\n" + + "Manifest-Version: 1.0\n" + + "Bundle-Activator: " + activator.getName() + "\n\n"; + + Class[] classesCombined; + + if (classes.length > 0) { + List list = new ArrayList(Arrays.asList(classes)); + list.add(activator); + classesCombined = list.toArray(new Class[0]); + } + else + { + classesCombined = new Class[]{activator}; + } + return createBundle(mf,classesCombined); + + } + + private static File createBundle(String manifest, Class... classes) throws IOException + { + File f = File.createTempFile("felix-bundle", ".jar"); + f.deleteOnExit(); + + Manifest mf = new Manifest(new ByteArrayInputStream(manifest.getBytes("utf-8"))); + JarOutputStream os = new JarOutputStream(new FileOutputStream(f), mf); + + for (Class clazz : classes) + { + String path = clazz.getName().replace('.', '/') + ".class"; + os.putNextEntry(new ZipEntry(path)); + + InputStream is = clazz.getClassLoader().getResourceAsStream(path); + byte[] buffer = new byte[8 * 1024]; + for (int i = is.read(buffer); i != -1; i = is.read(buffer)) + { + os.write(buffer, 0, i); + } + is.close(); + os.closeEntry(); + } + os.close(); + return f; + } + + private static void delete(File file) throws IOException + { + if (file.isDirectory()) + { + for (File child : file.listFiles()) + { + delete(child); + } + } + file.delete(); + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/DTOFactoryTest.java b/framework/src/test/java/org/apache/felix/framework/DTOFactoryTest.java new file mode 100644 index 00000000000..43658354b56 --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/DTOFactoryTest.java @@ -0,0 +1,290 @@ +/* + * 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.framework; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.dto.ServiceReferenceDTO; +import org.osgi.framework.launch.Framework; +import org.osgi.framework.namespace.BundleNamespace; +import org.osgi.framework.namespace.HostNamespace; +import org.osgi.framework.namespace.IdentityNamespace; +import org.osgi.framework.startlevel.BundleStartLevel; +import org.osgi.framework.startlevel.dto.BundleStartLevelDTO; +import org.osgi.framework.wiring.dto.BundleRevisionDTO; +import org.osgi.framework.wiring.dto.BundleWiringDTO; +import org.osgi.resource.dto.CapabilityDTO; + +public class DTOFactoryTest +{ + private int counter; + private Framework framework; + private File testDir; + + @Before + public void setUp() throws Exception + { + String path = "/" + getClass().getName().replace('.', '/') + ".class"; + String url = getClass().getResource(path).getFile(); + String baseDir = url.substring(0, url.length() - path.length()); + String rndStr = Long.toString(System.nanoTime(), Character.MAX_RADIX); + rndStr = rndStr.substring(rndStr.length() - 6, rndStr.length() - 1); + testDir = new File(baseDir, getClass().getSimpleName() + "_" + rndStr); + + File cacheDir = new File(testDir, "cache"); + cacheDir.mkdirs(); + String cache = cacheDir.getAbsolutePath(); + + Map params = new HashMap(); + params.put("felix.cache.profiledir", cache); + params.put("felix.cache.dir", cache); + params.put(Constants.FRAMEWORK_STORAGE, cache); + + framework = new Felix(params); + framework.init(); + framework.start(); + } + + @After + public void tearDown() throws Exception + { + framework.stop(); + } + + @Test + public void testBundleStartLevelDTO() throws Exception + { + String mf = "Bundle-SymbolicName: tb1\n" + + "Bundle-Version: 1.0.0\n" + + "Bundle-ManifestVersion: 2\n"; + File bf = createBundle(mf); + Bundle bundle = framework.getBundleContext().installBundle(bf.toURI().toURL().toExternalForm()); + + BundleStartLevel sl = bundle.adapt(BundleStartLevel.class); + sl.setStartLevel(7); + + BundleStartLevelDTO dto = bundle.adapt(BundleStartLevelDTO.class); + assertEquals(bundle.getBundleId(), dto.bundle); + assertEquals(7, dto.startLevel); + } + + @Test + public void testServiceReferenceDTOArray() throws Exception + { + ServiceRegistration reg = framework.getBundleContext().registerService(String.class, "hi", null); + Long sid = (Long) reg.getReference().getProperty(Constants.SERVICE_ID); + + ServiceReferenceDTO[] dtos = framework.adapt(ServiceReferenceDTO[].class); + assertTrue(dtos.length > 0); + + boolean found = false; + for (ServiceReferenceDTO dto : dtos) + { + if (dto.id == sid) + { + found = true; + assertEquals(0L, dto.bundle); + assertEquals(sid, dto.properties.get(Constants.SERVICE_ID)); + assertTrue(Arrays.equals(new String [] {String.class.getName()}, + (String []) dto.properties.get(Constants.OBJECTCLASS))); + assertEquals(0L, dto.properties.get(Constants.SERVICE_BUNDLEID)); + assertEquals(Constants.SCOPE_SINGLETON, dto.properties.get(Constants.SERVICE_SCOPE)); + assertEquals(0, dto.usingBundles.length); + } + } + assertTrue(found); + } + + @Test + public void testServiceReferenceDTOArrayStoppedBundle() throws Exception + { + String mf = "Bundle-SymbolicName: tb2\n" + + "Bundle-Version: 1.2.3\n" + + "Bundle-ManifestVersion: 2\n" + + "Import-Package: org.osgi.framework;version=\"[1.1,2)\""; + File bf = createBundle(mf); + Bundle bundle = framework.getBundleContext().installBundle(bf.toURI().toURL().toExternalForm()); + + assertNull("Precondition", bundle.getBundleContext()); + ServiceReferenceDTO[] dtos = bundle.adapt(ServiceReferenceDTO[].class); + + // Note this is incorrectly tested by the Core Framework R6 CT, which expects an + // empty array. However this is not correct and recorded as a deviation. + assertNull("As the bundle is not started, the dtos should be null", dtos); + } + + @Test + public void testBundleRevisionDTO() throws Exception + { + String mf = "Bundle-SymbolicName: tb2\n" + + "Bundle-Version: 1.2.3\n" + + "Bundle-ManifestVersion: 2\n" + + "Import-Package: org.osgi.framework;version=\"[1.1,2)\""; + File bf = createBundle(mf); + Bundle bundle = framework.getBundleContext().installBundle(bf.toURI().toURL().toExternalForm()); + bundle.start(); + assertEquals("Precondition", Bundle.ACTIVE, bundle.getState()); + + BundleRevisionDTO dto = bundle.adapt(BundleRevisionDTO.class); + assertEquals(bundle.getBundleId(), dto.bundle); + assertTrue(dto.id != 0); + assertEquals("tb2", dto.symbolicName); + assertEquals("1.2.3", dto.version); + assertEquals(0, dto.type); + + boolean foundBundle = false; + boolean foundHost = false; + boolean foundIdentity = false; + int resource = 0; + for (CapabilityDTO cap : dto.capabilities) + { + assertTrue(cap.id != 0); + if (resource == 0) + resource = cap.resource; + else + assertEquals(resource, cap.resource); + + if (BundleNamespace.BUNDLE_NAMESPACE.equals(cap.namespace)) + { + foundBundle = true; + assertEquals("tb2", cap.attributes.get(BundleNamespace.BUNDLE_NAMESPACE)); + assertEquals("1.2.3", cap.attributes.get(BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE)); + } + else if (HostNamespace.HOST_NAMESPACE.equals(cap.namespace)) + { + foundHost = true; + assertEquals("tb2", cap.attributes.get(HostNamespace.HOST_NAMESPACE)); + assertEquals("1.2.3", cap.attributes.get(HostNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE)); + } + else if (IdentityNamespace.IDENTITY_NAMESPACE.equals(cap.namespace)) + { + foundIdentity = true; + assertEquals("tb2", cap.attributes.get(IdentityNamespace.IDENTITY_NAMESPACE)); + assertEquals("1.2.3", cap.attributes.get(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE)); + assertEquals(IdentityNamespace.TYPE_BUNDLE, cap.attributes.get(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE)); + } + } + assertTrue(foundBundle); + assertTrue(foundHost); + assertTrue(foundIdentity); + } + + @Test + public void testBundleRevisionDTOArray() throws Exception { + String mf = "Bundle-SymbolicName: tb2\n" + + "Bundle-Version: 1.2.3\n" + + "Bundle-ManifestVersion: 2\n" + + "Import-Package: org.osgi.framework;version=\"[1.1,2)\""; + File bf = createBundle(mf); + Bundle bundle = framework.getBundleContext().installBundle(bf.toURI().toURL().toExternalForm()); + bundle.start(); + assertEquals("Precondition", Bundle.ACTIVE, bundle.getState()); + + BundleRevisionDTO[] dtos = bundle.adapt(BundleRevisionDTO[].class); + assertEquals(1, dtos.length); + BundleRevisionDTO dto = dtos[0]; + + assertEquals(bundle.getBundleId(), dto.bundle); + assertTrue(dto.id != 0); + assertEquals("tb2", dto.symbolicName); + assertEquals("1.2.3", dto.version); + assertEquals(0, dto.type); + + boolean foundBundle = false; + boolean foundHost = false; + boolean foundIdentity = false; + int resource = 0; + for (CapabilityDTO cap : dto.capabilities) + { + assertTrue(cap.id != 0); + if (resource == 0) + resource = cap.resource; + else + assertEquals(resource, cap.resource); + + if (BundleNamespace.BUNDLE_NAMESPACE.equals(cap.namespace)) + { + foundBundle = true; + assertEquals("tb2", cap.attributes.get(BundleNamespace.BUNDLE_NAMESPACE)); + assertEquals("1.2.3", cap.attributes.get(BundleNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE)); + } + else if (HostNamespace.HOST_NAMESPACE.equals(cap.namespace)) + { + foundHost = true; + assertEquals("tb2", cap.attributes.get(HostNamespace.HOST_NAMESPACE)); + assertEquals("1.2.3", cap.attributes.get(HostNamespace.CAPABILITY_BUNDLE_VERSION_ATTRIBUTE)); + } + else if (IdentityNamespace.IDENTITY_NAMESPACE.equals(cap.namespace)) + { + foundIdentity = true; + assertEquals("tb2", cap.attributes.get(IdentityNamespace.IDENTITY_NAMESPACE)); + assertEquals("1.2.3", cap.attributes.get(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE)); + assertEquals(IdentityNamespace.TYPE_BUNDLE, cap.attributes.get(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE)); + } + } + assertTrue(foundBundle); + assertTrue(foundHost); + assertTrue(foundIdentity); + } + + @Test + public void testBundleWiringDTO() throws Exception { + String mf = "Bundle-SymbolicName: tb2\n" + + "Bundle-Version: 1.2.3\n" + + "Bundle-ManifestVersion: 2\n" + + "Import-Package: org.osgi.framework;version=\"[1.1,2)\""; + File bf = createBundle(mf); + Bundle bundle = framework.getBundleContext().installBundle(bf.toURI().toURL().toExternalForm()); + bundle.start(); + assertEquals("Precondition", Bundle.ACTIVE, bundle.getState()); + + BundleWiringDTO dto = bundle.adapt(BundleWiringDTO.class); + assertEquals(bundle.getBundleId(), dto.bundle); + } + + private File createBundle(String manifest) throws IOException + { + File f = File.createTempFile("felix-bundle" + counter++, ".jar", testDir); + + Manifest mf = new Manifest(new ByteArrayInputStream(manifest.getBytes("utf-8"))); + mf.getMainAttributes().putValue("Manifest-Version", "1.0"); + JarOutputStream os = new JarOutputStream(new FileOutputStream(f), mf); + + os.close(); + return f; + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/EventDispatcherTest.java b/framework/src/test/java/org/apache/felix/framework/EventDispatcherTest.java new file mode 100644 index 00000000000..d9df01adc2f --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/EventDispatcherTest.java @@ -0,0 +1,157 @@ +/* + * 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.framework; + +import java.util.*; + +import junit.framework.TestCase; + +import org.easymock.EasyMock; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.hooks.service.EventHook; + +public class EventDispatcherTest extends TestCase +{ + public void testFireServiceEvent() + { + final Bundle b1 = getMockBundle(); + final Bundle b2 = getMockBundle(); + final Bundle b3 = getMockBundle(); + final Bundle b4 = getMockBundle(); + + final Set calledHooks = new HashSet(); + final EventHook eh1 = new EventHook() + { + public void event(ServiceEvent event, Collection contexts) + { + calledHooks.add(this); + } + }; + final EventHook eh2 = new EventHook() + { + public void event(ServiceEvent event, Collection contexts) + { + calledHooks.add(this); + for (Iterator it = contexts.iterator(); it.hasNext();) + { + BundleContext bc = (BundleContext) it.next(); + if (bc.getBundle() == b1) + { + it.remove(); + } + if (bc.getBundle() == b2) + { + it.remove(); + } + } + } + }; + + Logger logger = new Logger(); + ServiceRegistry registry = new ServiceRegistry(logger, null); + registry.registerService(b4, new String [] {EventHook.class.getName()}, eh1, new Hashtable()); + registry.registerService(b4, new String [] {EventHook.class.getName()}, eh2, new Hashtable()); + + // -- Set up event dispatcher + EventDispatcher ed = new EventDispatcher(logger, registry); + + // -- Register some listeners + final List fired = Collections.synchronizedList(new ArrayList()); + ServiceListener sl1 = new ServiceListener() + { + public void serviceChanged(ServiceEvent arg0) + { + fired.add(this); + } + }; + ed.addListener(b1.getBundleContext(), ServiceListener.class, sl1, null); + + ServiceListener sl2 = new ServiceListener() + { + public void serviceChanged(ServiceEvent arg0) + { + fired.add(this); + } + }; + ed.addListener(b2.getBundleContext(), ServiceListener.class, sl2, null); + + ServiceListener sl3 = new ServiceListener() + { + public void serviceChanged(ServiceEvent arg0) + { + fired.add(this); + } + }; + ed.addListener(b3.getBundleContext(), ServiceListener.class, sl3, null); + + // --- make the invocation + ServiceReference sr = EasyMock.createNiceMock(ServiceReference.class); + EasyMock.expect(sr.getProperty(Constants.OBJECTCLASS)).andReturn(new String[] + { + "java.lang.String" + }).anyTimes(); + sr.isAssignableTo(b1, String.class.getName()); + EasyMock.expectLastCall().andReturn(Boolean.TRUE).anyTimes(); + sr.isAssignableTo(b2, String.class.getName()); + EasyMock.expectLastCall().andReturn(Boolean.TRUE).anyTimes(); + sr.isAssignableTo(b3, String.class.getName()); + EasyMock.expectLastCall().andReturn(Boolean.TRUE).anyTimes(); + EasyMock.replay(new Object[] + { + sr + }); + + ServiceEvent event = new ServiceEvent(ServiceEvent.REGISTERED, sr); + + assertEquals("Precondition failed", 0, fired.size()); + + Felix framework = new Felix(new HashMap()); + + ed.fireServiceEvent(event, null, framework); + assertEquals(1, fired.size()); + assertSame(sl3, fired.iterator().next()); + + assertEquals(2, calledHooks.size()); + assertTrue(calledHooks.contains(eh1)); + assertTrue(calledHooks.contains(eh2)); + } + + private Bundle getMockBundle() + { + BundleContext bc = EasyMock.createNiceMock(BundleContext.class); + Bundle b = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(b.getBundleContext()).andReturn(bc).anyTimes(); + b.getState(); + EasyMock.expectLastCall().andReturn(Integer.valueOf(Bundle.ACTIVE)).anyTimes(); + + EasyMock.expect(bc.getBundle()).andReturn(b).anyTimes(); + + EasyMock.replay(new Object[] + { + bc, b + }); + + return b; + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/ExtensionManagerTest.java b/framework/src/test/java/org/apache/felix/framework/ExtensionManagerTest.java new file mode 100644 index 00000000000..7cb13e10d95 --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/ExtensionManagerTest.java @@ -0,0 +1,243 @@ +/* + * 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.framework; + +import org.apache.felix.framework.util.FelixConstants; +import org.apache.felix.framework.util.manifestparser.NativeLibraryClause; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.Version; +import org.osgi.framework.launch.Framework; +import org.osgi.framework.namespace.NativeNamespace; +import org.osgi.framework.wiring.BundleCapability; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * + * Test Classes for the ExtentionManager + * + */ +public class ExtensionManagerTest { + private int counter; + private File testDir; + + @Before + public void setUp() throws Exception { + testDir = File.createTempFile("felix-temp", ".dir"); + assertTrue("precondition", testDir.delete()); + assertTrue("precondition", testDir.mkdirs()); + + } + + @After + public void tearDown() throws Exception + { + deleteDir(testDir); + } + + private static void deleteDir(File root) throws IOException + { + if (root.isDirectory()) + { + for (File file : root.listFiles()) + { + deleteDir(file); + } + } + assertTrue(root.delete()); + } + + /** + * + * + * Ensure Native Bundle Capabilities are properly formed based on + * Framework properties. + * + */ + @Test + public void testBuildNativeCapabilities() { + Logger logger = new Logger(); + Map configMap = new HashMap(); + configMap.put(FelixConstants.FELIX_VERSION_PROPERTY, "1.0"); + configMap.put(FelixConstants.FRAMEWORK_LANGUAGE, "en"); + configMap.put(FelixConstants.FRAMEWORK_PROCESSOR, "x86_64"); + configMap.put(FelixConstants.FRAMEWORK_OS_NAME, "windows8"); + configMap.put(FelixConstants.FRAMEWORK_OS_VERSION, "6.3"); + configMap.put(FelixConstants.NATIVE_OS_NAME_ALIAS_PREFIX + ".windows8", "windows 8,win32"); + configMap.put(FelixConstants.NATIVE_PROC_NAME_ALIAS_PREFIX + ".x86-64", "amd64,em64t,x86_64"); + configMap.put(FelixConstants.FRAMEWORK_SYSTEMPACKAGES, "foo"); + NativeLibraryClause.initializeNativeAliases(configMap); + ExtensionManager extensionManager = new ExtensionManager(logger, configMap, null); + + BundleCapability nativeBundleCapability = extensionManager + .buildNativeCapabilites(extensionManager.getRevision(), configMap); + assertEquals( + "Native Language should be same as framework Language", + "en", + nativeBundleCapability.getAttributes().get( + NativeNamespace.CAPABILITY_LANGUAGE_ATTRIBUTE)); + assertTrue( + "Native Processor should be same as framework Processor", + Arrays.asList("x86-64", "amd64", "em64t", "x86_64").containsAll((List) + nativeBundleCapability.getAttributes().get( + NativeNamespace.CAPABILITY_PROCESSOR_ATTRIBUTE))); + assertTrue( + "Native OS Name should be the same as the framework os name", + Arrays.asList("windows8", "windows 8", "win32").containsAll((List) + nativeBundleCapability.getAttributes().get( + NativeNamespace.CAPABILITY_OSNAME_ATTRIBUTE))); + assertEquals( + "Native OS Version should be the same as the framework OS Version", + new Version("6.3"), + nativeBundleCapability.getAttributes().get( + NativeNamespace.CAPABILITY_OSVERSION_ATTRIBUTE)); + } + + @Test + public void testExtensionBundleActivator() throws Exception { + File cacheDir = new File(testDir, "cache"); + cacheDir.mkdirs(); + String cache = cacheDir.getAbsolutePath(); + + Map params = new HashMap(); + params.put("felix.cache.profiledir", cache); + params.put("felix.cache.dir", cache); + params.put(Constants.FRAMEWORK_STORAGE, cache); + + Framework framework = new Felix(params); + framework.init(); + framework.start(); + + try { + File ebf = createExtensionBundle(); + + assertEquals("Precondition", 0, activatorCalls.length()); + framework.getBundleContext().installBundle( + ebf.toURI().toURL().toExternalForm()); + + assertEquals("start", activatorCalls.toString()); + } finally { + framework.stop(); + } + + framework.waitForStop(10000); + assertEquals("startstop", activatorCalls.toString()); + } + + @Test + public void testSystemBundleHeaders() throws Exception + { + File cacheDir = new File(testDir, "cache"); + cacheDir.mkdirs(); + String cache = cacheDir.getAbsolutePath(); + + Map params = new HashMap(); + params.put("felix.cache.profiledir", cache); + params.put("felix.cache.dir", cache); + params.put(Constants.FRAMEWORK_STORAGE, cache); + + Framework framework = new Felix(params); + framework.init(); + framework.start(); + + Version version = new Version(System.getProperty("java.specification.version")); + String versionString; + if (version.getMajor() < 9) + { + versionString = String.format("0.0.0.JavaSE_001_%03d", version.getMinor() > 6 ? version.getMinor() : 6); + } + else + { + versionString = String.format("0.0.0.JavaSE_%03d", version.getMajor()); + } + assert(framework.getHeaders().get(Constants.EXPORT_PACKAGE).contains("java.lang; version=\"" + versionString + "\"")); + } + + private File createExtensionBundle() throws IOException { + File f = File.createTempFile("felix-bundle" + counter++, ".jar", testDir); + + Manifest mf = new Manifest(); + mf.getMainAttributes().putValue("Manifest-Version", "1.0"); + mf.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, "extension-bundle"); + mf.getMainAttributes().putValue(Constants.BUNDLE_VERSION, "3.2.1"); + mf.getMainAttributes().putValue(Constants.FRAGMENT_HOST, "system.bundle;extension:=framework"); + mf.getMainAttributes().putValue(Constants.BUNDLE_MANIFESTVERSION, "2"); + mf.getMainAttributes().putValue(Constants.EXTENSION_BUNDLE_ACTIVATOR, TestActivator.class.getName()); + JarOutputStream os = new JarOutputStream(new FileOutputStream(f), mf); + + String path = TestActivator.class.getName().replace('.', '/') + ".class"; + os.putNextEntry(new ZipEntry(path)); + + InputStream is = TestActivator.class.getClassLoader().getResourceAsStream(path); + pumpStreams(is, os); + + is.close(); + os.close(); + return f; + } + + static void pumpStreams(InputStream is, OutputStream os) throws IOException { + byte[] bytes = new byte[16384]; + + int length = 0; + int offset = 0; + + while ((length = is.read(bytes, offset, bytes.length - offset)) != -1) { + offset += length; + + if (offset == bytes.length) { + os.write(bytes, 0, bytes.length); + offset = 0; + } + } + if (offset != 0) { + os.write(bytes, 0, offset); + } + } + + private static StringBuilder activatorCalls = new StringBuilder(); + public static class TestActivator implements BundleActivator { + public void start(BundleContext context) throws Exception { + activatorCalls.append("start"); + } + + public void stop(BundleContext context) throws Exception { + activatorCalls.append("stop"); + } + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/FilterTest.java b/framework/src/test/java/org/apache/felix/framework/FilterTest.java new file mode 100644 index 00000000000..ceb111412be --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/FilterTest.java @@ -0,0 +1,46 @@ +/* + * 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.framework; + +import java.util.Dictionary; +import java.util.Hashtable; +import junit.framework.TestCase; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; + +public class FilterTest extends TestCase +{ + public void testMissingAttribute() + { + Dictionary dict = new Hashtable(); + dict.put("one", "one-value"); + dict.put("two", "two-value"); + dict.put("three", "three-value"); + Filter filter = null; + try + { + filter = FrameworkUtil.createFilter("(missing=value)"); + } + catch (Exception ex) + { + assertTrue("Filter should parse: " + ex, false); + } + assertFalse("Filter should not match: " + filter, filter.match(dict)); + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/FrameworkVersionTest.java b/framework/src/test/java/org/apache/felix/framework/FrameworkVersionTest.java new file mode 100644 index 00000000000..d9962fb8f41 --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/FrameworkVersionTest.java @@ -0,0 +1,49 @@ +/* + * 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.framework; + +import java.lang.reflect.Method; + +import junit.framework.TestCase; + +import org.osgi.framework.Version; + +public class FrameworkVersionTest extends TestCase +{ + public void testFrameworkVersion() throws Exception + { + testFrameworkVersion("1.0.0", "1"); + testFrameworkVersion("2.3.0", "2.3"); + testFrameworkVersion("1.0.0", "1.0.0"); + testFrameworkVersion("5.0.0.SNAPSHOT", "5-SNAPSHOT"); + testFrameworkVersion("1.0.0.SNAPSHOT", "1.0-SNAPSHOT"); + testFrameworkVersion("1.2.3.SNAPSHOT", "1.2.3-SNAPSHOT"); + testFrameworkVersion("1.2.3.foo-123", "1.2.3.foo-123"); + testFrameworkVersion("1.2.3.foo-123-hello", "1.2.3.foo-123-hello"); + } + + private void testFrameworkVersion(String out, String in) throws Exception + { + Method method = Felix.class.getDeclaredMethod("cleanMavenVersion", new Class [] {StringBuilder.class}); + method.setAccessible(true); + + StringBuilder sb = new StringBuilder(in); + assertEquals(new Version(out), new Version((String) method.invoke(null, sb))); + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/ImplicitBootDelegationTest.java b/framework/src/test/java/org/apache/felix/framework/ImplicitBootDelegationTest.java new file mode 100644 index 00000000000..60a629f7bff --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/ImplicitBootDelegationTest.java @@ -0,0 +1,333 @@ +/* + * 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.framework; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +import org.junit.Assert; +import org.junit.Assume; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleReference; +import org.osgi.framework.Constants; + +import junit.framework.Test; +import junit.framework.TestCase; + +public class ImplicitBootDelegationTest extends TestCase { + + public void testDoesBootdelegateForClassloaderClassload() throws Exception{ + withFelixDo(new ThrowingConsumer() { + @Override + public void accept(Felix felix) throws Exception { + BundleImpl bundle = (BundleImpl) felix.getBundleContext().installBundle(createBundle( + ImplicitBootDelegationTestActivator.class).toURI().toURL().toString()); + + bundle.start(); + + Runnable testClass = felix.getBundleContext().getService( + felix.getBundleContext().getServiceReference(Runnable.class)); + + Assert.assertEquals(TestClass.class, + testClass.getClass().getClassLoader().loadClass(TestClass.class.getName())); + } + }); + } + + public void testDoesNotBootdelegateForClassloadFromInsideBundle() throws Exception{ + withFelixDo(new ThrowingConsumer() { + @Override + public void accept(Felix felix) throws Exception { + BundleImpl bundle = (BundleImpl) felix.getBundleContext().installBundle(createBundle( + ImplicitBootDelegationTestActivator.class).toURI().toURL().toString()); + + bundle.start(); + + Runnable testClass = felix.getBundleContext().getService( + felix.getBundleContext().getServiceReference(Runnable.class)); + + try + { + testClass.run(); + Assert.fail("Expected to not be able to load an implicit bootdelegated class from inside the bundle"); + } catch (NoClassDefFoundError ex) { + + } + } + }); + } + + public void testDoesNotBootdelegateForBundleClassload() throws Exception { + withFelixDo(new ThrowingConsumer() { + @Override + public void accept(Felix felix) throws Exception { + BundleImpl bundle = (BundleImpl) felix.getBundleContext().installBundle(createBundle( + ImplicitBootDelegationTestActivator.class).toURI().toURL().toString()); + try + { + bundle.loadClass(TestClass.class.getName()); + Assert.fail("Expected to not be able to bundle.loadClass an implicit bootdelegated class"); + } + catch (ClassNotFoundException ex) { + + } + } + }); + } + + public void testDoesNotBootdelegateForServiceAssignability() throws Exception { + withFelixDo(new ThrowingConsumer() { + @Override + public void accept(Felix felix) throws Exception { + BundleImpl provider = (BundleImpl) felix.getBundleContext().installBundle(createBundle( + ProvidesActivator.class, TestClass.class).toURI().toURL().toString()); + + provider.start(); + + Assert.assertNotNull(felix.getBundleContext().getAllServiceReferences(TestClass.class.getName(), null)); + + BundleImpl requirer = (BundleImpl) felix.getBundleContext().installBundle(createBundle( + RequireActivator.class).toURI().toURL().toString()); + + requirer.start(); + + Runnable requirerActivtor = felix.getBundleContext().getService( + felix.getBundleContext().getServiceReference(Runnable.class)); + + Assume.assumeTrue(requirerActivtor.getClass().getClassLoader().loadClass(TestClass.class.getName()) + == TestClass.class); + + requirerActivtor.run(); + + Object service = requirer.getBundleContext().getService( + requirer.getBundleContext().getServiceReference(TestClass.class.getName())); + + assertNotNull(service); + assertTrue(!(service instanceof TestClass)); + assertTrue(service.getClass().getName().equals(TestClass.class.getName())); + } + }); + } + + public static class RequireActivator implements BundleActivator, Runnable { + private volatile BundleContext context; + @Override + public void start(BundleContext context) throws Exception { + this.context = context; + context.registerService(Runnable.class, this, null); + } + + @Override + public void stop(BundleContext context) throws Exception { + // TODO Auto-generated method stub + + } + + public void run() { + Object service = context.getService(context.getServiceReference( + "org.apache.felix.framework.ImplicitBootDelegationTest$TestClass")); + + if (service == null) + { + throw new IllegalStateException("Expected service to be available from inside bundle"); + } + + ClassLoader loader = service.getClass().getClassLoader(); + if (!(service.getClass().getClassLoader() instanceof BundleReference)) + { + throw new IllegalStateException("Expected service to be loaded from bundle"); + } + try + { + getClass().getClassLoader().loadClass(service.getClass().getName()); + throw new IllegalStateException("Expected to be unable to load service class"); + } + catch (ClassNotFoundException ex) { + + } + + try { + TestClass test = new TestClass(); + throw new IllegalStateException("Expected to be unable to create object of type TestClass"); + + } catch (NoClassDefFoundError ex) { + + } + } + } + + public static class ProvidesActivator implements BundleActivator { + + @Override + public void start(BundleContext context) throws Exception { + context.registerService(TestClass.class, new TestClass(), null); + } + + @Override + public void stop(BundleContext context) throws Exception { + // TODO Auto-generated method stub + + } + + } + + public static class TestClass { + } + + private void withFelixDo(ThrowingConsumer consumer) throws Exception { + File cacheDir = File.createTempFile("felix-cache", ".dir"); + try + { + Felix felix = getFramework(cacheDir); + try + { + felix.init(); + felix.start(); + consumer.accept(felix); + } + finally + { + felix.stop(); + felix.waitForStop(1000); + } + } + finally + { + delete(cacheDir); + } + } + + @FunctionalInterface + private static interface ThrowingConsumer { + public void accept(T t) throws Exception; + } + + + public static class ImplicitBootDelegationTestActivator implements BundleActivator, Runnable { + + @Override + public void start(BundleContext context) throws Exception { + context.registerService(Runnable.class, this, null); + } + + @Override + public void stop(BundleContext context) throws Exception { + } + + public void run() + { + new TestClass(); + } + } + + private Felix getFramework(File cacheDir) { + Map params = new HashMap(); + params.put(Constants.FRAMEWORK_SYSTEMPACKAGES, + "org.osgi.framework; version=1.4.0," + + "org.osgi.service.packageadmin; version=1.2.0," + + "org.osgi.service.startlevel; version=1.1.0," + + "org.osgi.util.tracker; version=1.3.3," + + "org.osgi.service.url; version=1.0.0"); + cacheDir.delete(); + cacheDir.mkdirs(); + String cache = cacheDir.getPath(); + params.put("felix.cache.profiledir", cache); + params.put("felix.cache.dir", cache); + params.put(Constants.FRAMEWORK_STORAGE, cache); + + return new Felix(params); + } + + private static File createBundle(Class activator, Class...classes) throws IOException + { + String mf = "Bundle-SymbolicName: " + activator.getName() +"\n" + + "Bundle-Version: 1.0.0\n" + + "Bundle-ManifestVersion: 2\n" + + "Import-Package: org.osgi.framework\n" + + "Manifest-Version: 1.0\n" + + "Bundle-Activator: " + activator.getName() + "\n\n"; + + Class[] classesCombined; + + if (classes.length > 0) { + List list = new ArrayList(Arrays.asList(classes)); + list.add(activator); + classesCombined = list.toArray(new Class[0]); + } + else + { + classesCombined = new Class[]{activator}; + } + return createBundle(mf,classesCombined); + + } + + private static File createBundle(String manifest, Class... classes) throws IOException + { + File f = File.createTempFile("felix-bundle", ".jar"); + f.deleteOnExit(); + + Manifest mf = new Manifest(new ByteArrayInputStream(manifest.getBytes("utf-8"))); + JarOutputStream os = new JarOutputStream(new FileOutputStream(f), mf); + + for (Class clazz : classes) + { + String path = clazz.getName().replace('.', '/') + ".class"; + os.putNextEntry(new ZipEntry(path)); + + InputStream is = clazz.getClassLoader().getResourceAsStream(path); + byte[] buffer = new byte[8 * 1024]; + for (int i = is.read(buffer); i != -1; i = is.read(buffer)) + { + os.write(buffer, 0, i); + } + is.close(); + os.closeEntry(); + } + os.close(); + return f; + } + + private static void delete(File file) throws IOException + { + if (file.isDirectory()) + { + for (File child : file.listFiles()) + { + delete(child); + } + } + file.delete(); + } +} + + + \ No newline at end of file diff --git a/framework/src/test/java/org/apache/felix/framework/MultiReleaseVersionTest.java b/framework/src/test/java/org/apache/felix/framework/MultiReleaseVersionTest.java new file mode 100644 index 00000000000..27ba50f5dd6 --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/MultiReleaseVersionTest.java @@ -0,0 +1,138 @@ +/* + * 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.framework; + +import junit.framework.TestCase; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.launch.Framework; +import org.osgi.framework.wiring.BundleWiring; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +public class MultiReleaseVersionTest extends TestCase +{ + public void testMultiReleaseVersionBundle() throws Exception + { + Map params = new HashMap(); + /*params.put(Constants.FRAMEWORK_SYSTEMPACKAGES, + "org.osgi.framework; version=1.4.0," + + "org.osgi.service.packageadmin; version=1.2.0," + + "org.osgi.service.startlevel; version=1.1.0," + + "org.osgi.util.tracker; version=1.3.3," + + "org.osgi.service.url; version=1.0.0");*/ + File cacheDir = File.createTempFile("felix-cache", ".dir"); + cacheDir.delete(); + cacheDir.mkdirs(); + String cache = cacheDir.getPath(); + params.put("felix.cache.profiledir", cache); + params.put("felix.cache.dir", cache); + params.put("java.specification.version", "9"); + params.put(Constants.FRAMEWORK_STORAGE, cache); + + String mf = "Bundle-SymbolicName: multi.test\n" + + "Bundle-Version: 1.0.0\n" + + "Bundle-ManifestVersion: 2\n" + + "Multi-Release: true\n"; + + File bundleFile = createBundle(mf, cacheDir); + + Framework f = null; + try + { + f = new Felix(params); + f.init(); + f.start(); + + Bundle b = f.getBundleContext().installBundle(bundleFile.toURI().toURL().toExternalForm()); + b.start(); + assertEquals(b.loadClass(Test.class.getName()).getName(), Test.class.getName()); + Collection org = b.adapt(BundleWiring.class).listResources("org", "*.class", BundleWiring.LISTRESOURCES_RECURSE); + assertEquals(1, org.size()); + assertEquals("org.osgi.framework", b.getHeaders().get(Constants.IMPORT_PACKAGE)); + } + finally + { + try + { + if (f != null) + { + f.stop(); + f.waitForStop(1000); + } + } + finally { + deleteDir(cacheDir); + } + } + } + + private static File createBundle(String manifest, File tempDir) throws IOException + { + File f = File.createTempFile("felix-bundle", ".jar", tempDir); + + Manifest mf = new Manifest(new ByteArrayInputStream(manifest.getBytes("utf-8"))); + mf.getMainAttributes().putValue("Manifest-Version", "1.0"); + //mf.getMainAttributes().putValue(Constants.BUNDLE_ACTIVATOR, StartStopBundleTest.TestBundleActivator.class.getName()); + JarOutputStream os = new JarOutputStream(new FileOutputStream(f), mf); + + String path = Test.class.getName().replace('.', '/') + ".class"; + os.putNextEntry(new ZipEntry("META-INF/versions/9/" + path)); + + InputStream is = Test.class.getClassLoader().getResourceAsStream(path); + byte[] b = new byte[is.available()]; + is.read(b); + is.close(); + os.write(b); + + os.putNextEntry(new ZipEntry("META-INF/versions/9/OSGI-INF/MANIFEST.MF")); + Manifest subMF = new Manifest( ); + subMF.getMainAttributes().putValue("Import-Package", "org.osgi.framework"); + subMF.getMainAttributes().putValue("Manifest-Version", "1.0"); + subMF.write(os); + os.close(); + + + return f; + } + + private static void deleteDir(File root) throws IOException + { + if (root.isDirectory()) + { + for (File file : root.listFiles()) + { + deleteDir(file); + } + } + assertTrue(root.delete()); + } + + public static class Test {} +} diff --git a/framework/src/test/java/org/apache/felix/framework/PackageAdminImplTest.java b/framework/src/test/java/org/apache/felix/framework/PackageAdminImplTest.java new file mode 100644 index 00000000000..bc295baee91 --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/PackageAdminImplTest.java @@ -0,0 +1,163 @@ +/* + * 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.framework; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +import junit.framework.TestCase; + +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.Version; +import org.osgi.service.packageadmin.ExportedPackage; + +public class PackageAdminImplTest extends TestCase +{ + private File tempDir; + private Felix felix; + private File cacheDir; + + @Override + protected void setUp() throws Exception + { + super.setUp(); + tempDir = File.createTempFile("felix-temp", ".dir"); + assertTrue("precondition", tempDir.delete()); + assertTrue("precondition", tempDir.mkdirs()); + + cacheDir = new File(tempDir, "felix-cache"); + assertTrue("precondition", cacheDir.mkdir()); + + String cache = cacheDir.getPath(); + + Map params = new HashMap(); + params.put("felix.cache.profiledir", cache); + params.put("felix.cache.dir", cache); + params.put(Constants.FRAMEWORK_STORAGE, cache); + + felix = new Felix(params); + felix.init(); + felix.start(); + } + + @Override + protected void tearDown() throws Exception + { + super.tearDown(); + + felix.stop(); // Note that this method is async + felix = null; + + deleteDir(tempDir); + tempDir = null; + cacheDir = null; + } + + public void testExportedPackages() throws Exception + { + String bmf = "Bundle-SymbolicName: pkg.bundle\n" + + "Bundle-Version: 1\n" + + "Bundle-ManifestVersion: 2\n" + + "Export-Package: org.foo.bundle\n"; + File bundleFile = createBundle(bmf); + + String fmf = "Bundle-SymbolicName: pkg.frag\n" + + "Bundle-Version: 1\n" + + "Fragment-Host: pkg.bundle\n" + + "Bundle-ManifestVersion: 2\n" + + "Export-Package: org.foo.fragment;version=\"2.0.0\"\n"; + File fragFile = createBundle(fmf); + + Bundle b = felix.getBundleContext().installBundle(bundleFile.toURI().toASCIIString()); + Bundle f = felix.getBundleContext().installBundle(fragFile.toURI().toASCIIString()); + b.start(); + + try + { + PackageAdminImpl pa = new PackageAdminImpl(felix); + assertEquals(b, pa.getExportedPackage("org.foo.bundle").getExportingBundle()); + assertEquals(b, pa.getExportedPackage("org.foo.fragment").getExportingBundle()); + + Set expected = new HashSet(); + expected.addAll(Arrays.asList("org.foo.bundle", "org.foo.fragment")); + + Set actual = new HashSet(); + for (ExportedPackage ep : pa.getExportedPackages(b)) + { + actual.add(ep.getName()); + assertEquals(b, ep.getExportingBundle()); + } + assertEquals(expected, actual); + + ExportedPackage[] bundlePkgs = pa.getExportedPackages("org.foo.bundle"); + assertEquals(1, bundlePkgs.length); + assertEquals(b, bundlePkgs[0].getExportingBundle()); + assertEquals(new Version("0"), bundlePkgs[0].getVersion()); + + ExportedPackage[] fragPkgs = pa.getExportedPackages("org.foo.fragment"); + assertEquals(1, fragPkgs.length); + assertEquals("The fragment package should be exposed through the bundle", + b, fragPkgs[0].getExportingBundle()); + assertEquals(new Version("2"), fragPkgs[0].getVersion()); + } + finally + { + b.stop(); + b.uninstall(); + f.uninstall(); + } + } + + private File createBundle(String manifest) throws IOException + { + File f = File.createTempFile("felix-bundle", ".jar", tempDir); + + Manifest mf = new Manifest(new ByteArrayInputStream(manifest.getBytes("UTF-8"))); + mf.getMainAttributes().putValue("Manifest-Version", "1.0"); + JarOutputStream os = new JarOutputStream(new FileOutputStream(f), mf); + os.close(); + return f; + } + + private static void deleteDir(File root) throws IOException + { + if (root.isDirectory()) + { + File[] files = root.listFiles(); + if (files != null) + { + for (File file : files) + { + deleteDir(file); + } + } + } + root.delete(); + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/RequirementsCapabilitiesTest.java b/framework/src/test/java/org/apache/felix/framework/RequirementsCapabilitiesTest.java new file mode 100644 index 00000000000..ecb3941f5d9 --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/RequirementsCapabilitiesTest.java @@ -0,0 +1,291 @@ +/* + * 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.framework; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +import junit.framework.TestCase; + +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.Version; +import org.osgi.framework.launch.Framework; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.resource.Capability; +import org.osgi.resource.Resource; + +public class RequirementsCapabilitiesTest extends TestCase +{ + private File tempDir; + private Framework felix; + private File cacheDir; + + @Override + protected void setUp() throws Exception + { + super.setUp(); + tempDir = File.createTempFile("felix-temp", ".dir"); + assertTrue("precondition", tempDir.delete()); + assertTrue("precondition", tempDir.mkdirs()); + + cacheDir = new File(tempDir, "felix-cache"); + assertTrue("precondition", cacheDir.mkdir()); + + String cache = cacheDir.getPath(); + + Map params = new HashMap(); + params.put("felix.cache.profiledir", cache); + params.put("felix.cache.dir", cache); + params.put(Constants.FRAMEWORK_STORAGE, cache); + + felix = new Felix(params); + felix.init(); + felix.start(); + } + + @Override + protected void tearDown() throws Exception + { + super.tearDown(); + + felix.stop(); // Note that this method is async + felix = null; + + deleteDir(tempDir); + tempDir = null; + cacheDir = null; + } + + public void testIdentityCapabilityBundleFragment() throws Exception + { + String bmf = "Bundle-SymbolicName: cap.bundle\n" + + "Bundle-Version: 1.2.3.Blah\n" + + "Bundle-ManifestVersion: 2\n" + + "Import-Package: org.osgi.framework\n"; + File bundleFile = createBundle(bmf); + + String fmf = "Bundle-SymbolicName: cap.frag\n" + + "Bundle-Version: 1.0.0\n" + + "Fragment-Host: cap.bundle\n" + + "Bundle-ManifestVersion: 2\n" + + "Export-Package: org.foo.bar;version=\"2.0.0\"\n" + + "Import-Package: org.osgi.util.tracker\n"; + File fragFile = createBundle(fmf); + + Bundle b = felix.getBundleContext().installBundle(bundleFile.toURI().toASCIIString()); + Bundle f = felix.getBundleContext().installBundle(fragFile.toURI().toASCIIString()); + + // Check the bundle capabilities. + // First check the capabilities on the Bundle Revision, which is available on installed bundles + BundleRevision bbr = b.adapt(BundleRevision.class); + List bwbCaps = bbr.getCapabilities("osgi.wiring.bundle"); + assertEquals(1, bwbCaps.size()); + + Map expectedBWBAttrs = new HashMap(); + expectedBWBAttrs.put("osgi.wiring.bundle", "cap.bundle"); + expectedBWBAttrs.put("bundle-version", Version.parseVersion("1.2.3.Blah")); + Capability expectedBWBCap = new TestCapability("osgi.wiring.bundle", + expectedBWBAttrs, Collections.emptyMap()); + assertCapsEquals(expectedBWBCap, bwbCaps.get(0)); + + List bwhCaps = bbr.getCapabilities("osgi.wiring.host"); + assertEquals(1, bwhCaps.size()); + + Map expectedBWHAttrs = new HashMap(); + expectedBWHAttrs.put("osgi.wiring.host", "cap.bundle"); + expectedBWHAttrs.put("bundle-version", Version.parseVersion("1.2.3.Blah")); + Capability expectedBWHCap = new TestCapability("osgi.wiring.host", + expectedBWHAttrs, Collections.emptyMap()); + assertCapsEquals(expectedBWHCap, bwhCaps.get(0)); + + List bwiCaps = bbr.getCapabilities("osgi.identity"); + assertEquals(1, bwiCaps.size()); + + Map expectedBWIAttrs = new HashMap(); + expectedBWIAttrs.put("osgi.identity", "cap.bundle"); + expectedBWIAttrs.put("type", "osgi.bundle"); + expectedBWIAttrs.put("version", Version.parseVersion("1.2.3.Blah")); + Capability expectedBWICap = new TestCapability("osgi.identity", + expectedBWIAttrs, Collections.emptyMap()); + assertCapsEquals(expectedBWICap, bwiCaps.get(0)); + + assertEquals("The Bundle should not directly expose osgi.wiring.package", + 0, bbr.getCapabilities("osgi.wiring.package").size()); + + // Check the fragment's capabilities. + // First check the capabilities on the Bundle Revision, which is available on installed fragments + BundleRevision fbr = f.adapt(BundleRevision.class); + List fwpCaps = fbr.getCapabilities("osgi.wiring.package"); + assertEquals(1, fwpCaps.size()); + + Map expectedFWAttrs = new HashMap(); + expectedFWAttrs.put("osgi.wiring.package", "org.foo.bar"); + expectedFWAttrs.put("version", Version.parseVersion("2")); + expectedFWAttrs.put("bundle-symbolic-name", "cap.frag"); + expectedFWAttrs.put("bundle-version", Version.parseVersion("1.0.0")); + Capability expectedFWCap = new TestCapability("osgi.wiring.package", + expectedFWAttrs, Collections.emptyMap()); + assertCapsEquals(expectedFWCap, fwpCaps.get(0)); + + List fiCaps = fbr.getCapabilities("osgi.identity"); + assertEquals(1, fiCaps.size()); + Map expectedFIAttrs = new HashMap(); + expectedFIAttrs.put("osgi.identity", "cap.frag"); + expectedFIAttrs.put("type", "osgi.fragment"); + expectedFIAttrs.put("version", Version.parseVersion("1.0.0")); + Capability expectedFICap = new TestCapability("osgi.identity", + expectedFIAttrs, Collections.emptyMap()); + assertCapsEquals(expectedFICap, fiCaps.get(0)); + + // Start the bundle. This will make the BundleWiring available on both the bundle and the fragment + b.start(); + + // Check the Bundle Wiring on the fragment. It should only contain the osgi.identity capability + // All the other capabilities should have migrated to the bundle's BundleWiring. + BundleWiring fbw = f.adapt(BundleWiring.class); + List fbwCaps = fbw.getCapabilities(null); + assertEquals("Fragment should only have 1 capability: it's osgi.identity", 1, fbwCaps.size()); + assertCapsEquals(expectedFICap, fbwCaps.get(0)); + + // Check the Bundle Wiring on the bundle. It should contain all the capabilities originally on the + // bundle and also contain the osgi.wiring.package capability from the fragment. + BundleWiring bbw = b.adapt(BundleWiring.class); + List bwbCaps2 = bbw.getCapabilities("osgi.wiring.bundle"); + assertEquals(1, bwbCaps2.size()); + assertCapsEquals(expectedBWBCap, bwbCaps2.get(0)); + List bwhCaps2 = bbw.getCapabilities("osgi.wiring.host"); + assertEquals(1, bwhCaps2.size()); + assertCapsEquals(expectedBWHCap, bwhCaps2.get(0)); + List bwiCaps2 = bbw.getCapabilities("osgi.identity"); + assertEquals(1, bwiCaps2.size()); + assertCapsEquals(expectedBWICap, bwiCaps2.get(0)); + List bwpCaps2 = bbw.getCapabilities("osgi.wiring.package"); + assertEquals("Bundle should have inherited the osgi.wiring.package capability from the fragment", + 1, bwpCaps2.size()); + assertCapsEquals(expectedFWCap, bwpCaps2.get(0)); + } + + public void testIdentityCapabilityFrameworkExtension() throws Exception + { + String femf = "Bundle-SymbolicName: fram.ext\n" + + "Bundle-Version: 1.2.3.test\n" + + "Fragment-Host: system.bundle; extension:=framework\n" + + "Bundle-ManifestVersion: 2\n" + + "Export-Package: org.foo.bar;version=\"2.0.0\"\n"; + File feFile = createBundle(femf); + + Bundle fe = felix.getBundleContext().installBundle(feFile.toURI().toASCIIString()); + + BundleRevision fbr = fe.adapt(BundleRevision.class); + + List feCaps = fbr.getCapabilities("osgi.identity"); + assertEquals(1, feCaps.size()); + Map expectedFEAttrs = new HashMap(); + expectedFEAttrs.put("osgi.identity", "fram.ext"); + expectedFEAttrs.put("type", "osgi.fragment"); + expectedFEAttrs.put("version", Version.parseVersion("1.2.3.test")); + Capability expectedFICap = new TestCapability("osgi.identity", + expectedFEAttrs, Collections.emptyMap()); + assertCapsEquals(expectedFICap, feCaps.get(0)); + } + + private File createBundle(String manifest) throws IOException + { + File f = File.createTempFile("felix-bundle", ".jar", tempDir); + + Manifest mf = new Manifest(new ByteArrayInputStream(manifest.getBytes("utf-8"))); + mf.getMainAttributes().putValue("Manifest-Version", "1.0"); + JarOutputStream os = new JarOutputStream(new FileOutputStream(f), mf); + os.close(); + return f; + } + + private static void assertCapsEquals(Capability expected, Capability actual) + { + assertEquals(expected.getNamespace(), actual.getNamespace()); + assertSubMap(expected.getAttributes(), actual.getAttributes()); + assertSubMap(expected.getDirectives(), actual.getDirectives()); + // We ignore the resource in the comparison + } + + private static void assertSubMap(Map subMap, Map fullMap) + { + for (Map.Entry entry : subMap.entrySet()) + { + assertEquals(entry.getValue(), fullMap.get(entry.getKey())); + } + } + + private static void deleteDir(File root) throws IOException + { + if (root.isDirectory()) + { + for (File file : root.listFiles()) + { + deleteDir(file); + } + } + assertTrue(root.delete()); + } + + static class TestCapability implements Capability + { + private final String namespace; + private final Map attributes; + private final Map directives; + + TestCapability(String ns, Map attrs, Map dirs) + { + namespace = ns; + attributes = attrs; + directives = dirs; + } + + public String getNamespace() + { + return namespace; + } + + public Map getAttributes() + { + return attributes; + } + + public Map getDirectives() + { + return directives; + } + + public Resource getResource() + { + return null; + } + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/ResolveTest.java b/framework/src/test/java/org/apache/felix/framework/ResolveTest.java new file mode 100644 index 00000000000..e898ad14e6d --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/ResolveTest.java @@ -0,0 +1,300 @@ +/* + * 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.framework; + +import junit.framework.TestCase; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.launch.Framework; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.framework.wiring.FrameworkWiring; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +public class ResolveTest extends TestCase +{ + private File tempDir; + private Framework felix; + private File cacheDir; + + @Override + protected void setUp() throws Exception + { + super.setUp(); + tempDir = File.createTempFile("felix-temp", ".dir"); + assertTrue("precondition", tempDir.delete()); + assertTrue("precondition", tempDir.mkdirs()); + + cacheDir = new File(tempDir, "felix-cache"); + assertTrue("precondition", cacheDir.mkdir()); + + String cache = cacheDir.getPath(); + + Map params = new HashMap(); + params.put("felix.cache.profiledir", cache); + params.put("felix.cache.dir", cache); + params.put(Constants.FRAMEWORK_STORAGE, cache); + + felix = new Felix(params); + felix.init(); + felix.start(); + } + + @Override + protected void tearDown() throws Exception + { + super.tearDown(); + + felix.stop(); // Note that this method is async + felix = null; + + deleteDir(tempDir); + tempDir = null; + cacheDir = null; + } + + public void testResolveFragmentWithHost() throws Exception + { + String bmf = "Bundle-SymbolicName: cap.bundle\n" + + "Bundle-Version: 1.2.3.Blah\n" + + "Bundle-ManifestVersion: 2\n" + + "Import-Package: org.osgi.framework\n"; + File bundleFile = createBundle(bmf); + + String fmf = "Bundle-SymbolicName: cap.frag\n" + + "Bundle-Version: 1.0.0\n" + + "Fragment-Host: cap.bundle\n" + + "Bundle-ManifestVersion: 2\n" + + "Export-Package: org.foo.bar;version=\"2.0.0\"\n" + + "Import-Package: org.osgi.util.tracker\n"; + File fragFile = createBundle(fmf); + + Bundle h = felix.getBundleContext().installBundle(bundleFile.toURI().toASCIIString()); + Bundle f = felix.getBundleContext().installBundle(fragFile.toURI().toASCIIString()); + + assertEquals(Bundle.INSTALLED, h.getState()); + assertEquals(Bundle.INSTALLED, f.getState()); + + felix.adapt(FrameworkWiring.class).resolveBundles(Collections.singletonList(h)); + + assertEquals(Bundle.RESOLVED, h.getState()); + assertEquals(Bundle.RESOLVED, f.getState()); + } + + public void testResolveOnlyMatchingFragmentWithHost() throws Exception + { + String bmf = "Bundle-SymbolicName: cap.bundle\n" + + "Bundle-Version: 1.2.3.Blah\n" + + "Bundle-ManifestVersion: 2\n" + + "Import-Package: org.osgi.framework\n"; + File bundleFile = createBundle(bmf); + + String bmfo = "Bundle-SymbolicName: cap.bundleo\n" + + "Bundle-Version: 1.2.3.Blah\n" + + "Bundle-ManifestVersion: 2\n" + + "Import-Package: org.osgi.framework\n"; + File bundleFileO = createBundle(bmfo); + + String fmf = "Bundle-SymbolicName: cap.frag\n" + + "Bundle-Version: 1.0.0\n" + + "Fragment-Host: cap.bundle\n" + + "Bundle-ManifestVersion: 2\n" + + "Export-Package: org.foo.bar;version=\"2.0.0\"\n" + + "Import-Package: org.osgi.util.tracker\n"; + File fragFile = createBundle(fmf); + + String fmfo = "Bundle-SymbolicName: cap.frago\n" + + "Bundle-Version: 1.0.0\n" + + "Fragment-Host: cap.bundleo\n" + + "Bundle-ManifestVersion: 2\n" + + "Export-Package: org.foo.bar;version=\"2.0.0\"\n" + + "Import-Package: org.osgi.util.tracker\n"; + File fragFileO = createBundle(fmfo); + + Bundle h = felix.getBundleContext().installBundle(bundleFile.toURI().toASCIIString()); + Bundle f = felix.getBundleContext().installBundle(fragFile.toURI().toASCIIString()); + + Bundle ho = felix.getBundleContext().installBundle(bundleFileO.toURI().toASCIIString()); + Bundle fo = felix.getBundleContext().installBundle(fragFileO.toURI().toASCIIString()); + + assertEquals(Bundle.INSTALLED, h.getState()); + assertEquals(Bundle.INSTALLED, f.getState()); + assertEquals(Bundle.INSTALLED, ho.getState()); + assertEquals(Bundle.INSTALLED, fo.getState()); + + felix.adapt(FrameworkWiring.class).resolveBundles(Collections.singletonList(h)); + + assertEquals(Bundle.RESOLVED, h.getState()); + assertEquals(Bundle.RESOLVED, f.getState()); + assertEquals(Bundle.INSTALLED, ho.getState()); + assertEquals(Bundle.INSTALLED, fo.getState()); + } + + public void testResolveDynamicWithOnlyMatchingFragmentWithHost() throws Exception + { + String bmf = "Bundle-SymbolicName: cap.bundle\n" + + "Bundle-Version: 1.2.3.Blah\n" + + "Bundle-ManifestVersion: 2\n" + + "Import-Package: org.osgi.framework\n"; + File bundleFile = createBundle(bmf); + + String bmfo = "Bundle-SymbolicName: cap.bundleo\n" + + "Bundle-Version: 1.2.3.Blah\n" + + "Bundle-ManifestVersion: 2\n" + + "Import-Package: org.osgi.framework\n"; + File bundleFileO = createBundle(bmfo); + + String fmf = "Bundle-SymbolicName: cap.frag\n" + + "Bundle-Version: 1.0.0\n" + + "Fragment-Host: cap.bundle\n" + + "Bundle-ManifestVersion: 2\n" + + "Export-Package: org.foo.bar;version=\"2.0.0\"\n" + + "Import-Package: org.osgi.util.tracker,test.baz\n"; + File fragFile = createBundle(fmf); + + String fmfo = "Bundle-SymbolicName: cap.frago\n" + + "Bundle-Version: 1.0.0\n" + + "Fragment-Host: cap.bundleo\n" + + "Bundle-ManifestVersion: 2\n" + + "Export-Package: org.foo.baz;version=\"2.0.0\"\n" + + "Import-Package: org.osgi.util.tracker\n"; + File fragFileO = createBundle(fmfo); + + String dynm = "Bundle-SymbolicName: cap.dyn\n" + + "Bundle-Version: 1.0.0\n" + + "Bundle-ManifestVersion: 2\n" + + "DynamicImport-Package: org.foo.*,org.osgi.*\n"; + File dynFile = createBundle(dynm); + + String reqm = "Bundle-SymbolicName: cap.req\n" + + "Bundle-Version: 1.0.0\n" + + "Bundle-ManifestVersion: 2\n" + + "Export-Package: test.baz\n"; + File reqFile = createBundle(reqm); + + String reqnm = "Bundle-SymbolicName: cap.reqn\n" + + "Bundle-Version: 1.0.0\n" + + "Bundle-ManifestVersion: 2\n" + + "Export-Package: test.bazz,org.foo.bar.blub\n"; + File reqnFile = createBundle(reqnm); + + Bundle h = felix.getBundleContext().installBundle(bundleFile.toURI().toASCIIString()); + Bundle f = felix.getBundleContext().installBundle(fragFile.toURI().toASCIIString()); + + Bundle ho = felix.getBundleContext().installBundle(bundleFileO.toURI().toASCIIString()); + Bundle fo = felix.getBundleContext().installBundle(fragFileO.toURI().toASCIIString()); + + Bundle dyn = felix.getBundleContext().installBundle(dynFile.toURI().toASCIIString()); + Bundle req = felix.getBundleContext().installBundle(reqFile.toURI().toASCIIString()); + Bundle reqn = felix.getBundleContext().installBundle(reqnFile.toURI().toASCIIString()); + + assertEquals(Bundle.INSTALLED, h.getState()); + assertEquals(Bundle.INSTALLED, f.getState()); + assertEquals(Bundle.INSTALLED, ho.getState()); + assertEquals(Bundle.INSTALLED, fo.getState()); + assertEquals(Bundle.INSTALLED, dyn.getState()); + assertEquals(Bundle.INSTALLED, req.getState()); + assertEquals(Bundle.INSTALLED, reqn.getState()); + + felix.adapt(FrameworkWiring.class).resolveBundles(Collections.singletonList(dyn)); + + assertEquals(Bundle.INSTALLED, h.getState()); + assertEquals(Bundle.INSTALLED, f.getState()); + assertEquals(Bundle.INSTALLED, ho.getState()); + assertEquals(Bundle.INSTALLED, fo.getState()); + assertEquals(Bundle.RESOLVED, dyn.getState()); + assertEquals(Bundle.INSTALLED, req.getState()); + assertEquals(Bundle.INSTALLED, reqn.getState()); + + try + { + dyn.loadClass("org.foo.bar.Bar"); + fail(); + } + catch (Exception ex) + { + // Expected + } + assertEquals(Bundle.RESOLVED, h.getState()); + assertEquals(Bundle.RESOLVED, f.getState()); + assertEquals(Bundle.INSTALLED, ho.getState()); + assertEquals(Bundle.INSTALLED, fo.getState()); + assertEquals(Bundle.RESOLVED, dyn.getState()); + assertEquals(Bundle.RESOLVED, req.getState()); + assertEquals(Bundle.INSTALLED, reqn.getState()); + List requiredWires = dyn.adapt(BundleWiring.class).getRequiredWires(BundleRevision.PACKAGE_NAMESPACE); + assertEquals(1, requiredWires.size()); + assertEquals(requiredWires.get(0).getProvider().getBundle(), h); + + try + { + dyn.loadClass("org.foo.baz.Bar"); + fail(); + } + catch (Exception ex) + { + // Expected + } + assertEquals(Bundle.RESOLVED, h.getState()); + assertEquals(Bundle.RESOLVED, f.getState()); + assertEquals(Bundle.RESOLVED, ho.getState()); + assertEquals(Bundle.RESOLVED, fo.getState()); + assertEquals(Bundle.RESOLVED, dyn.getState()); + assertEquals(Bundle.RESOLVED, req.getState()); + assertEquals(Bundle.INSTALLED, reqn.getState()); + requiredWires = dyn.adapt(BundleWiring.class).getRequiredWires(BundleRevision.PACKAGE_NAMESPACE); + assertEquals(2, requiredWires.size()); + assertEquals(requiredWires.get(0).getProvider().getBundle(), h); + assertEquals(requiredWires.get(1).getProvider().getBundle(), ho); + } + + private File createBundle(String manifest) throws IOException + { + File f = File.createTempFile("felix-bundle", ".jar", tempDir); + + Manifest mf = new Manifest(new ByteArrayInputStream(manifest.getBytes("utf-8"))); + mf.getMainAttributes().putValue("Manifest-Version", "1.0"); + JarOutputStream os = new JarOutputStream(new FileOutputStream(f), mf); + os.close(); + return f; + } + + private static void deleteDir(File root) throws IOException + { + if (root.isDirectory()) + { + for (File file : root.listFiles()) + { + deleteDir(file); + } + } + assertTrue(root.delete()); + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/ServiceRegistrationImplTest.java b/framework/src/test/java/org/apache/felix/framework/ServiceRegistrationImplTest.java new file mode 100644 index 00000000000..4712f3bfca8 --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/ServiceRegistrationImplTest.java @@ -0,0 +1,97 @@ +/* + * 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.framework; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.mockito.Mockito; +import org.osgi.framework.Bundle; + +public class ServiceRegistrationImplTest extends TestCase +{ + public void testMarkCurrentThread() throws Exception + { + final ServiceRegistrationImpl sri = new ServiceRegistrationImpl( + new ServiceRegistry(null, null), Mockito.mock(Bundle.class), + new String [] {String.class.getName()}, 1L, "foo", null); + + assertFalse(sri.currentThreadMarked()); + sri.markCurrentThread(); + assertTrue(sri.currentThreadMarked()); + + final List exceptions = new ArrayList(); + Thread t = new TestThread(exceptions, new Runnable() { + @Override + public void run() + { + assertFalse(sri.currentThreadMarked()); + sri.markCurrentThread(); + assertTrue(sri.currentThreadMarked()); + } + }); + t.start(); + t.join(); + assertEquals("There should be no exceptions: " + exceptions, 0, exceptions.size()); + + sri.unmarkCurrentThread(); + assertFalse(sri.currentThreadMarked()); + + Thread t2 = new TestThread(exceptions, new Runnable() { + @Override + public void run() + { + assertFalse(sri.currentThreadMarked()); + sri.markCurrentThread(); + assertTrue(sri.currentThreadMarked()); + sri.unmarkCurrentThread(); + assertFalse(sri.currentThreadMarked()); + } + }); + t2.start(); + t2.join(); + assertEquals("There should be no exceptions: " + exceptions, 0, exceptions.size()); + } + + static class TestThread extends Thread { + private final Runnable runnable; + private final List exceptions; + + public TestThread(List exceptionList, Runnable runnable) + { + this.runnable = runnable; + this.exceptions = exceptionList; + } + + @Override + public void run() + { + try + { + runnable.run(); + } + catch (Throwable th) + { + exceptions.add(th); + } + } + } +} \ No newline at end of file diff --git a/framework/src/test/java/org/apache/felix/framework/ServiceRegistryTest.java b/framework/src/test/java/org/apache/felix/framework/ServiceRegistryTest.java new file mode 100644 index 00000000000..b83eef4f646 --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/ServiceRegistryTest.java @@ -0,0 +1,1273 @@ +/* + * 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.framework; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Hashtable; +import java.util.List; +import java.util.Observable; +import java.util.Observer; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import junit.framework.TestCase; + +import org.apache.felix.framework.ServiceRegistrationImpl.ServiceReferenceImpl; +import org.apache.felix.framework.ServiceRegistry.ServiceHolder; +import org.apache.felix.framework.ServiceRegistry.UsageCount; +import org.easymock.MockControl; +import org.mockito.AdditionalAnswers; +import org.mockito.InOrder; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceException; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.hooks.service.EventHook; +import org.osgi.framework.hooks.service.FindHook; +import org.osgi.framework.hooks.service.ListenerHook; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +public class ServiceRegistryTest extends TestCase +{ + public void testRegisterEventHookService() + { + MockControl control = MockControl.createNiceControl(Bundle.class); + Bundle b = (Bundle) control.getMock(); + control.replay(); + + MockControl controlContext = MockControl.createNiceControl(BundleContext.class); + BundleContext c = (BundleContext) controlContext.getMock(); + controlContext.expectAndReturn(c.getBundle(), b); + controlContext.replay(); + + ServiceRegistry sr = new ServiceRegistry(new Logger(), null); + EventHook hook = new EventHook() + { + @Override + public void event(ServiceEvent event, Collection contexts) + { + } + }; + + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + ServiceRegistration reg = sr.registerService(c.getBundle(), new String [] {EventHook.class.getName()}, hook, new Hashtable()); + assertEquals(1, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertTrue(sr.getHookRegistry().getHooks(EventHook.class).iterator().next() instanceof ServiceReference); + assertSame(reg.getReference(), sr.getHookRegistry().getHooks(EventHook.class).iterator().next()); + assertSame(hook, ((ServiceRegistrationImpl) reg).getService()); + assertEquals("Postcondition failed", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertEquals("Postcondition failed", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + + sr.unregisterService(b, reg); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + } + + public void testRegisterEventHookServiceFactory() + { + MockControl control = MockControl.createNiceControl(Bundle.class); + Bundle b = (Bundle) control.getMock(); + control.replay(); + + MockControl controlContext = MockControl.createNiceControl(BundleContext.class); + BundleContext c = (BundleContext) controlContext.getMock(); + controlContext.expectAndReturn(c.getBundle(), b); + controlContext.replay(); + + ServiceRegistry sr = new ServiceRegistry(new Logger(), null); + MockControl sfControl = MockControl.createNiceControl(ServiceFactory.class); + sfControl.replay(); + ServiceFactory sf = (ServiceFactory) sfControl.getMock(); + + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + ServiceRegistration reg = sr.registerService(c.getBundle(), new String [] {EventHook.class.getName()}, sf, new Hashtable()); + assertEquals(1, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertSame(reg.getReference(), sr.getHookRegistry().getHooks(EventHook.class).iterator().next()); + assertSame(sf, ((ServiceRegistrationImpl) reg).getService()); + assertEquals("Postcondition failed", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertEquals("Postcondition failed", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + + sr.unregisterService(b, reg); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + } + + public void testRegisterFindHookService() + { + MockControl control = MockControl.createNiceControl(Bundle.class); + Bundle b = (Bundle) control.getMock(); + control.replay(); + + MockControl controlContext = MockControl.createNiceControl(BundleContext.class); + BundleContext c = (BundleContext) controlContext.getMock(); + controlContext.expectAndReturn(c.getBundle(), b); + controlContext.replay(); + + ServiceRegistry sr = new ServiceRegistry(new Logger(), null); + FindHook hook = new FindHook() + { + @Override + public void find(BundleContext context, String name, String filter, + boolean allServices, Collection references) + { + } + }; + + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + ServiceRegistration reg = sr.registerService(c.getBundle(), new String [] {FindHook.class.getName()}, hook, new Hashtable()); + assertEquals(1, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertSame(reg.getReference(), sr.getHookRegistry().getHooks(FindHook.class).iterator().next()); + assertSame(hook, ((ServiceRegistrationImpl) reg).getService()); + assertEquals("Postcondition failed", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Postcondition failed", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + + sr.unregisterService(b, reg); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + } + + public void testRegisterFindHookServiceFactory() + { + MockControl control = MockControl.createNiceControl(Bundle.class); + Bundle b = (Bundle) control.getMock(); + control.replay(); + + MockControl controlContext = MockControl.createNiceControl(BundleContext.class); + BundleContext c = (BundleContext) controlContext.getMock(); + controlContext.expectAndReturn(c.getBundle(), b); + controlContext.replay(); + + ServiceRegistry sr = new ServiceRegistry(new Logger(), null); + MockControl sfControl = MockControl.createNiceControl(ServiceFactory.class); + sfControl.replay(); + ServiceFactory sf = (ServiceFactory) sfControl.getMock(); + + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + ServiceRegistration reg = sr.registerService(c.getBundle(), new String [] {FindHook.class.getName()}, sf, new Hashtable()); + assertEquals(1, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertSame(reg.getReference(), sr.getHookRegistry().getHooks(FindHook.class).iterator().next()); + assertSame(sf, ((ServiceRegistrationImpl) reg).getService()); + assertEquals("Postcondition failed", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Postcondition failed", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + + sr.unregisterService(b, reg); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + } + + public void testRegisterListenerHookService() + { + MockControl control = MockControl.createNiceControl(Bundle.class); + Bundle b = (Bundle) control.getMock(); + control.replay(); + + MockControl controlContext = MockControl.createNiceControl(BundleContext.class); + BundleContext c = (BundleContext) controlContext.getMock(); + controlContext.expectAndReturn(c.getBundle(), b); + controlContext.replay(); + + ServiceRegistry sr = new ServiceRegistry(new Logger(), null); + ListenerHook hook = new ListenerHook() + { + @Override + public void added(Collection listeners) + { + } + + @Override + public void removed(Collection listener) + { + } + }; + + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + ServiceRegistration reg = sr.registerService(c.getBundle(), new String [] {ListenerHook.class.getName()}, hook, new Hashtable()); + assertEquals(1, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + assertSame(reg.getReference(), sr.getHookRegistry().getHooks(ListenerHook.class).iterator().next()); + assertSame(hook, ((ServiceRegistrationImpl) reg).getService()); + assertEquals("Postcondition failed", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Postcondition failed", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + + sr.unregisterService(b, reg); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + } + + public void testRegisterListenerHookServiceFactory() + { + MockControl control = MockControl.createNiceControl(Bundle.class); + Bundle b = (Bundle) control.getMock(); + control.replay(); + + MockControl controlContext = MockControl.createNiceControl(BundleContext.class); + BundleContext c = (BundleContext) controlContext.getMock(); + controlContext.expectAndReturn(c.getBundle(), b); + controlContext.replay(); + + ServiceRegistry sr = new ServiceRegistry(new Logger(), null); + MockControl sfControl = MockControl.createNiceControl(ServiceFactory.class); + sfControl.replay(); + ServiceFactory sf = (ServiceFactory) sfControl.getMock(); + + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + ServiceRegistration reg = sr.registerService(c.getBundle(), new String [] {ListenerHook.class.getName()}, sf, new Hashtable()); + assertEquals(1, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + assertSame(reg.getReference(), sr.getHookRegistry().getHooks(ListenerHook.class).iterator().next()); + assertSame(sf, ((ServiceRegistrationImpl) reg).getService()); + assertEquals("Postcondition failed", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Postcondition failed", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + + sr.unregisterService(b, reg); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + } + + public void testRegisterCombinedService() + { + MockControl control = MockControl.createNiceControl(Bundle.class); + Bundle b = (Bundle) control.getMock(); + control.replay(); + + MockControl controlContext = MockControl.createNiceControl(BundleContext.class); + BundleContext c = (BundleContext) controlContext.getMock(); + controlContext.expectAndReturn(c.getBundle(), b); + controlContext.replay(); + + ServiceRegistry sr = new ServiceRegistry(new Logger(), null); + class CombinedService implements ListenerHook, FindHook, EventHook, Runnable + { + @Override + public void added(Collection listeners) + { + } + + @Override + public void removed(Collection listener) + { + } + + @Override + public void find(BundleContext context, String name, String filter, + boolean allServices, Collection references) + { + } + + @Override + public void event(ServiceEvent event, Collection contexts) + { + } + + @Override + public void run() + { + } + + } + CombinedService hook = new CombinedService(); + + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + ServiceRegistration reg = sr.registerService(c.getBundle(), new String [] { + Runnable.class.getName(), + ListenerHook.class.getName(), + FindHook.class.getName(), + EventHook.class.getName()}, hook, new Hashtable()); + assertEquals(1, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + assertSame(reg.getReference(), sr.getHookRegistry().getHooks(ListenerHook.class).iterator().next()); + assertSame(hook, ((ServiceRegistrationImpl) reg).getService()); + assertEquals(1, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertSame(reg.getReference(), sr.getHookRegistry().getHooks(EventHook.class).iterator().next()); + assertSame(hook, ((ServiceRegistrationImpl) reg).getService()); + assertEquals(1, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertSame(reg.getReference(), sr.getHookRegistry().getHooks(FindHook.class).iterator().next()); + assertSame(hook, ((ServiceRegistrationImpl) reg).getService()); + + sr.unregisterService(b, reg); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertEquals("Should be no hooks left after unregistration", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + } + + public void testRegisterPlainService() + { + MockControl control = MockControl.createNiceControl(Bundle.class); + Bundle b = (Bundle) control.getMock(); + control.replay(); + + MockControl controlContext = MockControl.createNiceControl(BundleContext.class); + BundleContext c = (BundleContext) controlContext.getMock(); + controlContext.expectAndReturn(c.getBundle(), b); + controlContext.replay(); + + ServiceRegistry sr = new ServiceRegistry(new Logger(), null); + String svcObj = "hello"; + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertEquals("Precondition failed", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + ServiceRegistration reg = sr.registerService(c.getBundle(), new String [] {String.class.getName()}, svcObj, new Hashtable()); + assertEquals("Postcondition failed", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Postcondition failed", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertEquals("Postcondition failed", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + + sr.unregisterService(b, reg); + assertEquals("Unregistration should have no effect", 0, sr.getHookRegistry().getHooks(EventHook.class).size()); + assertEquals("Unregistration should have no effect", 0, sr.getHookRegistry().getHooks(FindHook.class).size()); + assertEquals("Unregistration should have no effect", 0, sr.getHookRegistry().getHooks(ListenerHook.class).size()); + } + + @SuppressWarnings("unchecked") + public void testGetService() + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + String svc = "foo"; + + Bundle b = Mockito.mock(Bundle.class); + ServiceRegistrationImpl reg = Mockito.mock(ServiceRegistrationImpl.class); + Mockito.when(reg.isValid()).thenReturn(true); + Mockito.when(reg.getService(b)).thenReturn(svc); + + ServiceReferenceImpl ref = Mockito.mock(ServiceReferenceImpl.class); + Mockito.when(ref.getRegistration()).thenReturn(reg); + + assertSame(svc, sr.getService(b, ref, false)); + } + + @SuppressWarnings("unchecked") + public void testGetServiceHolderAwait() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + final String svc = "test"; + + Bundle b = Mockito.mock(Bundle.class); + ServiceRegistrationImpl reg = Mockito.mock(ServiceRegistrationImpl.class); + Mockito.when(reg.isValid()).thenReturn(true); + + ServiceReferenceImpl ref = Mockito.mock(ServiceReferenceImpl.class); + Mockito.when(ref.getRegistration()).thenReturn(reg); + + UsageCount uc = sr.obtainUsageCount(b, ref, null, false); + + // Set an empty Service Holder so we can test that it waits. + final ServiceHolder sh = new ServiceHolder(); + uc.m_svcHolderRef.set(sh); + + final StringBuffer sb = new StringBuffer(); + final AtomicBoolean threadException = new AtomicBoolean(false); + Thread t = new Thread() { + @Override + public void run() + { + try { Thread.sleep(250); } catch (InterruptedException e) {} + sh.m_service = svc; + if (sb.length() > 0) + { + // Should not have put anything in SB until countDown() was called... + threadException.set(true); + } + sh.m_latch.countDown(); + } + }; + assertFalse(t.isInterrupted()); + t.start(); + + Object actualSvc = sr.getService(b, ref, false); + sb.append(actualSvc); + + t.join(); + assertFalse("This thread did not wait until the latch was count down", + threadException.get()); + + assertSame(svc, actualSvc); + } + + @SuppressWarnings("unchecked") + public void testGetServicePrototype() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + String svc = "xyz"; + + Bundle b = Mockito.mock(Bundle.class); + ServiceRegistrationImpl reg = Mockito.mock(ServiceRegistrationImpl.class); + Mockito.when(reg.isValid()).thenReturn(true); + Mockito.when(reg.getService(b)).thenReturn(svc); + + ServiceReferenceImpl ref = Mockito.mock(ServiceReferenceImpl.class); + Mockito.when(ref.getRegistration()).thenReturn(reg); + + assertSame(svc, sr.getService(b, ref, true)); + + final ConcurrentMap inUseMap = + (ConcurrentMap) getPrivateField(sr, "m_inUseMap"); + UsageCount[] uca = inUseMap.get(b); + assertEquals(1, uca.length); + assertEquals(1, uca[0].m_serviceObjectsCount.get()); + + sr.getService(b, ref, true); + assertEquals(2, uca[0].m_serviceObjectsCount.get()); + } + + @SuppressWarnings("unchecked") + public void testGetServiceThreadMarking() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + Bundle b = Mockito.mock(Bundle.class); + ServiceRegistrationImpl reg = Mockito.mock(ServiceRegistrationImpl.class); + + ServiceReferenceImpl ref = Mockito.mock(ServiceReferenceImpl.class); + Mockito.when(ref.getRegistration()).thenReturn(reg); + + sr.getService(b, ref, false); + + InOrder inOrder = Mockito.inOrder(reg); + inOrder.verify(reg, Mockito.times(1)).currentThreadMarked(); + inOrder.verify(reg, Mockito.times(1)).markCurrentThread(); + inOrder.verify(reg, Mockito.times(1)).unmarkCurrentThread(); + } + + @SuppressWarnings("unchecked") + public void testGetServiceThreadMarking2() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + String svc = "bar"; + + Bundle b = Mockito.mock(Bundle.class); + + ServiceRegistrationImpl reg = (ServiceRegistrationImpl) sr.registerService( + b, new String [] {String.class.getName()}, svc, null); + + ServiceReferenceImpl ref = Mockito.mock(ServiceReferenceImpl.class); + Mockito.when(ref.getRegistration()).thenReturn(reg); + + reg.markCurrentThread(); + try + { + sr.getService(b, ref, false); + fail("Should have thrown an exception to signal reentrant behaviour"); + } + catch (ServiceException se) + { + assertEquals(ServiceException.FACTORY_ERROR, se.getType()); + } + } + + @SuppressWarnings("unchecked") + public void testUngetService() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + Bundle b = Mockito.mock(Bundle.class); + ServiceRegistrationImpl reg = Mockito.mock(ServiceRegistrationImpl.class); + + ServiceReferenceImpl ref = Mockito.mock(ServiceReferenceImpl.class); + Mockito.when(ref.getRegistration()).thenReturn(reg); + + final ConcurrentMap inUseMap = + (ConcurrentMap) getPrivateField(sr, "m_inUseMap"); + + UsageCount uc = new UsageCount(ref, false); + uc.m_svcHolderRef.set(new ServiceHolder()); + + inUseMap.put(b, new UsageCount[] {uc}); + + assertFalse(sr.ungetService(b, ref, null)); + assertNull(uc.m_svcHolderRef.get()); + } + + @SuppressWarnings("unchecked") + public void testUngetService2() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + Bundle b = Mockito.mock(Bundle.class); + ServiceRegistrationImpl reg = Mockito.mock(ServiceRegistrationImpl.class); + + ServiceReferenceImpl ref = Mockito.mock(ServiceReferenceImpl.class); + Mockito.when(ref.getRegistration()).thenReturn(reg); + + final ConcurrentMap inUseMap = + (ConcurrentMap) getPrivateField(sr, "m_inUseMap"); + + UsageCount uc = new UsageCount(ref, false); + ServiceHolder sh = new ServiceHolder(); + Object svc = new Object(); + sh.m_service = svc; + uc.m_svcHolderRef.set(sh); + uc.m_count.incrementAndGet(); + + Mockito.verify(reg, Mockito.never()). + ungetService(Mockito.isA(Bundle.class), Mockito.any()); + inUseMap.put(b, new UsageCount[] {uc}); + + assertTrue(sr.ungetService(b, ref, null)); + assertNull(uc.m_svcHolderRef.get()); + + Mockito.verify(reg, Mockito.times(1)). + ungetService(Mockito.isA(Bundle.class), Mockito.eq(svc)); + } + + @SuppressWarnings("unchecked") + public void testUngetService3() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + Bundle b = Mockito.mock(Bundle.class); + ServiceRegistrationImpl reg = Mockito.mock(ServiceRegistrationImpl.class); + Mockito.when(reg.isValid()).thenReturn(true); + + ServiceReferenceImpl ref = Mockito.mock(ServiceReferenceImpl.class); + Mockito.when(ref.getRegistration()).thenReturn(reg); + + final ConcurrentMap inUseMap = + (ConcurrentMap) getPrivateField(sr, "m_inUseMap"); + + UsageCount uc = new UsageCount(ref, false); + uc.m_svcHolderRef.set(new ServiceHolder()); + uc.m_count.set(2); + + inUseMap.put(b, new UsageCount[] {uc}); + + assertTrue(sr.ungetService(b, ref, null)); + assertNotNull(uc.m_svcHolderRef.get()); + assertNotNull(inUseMap.get(b)); + + Mockito.verify(reg, Mockito.never()). + ungetService(Mockito.isA(Bundle.class), Mockito.any()); + } + + @SuppressWarnings("unchecked") + public void testUngetService4() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + Bundle b = Mockito.mock(Bundle.class); + ServiceRegistrationImpl reg = Mockito.mock(ServiceRegistrationImpl.class); + Mockito.when(reg.isValid()).thenReturn(false); + + ServiceReferenceImpl ref = Mockito.mock(ServiceReferenceImpl.class); + Mockito.when(ref.getRegistration()).thenReturn(reg); + + final ConcurrentMap inUseMap = + (ConcurrentMap) getPrivateField(sr, "m_inUseMap"); + + UsageCount uc = new UsageCount(ref, false); + uc.m_svcHolderRef.set(new ServiceHolder()); + uc.m_count.set(2); + + inUseMap.put(b, new UsageCount[] {uc}); + + assertTrue(sr.ungetService(b, ref, null)); + assertNull(uc.m_svcHolderRef.get()); + + Mockito.verify(reg, Mockito.never()). + ungetService(Mockito.isA(Bundle.class), Mockito.any()); + } + + @SuppressWarnings("unchecked") + public void testUngetService5() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + Bundle b = Mockito.mock(Bundle.class); + ServiceRegistrationImpl reg = Mockito.mock(ServiceRegistrationImpl.class); + Mockito.doThrow(new RuntimeException("Test!")).when(reg). + ungetService(Mockito.isA(Bundle.class), Mockito.any()); + + ServiceReferenceImpl ref = Mockito.mock(ServiceReferenceImpl.class); + Mockito.when(ref.getRegistration()).thenReturn(reg); + + final ConcurrentMap inUseMap = + (ConcurrentMap) getPrivateField(sr, "m_inUseMap"); + + String svc = "myService"; + UsageCount uc = new UsageCount(ref, false); + ServiceHolder sh = new ServiceHolder(); + sh.m_service = svc; + sh.m_latch.countDown(); + uc.m_svcHolderRef.set(sh); + uc.m_count.set(1); + + inUseMap.put(b, new UsageCount[] {uc}); + + try + { + assertTrue(sr.ungetService(b, ref, null)); + fail("Should have propagated the runtime exception"); + } + catch (RuntimeException re) + { + assertEquals("Test!", re.getMessage()); + } + assertNull(uc.m_svcHolderRef.get()); + + Mockito.verify(reg, Mockito.times(1)).ungetService(b, svc); + } + + public void testUngetServiceThreadMarking() + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + Bundle b = Mockito.mock(Bundle.class); + ServiceRegistrationImpl reg = Mockito.mock(ServiceRegistrationImpl.class); + + ServiceReferenceImpl ref = Mockito.mock(ServiceReferenceImpl.class); + Mockito.when(ref.getRegistration()).thenReturn(reg); + + assertFalse("There is no usage count, so this method should return false", + sr.ungetService(b, ref, null)); + + InOrder inOrder = Mockito.inOrder(reg); + inOrder.verify(reg, Mockito.times(1)).currentThreadMarked(); + inOrder.verify(reg, Mockito.times(1)).markCurrentThread(); + inOrder.verify(reg, Mockito.times(1)).unmarkCurrentThread(); + } + + public void testUngetServiceThreadMarking2() + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + Bundle b = Mockito.mock(Bundle.class); + ServiceRegistrationImpl reg = Mockito.mock(ServiceRegistrationImpl.class); + Mockito.when(reg.currentThreadMarked()).thenReturn(true); + + ServiceReferenceImpl ref = Mockito.mock(ServiceReferenceImpl.class); + Mockito.when(ref.getRegistration()).thenReturn(reg); + + try + { + sr.ungetService(b, ref, null); + fail("The thread should be observed as marked and hence throw an exception"); + } + catch (IllegalStateException ise) + { + // good + } + } + + public void testObtainUsageCount() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + @SuppressWarnings("unchecked") + ConcurrentMap inUseMap = (ConcurrentMap) getPrivateField(sr, "m_inUseMap"); + + assertEquals("Precondition", 0, inUseMap.size()); + + Bundle b = Mockito.mock(Bundle.class); + ServiceReference ref = Mockito.mock(ServiceReference.class); + UsageCount uc = sr.obtainUsageCount(b, ref, null, false); + assertEquals(1, inUseMap.size()); + assertEquals(1, inUseMap.get(b).length); + assertSame(uc, inUseMap.get(b)[0]); + assertSame(ref, uc.m_ref); + assertFalse(uc.m_prototype); + + UsageCount uc2 = sr.obtainUsageCount(b, ref, null, false); + assertSame(uc, uc2); + + ServiceReference ref2 = Mockito.mock(ServiceReference.class); + UsageCount uc3 = sr.obtainUsageCount(b, ref2, null, false); + assertNotSame(uc3, uc2); + assertSame(ref2, uc3.m_ref); + } + + public void testObtainUsageCountPrototype() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + @SuppressWarnings("unchecked") + ConcurrentMap inUseMap = (ConcurrentMap) getPrivateField(sr, "m_inUseMap"); + + Bundle b = Mockito.mock(Bundle.class); + ServiceReference ref = Mockito.mock(ServiceReference.class); + UsageCount uc = sr.obtainUsageCount(b, ref, null, true); + assertEquals(1, inUseMap.size()); + assertEquals(1, inUseMap.values().iterator().next().length); + + ServiceReference ref2 = Mockito.mock(ServiceReference.class); + UsageCount uc2 = sr.obtainUsageCount(b, ref2, null, true); + assertEquals(1, inUseMap.size()); + assertEquals(2, inUseMap.values().iterator().next().length); + List ucl = Arrays.asList(inUseMap.get(b)); + assertTrue(ucl.contains(uc)); + assertTrue(ucl.contains(uc2)); + } + + public void testObtainUsageCountPrototypeUnknownLookup() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + @SuppressWarnings("unchecked") + ConcurrentMap inUseMap = (ConcurrentMap) getPrivateField(sr, "m_inUseMap"); + + Bundle b = Mockito.mock(Bundle.class); + ServiceReference ref = Mockito.mock(ServiceReference.class); + + UsageCount uc = new UsageCount(ref, true); + ServiceHolder sh = new ServiceHolder(); + String svc = "foobar"; + sh.m_service = svc; + uc.m_svcHolderRef.set(sh); + inUseMap.put(b, new UsageCount[] {uc}); + + assertNull(sr.obtainUsageCount(b, Mockito.mock(ServiceReference.class), null, null)); + + UsageCount uc2 = sr.obtainUsageCount(b, ref, svc, null); + assertSame(uc, uc2); + } + + public void testObtainUsageCountPrototypeUnknownLookup2() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + @SuppressWarnings("unchecked") + ConcurrentMap inUseMap = (ConcurrentMap) getPrivateField(sr, "m_inUseMap"); + + Bundle b = Mockito.mock(Bundle.class); + ServiceReference ref = Mockito.mock(ServiceReference.class); + + UsageCount uc = new UsageCount(ref, false); + inUseMap.put(b, new UsageCount[] {uc}); + + assertNull(sr.obtainUsageCount(b, Mockito.mock(ServiceReference.class), null, null)); + + UsageCount uc2 = sr.obtainUsageCount(b, ref, null, null); + assertSame(uc, uc2); + } + + @SuppressWarnings("unchecked") + public void testObtainUsageCountRetry1() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + final Bundle b = Mockito.mock(Bundle.class); + + final ConcurrentMap orgInUseMap = + (ConcurrentMap) getPrivateField(sr, "m_inUseMap"); + + ConcurrentMap inUseMap = + Mockito.mock(ConcurrentMap.class, AdditionalAnswers.delegatesTo(orgInUseMap)); + Mockito.doAnswer(new Answer() + { + @Override + public UsageCount[] answer(InvocationOnMock invocation) throws Throwable + { + // This mimicks another thread putting another UsageCount in concurrently + // The putIfAbsent() will fail and it has to retry + UsageCount uc = new UsageCount(Mockito.mock(ServiceReference.class), false); + UsageCount[] uca = new UsageCount[] {uc}; + orgInUseMap.put(b, uca); + return uca; + } + }).when(inUseMap).putIfAbsent(Mockito.any(Bundle.class), Mockito.any(UsageCount[].class)); + setPrivateField(sr, "m_inUseMap", inUseMap); + + ServiceReference ref = Mockito.mock(ServiceReference.class); + + assertEquals(0, orgInUseMap.size()); + UsageCount uc = sr.obtainUsageCount(b, ref, null, false); + assertEquals(1, orgInUseMap.size()); + assertEquals(2, orgInUseMap.get(b).length); + assertSame(ref, uc.m_ref); + assertFalse(uc.m_prototype); + List l = new ArrayList(Arrays.asList(orgInUseMap.get(b))); + l.remove(uc); + assertEquals("There should be one UsageCount left", 1, l.size()); + assertNotSame(ref, l.get(0).m_ref); + } + + @SuppressWarnings("unchecked") + public void testObtainUsageCountRetry2() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + final Bundle b = Mockito.mock(Bundle.class); + + final ConcurrentMap orgInUseMap = + (ConcurrentMap) getPrivateField(sr, "m_inUseMap"); + orgInUseMap.put(b, new UsageCount[] {new UsageCount(Mockito.mock(ServiceReference.class), false)}); + + ConcurrentMap inUseMap = + Mockito.mock(ConcurrentMap.class, AdditionalAnswers.delegatesTo(orgInUseMap)); + Mockito.doAnswer(new Answer() + { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable + { + orgInUseMap.remove(b); + return false; + } + }).when(inUseMap).replace(Mockito.any(Bundle.class), + Mockito.any(UsageCount[].class), Mockito.any(UsageCount[].class)); + setPrivateField(sr, "m_inUseMap", inUseMap); + + ServiceReference ref = Mockito.mock(ServiceReference.class); + + assertEquals("Precondition", 1, inUseMap.size()); + assertEquals("Precondition", 1, inUseMap.values().iterator().next().length); + assertNotSame("Precondition", ref, inUseMap.get(b)[0].m_ref); + sr.obtainUsageCount(b, ref, null, false); + assertEquals(1, inUseMap.size()); + assertEquals(1, inUseMap.values().iterator().next().length); + assertSame("The old usage count should have been removed by the mock and this one should have been added", + ref, inUseMap.get(b)[0].m_ref); + } + + public void testFlushUsageCount() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + @SuppressWarnings("unchecked") + ConcurrentMap inUseMap = (ConcurrentMap) getPrivateField(sr, "m_inUseMap"); + + Bundle b = Mockito.mock(Bundle.class); + + ServiceReference ref = Mockito.mock(ServiceReference.class); + UsageCount uc = new UsageCount(ref, false); + ServiceReference ref2 = Mockito.mock(ServiceReference.class); + UsageCount uc2 = new UsageCount(ref2, true); + + inUseMap.put(b, new UsageCount[] {uc, uc2}); + + assertEquals("Precondition", 1, inUseMap.size()); + assertEquals("Precondition", 2, inUseMap.values().iterator().next().length); + + sr.flushUsageCount(b, ref, uc); + assertEquals(1, inUseMap.size()); + assertEquals(1, inUseMap.values().iterator().next().length); + assertSame(uc2, inUseMap.values().iterator().next()[0]); + + sr.flushUsageCount(b, ref2, uc2); + assertEquals(0, inUseMap.size()); + } + + public void testFlushUsageCountNullRef() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + @SuppressWarnings("unchecked") + ConcurrentMap inUseMap = (ConcurrentMap) getPrivateField(sr, "m_inUseMap"); + + Bundle b = Mockito.mock(Bundle.class); + Bundle b2 = Mockito.mock(Bundle.class); + + ServiceReference ref = Mockito.mock(ServiceReference.class); + UsageCount uc = new UsageCount(ref, false); + ServiceReference ref2 = Mockito.mock(ServiceReference.class); + UsageCount uc2 = new UsageCount(ref2, true); + ServiceReference ref3 = Mockito.mock(ServiceReference.class); + UsageCount uc3 = new UsageCount(ref3, true); + + inUseMap.put(b, new UsageCount[] {uc2, uc}); + inUseMap.put(b2, new UsageCount[] {uc3}); + + assertEquals("Precondition", 2, inUseMap.size()); + + sr.flushUsageCount(b, null, uc); + assertEquals(2, inUseMap.size()); + + sr.flushUsageCount(b, null, uc2); + assertEquals(1, inUseMap.size()); + } + + public void testFlushUsageCountAlienObject() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + @SuppressWarnings("unchecked") + ConcurrentMap inUseMap = (ConcurrentMap) getPrivateField(sr, "m_inUseMap"); + + Bundle b = Mockito.mock(Bundle.class); + + ServiceReference ref = Mockito.mock(ServiceReference.class); + UsageCount uc = new UsageCount(ref, false); + + inUseMap.put(b, new UsageCount[] {uc}); + assertEquals("Precondition", 1, inUseMap.size()); + assertEquals("Precondition", 1, inUseMap.values().iterator().next().length); + + UsageCount uc2 = new UsageCount(Mockito.mock(ServiceReference.class), false); + sr.flushUsageCount(b, ref, uc2); + assertEquals("Should be no changes", 1, inUseMap.size()); + assertEquals("Should be no changes", 1, inUseMap.values().iterator().next().length); + } + + public void testFlushUsageCountNull() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + @SuppressWarnings("unchecked") + ConcurrentMap inUseMap = (ConcurrentMap) getPrivateField(sr, "m_inUseMap"); + + Bundle b = Mockito.mock(Bundle.class); + Bundle b2 = Mockito.mock(Bundle.class); + + ServiceReference ref = Mockito.mock(ServiceReference.class); + UsageCount uc = new UsageCount(ref, false); + ServiceReference ref2 = Mockito.mock(ServiceReference.class); + UsageCount uc2 = new UsageCount(ref2, true); + ServiceReference ref3 = Mockito.mock(ServiceReference.class); + UsageCount uc3 = new UsageCount(ref3, true); + + inUseMap.put(b, new UsageCount[] {uc2, uc}); + inUseMap.put(b2, new UsageCount[] {uc3}); + + assertEquals("Precondition", 2, inUseMap.size()); + + sr.flushUsageCount(b, ref, null); + assertEquals(2, inUseMap.size()); + + sr.flushUsageCount(b, ref2, null); + assertEquals(1, inUseMap.size()); + + } + + @SuppressWarnings("unchecked") + public void testFlushUsageCountRetry() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + final Bundle b = Mockito.mock(Bundle.class); + final ServiceReference ref = Mockito.mock(ServiceReference.class); + final UsageCount uc = new UsageCount(ref, false); + final ServiceReference ref2 = Mockito.mock(ServiceReference.class); + final UsageCount uc2 = new UsageCount(ref2, false); + + final ConcurrentMap orgInUseMap = + (ConcurrentMap) getPrivateField(sr, "m_inUseMap"); + + final ConcurrentMap inUseMap = + Mockito.mock(ConcurrentMap.class, AdditionalAnswers.delegatesTo(orgInUseMap)); + Mockito.doAnswer(new Answer() + { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable + { + inUseMap.put(b, new UsageCount[] {uc}); + return false; + } + }).when(inUseMap).replace(Mockito.isA(Bundle.class), + Mockito.isA(UsageCount[].class), Mockito.isA(UsageCount[].class)); + setPrivateField(sr, "m_inUseMap", inUseMap); + + inUseMap.put(b, new UsageCount[] {uc, uc2}); + + sr.flushUsageCount(b, null, uc); + + assertNull("A 'concurrent' process has removed uc2 as well, " + + "so the entry for 'b' should have been removed", + inUseMap.get(b)); + } + + public void testFlushUsageCountRetry2() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + + final Bundle b = Mockito.mock(Bundle.class); + final ServiceReference ref = Mockito.mock(ServiceReference.class); + final UsageCount uc = new UsageCount(ref, false); + final ServiceReference ref2 = Mockito.mock(ServiceReference.class); + final UsageCount uc2 = new UsageCount(ref2, false); + + final ConcurrentMap orgInUseMap = + (ConcurrentMap) getPrivateField(sr, "m_inUseMap"); + + final ConcurrentMap inUseMap = + Mockito.mock(ConcurrentMap.class, AdditionalAnswers.delegatesTo(orgInUseMap)); + Mockito.doAnswer(new Answer() + { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable + { + inUseMap.put(b, new UsageCount[] {uc, uc2}); + return false; + } + }).when(inUseMap).remove(Mockito.isA(Bundle.class), Mockito.isA(UsageCount[].class)); + setPrivateField(sr, "m_inUseMap", inUseMap); + + inUseMap.put(b, new UsageCount[] {uc}); + + sr.flushUsageCount(b, null, uc); + + assertEquals(1, inUseMap.get(b).length); + assertSame(uc2, inUseMap.get(b)[0]); + } + + public void testGetUngetServiceFactory() throws Exception + { + final ServiceRegistry sr = new ServiceRegistry(null, null); + final Bundle regBundle = Mockito.mock(Bundle.class); + final ServiceRegistration reg = sr.registerService(regBundle, new String[] {Observer.class.getName()}, + new ServiceFactory() + { + + final class ObserverImpl implements Observer + { + private final AtomicInteger counter = new AtomicInteger(); + public volatile boolean active = true; + + @Override + public void update(Observable o, Object arg) + { + counter.incrementAndGet(); + if ( !active ) + { + throw new IllegalArgumentException("Iteration:" + counter.get()); + } + } + + }; + + @Override + public Observer getService(Bundle bundle, ServiceRegistration registration) + { + return new ObserverImpl(); + } + + @Override + public void ungetService(Bundle bundle, ServiceRegistration registration, Observer service) + { + ((ObserverImpl)service).active = false; + } + }, null); + + final Bundle clientBundle = Mockito.mock(Bundle.class); + Mockito.when(clientBundle.getBundleId()).thenReturn(42L); + + // check simple get/unget + final Object obj = sr.getService(clientBundle, reg.getReference(), false); + assertNotNull(obj); + assertTrue(obj instanceof Observer); + ((Observer)obj).update(null, null); + sr.ungetService(clientBundle, reg.getReference(), null); + try { + ((Observer)obj).update(null, null); + fail(); + } + catch ( final IllegalArgumentException iae) + { + // expected + } + + // start three threads + final int MAX_THREADS = 3; + final int MAX_LOOPS = 50000; + final CountDownLatch latch = new CountDownLatch(MAX_THREADS); + final Thread[] threads = new Thread[MAX_THREADS]; + final List exceptions = Collections.synchronizedList(new ArrayList()); + for(int i=0; i counterValues = new ArrayList(); + for (Exception ex : exceptions) + { + counterValues.add(ex.getMessage()); + } + + assertTrue("" + counterValues, exceptions.isEmpty()); + } + + public void testUsageCountCleanup() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + Bundle regBundle = Mockito.mock(Bundle.class); + + ServiceRegistration reg = sr.registerService( + regBundle, new String [] {String.class.getName()}, "hi", null); + + final Bundle clientBundle = Mockito.mock(Bundle.class); + Mockito.when(clientBundle.getBundleId()).thenReturn(327L); + + assertEquals("hi", sr.getService(clientBundle, reg.getReference(), false)); + sr.ungetService(clientBundle, reg.getReference(), null); + + ConcurrentMap inUseMap = + (ConcurrentMap) getPrivateField(sr, "m_inUseMap"); + + sr.unregisterService(regBundle, reg); + assertEquals(0, inUseMap.size()); + } + + @SuppressWarnings("unchecked") + public void testGetServiceThrowsException() throws Exception + { + final ServiceRegistry sr = new ServiceRegistry(null, null); + + final Bundle b = Mockito.mock(Bundle.class); + ServiceRegistrationImpl reg = Mockito.mock(ServiceRegistrationImpl.class); + Mockito.when(reg.isValid()).thenReturn(true); + Mockito.when(reg.getService(b)).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable + { + Thread.sleep(500); + throw new Exception("boo!"); + } + }); + + final ServiceReferenceImpl ref = Mockito.mock(ServiceReferenceImpl.class); + Mockito.when(ref.getRegistration()).thenReturn(reg); + + final StringBuffer sb = new StringBuffer(); + Thread t = new Thread() + { + @Override + public void run() + { + try + { + assertEquals("Should not yet have given the service to the other thread", + "", sb.toString()); + sr.getService(b, ref, false); + } + catch (Exception e) + { + // We expect an exception here. + } + } + }; + t.start(); + + // Wait until the other thread has called getService(); + Thread.sleep(250); + + // This thread has waited long enough for the other thread to call getService() + // however the actual getService() call blocks long enough for this one to then + // concurrently call getService() while the other thread is in getService() of the + // factory. This thread will then end up in m_latch.await(). + // The factory implementation of the other thread then throws an exception. This test + // ultimately checks that this thread here is not stuck waiting forwever. + assertNull(sr.getService(b, ref, false)); + sb.append("Obtained service"); + } + + public void testUsingBundlesWithoutZeroCounts() throws Exception + { + ServiceRegistry sr = new ServiceRegistry(null, null); + Bundle regBundle = Mockito.mock(Bundle.class); + + ServiceRegistration reg = sr.registerService( + regBundle, new String [] {String.class.getName()}, "hi", null); + @SuppressWarnings("unchecked") + ServiceReference ref = reg.getReference(); + + final Bundle clientBundle = Mockito.mock(Bundle.class); + Mockito.when(clientBundle.getBundleId()).thenReturn(42L); + assertThat(sr.getService(clientBundle, ref, false), is("hi")); + + final Bundle clientBundle2 = Mockito.mock(Bundle.class); + Mockito.when(clientBundle.getBundleId()).thenReturn(327L); + assertThat(sr.getService(clientBundle2, ref, false), is("hi")); + + assertThat(sr.ungetService(clientBundle, reg.getReference(), null), is(true)); + + assertThat(sr.getUsingBundles(reg.getReference()), is(new Bundle[]{clientBundle2})); + } + + private Object getPrivateField(Object obj, String fieldName) throws NoSuchFieldException, + IllegalAccessException + { + Field f = ServiceRegistry.class.getDeclaredField(fieldName); + f.setAccessible(true); + return f.get(obj); + } + + private void setPrivateField(ServiceRegistry obj, String fieldName, Object val) throws SecurityException, + NoSuchFieldException, IllegalArgumentException, IllegalAccessException + { + Field f = ServiceRegistry.class.getDeclaredField(fieldName); + f.setAccessible(true); + f.set(obj, val); + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/StartBundleTest.java b/framework/src/test/java/org/apache/felix/framework/StartBundleTest.java new file mode 100644 index 00000000000..1e201b7dad8 --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/StartBundleTest.java @@ -0,0 +1,57 @@ +/* + * 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.framework; + +import java.util.HashMap; + +import junit.framework.TestCase; + +import org.apache.felix.framework.util.FelixConstants; +import org.mockito.Mockito; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; +import org.osgi.framework.wiring.BundleRevision; + +public class StartBundleTest extends TestCase +{ + public void testTransientExeption() throws Exception + { + HashMap config = new HashMap(); + config.put(FelixConstants.BUNDLE_STARTLEVEL_PROP, "1"); + final Felix f = new Felix(config); + + BundleImpl b = Mockito.mock(BundleImpl.class); + Mockito.when(b.isLockable()).thenReturn(true); + Mockito.when(b.getState()).thenReturn(Bundle.INSTALLED); + Mockito.when(b.getStartLevel(1)).thenReturn(3); + + BundleRevisionImpl br = new BundleRevisionImpl(b, "test"); + Mockito.when(b.adapt(BundleRevision.class)).thenReturn(br); + + try + { + f.startBundle(b, Bundle.START_TRANSIENT); + fail("Should have thrown a Bundle Exception"); + } + catch (BundleException e) + { + assertEquals(BundleException.START_TRANSIENT_ERROR, e.getType()); + } + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/StartStopBundleTest.java b/framework/src/test/java/org/apache/felix/framework/StartStopBundleTest.java new file mode 100644 index 00000000000..4607b3f7eb5 --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/StartStopBundleTest.java @@ -0,0 +1,172 @@ +/* + * 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.framework; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +import junit.framework.TestCase; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.launch.Framework; + +public class StartStopBundleTest extends TestCase +{ + public static final int DELAY = 1000; + + public void testStartStopBundle() throws Exception + { + Map params = new HashMap(); + params.put(Constants.FRAMEWORK_SYSTEMPACKAGES, + "org.osgi.framework; version=1.4.0," + + "org.osgi.service.packageadmin; version=1.2.0," + + "org.osgi.service.startlevel; version=1.1.0," + + "org.osgi.util.tracker; version=1.3.3," + + "org.osgi.service.url; version=1.0.0"); + File cacheDir = File.createTempFile("felix-cache", ".dir"); + cacheDir.delete(); + cacheDir.mkdirs(); + String cache = cacheDir.getPath(); + params.put("felix.cache.profiledir", cache); + params.put("felix.cache.dir", cache); + params.put(Constants.FRAMEWORK_STORAGE, cache); + + String mf = "Bundle-SymbolicName: boot.test\n" + + "Bundle-Version: 1.1.0\n" + + "Bundle-ManifestVersion: 2\n" + + "Import-Package: org.osgi.framework\n"; + File bundleFile = createBundle(mf, cacheDir); + + Framework f = new Felix(params); + f.init(); + f.start(); + + try { + final Bundle bundle = f.getBundleContext().installBundle(bundleFile.toURI().toString()); + + new Thread() + { + public void run() + { + try + { + bundle.start(); + } + catch (BundleException e) + { + e.printStackTrace(); + } + } + }.start(); + Thread.sleep(DELAY / 4); + long t0 = System.currentTimeMillis(); + bundle.stop(); + long t1 = System.currentTimeMillis(); + + assertEquals(Bundle.RESOLVED, bundle.getState()); + assertTrue((t1 - t0) > DELAY / 2); + + bundle.start(); + + new Thread() + { + public void run() + { + try + { + bundle.stop(); + } + catch (BundleException e) + { + e.printStackTrace(); + } + } + }.start(); + Thread.sleep(DELAY / 4); + t0 = System.currentTimeMillis(); + bundle.start(); + t1 = System.currentTimeMillis(); + + assertEquals(Bundle.ACTIVE, bundle.getState()); + assertTrue((t1 - t0) > DELAY / 2); + } finally { + f.stop(); + Thread.sleep(DELAY); + deleteDir(cacheDir); + } + } + + private static File createBundle(String manifest, File tempDir) throws IOException + { + File f = File.createTempFile("felix-bundle", ".jar", tempDir); + + Manifest mf = new Manifest(new ByteArrayInputStream(manifest.getBytes("utf-8"))); + mf.getMainAttributes().putValue("Manifest-Version", "1.0"); + mf.getMainAttributes().putValue(Constants.BUNDLE_ACTIVATOR, TestBundleActivator.class.getName()); + JarOutputStream os = new JarOutputStream(new FileOutputStream(f), mf); + + String path = TestBundleActivator.class.getName().replace('.', '/') + ".class"; + os.putNextEntry(new ZipEntry(path)); + + InputStream is = TestBundleActivator.class.getClassLoader().getResourceAsStream(path); + byte[] b = new byte[is.available()]; + is.read(b); + is.close(); + os.write(b); + + os.close(); + return f; + } + + private static void deleteDir(File root) throws IOException + { + if (root.isDirectory()) + { + for (File file : root.listFiles()) + { + deleteDir(file); + } + } + assertTrue(root.delete()); + } + public static class TestBundleActivator implements BundleActivator + { + public void start(BundleContext context) throws Exception + { + Thread.sleep(DELAY); + } + + public void stop(BundleContext context) throws Exception + { + Thread.sleep(DELAY); + } + } +} \ No newline at end of file diff --git a/framework/src/test/java/org/apache/felix/framework/URLHandlersTest.java b/framework/src/test/java/org/apache/felix/framework/URLHandlersTest.java new file mode 100644 index 00000000000..ec9e4a34b4f --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/URLHandlersTest.java @@ -0,0 +1,524 @@ +/* + * Copyright 2013 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.felix.framework; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.ContentHandler; +import java.net.InetAddress; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLClassLoader; +import java.net.URLConnection; +import java.security.Permission; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import junit.framework.TestCase; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.launch.Framework; +import org.osgi.service.url.URLConstants; +import org.osgi.service.url.URLStreamHandlerService; +import org.osgi.service.url.URLStreamHandlerSetter; + +/** + * + * @author pauls + */ +public class URLHandlersTest extends TestCase +{ + + public void testURLHandlers() throws Exception + { + String mf = "Bundle-SymbolicName: url.test\n" + + "Bundle-Version: 1.0.0\n" + + "Bundle-ManifestVersion: 2\n" + + "Import-Package: org.osgi.framework,org.osgi.service.url\n" + + "Manifest-Version: 1.0\n" + + Constants.BUNDLE_ACTIVATOR + ": " + TestURLHandlersActivator.class.getName() + "\n\n"; + + File bundleFile = createBundle(mf, TestURLHandlersActivator.class, UC.class); + + Framework f = createFramework(); + f.init(); + f.start(); + + try + { + final Bundle bundle = f.getBundleContext().installBundle(bundleFile.toURI().toString()); + bundle.start(); + URL url = bundle.getEntry(TestURLHandlersActivator.class.getName().replace(".", "/") + ".class"); + url.openStream(); + new URL(url.toExternalForm()).openStream(); + } + finally + { + try + { + f.stop(); + } + catch (Throwable t) + { + } + } + } + + public void testURLHandlersWithClassLoaderIsolation() throws Exception + { + DelegatingClassLoader cl1 = new DelegatingClassLoader(this.getClass().getClassLoader()); + DelegatingClassLoader cl2 = new DelegatingClassLoader(this.getClass().getClassLoader()); + + Framework f = createFramework(); + f.init(); + f.start(); + + String mf = "Bundle-SymbolicName: url.test\n" + + "Bundle-Version: 1.0.0\n" + + "Bundle-ManifestVersion: 2\n" + + "Import-Package: org.osgi.framework\n" + + "Manifest-Version: 1.0\n" + + Constants.BUNDLE_ACTIVATOR + ": " + TestURLHandlersActivator.class.getName(); + + File bundleFile = createBundle(mf, TestURLHandlersActivator.class, UC.class); + + final Bundle bundle = f.getBundleContext().installBundle(bundleFile.toURI().toString()); + bundle.start(); + + Class clazz1 = cl1.loadClass(URLHandlersTest.class.getName()); + + clazz1.getMethod("testURLHandlers").invoke(clazz1.newInstance()); + + bundle.stop(); + bundle.start(); + Class clazz2 = cl2.loadClass(URLHandlersTest.class.getName()); + + clazz2.getMethod("testURLHandlers").invoke(clazz2.newInstance()); + bundle.stop(); + bundle.start(); + f.stop(); + } + + public void testURLHandlersWithSecurity() throws Exception + { + System.setSecurityManager(new SecurityManager() + { + @Override + public void checkPermission(Permission perm) {} + }); + try + { + testURLHandlers(); + } + finally + { + System.setSecurityManager(null); + } + } + + public void testURLHandlersWithClassLoaderIsolationWithSecurity() throws Exception + { + System.setSecurityManager(new SecurityManager() + { + @Override + public void checkPermission(Permission perm) {} + }); + try + { + testURLHandlersWithClassLoaderIsolation(); + } + finally + { + System.setSecurityManager(null); + } + } + + public static class DelegatingClassLoader extends ClassLoader + { + private final Object m_lock = new Object(); + private final ClassLoader m_source; + + public DelegatingClassLoader(ClassLoader source) + { + m_source = source; + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException + { + synchronized (m_lock) + { + Class result = findLoadedClass(name); + if (result != null) + { + return result; + } + } + if (!name.startsWith("org.apache.felix") && !name.startsWith("org.osgi.")) + { + return m_source.loadClass(name); + } + byte[] buffer = new byte[8 * 1024]; + try + { + InputStream is = m_source.getResourceAsStream(name.replace('.', '/') + ".class"); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + for (int i = is.read(buffer); i != -1; i = is.read(buffer)) + { + os.write(buffer, 0, i); + + } + is.close(); + os.close(); + buffer = os.toByteArray(); + } + catch (Exception ex) + { + throw new ClassNotFoundException("Unable to load class: " + name + " with cl: " + System.identityHashCode(this), ex); + } + return super.defineClass(name, buffer, 0, buffer.length, null); + } + } + public static class UC extends URLConnection + { + public UC(URL u) + { + super(u); + } + + @Override + public void connect() throws IOException + { + + } + + @Override + public String getContentType() + { + return "test"; + } + + @Override + public InputStream getInputStream() throws IOException + { + return null; + } + } + + public static class TestURLHandlersActivator extends ContentHandler implements BundleActivator, URLStreamHandlerService + { + + private volatile ServiceRegistration m_reg = null; + + public URLConnection openConnection(URL u) throws IOException + { + return new UC(u);//throw new UnsupportedOperationException("Not supported yet."); + } + + public void parseURL(URLStreamHandlerSetter realHandler, URL u, String spec, int start, int limit) + { + realHandler.setURL(u, spec, spec, start, spec, spec, spec, spec, spec); + //throw new UnsupportedOperationException("Not supported yet."); + } + + public String toExternalForm(URL u) + { + return u.toString();//throw new UnsupportedOperationException("Not supported yet."); + } + + public boolean equals(URL u1, URL u2) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public int getDefaultPort() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public InetAddress getHostAddress(URL u) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public int hashCode(URL u) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public boolean hostsEqual(URL u1, URL u2) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public boolean sameFile(URL u1, URL u2) + { + throw new UnsupportedOperationException("Not supported yet."); + } + + public void start(final BundleContext context) throws Exception + { + try + { + new URL("test" + System.identityHashCode(TestURLHandlersActivator.this) + ":").openConnection(); + throw new Exception("Unexpected url resolve"); + } + catch (IOException ex) + { + // pass + } + + Hashtable props = new Hashtable(); + props.put(URLConstants.URL_HANDLER_PROTOCOL, "test" + System.identityHashCode(TestURLHandlersActivator.this)); + + ServiceRegistration reg = context.registerService(URLStreamHandlerService.class, this, props); + + new URL("test" + System.identityHashCode(TestURLHandlersActivator.this) + ":").openConnection(); + + if (!(getClass().getProtectionDomain().getCodeSource().getLocation().openConnection() instanceof JarURLConnection)) { + throw new Exception("Unexpted Code Source"); + } + + try + { + if (new URL("test" + System.identityHashCode(TestURLHandlersActivator.this) + ":").getContent() != null) + { + throw new Exception("Unexpected content resolve"); + } + } + catch (IOException ex) + { + // pass + } + + props = new Hashtable(); + props.put(URLConstants.URL_CONTENT_MIMETYPE, "test"); + + try + { + ServiceRegistration reg2 = context.registerService(ContentHandler.class, this, props); + + try + { + if (new URL("test" + System.identityHashCode(TestURLHandlersActivator.this) + ":").getContent() != this) + { + throw new Exception("Unexpected content"); + } + } + finally + { + reg2.unregister(); + } + try + { + if (new URL("test" + System.identityHashCode(TestURLHandlersActivator.this) + ":").getContent() != null) + { + throw new Exception("Unexpected content resolve"); + } + } + catch (IOException ex) + { + // pass + } + } + finally + { + reg.unregister(); + } + + boolean fail; + try + { + new URL("test" + System.identityHashCode(TestURLHandlersActivator.this) + ":").openConnection(); + fail = true; + } + catch (Exception ex) + { + // pass + fail = false; + } + if (fail) { + throw new Exception("Unexpected url resolve"); + } + + Bundle bundle2 = null; + if (context.getBundle().getSymbolicName().equals("url.test")) + { + + String mf = "Bundle-SymbolicName: url.test2\n" + + "Bundle-Version: 1.0.0\n" + + "Bundle-ManifestVersion: 2\n" + + "Import-Package: org.osgi.framework,org.osgi.service.url\n" + + "Manifest-Version: 1.0\n" + + Constants.BUNDLE_ACTIVATOR + ": " + TestURLHandlersActivator.class.getName() + "\n\n"; + + File bundleFile = createBundle(mf, TestURLHandlersActivator.class, UC.class); + + bundle2 = context.installBundle(bundleFile.toURI().toURL().toString()); + } + if (bundle2 != null) + { + fail = false; + try + { + new URL("test" + System.identityHashCode(bundle2) + ":").openConnection(); + fail = true; + } + catch (Exception ex) + { + } + if (fail) { + throw new Exception("Unexpected url resolve"); + } + bundle2.start(); + new URL("test" + System.identityHashCode(bundle2) + ":").openConnection(); + bundle2.stop(); + fail = false; + try + { + new URL("test" + System.identityHashCode(bundle2) + ":").openConnection(); + fail = true; + } + catch (Exception ex) + { + } + if (fail) { + throw new Exception("Unexpected url resolve"); + } + } + else + { + fail = false; + try + { + new URL("test" + System.identityHashCode(context.getBundle()) + ":").openConnection(); + fail = true; + } + catch (Exception ex) + { + } + if (fail) { + throw new Exception("Unexpected url2 resolve"); + } + props = new Hashtable(); + props.put(URLConstants.URL_HANDLER_PROTOCOL, "test" + System.identityHashCode(context.getBundle())); + m_reg = context.registerService(URLStreamHandlerService.class, this, props); + new URL("test" + System.identityHashCode(context.getBundle()) + ":").openConnection(); + } + } + + private static File createBundle(String manifest, Class... classes) throws IOException + { + File f = File.createTempFile("felix-bundle", ".jar"); + f.deleteOnExit(); + + Manifest mf = new Manifest(new ByteArrayInputStream(manifest.getBytes("utf-8"))); + JarOutputStream os = new JarOutputStream(new FileOutputStream(f), mf); + + for (Class clazz : classes) + { + String path = clazz.getName().replace('.', '/') + ".class"; + os.putNextEntry(new ZipEntry(path)); + + InputStream is = clazz.getClassLoader().getResourceAsStream(path); + byte[] buffer = new byte[8 * 1024]; + for (int i = is.read(buffer); i != -1; i = is.read(buffer)) + { + os.write(buffer, 0, i); + } + is.close(); + os.closeEntry(); + } + os.close(); + return f; + } + + public void stop(BundleContext context) throws Exception + { + if (m_reg != null) + { + m_reg.unregister(); + } + } + + @Override + public Object getContent(URLConnection urlc) throws IOException + { + return this; + } + } + + private static File createBundle(String manifest, Class... classes) throws IOException + { + File f = File.createTempFile("felix-bundle", ".jar"); + f.deleteOnExit(); + + Manifest mf = new Manifest(new ByteArrayInputStream(manifest.getBytes("utf-8"))); + JarOutputStream os = new JarOutputStream(new FileOutputStream(f), mf); + + for (Class clazz : classes) + { + String path = clazz.getName().replace('.', '/') + ".class"; + os.putNextEntry(new ZipEntry(path)); + + InputStream is = clazz.getClassLoader().getResourceAsStream(path); + byte[] buffer = new byte[8 * 1024]; + for (int i = is.read(buffer); i != -1; i = is.read(buffer)) + { + os.write(buffer, 0, i); + } + is.close(); + os.closeEntry(); + } + os.close(); + return f; + } + + private static Felix createFramework() throws Exception + { + Map params = new HashMap(); + params.put(Constants.FRAMEWORK_SYSTEMPACKAGES, + "org.osgi.framework; version=1.4.0," + + "org.osgi.service.packageadmin; version=1.2.0," + + "org.osgi.service.startlevel; version=1.1.0," + + "org.osgi.util.tracker; version=1.3.3," + + "org.osgi.service.url; version=1.0.0"); + File cacheDir = File.createTempFile("felix-cache", ".dir"); + if (!cacheDir.delete() || !cacheDir.mkdirs()) + { + fail("Unable to set-up cache dir"); + } + String cache = cacheDir.getPath(); + params.put("felix.cache.profiledir", cache); + params.put("felix.cache.dir", cache); + params.put(Constants.FRAMEWORK_STORAGE, cache); + + return new Felix(params); + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/UninstallBundleTest.java b/framework/src/test/java/org/apache/felix/framework/UninstallBundleTest.java new file mode 100644 index 00000000000..241758a7acf --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/UninstallBundleTest.java @@ -0,0 +1,166 @@ +/* + * 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.framework; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +import junit.framework.TestCase; + +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.FrameworkListener; +import org.osgi.service.packageadmin.ExportedPackage; + +public class UninstallBundleTest extends TestCase +{ + private static final int DELAY = 1000; + + public void testUninstallBundleCleansUpRevision() throws Exception { + Map params = new HashMap(); + params.put(Constants.FRAMEWORK_SYSTEMPACKAGES, + "org.osgi.framework; version=1.4.0," + + "org.osgi.service.packageadmin; version=1.2.0," + + "org.osgi.service.startlevel; version=1.1.0," + + "org.osgi.util.tracker; version=1.3.3," + + "org.osgi.service.url; version=1.0.0"); + File cacheDir = File.createTempFile("felix-cache", ".dir"); + cacheDir.delete(); + cacheDir.mkdirs(); + String cache = cacheDir.getPath(); + params.put("felix.cache.profiledir", cache); + params.put("felix.cache.dir", cache); + params.put(Constants.FRAMEWORK_STORAGE, cache); + + String mfA = "Bundle-SymbolicName: A\n" + + "Bundle-ManifestVersion: 2\n" + + "Export-Package: org.foo.bar\n"; + File bundleAFile = createBundle(mfA, cacheDir); + + String mfB = "Bundle-SymbolicName: B\n" + + "Bundle-ManifestVersion: 2\n" + + "Export-Package: org.foo.bbr\n"; + File bundleBFile = createBundle(mfB, cacheDir); + + String mfC = "Bundle-SymbolicName: C\n" + + "Bundle-ManifestVersion: 2\n" + + "Import-Package: org.foo.bar, org.foo.bbr\n"; + File bundleCFile = createBundle(mfC, cacheDir); + + final List shouldNotBeRefreshed = new ArrayList(); + Felix felix = new Felix(params) { + @Override + void refreshPackages(Collection targets, FrameworkListener[] listeners) + { + if (targets != null) + { + for (Bundle b : targets) + { + if (shouldNotBeRefreshed.contains(b)) + fail("Bundle " + b + " should not be refreshed"); + } + } + super.refreshPackages(targets, listeners); + } + }; + + felix.init(); + felix.start(); + + try + { + Bundle bundleA = felix.getBundleContext().installBundle(bundleAFile.toURI().toString()); + bundleA.start(); + + Bundle bundleB = felix.getBundleContext().installBundle(bundleBFile.toURI().toString()); + bundleB.start(); + // This bundle is not going to be uninstalled, so it should not be refreshed + shouldNotBeRefreshed.add(bundleB); + + Bundle bundleC = felix.getBundleContext().installBundle(bundleCFile.toURI().toString()); + bundleC.start(); + + bundleA.uninstall(); + + boolean foundBar = false; + for (ExportedPackage ep : felix.getExportedPackages((Bundle) null)) + { + if ("org.foo.bar".equals(ep.getName())) + foundBar = true; + } + assertTrue("The system should still export org.foo.bar as C is importing it.", foundBar); + + bundleC.uninstall(); + + for (ExportedPackage ep : felix.getExportedPackages((Bundle) null)) + { + if ("org.foo.bar".equals(ep.getName())) + fail("Should not export org.foo.bar any more!"); + } + + boolean foundBbr = false; + for (ExportedPackage ep : felix.getExportedPackages((Bundle) null)) + { + if ("org.foo.bbr".equals(ep.getName())) + foundBbr = true; + } + assertTrue("The system should still export org.foo.bbr as it was not uninstalled.", foundBbr); + } + finally + { + felix.stop(); + Thread.sleep(DELAY); + deleteDir(cacheDir); + } + } + + private static File createBundle(String manifest, File tempDir) throws IOException + { + File f = File.createTempFile("felix-bundle", ".jar", tempDir); + + Manifest mf = new Manifest(new ByteArrayInputStream(manifest.getBytes("utf-8"))); + mf.getMainAttributes().putValue("Manifest-Version", "1.0"); + JarOutputStream os = new JarOutputStream(new FileOutputStream(f), mf); + + os.close(); + return f; + } + + private static void deleteDir(File root) throws IOException + { + if (root.isDirectory()) + { + for (File file : root.listFiles()) + { + deleteDir(file); + } + } + assertTrue(root.delete()); + } + +} diff --git a/framework/src/test/java/org/apache/felix/framework/cache/BundleCacheTest.java b/framework/src/test/java/org/apache/felix/framework/cache/BundleCacheTest.java new file mode 100644 index 00000000000..fc83cb1e9e4 --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/cache/BundleCacheTest.java @@ -0,0 +1,369 @@ +/* + * 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.framework.cache; + +import junit.framework.TestCase; +import org.apache.felix.framework.Logger; +import org.osgi.framework.Constants; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +public class BundleCacheTest extends TestCase +{ + private File tempDir; + private File cacheDir; + private File filesDir; + private BundleCache cache; + private File archiveFile; + private File jarFile; + + @Override + protected void setUp() throws Exception + { + super.setUp(); + tempDir = File.createTempFile("felix-temp", ".dir"); + assertTrue("precondition", tempDir.delete()); + assertTrue("precondition", tempDir.mkdirs()); + + cacheDir = new File(tempDir, "felix-cache"); + assertTrue("precondition", cacheDir.mkdir()); + + filesDir = new File(tempDir, "files"); + String cacheDirPath = cacheDir.getPath(); + + Map params = new HashMap(); + params.put("felix.cache.profiledir", cacheDirPath); + params.put("felix.cache.dir", cacheDirPath); + params.put(Constants.FRAMEWORK_STORAGE, cacheDirPath); + + cache = new BundleCache(new Logger(){ + @Override + protected void doLog(int level, String msg, Throwable throwable) { + } + }, params); + + archiveFile = new File(filesDir, "bundle1"); + + createTestArchive(archiveFile); + + File innerArchiveFile = new File(archiveFile, "inner"); + createTestArchive(innerArchiveFile); + + new File(innerArchiveFile, "empty").mkdirs(); + + createJar(archiveFile, new File(archiveFile, "inner/i+?äö \\§$%nner.jar")); + + Manifest manifest = new Manifest(); + manifest.getMainAttributes().putValue("foo", "bar"); + manifest.getMainAttributes().putValue("Manifest-Version", "v1"); + + File mf = new File(archiveFile, "META-INF/MANIFEST.MF"); + mf.getParentFile().mkdirs(); + FileOutputStream output = new FileOutputStream(mf); + manifest.write(output); + output.close(); + jarFile = new File(filesDir, "bundle1.jar"); + createJar(archiveFile, jarFile); + } + + public void testNoZipSlip() throws Exception + { + File bundle = new File(filesDir, "slip"); + Manifest manifest = new Manifest(); + manifest.getMainAttributes().putValue("Manifest-Version", "v1"); + manifest.getMainAttributes().putValue(Constants.BUNDLE_MANIFESTVERSION, "2"); + manifest.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, "slip"); + + JarOutputStream output = new JarOutputStream(new FileOutputStream(bundle),manifest); + + output.putNextEntry(new ZipEntry("../../bar.jar")); + + output.write(BundleCache.read(new FileInputStream(jarFile), jarFile.length())); + + output.closeEntry(); + + output.close(); + + BundleArchive archive = cache.create(1, 1, "slip", new FileInputStream(bundle)); + + testNoZipSlip(archive); + + archive = cache.create(1, 1, bundle.toURI().toURL().toString(), null); + + testNoZipSlip(archive); + + archive = cache.create(1, 1, "reference:" + bundle.toURI().toURL().toString(), null); + + testNoZipSlip(archive); + + File dir = new File(filesDir, "exploded"); + + dir.mkdirs(); + + File test = new File(dir, "../../bar.jar"); + test.createNewFile(); + test.deleteOnExit(); + + archive = cache.create(1, 1, "reference:" + dir.toURI().toURL().toString(), null); + + testNoZipSlip(archive); + } + + public void testNoZipSlip(BundleArchive archive) throws Exception + { + Content content = archive.getCurrentRevision().getContent().getEntryAsContent("../../bar.jar"); + + assertNull(content); + + String lib = archive.getCurrentRevision().getContent().getEntryAsNativeLibrary("../../bar.jar"); + + assertNull(lib); + } + + public void testDirectoryReference() throws Exception + { + testBundle("reference:" + archiveFile.toURI().toURL(), null); + } + + public void testJarReference() throws Exception + { + testBundle("reference:" + jarFile.toURI().toURL().toString(), null); + } + + public void testJar() throws Exception + { + testBundle(jarFile.toURI().toURL().toString(), null); + } + + public void testInputStream() throws Exception + { + testBundle("bla", jarFile); + } + + private void testBundle(String location, File file) throws Exception + { + BundleArchive archive = cache.create(1, 1, location, file != null ? new FileInputStream(file) : null); + + assertNotNull(archive); + + assertEquals(Long.valueOf(0), archive.getCurrentRevisionNumber()); + + testRevision(archive); + + String nativeLib = archive.getCurrentRevision().getContent().getEntryAsNativeLibrary("file1"); + + assertNotNull(nativeLib); + assertTrue(new File(nativeLib).isFile()); + + archive.revise(location, file != null ? new FileInputStream(file) : null); + + assertEquals(Long.valueOf(1), archive.getCurrentRevisionNumber()); + + testRevision(archive); + + String nativeLib2 = archive.getCurrentRevision().getContent().getEntryAsNativeLibrary("file1"); + + assertNotNull(nativeLib2); + assertTrue(new File(nativeLib).isFile()); + assertTrue(new File(nativeLib2).isFile()); + + archive.purge(); + + assertEquals(Long.valueOf(1), archive.getCurrentRevisionNumber()); + + testRevision(archive); + + String nativeLib3 = archive.getCurrentRevision().getContent().getEntryAsNativeLibrary("file1"); + + assertNotNull(nativeLib3); + assertNotSame(nativeLib, nativeLib2); + assertNotSame(nativeLib2, nativeLib3); + assertFalse(new File(nativeLib).isFile()); + assertTrue(new File(nativeLib2).isFile()); + assertTrue(new File(nativeLib3).isFile()); + } + + private void testRevision(BundleArchive archive) throws Exception + { + BundleArchiveRevision revision = archive.getCurrentRevision(); + assertNotNull(revision); + assertNotNull(revision.getManifestHeader()); + assertEquals("bar", revision.getManifestHeader().get("foo")); + perRevision(revision.getContent(), new TreeSet(Arrays.asList("META-INF/", "META-INF/MANIFEST.MF", "file1", "inner/", "inner/empty/", "inner/file1", "inner/i+?äö \\§$%nner.jar"))); + perRevision(revision.getContent().getEntryAsContent("inner"), new TreeSet(Arrays.asList("file1", "empty/", "i+?äö \\§$%nner.jar"))); + assertNull(revision.getContent().getEntryAsContent("inner/inner")); + assertNotNull(revision.getContent().getEntryAsContent("inner/empty/")); + assertNull(revision.getContent().getEntryAsContent("inner/empty").getEntries()); + perRevision(revision.getContent().getEntryAsContent("inner/").getEntryAsContent("i+?äö \\§$%nner.jar"), new TreeSet(Arrays.asList("file1", "inner/", "inner/empty/", "inner/file1"))); + } + + private void perRevision(Content content, Set expectedEntries) throws Exception + { + assertNotNull(content); + Enumeration entries = content.getEntries(); + Set foundEntries = new TreeSet(); + while (entries.hasMoreElements()) + { + foundEntries.add(entries.nextElement()); + } + assertEquals(expectedEntries, foundEntries); + + assertTrue(content.hasEntry("file1")); + assertFalse(content.hasEntry("foo")); + assertFalse(content.hasEntry("foo/bar")); + + byte[] entry = content.getEntryAsBytes("file1"); + assertNotNull(entry); + assertEquals("file1", new String(entry, "UTF-8")); + assertNull(content.getEntryAsBytes("foo")); + assertNull(content.getEntryAsBytes("foo/bar")); + + + InputStream input = content.getEntryAsStream("file1"); + assertNotNull(input); + entry = new byte[1014]; + int j = 0; + for (int i = input.read();i != -1; i = input.read()) + { + entry[j++] = (byte) i; + } + assertEquals("file1", new String(entry, 0 , j, "UTF-8")); + assertNull(content.getEntryAsStream("foo")); + assertNull(content.getEntryAsStream("foo/bar")); + + URL url = content.getEntryAsURL("file1"); + assertNotNull(url); + input = url.openStream(); + assertNotNull(input); + entry = new byte[1014]; + j = 0; + for (int i = input.read();i != -1; i = input.read()) + { + entry[j++] = (byte) i; + } + assertEquals("file1", new String(entry, 0 , j, "UTF-8")); + assertNull(content.getEntryAsURL("foo")); + assertNull(content.getEntryAsURL("foo/bar")); + + assertNull(content.getEntryAsNativeLibrary("blub")); + String nativeLib = content.getEntryAsNativeLibrary("file1"); + assertNotNull(nativeLib); + assertTrue(new File(nativeLib).isFile()); + content.close(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + cache.delete(); + assertTrue(!cacheDir.exists()); + assertTrue(BundleCache.deleteDirectoryTree(tempDir)); + } + + private void createTestArchive(File archiveFile) throws Exception + { + createFile(archiveFile, "file1", "file1".getBytes("UTF-8")); + } + + private void createFile(File parent, String path, byte[] content) throws IOException + { + File target = new File(parent, path); + + target.getParentFile().mkdirs(); + + assertTrue(target.getParentFile().isDirectory()); + + FileOutputStream output = new FileOutputStream(target); + try + { + output.write(content); + } + finally + { + output.close(); + } + } + + private void createJar(File source, File target) throws Exception + { + File tmp = File.createTempFile("bundle", ".jar", filesDir); + JarOutputStream output; + if (new File(source, "META-INF/MANIFEST.MF").isFile()) + { + output = new JarOutputStream(new FileOutputStream(tmp),new Manifest(new FileInputStream(new File(source, "META-INF/MANIFEST.MF")))); + } + else + { + output = new JarOutputStream(new FileOutputStream(tmp)); + } + + writeRecursive(source, "", output); + + output.close(); + target.delete(); + tmp.renameTo(target); + } + + private void writeRecursive(File current, String path, JarOutputStream output) throws Exception + { + if (current.isDirectory()) + { + File[] children = current.listFiles(); + if (children != null) + { + for (File file : children) + { + if (!file.getName().equals("MANIFEST.MF")) + { + String next = path + file.getName(); + if (file.isDirectory()) + { + next += "/"; + } + output.putNextEntry(new ZipEntry(next)); + if (file.isDirectory()) + { + output.closeEntry(); + } + writeRecursive(file, next, output); + } + } + } + } + else if (current.isFile()) + { + output.write(BundleCache.read(new FileInputStream(current), current.length())); + output.closeEntry(); + } + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/capabilityset/SimpleFilterTest.java b/framework/src/test/java/org/apache/felix/framework/capabilityset/SimpleFilterTest.java new file mode 100644 index 00000000000..b498a73fa57 --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/capabilityset/SimpleFilterTest.java @@ -0,0 +1,86 @@ +/* + * 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.framework.capabilityset; + +import junit.framework.TestCase; +import java.util.List; + +public class SimpleFilterTest extends TestCase +{ + public void testSubstringMatching() + { + List pieces; + + pieces = SimpleFilter.parseSubstring("*"); + assertTrue("Should match!", SimpleFilter.compareSubstring(pieces, "")); + + pieces = SimpleFilter.parseSubstring("foo"); + assertFalse("Should not match!", SimpleFilter.compareSubstring(pieces, "")); + + pieces = SimpleFilter.parseSubstring(""); + assertTrue("Should match!", SimpleFilter.compareSubstring(pieces, "")); + assertFalse("Should not match!", SimpleFilter.compareSubstring(pieces, "foo")); + + pieces = SimpleFilter.parseSubstring("foo"); + assertTrue("Should match!", SimpleFilter.compareSubstring(pieces, "foo")); + assertFalse("Should not match!", SimpleFilter.compareSubstring(pieces, "barfoo")); + assertFalse("Should not match!", SimpleFilter.compareSubstring(pieces, "foobar")); + + pieces = SimpleFilter.parseSubstring("foo*"); + assertTrue("Should match!", SimpleFilter.compareSubstring(pieces, "foo")); + assertFalse("Should not match!", SimpleFilter.compareSubstring(pieces, "barfoo")); + assertTrue("Should match!", SimpleFilter.compareSubstring(pieces, "foobar")); + + pieces = SimpleFilter.parseSubstring("*foo"); + assertTrue("Should match!", SimpleFilter.compareSubstring(pieces, "foo")); + assertTrue("Should match!", SimpleFilter.compareSubstring(pieces, "barfoo")); + assertFalse("Should match!", SimpleFilter.compareSubstring(pieces, "foobar")); + + pieces = SimpleFilter.parseSubstring("foo*bar"); + assertTrue("Should match!", SimpleFilter.compareSubstring(pieces, "foobar")); + assertFalse("Should not match!", SimpleFilter.compareSubstring(pieces, "barfoo")); + assertTrue("Should match!", SimpleFilter.compareSubstring(pieces, "foosldfjbar")); + + pieces = SimpleFilter.parseSubstring("*foo*bar"); + assertTrue("Should match!", SimpleFilter.compareSubstring(pieces, "foobar")); + assertFalse("Should not match!", SimpleFilter.compareSubstring(pieces, "foobarfoo")); + assertTrue("Should match!", SimpleFilter.compareSubstring(pieces, "barfoobar")); + assertTrue("Should match!", SimpleFilter.compareSubstring(pieces, "sdffoobsdfbar")); + + pieces = SimpleFilter.parseSubstring("*foo*bar*"); + assertTrue("Should match!", SimpleFilter.compareSubstring(pieces, "foobar")); + assertTrue("Should match!", SimpleFilter.compareSubstring(pieces, "foobarfoo")); + assertTrue("Should match!", SimpleFilter.compareSubstring(pieces, "barfoobar")); + assertTrue("Should match!", SimpleFilter.compareSubstring(pieces, "sdffoobsdfbar")); + assertTrue("Should match!", SimpleFilter.compareSubstring(pieces, "sdffoobsdfbarlj")); + assertFalse("Should not match!", SimpleFilter.compareSubstring(pieces, "sdffobsdfbarlj")); + + pieces = SimpleFilter.parseSubstring("*foo(*bar*"); + assertTrue("Should match!", SimpleFilter.compareSubstring(pieces, "foo()bar")); + + pieces = SimpleFilter.parseSubstring("*foo*bar*bar"); + assertFalse("Should not match!", SimpleFilter.compareSubstring(pieces, "foobar")); + + pieces = SimpleFilter.parseSubstring("aaaa*aaaa"); + assertFalse("Should not match!", SimpleFilter.compareSubstring(pieces, "aaaaaaa")); + + pieces = SimpleFilter.parseSubstring("aaa**aaa"); + assertTrue("Should match!", SimpleFilter.compareSubstring(pieces, "aaaaaa")); + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/util/UtilTest.java b/framework/src/test/java/org/apache/felix/framework/util/UtilTest.java new file mode 100644 index 00000000000..f3516b856f8 --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/util/UtilTest.java @@ -0,0 +1,112 @@ +/* + * 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.framework.util; + +import java.util.Properties; +import junit.framework.TestCase; + +public class UtilTest extends TestCase +{ + public void testVariableSubstitution() + { + Properties props = new Properties(); + props.setProperty("one", "${two}"); + props.setProperty("two", "2"); + String v = Util.substVars(props.getProperty("one"), "one", null, props); + assertEquals("2", v); + + props.clear(); + props.setProperty("one", "${two}${three}"); + props.setProperty("two", "2"); + props.setProperty("three", "3"); + v = Util.substVars(props.getProperty("one"), "one", null, props); + assertEquals("23", v); + + props.clear(); + props.setProperty("one", "${two${three}}"); + props.setProperty("two3", "2"); + props.setProperty("three", "3"); + v = Util.substVars(props.getProperty("one"), "one", null, props); + assertEquals("2", v); + + props.clear(); + props.setProperty("one", "${two${three}}"); + props.setProperty("two3", "2"); + System.setProperty("three", "3"); + v = Util.substVars(props.getProperty("one"), "one", null, props); + System.getProperties().remove("three"); + assertEquals("2", v); + + props.clear(); + props.setProperty("one", "${two}"); + v = Util.substVars(props.getProperty("one"), "one", null, props); + assertEquals("", v); + + props.clear(); + props.setProperty("one", "{two"); + v = Util.substVars(props.getProperty("one"), "one", null, props); + assertEquals("{two", v); + + props.clear(); + props.setProperty("one", "{two}"); + v = Util.substVars(props.getProperty("one"), "one", null, props); + assertEquals("{two}", v); + + props.clear(); + props.setProperty("one", "${two"); + v = Util.substVars(props.getProperty("one"), "one", null, props); + assertEquals("${two", v); + + props.clear(); + props.setProperty("one", "${two${two}"); + props.setProperty("two", "2"); + v = Util.substVars(props.getProperty("one"), "one", null, props); + assertEquals("${two2", v); + + props.clear(); + props.setProperty("one", "{two${two}}"); + props.setProperty("two", "2"); + v = Util.substVars(props.getProperty("one"), "one", null, props); + assertEquals("{two2}", v); + + props.clear(); + props.setProperty("one", "{two}${two"); + v = Util.substVars(props.getProperty("one"), "one", null, props); + assertEquals("{two}${two", v); + + props.clear(); + props.setProperty("one", "leading text ${two}"); + props.setProperty("two", "2"); + v = Util.substVars(props.getProperty("one"), "one", null, props); + assertEquals("leading text 2", v); + + props.clear(); + props.setProperty("one", "${two} trailing text"); + props.setProperty("two", "2"); + v = Util.substVars(props.getProperty("one"), "one", null, props); + assertEquals("2 trailing text", v); + + props.clear(); + props.setProperty("one", "${two} middle text ${three}"); + props.setProperty("two", "2"); + props.setProperty("three", "3"); + v = Util.substVars(props.getProperty("one"), "one", null, props); + assertEquals("2 middle text 3", v); + } +} \ No newline at end of file diff --git a/framework/src/test/java/org/apache/felix/framework/util/WeakZipFileTest.java b/framework/src/test/java/org/apache/felix/framework/util/WeakZipFileTest.java new file mode 100644 index 00000000000..1847056a3e2 --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/util/WeakZipFileTest.java @@ -0,0 +1,97 @@ +/* + * 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.framework.util; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import junit.framework.TestCase; +import org.apache.felix.framework.util.WeakZipFileFactory.WeakZipFile; + +public class WeakZipFileTest extends TestCase +{ + private static final String ENTRY_NAME = "entry.txt"; + + public void testWeakClose() + { + // Create a reasonably big random string. + byte[] contentBytes = new byte[16384]; + for (int i = 0; i < contentBytes.length; i++) + { + contentBytes[i] = (byte) ((i % 65) + 65); + } + String contentString = new String(contentBytes); + + // Create a temporary zip file. + File tmpZip = null; + try + { + tmpZip = File.createTempFile("felix.test", ".zip"); + tmpZip.deleteOnExit(); + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(tmpZip)); + ZipEntry ze = new ZipEntry(ENTRY_NAME); + zos.putNextEntry(ze); + zos.write(contentBytes, 0, contentBytes.length); + zos.close(); + } + catch (IOException ex) + { + fail("Unable to create temporary zip file: " + ex); + } + + try + { + WeakZipFileFactory factory = new WeakZipFileFactory(1); + WeakZipFile zipFile = factory.create(tmpZip); + assertTrue("Zip file not recorded.", + factory.getZipZiles().contains(zipFile)); + assertTrue("Open zip file not recorded.", + factory.getOpenZipZiles().contains(zipFile)); + ZipEntry ze = zipFile.getEntry(ENTRY_NAME); + assertNotNull("Zip entry not found", ze); + byte[] firstHalf = new byte[contentBytes.length / 2]; + byte[] secondHalf = new byte[contentBytes.length - firstHalf.length]; + InputStream is = zipFile.getInputStream(ze); + is.read(firstHalf); + zipFile.closeWeakly(); + assertTrue("Zip file not recorded.", + factory.getZipZiles().contains(zipFile)); + assertFalse("Open zip file still recorded.", + factory.getOpenZipZiles().contains(zipFile)); + is.read(secondHalf); + assertTrue("Zip file not recorded.", + factory.getZipZiles().contains(zipFile)); + assertTrue("Open zip file not recorded.", + factory.getOpenZipZiles().contains(zipFile)); + byte[] complete = new byte[firstHalf.length + secondHalf.length]; + System.arraycopy(firstHalf, 0, complete, 0, firstHalf.length); + System.arraycopy(secondHalf, 0, complete, firstHalf.length, secondHalf.length); + String completeString = new String(complete); + assertEquals(contentString, completeString); + zipFile.close(); + } + catch (IOException ex) + { + fail("Unable to read zip file entry: " + ex); + } + } +} \ No newline at end of file diff --git a/framework/src/test/java/org/apache/felix/framework/util/manifestparser/ManifestParserTest.java b/framework/src/test/java/org/apache/felix/framework/util/manifestparser/ManifestParserTest.java new file mode 100644 index 00000000000..c95e021fead --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/util/manifestparser/ManifestParserTest.java @@ -0,0 +1,203 @@ +/* + * 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.framework.util.manifestparser; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.felix.framework.util.FelixConstants; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.Version; +import org.osgi.framework.namespace.IdentityNamespace; +import org.osgi.framework.namespace.NativeNamespace; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; + +import junit.framework.TestCase; + +public class ManifestParserTest extends TestCase +{ + public void testIdentityCapabilityMinimal() throws BundleException + { + Map headers = new HashMap(); + headers.put(Constants.BUNDLE_MANIFESTVERSION, "2"); + headers.put(Constants.BUNDLE_SYMBOLICNAME, "foo.bar"); + ManifestParser mp = new ManifestParser(null, null, null, headers); + + BundleCapability ic = findCapability(mp.getCapabilities(), IdentityNamespace.IDENTITY_NAMESPACE); + assertEquals("foo.bar", ic.getAttributes().get(IdentityNamespace.IDENTITY_NAMESPACE)); + assertEquals(IdentityNamespace.TYPE_BUNDLE, ic.getAttributes().get(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE)); + assertEquals(0, ic.getDirectives().size()); + } + + public void testIdentityCapabilityFull() throws BundleException + { + Map headers = new HashMap(); + headers.put(Constants.BUNDLE_MANIFESTVERSION, "2"); + headers.put(Constants.BUNDLE_SYMBOLICNAME, "abc;singleton:=true"); + headers.put(Constants.BUNDLE_VERSION, "1.2.3.something"); + String copyright = "(c) 2013 Apache Software Foundation"; + headers.put(Constants.BUNDLE_COPYRIGHT, copyright); + String description = "A bundle description"; + headers.put(Constants.BUNDLE_DESCRIPTION, description); + String docurl = "http://felix.apache.org/"; + headers.put(Constants.BUNDLE_DOCURL, docurl); + String license = "http://www.apache.org/licenses/LICENSE-2.0"; + headers.put("Bundle-License", license); + ManifestParser mp = new ManifestParser(null, null, null, headers); + + BundleCapability ic = findCapability(mp.getCapabilities(), IdentityNamespace.IDENTITY_NAMESPACE); + assertEquals("abc", ic.getAttributes().get(IdentityNamespace.IDENTITY_NAMESPACE)); + assertEquals(new Version("1.2.3.something"), ic.getAttributes().get(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE)); + assertEquals(IdentityNamespace.TYPE_BUNDLE, ic.getAttributes().get(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE)); + assertEquals(copyright, ic.getAttributes().get(IdentityNamespace.CAPABILITY_COPYRIGHT_ATTRIBUTE)); + assertEquals(description, ic.getAttributes().get(IdentityNamespace.CAPABILITY_DESCRIPTION_ATTRIBUTE)); + assertEquals(docurl, ic.getAttributes().get(IdentityNamespace.CAPABILITY_DOCUMENTATION_ATTRIBUTE)); + assertEquals(license, ic.getAttributes().get(IdentityNamespace.CAPABILITY_LICENSE_ATTRIBUTE)); + + assertEquals(1, ic.getDirectives().size()); + assertEquals("true", ic.getDirectives().get(IdentityNamespace.CAPABILITY_SINGLETON_DIRECTIVE)); + } + + @SuppressWarnings("unchecked") + public void testNativeCapability() throws BundleException { + Map headers = new HashMap(); + headers.put(Constants.BUNDLE_MANIFESTVERSION, "2"); + headers.put(Constants.BUNDLE_SYMBOLICNAME, FelixConstants.SYSTEM_BUNDLE_SYMBOLICNAME); + headers.put(Constants.PROVIDE_CAPABILITY, " osgi.native;" + + "osgi.native.osname:List=\"Windows7,Windows 7,Win7,Win32\";"+ + "osgi.native.osversion:Version=\"7.0\";"+ + "osgi.native.processor:List=\"x86-64,amd64,em64t,x86_64\";"+ + "osgi.native.language=\"en\""); + BundleRevision mockBundleRevision = mock(BundleRevision.class); + when(mockBundleRevision.getSymbolicName()).thenReturn(FelixConstants.SYSTEM_BUNDLE_SYMBOLICNAME); + + ManifestParser mp = new ManifestParser(null, null, mockBundleRevision, headers); + + BundleCapability ic = findCapability(mp.getCapabilities(), NativeNamespace.NATIVE_NAMESPACE); + + assertEquals("en", ic.getAttributes().get(NativeNamespace.CAPABILITY_LANGUAGE_ATTRIBUTE)); + List osList = (List) ic.getAttributes().get(NativeNamespace.CAPABILITY_OSNAME_ATTRIBUTE); + assertEquals(4, osList.size()); + assertEquals(new Version("7.0"), ic.getAttributes().get(NativeNamespace.CAPABILITY_OSVERSION_ATTRIBUTE)); + List nativeProcesserList = (List) ic.getAttributes().get(NativeNamespace.CAPABILITY_PROCESSOR_ATTRIBUTE); + assertEquals(4, nativeProcesserList.size()); + + } + + @SuppressWarnings("unchecked") + public void testAttributes() throws BundleException { + Map headers = new HashMap(); + headers.put(Constants.BUNDLE_MANIFESTVERSION, "2"); + headers.put(Constants.BUNDLE_SYMBOLICNAME,"com.example.test.sample"); + headers.put(Constants.PROVIDE_CAPABILITY, + "com.example;theList:List=\"red,green,blue\";theLong:Long=111"); + headers.put(Constants.REQUIRE_CAPABILITY, + "com.example.other;theList:List=\"one,two,three\";theLong:Long=999"); + + BundleRevision mockBundleRevision = mock(BundleRevision.class); + + when(mockBundleRevision.getSymbolicName()).thenReturn("com.example.test.sample"); + + ManifestParser mp = new ManifestParser(null, null, mockBundleRevision, headers); + + BundleCapability bc = findCapability(mp.getCapabilities(), "com.example"); + Long cLong = (Long) bc.getAttributes().get("theLong"); + assertEquals(Long.valueOf(111), cLong); + List cList = (List) + bc.getAttributes().get("theList"); + assertEquals(3, cList.size()); + assertTrue(cList.contains("red")); + + BundleRequirement br = findRequirement(mp.getRequirements(), "com.example.other"); + Long rLong = (Long) br.getAttributes().get("theLong"); + assertEquals(Long.valueOf(999), rLong); + List rList = (List) br.getAttributes().get("theList"); + assertEquals(3, rList.size()); + } + + public void testConvertNativeCode() throws InvalidSyntaxException + { + List nativeLibraryClauses = new ArrayList(); + String[] libraryFiles = {"lib/http.dll", "lib/zlib.dll"}; + String[] osNames = {"Windows95", "Windows98", "WindowsNT"}; + String[] processors = {"x86"}; + String[] osVersions = null; + String[] languages = {"en", "se"}; + String selectionFilter = "(com.acme.windowing=win32)"; + NativeLibraryClause clause = new NativeLibraryClause(libraryFiles, osNames, processors, osVersions, languages, selectionFilter); + BundleRevision owner = mock(BundleRevision.class); + nativeLibraryClauses.add(clause); + + List nativeBundleReq = ManifestParser.convertNativeCode(owner, nativeLibraryClauses, false); + + BundleRequirement ir = findRequirement(nativeBundleReq, NativeNamespace.NATIVE_NAMESPACE); + + String filterStr = (String)ir.getDirectives().get(NativeNamespace.REQUIREMENT_FILTER_DIRECTIVE); + + Filter actualFilter = FrameworkUtil.createFilter(filterStr); + + Filter expectedFilter = FrameworkUtil.createFilter("(&(|" + + "(osgi.native.osname~=windows95)(osgi.native.osname~=windows98)(osgi.native.osname~=windowsnt)" + + ")" + + "(osgi.native.processor~=x86)" + + "(|(osgi.native.language~=en)" + + "(osgi.native.language~=se)" + + ")"+ + "(com.acme.windowing=win32))"); + assertEquals("Filter Should contain native requirements", expectedFilter, actualFilter); + + } + + private BundleCapability findCapability(Collection capabilities, String namespace) + { + for (BundleCapability capability : capabilities) + { + if (namespace.equals(capability.getNamespace())) + { + return capability; + } + } + return null; + } + + private BundleRequirement findRequirement(Collection requirements, String namespace) + { + for(BundleRequirement requirement: requirements) + { + if(namespace.equals(requirement.getNamespace())) + { + return requirement; + } + } + return null; + } +} diff --git a/framework/src/test/java/org/apache/felix/framework/util/manifestparser/NativeLibraryClauseTest.java b/framework/src/test/java/org/apache/felix/framework/util/manifestparser/NativeLibraryClauseTest.java new file mode 100644 index 00000000000..342e85de3bb --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/util/manifestparser/NativeLibraryClauseTest.java @@ -0,0 +1,171 @@ +/* + * 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.framework.util.manifestparser; + +import junit.framework.TestCase; + +public class NativeLibraryClauseTest extends TestCase { + public void testNormalizeOSName() { + assertEquals("win32", NativeLibraryClause.normalizeOSName("win 32")); + assertEquals("win32", NativeLibraryClause.normalizeOSName("Win*")); + assertEquals("win32", NativeLibraryClause.normalizeOSName("Windows NonExistingFutureVersion 4711")); + assertEquals("windows95", NativeLibraryClause.normalizeOSName("Windows 95")); + assertEquals("windows98", NativeLibraryClause.normalizeOSName("Windows 98")); + assertEquals("windowsnt", NativeLibraryClause.normalizeOSName("WinNT")); + assertEquals("windows2000", NativeLibraryClause.normalizeOSName("Win2000")); + assertEquals("windows2003", NativeLibraryClause.normalizeOSName("Win2003")); + assertEquals("windowsserver2008", NativeLibraryClause.normalizeOSName("Windows Server 2008")); + assertEquals("windowsserver2012", NativeLibraryClause.normalizeOSName("Windows Server 2012")); + assertEquals("windowsserver2016", NativeLibraryClause.normalizeOSName("Windows Server 2016")); + assertEquals("windowsxp", NativeLibraryClause.normalizeOSName("WinXP")); + assertEquals("windowsce", NativeLibraryClause.normalizeOSName("WinCE")); + assertEquals("windowsvista", NativeLibraryClause.normalizeOSName("WinVista")); + assertEquals("windows7", NativeLibraryClause.normalizeOSName("Windows 7")); + assertEquals("windows8", NativeLibraryClause.normalizeOSName("Win8")); + assertEquals("windows10", NativeLibraryClause.normalizeOSName("Windows 10")); + assertEquals("linux", NativeLibraryClause.normalizeOSName("Linux1.2.3")); + assertEquals("aix", NativeLibraryClause.normalizeOSName("AIX-4.5.6")); + assertEquals("digitalunix", NativeLibraryClause.normalizeOSName("digitalunix_blah")); + assertEquals("hpux", NativeLibraryClause.normalizeOSName("HPUX-999")); + assertEquals("irix", NativeLibraryClause.normalizeOSName("Irixxxx")); + assertEquals("macosx", NativeLibraryClause.normalizeOSName("mac OS X")); + assertEquals("netware", NativeLibraryClause.normalizeOSName("Netware")); + assertEquals("openbsd", NativeLibraryClause.normalizeOSName("OpenBSD-0000")); + assertEquals("netbsd", NativeLibraryClause.normalizeOSName("netbsd ")); + assertEquals("os2", NativeLibraryClause.normalizeOSName("os/2")); + assertEquals("qnx", NativeLibraryClause.normalizeOSName("procnto")); + assertEquals("solaris", NativeLibraryClause.normalizeOSName("Solaris 9")); + assertEquals("sunos", NativeLibraryClause.normalizeOSName("SunOS8")); + assertEquals("vxworks", NativeLibraryClause.normalizeOSName("VxWorks")); + + // Try all the already normalized names + assertEquals("aix", NativeLibraryClause.normalizeOSName("aix")); + assertEquals("digitalunix", NativeLibraryClause.normalizeOSName("digitalunix")); + assertEquals("hpux", NativeLibraryClause.normalizeOSName("hpux")); + assertEquals("irix", NativeLibraryClause.normalizeOSName("irix")); + assertEquals("linux", NativeLibraryClause.normalizeOSName("linux")); + assertEquals("macos", NativeLibraryClause.normalizeOSName("macos")); + assertEquals("netbsd", NativeLibraryClause.normalizeOSName("netbsd")); + assertEquals("netware", NativeLibraryClause.normalizeOSName("netware")); + assertEquals("openbsd", NativeLibraryClause.normalizeOSName("openbsd")); + assertEquals("os2", NativeLibraryClause.normalizeOSName("os2")); + assertEquals("qnx", NativeLibraryClause.normalizeOSName("qnx")); + assertEquals("solaris", NativeLibraryClause.normalizeOSName("solaris")); + assertEquals("sunos", NativeLibraryClause.normalizeOSName("sunos")); + assertEquals("vxworks", NativeLibraryClause.normalizeOSName("vxworks")); + assertEquals("windows2000", NativeLibraryClause.normalizeOSName("windows2000")); + assertEquals("windows2003", NativeLibraryClause.normalizeOSName("windows2003")); + assertEquals("windows7", NativeLibraryClause.normalizeOSName("windows7")); + assertEquals("windows8", NativeLibraryClause.normalizeOSName("windows8")); + assertEquals("windows9", NativeLibraryClause.normalizeOSName("windows9")); + assertEquals("windows10", NativeLibraryClause.normalizeOSName("windows10")); + assertEquals("windows95", NativeLibraryClause.normalizeOSName("windows95")); + assertEquals("windows98", NativeLibraryClause.normalizeOSName("windows98")); + assertEquals("windowsce", NativeLibraryClause.normalizeOSName("windowsce")); + assertEquals("windowsnt", NativeLibraryClause.normalizeOSName("windowsnt")); + assertEquals("windowsserver2008", NativeLibraryClause.normalizeOSName("windowsserver2008")); + assertEquals("windowsserver2012", NativeLibraryClause.normalizeOSName("windowsserver2012")); + assertEquals("windowsvista", NativeLibraryClause.normalizeOSName("windowsvista")); + assertEquals("windowsxp", NativeLibraryClause.normalizeOSName("windowsxp")); + assertEquals("win32", NativeLibraryClause.normalizeOSName("win32")); + } + + public void testgetOsNameWithAliases() { + assertTrue(NativeLibraryClause.getOsNameWithAliases("win 32").contains("win32")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("Win*").contains("win32")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("Windows 95").contains("windows95")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("Windows 98").contains("windows98")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("WinNT").contains("windowsnt")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("Win2000").contains("windows2000")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("Win2003").contains("windows2003")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("Windows Server 2008").contains("windowsserver2008")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("Windows Server 2012").contains("windowsserver2012")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("WinXP").contains("windowsxp")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("WinCE").contains("windowsce")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("WinVista").contains("windowsvista")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("Windows 7").contains("windows7")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("Windows7").contains("windows7")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("Win8").contains("windows8")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("Windows 10").contains("windows10")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("Linux1.2.3").contains("linux")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("AIX-4.5.6").contains("aix")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("digitalunix_blah").contains("digitalunix")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("HPUX-999").contains("hpux")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("Irixxxx").contains("irix")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("mac OS X").contains("mac os x")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("Netware").contains("netware")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("OpenBSD-0000").contains("openbsd")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("netbsd ").contains("netbsd")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("os/2").contains("os2")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("procnto").contains("qnx")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("Solaris 9").contains("solaris")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("SunOS8").contains("sunos")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("VxWorks").contains("vxworks")); + + // Try all the already normalized names + assertTrue(NativeLibraryClause.getOsNameWithAliases("aix").contains("aix")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("digitalunix").contains("digitalunix")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("hpux").contains("hpux")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("irix").contains("irix")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("linux").contains("linux")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("mac os").contains("mac os")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("netbsd").contains("netbsd")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("netware").contains("netware")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("openbsd").contains("openbsd")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("os2").contains("os2")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("qnx").contains("qnx")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("solaris").contains("solaris")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("sunos").contains("sunos")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("vxworks").contains("vxworks")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("windows2000").contains("windows2000")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("windows2003").contains("windows2003")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("windows7").contains("windows7")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("windows8").contains("windows8")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("windows9").contains("windows9")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("windows10").contains("windows10")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("windows95").contains("windows95")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("windows98").contains("windows98")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("windowsce").contains("windowsce")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("windowsnt").contains("windowsnt")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("windowsserver2008").contains("windowsserver2008")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("windowsserver2012").contains("windowsserver2012")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("windowsvista").contains("windowsvista")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("windowsxp").contains("windowsxp")); + assertTrue(NativeLibraryClause.getOsNameWithAliases("win32").contains("win32")); + } + + public void testNormalizeOSVersion() { + // valid + assertEquals("1.0.0", NativeLibraryClause.normalizeOSVersion("1")); + assertEquals("1.2.0", NativeLibraryClause.normalizeOSVersion("1.2")); + assertEquals("1.2.3", NativeLibraryClause.normalizeOSVersion("1.2.3")); + assertEquals("1.2.3.qualifier", NativeLibraryClause.normalizeOSVersion("1.2.3.qualifier")); + + // to normalize + assertEquals("1.0.0.qualifier", NativeLibraryClause.normalizeOSVersion("1.qualifier")); + assertEquals("1.2.0.qualifier", NativeLibraryClause.normalizeOSVersion("1.2.qualifier")); + + assertEquals("3.13.0.39-generic", NativeLibraryClause.normalizeOSVersion("3.13.0-39-generic")); + + assertEquals("3.14.22.100_fc19_i686_PAE", NativeLibraryClause.normalizeOSVersion("3.14.22-100.fc19.i686.PAE")); + assertEquals("4.9.35", NativeLibraryClause.normalizeOSVersion("4.9.35+")); + assertEquals("4.9.0", NativeLibraryClause.normalizeOSVersion("4.9+")); + assertEquals("4.0.0", NativeLibraryClause.normalizeOSVersion("4+")); + } +} diff --git a/framework/src/test/java/org/osgi/util/tracker/BundleTrackerTest.java b/framework/src/test/java/org/osgi/util/tracker/BundleTrackerTest.java new file mode 100644 index 00000000000..a534462ba8f --- /dev/null +++ b/framework/src/test/java/org/osgi/util/tracker/BundleTrackerTest.java @@ -0,0 +1,65 @@ +/* + * 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.osgi.util.tracker; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.BundleListener; + +/** + * Checks that BundleTracker actually tracks and untracks a bundle that is entering and then + * leaving the observed states + */ +public class BundleTrackerTest { + + @Test + public void testTracking() { + Bundle bundle = mock(Bundle.class); + when(bundle.getState()).thenReturn(Bundle.ACTIVE); + + BundleContext context = Mockito.mock(BundleContext.class); + + @SuppressWarnings("unchecked") + BundleTrackerCustomizer customizer = mock(BundleTrackerCustomizer.class); + when(customizer.addingBundle(Mockito.eq(bundle), Mockito.any(BundleEvent.class))).thenReturn(bundle); + + BundleTracker tracker = new BundleTracker(context , Bundle.ACTIVE | Bundle.STARTING, customizer); + tracker.open(); + ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(BundleListener.class); + verify(context).addBundleListener(listenerCaptor.capture()); + BundleListener listener = listenerCaptor.getValue(); + + BundleEvent startedEvent = new BundleEvent(BundleEvent.STARTED, bundle); + listener.bundleChanged(startedEvent); + verify(customizer).addingBundle(bundle, startedEvent); + + when(bundle.getState()).thenReturn(Bundle.INSTALLED); + BundleEvent stoppedEvent = new BundleEvent(BundleEvent.STOPPED, bundle); + listener.bundleChanged(stoppedEvent); + verify(customizer).removedBundle(bundle, stoppedEvent, bundle); + } +} diff --git a/framework/src/test/java/org/osgi/util/tracker/ServiceTrackerTest.java b/framework/src/test/java/org/osgi/util/tracker/ServiceTrackerTest.java new file mode 100644 index 00000000000..6ca0e21bb52 --- /dev/null +++ b/framework/src/test/java/org/osgi/util/tracker/ServiceTrackerTest.java @@ -0,0 +1,74 @@ +/* + * 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.osgi.util.tracker; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; + +public class ServiceTrackerTest { + + @Test + public void testTracking() throws InvalidSyntaxException { + Bundle bundle = mock(Bundle.class); + when(bundle.getState()).thenReturn(Bundle.ACTIVE); + + BundleContext context = Mockito.mock(BundleContext.class); + + @SuppressWarnings("unchecked") + ServiceTrackerCustomizer customizer = mock(ServiceTrackerCustomizer.class); + @SuppressWarnings("unchecked") + ServiceReference ref = mock(ServiceReference.class); + Runnable service = mock(Runnable.class); + when(customizer.addingService(ref)).thenReturn(service); + + Filter filter = mock(Filter.class); + String filterString = "(objectClass=java.lang.Runnable)"; + when(context.createFilter(Mockito.eq(filterString))).thenReturn(filter); + + ServiceTracker tracker = new ServiceTracker(context, Runnable.class, customizer); + tracker.open(); + + ArgumentCaptor listenerCaptor = ArgumentCaptor.forClass(ServiceListener.class); + verify(context).addServiceListener(listenerCaptor.capture(), Mockito.eq(filterString)); + ServiceListener listener = listenerCaptor.getValue(); + + listener.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, ref)); + verify(customizer).addingService(ref); + + listener.serviceChanged(new ServiceEvent(ServiceEvent.MODIFIED, ref)); + verify(customizer).modifiedService(ref, service); + + listener.serviceChanged(new ServiceEvent(ServiceEvent.UNREGISTERING, ref)); + verify(customizer).removedService(ref, service); + + tracker.close(); + } +} diff --git a/gogo/LICENSE b/gogo/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/gogo/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/gogo/NOTICE b/gogo/NOTICE new file mode 100644 index 00000000000..41d68e35c06 --- /dev/null +++ b/gogo/NOTICE @@ -0,0 +1,11 @@ +Apache Felix Gogo +Copyright 2011 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. diff --git a/gogo/bom/pom.xml b/gogo/bom/pom.xml new file mode 100644 index 00000000000..2b9ee4d327c --- /dev/null +++ b/gogo/bom/pom.xml @@ -0,0 +1,71 @@ + + + + + 4.0.0 + + + org.apache.felix + gogo-parent + 5 + ../gogo-parent/pom.xml + + + pom + Apache Felix Gogo BOM + Apache Felix Gogo Bill Of Materials + org.apache.felix.gogo.bom + 1.0.3-SNAPSHOT + http://felix.apache.org/ + + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/gogo/bom/ + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/gogo/bom/ + https://svn.apache.org/repos/asf/felix/trunk/gogo/bom/ + + + + + + org.apache.felix + org.apache.felix.gogo.command + 1.1.0 + runtime + + + org.apache.felix + org.apache.felix.gogo.jline + 1.1.2 + runtime + + + org.apache.felix + org.apache.felix.gogo.runtime + 1.1.2 + + + org.apache.felix + org.apache.felix.gogo.shell + 1.1.2 + runtime + + + + + diff --git a/gogo/command/DEPENDENCIES b/gogo/command/DEPENDENCIES new file mode 100644 index 00000000000..5b77ad2797f --- /dev/null +++ b/gogo/command/DEPENDENCIES @@ -0,0 +1,23 @@ +Apache Felix Gogo Command +Copyright 2014 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 diff --git a/gogo/command/LICENSE b/gogo/command/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/gogo/command/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/gogo/command/NOTICE b/gogo/command/NOTICE new file mode 100644 index 00000000000..ef6670bb39e --- /dev/null +++ b/gogo/command/NOTICE @@ -0,0 +1,11 @@ +Apache Felix Gogo Command +Copyright 2014 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. diff --git a/gogo/command/doc/changelog.txt b/gogo/command/doc/changelog.txt new file mode 100644 index 00000000000..c1c14517d56 --- /dev/null +++ b/gogo/command/doc/changelog.txt @@ -0,0 +1,107 @@ +Changes from 1.0.2 to 1.1.0 +--------------------------- +Bug + [FELIX-5143] - Gogo Command bundle should not include org.osgi.service.log classes + [FELIX-5958] - gogo command exports org.osgi.service.log + +New Feature + [FELIX-5835] - Upgrade to JDK 8 + [FELIX-5836] - Upgrade to OSGi r6 + +Improvement + [FELIX-5969] - remove transitive dependencies from gogo command + [FELIX-5970] - Add requirement & capabilities model so gogo can be resolved + [FELIX-5999] - cleanup compiler warnings + [FELIX-6002] - Remove legacy code in Gogo Command + [FELIX-6003] - Add some resolver checks to make sure @RequireGogo annotation works for both jline and shell + [FELIX-6007] - create a gogo bom + +Changes from 1.0.0 to 1.0.2 +--------------------------- +** Bug + * [FELIX-5392] - Gogo-command has mandatory dependency on bundlerepository + +Changes from 0.16.0 to 1.0.0 +---------------------------- +** Task + * [FELIX-5378] - [gogo] Upgrade packages and bundle to 1.0.0 + +Changes from 0.14.0 to 0.16.0 +----------------------------- +** Improvement + * FELIX-5021 [GOGO] Use system bundle to find bundles + * FELIX-5009 Relative URIs would be nice for install and update + * FELIX-5008 gogo usage messages could be less confusing + * FELIX-3499 felix:cd command works only with relative paths + +** Bug + * FELIX-4969 cd refuses to leave initial directory + +Changes from 0.12.0 to 0.14.0 +----------------------------- + +* Fix a NPE with the obr:info command + +Changes from 0.10.0 to 0.12.0 +----------------------------- + +** Improvement + * [FELIX-3050] - [Gogo Command] Update "inspect" command to work with OSGi R4.3 generic capabilities/requirements + * [FELIX-3088] - [Gogo Command] The "resolve" command should print some message if not all bundles could be resolved + * [FELIX-3118] - obr:deploy should allow user to specify whether optional bundles are deployed or not + +Changes from 0.8.0 to 0.10.0 +---------------------------- + +** Bug + * [FELIX-2937] - [Gogo Command] OBR commands do not output anything + when nothing is found + * [FELIX-2938] - [Gogo Command] The "info" command throws an NPE if + the specified bundle identifier doesn't exist + * [FELIX-3001] - [gogo] help command throws ClassCastException if any + osgi.command.function service property is not String[] + +Changes from 0.6.1 to 0.8.0 +--------------------------- + +** Bug + * [FELIX-2676] - bundlelevel gogo command will never work for modifying + bundle level + +Changes from 0.6.0 to 0.6.1 +--------------------------- + +* Import Gogo packages with mandatory attribute. + +Gogo Command 0.6.0 +------------------ + +** Bug + * [FELIX-1473] - [gogo] The syntax does not provide a way to call + methods on a string + * [FELIX-1474] - [gogo] result of commands is implicitly written to pipe + * [FELIX-1493] - [gogo] automatic expansion of $args in Closure stops + direct access to $args list + * [FELIX-2337] - [gogo] no way to access array[] elements produced by + assignment + * [FELIX-2375] - [gogo] when supplied args can't be coerced, the error + message prints the arg values, rather than their types + * [FELIX-2380] - [gogo] lock contention in piped writer when reader + doesn't read all input + +** Improvement + * [FELIX-1487] - Support for commands on multiple lines + * [FELIX-2328] - [gogo] tidy-up runtime to remove optional code etc + * [FELIX-2339] - [gogo] add support for running scripts + * [FELIX-2342] - [gogo] remove old felix command adaptor + +** New Feature + * [FELIX-2363] - [Gogo] Add annotations for creating commands with + optional and out-of-order arguments + +** Task + * [FELIX-1670] - [gogo] launcher bundle not required + * [FELIX-1889] - Gogo should depend on the official OSGi jars + * [FELIX-2334] - [Gogo] Use org.apache.felix as Maven groupId + * [FELIX-2367] - [Gogo] Use org.apache.felix namespace to avoid any + perceived legal issues diff --git a/gogo/command/pom.xml b/gogo/command/pom.xml new file mode 100644 index 00000000000..75d65a1a950 --- /dev/null +++ b/gogo/command/pom.xml @@ -0,0 +1,94 @@ + + + + + 4.0.0 + + + org.apache.felix + gogo-parent + 6-SNAPSHOT + ../gogo-parent/pom.xml + + + org.apache.felix.gogo.command + bundle + 1.1.1-SNAPSHOT + Apache Felix Gogo Command + + + Provides basic shell commands for Gogo. + + http://felix.apache.org/ + + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/gogo/command/ + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/gogo/command/ + https://svn.apache.org/repos/asf/felix/trunk/gogo/command/ + + + + + org.osgi + org.osgi.service.log + + + org.osgi + osgi.annotation + + + org.osgi + osgi.core + + + org.apache.felix + org.apache.felix.gogo.runtime + 1.0.0 + + + junit + junit + + + org.mockito + mockito-core + + + + + + + org.apache.felix + maven-bundle-plugin + + + + !org.osgi.service.log, + org.apache.felix.service.command, + * + + + org.osgi.service.log + + + + + + + diff --git a/gogo/command/src/main/java/org/apache/felix/gogo/command/Activator.java b/gogo/command/src/main/java/org/apache/felix/gogo/command/Activator.java new file mode 100644 index 00000000000..aa4c2819629 --- /dev/null +++ b/gogo/command/src/main/java/org/apache/felix/gogo/command/Activator.java @@ -0,0 +1,56 @@ +/* + * 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.gogo.command; + +import java.util.Hashtable; + +import org.osgi.annotation.bundle.Header; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; + +@Header(name = Constants.BUNDLE_ACTIVATOR, value = "${@class}") +public class Activator implements BundleActivator +{ + public void start(BundleContext bc) { + BundleContext systemBundleContext = bc.getBundle(0).getBundleContext(); + Hashtable props = new Hashtable<>(); + props.put("osgi.command.scope", "felix"); + props.put("osgi.command.function", new String[] { + "bundlelevel", "frameworklevel", "headers", + "help", "install", "lb", "log", "refresh", + "resolve", "start", "stop", "uninstall", "update", + "which" }); + bc.registerService( + Basic.class.getName(), new Basic(systemBundleContext), props); + + props.put("osgi.command.scope", "felix"); + props.put("osgi.command.function", new String[] { "inspect" }); + bc.registerService( + Inspect.class.getName(), new Inspect(systemBundleContext), props); + + props.put("osgi.command.scope", "felix"); + props.put("osgi.command.function", new String[] { "cd", "ls" }); + bc.registerService( + Files.class.getName(), new Files(bc), props); + } + + public void stop(BundleContext bc) { + } +} \ No newline at end of file diff --git a/gogo/command/src/main/java/org/apache/felix/gogo/command/Basic.java b/gogo/command/src/main/java/org/apache/felix/gogo/command/Basic.java new file mode 100644 index 00000000000..06fd8f21ea4 --- /dev/null +++ b/gogo/command/src/main/java/org/apache/felix/gogo/command/Basic.java @@ -0,0 +1,995 @@ +/* + * 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.gogo.command; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Formatter; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; + +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Descriptor; +import org.apache.felix.service.command.Parameter; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.BundleReference; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.startlevel.BundleStartLevel; +import org.osgi.framework.startlevel.FrameworkStartLevel; +import org.osgi.framework.wiring.FrameworkWiring; +import org.osgi.service.log.LogEntry; +import org.osgi.service.log.LogReaderService; +import org.osgi.service.log.LogService; + +public class Basic +{ + private final BundleContext m_bc; + private final Bundle m_b0; + + public Basic(BundleContext bc) + { + m_bc = bc; + m_b0 = m_bc.getBundle(0); + } + + @Descriptor("query bundle start level") + public String bundlelevel(@Descriptor("bundle to query") Bundle bundle) + { + return bundle + " is level " + bundle.adapt(BundleStartLevel.class).getStartLevel(); + } + + @Descriptor("set bundle start level or initial bundle start level") + public String bundlelevel( + @Descriptor("set the bundle's start level") @Parameter(names = { "-s", + "--setlevel" }, presentValue = "true", absentValue = "false") boolean set, + @Descriptor("set the initial bundle start level") @Parameter(names = { "-i", + "--setinitial" }, presentValue = "true", absentValue = "false") boolean initial, + @Descriptor("target level") int level, + @Descriptor("target identifiers") Bundle[] bundles) + { + + if (set && initial) + { + return "Cannot specify '-s' and '-i' at the same time."; + } + else if (!set && !initial) + { + return "Must specify either '-s' or '-i'."; + } + else if (level <= 0) + { + return "Specified start level must be greater than zero."; + } + // Set the initial bundle start level. + else if (initial) + { + if ((bundles != null) && (bundles.length == 0)) + { + m_b0.adapt(FrameworkStartLevel.class).setInitialBundleStartLevel(level); + } + else + { + return "Cannot specify bundles when setting initial start level."; + } + } + // Set the bundle start level. + else if (set) + { + if ((bundles != null) && (bundles.length != 0)) + { + for (Bundle bundle : bundles) + { + bundle.adapt(BundleStartLevel.class).setStartLevel(level); + } + } + else + { + return "Must specify target bundles."; + } + } + return null; + } + + @Descriptor("query framework active start level") + public String frameworklevel() + { + return "Level is " + m_b0.adapt(FrameworkStartLevel.class).getStartLevel(); + } + + @Descriptor("set framework active start level") + public void frameworklevel(@Descriptor("target start level") int level) + { + m_b0.adapt(FrameworkStartLevel.class).setStartLevel(level); + } + + @Descriptor("display bundle headers") + public String headers(@Descriptor("target bundles") Bundle[] bundles) + { + try (Formatter f = new Formatter()) { + bundles = ((bundles == null) || (bundles.length == 0)) ? m_bc.getBundles() + : bundles; + String prefix = ""; + for (Bundle bundle : bundles) + { + String title = Util.getBundleName(bundle); + f.format("%s%s%n", prefix, title); + f.format("%s%n", Util.getUnderlineString(title.length())); + Dictionary dict = bundle.getHeaders(); + Enumeration keys = dict.keys(); + while (keys.hasMoreElements()) + { + String k = keys.nextElement(); + String v = dict.get(k); + f.format("%s = %s%n", k, v); + } + prefix = "\n"; + } + return f.toString(); + } + } + + @Descriptor("displays available commands") + public String help() + { + try (Formatter f = new Formatter()) { + Map> commands = getCommands(); + for (String name : commands.keySet()) + { + f.format("%s%n", name); + } + return f.toString(); + } + } + + @Descriptor("displays information about a specific command") + public String help(@Descriptor("target command") String name) + { + Map> commands = getCommands(); + + List methods = null; + + // If the specified command doesn't have a scope, then + // search for matching methods by ignoring the scope. + int scopeIdx = name.indexOf(':'); + if (scopeIdx < 0) + { + for (Entry> entry : commands.entrySet()) + { + String k = entry.getKey().substring(entry.getKey().indexOf(':') + 1); + if (name.equals(k)) + { + name = entry.getKey(); + methods = entry.getValue(); + break; + } + } + } + // Otherwise directly look up matching methods. + else + { + methods = commands.get(name); + } + + if ((methods == null) || (methods.size() <= 0)) + { + return "No methods found matching: " + name; + } + + try (Formatter f = new Formatter()) { + String prefix = ""; + for (Method m : methods) + { + Descriptor d = m.getAnnotation(Descriptor.class); + if (d == null) + { + f.format("%s%s%n", prefix, m.getName()); + } + else + { + f.format("%s%s - %s%n", prefix, m.getName(), d.value()); + } + + f.format(" scope: %s%n", name.substring(0, name.indexOf(':'))); + + // Get flags and options. + Class[] paramTypes = m.getParameterTypes(); + Map flags = new TreeMap<>(); + Map flagDescs = new TreeMap<>(); + Map options = new TreeMap<>(); + Map optionDescs = new TreeMap<>(); + List params = new ArrayList<>(); + Annotation[][] anns = m.getParameterAnnotations(); + for (int paramIdx = 0; paramIdx < anns.length; paramIdx++) + { + Class paramType = m.getParameterTypes()[paramIdx]; + if (paramType == CommandSession.class) { + /* Do not bother the user with a CommandSession. */ + continue; + } + Parameter p = findAnnotation(anns[paramIdx], Parameter.class); + d = findAnnotation(anns[paramIdx], Descriptor.class); + if (p != null) + { + if (p.presentValue().equals(Parameter.UNSPECIFIED)) + { + options.put(p.names()[0], p); + if (d != null) + { + optionDescs.put(p.names()[0], d.value()); + } + } + else + { + flags.put(p.names()[0], p); + if (d != null) + { + flagDescs.put(p.names()[0], d.value()); + } + } + } + else if (d != null) + { + params.add(paramTypes[paramIdx].getSimpleName()); + params.add(d.value()); + } + else + { + params.add(paramTypes[paramIdx].getSimpleName()); + params.add(""); + } + } + + // Print flags and options. + if (flags.size() > 0) + { + f.format(" flags:%n"); + for (Entry entry : flags.entrySet()) + { + // Print all aliases. + String[] names = entry.getValue().names(); + f.format(" %s", names[0]); + for (int aliasIdx = 1; aliasIdx < names.length; aliasIdx++) + { + f.format(", %s", names[aliasIdx]); + } + f.format(" %s%n", flagDescs.get(entry.getKey())); + } + } + if (options.size() > 0) + { + f.format(" options:%n"); + for (Entry entry : options.entrySet()) + { + // Print all aliases. + String[] names = entry.getValue().names(); + f.format(" %s", names[0]); + for (int aliasIdx = 1; aliasIdx < names.length; aliasIdx++) + { + f.format(", %s", names[aliasIdx]); + } + f.format(" %s%s%n", + optionDescs.get(entry.getKey()), + ((entry.getValue().absentValue() == null) ? "" : " [optional]")); + } + } + if (params.size() > 0) + { + f.format(" parameters:%n"); + for (Iterator it = params.iterator(); it.hasNext();) + { + f.format(" %s %s%n", it.next(), it.next()); + } + } + prefix = "\n"; + } + return f.toString(); + } + } + + private static T findAnnotation(Annotation[] anns, + Class clazz) + { + for (int i = 0; (anns != null) && (i < anns.length); i++) + { + if (clazz.isInstance(anns[i])) + { + return clazz.cast(anns[i]); + } + } + return null; + } + + private Map> getCommands() + { + ServiceReference[] refs = null; + try + { + refs = m_bc.getAllServiceReferences(null, "(osgi.command.scope=*)"); + } + catch (InvalidSyntaxException ex) + { + // This should never happen. + } + + Map> commands = new TreeMap<>(); + + for (ServiceReference ref : refs) + { + Object svc = m_bc.getService(ref); + if (svc != null) + { + String scope = (String) ref.getProperty("osgi.command.scope"); + Object ofunc = ref.getProperty("osgi.command.function"); + String[] funcs = (ofunc instanceof String[]) ? (String[]) ofunc + : new String[] { String.valueOf(ofunc) }; + + for (String func : funcs) + { + commands.put(scope + ":" + func, new ArrayList()); + } + + if (!commands.isEmpty()) + { + Method[] methods = svc.getClass().getMethods(); + for (Method method : methods) + { + List commandMethods = commands.get(scope + ":" + + method.getName()); + if (commandMethods != null) + { + commandMethods.add(method); + } + } + } + + // Remove any missing commands. + Iterator>> it = commands.entrySet().iterator(); + while (it.hasNext()) + { + if (it.next().getValue().size() == 0) + { + it.remove(); + } + } + } + } + + return commands; + } + + @Descriptor("install bundle using URLs") + public String install(@Descriptor("command session")CommandSession session, + @Descriptor("target URLs") String[] urls) throws IOException + { + try (Formatter f = new Formatter()) { + + StringBuilder sb = new StringBuilder(); + + for (String url : urls) + { + String location = Util.resolveUri(session, url.trim()); + Bundle bundle = null; + try + { + bundle = m_bc.installBundle(location, null); + } + catch (IllegalStateException ex) + { + f.format("%s%n", ex.toString()); + } + catch (BundleException ex) + { + if (ex.getNestedException() != null) + { + f.format("%s%n", ex.getNestedException().toString()); + } + else + { + f.format("%s%n", ex.toString()); + } + } + catch (Exception ex) + { + f.format("%s%n", ex.toString()); + } + if (bundle != null) + { + if (sb.length() > 0) + { + sb.append(", "); + } + sb.append(bundle.getBundleId()); + } + } + if (sb.toString().indexOf(',') > 0) + { + return "Bundle IDs: " + sb.toString(); + } + else if (sb.length() > 0) + { + return "Bundle ID: " + sb.toString(); + } + return f.toString(); + } + } + + @Descriptor("list all installed bundles") + public String lb( + @Descriptor("show location") @Parameter(names = { "-l", "--location" }, presentValue = "true", absentValue = "false") boolean showLoc, + @Descriptor("show symbolic name") @Parameter(names = { "-s", "--symbolicname" }, presentValue = "true", absentValue = "false") boolean showSymbolic, + @Descriptor("show update location") @Parameter(names = { "-u", "--updatelocation" }, presentValue = "true", absentValue = "false") boolean showUpdate) + { + return lb(showLoc, showSymbolic, showUpdate, null); + } + + @Descriptor("list installed bundles matching a substring") + public String lb( + @Descriptor("show location") @Parameter(names = { "-l", "--location" }, presentValue = "true", absentValue = "false") boolean showLoc, + @Descriptor("show symbolic name") @Parameter(names = { "-s", "--symbolicname" }, presentValue = "true", absentValue = "false") boolean showSymbolic, + @Descriptor("show update location") @Parameter(names = { "-u", "--updatelocation" }, presentValue = "true", absentValue = "false") boolean showUpdate, + @Descriptor("subtring matched against name or symbolic name") String pattern) + { + if ((showLoc && showSymbolic && showUpdate) || + (showLoc && showSymbolic) || + (showSymbolic && showUpdate) || + (showLoc && showUpdate)) { + return "Only one of -l, -s, -u should be used."; + } + List found = new ArrayList<>(); + + if (pattern == null) + { + found.addAll(Arrays.asList(m_bc.getBundles())); + } + else + { + Bundle[] bundles = m_bc.getBundles(); + for (Bundle bundle : bundles) { + String name = bundle.getHeaders().get(Constants.BUNDLE_NAME); + if (matchBundleName(bundle.getSymbolicName(), pattern) + || matchBundleName(name, pattern)) { + found.add(bundle); + } + } + } + + if (found.size() > 0) + { + try (Formatter f = new Formatter()) { + printBundleList(found.toArray(new Bundle[found.size()]), + showLoc, showSymbolic, showUpdate, m_b0, f); + return f.toString(); + } + } + else + { + return "No matching bundles found."; + } + } + + private boolean matchBundleName(String name, String pattern) + { + return (name != null) && name.toLowerCase().contains(pattern.toLowerCase()); + } + + @Descriptor("display all matching log entries") + public String log( + @Descriptor("minimum log level [ debug | info | warn | error ]") String logLevel) + { + return log(-1, logLevel); + } + + @Descriptor("display some matching log entries") + public String log(@Descriptor("maximum number of entries") int maxEntries, + @Descriptor("minimum log level [ debug | info | warn | error ]") String logLevel) + { + // Keep track of service references. + List> refs = new ArrayList<>(); + + // Get start level service. + try { + LogReaderService lrs = Util.getService(m_bc, LogReaderService.class, refs); + if (lrs == null) + { + return "Log reader service is unavailable."; + } + else + { + try (Formatter f = new Formatter()) { + @SuppressWarnings("unchecked") + Enumeration entries = lrs.getLog(); + List select = new ArrayList<>(); + + int minLevel = logLevelAsInt(logLevel); + + int index = 0; + while (entries.hasMoreElements() && (maxEntries < 0 || index < maxEntries)) + { + LogEntry entry = entries.nextElement(); + if (entry.getLevel() <= minLevel) + { + select.add(0, entry); + index++; + } + } + + for (LogEntry e : select) { + display(e,f); + } + Util.ungetServices(m_bc, refs); + return f.toString(); + } + } + } + catch (NoClassDefFoundError ncdfe) { + return "Log reader service is unavailable."; + } + } + + private void display(LogEntry entry, Formatter f) + { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss"); + + StringBuilder buffer = new StringBuilder(); + buffer.append(sdf.format(new Date(entry.getTime()))).append(" "); + buffer.append(logLevelAsString(entry.getLevel())).append(" - "); + buffer.append("Bundle: ").append(entry.getBundle().getSymbolicName()); + if (entry.getServiceReference() != null) + { + buffer.append(" - "); + buffer.append(entry.getServiceReference().toString()); + } + buffer.append(" - ").append(entry.getMessage()); + if (entry.getException() != null) + { + buffer.append(" - "); + StringWriter writer = new StringWriter(); + PrintWriter pw = new PrintWriter(writer); + entry.getException().printStackTrace(pw); + buffer.append(writer.toString()); + } + + f.format("%s%n", buffer.toString()); + } + + private static int logLevelAsInt(String logLevel) + { + if ("error".equalsIgnoreCase(logLevel)) + { + return LogService.LOG_ERROR; + } + else if ("warn".equalsIgnoreCase(logLevel)) + { + return LogService.LOG_WARNING; + } + else if ("info".equalsIgnoreCase(logLevel)) + { + return LogService.LOG_INFO; + } + return LogService.LOG_DEBUG; + } + + private static String logLevelAsString(int level) + { + switch (level) + { + case LogService.LOG_ERROR: + return "ERROR"; + case LogService.LOG_WARNING: + return "WARNING"; + case LogService.LOG_INFO: + return "INFO"; + default: + return "DEBUG"; + } + } + + @Descriptor("refresh bundles") + public void refresh( + @Descriptor("target bundles (can be null or empty)") Bundle[] bundles) + { + if ((bundles != null) && (bundles.length == 0)) + { + bundles = null; + } + + m_b0.adapt(FrameworkWiring.class).refreshBundles(Arrays.asList(bundles)); + } + + @Descriptor("resolve bundles") + public String resolve( + @Descriptor("target bundles (can be null or empty)") Bundle[] bundles) + { + if (m_b0.adapt(FrameworkWiring.class).resolveBundles(bundles != null ? Arrays.asList(bundles) : null)) + { + return "Not all bundles could be resolved."; + } + return null; + } + + @Descriptor("start bundles") + public String start( + @Descriptor("start bundle transiently") @Parameter(names = { "-t", "--transient" }, presentValue = "true", absentValue = "false") boolean trans, + @Descriptor("use declared activation policy") @Parameter(names = { "-p", + "--policy" }, presentValue = "true", absentValue = "false") boolean policy, + @Descriptor("target bundle identifiers or URLs") String[] ss) + { + if ((ss == null) || (ss.length < 1)) { + return "Please specify the bundles to start."; + } + + int options = 0; + + // Check for "transient" switch. + if (trans) + { + options |= Bundle.START_TRANSIENT; + } + + // Check for "start policy" switch. + if (policy) + { + options |= Bundle.START_ACTIVATION_POLICY; + } + + try (Formatter f = new Formatter()) { + for (String s : ss) + { + String id = s.trim(); + + try + { + Bundle bundle = null; + + // The id may be a number or a URL, so check. + if (Character.isDigit(id.charAt(0))) + { + long l = Long.parseLong(id); + bundle = m_bc.getBundle(l); + } + else + { + bundle = m_bc.installBundle(id); + } + + if (bundle != null) + { + bundle.start(options); + } + else + { + f.format("Bundle ID '%s' is invalid.%n", id); + } + } + catch (NumberFormatException ex) + { + f.format("Unable to parse id '%s'.%n", id); + } + catch (BundleException ex) + { + if (ex.getNestedException() != null) + { + f.format("%s%n", ex.getNestedException().toString()); + } + else + { + f.format("%s%n", ex.toString()); + } + } + catch (Exception ex) + { + f.format("%s%n", ex.toString()); + } + } + return f.toString(); + } + } + + @Descriptor("stop bundles") + public String stop(@Descriptor("stop bundle transiently") @Parameter(names = { "-t", + "--transient" }, presentValue = "true", absentValue = "false") boolean trans, + @Descriptor("target bundles") Bundle[] bundles) + { + if ((bundles == null) || (bundles.length == 0)) + { + return "Please specify the bundles to stop."; + } + + int options = 0; + + // Check for "transient" switch. + if (trans) + { + options |= Bundle.STOP_TRANSIENT; + } + + try (Formatter f = new Formatter()) { + for (Bundle bundle : bundles) + { + try + { + bundle.stop(options); + } + catch (BundleException ex) + { + if (ex.getNestedException() != null) + { + f.format("%s%n", ex.getNestedException().toString()); + } + else + { + f.format("%s%n", ex.toString()); + } + } + catch (Exception ex) + { + f.format("%s%n", ex.toString()); + } + } + return f.toString(); + } + } + + @Descriptor("uninstall bundles") + public String uninstall(@Descriptor("target bundles") Bundle[] bundles) + { + if ((bundles == null) || (bundles.length == 0)) + { + return "Please specify the bundles to uninstall."; + } + + try (Formatter f = new Formatter()) { + for (Bundle bundle : bundles) + { + try + { + bundle.uninstall(); + } + catch (BundleException ex) + { + if (ex.getNestedException() != null) + { + f.format("%s%n", ex.getNestedException().toString()); + } + else { + f.format("%s%n", ex.toString()); + } + } + catch (Exception ex) + { + f.format("%s%n", ex.toString()); + } + } + return f.toString(); + } + } + + @Descriptor("update bundle") + public String update(@Descriptor("target bundle") Bundle bundle) + { + if (bundle == null) + { + return "Must specify a bundle."; + } + + try + { + bundle.update(); + return null; + } + catch (BundleException ex) + { + if (ex.getNestedException() != null) + { + return ex.getNestedException().toString(); + } + + return ex.toString(); + } + catch (Exception ex) + { + return ex.toString(); + } + } + + @Descriptor("update bundle from URL") + public String update( + @Descriptor("command session") CommandSession session, + @Descriptor("target bundle") Bundle bundle, + @Descriptor("URL from where to retrieve bundle") String location) throws IOException { + if (bundle == null) + { + return "Must specify a bundle."; + } + if (location == null) + { + return "Must specify a location."; + } + + try + { + location = Util.resolveUri(session, location.trim()); + InputStream is = new URL(location).openStream(); + bundle.update(is); + return null; + } + catch (MalformedURLException ex) + { + return "Unable to parse URL"; + } + catch (IOException ex) + { + return "Unable to open input stream: " + ex; + } + catch (BundleException ex) + { + if (ex.getNestedException() != null) + { + return ex.getNestedException().toString(); + } + + return ex.toString(); + } + catch (Exception ex) + { + return ex.toString(); + } + } + + @Descriptor("determines from where a bundle loads a class") + public String which(@Descriptor("target bundle") Bundle bundle, + @Descriptor("target class name") String className) + { + if (bundle == null) + { + return "Please specify a bundle"; + } + + Class clazz = null; + try + { + clazz = bundle.loadClass(className); + if (clazz.getClassLoader() == null) + { + return "Loaded from: boot class loader"; + } + else if (clazz.getClassLoader() instanceof BundleReference) + { + Bundle p = ((BundleReference) clazz.getClassLoader()).getBundle(); + return "Loaded from: " + p; + } + else + { + return "Loaded from: " + clazz.getClassLoader(); + } + } + catch (ClassNotFoundException ex) + { + return "Class not found"; + } + } + + private static void printBundleList(Bundle[] bundles, + boolean showLoc, boolean showSymbolic, boolean showUpdate, Bundle b0, Formatter f) + { + f.format("START LEVEL %s%n", b0.adapt(FrameworkStartLevel.class).getStartLevel()); + + // Determine last column. + String lastColumn = "Name"; + if (showLoc) + { + lastColumn = "Location"; + } + else if (showSymbolic) + { + lastColumn = "Symbolic name"; + } + else if (showUpdate) + { + lastColumn = "Update location"; + } + + f.format("%5s|%-11s|%5s|%s%n", "ID", "State", "Level", lastColumn); + for (Bundle bundle : bundles) + { + // Get the bundle name or location. + String name = bundle.getHeaders().get(Constants.BUNDLE_NAME); + // If there is no name, then default to symbolic name. + name = (name == null) ? bundle.getSymbolicName() : name; + // If there is no symbolic name, resort to location. + name = (name == null) ? bundle.getLocation() : name; + + // Overwrite the default value is the user specifically + // requested to display one or the other. + if (showLoc) + { + name = bundle.getLocation(); + } + else if (showSymbolic) + { + name = bundle.getSymbolicName(); + name = (name == null) ? "" : name; + } + else if (showUpdate) + { + name = bundle.getHeaders().get(Constants.BUNDLE_UPDATELOCATION); + name = (name == null) ? bundle.getLocation() : name; + } + + // Show bundle version if not showing location. + name = (!showLoc && !showUpdate) ? name + " (" + bundle.getVersion() + ")" + : name; + + // Get the bundle's start level. + int level = bundle.adapt(BundleStartLevel.class).getStartLevel(); + + f.format("%5d|%-11s|%5d|%s|%s%n", + bundle.getBundleId(), getStateString(bundle), level, name, + bundle.getVersion()); + } + } + + private static String getStateString(Bundle bundle) + { + int state = bundle.getState(); + if (state == Bundle.ACTIVE) + { + return "Active "; + } + else if (state == Bundle.INSTALLED) + { + return "Installed "; + } + else if (state == Bundle.RESOLVED) + { + return "Resolved "; + } + else if (state == Bundle.STARTING) + { + return "Starting "; + } + else if (state == Bundle.STOPPING) + { + return "Stopping "; + } + else + { + return "Unknown "; + } + } +} diff --git a/gogo/command/src/main/java/org/apache/felix/gogo/command/Files.java b/gogo/command/src/main/java/org/apache/felix/gogo/command/Files.java new file mode 100644 index 00000000000..a278e88975b --- /dev/null +++ b/gogo/command/src/main/java/org/apache/felix/gogo/command/Files.java @@ -0,0 +1,291 @@ +/* + * 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.gogo.command; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Descriptor; +import org.osgi.framework.BundleContext; +import static org.apache.felix.gogo.command.Util.CWD; + +public class Files +{ + @SuppressWarnings("unused") + private final BundleContext m_bc; + + public Files(BundleContext bc) + { + m_bc = bc; + } + + @Descriptor("get current directory") + public File cd( + @Descriptor("automatically supplied shell session") CommandSession session) + { + try + { + return cd(session, null); + } + catch (IOException ex) + { + throw new RuntimeException("Unable to get current directory"); + } + } + + @Descriptor("change current directory") + public File cd( + @Descriptor("automatically supplied shell session") CommandSession session, + @Descriptor("target directory") String dir) + throws IOException + { + File cwd = (File) session.get(CWD); + if (cwd == null) + { + cwd = new File(".").getCanonicalFile(); + session.put(CWD, cwd); + } + if ((dir == null) || (dir.length() == 0)) + { + return cwd; + } + + URI curUri = cwd.toURI(); + URI newUri = curUri.resolve(dir); + + cwd = new File(newUri); + if (!cwd.exists()) + { + throw new IOException("Directory does not exist"); + } + else if (!cwd.isDirectory()) + { + throw new IOException("Target is not a directory"); + } + session.put(CWD, cwd.getCanonicalFile()); + return cwd; + } + + @Descriptor("get current directory contents") + public File[] ls( + @Descriptor("automatically supplied shell session") CommandSession session) + throws IOException + { + return ls(session, null); + } + + @Descriptor("get specified path contents") + public File[] ls( + @Descriptor("automatically supplied shell session") CommandSession session, + @Descriptor("path with optionally wildcarded file name") String pattern) + throws IOException + { + pattern = ((pattern == null) || (pattern.length() == 0)) ? "." : pattern; + pattern = ((pattern.charAt(0) != File.separatorChar) && (pattern.charAt(0) != '.')) + ? "./" + pattern : pattern; + int idx = pattern.lastIndexOf(File.separatorChar); + String parent = (idx < 0) ? "." : pattern.substring(0, idx + 1); + String target = (idx < 0) ? pattern : pattern.substring(idx + 1); + + File actualParent = ((parent.charAt(0) == File.separatorChar) + ? new File(parent) : new File(cd(session), parent)).getCanonicalFile(); + + idx = target.indexOf(File.separatorChar, idx); + boolean isWildcarded = (target.indexOf('*', idx) >= 0); + File[] files; + if (isWildcarded) + { + if (!actualParent.exists()) + { + throw new IOException("File does not exist"); + } + final List pieces = parseSubstring(target); + files = actualParent.listFiles(new FileFilter() { + public boolean accept(File pathname) + { + return compareSubstring(pieces, pathname.getName()); + } + }); + } + else + { + File actualTarget = new File(actualParent, target).getCanonicalFile(); + if (!actualTarget.exists()) + { + throw new IOException("File does not exist"); + } + if (actualTarget.isDirectory()) + { + files = actualTarget.listFiles(); + } + else + { + files = new File[] { actualTarget }; + } + } + return files; + } + + public static List parseSubstring(String value) + { + List pieces = new ArrayList<>(); + StringBuilder ss = new StringBuilder(); + // int kind = SIMPLE; // assume until proven otherwise + boolean wasStar = false; // indicates last piece was a star + boolean leftstar = false; // track if the initial piece is a star + boolean rightstar = false; // track if the final piece is a star + + int idx = 0; + + // We assume (sub)strings can contain leading and trailing blanks + boolean escaped = false; +loop: for (;;) + { + if (idx >= value.length()) + { + if (wasStar) + { + // insert last piece as "" to handle trailing star + rightstar = true; + } + else + { + pieces.add(ss.toString()); + // accumulate the last piece + // note that in the case of + // (cn=); this might be + // the string "" (!=null) + } + ss.setLength(0); + break loop; + } + + // Read the next character and account for escapes. + char c = value.charAt(idx++); + if (!escaped && ((c == '(') || (c == ')'))) + { + throw new IllegalArgumentException( + "Illegal value: " + value); + } + else if (!escaped && (c == '*')) + { + if (wasStar) + { + // encountered two successive stars; + // I assume this is illegal + throw new IllegalArgumentException("Invalid filter string: " + value); + } + if (ss.length() > 0) + { + pieces.add(ss.toString()); // accumulate the pieces + // between '*' occurrences + } + ss.setLength(0); + // if this is a leading star, then track it + if (pieces.size() == 0) + { + leftstar = true; + } + wasStar = true; + } + else if (!escaped && (c == '\\')) + { + escaped = true; + } + else + { + escaped = false; + wasStar = false; + ss.append(c); + } + } + if (leftstar || rightstar || pieces.size() > 1) + { + // insert leading and/or trailing "" to anchor ends + if (rightstar) + { + pieces.add(""); + } + if (leftstar) + { + pieces.add(0, ""); + } + } + return pieces; + } + + public static boolean compareSubstring(List pieces, String s) + { + // Walk the pieces to match the string + // There are implicit stars between each piece, + // and the first and last pieces might be "" to anchor the match. + // assert (pieces.length > 1) + // minimal case is * + + boolean result = true; + int len = pieces.size(); + + int index = 0; + +loop: for (int i = 0; i < len; i++) + { + String piece = pieces.get(i); + + // If this is the first piece, then make sure the + // string starts with it. + if (i == 0) + { + if (!s.startsWith(piece)) + { + result = false; + break loop; + } + } + + // If this is the last piece, then make sure the + // string ends with it. + if (i == len - 1) + { + result = s.endsWith(piece); + break loop; + } + + // If this is neither the first or last piece, then + // make sure the string contains it. + if ((i > 0) && (i < (len - 1))) + { + index = s.indexOf(piece, index); + if (index < 0) + { + result = false; + break loop; + } + } + + // Move string index beyond the matching piece. + index += piece.length(); + } + + return result; + } +} diff --git a/gogo/command/src/main/java/org/apache/felix/gogo/command/Inspect.java b/gogo/command/src/main/java/org/apache/felix/gogo/command/Inspect.java new file mode 100644 index 00000000000..d1242513065 --- /dev/null +++ b/gogo/command/src/main/java/org/apache/felix/gogo/command/Inspect.java @@ -0,0 +1,461 @@ +/* + * 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.gogo.command; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Formatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.felix.service.command.Descriptor; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; + +public class Inspect +{ + public static final String NONSTANDARD_SERVICE_NAMESPACE = "service"; + + public static final String CAPABILITY = "capability"; + public static final String REQUIREMENT = "requirement"; + + private static final String EMPTY_MESSAGE = "[EMPTY]"; + private static final String UNUSED_MESSAGE = "[UNUSED]"; + private static final String UNRESOLVED_MESSAGE = "[UNRESOLVED]"; + + private final BundleContext m_bc; + + public Inspect(BundleContext bc) + { + m_bc = bc; + } + + @Descriptor("inspects bundle capabilities and requirements") + public String inspect( + @Descriptor("('capability' | 'requirement')") String direction, + @Descriptor("( | 'service')") String namespace, + @Descriptor("target bundles") Bundle[] bundles) + { + return inspect(m_bc, direction, namespace, bundles); + } + + private static String inspect( + BundleContext bc, String direction, String namespace, Bundle[] bundles) + { + // Verify arguments. + if (isValidDirection(direction)) + { + bundles = ((bundles == null) || (bundles.length == 0)) + ? bc.getBundles() : bundles; + + if (CAPABILITY.startsWith(direction)) + { + return printCapabilities(bc, Util.parseSubstring(namespace), bundles); + } + else + { + return printRequirements(bc, Util.parseSubstring(namespace), bundles); + } + } + + return "Invalid argument: " + direction; + } + + public static String printCapabilities( + BundleContext bc, List namespace, Bundle[] bundles) + { + try (Formatter f = new Formatter()) { + for (Bundle b : bundles) + { + // Print out any matching generic capabilities. + BundleWiring wiring = b.adapt(BundleWiring.class); + if (wiring != null) + { + String title = b + " provides:"; + f.format("%s%n%s%n", title, Util.getUnderlineString(title.length())); + + // Print generic capabilities for matching namespaces. + boolean matches = printMatchingCapabilities(wiring, namespace, f); + + // Handle service capabilities separately, since they aren't part + // of the generic model in OSGi. + if (matchNamespace(namespace, NONSTANDARD_SERVICE_NAMESPACE)) + { + matches |= printServiceCapabilities(b, f); + } + + // If there were no capabilities for the specified namespace, + // then say so. + if (!matches) + { + f.format("%s %s%n", Util.unparseSubstring(namespace), EMPTY_MESSAGE); + } + } + else + { + f.format("Bundle %s is not resolved.", + b.getBundleId()); + } + } + return f.toString(); + } + } + + private static boolean printMatchingCapabilities(BundleWiring wiring, List namespace, Formatter f) + { + List wires = wiring.getProvidedWires(null); + Map> aggregateCaps = + aggregateCapabilities(namespace, wires); + List allCaps = wiring.getCapabilities(null); + boolean matches = false; + for (BundleCapability cap : allCaps) + { + if (matchNamespace(namespace, cap.getNamespace())) + { + if ("osgi.service".equals(cap.getNamespace())) { + continue; + } + matches = true; + List dependents = aggregateCaps.get(cap); + Object keyAttr = + cap.getAttributes().get(cap.getNamespace()); + if ("osgi.native".equals(cap.getNamespace())) + { + f.format("%s with properties:%n", cap.getNamespace()); + List> sortedEntries = + new ArrayList>(cap.getAttributes().entrySet()); + Collections.sort(sortedEntries, new Comparator>() { + @Override + public int compare(Entry o1, Entry o2) { + return o1.getKey().compareTo(o2.getKey()); + }}); + + for (Entry e : sortedEntries) { + f.format(" %s = %s%n", e.getKey(), e.getValue()); + } + + if (dependents != null) + { + f.format(" required by:%n"); + for (BundleWire wire : dependents) { + f.format(" %s%n", wire.getRequirerWiring().getBundle()); + } + } + else + { + f.format(" %s%n", UNUSED_MESSAGE); + } + } + else if (dependents != null) + { + if (keyAttr != null) + { + f.format("%s; %s %s required by:%n", + cap.getNamespace(), + format(keyAttr), + getVersionFromCapability(cap)); + } + else + { + f.format("%s required by:%n", cap.toString()); + } + for (BundleWire wire : dependents) + { + f.format(" %s%n", wire.getRequirerWiring().getBundle()); + } + } + else if (keyAttr != null) + { + f.format("%s; %s %s %s%n", + cap.getNamespace(), + format(keyAttr), + getVersionFromCapability(cap), + UNUSED_MESSAGE); + } + else + { + f.format("%s %s%n", cap, UNUSED_MESSAGE); + } + } + } + return matches; + } + + private static String format(Object object) { + String retVal; + if (object.getClass().isArray() || object instanceof Collection) { + StringBuffer buffer = new StringBuffer(); + @SuppressWarnings("rawtypes") + Iterable formatTarget = object.getClass().isArray() ? Arrays.asList(object) : (Iterable) object; + for (Object elem : formatTarget) { + if (buffer.length()>0) { + buffer.append(','); + } + buffer.append(elem.toString()); + } + retVal = buffer.toString(); + } + else { + retVal = String.valueOf(object); + } + return retVal; + } + + private static Map> aggregateCapabilities( + List namespace, List wires) + { + // Aggregate matching capabilities. + Map> map = + new HashMap<>(); + for (BundleWire wire : wires) + { + if (matchNamespace(namespace, wire.getCapability().getNamespace())) + { + List dependents = map.get(wire.getCapability()); + if (dependents == null) + { + dependents = new ArrayList<>(); + map.put(wire.getCapability(), dependents); + } + dependents.add(wire); + } + } + return map; + } + + static boolean printServiceCapabilities(Bundle b, Formatter f) + { + boolean matches = false; + + try + { + ServiceReference[] refs = b.getRegisteredServices(); + + if ((refs != null) && (refs.length > 0)) + { + matches = true; + // Print properties for each service. + for (ServiceReference ref : refs) + { + // Print object class with "namespace". + f.format("%s; %s with properties:%n", + NONSTANDARD_SERVICE_NAMESPACE, + Util.getValueString(ref.getProperty("objectClass"))); + // Print service properties. + String[] keys = ref.getPropertyKeys(); + for (String key : keys) + { + if (!key.equalsIgnoreCase(Constants.OBJECTCLASS)) + { + Object v = ref.getProperty(key); + f.format(" %s = %s%n", key, Util.getValueString(v)); + } + } + Bundle[] users = ref.getUsingBundles(); + if ((users != null) && (users.length > 0)) + { + f.format(" Used by:%n"); + for (Bundle user : users) + { + f.format(" %s%n", user); + } + } + } + } + } + catch (Exception ex) + { + f.format("%s%n", ex.toString()); + } + + return matches; + } + + public static String printRequirements( + BundleContext bc, List namespace, Bundle[] bundles) + { + try (Formatter f = new Formatter()) { + for (Bundle b : bundles) + { + // Print out any matching generic requirements. + BundleWiring wiring = b.adapt(BundleWiring.class); + if (wiring != null) + { + String title = b + " requires:"; + f.format("%s%n%s%n", title, Util.getUnderlineString(title.length())); + boolean matches = printMatchingRequirements(wiring, namespace, f); + + // Handle service requirements separately, since they aren't part + // of the generic model in OSGi. + if (matchNamespace(namespace, NONSTANDARD_SERVICE_NAMESPACE)) + { + matches |= printServiceRequirements(b, f); + } + + // If there were no requirements for the specified namespace, + // then say so. + if (!matches) + { + f.format("%s %s%n", Util.unparseSubstring(namespace), EMPTY_MESSAGE); + } + } + else + { + f.format("Bundle %s is not resolved.%n", + b.getBundleId()); + } + } + return f.toString(); + } + } + + private static boolean printMatchingRequirements(BundleWiring wiring, List namespace, Formatter f) + { + List wires = wiring.getRequiredWires(null); + Map> aggregateReqs = + aggregateRequirements(namespace, wires); + List allReqs = wiring.getRequirements(null); + boolean matches = false; + for (BundleRequirement req : allReqs) + { + if (matchNamespace(namespace, req.getNamespace())) + { + matches = true; + List providers = aggregateReqs.get(req); + if (providers != null) + { + f.format("%s; %s resolved by:%n", + req.getNamespace(), + req.getDirectives().get(Constants.FILTER_DIRECTIVE)); + for (BundleWire wire : providers) + { + String msg; + Object keyAttr = + wire.getCapability().getAttributes() + .get(wire.getCapability().getNamespace()); + if (keyAttr != null) + { + msg = wire.getCapability().getNamespace() + + "; " + + keyAttr + + " " + + getVersionFromCapability(wire.getCapability()); + } + else + { + msg = wire.getCapability().toString(); + } + f.format(" %s from %s%n", msg, wire.getProviderWiring().getBundle()); + } + } + else + { + f.format("%s; %s %s%n", + req.getNamespace(), + req.getDirectives().get(Constants.FILTER_DIRECTIVE), + UNRESOLVED_MESSAGE); + } + } + } + return matches; + } + + private static Map> aggregateRequirements( + List namespace, List wires) + { + // Aggregate matching capabilities. + Map> map = + new HashMap<>(); + for (BundleWire wire : wires) + { + if (matchNamespace(namespace, wire.getRequirement().getNamespace())) + { + List providers = map.get(wire.getRequirement()); + if (providers == null) + { + providers = new ArrayList<>(); + map.put(wire.getRequirement(), providers); + } + providers.add(wire); + } + } + return map; + } + + static boolean printServiceRequirements(Bundle b, Formatter f) + { + boolean matches = false; + + try + { + ServiceReference[] refs = b.getServicesInUse(); + + if ((refs != null) && (refs.length > 0)) + { + matches = true; + // Print properties for each service. + for (ServiceReference ref : refs) + { + // Print object class with "namespace". + f.format("%s; %s provided by:%n %s%n", + NONSTANDARD_SERVICE_NAMESPACE, + Util.getValueString(ref.getProperty("objectClass")), + ref.getBundle()); + } + } + } + catch (Exception ex) + { + System.err.println(ex.toString()); + } + + return matches; + } + + private static String getVersionFromCapability(BundleCapability c) + { + Object o = c.getAttributes().get(Constants.VERSION_ATTRIBUTE); + if (o == null) + { + o = c.getAttributes().get(Constants.BUNDLE_VERSION_ATTRIBUTE); + } + return (o == null) ? "" : o.toString(); + } + + private static boolean matchNamespace(List namespace, String actual) + { + return Util.compareSubstring(namespace, actual); + } + + private static boolean isValidDirection(String direction) + { + return (CAPABILITY.startsWith(direction) || REQUIREMENT.startsWith(direction)); + } +} \ No newline at end of file diff --git a/gogo/command/src/main/java/org/apache/felix/gogo/command/Util.java b/gogo/command/src/main/java/org/apache/felix/gogo/command/Util.java new file mode 100644 index 00000000000..21a84272a1e --- /dev/null +++ b/gogo/command/src/main/java/org/apache/felix/gogo/command/Util.java @@ -0,0 +1,424 @@ +/* + * 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.gogo.command; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; + +import org.apache.felix.service.command.CommandSession; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + +public class Util +{ + + + static final String CWD = "_cwd"; + + public static String getBundleName(Bundle bundle) + { + if (bundle != null) + { + String name = bundle.getHeaders().get(Constants.BUNDLE_NAME); + return (name == null) + ? "Bundle " + Long.toString(bundle.getBundleId()) + : name + " (" + Long.toString(bundle.getBundleId()) + ")"; + } + return "[STALE BUNDLE]"; + } + private final static StringBuffer m_sb = new StringBuffer(); + + public static String getUnderlineString(int len) + { + synchronized (m_sb) + { + m_sb.delete(0, m_sb.length()); + for (int i = 0; i < len; i++) + { + m_sb.append('-'); + } + return m_sb.toString(); + } + } + + public static String getValueString(Object obj) + { + synchronized (m_sb) + { + if (obj instanceof String) + { + return (String) obj; + } + else if (obj instanceof String[]) + { + String[] array = (String[]) obj; + m_sb.delete(0, m_sb.length()); + for (int i = 0; i < array.length; i++) + { + if (i != 0) + { + m_sb.append(", "); + } + m_sb.append(array[i]); + } + return m_sb.toString(); + } + else if (obj instanceof Boolean) + { + return ((Boolean) obj).toString(); + } + else if (obj instanceof Long) + { + return ((Long) obj).toString(); + } + else if (obj instanceof Integer) + { + return ((Integer) obj).toString(); + } + else if (obj instanceof Short) + { + return ((Short) obj).toString(); + } + else if (obj instanceof Double) + { + return obj.toString(); + } + else if (obj instanceof Float) + { + return obj.toString(); + } + else if (obj == null) + { + return "null"; + } + else + { + return obj.toString(); + } + } + } + + public static T getService( + BundleContext bc, Class clazz, List> refs) + { + @SuppressWarnings("unchecked") + ServiceReference ref = (ServiceReference) bc.getServiceReference(clazz.getName()); + if (ref == null) + { + return null; + } + T t = bc.getService(ref); + if (t != null) + { + refs.add(ref); + } + return t; + } + + public static void ungetServices(BundleContext bc, List> refs) + { + while (refs.size() > 0) + { + bc.ungetService(refs.remove(0)); + } + } + + public static void unjar(JarInputStream jis, File dir) + throws IOException + { + // Reusable buffer. + byte[] buffer = new byte[4096]; + + // Loop through JAR entries. + for (JarEntry je = jis.getNextJarEntry(); + je != null; + je = jis.getNextJarEntry()) + { + if (je.getName().startsWith("/")) + { + throw new IOException("JAR resource cannot contain absolute paths."); + } + + File target = new File(dir, je.getName()); + + // Check to see if the JAR entry is a directory. + if (je.isDirectory()) + { + if (!target.exists()) + { + if (!target.mkdirs()) + { + throw new IOException("Unable to create target directory: " + + target); + } + } + // Just continue since directories do not have content to copy. + continue; + } + + int lastIndex = je.getName().lastIndexOf('/'); + String name = (lastIndex >= 0) ? + je.getName().substring(lastIndex + 1) : je.getName(); + String destination = (lastIndex >= 0) ? + je.getName().substring(0, lastIndex) : ""; + + // JAR files use '/', so convert it to platform separator. + destination = destination.replace('/', File.separatorChar); + copy(jis, dir, name, destination, buffer); + } + } + + public static void copy( + InputStream is, File dir, String destName, String destDir, byte[] buffer) + throws IOException + { + if (destDir == null) + { + destDir = ""; + } + + // Make sure the target directory exists and + // that is actually a directory. + File targetDir = new File(dir, destDir); + if (!targetDir.exists()) + { + if (!targetDir.mkdirs()) + { + throw new IOException("Unable to create target directory: " + + targetDir); + } + } + else if (!targetDir.isDirectory()) + { + throw new IOException("Target is not a directory: " + + targetDir); + } + + BufferedOutputStream bos = new BufferedOutputStream( + new FileOutputStream(new File(targetDir, destName))); + int count = 0; + while ((count = is.read(buffer)) > 0) + { + bos.write(buffer, 0, count); + } + bos.close(); + } + + public static List parseSubstring(String value) + { + List pieces = new ArrayList<>(); + StringBuilder ss = new StringBuilder(); + // int kind = SIMPLE; // assume until proven otherwise + boolean wasStar = false; // indicates last piece was a star + boolean leftstar = false; // track if the initial piece is a star + boolean rightstar = false; // track if the final piece is a star + + int idx = 0; + + // We assume (sub)strings can contain leading and trailing blanks + boolean escaped = false; +loop: for (;;) + { + if (idx >= value.length()) + { + if (wasStar) + { + // insert last piece as "" to handle trailing star + rightstar = true; + } + else + { + pieces.add(ss.toString()); + // accumulate the last piece + // note that in the case of + // (cn=); this might be + // the string "" (!=null) + } + ss.setLength(0); + break loop; + } + + // Read the next character and account for escapes. + char c = value.charAt(idx++); + if (!escaped && ((c == '(') || (c == ')'))) + { + throw new IllegalArgumentException( + "Illegal value: " + value); + } + else if (!escaped && (c == '*')) + { + if (wasStar) + { + // encountered two successive stars; + // I assume this is illegal + throw new IllegalArgumentException("Invalid filter string: " + value); + } + if (ss.length() > 0) + { + pieces.add(ss.toString()); // accumulate the pieces + // between '*' occurrences + } + ss.setLength(0); + // if this is a leading star, then track it + if (pieces.isEmpty()) + { + leftstar = true; + } + wasStar = true; + } + else if (!escaped && (c == '\\')) + { + escaped = true; + } + else + { + escaped = false; + wasStar = false; + ss.append(c); + } + } + if (leftstar || rightstar || pieces.size() > 1) + { + // insert leading and/or trailing "" to anchor ends + if (rightstar) + { + pieces.add(""); + } + if (leftstar) + { + pieces.add(0, ""); + } + } + return pieces; + } + + public static String unparseSubstring(List pieces) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < pieces.size(); i++) + { + if (i > 0) + { + sb.append("*"); + } + sb.append(pieces.get(i)); + } + return sb.toString(); + } + + public static boolean compareSubstring(List pieces, String s) + { + // Walk the pieces to match the string + // There are implicit stars between each piece, + // and the first and last pieces might be "" to anchor the match. + // assert (pieces.length > 1) + // minimal case is * + + boolean result = true; + int len = pieces.size(); + + // Special case, if there is only one piece, then + // we must perform an equality test. + if (len == 1) + { + return s.equals(pieces.get(0)); + } + + // Otherwise, check whether the pieces match + // the specified string. + + int index = 0; + +loop: for (int i = 0; i < len; i++) + { + String piece = pieces.get(i); + + // If this is the first piece, then make sure the + // string starts with it. + if (i == 0) + { + if (!s.startsWith(piece)) + { + result = false; + break loop; + } + } + + // If this is the last piece, then make sure the + // string ends with it. + if (i == len - 1) + { + result = s.endsWith(piece); + break loop; + } + + // If this is neither the first or last piece, then + // make sure the string contains it. + if ((i > 0) && (i < (len - 1))) + { + index = s.indexOf(piece, index); + if (index < 0) + { + result = false; + break loop; + } + } + + // Move string index beyond the matching piece. + index += piece.length(); + } + + return result; + } + + /** + * Intepret a string as a URI relative to the current working directory. + * @param session the session (where the CWD is stored) + * @param relativeUri the input URI + * @return the resulting URI as a string + * @throws IOException + */ + public static String resolveUri(CommandSession session, String relativeUri) throws IOException + { + File cwd = (File) session.get(CWD); + if (cwd == null) + { + cwd = new File("").getCanonicalFile(); + session.put(CWD, cwd); + } + if ((relativeUri == null) || (relativeUri.length() == 0)) + { + return relativeUri; + } + + URI curUri = cwd.toURI(); + URI newUri = curUri.resolve(relativeUri); + return newUri.toString(); + } +} diff --git a/gogo/command/src/main/java/org/apache/felix/gogo/command/package-info.java b/gogo/command/src/main/java/org/apache/felix/gogo/command/package-info.java new file mode 100644 index 00000000000..f2fecb598bd --- /dev/null +++ b/gogo/command/src/main/java/org/apache/felix/gogo/command/package-info.java @@ -0,0 +1,32 @@ +/* + * 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. + */ + +@org.osgi.annotation.bundle.Capability( + namespace = "org.apache.felix.gogo", + name = "command.implementation", + version = "1.0.0" +) +@org.osgi.annotation.bundle.Requirement( + effective = "active", + namespace = "org.apache.felix.gogo", + name = "runtime.implementation", + version = "1.0.0" +) +package org.apache.felix.gogo.command; + diff --git a/gogo/command/src/test/java/org/apache/felix/gogo/command/UtilTest.java b/gogo/command/src/test/java/org/apache/felix/gogo/command/UtilTest.java new file mode 100644 index 00000000000..89008cf7448 --- /dev/null +++ b/gogo/command/src/test/java/org/apache/felix/gogo/command/UtilTest.java @@ -0,0 +1,49 @@ +/* + * 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.gogo.command; + +import org.apache.felix.service.command.CommandSession; +import org.junit.Test; + +import java.io.File; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * + */ +public class UtilTest { + + @Test + public void relativeUri() throws Exception { + CommandSession session = mock(CommandSession.class); + when(session.get(Util.CWD)).thenReturn(null); // start with no CWD + String u = Util.resolveUri(session, "three"); + assertEquals(new File(new File("").getAbsoluteFile(), "three").toURI().toString(), u); + + when(session.get(Util.CWD)).thenReturn(new File(".").getCanonicalFile()); + u = Util.resolveUri(session, "file:/a/b/c"); + assertEquals("file:/a/b/c", u); + u = Util.resolveUri(session, "three"); + assertEquals(new File("./three").getCanonicalFile().toURI().toString(), u); + } +} diff --git a/gogo/gogo-parent/LICENSE b/gogo/gogo-parent/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/gogo/gogo-parent/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/gogo/gogo-parent/NOTICE b/gogo/gogo-parent/NOTICE new file mode 100644 index 00000000000..a845fa51d41 --- /dev/null +++ b/gogo/gogo-parent/NOTICE @@ -0,0 +1,11 @@ +Apache Felix Gogo +Copyright 2014 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. diff --git a/gogo/gogo-parent/pom.xml b/gogo/gogo-parent/pom.xml new file mode 100644 index 00000000000..07334275ff8 --- /dev/null +++ b/gogo/gogo-parent/pom.xml @@ -0,0 +1,128 @@ + + + + + org.apache.felix + felix-parent + 5 + ../pom/pom.xml + + 4.0.0 + pom + Apache Felix Gogo + Apache Felix Gogo Subproject + gogo-parent + 6-SNAPSHOT + http://felix.apache.org/ + + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/gogo/gogo-parent/ + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/gogo/gogo-parent/ + https://svn.apache.org/repos/asf/felix/trunk/gogo/gogo-parent/ + + + + 7 + 1.7 + 1.7 + 4.1.0 + + + + + + org.osgi + org.osgi.namespace.service + 1.0.0 + provided + + + org.osgi + org.osgi.service.event + 1.3.1 + provided + + + org.osgi + org.osgi.service.log + 1.3.0 + provided + + + org.osgi + osgi.annotation + 7.0.0 + provided + + + org.osgi + osgi.core + 6.0.0 + provided + + + junit + junit + 4.12 + test + + + org.apache.felix + org.apache.felix.framework + 6.0.1 + runtime + + + org.easymock + easymock + 3.4 + test + + + org.mockito + mockito-core + 2.2.29 + test + + + + + + + + + org.apache.felix + maven-bundle-plugin + 4.1.0 + true + + NONE + + ${project.artifactId} + The Apache Software Foundation + {maven-resources},META-INF/LICENSE=LICENSE,META-INF/NOTICE=NOTICE,META-INF/DEPENDENCIES=DEPENDENCIES + <_versionpolicy>[$(version;==;$(@)),$(version;+;$(@))) + <_removeheaders>Private-Package,Ignore-Package,Include-Resource + + + + + + + diff --git a/gogo/itest-jline/DEPENDENCIES b/gogo/itest-jline/DEPENDENCIES new file mode 100644 index 00000000000..1a17a4eba6d --- /dev/null +++ b/gogo/itest-jline/DEPENDENCIES @@ -0,0 +1,23 @@ +Apache Felix Gogo ITest JLine +Copyright 2014 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 diff --git a/gogo/itest-jline/LICENSE b/gogo/itest-jline/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/gogo/itest-jline/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/gogo/itest-jline/NOTICE b/gogo/itest-jline/NOTICE new file mode 100644 index 00000000000..70b226773d3 --- /dev/null +++ b/gogo/itest-jline/NOTICE @@ -0,0 +1,11 @@ +Apache Felix Gogo ITest JLine +Copyright 2014 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. diff --git a/gogo/itest-jline/itest.bndrun b/gogo/itest-jline/itest.bndrun new file mode 100644 index 00000000000..42301b8f42d --- /dev/null +++ b/gogo/itest-jline/itest.bndrun @@ -0,0 +1,28 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#-runtrace: true +#-runvm: -Xdebug, "-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000" + +-standalone: true + +-runrequires: osgi.identity;filter:='(osgi.identity=org.apache.felix.gogo.itest-jline)' + +-resolve.effective: resolve, active +-runee: JavaSE-1.8 +-runfw: org.apache.felix.framework +-runbundles: \ + org.apache.felix.gogo.command;version='[1.1.0,1.1.1)',\ + org.apache.felix.gogo.itest-jline;version='[0.0.1,0.0.2)',\ + org.apache.felix.gogo.jline;version='[1.1.2,1.1.3)',\ + org.apache.felix.gogo.runtime;version='[1.1.2,1.1.3)',\ + org.jline;version='[3.7.0,3.7.1)' diff --git a/gogo/itest-jline/pom.xml b/gogo/itest-jline/pom.xml new file mode 100644 index 00000000000..c3cb4a7db31 --- /dev/null +++ b/gogo/itest-jline/pom.xml @@ -0,0 +1,118 @@ + + + + + 4.0.0 + + + org.apache.felix + gogo-parent + 6-SNAPSHOT + ../gogo-parent/pom.xml + + + org.apache.felix.gogo.itest-jline + bundle + 0.0.1-SNAPSHOT + Apache Felix Gogo ITest JLine + Some tests for Gogo JLine shell. + http://felix.apache.org/ + + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/gogo/itest-jline/ + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/gogo/itest-jline/ + https://svn.apache.org/repos/asf/felix/trunk/gogo/itest-jline/ + + + + true + true + true + + + + + org.osgi + osgi.annotation + + + org.apache.felix + org.apache.felix.gogo.command + + + org.apache.felix + org.apache.felix.gogo.jline + + + org.apache.felix + org.apache.felix.gogo.runtime + + + org.apache.felix + org.apache.felix.framework + + + + + + + org.apache.felix + org.apache.felix.gogo.bom + 1.0.3-SNAPSHOT + import + pom + + + + + + + + org.apache.felix + maven-bundle-plugin + + + + + + + + biz.aQute.bnd + bnd-resolver-maven-plugin + ${bnd.version} + + false + + itest.bndrun + + + + + resolve + + resolve + + verify + + + + + + + \ No newline at end of file diff --git a/gogo/itest-jline/src/main/java/org/apache/felix/gogo/itest/jline/package-info.java b/gogo/itest-jline/src/main/java/org/apache/felix/gogo/itest/jline/package-info.java new file mode 100644 index 00000000000..556b8612d86 --- /dev/null +++ b/gogo/itest-jline/src/main/java/org/apache/felix/gogo/itest/jline/package-info.java @@ -0,0 +1,25 @@ +/* + * 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. + */ + +@RequireGogo(JLINE) +package org.apache.felix.gogo.itest.jline; + +import static org.apache.felix.service.command.annotations.RequireGogo.JLINE; + +import org.apache.felix.service.command.annotations.RequireGogo; \ No newline at end of file diff --git a/gogo/itest-shell/DEPENDENCIES b/gogo/itest-shell/DEPENDENCIES new file mode 100644 index 00000000000..12528b391f4 --- /dev/null +++ b/gogo/itest-shell/DEPENDENCIES @@ -0,0 +1,23 @@ +Apache Felix Gogo ITest Shell +Copyright 2014 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 diff --git a/gogo/itest-shell/LICENSE b/gogo/itest-shell/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/gogo/itest-shell/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/gogo/itest-shell/NOTICE b/gogo/itest-shell/NOTICE new file mode 100644 index 00000000000..05875cef377 --- /dev/null +++ b/gogo/itest-shell/NOTICE @@ -0,0 +1,11 @@ +Apache Felix Gogo ITest Shell +Copyright 2014 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. diff --git a/gogo/itest-shell/itest.bndrun b/gogo/itest-shell/itest.bndrun new file mode 100644 index 00000000000..ed53d044a25 --- /dev/null +++ b/gogo/itest-shell/itest.bndrun @@ -0,0 +1,27 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#-runtrace: true +#-runvm: -Xdebug, "-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000" + +-standalone: true + +-runrequires: osgi.identity;filter:='(osgi.identity=org.apache.felix.gogo.itest-shell)' + +-resolve.effective: resolve, active +-runee: JavaSE-1.8 +-runfw: org.apache.felix.framework +-runbundles: \ + org.apache.felix.gogo.command;version='[1.1.0,1.1.1)',\ + org.apache.felix.gogo.itest-shell;version='[0.0.1,0.0.2)',\ + org.apache.felix.gogo.runtime;version='[1.1.2,1.1.3)',\ + org.apache.felix.gogo.shell;version='[1.1.2,1.1.3)' diff --git a/gogo/itest-shell/pom.xml b/gogo/itest-shell/pom.xml new file mode 100644 index 00000000000..5046d358bdb --- /dev/null +++ b/gogo/itest-shell/pom.xml @@ -0,0 +1,118 @@ + + + + + 4.0.0 + + + org.apache.felix + gogo-parent + 6-SNAPSHOT + ../gogo-parent/pom.xml + + + org.apache.felix.gogo.itest-shell + bundle + 0.0.1-SNAPSHOT + Apache Felix Gogo ITest Shell + Some tests for Gogo Shell. + http://felix.apache.org/ + + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/gogo/itest-shell/ + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/gogo/itest-shell/ + https://svn.apache.org/repos/asf/felix/trunk/gogo/itest-shell/ + + + + true + true + true + + + + + org.osgi + osgi.annotation + + + org.apache.felix + org.apache.felix.gogo.command + + + org.apache.felix + org.apache.felix.gogo.shell + + + org.apache.felix + org.apache.felix.gogo.runtime + + + org.apache.felix + org.apache.felix.framework + + + + + + + org.apache.felix + org.apache.felix.gogo.bom + 1.0.3-SNAPSHOT + import + pom + + + + + + + + org.apache.felix + maven-bundle-plugin + + + + + + + + biz.aQute.bnd + bnd-resolver-maven-plugin + ${bnd.version} + + false + + itest.bndrun + + + + + resolve + + resolve + + verify + + + + + + + \ No newline at end of file diff --git a/gogo/itest-shell/src/main/java/org/apache/felix/gogo/itest/shell/package-info.java b/gogo/itest-shell/src/main/java/org/apache/felix/gogo/itest/shell/package-info.java new file mode 100644 index 00000000000..6c2cfbf7709 --- /dev/null +++ b/gogo/itest-shell/src/main/java/org/apache/felix/gogo/itest/shell/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +@RequireGogo +package org.apache.felix.gogo.itest.shell; + +import org.apache.felix.service.command.annotations.RequireGogo; \ No newline at end of file diff --git a/gogo/jline/DEPENDENCIES b/gogo/jline/DEPENDENCIES new file mode 100644 index 00000000000..664b8ac354d --- /dev/null +++ b/gogo/jline/DEPENDENCIES @@ -0,0 +1,20 @@ +Apache Felix Gogo Shell +Copyright 2011 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +None. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 diff --git a/gogo/jline/LICENSE b/gogo/jline/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/gogo/jline/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/gogo/jline/NOTICE b/gogo/jline/NOTICE new file mode 100644 index 00000000000..4007bd35915 --- /dev/null +++ b/gogo/jline/NOTICE @@ -0,0 +1,6 @@ +Apache Felix Gogo Shell +Copyright 2011 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. diff --git a/gogo/jline/doc/changelog.txt b/gogo/jline/doc/changelog.txt new file mode 100644 index 00000000000..7aa475648dd --- /dev/null +++ b/gogo/jline/doc/changelog.txt @@ -0,0 +1,154 @@ +Changes from 1.1.0 to 1.1.2 +---------------------------- +Improvement + [FELIX-5970] - Add requirement & capabilities model so gogo can be resolved + [FELIX-5999] - cleanup compiler warnings + [FELIX-6003] - Add some resolver checks to make sure @RequireGogo annotation works for both jline and shell + [FELIX-6007] - create a gogo bom + +Changes from 1.0.12 to 1.1.0 +---------------------------- +New Feature + [FELIX-5833] - Support for completion of quoted arguments + [FELIX-5834] - Upgrade to JLine 3.7.0 + [FELIX-5835] - Upgrade to JDK 8 + [FELIX-5836] - Upgrade to OSGi r6 + [FELIX-5837] - [gogo][jline] Improve styling support + +Improvement + [FELIX-5857] - Provide a context classloader on the session to help with class loading + [FELIX-5869] - [goto][jline] Weird error if the script contains unicode characters + +Changes from 1.0.10 to 1.0.12 +----------------------------- +Improvement + [FELIX-5822] - [gogo][jline] Improve syntax of procedural functions + +Changes from 1.0.8 to 1.0.10 +---------------------------- +Bug + [FELIX-5635] - [gogo][jline] The "cd" command should not attempt to complete multiple directories + [FELIX-5714] - ArrayIndexOutOfBoundsException running history | grep + +Improvement + [FELIX-5651] - Disable Log history in Gogo console + [FELIX-5705] - Provide completion for SCR commands + +Changes from 1.0.6 to 1.0.8 +--------------------------- +Bug + [FELIX-5687] - Do not use the threaded streams when passing the input stream to a command + +Changes from 1.0.4 to 1.0.6 +--------------------------- +Bug + [FELIX-5600] - System streams are not correctly set when running a new shell + [FELIX-5629] - [gogo][jline] When a job is in the foreground, the shell should wait for its completion + [FELIX-5631] - [gogo][runtime] The parser indicates wrong repair string when parsing heredocs + +Improvement + [FELIX-5594] - [gogo][jline] Improve color support for ls/grep and syntax highlighting + [FELIX-5596] - Allow to configure the colors for the gogo grep command + [FELIX-5598] - [gogo][jline] Support the JLine ttop function if available + +Changes from 1.0.2 to 1.0.4 +--------------------------- +Bug + [FELIX-5463] - [gogo][jline] The Main class should add a default Function->FunctionInterface converter + [FELIX-5498] - [gogo][jline] The shell should display exception thrown from commands + [FELIX-5584] - [gogo][jline] Remove dependency on LineReaderImpl + +Changes from 1.0.0 to 1.0.2 +--------------------------- +Bug + [FELIX-5077] - Gogo shell prints out nasty error on shutdown + [FELIX-5388] - Strange Prompt characters in the eclipse console + [FELIX-5442] - [gogo][jline] The gosh_script can not resolve the motd file inside the jar + [FELIX-5447] - [gogo][jline] Command results should be printed by default + +Changes from 0.10.0 to 1.0.0 +---------------------------- +** Bug + * [FELIX-5342] - Division by zero in new gogo for jline3 + +** New Feature + * [FELIX-5272] - New gogo features + +** Task + * [FELIX-5378] - [gogo] Upgrade packages and bundle to 1.0.0 + +Changes from 0.8.0 to 0.10.0 +---------------------------- + +** Improvement + * Added gosh_profile work around for issue wit.3 API to 1.0.0 + ambiguity. + ** Bug + * [FELIX-5342] - Division by zero in new gogo for jline3 + +** New Feature + * [FELIX-5272] - New gogo features + +** Task + * [FELIX-5378] - [gogo] Upgrade packages and bundle to 1. + +Changes from 0.6.1 to 0.8.0 +--------------------------- + +** Bug + * [FELIX-2651] - [Gogo] MOTD formatting is broken under Windows + +** Improvement + * [FELIX-2661] - [Gogo] It should be easier to start Gogo shell + non-interactively + +** New Feature + * [FELIX-2767] - gogo telnet IP address + +Changes from 0.6.0 to 0.6.1 +--------------------------- + +** Bug + * [FELIX-2446] - [Gogo] The bundle context command is not used with a + scope in gosh_profile + * [FELIX-2477] - [gogo] shell procedural commands don't inherit closure + arguments + +** Improvement + * [FELIX-2445] - [Gogo] Default gosh_profile should be updated to use + system bundle to load java.lang.System + * [FELIX-2543] - [Gogo] Should avoid using System.getProperty() to get + configuration properties + +Gogo Shell 0.6.0 +---------------- + +** Bug + * [FELIX-1473] - [gogo] The syntax does not provide a way to call methods + on a string + * [FELIX-1474] - [gogo] result of commands is implicitly written to pipe + * [FELIX-1493] - [gogo] automatic expansion of $args in Closure stops + direct access to $args list + * [FELIX-2337] - [gogo] no way to access array[] elements produced by + assignment + * [FELIX-2375] - [gogo] when supplied args can't be coerced, the error + message prints the arg values, rather than their types + * [FELIX-2380] - [gogo] lock contention in piped writer when reader + doesn't read all input + +** Improvement + * [FELIX-1487] - Support for commands on multiple lines + * [FELIX-2328] - [gogo] tidy-up runtime to remove optional code etc + * [FELIX-2339] - [gogo] add support for running scripts + * [FELIX-2342] - [gogo] remove old felix command adaptor + +** New Feature + * [FELIX-2363] - [Gogo] Add annotations for creating commands with + optional and out-of-order arguments + +** Task + * [FELIX-1670] - [gogo] launcher bundle not required + * [FELIX-1889] - Gogo should depend on the official OSGi jars + * [FELIX-2334] - [Gogo] Use org.apache.felix as Maven groupId + * [FELIX-2367] - [Gogo] Use org.apache.felix namespace to avoid any + perceived legal issues diff --git a/gogo/jline/pom.xml b/gogo/jline/pom.xml new file mode 100644 index 00000000000..d6562b232f2 --- /dev/null +++ b/gogo/jline/pom.xml @@ -0,0 +1,111 @@ + + + + + org.apache.felix + gogo-parent + 6-SNAPSHOT + ../gogo-parent/pom.xml + + 4.0.0 + bundle + Apache Felix Gogo JLine Shell + org.apache.felix.gogo.jline + 1.1.5-SNAPSHOT + http://felix.apache.org/ + + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/gogo/jline/ + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/gogo/jline/ + https://svn.apache.org/repos/asf/felix/trunk/gogo/jline/ + + + + true + 8 + 1.8 + 1.8 + 4.1.0 + + + + + org.osgi + osgi.annotation + + + org.osgi + osgi.core + + + org.apache.felix + org.apache.felix.gogo.runtime + 1.1.0 + + + org.jline + jline + 3.7.0 + + + org.apache.sshd + sshd-core + 1.2.0 + true + test + + + junit + junit + + + org.mockito + mockito-core + + + + + + org.apache.felix + maven-bundle-plugin + + + + org.apache.felix.gogo.jline + + + !org.apache.felix.gogo.runtime.threadio, + org.jline*;version="[3.0,4)", + * + + + + + + org.apache.rat + apache-rat-plugin + + + src/main/resources/motd + + + + + + diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Activator.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Activator.java new file mode 100644 index 00000000000..8ec8bd868e2 --- /dev/null +++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Activator.java @@ -0,0 +1,202 @@ +/* + * 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.gogo.jline; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.felix.gogo.jline.Shell.Context; +import org.apache.felix.gogo.jline.SingleServiceTracker.SingleServiceListener; +import org.apache.felix.gogo.runtime.Token; +import org.apache.felix.gogo.runtime.Tokenizer; +import org.apache.felix.service.command.CommandProcessor; +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Converter; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.osgi.annotation.bundle.Header; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceRegistration; + +public class Activator implements BundleActivator, SingleServiceListener { + private final Set> regs = new HashSet<>(); + private BundleContext context; + private SingleServiceTracker commandProcessorTracker; + + private Runnable closer; + + public Activator() { + } + + public void start(BundleContext context) throws Exception { + this.context = context; + this.commandProcessorTracker = new SingleServiceTracker<>(context, CommandProcessor.class, this); + this.commandProcessorTracker.open(); + } + + public void stop(BundleContext context) { + Iterator> iterator = regs.iterator(); + while (iterator.hasNext()) { + ServiceRegistration reg = iterator.next(); + reg.unregister(); + iterator.remove(); + } + this.commandProcessorTracker.close(); + } + + @Override + public void serviceFound() { + try { + closer = startShell(context, commandProcessorTracker.getService()); + } catch (Exception e) { + // Ignore + } + } + + @Override + public void serviceLost() { + stopShell(); + } + + @Override + public void serviceReplaced() { + serviceLost(); + serviceFound(); + } + + private Runnable startShell(BundleContext context, CommandProcessor processor) throws Exception { + Dictionary dict = new Hashtable<>(); + dict.put(CommandProcessor.COMMAND_SCOPE, "gogo"); + + // register converters + regs.add(context.registerService(Converter.class.getName(), new Converters(context.getBundle(0).getBundleContext()), null)); + + // register commands + + dict.put(CommandProcessor.COMMAND_FUNCTION, Builtin.functions); + regs.add(context.registerService(Builtin.class.getName(), new Builtin(), dict)); + + dict.put(CommandProcessor.COMMAND_FUNCTION, Procedural.functions); + regs.add(context.registerService(Procedural.class.getName(), new Procedural(), dict)); + + dict.put(CommandProcessor.COMMAND_FUNCTION, Posix.functions); + regs.add(context.registerService(Posix.class.getName(), new Posix(processor), dict)); + + Shell shell = new Shell(new ShellContext(), processor); + dict.put(CommandProcessor.COMMAND_FUNCTION, Shell.functions); + regs.add(context.registerService(Shell.class.getName(), shell, dict)); + + Terminal terminal = TerminalBuilder.builder() + .name("gogo") + .system(true) + .nativeSignals(true) + .signalHandler(Terminal.SignalHandler.SIG_IGN) + .build(); + CommandSession session = processor.createSession(terminal.input(), terminal.output(), terminal.output()); + AtomicBoolean closing = new AtomicBoolean(); + + Thread thread = new Thread(() -> { + String errorMessage = "gogo: unable to create console"; + try { + session.put(Shell.VAR_TERMINAL, terminal); + try { + List args = new ArrayList<>(); + args.add("--login"); + String argstr = shell.getContext().getProperty("gosh.args"); + if (argstr != null) { + Tokenizer tokenizer = new Tokenizer(argstr); + Token token; + while ((token = tokenizer.next()) != null) { + args.add(token.toString()); + } + } + shell.gosh(session, args.toArray(new String[args.size()])); + } catch (Throwable e) { + Object loc = session.get(".location"); + if (null == loc || !loc.toString().contains(":")) { + loc = "gogo"; + } + errorMessage = loc.toString(); + throw e; + } + } catch (Throwable e) { + if (!closing.get()) { + System.err.println(errorMessage + e.getClass().getSimpleName() + ": " + e.getMessage()); + e.printStackTrace(); + } + } + }, "Gogo shell"); + // start shell on a separate thread... + thread.start(); + + return () -> { + closing.set(true); + shell.stop(); + try { + terminal.close(); + } catch (IOException e) { + // Ignore + } + try { + long t0 = System.currentTimeMillis(); + while (thread.isAlive()) { + thread.interrupt(); + thread.join(10); + if (System.currentTimeMillis() - t0 > 5000) { + System.err.println("!!! FAILED TO STOP EXECUTOR !!!"); + break; + } + } + } catch (InterruptedException e) { + // Restore administration... + Thread.currentThread().interrupt(); + } + }; + } + + private void stopShell() { + if (closer != null) { + closer.run(); + } + while (!regs.isEmpty()) { + ServiceRegistration reg = regs.iterator().next(); + regs.remove(reg); + reg.unregister(); + } + } + + private class ShellContext implements Context { + public String getProperty(String name) { + return context.getProperty(name); + } + + public void exit() throws Exception { + context.getBundle(0).stop(); + } + } +} \ No newline at end of file diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/BaseConverters.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/BaseConverters.java new file mode 100644 index 00000000000..4eb1149408f --- /dev/null +++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/BaseConverters.java @@ -0,0 +1,145 @@ +/* + * 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.gogo.jline; + +import org.apache.felix.service.command.Converter; +import org.apache.felix.service.command.Function; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.Collections; + +public class BaseConverters implements Converter { + + public Object convert(Class desiredType, final Object in) throws Exception { + if (desiredType == Class.class) { + try { + return Class.forName(in.toString()); + } catch (ClassNotFoundException e) { + return null; + } + } + + if (desiredType.isAssignableFrom(String.class) && in instanceof InputStream) { + return read(((InputStream) in)); + } + + if (in instanceof Function && isFunctional(desiredType)) { + return Proxy.newProxyInstance(desiredType.getClassLoader(), + new Class[]{desiredType}, new InvocationHandler() { + Function command = ((Function) in); + + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable { + if (isObjectMethod(method)) { + return method.invoke(command, args); + } else if (method.isDefault()) { + final Field field = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP"); + field.setAccessible(true); + final MethodHandles.Lookup lookup = (MethodHandles.Lookup) field.get(null); + return lookup + .unreflectSpecial(method, method.getDeclaringClass()) + .bindTo(proxy) + .invokeWithArguments(args); + } else { + return command.execute(null, + args != null ? Arrays.asList(args) : Collections.emptyList()); + } + } + }); + } + + return null; + } + + public CharSequence format(Object target, int level, Converter converter) + throws IOException { + if (level == INSPECT && target instanceof InputStream) { + return read(((InputStream) target)); + } + return null; + } + + private CharSequence read(InputStream in) throws IOException { + int c; + StringBuffer sb = new StringBuffer(); + while ((c = in.read()) > 0) { + if (c >= 32 && c <= 0x7F || c == '\n' || c == '\r') { + sb.append((char) c); + } else { + String s = Integer.toHexString(c).toUpperCase(); + sb.append("\\"); + if (s.length() < 1) { + sb.append(0); + } + sb.append(s); + } + } + return sb; + } + + public static boolean isFunctional(Class clazz) { + if (!clazz.isInterface()) { + return false; + } + int nb = 0; + for (Method method : clazz.getMethods()) { + if (method.isDefault() || isObjectMethod(method) || isStatic(method)) { + continue; + } + nb++; + } + return nb == 1; + } + + public static boolean isStatic(Method method) { + return (method.getModifiers() & Modifier.STATIC) == Modifier.STATIC; + } + + public static boolean isObjectMethod(Method method) { + switch (method.getName()) { + case "toString": + if (method.getParameterCount() == 0 && method.getReturnType() == String.class) { + return true; + } + break; + case "equals": + if (method.getParameterCount() == 1 + && method.getParameterTypes()[0] == Object.class + && method.getReturnType() == boolean.class) { + return true; + } + break; + case "hashCode": + if (method.getParameterCount() == 0 && method.getReturnType() == int.class) { + return true; + } + break; + } + return false; + } + +} diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Builtin.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Builtin.java new file mode 100644 index 00000000000..0e4190d639e --- /dev/null +++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Builtin.java @@ -0,0 +1,771 @@ +/* + * 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.gogo.jline; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.io.StringWriter; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.felix.service.command.Job; +import org.apache.felix.service.command.Process; +import org.apache.felix.gogo.runtime.CommandSessionImpl; +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Converter; +import org.apache.felix.service.command.Function; +import org.jline.builtins.Commands; +import org.jline.builtins.Completers.DirectoriesCompleter; +import org.jline.builtins.Completers.FilesCompleter; +import org.jline.builtins.Options; +import org.jline.reader.Candidate; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; +import org.jline.reader.Widget; +import org.jline.terminal.Terminal; + +import static org.apache.felix.gogo.jline.Shell.getCommands; + +/** + * gosh built-in commands. + */ +public class Builtin { + + static final String[] functions = { + "format", "getopt", "new", "set", "tac", "type", + "jobs", "fg", "bg", + "keymap", "setopt", "unsetopt", "complete", "history", "widget", + "__files", "__directories", "__usage_completion" + }; + + private static final String[] packages = {"java.lang", "java.io", "java.net", "java.util"}; + + private final static Set KEYWORDS = new HashSet<>( + Arrays.asList("abstract", "continue", "for", "new", "switch", + "assert", "default", "goto", "package", "synchronized", "boolean", "do", + "if", "private", "this", "break", "double", "implements", "protected", + "throw", "byte", "else", "import", "public", "throws", "case", "enum", + "instanceof", "return", "transient", "catch", "extends", "int", "short", + "try", "char", "final", "interface", "static", "void", "class", + "finally", "long", "strictfp", "volatile", "const", "float", "native", + "super", "while")); + + public CharSequence format(CommandSession session) { + return format(session, session.get("_")); // last result + } + + public CharSequence format(CommandSession session, Object arg) { + Process process = Process.Utils.current(); + CharSequence result = session.format(arg, Converter.INSPECT); + process.out().println(result); + return result; + } + + /** + * script access to Options. + */ + public Options getopt(List spec, Object[] args) { + String[] optSpec = new String[spec.size()]; + for (int i = 0; i < optSpec.length; ++i) { + optSpec[i] = spec.get(i).toString(); + } + return Options.compile(optSpec).parse(args); + } + + // FIXME: the "new" command should be provided by runtime, + // so it can leverage same argument coercion mechanism, used to invoke methods. + public Object _new(CommandSession session, Object name, Object[] argv) throws Exception { + Class clazz; + + if (name instanceof Class) { + clazz = (Class) name; + } else { + clazz = loadClass(session, name.toString()); + } + + for (Constructor c : clazz.getConstructors()) { + Class[] types = c.getParameterTypes(); + if (types.length != argv.length) { + continue; + } + + boolean match = true; + + Object[] transformed = argv.clone(); + for (int i = 0; i < transformed.length; ++i) { + try { + transformed[i] = session.convert(types[i], transformed[i]); + } catch (IllegalArgumentException e) { + match = false; + break; + } + } + + if (!match) { + continue; + } + + try { + return c.newInstance(transformed); + } catch (InvocationTargetException ite) { + Throwable cause = ite.getCause(); + if (cause instanceof Exception) { + throw (Exception) cause; + } + throw ite; + } + } + + throw new IllegalArgumentException("can't coerce " + Arrays.asList(argv) + + " to any of " + Arrays.asList(clazz.getConstructors())); + } + + private Class loadClass(CommandSession session, String name) throws ClassNotFoundException { + if (!name.contains(".")) { + for (String p : packages) { + String pkg = p + "." + name; + try { + return Class.forName(pkg, true, session.classLoader()); + } catch (ClassNotFoundException e) { + } + } + } + return Class.forName(name, true, session.classLoader()); + } + + public void set(CommandSession session, String[] argv) { + final String[] usage = { + "set - show session variables", + "Usage: set [OPTIONS] [PREFIX]", + " -? --help show help", + " -a --all show all variables, including those starting with .", + " -x set xtrace option", + " +x unset xtrace option", + "If PREFIX given, then only show variable(s) starting with PREFIX"}; + + Process process = Process.Utils.current(); + Options opt = Options.compile(usage).parse(argv); + + if (opt.isSet("help")) { + opt.usage(process.err()); + return; + } + + List args = opt.args(); + String prefix = (args.isEmpty() ? "" : args.get(0)); + + if (opt.isSet("x")) { + session.put("echo", true); + } else if ("+x".equals(prefix)) { + session.put("echo", null); + } else { + boolean all = opt.isSet("all"); + for (String key : new TreeSet<>(Shell.getVariables(session))) { + if (!key.startsWith(prefix)) + continue; + + if (key.startsWith(".") && !(all || prefix.length() > 0)) + continue; + + Object target = session.get(key); + String type = null; + String value = null; + + if (target != null) { + Class clazz = target.getClass(); + type = clazz.getSimpleName(); + value = target.toString(); + } + + String trunc = value == null || value.length() < 55 ? "" : "..."; + process.out().println(String.format("%-15.15s %-15s %.45s%s", type, key, + value, trunc)); + } + } + } + + /* + * the following methods depend on the internals of the runtime implementation. + * ideally, they should be available via some API. + */ + + public Object tac(CommandSession session, String[] argv) throws IOException { + final String[] usage = { + "tac - capture stdin as String or List and optionally write to file.", + "Usage: tac [-al] [FILE]", + " -a --append append to FILE", + " -l --list return List", + " -? --help show help"}; + + Process process = Process.Utils.current(); + Options opt = Options.compile(usage).parse(argv); + + if (opt.isSet("help")) { + opt.usage(process.err()); + return null; + } + + List args = opt.args(); + BufferedWriter fw = null; + + if (args.size() == 1) { + Path path = session.currentDir().resolve(args.get(0)); + Set options = new HashSet<>(); + options.add(StandardOpenOption.WRITE); + options.add(StandardOpenOption.CREATE); + if (opt.isSet("append")) { + options.add(StandardOpenOption.APPEND); + } else { + options.add(StandardOpenOption.TRUNCATE_EXISTING); + } + fw = Files.newBufferedWriter(path, StandardCharsets.UTF_8, options.toArray(new OpenOption[options.size()])); + } + + StringWriter sw = new StringWriter(); + BufferedReader rdr = new BufferedReader(new InputStreamReader(process.in())); + + ArrayList list = null; + + if (opt.isSet("list")) { + list = new ArrayList<>(); + } + + boolean first = true; + String s; + + while ((s = rdr.readLine()) != null) { + if (list != null) { + list.add(s); + } else { + if (!first) { + sw.write(' '); + } + first = false; + sw.write(s); + } + + if (fw != null) { + fw.write(s); + fw.newLine(); + } + } + + if (fw != null) { + fw.close(); + } + + return list != null ? list : sw.toString(); + } + + // FIXME: expose API in runtime so type command doesn't have to duplicate the runtime + // command search strategy. + public boolean type(CommandSession session, String[] argv) throws Exception { + final String[] usage = {"type - show command type", + "Usage: type [OPTIONS] [name[:]]", + " -a --all show all matches", + " -? --help show help", + " -q --quiet don't print anything, just return status", + " -s --scope=NAME list all commands in named scope", + " -t --types show full java type names"}; + + Process process = Process.Utils.current(); + Options opt = Options.compile(usage).parse(argv); + List args = opt.args(); + + if (opt.isSet("help")) { + opt.usage(process.err()); + return true; + } + + boolean all = opt.isSet("all"); + + String optScope = null; + if (opt.isSet("scope")) { + optScope = opt.get("scope"); + } + + if (args.size() == 1) { + String arg = args.get(0); + if (arg.endsWith(":")) { + optScope = args.remove(0); + } + } + + if (optScope != null || (args.isEmpty() && all)) { + Set snames = new TreeSet<>(); + + for (String sname : (getCommands(session))) { + if ((optScope == null) || sname.startsWith(optScope)) { + snames.add(sname); + } + } + + for (String sname : snames) { + process.out().println(sname); + } + + return true; + } + + if (args.size() == 0) { + Map scopes = new TreeMap<>(); + + for (String sname : getCommands(session)) { + int colon = sname.indexOf(':'); + String scope = sname.substring(0, colon); + Integer count = scopes.get(scope); + if (count == null) { + count = 0; + } + scopes.put(scope, ++count); + } + + for (Entry entry : scopes.entrySet()) { + process.out().println(entry.getKey() + ":" + entry.getValue()); + } + + return true; + } + + final String name = args.get(0).toLowerCase(); + + final int colon = name.indexOf(':'); + final String MAIN = "_main"; // FIXME: must match Reflective.java + + StringBuilder buf = new StringBuilder(); + Set cmds = new LinkedHashSet<>(); + + // get all commands + if ((colon != -1) || (session.get(name) != null)) { + cmds.add(name); + } else if (session.get(MAIN) != null) { + cmds.add(MAIN); + } else { + String path = session.get("SCOPE") != null ? session.get("SCOPE").toString() + : "*"; + + for (String s : path.split(":")) { + if (s.equals("*")) { + for (String sname : getCommands(session)) { + if (sname.endsWith(":" + name)) { + cmds.add(sname); + if (!all) { + break; + } + } + } + } else { + String sname = s + ":" + name; + if (session.get(sname) != null) { + cmds.add(sname); + if (!all) { + break; + } + } + } + } + } + + for (String key : cmds) { + Object target = session.get(key); + if (target == null) { + continue; + } + + CharSequence source = getClosureSource(session, key); + + if (source != null) { + buf.append(name); + buf.append(" is function {"); + buf.append(source); + buf.append("}"); + continue; + } + + for (Method m : getMethods(session, key)) { + StringBuilder params = new StringBuilder(); + + for (Class type : m.getParameterTypes()) { + if (params.length() > 0) { + params.append(", "); + } + params.append(type.getSimpleName()); + } + + String rtype = m.getReturnType().getSimpleName(); + + if (buf.length() > 0) { + buf.append("\n"); + } + + if (opt.isSet("types")) { + String cname = m.getDeclaringClass().getName(); + buf.append(String.format("%s %s.%s(%s)", rtype, cname, m.getName(), + params)); + } else { + buf.append(String.format("%s is %s %s(%s)", name, rtype, key, params)); + } + } + } + + if (buf.length() > 0) { + if (!opt.isSet("quiet")) { + process.out().println(buf); + } + return true; + } + + if (!opt.isSet("quiet")) { + process.err().println("type: " + name + " not found."); + } + + return false; + } + + public void jobs(CommandSession session, String[] argv) { + final String[] usage = { + "jobs - list jobs", + "Usage: jobs [OPTIONS]", + " -? --help show help", + }; + Process process = Process.Utils.current(); + Options opt = Options.compile(usage).parse(argv); + if (opt.isSet("help")) { + opt.usage(process.err()); + return; + } + if (!opt.args().isEmpty()) { + process.err().println("usage: jobs"); + process.error(2); + return; + } + List jobs = session.jobs(); + Job current = Job.Utils.current(); + for (Job job : jobs) { + if (job != current) { + process.out().println("[" + job.id() + "] " + job.status().toString().toLowerCase() + + " " + job.command()); + } + } + } + + public void fg(CommandSession session, String[] argv) { + final String[] usage = { + "fg - put job in foreground", + "Usage: fg [OPTIONS] [jobid]", + " -? --help show help", + }; + Process process = Process.Utils.current(); + Options opt = Options.compile(usage).parse(argv); + if (opt.isSet("help")) { + opt.usage(process.err()); + return; + } + if (opt.args().size() > 1) { + process.err().println("usage: fg [jobid]"); + process.error(2); + return; + } + List jobs = new ArrayList<>(session.jobs()); + Collections.reverse(jobs); + Job current = Job.Utils.current(); + if (argv.length == 0) { + Job job = jobs.stream().filter(j -> j != current) + .findFirst().orElse(null); + if (job != null) { + job.foreground(); + } else { + process.err().println("fg: no current job"); + process.error(1); + } + } else { + Job job = jobs.stream().filter(j -> j != current && argv[0].equals(Integer.toString(j.id()))) + .findFirst().orElse(null); + if (job != null) { + job.foreground(); + } else { + process.err().println("fg: job not found: " + argv[0]); + process.error(1); + } + } + } + + public void bg(CommandSession session, String[] argv) { + final String[] usage = { + "bg - put job in background", + "Usage: bg [OPTIONS] [jobid]", + " -? --help show help", + }; + Process process = Process.Utils.current(); + Options opt = Options.compile(usage).parse(argv); + if (opt.isSet("help")) { + opt.usage(process.err()); + return; + } + if (opt.args().size() > 1) { + process.err().println("usage: bg [jobid]"); + process.error(2); + return; + } + List jobs = new ArrayList<>(session.jobs()); + Collections.reverse(jobs); + Job current = Job.Utils.current(); + if (argv.length == 0) { + Job job = jobs.stream().filter(j -> j != current) + .findFirst().orElse(null); + if (job != null) { + job.background(); + } else { + process.err().println("bg: no current job"); + process.error(1); + } + } else { + Job job = jobs.stream().filter(j -> j != current && argv[0].equals(Integer.toString(j.id()))) + .findFirst().orElse(null); + if (job != null) { + job.background(); + } else { + process.err().println("bg: job not found: " + argv[0]); + process.error(1); + } + } + } + + private boolean isClosure(Object target) { + return target.getClass().getSimpleName().equals("Closure"); + } + + private boolean isCommand(Object target) { + return target.getClass().getSimpleName().equals("CommandProxy"); + } + + private CharSequence getClosureSource(CommandSession session, String name) + throws Exception { + Object target = session.get(name); + + if (target == null) { + return null; + } + + if (!isClosure(target)) { + return null; + } + + Field sourceField = target.getClass().getDeclaredField("source"); + sourceField.setAccessible(true); + return (CharSequence) sourceField.get(target); + } + + private List getMethods(CommandSession session, String scmd) throws Exception { + final int colon = scmd.indexOf(':'); + final String function = colon == -1 ? scmd : scmd.substring(colon + 1); + final String name = KEYWORDS.contains(function) ? ("_" + function) : function; + final String get = "get" + function; + final String is = "is" + function; + final String set = "set" + function; + final String MAIN = "_main"; // FIXME: must match Reflective.java + + Object target = session.get(scmd); + if (target == null) { + return null; + } + + if (isClosure(target)) { + return null; + } + + if (isCommand(target)) { + Method method = target.getClass().getMethod("getTarget", (Class[]) null); + method.setAccessible(true); + target = method.invoke(target, (Object[]) null); + } + + ArrayList list = new ArrayList<>(); + Class tc = (target instanceof Class) ? (Class) target + : target.getClass(); + Method[] methods = tc.getMethods(); + + for (Method m : methods) { + String mname = m.getName().toLowerCase(); + + if (mname.equals(name) || mname.equals(get) || mname.equals(set) + || mname.equals(is) || mname.equals(MAIN)) { + list.add(m); + } + } + + return list; + } + + public void history(CommandSession session, String[] argv) throws IOException { + Process process = Process.Utils.current(); + Commands.history(Shell.getReader(session), process.out(), process.err(), argv); + } + + public void complete(CommandSession session, String[] argv) { + Process process = Process.Utils.current(); + Commands.complete(Shell.getReader(session), process.out(), process.err(), Shell.getCompletions(session), argv); + } + + public void widget(final CommandSession session, String[] argv) throws Exception { + java.util.function.Function creator = func -> () -> { + try { + session.execute(func); + } catch (Exception e) { + // TODO: log exception ? + return false; + } + return true; + }; + Process process = Process.Utils.current(); + Commands.widget(Shell.getReader(session), process.out(), process.err(), creator, argv); + } + + public void keymap(CommandSession session, String[] argv) { + Process process = Process.Utils.current(); + Commands.keymap(Shell.getReader(session), process.out(), process.err(), argv); + } + + public void setopt(CommandSession session, String[] argv) { + Process process = Process.Utils.current(); + Commands.setopt(Shell.getReader(session), process.out(), process.err(), argv); + } + + public void unsetopt(CommandSession session, String[] argv) { + Process process = Process.Utils.current(); + Commands.unsetopt(Shell.getReader(session), process.out(), process.err(), argv); + } + + public List __files(CommandSession session) { + ParsedLine line = Shell.getParsedLine(session); + LineReader reader = Shell.getReader(session); + List candidates = new ArrayList<>(); + new FilesCompleter(session.currentDir()) { + @Override + protected String getDisplay(Terminal terminal, Path p) { + return getFileDisplay(session, p); + } + }.complete(reader, line, candidates); + return candidates; + } + + public List __directories(CommandSession session) { + ParsedLine line = Shell.getParsedLine(session); + LineReader reader = Shell.getReader(session); + List candidates = new ArrayList<>(); + new DirectoriesCompleter(session.currentDir()) { + @Override + protected String getDisplay(Terminal terminal, Path p) { + return getFileDisplay(session, p); + } + }.complete(reader, line, candidates); + return candidates; + } + + private String getFileDisplay(CommandSession session, Path path) { + String type; + String suffix; + if (Files.isSymbolicLink(path)) { + type = "sl"; + suffix = "@"; + } else if (Files.isDirectory(path)) { + type = "dr"; + suffix = "/"; + } else if (Files.isExecutable(path)) { + type = "ex"; + suffix = "*"; + } else if (!Files.isRegularFile(path)) { + type = "ot"; + suffix = ""; + } else { + type = ""; + suffix = ""; + } + return Posix.applyStyle(path.getFileName().toString(), Posix.getLsColorMap(session), type) + suffix; + + } + + public void __usage_completion(CommandSession session, String command) throws Exception { + Object func = session.get(command.contains(":") ? command : "*:" + command); + if (func instanceof Function) { + ByteArrayInputStream bais = new ByteArrayInputStream(new byte[0]); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ByteArrayOutputStream baes = new ByteArrayOutputStream(); + CommandSession ts = ((CommandSessionImpl) session).processor().createSession(bais, new PrintStream(baos), new PrintStream(baes)); + ts.execute(command + " --help"); + + String regex = "(?x)\\s*" + "(?:-([^-]))?" + // 1: short-opt-1 + "(?:,?\\s*-(\\w))?" + // 2: short-opt-2 + "(?:,?\\s*--(\\w[\\w-]*)(=\\w+)?)?" + // 3: long-opt-1 and 4:arg-1 + "(?:,?\\s*--(\\w[\\w-]*))?" + // 5: long-opt-2 + ".*?(?:\\(default=(.*)\\))?\\s*" + // 6: default + "(.*)"; // 7: description + Pattern pattern = Pattern.compile(regex); + for (String l : baes.toString().split("\n")) { + Matcher matcher = pattern.matcher(l); + if (matcher.matches()) { + List args = new ArrayList<>(); + if (matcher.group(1) != null) { + args.add("--short-option"); + args.add(matcher.group(1)); + } + if (matcher.group(3) != null) { + args.add("--long-option"); + args.add(matcher.group(1)); + } + if (matcher.group(4) != null) { + args.add("--argument"); + args.add(""); + } + if (matcher.group(7) != null) { + args.add("--description"); + args.add(matcher.group(7)); + } + complete(session, args.toArray(new String[args.size()])); + } + } + } + } + +} diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Converters.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Converters.java new file mode 100644 index 00000000000..0ed4d8974ea --- /dev/null +++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Converters.java @@ -0,0 +1,180 @@ +/* + * 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.gogo.jline; + +import java.io.IOException; +import java.util.Formatter; + +import org.apache.felix.service.command.Converter; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.startlevel.BundleStartLevel; + +public class Converters extends BaseConverters { + private final BundleContext context; + + public Converters(BundleContext context) { + this.context = context; + } + + private CharSequence print(Bundle bundle) { + // [ ID ] [STATE ] [ SL ] symname + int level = bundle.adapt(BundleStartLevel.class).getStartLevel(); + + return String.format("%5d|%-11s|%5d|%s (%s)", bundle.getBundleId(), + getState(bundle), level, bundle.getSymbolicName(), bundle.getVersion()); + } + + private CharSequence print(ServiceReference ref) { + StringBuilder sb = new StringBuilder(); + try (Formatter f = new Formatter(sb);) + { + String spid = ""; + Object pid = ref.getProperty("service.pid"); + if (pid != null) { + spid = pid.toString(); + } + + f.format("%06d %3s %-40s %s", ref.getProperty("service.id"), + ref.getBundle().getBundleId(), + getShortNames((String[]) ref.getProperty("objectclass")), spid); + return sb; + } + } + + private CharSequence getShortNames(String[] list) { + StringBuilder sb = new StringBuilder(); + String del = ""; + for (String s : list) { + sb.append(del).append(getShortName(s)); + del = " | "; + } + return sb; + } + + private CharSequence getShortName(String name) { + int n = name.lastIndexOf('.'); + if (n < 0) { + n = 0; + } else { + n++; + } + return name.subSequence(n, name.length()); + } + + private String getState(Bundle bundle) { + switch (bundle.getState()) { + case Bundle.ACTIVE: + return "Active"; + + case Bundle.INSTALLED: + return "Installed"; + + case Bundle.RESOLVED: + return "Resolved"; + + case Bundle.STARTING: + return "Starting"; + + case Bundle.STOPPING: + return "Stopping"; + + case Bundle.UNINSTALLED: + return "Uninstalled "; + } + return null; + } + + public Bundle bundle(Bundle i) { + return i; + } + + public Object convert(Class desiredType, final Object in) throws Exception { + if (desiredType == Bundle.class) { + return convertBundle(in); + } + + if (desiredType == ServiceReference.class) { + return convertServiceReference(in); + } + + return super.convert(desiredType, in); + } + + private Object convertServiceReference(Object in) throws InvalidSyntaxException { + String s = in.toString(); + if (s.startsWith("(") && s.endsWith(")")) { + ServiceReference refs[] = context.getServiceReferences((String) null, String.format( + "(|(service.id=%s)(service.pid=%s))", in, in)); + if (refs != null && refs.length > 0) { + return refs[0]; + } + } + + ServiceReference refs[] = context.getServiceReferences((String) null, String.format( + "(|(service.id=%s)(service.pid=%s))", in, in)); + if (refs != null && refs.length > 0) { + return refs[0]; + } + return null; + } + + private Object convertBundle(Object in) { + String s = in.toString(); + try { + long id = Long.parseLong(s); + return context.getBundle(id); + } catch (NumberFormatException nfe) { + // Ignore + } + + Bundle bundles[] = context.getBundles(); + for (Bundle b : bundles) { + if (b.getLocation().equals(s)) { + return b; + } + + if (b.getSymbolicName().equals(s)) { + return b; + } + } + + return null; + } + + public CharSequence format(Object target, int level, Converter converter) + throws IOException { + if (level == LINE && target instanceof Bundle) { + return print((Bundle) target); + } + if (level == LINE && target instanceof ServiceReference) { + return print((ServiceReference) target); + } + if (level == PART && target instanceof Bundle) { + return ((Bundle) target).getSymbolicName(); + } + if (level == PART && target instanceof ServiceReference) { + return getShortNames((String[]) ((ServiceReference) target).getProperty("objectclass")); + } + return super.format(target, level, converter); + } + +} diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Expander.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Expander.java new file mode 100644 index 00000000000..e9a8d2d1745 --- /dev/null +++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Expander.java @@ -0,0 +1,58 @@ +/* + * 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.gogo.jline; + +import java.util.Collection; +import java.util.stream.Collectors; + +import org.apache.felix.gogo.runtime.Closure; +import org.apache.felix.gogo.runtime.CommandSessionImpl; +import org.apache.felix.service.command.CommandSession; +import org.jline.reader.impl.DefaultExpander; + +public class Expander extends DefaultExpander { + + private final CommandSession session; + + public Expander(CommandSession session) { + this.session = session; + } + + @Override + @SuppressWarnings("unchecked") + public String expandVar(String word) { + try { + Object o = org.apache.felix.gogo.runtime.Expander.expand( + word, + new Closure((CommandSessionImpl) session, null, null)); + if (o instanceof Collection) { + return ((Collection) o).stream() + .map(String::valueOf) + .collect(Collectors.joining(" ")); + } + else if (o != null) { + return o.toString(); + } + } catch (Exception e) { + // ignore + } + return word; + } + +} diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Highlighter.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Highlighter.java new file mode 100644 index 00000000000..d0156eda28d --- /dev/null +++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Highlighter.java @@ -0,0 +1,226 @@ +/* + * 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.gogo.jline; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.felix.gogo.runtime.CommandSessionImpl; +import org.apache.felix.gogo.runtime.EOFError; +import org.apache.felix.gogo.runtime.Parser.Program; +import org.apache.felix.gogo.runtime.Parser.Statement; +import org.apache.felix.gogo.runtime.SyntaxError; +import org.apache.felix.gogo.runtime.Token; +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Function; +import org.jline.reader.LineReader; +import org.jline.reader.LineReader.RegionType; +import org.jline.reader.impl.DefaultHighlighter; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.AttributedStyle; +import org.jline.utils.WCWidth; + +public class Highlighter extends DefaultHighlighter { + + public static final String DEFAULT_HIGHLIGHTER_COLORS = "rs=35:st=32:nu=32:co=32:va=36:vn=36:fu=94:bf=91:re=90"; + + private final CommandSession session; + + public Highlighter(CommandSession session) { + this.session = session; + } + + public AttributedString highlight(LineReader reader, String buffer) { + try { + Program program = null; + List tokens = null; + List statements = null; + String repaired = buffer; + while (program == null) { + try { + org.apache.felix.gogo.runtime.Parser parser = new org.apache.felix.gogo.runtime.Parser(repaired); + program = parser.program(); + tokens = parser.tokens(); + statements = parser.statements(); + } catch (EOFError e) { + repaired = repaired + " " + e.repair(); + // Make sure we don't loop forever + if (repaired.length() > buffer.length() + 1024) { + return new AttributedStringBuilder().append(buffer).toAttributedString(); + } + } + } + + Map colors = Posix.getColorMap(session, "HIGHLIGHTER", DEFAULT_HIGHLIGHTER_COLORS); + + int underlineStart = -1; + int underlineEnd = -1; + int negativeStart = -1; + int negativeEnd = -1; + String search = reader.getSearchTerm(); + if (search != null && search.length() > 0) { + underlineStart = buffer.indexOf(search); + if (underlineStart >= 0) { + underlineEnd = underlineStart + search.length() - 1; + } + } + if (reader.getRegionActive() != RegionType.NONE) { + negativeStart = reader.getRegionMark(); + negativeEnd = reader.getBuffer().cursor(); + if (negativeStart > negativeEnd) { + int x = negativeEnd; + negativeEnd = negativeStart; + negativeStart = x; + } + if (reader.getRegionActive() == RegionType.LINE) { + while (negativeStart > 0 && reader.getBuffer().atChar(negativeStart - 1) != '\n') { + negativeStart--; + } + while (negativeEnd < reader.getBuffer().length() - 1 && reader.getBuffer().atChar(negativeEnd + 1) != '\n') { + negativeEnd++; + } + } + } + + Type[] types = new Type[repaired.length()]; + + Arrays.fill(types, Type.Unknown); + + int cur = 0; + for (Token token : tokens) { + // We're on the repair side, so exit now + if (token.start() >= buffer.length()) { + break; + } + if (token.start() > cur) { + cur = token.start(); + } + // Find corresponding statement + Statement statement = null; + for (int i = statements.size() - 1; i >= 0; i--) { + Statement s = statements.get(i); + if (s.start() <= cur && cur < s.start() + s.length()) { + statement = s; + break; + } + } + + // Reserved tokens + Type type = Type.Unknown; + if (Token.eq(token, "{") + || Token.eq(token, "}") + || Token.eq(token, "(") + || Token.eq(token, ")") + || Token.eq(token, "[") + || Token.eq(token, "]") + || Token.eq(token, "|") + || Token.eq(token, ";") + || Token.eq(token, "=")) { + type = Type.Reserved; + } else if (token.charAt(0) == '\'' || token.charAt(0) == '"') { + type = Type.String; + } else if (token.toString().matches("^[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?$")) { + type = Type.Number; + } else if (token.charAt(0) == '$') { + type = Type.Variable; + } else if (((Set) session.get(CommandSessionImpl.CONSTANTS)).contains(token.toString()) + || Token.eq(token, "null") || Token.eq(token, "false") || Token.eq(token, "true")) { + type = Type.Constant; + } else { + boolean isFirst = statement != null && statement.tokens().size() > 0 + && token == statement.tokens().get(0); + boolean isThirdWithNext = statement != null && statement.tokens().size() > 3 + && token == statement.tokens().get(2); + boolean isAssign = statement != null && statement.tokens().size() > 1 + && Token.eq(statement.tokens().get(1), "="); + if (isFirst && isAssign) { + type = Type.VariableName; + } + if (isFirst && !isAssign || isAssign && isThirdWithNext) { + Object v = session.get(Shell.resolve(session, token.toString())); + type = (v instanceof Function) ? Type.Function : Type.BadFunction; + } + } + Arrays.fill(types, token.start(), Math.min(token.start() + token.length(), types.length), type); + cur = Math.min(token.start() + token.length(), buffer.length()); + } + + if (buffer.length() < repaired.length()) { + Arrays.fill(types, buffer.length(), repaired.length(), Type.Repair); + } + + AttributedStringBuilder sb = new AttributedStringBuilder(); + for (int i = 0; i < repaired.length(); i++) { + sb.style(AttributedStyle.DEFAULT); + applyStyle(sb, colors, types[i]); + if (i >= underlineStart && i <= underlineEnd) { + sb.style(sb.style().underline()); + } + if (i >= negativeStart && i <= negativeEnd) { + sb.style(sb.style().inverse()); + } + char c = repaired.charAt(i); + if (c == '\t' || c == '\n') { + sb.append(c); + } else if (c < 32) { + sb.style(sb.style().inverseNeg()) + .append('^') + .append((char) (c + '@')) + .style(sb.style().inverseNeg()); + } else { + int w = WCWidth.wcwidth(c); + if (w > 0) { + sb.append(c); + } + } + } + + return sb.toAttributedString(); + } catch (SyntaxError e) { + return super.highlight(reader, buffer); + } + } + + private void applyStyle(AttributedStringBuilder sb, Map colors, Type type) { + Posix.applyStyle(sb, colors, type.color); + } + + enum Type { + Reserved("rs"), + String("st"), + Number("nu"), + Variable("va"), + VariableName("vn"), + Function("fu"), + BadFunction("bf"), + Constant("co"), + Unknown("un"), + Repair("re"); + + private final String color; + + Type(String color) { + this.color = color; + } + } + +} diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Main.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Main.java new file mode 100644 index 00000000000..4fcaeccc771 --- /dev/null +++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Main.java @@ -0,0 +1,108 @@ +/* + * 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.gogo.jline; + +import java.io.FilterInputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.felix.gogo.jline.Shell.Context; +import org.apache.felix.gogo.runtime.CommandProcessorImpl; +import org.apache.felix.gogo.runtime.threadio.ThreadIOImpl; +import org.apache.felix.service.command.CommandSession; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; + +public class Main { + + public static void main(String[] args) throws IOException { + try (Terminal terminal = TerminalBuilder.builder() + .name("gogo") + .system(true) + .nativeSignals(true) + .signalHandler(Terminal.SignalHandler.SIG_IGN) + .build()) { + ThreadIOImpl tio = new ThreadIOImpl(); + tio.start(); + try { + CommandProcessorImpl processor = new CommandProcessorImpl(tio); + Context context = new MyContext(); + Shell shell = new Shell(context, processor, tio, null); + processor.addCommand("gogo", processor, "addCommand"); + processor.addCommand("gogo", processor, "removeCommand"); + processor.addCommand("gogo", processor, "eval"); + processor.addConverter(new BaseConverters()); + register(processor, new Builtin(), Builtin.functions); + register(processor, new Procedural(), Procedural.functions); + register(processor, new Posix(processor), Posix.functions); + register(processor, shell, Shell.functions); + InputStream in = new FilterInputStream(terminal.input()) { + @Override + public void close() { + } + }; + OutputStream out = new FilterOutputStream(terminal.output()) { + @Override + public void close() { + } + }; + CommandSession session = processor.createSession(in, out, out); + session.put(Shell.VAR_CONTEXT, context); + session.put(Shell.VAR_TERMINAL, terminal); + try { + String[] argv = new String[args.length + 1]; + argv[0] = "--login"; + System.arraycopy(args, 0, argv, 1, args.length); + shell.gosh(session, argv); + } catch (Exception e) { + Object loc = session.get(".location"); + if (null == loc || !loc.toString().contains(":")) { + loc = "gogo"; + } + + System.err.println(loc + ": " + e.getClass().getSimpleName() + ": " + e.getMessage()); + e.printStackTrace(); + } finally { + session.close(); + } + } finally { + tio.stop(); + } + } + } + + static void register(CommandProcessorImpl processor, Object target, String[] functions) { + for (String function : functions) { + processor.addCommand("gogo", target, function); + } + } + + private static class MyContext implements Context { + + public String getProperty(String name) { + return System.getProperty(name); + } + + public void exit() { + System.exit(0); + } + } +} diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/ParsedLineImpl.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/ParsedLineImpl.java new file mode 100644 index 00000000000..25b527019da --- /dev/null +++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/ParsedLineImpl.java @@ -0,0 +1,202 @@ +/* + * 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.gogo.jline; + +import org.apache.felix.gogo.runtime.Parser.Program; +import org.apache.felix.gogo.runtime.Token; +import org.jline.reader.CompletingParsedLine; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +public class ParsedLineImpl implements CompletingParsedLine { + + private final Program program; + private final String source; + private final int cursor; + private final List tokens; + private final int wordIndex; + private final int wordCursor; + private final CharSequence rawWord; + private final int rawWordCursor; + + public ParsedLineImpl(Program program, Token line, int cursor, List tokens) { + this.program = program; + this.source = line.toString(); + this.cursor = cursor - line.start(); + this.tokens = new ArrayList<>(); + for (Token token : tokens) { + this.tokens.add(unquote(token, null).toString()); + } + int wi = tokens.size(); + int wc = 0; + if (cursor >= 0) { + for (int i = 0; i < tokens.size(); i++) { + Token t = tokens.get(i); + if (t.start() > cursor) { + wi = i; + wc = 0; + this.tokens.add(i, ""); + break; + } + if (t.start() + t.length() >= cursor) { + wi = i; + wc = cursor - t.start(); + break; + } + } + } + if (wi == tokens.size()) { + this.tokens.add(""); + rawWord = ""; + wordCursor = 0; + } else { + rawWord = tokens.get(wi); + int[] c = new int[] { wc }; + unquote(rawWord, c); + wordCursor = c[0]; + } + wordIndex = wi; + rawWordCursor = wc; + } + + public String word() { + return tokens.get(wordIndex()); + } + + public int wordCursor() { + return wordCursor; + } + + public int wordIndex() { + return wordIndex; + } + + public List words() { + return tokens; + } + + public String line() { + return source; + } + + public int cursor() { + return cursor; + } + + public Program program() { + return program; + } + + public int rawWordCursor() { + return rawWordCursor; + } + + public int rawWordLength() { + return rawWord.length(); + } + + public CharSequence escape(CharSequence str, boolean complete) { + StringBuilder sb = new StringBuilder(str); + Predicate needToBeEscaped; + char quote = 0; + char first = rawWord.length() > 0 ? rawWord.charAt(0) : 0; + if (first == '\'') { + quote = '\''; + needToBeEscaped = i -> i == '\''; + } else if (first == '"') { + quote = '"'; + needToBeEscaped = i -> i == '"'; + } else { + needToBeEscaped = i -> i == ' ' || i == '\t'; + } + for (int i = 0; i < sb.length(); i++) { + if (needToBeEscaped.test(str.charAt(i))) { + sb.insert(i++, '\\'); + } + } + if (quote != 0) { + sb.insert(0, quote); + if (complete) { + sb.append(quote); + } + } + return sb; + } + + private CharSequence unquote(CharSequence arg, int[] cursor) { + boolean hasEscape = false; + for (int i = 0; i < arg.length(); i++) { + int c = arg.charAt(i); + if (c == '\\' || c == '"' || c == '\'') { + hasEscape = true; + break; + } + } + if (!hasEscape) { + return arg; + } + boolean singleQuoted = false; + boolean doubleQuoted = false; + boolean escaped = false; + StringBuilder buf = new StringBuilder(arg.length()); + for (int i = 0; i < arg.length(); i++) { + if (cursor != null && cursor[0] == i) { + cursor[0] = buf.length(); + cursor = null; + } + char c = arg.charAt(i); + if (doubleQuoted && escaped) { + if (c != '"' && c != '\\' && c != '$' && c != '%') { + buf.append('\\'); + } + buf.append(c); + escaped = false; + } else if (escaped) { + buf.append(c); + escaped = false; + } else if (singleQuoted) { + if (c == '\'') { + singleQuoted = false; + } else { + buf.append(c); + } + } else if (doubleQuoted) { + if (c == '\\') { + escaped = true; + } else if (c == '\"') { + doubleQuoted = false; + } else { + buf.append(c); + } + } else if (c == '\\') { + escaped = true; + } else if (c == '\'') { + singleQuoted = true; + } else if (c == '"') { + doubleQuoted = true; + } else { + buf.append(c); + } + } + return buf.toString(); + } + +} diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Parser.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Parser.java new file mode 100644 index 00000000000..d307b5a7b28 --- /dev/null +++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Parser.java @@ -0,0 +1,93 @@ +/* + * 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.gogo.jline; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.felix.gogo.runtime.EOFError; +import org.apache.felix.gogo.runtime.Parser.Program; +import org.apache.felix.gogo.runtime.Parser.Statement; +import org.apache.felix.gogo.runtime.SyntaxError; +import org.apache.felix.gogo.runtime.Token; +import org.jline.reader.ParsedLine; + +public class Parser implements org.jline.reader.Parser { + + public ParsedLine parse(String line, int cursor, ParseContext context) throws org.jline.reader.SyntaxError { + try { + return doParse(line, cursor, context); + } catch (EOFError e) { + throw new org.jline.reader.EOFError(e.line(), e.column(), e.getMessage(), e.missing()); + } catch (SyntaxError e) { + throw new org.jline.reader.SyntaxError(e.line(), e.column(), e.getMessage()); + } + } + + private ParsedLine doParse(String line, int cursor, ParseContext parseContext) throws SyntaxError { + Program program = null; + List statements = null; + String repaired = line; + while (program == null) { + try { + org.apache.felix.gogo.runtime.Parser parser = new org.apache.felix.gogo.runtime.Parser(repaired); + program = parser.program(); + statements = parser.statements(); + } catch (EOFError e) { + // Make sure we don't loop forever + if (parseContext == ParseContext.COMPLETE && repaired.length() < line.length() + 1024) { + repaired = repaired + " " + e.repair(); + } else { + throw e; + } + } + } + // Find corresponding statement + Statement statement = null; + for (int i = statements.size() - 1; i >= 0; i--) { + Statement s = statements.get(i); + if (s.start() <= cursor) { + boolean isOk = true; + // check if there are only spaces after the previous statement + if (s.start() + s.length() < cursor) { + for (int j = s.start() + s.length(); isOk && j < cursor; j++) { + isOk = Character.isWhitespace(line.charAt(j)); + } + } + statement = s; + break; + } + } + if (statement != null && statement.tokens() != null && !statement.tokens().isEmpty()) { + if (repaired != line) { + Token stmt = statement.subSequence(0, line.length() - statement.start()); + List tokens = new ArrayList<>(statement.tokens()); + Token last = tokens.get(tokens.size() - 1); + tokens.set(tokens.size() - 1, last.subSequence(0, line.length() - last.start())); + return new ParsedLineImpl(program, stmt, cursor, tokens); + } + return new ParsedLineImpl(program, statement, cursor, statement.tokens()); + } else { + // TODO: + return new ParsedLineImpl(program, program, cursor, Collections.singletonList(program)); + } + } + +} diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Posix.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Posix.java new file mode 100644 index 00000000000..ed170d1a434 --- /dev/null +++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Posix.java @@ -0,0 +1,2126 @@ +/* + * 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.gogo.jline; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.IntBinaryOperator; +import java.util.function.IntConsumer; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.felix.service.command.Process; +import org.apache.felix.gogo.jline.Shell.Context; +import org.apache.felix.service.command.CommandProcessor; +import org.apache.felix.service.command.CommandSession; +import org.jline.builtins.Commands; +import org.jline.builtins.Less; +import org.jline.builtins.Nano; +import org.jline.builtins.Options; +import org.jline.builtins.Source; +import org.jline.builtins.Source.PathSource; +import org.jline.builtins.TTop; +import org.jline.terminal.Attributes; +import org.jline.terminal.Terminal; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.InfoCmp.Capability; +import org.jline.utils.OSUtils; +import org.jline.utils.StyleResolver; + +/** + * Posix-like utilities. + * + * @see + * http://www.opengroup.org/onlinepubs/009695399/utilities/contents.html + */ +public class Posix { + + static final String[] functions; + + static { + // TTop function is new in JLine 3.2 + String[] func; + try { + @SuppressWarnings("unused") + Class cl = TTop.class; + func = new String[] { + "cat", "echo", "grep", "sort", "sleep", "cd", "pwd", "ls", + "less", "watch", "nano", "tmux", + "head", "tail", "clear", "wc", + "date", "ttop", + }; + } catch (Throwable t) { + func = new String[] { + "cat", "echo", "grep", "sort", "sleep", "cd", "pwd", "ls", + "less", "watch", "nano", "tmux", + "head", "tail", "clear", "wc", + "date" + }; + } + functions = func; + } + + public static final String DEFAULT_LS_COLORS = "dr=1;91:ex=1;92:sl=1;96:ot=34;43"; + public static final String DEFAULT_GREP_COLORS = "mt=1;31:fn=35:ln=32:se=36"; + + private static final LinkOption[] NO_FOLLOW_OPTIONS = new LinkOption[]{LinkOption.NOFOLLOW_LINKS}; + private static final List WINDOWS_EXECUTABLE_EXTENSIONS = Collections.unmodifiableList(Arrays.asList(".bat", ".exe", ".cmd")); + private static final LinkOption[] EMPTY_LINK_OPTIONS = new LinkOption[0]; + + private final CommandProcessor processor; + + public Posix(CommandProcessor processor) { + this.processor = processor; + } + + public void _main(CommandSession session, String[] argv) { + if (argv == null || argv.length < 1) { + throw new IllegalArgumentException(); + } + Process process = Process.Utils.current(); + try { + run(session, process, argv); + } catch (IllegalArgumentException e) { + process.err().println(e.getMessage()); + process.error(2); + } catch (HelpException e) { + process.err().println(e.getMessage()); + process.error(0); + } catch (Exception e) { + process.err().println(argv[0] + ": " + e.toString()); + process.error(1); + } + } + + @SuppressWarnings("serial") + protected static class HelpException extends Exception { + public HelpException(String message) { + super(message); + } + } + + protected Options parseOptions(CommandSession session, String[] usage, Object[] argv) throws Exception { + Options opt = Options.compile(usage, s -> get(session, s)).parse(argv, true); + if (opt.isSet("help")) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + opt.usage(new PrintStream(baos)); + throw new HelpException(baos.toString()); + } + return opt; + } + + protected String get(CommandSession session, String name) { + Object o = session.get(name); + return o != null ? o.toString() : null; + } + + protected Object run(CommandSession session, Process process, String[] argv) throws Exception { + switch (argv[0]) { + case "cat": + cat(session, process, argv); + break; + case "echo": + echo(session, process, argv); + break; + case "grep": + grep(session, process, argv); + break; + case "sort": + sort(session, process, argv); + break; + case "sleep": + sleep(session, process, argv); + break; + case "cd": + cd(session, process, argv); + break; + case "pwd": + pwd(session, process, argv); + break; + case "ls": + ls(session, process, argv); + break; + case "less": + less(session, process, argv); + break; + case "watch": + watch(session, process, argv); + break; + case "nano": + nano(session, process, argv); + break; + case "tmux": + tmux(session, process, argv); + break; + case "ttop": + ttop(session, process, argv); + break; + case "clear": + clear(session, process, argv); + break; + case "head": + head(session, process, argv); + break; + case "tail": + tail(session, process, argv); + break; + case "wc": + wc(session, process, argv); + break; + case "date": + date(session, process, argv); + break; + } + return null; + } + + protected void date(CommandSession session, Process process, String[] argv) throws Exception { + String[] usage = { + "date - display date", + "Usage: date [-r seconds] [-v[+|-]val[mwdHMS] ...] [-f input_fmt new_date] [+output_fmt]", + " -? --help Show help", + " -u Use UTC", + " -r Print the date represented by 'seconds' since January 1, 1970", + " -f Use 'input_fmt' to parse 'new_date'" + }; + Date input = new Date(); + String output = null; + for (int i = 1; i < argv.length; i++) { + if ("-?".equals(argv[i]) || "--help".equals(argv[i])) { + throw new HelpException(String.join("\n", usage)); + } + else if ("-r".equals(argv[i])) { + if (i + 1 < argv.length) { + input = new Date(Long.parseLong(argv[++i]) * 1000L); + } else { + throw new IllegalArgumentException("usage: date [-u] [-r seconds] [-v[+|-]val[mwdHMS] ...] [-f input_fmt new_date] [+output_fmt]"); + } + } + else if ("-f".equals(argv[i])) { + if (i + 2 < argv.length) { + String fmt = argv[++i]; + String inp = argv[++i]; + String jfmt = toJavaDateFormat(fmt); + input = new SimpleDateFormat(jfmt).parse(inp); + } else { + throw new IllegalArgumentException("usage: date [-u] [-r seconds] [-v[+|-]val[mwdHMS] ...] [-f input_fmt new_date] [+output_fmt]"); + } + } + else if (argv[i].startsWith("+")) { + if (output == null) { + output = argv[i].substring(1); + } else { + throw new IllegalArgumentException("usage: date [-u] [-r seconds] [-v[+|-]val[mwdHMS] ...] [-f input_fmt new_date] [+output_fmt]"); + } + } + else { + throw new IllegalArgumentException("usage: date [-u] [-r seconds] [-v[+|-]val[mwdHMS] ...] [-f input_fmt new_date] [+output_fmt]"); + } + } + if (output == null) { + output = "%c"; + } + // Print output + process.out().println(new SimpleDateFormat(toJavaDateFormat(output)).format(input)); + } + + private String toJavaDateFormat(String format) { + // transform Unix format to Java SimpleDateFormat (if required) + StringBuilder sb = new StringBuilder(); + boolean quote = false; + for (int i = 0; i < format.length(); i++) { + char c = format.charAt(i); + if (c == '%') { + if (i + 1 < format.length()) { + if (quote) { + sb.append('\''); + quote = false; + } + c = format.charAt(++i); + switch (c) { + case '+': + case 'A': sb.append("MMM EEE d HH:mm:ss yyyy"); break; + case 'a': sb.append("EEE"); break; + case 'B': sb.append("MMMMMMM"); break; + case 'b': sb.append("MMM"); break; + case 'C': sb.append("yy"); break; + case 'c': sb.append("MMM EEE d HH:mm:ss yyyy"); break; + case 'D': sb.append("MM/dd/yy"); break; + case 'd': sb.append("dd"); break; + case 'e': sb.append("dd"); break; + case 'F': sb.append("yyyy-MM-dd"); break; + case 'G': sb.append("YYYY"); break; + case 'g': sb.append("YY"); break; + case 'H': sb.append("HH"); break; + case 'h': sb.append("MMM"); break; + case 'I': sb.append("hh"); break; + case 'j': sb.append("DDD"); break; + case 'k': sb.append("HH"); break; + case 'l': sb.append("hh"); break; + case 'M': sb.append("mm"); break; + case 'm': sb.append("MM"); break; + case 'N': sb.append("S"); break; + case 'n': sb.append("\n"); break; + case 'P': sb.append("aa"); break; + case 'p': sb.append("aa"); break; + case 'r': sb.append("hh:mm:ss aa"); break; + case 'R': sb.append("HH:mm"); break; + case 'S': sb.append("ss"); break; + case 's': sb.append("S"); break; + case 'T': sb.append("HH:mm:ss"); break; + case 't': sb.append("\t"); break; + case 'U': sb.append("w"); break; + case 'u': sb.append("u"); break; + case 'V': sb.append("W"); break; + case 'v': sb.append("dd-MMM-yyyy"); break; + case 'W': sb.append("w"); break; + case 'w': sb.append("u"); break; + case 'X': sb.append("HH:mm:ss"); break; + case 'x': sb.append("MM/dd/yy"); break; + case 'Y': sb.append("yyyy"); break; + case 'y': sb.append("yy"); break; + case 'Z': sb.append("z"); break; + case 'z': sb.append("X"); break; + case '%': sb.append("%"); break; + } + } else { + if (!quote) { + sb.append('\''); + } + sb.append(c); + sb.append('\''); + } + } else { + if ((c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') && !quote) { + sb.append('\''); + quote = true; + } + sb.append(c); + } + } + return sb.toString(); + } + + protected void wc(CommandSession session, Process process, String[] argv) throws Exception { + String[] usage = { + "wc - word, line, character, and byte count", + "Usage: wc [OPTIONS] [FILES]", + " -? --help Show help", + " -l --lines Print line counts", + " -c --bytes Print byte counts", + " -m --chars Print character counts", + " -w --words Print word counts", + }; + Options opt = parseOptions(session, usage, argv); + List sources = new ArrayList<>(); + if (opt.args().isEmpty()) { + opt.args().add("-"); + } + for (String arg : opt.args()) { + if ("-".equals(arg)) { + sources.add(new StdInSource(process)); + } else { + sources.add(new PathSource(session.currentDir().resolve(arg), arg)); + } + } + boolean displayLines = opt.isSet("lines"); + boolean displayWords = opt.isSet("words"); + boolean displayChars = opt.isSet("chars"); + boolean displayBytes = opt.isSet("bytes"); + if (!displayLines && !displayWords && !displayChars && !displayBytes) { + displayLines = true; + displayWords = true; + displayBytes = true; + } + String format = ""; + if (displayLines) { + if (!displayBytes && !displayChars && !displayWords) { + format = "%1$d"; + } else { + format += "%1$8d"; + } + } + if (displayWords) { + if (!displayLines && !displayBytes && !displayChars) { + format = "%2$d"; + } else { + format += "%2$8d"; + } + } + if (displayChars) { + if (!displayLines && !displayBytes && !displayWords) { + format = "%3$d"; + } else { + format += "%3$8d"; + } + } + if (displayBytes) { + if (!displayLines && !displayChars && !displayWords) { + format = "%4$d"; + } else { + format += "%4$8d"; + } + } + if (sources.size() > 1 || (sources.size() == 1 && sources.get(0).getName() != null)) { + format += " %5$8s"; + } + int totalLines = 0; + int totalBytes = 0; + int totalChars = 0; + int totalWords = 0; + for (Source src : sources) { + try (InputStream is = src.read()) { + AtomicInteger lines = new AtomicInteger(); + AtomicInteger bytes = new AtomicInteger(); + AtomicInteger chars = new AtomicInteger(); + AtomicInteger words = new AtomicInteger(); + AtomicBoolean inWord = new AtomicBoolean(); + AtomicBoolean lastNl = new AtomicBoolean(true); + InputStream isc = new FilterInputStream(is) { + @Override + public int read() throws IOException { + int b = super.read(); + if (b >= 0) { + bytes.incrementAndGet(); + } + return b; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int nb = super.read(b, off, len); + if (nb > 0) { + bytes.addAndGet(nb); + } + return nb; + } + }; + IntConsumer consumer = cp -> { + chars.incrementAndGet(); + boolean ws = Character.isWhitespace(cp); + if (inWord.getAndSet(!ws) && ws) { + words.incrementAndGet(); + } + if (cp == '\n') { + lines.incrementAndGet(); + lastNl.set(true); + } else { + lastNl.set(false); + } + }; + Reader reader = new InputStreamReader(isc); + while (true) { + int h = reader.read(); + if (Character.isHighSurrogate((char) h)) { + int l = reader.read(); + if (Character.isLowSurrogate((char) l)) { + int cp = Character.toCodePoint((char) h, (char) l); + consumer.accept(cp); + } else { + consumer.accept(h); + if (l >= 0) { + consumer.accept(l); + } else { + break; + } + } + } else if (h >= 0) { + consumer.accept(h); + } else { + break; + } + } + if (inWord.get()) { + words.incrementAndGet(); + } + if (!lastNl.get()) { + lines.incrementAndGet(); + } + process.out().println(String.format(format, lines.get(), words.get(), chars.get(), bytes.get(), src.getName())); + totalBytes += bytes.get(); + totalChars += chars.get(); + totalWords += words.get(); + totalLines += lines.get(); + } + } + if (sources.size() > 1) { + process.out().println(String.format(format, totalLines, totalWords, totalChars, totalBytes, "total")); + } + } + + protected void head(CommandSession session, Process process, String[] argv) throws Exception { + String[] usage = { + "head - displays first lines of file", + "Usage: head [-n lines | -c bytes] [file ...]", + " -? --help Show help", + " -n --lines=LINES Print line counts", + " -c --bytes=BYTES Print byte counts", + }; + Options opt = parseOptions(session, usage, argv); + if (opt.isSet("lines") && opt.isSet("bytes")) { + throw new IllegalArgumentException("usage: head [-n # | -c #] [file ...]"); + } + int nbLines = Integer.MAX_VALUE; + int nbBytes = Integer.MAX_VALUE; + if (opt.isSet("lines")) { + nbLines = opt.getNumber("lines"); + } else if (opt.isSet("bytes")) { + nbBytes = opt.getNumber("bytes"); + } else { + nbLines = 10; + } + List sources = new ArrayList<>(); + if (opt.args().isEmpty()) { + opt.args().add("-"); + } + for (String arg : opt.args()) { + if ("-".equals(arg)) { + sources.add(new StdInSource(process)); + } else { + sources.add(new PathSource(session.currentDir().resolve(arg), arg)); + } + } + for (Source src : sources) { + int bytes = nbBytes; + int lines = nbLines; + if (sources.size() > 1) { + if (src != sources.get(0)) { + process.out().println(); + } + process.out().println("==> " + src.getName() + " <=="); + } + try (InputStream is = src.read()) { + byte[] buf = new byte[1024]; + int nb; + do { + nb = is.read(buf); + if (nb > 0 && lines > 0 && bytes > 0) { + nb = Math.min(nb, bytes); + for (int i = 0; i < nb; i++) { + if (buf[i] == '\n' && --lines <= 0) { + nb = i + 1; + break; + } + } + bytes -= nb; + process.out().write(buf, 0, nb); + } + } while (nb > 0 && lines > 0 && bytes > 0); + } + } + } + + protected void tail(CommandSession session, Process process, String[] argv) throws Exception { + String[] usage = { + "tail - displays last lines of file", + "Usage: tail [-f] [-q] [-c # | -n #] [file ...]", + " -? --help Show help", + " -q --quiet Suppress headers when printing multiple sources", + " -f --follow Do not stop at end of file", + " -F --FOLLOW Follow and check for file renaming or rotation", + " -n --lines=LINES Number of lines to print", + " -c --bytes=BYTES Number of bytes to print", + }; + Options opt = parseOptions(session, usage, argv); + if (opt.isSet("lines") && opt.isSet("bytes")) { + throw new IllegalArgumentException("usage: tail [-f] [-q] [-c # | -n #] [file ...]"); + } + int lines; + int bytes; + if (opt.isSet("lines")) { + lines = opt.getNumber("lines"); + bytes = Integer.MAX_VALUE; + } else if (opt.isSet("bytes")) { + lines = Integer.MAX_VALUE; + bytes = opt.getNumber("bytes"); + } else { + lines = 10; + bytes = Integer.MAX_VALUE; + } + boolean follow = opt.isSet("follow") || opt.isSet("FOLLOW"); + + AtomicReference lastPrinted = new AtomicReference<>(); + WatchService watchService = follow ? session.currentDir().getFileSystem().newWatchService() : null; + Set watched = new HashSet<>(); + + class Input implements Closeable { + String name; + Path path; + Reader reader; + StringBuilder buffer; + long ino; + long size; + + public Input(String name) { + this.name = name; + this.buffer = new StringBuilder(); + } + + public void open() { + if (reader == null) { + try { + InputStream is; + if ("-".equals(name)) { + is = new StdInSource(process).read(); + } else { + path = session.currentDir().resolve(name); + is = Files.newInputStream(path); + if (opt.isSet("FOLLOW")) { + try { + ino = (Long) Files.getAttribute(path, "unix:ino"); + } catch (Exception e) { + // Ignore + } + } + size = Files.size(path); + } + reader = new InputStreamReader(is); + } catch (IOException e) { + // Ignore + } + } + } + + @Override + public void close() throws IOException { + if (reader != null) { + try { + reader.close(); + } finally { + reader = null; + } + } + } + + public boolean tail() throws IOException { + open(); + if (reader != null) { + if (buffer != null) { + char[] buf = new char[1024]; + int nb; + while ((nb = reader.read(buf)) > 0) { + buffer.append(buf, 0, nb); + if (bytes > 0 && buffer.length() > bytes) { + buffer.delete(0, buffer.length() - bytes); + } else { + int l = 0; + int i = -1; + while ((i = buffer.indexOf("\n", i + 1)) >= 0) { + l++; + } + if (l > lines) { + i = -1; + l = l - lines; + while (--l >= 0) { + i = buffer.indexOf("\n", i + 1); + } + buffer.delete(0, i + 1); + } + } + } + String toPrint = buffer.toString(); + print(toPrint); + buffer = null; + if (follow && path != null) { + Path parent = path.getParent(); + if (!watched.contains(parent)) { + parent.register(watchService, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE, + StandardWatchEventKinds.ENTRY_MODIFY); + watched.add(parent); + } + } + return follow; + } + else if (follow && path != null) { + while (true) { + long newSize = Files.size(path); + if (size != newSize) { + char[] buf = new char[1024]; + int nb; + while ((nb = reader.read(buf)) > 0) { + print(new String(buf, 0, nb)); + } + size = newSize; + } + if (opt.isSet("FOLLOW")) { + long newIno = 0; + try { + newIno = (Long) Files.getAttribute(path, "unix:ino"); + } catch (Exception e) { + // Ignore + } + if (ino != newIno) { + close(); + open(); + ino = newIno; + size = -1; + continue; + } + } + break; + } + return true; + } else { + return false; + } + } else { + Path parent = path.getParent(); + if (!watched.contains(parent)) { + parent.register(watchService, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE, + StandardWatchEventKinds.ENTRY_MODIFY); + watched.add(parent); + } + return true; + } + } + + private void print(String toPrint) { + if (lastPrinted.get() != this && opt.args().size() > 1 && !opt.isSet("quiet")) { + process.out().println(); + process.out().println("==> " + name + " <=="); + } + process.out().print(toPrint); + lastPrinted.set(this); + } + } + + if (opt.args().isEmpty()) { + opt.args().add("-"); + } + List inputs = new ArrayList<>(); + for (String name : opt.args()) { + Input input = new Input(name); + inputs.add(input); + } + try { + boolean cont = true; + while (cont) { + cont = false; + for (Input input : inputs) { + cont |= input.tail(); + } + if (cont) { + WatchKey key = watchService.take(); + key.pollEvents(); + key.reset(); + } + } + } catch (InterruptedException e) { + // Ignore, this is the only way to quit + } finally { + for (Input input : inputs) { + input.close(); + } + } + } + + protected void clear(CommandSession session, Process process, String[] argv) throws Exception { + final String[] usage = { + "clear - clear screen", + "Usage: clear [OPTIONS]", + " -? --help Show help", + }; + @SuppressWarnings("unused") + Options opt = parseOptions(session, usage, argv); + if (process.isTty(1)) { + Shell.getTerminal(session).puts(Capability.clear_screen); + Shell.getTerminal(session).flush(); + } + } + + protected void tmux(final CommandSession session, Process process, String[] argv) throws Exception { + Commands.tmux(Shell.getTerminal(session), + process.out(), System.err, + () -> session.get(".tmux"), + t -> session.put(".tmux", t), + c -> startShell(session, c), + Arrays.copyOfRange(argv, 1, argv.length)); + } + + private void startShell(CommandSession session, Terminal terminal) { + new Thread(() -> runShell(session, terminal), terminal.getName() + " shell").start(); + } + + private void runShell(CommandSession session, Terminal terminal) { + InputStream in = terminal.input(); + OutputStream out = terminal.output(); + CommandSession newSession = processor.createSession(in, out, out); + newSession.put(Shell.VAR_TERMINAL, terminal); + newSession.put(".tmux", session.get(".tmux")); + Context context = new Context() { + public String getProperty(String name) { + return System.getProperty(name); + } + public void exit() throws Exception { + terminal.close(); + } + }; + try { + new Shell(context, processor).gosh(newSession, new String[]{"--login"}); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + terminal.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + protected void ttop(final CommandSession session, Process process, String[] argv) throws Exception { + TTop.ttop(Shell.getTerminal(session), process.out(), process.err(), argv); + } + + protected void nano(final CommandSession session, Process process, String[] argv) throws Exception { + final String[] usage = { + "nano - edit files", + "Usage: nano [FILES]", + " -? --help Show help", + }; + Options opt = parseOptions(session, usage, argv); + Nano edit = new Nano(Shell.getTerminal(session), session.currentDir()); + edit.open(opt.args()); + edit.run(); + } + + protected void watch(final CommandSession session, Process process, String[] argv) throws Exception { + final String[] usage = { + "watch - watches & refreshes the output of a command", + "Usage: watch [OPTIONS] COMMAND", + " -? --help Show help", + " -n --interval Interval between executions of the command in seconds", + " -a --append The output should be appended but not clear the console" + }; + + Options opt = parseOptions(session, usage, argv); + + List args = opt.args(); + if (args.isEmpty()) { + throw new IllegalArgumentException("usage: watch COMMAND"); + } + ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); + final Terminal terminal = Shell.getTerminal(session); + final CommandProcessor processor = Shell.getProcessor(session); + try { + int interval = 1; + if (opt.isSet("interval")) { + interval = opt.getNumber("interval"); + if (interval < 1) { + interval = 1; + } + } + final String cmd = String.join(" ", args); + Runnable task = () -> { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream os = new PrintStream(baos); + InputStream is = new ByteArrayInputStream(new byte[0]); + if (opt.isSet("append") || !terminal.puts(Capability.clear_screen)) { + terminal.writer().println(); + } + try { + CommandSession ns = processor.createSession(is, os, os); + Set vars = Shell.getCommands(session); + for (String n : vars) { + ns.put(n, session.get(n)); + } + ns.execute(cmd); + } catch (Throwable t) { + t.printStackTrace(os); + } + os.flush(); + terminal.writer().print(baos.toString()); + terminal.writer().flush(); + }; + executorService.scheduleAtFixedRate(task, 0, interval, TimeUnit.SECONDS); + Attributes attr = terminal.enterRawMode(); + terminal.reader().read(); + terminal.setAttributes(attr); + } finally { + executorService.shutdownNow(); + } + } + + protected void less(CommandSession session, Process process, String[] argv) throws Exception { + String[] usage = { + "less - file pager", + "Usage: less [OPTIONS] [FILES]", + " -? --help Show help", + " -e --quit-at-eof Exit on second EOF", + " -E --QUIT-AT-EOF Exit on EOF", + " -F --quit-if-one-screen Exit if entire file fits on first screen", + " -q --quiet --silent Silent mode", + " -Q --QUIET --SILENT Completely silent", + " -S --chop-long-lines Do not fold long lines", + " -i --ignore-case Search ignores lowercase case", + " -I --IGNORE-CASE Search ignores all case", + " -x --tabs Set tab stops", + " -N --LINE-NUMBERS Display line number for each line", + " --no-init Disable terminal initialization", + " --no-keypad Disable keypad handling" + }; + boolean hasExtendedOptions = false; + try { + Less.class.getField("quitIfOneScreen"); + hasExtendedOptions = true; + } catch (NoSuchFieldException e) { + List ustrs = new ArrayList<>(Arrays.asList(usage)); + ustrs.removeIf(s -> s.contains("--quit-if-one-screen") || s.contains("--no-init") || s.contains("--no-keypad")); + usage = ustrs.toArray(new String[ustrs.size()]); + } + Options opt = parseOptions(session, usage, argv); + List sources = new ArrayList<>(); + if (opt.args().isEmpty()) { + opt.args().add("-"); + } + for (String arg : opt.args()) { + if ("-".equals(arg)) { + sources.add(new StdInSource(process)); + } else { + sources.add(new PathSource(session.currentDir().resolve(arg), arg)); + } + } + + if (!process.isTty(1)) { + for (Source source : sources) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(source.read()))) { + cat(process, reader, opt.isSet("LINE-NUMBERS")); + } + } + return; + } + + Less less = new Less(Shell.getTerminal(session)); + less.quitAtFirstEof = opt.isSet("QUIT-AT-EOF"); + less.quitAtSecondEof = opt.isSet("quit-at-eof"); + less.quiet = opt.isSet("quiet"); + less.veryQuiet = opt.isSet("QUIET"); + less.chopLongLines = opt.isSet("chop-long-lines"); + less.ignoreCaseAlways = opt.isSet("IGNORE-CASE"); + less.ignoreCaseCond = opt.isSet("ignore-case"); + if (opt.isSet("tabs")) { + less.tabs = opt.getNumber("tabs"); + } + less.printLineNumbers = opt.isSet("LINE-NUMBERS"); + if (hasExtendedOptions) { + Less.class.getField("quitIfOneScreen").set(less, opt.isSet("quit-if-one-screen")); + Less.class.getField("noInit").set(less, opt.isSet("no-init")); + Less.class.getField("noKeypad").set(less, opt.isSet("no-keypad")); + } + less.run(sources); + } + + protected void sort(CommandSession session, Process process, String[] argv) throws Exception { + final String[] usage = { + "sort - writes sorted standard input to standard output.", + "Usage: sort [OPTIONS] [FILES]", + " -? --help show help", + " -f --ignore-case fold lower case to upper case characters", + " -r --reverse reverse the result of comparisons", + " -u --unique output only the first of an equal run", + " -t --field-separator=SEP use SEP instead of non-blank to blank transition", + " -b --ignore-leading-blanks ignore leading blancks", + " --numeric-sort compare according to string numerical value", + " -k --key=KEY fields to use for sorting separated by whitespaces"}; + + Options opt = parseOptions(session, usage, argv); + + List args = opt.args(); + + List lines = new ArrayList<>(); + if (!args.isEmpty()) { + for (String filename : args) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader( + session.currentDir().toUri().resolve(filename).toURL().openStream()))) { + read(reader, lines); + } + } + } else { + BufferedReader r = new BufferedReader(new InputStreamReader(process.in())); + read(r, lines); + } + + String separator = opt.get("field-separator"); + boolean caseInsensitive = opt.isSet("ignore-case"); + boolean reverse = opt.isSet("reverse"); + boolean ignoreBlanks = opt.isSet("ignore-leading-blanks"); + boolean numeric = opt.isSet("numeric-sort"); + boolean unique = opt.isSet("unique"); + List sortFields = opt.getList("key"); + + char sep = (separator == null || separator.length() == 0) ? '\0' : separator.charAt(0); + lines.sort(new SortComparator(caseInsensitive, reverse, ignoreBlanks, numeric, sep, sortFields)); + String last = null; + for (String s : lines) { + if (!unique || last == null || !s.equals(last)) { + process.out().println(s); + } + last = s; + } + } + + protected void pwd(CommandSession session, Process process, String[] argv) throws Exception { + final String[] usage = { + "pwd - get current directory", + "Usage: pwd [OPTIONS]", + " -? --help show help" + }; + Options opt = parseOptions(session, usage, argv); + if (!opt.args().isEmpty()) { + throw new IllegalArgumentException("usage: pwd"); + } + process.out().println(session.currentDir()); + } + + protected void cd(CommandSession session, Process process, String[] argv) throws Exception { + final String[] usage = { + "cd - get current directory", + "Usage: cd [OPTIONS] DIRECTORY", + " -? --help show help" + }; + Options opt = parseOptions(session, usage, argv); + if (opt.args().size() != 1) { + throw new IllegalArgumentException("usage: cd DIRECTORY"); + } + Path cwd = session.currentDir(); + cwd = cwd.resolve(opt.args().get(0)).toAbsolutePath(); + if (!Files.exists(cwd)) { + throw new IOException("no such file or directory: " + opt.args().get(0)); + } else if (!Files.isDirectory(cwd)) { + throw new IOException("not a directory: " + opt.args().get(0)); + } + session.currentDir(cwd); + } + + protected void ls(CommandSession session, Process process, String[] argv) throws Exception { + final String[] usage = { + "ls - list files", + "Usage: ls [OPTIONS] [PATTERNS...]", + " -? --help show help", + " -1 list one entry per line", + " -C multi-column output", + " --color=WHEN colorize the output, may be `always', `never' or `auto'", + " -a list entries starting with .", + " -F append file type indicators", + " -m comma separated", + " -l long listing", + " -S sort by size", + " -f output is not sorted", + " -r reverse sort order", + " -t sort by modification time", + " -x sort horizontally", + " -L list referenced file for links", + " -h print sizes in human readable form" + }; + Options opt = parseOptions(session, usage, argv); + String color = opt.isSet("color") ? opt.get("color") : "auto"; + boolean colored; + switch (color) { + case "always": + case "yes": + case "force": + colored = true; + break; + case "never": + case "no": + case "none": + colored = false; + break; + case "auto": + case "tty": + case "if-tty": + colored = process.isTty(1); + break; + default: + throw new IllegalArgumentException("invalid argument ‘" + color + "’ for ‘--color’"); + } + Map colors = colored ? getLsColorMap(session) : Collections.emptyMap(); + + class PathEntry implements Comparable { + final Path abs; + final Path path; + final Map attributes; + + public PathEntry(Path abs, Path root) { + this.abs = abs; + this.path = abs.startsWith(root) ? root.relativize(abs) : abs; + this.attributes = readAttributes(abs); + } + + @Override + public int compareTo(PathEntry o) { + int c = doCompare(o); + return opt.isSet("r") ? -c : c; + } + + private int doCompare(PathEntry o) { + if (opt.isSet("f")) { + return -1; + } + if (opt.isSet("S")) { + long s0 = attributes.get("size") != null ? ((Number) attributes.get("size")).longValue() : 0L; + long s1 = o.attributes.get("size") != null ? ((Number) o.attributes.get("size")).longValue() : 0L; + return s0 > s1 ? -1 : s0 < s1 ? 1 : path.toString().compareTo(o.path.toString()); + } + if (opt.isSet("t")) { + long t0 = attributes.get("lastModifiedTime") != null ? ((FileTime) attributes.get("lastModifiedTime")).toMillis() : 0L; + long t1 = o.attributes.get("lastModifiedTime") != null ? ((FileTime) o.attributes.get("lastModifiedTime")).toMillis() : 0L; + return t0 > t1 ? -1 : t0 < t1 ? 1 : path.toString().compareTo(o.path.toString()); + } + return path.toString().compareTo(o.path.toString()); + } + + boolean isNotDirectory() { + return is("isRegularFile") || is("isSymbolicLink") || is("isOther"); + } + + boolean isDirectory() { + return is("isDirectory"); + } + + private boolean is(String attr) { + Object d = attributes.get(attr); + return d instanceof Boolean && (Boolean) d; + } + + String display() { + String type; + String suffix; + String link = ""; + if (is("isSymbolicLink")) { + type = "sl"; + suffix = "@"; + try { + Path l = Files.readSymbolicLink(abs); + link = " -> " + l.toString(); + } catch (IOException e) { + // ignore + } + } else if (is("isDirectory")) { + type = "dr"; + suffix = "/"; + } else if (is("isExecutable")) { + type = "ex"; + suffix = "*"; + } else if (is("isOther")) { + type = "ot"; + suffix = ""; + } else { + type = ""; + suffix = ""; + } + boolean addSuffix = opt.isSet("F"); + return applyStyle(path.toString(), colors, type) + + (addSuffix ? suffix : "") + link; + } + + String longDisplay() { + String username; + if (attributes.containsKey("owner")) { + username = Objects.toString(attributes.get("owner"), null); + } else { + username = "owner"; + } + if (username.length() > 8) { + username = username.substring(0, 8); + } else { + for (int i = username.length(); i < 8; i++) { + username = username + " "; + } + } + String group; + if (attributes.containsKey("group")) { + group = Objects.toString(attributes.get("group"), null); + } else { + group = "group"; + } + if (group.length() > 8) { + group = group.substring(0, 8); + } else { + for (int i = group.length(); i < 8; i++) { + group = group + " "; + } + } + Number length = (Number) attributes.get("size"); + if (length == null) { + length = 0L; + } + String lengthString; + if (opt.isSet("h")) { + double l = length.longValue(); + String unit = "B"; + if (l >= 1000) { + l /= 1024; + unit = "K"; + if (l >= 1000) { + l /= 1024; + unit = "M"; + if (l >= 1000) { + l /= 1024; + unit = "T"; + } + } + } + if (l < 10 && length.longValue() > 1000) { + lengthString = String.format("%.1f", l) + unit; + } else { + lengthString = String.format("%3.0f", l) + unit; + } + } else { + lengthString = String.format("%1$8s", length); + } + @SuppressWarnings("unchecked") + Set perms = (Set) attributes.get("permissions"); + if (perms == null) { + perms = EnumSet.noneOf(PosixFilePermission.class); + } + // TODO: all fields should be padded to align + return (is("isDirectory") ? "d" : (is("isSymbolicLink") ? "l" : (is("isOther") ? "o" : "-"))) + + PosixFilePermissions.toString(perms) + " " + + String.format("%3s", (attributes.containsKey("nlink") ? attributes.get("nlink").toString() : "1")) + + " " + username + " " + group + " " + lengthString + " " + + toString((FileTime) attributes.get("lastModifiedTime")) + + " " + display(); + } + + protected String toString(FileTime time) { + long millis = (time != null) ? time.toMillis() : -1L; + if (millis < 0L) { + return "------------"; + } + ZonedDateTime dt = Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()); + // Less than six months + if (System.currentTimeMillis() - millis < 183L * 24L * 60L * 60L * 1000L) { + return DateTimeFormatter.ofPattern("MMM ppd HH:mm").format(dt); + } + // Older than six months + else { + return DateTimeFormatter.ofPattern("MMM ppd yyyy").format(dt); + } + } + + protected Map readAttributes(Path path) { + Map attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (String view : path.getFileSystem().supportedFileAttributeViews()) { + try { + Map ta = Files.readAttributes(path, view + ":*", + getLinkOptions(opt.isSet("L"))); + ta.forEach(attrs::putIfAbsent); + } catch (IOException e) { + // Ignore + } + } + attrs.computeIfAbsent("isExecutable", s -> Files.isExecutable(path)); + attrs.computeIfAbsent("permissions", s -> getPermissionsFromFile(path.toFile())); + return attrs; + } + } + + Path currentDir = session.currentDir(); + // Listing + List expanded = new ArrayList<>(); + if (opt.args().isEmpty()) { + expanded.add(currentDir); + } else { + opt.args().forEach(s -> expanded.add(currentDir.resolve(s))); + } + boolean listAll = opt.isSet("a"); + Predicate filter = p -> listAll || p.getFileName().toString().equals(".") + || p.getFileName().toString().equals("..") || !p.getFileName().toString().startsWith("."); + List all = expanded.stream() + .filter(filter) + .map(p -> new PathEntry(p, currentDir)) + .sorted() + .collect(Collectors.toList()); + // Print files first + List files = all.stream() + .filter(PathEntry::isNotDirectory) + .collect(Collectors.toList()); + PrintStream out = process.out(); + Consumer> display = s -> { + boolean optLine = opt.isSet("1"); + boolean optComma = opt.isSet("m"); + boolean optLong = opt.isSet("l"); + boolean optCol = opt.isSet("C"); + if (!optLine && !optComma && !optLong && !optCol) { + if (process.isTty(1)) { + optCol = true; + } + else { + optLine = true; + } + } + // One entry per line + if (optLine) { + s.map(PathEntry::display).forEach(out::println); + } + // Comma separated list + else if (optComma) { + out.println(s.map(PathEntry::display).collect(Collectors.joining(", "))); + } + // Long listing + else if (optLong) { + s.map(PathEntry::longDisplay).forEach(out::println); + } + // Column listing + else if (optCol) { + toColumn(session, process, out, s.map(PathEntry::display), opt.isSet("x")); + } + }; + boolean space = false; + if (!files.isEmpty()) { + display.accept(files.stream()); + space = true; + } + // Print directories + List directories = all.stream() + .filter(PathEntry::isDirectory) + .collect(Collectors.toList()); + for (PathEntry entry : directories) { + if (space) { + out.println(); + } + space = true; + Path path = currentDir.resolve(entry.path); + if (expanded.size() > 1) { + out.println(currentDir.relativize(path).toString() + ":"); + } + display.accept(Stream.concat(Stream.of(".", "..").map(path::resolve), Files.list(path)) + .filter(filter) + .map(p -> new PathEntry(p, path)) + .sorted() + ); + } + } + + private void toColumn(CommandSession session, Process process, PrintStream out, Stream ansi, boolean horizontal) { + Terminal terminal = Shell.getTerminal(session); + int width = process.isTty(1) ? terminal.getWidth() : 80; + List strings = ansi.map(AttributedString::fromAnsi).collect(Collectors.toList()); + if (!strings.isEmpty()) { + int max = strings.stream().mapToInt(AttributedString::columnLength).max().getAsInt(); + int c = Math.max(1, width / max); + while (c > 1 && c * max + (c - 1) >= width) { + c--; + } + int columns = c; + int lines = (strings.size() + columns - 1) / columns; + IntBinaryOperator index; + if (horizontal) { + index = (i, j) -> i * columns + j; + } else { + index = (i, j) -> j * lines + i; + } + AttributedStringBuilder sb = new AttributedStringBuilder(); + for (int i = 0; i < lines; i++) { + for (int j = 0; j < columns; j++) { + int idx = index.applyAsInt(i, j); + if (idx < strings.size()) { + AttributedString str = strings.get(idx); + boolean hasRightItem = j < columns - 1 && index.applyAsInt(i, j + 1) < strings.size(); + sb.append(str); + if (hasRightItem) { + for (int k = 0; k <= max - str.length(); k++) { + sb.append(' '); + } + } + } + } + sb.append('\n'); + } + out.print(sb.toAnsi(terminal)); + } + } + + protected void cat(CommandSession session, Process process, String[] argv) throws Exception { + final String[] usage = { + "cat - concatenate and print FILES", + "Usage: cat [OPTIONS] [FILES]", + " -? --help show help", + " -n number the output lines, starting at 1" + }; + Options opt = parseOptions(session, usage, argv); + List args = opt.args(); + if (args.isEmpty()) { + args = Collections.singletonList("-"); + } + Path cwd = session.currentDir(); + for (String arg : args) { + InputStream is; + if ("-".equals(arg)) { + is = process.in(); + } else { + is = cwd.toUri().resolve(arg).toURL().openStream(); + } + cat(process, new BufferedReader(new InputStreamReader(is)), opt.isSet("n")); + } + } + + protected void echo(CommandSession session, Process process, Object[] argv) throws Exception { + final String[] usage = { + "echo - echoes or prints ARGUMENT to standard output", + "Usage: echo [OPTIONS] [ARGUMENTS]", + " -? --help show help", + " -n no trailing new line" + }; + Options opt = parseOptions(session, usage, argv); + List args = opt.args(); + StringBuilder buf = new StringBuilder(); + if (args != null) { + for (String arg : args) { + if (buf.length() > 0) + buf.append(' '); + for (int i = 0; i < arg.length(); i++) { + int c = arg.charAt(i); + int ch; + if (c == '\\') { + c = i < arg.length() - 1 ? arg.charAt(++i) : '\\'; + switch (c) { + case 'a': + buf.append('\u0007'); + break; + case 'n': + buf.append('\n'); + break; + case 't': + buf.append('\t'); + break; + case 'r': + buf.append('\r'); + break; + case '\\': + buf.append('\\'); + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + ch = 0; + for (int j = 0; j < 3; j++) { + c = i < arg.length() - 1 ? arg.charAt(++i) : -1; + if (c >= 0) { + ch = ch * 8 + (c - '0'); + } + } + buf.append((char) ch); + break; + case 'u': + ch = 0; + for (int j = 0; j < 4; j++) { + c = i < arg.length() - 1 ? arg.charAt(++i) : -1; + if (c >= 0) { + if (c >= 'A' && c <= 'Z') { + ch = ch * 16 + (c - 'A' + 10); + } else if (c >= 'a' && c <= 'z') { + ch = ch * 16 + (c - 'a' + 10); + } else if (c >= '0' && c <= '9') { + ch = ch * 16 + (c - '0'); + } else { + break; + } + } + } + buf.append((char) ch); + break; + default: + buf.append((char) c); + break; + } + } else { + buf.append((char) c); + } + } + } + } + if (opt.isSet("n")) { + process.out().print(buf); + } else { + process.out().println(buf); + } + } + + protected void grep(CommandSession session, Process process, String[] argv) throws Exception { + final String[] usage = { + "grep - search for PATTERN in each FILE or standard input.", + "Usage: grep [OPTIONS] PATTERN [FILES]", + " -? --help Show help", + " -i --ignore-case Ignore case distinctions", + " -n --line-number Prefix each line with line number within its input file", + " -q --quiet, --silent Suppress all normal output", + " -v --invert-match Select non-matching lines", + " -w --word-regexp Select only whole words", + " -x --line-regexp Select only whole lines", + " -c --count Only print a count of matching lines per file", + " --color=WHEN Use markers to distinguish the matching string, may be `always', `never' or `auto'", + " -B --before-context=NUM Print NUM lines of leading context before matching lines", + " -A --after-context=NUM Print NUM lines of trailing context after matching lines", + " -C --context=NUM Print NUM lines of output context", + " --pad-lines Pad line numbers" + }; + Options opt = parseOptions(session, usage, argv); + List args = opt.args(); + if (args.isEmpty()) { + throw new IllegalArgumentException("no pattern supplied"); + } + + String regex = args.remove(0); + String regexp = regex; + if (opt.isSet("word-regexp")) { + regexp = "\\b" + regexp + "\\b"; + } + if (opt.isSet("line-regexp")) { + regexp = "^" + regexp + "$"; + } else { + regexp = ".*" + regexp + ".*"; + } + Pattern p; + Pattern p2; + if (opt.isSet("ignore-case")) { + p = Pattern.compile(regexp, Pattern.CASE_INSENSITIVE); + p2 = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + } else { + p = Pattern.compile(regexp); + p2 = Pattern.compile(regex); + } + int after = opt.isSet("after-context") ? opt.getNumber("after-context") : -1; + int before = opt.isSet("before-context") ? opt.getNumber("before-context") : -1; + int context = opt.isSet("context") ? opt.getNumber("context") : 0; + String lineFmt = opt.isSet("pad-lines") ? "%6d" : "%d"; + if (after < 0) { + after = context; + } + if (before < 0) { + before = context; + } + List lines = new ArrayList<>(); + boolean invertMatch = opt.isSet("invert-match"); + boolean lineNumber = opt.isSet("line-number"); + boolean count = opt.isSet("count"); + String color = opt.isSet("color") ? opt.get("color") : "auto"; + boolean colored; + switch (color) { + case "always": + case "yes": + case "force": + colored = true; + break; + case "never": + case "no": + case "none": + colored = false; + break; + case "auto": + case "tty": + case "if-tty": + colored = process.isTty(1); + break; + default: + throw new IllegalArgumentException("invalid argument ‘" + color + "’ for ‘--color’"); + } + Map colors = colored ? getColorMap(session, "GREP", DEFAULT_GREP_COLORS) : Collections.emptyMap(); + + List sources = new ArrayList<>(); + if (opt.args().isEmpty()) { + opt.args().add("-"); + } + for (String arg : opt.args()) { + if ("-".equals(arg)) { + sources.add(new StdInSource(process)); + } else { + sources.add(new PathSource(session.currentDir().resolve(arg), arg)); + } + } + boolean match = false; + for (Source source : sources) { + boolean firstPrint = true; + int nb = 0; + int lineno = 1; + String line; + int lineMatch = 0; + try (BufferedReader r = new BufferedReader(new InputStreamReader(source.read()))) { + while ((line = r.readLine()) != null) { + if (line.length() == 1 && line.charAt(0) == '\n') { + break; + } + boolean matches = p.matcher(line).matches(); + AttributedStringBuilder sbl = new AttributedStringBuilder(); + if (!count) { + if (sources.size() > 1) { + if (colored) { + applyStyle(sbl, colors, "fn"); + } + sbl.append(source.getName()); + if (colored) { + applyStyle(sbl, colors, "se"); + } + sbl.append(":"); + } + if (lineNumber) { + if (colored) { + applyStyle(sbl, colors, "ln"); + } + sbl.append(String.format(lineFmt, lineno)); + if (colored) { + applyStyle(sbl, colors, "se"); + } + sbl.append((matches ^ invertMatch) ? ":" : "-"); + } + String style = matches ^ invertMatch ^ (invertMatch && colors.containsKey("rv")) + ? "sl" : "cx"; + if (colored) { + applyStyle(sbl, colors, style); + } + AttributedString aLine = AttributedString.fromAnsi(line); + Matcher matcher2 = p2.matcher(aLine.toString()); + int cur = 0; + while (matcher2.find()) { + int index = matcher2.start(0); + AttributedString prefix = aLine.subSequence(cur, index); + sbl.append(prefix); + cur = matcher2.end(); + if (colored) { + applyStyle(sbl, colors, invertMatch ? "mc" : "ms", "mt"); + } + sbl.append(aLine.subSequence(index, cur)); + if (colored) { + applyStyle(sbl, colors, style); + } + nb++; + } + sbl.append(aLine.subSequence(cur, aLine.length())); + } + if (matches ^ invertMatch) { + lines.add(sbl.toAnsi(Shell.getTerminal(session))); + lineMatch = lines.size(); + } else { + if (lineMatch != 0 & lineMatch + after + before <= lines.size()) { + if (!count) { + if (!firstPrint && before + after > 0) { + AttributedStringBuilder sbl2 = new AttributedStringBuilder(); + if (colored) { + applyStyle(sbl2, colors, "se"); + } + sbl2.append("--"); + process.out().println(sbl2.toAnsi(Shell.getTerminal(session))); + } else { + firstPrint = false; + } + for (int i = 0; i < lineMatch + after; i++) { + process.out().println(lines.get(i)); + } + } + while (lines.size() > before) { + lines.remove(0); + } + lineMatch = 0; + } + lines.add(sbl.toAnsi(Shell.getTerminal(session))); + while (lineMatch == 0 && lines.size() > before) { + lines.remove(0); + } + } + lineno++; + } + if (!count && lineMatch > 0) { + if (!firstPrint && before + after > 0) { + AttributedStringBuilder sbl2 = new AttributedStringBuilder(); + if (colored) { + applyStyle(sbl2, colors, "se"); + } + sbl2.append("--"); + process.out().println(sbl2.toAnsi(Shell.getTerminal(session))); + } else { + firstPrint = false; + } + for (int i = 0; i < lineMatch + after && i < lines.size(); i++) { + process.out().println(lines.get(i)); + } + } + if (count) { + process.out().println(nb); + } + match |= nb > 0; + } + } + Process.Utils.current().error(match ? 0 : 1); + } + + protected void sleep(CommandSession session, Process process, String[] argv) throws Exception { + final String[] usage = { + "sleep - suspend execution for an interval of time", + "Usage: sleep seconds", + " -? --help show help"}; + + Options opt = parseOptions(session, usage, argv); + List args = opt.args(); + if (args.size() != 1) { + throw new IllegalArgumentException("usage: sleep seconds"); + } else { + int s = Integer.parseInt(args.get(0)); + Thread.sleep(s * 1000); + } + } + + protected static void read(BufferedReader r, List lines) throws IOException { + for (String s = r.readLine(); s != null; s = r.readLine()) { + lines.add(s); + } + } + + private static void cat(Process process, final BufferedReader reader, boolean displayLineNumbers) throws IOException { + String line; + int lineno = 1; + try { + while ((line = reader.readLine()) != null) { + if (displayLineNumbers) { + process.out().print(String.format("%6d ", lineno++)); + } + process.out().println(line); + } + } finally { + reader.close(); + } + } + + public static class SortComparator implements Comparator { + + private static Pattern fpPattern; + + static { + final String Digits = "(\\p{Digit}+)"; + final String HexDigits = "(\\p{XDigit}+)"; + final String Exp = "[eE][+-]?" + Digits; + final String fpRegex = "([\\x00-\\x20]*[+-]?(NaN|Infinity|(((" + Digits + "(\\.)?(" + Digits + "?)(" + Exp + ")?)|(\\.(" + Digits + ")(" + Exp + ")?)|(((0[xX]" + HexDigits + "(\\.)?)|(0[xX]" + HexDigits + "?(\\.)" + HexDigits + "))[pP][+-]?" + Digits + "))" + "[fFdD]?))[\\x00-\\x20]*)(.*)"; + fpPattern = Pattern.compile(fpRegex); + } + + private boolean caseInsensitive; + private boolean reverse; + private boolean ignoreBlanks; + private boolean numeric; + private char separator; + private List sortKeys; + + public SortComparator(boolean caseInsensitive, + boolean reverse, + boolean ignoreBlanks, + boolean numeric, + char separator, + List sortFields) { + this.caseInsensitive = caseInsensitive; + this.reverse = reverse; + this.separator = separator; + this.ignoreBlanks = ignoreBlanks; + this.numeric = numeric; + if (sortFields == null || sortFields.size() == 0) { + sortFields = new ArrayList<>(); + sortFields.add("1"); + } + sortKeys = sortFields.stream().map(Key::new).collect(Collectors.toList()); + } + + public int compare(String o1, String o2) { + int res = 0; + + List fi1 = getFieldIndexes(o1); + List fi2 = getFieldIndexes(o2); + for (Key key : sortKeys) { + int[] k1 = getSortKey(o1, fi1, key); + int[] k2 = getSortKey(o2, fi2, key); + if (key.numeric) { + Double d1 = getDouble(o1, k1[0], k1[1]); + Double d2 = getDouble(o2, k2[0], k2[1]); + res = d1.compareTo(d2); + } else { + res = compareRegion(o1, k1[0], k1[1], o2, k2[0], k2[1], key.caseInsensitive); + } + if (res != 0) { + if (key.reverse) { + res = -res; + } + break; + } + } + return res; + } + + protected Double getDouble(String s, int start, int end) { + Matcher m = fpPattern.matcher(s.substring(start, end)); + m.find(); + return new Double(s.substring(0, m.end(1))); + } + + protected int compareRegion(String s1, int start1, int end1, String s2, int start2, int end2, boolean caseInsensitive) { + for (int i1 = start1, i2 = start2; i1 < end1 && i2 < end2; i1++, i2++) { + char c1 = s1.charAt(i1); + char c2 = s2.charAt(i2); + if (c1 != c2) { + if (caseInsensitive) { + c1 = Character.toUpperCase(c1); + c2 = Character.toUpperCase(c2); + if (c1 != c2) { + c1 = Character.toLowerCase(c1); + c2 = Character.toLowerCase(c2); + if (c1 != c2) { + return c1 - c2; + } + } + } else { + return c1 - c2; + } + } + } + return end1 - end2; + } + + protected int[] getSortKey(String str, List fields, Key key) { + int start; + int end; + if (key.startField * 2 <= fields.size()) { + start = fields.get((key.startField - 1) * 2); + if (key.ignoreBlanksStart) { + while (start < fields.get((key.startField - 1) * 2 + 1) && Character.isWhitespace(str.charAt(start))) { + start++; + } + } + if (key.startChar > 0) { + start = Math.min(start + key.startChar - 1, fields.get((key.startField - 1) * 2 + 1)); + } + } else { + start = 0; + } + if (key.endField > 0 && key.endField * 2 <= fields.size()) { + end = fields.get((key.endField - 1) * 2); + if (key.ignoreBlanksEnd) { + while (end < fields.get((key.endField - 1) * 2 + 1) && Character.isWhitespace(str.charAt(end))) { + end++; + } + } + if (key.endChar > 0) { + end = Math.min(end + key.endChar - 1, fields.get((key.endField - 1) * 2 + 1)); + } + } else { + end = str.length(); + } + return new int[]{start, end}; + } + + protected List getFieldIndexes(String o) { + List fields = new ArrayList<>(); + if (o.length() > 0) { + if (separator == '\0') { + fields.add(0); + for (int idx = 1; idx < o.length(); idx++) { + if (Character.isWhitespace(o.charAt(idx)) && !Character.isWhitespace(o.charAt(idx - 1))) { + fields.add(idx - 1); + fields.add(idx); + } + } + fields.add(o.length() - 1); + } else { + int last = -1; + for (int idx = o.indexOf(separator); idx >= 0; idx = o.indexOf(separator, idx + 1)) { + if (last >= 0) { + fields.add(last); + fields.add(idx - 1); + } else if (idx > 0) { + fields.add(0); + fields.add(idx - 1); + } + last = idx + 1; + } + if (last < o.length()) { + fields.add(last < 0 ? 0 : last); + fields.add(o.length() - 1); + } + } + } + return fields; + } + + public class Key { + int startField; + int startChar; + int endField; + int endChar; + boolean ignoreBlanksStart; + boolean ignoreBlanksEnd; + boolean caseInsensitive; + boolean reverse; + boolean numeric; + + public Key(String str) { + boolean modifiers = false; + boolean startPart = true; + boolean inField = true; + boolean inChar = false; + for (char c : str.toCharArray()) { + switch (c) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + if (!inField && !inChar) { + throw new IllegalArgumentException("Bad field syntax: " + str); + } + if (startPart) { + if (inChar) { + startChar = startChar * 10 + (c - '0'); + } else { + startField = startField * 10 + (c - '0'); + } + } else { + if (inChar) { + endChar = endChar * 10 + (c - '0'); + } else { + endField = endField * 10 + (c - '0'); + } + } + break; + case '.': + if (!inField) { + throw new IllegalArgumentException("Bad field syntax: " + str); + } + inField = false; + inChar = true; + break; + case 'n': + inField = false; + inChar = false; + modifiers = true; + numeric = true; + break; + case 'f': + inField = false; + inChar = false; + modifiers = true; + caseInsensitive = true; + break; + case 'r': + inField = false; + inChar = false; + modifiers = true; + reverse = true; + break; + case 'b': + inField = false; + inChar = false; + modifiers = true; + if (startPart) { + ignoreBlanksStart = true; + } else { + ignoreBlanksEnd = true; + } + break; + case ',': + inField = true; + inChar = false; + startPart = false; + break; + default: + throw new IllegalArgumentException("Bad field syntax: " + str); + } + } + if (!modifiers) { + ignoreBlanksStart = ignoreBlanksEnd = SortComparator.this.ignoreBlanks; + reverse = SortComparator.this.reverse; + caseInsensitive = SortComparator.this.caseInsensitive; + numeric = SortComparator.this.numeric; + } + if (startField < 1) { + throw new IllegalArgumentException("Bad field syntax: " + str); + } + } + } + } + + private static LinkOption[] getLinkOptions(boolean followLinks) { + if (followLinks) { + return EMPTY_LINK_OPTIONS; + } else { // return a clone that modifications to the array will not affect others + return NO_FOLLOW_OPTIONS.clone(); + } + } + + /** + * @param fileName The file name to be evaluated - ignored if {@code null}/empty + * @return {@code true} if the file ends in one of the {@link #WINDOWS_EXECUTABLE_EXTENSIONS} + */ + private static boolean isWindowsExecutable(String fileName) { + if ((fileName == null) || (fileName.length() <= 0)) { + return false; + } + for (String suffix : WINDOWS_EXECUTABLE_EXTENSIONS) { + if (fileName.endsWith(suffix)) { + return true; + } + } + return false; + } + + /** + * @param f The {@link File} to be checked + * @return A {@link Set} of {@link PosixFilePermission}s based on whether + * the file is readable/writable/executable. If so, then all the + * relevant permissions are set (i.e., owner, group and others) + */ + private static Set getPermissionsFromFile(File f) { + Set perms = EnumSet.noneOf(PosixFilePermission.class); + if (f.canRead()) { + perms.add(PosixFilePermission.OWNER_READ); + perms.add(PosixFilePermission.GROUP_READ); + perms.add(PosixFilePermission.OTHERS_READ); + } + + if (f.canWrite()) { + perms.add(PosixFilePermission.OWNER_WRITE); + perms.add(PosixFilePermission.GROUP_WRITE); + perms.add(PosixFilePermission.OTHERS_WRITE); + } + + if (f.canExecute() || (OSUtils.IS_WINDOWS && isWindowsExecutable(f.getName()))) { + perms.add(PosixFilePermission.OWNER_EXECUTE); + perms.add(PosixFilePermission.GROUP_EXECUTE); + perms.add(PosixFilePermission.OTHERS_EXECUTE); + } + + return perms; + } + + public static Map getLsColorMap(CommandSession session) { + return getColorMap(session, "LS", DEFAULT_LS_COLORS); + } + + public static Map getColorMap(CommandSession session, String name, String def) { + Object obj = session.get(name + "_COLORS"); + String str = obj != null ? obj.toString() : null; + if (str == null) { + str = def; + } + String sep = str.matches("[a-z]{2}=[0-9]*(;[0-9]+)*(:[a-z]{2}=[0-9]*(;[0-9]+)*)*") ? ":" : " "; + return Arrays.stream(str.split(sep)) + .collect(Collectors.toMap(s -> s.substring(0, s.indexOf('=')), + s -> s.substring(s.indexOf('=') + 1))); + } + + static String applyStyle(String text, Map colors, String... types) { + String t = null; + for (String type : types) { + if (colors.get(type) != null) { + t = type; + break; + } + } + return new AttributedString(text, new StyleResolver(colors::get).resolve("." + t)) + .toAnsi(); + } + + static void applyStyle(AttributedStringBuilder sb, Map colors, String... types) { + String t = null; + for (String type : types) { + if (colors.get(type) != null) { + t = type; + break; + } + } + sb.style(new StyleResolver(colors::get).resolve("." + t)); + } + + private static class StdInSource implements Source { + + private final Process process; + + StdInSource(Process process) { + this.process = process; + } + + @Override + public String getName() { + return null; + } + + @Override + public InputStream read() { + return process.in(); + } + } +} diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Procedural.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Procedural.java new file mode 100644 index 00000000000..eece61abbcb --- /dev/null +++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Procedural.java @@ -0,0 +1,604 @@ +/* + * 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.gogo.jline; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Function; +import org.apache.felix.service.command.Process; +import org.jline.builtins.Options; + +public class Procedural { + + static final String[] functions = {"each", "if", "not", "throw", "try", "until", "while", "break", "continue"}; + + public Object _main(CommandSession session, Object[] argv) throws Throwable { + if (argv == null || argv.length < 1) { + throw new IllegalArgumentException(); + } + Process process = Process.Utils.current(); + try { + return run(session, process, argv); + } catch (OptionException e) { + process.err().println(e.getMessage()); + process.error(2); + } catch (HelpException e) { + process.err().println(e.getMessage()); + process.error(0); + } catch (ThrownException e) { + process.error(1); + throw e.getCause(); + } + return null; + } + + @SuppressWarnings("serial") + protected static class OptionException extends Exception { + public OptionException(String message, Throwable cause) { + super(message, cause); + } + } + + @SuppressWarnings("serial") + protected static class HelpException extends Exception { + public HelpException(String message) { + super(message); + } + } + + @SuppressWarnings("serial") + protected static class ThrownException extends Exception { + public ThrownException(Throwable cause) { + super(cause); + } + } + + @SuppressWarnings("serial") + protected static class BreakException extends Exception { + } + + @SuppressWarnings("serial") + protected static class ContinueException extends Exception { + } + + protected Options parseOptions(CommandSession session, String[] usage, Object[] argv) throws HelpException, OptionException { + try { + Options opt = Options.compile(usage, s -> get(session, s)).parse(argv, true); + if (opt.isSet("help")) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + opt.usage(new PrintStream(baos)); + throw new HelpException(baos.toString()); + } + return opt; + } catch (IllegalArgumentException e) { + throw new OptionException(e.getMessage(), e); + } + } + + protected String get(CommandSession session, String name) { + Object o = session.get(name); + return o != null ? o.toString() : null; + } + + protected Object run(CommandSession session, Process process, Object[] argv) throws Throwable { + switch (argv[0].toString()) { + case "each": + return doEach(session, process, argv); + case "if": + return doIf(session, process, argv); + case "not": + return doNot(session, process, argv); + case "throw": + return doThrow(session, process, argv); + case "try": + return doTry(session, process, argv); + case "until": + return doUntil(session, process, argv); + case "while": + return doWhile(session, process, argv); + case "break": + return doBreak(session, process, argv); + case "continue": + return doContinue(session, process, argv); + default: + throw new UnsupportedOperationException(); + } + } + + protected List doEach(CommandSession session, + Process process, + Object[] argv) throws Exception { + String[] usage = { + "each - loop over the elements", + "Usage: each [-r] elements [do] { closure }", + " elements an array to iterate on", + " closure a closure to call", + " -? --help Show help", + " -r --result Return a list containing each iteration result", + }; + Options opt = parseOptions(session, usage, argv); + + Collection elements = getElements(opt); + if (opt.argObjects().size() > 0 && "do".equals(opt.argObjects().get(0))) { + opt.argObjects().remove(0); + } + List functions = getFunctions(opt); + + + if (elements == null || functions == null || functions.size() != 1) { + process.err().println("usage: each elements [do] { closure }"); + process.err().println(" elements: an array to iterate on"); + process.err().println(" closure: a function or closure to call"); + process.error(2); + return null; + } + + List args = new ArrayList<>(); + List results = new ArrayList<>(); + args.add(null); + + for (Object x : elements) { + checkInterrupt(); + args.set(0, x); + try { + results.add(functions.get(0).execute(session, args)); + } catch (BreakException b) { + break; + } catch (ContinueException c) { + // Ignore + } + } + + return opt.isSet("result") ? results : null; + } + + protected Object doIf(CommandSession session, Process process, Object[] argv) throws Exception { + String[] usage = { + "if - if / then / else construct", + "Usage: if {condition} [then] {if-action} [elif {cond} [then] {elif-action}]... [else] {else-action}", + " -? --help Show help", + }; + Options opt = parseOptions(session, usage, argv); + + List conditions = new ArrayList<>(); + List actions = new ArrayList<>(); + Function elseFunction = null; + int step = 0; + boolean error = false; + for (Object obj : opt.argObjects()) { + switch (step) { + case 0: + if (obj instanceof Function) { + conditions.add((Function) obj); + } else { + error = true; + } + step = 1; + break; + case 1: + if ("then".equals(obj)) { + step = 2; + break; + } + case 2: + if (obj instanceof Function) { + actions.add((Function) obj); + step = 3; + } else { + error = true; + } + break; + case 3: + if ("elif".equals(obj)) { + step = 4; + } else if ("else".equals(obj)) { + step = 7; + } else if (obj instanceof Function) { + elseFunction = (Function) obj; + step = 8; + } else { + error = true; + } + break; + case 4: + if (obj instanceof Function) { + conditions.add((Function) obj); + } else { + error = true; + } + step = 5; + break; + case 5: + if ("then".equals(obj)) { + step = 6; + break; + } + case 6: + if (obj instanceof Function) { + actions.add((Function) obj); + step = 3; + } else { + error = true; + } + break; + case 7: + if (obj instanceof Function) { + elseFunction = (Function) obj; + step = 8; + } else { + error = true; + } + break; + case 8: + error = true; + break; + } + if (error) { + break; + } + } + error |= conditions.isEmpty(); + error |= conditions.size() != actions.size(); + + if (error) { + process.err().println("usage: if {condition} [then] {if-action} [elif {elif-action}]... [else] {else-action}"); + process.error(2); + return null; + } + for (int i = 0, length = conditions.size(); i < length; ++i) { + if (isTrue(session, conditions.get(i))) { + return actions.get(i).execute(session, null); + } + } + if (elseFunction != null) { + return elseFunction.execute(session, null); + } + return null; + } + + protected Boolean doNot(CommandSession session, Process process, Object[] argv) throws Exception { + String[] usage = { + "not - return the opposite condition", + "Usage: not { condition }", + " -? --help Show help", + }; + Options opt = parseOptions(session, usage, argv); + List functions = getFunctions(opt); + if (functions == null || functions.size() != 1) { + process.err().println("usage: not { condition }"); + process.error(2); + return null; + } + return !isTrue(session, functions.get(0)); + + } + + protected Object doThrow(CommandSession session, Process process, Object[] argv) throws ThrownException, HelpException, OptionException { + String[] usage = { + "throw - throw an exception", + "Usage: throw [ message [ cause ] ]", + " throw exception", + " throw", + " -? --help Show help", + }; + Options opt = parseOptions(session, usage, argv); + if (opt.argObjects().size() == 0) { + Object exception = session.get("exception"); + if (exception instanceof Throwable) + throw new ThrownException((Throwable) exception); + else + throw new ThrownException(new Exception()); + } + else if (opt.argObjects().size() == 1 && opt.argObjects().get(0) instanceof Throwable) { + throw new ThrownException((Throwable) opt.argObjects().get(0)); + } + else { + String message = opt.argObjects().get(0).toString(); + Throwable cause = null; + if (opt.argObjects().size() > 1) { + if (opt.argObjects().get(1) instanceof Throwable) { + cause = (Throwable) opt.argObjects().get(1); + } + } + throw new ThrownException(new Exception(message, cause)); + } + } + + protected Object doTry(CommandSession session, Process process, Object[] argv) throws Exception { + String[] usage = { + "try - try / catch / finally construct", + "Usage: try { try-action } [ [catch] { catch-action } [ [finally] { finally-action } ] ]", + " -? --help Show help", + }; + Options opt = parseOptions(session, usage, argv); + Function tryAction = null; + Function catchFunction = null; + Function finallyFunction = null; + int step = 0; + boolean error = false; + for (Object obj : opt.argObjects()) { + if (tryAction == null) { + if (obj instanceof Function) { + tryAction = (Function) obj; + } else { + error = true; + break; + } + step = 1; + } else if ("catch".equals(obj)) { + if (step != 1) { + error = true; + break; + } + step = 2; + } else if ("finally".equals(obj)) { + if (step != 1 && step != 3) { + error = true; + break; + } + step = 4; + } else if (step == 1 || step == 2) { + if (obj instanceof Function) { + catchFunction = (Function) obj; + } else { + error = true; + break; + } + step = 3; + } else if (step == 3 || step == 4) { + if (obj instanceof Function) { + finallyFunction = (Function) obj; + } else { + error = true; + break; + } + step = 5; + } else { + error = true; + break; + } + } + error |= tryAction == null; + error |= catchFunction == null && finallyFunction == null; + + if (error) { + process.err().println("usage: try { try-action } [ [catch] { catch-action } [ [finally] { finally-action } ] ]"); + process.error(2); + return null; + } + try { + return tryAction.execute(session, null); + } catch (BreakException b) { + throw b; + } catch (Exception e) { + session.put("exception", e); + if (catchFunction != null) { + catchFunction.execute(session, null); + } + return null; + } finally { + if (finallyFunction != null) { + finallyFunction.execute(session, null); + } + } + } + + protected Object doWhile(CommandSession session, Process process, Object[] argv) throws Exception { + String[] usage = { + "while - while loop", + "Usage: while { condition } [do] { action }", + " -? --help Show help", + }; + Options opt = parseOptions(session, usage, argv); + Function condition = null; + Function action = null; + int step = 0; + boolean error = false; + for (Object obj : opt.argObjects()) { + if (condition == null) { + if (obj instanceof Function) { + condition = (Function) obj; + } else { + error = true; + break; + } + step = 1; + } else if ("do".equals(obj)) { + if (step != 1) { + error = true; + break; + } + step = 2; + } else if (step == 1 || step == 2) { + if (obj instanceof Function) { + action = (Function) obj; + } else { + error = true; + break; + } + step = 3; + } else { + error = true; + break; + } + } + error |= condition == null; + error |= action == null; + + if (error) { + process.err().println("usage: while { condition } [do] { action }"); + process.error(2); + return null; + } + while (isTrue(session, condition)) { + try { + action.execute(session, null); + } catch (BreakException b) { + break; + } catch (ContinueException c) { + // Ignore + } + } + return null; + } + + protected Object doUntil(CommandSession session, Process process, Object[] argv) throws Exception { + String[] usage = { + "until - until loop", + "Usage: until { condition } [do] { action }", + " -? --help Show help", + }; + Options opt = parseOptions(session, usage, argv); + Function condition = null; + Function action = null; + int step = 0; + boolean error = false; + for (Object obj : opt.argObjects()) { + if (condition == null) { + if (obj instanceof Function) { + condition = (Function) obj; + } else { + error = true; + break; + } + step = 1; + } else if ("do".equals(obj)) { + if (step != 1) { + error = true; + break; + } + step = 2; + } else if (step == 1 || step == 2) { + if (obj instanceof Function) { + action = (Function) obj; + } else { + error = true; + break; + } + step = 3; + } else { + error = true; + break; + } + } + error |= condition == null; + error |= action == null; + + if (error) { + process.err().println("usage: until { condition } [do] { action }"); + process.error(2); + return null; + } + while (!isTrue(session, condition)) { + try { + action.execute(session, null); + } catch (BreakException e) { + break; + } catch (ContinueException c) { + // Ignore + } + } + return null; + } + + protected Object doBreak(CommandSession session, Process process, Object[] argv) throws Exception { + String[] usage = { + "break - break from loop", + "Usage: break", + " -? --help Show help", + }; + parseOptions(session, usage, argv); + throw new BreakException(); + } + + protected Object doContinue(CommandSession session, Process process, Object[] argv) throws Exception { + String[] usage = { + "continue - continue loop", + "Usage: continue", + " -? --help Show help", + }; + parseOptions(session, usage, argv); + throw new ContinueException(); + } + + private boolean isTrue(CommandSession session, Function function) throws Exception { + checkInterrupt(); + return isTrue(function.execute(session, null)); + } + + private boolean isTrue(Object result) throws InterruptedException { + checkInterrupt(); + + if (result == null) + return false; + + if (result instanceof Boolean) + return (Boolean) result; + + if (result instanceof Number) { + if (0 == ((Number) result).intValue()) + return false; + } + + if ("".equals(result)) + return false; + + return !"0".equals(result); + } + + private void checkInterrupt() throws InterruptedException { + if (Thread.currentThread().isInterrupted()) + throw new InterruptedException("interrupted"); + } + + @SuppressWarnings("unchecked") + private Collection getElements(Options opt) { + Collection elements = null; + if (opt.argObjects().size() > 0) { + Object o = opt.argObjects().remove(0); + if (o instanceof Collection) { + elements = (Collection) o; + } else if (o != null && o.getClass().isArray()) { + elements = Arrays.asList((Object[]) o); + } + } + return elements; + } + + private List getFunctions(Options opt) { + List functions = new ArrayList<>(); + for (Object o : opt.argObjects()) { + if (o instanceof Function) { + functions.add((Function) o); + } + else { + functions = null; + break; + } + } + return functions; + } + +} diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Shell.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Shell.java new file mode 100644 index 00000000000..eaf09011026 --- /dev/null +++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Shell.java @@ -0,0 +1,728 @@ +/* + * 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.gogo.jline; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.nio.CharBuffer; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.felix.gogo.runtime.Closure; +import org.apache.felix.gogo.runtime.CommandProxy; +import org.apache.felix.gogo.runtime.CommandSessionImpl; +import org.apache.felix.service.command.Job; +import org.apache.felix.service.command.Job.Status; +import org.apache.felix.gogo.runtime.Reflective; +import org.apache.felix.service.command.CommandProcessor; +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Converter; +import org.apache.felix.service.command.Descriptor; +import org.apache.felix.service.command.Function; +import org.apache.felix.service.command.Parameter; +import org.apache.felix.service.threadio.ThreadIO; +import org.jline.builtins.Completers.CompletionData; +import org.jline.builtins.Completers.CompletionEnvironment; +import org.jline.builtins.Options; +import org.jline.reader.EndOfFileException; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.ParsedLine; +import org.jline.reader.UserInterruptException; +import org.jline.terminal.Terminal; +import org.jline.terminal.Terminal.Signal; +import org.jline.terminal.Terminal.SignalHandler; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.AttributedStyle; + +public class Shell { + + public static final String VAR_COMPLETIONS = ".completions"; + public static final String VAR_COMMAND_LINE = ".commandLine"; + public static final String VAR_READER = ".reader"; + public static final String VAR_SESSION = ".session"; + public static final String VAR_PROCESSOR = ".processor"; + public static final String VAR_TERMINAL = ".terminal"; + public static final String VAR_EXCEPTION = "exception"; + public static final String VAR_RESULT = "_"; + public static final String VAR_LOCATION = ".location"; + public static final String VAR_PROMPT = "prompt"; + public static final String VAR_RPROMPT = "rprompt"; + public static final String VAR_SCOPE = "SCOPE"; + public static final String VAR_CONTEXT = org.apache.felix.gogo.runtime.activator.Activator.CONTEXT; + + static final String[] functions = {"gosh", "sh", "source", "help"}; + + private final URI baseURI; + private final String profile; + private final Context context; + private final CommandProcessor processor; + private final ThreadIO tio; + + private AtomicBoolean stopping = new AtomicBoolean(); + + public Shell(Context context, CommandProcessor processor) { + this(context, processor, null); + } + + public Shell(Context context, CommandProcessor processor, String profile) { + this(context, processor, null, profile); + } + + public Shell(Context context, CommandProcessor processor, ThreadIO tio, String profile) { + this.context = context; + this.processor = processor; + this.tio = tio != null ? tio : getThreadIO(processor); + String baseDir = context.getProperty("gosh.home"); + baseDir = (baseDir == null) ? context.getProperty("user.dir") : baseDir; + this.baseURI = new File(baseDir).toURI(); + this.profile = profile != null ? profile : "gosh_profile"; + } + + private ThreadIO getThreadIO(CommandProcessor processor) { + try { + Field field = processor.getClass().getDeclaredField("threadIO"); + field.setAccessible(true); + return (ThreadIO) field.get(processor); + } catch (Throwable t) { + return null; + } + } + + public Context getContext() { + return context; + } + + public static Terminal getTerminal(CommandSession session) { + return (Terminal) session.get(VAR_TERMINAL); + } + + public static LineReader getReader(CommandSession session) { + return (LineReader) session.get(VAR_READER); + } + + public static CommandProcessor getProcessor(CommandSession session) { + return (CommandProcessor) session.get(VAR_PROCESSOR); + } + + @SuppressWarnings("unchecked") + public static Map> getCompletions(CommandSession session) { + return (Map>) session.get(VAR_COMPLETIONS); + } + + @SuppressWarnings("unchecked") + public static Set getCommands(CommandSession session) { + return (Set) session.get(CommandSessionImpl.COMMANDS); + } + + public static ParsedLine getParsedLine(CommandSession session) { + return (ParsedLine) session.get(VAR_COMMAND_LINE); + } + + public static String getPrompt(CommandSession session) { + return expand(session, VAR_PROMPT, "gl! "); + } + + public static String getRPrompt(CommandSession session) { + return expand(session, VAR_RPROMPT, null); + } + + public static String expand(CommandSession session, String name, String def) { + Object prompt = session.get(name); + if (prompt != null) { + try { + Object o = org.apache.felix.gogo.runtime.Expander.expand( + prompt.toString(), + new Closure((CommandSessionImpl) session, null, null)); + if (o != null) { + return o.toString(); + } + } catch (Exception e) { + // ignore + } + } + return def; + } + + public static String resolve(CommandSession session, String command) { + String resolved = command; + if (command.indexOf(':') < 0) { + Set commands = getCommands(session); + Object path = session.get(VAR_SCOPE); + String scopePath = (null == path ? "*" : path.toString()); + for (String scope : scopePath.split(":")) { + for (String entry : commands) { + if ("*".equals(scope) && entry.endsWith(":" + command) + || entry.equals(scope + ":" + command)) { + resolved = entry; + break; + } + } + } + } + return resolved; + } + + public static CharSequence readScript(URI script) throws Exception { + CharBuffer buf = CharBuffer.allocate(4096); + StringBuilder sb = new StringBuilder(); + + URLConnection conn = script.toURL().openConnection(); + + try (InputStreamReader in = new InputStreamReader(conn.getInputStream())) + { + while (in.read(buf) > 0) + { + buf.flip(); + sb.append(buf); + buf.clear(); + } + } + finally + { + if (conn instanceof HttpURLConnection) + { + ((HttpURLConnection) conn).disconnect(); + } + } + + return sb; + } + + @SuppressWarnings("unchecked") + static Set getVariables(CommandSession session) { + return (Set) session.get(".variables"); + } + + private static T findAnnotation(Annotation[] anns, + Class clazz) { + for (int i = 0; (anns != null) && (i < anns.length); i++) { + if (clazz.isInstance(anns[i])) { + return clazz.cast(anns[i]); + } + } + return null; + } + + public void stop() { + stopping.set(true); + } + + public Object gosh(CommandSession currentSession, String[] argv) throws Exception { + final String[] usage = { + "gosh - execute script with arguments in a new session", + " args are available as session variables $1..$9 and $args.", + "Usage: gosh [OPTIONS] [script-file [args..]]", + " -c --command pass all remaining args to sub-shell", + " --nointeractive don't start interactive session", + " --nohistory don't save the command history", + " --login login shell (same session, reads etc/gosh_profile)", + " -s --noshutdown don't shutdown framework when script completes", + " -x --xtrace echo commands before execution", + " -? --help show help", + "If no script-file, an interactive shell is started, type $D to exit."}; + + Options opt = Options.compile(usage).setOptionsFirst(true).parse(argv); + List args = opt.args(); + + boolean login = opt.isSet("login"); + boolean interactive = !opt.isSet("nointeractive"); + + if (opt.isSet("help")) { + opt.usage(System.err); + if (login && !opt.isSet("noshutdown")) { + shutdown(); + } + return null; + } + + if (opt.isSet("command") && args.isEmpty()) { + throw opt.usageError("option --command requires argument(s)"); + } + + CommandSession session; + if (login) { + session = currentSession; + } else { + session = createChildSession(currentSession); + } + + if (opt.isSet("xtrace")) { + session.put("echo", true); + } + + Terminal terminal = getTerminal(session); + session.put(Shell.VAR_CONTEXT, context); + session.put(Shell.VAR_PROCESSOR, processor); + session.put(Shell.VAR_SESSION, session); + session.put("#TERM", (Function) (s, arguments) -> terminal.getType()); + session.put("#COLUMNS", (Function) (s, arguments) -> terminal.getWidth()); + session.put("#LINES", (Function) (s, arguments) -> terminal.getHeight()); + session.put("#PWD", (Function) (s, arguments) -> s.currentDir().toString()); + if (!opt.isSet("nohistory")) { + session.put(LineReader.HISTORY_FILE, Paths.get(System.getProperty("user.home"), ".gogo.history")); + } + + if (tio != null) { + PrintWriter writer = terminal.writer(); + PrintStream out = new PrintStream(new OutputStream() { + @Override + public void write(int b) throws IOException { + write(new byte[]{(byte) b}, 0, 1); + } + public void write(byte b[], int off, int len) { + writer.write(new String(b, off, len)); + } + public void flush() { + writer.flush(); + } + public void close() { + writer.close(); + } + }); + tio.setStreams(terminal.input(), out, out); + } + + try { + LineReader reader; + if (args.isEmpty() && interactive) { + CompletionEnvironment completionEnvironment = new CompletionEnvironment() { + @Override + public Map> getCompletions() { + return Shell.getCompletions(session); + } + + @Override + public Set getCommands() { + return Shell.getCommands(session); + } + + @Override + public String resolveCommand(String command) { + return Shell.resolve(session, command); + } + + @Override + public String commandName(String command) { + int idx = command.indexOf(':'); + return idx >= 0 ? command.substring(idx + 1) : command; + } + + @Override + public Object evaluate(LineReader reader, ParsedLine line, String func) throws Exception { + session.put(Shell.VAR_COMMAND_LINE, line); + return session.execute(func); + } + }; + reader = LineReaderBuilder.builder() + .terminal(terminal) + .variables(((CommandSessionImpl) session).getVariables()) + .completer(new org.jline.builtins.Completers.Completer(completionEnvironment)) + .highlighter(new Highlighter(session)) + .parser(new Parser()) + .expander(new Expander(session)) + .build(); + reader.setOpt(LineReader.Option.AUTO_FRESH_LINE); + session.put(Shell.VAR_READER, reader); + session.put(Shell.VAR_COMPLETIONS, new HashMap<>()); + } else { + reader = null; + } + + if (login || interactive) { + URI uri = baseURI.resolve("etc/" + profile); + if (!new File(uri).exists()) { + URL url = getClass().getResource("/ext/" + profile); + if (url == null) { + url = getClass().getResource("/" + profile); + } + uri = (url == null) ? null : url.toURI(); + } + if (uri != null) { + source(session, uri.toString()); + } + } + + Object result = null; + + if (args.isEmpty()) { + if (interactive) { + result = runShell(session, terminal, reader); + } + } else { + CharSequence program; + + if (opt.isSet("command")) { + StringBuilder buf = new StringBuilder(); + for (String arg : args) { + if (buf.length() > 0) { + buf.append(' '); + } + buf.append(arg); + } + program = buf; + } else { + URI script = session.currentDir().toUri().resolve(args.remove(0)); + + // set script arguments + session.put("0", script); + session.put("args", args); + + for (int i = 0; i < args.size(); ++i) { + session.put(String.valueOf(i + 1), args.get(i)); + } + + program = readScript(script); + } + + result = session.execute(program); + } + + if (login && interactive && !opt.isSet("noshutdown")) { + if (terminal != null) { + terminal.writer().println("gosh: stopping framework"); + terminal.flush(); + } + shutdown(); + } + + return result; + } finally { + if (tio != null) { + tio.close(); + } + } + } + + private CommandSession createChildSession(CommandSession parent) { + CommandSession session = processor.createSession(parent); + getVariables(parent).stream() + .filter(key -> key.matches("[.]?[A-Z].*")) + .forEach(key -> session.put(key, parent.get(key))); + session.put(Shell.VAR_TERMINAL, getTerminal(parent)); + return session; + } + + private Object runShell(final CommandSession session, Terminal terminal, + LineReader reader) throws InterruptedException { + AtomicBoolean reading = new AtomicBoolean(); + session.setJobListener((job, previous, current) -> { + if (previous == Status.Background || current == Status.Background + || previous == Status.Suspended || current == Status.Suspended) { + int width = terminal.getWidth(); + String status = current.name().toLowerCase(); + terminal.writer().write(getStatusLine(job, width, status)); + terminal.flush(); + if (reading.get() && !stopping.get()) { + reader.callWidget(LineReader.REDRAW_LINE); + reader.callWidget(LineReader.REDISPLAY); + } + } + }); + SignalHandler intHandler = terminal.handle(Signal.INT, s -> { + Job current = session.foregroundJob(); + if (current != null) { + current.interrupt(); + } + }); + SignalHandler suspHandler = terminal.handle(Signal.TSTP, s -> { + Job current = session.foregroundJob(); + if (current != null) { + current.suspend(); + } + }); + Object result = null; + try { + while (!stopping.get()) { + try { + reading.set(true); + try { + String prompt = Shell.getPrompt(session); + String rprompt = Shell.getRPrompt(session); + if (stopping.get()) { + break; + } + reader.readLine(prompt, rprompt, (Character) null, null); + } finally { + reading.set(false); + } + ParsedLine parsedLine = reader.getParsedLine(); + if (parsedLine == null) { + throw new EndOfFileException(); + } + try { + result = session.execute(((ParsedLineImpl) parsedLine).program()); + session.put(Shell.VAR_RESULT, result); // set $_ to last result + + if (result != null && !Boolean.FALSE.equals(session.get(".Gogo.format"))) { + System.out.println(session.format(result, Converter.INSPECT)); + } + } catch (Exception e) { + AttributedStringBuilder sb = new AttributedStringBuilder(); + sb.style(sb.style().foreground(AttributedStyle.RED)); + sb.append(e.toString()); + sb.style(sb.style().foregroundDefault()); + terminal.writer().println(sb.toAnsi(terminal)); + terminal.flush(); + session.put(Shell.VAR_EXCEPTION, e); + } + + waitJobCompletion(session); + + } catch (UserInterruptException e) { + // continue; + } catch (EndOfFileException e) { + try { + reader.getHistory().save(); + } catch (IOException e1) { + // ignore + } + break; + } + } + } finally { + terminal.handle(Signal.INT, intHandler); + terminal.handle(Signal.TSTP, suspHandler); + } + return result; + } + + private void waitJobCompletion(final CommandSession session) throws InterruptedException { + while (true) { + Job job = session.foregroundJob(); + if (job != null) { + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (job) { + if (job.status() == Status.Foreground) { + job.wait(); + } + } + } else { + break; + } + } + } + + private String getStatusLine(Job job, int width, String status) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < width - 1; i++) { + sb.append(' '); + } + sb.append('\r'); + sb.append("[").append(job.id()).append("] "); + sb.append(status); + for (int i = status.length(); i < "background".length(); i++) { + sb.append(' '); + } + sb.append(" ").append(job.command()).append("\n"); + return sb.toString(); + } + + @Descriptor("start a new shell") + public Object sh(final CommandSession session, String[] argv) throws Exception { + return gosh(session, argv); + } + + private void shutdown() throws Exception { + context.exit(); + } + + @Descriptor("Evaluates contents of file") + public Object source(CommandSession session, String script) throws Exception { + URI uri = session.currentDir().toUri().resolve(script); + session.put("0", uri); + try { + return session.execute(readScript(uri)); + } finally { + session.put("0", null); // API doesn't support remove + } + } + + private Map> getReflectionCommands(CommandSession session) { + Map> commands = new TreeMap<>(); + Set names = getCommands(session); + for (String name : names) { + Function function = (Function) session.get(name); + if (function instanceof CommandProxy) { + Object target = ((CommandProxy) function).getTarget(); + List methods = new ArrayList<>(); + String func = name.substring(name.indexOf(':') + 1).toLowerCase(); + List funcs = new ArrayList<>(); + funcs.add("is" + func); + funcs.add("get" + func); + funcs.add("set" + func); + if (Reflective.KEYWORDS.contains(func)) { + funcs.add("_" + func); + } else { + funcs.add(func); + } + for (Method method : target.getClass().getMethods()) { + if (funcs.contains(method.getName().toLowerCase())) { + methods.add(method); + } + } + commands.put(name, methods); + ((CommandProxy) function).ungetTarget(); + } + } + return commands; + } + + @Descriptor("displays available commands") + public void help(CommandSession session) { + Map> commands = getReflectionCommands(session); + commands.keySet().forEach(System.out::println); + } + + @Descriptor("displays information about a specific command") + public void help(CommandSession session, @Descriptor("target command") String name) { + Map> commands = getReflectionCommands(session); + + List methods = null; + + // If the specified command doesn't have a scope, then + // search for matching methods by ignoring the scope. + int scopeIdx = name.indexOf(':'); + if (scopeIdx < 0) { + for (Entry> entry : commands.entrySet()) { + String k = entry.getKey().substring(entry.getKey().indexOf(':') + 1); + if (name.equals(k)) { + name = entry.getKey(); + methods = entry.getValue(); + break; + } + } + } + // Otherwise directly look up matching methods. + else { + methods = commands.get(name); + } + + if ((methods != null) && (methods.size() > 0)) { + for (Method m : methods) { + Descriptor d = m.getAnnotation(Descriptor.class); + if (d == null) { + System.out.println("\n" + m.getName()); + } else { + System.out.println("\n" + m.getName() + " - " + d.value()); + } + + System.out.println(" scope: " + name.substring(0, name.indexOf(':'))); + + // Get flags and options. + Class[] paramTypes = m.getParameterTypes(); + Map flags = new TreeMap<>(); + Map flagDescs = new TreeMap<>(); + Map options = new TreeMap<>(); + Map optionDescs = new TreeMap<>(); + List params = new ArrayList<>(); + Annotation[][] anns = m.getParameterAnnotations(); + for (int paramIdx = 0; paramIdx < anns.length; paramIdx++) { + Class paramType = m.getParameterTypes()[paramIdx]; + if (paramType == CommandSession.class) { + /* Do not bother the user with a CommandSession. */ + continue; + } + Parameter p = findAnnotation(anns[paramIdx], Parameter.class); + d = findAnnotation(anns[paramIdx], Descriptor.class); + if (p != null) { + if (p.presentValue().equals(Parameter.UNSPECIFIED)) { + options.put(p.names()[0], p); + if (d != null) { + optionDescs.put(p.names()[0], d.value()); + } + } else { + flags.put(p.names()[0], p); + if (d != null) { + flagDescs.put(p.names()[0], d.value()); + } + } + } else if (d != null) { + params.add(paramTypes[paramIdx].getSimpleName()); + params.add(d.value()); + } else { + params.add(paramTypes[paramIdx].getSimpleName()); + params.add(""); + } + } + + // Print flags and options. + if (flags.size() > 0) { + System.out.println(" flags:"); + for (Entry entry : flags.entrySet()) { + // Print all aliases. + String[] names = entry.getValue().names(); + System.out.print(" " + names[0]); + for (int aliasIdx = 1; aliasIdx < names.length; aliasIdx++) { + System.out.print(", " + names[aliasIdx]); + } + System.out.println(" " + flagDescs.get(entry.getKey())); + } + } + if (options.size() > 0) { + System.out.println(" options:"); + for (Entry entry : options.entrySet()) { + // Print all aliases. + String[] names = entry.getValue().names(); + System.out.print(" " + names[0]); + for (int aliasIdx = 1; aliasIdx < names.length; aliasIdx++) { + System.out.print(", " + names[aliasIdx]); + } + System.out.println(" " + + optionDescs.get(entry.getKey()) + + ((entry.getValue().absentValue() == null) ? "" + : " [optional]")); + } + } + if (params.size() > 0) { + System.out.println(" parameters:"); + for (Iterator it = params.iterator(); it.hasNext(); ) { + System.out.println(" " + it.next() + " " + it.next()); + } + } + } + } + } + + public interface Context { + String getProperty(String name); + + void exit() throws Exception; + } + +} diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/SingleServiceTracker.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/SingleServiceTracker.java new file mode 100644 index 00000000000..c9e4be86faf --- /dev/null +++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/SingleServiceTracker.java @@ -0,0 +1,191 @@ +/* + * 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.gogo.jline; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; + +//This is from aries util +public final class SingleServiceTracker implements ServiceListener { + + public interface SingleServiceListener { + void serviceFound(); + + void serviceLost(); + + void serviceReplaced(); + } + + private final BundleContext ctx; + private final String className; + private final AtomicReference service = new AtomicReference<>(); + private final AtomicReference> ref = new AtomicReference<>(); + private final AtomicBoolean open = new AtomicBoolean(false); + private final SingleServiceListener serviceListener; + private final String filterString; + private final Filter filter; + + public SingleServiceTracker(BundleContext context, Class clazz, SingleServiceListener sl) throws InvalidSyntaxException { + this(context, clazz, null, sl); + } + + public SingleServiceTracker(BundleContext context, Class clazz, String filterString, SingleServiceListener sl) throws InvalidSyntaxException { + this(context, clazz.getName(), filterString, sl); + } + + public SingleServiceTracker(BundleContext context, String className, String filterString, SingleServiceListener sl) throws InvalidSyntaxException { + this.ctx = context; + this.className = className; + this.serviceListener = sl; + if (filterString == null || filterString.isEmpty()) { + this.filterString = null; + this.filter = null; + } else { + this.filterString = filterString; + this.filter = context.createFilter(filterString); + } + } + + public T getService() { + return service.get(); + } + + public ServiceReference getServiceReference() { + return ref.get(); + } + + public void open() { + if (open.compareAndSet(false, true)) { + try { + String filterString = '(' + Constants.OBJECTCLASS + '=' + className + ')'; + if (filter != null) filterString = "(&" + filterString + filter + ')'; + ctx.addServiceListener(this, filterString); + findMatchingReference(null); + } catch (InvalidSyntaxException e) { + // this can never happen. (famous last words :) + } + } + } + + public void serviceChanged(ServiceEvent event) { + if (open.get()) { + if (event.getType() == ServiceEvent.UNREGISTERING) { + ServiceReference deadRef = event.getServiceReference(); + if (deadRef.equals(ref.get())) { + findMatchingReference(deadRef); + } + } else if (event.getType() == ServiceEvent.REGISTERED && ref.get() == null) { + findMatchingReference(null); + } + } + } + + private void findMatchingReference(ServiceReference original) { + try { + boolean clear = true; + ServiceReference[] refs = ctx.getServiceReferences(className, filterString); + if (refs != null && refs.length > 0) { + if (refs.length > 1) { + Arrays.sort(refs); + } + @SuppressWarnings("unchecked") + T service = (T) ctx.getService(refs[0]); + if (service != null) { + clear = false; + + // We do the unget out of the lock so we don't exit this class while holding a lock. + if (!update(original, refs[0], service)) { + ctx.ungetService(refs[0]); + } + } + } else if (original == null) { + clear = false; + } + + if (clear) { + update(original, null, null); + } + } catch (InvalidSyntaxException e) { + // this can never happen. (famous last words :) + } + } + + private boolean update(ServiceReference deadRef, ServiceReference newRef, T service) { + boolean result = false; + int foundLostReplaced = -1; + + // Make sure we don't try to get a lock on null + Object lock; + + // we have to choose our lock. + if (newRef != null) lock = newRef; + else if (deadRef != null) lock = deadRef; + else lock = this; + + // This lock is here to ensure that no two threads can set the ref and service + // at the same time. + synchronized (lock) { + if (open.get()) { + result = this.ref.compareAndSet(deadRef, newRef); + if (result) { + this.service.set(service); + + if (deadRef == null && newRef != null) foundLostReplaced = 0; + if (deadRef != null && newRef == null) foundLostReplaced = 1; + if (deadRef != null && newRef != null) foundLostReplaced = 2; + } + } + } + + if (serviceListener != null) { + if (foundLostReplaced == 0) serviceListener.serviceFound(); + else if (foundLostReplaced == 1) serviceListener.serviceLost(); + else if (foundLostReplaced == 2) serviceListener.serviceReplaced(); + } + + return result; + } + + public void close() { + if (open.compareAndSet(true, false)) { + ctx.removeServiceListener(this); + + ServiceReference deadRef; + synchronized (this) { + deadRef = ref.getAndSet(null); + service.set(null); + } + if (deadRef != null) { + serviceListener.serviceLost(); + ctx.ungetService(deadRef); + } + } + } +} \ No newline at end of file diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/package-info.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/package-info.java new file mode 100644 index 00000000000..7a33720b106 --- /dev/null +++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/package-info.java @@ -0,0 +1,34 @@ +/* + * 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. + */ + +@org.osgi.annotation.bundle.Capability( + attribute = "implementation.name=gogo.jline", + namespace = "org.apache.felix.gogo", + name = "shell.implementation", + version = "1.0.0" +) +@Requirement( + effective = "active", + namespace = "org.apache.felix.gogo", + name = "command.implementation", + version = "1.0.0" +) +package org.apache.felix.gogo.jline; + +import org.osgi.annotation.bundle.Requirement; diff --git a/gogo/jline/src/main/resources/gosh_profile b/gogo/jline/src/main/resources/gosh_profile new file mode 100644 index 00000000000..64061d8612f --- /dev/null +++ b/gogo/jline/src/main/resources/gosh_profile @@ -0,0 +1,344 @@ +# +# 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. +# +# default gosh_profile +# only read if etc/gosh_profile doesn't exist relative to the System property +# gosh.home or failing that the current directory. + +# catch all exceptions from this script to avoid it aborting startup +try { + + # ensure gogo commands are found first + SCOPE = 'gogo:*' + + # add methods on BundleContext object as commands + addcommand context ${.context} + + # add methods on System object as commands + addcommand system (((${.context} getClass) getClassLoader) loadClass java.lang.System) + + # alias to print full stack trace + e = { $exception printStackTrace } + + ## disable console auto-formatting of each result + # you will then need to explicitly use the 'format' command + # to print the result of commands that don't write to stdout. + #.Gogo.format = false + + ## disable printing the formatted result of a command into pipelines + #.Format.Pipe = false + + # set prompt + prompt = 'g! ' + \#rprompt = { (new java.text.SimpleDateFormat \'$'\u001B\\[90m'\'HH:mm:ss) format (new Date) } + secondary-prompt-pattern = '%M%P > ' + # could also be written + # \#rprompt = { ${(qq)$(date +$'\u001B\[90m%T')} } + + + __option_not_present = { + res = true + opts = $argv + each $opts { + arg = $it + each ($.commandLine words) { + if { ($it toString) equals ($arg toString) } { + res = false + } + } + } + $res + } + + __load_class = { + (($.reader class) classLoader) loadClass $1 + } + + __set_unset_arguments = { + is_setopt = (($.commandLine words) get 0) equals "setopt" + enums = ((__load_class 'org.jline.reader.LineReader$Option') enumConstants) + candidates = new ArrayList + each $enums { + name = ${(GL)it/_/-} + is_set = ($.reader isSet $it) + neg = %(( if(is_setopt, is_set, not(is_set)) )) + if { $neg } { + name = "no-${name}" + } + if { not { (($.commandLine words) subList 1 ($.commandLine wordIndex)) contains $name } } { + $candidates add (new org.jline.reader.Candidate $name $name (if { $neg } { "unset" } { "set" }) null null null true) + } + } + $candidates + } + + setopt group + + complete -c gogo:complete -e + complete -c gogo:complete -d "Edit command specific completions" + complete -c gogo:complete -s c -l command --description "Command to add completion to" -n '__option_not_present -c --command' -a '$.commands' + complete -c gogo:complete -s s -l short-option --description "Posix-style option to complete" -n '__option_not_present -s --short-option' + complete -c gogo:complete -s l -l long-option --description "GNU-style option to complete" -n '__option_not_present -l --long-option' + complete -c gogo:complete -s a -l arguments --description "A list of possible arguments" -n '__option_not_present -a --argument' + complete -c gogo:complete -s d -l description --description "Description of this completions" -n '__option_not_present -d --description' + complete -c gogo:complete -s h -l help --description "Display help and exit" -n '__option_not_present -h --help' + complete -c gogo:complete -s n -l condition --description "The completion should only be used if the specified command has a zero exit status" -n '__option_not_present -n --condition' + complete -c gogo:complete -s e -l erase --description "Remove completion" -n '__option_not_present -e --erase' + + complete -c gogo:history -e + complete -c gogo:history -d "Show and manipulate command history" + complete -c gogo:history -l clear --description "Clear history" -n '__option_not_present --clear' + complete -c gogo:history -l save --description "Save history" -n '__option_not_present --save' + + complete -c gogo:setopt -e + complete -c gogo:setopt -d "Set or view set shell options" + complete -c gogo:setopt -a '__set_unset_arguments' + + complete -c gogo:unsetopt -e + complete -c gogo:unsetopt -d "Unset or view unset shell options" + complete -c gogo:unsetopt -a '__set_unset_arguments' + + complete -c gogo:cat -e + complete -c gogo:cat -d "Concatenate and print files" + complete -c gogo:cat -s n "Number the output lines, starting at 1" + complete -c gogo:cat -a '__files' + + complete -c gogo:pwd -e + complete -c gogo:pwd -d "Get current directory" + + complete -c gogo:ls -e + complete -c gogo:ls -d "List files" + + complete -c gogo:cd -e + complete -c gogo:cd -d "Change current directory" + complete -c gogo:cd -a 'wi = ($.commandLine wordIndex); if { %(wi==1) } { __directories } { [ ] }' + + complete -c gogo:sleep -e + complete -c gogo:sleep -d "Pause execution for the specified amount of time" + + complete -c gogo:echo -e + complete -c gogo:echo -d "Write arguments to the standard output" + complete -c gogo:echo -s n -d "No trailing new line" + + complete -c gogo:grep -e + complete -c gogo:grep -d "File pattern searcher" + # TODO + + complete -c gogo:sort -e + complete -c gogo:sort -d "Sort lines of text files" + # TODO + + complete -c gogo:gosh -e + complete -c gogo:gosh -d "Execute script with arguments in a new session" + # TODO + + complete -c gogo:sh -e + complete -c gogo:sh -d "Execute script with arguments in a new session" + # TODO + + complete -c gogo:source -e + complete -c gogo:source -d "Execute script with arguments" + # TODO + + # TODO: format getopt new set tac type addcommand removeCommand eval + + complete -c gogo:each -e + complete -c gogo:each -d "Loop and execute script on the specified elements" + + complete -c gogo:if -e + complete -c gogo:if -d "Conditionaly execute a script" + + complete -c gogo:not -e + complete -c gogo:not -d "Negates the result of a script" + + complete -c gogo:throw -e + complete -c gogo:throw -d "Throws an exception" + + complete -c gogo:try -e + complete -c gogo:try -d "Try executing a script and catch any exception" + + complete -c gogo:until -e + complete -c gogo:until -d "Loop and execute script until a condition is satisfied" + + complete -c gogo:while -e + complete -c gogo:while -d "Loop and execute script while a condition is satisfied" + + complete -c gogo:less -e + complete -c gogo:less -d "File pager" + complete -c gogo:less -s e -l quit-at-eof --description "Exit on second EOF" + complete -c gogo:less -s E -l QUIT-AT-EOF --description "Exit on EOF" + complete -c gogo:less -s q -l quiet -l silent --description "Silent mode" + complete -c gogo:less -s Q -l QUIET -l SILENT --description "Completely silent" + complete -c gogo:less -s S -l chop-long-lines --description "Do not fold long lines" + complete -c gogo:less -s i -l ignore-case --description "Search ignores lowercase case" + complete -c gogo:less -s I -l IGNORE-CASE --description "Search ignores all case" + complete -c gogo:less -s x -l tabs --description "Set tab stops" + complete -c gogo:less -s N -l LINE-NUMBERS --description "Display line number for each line" + complete -c gogo:less -a '__files' + + complete -c gogo:nano -e + complete -c gogo:nano -d "File editor" + complete -c gogo:nano -a '__files' + + complete -c gogo:keymap -e + complete -c gogo:keymap -d "Manipulate keymaps" + complete -c gogo:keymap -s N --description "Create a new keymap" -n '__option_not_present -N -d -D -l -r -s -A' + complete -c gogo:keymap -s d --description "Delete existing keymaps and reset to default state" -n '__option_not_present -N -d -D -l -r -s -A' + complete -c gogo:keymap -s D --description "Delete named keymaps" -n '__option_not_present -N -d -D -l -r -s -A' + complete -c gogo:keymap -s l --description "List existing keymap names" -n '__option_not_present -N -d -D -l -r -s -A' + complete -c gogo:keymap -s r --description "Unbind specified in-strings" -n '__option_not_present -N -d -D -l -r -s -A' + complete -c gogo:keymap -s s --description "Bind each in-string to each out-string" -n '__option_not_present -N -d -D -l -r -s -A' + complete -c gogo:keymap -s A --description "Create alias to keymap" -n '__option_not_present -N -d -D -l -r -s -A' + complete -c gogo:keymap -s e --description "Select emacs keymap and bind it to main" -n '__option_not_present -e -a -v -M' + complete -c gogo:keymap -s v --description "Select viins keymap and bind it to main" -n '__option_not_present -e -a -v -M' + complete -c gogo:keymap -s a --description "Select vicmd keymap" -n '__option_not_present -e -a -v -M' + complete -c gogo:keymap -s M --description "Specify keymap to select" -n '__option_not_present -e -a -v -M' -a '(keymap -l | tac) split " "' + complete -c gogo:keymap -s R --description "Interpret in-strings as ranges" + complete -c gogo:keymap -s p --description "List bindings which have given key sequence as a a prefix" + complete -c gogo:keymap -s L --description "Output in form of keymap commands" + + complete -c gogo:widget -e + complete -c gogo:widget -d "Manipulate widgets" + complete -c gogo:widget -s N --description "Create a new widget" -n '__option_not_present -N -A -D -U -l' + complete -c gogo:widget -s A --description "Create alias to widget" -n '__option_not_present -N -A -D -U -l' + complete -c gogo:widget -s D --description "Delete widgets" -n '__option_not_present -N -A -D -U -l' + complete -c gogo:widget -s U --description "Push characters to the stack" -n '__option_not_present -N -A -D -U -l' + complete -c gogo:widget -s l --description "List user-defined widgets" -n '__option_not_present -N -A -D -U -l' + complete -c gogo:widget -s a --description "With -l, list all widgets" -n '__option_not_present -l' + + complete -c gogo:telnetd -e + complete -c gogo:telnetd -d "Telnet daemon" + complete -c gogo:telnetd -s i -l ip --description "Listening IP interface" -n '__option_not_present -i --ip' + complete -c gogo:telnetd -s p -l port --description "Listening IP port" -n '__option_not_present -p --port' + complete -c gogo:telnetd -a '[start stop status]' + + complete -c gogo:sshd -e + complete -c gogo:sshd -d "SSH daemon" + complete -c gogo:sshd -s i -l ip --description "Listening IP interface" -n '__option_not_present -i --ip' + complete -c gogo:sshd -s p -l port --description "Listening IP port" -n '__option_not_present -p --port' + complete -c gogo:sshd -a '[start stop status]' + + complete -c gogo:tmux -e + complete -c gogo:tmux -d "Terminal multiplexer" + + complete -c gogo:bg -e + complete -c gogo:bg -d "Put job in background" + + complete -c gogo:fg -e + complete -c gogo:fg -d "Put job in foreground" + + complete -c gogo:jobs -e + complete -c gogo:jobs -d "List jobs" + + complete -c gogo:clear -e + complete -c gogo:clear -d "Clear screen" + + complete -c gogo:head -e + complete -c gogo:head -d "Displays first lines of file" + complete -c gogo:head -s n -l lines --description "Print line counts" + complete -c gogo:head -s c -l bytes --description "Print byte counts" + complete -c gogo:head -a '__files' + + complete -c gogo:tail -e + complete -c gogo:tail -d "Displays last lines of file" + complete -c gogo:tail -s q -l quiet --description "Suppress headers when printing multiple sources" + complete -c gogo:tail -s f -l follow --description "Do not stop at end of file" + complete -c gogo:tail -s F -l FOLLOW --description "Follow and check for file renaming or rotation" + complete -c gogo:tail -s n -l lines --description "Number of lines to print" + complete -c gogo:tail -s c -l bytes --description "Number of bytes to print" + complete -c gogo:tail -a '__files' + + complete -c gogo:date -e + complete -c gogo:date -d "Display date and time" + complete -c gogo:date -s u --description "Use UTC" + complete -c gogo:date -s r --description "Print the date represented by 'seconds' since January 1, 1970" + complete -c gogo:date -s v --description "Adjust date" + complete -c gogo:date -s f --description "Use 'input_fmt' to parse 'new_date'" + + complete -c gogo:wc -e + complete -c gogo:wc -d "Word, line, character, and byte count" + complete -c gogo:wc -s n -l lines --description "Print line count" + complete -c gogo:wc -s c -l bytes --description "Print byte count" + complete -c gogo:wc -s m -l chars --description "Print character count" + complete -c gogo:wc -s w -l words --description "Print word count" + complete -c gogo:wc -a '__files' + + __get_scr_components = { + list = [ ] + scrref = ($.context getServiceReference org.osgi.service.component.runtime.ServiceComponentRuntime) + scr = ($.context getService $scrref) + each ($scr getComponentDescriptionDTOs ($.context bundles)) { + $list add ((($it getClass) getField "name") get $it) + } + $.context ungetService $scrref + $list + } + __get_bundles_with_scr_components = { + list = [ ] + scrref = ($.context getServiceReference org.osgi.service.component.runtime.ServiceComponentRuntime) + scr = ($.context getService $scrref) + each ($.context bundles) { + if { ($scr getComponentDescriptionDTOs $it) isEmpty } { } { + $list add ($it symbolicName) + } + } + $.context ungetService $scrref + $list + } + + complete -c scr:config -e + complete -c scr:config -d "Show the current SCR configuration" + + complete -c scr:disable -e + complete -c scr:disable -d "Disable an enabled component" + complete -c scr:disable -a '__get_scr_components' + + complete -c scr:enable -e + complete -c scr:enable -d "Enable an disabled component" + complete -c scr:enable -a '__get_scr_components' + + complete -c scr:info -e + complete -c scr:info -d "Dump information of a component or component configuration" + complete -c scr:info -a '__get_scr_components' + + complete -c scr:list -e + complete -c scr:list -d "List component configurations of a specific bundle" + complete -c scr:list -a '__get_bundles_with_scr_components' + + # print welcome message + __resolve_uri = { + uri = $1 + path = $2 + if { "$uri" startsWith "jar:" } /* then */ { + idx = ("$uri" indexOf "!") + p1 = ("$uri" substring 0 $idx) + p2 = "!" + p3 = (new java.net.URI ("$uri" substring %(idx+1))) resolve $path + "$p1$p2$p3" + } /* else */ { + $uri resolve $path + } + } + + # print welcome message, unless we're explicitly told not to... + if { $.gosh_quiet } { } { cat (new java.net.URL ($0 toURL) motd) } +} { + echo "$0: ERROR: $exception" +} + +# end diff --git a/gogo/jline/src/main/resources/motd b/gogo/jline/src/main/resources/motd new file mode 100644 index 00000000000..01954f91c1a --- /dev/null +++ b/gogo/jline/src/main/resources/motd @@ -0,0 +1,3 @@ +____________________________ +Welcome to Apache Felix Gogo + diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/AbstractParserTest.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/AbstractParserTest.java new file mode 100644 index 00000000000..a785c2ec1c9 --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/AbstractParserTest.java @@ -0,0 +1,75 @@ +/* + * 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.gogo.jline; + +import java.io.FilterInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; + +import org.apache.felix.gogo.runtime.threadio.ThreadIOImpl; +import org.junit.After; +import org.junit.Before; + +public abstract class AbstractParserTest { + + private ThreadIOImpl threadIO; + private InputStream sin; + private PrintStream sout; + private PrintStream serr; + + @Before + public void setUp() { + sin = new NoCloseInputStream(System.in); + sout = new NoClosePrintStream(System.out); + serr = new NoClosePrintStream(System.err); + threadIO = new ThreadIOImpl(); + threadIO.start(); + } + + @After + public void tearDown() { + threadIO.stop(); + } + + public class Context extends org.apache.felix.gogo.jline.Context { + public Context() { + super(AbstractParserTest.this.threadIO, sin, sout, serr); + } + } + + private static class NoCloseInputStream extends FilterInputStream { + public NoCloseInputStream(InputStream in) { + super(in); + } + @Override + public void close() { + } + } + + private static class NoClosePrintStream extends PrintStream { + public NoClosePrintStream(OutputStream out) { + super(out); + } + @Override + public void close() { + } + } + +} diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/BaseConvertersTest.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/BaseConvertersTest.java new file mode 100644 index 00000000000..cae7a440117 --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/BaseConvertersTest.java @@ -0,0 +1,61 @@ +/* + * 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.gogo.jline; + +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Function; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class BaseConvertersTest { + + @Test + public void testFunctionProxy() throws Exception { + Function function = new Function() { + @Override + public Object execute(CommandSession session, List arguments) { + return "Hello "; + } + public String toString() { + return "MyFunction"; + } + }; + MyType myType = (MyType) new BaseConverters().convert(MyType.class, function); + assertEquals("MyFunction", myType.toString()); + assertEquals("Hello ", myType.run(null)); + assertEquals("World !", myType.hello()); + } + + @FunctionalInterface + public interface MyType { + + String toString(); + + Object run(List args); + + default String hello() { + return "World !"; + } + + } + +} diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/Context.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/Context.java new file mode 100644 index 00000000000..45040dca8f6 --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/Context.java @@ -0,0 +1,103 @@ +/* + * 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.gogo.jline; + +import java.io.InputStream; +import java.io.PrintStream; +import java.nio.file.Path; + +import org.apache.felix.gogo.runtime.CommandProcessorImpl; +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.threadio.ThreadIO; + +public class Context extends CommandProcessorImpl +{ + public static final String EMPTY = ""; + + private final CommandSession session; + + public Context(ThreadIO threadio, InputStream in, PrintStream out, PrintStream err) + { + super(threadio); + Shell shell = new Shell(new MyContext(), this); + addCommand("gogo", this, "addCommand"); + addCommand("gogo", this, "removeCommand"); + addCommand("gogo", this, "eval"); + register(this, new Builtin(), Builtin.functions); + register(this, new Procedural(), Procedural.functions); + register(this, new Posix(this), Posix.functions); + register(this, shell, Shell.functions); + session = createSession(in, out, err); + } + + static void register(CommandProcessorImpl processor, Object target, String[] functions) { + for (String function : functions) { + processor.addCommand("gogo", target, function); + } + } + + private static class MyContext implements Shell.Context { + + public String getProperty(String name) { + return System.getProperty(name); + } + + public void exit() { + System.exit(0); + } + } + + public Object execute(CharSequence source) throws Exception + { + Object result = new Exception(); + try + { + return result = session.execute(source); + } + finally + { + System.err.println("execute<" + source + "> = (" + + (null == result ? "Null" : result.getClass().getSimpleName()) + ")(" + + result + ")\n"); + } + } + + public void addCommand(String function, Object target) + { + addCommand("test", target, function); + } + + public Object set(String name, Object value) + { + return session.put(name, value); + } + + public Object get(String name) + { + return session.get(name); + } + + public void currentDir(Path path) { + session.currentDir(path); + } + + public Path currentDir() { + return session.currentDir(); + } +} diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ParserTest.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ParserTest.java new file mode 100644 index 00000000000..5d1990596bb --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ParserTest.java @@ -0,0 +1,59 @@ +/* + * 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.gogo.jline; + +import org.jline.reader.CompletingParsedLine; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class ParserTest { + + @Test + public void testEscapedWord() { + Parser parser = new Parser(); + CompletingParsedLine line = (CompletingParsedLine) parser.parse("foo second\\ param \"quoted param\"", 15); + assertNotNull(line); + assertNotNull(line.words()); + assertEquals("foo second\\ param \"quoted param\"", line.line()); + assertEquals(15, line.cursor()); + assertEquals(3, line.words().size()); + assertEquals("second param", line.word()); + assertEquals(10, line.wordCursor()); + assertEquals(11, line.rawWordCursor()); + assertEquals(13, line.rawWordLength()); + } + + @Test + public void testQuotedWord() { + Parser parser = new Parser(); + CompletingParsedLine line = (CompletingParsedLine) parser.parse("foo second\\ param \"quoted param\"", 20); + assertNotNull(line); + assertNotNull(line.words()); + assertEquals("foo second\\ param \"quoted param\"", line.line()); + assertEquals(20, line.cursor()); + assertEquals(3, line.words().size()); + assertEquals("quoted param", line.word()); + assertEquals(1, line.wordCursor()); + assertEquals(2, line.rawWordCursor()); + assertEquals(14, line.rawWordLength()); + } + +} diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/PosixTest.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/PosixTest.java new file mode 100644 index 00000000000..7317283a581 --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/PosixTest.java @@ -0,0 +1,114 @@ +/* + * 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.gogo.jline; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringWriter; + +import org.junit.Test; + +public class PosixTest extends AbstractParserTest { + + @Test + public void testGrepWithColoredInput() throws Exception { + Context context = new Context(); + context.addCommand("echo", new Posix(context)); + context.addCommand("grep", new Posix(context)); + context.addCommand("tac", this); + + Object res = context.execute("echo \" \\u001b[1mbold\\u001b[0m la\" | grep la | tac"); + assertEquals(" \u001b[1mbold\u001b[0m la", res); + } + + @Test + public void testLsDotDot() throws Exception { + Context context = new Context(); + context.addCommand("ls", new Posix(context)); + context.addCommand("tac", this); + + String current = (String) context.execute("ls -1 --color=never . | tac"); + assertTrue(current.indexOf("..") >= 0); + assertTrue(current.indexOf(".") >= 0); + + String parent = (String) context.execute("ls -1 --color=never .. | tac"); + assertTrue(parent.indexOf("..") >= 0); + assertTrue(parent.indexOf(".") >= 0); + + assertNotEquals(current, parent); + } + + @Test + public void testWcLines() throws Exception { + Context context = new Context(); + context.addCommand("echo", new Posix(context)); + context.addCommand("wc", new Posix(context)); + context.addCommand("tac", this); + + Object res = context.execute("echo \"test\" | wc -l | tac"); + assertEquals("1", res); + } + + @Test + public void testWcBytes() throws Exception { + Context context = new Context(); + context.addCommand("echo", new Posix(context)); + context.addCommand("wc", new Posix(context)); + context.addCommand("tac", this); + + Object res = context.execute("echo \"test\" | wc -c | tac"); + assertEquals("5", res); + } + + @Test + public void testWcLinesBytes() throws Exception { + Context context = new Context(); + context.addCommand("echo", new Posix(context)); + context.addCommand("wc", new Posix(context)); + context.addCommand("tac", this); + + Object res = context.execute("echo \"test\" | wc -l -c | tac"); + assertEquals(" 1 5", res); + } + + @Test + public void testWcLinesBytesChar() throws Exception { + Context context = new Context(); + context.addCommand("echo", new Posix(context)); + context.addCommand("wc", new Posix(context)); + context.addCommand("tac", this); + + Object res = context.execute("echo \"test\" | wc -l -c -m | tac"); + assertEquals(" 1 5 5", res); + } + + public String tac() throws IOException { + StringWriter sw = new StringWriter(); + Reader rdr = new InputStreamReader(System.in); + char[] buf = new char[1024]; + int len; + while ((len = rdr.read(buf)) >= 0) { + sw.write(buf, 0, len); + } + return sw.toString(); + } +} diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ShellTest.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ShellTest.java new file mode 100644 index 00000000000..74a3127dda8 --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ShellTest.java @@ -0,0 +1,54 @@ +/* + * 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.gogo.jline; + +import java.util.Arrays; + +import org.junit.Assert; +import org.junit.Test; + +public class ShellTest extends AbstractParserTest { + + @Test + public void testAssignmentWithEcho() throws Exception { + Context context = new Context(); + context.execute("a = \"foo\""); + Assert.assertEquals("foo", context.get("a")); + context.execute("a = $(echo bar)"); + Assert.assertEquals("bar", context.get("a")); + } + + @Test + public void testLoopBreak() throws Exception { + Context context = new Context(); + Object result = context.execute("$(each {1..10} { i = $it; if { %(i >= 5) } { break } ; echo $i })"); + Assert.assertEquals("1\n2\n3\n4", result); + } + + @Test + public void testJobIds() throws Exception { + Context context = new Context(); + // TODO: not than in zsh, the same thing is achieved using + // TODO: ${${${(@f)"$(jobs)"}%]*}#*\[} +// Object result = context.execute("sleep 1 & sleep 1 & ${${${(f)\"$(jobs)\"}%']*'}#'*\\['}"); + Object result = context.execute("sleep 1 & sleep 1 & ${${${(f)$(jobs)}%\\]*}#*\\[}"); + Assert.assertEquals(Arrays.asList("1", "2"), result); + } + +} diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ssh/ShellCommand.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ssh/ShellCommand.java new file mode 100644 index 00000000000..736cb4b9f1c --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ssh/ShellCommand.java @@ -0,0 +1,136 @@ +/* + * 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.gogo.jline.ssh; + +import java.io.CharArrayWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.Reader; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.felix.service.command.CommandProcessor; +import org.apache.felix.service.command.CommandSession; +import org.apache.sshd.server.Command; +import org.apache.sshd.server.Environment; +import org.apache.sshd.server.ExitCallback; +import org.apache.sshd.server.SessionAware; +import org.apache.sshd.server.session.ServerSession; + +public class ShellCommand implements Command, Runnable, SessionAware { + + public static final String SHELL_INIT_SCRIPT = "karaf.shell.init.script"; + public static final String EXEC_INIT_SCRIPT = "karaf.exec.init.script"; + + private static final Logger LOGGER = Logger.getLogger(ShellCommand.class.getName()); + + private String command; + private InputStream in; + private OutputStream out; + private OutputStream err; + private ExitCallback callback; + @SuppressWarnings("unused") + private ServerSession session; + private CommandProcessor processor; + private Environment env; + + public ShellCommand(CommandProcessor processor, String command) { + this.processor = processor; + this.command = command; + } + + public void setInputStream(InputStream in) { + this.in = in; + } + + public void setOutputStream(OutputStream out) { + this.out = out; + } + + public void setErrorStream(OutputStream err) { + this.err = err; + } + + public void setExitCallback(ExitCallback callback) { + this.callback = callback; + } + + public void setSession(ServerSession session) { + this.session = session; + } + + public void start(final Environment env) { + this.env = env; + new Thread(this).start(); + } + + public void run() { + int exitStatus = 0; + try { + final CommandSession session = processor.createSession(in, new PrintStream(out), new PrintStream(err)); + for (Map.Entry e : env.getEnv().entrySet()) { + session.put(e.getKey(), e.getValue()); + } + try { + String scriptFileName = System.getProperty(EXEC_INIT_SCRIPT); + if (scriptFileName == null) { + scriptFileName = System.getProperty(SHELL_INIT_SCRIPT); + } + executeScript(scriptFileName, session); + session.execute(command); + } catch (Throwable t) { + exitStatus = 1; + t.printStackTrace(); + } + } catch (Exception e) { + exitStatus = 1; + LOGGER.log(Level.SEVERE, "Unable to start shell", e); + } finally { + ShellFactoryImpl.close(in, out, err); + callback.onExit(exitStatus); + } + } + + public void destroy() { + } + + private void executeScript(String scriptFileName, CommandSession session) { + if (scriptFileName != null) { + File scriptFile = new File(scriptFileName); + try (Reader r = new InputStreamReader(new FileInputStream(scriptFile))) { + CharArrayWriter w = new CharArrayWriter(); + int n; + char[] buf = new char[8192]; + while ((n = r.read(buf)) > 0) { + w.write(buf, 0, n); + } + session.execute(new String(w.toCharArray())); + } catch (Exception e) { + LOGGER.log(Level.FINE, "Error in initialization script", e); + } + // Ignore + } + } + +} diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ssh/ShellCommandFactory.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ssh/ShellCommandFactory.java new file mode 100644 index 00000000000..d0a0a45a0fa --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ssh/ShellCommandFactory.java @@ -0,0 +1,37 @@ +/* + * 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.gogo.jline.ssh; + +import org.apache.felix.service.command.CommandProcessor; +import org.apache.sshd.server.Command; +import org.apache.sshd.server.CommandFactory; + +public class ShellCommandFactory implements CommandFactory { + + private CommandProcessor processor; + + public ShellCommandFactory(CommandProcessor processor) { + this.processor = processor; + } + + public Command createCommand(String command) { + return new ShellCommand(processor, command); + } + +} diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ssh/ShellFactoryImpl.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ssh/ShellFactoryImpl.java new file mode 100644 index 00000000000..91c40e7e574 --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ssh/ShellFactoryImpl.java @@ -0,0 +1,274 @@ +/* + * 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.gogo.jline.ssh; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.Map; + +import org.apache.felix.gogo.jline.Shell; +import org.apache.felix.gogo.jline.Shell.Context; +import org.apache.felix.service.command.CommandProcessor; +import org.apache.felix.service.command.CommandSession; +import org.apache.sshd.common.Factory; +import org.apache.sshd.common.channel.PtyMode; +import org.apache.sshd.server.Command; +import org.apache.sshd.server.Environment; +import org.apache.sshd.server.ExitCallback; +import org.apache.sshd.server.SessionAware; +import org.apache.sshd.server.Signal; +import org.apache.sshd.server.session.ServerSession; +import org.jline.terminal.Attributes; +import org.jline.terminal.Attributes.ControlChar; +import org.jline.terminal.Attributes.InputFlag; +import org.jline.terminal.Attributes.LocalFlag; +import org.jline.terminal.Attributes.OutputFlag; +import org.jline.terminal.Size; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; + +/** + * SSHD {@link org.apache.sshd.server.Command} factory which provides access to + * Shell. + */ +public class ShellFactoryImpl implements Factory { + private final CommandProcessor processor; + + public ShellFactoryImpl(CommandProcessor processor) { + this.processor = processor; + } + + private static void flush(OutputStream... streams) { + for (OutputStream s : streams) { + try { + s.flush(); + } catch (IOException e) { + // Ignore + } + } + } + + static void close(Closeable... closeables) { + for (Closeable c : closeables) { + try { + c.close(); + } catch (IOException e) { + // Ignore + } + } + } + + public Command create() { + return new ShellImpl(); + } + + public class ShellImpl implements Command, SessionAware { + private InputStream in; + + private OutputStream out; + + private OutputStream err; + + private ExitCallback callback; + + @SuppressWarnings("unused") + private ServerSession session; + + private boolean closed; + + public void setInputStream(final InputStream in) { + this.in = in; + } + + public void setOutputStream(final OutputStream out) { + this.out = out; + } + + public void setErrorStream(final OutputStream err) { + this.err = err; + } + + public void setExitCallback(ExitCallback callback) { + this.callback = callback; + } + + public void setSession(ServerSession session) { + this.session = session; + } + + public void start(final Environment env) throws IOException { + try { + new Thread(() -> { + try { + ShellImpl.this.run(env); + } catch (Throwable t) { + t.printStackTrace(); + } + }).start(); + } catch (Exception e) { + throw (IOException) new IOException("Unable to start shell", e); + } + } + + public void run(Environment env) { + try { + Terminal terminal = TerminalBuilder.builder() + .name("gogo") + .type(env.getEnv().get("TERM")) + .system(false) + .streams(in, out) + .build(); + terminal.setSize(new Size(Integer.parseInt(env.getEnv().get("COLUMNS")), + Integer.parseInt(env.getEnv().get("LINES")))); + Attributes attr = terminal.getAttributes(); + for (Map.Entry e : env.getPtyModes().entrySet()) { + switch (e.getKey()) { + case VINTR: + attr.setControlChar(ControlChar.VINTR, e.getValue()); + break; + case VQUIT: + attr.setControlChar(ControlChar.VQUIT, e.getValue()); + break; + case VERASE: + attr.setControlChar(ControlChar.VERASE, e.getValue()); + break; + case VKILL: + attr.setControlChar(ControlChar.VKILL, e.getValue()); + break; + case VEOF: + attr.setControlChar(ControlChar.VEOF, e.getValue()); + break; + case VEOL: + attr.setControlChar(ControlChar.VEOL, e.getValue()); + break; + case VEOL2: + attr.setControlChar(ControlChar.VEOL2, e.getValue()); + break; + case VSTART: + attr.setControlChar(ControlChar.VSTART, e.getValue()); + break; + case VSTOP: + attr.setControlChar(ControlChar.VSTOP, e.getValue()); + break; + case VSUSP: + attr.setControlChar(ControlChar.VSUSP, e.getValue()); + break; + case VDSUSP: + attr.setControlChar(ControlChar.VDSUSP, e.getValue()); + break; + case VREPRINT: + attr.setControlChar(ControlChar.VREPRINT, e.getValue()); + break; + case VWERASE: + attr.setControlChar(ControlChar.VWERASE, e.getValue()); + break; + case VLNEXT: + attr.setControlChar(ControlChar.VLNEXT, e.getValue()); + break; + /* + case VFLUSH: + attr.setControlChar(ControlChar.VMIN, e.getValue()); + break; + case VSWTCH: + attr.setControlChar(ControlChar.VTIME, e.getValue()); + break; + */ + case VSTATUS: + attr.setControlChar(ControlChar.VSTATUS, e.getValue()); + break; + case VDISCARD: + attr.setControlChar(ControlChar.VDISCARD, e.getValue()); + break; + case ECHO: + attr.setLocalFlag(LocalFlag.ECHO, e.getValue() != 0); + break; + case ICANON: + attr.setLocalFlag(LocalFlag.ICANON, e.getValue() != 0); + break; + case ISIG: + attr.setLocalFlag(LocalFlag.ISIG, e.getValue() != 0); + break; + case ICRNL: + attr.setInputFlag(InputFlag.ICRNL, e.getValue() != 0); + break; + case INLCR: + attr.setInputFlag(InputFlag.INLCR, e.getValue() != 0); + break; + case IGNCR: + attr.setInputFlag(InputFlag.IGNCR, e.getValue() != 0); + break; + case OCRNL: + attr.setOutputFlag(OutputFlag.OCRNL, e.getValue() != 0); + break; + case ONLCR: + attr.setOutputFlag(OutputFlag.ONLCR, e.getValue() != 0); + break; + case ONLRET: + attr.setOutputFlag(OutputFlag.ONLRET, e.getValue() != 0); + break; + case OPOST: + attr.setOutputFlag(OutputFlag.OPOST, e.getValue() != 0); + break; + default: + } + } + terminal.setAttributes(attr); + PrintStream pout = new PrintStream(terminal.output()); + final CommandSession session = processor.createSession(terminal.input(), pout, pout); + session.put(Shell.VAR_TERMINAL, terminal); + for (Map.Entry e : env.getEnv().entrySet()) { + session.put(e.getKey(), e.getValue()); + } + env.addSignalListener(signals -> { + terminal.setSize(new Size(Integer.parseInt(env.getEnv().get("COLUMNS")), + Integer.parseInt(env.getEnv().get("LINES")))); + terminal.raise(Terminal.Signal.WINCH); + }, Signal.WINCH); + Context context = new Context() { + @Override + public String getProperty(String name) { + return System.getProperty(name); + } + + @Override + public void exit() { + destroy(); + } + }; + new Shell(context, processor).gosh(session, new String[]{"--login"}); + } catch (Throwable t) { + t.printStackTrace(); + } + } + + public void destroy() { + if (!closed) { + closed = true; + ShellFactoryImpl.flush(out, err); + ShellFactoryImpl.close(in, out, err); + callback.onExit(0); + } + } + + } + +} diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ssh/Ssh.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ssh/Ssh.java new file mode 100644 index 00000000000..9fa10ae8e3c --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ssh/Ssh.java @@ -0,0 +1,114 @@ +/* + * 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.gogo.jline.ssh; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import org.apache.felix.service.command.CommandProcessor; +import org.apache.felix.service.command.CommandSession; +import org.apache.sshd.server.ServerBuilder; +import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; +import org.apache.sshd.server.scp.ScpCommandFactory; +import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory; +import org.jline.builtins.Options; + +public class Ssh { + + public static final String[] functions = {"sshd"}; + + private static final int defaultPort = 2022; + + private final CommandProcessor processor; + private SshServer server; + @SuppressWarnings("unused") + private Object context; + private int port; + private String ip; + + public Ssh(CommandProcessor processor) { + this.processor = processor; + } + + public void sshd(CommandSession session, String[] argv) throws IOException { + final String[] usage = {"sshd - start an ssh server", + "Usage: sshd [-i ip] [-p port] start | stop | status", + " -i --ip=INTERFACE listen interface (default=127.0.0.1)", + " -p --port=PORT listen port (default=" + defaultPort + ")", + " -? --help show help"}; + + Options opt = Options.compile(usage).parse(argv); + List args = opt.args(); + + if (opt.isSet("help") || args.isEmpty()) { + opt.usage(System.err); + return; + } + + String command = args.get(0); + + if ("start".equals(command)) { + if (server != null) { + throw new IllegalStateException("sshd is already running on port " + port); + } + ip = opt.get("ip"); + port = opt.getNumber("port"); + context = session.get(org.apache.felix.gogo.runtime.activator.Activator.CONTEXT); + start(); + status(); + } else if ("stop".equals(command)) { + if (server == null) { + throw new IllegalStateException("sshd is not running."); + } + stop(); + } else if ("status".equals(command)) { + status(); + } else { + throw opt.usageError("bad command: " + command); + } + + } + + private void status() { + if (server != null) { + System.out.println("sshd is running on " + ip + ":" + port); + } else { + System.out.println("sshd is not running."); + } + } + + private void start() throws IOException { + server = ServerBuilder.builder().build(); + server.setPort(port); + server.setHost(ip); + server.setShellFactory(new ShellFactoryImpl(processor)); + server.setCommandFactory(new ScpCommandFactory.Builder().withDelegate(new ShellCommandFactory(processor)).build()); + server.setSubsystemFactories(Collections.singletonList( + new SftpSubsystemFactory.Builder().build() + )); + server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider()); + server.start(); + } + + private void stop() throws IOException { + server.stop(); + } +} diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/BootException.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/BootException.java new file mode 100644 index 00000000000..ee1f0536e60 --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/BootException.java @@ -0,0 +1,74 @@ +/* + * 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. + */ + +/*** + * Java TelnetD library (embeddable telnet daemon) + * Copyright (c) 2000-2005 Dieter Wimberger + * All rights reserved. + *

      + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + *

      + * Neither the name of the author nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + *

      + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS + * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ***/ + +package org.apache.felix.gogo.jline.telnet; + +/** + * Class that implements a BootException.
      + * This exception will flag a broken boot process, + * which expresses startup failure and unavailabilty + * of telnet service for the container application. + * + * @author Dieter Wimberger + * @version 2.0 (16/07/2006) + */ +@SuppressWarnings("serial") +public class BootException extends Exception { + + /** + * Constructor method for a BootException.
      + * + * @param msg String that contains an understandable failure message. + */ + public BootException(String msg) { + super(msg); + }//constructor + +}//class BootException \ No newline at end of file diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/Connection.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/Connection.java new file mode 100644 index 00000000000..a6c87d7f755 --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/Connection.java @@ -0,0 +1,255 @@ +/* + * 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. + */ + +/*** + * Java TelnetD library (embeddable telnet daemon) + * Copyright (c) 2000-2005 Dieter Wimberger + * All rights reserved. + *

      + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + *

      + * Neither the name of the author nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + *

      + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS + * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ***/ + +package org.apache.felix.gogo.jline.telnet; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Class that implements a connection with this telnet daemon.
      + * It is derived from java.lang.Thread, which reflects the architecture + * constraint of one thread per connection. This might seem a waste of + * resources, but as a matter of fact sharing threads would require a + * far more complex imlementation, due to the fact that telnet is not a + * stateless protocol (i.e. alive throughout a session of multiple requests + * and responses).
      + * Each Connection instance is created by the listeners ConnectionManager + * instance, making it part of a threadgroup and passing in an associated + * ConnectionData instance, that holds vital information about the connection. + * Be sure to take a look at their documention.
      + *

      + * Once the thread has started and is running, it will get a login + * shell instance from the ShellManager and run passing its own reference. + * + * @author Dieter Wimberger + * @version 2.0 (16/07/2006) + * @see ConnectionManager + * @see ConnectionData + */ +public abstract class Connection + extends Thread { + + private static final Logger LOG = Logger.getLogger(Connection.class.getName()); + private static int number; //unique number for a thread in the thread group + private boolean dead; + private List listeners; + + //Associations + private ConnectionData connectionData; //associated information + + /** + * Constructs a TelnetConnection by invoking its parent constructor + * and setting of various members.
      + * Subsequently instantiates the whole i/o subsystem, negotiating + * telnet protocol level options etc.
      + * + * @param tcg ThreadGroup that this instance is running in. + * @param cd ConnectionData instance containing all vital information + * of this connection. + * @see ConnectionData + */ + public Connection(ThreadGroup tcg, ConnectionData cd) { + super(tcg, ("Connection" + (++number))); + + connectionData = cd; + //init the connection listeners for events + //(there should actually be only one or two) + listeners = new CopyOnWriteArrayList<>(); + dead = false; + }//constructor + + /** + * Method overloaded to implement following behaviour: + *

        + *
      1. On first entry, retrieve an instance of the configured + * login shell from the ShellManager and run it. + *
      2. Handle a shell switch or close down disgracefully when + * problems (i.e. unhandled unchecked exceptions) occur in the + * running shell. + *
      + */ + public void run() { + try { + doRun(); + + } catch (Exception ex) { + LOG.log(Level.SEVERE, "run()", ex); //Handle properly + } finally { + //call close if not dead already + if (!dead) { + close(); + } + } + LOG.log(Level.FINE, "run():: Returning from " + this.toString()); + }//run + + protected abstract void doRun() throws Exception; + + protected abstract void doClose() throws Exception; + + /** + * Method to access the associated connection data. + * + * @return ConnectionData associated with the Connection instance. + * @see ConnectionData + */ + public ConnectionData getConnectionData() { + return connectionData; + }//getConnectionData + + /** + * Closes the connection and its underlying i/o and network + * resources.
      + */ + public synchronized void close() { + if (!dead) { + try { + //connection dead + dead = true; + //close i/o + doClose(); + } catch (Exception ex) { + LOG.log(Level.SEVERE, "close()", ex); + //handle + } + try { + //close socket + connectionData.getSocket().close(); + } catch (Exception ex) { + LOG.log(Level.SEVERE, "close()", ex); + //handle + } + try { + //register closed connection in ConnectionManager + connectionData.getManager().registerClosedConnection(this); + } catch (Exception ex) { + LOG.log(Level.SEVERE, "close()", ex); + //handle + } + try { + //try to interrupt it + interrupt(); + } catch (Exception ex) { + LOG.log(Level.SEVERE, "close()", ex); + //handle + } + + + LOG.log(Level.FINE, "Closed " + this.toString() + " and inactive."); + } + }//close + + /** + * Returns if a connection has been closed.
      + * + * @return the state of the connection. + */ + public boolean isActive() { + return !dead; + }//isClosed + + /****** Event handling ****************/ + + /** + * Method that registers a ConnectionListener with the + * Connection instance. + * + * @param cl ConnectionListener to be registered. + * @see ConnectionListener + */ + public void addConnectionListener(ConnectionListener cl) { + listeners.add(cl); + }//addConnectionListener + + /** + * Method that removes a ConnectionListener from the + * Connection instance. + * + * @param cl ConnectionListener to be removed. + * @see ConnectionListener + */ + public void removeConnectionListener(ConnectionListener cl) { + listeners.remove(cl); + }//removeConnectionListener + + + /** + * Method called by the io subsystem to pass on a + * "low-level" event. It will be properly delegated to + * all registered listeners. + * + * @param ce ConnectionEvent to be processed. + * @see ConnectionEvent + */ + public void processConnectionEvent(ConnectionEvent ce) { + for (ConnectionListener cl : listeners) { + switch (ce.getType()) { + case CONNECTION_IDLE: + cl.connectionIdle(ce); + break; + case CONNECTION_TIMEDOUT: + cl.connectionTimedOut(ce); + break; + case CONNECTION_LOGOUTREQUEST: + cl.connectionLogoutRequest(ce); + break; + case CONNECTION_BREAK: + cl.connectionSentBreak(ce); + break; + case CONNECTION_TERMINAL_GEOMETRY_CHANGED: + cl.connectionTerminalGeometryChanged(ce); + } + } + }//processConnectionEvent + +}//class Connection diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionData.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionData.java new file mode 100644 index 00000000000..48ee8e4e6b1 --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionData.java @@ -0,0 +1,464 @@ +/* + * 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. + */ + +/*** + * Java TelnetD library (embeddable telnet daemon) + * Copyright (c) 2000-2005 Dieter Wimberger + * All rights reserved. + *

      + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + *

      + * Neither the name of the author nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + *

      + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS + * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ***/ + +package org.apache.felix.gogo.jline.telnet; + +import java.net.InetAddress; +import java.net.Socket; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * An utility class that is used to store and allow retrieval + * of all data associated with a connection. + * + * @author Dieter Wimberger + * @version 2.0 (16/07/2006) + * @see Connection + */ +public class ConnectionData { + + //Associations + private ConnectionManager connectionManager; //the connection's ConnectionManager + private Socket socket; //the connection's socket + private InetAddress address; //the connection's IP Address Object + private Map environment; //the environment + + //Members + private String hostName; //cache for the hostname + private String hostAddress; //cache for the host ip + private int port; //port of the connection + private Locale locale; //locale of the connection + private long lastActivity; //timestamp for the last activity + private boolean warned; //warned flag + private String negotiatedTerminalType; //negotiated TerminalType as String + private int[] terminalGeometry; //negotiated terminal geometry + private boolean terminalGeometryChanged = true; //flag for changes in the terminal geometry + private String loginShell; //the login shell + private boolean lineMode = false; + + /** + * Constructs a ConnectionData instance storing vital + * information about a connection. + * + * @param sock Socket of the inbound connection. + */ + public ConnectionData(Socket sock, ConnectionManager cm) { + socket = sock; + connectionManager = cm; + address = sock.getInetAddress(); + setHostName(); + setHostAddress(); + setLocale(); + port = sock.getPort(); + //this will set a default geometry and terminal type for the terminal + terminalGeometry = new int[2]; + terminalGeometry[0] = 80; //width + terminalGeometry[1] = 25; //height + negotiatedTerminalType = "default"; + environment = new HashMap<>(20); + //this will stamp the first activity for validity :) + activity(); + }//ConnectionData + + + /** + * Returns a reference to the ConnectionManager the + * connection is associated with. + * + * @return Reference to the associated ConnectionManager. + * @see ConnectionManager + */ + public ConnectionManager getManager() { + return connectionManager; + }//getManager + + /** + * Returns a reference to the socket the Connection + * is associated with. + * + * @return Reference to the associated Socket. + * @see java.net.Socket + */ + public Socket getSocket() { + return socket; + }//getSocket + + /** + * Returns the remote port to which the socket is connected. + * + * @return String that contains the remote port number to which the socket is connected. + */ + public int getPort() { + return port; + }//getPort + + /** + * Returns the fully qualified host name for the connection's IP address.
      + * The name is cached on creation for performance reasons. Subsequent calls + * will not result in resolve queries. + * + * @return String that contains the fully qualified host name for this address. + */ + public String getHostName() { + return hostName; + }//getHostName + + /** + * Returns the IP address of the connection. + * + * @return String that contains the connection's IP address.
      + * The format "%d.%d.%d.%d" is well known, where %d goes from zero to 255. + */ + public String getHostAddress() { + return hostAddress; + }//getHostAddress + + /** + * Returns the InetAddress object associated with the connection. + * + * @return InetAddress associated with the connection. + */ + public InetAddress getInetAddress() { + return address; + }//getInetAddress + + /** + * Returns the Locale object associated with the connection + * by carrying out a simple domain match.
      + * This can either be effective, if your users are really + * home in the country they are connecting from, + * or ineffective if they are on the move getting connected + * from anywhere in the world.
      + *
      + * Yet this gives the chance of capturing a default locale + * and starting from some point. On application context + * this can be by far better handled, so be aware that + * it makes sense to spend some thoughts on that thing when you + * build your application. + * + * @return the Locale object "guessed" for the connection based + * on its host name. + */ + public Locale getLocale() { + return locale; + }//getLocale + + + /** + * Returns a timestamp of the last activity that happened on + * the associated connection. + * + * @return the timestamp as a long representing the difference, + * measured in milliseconds, between the current time and + * midnight, January 1, 1970 UTC. + */ + public long getLastActivity() { + return lastActivity; + }//getLastActivity + + + /** + * Sets a new timestamp to the actual time in millis + * retrieved from the System. This will remove an idle warning + * flag if it has been set. Note that you can use this behaviour + * to implement your own complex idle timespan policies within + * the context of your application.
      + * The check frequency of the ConnectionManager should just be set + * according to the lowest time to warning and time to disconnect + * requirements. + */ + public void activity() { + warned = false; + lastActivity = System.currentTimeMillis(); + }//setLastActivity + + /** + * Returns the state of the idle warning flag, which + * will be true if a warning has been issued, and false + * if not. + * + * @return the state of the idle warning flag. + */ + public boolean isWarned() { + return warned; + }//isWarned + + /** + * Sets the state of the idle warning flag.
      + * Note that this method will also update the + * the timestamp if the idle warning flag is removed, + * which means its kind of a second way to achieve the + * same thing as with the activity method. + * + * @param bool true if a warning is to be issued, + * false if to be removed. + * @see #activity() + */ + public void setWarned(boolean bool) { + warned = bool; + if (!bool) { + lastActivity = System.currentTimeMillis(); + } + }//setWarned + + /** + * Sets the terminal geometry data.
      + * This method should not be called explicitly + * by the application (i.e. the its here for the io subsystem).
      + * A call will set the terminal geometry changed flag. + * + * @param width of the terminal in columns. + * @param height of the terminal in rows. + */ + public void setTerminalGeometry(int width, int height) { + terminalGeometry[0] = width; + terminalGeometry[1] = height; + terminalGeometryChanged = true; + }//setTerminalGeometry + + /** + * Returns the terminal geometry in an array of two integers. + *

        + *
      • index 0: Width in columns. + *
      • index 1: Height in rows. + *
      + * A call will reset the terminal geometry changed flag. + * + * @return integer array containing width and height. + */ + public int[] getTerminalGeometry() { + //we toggle the flag because the change should now be known + if (terminalGeometryChanged) terminalGeometryChanged = false; + return terminalGeometry; + }//getTerminalGeometry + + /** + * Returns the width of the terminal in columns for convenience. + * + * @return the number of columns. + */ + public int getTerminalColumns() { + return terminalGeometry[0]; + }//getTerminalColumns + + /** + * Returns the height of the terminal in rows for convenience. + * + * @return the number of rows. + */ + public int getTerminalRows() { + return terminalGeometry[1]; + }//getTerminalRows + + /** + * Returns the state of the terminal geometry changed flag, + * which will be true if it has been set, and false + * if not. + * + * @return the state of the terminal geometry changed flag. + */ + public boolean isTerminalGeometryChanged() { + return terminalGeometryChanged; + }//isTerminalGeometryChanged + + /** + * Returns the terminal type that has been negotiated + * between the telnet client and the telnet server, in + * of a String.
      + * + * @return the negotiated terminal type as String. + */ + public String getNegotiatedTerminalType() { + return negotiatedTerminalType; + }//getNegotiatedTerminalType + + /** + * Sets the terminal type that has been negotiated + * between telnet client and telnet server, in form of + * a String.
      + *

      + * This method should not be called explicitly + * by the application (i.e. the its here for the io subsystem).
      + * + * @param termtype the negotiated terminal type as String. + */ + public void setNegotiatedTerminalType(String termtype) { + negotiatedTerminalType = termtype; + }//setNegotiatedTerminalType + + /** + * Returns the hashmap for storing and + * retrieving environment variables to be passed + * between shells. + * + * @return a HashMap instance. + */ + public Map getEnvironment() { + return environment; + }//getEnvironment + + /** + * Returns the login shell name. + * + * @return the shell name as string. + */ + public String getLoginShell() { + return loginShell; + }//getLoginShell + + /** + * Sets the login shell name. + * + * @param s the shell name as string. + */ + public void setLoginShell(String s) { + loginShell = s; + }//setLoginShell + + /** + * Tests if in line mode. + * + * @return true if in line mode, false otherwise + */ + public boolean isLineMode() { + return lineMode; + }//isLineMode + + /** + * Sets the line mode flag for the connection. + * Note that the setting will only be used at + * startup at the moment. + * + * @param b true if to be initialized in linemode, + * false otherwise. + */ + public void setLineMode(boolean b) { + lineMode = b; + }//setLineMode + + /** + * Mutator for HostName cache + */ + private void setHostName() { + hostName = address.getHostName(); + }//setHostName + + /** + * Mutator for HostAddress cache + */ + private void setHostAddress() { + hostAddress = address.getHostAddress(); + }//setHostAddress + + /** + * Mutator for Locale + * Sets a Locale derived from the hostname, + * or the default which is Locale.ENGLISH if something + * goes wrong. + * The localhost represents a problem for example :) + */ + private void setLocale() { + String country = getHostName(); + try { + country = country.substring(country.lastIndexOf(".") + 1); + switch (country) { + case "at": + locale = new Locale("de", "AT"); + break; + case "de": + locale = new Locale("de", "DE"); + break; + case "mx": + locale = new Locale("es", "MX"); + break; + case "es": + locale = new Locale("es", "ES"); + break; + case "it": + locale = Locale.ITALY; + break; + case "fr": + locale = Locale.FRANCE; + break; + case "uk": + locale = new Locale("en", "GB"); + break; + case "arpa": + locale = Locale.US; + break; + case "com": + locale = Locale.US; + break; + case "edu": + locale = Locale.US; + break; + case "gov": + locale = Locale.US; + break; + case "org": + locale = Locale.US; + break; + case "mil": + locale = Locale.US; + break; + default: + //default to english + locale = Locale.ENGLISH; + break; + } + } catch (Exception ex) { + //default to english + locale = Locale.ENGLISH; + } + }//setLocale + +}//class ConnectionData diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionEvent.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionEvent.java new file mode 100644 index 00000000000..8348f5880f3 --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionEvent.java @@ -0,0 +1,137 @@ +/* + * 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. + */ + +/*** + * Java TelnetD library (embeddable telnet daemon) + * Copyright (c) 2000-2005 Dieter Wimberger + * All rights reserved. + *

      + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + *

      + * Neither the name of the author nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + *

      + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS + * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ***/ + +package org.apache.felix.gogo.jline.telnet; + +/** + * Class implmenting a ConnectionEvent.
      + * These events are used to communicate things that are + * supposed to be handled within the application context. + * These events are processed by the Connection instance + * calling upon its registered listeners. + * + * @author Dieter Wimberger + * @version 2.0 (16/07/2006) + * @see Connection + * @see ConnectionListener + */ +public class ConnectionEvent { + + private final Connection source; + private final Type type; + /** + * Constructs a new instance of a ConnectionEvent + * with a given source (Connection) and a given type. + * + * @param source Connection that represents the source of this event. + * @param type int that contains one of the defined event types. + */ + public ConnectionEvent(Connection source, Type type) { + this.type = type; + this.source = source; + }//constructor + + /** + * Accessor method returning the source of the + * ConnectionEvent instance. + * + * @return Connection representing the source. + */ + public Connection getSource() { + return source; + }//getSource + + /** + * Method that helps identifying the type. + * + * @return Event type. + */ + public Type getType() { + return type; + }//getType + + public enum Type { + /** + * Defines the connection idle event type.
      + * It occurs if a connection has been idle exceeding + * the configured time to warning. + */ + CONNECTION_IDLE, + + /** + * Defines the connection timed out event type.
      + * It occurs if a connection has been idle exceeding + * the configured time to warning and the configured time + * to timedout. + */ + CONNECTION_TIMEDOUT, + + /** + * Defines the connection requested logout event type.
      + * It occurs if a connection requested disgraceful logout by + * sending a - key combination. + */ + CONNECTION_LOGOUTREQUEST, + + /** + * Defines the connection sent break event type.
      + * It occurs when the connection sent a NVT BREAK. + */ + CONNECTION_BREAK, + + /** + * Defines the connection geometry event type. + * It occurs when the connection sent a NAWS. + */ + CONNECTION_TERMINAL_GEOMETRY_CHANGED + } + + +}//class ConnectionEvent \ No newline at end of file diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionFilter.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionFilter.java new file mode 100644 index 00000000000..e948166a125 --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionFilter.java @@ -0,0 +1,82 @@ +/* + * 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. + */ + +/*** + * Java TelnetD library (embeddable telnet daemon) + * Copyright (c) 2000-2005 Dieter Wimberger + * All rights reserved. + *

      + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + *

      + * Neither the name of the author nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + *

      + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS + * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ***/ + +package org.apache.felix.gogo.jline.telnet; + +import java.net.InetAddress; + +/** + * Interface defining a generic IP level connection + * filter.
      + * Due to the fact that this task depends heavily on + * application context, I chose a very generic way + * of applying IP level connection filtering. + *

      + * Implementations should consider following issues: + *

        + *
      • performance + *
      • administration (maybe via an admin shell) + *
      • logging denials + *
      + * + * @author Dieter Wimberger + * @version 2.0 (16/07/2006) + */ +public interface ConnectionFilter { + + /** + * Tests if a given ip address is allowed to connect. + * + * @param ip the address to be tested. + * @return true if allowed to connect, false otherwise. + */ + boolean isAllowed(InetAddress ip); + +}//interface ConnectionFilter \ No newline at end of file diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionListener.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionListener.java new file mode 100644 index 00000000000..38ed90bbe8c --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionListener.java @@ -0,0 +1,106 @@ +/* + * 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. + */ + +/*** + * Java TelnetD library (embeddable telnet daemon) + * Copyright (c) 2000-2005 Dieter Wimberger + * All rights reserved. + *

      + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + *

      + * Neither the name of the author nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + *

      + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS + * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ***/ + +package org.apache.felix.gogo.jline.telnet; + + +/** + * Interface to be implemented if a class wants to + * qualify as a ConnectionListener.
      + * Note that a Shell is per contract also forced to + * implement this interface. + * + * @author Dieter Wimberger + * @version 2.0 (16/07/2006) + * @see ConnectionEvent + */ +public interface ConnectionListener { + + /** + * Called when a CONNECTION_IDLE event occured. + * + * @param ce ConnectionEvent instance. + * @see ConnectionEvent.Type#CONNECTION_IDLE + */ + void connectionIdle(ConnectionEvent ce); + + /** + * Called when a CONNECTION_TIMEDOUT event occured. + * + * @param ce ConnectionEvent instance. + * @see ConnectionEvent.Type#CONNECTION_TIMEDOUT + */ + void connectionTimedOut(ConnectionEvent ce); + + /** + * Called when a CONNECTION_LOGOUTREQUEST occured. + * + * @param ce ConnectionEvent instance. + * @see ConnectionEvent.Type#CONNECTION_LOGOUTREQUEST + */ + void connectionLogoutRequest(ConnectionEvent ce); + + /** + * Called when a CONNECTION_BREAK event occured. + * + * @param ce ConnectionEvent instance. + * @see ConnectionEvent.Type#CONNECTION_BREAK + */ + void connectionSentBreak(ConnectionEvent ce); + + /** + * Called when a CONNECTION_TERMINAL_GEOMETRY_CHANGED event occured. + * + * @param ce ConnectionEvent instance. + * @see ConnectionEvent.Type#CONNECTION_TERMINAL_GEOMETRY_CHANGED + */ + void connectionTerminalGeometryChanged(ConnectionEvent ce); + +}//interface ConnectionListener \ No newline at end of file diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionManager.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionManager.java new file mode 100644 index 00000000000..d46b0e53c39 --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/ConnectionManager.java @@ -0,0 +1,393 @@ +/* + * 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. + */ + +/*** + * Java TelnetD library (embeddable telnet daemon) + * Copyright (c) 2000-2005 Dieter Wimberger + * All rights reserved. + *

      + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + *

      + * Neither the name of the author nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + *

      + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS + * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ***/ + +package org.apache.felix.gogo.jline.telnet; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Stack; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Class that takes care for active and queued connection. + * Housekeeping is done also for connections that were just broken + * off, or exceeded their timeout. + * + * @author Dieter Wimberger + * @version 2.0 (16/07/2006) + */ +public abstract class ConnectionManager implements Runnable { + + private static Logger LOG = Logger.getLogger(ConnectionManager.class.getName()); + private final List openConnections; + private Thread thread; + private ThreadGroup threadGroup; //ThreadGroup all connections run in + private Stack closedConnections; + private ConnectionFilter connectionFilter; //reference to the connection filter + private int maxConnections; //maximum allowed connections stored from the properties + private int warningTimeout; //time to idle warning + private int disconnectTimeout; //time to idle diconnection + private int housekeepingInterval; //interval for managing cleanups + private String loginShell; + private boolean lineMode = false; + private boolean stopping = false; + + public ConnectionManager() { + threadGroup = new ThreadGroup(toString() + "Connections"); + closedConnections = new Stack<>(); + openConnections = Collections.synchronizedList(new ArrayList<>(100)); + } + + public ConnectionManager(int con, int timew, int timedis, int hoke, ConnectionFilter filter, String lsh, boolean lm) { + this(); + connectionFilter = filter; + loginShell = lsh; + lineMode = lm; + maxConnections = con; + warningTimeout = timew; + disconnectTimeout = timedis; + housekeepingInterval = hoke; + }//constructor + + /** + * Gets the active ConnectionFilter instance or + * returns null if no filter is set. + * + * @return the managers ConnectionFilter. + */ + public ConnectionFilter getConnectionFilter() { + return connectionFilter; + }//getConnectionFilter + + /** + * Set a connection filter for this + * ConnectionManager instance. The filter is used to handle + * IP level allow/deny of incoming connections. + * + * @param filter ConnectionFilter instance. + */ + public void setConnectionFilter(ConnectionFilter filter) { + connectionFilter = filter; + }//setConnectionFilter + + /** + * Returns the number of open connections. + * @return the number of open connections as int. + */ + public int openConnectionCount() { + return openConnections.size(); + }//openConnectionCount + + /** + * Returns the {@link Connection} at the given index. + * @param idx + * @return + */ + public Connection getConnection(int idx) { + synchronized (openConnections) { + return openConnections.get(idx); + } + }//getConnection + + /** + * Get all {@link Connection} instances with the given + * InetAddress. + * + * @return all {@link Connection} instances with the given + * InetAddress. + */ + public Connection[] getConnectionsByAdddress(InetAddress addr) { + ArrayList l = new ArrayList<>(); + synchronized (openConnections) { + for (Connection connection : openConnections) { + if (connection.getConnectionData().getInetAddress().equals(addr)) { + l.add(connection); + } + } + } + Connection[] conns = new Connection[l.size()]; + return l.toArray(conns); + }//getConnectionsByAddress + + /** + * Starts this ConnectionManager. + */ + public void start() { + thread = new Thread(this); + thread.start(); + }//start + + /** + * Stops this ConnectionManager. + */ + public void stop() { + LOG.log(Level.FINE, "stop()::" + this.toString()); + stopping = true; + //wait for thread to die + try { + if (thread != null) { + thread.join(); + } + } catch (InterruptedException iex) { + LOG.log(Level.SEVERE, "stop()", iex); + } + synchronized (openConnections) { + for (Connection tc : openConnections) { + try { + //maybe write a disgrace to the socket? + tc.close(); + } catch (Exception exc) { + LOG.log(Level.SEVERE, "stop()", exc); + } + } + openConnections.clear(); + } + LOG.log(Level.FINE, "stop():: Stopped " + this.toString()); + }//stop + + /** + * Method that that tries to connect an incoming request. + * Properly queueing. + * + * @param insock Socket thats representing the incoming connection. + */ + public void makeConnection(Socket insock) { + LOG.log(Level.FINE, "makeConnection()::" + insock.toString()); + if (connectionFilter == null || connectionFilter.isAllowed(insock.getInetAddress())) { + //we create the connection data object at this point to + //store certain information there. + ConnectionData newCD = new ConnectionData(insock, this); + newCD.setLoginShell(loginShell); + newCD.setLineMode(lineMode); + if (openConnections.size() < maxConnections) { + //create a new Connection instance + Connection con = createConnection(threadGroup, newCD); + //log the newly created connection + Object[] args = {openConnections.size() + 1}; + LOG.info(MessageFormat.format("connection #{0,number,integer} made.", args)); + //register it for being managed + synchronized (openConnections) { + openConnections.add(con); + } + //start it + con.start(); + } + } else { + LOG.info("makeConnection():: Active Filter blocked incoming connection."); + try { + insock.close(); + } catch (IOException ex) { + //do nothing or log. + } + } + }//makeConnection + + protected abstract Connection createConnection(ThreadGroup threadGroup, ConnectionData newCD); + + /** + * Periodically does following work: + *

        + *
      • cleaning up died connections. + *
      • checking managed connections if they are working properly. + *
      • checking the open connections. + *
      + */ + public void run() { + //housekeep connections + try { + do { + //clean up and close all broken connections + //cleanupBroken(); + //clean up closed connections + cleanupClosed(); + //check all active connections + checkOpenConnections(); + //sleep interval + Thread.sleep(housekeepingInterval); + } while (!stopping); + + } catch (Exception e) { + LOG.log(Level.SEVERE, "run()", e); + } + LOG.log(Level.FINE, "run():: Ran out " + this.toString()); + }//run + + /* + private void cleanupBroken() { + //cleanup loop + while (!m_BrokenConnections.isEmpty()) { + Connection nextOne = (Connection) m_BrokenConnections.pop(); + log.info("cleanupBroken():: Closing broken connection " + nextOne.toString()); + //fire logoff event for shell site cleanup , beware could hog the daemon thread + nextOne.processConnectionEvent(new ConnectionEvent(nextOne, ConnectionEvent.CONNECTION_BROKEN)); + //close the connection, will be automatically registered as closed + nextOne.close(); + } + }//cleanupBroken + */ + private void cleanupClosed() { + if (stopping) { + return; + } + //cleanup loop + while (!closedConnections.isEmpty()) { + Connection nextOne = closedConnections.pop(); + LOG.info("cleanupClosed():: Removing closed connection " + nextOne.toString()); + synchronized (openConnections) { + openConnections.remove(nextOne); + } + } + }//cleanupBroken + + private void checkOpenConnections() { + if (stopping) { + return; + } + //do routine checks on active connections + synchronized (openConnections) { + for (Connection conn : openConnections) { + ConnectionData cd = conn.getConnectionData(); + //check if it is dead and remove it. + if (!conn.isActive()) { + registerClosedConnection(conn); + continue; + } + /* Timeouts check */ + //first we caculate the inactivity time + long inactivity = System.currentTimeMillis() - cd.getLastActivity(); + //now we check for warning and disconnection + if (inactivity > warningTimeout) { + //..and for disconnect + if (inactivity > (disconnectTimeout + warningTimeout)) { + //this connection needs to be disconnected :) + LOG.log(Level.FINE, "checkOpenConnections():" + conn.toString() + " exceeded total timeout."); + //fire logoff event for shell site cleanup , beware could hog the daemon thread + conn.processConnectionEvent(new ConnectionEvent(conn, ConnectionEvent.Type.CONNECTION_TIMEDOUT)); + //conn.close(); + } else { + //this connection needs to be warned :) + if (!cd.isWarned()) { + LOG.log(Level.FINE, "checkOpenConnections():" + conn.toString() + " exceeded warning timeout."); + cd.setWarned(true); + //warning event is fired but beware this could hog the daemon thread!! + conn.processConnectionEvent(new ConnectionEvent(conn, ConnectionEvent.Type.CONNECTION_IDLE)); + } + } + } + } + /* end Timeouts check */ + } + }//checkConnections + + public void registerClosedConnection(Connection con) { + if (stopping) { + return; + } + if (!closedConnections.contains(con)) { + LOG.log(Level.FINE, "registerClosedConnection()::" + con.toString()); + closedConnections.push(con); + } + }//unregister + + public int getDisconnectTimeout() { + return disconnectTimeout; + } + + public void setDisconnectTimeout(int disconnectTimeout) { + this.disconnectTimeout = disconnectTimeout; + } + + public int getHousekeepingInterval() { + return housekeepingInterval; + } + + public void setHousekeepingInterval(int housekeepingInterval) { + this.housekeepingInterval = housekeepingInterval; + } + + public boolean isLineMode() { + return lineMode; + } + + public void setLineMode(boolean lineMode) { + this.lineMode = lineMode; + } + + public String getLoginShell() { + return loginShell; + } + + public void setLoginShell(String loginShell) { + this.loginShell = loginShell; + } + + public int getMaxConnections() { + return maxConnections; + } + + public void setMaxConnections(int maxConnections) { + this.maxConnections = maxConnections; + } + + public int getWarningTimeout() { + return warningTimeout; + } + + public void setWarningTimeout(int warningTimeout) { + this.warningTimeout = warningTimeout; + } + +}//class ConnectionManager diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/PortListener.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/PortListener.java new file mode 100644 index 00000000000..1682db07342 --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/PortListener.java @@ -0,0 +1,222 @@ +/* + * 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. + */ + +/*** + * Java TelnetD library (embeddable telnet daemon) + * Copyright (c) 2000-2005 Dieter Wimberger + * All rights reserved. + *

      + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + *

      + * Neither the name of the author nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + *

      + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS + * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ***/ + +package org.apache.felix.gogo.jline.telnet; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.text.MessageFormat; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Class that implements a PortListener.
      + * If available, it accepts incoming connections and passes them + * to an associated ConnectionManager. + * + * @author Dieter Wimberger + * @version 2.0 (16/07/2006) + * @see ConnectionManager + */ +public class PortListener + implements Runnable { + + private static final Logger LOG = Logger.getLogger(PortListener.class.getName()); + private static final String logmsg = + "Listening to Port {0,number,integer} with a connectivity queue size of {1,number,integer}."; + private String name; + private int port; //port number running on + private int floodProtection; //flooding protection + private ServerSocket serverSocket = null; //server socket + private Thread thread; + private ConnectionManager connectionManager; //connection management thread + private boolean stopping = false; + private boolean available; //Flag for availability + + /** + * Constructs a PortListener instance.
      + * + * @param port int that specifies the port number of the server socket. + * @param floodprot that specifies the server socket queue size. + */ + public PortListener(String name, int port, int floodprot) { + this.name = name; + available = false; + this.port = port; + floodProtection = floodprot; + }//constructor + + /** + * Returns the name of this PortListener. + * + * @return the name as String. + */ + public String getName() { + return name; + }//getName + + /** + * Tests if this PortListener is available. + * + * @return true if available, false otherwise. + */ + public boolean isAvailable() { + return available; + }//isAvailable + + /** + * Sets the availability flag of this PortListener. + * + * @param b true if to be available, false otherwise. + */ + public void setAvailable(boolean b) { + available = b; + }//setAvailable + + /** + * Starts this PortListener. + */ + public void start() { + LOG.log(Level.FINE, "start()"); + thread = new Thread(this); + thread.start(); + available = true; + }//start + + /** + * Stops this PortListener, and returns + * when everything was stopped successfully. + */ + public void stop() { + LOG.log(Level.FINE, "stop()::" + this.toString()); + //flag stop + stopping = true; + available = false; + //take down all connections + connectionManager.stop(); + + //close server socket + try { + serverSocket.close(); + } catch (IOException ex) { + LOG.log(Level.SEVERE, "stop()", ex); + } + + //wait for thread to die + try { + thread.join(); + } catch (InterruptedException iex) { + LOG.log(Level.SEVERE, "stop()", iex); + } + + LOG.info("stop()::Stopped " + this.toString()); + }//stop + + /** + * Listen constantly to a server socket and handles incoming connections + * through the associated {a:link ConnectionManager}. + * + * @see ConnectionManager + */ + public void run() { + try { + /* + A server socket is opened with a connectivity queue of a size specified + in int floodProtection. Concurrent login handling under normal circumstances + should be handled properly, but denial of service attacks via massive parallel + program logins should be prevented with this. + */ + serverSocket = new ServerSocket(port, floodProtection); + + //log entry + LOG.info(MessageFormat.format(logmsg, port, floodProtection)); + + do { + try { + Socket s = serverSocket.accept(); + if (available) { + connectionManager.makeConnection(s); + } else { + //just shut down the socket + s.close(); + } + } catch (SocketException ex) { + if (stopping) { + //server socket was closed blocked in accept + LOG.log(Level.FINE, "run(): ServerSocket closed by stop()"); + } else { + LOG.log(Level.SEVERE, "run()", ex); + } + } + } while (!stopping); + + } catch (IOException e) { + LOG.log(Level.SEVERE, "run()", e); + } + LOG.log(Level.FINE, "run(): returning."); + }//run + + /** + * Returns reference to ConnectionManager instance associated + * with the PortListener. + * + * @return the associated ConnectionManager. + */ + public ConnectionManager getConnectionManager() { + return connectionManager; + }//getConnectionManager + + public void setConnectionManager(ConnectionManager connectionManager) { + this.connectionManager = connectionManager; + } + +}//class PortListener diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/Telnet.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/Telnet.java new file mode 100644 index 00000000000..1f3259b3fef --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/Telnet.java @@ -0,0 +1,203 @@ +/* + * 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.gogo.jline.telnet; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.List; + +import org.apache.felix.gogo.jline.Shell; +import org.apache.felix.gogo.jline.Shell.Context; +import org.apache.felix.service.command.CommandProcessor; +import org.apache.felix.service.command.CommandSession; +import org.jline.builtins.Options; +import org.jline.terminal.Size; +import org.jline.terminal.Terminal; +import org.jline.terminal.Terminal.Signal; +import org.jline.terminal.TerminalBuilder; + +/* + * a very simple Telnet server. + * real remote access should be via ssh. + */ +public class Telnet { + public static final String[] functions = {"telnetd"}; + + private static final int defaultPort = 2019; + private final CommandProcessor processor; + private PortListener portListener; + private int port; + private String ip; + + public Telnet(CommandProcessor procesor) { + this.processor = procesor; + } + + public void telnetd(CommandSession session, String[] argv) throws IOException { + final String[] usage = {"telnetd - start simple telnet server", + "Usage: telnetd [-i ip] [-p port] start | stop | status", + " -i --ip=INTERFACE listen interface (default=127.0.0.1)", + " -p --port=PORT listen port (default=" + defaultPort + ")", + " -? --help show help"}; + + Options opt = Options.compile(usage).parse(argv); + List args = opt.args(); + + if (opt.isSet("help") || args.isEmpty()) { + opt.usage(System.err); + return; + } + + String command = args.get(0); + + if ("start".equals(command)) { + if (portListener != null) { + throw new IllegalStateException("telnetd is already running on port " + port); + } + ip = opt.get("ip"); + port = opt.getNumber("port"); + start(session); + status(); + } else if ("stop".equals(command)) { + if (portListener == null) { + throw new IllegalStateException("telnetd is not running."); + } + stop(); + } else if ("status".equals(command)) { + status(); + } else { + throw opt.usageError("bad command: " + command); + } + } + + private void status() { + if (portListener != null) { + System.out.println("telnetd is running on " + ip + ":" + port); + } else { + System.out.println("telnetd is not running."); + } + } + + private void start(CommandSession session) { + ConnectionManager connectionManager = new ConnectionManager(1000, 5 * 60 * 1000, 5 * 60 * 1000, 60 * 1000, null, null, false) { + @Override + protected Connection createConnection(ThreadGroup threadGroup, ConnectionData newCD) { + return new Connection(threadGroup, newCD) { + TelnetIO telnetIO; + + @Override + protected void doRun() throws Exception { + telnetIO = new TelnetIO(); + telnetIO.setConnection(this); + telnetIO.initIO(); + + InputStream in = new InputStream() { + @Override + public int read() throws IOException { + return telnetIO.read(); + } + @Override + public int read(byte[] b, int off, int len) throws IOException { + int r = read(); + if (r >= 0) { + b[off] = (byte) r; + return 1; + } else { + return -1; + } + } + }; + PrintStream out = new PrintStream(new OutputStream() { + @Override + public void write(int b) throws IOException { + telnetIO.write(b); + } + @Override + public void flush() throws IOException { + telnetIO.flush(); + } + }); + Terminal terminal = TerminalBuilder.builder() + .type(getConnectionData().getNegotiatedTerminalType().toLowerCase()) + .streams(in, out) + .system(false) + .name("telnet") + .build(); + terminal.setSize(new Size(getConnectionData().getTerminalColumns(), getConnectionData().getTerminalRows())); + terminal.setAttributes(Shell.getTerminal(session).getAttributes()); + addConnectionListener(new ConnectionListener() { + @Override + public void connectionIdle(ConnectionEvent ce) { + } + + @Override + public void connectionTimedOut(ConnectionEvent ce) { + } + + @Override + public void connectionLogoutRequest(ConnectionEvent ce) { + } + + @Override + public void connectionSentBreak(ConnectionEvent ce) { + } + + @Override + public void connectionTerminalGeometryChanged(ConnectionEvent ce) { + terminal.setSize(new Size(getConnectionData().getTerminalColumns(), getConnectionData().getTerminalRows())); + terminal.raise(Signal.WINCH); + } + }); + PrintStream pout = new PrintStream(terminal.output()); + CommandSession session = processor.createSession(terminal.input(), pout, pout); + session.put(Shell.VAR_TERMINAL, terminal); + Context context = new Context() { + @Override + public String getProperty(String name) { + return System.getProperty(name); + } + @Override + public void exit() { + close(); + } + }; + new Shell(context, processor).gosh(session, new String[]{"--login"}); + } + + @Override + protected void doClose() { + telnetIO.closeOutput(); + telnetIO.closeInput(); + } + }; + } + }; + portListener = new PortListener("gogo", port, 10); + portListener.setConnectionManager(connectionManager); + portListener.start(); + } + + private void stop() { + portListener.stop(); + portListener = null; + } + +} diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/TelnetIO.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/TelnetIO.java new file mode 100644 index 00000000000..70153191d3b --- /dev/null +++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/telnet/TelnetIO.java @@ -0,0 +1,1532 @@ +/* + * 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. + */ + +/*** + * Java TelnetD library (embeddable telnet daemon) + * Copyright (c) 2000-2005 Dieter Wimberger + * All rights reserved. + *

      + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + *

      + * Neither the name of the author nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + *

      + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS + * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ***/ + +package org.apache.felix.gogo.jline.telnet; + +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.InetAddress; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.felix.gogo.jline.telnet.ConnectionEvent.Type; + +/** + * Class that represents the TelnetIO implementation. It contains + * an inner IACHandler class to handle the telnet protocol level + * communication. + *

      + * Although supposed to work full-duplex, we only process the telnet protocol + * layer communication in case of reading requests from the higher levels. + * This is the only way to meet the one thread per connection requirement. + *

      + *

      + * The output is done via byte-oriented streams, definately suitable for the + * telnet protocol. The format of the output is UTF-8 (Unicode), which is a + * standard and supported by any telnet client, including the ones included + * in Microsoft OS's. + *

      + * Notes: + *
        + *
      • The underlying output is buffered, to ensure that all bytes written + * are send, the flush() method has to be called. + *
      • This low-level routines ensure nice multithreading behaviour on I/O. + * Neither large outputs, nor input sequences excuted by the connection thread + * can hog the system. + *
      + * + * @author Dieter Wimberger + * @version 2.0 (16/07/2006) + */ +public class TelnetIO { + + /** + * Interpret As Command + */ + protected static final int IAC = 255; + /** + * Go Ahead
      Newer Telnets do not make use of this option + * that allows a specific communication mode. + */ + protected static final int GA = 249; + /** + * Negotiation: Will do option + */ + protected static final int WILL = 251; + /** + * Negotiation: Wont do option + */ + protected static final int WONT = 252; + /** + * Negotiation: Do option + */ + protected static final int DO = 253; + /** + * Negotiation: Dont do option + */ + protected static final int DONT = 254; + /** + * Marks start of a subnegotiation. + */ + protected static final int SB = 250; + /** + * Marks end of subnegotiation. + */ + protected static final int SE = 240; + /** + * No operation + */ + protected static final int NOP = 241; + /** + * Data mark its the data part of a SYNCH which helps to clean up the buffers between + * Telnet Server <-> Telnet Client.
      + * It should work like this we send a TCP urgent package and <IAC> <DM> the receiver + * should get the urgent package (SYNCH) and just discard everything until he receives + * our <IAC> <DM>.
      + * Remark: + *
        + *
      1. can we send a TCP urgent package? + *
      2. can we make use of the thing at all? + *
      + */ + protected static final int DM = 242; + /** + * Break + */ + protected static final int BRK = 243; + /** + * Interrupt Process + */ + protected static final int IP = 244; + /** + * Abort Output + */ + protected static final int AO = 245; + + /**** Implementation of OutputStream ****************************************************/ + /** + * Are You There + */ + protected static final int AYT = 246; + /** + * Erase Char + */ + protected static final int EC = 247; + /** + * Erase Line + */ + protected static final int EL = 248; + /** + * Telnet Option: ECHO + */ + protected static final int ECHO = 1; + /** + * Telnet Option: SUPress Go Ahead
      + * This will be negotiated, all new telnet protocol implementations are + * recommended to do this. + */ + protected static final int SUPGA = 3; + /** + * Telnet Option: Negotiate About Window Size
      + *
        + *
      • Server request is IAC DO NAWS + *
      • Client response contains subnegotiation with data (columns, rows). + *
      + */ + protected static final int NAWS = 31; + /** + * Telnet Option: Terminal TYPE
      + *
        + *
      • Server request contains subnegotiation SEND + *
      • Client response contains subnegotiation with data IS,terminal type string + *
      + */ + protected static final int TTYPE = 24; + /** + * TTYPE subnegotiation: IS + */ + protected static final int IS = 0; + /** + * TTYPE subnegotiation: SEND + */ + protected static final int SEND = 1; + + /**** End implementation of OutputStream ***********************************************/ + + + /**** Implementation of InputStream ****************************************************/ + /** + * Telnet Option: Logout
      + * This allows nice goodbye to time-outed or unwanted clients. + */ + protected static final int LOGOUT = 18; + /** + * Telnet Option: Linemode + *

      + * The infamous line mode option. + */ + protected static final int LINEMODE = 34; + protected static final int LM_MODE = 1; + protected static final int LM_EDIT = 1; + protected static final int LM_TRAPSIG = 2; + + /**** Implementation of InputStream ****************************************************/ + + + /**** + * Following methods implement init/request/answer procedures for telnet + * protocol level communication. + */ + protected static final int LM_MODEACK = 4; + protected static final int LM_FORWARDMASK = 2; + protected static final int LM_SLC = 3; + protected static final int LM_SLC_NOSUPPORT = 0; + protected static final int LM_SLC_DEFAULT = 3; + + + /**** End telnet protocol level communication methods *******************************/ + protected static final int LM_SLC_VALUE = 2; + + + /** Constants declaration ***********************************************/ + +//Telnet Protocoll Constants + protected static final int LM_SLC_CANTCHANGE = 1; + protected static final int LM_SLC_LEVELBITS = 3; + protected static final int LM_SLC_ACK = 128; + protected static final int LM_SLC_FLUSHIN = 64; + protected static final int LM_SLC_FLUSHOUT = 32; + protected static final int LM_SLC_SYNCH = 1; + protected static final int LM_SLC_BRK = 2; + protected static final int LM_SLC_IP = 3; + protected static final int LM_SLC_AO = 4; + protected static final int LM_SLC_AYT = 5; + protected static final int LM_SLC_EOR = 6; + + /** + * The following implement the NVT (network virtual terminal) which offers the concept + * of a simple "printer". They are the basical meanings of control possibilities + * on a standard telnet implementation. + */ + protected static final int LM_SLC_ABORT = 7; + protected static final int LM_SLC_EOF = 8; + protected static final int LM_SLC_SUSP = 9; + /** + * Telnet Option: Environment + */ + protected static final int NEWENV = 39; + protected static final int NE_INFO = 2; + + /** + * The following are constants for supported options, + * which can be negotiated based upon the telnet protocol + * specification. + */ + protected static final int NE_VAR = 0; + protected static final int NE_VALUE = 1; + + /** + * The following options are options for which we also support subnegotiation + * based upon the telnet protocol specification. + */ + protected static final int NE_ESC = 2; + protected static final int NE_USERVAR = 3; + protected static final int NE_VAR_OK = 2; + protected static final int NE_VAR_DEFINED = 1; + protected static final int NE_VAR_DEFINED_EMPTY = 0; + protected static final int NE_VAR_UNDEFINED = -1; + protected static final int NE_IN_ERROR = -2; + protected static final int NE_IN_END = -3; + protected static final int NE_VAR_NAME_MAXLENGTH = 50; + protected static final int NE_VAR_VALUE_MAXLENGTH = 1000; + /** + * Unused + */ + protected static final int EXT_ASCII = 17; //Defines Extended ASCII + protected static final int SEND_LOC = 23; //Defines Send Location + protected static final int AUTHENTICATION = 37; //Defines Authentication + protected static final int ENCRYPT = 38; //Defines Encryption + private static final Logger LOG = Logger.getLogger(TelnetIO.class.getName()); + /** + * Window Size Constants + */ + private static final int SMALLEST_BELIEVABLE_WIDTH = 20; + private static final int SMALLEST_BELIEVABLE_HEIGHT = 6; + private static final int DEFAULT_WIDTH = 80; + private static final int DEFAULT_HEIGHT = 25; + private Connection connection; //a reference to the connection this instance works for + private ConnectionData connectionData; //holds all important information of the connection + private DataOutputStream out; //the byte oriented outputstream + private DataInputStream in; //the byte oriented input stream + //Aggregations + private IACHandler iacHandler; //holds a reference to the aggregated IACHandler + //Members + private InetAddress localAddress; //address of the host the telnetd is running on + private boolean noIac = false; //describes if IAC was found and if its just processed + @SuppressWarnings("unused") + private boolean initializing; + private boolean crFlag; + /** + * Creates a TelnetIO object for the given connection.
      + * Input- and OutputStreams are properly set and the primary telnet + * protocol initialization is carried out by the inner IACHandler class.
      + */ + public TelnetIO() { + }//constructor + + public void initIO() throws IOException { + //we make an instance of our inner class + iacHandler = new IACHandler(); + //we setup underlying byte oriented streams + in = new DataInputStream(connectionData.getSocket().getInputStream()); + out = new DataOutputStream(new BufferedOutputStream(connectionData.getSocket().getOutputStream())); + + //we save the local address (necessary?) + localAddress = connectionData.getSocket().getLocalAddress(); + crFlag = false; + //bootstrap telnet communication + initTelnetCommunication(); + }//initIO + + public void setConnection(Connection con) { + connection = con; + connectionData = connection.getConnectionData(); + }//setConnection + + /** + * Method to output a byte. Ensures that CR(\r) is never send + * alone,but CRLF(\r\n), which is a rule of the telnet protocol. + * + * @param b Byte to be written. + */ + public void write(byte b) throws IOException { + //ensure CRLF(\r\n) is written for LF(\n) to adhere + //to the telnet protocol. + if (!crFlag && b == 10) { + out.write(13); + } + + out.write(b); + + crFlag = b == 13; + }//write(byte) + + /** + * Method to output an int. + * + * @param i Integer to be written. + */ + public void write(int i) + throws IOException { + write((byte) i); + }//write(int) + + /** + * Method to write an array of bytes. + * + * @param sequence byte[] to be written. + */ + public void write(byte[] sequence) throws IOException { + for (byte b : sequence) { + write(b); + } + }//write(byte[]) + + /** + * Method to output an array of int' s. + * + * @param sequence int [] to write + */ + public void write(int[] sequence) throws IOException { + for (int i : sequence) { + write((byte) i); + } + }//write(int[]) + + /** + * Method to write a char. + * + * @param ch char to be written. + */ + public void write(char ch) throws IOException { + write((byte) ch); + }//write(char) + + /** + * Method to output a string. + * + * @param str String to be written. + */ + public void write(String str) throws IOException { + write(str.getBytes()); + }//write(String) + + /** + * Method to flush all buffered output. + */ + public void flush() throws IOException { + out.flush(); + }//flush + + /** + * Method to close the underlying output stream to free system resources.
      + * Most likely only to be called by the ConnectionManager upon clean up of + * connections that ended or died. + */ + public void closeOutput() { + + try { + //sends telnetprotocol logout acknowledgement + write(IAC); + write(DO); + write(LOGOUT); + //and now close underlying outputstream + + out.close(); + } catch (IOException ex) { + LOG.log(Level.SEVERE, "closeOutput()", ex); + //handle? + } + }//close + + private void rawWrite(int i) throws IOException { + out.write(i); + }//rawWrite + + /** + * Method to read a byte from the InputStream. + * Invokes the IACHandler upon IAC (Byte=255). + * + * @return int read from stream. + */ + public int read() throws IOException { + int c = rawread(); + //if (c == 255) { + noIac = false; + while ((c == 255) && (!noIac)) { + /** + * Read next, and invoke + * the IACHandler he is taking care of the rest. Or at least he should :) + */ + c = rawread(); + if (c != 255) { + iacHandler.handleC(c); + c = rawread(); + } else { + noIac = true; + } + } + return stripCRSeq(c); + }//read + + /** + * Method to close the underlying inputstream to free system resources.
      + * Most likely only to be called by the ConnectionManager upon clean up of + * connections that ended or died. + */ + public void closeInput() { + try { + in.close(); + } catch (IOException e) { + //handle? + } + }//closeInput + + /** + * This method reads an unsigned 16bit Integer from the stream, + * its here for getting the NAWS Data Values for height and width. + */ + private int read16int() throws IOException { + int c = in.readUnsignedShort(); + return c; + }//read16int + + /** + * The following options are options which might be of interest, but are not + * yet implemented or in use. + */ + + /** + * Method to read a raw byte from the InputStream.
      + * Telnet protocol layer communication is filtered and processed here. + * + * @return int read from stream. + */ + private int rawread() throws IOException { + int b = 0; + + //try { + b = in.readUnsignedByte(); + connectionData.activity(); + return b; + }//rawread + + /** + * Checks for the telnet protocol specified CR followed by NULL or LF
      + * Subsequently reads for the next byte and forwards + * only a ENTER represented by LF internally. + */ + private int stripCRSeq(int input) throws IOException { + if (input == 13) { + rawread(); + return 10; + } + return input; + }//stripCRSeq + + /** + * Method that initializes the telnet communication layer. + */ + private void initTelnetCommunication() { + + initializing = true; + try { + //start out, some clients just wait + if (connectionData.isLineMode()) { + iacHandler.doLineModeInit(); + LOG.log(Level.FINE, "Line mode initialized."); + } else { + iacHandler.doCharacterModeInit(); + LOG.log(Level.FINE, "Character mode initialized."); + } + //open for a defined timeout so we read incoming negotiation + connectionData.getSocket().setSoTimeout(1000); + read(); + + } catch (Exception e) { + //handle properly + //log.error("initTelnetCommunication()",e); + } finally { + //this is important, dont ask me why :) + try { + connectionData.getSocket().setSoTimeout(0); + } catch (Exception ex) { + LOG.log(Level.SEVERE, "initTelnetCommunication()", ex); + } + } + initializing = false; + }//initTelnetCommunication + + /** + * Method that represents the answer to the + * AreYouThere question of the telnet protocol specification + *

      + * Output of the String [HostAdress:Yes] + */ + private void IamHere() { + try { + write("[" + localAddress.toString() + ":Yes]"); + flush(); + } catch (Exception ex) { + LOG.log(Level.SEVERE, "IamHere()", ex); + } + }//IamHere + + /** + * Network virtual terminal break. + */ + private void nvtBreak() { + connection.processConnectionEvent(new ConnectionEvent(connection, ConnectionEvent.Type.CONNECTION_BREAK)); + }//nvtBreak + + /** + * Method that checks reported terminal sizes and sets the + * asserted values in the ConnectionData instance associated with + * the connection. + * + * @param width Integer that represents the Window width in chars + * @param height Integer that represents the Window height in chars + */ + private void setTerminalGeometry(int width, int height) { + if (width < SMALLEST_BELIEVABLE_WIDTH) { + width = DEFAULT_WIDTH; + } + if (height < SMALLEST_BELIEVABLE_HEIGHT) { + height = DEFAULT_HEIGHT; + } + //DEBUG: write("[New Window Size " + window_width + "x" + window_height + "]"); + connectionData.setTerminalGeometry(width, height); + connection.processConnectionEvent(new ConnectionEvent(connection, + Type.CONNECTION_TERMINAL_GEOMETRY_CHANGED)); + }//setTerminalGeometry + + public void setEcho(boolean b) { + }//setEcho + + /** + * An inner class for handling incoming option negotiations implementing the telnet protocol + * specification based upon following Standards and RFCs: + *

        + *
      1. 854 Telnet Protocol Specification + *
      2. 855 Telnet Option Specifications + *
      3. 857 Telnet Echo Option + *
      4. 858 Telnet Supress Go Ahead Option + *
      5. 727 Telnet Logout Option + *
      6. 1073 Telnet Window Size Option + *
      7. 1091 Telnet Terminal-Type Option + *
      + *

      + * Furthermore there are some more, which helped to solve problems, or might be important + * for future enhancements:
      + * 1143 The Q Method of Implementing Option Negotiation
      + * 1416 Telnet Authentication Option
      + *

      + * After an intense study of the available material (mainly cryptical written RFCs, + * a telnet client implementation for the macintosh based upon NCSA telnet, and a server side + * implementation called key, a mud-like system completely written in Java) I realized + * the problems we are facing regarding to the telnet protocol: + *

        + *
      1. a minimal spread of invented options, which means there are a lot of invented options, + * but rarely they made it through to become a standard. + *
      2. Dependency on a special type of implementation is dangerous in our case. + * We are no kind of host that offers the user to run several processes at once, + * a BBS is intended to be a single process the user is interacting with. + *
      3. The LAMER has to be expected to log in with the standard Microsoft telnet + * implementation. This means forget every nice feature and most of the almost-standards. + *

        + *

      + *
      + * + * @author Dieter Wimberger + * @version 1.1 16/06/1998 + *

      + *

      + * To-Do:

        + *
      • UNIX conform new style TTYPE negotiation. Setting a list and selecting from it... + *
      + */ + class IACHandler { + + /** + * Telnet readin buffer + * Here its implemented guys. Open your eyes upon this solution. + * The others take a one byte solution :) + */ + private int[] buffer = new int[2]; + + /** + * DO_ECHO or not + */ + private boolean DO_ECHO = false; + + /** + * DO_SUPGA or not + */ + private boolean DO_SUPGA = false; + + /** + * DO_NAWS or not + */ + private boolean DO_NAWS = false; + + /** + * DO_TTYPE or not + */ + private boolean DO_TTYPE = false; + + /** + * DO_LINEMODE or not + */ + private boolean DO_LINEMODE = false; + + /** + * DO_NEWENV or not + */ + private boolean DO_NEWENV = false; + + /** + * Are we waiting for a DO reply? + */ + private boolean WAIT_DO_REPLY_SUPGA = false; + private boolean WAIT_DO_REPLY_ECHO = false; + private boolean WAIT_DO_REPLY_NAWS = false; + private boolean WAIT_DO_REPLY_TTYPE = false; + private boolean WAIT_DO_REPLY_LINEMODE = false; + private boolean WAIT_LM_MODE_ACK = false; + private boolean WAIT_LM_DO_REPLY_FORWARDMASK = false; + private boolean WAIT_DO_REPLY_NEWENV = false; + @SuppressWarnings("unused") + private boolean WAIT_NE_SEND_REPLY = false; + + /** + * Are we waiting for a WILL reply? + */ + private boolean WAIT_WILL_REPLY_SUPGA = false; + private boolean WAIT_WILL_REPLY_ECHO = false; + private boolean WAIT_WILL_REPLY_NAWS = false; + private boolean WAIT_WILL_REPLY_TTYPE = false; + + + public void doCharacterModeInit() throws IOException { + sendCommand(WILL, ECHO, true); + sendCommand(DONT, ECHO, true); //necessary for some clients + sendCommand(DO, NAWS, true); + sendCommand(WILL, SUPGA, true); + sendCommand(DO, SUPGA, true); + sendCommand(DO, TTYPE, true); + sendCommand(DO, NEWENV, true); //environment variables + }//doCharacterModeInit + + public void doLineModeInit() throws IOException { + sendCommand(DO, NAWS, true); + sendCommand(WILL, SUPGA, true); + sendCommand(DO, SUPGA, true); + sendCommand(DO, TTYPE, true); + sendCommand(DO, LINEMODE, true); + sendCommand(DO, NEWENV, true); + }//doLineModeInit + + + /** + * Method to handle a IAC that came in over the line. + * + * @param i (int)ed byte that followed the IAC + */ + public void handleC(int i) throws IOException { + buffer[0] = i; + if (!parseTWO(buffer)) { + buffer[1] = rawread(); + parse(buffer); + } + buffer[0] = 0; + buffer[1] = 0; + }//handleC + + /** + * Method that parses for options with two characters. + * + * @param buf int [] that represents the first byte that followed the IAC first. + * @return true when it was a two byte command (IAC OPTIONBYTE) + */ + private boolean parseTWO(int[] buf) { + switch (buf[0]) { + case IAC: + //doubled IAC to escape 255 is handled within the + //read method. + break; + case AYT: + IamHere(); + break; + case AO: + case IP: + case EL: + case EC: + case NOP: + break; + case BRK: + nvtBreak(); + break; + default: + return false; + } + return true; + }//parseTWO + + /** + * Method that parses further on for options. + * + * @param buf that represents the first two bytes that followed the IAC. + */ + private void parse(int[] buf) throws IOException { + switch (buf[0]) { + /* First switch on the Negotiation Option */ + case WILL: + if (supported(buf[1]) && isEnabled(buf[1])) { + // do nothing + } else { + if (waitDOreply(buf[1]) && supported(buf[1])) { + enable(buf[1]); + setWait(DO, buf[1], false); + } else { + if (supported(buf[1])) { + sendCommand(DO, buf[1], false); + enable(buf[1]); + } else { + sendCommand(DONT, buf[1], false); + } + } + } + break; + case WONT: + if (waitDOreply(buf[1]) && supported(buf[1])) { + setWait(DO, buf[1], false); + } else { + if (supported(buf[1]) && isEnabled(buf[1])) { + // eanable() Method disables an Option that is already enabled + enable(buf[1]); + } + } + break; + case DO: + if (supported(buf[1]) && isEnabled(buf[1])) { + // do nothing + } else { + if (waitWILLreply(buf[1]) && supported(buf[1])) { + enable(buf[1]); + setWait(WILL, buf[1], false); + } else { + if (supported(buf[1])) { + sendCommand(WILL, buf[1], false); + enable(buf[1]); + } else { + sendCommand(WONT, buf[1], false); + } + } + } + break; + case DONT: + if (waitWILLreply(buf[1]) && supported(buf[1])) { + setWait(WILL, buf[1], false); + } else { + if (supported(buf[1]) && isEnabled(buf[1])) { + // enable() Method disables an Option that is already enabled + enable(buf[1]); + } + } + break; + + /* Now about other two byte IACs */ + case DM: //How do I implement a SYNCH signal? + break; + case SB: //handle subnegotiations + if ((supported(buf[1])) && (isEnabled(buf[1]))) { + switch (buf[1]) { + case NAWS: + handleNAWS(); + break; + case TTYPE: + handleTTYPE(); + break; + case LINEMODE: + handleLINEMODE(); + break; + case NEWENV: + handleNEWENV(); + break; + default: + } + } else { + //do nothing + } + break; + default: + }//switch + }//parse + + /** + * Method that reads a NawsSubnegotiation that ends up with a IAC SE + * If the measurements are unbelieveable it switches to the defaults. + */ + private void handleNAWS() throws IOException { + int width = read16int(); + if (width == 255) { + width = read16int(); //handle doubled 255 value; + } + int height = read16int(); + if (height == 255) { + height = read16int(); //handle doubled 255 value; + } + skipToSE(); + setTerminalGeometry(width, height); + }//handleNAWS + + /** + * Method that reads a TTYPE Subnegotiation String that ends up with a IAC SE + * If no Terminal is valid, we switch to the dumb "none" terminal. + */ + private void handleTTYPE() throws IOException { + String tmpstr = ""; + // The next read should be 0 which is IS by the protocol + // specs. hmmm? + rawread(); //that should be the is :) + tmpstr = readIACSETerminatedString(40); + LOG.log(Level.FINE, "Reported terminal name " + tmpstr); + connectionData.setNegotiatedTerminalType(tmpstr); + }//handleTTYPE + + /** + * Method that handles LINEMODE subnegotiation. + */ + public void handleLINEMODE() throws IOException { + int c = rawread(); + switch (c) { + case LM_MODE: + handleLMMode(); + break; + case LM_SLC: + handleLMSLC(); + break; + case WONT: + case WILL: + handleLMForwardMask(c); + break; + default: + //skip to (including) SE + skipToSE(); + } + }//handleLINEMODE + + public void handleLMMode() throws IOException { + //we sent the default which no client might deny + //so we only wait the ACK + if (WAIT_LM_MODE_ACK) { + int mask = rawread(); + if (mask != (LM_EDIT | LM_TRAPSIG | LM_MODEACK)) { + LOG.log(Level.FINE, "Client violates linemodeack sent: " + mask); + } + WAIT_LM_MODE_ACK = false; + } + skipToSE(); + }//handleLMMode + + public void handleLMSLC() throws IOException { + int[] triple = new int[3]; + if (!readTriple(triple)) return; + + //SLC will be initiated by the client + //case 1. client requests set + //LINEMODE SLC 0 SLC_DEFAULT 0 + if ((triple[0] == 0) && (triple[1] == LM_SLC_DEFAULT) && (triple[2] == 0)) { + skipToSE(); + //reply with SLC xxx SLC_DEFAULT 0 + rawWrite(IAC); + rawWrite(SB); + rawWrite(LINEMODE); + rawWrite(LM_SLC); + //triples defaults for all + for (int i = 1; i < 12; i++) { + rawWrite(i); + rawWrite(LM_SLC_DEFAULT); + rawWrite(0); + } + rawWrite(IAC); + rawWrite(SE); + flush(); + } else { + + //case 2: just acknowledge anything we get from the client + rawWrite(IAC); + rawWrite(SB); + rawWrite(LINEMODE); + rawWrite(LM_SLC); + rawWrite(triple[0]); + rawWrite(triple[1] | LM_SLC_ACK); + rawWrite(triple[2]); + while (readTriple(triple)) { + rawWrite(triple[0]); + rawWrite(triple[1] | LM_SLC_ACK); + rawWrite(triple[2]); + } + rawWrite(IAC); + rawWrite(SE); + flush(); + } + }//handleLMSLC + + public void handleLMForwardMask(int WHAT) throws IOException { + switch (WHAT) { + case WONT: + if (WAIT_LM_DO_REPLY_FORWARDMASK) { + WAIT_LM_DO_REPLY_FORWARDMASK = false; + } + break; + } + skipToSE(); + }//handleLMForward + + public void handleNEWENV() throws IOException { + LOG.log(Level.FINE, "handleNEWENV()"); + int c = rawread(); + switch (c) { + case IS: + handleNEIs(); + break; + case NE_INFO: + handleNEInfo(); + break; + default: + //skip to (including) SE + skipToSE(); + } + }//handleNEWENV + + /* + The characters following a "type" up to the next "type" or VALUE specify the + variable name. + + If a "type" is not followed by a VALUE + (e.g., by another VAR, USERVAR, or IAC SE) then that variable is + undefined. + */ + private int readNEVariableName(StringBuffer sbuf) throws IOException { + LOG.log(Level.FINE, "readNEVariableName()"); + int i = -1; + do { + i = rawread(); + if (i == -1) { + return NE_IN_ERROR; + } else if (i == IAC) { + i = rawread(); + if (i == IAC) { + //duplicated IAC + sbuf.append((char) i); + } else if (i == SE) { + return NE_IN_END; + } else { + //Error should have been duplicated + return NE_IN_ERROR; + } + } else if (i == NE_ESC) { + i = rawread(); + if (i == NE_ESC || i == NE_VAR || i == NE_USERVAR || i == NE_VALUE) { + sbuf.append((char) i); + } else { + return NE_IN_ERROR; + } + } else if (i == NE_VAR || i == NE_USERVAR) { + return NE_VAR_UNDEFINED; + } else if (i == NE_VALUE) { + return NE_VAR_DEFINED; + } else { + //check maximum length to prevent overflow + if (sbuf.length() >= NE_VAR_NAME_MAXLENGTH) { + //TODO: Log Overflow + return NE_IN_ERROR; + } else { + sbuf.append((char) i); + } + } + } while (true); + }//readNEVariableName + + + /* + The characters following a VALUE up to the next + "type" specify the value of the variable. + If a VALUE is immediately + followed by a "type" or IAC, then the variable is defined, but has + no value. + If an IAC is contained between the IS and the IAC SE, + it must be sent as IAC IAC. + */ + private int readNEVariableValue(StringBuffer sbuf) throws IOException { + LOG.log(Level.FINE, "readNEVariableValue()"); + //check conditions for first character after VALUE + int i = rawread(); + if (i == -1) { + return NE_IN_ERROR; + } else if (i == IAC) { + i = rawread(); + if (i == IAC) { + //Double IAC + return NE_VAR_DEFINED_EMPTY; + } else if (i == SE) { + return NE_IN_END; + } else { + //according to rule IAC has to be duplicated + return NE_IN_ERROR; + } + } else if (i == NE_VAR || i == NE_USERVAR) { + return NE_VAR_DEFINED_EMPTY; + } else if (i == NE_ESC) { + //escaped value + i = rawread(); + if (i == NE_ESC || i == NE_VAR || i == NE_USERVAR || i == NE_VALUE) { + sbuf.append((char) i); + } else { + return NE_IN_ERROR; + } + } else { + //character + sbuf.append((char) i); + } + //loop until end of value (IAC SE or TYPE) + do { + i = rawread(); + if (i == -1) { + return NE_IN_ERROR; + } else if (i == IAC) { + i = rawread(); + if (i == IAC) { + //duplicated IAC + sbuf.append((char) i); + } else if (i == SE) { + return NE_IN_END; + } else { + //Error should have been duplicated + return NE_IN_ERROR; + } + } else if (i == NE_ESC) { + i = rawread(); + if (i == NE_ESC || i == NE_VAR || i == NE_USERVAR || i == NE_VALUE) { + sbuf.append((char) i); + } else { + return NE_IN_ERROR; + } + } else if (i == NE_VAR || i == NE_USERVAR) { + return NE_VAR_OK; + } else { + //check maximum length to prevent overflow + if (sbuf.length() > NE_VAR_VALUE_MAXLENGTH) { + //TODO: LOG Overflow + return NE_IN_ERROR; + } else { + sbuf.append((char) i); + } + } + } while (true); + }//readNEVariableValue + + + public void readNEVariables() throws IOException { + LOG.log(Level.FINE, "readNEVariables()"); + StringBuffer sbuf = new StringBuffer(50); + int i = rawread(); + if (i == IAC) { + //invalid or empty response + skipToSE(); + LOG.log(Level.FINE, "readNEVariables()::INVALID VARIABLE"); + return; + } + boolean cont = true; + if (i == NE_VAR || i == NE_USERVAR) { + do { + switch (readNEVariableName(sbuf)) { + case NE_IN_ERROR: + LOG.log(Level.FINE, "readNEVariables()::NE_IN_ERROR"); + return; + case NE_IN_END: + LOG.log(Level.FINE, "readNEVariables()::NE_IN_END"); + return; + case NE_VAR_DEFINED: + LOG.log(Level.FINE, "readNEVariables()::NE_VAR_DEFINED"); + String str = sbuf.toString(); + sbuf.delete(0, sbuf.length()); + switch (readNEVariableValue(sbuf)) { + case NE_IN_ERROR: + LOG.log(Level.FINE, "readNEVariables()::NE_IN_ERROR"); + return; + case NE_IN_END: + LOG.log(Level.FINE, "readNEVariables()::NE_IN_END"); + return; + case NE_VAR_DEFINED_EMPTY: + LOG.log(Level.FINE, "readNEVariables()::NE_VAR_DEFINED_EMPTY"); + break; + case NE_VAR_OK: + //add variable + LOG.log(Level.FINE, "readNEVariables()::NE_VAR_OK:VAR=" + str + " VAL=" + sbuf.toString()); + TelnetIO.this.connectionData.getEnvironment().put(str, sbuf.toString()); + sbuf.delete(0, sbuf.length()); + break; + } + break; + case NE_VAR_UNDEFINED: + LOG.log(Level.FINE, "readNEVariables()::NE_VAR_UNDEFINED"); + break; + } + } while (cont); + } + }//readVariables + + public void handleNEIs() throws IOException { + LOG.log(Level.FINE, "handleNEIs()"); + if (isEnabled(NEWENV)) { + readNEVariables(); + } + }//handleNEIs + + public void handleNEInfo() throws IOException { + LOG.log(Level.FINE, "handleNEInfo()"); + if (isEnabled(NEWENV)) { + readNEVariables(); + } + }//handleNEInfo + + /** + * Method that sends a TTYPE Subnegotiation Request. + * IAC SB TERMINAL-TYPE SEND + */ + public void getTTYPE() throws IOException { + if (isEnabled(TTYPE)) { + rawWrite(IAC); + rawWrite(SB); + rawWrite(TTYPE); + rawWrite(SEND); + rawWrite(IAC); + rawWrite(SE); + flush(); + } + }//getTTYPE + + /** + * Method that sends a LINEMODE MODE Subnegotiation request. + * IAC LINEMODE MODE MASK SE + */ + public void negotiateLineMode() throws IOException { + if (isEnabled(LINEMODE)) { + rawWrite(IAC); + rawWrite(SB); + rawWrite(LINEMODE); + rawWrite(LM_MODE); + rawWrite(LM_EDIT | LM_TRAPSIG); + rawWrite(IAC); + rawWrite(SE); + WAIT_LM_MODE_ACK = true; + + //dont forwardmask + rawWrite(IAC); + rawWrite(SB); + rawWrite(LINEMODE); + rawWrite(DONT); + rawWrite(LM_FORWARDMASK); + rawWrite(IAC); + rawWrite(SE); + WAIT_LM_DO_REPLY_FORWARDMASK = true; + flush(); + } + }//negotiateLineMode + + /** + * Method that sends a NEW-ENVIRON SEND subnegotiation request + * for default variables and user variables. + * IAC SB NEW-ENVIRON SEND VAR USERVAR IAC SE + */ + private void negotiateEnvironment() throws IOException { + //log.debug("negotiateEnvironment()"); + if (isEnabled(NEWENV)) { + rawWrite(IAC); + rawWrite(SB); + rawWrite(NEWENV); + rawWrite(SEND); + rawWrite(NE_VAR); + rawWrite(NE_USERVAR); + rawWrite(IAC); + rawWrite(SE); + WAIT_NE_SEND_REPLY = true; + flush(); + } + }//negotiateEnvironment + + /** + * Method that skips a subnegotiation response. + */ + private void skipToSE() throws IOException { + while (rawread() != SE) ; + }//skipSubnegotiation + + private boolean readTriple(int[] triple) throws IOException { + triple[0] = rawread(); + triple[1] = rawread(); + if ((triple[0] == IAC) && (triple[1] == SE)) { + return false; + } else { + triple[2] = rawread(); + return true; + } + }//readTriple + + /** + * Method that reads a subnegotiation String, + * one of those that end with a IAC SE combination. + * A maximum length is observed to prevent overflow. + * + * @return IAC SE terminated String + */ + private String readIACSETerminatedString(int maxlength) throws IOException { + int where = 0; + char[] cbuf = new char[maxlength]; + char b = ' '; + boolean cont = true; + + do { + int i; + i = rawread(); + switch (i) { + case IAC: + i = rawread(); + if (i == SE) { + cont = false; + } + break; + case -1: + return "default"; + default: + } + if (cont) { + b = (char) i; + //Fix for overflow wimpi (10/06/2004) + if (b == '\n' || b == '\r' || where == maxlength) { + cont = false; + } else { + cbuf[where++] = b; + } + } + } while (cont); + + return (new String(cbuf, 0, where)); + }//readIACSETerminatedString + + /** + * Method that informs internally about the supported Negotiation Options + * + * @param i int that represents requested the Option + * @return Boolean that represents support status + */ + private boolean supported(int i) { + switch (i) { + case SUPGA: + case ECHO: + case NAWS: + case TTYPE: + case NEWENV: + return true; + case LINEMODE: + return connectionData.isLineMode(); + default: + return false; + } + }//supported + + /** + * Method that sends a Telnet IAC String with TelnetIO.write(byte b) method. + * + * @param i int that represents requested Command Type (DO,DONT,WILL,WONT) + * @param j int that represents the Option itself (e.g. ECHO, NAWS) + */ + private void sendCommand(int i, int j, boolean westarted) throws IOException { + rawWrite(IAC); + rawWrite(i); + rawWrite(j); + // we started with DO OPTION and now wait for reply + if ((i == DO) && westarted) setWait(DO, j, true); + // we started with WILL OPTION and now wait for reply + if ((i == WILL) && westarted) setWait(WILL, j, true); + flush(); + }//sendCommand + + /** + * Method enables or disables a supported Option + * + * @param i int that represents the Option + */ + private void enable(int i) throws IOException { + switch (i) { + case SUPGA: + DO_SUPGA = !DO_SUPGA; + break; + case ECHO: + DO_ECHO = !DO_ECHO; + break; + case NAWS: + DO_NAWS = !DO_NAWS; + break; + case TTYPE: + if (DO_TTYPE) { + DO_TTYPE = false; + } else { + DO_TTYPE = true; + getTTYPE(); + } + break; + case LINEMODE: + if (DO_LINEMODE) { + DO_LINEMODE = false; + //set false in connection data, so the application knows. + connectionData.setLineMode(false); + } else { + DO_LINEMODE = true; + negotiateLineMode(); + } + break; + case NEWENV: + if (DO_NEWENV) { + DO_NEWENV = false; + } else { + DO_NEWENV = true; + negotiateEnvironment(); + } + break; + } + }//enable + + /** + * Method that informs internally about the status of the supported + * Negotiation Options. + * + * @param i int that represents requested the Option + * @return Boolean that represents the enabled status + */ + private boolean isEnabled(int i) { + switch (i) { + case SUPGA: + return DO_SUPGA; + case ECHO: + return DO_ECHO; + case NAWS: + return DO_NAWS; + case TTYPE: + return DO_TTYPE; + case LINEMODE: + return DO_LINEMODE; + case NEWENV: + return DO_NEWENV; + default: + return false; + } + }//isEnabled + + /** + * Method that informs internally about the WILL wait status + * of an option. + * + * @param i that represents requested the Option + * @return Boolean that represents WILL wait status of the Option + */ + private boolean waitWILLreply(int i) { + switch (i) { + case SUPGA: + return WAIT_WILL_REPLY_SUPGA; + case ECHO: + return WAIT_WILL_REPLY_ECHO; + case NAWS: + return WAIT_WILL_REPLY_NAWS; + case TTYPE: + return WAIT_WILL_REPLY_TTYPE; + default: + return false; + } + }//waitWILLreply + + /** + * Method that informs internally about the DO wait status + * of an option. + * + * @param i Integer that represents requested the Option + * @return Boolean that represents DO wait status of the Option + */ + private boolean waitDOreply(int i) { + switch (i) { + case SUPGA: + return WAIT_DO_REPLY_SUPGA; + case ECHO: + return WAIT_DO_REPLY_ECHO; + case NAWS: + return WAIT_DO_REPLY_NAWS; + case TTYPE: + return WAIT_DO_REPLY_TTYPE; + case LINEMODE: + return WAIT_DO_REPLY_LINEMODE; + case NEWENV: + return WAIT_DO_REPLY_NEWENV; + default: + return false; + } + }//waitDOreply + + /** + * Method that mutates the wait status of an option in + * negotiation. We need the wait status to keep track of + * negotiation in process. So we cant miss if we started out + * or the other and so on. + * + * @param WHAT Integer values of DO or WILL + * @param OPTION Integer that represents the Option + * @param WAIT Boolean that represents the status of wait that should be set + */ + private void setWait(int WHAT, int OPTION, boolean WAIT) { + switch (WHAT) { + case DO: + switch (OPTION) { + case SUPGA: + WAIT_DO_REPLY_SUPGA = WAIT; + break; + case ECHO: + WAIT_DO_REPLY_ECHO = WAIT; + break; + case NAWS: + WAIT_DO_REPLY_NAWS = WAIT; + break; + case TTYPE: + WAIT_DO_REPLY_TTYPE = WAIT; + break; + case LINEMODE: + WAIT_DO_REPLY_LINEMODE = WAIT; + break; + case NEWENV: + WAIT_DO_REPLY_NEWENV = WAIT; + break; + } + break; + case WILL: + switch (OPTION) { + case SUPGA: + WAIT_WILL_REPLY_SUPGA = WAIT; + break; + case ECHO: + WAIT_WILL_REPLY_ECHO = WAIT; + break; + case NAWS: + WAIT_WILL_REPLY_NAWS = WAIT; + break; + case TTYPE: + WAIT_WILL_REPLY_TTYPE = WAIT; + break; + } + break; + } + }//setWait + + }//inner class IACHandler + + /** end Constants declaration **************************************************/ + +}//class TelnetIO diff --git a/gogo/pom.xml b/gogo/pom.xml new file mode 100644 index 00000000000..76bab43e2d2 --- /dev/null +++ b/gogo/pom.xml @@ -0,0 +1,54 @@ + + + + + 4.0.0 + + + org.apache.felix + felix-parent + 5 + ../pom/pom.xml + + + pom + Apache Felix Gogo + Apache Felix Gogo Subproject + gogo-reactor + 1.1.0-SNAPSHOT + http://felix.apache.org/ + + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/gogo/ + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/gogo/ + https://svn.apache.org/repos/asf/felix/trunk/gogo/ + + + + gogo-parent + runtime + jline + shell + command + itest-shell + itest-jline + bom + + + diff --git a/gogo/runtime/DEPENDENCIES b/gogo/runtime/DEPENDENCIES new file mode 100644 index 00000000000..500120bfdbb --- /dev/null +++ b/gogo/runtime/DEPENDENCIES @@ -0,0 +1,23 @@ +Apache Felix Gogo Runtime +Copyright 2011 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 diff --git a/gogo/runtime/LICENSE b/gogo/runtime/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/gogo/runtime/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/gogo/runtime/NOTICE b/gogo/runtime/NOTICE new file mode 100644 index 00000000000..cb48c521e18 --- /dev/null +++ b/gogo/runtime/NOTICE @@ -0,0 +1,10 @@ +Apache Felix Gogo Runtime +Copyright 2014 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developped by +Udo Klimaschewski (http://UdoJava.com/). +Licensed under the MIT License. diff --git a/gogo/runtime/doc/apache-felix-gogo-Dateien/apache.png b/gogo/runtime/doc/apache-felix-gogo-Dateien/apache.png new file mode 100644 index 00000000000..5132f65ec15 Binary files /dev/null and b/gogo/runtime/doc/apache-felix-gogo-Dateien/apache.png differ diff --git a/gogo/runtime/doc/apache-felix-gogo-Dateien/button.html b/gogo/runtime/doc/apache-felix-gogo-Dateien/button.html new file mode 100644 index 00000000000..8503c58f645 --- /dev/null +++ b/gogo/runtime/doc/apache-felix-gogo-Dateien/button.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/gogo/runtime/doc/apache-felix-gogo-Dateien/button_data/2010-na-125x125.png b/gogo/runtime/doc/apache-felix-gogo-Dateien/button_data/2010-na-125x125.png new file mode 100644 index 00000000000..90bd87b2636 Binary files /dev/null and b/gogo/runtime/doc/apache-felix-gogo-Dateien/button_data/2010-na-125x125.png differ diff --git a/gogo/runtime/doc/apache-felix-gogo-Dateien/logo.png b/gogo/runtime/doc/apache-felix-gogo-Dateien/logo.png new file mode 100644 index 00000000000..dccbddcc35c Binary files /dev/null and b/gogo/runtime/doc/apache-felix-gogo-Dateien/logo.png differ diff --git a/gogo/runtime/doc/apache-felix-gogo-Dateien/site.css b/gogo/runtime/doc/apache-felix-gogo-Dateien/site.css new file mode 100644 index 00000000000..959ab0a592d --- /dev/null +++ b/gogo/runtime/doc/apache-felix-gogo-Dateien/site.css @@ -0,0 +1,25 @@ +/* @override http://felix.apache.org/site/media.data/site.css */ + +body { background-color: #ffffff; color: #3b3b3b; font-family: Tahoma, Arial, sans-serif; font-size: 10pt; line-height: 140% } +h1, h2, h3, h4, h5, h6 { font-weight: normal; color: #000000; line-height: 100%; margin-top: 0px} +h1 { font-size: 200% } +h2 { font-size: 175% } +h3 { font-size: 150% } +h4 { font-size: 140% } +h5 { font-size: 130% } +h6 { font-size: 120% } +a { color: #1980af } +a:visited { color: #1980af } +a:hover { color: #1faae9 } +.title { position: absolute; left: 1px; right: 1px; top:25px; height: 81px; background: url(http://felix.apache.org/site/media.data/gradient.png) repeat-x; background-position: bottom; } +.logo { position: absolute; width: 15em; height: 81px; text-align: center; } +.header { text-align: right; margin-right: 20pt; margin-top: 30pt;} +.menu { border-top: 10px solid #f9bb00; position: absolute; top: 107px; left: 1px; width: 15em; bottom: 0px; padding: 0px; background-color: #fcfcfc } +.menu ul { background-color: #fdf5d9; list-style: none; padding-left: 4em; margin-top: 0px; padding-top: 2em; padding-bottom: 2em; margin-left: 0px; color: #4a4a43} +.menu a { text-decoration: none; color: #4a4a43 } +.main { position: absolute; border-top: 10px solid #cde0ea; top: 107px; left: 15em; right: 1px; margin-left: 2px; padding-right: 4em; padding-left: 1em; padding-top: 1em;} +.code { background-color: #eeeeee; border: solid 1px black; padding: 0.5em } +.code-keyword { color: #880000 } +.code-quote { color: #008800 } +.code-object { color: #0000dd } +.code-java { margin: 0em } \ No newline at end of file diff --git a/gogo/runtime/doc/apache-felix-gogo.html b/gogo/runtime/doc/apache-felix-gogo.html new file mode 100644 index 00000000000..3d18fe26ec5 --- /dev/null +++ b/gogo/runtime/doc/apache-felix-gogo.html @@ -0,0 +1,70 @@ + + + + + + Apache Felix - Apache Felix Gogo + + + +
      Apache
      + +
      +

      Apache Felix Gogo

      + +

      Apache Felix Gogo is a subproject of Apache Felix implementing the +OSGi RFC 147, which describes a standard shell for OSGi-based +environments. See RFC 147 Overview for more information.

      + +

      Using Gogo with the Felix Framework

      + +

      The Gogo subproject consists of three bundles:

      + +
        +
      1. runtime - implements the core command processing functionality.
      2. +
      3. shell - provides a simple textual user interface to interact with the command processor.
      4. +
      5. command - implements a set of basic commands.
      6. +
      + + +

      As of the Apache Felix Framework 3.0.0, Gogo is included as the +default shell in the framework distribution. To use it, you just start +the framework like normal:

      + +
      +
      $ cd felix-framework-3.0.0
      +$ java -jar bin/felix.jar
      +_______________
      +Welcome to Apache Felix Gogo
      +
      +g! lb
      +START LEVEL 1
      +   ID|State      |Level|Name
      +    0|Active     |    0|System Bundle (3.0.0)
      +    1|Active     |    1|Apache Felix Bundle Repository (1.6.2)
      +    2|Active     |    1|Apache Felix Gogo Command (0.6.0)
      +    3|Active     |    1|Apache Felix Gogo Runtime (0.6.0)
      +    4|Active     |    1|Apache Felix Gogo Shell (0.6.0)
      +g! 
      +
      +
      + +

      Gogo shell integration in the framework distribution is also discussed in the framework usage document

      +
      + \ No newline at end of file diff --git a/gogo/runtime/doc/changelog.txt b/gogo/runtime/doc/changelog.txt new file mode 100644 index 00000000000..5e3cf5c2039 --- /dev/null +++ b/gogo/runtime/doc/changelog.txt @@ -0,0 +1,187 @@ +Changes 1.1.0 to 1.1.2 +---------------------- +Improvement + [FELIX-5970] - Add requirement & capabilities model so gogo can be resolved + [FELIX-5999] - cleanup compiler warnings + [FELIX-6001] - cleanup compiler warnings + [FELIX-6003] - Add some resolver checks to make sure @RequireGogo annotation works for both jline and shell + [FELIX-6007] - create a gogo bom + +Changes 1.0.12 to 1.1.0 +----------------------- +New Feature + [FELIX-5835] - Upgrade to JDK 8 + [FELIX-5836] - Upgrade to OSGi r6 + +Improvement + [FELIX-5855] - Support array subscript in expander + [FELIX-5856] - Coercion between Object[] and List + [FELIX-5857] - Provide a context classloader on the session to help with class loading + +Changes 1.0.10 to 1.0.12 +------------------------ +Bug + [FELIX-5805] - [gogo][runtime] OOM caused by endless loop when parsing "]" + [FELIX-5821] - [gogo][runtime] Hitting Ctrl+C may kill the console output thread + +Improvement + [FELIX-5726] - Thread create by gogo should be named + +Changes 1.0.8 to 1.0.10 +----------------------- +Bug + [FELIX-5706] - Unable to access DTO fields using reflection + +Improvement + [FELIX-5715] - Central point to allow customization of security related checks + +Changes 1.0.6 to 1.0.8 +---------------------- +Bug + [FELIX-5637] - [gogo][runtime] Error throwns by commands are swallowed and lost + [FELIX-5655] - Possible NPE in Expression.isNumber when passing an empty string + [FELIX-5656] - [gogo][runtime] The Expander does not correctly support octal values + +Changes 1.0.4 to 1.0.6 +---------------------- +Bug + [FELIX-5599] - Allow reusing a closure from a parent session + +Improvement + [FELIX-5633] - [gogo][runtime] The file name generation should take ~ into account + [FELIX-5634] - [gogo][runtime] The file name generation may loop into subtrees for nothing + +Changes 1.0.2 to 1.0.4 +---------------------- +Bug + [FELIX-5462] - [gogo][runtime] Unable to add a converter using reflection + [FELIX-5465] - [gogo][runtime] No way to concatenate arrays + [FELIX-5486] - [gogo][runtime] Avoid unnecessary conversions from strings to booleans/integers + [FELIX-5541] - ArrayIndexOutOfBoundsException in the parser + +Changes 1.0.0 to 1.0.2 +---------------------- +Bug + [FELIX-5433] - Interrupting a job should interrupt its children + [FELIX-5440] - [gogo] Do not print results at end of pipe + [FELIX-5441] - [gogo] Do not print stack traces when inspection can not access fields + +Changes 0.16.2 to 1.0.0 +----------------------- +New Feature + [FELIX-5272] - New gogo features + +Improvement + [FELIX-4970] - all felix poms should advertise their SCM + +Task + [FELIX-5378] - [gogo] Upgrade packages and bundle to 1.0.0 + +Changes 0.14.0 to 0.16.2 +------------------------ + +** Bug + * [FELIX-4783] - ConcurrentModificationException when stopping + +** New Feature + * [FELIX-4671] - Provide an expression parser + +Changes 0.12.1 to 0.14.0 +------------------------ + +** Bug + * [FELIX-4637] - Gogo can't cope without several commands with defined service.ranking + +** New Feature + * [FELIX-4671] - Provide an expression parser + + +Changes 0.12.0 to 0.12.1 +------------------------ + +** Bug + * [FELIX-4533] - Gogo runtime does not advertise published services + + +Changes 0.10.0 to 0.12.0 +------------------------ + +** Bug + * [FELIX-4336] - The use of inheritable thread locals in ThreadIO can cause problems + +** Improvement + * [FELIX-3590] - Add system property resolution to CommandSessionImpl + * [FELIX-4363] - The CommandSession get/set methods are not thread safe + + +Changes 0.8.0 to 0.10.0 +----------------------- + +** Bug + * [FELIX-2870] - ConcurrentModificationException in gogo runtime + * [FELIX-2894] - Gogo does not handles options but not parameters + * [FELIX-2927] - [gogo] coercion mechanism invokes foo(String) instead + of foo(int) - even with explicit int argument + +Changes 0.6.1 to 0.8.0 +---------------------- + +** Bug + * [FELIX-2723] - When the execution of a closure with pipes is + interrupted, pipes should be interrupted too + +** New Feature + * [FELIX-2761] - Add a way to listen to execution of command lines + * [FELIX-2764] - Publish OSGi EventAdmin events when executing a command + line + +Changes 0.6.0 to 0.6.1 +---------------------- + +** Bug + * [FELIX-1473] - [gogo] The syntax does not provide a way to call + methods on a string + * [FELIX-2432] - [gogo] NPE/coercion failure when null first parameter + to method expecting arry + * [FELIX-2545] - [gogo] runtime should close all open sessions when it + is stopped + * [FELIX-2606] - Gogo isn't decoupled from OSGi anymore + +** Improvement + * [FELIX-2396] - Ability to have callbacks when accessing session variables + * [FELIX-2433] - [gogo] allow command expansion inside double quotes + * [FELIX-2607] - Introduce a specific exception when a command is not + foundGogo Runtime 0.6.1 + +Gogo Runtime 0.6.0 +------------------ + +** Bug + * [FELIX-1473] - [gogo] The syntax does not provide a way to call methods + on a string + * [FELIX-1474] - [gogo] result of commands is implicitly written to pipe + * [FELIX-1493] - [gogo] automatic expansion of $args in Closure stops + direct access to $args list + * [FELIX-2337] - [gogo] no way to access array[] elements produced by + assignment + * [FELIX-2375] - [gogo] when supplied args can't be coerced, the error + message prints the arg values, rather than their types + * [FELIX-2380] - [gogo] lock contention in piped writer when reader + doesn't read all input + +** Improvement + * [FELIX-1487] - Support for commands on multiple lines + * [FELIX-2328] - [gogo] tidy-up runtime to remove optional code etc + * [FELIX-2339] - [gogo] add support for running scripts + * [FELIX-2342] - [gogo] remove old felix command adaptor + +** New Feature + * [FELIX-2363] - [Gogo] Add annotations for creating commands with + optional and out-of-order arguments + +** Task + * [FELIX-1670] - [gogo] launcher bundle not required + * [FELIX-1889] - Gogo should depend on the official OSGi jars + * [FELIX-2334] - [Gogo] Use org.apache.felix as Maven groupId + * [FELIX-2367] - [Gogo] Use org.apache.felix namespace to avoid any + perceived legal issues diff --git a/gogo/runtime/pom.xml b/gogo/runtime/pom.xml new file mode 100644 index 00000000000..7919ff6d01a --- /dev/null +++ b/gogo/runtime/pom.xml @@ -0,0 +1,97 @@ + + + + + 4.0.0 + + + org.apache.felix + gogo-parent + 6-SNAPSHOT + ../gogo-parent/pom.xml + + + bundle + Apache Felix Gogo Runtime + org.apache.felix.gogo.runtime + 1.1.3-SNAPSHOT + http://felix.apache.org/ + + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/gogo/runtime/ + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/gogo/runtime/ + https://svn.apache.org/repos/asf/felix/trunk/gogo/runtime/ + + + + + org.osgi + org.osgi.namespace.service + + + org.osgi + org.osgi.service.component.annotations + 1.4.0 + provided + + + org.osgi + org.osgi.service.event + + + org.osgi + osgi.annotation + + + org.osgi + osgi.core + + + junit + junit + + + org.mockito + mockito-core + + + + + + org.apache.felix + maven-bundle-plugin + + + + !org.apache.felix.gogo.runtime.util.function, + org.apache.felix.gogo.runtime.*; version=${project.version}, + org.apache.felix.service.command, + org.apache.felix.service.command.annotations, + org.apache.felix.service.threadio, + + + org.osgi.service.event.*; resolution:=optional, + * + + + + + + + diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/ArgList.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/ArgList.java new file mode 100644 index 00000000000..fe8eefb8d12 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/ArgList.java @@ -0,0 +1,69 @@ +/* + * 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.gogo.runtime; + +import java.util.AbstractList; +import java.util.List; + +/** + * List that overrides toString() for implicit $args expansion. + * Also checks for index out of bounds, so that $1 evaluates to null + * rather than throwing IndexOutOfBoundsException. + * e.g. x = { a$args }; x 1 2 => a1 2 and not a[1, 2] + */ +public class ArgList extends AbstractList +{ + private List list; + + public ArgList(List args) + { + this.list = args; + } + + @Override + public String toString() + { + StringBuilder buf = new StringBuilder(); + for (Object o : list) + { + if (buf.length() > 0) + buf.append(' '); + buf.append(o); + } + return buf.toString(); + } + + @Override + public Object get(int index) + { + return index < list.size() ? list.get(index) : null; + } + + @Override + public Object remove(int index) + { + return list.remove(index); + } + + @Override + public int size() + { + return list.size(); + } +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/BaseTokenizer.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/BaseTokenizer.java new file mode 100644 index 00000000000..57ca64f2c46 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/BaseTokenizer.java @@ -0,0 +1,290 @@ +/* + * 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.gogo.runtime; + +public class BaseTokenizer +{ + + protected static final char EOT = (char) -1; + + protected final Token text; + + protected short line; + protected short column; + protected char ch; + protected int index; + + public BaseTokenizer(CharSequence text) + { + this.text = text instanceof Token ? (Token) text : new Token(text); + getch(); + } + + public Token text() + { + return text; + } + + protected void find(char target, char deeper) + { + final short sLine = line; + final short sCol = column; + int level = 1; + + while (level != 0) + { + if (eot()) + { + throw new EOFError(sLine, sCol, "unexpected eof found in the middle of a compound for '" + + deeper + target + "'", "compound", Character.toString(target)); + // TODO: fill context correctly + } + + getch(); + if (ch == '\\') + { + escape(); + continue; + } + if (ch == target) + { + level--; + } + else + { + if (ch == deeper) + { + level++; + } + else + { + if (ch == '"' || ch == '\'' || ch == '`') + { + skipQuote(); + } + } + } + } + } + + protected char escape() + { + assert '\\' == ch; + final short sLine = line; + final short sCol = column; + + switch (getch()) + { + case 'u': + getch(); + int nb = 0; + for (int i = 0; i < 4; i++) + { + char ch = Character.toUpperCase(this.ch); + if (ch >= '0' && ch <= '9') + { + nb = nb * 16 + (ch - '0'); + getch(); + continue; + } + if (ch >= 'A' && ch <= 'F') + { + nb = nb * 16 + (ch - 'A' + 10); + getch(); + continue; + } + if (ch == 0) { + throw new EOFError(sLine, sCol, "unexpected EOT in \\ escape", "escape", "0"); + } else { + throw new SyntaxError(sLine, sCol, "bad unicode", text); + } + } + index--; + return (char) nb; + + case EOT: + throw new EOFError(sLine, sCol, "unexpected EOT in \\ escape", "escape", " "); + + case '\n': + return '\0'; // line continuation + + case '\\': + case '\'': + case '"': + case '$': + return ch; + + default: + return ch; + } + } + + protected void skipQuote() + { + assert '\'' == ch || '"' == ch; + final char quote = ch; + final short sLine = line; + final short sCol = column; + + while (getch() != EOT) + { + if (quote == ch) + { + return; + } + + if ((quote == '"') && ('\\' == ch)) + escape(); + } + + throw new EOFError(sLine, sCol, "unexpected EOT looking for matching quote: " + + quote, + quote == '"' ? "dquote" : "quote", + Character.toString(quote)); + } + + protected void skipSpace() + { + skipSpace(false); + } + + protected void skipSpace(boolean skipNewLines) + { + while (true) + { + while (isBlank(ch)) + { + getch(); + } + + // skip continuation lines, but not other escapes + if (('\\' == ch) && (peek() == '\n')) + { + getch(); + getch(); + continue; + } + + if (skipNewLines && ('\n' == ch)) + { + getch(); + continue; + } + + if (skipNewLines && ('\r' == ch) && (peek() == '\n')) + { + getch(); + getch(); + continue; + } + + // skip comments + if (('/' == ch) || ('#' == ch)) + { + if (('#' == ch) || (peek() == '/')) + { + while ((getch() != EOT) && ('\n' != ch)) + { + //loop + } + continue; + } + else if ('*' == peek()) + { + short sLine = line; + short sCol = column; + getch(); + + while ((getch() != EOT) && !(('*' == ch) && (peek() == '/'))) + { + //loop + } + + if (EOT == ch) + { + throw new EOFError(sLine, sCol, + "unexpected EOT looking for closing comment: */", + "comment", + "*/"); + } + + getch(); + getch(); + continue; + } + } + + break; + } + } + + protected boolean isBlank(char ch) + { + return ' ' == ch || '\t' == ch; + } + + protected boolean eot() + { + return index >= text.length(); + } + + protected char getch() + { + return ch = getch(false); + } + + protected char peek() + { + return getch(true); + } + + protected char getch(boolean peek) + { + if (eot()) + { + if (!peek) + { + ++index; + ch = EOT; + } + return EOT; + } + + int current = index; + char c = text.charAt(index++); + +// if (('\r' == c) && !eot() && (text.charAt(index) == '\n')) +// c = text.charAt(index++); + + if (peek) + { + index = current; + } + else if ('\n' == c) + { + ++line; + column = 0; + } + else + ++column; + + return c; + } + +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Closure.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Closure.java new file mode 100644 index 00000000000..8667b944f9a --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Closure.java @@ -0,0 +1,778 @@ +/* + * 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.gogo.runtime; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.channels.Channel; +import java.nio.channels.Channels; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.WritableByteChannel; +import java.nio.channels.spi.AbstractInterruptibleChannel; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.felix.service.command.Job.Status; +import org.apache.felix.gogo.runtime.Parser.Array; +import org.apache.felix.gogo.runtime.Parser.Executable; +import org.apache.felix.gogo.runtime.Parser.Operator; +import org.apache.felix.gogo.runtime.Parser.Pipeline; +import org.apache.felix.gogo.runtime.Parser.Program; +import org.apache.felix.gogo.runtime.Parser.Sequence; +import org.apache.felix.gogo.runtime.Parser.Statement; +import org.apache.felix.gogo.runtime.Pipe.Result; +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Function; + +public class Closure implements Function, Evaluate +{ + + public static final String LOCATION = ".location"; + public static final String PIPE_EXCEPTION = "pipe-exception"; + private static final String DEFAULT_LOCK = ".defaultLock"; + + private static final ThreadLocal location = new ThreadLocal<>(); + + private final CommandSessionImpl session; + private final Closure parent; + private final CharSequence source; + private final Program program; + private final Object script; + + private Token errTok; + private Token errTok2; + private List parms = null; + private List parmv = null; + + public Closure(CommandSessionImpl session, Closure parent, CharSequence source) throws Exception + { + this.session = session; + this.parent = parent; + this.source = source; + this.script = session.get("0"); // by convention, $0 is script name + + if (source instanceof Program) + { + program = (Program) source; + } + else + { + try + { + this.program = new Parser(source).program(); + } + catch (Exception e) + { + throw setLocation(e); + } + } + } + + public Closure(CommandSessionImpl session, Closure parent, Program program) + { + this.session = session; + this.parent = parent; + this.source = program; + this.script = session.get("0"); // by convention, $0 is script name + this.program = program; + } + + public CommandSessionImpl session() + { + return session; + } + + private Exception setLocation(Exception e) + { + if (session.get(DEFAULT_LOCK) == null) + { + String loc = location.get(); + if (null == loc) + { + loc = (null == script ? "" : script + ":"); + + if (e instanceof SyntaxError) + { + SyntaxError se = (SyntaxError) e; + loc += se.line() + "." + se.column(); + } + else if (null != errTok) + { + loc += errTok.line + "." + errTok.column; + } + + location.set(loc); + } + else if (null != script && !loc.contains(":")) + { + location.set(script + ":" + loc); + } + + session.put(LOCATION, location.get()); + } + + if (e instanceof EOFError) + { // map to public exception, so interactive clients can provide more input + EOFException eofe = new EOFException(e.getMessage()); + eofe.initCause(e); + return eofe; + } + + return e; + } + + // implements Function interface + public Object execute(CommandSession x, List values) throws Exception + { + return execute(x, values, null); + } + + public Object execute(CommandSession x, List values, Channel capturingOutput) throws Exception + { + if (x != null && x != session) + { + if (x instanceof CommandSessionImpl) + { + return new Closure((CommandSessionImpl) x, null, program).execute(x, values, capturingOutput); + } + else + { + throw new IllegalStateException("The current session is not a Gogo session"); + } + } + try + { + location.remove(); + session.put(LOCATION, null); + return execute(values, capturingOutput); + } + catch (Exception e) + { + throw setLocation(e); + } + } + + @SuppressWarnings("unchecked") + private Object execute(List values, Channel capturingOutput) throws Exception + { + if (null != values) + { + parmv = new ArrayList<>(values); + parms = new ArgList(parmv); + } + else if (null != parent) + { + // inherit parent closure parameters + parmv = parent.parmv != null ? new ArrayList<>(parent.parmv) : null; + parms = parmv != null ? new ArgList(parmv) : null; + } + else + { + // inherit session parameters + Object args = session.get("args"); + if (null != args && args instanceof List) + { + parmv = new ArrayList<>((List) args); + parms = new ArgList(parmv); + } + } + + Result last = null; + Operator operator = null; + for (Iterator iterator = program.tokens().iterator(); iterator.hasNext();) + { + Operator prevOperator = operator; + Executable executable = iterator.next(); + if (iterator.hasNext()) { + operator = (Operator) iterator.next(); + } else { + operator = null; + } + if (prevOperator != null) { + if (Token.eq("&&", prevOperator)) { + if (!last.isSuccess()) { + continue; + } + } + else if (Token.eq("||", prevOperator)) { + if (last.isSuccess()) { + continue; + } + } + } + + Channel[] streams; + boolean[] toclose = new boolean[10]; + if (Pipe.getCurrentPipe() != null) { + streams = Pipe.getCurrentPipe().streams.clone(); + } else { + streams = new Channel[10]; + streams[0] = session.channels[0]; + streams[1] = new WritableByteChannelImpl((WritableByteChannel) session.channels[1]); + streams[2] = session.channels[1] == session.channels[2] + ? streams[1] + : new WritableByteChannelImpl((WritableByteChannel) session.channels[2]); + } + if (capturingOutput != null) { + streams[1] = capturingOutput; + toclose[1] = true; + } + + CommandSessionImpl.JobImpl job; + if (executable instanceof Pipeline) { + Pipeline pipeline = (Pipeline) executable; + List exec = pipeline.tokens(); + Token s = exec.get(0); + Token e = exec.get(exec.size() - 1); + Token t = program.subSequence(s.start - program.start, e.start + e.length - program.start); + job = session().createJob(t); + for (int i = 0; i < exec.size(); i++) { + Statement ex = (Statement) exec.get(i); + Operator op = i < exec.size() - 1 ? (Operator) exec.get(++i) : null; + Channel[] nstreams; + boolean[] ntoclose; + boolean endOfPipe; + if (i == exec.size() - 1) { + nstreams = streams; + ntoclose = toclose; + endOfPipe = true; + } else if (Token.eq("|", op)) { + PipedInputStream pis = new PipedInputStream(); + PipedOutputStream pos = new PipedOutputStream(pis); + nstreams = streams.clone(); + nstreams[1] = Channels.newChannel(pos); + ntoclose = toclose.clone(); + ntoclose[1] = true; + streams[0] = Channels.newChannel(pis); + toclose[0] = true; + endOfPipe = false; + } else if (Token.eq("|&", op)) { + PipedInputStream pis = new PipedInputStream(); + PipedOutputStream pos = new PipedOutputStream(pis); + nstreams = streams.clone(); + nstreams[1] = nstreams[2] = Channels.newChannel(pos); + ntoclose = toclose.clone(); + ntoclose[1] = ntoclose[2] = true; + streams[0] = Channels.newChannel(pis); + toclose[0] = true; + endOfPipe = false; + } else { + throw new IllegalStateException("Unrecognized pipe operator: '" + op + "'"); + } + Pipe pipe = new Pipe(this, job, ex, nstreams, ntoclose, endOfPipe); + job.addPipe(pipe); + } + } else { + job = session().createJob(executable); + Pipe pipe = new Pipe(this, job, (Statement) executable, streams, toclose, true); + job.addPipe(pipe); + } + + // Start pipe in background + if (operator != null && Token.eq("&", operator)) { + job.start(Status.Background); + last = new Result((Object) null); + } + // Start in foreground and wait for results + else { + last = job.start(Status.Foreground); + if (last == null) { + last = new Result((Object) null); + } else if (last.exception != null) { + throw last.exception; + } + } + } + + return last == null ? null : last.result; + } + + private static class WritableByteChannelImpl extends AbstractInterruptibleChannel + implements WritableByteChannel { + private final WritableByteChannel out; + + WritableByteChannelImpl(WritableByteChannel out) { + this.out = out; + } + + public int write(ByteBuffer src) throws IOException { + if (!isOpen()) { + throw new ClosedChannelException(); + } + begin(); + try { + return out.write(src); + } finally { + end(true); + } + } + + protected void implCloseChannel() { + } + } + + static Object eval(Object v) + { + String s = v.toString(); + if ("null".equals(s)) + { + v = null; + } + else if ("false".equals(s)) + { + v = false; + } + else if ("true".equals(s)) + { + v = true; + } + else + { + try + { + v = s; + v = Double.parseDouble(s); // if it parses as double + v = Long.parseLong(s); // see whether it is integral + } + catch (NumberFormatException e) + { + // Ignore + } + } + return v; + } + + public Object eval(final Token t) throws Exception + { + return eval(t, true); + } + + public Object eval(final Token t, boolean convertNumeric) throws Exception + { + if (t instanceof Parser.Closure) + { + return new Closure(session, this, ((Parser.Closure) t).program()); + } + else if (t instanceof Sequence) + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Channel out = Channels.newChannel(baos); + Object result = new Closure(session, this, ((Sequence) t).program()) + .execute(session, parms, out); + if (result != null) { + return result; + } else { + String s = baos.toString(); + while (!s.isEmpty() && s.charAt(s.length() - 1) == '\n') { + s = s.substring(0, s.length() - 1); + } + return s; + } + } + else if (t instanceof Array) + { + return array((Array) t); + } + else { + Object v = Expander.expand(t, this); + if (t == v) + { + if (convertNumeric) + { + v = eval(v); + } + } + return v; + } + } + + public Object execute(Executable executable) throws Exception + { + if (executable instanceof Statement) + { + return executeStatement((Statement) executable); + } + else if (executable instanceof Sequence) + { + return new Closure(session, this, ((Sequence) executable).program()).execute(new ArrayList<>(), null); + } + else + { + throw new IllegalStateException(); + } + } + + /* + * executeStatement handles the following cases: + * '=' word // simple assignment + * '=' word word.. // complex assignment + * word.. // command invocation + * // value of + * word.. // method call + */ + public Object executeStatement(Statement statement) throws Exception + { + Object echo = session.get("echo"); + String xtrace = null; + + if (echo != null && !"false".equals(echo.toString())) + { + // set -x execution trace + xtrace = "+" + statement; + session.perr.println(xtrace); + } + + List tokens = statement.tokens(); + if (tokens.isEmpty()) + { + return null; + } + + List values = new ArrayList<>(); + errTok = tokens.get(0); + + if ((tokens.size() > 3) && Token.eq("=", tokens.get(1))) + { + errTok2 = tokens.get(2); + } + + for (Token t : tokens) + { + Object v = eval(t, values.isEmpty()); + +// if ((Token.Type.EXECUTION == t.type) && (tokens.size() == 1)) { +// return v; +// } + + if (v instanceof ArgList) + { + values.addAll((ArgList) v); // explode $args array + } + else + { + values.add(v); + } + } + + Object cmd = values.remove(0); + if (cmd == null) + { + if (values.isEmpty()) + { + return null; + } + + throw new RuntimeException("Command name evaluates to null: " + errTok); + } + + if (cmd instanceof CharSequence && values.size() > 0 + && Token.eq("=", tokens.get(1))) + { + values.remove(0); + String scmd = cmd.toString(); + Object value; + + if (values.size() == 0) + { + return session.put(scmd, null); + } + + if (values.size() == 1) + { + value = values.get(0); + } + else + { + cmd = values.remove(0); + if (null == cmd) + { + throw new RuntimeException("Command name evaluates to null: " + + errTok2); + } + + trace2(xtrace, cmd, values); + + value = bareword(tokens.get(2), cmd) ? executeCmd(cmd.toString(), values) + : executeMethod(cmd, values); + } + + return assignment(scmd, value); + } + + trace2(xtrace, cmd, values); + + return bareword(tokens.get(0), cmd) ? executeCmd(cmd.toString(), values) + : executeMethod(cmd, values); + } + + // second level expanded execution trace + private void trace2(String trace1, Object cmd, List values) + { + if ("verbose".equals(session.get("echo"))) + { + StringBuilder buf = new StringBuilder("+ " + cmd); + + for (Object value : values) + { + buf.append(' '); + buf.append(value); + } + + String trace2 = buf.toString(); + + if (!trace2.equals(trace1)) + { + session.perr.println("+" + trace2); + } + } + } + + private boolean bareword(Token t, Object v) { + return v instanceof CharSequence && Token.eq(t, (CharSequence) v); + } + + private Object executeCmd(String scmd, List values) throws Exception + { + String scopedFunction = scmd; + Object x = get(scmd); + + if (!(x instanceof Function)) + { + if (scmd.indexOf(':') < 0) + { + scopedFunction = "*:" + scmd; + } + + x = get(scopedFunction); + + if (x == null || !(x instanceof Function)) + { + // try default command handler + if (session.get(DEFAULT_LOCK) == null) + { + x = get("default"); + if (x == null) + { + x = get("*:default"); + } + + if (x instanceof Function) + { + try + { + session.put(DEFAULT_LOCK, true); + values.add(0, scmd); + return ((Function) x).execute(session, values); + } + finally + { + session.put(DEFAULT_LOCK, null); + } + } + } + + throw new CommandNotFoundException(scmd); + } + } + return ((Function) x).execute(session, values); + } + + private Object executeMethod(Object cmd, List values) throws Exception + { + if (values.isEmpty()) + { + return cmd; + } + + boolean dot = values.size() > 1 && ".".equals(String.valueOf(values.get(0))); + + // FELIX-1473 - allow method chaining using dot pseudo-operator, e.g. + // (bundle 0) . loadClass java.net.InetAddress . localhost . hostname + // (((bundle 0) loadClass java.net.InetAddress ) localhost ) hostname + if (dot) + { + Object target = cmd; + ArrayList args = new ArrayList<>(); + values.remove(0); + + for (Object arg : values) + { + if (".".equals(arg)) + { + target = invoke(target, args.remove(0).toString(), args); + args.clear(); + } + else + { + args.add(arg); + } + } + + if (args.size() == 0) + { + return target; + } + + return invoke(target, args.remove(0).toString(), args); + } + else if (cmd.getClass().isArray() && values.size() == 1) + { + Object[] cmdv = (Object[]) cmd; + String index = values.get(0).toString(); + return "length".equals(index) ? cmdv.length : cmdv[Integer.parseInt(index)]; + } + else + { + return invoke(cmd, values.remove(0).toString(), values); + } + } + + private Object invoke(Object target, String name, List args) throws Exception + { + return session.invoke(target, name, args); + } + + private Object assignment(String name, Object value) + { + session.put(name, value); + return value; + } + + public Object expr(Token expr) + { + return session.expr(expr); + } + + private Object array(Array array) throws Exception + { + List list = array.list(); + Map map = array.map(); + + if (list != null) + { + List olist = new ArrayList<>(); + for (Token t : list) + { + Object oval = eval(t); + if (oval.getClass().isArray()) + { + Collections.addAll(olist, (Object[]) oval); + } + else if (oval instanceof ArgList) + { + olist.addAll((ArgList) oval); + } + else + { + olist.add(oval); + } + } + return olist; + } + else + { + Map omap = new LinkedHashMap<>(); + for (Entry e : map.entrySet()) + { + Token key = e.getKey(); + Object k = eval(key); + if (!(k instanceof String)) + { + throw new SyntaxError(key.line(), key.column(), + "map key null or not String: " + key); + } + omap.put(k, eval(e.getValue())); + } + return omap; + } + } + + public Object get(String name) + { + if (parms != null) + { + if ("args".equals(name)) + { + return parms; + } + + if ("argv".equals(name)) + { + return parmv; + } + + if ("it".equals(name)) + { + return parms.get(0); + } + + if (name.length() == 1 && Character.isDigit(name.charAt(0))) + { + int i = name.charAt(0) - '0'; + if (i > 0) + { + return parms.get(i - 1); + } + } + } + + return session.get(name); + } + + public Object put(String key, Object value) + { + return session.put(key, value); + } + + @Override + public Path currentDir() { + return isSet(CommandSession.OPTION_NO_GLOB, false) ? null : session().currentDir(); + } + + @Override + public ClassLoader classLoader() { + return session.classLoader(); + } + + protected boolean isSet(String name, boolean def) { + Object v = session.get(name); + if (v instanceof Boolean) { + return (Boolean) v; + } else if (v != null) { + String s = v.toString(); + return s.isEmpty() || s.equalsIgnoreCase("on") + || s.equalsIgnoreCase("1") || s.equalsIgnoreCase("true"); + } + return def; + } + + @Override + public String toString() + { + return source.toString().trim().replaceAll("\n+", "\n").replaceAll( + "([^\\\\{}(\\[])[\\s\n]*\n", "$1;").replaceAll("[ \\\\\t\n]+", " "); + } + +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandNotFoundException.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandNotFoundException.java new file mode 100644 index 00000000000..ce0bdd1a8aa --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandNotFoundException.java @@ -0,0 +1,38 @@ +/** + * + * 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.gogo.runtime; + +/** + * Thrown if an unknown command is entered into a shell or passed on the command line + */ +@SuppressWarnings("serial") +public class CommandNotFoundException extends IllegalArgumentException +{ + private final String command; + + public CommandNotFoundException(String command) + { + super("Command not found: " + command); + this.command = command; + } + + public String getCommand() + { + return command; + } +} \ No newline at end of file diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandProcessorImpl.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandProcessorImpl.java new file mode 100644 index 00000000000..e558e0af0bc --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandProcessorImpl.java @@ -0,0 +1,422 @@ +/* + * 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.gogo.runtime; + +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Method; +import java.nio.file.Path; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArraySet; + +import org.apache.felix.service.command.*; +import org.apache.felix.service.threadio.ThreadIO; +import org.osgi.annotation.bundle.Capability; +import org.osgi.namespace.service.ServiceNamespace; + +@Capability( + namespace = ServiceNamespace.SERVICE_NAMESPACE, + attribute = "objectClass='org.apache.felix.service.command.CommandProcessor'" +) +public class CommandProcessorImpl implements CommandProcessor +{ + protected final Set converters = new CopyOnWriteArraySet<>(); + protected final Set listeners = new CopyOnWriteArraySet<>(); + protected final ConcurrentMap> commands = new ConcurrentHashMap<>(); + protected final Map constants = new ConcurrentHashMap<>(); + protected final ThreadIO threadIO; + protected final WeakHashMap sessions = new WeakHashMap<>(); + protected boolean stopped; + + public CommandProcessorImpl() + { + this(null); + } + + public CommandProcessorImpl(ThreadIO tio) + { + threadIO = tio; + } + + @Override + public CommandSessionImpl createSession(CommandSession parent) { + synchronized (sessions) { + if (stopped) + { + throw new IllegalStateException("CommandProcessor has been stopped"); + } + if (!sessions.containsKey(parent) || !(parent instanceof CommandSessionImpl)) { + throw new IllegalArgumentException(); + } + CommandSessionImpl session = new CommandSessionImpl(this, (CommandSessionImpl) parent); + sessions.put(session, null); + return session; + } + } + + public CommandSessionImpl createSession(InputStream in, OutputStream out, OutputStream err) + { + synchronized (sessions) + { + if (stopped) + { + throw new IllegalStateException("CommandProcessor has been stopped"); + } + CommandSessionImpl session = new CommandSessionImpl(this, in, out, err); + sessions.put(session, null); + return session; + } + } + + void closeSession(CommandSessionImpl session) + { + synchronized (sessions) + { + sessions.remove(session); + } + } + + public void stop() + { + synchronized (sessions) + { + stopped = true; + // Create a copy, as calling session.close() will remove the session from the map + CommandSession[] toClose = this.sessions.keySet().toArray(new CommandSession[this.sessions.size()]); + for (CommandSession session : toClose) + { + session.close(); + } + // Just in case... + sessions.clear(); + } + } + + public void addConverter(Converter c) + { + converters.add(c); + } + + public void removeConverter(Converter c) + { + converters.remove(c); + } + + public void addListener(CommandSessionListener l) + { + listeners.add(l); + } + + public void removeListener(CommandSessionListener l) + { + listeners.remove(l); + } + + public Set getCommands() + { + return Collections.unmodifiableSet(commands.keySet()); + } + + protected Function getCommand(String name, final Object path) + { + int colon = name.indexOf(':'); + + if (colon < 0) + { + return null; + } + + name = name.toLowerCase(); + String cfunction = name.substring(colon); + boolean anyScope = (colon == 1 && name.charAt(0) == '*'); + + Map cmdMap = commands.get(name); + + if (null == cmdMap && anyScope) + { + String scopePath = (null == path ? "*" : path.toString()); + + for (String scope : scopePath.split(":")) + { + if (scope.equals("*")) + { + for (Entry> entry : commands.entrySet()) + { + if (entry.getKey().endsWith(cfunction)) + { + cmdMap = entry.getValue(); + break; + } + } + } + else + { + cmdMap = commands.get(scope + cfunction); + if (cmdMap != null) + { + break; + } + } + } + } + + Object cmd = null; + if (cmdMap != null && !cmdMap.isEmpty()) + { + for (Entry e : cmdMap.entrySet()) + { + if (cmd == null || e.getValue() > cmdMap.get(cmd)) + { + cmd = e.getKey(); + } + } + } + if ((null == cmd) || (cmd instanceof Function)) + { + return (Function) cmd; + } + + return new CommandProxy(cmd, cfunction.substring(1)); + } + + @Descriptor("add commands") + public void addCommand(@Descriptor("scope") String scope, @Descriptor("target") Object target) + { + Class tc = (target instanceof Class) ? (Class) target : target.getClass(); + addCommand(scope, target, tc); + } + + @Descriptor("add commands") + public void addCommand(@Descriptor("scope") String scope, @Descriptor("target") Object target, @Descriptor("functions") Class functions) + { + addCommand(scope, target, functions, 0); + } + + public void addCommand(String scope, Object target, Class functions, int ranking) + { + if (target == null) + { + return; + } + + String[] names = getFunctions(functions); + for (String function : names) + { + addCommand(scope, target, function, ranking); + } + } + + public Object addConstant(String name, Object target) + { + return constants.put(name, target); + } + + public Object removeConstant(String name) + { + return constants.remove(name); + } + + public void addCommand(String scope, Object target, String function) + { + addCommand(scope, target, function, 0); + } + + public void addCommand(String scope, Object target, String function, int ranking) + { + String key = (scope + ":" + function).toLowerCase(); + Map cmdMap = commands.get(key); + if (cmdMap == null) + { + commands.putIfAbsent(key, new LinkedHashMap()); + cmdMap = commands.get(key); + } + cmdMap.put(target, ranking); + } + + public void removeCommand(String scope, String function) + { + // TODO: WARNING: this method does remove all mapping for scope:function + String key = (scope + ":" + function).toLowerCase(); + commands.remove(key); + } + + public void removeCommand(String scope, String function, Object target) + { + // TODO: WARNING: this method does remove all mapping for scope:function + String key = (scope + ":" + function).toLowerCase(); + Map cmdMap = commands.get(key); + if (cmdMap != null) + { + cmdMap.remove(target); + } + } + + public void removeCommand(Object target) + { + for (Map cmdMap : commands.values()) + { + cmdMap.remove(target); + } + } + + private String[] getFunctions(Class target) + { + String[] functions; + Set list = new TreeSet<>(); + Method methods[] = target.getMethods(); + for (Method m : methods) + { + if (m.getDeclaringClass().equals(Object.class)) + { + continue; + } + list.add(m.getName()); + if (m.getName().startsWith("get")) + { + String s = m.getName().substring(3); + if (s.length() > 0) + { + list.add(s.substring(0, 1).toLowerCase() + s.substring(1)); + } + } + } + + functions = list.toArray(new String[list.size()]); + return functions; + } + + public Object convert(CommandSession session, Class desiredType, Object in) + { + int[] cost = new int[1]; + Object ret = Reflective.coerce(session, desiredType, in, cost); + if (ret == Reflective.NO_MATCH) + { + throw new IllegalArgumentException(String.format( + "Cannot convert %s(%s) to %s", in, in != null ? in.getClass() : "null", desiredType)); + } + return ret; + } + + Object doConvert(Class desiredType, Object in) + { + for (Converter c : converters) + { + try + { + Object converted = c.convert(desiredType, in); + if (converted != null) + { + return converted; + } + } + catch (Exception e) + { + // Ignore + e.getCause(); + } + } + + return null; + } + + // eval is needed to force expansions to be treated as commands (FELIX-1473) + public Object eval(CommandSession session, Object[] argv) throws Exception + { + StringBuilder buf = new StringBuilder(); + + for (Object arg : argv) + { + if (buf.length() > 0) + buf.append(' '); + buf.append(arg); + } + + return session.execute(buf); + } + + void beforeExecute(CommandSession session, CharSequence commandline) + { + for (CommandSessionListener l : listeners) + { + try + { + l.beforeExecute(session, commandline); + } + catch (Throwable t) + { + // Ignore + } + } + } + + void afterExecute(CommandSession session, CharSequence commandline, Exception exception) + { + for (CommandSessionListener l : listeners) + { + try + { + l.afterExecute(session, commandline, exception); + } + catch (Throwable t) + { + // Ignore + } + } + } + + void afterExecute(CommandSession session, CharSequence commandline, Object result) + { + for (CommandSessionListener l : listeners) + { + try + { + l.afterExecute(session, commandline, result); + } + catch (Throwable t) + { + // Ignore + } + } + } + + public Object expr(CommandSessionImpl session, CharSequence expr) + { + return new Expression(expr.toString()).eval(session.variables); + } + + public Object invoke(CommandSessionImpl session, Object target, String name, List args) throws Exception + { + return Reflective.invoke(session, target, name, args); + } + + public Path redirect(CommandSessionImpl session, Path path, int mode) + { + return session.currentDir().resolve(path); + } +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandProxy.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandProxy.java new file mode 100644 index 00000000000..71df32fe2ad --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandProxy.java @@ -0,0 +1,99 @@ +/* + * 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.gogo.runtime; + +import java.util.List; + +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Function; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; + +public class CommandProxy implements Function +{ + private BundleContext context; + private ServiceReference reference; + private String function; + private Object target; + + public CommandProxy(BundleContext context, ServiceReference reference, String function) + { + this.context = context; + this.reference = reference; + this.function = function; + } + + public CommandProxy(Object target, String function) + { + this.function = function; + this.target = target; + } + + public Object getTarget() + { + return (context != null ? context.getService(reference) : target); + } + + public void ungetTarget() + { + if (context != null) + { + try + { + context.ungetService(reference); + } + catch (IllegalStateException e) + { + // ignore - probably due to shutdown + // java.lang.IllegalStateException: BundleContext is no longer valid + } + } + } + + public Object execute(CommandSession session, List arguments) + throws Exception + { + Object tgt = getTarget(); + + try + { + if (tgt instanceof Function) + { + for (int i = 0; i < arguments.size(); i++) + { + Object obj = arguments.get(i); + if (obj instanceof Token) + { + arguments.set(i, obj.toString()); + } + } + + return ((Function) tgt).execute(session, arguments); + } + else + { + return Reflective.invoke(session, tgt, function, arguments); + } + } + finally + { + ungetTarget(); + } + } +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandSessionImpl.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandSessionImpl.java new file mode 100644 index 00000000000..a2271b6605b --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandSessionImpl.java @@ -0,0 +1,881 @@ +/* + * 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. + */ +// DWB8: throw IllegatlStateException if session used after closed (as per rfc132) +// DWB9: there is no API to list all variables: https://www.osgi.org/bugzilla/show_bug.cgi?id=49 +// DWB10: add SCOPE support: https://www.osgi.org/bugzilla/show_bug.cgi?id=51 +package org.apache.felix.gogo.runtime; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.channels.Channel; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Formatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.apache.felix.service.command.Job; +import org.apache.felix.service.command.Job.Status; +import org.apache.felix.service.command.JobListener; +import org.apache.felix.service.command.Process; +import org.apache.felix.gogo.runtime.Pipe.Result; +import org.apache.felix.service.command.CommandProcessor; +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Converter; +import org.apache.felix.service.command.Function; +import org.apache.felix.service.threadio.ThreadIO; + +public class CommandSessionImpl implements CommandSession, Converter +{ + public static final String SESSION_CLOSED = "session is closed"; + public static final String VARIABLES = ".variables"; + public static final String COMMANDS = ".commands"; + public static final String CONSTANTS = ".constants"; + private static final String COLUMN = "%-20s %s\n"; + + // Streams and channels + protected InputStream in; + protected OutputStream out; + protected PrintStream pout; + protected OutputStream err; + protected PrintStream perr; + protected Channel[] channels; + + private final CommandProcessorImpl processor; + protected final ConcurrentMap variables = new ConcurrentHashMap<>(); + private volatile boolean closed; + private final List jobs = new ArrayList<>(); + private JobListener jobListener; + + private final ExecutorService executor; + + private Path currentDir; + private ClassLoader classLoader; + + protected CommandSessionImpl(CommandProcessorImpl shell, CommandSessionImpl parent) + { + this.currentDir = parent.currentDir; + this.executor = Executors.newCachedThreadPool(ThreadUtils.namedThreadFactory("session")); + this.processor = shell; + this.channels = parent.channels; + this.in = parent.in; + this.out = parent.out; + this.err = parent.err; + this.pout = parent.pout; + this.perr = parent.perr; + } + + protected CommandSessionImpl(CommandProcessorImpl shell, InputStream in, OutputStream out, OutputStream err) + { + this.currentDir = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize(); + this.executor = Executors.newCachedThreadPool(ThreadUtils.namedThreadFactory("session")); + this.processor = shell; + ReadableByteChannel inCh = Channels.newChannel(in); + WritableByteChannel outCh = Channels.newChannel(out); + WritableByteChannel errCh = out == err ? outCh : Channels.newChannel(err); + this.channels = new Channel[] { inCh, outCh, errCh }; + this.in = in; + this.out = out; + this.err = err; + this.pout = out instanceof PrintStream ? (PrintStream) out : new PrintStream(out, true); + this.perr = out == err ? pout : err instanceof PrintStream ? (PrintStream) err : new PrintStream(err, true); + } + + ThreadIO threadIO() + { + return processor.threadIO; + } + + public CommandProcessor processor() + { + return processor; + } + + public ConcurrentMap getVariables() + { + return variables; + } + + public Path currentDir() + { + return currentDir; + } + + public void currentDir(Path path) + { + currentDir = path; + } + + public ClassLoader classLoader() + { + return classLoader != null ? classLoader : getClass().getClassLoader(); + } + + public void classLoader(ClassLoader classLoader) + { + this.classLoader = classLoader; + } + + public void close() + { + if (!this.closed) + { + this.closed = true; + this.processor.closeSession(this); + executor.shutdownNow(); + } + } + + public Object execute(CharSequence commandline) throws Exception + { + assert processor != null; + + if (closed) + { + throw new IllegalStateException(SESSION_CLOSED); + } + + processor.beforeExecute(this, commandline); + + try + { + Closure impl = new Closure(this, null, commandline); + Object result = impl.execute(this, null); + processor.afterExecute(this, commandline, result); + return result; + } + catch (Exception e) + { + processor.afterExecute(this, commandline, e); + throw e; + } + } + + public InputStream getKeyboard() + { + return in; + } + + public Object get(String name) + { + // there is no API to list all variables, so overload name == null + if (name == null || VARIABLES.equals(name)) + { + return Collections.unmodifiableSet(variables.keySet()); + } + + if (COMMANDS.equals(name)) + { + return processor.getCommands(); + } + + if (CONSTANTS.equals(name)) + { + return Collections.unmodifiableSet(processor.constants.keySet()); + } + + Object val = processor.constants.get(name); + if (val != null) + { + return val; + } + + val = variables.get("#" + name); + if (val instanceof Function) + { + try + { + val = ((Function) val).execute(this, null); + } + catch (Exception e) + { + // Ignore + } + return val; + } + else if (val != null) + { + return val; + } + + val = variables.get(name); + if (val != null) + { + return val; + } + + return processor.getCommand(name, variables.get("SCOPE")); + } + + public Object put(String name, Object value) + { + if (value != null) + { + return variables.put(name, value); + } + else + { + return variables.remove(name); + } + } + + public PrintStream getConsole() + { + return pout; + } + + @SuppressWarnings("unchecked") + public CharSequence format(Object target, int level, Converter escape) throws Exception + { + if (target == null) + { + return "null"; + } + + if (target instanceof CharSequence) + { + return (CharSequence) target; + } + + for (Converter c : processor.converters) + { + CharSequence s = c.format(target, level, this); + if (s != null) + { + return s; + } + } + + if (target.getClass().isArray()) + { + if (target.getClass().getComponentType().isPrimitive()) + { + if (target.getClass().getComponentType() == boolean.class) + { + return Arrays.toString((boolean[]) target); + } + else + { + if (target.getClass().getComponentType() == byte.class) + { + return Arrays.toString((byte[]) target); + } + else + { + if (target.getClass().getComponentType() == short.class) + { + return Arrays.toString((short[]) target); + } + else + { + if (target.getClass().getComponentType() == int.class) + { + return Arrays.toString((int[]) target); + } + else + { + if (target.getClass().getComponentType() == long.class) + { + return Arrays.toString((long[]) target); + } + else + { + if (target.getClass().getComponentType() == float.class) + { + return Arrays.toString((float[]) target); + } + else + { + if (target.getClass().getComponentType() == double.class) + { + return Arrays.toString((double[]) target); + } + else + { + if (target.getClass().getComponentType() == char.class) + { + return Arrays.toString((char[]) target); + } + } + } + } + } + } + } + } + } + target = Arrays.asList((Object[]) target); + } + if (target instanceof Collection) + { + if (level == Converter.INSPECT) + { + StringBuilder sb = new StringBuilder(); + Collection c = (Collection) target; + for (Object o : c) + { + sb.append(format(o, level + 1, this)); + sb.append("\n"); + } + return sb; + } + else + { + if (level == Converter.LINE) + { + StringBuilder sb = new StringBuilder(); + Collection c = (Collection) target; + sb.append("["); + for (Object o : c) + { + if (sb.length() > 1) + { + sb.append(", "); + } + sb.append(format(o, level + 1, this)); + } + sb.append("]"); + return sb; + } + } + } + if (target instanceof Dictionary) + { + Map result = new HashMap<>(); + for (Enumeration e = ((Dictionary) target).keys(); e.hasMoreElements();) + { + Object key = e.nextElement(); + result.put(key, ((Dictionary) target).get(key)); + } + target = result; + } + if (target instanceof Map) + { + if (level == Converter.INSPECT) + { + StringBuilder sb = new StringBuilder(); + Map c = (Map) target; + for (Map.Entry entry : c.entrySet()) + { + CharSequence key = format(entry.getKey(), level + 1, this); + sb.append(key); + for (int i = key.length(); i < 20; i++) + { + sb.append(' '); + } + sb.append(format(entry.getValue(), level + 1, this)); + sb.append("\n"); + } + return sb; + } + else + { + if (level == Converter.LINE) + { + StringBuilder sb = new StringBuilder(); + Map c = (Map) target; + sb.append("["); + for (Map.Entry entry : c.entrySet()) + { + if (sb.length() > 1) + { + sb.append(", "); + } + sb.append(format(entry, level + 1, this)); + } + sb.append("]"); + return sb; + } + } + } + if (target instanceof Path) + { + return target.toString(); + } + if (level == Converter.INSPECT) + { + return inspect(target); + } + else + { + return target.toString(); + } + } + + CharSequence inspect(Object b) + { + boolean found = false; + try (Formatter f = new Formatter();) + { + Method methods[] = b.getClass().getMethods(); + for (Method m : methods) + { + try + { + String name = m.getName(); + if (m.getName().startsWith("get") && !m.getName().equals("getClass") && m.getParameterTypes().length == 0 && Modifier.isPublic(m.getModifiers())) + { + found = true; + name = name.substring(3); + m.setAccessible(true); + Object value = m.invoke(b, (Object[]) null); + f.format(COLUMN, name, format(value, Converter.LINE, this)); + } + } + catch (Exception e) + { + // Ignore + } + } + if (found) + { + return (StringBuilder) f.out(); + } + else + { + return b.toString(); + } + } + } + + public Object convert(Class desiredType, Object in) + { + return processor.convert(this, desiredType, in); + } + + public Object doConvert(Class desiredType, Object in) + { + if (desiredType == Class.class) + { + try + { + return Class.forName(in.toString(), true, classLoader()); + } + catch (ClassNotFoundException e) + { + return null; + } + } + return processor.doConvert(desiredType, in); + } + + public CharSequence format(Object result, int inspect) + { + try + { + return format(result, inspect, this); + } + catch (Exception e) + { + return " args) throws Exception + { + return processor.invoke(this, target, name, args); + } + + public Path redirect(Path path, int mode) + { + return processor.redirect(this, path, mode); + } + + @Override + public List jobs() + { + synchronized (jobs) + { + return Collections.unmodifiableList(jobs); + } + } + + public static JobImpl currentJob() + { + return (JobImpl) Job.Utils.current(); + } + + @Override + public JobImpl foregroundJob() + { + List jobs; + synchronized (this.jobs) + { + jobs = new ArrayList<>(this.jobs); + } + for (JobImpl j : jobs) { + if (j.parent == null && j.status() == Status.Foreground) { + return j; + } + } + return null; + } + + @Override + public void setJobListener(JobListener listener) + { + synchronized (jobs) + { + jobListener = listener; + } + } + + public JobImpl createJob(CharSequence command) + { + synchronized (jobs) + { + int id = 1; + + boolean found; + do + { + found = false; + for (Job job : jobs) + { + if (job.id() == id) + { + found = true; + id++; + break; + } + } + } + while (found); + + JobImpl cur = currentJob(); + JobImpl job = new JobImpl(id, cur, command); + if (cur == null) + { + jobs.add(job); + } + else + { + cur.add(job); + } + return job; + } + } + + class JobImpl implements Job, Runnable + { + private final int id; + private final JobImpl parent; + private final CharSequence command; + private final List pipes = new ArrayList<>(); + private final List children = new ArrayList<>(); + private Status status = Status.Created; + private Future future; + private Result result; + + public JobImpl(int id, JobImpl parent, CharSequence command) + { + this.id = id; + this.parent = parent; + this.command = command; + } + + void addPipe(Pipe pipe) + { + pipes.add(pipe); + } + + @Override + public int id() + { + return id; + } + + public CharSequence command() + { + return command; + } + + @Override + public synchronized Status status() + { + return status; + } + + @Override + public synchronized void suspend() + { + if (status == Status.Done) + { + throw new IllegalStateException("Job is finished"); + } + if (status != Status.Suspended) + { + setStatus(Status.Suspended); + } + } + + @Override + public synchronized void background() + { + if (status == Status.Done) + { + throw new IllegalStateException("Job is finished"); + } + if (status != Status.Background) + { + setStatus(Status.Background); + } + } + + @Override + public synchronized void foreground() + { + if (status == Status.Done) + { + throw new IllegalStateException("Job is finished"); + } + JobImpl cr = CommandSessionImpl.currentJob(); + JobImpl fg = foregroundJob(); + if (parent == null && fg != null && fg != this && fg != cr) + { + throw new IllegalStateException("A job is already in foreground"); + } + if (status != Status.Foreground) + { + setStatus(Status.Foreground); + } + } + + @Override + public void interrupt() + { + Future future; + List children; + synchronized (this) + { + future = this.future; + } + synchronized (this.children) + { + children = new ArrayList<>(this.children); + } + for (Job job : children) + { + job.interrupt(); + } + if (future != null) + { + future.cancel(true); + } + } + + protected synchronized void done() + { + if (status == Status.Done) + { + throw new IllegalStateException("Job is finished"); + } + setStatus(Status.Done); + } + + private void setStatus(Status newStatus) + { + setStatus(newStatus, true); + } + + private void setStatus(Status newStatus, boolean callListeners) + { + Status previous; + synchronized (this) + { + previous = this.status; + status = newStatus; + } + if (callListeners) + { + JobListener listener; + synchronized (jobs) + { + listener = jobListener; + if (newStatus == Status.Done) + { + jobs.remove(this); + } + } + if (listener != null) + { + listener.jobChanged(this, previous, newStatus); + } + } + synchronized (this) + { + JobImpl.this.notifyAll(); + } + } + + @Override + public synchronized Result result() + { + return result; + } + + @Override + public Job parent() + { + return parent; + } + + /** + * Start the job. + * If the job is started in foreground, + * waits for the job to finish or to be + * suspended or moved to background. + * + * @param status the desired job status + * @return null if the job + * has been suspended or moved to background, + * + */ + public synchronized Result start(Status status) throws InterruptedException + { + if (status == Status.Created || status == Status.Done) + { + throw new IllegalArgumentException("Illegal start status"); + } + if (this.status != Status.Created) + { + throw new IllegalStateException("Job already started"); + } + switch (status) + { + case Suspended: + suspend(); + break; + case Background: + background(); + break; + case Foreground: + foreground(); + break; + case Created: + case Done: + } + future = executor.submit(this); + while (this.status == Status.Foreground) + { + JobImpl.this.wait(); + } + return result; + } + + public List processes() + { + return Collections.unmodifiableList((List)pipes); + } + + @Override + public CommandSession session() + { + return CommandSessionImpl.this; + } + + public void run() + { + Thread thread = Thread.currentThread(); + String name = thread.getName(); + try + { + thread.setName("job controller " + id); + + List> wrapped = new ArrayList>(pipes); + List> results = executor.invokeAll(wrapped); + + // Get pipe exceptions + Exception pipeException = null; + for (int i = 0; i < results.size() - 1; i++) + { + Future future = results.get(i); + Throwable e; + try + { + Result r = future.get(); + e = r.exception; + } + catch (ExecutionException ee) + { + e = ee.getCause(); + } + if (e != null) + { + if (pipeException == null) + { + pipeException = new Exception("Exception caught during pipe execution"); + } + pipeException.addSuppressed(e); + } + } + put(Closure.PIPE_EXCEPTION, pipeException); + + result = results.get(results.size() - 1).get(); + } + catch (Exception e) + { + result = new Result(e); + } + catch (Throwable t) + { + result = new Result(new ExecutionException(t)); + } + finally + { + done(); + thread.setName(name); + } + } + + public void add(Job child) + { + synchronized (children) + { + children.add(child); + } + } + } + +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/EOFError.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/EOFError.java new file mode 100644 index 00000000000..5b0872af0ac --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/EOFError.java @@ -0,0 +1,45 @@ +/* + * 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.gogo.runtime; + +public class EOFError extends SyntaxError +{ + private static final long serialVersionUID = 1L; + + private final String missing; + private final String repair; + + public EOFError(int line, int column, String message, String missing, String repair) + { + super(line, column, message); + this.missing = missing; + this.repair = repair; + } + + public String repair() + { + return repair; + } + + public String missing() + { + return missing; + } + +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Evaluate.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Evaluate.java new file mode 100644 index 00000000000..058ed335dbb --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Evaluate.java @@ -0,0 +1,36 @@ +/* + * 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.gogo.runtime; + +import java.nio.file.Path; + +public interface Evaluate +{ + Object eval(Token t) throws Exception; + + Object get(String key); + + Object put(String key, Object value); + + Object expr(Token t); + + Path currentDir(); + + ClassLoader classLoader(); +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Expander.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Expander.java new file mode 100644 index 00000000000..7792fa417b9 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Expander.java @@ -0,0 +1,2628 @@ +/* + * 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.gogo.runtime; + +import java.io.IOException; +import java.lang.reflect.Array; +import java.nio.file.FileVisitOption; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.apache.felix.gogo.runtime.util.function.BiFunction; +import org.apache.felix.gogo.runtime.util.function.Function; + +public class Expander extends BaseTokenizer +{ + + /** + * expand variables, quotes and escapes in word. + * @param word the word + * @param eval the eval + * @return Object + * @throws Exception on exception + */ + public static Object expand(CharSequence word, Evaluate eval) throws Exception + { + return expand(word, eval, false, true, false, true, false); + } + + private static Object expand(CharSequence word, Evaluate eval, boolean inQuote) throws Exception + { + return new Expander(word, eval, inQuote, true, false, false, false).expand(); + } + + private static Object expand(CharSequence word, Evaluate eval, + boolean inQuote, + boolean generateFileNames, + boolean semanticJoin, + boolean unquote, + boolean asPattern) throws Exception + { + return new Expander(word, eval, inQuote, generateFileNames, semanticJoin, unquote, asPattern).expand(); + } + + private final Evaluate evaluate; + private boolean inQuote; + private boolean generateFileNames; + private boolean semanticJoin; + private boolean unquote; + private boolean asPattern; + private boolean rawVariable; + + public Expander(CharSequence text, Evaluate evaluate, + boolean inQuote, + boolean generateFileNames, + boolean semanticJoin, + boolean unquote, + boolean asPattern) + { + super(text); + this.evaluate = evaluate; + this.inQuote = inQuote; + this.generateFileNames = generateFileNames; + this.semanticJoin = semanticJoin; + this.unquote = unquote; + this.asPattern = asPattern; + } + + public Object expand(CharSequence word) throws Exception + { + return expand(word, evaluate, inQuote, true, false, false, false); + } + + public Object expand(CharSequence word, + boolean generateFileNames, + boolean semanticJoin, + boolean unquote) throws Exception + { + return expand(word, evaluate, inQuote, generateFileNames, semanticJoin, unquote, false); + } + + public Object expandPattern(CharSequence word) throws Exception + { + return expand(word, evaluate, inQuote, false, false, false, true); + } + + private Object expand() throws Exception + { + Object expanded = doExpand(); + if (rawVariable) + { + return expanded; + } + List args = new ArrayList<>(); + for (Object o1 : toCollection(expanded)) { + for (Object o2 : o1 instanceof CharSequence + ? expandBraces((CharSequence) o1) + : Collections.singleton(o1)) { + for (Object o3 : generateFileNames && o2 instanceof CharSequence + ? generateFileNames(((CharSequence) o2)) + : Collections.singleton(o2)) { + if (unquote && o3 instanceof CharSequence) { + o3 = unquote((CharSequence) o3); + } + args.add(o3); + } + } + } + if (args.size() == 1) + { + return args.get(0); + } + if (expanded instanceof ArgList) + { + return new ArgList(args); + } + return args; + } + + private CharSequence unquote(CharSequence arg) + { + if (inQuote) + { + return arg; + } + boolean hasEscape = false; + for (int i = 0; i < arg.length(); i++) + { + int c = arg.charAt(i); + if (c == '\\' || c == '"' || c == '\'') + { + hasEscape = true; + break; + } + } + if (!hasEscape) + { + return arg; + } + boolean singleQuoted = false; + boolean doubleQuoted = false; + boolean escaped = false; + StringBuilder buf = new StringBuilder(arg.length()); + for (int i = 0; i < arg.length(); i++) + { + char c = arg.charAt(i); + if (doubleQuoted && escaped) + { + if (c != '"' && c != '\\' && c != '$' && c != '%') + { + buf.append('\\'); + } + buf.append(c); + escaped = false; + } + else if (escaped) + { + buf.append(c); + escaped = false; + } + else if (singleQuoted) + { + if (c == '\'') + { + singleQuoted = false; + } + else + { + buf.append(c); + } + } + else if (doubleQuoted) + { + if (c == '\\') + { + escaped = true; + } + else if (c == '\"') + { + doubleQuoted = false; + } + else + { + buf.append(c); + } + } + else if (c == '\\') + { + escaped = true; + } + else if (c == '\'') + { + singleQuoted = true; + } + else if (c == '"') + { + doubleQuoted = true; + } + else + { + buf.append(c); + } + } + return buf.toString(); + } + + protected List expandBraces(CharSequence arg) throws Exception + { + int braces = 0; + boolean escaped = false; + boolean doubleQuoted = false; + boolean singleQuoted = false; + List parts = new ArrayList<>(); + int start = 0; + for (int i = 0; i < arg.length(); i++) + { + char c = arg.charAt(i); + if (doubleQuoted && escaped) + { + escaped = false; + } + else if (escaped) + { + escaped = false; + } + else if (singleQuoted) + { + if (c == '\'') + { + singleQuoted = false; + } + } + else if (doubleQuoted) + { + if (c == '\\') + { + escaped = true; + } + else if (c == '\"') + { + doubleQuoted = false; + } + } + else if (c == '\\') + { + escaped = true; + } + else if (c == '\'') + { + singleQuoted = true; + } + else if (c == '"') + { + doubleQuoted = true; + } + else + { + if (c == '{') + { + if (braces++ == 0) + { + if (i > start) + { + parts.add(arg.subSequence(start, i)); + } + start = i; + } + } + else if (c == '}') + { + if (--braces == 0) + { + parts.add(arg.subSequence(start, i + 1)); + start = i + 1; + } + } + } + } + if (start < arg.length()) + { + parts.add(arg.subSequence(start, arg.length())); + } + if (start == 0) + { + return Collections.singletonList(arg); + } + List generated = new ArrayList<>(); + Pattern pattern = Pattern.compile( + "\\{(((?\\-?[0-9]+)\\.\\.(?\\-?[0-9]+)(\\.\\.(?\\-?0*[1-9][0-9]*))?)" + + "|((?\\S)\\.\\.(?\\S)))\\}"); + for (CharSequence part : parts) + { + List generators = new ArrayList<>(); + Matcher matcher = pattern.matcher(part); + if (matcher.matches()) + { + if (matcher.group("intstart") != null) + { + int intstart = Integer.parseInt(matcher.group("intstart")); + int intend = Integer.parseInt(matcher.group("intend")); + int intinc = matcher.group("intinc") != null ? Integer.parseInt(matcher.group("intinc")) : 1; + if (intstart > intend) + { + if (intinc < 0) + { + int k = intstart; + intstart = intend; + intend = k; + } + intinc = -intinc; + } + else + { + if (intinc < 0) + { + int k = intstart; + intstart = intend; + intend = k; + } + } + if (intinc > 0) + { + for (int k = intstart; k <= intend; k += intinc) + { + generators.add(Integer.toString(k)); + } + } + else + { + for (int k = intstart; k >= intend; k += intinc) + { + generators.add(Integer.toString(k)); + } + } + } + else + { + char charstart = matcher.group("charstart").charAt(0); + char charend = matcher.group("charend").charAt(0); + if (charstart < charend) + { + for (char c = charstart; c <= charend; c++) + { + generators.add(Character.toString(c)); + } + } + else + { + for (char c = charstart; c >= charend; c--) + { + generators.add(Character.toString(c)); + } + } + } + } + else if (part.charAt(0) == '{' && part.charAt(part.length() - 1) == '}') + { + // Split on commas + braces = 0; + escaped = false; + doubleQuoted = false; + singleQuoted = false; + start = 1; + for (int i = 1; i < part.length() - 1; i++) + { + char c = part.charAt(i); + if (doubleQuoted && escaped) + { + escaped = false; + } + else if (escaped) + { + escaped = false; + } + else if (singleQuoted) + { + if (c == '\'') + { + singleQuoted = false; + } + } + else if (doubleQuoted) + { + if (c == '\\') + { + escaped = true; + } + else if (c == '\"') + { + doubleQuoted = false; + } + } + else if (c == '\\') + { + escaped = true; + } + else if (c == '\'') + { + singleQuoted = true; + } + else if (c == '"') + { + doubleQuoted = true; + } + else + { + if (c == '}') + { + braces--; + } + else if (c == '{') + { + braces++; + } + else if (c == ',' && braces == 0) + { + generators.add(part.subSequence(start, i)); + start = i + 1; + } + } + } + if (start < part.length() - 1) + { + generators.add(part.subSequence(start, part.length() - 1)); + } + List l = new ArrayList<>(); + for (CharSequence cs : generators) { + Object o1 = expand(cs, false, false, false); + for (Object o2 : toCollection(o1)) { + l.add(String.valueOf(o2)); + } + } + generators = l; + + // If there's no splitting comma, expand with the braces + if (generators.size() < 2) + { + generators = Collections.singletonList(part.toString()); + } + } + else + { + generators.add(part.toString()); + } + if (generated.isEmpty()) + { + generated.addAll(generators); + } + else + { + List prevGenerated = generated; + generated = new ArrayList<>(); + for (CharSequence s : generators) { + for (Object cs : prevGenerated) { + generated.add(String.valueOf(cs) + s); + } + } + } + } + return generated; + } + + protected List generateFileNames(CharSequence arg) throws IOException + { + // Disable if currentDir is not set + Path currentDir = evaluate.currentDir(); + if (currentDir == null || inQuote) + { + return Collections.singletonList(arg); + } + // Search for unquoted escapes + boolean hasUnescapedReserved = false; + boolean escaped = false; + boolean doubleQuoted = false; + boolean singleQuoted = false; + StringBuilder buf = new StringBuilder(arg.length()); + String pfx = ""; + for (int i = 0; i < arg.length(); i++) + { + char c = arg.charAt(i); + if (doubleQuoted && escaped) + { + if (c != '"' && c != '\\' && c != '$' && c != '%') + { + buf.append('\\'); + } + buf.append(c); + escaped = false; + } + else if (escaped) + { + buf.append(c); + escaped = false; + } + else if (singleQuoted) + { + if (c == '\'') + { + singleQuoted = false; + } + else + { + buf.append(c); + } + } + else if (doubleQuoted) + { + if (c == '\\') + { + escaped = true; + } + else if (c == '\"') + { + doubleQuoted = false; + } + else + { + buf.append(c); + } + } + else if (c == '\\') + { + escaped = true; + } + else if (c == '\'') + { + singleQuoted = true; + } + else if (c == '"') + { + doubleQuoted = true; + } + else if (c == '~') + { + Object home = evaluate.get("HOME"); + if (home != null) + { + buf.append(home.toString()); + } + } + else + { + if ("*(|<[?".indexOf(c) >= 0 && !hasUnescapedReserved) + { + hasUnescapedReserved = true; + pfx = buf.toString(); + } + buf.append(c); + } + } + if (!hasUnescapedReserved) + { + return Collections.singletonList(arg); + } + + String org = buf.toString(); + final List expanded = new ArrayList<>(); + final Path dir; + final String prefix; + if (pfx.indexOf('/') >= 0) + { + pfx = pfx.substring(0, pfx.lastIndexOf('/')); + arg = org.substring(pfx.length() + 1); + dir = currentDir.resolve(pfx).normalize(); + prefix = pfx + "/"; + } + else + { + dir = currentDir; + prefix = ""; + } + final GlobPathMatcher matcher = new GlobPathMatcher(arg.toString()); + Files.walkFileTree(dir, + EnumSet.of(FileVisitOption.FOLLOW_LINKS), + Integer.MAX_VALUE, + new FileVisitor() + { + @Override + public FileVisitResult preVisitDirectory(Path file, BasicFileAttributes attrs) throws IOException + { + if (file.equals(dir)) + { + return FileVisitResult.CONTINUE; + } + if (Files.isHidden(file)) + { + return FileVisitResult.SKIP_SUBTREE; + } + Path r = dir.relativize(file); + if (matcher.matches(r.toString(), true)) + { + expanded.add(prefix + r.toString()); + } + if (matcher.matches(r.toString(), false)) + { + return FileVisitResult.CONTINUE; + } + else + { + return FileVisitResult.SKIP_SUBTREE; + } + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException + { + if (!Files.isHidden(file)) + { + Path r = dir.relativize(file); + if (matcher.matches(r.toString(), true)) + { + expanded.add(prefix + r.toString()); + } + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) { + return FileVisitResult.CONTINUE; + } + }); + Collections.sort(expanded); + if (expanded.isEmpty()) + { + throw new IOException("no matches found: " + org); + } + return expanded; + } + + + private Object doExpand() throws Exception + { + final String special = "%$\\\"'"; + int i = text.length(); + while ((--i >= 0) && (special.indexOf(text.charAt(i)) == -1)); + // shortcut if word doesn't contain any special characters + if (i < 0) + return text; + + StringBuilder buf = new StringBuilder(); + Token value; + + while (ch != EOT) + { + int start = index; + + switch (ch) + { + case '%': + Object exp = expandExp(); + if (EOT == ch && buf.length() == 0) + { + return exp; + } + if (null != exp) + { + buf.append(exp); + } + break; + + case '$': + // Posix quote + if (peek() == '\'') + { + getch(); + skipQuote(); + value = text.subSequence(start + 1, index - 1); + getch(); + buf.append("\'"); + buf.append(ansiEscape(value)); + buf.append("\'"); + } + // Parameter expansion + else + { + Object val = expandVar(true); + if (EOT == ch && buf.length() == 0) + { + return val; + } + rawVariable = false; + if (null != val) + { + buf.append(val); + } + } + break; + + case '\\': + buf.append(ch); + if (peek() != EOT) + { + getch(); + buf.append(ch); + } + getch(); + break; + + case '"': + skipQuote(); + value = text.subSequence(start, index - 1); + getch(); + Object expand = expand(value, evaluate, true); + if (eot() && buf.length() == 0) + { + if (expand instanceof ArgList) + { + List l = new ArrayList<>(); + for (Object o : (ArgList) expand) { + l.add("\"" + String.valueOf(o) + "\""); + } + return l; + } + else if (expand instanceof Collection) + { + StringBuilder sb = new StringBuilder(); + sb.append("\""); + boolean first = true; + for (Object o : asCollection(expand)) { + if (first) { + first = false; + } else { + sb.append(" "); + } + sb.append(String.valueOf(o)); + } + return sb.append("\"").toString(); + } + else if (expand != null) + { + return "\"" + expand.toString() + "\""; + } + else + { + return ""; + } + } + if (expand instanceof Collection) + { + boolean first = true; + buf.append("\""); + for (Object o : ((Collection) expand)) + { + if (!first) + { + buf.append(" "); + } + first = false; + buf.append(o); + } + buf.append("\""); + } + else if (expand != null) + { + buf.append("\""); + buf.append(expand.toString()); + buf.append("\""); + } + break; + + case '\'': + skipQuote(); + value = text.subSequence(start - 1, index); + getch(); + if (eot() && buf.length() == 0) + { + return value; + } + buf.append(value); + break; + + default: + buf.append(ch); + getch(); + break; + } + + } + + return buf.toString(); + } + + private CharSequence ansiEscape(CharSequence arg) + { + StringBuilder buf = new StringBuilder(arg.length()); + for (int i = 0; i < arg.length(); i++) + { + int c = arg.charAt(i); + int ch; + if (c == '\\') + { + c = i < arg.length() - 1 ? arg.charAt(++i) : '\\'; + switch (c) + { + case 'a': + buf.append('\u0007'); + break; + case 'n': + buf.append('\n'); + break; + case 't': + buf.append('\t'); + break; + case 'r': + buf.append('\r'); + break; + case '\\': + buf.append('\\'); + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + ch = 0; + i--; + for (int j = 0; j < 3; j++) + { + c = i < arg.length() - 1 ? arg.charAt(++i) : -1; + if (c >= 0) + { + ch = ch * 8 + (c - '0'); + } + } + buf.append((char) ch); + break; + case 'u': + ch = 0; + for (int j = 0; j < 4; j++) + { + c = i < arg.length() - 1 ? arg.charAt(++i) : -1; + if (c >= 0) + { + if (c >= 'A' && c <= 'F') + { + ch = ch * 16 + (c - 'A' + 10); + } + else if (c >= 'a' && c <= 'f') + { + ch = ch * 16 + (c - 'a' + 10); + } + else if (c >= '0' && c <= '9') + { + ch = ch * 16 + (c - '0'); + } + else + { + i--; + break; + } + } + } + buf.append((char) ch); + break; + default: + buf.append((char) c); + break; + } + } else + { + buf.append((char) c); + } + } + return buf; + } + + private Object expandExp() + { + assert '%' == ch; + Object val; + + if (getch() == '(') + { + val = evaluate.expr(group()); + getch(); + return val; + } + else + { + throw new SyntaxError(line, column, "bad expression: " + text); + } + } + + private Token group() + { + final char push = ch; + final char pop; + + switch (ch) + { + case '{': + pop = '}'; + break; + case '(': + pop = ')'; + break; + case '[': + pop = ']'; + break; + default: + assert false; + pop = 0; + } + + short sLine = line; + short sCol = column; + int start = index; + int depth = 1; + + while (true) + { + boolean comment = false; + + switch (ch) + { + case '{': + case '(': + case '[': + case '\n': + comment = true; + break; + } + + if (getch() == EOT) + { + throw new EOFError(sLine, sCol, "unexpected EOT looking for matching '" + + pop + "'", "compound", Character.toString(pop)); + } + + // don't recognize comments that start within a word + if (comment || isBlank(ch)) + skipSpace(); + + switch (ch) + { + case '"': + case '\'': + skipQuote(); + break; + + case '\\': + ch = escape(); + break; + + default: + if (push == ch) + depth++; + else if (pop == ch && --depth == 0) + return text.subSequence(start, index - 1); + } + } + + } + + private Object expandVar() throws Exception + { + return expandVar(false); + } + + private Object expandVar(boolean rawVariable) throws Exception + { + assert '$' == ch; + + Object val = null; + + short sLine = line; + short sCol = column; + + if (getch() != '{') + { + if ('(' == ch) + { // support $(...) FELIX-2433 + int start = index - 1; + find(')', '('); + Token p = text.subSequence(start, index); + val = evaluate.eval(new Parser(p).sequence()); + getch(); + } + else + { + int start = index - 1; + while (isName(ch)) + { + getch(); + } + + if (index - 1 == start) + { + val = "$"; + } + else + { + String name = text.subSequence(start, index - 1).toString(); + val = evaluate.get(name); + this.rawVariable = rawVariable; + } + } + } + else + { + getch(); + + // unique flag + boolean flagu = false; + // sort flags + boolean flago = false; + boolean flagO = false; + boolean flaga = false; + boolean flagi = false; + boolean flagn = false; + // map flags + boolean flagk = false; + boolean flagv = false; + // param flags + boolean flagP = false; + // case transformation flags + boolean flagC = false; + boolean flagL = false; + boolean flagU = false; + // pattern flags + boolean flagG = false; + // expand flag + boolean flagExpand = false; + // visible chars + boolean flagV = false; + // codepoint + boolean flagSharp = false; + // quoting + int flagq = 0; + boolean flagQ = false; + // split / join + String flags = null; + String flagj = null; + // pattern + boolean flagPattern = false; + // length + boolean computeLength = false; + // Parse flags + if (ch == '(') { + getch(); + boolean flagp = false; + while (ch != EOT && ch != ')') + { + switch (ch) + { + case 'u': + flagu = true; + break; + case 'p': + flagp = true; + break; + case 'f': + flags = "\n"; + break; + case 'F': + flagj = "\n"; + break; + case 's': + case 'j': { + char opt = ch; + char c = getch(); + if (c == EOT) + { + throw new IllegalArgumentException("error in flags"); + } + int start = index; + while (true) + { + char n = getch(); + if (n == EOT) + { + throw new IllegalArgumentException("error in flags"); + } + else if (n == c) + { + break; + } + } + String s = text.subSequence(start, index - 1).toString(); + if (flagp) + { + s = ansiEscape(s).toString(); + } + if (opt == 's') + { + flags = s; + } + else if (opt == 'j') + { + flagj = s; + } + else + { + throw new IllegalArgumentException("error in flags"); + } + flagp = false; + break; + } + case 'q': + if (flagq != 0) + { + throw new IllegalArgumentException("error in flags"); + } + flagq = 1; + if (peek() == '-') + { + flagq = -1; + getch(); + } + else + { + while (peek() == 'q') + { + getch(); + flagq++; + } + if (peek() == '-') + { + throw new IllegalArgumentException("error in flags"); + } + } + break; + case 'Q': + flagQ = true; + break; + case '#': + flagSharp = true; + break; + case 'V': + flagV = true; + break; + case 'o': + flago = true; + break; + case 'O': + flagO = true; + break; + case 'a': + flaga = true; + break; + case 'i': + flagi = true; + break; + case 'n': + flagn = true; + break; + case 'P': + flagP = true; + break; + case '@': + flagExpand = true; + break; + case 'G': + flagG = true; + break; + case 'k': + flagk = true; + break; + case 'v': + flagv = true; + break; + case 'C': + flagC = true; + flagL = false; + flagU = false; + break; + case 'L': + flagC = false; + flagL = true; + flagU = false; + break; + case 'U': + flagC = false; + flagL = false; + flagU = true; + break; + default: + throw new SyntaxError(line, column, "unsupported flag: " + ch); + } + getch(); + } + getch(); + } + + // Map to List conversion + final boolean _flagk = flagk; + final boolean _flagv = flagv; + final Function toCollection = new Function() { + @Override + public Object apply(Object v) { + return v instanceof Map + ? toList(asMap(v), _flagk, _flagv) + : v != null && v.getClass().isArray() + ? Arrays.asList((Object[]) v) + : v; + } + }; + // + // String transformations + // + final BiFunction, Object, Object> stringApplyer = new BiFunction, Object, Object>() { + @Override + public Object apply(Function func, Object v) { + v = toCollection.apply(v); + if (v instanceof Collection) + { + List l = new ArrayList<>(); + for (Object i : asCollection(v)) { + l.add(func.apply(String.valueOf(i))); + } + return l; + } + else if (v != null) + { + return func.apply(v.toString()); + } + else { + return null; + } + } + }; + + if (ch == '+') + { + getch(); + val = getAndEvaluateName(); + } + else + { + while (true) + { + if (ch == '#') + { + computeLength = true; + getch(); + } + else if (ch == '=') + { + if (flags == null) { + flags = "\\s"; + } + getch(); + } + else if (ch == '~') + { + flagPattern = true; + getch(); + } + else + { + break; + } + } + + Object val1 = getName('}'); + + if (ch == '}' || ch == '[') + { + val = val1 instanceof Token ? evaluate.get(expand((Token) val1).toString()) : val1; + } + else + { + int start = index - 1; + while (ch != EOT && ch != '}' && ":-+=?#%/".indexOf(ch) >= 0) + { + getch(); + } + Token op = text.subSequence(start, index - 1); + if (Token.eq("-", op) || Token.eq(":-", op)) + { + val1 = val1 instanceof Token ? evaluate.get(expand((Token) val1).toString()) : val1; + Object val2 = getValue(); + val = val1 == null ? val2 : val1; + } + else if (Token.eq("+", op) || Token.eq(":+", op)) + { + val1 = val1 instanceof Token ? evaluate.get(expand((Token) val1).toString()) : val1; + Object val2 = getValue(); + val = val1 != null ? val2 : null; + } + else if (Token.eq("=", op) || Token.eq(":=", op) || Token.eq("::=", op)) + { + if (!(val1 instanceof Token)) + { + throw new SyntaxError(line, column, "not an identifier"); + } + String name = expand((Token) val1).toString(); + val1 = evaluate.get(name); + val = getValue(); + if (Token.eq("::=", op) || val1 == null) + { + evaluate.put(name, val); + } + } + else if (Token.eq("?", op) || Token.eq(":?", op)) + { + String name; + if (val1 instanceof Token) + { + name = expand((Token) val1).toString(); + val = evaluate.get(name); + } + else + { + name = ""; + val = val1; + } + if (val == null || val.toString().length() == 0) + { + throw new IllegalArgumentException(name + ": parameter not set"); + } + } + else if (Token.eq("#", op) || Token.eq("##", op) + || Token.eq("%", op) || Token.eq("%%", op) + || Token.eq("/", op) || Token.eq("//", op)) + { + val1 = val1 instanceof Token ? evaluate.get(expand((Token) val1).toString()) : val1; + String val2 = getPattern(op.charAt(0) == '/' ? "/}" : "}"); + if (val2 != null) + { + String p = toRegexPattern(unquoteGlob(val2), op.length() == 1); + String r; + if (op.charAt(0) == '/') + { + if (ch == '/') + { + getch(); + r = getValue().toString(); + } + else + { + r = ""; + } + } + else + { + r = ""; + } + String m = op.charAt(0) == '#' ? "^" + p : op.charAt(0) == '%' ? p + "$" : p; + val1 = toCollection.apply(val1); + if (val1 instanceof Collection) + { + List l = new ArrayList<>(); + for (Object o : ((Collection) val1)) + { + if (flagG) + { + l.add(o.toString().replaceAll(m, r)); + } + else + { + l.add(o.toString().replaceFirst(m, r)); + } + } + val = l; + } + else if (val1 != null) + { + if (flagG) + { + val = val1.toString().replaceAll(m, r); + } + else + { + val = val1.toString().replaceFirst(m, r); + } + } + } + else + { + val = val1; + } + } + } + } + + // + // Subscripts + // + while (ch == '[') + { + Object left; + boolean nLeft = false; + Object right; + boolean nRight = false; + getch(); + if (ch == '*') + { + left = text.subSequence(index - 1, index); + getch(); + } + else if (ch == '@') + { + left = text.subSequence(index - 1, index); + flagExpand = true; + getch(); + } + else + { + if (ch == '-') + { + nLeft = true; + getch(); + } + left = getName(']'); + } + if (ch == ',') + { + getch(); + if (ch == '-') + { + nRight = true; + getch(); + } + right = getName(']'); + } + else + { + right = null; + } + if (ch != ']') + { + throw new SyntaxError(line, column, "invalid subscript"); + } + getch(); + if (right == null) + { + left = left instanceof Token ? expand((Token) left) : left; + String sLeft = left.toString(); + if (val instanceof Map) + { + if (sLeft.equals("@") || sLeft.equals("*")) + { + val = toList(asMap(val), flagk, flagv); + } + else + { + val = ((Map) val).get(sLeft); + } + } + else if (val instanceof List) + { + if (sLeft.equals("@") || sLeft.equals("*")) + { + val = new ArgList((List) val); + } + else + { + int iLeft = Integer.parseInt(sLeft); + List list = (List) val; + val = list.get(nLeft ? list.size() - 1 - iLeft : iLeft); + } + } + else if (val != null && val.getClass().isArray()) + { + if (sLeft.equals("@") || sLeft.equals("*")) + { + final Object array = val; + List l = new AbstractList() + { + @Override + public Object get(int index) + { + return Array.get(array, index); + } + @Override + public int size() + { + return Array.getLength(array); + } + }; + val = new ArgList(l); + } + else + { + int iLeft = Integer.parseInt(sLeft); + Object array = val; + val = Array.get(array, iLeft); + } + } + else if (val != null) + { + if (sLeft.equals("@") || sLeft.equals("*")) + { + val = val.toString(); + } + else + { + int iLeft = Integer.parseInt(sLeft); + String str = val.toString(); + val = str.charAt(nLeft ? str.length() - 1 - iLeft : iLeft); + } + } + } + else + { + if (val instanceof Map) + { + val = null; + } + else + { + left = left instanceof Token ? expand((Token) left) : left; + right = right instanceof Token ? expand((Token) right) : right; + int iLeft = Integer.parseInt(left.toString()); + int iRight = Integer.parseInt(right.toString()); + if (val instanceof List) + { + List list = (List) val; + val = list.subList(nLeft ? list.size() - iLeft : iLeft, + nRight ? list.size() - iRight : iRight); + } + else + { + String str = val.toString(); + val = str.substring(nLeft ? str.length() - iLeft : iLeft, + nRight ? str.length() - iRight : iRight); + } + } + } + } + + if (ch != '}') + { + throw new SyntaxError(sLine, sCol, "bad substitution"); + } + + // Parameter name replacement + if (flagP) + { + val = val != null ? evaluate.get(val.toString()) : null; + } + + // Double quote joining + boolean joined = false; + if (inQuote && !computeLength && !flagExpand) + { + val = toCollection.apply(val); + if (val instanceof Collection) + { + String j = flagj != null ? flagj : " "; + StringBuilder sb = new StringBuilder(); + for (Object i : asCollection(val)) { + if (sb.length() > 0) { + sb.append(j); + } + sb.append(String.valueOf(i)); + } + val = sb.toString(); + joined = true; + } + } + + // Character evaluation + if (flagSharp) + { + val = stringApplyer.apply(new Function() { + public String apply(String s) { return Expander.this.sharp(s); }; + }, val); + } + + // Length + if (computeLength) + { + if (val instanceof Collection) + { + val = ((Collection) val).size(); + } + else if (val instanceof Map) + { + val = ((Map) val).size(); + } + else if (val != null) + { + val = val.toString().length(); + } + else + { + val = 0; + } + } + + // Forced joining + if (flagj != null || flags != null && !joined) + { + val = toCollection.apply(val); + if (val instanceof Collection) + { + String j = flagj != null ? flagj : " "; + StringBuilder sb = new StringBuilder(); + for (Object i : asCollection(val)) { + if (sb.length() > 0) { + sb.append(j); + } + sb.append(String.valueOf(i)); + } + val = sb.toString(); + } + } + + // Simple word splitting + if (flags != null) + { + val = toCollection.apply(val); + if (!(val instanceof Collection)) + { + val = Collections.singletonList(val); + } + List l = new ArrayList<>(); + for (Object i : asCollection(val)) { + Collections.addAll(l, String.valueOf(i).split(flags)); + } + val = l; + } + + Function toLowerCase = new Function() { + @Override + public String apply(String t) { + return t.toLowerCase(); + } + }; + + Function toUpperCase = new Function() { + @Override + public String apply(String t) { + return t.toUpperCase(); + } + }; + + // Case modification + if (flagC) + { + val = stringApplyer.apply(new Function(){ + @Override + public String apply(String t) { + return Expander.this.toCamelCase(t); + }}, val); + } + else if (flagL) + { + val = stringApplyer.apply(toLowerCase, val); + } + else if (flagU) + { + val = stringApplyer.apply(toUpperCase, val); + } + + // Visibility enhancement + if (flagV) + { + val = stringApplyer.apply(new Function() { + @Override + public String apply(String t) { + return visible(t); + }}, val); + } + + // Quote + if (flagq != 0) + { + final int _flagq = flagq; + val = stringApplyer.apply(new Function() { + @Override + public String apply(String s) { + return quote(s, _flagq); + } + }, val); + inQuote = true; + } + else if (flagQ) { + val = stringApplyer.apply(new Function(){ + @Override + public String apply(String t) { + return unquote(t); + }}, val); + } + + // Uniqueness + if (flagu) + { + val = toCollection.apply(val); + if (val instanceof Collection) + { + val = new ArrayList<>(new LinkedHashSet<>(asCollection(val))); + } + } + + // Ordering + if (flaga || flagi || flagn || flago || flagO) + { + val = toCollection.apply(val); + if (val instanceof Collection) + { + List list; + if (flagn) + { + final boolean _flagi = flagi; + List l = new ArrayList<>(); + for (Object i : asCollection(val)) { + l.add(String.valueOf(i)); + } + Collections.sort(l, new Comparator() { + @Override + public int compare(String s1, String s2) { + return numericCompare(s1, s2, _flagi); + } + }); + list = l; + } + else if (flaga) + { + list = new ArrayList((Collection)val); + } + else + { + //Comparator comparator = flagi ? String.CASE_INSENSITIVE_ORDER : String::compareTo; + Comparator comparator; + if (flagi) { + comparator = String.CASE_INSENSITIVE_ORDER; + } + else { + comparator = new Comparator() { + @Override + public int compare(String s1, String s2) { + return s1.compareTo(s2); + }}; + } + + List l = new ArrayList<>(); + for (Object i : asCollection(val)) { + l.add(String.valueOf(i)); + } + //l.sort(comparator); + Collections.sort(l, comparator); + list = l; + } + if (flagO) + { + Collections.reverse(list); + } + val = list; + } + } + + // Semantic joining + if (semanticJoin) + { + val = toCollection.apply(val); + if (val instanceof Collection) + { + StringBuilder sb = new StringBuilder(); + for (Object i : asCollection(val)) { + if (sb.length() > 0) { + sb.append(" "); + } + sb.append(String.valueOf(i)); + } + val = sb.toString(); + } + } + + // Empty argument removal + if (val instanceof Collection) + { + List l = new ArrayList<>(); + for (Object o : asCollection(val)) { + if (!(o instanceof CharSequence) || ((CharSequence) o).length() > 0) { + l.add(o); + } + } + val = l; + } + + if (asPattern && !inQuote && !flagPattern) + { + val = toCollection.apply(val); + List patterns = new ArrayList<>(); + for (Object o : toCollection(val)) { + patterns.add(quote(String.valueOf(o), 2)); + } + val = patterns.size() == 1 ? patterns.get(0) : patterns; + } + + if (inQuote) + { + val = toCollection.apply(val); + if (val instanceof Collection) + { + List l = new ArrayList<>(asCollection(val)); + if (flagExpand) + { + val = new ArgList(l); + } + else + { + val = l; + } + } + } + else + { + if (flagExpand && val instanceof List) + { + val = new ArgList((List) val); + } + } + + getch(); + } + + return val; + } + + private String quote(String s, int flagq) + { + StringBuilder buf = new StringBuilder(); + // Backslashes + if (flagq == 1) + { + for (int i = 0; i < s.length(); i++) + { + char ch = s.charAt(i); + if (ch < 32 || ch >= 127) + { + buf.append("$'\\").append(Integer.toOctalString(ch)).append("\'"); + } + else if (" !\"#$&'()*;<=>?[\\]{|}~%".indexOf(ch) >= 0) + { + buf.append("\\").append(ch); + } + else + { + buf.append(ch); + } + } + } + // Single quotes + else if (flagq == 2) + { + buf.append("'"); + for (int i = 0; i < s.length(); i++) + { + char ch = s.charAt(i); + if (ch == '\'') + { + buf.append("'\\''"); + } + else + { + buf.append(ch); + } + } + buf.append("'"); + } + // Double quotes + else if (flagq == 3) + { + buf.append("\""); + for (int i = 0; i < s.length(); i++) + { + char ch = s.charAt(i); + if ("\"\\$%".indexOf(ch) >= 0) + { + buf.append("\\").append(ch); + } + else + { + buf.append(ch); + } + } + buf.append("\""); + } + // Posix + else if (flagq == 4) + { + buf.append("$'"); + for (int i = 0; i < s.length(); i++) + { + char ch = s.charAt(i); + switch (ch) + { + case '\n': + buf.append("\\n"); + break; + case '\t': + buf.append("\\t"); + break; + case '\r': + buf.append("\\r"); + break; + case '\'': + buf.append("\\'"); + break; + default: + if (ch < 32 || ch >= 127) + { + buf.append("\\").append(Integer.toOctalString(ch)); + } + else + { + buf.append(ch); + } + break; + } + } + buf.append("'"); + } + // Readable + else + { + boolean needQuotes = false; + for (int i = 0; i < s.length(); i++) + { + char ch = s.charAt(i); + if (ch < 32 || ch >= 127 || " !\"#$&'()*;<=>?[\\]{|}~%".indexOf(ch) >= 0) + { + needQuotes = true; + break; + } + } + return needQuotes ? quote(s, 2) : s; + } + return buf.toString(); + } + + private String unquote(String arg) + { + boolean hasEscape = false; + for (int i = 0; i < arg.length(); i++) + { + int c = arg.charAt(i); + if (c == '\\' || c == '"' || c == '\'') + { + hasEscape = true; + break; + } + } + if (!hasEscape) + { + return arg; + } + boolean singleQuoted = false; + boolean doubleQuoted = false; + boolean escaped = false; + StringBuilder buf = new StringBuilder(arg.length()); + for (int i = 0; i < arg.length(); i++) + { + char c = arg.charAt(i); + if (doubleQuoted && escaped) + { + if (c != '"' && c != '\\' && c != '$' && c != '%') + { + buf.append('\\'); + } + buf.append(c); + escaped = false; + } + else if (escaped) + { + buf.append(c); + escaped = false; + } + else if (singleQuoted) + { + if (c == '\'') + { + singleQuoted = false; + } + else + { + buf.append(c); + } + } + else if (doubleQuoted) + { + if (c == '\\') + { + escaped = true; + } + else if (c == '\"') + { + doubleQuoted = false; + } + else { + buf.append(c); + } + } + else if (c == '\\') + { + escaped = true; + } + else if (c == '\'') + { + singleQuoted = true; + } + else if (c == '"') + { + doubleQuoted = true; + } + else { + buf.append(c); + } + } + return buf.toString(); + } + + private int numericCompare(String s1, String s2, boolean caseInsensitive) + { + int i1s = 0, i2s = 0; + while (i1s < s1.length() && i2s < s2.length()) + { + char c1 = s1.charAt(i1s); + char c2 = s2.charAt(i2s); + if (caseInsensitive) + { + c1 = Character.toLowerCase(c1); + c2 = Character.toLowerCase(c2); + } + if (c1 != c2) + { + if (c1 >= '0' && c1 <= '9' && c2 >= '0' && c2 <= '9') + { + break; + } + else + { + return c1 < c2 ? -1 : 1; + } + } + i1s++; + i2s++; + } + while (i1s > 0) + { + char c1 = s1.charAt(i1s - 1); + if (c1 < '0' || c1 > '9') + { + break; + } + i1s--; + } + while (i2s > 0) + { + char c2 = s2.charAt(i2s - 1); + if (c2 < '0' || c2 > '9') + { + break; + } + i2s--; + } + int i1e = i1s; + int i2e = i2s; + while (i1e < s1.length() - 1) + { + char c1 = s1.charAt(i1e + 1); + if (c1 < '0' || c1 > '9') + { + break; + } + i1e++; + } + while (i2e < s2.length() - 1) + { + char c2 = s2.charAt(i2e + 1); + if (c2 < '0' || c2 > '9') + { + break; + } + i2e++; + } + int i1 = Integer.parseInt(s1.substring(i1s, i1e + 1)); + int i2 = Integer.parseInt(s2.substring(i2s, i2e + 1)); + if (i1 < i2) + { + return -1; + } + else if (i1 > i2) + { + return 1; + } + else + { + return i1e > i2e ? -1 : 1; + } + } + + private String toCamelCase(String s) + { + return s.isEmpty() ? s : s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); + } + + private String sharp(String s) + { + int codepoint = 0; + try + { + codepoint = Integer.parseInt(s); + } + catch (NumberFormatException e) + { + // Ignore + } + return new String(Character.toChars(codepoint)); + } + + private String visible(String s) + { + StringBuilder sb = new StringBuilder(s.length() * 2); + for (int i = 0; i < s.length(); i++) + { + char ch = s.charAt(i); + if (ch < 32) + { + sb.append('^'); + sb.append((char)(ch + '@')); + } + else + { + sb.append(ch); + } + } + return sb.toString(); + } + + @SuppressWarnings("unchecked") + private Collection asCollection(Object val) + { + return (Collection) val; + } + + private Collection toCollection(Object val) { + return val instanceof Collection + ? asCollection(val) + : Collections.singleton(val); + } + + @SuppressWarnings("unchecked") + private Map asMap(Object val) + { + return (Map) val; + } + + private List toList(Map val1, boolean flagk, boolean flagv) + { + List l = new ArrayList<>(); + if (flagk && flagv) + { + for (Map.Entry entry : val1.entrySet()) + { + l.add(entry.getKey()); + l.add(entry.getValue()); + } + } + else if (flagk) + { + l.addAll(val1.keySet()); + } + else + { + l.addAll(val1.values()); + } + return l; + } + + private Object getAndEvaluateName() throws Exception + { + Object r = getName('}'); + if (r instanceof Token) + { + return evaluate.get(expand((Token) r).toString()); + } + else + { + return r; + } + } + + private Object getName(char closing) throws Exception + { + if (ch == '\"') + { + if (peek() != '$') + { + throw new IllegalArgumentException("bad substitution"); + } + boolean oldInQuote = inQuote; + try + { + inQuote = true; + getch(); + return getName(closing); + } + finally + { + inQuote = oldInQuote; + } + } + else if (ch == '$') + { + return expandVar(); + } + else { + int start = index - 1; + while (ch != EOT && ch != closing && isName(ch)) + { + getch(); + if (ch == '\\') + { + escape(); + } + else if (ch == '{') + { + findClosing(); + } + } + if (ch == EOT) + { + throw new EOFError(line, column, "unexpected EOT looking for matching '}'", "compound", Character.toString('}')); + } + return text.subSequence(start, index - 1); + } + } + + private String getPattern(String closing) throws Exception + { + CharSequence sub = findUntil(text, index - 1, closing); + index += sub.length() - 1; + getch(); + return expandPattern(sub).toString(); + } + + private CharSequence findUntil(CharSequence text, int start, String closing) { + int braces = 0; + boolean escaped = false; + boolean doubleQuoted = false; + boolean singleQuoted = false; + for (int i = start; i < text.length(); i++) + { + char c = text.charAt(i); + if (doubleQuoted && escaped) + { + escaped = false; + } + else if (escaped) + { + escaped = false; + } + else if (singleQuoted) + { + if (c == '\'') + { + singleQuoted = false; + } + } + else if (doubleQuoted) + { + if (c == '\\') + { + escaped = true; + } + else if (c == '\"') + { + doubleQuoted = false; + } + } + else if (c == '\\') + { + escaped = true; + } + else if (c == '\'') + { + singleQuoted = true; + } + else if (c == '"') + { + doubleQuoted = true; + } + else { + if (braces == 0 && closing.indexOf(c) >= 0) + { + return text.subSequence(start, i); + } + else if (c == '{') + { + braces++; + } + else if (c == '}') + { + braces--; + } + } + } + return text.subSequence(start, text.length()); + } + + private Object getValue() throws Exception + { + if (ch == '$') + { + return expandVar(); + } + else + { + int start = index - 1; + while (ch != EOT && ch != '}') + { + if (ch == '\\') + { + escape(); + getch(); + } + else if (ch == '{' || ch == '(' || ch == '[') + { + findClosing(); + } + else + { + getch(); + } + } + if (ch == EOT) + { + throw new EOFError(line, column, "unexpected EOT looking for matching '}'", "compound", Character.toString('}')); + } + Token name = text.subSequence(start, index - 1); + return expand(name).toString(); + } + } + + private void findClosing() + { + char start = ch; + while (getch() != EOT) + { + if (ch == '(' || ch == '{' || ch == '[') + { + findClosing(); + } + else if (start == '(' && ch == ')' + || start == '{' && ch == '}' + || start == '[' && ch == ']') + { + return; + } + } + } + + private static final char EOL = 0; + + private static boolean isRegexMeta(char ch) + { + return ".^$+{[]|()".indexOf(ch) != -1; + } + + private static boolean isGlobMeta(char ch) + { + return "\\*?[{".indexOf(ch) != -1; + } + + private static char next(String str, int index) + { + return index < str.length() ? str.charAt(index) : EOL; + } + + /** + * Convert a string containing escape sequences and quotes, representing a glob pattern + * to the corresponding regexp pattern + */ + private static String unquoteGlob(String str) + { + StringBuilder sb = new StringBuilder(); + int index = 0; + boolean escaped = false; + boolean doubleQuoted = false; + boolean singleQuoted = false; + while (index < str.length()) + { + char ch = str.charAt(index++); + if (escaped) + { + if (isGlobMeta(ch)) + { + sb.append('\\'); + } + sb.append(ch); + escaped = false; + } + else if (singleQuoted) + { + if (ch == '\'') + { + singleQuoted = false; + } + else + { + if (isGlobMeta(ch)) + { + sb.append('\\'); + } + sb.append(ch); + } + } + else if (doubleQuoted) + { + if (ch == '\\') + { + escaped = true; + } + else if (ch == '\"') + { + doubleQuoted = false; + } + else + { + if (isGlobMeta(ch)) + { + sb.append('\\'); + } + sb.append(ch); + } + } + else + { + switch (ch) + { + case '\\': + escaped = true; + break; + case '\'': + singleQuoted = true; + break; + case '"': + doubleQuoted = true; + break; + default: + sb.append(ch); + break; + } + } + } + return sb.toString(); + } + + private static String toRegexPattern(String str, boolean shortest) + { + boolean inGroup = false; + StringBuilder sb = new StringBuilder(); + int index = 0; + while (index < str.length()) + { + char ch = str.charAt(index++); + switch (ch) + { + case '*': + sb.append(shortest ? ".*?" : ".*"); + break; + case ',': + if (inGroup) + { + sb.append(")|(?:"); + } + else + { + sb.append(','); + } + break; + case '?': + sb.append("."); + break; + case '[': + sb.append("["); + if (next(str, index) == '^') + { + sb.append("\\^"); + ++index; + } + else + { + if (next(str, index) == '!') + { + sb.append('^'); + ++index; + } + if (next(str, index) == '-') + { + sb.append('-'); + ++index; + } + } + boolean inLeft = false; + char left = 0; + while (index < str.length()) + { + ch = str.charAt(index++); + if (ch == ']') + { + break; + } + if (ch == '\\' || ch == '[' || ch == '&' && next(str, index) == '&') + { + sb.append('\\'); + } + sb.append(ch); + if (ch == '-') + { + if (!inLeft) + { + throw new PatternSyntaxException("Invalid range", str, index - 1); + } + if ((ch = next(str, index++)) == EOL || ch == ']') + { + break; + } + if (ch < left) + { + throw new PatternSyntaxException("Invalid range", str, index - 3); + } + sb.append(ch); + inLeft = false; + } + else + { + inLeft = true; + left = ch; + } + } + if (ch != ']') + { + throw new PatternSyntaxException("Missing \']", str, index - 1); + } + sb.append("]"); + break; + case '\\': + if (index == str.length()) + { + throw new PatternSyntaxException("No character to escape", str, index - 1); + } + char ch2 = str.charAt(index++); + if (isGlobMeta(ch2) || isRegexMeta(ch2)) + { + sb.append('\\'); + } + sb.append(ch2); + break; + case '{': + if (inGroup) + { + throw new PatternSyntaxException("Cannot nest groups", str, index - 1); + } + sb.append("(?:(?:"); + inGroup = true; + break; + case '}': + if (inGroup) + { + sb.append("))"); + inGroup = false; + } + else + { + sb.append('}'); + } + break; + default: + if (isRegexMeta(ch)) + { + sb.append('\\'); + } + sb.append(ch); + break; + } + } + if (inGroup) + { + throw new PatternSyntaxException("Missing \'}", str, index - 1); + } + return sb.toString(); + } + + + private boolean isName(char ch) + { + return Character.isJavaIdentifierPart(ch) && (ch != '$') || ('.' == ch); + } + +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Expression.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Expression.java new file mode 100644 index 00000000000..24459aa03fb --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Expression.java @@ -0,0 +1,1365 @@ +/* + * 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. + */ +/* + * Copyright 2012 Udo Klimaschewski + * + * http://UdoJava.com/ + * http://about.me/udo.klimaschewski + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package org.apache.felix.gogo.runtime; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.MathContext; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Stack; + +/** + * Enhanced to provide assignment operators and variables from a map, comparison operators, string operations and more. + */ +/** + *

      EvalEx - Java Expression Evaluator

      + * + *

      Introduction

      + * EvalEx is a handy expression evaluator for Java, that allows to evaluate simple mathematical and boolean expressions. + *
      + * Key Features: + *
        + *
      • Uses BigDecimal for calculation and result
      • + *
      • Single class implementation, very compact
      • + *
      • No dependencies to external libraries
      • + *
      • Precision and rounding mode can be set
      • + *
      • Supports variables
      • + *
      • Standard boolean and mathematical operators
      • + *
      • Standard basic mathematical and boolean functions
      • + *
      • Custom functions and operators can be added at runtime
      • + *
      + *
      + *

      Examples

      + *
      + *  BigDecimal result = null;
      + *
      + *  Expression expression = new Expression("1+1/3");
      + *  result = expression.eval():
      + *  expression.setPrecision(2);
      + *  result = expression.eval():
      + *
      + *  result = new Expression("(3.4 + -4.1)/2").eval();
      + *
      + *  result = new Expression("SQRT(a^2 + b^2").with("a","2.4").and("b","9.253").eval();
      + *
      + *  BigDecimal a = new BigDecimal("2.4");
      + *  BigDecimal b = new BigDecimal("9.235");
      + *  result = new Expression("SQRT(a^2 + b^2").with("a",a).and("b",b).eval();
      + *
      + *  result = new Expression("2.4/PI").setPrecision(128).setRoundingMode(RoundingMode.UP).eval();
      + *
      + *  result = new Expression("random() > 0.5").eval();
      + *
      + *  result = new Expression("not(x < 7 || sqrt(max(x,9)) <= 3))").with("x","22.9").eval();
      + * 
      + *
      + *

      Supported Operators

      + * + * + * + * + * + * + * + * + * + *
      Mathematical Operators
      OperatorDescription
      +Additive operator
      -Subtraction operator
      *Multiplication operator
      /Division operator
      %Remainder operator (Modulo)
      ^Power operator
      + *
      + * + * + * + * + * + * + * + * + * + * + * + * + * + *
      Boolean Operators*
      OperatorDescription
      =Equals
      ==Equals
      !=Not equals
      <>Not equals
      <Less than
      <=Less than or equal to
      >Greater than
      >=Greater than or equal to
      &&Boolean and
      ||Boolean or
      + * *Boolean operators result always in a BigDecimal value of 1 or 0 (zero). Any non-zero value is treated as a _true_ value. Boolean _not_ is implemented by a function. + *
      + *

      Supported Functions

      + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
      Function*Description
      NOT(expression)Boolean negation, 1 (means true) if the expression is not zero
      IF(condition,value_if_true,value_if_false)Returns one value if the condition evaluates to true or the other if it evaluates to false
      RANDOM()Produces a random number between 0 and 1
      MIN(e1,e2)Returns the smaller of both expressions
      MAX(e1,e2)Returns the bigger of both expressions
      ABS(expression)Returns the absolute (non-negative) value of the expression
      ROUND(expression,precision)Rounds a value to a certain number of digits, uses the current rounding mode
      FLOOR(expression)Rounds the value down to the nearest integer
      CEILING(expression)Rounds the value up to the nearest integer
      LOG(expression)Returns the natural logarithm (base e) of an expression
      SQRT(expression)Returns the square root of an expression
      SIN(expression)Returns the trigonometric sine of an angle (in degrees)
      COS(expression)Returns the trigonometric cosine of an angle (in degrees)
      TAN(expression)Returns the trigonometric tangens of an angle (in degrees)
      SINH(expression)Returns the hyperbolic sine of a value
      COSH(expression)Returns the hyperbolic cosine of a value
      TANH(expression)Returns the hyperbolic tangens of a value
      RAD(expression)Converts an angle measured in degrees to an approximately equivalent angle measured in radians
      DEG(expression)Converts an angle measured in radians to an approximately equivalent angle measured in degrees
      + * *Functions names are case insensitive. + *
      + *

      Supported Constants

      + * + * + * + * + * + *
      ConstantDescription
      PIThe value of PI, exact to 100 digits
      TRUEThe value one
      FALSEThe value zero
      + * + *

      Add Custom Operators

      + * + * Custom operators can be added easily, simply create an instance of `Expression.Operator` and add it to the expression. + * Parameters are the operator string, its precedence and if it is left associative. The operators `eval()` method will be called with the BigDecimal values of the operands. + * All existing operators can also be overridden. + *
      + * For example, add an operator `x >> n`, that moves the decimal point of _x_ _n_ digits to the right: + * + *
      + * Expression e = new Expression("2.1234 >> 2");
      + *
      + * e.addOperator(e.new Operator(">>", 30, true) {
      + *     {@literal @}Override
      + *     public BigDecimal eval(BigDecimal v1, BigDecimal v2) {
      + *         return v1.movePointRight(v2.toBigInteger().intValue());
      + *     }
      + * });
      + *
      + * e.eval(); // returns 212.34
      + * 
      + *
      + *

      Add Custom Functions

      + * + * Adding custom functions is as easy as adding custom operators. Create an instance of `Expression.Function`and add it to the expression. + * Parameters are the function name and the count of required parameters. The functions `eval()` method will be called with a list of the BigDecimal parameters. + * All existing functions can also be overridden. + *
      + * For example, add a function `average(a,b,c)`, that will calculate the average value of a, b and c: + *
      + *
      + * Expression e = new Expression("2 * average(12,4,8)");
      + *
      + * e.addFunction(e.new Function("average", 3) {
      + *     {@literal @}Override
      + *     public BigDecimal eval(List<BigDecimal> parameters) {
      + *         BigDecimal sum = parameters.get(0).add(parameters.get(1)).add(parameters.get(2));
      + *         return sum.divide(new BigDecimal(3));
      + *     }
      + * });
      + *
      + * e.eval(); // returns 16
      + * 
      + * The software is licensed under the MIT Open Source license (see LICENSE file). + *
      + *
        + *
      • The *power of* operator (^) implementation was copied from [Stack Overflow](http://stackoverflow.com/questions/3579779/how-to-do-a-fractional-power-on-bigdecimal-in-java) Thanks to Gene Marin
      • + *
      • The SQRT() function implementation was taken from the book [The Java Programmers Guide To numerical Computing](http://www.amazon.de/Java-Number-Cruncher-Programmers-Numerical/dp/0130460419) (Ronald Mak, 2002)
      • + *
      + * + *@author Udo Klimaschewski (http://about.me/udo.klimaschewski) + */ +public class Expression { + + /** + * Definition of PI as a constant, can be used in expressions as variable. + */ + public static final BigDecimal PI = new BigDecimal( + "3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679"); + + /** + * The {@link MathContext} to use for calculations. + */ + private MathContext mc = MathContext.DECIMAL32; + + /** + * The original infix expression. + */ + private String expression = null; + + /** + * The cached RPN (Reverse Polish Notation) of the expression. + */ + private List rpn = null; + + /** + * All defined operators with name and implementation. + */ + private Map operators = new HashMap<>(); + + /** + * All defined functions with name and implementation. + */ + private Map functions = new HashMap<>(); + + /** + * All defined variables with name and value. + */ + private Map constants = new HashMap<>(); + + /** + * What character to use for decimal separators. + */ + private final char decimalSeparator = '.'; + + /** + * What character to use for minus sign (negative values). + */ + private final char minusSign = '-'; + + /** + * The expression evaluators exception class. + */ + public class ExpressionException extends RuntimeException { + private static final long serialVersionUID = 1118142866870779047L; + + public ExpressionException(String message) { + super(message); + } + } + + interface Token { + + } + + public class Constant implements Token { + + private final Object value; + + public Constant(Object value) { + this.value = value; + } + + public Object getValue() { + return value; + } + } + + public class Variable implements Token { + + private final String name; + + public Variable(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return name; + } + } + + public class LeftParen implements Token { + @Override + public String toString() { + return "("; + } + } + + public class Comma implements Token { + } + + /** + * Abstract definition of a supported expression function. A function is + * defined by a name, the number of parameters and the actual processing + * implementation. + */ + public abstract class Function implements Token { + /** + * Name of this function. + */ + private String name; + /** + * Number of parameters expected for this function. + */ + private int numParams; + + /** + * Creates a new function with given name and parameter count. + * + * @param name + * The name of the function. + * @param numParams + * The number of parameters for this function. + */ + public Function(String name, int numParams) { + this.name = name.toUpperCase(); + this.numParams = numParams; + } + + public String getName() { + return name; + } + + public int getNumParams() { + return numParams; + } + + public BigDecimal eval(Map variables, List parameters) { + List numericParameters = new ArrayList<>(); + for (Object o : parameters) { + numericParameters.add(toBigDecimal(variables, o)); + } + return eval(numericParameters); + } + + /** + * Implementation for this function. + * + * @param parameters + * Parameters will be passed by the expression evaluator as a + * {@link List} of {@link BigDecimal} values. + * @return The function must return a new {@link BigDecimal} value as a + * computing result. + */ + public abstract BigDecimal eval(List parameters); + + @Override + public String toString() { + return name; + } + } + + /** + * Abstract definition of a supported operator. An operator is defined by + * its name (pattern), precedence and if it is left- or right associative. + */ + public abstract class Operator implements Token { + /** + * This operators name (pattern). + */ + private String oper; + /** + * Operators precedence. + */ + private int precedence; + /** + * Operator is left associative. + */ + private boolean leftAssoc; + + /** + * Creates a new operator. + * + * @param oper + * The operator name (pattern). + * @param precedence + * The operators precedence. + * @param leftAssoc + * true if the operator is left associative, + * else false. + */ + public Operator(String oper, int precedence, boolean leftAssoc) { + this.oper = oper; + this.precedence = precedence; + this.leftAssoc = leftAssoc; + } + + public String getOper() { + return oper; + } + + public int getPrecedence() { + return precedence; + } + + public boolean isLeftAssoc() { + return leftAssoc; + } + + public Object eval(Map variables, Object v1, Object v2) { + if (v1 instanceof Variable) { + v1 = variables.get(((Variable) v1).getName()); + } + if (v2 instanceof Variable) { + v2 = variables.get(((Variable) v2).getName()); + } + boolean numeric = isNumber(v1) && isNumber(v2); + if (numeric) { + return eval(toBigDecimal(variables, v1), toBigDecimal(variables, v2)); + } else { + return eval(v1 != null ? v1.toString() : "", v2 != null ? v2.toString() : ""); + } + } + + public Object eval(String v1, String v2) { + return eval(toBigDecimal(v1), toBigDecimal(v2)); + } + + /** + * Implementation for this operator. + * + * @param v1 + * Operand 1. + * @param v2 + * Operand 2. + * @return The result of the operation. + */ + public abstract BigDecimal eval(BigDecimal v1, BigDecimal v2); + + @Override + public String toString() { + return oper; + } + } + + public abstract class Comparator extends Operator { + + public Comparator(String oper, int precedence) { + super(oper, precedence, false); + } + + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return compare(v1, v2) ? BigDecimal.ONE : BigDecimal.ZERO; + } + + @Override + public Object eval(String v1, String v2) { + return compare(v1, v2) ? BigDecimal.ONE : BigDecimal.ZERO; + } + + /** + * This method actually implements the comparison. + * It will be called with either 2 BigIntegers or 2 Strings. + * + * @param v1 + * Operand 1. + * @param v2 + * Operand 2. + * @return The result of the comparison. + */ + public abstract boolean compare(Comparable v1, Comparable v2); + } + + /** + * Marker class for assignment operators. + * Those operators need a variable on the left hand side. + */ + public abstract class Assignment extends Operator { + + public Assignment(String assign, int precedence) { + super(assign, precedence, true); + } + + public Object eval(Map variables, Object v1, Object v2) { + if (!(v1 instanceof Variable)) { + throw new IllegalArgumentException("Left hand side of operator " + getOper() + " should be a variable but found " + v1.toString()); + } + String name = ((Variable) v1).getName(); + Object r = super.eval(variables, v1, v2); + if (r instanceof Number) { + r = toResult(toBigDecimal(r)); + } + variables.put(name, r); + return r; + } + + } + + + /** + * Expression tokenizer that allows to iterate over a {@link String} + * expression token by token. Blank characters will be skipped. + */ + private class Tokenizer implements Iterator { + + /** + * Actual position in expression string. + */ + private int pos = 0; + + /** + * The original input expression. + */ + private String input; + /** + * The previous token or null if none. + */ + private String previousToken; + + /** + * Creates a new tokenizer for an expression. + * + * @param input + * The expression string. + */ + public Tokenizer(String input) { + this.input = input; + } + + public boolean hasNext() { + return (pos < input.length()); + } + + /** + * Peek at the next character, without advancing the iterator. + * + * @return The next character or character 0, if at end of string. + */ + private char peekNextChar() { + if (pos < (input.length() - 1)) { + return input.charAt(pos + 1); + } else { + return 0; + } + } + + public String next() { + StringBuilder token = new StringBuilder(); + if (pos >= input.length()) { + return previousToken = null; + } + char ch = input.charAt(pos); + while (Character.isWhitespace(ch) && pos < input.length()) { + ch = input.charAt(++pos); + } + if (Character.isDigit(ch)) { + while ((Character.isDigit(ch) || ch == decimalSeparator) + && (pos < input.length())) { + token.append(input.charAt(pos++)); + ch = pos == input.length() ? 0 : input.charAt(pos); + } + } else if (ch == minusSign + && Character.isDigit(peekNextChar()) + && ("(".equals(previousToken) || ",".equals(previousToken) + || previousToken == null || operators + .containsKey(previousToken))) { + token.append(minusSign); + pos++; + token.append(next()); + } else if (Character.isLetter(ch)) { + while ((Character.isLetter(ch) || Character.isDigit(ch) || (ch == '_')) && (pos < input.length())) { + token.append(input.charAt(pos++)); + ch = pos == input.length() ? 0 : input.charAt(pos); + } + } else if (ch == '"') { + boolean escaped = false; + token.append(input.charAt(pos++)); + do { + if (pos == input.length()) { + throw new IllegalArgumentException("Non terminated quote"); + } + ch = input.charAt(pos++); + escaped = (!escaped && ch == '\\'); + token.append(ch); + } while (escaped || ch != '"'); + } else if (ch == '(' || ch == ')' || ch == ',') { + token.append(ch); + pos++; + } else { + while (!Character.isLetter(ch) && !Character.isDigit(ch) + && !Character.isWhitespace(ch) && ch != '(' + && ch != ')' && ch != ',' && (pos < input.length())) { + token.append(input.charAt(pos)); + pos++; + ch = pos == input.length() ? 0 : input.charAt(pos); + if (ch == minusSign) { + break; + } + } + if (!operators.containsKey(token.toString())) { + throw new ExpressionException("Unknown operator '" + token + + "' at position " + (pos - token.length() + 1)); + } + } + return previousToken = token.toString(); + } + + public void remove() { + throw new ExpressionException("remove() not supported"); + } + + /** + * Get the actual character position in the string. + * + * @return The actual character position. + */ + public int getPos() { + return pos; + } + + } + + /** + * Creates a new expression instance from an expression string. + * + * @param expression + * The expression. E.g. "2.4*sin(3)/(2-4)" or + * "sin(y)>0 & max(z, 3)>3" + */ + public Expression(String expression) { + this.expression = expression; + + addOperator(new Assignment("=", 5) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return v2; + } + }); + addOperator(new Assignment("+=", 5) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return v1.add(v2, mc); + } + + @Override + public Object eval(String v1, String v2) { + return v1 + v2; + } + }); + addOperator(new Assignment("-=", 5) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return v1.subtract(v2, mc); + } + }); + addOperator(new Assignment("*=", 5) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return v1.multiply(v2, mc); + } + }); + addOperator(new Assignment("/=", 5) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return v1.divide(v2, mc); + } + }); + addOperator(new Assignment("%=", 5) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return v1.remainder(v2, mc); + } + }); + addOperator(new Assignment("|=", 5) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return new BigDecimal(v1.toBigInteger().or(v2.toBigInteger()), mc); + } + }); + addOperator(new Assignment("&=", 5) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return new BigDecimal(v1.toBigInteger().and(v2.toBigInteger()), mc); + } + }); + addOperator(new Assignment("^=", 5) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return new BigDecimal(v1.toBigInteger().xor(v2.toBigInteger()), mc); + } + }); + addOperator(new Assignment("<<=", 5) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return new BigDecimal(v1.toBigInteger().shiftLeft(v2.intValue()), mc); + } + }); + addOperator(new Assignment(">>=", 5) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return new BigDecimal(v1.toBigInteger().shiftRight(v2.intValue()), mc); + } + }); + + addOperator(new Operator("<<", 10, true) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return new BigDecimal(v1.toBigInteger().shiftLeft(v2.intValue()), mc); + } + }); + addOperator(new Operator(">>", 10, true) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return new BigDecimal(v1.toBigInteger().shiftRight(v2.intValue()), mc); + } + }); + addOperator(new Operator("|", 15, true) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return new BigDecimal(v1.toBigInteger().or(v2.toBigInteger()), mc); + } + }); + addOperator(new Operator("&", 15, true) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return new BigDecimal(v1.toBigInteger().and(v2.toBigInteger()), mc); + } + }); + addOperator(new Operator("^", 15, true) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return new BigDecimal(v1.toBigInteger().xor(v2.toBigInteger()), mc); + } + }); + addOperator(new Operator("+", 20, true) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return v1.add(v2, mc); + } + @Override + public Object eval(String v1, String v2) { + return v1 + v2; + } + }); + addOperator(new Operator("-", 20, true) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return v1.subtract(v2, mc); + } + }); + addOperator(new Operator("*", 30, true) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return v1.multiply(v2, mc); + } + }); + addOperator(new Operator("/", 30, true) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return v1.divide(v2, mc); + } + }); + addOperator(new Operator("%", 30, true) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + return v1.remainder(v2, mc); + } + }); + addOperator(new Operator("**", 40, false) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + /*- + * Thanks to Gene Marin: + * http://stackoverflow.com/questions/3579779/how-to-do-a-fractional-power-on-bigdecimal-in-java + */ + int signOf2 = v2.signum(); + double dn1 = v1.doubleValue(); + v2 = v2.multiply(new BigDecimal(signOf2)); // n2 is now positive + BigDecimal remainderOf2 = v2.remainder(BigDecimal.ONE); + BigDecimal n2IntPart = v2.subtract(remainderOf2); + BigDecimal intPow = v1.pow(n2IntPart.intValueExact(), mc); + BigDecimal doublePow = new BigDecimal(Math.pow(dn1, + remainderOf2.doubleValue())); + + BigDecimal result = intPow.multiply(doublePow, mc); + if (signOf2 == -1) { + result = BigDecimal.ONE.divide(result, mc.getPrecision(), + RoundingMode.HALF_UP); + } + return result; + } + }); + addOperator(new Operator("&&", 4, false) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + boolean b1 = !v1.equals(BigDecimal.ZERO); + boolean b2 = !v2.equals(BigDecimal.ZERO); + return b1 && b2 ? BigDecimal.ONE : BigDecimal.ZERO; + } + }); + + addOperator(new Operator("||", 2, false) { + @Override + public BigDecimal eval(BigDecimal v1, BigDecimal v2) { + boolean b1 = !v1.equals(BigDecimal.ZERO); + boolean b2 = !v2.equals(BigDecimal.ZERO); + return b1 || b2 ? BigDecimal.ONE : BigDecimal.ZERO; + } + }); + + addOperator(new Comparator(">", 10) { + @Override @SuppressWarnings({ "unchecked", "rawtypes" }) + public boolean compare(Comparable v1, Comparable v2) { + return v1.compareTo(v2) > 0; + } + }); + + addOperator(new Comparator(">=", 10) { + @Override @SuppressWarnings({ "unchecked", "rawtypes" }) + public boolean compare(Comparable v1, Comparable v2) { + return v1.compareTo(v2) >= 0; + } + }); + + addOperator(new Comparator("<", 10) { + @Override @SuppressWarnings({ "unchecked", "rawtypes" }) + public boolean compare(Comparable v1, Comparable v2) { + return v1.compareTo(v2) < 0; + } + }); + + addOperator(new Comparator("<=", 10) { + @Override @SuppressWarnings({ "unchecked", "rawtypes" }) + public boolean compare(Comparable v1, Comparable v2) { + return v1.compareTo(v2) <= 0; + } + }); + + addOperator(new Comparator("==", 7) { + @Override @SuppressWarnings({ "unchecked", "rawtypes" }) + public boolean compare(Comparable v1, Comparable v2) { + return v1.compareTo(v2) == 0; + } + }); + + addOperator(new Comparator("!=", 7) { + @Override @SuppressWarnings({ "unchecked", "rawtypes" }) + public boolean compare(Comparable v1, Comparable v2) { + return v1.compareTo(v2) != 0; + } + }); + + addFunction(new Function("NOT", 1) { + @Override + public BigDecimal eval(List parameters) { + boolean zero = parameters.get(0).compareTo(BigDecimal.ZERO) == 0; + return zero ? BigDecimal.ONE : BigDecimal.ZERO; + } + }); + + addFunction(new Function("IF", 3) { + @Override + public BigDecimal eval(List parameters) { + boolean isTrue = !parameters.get(0).equals(BigDecimal.ZERO); + return isTrue ? parameters.get(1) : parameters.get(2); + } + }); + + addFunction(new Function("RANDOM", 0) { + @Override + public BigDecimal eval(List parameters) { + double d = Math.random(); + return new BigDecimal(d, mc); + } + }); + addFunction(new Function("SIN", 1) { + @Override + public BigDecimal eval(List parameters) { + double d = Math.sin(Math.toRadians(parameters.get(0) + .doubleValue())); + return new BigDecimal(d, mc); + } + }); + addFunction(new Function("COS", 1) { + @Override + public BigDecimal eval(List parameters) { + double d = Math.cos(Math.toRadians(parameters.get(0) + .doubleValue())); + return new BigDecimal(d, mc); + } + }); + addFunction(new Function("TAN", 1) { + @Override + public BigDecimal eval(List parameters) { + double d = Math.tan(Math.toRadians(parameters.get(0) + .doubleValue())); + return new BigDecimal(d, mc); + } + }); + addFunction(new Function("SINH", 1) { + @Override + public BigDecimal eval(List parameters) { + double d = Math.sinh(parameters.get(0).doubleValue()); + return new BigDecimal(d, mc); + } + }); + addFunction(new Function("COSH", 1) { + @Override + public BigDecimal eval(List parameters) { + double d = Math.cosh(parameters.get(0).doubleValue()); + return new BigDecimal(d, mc); + } + }); + addFunction(new Function("TANH", 1) { + @Override + public BigDecimal eval(List parameters) { + double d = Math.tanh(parameters.get(0).doubleValue()); + return new BigDecimal(d, mc); + } + }); + addFunction(new Function("RAD", 1) { + @Override + public BigDecimal eval(List parameters) { + double d = Math.toRadians(parameters.get(0).doubleValue()); + return new BigDecimal(d, mc); + } + }); + addFunction(new Function("DEG", 1) { + @Override + public BigDecimal eval(List parameters) { + double d = Math.toDegrees(parameters.get(0).doubleValue()); + return new BigDecimal(d, mc); + } + }); + addFunction(new Function("MAX", 2) { + @Override + public BigDecimal eval(List parameters) { + BigDecimal v1 = parameters.get(0); + BigDecimal v2 = parameters.get(1); + return v1.compareTo(v2) > 0 ? v1 : v2; + } + }); + addFunction(new Function("MIN", 2) { + @Override + public BigDecimal eval(List parameters) { + BigDecimal v1 = parameters.get(0); + BigDecimal v2 = parameters.get(1); + return v1.compareTo(v2) < 0 ? v1 : v2; + } + }); + addFunction(new Function("ABS", 1) { + @Override + public BigDecimal eval(List parameters) { + return parameters.get(0).abs(mc); + } + }); + addFunction(new Function("LOG", 1) { + @Override + public BigDecimal eval(List parameters) { + double d = Math.log(parameters.get(0).doubleValue()); + return new BigDecimal(d, mc); + } + }); + addFunction(new Function("ROUND", 2) { + @Override + public BigDecimal eval(List parameters) { + BigDecimal toRound = parameters.get(0); + int precision = parameters.get(1).intValue(); + return toRound.setScale(precision, mc.getRoundingMode()); + } + }); + addFunction(new Function("FLOOR", 1) { + @Override + public BigDecimal eval(List parameters) { + BigDecimal toRound = parameters.get(0); + return toRound.setScale(0, RoundingMode.FLOOR); + } + }); + addFunction(new Function("CEILING", 1) { + @Override + public BigDecimal eval(List parameters) { + BigDecimal toRound = parameters.get(0); + return toRound.setScale(0, RoundingMode.CEILING); + } + }); + addFunction(new Function("SQRT", 1) { + @Override + public BigDecimal eval(List parameters) { + /* + * From The Java Programmers Guide To numerical Computing + * (Ronald Mak, 2003) + */ + BigDecimal x = parameters.get(0); + if (x.compareTo(BigDecimal.ZERO) == 0) { + return new BigDecimal(0); + } + if (x.signum() < 0) { + throw new ExpressionException( + "Argument to SQRT() function must not be negative"); + } + BigInteger n = x.movePointRight(mc.getPrecision() << 1) + .toBigInteger(); + + int bits = (n.bitLength() + 1) >> 1; + BigInteger ix = n.shiftRight(bits); + BigInteger ixPrev; + + do { + ixPrev = ix; + ix = ix.add(n.divide(ix)).shiftRight(1); + // Give other threads a chance to work; + Thread.yield(); + } while (ix.compareTo(ixPrev) != 0); + + return new BigDecimal(ix, mc.getPrecision()); + } + }); + + constants.put("PI", PI); + constants.put("TRUE", Boolean.TRUE); + constants.put("FALSE", Boolean.FALSE); + + } + + /** + * Is the string a number? + * + * @param st + * The string. + * @return true, if the input string is a number. + */ + private boolean isNumber(String st) { + if (st == null || st.isEmpty() || st.charAt(0) == minusSign && st.length() == 1) + return false; + for (char ch : st.toCharArray()) { + if (!Character.isDigit(ch) && ch != minusSign + && ch != decimalSeparator) + return false; + } + return true; + } + + private boolean isNumber(Object obj) { + if (obj instanceof Number) { + return true; + } else if (obj != null) { + return isNumber(obj.toString()); + } else { + return false; + } + } + + /** + * Implementation of the Shunting Yard algorithm to transform an + * infix expression to a RPN expression. + * + * @param expression + * The input expression in infx. + * @return A RPN representation of the expression, with each token as a list + * member. + */ + private List shuntingYard(String expression) { + List outputQueue = new ArrayList<>(); + Stack stack = new Stack<>(); + + Tokenizer tokenizer = new Tokenizer(expression); + + String previousToken = null; + while (tokenizer.hasNext()) { + String token = tokenizer.next(); + if (token.charAt(0) == '"') { + StringBuilder sb = new StringBuilder(); + boolean escaped = false; + for (int i = 1; i < token.length() - 1; i++) { + char ch = token.charAt(i); + if (escaped || ch != '\\') { + sb.append(ch); + } else { + escaped = true; + } + } + outputQueue.add(new Constant(sb.toString())); + } else if (isNumber(token)) { + outputQueue.add(new Constant(toBigDecimal(token))); + } else if (constants.containsKey(token)) { + outputQueue.add(new Constant(constants.get(token))); + } else if (functions.containsKey(token.toUpperCase())) { + stack.push(functions.get(token.toUpperCase())); + } else if (Character.isLetter(token.charAt(0))) { + outputQueue.add(new Variable(token)); + } else if (",".equals(token)) { + while (!stack.isEmpty() && !(stack.peek() instanceof LeftParen)) { + outputQueue.add(stack.pop()); + } + if (stack.isEmpty()) { + outputQueue.add(new Comma()); + } + } else if (operators.containsKey(token)) { + Operator o1 = operators.get(token); + Token token2 = stack.isEmpty() ? null : stack.peek(); + while (token2 instanceof Operator + && ((o1.isLeftAssoc() && o1.getPrecedence() <= ((Operator) token2).getPrecedence()) + || (o1.getPrecedence() < ((Operator) token2).getPrecedence()))) { + outputQueue.add(stack.pop()); + token2 = stack.isEmpty() ? null : stack.peek(); + } + stack.push(o1); + } else if ("(".equals(token)) { + if (previousToken != null) { + if (isNumber(previousToken)) { + throw new ExpressionException("Missing operator at character position " + tokenizer.getPos()); + } + } + stack.push(new LeftParen()); + } else if (")".equals(token)) { + while (!stack.isEmpty() && !(stack.peek() instanceof LeftParen)) { + outputQueue.add(stack.pop()); + } + if (stack.isEmpty()) { + throw new RuntimeException("Mismatched parentheses"); + } + stack.pop(); + if (!stack.isEmpty() && stack.peek() instanceof Function) { + outputQueue.add(stack.pop()); + } + } + previousToken = token; + } + while (!stack.isEmpty()) { + Token element = stack.pop(); + if (element instanceof LeftParen) { + throw new RuntimeException("Mismatched parentheses"); + } + if (!(element instanceof Operator)) { + throw new RuntimeException("Unknown operator or function: " + element); + } + outputQueue.add(element); + } + return outputQueue; + } + + /** + * Evaluates the expression. + * + * @return The result of the expression. + */ + public Object eval() { + return eval(new HashMap()); + } + + /** + * Evaluates the expression. + * + * @param variables the variables + * @return The result of the expression. + */ + public Object eval(Map variables) { + + Stack stack = new Stack<>(); + + for (Token token : getRPN()) { + if (token instanceof Operator) { + Object v1 = stack.pop(); + Object v2 = stack.pop(); + Object oResult = ((Operator) token).eval(variables, v2, v1); + stack.push(oResult); + } else if (token instanceof Constant) { + stack.push(((Constant) token).getValue()); + } else if (token instanceof Function) { + Function f = (Function) token; + List p = new ArrayList<>(f.getNumParams()); + for (int i = 0; i < f.numParams; i++) { + p.add(0, stack.pop()); + } + Object fResult = f.eval(variables, p); + stack.push(fResult); + } else if (token instanceof Comma) { + stack.pop(); + } else { + stack.push(token); + } + } + if (stack.size() > 1) { + throw new IllegalArgumentException("Missing operator"); + } + Object result = stack.pop(); + if (result instanceof Variable) { + result = variables.get(((Variable) result).getName()); + } + if (result instanceof BigDecimal) { + result = toResult((BigDecimal) result); + } + return result; + } + + private Number toResult(BigDecimal r) { + long l = r.longValue(); + if (new BigDecimal(l).compareTo(r) == 0) { + return l; + } + double d = r.doubleValue(); + if (new BigDecimal(d).compareTo(r) == 0) { + return d; + } else { + return r.stripTrailingZeros(); + } + } + + private BigDecimal toBigDecimal(Map variables, Object o) { + if (o instanceof Variable) { + o = variables.get(((Variable) o).getName()); + } + if (o instanceof String) { + if (isNumber((String) o)) { + return new BigDecimal((String) o, mc); + } else if (Character.isLetter(((String) o).charAt(0))) { + o = variables.get(o); + } + } + return toBigDecimal(o); + } + + private BigDecimal toBigDecimal(Object o) { + if (o == null) { + return BigDecimal.ZERO; + } else if (o instanceof Boolean) { + return ((Boolean) o) ? BigDecimal.ONE : BigDecimal.ZERO; + } else if (o instanceof BigDecimal) { + return ((BigDecimal) o).round(mc); + } else if (o instanceof BigInteger) { + return new BigDecimal((BigInteger) o, mc); + } else if (o instanceof Number) { + return new BigDecimal(((Number) o).doubleValue(), mc); + } else { + try { + return new BigDecimal(o.toString(), mc); + } catch (NumberFormatException e) { + return new BigDecimal(Double.NaN); + } + } + } + + /** + * Sets the precision for expression evaluation. + * + * @param precision + * The new precision. + * + * @return The expression, allows to chain methods. + */ + public Expression setPrecision(int precision) { + this.mc = new MathContext(precision); + return this; + } + + /** + * Sets the rounding mode for expression evaluation. + * + * @param roundingMode + * The new rounding mode. + * @return The expression, allows to chain methods. + */ + public Expression setRoundingMode(RoundingMode roundingMode) { + this.mc = new MathContext(mc.getPrecision(), roundingMode); + return this; + } + + /** + * Adds an operator to the list of supported operators. + * + * @param operator + * The operator to add. + * @return The previous operator with that name, or null if + * there was none. + */ + public Operator addOperator(Operator operator) { + return operators.put(operator.getOper(), operator); + } + + /** + * Adds a function to the list of supported functions + * + * @param function + * The function to add. + * @return The previous operator with that name, or null if + * there was none. + */ + public Function addFunction(Function function) { + return functions.put(function.getName(), function); + } + + /** + * Sets a constant value. + * + * @param name + * The constant name. + * @param value + * The constant value. + * @return The expression, allows to chain methods. + */ + public Expression addConstant(String name, Object value) { + constants.put(name, value); + return this; + } + + /** + * Get an iterator for this expression, allows iterating over an expression + * token by token. + * + * @return A new iterator instance for this expression. + */ + public Iterator getExpressionTokenizer() { + return new Tokenizer(this.expression); + } + + /** + * Cached access to the RPN notation of this expression, ensures only one + * calculation of the RPN per expression instance. If no cached instance + * exists, a new one will be created and put to the cache. + * + * @return The cached RPN instance. + */ + private List getRPN() { + if (rpn == null) { + rpn = shuntingYard(this.expression); + } + return rpn; + } + + /** + * Get a string representation of the RPN (Reverse Polish Notation) for this + * expression. + * + * @return A string with the RPN representation for this expression. + */ + public String toRPN() { + StringBuilder result = new StringBuilder(); + for (Token st : getRPN()) { + if (result.length() > 0) { + result.append(" "); + } + result.append(st); + } + return result.toString(); + } + +} \ No newline at end of file diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/GlobPathMatcher.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/GlobPathMatcher.java new file mode 100644 index 00000000000..283792b2982 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/GlobPathMatcher.java @@ -0,0 +1,304 @@ +/* + * 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.gogo.runtime; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.StringTokenizer; +import java.util.regex.Pattern; + +/** + * Freely adapted from Spring's AntPathMatcher. + * We don't use the file system's glob PathMatcher + * because it can't detect directories which can't be + * a start of a match. + */ +public class GlobPathMatcher { + + /** Default path separator: "/" */ + public static final String DEFAULT_PATH_SEPARATOR = "/"; + + private String pattern; + private String pathSeparator; + private boolean caseSensitive; + + private String[] pattDirs; + private Pattern[] pattPats; + + /** + * Create a new instance with the {@link #DEFAULT_PATH_SEPARATOR}. + * @param pattern the pattern + */ + public GlobPathMatcher(String pattern) { + this(pattern, DEFAULT_PATH_SEPARATOR, true); + } + + /** + * A convenient, alternative constructor to use with a custom path separator. + * @param pattern the pattern + * @param pathSeparator the path separator to use, must not be {@code null}. + * @param caseSensitive is case sensitive + */ + public GlobPathMatcher(String pattern, String pathSeparator, boolean caseSensitive) { + Objects.requireNonNull(pathSeparator, "'pathSeparator' is required"); + this.pattern = pattern; + this.pathSeparator = pathSeparator; + this.caseSensitive = caseSensitive; + this.pattDirs = tokenizePath(pattern); + this.pattPats = new Pattern[pattDirs.length]; + for (int i = 0; i < pattDirs.length; i++) { + pattPats[i] = createMatcherPattern(pattDirs[i]); + } + } + + + /** + * Actually match the given {@code path} against the given {@code pattern}. + * @param path the path String to test + * @param fullMatch whether a full pattern match is required (else a pattern match + * as far as the given base path goes is sufficient) + * @return {@code true} if the supplied {@code path} matched, {@code false} if it didn't + */ + public boolean matches(String path, boolean fullMatch) { + if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) { + return false; + } + + String[] pathDirs = tokenizePath(path); + + int pattIdxStart = 0; + int pattIdxEnd = pattDirs.length - 1; + int pathIdxStart = 0; + int pathIdxEnd = pathDirs.length - 1; + + // Match all elements up to the first ** + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String pattDir = pattDirs[pattIdxStart]; + if ("**".equals(pattDir)) { + break; + } + if (!matchStrings(pattIdxStart, pathDirs[pathIdxStart])) { + return false; + } + pattIdxStart++; + pathIdxStart++; + } + + if (pathIdxStart > pathIdxEnd) { + // Path is exhausted, only match if rest of pattern is * or **'s + if (pattIdxStart > pattIdxEnd) { + return (pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator)); + } + if (!fullMatch) { + return true; + } + if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) { + return true; + } + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } + else if (pattIdxStart > pattIdxEnd) { + // String not exhausted, but pattern is. Failure. + return false; + } + else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) { + // Path start definitely matches due to "**" part in pattern. + return true; + } + + // up to last '**' + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String pattDir = pattDirs[pattIdxEnd]; + if (pattDir.equals("**")) { + break; + } + if (!matchStrings(pattIdxEnd, pathDirs[pathIdxEnd])) { + return false; + } + pattIdxEnd--; + pathIdxEnd--; + } + if (pathIdxStart > pathIdxEnd) { + // String is exhausted + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } + + while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) { + int patIdxTmp = -1; + for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) { + if (pattDirs[i].equals("**")) { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == pattIdxStart + 1) { + // '**/**' situation, so skip one + pattIdxStart++; + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + int patLength = (patIdxTmp - pattIdxStart - 1); + int strLength = (pathIdxEnd - pathIdxStart + 1); + int foundIdx = -1; + + strLoop: + for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + String subStr = pathDirs[pathIdxStart + i + j]; + if (!matchStrings(pattIdxStart + j + 1, subStr)) { + continue strLoop; + } + } + foundIdx = pathIdxStart + i; + break; + } + + if (foundIdx == -1) { + return false; + } + + pattIdxStart = patIdxTmp; + pathIdxStart = foundIdx + patLength; + } + + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + + return true; + } + + /** + * Tokenize the given path String into parts, based on this matcher's settings. + * @param path the path to tokenize + * @return the tokenized path parts + */ + private String[] tokenizePath(String path) { + StringTokenizer st = new StringTokenizer(path, pathSeparator); + List tokens = new ArrayList<>(); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (token.length() > 0) { + tokens.add(token); + } + } + return tokens.toArray(new String[tokens.size()]); + } + + private boolean matchStrings(int pattIdx, String str) { + return pattPats[pattIdx].matcher(str).matches(); + } + + private Pattern createMatcherPattern(String pattern) { + StringBuilder sb = new StringBuilder(pattern.length()); + int inGroup = 0; + int inClass = 0; + int firstIndexInClass = -1; + char[] arr = pattern.toCharArray(); + for (int i = 0; i < arr.length; i++) { + char ch = arr[i]; + switch (ch) { + case '\\': + if (++i >= arr.length) { + sb.append('\\'); + } else { + char next = arr[i]; + switch (next) { + case ',': + // escape not needed + break; + case 'Q': + case 'E': + // extra escape needed + sb.append("\\\\"); + break; + default: + sb.append('\\'); + break; + } + sb.append(next); + } + break; + case '*': + sb.append(inClass == 0 ? ".*" : "*"); + break; + case '?': + sb.append(inClass == 0 ? '.' : '?'); + break; + case '[': + inClass++; + firstIndexInClass = i + 1; + sb.append('['); + break; + case ']': + inClass--; + sb.append(']'); + break; + case '.': + case '(': + case ')': + case '+': + case '|': + case '^': + case '$': + case '@': + case '%': + if (inClass == 0 || (firstIndexInClass == i && ch == '^')) { + sb.append('\\'); + } + sb.append(ch); + break; + case '!': + sb.append(firstIndexInClass == i ? '^' : '!'); + break; + case '{': + inGroup++; + sb.append('('); + break; + case '}': + inGroup--; + sb.append(')'); + break; + case ',': + sb.append(inGroup > 0 ? '|' : ','); + break; + default: + sb.append(ch); + } + } + return (caseSensitive ? Pattern.compile(sb.toString()) : + Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE)); + } + + +} \ No newline at end of file diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Parser.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Parser.java new file mode 100644 index 00000000000..8e93a2217a2 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Parser.java @@ -0,0 +1,580 @@ +/* + * 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.gogo.runtime; + +import java.util.*; +import java.util.regex.Pattern; + + +public class Parser +{ + + public static abstract class Executable extends Token + { + public Executable(Token cs) + { + super(cs); + } + } + + public static class Operator extends Executable + { + public Operator(Token cs) { + super(cs); + } + } + + public static class Statement extends Executable + { + private final List tokens; + private final List redirections; + + public Statement(Token cs, List tokens, List redirections) + { + super(cs); + this.tokens = tokens; + this.redirections = redirections; + } + + public List tokens() + { + return tokens; + } + + public List redirections() { + return redirections; + } + } + + /** + * pipe1 ; pipe2 ; ... + */ + public static class Program extends Token + { + private final List tokens; + + public Program(Token cs, List tokens) + { + super(cs); + this.tokens = tokens; + } + + public List tokens() + { + return tokens; + } + } + + /** + * token1 | token2 | ... + */ + public static class Pipeline extends Executable + { + private final List tokens; + + public Pipeline(Token cs, List tokens) + { + super(cs); + this.tokens = tokens; + } + + public List tokens() + { + return tokens; + } + + } + + /** + * ( program ) + */ + public static class Sequence extends Executable + { + private final Program program; + + public Sequence(Token cs, Program program) + { + super(cs); + this.program = program; + } + + public Program program() + { + return program; + } + } + + /** + * { program } + */ + public static class Closure extends Token + { + private final Program program; + + public Closure(Token cs, Program program) + { + super(cs); + this.program = program; + } + + public Program program() + { + return program; + } + } + + /** + * [ a b ...] + * [ k1=v1 k2=v2 ...] + */ + public static class Array extends Token + { + private final List list; + private final Map map; + + public Array(Token cs, List list, Map map) + { + super(cs); + assert list != null ^ map != null; + this.list = list; + this.map = map; + } + + public List list() + { + return list; + } + + public Map map() + { + return map; + } + } + + protected final Tokenizer tz; + protected final LinkedList stack = new LinkedList<>(); + protected final List tokens = new ArrayList<>(); + protected final List statements = new ArrayList<>(); + + public Parser(CharSequence line) + { + this.tz = new Tokenizer(line); + } + + public List tokens() { + return Collections.unmodifiableList(tokens); + } + + public List statements() { + Collections.sort(statements, new Comparator() { + @Override + public int compare(Statement o1, Statement o2) { + return Integer.compare(o1.start(), o2.start()); + } + + }); + return Collections.unmodifiableList(statements); + } + + public Program program() + { + List tokens = new ArrayList<>(); + List pipes = null; + int start = tz.index - 1; + while (true) + { + Statement ex; + Token t = next(); + if (t == null) + { + if (pipes != null) + { + throw new EOFError(tz.line, tz.column, "unexpected EOT while looking for a statement after |", getMissing("pipe"), "0"); + } + else + { + return new Program(whole(tokens, start), tokens); + } + } + if (Token.eq("}", t) || Token.eq(")", t) || Token.eq("]", t)) + { + if (pipes != null) + { + throw new EOFError(t.line, t.column, "unexpected token '" + t + "' while looking for a statement after |", getMissing("pipe"), "0"); + } + else if (stack.isEmpty()) + { + throw new SyntaxError(t.line, t.column, "unexpected token '" + t + "'"); + } + else + { + push(t); + return new Program(whole(tokens, start), tokens); + } + } + else + { + push(t); + ex = statement(); + } + t = next(); + if (t == null || Token.eq(";", t) || Token.eq("\n", t) || Token.eq("&", t) || Token.eq("&&", t) || Token.eq("||", t)) + { + if (pipes != null) + { + pipes.add(ex); + tokens.add(new Pipeline(whole(pipes, start), pipes)); + pipes = null; + } + else + { + tokens.add(ex); + } + if (t == null) + { + return new Program(whole(tokens, start), tokens); + } + else { + tokens.add(new Operator(t)); + } + } + else if (Token.eq("|", t) || Token.eq("|&", t)) + { + if (pipes == null) + { + pipes = new ArrayList<>(); + } + pipes.add(ex); + pipes.add(new Operator(t)); + } + else + { + if (pipes != null) + { + pipes.add(ex); + tokens.add(new Pipeline(whole(pipes, start), pipes)); + pipes = null; + } + else + { + tokens.add(ex); + } + push(t); + } + } + } + + protected void push(Token t) { + tz.push(t); + } + + protected Token next() { + boolean pushed = tz.pushed != null; + Token token = tz.next(); + if (!pushed && token != null) { + tokens.add(token); + } + return token; + } + + public Sequence sequence() + { + Token start = start("(", "sequence"); + expectNotNull(); + Program program = program(); + Token end = end(")"); + return new Sequence(whole(start, end), program); + } + + public Closure closure() + { + Token start = start("{", "closure"); + expectNotNull(); + Program program = program(); + Token end = end("}"); + return new Closure(whole(start, end), program); + } + + private static final Pattern redirNoArg = Pattern.compile("[0-9]?>&[0-9-]|[0-9-]?<&[0-9-]"); + private static final Pattern redirArg = Pattern.compile("[0-9&]?>|[0-9]?>>|[0-9]?<|[0-9]?<>|<<<"); + private static final Pattern redirHereDoc = Pattern.compile("<<-?"); + + public Statement statement() + { + List tokens = new ArrayList<>(); + List redirs = new ArrayList<>(); + boolean needRedirArg = false; + int start = tz.index; + while (true) + { + Token t = next(); + if (t == null + || Token.eq("\n", t) + || Token.eq(";", t) + || Token.eq("&", t) + || Token.eq("&&", t) + || Token.eq("||", t) + || Token.eq("|", t) + || Token.eq("|&", t) + || Token.eq("}", t) + || Token.eq(")", t) + || Token.eq("]", t)) + { + if (needRedirArg) + { + throw new EOFError(tz.line, tz.column, "Expected file name for redirection", "redir", "foo"); + } + push(t); + break; + } + if (Token.eq("{", t)) + { + push(t); + tokens.add(closure()); + } + else if (Token.eq("[", t)) + { + push(t); + tokens.add(array()); + } + else if (Token.eq("(", t)) + { + push(t); + tokens.add(sequence()); + } + else if (needRedirArg) + { + redirs.add(t); + needRedirArg = false; + } + else if (redirNoArg.matcher(t).matches()) + { + redirs.add(t); + } + else if (redirArg.matcher(t).matches()) + { + redirs.add(t); + needRedirArg = true; + } + else if (redirHereDoc.matcher(t).matches()) + { + redirs.add(t); + redirs.add(tz.readHereDoc(t.charAt(t.length() - 1) == '-')); + } + else + { + tokens.add(t); + } + } + Statement statement = new Statement(whole(tokens, start), tokens, redirs); + statements.add(statement); + return statement; + } + + public Array array() + { + Token start = start("[", "array"); + Boolean isMap = null; + List list = new ArrayList<>(); + Map map = new LinkedHashMap<>(); + while (true) + { + Token key = next(); + if (key == null) + { + throw new EOFError(tz.line, tz.column, "unexpected EOT", getMissing(), "]"); + } + if (Token.eq("]", key)) + { + push(key); + break; + } + if (Token.eq("\n", key)) + { + continue; + } + if (Token.eq("{", key) || Token.eq(";", key) || Token.eq("&", key) || Token.eq("&&", key) || Token.eq("||", key) + || Token.eq("|", key) || Token.eq("|&", key) || Token.eq(")", key) || Token.eq("}", key) || Token.eq("=", key)) + { + throw new SyntaxError(key.line(), key.column(), "unexpected token '" + key + "' while looking for array key"); + } + if (Token.eq("(", key)) + { + push(key); + key = sequence(); + } + if (Token.eq("[", key)) + { + push(key); + key = array(); + } + if (isMap == null) + { + Token n = next(); + if (n == null) + { + throw new EOFError(tz.line, tz.column, "unexpected EOF while looking for array token", getMissing(), "]"); + } + isMap = Token.eq("=", n); + push(n); + } + if (isMap) + { + expect("="); + Token val = next(); + if (val == null) + { + throw new EOFError(tz.line, tz.column, "unexpected EOF while looking for array value", getMissing(), "0"); + } + else if (Token.eq(";", val) || Token.eq("&", val) || Token.eq("&&", val) || Token.eq("||", val) || Token.eq("|", val) || Token.eq("|&", val) + || Token.eq(")", key) || Token.eq("}", key) || Token.eq("=", key)) + { + throw new SyntaxError(key.line(), key.column(), "unexpected token '" + key + "' while looking for array value"); + } + else if (Token.eq("[", val)) + { + push(val); + val = array(); + } + else if (Token.eq("(", val)) + { + push(val); + val = sequence(); + } + else if (Token.eq("{", val)) + { + push(val); + val = closure(); + } + map.put(key, val); + } + else + { + list.add(key); + } + } + Token end = end("]"); + if (isMap == null || !isMap) + { + return new Array(whole(start, end), list, null); + } + else + { + return new Array(whole(start, end), null, map); + } + } + + protected void expectNotNull() + { + Token t = next(); + if (t == null) + { + throw new EOFError(tz.line, tz.column, + "unexpected EOT", + getMissing(), "0"); + } + push(t); + } + + private String getMissing() { + return getMissing(null); + } + + private String getMissing(String additional) { + StringBuilder sb = new StringBuilder(); + LinkedList stack = this.stack; + if (additional != null) { + stack = new LinkedList<>(stack); + stack.addLast(additional); + } + String last = null; + int nb = 0; + for (String cur : stack) { + if (last == null) { + last = cur; + nb = 1; + } else if (last.equals(cur)) { + nb++; + } else { + if (sb.length() > 0) { + sb.append(" "); + } + sb.append(last); + if (nb > 1) { + sb.append("(").append(nb).append(")"); + } + last = cur; + nb = 1; + } + } + if (sb.length() > 0) { + sb.append(" "); + } + sb.append(last); + if (nb > 1) { + sb.append("(").append(nb).append(")"); + } + return sb.toString(); + } + + protected Token start(String str, String missing) { + stack.addLast(missing); + return expect(str); + } + + protected Token end(String str) { + Token t = expect(str); + stack.removeLast(); + return t; + } + + protected Token expect(String str) + { + Token start = next(); + if (start == null) + { + throw new EOFError(tz.line, tz.column, + "unexpected EOT looking for '" + str + "", + getMissing(), str); + } + if (!Token.eq(str, start)) + { + throw new SyntaxError(start.line, start.column, "expected '" + str + "' but got '" + start.toString() + "'"); + } + return start; + } + + protected Token whole(List tokens, int index) + { + if (tokens.isEmpty()) + { + index = Math.min(index, tz.text().length()); + return tz.text().subSequence(index, index); + } + Token b = tokens.get(0); + Token e = tokens.get(tokens.size() - 1); + return whole(b, e); + } + + protected Token whole(Token b, Token e) + { + return tz.text.subSequence(b.start - tz.text.start, e.start + e.length() - tz.text.start); + } + +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Pipe.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Pipe.java new file mode 100644 index 00000000000..79120644431 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Pipe.java @@ -0,0 +1,671 @@ +/* + * 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.gogo.runtime; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.PrintStream; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; +import java.nio.channels.Channel; +import java.nio.channels.Channels; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.felix.service.command.Job; +import org.apache.felix.service.command.Job.Status; +import org.apache.felix.service.command.Process; +import org.apache.felix.gogo.runtime.CommandSessionImpl.JobImpl; +import org.apache.felix.gogo.runtime.Parser.Statement; +import org.apache.felix.gogo.runtime.Pipe.Result; +import org.apache.felix.service.command.Converter; +import org.apache.felix.service.threadio.ThreadIO; + +public class Pipe implements Callable, Process +{ + private static final ThreadLocal CURRENT = new ThreadLocal<>(); + + public static class Result implements org.apache.felix.service.command.Result { + public final Object result; + public final Exception exception; + public final int error; + + public Result(Object result) { + this.result = result; + this.exception = null; + this.error = 0; + } + + public Result(Exception exception) { + this.result = null; + this.exception = exception; + this.error = 1; + } + + public Result(int error) { + this.result = null; + this.exception = null; + this.error = error; + } + + public boolean isSuccess() { + return exception == null && error == 0; + } + + @Override + public Object result() { + return result; + } + + @Override + public Exception exception() { + return exception; + } + + @Override + public int error() { + return error; + } + } + + public static Pipe getCurrentPipe() { + return CURRENT.get(); + } + + private static Pipe setCurrentPipe(Pipe pipe) { + Pipe previous = CURRENT.get(); + CURRENT.set(pipe); + return previous; + } + + final Closure closure; + final Job job; + final Statement statement; + final Channel[] streams; + final boolean[] toclose; + final boolean endOfPipe; + int error; + + InputStream in; + PrintStream out; + PrintStream err; + + public Pipe(Closure closure, JobImpl job, Statement statement, Channel[] streams, boolean[] toclose, boolean endOfPipe) + { + this.closure = closure; + this.job = job; + this.statement = statement; + this.streams = streams; + this.toclose = toclose; + this.endOfPipe = endOfPipe; + } + + public String toString() + { + return "pipe<" + statement + "> out=" + streams[1]; + } + + private static final int READ = 1; + private static final int WRITE = 2; + + private void setStream(Channel ch, int fd, int readWrite) { + if ((readWrite & (READ | WRITE)) == 0) { + throw new IllegalArgumentException("Should specify READ and/or WRITE"); + } + if ((readWrite & READ) != 0 && !(ch instanceof ReadableByteChannel)) { + throw new IllegalArgumentException("Channel is not readable"); + } + if ((readWrite & WRITE) != 0 && !(ch instanceof WritableByteChannel)) { + throw new IllegalArgumentException("Channel is not writable"); + } + if (fd == 0 && (readWrite & READ) == 0) { + throw new IllegalArgumentException("Stdin is not readable"); + } + if (fd == 1 && (readWrite & WRITE) == 0) { + throw new IllegalArgumentException("Stdout is not writable"); + } + if (fd == 2 && (readWrite & WRITE) == 0) { + throw new IllegalArgumentException("Stderr is not writable"); + } + if (streams[fd] != null && (readWrite & READ) != 0 && (readWrite & WRITE) != 0) { + throw new IllegalArgumentException("Can not do multios with read/write streams"); + } + // If channel is inherited (for example standard input / output), replace it + if (streams[fd] != null && !toclose[fd]) { + streams[fd] = ch; + toclose[fd] = true; + } + // Else do multios + else { + MultiChannel mrbc; + // If the channel is already multios + if (streams[fd] instanceof MultiChannel) { + mrbc = (MultiChannel) streams[fd]; + } + // Else create a multios channel + else { + mrbc = new MultiChannel(); + mrbc.addChannel(streams[fd], toclose[fd]); + streams[fd] = mrbc; + toclose[fd] = true; + } + mrbc.addChannel(ch, true); + } + } + + @Override + public InputStream in() { + return in; + } + + @Override + public PrintStream out() { + return out; + } + + @Override + public PrintStream err() { + return err; + } + + @Override + public Job job() { + return job; + } + + public boolean isTty(int fd) { + // TODO: this assumes that the session is always created with input/output tty streams + if (fd < 0 || fd > streams.length) { + return false; + } + return streams[fd] != null && !toclose[fd]; + } + + public void error(int error) { + this.error = error; + } + + @Override + public Result call() { + Thread thread = Thread.currentThread(); + String name = thread.getName(); + try { + thread.setName("pipe-" + statement); + return doCall(); + } finally { + thread.setName(name); + } + } + + private Result doCall() + { + // The errChannel will be used to print errors to the error stream + // Before the command is actually executed (i.e. during the initialization, + // including the redirection processing), it will be the original error stream. + // This value may be modified by redirections and the redirected error stream + // will be effective just before actually running the command. + WritableByteChannel errChannel = (WritableByteChannel) streams[2]; + + ThreadIO threadIo = closure.session().threadIO(); + + try + { + List tokens = statement.redirections(); + for (int i = 0; i < tokens.size(); i++) + { + Token t = tokens.get(i); + Matcher m; + if ((m = Pattern.compile("(?:([0-9])?|(&)?)>(>)?").matcher(t)).matches()) + { + int fd; + if (m.group(1) != null) + { + fd = Integer.parseInt(m.group(1)); + } + else if (m.group(2) != null) + { + fd = -1; // both 1 and 2 + } + else + { + fd = 1; + } + boolean append = m.group(3) != null; + Set options = new HashSet<>(); + options.add(StandardOpenOption.WRITE); + options.add(StandardOpenOption.CREATE); + options.add(append ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING); + Token tok = tokens.get(++i); + Object val = Expander.expand(tok, closure); + for (Path p : toPaths(val)) + { + p = closure.session().redirect(p, WRITE); + Channel ch = Files.newByteChannel(p, options); + if (fd >= 0) + { + setStream(ch, fd, WRITE); + } + else + { + setStream(ch, 1, WRITE); + setStream(ch, 2, WRITE); + } + } + } + else if ((m = Pattern.compile("([0-9])?>&([0-9])").matcher(t)).matches()) + { + int fd0 = 1; + if (m.group(1) != null) + { + fd0 = Integer.parseInt(m.group(1)); + } + int fd1 = Integer.parseInt(m.group(2)); + if (streams[fd0] != null && toclose[fd0]) + { + streams[fd0].close(); + } + // If the stream has to be closed, close it when both streams are closed + if (toclose[fd1]) + { + Channel channel = streams[fd1]; + AtomicInteger references = new AtomicInteger(); + streams[fd0] = new RefByteChannel(channel, references); + streams[fd1] = new RefByteChannel(channel, references); + toclose[fd0] = true; + } + else + { + streams[fd0] = streams[fd1]; + toclose[fd0] = false; + } + } + else if ((m = Pattern.compile("([0-9])?<(>)?").matcher(t)).matches()) + { + int fd = 0; + if (m.group(1) != null) + { + fd = Integer.parseInt(m.group(1)); + } + boolean output = m.group(2) != null; + Set options = new HashSet<>(); + options.add(StandardOpenOption.READ); + if (output) + { + options.add(StandardOpenOption.WRITE); + options.add(StandardOpenOption.CREATE); + } + Token tok = tokens.get(++i); + Object val = Expander.expand(tok, closure); + for (Path p : toPaths(val)) + { + p = closure.session().redirect(p, READ + (output ? WRITE : 0)); + Channel ch = Files.newByteChannel(p, options); + setStream(ch, fd, READ + (output ? WRITE : 0)); + } + } + else if ((m = Pattern.compile("<<-?").matcher(t)).matches()) + { + final Token hereDoc = tokens.get(++i); + final boolean stripLeadingTabs = t.charAt(t.length() - 1) == '-'; + InputStream doc = new InputStream() + { + final byte[] bytes = hereDoc.toString().getBytes(); + int index = 0; + boolean nl = true; + @Override + public int read() { + if (nl && stripLeadingTabs) + { + while (index < bytes.length && bytes[index] == '\t') + { + index++; + } + } + if (index < bytes.length) + { + int ch = bytes[index++]; + nl = ch == '\n'; + return ch; + } + return -1; + } + }; + Channel ch = Channels.newChannel(doc); + setStream(ch, 0, READ); + } + else if (Token.eq("<<<", t)) + { + Token word = tokens.get(++i); + Object val = Expander.expand("\"" + word + "\"", closure); + String str = val != null ? String.valueOf(val) : ""; + Channel ch = Channels.newChannel(new ByteArrayInputStream(str.getBytes())); + setStream(ch, 0, READ); + } + } + + for (int i = 0; i < streams.length; i++) { + streams[i] = wrap(streams[i]); + } + + // Create streams + in = Channels.newInputStream((ReadableByteChannel) streams[0]); + out = new PrintStream(Channels.newOutputStream((WritableByteChannel) streams[1]), true); + err = new PrintStream(Channels.newOutputStream((WritableByteChannel) streams[2]), true); + // Change the error stream to the redirected one, now that + // the command is about to be executed. + errChannel = (WritableByteChannel) streams[2]; + + if (threadIo != null) + { + threadIo.setStreams(in, out, err); + } + + Pipe previous = setCurrentPipe(this); + try + { + Object result; + // Very special case for empty statements with redirection + if (statement.tokens().isEmpty() && toclose[0]) + { + ByteBuffer bb = ByteBuffer.allocate(1024); + while (((ReadableByteChannel) streams[0]).read(bb) >= 0 || bb.position() != 0) + { + bb.flip(); + ((WritableByteChannel) streams[1]).write(bb); + bb.compact(); + } + result = null; + } + else + { + result = closure.execute(statement); + } + // If an error has been set + if (error != 0) + { + return new Result(error); + } + // We don't print the result if we're at the end of the pipe + if (result != null && !endOfPipe && !Boolean.FALSE.equals(closure.session().get(".FormatPipe"))) + { + out.println(closure.session().format(result, Converter.INSPECT)); + } + return new Result(result); + + } + finally + { + setCurrentPipe(previous); + } + } + catch (Exception e) + { + if (!endOfPipe) + { + String msg = "gogo: " + e.getClass().getSimpleName() + ": " + e.getMessage() + "\n"; + try + { + errChannel.write(ByteBuffer.wrap(msg.getBytes())); + } + catch (IOException ioe) + { + e.addSuppressed(ioe); + } + } + return new Result(e); + } + finally + { + if (out != null) + { + out.flush(); + } + if (err != null) + { + err.flush(); + } + if (threadIo != null) + { + threadIo.close(); + } + + try + { + for (int i = 0; i < 10; i++) + { + if (toclose[i] && streams[i] != null) + { + streams[i].close(); + } + } + } + catch (Exception e) + { + e.printStackTrace(); + } + } + } + + private List toPaths(Object val) throws IOException + { + List paths = new ArrayList<>(); + if (val instanceof Collection) + { + for (Object o : (Collection) val) + { + Path p = toPath(o); + if (p != null) + { + paths.add(p); + } + } + } + else if (val != null) + { + Path p = toPath(val); + if (p != null) + { + paths.add(p); + } + } + if (paths.isEmpty()) + { + throw new IOException("no such file or directory"); + } + return paths; + } + + private Path toPath(Object o) + { + if (o instanceof Path) + { + return (Path) o; + } + else if (o instanceof File) + { + return ((File) o).toPath(); + } + else if (o instanceof URI) + { + return Paths.get((URI) o); + } + else if (o != null) + { + String s = String.valueOf(o); + if (!s.isEmpty()) + { + return Paths.get(String.valueOf(o)); + } + } + return null; + } + + private Channel wrap(Channel channel) + { + if (channel == null) + { + return null; + } + if (channel instanceof MultiChannel) + { + return channel; + } + MultiChannel mch = new MultiChannel(); + mch.addChannel(channel, true); + return mch; + } + + private class RefByteChannel implements ByteChannel { + + private final Channel channel; + private final AtomicInteger references; + private final AtomicBoolean closed = new AtomicBoolean(false); + + public RefByteChannel(Channel channel, AtomicInteger references) { + this.channel = channel; + this.references = references; + references.incrementAndGet(); + } + + @Override + public int read(ByteBuffer dst) throws IOException { + ensureOpen(); + return ((ReadableByteChannel) channel).read(dst); + } + + @Override + public int write(ByteBuffer src) throws IOException { + ensureOpen(); + return ((WritableByteChannel) channel).write(src); + } + + @Override + public boolean isOpen() { + return !closed.get(); + } + + private void ensureOpen() throws ClosedChannelException { + if (closed.get()) throw new ClosedChannelException(); + } + + @Override + public void close() throws IOException { + if (closed.compareAndSet(false, true)) { + if (references.decrementAndGet() == 0) { + channel.close(); + } + } + } + + } + + private class MultiChannel implements ByteChannel { + protected final List channels = new ArrayList<>(); + protected final List toClose = new ArrayList<>(); + protected final AtomicBoolean opened = new AtomicBoolean(true); + int index = 0; + + public void addChannel(Channel channel, boolean toclose) { + channels.add(channel); + if (toclose) { + toClose.add(channel); + } + } + + public boolean isOpen() { + return opened.get(); + } + + public void close() throws IOException { + if (opened.compareAndSet(true, false)) { + for (Channel channel : toClose) { + channel.close(); + } + } + } + + public int read(ByteBuffer dst) throws IOException { + int nbRead = -1; + while (nbRead < 0 && index < channels.size()) { + Channel ch = channels.get(index); + checkSuspend(ch); + nbRead = ((ReadableByteChannel) ch).read(dst); + if (nbRead < 0) { + index++; + } else { + break; + } + } + return nbRead; + } + + public int write(ByteBuffer src) throws IOException { + int pos = src.position(); + for (Channel ch : channels) { + checkSuspend(ch); + src.position(pos); + while (src.hasRemaining()) { + ((WritableByteChannel) ch).write(src); + } + } + return src.position() - pos; + } + + private void checkSuspend(Channel ch) throws IOException { + Channel[] sch = closure.session().channels; + if (ch == sch[0] || ch == sch[1] || ch == sch[2]) { + synchronized (job) { + if (job.status() == Status.Background) { + // TODO: Send SIGTIN / SIGTOU + job.suspend(); + } + } + } + synchronized (job) { + while (job.status() == Status.Suspended) { + try { + job.wait(); + } catch (InterruptedException e) { + throw (IOException) new InterruptedIOException().initCause(e); + } + } + } + } + } + +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Reflective.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Reflective.java new file mode 100644 index 00000000000..3546a9e5301 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Reflective.java @@ -0,0 +1,642 @@ +/* + * 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.gogo.runtime; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Parameter; + +public final class Reflective +{ + public final static Object NO_MATCH = new Object(); + public final static String MAIN = "_main"; + public final static Set KEYWORDS = new HashSet<>( + Arrays.asList("abstract", "continue", "for", "new", "switch", + "assert", "default", "goto", "package", "synchronized", "boolean", "do", + "if", "private", "this", "break", "double", "implements", "protected", + "throw", "byte", "else", "import", "public", "throws", "case", "enum", + "instanceof", "return", "transient", "catch", "extends", "int", "short", + "try", "char", "final", "interface", "static", "void", "class", + "finally", "long", "strictfp", "volatile", "const", "float", "native", + "super", "while")); + + /** + * invokes the named method on the given target using the supplied args, + * which are converted if necessary. + * @param session the session + * @param target the target + * @param name the name + * @param args the args + * @return the result of the invoked method + * @throws Exception on exception + */ + public static Object invoke(CommandSession session, Object target, String name, + List args) throws Exception + { + Method[] methods = target.getClass().getMethods(); + name = name.toLowerCase(Locale.ENGLISH); + + String org = name; + String get = "get" + name; + String is = "is" + name; + String set = "set" + name; + + if (KEYWORDS.contains(name)) + { + name = "_" + name; + } + + if (target instanceof Class) + { + Method[] staticMethods = ((Class) target).getMethods(); + for (Method m : staticMethods) + { + String mname = m.getName().toLowerCase(Locale.ENGLISH); + if (mname.equals(name) || mname.equals(get) || mname.equals(set) + || mname.equals(is) || mname.equals(MAIN)) + { + methods = staticMethods; + break; + } + } + } + + Method bestMethod = null; + Object[] bestArgs = null; + int lowestMatch = Integer.MAX_VALUE; + ArrayList[]> possibleTypes = new ArrayList<>(); + + for (Method m : methods) + { + String mname = m.getName().toLowerCase(Locale.ENGLISH); + if (mname.equals(name) || mname.equals(get) || mname.equals(set) + || mname.equals(is) || mname.equals(MAIN)) + { + Class[] types = m.getParameterTypes(); + ArrayList xargs = new ArrayList<>(args); + + // pass command name as argv[0] to main, so it can handle + // multiple commands + if (mname.equals(MAIN)) + { + xargs.add(0, org); + } + + Object[] parms = new Object[types.length]; + int match = coerce(session, target, m, types, parms, xargs); + + if (match < 0) + { + // coerce failed + possibleTypes.add(types); + } + else + { + if (match < lowestMatch) + { + lowestMatch = match; + bestMethod = m; + bestArgs = parms; + } + + if (match == 0) + break; // can't get better score + } + } + } + + if (bestMethod != null) + { + bestMethod.setAccessible(true); + try + { + return bestMethod.invoke(target, bestArgs); + } + catch (InvocationTargetException e) + { + Throwable cause = e.getCause(); + if (cause instanceof Exception) + { + throw (Exception) cause; + } + throw e; + } + } + else + { + if (args.isEmpty()) + { + Field[] fields; + if (target instanceof Class) + { + fields = ((Class) target).getFields(); + } + else + { + fields = target.getClass().getFields(); + } + for (Field f : fields) + { + String mname = f.getName().toLowerCase(Locale.ENGLISH); + if (mname.equals(name)) + { + return f.get(target); + } + } + } + ArrayList list = new ArrayList<>(); + for (Class[] types : possibleTypes) + { + StringBuilder buf = new StringBuilder(); + buf.append('('); + for (Class type : types) + { + if (buf.length() > 1) + { + buf.append(", "); + } + buf.append(type.getSimpleName()); + } + buf.append(')'); + list.add(buf.toString()); + } + + StringBuilder params = new StringBuilder(); + for (Object arg : args) + { + if (params.length() > 1) + { + params.append(", "); + } + params.append(arg == null ? "null" : arg.getClass().getSimpleName()); + } + + throw new IllegalArgumentException(String.format( + "Cannot coerce %s(%s) to any of %s", name, params, list)); + } + } + + /** + * transform name/value parameters into ordered argument list. + * params: --param2, value2, --flag1, arg3 + * args: true, value2, arg3 + * @return new ordered list of args. + */ + private static List transformParameters(Method method, List in) + { + Annotation[][] pas = method.getParameterAnnotations(); + ArrayList out = new ArrayList<>(); + ArrayList parms = new ArrayList<>(in); + + for (Annotation as[] : pas) + { + for (Annotation a : as) + { + if (a instanceof Parameter) + { + int i = -1; + Parameter p = (Parameter) a; + for (String name : p.names()) + { + i = parms.indexOf(name); + if (i >= 0) + break; + } + + if (i >= 0) + { + // parameter present + parms.remove(i); + Object value = p.presentValue(); + if (Parameter.UNSPECIFIED.equals(value)) + { + if (i >= parms.size()) + return null; // missing parameter, so try other methods + value = parms.remove(i); + } + out.add(value); + } + else + { + out.add(p.absentValue()); + } + + } + } + } + + out.addAll(parms); + + return out; + } + + /** + * Complex routein to convert the arguments given from the command line to + * the arguments of the method call. First, an attempt is made to convert + * each argument. If this fails, a check is made to see if varargs can be + * applied. This happens when the last method argument is an array. + * @return -1 if arguments can't be coerced; 0 if no coercion was necessary; + * > 0 if coercion was needed. + */ + private static int coerce(CommandSession session, Object target, Method m, + Class types[], Object out[], List in) + { + List cnvIn = new ArrayList<>(); + List cnvIn2 = new ArrayList<>(); + int different = 0; + for (Object obj : in) + { + if (obj instanceof Token) + { + Object s1 = Closure.eval(obj); + Object s2 = obj.toString(); + cnvIn.add(s1); + cnvIn2.add(s2); + different += s2.equals(s1) ? 0 : 1; + } else + { + cnvIn.add(obj); + cnvIn2.add(obj); + } + } + + cnvIn = transformParameters(m, cnvIn); + if (different != 0) + { + cnvIn2 = transformParameters(m, cnvIn2); + } + if (cnvIn == null || cnvIn2 == null) + { + // missing parameter argument? + return -1; + } + + int res; + + res = docoerce(session, target, m, types, out, cnvIn); + // Without conversion + if (different != 0 && res < 0) + { + res = docoerce(session, target, m, types, out, cnvIn2); + } + else if (different != 0 && res > 0) + { + int res2; + Object[] out2 = out.clone(); + res2 = docoerce(session, target, m, types, out2, cnvIn2) + different * 2; + if (res >= 0 && res2 <= res) + { + res = res2; + System.arraycopy(out2, 0, out, 0, out.length); + } + } + // Check if the command takes a session + if (res < 0 && (types.length > 0) && types[0].isInterface() + && types[0].isAssignableFrom(session.getClass())) + { + cnvIn.add(0, session); + res = docoerce(session, target, m, types, out, cnvIn); + if (different != 0 && res < 0) + { + cnvIn2.add(0, session); + res = docoerce(session, target, m, types, out, cnvIn2); + } + else if (different != 0 && res > 0) + { + int res2; + cnvIn2.add(0, session); + Object[] out2 = out.clone(); + res2 = docoerce(session, target, m, types, out2, cnvIn2) + different * 2; + if (res >= 0 && res2 <= res) + { + res = res2; + System.arraycopy(out2, 0, out, 0, out.length); + } + } + } + return res; + } + + private static int docoerce(CommandSession session, Object target, Method m, + Class types[], Object out[], List in) + { + int[] convert = { 0 }; + + int i = 0; + while (i < out.length) + { + out[i] = null; + + // Try to convert one argument + if (in.size() == 0 || i == types.length - 1 && types[i].isArray() && in.size() > 1) + { + out[i] = NO_MATCH; + } + else + { + out[i] = coerce(session, types[i], in.get(0), convert); + + if (out[i] == null && types[i].isArray() && in.size() > 0) + { + // don't coerce null to array FELIX-2432 + out[i] = NO_MATCH; + } + + if (out[i] != NO_MATCH) + { + in.remove(0); + } + } + + if (out[i] == NO_MATCH) + { + // No match, check for varargs + if (types[i].isArray() && (i == types.length - 1)) + { + // Try to parse the remaining arguments in an array + Class ctype = types[i].getComponentType(); + int asize = in.size(); + Object array = Array.newInstance(ctype, asize); + int n = i; + while (in.size() > 0) + { + Object t = coerce(session, ctype, in.remove(0), convert); + if (t == NO_MATCH) + { + return -1; + } + Array.set(array, i - n, t); + i++; + } + out[n] = array; + + /* + * 1. prefer f() to f(T[]) with empty array + * 2. prefer f(T) to f(T[1]) + * 3. prefer f(T) to f(Object[1]) even if there is a conversion cost for T + * + * 1 & 2 require to add 1 to conversion cost, but 3 also needs to match + * the conversion cost for T. + */ + return convert[0] + 1 + (asize * 2); + } + return -1; + } + i++; + } + + if (in.isEmpty()) + return convert[0]; + return -1; + } + + /** + * converts given argument to specified type and increments convert[0] if any conversion was needed. + * @param session the session + * @param type the type + * @param arg the arg + * @param convert convert[0] is incremented according to the conversion needed, + * to allow the "best" conversion to be determined. + * @return converted arg or NO_MATCH if no conversion possible. + */ + public static Object coerce(CommandSession session, Class type, final Object arg, + int[] convert) + { + if (arg == null) + { + return null; + } + + if (type.isAssignableFrom(arg.getClass())) + { + return arg; + } + + if (type.isArray() && arg instanceof Collection) + { + Collection col = (Collection) arg; + return col.toArray((Object[]) Array.newInstance(type.getComponentType(), col.size())); + } + + if (type.isAssignableFrom(List.class) && arg.getClass().isArray()) + { + return new AbstractList() + { + @Override + public Object get(int index) + { + return Array.get(arg, index); + } + + @Override + public int size() + { + return Array.getLength(arg); + } + }; + } + + if (type.isArray()) + { + return NO_MATCH; + } + + if (type.isPrimitive() && arg instanceof Long) + { + // no-cost conversions between integer types + Number num = (Number) arg; + + if (type == short.class) + { + return num.shortValue(); + } + if (type == int.class) + { + return num.intValue(); + } + if (type == long.class) + { + return num.longValue(); + } + } + + // all following conversions cost 2 points + convert[0] += 2; + + Object converted = ((CommandSessionImpl) session).doConvert(type, arg); + if (converted != null) + { + return converted; + } + + String string = toString(arg); + + if (type.isAssignableFrom(String.class)) + { + return string; + } + + if (type.isEnum()) + { + for (Object o : type.getEnumConstants()) + { + if (o.toString().equalsIgnoreCase(string)) + { + return o; + } + } + } + + if (type.isPrimitive()) + { + type = primitiveToObject(type); + } + + try + { + return type.getConstructor(String.class).newInstance(string); + } + catch (Exception e) + { + } + + if (type == Character.class && string.length() == 1) + { + return string.charAt(0); + } + + return NO_MATCH; + } + + private static String toString(Object arg) + { + if (arg instanceof Map) + { + StringBuilder sb = new StringBuilder(); + sb.append("["); + boolean first = true; + for (Map.Entry entry : ((Map) arg).entrySet()) + { + if (!first) { + sb.append(" "); + } + first = false; + writeValue(sb, entry.getKey()); + sb.append("="); + writeValue(sb, entry.getValue()); + } + sb.append("]"); + return sb.toString(); + } + else if (arg instanceof Collection) + { + StringBuilder sb = new StringBuilder(); + sb.append("["); + boolean first = true; + for (Object o : ((Collection) arg)) + { + if (!first) { + sb.append(" "); + } + first = false; + writeValue(sb, o); + } + sb.append("]"); + return sb.toString(); + } + else + { + return arg.toString(); + } + } + + private static void writeValue(StringBuilder sb, Object o) { + if (o == null || o instanceof Boolean || o instanceof Number) + { + sb.append(o); + } + else + { + String s = o.toString(); + sb.append("\""); + for (int i = 0; i < s.length(); i++) + { + char c = s.charAt(i); + if (c == '\"' || c == '=') + { + sb.append("\\"); + } + sb.append(c); + } + sb.append("\""); + } + } + + private static Class primitiveToObject(Class type) + { + if (type == boolean.class) + { + return Boolean.class; + } + if (type == byte.class) + { + return Byte.class; + } + if (type == char.class) + { + return Character.class; + } + if (type == short.class) + { + return Short.class; + } + if (type == int.class) + { + return Integer.class; + } + if (type == float.class) + { + return Float.class; + } + if (type == double.class) + { + return Double.class; + } + if (type == long.class) + { + return Long.class; + } + return null; + } + +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/SyntaxError.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/SyntaxError.java new file mode 100644 index 00000000000..ff875195f63 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/SyntaxError.java @@ -0,0 +1,55 @@ +/* + * 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.gogo.runtime; + +public class SyntaxError extends RuntimeException +{ + private static final long serialVersionUID = 1L; + private final int line; + private final int column; + private final Token statement; + + public SyntaxError(int line, int column, String message) + { + this(line, column, message, null); + } + + public SyntaxError(int line, int column, String message, Token statement) + { + super(message); + this.line = line; + this.column = column; + this.statement = statement; + } + + public int column() + { + return column; + } + + public int line() + { + return line; + } + + public Token statement() { + return statement; + } + +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/ThreadUtils.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/ThreadUtils.java new file mode 100644 index 00000000000..1845bb5a2b4 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/ThreadUtils.java @@ -0,0 +1,58 @@ +/* + * 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.gogo.runtime; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +public class ThreadUtils { + + /** + * Constructs threads with names <prefix>-<pool number>-thread-<thread number>. + * @param prefix prefix to be used for thread names created by this {@link ThreadFactory} + * @return ThreadFactory + */ + public static ThreadFactory namedThreadFactory(String prefix) { + return new NamedThreadFactory(prefix); + } + + private static class NamedThreadFactory implements ThreadFactory { + + private static final AtomicInteger poolNumber = new AtomicInteger(1); + private final ThreadGroup group; + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + public NamedThreadFactory(String prefix) { + SecurityManager s = System.getSecurityManager(); + group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); + namePrefix = prefix + "-" + poolNumber.getAndIncrement() + "-thread-"; + } + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); + if (t.isDaemon()) + t.setDaemon(false); + if (t.getPriority() != Thread.NORM_PRIORITY) + t.setPriority(Thread.NORM_PRIORITY); + return t; + } + + } + +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Token.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Token.java new file mode 100644 index 00000000000..bbc087a42e6 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Token.java @@ -0,0 +1,130 @@ +/* + * 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.gogo.runtime; + +public class Token implements CharSequence { + + protected final char[] ch; + protected final int start; + protected final int length; + protected final int line; + protected final int column; + + public Token(CharSequence cs) + { + if (cs instanceof Token) + { + Token ca = (Token) cs; + this.ch = ca.ch; + this.start = ca.start; + this.length = ca.length; + this.line = ca.line; + this.column = ca.column; + } + else + { + this.ch = cs.toString().toCharArray(); + this.start = 0; + this.length = ch.length; + this.line = 0; + this.column = 0; + } + } + + public Token(char[] _ch, int _start, int _length, int _line, int _col) + { + this.ch = _ch; + this.start = _start; + this.length = _length; + this.line = _line; + this.column = _col; + } + + public int line() + { + return line; + } + + public int column() + { + return column; + } + + public int start() + { + return start; + } + + public int length() + { + return this.length; + } + + public char charAt(int index) + { + return this.ch[this.start + index]; + } + + public Token subSequence(int start, int end) + { + int line = this.line; + int col = this.column; + for (int i = this.start; i < this.start + start; i++) + { + if (ch[i] == '\n') + { + line++; + col = 0; + } + else + { + col++; + } + } + return new Token(this.ch, this.start + start, end - start, line, col); + } + + public String toString() + { + return new String(this.ch, this.start, this.length); + } + + public static boolean eq(CharSequence cs1, CharSequence cs2) + { + if (cs1 == cs2) + { + return true; + } + int l1 = cs1.length(); + int l2 = cs2.length(); + if (l1 != l2) + { + return false; + } + for (int i = 0; i < l1; i++) + { + if (cs1.charAt(i) != cs2.charAt(i)) + { + return false; + } + } + return true; + } + +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Tokenizer.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Tokenizer.java new file mode 100644 index 00000000000..ba16fc9b091 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Tokenizer.java @@ -0,0 +1,282 @@ +/* + * 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.gogo.runtime; + +import java.util.regex.Pattern; + +public class Tokenizer extends BaseTokenizer +{ + + private final Pattern redir = Pattern.compile("[0-9&]?>|[0-9]?>>|[0-9]?>&|[0-9]?<|[0-9]?<>|<<<|<<\\-?"); + + protected boolean inArray; + protected int word = 0; + + protected Token pushed; + protected Token last; + + public Tokenizer(CharSequence text) + { + super(text); + } + + public Token text() + { + return text; + } + + public Token next() + { + if (pushed != null) + { + Token t = pushed; + pushed = null; + return t; + } + skipSpace(last == null || Token.eq(last, "\n")); + int start = index - 1; + Token t, tn; + while (true) + { + switch (ch) + { + case EOT: + return token(start); + case '[': + word = 0; + inArray = true; + return token(start); + case ']': + inArray = false; + word++; + return token(start); + case '{': + if (start == index - 1 && Character.isWhitespace(peek())) + { + word = 0; + return token(start); + } + else + { + if (ch == '{') + { + find('}', '{'); + } + else + { + find(')', '('); + } + getch(); + break; + } + case '(': + if (start == index - 1) + { + word = 0; + return token(start); + } + else + { + if (ch == '{') + { + find('}', '{'); + } + else + { + find(')', '('); + } + getch(); + break; + } + case '>': + case '<': + t = text.subSequence(start, index); + if (!eot()) + { + tn = text.subSequence(start, index + 1); + if (redir.matcher(tn).matches()) + { + getch(); + break; + } + } + if (redir.matcher(t).matches() && start < index - 1) + { + getch(); + } + word = 0; + return token(start); + case '-': + t = text.subSequence(start, index); + if (redir.matcher(t).matches()) + { + getch(); + return token(start); + } + else { + getch(); + break; + } + case '&': + // beginning of token + if (start == index - 1) { + if (peek() == '&' || peek() == '>') + { + getch(); + getch(); + } + word = 0; + return token(start); + } + // in the middle of a redirection + else if (redir.matcher(text.subSequence(start, index)).matches()) + { + getch(); + break; + } + else + { + word = 0; + return token(start); + } + case '|': + if (start == index - 1 && (peek() == '|' || peek() == '&')) + { + getch(); + getch(); + } + word = 0; + return token(start); + case ';': + word = 0; + return token(start); + case '}': + case ')': + case ' ': + case '\t': + case '\n': + case '\r': + word++; + return token(start); + case '=': + if (inArray || word < 1 || index == start + 1) + { + word++; + return token(start); + } + getch(); + break; + case '\\': + escape(); + getch(); + break; + case '\'': + case '"': + skipQuote(); + getch(); + break; + default: + getch(); + break; + } + } + } + + private Token token(int start) + { + if (start == index - 1) + { + if (ch == EOT) + { + return null; + } + if (ch == '\r' && peek() == '\n') + { + getch(); + } + getch(); + last = text.subSequence(index - 2, index - 1); + } + else + { + last = text.subSequence(start, index - 1); + } + return last; + } + + public void push(Token token) + { + this.pushed = token; + } + + public Token readHereDoc(boolean ignoreLeadingTabs) + { + final short sLine = line; + final short sCol = column; + int start; + int nlIndex; + boolean nl; + // Find word + skipSpace(); + start = index - 1; + while (ch != '\n' && ch != EOT) { + getch(); + } + if (ch == EOT) { + throw new EOFError(sLine, sCol, "expected here-doc start", "heredoc", "foo\n"); + } + Token token = text.subSequence(start, index - 1); + getch(); + start = index - 1; + nlIndex = start; + nl = true; + // Get heredoc + while (true) + { + if (nl) + { + if (ignoreLeadingTabs && ch == '\t') + { + nlIndex++; + } + else + { + nl = false; + } + } + if (ch == '\n' || ch == EOT) + { + Token s = text.subSequence(nlIndex, index - 1); + if (Token.eq(s, token)) + { + Token hd = text.subSequence(start, s.start()); + getch(); + return hd; + } + nlIndex = index; + nl = true; + } + if (ch == EOT) + { + throw new EOFError(sLine, sCol, "unexpected eof found in here-doc", "heredoc", "\n" + token.toString() + "\n"); + } + getch(); + } + } +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/activator/Activator.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/activator/Activator.java new file mode 100644 index 00000000000..7f4f2420e07 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/activator/Activator.java @@ -0,0 +1,216 @@ +/* + * 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.gogo.runtime.activator; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.felix.gogo.runtime.CommandProcessorImpl; +import org.apache.felix.gogo.runtime.CommandProxy; +import org.apache.felix.gogo.runtime.threadio.ThreadIOImpl; +import org.apache.felix.service.command.CommandSessionListener; +import org.apache.felix.service.command.CommandProcessor; +import org.apache.felix.service.command.Converter; +import org.apache.felix.service.threadio.ThreadIO; +import org.osgi.annotation.bundle.Header; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.util.tracker.ServiceTracker; + +@Header(name = Constants.BUNDLE_ACTIVATOR, value = "${@class}") +public class Activator implements BundleActivator +{ + protected CommandProcessorImpl processor; + private ThreadIOImpl threadio; + private ServiceTracker commandTracker; + private ServiceTracker converterTracker; + private ServiceTracker listenerTracker; + private ServiceRegistration processorRegistration; + private ServiceRegistration threadioRegistration; + + public static final String CONTEXT = ".context"; + + protected ServiceRegistration newProcessor(ThreadIO tio, BundleContext context) + { + processor = new CommandProcessorImpl(tio); + try + { + processor.addListener(new EventAdminListener(context)); + } + catch (NoClassDefFoundError error) + { + // Ignore the listener if EventAdmin package isn't present + } + + // Setup the variables and commands exposed in an OSGi environment. + processor.addConstant(CONTEXT, context); + processor.addCommand("osgi", processor, "addCommand"); + processor.addCommand("osgi", processor, "removeCommand"); + processor.addCommand("osgi", processor, "eval"); + + return context.registerService(CommandProcessor.class.getName(), processor, null); + } + + public void start(final BundleContext context) throws Exception + { + threadio = new ThreadIOImpl(); + threadio.start(); + threadioRegistration = context.registerService(ThreadIO.class.getName(), threadio, null); + + processorRegistration = newProcessor(threadio, context); + + commandTracker = trackOSGiCommands(context); + commandTracker.open(); + + converterTracker = new ServiceTracker(context, Converter.class, null) + { + @Override + public Converter addingService(ServiceReference reference) + { + Converter converter = super.addingService(reference); + processor.addConverter(converter); + return converter; + } + + @Override + public void removedService(ServiceReference reference, Converter service) + { + processor.removeConverter(service); + super.removedService(reference, service); + } + }; + converterTracker.open(); + + listenerTracker = new ServiceTracker(context, CommandSessionListener.class.getName(), null) + { + @Override + public CommandSessionListener addingService(ServiceReference reference) + { + CommandSessionListener listener = super.addingService(reference); + processor.addListener(listener); + return listener; + } + + @Override + public void removedService(ServiceReference reference, CommandSessionListener service) + { + processor.removeListener(service); + super.removedService(reference, service); + } + }; + listenerTracker.open(); + } + + public void stop(BundleContext context) { + processorRegistration.unregister(); + threadioRegistration.unregister(); + commandTracker.close(); + converterTracker.close(); + listenerTracker.close(); + threadio.stop(); + processor.stop(); + } + + private ServiceTracker trackOSGiCommands(final BundleContext context) + throws InvalidSyntaxException + { + Filter filter = context.createFilter(String.format("(&(%s=*)(%s=*))", + CommandProcessor.COMMAND_SCOPE, CommandProcessor.COMMAND_FUNCTION)); + + return new ServiceTracker>(context, filter, null) + { + private final ConcurrentMap, Map> proxies + = new ConcurrentHashMap<>(); + + @Override + public List addingService(ServiceReference reference) + { + Object scope = reference.getProperty(CommandProcessor.COMMAND_SCOPE); + Object function = reference.getProperty(CommandProcessor.COMMAND_FUNCTION); + Object ranking = reference.getProperty(Constants.SERVICE_RANKING); + List commands = new ArrayList<>(); + + int rank = 0; + if (ranking != null) + { + try + { + rank = Integer.parseInt(ranking.toString()); + } + catch (NumberFormatException e) + { + // Ignore + } + } + if (scope != null && function != null) + { + Map proxyMap = new HashMap<>(); + if (function.getClass().isArray()) + { + for (Object f : ((Object[]) function)) + { + CommandProxy target = new CommandProxy(context, reference, f.toString()); + proxyMap.put(f.toString(), target); + processor.addCommand(scope.toString(), target, f.toString(), rank); + commands.add(target); + } + } + else + { + CommandProxy target = new CommandProxy(context, reference, function.toString()); + proxyMap.put(function.toString(), target); + processor.addCommand(scope.toString(), target, function.toString(), rank); + commands.add(target); + } + proxies.put(reference, proxyMap); + return commands; + } + return null; + } + + @Override + public void removedService(ServiceReference reference, List service) + { + Object scope = reference.getProperty(CommandProcessor.COMMAND_SCOPE); + Object function = reference.getProperty(CommandProcessor.COMMAND_FUNCTION); + + if (scope != null && function != null) + { + Map proxyMap = proxies.remove(reference); + for (Map.Entry entry : proxyMap.entrySet()) + { + processor.removeCommand(scope.toString(), entry.getKey(), entry.getValue()); + } + } + + super.removedService(reference, service); + } + }; + } + +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/activator/EventAdminListener.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/activator/EventAdminListener.java new file mode 100644 index 00000000000..a84317e820b --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/activator/EventAdminListener.java @@ -0,0 +1,61 @@ +/* + * 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.gogo.runtime.activator; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.CommandSessionListener; +import org.osgi.framework.BundleContext; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventAdmin; +import org.osgi.util.tracker.ServiceTracker; + +public class EventAdminListener implements CommandSessionListener +{ + private ServiceTracker tracker; + + public EventAdminListener(BundleContext bundleContext) + { + tracker = new ServiceTracker<>(bundleContext, EventAdmin.class, null); + tracker.open(); + } + + public void beforeExecute(CommandSession session, CharSequence command) + { + EventAdmin admin = tracker.getService(); + if (admin != null) + { + Map props = new HashMap<>(); + props.put("command", command.toString()); + Event event = new Event("org/apache/felix/service/command/EXECUTING", props); + admin.postEvent(event); + } + } + + public void afterExecute(CommandSession session, CharSequence command, Exception exception) + { + } + + public void afterExecute(CommandSession session, CharSequence command, Object result) + { + } + +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/package-info.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/package-info.java new file mode 100644 index 00000000000..04d1bc395a8 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/package-info.java @@ -0,0 +1,32 @@ +/* + * 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. + */ + +@org.osgi.annotation.bundle.Capability( + namespace = "org.apache.felix.gogo", + name = "runtime.implementation", + version = "1.0.0" +) +@org.osgi.annotation.bundle.Requirement( + effective = "active", + namespace = "org.apache.felix.gogo", + name = "shell.implementation", + version = "1.0.0" +) +package org.apache.felix.gogo.runtime; + diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/threadio/Marker.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/threadio/Marker.java new file mode 100644 index 00000000000..84dc7cef8ed --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/threadio/Marker.java @@ -0,0 +1,63 @@ +/* + * 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.gogo.runtime.threadio; + +import java.io.InputStream; +import java.io.PrintStream; + +public class Marker +{ + final Marker previous; + InputStream in; + PrintStream out; + PrintStream err; + volatile boolean deactivated; + + public Marker(InputStream in, PrintStream out, PrintStream err, Marker previous) + { + this.previous = previous; + this.in = in; + this.out = out; + this.err = err; + } + + public InputStream getIn() + { + return deactivated ? previous.getIn() : in; + } + + public PrintStream getOut() + { + return deactivated ? previous.getOut() : out; + } + + public PrintStream getErr() + { + return deactivated ? previous.getErr() : err; + } + + void deactivate() + { + deactivated = true; + // Set to null for garbage collection + in = null; + out = null; + err = null; + } +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/threadio/ThreadIOImpl.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/threadio/ThreadIOImpl.java new file mode 100644 index 00000000000..60d4ddfdba6 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/threadio/ThreadIOImpl.java @@ -0,0 +1,138 @@ +/* + * 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. + */ +// DWB20: ThreadIO should check and reset IO if something (e.g. jetty) overrides +package org.apache.felix.gogo.runtime.threadio; + +import java.io.InputStream; +import java.io.PrintStream; +import java.util.logging.Logger; + +import org.apache.felix.service.threadio.ThreadIO; +import org.osgi.annotation.bundle.Capability; +import org.osgi.namespace.service.ServiceNamespace; + +@Capability( + namespace = ServiceNamespace.SERVICE_NAMESPACE, + attribute = "objectClass='org.apache.felix.service.threadio.ThreadIO'" +) +public class ThreadIOImpl implements ThreadIO +{ + static private final Logger log = Logger.getLogger(ThreadIOImpl.class.getName()); + + final Marker defaultMarker = new Marker(System.in, System.out, System.err, null); + final ThreadPrintStream err = new ThreadPrintStream(this, System.err, true); + final ThreadPrintStream out = new ThreadPrintStream(this, System.out, false); + final ThreadInputStream in = new ThreadInputStream(this, System.in); + final ThreadLocal current = new InheritableThreadLocal() + { + @Override + protected Marker initialValue() + { + return defaultMarker; + } + }; + + public void start() + { + if (System.out instanceof ThreadPrintStream) + { + throw new IllegalStateException("Thread Print Stream already set"); + } + System.setOut(out); + System.setIn(in); + System.setErr(err); + } + + public void stop() + { + System.setErr(defaultMarker.err); + System.setOut(defaultMarker.out); + System.setIn(defaultMarker.in); + } + + private void checkIO() + { // derek + if (System.in != in) + { + log.fine("ThreadIO: eek! who's set System.in=" + System.in); + System.setIn(in); + } + + if (System.out != out) + { + log.fine("ThreadIO: eek! who's set System.out=" + System.out); + System.setOut(out); + } + + if (System.err != err) + { + log.fine("ThreadIO: eek! who's set System.err=" + System.err); + System.setErr(err); + } + } + + Marker current() + { + Marker m = current.get(); + if (m.deactivated) + { + while (m.deactivated) + { + m = m.previous; + } + current.set(m); + } + return m; + } + + public void close() + { + checkIO(); // derek + Marker top = this.current.get(); + if (top == null) + { + throw new IllegalStateException("No thread io active"); + } + if (top != defaultMarker) + { + top.deactivate(); + this.current.set(top.previous); + } + } + + public void setStreams(InputStream in, PrintStream out, PrintStream err) + { + assert in != null; + assert out != null; + assert err != null; + checkIO(); // derek + Marker prev = current(); + if (in == this.in) { + in = prev.getIn(); + } + if (out == this.out) { + out = prev.getOut(); + } + if (err == this.err) { + err = prev.getErr(); + } + Marker marker = new Marker(in, out, err, prev); + this.current.set(marker); + } +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/threadio/ThreadInputStream.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/threadio/ThreadInputStream.java new file mode 100644 index 00000000000..af86a316f09 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/threadio/ThreadInputStream.java @@ -0,0 +1,98 @@ +/* + * 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.gogo.runtime.threadio; + +import java.io.IOException; +import java.io.InputStream; + +public class ThreadInputStream extends InputStream +{ + final InputStream dflt; + final ThreadIOImpl io; + + public ThreadInputStream(ThreadIOImpl threadIO, InputStream in) + { + io = threadIO; + dflt = in; + } + + private InputStream getCurrent() + { + Marker marker = io.current(); + return marker.getIn(); + } + + /** + * Access to the root stream through reflection + * @return InputStream + */ + public InputStream getRoot() + { + return dflt; + } + + // + // Delegate methods + // + + public int read() throws IOException + { + return getCurrent().read(); + } + + public int read(byte[] b) throws IOException + { + return getCurrent().read(b); + } + + public int read(byte[] b, int off, int len) throws IOException + { + return getCurrent().read(b, off, len); + } + + public long skip(long n) throws IOException + { + return getCurrent().skip(n); + } + + public int available() throws IOException + { + return getCurrent().available(); + } + + public void close() throws IOException + { + getCurrent().close(); + } + + public void mark(int readlimit) + { + getCurrent().mark(readlimit); + } + + public void reset() throws IOException + { + getCurrent().reset(); + } + + public boolean markSupported() + { + return getCurrent().markSupported(); + } +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/threadio/ThreadPrintStream.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/threadio/ThreadPrintStream.java new file mode 100644 index 00000000000..decca062fe1 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/threadio/ThreadPrintStream.java @@ -0,0 +1,226 @@ +/* + * 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.gogo.runtime.threadio; + +import java.io.IOException; +import java.io.PrintStream; +import java.util.Locale; + +public class ThreadPrintStream extends PrintStream +{ + final PrintStream dflt; + final ThreadIOImpl io; + final boolean errorStream; + + public ThreadPrintStream(ThreadIOImpl threadIO, PrintStream out, boolean error) + { + super(out); + dflt = out; + io = threadIO; + errorStream = error; + } + + public PrintStream getCurrent() + { + Marker marker = io.current(); + return errorStream ? marker.getErr() : marker.getOut(); + } + + /** + * Access to the root stream through reflection + */ + public PrintStream getRoot() + { + return dflt; + } + + // + // Delegate methods + // + + public void flush() + { + getCurrent().flush(); + } + + public void close() + { + getCurrent().close(); + } + + public boolean checkError() + { + return getCurrent().checkError(); + } + + public void setError() + { + // getCurrent().setError(); + } + + public void clearError() + { + // getCurrent().clearError(); + } + + public void write(int b) + { + getCurrent().write(b); + } + + public void write(byte[] buf, int off, int len) + { + getCurrent().write(buf, off, len); + } + + public void print(boolean b) + { + getCurrent().print(b); + } + + public void print(char c) + { + getCurrent().print(c); + } + + public void print(int i) + { + getCurrent().print(i); + } + + public void print(long l) + { + getCurrent().print(l); + } + + public void print(float f) + { + getCurrent().print(f); + } + + public void print(double d) + { + getCurrent().print(d); + } + + public void print(char[] s) + { + getCurrent().print(s); + } + + public void print(String s) + { + getCurrent().print(s); + } + + public void print(Object obj) + { + getCurrent().print(obj); + } + + public void println() + { + getCurrent().println(); + } + + public void println(boolean x) + { + getCurrent().println(x); + } + + public void println(char x) + { + getCurrent().println(x); + } + + public void println(int x) + { + getCurrent().println(x); + } + + public void println(long x) + { + getCurrent().println(x); + } + + public void println(float x) + { + getCurrent().println(x); + } + + public void println(double x) + { + getCurrent().println(x); + } + + public void println(char[] x) + { + getCurrent().println(x); + } + + public void println(String x) + { + getCurrent().println(x); + } + + public void println(Object x) + { + getCurrent().println(x); + } + + public PrintStream printf(String format, Object... args) + { + return getCurrent().printf(format, args); + } + + public PrintStream printf(Locale l, String format, Object... args) + { + return getCurrent().printf(l, format, args); + } + + public PrintStream format(String format, Object... args) + { + return getCurrent().format(format, args); + } + + public PrintStream format(Locale l, String format, Object... args) + { + return getCurrent().format(l, format, args); + } + + public PrintStream append(CharSequence csq) + { + return getCurrent().append(csq); + } + + public PrintStream append(CharSequence csq, int start, int end) + { + return getCurrent().append(csq, start, end); + } + + public PrintStream append(char c) + { + return getCurrent().append(c); + } + + public void write(byte[] b) throws IOException + { + getCurrent().write(b); + } +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/util/function/BiFunction.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/util/function/BiFunction.java new file mode 100644 index 00000000000..95e95827ad2 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/util/function/BiFunction.java @@ -0,0 +1,26 @@ +/* + * 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.gogo.runtime.util.function; + +//This interface was added to ease conversion of existing runtime bundle code +//targeting java 8 back to target java 7. +public interface BiFunction { + + R apply(T t, U u); +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/util/function/Function.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/util/function/Function.java new file mode 100644 index 00000000000..c5f9d8e38aa --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/util/function/Function.java @@ -0,0 +1,27 @@ +/* + * 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.gogo.runtime.util.function; + +// This interface was added to ease conversion of existing runtime bundle code +// targeting java 8 back to target java 7. +public interface Function { + R apply(T t); + + } + diff --git a/gogo/runtime/src/main/java/org/apache/felix/service/command/CommandProcessor.java b/gogo/runtime/src/main/java/org/apache/felix/service/command/CommandProcessor.java new file mode 100644 index 00000000000..55225408205 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/service/command/CommandProcessor.java @@ -0,0 +1,65 @@ +/* + * 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.service.command; + +import java.io.InputStream; +import java.io.OutputStream; + +/** + * A command shell can create and maintain a number of command sessions. + * + * @author aqute + */ +public interface CommandProcessor +{ + /** + * The scope of commands provided by this service. This name can be used to distinguish + * between different command providers with the same function names. + */ + String COMMAND_SCOPE = "osgi.command.scope"; + + /** + * A list of method names that may be called for this command provider. A + * name may end with a *, this will then be calculated from all declared public + * methods in this service. + *

      + * Help information for the command may be supplied with a space as + * separation. + */ + String COMMAND_FUNCTION = "osgi.command.function"; + + /** + * Create a new command session associated with IO streams. + *

      + * The session is bound to the life cycle of the bundle getting this + * service. The session will be automatically closed when this bundle is + * stopped or the service is returned. + *

      + * The shell will provide any available commands to this session and + * can set additional variables. + * + * @param in The value used for System.in + * @param out The stream used for System.out + * @param err The stream used for System.err + * @return A new session. + */ + CommandSession createSession(InputStream in, OutputStream out, OutputStream err); + + CommandSession createSession(CommandSession parent); +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/service/command/CommandSession.java b/gogo/runtime/src/main/java/org/apache/felix/service/command/CommandSession.java new file mode 100644 index 00000000000..e4e6df1800b --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/service/command/CommandSession.java @@ -0,0 +1,148 @@ +/* + * 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.service.command; + +import java.io.InputStream; +import java.io.PrintStream; +import java.nio.file.Path; +import java.util.List; + +public interface CommandSession extends AutoCloseable +{ + + /* + * Variable name to disable glob (filename) expansion + */ + String OPTION_NO_GLOB = "gogo.option.noglob"; + + Path currentDir(); + + void currentDir(Path path); + + ClassLoader classLoader(); + + void classLoader(ClassLoader classLoader); + + /** + * Execute a program in this session. + * + * @param commandline the commandline + * @return the result of the execution + * @throws Exception on exception + */ + Object execute(CharSequence commandline) throws Exception; + + /** + * Close this command session. After the session is closed, it will throw + * IllegalStateException when it is used. + */ + void close(); + + /** + * Return the input stream that is the first of the pipeline. This stream is + * sometimes necessary to communicate directly to the end user. For example, + * a "less" or "more" command needs direct input from the keyboard to + * control the paging. + * + * @return InpuStream used closest to the user or null if input is from a + * file. + */ + InputStream getKeyboard(); + + /** + * Return the PrintStream for the console. This must always be the stream + * "closest" to the user. This stream can be used to post messages that + * bypass the piping. If the output is piped to a file, then the object + * returned must be null. + * + * @return PrintStream the console print stream + */ + PrintStream getConsole(); + + /** + * Get the value of a variable. + * + * @param name the name + * @return Object + */ + Object get(String name); + + /** + * Set the value of a variable. + * + * @param name Name of the variable. + * @param value Value of the variable + * @return Object + */ + Object put(String name, Object value); + + /** + * Convert an object to string form (CharSequence). The level is defined in + * the Converter interface, it can be one of INSPECT, LINE, PART. This + * function always returns a non null value. As a last resort, toString is + * called on the Object. + * + * @param target the target + * @param level the level + * @return CharSequence + */ + CharSequence format(Object target, int level); + + /** + * Convert an object to another type. + * @param type the type + * @param instance the instance + * @return Object + */ + Object convert(Class type, Object instance); + + // + // Job support + // + + /** + * List jobs. Always return a non-null list. + * @return List<Job> + */ + List jobs(); + + /** + * Get the current foreground job or null. + * @return Job + */ + Job foregroundJob(); + + /** + * Set the job listener for this session. + * @param listener the listener + */ + void setJobListener(JobListener listener); + + /** + * Return the current session. + * Available inside from a command call. + */ + class Utils { + public static CommandSession current() { + Job j = Job.Utils.current(); + return j != null ? j.session() : null; + } + } + +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/service/command/CommandSessionListener.java b/gogo/runtime/src/main/java/org/apache/felix/service/command/CommandSessionListener.java new file mode 100644 index 00000000000..23575ddc9e4 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/service/command/CommandSessionListener.java @@ -0,0 +1,36 @@ +/* + * 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.service.command; + +/** + * Listener for command executions. + * + * Such listeners must be registered in the OSGi registry and will be called + * by the CommandProcessor when a command line is executed in a given session. + */ +public interface CommandSessionListener +{ + + void beforeExecute(CommandSession session, CharSequence command); + + void afterExecute(CommandSession session, CharSequence command, Exception exception); + + void afterExecute(CommandSession session, CharSequence command, Object result); + +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/service/command/Converter.java b/gogo/runtime/src/main/java/org/apache/felix/service/command/Converter.java new file mode 100644 index 00000000000..6821323c1c7 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/service/command/Converter.java @@ -0,0 +1,85 @@ +/* + * 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.service.command; + +/** + * A converter is a service that can help create specific object types from a + * string, and vice versa. + *

      + * The shell is capable of coercing arguments to the their proper type. However, + * sometimes commands require extra help to do this conversion. This service can + * implement a converter for a number of types. + *

      + * The command shell will rank these services in order of service.ranking and + * will then call them until one of the converters succeeds. + */ +public interface Converter +{ + /** + * This property is a string, or array of strings, and defines the classes + * or interfaces that this converter recognizes. Recognized classes can be + * converted from a string to a class and they can be printed in 3 different + * modes. + */ + String CONVERTER_CLASSES = "osgi.converter.classes"; + + /** + * Print the object in detail. This can contain multiple lines. + */ + int INSPECT = 0; + + /** + * Print the object as a row in a table. The columns should align for + * multiple objects printed beneath each other. The print may run over + * multiple lines but must not end in a CR. + */ + int LINE = 1; + + /** + * Print the value in a small format so that it is identifiable. This + * printed format must be recognizable by the conversion method. + */ + int PART = 2; + + /** + * Convert an object to the desired type. + *

      + * Return null if the conversion can not be done. Otherwise return and + * object that extends the desired type or implements it. + * + * @param desiredType The type that the returned object can be assigned to + * @param in The object that must be converted + * @return An object that can be assigned to the desired type or null. + * @throws Exception on exception + */ + Object convert(Class desiredType, Object in) throws Exception; + + /** + * Convert an objet to a CharSequence object in the requested format. The + * format can be INSPECT, LINE, or PART. Other values must throw + * IllegalArgumentException. + * + * @param target The object to be converted to a String + * @param level One of INSPECT, LINE, or PART. + * @param escape Use this object to format sub ordinate objects. + * @return A printed object of potentially multiple lines + * @throws Exception on exception + */ + CharSequence format(Object target, int level, Converter escape) throws Exception; +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/service/command/Descriptor.java b/gogo/runtime/src/main/java/org/apache/felix/service/command/Descriptor.java new file mode 100644 index 00000000000..bd57ac61949 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/service/command/Descriptor.java @@ -0,0 +1,31 @@ +/* + * 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.service.command; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER}) +public @interface Descriptor +{ + String value(); +} \ No newline at end of file diff --git a/gogo/runtime/src/main/java/org/apache/felix/service/command/Function.java b/gogo/runtime/src/main/java/org/apache/felix/service/command/Function.java new file mode 100644 index 00000000000..830f688b34d --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/service/command/Function.java @@ -0,0 +1,38 @@ +/* + * 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.service.command; + +import java.util.List; + +/** + * A Function is a a block of code that can be executed with a set of arguments, + * it returns the result object of executing the script. + */ +public interface Function +{ + /** + * Execute this function and return the result. + * + * @param session the session + * @param arguments the arguments + * @return the result from the execution. + * @throws Exception if anything goes terribly wrong + */ + Object execute(CommandSession session, List arguments) throws Exception; +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/service/command/Job.java b/gogo/runtime/src/main/java/org/apache/felix/service/command/Job.java new file mode 100644 index 00000000000..ecfa2a2b12d --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/service/command/Job.java @@ -0,0 +1,71 @@ +/* + * 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.service.command; + +import java.util.List; + +public interface Job +{ + + class Utils { + /** + * Get the job running in the current thread or null. + */ + public static Job current() { + Process p = Process.Utils.current(); + Job j = p != null ? p.job() : null; + while (j != null && j.parent() != null) { + j = j.parent(); + } + return j; + } + } + + enum Status + { + Created, + Suspended, + Background, + Foreground, + Done + } + + int id(); + + CharSequence command(); + + Status status(); + + void suspend(); + + void background(); + + void foreground(); + + void interrupt(); + + Result result(); + + List processes(); + + CommandSession session(); + + Job parent(); + +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/service/command/JobListener.java b/gogo/runtime/src/main/java/org/apache/felix/service/command/JobListener.java new file mode 100644 index 00000000000..fd30089f149 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/service/command/JobListener.java @@ -0,0 +1,32 @@ +/* + * 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.service.command; + +/** + * Listener for command executions. + * + * Such listeners must be registered in the OSGi registry and will be called + * by the CommandProcessor when a command line is executed in a given session. + */ +public interface JobListener +{ + + void jobChanged(Job job, Job.Status previous, Job.Status current); + +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/service/command/Parameter.java b/gogo/runtime/src/main/java/org/apache/felix/service/command/Parameter.java new file mode 100644 index 00000000000..947f5b66ec7 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/service/command/Parameter.java @@ -0,0 +1,57 @@ +/* + * 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.service.command; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER}) +public @interface Parameter +{ + String UNSPECIFIED = "org.apache.felix.service.command.unspecified.parameter"; + + /** + * Parameter name and aliases which must start with the hyphen character. + * @return parameter names. + **/ + String[] names(); + + /** + * The default value of the parameter if its name is present on the + * command line. If this value is specified, then the command parsing + * will not expect a value on the command line for this parameter. + * If this value is UNSPECIFIED, then an argument must be specified on the + * command line for the parameter. + * @return default value of the parameter if its name is present on the + * command line. + **/ + String presentValue() default UNSPECIFIED; + + /** + * The default value of the parameter if its name is not present on the + * command line. This value is effectively the default value for the + * parameter. + * @return default value of the parameter if its name is not present + * on the command line. + **/ + String absentValue(); +} \ No newline at end of file diff --git a/gogo/runtime/src/main/java/org/apache/felix/service/command/Process.java b/gogo/runtime/src/main/java/org/apache/felix/service/command/Process.java new file mode 100644 index 00000000000..0c3db97fe49 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/service/command/Process.java @@ -0,0 +1,60 @@ +/* + * 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.service.command; + +import java.io.InputStream; +import java.io.PrintStream; + +import org.apache.felix.gogo.runtime.Pipe; + +public interface Process +{ + + class Utils { + public static Process current() { + return Pipe.getCurrentPipe(); + } + } + + InputStream in(); + + PrintStream out(); + + PrintStream err(); + + /** + * Get the job controlling this process + * @return Job + */ + Job job(); + + /** + * Check if the given descriptor for the currently running pipe is the terminal or not. + * @param fd the fd + * @return boolean + */ + boolean isTty(int fd); + + /** + * Set the error code for the currently running pipe. + * @param error the error + */ + void error(int error); + +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/service/command/Result.java b/gogo/runtime/src/main/java/org/apache/felix/service/command/Result.java new file mode 100644 index 00000000000..83162c73618 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/service/command/Result.java @@ -0,0 +1,31 @@ +/* + * 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.service.command; + +public interface Result +{ + + boolean isSuccess(); + + Object result(); + + Exception exception(); + + int error(); +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/service/command/annotations/GogoCommand.java b/gogo/runtime/src/main/java/org/apache/felix/service/command/annotations/GogoCommand.java new file mode 100644 index 00000000000..e6a0e821972 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/service/command/annotations/GogoCommand.java @@ -0,0 +1,48 @@ +/* + * 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.service.command.annotations; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.CLASS; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.osgi.service.component.annotations.ComponentPropertyType; + +@ComponentPropertyType +@RequireGogoWhiteboard +@Retention(CLASS) +@Target(TYPE) +public @interface GogoCommand { + + String PREFIX_ = "osgi.command."; + + /** + * @return the scope used to disambiguate command functions + */ + String scope(); + + /** + * @return the command functions provided by the service + */ + String[] function(); + +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/service/command/annotations/RequireGogo.java b/gogo/runtime/src/main/java/org/apache/felix/service/command/annotations/RequireGogo.java new file mode 100644 index 00000000000..3630ba00eae --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/service/command/annotations/RequireGogo.java @@ -0,0 +1,45 @@ +/* + * 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.service.command.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.osgi.annotation.bundle.Attribute; +import org.osgi.annotation.bundle.Requirement; + +import static java.lang.annotation.RetentionPolicy.CLASS; + +import static java.lang.annotation.ElementType.*; + +@Retention(CLASS) +@Target({TYPE, PACKAGE}) +@Requirement( + effective = "active", + namespace = "org.apache.felix.gogo", + name = "shell.implementation" +) +public @interface RequireGogo { + String JLINE = "gogo.jline"; + String SHELL = "gogo.shell"; + + @Attribute("implementation.name") + String value() default SHELL; +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/service/command/annotations/RequireGogoWhiteboard.java b/gogo/runtime/src/main/java/org/apache/felix/service/command/annotations/RequireGogoWhiteboard.java new file mode 100644 index 00000000000..376250109a1 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/service/command/annotations/RequireGogoWhiteboard.java @@ -0,0 +1,40 @@ +/* + * 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.service.command.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.osgi.annotation.bundle.Requirement; + +import static java.lang.annotation.RetentionPolicy.CLASS; + +import static java.lang.annotation.ElementType.*; + +@Retention(CLASS) +@Target({TYPE, PACKAGE}) +@Requirement( + effective = "active", + namespace = "org.apache.felix.gogo", + name = "runtime.implementation", + version = "1.0.0" +) +public @interface RequireGogoWhiteboard { +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/service/command/annotations/package-info.java b/gogo/runtime/src/main/java/org/apache/felix/service/command/annotations/package-info.java new file mode 100644 index 00000000000..d88f1ed85cd --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/service/command/annotations/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("1.0.0") +package org.apache.felix.service.command.annotations; + diff --git a/gogo/runtime/src/main/java/org/apache/felix/service/command/package-info.java b/gogo/runtime/src/main/java/org/apache/felix/service/command/package-info.java new file mode 100644 index 00000000000..dde0b92bcb0 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/service/command/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("1.0.0") +package org.apache.felix.service.command; + diff --git a/gogo/runtime/src/main/java/org/apache/felix/service/threadio/ThreadIO.java b/gogo/runtime/src/main/java/org/apache/felix/service/threadio/ThreadIO.java new file mode 100644 index 00000000000..08b04f53aa2 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/service/threadio/ThreadIO.java @@ -0,0 +1,57 @@ +/* + * 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.service.threadio; + +import java.io.InputStream; +import java.io.PrintStream; + +/** + * Enable multiplexing of the standard IO streams for input, output, and error. + *

      + * This service guards the central resource of IO streams. The standard streams + * are singletons. This service replaces the singletons with special versions that + * can find a unique stream for each thread. If no stream is associated with a + * thread, it will use the standard input/output that was originally set. + * + * @author aqute + */ +public interface ThreadIO +{ + /** + * Associate this streams with the current thread. + *

      + * Ensure that when output is performed on System.in, System.out, System.err it + * will happen on the given streams. + *

      + * The streams will automatically be canceled when the bundle that has gotten + * this service is stopped or returns this service. + * + * @param in InputStream to use for the current thread when System.in is used + * @param out PrintStream to use for the current thread when System.out is used + * @param err PrintStream to use for the current thread when System.err is used + */ + void setStreams(InputStream in, PrintStream out, PrintStream err); + + /** + * Cancel the streams associated with the current thread. + *

      + * This method will not do anything when no streams are associated. + */ + void close(); +} diff --git a/gogo/runtime/src/main/java/org/apache/felix/service/threadio/package-info.java b/gogo/runtime/src/main/java/org/apache/felix/service/threadio/package-info.java new file mode 100644 index 00000000000..d55711be340 --- /dev/null +++ b/gogo/runtime/src/main/java/org/apache/felix/service/threadio/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("1.0.0") +package org.apache.felix.service.threadio; + diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/AbstractParserTest.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/AbstractParserTest.java new file mode 100644 index 00000000000..323842588b9 --- /dev/null +++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/AbstractParserTest.java @@ -0,0 +1,75 @@ +/* + * 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.gogo.runtime; + +import java.io.FilterInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; + +import org.apache.felix.gogo.runtime.threadio.ThreadIOImpl; +import org.junit.After; +import org.junit.Before; + +public abstract class AbstractParserTest { + + private ThreadIOImpl threadIO; + private InputStream sin; + private PrintStream sout; + private PrintStream serr; + + @Before + public void setUp() { + sin = new NoCloseInputStream(System.in); + sout = new NoClosePrintStream(System.out); + serr = new NoClosePrintStream(System.err); + threadIO = new ThreadIOImpl(); + threadIO.start(); + } + + @After + public void tearDown() { + threadIO.stop(); + } + + public class Context extends org.apache.felix.gogo.runtime.Context { + public Context() { + super(AbstractParserTest.this.threadIO, sin, sout, serr); + } + } + + private static class NoCloseInputStream extends FilterInputStream { + public NoCloseInputStream(InputStream in) { + super(in); + } + @Override + public void close() { + } + } + + private static class NoClosePrintStream extends PrintStream { + public NoClosePrintStream(OutputStream out) { + super(out); + } + @Override + public void close() { + } + } + +} diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/ClosureTest.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/ClosureTest.java new file mode 100644 index 00000000000..9739d6bea29 --- /dev/null +++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/ClosureTest.java @@ -0,0 +1,44 @@ +/* + * 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.gogo.runtime; + +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.Collections; + +import static org.junit.Assert.assertEquals; + +public class ClosureTest { + + @Test + public void testParentSessionClosure() throws Exception { + CommandProcessorImpl processor = new CommandProcessorImpl(null); + ByteArrayInputStream bais = new ByteArrayInputStream("".getBytes()); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CommandSessionImpl parent = processor.createSession(bais, baos, baos); + parent.execute("var = a"); + parent.execute("cmd = { $var }"); + CommandSessionImpl child = processor.createSession(parent); + child.execute("var = b"); + assertEquals("a", ((Closure) parent.get("cmd")).execute(parent, Collections.emptyList()).toString()); + assertEquals("b", ((Closure) parent.get("cmd")).execute(child, Collections.emptyList()).toString()); + } +} diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/Context.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/Context.java new file mode 100644 index 00000000000..4db6b5f6bbc --- /dev/null +++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/Context.java @@ -0,0 +1,80 @@ +/* + * 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.gogo.runtime; + +import java.io.InputStream; +import java.io.PrintStream; +import java.nio.file.Path; + +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.threadio.ThreadIO; + +public class Context extends CommandProcessorImpl +{ + public static final String EMPTY = ""; + + private final CommandSession session; + + public Context(ThreadIO threadio, InputStream in, PrintStream out, PrintStream err) + { + super(threadio); + addCommand("osgi", this, "addCommand"); + addCommand("osgi", this, "removeCommand"); + addCommand("osgi", this, "eval"); + session = createSession(in, out, err); + } + + public Object execute(CharSequence source) throws Exception + { + Object result = new Exception(); + try + { + return result = session.execute(source); + } + finally + { + System.err.println("execute<" + source + "> = (" + + (null == result ? "Null" : result.getClass().getSimpleName()) + ")(" + + result + ")\n"); + } + } + + public void addCommand(String function, Object target) + { + addCommand("test", target, function); + } + + public Object set(String name, Object value) + { + return session.put(name, value); + } + + public Object get(String name) + { + return session.get(name); + } + + public void currentDir(Path path) { + session.currentDir(path); + } + + public Path currentDir() { + return session.currentDir(); + } +} diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/ExpanderTest.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/ExpanderTest.java new file mode 100644 index 00000000000..d752025aefb --- /dev/null +++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/ExpanderTest.java @@ -0,0 +1,135 @@ +/* + * 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.gogo.runtime; + +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class ExpanderTest { + + @Test + public void testSubscriptOnArrays() throws Exception { + Evaluate evaluate = new TestEvaluate() { + @Override + public Object get(String key) { + switch (key) { + case "a": + return new Object[] { 1, 3 }; + case "b": + return new int[] { 1, 3 }; + case "c": + return Arrays.asList(1, 3); + } + return null; + } + }; + + assertEquals(3, Expander.expand("${a[1]}", evaluate)); + assertEquals(3, Expander.expand("${b[1]}", evaluate)); + assertEquals(3, Expander.expand("${c[1]}", evaluate)); + } + + @Test + public void testOctalAndHex() throws Exception { + Evaluate evaluate = new TestEvaluate(); + assertEquals("\033\033", Expander.expand("$'\\033\\u001B'", evaluate)); + } + + @Test + public void testSortingFlags() throws Exception { + Evaluate evaluate = new TestEvaluate() { + @Override + public Object get(String key) { + switch (key) { + case "a": + return Arrays.asList(5, 3, 4, 2, 1, 3); + case "b": + return Arrays.asList(5, 3, 4, 2, 1, 31); + case "c": + return Arrays.asList("foo1", "foo02", "foo2", "foo3", "foo20", "foo23"); + case "d": + return Arrays.asList("foo1", "foo02", "Foo2", "Foo3", "foo20", "foo23"); + } + return null; + } + }; + + assertEquals("5 3 4 2 1 3", Expander.expand("${(j: :)a}", evaluate)); + assertEquals("5 3 4 2 1", Expander.expand("${(j: :)${(u)a}}", evaluate)); + assertEquals("3 1 2 4 3 5", Expander.expand("${(j: :)${(Oa)a}}", evaluate)); + assertEquals("1 2 3 31 4 5", Expander.expand("${(j: :)${(i)b}}", evaluate)); + assertEquals("1 2 3 4 5 31", Expander.expand("${(j: :)${(n)b}}", evaluate)); + assertEquals("foo1 foo02 foo2 foo3 foo20 foo23", Expander.expand("${(j: :)${(n)c}}", evaluate)); + assertEquals("foo23 foo20 foo3 foo2 foo02 foo1", Expander.expand("${(j: :)${(On)c}}", evaluate)); + assertEquals("Foo2 Foo3 foo1 foo02 foo20 foo23", Expander.expand("${(j: :)${(n)d}}", evaluate)); + assertEquals("foo1 foo02 Foo2 Foo3 foo20 foo23", Expander.expand("${(j: :)${(ni)d}}", evaluate)); + } + + @Test + public void testGenerateFiles() throws IOException { + final Path testdir = Paths.get(".").toAbsolutePath().resolve("target/testdir").normalize(); + Evaluate evaluate = new TestEvaluate() { + @Override + public Object get(String key) { + if ("HOME".equals(key)) { + return testdir.resolve("Users/gogo"); + } + return null; + } + @Override + public Path currentDir() { + return testdir.resolve("Users/gogo/karaf/home"); + } + }; + deleteRecursive(testdir); + Files.createDirectories(testdir); + Files.createDirectories(evaluate.currentDir()); + Path home = Paths.get(evaluate.get("HOME").toString()); + Files.createDirectories(home); + Files.createFile(home.resolve("test1.txt")); + Files.createDirectories(home.resolve("child")); + Files.createFile(home.resolve("child/test2.txt")); + + Expander expander = new Expander("", evaluate, false, false, false, false, false); + List files = expander.generateFileNames("~/*.[tx][v-z][!a]"); + assertNotNull(files); + assertEquals(1, files.size()); + assertEquals("test1.txt", home.relativize(Paths.get(files.get(0).toString())).toString()); + } + + private static void deleteRecursive(Path file) throws IOException { + if (file != null) { + if (Files.isDirectory(file)) { + for (Path child : Files.newDirectoryStream(file)) { + deleteRecursive(child); + } + } + Files.deleteIfExists(file); + } + } +} diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/GlobPathMatcherTest.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/GlobPathMatcherTest.java new file mode 100644 index 00000000000..84002930d2c --- /dev/null +++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/GlobPathMatcherTest.java @@ -0,0 +1,343 @@ +/* + * 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.gogo.runtime; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Part of these test case have been kindly borrowed from + * https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java + */ +public class GlobPathMatcherTest { + + @Test + public void isMatchWithCaseSensitiveWithDefaultPathSeparator() { + + final Builder.Matcher pathMatcher = new Builder().build(); + + // test exact matching + assertTrue(pathMatcher.isMatch("test", "test")); + assertTrue(pathMatcher.isMatch("/test", "/test")); + assertTrue(pathMatcher.isMatch("http://example.org", "http://example.org")); // SPR-14141 + assertFalse(pathMatcher.isMatch("/test.jpg", "test.jpg")); + assertFalse(pathMatcher.isMatch("test", "/test")); + assertFalse(pathMatcher.isMatch("/test", "test")); + + // test matching with ?'s + assertTrue(pathMatcher.isMatch("t?st", "test")); + assertTrue(pathMatcher.isMatch("??st", "test")); + assertTrue(pathMatcher.isMatch("tes?", "test")); + assertTrue(pathMatcher.isMatch("te??", "test")); + assertTrue(pathMatcher.isMatch("?es?", "test")); + assertFalse(pathMatcher.isMatch("tes?", "tes")); + assertFalse(pathMatcher.isMatch("tes?", "testt")); + assertFalse(pathMatcher.isMatch("tes?", "tsst")); + + // test matching with *'s + assertTrue(pathMatcher.isMatch("*", "test")); + assertTrue(pathMatcher.isMatch("test*", "test")); + assertTrue(pathMatcher.isMatch("test*", "testTest")); + assertTrue(pathMatcher.isMatch("test/*", "test/Test")); + assertTrue(pathMatcher.isMatch("test/*", "test/t")); + assertTrue(pathMatcher.isMatch("test/*", "test/")); + assertTrue(pathMatcher.isMatch("*test*", "AnothertestTest")); + assertTrue(pathMatcher.isMatch("*test", "Anothertest")); + assertTrue(pathMatcher.isMatch("*.*", "test.")); + assertTrue(pathMatcher.isMatch("*.*", "test.test")); + assertTrue(pathMatcher.isMatch("*.*", "test.test.test")); + assertTrue(pathMatcher.isMatch("test*aaa", "testblaaaa")); + assertFalse(pathMatcher.isMatch("test*", "tst")); + assertFalse(pathMatcher.isMatch("test*", "tsttest")); + assertFalse(pathMatcher.isMatch("test*", "test/")); + assertFalse(pathMatcher.isMatch("test*", "test/t")); + assertFalse(pathMatcher.isMatch("test/*", "test")); + assertFalse(pathMatcher.isMatch("*test*", "tsttst")); + assertFalse(pathMatcher.isMatch("*test", "tsttst")); + assertFalse(pathMatcher.isMatch("*.*", "tsttst")); + assertFalse(pathMatcher.isMatch("test*aaa", "test")); + assertFalse(pathMatcher.isMatch("test*aaa", "testblaaab")); + + // test matching with ?'s and /'s + assertTrue(pathMatcher.isMatch("/?", "/a")); + assertTrue(pathMatcher.isMatch("/?/a", "/a/a")); + assertTrue(pathMatcher.isMatch("/a/?", "/a/b")); + assertTrue(pathMatcher.isMatch("/??/a", "/aa/a")); + assertTrue(pathMatcher.isMatch("/a/??", "/a/bb")); + assertTrue(pathMatcher.isMatch("/?", "/a")); + + // test matching with **'s + assertTrue(pathMatcher.isMatch("/**", "/testing/testing")); + assertTrue(pathMatcher.isMatch("/*/**", "/testing/testing")); + assertTrue(pathMatcher.isMatch("/**/*", "/testing/testing")); + assertTrue(pathMatcher.isMatch("/bla/**/bla", "/bla/testing/testing/bla")); + assertTrue(pathMatcher.isMatch("/bla/**/bla", "/bla/testing/testing/bla/bla")); + assertTrue(pathMatcher.isMatch("/**/test", "/bla/bla/test")); + assertTrue(pathMatcher.isMatch("/bla/**/**/bla", "/bla/bla/bla/bla/bla/bla")); + assertTrue(pathMatcher.isMatch("/bla*bla/test", "/blaXXXbla/test")); + assertTrue(pathMatcher.isMatch("/*bla/test", "/XXXbla/test")); + assertFalse(pathMatcher.isMatch("/bla*bla/test", "/blaXXXbl/test")); + assertFalse(pathMatcher.isMatch("/*bla/test", "XXXblab/test")); + assertFalse(pathMatcher.isMatch("/*bla/test", "XXXbl/test")); + + assertFalse(pathMatcher.isMatch("/????", "/bala/bla")); + assertFalse(pathMatcher.isMatch("/**/*bla", "/bla/bla/bla/bbb")); + + assertTrue(pathMatcher.isMatch("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing/")); + assertTrue(pathMatcher.isMatch("/*bla*/**/bla/*", "/XXXblaXXXX/testing/testing/bla/testing")); + assertTrue(pathMatcher.isMatch("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing")); + assertTrue(pathMatcher.isMatch("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing.jpg")); + + assertTrue(pathMatcher.isMatch("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing/")); + assertTrue(pathMatcher.isMatch("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing")); + assertTrue(pathMatcher.isMatch("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing")); + assertFalse(pathMatcher.isMatch("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing/testing")); + + assertFalse(pathMatcher.isMatch("/x/x/**/bla", "/x/x/x/")); + + assertTrue(pathMatcher.isMatch("/foo/bar/**", "/foo/bar")); + + assertTrue(pathMatcher.isMatch("", "")); + + assertTrue(pathMatcher.isMatch("/foo/bar/**", "/foo/bar")); + assertTrue(pathMatcher.isMatch("/resource/1", "/resource/1")); + assertTrue(pathMatcher.isMatch("/resource/*", "/resource/1")); + assertTrue(pathMatcher.isMatch("/resource/*/", "/resource/1/")); + assertTrue(pathMatcher.isMatch("/top-resource/*/resource/*/sub-resource/*", "/top-resource/1/resource/2/sub-resource/3")); + assertTrue(pathMatcher.isMatch("/top-resource/*/resource/*/sub-resource/*", "/top-resource/999999/resource/8888888/sub-resource/77777777")); + assertTrue(pathMatcher.isMatch("/*/*/*/*/secret.html", "/this/is/protected/path/secret.html")); + assertTrue(pathMatcher.isMatch("/*/*/*/*/*.html", "/this/is/protected/path/secret.html")); + assertTrue(pathMatcher.isMatch("/*/*/*/*", "/this/is/protected/path")); + assertTrue(pathMatcher.isMatch("org/springframework/**/*.jsp", "org/springframework/web/views/hello.jsp")); + assertTrue(pathMatcher.isMatch("org/springframework/**/*.jsp", "org/springframework/web/default.jsp")); + assertTrue(pathMatcher.isMatch("org/springframework/**/*.jsp", "org/springframework/default.jsp")); + assertTrue(pathMatcher.isMatch("org/**/servlet/bla.jsp", "org/springframework/servlet/bla.jsp")); + assertTrue(pathMatcher.isMatch("org/**/servlet/bla.jsp", "org/springframework/testing/servlet/bla.jsp")); + assertTrue(pathMatcher.isMatch("org/**/servlet/bla.jsp", "org/servlet/bla.jsp")); + assertTrue(pathMatcher.isMatch("**/hello.jsp", "org/springframework/servlet/web/views/hello.jsp")); + assertTrue(pathMatcher.isMatch("**/**/hello.jsp", "org/springframework/servlet/web/views/hello.jsp")); + + assertFalse(pathMatcher.isMatch("/foo/bar/**", "/foo /bar")); + assertFalse(pathMatcher.isMatch("/foo/bar/**", "/foo /bar")); + assertFalse(pathMatcher.isMatch("/foo/bar/**", "/foo / bar")); + assertFalse(pathMatcher.isMatch("/foo/bar/**", " / foo / bar")); + assertFalse(pathMatcher.isMatch("org/**/servlet/bla.jsp", " org / servlet / bla . jsp")); + } + + @Test + public void isMatchWithCustomSeparator() { + final Builder.Matcher pathMatcher = new Builder().withPathSeparator(".").build(); + + assertTrue(pathMatcher.isMatch(".foo.bar.**", ".foo.bar")); + assertTrue(pathMatcher.isMatch(".resource.1", ".resource.1")); + assertTrue(pathMatcher.isMatch(".resource.*", ".resource.1")); + assertTrue(pathMatcher.isMatch(".resource.*.", ".resource.1.")); + assertTrue(pathMatcher.isMatch("org.springframework.**.*.jsp", "org.springframework.web.views.hello.jsp")); + assertTrue(pathMatcher.isMatch("org.springframework.**.*.jsp", "org.springframework.web.default.jsp")); + assertTrue(pathMatcher.isMatch("org.springframework.**.*.jsp", "org.springframework.default.jsp")); + assertTrue(pathMatcher.isMatch("org.**.servlet.bla.jsp", "org.springframework.servlet.bla.jsp")); + assertTrue(pathMatcher.isMatch("org.**.servlet.bla.jsp", "org.springframework.testing.servlet.bla.jsp")); + assertTrue(pathMatcher.isMatch("org.**.servlet.bla.jsp", "org.servlet.bla.jsp")); + assertTrue(pathMatcher.isMatch("http://example.org", "http://example.org")); + assertTrue(pathMatcher.isMatch("**.hello.jsp", "org.springframework.servlet.web.views.hello.jsp")); + assertTrue(pathMatcher.isMatch("**.**.hello.jsp", "org.springframework.servlet.web.views.hello.jsp")); + + // test matching with ?'s and .'s + assertTrue(pathMatcher.isMatch(".?", ".a")); + assertTrue(pathMatcher.isMatch(".?.a", ".a.a")); + assertTrue(pathMatcher.isMatch(".a.?", ".a.b")); + assertTrue(pathMatcher.isMatch(".??.a", ".aa.a")); + assertTrue(pathMatcher.isMatch(".a.??", ".a.bb")); + assertTrue(pathMatcher.isMatch(".?", ".a")); + + // test matching with **'s + assertTrue(pathMatcher.isMatch(".**", ".testing.testing")); + assertTrue(pathMatcher.isMatch(".*.**", ".testing.testing")); + assertTrue(pathMatcher.isMatch(".**.*", ".testing.testing")); + assertTrue(pathMatcher.isMatch(".bla.**.bla", ".bla.testing.testing.bla")); + assertTrue(pathMatcher.isMatch(".bla.**.bla", ".bla.testing.testing.bla.bla")); + assertTrue(pathMatcher.isMatch(".**.test", ".bla.bla.test")); + assertTrue(pathMatcher.isMatch(".bla.**.**.bla", ".bla.bla.bla.bla.bla.bla")); + assertFalse(pathMatcher.isMatch(".bla*bla.test", ".blaXXXbl.test")); + assertFalse(pathMatcher.isMatch(".*bla.test", "XXXblab.test")); + assertFalse(pathMatcher.isMatch(".*bla.test", "XXXbl.test")); + } + + @Test + public void isMatchWithIgnoreCase() { + final Builder.Matcher pathMatcher = new Builder().withIgnoreCase().build(); + + assertTrue(pathMatcher.isMatch("/foo/bar/**", "/FoO/baR")); + assertTrue(pathMatcher.isMatch("org/springframework/**/*.jsp", "ORG/SpringFramework/web/views/hello.JSP")); + assertTrue(pathMatcher.isMatch("org/**/servlet/bla.jsp", "Org/SERVLET/bla.jsp")); + assertTrue(pathMatcher.isMatch("/?", "/A")); + assertTrue(pathMatcher.isMatch("/?/a", "/a/A")); + assertTrue(pathMatcher.isMatch("/a/??", "/a/Bb")); + assertTrue(pathMatcher.isMatch("/?", "/a")); + assertTrue(pathMatcher.isMatch("/**", "/testing/teSting")); + assertTrue(pathMatcher.isMatch("/*/**", "/testing/testing")); + assertTrue(pathMatcher.isMatch("/**/*", "/tEsting/testinG")); + assertTrue(pathMatcher.isMatch("http://example.org", "HtTp://exAmple.org")); + assertTrue(pathMatcher.isMatch("HTTP://EXAMPLE.ORG", "HtTp://exAmple.org")); + } + + @Test + public void isMatchWithIgnoreCaseWithCustomPathSeparator() { + final Builder.Matcher pathMatcher = new Builder() + .withIgnoreCase() + .withPathSeparator(".").build(); + + assertTrue(pathMatcher.isMatch(".foo.bar.**", ".FoO.baR")); + assertTrue(pathMatcher.isMatch("org.springframework.**.*.jsp", "ORG.SpringFramework.web.views.hello.JSP")); + assertTrue(pathMatcher.isMatch("org.**.servlet.bla.jsp", "Org.SERVLET.bla.jsp")); + assertTrue(pathMatcher.isMatch(".?", ".A")); + assertTrue(pathMatcher.isMatch(".?.a", ".a.A")); + assertTrue(pathMatcher.isMatch(".a.??", ".a.Bb")); + assertTrue(pathMatcher.isMatch(".?", ".a")); + assertTrue(pathMatcher.isMatch(".**", ".testing.teSting")); + assertTrue(pathMatcher.isMatch(".*.**", ".testing.testing")); + assertTrue(pathMatcher.isMatch(".**.*", ".tEsting.testinG")); + assertTrue(pathMatcher.isMatch("http:..example.org", "HtTp:..exAmple.org")); + assertTrue(pathMatcher.isMatch("HTTP:..EXAMPLE.ORG", "HtTp:..exAmple.org")); + } + + @Test + public void isMatchWithMatchStart() { + final Builder.Matcher pathMatcher = new Builder().withMatchStart().build(); + + // test exact matching + assertTrue(pathMatcher.isMatch("test", "test")); + assertTrue(pathMatcher.isMatch("/test", "/test")); + assertFalse(pathMatcher.isMatch("/test.jpg", "test.jpg")); + assertFalse(pathMatcher.isMatch("test", "/test")); + assertFalse(pathMatcher.isMatch("/test", "test")); + + // test matching with ?'s + assertTrue(pathMatcher.isMatch("t?st", "test")); + assertTrue(pathMatcher.isMatch("??st", "test")); + assertTrue(pathMatcher.isMatch("tes?", "test")); + assertTrue(pathMatcher.isMatch("te??", "test")); + assertTrue(pathMatcher.isMatch("?es?", "test")); + assertFalse(pathMatcher.isMatch("tes?", "tes")); + assertFalse(pathMatcher.isMatch("tes?", "testt")); + assertFalse(pathMatcher.isMatch("tes?", "tsst")); + + // test matching with *'s + assertTrue(pathMatcher.isMatch("*", "test")); + assertTrue(pathMatcher.isMatch("test*", "test")); + assertTrue(pathMatcher.isMatch("test*", "testTest")); + assertTrue(pathMatcher.isMatch("test/*", "test/Test")); + assertTrue(pathMatcher.isMatch("test/*", "test/t")); + assertTrue(pathMatcher.isMatch("test/*", "test/")); + assertTrue(pathMatcher.isMatch("*test*", "AnothertestTest")); + assertTrue(pathMatcher.isMatch("*test", "Anothertest")); + assertTrue(pathMatcher.isMatch("*.*", "test.")); + assertTrue(pathMatcher.isMatch("*.*", "test.test")); + assertTrue(pathMatcher.isMatch("*.*", "test.test.test")); + assertTrue(pathMatcher.isMatch("test*aaa", "testblaaaa")); + assertFalse(pathMatcher.isMatch("test*", "tst")); + assertFalse(pathMatcher.isMatch("test*", "test/")); + assertFalse(pathMatcher.isMatch("test*", "tsttest")); + assertFalse(pathMatcher.isMatch("test*", "test/")); + assertFalse(pathMatcher.isMatch("test*", "test/t")); + assertTrue(pathMatcher.isMatch("test/*", "test")); + assertTrue(pathMatcher.isMatch("test/t*.txt", "test")); + assertFalse(pathMatcher.isMatch("*test*", "tsttst")); + assertFalse(pathMatcher.isMatch("*test", "tsttst")); + assertFalse(pathMatcher.isMatch("*.*", "tsttst")); + assertFalse(pathMatcher.isMatch("test*aaa", "test")); + assertFalse(pathMatcher.isMatch("test*aaa", "testblaaab")); + + // test matching with ?'s and /'s + assertTrue(pathMatcher.isMatch("/?", "/a")); + assertTrue(pathMatcher.isMatch("/?/a", "/a/a")); + assertTrue(pathMatcher.isMatch("/a/?", "/a/b")); + assertTrue(pathMatcher.isMatch("/??/a", "/aa/a")); + assertTrue(pathMatcher.isMatch("/a/??", "/a/bb")); + assertTrue(pathMatcher.isMatch("/?", "/a")); + + // test matching with **'s + assertTrue(pathMatcher.isMatch("/**", "/testing/testing")); + assertTrue(pathMatcher.isMatch("/*/**", "/testing/testing")); + assertTrue(pathMatcher.isMatch("/**/*", "/testing/testing")); + assertTrue(pathMatcher.isMatch("test*/**", "test/")); + assertTrue(pathMatcher.isMatch("test*/**", "test/t")); + assertTrue(pathMatcher.isMatch("/bla/**/bla", "/bla/testing/testing/bla")); + assertTrue(pathMatcher.isMatch("/bla/**/bla", "/bla/testing/testing/bla/bla")); + assertTrue(pathMatcher.isMatch("/**/test", "/bla/bla/test")); + assertTrue(pathMatcher.isMatch("/bla/**/**/bla", "/bla/bla/bla/bla/bla/bla")); + assertTrue(pathMatcher.isMatch("/bla*bla/test", "/blaXXXbla/test")); + assertTrue(pathMatcher.isMatch("/*bla/test", "/XXXbla/test")); + assertFalse(pathMatcher.isMatch("/bla*bla/test", "/blaXXXbl/test")); + assertFalse(pathMatcher.isMatch("/*bla/test", "XXXblab/test")); + assertFalse(pathMatcher.isMatch("/*bla/test", "XXXbl/test")); + + assertFalse(pathMatcher.isMatch("/????", "/bala/bla")); + assertTrue(pathMatcher.isMatch("/**/*bla", "/bla/bla/bla/bbb")); + + assertTrue(pathMatcher.isMatch("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing/")); + assertTrue(pathMatcher.isMatch("/*bla*/**/bla/*", "/XXXblaXXXX/testing/testing/bla/testing")); + assertTrue(pathMatcher.isMatch("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing")); + assertTrue(pathMatcher.isMatch("/*bla*/**/bla/**", "/XXXblaXXXX/testing/testing/bla/testing/testing.jpg")); + + assertTrue(pathMatcher.isMatch("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing/")); + assertTrue(pathMatcher.isMatch("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing")); + assertTrue(pathMatcher.isMatch("*bla*/**/bla/**", "XXXblaXXXX/testing/testing/bla/testing/testing")); + assertTrue(pathMatcher.isMatch("*bla*/**/bla/*", "XXXblaXXXX/testing/testing/bla/testing/testing")); + + assertTrue(pathMatcher.isMatch("/x/x/**/bla", "/x/x/x/")); + + assertTrue(pathMatcher.isMatch("", "")); + } + + public static class Builder { + + private boolean matchStart; + private boolean ignoreCase; + private String pathSeparator = GlobPathMatcher.DEFAULT_PATH_SEPARATOR; + + public Matcher build() { + return new Matcher(); + } + + public Builder withMatchStart() { + this.matchStart = true; + return this; + } + + public Builder withIgnoreCase() { + this.ignoreCase = true; + return this; + } + + public Builder withPathSeparator(String sep) { + this.pathSeparator = sep; + return this; + } + + private class Matcher { + public boolean isMatch(String pattern, String str) { + GlobPathMatcher matcher = new GlobPathMatcher(pattern, pathSeparator, !ignoreCase); + return matcher.matches(str, !matchStart); + } + } + } +} \ No newline at end of file diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/ReflectiveTest.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/ReflectiveTest.java new file mode 100644 index 00000000000..bd3235b3852 --- /dev/null +++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/ReflectiveTest.java @@ -0,0 +1,119 @@ +/* + * 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.gogo.runtime; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.*; + +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Converter; +import org.junit.Assert; +import org.junit.Test; + +public class ReflectiveTest { + + @Test + public void testDtoAccess() throws Exception { + InputStream in = new ByteArrayInputStream(new byte[0]); + OutputStream out = new ByteArrayOutputStream(); + CommandProcessorImpl processor = new CommandProcessorImpl(null); + Object result = Reflective.invoke(new CommandSessionImpl(processor, in, out, out), new TheDTO("foo"), "name", Collections.emptyList()); + assertEquals("foo", result); + } + + @Test + public void testArrayInvocation() throws Exception { + assertEquals(new Object[] { 1, "ab" }, invoke("test1", Arrays.asList(1, "ab"))); + assertEquals(new String[] { "1", "ab" }, invoke("test2", Arrays.asList(1, "ab"))); + + assertEquals(new Object[] { Arrays.asList(1, 2), "ab" }, invoke("test1", Arrays.asList(Arrays.asList(1, 2), "ab"))); + assertEquals(new Object[] { new Object[] { 1, 2 }, "ab" }, invoke("test1", Arrays.asList(new Object[] { 1, 2 }, "ab"))); + } + + @Test + public void testAddConverter() throws Exception { + InputStream in = new ByteArrayInputStream(new byte[0]); + OutputStream out = new ByteArrayOutputStream(); + CommandProcessorImpl processor = new CommandProcessorImpl(null); + Converter conv = new Converter() { + @Override + public Object convert(Class desiredType, Object in) { + return null; + } + @Override + public CharSequence format(Object target, int level, Converter escape) { + return null; + } + }; + Reflective.invoke(new CommandSessionImpl(processor, in, out, out), processor, "addConverter", + Collections.singletonList(conv)); + } + + static class Target { + public Object test1(CommandSession session, Object[] argv) { + return argv; + } + + public Object test2(CommandSession session, String[] argv) { + return argv; + } + + public Object test3(CommandSession session, Collection argv) { + return argv; + } + + public Object test4(CommandSession session, List argv) { + return argv; + } + } + + static Object invoke(String method, List args) throws Exception { + InputStream in = new ByteArrayInputStream(new byte[0]); + OutputStream out = new ByteArrayOutputStream(); + CommandProcessorImpl processor = new CommandProcessorImpl(null); + return Reflective.invoke(new CommandSessionImpl(processor, in, out, out), new Target(), method, args); + } + + static void assertEquals(Object o1, Object o2) { + assertEquals(null, o1, o2); + } + + static void assertEquals(String msg, Object o1, Object o2) { + if (o1 == null || o2 == null) { + Assert.assertEquals(msg, o1, o2); + } + else if (o1.getClass().isArray() && o2.getClass().isArray()) { + Assert.assertArrayEquals(msg, (Object[]) o1, (Object[]) o2); + } + else { + Assert.assertEquals(msg, o1, o2); + } + } + + static class TheDTO { + public String name; + + public TheDTO(String name) { + this.name = name; + } + } +} diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestCoercion.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestCoercion.java new file mode 100644 index 00000000000..02a1903cf6b --- /dev/null +++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestCoercion.java @@ -0,0 +1,262 @@ +/* + * 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.gogo.runtime; + +import java.util.Arrays; +import java.util.List; + +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Descriptor; +import org.apache.felix.service.command.Parameter; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +public class TestCoercion extends AbstractParserTest +{ + public boolean fBool(boolean t) + { + return t; + } + + public double fDouble(double x) + { + return x; + } + + public int fInt(int x) + { + return x; + } + + public long fLong(long x) + { + return x; + } + + // the presence of this method checks that it is not invoked instead of fInt(int) + public String fInt(Object[] x) + { + return "array"; + } + + public String fString(String s) + { + return s; + } + + @Test + public void testSimpleTypes() throws Exception + { + Context c = new Context(); + c.addCommand("fBool", this); + c.addCommand("fDouble", this); + c.addCommand("fInt", this); + c.addCommand("fLong", this); + c.addCommand("fString", this); + + assertEquals("fBool true", true, c.execute("fBool true")); + assertEquals("fBool 'false'", false, c.execute("fBool 'false'")); + + assertEquals("fDouble 11", 11.0, c.execute("fDouble 11")); + assertEquals("fDouble '11'", 11.0, c.execute("fDouble '11'")); + + assertEquals("fInt 22", 22, c.execute("fInt 22")); + assertEquals("fInt '23'", 23, c.execute("fInt '23'")); + assertEquals("fInt 1 2", "array", c.execute("fInt 1 2")); + + assertEquals("fLong 33", 33L, c.execute("fLong 33")); + assertEquals("fLong '34'", 34L, c.execute("fLong '34'")); + + assertEquals("fString wibble", "wibble", c.execute("fString wibble")); + assertEquals("fString 'wibble'", "wibble", c.execute("fString 'wibble'")); + + try + { + Object r = c.execute("fString "); + fail("too few args: expected IllegalArgumentException, got: " + r); + } + catch (IllegalArgumentException e) + { + } + + try + { + Object r = c.execute("fString a b"); + fail("too many args: expected IllegalArgumentException, got: " + r); + } + catch (IllegalArgumentException e) + { + } + + try + { + Object r = c.execute("fLong string"); + fail("wrong arg type: expected IllegalArgumentException, got: " + r); + } + catch (IllegalArgumentException e) + { + } + } + + public String bundles(Long id) + { + return "long"; + } + + public String bundles(String loc) + { + return "string"; + } + + public String mymethod(String loc) + { + return loc; + } + + public String methodarray(Object[] array) + { + return Arrays.toString(array); + } + + public String methodlist(List array) + { + return array.toString(); + } + + @Test + public void testBestCoercion() throws Exception + { + Context c = new Context(); + c.addCommand("bundles", this); + + assertEquals("bundles myloc", "string", c.execute("bundles myloc")); + assertEquals("bundles 1", "long", c.execute("bundles 1")); + assertEquals("bundles '1'", "string", c.execute("bundles '1'")); + } + + @Test + public void testNoCoercionToString() throws Exception + { + Context c = new Context(); + c.addCommand("mymethod", this); + + assertEquals("mymethod '1.10'", "1.10", c.execute("mymethod '1.10'")); + assertEquals("mymethod 1.10", "1.10", c.execute("mymethod 1.10")); + } + + @Test + public void testListToArray() throws Exception + { + Context c = new Context(); + c.addCommand("methodarray", this); + c.set("a", Arrays.asList(1, 3)); + + assertEquals("methodarray [1 3]", "[1, 3]", c.execute("methodarray $a")); + } + + @Test + public void testArrayToList() throws Exception + { + Context c = new Context(); + c.addCommand("methodlist", this); + c.set("a", new int[] { 1, 3 }); + + assertEquals("methodlist [1 3]", "[1, 3]", c.execute("methodlist $a")); + } + + @Descriptor("list all installed bundles") + public String p0( + @Descriptor("show location") @Parameter(names = { "-l", "--location" }, presentValue = "true", absentValue = "false") boolean showLoc, + @Descriptor("show symbolic name") @Parameter(names = { "-s", "--symbolicname" }, presentValue = "true", absentValue = "false") boolean showSymbolic) + { + return showLoc + ":" + showSymbolic; + } + + // function with session and parameter + public boolean p01( + CommandSession session, + @Parameter(names = { "-f", "--flag" }, presentValue = "true", absentValue = "false") boolean flag) + { + assertNotNull("session must not be null", session); + return flag; + } + + @Test + public void testParameter0() throws Exception + { + Context c = new Context(); + c.addCommand("p0", this); + c.addCommand("p01", this); + + assertEquals("p0", "false:false", c.execute("p0")); + assertEquals("p0 -l", "true:false", c.execute("p0 -l")); + assertEquals("p0 --location", "true:false", c.execute("p0 --location")); + assertEquals("p0 -l -s", "true:true", c.execute("p0 -l -s")); + assertEquals("p0 -s -l", "true:true", c.execute("p0 -s -l")); + try + { + Object r = c.execute("p0 wibble"); + fail("too many args: expected IllegalArgumentException, got: " + r); + } + catch (IllegalArgumentException e) + { + } + assertEquals("p01 -f", true, c.execute("p01 -f")); + } + + public String p1( + @Parameter(names = { "-p", "--parameter" }, absentValue = "absent") String parm1) + { + return parm1; + } + + @Test + public void testParameter1() throws Exception + { + Context c = new Context(); + c.addCommand("p1", this); + + assertEquals("no parameter", "absent", c.execute("p1")); + + // FELIX-2894 + assertEquals("correct parameter", "wibble", c.execute("p1 -p wibble")); + + try + { + Object r = c.execute("p1 -p"); + fail("missing parameter: expected IllegalArgumentException, got: " + r); + } + catch (IllegalArgumentException e) + { + } + + try + { + Object r = c.execute("p1 -X"); + fail("wrong parameter: expected IllegalArgumentException, got: " + r); + } + catch (IllegalArgumentException e) + { + } + } + +} diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestEvaluate.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestEvaluate.java new file mode 100644 index 00000000000..387e7f09dea --- /dev/null +++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestEvaluate.java @@ -0,0 +1,53 @@ +/* + * 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.gogo.runtime; + +import java.nio.file.Path; + +public class TestEvaluate implements Evaluate { + @Override + public Object eval(Token t) throws Exception { + return null; + } + + @Override + public Object get(String key) { + return null; + } + + @Override + public Object put(String key, Object value) { + return null; + } + + @Override + public Object expr(Token t) { + return null; + } + + @Override + public Path currentDir() { + return null; + } + + @Override + public ClassLoader classLoader() { + return getClass().getClassLoader(); + } +} diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser.java new file mode 100644 index 00000000000..90bf1483d7e --- /dev/null +++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser.java @@ -0,0 +1,523 @@ +/* + * 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.gogo.runtime; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; + +import org.apache.felix.service.command.Process; +import org.apache.felix.gogo.runtime.Parser.Pipeline; +import org.apache.felix.gogo.runtime.Parser.Program; +import org.apache.felix.gogo.runtime.Parser.Sequence; +import org.apache.felix.gogo.runtime.Parser.Statement; +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Function; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +public class TestParser extends AbstractParserTest +{ + int beentheredonethat = 0; + + @Test + public void testError() { + Context context = new Context(); + context.addCommand("gogo", new Function() { + @Override + public Object execute(CommandSession session, List arguments) throws Exception { + throw new Error(arguments.get(0).toString()); + }}, "error"); + + try { + context.execute("error bar"); + fail("Expected an exception"); + } catch (Throwable t) { + assertEquals("java.util.concurrent.ExecutionException: java.lang.Error: bar", t.toString()); + } + } + + @Test + public void testEvaluatation() throws Exception + { + Context c = new Context(); + c.addCommand("echo", this); + c.addCommand("capture", this); + + assertEquals("a", c.execute("echo a | capture")); + assertEquals("a", c.execute("(echo a) | capture")); + assertEquals("a", c.execute("((echo a)) | capture")); + } + + @Test + public void testUnknownCommand() throws Exception + { + Context c = new Context(); + try + { + c.execute("echo"); + fail("Execution should have failed due to missing command"); + } + catch (IllegalArgumentException e) + { + // expected + } + } + + @Test + public void testSpecialValues() throws Exception + { + Context c = new Context(); + assertEquals(false, c.execute("false")); + assertEquals(true, c.execute("true")); + assertEquals(null, c.execute("null")); + } + + @Test + public void testQuotes() throws Exception + { + Context c = new Context(); + c.addCommand("echo", this); + c.set("c", "a"); + + assertEquals("a b", c.execute("echo a b")); + assertEquals("a b", c.execute("echo 'a b'")); + assertEquals("a b", c.execute("echo \"a b\"")); + assertEquals("a b", c.execute("echo a b")); + assertEquals("a b", c.execute("echo 'a b'")); + assertEquals("a b", c.execute("echo \"a b\"")); + assertEquals("a b", c.execute("echo $c b")); + assertEquals("$c b", c.execute("echo '$c b'")); + assertEquals("a b", c.execute("echo \"$c b\"")); + assertEquals("a b", c.execute("echo ${c} b")); + assertEquals("${c} b", c.execute("echo '${c} b'")); + assertEquals("a b", c.execute("echo \"${c} b\"")); + assertEquals("aa", c.execute("echo $c$c")); + assertEquals("a ;a", c.execute("echo a\\ \\;a")); + assertEquals("baabab", c.execute("echo b${c}${c}b${c}b")); + + c.set("d", "a b "); + assertEquals("a b ", c.execute("echo \"$d\"")); + } + + @Test + public void testScope() throws Exception + { + Context c = new Context(); + c.addCommand("echo", this); + assertEquals("$a", c.execute("test:echo \\$a")); + assertEquals("file://poo", c.execute("test:echo file://poo")); + } + + @Test + public void testPipe() throws Exception + { + Context c = new Context(); + c.addCommand("echo", this); + c.addCommand("capture", this); + c.addCommand("grep", this); + c.addCommand("echoout", this); + c.execute("myecho = { echoout $args }"); + + // Disable file name generation to avoid escaping 'd.*' + c.currentDir(null); + + assertEquals("def", c.execute("echo def|grep d.*|capture")); + assertEquals("def", c.execute("echoout def|grep d.*|capture")); + assertEquals("def", c.execute("myecho def|grep d.*|capture")); + assertEquals("def", c.execute("(echoout abc; echoout def; echoout ghi)|grep d.*|capture")); + assertEquals("", c.execute("echoout def; echoout ghi | grep d.* | capture")); + assertEquals("hello world", c.execute("echo hello world|capture")); + assertEquals("defghi", c.execute("(echoout abc; echoout def; echoout ghi)|grep 'def|ghi'|capture")); + } + + @Test + public void testAssignment() throws Exception + { + Context c = new Context(); + c.addCommand("echo", this); + c.addCommand("grep", this); + assertEquals("a", c.execute("a = a; echo ${$a}")); + + assertEquals("hello", c.execute("echo hello")); + assertEquals("hello", c.execute("a = (echo hello)")); + //assertEquals("a", c.execute("a = a; echo $(echo a)")); // #p2 - no eval in var expansion + assertEquals("3", c.execute("a=3; echo $a")); + assertEquals("3", c.execute("a = 3; echo $a")); + assertEquals("a", c.execute("a = a; echo ${$a}")); + } + + @Test + public void testComment() throws Exception + { + Context c = new Context(); + c.addCommand("echo", this); + assertEquals("1", c.execute("echo 1 // hello")); + } + + @Test + public void testClosure() throws Exception + { + Context c = new Context(); + c.addCommand("echo", this); + c.addCommand("capture", this); + + assertEquals("a", c.execute("e = { echo $1 } ; e a b")); + assertEquals("b", c.execute("e = { echo $2 } ; e a b")); + assertEquals("b", c.execute("e = { eval $args } ; e echo b")); + assertEquals("ca b", c.execute("e = { echo c$args } ; e a b")); + assertEquals("c a b", c.execute("e = { echo c $args } ; e a b")); + assertEquals("ca b", c.execute("e = { echo c$args } ; e 'a b'")); + } + + @Test + public void testArray() throws Exception + { + Context c = new Context(); + c.set("echo", true); + assertEquals("http://www.aqute.biz?com=2&biz=1", + c.execute("['http://www.aqute.biz?com=2&biz=1'] get 0")); + assertEquals("{a=2, b=3}", c.execute("[a=2 b=3]").toString()); + assertEquals(3L, c.execute("[a=2 b=3] get b")); + assertEquals("[3, 4]", c.execute("[1 2 [3 4] 5 6] get 2").toString()); + assertEquals(5, c.execute("[1 2 [3 4] 5 6] size")); + } + + @Test + public void testParentheses() + { + Parser parser = new Parser("(a|b)|(d|f)"); + Program p = parser.program(); + assertEquals("a|b", ((Sequence) ((Statement) ((Pipeline) p.tokens().get(0)).tokens().get(0)).tokens().get(0)).program().toString()); + + parser = new Parser("grep (d.*)|grep (d|f)"); + p = parser.program(); + assertEquals("d.*", ((Sequence)((Statement) ((Pipeline) p.tokens().get(0)).tokens().get(0)).tokens().get(1)).program().toString()); + } + + @Test + public void testEcho() throws Exception + { + Context c = new Context(); + c.addCommand("echo", this); + c.execute("echo peter"); + } + + public void grep(String match) throws IOException + { + Pattern p = Pattern.compile(match); + BufferedReader rdr = new BufferedReader(new InputStreamReader(System.in)); + String s = rdr.readLine(); + while (s != null) + { + if (p.matcher(s).find()) + { + System.out.println(s); + } + s = rdr.readLine(); + } + } + + public String capture() throws IOException + { + StringWriter sw = new StringWriter(); + BufferedReader rdr = new BufferedReader(new InputStreamReader(System.in)); + String s = rdr.readLine(); + while (s != null) + { + sw.write(s); + s = rdr.readLine(); + } + return sw.toString(); + } + + @Test + public void testVars() throws Exception + { + Context c = new Context(); + c.addCommand("echo", this); + + assertEquals("", c.execute("echo ${very.likely.that.this.does.not.exist}")); + assertNotNull(c.execute("echo ${java.shell.name}")); + assertEquals("a", c.execute("a = a; echo ${a}")); + } + + @Test + public void testFunny() throws Exception + { + Context c = new Context(); + c.addCommand("echo", this); + assertEquals("a", c.execute("echo a") + ""); + assertEquals("a", c.execute("eval (echo echo) a") + ""); + //assertEquals("a", c.execute("((echo echo) echo) (echo a)") + ""); + assertEquals("3", c.execute("[a=2 (echo b)=(echo 3)] get b").toString()); + } + + public CharSequence echo(Object args[]) + { + if (args == null) + { + return ""; + } + + StringBuilder sb = new StringBuilder(); + for (Object arg : args) + { + if (arg != null) + { + if (sb.length() > 0) + sb.append(' '); + sb.append(arg); + } + } + return sb.toString(); + } + + public void echoout(Object args[]) + { + System.out.println(echo(args)); + } + + @Test + public void testContext() throws Exception + { + Context c = new Context(); + c.addCommand("ls", this); + beentheredonethat = 0; + c.execute("ls"); + assertEquals(1, beentheredonethat); + + beentheredonethat = 0; + c.execute("ls 10"); + assertEquals(10, beentheredonethat); + + beentheredonethat = 0; + c.execute("ls a b c d e f g h i j"); + assertEquals(10, beentheredonethat); + + beentheredonethat = 0; + Integer result = (Integer) c.execute("ls (ls 5)"); + assertEquals(10, beentheredonethat); + assertEquals((Integer) 5, result); + } + + public void ls() + { + beentheredonethat++; + System.out.println("ls(): Yes!"); + } + + public int ls(int onoff) + { + beentheredonethat += onoff; + System.out.println("ls(int) " + onoff); + return onoff; + } + + public void ls(Object args[]) + { + beentheredonethat = args.length; + System.out.print("ls(Object[]) ["); + for (Object i : args) + { + System.out.print(i + " "); + } + System.out.println("]"); + } + + @Test + public void testProgram() + { + Program x = new Parser("abc def|ghi jkl;mno pqr|stu vwx").program(); + Pipeline p0 = (Pipeline) x.tokens().get(0); + Statement s00 = (Statement) p0.tokens().get(0); + assertEquals("|", p0.tokens().get(1).toString()); + Statement s01 = (Statement) p0.tokens().get(2); + assertEquals(";", x.tokens().get(1).toString()); + Pipeline p1 = (Pipeline) x.tokens().get(2); + Statement s10 = (Statement) p1.tokens().get(0); + assertEquals("|", p1.tokens().get(1).toString()); + Statement s11 = (Statement) p1.tokens().get(2); + assertEquals("abc", s00.tokens().get(0).toString()); + assertEquals("def", s00.tokens().get(1).toString()); + assertEquals("ghi", s01.tokens().get(0).toString()); + assertEquals("jkl", s01.tokens().get(1).toString()); + assertEquals("mno", s10.tokens().get(0).toString()); + assertEquals("pqr", s10.tokens().get(1).toString()); + assertEquals("stu", s11.tokens().get(0).toString()); + assertEquals("vwx", s11.tokens().get(1).toString()); + } + + @Test + public void testStatements() + { + Program x = new Parser("abc def|ghi jkl|mno pqr").program(); + Pipeline p0 = (Pipeline) x.tokens().get(0); + Statement s00 = (Statement) p0.tokens().get(0); + Statement s01 = (Statement) p0.tokens().get(2); + Statement s02 = (Statement) p0.tokens().get(4); + assertEquals("abc", s00.tokens().get(0).toString()); + assertEquals("def", s00.tokens().get(1).toString()); + assertEquals("ghi", s01.tokens().get(0).toString()); + assertEquals("jkl", s01.tokens().get(1).toString()); + assertEquals("mno", s02.tokens().get(0).toString()); + assertEquals("pqr", s02.tokens().get(1).toString()); + } + + @Test + public void testPipeRedir() + { + Program x = new Parser("abc def|&ghi").program(); + Pipeline p0 = (Pipeline) x.tokens().get(0); + Statement s00 = (Statement) p0.tokens().get(0); + assertEquals("|&", p0.tokens().get(1).toString()); + Statement s01 = (Statement) p0.tokens().get(2); + assertEquals("abc", s00.tokens().get(0).toString()); + assertEquals("def", s00.tokens().get(1).toString()); + assertEquals("ghi", s01.tokens().get(0).toString()); + } + + @Test + public void testPipeAndOr() + { + Program x = new Parser("abc|def&&ghi || jkl").program(); + Pipeline p0 = (Pipeline) x.tokens().get(0); + Statement s00 = (Statement) p0.tokens().get(0); + assertEquals("|", p0.tokens().get(1).toString()); + Statement s01 = (Statement) p0.tokens().get(2); + assertEquals("&&", x.tokens().get(1).toString()); + Statement s1 = (Statement) x.tokens().get(2); + assertEquals("||", x.tokens().get(3).toString()); + Statement s2 = (Statement) x.tokens().get(4); + assertEquals("abc", s00.tokens().get(0).toString()); + assertEquals("def", s01.tokens().get(0).toString()); + assertEquals("ghi", s1.tokens().get(0).toString()); + assertEquals("jkl", s2.tokens().get(0).toString()); + } + + @Test + public void testBackground() { + Program x = new Parser("echo foo&echo bar").program(); + Statement s0 = (Statement) x.tokens().get(0); + assertEquals("&", x.tokens().get(1).toString()); + Statement s1 = (Statement) x.tokens().get(2); + assertEquals("echo", s0.tokens().get(0).toString()); + assertEquals("foo", s0.tokens().get(1).toString()); + assertEquals("echo", s1.tokens().get(0).toString()); + assertEquals("bar", s1.tokens().get(1).toString()); + } + + @Test + public void testRedir() { + Program x = new Parser("echo foo&>bar").program(); + Statement s0 = (Statement) x.tokens().get(0); + assertEquals("echo", s0.tokens().get(0).toString()); + assertEquals("foo", s0.tokens().get(1).toString()); + assertEquals("&>", s0.redirections().get(0).toString()); + assertEquals("bar", s0.redirections().get(1).toString()); + + x = new Parser("echo foo1>bar").program(); + s0 = (Statement) x.tokens().get(0); + assertEquals("echo", s0.tokens().get(0).toString()); + assertEquals("foo1", s0.tokens().get(1).toString()); + assertEquals(">", s0.redirections().get(0).toString()); + assertEquals("bar", s0.redirections().get(1).toString()); + + x = new Parser("echo foo 1>bar").program(); + s0 = (Statement) x.tokens().get(0); + assertEquals("echo", s0.tokens().get(0).toString()); + assertEquals("foo", s0.tokens().get(1).toString()); + assertEquals("1>", s0.redirections().get(0).toString()); + assertEquals("bar", s0.redirections().get(1).toString()); + } + + @Test + public void testClosingSquareBracket() + { + expectSyntaxError("{ a } }"); + expectSyntaxError("a }"); + expectSyntaxError("}"); + expectSyntaxError("{ a } ]"); + expectSyntaxError("a ]"); + expectSyntaxError("]"); + } + + private void expectSyntaxError(String txt) { + try { + new Parser(txt).program(); + fail("Expected a SyntaxError to be thrown"); + } catch (SyntaxError e) { + // ok + } + } + + @Test + public void testSimpleValue() + { + Program p = new Parser( + "abc def.ghi http://www.osgi.org?abc=\\&x=1 [1,2,3] {{{{{{{xyz}}}}}}} (immediate) {'{{{{{'} {\\{} 'abc{}'") + .program(); + List x = ((Statement) p.tokens().get(0)).tokens(); + assertEquals("abc", x.get(0).toString()); + assertEquals("def.ghi", x.get(1).toString()); + assertEquals("http://www.osgi.org?abc=\\&x=1", x.get(2).toString()); + assertEquals("[1,2,3]", x.get(3).toString()); + assertEquals("{{{{{{{xyz}}}}}}}", x.get(4).toString()); + assertEquals("(immediate)", x.get(5).toString()); + assertEquals("{'{{{{{'}", x.get(6).toString()); + assertEquals("{\\{}", x.get(7).toString()); + assertEquals("'abc{}'", x.get(8).toString()); + } + + @Test + public void testIsTty() throws Exception + { + Context c = new Context(); + c.addCommand("istty", this); + c.addCommand("echo", this); + assertEquals(true, c.execute("istty 1")); + assertEquals(false, c.execute("$(istty 1)")); + } + + public boolean istty(CommandSession session, int fd) + { + return Process.Utils.current().isTty(fd); + } + + void each(CommandSession session, Collection list, Function closure) + throws Exception + { + List args = new ArrayList<>(); + args.add(null); + for (Object x : list) + { + args.set(0, x); + closure.execute(session, args); + } + } + +} diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser2.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser2.java new file mode 100644 index 00000000000..207dcd81b6e --- /dev/null +++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser2.java @@ -0,0 +1,120 @@ +/* + * 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.gogo.runtime; + +import java.io.EOFException; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/* + * Test features of the new parser/tokenizer, many of which are not supported + * by the original parser. + */ +public class TestParser2 extends AbstractParserTest +{ + @Test + public void testComment() throws Exception + { + Context c = new Context(); + c.addCommand("echo", this); + + c.currentDir(null); + + assertEquals("file://wibble#tag", c.execute("echo file://wibble#tag")); +//CHANGE assertEquals("file:", c.execute("echo file: //wibble#tag")); + + assertEquals("PWD/*.java", c.execute("echo PWD/*.java")); + try + { + c.execute("echo PWD /*.java"); + fail("expected EOFException"); + } + catch (EOFException e) + { + // expected + } + + assertEquals("ok", c.execute("// can't quote\necho ok\n")); + + // quote in comment in closure + assertEquals("ok", c.execute("x = { // can't quote\necho ok\n}; x")); + assertEquals("ok", c.execute("x = {\n// can't quote\necho ok\n}; x")); +//CHANGE assertEquals("ok", c.execute("x = {// can't quote\necho ok\n}; x")); + } + + @Test + public void testCoercion() throws Exception + { + Context c = new Context(); + c.addCommand("echo", this); + // FELIX-2432 + assertEquals("null x", c.execute("echo $expandsToNull x")); + } + + @Test + public void testStringExecution() throws Exception + { + Context c = new Context(); + c.addCommand("echo", this); + c.addCommand("new", this); + + // FELIX-2433 + assertEquals("helloworld", c.execute("echo \"$(echo hello)world\"")); + + // FELIX-1473 - allow method calls on String objects + assertEquals("hello", c.execute("cmd = echo; eval $cmd hello")); + assertEquals(4, c.execute("'four' length")); + try { + c.execute("four length"); + fail("expected: command not found: four"); + } catch (IllegalArgumentException e) { + } + + // check CharSequence types are preserved + Object b = c.execute("b = new java.lang.StringBuilder"); + assertTrue(b instanceof StringBuilder); + assertEquals(b, c.execute("c = $b")); + } + + public CharSequence echo(Object args[]) + { + if (args == null) + { + return "null args!"; + } + + StringBuilder sb = new StringBuilder(); + for (Object arg : args) + { + if (sb.length() > 0) + sb.append(' '); + sb.append(String.valueOf(arg)); + } + return sb.toString(); + } + + public Object _new(String className) throws Exception { + return Class.forName(className).newInstance(); + } + +} diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser3.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser3.java new file mode 100644 index 00000000000..f9988150f0c --- /dev/null +++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser3.java @@ -0,0 +1,62 @@ +/* + * 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.gogo.runtime; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/* + * Test features of the new parser/tokenizer, many of which are not supported + * by the original parser. + */ +public class TestParser3 extends AbstractParserTest +{ + @Test + public void testArithmetic() throws Exception + { + Context c = new Context(); + c.addCommand("echo", this); + + assertEquals("10d", c.execute("echo %(2*(3+2))d")); + assertEquals(3L, c.execute("%(1+2)")); + + c.set("a", 2L); + assertEquals(3L, c.execute("%(a+=1)")); + assertEquals(3L, c.get("a")); + } + + public CharSequence echo(Object args[]) + { + if (args == null) + { + return "null args!"; + } + + StringBuilder sb = new StringBuilder(); + for (Object arg : args) + { + if (sb.length() > 0) + sb.append(' '); + sb.append(String.valueOf(arg)); + } + return sb.toString(); + } + +} diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser4.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser4.java new file mode 100644 index 00000000000..e7d9d441fd1 --- /dev/null +++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser4.java @@ -0,0 +1,222 @@ +/* + * 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.gogo.runtime; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/* + * Test features of the new parser/tokenizer, many of which are not supported + * by the original parser. + */ +public class TestParser4 extends AbstractParserTest +{ + public void testPipeErr() throws Exception + { + Context c = new Context(); + c.addCommand("echo", this); + c.addCommand("echoerr", this); + c.addCommand("tac", this); + + assertEquals("hello\n", c.execute("echo hello| tac")); + assertEquals("hello\n", c.execute("echoerr hello|& tac")); + } + + @Test + public void testRedir() throws Exception { + Context c = new Context(); + c.addCommand("echo", this); + c.addCommand("tac", this); + + Path path = Paths.get("target/tmp"); + Files.createDirectories(path); + c.currentDir(path); + + Files.deleteIfExists(path.resolve("foo")); + assertEquals("hello\n", c.execute("echo hello>foo | tac")); + assertEquals("hello\n", new String(Files.readAllBytes(path.resolve("foo")))); + + Files.deleteIfExists(path.resolve("foo2")); + assertEquals("hello\n", c.execute("echo hello>\\\nfoo2 | tac")); + } + + @Test + public void testRedirInput() throws Exception + { + Context c = new Context(); + c.addCommand("echo", this); + c.addCommand("tac", this); + c.addCommand("cat", this); + + Path path = Paths.get("target/tmp"); + Files.createDirectories(path); + c.currentDir(path); + + Files.deleteIfExists(path.resolve("fooa")); + Files.deleteIfExists(path.resolve("foob")); + c.execute("echo a>fooa"); + c.execute("echo b>foob"); + assertEquals("a\nb\n", c.execute("cat fooa"); + c.execute("echo b>foob"); + assertEquals("foo\na\nb\n", c.execute("echo foo | cat $a"); + assertEquals("bar\n", c.execute("cat <$a | tac")); + + // Empty var + try { + c.execute("echo bar > $b"); + fail("Expected IOException"); + } catch (IOException e) { + } + + try { + c.execute("cat < $b"); + fail("Expected IOException"); + } catch (IOException e) { + } + + // Array var + c.execute("c = [ ar1 ar2 ]"); + c.execute("echo bar > $c"); + assertEquals("bar\nbar\n", c.execute("cat <$c | tac")); + } + + @Test + public void testHereString() throws Exception + { + Context c = new Context(); + c.addCommand("echo", this); + c.addCommand("tac", this); + c.addCommand("cat", this); + + c.execute("a=foo"); + assertEquals("foo\n", c.execute("cat <<< $a | tac")); + + c.execute("c = [ ar1 ar2 ]"); + assertEquals("ar1 ar2\n", c.execute("cat <<<$c | tac")); + } + + @Test + public void testHereDoc() throws Exception + { + Context c = new Context(); + c.addCommand("echo", this); + c.addCommand("tac", this); + c.addCommand("cat", this); + + assertEquals("bar\nbaz\n", c.execute("cat <= 0) { + sw.write(buf, 0, len); + } + return sw.toString(); + } + +} diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestTokenizer.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestTokenizer.java new file mode 100644 index 00000000000..d41d2617b7d --- /dev/null +++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestTokenizer.java @@ -0,0 +1,560 @@ +/* + * 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.gogo.runtime; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.PrintStream; +import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.felix.gogo.runtime.threadio.ThreadIOImpl; +import org.junit.Before; +import org.junit.Test; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TestTokenizer +{ + private final Map vars = new HashMap<>(); + private final Evaluate evaluate; + private Path currentDir = null; + + public TestTokenizer() + { + evaluate = new TestEvaluate() + { + public Object eval(Token t) { + throw new UnsupportedOperationException("eval not implemented."); + } + + public Object get(String key) + { + return vars.get(key); + } + + public Object put(String key, Object value) + { + return vars.put(key, value); + } + + public Object expr(Token t) { + throw new UnsupportedOperationException("expr not implemented."); + } + + public Path currentDir() { + return currentDir; + } + }; + } + + @Before + public void setUp() { + currentDir = null; + vars.clear(); + } + + @Test + public void testHello() throws Exception + { + testHello("hello world\n"); + testHello("hello world\n\n"); // multiple \n reduced to single Token.NL + testHello("hello world\r\n"); // \r\n -> \n + + // escapes + + testHello("hello \\\nworld\n"); + try + { + testHello("hello\\u20world\n"); + fail("bad unicode accepted"); + } + catch (SyntaxError e) + { + // expected + } + + // whitespace and comments + + testHello(" hello world \n "); + testHello("hello world // comment\n\n"); + testHello("hello world #\\ comment\n\n"); + testHello("// comment\nhello world\n"); + testHello("// comment ?\\ \nhello world\n"); + testHello("hello /*\n * comment\n */ world\n"); + } + + // hello world + private void testHello(CharSequence text) { + Tokenizer t = new Tokenizer(text); + assertEquals("hello", t.next().toString()); + assertEquals("world", t.next().toString()); + assertEquals("\n", t.next().toString()); + assertNull(t.next()); + } + + @Test + public void testString() throws Exception + { + testString("'single $quote' \"double $quote\"\n"); + } + + // 'single quote' "double quote" + private void testString(CharSequence text) { + Tokenizer t = new Tokenizer(text); + assertEquals("'single $quote'", t.next().toString()); + assertEquals("\"double $quote\"", t.next().toString()); + assertEquals("\n", t.next().toString()); + assertNull(t.next()); + } + + @Test + public void testClosure() throws Exception + { + testClosure2("x = { echo '}' $args //comment's\n}\n"); + testClosure2("x={ echo '}' $args //comment's\n}\n"); + token1("{ echo \\{ $args \n}"); + token1("{ echo \\} $args \n}"); + } + + // + // x = {echo $args}; + // + private void testClosure2(CharSequence text) { + Tokenizer t = new Tokenizer(text); + assertEquals("x", t.next().toString()); + assertEquals("=", t.next().toString()); + assertEquals("{", t.next().toString()); + assertEquals("echo", t.next().toString()); + assertEquals("'}'", t.next().toString()); + assertEquals("$args", t.next().toString()); + assertEquals("\n", t.next().toString()); + assertEquals("}", t.next().toString()); + assertEquals("\n", t.next().toString()); + assertEquals(null, t.next()); + } + + private void token1(CharSequence text) { + Tokenizer t = new Tokenizer(text); + assertEquals("{", t.next().toString()); + assertEquals("echo", t.next().toString()); + t.next(); + assertEquals("$args", t.next().toString()); + assertEquals("\n", t.next().toString()); + assertEquals("}", t.next().toString()); + assertNull(t.next()); + } + + @Test + public void testJavaArray() throws Exception + { + vars.put("a", new Object[] { "b", "c"}); + + assertEquals(Arrays.asList("B", "C"), expand("${(U)a}")); + } + + @Test + public void testRawVariable() throws Exception + { + vars.put("a1", Arrays.asList("a", 1)); + + assertSame(vars.get("a1"), expand("$a1")); + assertNotSame(vars.get("a1"), expand("${a1}")); + assertEquals(vars.get("a1"), expand("${a1}")); + } + + @Test + public void testSubscripts() throws Exception + { + Map map = new LinkedHashMap<>(); + map.put("a1", "baz"); + map.put("a2", "bar"); + map.put("b1", "foo"); + + vars.put("key", "a1"); + vars.put("map", map); + + assertEquals("baz", expand("${map[a1]}")); + assertEquals("baz", expand("${map[$key]}")); + assertEquals("az", expand("${map[a1][1,-0]}")); + assertEquals("AZ", expand("${(U)map[a1][1,3]}")); + assertEquals(map, expand("${map}")); + assertEquals("baz bar foo", expand("\"${map}\"")); + assertEquals("baz bar foo", expand("\"${map[*]}\"")); + assertEquals(Arrays.asList("baz", "bar", "foo"), expand("\"${map[@]}\"")); + assertEquals(Arrays.asList("a1", "a2", "b1"), expand("\"${(k)map[@]}\"")); + assertEquals(Arrays.asList("a1", "baz", "a2", "bar", "b1", "foo"), expand("\"${(kv)map[@]}\"")); + assertEquals(Arrays.asList("a2", "bar"), expand("${${(kv)map[@]}[2,4]}")); + assertEquals(Arrays.asList("a2", "bar"), expand("${${(kv)=map}[2,4]}")); + + // TODO: test subscripts on array resulting in a single element + } + + @Test + public void testBraces() throws Exception { + assertEquals(Arrays.asList("1", "3"), expand("{1..3..2}")); + assertEquals(Arrays.asList("3", "1"), expand("{1..3..-2}")); + assertEquals(Arrays.asList("1", "3"), expand("{3..1..-2}")); + assertEquals(Arrays.asList("3", "1"), expand("{3..1..2}")); + + assertEquals(Arrays.asList("1", "2", "3"), expand("{1..3}")); + assertEquals(Arrays.asList("a1b", "a2b", "a3b"), expand("a{1..3}b")); + + assertEquals(Arrays.asList("a1b", "a2b", "a3b"), expand("a{1,2,3}b")); + + assertEquals(Arrays.asList("a1b", "a2b", "a3b"), expand("a{1,2,3}b")); + assertEquals(Arrays.asList("a1b", "a,b", "a3b"), expand("a{1,',',3}b")); + + currentDir = Paths.get("."); + try { + expand("a{1,*,3}b"); + fail("Expected exception"); + } catch (Exception e) { + assertEquals("no matches found: a*b", e.getMessage()); + } finally { + currentDir = null; + } + + vars.put("a", "1"); + assertEquals(Arrays.asList("a1b", "a\nb", "a3b"), expand("a{$a,$'\\n',3}b")); + assertEquals(Arrays.asList("ab1*z", "ab2*z", "arz"), expand("a{b{1..2}'*',r}z")); + + } + + @Test + public void testJoinSplit() throws Exception { + vars.put("array", Arrays.asList("a", "b", "c")); + vars.put("string", "a\n\nb\nc"); + vars.put("str", "such a bad bad trip"); + + assertEquals("a:b:c", expand("${(j.:.)array}")); + assertEquals("a\nb\nc", expand("${(pj.\\n.)array}")); + assertEquals("a\nb\nc", expand("${(F)array}")); + + assertEquals(Arrays.asList("a", "b", "c"), expand("${(f)string}")); + assertEquals(Arrays.asList("a\n\n", "\nc"), expand("${(s:b:)string}")); + + assertEquals("a:b:c", expand("${(Fj':')array}")); + + assertEquals("a bad such trip", expand("${(j' ')${(s' 'uo)str}}")); + } + + @Test + public void testParamFlag() throws Exception { + vars.put("foo", "bar"); + vars.put("bar", "baz"); + + assertEquals("bar", expand("${${foo}}")); + assertEquals("baz", expand("${(P)foo}")); + assertEquals("baz", expand("${(P)${foo}}")); + } + + @Test + public void testCaseFlags() throws Exception { + vars.put("foo", "bAr"); + + assertEquals("bAr", expand("${foo}")); + assertEquals("bar", expand("${(L)foo}")); + assertEquals("BAR", expand("${(U)${foo}}")); + assertEquals("Bar", expand("${(C)${foo}}")); + } + + @Test + public void testQuotes() throws Exception { + vars.put("foo", "\"{a'}\\b"); + vars.put("q1", "\"foo\""); + + assertEquals("\\\"\\{a\\'\\}\\\\b", expand("${(q)foo}")); + assertEquals("'\"{a'\\''}\\b'", expand("${(qq)foo}")); + assertEquals("\"\\\"{a'}\\\\b\"", expand("${(qqq)foo}")); + assertEquals("$'\"{a\\'}\\b'", expand("${(qqqq)foo}")); + assertEquals("'\"{a'\\''}\\b'", expand("${(q-)foo}")); + + assertEquals("foo", expand("${(Q)q1}")); + } + + @Test + public void testChars() throws Exception { + List array = new ArrayList<>(); + for (int i = 0; i < 64; i++) { + array.add(i); + } + vars.put("array", array); + + assertEquals(Arrays.asList("^@", "^A", "^B", "^C", "^D", "^E", "^F", "^G", + "^H", "^I", "^J", "^K", "^L", "^M", "^N", "^O", + "^P", "^Q", "^R", "^S", "^T", "^U", "^V", "^W", + "^X", "^Y", "^Z", "^\\[", "^\\\\", "^\\]", "^^", "^_", + "\\ ", "\\!", "\\\"", "\\#", "\\$", "\\%", "\\&", "\\'", + "\\(", "\\)", "\\*", "+", ",", "-", ".", "/", + "0", "1", "2", "3", "4", "5", "6", "7", + "8", "9", ":", "\\;", "\\<", "\\=", "\\>", "\\?"), expand("${(qV#)array}")); + } + + @Test + public void testSorting() throws Exception { + vars.put("array", Arrays.asList("foo1", "foo02", "foo2", "fOo3", "Foo20", "foo23")); + + assertEquals(Arrays.asList("Foo20", "fOo3", "foo02", "foo1", "foo2", "foo23"), expand("${(o)array}")); + assertEquals(Arrays.asList("foo23", "foo2", "foo1", "foo02", "fOo3", "Foo20"), expand("${(O)array}")); + assertEquals(Arrays.asList("foo02", "foo1", "foo2", "Foo20", "foo23", "fOo3"), expand("${(oi)array}")); + assertEquals(Arrays.asList("fOo3", "foo23", "Foo20", "foo2", "foo1", "foo02"), expand("${(Oi)array}")); + assertEquals(Arrays.asList("foo1", "foo02", "foo2", "fOo3", "Foo20", "foo23"), expand("${(oa)array}")); + assertEquals(Arrays.asList("foo23", "Foo20", "fOo3", "foo2", "foo02", "foo1"), expand("${(Oa)array}")); + assertEquals(Arrays.asList("foo1", "foo02", "foo2", "fOo3", "Foo20", "foo23"), expand("${(oia)array}")); + assertEquals(Arrays.asList("foo23", "Foo20", "fOo3", "foo2", "foo02", "foo1"), expand("${(Oia)array}")); + assertEquals(Arrays.asList("Foo20", "fOo3", "foo1", "foo02", "foo2", "foo23"), expand("${(on)array}")); + assertEquals(Arrays.asList("foo23", "foo2", "foo02", "foo1", "fOo3", "Foo20"), expand("${(On)array}")); + assertEquals(Arrays.asList("foo1", "foo02", "foo2", "fOo3", "Foo20", "foo23"), expand("${(oin)array}")); + assertEquals(Arrays.asList("foo23", "Foo20", "fOo3", "foo2", "foo02", "foo1"), expand("${(Oin)array}")); + } + + @Test + public void testPatterns() throws Exception { + vars.put("foo", "twinkle twinkle little star"); + vars.put("sub", "t*e"); + vars.put("sb", "*e"); + vars.put("rep", "spy"); + + assertEquals("spynkle spynkle little star", expand("${(G)foo/'twi'/$rep}")); + + assertEquals("twinkle twinkle little star", expand("${foo/${sub}/${rep}}")); + assertEquals("spy twinkle little star", expand("${foo/${~sub}/${rep}}")); + assertEquals("spy star", expand("${foo//${~sub}/${rep}}")); + assertEquals("spy spy lispy star", expand("${(G)foo/${~sub}/${rep}}")); + assertEquals("spy star", expand("${(G)foo//${~sub}/${rep}}")); + assertEquals("spy twinkle little star", expand("${foo/t${~sb}/${rep}}")); + } + + @Test + public void testExpand() throws Exception + { + final URI home = new URI("/home/derek"); + final File pwd = new File("/tmp"); + final String user = "derek"; + + vars.put("HOME", home); + vars.put("PWD", pwd); + vars.put("USER", user); + vars.put(user, "Derek Baum"); + + // quote removal + assertEquals("hello", expand("hello").toString()); + assertEquals("hello", expand("'hello'")); + assertEquals("\"hello\"", expand("'\"hello\"'")); + assertEquals("hello", expand("\"hello\"")); + assertEquals("'hello'", expand("\"'hello'\"")); + + // escapes + assertEquals("hello\\w", expand("hello\\\\w")); + assertEquals("hellow", expand("hello\\w")); + assertEquals("hello\\w", expand("\"hello\\\\w\"")); + assertEquals("hello\\w", expand("\"hello\\w\"")); + assertEquals("hello\\\\w", expand("'hello\\\\w'")); +//CHANGE assertEquals("hello", expand("he\\\nllo")); + assertEquals("he\\llo", expand("'he\\llo'")); + assertEquals("he'llo", expand("'he'\\''llo'")); + assertEquals("he\"llo", expand("\"he\\\"llo\"")); + assertEquals("he'llo", expand("he\\'llo")); + assertEquals("he$llo", expand("\"he\\$llo\"")); + assertEquals("he\\'llo", expand("\"he\\'llo\"")); + assertEquals("hello\\w", expand("\"hello\\w\"")); + + // unicode + + // Note: we could use literal Unicode pound '£' instead of \u00a3 in next test. + // if above is not UK currency symbol, then your locale is not configured for UTF-8. + // Java on Macs cannot handle UTF-8 unless you explicitly set '-Dfile.encoding=UTF-8'. + assertEquals("pound\u00a3cent\u00a2", expand("$'pound\\u00a3cent\\u00a2'")); + assertEquals("euro\\u20ac", expand("$'euro\\\\u20ac'")); + assertEquals("euro\u20ac", expand("$'euro\\u20ac'")); + assertEquals("euro\u020a", expand("$'euro\\u20a'")); + assertEquals("euro\u020ag", expand("$'euro\\u20ag'")); + + // simple variable expansion - quoting or concatenation converts result to String + assertEquals(user, expand("$USER")); + assertEquals(home, expand("$HOME")); + assertEquals(home.toString(), expand("$HOME$W")); + assertEquals(pwd, expand("$PWD")); + assertEquals("$PWD", expand("'$PWD'")); + assertEquals("$PWD", expand("\\$PWD")); + assertEquals(pwd.toString(), expand("\"$PWD\"")); + assertEquals("W" + pwd, expand("W$PWD")); + assertEquals(pwd + user, expand("$PWD$USER")); + + // variable substitution ${NAME:-WORD} etc + assertNull(expand("$JAVA_HOME")); + assertEquals(user, expand("${USER}")); + assertEquals(user + "W", expand("${USER}W")); + assertEquals("java_home", expand("${JAVA_HOME:-java_home}")); + assertEquals(pwd, expand("${NOTSET:-$PWD}")); + assertNull(vars.get("JAVA_HOME")); + assertEquals("java_home", expand("${JAVA_HOME:=java_home}")); + assertEquals("java_home", vars.get("JAVA_HOME")); + assertEquals("java_home", expand("$JAVA_HOME")); + assertEquals("yes", expand("${JAVA_HOME:+yes}")); + assertNull(expand("${NOTSET:+yes}")); + assertEquals("", expand("\"${NOTSET:+yes}\"")); + try + { + expand("${NOTSET:?}"); + fail("expected 'not set' exception"); + } + catch (IllegalArgumentException e) + { + // expected + } + + // bad variable names + assertEquals("$ W", expand("$ W")); + assertEquals("$ {W}", expand("$ {W}")); + try + { + expand("${W }"); + fail("expected syntax error"); + } + catch (SyntaxError e) + { + // expected + } + + try { + expand("${USER\\\n:?}"); + } + catch (SyntaxError e) + { + // expected + } +//CHANGE assertEquals(user, expand("${US\\u0045R:?}")); + + // bash doesn't supported nested expansions + // gogo only supports them in the ${} syntax + assertEquals("Derek Baum", expand("${(P)$USER}")); + assertEquals("Derek Baum", expand("${${(P)USER}:-Derek Baum}")); + assertEquals("Derek Baum", expand("${${(P)USR}:-$derek}")); + assertEquals("derek", expand("${${USER}}")); + assertEquals("derek", expand("${${USER:-d}}")); + assertEquals("x", expand("${$USR:-x}")); + assertEquals("$" + user, expand("$$USER")); + } + + private Object expand(CharSequence word) throws Exception + { + return Expander.expand(word, evaluate); + } + + @Test + public void testParser() { + new Parser("// comment\n" + "a=\"who's there?\"; ps -ef;\n" + "ls | \n grep y\n").program(); + String p1 = "a=1 \\$b=2 c={closure}\n"; + new Parser(p1).program(); + new Parser("[" + p1 + "]").program(); + } + + // + // FELIX-4679 / FELIX-4671. + // + @Test + public void testScriptFelix4679() throws Exception + { + String script = "addcommand system (((${.context} bundles) 0) loadclass java.lang.System)"; + + PrintStream sout = new PrintStream(System.out) { + @Override + public void close() { + } + }; + PrintStream serr = new PrintStream(System.err) { + @Override + public void close() { + } + }; + ThreadIOImpl tio = new ThreadIOImpl(); + tio.start(); + + try + { + BundleContext bc = createMockContext(); + + CommandProcessorImpl processor = new CommandProcessorImpl(tio); + processor.addCommand("gogo", processor, "addcommand"); + processor.addConstant(".context", bc); + + CommandSessionImpl session = new CommandSessionImpl(processor, new ByteArrayInputStream(script.getBytes()), sout, serr); + + Closure c = new Closure(session, null, script); + assertNull(c.execute(session, null)); + } + finally + { + tio.stop(); + } + } + + @Test + public void testFelix5541() { + Tokenizer t = new Tokenizer("<"); + assertEquals("<", t.next().toString()); + assertNull(t.next()); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private BundleContext createMockContext() throws ClassNotFoundException + { + Bundle systemBundle = mock(Bundle.class); + when(systemBundle.loadClass(eq("java.lang.System"))).thenReturn((Class) System.class); + + BundleContext bc = mock(BundleContext.class); + when(bc.getBundles()).thenReturn(new Bundle[] { systemBundle }); + return bc; + } + + @Test + public void testHereDocMissing() { + try { + new Parser("a <<").statement(); + fail("Expected exception"); + } catch (EOFError e) { + assertEquals("foo\n", e.repair()); + } + try { + new Parser("a << foo\n").statement(); + fail("Expected exception"); + } catch (EOFError e) { + assertEquals("\nfoo\n", e.repair()); + } + new Parser("a << foo\n \nfoo\n").statement(); + } +} diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/expr/ExpressionTest.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/expr/ExpressionTest.java new file mode 100644 index 00000000000..3a882b3a918 --- /dev/null +++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/expr/ExpressionTest.java @@ -0,0 +1,60 @@ +/* + * 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.gogo.runtime.expr; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.felix.gogo.runtime.Expression; +import org.junit.Assert; +import org.junit.Test; + +public class ExpressionTest { + + @Test + public void testExpr() { + + Map variables = new HashMap<>(); + variables.put("a", 2d); + variables.put("b", 5L); + variables.put("c", 1L); + variables.put("d", 2L); + variables.put("s", " foo "); + variables.put("t", "bar"); + + Assert.assertEquals(4L, new Expression("c+=1, d+=2").eval(variables)); + + Assert.assertEquals(" foo ", new Expression("\" foo \"").eval()); + Assert.assertEquals(" foo bar", new Expression("s + t").eval(variables)); + Assert.assertEquals(1L, new Expression("s < t").eval(variables)); + Assert.assertEquals(1L, new Expression("s > t || t == \"bar\"").eval(variables)); + + Assert.assertEquals(3L, new Expression("a += 1").eval(variables)); + Assert.assertEquals(3L, variables.get("a")); + + Assert.assertEquals(30L, new Expression("10 + 20 | 30").eval()); + + Assert.assertEquals(8L, new Expression("a + b").eval(variables)); + Assert.assertEquals(3L, new Expression("if(a < b, a, b)").eval(variables)); + + Assert.assertEquals(16L, new Expression("2 + 2 << 2").eval()); + Assert.assertEquals(8L, new Expression("2 | 2 << 2").eval()); + } + +} diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/threadio/TestThreadIO.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/threadio/TestThreadIO.java new file mode 100644 index 00000000000..5ffe41737e0 --- /dev/null +++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/threadio/TestThreadIO.java @@ -0,0 +1,91 @@ +/* + * 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.gogo.runtime.threadio; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +public class TestThreadIO { + + /** + * Test if the threadio works in a nested fashion. We first push + * ten markers on the stack and print a message for each, capturing + * the output in a ByteArrayOutputStream. Then we pop them, also printing + * a message identifying the level. Then we verify the output for each level. + */ + @Test + public void testNested() + { + ThreadIOImpl tio = new ThreadIOImpl(); + tio.start(); + List list = new ArrayList<>(); + for (int i = 0; i < 10; i++) + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + list.add(out); + tio.setStreams(System.in, new PrintStream(out), System.err); + System.out.print("b" + i); + } + for (int i = 9; i >= 0; i--) + { + System.out.println("e" + i); + tio.close(); + } + tio.stop(); + for (int i = 0; i < 10; i++) + { + String message = list.get(i).toString().trim(); + Assert.assertEquals("b" + i + "e" + i, message); + } + } + + /** + * Simple test too see if the basics work. + */ + @Test + public void testSimple() + { + ThreadIOImpl tio = new ThreadIOImpl(); + tio.start(); + System.out.println("Hello World"); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + tio.setStreams(System.in, new PrintStream(out), new PrintStream(err)); + try + { + System.out.println("Simple Normal Message"); + System.err.println("Simple Error Message"); + } + finally + { + tio.close(); + } + tio.stop(); + String normal = out.toString().trim(); + //String error = err.toString().trim(); + Assert.assertEquals("Simple Normal Message", normal); + //assertEquals("Simple Error Message", error ); + System.out.println("Goodbye World"); + } +} diff --git a/gogo/shell/DEPENDENCIES b/gogo/shell/DEPENDENCIES new file mode 100644 index 00000000000..664b8ac354d --- /dev/null +++ b/gogo/shell/DEPENDENCIES @@ -0,0 +1,20 @@ +Apache Felix Gogo Shell +Copyright 2011 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +None. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 diff --git a/gogo/shell/LICENSE b/gogo/shell/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/gogo/shell/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/gogo/shell/NOTICE b/gogo/shell/NOTICE new file mode 100644 index 00000000000..4007bd35915 --- /dev/null +++ b/gogo/shell/NOTICE @@ -0,0 +1,6 @@ +Apache Felix Gogo Shell +Copyright 2011 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. diff --git a/gogo/shell/doc/changelog.txt b/gogo/shell/doc/changelog.txt new file mode 100644 index 00000000000..b2896e60a71 --- /dev/null +++ b/gogo/shell/doc/changelog.txt @@ -0,0 +1,109 @@ +Changes from 1.1.0 to 1.1.2 +---------------------------- +Improvement + [FELIX-5970] - Add requirement & capabilities model so gogo can be resolved + [FELIX-5999] - cleanup compiler warnings + [FELIX-6000] - cleanup compiler warnings + [FELIX-6003] - Add some resolver checks to make sure @RequireGogo annotation works for both jline and shell + [FELIX-6007] - create a gogo bom + +Changes from 1.0.0 to 1.1.0 +---------------------------- +New Feature + [FELIX-5835] - Upgrade to JDK 8 + [FELIX-5836] - Upgrade to OSGi r6 + +Improvement + [FELIX-5857] - Provide a context classloader on the session to help with class loading + +Changes from 0.12.0 to 1.0.0 +---------------------------- +New Feature + [FELIX-5272] - New gogo features + +Improvement + [FELIX-2536] - Gogo Shell should export org.apache.felix.gogo.options package + +Task + [FELIX-5378] - [gogo] Upgrade packages and bundle to 1.0.0 + +Changes from 0.10.0 to 0.12.0 +----------------------------- +** Improvement + * FELIX-5021 [GOGO] Use system bundle to find bundles + * FELIX-4529 [Gogo] Let gosh be configured by files contributed by a bundle fragment + * FELIX-3341 Simple csh-like Command History + * FELIX-3340 Allow the prompt to be a Function + +** Bug + * FELIX-4425 Short command in Gogo Shell not working with Java 8 + * FELIX-3706 gogo shell startup failure in busy system + * FELIX-3703 Race condition in gogo runtime activator + +Changes from 0.8.0 to 0.10.0 +---------------------------- + +** Improvement + * Added gosh_profile work around for issue with OSGi R4.3 API + ambiguity. + +Changes from 0.6.1 to 0.8.0 +--------------------------- + +** Bug + * [FELIX-2651] - [Gogo] MOTD formatting is broken under Windows + +** Improvement + * [FELIX-2661] - [Gogo] It should be easier to start Gogo shell + non-interactively + +** New Feature + * [FELIX-2767] - gogo telnet IP address + +Changes from 0.6.0 to 0.6.1 +--------------------------- + +** Bug + * [FELIX-2446] - [Gogo] The bundle context command is not used with a + scope in gosh_profile + * [FELIX-2477] - [gogo] shell procedural commands don't inherit closure + arguments + +** Improvement + * [FELIX-2445] - [Gogo] Default gosh_profile should be updated to use + system bundle to load java.lang.System + * [FELIX-2543] - [Gogo] Should avoid using System.getProperty() to get + configuration properties + +Gogo Shell 0.6.0 +---------------- + +** Bug + * [FELIX-1473] - [gogo] The syntax does not provide a way to call methods + on a string + * [FELIX-1474] - [gogo] result of commands is implicitly written to pipe + * [FELIX-1493] - [gogo] automatic expansion of $args in Closure stops + direct access to $args list + * [FELIX-2337] - [gogo] no way to access array[] elements produced by + assignment + * [FELIX-2375] - [gogo] when supplied args can't be coerced, the error + message prints the arg values, rather than their types + * [FELIX-2380] - [gogo] lock contention in piped writer when reader + doesn't read all input + +** Improvement + * [FELIX-1487] - Support for commands on multiple lines + * [FELIX-2328] - [gogo] tidy-up runtime to remove optional code etc + * [FELIX-2339] - [gogo] add support for running scripts + * [FELIX-2342] - [gogo] remove old felix command adaptor + +** New Feature + * [FELIX-2363] - [Gogo] Add annotations for creating commands with + optional and out-of-order arguments + +** Task + * [FELIX-1670] - [gogo] launcher bundle not required + * [FELIX-1889] - Gogo should depend on the official OSGi jars + * [FELIX-2334] - [Gogo] Use org.apache.felix as Maven groupId + * [FELIX-2367] - [Gogo] Use org.apache.felix namespace to avoid any + perceived legal issues diff --git a/gogo/shell/pom.xml b/gogo/shell/pom.xml new file mode 100644 index 00000000000..ee7d6618715 --- /dev/null +++ b/gogo/shell/pom.xml @@ -0,0 +1,94 @@ + + + + + 4.0.0 + + + org.apache.felix + gogo-parent + 6-SNAPSHOT + ../gogo-parent/pom.xml + + + bundle + Apache Felix Gogo Shell + org.apache.felix.gogo.shell + 1.1.3-SNAPSHOT + http://felix.apache.org/ + + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/gogo/shell/ + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/gogo/shell/ + https://svn.apache.org/repos/asf/felix/trunk/gogo/shell/ + + + + + org.osgi + osgi.annotation + + + org.osgi + osgi.core + + + org.apache.felix + org.apache.felix.gogo.runtime + 1.1.3-SNAPSHOT + + + junit + junit + + + org.mockito + mockito-core + + + + + + org.apache.felix + maven-bundle-plugin + + + + + + + + org.apache.rat + apache-rat-plugin + + false + true + true + + doc/* + maven-eclipse.xml + .checkstyle + .externalToolBuilders/* + src/main/resources/motd + + + + + + diff --git a/gogo/shell/src/main/java/org/apache/felix/gogo/options/Option.java b/gogo/shell/src/main/java/org/apache/felix/gogo/options/Option.java new file mode 100644 index 00000000000..227a8defffa --- /dev/null +++ b/gogo/shell/src/main/java/org/apache/felix/gogo/options/Option.java @@ -0,0 +1,168 @@ +/* + * 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.gogo.options; + +import java.util.List; + +public interface Option { + /** + * stop parsing on the first unknown option. This allows one parser to get its own options and + * then pass the remaining options to another parser. + * + * @param stopOnBadOption stopOnBadOption + * @return Option + */ + Option setStopOnBadOption(boolean stopOnBadOption); + + /** + * require options to precede args. Default is false, so options can appear between or after + * args. + * + * @param optionsFirst optionsFirst + * @return Option + */ + Option setOptionsFirst(boolean optionsFirst); + + /** + * parse arguments. If skipArgv0 is true, then parsing begins at arg1. This allows for commands + * where argv0 is the command name rather than a real argument. + * + * @param argv argv + * @param skipArg0 skipArg0 + * @return Option + */ + Option parse(List argv, boolean skipArg0); + + /** + * parse arguments. + * + * See {@link #parse(List, boolean)} + * + * @param argv the arg + * @return Option + */ + Option parse(List argv); + + /** + * parse arguments. + * + * See {@link #parse(List, boolean)} + * + * @param argv the arg + * @param skipArg0 skipArg0 + * @return Option + */ + Option parse(Object[] argv, boolean skipArg0); + + /** + * parse arguments. + * + * See {@link #parse(List, boolean)} + * + * @param argv argv + * @return Option + */ + Option parse(Object[] argv); + + /** + * test whether specified option has been explicitly set. + * + * @param name name + * @return boolean + */ + boolean isSet(String name); + + /** + * get value of named option. If multiple options given, this method returns the last one. Use + * {@link #getList(String)} to get all values. + * + * @param name the name + * @return String + * @throws IllegalArgumentException + * if value is not a String. + */ + String get(String name); + + /** + * get list of all values for named option. + * + * @param name the name + * @return empty list if option not given and no default specified. + * @throws IllegalArgumentException + * if all values are not Strings. + */ + List getList(String name); + + /** + * get value of named option as an Object. If multiple options given, this method returns the + * last one. Use {@link #getObjectList(String)} to get all values. + * + * @param name the name + * @return Object + */ + Object getObject(String name); + + /** + * get list of all Object values for named option. + * + * @param name the name + * @return List<Object> + */ + List getObjectList(String name); + + /** + * get value of named option as a Number. + * + * @param name the name + * @return int + * @throws IllegalArgumentException + * if argument is not a Number. + */ + int getNumber(String name); + + /** + * get remaining non-options args as Strings. + * + * @return List<String> + * @throws IllegalArgumentException + * if args are not Strings. + */ + List args(); + + /** + * get remaining non-options args as Objects. + * + * @return List<Object> + */ + List argObjects(); + + /** + * print usage message to System.err. + */ + void usage(); + + /** + * print specified usage error to System.err. You should explicitly throw the returned + * exception. + * + * @param error the error + * @return IllegalArgumentException + */ + IllegalArgumentException usageError(String error); +} diff --git a/gogo/shell/src/main/java/org/apache/felix/gogo/options/Options.java b/gogo/shell/src/main/java/org/apache/felix/gogo/options/Options.java new file mode 100644 index 00000000000..62ca296b67c --- /dev/null +++ b/gogo/shell/src/main/java/org/apache/felix/gogo/options/Options.java @@ -0,0 +1,637 @@ +/* + * 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.gogo.options; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Yet another GNU long options parser. This one is configured by parsing its Usage string. + */ +public class Options implements Option +{ + public static void main(String[] args) + { + final String[] usage = { + "test - test Options usage", + " text before Usage: is displayed when usage() is called and no error has occurred.", + " so can be used as a simple help message.", + "", + "Usage: testOptions [OPTION]... PATTERN [FILES]...", + " Output control: arbitary non-option text can be included.", + " -? --help show help", + " -c --count=COUNT show COUNT lines", + " -h --no-filename suppress the prefixing filename on output", + " -q --quiet, --silent suppress all normal output", + " --binary-files=TYPE assume that binary files are TYPE", + " TYPE is 'binary', 'text', or 'without-match'", + " -I equivalent to --binary-files=without-match", + " -d --directories=ACTION how to handle directories (default=skip)", + " ACTION is 'read', 'recurse', or 'skip'", + " -D --devices=ACTION how to handle devices, FIFOs and sockets", + " ACTION is 'read' or 'skip'", + " -R, -r --recursive equivalent to --directories=recurse" }; + + Option opt = Options.compile(usage).parse(args); + + if (opt.isSet("help")) + { + opt.usage(); // includes text before Usage: + return; + } + + if (opt.args().isEmpty()) + { + throw opt.usageError("PATTERN not specified"); + } + System.out.println(opt); + if (opt.isSet("count")) + { + System.out.println("count = " + opt.getNumber("count")); + } + System.out.println("--directories specified: " + opt.isSet("directories")); + System.out.println("directories=" + opt.get("directories")); + } + + public static final String NL = System.getProperty("line.separator", "\n"); + + // Note: need to double \ within "" + private static final String regex = "(?x)\\s*" + "(?:-([^-]))?" + // 1: short-opt-1 + "(?:,?\\s*-(\\w))?" + // 2: short-opt-2 + "(?:,?\\s*--(\\w[\\w-]*)(=\\w+)?)?" + // 3: long-opt-1 and 4:arg-1 + "(?:,?\\s*--(\\w[\\w-]*))?" + // 5: long-opt-2 + ".*?(?:\\(default=(.*)\\))?\\s*"; // 6: default + + private static final int GROUP_SHORT_OPT_1 = 1; + private static final int GROUP_SHORT_OPT_2 = 2; + private static final int GROUP_LONG_OPT_1 = 3; + private static final int GROUP_ARG_1 = 4; + private static final int GROUP_LONG_OPT_2 = 5; + private static final int GROUP_DEFAULT = 6; + + private final Pattern parser = Pattern.compile(regex); + private final Pattern uname = Pattern.compile("^Usage:\\s+(\\w+)"); + + private final Map unmodifiableOptSet; + private final Map unmodifiableOptArg; + private final Map optSet = new HashMap<>(); + private final Map optArg = new HashMap<>(); + + private final Map optName = new HashMap<>(); + private final Map optAlias = new HashMap<>(); + private final List xargs = new ArrayList<>(); + private List args = null; + + private static final String UNKNOWN = "unknown"; + private String usageName = UNKNOWN; + private int usageIndex = 0; + + private final String[] spec; + private final String[] gspec; + private final String defOpts; + private final String[] defArgs; + private PrintStream errStream = System.err; + private String error = null; + + private boolean optionsFirst = false; + private boolean stopOnBadOption = false; + + public static Option compile(String[] optSpec) + { + return new Options(optSpec, null, null); + } + + public static Option compile(String optSpec) + { + return compile(optSpec.split("\\n")); + } + + public static Option compile(String[] optSpec, Option gopt) + { + return new Options(optSpec, null, gopt); + } + + public static Option compile(String[] optSpec, String[] gspec) + { + return new Options(optSpec, gspec, null); + } + + public Option setStopOnBadOption(boolean stopOnBadOption) + { + this.stopOnBadOption = stopOnBadOption; + return this; + } + + public Option setOptionsFirst(boolean optionsFirst) + { + this.optionsFirst = optionsFirst; + return this; + } + + public boolean isSet(String name) + { + if (!optSet.containsKey(name)) + { + throw new IllegalArgumentException("option not defined in spec: " + name); + } + return optSet.get(name); + } + + public Object getObject(String name) + { + if (!optArg.containsKey(name)) + { + throw new IllegalArgumentException("option not defined with argument: " + name); + } + List list = getObjectList(name); + + return list.isEmpty() ? "" : list.get(list.size() - 1); + } + + @SuppressWarnings("unchecked") + public List getObjectList(String name) + { + List list; + Object arg = optArg.get(name); + + if (arg == null) + { + throw new IllegalArgumentException("option not defined with argument: " + name); + } + + if (arg instanceof String) + { // default value + list = new ArrayList<>(); + if (!"".equals(arg)) + { + list.add(arg); + } + } + else + { + list = (List) arg; + } + + return list; + } + + public List getList(String name) + { + ArrayList list = new ArrayList<>(); + for (Object o : getObjectList(name)) + { + try + { + list.add((String) o); + } + catch (ClassCastException e) + { + throw new IllegalArgumentException("option not String: " + name); + } + } + return list; + } + + @SuppressWarnings("unchecked") + private void addArg(String name, Object value) + { + List list; + Object arg = optArg.get(name); + + if (arg instanceof String) + { // default value + list = new ArrayList<>(); + optArg.put(name, list); + } + else + { + list = (List) arg; + } + + list.add(value); + } + + public String get(String name) + { + try + { + return (String) getObject(name); + } + catch (ClassCastException e) + { + throw new IllegalArgumentException("option not String: " + name); + } + } + + public int getNumber(String name) + { + String number = get(name); + try + { + if (number != null) + { + return Integer.parseInt(number); + } + return 0; + } + catch (NumberFormatException e) + { + throw new IllegalArgumentException("option '" + name + "' not Number: " + number); + } + } + + public List argObjects() + { + return xargs; + } + + public List args() + { + if (args == null) + { + args = new ArrayList<>(); + for (Object arg : xargs) + { + args.add(arg == null ? "null" : arg.toString()); + } + } + return args; + } + + public void usage() + { + StringBuilder buf = new StringBuilder(); + int index = 0; + + if (error != null) + { + buf.append(error); + buf.append(NL); + index = usageIndex; + } + + for (int i = index; i < spec.length; ++i) + { + buf.append(spec[i]); + buf.append(NL); + } + + String msg = buf.toString(); + + if (errStream != null) + { + errStream.print(msg); + } + } + + /** + * prints usage message and returns IllegalArgumentException, for you to throw. + */ + public IllegalArgumentException usageError(String s) + { + error = usageName + ": " + s; + usage(); + return new IllegalArgumentException(error); + } + + // internal constructor + private Options(String[] spec, String[] gspec, Option opt) + { + this.gspec = gspec; + Options gopt = (Options) opt; + + if (gspec == null && gopt == null) + { + this.spec = spec; + } + else + { + ArrayList list = new ArrayList<>(); + list.addAll(Arrays.asList(spec)); + list.addAll(Arrays.asList(gspec != null ? gspec : gopt.gspec)); + this.spec = list.toArray(new String[0]); + } + + Map myOptSet = new HashMap<>(); + Map myOptArg = new HashMap<>(); + + parseSpec(myOptSet, myOptArg); + + if (gopt != null) + { + for (Entry e : gopt.optSet.entrySet()) + { + if (e.getValue()) + { + myOptSet.put(e.getKey(), true); + } + } + + for (Entry e : gopt.optArg.entrySet()) + { + if (!"".equals(e.getValue())) + { + myOptArg.put(e.getKey(), e.getValue()); + } + } + + gopt.reset(); + } + + unmodifiableOptSet = Collections.unmodifiableMap(myOptSet); + unmodifiableOptArg = Collections.unmodifiableMap(myOptArg); + + defOpts = System.getenv(usageName.toUpperCase() + "_OPTS"); + defArgs = (defOpts != null) ? defOpts.split("\\s+") : new String[0]; + } + + /** + * parse option spec. + */ + private void parseSpec(Map myOptSet, Map myOptArg) + { + int index = 0; + for (String line : spec) + { + Matcher m = parser.matcher(line); + + if (m.matches()) + { + final String opt = m.group(GROUP_LONG_OPT_1); + final String name = (opt != null) ? opt : m.group(GROUP_SHORT_OPT_1); + + if (name != null) + { + if (myOptSet.containsKey(name)) + { + throw new IllegalArgumentException("duplicate option in spec: --" + name); + } + myOptSet.put(name, false); + } + + String dflt = (m.group(GROUP_DEFAULT) != null) ? m.group(GROUP_DEFAULT) : ""; + if (m.group(GROUP_ARG_1) != null) + myOptArg.put(opt, dflt); + + String opt2 = m.group(GROUP_LONG_OPT_2); + if (opt2 != null) + { + optAlias.put(opt2, opt); + myOptSet.put(opt2, false); + if (m.group(GROUP_ARG_1) != null) + { + myOptArg.put(opt2, ""); + } + } + + for (int i = 0; i < 2; ++i) + { + String sopt = m.group(i == 0 ? GROUP_SHORT_OPT_1 : GROUP_SHORT_OPT_2); + if (sopt != null) + { + if (optName.containsKey(sopt)) + { + throw new IllegalArgumentException("duplicate option in spec: -" + sopt); + } + optName.put(sopt, name); + } + } + } + + if (usageName == UNKNOWN) + { + Matcher u = uname.matcher(line); + if (u.find()) + { + usageName = u.group(1); + usageIndex = index; + } + } + + index++; + } + } + + private void reset() + { + optSet.clear(); + optSet.putAll(unmodifiableOptSet); + optArg.clear(); + optArg.putAll(unmodifiableOptArg); + xargs.clear(); + args = null; + error = null; + } + + public Option parse(Object[] argv) + { + return parse(argv, false); + } + + public Option parse(List argv) + { + return parse(argv, false); + } + + public Option parse(Object[] argv, boolean skipArg0) + { + if (null == argv) + { + throw new IllegalArgumentException("argv is null"); + } + return parse(Arrays.asList(argv), skipArg0); + } + + public Option parse(List argv, boolean skipArg0) + { + reset(); + List args = new ArrayList(Arrays.asList(defArgs)); + for (Object arg : argv) + { + if (skipArg0) + { + skipArg0 = false; + usageName = arg.toString(); + } + else + { + args.add(arg); + } + } + + String needArg = null; + String needOpt = null; + boolean endOpt = false; + + for (Object oarg : args) + { + String arg = oarg == null ? "null" : oarg.toString(); + + if (endOpt) + { + xargs.add(oarg); + } + else if (needArg != null) + { + addArg(needArg, oarg); + needArg = null; + needOpt = null; + } + else if (!arg.startsWith("-") || "-".equals(oarg)) + { + if (optionsFirst) + { + endOpt = true; + } + xargs.add(oarg); + } + else + { + if (arg.equals("--")) + { + endOpt = true; + } + else if (arg.startsWith("--")) + { + int eq = arg.indexOf("="); + String value = (eq == -1) ? null : arg.substring(eq + 1); + String name = arg.substring(2, ((eq == -1) ? arg.length() : eq)); + List names = new ArrayList<>(); + + if (optSet.containsKey(name)) + { + names.add(name); + } + else + { + for (String k : optSet.keySet()) + { + if (k.startsWith(name)) + names.add(k); + } + } + + switch (names.size()) + { + case 1: + name = names.get(0); + optSet.put(name, true); + if (optArg.containsKey(name)) + { + if (value != null) + addArg(name, value); + else + needArg = name; + } + else if (value != null) + { + throw usageError("option '--" + name + "' doesn't allow an argument"); + } + break; + + case 0: + if (stopOnBadOption) + { + endOpt = true; + xargs.add(oarg); + break; + } + else + throw usageError("invalid option '--" + name + "'"); + + default: + throw usageError("option '--" + name + "' is ambiguous: " + names); + } + } + else + { + for (int i = 1; i < arg.length(); i++) + { + String c = String.valueOf(arg.charAt(i)); + if (optName.containsKey(c)) + { + String name = optName.get(c); + optSet.put(name, true); + if (optArg.containsKey(name)) + { + int k = i + 1; + if (k < arg.length()) + { + addArg(name, arg.substring(k)); + } + else + { + needOpt = c; + needArg = name; + } + break; + } + } + else + { + if (stopOnBadOption) + { + xargs.add("-" + c); + endOpt = true; + } + else + throw usageError("invalid option '" + c + "'"); + } + } + } + } + } + + if (needArg != null) + { + String name = (needOpt != null) ? needOpt : "--" + needArg; + throw usageError("option '" + name + "' requires an argument"); + } + + // remove long option aliases + for (Entry alias : optAlias.entrySet()) + { + if (optSet.get(alias.getKey())) + { + optSet.put(alias.getValue(), true); + if (optArg.containsKey(alias.getKey())) + optArg.put(alias.getValue(), optArg.get(alias.getKey())); + } + optSet.remove(alias.getKey()); + optArg.remove(alias.getKey()); + } + + return this; + } + + @Override + public String toString() + { + return "isSet" + optSet + "\nArg" + optArg + "\nargs" + xargs; + } + +} diff --git a/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Activator.java b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Activator.java new file mode 100644 index 00000000000..f1125e5aab3 --- /dev/null +++ b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Activator.java @@ -0,0 +1,250 @@ +/* + * 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.gogo.shell; + +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.Arrays; +import java.util.Dictionary; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +import org.apache.felix.service.command.CommandProcessor; +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Converter; +import org.osgi.annotation.bundle.Header; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.util.tracker.ServiceTracker; + +@Header(name = Constants.BUNDLE_ACTIVATOR, value = "${@class}") +public class Activator implements BundleActivator +{ + private BundleContext context; + private ServiceTracker commandProcessorTracker; + private Set> regs; + + private volatile ExecutorService executor; + private volatile StartShellJob shellJob; + + public Activator() + { + regs = new HashSet<>(); + } + + public void start(BundleContext context) { + this.context = context; + this.commandProcessorTracker = createCommandProcessorTracker(); + this.commandProcessorTracker.open(); + } + + public void stop(BundleContext context) { + Set> currentRegs; + synchronized (regs) + { + currentRegs = new HashSet<>(regs); + regs.clear(); + } + + for (ServiceRegistration reg : currentRegs) + { + reg.unregister(); + } + + this.commandProcessorTracker.close(); + + stopShell(); + } + + private ServiceTracker createCommandProcessorTracker() + { + return new ServiceTracker(context, CommandProcessor.class, null) + { + @Override + public CommandProcessor addingService(ServiceReference reference) + { + CommandProcessor processor = super.addingService(reference); + startShell(context, processor); + return processor; + } + + @Override + public void removedService(ServiceReference reference, CommandProcessor service) + { + stopShell(); + super.removedService(reference, service); + } + }; + } + + private void startShell(BundleContext context, CommandProcessor processor) + { + Dictionary dict = new Hashtable<>(); + dict.put(CommandProcessor.COMMAND_SCOPE, "gogo"); + + Set> currentRegs = new HashSet<>(); + + // register converters + currentRegs.add(context.registerService(Converter.class.getName(), new Converters(context.getBundle(0).getBundleContext()), null)); + + // register commands + + dict.put(CommandProcessor.COMMAND_FUNCTION, Builtin.functions); + currentRegs.add(context.registerService(Builtin.class.getName(), new Builtin(), dict)); + + dict.put(CommandProcessor.COMMAND_FUNCTION, Procedural.functions); + currentRegs.add(context.registerService(Procedural.class.getName(), new Procedural(), dict)); + + dict.put(CommandProcessor.COMMAND_FUNCTION, Posix.functions); + currentRegs.add(context.registerService(Posix.class.getName(), new Posix(), dict)); + + dict.put(CommandProcessor.COMMAND_FUNCTION, Telnet.functions); + currentRegs.add(context.registerService(Telnet.class.getName(), new Telnet(processor), dict)); + + Shell shell = new Shell(context, processor); + dict.put(CommandProcessor.COMMAND_FUNCTION, Shell.functions); + currentRegs.add(context.registerService(Shell.class.getName(), shell, dict)); + + synchronized (regs) + { + regs.addAll(currentRegs); + currentRegs.clear(); + } + + // start shell on a separate thread... + executor = Executors.newSingleThreadExecutor(new ThreadFactory() { + @Override + public Thread newThread(Runnable runnable) { + return new Thread(runnable, "Gogo shell"); + } + }); + shellJob = new StartShellJob(context, processor); + executor.submit(shellJob); + } + + private void stopShell() + { + if (executor != null && !(executor.isShutdown() || executor.isTerminated())) + { + if (shellJob != null) + { + shellJob.terminate(); + } + executor.shutdown(); + + try + { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) + { + System.err.println("!!! FAILED TO STOP EXECUTOR !!!"); + Map allStackTraces = Thread.getAllStackTraces(); + for (Map.Entry entry : allStackTraces.entrySet()) + { + Thread t = entry.getKey(); + System.err.printf("Thread: %s (%s): %s\n", t.getName(), t.getState(), Arrays.toString(entry.getValue())); + } + } + } + catch (InterruptedException e) + { + // Restore administration... + Thread.currentThread().interrupt(); + } + executor = null; + } + } + + private static class StartShellJob implements Runnable + { + private final BundleContext context; + private final CommandProcessor processor; + private volatile CommandSession session; + private volatile Thread shellThread; + + + public StartShellJob(BundleContext context, CommandProcessor processor) + { + this.context = context; + this.processor = processor; + } + + public void run() + { + + shellThread = Thread.currentThread(); + session = processor.createSession(new FileInputStream(FileDescriptor.in), + new FileOutputStream(FileDescriptor.out), + new FileOutputStream(FileDescriptor.err)); + try + { + // wait for gosh command to be registered + for (int i = 0; (i < 100) && session.get("gogo:gosh") == null; ++i) + { + TimeUnit.MILLISECONDS.sleep(10); + } + + String args = context.getProperty("gosh.args"); + args = (args == null) ? "" : args; + session.execute("gosh --login " + args); + } + catch (InterruptedException e) + { + // Ok, back off... + Thread.currentThread().interrupt(); + } + catch (Exception e) + { + Object loc = session.get(".location"); + if (null == loc || !loc.toString().contains(":")) + { + loc = "gogo"; + } + + System.err.println(loc + ": " + e.getClass().getSimpleName() + ": " + e.getMessage()); + e.printStackTrace(); + } + finally + { + terminate(); + } + } + + public void terminate() + { + if (session != null) + { + session.close(); + session = null; + } + if (shellThread != null) { + shellThread.interrupt(); + } + } + } +} \ No newline at end of file diff --git a/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Builtin.java b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Builtin.java new file mode 100644 index 00000000000..177c3386f8c --- /dev/null +++ b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Builtin.java @@ -0,0 +1,598 @@ +/* + * 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.gogo.shell; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.Map.Entry; + +import org.apache.felix.gogo.options.Option; +import org.apache.felix.gogo.options.Options; +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Converter; + +/** + * gosh built-in commands. + */ +public class Builtin +{ + + static final String[] functions = { "format", "getopt", "new", "set", "tac", "type" }; + + private static final String[] packages = { "java.lang", "java.io", "java.net", + "java.util" }; + + public CharSequence format(CommandSession session) + { + return format(session, session.get("_")); // last result + } + + public CharSequence format(CommandSession session, Object arg) + { + CharSequence result = session.format(arg, Converter.INSPECT); + System.out.println(result); + return result; + } + + /** + * script access to Options. + * @param spec the spec + * @param args the args + * @return Option + */ + public Option getopt(List spec, Object[] args) + { + String[] optSpec = new String[spec.size()]; + for (int i = 0; i < optSpec.length; ++i) + { + optSpec[i] = spec.get(i).toString(); + } + return Options.compile(optSpec).parse(args); + } + + // FIXME: the "new" command should be provided by runtime, + // so it can leverage same argument coercion mechanism, used to invoke methods. + public Object _new(CommandSession session, Object name, Object[] argv) throws Exception + { + Class clazz = null; + + if (name instanceof Class) + { + clazz = (Class) name; + } + else + { + clazz = loadClass(session, name.toString()); + } + + for (Constructor c : clazz.getConstructors()) + { + Class[] types = c.getParameterTypes(); + if (types.length != argv.length) + { + continue; + } + + boolean match = true; + + for (int i = 0; i < argv.length; ++i) + { + if (!types[i].isAssignableFrom(argv[i].getClass())) + { + if (!types[i].isAssignableFrom(String.class)) + { + match = false; + break; + } + argv[i] = argv[i].toString(); + } + } + + if (!match) + { + continue; + } + + try + { + return c.newInstance(argv); + } + catch (InvocationTargetException ite) + { + Throwable cause = ite.getCause(); + if (cause instanceof Exception) + { + throw (Exception) cause; + } + throw ite; + } + } + + throw new IllegalArgumentException("can't coerce " + Arrays.asList(argv) + + " to any of " + Arrays.asList(clazz.getConstructors())); + } + + private Class loadClass(CommandSession session, String name) throws ClassNotFoundException + { + if (!name.contains(".")) + { + for (String p : packages) + { + String pkg = p + "." + name; + try + { + return Class.forName(pkg, true, session.classLoader()); + } + catch (ClassNotFoundException e) + { + } + } + } + return Class.forName(name, true, session.classLoader()); + } + + public void set(CommandSession session, String[] argv) { + final String[] usage = { + "set - show session variables", + "Usage: set [OPTIONS] [PREFIX]", + " -? --help show help", + " -a --all show all variables, including those starting with .", + " -x set xtrace option", + " +x unset xtrace option", + "If PREFIX given, then only show variable(s) starting with PREFIX" }; + + Option opt = Options.compile(usage).parse(argv); + + if (opt.isSet("help")) + { + opt.usage(); + return; + } + + List args = opt.args(); + String prefix = (args.isEmpty() ? "" : args.get(0)); + + if (opt.isSet("x")) + { + session.put("echo", true); + } + else if ("+x".equals(prefix)) + { + session.put("echo", null); + } + else + { + boolean all = opt.isSet("all"); + for (String key : new TreeSet<>(Shell.getVariables(session))) + { + if (!key.startsWith(prefix)) + continue; + + if (key.startsWith(".") && !(all || prefix.length() > 0)) + continue; + + Object target = session.get(key); + String type = null; + String value = null; + + if (target != null) + { + Class clazz = target.getClass(); + type = clazz.getSimpleName(); + value = target.toString(); + } + + String trunc = value == null || value.length() < 55 ? "" : "..."; + System.out.println(String.format("%-15.15s %-15s %.45s%s", type, key, + value, trunc)); + } + } + } + + public Object tac(CommandSession session, String[] argv) throws IOException + { + final String[] usage = { + "tac - capture stdin as String or List and optionally write to file.", + "Usage: tac [-al] [FILE]", " -a --append append to FILE", + " -l --list return List", + " -? --help show help" }; + + Option opt = Options.compile(usage).parse(argv); + + if (opt.isSet("help")) + { + opt.usage(); + return null; + } + + List args = opt.args(); + BufferedWriter fw = null; + + if (args.size() == 1) + { + String path = args.get(0); + File file = new File(Shell.cwd(session).resolve(path)); + fw = new BufferedWriter(new FileWriter(file, opt.isSet("append"))); + } + + StringWriter sw = new StringWriter(); + BufferedReader rdr = new BufferedReader(new InputStreamReader(System.in)); + + ArrayList list = null; + + if (opt.isSet("list")) + { + list = new ArrayList<>(); + } + + boolean first = true; + String s; + + while ((s = rdr.readLine()) != null) + { + if (list != null) + { + list.add(s); + } + else + { + if (!first) + { + sw.write(' '); + } + first = false; + sw.write(s); + } + + if (fw != null) + { + fw.write(s); + fw.newLine(); + } + } + + if (fw != null) + { + fw.close(); + } + + return list != null ? list : sw.toString(); + } + + // FIXME: expose API in runtime so type command doesn't have to duplicate the runtime + // command search strategy. + public boolean type(CommandSession session, String[] argv) throws Exception + { + final String[] usage = { "type - show command type", + "Usage: type [OPTIONS] [name[:]]", + " -a --all show all matches", + " -? --help show help", + " -q --quiet don't print anything, just return status", + " -s --scope=NAME list all commands in named scope", + " -t --types show full java type names" }; + + Option opt = Options.compile(usage).parse(argv); + List args = opt.args(); + + if (opt.isSet("help")) + { + opt.usage(); + return true; + } + + boolean all = opt.isSet("all"); + + String optScope = null; + if (opt.isSet("scope")) + { + optScope = opt.get("scope"); + } + + if (args.size() == 1) + { + String arg = args.get(0); + if (arg.endsWith(":")) + { + optScope = args.remove(0); + } + } + + if (optScope != null || (args.isEmpty() && all)) + { + Set snames = new TreeSet<>(); + + for (String sname : (getCommands(session))) + { + if ((optScope == null) || sname.startsWith(optScope)) + { + snames.add(sname); + } + } + + for (String sname : snames) + { + System.out.println(sname); + } + + return true; + } + + if (args.size() == 0) + { + Map scopes = new TreeMap<>(); + + for (String sname : getCommands(session)) + { + int colon = sname.indexOf(':'); + String scope = sname.substring(0, colon); + Integer count = scopes.get(scope); + if (count == null) + { + count = 0; + } + scopes.put(scope, ++count); + } + + for (Entry entry : scopes.entrySet()) + { + System.out.println(entry.getKey() + ":" + entry.getValue()); + } + + return true; + } + + final String name = args.get(0).toLowerCase(); + + final int colon = name.indexOf(':'); + final String MAIN = "_main"; // FIXME: must match Reflective.java + + StringBuilder buf = new StringBuilder(); + Set cmds = new LinkedHashSet<>(); + + // get all commands + if ((colon != -1) || (session.get(name) != null)) + { + cmds.add(name); + } + else if (session.get(MAIN) != null) + { + cmds.add(MAIN); + } + else + { + String path = session.get("SCOPE") != null ? session.get("SCOPE").toString() + : "*"; + + for (String s : path.split(":")) + { + if (s.equals("*")) + { + for (String sname : getCommands(session)) + { + if (sname.endsWith(":" + name)) + { + cmds.add(sname); + if (!all) + { + break; + } + } + } + } + else + { + String sname = s + ":" + name; + if (session.get(sname) != null) + { + cmds.add(sname); + if (!all) + { + break; + } + } + } + } + } + + for (String key : cmds) + { + Object target = session.get(key); + if (target == null) + { + continue; + } + + CharSequence source = getClosureSource(session, key); + + if (source != null) + { + buf.append(name); + buf.append(" is function {"); + buf.append(source); + buf.append("}"); + continue; + } + + for (Method m : getMethods(session, key)) + { + StringBuilder params = new StringBuilder(); + + for (Class type : m.getParameterTypes()) + { + if (params.length() > 0) + { + params.append(", "); + } + params.append(type.getSimpleName()); + } + + String rtype = m.getReturnType().getSimpleName(); + + if (buf.length() > 0) + { + buf.append("\n"); + } + + if (opt.isSet("types")) + { + String cname = m.getDeclaringClass().getName(); + buf.append(String.format("%s %s.%s(%s)", rtype, cname, m.getName(), + params)); + } + else + { + buf.append(String.format("%s is %s %s(%s)", name, rtype, key, params)); + } + } + } + + if (buf.length() > 0) + { + if (!opt.isSet("quiet")) + { + System.out.println(buf); + } + return true; + } + + if (!opt.isSet("quiet")) + { + System.err.println("type: " + name + " not found."); + } + + return false; + } + + /* + * the following methods depend on the internals of the runtime implementation. + * ideally, they should be available via some API. + */ + + @SuppressWarnings("unchecked") + static Set getCommands(CommandSession session) + { + return (Set) session.get(".commands"); + } + + private boolean isClosure(Object target) + { + return target.getClass().getSimpleName().equals("Closure"); + } + + private boolean isCommand(Object target) + { + return target.getClass().getSimpleName().equals("CommandProxy"); + } + + private CharSequence getClosureSource(CommandSession session, String name) + throws Exception + { + Object target = session.get(name); + + if (target == null) + { + return null; + } + + if (!isClosure(target)) + { + return null; + } + + Field sourceField = target.getClass().getDeclaredField("source"); + sourceField.setAccessible(true); + return (CharSequence) sourceField.get(target); + } + + private List getMethods(CommandSession session, String scmd) throws Exception + { + final int colon = scmd.indexOf(':'); + final String function = colon == -1 ? scmd : scmd.substring(colon + 1); + final String name = KEYWORDS.contains(function) ? ("_" + function) : function; + final String get = "get" + function; + final String is = "is" + function; + final String set = "set" + function; + final String MAIN = "_main"; // FIXME: must match Reflective.java + + Object target = session.get(scmd); + if (target == null) + { + return null; + } + + if (isClosure(target)) + { + return null; + } + + if (isCommand(target)) + { + Method method = target.getClass().getMethod("getTarget", (Class[])null); + method.setAccessible(true); + target = method.invoke(target, (Object[])null); + } + + ArrayList list = new ArrayList<>(); + Class tc = (target instanceof Class) ? (Class) target + : target.getClass(); + Method[] methods = tc.getMethods(); + + for (Method m : methods) + { + String mname = m.getName().toLowerCase(); + + if (mname.equals(name) || mname.equals(get) || mname.equals(set) + || mname.equals(is) || mname.equals(MAIN)) + { + list.add(m); + } + } + + return list; + } + + private final static Set KEYWORDS = new HashSet<>( + Arrays.asList("abstract", "continue", "for", "new", "switch", + "assert", "default", "goto", "package", "synchronized", "boolean", "do", + "if", "private", "this", "break", "double", "implements", "protected", + "throw", "byte", "else", "import", "public", "throws", "case", "enum", + "instanceof", "return", "transient", "catch", "extends", "int", "short", + "try", "char", "final", "interface", "static", "void", "class", + "finally", "long", "strictfp", "volatile", "const", "float", "native", + "super", "while")); + +} diff --git a/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Console.java b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Console.java new file mode 100644 index 00000000000..efba82062e5 --- /dev/null +++ b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Console.java @@ -0,0 +1,198 @@ +/* + * 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.gogo.shell; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; + +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Converter; +import org.apache.felix.service.command.Function; + +public class Console implements Runnable +{ + private final CommandSession session; + private final InputStream in; + private final PrintStream out; + private final History history; + private volatile boolean quit; + + public Console(CommandSession session, History history) + { + this.session = session; + in = session.getKeyboard(); + out = session.getConsole(); + this.history = history; + } + + public void run() + { + try + { + while (!Thread.currentThread().isInterrupted() && !quit) + { + CharSequence line = getLine(getPrompt()); + + if (line == null) + { + break; + } + + try + { + if (line.charAt(0) == '!' || line.charAt(0) == '^') + { + line = history.evaluate(line); + System.out.println(line); + } + + Object result = session.execute(line); + session.put("_", result); // set $_ to last result + + if (result != null + && !Boolean.FALSE.equals(session.get(".Gogo.format"))) + { + out.println(session.format(result, Converter.INSPECT)); + } + } + catch (Throwable e) + { + final String SESSION_CLOSED = "session is closed"; + if ((e instanceof IllegalStateException) && SESSION_CLOSED.equals(e.getMessage())) + { + // FIXME: we assume IllegalStateException is because the session is closed; + // but it may be for another reason, so we also check the message (yuk). + // It would be better if the RFC-147 API threw a unique exception, such as + // org.osgi.service.command.SessionClosedException + out.println("gosh: " + e); + quit = true; + } + + if (!quit) + { + session.put("exception", e); + Object loc = session.get(".location"); + + if (null == loc || !loc.toString().contains(":")) + { + loc = "gogo"; + } + + out.println(loc + ": " + e.getClass().getSimpleName() + ": " + e.getMessage()); + } + } + finally + { + this.history.append(line); + } + } + } + catch (Exception e) + { + if (!quit) + { + e.printStackTrace(); + } + } + } + + private String getPrompt() + { + Object prompt = session.get("prompt"); + if (prompt instanceof Function) + { + try + { + prompt = ((Function) prompt).execute(session, null); + } + catch (Exception e) + { + out.println(prompt + ": " + e.getClass().getSimpleName() + ": " + e.getMessage()); + prompt = null; + } + } + + if (prompt == null) + { + prompt = "g! "; + } + + return prompt.toString(); + } + + private CharSequence getLine(String prompt) throws IOException + { + StringBuilder sb = new StringBuilder(); + out.print(prompt); + + while (!quit) + { + out.flush(); + + int c = 0; + try + { + c = in.read(); + } + catch (IOException e) + { + if ("Stream closed".equals(e.getMessage())) { + quit = true; + } else { + throw e; + } + } + + switch (c) + { + case -1: + case 4: // EOT, ^D from telnet + quit = true; + break; + + case '\r': + break; + + case '\n': + if (sb.length() > 0) + { + return sb; + } + out.print(prompt); + break; + + case '\b': + if (sb.length() > 0) + { + out.print("\b \b"); + sb.deleteCharAt(sb.length() - 1); + } + break; + + default: + sb.append((char) c); + break; + } + } + + return null; + } + +} diff --git a/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Converters.java b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Converters.java new file mode 100644 index 00000000000..6007dd03db4 --- /dev/null +++ b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Converters.java @@ -0,0 +1,274 @@ +/* + * 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.gogo.shell; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; + +import org.apache.felix.service.command.Converter; +import org.apache.felix.service.command.Function; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.startlevel.BundleStartLevel; + +public class Converters implements Converter +{ + private final BundleContext context; + + public Converters(BundleContext context) + { + this.context = context; + } + + private CharSequence print(Bundle bundle) + { + // [ ID ] [STATE ] [ SL ] symname + int level = bundle.adapt(BundleStartLevel.class).getStartLevel(); + + return String.format("%5d|%-11s|%5d|%s (%s)", bundle.getBundleId(), + getState(bundle), level, bundle.getSymbolicName(), bundle.getVersion()); + } + + private CharSequence print(ServiceReference ref) + { + String spid = ""; + Object pid = ref.getProperty("service.pid"); + if (pid != null) + { + spid = pid.toString(); + } + + return String.format("%06d %3s %-40s %s", ref.getProperty("service.id"), + ref.getBundle().getBundleId(), + getShortNames((String[]) ref.getProperty("objectclass")), spid); + } + + private CharSequence getShortNames(String[] list) + { + StringBuilder sb = new StringBuilder(); + String del = ""; + for (String s : list) + { + sb.append(del).append(getShortName(s)); + del = " | "; + } + return sb; + } + + private CharSequence getShortName(String name) + { + int n = name.lastIndexOf('.'); + if (n < 0) + { + n = 0; + } + else + { + n++; + } + return name.subSequence(n, name.length()); + } + + private String getState(Bundle bundle) + { + switch (bundle.getState()) + { + case Bundle.ACTIVE: + return "Active"; + + case Bundle.INSTALLED: + return "Installed"; + + case Bundle.RESOLVED: + return "Resolved"; + + case Bundle.STARTING: + return "Starting"; + + case Bundle.STOPPING: + return "Stopping"; + + case Bundle.UNINSTALLED: + return "Uninstalled "; + } + return null; + } + + public Bundle bundle(Bundle i) + { + return i; + } + + public Object convert(Class desiredType, final Object in) throws Exception + { + if (desiredType == Bundle.class) + { + return convertBundle(in); + } + + if (desiredType == ServiceReference.class) + { + return convertServiceReference(in); + } + + if (desiredType == Class.class) + { + try + { + return Class.forName(in.toString()); + } + catch (ClassNotFoundException e) + { + return null; + } + } + + if (desiredType.isAssignableFrom(String.class) && in instanceof InputStream) + { + return read(((InputStream) in)); + } + + if (in instanceof Function && desiredType.isInterface() + && desiredType.getDeclaredMethods().length == 1) + { + return Proxy.newProxyInstance(desiredType.getClassLoader(), + new Class[] { desiredType }, new InvocationHandler() + { + Function command = ((Function) in); + + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable + { + return command.execute(null, Arrays.asList(args)); + } + }); + } + + return null; + } + + private Object convertServiceReference(Object in) throws InvalidSyntaxException + { + String s = in.toString(); + if (s.startsWith("(") && s.endsWith(")")) + { + ServiceReference refs[] = context.getServiceReferences((String) null, String.format( + "(|(service.id=%s)(service.pid=%s))", in, in)); + if (refs != null && refs.length > 0) + { + return refs[0]; + } + } + + ServiceReference refs[] = context.getServiceReferences((String) null, String.format( + "(|(service.id=%s)(service.pid=%s))", in, in)); + if (refs != null && refs.length > 0) + { + return refs[0]; + } + return null; + } + + private Object convertBundle(Object in) + { + String s = in.toString(); + try + { + long id = Long.parseLong(s); + return context.getBundle(id); + } + catch (NumberFormatException nfe) + { + // Ignore + } + + Bundle bundles[] = context.getBundles(); + for (Bundle b : bundles) + { + if (b.getLocation().equals(s)) + { + return b; + } + + if (b.getSymbolicName().equals(s)) + { + return b; + } + } + + return null; + } + + public CharSequence format(Object target, int level, Converter converter) + throws IOException + { + if (level == INSPECT && target instanceof InputStream) + { + return read(((InputStream) target)); + } + if (level == LINE && target instanceof Bundle) + { + return print((Bundle) target); + } + if (level == LINE && target instanceof ServiceReference) + { + return print((ServiceReference) target); + } + if (level == PART && target instanceof Bundle) + { + return ((Bundle) target).getSymbolicName(); + } + if (level == PART && target instanceof ServiceReference) + { + return getShortNames((String[]) ((ServiceReference) target).getProperty("objectclass")); + } + return null; + } + + private CharSequence read(InputStream in) throws IOException + { + int c; + StringBuffer sb = new StringBuffer(); + while ((c = in.read()) > 0) + { + if (c >= 32 && c <= 0x7F || c == '\n' || c == '\r') + { + sb.append((char) c); + } + else + { + String s = Integer.toHexString(c).toUpperCase(); + sb.append("\\"); + if (s.length() < 1) + { + sb.append(0); + } + sb.append(s); + } + } + return sb; + } + +} diff --git a/gogo/shell/src/main/java/org/apache/felix/gogo/shell/History.java b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/History.java new file mode 100644 index 00000000000..b1c264bb928 --- /dev/null +++ b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/History.java @@ -0,0 +1,177 @@ +/* + * 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.gogo.shell; + +import java.text.CharacterIterator; +import java.text.StringCharacterIterator; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.ListIterator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class History { + + private static final int SIZE_DEFAULT = 100; + + private LinkedList commands; + + private int limit; + + public History() { + this.limit = SIZE_DEFAULT; + this.commands = new LinkedList<>(); + } + + CharSequence evaluate(final CharSequence commandLine) { + + /* + *
      +         * hist = ( '!' spec ) | ( '^' subst ) .
      +         * spec = '!' ( '!' | idx | '?' find | string ) [ ':' [ 'a' | 'g' ] 's' regex ] . idx = [ '-' ] { 0..9 } .
      +         * find = string ( '?' | EOL ) .
      +         * subst = pat '^' repl '^' EOL .
      +         * regex = str pat str repl str EOL .
      +         * 
      + */ + + final CharacterIterator ci = new StringCharacterIterator(commandLine.toString()); + + String event; + char c = ci.current(); + if (c == '!') { + c = ci.next(); + if (c == '!') { + event = this.commands.getLast(); + ci.next(); + } else if ((c >= '0' && c <= '9') || c == '-') { + event = getCommand(ci); + } else if (c == '?') { + event = findContains(ci); + ci.next(); + } else { + ci.previous(); + event = findStartsWith(ci); + } + } else if (c == '^') { + event = subst(ci, c, false, this.commands.getLast()); + } else { + throw new IllegalArgumentException(commandLine + ": Unsupported event"); + } + + if (ci.current() == ':') { + c = ci.next(); + boolean global = (c == 'a' || c == 'g'); + if (global) { + c = ci.next(); + } + if (c == 's') { + event = subst(ci, ci.next(), global, event); + } + } + + return event; + } + + /** + * Returns the command history, oldest command first + */ + Iterator getHistory() { + return this.commands.iterator(); + } + + void append(final CharSequence commandLine) { + commands.add(commandLine.toString()); + if (commands.size() > this.limit) { + commands.removeFirst(); + } + } + + private String getCommand(final CharacterIterator ci) { + final StringBuilder s = new StringBuilder(); + char c = ci.current(); + do { + s.append(c); + c = ci.next(); + } while (c >= '0' && c <= '9'); + final int n = Integer.parseInt(s.toString()); + final int pos = ((n < 0) ? this.commands.size() : -1) + n; + if (pos >= 0 && pos < this.commands.size()) { + return this.commands.get(pos); + } + throw new IllegalArgumentException("!" + n + ": event not found"); + } + + private String findContains(final CharacterIterator ci) { + CharSequence part = findDelimiter(ci, '?'); + final ListIterator iter = this.commands.listIterator(this.commands.size()); + while (iter.hasPrevious()) { + String value = iter.previous(); + if (value.contains(part)) { + return value; + } + } + + throw new IllegalArgumentException("No command containing '" + part + "' in the history"); + } + + private String findStartsWith(final CharacterIterator ci) { + String part = findDelimiter(ci, ':').toString(); + final ListIterator iter2 = this.commands.listIterator(this.commands.size()); + while (iter2.hasPrevious()) { + String value = iter2.previous(); + if (value.startsWith(part)) { + return value; + } + } + + throw new IllegalArgumentException("No command containing '" + part + "' in the history"); + } + + private String subst(final CharacterIterator ci, final char delimiter, final boolean replaceAll, final String event) { + final String pattern = findDelimiter(ci, delimiter).toString(); + final String repl = findDelimiter(ci, delimiter).toString(); + if (pattern.length() == 0) { + throw new IllegalArgumentException(":s" + event + ": substitution failed"); + } + final Pattern regex = Pattern.compile(pattern); + final Matcher m = regex.matcher(event); + final StringBuffer res = new StringBuffer(); + + if (!m.find()) { + throw new IllegalArgumentException(":s" + event + ": substitution failed"); + } + do { + m.appendReplacement(res, repl); + } while (replaceAll && m.find()); + m.appendTail(res); + return res.toString(); + } + + private CharSequence findDelimiter(final CharacterIterator ci, char delimiter) { + final StringBuilder b = new StringBuilder(); + for (char c = ci.next(); c != CharacterIterator.DONE && c != delimiter; c = ci.next()) { + if (c == '\\') { + c = ci.next(); + } + b.append(c); + } + return b; + } +} diff --git a/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Posix.java b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Posix.java new file mode 100644 index 00000000000..eee356351cb --- /dev/null +++ b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Posix.java @@ -0,0 +1,203 @@ +/* + * 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.gogo.shell; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.URI; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.felix.gogo.options.Option; +import org.apache.felix.gogo.options.Options; +import org.apache.felix.service.command.CommandSession; + +/** + * Posix-like utilities. + * + * See http://www.opengroup.org/onlinepubs/009695399/utilities/contents.html + */ +public class Posix +{ + static final String[] functions = { "cat", "echo", "grep" }; + + public void cat(CommandSession session, String[] args) throws Exception + { + if (args.length == 0) + { + copy(System.in, System.out); + return; + } + + URI cwd = Shell.cwd(session); + + for (String arg : args) + { + copy(cwd.resolve(arg), System.out); + } + } + + public void echo(Object[] args) + { + StringBuilder buf = new StringBuilder(); + + if (args == null) + { + System.out.println("Null"); + return; + } + + for (Object arg : args) + { + if (buf.length() > 0) + buf.append(' '); + buf.append(String.valueOf(arg)); + } + + System.out.println(buf); + } + + public boolean grep(CommandSession session, String[] argv) throws IOException + { + final String[] usage = { + "grep - search for PATTERN in each FILE or standard input.", + "Usage: grep [OPTIONS] PATTERN [FILES]", + " -? --help show help", + " -i --ignore-case ignore case distinctions", + " -n --line-number prefix each line with line number within its input file", + " -q --quiet, --silent suppress all normal output", + " -v --invert-match select non-matching lines" }; + + Option opt = Options.compile(usage).parse(argv); + + if (opt.isSet("help")) + { + opt.usage(); + return true; + } + + List args = opt.args(); + + if (args.size() == 0) + { + throw opt.usageError("no pattern supplied."); + } + + String regex = args.remove(0); + if (opt.isSet("ignore-case")) + { + regex = "(?i)" + regex; + } + + if (args.isEmpty()) + { + args.add(null); + } + + StringBuilder buf = new StringBuilder(); + + if (args.size() > 1) + { + buf.append("%1$s:"); + } + + if (opt.isSet("line-number")) + { + buf.append("%2$s:"); + } + + buf.append("%3$s"); + String format = buf.toString(); + + Pattern pattern = Pattern.compile(regex); + boolean status = true; + boolean match = false; + + for (String arg : args) + { + InputStream in = null; + + try + { + URI cwd = Shell.cwd(session); + in = (arg == null) ? System.in : cwd.resolve(arg).toURL().openStream(); + + BufferedReader rdr = new BufferedReader(new InputStreamReader(in)); + int line = 0; + String s; + while ((s = rdr.readLine()) != null) + { + line++; + Matcher matcher = pattern.matcher(s); + if (matcher.find() == !opt.isSet("invert-match")) + { + match = true; + if (opt.isSet("quiet")) + break; + + System.out.println(String.format(format, arg, line, s)); + } + } + + if (match && opt.isSet("quiet")) + { + break; + } + } + catch (IOException e) + { + System.err.println("grep: " + e.getMessage()); + status = false; + } + finally + { + if (arg != null && in != null) + { + in.close(); + } + } + } + + return match && status; + } + + public static void copy(URI source, OutputStream out) throws IOException + { + try (InputStream in = source.toURL().openStream()) + { + copy(in, out); + } + } + + public static void copy(InputStream in, OutputStream out) throws IOException + { + byte buf[] = new byte[10240]; + int len; + while ((len = in.read(buf)) > 0) + { + out.write(buf, 0, len); + } + out.flush(); + } + +} diff --git a/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Procedural.java b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Procedural.java new file mode 100644 index 00000000000..673e1e9ac20 --- /dev/null +++ b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Procedural.java @@ -0,0 +1,171 @@ +/* + * 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.gogo.shell; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import org.apache.felix.service.command.CommandSession; +import org.apache.felix.service.command.Function; + +public class Procedural +{ + static final String[] functions = { "each", "if", "not", "throw", "try", "until", + "while" }; + + public List each(CommandSession session, Collection list, + Function closure) throws Exception + { + List args = new ArrayList<>(); + List results = new ArrayList<>(); + args.add(null); + + for (Object x : list) + { + checkInterrupt(); + args.set(0, x); + results.add(closure.execute(session, args)); + } + + return results; + } + + public Object _if(CommandSession session, Function[] fns) throws Exception + { + int length = fns.length; + if (length < 2) + { + throw new IllegalArgumentException( + "Usage: if {condition} {if-action} ... {else-action}"); + } + + for (int i = 0; i < length; ++i) + { + if (i == length - 1 || isTrue(fns[i++].execute(session, null))) + { + return fns[i].execute(session, null); + } + } + + return null; + } + + public boolean not(CommandSession session, Function condition) throws Exception + { + if (null == condition) + { + return true; + } + + return !isTrue(condition.execute(session, null)); + } + + // Reflective.coerce() prefers to construct a new Throwable(String) + // than to call this method directly. + public void _throw(String message) + { + throw new IllegalArgumentException(message); + } + + public void _throw(Exception e) throws Exception + { + throw e; + } + + public void _throw(CommandSession session) throws Throwable + { + Object exception = session.get("exception"); + if (exception instanceof Throwable) + throw (Throwable) exception; + else + throw new IllegalArgumentException("exception not set or not Throwable."); + } + + public Object _try(CommandSession session, Function func) { + try + { + return func.execute(session, null); + } + catch (Exception e) + { + session.put("exception", e); + return null; + } + } + + public Object _try(CommandSession session, Function func, Function error) + throws Exception + { + try + { + return func.execute(session, null); + } + catch (Exception e) + { + session.put("exception", e); + return error.execute(session, null); + } + } + + public void _while(CommandSession session, Function condition, Function ifTrue) + throws Exception + { + while (isTrue(condition.execute(session, null))) + { + ifTrue.execute(session, null); + } + } + + public void until(CommandSession session, Function condition, Function ifTrue) + throws Exception + { + while (!isTrue(condition.execute(session, null))) + { + ifTrue.execute(session, null); + } + } + + private boolean isTrue(Object result) throws InterruptedException + { + checkInterrupt(); + + if (result == null) + return false; + + if (result instanceof Boolean) + return (Boolean) result; + + if (result instanceof Number) + { + if (0 == ((Number) result).intValue()) + return false; + } + + if ("".equals(result)) + return false; + + return !"0".equals(result); + } + + private void checkInterrupt() throws InterruptedException + { + if (Thread.currentThread().isInterrupted()) + throw new InterruptedException("loop interrupted"); + } +} diff --git a/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Shell.java b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Shell.java new file mode 100644 index 00000000000..60159eab52f --- /dev/null +++ b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Shell.java @@ -0,0 +1,298 @@ +/* + * 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.gogo.shell; + +import java.io.File; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.nio.CharBuffer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.apache.felix.gogo.options.Option; +import org.apache.felix.gogo.options.Options; +import org.apache.felix.service.command.CommandProcessor; +import org.apache.felix.service.command.CommandSession; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; + +public class Shell +{ + static final String[] functions = { "gosh", "sh", "source", "history" }; + + private final static URI CWD = new File(".").toURI(); + + private final URI baseURI; + private final BundleContext context; + private final CommandProcessor processor; + private final History history; + + private volatile Bundle systemBundle; + + public Shell(BundleContext context, CommandProcessor processor) + { + this.context = context; + this.processor = processor; + String baseDir = context.getProperty("gosh.home"); + baseDir = (baseDir == null) ? context.getProperty("user.dir") : baseDir; + baseURI = new File(baseDir).toURI(); + this.history = new History(); + } + + public Object gosh(final CommandSession session, String[] argv) throws Exception + { + final String[] usage = { + "gosh - execute script with arguments in a new session", + " args are available as session variables $1..$9 and $args.", + "Usage: gosh [OPTIONS] [script-file [args..]]", + " -c --command pass all remaining args to sub-shell", + " --nointeractive don't start interactive session", + " --login login shell (same session, reads etc/gosh_profile)", + " -s --noshutdown don't shutdown framework when script completes", + " -q --quiet don't display message-of-the-day", + " -x --xtrace echo commands before execution", + " -? --help show help", + "If no script-file, an interactive shell is started, type $D to exit." }; + + Option opt = Options.compile(usage).setOptionsFirst(true).parse(argv); + List args = opt.args(); + + boolean login = opt.isSet("login"); + boolean interactive = !opt.isSet("nointeractive"); + + // We grab this bundle as early as possible to avoid having to deal with invalid bundleContexts of this bundle during shutdowns... + systemBundle = context.getBundle(0); + + if (opt.isSet("help")) + { + opt.usage(); + if (login && !opt.isSet("noshutdown")) + { + shutdown(); + } + return null; + } + + if (opt.isSet("command") && args.isEmpty()) + { + throw opt.usageError("option --command requires argument(s)"); + } + + CommandSession newSession = (login ? session : processor.createSession(session.getKeyboard(), session.getConsole(), System.err)); + // Make some of the given arguments available to the shell itself... + newSession.put(".gosh_login", login); + newSession.put(".gosh_interactive", interactive); + newSession.put(".gosh_quiet", opt.isSet("quiet")); + + if (opt.isSet("xtrace")) + { + newSession.put("echo", true); + } + + if (login && interactive) + { + URI uri = baseURI.resolve("etc/gosh_profile"); + if (!new File(uri).exists()) + { + URL url = getClass().getResource("/ext/gosh_profile"); + if (url == null) + { + url = getClass().getResource("/gosh_profile"); + } + uri = (url == null) ? null : url.toURI(); + } + if (uri != null) + { + source(session, uri.toString()); + } + } + + // export variables starting with upper-case to newSession + for (String key : getVariables(session)) + { + if (key.matches("[.]?[A-Z].*")) + { + newSession.put(key, session.get(key)); + } + } + + Object result = null; + + if (args.isEmpty()) + { + if (interactive) + { + result = console(newSession); + } + } + else + { + CharSequence program; + + if (opt.isSet("command")) + { + StringBuilder buf = new StringBuilder(); + for (String arg : args) + { + if (buf.length() > 0) + { + buf.append(' '); + } + buf.append(arg); + } + program = buf; + } + else + { + URI script = cwd(session).resolve(args.remove(0)); + + // set script arguments + newSession.put("0", script); + newSession.put("args", args); + + for (int i = 0; i < args.size(); ++i) + { + newSession.put(String.valueOf(i + 1), args.get(i)); + } + + program = readScript(script); + } + + result = newSession.execute(program); + } + + if (login && interactive) + { + if (opt.isSet("noshutdown")) + { + System.out.println("gosh: stopping shell"); + } + else + { + System.out.println("gosh: stopping shell and framework"); + shutdown(); + } + } + + return result; + } + + public Object sh(final CommandSession session, String[] argv) throws Exception + { + return gosh(session, argv); + } + + private void shutdown() throws BundleException + { + if (systemBundle != null) + { + systemBundle.stop(); + systemBundle = null; + } + } + + public Object source(CommandSession session, String script) throws Exception + { + URI uri = cwd(session).resolve(script); + session.put("0", uri); + try + { + return session.execute(readScript(uri)); + } + finally + { + session.put("0", null); // API doesn't support remove + } + } + + private Object console(CommandSession session) + { + Console console = new Console(session, history); + console.run(); + return null; + } + + private CharSequence readScript(URI script) throws Exception + { + CharBuffer buf = CharBuffer.allocate(4096); + StringBuilder sb = new StringBuilder(); + + URLConnection conn = script.toURL().openConnection(); + + try (InputStreamReader in = new InputStreamReader(conn.getInputStream())) + { + while (in.read(buf) > 0) + { + buf.flip(); + sb.append(buf); + buf.clear(); + } + } + finally + { + if (conn instanceof HttpURLConnection) + { + ((HttpURLConnection) conn).disconnect(); + } + } + + return sb; + } + + @SuppressWarnings("unchecked") + static Set getVariables(CommandSession session) + { + return (Set) session.get(".variables"); + } + + static URI cwd(CommandSession session) + { + Object cwd = session.get("_cwd"); // _cwd is set by felixcommands:cd + + if (cwd instanceof URI) + { + return (URI) cwd; + } + else if (cwd instanceof File) + { + return ((File) cwd).toURI(); + } + else + { + return CWD; + } + } + + public String[] history() + { + Iterator history = this.history.getHistory(); + List lines = new ArrayList<>(); + for (int i = 1; history.hasNext(); i++) + { + lines.add(String.format("%5d %s", i, history.next())); + } + return lines.toArray(new String[lines.size()]); + } +} diff --git a/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Telnet.java b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Telnet.java new file mode 100644 index 00000000000..44ea2f6323b --- /dev/null +++ b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Telnet.java @@ -0,0 +1,189 @@ +/* + * 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.gogo.shell; + +import java.io.IOException; +import java.io.PrintStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.InetAddress; +import java.util.List; + +import org.apache.felix.gogo.options.Option; +import org.apache.felix.gogo.options.Options; +import org.apache.felix.service.command.CommandProcessor; +import org.apache.felix.service.command.CommandSession; + +/* + * a very simple Telnet server. + * real remote access should be via ssh. + */ +public class Telnet implements Runnable +{ + static final String[] functions = { "telnetd" }; + + private static final int defaultPort = 2019; + private final CommandProcessor processor; + private ServerSocket server; + private Thread thread; + private boolean quit; + private int port; + private String ip; + + public Telnet(CommandProcessor procesor) + { + this.processor = procesor; + } + + public void telnetd(String[] argv) throws IOException + { + final String[] usage = { "telnetd - start simple telnet server", + "Usage: telnetd [-i ip] [-p port] start | stop | status", + " -i --ip=INTERFACE listen interface (default=127.0.0.1)", + " -p --port=PORT listen port (default=" + defaultPort + ")", + " -? --help show help" }; + + Option opt = Options.compile(usage).parse(argv); + List args = opt.args(); + + if (opt.isSet("help") || args.isEmpty()) + { + opt.usage(); + return; + } + + String command = args.get(0); + + if ("start".equals(command)) + { + if (server != null) + { + throw new IllegalStateException("telnetd is already running on port " + port); + } + ip = opt.get("ip"); + port = opt.getNumber("port"); + start(); + status(); + } + else if ("stop".equals(command)) + { + if (server == null) + { + throw new IllegalStateException("telnetd is not running."); + } + stop(); + } + else if ("status".equals(command)) + { + status(); + } + else + { + throw opt.usageError("bad command: " + command); + } + } + + private void status() + { + if (server != null) + { + System.out.println("telnetd is running on " + ip + ":" + port); + } + else + { + System.out.println("telnetd is not running."); + } + } + + private void start() throws IOException + { + quit = false; + InetAddress addr = "".equals(ip) ? null : InetAddress.getByName(ip); + server = new ServerSocket(port, 0, addr); + thread = new Thread(this, "Gogo telnet"); + thread.start(); + } + + private void stop() throws IOException + { + quit = true; + server.close(); + server = null; + thread.interrupt(); + } + + public void run() + { + try + { + while (!quit) + { + final Socket socket = server.accept(); + PrintStream out = new PrintStream(socket.getOutputStream()); + final CommandSession session = processor.createSession( + socket.getInputStream(), out, out); + Thread handler = new Thread(new Runnable() { + @Override + public void run() { + try + { + session.execute("gosh --login --noshutdown"); + } + catch (Exception e) + { + e.printStackTrace(); + } + finally + { + session.close(); + try + { + socket.close(); + } + catch (IOException e) + { + } + } + } + }); + handler.start(); + } + } + catch (IOException e) + { + if (!quit) + { + e.printStackTrace(); + } + } + finally + { + try + { + if (server != null) + { + server.close(); + } + } + catch (IOException e) + { + } + } + } +} diff --git a/gogo/shell/src/main/java/org/apache/felix/gogo/shell/package-info.java b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/package-info.java new file mode 100644 index 00000000000..8e7302f6ae7 --- /dev/null +++ b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/package-info.java @@ -0,0 +1,34 @@ +/* + * 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. + */ + +@org.osgi.annotation.bundle.Capability( + attribute = "implementation.name=gogo.shell", + namespace = "org.apache.felix.gogo", + name = "shell.implementation", + version = "1.0.0" +) +@Requirement( + effective = "active", + namespace = "org.apache.felix.gogo", + name = "command.implementation", + version = "1.0.0" +) +package org.apache.felix.gogo.shell; + +import org.osgi.annotation.bundle.Requirement; diff --git a/gogo/shell/src/main/resources/gosh_profile b/gogo/shell/src/main/resources/gosh_profile new file mode 100644 index 00000000000..5610267a527 --- /dev/null +++ b/gogo/shell/src/main/resources/gosh_profile @@ -0,0 +1,57 @@ +# +# 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. +# +# default gosh_profile +# only read if etc/gosh_profile doesn't exist relative to the System property +# gosh.home or failing that the current directory. + +# catch all exceptions from this script to avoid it aborting startup +try { + + # ensure gogo commands are found first + SCOPE = 'gogo:*' + + # add methods on BundleContext object as commands + #addcommand context ${.context} (${.context} class) + # bug: above invokes (String, Object, String) instead of (String, Object, Class) + addcommand context ${.context} + + # add methods on System object as commands + addcommand system (((${.context} bundles) 0) loadclass java.lang.System) + + # alias to print full stack trace + e = { $exception printStackTrace } + + ## disable console auto-formatting of each result + # you will then need to explicitly use the 'format' command + # to print the result of commands that don't write to stdout. + #.Gogo.format = false + + ## disable printing the formatted result of a command into pipelines + #.Format.Pipe = false + + # set prompt + prompt = 'g! ' + + # print welcome message, unless we're explicitly told not to... + if { $.gosh_quiet } { } { cat ($0 resolve motd) } +} { + echo "$0: ERROR: $exception" +} + +# end diff --git a/gogo/shell/src/main/resources/motd b/gogo/shell/src/main/resources/motd new file mode 100644 index 00000000000..7ea9c9fb20d --- /dev/null +++ b/gogo/shell/src/main/resources/motd @@ -0,0 +1,3 @@ +____________________________ +Welcome to Apache Felix Gogo + diff --git a/gogo/shell/src/test/java/org/apache/felix/gogo/shell/HistoryTest.java b/gogo/shell/src/test/java/org/apache/felix/gogo/shell/HistoryTest.java new file mode 100644 index 00000000000..03b90e01501 --- /dev/null +++ b/gogo/shell/src/test/java/org/apache/felix/gogo/shell/HistoryTest.java @@ -0,0 +1,240 @@ +/* + * 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.gogo.shell; + +import java.util.Iterator; + +import junit.framework.TestCase; + +import org.apache.felix.gogo.shell.History; +import org.junit.Before; +import org.junit.Test; + +public class HistoryTest { + + private static final String[] COMMANDS = { + "cd home", "ls", "more config", "more xconfig" + }; + + private History history; + + @Before + public void setup() { + this.history = new History(); + for (String cmd : COMMANDS) { + this.history.append(cmd); + } + } + + @Test + public void test_add_get_history() { + final History his = new History(); + + his.append("cmd1"); + Iterator hi = his.getHistory(); + TestCase.assertEquals("cmd1", hi.next()); + TestCase.assertFalse(hi.hasNext()); + } + + @Test + public void test_fill_history() { + final History his = new History(); + + // NOTE: Assumes a fixed history size of 100 + for (int i = 0; i < 100; i++) { + his.append("cmd" + i); + } + + Iterator hi = his.getHistory(); + for (int i = 0; i < 100; i++) { + TestCase.assertEquals("cmd" + i, hi.next()); + } + TestCase.assertFalse(hi.hasNext()); + } + + @Test + public void test_overflow_history() { + final History his = new History(); + + // NOTE: Assumes a fixed history size of 100 + for (int i = -20; i < 100; i++) { + his.append("cmd" + i); + } + + Iterator hi = his.getHistory(); + for (int i = 0; i < 100; i++) { + TestCase.assertEquals("cmd" + i, hi.next()); + } + TestCase.assertFalse(hi.hasNext()); + } + + @Test + public void test_get_recent() { + TestCase.assertEquals(COMMANDS[3], history.evaluate("!!")); + + TestCase.assertEquals(COMMANDS[3], history.evaluate("!-1")); + TestCase.assertEquals(COMMANDS[2], history.evaluate("!-2")); + TestCase.assertEquals(COMMANDS[1], history.evaluate("!-3")); + TestCase.assertEquals(COMMANDS[0], history.evaluate("!-4")); + + TestCase.assertEquals(COMMANDS[0], history.evaluate("!1")); + TestCase.assertEquals(COMMANDS[1], history.evaluate("!2")); + TestCase.assertEquals(COMMANDS[2], history.evaluate("!3")); + TestCase.assertEquals(COMMANDS[3], history.evaluate("!4")); + } + + @Test + public void test_get_recent_wrong_index() { + try { + history.evaluate("!0"); + TestCase.fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // expected + } + + try { + history.evaluate("!5"); + TestCase.fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // expected + } + + try { + history.evaluate("!-5"); + TestCase.fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException iae) { + // expected + } + } + + @Test + public void test_caret() { + TestCase.assertEquals("ls xconfig", history.evaluate("^more^ls^")); + TestCase.assertEquals("ls xconfig", history.evaluate("^more^ls")); + TestCase.assertEquals("ls -l xconfig", history.evaluate("^more^ls -l")); + } + + @Test + public void test_caret_fail() { + try { + history.evaluate("^ls^cat^"); + TestCase.fail("Expected failure for impossible replacement"); + } catch (IllegalArgumentException iae) { + // expected + } + } + + @Test + public void test_s_last() { + TestCase.assertEquals("ls xconfig", history.evaluate("!!:s?more?ls?")); + TestCase.assertEquals("ls xconfig", history.evaluate("!!:s/more/ls")); + TestCase.assertEquals("ls -l xconfig", history.evaluate("!!:s^more^ls -l")); + + TestCase.assertEquals("mare xconfig", history.evaluate("!!:s?o?a?")); + } + + @Test + public void test_s_last_fail() { + try { + history.evaluate("!!:s/ls/cat/"); + TestCase.fail("Expected failure for impossible replacement"); + } catch (IllegalArgumentException iae) { + // expected + } + } + + @Test + public void test_s_g_a_last() { + TestCase.assertEquals("mire xcinfig", history.evaluate("!!:gs?o?i?")); + TestCase.assertEquals("mire xcinfig", history.evaluate("!!:gs/o/i")); + TestCase.assertEquals("mi-xre xci-xnfig", history.evaluate("!!:gs^o^i-x")); + + TestCase.assertEquals("mire xcinfig", history.evaluate("!!:as?o?i?")); + TestCase.assertEquals("mire xcinfig", history.evaluate("!!:as/o/i")); + TestCase.assertEquals("mi-xre xci-xnfig", history.evaluate("!!:as^o^i-x")); + } + + @Test + public void test_s_neg_2() { + TestCase.assertEquals("ls config", history.evaluate("!-2:s?more?ls?")); + TestCase.assertEquals("ls config", history.evaluate("!-2:s/more/ls")); + TestCase.assertEquals("ls -l config", history.evaluate("!-2:s^more^ls -l")); + + TestCase.assertEquals("mare config", history.evaluate("!-2:s?o?a?")); + } + + @Test + public void test_s_neg_2_fail() { + try { + history.evaluate("!-2:s/ls/cat/"); + TestCase.fail("Expected failure for impossible replacement"); + } catch (IllegalArgumentException iae) { + // expected + } + } + + @Test + public void test_s_3() { + TestCase.assertEquals("ls config", history.evaluate("!-2:s?more?ls?")); + TestCase.assertEquals("ls config", history.evaluate("!-2:s/more/ls")); + TestCase.assertEquals("ls -l config", history.evaluate("!-2:s^more^ls -l")); + + TestCase.assertEquals("mare config", history.evaluate("!-2:s?o?a?")); + } + + @Test + public void test_s_3_fail() { + try { + history.evaluate("!-2:s/ls/cat/"); + TestCase.fail("Expected failure for impossible replacement"); + } catch (IllegalArgumentException iae) { + // expected + } + } + + @Test + public void test_contains() { + TestCase.assertEquals("ls", history.evaluate("!?s")); + TestCase.assertEquals("ls", history.evaluate("!?s?")); + + try { + history.evaluate("!?s:s/l/x/"); + TestCase.fail("IllegalArgumentException expected"); + } catch (IllegalArgumentException iae) { + // expected + } + + TestCase.assertEquals("xs", history.evaluate("!?s?:s/l/x/")); + } + + @Test + public void test_startsWith() { + TestCase.assertEquals("cd home", history.evaluate("!cd")); + TestCase.assertEquals("cd home", history.evaluate("!cd ")); + + try { + history.evaluate("!cd:s/x/y/"); + TestCase.fail("IllegalArgumentException expected"); + } catch (IllegalArgumentException iae) { + // expected + } + + TestCase.assertEquals("ls home", history.evaluate("!cd:s/cd/ls/")); + } +} diff --git a/healthcheck/README.md b/healthcheck/README.md new file mode 100644 index 00000000000..8977fa4f7f8 --- /dev/null +++ b/healthcheck/README.md @@ -0,0 +1,178 @@ + +# Felix Health Checks + +Based on simple `HealthCheck` OSGi services, the Felix Health Check Tools ("hc" in short form) are used to +check the health of live Felix systems, based on inputs like OSGi framework status, JMX MBean attribute values, or any context information retrieved via any API. + +Health checks are easily extensible either by configuring the supplied default `HealthCheck` services, or +by implementing your own `HealthCheck` services to cater for project specific requirements. + +However for simple setups, the out of the box health checks are often sufficient. [Executing Health Checks](#executing-health-checks) +is a good starting point to run existing checks and to get familiar with how health checks work. + +See also: + +* [Source code for the HealthCheck modules](http://svn.apache.org/repos/asf/felix/trunk/healthcheck) +* adaptTo() slides about Health Checks (from the time when they were part of Apache Sling): + * [adaptTo() 2013 - Automated self-testing and health check of live Sling instances](https://adapt.to/2013/en/schedule/18_healthcheck.html) + * [adaptTo() 2014 - New features of the sling health check](https://adapt.to/2014/en/schedule/new-features-of-the-sling-health-check.html) + +## Use cases +Generally health checks have two high level use cases: + +* **Load balancers can query the health of an instance** and decide to take it outor back into the list of used backends automatically +* **Operations teams checking instances** for their internal state **manually** + +The strength of Health Checks are to surface internal state for external use: + +* Check that all OSGi bundles are up and running +* Verify that performance counters are in range +* Ping external systems and raise alarms if they are down +* Run smoke tests at system startup +* Check that demo content has been removed from a production system +* Check that demo accounts are disabled + +The health check subsystem uses tags to select which health checks to execute so you can for example execute just the _performance_ or _security_ health +checks once they are configured with the corresponding tags. + +The out of the box health check services also allow for using them as JMX aggregators and processors, which take JMX +attribute values as input and make the results accessible via JMX MBeans. + +## Implementing `HealthCheck`s + +Health checks checks can be contributed by any bundle via the provided SPI interface. It is best practice to implement a health check as part of the bundle that contains the functionality being checked. + +## The `HealthCheck` SPI interface + +A `HealthCheck` is just an OSGi service that returns a `Result`. + +``` + public interface HealthCheck { + + /** Execute this health check and return a {@link Result} + * This is meant to execute quickly, access to external + * systems, for example, should be managed asynchronously. + */ + public Result execute(); + } +``` + +A simple health check implementation might look like follows: + +``` + public class SampleHealthCheck implements HealthCheck { + + @Override + public Result execute() { + FormattingResultLog log = new FormattingResultLog(); + ... + log.info("Checking my context {}", myContextObject); + if(myContextObject.retrieveStatus() != ...expected value...) { + log.warn("Problem with ..."); + } + if(myContextObject.retrieveOtherStatus() != ...expected value...) { + log.critical("Cricital Problem with ..."); + } + return new Result(log); + } + + } +``` + +The `Result` is a simple immutable class that provides a `Status` via `getStatus()` (OK, WARN, CRITICAL etc.) and one or more log-like messages that +can provide more info about what, if anything, went wrong. + +### Semantic meaning of health check results +In order to make health check results aggregatable in a reasonable way, it is important that result status values are used in a consistent way across different checks. When implementing custom health checks, comply to the following table: + +Status | System is functional | Meaning | Actions possible for machine clients | Actions possible for human clients +--- | --- | --- | --- | --- +OK | yes | Everything is ok. |
      • If system is not actively used yet, a load balancer might decide to take the system to production after receiving this status for the first time.
      • Otherwise no action needed
      | Response logs might still provide information to a human on why the system currently is healthy. E.g. it might show 30% disk used which indicates that no action will be required for a long time +WARN | yes | **Tendency to CRITICAL**
      System is fully functional but actions are needed to avoid a CRITICAL status in the future |
      • Certain actions can be configured for known, actionable warnings, e.g. if disk space is low, it could be dynamically extended using infrastructure APIs if on virtual infrastructure)
      • Pass on information to monitoring system to be available to humans (in other aggregator UIs)
      | Any manual steps that a human can perform based on their knowledge to avoid the system to get to CRITICAL state +TEMPORARILY_UNAVAILABLE | no | **Tendency to OK**
      System is not functional at the moment but is expected to become OK (or at least WARN) without action. An health check using this status is expected to turn CRITICAL after a certain period returning TEMPORARILY_UNAVAILABLE |
      • Take out system from load balancing
      • Wait until TEMPORARILY_UNAVAILABLE status turns into either OK or CRITICAL
      | Wait and monitor result logs of health check returning TEMPORARILY_UNAVAILABLE +CRITICAL | no | System is not functional and must not be used |
      • Take out system from load balancing
      • Decommission system entirely and re-provision from scratch
      | Any manual steps that a human can perform based on their knowledge to bring the system back to state OK +HEALTH\_CHECK\_ERROR | no | **Actual status unknown**
      There was an error in correctly calculating one of the status values above. Like CRITICAL but with the hint that the health check probe itself might be the problem (and the system could well be in state OK) |
      • Treat exactly the same as CRITICAL
      | Fix health check implementation or configuration to ensure a correct status can be calculated + + +### Configuring Health Checks + +`HealthCheck` services are created via OSGi configurations. Generic health check service properties are interpreted by the health check executor service. Custom health check service properties can be used by the health check implementation itself to configure its behaviour. + +The following generic Health Check properties may be used for all checks: + +Property | Type | Description +----------- | -------- | ------------ +hc.name | String | The name of the health check as shown in UI +hc.tags | String[] | List of tags: Both Felix Console Plugin and Health Check servlet support selecting relevant checks by providing a list of tags +hc.mbean.name | String | Makes the HC result available via given MBean name. If not provided no MBean is created for that `HealthCheck` +hc.async.cronExpression | String | Used to schedule the execution of a `HealthCheck` at regular intervals, using a cron expression as supported by the [Quartz Cron Trigger](http://www.quartz-scheduler.org/api/previous_versions/1.8.5/org/quartz/CronTrigger.html) module. +hc.async.intervalInSec | Long | Used to schedule the execution of a `HealthCheck` at regular intervals, specifying a period in seconds +hc.resultCacheTtlInMs | Long | Overrides the global default TTL as configured in health check executor for health check responses (since v1.2.6 of core) +hc.keepNonOkResultsStickyForSec | Long | If given, non-ok results from past executions will be taken into account as well for the given seconds (use Long.MAX_VALUE for indefinitely). Useful for unhealthy system states that disappear but might leave the system at an inconsistent state (e.g. an event queue overflow where somebody needs to intervene manually) or for checks that should only go back to OK with a delay (can be useful for load balancers). + +All service properties are optional. + +## General purpose health checks available out-of-the-box + +The following checks are contained in bundle `org.apache.felix.healthcheck.generalchecks` and can be activated by simple configuration: + +Default Name | PID | Factory | Description +--- | --- | --- | --- +Disk Space | org.apache.felix.hc.generalchecks.DiskSpaceCheck | yes | Checks for disk space usage at the given paths `diskPaths` and checks them against thresholds `diskUsedThresholdWarn` (default 90%) and diskUsedThresholdCritical (default 97%) +Memory | org.apache.felix.hc.generalchecks.MemoryCheck | no | Checks for Memory usage - `heapUsedPercentageThresholdWarn` (default 90%) and `heapUsedPercentageThresholdCritical` (default 99%) can be set to control what memory usage produces status `WARN` and `CRITICAL` +CPU | org.apache.felix.hc.generalchecks.CpuCheck | no | Checks for CPU usage - `cpuPercentageThresholdWarn` (default 95%) can be set to control what CPU usage produces status `WARN` (check never results in `CRITICAL`) +Thread Usage | org.apache.felix.hc.generalchecks.ThreadUsageCheck | no | Checks via `ThreadMXBean.findDeadlockedThreads()` for deadlocks and analyses the CPU usage of each thread via a configurable time period (`samplePeriodInMs` defaults to 200ms). Uses `cpuPercentageThresholdWarn` (default 95%) to `WARN` about high thread utilisation. +Bundles Started | org.apache.felix.hc.generalchecks.BundlesStartedCheck | yes | Checks for started bundles - `includesRegex` and `excludesRegex` control what bundles are checked. +JMX Attribute Check | org.apache.felix.hc.generalchecks.JmxAttributeCheck | yes | Allows to check an arbitrary JMX attribute (using the configured mbean `mbean.name`'s attribute `attribute.name`) against a given constraint `attribute.value.constraint`. Can check multiple attributes by providing additional config properties with numbers: `mbean2.name`' `attribute2.name` and `attribute2.value.constraint`. +HttpRequestsCheck | org.apache.felix.hc.generalchecks.HttpRequestsCheck | yes | Allows to check a list of URLs against response code, response headers, timing, response content (plain content via RegEx or JSON via path expression). + + +## Executing Health Checks + +Health Checks can be executed via a [webconsole plugin](#webconsole-plugin), the [health check servlet](#health-check-servlet) or via [JMX](#jmx-access-to-health-checks). `HealthCheck` services can be selected for execution based on their `hc.tags` multi-value service property. + +The `HealthCheckFilter` utility accepts positive and negative tag parameters, so that `osgi,-security` +selects all `HealthCheck` having the `osgi` tag but not the `security` tag, for example. + +For advanced use cases it is also possible to use the API directly by using the interface `org.apache.felix.hc.api.execution.HealthCheckExecutor`. + +### Configuring the Health Check Executor +The health check executor can **optionally** be configured via service PID `org.apache.felix.hc.core.impl.executor.HealthCheckExecutorImpl`: + +Property | Type | Default | Description +----------- | -------- | ------ | ------------ +timeoutInMs | Long | 2000ms | Timeout in ms until a check is marked as timed out +longRunningFutureThresholdForCriticalMs | Long | 300000ms = 5min | Threshold in ms until a check is marked as 'exceedingly' timed out and will marked CRITICAL instead of WARN only +resultCacheTtlInMs | Long | 2000ms | Result Cache time to live - results will be cached for the given time + + +### JMX access to health checks +If the `org.apache.felix.hc.jmx` bundle is active, a JMX MBean is created for each `HealthCheck` which has the +service property `hc.mbean.name` service property set. All health check MBeans are registered in the +domain `org.apache.felix.healthcheck` with a type of `HealthCheck`. + +The MBean gives access to the `Result` and the log, as shown on the screenshot below. + +### Health Check Servlet +The health check servlet allows to query the checks via http. It provides +similar features to the Web Console plugin described above, with output in HTML, JSON (plain or jsonp) and TXT (concise or verbose) formats (see HTML format rendering page for more documentation). + +The Health Checks Servlet is disabled by default, to enable it create an OSGi configuration like + + PID = org.apache.felix.hc.core.impl.servlet.HealthCheckExecutorServlet + servletPath = /system/health + +which specifies the servlet's base path. That URL then returns an HTML page, by default with the results of all active health checks and +with instructions at the end of the page about URL parameters which can be used to select specific Health Checks and control their execution and output format. + +Note that by design **the Health Checks Servlet doesn't do any access control by itself** to ensure it can detect unhealthy states of the authentication itself. Make sure the configured path is only accessible to relevant infrastructure and operations people. Usually all `/system/*` paths are only accessible from a local network and not routed to the Internet. + +By default the HC servlet sends the CORS header `Access-Control-Allow-Origin: *` to allow for client-side browser integrations. The behaviour can be configured using the OSGi config property `cors.accessControlAllowOrigin` (a blank value disables the header). + +### Webconsole plugin + +If the `org.apache.felix.hc.webconsole` bundle is active, a webconsole plugin +at `/system/console/healthcheck` allows for executing health checks, optionally selected +based on their tags (positive and negative selection, see the `HealthCheckFilter` mention above). + +The DEBUG logs of health checks can optionally be displayed, and an option allows for showing only health checks that have a non-OK status. \ No newline at end of file diff --git a/healthcheck/annotation/LICENSE b/healthcheck/annotation/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/healthcheck/annotation/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/healthcheck/annotation/pom.xml b/healthcheck/annotation/pom.xml new file mode 100644 index 00000000000..b37e77de0ee --- /dev/null +++ b/healthcheck/annotation/pom.xml @@ -0,0 +1,73 @@ + + + + 4.0.0 + + + org.apache.felix + felix-parent + 6 + + + + org.apache.felix.healthcheck.annotation + 2.0.1-SNAPSHOT + + Apache Felix Health Check Annotations + 2018 + + + The Felix Health Check Annotations + + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/healthcheck/annotation + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/healthcheck/annotation + http://svn.apache.org/viewvc/felix/trunk/http/annotation/ + + + + + org.osgi + osgi.core + 7.0.0 + provided + + + org.osgi + osgi.cmpn + 7.0.0 + provided + + + org.osgi + osgi.annotation + 7.0.0 + provided + + + org.slf4j + slf4j-api + 1.7.6 + provided + + + + \ No newline at end of file diff --git a/healthcheck/annotation/src/main/java/org/apache/felix/hc/annotation/Async.java b/healthcheck/annotation/src/main/java/org/apache/felix/hc/annotation/Async.java new file mode 100644 index 00000000000..c4d7b34546a --- /dev/null +++ b/healthcheck/annotation/src/main/java/org/apache/felix/hc/annotation/Async.java @@ -0,0 +1,31 @@ +/* + * 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 SF 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.hc.annotation; + +import org.osgi.service.component.annotations.ComponentPropertyType; + +@ComponentPropertyType +public @interface Async { + + public static final String PREFIX_ = "hc.async."; + + String cronExpression() default ""; + + long intervalInSec() default -1; + +} \ No newline at end of file diff --git a/healthcheck/annotation/src/main/java/org/apache/felix/hc/annotation/HealthCheckMBean.java b/healthcheck/annotation/src/main/java/org/apache/felix/hc/annotation/HealthCheckMBean.java new file mode 100644 index 00000000000..4ad168c8e47 --- /dev/null +++ b/healthcheck/annotation/src/main/java/org/apache/felix/hc/annotation/HealthCheckMBean.java @@ -0,0 +1,29 @@ +/* + * 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 SF 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.hc.annotation; + +import org.osgi.service.component.annotations.ComponentPropertyType; + +@ComponentPropertyType +public @interface HealthCheckMBean { + + public static final String PREFIX_ = "hc.mbean."; + + String name(); + +} \ No newline at end of file diff --git a/healthcheck/annotation/src/main/java/org/apache/felix/hc/annotation/HealthCheckService.java b/healthcheck/annotation/src/main/java/org/apache/felix/hc/annotation/HealthCheckService.java new file mode 100644 index 00000000000..38261928998 --- /dev/null +++ b/healthcheck/annotation/src/main/java/org/apache/felix/hc/annotation/HealthCheckService.java @@ -0,0 +1,31 @@ +/* + * 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 SF 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.hc.annotation; + +import org.osgi.service.component.annotations.ComponentPropertyType; + +@ComponentPropertyType +public @interface HealthCheckService { + + public static final String PREFIX_ = "hc."; + + String name() default ""; + + String[] tags() default {}; + +} \ No newline at end of file diff --git a/healthcheck/annotation/src/main/java/org/apache/felix/hc/annotation/ResultTTL.java b/healthcheck/annotation/src/main/java/org/apache/felix/hc/annotation/ResultTTL.java new file mode 100644 index 00000000000..ec8de270ffb --- /dev/null +++ b/healthcheck/annotation/src/main/java/org/apache/felix/hc/annotation/ResultTTL.java @@ -0,0 +1,29 @@ +/* + * 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 SF 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.hc.annotation; + +import org.osgi.service.component.annotations.ComponentPropertyType; + +@ComponentPropertyType +public @interface ResultTTL { + + public static final String PREFIX_ = "hc."; + + long resultCacheTtlInMs(); + +} \ No newline at end of file diff --git a/healthcheck/annotation/src/main/java/org/apache/felix/hc/annotation/Sticky.java b/healthcheck/annotation/src/main/java/org/apache/felix/hc/annotation/Sticky.java new file mode 100644 index 00000000000..5df5c1231d6 --- /dev/null +++ b/healthcheck/annotation/src/main/java/org/apache/felix/hc/annotation/Sticky.java @@ -0,0 +1,29 @@ +/* + * 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 SF 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.hc.annotation; + +import org.osgi.service.component.annotations.ComponentPropertyType; + +@ComponentPropertyType +public @interface Sticky { + + public static final String PREFIX_ = "hc."; + + long keepNonOkResultsStickyForSec(); + +} \ No newline at end of file diff --git a/healthcheck/annotation/src/main/java/org/apache/felix/hc/annotation/package-info.java b/healthcheck/annotation/src/main/java/org/apache/felix/hc/annotation/package-info.java new file mode 100644 index 00000000000..ade9aa3b4b5 --- /dev/null +++ b/healthcheck/annotation/src/main/java/org/apache/felix/hc/annotation/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +@Version("2.0.0") +package org.apache.felix.hc.annotation; + +import org.osgi.annotation.versioning.Version; diff --git a/healthcheck/api/LICENSE b/healthcheck/api/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/healthcheck/api/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/healthcheck/api/bnd.bnd b/healthcheck/api/bnd.bnd new file mode 100644 index 00000000000..b5f27195358 --- /dev/null +++ b/healthcheck/api/bnd.bnd @@ -0,0 +1,12 @@ +Bundle-Category: healthcheck + +Bundle-Description: ${project.description} + +Bundle-DocURL: https://felix.apache.org + +Bundle-License: Apache License, Version 2.0 + +Bundle-Vendor: The Apache Software Foundation + +Export-Package: org.apache.felix.hc.api.* + diff --git a/healthcheck/api/pom.xml b/healthcheck/api/pom.xml new file mode 100644 index 00000000000..53ff39fbe63 --- /dev/null +++ b/healthcheck/api/pom.xml @@ -0,0 +1,113 @@ + + + + 4.0.0 + + + org.apache.felix + felix-parent + 6 + + + + org.apache.felix.healthcheck.api + 2.0.1-SNAPSHOT + + Apache Felix Health Check API + 2013 + + + The Felix Health Check API + + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/healthcheck/api + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/healthcheck/api + http://svn.apache.org/viewvc/felix/trunk/http/api/ + + + + + + + biz.aQute.bnd + bnd-maven-plugin + 4.1.0 + + + + bnd-process + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + org.apache.felix + maven-bundle-plugin + 4.1.0 + + + baseline + + baseline + + + + + + + + + + org.osgi + osgi.core + 6.0.0 + + + org.osgi + osgi.annotation + 6.0.1 + provided + + + org.slf4j + slf4j-api + 1.7.6 + provided + + + + junit + junit + 4.12 + test + + + + \ No newline at end of file diff --git a/healthcheck/api/src/main/java/org/apache/felix/hc/api/FormattingResultLog.java b/healthcheck/api/src/main/java/org/apache/felix/hc/api/FormattingResultLog.java new file mode 100644 index 00000000000..d466e46b7be --- /dev/null +++ b/healthcheck/api/src/main/java/org/apache/felix/hc/api/FormattingResultLog.java @@ -0,0 +1,115 @@ +/* + * 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 SF 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.hc.api; + +import java.text.NumberFormat; +import java.util.Locale; + +import org.osgi.annotation.versioning.ProviderType; +import org.slf4j.helpers.MessageFormatter; + +/** Utility that provides a logging-like facade on a ResultLog. */ +@ProviderType +public class FormattingResultLog extends ResultLog { + + private ResultLog.Entry createEntry(Result.Status status, String format, Object... args) { + return new ResultLog.Entry(status, MessageFormatter.arrayFormat(format, args).getMessage()); + } + private ResultLog.Entry createEntry(boolean isDebug, String format, Object... args) { + return new ResultLog.Entry(MessageFormatter.arrayFormat(format, args).getMessage(), isDebug); + } + + public void debug(String format, Object... args) { + add(createEntry(true, format, args)); + } + + public void info(String format, Object... args) { + add(createEntry(false, format, args)); + } + + public void warn(String format, Object... args) { + add(createEntry(Result.Status.WARN, format, args)); + } + + public void critical(String format, Object... args) { + add(createEntry(Result.Status.CRITICAL, format, args)); + } + + public void temporarilyUnavailable(String format, Object... args) { + add(createEntry(Result.Status.TEMPORARILY_UNAVAILABLE, format, args)); + } + + public void healthCheckError(String format, Object... args) { + add(createEntry(Result.Status.HEALTH_CHECK_ERROR, format, args)); + } + + /** Utility method to return any magnitude of milliseconds in a human readable format using the appropriate time unit (ms, sec, min) + * depending on the magnitude of the input. + * + * @param millis milliseconds + * @return a string with a number and a unit */ + public static String msHumanReadable(final long millis) { + + double number = millis; + final String[] units = new String[] { "ms", "sec", "min", "h", "days" }; + final double[] divisors = new double[] { 1000, 60, 60, 24 }; + + int magnitude = 0; + do { + double currentDivisor = divisors[Math.min(magnitude, divisors.length - 1)]; + if (number < currentDivisor) { + break; + } + number /= currentDivisor; + magnitude++; + } while (magnitude < units.length - 1); + NumberFormat format = NumberFormat.getNumberInstance(Locale.UK); + format.setMinimumFractionDigits(0); + format.setMaximumFractionDigits(1); + String result = format.format(number) + units[magnitude]; + return result; + } + + + /** + * Utility method to return any magnitude of bytes in a human readable format using the appropriate unit (kB, MB, GB) + * depending on the magnitude of the input. + * + * @param size in bytes + * @return a human readable result + */ + public static String bytesHumanReadable(double size) { + + double step = 1024, current = step; + final String SIZES[] = { "kB", "MB", "GB", "TB" }; + int i; + for (i = 0; i < SIZES.length - 1; ++i) { + if (size < current * step) { + break; + } + current *= step; + } + + String unit = SIZES[i]; + double value = size / current; + String retVal = String.format("%.1f", value) + unit; + return retVal; + } + + +} \ No newline at end of file diff --git a/healthcheck/api/src/main/java/org/apache/felix/hc/api/HealthCheck.java b/healthcheck/api/src/main/java/org/apache/felix/hc/api/HealthCheck.java new file mode 100644 index 00000000000..26a4c803b30 --- /dev/null +++ b/healthcheck/api/src/main/java/org/apache/felix/hc/api/HealthCheck.java @@ -0,0 +1,66 @@ +/* + * 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 SF 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.hc.api; + +import org.osgi.annotation.versioning.ConsumerType; + +/** The Health Check SPI provides a means to check a certain system aspect programmatically. Health checks return a result {@link Result}, + * for most cases it is most convenient to use {@link FormattingResultLog} that automatically derives the correct {@link Result.Status} from + * the log messages. + * + * Clients should not look up health checks directly but rather use the {@link org.apache.felix.hc.api.execution.HealthCheckExecutor} + * service and executed checks based on tags. + * + * If the {@link #MBEAN_NAME} service registration property is set, the health check is registered as an mbean and can be invoked by getting + * the MBean from the JMX registry. */ +@ConsumerType +public interface HealthCheck { + + /** Optional service property: the name of a health check. This name should be unique, however there might be more than one health check + * service with the same value for this property. The value of this property must be of type String. */ + String NAME = "hc.name"; + + /** Optional service property: tags for categorizing the health check services. The value of this property must be of type String or + * String array. */ + String TAGS = "hc.tags"; + + /** Optional service property: the name of the MBean for registering the health check as an MBean. If this property is missing the + * health check is not registered as a JMX MBean. If there is more than one service with the same value for this property, the one with + * the highest service ranking is registered only. The value of this property must be of type String. */ + String MBEAN_NAME = "hc.mbean.name"; + + /** Optional service property: If this property is set the health check will be executed asynchronously using the cron expression + * provided. */ + String ASYNC_CRON_EXPRESSION = "hc.async.cronExpression"; + + /** Optional service property: If this property is set the health check will be executed asynchronously every n seconds */ + String ASYNC_INTERVAL_IN_SEC = "hc.async.intervalInSec"; + + /** Optional service property: TTL for health check {@link Result}. The value of this property must be of type {@link Long} and is + * specified in ms. */ + String RESULT_CACHE_TTL_IN_MS = "hc.resultCacheTtlInMs"; + + /** Optional service property: If given, non-ok results from past executions will be taken into account as well for the given seconds + * (use Long.MAX_VALUE for indefinitely). Useful for unhealthy system states that disappear but might leave the system at an + * inconsistent state (e.g. an event queue overflow where somebody needs to intervene manually) or for checks that should only go back + * to OK with a delay (can be useful for load balancers). */ + String KEEP_NON_OK_RESULTS_STICKY_FOR_SEC = "hc.keepNonOkResultsStickyForSec"; + + /** Execute this health check and return a {@link Result}.*/ + Result execute(); +} diff --git a/healthcheck/api/src/main/java/org/apache/felix/hc/api/Result.java b/healthcheck/api/src/main/java/org/apache/felix/hc/api/Result.java new file mode 100644 index 00000000000..68737c05f64 --- /dev/null +++ b/healthcheck/api/src/main/java/org/apache/felix/hc/api/Result.java @@ -0,0 +1,75 @@ +/* + * 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 SF 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.hc.api; + +import java.util.Iterator; + +/** The result of executing a {@link HealthCheck} */ +public class Result implements Iterable { + + public enum Status { + OK, // system is fully operational + WARN, // attention required but system is operational + TEMPORARILY_UNAVAILABLE, // system is not operational, it may become available soon + CRITICAL, // critical problem exists, system should not be used + HEALTH_CHECK_ERROR // health check itself did not execute properly (no reliable status is known) + } + + protected final ResultLog resultLog; + + /** Build a single-value Result + * + * @param s if lower than OK, our status is set to OK */ + public Result(final Status s, final String explanation) { + resultLog = new ResultLog().add(new ResultLog.Entry(s, explanation)); + } + + /** Build a single-value Result with exception + * + * @param s if lower than OK, our status is set to OK */ + public Result(final Status s, final String explanation, final Exception e) { + resultLog = new ResultLog().add(new ResultLog.Entry(s, explanation, e)); + } + + /** Build a a Result based on a ResultLog, which can provide more details than a single-value Result. */ + public Result(final ResultLog log) { + resultLog = new ResultLog(log); + } + + /** True if our status is OK - provides a convenient way of checking that. */ + public boolean isOk() { + return getStatus().equals(Status.OK); + } + + /** Return our Status */ + public Status getStatus() { + return resultLog.getAggregateStatus(); + } + + /** Return an Iterator on the entries of our ResultLog */ + @Override + public Iterator iterator() { + return resultLog.iterator(); + } + + @Override + public String toString() { + return "Result [status=" + getStatus() + ", resultLog=" + resultLog + "]"; + } + +} \ No newline at end of file diff --git a/healthcheck/api/src/main/java/org/apache/felix/hc/api/ResultLog.java b/healthcheck/api/src/main/java/org/apache/felix/hc/api/ResultLog.java new file mode 100644 index 00000000000..7dd821ae9fb --- /dev/null +++ b/healthcheck/api/src/main/java/org/apache/felix/hc/api/ResultLog.java @@ -0,0 +1,141 @@ +/* + * 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 SF 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.hc.api; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import org.apache.felix.hc.api.Result.Status; + +/** The log of a Result, allows for providing multiple lines of information which are aggregated as a single Result. */ +public class ResultLog implements Iterable { + + private List entries = new LinkedList(); + private Status aggregateStatus; + + /** An entry in this log */ + public static class Entry { + private final Status status; + private final String message; + private final boolean isDebug; + private final Exception exception; + + public Entry(Status s, String message) { + this(s, message, false, null); + } + + public Entry(String message, boolean isDebug) { + this(Status.OK, message, isDebug, null); + } + public Entry(String message, boolean isDebug, Exception exception) { + this(Status.OK, message, isDebug, exception); + } + + public Entry(Status s, String message, Exception exception) { + this(s, message, false, exception); + } + + // private to not allow invalid combinations of isDebug=true and a status different than Status.OK + private Entry(Status s, String message, boolean isDebug, Exception exception) { + this.status = s; + this.message = message; + this.exception = exception; + this.isDebug = isDebug; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(getLogLevel()).append(" ").append(message); + if (exception != null) { + builder.append(" Exception: " + exception.getMessage()); + } + return builder.toString(); + } + + public Status getStatus() { + return status; + } + + public String getLogLevel() { + switch (status) { + case OK: + return isDebug ? "DEBUG" : "INFO"; + default: + return status.toString(); + } + } + + public String getMessage() { + return message; + } + + public Exception getException() { + return exception; + } + + public boolean isDebug() { + return isDebug; + } + } + + /** Build a log. Initial aggregate status is set to WARN, as an empty log is not considered ok. That's reset to OK before adding the + * first log entry, and then the status aggregation rules take over. */ + public ResultLog() { + aggregateStatus = Result.Status.WARN; + } + + /** Create a copy of the result log */ + public ResultLog(final ResultLog log) { + this.aggregateStatus = log.aggregateStatus; + this.entries = new ArrayList(log.entries); + } + + /** Add an entry to this log. The aggregate status of this is set to the highest of the current aggregate status and the new Entry's + * status */ + public ResultLog add(Entry e) { + if (entries.isEmpty()) { + aggregateStatus = Result.Status.OK; + } + entries.add(e); + if (e.getStatus().ordinal() > aggregateStatus.ordinal()) { + aggregateStatus = e.getStatus(); + } + return this; + } + + /** Return an Iterator on our entries */ + @Override + public Iterator iterator() { + return entries.iterator(); + } + + /** Return our aggregate status, i.e. the highest status of the entries added to this log. Starts at OK for an empty ResultLog, so + * cannot be lower than that. */ + public Status getAggregateStatus() { + return aggregateStatus; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("ResultLog: "); + sb.append(this.entries.toString()); + return sb.toString(); + } +} \ No newline at end of file diff --git a/healthcheck/api/src/main/java/org/apache/felix/hc/api/execution/HealthCheckExecutionOptions.java b/healthcheck/api/src/main/java/org/apache/felix/hc/api/execution/HealthCheckExecutionOptions.java new file mode 100644 index 00000000000..8ad1c4ef040 --- /dev/null +++ b/healthcheck/api/src/main/java/org/apache/felix/hc/api/execution/HealthCheckExecutionOptions.java @@ -0,0 +1,100 @@ +/* + * 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 SF 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.hc.api.execution; + +/** Options for behavior of health check execution. */ +public class HealthCheckExecutionOptions { + + private boolean forceInstantExecution = false; + private boolean combineTagsWithOr = false; + private int overrideGlobalTimeout = 0; + + @Override + public String toString() { + return "[HealthCheckExecutionOptions forceInstantExecution=" + forceInstantExecution + ", combineTagsWithOr=" + combineTagsWithOr + + ", overrideGlobalTimeout=" + overrideGlobalTimeout + "]"; + } + + /** If activated, this will ensure that asynchronous checks will be executed immediately. + * + * @param forceInstantExecution boolean flag */ + public HealthCheckExecutionOptions setForceInstantExecution(boolean forceInstantExecution) { + this.forceInstantExecution = forceInstantExecution; + return this; + } + + /** If activated, the given tags will be combined with a logical "or" instead of "and". + * + * @param combineTagsWithOr boolean flag */ + public HealthCheckExecutionOptions setCombineTagsWithOr(boolean combineTagsWithOr) { + this.combineTagsWithOr = combineTagsWithOr; + return this; + } + + /** Allows to override the global timeout for this particular execution of the health check. + * + * @param overrideGlobalTimeout timeout in ms to be used for this execution of the execution */ + public HealthCheckExecutionOptions setOverrideGlobalTimeout(int overrideGlobalTimeout) { + this.overrideGlobalTimeout = overrideGlobalTimeout; + return this; + } + + /** @return true if instant execution is turned on */ + public boolean isForceInstantExecution() { + return forceInstantExecution; + } + + /** @return true if combining tags with or is turned on */ + public boolean isCombineTagsWithOr() { + return combineTagsWithOr; + } + + /** @return the timeout to be used for this execution (overriding the global timeout) */ + public int getOverrideGlobalTimeout() { + return overrideGlobalTimeout; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (combineTagsWithOr ? 1231 : 1237); + result = prime * result + (forceInstantExecution ? 1231 : 1237); + result = prime * result + overrideGlobalTimeout; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + HealthCheckExecutionOptions other = (HealthCheckExecutionOptions) obj; + if (combineTagsWithOr != other.combineTagsWithOr) + return false; + if (forceInstantExecution != other.forceInstantExecution) + return false; + if (overrideGlobalTimeout != other.overrideGlobalTimeout) + return false; + return true; + } + +} \ No newline at end of file diff --git a/healthcheck/api/src/main/java/org/apache/felix/hc/api/execution/HealthCheckExecutionResult.java b/healthcheck/api/src/main/java/org/apache/felix/hc/api/execution/HealthCheckExecutionResult.java new file mode 100644 index 00000000000..4ab1f588170 --- /dev/null +++ b/healthcheck/api/src/main/java/org/apache/felix/hc/api/execution/HealthCheckExecutionResult.java @@ -0,0 +1,48 @@ +/* + * 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 SF 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.hc.api.execution; + +import java.util.Date; + +import org.apache.felix.hc.api.Result; +import org.osgi.annotation.versioning.ProviderType; + +/** Interface for health check executions via the {@link HealthCheckExecutor}. */ +@ProviderType +public interface HealthCheckExecutionResult { + + /** Get the result of the health check run. */ + Result getHealthCheckResult(); + + /** Get the elapsed time in ms */ + long getElapsedTimeInMs(); + + /** Get the date, the health check finished or if the execution timed out, the execution was aborted. + * + * @return The finished date of the execution. */ + Date getFinishedAt(); + + /** Returns true if the execution has timed out. In this case the result does not reflect the real result of the underlying check, but a + * result indicating the timeout. + * + * @return true if execution timed out. */ + boolean hasTimedOut(); + + /** Get the meta data about the health check service */ + HealthCheckMetadata getHealthCheckMetadata(); +} \ No newline at end of file diff --git a/healthcheck/api/src/main/java/org/apache/felix/hc/api/execution/HealthCheckExecutor.java b/healthcheck/api/src/main/java/org/apache/felix/hc/api/execution/HealthCheckExecutor.java new file mode 100644 index 00000000000..939db093ee7 --- /dev/null +++ b/healthcheck/api/src/main/java/org/apache/felix/hc/api/execution/HealthCheckExecutor.java @@ -0,0 +1,43 @@ +/* + * 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 SF 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.hc.api.execution; + +import java.util.List; + +import org.apache.felix.hc.api.HealthCheck; +import org.osgi.annotation.versioning.ProviderType; + +/** Executes health checks registered as OSGi services and implementing the interface {@link HealthCheck}. */ +@ProviderType +public interface HealthCheckExecutor { + + /** Executes all health checks matching the supplied filter options. If no options are supplied, all health checks are executed. + * + * @param selector filter selector + * @return List of results. The list might be empty. */ + List execute(HealthCheckSelector selector); + + /** Executes all health checks with the supplied filter options. If no options are supplied, all health checks are executed. + * + * @param selector filter selector + * @param options options for controlling execution behavior + * + * @return List of results. The list might be empty. */ + List execute(HealthCheckSelector selector, HealthCheckExecutionOptions options); + +} \ No newline at end of file diff --git a/healthcheck/api/src/main/java/org/apache/felix/hc/api/execution/HealthCheckMetadata.java b/healthcheck/api/src/main/java/org/apache/felix/hc/api/execution/HealthCheckMetadata.java new file mode 100644 index 00000000000..1705585837c --- /dev/null +++ b/healthcheck/api/src/main/java/org/apache/felix/hc/api/execution/HealthCheckMetadata.java @@ -0,0 +1,207 @@ +/* + * 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 SF 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.hc.api.execution; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import org.apache.felix.hc.api.HealthCheck; +import org.osgi.annotation.versioning.ProviderType; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + +/** This class helps retrieving meta data information about a health check service. + * + * @since 1.1 */ +@ProviderType +public class HealthCheckMetadata { + + private final String name; + + private final String mbeanName; + + private final String title; + + private final long serviceId; + + private final List tags; + + private final String asyncCronExpression; + private final Long asyncIntervalInSec; + + private final ServiceReference serviceReference; + + private final Long resultCacheTtlInMs; + + private final Long keepNonOkResultsStickyForSec; + + @Deprecated + private final Long warningsStickForMinutes; + @Deprecated + private final String WARNINGS_STICK_FOR_MINUTES = "hc.warningsStickForMinutes"; + + + public HealthCheckMetadata(final ServiceReference ref) { + this.serviceId = (Long) ref.getProperty(Constants.SERVICE_ID); + this.name = (String) ref.getProperty(HealthCheck.NAME); + this.mbeanName = (String) ref.getProperty(HealthCheck.MBEAN_NAME); + this.title = getHealthCheckTitle(ref); + this.tags = arrayPropertyToListOfStr(ref.getProperty(HealthCheck.TAGS)); + + this.asyncCronExpression = (String) ref.getProperty(HealthCheck.ASYNC_CRON_EXPRESSION); + this.asyncIntervalInSec = toLong(ref.getProperty(HealthCheck.ASYNC_INTERVAL_IN_SEC)); + + this.resultCacheTtlInMs = (Long) ref.getProperty(HealthCheck.RESULT_CACHE_TTL_IN_MS); + + this.keepNonOkResultsStickyForSec = toLong(ref.getProperty(HealthCheck.KEEP_NON_OK_RESULTS_STICKY_FOR_SEC)); + this.warningsStickForMinutes = toLong(ref.getProperty(WARNINGS_STICK_FOR_MINUTES)); + + this.serviceReference = ref; + } + + private Long toLong(Object configValue) { + if (configValue == null) { + return null; + } + if (configValue instanceof Long) { + return (Long) configValue; + } + return Long.valueOf(configValue.toString()); + } + + /** The name of the health check as defined through the {@link HealthCheck#NAME} property. + * + * @return The name or null */ + public String getName() { + return name; + } + + /** The mbean name of the health check as defined through the {@link HealthCheck#MBEAN_NAME} property. + * + * @return The mbean name or null */ + public String getMBeanName() { + return mbeanName; + } + + /** The title of the health check. If the health check has a name, this is used as the title. Otherwise the description, PID and service + * ID are checked for values. */ + public String getTitle() { + return title; + } + + /** Return the list of defined tags for this check as set through {@link HealthCheckMetadata#tags} + * + * @return */ + public List getTags() { + return tags; + } + + /** Return the cron expression used for asynchronous execution. */ + public String getAsyncCronExpression() { + return asyncCronExpression; + } + + /** Return the interval in sec used for asynchronous execution. */ + public Long getAsyncIntervalInSec() { + return asyncIntervalInSec; + } + + /** Return the service id. */ + public long getServiceId() { + return this.serviceId; + } + + /** Get the service reference. */ + public ServiceReference getServiceReference() { + return this.serviceReference; + } + + /** TTL for the result cache in ms. + * + * @return TTL for the result cache or null if not configured. */ + public Long getResultCacheTtlInMs() { + return resultCacheTtlInMs; + } + + /** Makes non-ok results stick for the given amount of time. + * + * @return Time to make non-ok results sticky in seconds. */ + public Long getKeepNonOkResultsStickyForSec() { + if(keepNonOkResultsStickyForSec==null && warningsStickForMinutes!=null) { + return warningsStickForMinutes * 60; + } + return keepNonOkResultsStickyForSec; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (int) (serviceId ^ (serviceId >>> 32)); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof HealthCheckMetadata)) { + return false; + } + final HealthCheckMetadata other = (HealthCheckMetadata) obj; + return serviceId == other.serviceId; + } + + @Override + public String toString() { + return "HealthCheck '" + name + "'"; + } + + private String getHealthCheckTitle(final ServiceReference ref) { + String name = (String) ref.getProperty(HealthCheck.NAME); + if (name == null || name.isEmpty()) { + final Object val = ref.getProperty(Constants.SERVICE_DESCRIPTION); + if (val != null) { + name = val.toString(); + } + } + if (name == null || name.isEmpty()) { + name = "HealthCheck:" + ref.getProperty(Constants.SERVICE_ID); + final Object val = ref.getProperty(Constants.SERVICE_PID); + String pid = null; + if (val instanceof String) { + pid = (String) val; + } else if (val instanceof String[]) { + pid = Arrays.toString((String[]) val); + } + if (pid != null && !pid.isEmpty()) { + name = name + " (" + pid + ")"; + } + } + return name; + } + + private List arrayPropertyToListOfStr(final Object arrayProp) { + List res = new LinkedList<>(); + if (arrayProp instanceof String) { + res.add((String) arrayProp); + } else if (arrayProp instanceof String[]) { + res.addAll(Arrays.asList((String[]) arrayProp)); + } + return res; + } +} diff --git a/healthcheck/api/src/main/java/org/apache/felix/hc/api/execution/HealthCheckSelector.java b/healthcheck/api/src/main/java/org/apache/felix/hc/api/execution/HealthCheckSelector.java new file mode 100644 index 00000000000..150ed668555 --- /dev/null +++ b/healthcheck/api/src/main/java/org/apache/felix/hc/api/execution/HealthCheckSelector.java @@ -0,0 +1,98 @@ +/* + * 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.hc.api.execution; + +import java.util.Arrays; + +import org.osgi.annotation.versioning.ProviderType; + +/** Parameter class to pass a set of tags and names to the filter. */ +@ProviderType +public final class HealthCheckSelector { + + private String[] tags; + private String[] names; + + public String[] tags() { + return tags; + } + + public String[] names() { + return names; + } + + private HealthCheckSelector() { + } + + /** Copy the specified names into the current tags array. + * + * @param tags the new tags. Specify null to clear the current tag array + * @return this */ + public HealthCheckSelector withTags(String... tags) { + if (this.tags == null) { + this.tags = tags; + } else if (tags != null) { + String[] copy = Arrays.copyOf(this.tags, this.tags.length + tags.length); + System.arraycopy(tags, 0, copy, this.tags.length, tags.length); + this.tags = copy; + } else { + this.tags = null; + } + return this; + } + + /** Copy the specified names into the current names array. + * + * @param names the new names. Specify null to clear the current name array + * @return this */ + public HealthCheckSelector withNames(String... names) { + if (this.names == null) { + this.names = names; + } else if (names != null) { + String[] copy = Arrays.copyOf(this.names, this.names.length + names.length); + System.arraycopy(names, 0, copy, this.names.length, names.length); + this.names = copy; + } else { + this.names = null; + } + return this; + } + + public static HealthCheckSelector empty() { + return new HealthCheckSelector(); + } + + public static HealthCheckSelector tags(String... tags) { + HealthCheckSelector selector = new HealthCheckSelector(); + selector.tags = tags; + return selector; + } + + public static HealthCheckSelector names(String... names) { + HealthCheckSelector selector = new HealthCheckSelector(); + selector.names = names; + return selector; + } + + @Override + public String toString() { + return "HealthCheckSelector{" + + "tags=" + (tags == null ? "*" : Arrays.toString(tags)) + + ", names=" + (names == null ? "*" : Arrays.toString(names)) + + '}'; + } +} diff --git a/healthcheck/api/src/main/java/org/apache/felix/hc/api/execution/package-info.java b/healthcheck/api/src/main/java/org/apache/felix/hc/api/execution/package-info.java new file mode 100644 index 00000000000..9279ec5e2be --- /dev/null +++ b/healthcheck/api/src/main/java/org/apache/felix/hc/api/execution/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +@Version("2.0.0") +package org.apache.felix.hc.api.execution; + +import org.osgi.annotation.versioning.Version; diff --git a/healthcheck/api/src/main/java/org/apache/felix/hc/api/package-info.java b/healthcheck/api/src/main/java/org/apache/felix/hc/api/package-info.java new file mode 100644 index 00000000000..5058bfd97e0 --- /dev/null +++ b/healthcheck/api/src/main/java/org/apache/felix/hc/api/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +@Version("2.0.0") +package org.apache.felix.hc.api; + +import org.osgi.annotation.versioning.Version; diff --git a/healthcheck/api/src/test/java/org/apache/felix/hc/api/FormattingResultLogTest.java b/healthcheck/api/src/test/java/org/apache/felix/hc/api/FormattingResultLogTest.java new file mode 100644 index 00000000000..5875e0f9137 --- /dev/null +++ b/healthcheck/api/src/test/java/org/apache/felix/hc/api/FormattingResultLogTest.java @@ -0,0 +1,44 @@ +/* + * 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 SF 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.hc.api; + +import static org.apache.felix.hc.api.FormattingResultLog.bytesHumanReadable; +import static org.apache.felix.hc.api.FormattingResultLog.msHumanReadable; +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class FormattingResultLogTest { + + @Test + public void testBytesHumanReadable() { + assertEquals("5.0MB", bytesHumanReadable(5d*1024*1024)); + assertEquals("2.0GB", bytesHumanReadable(2d*1024*1024*1024)); + assertEquals("706.1kB", bytesHumanReadable(722998)); + } + + @Test + public void testMSHumanReadable() { + assertEquals("320ms", msHumanReadable(320)); + assertEquals("5sec", msHumanReadable(5*1000)); + assertEquals("1min", msHumanReadable(60*1000)); + assertEquals("1.2min", msHumanReadable(72*1000)); + assertEquals("3h", msHumanReadable(3*60*60*1000)); + } + +} diff --git a/healthcheck/api/src/test/java/org/apache/felix/hc/api/ResultLogTest.java b/healthcheck/api/src/test/java/org/apache/felix/hc/api/ResultLogTest.java new file mode 100644 index 00000000000..302aef2db43 --- /dev/null +++ b/healthcheck/api/src/test/java/org/apache/felix/hc/api/ResultLogTest.java @@ -0,0 +1,91 @@ +/* + * 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 SF 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.hc.api; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.util.Iterator; + +import org.junit.Before; +import org.junit.Test; + +public class ResultLogTest { + + private ResultLog log; + + @Before + public void setup() { + log = new ResultLog(); + } + + @Test + public void testEmptyLogIsNotOk() { + assertEquals(Result.Status.WARN, log.getAggregateStatus()); + assertFalse(log.iterator().hasNext()); + } + + @Test + public void testSetStatusGoingUp() { + log.add(new ResultLog.Entry(Result.Status.OK, "argh")); + assertEquals(Result.Status.OK, log.getAggregateStatus()); + log.add(new ResultLog.Entry(Result.Status.WARN, "argh")); + assertEquals(Result.Status.WARN, log.getAggregateStatus()); + log.add(new ResultLog.Entry(Result.Status.CRITICAL, "argh")); + assertEquals(Result.Status.CRITICAL, log.getAggregateStatus()); + log.add(new ResultLog.Entry(Result.Status.HEALTH_CHECK_ERROR, "argh")); + assertEquals(Result.Status.HEALTH_CHECK_ERROR, log.getAggregateStatus()); + } + + @Test + public void testSetStatusGoingDownHCE() { + log.add(new ResultLog.Entry(Result.Status.HEALTH_CHECK_ERROR, "argh")); + assertEquals(Result.Status.HEALTH_CHECK_ERROR, log.getAggregateStatus()); + log.add(new ResultLog.Entry(Result.Status.CRITICAL, "argh")); + assertEquals(Result.Status.HEALTH_CHECK_ERROR, log.getAggregateStatus()); + } + + @Test + public void testSetStatusGoingDownCRIT() { + log.add(new ResultLog.Entry(Result.Status.CRITICAL, "argh")); + assertEquals(Result.Status.CRITICAL, log.getAggregateStatus()); + log.add(new ResultLog.Entry(Result.Status.WARN, "argh")); + assertEquals(Result.Status.CRITICAL, log.getAggregateStatus()); + } + + @Test + public void testSetStatusGoingDownWARN() { + log.add(new ResultLog.Entry(Result.Status.WARN, "argh")); + assertEquals(Result.Status.WARN, log.getAggregateStatus()); + log.add(new ResultLog.Entry(Result.Status.OK, "argh")); + assertEquals(Result.Status.WARN, log.getAggregateStatus()); + } + + @Test + public void testLogEntries() { + log.add(new ResultLog.Entry(Result.Status.OK, "ok 1")); + log.add(new ResultLog.Entry(Result.Status.WARN, "warn 3")); + log.add(new ResultLog.Entry(Result.Status.CRITICAL, "critical 4")); + + final Iterator it = log.iterator(); + assertEquals("INFO ok 1", it.next().toString()); + assertEquals("WARN warn 3", it.next().toString()); + assertEquals("CRITICAL critical 4", it.next().toString()); + assertFalse(it.hasNext()); + } +} diff --git a/healthcheck/api/src/test/java/org/apache/felix/hc/api/ResultTest.java b/healthcheck/api/src/test/java/org/apache/felix/hc/api/ResultTest.java new file mode 100644 index 00000000000..31dcb81bdb3 --- /dev/null +++ b/healthcheck/api/src/test/java/org/apache/felix/hc/api/ResultTest.java @@ -0,0 +1,63 @@ +/* + * 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 SF 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.hc.api; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +public class ResultTest { + + private final AtomicInteger counter = new AtomicInteger(); + + private void assertSingleResult(Result.Status toSet, Result.Status expected, boolean expectOk) { + final String msg = "test " + counter.incrementAndGet(); + final Result r = new Result(toSet, msg); + assertEquals(expected, r.getStatus()); + assertEquals(expectOk, r.isOk()); + assertTrue(r.iterator().hasNext()); + assertEquals(new ResultLog.Entry(expected, msg).toString(), r.iterator().next().toString()); + } + + @Test + public void testSingleResult() { + assertSingleResult(Result.Status.OK, Result.Status.OK, true); + assertSingleResult(Result.Status.WARN, Result.Status.WARN, false); + assertSingleResult(Result.Status.CRITICAL, Result.Status.CRITICAL, false); + assertSingleResult(Result.Status.HEALTH_CHECK_ERROR, Result.Status.HEALTH_CHECK_ERROR, false); + } + + @Test + public void testLog() { + final ResultLog log = new ResultLog(); + log.add(new ResultLog.Entry(Result.Status.OK, "some msg")); + log.add(new ResultLog.Entry(Result.Status.WARN, "problematic condition")); + + final Result result = new Result(log); + assertEquals(Result.Status.WARN, result.getStatus()); + + final StringBuilder sb = new StringBuilder(); + for (ResultLog.Entry e : result) { + sb.append(e.toString()).append("#"); + } + assertEquals("INFO some msg#WARN problematic condition#", sb.toString()); + } +} diff --git a/healthcheck/core/LICENSE b/healthcheck/core/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/healthcheck/core/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/healthcheck/core/bnd.bnd b/healthcheck/core/bnd.bnd new file mode 100644 index 00000000000..44ae747870e --- /dev/null +++ b/healthcheck/core/bnd.bnd @@ -0,0 +1,17 @@ +Bundle-Category: healthcheck + +Bundle-Description: ${project.description} + +Bundle-DocURL: https://felix.apache.org + +Bundle-License: Apache License, Version 2.0 + +Bundle-Vendor: The Apache Software Foundation + +Import-Package: org.quartz*;resolution:="optional", * + +Conditional-Package: org.apache.felix.utils.* + +-removeheaders:\ + Include-Resource,\ + Private-Package diff --git a/healthcheck/core/pom.xml b/healthcheck/core/pom.xml new file mode 100644 index 00000000000..f02e007cf6a --- /dev/null +++ b/healthcheck/core/pom.xml @@ -0,0 +1,260 @@ + + + + 4.0.0 + + + org.apache.felix + felix-parent + 6 + + + + org.apache.felix.healthcheck.core + 2.0.1-SNAPSHOT + + Apache Felix Health Check Core + 2013 + + + The Felix Health Check Core + + + + 4.11.0 + 2.4.3 + INFO + false + ${project.build.directory}/${project.build.finalName}.jar + + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/healthcheck/core + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/healthcheck/core + http://svn.apache.org/viewvc/felix/trunk/http/core/ + + + + + + + biz.aQute.bnd + bnd-maven-plugin + 4.1.0 + + + + bnd-process + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + org.jacoco + jacoco-maven-plugin + 0.8.2 + + + prepare-agent-integration + + prepare-agent-integration + + + coverage.command + + + + + + org.apache.servicemix.tooling + depends-maven-plugin + 1.4.0 + + + + generate-depends-file + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + + ${org.ops4j.pax.logging.DefaultServiceLog.level} + ${felix.shell} + ${coverage.command} + ${project.bundle.file} + + + + + + + + + org.osgi + osgi.core + 6.0.0 + provided + + + org.osgi + osgi.cmpn + 6.0.0 + provided + + + + org.apache.felix + org.apache.felix.healthcheck.api + 2.0.0 + provided + + + + org.slf4j + slf4j-api + 1.7.6 + provided + + + + org.apache.servicemix.bundles + org.apache.servicemix.bundles.quartz + 2.3.0_2 + provided + + true + + + + org.apache.commons + commons-lang3 + 3.4 + provided + + + + javax.servlet + javax.servlet-api + 3.1.0 + provided + + + + + org.apache.felix + org.apache.felix.utils + 1.11.0 + provided + + + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-core + 1.9.5 + test + + + rhino + js + 1.6R6 + test + + + org.slf4j + slf4j-simple + 1.7.6 + test + + + org.ops4j.pax.exam + pax-exam-container-forked + ${pax-exam.version} + test + + + org.ops4j.pax.exam + pax-exam-junit4 + ${pax-exam.version} + test + + + org.ops4j.pax.exam + pax-exam-link-mvn + ${pax-exam.version} + test + + + org.ops4j.pax.url + pax-url-aether + ${pax-link.version} + test + + + org.ops4j.pax.url + pax-url-wrap + ${pax-link.version} + test + + + org.apache.geronimo.specs + geronimo-atinject_1.0_spec + 1.0 + test + + + org.apache.felix + org.apache.felix.framework + 5.6.10 + test + + + + + diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/CompositeHealthCheck.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/CompositeHealthCheck.java new file mode 100644 index 00000000000..80712502dab --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/CompositeHealthCheck.java @@ -0,0 +1,189 @@ +/* + * 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 SF 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.hc.core.impl; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.felix.hc.api.FormattingResultLog; +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.Result.Status; +import org.apache.felix.hc.api.execution.HealthCheckExecutionOptions; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckExecutor; +import org.apache.felix.hc.api.execution.HealthCheckMetadata; +import org.apache.felix.hc.api.execution.HealthCheckSelector; +import org.apache.felix.hc.core.impl.util.HealthCheckFilter; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.service.component.ComponentConstants; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.metatype.annotations.Designate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** {@link HealthCheck} that executes a number of other HealthChecks, selected by their tags, and merges their Results. */ + +@Component(service = HealthCheck.class, configurationPolicy = ConfigurationPolicy.REQUIRE) +@Designate(ocd = CompositeHealthCheckConfiguration.class, factory = true) +public class CompositeHealthCheck implements HealthCheck { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + static final String PROP_FILTER_TAGS = "filter.tags"; + + private String[] filterTags; + + private boolean combineTagsWithOr; + + @Reference + private HealthCheckExecutor healthCheckExecutor; + + private BundleContext bundleContext; + private HealthCheckFilter healthCheckFilter; + + private volatile ComponentContext componentContext; + + @Activate + protected void activate(final CompositeHealthCheckConfiguration configuration, final ComponentContext ctx) { + bundleContext = ctx.getBundleContext(); + componentContext = ctx; + healthCheckFilter = new HealthCheckFilter(bundleContext); + + filterTags = configuration.filter_tags(); + combineTagsWithOr = configuration.filter_combineTagsWithOr(); + log.debug("Activated, will select HealthCheck having tags {} {}", Arrays.asList(filterTags), + combineTagsWithOr ? "using OR" : "using AND"); + } + + @Deactivate + protected void deactivate() { + bundleContext = null; + healthCheckFilter = null; + componentContext = null; + } + + @Override + public Result execute() { + final ComponentContext localCtx = this.componentContext; + final ServiceReference referenceToThis = localCtx == null ? null : localCtx.getServiceReference(); + Result result = referenceToThis == null ? null : checkForRecursion(referenceToThis, new HashSet()); + if (result != null) { + // return recursion error + return result; + } + + FormattingResultLog resultLog = new FormattingResultLog(); + HealthCheckExecutionOptions options = new HealthCheckExecutionOptions(); + options.setCombineTagsWithOr(combineTagsWithOr); + List executionResults = healthCheckExecutor.execute(HealthCheckSelector.tags(filterTags), options); + resultLog.debug("Executing {} HealthChecks selected by tags {}", executionResults.size(), Arrays.asList(filterTags)); + result = new CompositeResult(resultLog, executionResults); + + return result; + } + + Result checkForRecursion(ServiceReference hcReference, Set alreadyBannedTags) { + + HealthCheckMetadata thisCheckMetadata = new HealthCheckMetadata(hcReference); + + Set bannedTagsForThisCompositeCheck = new HashSet(); + bannedTagsForThisCompositeCheck.addAll(alreadyBannedTags); + bannedTagsForThisCompositeCheck.addAll(thisCheckMetadata.getTags()); + + String[] tagsForIncludedChecksArr = toStringArray(hcReference.getProperty(PROP_FILTER_TAGS), new String[0]); + Set tagsForIncludedChecks = new HashSet(Arrays.asList(tagsForIncludedChecksArr)); + + log.debug("HC {} has banned tags {}", thisCheckMetadata.getName(), bannedTagsForThisCompositeCheck); + log.debug("tagsForIncludedChecks {}", tagsForIncludedChecks); + + // is this HC ok? + Set intersection = new HashSet(); + intersection.addAll(bannedTagsForThisCompositeCheck); + intersection.retainAll(tagsForIncludedChecks); + + if (!intersection.isEmpty()) { + return new Result(Status.HEALTH_CHECK_ERROR, + "INVALID CONFIGURATION: Cycle detected in composite health check hierarchy. Health check '" + + thisCheckMetadata.getName() + + "' (" + hcReference.getProperty(Constants.SERVICE_ID) + ") must not have tag(s) " + intersection + + " as a composite check in the hierarchy is itself already tagged alike (tags assigned to composite checks: " + + bannedTagsForThisCompositeCheck + ")"); + } + + // check each sub composite check + ServiceReference[] hcRefsOfCompositeCheck = healthCheckFilter + .getHealthCheckServiceReferences(HealthCheckSelector.tags(tagsForIncludedChecksArr), combineTagsWithOr); + for (ServiceReference hcRefOfCompositeCheck : hcRefsOfCompositeCheck) { + if (CompositeHealthCheck.class.getName().equals(hcRefOfCompositeCheck.getProperty(ComponentConstants.COMPONENT_NAME))) { + log.debug("Checking sub composite HC {}, {}", hcRefOfCompositeCheck, + hcRefOfCompositeCheck.getProperty(ComponentConstants.COMPONENT_NAME)); + Result result = checkForRecursion(hcRefOfCompositeCheck, bannedTagsForThisCompositeCheck); + if (result != null) { + // found recursion + return result; + } + } + + } + + // no recursion detected + return null; + + } + + public static String[] toStringArray(Object propValue, String[] defaultArray) { + if (propValue instanceof String[]) { + return (String[]) propValue; + } else if (propValue instanceof String) { + return new String[] { (String) propValue }; + } else if (propValue == null) { + return defaultArray; + } + return defaultArray; + } + + void setHealthCheckFilter(HealthCheckFilter healthCheckFilter) { + this.healthCheckFilter = healthCheckFilter; + } + + void setFilterTags(String[] filterTags) { + this.filterTags = filterTags; + } + + void setCombineTagsWithOr(boolean combineTagsWithOr) { + this.combineTagsWithOr = combineTagsWithOr; + } + + void setHealthCheckExecutor(HealthCheckExecutor healthCheckExecutor) { + this.healthCheckExecutor = healthCheckExecutor; + } + + void setComponentContext(ComponentContext ctx) { + this.componentContext = ctx; + } +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/CompositeHealthCheckConfiguration.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/CompositeHealthCheckConfiguration.java new file mode 100644 index 00000000000..32760b1371e --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/CompositeHealthCheckConfiguration.java @@ -0,0 +1,43 @@ +/* + * 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 SF 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.hc.core.impl; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@ObjectClassDefinition(name = "Apache Felix Composite Health Check", description = "Executes a set of health checks, selected by tags.") +@interface CompositeHealthCheckConfiguration { + + @AttributeDefinition(name = "Name", description = "Name of this health check.") + String hc_name() default ""; + + @AttributeDefinition(name = "Tags", description = "List of tags for this health check, used to select subsets of health checks for execution e.g. by a composite health check.") + String[] hc_tags() default {}; + + @AttributeDefinition(name = "MBean Name", description = "Name of the MBean to create for this health check. If empty, no MBean is registered.") + String hc_mbean_name() default ""; + + // + + @AttributeDefinition(name = "Filter Tags", description = "Tags used to select which health checks the composite health check executes.") + String[] filter_tags() default {}; + + @AttributeDefinition(name = "Combine Tags With Or", description = "Tags used to select which health checks the composite health check executes.") + boolean filter_combineTagsWithOr() default false; + +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/CompositeResult.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/CompositeResult.java new file mode 100644 index 00000000000..847101cd5e6 --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/CompositeResult.java @@ -0,0 +1,47 @@ +/* + * 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 SF 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.hc.core.impl; + +import static org.apache.felix.hc.api.FormattingResultLog.msHumanReadable; + +import java.util.List; + +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.ResultLog; +import org.apache.felix.hc.api.ResultLog.Entry; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckMetadata; + +public class CompositeResult extends Result { + + public CompositeResult(ResultLog log, List executionResults) { + super(log); + + for (HealthCheckExecutionResult executionResult : executionResults) { + HealthCheckMetadata healthCheckMetadata = executionResult.getHealthCheckMetadata(); + Result healthCheckResult = executionResult.getHealthCheckResult(); + for (Entry entry : healthCheckResult) { + resultLog.add(new ResultLog.Entry(entry.getStatus(), healthCheckMetadata.getName() + ": " + entry.getMessage(), + entry.getException())); + } + resultLog.add(new ResultLog.Entry(healthCheckMetadata.getName() + " finished after " + + msHumanReadable(executionResult.getElapsedTimeInMs()) + (executionResult.hasTimedOut() ? " (timed out)" : ""), true)); + } + } + +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/JmxAdjustableStatusHealthCheck.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/JmxAdjustableStatusHealthCheck.java new file mode 100644 index 00000000000..ed1c403fdee --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/JmxAdjustableStatusHealthCheck.java @@ -0,0 +1,234 @@ +/* + * 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 SF 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.hc.core.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; + +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.AttributeNotFoundException; +import javax.management.DynamicMBean; +import javax.management.InvalidAttributeValueException; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanException; +import javax.management.MBeanInfo; +import javax.management.MBeanOperationInfo; +import javax.management.MBeanParameterInfo; +import javax.management.ReflectionException; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.Result.Status; +import org.apache.felix.hc.core.impl.util.AdhocStatusHealthCheck; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Allows to dynamically add a health check that returns WARN or CRITICAL for certain tags for testing purposes or go-live sequences. Uses an MBean to + * add/remove the DynamicTestingHealthCheck dynamically. */ +@Component +public class JmxAdjustableStatusHealthCheck { + private static final Logger LOG = LoggerFactory.getLogger(JmxAdjustableStatusHealthCheck.class); + + public static final String OBJECT_NAME = "org.apache.felix.healthcheck:type=AdjustableStatusHealthCheck"; + + private BundleContext bundleContext; + + private ServiceRegistration mbeanRegistration = null; + private ServiceRegistration healthCheckRegistration = null; + + @Activate + protected final void activate(final ComponentContext context) { + this.bundleContext = context.getBundleContext(); + registerMbean(); + } + + @Deactivate + protected final void deactivate(final ComponentContext context) { + unregisterMbean(); + unregisterDynamicHealthCheck(); + } + + private void registerMbean() { + final Dictionary mbeanProps = new Hashtable(); + mbeanProps.put("jmx.objectname", OBJECT_NAME); + AdjustableHealthCheckStatusMBean adjustableHealthCheckStatusMBean = new AdjustableHealthCheckStatusMBean(); + this.mbeanRegistration = bundleContext.registerService(DynamicMBean.class.getName(), adjustableHealthCheckStatusMBean, mbeanProps); + LOG.debug("Registered mbean {} as {}", adjustableHealthCheckStatusMBean, OBJECT_NAME); + } + + private void unregisterMbean() { + if (this.mbeanRegistration != null) { + this.mbeanRegistration.unregister(); + this.mbeanRegistration = null; + LOG.debug("Unregistered mbean AdjustableHealthCheckStatusMBean"); + } + } + + /* synchronized as potentially multiple users can run JMX operations */ + private synchronized void registerDynamicHealthCheck(Result.Status status, String[] tags) { + unregisterDynamicHealthCheck(); + HealthCheck healthCheck = new AdhocStatusHealthCheck(status, "Set dynamically via JMX bean " + JmxAdjustableStatusHealthCheck.OBJECT_NAME); + Dictionary props = new Hashtable(); + props.put(HealthCheck.NAME, "JMX Adhoc Result"); + props.put(HealthCheck.TAGS, tags); + + healthCheckRegistration = bundleContext.registerService(HealthCheck.class.getName(), healthCheck, props); + } + + /* synchronized as potentially multiple users can run JMX operations */ + private synchronized void unregisterDynamicHealthCheck() { + if (this.healthCheckRegistration != null) { + this.healthCheckRegistration.unregister(); + this.healthCheckRegistration = null; + LOG.debug("Unregistered DynamicTestingHealthCheck"); + } + } + + private class AdjustableHealthCheckStatusMBean implements DynamicMBean { + + private static final String OP_RESET = "reset"; + private static final String OP_ADD_WARN_RESULT_FOR_TAGS = "addWarnResultForTags"; + private static final String OP_ADD_TEMPORARILY_UNAVAILABLE_RESULT_FOR_TAGS = "addTemporarilyUnavailableResultForTags"; + private static final String OP_ADD_CRITICAL_RESULT_FOR_TAGS = "addCriticalResultForTags"; + + private static final String ATT_TAGS = "tags"; + private static final String ATT_STATUS = "status"; + + private static final String STATUS_INACTIVE = "INACTIVE"; + + /** The mbean info. */ + private final MBeanInfo mbeanInfo; + + private List tags = new ArrayList(); + private String status = STATUS_INACTIVE; + + public AdjustableHealthCheckStatusMBean() { + this.mbeanInfo = this.createMBeanInfo(); + } + + @Override + public Object getAttribute(final String attribute) throws AttributeNotFoundException, MBeanException, ReflectionException { + + if (ATT_TAGS.equals(attribute)) { + return StringUtils.join(tags, ","); + } else if (ATT_STATUS.equals(attribute)) { + return status.toString(); + } else { + throw new AttributeNotFoundException("Attribute " + attribute + " not found."); + } + } + + @Override + public AttributeList getAttributes(final String[] attributes) { + final AttributeList result = new AttributeList(); + for (String att : attributes) { + try { + result.add(new Attribute(att, getAttribute(att))); + } catch (Exception e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + return result; + } + + private MBeanInfo createMBeanInfo() { + final List attrs = new ArrayList(); + + attrs.add(new MBeanAttributeInfo(ATT_TAGS, String.class.getName(), "Tags", true, false, false)); + attrs.add(new MBeanAttributeInfo(ATT_STATUS, String.class.getName(), "Status", true, false, false)); + + MBeanParameterInfo[] params = new MBeanParameterInfo[] { + new MBeanParameterInfo(ATT_TAGS, "java.lang.String", "Comma separated list of tags") }; + + final List ops = new ArrayList(); + ops.add(new MBeanOperationInfo(OP_RESET, "Resets this testing mechanism and removes the failing HC", + new MBeanParameterInfo[0], "java.lang.String", MBeanOperationInfo.ACTION)); + ops.add(new MBeanOperationInfo(OP_ADD_TEMPORARILY_UNAVAILABLE_RESULT_FOR_TAGS, "Adds a TEMPORARILY_UNAVAILABLE result for the given tags", + params, "java.lang.String", MBeanOperationInfo.ACTION)); + ops.add(new MBeanOperationInfo(OP_ADD_CRITICAL_RESULT_FOR_TAGS, "Adds a CRITICAL result for the given tags", + params, "java.lang.String", MBeanOperationInfo.ACTION)); + ops.add(new MBeanOperationInfo(OP_ADD_WARN_RESULT_FOR_TAGS, "Adds a WARN result for the given tags", + params, "java.lang.String", MBeanOperationInfo.ACTION)); + + return new MBeanInfo(this.getClass().getName(), + "Adjustable Health Check", attrs.toArray(new MBeanAttributeInfo[attrs.size()]), null, + ops.toArray(new MBeanOperationInfo[ops.size()]), null); + } + + @Override + public MBeanInfo getMBeanInfo() { + return this.mbeanInfo; + } + + @Override + public Object invoke(final String actionName, final Object[] params, final String[] signature) + throws MBeanException, ReflectionException { + + Status newStatus = null; + + tags = params.length > 0 ? Arrays.asList(params[0].toString().split("[,; ]+")) : Arrays.asList(""); + + if (OP_RESET.equals(actionName)) { + unregisterDynamicHealthCheck(); + LOG.info("JMX-adjustable Health Check was reset"); + status = STATUS_INACTIVE; + return "Reset successful"; + } else if (OP_ADD_TEMPORARILY_UNAVAILABLE_RESULT_FOR_TAGS.equals(actionName)) { + newStatus = Result.Status.TEMPORARILY_UNAVAILABLE; + } else if (OP_ADD_CRITICAL_RESULT_FOR_TAGS.equals(actionName)) { + newStatus = Result.Status.CRITICAL; + } else if (OP_ADD_WARN_RESULT_FOR_TAGS.equals(actionName)) { + newStatus = Result.Status.WARN; + } else { + throw new MBeanException( + new UnsupportedOperationException(getClass().getSimpleName() + " does not support operation " + actionName)); + } + + status = newStatus.toString(); + registerDynamicHealthCheck(newStatus, tags.toArray(new String[tags.size()])); + LOG.info("Activated JMX-adjustable Health Check with status "+newStatus+" and tags " + StringUtils.join(tags, ",")); + return "Added check with result "+newStatus; + + } + + @Override + public void setAttribute(final Attribute attribute) + throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException, + ReflectionException { + throw new MBeanException( + new UnsupportedOperationException(getClass().getSimpleName() + " does not support setting attributes.")); + } + + @Override + public AttributeList setAttributes(final AttributeList attributes) { + return new AttributeList(); + } + } + +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/CombinedExecutionResult.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/CombinedExecutionResult.java new file mode 100644 index 00000000000..7c9c9ed3a1c --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/CombinedExecutionResult.java @@ -0,0 +1,86 @@ +/* + * 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 SF 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.hc.core.impl.executor; + +import java.util.Date; +import java.util.List; + +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.Result.Status; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckMetadata; + +/** Used to group execution results. */ +public class CombinedExecutionResult implements HealthCheckExecutionResult { + + final List executionResults; + final Result overallResult; + + public CombinedExecutionResult(List executionResults) { + this.executionResults = executionResults; + Result.Status mostSevereStatus = Result.Status.OK; + for (HealthCheckExecutionResult executionResult : executionResults) { + Status status = executionResult.getHealthCheckResult().getStatus(); + if (status.ordinal() > mostSevereStatus.ordinal()) { + mostSevereStatus = status; + } + } + overallResult = new Result(mostSevereStatus, "Overall status " + mostSevereStatus); + } + + @Override + public Result getHealthCheckResult() { + return overallResult; + } + + @Override + public long getElapsedTimeInMs() { + long maxElapsed = 0; + for(HealthCheckExecutionResult result: executionResults) { + maxElapsed = Math.max(maxElapsed, result.getElapsedTimeInMs()); + } + return maxElapsed; + } + + @Override + public Date getFinishedAt() { + Date latestFinishedAt = null; + for(HealthCheckExecutionResult result: executionResults) { + if(latestFinishedAt == null || latestFinishedAt.before(result.getFinishedAt())) { + latestFinishedAt = result.getFinishedAt(); + } + } + return latestFinishedAt; + } + + @Override + public boolean hasTimedOut() { + for(HealthCheckExecutionResult result: executionResults) { + if(result.hasTimedOut()) { + return true; + } + } + return false; + } + + @Override + public HealthCheckMetadata getHealthCheckMetadata() { + throw new UnsupportedOperationException(); + } + +} \ No newline at end of file diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/ExecutionResult.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/ExecutionResult.java new file mode 100644 index 00000000000..b14759ded18 --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/ExecutionResult.java @@ -0,0 +1,124 @@ +/* + * 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 SF 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.hc.core.impl.executor; + +import java.text.Collator; +import java.util.Date; + +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckMetadata; + +/** The result of executing a {@link HealthCheck}. */ +public class ExecutionResult implements Comparable, HealthCheckExecutionResult { + + private final Result resultFromHC; + + private final HealthCheckMetadata metaData; + + private final Date finishedAt; + + private final long elapsedTimeInMs; + + private final boolean timedOut; + + public ExecutionResult(final HealthCheckMetadata metadata, + final Result simpleResult, + final long elapsedTimeInMs, + final boolean timedout) { + this(metadata, simpleResult, new Date(), elapsedTimeInMs, timedout); + } + + /** Full constructor */ + public ExecutionResult(final HealthCheckMetadata metadata, + final Result simpleResult, + final Date finishedAt, + final long elapsedTimeInMs, + final boolean timedout) { + this.metaData = metadata; + this.resultFromHC = simpleResult; + this.finishedAt = finishedAt; + this.elapsedTimeInMs = elapsedTimeInMs; + this.timedOut = timedout; + } + + /** Shortcut constructor for a result */ + public ExecutionResult(final HealthCheckMetadata metadata, + final Result simpleResult, + final long elapsedTimeInMs) { + this(metadata, simpleResult, elapsedTimeInMs, false); + } + + /** Shortcut constructor to create error/timed out result. */ + public ExecutionResult(final HealthCheckMetadata metadata, + final Result.Status status, + final String errorMessage, + final long elapsedTime, boolean timedOut) { + this(metadata, new Result(status, errorMessage), elapsedTime, timedOut); + } + + @Override + public Result getHealthCheckResult() { + return this.resultFromHC; + } + + @Override + public String toString() { + return "ExecutionResult [status=" + this.resultFromHC.getStatus() + + ", finishedAt=" + finishedAt + + ", elapsedTimeInMs=" + elapsedTimeInMs + + ", timedOut=" + timedOut + + "]"; + } + + @Override + public long getElapsedTimeInMs() { + return elapsedTimeInMs; + } + + @Override + public HealthCheckMetadata getHealthCheckMetadata() { + return this.metaData; + } + + @Override + public Date getFinishedAt() { + return finishedAt; + } + + @Override + public boolean hasTimedOut() { + return this.timedOut; + } + + /** Natural order of results (failed results are sorted before ok results). */ + @Override + public int compareTo(ExecutionResult otherResult) { + int retVal = otherResult.getHealthCheckResult().getStatus().compareTo(this.getHealthCheckResult().getStatus()); + if (retVal == 0) { + retVal = Collator.getInstance().compare(this.getHealthCheckMetadata().getTitle(), + otherResult.getHealthCheckMetadata().getTitle()); + } + return retVal; + } + + long getServiceId() { + return this.metaData.getServiceId(); + } +} \ No newline at end of file diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/ExtendedHealthCheckExecutor.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/ExtendedHealthCheckExecutor.java new file mode 100644 index 00000000000..0922e30a738 --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/ExtendedHealthCheckExecutor.java @@ -0,0 +1,41 @@ +/* + * 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 SF 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.hc.core.impl.executor; + +import java.util.List; + +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.execution.HealthCheckExecutionOptions; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckExecutor; +import org.apache.felix.hc.api.execution.HealthCheckSelector; +import org.osgi.framework.ServiceReference; + +/** Internal service used by the JMX and ServiceUnavailableFilter */ +public interface ExtendedHealthCheckExecutor extends HealthCheckExecutor { + + /** execute single health check using cache, used by JMX */ + HealthCheckExecutionResult execute(ServiceReference ref); + + /** internal interface to retrieve service references */ + ServiceReference[] selectHealthCheckReferences(HealthCheckSelector selector, HealthCheckExecutionOptions options); + + /** internal interface to execute checks for service references */ + List execute(final ServiceReference[] healthCheckReferences, HealthCheckExecutionOptions options); + +} \ No newline at end of file diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckExecutorImpl.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckExecutorImpl.java new file mode 100644 index 00000000000..1b895dce704 --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckExecutorImpl.java @@ -0,0 +1,470 @@ +/* + * 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 SF 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.hc.core.impl.executor; + +import static org.apache.felix.hc.api.FormattingResultLog.msHumanReadable; +import static org.apache.felix.hc.core.impl.executor.HealthCheckExecutorImplConfiguration.LONGRUNNING_FUTURE_THRESHOLD_CRITICAL_DEFAULT_MS; +import static org.apache.felix.hc.core.impl.executor.HealthCheckExecutorImplConfiguration.RESULT_CACHE_TTL_DEFAULT_MS; +import static org.apache.felix.hc.core.impl.executor.HealthCheckExecutorImplConfiguration.TIMEOUT_DEFAULT_MS; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.time.StopWatch; +import org.apache.felix.hc.api.FormattingResultLog; +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.ResultLog; +import org.apache.felix.hc.api.execution.HealthCheckExecutionOptions; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckExecutor; +import org.apache.felix.hc.api.execution.HealthCheckMetadata; +import org.apache.felix.hc.api.execution.HealthCheckSelector; +import org.apache.felix.hc.core.impl.executor.async.AsyncHealthCheckExecutor; +import org.apache.felix.hc.core.impl.util.HealthCheckFilter; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Modified; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.metatype.annotations.Designate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Runs health checks for a given list of tags in parallel. */ +@Component(service = { HealthCheckExecutor.class, ExtendedHealthCheckExecutor.class }, immediate = true // immediate = true to keep the + // cache! +) +@Designate(ocd = HealthCheckExecutorImplConfiguration.class) +public class HealthCheckExecutorImpl implements ExtendedHealthCheckExecutor, ServiceListener { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + private long timeoutInMs; + + private long longRunningFutureThresholdForRedMs; + + private long resultCacheTtlInMs; + + private String[] defaultTags; + + private HealthCheckResultCache healthCheckResultCache = new HealthCheckResultCache(); + + private final Map stillRunningFutures = new HashMap(); + + @Reference + private AsyncHealthCheckExecutor asyncHealthCheckExecutor; + + @Reference + HealthCheckExecutorThreadPool healthCheckExecutorThreadPool; + + private BundleContext bundleContext; + + @Activate + protected final void activate(final HealthCheckExecutorImplConfiguration configuration, final BundleContext bundleContext) { + this.bundleContext = bundleContext; + + configure(configuration); + + try { + this.bundleContext.addServiceListener(this, "(" + + Constants.OBJECTCLASS + "=" + HealthCheck.class.getName() + ")"); + } catch (final InvalidSyntaxException ise) { + // this should really never happen as the expression above is constant + throw new RuntimeException("Unexpected exception occured.", ise); + } + } + + @Modified + protected final void modified(final HealthCheckExecutorImplConfiguration configuration) { + configure(configuration); + } + + @Deactivate + protected final void deactivate() { + this.bundleContext.removeServiceListener(this); + this.bundleContext = null; + this.healthCheckResultCache.clear(); + } + + protected final void configure(final HealthCheckExecutorImplConfiguration configuration) { + this.timeoutInMs = configuration.timeoutInMs(); + if (this.timeoutInMs <= 0L) { + this.timeoutInMs = TIMEOUT_DEFAULT_MS; + } + + this.longRunningFutureThresholdForRedMs = configuration.longRunningFutureThresholdForCriticalMs(); + if (this.longRunningFutureThresholdForRedMs <= 0L) { + this.longRunningFutureThresholdForRedMs = LONGRUNNING_FUTURE_THRESHOLD_CRITICAL_DEFAULT_MS; + } + + this.resultCacheTtlInMs = configuration.resultCacheTtlInMs(); + if (this.resultCacheTtlInMs <= 0L) { + this.resultCacheTtlInMs = RESULT_CACHE_TTL_DEFAULT_MS; + } + + this.defaultTags = configuration.defaultTags(); + + } + + @Override + public void serviceChanged(final ServiceEvent event) { + if (event.getType() == ServiceEvent.UNREGISTERING) { + final Long serviceId = (Long) event.getServiceReference().getProperty(Constants.SERVICE_ID); + this.healthCheckResultCache.removeCachedResult(serviceId); + } + } + + @Override + public List execute(HealthCheckSelector selector) { + return execute(selector, new HealthCheckExecutionOptions()); + } + + @Override + public List execute(HealthCheckSelector selector, HealthCheckExecutionOptions options) { + logger.debug("Starting executing checks for filter selector {} and execution options {}", selector, options); + + if(ArrayUtils.isEmpty(selector.tags())) { + logger.debug("Using default tags"); + selector.withTags(defaultTags); + } + + final ServiceReference[] healthCheckReferences = selectHealthCheckReferences(selector, options); + List results = this.execute(healthCheckReferences, options); + return results; + + } + + /** @see org.apache.felix.hc.core.impl.executor.ExtendedHealthCheckExecutor#selectHealthCheckReferences(HealthCheckSelector, HealthCheckExecutionOptions) */ + @Override + public ServiceReference[] selectHealthCheckReferences(HealthCheckSelector selector, HealthCheckExecutionOptions options) { + final HealthCheckFilter filter = new HealthCheckFilter(this.bundleContext); + final ServiceReference[] healthCheckReferences = filter.getHealthCheckServiceReferences(selector, options.isCombineTagsWithOr()); + return healthCheckReferences; + } + + /** @see org.apache.felix.hc.core.impl.executor.ExtendedHealthCheckExecutor#execute(org.osgi.framework.ServiceReference) */ + @Override + public HealthCheckExecutionResult execute(final ServiceReference ref) { + final HealthCheckMetadata metadata = this.getHealthCheckMetadata(ref); + return createResultsForDescriptor(metadata); + } + + /** @see org.apache.felix.hc.core.impl.executor.ExtendedHealthCheckExecutor#execute(ServiceReference[], HealthCheckExecutionOptions) */ + @Override + public List execute(final ServiceReference[] healthCheckReferences, + HealthCheckExecutionOptions options) { + final long startTime = System.currentTimeMillis(); + + final List results = new ArrayList(); + final List healthCheckDescriptors = getHealthCheckMetadata(healthCheckReferences); + + createResultsForDescriptors(healthCheckDescriptors, results, options); + + if (logger.isDebugEnabled()) { + logger.debug("Time consumed for all checks: {}", msHumanReadable(System.currentTimeMillis() - startTime)); + } + + // sort result + Collections.sort(results, new Comparator() { + + @Override + public int compare(final HealthCheckExecutionResult arg0, + final HealthCheckExecutionResult arg1) { + return ((ExecutionResult) arg0).compareTo((ExecutionResult) arg1); + } + + }); + return results; + } + + private void createResultsForDescriptors(final List healthCheckDescriptors, + final List results, HealthCheckExecutionOptions options) { + // -- All methods below check if they can transform a healthCheckDescriptor into a result + // -- if yes the descriptor is removed from the list and the result added + + // get async results + if (!options.isForceInstantExecution()) { + if (asyncHealthCheckExecutor != null) { + asyncHealthCheckExecutor.collectAsyncResults(healthCheckDescriptors, results, healthCheckResultCache); + } + } + + // reuse cached results where possible + if (!options.isForceInstantExecution()) { + healthCheckResultCache.useValidCacheResults(healthCheckDescriptors, results, resultCacheTtlInMs); + } + + // everything else is executed in parallel via futures + List futures = createOrReuseFutures(healthCheckDescriptors); + + // wait for futures at most until timeout (but will return earlier if all futures are finished) + waitForFuturesRespectingTimeout(futures, options); + collectResultsFromFutures(futures, results); + + // respect sticky results if configured via HealthCheck.KEEP_NON_OK_RESULTS_STICKY_FOR_SEC + appendStickyResultLogIfConfigured(results); + + } + + private void appendStickyResultLogIfConfigured(List results) { + ListIterator resultsIt = results.listIterator(); + while (resultsIt.hasNext()) { + HealthCheckExecutionResult result = resultsIt.next(); + Long warningsStickForMinutes = result.getHealthCheckMetadata().getKeepNonOkResultsStickyForSec(); + if (warningsStickForMinutes != null && warningsStickForMinutes > 0) { + result = healthCheckResultCache.createExecutionResultWithStickyResults(result); + resultsIt.set(result); + } + } + } + + private HealthCheckExecutionResult createResultsForDescriptor(final HealthCheckMetadata metadata) { + // create result for a single descriptor + + // reuse cached results where possible + HealthCheckExecutionResult result; + + result = healthCheckResultCache.getValidCacheResult(metadata, resultCacheTtlInMs); + + if (result == null) { + final HealthCheckFuture future; + synchronized (this.stillRunningFutures) { + future = createOrReuseFuture(metadata); + } + + // wait for futures at most until timeout (but will return earlier if all futures are finished) + waitForFuturesRespectingTimeout(Collections.singletonList(future), null); + result = collectResultFromFuture(future); + } + + return result; + } + + /** Create the health check meta data */ + private List getHealthCheckMetadata(final ServiceReference... healthCheckReferences) { + final List descriptors = new LinkedList(); + for (final ServiceReference serviceReference : healthCheckReferences) { + final HealthCheckMetadata descriptor = getHealthCheckMetadata(serviceReference); + + descriptors.add(descriptor); + } + + return descriptors; + } + + /** Create the health check meta data */ + private HealthCheckMetadata getHealthCheckMetadata(final ServiceReference healthCheckReference) { + final HealthCheckMetadata descriptor = new HealthCheckMetadata(healthCheckReference); + return descriptor; + } + + /** Create or reuse future for the list of health checks */ + private List createOrReuseFutures(final List healthCheckDescriptors) { + final List futuresForResultOfThisCall = new LinkedList(); + + synchronized (this.stillRunningFutures) { + for (final HealthCheckMetadata md : healthCheckDescriptors) { + + futuresForResultOfThisCall.add(createOrReuseFuture(md)); + + } + } + return futuresForResultOfThisCall; + } + + /** Create or reuse future for the health check This method must be synchronized by the caller(!) on stillRunningFutures */ + private HealthCheckFuture createOrReuseFuture(final HealthCheckMetadata metadata) { + HealthCheckFuture future = this.stillRunningFutures.get(metadata); + if (future != null) { + logger.debug("Found a future that is still running for {}", metadata); + } else { + logger.debug("Creating future for {}", metadata); + future = new HealthCheckFuture(metadata, bundleContext, new HealthCheckFuture.Callback() { + + @Override + public void finished(final HealthCheckExecutionResult result) { + healthCheckResultCache.updateWith(result); + asyncHealthCheckExecutor.updateWith(result); + synchronized (stillRunningFutures) { + stillRunningFutures.remove(metadata); + } + } + }); + this.stillRunningFutures.put(metadata, future); + + final HealthCheckFuture newFuture = future; + + healthCheckExecutorThreadPool.execute(new Runnable() { + @Override + public void run() { + newFuture.run(); + synchronized (stillRunningFutures) { + // notify executor threads that newFuture is finished. Wrapping it in another runnable + // ensures that newFuture.isDone() will return true (if e.g. done in callback above, there are + // still a few lines of code until the future is really done and hence then the executor thread + // is sometime notified a bit too early, still receives the result isDone()=false and then waits + // for another 50ms, even though the future was about to be done one ms later) + stillRunningFutures.notifyAll(); + } + } + }); + } + + return future; + } + + /** Wait for the futures until the timeout is reached */ + private void waitForFuturesRespectingTimeout(final List futuresForResultOfThisCall, + HealthCheckExecutionOptions options) { + final StopWatch callExcutionTimeStopWatch = new StopWatch(); + callExcutionTimeStopWatch.start(); + boolean allFuturesDone; + + long effectiveTimeout = this.timeoutInMs; + if (options != null && options.getOverrideGlobalTimeout() > 0) { + effectiveTimeout = options.getOverrideGlobalTimeout(); + } + + if (futuresForResultOfThisCall.isEmpty()) { + return; // nothing to wait for (usually because of cached results) + } + + do { + try { + synchronized (stillRunningFutures) { + stillRunningFutures.wait(50); // wait for notifications of callbacks of HealthCheckFutures + } + } catch (final InterruptedException ie) { + logger.warn("Unexpected InterruptedException while waiting for healthCheckContributors", ie); + } + + allFuturesDone = true; + for (final HealthCheckFuture healthCheckFuture : futuresForResultOfThisCall) { + allFuturesDone &= healthCheckFuture.isDone(); + } + } while (!allFuturesDone && callExcutionTimeStopWatch.getTime() < effectiveTimeout); + } + + /** Collect the results from all futures + * + * @param futuresForResultOfThisCall The list of futures + * @param results The result collection */ + void collectResultsFromFutures(final List futuresForResultOfThisCall, + final Collection results) { + + final Set resultsFromFutures = new HashSet(); + + final Iterator futuresIt = futuresForResultOfThisCall.iterator(); + while (futuresIt.hasNext()) { + final HealthCheckFuture future = futuresIt.next(); + final HealthCheckExecutionResult result = this.collectResultFromFuture(future); + + resultsFromFutures.add(result); + futuresIt.remove(); + } + + logger.debug("Adding {} results from futures", resultsFromFutures.size()); + results.addAll(resultsFromFutures); + } + + /** Collect the result from a single future + * + * @param future The future + * @return The execution result or a result for a reached timeout */ + HealthCheckExecutionResult collectResultFromFuture(final HealthCheckFuture future) { + + HealthCheckExecutionResult result; + HealthCheckMetadata hcMetadata = future.getHealthCheckMetadata(); + if (future.isDone()) { + logger.debug("Health Check is done: {}", hcMetadata); + + try { + result = future.get(); + } catch (final Exception e) { + logger.warn("Unexpected Exception during future.get(): " + e, e); + long futureElapsedTimeMs = new Date().getTime() - future.getCreatedTime().getTime(); + result = new ExecutionResult(hcMetadata, Result.Status.HEALTH_CHECK_ERROR, + "Unexpected Exception during future.get(): " + e, futureElapsedTimeMs, false); + } + + } else { + logger.debug("Health Check timed out: {}", hcMetadata); + // Futures must not be cancelled as interrupting a health check might leave the system in invalid state + // (worst case could be a corrupted repository index if using write operations) + + // normally we turn the check into WARN (normal timeout), but if the threshold time for CRITICAL is reached for a certain + // future we turn the result CRITICAL + long futureElapsedTimeMs = new Date().getTime() - future.getCreatedTime().getTime(); + FormattingResultLog resultLog = new FormattingResultLog(); + if (futureElapsedTimeMs < this.longRunningFutureThresholdForRedMs) { + resultLog.warn("Timeout: Check still running after " + msHumanReadable(futureElapsedTimeMs)); + } else { + resultLog.critical("Timeout: Check still running after " + msHumanReadable(futureElapsedTimeMs) + + " (exceeding the configured threshold for CRITICAL: " + + msHumanReadable(this.longRunningFutureThresholdForRedMs) + ")"); + } + + // add logs from previous, cached result if exists (using a 1 year TTL) + HealthCheckExecutionResult lastCachedResult = healthCheckResultCache.getValidCacheResult(hcMetadata, 1000 * 60 * 60 * 24 * 365); + if (lastCachedResult != null) { + DateFormat df = new SimpleDateFormat("HH:mm:ss.SSS"); + resultLog.info("*** Result log of last execution finished at {} after {} ***", + df.format(lastCachedResult.getFinishedAt()), + msHumanReadable(lastCachedResult.getElapsedTimeInMs())); + for (ResultLog.Entry entry : lastCachedResult.getHealthCheckResult()) { + resultLog.add(entry); + } + } + + result = new ExecutionResult(hcMetadata, new Result(resultLog), futureElapsedTimeMs, true); + + } + + return result; + } + + public void setTimeoutInMs(final long timeoutInMs) { + this.timeoutInMs = timeoutInMs; + } + + public void setLongRunningFutureThresholdForRedMs( + final long longRunningFutureThresholdForRedMs) { + this.longRunningFutureThresholdForRedMs = longRunningFutureThresholdForRedMs; + } +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckExecutorImplConfiguration.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckExecutorImplConfiguration.java new file mode 100644 index 00000000000..92bb9d38be1 --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckExecutorImplConfiguration.java @@ -0,0 +1,43 @@ +/* + * 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 SF 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.hc.core.impl.executor; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@ObjectClassDefinition(name = "Apache Felix Health Check Executor", description = "Runs health checks for a given list of tags in parallel.") +@interface HealthCheckExecutorImplConfiguration { + + long TIMEOUT_DEFAULT_MS = 2000L; + + long LONGRUNNING_FUTURE_THRESHOLD_CRITICAL_DEFAULT_MS = 1000L * 60 * 5; + + long RESULT_CACHE_TTL_DEFAULT_MS = 1000L * 2; + + @AttributeDefinition(name = "Timeout", description = "Timeout in ms until a check is marked as timed out") + long timeoutInMs() default TIMEOUT_DEFAULT_MS; + + @AttributeDefinition(name = "Timeout threshold for CRITICAL", description = "Threshold in ms until a check is marked as 'exceedingly' timed out and will marked CRITICAL instead of WARN only") + long longRunningFutureThresholdForCriticalMs() default LONGRUNNING_FUTURE_THRESHOLD_CRITICAL_DEFAULT_MS; + + @AttributeDefinition(name = "Results Cache TTL in Ms", description = "Result Cache time to live - results will be cached for the given time") + long resultCacheTtlInMs() default RESULT_CACHE_TTL_DEFAULT_MS; + + @AttributeDefinition(name = "Default Tags", description = "Default tags to be executed if no tags are explicitly supplied") + String[] defaultTags() default {"default"}; +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckExecutorThreadPool.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckExecutorThreadPool.java new file mode 100644 index 00000000000..794fdef5c1a --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckExecutorThreadPool.java @@ -0,0 +1,106 @@ +/* + * 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 SF 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.hc.core.impl.executor; + +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.osgi.framework.BundleContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.metatype.annotations.Designate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Creates a thread pool via standard java.util.concurrent package to be used for parallel execution of health checks in + * HealthCheckExecutorImpl and AsyncHealthCheckExecutor */ +@Component(service = { HealthCheckExecutorThreadPool.class }) +@Designate(ocd = HealthCheckExecutorThreadPoolConfiguration.class) +public class HealthCheckExecutorThreadPool { + private final static Logger LOG = LoggerFactory.getLogger(HealthCheckExecutorThreadPool.class); + + private int threadPoolSize; + + private ScheduledThreadPoolExecutor executor; + + @Activate + protected final void activate(final HealthCheckExecutorThreadPoolConfiguration configuration, final BundleContext bundleContext) { + + this.threadPoolSize = configuration.threadPoolSize(); + + executor = new ScheduledThreadPoolExecutor(threadPoolSize, new HcThreadFactory(), new HcRejectedExecutionHandler()); + + LOG.info("Created HC Thread Pool: threadPoolSize={}", threadPoolSize); + + } + + @Deactivate + protected final void deactivate() { + executor.shutdown(); + } + + // Method called by HealthCheckExecutorImpl (regular synchronous checks) + public void execute(final Runnable job) { + this.executor.execute(job); + } + + // used for interval execution (asynchronous checks) + public ScheduledFuture scheduleAtFixedRate(final Runnable job, long intervalInSec) { + ScheduledFuture scheduleFuture = executor.scheduleAtFixedRate(job, 0, intervalInSec, TimeUnit.SECONDS); + return scheduleFuture; + } + + // methods below are used by AsyncHealthCheckExecutor.QuartzThreadPool + public int getPoolSize() { + return this.executor.getPoolSize(); + } + + public int getMaxCurrentlyAvailableThreads() { + return this.threadPoolSize - executor.getQueue().size(); + } + + static class HcThreadFactory implements ThreadFactory { + private final ThreadGroup group; + private final AtomicInteger threadNumber = new AtomicInteger(1); + + HcThreadFactory() { + group = Thread.currentThread().getThreadGroup(); + } + + public Thread newThread(Runnable r) { + Thread t = new Thread(group, r, "hc-thread-" + threadNumber.getAndIncrement()); + t.setDaemon(true); // using daemon thread to not delay JVM shutdown (HC status is non-transactional and only in memory) + t.setPriority(Thread.NORM_PRIORITY); + return t; + } + } + + private final class HcRejectedExecutionHandler implements RejectedExecutionHandler { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + LOG.warn("Thread Pool {} rejected to run runnable {}", executor, r); + } + } + +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckExecutorThreadPoolConfiguration.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckExecutorThreadPoolConfiguration.java new file mode 100644 index 00000000000..f9e2144b021 --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckExecutorThreadPoolConfiguration.java @@ -0,0 +1,31 @@ +/* + * 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 SF 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.hc.core.impl.executor; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@ObjectClassDefinition(name = "Apache Felix Health Check Executor Thread Pool", description = "Runs health checks for a given list of tags in parallel.") +@interface HealthCheckExecutorThreadPoolConfiguration { + + int THREAD_POOL_SIZE_DEFAULT = 25; + + @AttributeDefinition(name = "Thread Pool Size", description = "Number of threads to be used for parallel health check execution") + int threadPoolSize() default THREAD_POOL_SIZE_DEFAULT; + +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckFuture.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckFuture.java new file mode 100644 index 00000000000..9968f5981b5 --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckFuture.java @@ -0,0 +1,145 @@ +/* + * 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 SF 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.hc.core.impl.executor; + +import static org.apache.felix.hc.api.FormattingResultLog.msHumanReadable; + +import java.lang.reflect.InvocationTargetException; +import java.util.Date; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.commons.lang3.reflect.MethodUtils; +import org.apache.commons.lang3.time.StopWatch; +import org.apache.felix.hc.api.FormattingResultLog; +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.ResultLog; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckMetadata; +import org.osgi.framework.BundleContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Future to be able to schedule a health check for parallel execution. */ +public class HealthCheckFuture extends FutureTask { + + public interface Callback { + public void finished(final HealthCheckExecutionResult result); + } + + private final static Logger LOG = LoggerFactory.getLogger(HealthCheckFuture.class); + + private final HealthCheckMetadata metadata; + private final Date createdTime; + + public HealthCheckFuture(final HealthCheckMetadata metadata, final BundleContext bundleContext, final Callback callback) { + super(new Callable() { + @Override + public ExecutionResult call() throws Exception { + Thread.currentThread().setName("HealthCheck " + metadata.getTitle()); + LOG.debug("Starting check {}", metadata); + + final StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + Result resultFromHealthCheck = null; + ExecutionResult executionResult = null; + + Object healthCheck = bundleContext.getService(metadata.getServiceReference()); + try { + if (healthCheck != null) { + if ((healthCheck instanceof HealthCheck)) { + resultFromHealthCheck = ((HealthCheck) healthCheck).execute(); + } else { + resultFromHealthCheck = executeLegacyHc(healthCheck); + } + } else { + throw new IllegalStateException("Service cannot be retrieved via - activate() probably failed"); + } + + } catch (final Exception e) { + resultFromHealthCheck = new Result(Result.Status.HEALTH_CHECK_ERROR, + "Exception during execution of '" + metadata.getName() + "'", e); + } finally { + // unget service ref + bundleContext.ungetService(metadata.getServiceReference()); + + // update result with information about this run + stopWatch.stop(); + long elapsedTime = stopWatch.getTime(); + if (resultFromHealthCheck != null) { + // wrap the result in an execution result + executionResult = new ExecutionResult(metadata, resultFromHealthCheck, elapsedTime); + } + LOG.debug("Time consumed for {}: {}", metadata, msHumanReadable(elapsedTime)); + } + + callback.finished(executionResult); + Thread.currentThread().setName("HealthCheck-idle"); + return executionResult; + } + + }); + this.createdTime = new Date(); + this.metadata = metadata; + + } + + Date getCreatedTime() { + return this.createdTime; + } + + public HealthCheckMetadata getHealthCheckMetadata() { + return metadata; + } + + @Override + public String toString() { + return "[Future for " + this.metadata + ", createdTime=" + this.createdTime + "]"; + } + + @SuppressWarnings("rawtypes") + private static Result executeLegacyHc(Object healthCheck) { + + FormattingResultLog log = new FormattingResultLog(); + log.debug("Running legacy HC {}, please convert to new interface org.apache.felix.hc.api.HealthCheck!", + healthCheck.getClass().getName()); + try { + Object result = MethodUtils.invokeMethod(healthCheck, "execute"); + Object resultLog = FieldUtils.readField(result, "resultLog", true); + + List entries = (List) FieldUtils.readField(resultLog, "entries", true); + for (Object object : entries) { + String statusLegacy = String.valueOf(FieldUtils.readField(object, "status", true)); + String message = (String) FieldUtils.readField(object, "message", true); + Exception exception = (Exception) FieldUtils.readField(object, "exception", true); + if(statusLegacy.equals("DEBUG")) { + log.add(new ResultLog.Entry(message, true, exception)); + } else { + statusLegacy = statusLegacy.replace("INFO", "OK"); + log.add(new ResultLog.Entry(Result.Status.valueOf(statusLegacy), message, exception)); + } + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + log.healthCheckError("Could call and convert Sling HC {} for Felix Runtime", healthCheck.getClass().getName()); + } + return new Result(log); + } +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckResultCache.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckResultCache.java new file mode 100644 index 00000000000..53de218c681 --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckResultCache.java @@ -0,0 +1,216 @@ +/* + * 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 SF 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.hc.core.impl.executor; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.Result.Status; +import org.apache.felix.hc.api.ResultLog; +import org.apache.felix.hc.api.ResultLog.Entry; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckMetadata; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Caches health check results. */ +public class HealthCheckResultCache { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + /** The map holding the cached results. */ + private final Map cache = new ConcurrentHashMap(); + + private final Map> cacheOfNotOkResults = new ConcurrentHashMap>(); + + /** Update the cache with the result */ + public void updateWith(HealthCheckExecutionResult result) { + final ExecutionResult executionResult = (ExecutionResult) result; + cache.put(executionResult.getServiceId(), result); + + Status status = executionResult.getHealthCheckResult().getStatus(); + if (status != Result.Status.OK) { + logger.debug("Caching {} result for HC {}", status, executionResult.getServiceId()); + Map nonOkResultsForStatus = cacheOfNotOkResults.get(status); + if(nonOkResultsForStatus==null) { + nonOkResultsForStatus = new ConcurrentHashMap(); + cacheOfNotOkResults.put(status, nonOkResultsForStatus); + } + nonOkResultsForStatus.put(executionResult.getServiceId(), result); + } + } + + /** Get the valid cache results */ + public void useValidCacheResults(final List metadatas, + final Collection results, + final long resultCacheTtlInMs) { + final Set cachedResults = new TreeSet(); + final Iterator checksIt = metadatas.iterator(); + while (checksIt.hasNext()) { + final HealthCheckMetadata md = checksIt.next(); + final HealthCheckExecutionResult result = getValidCacheResult(md, resultCacheTtlInMs); + if (result != null) { + cachedResults.add(result); + checksIt.remove(); + } + } + logger.debug("Adding {} results from cache", cachedResults.size()); + results.addAll(cachedResults); + } + + /** Return the cached result if it's still valid. */ + public HealthCheckExecutionResult getValidCacheResult(final HealthCheckMetadata metadata, + final long resultCacheTtlInMs) { + return get(metadata, resultCacheTtlInMs); + } + + private HealthCheckExecutionResult get(final HealthCheckMetadata metadata, final long globalResultCacheTtlInMs) { + final Long key = metadata.getServiceId(); + final HealthCheckExecutionResult cachedResult = cache.get(key); + if (cachedResult != null) { + Date finishedAt = cachedResult.getFinishedAt(); + if (finishedAt == null) { + // never cache without proper meta data -> remove it + cache.remove(key); + return null; + } + + long effectiveTtl = getEffectiveTtl(metadata, globalResultCacheTtlInMs); + long validUntilLong = finishedAt.getTime() + effectiveTtl; + if (validUntilLong < 0) { // if Long.MAX_VALUE is configured, this can become negative + validUntilLong = Long.MAX_VALUE; + } + Date validUntil = new Date(validUntilLong); + Date now = new Date(); + if (validUntil.after(now)) { + logger.debug("Cache hit: validUntil={} cachedResult={}", validUntil, cachedResult); + return cachedResult; + } else { + logger.debug("Outdated result: validUntil={} cachedResult={}", validUntil, cachedResult); + // not removing result for key as out-dated results are shown for timed out checks if available + } + } + + // null => no cache hit + return null; + } + + /** Obtains the effective TTL for a given Metadata descriptor. + * + * @param metadata Metadata descriptor of health check + * @param globalTtl TTL from service configuration of health check executor (used as default) + * @return effective TTL */ + private long getEffectiveTtl(HealthCheckMetadata metadata, long globalTtl) { + final long ttl; + Long hcTtl = metadata.getResultCacheTtlInMs(); + if (hcTtl != null && hcTtl > -1) { + ttl = hcTtl; + } else { + ttl = globalTtl; + } + return ttl; + } + + /** Creates a new execution result + * + * @param origResult + * @return */ + public HealthCheckExecutionResult createExecutionResultWithStickyResults(HealthCheckExecutionResult origResult) { + HealthCheckExecutionResult result = origResult; + + HealthCheckMetadata healthCheckMetadata = origResult.getHealthCheckMetadata(); + Long keepNonOkResultsStickyForSec = healthCheckMetadata.getKeepNonOkResultsStickyForSec(); + if (keepNonOkResultsStickyForSec != null && keepNonOkResultsStickyForSec > 0) { + logger.debug("Taking into account sticky results (up to {} sec old) for health check {}", keepNonOkResultsStickyForSec, + healthCheckMetadata.getName()); + List nonOkResultsFromPast = new ArrayList(); + long cutOffTime = System.currentTimeMillis() - (keepNonOkResultsStickyForSec * 1000); + for (Status status : cacheOfNotOkResults.keySet()) { + long hcServiceId = ((ExecutionResult) origResult).getServiceId(); + HealthCheckExecutionResult nonOkResultFromPast = cacheOfNotOkResults.get(status).get(hcServiceId); + if (nonOkResultFromPast == null) { + logger.debug("no sticky result in cache for HC {}", hcServiceId); + continue; + } + if (nonOkResultFromPast == origResult) { + logger.debug("result already in cache: {} for HC {}, not adding sticky result", origResult, hcServiceId); + continue; + } + long pastHcTime = nonOkResultFromPast.getFinishedAt().getTime(); + logger.debug("Time of old {} result: {}", status, pastHcTime); + logger.debug("Cut off time: {}", cutOffTime); + if (pastHcTime > cutOffTime) { + logger.debug("Found sticky result: {}", nonOkResultFromPast); + nonOkResultsFromPast.add(nonOkResultFromPast); + } + } + + if (!nonOkResultsFromPast.isEmpty()) { + ResultLog resultLog = new ResultLog(); + resultLog.add(new Entry(Result.Status.OK, "*** Current Result: "+origResult.getHealthCheckResult().getStatus()+" ***")); + for (ResultLog.Entry entry : origResult.getHealthCheckResult()) { + resultLog.add(entry); + } + DateFormat df = new SimpleDateFormat("HH:mm:ss.SSS"); + for (HealthCheckExecutionResult nonOkResultFromPast : nonOkResultsFromPast) { + Status status = nonOkResultFromPast.getHealthCheckResult().getStatus(); + resultLog.add( + new Entry(Result.Status.OK, + "*** Sticky Result: " + status + " from " + df.format(nonOkResultFromPast.getFinishedAt()) + " ***")); + for (ResultLog.Entry entry : nonOkResultFromPast.getHealthCheckResult()) { + resultLog.add(entry); + } + } + result = new ExecutionResult(healthCheckMetadata, new Result(resultLog), origResult.getFinishedAt(), origResult.getElapsedTimeInMs(), false); + } + } + + return result; + } + + /** Clear the whole cache */ + public void clear() { + this.cache.clear(); + this.cacheOfNotOkResults.clear(); + } + + /** Remove entry from cache */ + public void removeCachedResult(final Long serviceId) { + this.cache.remove(serviceId); + for (Map cacheOfNotOkResultsForStatus : cacheOfNotOkResults.values()) { + cacheOfNotOkResultsForStatus.remove(serviceId); + } + } + + @Override + public String toString() { + return "[HealthCheckResultCache size=" + cache.size() + "]"; + } + +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckExecutor.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckExecutor.java new file mode 100644 index 00000000000..a2d66373199 --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckExecutor.java @@ -0,0 +1,261 @@ +/* + * 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 SF 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.hc.core.impl.executor.async; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckMetadata; +import org.apache.felix.hc.api.execution.HealthCheckSelector; +import org.apache.felix.hc.core.impl.executor.ExecutionResult; +import org.apache.felix.hc.core.impl.executor.HealthCheckExecutorThreadPool; +import org.apache.felix.hc.core.impl.executor.HealthCheckResultCache; +import org.apache.felix.hc.core.impl.util.HealthCheckFilter; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Runs health checks asynchronously, either via cron or via interval. */ +@Component(service = AsyncHealthCheckExecutor.class, immediate = true) +public class AsyncHealthCheckExecutor implements ServiceListener { + + private static final Logger LOG = LoggerFactory.getLogger(AsyncHealthCheckExecutor.class); + + @Reference + HealthCheckExecutorThreadPool healthCheckExecutorThreadPool; + + private Map asyncResultsByDescriptor = new ConcurrentHashMap(); + + private Map registeredJobs = new HashMap(); + + private BundleContext bundleContext; + + private QuartzCronScheduler quartzCronScheduler = null; + + @Activate + protected final void activate(final ComponentContext componentContext) { + this.bundleContext = componentContext.getBundleContext(); + this.bundleContext.addServiceListener(this); + + int count = 0; + HealthCheckFilter healthCheckFilter = new HealthCheckFilter(bundleContext); + final ServiceReference[] healthCheckReferences = healthCheckFilter.getHealthCheckServiceReferences(HealthCheckSelector.empty(), false); + for (ServiceReference serviceReference : healthCheckReferences) { + HealthCheckMetadata healthCheckMetadata = new HealthCheckMetadata(serviceReference); + if (isAsync(healthCheckMetadata)) { + if (scheduleHealthCheck(healthCheckMetadata)) { + count++; + } + } + } + LOG.debug("Scheduled {} jobs for asynchronous health checks during bundle startup", count); + } + + @Deactivate + protected final void deactivate(final ComponentContext componentContext) { + this.bundleContext.removeServiceListener(this); + this.bundleContext = null; + + LOG.debug("Unscheduling {} jobs for asynchronous health checks", registeredJobs.size()); + for (HealthCheckMetadata healthCheckDescriptor : new LinkedList(registeredJobs.keySet())) { + unscheduleHealthCheck(healthCheckDescriptor); + } + + if (quartzCronScheduler != null) { + quartzCronScheduler.shutdown(); + } + } + + @Override + public void serviceChanged(ServiceEvent event) { + if (bundleContext == null) { + // already deactivated? + return; + } + ServiceReference serviceReference = event.getServiceReference(); + final boolean isHealthCheck = serviceReference.isAssignableTo(bundleContext.getBundle(), HealthCheck.class.getName()); + + if (isHealthCheck) { + HealthCheckMetadata healthCheckMetadata = new HealthCheckMetadata(serviceReference); + int eventType = event.getType(); + if (eventType == ServiceEvent.REGISTERED) { + LOG.debug("Received service event REGISTERED for health check {}", healthCheckMetadata); + scheduleHealthCheck(healthCheckMetadata); + } else if (eventType == ServiceEvent.UNREGISTERING) { + LOG.debug("Received service event UNREGISTERING for health check {}", healthCheckMetadata); + unscheduleHealthCheck(healthCheckMetadata); + } else if (eventType == ServiceEvent.MODIFIED) { + LOG.debug("Received service event MODIFIED for health check {}", healthCheckMetadata); + unscheduleHealthCheck(healthCheckMetadata); + scheduleHealthCheck(healthCheckMetadata); + } + + } + } + + private boolean scheduleHealthCheck(HealthCheckMetadata descriptor) { + + try { + AsyncHealthCheckJob healthCheckAsyncJob = null; + + if (isAsyncCron(descriptor)) { + + if (quartzCronScheduler == null) { + if (classExists("org.quartz.CronTrigger")) { + quartzCronScheduler = new QuartzCronScheduler(healthCheckExecutorThreadPool); + LOG.info("Created quartz scheduler for async HC"); + } else { + LOG.warn("Can not schedule async health check '{}' with cron expression '{}' since quartz library is not on classpath", descriptor.getName(), descriptor.getAsyncCronExpression()); + return false; + } + } + + healthCheckAsyncJob = new AsyncHealthCheckQuartzCronJob(descriptor, this, bundleContext, quartzCronScheduler); + } else if (isAsyncInterval(descriptor)) { + + healthCheckAsyncJob = new AsyncHealthCheckIntervalJob(descriptor, this, bundleContext, healthCheckExecutorThreadPool); + } + + if (healthCheckAsyncJob != null) { + healthCheckAsyncJob.schedule(); + registeredJobs.put(descriptor, healthCheckAsyncJob); + return true; + } else { + return false; + } + + } catch (Exception e) { + LOG.warn("Could not schedule job for " + descriptor + ". Exception: " + e, e); + return false; + } + + } + + private boolean unscheduleHealthCheck(HealthCheckMetadata descriptor) { + + // here no check for isAsync must be used to ensure previously + // scheduled async checks are correctly unscheduled if they have + // changed from async to sync. + + AsyncHealthCheckJob job = registeredJobs.remove(descriptor); + if (job != null) { + return job.unschedule(); + } else { + LOG.warn("No job was registered for descriptor {}", descriptor); + return false; + } + } + + /** Called by the main Executor to get results from async HCs */ + public void collectAsyncResults(List healthCheckDescriptors, Collection results, + HealthCheckResultCache cache) { + Iterator checksIt = healthCheckDescriptors.iterator(); + + Set asyncResults = new TreeSet(); + while (checksIt.hasNext()) { + HealthCheckMetadata healthCheckMetadata = checksIt.next(); + if (isAsync(healthCheckMetadata)) { + ExecutionResult result = asyncResultsByDescriptor.get(healthCheckMetadata); + if (result == null) { + result = handleMissingResult(healthCheckMetadata); + } + asyncResults.add(result); + // remove from HC collection to not execute the check in HealthCheckExecutorImpl + checksIt.remove(); + } + } + + LOG.debug("Caching {} results from async results", asyncResults.size()); + for (ExecutionResult result : asyncResults) { + cache.updateWith(result); + } + + LOG.debug("Adding {} results from async results", asyncResults.size()); + results.addAll(asyncResults); + + } + + private ExecutionResult handleMissingResult(HealthCheckMetadata healthCheckMetadata) { + ExecutionResult result; + if(isAsyncCron(healthCheckMetadata)) { + if(registeredJobs.containsKey(healthCheckMetadata)) { + result = new ExecutionResult(healthCheckMetadata, + new Result(Result.Status.OK, "Async Health Check with cron expression '" + healthCheckMetadata.getAsyncCronExpression() + + "' has not yet been executed."), 0L); + } else { + result = new ExecutionResult(healthCheckMetadata, + new Result(Result.Status.WARN, "Async Health Check with cron expression '" + healthCheckMetadata.getAsyncCronExpression() + + "' is never executed because quartz bundle is missing."), 0L); + } + + } else { + result = new ExecutionResult(healthCheckMetadata, + new Result(Result.Status.OK, "Async Health Check with interval '" + healthCheckMetadata.getAsyncIntervalInSec() + + "' has not yet been executed."), 0L); + } + return result; + } + + public void updateWith(HealthCheckExecutionResult result) { + if (isAsync(result.getHealthCheckMetadata())) { + asyncResultsByDescriptor.put(result.getHealthCheckMetadata(), (ExecutionResult) result); + LOG.debug("Updated result for async hc {} with {}", result.getHealthCheckMetadata(), result); + } + } + + private boolean isAsync(HealthCheckMetadata healthCheckMetadata) { + return isAsyncCron(healthCheckMetadata) || isAsyncInterval(healthCheckMetadata); + } + + private boolean isAsyncCron(HealthCheckMetadata healthCheckMetadata) { + return StringUtils.isNotBlank(healthCheckMetadata.getAsyncCronExpression()); + } + + private boolean isAsyncInterval(HealthCheckMetadata healthCheckMetadata) { + return healthCheckMetadata.getAsyncIntervalInSec() != null && healthCheckMetadata.getAsyncIntervalInSec() > 0L; + } + + private boolean classExists(String className) { + try { + Class.forName(className); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckIntervalJob.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckIntervalJob.java new file mode 100644 index 00000000000..35613829d6e --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckIntervalJob.java @@ -0,0 +1,60 @@ +/* + * 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 SF 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.hc.core.impl.executor.async; + +import java.util.concurrent.ScheduledFuture; + +import org.apache.felix.hc.api.execution.HealthCheckMetadata; +import org.apache.felix.hc.core.impl.executor.HealthCheckExecutorThreadPool; +import org.osgi.framework.BundleContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Runs health checks that are configured with an interval (ScheduledThreadPoolExecutor.scheduleAtFixedRate()) for asynchronous execution. */ +public class AsyncHealthCheckIntervalJob extends AsyncHealthCheckJob implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(AsyncHealthCheckExecutor.class); + + private final HealthCheckExecutorThreadPool healthCheckExecutorThreadPool; + private ScheduledFuture scheduleFuture = null; + + public AsyncHealthCheckIntervalJob(HealthCheckMetadata healthCheckDescriptor, AsyncHealthCheckExecutor asyncHealthCheckExecutor, + BundleContext bundleContext, HealthCheckExecutorThreadPool healthCheckExecutorThreadPool) { + super(healthCheckDescriptor, asyncHealthCheckExecutor, bundleContext); + this.healthCheckExecutorThreadPool = healthCheckExecutorThreadPool; + } + + public boolean schedule() { + Long asyncIntervalInSec = healthCheckDescriptor.getAsyncIntervalInSec(); + scheduleFuture = healthCheckExecutorThreadPool.scheduleAtFixedRate(this, asyncIntervalInSec); + LOG.info("Scheduled job {} for execution every {}sec", this, asyncIntervalInSec); + return true; + } + + @Override + public boolean unschedule() { + + if (scheduleFuture != null) { + LOG.debug("Unscheduling async job for {}", healthCheckDescriptor); + return scheduleFuture.cancel(false); + } else { + LOG.debug("No scheduled future for {} exists", healthCheckDescriptor); + return false; + } + } + +} \ No newline at end of file diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckJob.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckJob.java new file mode 100644 index 00000000000..161d7d43917 --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckJob.java @@ -0,0 +1,68 @@ +/* + * 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 SF 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.hc.core.impl.executor.async; + +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckMetadata; +import org.apache.felix.hc.core.impl.executor.HealthCheckFuture; +import org.apache.felix.hc.core.impl.executor.HealthCheckFuture.Callback; +import org.osgi.framework.BundleContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Abstract class for async execution variants cron/interval. */ +public abstract class AsyncHealthCheckJob implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(AsyncHealthCheckJob.class); + + protected final HealthCheckMetadata healthCheckDescriptor; + protected final AsyncHealthCheckExecutor asyncHealthCheckExecutor; + protected final BundleContext bundleContext; + + public AsyncHealthCheckJob(HealthCheckMetadata healthCheckDescriptor, AsyncHealthCheckExecutor asyncHealthCheckExecutor, + BundleContext bundleContext) { + this.healthCheckDescriptor = healthCheckDescriptor; + this.asyncHealthCheckExecutor = asyncHealthCheckExecutor; + this.bundleContext = bundleContext; + } + + @Override + public void run() { + + LOG.debug("Running job {}", this); + HealthCheckFuture healthCheckFuture = new HealthCheckFuture(healthCheckDescriptor, bundleContext, new Callback() { + + @Override + public void finished(HealthCheckExecutionResult result) { + asyncHealthCheckExecutor.updateWith(result); + } + }); + + // run future in same thread (as we are already async via scheduler) + healthCheckFuture.run(); + + } + + public abstract boolean schedule(); + + public abstract boolean unschedule(); + + @Override + public String toString() { + return "[Async job for " + this.healthCheckDescriptor + "]"; + } +} \ No newline at end of file diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckQuartzCronJob.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckQuartzCronJob.java new file mode 100644 index 00000000000..fed885c6cb7 --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckQuartzCronJob.java @@ -0,0 +1,116 @@ +/* + * 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 SF 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.hc.core.impl.executor.async; + +import static org.quartz.CronScheduleBuilder.cronSchedule; +import static org.quartz.JobBuilder.newJob; +import static org.quartz.TriggerBuilder.newTrigger; + +import org.apache.felix.hc.api.execution.HealthCheckMetadata; +import org.osgi.framework.BundleContext; +import org.quartz.CronTrigger; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Runs health checks that are configured with a cron expression for asynchronous execution. + * + * This implementation uses quartz to support the cron syntax (which is not supported by executors from standard java java.util.concurrent + * package) */ +public class AsyncHealthCheckQuartzCronJob extends AsyncHealthCheckJob implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(AsyncHealthCheckExecutor.class); + + private static final String JOB_DATA_KEY_JOB = "asyncHcJob"; + + protected final QuartzCronScheduler quartzCronScheduler; + private JobKey jobKey = null; + + public AsyncHealthCheckQuartzCronJob(HealthCheckMetadata healthCheckDescriptor, AsyncHealthCheckExecutor asyncHealthCheckExecutor, + BundleContext bundleContext, QuartzCronScheduler quartzScheduler) { + super(healthCheckDescriptor, asyncHealthCheckExecutor, bundleContext); + this.quartzCronScheduler = quartzScheduler; + } + + public JobKey getJobKey() { + return jobKey; + } + + private JobDetail getQuartzJobDetail() { + JobDataMap jobData = new JobDataMap(); + jobData.put(JOB_DATA_KEY_JOB, this); + + JobDetail job = newJob(AsyncHealthCheckQuartzCronJob.QuartzJob.class).setJobData(jobData) + .withIdentity("job-hc-" + healthCheckDescriptor.getServiceId(), "async-healthchecks") + .build(); + + jobKey = job.getKey(); + + return job; + } + + public boolean schedule() { + + try { + Scheduler scheduler = quartzCronScheduler.getScheduler(); + + JobDetail job = getQuartzJobDetail(); + CronTrigger cronTrigger = newTrigger().withSchedule(cronSchedule(healthCheckDescriptor.getAsyncCronExpression())).forJob(job) + .build(); + + scheduler.scheduleJob(job, cronTrigger); + LOG.info("Scheduled job {} with trigger {}", job, cronTrigger); + return true; + } catch (SchedulerException e) { + LOG.error("Could not schedule job for " + healthCheckDescriptor + ": " + e, e); + return false; + } + + } + + @Override + public boolean unschedule() { + Scheduler scheduler = quartzCronScheduler.getScheduler(); + LOG.debug("Unscheduling job {}", jobKey); + try { + scheduler.deleteJob(jobKey); + return true; + } catch (SchedulerException e) { + LOG.error("Could not unschedule job for " + jobKey + ": " + e, e); + return false; + } + } + + // quartz forces to pass in a class object (and not an instance), hence this helper class is needed + public static class QuartzJob implements Job { + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + AsyncHealthCheckQuartzCronJob hc = (AsyncHealthCheckQuartzCronJob) context.getJobDetail().getJobDataMap().get(JOB_DATA_KEY_JOB); + hc.run(); + } + + } + +} \ No newline at end of file diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/QuartzCronScheduler.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/QuartzCronScheduler.java new file mode 100644 index 00000000000..c43b97e5db1 --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/QuartzCronScheduler.java @@ -0,0 +1,116 @@ +/* + * 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 SF 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.hc.core.impl.executor.async; + +import org.apache.felix.hc.core.impl.executor.HealthCheckExecutorThreadPool; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.impl.DirectSchedulerFactory; +import org.quartz.simpl.RAMJobStore; +import org.quartz.spi.ThreadPool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class QuartzCronScheduler { + private static final Logger LOG = LoggerFactory.getLogger(QuartzCronScheduler.class); + + private static final String HC_SCHEDULER_NAME = "quartz.hc.scheduler_name"; + + private Scheduler scheduler; + + public QuartzCronScheduler(HealthCheckExecutorThreadPool healthCheckExecutorThreadPool) { + try { + DirectSchedulerFactory schedulerFactory = DirectSchedulerFactory.getInstance(); + ThreadPool threadPool = new QuartzThreadPool(healthCheckExecutorThreadPool); + schedulerFactory.createScheduler(HC_SCHEDULER_NAME, "id_" + System.currentTimeMillis(), threadPool, new RAMJobStore()); + scheduler = schedulerFactory.getScheduler(HC_SCHEDULER_NAME); + scheduler.start(); + LOG.debug("Started quartz scheduler {}", scheduler); + } catch (SchedulerException e) { + throw new IllegalStateException("Could not initialise/start quartz scheduler " + HC_SCHEDULER_NAME, e); + } + } + + public void shutdown() { + if (scheduler != null) { + try { + scheduler.shutdown(false); + LOG.debug("Shutdown of quartz scheduler finished: {}", scheduler); + } catch (SchedulerException e) { + throw new IllegalStateException("Could not shutdown quartz scheduler " + HC_SCHEDULER_NAME, e); + } + } + } + + public Scheduler getScheduler() { + return scheduler; + } + + public class QuartzThreadPool implements ThreadPool { + + private final HealthCheckExecutorThreadPool healthCheckExecutorThreadPool; + + public QuartzThreadPool(HealthCheckExecutorThreadPool healthCheckExecutorThreadPool) { + this.healthCheckExecutorThreadPool = healthCheckExecutorThreadPool; + } + + /** @see org.quartz.spi.QuartzThreadPool#getPoolSize() */ + @Override + public int getPoolSize() { + return healthCheckExecutorThreadPool.getPoolSize(); + } + + /** @see org.quartz.spi.QuartzThreadPool#initialize() */ + @Override + public void initialize() { + // nothing to do + } + + /** @see org.quartz.spi.ThreadPool#setInstanceId(java.lang.String) */ + @Override + public void setInstanceId(final String id) { + // we ignore this + } + + /** @see org.quartz.spi.ThreadPool#setInstanceName(java.lang.String) */ + @Override + public void setInstanceName(final String name) { + // we ignore this + } + + /** @see org.quartz.spi.QuartzThreadPool#runInThread(java.lang.Runnable) */ + @Override + public boolean runInThread(final Runnable job) { + healthCheckExecutorThreadPool.execute(job); + return true; + } + + /** @see org.quartz.spi.ThreadPool#blockForAvailableThreads() */ + @Override + public int blockForAvailableThreads() { + return healthCheckExecutorThreadPool.getMaxCurrentlyAvailableThreads(); + } + + /** @see org.quartz.spi.QuartzThreadPool#shutdown(boolean) */ + @Override + public void shutdown(final boolean waitForJobsToComplete) { + // this executor is bound to the SCR lifecycle of HealthCheckExecutorThreadPool + } + } + +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/filter/AdhocResultDuringRequestProcessingFilter.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/filter/AdhocResultDuringRequestProcessingFilter.java new file mode 100644 index 00000000000..e0a22a40327 --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/filter/AdhocResultDuringRequestProcessingFilter.java @@ -0,0 +1,277 @@ +/* + * 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 SF 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.hc.core.impl.filter; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.regex.Pattern; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.execution.HealthCheckExecutionOptions; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckExecutor; +import org.apache.felix.hc.api.execution.HealthCheckSelector; +import org.apache.felix.hc.core.impl.executor.CombinedExecutionResult; +import org.apache.felix.hc.core.impl.servlet.ResultTxtVerboseSerializer; +import org.apache.felix.hc.core.impl.util.AdhocStatusHealthCheck; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Dynamically adds a HC result for configured requests. */ +@Component(configurationPolicy = ConfigurationPolicy.REQUIRE) +@Designate(ocd = AdhocResultDuringRequestProcessingFilter.Config.class, factory = true) +public class AdhocResultDuringRequestProcessingFilter implements Filter { + private static final Logger LOG = LoggerFactory.getLogger(AdhocResultDuringRequestProcessingFilter.class); + + + @ObjectClassDefinition(name = "Health Check Adhoc Result during Request Processing", description = "Registers an health check with an adhoc result during request processing") + public @interface Config { + + @AttributeDefinition(name = "Filter Request Path RegEx", description = "Regex to be matched against request path") + String osgi_http_whiteboard_filter_regex(); + + @AttributeDefinition(name = "Filter Context", description = "Needs to be set to correct whiteboard context filter (e.g. '(osgi.http.whiteboard.context.name=default)'") + String osgi_http_whiteboard_context_select() default "(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=*)"; + + @AttributeDefinition(name = "Request Method", description = "Relevant request method (leave empty to not restrict to a method)") + String method() default ""; + + @AttributeDefinition(name = "User Agent RegEx", description = "Relevant user agent header (leave emtpy to not restrict to a user agent)") + String userAgentRegEx() default ""; + + @AttributeDefinition(name = "Health Check Name", description = "Name of health check during request processing") + String hcName() default "Ongoing request"; + + @AttributeDefinition(name = "Tags to register", description = "List of tags the adhoc result shall be registered for (tags are not active during configured delay in case 'delayProcessingInSec' is configured)") + String[] tags() default {}; + + @AttributeDefinition(name = "Status during request processing", description = "Status to be sent during request processing") + Result.Status statusDuringRequestProcessing() default Result.Status.TEMPORARILY_UNAVAILABLE; + + @AttributeDefinition(name = "Delay before request processing", description = "Time to delay processing of request in sec (the default 0 turns the delay off). Use together with 'tagsDuringDelayedProcessing' advertise request processing before actual action (e.g. to signal a deployment request to a periodically querying load balancer before deployment starts)") + long delayProcessingInSec() default 0; + + @AttributeDefinition(name = "Tags to register during delay before processing", description = "List of tags the adhoc result is be registered also during waiting for the configured delay") + String[] tagsDuringDelayedProcessing() default {}; + + @AttributeDefinition(name = "Tags to wait for after processing", description = "List of tags to be waited for after processing (leave empty to not wait). While waiting the tags from property 'tags' remain in configured state.") + String[] waitAfterProcessing_forTags() default {}; + + @AttributeDefinition(name = "Initial waiting time", description = "Initial waiting time until 'waitAfterProcessing.forTags' are checked for the first time.") + long waitAfterProcessing_initialWait() default 3; + + @AttributeDefinition(name = "Maximum delay after processing", description = "Maximum delay that can be caused when 'waitAfterProcessing.forTags' is configured (waiting is aborted after that time)") + long waitAfterProcessing_maxDelay() default 120; + + @AttributeDefinition + String webconsole_configurationFactory_nameHint() default "{hc.name} ({osgi.http.whiteboard.filter.regex} {method} {userAgentRegEx}) -> {statusDuringRequestProcessing} for tags {tags} {tagsDuringDelayedProcessing}"; + } + + private BundleContext bundleContext; + + private Result.Status statusDuringRequestProcessing; + private String hcNameDuringRequestProcessing; + private String[] hcTagsDuringRequestProcessing; + + private Long delayProcessingInSec; + private String[] tagsDuringDelayedProcessing; + + private String[] waitAfterProcessingForTags; + private long waitAfterProcessingInitialWait; + private long waitAfterProcessingMaxDelay; + + private String requiredMethod; + private Pattern userAgentRegEx; + + + @Reference + private HealthCheckExecutor executor; + + @Reference + ResultTxtVerboseSerializer verboseTxtSerializer; + + @Activate + protected final void activate(ComponentContext context, Config config) { + this.bundleContext = context.getBundleContext(); + this.statusDuringRequestProcessing = config.statusDuringRequestProcessing(); + this.hcNameDuringRequestProcessing = config.hcName(); + this.hcTagsDuringRequestProcessing = config.tags(); + + this.delayProcessingInSec = config.delayProcessingInSec() > 0 ? config.delayProcessingInSec(): null; + this.tagsDuringDelayedProcessing = config.tagsDuringDelayedProcessing(); + + this.waitAfterProcessingForTags = config.waitAfterProcessing_forTags(); + this.waitAfterProcessingInitialWait = config.waitAfterProcessing_initialWait(); + this.waitAfterProcessingMaxDelay = config.waitAfterProcessing_maxDelay(); + + this.requiredMethod = StringUtils.defaultIfBlank(config.method(), null); + this.userAgentRegEx = StringUtils.isNotBlank(config.userAgentRegEx()) ? Pattern.compile(config.userAgentRegEx()) : null; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { + + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + String requestPath = httpServletRequest.getRequestURI(); + String userAgent = httpServletRequest.getHeader("User-Agent"); + String method = httpServletRequest.getMethod(); + boolean isRelevantRequest = /* path is checked by filter pattern */ + (requiredMethod==null || requiredMethod.equals(method)) + && (userAgentRegEx==null || userAgentRegEx.matcher(userAgent).matches()); + if (isRelevantRequest) { + processRelevantRequest(request, response, filterChain, requestPath); + + } else { + // regular request processing + filterChain.doFilter(request, response); + } + } + + private void processRelevantRequest(ServletRequest request, ServletResponse response, FilterChain filterChain, String requestPath) + throws IOException, ServletException { + + AdhocStatusHealthCheck adhocStatusHealthCheck = null; + try { + + String mainHcMsg = "Request "+requestPath+" is being processed"; + if(delayProcessingInSec!=null) { + + String hcNameDuringDelay = hcNameDuringRequestProcessing +" (waiting)"; + AdhocStatusHealthCheck adhocStatusHealthCheckDelayedProcessing = null; + try { + adhocStatusHealthCheckDelayedProcessing = registerDynamicHealthCheck(statusDuringRequestProcessing, tagsDuringDelayedProcessing, hcNameDuringDelay, "Waiting "+delayProcessingInSec+"sec until continuing request "+requestPath); + + LOG.info("Delaying processing of request {} for {}sec", requestPath, delayProcessingInSec); + try { + Thread.sleep(delayProcessingInSec * 1000); + } catch (InterruptedException e) { + LOG.warn("Exception during delaying processing of request {} for {}sec", requestPath, delayProcessingInSec, e); + } + } finally { + // for the case delayProcessingInSec is set, register regular HC first and then unregister delay HC to ensure there is not even a short time span without result + adhocStatusHealthCheck = registerDynamicHealthCheck(statusDuringRequestProcessing, hcTagsDuringRequestProcessing, hcNameDuringRequestProcessing, mainHcMsg); + unregisterDynamicHealthCheck(adhocStatusHealthCheckDelayedProcessing); + } + } else { + adhocStatusHealthCheck = registerDynamicHealthCheck(statusDuringRequestProcessing, hcTagsDuringRequestProcessing, hcNameDuringRequestProcessing, mainHcMsg); + } + + filterChain.doFilter(request, response); + LOG.info("Request {} is processed", requestPath); + + if(waitAfterProcessingForTags.length > 0) { + + String initialWaitMsg = "Request "+requestPath +": Waiting for tags "+Arrays.asList(waitAfterProcessingForTags)+": initial wait " + waitAfterProcessingInitialWait+ "sec"; + adhocStatusHealthCheck.updateMessage(initialWaitMsg); + wait(requestPath, waitAfterProcessingInitialWait * 1000); + + long startTime = System.currentTimeMillis(); + for(;;) { + List executionResults = executor.execute(HealthCheckSelector.tags(waitAfterProcessingForTags).withNames("-"+hcNameDuringRequestProcessing), new HealthCheckExecutionOptions().setCombineTagsWithOr(true).setForceInstantExecution(true)); + CombinedExecutionResult combinedExecutionResult = new CombinedExecutionResult(executionResults); + Result overallResult = combinedExecutionResult.getHealthCheckResult(); + String verboseTxtResult = verboseTxtSerializer.serialize(overallResult, executionResults, false); + + String msg = "Request "+requestPath +": Waiting for tags "+Arrays.asList(waitAfterProcessingForTags)+": "+ overallResult.getStatus(); + LOG.info(msg); + if(LOG.isDebugEnabled()) { + LOG.debug("\n"+verboseTxtResult); + } + adhocStatusHealthCheck.updateMessage(msg); + if(overallResult.isOk()) { + break; + } + if((System.currentTimeMillis() - startTime) > (waitAfterProcessingMaxDelay*1000)) { + LOG.warn("Maximum delay time {}sec for tags {} exceeded - continuing anyway", waitAfterProcessingMaxDelay, Arrays.asList(waitAfterProcessingForTags)); + throw new ServletException("Maximum wait time "+waitAfterProcessingMaxDelay+"sec for tags "+Arrays.asList(waitAfterProcessingForTags)+" exceeded:\n"+verboseTxtResult); + } + + LOG.info("Waiting for tags {} before returning from {}", waitAfterProcessingForTags, requestPath); + wait(requestPath, 500); + } + } + + } finally { + unregisterDynamicHealthCheck(adhocStatusHealthCheck); + } + } + + private void wait(String requestPath, long waitTime) { + try { + Thread.sleep(waitTime); + } catch (InterruptedException e) { + LOG.warn("Exception during delaying processing of request {} for {}sec", requestPath, delayProcessingInSec, e); + } + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // no action required + } + + @Override + public void destroy() { + // no action required + } + + private AdhocStatusHealthCheck registerDynamicHealthCheck(Result.Status status, String[] tags, String hcName, String msg) { + AdhocStatusHealthCheck healthCheck = new AdhocStatusHealthCheck(status, msg); + Dictionary props = new Hashtable(); + props.put(HealthCheck.NAME, hcName); + props.put(HealthCheck.TAGS, tags); + + ServiceRegistration registration = bundleContext.registerService(HealthCheck.class, healthCheck, props); + healthCheck.setServiceRegistration(registration); + + return healthCheck; + } + + private synchronized void unregisterDynamicHealthCheck(AdhocStatusHealthCheck healthCheck) { + ServiceRegistration serviceRegistration = healthCheck !=null ? healthCheck.getServiceRegistration() : null; + if (serviceRegistration != null) { + serviceRegistration.unregister(); + LOG.debug("Unregistered adhoc HC"); + } + } + + +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/filter/ServiceUnavailableFilter.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/filter/ServiceUnavailableFilter.java new file mode 100644 index 00000000000..85498f01eb6 --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/filter/ServiceUnavailableFilter.java @@ -0,0 +1,257 @@ +/* + * 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 SF 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.hc.core.impl.filter; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Dictionary; +import java.util.List; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.execution.HealthCheckExecutionOptions; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckSelector; +import org.apache.felix.hc.core.impl.executor.CombinedExecutionResult; +import org.apache.felix.hc.core.impl.executor.ExtendedHealthCheckExecutor; +import org.apache.felix.hc.core.impl.servlet.ResultTxtVerboseSerializer; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceEvent; +import org.osgi.framework.ServiceListener; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Returns a 503 Service Unavailable Page if certain tags are in non-ok result. */ +@Component(service= {} /* Filter registers itself for better control */, immediate = true, + configurationPolicy = ConfigurationPolicy.REQUIRE) +@Designate(ocd = ServiceUnavailableFilter.Config.class, factory = true) +public class ServiceUnavailableFilter implements Filter { + private static final Logger LOG = LoggerFactory.getLogger(ServiceUnavailableFilter.class); + + private static final String CONTENT_TYPE_HTML = "text/html"; + private static final String CACHE_CONTROL_KEY = "Cache-control"; + private static final String CACHE_CONTROL_VALUE = "no-cache"; + + @ObjectClassDefinition(name = "Health Check Service Unavailable Filter", description = "Returns a 503 Service Unavailable Page if configured tags are in non-ok result") + public @interface Config { + + String HTML_RESPONSE_DEFAULT = "Service UnavailableService Unavailable"; + + @AttributeDefinition(name = "Filter Request Path RegEx", description = "Regex to be matched against request path") + String osgi_http_whiteboard_filter_regex(); + + @AttributeDefinition(name = "Filter Context", description = "Needs to be set to correct whiteboard context filter (e.g. '(osgi.http.whiteboard.context.name=default)'") + String osgi_http_whiteboard_context_select() default "(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=*)"; + + @AttributeDefinition(name = "Tags", description = "List of tags to query the status in order to decide if it is 503 or not") + String[] tags() default {}; + + @AttributeDefinition(name = "Status for 503 response", description = "First status that causes a 503 response. The default TEMPORARILY_UNAVAILABLE will not send 503 for OK and WARN but for TEMPORARILY_UNAVAILABLE, CRITICAL and HEALTH_CHECK_ERROR") + Result.Status statusFor503() default Result.Status.TEMPORARILY_UNAVAILABLE; + + @AttributeDefinition(name = "Include execution result as html comment", description = "Will include the execution result in html comment.") + boolean includeExecutionResultInHtmlComment() default true; + + @AttributeDefinition(name = "503 Html Content", description = "Html content for 503 responses") + String htmlFor503() default HTML_RESPONSE_DEFAULT; + + @AttributeDefinition(name = "Auto-disable filter", description = "If true, will automatically disable the filter once the filter continued the filter chain without 503 for the first time. Useful for server startup scenarios.") + boolean autoDisableFilter() default false; + + @AttributeDefinition + String webconsole_configurationFactory_nameHint() default "Send 503 for tags {tags} at status {statusFor503} (and worse) for path(s) {osgi.http.whiteboard.filter.regex}"; + } + + private String[] tags; + private Result.Status statusFor503; + private String htmlFor503; + private boolean includeExecutionResultInHtmlComment; + private boolean autoDisableFilter; + + @Reference + private ExtendedHealthCheckExecutor executor; + + @Reference + ResultTxtVerboseSerializer verboseTxtSerializer; + + private BundleContext bundleContext; + private Dictionary compProperties; + private ServiceListener healthCheckServiceListener; + private FrameworkListener frameworkListener; + private volatile ServiceRegistration filterServiceRegistration; + + private HealthCheckExecutionOptions healthCheckExecutionOptions; + private ServiceReference[] relevantHealthCheckServiceReferences; + + @Activate + protected final void activate(BundleContext bundleContext, ComponentContext componentContext, Config config) throws InvalidSyntaxException { + this.bundleContext = bundleContext; + this.compProperties = componentContext.getProperties(); + + this.tags = config.tags(); + this.statusFor503 = config.statusFor503(); + this.htmlFor503 = config.htmlFor503(); + this.includeExecutionResultInHtmlComment = config.includeExecutionResultInHtmlComment(); + this.autoDisableFilter = config.autoDisableFilter(); + + healthCheckExecutionOptions = new HealthCheckExecutionOptions().setCombineTagsWithOr(true); + healthCheckServiceListener = new HealthCheckServiceListener(); + bundleContext.addServiceListener(healthCheckServiceListener, "(objectclass="+HealthCheck.class.getName()+")"); + + if(autoDisableFilter) { + frameworkListener = new ReregisteringFilterFramworkListener(); + bundleContext.addFrameworkListener(frameworkListener); + } + + selectHcServiceReferences(); + registerFilter(); + } + + + @Deactivate + protected final void deactivate() { + if(healthCheckServiceListener!=null) { + bundleContext.removeServiceListener(healthCheckServiceListener); + } + if(frameworkListener!=null) { + bundleContext.removeFrameworkListener(frameworkListener); + } + + // unregisterFilter() last because above listeners potentially register the filter if they are still active + unregisterFilter(); + } + + + private synchronized void registerFilter() { + if(filterServiceRegistration == null) { + filterServiceRegistration = bundleContext.registerService(Filter.class, this, compProperties); + LOG.debug("Registered ServiceUnavailableFilter for tags {}", Arrays.asList(tags)); + } + } + + private synchronized void unregisterFilter() { + if(filterServiceRegistration!=null) { + filterServiceRegistration.unregister(); + filterServiceRegistration = null; + LOG.debug("Filter ServiceUnavailableFilter for tags {} unregistered", Arrays.asList(tags)); + } + } + + // using ServiceListener and ExtendedHealthCheckExecutor to avoid overhead of searching the service references on every request + private final void selectHcServiceReferences() { + LOG.debug("Reloading HC references for tags {}", Arrays.asList(tags)); + relevantHealthCheckServiceReferences = executor.selectHealthCheckReferences(HealthCheckSelector.tags(tags), healthCheckExecutionOptions); + LOG.debug("Found {} health check service references for tags {}", relevantHealthCheckServiceReferences.length, tags); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { + + final long startTimeNs = System.nanoTime(); + + List executionResults = executor.execute(relevantHealthCheckServiceReferences, healthCheckExecutionOptions); + CombinedExecutionResult combinedExecutionResult = new CombinedExecutionResult(executionResults); + Result overallResult = combinedExecutionResult.getHealthCheckResult(); + + if (LOG.isDebugEnabled()) { + LOG.debug("Time consumed for executing checks: {}ns", System.nanoTime() - startTimeNs); + } + + if(overallResult.getStatus().ordinal() >= statusFor503.ordinal()) { + LOG.debug("Result for tags {} is {}, sending 503 for {}", tags, overallResult.getStatus(), ((HttpServletRequest)request).getRequestURI()); + String verboseTxtResult = includeExecutionResultInHtmlComment ? verboseTxtSerializer.serialize(overallResult, executionResults, false) : null; + send503((HttpServletResponse) response, verboseTxtResult); + + } else { + if(autoDisableFilter && filterServiceRegistration!=null) { + LOG.info("Unregistering filter ServiceUnavailableFilter for tags {} since result was ok ", Arrays.asList(tags)); + unregisterFilter(); + } + + // regular request processing + filterChain.doFilter(request, response); + } + } + + private void send503(HttpServletResponse response, String verboseTxtResult) throws IOException { + response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + response.setContentType(CONTENT_TYPE_HTML); + response.setHeader(CACHE_CONTROL_KEY, CACHE_CONTROL_VALUE); + response.setCharacterEncoding("UTF-8"); + String bodyClosingTag = ""; + String htmlContent = verboseTxtResult!=null ? htmlFor503.replace(bodyClosingTag, "" + bodyClosingTag) : htmlFor503; + response.getWriter().append(htmlContent); + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // no action required + } + + @Override + public void destroy() { + // no action required + } + + private final class HealthCheckServiceListener implements ServiceListener { + @Override + public void serviceChanged(ServiceEvent event) { + LOG.debug("Service Event for Health Check: {}", event.getType()); + selectHcServiceReferences(); + if(filterServiceRegistration==null) { + registerFilter(); + } + } + } + + private final class ReregisteringFilterFramworkListener implements FrameworkListener { + @Override + public void frameworkEvent(FrameworkEvent event) { + if(event.getType() == FrameworkEvent.STARTLEVEL_CHANGED) { + if(filterServiceRegistration==null) { + registerFilter(); + } + } + } + } + +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServlet.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServlet.java new file mode 100644 index 00000000000..f4a6318f0ce --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServlet.java @@ -0,0 +1,432 @@ +/* + * 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 SF 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.hc.core.impl.servlet; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.StringEscapeUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.Result.Status; +import org.apache.felix.hc.api.execution.HealthCheckExecutionOptions; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckExecutor; +import org.apache.felix.hc.api.execution.HealthCheckSelector; +import org.apache.felix.hc.core.impl.executor.CombinedExecutionResult; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.http.HttpService; +import org.osgi.service.metatype.annotations.Designate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Servlet that triggers the health check executor to return results via http. + * + * Parameters: + *
        + *
      • tags: The health check tags to take into account + *
      • format: html|json|jsonp|txt|verbose.txt + *
      • includeDebug: If true, debug messages from result log are included. + *
      • callback: For jsonp, the JS callback function name (defaults to "processHealthCheckResults") + *
      • httpStatus: health check status to http status mapping in format httpStatus=WARN:418,CRITICAL:503,HEALTH_CHECK_ERROR:500. + *
      + * + * For omitted health check status values the next best code will be used (e.g. for httpStatus=CRITICAL:503 a result WARN will return 200, + * CRITICAL 503 and HEALTH_CHECK_ERROR also 503). By default all requests answer with an http status of 200. + *

      + * Useful in combination with load balancers. */ +@Component(configurationPolicy = ConfigurationPolicy.REQUIRE) +@Designate(ocd = HealthCheckExecutorServletConfiguration.class, factory=true) +public class HealthCheckExecutorServlet extends HttpServlet { + private static final long serialVersionUID = 8013511523994541848L; + + private static final Logger LOG = LoggerFactory.getLogger(HealthCheckExecutorServlet.class); + public static final String PARAM_SPLIT_REGEX = "[,;]+"; + + static class Param { + final String name; + final String description; + + Param(String n, String d) { + name = n; + description = d; + } + } + + static final Param PARAM_TAGS = new Param("tags", + "Comma-separated list of health checks tags to select - can also be specified via path, e.g. /system/health/tag1,tag2.json. Exclusions can be done by prepending '-' to the tag name"); + static final Param PARAM_FORMAT = new Param("format", "Output format, html|json|jsonp|txt - an extension in the URL overrides this"); + static final Param PARAM_HTTP_STATUS = new Param("httpStatus", "Specify HTTP result code, for example" + + " CRITICAL:503 (status 503 if result >= CRITICAL)" + + " or CRITICAL:503,HEALTH_CHECK_ERROR:500,OK:418 for more specific HTTP status"); + + static final Param PARAM_COMBINE_TAGS_WITH_OR = new Param("combineTagsWithOr", + "Combine tags with OR, active by default. Set to false to combine with AND"); + static final Param PARAM_FORCE_INSTANT_EXECUTION = new Param("forceInstantExecution", + "If true, forces instant execution by executing async health checks directly, circumventing the cache (2sec by default) of the HealthCheckExecutor"); + static final Param PARAM_OVERRIDE_GLOBAL_TIMEOUT = new Param("timeout", + "(msec) a timeout status is returned for any health check still running after this period. Overrides the default HealthCheckExecutor timeout"); + + static final Param PARAM_INCLUDE_DEBUG = new Param("hcDebug", "Include the DEBUG output of the Health Checks"); + + static final Param PARAM_NAMES = new Param("names", + "Comma-separated list of health check names to select. Exclusions can be done by prepending '-' to the health check name"); + + static final String JSONP_CALLBACK_DEFAULT = "processHealthCheckResults"; + static final Param PARAM_JSONP_CALLBACK = new Param("callback", + "name of the JSONP callback function to use, defaults to " + JSONP_CALLBACK_DEFAULT); + + static final Param[] PARAM_LIST = { PARAM_TAGS, PARAM_NAMES, PARAM_FORMAT, PARAM_HTTP_STATUS, PARAM_COMBINE_TAGS_WITH_OR, + PARAM_FORCE_INSTANT_EXECUTION, PARAM_OVERRIDE_GLOBAL_TIMEOUT, PARAM_INCLUDE_DEBUG, PARAM_JSONP_CALLBACK }; + + static final String FORMAT_HTML = "html"; + static final String FORMAT_JSON = "json"; + static final String FORMAT_JSONP = "jsonp"; + static final String FORMAT_TXT = "txt"; + static final String FORMAT_VERBOSE_TXT = "verbose.txt"; + + private static final String CONTENT_TYPE_HTML = "text/html"; + private static final String CONTENT_TYPE_TXT = "text/plain"; + private static final String CONTENT_TYPE_JSON = "application/json"; + private static final String CONTENT_TYPE_JSONP = "application/javascript"; + private static final String STATUS_HEADER_NAME = "X-Health"; + + private static final String CACHE_CONTROL_KEY = "Cache-control"; + private static final String CACHE_CONTROL_VALUE = "no-cache"; + private static final String CORS_ORIGIN_HEADER_NAME = "Access-Control-Allow-Origin"; + + private String[] servletPaths; + + private boolean disabled; + + private String servletPath; + + private String corsAccessControlAllowOrigin; + + private Map defaultStatusMapping; + + private long servletDefaultTimeout; + private String[] servletDefaultTags; + private String defaultFormat; + private boolean defaultCombineTagsWithOr; + + @Reference + private HttpService httpService; + + @Reference + HealthCheckExecutor healthCheckExecutor; + + @Reference + ResultHtmlSerializer htmlSerializer; + + @Reference + ResultJsonSerializer jsonSerializer; + + @Reference + ResultTxtSerializer txtSerializer; + + @Reference + ResultTxtVerboseSerializer verboseTxtSerializer; + + @Activate + protected final void activate(final HealthCheckExecutorServletConfiguration configuration) { + this.servletPath = configuration.servletPath(); + LOG.info("servletPath={}", servletPath); + + this.disabled = configuration.disabled(); + LOG.info("disabled={}", disabled); + + this.defaultStatusMapping = getStatusMapping(configuration.httpStatusMapping()); + LOG.info("defaultStatusMapping={}", defaultStatusMapping); + + this.servletDefaultTimeout = configuration.timeout(); + LOG.info("servletDefaultTimeout={}", servletDefaultTimeout); + + this.servletDefaultTags = configuration.tags(); + LOG.info("servletDefaultTags={}", servletDefaultTags!=null ? Arrays.asList(servletDefaultTags): ""); + + this.defaultCombineTagsWithOr = configuration.combineTagsWithOr(); + LOG.info("defaultCombineTagsWithOr={}", defaultCombineTagsWithOr); + + this.defaultFormat = configuration.format(); + LOG.info("defaultFormat={}", defaultFormat); + + this.corsAccessControlAllowOrigin = configuration.cors_accessControlAllowOrigin(); + LOG.info("corsAccessControlAllowOrigin={}", corsAccessControlAllowOrigin); + + if (disabled) { + LOG.info("Health Check Servlet is disabled by configuration"); + return; + } + + Map servletsToRegister = new LinkedHashMap(); + servletsToRegister.put(this.servletPath, this); + servletsToRegister.put(this.servletPath + "." + FORMAT_HTML, new ProxyServlet(FORMAT_HTML)); + servletsToRegister.put(this.servletPath + "." + FORMAT_JSON, new ProxyServlet(FORMAT_JSON)); + servletsToRegister.put(this.servletPath + "." + FORMAT_JSONP, new ProxyServlet(FORMAT_JSONP)); + servletsToRegister.put(this.servletPath + "." + FORMAT_TXT, new ProxyServlet(FORMAT_TXT)); + servletsToRegister.put(this.servletPath + "." + FORMAT_VERBOSE_TXT, new ProxyServlet(FORMAT_VERBOSE_TXT)); + + for (final Map.Entry servlet : servletsToRegister.entrySet()) { + try { + LOG.info("Registering HC servlet {} to path {}", getClass().getSimpleName(), servlet.getKey()); + this.httpService.registerServlet(servlet.getKey(), servlet.getValue(), null, null); + } catch (Exception e) { + LOG.error("Could not register health check servlet: " + e, e); + } + } + this.servletPaths = servletsToRegister.keySet().toArray(new String[0]); + + } + + @Deactivate + public void deactivate(final ComponentContext componentContext) { + if (disabled || this.servletPaths == null) { + return; + } + + for (final String servletPath : this.servletPaths) { + try { + LOG.debug("Unregistering path {}", servletPath); + this.httpService.unregister(servletPath); + } catch (Exception e) { + LOG.error("Could not unregister health check servlet: " + e, e); + } + } + this.servletPaths = null; + } + + protected void doGet(final HttpServletRequest request, final HttpServletResponse response, final String format) + throws ServletException, IOException { + HealthCheckSelector selector = HealthCheckSelector.empty(); + String pathInfo = request.getPathInfo(); + String pathTokensStr = StringUtils.removeStart(splitFormat(pathInfo)[0], "/"); + + List tags = new ArrayList(); + List names = new ArrayList(); + + if (StringUtils.isNotBlank(pathTokensStr)) { + String[] pathTokens = pathTokensStr.split(PARAM_SPLIT_REGEX); + for (String pathToken : pathTokens) { + if (pathToken.indexOf(' ') >= 0) { + // token contains space. assume it is a name + names.add(pathToken); + } else { + tags.add(pathToken); + } + } + } + if (tags.size() == 0) { + // if not provided via path use parameter or configured default + String tagsParameter = request.getParameter(PARAM_TAGS.name); + tags = Arrays.asList(StringUtils.isNotBlank(tagsParameter) ? tagsParameter.split(PARAM_SPLIT_REGEX): servletDefaultTags); + } + selector.withTags(tags.toArray(new String[0])); + + if (names.size() == 0) { + // if not provided via path use parameter or default + names = Arrays.asList(StringUtils.defaultIfEmpty(request.getParameter(PARAM_NAMES.name), "").split(PARAM_SPLIT_REGEX)); + } + selector.withNames(names.toArray(new String[0])); + + final Boolean includeDebug = Boolean.valueOf(request.getParameter(PARAM_INCLUDE_DEBUG.name)); + + String httpStatusMappingParameterVal = request.getParameter(PARAM_HTTP_STATUS.name); + final Map statusMapping = httpStatusMappingParameterVal!=null ? getStatusMapping(httpStatusMappingParameterVal) : defaultStatusMapping; + + HealthCheckExecutionOptions executionOptions = new HealthCheckExecutionOptions(); + + String paramCombineTagsWithOr = request.getParameter(PARAM_COMBINE_TAGS_WITH_OR.name); + executionOptions.setCombineTagsWithOr( paramCombineTagsWithOr!=null ? Boolean.valueOf(paramCombineTagsWithOr) : defaultCombineTagsWithOr); + + executionOptions.setForceInstantExecution(Boolean.valueOf(request.getParameter(PARAM_FORCE_INSTANT_EXECUTION.name))); + + String overrideGlobalTimeoutVal = request.getParameter(PARAM_OVERRIDE_GLOBAL_TIMEOUT.name); + if (StringUtils.isNumeric(overrideGlobalTimeoutVal)) { + executionOptions.setOverrideGlobalTimeout(Integer.valueOf(overrideGlobalTimeoutVal)); + } else if(servletDefaultTimeout > -1) { + executionOptions.setOverrideGlobalTimeout((int) servletDefaultTimeout); + } + + List executionResults = this.healthCheckExecutor.execute(selector, executionOptions); + + CombinedExecutionResult combinedExecutionResult = new CombinedExecutionResult(executionResults); + Result overallResult = combinedExecutionResult.getHealthCheckResult(); + + sendNoCacheHeaders(response); + sendCorsHeaders(response); + + Integer httpStatus = statusMapping.get(overallResult.getStatus()); + response.setStatus(httpStatus); + + response.setHeader(STATUS_HEADER_NAME, overallResult.getStatus().toString()); + if (FORMAT_HTML.equals(format)) { + sendHtmlResponse(overallResult, executionResults, request, response, includeDebug); + } else if (FORMAT_JSON.equals(format)) { + sendJsonResponse(overallResult, executionResults, null, response, includeDebug); + } else if (FORMAT_JSONP.equals(format)) { + String jsonpCallback = StringUtils.defaultIfEmpty(request.getParameter(PARAM_JSONP_CALLBACK.name), JSONP_CALLBACK_DEFAULT); + sendJsonResponse(overallResult, executionResults, jsonpCallback, response, includeDebug); + } else if (StringUtils.endsWith(format, FORMAT_TXT)) { + sendTxtResponse(overallResult, response, StringUtils.equals(format, FORMAT_VERBOSE_TXT), executionResults, includeDebug); + } else { + response.setContentType("text/plain"); + response.getWriter().println("Invalid format " + format + " - supported formats: html|json|jsonp|txt|verbose.txt"); + } + } + + @Override + protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { + String pathInfo = request.getPathInfo(); + String format = splitFormat(pathInfo)[1]; + if (StringUtils.isBlank(format)) { + // if not provided via extension use parameter or default + format = StringUtils.defaultIfEmpty(request.getParameter(PARAM_FORMAT.name), defaultFormat); + } + doGet(request, response, format); + } + + private String[] splitFormat(String pathInfo) { + for (String format : new String[] { FORMAT_HTML, FORMAT_JSON, FORMAT_JSONP, FORMAT_VERBOSE_TXT, FORMAT_TXT }) { + String formatWithDot = "." + format; + if (StringUtils.endsWith(pathInfo, formatWithDot)) { + return new String[] { StringUtils.substringBeforeLast(pathInfo, formatWithDot), format }; + } + } + return new String[] { pathInfo, null }; + } + + private void sendTxtResponse(final Result overallResult, final HttpServletResponse response, boolean verbose, + List executionResults, boolean includeDebug) throws IOException { + response.setContentType(CONTENT_TYPE_TXT); + response.setCharacterEncoding("UTF-8"); + if (verbose) { + response.getWriter().write(verboseTxtSerializer.serialize(overallResult, executionResults, includeDebug)); + } else { + response.getWriter().write(txtSerializer.serialize(overallResult)); + } + } + + private void sendJsonResponse(final Result overallResult, final List executionResults, + final String jsonpCallback, + final HttpServletResponse response, boolean includeDebug) + throws IOException { + if (StringUtils.isNotBlank(jsonpCallback)) { + response.setContentType(CONTENT_TYPE_JSONP); + } else { + response.setContentType(CONTENT_TYPE_JSON); + } + response.setCharacterEncoding("UTF-8"); + + String resultJson = this.jsonSerializer.serialize(overallResult, executionResults, jsonpCallback, includeDebug); + PrintWriter writer = response.getWriter(); + writer.append(resultJson); + } + + private void sendHtmlResponse(final Result overallResult, final List executionResults, + final HttpServletRequest request, final HttpServletResponse response, boolean includeDebug) + throws IOException { + response.setContentType(CONTENT_TYPE_HTML); + response.setCharacterEncoding("UTF-8"); + response.getWriter().append(this.htmlSerializer.serialize(overallResult, executionResults, getHtmlHelpText(), includeDebug)); + } + + private void sendNoCacheHeaders(final HttpServletResponse response) { + response.setHeader(CACHE_CONTROL_KEY, CACHE_CONTROL_VALUE); + } + + private void sendCorsHeaders(final HttpServletResponse response) { + if (StringUtils.isNotBlank(corsAccessControlAllowOrigin)) { + response.setHeader(CORS_ORIGIN_HEADER_NAME, corsAccessControlAllowOrigin); + } + } + + private String getHtmlHelpText() { + final StringBuilder sb = new StringBuilder(); + sb.append("

      Supported URL parameters

      \n"); + for (Param p : PARAM_LIST) { + sb.append("").append(p.name).append(":"); + sb.append(StringEscapeUtils.escapeHtml4(p.description)); + sb.append("
      "); + } + return sb.toString(); + } + + Map getStatusMapping(String mappingStr) { + Map statusMapping = new TreeMap(); + + try { + String[] bits = mappingStr.split("[,]"); + for (String bit : bits) { + String[] tuple = bit.split("[:]"); + statusMapping.put(Result.Status.valueOf(tuple[0]), Integer.parseInt(tuple[1])); + } + } catch (Exception e) { + throw new IllegalArgumentException("Invalid parameter httpStatus=" + mappingStr + " " + e, e); + } + + if (!statusMapping.containsKey(Result.Status.OK)) { + statusMapping.put(Result.Status.OK, 200); + } + if (!statusMapping.containsKey(Result.Status.WARN)) { + statusMapping.put(Result.Status.WARN, statusMapping.get(Result.Status.OK)); + } + if (!statusMapping.containsKey(Result.Status.TEMPORARILY_UNAVAILABLE)) { + statusMapping.put(Result.Status.TEMPORARILY_UNAVAILABLE, 503); + } + if (!statusMapping.containsKey(Result.Status.CRITICAL)) { + statusMapping.put(Result.Status.CRITICAL, 503); + } + if (!statusMapping.containsKey(Result.Status.HEALTH_CHECK_ERROR)) { + statusMapping.put(Result.Status.HEALTH_CHECK_ERROR, 500); + } + return statusMapping; + } + + private class ProxyServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + + private final String format; + + private ProxyServlet(final String format) { + this.format = format; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + HealthCheckExecutorServlet.this.doGet(req, resp, format); + } + } + +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServletConfiguration.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServletConfiguration.java new file mode 100644 index 00000000000..27945852519 --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServletConfiguration.java @@ -0,0 +1,56 @@ +/* + * 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 SF 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.hc.core.impl.servlet; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@ObjectClassDefinition(name = "Apache Felix Health Check Executor Servlet", description = "Serializes health check results into html, json or txt format") +@interface HealthCheckExecutorServletConfiguration { + + String SERVLET_PATH_DEFAULT = "/system/health"; + + @AttributeDefinition(name = "Path", description = "Servlet path (defaults to " + SERVLET_PATH_DEFAULT + + " in order to not be accessible via Apache/Internet)") + String servletPath() default SERVLET_PATH_DEFAULT; + + @AttributeDefinition(name = "Http Status Mapping", description = "Maps HC result status values to http response codes. Can be overwritten via request parameter 'httpStatus'") + String httpStatusMapping() default "OK:200,WARN:200,CRITICAL:503,TEMPORARILY_UNAVAILABLE:503,HEALTH_CHECK_ERROR:500"; + + @AttributeDefinition(name = "Timeout", description = "Timeout for health check executor. If not configured (left to -1), the default from health check executor's configuration is taken. The setting can always be overwritten by request parameter 'timeout'") + long timeout() default -1; + + @AttributeDefinition(name = "Default Tags", description = "Default tags if no tags are provided in URL.") + String[] tags() default {}; + + @AttributeDefinition(name = "Combine Tags with OR", description = "If true, will execute checks that have any of the given tags. If false, will only execute checks that have *all* of the given tags.") + boolean combineTagsWithOr() default true; + + @AttributeDefinition(name = "Default Format", description = "Default format if format is not provided in URL") + String format() default HealthCheckExecutorServlet.FORMAT_HTML; + + @AttributeDefinition(name = "Disabled", description = "Allows to disable the servlet if required for security reasons") + boolean disabled() default false; + + @AttributeDefinition(name = "CORS Access-Control-Allow-Origin", description = "Sets the Access-Control-Allow-Origin CORS header. If blank no header is sent.") + String cors_accessControlAllowOrigin() default "*"; + + @AttributeDefinition + String webconsole_configurationFactory_nameHint() default "{servletPath} default format:{format} default tags:{tags} "; + +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultHtmlSerializer.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultHtmlSerializer.java new file mode 100644 index 00000000000..c315c8b7788 --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultHtmlSerializer.java @@ -0,0 +1,154 @@ +/* + * 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 SF 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.hc.core.impl.servlet; + +import static org.apache.felix.hc.api.FormattingResultLog.msHumanReadable; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +import org.apache.commons.lang3.StringEscapeUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.ResultLog.Entry; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; + +/** Serializes health check results into html format. */ +@Component(service = ResultHtmlSerializer.class) +public class ResultHtmlSerializer { + + private String styleString; + + @Activate + protected final void activate(final ResultHtmlSerializerConfiguration configuration) { + this.styleString = configuration.styleString(); + } + + public String serialize(final Result overallResult, final List executionResults, String escapedHelpText, + boolean includeDebug) { + + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter); + + writer.println("System Health" + + "

      System Health

      "); + + writer.println("

      Overall Result: " + + overallResult.getStatus() + "

      "); + + final DateFormat dfShort = new SimpleDateFormat("HH:mm:ss.SSS"); + final DateFormat dfLong = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + + writer.println(""); + writer.println( + ""); + for (HealthCheckExecutionResult executionResult : executionResults) { + Result result = executionResult.getHealthCheckResult(); + List tags = executionResult.getHealthCheckMetadata().getTags(); + boolean hasTags = tags != null && tags.size() > 0 && StringUtils.isNotBlank(tags.get(0)); + writer.print(""); + writer.print(""); + writer.println(""); + writer.println(""); + Date finishedAt = executionResult.getFinishedAt(); + writer.println(""); + writer.println(""); + + writer.println(""); + } + writer.println("
      Health Check (tags)StatusLogFinished AtTime

      " + + StringEscapeUtils.escapeHtml4(executionResult.getHealthCheckMetadata().getTitle()) + ""); + if (hasTags) { + writer.println("
      " + StringEscapeUtils.escapeHtml4(StringUtils.join(tags, ", ")) + ""); + } + writer.println("

      " + StringEscapeUtils.escapeHtml4(result.getStatus().toString()) + ""); + boolean isFirst = true; + + boolean isSingleResult = isSingleResult(result); + + for (Entry entry : result) { + if (!includeDebug && entry.isDebug()) { + continue; + } + + if (isFirst) { + isFirst = false; + } else { + writer.println("
      \n"); + } + + boolean showStatus = !isSingleResult && !entry.isDebug() && entry.getStatus() != Result.Status.OK; + + String message = StringEscapeUtils.escapeHtml4(entry.getMessage()); + if (entry.isDebug()) { + message = "" + message + ""; + } + writer.println((showStatus ? StringEscapeUtils.escapeHtml4(entry.getStatus().toString()) + " " : "") + message); + + Exception exception = entry.getException(); + if (exception != null) { + writer.println("" + StringEscapeUtils.escapeHtml4(exception.toString())); + writer.println(""); + } + } + writer.println("
      " + (isToday(finishedAt) ? dfShort.format(finishedAt) : dfLong.format(finishedAt)) + "" + msHumanReadable(executionResult.getElapsedTimeInMs()) + "
      "); + + writer.println("
      "); + writer.println(escapedHelpText); + writer.println("
      "); + writer.println(""); + + return stringWriter.toString(); + + } + + private String getClassForStatus(final Result.Status status) { + return "status" + status.name(); + } + + private boolean isSingleResult(final Result result) { + int count = 0; + for (Entry entry : result) { + count++; + if (count > 1) { + return false; + } + } + return true; + } + + private boolean isToday(Date date) { + Calendar cal1 = Calendar.getInstance(); + Calendar cal2 = Calendar.getInstance(); + cal2.setTime(date); + boolean isToday = cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && + cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR); + return isToday; + + } +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultHtmlSerializerConfiguration.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultHtmlSerializerConfiguration.java new file mode 100644 index 00000000000..392e4fec8a9 --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultHtmlSerializerConfiguration.java @@ -0,0 +1,41 @@ +/* + * 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 SF 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.hc.core.impl.servlet; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@ObjectClassDefinition(name = "Apache Felix Health Check Result HTML Serializer", description = "Serializer for health check results in HTML format") +@interface ResultHtmlSerializerConfiguration { + + String CSS_STYLE_DEFAULT = "body { font-size:12px; font-family:arial,verdana,sans-serif;background-color:#FFFDF1; }\n" + + "h1 { font-size:20px;}\n" + + "table { font-size:12px; border:#ccc 1px solid; border-radius:3px; }\n" + + "table th { padding:5px; text-align: left; background: #ededed; }\n" + + "table td { padding:5px; border-top: 1px solid #ffffff; border-bottom:1px solid #e0e0e0; border-left: 1px solid #e0e0e0; }\n" + + ".statusOK { background-color:#CCFFCC;}\n" + + ".statusWARN { background-color:#FFE569;}\n" + + ".statusTEMPORARILY_UNAVAILABLE { background-color:#dab6fc;}\n" + + ".statusCRITICAL { background-color:#F0975A;}\n" + + ".statusHEALTH_CHECK_ERROR { background-color:#F16D4E;}\n" + + ".helpText { color:grey; font-size:80%; }\n"; + + @AttributeDefinition(name = "CSS Style", description = "CSS Style - can be configured to change the look and feel of the html result page.") + String styleString() default CSS_STYLE_DEFAULT; + +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultJsonSerializer.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultJsonSerializer.java new file mode 100644 index 00000000000..c373d66aaee --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultJsonSerializer.java @@ -0,0 +1,112 @@ +/* + * 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 SF 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.hc.core.impl.servlet; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.text.SimpleDateFormat; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.ResultLog; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.utils.json.JSONWriter; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Serializes health check results into json format. */ +@Component(service = ResultJsonSerializer.class) +public class ResultJsonSerializer { + + private static final Logger LOG = LoggerFactory.getLogger(ResultJsonSerializer.class); + + static final String OVERALL_RESULT_KEY = "OverallResult"; + + public String serialize(final Result overallResult, final List executionResults, final String jsonpCallback, + boolean includeDebug) { + + LOG.debug("Sending json response... "); + + StringWriter writer = new StringWriter(); + try { + JSONWriter jsonWriter = new JSONWriter(writer); + jsonWriter.object(); + jsonWriter.key("overallResult"); + jsonWriter.value(overallResult.getStatus().toString()); + jsonWriter.key("results"); + jsonWriter.array(); + for (HealthCheckExecutionResult healthCheckResult : executionResults) { + writeResult(healthCheckResult, includeDebug, jsonWriter); + } + jsonWriter.endArray(); + jsonWriter.endObject(); + } catch(IOException e) { + LOG.error("Could not serialise health check result: e="+e, e); + writer.write("{error:'"+e.getMessage()+"'}"); + } + String resultStr = writer.toString(); + + if (StringUtils.isNotBlank(jsonpCallback)) { + resultStr = jsonpCallback + "(" + resultStr + ");"; + } + + return resultStr; + + } + + private void writeResult(final HealthCheckExecutionResult healthCheckResult, boolean includeDebug, JSONWriter jsonWriter) throws IOException { + + jsonWriter.object() + .key("name").value(healthCheckResult.getHealthCheckMetadata().getTitle()) + .key("status").value(healthCheckResult.getHealthCheckResult().getStatus().toString()) + .key("timeInMs").value(healthCheckResult.getElapsedTimeInMs()) + .key("finishedAt").value(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS").format(healthCheckResult.getFinishedAt())) ; + + jsonWriter.key("tags").array(); + for(String tag: healthCheckResult.getHealthCheckMetadata().getTags()) { + jsonWriter.value(tag); + } + jsonWriter.endArray(); + + jsonWriter.key("messages").array(); + for (ResultLog.Entry entry : healthCheckResult.getHealthCheckResult()) { + if (!includeDebug && entry.isDebug()) { + continue; + } + jsonWriter.object() + .key("status").value(entry.getStatus().toString()) + .key("message").value(entry.getMessage()); + + Exception exception = entry.getException(); + if (exception != null) { + StringWriter stringWriter = new StringWriter(); + exception.printStackTrace(new PrintWriter(stringWriter)); + jsonWriter.key("exception").value(stringWriter.toString()); + } + jsonWriter.endObject(); + } + jsonWriter.endArray(); + + + jsonWriter.endObject(); + } + +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultTxtSerializer.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultTxtSerializer.java new file mode 100644 index 00000000000..eda14b5ceec --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultTxtSerializer.java @@ -0,0 +1,30 @@ +/* + * 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 SF 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.hc.core.impl.servlet; + +import org.apache.felix.hc.api.Result; +import org.osgi.service.component.annotations.Component; + +/** Serializes health check results into a simple text message (ideal to be used by a load balancer that would discard further + * information). */ +@Component(service = ResultTxtSerializer.class) +public class ResultTxtSerializer { + public String serialize(final Result overallResult) { + return overallResult.getStatus().toString(); + } +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultTxtVerboseSerializer.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultTxtVerboseSerializer.java new file mode 100644 index 00000000000..9d20a4bc1df --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultTxtVerboseSerializer.java @@ -0,0 +1,139 @@ +/* + * 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 SF 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.hc.core.impl.servlet; + +import static org.apache.felix.hc.api.FormattingResultLog.msHumanReadable; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.text.WordUtils; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.ResultLog; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Serializes health check results into a verbose text message. */ +@Component(service = ResultTxtVerboseSerializer.class) +public class ResultTxtVerboseSerializer { + + private static final Logger LOG = LoggerFactory.getLogger(ResultTxtVerboseSerializer.class); + + private static final String NEWLINE = "\n"; // not using system prop 'line.separator' as not the local but the calling system is + // relevant. + + private int totalWidth; + + private int colWidthName; + + private int colWidthResult; + + private int colWidthTiming; + + private int colWidthWithoutLog; + private int colWidthLog; + + @Activate + protected final void activate(final ResultTxtVerboseSerializerConfiguration configuration) { + this.totalWidth = configuration.totalWidth(); + this.colWidthName = configuration.colWidthName(); + this.colWidthResult = configuration.colWidthResult(); + this.colWidthTiming = configuration.colWidthTiming(); + colWidthWithoutLog = colWidthName + colWidthResult + colWidthTiming; + colWidthLog = totalWidth - colWidthWithoutLog; + } + + public String serialize(final Result overallResult, final List executionResults, boolean includeDebug) { + + LOG.debug("Sending verbose txt response... "); + + StringBuilder resultStr = new StringBuilder(); + + resultStr.append(StringUtils.repeat("-", totalWidth) + NEWLINE); + resultStr.append(StringUtils.center("Overall Health Result: " + overallResult.getStatus().toString(), totalWidth) + NEWLINE); + resultStr.append(StringUtils.repeat("-", totalWidth) + NEWLINE); + resultStr.append(StringUtils.rightPad("Name", colWidthName)); + resultStr.append(StringUtils.rightPad("Result", colWidthResult)); + resultStr.append(StringUtils.rightPad("Timing", colWidthTiming)); + resultStr.append("Logs" + NEWLINE); + resultStr.append(StringUtils.repeat("-", totalWidth) + NEWLINE); + + final DateFormat dfShort = new SimpleDateFormat("HH:mm:ss.SSS"); + + for (HealthCheckExecutionResult healthCheckResult : executionResults) { + appendVerboseTxtForResult(resultStr, healthCheckResult, includeDebug, dfShort); + } + resultStr.append(StringUtils.repeat("-", totalWidth) + NEWLINE); + + return resultStr.toString(); + + } + + private void appendVerboseTxtForResult(StringBuilder resultStr, HealthCheckExecutionResult healthCheckResult, boolean includeDebug, + DateFormat dfShort) { + + String wrappedName = WordUtils.wrap(healthCheckResult.getHealthCheckMetadata().getTitle(), colWidthName); + + String relevantNameStringForPadding = StringUtils.contains(wrappedName, "\n") ? StringUtils.substringAfterLast(wrappedName, "\n") + : wrappedName; + int paddingSize = colWidthName - relevantNameStringForPadding.length(); + + resultStr.append(wrappedName + StringUtils.repeat(" ", paddingSize)); + resultStr.append(StringUtils.rightPad(healthCheckResult.getHealthCheckResult().getStatus().toString(), colWidthResult)); + resultStr.append(StringUtils.rightPad("[" + dfShort.format(healthCheckResult.getFinishedAt()) + + "|" + msHumanReadable(healthCheckResult.getElapsedTimeInMs()) + "]", colWidthTiming)); + + boolean isFirst = true; + for (ResultLog.Entry logEntry : healthCheckResult.getHealthCheckResult()) { + if (!includeDebug && logEntry.isDebug()) { + continue; + } + if (isFirst) { + isFirst = false; + } else { + resultStr.append(StringUtils.repeat(" ", colWidthWithoutLog)); + } + + String oneLineMessage = getStatusForTxtLog(logEntry) + logEntry.getMessage(); + String messageToPrint = WordUtils.wrap(oneLineMessage, colWidthLog, "\n" + StringUtils.repeat(" ", colWidthWithoutLog), true); + + resultStr.append(messageToPrint); + resultStr.append(NEWLINE); + } + + if (isFirst) { + // no log entry exists, ensure newline + resultStr.append(NEWLINE); + } + + } + + private String getStatusForTxtLog(ResultLog.Entry logEntry) { + if (logEntry.getStatus() == Result.Status.OK) { + return ""; + } else { + return logEntry.getStatus().toString() + " "; + } + } + +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultTxtVerboseSerializerConfiguration.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultTxtVerboseSerializerConfiguration.java new file mode 100644 index 00000000000..ac515f67955 --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultTxtVerboseSerializerConfiguration.java @@ -0,0 +1,38 @@ +/* + * 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 SF 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.hc.core.impl.servlet; + +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@ObjectClassDefinition(name = "Apache Felix Health Check Verbose Text Serializer", description = "Serializes health check results to a verbose text format") +@interface ResultTxtVerboseSerializerConfiguration { + + @AttributeDefinition(name = "Total Width", description = "Total width of all columns in verbose txt rendering (in characters)") + int totalWidth() default 140; + + @AttributeDefinition(name = "Name Column Width", description = "Column width of health check name (in characters)") + int colWidthName() default 30; + + @AttributeDefinition(name = "Result Column Width", description = "Column width of health check result (in characters)") + int colWidthResult() default 25; + + @AttributeDefinition(name = "Timing Column Width", description = "Column width of health check timing (in characters)") + int colWidthTiming() default 22; + +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/util/AdhocStatusHealthCheck.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/util/AdhocStatusHealthCheck.java new file mode 100644 index 00000000000..2c1aeaa640c --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/util/AdhocStatusHealthCheck.java @@ -0,0 +1,55 @@ +/* + * 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 SF 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.hc.core.impl.util; + +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.osgi.framework.ServiceRegistration; + +public class AdhocStatusHealthCheck implements HealthCheck { + + private Result result; + + private ServiceRegistration serviceRegistration; + + public AdhocStatusHealthCheck(Result.Status status, String msg) { + result = new Result(status, msg); + } + + @Override + public Result execute() { + return result; + } + + public void updateMessage(String msg) { + this.result = new Result(result.getStatus(), msg); + } + + public void updateResult(Result result) { + this.result = result; + } + + public ServiceRegistration getServiceRegistration() { + return serviceRegistration; + } + + public void setServiceRegistration(ServiceRegistration serviceRegistration) { + this.serviceRegistration = serviceRegistration; + } + +} \ No newline at end of file diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/util/HealthCheckFilter.java b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/util/HealthCheckFilter.java new file mode 100644 index 00000000000..a3f30927e19 --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/util/HealthCheckFilter.java @@ -0,0 +1,145 @@ +/* + * 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 SF 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.hc.core.impl.util; + +import static org.apache.felix.hc.api.execution.HealthCheckSelector.empty; + +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.execution.HealthCheckSelector; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Select from available {@link HealthCheck} services. Once this filter object and the returned health check services are no longer be used + * {@link #dispose()} should be called, to free the service references. + * + * This class is not thread safe and instances shouldn't be used concurrently from different threads. */ +public class HealthCheckFilter { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + // object class (supporting current interface and legacy) + public static final String HC_FILTER_OBJECT_CLASS = "(|(objectClass="+HealthCheck.class.getName()+")(objectClass=org.apache.sling.hc.api.HealthCheck))"; + public static final String OMIT_PREFIX = "-"; + + private final BundleContext bundleContext; + + /** Create a new filter object */ + public HealthCheckFilter(final BundleContext bc) { + bundleContext = bc; + } + + public ServiceReference[] getHealthCheckServiceReferences(final HealthCheckSelector selector) { + return getHealthCheckServiceReferences(selector, false); + } + + public ServiceReference[] getHealthCheckServiceReferences(final HealthCheckSelector selector, boolean combineTagsWithOr) { + final CharSequence filterBuilder = selector != null ? getServiceFilter(selector, combineTagsWithOr) + : getServiceFilter(empty(), combineTagsWithOr); + + log.trace("OSGi service filter in getHealthCheckServiceReferences(): {}", filterBuilder); + + try { + final String filterString = filterBuilder.length() == 0 ? null : filterBuilder.toString(); + bundleContext.createFilter(filterString); // check syntax early + final ServiceReference[] refs = bundleContext.getServiceReferences((String) null, filterString); + if (refs == null) { + log.debug("Found no HealthCheck services for filter: {}", filterString); + return new ServiceReference[0]; + } else { + log.debug("Found {} HealthCheck services for filter: {}", refs.length, filterString); + } + return refs; + } catch (final InvalidSyntaxException ise) { + // this should not happen, but we fail gracefully + log.error("Invalid OSGi filter syntax in '" + filterBuilder + "'", ise); + return new ServiceReference[0]; + } + } + + + CharSequence getServiceFilter(HealthCheckSelector selector, boolean combineTagsWithOr) { + // Build service filter + final StringBuilder filterBuilder = new StringBuilder(); + filterBuilder.append("(&"); + filterBuilder.append(HC_FILTER_OBJECT_CLASS); + + final int prefixLen = HealthCheckFilter.OMIT_PREFIX.length(); + final StringBuilder filterBuilderForOrOperator = new StringBuilder(); // or filters + final StringBuilder tagsBuilder = new StringBuilder(); + int tagsAndClauses = 0; + if (selector.tags() != null) { + for (String tag : selector.tags()) { + tag = tag.trim(); + if (tag.length() == 0) { + continue; + } + if (tag.startsWith(HealthCheckFilter.OMIT_PREFIX)) { + // ommit tags always have to be added as and-clause + filterBuilder.append("(!(").append(HealthCheck.TAGS).append("=").append(tag.substring(prefixLen)).append("))"); + } else { + // add regular tags in the list either to outer and-clause or inner or-clause + if (combineTagsWithOr) { + filterBuilderForOrOperator.append("(").append(HealthCheck.TAGS).append("=").append(tag).append(")"); + } else { + tagsBuilder.append("(").append(HealthCheck.TAGS).append("=").append(tag).append(")"); + tagsAndClauses++; + } + } + } + } + boolean addedNameToOrBuilder = false; + if (selector.names() != null) { + for (String name : selector.names()) { + name = name.trim(); + if (name.length() == 0) { + continue; + } + if (name.startsWith(HealthCheckFilter.OMIT_PREFIX)) { + // ommit tags always have to be added as and-clause + filterBuilder.append("(!(").append(HealthCheck.NAME).append("=").append(escapeOsgiFilterLiteral(name.substring(prefixLen))).append("))"); + } else { + // names are always ORd + filterBuilderForOrOperator.append("(").append(HealthCheck.NAME).append("=").append(escapeOsgiFilterLiteral(name)).append(")"); + addedNameToOrBuilder = true; + } + } + } + if (addedNameToOrBuilder) { + if (tagsAndClauses > 1) { + filterBuilderForOrOperator.append("(&").append(tagsBuilder).append(")"); + } else { + filterBuilderForOrOperator.append(tagsBuilder); + } + } else { + filterBuilder.append(tagsBuilder); + } + // add "or" clause if we have accumulated any + if (filterBuilderForOrOperator.length() > 0) { + filterBuilder.append("(|").append(filterBuilderForOrOperator).append(")"); + } + filterBuilder.append(")"); + return filterBuilder; + } + + private Object escapeOsgiFilterLiteral(String name) { + return name.replace("*", "\\*").replace("(", "\\(").replace(")", "\\)"); + } +} diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/jmx/impl/HealthCheckMBean.java b/healthcheck/core/src/main/java/org/apache/felix/hc/jmx/impl/HealthCheckMBean.java new file mode 100644 index 00000000000..7b26cde8f2e --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/jmx/impl/HealthCheckMBean.java @@ -0,0 +1,258 @@ +/* + * 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 SF 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.hc.jmx.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.AttributeNotFoundException; +import javax.management.DynamicMBean; +import javax.management.InvalidAttributeValueException; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanException; +import javax.management.MBeanInfo; +import javax.management.ReflectionException; +import javax.management.openmbean.CompositeDataSupport; +import javax.management.openmbean.CompositeType; +import javax.management.openmbean.OpenDataException; +import javax.management.openmbean.OpenMBeanAttributeInfoSupport; +import javax.management.openmbean.OpenType; +import javax.management.openmbean.SimpleType; +import javax.management.openmbean.TabularData; +import javax.management.openmbean.TabularDataSupport; +import javax.management.openmbean.TabularType; + +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.ResultLog; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.core.impl.executor.ExtendedHealthCheckExecutor; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + +/** A {@link DynamicMBean} used to execute a {@link HealthCheck} service */ +public class HealthCheckMBean implements DynamicMBean { + + private static final String HC_OK_ATTRIBUTE_NAME = "ok"; + private static final String HC_STATUS_ATTRIBUTE_NAME = "status"; + private static final String HC_LOG_ATTRIBUTE_NAME = "log"; + private static final String HC_TIMED_OUT_ATTRIBUTE_NAME = "timedOut"; + private static final String HC_ELAPSED_TIMED_ATTRIBUTE_NAME = "elapsedTime"; + private static final String HC_FINISHED_AT_ATTRIBUTE_NAME = "finishedAt"; + private static CompositeType LOG_ROW_TYPE; + private static TabularType LOG_TABLE_TYPE; + + private static final String INDEX_COLUMN = "index"; + private static final String LEVEL_COLUMN = "level"; + private static final String MESSAGE_COLUMN = "message"; + + /** The health check service to call. */ + private final ServiceReference healthCheckRef; + + /** The executor service. */ + private final ExtendedHealthCheckExecutor executor; + + /** The mbean info. */ + private final MBeanInfo mbeanInfo; + + /** The default attributes. */ + private final Map defaultAttributes; + + static { + try { + // Define the log row and table types + LOG_ROW_TYPE = new CompositeType( + "LogLine", + "A line in the result log", + new String[] { INDEX_COLUMN, LEVEL_COLUMN, MESSAGE_COLUMN }, + new String[] { "log line index", "log level", "log message" }, + new OpenType[] { SimpleType.INTEGER, SimpleType.STRING, SimpleType.STRING }); + final String[] indexes = { INDEX_COLUMN }; + LOG_TABLE_TYPE = new TabularType("LogTable", "Result log messages", LOG_ROW_TYPE, indexes); + } catch (Exception ignore) { + // row or table type will be null if this happens + } + } + + public HealthCheckMBean(final ServiceReference ref, final ExtendedHealthCheckExecutor executor) { + this.healthCheckRef = ref; + this.executor = executor; + this.mbeanInfo = this.createMBeanInfo(ref); + this.defaultAttributes = this.createDefaultAttributes(ref); + } + + @Override + public Object getAttribute(final String attribute) + throws AttributeNotFoundException, MBeanException, ReflectionException { + // we should call getAttributes - and not vice versa to have the result + // of a single check call - and not do a check call for each attribute + final AttributeList result = this.getAttributes(new String[] { attribute }); + if (result.size() == 0) { + throw new AttributeNotFoundException(attribute); + } + final Attribute attr = (Attribute) result.get(0); + return attr.getValue(); + } + + private TabularData logData(final Result er) throws OpenDataException { + final TabularDataSupport result = new TabularDataSupport(LOG_TABLE_TYPE); + int i = 1; + for (final ResultLog.Entry e : er) { + final Map data = new HashMap(); + data.put(INDEX_COLUMN, i++); + data.put(LEVEL_COLUMN, e.getLogLevel()); + data.put(MESSAGE_COLUMN, e.getMessage()); + + result.put(new CompositeDataSupport(LOG_ROW_TYPE, data)); + } + return result; + } + + @Override + public AttributeList getAttributes(final String[] attributes) { + final AttributeList result = new AttributeList(); + if (attributes != null) { + HealthCheckExecutionResult hcResult = null; + for (final String key : attributes) { + final Object defaultValue = this.defaultAttributes.get(key); + if (defaultValue != null) { + result.add(new Attribute(key, defaultValue)); + } else { + // we assume that a valid attribute name is used + // which is requesting a hc result + if (hcResult == null) { + hcResult = this.getHealthCheckResult(); + } + + if (HC_OK_ATTRIBUTE_NAME.equals(key)) { + result.add(new Attribute(key, hcResult.getHealthCheckResult().isOk())); + } else if (HC_LOG_ATTRIBUTE_NAME.equals(key)) { + try { + result.add(new Attribute(key, logData(hcResult.getHealthCheckResult()))); + } catch (final OpenDataException ignore) { + // we ignore this and simply don't add the attribute + } + } else if (HC_STATUS_ATTRIBUTE_NAME.equals(key)) { + result.add(new Attribute(key, hcResult.getHealthCheckResult().getStatus().toString())); + } else if (HC_ELAPSED_TIMED_ATTRIBUTE_NAME.equals(key)) { + result.add(new Attribute(key, hcResult.getElapsedTimeInMs())); + } else if (HC_FINISHED_AT_ATTRIBUTE_NAME.equals(key)) { + result.add(new Attribute(key, hcResult.getFinishedAt())); + } else if (HC_TIMED_OUT_ATTRIBUTE_NAME.equals(key)) { + result.add(new Attribute(key, hcResult.hasTimedOut())); + } + } + } + } + + return result; + } + + /** Create the mbean info */ + private MBeanInfo createMBeanInfo(final ServiceReference serviceReference) { + final List attrs = new ArrayList(); + + // add relevant service properties + if (serviceReference.getProperty(HealthCheck.NAME) != null) { + attrs.add(new MBeanAttributeInfo(HealthCheck.NAME, String.class.getName(), "The name of the health check service.", true, false, + false)); + } + if (serviceReference.getProperty(HealthCheck.TAGS) != null) { + attrs.add(new MBeanAttributeInfo(HealthCheck.TAGS, String.class.getName(), "The tags of the health check service.", true, false, + false)); + } + + // add standard attributes + attrs.add(new MBeanAttributeInfo(HC_OK_ATTRIBUTE_NAME, Boolean.class.getName(), "The health check result", true, false, false)); + attrs.add(new MBeanAttributeInfo(HC_STATUS_ATTRIBUTE_NAME, String.class.getName(), "The health check status", true, false, false)); + attrs.add(new MBeanAttributeInfo(HC_ELAPSED_TIMED_ATTRIBUTE_NAME, Long.class.getName(), "The elapsed time in miliseconds", true, + false, false)); + attrs.add(new MBeanAttributeInfo(HC_FINISHED_AT_ATTRIBUTE_NAME, Date.class.getName(), "The date when the execution finished", true, + false, false)); + attrs.add(new MBeanAttributeInfo(HC_TIMED_OUT_ATTRIBUTE_NAME, Boolean.class.getName(), "Indicates of the execution timed out", true, + false, false)); + attrs.add(new OpenMBeanAttributeInfoSupport(HC_LOG_ATTRIBUTE_NAME, "The health check result log", LOG_TABLE_TYPE, true, false, + false)); + + final String description; + if (serviceReference.getProperty(Constants.SERVICE_DESCRIPTION) != null) { + description = serviceReference.getProperty(Constants.SERVICE_DESCRIPTION).toString(); + } else { + description = "Health check"; + } + return new MBeanInfo(this.getClass().getName(), + description, + attrs.toArray(new MBeanAttributeInfo[attrs.size()]), null, null, null); + } + + /** Create the default attributes. */ + private Map createDefaultAttributes(final ServiceReference serviceReference) { + final Map list = new HashMap(); + if (serviceReference.getProperty(HealthCheck.NAME) != null) { + list.put(HealthCheck.NAME, serviceReference.getProperty(HealthCheck.NAME).toString()); + } + if (serviceReference.getProperty(HealthCheck.TAGS) != null) { + final Object value = serviceReference.getProperty(HealthCheck.TAGS); + if (value instanceof String[]) { + list.put(HealthCheck.TAGS, Arrays.toString((String[]) value)); + } else { + list.put(HealthCheck.TAGS, value.toString()); + } + } + + return list; + } + + @Override + public MBeanInfo getMBeanInfo() { + return this.mbeanInfo; + } + + @Override + public Object invoke(final String actionName, final Object[] params, final String[] signature) + throws MBeanException, ReflectionException { + throw new MBeanException(new UnsupportedOperationException(getClass().getSimpleName() + " does not support operations.")); + } + + @Override + public void setAttribute(final Attribute attribute) + throws AttributeNotFoundException, InvalidAttributeValueException, + MBeanException, ReflectionException { + throw new MBeanException(new UnsupportedOperationException(getClass().getSimpleName() + " does not support setting attributes.")); + } + + @Override + public AttributeList setAttributes(final AttributeList attributes) { + return new AttributeList(); + } + + @Override + public String toString() { + return "HealthCheckMBean [healthCheck=" + this.healthCheckRef + "]"; + } + + private HealthCheckExecutionResult getHealthCheckResult() { + return this.executor.execute(this.healthCheckRef); + } +} \ No newline at end of file diff --git a/healthcheck/core/src/main/java/org/apache/felix/hc/jmx/impl/HealthCheckMBeanCreator.java b/healthcheck/core/src/main/java/org/apache/felix/hc/jmx/impl/HealthCheckMBeanCreator.java new file mode 100644 index 00000000000..248971b375b --- /dev/null +++ b/healthcheck/core/src/main/java/org/apache/felix/hc/jmx/impl/HealthCheckMBeanCreator.java @@ -0,0 +1,205 @@ +/* + * 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 SF 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.hc.jmx.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +import javax.management.DynamicMBean; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.core.impl.executor.ExtendedHealthCheckExecutor; +import org.apache.felix.hc.core.impl.util.HealthCheckFilter; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.util.tracker.ServiceTracker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Creates an {@link HealthCheckMBean} for every {@link HealthCheckMBean} service */ +@Component +public class HealthCheckMBeanCreator { + + private static final String JMX_TYPE_NAME = "HealthCheck"; + private static final String JMX_DOMAIN = "org.apache.felix.healthcheck"; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final Map, Registration> registeredServices = new HashMap<>(); + + private final Map>> sortedRegistrations = new HashMap<>(); + + private ServiceTracker hcTracker; + + @Reference + private ExtendedHealthCheckExecutor executor; + + @Activate + protected void activate(final BundleContext btx) { + try { + Filter filter = btx.createFilter("(&"+HealthCheckFilter.HC_FILTER_OBJECT_CLASS+"("+HealthCheck.MBEAN_NAME+"=*))"); + this.hcTracker = new HealthCheckServiceTracker(btx, filter); + this.hcTracker.open(); + } catch (InvalidSyntaxException e) { + logger.warn("Could not create service tracker for filter "+HealthCheckFilter.HC_FILTER_OBJECT_CLASS, e); + } + + } + + @Deactivate + protected void deactivate() { + if (this.hcTracker != null) { + this.hcTracker.close(); + this.hcTracker = null; + } + } + + /** Register an mbean for a health check service. The mbean is only registered if - the service has an mbean registration property - if + * there is no other service with the same name but a higher service ranking + * + * @param bundleContext The bundle context + * @param reference The service reference to the health check service + * @return The registered mbean or null */ + private synchronized Object registerHCMBean(final BundleContext bundleContext, final ServiceReference reference) { + final Registration reg = getRegistration(reference); + if (reg != null) { + this.registeredServices.put(reference, reg); + + List> registered = this.sortedRegistrations.get(reg.name); + if (registered == null) { + registered = new ArrayList<>(); + this.sortedRegistrations.put(reg.name, registered); + } + registered.add(reference); + // sort orders the references with lowest ranking first + // we want the highest! + Collections.sort(registered); + final int lastIndex = registered.size() - 1; + if (registered.get(lastIndex).equals(reference)) { + if (registered.size() > 1) { + final ServiceReference prevRef = registered.get(lastIndex - 1); + final Registration prevReg = this.registeredServices.get(prevRef); + prevReg.unregister(); + } + reg.register(bundleContext); + } + } + return reg; + } + + private synchronized void unregisterHCMBean(final BundleContext bundleContext, final ServiceReference ref) { + final Registration reg = registeredServices.remove(ref); + if (reg != null) { + final boolean registerFirst = reg.unregister(); + final List> registered = this.sortedRegistrations.get(reg.name); + registered.remove(ref); + if (registered.size() == 0) { + this.sortedRegistrations.remove(reg.name); + } else if (registerFirst) { + final ServiceReference newRef = registered.get(0); + final Registration newReg = this.registeredServices.get(newRef); + newReg.register(bundleContext); + } + bundleContext.ungetService(ref); + } + } + + private final class HealthCheckServiceTracker extends ServiceTracker { + private final BundleContext bundleContext; + + private HealthCheckServiceTracker(BundleContext bundleContext, Filter filter) { + super(bundleContext, filter, null); + this.bundleContext = bundleContext; + } + + @Override + public Object addingService(final ServiceReference reference) { + return registerHCMBean(bundleContext, reference); + } + + @Override + public void modifiedService(final ServiceReference reference, + final Object service) { + unregisterHCMBean(bundleContext, reference); + registerHCMBean(bundleContext, reference); + } + + @Override + public void removedService(final ServiceReference reference, + final Object service) { + unregisterHCMBean(bundleContext, reference); + } + } + + private final class Registration { + private final String name; + private final HealthCheckMBean mbean; + + private final String objectName; + + private ServiceRegistration registration; + + Registration(final String name, final HealthCheckMBean mbean) { + this.name = name; + this.mbean = mbean; + objectName = String.format("%s:type=%s,name=%s", JMX_DOMAIN, JMX_TYPE_NAME, name); + } + + void register(final BundleContext btx) { + logger.debug("Registering health check mbean {} with name {}", mbean, objectName); + final Dictionary mbeanProps = new Hashtable(); + mbeanProps.put("jmx.objectname", objectName); + this.registration = btx.registerService(DynamicMBean.class, this.mbean, mbeanProps); + } + + boolean unregister() { + if (this.registration != null) { + logger.debug("Unregistering health check mbean {} with name {}", mbean, objectName); + this.registration.unregister(); + this.registration = null; + return true; + } + return false; + } + } + + private Registration getRegistration(final ServiceReference ref) { + final String hcMBeanName = (String) ref.getProperty(HealthCheck.MBEAN_NAME); + if (StringUtils.isNotBlank(hcMBeanName)) { + final HealthCheckMBean mbean = new HealthCheckMBean(ref, executor); + return new Registration(hcMBeanName.replace(',', '.'), mbean); + } else { + logger.warn("Service reference {} unexpectedly has property {} not set", ref, HealthCheck.MBEAN_NAME); + return null; + } + } + +} diff --git a/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/CompositeHealthCheckTest.java b/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/CompositeHealthCheckTest.java new file mode 100644 index 00000000000..347c67d236d --- /dev/null +++ b/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/CompositeHealthCheckTest.java @@ -0,0 +1,293 @@ +/* + * 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 SF 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.hc.core.impl; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.execution.HealthCheckExecutionOptions; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckExecutor; +import org.apache.felix.hc.api.execution.HealthCheckMetadata; +import org.apache.felix.hc.api.execution.HealthCheckSelector; +import org.apache.felix.hc.core.impl.executor.ExecutionResult; +import org.apache.felix.hc.core.impl.util.HealthCheckFilter; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Matchers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.service.component.ComponentConstants; +import org.osgi.service.component.ComponentContext; + +public class CompositeHealthCheckTest { + + @Spy + private CompositeHealthCheck compositeHealthCheck = new CompositeHealthCheck(); + + @Mock + private HealthCheckExecutor healthCheckExecutor; + + @Mock + private ComponentContext componentContext; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + compositeHealthCheck.setHealthCheckExecutor(healthCheckExecutor); + compositeHealthCheck.setFilterTags(new String[] {}); + compositeHealthCheck.setComponentContext(componentContext); + } + + @Test + public void testExecution() { + + doReturn((Result) null).when(compositeHealthCheck).checkForRecursion(Matchers. any(), + Matchers.> any()); + String[] testTags = new String[] { "tag1" }; + compositeHealthCheck.setFilterTags(testTags); + + List executionResults = new LinkedList(); + executionResults.add(createExecutionResult("Check 1", testTags, new Result(Result.Status.OK, "Good"))); + executionResults.add(createExecutionResult("Check 2", testTags, new Result(Result.Status.CRITICAL, "Bad"))); + + when(healthCheckExecutor.execute(any(HealthCheckSelector.class), any(HealthCheckExecutionOptions.class))) + .thenReturn(executionResults); + + Result result = compositeHealthCheck.execute(); + + verify(healthCheckExecutor, times(1)).execute(argThat(selectorWithTags(testTags)), argThat(andOptions)); + + assertEquals(Result.Status.CRITICAL, result.getStatus()); + + } + + private Matcher selectorWithTags(final String[] tags) { + return new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(HealthCheckSelector healthCheckSelector) { + return Arrays.equals(healthCheckSelector.tags(), tags) && healthCheckSelector.names() == null; + } + + @Override + public void describeTo(Description description) { + description.appendText("a select with tags (" + Arrays.toString(tags) + ") and no names."); + } + }; + } + + private HealthCheckExecutionResult createExecutionResult(String name, String[] testTags, Result result) { + HealthCheckExecutionResult healthCheckExecutionResult = new ExecutionResult( + new HealthCheckMetadata(new DummyHcServiceReference(name, testTags, + new String[0])), + result, 0L); + return healthCheckExecutionResult; + } + + @Test + public void testSimpleRecursion() { + + // composite check referencing itself + final String[] filterTags = new String[] { "check1" }; + final DummyHcServiceReference hcRef = new DummyHcServiceReference("Check 1", new String[] { "check1" }, filterTags); + + // test check is hcRef + doReturn(hcRef).when(componentContext).getServiceReference(); + compositeHealthCheck.setFilterTags(filterTags); + + compositeHealthCheck.setHealthCheckFilter(new HealthCheckFilter(null) { + + @Override + public ServiceReference[] getHealthCheckServiceReferences(HealthCheckSelector selector) { + String[] tags = selector.tags(); + ServiceReference[] result = new ServiceReference[] {}; + if (tags.length > 0) { + if (tags[0].equals(filterTags[0])) { + result = new ServiceReference[] { hcRef }; + } + } + return result; + } + + }); + + Result result = compositeHealthCheck.execute(); + + verify(healthCheckExecutor, never()).execute(any(HealthCheckSelector.class)); + assertEquals(Result.Status.HEALTH_CHECK_ERROR, result.getStatus()); + } + + @Test + public void testCyclicRecursion() { + + // three checks, cyclic + final String[] filterTags = new String[] { "check2" }; + final DummyHcServiceReference hcRef1 = new DummyHcServiceReference("Check 1", new String[] { "check1" }, filterTags); + final DummyHcServiceReference hcRef2 = new DummyHcServiceReference("Check 2", new String[] { "check2" }, new String[] { "check3" }); + final DummyHcServiceReference hcRef3 = new DummyHcServiceReference("Check 3", new String[] { "check3" }, new String[] { "check1" }); + + // test check is hcRef1 + doReturn(hcRef1).when(componentContext).getServiceReference(); + compositeHealthCheck.setFilterTags(filterTags); + + compositeHealthCheck.setHealthCheckFilter(new HealthCheckFilter(null) { + + @Override + public ServiceReference[] getHealthCheckServiceReferences(HealthCheckSelector selector, boolean combineTagsWithOr) { + String[] tags = selector.tags(); + ServiceReference[] result = new ServiceReference[] {}; + if (tags.length > 0) { + if (tags[0].equals(filterTags[0])) { + result = new ServiceReference[] { hcRef2 }; + } else if (tags[0].equals("check3")) { + result = new ServiceReference[] { hcRef3 }; + } else if (tags[0].equals("check1")) { + result = new ServiceReference[] { hcRef1 }; + } + } + + return result; + } + + }); + + Result result = compositeHealthCheck.execute(); + + verify(healthCheckExecutor, never()).execute(any(HealthCheckSelector.class)); + assertEquals(Result.Status.HEALTH_CHECK_ERROR, result.getStatus()); + } + + @Test + public void testCombineWithOr() { + + // composite check referencing itself + final String[] filterTags = new String[] { "check1" }; + compositeHealthCheck.setFilterTags(filterTags); + compositeHealthCheck.setCombineTagsWithOr(true); + + compositeHealthCheck.execute(); + + verify(healthCheckExecutor, times(1)).execute(argThat(selectorWithTags(filterTags)), argThat(orOptions)); + } + + private Matcher orOptions = new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(HealthCheckExecutionOptions options) { + return options.isCombineTagsWithOr(); + } + + @Override + public void describeTo(Description description) { + description.appendText("options combining tags with or."); + } + }; + + private Matcher andOptions = new TypeSafeMatcher() { + @Override + protected boolean matchesSafely(HealthCheckExecutionOptions options) { + return !options.isCombineTagsWithOr(); + } + + @Override + public void describeTo(Description description) { + description.appendText("options combining tags with and."); + } + }; + + private static class DummyHcServiceReference implements ServiceReference { + + private long id; + private String name; + private String[] tags; + private String[] filterTags; + + public DummyHcServiceReference(String name, String[] tags, String[] filterTags) { + super(); + this.id = (long) (Math.random() * Long.MAX_VALUE); + this.name = name; + this.tags = tags; + this.filterTags = filterTags; + } + + @Override + public Object getProperty(String key) { + + if (Constants.SERVICE_ID.equals(key)) { + return id; + } else if (HealthCheck.NAME.equals(key)) { + return name; + } else if (HealthCheck.MBEAN_NAME.equals(key)) { + return name; + } else if (HealthCheck.TAGS.equals(key)) { + return tags; + } else if (CompositeHealthCheck.PROP_FILTER_TAGS.equals(key)) { + return filterTags; + } else if (ComponentConstants.COMPONENT_NAME.equals(key)) { + return filterTags != null ? CompositeHealthCheck.class.getName() : "some.other.HealthCheck"; + } else { + return null; + } + } + + @Override + public String[] getPropertyKeys() { + throw new UnsupportedOperationException(); + } + + @Override + public Bundle getBundle() { + throw new UnsupportedOperationException(); + } + + @Override + public Bundle[] getUsingBundles() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isAssignableTo(Bundle bundle, String className) { + throw new UnsupportedOperationException(); + } + + @Override + public int compareTo(Object reference) { + throw new UnsupportedOperationException(); + } + + } +} diff --git a/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/executor/HealthCheckExecutorImplTest.java b/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/executor/HealthCheckExecutorImplTest.java new file mode 100644 index 00000000000..faf76a2fc4e --- /dev/null +++ b/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/executor/HealthCheckExecutorImplTest.java @@ -0,0 +1,180 @@ +/* + * 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 SF 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.hc.core.impl.executor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import java.util.Collection; +import java.util.Date; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.Result.Status; +import org.apache.felix.hc.api.ResultLog.Entry; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckMetadata; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +public class HealthCheckExecutorImplTest { + + @InjectMocks + private HealthCheckExecutorImpl healthCheckExecutorImpl = new HealthCheckExecutorImpl();; + + @Mock + private HealthCheckFuture future; + + @Mock + private HealthCheckMetadata healthCheckMetadata; + + @Spy + private HealthCheckResultCache healthCheckResultCache = new HealthCheckResultCache(); + + @Before + public void setup() { + initMocks(this); + + when(future.getHealthCheckMetadata()).thenReturn(healthCheckMetadata); + when(healthCheckMetadata.getTitle()).thenReturn("Test Check"); + when(healthCheckMetadata.getResultCacheTtlInMs()).thenReturn(null); + + // 2 sec normal timeout + healthCheckExecutorImpl.setTimeoutInMs(2000L); + // 10 sec timeout for critical + healthCheckExecutorImpl.setLongRunningFutureThresholdForRedMs(10000L); + } + + @Test + public void testCollectResultsFromFutures() throws Exception { + + List futures = new LinkedList(); + futures.add(future); + Collection results = new TreeSet(); + + when(future.isDone()).thenReturn(true); + ExecutionResult testResult = new ExecutionResult(healthCheckMetadata, new Result(Result.Status.OK, "test"), 10L); + when(future.get()).thenReturn(testResult); + + healthCheckExecutorImpl.collectResultsFromFutures(futures, results); + + verify(future, times(1)).get(); + + assertEquals(1, results.size()); + assertTrue(results.contains(testResult)); + } + + @Test + public void testCollectResultsFromFuturesTimeout() throws Exception { + + // add an earlier result with status ok (that will be shown as part of the log) + addResultToCache(Status.OK); + + List futures = new LinkedList(); + futures.add(future); + Set results = new TreeSet(); + + when(future.isDone()).thenReturn(false); + // simulating a future that was created 5sec ago + when(future.getCreatedTime()).thenReturn(new Date(new Date().getTime() - 1000 * 5)); + + healthCheckExecutorImpl.collectResultsFromFutures(futures, results); + + verify(future, times(0)).get(); + + assertEquals(1, results.size()); + HealthCheckExecutionResult result = results.iterator().next(); + + assertEquals(Result.Status.WARN, result.getHealthCheckResult().getStatus()); + + // 3 because previous result exists and is part of log + assertEquals(3, getLogEntryCount(result)); + } + + @Test + public void testCollectResultsFromFuturesCriticalTimeout() throws Exception { + + List futures = new LinkedList(); + futures.add(future); + Set results = new TreeSet(); + + when(future.isDone()).thenReturn(false); + + // use an old date now (simulating a future that has run for an hour) + when(future.getCreatedTime()).thenReturn(new Date(new Date().getTime() - 1000 * 60 * 60)); + + healthCheckExecutorImpl.collectResultsFromFutures(futures, results); + assertEquals(1, results.size()); + HealthCheckExecutionResult result = results.iterator().next(); + + verify(future, times(0)).get(); + + assertEquals(Result.Status.CRITICAL, result.getHealthCheckResult().getStatus()); + assertEquals(1, getLogEntryCount(result)); + } + + @Test + public void testCollectResultsFromFuturesWarnTimeoutWithPreviousCritical() throws Exception { + + // an earlier result with critical + addResultToCache(Status.CRITICAL); + + List futures = new LinkedList(); + futures.add(future); + Set results = new TreeSet(); + + when(future.isDone()).thenReturn(false); + // simulating a future that was created 5sec ago + when(future.getCreatedTime()).thenReturn(new Date(new Date().getTime() - 1000 * 5)); + + healthCheckExecutorImpl.collectResultsFromFutures(futures, results); + assertEquals(1, results.size()); + HealthCheckExecutionResult result = results.iterator().next(); + + verify(future, times(0)).get(); + + // expect CRITICAL because previous result (before timeout) was CRITICAL (and not only WARN) + assertEquals(Result.Status.CRITICAL, result.getHealthCheckResult().getStatus()); + assertEquals(3, getLogEntryCount(result)); + } + + private int getLogEntryCount(HealthCheckExecutionResult result) { + int logEntryCount = 0; + final Iterator it = result.getHealthCheckResult().iterator(); + while (it.hasNext()) { + it.next(); + logEntryCount++; + } + return logEntryCount; + } + + private void addResultToCache(Status status) { + healthCheckResultCache.updateWith(new ExecutionResult(healthCheckMetadata, new Result(status, "Status " + status), 1000)); + } +} diff --git a/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/executor/HealthCheckResultCacheTest.java b/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/executor/HealthCheckResultCacheTest.java new file mode 100644 index 00000000000..97e53dc2c36 --- /dev/null +++ b/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/executor/HealthCheckResultCacheTest.java @@ -0,0 +1,204 @@ +/* + * 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 SF 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.hc.core.impl.executor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.MockitoAnnotations.initMocks; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.ResultLog; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckMetadata; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + +public class HealthCheckResultCacheTest { + + private static final int HC_TIMEOUT_NOT_SET = -1; + private static final int DUR_1_MIN = 60 * 1000; + private static final int DUR_2_MIN = 2 * DUR_1_MIN; + private static final int DUR_3_MIN = 3 * DUR_1_MIN; + private static final int DUR_4_MIN = 4 * DUR_1_MIN; + + private HealthCheckResultCache healthCheckResultCache = new HealthCheckResultCache(); + + @Mock + ServiceReference serviceRef; + + @Before + public void setup() { + initMocks(this); + } + + private HealthCheckMetadata setupHealthCheckMetadata(long id, long ttl) { + reset(serviceRef); + doReturn(id).when(serviceRef).getProperty(Constants.SERVICE_ID); + doReturn(ttl).when(serviceRef).getProperty(HealthCheck.RESULT_CACHE_TTL_IN_MS); + doReturn("HC id=" + id).when(serviceRef).getProperty(HealthCheck.NAME); + return new HealthCheckMetadata(serviceRef); + } + + @Test + public void testHealthCheckResultCache() { + + HealthCheckMetadata hc1 = setupHealthCheckMetadata(1, HC_TIMEOUT_NOT_SET); + ExecutionResult executionResult1 = spy(new ExecutionResult(hc1, new Result(Result.Status.OK, "result for hc1"), 1)); + doReturn(new Date(new Date().getTime() - DUR_1_MIN)).when(executionResult1).getFinishedAt(); + healthCheckResultCache.updateWith(executionResult1); + + HealthCheckMetadata hc2 = setupHealthCheckMetadata(2, HC_TIMEOUT_NOT_SET); + ExecutionResult executionResult2 = spy(new ExecutionResult(hc2, new Result(Result.Status.OK, "result for hc2"), 1)); + doReturn(new Date(new Date().getTime() - DUR_3_MIN)).when(executionResult2).getFinishedAt(); + healthCheckResultCache.updateWith(executionResult2); + + HealthCheckMetadata hc3 = setupHealthCheckMetadata(3, DUR_4_MIN); + ExecutionResult executionResult3 = spy(new ExecutionResult(hc3, new Result(Result.Status.OK, "result for hc3"), 1)); + doReturn(new Date(new Date().getTime() - DUR_3_MIN)).when(executionResult3).getFinishedAt(); + healthCheckResultCache.updateWith(executionResult3); + + HealthCheckMetadata hc4 = setupHealthCheckMetadata(4, HC_TIMEOUT_NOT_SET); + // no result for this yet + + List hcList = new ArrayList(Arrays.asList(hc1, hc2, hc3, hc4)); + List results = new ArrayList(); + + healthCheckResultCache.useValidCacheResults(hcList, results, DUR_2_MIN); + + assertTrue(hcList.contains(hc2)); // result too old, left in hcList for later execution + assertTrue(hcList.contains(hc4)); // no result was added to cache via updateWith() + + assertTrue(results.contains(executionResult1)); // true <= result one min old, global timeout 2min + assertFalse(results.contains(executionResult2)); // false <= result three min old, global timeout 2min + assertTrue(results.contains(executionResult3)); // true <= result one three old, HC timeout 4min + + // values not found in cache are left in hcList + assertEquals(2, hcList.size()); + assertEquals(2, results.size()); + + } + + @Test + public void testHealthCheckResultCacheTtl() { + + // -- test cache miss due to HC TTL + HealthCheckMetadata hcWithTtl = setupHealthCheckMetadata(1, DUR_1_MIN); + ExecutionResult executionResult = spy(new ExecutionResult(hcWithTtl, new Result(Result.Status.OK, "result for hc"), 1)); + doReturn(new Date(new Date().getTime() - DUR_2_MIN)).when(executionResult).getFinishedAt(); + healthCheckResultCache.updateWith(executionResult); + + HealthCheckExecutionResult result = healthCheckResultCache.getValidCacheResult(hcWithTtl, DUR_3_MIN); + assertNull(result); // even though global timeout would be ok (2min<3min, the hc timeout of 1min invalidates the result) + + // -- test cache hit due to HC TTL + hcWithTtl = setupHealthCheckMetadata(2, DUR_3_MIN); + executionResult = spy(new ExecutionResult(hcWithTtl, new Result(Result.Status.OK, "result for hc"), 1)); + doReturn(new Date(new Date().getTime() - DUR_2_MIN)).when(executionResult).getFinishedAt(); + healthCheckResultCache.updateWith(executionResult); + + result = healthCheckResultCache.getValidCacheResult(hcWithTtl, DUR_1_MIN); + assertEquals(executionResult, result); // even though global timeout would invalidate this result (1min<2min, the hc timeout of 3min + // allows the result) + + // -- test Long.MAX_VALUE + hcWithTtl = setupHealthCheckMetadata(3, Long.MAX_VALUE); + executionResult = spy(new ExecutionResult(hcWithTtl, new Result(Result.Status.OK, "result for hc"), 1)); + doReturn(new Date(new Date().getTime() - DUR_4_MIN)).when(executionResult).getFinishedAt(); + healthCheckResultCache.updateWith(executionResult); + + result = healthCheckResultCache.getValidCacheResult(hcWithTtl, DUR_1_MIN); + assertEquals(executionResult, result); + + } + + private HealthCheckMetadata setupHealthCheckMetadataWithStickyResults(long id, long nonOkStickyForSec) { + reset(serviceRef); + doReturn(id).when(serviceRef).getProperty(Constants.SERVICE_ID); + doReturn(nonOkStickyForSec).when(serviceRef).getProperty(HealthCheck.KEEP_NON_OK_RESULTS_STICKY_FOR_SEC); + doReturn("HC id=" + id).when(serviceRef).getProperty(HealthCheck.NAME); + return new HealthCheckMetadata(serviceRef); + } + + @Test + public void testCreateExecutionResultWithStickyResults() { + + HealthCheckMetadata hcWithStickyResultsSet = setupHealthCheckMetadataWithStickyResults(1, 120 /* 2 minutes */); + ExecutionResult currentResult = spy(new ExecutionResult(hcWithStickyResultsSet, new Result(Result.Status.OK, "result for hc"), 1)); + HealthCheckExecutionResult overallResultWithStickyResults = healthCheckResultCache + .createExecutionResultWithStickyResults(currentResult); + assertTrue("Exact same result is expected if no history exists", currentResult == overallResultWithStickyResults); + + // add 4 minutes old WARN to cache + ExecutionResult oldWarnResult = spy( + new ExecutionResult(hcWithStickyResultsSet, new Result(Result.Status.WARN, "result for hc"), 1)); + doReturn(new Date(System.currentTimeMillis() - DUR_4_MIN)).when(oldWarnResult).getFinishedAt(); + healthCheckResultCache.updateWith(oldWarnResult); + + // check that it is not used + currentResult = new ExecutionResult(hcWithStickyResultsSet, new Result(Result.Status.OK, "result for hc"), 1); + overallResultWithStickyResults = healthCheckResultCache.createExecutionResultWithStickyResults(currentResult); + assertTrue("Exact same result is expected if WARN HC Result is too old", currentResult == overallResultWithStickyResults); + + // change WARN to 1 minute age + doReturn(new Date(System.currentTimeMillis() - DUR_1_MIN)).when(oldWarnResult).getFinishedAt(); + overallResultWithStickyResults = healthCheckResultCache.createExecutionResultWithStickyResults(currentResult); + assertTrue("Expect newly created result as sticky result should be taken into account", + currentResult != overallResultWithStickyResults); + assertEquals("Expect status to be taken over from old, sticky WARN", Result.Status.WARN, + overallResultWithStickyResults.getHealthCheckResult().getStatus()); + assertEquals("Expect 4 entries, two each for current and WARN", 4, getLogMsgCount(overallResultWithStickyResults)); + + // add 1 minutes old CRITICAL to cache + ExecutionResult oldCriticalResult = spy( + new ExecutionResult(hcWithStickyResultsSet, new Result(Result.Status.CRITICAL, "result for hc"), 1)); + doReturn(new Date(System.currentTimeMillis() - DUR_1_MIN)).when(oldCriticalResult).getFinishedAt(); + healthCheckResultCache.updateWith(oldCriticalResult); + + overallResultWithStickyResults = healthCheckResultCache.createExecutionResultWithStickyResults(currentResult); + assertTrue("Expect newly created result as sticky result should be taken into account", + currentResult != overallResultWithStickyResults); + assertEquals("Expect status to be taken over from old, sticky CRITICAL", Result.Status.CRITICAL, + overallResultWithStickyResults.getHealthCheckResult().getStatus()); + assertEquals("Expect six entries, two each for current, WARN and CRITICAL result", 6, + getLogMsgCount(overallResultWithStickyResults)); + + } + + private int getLogMsgCount(HealthCheckExecutionResult result) { + int count = 0; + for (ResultLog.Entry entry : result.getHealthCheckResult()) { + count++; + } + return count; + } + +} diff --git a/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServletTest.java b/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServletTest.java new file mode 100644 index 00000000000..5e5aa169412 --- /dev/null +++ b/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServletTest.java @@ -0,0 +1,318 @@ +/* + * 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 SF 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.hc.core.impl.servlet; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.contains; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.MockitoAnnotations.initMocks; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.Result.Status; +import org.apache.felix.hc.api.execution.HealthCheckExecutionOptions; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckExecutor; +import org.apache.felix.hc.api.execution.HealthCheckMetadata; +import org.apache.felix.hc.api.execution.HealthCheckSelector; +import org.apache.felix.hc.core.impl.executor.ExecutionResult; +import org.hamcrest.Description; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + +public class HealthCheckExecutorServletTest { + + @InjectMocks + private HealthCheckExecutorServlet healthCheckExecutorServlet = new HealthCheckExecutorServlet(); + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private HealthCheckExecutor healthCheckExecutor; + + @Mock + private ResultHtmlSerializer htmlSerializer; + + @Mock + private ResultJsonSerializer jsonSerializer; + + @Mock + private ResultTxtSerializer txtSerializer; + + @Mock + private ResultTxtVerboseSerializer verboseTxtSerializer; + + @Mock + private ServiceReference hcServiceRef; + + @Mock + private PrintWriter writer; + + @Mock + private HealthCheckExecutorServletConfiguration healthCheckExecutorServletConfig; + + @Before + public void setup() throws IOException { + initMocks(this); + + doReturn(500L).when(hcServiceRef).getProperty(Constants.SERVICE_ID); + doReturn(writer).when(response).getWriter(); + + doReturn(true).when(healthCheckExecutorServletConfig).disabled(); + doReturn("OK:200").when(healthCheckExecutorServletConfig).httpStatusMapping(); + doReturn(new String[0]).when(healthCheckExecutorServletConfig).tags(); + doReturn(HealthCheckExecutorServlet.FORMAT_HTML).when(healthCheckExecutorServletConfig).format(); + healthCheckExecutorServlet.activate(healthCheckExecutorServletConfig); + } + + @Test + public void testDoGetHtml() throws ServletException, IOException { + + final String testTag = "testTag"; + doReturn(testTag).when(request).getParameter(HealthCheckExecutorServlet.PARAM_TAGS.name); + doReturn("false").when(request).getParameter(HealthCheckExecutorServlet.PARAM_COMBINE_TAGS_WITH_OR.name); + final List executionResults = getExecutionResults(Result.Status.CRITICAL); + doReturn(executionResults).when(healthCheckExecutor).execute(selector(new String[] { testTag }, new String[0]), + eq(new HealthCheckExecutionOptions())); + + healthCheckExecutorServlet.doGet(request, response); + + verifyZeroInteractions(jsonSerializer); + verifyZeroInteractions(txtSerializer); + verifyZeroInteractions(verboseTxtSerializer); + verify(htmlSerializer) + .serialize(resultEquals(new Result(Result.Status.CRITICAL, "Overall Status CRITICAL")), eq(executionResults), + contains("Supported URL parameters"), eq(false)); + } + + @Test + public void testDoGetNameAndTagInPath() throws ServletException, IOException { + + final String testTag = "testTag"; + final String testName = "test name"; + + doReturn(testTag + "," + testName).when(request).getPathInfo(); + doReturn("false").when(request).getParameter(HealthCheckExecutorServlet.PARAM_COMBINE_TAGS_WITH_OR.name); + final List executionResults = getExecutionResults(Result.Status.CRITICAL); + doReturn(executionResults).when(healthCheckExecutor).execute(selector(new String[] { testTag }, new String[] { testName }), + eq(new HealthCheckExecutionOptions())); + + healthCheckExecutorServlet.doGet(request, response); + + verify(request, never()).getParameter(HealthCheckExecutorServlet.PARAM_TAGS.name); + verify(request, never()).getParameter(HealthCheckExecutorServlet.PARAM_NAMES.name); + verifyZeroInteractions(jsonSerializer); + verifyZeroInteractions(txtSerializer); + verifyZeroInteractions(verboseTxtSerializer); + verify(htmlSerializer) + .serialize(resultEquals(new Result(Result.Status.CRITICAL, "Overall Status CRITICAL")), eq(executionResults), + contains("Supported URL parameters"), eq(false)); + } + + @Test + public void testDoGetJson() throws ServletException, IOException { + + final String testTag = "testTag"; + doReturn("true").when(request).getParameter(HealthCheckExecutorServlet.PARAM_COMBINE_TAGS_WITH_OR.name); + int timeout = 5000; + doReturn(timeout + "").when(request).getParameter(HealthCheckExecutorServlet.PARAM_OVERRIDE_GLOBAL_TIMEOUT.name); + doReturn("/" + testTag + ".json").when(request).getPathInfo(); + final List executionResults = getExecutionResults(Result.Status.WARN); + HealthCheckExecutionOptions options = new HealthCheckExecutionOptions(); + options.setCombineTagsWithOr(true); + options.setOverrideGlobalTimeout(timeout); + doReturn(executionResults).when(healthCheckExecutor).execute(selector(new String[] { testTag }, new String[0]), eq(options)); + + healthCheckExecutorServlet.doGet(request, response); + + verifyZeroInteractions(htmlSerializer); + verifyZeroInteractions(txtSerializer); + verifyZeroInteractions(verboseTxtSerializer); + verify(jsonSerializer).serialize(resultEquals(new Result(Result.Status.WARN, "Overall Status WARN")), eq(executionResults), + anyString(), + eq(false)); + + } + + @Test + public void testDoGetTxt() throws ServletException, IOException { + + final String testTag = "testTag"; + doReturn(testTag).when(request).getParameter(HealthCheckExecutorServlet.PARAM_TAGS.name); + doReturn(HealthCheckExecutorServlet.FORMAT_TXT).when(request).getParameter(HealthCheckExecutorServlet.PARAM_FORMAT.name); + doReturn("true").when(request).getParameter(HealthCheckExecutorServlet.PARAM_COMBINE_TAGS_WITH_OR.name); + int timeout = 5000; + doReturn(timeout + "").when(request).getParameter(HealthCheckExecutorServlet.PARAM_OVERRIDE_GLOBAL_TIMEOUT.name); + final List executionResults = getExecutionResults(Result.Status.WARN); + HealthCheckExecutionOptions options = new HealthCheckExecutionOptions(); + options.setCombineTagsWithOr(true); + options.setOverrideGlobalTimeout(timeout); + + doReturn(executionResults).when(healthCheckExecutor).execute(selector(new String[] { testTag }, new String[0]), eq(options)); + + healthCheckExecutorServlet.doGet(request, response); + + verifyZeroInteractions(htmlSerializer); + verifyZeroInteractions(jsonSerializer); + verifyZeroInteractions(verboseTxtSerializer); + verify(txtSerializer).serialize(resultEquals(new Result(Result.Status.WARN, "Overall Status WARN"))); + + } + + @Test + public void testDoGetVerboseTxt() throws ServletException, IOException { + + String testTag = "testTag"; + doReturn(testTag).when(request).getParameter(HealthCheckExecutorServlet.PARAM_TAGS.name); + doReturn(HealthCheckExecutorServlet.FORMAT_VERBOSE_TXT).when(request).getParameter(HealthCheckExecutorServlet.PARAM_FORMAT.name); + + List executionResults = getExecutionResults(Result.Status.WARN); + doReturn(executionResults).when(healthCheckExecutor).execute(selector(new String[] { testTag }, new String[0]), + any(HealthCheckExecutionOptions.class)); + + healthCheckExecutorServlet.doGet(request, response); + + verifyZeroInteractions(htmlSerializer); + verifyZeroInteractions(jsonSerializer); + verifyZeroInteractions(txtSerializer); + verify(verboseTxtSerializer).serialize(resultEquals(new Result(Result.Status.WARN, "Overall Status WARN")), eq(executionResults), + eq(false)); + + } + + private List getExecutionResults(Result.Status worstStatus) { + List results = new ArrayList(); + results.add(new ExecutionResult(new HealthCheckMetadata(hcServiceRef), new Result(worstStatus, worstStatus.name()), 100)); + results.add(new ExecutionResult(new HealthCheckMetadata(hcServiceRef), new Result(Result.Status.OK, "OK"), 100)); + return results; + } + + @Test + public void testGetStatusMapping() throws ServletException { + + Map statusMapping = healthCheckExecutorServlet.getStatusMapping("CRITICAL:500"); + assertEquals(statusMapping.get(Result.Status.OK), (Integer) 200); + assertEquals(statusMapping.get(Result.Status.WARN), (Integer) 200); + assertEquals(statusMapping.get(Result.Status.TEMPORARILY_UNAVAILABLE), (Integer) 503); + assertEquals(statusMapping.get(Result.Status.CRITICAL), (Integer) 500); + assertEquals(statusMapping.get(Result.Status.HEALTH_CHECK_ERROR), (Integer) 500); + + statusMapping = healthCheckExecutorServlet.getStatusMapping("OK:333"); + assertEquals(statusMapping.get(Result.Status.OK), (Integer) 333); + assertEquals(statusMapping.get(Result.Status.WARN), (Integer) 333); + assertEquals(statusMapping.get(Result.Status.TEMPORARILY_UNAVAILABLE), (Integer) 503); + assertEquals(statusMapping.get(Result.Status.CRITICAL), (Integer) 503); + assertEquals(statusMapping.get(Result.Status.HEALTH_CHECK_ERROR), (Integer) 500); + + statusMapping = healthCheckExecutorServlet.getStatusMapping("OK:200,WARN:418,CRITICAL:503,TEMPORARILY_UNAVAILABLE:503,HEALTH_CHECK_ERROR:500"); + assertEquals(statusMapping.get(Result.Status.OK), (Integer) 200); + assertEquals(statusMapping.get(Result.Status.WARN), (Integer) 418); + assertEquals(statusMapping.get(Result.Status.TEMPORARILY_UNAVAILABLE), (Integer) 503); + assertEquals(statusMapping.get(Result.Status.CRITICAL), (Integer) 503); + assertEquals(statusMapping.get(Result.Status.HEALTH_CHECK_ERROR), (Integer) 500); + + statusMapping = healthCheckExecutorServlet.getStatusMapping("WARN:418,HEALTH_CHECK_ERROR:503"); + assertEquals(statusMapping.get(Result.Status.OK), (Integer) 200); + assertEquals(statusMapping.get(Result.Status.WARN), (Integer) 418); + assertEquals(statusMapping.get(Result.Status.TEMPORARILY_UNAVAILABLE), (Integer) 503); + assertEquals(statusMapping.get(Result.Status.CRITICAL), (Integer) 503); + assertEquals(statusMapping.get(Result.Status.HEALTH_CHECK_ERROR), (Integer) 503); + + } + + @Test(expected = IllegalArgumentException.class) + public void testGetStatusMappingInvalidToken() throws ServletException { + healthCheckExecutorServlet.getStatusMapping("CRITICAL"); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetStatusMappingInvalidStatus() throws ServletException { + healthCheckExecutorServlet.getStatusMapping("INVALID:200"); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetStatusMappingInvalidStatusCode() throws ServletException { + healthCheckExecutorServlet.getStatusMapping("CRITICAL:xxx"); + } + + static Result resultEquals(Result expected) { + return argThat(new ResultMatcher(expected)); + } + + static class ResultMatcher extends ArgumentMatcher { + + private final Result expectedResult; + + public ResultMatcher(Result expected) { + this.expectedResult = expected; + } + + @Override + public boolean matches(Object actual) { + Result actualResult = (Result) actual; + return actualResult.getStatus().equals(expectedResult.getStatus()); // simple status matching only sufficient for this test case + } + + @Override + public void describeTo(Description description) { + description.appendText(expectedResult == null ? null : expectedResult.toString()); + } + } + + HealthCheckSelector selector(final String[] tags, final String[] names) { + return argThat(new ArgumentMatcher() { + @Override + public boolean matches(Object actual) { + if (actual instanceof HealthCheckSelector) { + HealthCheckSelector actualSelector = (HealthCheckSelector) actual; + return Arrays.equals(actualSelector.tags(), tags.length == 0 ? new String[] { "" } : tags) && + Arrays.equals(actualSelector.names(), names.length == 0 ? new String[] { "" } : names); + } else { + return false; + } + } + }); + } + +} diff --git a/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/servlet/ResultJsonSerializerTest.java b/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/servlet/ResultJsonSerializerTest.java new file mode 100644 index 00000000000..9d5c3e8e1d4 --- /dev/null +++ b/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/servlet/ResultJsonSerializerTest.java @@ -0,0 +1,72 @@ +/* + * 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 SF 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.hc.core.impl.servlet; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import java.util.Arrays; + +import org.apache.felix.hc.api.FormattingResultLog; +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckMetadata; +import org.apache.felix.hc.core.impl.executor.ExecutionResult; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + +public class ResultJsonSerializerTest { + + @Mock + private ServiceReference serviceReference; + + ResultJsonSerializer resultJsonSerializer = new ResultJsonSerializer(); + + @Before + public void setup() { + initMocks(this); + + when(serviceReference.getProperty(HealthCheck.NAME)).thenReturn("Test"); + when(serviceReference.getProperty(HealthCheck.TAGS)).thenReturn(new String[] { "tag1", "tag2" }); + when(serviceReference.getProperty(Constants.SERVICE_ID)).thenReturn(1L); + } + + @Test + public void testJsonSerialisation() { + + FormattingResultLog log = new FormattingResultLog(); + log.info("test message"); + Result result = new Result(log); + HealthCheckMetadata hcMetadata = new HealthCheckMetadata(serviceReference); + HealthCheckExecutionResult executionResult = new ExecutionResult(hcMetadata, result, 100); + Result overallResult = new Result(Result.Status.OK, "Overall status"); + + String json = resultJsonSerializer.serialize(overallResult, Arrays.asList(executionResult), null, false); + assertThat(json, containsString("\"overallResult\":\"OK\"")); + assertThat(json, containsString("\"tags\":[\"tag1\",\"tag2\"]")); + assertThat(json, containsString("\"messages\":[{\"status\":\"OK\",\"message\":\"test message\"}]")); + } + +} diff --git a/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/util/HealthCheckFilterTest.java b/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/util/HealthCheckFilterTest.java new file mode 100644 index 00000000000..c53176f5237 --- /dev/null +++ b/healthcheck/core/src/test/java/org/apache/felix/hc/core/impl/util/HealthCheckFilterTest.java @@ -0,0 +1,109 @@ +/* + * 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.hc.core.impl.util; + +import static org.apache.felix.hc.api.execution.HealthCheckSelector.empty; +import static org.apache.felix.hc.api.execution.HealthCheckSelector.names; +import static org.apache.felix.hc.api.execution.HealthCheckSelector.tags; +import static org.junit.Assert.assertEquals; + +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.execution.HealthCheckSelector; +import org.apache.felix.hc.core.impl.util.HealthCheckFilter; +import org.junit.Test; + +public class HealthCheckFilterTest { + + private static final String HC_OBJ_CLASS_QUERY = "(|(objectClass=" + HealthCheck.class.getName() + + ")(objectClass=org.apache.sling.hc.api.HealthCheck))"; + + private HealthCheckFilter filter = new HealthCheckFilter(null); + + private static void assertStrEquals(String expected, CharSequence actual) { + assertEquals(expected, actual.toString()); + } + + @Test + public void testEmptyOptions() { + assertStrEquals("(&" + HC_OBJ_CLASS_QUERY + ")", filter.getServiceFilter(empty(), false)); + } + + @Test + public void testWithOneTag() { + HealthCheckSelector selector = tags("foo"); + assertStrEquals("(&" + HC_OBJ_CLASS_QUERY + "(hc.tags=foo))", filter.getServiceFilter(selector, false)); + } + + @Test + public void testWithTwoTags() { + HealthCheckSelector selector = tags("foo", "bar"); + assertStrEquals("(&" + HC_OBJ_CLASS_QUERY + "(hc.tags=foo)(hc.tags=bar))", filter.getServiceFilter(selector, false)); + } + + @Test + public void testWithTwoTagsOr() { + HealthCheckSelector selector = tags("foo", "bar"); + assertStrEquals("(&" + HC_OBJ_CLASS_QUERY + "(|(hc.tags=foo)(hc.tags=bar)))", filter.getServiceFilter(selector, true)); + } + + @Test + public void testWithTwoTagsExcludeOne() { + HealthCheckSelector selector = tags("foo", "bar").withTags("-baz"); + assertStrEquals("(&" + HC_OBJ_CLASS_QUERY + "(!(hc.tags=baz))(hc.tags=foo)(hc.tags=bar))", + filter.getServiceFilter(selector, false)); + } + + @Test + public void testWithOneName() { + HealthCheckSelector selector = names("foo"); + assertStrEquals("(&" + HC_OBJ_CLASS_QUERY + "(|(hc.name=foo)))", filter.getServiceFilter(selector, false)); + } + + @Test + public void testWithTwoNames() { + HealthCheckSelector selector = names("foo").withNames("bar"); + assertStrEquals("(&" + HC_OBJ_CLASS_QUERY + "(|(hc.name=foo)(hc.name=bar)))", filter.getServiceFilter(selector, false)); + } + + @Test + public void testWithTwoNamesExcludingOne() { + HealthCheckSelector selector = names("foo", "bar", "-baz"); + assertStrEquals("(&" + HC_OBJ_CLASS_QUERY + "(!(hc.name=baz))(|(hc.name=foo)(hc.name=bar)))", + filter.getServiceFilter(selector, false)); + } + + @Test + public void testWithTagAndName() { + HealthCheckSelector selector = empty().withTags("t1").withNames("foo"); + assertStrEquals("(&" + HC_OBJ_CLASS_QUERY + "(|(hc.name=foo)(hc.tags=t1)))", filter.getServiceFilter(selector, false)); + } + + @Test + public void testWithTwoOrTagsAndTwoNames() { + HealthCheckSelector selector = empty().withNames("foo", "bar").withTags("t1", "t2"); + assertStrEquals("(&" + HC_OBJ_CLASS_QUERY + "(|(hc.tags=t1)(hc.tags=t2)(hc.name=foo)(hc.name=bar)))", + filter.getServiceFilter(selector, true)); + } + + @Test + public void testWithTwoAndTagsAndTwoNames() { + HealthCheckSelector selector = empty().withNames("foo", "bar").withTags("t1", "t2"); + assertStrEquals("(&" + HC_OBJ_CLASS_QUERY + "(|(hc.name=foo)(hc.name=bar)(&(hc.tags=t1)(hc.tags=t2))))", + filter.getServiceFilter(selector, false)); + } + +} diff --git a/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/AsyncHealthCheckIT.java b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/AsyncHealthCheckIT.java new file mode 100644 index 00000000000..88b542332ca --- /dev/null +++ b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/AsyncHealthCheckIT.java @@ -0,0 +1,216 @@ +/* + * 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 SF 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.hc.core.it; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.inject.Inject; + +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.execution.HealthCheckExecutor; +import org.apache.felix.hc.api.execution.HealthCheckSelector; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.Configuration; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.junit.PaxExam; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; + +@RunWith(PaxExam.class) +public class AsyncHealthCheckIT { + + @Inject + private HealthCheckExecutor executor; + + @Inject + private BundleContext bundleContext; + + @Configuration + public Option[] config() { + return U.config(); + } + + final AtomicInteger counter = new AtomicInteger(Integer.MIN_VALUE); + + final static int MAX_VALUE = 12345678; + + class TestHC implements HealthCheck { + @Override + public Result execute() { + final int v = counter.incrementAndGet(); + return new Result(v > MAX_VALUE ? Result.Status.WARN : Result.Status.OK, "counter is now " + v); + } + } + + private ServiceRegistration registerAsyncHc(HealthCheck hc, String id, Object async, int stickySec) { + final Dictionary props = new Hashtable(); + props.put(HealthCheck.NAME, "async_HC_" + id); + props.put(HealthCheck.TAGS, id); + if (async instanceof String) { + props.put(HealthCheck.ASYNC_CRON_EXPRESSION, async); + } else if (async instanceof Long) { + props.put(HealthCheck.ASYNC_INTERVAL_IN_SEC, async); + } + + if (stickySec > 0) { + props.put(HealthCheck.KEEP_NON_OK_RESULTS_STICKY_FOR_SEC, stickySec); + } + + final ServiceRegistration result = bundleContext.registerService(HealthCheck.class.getName(), hc, props); + + // Wait for HC to be registered + U.expectHealthChecks(1, executor, id); + + return result; + } + + private void assertStatus(String id, Result.Status expected, long maxMsec, String msg) throws InterruptedException { + final long timeout = System.currentTimeMillis() + 5000L; + while (System.currentTimeMillis() < timeout) { + final Result.Status actual = executor.execute(HealthCheckSelector.tags(id)).get(0).getHealthCheckResult().getStatus(); + if (actual == expected) { + return; + } + Thread.sleep(100L); + } + fail("Did not get status " + expected + " after " + maxMsec + " msec " + msg); + } + + @Test + public void testAsyncHealthCheckExecution() throws InterruptedException { + + final String id = UUID.randomUUID().toString(); + final HealthCheck hc = new TestHC(); + final ServiceRegistration reg = registerAsyncHc(hc, id, "*/1 * * * * ?", 0); + final long maxMsec = 5000L; + + try { + // Reset the counter and check that HC increments it without explicitly calling the executor + { + counter.set(0); + final long timeout = System.currentTimeMillis() + maxMsec; + while (System.currentTimeMillis() < timeout) { + int currentVal = counter.get(); + if (currentVal > 0) { + break; + } + Thread.sleep(100L); + } + assertTrue("Expecting counter to be incremented", counter.get() > 0); + } + + // Verify that we get the right log + final String msg = executor.execute(HealthCheckSelector.tags(id)).get(0).getHealthCheckResult().iterator().next().getMessage(); + assertTrue("Expecting the right message: " + msg, msg.contains("counter is now")); + + // And verify that calling executor lots of times doesn't increment as much + final int previous = counter.get(); + final int n = 100; + for (int i = 0; i < n; i++) { + executor.execute(HealthCheckSelector.tags(id)); + } + assertTrue("Expecting counter to increment asynchronously", counter.get() < previous + n); + + // Verify that results are not sticky + assertStatus(id, Result.Status.OK, maxMsec, "before WARN"); + counter.set(MAX_VALUE + 1); + assertStatus(id, Result.Status.WARN, maxMsec, "right after WARN"); + counter.set(0); + assertStatus(id, Result.Status.OK, maxMsec, "after resetting counter"); + + } finally { + reg.unregister(); + } + + } + + @Test + public void testAsyncHealthCheckExecutionWithInterval() throws InterruptedException { + + final String id = UUID.randomUUID().toString(); + final HealthCheck hc = new TestHC(); + final ServiceRegistration reg = registerAsyncHc(hc, id, new Long(2), 0); + final long maxMsec = 5000L; + + try { + // Reset the counter and check that HC increments it without explicitly calling the executor + { + counter.set(0); + final long timeout = System.currentTimeMillis() + maxMsec; + while (System.currentTimeMillis() < timeout) { + int currentVal = counter.get(); + if (currentVal > 0) { + break; + } + Thread.sleep(100L); + } + assertTrue("Expecting counter to be incremented", counter.get() > 0); + } + + // Verify that we get the right log + final String msg = executor.execute(HealthCheckSelector.tags(id)).get(0).getHealthCheckResult().iterator().next().getMessage(); + assertTrue("Expecting the right message: " + msg, msg.contains("counter is now")); + + } finally { + reg.unregister(); + } + + } + + @Test + public void testAsyncHealthCheckWithStickyResults() throws InterruptedException { + final String id = UUID.randomUUID().toString(); + final HealthCheck hc = new TestHC(); + final long maxMsec = 5000L; + final int stickySeconds = 60; + final ServiceRegistration reg = registerAsyncHc(hc, id, "*/1 * * * * ?", stickySeconds); + + try { + assertStatus(id, Result.Status.OK, maxMsec, "before WARN"); + counter.set(MAX_VALUE + 1); + assertStatus(id, Result.Status.WARN, maxMsec, "right after WARN"); + counter.set(0); + + // Counter should be incremented after a while, and in range, but with sticky WARN result + final long timeout = System.currentTimeMillis() + maxMsec; + boolean ok = false; + while (System.currentTimeMillis() < timeout) { + if (counter.get() > 0 && counter.get() < MAX_VALUE) { + ok = true; + break; + } + Thread.sleep(100L); + } + + assertTrue("expecting counter to be incremented", ok); + assertStatus(id, Result.Status.WARN, maxMsec, "after resetting counter, expecting sticky result"); + + } finally { + reg.unregister(); + } + } + +} diff --git a/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/ExtendedHealthCheckExecutorIT.java b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/ExtendedHealthCheckExecutorIT.java new file mode 100644 index 00000000000..3f0ae9f5e4e --- /dev/null +++ b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/ExtendedHealthCheckExecutorIT.java @@ -0,0 +1,111 @@ +/* + * 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 SF 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.hc.core.it; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.UUID; + +import javax.inject.Inject; + +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckExecutor; +import org.apache.felix.hc.api.execution.HealthCheckSelector; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.Configuration; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.junit.PaxExam; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; + +/** Additional executor tests */ +@RunWith(PaxExam.class) +public class ExtendedHealthCheckExecutorIT { + + @Inject + private HealthCheckExecutor executor; + + @Inject + private BundleContext bundleContext; + + @SuppressWarnings("rawtypes") + private List regs = new ArrayList(); + + private String testTag; + private final Result.Status testResult = Result.Status.OK; + + @Configuration + public Option[] config() { + return U.config(); + } + + private void registerHC(final String... tags) { + final HealthCheck hc = new HealthCheck() { + @Override + public Result execute() { + return new Result(testResult, "Returning " + testResult + " for " + tags[0]); + } + + }; + + final Dictionary props = new Hashtable(); + props.put(HealthCheck.NAME, "name_" + tags[0]); + props.put(HealthCheck.TAGS, tags); + + regs.add(bundleContext.registerService(HealthCheck.class.getName(), hc, props)); + } + + @Before + public void setup() { + testTag = "TEST_" + UUID.randomUUID().toString(); + registerHC(testTag); + U.expectHealthChecks(1, executor, testTag); + } + + @After + public void cleanup() { + for (ServiceRegistration reg : regs) { + reg.unregister(); + } + } + + @Test + public void testSingleExecution() throws Exception { + ServiceReference[] refs = U.callSelectHealthCheckReferences(executor, HealthCheckSelector.tags(testTag)); + assertNotNull(refs); + assertEquals(1, refs.length); + + // The ExtendedHealthCheckExecutor interface is not public, so we cheat + // to be able to test its implementation + final Method m = executor.getClass().getMethod("execute", ServiceReference.class); + final HealthCheckExecutionResult result = (HealthCheckExecutionResult) m.invoke(executor, refs[0]); + assertEquals(testResult, result.getHealthCheckResult().getStatus()); + } +} \ No newline at end of file diff --git a/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/HealthCheckExecutorSelectionIT.java b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/HealthCheckExecutorSelectionIT.java new file mode 100644 index 00000000000..98c9783c84c --- /dev/null +++ b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/HealthCheckExecutorSelectionIT.java @@ -0,0 +1,147 @@ +/* + * 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 SF 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.hc.core.it; + +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.UUID; + +import javax.inject.Inject; + +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.execution.HealthCheckExecutionOptions; +import org.apache.felix.hc.api.execution.HealthCheckExecutor; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.Configuration; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.junit.PaxExam; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; + +/** Test the HealthCheckExecutor selection mechanism */ +@RunWith(PaxExam.class) +public class HealthCheckExecutorSelectionIT { + + @Inject + private HealthCheckExecutor executor; + + @Inject + private BundleContext bundleContext; + + private static String idA; + private static String idB; + private HealthCheckExecutionOptions options; + + @SuppressWarnings("rawtypes") + private List regs = new ArrayList(); + + @Configuration + public Option[] config() { + return U.config(); + } + + private void registerHC(final String... tags) { + final HealthCheck hc = new HealthCheck() { + @Override + public Result execute() { + return new Result(Result.Status.OK, "All good for " + tags[0]); + } + + }; + + final Dictionary props = new Hashtable(); + props.put(HealthCheck.NAME, "name_" + tags[0]); + props.put(HealthCheck.TAGS, tags); + + regs.add(bundleContext.registerService(HealthCheck.class.getName(), hc, props)); + } + + @BeforeClass + public static void setId() { + idA = UUID.randomUUID().toString(); + idB = UUID.randomUUID().toString(); + } + + @Before + public void setup() { + options = new HealthCheckExecutionOptions(); + + U.expectHealthChecks(0, executor, idA); + U.expectHealthChecks(0, executor, idB); + + registerHC(idA); + registerHC(idB); + registerHC(idB); + registerHC(idA, idB); + } + + @After + @SuppressWarnings("rawtypes") + public void cleanup() { + for (ServiceRegistration r : regs) { + r.unregister(); + } + regs.clear(); + + U.expectHealthChecks(0, executor, idA); + U.expectHealthChecks(0, executor, idB); + } + + @Test + public void testDefaultSelectionA() { + U.expectHealthChecks(2, executor, idA); + U.expectHealthChecks(2, executor, options, idA); + } + + @Test + public void testDefaultSelectionB() { + U.expectHealthChecks(3, executor, idB); + U.expectHealthChecks(3, executor, options, idB); + } + + @Test + public void testDefaultSelectionAB() { + U.expectHealthChecks(1, executor, idA, idB); + U.expectHealthChecks(1, executor, options, idA, idB); + } + + @Test + public void testOrSelectionA() { + options.setCombineTagsWithOr(true); + U.expectHealthChecks(1, executor, options, idA); + } + + @Test + public void testOrSelectionB() { + options.setCombineTagsWithOr(true); + U.expectHealthChecks(3, executor, options, idB); + } + + @Test + public void testOrSelectionAB() { + options.setCombineTagsWithOr(true); + U.expectHealthChecks(4, executor, options, idA, idB); + } +} diff --git a/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/HealthCheckFilterIT.java b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/HealthCheckFilterIT.java new file mode 100644 index 00000000000..af5cf20367b --- /dev/null +++ b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/HealthCheckFilterIT.java @@ -0,0 +1,284 @@ +/* + * 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 SF 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.hc.core.it; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; + +import javax.inject.Inject; + +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.execution.HealthCheckExecutor; +import org.apache.felix.hc.api.execution.HealthCheckSelector; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.Configuration; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.junit.PaxExam; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@RunWith(PaxExam.class) +public class HealthCheckFilterIT { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + @Inject + private HealthCheckExecutor executor; + + @Inject + private BundleContext bundleContext; + + private List testServices = new ArrayList(); + private static int instanceCounter = 0; + + class TestHealthCheckBuilder { + + String[] tags; + String name; + + TestHealthCheckBuilder withTags(String... tags) { + this.tags = tags; + return this; + } + + TestHealthCheckBuilder withName(String name) { + this.name = name; + return this; + } + + TestHealthCheck build() { + final Dictionary props = new Hashtable(); + if (tags != null) { + props.put(HealthCheck.TAGS, tags); + } + if (name != null) { + props.put(HealthCheck.NAME, name); + } + + return new TestHealthCheck(props); + + } + } + + class TestHealthCheck implements HealthCheck { + + private final int id; + private final ServiceRegistration reg; + private final ServiceReference serviceRef; + + TestHealthCheck(Dictionary props) { + id = instanceCounter++; + reg = bundleContext.registerService(HealthCheck.class, this, props); + serviceRef = reg.getReference(); + log.info("Registered {} with name {} and tags {}", + new Object[] { this, props.get(HealthCheck.NAME), Arrays.toString((String[]) props.get(HealthCheck.TAGS)) }); + + } + + @Override + public String toString() { + return "TestHealthCheck#" + id; + } + + @Override + public boolean equals(Object other) { + return other instanceof TestHealthCheck + && ((TestHealthCheck) other).id == id; + } + + @Override + public int hashCode() { + return id; + } + + @Override + public Result execute() { + return null; + } + + void unregister() { + reg.unregister(); + } + } + + private TestHealthCheckBuilder builder() { + return new TestHealthCheckBuilder(); + } + + @Configuration + public Option[] config() { + return U.config(); + } + + + @Before + public void setup() { + testServices.add(builder().withTags("foo").withName("test1").build()); + testServices.add(builder().withTags("bar").withName("test2").build()); + testServices.add(builder().withTags("foo", "bar").withName("test3").build()); + testServices.add(builder().withTags("other", "thing").withName("test4").build()); + testServices.add(builder().withName("test5").build()); + + } + + @After + public void cleanup() { + for (TestHealthCheck tc : testServices) { + tc.unregister(); + } + } + + + private ServiceReference[] callSelectHealthCheckReferences(HealthCheckSelector selector) { + return U.callSelectHealthCheckReferences(executor, selector); + } + + + /** @param included true or false, in the same order as testServices */ + private void assertServices(ServiceReference[] serviceRefs, boolean... included) { + List> serviceRefList = Arrays.asList(serviceRefs); + final Iterator it = testServices.iterator(); + for (boolean inc : included) { + final TestHealthCheck thc = it.next(); + if (inc) { + assertTrue("Expecting list of services to include " + thc, + serviceRefList.contains(thc.serviceRef)); + } else { + assertFalse("Not expecting list of services to include " + thc, + serviceRefList.contains(thc.serviceRef)); + } + } + } + + @Test + public void testSelectorService() throws ClassNotFoundException, IOException, URISyntaxException { + assertNotNull("Expecting HealthCheckSelector service to be provided", executor); + } + + + @Test + public void testAllServices() { + ServiceReference[] s = callSelectHealthCheckReferences(null); + assertServices(s, true, true, true, true, true); + } + + @Test + public void testEmptyTags() { + ServiceReference[] s = callSelectHealthCheckReferences(HealthCheckSelector.tags("", "", "")); + assertServices(s, true, true, true, true, true); + } + + @Test + public void testFooTag() { + ServiceReference[] s = callSelectHealthCheckReferences(HealthCheckSelector.tags("foo")); + assertServices(s, true, false, true, false, false); + } + + @Test + public void testBarTag() { + ServiceReference[] s = callSelectHealthCheckReferences(HealthCheckSelector.tags("bar")); + assertServices(s, false, true, true, false, false); + } + + @Test + public void testFooAndBar() { + ServiceReference[] s = callSelectHealthCheckReferences(HealthCheckSelector.tags("foo", "bar")); + assertServices(s, false, false, true, false, false); + } + + @Test + public void testFooMinusBar() { + ServiceReference[] s = callSelectHealthCheckReferences(HealthCheckSelector.tags("foo", "-bar")); + assertServices(s, true, false, false, false, false); + } + + @Test + public void testWhitespace() { + ServiceReference[] s = callSelectHealthCheckReferences(HealthCheckSelector.tags("\t \n\r foo \t", "", " \t-bar\n", "")); + assertServices(s, true, false, false, false, false); + } + + @Test + public void testOther() { + ServiceReference[] s = callSelectHealthCheckReferences(HealthCheckSelector.tags("other")); + assertServices(s, false, false, false, true, false); + } + + @Test + public void testMinusOther() { + ServiceReference[] s = callSelectHealthCheckReferences(HealthCheckSelector.tags("-other")); + assertServices(s, true, true, true, false, true); + } + + @Test + public void testMinusOtherFoo() { + ServiceReference[] s = callSelectHealthCheckReferences(HealthCheckSelector.tags("-other", "-foo")); + assertServices(s, false, true, false, false, true); + } + + @Test + public void testNoResults() { + ServiceReference[] s = callSelectHealthCheckReferences(HealthCheckSelector.tags("NOT A TAG")); + assertEquals("Expecting no services", 0, s.length); + } + + @Test + public void testSingleName() { + ServiceReference[] s = callSelectHealthCheckReferences(HealthCheckSelector.names("test1")); + assertServices(s, true, false, false, false, false); + } + + @Test + public void testMultipleNames() { + ServiceReference[] s = callSelectHealthCheckReferences(HealthCheckSelector.names("test1", "test3")); + assertServices(s, true, false, true, false, false); + } + + @Test + public void testExcludeName() { + ServiceReference[] s = callSelectHealthCheckReferences(HealthCheckSelector.tags("foo").withNames("-test1")); + assertServices(s, false, false, true, false, false); + } + + @Test + public void testNameOrTag() { + ServiceReference[] s = callSelectHealthCheckReferences(HealthCheckSelector.tags("foo").withNames("test4")); + assertServices(s, true, false, true, true, false); + } + + +} diff --git a/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/HealthCheckServletIT.java b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/HealthCheckServletIT.java new file mode 100644 index 00000000000..2fcd2840e48 --- /dev/null +++ b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/HealthCheckServletIT.java @@ -0,0 +1,128 @@ +/* + * 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 SF 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.hc.core.it; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.UUID; + +import javax.inject.Inject; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.Configuration; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.junit.PaxExam; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.http.HttpService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Verify that the HealthCheckExecutorServlet becomes available after creating the corresponding config */ +@RunWith(PaxExam.class) +public class HealthCheckServletIT { + private final Logger log = LoggerFactory.getLogger(getClass()); + + @Inject + private ConfigurationAdmin configAdmin; + + @Inject + private BundleContext bundleContext; + + private MockHttpService httpService; + private ServiceRegistration reg; + + @Configuration + public Option[] config() { + return U.config(); + } + + private int countServletServices(String packageNamePrefix) throws InvalidSyntaxException { + final List classNames = httpService.getServletClassNames(); + int count = 0; + for (final String className : classNames) { + if (className.startsWith(packageNamePrefix)) { + count++; + } + } + return count; + } + + @Before + public void setup() { + httpService = new MockHttpService(); + reg = bundleContext.registerService(HttpService.class.getName(), httpService, null); + } + + @After + public void cleanup() { + reg.unregister(); + reg = null; + httpService = null; + } + + @Test + public void testServletBecomesActive() throws InvalidSyntaxException, IOException, InterruptedException { + final String servletPathPropertyName = "servletPath"; + final String servletPathPropertyVal = "/test/" + UUID.randomUUID(); + final String packagePrefix = "org.apache.felix.hc"; + assertEquals("Initially expecting no servlet from " + packagePrefix, 0, countServletServices(packagePrefix)); + final int pathsBefore = httpService.getPaths().size(); + + // Activate servlet and wait for it to show up + final String factoryPid = "org.apache.felix.hc.core.impl.servlet.HealthCheckExecutorServlet"; + org.osgi.service.cm.Configuration cfg = configAdmin.createFactoryConfiguration(factoryPid, null); + log.info("Created factory configuration for servlet with pid = {}", cfg.getPid()); + final Dictionary props = new Hashtable(); + props.put(servletPathPropertyName, servletPathPropertyVal); + cfg.update(props); + log.info("Updated config with properties {}", props); + + final long timeoutMsec = 5000L; + final long endTime = System.currentTimeMillis() + timeoutMsec; + while (System.currentTimeMillis() < endTime) { + if (countServletServices(packagePrefix) > 0) { + break; + } + Thread.sleep(50L); + } + + int expectedServletCount = 6; + assertEquals("After adding configuration, expecting six servlets from " + packagePrefix, expectedServletCount, + countServletServices(packagePrefix)); + final List paths = httpService.getPaths(); + assertEquals("Expecting six new servlet registration", pathsBefore + expectedServletCount, paths.size()); + assertEquals("Expecting the HC servlet to be registered at " + servletPathPropertyVal, servletPathPropertyVal, paths.get(paths.size() - 6)); // paths list is longer, + // use last entries in list + assertEquals("Expecting the HTML HC servlet to be registered at " + servletPathPropertyVal + ".html", servletPathPropertyVal + ".html", paths.get(paths.size() - 5)); + assertEquals("Expecting the JSON HC servlet to be registered at " + servletPathPropertyVal + ".json", servletPathPropertyVal + ".json", paths.get(paths.size() - 4)); + assertEquals("Expecting the JSONP HC servlet to be registered at " + servletPathPropertyVal + ".jsonp", servletPathPropertyVal + ".jsonp", paths.get(paths.size() - 3)); + assertEquals("Expecting the TXT HC servlet to be registered at " + servletPathPropertyVal + ".txt", servletPathPropertyVal + ".txt", paths.get(paths.size() - 2)); + assertEquals("Expecting the verbose TXT HC servlet to be registered at " + servletPathPropertyVal + ".verbose.txt", servletPathPropertyVal + ".verbose.txt", + paths.get(paths.size() - 1)); + } +} diff --git a/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/JmxAdjustableStatusHealthCheckIT.java b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/JmxAdjustableStatusHealthCheckIT.java new file mode 100644 index 00000000000..5bd5683bfb1 --- /dev/null +++ b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/JmxAdjustableStatusHealthCheckIT.java @@ -0,0 +1,157 @@ +/* + * 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 SF 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.hc.core.it; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; + +import javax.inject.Inject; +import javax.management.DynamicMBean; + +import org.apache.felix.hc.api.FormattingResultLog; +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.ResultLog; +import org.apache.felix.hc.api.ResultLog.Entry; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckExecutor; +import org.apache.felix.hc.api.execution.HealthCheckSelector; +import org.apache.felix.hc.core.impl.JmxAdjustableStatusHealthCheck; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.Configuration; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.junit.PaxExam; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; + +/** Test jmx-adjustable status for testing HC. */ +@RunWith(PaxExam.class) +public class JmxAdjustableStatusHealthCheckIT { + + @Inject + private HealthCheckExecutor executor; + + @Inject + private BundleContext bundleContext; + + private static String testTag = "testTagName"; + + @SuppressWarnings("rawtypes") + private List regs = new ArrayList(); + + @Configuration + public Option[] config() { + return U.config(); + } + + private void assertResult(String tag, Result.Status expected) { + final Result result = getOverallResult(executor.execute(HealthCheckSelector.tags(tag))); + assertEquals("Expected status " + expected + " for tag " + tag, expected, result.getStatus()); + } + + @Before + public void setup() { + U.expectHealthChecks(0, executor, testTag); + registerHC(testTag); + U.expectHealthChecks(1, executor, testTag); + assertResult(testTag, Result.Status.OK); + } + + @After + @SuppressWarnings("rawtypes") + public void cleanup() throws Exception { + invokeMBean("reset", new Object[] {}, new String[] {}); + + U.expectHealthChecks(1, executor, testTag); + assertResult(testTag, Result.Status.OK); + + for (ServiceRegistration r : regs) { + r.unregister(); + } + regs.clear(); + U.expectHealthChecks(0, executor, testTag); + } + + @Test + public void testWarnStatus() throws Exception { + invokeMBean("addWarnResultForTags", new Object[] { testTag }, new String[] { String.class.getName() }); + U.expectHealthChecks(2, executor, testTag); + assertResult(testTag, Result.Status.WARN); + } + + @Test + public void testCriticalStatus() throws Exception { + invokeMBean("addCriticalResultForTags", new Object[] { "anotherTag," + testTag }, + new String[] { String.class.getName() }); + + final String[] tags = { "anotherTag", testTag }; + for (String tag : tags) { + U.expectHealthChecks(2, executor, testTag); + assertResult(tag, Result.Status.CRITICAL); + } + } + + @Test + public void testAnotherTag() throws Exception { + // Selecting an unused tag returns WARN - not sure why but + // if that changes we should detect it. + assertResult("some_unused_tag", Result.Status.WARN); + } + + private void registerHC(final String... tags) { + final HealthCheck hc = new HealthCheck() { + @Override + public Result execute() { + return new Result(Result.Status.OK, "All good for " + tags[0]); + } + }; + + final Dictionary props = new Hashtable(); + props.put(HealthCheck.NAME, "name_" + tags[0]); + props.put(HealthCheck.TAGS, tags); + + regs.add(bundleContext.registerService(HealthCheck.class.getName(), hc, props)); + } + + private void invokeMBean(String operation, Object[] args, String[] signature) throws Exception { + + ServiceReference[] serviceReference = bundleContext.getServiceReferences(DynamicMBean.class.getName(), + "(jmx.objectname=" + JmxAdjustableStatusHealthCheck.OBJECT_NAME + ")"); + DynamicMBean mBean = (DynamicMBean) bundleContext.getService(serviceReference[0]); + mBean.invoke(operation, args, signature); + + } + + private Result getOverallResult(List results) { + FormattingResultLog resultLog = new FormattingResultLog(); + for (HealthCheckExecutionResult executionResult : results) { + for (Entry entry : executionResult.getHealthCheckResult()) { + resultLog.add(new ResultLog.Entry(entry.getStatus(), entry.getMessage(), entry.getException())); + } + } + return new Result(resultLog); + } +} diff --git a/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/MockHttpService.java b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/MockHttpService.java new file mode 100644 index 00000000000..5bfdabc25e6 --- /dev/null +++ b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/MockHttpService.java @@ -0,0 +1,62 @@ +/* + * 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 SF 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.hc.core.it; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Dictionary; +import java.util.List; + +import javax.servlet.Servlet; + +import org.osgi.service.http.HttpContext; +import org.osgi.service.http.HttpService; + +class MockHttpService implements HttpService { + + private List paths = new ArrayList(); + + private List classNames = new ArrayList(); + + @Override + public void registerResources(String alias, String name, HttpContext context) { + } + + @Override + public void registerServlet(String alias, Servlet servlet, Dictionary initparams, HttpContext context) { + paths.add(alias); + classNames.add(servlet.getClass().getName()); + } + + public void unregister(String alias) { + paths.remove(alias); + } + + @Override + public HttpContext createDefaultHttpContext() { + return null; + } + + List getPaths() { + return Collections.unmodifiableList(paths); + } + + List getServletClassNames() { + return Collections.unmodifiableList(classNames); + } +} diff --git a/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/U.java b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/U.java new file mode 100644 index 00000000000..3a71d8f7dda --- /dev/null +++ b/healthcheck/core/src/test/java/org/apache/felix/hc/core/it/U.java @@ -0,0 +1,128 @@ +/* + * 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 SF 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.hc.core.it; + +import static org.junit.Assert.fail; +import static org.ops4j.pax.exam.CoreOptions.bundle; +import static org.ops4j.pax.exam.CoreOptions.junitBundles; +import static org.ops4j.pax.exam.CoreOptions.mavenBundle; +import static org.ops4j.pax.exam.CoreOptions.options; +import static org.ops4j.pax.exam.CoreOptions.provision; +import static org.ops4j.pax.exam.CoreOptions.systemProperty; +import static org.ops4j.pax.exam.CoreOptions.when; + +import java.io.File; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; + +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.execution.HealthCheckExecutionOptions; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckExecutor; +import org.apache.felix.hc.api.execution.HealthCheckSelector; +import org.ops4j.pax.exam.CoreOptions; +import org.ops4j.pax.exam.Option; +import org.osgi.framework.ServiceReference; + +/** Test utilities */ +public class U { + + // the name of the system property providing the bundle file to be installed and tested + private static final String BUNDLE_JAR_SYS_PROP = "project.bundle.file"; + + static Option[] config() { + final String localRepo = System.getProperty("maven.repo.local", ""); + final boolean felixShell = "true".equals(System.getProperty("felix.shell", "false")); + + final String bundleFileName = System.getProperty(BUNDLE_JAR_SYS_PROP); + final File bundleFile = new File(bundleFileName); + System.out.println("Using project bundle file "+bundleFileName); + if (!bundleFile.canRead()) { + throw new IllegalArgumentException("Cannot read from bundle file " + bundleFileName + " specified in the " + + BUNDLE_JAR_SYS_PROP + " system property"); + } + + // As we're using the forked pax exam container, we need to add a VM + // option to activate the jacoco test coverage agent. + final String coverageCommand = System.getProperty("coverage.command"); + + return options( + when(localRepo.length() > 0).useOptions( + systemProperty("org.ops4j.pax.url.mvn.localRepository").value(localRepo)), + junitBundles(), + when(coverageCommand != null && coverageCommand.trim().length() > 0).useOptions( + CoreOptions.vmOption(coverageCommand)), + when(felixShell).useOptions( + provision( + mavenBundle("org.apache.felix", "org.apache.felix.gogo.shell", "0.10.0"), + mavenBundle("org.apache.felix", "org.apache.felix.gogo.runtime", "0.10.0"), + mavenBundle("org.apache.felix", "org.apache.felix.gogo.command", "0.12.0"), + mavenBundle("org.apache.felix", "org.apache.felix.shell.remote", "1.1.2"))), + provision( + bundle(bundleFile.toURI().toString()), + mavenBundle("org.apache.felix", "org.apache.felix.scr", "2.0.14"), + mavenBundle("org.apache.felix", "org.apache.felix.configadmin", "1.8.16"), + mavenBundle("org.apache.felix", "org.apache.felix.healthcheck.api").versionAsInProject(), + mavenBundle().groupId("org.apache.commons").artifactId("commons-lang3").versionAsInProject(), + mavenBundle().groupId("javax.servlet").artifactId("javax.servlet-api").versionAsInProject(), + mavenBundle().groupId("org.apache.servicemix.bundles").artifactId("org.apache.servicemix.bundles.quartz") + .versionAsInProject())); + } + + // -- util methods + + /** Wait until the specified number of health checks are seen by supplied executor */ + static void expectHealthChecks(int howMany, HealthCheckExecutor executor, String... tags) { + expectHealthChecks(howMany, executor, new HealthCheckExecutionOptions(), tags); + } + + /** Wait until the specified number of health checks are seen by supplied executor */ + static void expectHealthChecks(int howMany, HealthCheckExecutor executor, HealthCheckExecutionOptions options, String... tags) { + final long timeout = System.currentTimeMillis() + 10000L; + int count = 0; + while (System.currentTimeMillis() < timeout) { + final List results = executor.execute(HealthCheckSelector.tags(tags), options); + count = results.size(); + if (count == howMany) { + return; + } + try { + Thread.sleep(100L); + } catch (InterruptedException iex) { + throw new RuntimeException("Unexpected InterruptedException"); + } + } + fail("Did not get " + howMany + " health checks with tags " + Arrays.asList(tags) + " after " + timeout + " msec (last count=" + + count + ")"); + } + + + static ServiceReference[] callSelectHealthCheckReferences(HealthCheckExecutor executor, HealthCheckSelector selector) { + String methodName = "selectHealthCheckReferences"; + try { + Method method = executor.getClass().getDeclaredMethod(methodName, HealthCheckSelector.class, HealthCheckExecutionOptions.class); + Object result = method.invoke(executor, selector, new HealthCheckExecutionOptions().setCombineTagsWithOr(false)); + return (ServiceReference[]) result; + } catch(Exception e) { + throw new IllegalStateException("Could not call method "+methodName+ " of class "+executor.getClass(), e); + } + } + + +} diff --git a/healthcheck/core/src/test/java/org/apache/felix/hc/jmx/impl/HealthCheckMBeanTest.java b/healthcheck/core/src/test/java/org/apache/felix/hc/jmx/impl/HealthCheckMBeanTest.java new file mode 100644 index 00000000000..45ae8ea3e9d --- /dev/null +++ b/healthcheck/core/src/test/java/org/apache/felix/hc/jmx/impl/HealthCheckMBeanTest.java @@ -0,0 +1,121 @@ +/* + * 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 SF 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.hc.jmx.impl; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import java.lang.management.ManagementFactory; + +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.core.impl.executor.ExtendedHealthCheckExecutor; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.osgi.framework.ServiceReference; + +public class HealthCheckMBeanTest { + private final MBeanServer jmxServer = ManagementFactory.getPlatformMBeanServer(); + private boolean resultOk; + public static final String OBJECT_NAME = "org.apache.sling.testing:type=HealthCheckMBeanTest"; + + @Mock + private HealthCheck testHealthCheck; + + @Mock + private ServiceReference ref; + + @Mock + private ExtendedHealthCheckExecutor extendedHealthCheckExecutor; + + @Mock + private HealthCheckExecutionResult result; + + + @Before + public void setup() { + initMocks(this); + + when(testHealthCheck.execute()).then(new Answer() { + + @Override + public Result answer(InvocationOnMock invocation) throws Throwable { + if (resultOk) { + return new Result(Result.Status.OK, "Nothing to report, result ok"); + } else { + return new Result(Result.Status.WARN, "Result is not ok!"); + } + } + }); + + when(extendedHealthCheckExecutor.execute(ref)).thenReturn(result); + when(result.getHealthCheckResult()).then(new Answer() { + + @Override + public Result answer(InvocationOnMock invocation) throws Throwable { + return testHealthCheck.execute(); + } + }); + } + + @Test + public void testBean() throws Exception { + + final HealthCheckMBean mbean = new HealthCheckMBean(ref, extendedHealthCheckExecutor); + final ObjectName name = new ObjectName(OBJECT_NAME); + jmxServer.registerMBean(mbean, name); + try { + resultOk = true; + assertEquals(true, getJmxValue(OBJECT_NAME, "ok")); + + Thread.sleep(1500); + resultOk = false; + assertEquals(false, getJmxValue(OBJECT_NAME, "ok")); + + Thread.sleep(1500); + assertThat(String.valueOf(getJmxValue(OBJECT_NAME, "log")), containsString("message=Result is not ok!")); + + } finally { + jmxServer.unregisterMBean(name); + } + } + + + private Object getJmxValue(String mbeanName, String attributeName) throws Exception { + final MBeanServer jmxServer = ManagementFactory.getPlatformMBeanServer(); + final ObjectName objectName = new ObjectName(mbeanName); + if (jmxServer.queryNames(objectName, null).size() == 0) { + fail("MBean not found: " + objectName); + } + final Object value = jmxServer.getAttribute(objectName, attributeName); + return value; + } + + +} \ No newline at end of file diff --git a/healthcheck/generalchecks/LICENSE b/healthcheck/generalchecks/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/healthcheck/generalchecks/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/healthcheck/generalchecks/bnd.bnd b/healthcheck/generalchecks/bnd.bnd new file mode 100644 index 00000000000..3130e6fa7ca --- /dev/null +++ b/healthcheck/generalchecks/bnd.bnd @@ -0,0 +1,13 @@ +Bundle-Category: healthcheck + +Bundle-Description: ${project.description} + +Bundle-DocURL: https://felix.apache.org + +Bundle-License: Apache License, Version 2.0 + +Bundle-Vendor: The Apache Software Foundation + +Conditional-Package: org.apache.commons.cli.*,org.apache.felix.utils.* + +Export-Package: org.apache.felix.hc.generalchecks.util diff --git a/healthcheck/generalchecks/pom.xml b/healthcheck/generalchecks/pom.xml new file mode 100644 index 00000000000..41cfaab5cd2 --- /dev/null +++ b/healthcheck/generalchecks/pom.xml @@ -0,0 +1,193 @@ + + + + 4.0.0 + + + org.apache.felix + felix-parent + 6 + + + + org.apache.felix.healthcheck.generalchecks + 2.0.1-SNAPSHOT + + Apache Felix Health Check General Checks + 2013 + + + General purpose health checks that can be simply configured to check for generic aspects like disk space, started bundles, remote service availability, etc. + + + + 8 + + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/healthcheck/generalchecks + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/healthcheck/generalchecks + http://svn.apache.org/viewvc/felix/trunk/http/generalchecks/ + + + + + + biz.aQute.bnd + bnd-maven-plugin + 4.1.0 + + + + bnd-process + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + org.apache.felix + maven-bundle-plugin + 4.1.0 + + + baseline + + baseline + + + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + + org.osgi + osgi.cmpn + 7.0.0 + + + + + + + + + + + org.osgi + osgi.core + 6.0.0 + provided + + + org.osgi + osgi.cmpn + 6.0.0 + provided + + + org.osgi + osgi.annotation + 6.0.1 + provided + + + + org.apache.felix + org.apache.felix.healthcheck.api + 2.0.0 + provided + + + + org.apache.felix + org.apache.felix.healthcheck.annotation + 2.0.0 + provided + + + + org.slf4j + slf4j-api + 1.7.6 + provided + + + + org.apache.commons + commons-lang3 + 3.4 + provided + + + + + commons-cli + commons-cli + 1.4 + provided + + + org.apache.felix + org.apache.felix.utils + 1.11.0 + provided + + + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-core + 1.9.5 + test + + + org.codehaus.groovy + groovy-all + 2.4.13 + test + + + + + diff --git a/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/BundlesStartedCheck.java b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/BundlesStartedCheck.java new file mode 100644 index 00000000000..541002ccbf9 --- /dev/null +++ b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/BundlesStartedCheck.java @@ -0,0 +1,163 @@ +/* + * 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 SF 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.hc.generalchecks; + +import java.util.regex.Pattern; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.hc.annotation.HealthCheckService; +import org.apache.felix.hc.api.FormattingResultLog; +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@HealthCheckService(name = BundlesStartedCheck.HC_NAME) +@Component(configurationPolicy = ConfigurationPolicy.REQUIRE) +@Designate(ocd = BundlesStartedCheck.Config.class, factory = true) +public class BundlesStartedCheck implements HealthCheck { + + private static final Logger LOG = LoggerFactory.getLogger(BundlesStartedCheck.class); + + public static final String HC_NAME = "Bundles Started"; + public static final String HC_LABEL = "Health Check: " + HC_NAME; + + @ObjectClassDefinition(name = HC_LABEL, description = "Checks the configured path(s) against the given thresholds") + public @interface Config { + @AttributeDefinition(name = "Name", description = "Name of this health check") + String hc_name() default HC_NAME; + + @AttributeDefinition(name = "Tags", description = "List of tags for this health check, used to select subsets of health checks for execution e.g. by a composite health check.") + String[] hc_tags() default {}; + + @AttributeDefinition(name = "Includes RegEx", description = "RegEx to select all relevant bundles for this check. The RegEx is matched against the symbolic name of the bundle.") + String includesRegex() default ".*"; + + @AttributeDefinition(name = "Excludes RegEx", description = "Optional RegEx to exclude bundles from this check (matched against symbolic name). Allows to exclude specific bundles from selected set as produced by 'Includes RegEx'.") + String excludesRegex() default ""; + + @AttributeDefinition(name = "CRITICAL for inactive bundles", description = "By default inactive bundles produce warnings, if this is set to true inactive bundles produce a CRITICAL result") + boolean useCriticalForInactive() default false; + + @AttributeDefinition + String webconsole_configurationFactory_nameHint() default "Bundles started includes: {includesRegex} excludes: {excludesRegex}"; + } + + private BundleContext bundleContext; + private Pattern includesRegex; + private Pattern excludesRegex; + boolean useCriticalForInactive; + + @Activate + protected void activate(BundleContext bundleContext, Config config) { + this.bundleContext = bundleContext; + this.includesRegex = Pattern.compile(config.includesRegex()); + this.excludesRegex = StringUtils.isNotBlank(config.excludesRegex()) ? Pattern.compile(config.excludesRegex()) : null; + this.useCriticalForInactive = config.useCriticalForInactive(); + LOG.info("Activated bundles started HC for includesRegex={} excludesRegex={}% useCriticalForInactive={}", includesRegex, excludesRegex, useCriticalForInactive); + } + + + @Override + public Result execute() { + FormattingResultLog log = new FormattingResultLog(); + + Bundle[] bundles = this.bundleContext.getBundles(); + log.debug("Framwork has {} bundles in total", bundles.length); + + int countExcluded = 0; + int relevantBundlesCount = 0; + int inctiveCount = 0; + for (Bundle bundle : bundles) { + String bundleSymbolicName = bundle.getSymbolicName(); + int bundleState = bundle.getState(); + + if(!includesRegex.matcher(bundleSymbolicName).matches()) { + LOG.debug("Bundle {} not matched by {}", bundleSymbolicName, includesRegex); + continue; + } + + if(excludesRegex!=null && excludesRegex.matcher(bundleSymbolicName).matches()) { + LOG.debug("Bundle {} excluded {}", bundleSymbolicName, excludesRegex); + countExcluded ++; + continue; + } + relevantBundlesCount++; + + boolean bundleIsLogged = false; + if (bundleState != Bundle.ACTIVE) { + // support lazy activation (https://www.osgi.org/developer/design/lazy-start/) + if (bundleState == Bundle.STARTING && isLazyActivation(bundle)) { + LOG.debug("Ignoring lazily activated bundle {}", bundleSymbolicName); + } else if (StringUtils.isNotBlank((String) bundle.getHeaders().get(Constants.FRAGMENT_HOST))) { + LOG.debug("Ignoring bundle fragment: {}", bundleSymbolicName); + } else { + String msg = "Inactive bundle {} {}: {}"; + Object[] msgObjs = new Object[] {bundle.getBundleId(), bundleSymbolicName, getStateLabel(bundleState)}; + LOG.debug(msg, msgObjs); + if(useCriticalForInactive) { + log.critical(msg, msgObjs); + } else { + log.warn(msg, msgObjs); + } + bundleIsLogged = true; + inctiveCount++; + } + } + if(!bundleIsLogged) { + log.debug("Bundle {} {}: {}", bundle.getBundleId(), bundleSymbolicName, getStateLabel(bundleState)); + } + } + + String baseMsg = relevantBundlesCount+" bundles"+(!includesRegex.pattern().equals(".*")?" for pattern "+includesRegex.pattern(): ""); + String excludedMsg = countExcluded > 0 ? " (" + countExcluded + " excluded via pattern "+excludesRegex.pattern()+")" : ""; + if (inctiveCount > 0) { + log.info("Found "+inctiveCount + " inactive of "+baseMsg + excludedMsg); + } else { + log.info("All "+baseMsg+" are started" + excludedMsg); + } + + return new Result(log); + } + + private static boolean isLazyActivation(Bundle b) { + return Constants.ACTIVATION_LAZY.equals(b.getHeaders().get(Constants.BUNDLE_ACTIVATIONPOLICY)); + } + + private static String getStateLabel(int state) { + switch(state) { + case Bundle.UNINSTALLED: return "UNINSTALLED"; + case Bundle.INSTALLED: return "INSTALLED"; + case Bundle.RESOLVED: return "RESOLVED"; + case Bundle.STARTING: return "STARTING"; + case Bundle.STOPPING: return "STOPPING"; + case Bundle.ACTIVE: return "ACTIVE"; + default: return ""+state; + } + } + +} diff --git a/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/CpuCheck.java b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/CpuCheck.java new file mode 100644 index 00000000000..0cc552d5c6f --- /dev/null +++ b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/CpuCheck.java @@ -0,0 +1,118 @@ +/* + * 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 SF 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.hc.generalchecks; + +import java.lang.management.ManagementFactory; + +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.InstanceNotFoundException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.management.ReflectionException; + +import org.apache.felix.hc.annotation.HealthCheckService; +import org.apache.felix.hc.api.FormattingResultLog; +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.ResultLog; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@HealthCheckService(name = CpuCheck.HC_NAME) +@Component(configurationPolicy = ConfigurationPolicy.REQUIRE) +@Designate(ocd = CpuCheck.Config.class, factory = false) +public class CpuCheck implements HealthCheck { + + private static final Logger LOG = LoggerFactory.getLogger(CpuCheck.class); + + public static final String HC_NAME = "CPU"; + public static final String HC_LABEL = "Health Check: " + HC_NAME; + + @ObjectClassDefinition(name = HC_LABEL, description = "Checks for high CPU load") + public @interface Config { + @AttributeDefinition(name = "Name", description = "Name of this health check") + String hc_name() default HC_NAME; + + @AttributeDefinition(name = "Tags", description = "List of tags for this health check, used to select subsets of health checks for execution e.g. by a composite health check.") + String[] hc_tags() default {}; + + @AttributeDefinition(name = "CPU usage threshold for WARN", description = "in percent, if CPU usage is over this limit the result is WARN") + long cpuPercentageThresholdWarn() default 95; + + } + + private long cpuPercentageThresholdWarn; + + @Activate + protected void activate(final Config config) { + cpuPercentageThresholdWarn = config.cpuPercentageThresholdWarn(); + LOG.info("Activated CPU HC: cpuPercentageThresholdWarn={}%", cpuPercentageThresholdWarn); + } + + @Override + public Result execute() { + + FormattingResultLog log = new FormattingResultLog(); + + double processCpuLoad = Double.NaN; + try { + processCpuLoad = getProcessCpuLoad(); + } catch (Exception e) { + log.add(new ResultLog.Entry(Result.Status.HEALTH_CHECK_ERROR, "Could not get process CPU load: " + e, e)); + } + + if (Double.isNaN(processCpuLoad)) { + log.info("No CPU load available yet"); + } else { + String loadStr = String.format("%.1f", processCpuLoad); + Result.Status status = processCpuLoad < cpuPercentageThresholdWarn ? Result.Status.OK : Result.Status.WARN; + log.add(new ResultLog.Entry(status, "Process CPU Usage: " + loadStr + "%")); + } + return new Result(log); + + } + + public double getProcessCpuLoad() throws MalformedObjectNameException, ReflectionException, InstanceNotFoundException { + + MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); + ObjectName name = ObjectName.getInstance("java.lang:type=OperatingSystem"); + AttributeList list = mbs.getAttributes(name, new String[] { "ProcessCpuLoad" }); + + if (list.isEmpty()) { + return Double.NaN; + } + + Attribute att = (Attribute) list.get(0); + Double value = (Double) att.getValue(); + + if (value == -1.0) { + return Double.NaN; + } + + return value * 100; + } + +} diff --git a/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/DiskSpaceCheck.java b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/DiskSpaceCheck.java new file mode 100644 index 00000000000..75f49327e84 --- /dev/null +++ b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/DiskSpaceCheck.java @@ -0,0 +1,123 @@ +/* + * 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 SF 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.hc.generalchecks; + +import static org.apache.felix.hc.api.FormattingResultLog.bytesHumanReadable; + +import java.io.File; +import java.util.Arrays; + +import org.apache.felix.hc.annotation.HealthCheckService; +import org.apache.felix.hc.api.FormattingResultLog; +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.ResultLog; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@HealthCheckService(name = DiskSpaceCheck.HC_NAME) +@Component(configurationPolicy = ConfigurationPolicy.REQUIRE) +@Designate(ocd = DiskSpaceCheck.Config.class, factory = true) +public class DiskSpaceCheck implements HealthCheck { + + private static final Logger LOG = LoggerFactory.getLogger(DiskSpaceCheck.class); + + public static final String HC_NAME = "Disk Space"; + public static final String HC_LABEL = "Health Check: " + HC_NAME; + + @ObjectClassDefinition(name = HC_LABEL, description = "Checks the configured path(s) against the given thresholds") + public @interface Config { + @AttributeDefinition(name = "Name", description = "Name of this health check") + String hc_name() default HC_NAME; + + @AttributeDefinition(name = "Tags", description = "List of tags for this health check, used to select subsets of health checks for execution e.g. by a composite health check.") + String[] hc_tags() default {}; + + @AttributeDefinition(name = "Disk used threshold for WARN", description = "in percent, if disk usage is over this limit the result is WARN") + long diskUsedThresholdWarn() default 90; + + @AttributeDefinition(name = "Disk used threshold for CRITICAL", description = "in percent, if disk usage is over this limit the result is CRITICAL") + long diskUsedThresholdCritical() default 97; + + @AttributeDefinition(name = "Paths to check for disk usage", description = "Paths that is checked for free space according the configured thresholds") + String[] diskPaths() default { "." }; + + @AttributeDefinition + String webconsole_configurationFactory_nameHint() default "{hc.name}: {diskPaths} used>{diskUsedThresholdWarn}% -> WARN used>{diskUsedThresholdCritical}% -> CRITICAL"; + } + + private long diskUsedThresholdWarn; + private long diskUsedThresholdCritical; + private String[] diskPaths; + + @Activate + protected void activate(final Config config) { + diskUsedThresholdWarn = config.diskUsedThresholdWarn(); + diskUsedThresholdCritical = config.diskUsedThresholdCritical(); + diskPaths = config.diskPaths(); + + LOG.info("Activated disk usage HC for path(s) {} diskUsedThresholdWarn={}% diskUsedThresholdCritical={}%", Arrays.asList(diskPaths), + diskUsedThresholdWarn, diskUsedThresholdCritical); + } + + @Override + public Result execute() { + + FormattingResultLog log = new FormattingResultLog(); + + for (String diskPath : diskPaths) { + + File diskPathFile = new File(diskPath); + + if (!diskPathFile.exists()) { + log.warn("Directory '{}' does not exist", diskPathFile); + continue; + } else if (!diskPathFile.isDirectory()) { + log.warn("Directory '{}' is not a directory", diskPathFile); + continue; + } + + double total = diskPathFile.getTotalSpace(); + double free = diskPathFile.getUsableSpace(); + double usedPercentage = (total - free) / total * 100d; + + String totalStr = bytesHumanReadable(total); + String freeStr = bytesHumanReadable(free); + String msg = String.format("Disk Usage %s: %.1f%% of %s used / %s free", diskPathFile.getAbsolutePath(), + usedPercentage, + totalStr, freeStr); + + Result.Status status = usedPercentage > this.diskUsedThresholdCritical ? Result.Status.CRITICAL + : usedPercentage > this.diskUsedThresholdWarn ? Result.Status.WARN + : Result.Status.OK; + + log.add(new ResultLog.Entry(status, msg)); + + } + + return new Result(log); + } + + +} diff --git a/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/HttpRequestsCheck.java b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/HttpRequestsCheck.java new file mode 100644 index 00000000000..a29cde8dd33 --- /dev/null +++ b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/HttpRequestsCheck.java @@ -0,0 +1,631 @@ +/* + * 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 SF 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.hc.generalchecks; + +import static java.util.stream.StreamSupport.stream; +import static org.apache.felix.hc.api.FormattingResultLog.msHumanReadable; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.ProtocolException; +import java.net.Proxy; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.hc.annotation.HealthCheckService; +import org.apache.felix.hc.api.FormattingResultLog; +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.ResultLog; +import org.apache.felix.hc.generalchecks.util.SimpleConstraintChecker; +import org.apache.felix.utils.json.JSONParser; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@HealthCheckService(name = HttpRequestsCheck.HC_NAME) +@Component(configurationPolicy = ConfigurationPolicy.REQUIRE) +@Designate(ocd = HttpRequestsCheck.Config.class, factory = true) +public class HttpRequestsCheck implements HealthCheck { + + private static final Logger LOG = LoggerFactory.getLogger(HttpRequestsCheck.class); + + public static final String HC_NAME = "Http Requests"; + public static final String HC_LABEL = "Health Check: " + HC_NAME; + + @ObjectClassDefinition(name = HC_LABEL, description = "Performs http(s) request(s) and checks the response for return code and optionally checks the response entity") + public @interface Config { + @AttributeDefinition(name = "Name", description = "Name of this health check") + String hc_name() default HC_NAME; + + @AttributeDefinition(name = "Tags", description = "List of tags for this health check, used to select subsets of health checks for execution e.g. by a composite health check.") + String[] hc_tags() default {}; + + @AttributeDefinition(name = "Request Specs", description = "List of requests to be made. Requests specs have two parts: " + + "Before '=>' can be a simple URL/path with curl-syntax advanced options (e.g. setting a header with -H \"Test: Test val\"), " + + "after the '=>' it is a simple response code that can be followed ' && MATCHES ' to match the response entity against or other matchers like HEADER, TIME or JSON (see defaults when creating a new configuration for examples).") + String[] requests() default { + "/path/example.html", + "/path/example.html => 200", + "/protected/example.html => 401", + "-u admin:admin /protected/example.html => 200", + "/path/example.html => 200 && MATCHES html title.*", + "/path/example.html => 200 && MATCHES html title.* && MATCHES anotherRegEx[a-z]", + "/path/example.html => 200 && HEADER Content-Type MATCHES text/html.*", + "/path/example.json => 200 && JSON root.arr[3].prop = myval", + "/path/example-timing-important.html => 200 && TIME < 2000", + "-X GET -H \"Accept: application/javascript\" http://api.example.com/path/example.json => 200 && JSON root.arr[3].prop = myval", + "-X HEAD --data \"{....}\" http://www.example.com/path/to/data.json => 303", + "--proxy proxyhost:2000 /path/example-timing-important.html => 200 && TIME < 2000" + }; + + @AttributeDefinition(name = "Connect Timeout", description = "Default connect timeout in ms. Can be overwritten per request with option --connect-timeout (in sec)") + int connectTimeoutInMs() default 7000; + + @AttributeDefinition(name = "Read Timeout", description = "Default read timeout in ms. Can be overwritten with per request option -m or --max-time (in sec)") + int readTimeoutInMs() default 7000; + + @AttributeDefinition(name = "Status for failed request constraint", description = "Status to fail with if the constraint check fails") + Result.Status statusForFailedContraint() default Result.Status.WARN; + + @AttributeDefinition(name = "Run in parallel", description = "Run requests in parallel (only active if more than one request spec is configured)") + boolean runInParallel() default true; + + + @AttributeDefinition + String webconsole_configurationFactory_nameHint() default "{hc.name}: {requests}"; + + } + + private List requestSpecs; + private int connectTimeoutInMs; + private int readTimeoutInMs; + private Result.Status statusForFailedContraint; + private boolean runInParallel; + + private String defaultBaseUrl = null; + + private FormattingResultLog configErrors; + + @Activate + protected void activate(BundleContext bundleContext, Config config) { + this.requestSpecs = getRequestSpecs(config.requests()); + this.connectTimeoutInMs = config.connectTimeoutInMs(); + this.readTimeoutInMs = config.readTimeoutInMs(); + this.statusForFailedContraint = config.statusForFailedContraint(); + this.runInParallel = config.runInParallel() && requestSpecs.size() > 1; + + setupDefaultBaseUrl(bundleContext); + + LOG.info("Default BaseURL: {}", defaultBaseUrl); + LOG.info("Activated Requests HC: {}", requestSpecs); + } + + private void setupDefaultBaseUrl(BundleContext bundleContext) { + ServiceReference serviceReference = bundleContext.getServiceReference("org.osgi.service.http.HttpService"); + boolean isHttp = Boolean.parseBoolean(String.valueOf(serviceReference.getProperty("org.apache.felix.http.enable"))); + boolean isHttps = Boolean.parseBoolean(String.valueOf(serviceReference.getProperty("org.apache.felix.https.enable"))); + if(isHttp) { + defaultBaseUrl = "http://localhost:"+serviceReference.getProperty("org.osgi.service.http.port"); + } else if(isHttps) { + defaultBaseUrl = "http://localhost:"+serviceReference.getProperty("org.osgi.service.https.port"); + } + } + + @Override + public Result execute() { + + FormattingResultLog overallLog = new FormattingResultLog(); + + // take over config errors + for(ResultLog.Entry entry: configErrors) { + overallLog.add(entry); + } + + // execute requests + Stream requestSpecsStream = runInParallel ? requestSpecs.parallelStream() : requestSpecs.stream(); + List logsForEachRequest = requestSpecsStream + .map(requestSpec -> requestSpec.check(defaultBaseUrl, connectTimeoutInMs, readTimeoutInMs, statusForFailedContraint, requestSpecs.size()>1)) + .collect(Collectors.toList()); + + // aggregate logs never in parallel + logsForEachRequest.stream().forEach( l -> stream(l.spliterator(), false).forEach(e -> overallLog.add(e))); + + return new Result(overallLog); + + } + + private List getRequestSpecs(String[] requestSpecStrArr) { + + configErrors = new FormattingResultLog(); + + List requestSpecs = new ArrayList(); + for(String requestSpecStr: requestSpecStrArr) { + try { + RequestSpec requestSpec = new RequestSpec(requestSpecStr); + requestSpecs.add(requestSpec); + } catch(Exception e) { + configErrors.critical("Invalid config: {}", requestSpecStr); + configErrors.add(new ResultLog.Entry(Result.Status.CRITICAL, " "+e.getMessage(), e)); + } + + } + return requestSpecs; + } + + static class RequestSpec { + + private static final String HEADER_AUTHORIZATION = "Authorization"; + + String method = "GET"; + String url; + Map headers = new HashMap(); + String data = null; + + String user; + + Integer connectTimeoutInMs; + Integer readTimeoutInMs; + + Proxy proxy; + + List responseChecks = new ArrayList(); + + RequestSpec(String requestSpecStr) throws ParseException, URISyntaxException { + + String[] requestSpecBits = requestSpecStr.split(" *=> *", 2); + + String requestInfo = requestSpecBits[0]; + parseCurlLikeRequestInfo(requestInfo); + + if(requestSpecBits.length > 1) { + parseResponseAssertion(requestSpecBits[1]); + } else { + // check for 200 as default + responseChecks.add(new ResponseCodeCheck(200)); + } + } + + private void parseResponseAssertion(String responseAssertions) { + + String[] responseAssertionArr = responseAssertions.split(" +&& +"); + for(String clause: responseAssertionArr) { + if(StringUtils.isNumeric(clause)) { + responseChecks.add(new ResponseCodeCheck(Integer.parseInt(clause))); + } else if(StringUtils.startsWithIgnoreCase(clause, ResponseTimeCheck.TIME)) { + responseChecks.add(new ResponseTimeCheck(clause.substring(ResponseTimeCheck.TIME.length()))); + } else if(StringUtils.startsWithIgnoreCase(clause, ResponseEntityRegExCheck.MATCHES)) { + responseChecks.add(new ResponseEntityRegExCheck(Pattern.compile(clause.substring(ResponseEntityRegExCheck.MATCHES.length())))); + } else if(StringUtils.startsWithIgnoreCase(clause, ResponseHeaderCheck.HEADER)) { + responseChecks.add(new ResponseHeaderCheck(clause.substring(ResponseHeaderCheck.HEADER.length()))); + } else if(StringUtils.startsWithIgnoreCase(clause, JsonPropertyCheck.JSON)) { + responseChecks.add(new JsonPropertyCheck(clause.substring(JsonPropertyCheck.JSON.length()))); + } else { + throw new IllegalArgumentException("Invalid response content assertion clause: '"+clause+"'"); + } + } + } + + private void parseCurlLikeRequestInfo(String requestInfo) throws ParseException, URISyntaxException { + CommandLineParser parser = new DefaultParser(); + + Options options = new Options(); + options.addOption("H", "header", true, ""); + options.addOption("X", "method", true, ""); + options.addOption("d", "data", true, ""); + options.addOption("u", "user", true, ""); + options.addOption(null, "connect-timeout", true, ""); + options.addOption("m", "max-time", true, ""); + options.addOption("x", "proxy", true, ""); + + String[] args = splitArgsRespectingQuotes(requestInfo); + + CommandLine line = parser.parse(options, args); + + if (line.hasOption("header")) { + String[] headerValues = line.getOptionValues("header"); + for(String headerVal: headerValues) { + String[] headerBits = headerVal.split(" *: *", 2); + headers.put(headerBits[0], headerBits[1]); + } + } + if (line.hasOption("method")) { + method = line.getOptionValue("method"); + } + if (line.hasOption("data")) { + data = line.getOptionValue("data"); + } + if (line.hasOption("user")) { + String userAndPw = line.getOptionValue("user"); + user = userAndPw.split(":")[0]; + byte[] encodedUserAndPw = Base64.getEncoder().encode(userAndPw.getBytes()); + headers.put(HEADER_AUTHORIZATION, "Basic "+new String(encodedUserAndPw)); + } + + if (line.hasOption("connect-timeout")) { + connectTimeoutInMs = Integer.valueOf(line.getOptionValue("connect-timeout")) * 1000; + } + if (line.hasOption("max-time")) { + readTimeoutInMs = Integer.valueOf(line.getOptionValue("max-time")) * 1000; + } + if (line.hasOption("proxy")) { + String curlProxy = line.getOptionValue("proxy"); + if(curlProxy.contains("@")) { + throw new IllegalArgumentException("Proxy authentication is not support"); + } + String proxyHost; + int proxyPort; + if(curlProxy.startsWith("http")) { + URI uri = new URI(curlProxy); + proxyHost = uri.getHost(); + proxyPort = uri.getPort(); + } else { + String[] curlProxyBits = curlProxy.split(":"); + proxyHost = curlProxyBits[0]; + proxyPort = curlProxyBits.length > 1 ? Integer.parseInt(curlProxyBits[1]) : 1080; + } + proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); + } + + url = line.getArgList().get(0); + + } + + String[] splitArgsRespectingQuotes(String requestInfo) { + List argList = new ArrayList(); + Pattern regex = Pattern.compile("[^\\s\"']+|\"[^\"]*\"|'[^']*'"); + Matcher regexMatcher = regex.matcher(requestInfo); + while (regexMatcher.find()) { + argList.add(regexMatcher.group()); + } + return argList.toArray(new String[argList.size()]); + } + + @Override + public String toString() { + return "RequestSpec [method=" + method + ", url=" + url + ", headers=" + headers + ", responseChecks=" + responseChecks + "]"; + } + + public FormattingResultLog check(String defaultBaseUrl, int connectTimeoutInMs, int readTimeoutInMs, Result.Status statusForFailedContraint, boolean showTiming) { + + FormattingResultLog log = new FormattingResultLog(); + String urlWithUser = user!=null ? user + " @ " + url: url; + log.debug("Checking {}", urlWithUser); + log.debug(" configured headers {}", headers.keySet()); + + Response response = null; + try { + response = performRequest(defaultBaseUrl, urlWithUser, connectTimeoutInMs, readTimeoutInMs, log); + } catch (IOException e) { + // request generally failed + log.add(new ResultLog.Entry(statusForFailedContraint, urlWithUser+": "+ e.getMessage(), e)); + } + + if(response != null) { + List resultBits = new ArrayList(); + boolean hasFailed = false; + for(ResponseCheck responseCheck: responseChecks) { + ResponseCheck.ResponseCheckResult result = responseCheck.checkResponse(response, log); + hasFailed = hasFailed || result.contraintFailed; + resultBits.add(result.message); + } + Result.Status status = hasFailed ? statusForFailedContraint : Result.Status.OK; + String timing = showTiming ? " " + msHumanReadable(response.requestDurationInMs) : ""; + // result of response assertion(s) + log.add(new ResultLog.Entry(status, urlWithUser+timing+": "+ StringUtils.join(resultBits,", "))); + } + + return log; + } + + public Response performRequest(String defaultBaseUrl, String urlWithUser, int connectTimeoutInMs, int readTimeoutInMs, FormattingResultLog log) throws IOException { + Response response = null; + HttpURLConnection conn = null; + try { + URL effectiveUrl; + if(url.startsWith("/")) { + effectiveUrl = new URL(defaultBaseUrl + url); + } else { + effectiveUrl = new URL(url); + } + + conn = openConnection(connectTimeoutInMs, readTimeoutInMs, effectiveUrl, log); + response = readResponse(conn, log); + + } finally { + if(conn!=null) { + conn.disconnect(); + } + } + return response; + } + + private HttpURLConnection openConnection(int defaultConnectTimeoutInMs, int defaultReadTimeoutInMs, URL effectiveUrl, FormattingResultLog log) + throws IOException, ProtocolException { + HttpURLConnection conn; + conn = (HttpURLConnection) (proxy==null ? effectiveUrl.openConnection() : effectiveUrl.openConnection(proxy)); + conn.setInstanceFollowRedirects(false); + conn.setUseCaches(false); + + int effectiveConnectTimeout = this.connectTimeoutInMs !=null ? this.connectTimeoutInMs : defaultConnectTimeoutInMs; + int effectiveReadTimeout = this.readTimeoutInMs !=null ? this.readTimeoutInMs : defaultReadTimeoutInMs; + log.debug("connectTimeout={}ms readTimeout={}ms", effectiveConnectTimeout, effectiveReadTimeout); + conn.setConnectTimeout(effectiveConnectTimeout); + conn.setReadTimeout(effectiveReadTimeout); + + conn.setRequestMethod(method); + for(Entry header: headers.entrySet()) { + conn.setRequestProperty(header.getKey(), header.getValue()); + } + if(data != null) { + conn.setDoOutput(true); + byte[] bytes = data.getBytes(); + log.debug("Sending request entity with {}bytes", bytes.length); + try(DataOutputStream wr = new DataOutputStream(conn.getOutputStream())) { + wr.write(bytes); + } + } + return conn; + } + + private Response readResponse(HttpURLConnection conn, FormattingResultLog log) throws IOException { + + long startTime = System.currentTimeMillis(); + + int actualResponseCode = conn.getResponseCode(); + String actualResponseMessage = conn.getResponseMessage(); + log.debug("Result: {} {}", actualResponseCode, actualResponseMessage); + Map> responseHeaders = conn.getHeaderFields(); + + StringWriter responseEntityWriter = new StringWriter(); + try (BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { + String inputLine; + while ((inputLine = in.readLine()) != null) { + responseEntityWriter.write(inputLine + "\n"); + } + } catch(IOException e) { + log.debug("Could not get response entity: {}", e.getMessage()); + } + + long requestDurationInMs = System.currentTimeMillis() - startTime; + Response response = new Response(actualResponseCode, actualResponseMessage, responseHeaders, responseEntityWriter.toString(), requestDurationInMs); + + return response; + } + + } + + static class Response { + final int actualResponseCode; + final String actualResponseMessage; + final Map> actualResponseHeaders; + final String actualResponseEntity; + final long requestDurationInMs; + + public Response(int actualResponseCode, String actualResponseMessage, Map> actualResponseHeaders, + String actualResponseEntity, long requestDurationInMs) { + super(); + this.actualResponseCode = actualResponseCode; + this.actualResponseMessage = actualResponseMessage; + this.actualResponseHeaders = actualResponseHeaders; + this.actualResponseEntity = actualResponseEntity; + this.requestDurationInMs = requestDurationInMs; + } + } + + static interface ResponseCheck { + + class ResponseCheckResult { + final boolean contraintFailed; + final String message; + + ResponseCheckResult(boolean contraintFailed, String message) { + this.contraintFailed = contraintFailed; + this.message = message; + } + + } + + ResponseCheckResult checkResponse(Response response, FormattingResultLog log); + } + + static class ResponseCodeCheck implements ResponseCheck { + + private final int expectedResponseCode; + + public ResponseCodeCheck(int expectedResponseCode) { + this.expectedResponseCode = expectedResponseCode; + } + + public ResponseCheckResult checkResponse(Response response, FormattingResultLog log) { + + if(expectedResponseCode != response.actualResponseCode) { + return new ResponseCheckResult(true, response.actualResponseCode + " (expected "+expectedResponseCode+")"); + } else { + return new ResponseCheckResult(false, "["+response.actualResponseCode + " "+response.actualResponseMessage+"]"); + } + } + } + + static class ResponseTimeCheck implements ResponseCheck { + final static String TIME = "TIME "; + + private final String timeConstraint; + + private final SimpleConstraintChecker simpleConstraintChecker = new SimpleConstraintChecker(); + + public ResponseTimeCheck(String timeConstraint) { + this.timeConstraint = timeConstraint; + } + + public ResponseCheckResult checkResponse(Response response, FormattingResultLog log) { + + log.debug("Checking request time [{}ms] for constraint [{}]", response.requestDurationInMs, timeConstraint); + if(!simpleConstraintChecker.check((Long) response.requestDurationInMs, timeConstraint)) { + return new ResponseCheckResult(true, "time ["+response.requestDurationInMs + "ms] does not fulfil constraint ["+timeConstraint+"]"); + } else { + return new ResponseCheckResult(false, "time ["+response.requestDurationInMs + "ms] fulfils constraint ["+timeConstraint+"]"); + } + } + } + + static class ResponseEntityRegExCheck implements ResponseCheck { + final static String MATCHES = "MATCHES "; + + private final Pattern expectedResponseEntityRegEx; + + public ResponseEntityRegExCheck(Pattern expectedResponseEntityRegEx) { + this.expectedResponseEntityRegEx = expectedResponseEntityRegEx; + } + + public ResponseCheckResult checkResponse(Response response, FormattingResultLog log) { + if(!expectedResponseEntityRegEx.matcher(response.actualResponseEntity).find()) { + return new ResponseCheckResult(true, "response does not match ["+expectedResponseEntityRegEx+']'); + } else { + return new ResponseCheckResult(false, "response matches ["+expectedResponseEntityRegEx+"]"); + } + } + } + + static class ResponseHeaderCheck implements ResponseCheck { + final static String HEADER = "HEADER "; + + private final String headerName; + private final String headerConstraint; + + private final SimpleConstraintChecker simpleConstraintChecker = new SimpleConstraintChecker(); + + + public ResponseHeaderCheck(String headerExpression) { + String[] headerCheckBits = headerExpression.split(" +", 2); + this.headerName = headerCheckBits[0]; + this.headerConstraint = headerCheckBits[1]; + } + + public ResponseCheckResult checkResponse(Response response, FormattingResultLog log) { + + List headerValues = response.actualResponseHeaders.get(headerName); + String headerVal = headerValues!=null && !headerValues.isEmpty() ? headerValues.get(0): null; + + log.debug("Checking {} with value [{}] for constraint [{}]", headerName, headerVal, headerConstraint); + if(!simpleConstraintChecker.check(headerVal, headerConstraint)) { + return new ResponseCheckResult(true, "header ["+headerName+"] has value ["+headerVal+"] which does not fulfil constraint ["+headerConstraint+"]"); + } else { + return new ResponseCheckResult(false, "header ["+headerName+"] ok"); + } + } + } + + static class JsonPropertyCheck implements ResponseCheck { + final static String JSON = "JSON "; + + private final String jsonPropertyPath; + private final String jsonPropertyConstraint; + + private final SimpleConstraintChecker simpleConstraintChecker = new SimpleConstraintChecker(); + + + public JsonPropertyCheck(String jsonExpression) { + String[] jsonCheckBits = jsonExpression.split(" +", 2); + this.jsonPropertyPath = jsonCheckBits[0]; + this.jsonPropertyConstraint = jsonCheckBits[1]; + } + + public ResponseCheckResult checkResponse(Response response, FormattingResultLog log) { + + JSONParser jsonParser; + try { + jsonParser = new JSONParser(response.actualResponseEntity); + } catch(Exception e) { + return new ResponseCheckResult(true, "invalid json response (["+jsonPropertyPath+"] cannot be checked agains constraint ["+jsonPropertyConstraint+"])"); + } + + Object propertyVal = getJsonProperty(jsonParser, jsonPropertyPath); + + log.debug("JSON property [{}] has value [{}]", jsonPropertyPath, propertyVal); + + log.debug("Checking [{}] with value [{}] for constraint [{}]", jsonPropertyPath, propertyVal, jsonPropertyConstraint); + if(!simpleConstraintChecker.check(propertyVal, jsonPropertyConstraint)) { + return new ResponseCheckResult(true, "json ["+jsonPropertyPath+"] has value ["+propertyVal+"] which does not fulfil constraint ["+jsonPropertyConstraint+"]"); + } else { + return new ResponseCheckResult(false, "json property ["+jsonPropertyPath+"] with ["+propertyVal+"] fulfils constraint ["+jsonPropertyConstraint+"]"); + } + } + + private Object getJsonProperty(JSONParser jsonParser, String jsonPropertyPath) { + String[] jsonPropertyPathBits = jsonPropertyPath.split("(?=\\.|\\[)"); + Object currentObject = null; + for (int i=0; i < jsonPropertyPathBits.length; i++) { + String jsonPropertyPathBit = jsonPropertyPathBits[i]; + if(jsonPropertyPathBit.startsWith("[")) { + int arrayIndex = Integer.parseInt(jsonPropertyPathBit.substring(1,jsonPropertyPathBit.length()-1)); + if(currentObject==null) { + currentObject = jsonParser.getParsedList(); + } + if(!(currentObject instanceof List)) { + throw new IllegalArgumentException("Path '"+StringUtils.defaultIfEmpty(StringUtils.join(jsonPropertyPathBits, "", 0, i), "")+"' is not a json list"); + } + currentObject = ((List) currentObject).get(arrayIndex); + } else { + String propertyName = jsonPropertyPathBit.startsWith(".") ? jsonPropertyPathBit.substring(1) : jsonPropertyPathBit; + if(currentObject==null) { + currentObject = jsonParser.getParsed(); + } + if(!(currentObject instanceof Map)) { + throw new IllegalArgumentException("Path '"+StringUtils.defaultIfEmpty(StringUtils.join(jsonPropertyPathBits, "", 0, i), "")+"' is not a json object"); + } + currentObject = ((Map) currentObject).get(propertyName); + } + if(currentObject==null && /* not last */ i+1 < jsonPropertyPathBits.length) { + throw new IllegalArgumentException("Path "+StringUtils.join(jsonPropertyPathBits, "", 0, i+1)+" is null, cannot evaluate left-over part '"+StringUtils.join(jsonPropertyPathBits, "", i+1, jsonPropertyPathBits.length)+"'"); + } + } + return currentObject; + } + } + +} diff --git a/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/JmxAttributeCheck.java b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/JmxAttributeCheck.java new file mode 100644 index 00000000000..03e1b6d9477 --- /dev/null +++ b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/JmxAttributeCheck.java @@ -0,0 +1,187 @@ +/* + * 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 SF 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.hc.generalchecks; + +import java.lang.management.ManagementFactory; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.hc.api.FormattingResultLog; +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.ResultLog; +import org.apache.felix.hc.generalchecks.util.SimpleConstraintChecker; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** {@link HealthCheck} that checks one (or multiple) JMX attribute(s). */ +@Component(service = HealthCheck.class, configurationPolicy = ConfigurationPolicy.REQUIRE) +@Designate(ocd = JmxAttributeCheck.Config.class, factory = true) +public class JmxAttributeCheck implements HealthCheck { + + private static final Logger LOG = LoggerFactory.getLogger(JmxAttributeCheck.class); + + public static final String HC_NAME = "JMX Attribute"; + public static final String HC_LABEL = "Health Check: " + HC_NAME; + + private Result.Status statusForFailedContraint; + + @ObjectClassDefinition(name = HC_LABEL, description = "Checks the value of a single JMX attribute.") + @interface Config { + + @AttributeDefinition(name = "Name", description = "Name of this health check.") + String hc_name() default HC_NAME; + + @AttributeDefinition(name = "Tags", description = "List of tags for this health check, used to select subsets of health checks for execution e.g. by a composite health check.") + String[] hc_tags() default {}; + + @AttributeDefinition(name = "MBean Name", description = "The name of the MBean to retrieve the attribute to be checked from.") + String mbean_name() default ""; + + @AttributeDefinition(name = "Attribute Name", description = "The name of the MBean attribute to check against the constraint.") + String attribute_name() default ""; + + @AttributeDefinition(name = "Attribute Constraint", description = "Constraint on the MBean attribute value. If simple value, uses equals. For strings constraints like 'CONTAINS mystr', 'STARTS_WITH mystr' or 'ENDS_WITH mystr' can be used, for numbers constraints like '> 4', '= 7', '< 9' or 'between 3 and 7' work.") + String attribute_value_constraint() default ""; + + @AttributeDefinition(name = "Status for failed constraint", description = "Status to fail with if the constraint check fails") + Result.Status statusForFailedContraint() default Result.Status.WARN; + + @AttributeDefinition + String webconsole_configurationFactory_nameHint() default "JMX MBean {mbean.name} Attribute '{attribute.name}' constraint: {attribute.value.constraint}"; + } + + private List attributeConstraintConfigs; + + @Activate + protected void activate(final Config config, final Map rawConfig) { + statusForFailedContraint = config.statusForFailedContraint(); + attributeConstraintConfigs = AttributeConstraintConfig.load(config, rawConfig); + + LOG.info("Activated JMX Attribute HC with statusForFailedContraint={} and attribute constraint config(s):", statusForFailedContraint); + for (AttributeConstraintConfig attributeConstraintConfig : attributeConstraintConfigs) { + LOG.info(attributeConstraintConfig.toString()); + } + } + + + @Override + public Result execute() { + FormattingResultLog resultLog = new FormattingResultLog(); + for (AttributeConstraintConfig attributeConstraintConfig : attributeConstraintConfigs) { + checkAttributeConstraint(resultLog, attributeConstraintConfig); + } + return new Result(resultLog); + } + + private void checkAttributeConstraint(final FormattingResultLog resultLog, AttributeConstraintConfig attributeConstraintConfig) { + resultLog.debug("Checking {}", attributeConstraintConfig); + try { + final MBeanServer jmxServer = ManagementFactory.getPlatformMBeanServer(); + final ObjectName objectName = new ObjectName(attributeConstraintConfig.mbeanName); + if (jmxServer.queryNames(objectName, null).size() == 0) { + resultLog.warn("MBean not found: {}", objectName); + } else { + final Object value = jmxServer.getAttribute(objectName, attributeConstraintConfig.attributeName); + resultLog.debug("{} {} returns {}", attributeConstraintConfig.mbeanName, attributeConstraintConfig.attributeName, value); + boolean matches = new SimpleConstraintChecker().check(value, attributeConstraintConfig.attributeValueConstraint); + String baseMsg = "JMX attribute "+attributeConstraintConfig.mbeanName+" -> '"+attributeConstraintConfig.attributeName+"': Value [" + value + "] "; + if (matches) { + resultLog.add(new ResultLog.Entry(Result.Status.OK, baseMsg+"matches constraint [" + attributeConstraintConfig.attributeValueConstraint + "]")); + } else { + resultLog.add(new ResultLog.Entry( statusForFailedContraint, baseMsg+"does not match constraint [" + attributeConstraintConfig.attributeValueConstraint + "]")); + } + } + } catch (Exception e) { + LOG.warn("JMX check {} failed: {}", attributeConstraintConfig, e.getMessage(), e); + resultLog.healthCheckError("JMX attribute check failed: {}", attributeConstraintConfig, e); + } + } + + + private static class AttributeConstraintConfig { + + public static final String PROP_MBEAN = "mbean"; + public static final String PROP_ATTRIBUTE = "attribute"; + + public static final String SUFFIX_NAME = ".name"; + public static final String SUFFIX_VALUE_CONSTRAINT = ".value.constraint"; + + private static List load(final Config config, final Map rawConfig) { + List attributeConstraintConfigs = new ArrayList(); + + // first attribute via metatype + attributeConstraintConfigs.add(new AttributeConstraintConfig(config.mbean_name(), config.attribute_name(),config.attribute_value_constraint())); + + // additional attributes possible via naming scheme "mbean2.name" / "attribute2.name" ... + int attributeCounter = 2; + while(AttributeConstraintConfig.hasConfig(rawConfig, attributeCounter)) { + attributeConstraintConfigs.add(new AttributeConstraintConfig(rawConfig, attributeCounter)); + attributeCounter++; + } + return attributeConstraintConfigs; + } + + private static String getAttributePropName(int attributeCounter) { + return PROP_ATTRIBUTE + attributeCounter + SUFFIX_NAME; + } + + private static boolean hasConfig(Map rawConfig, int attributeCounter) { + return rawConfig.containsKey(getAttributePropName(attributeCounter)); + } + + final String mbeanName; + + final String attributeName; + final String attributeValueConstraint; + + public AttributeConstraintConfig(String mbeanName, String attributeName, String attributeValueConstraint) { + this.mbeanName = mbeanName; + this.attributeName = attributeName; + this.attributeValueConstraint = attributeValueConstraint; + } + + public AttributeConstraintConfig(Map rawConfig, int attributeCounter) { + String propNameAttribute = getAttributePropName(attributeCounter); + String defaultMBeanName = (String) rawConfig.get(PROP_MBEAN + SUFFIX_NAME); + String mBeanName = (String) rawConfig.get(PROP_MBEAN + attributeCounter + SUFFIX_NAME); + this.mbeanName = StringUtils.defaultIfBlank(mBeanName, defaultMBeanName); + this.attributeName = (String) rawConfig.get(propNameAttribute); + this.attributeValueConstraint = (String) rawConfig.get(PROP_ATTRIBUTE + attributeCounter + SUFFIX_VALUE_CONSTRAINT); + if(StringUtils.isAnyBlank(mbeanName, attributeName, attributeValueConstraint)) { + throw new IllegalArgumentException("Invalid JmxAttributeCheck config for property "+mbeanName+" -> "+propNameAttribute+": "+toString()); + } + } + + @Override + public String toString() { + return "JMX attribute "+mbeanName+" -> '"+attributeName+"': Constraint: "+attributeValueConstraint; + }; + } +} diff --git a/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/MemoryCheck.java b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/MemoryCheck.java new file mode 100644 index 00000000000..40e649de5db --- /dev/null +++ b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/MemoryCheck.java @@ -0,0 +1,99 @@ +/* + * 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 SF 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.hc.generalchecks; + +import static org.apache.felix.hc.api.FormattingResultLog.bytesHumanReadable; + +import org.apache.felix.hc.annotation.HealthCheckService; +import org.apache.felix.hc.api.FormattingResultLog; +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.ResultLog.Entry; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@HealthCheckService(name = MemoryCheck.HC_NAME) +@Component(configurationPolicy = ConfigurationPolicy.REQUIRE) +@Designate(ocd = MemoryCheck.Config.class, factory = false) +public class MemoryCheck implements HealthCheck { + + private static final Logger LOG = LoggerFactory.getLogger(MemoryCheck.class); + + public static final String HC_NAME = "Memory"; + public static final String HC_LABEL = "Health Check: " + HC_NAME; + + @ObjectClassDefinition(name = HC_LABEL, description = "Checks for high CPU load") + public @interface Config { + @AttributeDefinition(name = "Name", description = "Name of this health check") + String hc_name() default HC_NAME; + + @AttributeDefinition(name = "Tags", description = "List of tags for this health check, used to select subsets of health checks for execution e.g. by a composite health check.") + String[] hc_tags() default {}; + + @AttributeDefinition(name = "Heap used threshold for WARN", description = "in percent, if heap usage is over this limit the result is WARN") + long heapUsedPercentageThresholdWarn() default 90; + + @AttributeDefinition(name = "Heap used threshold for CRITICAL", description = "in percent, if heap usage is over this limit the result is CRITICAL") + long heapUsedPercentageThresholdCritical() default 99; + } + + private long heapUsedPercentageThresholdWarn; + private long heapUsedPercentageThresholdCritical; + + @Activate + protected void activate(final Config config) { + heapUsedPercentageThresholdWarn = config.heapUsedPercentageThresholdWarn(); + heapUsedPercentageThresholdCritical = config.heapUsedPercentageThresholdCritical(); + LOG.info("Activated Memory HC: heapUsedPercentageThresholdWarn={}% heapUsedPercentageThresholdCritical={}%", heapUsedPercentageThresholdWarn, heapUsedPercentageThresholdCritical); + } + + @Override + public Result execute() { + FormattingResultLog log = new FormattingResultLog(); + + Runtime runtime = Runtime.getRuntime(); + + long freeMemory = runtime.freeMemory(); + log.debug("Free memory: {}", bytesHumanReadable(freeMemory)); + long currentlyAllocatedByJVM = runtime.totalMemory(); + log.debug("Currently allocated memory: {}", bytesHumanReadable(currentlyAllocatedByJVM)); + long usedMemory = currentlyAllocatedByJVM - freeMemory; + log.debug("Used memory: {}", bytesHumanReadable(usedMemory)); + long maxMemoryAvailableToJVM = runtime.maxMemory(); + + double memoryUsedPercentage = ((double) usedMemory / maxMemoryAvailableToJVM * 100d); + + Result.Status status = + memoryUsedPercentage < this.heapUsedPercentageThresholdWarn ? Result.Status.OK : + memoryUsedPercentage < this.heapUsedPercentageThresholdCritical ? Result.Status.WARN + : Result.Status.CRITICAL; + + String message = String.format("Memory Usage: %.1f%% of %s maximal heap used", memoryUsedPercentage, bytesHumanReadable(maxMemoryAvailableToJVM)); + + log.add(new Entry(status, message)); + + return new Result(log); + } + +} diff --git a/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/ScriptedHealthCheck.java b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/ScriptedHealthCheck.java new file mode 100644 index 00000000000..6e172a87da1 --- /dev/null +++ b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/ScriptedHealthCheck.java @@ -0,0 +1,124 @@ +/* + * 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 SF 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.hc.generalchecks; + +import javax.script.ScriptEngine; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.hc.api.FormattingResultLog; +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.generalchecks.util.ScriptEnginesTracker; +import org.apache.felix.hc.generalchecks.util.ScriptHelper; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** {@link HealthCheck} that runs an arbitrary script. */ +@Component(service = HealthCheck.class, configurationPolicy = ConfigurationPolicy.REQUIRE) +@Designate(ocd = ScriptedHealthCheck.Config.class, factory = true) +public class ScriptedHealthCheck implements HealthCheck { + + private static final Logger LOG = LoggerFactory.getLogger(ScriptedHealthCheck.class); + + public static final String HC_LABEL = "Health Check: Script"; + + @ObjectClassDefinition(name = HC_LABEL, description = "Runs an arbitrary script in given scriping language (via javax.script). " + + "The script has the following default bindings available: 'log', 'scriptHelper' and 'bundleContext'. " + + "'log' is an instance of org.apache.felix.hc.api.FormattingResultLog and is used to define the result of the HC. " + + "'scriptHelper.getService(classObj)' can be used as shortcut to retrieve a service." + + "'scriptHelper.getServices(classObj, filter)' used to retrieve multiple services for a class using given filter. " + + "For all services retrieved via scriptHelper, unget() is called automatically at the end of the script execution." + + "'bundleContext' is available for advanced use cases. The script does not need to return any value, but if it does and it is " + + "a org.apache.felix.hc.api.Result, that result and entries in 'log' are combined then).") + @interface Config { + + @AttributeDefinition(name = "Name", description = "Name of this health check.") + String hc_name() default "Scripted Health Check"; + + @AttributeDefinition(name = "Tags", description = "List of tags for this health check, used to select subsets of health checks for execution e.g. by a composite health check.") + String[] hc_tags() default {}; + + @AttributeDefinition(name = "Language", description = "The language the script is written in. To use e.g. 'groovy', ensure osgi bundle 'groovy-all' is available.") + String language() default "groovy"; + + @AttributeDefinition(name = "Script", description = "The script itself (either use 'script' or 'scriptUrl').") + String script() default "log.info('ok'); log.warn('not so good'); log.critical('bad') // minimal example"; + + @AttributeDefinition(name = "Script Url", description = "Url to the script to be used as alternative source (either use 'script' or 'scriptUrl').") + String scriptUrl() default ""; + + @AttributeDefinition + String webconsole_configurationFactory_nameHint() default "Scripted HC: {hc.name} (tags: {hc.tags}) {scriptUrl} language: {language}"; + } + + private String language; + private String script; + private String scriptUrl; + + private BundleContext bundleContext; + + @Reference + private ScriptEnginesTracker scriptEnginesTracker; + + private ScriptHelper scriptHelper = new ScriptHelper(); + + @Activate + protected void activate(BundleContext context, Config config) { + this.bundleContext = context; + this.language = config.language().toLowerCase(); + this.script = config.script(); + this.scriptUrl = config.scriptUrl(); + + if(StringUtils.isNotBlank(script) && StringUtils.isNotBlank(scriptUrl)) { + LOG.info("Both 'script' and 'scriptUrl' (=()) are configured, ignoring 'scriptUrl'", scriptUrl); + scriptUrl = null; + } + + LOG.info("Activated Scripted HC "+config.hc_name()+" with "+ (StringUtils.isNotBlank(script)?"script "+script: "script url "+scriptUrl)); + + } + + @Override + public Result execute() { + FormattingResultLog log = new FormattingResultLog(); + + + boolean urlIsUsed = StringUtils.isBlank(script); + String scriptToExecute = urlIsUsed ? scriptHelper.getFileContents(scriptUrl): script; + log.info("Executing script {} ({} lines)...", (urlIsUsed?scriptUrl:" as configured"), scriptToExecute.split("\n").length); + + try { + ScriptEngine scriptEngine = scriptHelper.getScriptEngine(scriptEnginesTracker, language); + scriptHelper.evalScript(bundleContext, scriptEngine, scriptToExecute, log, null, true); + } catch (Exception e) { + log.healthCheckError("Exception while executing script: "+e, e); + } + + return new Result(log); + } + + +} diff --git a/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/ThreadUsageCheck.java b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/ThreadUsageCheck.java new file mode 100644 index 00000000000..8254ebd0d21 --- /dev/null +++ b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/ThreadUsageCheck.java @@ -0,0 +1,198 @@ +/* + * 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 SF 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.hc.generalchecks; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.felix.hc.annotation.HealthCheckService; +import org.apache.felix.hc.api.FormattingResultLog; +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.ResultLog; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@HealthCheckService(name = ThreadUsageCheck.HC_NAME) +@Component(configurationPolicy = ConfigurationPolicy.REQUIRE) +@Designate(ocd = ThreadUsageCheck.Config.class, factory = false) +public class ThreadUsageCheck implements HealthCheck { + + private static final Logger LOG = LoggerFactory.getLogger(ThreadUsageCheck.class); + + public static final String HC_NAME = "Thread Usage"; + public static final String HC_LABEL = "Health Check: " + HC_NAME; + + @ObjectClassDefinition(name = HC_LABEL, description = "Checks for thread usage and deadlocks") + public @interface Config { + @AttributeDefinition(name = "Name", description = "Name of this health check") + String hc_name() default HC_NAME; + + @AttributeDefinition(name = "Tags", description = "List of tags for this health check, used to select subsets of health checks for execution e.g. by a composite health check.") + String[] hc_tags() default {}; + + @AttributeDefinition(name = "Sample Period", description = "Period to measure usage per thread") + long samplePeriodInMs() default 200; + + @AttributeDefinition(name = "CPU Time Threshold for WARN in %", description = "Will WARN once this threshold is reached in average for all threads. This value is multiplied by number of available cores as reported by Runtime.getRuntime().availableProcessors()") + long cpuPercentageThresholdWarn() default 95; + + } + + private long samplePeriodInMs; + + private long cpuPercentageThresholdWarn; + + @Activate + protected final void activate(Config config) { + this.samplePeriodInMs = config.samplePeriodInMs(); + this.cpuPercentageThresholdWarn = config.cpuPercentageThresholdWarn(); + LOG.info("Activated thread usage HC samplePeriodInMs={}ms cpuPercentageThresholdWarn={}%", samplePeriodInMs, cpuPercentageThresholdWarn); + } + + @Override + public Result execute() { + FormattingResultLog log = new FormattingResultLog(); + + log.debug("Checking threads for exceeding {}% CPU time within time period of {}ms", cpuPercentageThresholdWarn, samplePeriodInMs); + + try { + ThreadMXBean threadMxBean = ManagementFactory.getThreadMXBean(); + + List threadTimeInfos = collectThreadTimeInfos(log, threadMxBean); + + Collections.sort(threadTimeInfos); + + float totalCpuTimePercentage = 0; + for (int i = 0; i < threadTimeInfos.size(); i++) { + + ThreadTimeInfo threadInfo = threadTimeInfos.get(i); + float cpuTimePercentage = ((float) threadInfo.getCpuTime() / ((float) samplePeriodInMs * 1000000)) * 100f; + totalCpuTimePercentage += cpuTimePercentage; + + String msg = String.format("%4.1f", cpuTimePercentage) + "% used by thread \"" + threadInfo.name + "\""; + + // usually just shows the 3 busiest threads. For the case more threads take more than 15% CPU time, up to 10 threads are shown. + // use hcDebug=true to show all threads + if (i < 3 || (i < 10 && cpuTimePercentage > 15)) { + log.info(msg); + } else { + log.debug(msg); + } + } + + int availableProcessors = Runtime.getRuntime().availableProcessors(); + boolean isHighCpuTime = totalCpuTimePercentage > (cpuPercentageThresholdWarn * availableProcessors); + Result.Status status = isHighCpuTime ? Result.Status.WARN : Result.Status.OK; + + double cpuTimePerProcessor = totalCpuTimePercentage / availableProcessors; + String msg = threadTimeInfos.size() + " threads using " + String.format("%.1f", totalCpuTimePercentage) + "% CPU Time (" + + String.format("%.1f", cpuTimePerProcessor) + "% per single core having " + availableProcessors + " cores)"; + if (isHighCpuTime) { + msg += ">" + cpuPercentageThresholdWarn + "% threshold for WARN"; + } + log.add(new ResultLog.Entry(status, msg)); + + checkForDeadlock(log, threadMxBean); + + } catch (Exception e) { + LOG.error("Could not analyse thread usage " + e, e); + log.healthCheckError("Could not analyse thread usage", e); + } + + return new Result(log); + + } + + List collectThreadTimeInfos(FormattingResultLog log, ThreadMXBean threadMxBean) { + + Map threadTimeInfos = new HashMap(); + + long[] allThreadIds = threadMxBean.getAllThreadIds(); + for (long threadId : allThreadIds) { + ThreadTimeInfo threadTimeInfo = new ThreadTimeInfo(); + long threadCpuTimeStart = threadMxBean.getThreadCpuTime(threadId); + threadTimeInfo.start = threadCpuTimeStart; + ThreadInfo threadInfo = threadMxBean.getThreadInfo(threadId); + threadTimeInfo.name = threadInfo != null ? threadInfo.getThreadName() : "Thread id " + threadId + " (name not resolvable)"; + threadTimeInfos.put(threadId, threadTimeInfo); + } + + try { + Thread.sleep(samplePeriodInMs); + } catch (InterruptedException e) { + log.warn("Could not sleep configured samplePeriodInMs={} to gather thread load", samplePeriodInMs); + } + + for (long threadId : allThreadIds) { + ThreadTimeInfo threadTimeInfo = threadTimeInfos.get(threadId); + if (threadTimeInfo == null) { + continue; + } + long threadCpuTimeStop = threadMxBean.getThreadCpuTime(threadId); + threadTimeInfo.stop = threadCpuTimeStop; + } + + List threads = new ArrayList(threadTimeInfos.values()); + + return threads; + } + + void checkForDeadlock(FormattingResultLog log, ThreadMXBean threadMxBean) { + long[] findDeadlockedThreads = threadMxBean.findDeadlockedThreads(); + if (findDeadlockedThreads != null) { + for (long threadId : findDeadlockedThreads) { + log.critical("Thread " + threadMxBean.getThreadInfo(threadId).getThreadName() + " is DEADLOCKED"); + } + } + } + + static class ThreadTimeInfo implements Comparable { + long start; + long stop; + String name; + + long getCpuTime() { + long cpuTime = stop - start; + if (cpuTime < 0) { + cpuTime = 0; + } + return cpuTime; + } + + @Override + public int compareTo(ThreadTimeInfo otherThreadTimeInfo) { + if (otherThreadTimeInfo == null) { + return -1; + } + return (int) (otherThreadTimeInfo.getCpuTime() - this.getCpuTime()); + } + } +} diff --git a/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/util/ScriptEnginesTracker.java b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/util/ScriptEnginesTracker.java new file mode 100644 index 00000000000..3ebd9673017 --- /dev/null +++ b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/util/ScriptEnginesTracker.java @@ -0,0 +1,195 @@ +/* + * 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 SF 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.hc.generalchecks.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.BundleListener; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Simple service to track script engines available via osgi bundles that define META-INF/services/javax.script.ScriptEngineFactory, e.g. like groovy-all. */ +@Component(immediate=true, service = ScriptEnginesTracker.class) +public class ScriptEnginesTracker implements BundleListener { + private static final Logger LOG = LoggerFactory.getLogger(ScriptEnginesTracker.class); + + private static final String ENGINE_FACTORY_SERVICE = "META-INF/services/" + ScriptEngineFactory.class.getName(); + private final Map enginesByLanguage = new ConcurrentHashMap(); + private final Map> languagesByBundle = new ConcurrentHashMap>(); + + /** ServiceTracker for ScriptEngineFactory */ + private BundleContext context; + + @Activate + public void activate(BundleContext context) { + this.context = context; + this.context.addBundleListener(this); + registerInitialScriptEngineFactories(); + } + + @Deactivate + public void deactivate() { + this.context.removeBundleListener(this); + + enginesByLanguage.clear(); + languagesByBundle.clear(); + } + + public ScriptEngine getEngineByLanguage(String language) { + ScriptEngineFactory factory = enginesByLanguage.get(language.toLowerCase()); + if (factory == null) { + return null; + } + + ScriptEngine engine = factory.getScriptEngine(); + return engine; + } + + public Map> getLanguagesByBundle() { + return languagesByBundle; + } + + + public void bundleChanged(BundleEvent event) { + if (event.getType() == BundleEvent.STARTED && event.getBundle().getEntry(ENGINE_FACTORY_SERVICE) != null) { + registerFactories(event.getBundle()); + } else if (event.getType() == BundleEvent.STOPPED) { + unregisterFactories(event.getBundle()); + } + } + + + private void registerInitialScriptEngineFactories() { + Bundle[] bundles = this.context.getBundles(); + for (Bundle bundle : bundles) { + if ( bundle.getState() == Bundle.ACTIVE && bundle.getEntry(ENGINE_FACTORY_SERVICE)!=null) { + registerFactories(bundle); + } + } + } + + private void registerFactories(Bundle bundle) { + List scriptEngineFactoriesForBundle = getScriptEngineFactoriesForBundle(bundle); + for (ScriptEngineFactory scriptEngineFactory : scriptEngineFactoriesForBundle) { + registerFactory(bundle, scriptEngineFactory); + + } + } + + private void unregisterFactories(Bundle bundle) { + List languagesForBundle = languagesByBundle.get(bundle); + if(languagesForBundle != null) { + for (String lang : languagesForBundle) { + ScriptEngineFactory removed = enginesByLanguage.remove(lang); + LOG.info("Removing ScriptEngine {} for language {}", removed, lang); + } + } + } + + @SuppressWarnings("unchecked") + private List getScriptEngineFactoriesForBundle(final Bundle bundle) { + URL url = bundle.getEntry(ENGINE_FACTORY_SERVICE); + InputStream ins = null; + + List scriptEngineFactoriesInBundle = new ArrayList(); + + try { + ins = url.openStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(ins)); + for (String className : getClassNames(reader)) { + try { + Class clazz = (Class) bundle.loadClass(className); + ScriptEngineFactory spi = clazz.newInstance(); + scriptEngineFactoriesInBundle.add(spi); + + } catch (Throwable t) { + LOG.error("Cannot register ScriptEngineFactory {}", className, t); + } + } + + } catch (IOException ioe) { + LOG.warn("Exception while trying to load factories as defined in {}", ENGINE_FACTORY_SERVICE, ioe); + } finally { + closeQuietly(ins); + } + + return scriptEngineFactoriesInBundle; + } + + private void closeQuietly(InputStream ins) { + if (ins != null) { + try { + ins.close(); + } catch (IOException e) { + // ignore + } + } + } + + private void registerFactory(Bundle bundle, final ScriptEngineFactory factory) { + LOG.info("Adding ScriptEngine {}, {} for language {}, {}", + factory.getEngineName(), factory.getEngineVersion(), + factory.getLanguageName(), factory.getLanguageVersion()); + + String scriptLang = factory.getLanguageName().toLowerCase(); + + enginesByLanguage.put(scriptLang, factory); + + List languages = languagesByBundle.get(bundle); + if(languages==null) { + languages = new ArrayList(); + languagesByBundle.put(bundle, languages); + } + languages.add(scriptLang); + } + + + static List getClassNames(BufferedReader reader) throws IOException { + List classNames = new ArrayList(); + String line; + while ((line = reader.readLine()) != null) { + if (!line.startsWith("#") && line.trim().length() > 0) { + int indexOfHash = line.indexOf('#'); + if (indexOfHash >= 0) { + line = line.substring(0, indexOfHash); + } + line = line.trim(); + classNames.add(line); + } + } + return classNames; + } + +} diff --git a/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/util/ScriptHelper.java b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/util/ScriptHelper.java new file mode 100644 index 00000000000..c6b8e7dfc72 --- /dev/null +++ b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/util/ScriptHelper.java @@ -0,0 +1,207 @@ +/* + * 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 SF 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.hc.generalchecks.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.lang.reflect.Array; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.script.Bindings; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptException; +import javax.script.SimpleBindings; +import javax.script.SimpleScriptContext; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.hc.api.FormattingResultLog; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.ResultLog; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; + +/** Script Helper to simplify interaction with scripting engines in OSGi context. Used by ScriptedHealthCheck and can be used by other custom checks that want to allow to evaluate expressions via a scripting engine. */ +public class ScriptHelper { + + public String getFileContents(String url) { + String content; + try { + URLConnection conn = new URL(url).openConnection(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { + content = reader.lines().collect(Collectors.joining("\n")); + } + return content; + }catch(IOException e) { + throw new IllegalArgumentException("Could not read URL "+url+": "+e, e); + } + } + + public ScriptEngine getScriptEngine(ScriptEnginesTracker scriptEnginesTracker, String language) { + ScriptEngine scriptEngine = scriptEnginesTracker.getEngineByLanguage(language); + if(scriptEngine == null) { + throw new IllegalArgumentException("No ScriptEngineFactory found for language "+ language + " (available languages: "+scriptEnginesTracker.getLanguagesByBundle()+")"); + } + return scriptEngine; + } + + public Object evalScript(BundleContext bundleContext, ScriptEngine scriptEngine, String scriptToExecute, FormattingResultLog log, Map additionalBindings, boolean logScriptResult) throws ScriptException, IOException { + + final Bindings bindings = new SimpleBindings(); + final ScriptHelperBinding scriptHelper = new ScriptHelperBinding(bundleContext); + + StringWriter stdout = new StringWriter(); + StringWriter stderr = new StringWriter(); + + bindings.put("scriptHelper", scriptHelper); + bindings.put("osgi", scriptHelper); // also register script helper like in web console script console + bindings.put("log", log); + bindings.put("bundleContext", bundleContext); + if (additionalBindings != null) { + for (Map.Entry additionalBinding : additionalBindings.entrySet()) { + bindings.put(additionalBinding.getKey(), additionalBinding.getValue()); + } + } + + SimpleScriptContext scriptContext = new SimpleScriptContext(); + scriptContext.setBindings(bindings, ScriptContext.ENGINE_SCOPE); + scriptContext.setWriter(stdout); + scriptContext.setErrorWriter(stderr); + + try { + log.debug(scriptToExecute); + Object scriptResult = scriptEngine.eval(scriptToExecute, scriptContext); + appendStreamsToResult(log, stdout, stderr, scriptContext); + + if(scriptResult instanceof Result) { + Result result = (Result) scriptResult; + for(ResultLog.Entry entry: result) { + log.add(entry); + } + } else if(scriptResult != null && logScriptResult){ + log.info("Script result: {}", scriptResult); + } + + return scriptResult; + } finally { + scriptHelper.ungetServices(); + } + } + + + + private void appendStreamsToResult(FormattingResultLog log, StringWriter stdout, StringWriter stderr, SimpleScriptContext scriptContext) + throws IOException { + scriptContext.getWriter().flush(); + String stdoutStr = stdout.toString(); + if(StringUtils.isNotBlank(stdoutStr)) { + log.info("stdout of script: {}", stdoutStr); + } + + scriptContext.getErrorWriter().flush(); + String stderrStr = stderr.toString(); + if(StringUtils.isNotBlank(stderrStr)) { + log.critical("stderr of script: {}", stderrStr); + } + } + + // Script Helper for OSGi available as binding 'scriptHelper' + class ScriptHelperBinding { + + private final BundleContext bundleContext; + private List> references; + private Map services; + + public ScriptHelperBinding(BundleContext bundleContext) { + this.bundleContext = bundleContext; + } + + @SuppressWarnings("unchecked") + public ServiceType getService(Class type) { + ServiceType service = (this.services == null ? null : (ServiceType) this.services.get(type.getName())); + if (service == null) { + final ServiceReference ref = this.bundleContext.getServiceReference(type.getName()); + if (ref != null) { + service = (ServiceType) this.bundleContext.getService(ref); + if (service != null) { + if (this.services == null) { + this.services = new HashMap(); + } + if (this.references == null) { + this.references = new ArrayList>(); + } + this.references.add(ref); + this.services.put(type.getName(), service); + } + } + } + return service; + } + + public T[] getServices(Class serviceType, String filter) throws InvalidSyntaxException { + final ServiceReference[] refs = this.bundleContext.getServiceReferences(serviceType.getName(), filter); + T[] result = null; + if (refs != null) { + final List objects = new ArrayList(); + for (int i = 0; i < refs.length; i++) { + @SuppressWarnings("unchecked") + final T service = (T) this.bundleContext.getService(refs[i]); + if (service != null) { + if (this.references == null) { + this.references = new ArrayList>(); + } + this.references.add(refs[i]); + objects.add(service); + } + } + if (objects.size() > 0) { + @SuppressWarnings("unchecked") + T[] srv = (T[]) Array.newInstance(serviceType, objects.size()); + result = objects.toArray(srv); + } + } + return result; + } + + public void ungetServices() { + if (this.references != null) { + final Iterator> i = this.references.iterator(); + while (i.hasNext()) { + final ServiceReference ref = i.next(); + this.bundleContext.ungetService(ref); + } + this.references.clear(); + } + if (this.services != null) { + this.services.clear(); + } + } + } + +} diff --git a/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/util/SimpleConstraintChecker.java b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/util/SimpleConstraintChecker.java new file mode 100644 index 00000000000..96eaa306b0c --- /dev/null +++ b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/util/SimpleConstraintChecker.java @@ -0,0 +1,147 @@ +/* + * 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 SF 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.hc.generalchecks.util; + +import java.util.Calendar; + +import org.apache.commons.lang3.StringUtils; + +/** Simple check of values against expressions like < N, > N, between two values etc. See the SimpleConstraintCheckerTest for + * examples. */ +public class SimpleConstraintChecker { + + public static final String GREATER_THAN = ">"; + public static final String LESS_THAN = "<"; + public static final String EQUALS = "="; + + public static final String BETWEEN = "between"; + public static final String AND = "and"; + public static final String CONTAINS = "contains"; + public static final String STARTS_WITH = "starts_with"; + public static final String ENDS_WITH = "ends_with"; + public static final String MATCHES = "matches"; // regex + public static final String OLDER_THAN = "older_than"; // for unix timestamps + + /** Check value against expression and report to result + * @param inputValue the value to check + * @param constraint the constraint to check the value against + * @return true if constraint is met + * */ + public boolean check(Object inputValue, String constraint) throws NumberFormatException { + + if(inputValue == null) { + return false; + } + + final String stringValue = inputValue == null ? "" : inputValue.toString(); + + if (StringUtils.isBlank(constraint)) { + throw new IllegalArgumentException("Empty constraint, cannot evaluate"); + } + + String[] parts = constraint.split(" +"); + boolean matches = false; + boolean inverseResult = false; + if(parts[0].equalsIgnoreCase("not")) { + inverseResult = true; + String[] newParts = new String[parts.length - 1]; + System.arraycopy(parts, 1, newParts, 0, newParts.length); + parts = newParts; + } + + if (parts[0].equals(GREATER_THAN) && parts.length == 2) { + long value = Long.valueOf(stringValue).longValue(); + matches = value > Long.valueOf(parts[1]); + + } else if (parts[0].equals(LESS_THAN) && parts.length == 2) { + long value = Long.valueOf(stringValue).longValue(); + matches = value < Long.valueOf(parts[1]); + + } else if (parts[0].equals(EQUALS) && parts.length == 2) { + if(StringUtils.isNumeric(stringValue)) { + long value = Long.valueOf(stringValue).longValue(); + matches = value == Long.valueOf(parts[1]).longValue(); + } else { + matches = stringValue.equals(parts[1]); + } + } else if (parts.length == 4 && BETWEEN.equalsIgnoreCase(parts[0]) && AND.equalsIgnoreCase(parts[2])) { + long value = Long.valueOf(stringValue).longValue(); + long lowerBound = Long.valueOf(parts[1]).longValue(); + long upperBound = Long.valueOf(parts[3]).longValue(); + matches = value > lowerBound && value < upperBound; + + } else if (parts.length > 1 && CONTAINS.equalsIgnoreCase(parts[0])) { + String pattern = StringUtils.join(parts, " ", 1, parts.length); + matches = stringValue.contains(pattern); + } else if (parts.length > 1 && STARTS_WITH.equalsIgnoreCase(parts[0])) { + String pattern = StringUtils.join(parts, " ", 1, parts.length); + matches = stringValue.startsWith(pattern); + } else if (parts.length > 1 && ENDS_WITH.equalsIgnoreCase(parts[0])) { + String pattern = StringUtils.join(parts, " ", 1, parts.length); + matches = stringValue.endsWith(pattern); + } else if (parts.length > 1 && MATCHES.equalsIgnoreCase(parts[0])) { + String pattern = StringUtils.join(parts, " ", 1, parts.length); + matches = stringValue.matches(pattern); + } else if (parts.length > 1 && OLDER_THAN.equalsIgnoreCase(parts[0]) && parts.length == 3) { + int unit = stringToUnit(parts[2]); + long timestamp = Long.valueOf(stringValue).longValue(); + int timeDiff = Integer.valueOf(parts[1]).intValue(); + + Calendar cal = Calendar.getInstance(); + cal.add(unit, -timeDiff); + long compareTimestamp = cal.getTime().getTime(); + + matches = timestamp < compareTimestamp; + + } else { + matches = StringUtils.join(parts, "").equals(stringValue); + } + + boolean result = matches ^ inverseResult; + return result; + + } + + private int stringToUnit(String unitString) { + int unit; + switch(unitString) { + case "ms": + unit = Calendar.MILLISECOND; break; + case "min": + case "minute": + case "minutes": + unit = Calendar.MINUTE; break; + case "h": + case "hour": + case "hours": + unit = Calendar.HOUR; break; + case "d": + case "day": + case "days": + unit = Calendar.DAY_OF_YEAR; break; + case "s": + case "sec": + case "second": + case "seconds": + unit = Calendar.SECOND; break; + default: + throw new IllegalArgumentException("Unexpected unit '"+unitString+"'"); + } + return unit; + } +} \ No newline at end of file diff --git a/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/util/package-info.java b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/util/package-info.java new file mode 100644 index 00000000000..07879ea91ab --- /dev/null +++ b/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/util/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +@Version("2.0.0") +package org.apache.felix.hc.generalchecks.util; + +import org.osgi.annotation.versioning.Version; diff --git a/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/HttpRequestsCheckTest.java b/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/HttpRequestsCheckTest.java new file mode 100644 index 00000000000..6cca68b12bb --- /dev/null +++ b/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/HttpRequestsCheckTest.java @@ -0,0 +1,160 @@ +/* + * 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 SF 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.hc.generalchecks; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doReturn; + +import java.util.HashMap; +import java.util.Iterator; + +import org.apache.felix.hc.api.FormattingResultLog; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.ResultLog.Entry; +import org.apache.felix.hc.generalchecks.HttpRequestsCheck.RequestSpec; +import org.apache.felix.hc.generalchecks.HttpRequestsCheck.ResponseCheck.ResponseCheckResult; +import org.junit.Test; +import org.mockito.Mockito; + +public class HttpRequestsCheckTest { + + FormattingResultLog log = new FormattingResultLog(); + + HttpRequestsCheck.Response simple200HtmlResponse = new HttpRequestsCheck.Response(200, "OK", null, "testbody text", 200); + + @Test + public void testRequestSpecParsing() throws Exception { + + HttpRequestsCheck.RequestSpec requestSpec = new HttpRequestsCheck.RequestSpec("/path/to/page.html"); + assertEquals("/path/to/page.html", requestSpec.url); + assertEquals("GET", requestSpec.method); + assertEquals(new HashMap(), requestSpec.headers); + assertNull(requestSpec.data); + assertNull(requestSpec.user); + assertNull(requestSpec.connectTimeoutInMs); + assertNull(requestSpec.readTimeoutInMs); + assertNull(requestSpec.proxy); + + requestSpec = new HttpRequestsCheck.RequestSpec("-X POST -H \"X-Test: Test\" -d \"{ 1,2,3 }\" -u admin:admin --connect-timeout 4 -m 5 --proxy http://proxy:2000 /path/to/page.html => 201"); + assertEquals("/path/to/page.html", requestSpec.url); + assertEquals("POST", requestSpec.method); + HashMap expectedHeaders = new HashMap(); + expectedHeaders.put("X-Test", "Test"); + expectedHeaders.put("Authorization", "Basic YWRtaW46YWRtaW4="); + assertEquals(expectedHeaders, requestSpec.headers); + assertEquals("{ 1,2,3 }", requestSpec.data); + assertEquals("admin", requestSpec.user); + assertEquals((Integer) 4000, requestSpec.connectTimeoutInMs); + assertEquals((Integer) 5000, requestSpec.readTimeoutInMs); + assertEquals("proxy:2000", requestSpec.proxy.address().toString()); + + } + + @Test + public void testSimpleRequestSpec() throws Exception { + + HttpRequestsCheck.RequestSpec requestSpec = new HttpRequestsCheck.RequestSpec("/path/to/page.html"); + Entry entry = fakeRequestForSpecAndReturnResponse(requestSpec, simple200HtmlResponse); + assertEquals(Result.Status.OK, entry.getStatus()); + + requestSpec = new HttpRequestsCheck.RequestSpec("/path/to/page.html => 200"); + entry = fakeRequestForSpecAndReturnResponse(requestSpec, simple200HtmlResponse); + assertEquals(Result.Status.OK, entry.getStatus()); + + requestSpec = new HttpRequestsCheck.RequestSpec("/path/to/page.html => 401"); + entry = fakeRequestForSpecAndReturnResponse(requestSpec, simple200HtmlResponse); + assertEquals(Result.Status.WARN, entry.getStatus()); + assertThat(entry.getMessage(), containsString("200 (expected 401)")); + } + + @Test + public void testSimpleRequestSpecWithContentCheck() throws Exception { + + HttpRequestsCheck.RequestSpec requestSpec = new HttpRequestsCheck.RequestSpec("/path/to/page.html => 200 && MATCHES (body|other) text"); + Entry entry = fakeRequestForSpecAndReturnResponse(requestSpec, simple200HtmlResponse); + assertEquals(Result.Status.OK, entry.getStatus()); + assertThat(entry.getMessage(), containsString("[200 OK], response matches [(body|other) text]")); + + requestSpec = new HttpRequestsCheck.RequestSpec("/path/to/page.html => 200 && MATCHES special text"); + entry = fakeRequestForSpecAndReturnResponse(requestSpec, simple200HtmlResponse); + assertEquals(Result.Status.WARN, entry.getStatus()); + assertThat(entry.getMessage(), containsString("[200 OK], response does not match [special text]")); + + } + + private Entry fakeRequestForSpecAndReturnResponse(HttpRequestsCheck.RequestSpec requestSpecOrig, HttpRequestsCheck.Response response) throws Exception { + RequestSpec requestSpec = Mockito.spy(requestSpecOrig); + doReturn(response).when(requestSpec).performRequest(anyString(), anyString(), anyInt(), anyInt(), any(FormattingResultLog.class)); + FormattingResultLog resultLog = requestSpec.check("http://localhost:8080", 10000, 10000, Result.Status.WARN, true); + Iterator entryIt = resultLog.iterator(); + Entry lastEntry = null; + while(entryIt.hasNext()) { + lastEntry = entryIt.next(); + } + return lastEntry; + } + + + @Test + public void testJsonConstraint() { + + String testJson = "{\"test\": { \"intProp\": 2, \"arrProp\": [\"test1\",\"test2\",\"test3\",{\"deepProp\": \"deepVal\"}]} }"; + + assertJsonResponse(testJson, "test.intProp = 2", true); + assertJsonResponse(testJson, "test.arrProp[2] = test3", true); + assertJsonResponse(testJson, "test.intProp between 1 and 3", true); + assertJsonResponse(testJson, "test.arrProp[3].deepProp matches deep.*", true); + } + + private void assertJsonResponse(String testJson, String jsonExpression, boolean expectedTrueOrFalse) { + HttpRequestsCheck.JsonPropertyCheck jsonPropertyCheck = new HttpRequestsCheck.JsonPropertyCheck(jsonExpression); + HttpRequestsCheck.Response response = new HttpRequestsCheck.Response(200, "OK", null, testJson, 200); + ResponseCheckResult checkResult = jsonPropertyCheck.checkResponse(response, log); + assertEquals("Expected "+expectedTrueOrFalse + " for expression ["+jsonExpression+"] against json: "+testJson, expectedTrueOrFalse, !checkResult.contraintFailed); + } + + @Test + public void testTimeConstraint() { + assertTimeConstraint(1000, "< 2000", true); + assertTimeConstraint(1000, "between 500 and 2000", true); + } + + private void assertTimeConstraint(long time, String constraint, boolean expectedTrueOrFalse) { + HttpRequestsCheck.ResponseTimeCheck responseTimeCheck = new HttpRequestsCheck.ResponseTimeCheck(constraint); + HttpRequestsCheck.Response response = new HttpRequestsCheck.Response(200, "OK", null, "", time); + ResponseCheckResult checkResult = responseTimeCheck.checkResponse(response, log); + assertEquals("Expected "+expectedTrueOrFalse + " for expression ["+constraint+"] against json: "+time+"ms", expectedTrueOrFalse, !checkResult.contraintFailed); + } + + @Test + public void testSplitArgsRespectingQuotes() throws Exception { + + HttpRequestsCheck.RequestSpec requestSpec = new HttpRequestsCheck.RequestSpec("/page.html"); + String[] args = requestSpec.splitArgsRespectingQuotes("normal1 \"one two three\" normal2 'one two three' -p --words \"w1 w2 w3\""); + assertArrayEquals(new String[] {"normal1", "\"one two three\"", "normal2", "'one two three'", "-p", "--words", "\"w1 w2 w3\""}, args); + } + + +} diff --git a/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/JmxAttributeHealthCheckTest.java b/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/JmxAttributeHealthCheckTest.java new file mode 100644 index 00000000000..b012c673388 --- /dev/null +++ b/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/JmxAttributeHealthCheckTest.java @@ -0,0 +1,56 @@ +/* + * 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 SF 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.hc.generalchecks; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.HashMap; + +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.generalchecks.JmxAttributeCheck; +import org.junit.Test; + +public class JmxAttributeHealthCheckTest { + + static void assertJmxValue(String objectName, String attributeName, String constraint, boolean expected) { + final JmxAttributeCheck hc = new JmxAttributeCheck(); + + final JmxAttributeCheck.Config configuration = mock(JmxAttributeCheck.Config.class); + when(configuration.mbean_name()).thenReturn(objectName); + when(configuration.attribute_name()).thenReturn(attributeName); + when(configuration.attribute_value_constraint()).thenReturn(constraint); + + hc.activate(configuration, new HashMap()); + + final Result r = hc.execute(); + assertEquals("Expected result " + expected, expected, r.isOk()); + } + + @Test + public void testJmxAttributeMatch() { + assertJmxValue("java.lang:type=ClassLoading", "LoadedClassCount", "> 10", true); + } + + @Test + public void testJmxAttributeNoMatch() { + assertJmxValue("java.lang:type=ClassLoading", "LoadedClassCount", "< 10", false); + } + +} diff --git a/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/ScriptedHealthCheckTest.java b/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/ScriptedHealthCheckTest.java new file mode 100644 index 00000000000..34757751fdf --- /dev/null +++ b/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/ScriptedHealthCheckTest.java @@ -0,0 +1,234 @@ +/* + * 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 SF 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.hc.generalchecks; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import java.lang.annotation.Annotation; +import java.net.URL; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; + +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.ResultLog.Entry; +import org.apache.felix.hc.generalchecks.util.ScriptEnginesTracker; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; + +public class ScriptedHealthCheckTest { + + private static final String GROOVY = "Groovy"; + + + @Spy + @InjectMocks + ScriptedHealthCheck scriptedHealthCheck; + + @Spy + private ScriptEnginesTracker scriptEnginesTracker; + + @Mock + private BundleContext bundleContext; + + @Mock + private ServiceReference testServiceReference; + + @Mock + private TestService testService; + + @Before + public void setup() { + initMocks(this); + ScriptEngineManager factory = new ScriptEngineManager(); + // create JavaScript engine + ScriptEngine groovyEngine = factory.getEngineByName(GROOVY); + doReturn(groovyEngine).when(scriptEnginesTracker).getEngineByLanguage(GROOVY.toLowerCase()); + } + + + + @Test + public void testSimpleStatusValues() { + scriptedHealthCheck.activate(bundleContext, new TestConfig(GROOVY, "log.info('good')", "")); + assertEquals(Result.Status.OK, scriptedHealthCheck.execute().getStatus()); + + scriptedHealthCheck.activate(bundleContext, new TestConfig(GROOVY, "log.warn('not so good')", "")); + assertEquals(Result.Status.WARN, scriptedHealthCheck.execute().getStatus()); + + scriptedHealthCheck.activate(bundleContext, new TestConfig(GROOVY, "log.critical('bad')", "")); + assertEquals(Result.Status.CRITICAL, scriptedHealthCheck.execute().getStatus()); + + scriptedHealthCheck.activate(bundleContext, new TestConfig(GROOVY, "log.temporarilyUnavailable('tmp away')", "")); + assertEquals(Result.Status.TEMPORARILY_UNAVAILABLE, scriptedHealthCheck.execute().getStatus()); + + } + + @Test + public void testExceptionInScript() { + scriptedHealthCheck.activate(bundleContext, new TestConfig(GROOVY, "throw new IllegalStateException()", "")); + assertEquals(Result.Status.HEALTH_CHECK_ERROR, scriptedHealthCheck.execute().getStatus()); + } + + @Test + public void testWithScriptUrl() { + URL scriptUrl = getClass().getResource("testHcScript.groovy"); + scriptedHealthCheck.activate(bundleContext, new TestConfig(GROOVY, "", scriptUrl.toString())); + Result result = scriptedHealthCheck.execute(); + assertEquals(Result.Status.OK, result.getStatus()); + + List entries = getEntries(result); + assertEquals(3, entries.size()); + assertEquals("Test Script URL", entries.get(entries.size()-1).getMessage()); + } + + @Test + public void testStdOut() { + scriptedHealthCheck.activate(bundleContext, new TestConfig(GROOVY, "print 'test'", "")); + Result result = scriptedHealthCheck.execute(); + + List entries = getEntries(result); + assertEquals(3, entries.size()); + assertEquals("stdout of script: test", entries.get(entries.size()-1).getMessage()); + } + + @Test + public void testCombinedResult() { + scriptedHealthCheck.activate(bundleContext, new TestConfig(GROOVY, + "import org.apache.felix.hc.api.*\n" + + "log.info('test1')\n" + + "return new Result(Result.Status.WARN, 'warn')","")); + Result result = scriptedHealthCheck.execute(); + assertEquals(Result.Status.WARN, result.getStatus()); + + List entries = getEntries(result); + assertEquals(4, entries.size()); + assertEquals("test1", entries.get(entries.size()-2).getMessage()); + assertEquals("warn", entries.get(entries.size()-1).getMessage()); + } + + @Test + public void testArbitraryResultLogged() { + scriptedHealthCheck.activate(bundleContext, new TestConfig(GROOVY, + "return 'ARBITRARY_RESULT_OBJECT'","")); + Result result = scriptedHealthCheck.execute(); + assertEquals(Result.Status.OK, result.getStatus()); + + List entries = getEntries(result); + assertEquals(3, entries.size()); + assertEquals("Script result: ARBITRARY_RESULT_OBJECT", entries.get(entries.size()-1).getMessage()); + } + + @Test + public void testOsgiBinding() { + + TestService testService = new TestService(); + doReturn(testServiceReference).when(bundleContext).getServiceReference(TestService.class.getName()); + doReturn(testService).when(bundleContext).getService(testServiceReference); + + scriptedHealthCheck.activate(bundleContext, new TestConfig(GROOVY, + "return osgi.getService(org.apache.felix.hc.generalchecks.ScriptedHealthCheckTest.TestService.class)","")); + + Result result = scriptedHealthCheck.execute(); + assertEquals(Result.Status.OK, result.getStatus()); + assertEquals("Script result: TestService", getEntries(result).get(2).getMessage()); + + verify(bundleContext, times(1)).getServiceReference(TestService.class.getName()); + verify(bundleContext, times(1)).getService(testServiceReference); + verify(bundleContext, times(1)).ungetService(testServiceReference); + } + + + private List getEntries(Result execute) { + return StreamSupport.stream(execute.spliterator(), false).collect(Collectors.toList()); + } + + public class TestService { + + @Override + public String toString() { + return "TestService"; + } + + } + + private final class TestConfig implements ScriptedHealthCheck.Config { + + private final String language; + private final String script; + private final String scriptUrl; + + public TestConfig(String language, String script, String scriptUrl) { + super(); + this.language = language; + this.script = script; + this.scriptUrl = scriptUrl; + } + + @Override + public Class annotationType() { + return ScriptedHealthCheck.Config.class; + } + + @Override + public String webconsole_configurationFactory_nameHint() { + throw new UnsupportedOperationException(); + } + + @Override + public String scriptUrl() { + return scriptUrl; + } + + @Override + public String script() { + return script; + } + + @Override + public String language() { + return language; + } + + @Override + public String[] hc_tags() { + return new String[] {"test"}; + } + + @Override + public String hc_name() { + return "Test HC"; + } + } + +} diff --git a/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/util/SimpleConstraintCheckerTest.java b/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/util/SimpleConstraintCheckerTest.java new file mode 100644 index 00000000000..2771b4433ff --- /dev/null +++ b/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/util/SimpleConstraintCheckerTest.java @@ -0,0 +1,201 @@ +/* + * 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 SF 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.hc.generalchecks.util; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Calendar; +import java.util.Date; + +import org.junit.Before; +import org.junit.Test; + +public class SimpleConstraintCheckerTest { + + private final SimpleConstraintChecker checker = new SimpleConstraintChecker(); + + @Before + public void setup() { + } + + @Test + public void testStringEquals() { + final String s = "test_" + System.currentTimeMillis(); + assertTrue(checker.check(s, s)); + } + + @Test + public void testStringNotEquals() { + final String s = "test_" + System.currentTimeMillis(); + assertFalse(checker.check(s, "something else")); + } + + @Test + public void testFiveEquals() { + final String s = "5"; + assertTrue(checker.check(s, s)); + } + + @Test + public void testIntTwelveEquals() { + assertTrue(checker.check(12, "12")); + } + + @Test + public void testIntTwelveGreaterThan() { + assertTrue(checker.check(12, "> 11")); + } + + @Test + public void testFiveNotEquals() { + assertFalse(checker.check("5", "foo")); + } + + @Test + public void testNullNotEquals() { + assertFalse(checker.check(null, "foo")); + } + + + @Test + public void testNumberEquals() { + assertTrue(checker.check("7", "= 7")); + } + + @Test + public void testNullNotGreater() { + assertFalse(checker.check(null, "> 2")); + } + + @Test + public void testGreaterThanTrue() { + assertTrue(checker.check("5", "> 2")); + } + + @Test + public void testGreaterThanFalse() { + assertFalse(checker.check("5", "> 12")); + } + + @Test + public void testLessThanTrue() { + assertTrue(checker.check("5", "< 12")); + } + + @Test + public void testLessThanFalse() { + assertFalse(checker.check("5", "< 2")); + } + + @Test + public void testNot() { + assertTrue(checker.check("5", "not < 2")); + assertFalse(checker.check("5", "not < 6")); + } + + @Test + public void testBetweenA() { + assertTrue(checker.check("5", "between 2 and 6")); + } + + @Test + public void testBetweenB() { + assertFalse(checker.check("5", "between 12 and 16")); + } + + @Test + public void testBetweenC() { + assertFalse(checker.check(5L, "between 12 and 16")); + } + + @Test + public void testBetweenD() { + assertTrue(checker.check(5L, "between 4 and 16")); + } + + @Test + public void testBetweenE() { + assertTrue(checker.check(5L, "betWEEN 4 aND 16")); + } + + @Test(expected=NumberFormatException.class) + public void testNotAnInteger() { + assertFalse(checker.check("foo", "between 12 and 16")); + } + + @Test + public void testContainsA() { + assertTrue(checker.check("This is a NICE STRING ok?", "contains NICE STRING")); + } + + @Test + public void testContainsB() { + assertFalse(checker.check("This is a NICE TOUCH ok?", "contains NICE STRING")); + } + + @Test + public void testContainsC() { + assertTrue(checker.check("This is a NICE TOUCH ok?", "contains NICE")); + } + + @Test + public void testStartsWithA() { + assertTrue(checker.check("This is a NICE TOUCH ok?", "STARTS_WITH This is")); + } + + @Test + public void testStartsWithB() { + assertFalse(checker.check("This is a NICE TOUCH ok?", "STARTS_WITH is")); + } + + @Test + public void testEndsWithA() { + assertTrue(checker.check("This is a NICE TOUCH ok?", "ENDS_WITH TOUCH ok?")); + } + + @Test + public void testEndsWithB() { + assertFalse(checker.check("This is a NICE TOUCH ok?", "ENDS_WITH is")); + } + + @Test + public void testMatches() { + assertTrue(checker.check("testABCtest", "matches .*ABC.*")); + assertFalse(checker.check("testABCtest", "matches .*XYZ.*")); + assertTrue(checker.check("testABCtest", "not matches .*XYZ.*")); + assertTrue(checker.check("2.1.0", "matches ^2\\.[1-9]\\.[0-9]+")); + } + + @Test + public void testOlderThan() { + long timestampNow = new Date().getTime(); + assertFalse(checker.check(new Long(timestampNow), "OLDER_THAN 5 sec")); + + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.MINUTE, -55); + + assertTrue(checker.check(cal.getTime().getTime()+"", "OLDER_THAN 53 min")); + assertFalse(checker.check(cal.getTime().getTime()+"", "OLDER_THAN 57 min")); + assertTrue(checker.check(cal.getTime().getTime()+"", "NOT OLDER_THAN 57 min")); + assertFalse(checker.check(cal.getTime().getTime()+"", "OLDER_THAN 1 hour")); + assertFalse(checker.check(cal.getTime().getTime()+"", "OLDER_THAN 1 days")); + assertTrue(checker.check(cal.getTime().getTime()+"", "OLDER_THAN 100 ms")); + } + +} diff --git a/healthcheck/generalchecks/src/test/resources/org/apache/felix/hc/generalchecks/testHcScript.groovy b/healthcheck/generalchecks/src/test/resources/org/apache/felix/hc/generalchecks/testHcScript.groovy new file mode 100644 index 00000000000..3600ba684ff --- /dev/null +++ b/healthcheck/generalchecks/src/test/resources/org/apache/felix/hc/generalchecks/testHcScript.groovy @@ -0,0 +1,19 @@ +/* + * 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 SF 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. + */ + +log.info('Test Script URL') \ No newline at end of file diff --git a/healthcheck/webconsoleplugin/LICENSE b/healthcheck/webconsoleplugin/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/healthcheck/webconsoleplugin/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/healthcheck/webconsoleplugin/bnd.bnd b/healthcheck/webconsoleplugin/bnd.bnd new file mode 100644 index 00000000000..bf7b7a24413 --- /dev/null +++ b/healthcheck/webconsoleplugin/bnd.bnd @@ -0,0 +1,10 @@ +Bundle-Category: healthcheck + +Bundle-Description: ${project.description} + +Bundle-DocURL: https://felix.apache.org + +Bundle-License: Apache License, Version 2.0 + +Bundle-Vendor: The Apache Software Foundation + diff --git a/healthcheck/webconsoleplugin/pom.xml b/healthcheck/webconsoleplugin/pom.xml new file mode 100644 index 00000000000..790a6278880 --- /dev/null +++ b/healthcheck/webconsoleplugin/pom.xml @@ -0,0 +1,140 @@ + + + + 4.0.0 + + + org.apache.felix + felix-parent + 6 + + + + org.apache.felix.healthcheck.webconsoleplugin + 2.0.1-SNAPSHOT + + Apache Felix Health Check Webconsole Plugin + 2013 + + + Plugin for the felix web console + + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/healthcheck/webconsoleplugin + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/healthcheck/webconsoleplugin + http://svn.apache.org/viewvc/felix/trunk/http/webconsoleplugin/ + + + + + + + biz.aQute.bnd + bnd-maven-plugin + 4.0.0 + + + + bnd-process + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + + + + + + org.apache.felix + org.apache.felix.healthcheck.api + 2.0.0 + + + + org.osgi + osgi.core + 6.0.0 + provided + + + org.osgi + osgi.cmpn + 6.0.0 + provided + + + + javax.servlet + servlet-api + 2.4 + provided + + + org.slf4j + slf4j-api + 1.7.6 + provided + + + + org.apache.commons + commons-lang3 + 3.4 + provided + + + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-core + 1.9.5 + test + + + + + org.apache.felix + org.apache.felix.healthcheck.core + 2.0.0 + test + + + + + + \ No newline at end of file diff --git a/healthcheck/webconsoleplugin/src/main/java/org/apache/felix/hc/webconsole/impl/HealthCheckWebconsolePlugin.java b/healthcheck/webconsoleplugin/src/main/java/org/apache/felix/hc/webconsole/impl/HealthCheckWebconsolePlugin.java new file mode 100644 index 00000000000..a858a29b788 --- /dev/null +++ b/healthcheck/webconsoleplugin/src/main/java/org/apache/felix/hc/webconsole/impl/HealthCheckWebconsolePlugin.java @@ -0,0 +1,295 @@ +/* + * 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 SF 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.hc.webconsole.impl; + +import static org.apache.commons.lang3.StringEscapeUtils.escapeHtml4; +import static org.apache.felix.hc.api.FormattingResultLog.msHumanReadable; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.Collection; + +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.StringUtils; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.ResultLog; +import org.apache.felix.hc.api.execution.HealthCheckExecutionOptions; +import org.apache.felix.hc.api.execution.HealthCheckExecutionResult; +import org.apache.felix.hc.api.execution.HealthCheckExecutor; +import org.apache.felix.hc.api.execution.HealthCheckSelector; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** Webconsole plugin to execute health check services */ +@Component(service=Servlet.class, property={ + org.osgi.framework.Constants.SERVICE_DESCRIPTION + "=Apache Felix Health Check Web Console Plugin", + "felix.webconsole.label=" + HealthCheckWebconsolePlugin.LABEL, + "felix.webconsole.title="+ HealthCheckWebconsolePlugin.TITLE, + "felix.webconsole.category="+ HealthCheckWebconsolePlugin.CATEGORY, + "felix.webconsole.css=/healthcheck/res/ui/healthcheck.css" +}) +@SuppressWarnings("serial") +public class HealthCheckWebconsolePlugin extends HttpServlet { + + public static final String TITLE = "Health Check"; + public static final String LABEL = "healthcheck"; + public static final String CATEGORY = "Main"; + public static final String PARAM_TAGS = "tags"; + public static final String PARAM_DEBUG = "debug"; + public static final String PARAM_QUIET = "quiet"; + + public static final String PARAM_FORCE_INSTANT_EXECUTION = "forceInstantExecution"; + public static final String PARAM_COMBINE_TAGS_WITH_OR = "combineTagsWithOr"; + public static final String PARAM_OVERRIDE_GLOBAL_TIMEOUT = "overrideGlobalTimeout"; + + @Reference + private HealthCheckExecutor healthCheckExecutor; + + /** Serve static resource if applicable, and return true in that case */ + private boolean getStaticResource(final HttpServletRequest req, final HttpServletResponse resp) + throws ServletException, IOException { + final String pathInfo = req.getPathInfo(); + if (pathInfo!= null && pathInfo.contains("res/ui")) { + final String prefix = "/" + LABEL; + final InputStream is = getClass().getResourceAsStream(pathInfo.substring(prefix.length())); + if (is == null) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND, pathInfo); + } else { + final OutputStream os = resp.getOutputStream(); + try { + final byte [] buffer = new byte[16384]; + int n=0; + while( (n = is.read(buffer, 0, buffer.length)) > 0) { + os.write(buffer, 0, n); + } + } finally { + try { + is.close(); + } catch ( final IOException ignore ) { + // ignore + } + } + } + return true; + } + return false; + } + + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) + throws ServletException, IOException { + if (getStaticResource(req, resp)) { + return; + } + + final String tags = getParam(req, PARAM_TAGS, null); + final boolean debug = Boolean.valueOf(getParam(req, PARAM_DEBUG, "false")); + final boolean quiet = Boolean.valueOf(getParam(req, PARAM_QUIET, "false")); + final boolean combineTagsWithOr = Boolean.valueOf(getParam(req, PARAM_COMBINE_TAGS_WITH_OR, "false")); + final boolean forceInstantExecution = Boolean.valueOf(getParam(req, PARAM_FORCE_INSTANT_EXECUTION, "false")); + final String overrideGlobalTimeoutStr = getParam(req, PARAM_OVERRIDE_GLOBAL_TIMEOUT, ""); + + final PrintWriter pw = resp.getWriter(); + doForm(pw, tags, debug, quiet, combineTagsWithOr, forceInstantExecution, overrideGlobalTimeoutStr); + + // Execute health checks only if tags are specified (even if empty) + if (tags != null) { + HealthCheckExecutionOptions options = new HealthCheckExecutionOptions(); + options.setCombineTagsWithOr(combineTagsWithOr); + options.setForceInstantExecution(forceInstantExecution); + try { + options.setOverrideGlobalTimeout(Integer.valueOf(overrideGlobalTimeoutStr)); + } catch (NumberFormatException nfe) { + // override not set in UI + } + + HealthCheckSelector selector = StringUtils.isNotBlank(tags) ? HealthCheckSelector.tags(tags.split(",")) : HealthCheckSelector.empty(); + Collection results = healthCheckExecutor.execute(selector, options); + + pw.println(""); + int total = 0; + int failed = 0; + for (final HealthCheckExecutionResult exR : results) { + + final Result r = exR.getHealthCheckResult(); + total++; + if (!r.isOk()) { + failed++; + } + if (!quiet || !r.isOk()) { + renderResult(pw, exR, debug); + } + + } + final WebConsoleHelper c = new WebConsoleHelper(resp.getWriter()); + c.titleHtml("Summary", total + " HealthCheck executed, " + failed + " failures"); + pw.println("
      "); + pw.println("Configure executor

      "); + + } + } + + void renderResult(final PrintWriter pw, final HealthCheckExecutionResult exResult, final boolean debug) throws IOException { + final Result result = exResult.getHealthCheckResult(); + final WebConsoleHelper c = new WebConsoleHelper(pw); + + final StringBuilder status = new StringBuilder(); + + status.append("Tags: ").append(exResult.getHealthCheckMetadata().getTags()); + status.append(" Finished: ").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(exResult.getFinishedAt()) + " after " + + msHumanReadable(exResult.getElapsedTimeInMs())); + + c.titleHtml(exResult.getHealthCheckMetadata().getTitle(), null); + + c.tr(); + c.tdContent(); + c.writer().print(escapeHtml4(status.toString())); + c.writer().print("
      Result: "); + c.writer().print(result.getStatus().toString()); + c.writer().print(""); + c.closeTd(); + c.closeTr(); + + c.tr(); + c.tdContent(); + for(final ResultLog.Entry e : result) { + if (!debug && e.isDebug()) { + continue; + } + c.writer().print("
      "); + c.writer().print(e.getStatus().toString()); + c.writer().print(' '); + c.writer().print(escapeHtml4(e.getMessage())); + if (e.getException() != null) { + c.writer().print(" "); + c.writer().print(escapeHtml4(e.getException().toString())); + } + c.writer().println("
      "); + } + c.closeTd(); + } + + private void doForm(final PrintWriter pw, + final String tags, + final boolean debug, + final boolean quiet, + final boolean combineTagsWithOr, + final boolean forceInstantExecution, + final String overrideGlobalTimeoutStr) + throws IOException { + final WebConsoleHelper c = new WebConsoleHelper(pw); + pw.print("
      "); + pw.println(""); + c.titleHtml(TITLE, "Enter tags to selected health checks to be executed. Leave empty to execute default checks or use '*' to execute all checks." + + " Prefix a tag with a minus sign (-) to omit checks having that tag (can be also used in combination with '*', e.g. '*,-excludedtag')."); + + c.tr(); + c.tdLabel("Health Check tags (comma-separated)"); + c.tdContent(); + c.writer().print(""); + c.closeTd(); + c.closeTr(); + + c.tr(); + c.tdLabel("Combine tags with logical 'OR' instead of the default 'AND'"); + c.tdContent(); + c.writer().print(""); + c.closeTd(); + c.closeTr(); + + c.tr(); + c.tdLabel("Show DEBUG logs"); + c.tdContent(); + c.writer().print(""); + c.closeTd(); + c.closeTr(); + + c.tr(); + c.tdLabel("Show failed checks only"); + c.tdContent(); + c.writer().print(""); + c.closeTd(); + c.closeTr(); + + c.tr(); + c.tdLabel("Force instant execution (no cache, async checks are executed)"); + c.tdContent(); + c.writer().print(""); + c.closeTd(); + c.closeTr(); + + c.tr(); + c.tdLabel("Override global timeout"); + c.tdContent(); + c.writer().print(""); + c.closeTd(); + c.closeTr(); + + c.tr(); + c.tdContent(); + c.writer().println(""); + c.closeTd(); + c.closeTr(); + + c.writer().println("
      "); + } + + private String getParam(final HttpServletRequest req, final String name, final String defaultValue) { + String result = req.getParameter(name); + if(result == null) { + result = defaultValue; + } + return result; + } +} diff --git a/healthcheck/webconsoleplugin/src/main/java/org/apache/felix/hc/webconsole/impl/WebConsoleHelper.java b/healthcheck/webconsoleplugin/src/main/java/org/apache/felix/hc/webconsole/impl/WebConsoleHelper.java new file mode 100644 index 00000000000..6ffeeeb409d --- /dev/null +++ b/healthcheck/webconsoleplugin/src/main/java/org/apache/felix/hc/webconsole/impl/WebConsoleHelper.java @@ -0,0 +1,74 @@ +/* + * 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 SF 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.hc.webconsole.impl; + +import java.io.PrintWriter; + +import static org.apache.commons.lang3.StringEscapeUtils.escapeHtml4; + +/** Webconsole plugin helper for writing html. */ +class WebConsoleHelper { + + final PrintWriter pw; + + WebConsoleHelper(final PrintWriter w) { + pw = w; + } + + PrintWriter writer() { + return pw; + } + + void tdContent() { + pw.print(""); + } + + void closeTd() { + pw.print(""); + } + + void closeTr() { + pw.println(""); + } + + void tdLabel(final String label) { + pw.print(""); + pw.print(escapeHtml4(label)); + pw.println(""); + } + + void tr() { + pw.println(""); + } + + void titleHtml(String title, String description) { + tr(); + pw.print(""); + pw.print(escapeHtml4(title)); + pw.println(""); + closeTr(); + + if (description != null) { + tr(); + pw.print(""); + pw.print(escapeHtml4(description)); + pw.println(""); + closeTr(); + } + } +} diff --git a/healthcheck/webconsoleplugin/src/main/resources/res/ui/healthcheck.css b/healthcheck/webconsoleplugin/src/main/resources/res/ui/healthcheck.css new file mode 100644 index 00000000000..75b79cfa29c --- /dev/null +++ b/healthcheck/webconsoleplugin/src/main/resources/res/ui/healthcheck.css @@ -0,0 +1,46 @@ +/* + * 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. + */ + +.healthcheck .logDEBUG { + color:grey; +} + +.healthcheck .logINFO { + color:blue; +} + +.healthcheck .logWARN, +.healthcheck .logCRITICAL, +.healthcheck .logHEALTH_CHECK_ERROR +{ + color:red; +} + +.healthcheck .logERROR { + color:red; + font-weight:bold; +} + +.healthcheck .resultOktrue { + color:green; + font-weight:bold; +} + +.healthcheck .resultOkfalse { + color:red; + font-weight:bold; +} diff --git a/healthcheck/webconsoleplugin/src/test/java/org/apache/felix/hc/webconsole/impl/HealthCheckWebconsolePluginTest.java b/healthcheck/webconsoleplugin/src/test/java/org/apache/felix/hc/webconsole/impl/HealthCheckWebconsolePluginTest.java new file mode 100644 index 00000000000..42d30da39aa --- /dev/null +++ b/healthcheck/webconsoleplugin/src/test/java/org/apache/felix/hc/webconsole/impl/HealthCheckWebconsolePluginTest.java @@ -0,0 +1,95 @@ +/* + * 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 SF 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.hc.webconsole.impl; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.MockitoAnnotations.initMocks; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.apache.felix.hc.api.FormattingResultLog; +import org.apache.felix.hc.api.HealthCheck; +import org.apache.felix.hc.api.Result; +import org.apache.felix.hc.api.execution.HealthCheckMetadata; +import org.apache.felix.hc.core.impl.executor.ExecutionResult; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; + +public class HealthCheckWebconsolePluginTest { + + @Mock + ServiceReference serviceRef; + + private HealthCheckWebconsolePlugin webConsolePlugin = new HealthCheckWebconsolePlugin(); + + + @Before + public void setup() { + initMocks(this); + + doReturn("Test HC ").when(serviceRef).getProperty(HealthCheck.NAME); + doReturn(1L).when(serviceRef).getProperty(Constants.SERVICE_ID); + + } + + + @Test + public void testRenderResultNoDebug() throws IOException { + + StringWriter writer = new StringWriter(); + + webConsolePlugin.renderResult(new PrintWriter(writer), getExecutionResult(), false); + + String resultStr = writer.toString(); + assertThat(resultStr, containsString("Test HC <should-be-escaped>")); + assertThat(resultStr, containsString("
      OK HC log with level 'info'
      ")); + assertThat(resultStr, not(containsString("
      OK HC log with level 'debug'
      "))); + + } + + @Test + public void testRenderResultWithDebug() throws IOException { + + StringWriter writer = new StringWriter(); + + webConsolePlugin.renderResult(new PrintWriter(writer), getExecutionResult(), true); + + String resultStr = writer.toString(); + assertThat(resultStr, containsString("
      OK HC log with level 'debug'
      ")); + + } + + private ExecutionResult getExecutionResult() { + FormattingResultLog resultLog = new FormattingResultLog(); + resultLog.info("HC log with level 'info'"); + resultLog.debug("HC log with level 'debug'"); + + HealthCheckMetadata metadata = new HealthCheckMetadata(serviceRef); + Result result = new Result(resultLog); + return new ExecutionResult(metadata, result, 777); + } + +} diff --git a/http.jetty/pom.xml b/http.jetty/pom.xml deleted file mode 100644 index 0b1002de499..00000000000 --- a/http.jetty/pom.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - org.apache.felix - felix - 0.8.0-SNAPSHOT - - 4.0.0 - osgi-bundle - Apache Felix HTTP Service - org.apache.felix.http.jetty - - - ${pom.groupId} - org.osgi.core - ${pom.version} - provided - - - ${pom.groupId} - org.osgi.compendium - ${pom.version} - provided - - - tomcat - servlet - 4.0.6 - - - jetty - org.mortbay.jetty-jdk1.2 - 4.2.25 - - - - - - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} - true - - - com.sun.net.ssl.internal.ssl, - com.sun.net.ssl, - org.xml.sax, - org.xml.sax.helpers, - javax.xml.parsers, - javax.security.cert - - - HTTP Service - An implementation of the OSGi HTTP Service using Jetty. - auto-detect - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/ - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}.jar - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}-src.jar - ${pom.artifactId} - javax.net.ssl - org.osgi.service.http; version=1.1,javax.servlet;javax.servlet.http;version=2.3.0 - org.osgi.service.http.HttpService - - - - - - diff --git a/http.jetty/src/main/java/org/apache/felix/http/jetty/Activator.java b/http.jetty/src/main/java/org/apache/felix/http/jetty/Activator.java deleted file mode 100644 index 76467cdd468..00000000000 --- a/http.jetty/src/main/java/org/apache/felix/http/jetty/Activator.java +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.http.jetty; - -import java.lang.reflect.Constructor; - -import org.mortbay.http.HashUserRealm; -import org.mortbay.http.HttpServer; -import org.mortbay.http.SocketListener; -import org.mortbay.util.Code; -import org.mortbay.util.InetAddrPort; -import org.osgi.framework.*; -import org.osgi.service.http.HttpService; -import org.mortbay.http.SunJsseListener; -import org.mortbay.http.JsseListener; - -/** - * Basic implementation of OSGi HTTP service 1.1. - * - * TODO: - * - * - fuller suite of testing and compatibility tests - * - * - only exposed params are those defined in the OSGi spec. Jetty is - * very tunable via params, some of which it may be useful to expose - * - * - no cacheing is performed on delivered resources. Although not part - * of the OSGi spec, it also isn't precluded and would enhance - * performance in a high usage environment. Jetty's ResourceHandler - * class could be a model for this. - * - * - scanning the Jetty ResourceHandler class it's clear that there are - * many other sophisticated areas to do with resource handling such - * as checking date and range fields in the http headers. It's not clear - * whether any of these play a part in the OSGi service - the spec - * just describes "returning the contents of the URL to the client" which - * doesn't state what other HTTP handling might be compliant or desirable - */ -public class Activator implements BundleActivator -{ - protected static boolean debug = false; - - private BundleContext m_bundleContext = null; - private ServiceRegistration m_svcReg = null; - private HttpServiceFactory m_httpServ = null; - private HttpServer m_server = null; - - private int m_httpPort; - private int m_httpsPort; - - - public void start(BundleContext bundleContext) - throws BundleException - { - m_bundleContext = bundleContext; - - // org.mortbay.util.Loader needs this (used for JDK 1.4 log classes) - Thread.currentThread().setContextClassLoader( - this.getClass().getClassLoader()); - - String optDebug = - m_bundleContext.getProperty("org.apache.felix.http.jetty.debug"); - if (optDebug != null && optDebug.toLowerCase().equals("true")) - { - Code.setDebug(true); - debug = true; - } - - // get default HTTP and HTTPS ports as per the OSGi spec - try - { - m_httpPort = Integer.parseInt(m_bundleContext.getProperty( - "org.osgi.service.http.port")); - } - catch (Exception e) - { - // maybe log a message saying using default? - m_httpPort = 80; - } - - try - { - // TODO: work out how/when we should use the HTTPS port - m_httpsPort = Integer.parseInt(m_bundleContext.getProperty( - "org.osgi.service.http.port.secure")); - } - catch (Exception e) - { - // maybe log a message saying using default? - m_httpsPort = 443; - } - - try - { - initializeJetty(); - - } catch (Exception ex) { - //TODO: maybe throw a bundle exception in here? - System.out.println("Http2: " + ex); - return; - } - - m_httpServ = new HttpServiceFactory(); - m_svcReg = m_bundleContext.registerService( - HttpService.class.getName(), m_httpServ, null); - } - - - public void stop(BundleContext bundleContext) - throws BundleException - { - //TODO: wonder if we need to closedown service factory ??? - - if (m_svcReg != null) - { - m_svcReg.unregister(); - } - - try - { - m_server.stop(); - } - catch (Exception e) - { - //TODO: log some form of error - } - } - - protected void initializeJetty() - throws Exception - { - //TODO: Maybe create a separate "JettyServer" object here? - // Realm - HashUserRealm realm = - new HashUserRealm("OSGi HTTP Service Realm"); - - // Create server - m_server = new HttpServer(); - m_server.addRealm(realm); - - // Add a regular HTTP listener - SocketListener listener = null; - listener = (SocketListener) - m_server.addListener(new InetAddrPort(m_httpPort)); - listener.setMaxIdleTimeMs(60000); - - // See if we need to add an HTTPS listener - String enableHTTPS = m_bundleContext.getProperty("org.ungoverned.osgi.bundle.https.enable"); - if (enableHTTPS != null && enableHTTPS.toLowerCase().equals("true")) - { - initializeHTTPS(); - } - - m_server.start(); - } - - //TODO: Just a basic implementation to give us a working HTTPS port. A better - // long-term solution may be to separate out the SSL provider handling, - // keystore, passwords etc. into it's own pluggable service - protected void initializeHTTPS() - throws Exception - { - String sslProvider = m_bundleContext.getProperty("org.ungoverned.osgi.bundle.https.provider"); - if (sslProvider == null) - { - sslProvider = "org.mortbay.http.SunJsseListener"; - } - - // Set default jetty properties for supplied values. For any not set, - // Jetty will fallback to checking system properties. - String keystore = m_bundleContext.getProperty("org.ungoverned.osgi.bundle.https.keystore"); - if (keystore != null) - { - System.setProperty(JsseListener.KEYSTORE_PROPERTY, keystore); - } - - String passwd = m_bundleContext.getProperty("org.ungoverned.osgi.bundle.https.password"); - if (passwd != null) - { - System.setProperty(JsseListener.PASSWORD_PROPERTY, passwd); - } - - String keyPasswd = m_bundleContext.getProperty("org.ungoverned.osgi.bundle.https.key.password"); - if (keyPasswd != null) - { - System.setProperty(JsseListener.KEYPASSWORD_PROPERTY, keyPasswd); - } - - //SunJsseListener s_listener = new SunJsseListener(new InetAddrPort(m_httpsPort)); - Object args[] = { new InetAddrPort(m_httpsPort) }; - Class argTypes[] = { args[0].getClass() }; - Class clazz = Class.forName(sslProvider); - Constructor cstruct = clazz.getDeclaredConstructor(argTypes); - JsseListener s_listener = (JsseListener) cstruct.newInstance(args); - - m_server.addListener(s_listener); - s_listener.setMaxIdleTimeMs(60000); - } - - - protected static void debug(String txt) - { - if (debug) - { - System.err.println(">>Oscar HTTP: " + txt); - } - } - - // Inner class to provide basic service factory functionality - - public class HttpServiceFactory implements ServiceFactory - { - public HttpServiceFactory() - { - // Initialize the statics for the service implementation. - HttpServiceImpl.initializeStatics(); - } - - public Object getService(Bundle bundle, - ServiceRegistration registration) - { - Object srv = new HttpServiceImpl(bundle, m_server); - debug("** http service get:" + bundle + ", service: " + srv); - return srv; - } - - public void ungetService(Bundle bundle, - ServiceRegistration registration, Object service) - { - debug("** http service unget:" + bundle + ", service: " - + service); - ((HttpServiceImpl) service).unregisterAll(); - } - } - -} \ No newline at end of file diff --git a/http.jetty/src/main/java/org/apache/felix/http/jetty/DefaultContextImpl.java b/http.jetty/src/main/java/org/apache/felix/http/jetty/DefaultContextImpl.java deleted file mode 100644 index c8fbc224a7f..00000000000 --- a/http.jetty/src/main/java/org/apache/felix/http/jetty/DefaultContextImpl.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.http.jetty; - -import java.net.URL; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.osgi.framework.Bundle; -import org.osgi.service.http.HttpContext; - -/** - * Implementation of default HttpContext as per OSGi specification. - * - * Notes - * - * - no current inclusion/support for permissions - * - security allows all request. Spec leaves security handling to be - * implementation specific, but does outline some suggested handling. - * Deeper than my understanding of HTTP at this stage, so left for now. - */ -public class DefaultContextImpl implements HttpContext -{ - private Bundle m_bundle; - - public DefaultContextImpl(Bundle bundle) - { - m_bundle = bundle; - } - - public String getMimeType(String name) - { - return null; - } - - public URL getResource(String name) - { - //TODO: need to grant "org.osgi.framework.AdminPermission" when - // permissions are included. - Activator.debug("getResource for:" + name); - - //TODO: temp measure for name. Bundle classloading doesn't seem to find - // resources which have a leading "/". This code should be removed - // if the bundle classloader is changed to allow a leading "/" - if (name.startsWith("/")) - { - name = name.substring(1); - } - - return m_bundle.getResource(name); - } - - public boolean handleSecurity(HttpServletRequest request, - HttpServletResponse response) - { - //TODO: need to look into what's appropriate for default security - // handling. Default to all requests to be serviced for now. - return true; - } -} \ No newline at end of file diff --git a/http.jetty/src/main/java/org/apache/felix/http/jetty/HttpServiceImpl.java b/http.jetty/src/main/java/org/apache/felix/http/jetty/HttpServiceImpl.java deleted file mode 100644 index ba603e588c7..00000000000 --- a/http.jetty/src/main/java/org/apache/felix/http/jetty/HttpServiceImpl.java +++ /dev/null @@ -1,378 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.http.jetty; - -import java.security.AccessControlContext; -import java.security.AccessController; -import java.util.*; - -import javax.servlet.Servlet; -import javax.servlet.ServletException; - -import org.mortbay.http.HttpServer; -import org.mortbay.jetty.servlet.*; -import org.osgi.framework.Bundle; -import org.osgi.service.http.HttpService; -import org.osgi.service.http.NamespaceException; - -public class HttpServiceImpl implements HttpService -{ - /** global namesspace of all aliases that have been registered */ - private static Map m_aliasNamespace = null; - /** global pool of all OSGi HttpContext that have been created */ - private static Map m_contextMap = null; - /** global set of all servlet instances that have been registered */ - private static Set m_servletSet = null; - - /** local list of aliases registered by the bundle holding this service */ - private Set m_localAliasSet = null; - - /** Bundle which "got" this service instance from the service factory */ - private Bundle m_bundle = null; - /** Instance of Jetty server which provides underlying http server */ - private HttpServer m_server = null; - - public HttpServiceImpl(Bundle bundle, HttpServer server) - { - m_bundle = bundle; - m_server = server; - m_localAliasSet = new HashSet(); - - if (m_aliasNamespace == null) - { - m_aliasNamespace = new HashMap(); - } - - if (m_contextMap == null) - { - m_contextMap = new HashMap(); - } - - if (m_servletSet == null) - { - m_servletSet = new HashSet(); - } - } - - /** - * Initializes static variables. - **/ - public static void initializeStatics() - { - if (m_aliasNamespace != null) - { - m_aliasNamespace.clear(); - } - if (m_contextMap != null) - { - m_contextMap.clear(); - } - if (m_servletSet != null) - { - m_servletSet.clear(); - } - } - - public org.osgi.service.http.HttpContext createDefaultHttpContext() - { - return new DefaultContextImpl(m_bundle); - } - - public void registerServlet(String alias, Servlet servlet, - Dictionary params, org.osgi.service.http.HttpContext osgiHttpContext) - throws ServletException, NamespaceException - { - Activator.debug("http register servlet :" + m_bundle + ", alias: " + alias); - - if (!aliasValid(alias)) - { - throw new IllegalArgumentException("malformed alias"); - } - - if (m_servletSet.contains(servlet)) - { - throw new ServletException("servlet already registered"); - } - - // add alias with null details, and record servlet instance details - addAlias(alias, null); - - //make sure alias is unique, and create - ServletContextGroup grp = null; - - if (osgiHttpContext == null) - { - osgiHttpContext = createDefaultHttpContext(); - } - - // servlets using same context must get same handler to ensure - // they share a common ServletContext - Activator.debug("looking for context: " + osgiHttpContext); - grp = (ServletContextGroup) m_contextMap.get(osgiHttpContext); - if (grp == null) - { - grp = new ServletContextGroup( - servlet.getClass().getClassLoader(), osgiHttpContext); - } - - grp.addServlet(servlet, alias, params); - - // update alias namespace with reference to group object for later - // unregistering - updateAlias(alias, grp); - - // maybe should remove alias/servlet entries if exceptions? - } - - public void registerResources(String alias, String name, - org.osgi.service.http.HttpContext osgiHttpContext) - throws NamespaceException - { - Activator.debug("** http register resource :" + m_bundle + ", alias: " + alias); - - if (!aliasValid(alias)) - { - throw new IllegalArgumentException("malformed alias"); - } - - // add alias with null details - addAlias(alias, null); - - //make sure alias is unique, and create - org.mortbay.http.HttpContext hdlrContext = null; - - if (osgiHttpContext == null) - { - osgiHttpContext = createDefaultHttpContext(); - } - - hdlrContext = m_server.addContext(alias); - - // update alias namespace with reference to context object for later - // unregistering - updateAlias(alias, hdlrContext); - - // create resource handler, observing any access controls - AccessControlContext acc = null; - if (System.getSecurityManager() != null) - { - acc = AccessController.getContext(); - } - OsgiResourceHandler hdlr = new OsgiResourceHandler(osgiHttpContext, - name, acc); - - hdlrContext.addHandler(hdlr); - try - { - hdlrContext.start(); - } - catch (Exception e) - { - System.err.println("Oscar exception adding resource: " + e); - e.printStackTrace(System.err); - // maybe we should remove alias here? - } - } - - public void unregister(String alias) - { - doUnregister(alias, true); - } - - protected void unregisterAll() - { - // note that this is a forced unregister, so we shouldn't call destroy - // on any servlets - // unregister each alias for the bundle - copy list since it will - // change - String[] all = (String[]) m_localAliasSet.toArray(new String[0]); - for (int ix = 0; ix < all.length; ix++) - { - doUnregister(all[ix], false); - } - } - - protected void doUnregister(String alias, boolean forced) - { - Object obj = removeAlias(alias); - - if (obj instanceof org.mortbay.http.HttpContext) - { - Activator.debug("** http unregister resource :" + m_bundle + ", alias: " + alias); - - org.mortbay.http.HttpContext ctxt = (org.mortbay.http.HttpContext) obj; - try - { - ctxt.stop(); - m_server.removeContext(ctxt); - } - catch(Exception e) - { - System.err.println("Oscar exception removing resource: " + e); - e.printStackTrace(); - } - } - else if (obj instanceof ServletContextGroup) - { - Activator.debug("** http unregister servlet :" + m_bundle + ", alias: " + alias + ",forced:" + forced); - - ServletContextGroup grp = (ServletContextGroup) obj; - grp.removeServlet(alias, forced); - } - else - { - // oops - this shouldn't happen ! - } - } - - protected void addAlias(String alias, Object obj) - throws NamespaceException - { - synchronized (m_aliasNamespace) - { - if (m_aliasNamespace.containsKey(alias)) - { - throw new NamespaceException("alias already registered"); - } - - m_aliasNamespace.put(alias, obj); - m_localAliasSet.add(alias); - } - } - - protected Object removeAlias(String alias) - { - synchronized (m_aliasNamespace) - { - // remove alias, don't worry if doesn't exist - Object obj = m_aliasNamespace.remove(alias); - m_localAliasSet.remove(alias); - return obj; - } - } - - protected void updateAlias(String alias, Object obj) - { - synchronized (m_aliasNamespace) - { - // only update if already present - if (m_aliasNamespace.containsKey(alias)) - { - m_aliasNamespace.put(alias, obj); - } - } - } - - protected boolean aliasValid(String alias) - { - if (!alias.equals("/") && - (!alias.startsWith("/") || alias.endsWith("/"))) - { - return false; - } - - return true; - } - - private class ServletContextGroup - { - private OsgiServletHttpContext m_hdlrContext = null; - private OsgiServletHandler m_hdlr = null; - private org.osgi.service.http.HttpContext m_osgiHttpContext = null; - private int m_servletCount = 0; - - private ServletContextGroup(ClassLoader loader, - org.osgi.service.http.HttpContext osgiHttpContext) - { - init(loader, osgiHttpContext); - } - - private void init(ClassLoader loader, - org.osgi.service.http.HttpContext osgiHttpContext) - { - m_osgiHttpContext = osgiHttpContext; - m_hdlrContext = new OsgiServletHttpContext(m_osgiHttpContext); - m_hdlrContext.setContextPath("/"); - //TODO: was in original code, but seems we shouldn't serve - // resources in servlet context - //m_hdlrContext.setServingResources(true); - m_hdlrContext.setClassLoader(loader); - Activator.debug(" adding handler context : " + m_hdlrContext); - m_server.addContext(m_hdlrContext); - - m_hdlr = new OsgiServletHandler(m_osgiHttpContext); - m_hdlrContext.addHandler(m_hdlr); - - try - { - m_hdlrContext.start(); - } - catch (Exception e) - { - // make sure we unwind the adding process - System.err.println("Oscar exception adding servlet: " + e); - e.printStackTrace(System.err); - } - - m_contextMap.put(m_osgiHttpContext, this); - } - - private void destroy() - { - Activator.debug(" removing handler context : " + m_hdlrContext); - m_server.removeContext(m_hdlrContext); - m_contextMap.remove(m_osgiHttpContext); - } - - private void addServlet(Servlet servlet, String alias, - Dictionary params) - { - String wAlias = aliasWildcard(alias); - ServletHolder holder = new OsgiServletHolder(m_hdlr, servlet, wAlias, params); - m_hdlr.addOsgiServletHolder(wAlias, holder); - Activator.debug(" adding servlet instance: " + servlet); - m_servletSet.add(servlet); - m_servletCount++; - } - - private void removeServlet(String alias, boolean destroy) - { - String wAlias = aliasWildcard(alias); - OsgiServletHolder holder = m_hdlr.removeOsgiServletHolder(wAlias); - Servlet servlet = holder.getOsgiServlet(); - Activator.debug(" removing servlet instance: " + servlet); - m_servletSet.remove(servlet); - - if (destroy) - { - servlet.destroy(); - } - - if (--m_servletCount == 0) - { - destroy(); - } - } - - private String aliasWildcard(String alias) - { - // add wilcard filter at the end of the alias to allow servlet to - // get requests which include sub-paths - return "/".equals(alias) ? "/*" : alias + "/*"; - } - } -} \ No newline at end of file diff --git a/http.jetty/src/main/java/org/apache/felix/http/jetty/OsgiResourceHandler.java b/http.jetty/src/main/java/org/apache/felix/http/jetty/OsgiResourceHandler.java deleted file mode 100644 index c65c39cc8cf..00000000000 --- a/http.jetty/src/main/java/org/apache/felix/http/jetty/OsgiResourceHandler.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.http.jetty; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URL; -import java.security.*; - -import org.mortbay.http.HttpException; -import org.mortbay.http.HttpRequest; -import org.mortbay.http.HttpResponse; -import org.mortbay.http.handler.AbstractHttpHandler; -import org.mortbay.jetty.servlet.*; - -/** - * - */ -public class OsgiResourceHandler extends AbstractHttpHandler -{ - protected org.osgi.service.http.HttpContext m_osgiHttpContext; - protected String m_name; - protected OsgiServletHandler m_dummyHandler; - protected AccessControlContext m_acc; - - - public OsgiResourceHandler( - org.osgi.service.http.HttpContext osgiHttpContext, String name, - AccessControlContext acc) - { - m_osgiHttpContext = osgiHttpContext; - m_name = name; - // needed for OSGi security handling - m_dummyHandler = new OsgiServletHandler(osgiHttpContext); - m_acc = acc; - } - - - public void initialize(org.mortbay.http.HttpContext context) - { - super.initialize(context); - m_dummyHandler.initialize(context); - } - - - public void handle(String pathInContext, - String pathParams, - HttpRequest request, - HttpResponse response) - throws HttpException, IOException - { - Activator.debug("handle for name:" + m_name - + "(path=" + pathInContext + ")"); - - ServletHttpRequest servletRequest = new DummyServletHttpRequest( - m_dummyHandler, pathInContext, request); - ServletHttpResponse servletResponse = new DummyServletHttpResponse( - servletRequest, response); - - if (!m_osgiHttpContext.handleSecurity(servletRequest, servletResponse)) - { - // spec doesn't state specific processing here apart from - // "send the response back to the client". We take this to mean - // any response generated in the context, and so all we do here - // is set handled to "true" to ensure any output is sent back - request.setHandled(true); - return; - } - - // Create resource based name and see if we can resolve it - String resName = m_name + pathInContext; - Activator.debug("** looking for: " + resName); - URL url = m_osgiHttpContext.getResource(resName); - - if (url == null) - { - return; - } - - Activator.debug("serving up:" + resName); - - // It doesn't state so in the OSGi spec, but can't see how anything - // other than GET and variants would be supported - String method=request.getMethod(); - if (method.equals(HttpRequest.__GET) || - method.equals(HttpRequest.__POST) || - method.equals(HttpRequest.__HEAD)) - { - handleGet(request, response, url, resName); - } - else - { - try - { - response.sendError(HttpResponse.__501_Not_Implemented); - } - catch(Exception e) {/*TODO: include error logging*/} - } - } - - - public void handleGet(HttpRequest request, final HttpResponse response, - final URL url, String resName) - throws IOException - { - String encoding = m_osgiHttpContext.getMimeType(resName); - - if (encoding == null) - { - encoding = getHttpContext().getMimeByExtension(resName); - } - - if (encoding == null) - { - encoding = getHttpContext().getMimeByExtension(".default"); - } - - //TODO: not sure why this is needed, but sometimes get "IllegalState" - // errors if not included - response.setAcceptTrailer(true); - response.setContentType(encoding); - - //TODO: check other http fields e.g. ranges, timestamps etc. - - // make sure we access the resource inside the bundle's access control - // context if supplied - if (System.getSecurityManager() != null) - { - try - { - AccessController.doPrivileged(new PrivilegedExceptionAction() - { - public Object run() - throws Exception - { - copyResourceBytes(url, response); - return null; - } - }, m_acc); - } - catch (PrivilegedActionException ex) - { - IOException ioe = (IOException) ex.getException(); - throw ioe; - } - } - else - { - copyResourceBytes(url, response); - } - - request.setHandled(true); - //TODO: set other http fields e.g. __LastModified, __ContentLength - } - - - private void copyResourceBytes(URL url, HttpResponse response) - throws - IOException - { - OutputStream os = response.getOutputStream(); - InputStream is = url.openStream(); - int len = 0; - byte[] buf = new byte[1024]; - int n = 0; - - while ((n = is.read(buf, 0, buf.length)) >= 0) - { - os.write(buf, 0, n); - len += n; - } - - try - { - response.setContentLength(len); - } - catch (IllegalStateException ex) - { - System.err.println("OsgiResourceHandler: " + ex); - } - } -} diff --git a/http.jetty/src/main/java/org/mortbay/jetty/servlet/DummyServletHttpRequest.java b/http.jetty/src/main/java/org/mortbay/jetty/servlet/DummyServletHttpRequest.java deleted file mode 100644 index 962a267c112..00000000000 --- a/http.jetty/src/main/java/org/mortbay/jetty/servlet/DummyServletHttpRequest.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.mortbay.jetty.servlet; - -import org.mortbay.http.HttpRequest; - -public class DummyServletHttpRequest - extends - ServletHttpRequest -{ - public DummyServletHttpRequest(ServletHandler servletHandler, - String pathInContext, - HttpRequest request) - { - super(servletHandler, pathInContext, request); - } - -} \ No newline at end of file diff --git a/http.jetty/src/main/java/org/mortbay/jetty/servlet/DummyServletHttpResponse.java b/http.jetty/src/main/java/org/mortbay/jetty/servlet/DummyServletHttpResponse.java deleted file mode 100644 index e2c9387a602..00000000000 --- a/http.jetty/src/main/java/org/mortbay/jetty/servlet/DummyServletHttpResponse.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.mortbay.jetty.servlet; - -import org.mortbay.http.HttpResponse; - -public class DummyServletHttpResponse - extends - ServletHttpResponse -{ - public DummyServletHttpResponse(ServletHttpRequest request, - HttpResponse response) - { - super(request, response); - } - -} - diff --git a/http.jetty/src/main/java/org/mortbay/jetty/servlet/OsgiServletHandler.java b/http.jetty/src/main/java/org/mortbay/jetty/servlet/OsgiServletHandler.java deleted file mode 100644 index a3528b414cd..00000000000 --- a/http.jetty/src/main/java/org/mortbay/jetty/servlet/OsgiServletHandler.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.mortbay.jetty.servlet; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; - -import javax.servlet.ServletException; -import javax.servlet.UnavailableException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.mortbay.http.PathMap; -import org.mortbay.util.Code; - - -public class OsgiServletHandler - extends ServletHandler -{ - protected org.osgi.service.http.HttpContext m_osgiHttpContext; - - - public OsgiServletHandler( - org.osgi.service.http.HttpContext osgiHttpContext) - { - m_osgiHttpContext = osgiHttpContext; - } - - - // allow external adding of osgi servlet holder - public void addOsgiServletHolder(String pathSpec, ServletHolder holder) - { - super.addServletHolder(pathSpec, holder); - } - - - public OsgiServletHolder removeOsgiServletHolder(String pathSpec) - { - OsgiServletHolder holder = (OsgiServletHolder) - super.getServletHolder(pathSpec); - PathMap map = super.getServletMap(); - map.remove(pathSpec); - - // Remove holder from handler name map to allow re-registration. - super._nameMap.remove(holder.getName()); - - return holder; - } - - - // override standard handler behaviour to return resource from OSGi - // HttpContext - public URL getResource(String uriInContext) - throws MalformedURLException - { - Code.debug("OSGI ServletHandler getResource:" + uriInContext); - return m_osgiHttpContext.getResource(uriInContext); - } - - // override standard behaviour to check context first - protected void dispatch(String pathInContext, - HttpServletRequest request, - HttpServletResponse response, - ServletHolder servletHolder) - throws ServletException, - UnavailableException, - IOException - { - Code.debug("dispatch path = " + pathInContext); - if (m_osgiHttpContext.handleSecurity(request, response)) - { - // service request - servletHolder.handle(request,response); - } - else - { - //TODO: any other error/auth handling we should do in here? - response.flushBuffer(); - } - } -} - - diff --git a/http.jetty/src/main/java/org/mortbay/jetty/servlet/OsgiServletHolder.java b/http.jetty/src/main/java/org/mortbay/jetty/servlet/OsgiServletHolder.java deleted file mode 100644 index e4dba28dc59..00000000000 --- a/http.jetty/src/main/java/org/mortbay/jetty/servlet/OsgiServletHolder.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.mortbay.jetty.servlet; - - -import java.util.Dictionary; -import java.util.Enumeration; - -import javax.servlet.Servlet; -import javax.servlet.ServletConfig; -import javax.servlet.UnavailableException; - -public class OsgiServletHolder - extends - ServletHolder -{ - private Servlet m_servlet; - private ServletConfig m_config; - - - public OsgiServletHolder(ServletHandler handler, Servlet servlet, - String name, Dictionary params) - { - super(handler,name,servlet.getClass().getName()); - m_servlet = servlet; - - // Seemed safer to copy params into parent holder, rather than override - // the getInitxxx methods. - if (params != null) - { - Enumeration e = params.keys(); - while (e.hasMoreElements()) - { - Object key = e.nextElement(); - super.put(key, params.get(key)); - } - } - } - - public synchronized Servlet getServlet() - throws UnavailableException - { - return m_servlet; - } - - public Servlet getOsgiServlet() - { - return m_servlet; - } - - - // override "Holder" method to prevent instantiation - public synchronized Object newInstance() - throws InstantiationException, - IllegalAccessException - { - return getOsgiServlet(); - } - - // override "Holder" method to prevent attempt to load - // the servlet class. - public void start() - throws Exception - { - _class=m_servlet.getClass(); - - m_config=new Config(); - m_servlet.init(m_config); - } - - // override "Holder" method to prevent destroy, which is only called - // when a bundle manually unregisters - public void stop() - { - } -} - diff --git a/http.jetty/src/main/java/org/mortbay/jetty/servlet/OsgiServletHttpContext.java b/http.jetty/src/main/java/org/mortbay/jetty/servlet/OsgiServletHttpContext.java deleted file mode 100644 index 3a95d137401..00000000000 --- a/http.jetty/src/main/java/org/mortbay/jetty/servlet/OsgiServletHttpContext.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2006 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.mortbay.jetty.servlet; - - -import org.mortbay.util.Code; - -public class OsgiServletHttpContext - extends - ServletHttpContext -{ - protected org.osgi.service.http.HttpContext m_osgiHttpContext; - - public OsgiServletHttpContext( - org.osgi.service.http.HttpContext osgiHttpContext) - { - m_osgiHttpContext = osgiHttpContext; - } - - // intercept to ensure OSGi context is used first for servlet calls to - // getMimeType() - public String getMimeByExtension(String filename) - { - Code.debug("OSGi servlet context: get mime type"); - String encoding = m_osgiHttpContext.getMimeType(filename); - - if (encoding == null) - { - encoding = super.getMimeByExtension(filename); - } - - return encoding; - } - -} diff --git a/http.jetty/src/main/java/org/osgi/service/http/HttpContext.java b/http.jetty/src/main/java/org/osgi/service/http/HttpContext.java deleted file mode 100644 index d5a0eb52254..00000000000 --- a/http.jetty/src/main/java/org/osgi/service/http/HttpContext.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * $Header: /cvshome/build/org.osgi.service.http/src/org/osgi/service/http/HttpContext.java,v 1.9 2006/03/14 01:20:39 hargrave Exp $ - * - * Copyright (c) OSGi Alliance (2000, 2005). All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.osgi.service.http; - -import java.io.IOException; -import java.net.URL; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * This interface defines methods that the Http Service may call to get - * information about a registration. - * - *

      - * Servlets and resources may be registered with an HttpContext - * object; if no HttpContext object is specified, a default - * HttpContext object is used. Servlets that are registered using the - * same HttpContext object will share the same - * ServletContext object. - * - *

      - * This interface is implemented by users of the HttpService. - * - * @version $Revision: 1.9 $ - */ -public interface HttpContext { - /** - * HttpServletRequest attribute specifying the name of the - * authenticated user. The value of the attribute can be retrieved by - * HttpServletRequest.getRemoteUser. This attribute name is - * org.osgi.service.http.authentication.remote.user. - * - * @since 1.1 - */ - public static final String REMOTE_USER = "org.osgi.service.http.authentication.remote.user"; - /** - * HttpServletRequest attribute specifying the scheme used in - * authentication. The value of the attribute can be retrieved by - * HttpServletRequest.getAuthType. This attribute name is - * org.osgi.service.http.authentication.type. - * - * @since 1.1 - */ - public static final String AUTHENTICATION_TYPE = "org.osgi.service.http.authentication.type"; - /** - * HttpServletRequest attribute specifying the - * Authorization object obtained from the - * org.osgi.service.useradmin.UserAdmin service. The value of the - * attribute can be retrieved by - * HttpServletRequest.getAttribute(HttpContext.AUTHORIZATION). - * This attribute name is org.osgi.service.useradmin.authorization. - * - * @since 1.1 - */ - public static final String AUTHORIZATION = "org.osgi.service.useradmin.authorization"; - - /** - * Handles security for the specified request. - * - *

      - * The Http Service calls this method prior to servicing the specified - * request. This method controls whether the request is processed in the - * normal manner or an error is returned. - * - *

      - * If the request requires authentication and the Authorization header in - * the request is missing or not acceptable, then this method should set the - * WWW-Authenticate header in the response object, set the status in the - * response object to Unauthorized(401) and return false. See - * also RFC 2617: HTTP Authentication: Basic and Digest Access - * Authentication (available at http://www.ietf.org/rfc/rfc2617.txt). - * - *

      - * If the request requires a secure connection and the getScheme - * method in the request does not return 'https' or some other acceptable - * secure protocol, then this method should set the status in the response - * object to Forbidden(403) and return false. - * - *

      - * When this method returns false, the Http Service will send - * the response back to the client, thereby completing the request. When - * this method returns true, the Http Service will proceed with - * servicing the request. - * - *

      - * If the specified request has been authenticated, this method must set the - * {@link #AUTHENTICATION_TYPE}request attribute to the type of - * authentication used, and the {@link #REMOTE_USER}request attribute to - * the remote user (request attributes are set using the - * setAttribute method on the request). If this method does not - * perform any authentication, it must not set these attributes. - * - *

      - * If the authenticated user is also authorized to access certain resources, - * this method must set the {@link #AUTHORIZATION}request attribute to the - * Authorization object obtained from the - * org.osgi.service.useradmin.UserAdmin service. - * - *

      - * The servlet responsible for servicing the specified request determines - * the authentication type and remote user by calling the - * getAuthType and getRemoteUser methods, - * respectively, on the request. - * - * @param request the HTTP request - * @param response the HTTP response - * @return true if the request should be serviced, false - * if the request should not be serviced and Http Service will send - * the response back to the client. - * @throws java.io.IOException may be thrown by this method. If this - * occurs, the Http Service will terminate the request and close - * the socket. - */ - public boolean handleSecurity(HttpServletRequest request, - HttpServletResponse response) throws IOException; - - /** - * Maps a resource name to a URL. - * - *

      - * Called by the Http Service to map a resource name to a URL. For servlet - * registrations, Http Service will call this method to support the - * ServletContext methods getResource and - * getResourceAsStream. For resource registrations, Http Service - * will call this method to locate the named resource. The context can - * control from where resources come. For example, the resource can be - * mapped to a file in the bundle's persistent storage area via - * bundleContext.getDataFile(name).toURL() or to a resource in - * the context's bundle via getClass().getResource(name) - * - * @param name the name of the requested resource - * @return URL that Http Service can use to read the resource or - * null if the resource does not exist. - */ - public URL getResource(String name); - - /** - * Maps a name to a MIME type. - * - * Called by the Http Service to determine the MIME type for the name. For - * servlet registrations, the Http Service will call this method to support - * the ServletContext method getMimeType. For - * resource registrations, the Http Service will call this method to - * determine the MIME type for the Content-Type header in the response. - * - * @param name determine the MIME type for this name. - * @return MIME type (e.g. text/html) of the name or null to - * indicate that the Http Service should determine the MIME type - * itself. - */ - public String getMimeType(String name); -} diff --git a/http.jetty/src/main/java/org/osgi/service/http/HttpService.java b/http.jetty/src/main/java/org/osgi/service/http/HttpService.java deleted file mode 100644 index 95acce9cb26..00000000000 --- a/http.jetty/src/main/java/org/osgi/service/http/HttpService.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * $Header: /cvshome/build/org.osgi.service.http/src/org/osgi/service/http/HttpService.java,v 1.10 2006/03/14 01:20:39 hargrave Exp $ - * - * Copyright (c) OSGi Alliance (2000, 2005). All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.osgi.service.http; - -import javax.servlet.Servlet; -import javax.servlet.ServletException; -import java.util.Dictionary; - -/** - * The Http Service allows other bundles in the OSGi environment to dynamically - * register resources and servlets into the URI namespace of Http Service. A - * bundle may later unregister its resources or servlets. - * - * @version $Revision: 1.10 $ - * @see HttpContext - */ -public interface HttpService { - /** - * Registers a servlet into the URI namespace. - * - *

      - * The alias is the name in the URI namespace of the Http Service at which - * the registration will be mapped. - * - *

      - * An alias must begin with slash ('/') and must not end with slash ('/'), - * with the exception that an alias of the form "/" is used to - * denote the root alias. See the specification text for details on how HTTP - * requests are mapped to servlet and resource registrations. - * - *

      - * The Http Service will call the servlet's init method before - * returning. - * - *

      -	 * httpService.registerServlet("/myservlet", servlet, initparams, context);
      -	 * 
      - * - *

      - * Servlets registered with the same HttpContext object will - * share the same ServletContext. The Http Service will call the - * context argument to support the ServletContext - * methods getResource,getResourceAsStream and - * getMimeType, and to handle security for requests. If the - * context argument is null, a default - * HttpContext object is used (see - * {@link #createDefaultHttpContext}). - * - * @param alias name in the URI namespace at which the servlet is registered - * @param servlet the servlet object to register - * @param initparams initialization arguments for the servlet or - * null if there are none. This argument is used by the - * servlet's ServletConfig object. - * @param context the HttpContext object for the registered - * servlet, or null if a default HttpContext is - * to be created and used. - * @throws NamespaceException if the registration fails because the alias - * is already in use. - * @throws javax.servlet.ServletException if the servlet's init - * method throws an exception, or the given servlet object has - * already been registered at a different alias. - * @throws java.lang.IllegalArgumentException if any of the arguments are - * invalid - */ - public void registerServlet(String alias, Servlet servlet, - Dictionary initparams, HttpContext context) - throws ServletException, NamespaceException; - - /** - * Registers resources into the URI namespace. - * - *

      - * The alias is the name in the URI namespace of the Http Service at which - * the registration will be mapped. An alias must begin with slash ('/') and - * must not end with slash ('/'), with the exception that an alias of the - * form "/" is used to denote the root alias. The name parameter - * must also not end with slash ('/'). See the specification text for - * details on how HTTP requests are mapped to servlet and resource - * registrations. - *

      - * For example, suppose the resource name /tmp is registered to the alias - * /files. A request for /files/foo.txt will map to the resource name - * /tmp/foo.txt. - * - *

      -	 * httpservice.registerResources("/files", "/tmp", context);
      -	 * 
      - * - * The Http Service will call the HttpContext argument to map - * resource names to URLs and MIME types and to handle security for - * requests. If the HttpContext argument is null, a - * default HttpContext is used (see - * {@link #createDefaultHttpContext}). - * - * @param alias name in the URI namespace at which the resources are - * registered - * @param name the base name of the resources that will be registered - * @param context the HttpContext object for the registered - * resources, or null if a default HttpContext - * is to be created and used. - * @throws NamespaceException if the registration fails because the alias - * is already in use. - * @throws java.lang.IllegalArgumentException if any of the parameters - * are invalid - */ - public void registerResources(String alias, String name, - HttpContext context) throws NamespaceException; - - /** - * Unregisters a previous registration done by registerServlet or - * registerResources methods. - * - *

      - * After this call, the registered alias in the URI name-space will no - * longer be available. If the registration was for a servlet, the Http - * Service must call the destroy method of the servlet before - * returning. - *

      - * If the bundle which performed the registration is stopped or otherwise - * "unget"s the Http Service without calling {@link #unregister}then Http - * Service must automatically unregister the registration. However, if the - * registration was for a servlet, the destroy method of the - * servlet will not be called in this case since the bundle may be stopped. - * {@link #unregister}must be explicitly called to cause the - * destroy method of the servlet to be called. This can be done - * in the BundleActivator.stop method of the - * bundle registering the servlet. - * - * @param alias name in the URI name-space of the registration to unregister - * @throws java.lang.IllegalArgumentException if there is no registration - * for the alias or the calling bundle was not the bundle which - * registered the alias. - */ - public void unregister(String alias); - - /** - * Creates a default HttpContext for registering servlets or - * resources with the HttpService, a new HttpContext object is - * created each time this method is called. - * - *

      - * The behavior of the methods on the default HttpContext is - * defined as follows: - *

        - *
      • getMimeType- Does not define any customized MIME types - * for the Content-Type header in the response, and always returns - * null. - *
      • handleSecurity- Performs implementation-defined - * authentication on the request. - *
      • getResource- Assumes the named resource is in the - * context bundle; this method calls the context bundle's - * Bundle.getResource method, and returns the appropriate URL to - * access the resource. On a Java runtime environment that supports - * permissions, the Http Service needs to be granted - * org.osgi.framework.AdminPermission[*,RESOURCE]. - *
      - * - * @return a default HttpContext object. - * @since 1.1 - */ - public HttpContext createDefaultHttpContext(); -} diff --git a/http.jetty/src/main/java/org/osgi/service/http/NamespaceException.java b/http.jetty/src/main/java/org/osgi/service/http/NamespaceException.java deleted file mode 100644 index c8e78893d40..00000000000 --- a/http.jetty/src/main/java/org/osgi/service/http/NamespaceException.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * $Header: /cvshome/build/org.osgi.service.http/src/org/osgi/service/http/NamespaceException.java,v 1.9 2006/03/14 01:20:39 hargrave Exp $ - * - * Copyright (c) OSGi Alliance (2000, 2005). All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.osgi.service.http; - -/** - * A NamespaceException is thrown to indicate an error with the caller's request - * to register a servlet or resources into the URI namespace of the Http - * Service. This exception indicates that the requested alias already is in use. - * - * @version $Revision: 1.9 $ - */ -public class NamespaceException extends Exception { - static final long serialVersionUID = 7235606031147877747L; - /** - * Nested exception. - */ - private Throwable cause; - - /** - * Construct a NamespaceException object with a detail message. - * - * @param message the detail message - */ - public NamespaceException(String message) { - super(message); - cause = null; - } - - /** - * Construct a NamespaceException object with a detail message - * and a nested exception. - * - * @param message The detail message. - * @param cause The nested exception. - */ - public NamespaceException(String message, Throwable cause) { - super(message); - this.cause = cause; - } - - /** - * Returns the nested exception. - * - *

      This method predates the general purpose exception chaining mechanism. - * The {@link #getCause()} method is now the preferred means of - * obtaining this information. - * - * @return the nested exception or null if there is no nested - * exception. - */ - public Throwable getException() { - return cause; - } - - /** - * Returns the cause of this exception or null if no - * cause was specified when this exception was created. - * - * @return The cause of this exception or null if no - * cause was specified. - * @since 1.2 - */ - public Throwable getCause() { - return cause; - } - - /** - * The cause of this exception can only be set when constructed. - * - * @param cause Cause of the exception. - * @return This object. - * @throws java.lang.IllegalStateException - * This method will always throw an IllegalStateException - * since the cause of this exception can only be set when constructed. - * @since 1.2 - */ - public Throwable initCause(Throwable cause) { - throw new IllegalStateException(); - } -} diff --git a/http/LICENSE b/http/LICENSE new file mode 100644 index 00000000000..6b0b1270ff0 --- /dev/null +++ b/http/LICENSE @@ -0,0 +1,203 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/http/base/pom.xml b/http/base/pom.xml new file mode 100644 index 00000000000..a200f826c9d --- /dev/null +++ b/http/base/pom.xml @@ -0,0 +1,80 @@ + + + + 4.0.0 + + org.apache.felix + org.apache.felix.http.parent + 12 + ../parent/pom.xml + + + Apache Felix Http Base + org.apache.felix.http.base + 4.0.7-SNAPSHOT + jar + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/http/base + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/http/base + http://svn.apache.org/viewvc/felix/trunk/http/base/ + + + + + javax.servlet + javax.servlet-api + + + org.osgi + osgi.core + + + org.osgi + org.osgi.service.http + 1.2.1 + provided + + + org.osgi + org.osgi.service.http.whiteboard + 1.1.0 + provided + + + org.osgi + org.osgi.service.log + 1.3.0 + provided + + + org.osgi + org.osgi.service.useradmin + 1.1.0 + provided + + + commons-fileupload + commons-fileupload + 1.3.3 + provided + + + diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/AbstractActivator.java b/http/base/src/main/java/org/apache/felix/http/base/internal/AbstractActivator.java new file mode 100644 index 00000000000..f7a062b1e93 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/AbstractActivator.java @@ -0,0 +1,55 @@ +/* + * 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.http.base.internal; + +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +public abstract class AbstractActivator + implements BundleActivator +{ + private volatile BundleContext context; + + protected final BundleContext getBundleContext() + { + return this.context; + } + + @Override + public final void start(final BundleContext context) + throws Exception + { + this.context = context; + SystemLogger.init(context); + doStart(); + } + + @Override + public final void stop(final BundleContext context) + throws Exception + { + doStop(); + SystemLogger.destroy(); + } + + protected abstract void doStart() + throws Exception; + + protected abstract void doStop() + throws Exception; +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/AbstractHttpActivator.java b/http/base/src/main/java/org/apache/felix/http/base/internal/AbstractHttpActivator.java new file mode 100644 index 00000000000..937c7ad88da --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/AbstractHttpActivator.java @@ -0,0 +1,42 @@ +/* + * 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.http.base.internal; + +public abstract class AbstractHttpActivator + extends AbstractActivator +{ + private HttpServiceController controller; + + protected final HttpServiceController getHttpServiceController() + { + return this.controller; + } + + @Override + protected void doStart() + throws Exception + { + this.controller = new HttpServiceController(getBundleContext()); + } + + @Override + protected void doStop() + throws Exception + { + this.controller.unregister(); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/EventDispatcher.java b/http/base/src/main/java/org/apache/felix/http/base/internal/EventDispatcher.java new file mode 100644 index 00000000000..044ed1500cc --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/EventDispatcher.java @@ -0,0 +1,76 @@ +/* + * 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.http.base.internal; + +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionIdListener; +import javax.servlet.http.HttpSessionListener; + +/** + * The EventDispatcher dispatches events sent from the servlet + * container (embedded Jetty or container in which the framework is running + * in bridged mode) to any {@link HttpSessionListener} or + * {@link HttpSessionListener} services. + * + * TODO - only HttpSessionIdListener and HttpSessionListener should be + * required; HttpSessionListener only for getting notified of + * terminated session. + */ +public class EventDispatcher implements HttpSessionListener, HttpSessionIdListener +{ + private volatile boolean active = false; + + private final HttpServiceController controller; + + EventDispatcher(final HttpServiceController controller) + { + this.controller = controller; + } + + public void setActive(final boolean flag) + { + this.active = flag; + } + + @Override + public void sessionCreated(final HttpSessionEvent se) + { + if ( this.active ) + { + controller.getSessionListener().sessionCreated(se); + } + } + + @Override + public void sessionDestroyed(final HttpSessionEvent se) + { + if ( this.active ) + { + controller.getSessionListener().sessionDestroyed(se); + } + } + + @Override + public void sessionIdChanged(final HttpSessionEvent event, final String oldSessionId) { + if ( this.active ) + { + controller.getSessionIdListener().sessionIdChanged(event, oldSessionId); + } + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/HttpConfig.java b/http/base/src/main/java/org/apache/felix/http/base/internal/HttpConfig.java new file mode 100644 index 00000000000..b93febdd02b --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/HttpConfig.java @@ -0,0 +1,70 @@ +/* + * 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.http.base.internal; + +import java.util.Dictionary; + +import org.jetbrains.annotations.NotNull; + +public class HttpConfig { + + public static final String PROP_INVALIDATE_SESSION = "org.apache.felix.http.session.invalidate"; + + public static final boolean DEFAULT_INVALIDATE_SESSION = true; + + public static final String PROP_UNIQUE_SESSION_ID = "org.apache.felix.http.session.uniqueid"; + + public static final boolean DEFAULT_UNIQUE_SESSION_ID = true; + + private volatile boolean uniqueSessionId; + + private volatile boolean invalidateContainerSession; + + public boolean isUniqueSessionId() { + return uniqueSessionId; + } + + public void setUniqueSessionId(boolean appendSessionId) { + this.uniqueSessionId = appendSessionId; + } + + public boolean isInvalidateContainerSession() { + return invalidateContainerSession; + } + + public void setInvalidateContainerSession(boolean invalidateContainerSession) { + this.invalidateContainerSession = invalidateContainerSession; + } + + public void configure(@NotNull final Dictionary props) { + this.setUniqueSessionId(this.getBooleanProperty(props, PROP_UNIQUE_SESSION_ID, DEFAULT_UNIQUE_SESSION_ID)); + this.setInvalidateContainerSession(this.getBooleanProperty(props, PROP_INVALIDATE_SESSION, DEFAULT_INVALIDATE_SESSION)); + } + + + private boolean getBooleanProperty(final Dictionary props, final String name, final boolean defValue) + { + final Object v = props.get(name); + if ( v != null ) + { + final String value = String.valueOf(v); + return "true".equalsIgnoreCase(value) || "yes".equalsIgnoreCase(value); + } + + return defValue; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/HttpServiceController.java b/http/base/src/main/java/org/apache/felix/http/base/internal/HttpServiceController.java new file mode 100644 index 00000000000..67610f90175 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/HttpServiceController.java @@ -0,0 +1,140 @@ +/* + * 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.http.base.internal; + +import java.util.Hashtable; + +import javax.servlet.Servlet; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionIdListener; +import javax.servlet.http.HttpSessionListener; + +import org.apache.felix.http.base.internal.dispatch.Dispatcher; +import org.apache.felix.http.base.internal.dispatch.DispatcherServlet; +import org.apache.felix.http.base.internal.handler.HttpSessionWrapper; +import org.apache.felix.http.base.internal.registry.HandlerRegistry; +import org.apache.felix.http.base.internal.service.HttpServiceFactory; +import org.apache.felix.http.base.internal.whiteboard.WhiteboardManager; +import org.jetbrains.annotations.NotNull; +import org.osgi.framework.BundleContext; + +public final class HttpServiceController +{ + private final BundleContext bundleContext; + private final HandlerRegistry registry; + private final Dispatcher dispatcher; + private final EventDispatcher eventDispatcher; + private final HttpServiceFactory httpServiceFactory; + private final WhiteboardManager whiteboardManager; + + private final HttpConfig config = new HttpConfig(); + + private volatile HttpSessionListener httpSessionListener; + + public HttpServiceController(final BundleContext bundleContext) + { + this.bundleContext = bundleContext; + this.registry = new HandlerRegistry(config); + this.dispatcher = new Dispatcher(this.registry); + this.eventDispatcher = new EventDispatcher(this); + this.httpServiceFactory = new HttpServiceFactory(this.bundleContext, this.registry); + this.whiteboardManager = new WhiteboardManager(bundleContext, this.httpServiceFactory, this.registry); + } + + public void stop() + { + this.unregister(); + } + + /** + * Create a new dispatcher servlet + * @return The dispatcher servlet. + */ + public @NotNull Servlet createDispatcherServlet() + { + return new DispatcherServlet(this.dispatcher); + } + + public EventDispatcher getEventDispatcher() + { + return this.eventDispatcher; + } + + HttpSessionListener getSessionListener() + { + // we don't need to sync here, if the object gets created several times + // its not a problem + if ( httpSessionListener == null ) + { + httpSessionListener = new HttpSessionListener() { + + @Override + public void sessionDestroyed(final HttpSessionEvent se) { + whiteboardManager.sessionDestroyed(se.getSession(), HttpSessionWrapper.getSessionContextNames(se.getSession())); + } + + @Override + public void sessionCreated(final HttpSessionEvent se) { + // nothing to do, session created event is sent from within the session + } + }; + } + return httpSessionListener; + } + + HttpSessionIdListener getSessionIdListener() + { + return new HttpSessionIdListener() { + + @Override + public void sessionIdChanged(final HttpSessionEvent event, final String oldSessionId) { + whiteboardManager.sessionIdChanged(event, oldSessionId, HttpSessionWrapper.getSessionContextNames(event.getSession())); + } + }; + } + + /** + * Start the http and http whiteboard service in the provided context. + * @param containerContext The container context. + */ + public void register(@NotNull final ServletContext containerContext, @NotNull final Hashtable props) + { + this.config.configure(props); + + this.registry.init(); + + this.httpServiceFactory.start(containerContext, props); + this.whiteboardManager.start(containerContext, props); + + this.dispatcher.setWhiteboardManager(this.whiteboardManager); + } + + /** + * Stops the http and http whiteboard service. + */ + public void unregister() + { + this.dispatcher.setWhiteboardManager(null); + + this.whiteboardManager.stop(); + this.httpServiceFactory.stop(); + + this.registry.shutdown(); + this.httpSessionListener = null; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/console/HttpServicePlugin.java b/http/base/src/main/java/org/apache/felix/http/base/internal/console/HttpServicePlugin.java new file mode 100644 index 00000000000..074934b1f83 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/console/HttpServicePlugin.java @@ -0,0 +1,1346 @@ +/* + * 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.http.base.internal.console; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.dto.ServiceReferenceDTO; +import org.osgi.service.http.runtime.HttpServiceRuntime; +import org.osgi.service.http.runtime.dto.DTOConstants; +import org.osgi.service.http.runtime.dto.ErrorPageDTO; +import org.osgi.service.http.runtime.dto.FailedErrorPageDTO; +import org.osgi.service.http.runtime.dto.FailedFilterDTO; +import org.osgi.service.http.runtime.dto.FailedListenerDTO; +import org.osgi.service.http.runtime.dto.FailedResourceDTO; +import org.osgi.service.http.runtime.dto.FailedServletContextDTO; +import org.osgi.service.http.runtime.dto.FailedServletDTO; +import org.osgi.service.http.runtime.dto.FilterDTO; +import org.osgi.service.http.runtime.dto.ListenerDTO; +import org.osgi.service.http.runtime.dto.RequestInfoDTO; +import org.osgi.service.http.runtime.dto.ResourceDTO; +import org.osgi.service.http.runtime.dto.RuntimeDTO; +import org.osgi.service.http.runtime.dto.ServletContextDTO; +import org.osgi.service.http.runtime.dto.ServletDTO; + +/** + * This is a web console plugin. + */ +@SuppressWarnings("serial") +public class HttpServicePlugin extends HttpServlet +{ + private static final String ATTR_TEST = "test"; + private static final String ATTR_MSG = "msg"; + private static final String ATTR_SUBMIT = "resolve"; + + + private final HttpServiceRuntime runtime; + private final BundleContext context; + + private volatile ServiceRegistration serviceReg; + + public HttpServicePlugin(final BundleContext context, final HttpServiceRuntime runtime) + { + this.runtime = runtime; + this.context = context; + } + + public void register() + { + final Dictionary props = new Hashtable<>(); + props.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation"); + props.put(Constants.SERVICE_DESCRIPTION, "HTTP Service Web Console Plugin"); + props.put("felix.webconsole.label", "httpservice"); + props.put("felix.webconsole.title", "HTTP Service"); + props.put("felix.webconsole.configprinter.modes", "always"); + this.serviceReg = context.registerService(Servlet.class, this, props); + } + + /** Escape xml text */ + private static String escapeXml(final String input) { + if (input == null) { + return null; + } + + final StringBuilder b = new StringBuilder(input.length()); + for(int i = 0;i < input.length(); i++) { + final char c = input.charAt(i); + if(c == '&') { + b.append("&"); + } else if(c == '<') { + b.append("<"); + } else if(c == '>') { + b.append(">"); + } else if(c == '"') { + b.append("""); + } else if(c == '\'') { + b.append("'"); + } else { + b.append(c); + } + } + return b.toString(); + } + + @Override + protected void doPost(HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + + final String test = request.getParameter(ATTR_TEST); + String msg = null; + if (test != null && test.length() > 0) { + + final RequestInfoDTO dto = this.runtime.calculateRequestInfoDTO(test); + + final StringBuilder sb = new StringBuilder(); + if ( dto.servletDTO != null ) + { + sb.append("Servlet: "); + sb.append(getValueAsString(dto.servletDTO.patterns)); + sb.append(" ("); + sb.append("service.id="); + sb.append(String.valueOf(dto.servletDTO.serviceId)); + sb.append("), Filters: ["); + boolean first = true; + for(final FilterDTO f : dto.filterDTOs) + { + if ( first ) + { + first = false; + } + else + { + sb.append(", "); + } + sb.append(f.serviceId); + } + sb.append("]"); + } + else if ( dto.resourceDTO != null ) + { + sb.append("Resource: "); + sb.append(getValueAsString(dto.resourceDTO.patterns)); + sb.append(" ("); + sb.append("service.id="); + sb.append(String.valueOf(dto.resourceDTO.serviceId)); + sb.append("), Filters: ["); + boolean first = true; + for(final FilterDTO f : dto.filterDTOs) + { + if ( first ) + { + first = false; + } + else + { + sb.append(", "); + } + sb.append(f.serviceId); + } + sb.append("]"); + } + else + { + sb.append("<404>"); + } + msg = sb.toString(); + } + + // finally redirect + final String path = request.getContextPath() + request.getServletPath() + + request.getPathInfo(); + final String redirectTo; + if (msg == null) { + redirectTo = path; + } else { + redirectTo = path + '?' + ATTR_MSG + '=' + encodeParam(msg) + '&' + + ATTR_TEST + '=' + encodeParam(test); + } + response.sendRedirect(redirectTo); + } + + private String encodeParam(final String value) { + try { + return URLEncoder.encode(value, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // should never happen + return value; + } + } + + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws IOException + { + final RuntimeDTO dto = this.runtime.getRuntimeDTO(); + + final PrintWriter pw = resp.getWriter(); + + String path = req.getContextPath() + req.getServletPath(); + if ( req.getPathInfo() != null ) { + path = path + req.getPathInfo(); + } + printForm(pw, req.getParameter(ATTR_TEST), req.getParameter(ATTR_MSG), path); + + printRuntimeDetails(pw, dto.serviceDTO); + + for(final ServletContextDTO ctxDto : dto.servletContextDTOs ) + { + printContextDetails(pw, ctxDto); + } + for(final FailedServletContextDTO ctxDto : dto.failedServletContextDTOs ) + { + printFailedContextDetails(pw, ctxDto); + } + printFailedServletDetails(pw, dto); + printFailedFilterDetails(pw, dto); + printFailedResourceDetails(pw, dto); + printFailedErrorPageDetails(pw, dto); + printFailedListenerDetails(pw, dto); + + pw.println("
      "); + } + + private void printForm(final PrintWriter pw, final String value, final String msg, final String path) + { + pw.println(""); + + separatorHtml(pw); + + titleHtml( + pw, + "Test Servlet Resolution", + "To test the servlet resolution, enter a relative URL into " + + "the field and click 'Resolve'."); + + pw.println(""); + pw.println(""); + pw.print(""); + pw.println(""); + + if (msg != null) { + pw.println(""); + pw.println(""); + pw.print(""); + pw.println(""); + } + pw.println("
      Test"); + pw.print("
      "); + pw.print(""); + pw.println("  "); + pw.print("
      "); + pw.print("
       "); + pw.print(escapeXml(msg)); + pw.println("
      "); + } + + private void titleHtml(PrintWriter pw, String title, String description) + { + pw.println(""); + pw.println("" + title + + ""); + pw.println(""); + + if (description != null) { + pw.println(""); + pw.println("" + description + + ""); + pw.println(""); + } + } + + private void separatorHtml(PrintWriter pw) + { + pw.println(""); + pw.println(" "); + pw.println(""); + } + + private String getValueAsString(final Object value) + { + if ( value.getClass().isArray() ) + { + if (value instanceof long[]) + { + return Arrays.toString((long[])value); + } + else if (value instanceof int[]) + { + return Arrays.toString((int[])value); + } + else if (value instanceof double[]) + { + return Arrays.toString((double[])value); + } + else if (value instanceof byte[]) + { + return Arrays.toString((byte[])value); + } + else if (value instanceof float[]) + { + return Arrays.toString((float[])value); + } + else if (value instanceof short[]) + { + return Arrays.toString((short[])value); + } + else if (value instanceof boolean[]) + { + return Arrays.toString((boolean[])value); + } + else if (value instanceof char[]) + { + return Arrays.toString((char[])value); + } + else + { + return Arrays.toString((Object[])value); + } + } + return value.toString(); + } + + private void printRuntimeDetails(final PrintWriter pw, final ServiceReferenceDTO dto) + { + pw.println("

      ${Runtime Properties}

      "); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + boolean odd = true; + for(final Map.Entry prop : dto.properties.entrySet()) + { + odd = printRow(pw, odd, prop.getKey(), getValueAsString(prop.getValue())); + } + pw.println("
      ${Name}${Value}
      "); + pw.println("
      "); + } + + private boolean printRow(final PrintWriter pw, final boolean odd, final String...columns) + { + pw.print(""); + + for(final String val : columns) + { + pw.print(""); + if ( val != null ) + { + String text = escapeXml(val).replace("\n", "
      "); + int pos; + while ( (pos = text.indexOf("${#link:")) != -1) + { + final int endPos = text.indexOf("}", pos); + final int bundleId = Integer.valueOf(text.substring(pos + 8, endPos)); + final int tokenEndPos = text.indexOf("${link#}", pos); + + text = text.substring(0, pos) + "" + + text.substring(endPos + 1, tokenEndPos) + "" + text.substring(tokenEndPos + 8); + } + pw.print(text); + } + pw.println(""); + } + + pw.println(""); + return !odd; + } + + private String getContextPath(final String path) + { + if ( path.length() == 0 ) + { + return ""; + } + return path; + } + + private boolean printServiceRankingRow(final PrintWriter pw, final long serviceId, final boolean odd) + { + int ranking = 0; + final ServiceReference ref = this.getServiceReference(serviceId); + if ( ref != null ) + { + final Object obj = ref.getProperty(Constants.SERVICE_RANKING); + if ( obj instanceof Integer) + { + ranking = (Integer)obj; + } + } + return printRow(pw, odd, "${ranking}", String.valueOf(ranking)); + } + + private void printContextDetails(final PrintWriter pw, final ServletContextDTO dto) + { + pw.print("

      ${Servlet Context} '"); + pw.print(escapeXml(dto.name)); + pw.println("'

      "); + + pw.println(""); + + boolean odd = true; + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + odd = printRow(pw, odd, "${Path}", getContextPath(dto.contextPath)); + odd = printRow(pw, odd, "${service.id}", String.valueOf(dto.serviceId)); + odd = printServiceRankingRow(pw, dto.serviceId, odd); + pw.println("
      ${Name}${Value)}
      "); + + printServletDetails(pw, dto); + printFilterDetails(pw, dto); + printResourceDetails(pw, dto); + printErrorPageDetails(pw, dto); + printListenerDetails(pw, dto); + + pw.println("
      "); + } + + private void printFailedContextDetails(final PrintWriter pw, final FailedServletContextDTO dto) + { + pw.print("

      ${Servlet Context} '"); + pw.print(escapeXml(dto.name)); + pw.println("'

      "); + + pw.println(""); + + boolean odd = true; + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + odd = printRow(pw, odd, "${Path}", getContextPath(dto.contextPath)); + odd = printRow(pw, odd, "${reason}", getErrorText(dto.failureReason)); + odd = printRow(pw, odd, "${service.id}", String.valueOf(dto.serviceId)); + pw.println("
      ${Name}${Value)}
      "); + } + + private void appendServiceRanking(final StringBuilder sb, final ServiceReference ref) + { + int ranking = 0; + if ( ref != null ) + { + final Object obj = ref.getProperty(Constants.SERVICE_RANKING); + if ( obj instanceof Integer) + { + ranking = (Integer)obj; + } + } + sb.append("${ranking} : ").append(String.valueOf(ranking)).append("\n"); + } + + private void printFilterDetails(final PrintWriter pw, final ServletContextDTO dto) + { + if ( dto.filterDTOs.length == 0 ) + { + return; + } + pw.print("

      ${Servlet Context} '"); + pw.print(escapeXml(dto.name)); + pw.println("' ${Registered Filter Services}

      "); + + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + + boolean odd = true; + for (final FilterDTO filter : dto.filterDTOs) + { + final ServiceReference ref = this.getServiceReference(filter.serviceId); + final StringBuilder sb = new StringBuilder(); + sb.append("${service.id} : ").append(String.valueOf(filter.serviceId)).append("\n"); + appendServiceRanking(sb, ref); + sb.append("${async} : ").append(String.valueOf(filter.asyncSupported)).append("\n"); + sb.append("${dispatcher} : ").append(getValueAsString(filter.dispatcher)).append("\n"); + if ( ref != null ) + { + sb.append("${bundle} : "); + sb.append("${#link:"); + sb.append(ref.getBundle().getBundleId()); + sb.append("}"); + sb.append(ref.getBundle().getSymbolicName()); + sb.append("${link#}\n"); + } + + final List patterns = new ArrayList<>(); + patterns.addAll(Arrays.asList(filter.patterns)); + patterns.addAll(Arrays.asList(filter.regexs)); + for(final String name : filter.servletNames) + { + patterns.add("Servlet : " + name); + } + Collections.sort(patterns); + final StringBuilder psb = new StringBuilder(); + for(final String p : patterns) + { + psb.append(p).append('\n'); + } + odd = printRow(pw, odd, psb.toString(), filter.name, sb.toString()); + } + pw.println("
      ${Pattern}${Filter}${Info}
      "); + } + + private String getErrorText(final int reason) + { + switch ( reason ) + { + case DTOConstants.FAILURE_REASON_EXCEPTION_ON_INIT : return "Exception on init"; + case DTOConstants.FAILURE_REASON_NO_SERVLET_CONTEXT_MATCHING : return "No match"; + case DTOConstants.FAILURE_REASON_SERVICE_IN_USE : return "In use"; + case DTOConstants.FAILURE_REASON_SERVICE_NOT_GETTABLE : return "Not gettable"; + case DTOConstants.FAILURE_REASON_SERVLET_CONTEXT_FAILURE : return "Context failure"; + case DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE : return "Shadowed"; + case DTOConstants.FAILURE_REASON_VALIDATION_FAILED : return "Invalid"; + default: return "unknown"; + } + } + private void printFailedFilterDetails(final PrintWriter pw, final RuntimeDTO dto) + { + if ( dto.failedFilterDTOs.length == 0 ) + { + return; + } + pw.print("

      ${Failed Filter Services}

      "); + + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + + boolean odd = true; + for (final FailedFilterDTO filter : dto.failedFilterDTOs) + { + final StringBuilder sb = new StringBuilder(); + sb.append("${reason} : ").append(getErrorText(filter.failureReason)).append("\n"); + final ServiceReference ref = this.getServiceReference(filter.serviceId); + sb.append("${service.id} : ").append(String.valueOf(filter.serviceId)).append("\n"); + appendServiceRanking(sb, ref); + sb.append("${async} : ").append(String.valueOf(filter.asyncSupported)).append("\n"); + sb.append("${dispatcher} : ").append(getValueAsString(filter.dispatcher)).append("\n"); + if ( ref != null ) + { + sb.append("${bundle} : "); + sb.append("${#link:"); + sb.append(ref.getBundle().getBundleId()); + sb.append("}"); + sb.append(ref.getBundle().getSymbolicName()); + sb.append("${link#}\n"); + } + + final List patterns = new ArrayList<>(); + patterns.addAll(Arrays.asList(filter.patterns)); + patterns.addAll(Arrays.asList(filter.regexs)); + for(final String name : filter.servletNames) + { + patterns.add("Servlet : " + name); + } + Collections.sort(patterns); + final StringBuilder psb = new StringBuilder(); + for(final String p : patterns) + { + psb.append(p).append('\n'); + } + odd = printRow(pw, odd, psb.toString(), filter.name, sb.toString()); + } + pw.println("
      ${Pattern}${Filter}${Info}
      "); + } + + private ServiceReference getServiceReference(final long serviceId) + { + if ( serviceId > 0 ) + { + try + { + final ServiceReference[] ref = this.context.getServiceReferences((String)null, "(" + Constants.SERVICE_ID + "=" + String.valueOf(serviceId) + ")"); + if ( ref != null && ref.length > 0 ) + { + return ref[0]; + } + } + catch (final InvalidSyntaxException e) + { + // ignore + } + } + return null; + } + + private void printServletDetails(final PrintWriter pw, final ServletContextDTO dto) + { + if ( dto.servletDTOs.length == 0 ) + { + return; + } + pw.print("

      ${Servlet Context} '"); + pw.print(escapeXml(dto.name)); + pw.println("' ${Registered Servlet Services}

      "); + + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + + boolean odd = true; + for (final ServletDTO servlet : dto.servletDTOs) + { + final StringBuilder sb = new StringBuilder(); + final ServiceReference ref = this.getServiceReference(servlet.serviceId); + sb.append("${service.id} : ").append(String.valueOf(servlet.serviceId)).append("\n"); + appendServiceRanking(sb, ref); + sb.append("${async} : ").append(String.valueOf(servlet.asyncSupported)).append("\n"); + if ( ref != null ) + { + sb.append("${bundle} : "); + sb.append("${#link:"); + sb.append(ref.getBundle().getBundleId()); + sb.append("}"); + sb.append(ref.getBundle().getSymbolicName()); + sb.append("${link#}\n"); + } + + final StringBuilder psb = new StringBuilder(); + for(final String p : servlet.patterns) + { + psb.append(p).append('\n'); + } + odd = printRow(pw, odd, psb.toString(), servlet.name, sb.toString()); + } + pw.println("
      ${Path}${Name}${Info}
      "); + } + + private void printFailedServletDetails(final PrintWriter pw, final RuntimeDTO dto) + { + if ( dto.failedServletDTOs.length == 0 ) + { + return; + } + pw.print("

      ${Failed Servlet Services}

      "); + + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + + boolean odd = true; + for (final FailedServletDTO servlet : dto.failedServletDTOs) + { + final StringBuilder sb = new StringBuilder(); + sb.append("${reason} : ").append(getErrorText(servlet.failureReason)).append("\n"); + final ServiceReference ref = this.getServiceReference(servlet.serviceId); + sb.append("${service.id} : ").append(String.valueOf(servlet.serviceId)).append("\n"); + appendServiceRanking(sb, ref); + sb.append("${async} : ").append(String.valueOf(servlet.asyncSupported)).append("\n"); + if ( ref != null ) + { + sb.append("${bundle} : "); + sb.append("${#link:"); + sb.append(ref.getBundle().getBundleId()); + sb.append("}"); + sb.append(ref.getBundle().getSymbolicName()); + sb.append("${link#}\n"); + } + + final StringBuilder psb = new StringBuilder(); + for(final String p : servlet.patterns) + { + psb.append(p).append('\n'); + } + odd = printRow(pw, odd, psb.toString(), servlet.name, sb.toString()); + } + pw.println("
      ${Path}${Name}${Info}
      "); + } + + private void printResourceDetails(final PrintWriter pw, final ServletContextDTO dto) + { + if ( dto.resourceDTOs.length == 0 ) + { + return; + } + pw.print("

      ${Servlet Context} '"); + pw.print(escapeXml(dto.name)); + pw.println("' ${Registered Resource Services}

      "); + + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + + boolean odd = true; + for (final ResourceDTO rsrc : dto.resourceDTOs) + { + final StringBuilder sb = new StringBuilder(); + final ServiceReference ref = this.getServiceReference(rsrc.serviceId); + sb.append("${service.id} : ").append(String.valueOf(rsrc.serviceId)).append("\n"); + appendServiceRanking(sb, ref); + if ( ref != null ) + { + sb.append("${bundle} : "); + sb.append("${#link:"); + sb.append(ref.getBundle().getBundleId()); + sb.append("}"); + sb.append(ref.getBundle().getSymbolicName()); + sb.append("${link#}\n"); + } + + final StringBuilder psb = new StringBuilder(); + for(final String p : rsrc.patterns) + { + psb.append(p).append('\n'); + } + odd = printRow(pw, odd, psb.toString(), rsrc.prefix, sb.toString()); + } + pw.println("
      ${Path}${Prefix}${Info}
      "); + } + + private void printFailedResourceDetails(final PrintWriter pw, final RuntimeDTO dto) + { + if ( dto.failedResourceDTOs.length == 0 ) + { + return; + } + pw.print("

      ${Failed Resource Services}

      "); + + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + + boolean odd = true; + for (final FailedResourceDTO rsrc : dto.failedResourceDTOs) + { + final StringBuilder sb = new StringBuilder(); + sb.append("${reason} : ").append(getErrorText(rsrc.failureReason)).append("\n"); + final ServiceReference ref = this.getServiceReference(rsrc.serviceId); + sb.append("${service.id} : ").append(String.valueOf(rsrc.serviceId)).append("\n"); + appendServiceRanking(sb, ref); + if ( ref != null ) + { + sb.append("${bundle} : "); + sb.append("${#link:"); + sb.append(ref.getBundle().getBundleId()); + sb.append("}"); + sb.append(ref.getBundle().getSymbolicName()); + sb.append("${link#}\n"); + } + + final StringBuilder psb = new StringBuilder(); + for(final String p : rsrc.patterns) + { + psb.append(p).append('\n'); + } + odd = printRow(pw, odd, psb.toString(), rsrc.prefix, sb.toString()); + } + pw.println("
      ${Path}${Prefix}${Info}
      "); + } + + private void printErrorPageDetails(final PrintWriter pw, final ServletContextDTO dto) + { + if ( dto.errorPageDTOs.length == 0 ) + { + return; + } + pw.print("

      ${Servlet Context} '"); + pw.print(escapeXml(dto.name)); + pw.println("' ${Registered Error Pages}

      "); + + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + + boolean odd = true; + for (final ErrorPageDTO ep : dto.errorPageDTOs) + { + final StringBuilder sb = new StringBuilder(); + final ServiceReference ref = this.getServiceReference(ep.serviceId); + sb.append("${service.id} : ").append(String.valueOf(ep.serviceId)).append("\n"); + appendServiceRanking(sb, ref); + sb.append("${async} : ").append(String.valueOf(ep.asyncSupported)).append("\n"); + if ( ref != null ) + { + sb.append("${bundle} : "); + sb.append("${#link:"); + sb.append(ref.getBundle().getBundleId()); + sb.append("}"); + sb.append(ref.getBundle().getSymbolicName()); + sb.append("${link#}\n"); + } + + final StringBuilder psb = new StringBuilder(); + for(final long p : ep.errorCodes) + { + psb.append(p).append('\n'); + } + for(final String p : ep.exceptions) + { + psb.append(p).append('\n'); + } + odd = printRow(pw, odd, psb.toString(), ep.name, sb.toString()); + } + pw.println("
      ${Path}${Name}${Info}
      "); + } + + private void printFailedErrorPageDetails(final PrintWriter pw, final RuntimeDTO dto) + { + if ( dto.failedErrorPageDTOs.length == 0 ) + { + return; + } + pw.print("

      ${Registered Error Pages}

      "); + + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + + boolean odd = true; + for (final FailedErrorPageDTO ep : dto.failedErrorPageDTOs) + { + final StringBuilder sb = new StringBuilder(); + sb.append("${reason} : ").append(getErrorText(ep.failureReason)).append("\n"); + final ServiceReference ref = this.getServiceReference(ep.serviceId); + sb.append("${service.id} : ").append(String.valueOf(ep.serviceId)).append("\n"); + appendServiceRanking(sb, ref); + sb.append("${async} : ").append(String.valueOf(ep.asyncSupported)).append("\n"); + if ( ref != null ) + { + sb.append("${bundle} : "); + sb.append("${#link:"); + sb.append(ref.getBundle().getBundleId()); + sb.append("}"); + sb.append(ref.getBundle().getSymbolicName()); + sb.append("${link#}\n"); + } + + final StringBuilder psb = new StringBuilder(); + for(final long p : ep.errorCodes) + { + psb.append(p).append('\n'); + } + for(final String p : ep.exceptions) + { + psb.append(p).append('\n'); + } + odd = printRow(pw, odd, psb.toString(), ep.name, sb.toString()); + } + pw.println("
      ${Path}${Name}${Info}
      "); + } + + private void printListenerDetails(final PrintWriter pw, final ServletContextDTO dto) + { + if ( dto.listenerDTOs.length == 0 ) + { + return; + } + pw.print("

      ${Servlet Context} '"); + pw.print(escapeXml(dto.name)); + pw.println("' ${Registered Listeners}

      "); + + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + + boolean odd = true; + for (final ListenerDTO ep : dto.listenerDTOs) + { + final StringBuilder sb = new StringBuilder(); + final ServiceReference ref = this.getServiceReference(ep.serviceId); + sb.append("${service.id} : ").append(String.valueOf(ep.serviceId)).append("\n"); + appendServiceRanking(sb, ref); + if ( ref != null ) + { + sb.append("${bundle} : "); + sb.append("${#link:"); + sb.append(ref.getBundle().getBundleId()); + sb.append("}"); + sb.append(ref.getBundle().getSymbolicName()); + sb.append("${link#}\n"); + } + final StringBuilder tsb = new StringBuilder(); + for(final String t : ep.types) + { + tsb.append(t).append('\n'); + } + odd = printRow(pw, odd, tsb.toString(), sb.toString()); + } + pw.println("
      ${Type}${Info}
      "); + } + + private void printFailedListenerDetails(final PrintWriter pw, final RuntimeDTO dto) + { + if ( dto.failedListenerDTOs.length == 0 ) + { + return; + } + pw.print("

      ${Failed Listeners}

      "); + + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + pw.println(""); + + boolean odd = true; + for (final FailedListenerDTO ep : dto.failedListenerDTOs) + { + final StringBuilder sb = new StringBuilder(); + sb.append("${reason} : ").append(getErrorText(ep.failureReason)).append("\n"); + final ServiceReference ref = this.getServiceReference(ep.serviceId); + sb.append("${service.id} : ").append(String.valueOf(ep.serviceId)).append("\n"); + appendServiceRanking(sb, ref); + if ( ref != null ) + { + sb.append("${bundle} : "); + sb.append("${#link:"); + sb.append(ref.getBundle().getBundleId()); + sb.append("}"); + sb.append(ref.getBundle().getSymbolicName()); + sb.append("${link#}\n"); + } + final StringBuilder tsb = new StringBuilder(); + for(final String t : ep.types) + { + tsb.append(t).append('\n'); + } + odd = printRow(pw, odd, tsb.toString(), sb.toString()); + } + pw.println("
      ${Type}${Info}
      "); + } + + private void printServiceIdAndRanking(final PrintWriter pw, final ServiceReference ref, final long serviceId) + { + pw.print("service.id : "); + pw.println(String.valueOf(serviceId)); + int ranking = 0; + if ( ref != null ) + { + final Object obj = ref.getProperty(Constants.SERVICE_RANKING); + if ( obj instanceof Integer) + { + ranking = (Integer)obj; + } + } + pw.print("Ranking : "); + pw.println(String.valueOf(ranking)); + } + + /** + * @see org.apache.felix.webconsole.ConfigurationPrinter#printConfiguration(java.io.PrintWriter) + */ + public void printConfiguration(final PrintWriter pw) + { + final RuntimeDTO dto = this.runtime.getRuntimeDTO(); + + pw.println("HTTP Service Details"); + pw.println("===================="); + pw.println(); + pw.println("Runtime Properties"); + pw.println("------------------"); + + for(final Map.Entry prop : dto.serviceDTO.properties.entrySet()) + { + pw.print(prop.getKey()); + pw.print(" : "); + pw.println(getValueAsString(prop.getValue())); + } + pw.println(); + for(final ServletContextDTO ctxDto : dto.servletContextDTOs ) + { + pw.print("Servlet Context "); + pw.println(ctxDto.name); + pw.println("-----------------------------------------------"); + + pw.print("Path : "); + pw.println(getContextPath(ctxDto.contextPath)); + printServiceIdAndRanking(pw, this.getServiceReference(ctxDto.serviceId), ctxDto.serviceId); + pw.println(); + if ( ctxDto.servletDTOs.length > 0 ) + { + pw.println("Servlets"); + for (final ServletDTO servlet : ctxDto.servletDTOs) + { + pw.print("Patterns : "); + pw.println(getValueAsString(servlet.patterns)); + pw.print("Name : "); + pw.println(servlet.name); + final ServiceReference ref = this.getServiceReference(servlet.serviceId); + printServiceIdAndRanking(pw, ref, servlet.serviceId); + pw.print("async : "); + pw.println(String.valueOf(servlet.asyncSupported)); + if ( ref != null ) + { + pw.print("Bundle : "); + pw.print(ref.getBundle().getSymbolicName()); + pw.print(" <"); + pw.print(String.valueOf(ref.getBundle().getBundleId())); + pw.println(">"); + } + pw.println(); + } + pw.println(); + } + + if ( ctxDto.filterDTOs.length > 0 ) + { + pw.println("Filters"); + for (final FilterDTO filter : ctxDto.filterDTOs) + { + final List patterns = new ArrayList<>(); + patterns.addAll(Arrays.asList(filter.patterns)); + patterns.addAll(Arrays.asList(filter.regexs)); + for(final String name : filter.servletNames) + { + patterns.add("Servlet : " + name); + } + Collections.sort(patterns); + + pw.print("Patterns : "); + pw.println(patterns); + pw.print("Name : "); + pw.println(filter.name); + final ServiceReference ref = this.getServiceReference(filter.serviceId); + printServiceIdAndRanking(pw, ref, filter.serviceId); + pw.print("async : "); + pw.println(String.valueOf(filter.asyncSupported)); + pw.print("dispatcher : "); + pw.println(getValueAsString(filter.dispatcher)); + if ( ref != null ) + { + pw.print("Bundle : "); + pw.print(ref.getBundle().getSymbolicName()); + pw.print(" <"); + pw.print(String.valueOf(ref.getBundle().getBundleId())); + pw.println(">"); + } + pw.println(); + } + pw.println(); + } + if ( ctxDto.resourceDTOs.length > 0 ) + { + pw.println("Resources"); + for (final ResourceDTO rsrc : ctxDto.resourceDTOs) + { + pw.print("Patterns : "); + pw.println(getValueAsString(rsrc.patterns)); + pw.print("Prefix : "); + pw.println(rsrc.prefix); + final ServiceReference ref = this.getServiceReference(rsrc.serviceId); + printServiceIdAndRanking(pw, ref, rsrc.serviceId); + if ( ref != null ) + { + pw.print("Bundle : "); + pw.print(ref.getBundle().getSymbolicName()); + pw.print(" <"); + pw.print(String.valueOf(ref.getBundle().getBundleId())); + pw.println(">"); + } + pw.println(); + } + pw.println(); + + } + if ( ctxDto.errorPageDTOs.length > 0 ) + { + pw.println("Error Pages"); + for (final ErrorPageDTO ep : ctxDto.errorPageDTOs) + { + final List patterns = new ArrayList<>(); + for(final long p : ep.errorCodes) + { + patterns.add(String.valueOf(p)); + } + for(final String p : ep.exceptions) + { + patterns.add(p); + } + pw.print("Patterns : "); + pw.println(patterns); + pw.print("Name : "); + pw.println(ep.name); + final ServiceReference ref = this.getServiceReference(ep.serviceId); + printServiceIdAndRanking(pw, ref, ep.serviceId); + pw.print("async : "); + pw.println(String.valueOf(ep.asyncSupported)); + if ( ref != null ) + { + pw.print("Bundle : "); + pw.print(ref.getBundle().getSymbolicName()); + pw.print(" <"); + pw.print(String.valueOf(ref.getBundle().getBundleId())); + pw.println(">"); + } + pw.println(); + } + pw.println(); + } + + if ( ctxDto.listenerDTOs.length > 0 ) + { + pw.println("Listeners"); + for (final ListenerDTO ep : ctxDto.listenerDTOs) + { + pw.print("Types : "); + pw.println(getValueAsString(ep.types)); + final ServiceReference ref = this.getServiceReference(ep.serviceId); + printServiceIdAndRanking(pw, ref, ep.serviceId); + if ( ref != null ) + { + pw.print("Bundle : "); + pw.print(ref.getBundle().getSymbolicName()); + pw.print(" <"); + pw.print(String.valueOf(ref.getBundle().getBundleId())); + pw.println(">"); + } + pw.println(); + } + pw.println(); + } + pw.println(); + } + + if ( dto.failedServletContextDTOs.length > 0 ) + { + for(final FailedServletContextDTO ctxDto : dto.failedServletContextDTOs ) + { + pw.print("Failed Servlet Context "); + pw.println(ctxDto.name); + pw.println("-----------------------------------------------"); + + pw.print("Reason : "); + pw.println(getErrorText(ctxDto.failureReason)); + pw.print("Path : "); + pw.println(getContextPath(ctxDto.contextPath)); + printServiceIdAndRanking(pw, this.getServiceReference(ctxDto.serviceId), ctxDto.serviceId); + pw.println(); + } + } + if ( dto.failedServletDTOs.length > 0 ) + { + pw.println("Failed Servlets"); + for (final FailedServletDTO servlet : dto.failedServletDTOs) + { + pw.print("Patterns : "); + pw.println(getValueAsString(servlet.patterns)); + pw.print("Reason : "); + pw.println(getErrorText(servlet.failureReason)); + pw.print("Name : "); + pw.println(servlet.name); + final ServiceReference ref = this.getServiceReference(servlet.serviceId); + printServiceIdAndRanking(pw, ref, servlet.serviceId); + pw.print("async : "); + pw.println(String.valueOf(servlet.asyncSupported)); + if ( ref != null ) + { + pw.print("Bundle : "); + pw.print(ref.getBundle().getSymbolicName()); + pw.print(" <"); + pw.print(String.valueOf(ref.getBundle().getBundleId())); + pw.println(">"); + } + pw.println(); + } + pw.println(); + } + + if ( dto.failedFilterDTOs.length > 0 ) + { + pw.println("Failed Filters"); + for (final FailedFilterDTO filter : dto.failedFilterDTOs) + { + final List patterns = new ArrayList<>(); + patterns.addAll(Arrays.asList(filter.patterns)); + patterns.addAll(Arrays.asList(filter.regexs)); + for(final String name : filter.servletNames) + { + patterns.add("Servlet : " + name); + } + Collections.sort(patterns); + + pw.print("Patterns : "); + pw.println(patterns); + pw.print("Reason : "); + pw.println(getErrorText(filter.failureReason)); + pw.print("Name : "); + pw.println(filter.name); + final ServiceReference ref = this.getServiceReference(filter.serviceId); + printServiceIdAndRanking(pw, ref, filter.serviceId); + pw.print("async : "); + pw.println(String.valueOf(filter.asyncSupported)); + pw.print("dispatcher : "); + pw.println(getValueAsString(filter.dispatcher)); + if ( ref != null ) + { + pw.print("Bundle : "); + pw.print(ref.getBundle().getSymbolicName()); + pw.print(" <"); + pw.print(String.valueOf(ref.getBundle().getBundleId())); + pw.println(">"); + } + pw.println(); + } + pw.println(); + } + if ( dto.failedResourceDTOs.length > 0 ) + { + pw.println("Failed Resources"); + for (final FailedResourceDTO rsrc : dto.failedResourceDTOs) + { + pw.print("Patterns : "); + pw.println(getValueAsString(rsrc.patterns)); + pw.print("Reason : "); + pw.println(getErrorText(rsrc.failureReason)); + pw.print("Prefix : "); + pw.println(rsrc.prefix); + final ServiceReference ref = this.getServiceReference(rsrc.serviceId); + printServiceIdAndRanking(pw, ref, rsrc.serviceId); + if ( ref != null ) + { + pw.print("Bundle : "); + pw.print(ref.getBundle().getSymbolicName()); + pw.print(" <"); + pw.print(String.valueOf(ref.getBundle().getBundleId())); + pw.println(">"); + } + pw.println(); + } + pw.println(); + + } + if ( dto.failedErrorPageDTOs.length > 0 ) + { + pw.println("Failed Error Pages"); + for (final FailedErrorPageDTO ep : dto.failedErrorPageDTOs) + { + final List patterns = new ArrayList<>(); + for(final long p : ep.errorCodes) + { + patterns.add(String.valueOf(p)); + } + for(final String p : ep.exceptions) + { + patterns.add(p); + } + pw.print("Patterns : "); + pw.println(patterns); + pw.print("Reason : "); + pw.println(getErrorText(ep.failureReason)); + pw.print("Name : "); + pw.println(ep.name); + final ServiceReference ref = this.getServiceReference(ep.serviceId); + printServiceIdAndRanking(pw, ref, ep.serviceId); + pw.print("async : "); + pw.println(String.valueOf(ep.asyncSupported)); + if ( ref != null ) + { + pw.print("Bundle : "); + pw.print(ref.getBundle().getSymbolicName()); + pw.print(" <"); + pw.print(String.valueOf(ref.getBundle().getBundleId())); + pw.println(">"); + } + pw.println(); + } + pw.println(); + } + + if ( dto.failedListenerDTOs.length > 0 ) + { + pw.println("Listeners"); + for (final FailedListenerDTO ep : dto.failedListenerDTOs) + { + pw.print("Types : "); + pw.println(getValueAsString(ep.types)); + pw.print("Reason : "); + pw.println(getErrorText(ep.failureReason)); + final ServiceReference ref = this.getServiceReference(ep.serviceId); + printServiceIdAndRanking(pw, ref, ep.serviceId); + if ( ref != null ) + { + pw.print("Bundle : "); + pw.print(ref.getBundle().getSymbolicName()); + pw.print(" <"); + pw.print(String.valueOf(ref.getBundle().getBundleId())); + pw.println(">"); + } + pw.println(); + } + pw.println(); + } + pw.println(); + } + + public void unregister() + { + if (this.serviceReg != null) + { + this.serviceReg.unregister(); + this.serviceReg = null; + } + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/context/ExtServletContext.java b/http/base/src/main/java/org/apache/felix/http/base/internal/context/ExtServletContext.java new file mode 100644 index 00000000000..1dd59fad0d7 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/context/ExtServletContext.java @@ -0,0 +1,46 @@ +/* + * 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.http.base.internal.context; + +import java.io.IOException; + +import javax.servlet.ServletContext; +import javax.servlet.ServletRequestAttributeListener; +import javax.servlet.ServletRequestListener; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionListener; + +import org.apache.felix.http.base.internal.HttpConfig; + +public interface ExtServletContext extends ServletContext +{ + boolean handleSecurity(HttpServletRequest req, HttpServletResponse res) throws IOException; + + void finishSecurity(HttpServletRequest req, HttpServletResponse res); + + HttpSessionAttributeListener getHttpSessionAttributeListener(); + + HttpSessionListener getHttpSessionListener(); + + ServletRequestListener getServletRequestListener(); + + ServletRequestAttributeListener getServletRequestAttributeListener(); + + HttpConfig getConfig(); +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/context/ExtServletContextWrapper.java b/http/base/src/main/java/org/apache/felix/http/base/internal/context/ExtServletContextWrapper.java new file mode 100644 index 00000000000..6a04c24acfe --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/context/ExtServletContextWrapper.java @@ -0,0 +1,412 @@ +/* + * 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.http.base.internal.context; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.Map; +import java.util.Set; + +import javax.servlet.Filter; +import javax.servlet.FilterRegistration; +import javax.servlet.RequestDispatcher; +import javax.servlet.Servlet; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; +import javax.servlet.ServletRegistration.Dynamic; +import javax.servlet.ServletRequestAttributeListener; +import javax.servlet.ServletRequestListener; +import javax.servlet.SessionCookieConfig; +import javax.servlet.SessionTrackingMode; +import javax.servlet.descriptor.JspConfigDescriptor; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionListener; + +/** + * Wrapper of an {code ExtServletContex}. + * This implementation simply forwards to the delegate. + */ +public abstract class ExtServletContextWrapper implements ExtServletContext +{ + private final ExtServletContext delegate; + + public ExtServletContextWrapper(final ExtServletContext delegate) + { + this.delegate = delegate; + } + + @Override + public boolean handleSecurity(final HttpServletRequest req, + final HttpServletResponse res) throws IOException + { + return delegate.handleSecurity(req, res); + } + + @Override + public HttpSessionAttributeListener getHttpSessionAttributeListener() + { + return delegate.getHttpSessionAttributeListener(); + } + + @Override + public HttpSessionListener getHttpSessionListener() + { + return delegate.getHttpSessionListener(); + } + + @Override + public ServletRequestListener getServletRequestListener() + { + return delegate.getServletRequestListener(); + } + + @Override + public ServletRequestAttributeListener getServletRequestAttributeListener() + { + return delegate.getServletRequestAttributeListener(); + } + + @Override + public String getContextPath() + { + return delegate.getContextPath(); + } + + @Override + public ServletContext getContext(final String uripath) + { + return delegate.getContext(uripath); + } + + @Override + public int getMajorVersion() + { + return delegate.getMajorVersion(); + } + + @Override + public int getMinorVersion() + { + return delegate.getMinorVersion(); + } + + @Override + public int getEffectiveMajorVersion() + { + return delegate.getEffectiveMajorVersion(); + } + + @Override + public int getEffectiveMinorVersion() + { + return delegate.getEffectiveMinorVersion(); + } + + @Override + public String getMimeType(final String file) + { + return delegate.getMimeType(file); + } + + @Override + public Set getResourcePaths(final String path) + { + return delegate.getResourcePaths(path); + } + + @Override + public URL getResource(final String path) throws MalformedURLException + { + return delegate.getResource(path); + } + + @Override + public InputStream getResourceAsStream(final String path) + { + return delegate.getResourceAsStream(path); + } + + @Override + public RequestDispatcher getRequestDispatcher(final String path) + { + return delegate.getRequestDispatcher(path); + } + + @Override + public RequestDispatcher getNamedDispatcher(final String name) + { + return delegate.getNamedDispatcher(name); + } + + @Override + @SuppressWarnings("deprecation") + public Servlet getServlet(final String name) throws ServletException + { + return delegate.getServlet(name); + } + + @Override + @SuppressWarnings("deprecation") + public Enumeration getServlets() + { + return delegate.getServlets(); + } + + @Override + @SuppressWarnings("deprecation") + public Enumeration getServletNames() + { + return delegate.getServletNames(); + } + + @Override + public void log(final String msg) + { + delegate.log(msg); + } + + @Override + @SuppressWarnings("deprecation") + public void log(final Exception exception, final String msg) + { + delegate.log(exception, msg); + } + + @Override + public void log(final String message, final Throwable throwable) + { + delegate.log(message, throwable); + } + + @Override + public String getRealPath(final String path) + { + return delegate.getRealPath(path); + } + + @Override + public String getServerInfo() + { + return delegate.getServerInfo(); + } + + @Override + public String getInitParameter(final String name) + { + return delegate.getInitParameter(name); + } + + @Override + public Enumeration getInitParameterNames() + { + return delegate.getInitParameterNames(); + } + + @Override + public boolean setInitParameter(final String name, final String value) + { + return delegate.setInitParameter(name, value); + } + + @Override + public Object getAttribute(final String name) + { + return delegate.getAttribute(name); + } + + @Override + public Enumeration getAttributeNames() + { + return delegate.getAttributeNames(); + } + + @Override + public void setAttribute(final String name, final Object object) + { + delegate.setAttribute(name, object); + } + + @Override + public void removeAttribute(final String name) + { + delegate.removeAttribute(name); + } + + @Override + public String getServletContextName() { + return delegate.getServletContextName(); + } + + @Override + public Dynamic addServlet(final String servletName, final String className) + { + return delegate.addServlet(servletName, className); + } + + @Override + public Dynamic addServlet(final String servletName, final Servlet servlet) + { + return delegate.addServlet(servletName, servlet); + } + + @Override + public Dynamic addServlet(final String servletName, + final Class servletClass) + { + return delegate.addServlet(servletName, servletClass); + } + + @Override + public T createServlet(final Class clazz) + throws ServletException + { + return delegate.createServlet(clazz); + } + + @Override + public ServletRegistration getServletRegistration(final String servletName) + { + return delegate.getServletRegistration(servletName); + } + + @Override + public Map getServletRegistrations() + { + return delegate.getServletRegistrations(); + } + + @Override + public javax.servlet.FilterRegistration.Dynamic addFilter( + final String filterName, final String className) + { + return delegate.addFilter(filterName, className); + } + + @Override + public javax.servlet.FilterRegistration.Dynamic addFilter( + final String filterName, final Filter filter) + { + return delegate.addFilter(filterName, filter); + } + + @Override + public javax.servlet.FilterRegistration.Dynamic addFilter( + final String filterName, final Class filterClass) + { + return delegate.addFilter(filterName, filterClass); + } + + @Override + public T createFilter(final Class clazz) + throws ServletException + { + return delegate.createFilter(clazz); + } + + @Override + public FilterRegistration getFilterRegistration(final String filterName) + { + return delegate.getFilterRegistration(filterName); + } + + @Override + public Map getFilterRegistrations() + { + return delegate.getFilterRegistrations(); + } + + @Override + public SessionCookieConfig getSessionCookieConfig() + { + return delegate.getSessionCookieConfig(); + } + + @Override + public void setSessionTrackingModes( + final Set sessionTrackingModes) + { + delegate.setSessionTrackingModes(sessionTrackingModes); + } + + @Override + public Set getDefaultSessionTrackingModes() + { + return delegate.getDefaultSessionTrackingModes(); + } + + @Override + public Set getEffectiveSessionTrackingModes() + { + return delegate.getEffectiveSessionTrackingModes(); + } + + @Override + public void addListener(final String className) + { + delegate.addListener(className); + } + + @Override + public void addListener(final T t) + { + delegate.addListener(t); + } + + @Override + public void addListener(final Class listenerClass) + { + delegate.addListener(listenerClass); + } + + @Override + public T createListener(final Class clazz) + throws ServletException + { + return delegate.createListener(clazz); + } + + @Override + public JspConfigDescriptor getJspConfigDescriptor() + { + return delegate.getJspConfigDescriptor(); + } + + @Override + public ClassLoader getClassLoader() + { + return delegate.getClassLoader(); + } + + @Override + public void declareRoles(final String... roleNames) + { + delegate.declareRoles(roleNames); + } + + @Override + public String getVirtualServerName() + { + return delegate.getVirtualServerName(); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/Dispatcher.java b/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/Dispatcher.java new file mode 100644 index 00000000000..e3f703992e0 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/Dispatcher.java @@ -0,0 +1,173 @@ +/* + * 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.http.base.internal.dispatch; + +import java.io.IOException; +import java.util.Set; + +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletRequestEvent; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.handler.FilterHandler; +import org.apache.felix.http.base.internal.handler.HttpSessionWrapper; +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.apache.felix.http.base.internal.registry.HandlerRegistry; +import org.apache.felix.http.base.internal.registry.PathResolution; +import org.apache.felix.http.base.internal.registry.PerContextHandlerRegistry; +import org.apache.felix.http.base.internal.whiteboard.WhiteboardManager; +import org.jetbrains.annotations.Nullable; +import org.osgi.service.http.whiteboard.Preprocessor; + +public final class Dispatcher +{ + private final HandlerRegistry handlerRegistry; + + private volatile WhiteboardManager whiteboardManager; + + public Dispatcher(final HandlerRegistry handlerRegistry) + { + this.handlerRegistry = handlerRegistry; + } + + /** + * Set or unset the whiteboard manager. + * @param service The whiteboard manager or {@code null} + */ + public void setWhiteboardManager(@Nullable final WhiteboardManager service) + { + this.whiteboardManager = service; + } + + /** + * Responsible for dispatching a given request to the actual applicable servlet and/or filters in the local registry. + * + * @param req the {@link ServletRequest} to dispatch; + * @param res the {@link ServletResponse} to dispatch. + * @throws ServletException in case of exceptions during the actual dispatching; + * @throws IOException in case of I/O problems. + */ + public void dispatch(final HttpServletRequest req, final HttpServletResponse res) throws ServletException, IOException + { + final WhiteboardManager mgr = this.whiteboardManager; + if ( mgr == null ) + { + // not active, always return 404 + res.sendError(404); + return; + } + + // check for invalidating session(s) first + final HttpSession session = req.getSession(false); + if ( session != null ) + { + final Set names = HttpSessionWrapper.getExpiredSessionContextNames(session); + mgr.sessionDestroyed(session, names); + } + + // invoke preprocessors and then dispatching + mgr.invokePreprocessors(req, res, new Preprocessor() { + + @Override + public void init(final FilterConfig filterConfig) throws ServletException + { + // nothing to do + } + + @Override + public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) + throws IOException, ServletException + { + final HttpServletRequest req = (HttpServletRequest)request; + final HttpServletResponse res = (HttpServletResponse)response; + // get full decoded path for dispatching + // we can't use req.getRequestURI() or req.getRequestURL() as these are returning the encoded path + String path = req.getServletPath(); + if ( path == null ) + { + path = ""; + } + if ( req.getPathInfo() != null ) + { + path = path.concat(req.getPathInfo()); + } + final String requestURI = path; + + // Determine which servlet we should forward the request to... + final PathResolution pr = handlerRegistry.resolveServlet(requestURI); + + final PerContextHandlerRegistry errorRegistry = (pr != null ? pr.handlerRegistry : handlerRegistry.getBestMatchingRegistry(requestURI)); + final String servletName = (pr != null ? pr.handler.getName() : null); + final HttpServletResponse wrappedResponse = new ServletResponseWrapper(req, res, servletName, errorRegistry); + if ( pr == null ) + { + wrappedResponse.sendError(404); + return; + } + + final ExtServletContext servletContext = pr.handler.getContext(); + final RequestInfo requestInfo = new RequestInfo(pr.servletPath, pr.pathInfo, null, req.getRequestURI()); + + final HttpServletRequest wrappedRequest = new ServletRequestWrapper(req, servletContext, requestInfo, null, + pr.handler.getServletInfo().isAsyncSupported(), + pr.handler.getMultipartConfig(), + pr.handler.getMultipartSecurityContext()); + final FilterHandler[] filterHandlers = handlerRegistry.getFilters(pr, req.getDispatcherType(), pr.requestURI); + + try + { + if ( servletContext.getServletRequestListener() != null ) + { + servletContext.getServletRequestListener().requestInitialized(new ServletRequestEvent(servletContext, wrappedRequest)); + } + final FilterChain filterChain = new InvocationChain(pr.handler, filterHandlers); + filterChain.doFilter(wrappedRequest, wrappedResponse); + + } + catch ( final Exception e) + { + SystemLogger.error("Exception while processing request to " + requestURI, e); + req.setAttribute(RequestDispatcher.ERROR_EXCEPTION, e); + req.setAttribute(RequestDispatcher.ERROR_EXCEPTION_TYPE, e.getClass().getName()); + + wrappedResponse.sendError(500); + } + finally + { + if ( servletContext.getServletRequestListener() != null ) + { + servletContext.getServletRequestListener().requestDestroyed(new ServletRequestEvent(servletContext, wrappedRequest)); + } + } } + + @Override + public void destroy() + { + // nothing to do + } + }); + + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/DispatcherServlet.java b/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/DispatcherServlet.java new file mode 100644 index 00000000000..47544bfcf1e --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/DispatcherServlet.java @@ -0,0 +1,51 @@ +/* + * 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.http.base.internal.dispatch; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.felix.http.base.internal.HttpServiceController; + +/** + * The dispatcher servlet is registered in the container. + * It is dispatching requests to the http implementation. + * It does not start the http service, this needs to be done + * through the {@link HttpServiceController}. + */ +public class DispatcherServlet extends HttpServlet +{ + private static final long serialVersionUID = -7692620012572476116L; + + private final Dispatcher controller; + + public DispatcherServlet(final Dispatcher controller) + { + this.controller = controller; + } + + @Override + protected void service(final HttpServletRequest req, final HttpServletResponse res) + throws ServletException, IOException + { + this.controller.dispatch(req, res); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/InvocationChain.java b/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/InvocationChain.java new file mode 100644 index 00000000000..ca547d4bb54 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/InvocationChain.java @@ -0,0 +1,99 @@ +/* + * 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.http.base.internal.dispatch; + +import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; +import static javax.servlet.http.HttpServletResponse.SC_OK; + +import java.io.IOException; + +import org.jetbrains.annotations.NotNull; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.felix.http.base.internal.handler.FilterHandler; +import org.apache.felix.http.base.internal.handler.ServletHandler; + +public class InvocationChain implements FilterChain +{ + private final ServletHandler servletHandler; + private final FilterHandler[] filterHandlers; + + private int index = -1; + + public InvocationChain(@NotNull final ServletHandler servletHandler, @NotNull final FilterHandler[] filterHandlers) + { + this.filterHandlers = filterHandlers; + this.servletHandler = servletHandler; + } + + @Override + public final void doFilter(@NotNull final ServletRequest req, @NotNull final ServletResponse res) throws IOException, ServletException + { + boolean callFinish = false; + if ( this.index == -1 ) + { + final HttpServletRequest hReq = (HttpServletRequest) req; + final HttpServletResponse hRes = (HttpServletResponse) res; + + // invoke security + if ( !servletHandler.getContext().handleSecurity(hReq, hRes)) + { + // FELIX-3988: If the response is not yet committed and still has the default + // status, we're going to override this and send an error instead. + if (!res.isCommitted() && (hRes.getStatus() == SC_OK || hRes.getStatus() == 0)) + { + hRes.sendError(SC_FORBIDDEN); + } + + // we're done + return; + } + else + { + callFinish = true; + } + } + this.index++; + + try + { + if (this.index < this.filterHandlers.length) + { + this.filterHandlers[this.index].handle(req, res, this); + } + else + { + // Last entry in the chain... + this.servletHandler.handle(req, res); + } + } + finally { + if ( callFinish ) + { + final HttpServletRequest hReq = (HttpServletRequest) req; + final HttpServletResponse hRes = (HttpServletResponse) res; + + servletHandler.getContext().finishSecurity(hReq, hRes); + } + } + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/MultipartConfig.java b/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/MultipartConfig.java new file mode 100644 index 00000000000..42e04caa734 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/MultipartConfig.java @@ -0,0 +1,76 @@ +/* + * 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.http.base.internal.dispatch; + +import org.apache.commons.fileupload.disk.DiskFileItemFactory; + +public final class MultipartConfig +{ + public static final MultipartConfig DEFAULT_CONFIG = new MultipartConfig(null, null, -1, -1); + + public static final MultipartConfig INVALID_CONFIG = new MultipartConfig(null, null, -1, -1); + + /** + * Specifies the multipart threshold + */ + public final int multipartThreshold; + + /** + * Specifies the multipart location + */ + public final String multipartLocation; + + /** + * Specifies the multipart max file size + */ + public final long multipartMaxFileSize; + + /** + * Specifies the multipart max request size + */ + public final long multipartMaxRequestSize; + + public MultipartConfig(final Integer threshold, + final String location, + final long maxFileSize, + final long maxRequestSize) + { + if ( threshold != null && threshold > 0) + { + this.multipartThreshold = threshold; + } + else + { + this.multipartThreshold = DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD; + } + this.multipartLocation = location; + if ( maxFileSize > 0 || maxFileSize == -1 ) { + this.multipartMaxFileSize = maxFileSize; + } + else + { + this.multipartMaxFileSize = -1; + } + if ( maxRequestSize > 0 || maxRequestSize == -1 ) { + this.multipartMaxRequestSize = maxRequestSize; + } + else + { + this.multipartMaxRequestSize = -1; + } + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/RequestDispatcherImpl.java b/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/RequestDispatcherImpl.java new file mode 100644 index 00000000000..34427b207a6 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/RequestDispatcherImpl.java @@ -0,0 +1,113 @@ +/* + * 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.http.base.internal.dispatch; + +import java.io.IOException; + +import javax.servlet.DispatcherType; +import javax.servlet.FilterChain; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import org.apache.felix.http.base.internal.handler.FilterHandler; +import org.apache.felix.http.base.internal.registry.ServletResolution; +import org.apache.felix.http.base.internal.util.UriUtils; + +/** + * Wrapper implementation for {@link RequestDispatcher}. + */ +public final class RequestDispatcherImpl implements RequestDispatcher +{ + private final RequestInfo requestInfo; + private final ServletResolution resolution; + + public RequestDispatcherImpl(final ServletResolution resolution, + final RequestInfo requestInfo) + { + this.resolution = resolution; + this.requestInfo = requestInfo; + } + + @Override + public void forward(ServletRequest request, ServletResponse response) throws ServletException, IOException + { + if (response.isCommitted()) + { + throw new ServletException("Response has been committed"); + } + else + { + // See section 9.4 of Servlet 3.0 spec + response.resetBuffer(); + } + + try + { + final ServletRequestWrapper req = new ServletRequestWrapper((HttpServletRequest) request, + this.resolution.handler.getContext(), + this.requestInfo, + DispatcherType.FORWARD, + this.resolution.handler.getServletInfo().isAsyncSupported(), + this.resolution.handler.getMultipartConfig(), + this.resolution.handler.getMultipartSecurityContext()); + final String requestURI = UriUtils.concat(this.requestInfo.servletPath, this.requestInfo.pathInfo); + final FilterHandler[] filterHandlers = this.resolution.handlerRegistry.getFilterHandlers(this.resolution.handler, DispatcherType.FORWARD, requestURI); + + final FilterChain filterChain = new InvocationChain(resolution.handler, filterHandlers); + filterChain.doFilter( req, response); + } + finally + { + // After a forward has taken place, the results should be committed, + // see section 9.4 of Servlet 3.0 spec... + if (!request.isAsyncStarted()) + { + response.flushBuffer(); + try { + try { + response.getWriter().close(); + } catch ( final IllegalStateException ise ) { + // output stream has been used + response.getOutputStream().close(); + } + } catch ( final Exception ignore ) { + // ignore everything, see FELIX-5053 + } + } + } + } + + @Override + public void include(ServletRequest request, ServletResponse response) throws ServletException, IOException + { + final ServletRequestWrapper req = new ServletRequestWrapper((HttpServletRequest) request, + this.resolution.handler.getContext(), + this.requestInfo, + DispatcherType.INCLUDE, + this.resolution.handler.getServletInfo().isAsyncSupported(), + this.resolution.handler.getMultipartConfig(), + this.resolution.handler.getMultipartSecurityContext()); + final String requestURI = UriUtils.concat(this.requestInfo.servletPath, this.requestInfo.pathInfo); + final FilterHandler[] filterHandlers = this.resolution.handlerRegistry.getFilterHandlers(this.resolution.handler, DispatcherType.INCLUDE, requestURI); + + final FilterChain filterChain = new InvocationChain(resolution.handler, filterHandlers); + filterChain.doFilter( req, response); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/RequestInfo.java b/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/RequestInfo.java new file mode 100644 index 00000000000..c9112dcbbbd --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/RequestInfo.java @@ -0,0 +1,45 @@ +/* + * 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.http.base.internal.dispatch; + +public final class RequestInfo +{ + final String servletPath; + final String pathInfo; + final String queryString; + final String requestURI; + + public RequestInfo(final String servletPath, + final String pathInfo, + final String queryString, + final String requestURI) + { + this.servletPath = servletPath; + this.pathInfo = pathInfo; + this.queryString = queryString; + this.requestURI = requestURI; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder("RequestInfo[servletPath ="); + sb.append(this.servletPath).append(", pathInfo = ").append(this.pathInfo); + sb.append(", queryString = ").append(this.queryString).append("]"); + return sb.toString(); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/ServletRequestWrapper.java b/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/ServletRequestWrapper.java new file mode 100644 index 00000000000..09b98b68342 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/ServletRequestWrapper.java @@ -0,0 +1,540 @@ +/* + * 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.http.base.internal.dispatch; + +import static javax.servlet.RequestDispatcher.FORWARD_CONTEXT_PATH; +import static javax.servlet.RequestDispatcher.FORWARD_PATH_INFO; +import static javax.servlet.RequestDispatcher.FORWARD_QUERY_STRING; +import static javax.servlet.RequestDispatcher.FORWARD_REQUEST_URI; +import static javax.servlet.RequestDispatcher.FORWARD_SERVLET_PATH; +import static javax.servlet.RequestDispatcher.INCLUDE_CONTEXT_PATH; +import static javax.servlet.RequestDispatcher.INCLUDE_PATH_INFO; +import static javax.servlet.RequestDispatcher.INCLUDE_QUERY_STRING; +import static javax.servlet.RequestDispatcher.INCLUDE_REQUEST_URI; +import static javax.servlet.RequestDispatcher.INCLUDE_SERVLET_PATH; +import static org.apache.felix.http.base.internal.util.UriUtils.concat; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import javax.servlet.AsyncContext; +import javax.servlet.DispatcherType; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletRequestAttributeEvent; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpSession; +import javax.servlet.http.Part; + +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.fileupload.servlet.ServletRequestContext; +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.handler.HttpSessionWrapper; +import org.osgi.framework.Bundle; +import org.osgi.service.http.HttpContext; +import org.osgi.service.useradmin.Authorization; + +final class ServletRequestWrapper extends HttpServletRequestWrapper +{ + private final DispatcherType type; + private final RequestInfo requestInfo; + private final ExtServletContext servletContext; + private final boolean asyncSupported; + private final MultipartConfig multipartConfig; + private final Bundle bundleForSecurityCheck; + + private Collection parts; + + public ServletRequestWrapper(final HttpServletRequest req, + final ExtServletContext servletContext, + final RequestInfo requestInfo, + final DispatcherType type, + final boolean asyncSupported, + final MultipartConfig multipartConfig, + final Bundle bundleForSecurityCheck) + { + super(req); + + this.asyncSupported = asyncSupported; + this.multipartConfig = multipartConfig; + this.servletContext = servletContext; + this.requestInfo = requestInfo; + this.type = type; + this.bundleForSecurityCheck = bundleForSecurityCheck; + } + + @Override + public Object getAttribute(String name) + { + HttpServletRequest request = (HttpServletRequest) getRequest(); + if (isInclusionDispatcher()) + { + // The javax.servlet.include.* attributes refer to the information of the *included* request, + // meaning that the request information comes from the *original* request... + if (INCLUDE_REQUEST_URI.equals(name)) + { + return this.requestInfo.requestURI; + } + else if (INCLUDE_CONTEXT_PATH.equals(name)) + { + return request.getContextPath(); + } + else if (INCLUDE_SERVLET_PATH.equals(name)) + { + return this.requestInfo.servletPath; + } + else if (INCLUDE_PATH_INFO.equals(name)) + { + return this.requestInfo.pathInfo; + } + else if (INCLUDE_QUERY_STRING.equals(name)) + { + return this.requestInfo.queryString; + } + } + else if (isForwardingDispatcher()) + { + // The javax.servlet.forward.* attributes refer to the information of the *original* request, + // meaning that the request information comes from the *forwarded* request... + if (FORWARD_REQUEST_URI.equals(name)) + { + return super.getRequestURI(); + } + else if (FORWARD_CONTEXT_PATH.equals(name)) + { + return request.getContextPath(); + } + else if (FORWARD_SERVLET_PATH.equals(name)) + { + return super.getServletPath(); + } + else if (FORWARD_PATH_INFO.equals(name)) + { + return super.getPathInfo(); + } + else if (FORWARD_QUERY_STRING.equals(name)) + { + return super.getQueryString(); + } + } + return super.getAttribute(name); + } + + @Override + public String getAuthType() + { + String authType = (String) getAttribute(HttpContext.AUTHENTICATION_TYPE); + if (authType == null) + { + authType = super.getAuthType(); + } + return authType; + } + + @Override + public String getContextPath() + { + return this.getServletContext().getContextPath(); + } + + @Override + public DispatcherType getDispatcherType() + { + return (this.type == null) ? super.getDispatcherType() : this.type; + } + + @Override + public String getPathInfo() + { + if ( this.isInclusionDispatcher() ) + { + return super.getPathInfo(); + } + return this.requestInfo.pathInfo; + } + + @Override + @SuppressWarnings("deprecation") + public String getPathTranslated() + { + final String info = getPathInfo(); + return (null == info) ? null : getRealPath(info); + } + + @Override + public String getRemoteUser() + { + String remoteUser = (String) getAttribute(HttpContext.REMOTE_USER); + if (remoteUser != null) + { + return remoteUser; + } + + return super.getRemoteUser(); + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) + { + // See section 9.1 of Servlet 3.0 specification... + if (path == null) + { + return null; + } + // Handle relative paths, see Servlet 3.0 spec, section 9.1 last paragraph. + boolean relPath = !path.startsWith("/") && !"".equals(path); + if (relPath) + { + path = concat(getServletPath(), path); + } + return this.servletContext.getRequestDispatcher(path); + } + + @Override + public String getRequestURI() + { + if ( isInclusionDispatcher() ) + { + return super.getRequestURI(); + } + return this.requestInfo.requestURI; + } + + @Override + public ServletContext getServletContext() + { + return this.servletContext; + } + + @Override + public String getServletPath() + { + if ( isInclusionDispatcher() ) + { + return super.getServletPath(); + } + return this.requestInfo.servletPath; + } + + @Override + public HttpSession getSession() { + return this.getSession(true); + } + + @Override + public HttpSession getSession(boolean create) + { + // FELIX-2797: wrap the original HttpSession to provide access to the correct ServletContext... + final HttpSession session = super.getSession(create); + if (session == null) + { + return null; + } + // check if internal session is available + if ( !create && !HttpSessionWrapper.hasSession(this.servletContext.getServletContextName(), session) ) + { + return null; + } + return new HttpSessionWrapper(session, this.servletContext, this.servletContext.getConfig(), false); + } + + @Override + public boolean isUserInRole(String role) + { + Authorization authorization = (Authorization) getAttribute(HttpContext.AUTHORIZATION); + if (authorization != null) + { + return authorization.hasRole(role); + } + + return super.isUserInRole(role); + } + + @Override + public void setAttribute(final String name, final Object value) + { + if ( value == null ) + { + this.removeAttribute(name); + } + final Object oldValue = this.getAttribute(name); + super.setAttribute(name, value); + if ( this.servletContext.getServletRequestAttributeListener() != null ) + { + if ( oldValue == null ) + { + this.servletContext.getServletRequestAttributeListener().attributeAdded(new ServletRequestAttributeEvent(this.servletContext, this, name, value)); + } + else + { + this.servletContext.getServletRequestAttributeListener().attributeReplaced(new ServletRequestAttributeEvent(this.servletContext, this, name, oldValue)); + } + } + } + + @Override + public void removeAttribute(final String name) { + final Object oldValue = this.getAttribute(name); + if ( oldValue != null ) + { + super.removeAttribute(name); + if ( this.servletContext.getServletRequestAttributeListener() != null ) + { + this.servletContext.getServletRequestAttributeListener().attributeRemoved(new ServletRequestAttributeEvent(this.servletContext, this, name, oldValue)); + } + } + } + + @Override + public String toString() + { + return getClass().getSimpleName() + "->" + super.getRequest(); + } + + private boolean isForwardingDispatcher() + { + return (DispatcherType.FORWARD == this.type) && (this.requestInfo != null); + } + + private boolean isInclusionDispatcher() + { + return (DispatcherType.INCLUDE == this.type) && (this.requestInfo != null); + } + + @Override + public AsyncContext startAsync() throws IllegalStateException + { + if ( !this.asyncSupported ) + { + throw new IllegalStateException(); + } + return super.startAsync(); + } + + @Override + public AsyncContext startAsync(final ServletRequest servletRequest, + final ServletResponse servletResponse) throws IllegalStateException + { + if ( !this.asyncSupported ) + { + throw new IllegalStateException(); + } + return super.startAsync(servletRequest, servletResponse); + } + + @Override + public boolean isAsyncSupported() + { + return this.asyncSupported; + } + + private Collection checkMultipart() throws IOException, ServletException + { + if ( parts == null ) + { + if ( ServletFileUpload.isMultipartContent(this) ) + { + if ( this.multipartConfig == null) + { + throw new IllegalStateException("Multipart not enabled for servlet."); + } + + if ( System.getSecurityManager() == null ) + { + handleMultipart(); + } + else + { + final AccessControlContext ctx = bundleForSecurityCheck.adapt(AccessControlContext.class); + final IOException ioe = AccessController.doPrivileged(new PrivilegedAction() + { + + @Override + public IOException run() + { + try + { + handleMultipart(); + } + catch ( final IOException ioe) + { + return ioe; + } + return null; + } + }, ctx); + if ( ioe != null ) + { + throw ioe; + } + } + + } + else + { + throw new ServletException("Not a multipart request"); + } + } + return parts; + } + + private void handleMultipart() throws IOException + { + // Create a new file upload handler + final ServletFileUpload upload = new ServletFileUpload(); + upload.setSizeMax(this.multipartConfig.multipartMaxRequestSize); + upload.setFileSizeMax(this.multipartConfig.multipartMaxFileSize); + upload.setFileItemFactory(new DiskFileItemFactory(this.multipartConfig.multipartThreshold, + new File(this.multipartConfig.multipartLocation))); + + // Parse the request + List items = null; + try + { + items = upload.parseRequest(new ServletRequestContext(this)); + } + catch (final FileUploadException fue) + { + throw new IOException("Error parsing multipart request", fue); + } + parts = new ArrayList<>(); + for(final FileItem item : items) + { + parts.add(new Part() { + + @Override + public InputStream getInputStream() throws IOException + { + return item.getInputStream(); + } + + @Override + public String getContentType() + { + return item.getContentType(); + } + + @Override + public String getName() + { + return item.getFieldName(); + } + + @Override + public String getSubmittedFileName() + { + return item.getName(); + } + + @Override + public long getSize() + { + return item.getSize(); + } + + @Override + public void write(String fileName) throws IOException + { + try + { + item.write(new File(fileName)); + } + catch (IOException e) + { + throw e; + } + catch (Exception e) + { + throw new IOException(e); + } + } + + @Override + public void delete() throws IOException + { + item.delete(); + } + + @Override + public String getHeader(String name) + { + return item.getHeaders().getHeader(name); + } + + @Override + public Collection getHeaders(String name) + { + final List values = new ArrayList<>(); + final Iterator iter = item.getHeaders().getHeaders(name); + while ( iter.hasNext() ) + { + values.add(iter.next()); + } + return values; + } + + @Override + public Collection getHeaderNames() + { + final List names = new ArrayList<>(); + final Iterator iter = item.getHeaders().getHeaderNames(); + while ( iter.hasNext() ) + { + names.add(iter.next()); + } + return names; + } + }); + } + } + @Override + public Collection getParts() throws IOException, ServletException + { + return checkMultipart(); + + } + + @Override + public Part getPart(String name) throws IOException, ServletException + { + Collection parts = this.checkMultipart(); + for(final Part p : parts) + { + if ( p.getName().equals(name) ) + { + return p; + } + } + return null; + } + +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/ServletResponseWrapper.java b/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/ServletResponseWrapper.java new file mode 100644 index 00000000000..8c61d7f1507 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/dispatch/ServletResponseWrapper.java @@ -0,0 +1,141 @@ +/* + * 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.http.base.internal.dispatch; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; + +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import javax.servlet.DispatcherType; +import javax.servlet.FilterChain; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +import org.apache.felix.http.base.internal.handler.FilterHandler; +import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.registry.PerContextHandlerRegistry; + +final class ServletResponseWrapper extends HttpServletResponseWrapper +{ + + private final HttpServletRequest request; + + private final AtomicInteger invocationCount = new AtomicInteger(); + + private final PerContextHandlerRegistry errorRegistry; + + private final String servletName; + + public ServletResponseWrapper(@NotNull final HttpServletRequest req, + @NotNull final HttpServletResponse res, + @Nullable final String servletName, + @Nullable final PerContextHandlerRegistry errorRegistry) + { + super(res); + this.request = req; + this.servletName = servletName; + this.errorRegistry = errorRegistry; + } + + @Override + public void sendError(int sc) throws IOException + { + sendError(sc, null); + } + + @Override + public void sendError(final int code, final String message) throws IOException + { + resetBuffer(); + + setStatus(code); + + boolean invokeSuper = true; + + if ( invocationCount.incrementAndGet() == 1 ) + { + // If we are allowed to have a body + if (code != SC_NO_CONTENT && + code != SC_NOT_MODIFIED && + code != SC_PARTIAL_CONTENT && + code >= SC_OK) + { + final Throwable exception = (Throwable)request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); + final ServletHandler errorResolution = (errorRegistry == null ? null : + errorRegistry.getErrorHandler(code, exception)); + + if ( errorResolution != null ) + { + try + { + request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, new Integer(code)); + if ( message != null ) + { + request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message); + } + request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, request.getRequestURI()); + if ( this.servletName != null ) + { + request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME, this.servletName); + } + + final String servletPath = null; + final String pathInfo = request.getRequestURI(); + final String queryString = null; // XXX + + final RequestInfo requestInfo = new RequestInfo(servletPath, pathInfo, queryString, pathInfo); + + final FilterHandler[] filterHandlers = errorRegistry.getFilterHandlers(errorResolution, DispatcherType.ERROR, request.getRequestURI()); + + final ServletRequestWrapper reqWrapper = new ServletRequestWrapper(request, + errorResolution.getContext(), + requestInfo, + null, + false, + null, + null); + final FilterChain filterChain = new InvocationChain(errorResolution, filterHandlers); + filterChain.doFilter(reqWrapper, this); + + invokeSuper = false; + } + catch (final ServletException e) + { + // ignore + } + finally + { + request.removeAttribute(RequestDispatcher.ERROR_STATUS_CODE); + request.removeAttribute(RequestDispatcher.ERROR_MESSAGE); + request.removeAttribute(RequestDispatcher.ERROR_REQUEST_URI); + request.removeAttribute(RequestDispatcher.ERROR_SERVLET_NAME); + request.removeAttribute(RequestDispatcher.ERROR_EXCEPTION); + request.removeAttribute(RequestDispatcher.ERROR_EXCEPTION_TYPE); + } + } + } + } + if ( invokeSuper ) + { + super.sendError(code, message); + } + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/FilterConfigImpl.java b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/FilterConfigImpl.java new file mode 100644 index 00000000000..342260f2455 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/FilterConfigImpl.java @@ -0,0 +1,65 @@ +/* + * 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.http.base.internal.handler; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.Map; + +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; + +/** + * Implementation of the filter configuration. + */ +public final class FilterConfigImpl implements FilterConfig +{ + private final String name; + private final ServletContext context; + private final Map initParams; + + public FilterConfigImpl(final String name, final ServletContext context, final Map initParams) + { + this.name = name; + this.context = context; + this.initParams = initParams; + } + + @Override + public String getFilterName() + { + return this.name; + } + + @Override + public ServletContext getServletContext() + { + return this.context; + } + + @Override + public String getInitParameter(final String name) + { + return this.initParams.get(name); + } + + @Override + public Enumeration getInitParameterNames() + { + return Collections.enumeration(this.initParams.keySet()); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/FilterHandler.java b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/FilterHandler.java new file mode 100644 index 00000000000..e7c754cede7 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/FilterHandler.java @@ -0,0 +1,205 @@ +/* + * 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.http.base.internal.handler; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.apache.felix.http.base.internal.runtime.FilterInfo; +import org.jetbrains.annotations.NotNull; +import org.osgi.service.http.runtime.dto.DTOConstants; + +/** + * The filter handler handles the initialization and destruction of filter + * objects. + */ +public abstract class FilterHandler implements Comparable +{ + private final long contextServiceId; + + private final FilterInfo filterInfo; + + private final ExtServletContext context; + + private volatile Filter filter; + + protected volatile int useCount; + + public FilterHandler(final long contextServiceId, + final ExtServletContext context, + final FilterInfo filterInfo) + { + this.contextServiceId = contextServiceId; + this.context = context; + this.filterInfo = filterInfo; + } + + @Override + public int compareTo(final FilterHandler other) + { + return this.filterInfo.compareTo(other.filterInfo); + } + + public long getContextServiceId() + { + return this.contextServiceId; + } + + public ExtServletContext getContext() + { + return this.context; + } + + public Filter getFilter() + { + return filter; + } + + protected void setFilter(final Filter f) + { + this.filter = f; + } + + public FilterInfo getFilterInfo() + { + return this.filterInfo; + } + + public String getName() + { + String name = this.filterInfo.getName(); + if (name == null) + { + final Filter local = this.filter; + if ( local != null ) + { + name = local.getClass().getName(); + } + } + return name; + } + + /** + * Initialize the object + * @return {code -1} on success, a failure reason according to {@link DTOConstants} otherwise. + */ + public int init() + { + if ( this.useCount > 0 ) + { + this.useCount++; + return -1; + } + + if (this.filter == null) + { + return DTOConstants.FAILURE_REASON_SERVICE_NOT_GETTABLE; + } + + try + { + this.filter.init(new FilterConfigImpl(getName(), getContext(), getFilterInfo().getInitParameters())); + } + catch (final Exception e) + { + SystemLogger.error(this.getFilterInfo().getServiceReference(), + "Error during calling init() on filter " + this.filter, + e); + return DTOConstants.FAILURE_REASON_EXCEPTION_ON_INIT; + } + this.useCount++; + return -1; + } + + public void handle(@NotNull final ServletRequest req, + @NotNull final ServletResponse res, + @NotNull final FilterChain chain) throws ServletException, IOException + { + final Filter local = this.filter; + if ( local != null ) + { + local.doFilter(req, res, chain); + } + else + { + throw new ServletException("Filter has been unregistered."); + } + } + + public boolean destroy() + { + if (this.filter == null) + { + return false; + } + + this.useCount--; + if ( this.useCount == 0 ) + { + try + { + filter.destroy(); + } + catch ( final Exception ignore ) + { + // we ignore this + SystemLogger.error(this.getFilterInfo().getServiceReference(), + "Error during calling destroy() on filter " + this.filter, + ignore); + } + + filter = null; + return true; + } + return false; + } + + public boolean dispose() + { + // fully destroy the filter + this.useCount = 1; + return this.destroy(); + } + + @Override + public int hashCode() + { + return 31 + filterInfo.hashCode(); + } + + @Override + public boolean equals(final Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null || getClass() != obj.getClass() ) + { + return false; + } + final FilterHandler other = (FilterHandler) obj; + return filterInfo.equals(other.filterInfo); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpServiceFilterHandler.java b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpServiceFilterHandler.java new file mode 100644 index 00000000000..013af7f8b81 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpServiceFilterHandler.java @@ -0,0 +1,37 @@ +/* + * 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.http.base.internal.handler; + +import javax.servlet.Filter; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.runtime.FilterInfo; +import org.apache.felix.http.base.internal.service.HttpServiceFactory; + +/** + * Servlet handler for filters registered through the ext http service. + */ +public final class HttpServiceFilterHandler extends FilterHandler +{ + public HttpServiceFilterHandler(final ExtServletContext context, + final FilterInfo filterInfo, + final Filter filter) + { + super(HttpServiceFactory.HTTP_SERVICE_CONTEXT_SERVICE_ID, context, filterInfo); + this.setFilter(filter); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpServiceServletHandler.java b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpServiceServletHandler.java new file mode 100644 index 00000000000..ec1781c7e06 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpServiceServletHandler.java @@ -0,0 +1,45 @@ +/* + * 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.http.base.internal.handler; + +import javax.servlet.Servlet; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.apache.felix.http.base.internal.service.HttpServiceFactory; + +/** + * Servlet handler for servlets registered through the http service. + */ +public final class HttpServiceServletHandler extends ServletHandler +{ + public HttpServiceServletHandler(final ExtServletContext context, + final ServletInfo servletInfo, + final Servlet servlet) + { + this(HttpServiceFactory.HTTP_SERVICE_CONTEXT_SERVICE_ID, context, servletInfo, servlet); + } + + public HttpServiceServletHandler(final long contextServiceId, + final ExtServletContext context, + final ServletInfo servletInfo, + final Servlet servlet) + { + super(contextServiceId, context, servletInfo); + this.setServlet(servlet); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpSessionWrapper.java b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpSessionWrapper.java new file mode 100644 index 00000000000..e446c55775d --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/HttpSessionWrapper.java @@ -0,0 +1,491 @@ +/* + * 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.http.base.internal.handler; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionBindingListener; +import javax.servlet.http.HttpSessionContext; +import javax.servlet.http.HttpSessionEvent; + +import org.apache.felix.http.base.internal.HttpConfig; +import org.apache.felix.http.base.internal.context.ExtServletContext; + +/** + * The session wrapper keeps track of the internal session, manages their attributes + * separately and also handles session timeout. + */ +@SuppressWarnings("deprecation") +public class HttpSessionWrapper implements HttpSession +{ + /** All special attributes are prefixed with this prefix. */ + private static final String PREFIX = "org.apache.felix.http.session.context."; + + /** For each internal session, the attributes are prefixed with this followed by the context id */ + private static final String ATTR_PREFIX = PREFIX + "attr."; + + /** The created time for the internal session (appended with context id) */ + private static final String ATTR_CREATED = PREFIX + "created."; + + /** The last accessed time for the internal session (appended with context id), as Epoch time (milliseconds). */ + private static final String ATTR_LAST_ACCESSED = PREFIX + "lastaccessed."; + + /** The max inactive time (appended with context id), in seconds. */ + private static final String ATTR_MAX_INACTIVE = PREFIX + "maxinactive."; + + /** The underlying container session. */ + private final HttpSession delegate; + + /** The corresponding servlet context. */ + private final ExtServletContext context; + + /** The id for this session. */ + private final String sessionId; + + /** The key prefix for attributes belonging to this session. */ + private final String keyPrefix; + + /** Flag to handle the validity of this session. */ + private volatile boolean isInvalid = false; + + /** The time this has been created. */ + private final long created; + + /** The time this has been last accessed. */ + private final long lastAccessed; + + /** The max timeout interval. */ + private int maxTimeout; + + /** + * Is this a new session? + */ + private final boolean isNew; + + /** + * Session handling configuration + */ + private final HttpConfig config; + + + + public static boolean hasSession(final String contextName, final HttpSession session) + { + return session.getAttribute(ATTR_CREATED.concat(contextName)) != null; + } + + public static Set getExpiredSessionContextNames(final HttpSession session) + { + final long now = System.currentTimeMillis(); + + final Set names = new HashSet<>(); + final Enumeration attrNames = session.getAttributeNames(); + while (attrNames.hasMoreElements()) + { + final String name = attrNames.nextElement(); + if (name.startsWith(ATTR_LAST_ACCESSED)) + { + final String id = name.substring(ATTR_LAST_ACCESSED.length()); + + final long lastAccess = (Long) session.getAttribute(name); + final long maxTimeout = 1000L * ((Integer) session.getAttribute(ATTR_MAX_INACTIVE + id)); + + if ((maxTimeout > 0) && (lastAccess + maxTimeout) < now) + { + names.add(id); + } + } + } + return names; + } + + /** + * Get the names of all contexts using a session. + * @param session The underlying session + * @return The set of names + */ + public static Set getSessionContextNames(final HttpSession session) + { + final Set names = new HashSet<>(); + final Enumeration attrNames = session.getAttributeNames(); + while (attrNames.hasMoreElements()) + { + final String name = attrNames.nextElement(); + if (name.startsWith(ATTR_LAST_ACCESSED)) + { + final String id = name.substring(ATTR_LAST_ACCESSED.length()); + names.add(id); + } + } + + return names; + } + + /** + * Creates a new {@link HttpSessionWrapper} instance. + */ + public HttpSessionWrapper(final HttpSession session, + final ExtServletContext context, + final HttpConfig config, + final boolean terminate) + { + this.config = config; + this.delegate = session; + this.context = context; + this.sessionId = context.getServletContextName(); + this.keyPrefix = ATTR_PREFIX.concat(this.sessionId).concat("."); + + final String createdAttrName = ATTR_CREATED.concat(this.sessionId); + + final long now = System.currentTimeMillis(); + if ( session.getAttribute(createdAttrName) == null ) + { + this.created = now; + this.maxTimeout = session.getMaxInactiveInterval(); + this.isNew = true; + + session.setAttribute(createdAttrName, this.created); + session.setAttribute(ATTR_MAX_INACTIVE.concat(this.sessionId), this.maxTimeout); + + context.getHttpSessionListener().sessionCreated(new HttpSessionEvent(this)); + } + else + { + this.created = (Long)session.getAttribute(createdAttrName); + this.maxTimeout = (Integer)session.getAttribute(ATTR_MAX_INACTIVE.concat(this.sessionId)); + this.isNew = false; + } + + this.lastAccessed = now; + if ( !terminate ) + { + session.setAttribute(ATTR_LAST_ACCESSED.concat(this.sessionId), this.lastAccessed); + } + } + + /** + * Helper method to get the real key within the real session. + */ + private String getKey(final String name) + { + return this.keyPrefix.concat(name); + } + + /** + * Check whether this session is still valid. + * @throws IllegalStateException if session is not valid anymore + */ + private void checkInvalid() + { + if ( this.isInvalid ) + { + throw new IllegalStateException("Session is invalid."); + } + } + + @Override + public Object getAttribute(final String name) + { + this.checkInvalid(); + Object result = this.delegate.getAttribute(this.getKey(name)); + if ( result instanceof SessionBindingValueListenerWrapper ) + { + result = ((SessionBindingValueListenerWrapper)result).getHttpSessionBindingListener(); + } + return result; + } + + @Override + public Enumeration getAttributeNames() + { + this.checkInvalid(); + final Enumeration e = this.delegate.getAttributeNames(); + return new Enumeration() { + + String next = peek(); + + private String peek() + { + while ( e.hasMoreElements() ) + { + final String name = e.nextElement(); + if ( name.startsWith(keyPrefix)) + { + return name.substring(keyPrefix.length()); + } + } + return null; + } + + @Override + public boolean hasMoreElements() { + return next != null; + } + + @Override + public String nextElement() { + if ( next == null ) + { + throw new NoSuchElementException(); + } + final String result = next; + next = this.peek(); + return result; + } + }; + + } + + @Override + public long getCreationTime() + { + this.checkInvalid(); + return this.created; + } + + @Override + public String getId() + { + this.checkInvalid(); + if ( this.config.isUniqueSessionId() ) + { + return this.delegate.getId().concat("-").concat(this.sessionId); + } + return this.delegate.getId(); + } + + @Override + public long getLastAccessedTime() + { + this.checkInvalid(); + return this.lastAccessed; + } + + @Override + public int getMaxInactiveInterval() + { + // no validity check conforming to the javadocs + return this.maxTimeout; + } + + @Override + public ServletContext getServletContext() + { + // no validity check conforming to the javadocs + return this.context; + } + + @Override + public Object getValue(String name) + { + return this.getAttribute(name); + } + + @Override + public String[] getValueNames() + { + final List names = new ArrayList<>(); + final Enumeration e = this.getAttributeNames(); + while ( e.hasMoreElements() ) + { + names.add(e.nextElement()); + } + return names.toArray(new String[names.size()]); + } + + @Override + public void invalidate() + { + this.checkInvalid(); + + // session listener must be called before the session is invalidated + context.getHttpSessionListener().sessionDestroyed(new HttpSessionEvent(this)); + + this.delegate.removeAttribute(ATTR_CREATED + this.sessionId); + this.delegate.removeAttribute(ATTR_LAST_ACCESSED + this.sessionId); + this.delegate.removeAttribute(ATTR_MAX_INACTIVE + this.sessionId); + + // remove all attributes belonging to this session + final Enumeration names = this.delegate.getAttributeNames(); + while ( names.hasMoreElements() ) + { + final String name = names.nextElement(); + + if ( name.startsWith(this.keyPrefix) ) { + this.removeAttribute(name.substring(this.keyPrefix.length())); + } + } + + if ( this.config.isInvalidateContainerSession() ) + { + // if the session is empty we can invalidate + final Enumeration remainingNames = this.delegate.getAttributeNames(); + if ( !remainingNames.hasMoreElements() ) + { + this.delegate.invalidate(); + } + } + + this.isInvalid = true; + } + + @Override + public boolean isNew() + { + this.checkInvalid(); + return this.isNew; + } + + @Override + public void putValue(final String name, final Object value) + { + this.setAttribute(name, value); + } + + @Override + public void removeAttribute(final String name) + { + this.checkInvalid(); + final Object oldValue = this.getAttribute(name); + if ( oldValue != null ) + { + this.delegate.removeAttribute(this.getKey(name)); + if ( oldValue instanceof HttpSessionBindingListener ) + { + ((HttpSessionBindingListener)oldValue).valueUnbound(new HttpSessionBindingEvent(this, name)); + } + if ( this.context.getHttpSessionAttributeListener() != null ) + { + this.context.getHttpSessionAttributeListener().attributeRemoved(new HttpSessionBindingEvent(this, name, oldValue)); + } + } + } + + @Override + public void removeValue(final String name) + { + this.removeAttribute(name); + } + + @Override + public void setAttribute(final String name, final Object value) + { + this.checkInvalid(); + if ( value == null ) + { + this.removeAttribute(name); + return; + } + + final Object oldValue = this.getAttribute(name); + // wrap http session binding listener to avoid container calling it! + if ( value instanceof HttpSessionBindingListener ) + { + this.delegate.setAttribute(this.getKey(name), + new SessionBindingValueListenerWrapper((HttpSessionBindingListener)value)); + } + else + { + this.delegate.setAttribute(this.getKey(name), value); + } + if ( value instanceof HttpSessionBindingListener ) + { + ((HttpSessionBindingListener)value).valueBound(new HttpSessionBindingEvent(this, name)); + } + + if ( this.context.getHttpSessionAttributeListener() != null ) + { + if ( oldValue != null ) + { + this.context.getHttpSessionAttributeListener().attributeReplaced(new HttpSessionBindingEvent(this, name, oldValue)); + } + else + { + this.context.getHttpSessionAttributeListener().attributeAdded(new HttpSessionBindingEvent(this, name, value)); + } + } + } + + @Override + public void setMaxInactiveInterval(final int interval) + { + if ( this.delegate.getMaxInactiveInterval() < interval ) + { + this.delegate.setMaxInactiveInterval(interval); + } + this.maxTimeout = interval; + this.delegate.setAttribute(ATTR_MAX_INACTIVE + this.sessionId, interval); + } + + @Override + public HttpSessionContext getSessionContext() + { + // no need to check validity conforming to the javadoc + return this.delegate.getSessionContext(); + } + + private static final class SessionBindingValueListenerWrapper implements Serializable + { + + private static final long serialVersionUID = 4009563108883768425L; + + private final HttpSessionBindingListener listener; + + public SessionBindingValueListenerWrapper(final HttpSessionBindingListener listener) + { + this.listener = listener; + } + + public HttpSessionBindingListener getHttpSessionBindingListener() + { + return listener; + } + } + + @Override + public int hashCode() + { + return this.getId().concat(this.sessionId).hashCode(); + } + + @Override + public boolean equals(final Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null || this.getClass() != obj.getClass() ) + { + return false; + } + final HttpSessionWrapper other = (HttpSessionWrapper) obj; + return other.getId().concat(other.sessionId).equals(this.getId().concat(this.sessionId)); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/ListenerHandler.java b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/ListenerHandler.java new file mode 100644 index 00000000000..415ff633f4f --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/ListenerHandler.java @@ -0,0 +1,146 @@ +/* + * 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.http.base.internal.handler; + +import java.util.EventListener; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.runtime.ListenerInfo; +import org.osgi.service.http.runtime.dto.DTOConstants; + +/** + * The listener handler handles the initialization and destruction of listener + * objects. + */ +public abstract class ListenerHandler implements Comparable +{ + private final long contextServiceId; + + private final ListenerInfo listenerInfo; + + private final ExtServletContext context; + + private EventListener listener; + + protected volatile int useCount; + + public ListenerHandler(final long contextServiceId, + final ExtServletContext context, + final ListenerInfo listenerInfo) + { + this.contextServiceId = contextServiceId; + this.context = context; + this.listenerInfo = listenerInfo; + } + + @Override + public int compareTo(final ListenerHandler other) + { + return this.listenerInfo.compareTo(other.listenerInfo); + } + + public ExtServletContext getContext() + { + return this.context; + } + + public long getContextServiceId() + { + return this.contextServiceId; + } + + public EventListener getListener() + { + return listener; + } + + protected void setListener(final EventListener f) + { + this.listener = f; + } + + public ListenerInfo getListenerInfo() + { + return this.listenerInfo; + } + + /** + * Initialize the object + * @return {code -1} on success, a failure reason according to {@link DTOConstants} otherwise. + */ + public int init() + { + if ( this.useCount > 0 ) + { + this.useCount++; + return -1; + } + + if (this.listener == null) + { + return DTOConstants.FAILURE_REASON_SERVICE_NOT_GETTABLE; + } + + this.useCount++; + return -1; + } + + public boolean destroy() + { + if (this.listener == null) + { + return false; + } + + this.useCount--; + if ( this.useCount == 0 ) + { + + listener = null; + return true; + } + return false; + } + + public boolean dispose() + { + // fully destroy the listener + this.useCount = 1; + return this.destroy(); + } + + @Override + public int hashCode() + { + return 31 + listenerInfo.hashCode(); + } + + @Override + public boolean equals(final Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null || getClass() != obj.getClass() ) + { + return false; + } + final ListenerHandler other = (ListenerHandler) obj; + return listenerInfo.equals(other.listenerInfo); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/PreprocessorHandler.java b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/PreprocessorHandler.java new file mode 100644 index 00000000000..22439b44055 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/PreprocessorHandler.java @@ -0,0 +1,170 @@ +/* + * 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.http.base.internal.handler; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.apache.felix.http.base.internal.runtime.PreprocessorInfo; +import org.jetbrains.annotations.NotNull; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.runtime.dto.DTOConstants; +import org.osgi.service.http.whiteboard.Preprocessor; + +/** + * The preprocessor handler handles the initialization and destruction of preprocessor + * objects. + */ +public class PreprocessorHandler implements Comparable +{ + private final PreprocessorInfo info; + + private final ServletContext context; + + private final BundleContext bundleContext; + + private volatile Preprocessor preprocessor; + + public PreprocessorHandler(final BundleContext bundleContext, + final ServletContext context, + final PreprocessorInfo info) + { + this.bundleContext = bundleContext; + this.context = context; + this.info = info; + } + + @Override + public int compareTo(final PreprocessorHandler other) + { + return this.info.compareTo(other.info); + } + + public ServletContext getContext() + { + return this.context; + } + + public PreprocessorInfo getPreprocessorInfo() + { + return this.info; + } + + public int init() + { + final ServiceReference serviceReference = this.info.getServiceReference(); + this.preprocessor = this.bundleContext.getService(serviceReference); + + if (this.preprocessor == null) + { + return DTOConstants.FAILURE_REASON_SERVICE_NOT_GETTABLE; + } + + try + { + this.preprocessor.init(new FilterConfigImpl(this.preprocessor.getClass().getName(), + getContext(), + getPreprocessorInfo().getInitParameters())); + } + catch (final Exception e) + { + SystemLogger.error(this.getPreprocessorInfo().getServiceReference(), + "Error during calling init() on preprocessor " + this.preprocessor, + e); + + this.preprocessor = null; + this.bundleContext.ungetService(serviceReference); + + return DTOConstants.FAILURE_REASON_EXCEPTION_ON_INIT; + } + + return -1; + } + + public boolean destroy() + { + if (this.preprocessor == null) + { + return false; + } + + try + { + preprocessor.destroy(); + } + catch ( final Exception ignore ) + { + // we ignore this + SystemLogger.error(this.getPreprocessorInfo().getServiceReference(), + "Error during calling destroy() on preprocessor " + this.preprocessor, + ignore); + } + this.preprocessor = null; + this.bundleContext.ungetService(this.info.getServiceReference()); + + return true; + } + + public void handle(@NotNull final ServletRequest req, + @NotNull final ServletResponse res, + @NotNull final FilterChain chain) throws ServletException, IOException + { + final Preprocessor local = this.preprocessor; + if ( local != null ) + { + local.doFilter(req, res, chain); + } + else + { + throw new ServletException("Preprocessor has been unregistered"); + } + } + + public boolean dispose() + { + // fully destroy the preprocessor + return this.destroy(); + } + + @Override + public int hashCode() + { + return 31 + info.hashCode(); + } + + @Override + public boolean equals(final Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null || getClass() != obj.getClass() ) + { + return false; + } + final PreprocessorHandler other = (PreprocessorHandler) obj; + return info.equals(other.info); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/ServletConfigImpl.java b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/ServletConfigImpl.java new file mode 100644 index 00000000000..4ac55d6e50f --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/ServletConfigImpl.java @@ -0,0 +1,67 @@ +/* + * 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.http.base.internal.handler; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.Map; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; + +/** + * Implementation of the servlet configuration + */ +public final class ServletConfigImpl implements ServletConfig +{ + private final String name; + private final ServletContext context; + private final Map initParams; + + public ServletConfigImpl(final String name, + final ServletContext context, + final Map initParams) + { + this.name = name; + this.context = context; + this.initParams = initParams; + } + + @Override + public String getServletName() + { + return this.name; + } + + @Override + public ServletContext getServletContext() + { + return this.context; + } + + @Override + public String getInitParameter(final String name) + { + return this.initParams.get(name); + } + + @Override + public Enumeration getInitParameterNames() + { + return Collections.enumeration(this.initParams.keySet()); + } +} \ No newline at end of file diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/ServletHandler.java b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/ServletHandler.java new file mode 100644 index 00000000000..8d6635157e9 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/ServletHandler.java @@ -0,0 +1,248 @@ +/* + * 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.http.base.internal.handler; + +import java.io.File; +import java.io.IOException; + +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.dispatch.MultipartConfig; +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.osgi.framework.Bundle; +import org.osgi.service.http.runtime.dto.DTOConstants; + +/** + * The servlet handler handles the initialization and destruction of + * a servlet. + */ +public abstract class ServletHandler implements Comparable +{ + private static final String TEMP_DIR = System.getProperty("java.io.tmpdir"); + + private static final String JAVA_SERVLET_TEMP_DIR_PROP = "javax.servlet.content.tempdir"; + + private final long contextServiceId; + + private final ServletInfo servletInfo; + + private final ExtServletContext context; + + private volatile Servlet servlet; + + protected volatile int useCount; + + private final MultipartConfig mpConfig; + + public ServletHandler(final long contextServiceId, + final ExtServletContext context, + final ServletInfo servletInfo) + { + this.contextServiceId = contextServiceId; + this.context = context; + this.servletInfo = servletInfo; + final MultipartConfig origConfig = servletInfo.getMultipartConfig(); + if ( origConfig != null ) + { + String location = origConfig.multipartLocation; + if ( location == null ) { + final Object obj = context == null ? null : context.getAttribute(JAVA_SERVLET_TEMP_DIR_PROP); + if ( obj != null ) { + if ( obj instanceof File ) { + location = ((File)obj).getAbsolutePath(); + } else { + location = obj.toString(); + } + } + } + if ( location == null ) { + location = TEMP_DIR; + } + this.mpConfig = new MultipartConfig(origConfig.multipartThreshold, + location, + origConfig.multipartMaxFileSize, + origConfig.multipartMaxRequestSize); + } + else + { + this.mpConfig = null; + } + } + + @Override + public int compareTo(final ServletHandler other) + { + return this.servletInfo.compareTo(other.servletInfo); + } + + public long getContextServiceId() + { + return this.contextServiceId; + } + + public ExtServletContext getContext() + { + return this.context; + } + + public Servlet getServlet() + { + return servlet; + } + + protected void setServlet(final Servlet s) + { + this.servlet = s; + } + + public void handle(final ServletRequest req, final ServletResponse res) + throws ServletException, IOException + { + final Servlet local = this.servlet; + if ( local != null ) + { + local.service(req, res); + } + else + { + throw new ServletException("Servlet has been unregistered"); + } + } + + public ServletInfo getServletInfo() + { + return this.servletInfo; + } + + public String getName() + { + String name = this.servletInfo.getName(); + if (name == null ) + { + final Servlet local = this.servlet; + if ( local != null ) + { + name = local.getClass().getName(); + } + } + return name; + } + + /** + * Initialize the object + * @return {code -1} on success, a failure reason according to {@link DTOConstants} otherwise. + */ + public int init() + { + if ( this.useCount > 0 ) + { + this.useCount++; + return -1; + } + + if (this.servlet == null) + { + return DTOConstants.FAILURE_REASON_SERVICE_NOT_GETTABLE; + } + + try + { + servlet.init(new ServletConfigImpl(getName(), getContext(), getServletInfo().getInitParameters())); + } + catch (final Exception e) + { + SystemLogger.error(this.getServletInfo().getServiceReference(), + "Error during calling init() on servlet " + this.servlet, + e); + return DTOConstants.FAILURE_REASON_EXCEPTION_ON_INIT; + } + this.useCount++; + return -1; + } + + + public boolean destroy() + { + if (this.servlet == null) + { + return false; + } + + this.useCount--; + if ( this.useCount == 0 ) + { + try + { + servlet.destroy(); + } + catch ( final Exception ignore ) + { + // we ignore this + SystemLogger.error(this.getServletInfo().getServiceReference(), + "Error during calling destroy() on servlet " + this.servlet, + ignore); + } + + servlet = null; + return true; + } + return false; + } + + public boolean dispose() + { + // fully destroy the servlet + this.useCount = 1; + return this.destroy(); + } + + @Override + public int hashCode() + { + return 31 + servletInfo.hashCode(); + } + + @Override + public boolean equals(final Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null || getClass() != obj.getClass() ) + { + return false; + } + final ServletHandler other = (ServletHandler) obj; + return servletInfo.equals(other.servletInfo); + } + + public MultipartConfig getMultipartConfig() + { + return mpConfig; + } + + public Bundle getMultipartSecurityContext() + { + return null; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/WhiteboardFilterHandler.java b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/WhiteboardFilterHandler.java new file mode 100644 index 00000000000..dc33ff78c29 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/WhiteboardFilterHandler.java @@ -0,0 +1,88 @@ +/* + * 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.http.base.internal.handler; + +import javax.servlet.Filter; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.runtime.FilterInfo; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceObjects; +import org.osgi.framework.ServiceReference; + +/** + * Filter holder for filters registered through the http whiteboard. + */ +public final class WhiteboardFilterHandler extends FilterHandler +{ + private final BundleContext bundleContext; + + public WhiteboardFilterHandler(final long contextServiceId, + final ExtServletContext context, + final FilterInfo filterInfo, + final BundleContext bundleContext) + { + super(contextServiceId, context, filterInfo); + this.bundleContext = bundleContext; + } + + @Override + public int init() + { + if ( this.useCount > 0 ) + { + this.useCount++; + return -1; + } + + final ServiceReference serviceReference = getFilterInfo().getServiceReference(); + final ServiceObjects so = this.bundleContext.getServiceObjects(serviceReference); + + this.setFilter((so == null ? null : so.getService())); + + final int reason = super.init(); + if ( reason != -1 ) + { + if ( so != null ) + { + so.ungetService(this.getFilter()); + } + this.setFilter(null); + } + return reason; + } + + @Override + public boolean destroy() + { + final Filter s = this.getFilter(); + if ( s != null ) + { + if ( super.destroy() ) + { + + final ServiceObjects so = this.bundleContext.getServiceObjects(getFilterInfo().getServiceReference()); + if (so != null) + { + so.ungetService(s); + } + return true; + } + } + return false; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/WhiteboardListenerHandler.java b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/WhiteboardListenerHandler.java new file mode 100644 index 00000000000..a9edbe633a0 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/WhiteboardListenerHandler.java @@ -0,0 +1,88 @@ +/* + * 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.http.base.internal.handler; + +import java.util.EventListener; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.runtime.ListenerInfo; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceObjects; +import org.osgi.framework.ServiceReference; + +/** + * Listener handler for listeners registered through the http whiteboard. + */ +public final class WhiteboardListenerHandler extends ListenerHandler +{ + private final BundleContext bundleContext; + + public WhiteboardListenerHandler(final long contextServiceId, + final ExtServletContext context, + final ListenerInfo listenerInfo, + final BundleContext bundleContext) + { + super(contextServiceId, context, listenerInfo); + this.bundleContext = bundleContext; + } + + @Override + public int init() + { + if ( this.useCount > 0 ) + { + this.useCount++; + return -1; + } + + final ServiceReference serviceReference = getListenerInfo().getServiceReference(); + final ServiceObjects so = this.bundleContext.getServiceObjects(serviceReference); + + this.setListener((so == null ? null : so.getService())); + + final int reason = super.init(); + if ( reason != -1 ) + { + if ( so != null ) + { + so.ungetService(this.getListener()); + } + this.setListener(null); + } + return reason; + } + + @Override + public boolean destroy() + { + final EventListener s = this.getListener(); + if ( s != null ) + { + if ( super.destroy() ) + { + + final ServiceObjects so = this.bundleContext.getServiceObjects(getListenerInfo().getServiceReference()); + if (so != null) + { + so.ungetService(s); + } + return true; + } + } + return false; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/handler/WhiteboardServletHandler.java b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/WhiteboardServletHandler.java new file mode 100644 index 00000000000..b942f5dea7e --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/handler/WhiteboardServletHandler.java @@ -0,0 +1,150 @@ +/* + * 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.http.base.internal.handler; + +import java.io.FilePermission; + +import javax.servlet.Servlet; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceObjects; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.runtime.dto.DTOConstants; + +/** + * Servlet handler for servlets registered through the http whiteboard. + */ +public final class WhiteboardServletHandler extends ServletHandler +{ + private final BundleContext bundleContext; + + private final int multipartErrorCode; + + private final Bundle multipartSecurityContext; + + public WhiteboardServletHandler(final long contextServiceId, + final ExtServletContext context, + final ServletInfo servletInfo, + final BundleContext contextBundleContext, + final Bundle registeringBundle, + final Bundle httpWhiteboardBundle) + { + super(contextServiceId, context, servletInfo); + this.bundleContext = contextBundleContext; + int errorCode = -1; + // if multipart upload is enabled and a security manager is active + // we need to check permissions + if ( this.getMultipartConfig() != null && System.getSecurityManager() != null ) + { + final FilePermission writePerm = new FilePermission(this.getMultipartConfig().multipartLocation, "read,write,delete"); + if ( servletInfo.getMultipartConfig().multipartLocation == null ) + { + // Default location, whiteboard need writePerm, using bundle read perm + multipartSecurityContext = httpWhiteboardBundle; + if ( !httpWhiteboardBundle.hasPermission(writePerm)) + { + errorCode = DTOConstants.FAILURE_REASON_WHITEBOARD_WRITE_TO_DEFAULT_DENIED; + } + else + { + final FilePermission readPerm = new FilePermission(this.getMultipartConfig().multipartLocation, "read"); + if ( !registeringBundle.hasPermission(readPerm) ) + { + errorCode = DTOConstants.FAILURE_REASON_SERVLET_READ_FROM_DEFAULT_DENIED; + } + } + } + else + { + multipartSecurityContext = registeringBundle; + // Provided location, whiteboard and using bundle need write perm + if ( !registeringBundle.hasPermission(writePerm) ) + { + errorCode = DTOConstants.FAILURE_REASON_SERVLET_WRITE_TO_LOCATION_DENIED; + } + if ( !httpWhiteboardBundle.hasPermission(writePerm) ) + { + errorCode = DTOConstants.FAILURE_REASON_WHITEBOARD_WRITE_TO_LOCATION_DENIED; + } + } + } + else + { + multipartSecurityContext = null; + } + multipartErrorCode = errorCode; + } + + @Override + public int init() + { + if ( this.multipartErrorCode != -1 ) + { + return this.multipartErrorCode; + } + if ( this.useCount > 0 ) + { + this.useCount++; + return -1; + } + + final ServiceReference serviceReference = getServletInfo().getServiceReference(); + final ServiceObjects so = this.bundleContext.getServiceObjects(serviceReference); + + this.setServlet((so == null ? null : so.getService())); + + final int reason = super.init(); + if ( reason != -1 ) + { + if ( so != null ) + { + so.ungetService(this.getServlet()); + } + this.setServlet(null); + } + return reason; + } + + @Override + public boolean destroy() + { + final Servlet s = this.getServlet(); + if ( s != null ) + { + if ( super.destroy() ) + { + + final ServiceObjects so = this.bundleContext.getServiceObjects(getServletInfo().getServiceReference()); + if (so != null) + { + so.ungetService(s); + } + return true; + } + } + return false; + } + + @Override + public Bundle getMultipartSecurityContext() + { + return multipartSecurityContext; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/logger/ConsoleLogger.java b/http/base/src/main/java/org/apache/felix/http/base/internal/logger/ConsoleLogger.java new file mode 100644 index 00000000000..a669cec2954 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/logger/ConsoleLogger.java @@ -0,0 +1,81 @@ +/* + * 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.http.base.internal.logger; + +import java.io.PrintStream; + +import org.osgi.service.log.LogService; + +public final class ConsoleLogger implements InternalLogger +{ + @Override + public void log(final int level, final String message, final Throwable ex) + { + if ( isLogEnabled(level) ) + { + // output depending on level + final PrintStream out = ( level == LogService.LOG_ERROR )? System.err: System.out; + + // level as a string + final StringBuilder buf = new StringBuilder(); + switch (level) + { + case ( LogService.LOG_DEBUG ): + buf.append( "[DEBUG] " ); + break; + case ( LogService.LOG_INFO ): + buf.append( "[INFO] " ); + break; + case ( LogService.LOG_WARNING ): + buf.append( "[WARN] " ); + break; + case ( LogService.LOG_ERROR ): + buf.append( "[ERROR] " ); + break; + default: + buf.append( "UNK : " ); + break; + } + + buf.append(message); + + final String msg = buf.toString(); + + if ( ex == null ) + { + out.println(msg); + } + else + { + // keep the message and the stacktrace together + synchronized ( out ) + { + out.println( msg ); + ex.printStackTrace( out ); + } + } + } + } + + @Override + public boolean isLogEnabled(final int level) + { + return true; + } +} \ No newline at end of file diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/logger/InternalLogger.java b/http/base/src/main/java/org/apache/felix/http/base/internal/logger/InternalLogger.java new file mode 100644 index 00000000000..d3fc9aa131d --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/logger/InternalLogger.java @@ -0,0 +1,26 @@ +/* + * 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.http.base.internal.logger; + +interface InternalLogger { + + void log(int level, String message, Throwable exception); + + boolean isLogEnabled(int level); +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/logger/JDK14Logger.java b/http/base/src/main/java/org/apache/felix/http/base/internal/logger/JDK14Logger.java new file mode 100644 index 00000000000..8e7f9079f70 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/logger/JDK14Logger.java @@ -0,0 +1,74 @@ +/* + * 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.http.base.internal.logger; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.osgi.service.log.LogService; + +/** + * Logger based on Java Logging API. + */ +public final class JDK14Logger implements InternalLogger +{ + private final Logger logger = Logger.getLogger("org.apache.felix.http"); + + private Level getLevel(final int level) + { + Level logLevel; + switch (level) + { + case LogService.LOG_DEBUG: + logLevel = Level.FINE; + break; + case LogService.LOG_INFO: + logLevel = Level.INFO; + break; + case LogService.LOG_WARNING: + logLevel = Level.WARNING; + break; + case LogService.LOG_ERROR: + logLevel = Level.SEVERE; + break; + default: logLevel = Level.FINE; + } + return logLevel; + } + + @Override + public boolean isLogEnabled(final int level) { + return this.logger.isLoggable(getLevel(level)); + } + + @Override + public void log(final int level, final String message, final Throwable exception) + { + final Level logLevel = getLevel(level); + + if (exception != null) + { + this.logger.log(logLevel, message, exception); + } + else + { + this.logger.log(logLevel, message); + } + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/logger/LogServiceEnabledLogger.java b/http/base/src/main/java/org/apache/felix/http/base/internal/logger/LogServiceEnabledLogger.java new file mode 100644 index 00000000000..e2d9322bced --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/logger/LogServiceEnabledLogger.java @@ -0,0 +1,157 @@ +/* + * 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.http.base.internal.logger; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +/** + * This abstract class adds support for using a LogService + */ +public class LogServiceEnabledLogger +{ + // name of the LogService class (this is a string to not create a reference to the class) + // With R7, LogService is deprecated but extends the newer LoggerFactory + private static final String LOGSERVICE_CLASS = "org.osgi.service.log.LogService"; + + private static final String JUL_LOGGER = "org.apache.felix.http.log.jul"; + + // the log service to log messages to + protected final ServiceTracker logServiceTracker; + + private volatile InternalLogger currentLogger; + + protected volatile int trackingCount = -2; + + private final InternalLogger defaultLogger; + + public LogServiceEnabledLogger(final BundleContext bundleContext) + { + Object julLogOpt = bundleContext.getProperty(JUL_LOGGER); + if ( julLogOpt == null ) { + julLogOpt = System.getProperty(JUL_LOGGER); + } + if ( julLogOpt != null ) { + this.defaultLogger = new JDK14Logger(); + } else { + this.defaultLogger = new ConsoleLogger(); + } + + // Start a tracker for the log service + // we only track a single log service which in reality should be enough + logServiceTracker = new ServiceTracker<>( bundleContext, LOGSERVICE_CLASS, new ServiceTrackerCustomizer() + { + private volatile boolean hasService = false; + + @Override + public Object addingService(final ServiceReference reference) + { + if ( !hasService ) + { + final Object logService = bundleContext.getService(reference); + if ( logService != null ) + { + hasService = true; + final LogServiceSupport lsl = new LogServiceSupport(logService); + return lsl; + } + } + return null; + } + + @Override + public void modifiedService(final ServiceReference reference, final Object service) + { + // nothing to do + } + + @Override + public void removedService(final ServiceReference reference, final Object service) + { + hasService = false; + bundleContext.ungetService(reference); + } + } ); + logServiceTracker.open(); + } + + /** + * Close the logger + */ + public void close() + { + // stop the tracker + logServiceTracker.close(); + } + + /** + * Returns {@code true} if logging for the given level is enabled. + */ + public boolean isLogEnabled(final int level) + { + return getLogger().isLogEnabled(level); + } + + /** + * Method to actually emit the log message. If the LogService is available, + * the message will be logged through the LogService. Otherwise the message + * is logged to stdout (or stderr in case of LOG_ERROR level messages), + * + * @param level The log level of the messages. This corresponds to the log + * levels defined by the OSGi LogService. + * @param message The message to print + * @param ex The Throwable causing the message to be logged. + */ + public void log(final int level, final String message, final Throwable ex) + { + if ( isLogEnabled( level ) ) + { + getLogger().log(level, message, ex); + } + } + + /** + * Get the internal logger + * @return The internal logger + */ + InternalLogger getLogger() + { + if ( this.trackingCount < this.logServiceTracker.getTrackingCount() ) + { + final Object logServiceSupport = this.logServiceTracker.getService(); + if ( logServiceSupport == null ) + { + this.currentLogger = this.getDefaultLogger(); + } + else + { + this.currentLogger = ((LogServiceSupport)logServiceSupport).getLogger(); + } + this.trackingCount = this.logServiceTracker.getTrackingCount(); + } + return currentLogger; + } + + InternalLogger getDefaultLogger() + { + return this.defaultLogger; + } +} \ No newline at end of file diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/logger/LogServiceSupport.java b/http/base/src/main/java/org/apache/felix/http/base/internal/logger/LogServiceSupport.java new file mode 100644 index 00000000000..a368185166f --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/logger/LogServiceSupport.java @@ -0,0 +1,40 @@ +/* + * 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.http.base.internal.logger; + +import org.osgi.service.log.LogService; + +/** + * This is a logger based on the LogService. + */ +class LogServiceSupport +{ + + private final LogService logService; + + public LogServiceSupport(final Object logService) + { + this.logService = (LogService) logService; + } + + InternalLogger getLogger() + { + return new R6LogServiceLogger(this.logService); + } +} \ No newline at end of file diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/logger/R6LogServiceLogger.java b/http/base/src/main/java/org/apache/felix/http/base/internal/logger/R6LogServiceLogger.java new file mode 100644 index 00000000000..f92303d85ca --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/logger/R6LogServiceLogger.java @@ -0,0 +1,46 @@ +/* + * 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.http.base.internal.logger; + +import org.osgi.service.log.LogService; + +/** + * This is a logger based on the R6 LogService. + */ +class R6LogServiceLogger implements InternalLogger +{ + private final LogService logService; + + public R6LogServiceLogger(final LogService logService) + { + this.logService = logService; + } + + @Override + public boolean isLogEnabled(final int level) + { + return true; + } + + @Override + public void log(final int level, final String message, final Throwable ex) + { + this.logService.log(level, message, ex); + } +} \ No newline at end of file diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/logger/SystemLogger.java b/http/base/src/main/java/org/apache/felix/http/base/internal/logger/SystemLogger.java new file mode 100644 index 00000000000..ec7053397e2 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/logger/SystemLogger.java @@ -0,0 +1,161 @@ +/* + * 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.http.base.internal.logger; + +import java.lang.reflect.Array; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.service.log.LogService; + +public final class SystemLogger +{ + private static volatile LogServiceEnabledLogger LOGGER; + + public static void init(final BundleContext bundleContext) { + LOGGER = new LogServiceEnabledLogger(bundleContext); + } + + public static void destroy() { + if ( LOGGER != null ) { + LOGGER.close(); + LOGGER = null; + } + } + + private static String getMessage(final ServiceReference ref, final String message) + { + if ( ref == null ) + { + return message; + } + final Bundle bundle = ref.getBundle(); + final StringBuilder ib = new StringBuilder(); + ib.append("[ServiceReference "); + ib.append(String.valueOf(ref.getProperty(Constants.SERVICE_ID))); + ib.append(" from bundle "); + if ( bundle == null ) + { + ib.append(""); + } + else + { + ib.append(bundle.getBundleId()); + if ( bundle.getSymbolicName() != null ) + { + ib.append(" : "); + ib.append(bundle.getSymbolicName()); + ib.append(":"); + ib.append(bundle.getVersion()); + } + } + ib.append(" ref="); + ib.append(ref); + ib.append(" properties={"); + boolean first = true; + for(final String name : ref.getPropertyKeys()) + { + if ( first ) + { + first = false; + } + else + { + ib.append(", "); + } + final Object val = ref.getProperty(name); + ib.append(name); + ib.append("="); + if ( val.getClass().isArray() ) + { + boolean fa = true; + ib.append('['); + for(int i=0;i ref, + final String message, + final Throwable cause) { + final LogServiceEnabledLogger l = LOGGER; + if ( l != null ) { + l.log(level, getMessage(ref, message), cause); + } + } + + public static void debug(final String message) + { + log(LogService.LOG_DEBUG, null, message, null); + } + + public static void debug(final ServiceReference ref, final String message) + { + log(LogService.LOG_DEBUG, ref, message, null); + } + + public static void debug(final String message, final Throwable cause) + { + log(LogService.LOG_DEBUG, null, message, cause); + } + + public static void info(final String message) + { + log(LogService.LOG_INFO, null, message, null); + } + + public static void warning(final String message, final Throwable cause) + { + log(LogService.LOG_WARNING, null, message, cause); + } + + public static void error(final String message, final Throwable cause) + { + log(LogService.LOG_ERROR, null, message, cause); + } + + public static void error(final ServiceReference ref, final String message, final Throwable cause) + { + log(LogService.LOG_ERROR, ref, message, cause); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/registry/ErrorPageRegistry.java b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/ErrorPageRegistry.java new file mode 100644 index 00000000000..9357f61b659 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/ErrorPageRegistry.java @@ -0,0 +1,609 @@ +/* + * 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.http.base.internal.registry; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; + +import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.apache.felix.http.base.internal.runtime.dto.BuilderConstants; +import org.apache.felix.http.base.internal.runtime.dto.ErrorPageDTOBuilder; +import org.osgi.service.http.runtime.dto.DTOConstants; +import org.osgi.service.http.runtime.dto.ErrorPageDTO; +import org.osgi.service.http.runtime.dto.FailedErrorPageDTO; +import org.osgi.service.http.runtime.dto.ServletContextDTO; + +/** + * The error page registry keeps tracks of the active/inactive servlets handling + * error pages (error code and/or exception). + * This registry is per servlet context. + */ +public final class ErrorPageRegistry +{ + private static final String CLIENT_ERROR = "4xx"; + private static final String SERVER_ERROR = "5xx"; + private static final Pattern ERROR_CODE_PATTERN = Pattern.compile("\\d{3}"); + + private static final List CLIENT_ERROR_CODES = hundredOf(400); + private static final List SERVER_ERROR_CODES = hundredOf(500); + + private final Map> errorMapping = new ConcurrentHashMap>(); + + private volatile List status = Collections.emptyList(); + + public static final class ErrorRegistration { + public final long[] errorCodes; + public final String[] exceptions; + + public ErrorRegistration(final long[] errorCodes, final String[] exceptions) + { + this.errorCodes = errorCodes; + this.exceptions = exceptions; + } + } + + static final class ErrorRegistrationStatus implements Comparable { + + private final ServletHandler handler; + public final Map reasonMapping = new HashMap(); + + public final boolean usesClientErrorCodes; + public final boolean usesServerErrorCodes; + + public ErrorRegistrationStatus(final ServletHandler handler) + { + this.handler = handler; + this.usesClientErrorCodes = hasErrorCode(handler, CLIENT_ERROR); + this.usesServerErrorCodes = hasErrorCode(handler, SERVER_ERROR); + } + + public ServletHandler getHandler() + { + return this.handler; + } + + @Override + public int compareTo(final ErrorRegistrationStatus o) + { + return this.handler.compareTo(o.getHandler()); + } + } + + private static boolean hasErrorCode(final ServletHandler handler, final String key) + { + for(final String val : handler.getServletInfo().getErrorPage()) + { + if ( key.equals(val) ) + { + return true; + } + } + return false; + } + + private static List hundredOf(final int start) + { + List result = new ArrayList(); + for (long i = start; i < start + 100; i++) + { + result.add(i); + } + return Collections.unmodifiableList(result); + } + + private static long[] toLongArray(final Set set) + { + long[] codes = BuilderConstants.EMPTY_LONG_ARRAY; + if ( !set.isEmpty() ) + { + codes = new long[set.size()]; + int index = 0; + for(final Long code : set) + { + codes[index++] = code; + } + } + return codes; + } + + private static Set toLongSet(final long[] codes) + { + final Set set = new TreeSet(); + for(final long c : codes) + { + set.add(c); + } + return set; + } + + private static String[] toStringArray(final Set set) + { + String[] array = BuilderConstants.EMPTY_STRING_ARRAY; + if ( !set.isEmpty() ) + { + array = set.toArray(new String[set.size()]); + } + return array; + } + + private static Set toStringSet(final String[] array) + { + final Set set = new TreeSet(); + for(final String s : array) + { + set.add(s); + } + return set; + } + + private static String parseErrorCodes(final Set codes, final String string) + { + if (CLIENT_ERROR.equalsIgnoreCase(string)) + { + codes.addAll(CLIENT_ERROR_CODES); + } + else if (SERVER_ERROR.equalsIgnoreCase(string)) + { + codes.addAll(SERVER_ERROR_CODES); + } + else if (ERROR_CODE_PATTERN.matcher(string).matches()) + { + codes.add(Long.parseLong(string)); + } + else + { + return string; + } + return null; + } + + /** + * Parse the registration properties of the servlet for error handling + * @param info The servlet info + * @return An error registration object if the servlet handles errors + */ + public static @Nullable ErrorRegistration getErrorRegistration(@NotNull final ServletInfo info) + { + if ( info.getErrorPage() != null ) + { + final Set errorCodes = new TreeSet(); + final Set exceptions = new TreeSet(); + + for(final String val : info.getErrorPage()) + { + final String exception = parseErrorCodes(errorCodes, val); + if ( exception != null ) + { + exceptions.add(exception); + } + } + final long[] codes = toLongArray(errorCodes); + final String[] exceptionsArray = toStringArray(exceptions); + + return new ErrorRegistration(codes, exceptionsArray); + } + return null; + } + + /** + * Add the servlet for error handling + * @param handler The servlet handler. + */ + public synchronized void addServlet(@NotNull final ServletHandler handler) + { + final ErrorRegistration reg = getErrorRegistration(handler.getServletInfo()); + if ( reg != null ) + { + final ErrorRegistrationStatus status = new ErrorRegistrationStatus(handler); + for(final long code : reg.errorCodes) + { + addErrorHandling(handler, status, code, null); + } + for(final String exception : reg.exceptions) + { + addErrorHandling(handler, status, 0, exception); + } + final List newList = new ArrayList(this.status); + newList.add(status); + Collections.sort(newList); + this.status = newList; + } + } + + /** + * Remove the servlet from error handling + * @param info The servlet info. + */ + public synchronized void removeServlet(@NotNull final ServletInfo info, final boolean destroy) + { + final ErrorRegistration reg = getErrorRegistration(info); + if ( reg != null ) + { + final List newList = new ArrayList(this.status); + final Iterator i = newList.iterator(); + while ( i.hasNext() ) + { + final ErrorRegistrationStatus status = i.next(); + if ( status.handler.getServletInfo().equals(info) ) + { + i.remove(); + break; + } + } + this.status = newList; + + for(final long code : reg.errorCodes) + { + removeErrorHandling(info, code, null); + } + for(final String exception : reg.exceptions) + { + removeErrorHandling(info, 0, exception); + } + } + } + + public synchronized void cleanup() + { + this.errorMapping.clear(); + this.status = Collections.emptyList(); + } + + private void addErrorHandling(final ServletHandler handler, final ErrorRegistrationStatus status, final long code, final String exception) + { + final String key = (exception != null ? exception : String.valueOf(code)); + + final List newList; + final List list = errorMapping.get(key); + if ( list == null ) + { + newList = Collections.singletonList(handler); + } + else + { + newList = new ArrayList(list); + newList.add(handler); + Collections.sort(newList); + } + if ( newList.get(0) == handler ) + { + // try to activate (and deactivate old handler) + final int result = handler.init(); + addReason(status, code, exception, result); + if ( result == -1 ) + { + if ( list != null ) + { + final ServletHandler old = list.get(0); + old.destroy(); + errorMapping.put(key, newList); + ErrorRegistrationStatus oldStatus = null; + final Iterator i = this.status.iterator(); + while ( oldStatus == null && i.hasNext() ) + { + final ErrorRegistrationStatus current = i.next(); + if ( current.handler.getServletInfo().equals(old.getServletInfo()) ) + { + oldStatus = current; + } + } + if ( oldStatus != null ) + { + removeReason(oldStatus, code, exception, -1); + boolean addReason = true; + if ( exception == null ) + { + if ( code >= 400 && code < 500 && oldStatus.usesClientErrorCodes && !status.usesClientErrorCodes ) + { + addReason = false; + } + else if ( code >= 500 && code < 600 && oldStatus.usesServerErrorCodes && !status.usesServerErrorCodes ) + { + addReason = false; + } + } + if ( addReason ) + { + addReason(oldStatus, code, exception, DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE); + } + } + } + else + { + errorMapping.put(key, newList); + } + } + } + else + { + // failure + boolean addReason = true; + if ( exception == null ) + { + if ( code >= 400 && code < 500 && status.usesClientErrorCodes && !hasErrorCode(newList.get(0), CLIENT_ERROR) ) + { + addReason = false; + } + else if ( code >= 500 && code < 600 && status.usesServerErrorCodes && !hasErrorCode(newList.get(0), SERVER_ERROR) ) + { + addReason = false; + } + } + if ( addReason ) + { + addReason(status, code, exception, DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE); + } + errorMapping.put(key, newList); + } + } + + /** + * Make an entry in the status object (which are used to create the DTOs) + * @param status The status object + * @param code Either the code + * @param exception or the exception + * @param reason The code for the failure reason or {@code -1} for success. + */ + private void addReason(final ErrorRegistrationStatus status, + final long code, + final String exception, + final int reason) + { + ErrorRegistration reg = status.reasonMapping.get(reason); + if ( reg == null ) + { + if ( exception != null ) + { + reg = new ErrorRegistration(BuilderConstants.EMPTY_LONG_ARRAY, new String[] {exception}); + } + else + { + reg = new ErrorRegistration(new long[] {code}, BuilderConstants.EMPTY_STRING_ARRAY); + } + } + else + { + long[] codes = reg.errorCodes; + String[] exceptions = reg.exceptions; + if ( exception != null ) + { + final Set set = toStringSet(exceptions); + set.add(exception); + exceptions = toStringArray(set); + } + else + { + final Set set = toLongSet(codes); + set.add(code); + codes = toLongArray(set); + } + + reg = new ErrorRegistration(codes, exceptions); + } + status.reasonMapping.put(reason, reg); + } + + private void removeReason(final ErrorRegistrationStatus status, final long code, final String exception, final int reason) + { + ErrorRegistration reg = status.reasonMapping.get(reason); + if ( reg != null ) + { + long[] codes = reg.errorCodes; + String[] exceptions = reg.exceptions; + if ( exception != null ) + { + final Set set = toStringSet(exceptions); + set.remove(exception); + exceptions = toStringArray(set); + } + else + { + final Set set = toLongSet(codes); + set.remove(code); + codes = toLongArray(set); + } + if ( codes.length == 0 && exceptions.length == 0 ) + { + status.reasonMapping.remove(reason); + } + else + { + status.reasonMapping.put(reason, new ErrorRegistration(codes, exceptions)); + } + } + } + + private void removeErrorHandling(final ServletInfo info, final long code, final String exception) + { + final String key = (exception != null ? exception : String.valueOf(code)); + + final List list = errorMapping.get(key); + if ( list != null ) + { + int index = 0; + final Iterator i = list.iterator(); + while ( i.hasNext() ) + { + final ServletHandler handler = i.next(); + if ( handler.getServletInfo().equals(info) ) + { + final List newList = new ArrayList(list); + newList.remove(handler); + + if ( index == 0 ) + { + handler.destroy(); + + index++; + while ( index < list.size() ) + { + final ServletHandler next = list.get(index); + ErrorRegistrationStatus nextStatus = null; + for(final ErrorRegistrationStatus s : this.status) + { + if ( s.handler.getServletInfo().equals(next.getServletInfo()) ) + { + nextStatus = s; + break; + } + } + this.removeReason(nextStatus, code, exception, DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE); + final int reason = next.init(); + this.addReason(nextStatus, code, exception, reason); + if ( reason == -1 ) + { + break; + } + else + { + newList.remove(next); + } + } + } + if ( newList.isEmpty() ) + { + errorMapping.remove(key); + } + else + { + errorMapping.put(key, newList); + } + + break; + } + index++; + } + } + } + + /** + * Get the servlet handling the error (error code or exception). + * If an exception is provided, a handler for the exception is searched first. + * If no handler is found (or no exception provided) a handler for the error + * code is searched. + * @param exception Optional exception + * @param errorCode Error code + * @return The servlet handling the error or {@code null} + */ + public ServletHandler get(final Throwable exception, final int errorCode) + { + ServletHandler errorHandler = this.get(exception); + if (errorHandler != null) + { + return errorHandler; + } + + return get(errorCode); + } + + /** + * Get the servlet handling the error code + * @param errorCode Error code + * @return The servlet handling the error or {@code null} + */ + private ServletHandler get(final long errorCode) + { + final List list = this.errorMapping.get(String.valueOf(errorCode)); + if ( list != null ) + { + return list.get(0); + } + return null; + } + + /** + * Get the servlet handling the exception + * @param exception Error exception + * @return The servlet handling the error or {@code null} + */ + private ServletHandler get(final Throwable exception) + { + if (exception == null) + { + return null; + } + + ServletHandler servletHandler = null; + Class throwableClass = exception.getClass(); + while ( servletHandler == null && throwableClass != null ) + { + final List list = this.errorMapping.get(throwableClass.getName()); + if ( list != null ) + { + servletHandler = list.get(0); + } + if ( servletHandler == null ) + { + throwableClass = throwableClass.getSuperclass(); + if ( !Throwable.class.isAssignableFrom(throwableClass) ) + { + throwableClass = null; + } + } + + } + return servletHandler; + } + + /** + * Get DTOs for error pages. + * @param dto The servlet context DTO + * @param failedErrorPageDTOs The failed error page DTOs + */ + public void getRuntimeInfo(final ServletContextDTO dto, + final Collection failedErrorPageDTOs) + { + final List errorPageDTOs = new ArrayList(); + final List statusList = this.status; + for(final ErrorRegistrationStatus status : statusList) + { + for(final Map.Entry entry : status.reasonMapping.entrySet()) + { + final ErrorPageDTO state = ErrorPageDTOBuilder.build(status.getHandler(), entry.getKey()); + state.errorCodes = Arrays.copyOf(entry.getValue().errorCodes, entry.getValue().errorCodes.length); + state.exceptions = Arrays.copyOf(entry.getValue().exceptions, entry.getValue().exceptions.length); + + if ( entry.getKey() == -1 ) + { + errorPageDTOs.add(state); + } + else + { + failedErrorPageDTOs.add((FailedErrorPageDTO)state); + } + } + } + if ( !errorPageDTOs.isEmpty() ) + { + dto.errorPageDTOs = errorPageDTOs.toArray(new ErrorPageDTO[errorPageDTOs.size()]); + } + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/registry/EventListenerRegistry.java b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/EventListenerRegistry.java new file mode 100644 index 00000000000..3adc1f527a1 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/EventListenerRegistry.java @@ -0,0 +1,474 @@ +/* + * 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.http.base.internal.registry; + +import java.util.ArrayList; +import java.util.List; + +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import javax.servlet.ServletContextAttributeEvent; +import javax.servlet.ServletContextAttributeListener; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletRequestAttributeEvent; +import javax.servlet.ServletRequestAttributeListener; +import javax.servlet.ServletRequestEvent; +import javax.servlet.ServletRequestListener; +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionIdListener; +import javax.servlet.http.HttpSessionListener; + +import org.apache.felix.http.base.internal.handler.ListenerHandler; +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.apache.felix.http.base.internal.runtime.ListenerInfo; +import org.osgi.service.http.runtime.dto.FailedListenerDTO; +import org.osgi.service.http.runtime.dto.ListenerDTO; +import org.osgi.service.http.runtime.dto.ServletContextDTO; + +/** + * Per context event listener registry. + */ +public final class EventListenerRegistry implements + HttpSessionListener, + HttpSessionAttributeListener, + HttpSessionIdListener, + ServletContextAttributeListener, + ServletRequestListener, + ServletRequestAttributeListener +{ + /** Servlet context listeners. */ + private final ListenerMap contextListeners = new ListenerMap(); + + /** Servlet context attribute listeners. */ + private final ListenerMap contextAttributeListeners = new ListenerMap(); + + /** Session attribute listeners. */ + private final ListenerMap sessionAttributeListeners = new ListenerMap(); + + /** Session listeners. */ + private final ListenerMap sessionListeners = new ListenerMap(); + + /** Session id listeners. */ + private final ListenerMap sessionIdListeners = new ListenerMap(); + + /** Request listeners. */ + private final ListenerMap requestListeners = new ListenerMap(); + + /** Request attribute listeners. */ + private final ListenerMap requestAttributeListeners = new ListenerMap(); + + public void cleanup() + { + this.contextListeners.cleanup(); + this.contextAttributeListeners.cleanup(); + this.sessionAttributeListeners.cleanup(); + this.sessionListeners.cleanup(); + this.sessionIdListeners.cleanup(); + this.requestListeners.cleanup(); + this.requestAttributeListeners.cleanup(); + } + + /** + * Add listeners + * + * @param listener handler + */ + public void addListeners(@NotNull final ListenerHandler handler) + { + final int reason = handler.init(); + + if ( handler.getListenerInfo().isListenerType(ServletContextListener.class.getName())) + { + this.contextListeners.add(handler, reason); + } + if ( handler.getListenerInfo().isListenerType(ServletContextAttributeListener.class.getName())) + { + this.contextAttributeListeners.add(handler, reason); + } + if ( handler.getListenerInfo().isListenerType(HttpSessionListener.class.getName())) + { + this.sessionListeners.add(handler, reason); + } + if ( handler.getListenerInfo().isListenerType(HttpSessionIdListener.class.getName())) + { + this.sessionIdListeners.add(handler, reason); + } + if ( handler.getListenerInfo().isListenerType(HttpSessionAttributeListener.class.getName())) + { + this.sessionAttributeListeners.add(handler, reason); + } + if ( handler.getListenerInfo().isListenerType(ServletRequestListener.class.getName())) + { + this.requestListeners.add(handler, reason); + } + if ( handler.getListenerInfo().isListenerType(ServletRequestAttributeListener.class.getName())) + { + this.requestAttributeListeners.add(handler, reason); + } + } + + /** + * Remove listeners + * + * @param info + */ + public void removeListeners(@NotNull final ListenerInfo info) + { + // each listener map returns the same handler, we just need it once to destroy + ListenerHandler handler = null; + if ( info.isListenerType(ServletContextListener.class.getName())) + { + handler = this.contextListeners.remove(info); + } + if ( info.isListenerType(ServletContextAttributeListener.class.getName())) + { + handler = this.contextAttributeListeners.remove(info); + } + if ( info.isListenerType(HttpSessionListener.class.getName())) + { + handler = this.sessionListeners.remove(info); + } + if ( info.isListenerType(HttpSessionIdListener.class.getName())) + { + handler = this.sessionIdListeners.remove(info); + } + if ( info.isListenerType(HttpSessionAttributeListener.class.getName())) + { + handler = this.sessionAttributeListeners.remove(info); + } + if ( info.isListenerType(ServletRequestListener.class.getName())) + { + handler = this.requestListeners.remove(info); + } + if ( info.isListenerType(ServletRequestAttributeListener.class.getName())) + { + handler = this.requestAttributeListeners.remove(info); + } + if ( handler != null ) + { + handler.destroy(); + } + } + + /** + * Get the listener handler for the listener info + * @param info The listener info + * @return The handler or {@code null}. + */ + public @Nullable ListenerHandler getServletContextListener(@NotNull final ListenerInfo info) + { + return this.contextListeners.getListenerHandler(info); + } + + public void contextInitialized() { + for (final ListenerHandler l : contextListeners.getActiveHandlers()) + { + final ServletContextListener listener = (ServletContextListener)l.getListener(); + if ( listener != null ) + { + contextInitialized(l.getListenerInfo(), listener, new ServletContextEvent(l.getContext())); + } + } + } + + public void contextDestroyed() { + for (final ListenerHandler l : contextListeners.getActiveHandlers()) + { + final ServletContextListener listener = (ServletContextListener) l.getListener(); + if ( listener != null ) + { + contextDestroyed(l.getListenerInfo(), listener, new ServletContextEvent(l.getContext())); + } + } + } + + @Override + public void attributeReplaced(final HttpSessionBindingEvent event) + { + for (final HttpSessionAttributeListener l : sessionAttributeListeners.getActiveListeners()) + { + try + { + l.attributeReplaced(event); + } + catch (final Throwable t) + { + SystemLogger.error(null, "Exception while calling listener " + l, t); + } + } + } + + @Override + public void attributeRemoved(final HttpSessionBindingEvent event) + { + for (final HttpSessionAttributeListener l : sessionAttributeListeners.getActiveListeners()) + { + try + { + l.attributeRemoved(event); + } + catch (final Throwable t) + { + SystemLogger.error(null, "Exception while calling listener " + l, t); + } + } + } + + @Override + public void attributeAdded(final HttpSessionBindingEvent event) + { + for (final HttpSessionAttributeListener l : sessionAttributeListeners.getActiveListeners()) + { + try + { + l.attributeAdded(event); + } + catch (final Throwable t) + { + SystemLogger.error(null, "Exception while calling listener " + l, t); + } + } + } + + @Override + public void attributeReplaced(final ServletContextAttributeEvent event) + { + for (final ServletContextAttributeListener l : contextAttributeListeners.getActiveListeners()) + { + try + { + l.attributeReplaced(event); + } + catch (final Throwable t) + { + SystemLogger.error(null, "Exception while calling listener " + l, t); + } + } + } + + @Override + public void attributeRemoved(final ServletContextAttributeEvent event) + { + for (final ServletContextAttributeListener l : contextAttributeListeners.getActiveListeners()) + { + try + { + l.attributeRemoved(event); + } + catch (final Throwable t) + { + SystemLogger.error(null, "Exception while calling listener " + l, t); + } + } + } + + @Override + public void attributeAdded(final ServletContextAttributeEvent event) + { + for (final ServletContextAttributeListener l : contextAttributeListeners.getActiveListeners()) + { + try + { + l.attributeAdded(event); + } + catch (final Throwable t) + { + SystemLogger.error(null, "Exception while calling listener " + l, t); + } + } + } + + @Override + public void sessionCreated(final HttpSessionEvent se) + { + for (final HttpSessionListener l : sessionListeners.getActiveListeners()) + { + try + { + l.sessionCreated(se); + } + catch (final Throwable t) + { + SystemLogger.error(null, "Exception while calling listener " + l, t); + } + } + } + + @Override + public void sessionDestroyed(final HttpSessionEvent se) + { + for (final HttpSessionListener l : sessionListeners.getActiveListeners()) + { + try + { + l.sessionDestroyed(se); + } + catch (final Throwable t) + { + SystemLogger.error(null, "Exception while calling listener " + l, t); + } + } + } + + @Override + public void requestDestroyed(final ServletRequestEvent sre) + { + for (final ServletRequestListener l : requestListeners.getActiveListeners()) + { + try + { + l.requestDestroyed(sre); + } + catch (final Throwable t) + { + SystemLogger.error(null, "Exception while calling listener " + l, t); + } + } + } + + @Override + public void requestInitialized(final ServletRequestEvent sre) + { + for (final ServletRequestListener l : requestListeners.getActiveListeners()) + { + try + { + l.requestInitialized(sre); + } + catch (final Throwable t) + { + SystemLogger.error(null, "Exception while calling listener " + l, t); + } + } + } + + @Override + public void attributeAdded(final ServletRequestAttributeEvent srae) + { + for (final ServletRequestAttributeListener l : requestAttributeListeners.getActiveListeners()) + { + try + { + l.attributeAdded(srae); + } + catch (final Throwable t) + { + SystemLogger.error(null, "Exception while calling listener " + l, t); + } + } + } + + @Override + public void attributeRemoved(final ServletRequestAttributeEvent srae) + { + for (final ServletRequestAttributeListener l : requestAttributeListeners.getActiveListeners()) + { + try + { + l.attributeRemoved(srae); + } + catch (final Throwable t) + { + SystemLogger.error(null, "Exception while calling listener " + l, t); + } + } + } + + @Override + public void attributeReplaced(final ServletRequestAttributeEvent srae) + { + for (final ServletRequestAttributeListener l : requestAttributeListeners.getActiveListeners()) + { + try + { + l.attributeReplaced(srae); + } + catch (final Throwable t) + { + SystemLogger.error(null, "Exception while calling listener " + l, t); + } + } + } + + /** + * @see javax.servlet.http.HttpSessionIdListener#sessionIdChanged(javax.servlet.http.HttpSessionEvent, java.lang.String) + */ + @Override + public void sessionIdChanged(@NotNull final HttpSessionEvent event, @NotNull final String oldSessionId) { + for (final HttpSessionIdListener l : sessionIdListeners.getActiveListeners()) + { + try + { + l.sessionIdChanged(event, oldSessionId); + } + catch (final Throwable t) + { + SystemLogger.error(null, "Exception while calling listener " + l, t); + } + } + } + + public void getRuntimeInfo(final ServletContextDTO dto, final List failedListenerDTOs) + { + final List listenerDTOs = new ArrayList(); + this.contextListeners.getRuntimeInfo(listenerDTOs, failedListenerDTOs); + this.contextAttributeListeners.getRuntimeInfo(listenerDTOs, failedListenerDTOs); + this.requestListeners.getRuntimeInfo(listenerDTOs, failedListenerDTOs); + this.requestAttributeListeners.getRuntimeInfo(listenerDTOs, failedListenerDTOs); + this.sessionListeners.getRuntimeInfo(listenerDTOs, failedListenerDTOs); + this.sessionAttributeListeners.getRuntimeInfo(listenerDTOs, failedListenerDTOs); + this.sessionIdListeners.getRuntimeInfo(listenerDTOs, failedListenerDTOs); + + if ( listenerDTOs.size() > 0 ) + { + dto.listenerDTOs = listenerDTOs.toArray(new ListenerDTO[listenerDTOs.size()]); + } + } + + public static void contextInitialized( + @NotNull final ListenerInfo info, + @NotNull final ServletContextListener listener, + @NotNull final ServletContextEvent event) + { + try + { + listener.contextInitialized(event); + } + catch (final Throwable t) + { + SystemLogger.error(info.getServiceReference(), "Exception while calling servlet context listener.", t); + } + } + + public static void contextDestroyed( + @NotNull final ListenerInfo info, + @NotNull final ServletContextListener listener, + @NotNull final ServletContextEvent event) + { + try + { + listener.contextDestroyed(event); + } + catch (final Throwable t) + { + SystemLogger.error(info.getServiceReference(), "Exception while calling servlet context listener.", t); + } + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/registry/FilterRegistry.java b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/FilterRegistry.java new file mode 100644 index 00000000000..79554354ba8 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/FilterRegistry.java @@ -0,0 +1,268 @@ +/* + * 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.http.base.internal.registry; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import javax.servlet.DispatcherType; + +import org.apache.felix.http.base.internal.handler.FilterHandler; +import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.runtime.FilterInfo; +import org.apache.felix.http.base.internal.runtime.dto.FilterDTOBuilder; +import org.osgi.service.http.runtime.dto.FailedFilterDTO; +import org.osgi.service.http.runtime.dto.FilterDTO; +import org.osgi.service.http.runtime.dto.ServletContextDTO; + +/** + * The filter registry keeps track of all filter mappings for a single servlet context. + * + */ +public final class FilterRegistry +{ + /** List of all filter registrations. These are sorted by the status objects. */ + private volatile List filters = Collections.emptyList(); + + /** + * The status object keeps track of the registration status of a filter and holds + * the resolvers to match against a uri. + * The status objects are sorted by result first, followed by ranking. The active + * filters ( result == -1) are first, followed by the inactive ones. This sorting + * allows to only traverse over the active ones and also avoids any sorting + * as the filters are processed in the correct order already. + */ + private static final class FilterRegistrationStatus implements Comparable + { + private final int result; + private final FilterHandler handler; + private final PathResolver[] resolvers; + + public FilterRegistrationStatus(@NotNull final FilterHandler handler, @Nullable final PathResolver[] resolvers, final int result) + { + this.handler = handler; + this.resolvers = resolvers; + this.result = result; + } + + public int getResult() + { + return this.result; + } + + public @NotNull FilterHandler getHandler() + { + return this.handler; + } + + public @Nullable PathResolver[] getResolvers() + { + return this.resolvers; + } + + @Override + public int compareTo(final FilterRegistrationStatus o) { + int result = this.result - o.result; + if ( result == 0 ) + { + result = this.handler.compareTo(o.handler); + } + return result; + } + } + + /** + * Add a filter. + * @param handler The handler for the filter + */ + public synchronized void addFilter(@NotNull final FilterHandler handler) + { + final int result = handler.init(); + PathResolver[] prs = null; + + if ( result == -1 ) + { + final List resolvers = new ArrayList(); + if ( handler.getFilterInfo().getPatterns() != null ) + { + for(final String pattern : handler.getFilterInfo().getPatterns() ) { + resolvers.add(PathResolverFactory.createPatternMatcher(null, pattern)); + } + } + if ( handler.getFilterInfo().getRegexs() != null ) + { + for(final String regex : handler.getFilterInfo().getRegexs() ) { + resolvers.add(PathResolverFactory.createRegexMatcher(regex)); + } + } + Collections.sort(resolvers); + + prs = resolvers.toArray(new PathResolver[resolvers.size()]); + } + + final FilterRegistrationStatus status = new FilterRegistrationStatus(handler, prs, result); + + final List newList = new ArrayList(this.filters); + newList.add(status); + Collections.sort(newList); + + this.filters = newList; + } + + /** + * Remove a filter + * @param filterInfo The filter info + * @param destroy boolean flag indicating whether to call destroy on the filter. + */ + public synchronized void removeFilter(@NotNull final FilterInfo filterInfo, final boolean destroy) + { + FilterRegistrationStatus found = null; + final List newList = new ArrayList(this.filters); + final Iterator i = newList.iterator(); + while ( i.hasNext() ) + { + final FilterRegistrationStatus status = i.next(); + if ( status.getHandler().getFilterInfo().equals(filterInfo) ) + { + found = status; + i.remove(); + break; + } + } + if ( found != null ) + { + this.filters = newList; + + if ( found.getResult() == -1 && destroy ) + { + found.getHandler().dispose(); + } + } + } + + public synchronized void cleanup() + { + this.filters = Collections.emptyList(); + } + + /** + * Get all filters handling the request. + * Filters are applied to the url and/or the servlet + * @param handler Optional servlet handler + * @param dispatcherType The dispatcher type + * @param requestURI The request uri + * @return The array of filter handlers, might be empty. + */ + public @NotNull FilterHandler[] getFilterHandlers(@Nullable final ServletHandler handler, + @NotNull final DispatcherType dispatcherType, + @NotNull final String requestURI) + { + final List result = new ArrayList(); + final List allFilters = this.filters; + + for(final FilterRegistrationStatus status : allFilters) + { + // as soon as we encounter a failing filter, we can stop + if ( status.getResult() != -1 ) + { + break; + } + if (referencesDispatcherType(status.getHandler(), dispatcherType) ) + { + boolean added = false; + for(final PathResolver resolver : status.getResolvers()) + { + if ( resolver.resolve(requestURI) != null ) + { + result.add(status.getHandler()); + added = true; + break; + } + } + // check for servlet name if it's not a resource + final String servletName = (handler != null && !handler.getServletInfo().isResource()) ? handler.getName() : null; + if ( !added && servletName != null && status.getHandler().getFilterInfo().getServletNames() != null ) + { + for(final String name : status.getHandler().getFilterInfo().getServletNames()) + { + if ( servletName.equals(name) ) + { + result.add(status.getHandler()); + added = true; + break; + } + } + } + + } + } + + return result.toArray(new FilterHandler[result.size()]); + } + + /** + * Check if the filter is registered for the required dispatcher type + * @param handler The filter handler + * @param dispatcherType The requested dispatcher type + * @return {@code true} if the filter can be applied. + */ + private boolean referencesDispatcherType(final FilterHandler handler, final DispatcherType dispatcherType) + { + for(final DispatcherType dt : handler.getFilterInfo().getDispatcher()) + { + if ( dt == dispatcherType ) + { + return true; + } + } + return false; + } + + /** + * Get the runtime information about filters + * @param servletContextDTO The servlet context DTO + * @param failedFilterDTOs The collection holding the failed filters. + */ + public void getRuntimeInfo(final ServletContextDTO servletContextDTO, + final Collection failedFilterDTOs) + { + final List filterDTOs = new ArrayList(); + + final List allFilters = this.filters; + for(final FilterRegistrationStatus status : allFilters) + { + if ( status.getResult() != -1 ) + { + failedFilterDTOs.add((FailedFilterDTO)FilterDTOBuilder.build(status.getHandler(), status.getResult())); + } + else + { + filterDTOs.add(FilterDTOBuilder.build(status.getHandler(), status.getResult())); + } + } + + if ( !filterDTOs.isEmpty() ) + { + servletContextDTO.filterDTOs = filterDTOs.toArray(new FilterDTO[filterDTOs.size()]); + } + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/registry/HandlerRegistry.java b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/HandlerRegistry.java new file mode 100644 index 00000000000..82aa97e7e69 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/HandlerRegistry.java @@ -0,0 +1,275 @@ +/* + * Licensed to the Apaanche 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.http.base.internal.registry; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import javax.servlet.DispatcherType; + +import org.apache.felix.http.base.internal.HttpConfig; +import org.apache.felix.http.base.internal.handler.FilterHandler; +import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.runtime.ServletContextHelperInfo; +import org.apache.felix.http.base.internal.runtime.dto.FailedDTOHolder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.osgi.service.http.runtime.dto.ServletContextDTO; + +/** + * Registry for all services. + * + * The registry is organized per servlet context and is dispatching to one + * of the {@link PerContextHandlerRegistry} registries. + */ +public final class HandlerRegistry +{ + private static FilterHandler[] EMPTY_FILTER_HANDLER = new FilterHandler[0]; + + /** Current list of context registrations. */ + private volatile List registrations = Collections.emptyList(); + + private final HttpConfig config; + + public HandlerRegistry(final HttpConfig config) + { + this.config = config; + } + + public HttpConfig getConfig() + { + return this.config; + } + + /** + * Register default context registry for Http Service + */ + public void init() + { + this.add(new PerContextHandlerRegistry(config)); + } + + /** + * Reset to initial state + */ + public void reset() + { + this.registrations.clear(); + this.init(); + } + + /** + * Shutdown + */ + public void shutdown() + { + final List list; + + synchronized ( this ) + { + list = new ArrayList<>(this.registrations); + this.registrations = Collections.emptyList(); + + } + + for(final PerContextHandlerRegistry r : list) + { + r.removeAll(); + } + } + + /** + * Remove a context registration. + * @param info The servlet context helper info + */ + public void remove(@NotNull ServletContextHelperInfo info) + { + synchronized ( this ) + { + final List updatedList = new ArrayList<>(this.registrations); + final Iterator i = updatedList.iterator(); + while ( i.hasNext() ) + { + final PerContextHandlerRegistry reg = i.next(); + if ( reg.getContextServiceId() == info.getServiceId() ) + { + i.remove(); + this.registrations = updatedList; + break; + } + } + } + } + + + /** + * Add a new context registration. + */ + public void add(@NotNull PerContextHandlerRegistry registry) + { + synchronized ( this ) + { + final List updatedList = new ArrayList<>(this.registrations); + updatedList.add(registry); + Collections.sort(updatedList); + + this.registrations = updatedList; + } + } + + public PerContextHandlerRegistry getRegistry(final long key) + { + final List list = this.registrations; + for(final PerContextHandlerRegistry r : list) + { + if ( key == r.getContextServiceId()) + { + return r; + } + } + return null; + } + + public @Nullable ServletResolution getErrorHandler(@NotNull final String requestURI, + final Long serviceId, + final int code, + final Throwable exception) + { + final PerContextHandlerRegistry reg; + if ( serviceId == null ) + { + // if the context is unknown, we use the first matching one! + PerContextHandlerRegistry found = null; + final List regs = this.registrations; + for(final PerContextHandlerRegistry r : regs) + { + final String path = r.isMatching(requestURI); + if ( path != null ) + { + found = r; + break; + } + } + reg = found; + } + else + { + reg = this.getRegistry(serviceId); + } + if ( reg != null ) + { + final ServletHandler handler = reg.getErrorHandler(code, exception); + if ( handler != null ) + { + final ServletResolution res = new ServletResolution(); + res.handler = handler; + res.handlerRegistry = reg; + + return res; + } + } + return null; + } + + public FilterHandler[] getFilters(@NotNull final ServletResolution pr, + @NotNull final DispatcherType dispatcherType, + @NotNull String requestURI) + { + if ( pr != null && pr.handlerRegistry != null ) + { + return pr.handlerRegistry.getFilterHandlers(pr.handler, dispatcherType, requestURI); + } + return EMPTY_FILTER_HANDLER; + } + + public PathResolution resolveServlet(@NotNull final String requestURI) + { + final List regs = this.registrations; + for(final PerContextHandlerRegistry r : regs) + { + final String path = r.isMatching(requestURI); + if ( path != null ) + { + final PathResolution ps = r.resolve(path); + if ( ps != null ) + { + // remove context path from request URI and add registry object + ps.requestURI = path; + ps.handlerRegistry = r; + return ps; + } + } + } + + return null; + } + + /** + * Get the servlet handler for a servlet by name + * @param contextId The context id + * @param name The servlet name + * @return The servlet handler or {@code null} + */ + public ServletResolution resolveServletByName(final long contextId, @NotNull final String name) + { + final PerContextHandlerRegistry reg = this.getRegistry(contextId); + if ( reg != null ) + { + final ServletHandler handler = reg.resolveServletByName(name); + if ( handler != null ) + { + final ServletResolution resolution = new ServletResolution(); + resolution.handler = handler; + resolution.handlerRegistry = reg; + + return resolution; + } + } + return null; + } + + public boolean getRuntimeInfo(@NotNull final ServletContextDTO dto, + @NotNull final FailedDTOHolder failedDTOHolder) + { + final PerContextHandlerRegistry reg = this.getRegistry(dto.serviceId); + if ( reg != null ) + { + reg.getRuntime(dto, failedDTOHolder); + return true; + } + return false; + } + + public PerContextHandlerRegistry getBestMatchingRegistry(String requestURI) + { + // if the context is unknown, we use the first matching one! + PerContextHandlerRegistry found = null; + final List regs = this.registrations; + for(final PerContextHandlerRegistry r : regs) + { + final String path = r.isMatching(requestURI); + if ( path != null ) + { + found = r; + break; + } + } + return found; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/registry/ListenerMap.java b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/ListenerMap.java new file mode 100644 index 00000000000..3370b477e86 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/ListenerMap.java @@ -0,0 +1,292 @@ +/* + * 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.http.base.internal.registry; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EventListener; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.jetbrains.annotations.NotNull; + +import org.apache.felix.http.base.internal.handler.ListenerHandler; +import org.apache.felix.http.base.internal.runtime.ListenerInfo; +import org.apache.felix.http.base.internal.runtime.dto.ListenerDTOBuilder; +import org.osgi.service.http.runtime.dto.FailedListenerDTO; +import org.osgi.service.http.runtime.dto.ListenerDTO; + +public class ListenerMap { + + private volatile List> handlers = Collections.emptyList(); + + /** + * The status object keeps track of the registration status of a listener + * The status objects are sorted by result first, followed by ranking. The active + * listeners ( result == -1) are first, followed by the inactive ones. This sorting + * allows to only traverse over the active ones and also avoids any sorting + * as the listeners are processed in the correct order already. + */ + private static final class ListenerRegistrationStatus implements Comparable> + { + private final int result; + private final ListenerHandler handler; + + public ListenerRegistrationStatus(@NotNull final ListenerHandler handler, final int result) + { + this.handler = handler; + this.result = result; + } + + public int getResult() + { + return this.result; + } + + public @NotNull ListenerHandler getHandler() + { + return this.handler; + } + + @Override + public int compareTo(final ListenerRegistrationStatus o) + { + int result = this.result - o.result; + if ( result == 0 ) + { + result = this.handler.compareTo(o.handler); + } + return result; + } + } + + public synchronized void cleanup() + { + this.handlers = Collections.emptyList(); + } + + public synchronized void add(final ListenerHandler handler, final int reason) + { + final ListenerRegistrationStatus status = new ListenerRegistrationStatus(handler, reason); + + final List> newList = new ArrayList>(this.handlers); + newList.add(status); + Collections.sort(newList); + this.handlers = newList; + } + + public synchronized ListenerHandler remove(final ListenerInfo info) + { + final List> newList = new ArrayList>(this.handlers); + final Iterator> i = newList.iterator(); + while ( i.hasNext() ) + { + final ListenerRegistrationStatus status = i.next(); + if ( status.getHandler().getListenerInfo().equals(info) ) + { + i.remove(); + this.handlers = newList; + + return status.getResult() == -1 ? status.getHandler() : null; + } + } + return null; + } + + public ListenerHandler getListenerHandler(@NotNull final ListenerInfo info) + { + final List> list = this.handlers; + for(final ListenerRegistrationStatus status : list) + { + if ( status.getHandler().getListenerInfo().equals(info) ) + { + return status.getHandler(); + } + } + return null; + } + + public Iterable getActiveHandlers() + { + final List> list = this.handlers; + final Iterator> iter = list.iterator(); + final Iterator newIter = new Iterator() + { + + private ListenerHandler next; + + { + peek(); + } + + private void peek() + { + next = null; + if ( iter.hasNext() ) + { + final ListenerRegistrationStatus status = iter.next(); + if ( status.getResult() == -1 ) + { + next = status.getHandler(); + } + } + } + + @Override + public boolean hasNext() + { + return this.next != null; + } + + @Override + public ListenerHandler next() + { + if ( this.next == null ) + { + throw new NoSuchElementException(); + } + final ListenerHandler result = this.next; + peek(); + return result; + } + + @Override + public void remove() + { + throw new UnsupportedOperationException(); + } + }; + return new Iterable() + { + + @Override + public Iterator iterator() + { + return newIter; + } + }; + } + + public Iterable getActiveListeners() + { + final Iterator iter = this.getActiveHandlers().iterator(); + final Iterator newIter = new Iterator() + { + + private T next; + + { + peek(); + } + + @SuppressWarnings("unchecked") + private void peek() + { + next = null; + while ( next == null && iter.hasNext() ) + { + final ListenerHandler handler = iter.next(); + next = (T)handler.getListener(); + } + } + + @Override + public boolean hasNext() + { + return this.next != null; + } + + @Override + public T next() + { + if ( this.next == null ) + { + throw new NoSuchElementException(); + } + final T result = this.next; + peek(); + return result; + } + + @Override + public void remove() + { + throw new UnsupportedOperationException(); + } + }; + return new Iterable() + { + + @Override + public Iterator iterator() + { + return newIter; + } + }; + } + + public void getRuntimeInfo(final List listenerDTOs, final List failedListenerDTOs) + { + final int length = listenerDTOs.size(); + final int failedLength = failedListenerDTOs.size(); + final List> list = this.handlers; + for (final ListenerRegistrationStatus status : list) + { + // search for DTO with same service id and failureCode + if ( status.getResult() == -1 ) + { + int index = 0; + boolean found = false; + final Iterator i = listenerDTOs.iterator(); + while ( !found && index < length ) + { + final ListenerDTO dto = i.next(); + if ( dto.serviceId == status.getHandler().getListenerInfo().getServiceId() ) + { + found = true; + } + index++; + } + if ( !found ) + { + listenerDTOs.add(ListenerDTOBuilder.build(status.getHandler(), status.getResult())); + } + } + else + { + int index = 0; + boolean found = false; + final Iterator i = failedListenerDTOs.iterator(); + while ( !found && index < failedLength ) + { + final FailedListenerDTO dto = i.next(); + if ( dto.serviceId == status.getHandler().getListenerInfo().getServiceId() + && dto.failureReason == status.getResult() ) + { + found = true; + } + index++; + } + if ( !found ) + { + failedListenerDTOs.add((FailedListenerDTO)ListenerDTOBuilder.build(status.getHandler(), status.getResult())); + } + } + } + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/registry/PathResolution.java b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/PathResolution.java new file mode 100644 index 00000000000..8287f33e531 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/PathResolution.java @@ -0,0 +1,29 @@ +/* + * 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.http.base.internal.registry; + + +public class PathResolution extends ServletResolution { + + public String servletPath; + + public String pathInfo; + + public String requestURI; + + public String[] patterns; +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/registry/PathResolver.java b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/PathResolver.java new file mode 100644 index 00000000000..389de1ddd18 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/PathResolver.java @@ -0,0 +1,33 @@ +/* + * 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.http.base.internal.registry; + +import org.apache.felix.http.base.internal.handler.ServletHandler; + + +public interface PathResolver extends Comparable { + + PathResolution resolve(String uri); + + ServletHandler getServletHandler(); + + int getRanking(); + + int getOrdering(); + + String getPattern(); +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/registry/PathResolverFactory.java b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/PathResolverFactory.java new file mode 100644 index 00000000000..d3a9ec4a1b6 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/PathResolverFactory.java @@ -0,0 +1,368 @@ +/* + * 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.http.base.internal.registry; + +import java.util.regex.Pattern; + +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; + +import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.service.HttpServiceFactory; + +/** + * The path resolver factory creates a path resolver for a pattern. + * The servlet spec supports different patterns + * - path mapping, a pattern starting with / and ending with /* + * - extension mapping, a pattern starting with *. + * - default mapping, the pattern / + * - root mapping, the pattern is the empty string + * - exact match + * + * Exact match is tried first, followed by longest match and finally + * extension match. + */ +public abstract class PathResolverFactory { + + public static @NotNull PathResolver createPatternMatcher(@Nullable final ServletHandler handler, @NotNull final String pattern) + { + if ( pattern.length() == 0 ) + { + return new RootMatcher(handler); + } + else if ( pattern.equals("/") ) + { + return new DefaultMatcher(handler); + } + else if ( pattern.startsWith("*.") ) + { + return new ExtensionMatcher(handler, pattern); + } + else if ( pattern.endsWith("/*") ) + { + return new PathMatcher(handler, pattern); + } + else if ( handler != null && handler.getContextServiceId() == HttpServiceFactory.HTTP_SERVICE_CONTEXT_SERVICE_ID ) + { + return new ExactAndPathMatcher(handler, pattern); + } + return new ExactMatcher(handler, pattern); + } + + public static @NotNull PathResolver createRegexMatcher(@NotNull final String regex) + { + return new RegexMatcher(regex); + } + + public static abstract class AbstractMatcher implements PathResolver + { + private final int ranking; + + private final String pattern; + + private final ServletHandler handler; + + public AbstractMatcher(final ServletHandler handler, final String pattern, final int ranking) + { + this.handler = handler; + this.ranking = ranking; + this.pattern = pattern; + } + + @Override + public int compareTo(final PathResolver o) + { + int result = o.getRanking() - this.ranking; + if ( result == 0 ) + { + result = o.getOrdering() - this.getOrdering(); + } + return result; + } + + @Override + public ServletHandler getServletHandler() + { + return this.handler; + } + + @Override + public int getRanking() + { + return this.ranking; + } + + @Override + public int getOrdering() + { + return 0; + } + + @Override + public String getPattern() + { + return this.pattern; + } + + @Override + public int hashCode() + { + return pattern.hashCode(); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) + { + return true; + } + if (obj == null || getClass() != obj.getClass()) + { + return false; + } + final AbstractMatcher other = (AbstractMatcher) obj; + return this.pattern.equals(other.pattern); + } + } + + public static final class RootMatcher extends AbstractMatcher + { + public RootMatcher(final ServletHandler handler) + { + super(handler, "", 2); + } + + @Override + public PathResolution resolve(final String uri) { + if ( uri.length() == 0 || uri.equals("/") ) + { + final PathResolution pr = new PathResolution(); + pr.pathInfo = "/"; + pr.servletPath = ""; + pr.requestURI = uri; + pr.handler = this.getServletHandler(); + + return pr; + } + return null; + } + } + + public static final class DefaultMatcher extends AbstractMatcher + { + public DefaultMatcher(final ServletHandler handler) + { + super(handler, "/", 1); + } + + @Override + public PathResolution resolve(final String uri) { + final PathResolution pr = new PathResolution(); + pr.pathInfo = null; + pr.servletPath = uri; + pr.requestURI = uri; + pr.handler = this.getServletHandler(); + + return pr; + } + } + + public static final class ExactAndPathMatcher extends AbstractMatcher + { + private final String path; + + private final String prefix; + + public ExactAndPathMatcher(final ServletHandler handler, final String pattern) + { + super(handler, pattern, 5); + this.path = pattern; + this.prefix = pattern.concat("/"); + } + + @Override + public PathResolution resolve(final String uri) { + if ( uri.equals(this.path) ) + { + final PathResolution pr = new PathResolution(); + pr.pathInfo = null; + pr.servletPath = uri; + pr.requestURI = uri; + pr.handler = this.getServletHandler(); + + return pr; + } + else if ( uri.startsWith(prefix) ) + { + final PathResolution pr = new PathResolution(); + pr.servletPath = this.prefix.substring(0, this.prefix.length() - 1); + pr.pathInfo = uri.substring(pr.servletPath.length()); + pr.requestURI = uri; + pr.handler = this.getServletHandler(); + + return pr; + } + return null; + } + + @Override + public int getOrdering() + { + return this.path.length(); + } + } + + public static final class ExactMatcher extends AbstractMatcher + { + private final String path; + + public ExactMatcher(final ServletHandler handler, final String pattern) + { + super(handler, pattern, 5); + this.path = pattern; + } + + @Override + public PathResolution resolve(final String uri) { + if ( uri.equals(this.path) ) + { + final PathResolution pr = new PathResolution(); + pr.servletPath = uri; + pr.pathInfo = null; + pr.requestURI = uri; + pr.handler = this.getServletHandler(); + + return pr; + } + return null; + } + + @Override + public int getOrdering() + { + return this.path.length(); + } + } + + public static final class PathMatcher extends AbstractMatcher + { + private final String prefix; + + private final String path; + + public PathMatcher(final ServletHandler handler, final String pattern) + { + super(handler, pattern, 4); + this.prefix = pattern.substring(0, pattern.length() - 1); + this.path = pattern.substring(0, pattern.length() - 2); + } + + @Override + public PathResolution resolve(final String uri) { + if ( uri.equals(this.path) ) + { + final PathResolution pr = new PathResolution(); + pr.servletPath = this.path; + pr.pathInfo = null; + pr.requestURI = uri; + pr.handler = this.getServletHandler(); + + return pr; + } + if ( uri.startsWith(this.prefix) ) + { + final PathResolution pr = new PathResolution(); + pr.servletPath = this.prefix.substring(0, this.prefix.length() - 1); + pr.pathInfo = uri.substring(pr.servletPath.length()); + pr.requestURI = uri; + pr.handler = this.getServletHandler(); + + return pr; + } + return null; + } + + @Override + public int getOrdering() + { + return this.prefix.length() + 1; + } + } + + public static final class ExtensionMatcher extends AbstractMatcher + { + private final String extension; + + public ExtensionMatcher(final ServletHandler handler, final String pattern) + { + super(handler, pattern, 3); + this.extension = pattern.substring(1); + } + + @Override + public PathResolution resolve(final String uri) { + if ( uri.endsWith(this.extension) ) + { + final PathResolution pr = new PathResolution(); + pr.pathInfo = null; + pr.servletPath = uri; + pr.requestURI = uri; + pr.handler = this.getServletHandler(); + + return pr; + } + return null; + } + + @Override + public int getOrdering() + { + return this.extension.length(); + } + } + + public static final class RegexMatcher extends AbstractMatcher + { + private final Pattern pattern; + + public RegexMatcher(final String regex) + { + super(null, regex, 0); + this.pattern = Pattern.compile(regex); + } + + @Override + public @Nullable PathResolution resolve(@NotNull final String uri) { + if ( pattern.matcher(uri).matches() ) + { + final PathResolution pr = new PathResolution(); + pr.pathInfo = null; + pr.servletPath = uri; + pr.requestURI = uri; + + return pr; + } + return null; + } + + @Override + public int getOrdering() + { + return this.pattern.toString().length(); + } + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/registry/PerContextHandlerRegistry.java b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/PerContextHandlerRegistry.java new file mode 100644 index 00000000000..90c8b5cc67c --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/PerContextHandlerRegistry.java @@ -0,0 +1,270 @@ +/* + * 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.http.base.internal.registry; + +import javax.servlet.DispatcherType; + +import org.apache.felix.http.base.internal.HttpConfig; +import org.apache.felix.http.base.internal.handler.FilterHandler; +import org.apache.felix.http.base.internal.handler.ListenerHandler; +import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.runtime.FilterInfo; +import org.apache.felix.http.base.internal.runtime.ListenerInfo; +import org.apache.felix.http.base.internal.runtime.ServletContextHelperInfo; +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.apache.felix.http.base.internal.runtime.dto.FailedDTOHolder; +import org.apache.felix.http.base.internal.service.HttpServiceFactory; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.osgi.service.http.runtime.dto.ServletContextDTO; + +/** + * This registry keeps track of all processing components per context: + * - servlets + * - filters + * - error pages + */ +public final class PerContextHandlerRegistry implements Comparable +{ + /** Service id of the context. */ + private final long serviceId; + + /** Ranking of the context. */ + private final int ranking; + + /** The context path. */ + private final String path; + + /** The context prefix. */ + private final String prefix; + + private final ServletRegistry servletRegistry = new ServletRegistry(); + + private final FilterRegistry filterRegistry = new FilterRegistry(); + + private final ErrorPageRegistry errorPageRegistry = new ErrorPageRegistry(); + + private final EventListenerRegistry eventListenerRegistry = new EventListenerRegistry(); + + private final HttpConfig config; + + + /** + * Default http service registry + */ + public PerContextHandlerRegistry(@NotNull final HttpConfig config) + { + this.config = config; + this.serviceId = HttpServiceFactory.HTTP_SERVICE_CONTEXT_SERVICE_ID; + this.ranking = Integer.MAX_VALUE; + this.path = "/"; + this.prefix = null; + } + + /** + * Registry for a servlet context helper (whiteboard support) + * @param info The servlet context helper info + */ + public PerContextHandlerRegistry(@NotNull final ServletContextHelperInfo info, @NotNull final HttpConfig config) + { + this.config = config; + this.serviceId = info.getServiceId(); + this.ranking = info.getRanking(); + this.path = info.getPath(); + if ( this.path.equals("/") ) + { + this.prefix = null; + } + else + { + this.prefix = this.path + "/"; + } + } + + public long getContextServiceId() + { + return this.serviceId; + } + + public HttpConfig getConfig() + { + return this.config; + } + + public void removeAll() + { + this.errorPageRegistry.cleanup(); + this.eventListenerRegistry.cleanup(); + this.filterRegistry.cleanup(); + this.servletRegistry.cleanup(); + } + + @Override + public int compareTo(@NotNull final PerContextHandlerRegistry other) + { + final int result = new Integer(other.path.length()).compareTo(this.path.length()); + if ( result == 0 ) { + if (this.ranking == other.ranking) + { + // Service id's can be negative. Negative id's follow the reverse natural ordering of integers. + int reverseOrder = ( this.serviceId <= 0 && other.serviceId <= 0 ) ? -1 : 1; + return reverseOrder * new Long(this.serviceId).compareTo(other.serviceId); + } + + return new Integer(other.ranking).compareTo(this.ranking); + } + return result; + } + + public String isMatching(@NotNull final String requestURI) + { + if (requestURI.equals(this.path)) + { + return ""; + } + if (this.prefix == null) + { + return requestURI; + } + if (requestURI.startsWith(this.prefix)) + { + return requestURI.substring(this.prefix.length() - 1); + } + return null; + } + + public PathResolution resolve(@NotNull final String relativeRequestURI) + { + return this.servletRegistry.resolve(relativeRequestURI); + } + + public ServletHandler resolveServletByName(final String name) + { + return this.servletRegistry.resolveByName(name); + } + + /** + * Get filter handlers for the request uri + * @param servletHandler The servlet handler (might be null) + * @param dispatcherType The dispatcher type + * @param requestURI The request uri + * @return The array of filter handlers, the array might be empty. + */ + public @NotNull FilterHandler[] getFilterHandlers(@Nullable final ServletHandler servletHandler, + @NotNull final DispatcherType dispatcherType, + @NotNull final String requestURI) + { + return this.filterRegistry.getFilterHandlers(servletHandler, dispatcherType, requestURI); + } + + /** + * Get the servlet handling the error. + * @param code The error code + * @param exception The optional exception + * @return The servlet handler or {@code null}. + */ + public @Nullable ServletHandler getErrorHandler(final int code, @Nullable final Throwable exception) + { + return this.errorPageRegistry.get(exception, code); + } + + public EventListenerRegistry getEventListenerRegistry() + { + return this.eventListenerRegistry; + } + + /** + * Create all DTOs for servlets, filters, resources and error pages + * @param dto The servlet context DTO + * @param failedDTOHolder The container for all failed DTOs + */ + public void getRuntime(final ServletContextDTO dto, + final FailedDTOHolder failedDTOHolder) + { + // collect filters + this.filterRegistry.getRuntimeInfo(dto, failedDTOHolder.failedFilterDTOs); + + // collect error pages + this.errorPageRegistry.getRuntimeInfo(dto, failedDTOHolder.failedErrorPageDTOs); + + // collect servlets and resources + this.servletRegistry.getRuntimeInfo(dto, failedDTOHolder.failedServletDTOs, failedDTOHolder.failedResourceDTOs); + + // collect listeners + this.eventListenerRegistry.getRuntimeInfo(dto, failedDTOHolder.failedListenerDTOs); + } + + /** + * Add a servlet + * @param handler The servlet handler + */ + public void registerServlet(@NotNull final ServletHandler handler) + { + this.servletRegistry.addServlet(handler); + this.errorPageRegistry.addServlet(handler); + } + + /** + * Remove a servlet + * @param servletInfo The servlet info + * @param destroy Destroy the servlet + */ + public void unregisterServlet(@NotNull final ServletInfo servletInfo, final boolean destroy) + { + this.servletRegistry.removeServlet(servletInfo, destroy); + this.errorPageRegistry.removeServlet(servletInfo, destroy); + } + + /** + * Add a filter + * @param handler The filter handler + */ + public void registerFilter(@NotNull final FilterHandler handler) + { + this.filterRegistry.addFilter(handler); + } + + /** + * Remove a filter + * @param info The filter info + * @param destroy Destroy the filter + */ + public void unregisterFilter(@NotNull final FilterInfo info, final boolean destroy) + { + this.filterRegistry.removeFilter(info, destroy); + } + + /** + * Register listeners + * @param listenerHandler + */ + public void registerListeners(@NotNull final ListenerHandler listenerHandler) + { + this.eventListenerRegistry.addListeners(listenerHandler); + } + + /** + * Unregister listeners + * + * @param info The listener info + */ + public void unregisterListeners(@NotNull final ListenerInfo info) + { + this.eventListenerRegistry.removeListeners(info); + } + +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/registry/ServletRegistry.java b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/ServletRegistry.java new file mode 100644 index 00000000000..e8492780747 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/ServletRegistry.java @@ -0,0 +1,529 @@ +/* + * 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.http.base.internal.registry; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; + +import org.jetbrains.annotations.NotNull; + +import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.apache.felix.http.base.internal.runtime.dto.BuilderConstants; +import org.apache.felix.http.base.internal.runtime.dto.ResourceDTOBuilder; +import org.apache.felix.http.base.internal.runtime.dto.ServletDTOBuilder; +import org.osgi.service.http.runtime.dto.DTOConstants; +import org.osgi.service.http.runtime.dto.FailedResourceDTO; +import org.osgi.service.http.runtime.dto.FailedServletDTO; +import org.osgi.service.http.runtime.dto.ResourceDTO; +import org.osgi.service.http.runtime.dto.ServletContextDTO; +import org.osgi.service.http.runtime.dto.ServletDTO; + +/** + * The servlet registry keeps the mappings for all servlets (by using their pattern) + * for a single servlet context. + */ +public final class ServletRegistry +{ + private static final String NAMED_SERVLET_PATTERN = ":::"; + + private volatile List activeResolvers = Collections.emptyList(); + + private final Map> inactiveServletMappings = new HashMap>(); + + private final Map> servletsByName = new ConcurrentHashMap>(); + + private static final class RegistrationStatus + { + public ServletHandler handler; + public Map statusToPath = new HashMap(); + } + + private volatile Map mapping = Collections.emptyMap(); + + /** + * Resolve a request uri + * + * @param relativeRequestURI The request uri + * @return A path resolution if a servlet matched, {@code null} otherwise + */ + public PathResolution resolve(@NotNull final String relativeRequestURI) + { + final List resolvers = this.activeResolvers; + for(final PathResolver entry : resolvers) + { + final PathResolution pr = entry.resolve(relativeRequestURI); + if ( pr != null ) + { + // TODO - we should have all patterns under which this servlet is actively registered + pr.patterns = new String[] {entry.getPattern()}; + return pr; + } + } + return null; + } + + private PathResolver findResolver(final List resolvers, final String pattern) + { + for(final PathResolver pr : resolvers) + { + if ( pr.getPattern().equals(pattern) ) + { + return pr; + } + } + return null; + } + + /** + * Add a servlet. + * + * @param handler The servlet handler + */ + public synchronized void addServlet(@NotNull final ServletHandler handler) + { + // we have to check for every pattern in the info + // Can be null in case of error-handling servlets and named servlets + if ( handler.getServletInfo().getPatterns() != null ) + { + final Map newMap = new TreeMap(this.mapping); + + final List resolvers = new ArrayList(this.activeResolvers); + + final RegistrationStatus status = new RegistrationStatus(); + status.handler = handler; + + boolean isActive = false; + // used for detecting duplicates + final Set patterns = new HashSet(); + for(final String pattern : handler.getServletInfo().getPatterns()) + { + if ( patterns.contains(pattern) ) + { + continue; + } + patterns.add(pattern); + final PathResolver regHandler = findResolver(resolvers, pattern); + if ( regHandler != null ) + { + if ( regHandler.getServletHandler().getServletInfo().compareTo(handler.getServletInfo()) > 0 ) + { + // replace if no error with new servlet + if ( this.tryToActivate(resolvers, pattern, handler, status, regHandler) ) + { + isActive = true; + final String oldName = regHandler.getServletHandler().getName(); + regHandler.getServletHandler().destroy(); + + final RegistrationStatus oldStatus = newMap.get(regHandler.getServletHandler().getServletInfo()); + final RegistrationStatus newOldStatus = new RegistrationStatus(); + newOldStatus.handler = oldStatus.handler; + newOldStatus.statusToPath = new HashMap(oldStatus.statusToPath); + newMap.put(regHandler.getServletHandler().getServletInfo(), newOldStatus); + this.addToInactiveList(pattern, regHandler.getServletHandler(), newOldStatus); + + if ( regHandler.getServletHandler().getServlet() == null ) + { + removeFromNameMapping(oldName, regHandler.getServletHandler()); + } + } + } + else + { + // add to inactive + this.addToInactiveList(pattern, handler, status); + } + } + else + { + // add to active + if ( this.tryToActivate(resolvers, pattern, handler, status, null) ) + { + isActive = true; + } + } + } + newMap.put(handler.getServletInfo(), status); + if ( isActive ) + { + addToNameMapping(handler); + } + Collections.sort(resolvers); + this.activeResolvers = resolvers; + this.mapping = newMap; + } + else if ( !handler.getServletInfo().isResource() && handler.getServletInfo().getName() != null ) + { + final Map newMap = new TreeMap(this.mapping); + + final RegistrationStatus status = new RegistrationStatus(); + status.handler = handler; + + // if a servlet has only a name we always try to activate + // this is not very efficient, but works + // add to active + final int result = handler.init(); + if ( result == -1 ) + { + addToNameMapping(handler); + } + addPattern(status, result, NAMED_SERVLET_PATTERN); + + newMap.put(handler.getServletInfo(), status); + this.mapping = newMap; + } + } + + private void addToNameMapping(final ServletHandler handler) + { + if ( !handler.getServletInfo().isResource() ) + { + final String servletName = handler.getName(); + List list = this.servletsByName.get(servletName); + if ( list == null ) + { + list = new ArrayList(); + list.add(handler); + } + else + { + list = new ArrayList(list); + list.add(handler); + Collections.sort(list); + } + this.servletsByName.put(servletName, list); + } + } + + private void removeFromNameMapping(final String servletName, final ServletHandler handler) + { + if ( !handler.getServletInfo().isResource() ) + { + List list = this.servletsByName.get(servletName); + if ( list != null ) + { + final List newList = new ArrayList(list); + final Iterator i = newList.iterator(); + while ( i.hasNext() ) + { + final ServletHandler s = i.next(); + if ( s == handler ) + { + i.remove(); + break; + } + } + if ( newList.isEmpty() ) + { + this.servletsByName.remove(servletName); + } + else + { + this.servletsByName.put(servletName, newList); + } + } + } + } + + /** + * Remove a servlet + * @param info The servlet info + */ + public synchronized void removeServlet(@NotNull final ServletInfo info, final boolean destroy) + { + if ( info.getPatterns() != null ) + { + final List resolvers = new ArrayList(this.activeResolvers); + + final Map newMap = new TreeMap(this.mapping); + newMap.remove(info); + + ServletHandler cleanupHandler = null; + + // used for detecting duplicates + final Set patterns = new HashSet(); + for(final String pattern : info.getPatterns()) + { + if ( patterns.contains(pattern) ) + { + continue; + } + patterns.add(pattern); + final PathResolver regHandler = this.findResolver(resolvers, pattern); + if ( regHandler != null && regHandler.getServletHandler().getServletInfo().equals(info) ) + { + cleanupHandler = regHandler.getServletHandler(); + removeFromNameMapping(cleanupHandler.getName(), cleanupHandler); + + final List inactiveList = this.inactiveServletMappings.get(pattern); + if ( inactiveList == null ) + { + resolvers.remove(regHandler); + } + else + { + boolean done = false; + while ( !done ) + { + final ServletHandler h = inactiveList.remove(0); + boolean activate = h.getServlet() == null; + final RegistrationStatus oldStatus = newMap.get(h.getServletInfo()); + if ( oldStatus != null ) { + final RegistrationStatus newOldStatus = new RegistrationStatus(); + newOldStatus.handler = oldStatus.handler; + newOldStatus.statusToPath = new HashMap(oldStatus.statusToPath); + removePattern(newOldStatus, DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE, pattern); + newMap.put(h.getServletInfo(), newOldStatus); + done = this.tryToActivate(resolvers, pattern, h, newOldStatus, regHandler); + if ( done && activate ) { + this.addToNameMapping(h); + } + } + if ( !done ) + { + done = inactiveList.isEmpty(); + } + } + if ( inactiveList.isEmpty() ) + { + this.inactiveServletMappings.remove(pattern); + } + } + } + else + { + final List inactiveList = this.inactiveServletMappings.get(pattern); + if ( inactiveList != null ) + { + final Iterator i = inactiveList.iterator(); + while ( i.hasNext() ) + { + final ServletHandler h = i.next(); + if ( h.getServletInfo().equals(info) ) + { + i.remove(); + cleanupHandler = h; + break; + } + } + if ( inactiveList.isEmpty() ) + { + this.inactiveServletMappings.remove(pattern); + } + } + } + } + + Collections.sort(resolvers); + this.activeResolvers = resolvers; + this.mapping = newMap; + + if ( cleanupHandler != null ) + { + cleanupHandler.dispose(); + } + } + else if ( !info.isResource() && info.getName() != null ) + { + final Map newMap = new TreeMap(this.mapping); + final RegistrationStatus status = newMap.remove(info); + + if ( status != null ) + { + removeFromNameMapping(info.getName(), status.handler); + + this.mapping = newMap; + + status.handler.dispose(); + } + + } + } + + public synchronized void cleanup() + { + this.activeResolvers = Collections.emptyList(); + this.inactiveServletMappings.clear(); + this.servletsByName.clear(); + this.mapping = Collections.emptyMap(); + } + + private void addToInactiveList(final String pattern, final ServletHandler handler, final RegistrationStatus status) + { + List inactiveList = this.inactiveServletMappings.get(pattern); + if ( inactiveList == null ) + { + inactiveList = new ArrayList(); + this.inactiveServletMappings.put(pattern, inactiveList); + } + inactiveList.add(handler); + Collections.sort(inactiveList); + removePattern(status, -1, pattern); + addPattern(status, DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE, pattern); + } + + private boolean tryToActivate(final List resolvers, + final String pattern, + final ServletHandler handler, + final RegistrationStatus status, + final PathResolver oldResolver) + { + // add to active + final int result = handler.init(); + if ( result == -1 ) + { + if ( oldResolver != null ) + { + resolvers.remove(oldResolver); + } + final PathResolver resolver = PathResolverFactory.createPatternMatcher(handler, pattern); + resolvers.add(resolver); + } + // update status + addPattern(status, result, pattern); + return result == -1; + } + + private void addPattern(final RegistrationStatus status, final int failureCode, final String pattern) + { + String[] paths = status.statusToPath.get(failureCode); + if ( paths == null ) + { + status.statusToPath.put(failureCode, new String[] {pattern}); + } + else + { + final String[] newPaths = new String[paths.length + 1]; + System.arraycopy(paths, 0, newPaths, 0, paths.length); + newPaths[paths.length] = pattern; + status.statusToPath.put(failureCode, newPaths); + } + } + + private void removePattern(final RegistrationStatus status, final int failureCode, final String pattern) + { + String[] paths = status.statusToPath.get(failureCode); + if ( paths != null ) + { + final List array = new ArrayList(Arrays.asList(paths)); + if ( array.remove(pattern) ) + { + if ( array.isEmpty() ) + { + status.statusToPath.remove(failureCode); + } + else + { + status.statusToPath.put(failureCode, array.toArray(new String[array.size()])); + } + } + } + } + + public ServletHandler resolveByName(final @NotNull String name) + { + final List handlerList = this.servletsByName.get(name); + if ( handlerList != null ) + { + return handlerList.get(0); + } + return null; + } + + public void getRuntimeInfo( + final ServletContextDTO servletContextDTO, + final Collection allFailedServletDTOs, + final Collection allFailedResourceDTOs) + { + final Map servletDTOs = new HashMap(); + final Map resourceDTOs = new HashMap(); + + final Map failedServletDTOs = new HashMap(); + final Map failedResourceDTOs = new HashMap(); + + for(final Map.Entry entry : mapping.entrySet()) + { + final long serviceId = entry.getKey().getServiceId(); + for(final Map.Entry map : entry.getValue().statusToPath.entrySet()) + { + if ( entry.getKey().isResource() ) + { + final ResourceDTO state = ResourceDTOBuilder.build(entry.getValue().handler, map.getKey()); + if ( map.getValue().length == 1 && NAMED_SERVLET_PATTERN == map.getValue()[0] ) + { + state.patterns = BuilderConstants.EMPTY_STRING_ARRAY; + } + else + { + state.patterns = Arrays.copyOf(map.getValue(), map.getValue().length); + } + if ( map.getKey() == -1 ) + { + resourceDTOs.put(serviceId, state); + } + else + { + failedResourceDTOs.put(serviceId, (FailedResourceDTO)state); + } + } + else + { + final ServletDTO state = ServletDTOBuilder.build(entry.getValue().handler, map.getKey()); + if ( map.getValue().length == 1 && NAMED_SERVLET_PATTERN == map.getValue()[0] ) + { + state.patterns = BuilderConstants.EMPTY_STRING_ARRAY; + } + else + { + state.patterns = Arrays.copyOf(map.getValue(), map.getValue().length); + } + if ( map.getKey() == -1 ) + { + servletDTOs.put(serviceId, state); + } + else + { + failedServletDTOs.put(serviceId, (FailedServletDTO)state); + } + } + } + } + + final Collection servletDTOsArray = servletDTOs.values(); + if ( !servletDTOsArray.isEmpty() ) + { + servletContextDTO.servletDTOs = servletDTOsArray.toArray(new ServletDTO[servletDTOsArray.size()]); + } + final Collection resourceDTOsArray = resourceDTOs.values(); + if ( !resourceDTOsArray.isEmpty() ) + { + servletContextDTO.resourceDTOs = resourceDTOsArray.toArray(new ResourceDTO[resourceDTOsArray.size()]); + } + allFailedResourceDTOs.addAll(failedResourceDTOs.values()); + allFailedServletDTOs.addAll(failedServletDTOs.values()); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/registry/ServletResolution.java b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/ServletResolution.java new file mode 100644 index 00000000000..5729a8bb9ba --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/registry/ServletResolution.java @@ -0,0 +1,26 @@ +/* + * 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.http.base.internal.registry; + +import org.apache.felix.http.base.internal.handler.ServletHandler; + +public class ServletResolution { + + public ServletHandler handler; + + public PerContextHandlerRegistry handlerRegistry; +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/AbstractInfo.java b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/AbstractInfo.java new file mode 100644 index 00000000000..630f90b4bad --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/AbstractInfo.java @@ -0,0 +1,229 @@ +/* + * 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.http.base.internal.runtime; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; + +/** + * Base class for all info objects. + * Provides support for ranking and ordering of services. + */ +public abstract class AbstractInfo implements Comparable> +{ + /** Service ranking. */ + private final int ranking; + + /** Service id. */ + private final long serviceId; + + /** Service reference. */ + private final ServiceReference serviceReference; + + /** Target. */ + private final String target; + + public AbstractInfo(final ServiceReference ref) + { + this.serviceId = (Long) ref.getProperty(Constants.SERVICE_ID); + final Object rankingObj = ref.getProperty(Constants.SERVICE_RANKING); + if (rankingObj instanceof Integer) + { + this.ranking = (Integer) rankingObj; + } + else + { + this.ranking = 0; + } + this.serviceReference = ref; + this.target = getStringProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_TARGET); + } + + public AbstractInfo(final int ranking, final long serviceId) + { + this.ranking = ranking; + this.serviceId = serviceId; + this.serviceReference = null; + this.target = null; + } + + public boolean isValid() + { + return true; + } + + /** + * Compare two info objects based on their ranking (aka reverse ServiceReference ordering) + */ + @Override + public int compareTo(final AbstractInfo other) + { + if (this.ranking == other.ranking) + { + if ( this.serviceId == other.serviceId ) + { + return this.getClass().getName().compareTo(other.getClass().getName()); + } + // Service id's can be negative. Negative id's follow the reverse natural ordering of integers. + int reverseOrder = ( this.serviceId >= 0 && other.serviceId >= 0 ) ? 1 : -1; + return reverseOrder * new Long(this.serviceId).compareTo(other.serviceId); + } + + int result = new Integer(other.ranking).compareTo(this.ranking); + return result; + } + + protected boolean isEmpty(final String value) + { + return value == null || value.length() == 0; + } + + protected boolean isEmpty(final String[] value) + { + return value == null || value.length == 0; + } + + protected String getStringProperty(final ServiceReference ref, final String key) + { + final Object value = ref.getProperty(key); + return (value instanceof String) ? ((String) value).trim() : null; + } + + protected String[] getStringArrayProperty(ServiceReference ref, String key) + { + Object value = ref.getProperty(key); + + if (value instanceof String) + { + return new String[] { ((String) value).trim() }; + } + else if (value instanceof String[]) + { + final String[] arr = (String[]) value; + String[] values = new String[arr.length]; + + for (int i = 0, j = 0; i < arr.length; i++) + { + if (arr[i] != null) + { + values[j++] = arr[i].trim(); + } + } + return values; + } + else if (value instanceof Collection) + { + Collection collectionValues = (Collection) value; + String[] values = new String[collectionValues.size()]; + + int i = 0; + for (Object current : collectionValues) + { + values[i++] = current != null ? String.valueOf(current).trim() : null; + } + + return values; + } + + return null; + } + + protected boolean getBooleanProperty(ServiceReference ref, String key) + { + Object value = ref.getProperty(key); + if (value instanceof String) + { + return Boolean.valueOf((String) value); + } + else if (value instanceof Boolean) + { + return ((Boolean) value).booleanValue(); + } + return false; + } + + /** + * Get the init parameters. + */ + protected Map getInitParams(final ServiceReference ref, final String prefix) + { + final Map result = new HashMap(); + for (final String key : ref.getPropertyKeys()) + { + if (key.startsWith(prefix)) + { + final String paramKey = key.substring(prefix.length()); + final String paramValue = getStringProperty(ref, key); + + if (paramValue != null) + { + result.put(paramKey, paramValue); + } + } + } + return Collections.unmodifiableMap(result); + } + + public int getRanking() + { + return this.ranking; + } + + public long getServiceId() + { + return this.serviceId; + } + + public String getTarget() + { + return this.target; + } + + public ServiceReference getServiceReference() + { + return this.serviceReference; + } + + @Override + public int hashCode() + { + return 31 + (int) (serviceId ^ (serviceId >>> 32)); + } + + @Override + public boolean equals(final Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null || getClass() != obj.getClass()) + { + return false; + } + final AbstractInfo other = (AbstractInfo) obj; + return serviceId == other.serviceId; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/FilterInfo.java b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/FilterInfo.java new file mode 100644 index 00000000000..c477bdeeb5a --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/FilterInfo.java @@ -0,0 +1,224 @@ +/* + * 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.http.base.internal.runtime; + +import java.util.Collections; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import javax.servlet.DispatcherType; +import javax.servlet.Filter; + +import org.apache.felix.http.base.internal.util.PatternUtil; +import org.osgi.dto.DTO; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.runtime.dto.FilterDTO; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; + +/** + * Provides registration information for a {@link Filter}, and is used to programmatically register {@link Filter}s. + *

      + * This class only provides information used at registration time, and as such differs slightly from {@link DTO}s like, {@link FilterDTO}. + *

      + */ +public final class FilterInfo extends WhiteboardServiceInfo +{ + /** + * The name of the filter. + */ + private final String name; + + /** + * The request mappings for the servlet. + *

      + * The specified patterns are used to determine whether a request is mapped to the servlet filter.
      + * Note that these patterns should conform to the Servlet specification. + *

      + */ + private final String[] patterns; + + /** + * The servlet names for the servlet filter. + *

      + * The specified names are used to determine the servlets whose requests are mapped to the servlet filter. + *

      + */ + private final String[] servletNames; + + /** + * The request mappings for the servlet filter. + *

      + * The specified regular expressions are used to determine whether a request is mapped to the servlet filter.
      + * These regular expressions are a convenience extension allowing one to specify filters that match paths that are difficult to match with plain Servlet patterns alone. + *

      + */ + private final String[] regexs; + + /** + * Specifies whether the servlet filter supports asynchronous processing. + */ + private final boolean asyncSupported; + + /** + * The dispatcher associations for the servlet filter. + *

      + * The specified names are used to determine in what occasions the servlet filter is called. + * See {@link DispatcherType} and Servlet 3.0 specification, section 6.2.5. + *

      + */ + private final DispatcherType[] dispatcher; + + /** + * The filter initialization parameters as provided during registration of the filter. + */ + private final Map initParams; + + public FilterInfo(final ServiceReference ref) + { + super(ref); + this.name = getStringProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_NAME); + this.asyncSupported = getBooleanProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_ASYNC_SUPPORTED); + this.servletNames = getStringArrayProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_SERVLET); + this.patterns = getStringArrayProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_PATTERN); + this.regexs = getStringArrayProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_REGEX); + this.initParams = getInitParams(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_INIT_PARAM_PREFIX); + String[] dispatcherNames = getStringArrayProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_DISPATCHER); + if (dispatcherNames != null && dispatcherNames.length > 0) + { + DispatcherType[] dispatchers = new DispatcherType[dispatcherNames.length]; + for (int i = 0; i < dispatchers.length; i++) + { + try + { + dispatchers[i] = DispatcherType.valueOf(dispatcherNames[i].toUpperCase()); + } + catch ( final IllegalArgumentException iae) + { + dispatchers = null; + break; + } + } + this.dispatcher = dispatchers; + } + else + { + this.dispatcher = new DispatcherType[] {DispatcherType.REQUEST}; + } + } + + /** + * Constructor for Http Service + */ + public FilterInfo(final String name, + final String regex, + final int serviceRanking, + final Map initParams) + { + super(serviceRanking); + this.name = name; + this.patterns = null; + this.servletNames = null; + this.regexs = new String[] {regex}; + this.initParams = Collections.unmodifiableMap(initParams); + this.asyncSupported = false; + this.dispatcher = new DispatcherType[] {DispatcherType.REQUEST}; + } + + @Override + public boolean isValid() + { + boolean valid = super.isValid() && (!isEmpty(this.patterns) || !isEmpty(this.regexs) || !isEmpty(this.servletNames)); + if ( valid ) + { + if ( this.patterns != null ) + { + for(final String p : this.patterns) + { + if ( !PatternUtil.isValidPattern(p) ) + { + valid = false; + break; + } + } + } + if ( valid && this.regexs != null ) + { + for(final String p : this.regexs) + { + try + { + Pattern.compile(p); + } + catch ( final PatternSyntaxException pse) + { + valid = false; + break; + } + } + } + } + if ( valid ) + { + if ( this.dispatcher == null || this.dispatcher.length == 0 ) + { + valid = false; + } + } + return valid; + } + + public String getName() + { + return name; + } + + public String[] getPatterns() + { + return patterns; + } + + public String[] getServletNames() + { + return servletNames; + } + + public String[] getRegexs() + { + return regexs; + } + + public boolean isAsyncSupported() + { + return asyncSupported; + } + + public DispatcherType[] getDispatcher() + { + return dispatcher; + } + + /** + * Returns an immutable map of the init parameters. + */ + public Map getInitParameters() + { + return initParams; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/ListenerInfo.java b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/ListenerInfo.java new file mode 100644 index 00000000000..668a935ee22 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/ListenerInfo.java @@ -0,0 +1,125 @@ +/* + * 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.http.base.internal.runtime; + +import java.util.EventListener; +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.ServletContextAttributeListener; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletRequestAttributeListener; +import javax.servlet.ServletRequestListener; +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionIdListener; +import javax.servlet.http.HttpSessionListener; + +import org.jetbrains.annotations.NotNull; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; + +/** + * Info object for registered listeners. + */ +public class ListenerInfo extends WhiteboardServiceInfo +{ + private static final Set ALLOWED_INTERFACES; + static { + ALLOWED_INTERFACES = new HashSet(); + ALLOWED_INTERFACES.add(HttpSessionAttributeListener.class.getName()); + ALLOWED_INTERFACES.add(HttpSessionIdListener.class.getName()); + ALLOWED_INTERFACES.add(HttpSessionListener.class.getName()); + ALLOWED_INTERFACES.add(ServletContextAttributeListener.class.getName()); + ALLOWED_INTERFACES.add(ServletContextListener.class.getName()); + ALLOWED_INTERFACES.add(ServletRequestAttributeListener.class.getName()); + ALLOWED_INTERFACES.add(ServletRequestListener.class.getName()); + } + + private final boolean enabled; + + private final String[] types; + + /** + * Constructor for http whiteboard + * @param ref The service reference + */ + public ListenerInfo(final ServiceReference ref) + { + super(ref); + this.enabled = this.getBooleanProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_LISTENER); + final String[] objectClass = (String[])ref.getProperty(Constants.OBJECTCLASS); + final Set names = new HashSet(); + for(final String name : objectClass) + { + if ( ALLOWED_INTERFACES.contains(name) ) + { + names.add(name); + } + } + this.types = names.toArray(new String[names.size()]); + } + + /** + * Constructor for Apache Felix proprietary listener support + * @param ref The service reference + * @param marker Just a marker parameter to distinguish from other constructor + */ + public ListenerInfo(final ServiceReference ref, final boolean marker) + { + super(ref); + this.enabled = true; + final String[] objectClass = (String[])ref.getProperty(Constants.OBJECTCLASS); + final Set names = new HashSet(); + for(final String name : objectClass) + { + if ( ALLOWED_INTERFACES.contains(name) ) + { + names.add(name); + } + } + // remove interfaces not supported by Felix whiteboard + names.remove(HttpSessionIdListener.class.getName()); + names.remove(ServletContextListener.class.getName()); + this.types = names.toArray(new String[names.size()]); + } + + @Override + public boolean isValid() + { + return super.isValid() && this.enabled; + } + + public String[] getListenerTypes() + { + return this.types; + } + + public boolean isListenerType(@NotNull final String className) + { + for(final String t : this.types) + { + if ( t.equals(className) ) + { + return true; + } + } + return false; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/PreprocessorInfo.java b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/PreprocessorInfo.java new file mode 100644 index 00000000000..158eb0d4c8f --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/PreprocessorInfo.java @@ -0,0 +1,54 @@ +/* + * 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.http.base.internal.runtime; + +import java.util.Map; + +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; +import org.osgi.service.http.whiteboard.Preprocessor; + +/** + * Provides registration information for a {@link Preprocessor}. + *

      + * This class only provides information used at registration time, and as such differs + * slightly from the corresponding DTO + *

      + */ +public final class PreprocessorInfo extends WhiteboardServiceInfo +{ + /** + * The preprocessor initialization parameters as provided during registration of the preprocessor. + */ + private final Map initParams; + + public PreprocessorInfo(final ServiceReference ref) + { + super(ref); + this.initParams = getInitParams(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_PREPROCESSOR_INIT_PARAM_PREFIX); + } + + /** + * Returns an immutable map of the init parameters. + */ + public Map getInitParameters() + { + return initParams; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/ResourceInfo.java b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/ResourceInfo.java new file mode 100644 index 00000000000..abd07d9ab05 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/ResourceInfo.java @@ -0,0 +1,89 @@ +/* + * 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.http.base.internal.runtime; + +import org.apache.felix.http.base.internal.util.PatternUtil; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; + +/** + * Info object for a resource registration + */ +public final class ResourceInfo extends WhiteboardServiceInfo +{ + /** + * The request mappings for the resource. + */ + private final String[] patterns; + + /** + * The error pages and/or codes. + */ + private final String prefix; + + private final ServletInfo servletInfo; + + private static final class ResourceServletInfo extends ServletInfo { + + public ResourceServletInfo(ResourceInfo resource) { + super(resource); + } + }; + + public ResourceInfo(final ServiceReference ref) + { + super(ref); + this.patterns = getStringArrayProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_RESOURCE_PATTERN); + this.prefix = getStringProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_RESOURCE_PREFIX); + this.servletInfo = new ResourceServletInfo(this); + } + + @Override + public boolean isValid() + { + // TODO - do we need to check the prefix? + boolean valid = super.isValid() && !isEmpty(this.patterns) && !isEmpty(this.prefix); + if ( valid ) { + for(final String p : patterns) + { + if ( !PatternUtil.isValidPattern(p) ) + { + valid = false; + break; + } + } + } + return valid; + } + + public String getPrefix() + { + return this.prefix; + } + + public String[] getPatterns() + { + return patterns; + } + + public ServletInfo getServletInfo() + { + return this.servletInfo; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/ServletContextHelperInfo.java b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/ServletContextHelperInfo.java new file mode 100644 index 00000000000..6d0de3eea2e --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/ServletContextHelperInfo.java @@ -0,0 +1,109 @@ +/* + * 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.http.base.internal.runtime; + +import java.util.Collections; +import java.util.Map; + +import org.apache.felix.http.base.internal.service.HttpServiceFactory; +import org.apache.felix.http.base.internal.util.PatternUtil; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.context.ServletContextHelper; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; + +/** + * Provides registration information for a {@link ServletContextHelper} + */ +public final class ServletContextHelperInfo extends AbstractInfo +{ + + private final String name; + + private final String path; + + /** + * The filter initialization parameters as provided during registration of the filter. + */ + private final Map initParams; + + public ServletContextHelperInfo(final ServiceReference ref) + { + super(ref); + this.name = this.getStringProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME); + this.path = this.getStringProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_PATH); + this.initParams = getInitParams(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_INIT_PARAM_PREFIX); + } + + public ServletContextHelperInfo(final int serviceRanking, + final long serviceId, + final String name, + final String path, + final Map initParams) + { + super(serviceRanking, serviceId); + this.name = name; + this.path = path; + this.initParams = initParams == null ? Collections.emptyMap(): Collections.unmodifiableMap(initParams); + } + + private boolean isValidPath() + { + if (!this.isEmpty(path)) + { + if (path.equals("/")) + { + return true; + } + // TODO we need more validation + if (path.startsWith("/") && !path.endsWith("/")) + { + return true; + } + } + return false; + } + + @Override + public boolean isValid() + { + return super.isValid() + && PatternUtil.isValidSymbolicName(this.name) + && !HttpServiceFactory.HTTP_SERVICE_CONTEXT_NAME.equals(this.name) + && isValidPath(); + } + + public String getName() + { + return this.name; + } + + public String getPath() + { + return this.path; + } + + /** + * Returns an unmodifiable map of the parameters. + * @return + */ + public Map getInitParameters() + { + return initParams; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/ServletInfo.java b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/ServletInfo.java new file mode 100644 index 00000000000..59724dba0e2 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/ServletInfo.java @@ -0,0 +1,255 @@ +/* + * 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.http.base.internal.runtime; + +import java.util.Collections; +import java.util.Map; + +import javax.servlet.Servlet; + +import org.apache.felix.http.base.internal.dispatch.MultipartConfig; +import org.apache.felix.http.base.internal.util.PatternUtil; +import org.osgi.dto.DTO; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.runtime.dto.ServletDTO; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; + +/** + * Provides registration information for a {@link Servlet}, and is used to programmatically register {@link Servlet}s. + *

      + * This class only provides information used at registration time, and as such differs slightly from {@link DTO}s like, {@link ServletDTO}. + *

      + */ +public class ServletInfo extends WhiteboardServiceInfo +{ + /** + * The name of the servlet. + */ + private final String name; + + /** + * The request mappings for the servlet. + *

      + * The specified patterns are used to determine whether a request is mapped to the servlet. + *

      + */ + private final String[] patterns; + + /** + * The error pages and/or codes. + */ + private final String[] errorPage; + + /** + * Specifies whether the servlet supports asynchronous processing. + */ + private final boolean asyncSupported; + + /** + * Specifies whether the info represents a resource. + */ + private final boolean isResource; + + /** + * Specifies the multipart config for this servlet + * or {@code null} if not enabled. + */ + private final MultipartConfig multipartConfig; + + + /** + * The servlet initialization parameters as provided during registration of the servlet. + */ + private final Map initParams; + + private final String prefix; + + public ServletInfo(final ServiceReference ref) + { + super(ref); + this.name = getStringProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_NAME); + this.errorPage = getStringArrayProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_ERROR_PAGE); + this.patterns = getStringArrayProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN); + this.asyncSupported = getBooleanProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_ASYNC_SUPPORTED); + if ( getBooleanProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_MULTIPART_ENABLED) ) + { + MultipartConfig cfg = null; + try + { + cfg = new MultipartConfig(getIntProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_MULTIPART_FILESIZETHRESHOLD), + getStringProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_MULTIPART_LOCATION), + getLongProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_MULTIPART_MAXFILESIZE), + getLongProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_MULTIPART_MAXREQUESTSIZE)); + } + catch (final IllegalArgumentException iae) + { + cfg = MultipartConfig.INVALID_CONFIG; + } + this.multipartConfig = cfg; + } + else + { + this.multipartConfig = null; + } + this.initParams = getInitParams(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX); + this.isResource = false; + this.prefix = null; + } + + @SuppressWarnings("unchecked") + public ServletInfo(final ResourceInfo resource) + { + super(getRef(resource.getServiceReference())); + this.name = null; + this.patterns = resource.getPatterns(); + this.errorPage = null; + this.asyncSupported = false; + this.multipartConfig = null; + this.initParams = Collections.emptyMap(); + this.isResource = true; + this.prefix = resource.getPrefix(); + } + + private Integer getIntProperty(final ServiceReference ref, final String key) + { + final Object value = ref.getProperty(key); + if (value instanceof String) + { + return Integer.valueOf((String) value); + } + else if (value instanceof Number) + { + return ((Number) value).intValue(); + } + else if ( value != null ) + { + throw new IllegalArgumentException(); + } + return null; + } + + private long getLongProperty(final ServiceReference ref, final String key) + { + final Object value = ref.getProperty(key); + if (value instanceof String) + { + return Long.valueOf((String) value); + } + else if (value instanceof Number) + { + return ((Number) value).longValue(); + } + else if ( value != null ) + { + throw new IllegalArgumentException(); + } + return -1; + } + + @SuppressWarnings("rawtypes") + private static ServiceReference getRef(ServiceReference ref) + { + return ref; + } + + /** + * Constructor for Http Service + */ + public ServletInfo(final String name, + final String pattern, + final Map initParams) + { + super(Integer.MAX_VALUE); + this.name = name; + this.patterns = new String[] {pattern}; + this.initParams = Collections.unmodifiableMap(initParams); + this.asyncSupported = true; + this.multipartConfig = MultipartConfig.DEFAULT_CONFIG; + this.errorPage = null; + this.isResource = false; + this.prefix = null; + } + + @Override + public boolean isValid() + { + boolean valid = super.isValid() + && !(isEmpty(this.patterns) && isEmpty(this.errorPage) && isEmpty(this.name)) + && this.multipartConfig != MultipartConfig.INVALID_CONFIG; + if ( valid ) + { + if ( this.patterns != null ) + { + for(final String p : this.patterns) + { + if ( !PatternUtil.isValidPattern(p) ) + { + valid = false; + break; + } + } + } + } + return valid; + } + + public String getName() + { + return name; + } + + public String[] getPatterns() + { + return patterns; + } + + public String[] getErrorPage() + { + return errorPage; + } + + public boolean isAsyncSupported() + { + return asyncSupported; + } + + /** + * Returns an unmodifiable map of the init parameters. + * @return + */ + public Map getInitParameters() + { + return initParams; + } + + public boolean isResource() + { + return isResource; + } + + public String getPrefix() + { + return prefix; + } + + public MultipartConfig getMultipartConfig() + { + return multipartConfig; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/WhiteboardServiceInfo.java b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/WhiteboardServiceInfo.java new file mode 100644 index 00000000000..990b8ddc44a --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/WhiteboardServiceInfo.java @@ -0,0 +1,102 @@ +/* + * 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.http.base.internal.runtime; + +import org.apache.felix.http.base.internal.util.InternalIdFactory; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; + +/** + * Base class for all info objects. + * Provides support for ranking and ordering of services. + */ +public abstract class WhiteboardServiceInfo extends AbstractInfo +{ + /** The context selection. */ + private final String contextSelection; + + private final Filter filter; + + public WhiteboardServiceInfo(final ServiceReference ref) + { + super(ref); + String sel = getStringProperty(ref, HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT); + if ( isEmpty(sel) ) + { + this.contextSelection = "(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=" + + HttpWhiteboardConstants.HTTP_WHITEBOARD_DEFAULT_CONTEXT_NAME + ")"; + } + else + { + this.contextSelection = sel; + } + Filter f = null; + try + { + final Bundle bundle = ref.getBundle(); + if ( bundle != null ) + { + final BundleContext ctx = bundle.getBundleContext(); + if ( ctx != null ) + { + f = ctx.createFilter(this.contextSelection); + } + } + } + catch ( final InvalidSyntaxException ise) + { + // ignore + f = null; + } + this.filter = f; + } + + public WhiteboardServiceInfo(final int ranking) + { + this(ranking, InternalIdFactory.INSTANCE.next()); + + } + + public WhiteboardServiceInfo(final int ranking, final long serviceId) + { + super(ranking, serviceId); + this.contextSelection = null; + this.filter = null; + } + + @Override + public boolean isValid() + { + return super.isValid() && (this.filter != null || this.getServiceReference() == null); + } + + public String getContextSelection() + { + return this.contextSelection; + } + + public Filter getContextSelectionFilter() + { + return this.filter; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/BaseServletDTOBuilder.java b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/BaseServletDTOBuilder.java new file mode 100644 index 00000000000..e107b5cf356 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/BaseServletDTOBuilder.java @@ -0,0 +1,54 @@ +/* + * 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.http.base.internal.runtime.dto; + +import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.osgi.service.http.runtime.dto.BaseServletDTO; + +abstract class BaseServletDTOBuilder +{ + /** + * Build a servlet DTO from a servlet info + * @param info The servlet info + * @return A servlet DTO + */ + public static void fill(final BaseServletDTO dto, final ServletHandler handler) + { + dto.name = handler.getName(); + if ( handler.getServlet() != null ) + { + dto.servletInfo = handler.getServlet().getServletInfo(); + } + dto.servletContextId = handler.getContextServiceId(); + } + + /** + * Build a servlet DTO from a servlet info + * @param info The servlet info + * @return A servlet DTO + */ + public static void fill(final BaseServletDTO dto, final ServletInfo info) + { + dto.asyncSupported = info.isAsyncSupported(); + dto.initParams = info.getInitParameters(); + dto.name = info.getName(); + dto.serviceId = info.getServiceId(); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/BuilderConstants.java b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/BuilderConstants.java new file mode 100644 index 00000000000..708477e1df4 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/BuilderConstants.java @@ -0,0 +1,63 @@ +/* + * 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.http.base.internal.runtime.dto; + +import static java.util.Arrays.copyOf; + +import org.osgi.service.http.runtime.dto.ErrorPageDTO; +import org.osgi.service.http.runtime.dto.FailedErrorPageDTO; +import org.osgi.service.http.runtime.dto.FailedFilterDTO; +import org.osgi.service.http.runtime.dto.FailedListenerDTO; +import org.osgi.service.http.runtime.dto.FailedResourceDTO; +import org.osgi.service.http.runtime.dto.FailedServletContextDTO; +import org.osgi.service.http.runtime.dto.FailedServletDTO; +import org.osgi.service.http.runtime.dto.FilterDTO; +import org.osgi.service.http.runtime.dto.ListenerDTO; +import org.osgi.service.http.runtime.dto.ResourceDTO; +import org.osgi.service.http.runtime.dto.ServletContextDTO; +import org.osgi.service.http.runtime.dto.ServletDTO; + +public abstract class BuilderConstants +{ + + public static final String[] EMPTY_STRING_ARRAY = new String[0]; + public static final long[] EMPTY_LONG_ARRAY = new long[0]; + + public static final ServletContextDTO[] CONTEXT_DTO_ARRAY = new ServletContextDTO[0]; + + public static final ServletDTO[] SERVLET_DTO_ARRAY = new ServletDTO[0]; + public static final ResourceDTO[] RESOURCE_DTO_ARRAY = new ResourceDTO[0]; + public static final FilterDTO[] EMPTY_FILTER_DTO_ARRAY = new FilterDTO[0]; + public static final ErrorPageDTO[] ERROR_PAGE_DTO_ARRAY = new ErrorPageDTO[0]; + public static final ListenerDTO[] LISTENER_DTO_ARRAY = new ListenerDTO[0]; + + public static final FailedServletContextDTO[] CONTEXT_FAILURE_DTO_ARRAY = new FailedServletContextDTO[0]; + + public static final FailedServletDTO[] SERVLET_FAILURE_DTO_ARRAY = new FailedServletDTO[0]; + public static final FailedFilterDTO[] FILTER_FAILURE_DTO_ARRAY = new FailedFilterDTO[0]; + public static final FailedResourceDTO[] RESOURCE_FAILURE_DTO_ARRAY = new FailedResourceDTO[0]; + public static final FailedErrorPageDTO[] ERROR_PAGE_FAILURE_DTO_ARRAY = new FailedErrorPageDTO[0]; + public static final FailedListenerDTO[] LISTENER_FAILURE_DTO_ARRAY = new FailedListenerDTO[0]; + + public static V[] copyWithDefault(V[] array, V[] defaultArray) + { + return array == null ? defaultArray : copyOf(array, array.length); + } + +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/ErrorPageDTOBuilder.java b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/ErrorPageDTOBuilder.java new file mode 100644 index 00000000000..c3578e70b79 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/ErrorPageDTOBuilder.java @@ -0,0 +1,64 @@ +/* + * 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.http.base.internal.runtime.dto; + +import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.osgi.service.http.runtime.dto.ErrorPageDTO; +import org.osgi.service.http.runtime.dto.FailedErrorPageDTO; + +public final class ErrorPageDTOBuilder extends BaseServletDTOBuilder +{ + /** + * Build a servlet DTO from a servlet handler + * @param handler The servlet handler + * @param reason If reason is -1, a servlet DTO is created, otherwise a failed servlet DTO is returned + * @return A servlet DTO + */ + public static ErrorPageDTO build(final ServletHandler handler, final int reason) + { + final ErrorPageDTO dto = build(handler.getServletInfo(), reason != -1); + + BaseServletDTOBuilder.fill(dto, handler); + + if ( reason != -1 ) + { + ((FailedErrorPageDTO)dto).failureReason = reason; + } + + return dto; + } + + /** + * Build a servlet DTO from a servlet info + * @param info The servlet info + * @return A servlet DTO + */ + public static ErrorPageDTO build(final ServletInfo info, final boolean failed) + { + final ErrorPageDTO dto = (failed ? new FailedErrorPageDTO() : new ErrorPageDTO()); + + BaseServletDTOBuilder.fill(dto, info); + + dto.errorCodes = BuilderConstants.EMPTY_LONG_ARRAY; + dto.exceptions = BuilderConstants.EMPTY_STRING_ARRAY; + + return dto; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/FailedDTOHolder.java b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/FailedDTOHolder.java new file mode 100644 index 00000000000..14ce4c53c6e --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/FailedDTOHolder.java @@ -0,0 +1,128 @@ +/* + * 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.http.base.internal.runtime.dto; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.apache.felix.http.base.internal.registry.ErrorPageRegistry; +import org.apache.felix.http.base.internal.runtime.AbstractInfo; +import org.apache.felix.http.base.internal.runtime.FilterInfo; +import org.apache.felix.http.base.internal.runtime.ListenerInfo; +import org.apache.felix.http.base.internal.runtime.PreprocessorInfo; +import org.apache.felix.http.base.internal.runtime.ResourceInfo; +import org.apache.felix.http.base.internal.runtime.ServletContextHelperInfo; +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.osgi.service.http.runtime.dto.FailedErrorPageDTO; +import org.osgi.service.http.runtime.dto.FailedFilterDTO; +import org.osgi.service.http.runtime.dto.FailedListenerDTO; +import org.osgi.service.http.runtime.dto.FailedPreprocessorDTO; +import org.osgi.service.http.runtime.dto.FailedResourceDTO; +import org.osgi.service.http.runtime.dto.FailedServletContextDTO; +import org.osgi.service.http.runtime.dto.FailedServletDTO; + +public final class FailedDTOHolder +{ + + public final List failedFilterDTOs = new ArrayList(); + + public final List failedListenerDTOs = new ArrayList(); + + public final List failedServletDTOs = new ArrayList(); + + public final List failedResourceDTOs = new ArrayList(); + + public final List failedErrorPageDTOs = new ArrayList(); + + public final List failedServletContextDTOs = new ArrayList(); + + public final List failedPreprocessorDTOs = new ArrayList(); + + public void add(final AbstractInfo info, final long contextId, final int failureCode) + { + if (info instanceof ServletContextHelperInfo) + { + final FailedServletContextDTO dto = (FailedServletContextDTO)ServletContextDTOBuilder.build((ServletContextHelperInfo)info, null, failureCode); + this.failedServletContextDTOs.add(dto); + } + else if (info instanceof ServletInfo ) + { + boolean isError = false; + if ( ((ServletInfo) info).getErrorPage() != null) + { + isError = true; + final FailedErrorPageDTO dto = (FailedErrorPageDTO)ErrorPageDTOBuilder.build((ServletInfo)info, true); + dto.failureReason = failureCode; + final ErrorPageRegistry.ErrorRegistration reg = ErrorPageRegistry.getErrorRegistration((ServletInfo)info); + dto.errorCodes = reg.errorCodes; + dto.exceptions = reg.exceptions; + dto.servletContextId = contextId; + this.failedErrorPageDTOs.add(dto); + } + + if ( ((ServletInfo) info).getPatterns() != null || ((ServletInfo)info).getName() != null || !isError ) + { + final FailedServletDTO dto = (FailedServletDTO)ServletDTOBuilder.build((ServletInfo) info, failureCode); + if ( ((ServletInfo) info).getPatterns() != null ) + { + dto.patterns = ((ServletInfo) info).getPatterns(); + } + else + { + dto.patterns = BuilderConstants.EMPTY_STRING_ARRAY; + } + dto.name = ((ServletInfo)info).getName(); + dto.servletContextId = contextId; + this.failedServletDTOs.add(dto); + } + } + else if (info instanceof FilterInfo) + { + final FailedFilterDTO dto = (FailedFilterDTO)FilterDTOBuilder.build((FilterInfo) info, failureCode); + dto.failureReason = failureCode; + + dto.servletContextId = contextId; + this.failedFilterDTOs.add(dto); + } + else if (info instanceof ResourceInfo) + { + final FailedResourceDTO dto = (FailedResourceDTO)ResourceDTOBuilder.build((ResourceInfo) info, true); + dto.failureReason = failureCode; + dto.servletContextId = contextId; + this.failedResourceDTOs.add(dto); + } + else if (info instanceof ListenerInfo) + { + final FailedListenerDTO dto = (FailedListenerDTO)ListenerDTOBuilder.build((ListenerInfo)info, failureCode); + dto.servletContextId = contextId; + this.failedListenerDTOs.add(dto); + } + else if ( info instanceof PreprocessorInfo ) + { + final FailedPreprocessorDTO dto = (FailedPreprocessorDTO)PreprocessorDTOBuilder.build((PreprocessorInfo) info, failureCode); + this.failedPreprocessorDTOs.add(dto); + } + else + { + SystemLogger.error("Unsupported info type: " + info.getClass(), null); + } + } +} + diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/FilterDTOBuilder.java b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/FilterDTOBuilder.java new file mode 100644 index 00000000000..7811deb74e7 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/FilterDTOBuilder.java @@ -0,0 +1,101 @@ +/* + * 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.http.base.internal.runtime.dto; + +import org.jetbrains.annotations.NotNull; +import javax.servlet.DispatcherType; + +import org.apache.felix.http.base.internal.handler.FilterHandler; +import org.apache.felix.http.base.internal.runtime.FilterInfo; +import org.osgi.service.http.runtime.dto.FailedFilterDTO; +import org.osgi.service.http.runtime.dto.FilterDTO; + +public final class FilterDTOBuilder +{ + /** + * Build an array of filter DTOs from a filter handler array + * @param handlers The filter handler array + * @return The filter DTO array + */ + public static @NotNull FilterDTO[] build(@NotNull final FilterHandler[] handlers) + { + if ( handlers.length == 0 ) + { + return BuilderConstants.EMPTY_FILTER_DTO_ARRAY; + } + final FilterDTO[] array = new FilterDTO[handlers.length]; + for(int i=0; i contextDTOs; + private final FailedDTOHolder failedDTOHolder; + private final Collection preprocessorDTOs; + + public RegistryRuntime(final FailedDTOHolder failedDTOHolder, + final Collection contextDTOs, + final Collection preprocessorDTOs) + { + this.failedDTOHolder = failedDTOHolder; + this.contextDTOs = contextDTOs; + this.preprocessorDTOs = preprocessorDTOs; + } + + public FailedDTOHolder getFailedDTOHolder() + { + return this.failedDTOHolder; + } + + public Collection getServletContextDTOs() + { + return this.contextDTOs; + } + + public Collection getPreprocessorDTOs() + { + return this.preprocessorDTOs; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/RequestInfoDTOBuilder.java b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/RequestInfoDTOBuilder.java new file mode 100644 index 00000000000..9c15e8995d2 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/RequestInfoDTOBuilder.java @@ -0,0 +1,69 @@ +/* + * 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.http.base.internal.runtime.dto; + +import javax.servlet.DispatcherType; + +import org.apache.felix.http.base.internal.handler.FilterHandler; +import org.apache.felix.http.base.internal.registry.HandlerRegistry; +import org.apache.felix.http.base.internal.registry.PathResolution; +import org.osgi.service.http.runtime.dto.FilterDTO; +import org.osgi.service.http.runtime.dto.RequestInfoDTO; + +public final class RequestInfoDTOBuilder +{ + private static final FilterDTO[] FILTER_DTO_ARRAY = new FilterDTO[0]; + + private final HandlerRegistry registry; + private final String path; + + public RequestInfoDTOBuilder(final HandlerRegistry registry, final String path) + { + this.registry = registry; + this.path = path; + } + + public RequestInfoDTO build() + { + final RequestInfoDTO requestInfoDTO = new RequestInfoDTO(); + requestInfoDTO.path = path; + + final PathResolution pr = registry.resolveServlet(path); + if ( pr == null ) + { + // no servlet found, return empty DTO + requestInfoDTO.filterDTOs = FILTER_DTO_ARRAY; + return requestInfoDTO; + } + requestInfoDTO.servletContextId = pr.handler.getContextServiceId(); + if (pr.handler.getServletInfo().isResource()) + { + requestInfoDTO.resourceDTO = ResourceDTOBuilder.build(pr.handler, -1); + requestInfoDTO.resourceDTO.patterns = pr.patterns; + } + else + { + requestInfoDTO.servletDTO = ServletDTOBuilder.build(pr.handler, -1); + requestInfoDTO.servletDTO.patterns = pr.patterns; + } + + final FilterHandler[] filterHandlers = registry.getFilters(pr, DispatcherType.REQUEST, path); + requestInfoDTO.filterDTOs = FilterDTOBuilder.build(filterHandlers); + + return requestInfoDTO; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/ResourceDTOBuilder.java b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/ResourceDTOBuilder.java new file mode 100644 index 00000000000..5f53cc9836b --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/ResourceDTOBuilder.java @@ -0,0 +1,80 @@ +/* + * 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.http.base.internal.runtime.dto; + +import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.runtime.ResourceInfo; +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.osgi.service.http.runtime.dto.FailedResourceDTO; +import org.osgi.service.http.runtime.dto.ResourceDTO; + +public final class ResourceDTOBuilder +{ + /** + * Build a servlet DTO from a servlet handler + * @param handler The servlet handler + * @param reason If reason is -1, a servlet DTO is created, otherwise a failed servlet DTO is returned + * @return A servlet DTO + */ + public static ResourceDTO build(final ServletHandler handler, final int reason) + { + final ResourceDTO dto = build(handler.getServletInfo(), reason != -1); + + dto.servletContextId = handler.getContextServiceId(); + + if ( reason != -1 ) + { + ((FailedResourceDTO)dto).failureReason = reason; + } + + return dto; + } + + /** + * Build a servlet DTO from a servlet info + * @param info The servlet info + * @return A servlet DTO + */ + public static ResourceDTO build(final ServletInfo info, final boolean failed) + { + final ResourceDTO dto = (failed ? new FailedResourceDTO() : new ResourceDTO()); + + dto.patterns = BuilderConstants.EMPTY_STRING_ARRAY; + dto.prefix = info.getPrefix(); + dto.serviceId = info.getServiceId(); + + return dto; + } + + /** + * Build a servlet DTO from a servlet info + * @param info The servlet info + * @return A servlet DTO + */ + public static ResourceDTO build(final ResourceInfo info, final boolean failed) + { + final ResourceDTO dto = (failed ? new FailedResourceDTO() : new ResourceDTO()); + + dto.patterns = BuilderConstants.copyWithDefault(info.getPatterns(), BuilderConstants.EMPTY_STRING_ARRAY); + dto.prefix = info.getPrefix(); + dto.serviceId = info.getServiceId(); + + return dto; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/RuntimeDTOBuilder.java b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/RuntimeDTOBuilder.java new file mode 100644 index 00000000000..5a14735e0f4 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/RuntimeDTOBuilder.java @@ -0,0 +1,82 @@ +/* + * 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.http.base.internal.runtime.dto; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.dto.ServiceReferenceDTO; +import org.osgi.service.http.runtime.HttpServiceRuntime; +import org.osgi.service.http.runtime.dto.FailedErrorPageDTO; +import org.osgi.service.http.runtime.dto.FailedFilterDTO; +import org.osgi.service.http.runtime.dto.FailedListenerDTO; +import org.osgi.service.http.runtime.dto.FailedPreprocessorDTO; +import org.osgi.service.http.runtime.dto.FailedResourceDTO; +import org.osgi.service.http.runtime.dto.FailedServletContextDTO; +import org.osgi.service.http.runtime.dto.FailedServletDTO; +import org.osgi.service.http.runtime.dto.PreprocessorDTO; +import org.osgi.service.http.runtime.dto.RuntimeDTO; +import org.osgi.service.http.runtime.dto.ServletContextDTO; + +public final class RuntimeDTOBuilder +{ + + private final RegistryRuntime registry; + private final ServiceReferenceDTO serviceRefDTO; + + public RuntimeDTOBuilder(final RegistryRuntime registry, final ServiceReferenceDTO srDTO) + { + this.registry = registry; + this.serviceRefDTO = srDTO; + } + + public RuntimeDTO build() + { + final RuntimeDTO runtimeDTO = new RuntimeDTO(); + runtimeDTO.serviceDTO = this.serviceRefDTO; + runtimeDTO.servletContextDTOs = createContextDTOs(); + runtimeDTO.preprocessorDTOs = createPreprocessorDTOs(); + + runtimeDTO.failedErrorPageDTOs = registry.getFailedDTOHolder().failedErrorPageDTOs.toArray(new FailedErrorPageDTO[registry.getFailedDTOHolder().failedErrorPageDTOs.size()]); + runtimeDTO.failedFilterDTOs = registry.getFailedDTOHolder().failedFilterDTOs.toArray(new FailedFilterDTO[registry.getFailedDTOHolder().failedFilterDTOs.size()]); + runtimeDTO.failedListenerDTOs = registry.getFailedDTOHolder().failedListenerDTOs.toArray(new FailedListenerDTO[registry.getFailedDTOHolder().failedListenerDTOs.size()]); + runtimeDTO.failedResourceDTOs = registry.getFailedDTOHolder().failedResourceDTOs.toArray(new FailedResourceDTO[registry.getFailedDTOHolder().failedResourceDTOs.size()]); + runtimeDTO.failedServletContextDTOs = registry.getFailedDTOHolder().failedServletContextDTOs.toArray(new FailedServletContextDTO[registry.getFailedDTOHolder().failedServletContextDTOs.size()]); + runtimeDTO.failedServletDTOs = registry.getFailedDTOHolder().failedServletDTOs.toArray(new FailedServletDTO[registry.getFailedDTOHolder().failedServletDTOs.size()]); + runtimeDTO.failedPreprocessorDTOs = registry.getFailedDTOHolder().failedPreprocessorDTOs.toArray(new FailedPreprocessorDTO[registry.getFailedDTOHolder().failedPreprocessorDTOs.size()]); + + return runtimeDTO; + } + + private ServletContextDTO[] createContextDTOs() + { + final Collection contexts = registry.getServletContextDTOs(); + return contexts.toArray(new ServletContextDTO[contexts.size()]); + } + + private PreprocessorDTO[] createPreprocessorDTOs() + { + final Collection dtos = registry.getPreprocessorDTOs(); + return dtos.toArray(new PreprocessorDTO[dtos.size()]); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/ServletContextDTOBuilder.java b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/ServletContextDTOBuilder.java new file mode 100644 index 00000000000..081556338cd --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/ServletContextDTOBuilder.java @@ -0,0 +1,98 @@ +/* + * 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.http.base.internal.runtime.dto; + +import static java.util.Collections.list; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletContext; + +import org.apache.felix.http.base.internal.runtime.ServletContextHelperInfo; +import org.osgi.dto.DTO; +import org.osgi.service.http.runtime.dto.FailedServletContextDTO; +import org.osgi.service.http.runtime.dto.ServletContextDTO; + +public final class ServletContextDTOBuilder +{ + + public static ServletContextDTO build(final ServletContextHelperInfo info, final ServletContext context, final int reason) + { + final ServletContextDTO dto = (reason == -1 ? new ServletContextDTO() : new FailedServletContextDTO()); + + dto.attributes = getAttributes(context); + dto.contextPath = context != null ? context.getContextPath() : info.getPath(); + dto.initParams = info.getInitParameters(); + dto.name = info.getName(); + dto.serviceId = info.getServiceId(); + + dto.errorPageDTOs = BuilderConstants.ERROR_PAGE_DTO_ARRAY; + dto.filterDTOs = BuilderConstants.FILTER_FAILURE_DTO_ARRAY; + dto.listenerDTOs = BuilderConstants.LISTENER_DTO_ARRAY; + dto.resourceDTOs = BuilderConstants.RESOURCE_DTO_ARRAY; + dto.servletDTOs = BuilderConstants.SERVLET_DTO_ARRAY; + + if ( reason != -1 ) + { + ((FailedServletContextDTO)dto).failureReason = reason; + } + return dto; + } + + private static Map getAttributes(final ServletContext context) + { + if (context == null) + { + return Collections.emptyMap(); + } + + Map attributes = new HashMap(); + for (String name : list(context.getAttributeNames())) + { + Object attribute = context.getAttribute(name); + if (isSupportedType(attribute)) + { + attributes.put(name, attribute); + } + } + return attributes; + } + + private static boolean isSupportedType(Object attribute) + { + Class attributeClass = attribute.getClass(); + Class type = !attributeClass.isArray() ? + attributeClass : attributeClass.getComponentType(); + + return Number.class.isAssignableFrom(type) || + Boolean.class.isAssignableFrom(type) || + String.class.isAssignableFrom(type) || + DTO.class.isAssignableFrom(type) || + boolean.class.isAssignableFrom(type) || + int.class.isAssignableFrom(type) || + double.class.isAssignableFrom(type) || + float.class.isAssignableFrom(type) || + long.class.isAssignableFrom(type) || + short.class.isAssignableFrom(type) || + byte.class.isAssignableFrom(type) || + char.class.isAssignableFrom(type); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/ServletDTOBuilder.java b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/ServletDTOBuilder.java new file mode 100644 index 00000000000..b9575d491a9 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/runtime/dto/ServletDTOBuilder.java @@ -0,0 +1,72 @@ +/* + * 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.http.base.internal.runtime.dto; + +import org.apache.felix.http.base.internal.dispatch.MultipartConfig; +import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.osgi.service.http.runtime.dto.FailedServletDTO; +import org.osgi.service.http.runtime.dto.ServletDTO; + +public final class ServletDTOBuilder extends BaseServletDTOBuilder +{ + /** + * Build a servlet DTO from a servlet handler + * @param handler The servlet handler + * @param reason If reason is -1, a servlet DTO is created, otherwise a failed servlet DTO is returned + * @return A servlet DTO + */ + public static ServletDTO build(final ServletHandler handler, final int reason) + { + final ServletDTO dto = build(handler.getServletInfo(), reason); + + BaseServletDTOBuilder.fill(dto, handler); + + return dto; + } + + /** + * Build a servlet DTO from a servlet info + * @param info The servlet info + * @return A servlet DTO + */ + public static ServletDTO build(final ServletInfo info, final int reason) + { + final ServletDTO dto = (reason != -1 ? new FailedServletDTO() : new ServletDTO()); + + BaseServletDTOBuilder.fill(dto, info); + + if ( reason != -1 ) + { + ((FailedServletDTO)dto).failureReason = reason; + } + + dto.patterns = BuilderConstants.EMPTY_STRING_ARRAY; + final MultipartConfig config = info.getMultipartConfig(); + dto.multipartEnabled = config != null; + if ( config != null ) + { + dto.multipartFileSizeThreshold = config.multipartThreshold; + dto.multipartLocation = config.multipartLocation; + dto.multipartMaxFileSize = config.multipartMaxFileSize; + dto.multipartMaxRequestSize = config.multipartMaxRequestSize; + } + return dto; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/service/DefaultHttpContext.java b/http/base/src/main/java/org/apache/felix/http/base/internal/service/DefaultHttpContext.java new file mode 100644 index 00000000000..22a048d6191 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/service/DefaultHttpContext.java @@ -0,0 +1,53 @@ +/* + * 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.http.base.internal.service; + +import org.osgi.service.http.HttpContext; +import org.osgi.framework.Bundle; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.net.URL; + +public final class DefaultHttpContext + implements HttpContext +{ + private Bundle bundle; + + public DefaultHttpContext(Bundle bundle) + { + this.bundle = bundle; + } + + public String getMimeType(String name) + { + return null; + } + + public URL getResource(String name) + { + if (name.startsWith("/")) { + name = name.substring(1); + } + + return this.bundle.getResource(name); + } + + public boolean handleSecurity(HttpServletRequest req, HttpServletResponse res) + { + return true; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/service/HttpServiceFactory.java b/http/base/src/main/java/org/apache/felix/http/base/internal/service/HttpServiceFactory.java new file mode 100644 index 00000000000..87f5ee03a88 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/service/HttpServiceFactory.java @@ -0,0 +1,164 @@ +/* + * 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.http.base.internal.service; + +import java.util.Hashtable; + +import org.jetbrains.annotations.NotNull; +import javax.servlet.ServletContext; + +import org.apache.felix.http.base.internal.registry.HandlerRegistry; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.http.HttpService; +import org.osgi.service.http.runtime.HttpServiceRuntimeConstants; + +public final class HttpServiceFactory + implements ServiceFactory +{ + public static final String HTTP_SERVICE_CONTEXT_NAME = "org.osgi.service.http"; + + public static final long HTTP_SERVICE_CONTEXT_SERVICE_ID = -1; + + + /** + * Name of the Framework property indicating whether the servlet context + * attributes of the ServletContext objects created for each HttpContext + * used to register servlets and resources share their attributes or not. + * By default (if this property is not specified or it's value is not + * true (case-insensitive)) servlet context attributes are not + * shared. To have servlet context attributes shared amongst servlet context + * and also with the ServletContext provided by the servlet container ensure + * setting the property as follows: + *
      +     * org.apache.felix.http.shared_servlet_context_attributes = true
      +     * 
      + *

      + * WARNING: Only set this property if absolutely needed (for example + * you implement an HttpSessionListener and want to access servlet context + * attributes of the ServletContext to which the HttpSession is linked). + * Otherwise leave this property unset. + */ + private static final String FELIX_HTTP_SHARED_SERVLET_CONTEXT_ATTRIBUTES = "org.apache.felix.http.shared_servlet_context_attributes"; + + /** Compatibility property with previous versions. */ + private static final String OBSOLETE_REG_PROPERTY_ENDPOINTS = "osgi.http.service.endpoints"; + + private volatile boolean active = false; + private final BundleContext bundleContext; + private final boolean sharedContextAttributes; + + private final Hashtable httpServiceProps = new Hashtable<>(); + private volatile ServletContext context; + private volatile ServiceRegistration httpServiceReg; + + private final HandlerRegistry handlerRegistry; + private volatile SharedHttpServiceImpl sharedHttpService; + + + public HttpServiceFactory(final BundleContext bundleContext, + final HandlerRegistry handlerRegistry) + { + this.bundleContext = bundleContext; + this.handlerRegistry = handlerRegistry; + this.sharedContextAttributes = getBoolean(FELIX_HTTP_SHARED_SERVLET_CONTEXT_ATTRIBUTES); + + } + + public void start(final ServletContext context, + @NotNull final Hashtable props) + { + this.httpServiceProps.clear(); + this.httpServiceProps.putAll(props); + + if ( this.httpServiceProps.get(HttpServiceRuntimeConstants.HTTP_SERVICE_ENDPOINT) != null ) + { + this.httpServiceProps.put(OBSOLETE_REG_PROPERTY_ENDPOINTS, + this.httpServiceProps.get(HttpServiceRuntimeConstants.HTTP_SERVICE_ENDPOINT)); + } + + this.context = context; + + this.sharedHttpService = new SharedHttpServiceImpl(handlerRegistry); + + this.active = true; + this.httpServiceReg = bundleContext.registerService(HttpService.class, this, this.httpServiceProps); + } + + public void stop() + { + this.active = false; + + if ( this.httpServiceReg != null ) + { + this.httpServiceReg.unregister(); + this.httpServiceReg = null; + } + + this.context = null; + this.sharedHttpService = null; + + this.httpServiceProps.clear(); + } + + @Override + public HttpService getService(final Bundle bundle, final ServiceRegistration reg) + { + // Only return a service after start() has been called. + if (active) { + // Store everything that we want to pass to the PerBundleHttpServiceImpl in local vars to avoid + // a race condition where the service might be stopped while this method is executing. + final SharedHttpServiceImpl sharedHttpSvc = this.sharedHttpService; + final ServletContext servletCtx = this.context; + + final boolean sharedCtxAttrs = this.sharedContextAttributes; + + if ( active ) { + // Only return the service if we're still active + return new PerBundleHttpServiceImpl(bundle, + sharedHttpSvc, + servletCtx, + sharedCtxAttrs); + } + } + return null; + } + + @Override + public void ungetService(final Bundle bundle, final ServiceRegistration reg, + final HttpService service) + { + if ( service instanceof PerBundleHttpServiceImpl ) + { + ((PerBundleHttpServiceImpl)service).unregisterAll(); + } + } + + public long getHttpServiceServiceId() + { + return (Long) this.httpServiceReg.getReference().getProperty(Constants.SERVICE_ID); + } + + private boolean getBoolean(final String property) + { + String prop = this.bundleContext.getProperty(property); + return (prop != null) ? Boolean.valueOf(prop).booleanValue() : false; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/service/HttpServiceRuntimeImpl.java b/http/base/src/main/java/org/apache/felix/http/base/internal/service/HttpServiceRuntimeImpl.java new file mode 100644 index 00000000000..9b81137535f --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/service/HttpServiceRuntimeImpl.java @@ -0,0 +1,242 @@ +/* + * 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.http.base.internal.service; + +import static java.util.Collections.list; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Timer; +import java.util.TimerTask; + +import org.apache.felix.http.base.internal.registry.HandlerRegistry; +import org.apache.felix.http.base.internal.runtime.dto.RequestInfoDTOBuilder; +import org.apache.felix.http.base.internal.runtime.dto.RuntimeDTOBuilder; +import org.apache.felix.http.base.internal.whiteboard.WhiteboardManager; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.dto.ServiceReferenceDTO; +import org.osgi.service.http.runtime.HttpServiceRuntime; +import org.osgi.service.http.runtime.dto.RequestInfoDTO; +import org.osgi.service.http.runtime.dto.RuntimeDTO; + +public final class HttpServiceRuntimeImpl implements HttpServiceRuntime +{ + /** + * Service property for change count. This constant is defined here to avoid + * a dependency on R7 of the framework. + * The value of the property is of type {@code Long}. + */ + private static final String PROP_CHANGECOUNT = "service.changecount"; + + private static final String PROP_CHANGECOUNTDELAY = "org.apache.felix.http.whiteboard.changecount.delay"; + + private volatile Hashtable attributes = new Hashtable<>(); + + private final HandlerRegistry registry; + private final WhiteboardManager contextManager; + + private volatile long changeCount; + + private volatile ServiceRegistration serviceReg; + + private volatile ServiceReferenceDTO serviceRefDTO; + + private volatile Timer timer; + + private final long updateChangeCountDelay; + + public HttpServiceRuntimeImpl(HandlerRegistry registry, + WhiteboardManager contextManager, + BundleContext bundleContext) + { + this.registry = registry; + this.contextManager = contextManager; + final Object val = bundleContext.getProperty(PROP_CHANGECOUNTDELAY); + long value = 2000L; + if ( val != null ) + { + try + { + value = Long.parseLong(val.toString()); + } + catch ( final NumberFormatException nfe) + { + // ignore + } + if ( value < 1 ) + { + value = 0L; + } + } + updateChangeCountDelay = value; + } + + @Override + public RuntimeDTO getRuntimeDTO() + { + if ( this.serviceRefDTO == null ) + { + // it might happen that this code is executed in several threads + // but that's very unlikely and even if, fetching the service + // reference several times is not a big deal + final ServiceRegistration reg = this.serviceReg; + if ( reg != null ) + { + final long id = (long) reg.getReference().getProperty(Constants.SERVICE_ID); + final ServiceReferenceDTO[] dtos = reg.getReference().getBundle().adapt(ServiceReferenceDTO[].class); + for(final ServiceReferenceDTO dto : dtos) + { + if ( dto.id == id) + { + this.serviceRefDTO = dto; + break; + } + } + + } + } + if ( this.serviceRefDTO != null ) + { + final RuntimeDTOBuilder runtimeDTOBuilder = new RuntimeDTOBuilder(contextManager.getRuntimeInfo(), + this.serviceRefDTO); + return runtimeDTOBuilder.build(); + } + throw new IllegalStateException("Service is already unregistered"); + } + + @Override + public RequestInfoDTO calculateRequestInfoDTO(final String path) + { + return new RequestInfoDTOBuilder(registry, path).build(); + } + + public synchronized void setAttribute(String name, Object value) + { + Hashtable newAttributes = new Hashtable<>(attributes); + newAttributes.put(name, value); + attributes = newAttributes; + } + + public synchronized void setAllAttributes(Dictionary newAttributes) + { + Hashtable replacement = new Hashtable<>(); + for (String key : list(newAttributes.keys())) + { + replacement.put(key, newAttributes.get(key)); + } + replacement.put(PROP_CHANGECOUNT, this.changeCount); + attributes = replacement; + } + + public void register(final BundleContext bundleContext) + { + this.serviceReg = bundleContext.registerService(HttpServiceRuntime.class, + this, + attributes); + } + + public void unregister() + { + if ( this.serviceReg != null ) + { + try + { + this.serviceReg.unregister(); + } + catch ( final IllegalStateException ise) + { + // we just ignore it + } + this.serviceReg = null; + } + this.serviceRefDTO = null; + } + + public ServiceReference getServiceReference() + { + final ServiceRegistration reg = this.serviceReg; + if ( reg != null ) + { + return reg.getReference(); + } + return null; + } + + public void updateChangeCount() + { + final ServiceRegistration reg = this.serviceReg; + if ( reg != null ) + { + boolean setPropsDirectly = false; + synchronized ( this ) + { + this.changeCount++; + final long count = this.changeCount; + this.setAttribute(PROP_CHANGECOUNT, this.changeCount); + if ( this.updateChangeCountDelay <= 0L ) + { + setPropsDirectly = true; + } + else + { + if ( this.timer == null ) + { + this.timer = new Timer(); + } + timer.schedule(new TimerTask() + { + + @Override + public void run() + { + synchronized ( HttpServiceRuntimeImpl.this ) + { + if ( changeCount == count ) + { + try + { + reg.setProperties(attributes); + } + catch ( final IllegalStateException ise) + { + // we ignore this as this might happen on shutdown + } + timer.cancel(); + timer = null; + } + } + } + }, this.updateChangeCountDelay); + } + } + if ( setPropsDirectly ) + { + try + { + reg.setProperties(attributes); + } + catch ( final IllegalStateException ise) + { + // we ignore this as this might happen on shutdown + } + } + } + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/service/PerBundleHttpServiceImpl.java b/http/base/src/main/java/org/apache/felix/http/base/internal/service/PerBundleHttpServiceImpl.java new file mode 100644 index 00000000000..89fe92d8246 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/service/PerBundleHttpServiceImpl.java @@ -0,0 +1,228 @@ +/* + * 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.http.base.internal.service; + +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.servlet.Servlet; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.apache.felix.http.base.internal.util.PatternUtil; +import org.osgi.framework.Bundle; +import org.osgi.service.http.HttpContext; +import org.osgi.service.http.HttpService; +import org.osgi.service.http.NamespaceException; + +/** + * This implementation of the {@link HttpService} implements the front end + * used by client bundles. It performs the validity checks and passes the + * real operation to the shared http service. + */ +public final class PerBundleHttpServiceImpl implements HttpService +{ + private final Bundle bundle; + private final Set localServlets = new HashSet<>(); + private final ServletContextManager contextManager; + private final SharedHttpServiceImpl sharedHttpService; + + public PerBundleHttpServiceImpl(final Bundle bundle, + final SharedHttpServiceImpl sharedHttpService, + final ServletContext context, + final boolean sharedContextAttributes) + { + if (bundle == null) + { + throw new IllegalArgumentException("Bundle cannot be null!"); + } + if (context == null) + { + throw new IllegalArgumentException("Context cannot be null!"); + } + + this.bundle = bundle; + this.contextManager = new ServletContextManager(this.bundle, + context, + sharedContextAttributes, + sharedHttpService.getHandlerRegistry().getRegistry(HttpServiceFactory.HTTP_SERVICE_CONTEXT_SERVICE_ID)); + this.sharedHttpService = sharedHttpService; + } + + @Override + public HttpContext createDefaultHttpContext() + { + return new DefaultHttpContext(this.bundle); + } + + /** + * No need to sync this method, syncing is done via {@link #registerServlet(String, Servlet, Dictionary, HttpContext)} + * @see org.osgi.service.http.HttpService#registerResources(java.lang.String, java.lang.String, org.osgi.service.http.HttpContext) + */ + @Override + public void registerResources(final String alias, final String name, final HttpContext context) throws NamespaceException + { + if (!isNameValid(name)) + { + throw new IllegalArgumentException("Malformed resource name [" + name + "]"); + } + + if (!PatternUtil.isValidPattern(alias) || !alias.startsWith("/") ) + { + throw new IllegalArgumentException("Malformed resource alias [" + alias + "]"); + } + try + { + final Servlet servlet = new ResourceServlet(name); + registerServlet(alias, servlet, null, context); + } + catch (ServletException e) + { + SystemLogger.error("Failed to register resources", e); + } + } + + /** + * @see org.osgi.service.http.HttpService#registerServlet(java.lang.String, javax.servlet.Servlet, java.util.Dictionary, org.osgi.service.http.HttpContext) + */ + @Override + public void registerServlet(String alias, Servlet servlet, Dictionary initParams, HttpContext context) throws ServletException, NamespaceException + { + if (servlet == null) + { + throw new IllegalArgumentException("Servlet must not be null"); + } + if (!PatternUtil.isValidPattern(alias) || !alias.startsWith("/") ) + { + throw new IllegalArgumentException("Malformed servlet alias [" + alias + "]"); + } + + final Map paramMap = new HashMap<>(); + if (initParams != null && initParams.size() > 0) + { + Enumeration e = initParams.keys(); + while (e.hasMoreElements()) + { + Object key = e.nextElement(); + Object value = initParams.get(key); + + if ((key instanceof String) && (value instanceof String)) + { + paramMap.put((String) key, (String) value); + } + } + } + + synchronized (this.localServlets) + { + if (this.localServlets.contains(servlet)) + { + throw new ServletException("Servlet instance " + servlet + " already registered"); + } + this.localServlets.add(servlet); + } + + final ServletInfo servletInfo = new ServletInfo(String.format("%s_%d", servlet.getClass(), this.hashCode()), alias, paramMap); + final ExtServletContext httpContext = getServletContext(context); + + boolean success = false; + try + { + this.sharedHttpService.registerServlet(alias, httpContext, servlet, servletInfo); + success = true; + } + finally + { + if ( !success ) + { + synchronized ( this.localServlets ) + { + this.localServlets.remove(servlet); + } + } + } + } + + /** + * @see org.osgi.service.http.HttpService#unregister(java.lang.String) + */ + @Override + public void unregister(final String alias) + { + final Servlet servlet = this.sharedHttpService.unregister(alias); + if ( servlet != null ) + { + synchronized ( this.localServlets ) + { + this.localServlets.remove(servlet); + } + } + } + + public void unregisterAll() + { + final Set servlets = new HashSet<>(this.localServlets); + for (final Servlet servlet : servlets) + { + unregisterServlet(servlet); + } + } + + private void unregisterServlet(final Servlet servlet) + { + if (servlet != null) + { + synchronized ( this.localServlets ) + { + this.localServlets.remove(servlet); + } + this.sharedHttpService.unregisterServlet(servlet); + } + } + + public ExtServletContext getServletContext(HttpContext context) + { + if (context == null) + { + context = createDefaultHttpContext(); + } + + return this.contextManager.getServletContext(context); + } + + private boolean isNameValid(final String name) + { + if (name == null) + { + return false; + } + + if (!name.equals("/") && name.endsWith("/")) + { + return false; + } + + return true; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/service/ResourceServlet.java b/http/base/src/main/java/org/apache/felix/http/base/internal/service/ResourceServlet.java new file mode 100644 index 00000000000..19f459d4784 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/service/ResourceServlet.java @@ -0,0 +1,193 @@ +/* + * 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.http.base.internal.service; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * The resource servlet + */ +public final class ResourceServlet extends HttpServlet +{ + private static final long serialVersionUID = 1L; + + /** The path of the resource registration. */ + private final String prefix; + + public ResourceServlet(final String prefix) + { + this.prefix = prefix; + } + + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse res) + throws ServletException, IOException + { + final String target = req.getPathInfo(); + final String resName = (target == null ? this.prefix : this.prefix + target); + + final URL url = getServletContext().getResource(resName); + + if (url == null) + { + res.sendError(HttpServletResponse.SC_NOT_FOUND); + } + else + { + handle(req, res, url, resName); + } + } + + private void handle(final HttpServletRequest req, + final HttpServletResponse res, final URL url, final String resName) + throws IOException + { + final String contentType = getServletContext().getMimeType(resName); + if (contentType != null) + { + res.setContentType(contentType); + } + + final long lastModified = getLastModified(url); + if (lastModified != 0) + { + res.setDateHeader("Last-Modified", lastModified); + } + + if (!resourceModified(lastModified, req.getDateHeader("If-Modified-Since"))) + { + res.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + } + else + { + copyResource(url, res); + } + } + + private long getLastModified(final URL url) + { + long lastModified = 0; + + try + { + final URLConnection conn = url.openConnection(); + lastModified = conn.getLastModified(); + } + catch (final Exception e) + { + // Do nothing + } + + if (lastModified == 0) + { + final String filepath = url.getPath(); + if (filepath != null) + { + final File f = new File(filepath); + if (f.exists()) + { + lastModified = f.lastModified(); + } + } + } + + return lastModified; + } + + private boolean resourceModified(long resTimestamp, long modSince) + { + modSince /= 1000; + resTimestamp /= 1000; + + return resTimestamp == 0 || modSince == -1 || resTimestamp > modSince; + } + + private void copyResource(final URL url, final HttpServletResponse res) throws IOException + { + URLConnection conn = null; + OutputStream os = null; + InputStream is = null; + + try + { + conn = url.openConnection(); + + is = conn.getInputStream(); + os = res.getOutputStream(); + // FELIX-3987 content length should be set *before* any streaming is done + // as headers should be written before the content is actually written... + int len = getContentLength(conn); + if (len >= 0) + { + res.setContentLength(len); + } + + byte[] buf = new byte[1024]; + int n; + + while ((n = is.read(buf, 0, buf.length)) >= 0) + { + os.write(buf, 0, n); + } + } + finally + { + if (is != null) + { + is.close(); + } + + if (os != null) + { + os.close(); + } + } + } + + private int getContentLength(final URLConnection conn) + { + int length = -1; + + length = conn.getContentLength(); + if (length < 0) + { + // Unknown, try whether it is a file, and if so, use the file + // API to get the length of the content... + String path = conn.getURL().getPath(); + if (path != null) + { + File f = new File(path); + // In case more than 2GB is streamed + if (f.length() < Integer.MAX_VALUE) + { + length = (int) f.length(); + } + } + } + return length; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/service/ServletContextImpl.java b/http/base/src/main/java/org/apache/felix/http/base/internal/service/ServletContextImpl.java new file mode 100644 index 00000000000..9e9b499efd8 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/service/ServletContextImpl.java @@ -0,0 +1,590 @@ +/* + * 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.http.base.internal.service; + +import static org.apache.felix.http.base.internal.util.UriUtils.decodePath; +import static org.apache.felix.http.base.internal.util.UriUtils.removeDotSegments; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Collections; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.servlet.Filter; +import javax.servlet.FilterRegistration; +import javax.servlet.RequestDispatcher; +import javax.servlet.Servlet; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextAttributeEvent; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; +import javax.servlet.ServletRequestAttributeListener; +import javax.servlet.ServletRequestListener; +import javax.servlet.SessionCookieConfig; +import javax.servlet.SessionTrackingMode; +import javax.servlet.descriptor.JspConfigDescriptor; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionListener; + +import org.apache.felix.http.base.internal.HttpConfig; +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.dispatch.RequestDispatcherImpl; +import org.apache.felix.http.base.internal.dispatch.RequestInfo; +import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.apache.felix.http.base.internal.registry.PathResolution; +import org.apache.felix.http.base.internal.registry.PerContextHandlerRegistry; +import org.apache.felix.http.base.internal.registry.ServletResolution; +import org.apache.felix.http.base.internal.util.MimeTypes; +import org.apache.felix.http.base.internal.util.UriUtils; +import org.osgi.framework.Bundle; +import org.osgi.service.http.HttpContext; + +@SuppressWarnings("deprecation") +public class ServletContextImpl implements ExtServletContext +{ + private final Bundle bundle; + private final ServletContext context; + private final HttpContext httpContext; + private final Map attributes; + private final PerContextHandlerRegistry handlerRegistry; + + public ServletContextImpl(final Bundle bundle, + final ServletContext context, + final HttpContext httpContext, + final boolean sharedAttributes, + final PerContextHandlerRegistry registry) + { + this.bundle = bundle; + this.context = context; + this.httpContext = httpContext; + this.attributes = sharedAttributes ? null : new ConcurrentHashMap(); + this.handlerRegistry = registry; + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, Class type) + { + throw new UnsupportedOperationException(); + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, Filter filter) + { + throw new UnsupportedOperationException(); + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, String className) + { + throw new UnsupportedOperationException(); + } + + @Override + public void addListener(Class type) + { + throw new UnsupportedOperationException(); + } + + @Override + public void addListener(String className) + { + throw new UnsupportedOperationException(); + } + + @Override + public void addListener(T listener) + { + throw new UnsupportedOperationException(); + } + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, Class type) + { + throw new UnsupportedOperationException(); + } + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) + { + throw new UnsupportedOperationException(); + } + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, String className) + { + throw new UnsupportedOperationException(); + } + + @Override + public T createFilter(Class type) throws ServletException + { + throw new UnsupportedOperationException(); + } + + @Override + public T createListener(Class type) throws ServletException + { + throw new UnsupportedOperationException(); + } + + @Override + public T createServlet(Class type) throws ServletException + { + throw new UnsupportedOperationException(); + } + + @Override + public void declareRoles(String... roleNames) + { + this.context.declareRoles(roleNames); + } + + @Override + public String getVirtualServerName() { + return context.getVirtualServerName(); + } + + @Override + public Object getAttribute(String name) + { + return (this.attributes != null) ? this.attributes.get(name) : this.context.getAttribute(name); + } + + @Override + public Enumeration getAttributeNames() + { + return (this.attributes != null) ? Collections.enumeration(this.attributes.keySet()) : this.context.getAttributeNames(); + } + + @Override + public ClassLoader getClassLoader() + { + return bundle.getClass().getClassLoader(); + } + + @Override + public ServletContext getContext(String uri) + { + return this.context.getContext(uri); + } + + @Override + public String getContextPath() + { + return this.context.getContextPath(); + } + + @Override + public Set getDefaultSessionTrackingModes() + { + return this.context.getDefaultSessionTrackingModes(); + } + + @Override + public int getEffectiveMajorVersion() + { + return this.context.getEffectiveMajorVersion(); + } + + @Override + public int getEffectiveMinorVersion() + { + return this.context.getEffectiveMinorVersion(); + } + + @Override + public Set getEffectiveSessionTrackingModes() + { + return this.context.getEffectiveSessionTrackingModes(); + } + + @Override + public FilterRegistration getFilterRegistration(String filterName) + { + return this.context.getFilterRegistration(filterName); + } + + @Override + public Map getFilterRegistrations() + { + return this.context.getFilterRegistrations(); + } + + @Override + public String getInitParameter(String name) + { + return this.context.getInitParameter(name); + } + + @Override + public Enumeration getInitParameterNames() + { + return this.context.getInitParameterNames(); + } + + @Override + public JspConfigDescriptor getJspConfigDescriptor() + { + throw new UnsupportedOperationException(); + } + + @Override + public int getMajorVersion() + { + return this.context.getMajorVersion(); + } + + @Override + public String getMimeType(String file) + { + String type = this.httpContext.getMimeType(file); + if (type != null) + { + return type; + } + + return MimeTypes.get().getByFile(file); + } + + @Override + public int getMinorVersion() + { + return this.context.getMinorVersion(); + } + + @Override + public String getRealPath(String name) + { + URL url = getResource(name); + if (url == null) + { + return null; + } + return url.toExternalForm(); + } + + @Override + public URL getResource(String path) + { + return this.httpContext.getResource(normalizeResourcePath(path)); + } + + @Override + public InputStream getResourceAsStream(String path) + { + URL res = getResource(path); + if (res != null) + { + try + { + return res.openStream(); + } + catch (IOException e) + { + // Do nothing + } + } + return null; + } + + @Override + public Set getResourcePaths(String path) + { + Enumeration paths = this.bundle.getEntryPaths(normalizePath(path)); + if ((paths == null) || !paths.hasMoreElements()) + { + return null; + } + + Set set = new HashSet<>(); + while (paths.hasMoreElements()) + { + set.add(paths.nextElement()); + } + + return set; + } + + @Override + public String getServerInfo() + { + return this.context.getServerInfo(); + } + + @Override + public Servlet getServlet(String name) throws ServletException + { + return this.context.getServlet(name); + } + + @Override + public String getServletContextName() + { + return HttpServiceFactory.HTTP_SERVICE_CONTEXT_NAME; + } + + @Override + public Enumeration getServletNames() + { + return this.context.getServletNames(); + } + + @Override + public ServletRegistration getServletRegistration(String servletName) + { + return this.context.getServletRegistration(servletName); + } + + @Override + public Map getServletRegistrations() + { + return this.context.getServletRegistrations(); + } + + @Override + public Enumeration getServlets() + { + return this.context.getServlets(); + } + + @Override + public SessionCookieConfig getSessionCookieConfig() + { + return this.context.getSessionCookieConfig(); + } + + @Override + public HttpSessionListener getHttpSessionListener() + { + return this.handlerRegistry.getEventListenerRegistry(); + } + + @Override + public HttpSessionAttributeListener getHttpSessionAttributeListener() + { + return this.handlerRegistry.getEventListenerRegistry(); + } + + @Override + public ServletRequestListener getServletRequestListener() + { + return this.handlerRegistry.getEventListenerRegistry(); + } + + @Override + public ServletRequestAttributeListener getServletRequestAttributeListener() + { + return this.handlerRegistry.getEventListenerRegistry(); + } + + @Override + public boolean handleSecurity(HttpServletRequest req, HttpServletResponse res) throws IOException + { + return this.httpContext.handleSecurity(req, res); + } + + @Override + public void finishSecurity(HttpServletRequest req, HttpServletResponse res) { + // nothing to do + } + + @Override + public void log(Exception cause, String message) + { + SystemLogger.error(message, cause); + } + + @Override + public void log(String message) + { + SystemLogger.info(message); + } + + @Override + public void log(String message, Throwable cause) + { + SystemLogger.error(message, cause); + } + + @Override + public void removeAttribute(String name) + { + Object oldValue; + if (this.attributes != null) + { + oldValue = this.attributes.remove(name); + } + else + { + oldValue = this.context.getAttribute(name); + this.context.removeAttribute(name); + } + + if (oldValue != null) + { + this.handlerRegistry.getEventListenerRegistry().attributeRemoved(new ServletContextAttributeEvent(this, name, oldValue)); + } + } + + @Override + public void setAttribute(String name, Object value) + { + if (value == null) + { + this.removeAttribute(name); + } + else if (name != null) + { + Object oldValue; + if (this.attributes != null) + { + oldValue = this.attributes.put(name, value); + } + else + { + oldValue = this.context.getAttribute(name); + this.context.setAttribute(name, value); + } + + if (oldValue == null) + { + this.handlerRegistry.getEventListenerRegistry().attributeAdded(new ServletContextAttributeEvent(this, name, value)); + } + else + { + this.handlerRegistry.getEventListenerRegistry().attributeReplaced(new ServletContextAttributeEvent(this, name, oldValue)); + } + } + } + + @Override + public boolean setInitParameter(String name, String value) + { + return this.context.setInitParameter(name, value); + } + + @Override + public void setSessionTrackingModes(Set modes) + { + this.context.setSessionTrackingModes(modes); + } + + @Override + public RequestDispatcher getNamedDispatcher(final String name) + { + if (name == null) + { + return null; + } + + final RequestDispatcher dispatcher; + final ServletHandler servletHandler = this.handlerRegistry.resolveServletByName(name); + if ( servletHandler != null ) + { + final ServletResolution resolution = new ServletResolution(); + resolution.handler = servletHandler; + resolution.handlerRegistry = this.handlerRegistry; + // TODO - what is the path of a named servlet? + final RequestInfo requestInfo = new RequestInfo("", null, null, null); + dispatcher = new RequestDispatcherImpl(resolution, requestInfo); + } + else + { + dispatcher = null; + } + return dispatcher; + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) + { + // See section 9.1 of Servlet 3.x specification... + if (path == null || (!path.startsWith("/") && !"".equals(path))) + { + return null; + } + + String query = null; + int q = 0; + if ((q = path.indexOf('?')) > 0) + { + query = path.substring(q + 1); + path = path.substring(0, q); + } + // TODO remove path parameters... + final String encodedRequestURI = path == null ? "" : removeDotSegments(path); + final String requestURI = decodePath(encodedRequestURI); + + final RequestDispatcher dispatcher; + final PathResolution pathResolution = this.handlerRegistry.resolve(requestURI); + if ( pathResolution != null ) + { + final ServletResolution resolution = new ServletResolution(); + resolution.handler = pathResolution.handler; + resolution.handlerRegistry = this.handlerRegistry; + final RequestInfo requestInfo = new RequestInfo(pathResolution.servletPath, pathResolution.pathInfo, query, UriUtils.concat(this.getContextPath(), encodedRequestURI)); + dispatcher = new RequestDispatcherImpl(resolution, requestInfo); + } + else + { + dispatcher = null; + } + return dispatcher; + } + + @Override + public HttpConfig getConfig() + { + return this.handlerRegistry.getConfig(); + } + + private String normalizePath(String path) + { + if (path == null) + { + return null; + } + + String normalizedPath = normalizeResourcePath(path); + if (normalizedPath.startsWith("/") && (normalizedPath.length() > 1)) + { + normalizedPath = normalizedPath.substring(1); + } + + return normalizedPath; + } + + private String normalizeResourcePath(String path) + { + if ( path == null) + { + return null; + } + String normalizedPath = path.trim().replaceAll("/+", "/"); + + return normalizedPath; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/service/ServletContextManager.java b/http/base/src/main/java/org/apache/felix/http/base/internal/service/ServletContextManager.java new file mode 100644 index 00000000000..c2bf61101b1 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/service/ServletContextManager.java @@ -0,0 +1,77 @@ +/* + * 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.http.base.internal.service; + +import java.util.Map; +import java.util.WeakHashMap; + +import javax.servlet.ServletContext; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.registry.PerContextHandlerRegistry; +import org.osgi.framework.Bundle; +import org.osgi.service.http.HttpContext; + +public final class ServletContextManager +{ + private final Bundle bundle; + private final ServletContext context; + private final Map contextMap; + private final boolean sharedAttributes; + private final PerContextHandlerRegistry handlerRegistry; + + public ServletContextManager( + final Bundle bundle, + final ServletContext context, + final boolean sharedAttributes, + final PerContextHandlerRegistry registry) + { + this.bundle = bundle; + this.context = context; + // FELIX-4424 : avoid classloader leakage through HttpContext, for now this is sufficient, + // the real fix should be to remove ExtServletContext's when the usage count of HttpContext + // drops to zero. + this.contextMap = new WeakHashMap(); + this.sharedAttributes = sharedAttributes; + this.handlerRegistry = registry; + } + + public ExtServletContext getServletContext(HttpContext httpContext) + { + ExtServletContext context; + synchronized (this.contextMap) + { + context = this.contextMap.get(httpContext); + if (context == null) + { + context = addServletContext(httpContext); + } + } + return context; + } + + private ExtServletContext addServletContext(HttpContext httpContext) + { + ExtServletContext context = new ServletContextImpl(this.bundle, + this.context, + httpContext, + this.sharedAttributes, + handlerRegistry); + this.contextMap.put(httpContext, context); + return context; + } +} \ No newline at end of file diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/service/SharedHttpServiceImpl.java b/http/base/src/main/java/org/apache/felix/http/base/internal/service/SharedHttpServiceImpl.java new file mode 100644 index 00000000000..5ac6dece0d3 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/service/SharedHttpServiceImpl.java @@ -0,0 +1,128 @@ +/* + * 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.http.base.internal.service; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.jetbrains.annotations.NotNull; +import javax.servlet.Servlet; +import javax.servlet.ServletException; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.handler.FilterHandler; +import org.apache.felix.http.base.internal.handler.HttpServiceServletHandler; +import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.registry.HandlerRegistry; +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.osgi.service.http.NamespaceException; + +public final class SharedHttpServiceImpl +{ + private final HandlerRegistry handlerRegistry; + + private final Map aliasMap = new HashMap<>(); + + public SharedHttpServiceImpl(final HandlerRegistry handlerRegistry) + { + if (handlerRegistry == null) + { + throw new IllegalArgumentException("HandlerRegistry cannot be null!"); + } + + this.handlerRegistry = handlerRegistry; + } + + /** + * Register a filter + */ + public boolean registerFilter(@NotNull final FilterHandler handler) + { + this.handlerRegistry.getRegistry(handler.getContextServiceId()).registerFilter(handler); + return true; + } + + /** + * Register a servlet + */ + public void registerServlet(@NotNull final String alias, + @NotNull final ExtServletContext httpContext, + @NotNull final Servlet servlet, + @NotNull final ServletInfo servletInfo) throws ServletException, NamespaceException + { + final ServletHandler handler = new HttpServiceServletHandler(httpContext, servletInfo, servlet); + + synchronized (this.aliasMap) + { + if (this.aliasMap.containsKey(alias)) + { + throw new NamespaceException("Alias " + alias + " is already in use."); + } + this.handlerRegistry.getRegistry(handler.getContextServiceId()).registerServlet(handler); + + this.aliasMap.put(alias, handler); + } + } + + /** + * @see org.osgi.service.http.HttpService#unregister(java.lang.String) + */ + public Servlet unregister(final String alias) + { + synchronized (this.aliasMap) + { + final ServletHandler handler = this.aliasMap.remove(alias); + if (handler == null) + { + throw new IllegalArgumentException("Nothing registered at " + alias); + } + + final Servlet s = handler.getServlet(); + this.handlerRegistry.getRegistry(handler.getContextServiceId()).unregisterServlet(handler.getServletInfo(), true); + return s; + } + } + + public void unregisterServlet(final Servlet servlet) + { + if (servlet != null) + { + synchronized (this.aliasMap) + { + final Iterator> i = this.aliasMap.entrySet().iterator(); + while (i.hasNext()) + { + final Map.Entry entry = i.next(); + if (entry.getValue().getServlet() == servlet) + { + this.handlerRegistry.getRegistry(entry.getValue().getContextServiceId()).unregisterServlet(entry.getValue().getServletInfo(), false); + + i.remove(); + break; + } + + } + } + } + } + + public HandlerRegistry getHandlerRegistry() + { + return this.handlerRegistry; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/util/InternalIdFactory.java b/http/base/src/main/java/org/apache/felix/http/base/internal/util/InternalIdFactory.java new file mode 100644 index 00000000000..0ab73ebbf3f --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/util/InternalIdFactory.java @@ -0,0 +1,39 @@ +/* + * 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.http.base.internal.util; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Provides service ids for services not provided through the service registry. + *

      + * All provided ids are unique negative {@code long}s. + */ +public enum InternalIdFactory +{ + INSTANCE; + + /** -1 is reserved for the http service servlet context. */ + private final AtomicLong idCounter = new AtomicLong(-1); + + public long next() + { + return idCounter.decrementAndGet(); + } +} \ No newline at end of file diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/util/MimeTypes.java b/http/base/src/main/java/org/apache/felix/http/base/internal/util/MimeTypes.java new file mode 100644 index 00000000000..3833fdaa563 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/util/MimeTypes.java @@ -0,0 +1,211 @@ +/* + * 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.http.base.internal.util; + +import java.util.Map; +import java.util.HashMap; + +public final class MimeTypes +{ + private final static MimeTypes INSTANCE = + new MimeTypes(); + + private final Map extMap; + + private MimeTypes() + { + this.extMap = new HashMap(); + this.extMap.put("abs", "audio/x-mpeg"); + this.extMap.put("ai", "application/postscript"); + this.extMap.put("aif", "audio/x-aiff"); + this.extMap.put("aifc", "audio/x-aiff"); + this.extMap.put("aiff", "audio/x-aiff"); + this.extMap.put("aim", "application/x-aim"); + this.extMap.put("art", "image/x-jg"); + this.extMap.put("asf", "video/x-ms-asf"); + this.extMap.put("asx", "video/x-ms-asf"); + this.extMap.put("au", "audio/basic"); + this.extMap.put("avi", "video/x-msvideo"); + this.extMap.put("avx", "video/x-rad-screenplay"); + this.extMap.put("bcpio", "application/x-bcpio"); + this.extMap.put("bin", "application/octet-stream"); + this.extMap.put("bmp", "image/bmp"); + this.extMap.put("body", "text/html"); + this.extMap.put("cdf", "application/x-cdf"); + this.extMap.put("cer", "application/x-x509-ca-cert"); + this.extMap.put("class", "application/java"); + this.extMap.put("cpio", "application/x-cpio"); + this.extMap.put("csh", "application/x-csh"); + this.extMap.put("css", "text/css"); + this.extMap.put("dib", "image/bmp"); + this.extMap.put("doc", "application/msword"); + this.extMap.put("dtd", "application/xml-dtd"); + this.extMap.put("dv", "video/x-dv"); + this.extMap.put("dvi", "application/x-dvi"); + this.extMap.put("eps", "application/postscript"); + this.extMap.put("etx", "text/x-setext"); + this.extMap.put("exe", "application/octet-stream"); + this.extMap.put("gif", "image/gif"); + this.extMap.put("gk", "application/octet-stream"); + this.extMap.put("gtar", "application/x-gtar"); + this.extMap.put("gz", "application/x-gzip"); + this.extMap.put("hdf", "application/x-hdf"); + this.extMap.put("hqx", "application/mac-binhex40"); + this.extMap.put("htc", "text/x-component"); + this.extMap.put("htm", "text/html"); + this.extMap.put("html", "text/html"); + this.extMap.put("hqx", "application/mac-binhex40"); + this.extMap.put("ief", "image/ief"); + this.extMap.put("jad", "text/vnd.sun.j2me.app-descriptor"); + this.extMap.put("jar", "application/java-archive"); + this.extMap.put("java", "text/plain"); + this.extMap.put("jnlp", "application/x-java-jnlp-file"); + this.extMap.put("jpe", "image/jpeg"); + this.extMap.put("jpeg", "image/jpeg"); + this.extMap.put("jpg", "image/jpeg"); + this.extMap.put("js", "text/javascript"); + this.extMap.put("kar", "audio/x-midi"); + this.extMap.put("latex", "application/x-latex"); + this.extMap.put("m3u", "audio/x-mpegurl"); + this.extMap.put("mac", "image/x-macpaint"); + this.extMap.put("man", "application/x-troff-man"); + this.extMap.put("mathml", "application/mathml+xml"); + this.extMap.put("me", "application/x-troff-me"); + this.extMap.put("mid", "audio/x-midi"); + this.extMap.put("midi", "audio/x-midi"); + this.extMap.put("mif", "application/x-mif"); + this.extMap.put("mov", "video/quicktime"); + this.extMap.put("movie", "video/x-sgi-movie"); + this.extMap.put("mp1", "audio/x-mpeg"); + this.extMap.put("mp2", "audio/x-mpeg"); + this.extMap.put("mp3", "audio/x-mpeg"); + this.extMap.put("mpa", "audio/x-mpeg"); + this.extMap.put("mpe", "video/mpeg"); + this.extMap.put("mpeg", "video/mpeg"); + this.extMap.put("mpega", "audio/x-mpeg"); + this.extMap.put("mpg", "video/mpeg"); + this.extMap.put("mpv2", "video/mpeg2"); + this.extMap.put("ms", "application/x-wais-source"); + this.extMap.put("nc", "application/x-netcdf"); + this.extMap.put("oda", "application/oda"); + this.extMap.put("ogg", "application/ogg"); + this.extMap.put("pbm", "image/x-portable-bitmap"); + this.extMap.put("pct", "image/pict"); + this.extMap.put("pdf", "application/pdf"); + this.extMap.put("pgm", "image/x-portable-graymap"); + this.extMap.put("pic", "image/pict"); + this.extMap.put("pict", "image/pict"); + this.extMap.put("pls", "audio/x-scpls"); + this.extMap.put("png", "image/png"); + this.extMap.put("pnm", "image/x-portable-anymap"); + this.extMap.put("pnt", "image/x-macpaint"); + this.extMap.put("ppm", "image/x-portable-pixmap"); + this.extMap.put("ppt", "application/powerpoint"); + this.extMap.put("ps", "application/postscript"); + this.extMap.put("psd", "image/x-photoshop"); + this.extMap.put("qt", "video/quicktime"); + this.extMap.put("qti", "image/x-quicktime"); + this.extMap.put("qtif", "image/x-quicktime"); + this.extMap.put("ras", "image/x-cmu-raster"); + this.extMap.put("rdf", "application/rdf+xml"); + this.extMap.put("rgb", "image/x-rgb"); + this.extMap.put("rm", "application/vnd.rn-realmedia"); + this.extMap.put("roff", "application/x-troff"); + this.extMap.put("rtf", "application/rtf"); + this.extMap.put("rtx", "text/richtext"); + this.extMap.put("sh", "application/x-sh"); + this.extMap.put("shar", "application/x-shar"); + this.extMap.put("shtml", "text/x-server-parsed-html"); + this.extMap.put("sit", "application/x-stuffit"); + this.extMap.put("smf", "audio/x-midi"); + this.extMap.put("snd", "audio/basic"); + this.extMap.put("src", "application/x-wais-source"); + this.extMap.put("sv4cpio", "application/x-sv4cpio"); + this.extMap.put("sv4crc", "application/x-sv4crc"); + this.extMap.put("svg", "image/svg+xml"); + this.extMap.put("svgz", "image/svg+xml"); + this.extMap.put("swf", "application/x-shockwave-flash"); + this.extMap.put("t", "application/x-troff"); + this.extMap.put("tar", "application/x-tar"); + this.extMap.put("tcl", "application/x-tcl"); + this.extMap.put("tex", "application/x-tex"); + this.extMap.put("texi", "application/x-texinfo"); + this.extMap.put("texinfo", "application/x-texinfo"); + this.extMap.put("tif", "image/tiff"); + this.extMap.put("tiff", "image/tiff"); + this.extMap.put("tr", "application/x-troff"); + this.extMap.put("tsv", "text/tab-separated-values"); + this.extMap.put("txt", "text/plain"); + this.extMap.put("ulw", "audio/basic"); + this.extMap.put("ustar", "application/x-ustar"); + this.extMap.put("xbm", "image/x-xbitmap"); + this.extMap.put("xml", "text/xml"); + this.extMap.put("xpm", "image/x-xpixmap"); + this.extMap.put("xsl", "application/xml"); + this.extMap.put("xslt", "application/xslt+xml"); + this.extMap.put("xwd", "image/x-xwindowdump"); + this.extMap.put("vsd", "application/x-visio"); + this.extMap.put("vxml", "application/voicexml+xml"); + this.extMap.put("wav", "audio/x-wav"); + this.extMap.put("wbmp", "image/vnd.wap.wbmp"); + this.extMap.put("wml", "text/vnd.wap.wml"); + this.extMap.put("wmlc", "application/vnd.wap.wmlc"); + this.extMap.put("wmls", "text/vnd.wap.wmls"); + this.extMap.put("wmlscriptc", "application/vnd.wap.wmlscriptc"); + this.extMap.put("wrl", "x-world/x-vrml"); + this.extMap.put("xht", "application/xhtml+xml"); + this.extMap.put("xhtml", "application/xhtml+xml"); + this.extMap.put("xls", "application/vnd.ms-excel"); + this.extMap.put("xul", "application/vnd.mozilla.xul+xml"); + this.extMap.put("Z", "application/x-compress"); + this.extMap.put("z", "application/x-compress"); + this.extMap.put("zip", "application/zip"); + } + + public String getByFile(String file) + { + if (file == null) { + return null; + } + + int dot = file.lastIndexOf("."); + if (dot < 0) { + return null; + } + + String ext = file.substring(dot + 1); + if (ext.length() < 1) { + return null; + } + + return getByExtension(ext); + } + + public String getByExtension(String ext) + { + if (ext == null) { + return null; + } + + return this.extMap.get(ext); + } + + public static MimeTypes get() + { + return INSTANCE; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/util/PatternUtil.java b/http/base/src/main/java/org/apache/felix/http/base/internal/util/PatternUtil.java new file mode 100644 index 00000000000..d54f43159a9 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/util/PatternUtil.java @@ -0,0 +1,144 @@ +/* + * 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.http.base.internal.util; + +import java.util.StringTokenizer; + +/** + * Some convenience utilities to deal with path patterns. + */ +public abstract class PatternUtil +{ + + /** + * Check for valid servlet pattern + * @param pattern The pattern + * @return {@code true} if its valid + */ + public static boolean isValidPattern(final String pattern) + { + if ( pattern == null ) + { + return false; + } + if ( pattern.indexOf("?") != -1 ) + { + return false; + } + // default and root + if ( pattern.length() == 0 || pattern.equals("/") ) + { + return true; + } + // extension + if ( pattern.startsWith("*.") ) + { + return pattern.indexOf("/") == -1; + } + if ( !pattern.startsWith("/") ) + { + return false; + } + final int pos = pattern.indexOf('*'); + if ( pos != -1 && pos < pattern.length() - 1 ) + { + return false; + } + if ( pos != -1 && pattern.charAt(pos - 1) != '/') + { + return false; + } + if ( pattern.charAt(pattern.length() - 1) == '/') + { + return false; + } + return true; + } + + // check for valid symbolic name + public static boolean isValidSymbolicName(final String name) + { + if ( name == null || name.isEmpty() ) + { + return false; + } + boolean valid = true; + boolean expectToken = false; + boolean done = false; + final StringTokenizer st = new StringTokenizer(name, ".", true); + while ( !done && st.hasMoreTokens() ) + { + final String token = st.nextToken(); + if ( expectToken ) + { + if ( !".".equals(token) ) + { + valid = false; + done = true; + } + else + { + expectToken = false; + } + } + else + { + if ( ".".equals(token) ) + { + valid = false; + done = true; + } + else + { + int i = 0; + while ( i < token.length() && valid ) + { + final char c = token.charAt(i); + i++; + if ( c >= 'a' && c <= 'z' ) + { + continue; + } + if ( c >= 'A' && c <= 'Z' ) + { + continue; + } + if ( c >= '0' && c <= '9' ) + { + continue; + } + if ( c == '-' || c == '_' ) + { + continue; + } + valid = false; + done = true; + } + } + expectToken = true; + } + } + if ( !expectToken ) + { + valid = false; + } + + return valid; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/util/UriUtils.java b/http/base/src/main/java/org/apache/felix/http/base/internal/util/UriUtils.java new file mode 100644 index 00000000000..3c12958f1a1 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/util/UriUtils.java @@ -0,0 +1,368 @@ +/* + * 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.http.base.internal.util; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; + +/** + * Some convenience methods for handling URI(-parts). + */ +public abstract class UriUtils +{ + private static final String SLASH_STR = "/"; + private static final char DOT = '.'; + private static final char SLASH = '/'; + + /** + * Concatenates two paths keeping their respective path-parts into consideration. + * + * @param path1 the first part of the path, can be null; + * @param path2 the second part of the path, can be null. + * @return the concatenated path, can be null in case both given arguments were null. + */ + public static String concat(String path1, String path2) + { + // Handle special cases... + if (path1 == null && path2 == null) + { + return null; + } + if (path1 == null) + { + path1 = ""; + } + if (path2 == null) + { + path2 = ""; + } + if (isEmpty(path1) && isEmpty(path2)) + { + return ""; + } + + StringBuilder sb = new StringBuilder(); + + int idx = path1.indexOf('?'); + if (idx == 0) + { + // path1 only consists of a query, append it to the second path... + return path2.concat(path1); + } + else if (idx > 0) + { + // path1 contains of a path + query, append the path first... + sb.append(path1.substring(0, idx)); + } + else + { + // Plain paths... + sb.append(path1); + // need a slash? + } + + if (endsWith(sb, SLASH_STR)) + { + if (path2.startsWith(SLASH_STR)) + { + sb.append(path2.substring(1)); + } + else + { + sb.append(path2); + } + } + else + { + if (path2.startsWith(SLASH_STR)) + { + sb.append(path2); + } + else if (sb.length() > 0 && !isEmpty(path2)) + { + sb.append(SLASH_STR).append(path2); + } + else + { + sb.append(path2); + } + } + + if (idx > 0) + { + // Add the query of path1... + sb.append(path1.substring(idx, path1.length())); + } + + return sb.toString(); + } + + /** + * Decodes a given URL-encoded path assuming it is UTF-8 encoded. + * + * @param path the URL-encoded path, can be null. + * @return the decoded path, can be null only if the given path was null. + */ + public static String decodePath(String path) + { + return decodePath(path, "UTF-8"); + } + + /** + * Decodes a given URL-encoded path using a given character encoding. + * + * @param path the URL-encoded path, can be null; + * @param encoding the character encoding to use, cannot be null. + * @return the decoded path, can be null only if the given path was null. + */ + private static String decodePath(String path, String encoding) + { + // Special cases... + if (path == null) + { + return null; + } + + CharsetDecoder decoder = Charset.forName(encoding).newDecoder(); + decoder.onMalformedInput(CodingErrorAction.REPORT); + decoder.onUnmappableCharacter(CodingErrorAction.REPORT); + + int len = path.length(); + ByteBuffer buf = ByteBuffer.allocate(len); + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < len; i++) + { + char ch = path.charAt(i); + if (ch == '%' && (i + 2 < len)) + { + // URL-encoded char... + buf.put((byte) ((16 * hexVal(path, ++i)) + hexVal(path, ++i))); + } + else + { + if (buf.position() > 0) + { + // flush encoded chars first... + sb.append(decode(buf, decoder)); + buf.clear(); + } + + sb.append(ch); + } + } + + // flush trailing encoded characters... + if (buf.position() > 0) + { + sb.append(decode(buf, decoder)); + buf.clear(); + } + + return sb.toString(); + } + + /** + * Removes all superfluous dot-segments using the algorithm described in RFC-3986 section 5.2.4. + * + * @param path the path to remove all dot-segments from, can be null. + * @return the cleaned path, can be null only if the given path was null. + */ + public static String removeDotSegments(String path) + { + // Handle special cases... + if (path == null) + { + return null; + } + if (isEmpty(path)) + { + return ""; + } + + StringBuilder scratch = new StringBuilder(path); + StringBuilder sb = new StringBuilder(); + char l, la = 0, laa = 0, laaa = 0; + + while (scratch.length() > 0) + { + l = la(scratch, 0); + la = la(scratch, 1); + laa = la(scratch, 2); + + if (l == DOT) + { + if (la == 0) + { + // (D) found '.' at the end of the URL + break; + } + else if (la == DOT && laa == SLASH) + { + // (A) found '../', remove it from the input... + scratch.delete(0, 3); + continue; + } + else if (la == DOT && laa == 0) + { + // (D) found '..' at the end of the URL + break; + } + else if (la == SLASH) + { + // (A) found './', remove it from the input... + scratch.delete(0, 2); + continue; + } + } + else if (l == SLASH && la == DOT) + { + if (laa == SLASH) + { + // (B) found '/./', remove the leading '/.'... + scratch.delete(0, 2); + continue; + } + else if (laa == 0) + { + // (B) found '/.' as last part of the URL + sb.append(SLASH); + // we're done... + break; + } + else if (laa == DOT) + { + laaa = la(scratch, 3); + if (laaa == SLASH) + { + // (C) found '/../', remove the '/..' part from the input... + scratch.delete(0, 3); + + // go back one segment in the output, including the last '/'... + sb.setLength(lb(sb, 0)); + continue; + } + else if (laaa == 0) + { + // (C) found '/..' as last part of the URL, go back one segment in the output, excluding the last '/'... + sb.setLength(lb(sb, -1)); + // we're done... + break; + } + } + } + + // (E) Copy everything up to (but not including) the next '/'... + do + { + sb.append(l); + scratch.delete(0, 1); + l = la(scratch, 0); + } + while (l != SLASH && l != 0); + } + + return sb.toString(); + } + + private static char la(CharSequence sb, int idx) + { + if (sb.length() > idx) + { + return sb.charAt(idx); + } + return 0; + } + + private static int lb(CharSequence sb, int offset) + { + int pos = sb.length() - 1 - offset; + while (pos > 0 && sb.charAt(pos + offset) != SLASH) + { + pos--; + } + return pos; + } + + private static String decode(ByteBuffer bb, CharsetDecoder decoder) + { + CharBuffer cb = CharBuffer.allocate(128); + + CoderResult result = decoder.decode((ByteBuffer) bb.flip(), cb, true /* endOfInput */); + if (result.isError()) + { + throw new IllegalArgumentException("Malformed UTF-8!"); + } + + return ((CharBuffer) cb.flip()).toString(); + } + + private static boolean endsWith(CharSequence seq, String part) + { + int len = part.length(); + if (seq.length() < len) + { + return false; + } + for (int i = 0; i < len; i++) + { + if (seq.charAt(seq.length() - (i + 1)) != part.charAt(i)) + { + return false; + } + } + return true; + } + + private static int hexVal(CharSequence seq, int idx) + { + char ch = seq.charAt(idx); + if (ch >= '0' && ch <= '9') + { + return ch - '0'; + } + else if (ch >= 'a' && ch <= 'f') + { + return 10 + (ch - 'a'); + } + else if (ch >= 'A' && ch <= 'F') + { + return 10 + (ch - 'A'); + } + throw new IllegalArgumentException("Invalid hex digit: " + ch); + } + + private static boolean isEmpty(String value) + { + return value == null || "".equals(value.trim()); + } + + /** + * Creates a new {@link UriUtils} instance. + */ + private UriUtils() + { + // Nop + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/FailureStateHandler.java b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/FailureStateHandler.java new file mode 100644 index 00000000000..27d8ef468ac --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/FailureStateHandler.java @@ -0,0 +1,184 @@ +/* + * 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.http.base.internal.whiteboard; + +import static org.osgi.service.http.runtime.dto.DTOConstants.FAILURE_REASON_NO_SERVLET_CONTEXT_MATCHING; +import static org.osgi.service.http.runtime.dto.DTOConstants.FAILURE_REASON_SERVICE_NOT_GETTABLE; +import static org.osgi.service.http.runtime.dto.DTOConstants.FAILURE_REASON_SERVLET_CONTEXT_FAILURE; +import static org.osgi.service.http.runtime.dto.DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE; +import static org.osgi.service.http.runtime.dto.DTOConstants.FAILURE_REASON_UNKNOWN; +import static org.osgi.service.http.runtime.dto.DTOConstants.FAILURE_REASON_VALIDATION_FAILED; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.apache.felix.http.base.internal.runtime.AbstractInfo; +import org.apache.felix.http.base.internal.runtime.dto.FailedDTOHolder; +import org.osgi.framework.ServiceReference; + +public class FailureStateHandler { + + private static final class FailureStatus + { + public final Map> reasonToContextsMapping = new ConcurrentHashMap<>(); + } + + private final Map, FailureStatus> serviceFailures = new ConcurrentHashMap<>(); + + /** + * Remove all failures. + */ + public void clear() + { + this.serviceFailures.clear(); + } + + public void addFailure(final AbstractInfo info, final int reason, final Exception ex) + { + this.addFailure(info, 0, reason, ex); + } + + public void addFailure(final AbstractInfo info, final int reason) + { + this.addFailure(info, 0, reason); + } + + public void addFailure(final AbstractInfo info, final long contextId, final int reason) + { + this.addFailure(info, contextId, reason, null); + } + + public void addFailure(final AbstractInfo info, final long contextId, final int reason, final Exception ex) + { + final String type = info.getClass().getSimpleName().substring(0, info.getClass().getSimpleName().length() - 4); + final String serviceInfo; + final ServiceReference ref = info.getServiceReference(); + if ( ref == null ) { + serviceInfo = " with id " + String.valueOf(info.getServiceId()); + } else { + serviceInfo = ""; + } + if ( reason == FAILURE_REASON_NO_SERVLET_CONTEXT_MATCHING ) + { + SystemLogger.debug(ref, "Ignoring unmatching " + type + " service" + serviceInfo); + } + else if ( reason == FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE ) + { + SystemLogger.debug(ref, "Ignoring shadowed " + type + " service" + serviceInfo); + } + else if ( reason == FAILURE_REASON_SERVICE_NOT_GETTABLE ) + { + SystemLogger.error(ref, "Ignoring ungettable " + type + " service" + serviceInfo, ex); + } + else if ( reason == FAILURE_REASON_VALIDATION_FAILED ) + { + SystemLogger.debug(ref, "Ignoring invalid " + type + " service" + serviceInfo); + } + else if ( reason == FAILURE_REASON_NO_SERVLET_CONTEXT_MATCHING ) + { + SystemLogger.debug(ref, "Ignoring unmatched " + type + " service" + serviceInfo); + } + else if ( reason == FAILURE_REASON_SERVLET_CONTEXT_FAILURE ) + { + SystemLogger.debug(ref, "Servlet context " + String.valueOf(contextId) + " failure: Ignoring " + type + " service" + serviceInfo); + } + else if ( reason == FAILURE_REASON_UNKNOWN) + { + SystemLogger.error(ref, "Exception while registering " + type + " service" + serviceInfo, ex); + } + + FailureStatus status = serviceFailures.get(info); + if ( status == null ) + { + // we don't need to sync the add operation, that's taken care of by the caller. + status = new FailureStatus(); + this.serviceFailures.put(info, status); + } + Set contexts = status.reasonToContextsMapping.get(reason); + if ( contexts == null ) + { + contexts = new HashSet<>(); + } + else + { + contexts = new HashSet<>(contexts); + } + contexts.add(contextId); + status.reasonToContextsMapping.put(reason, contexts); + } + + public boolean remove(final AbstractInfo info) + { + return remove(info, 0); + } + + public boolean removeAll(final AbstractInfo info) + { + final boolean result = remove(info, 0); + this.serviceFailures.remove(info); + return result; + } + + public boolean remove(final AbstractInfo info, final long contextId) + { + final FailureStatus status = serviceFailures.get(info); + if ( status != null ) + { + final Iterator>> i = status.reasonToContextsMapping.entrySet().iterator(); + while ( i.hasNext() ) + { + final Map.Entry> entry = i.next(); + if ( entry.getValue().contains(contextId) ) + { + if ( entry.getValue().size() == 1 ) + { + i.remove(); + } + else + { + final Set set = new HashSet<>(entry.getValue()); + set.remove(contextId); + entry.setValue(set); + } + return true; + } + } + } + return false; + } + + public void getRuntimeInfo(final FailedDTOHolder failedDTOHolder) { + for(final Map.Entry, FailureStatus> entry : this.serviceFailures.entrySet() ) + { + final Iterator>> i = entry.getValue().reasonToContextsMapping.entrySet().iterator(); + while ( i.hasNext() ) + { + final Map.Entry> status = i.next(); + + for(final long contextId : status.getValue()) + { + failedDTOHolder.add(entry.getKey(), contextId, status.getKey()); + } + } + } + + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/HttpServiceContextHandler.java b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/HttpServiceContextHandler.java new file mode 100644 index 00000000000..0ed049b3251 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/HttpServiceContextHandler.java @@ -0,0 +1,118 @@ +/* + * 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.http.base.internal.whiteboard; + +import java.util.HashMap; +import java.util.Map; + +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import javax.servlet.ServletContext; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.registry.PerContextHandlerRegistry; +import org.apache.felix.http.base.internal.runtime.ServletContextHelperInfo; +import org.apache.felix.http.base.internal.service.HttpServiceFactory; +import org.apache.felix.http.base.internal.service.PerBundleHttpServiceImpl; +import org.osgi.framework.Bundle; + +public class HttpServiceContextHandler extends WhiteboardContextHandler +{ + private final PerContextHandlerRegistry registry; + + private final HttpServiceFactory httpServiceFactory; + + /** A map of all created servlet contexts. Each bundle gets it's own instance. */ + private final Map perBundleContextMap = new HashMap(); + + private final ServletContext sharedContext; + + public HttpServiceContextHandler(final ServletContextHelperInfo info, + final PerContextHandlerRegistry registry, + final HttpServiceFactory httpServiceFactory, + final ServletContext webContext, + final Bundle httpBundle) + { + super(info, webContext, httpBundle); + this.registry = registry; + this.httpServiceFactory = httpServiceFactory; + this.sharedContext = webContext; + } + + @Override + public PerContextHandlerRegistry getRegistry() + { + return this.registry; + } + + @Override + public ServletContext getSharedContext() + { + return this.sharedContext; + } + + @Override + public @Nullable ExtServletContext getServletContext(@Nullable final Bundle bundle) + { + if ( bundle == null ) + { + return null; + } + final Long key = bundle.getBundleId(); + synchronized ( this.perBundleContextMap ) + { + ContextHolder holder = this.perBundleContextMap.get(key); + if ( holder == null ) + { + holder = new ContextHolder(); + final PerBundleHttpServiceImpl service = (PerBundleHttpServiceImpl)this.httpServiceFactory.getService(bundle, null); + holder.servletContext = service.getServletContext(service.createDefaultHttpContext()); + holder.httpService = service; + this.perBundleContextMap.put(key, holder); + } + holder.counter++; + + return holder.servletContext; + } + } + + @Override + public void ungetServletContext(@NotNull final Bundle bundle) + { + final Long key = bundle.getBundleId(); + synchronized ( this.perBundleContextMap ) + { + ContextHolder holder = this.perBundleContextMap.get(key); + if ( holder != null ) + { + holder.counter--; + if ( holder.counter == 0 ) + { + this.perBundleContextMap.remove(key); + holder.httpService.unregisterAll(); + } + } + } + } + + private static final class ContextHolder + { + public long counter; + public ExtServletContext servletContext; + public PerBundleHttpServiceImpl httpService; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/PerBundleServletContextImpl.java b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/PerBundleServletContextImpl.java new file mode 100644 index 00000000000..8230270e635 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/PerBundleServletContextImpl.java @@ -0,0 +1,464 @@ +/* + * 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.http.base.internal.whiteboard; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.Map; +import java.util.Set; + +import javax.servlet.Filter; +import javax.servlet.FilterRegistration; +import javax.servlet.RequestDispatcher; +import javax.servlet.Servlet; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; +import javax.servlet.ServletRegistration.Dynamic; +import javax.servlet.ServletRequestAttributeListener; +import javax.servlet.ServletRequestListener; +import javax.servlet.SessionCookieConfig; +import javax.servlet.SessionTrackingMode; +import javax.servlet.descriptor.JspConfigDescriptor; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionListener; + +import org.apache.felix.http.base.internal.HttpConfig; +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.registry.PerContextHandlerRegistry; +import org.apache.felix.http.base.internal.util.MimeTypes; +import org.osgi.framework.Bundle; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.service.http.context.ServletContextHelper; + +/** + * This servlet context implementation represents the per + * bundle specific part of a servlet context backed by a + * servlet context helper. + * + */ +public class PerBundleServletContextImpl implements ExtServletContext { + + private final Bundle bundle; + private final ServletContext delegatee; + private final ServletContextHelper contextHelper; + private final PerContextHandlerRegistry handlerRegistry; + + public PerBundleServletContextImpl(final Bundle bundle, + final ServletContext sharedContext, + final ServletContextHelper delegatee, + final PerContextHandlerRegistry handlerRegistry) + { + this.bundle = bundle; + this.delegatee = sharedContext; + this.contextHelper = delegatee; + this.handlerRegistry = handlerRegistry; + } + + @Override + public boolean handleSecurity(final HttpServletRequest req, + final HttpServletResponse res) + throws IOException + { + return this.contextHelper.handleSecurity(req, res); + } + + @Override + public void finishSecurity(HttpServletRequest req, HttpServletResponse res) + { + this.contextHelper.finishSecurity(req, res); + } + + @Override + public HttpSessionListener getHttpSessionListener() + { + return this.handlerRegistry.getEventListenerRegistry(); + } + + @Override + public HttpSessionAttributeListener getHttpSessionAttributeListener() + { + return this.handlerRegistry.getEventListenerRegistry(); + } + + @Override + public ServletRequestListener getServletRequestListener() + { + return this.handlerRegistry.getEventListenerRegistry(); + } + + @Override + public ServletRequestAttributeListener getServletRequestAttributeListener() + { + return this.handlerRegistry.getEventListenerRegistry(); + } + + @Override + public HttpConfig getConfig() + { + return this.handlerRegistry.getConfig(); + } + + @Override + public ClassLoader getClassLoader() + { + return this.bundle.adapt(BundleWiring.class).getClassLoader(); + } + + /** + * @see javax.servlet.ServletContext#getResource(java.lang.String) + */ + @Override + public URL getResource(final String path) + { + return this.contextHelper.getResource(path); + } + + @Override + public String getMimeType(final String name) + { + String type = this.contextHelper.getMimeType(name); + if (type != null) { + return type; + } + + return MimeTypes.get().getByFile(name); + } + + @Override + public String getRealPath(final String path) + { + return this.contextHelper.getRealPath(path); + } + + @Override + public Set getResourcePaths(final String path) + { + return this.contextHelper.getResourcePaths(path); + } + + @Override + public String getContextPath() + { + return delegatee.getContextPath(); + } + + @Override + public ServletContext getContext(String uripath) + { + return delegatee.getContext(uripath); + } + + @Override + public int getMajorVersion() + { + return delegatee.getMajorVersion(); + } + + @Override + public int getMinorVersion() + { + return delegatee.getMinorVersion(); + } + + @Override + public int getEffectiveMajorVersion() + { + return delegatee.getEffectiveMajorVersion(); + } + + @Override + public int getEffectiveMinorVersion() + { + return delegatee.getEffectiveMinorVersion(); + } + + @Override + public InputStream getResourceAsStream(final String path) + { + final URL res = getResource(path); + if (res != null) + { + try + { + return res.openStream(); + } + catch (IOException e) + { + // Do nothing + } + } + return null; + } + + @Override + public RequestDispatcher getRequestDispatcher(final String path) + { + return delegatee.getRequestDispatcher(path); + } + + @Override + public RequestDispatcher getNamedDispatcher(String name) + { + return delegatee.getNamedDispatcher(name); + } + + @SuppressWarnings("deprecation") + @Override + public Servlet getServlet(String name) throws ServletException + { + return delegatee.getServlet(name); + } + + @SuppressWarnings("deprecation") + @Override + public Enumeration getServlets() + { + return delegatee.getServlets(); + } + + @SuppressWarnings("deprecation") + @Override + public Enumeration getServletNames() + { + return delegatee.getServletNames(); + } + + @Override + public void log(String msg) + { + delegatee.log(msg); + } + + @SuppressWarnings("deprecation") + @Override + public void log(Exception exception, String msg) + { + delegatee.log(exception, msg); + } + + @Override + public void log(String message, Throwable throwable) + { + delegatee.log(message, throwable); + } + + @Override + public String getServerInfo() + { + return delegatee.getServerInfo(); + } + + @Override + public String getInitParameter(String name) + { + return delegatee.getInitParameter(name); + } + + @Override + public Enumeration getInitParameterNames() + { + return delegatee.getInitParameterNames(); + } + + @Override + public boolean setInitParameter(String name, String value) + { + return delegatee.setInitParameter(name, value); + } + + @Override + public Object getAttribute(String name) + { + return delegatee.getAttribute(name); + } + + @Override + public Enumeration getAttributeNames() + { + return delegatee.getAttributeNames(); + } + + @Override + public void setAttribute(String name, Object object) + { + delegatee.setAttribute(name, object); + } + + @Override + public void removeAttribute(String name) + { + delegatee.removeAttribute(name); + } + + @Override + public String getServletContextName() + { + return delegatee.getServletContextName(); + } + + @Override + public Dynamic addServlet(String servletName, String className) + { + return delegatee.addServlet(servletName, className); + } + + @Override + public Dynamic addServlet(String servletName, Servlet servlet) + { + return delegatee.addServlet(servletName, servlet); + } + + @Override + public Dynamic addServlet(String servletName, + Class servletClass) + { + return delegatee.addServlet(servletName, servletClass); + } + + @Override + public T createServlet(Class clazz) + throws ServletException + { + return delegatee.createServlet(clazz); + } + + @Override + public ServletRegistration getServletRegistration(String servletName) + { + return delegatee.getServletRegistration(servletName); + } + + @Override + public Map getServletRegistrations() + { + return delegatee.getServletRegistrations(); + } + + @Override + public javax.servlet.FilterRegistration.Dynamic addFilter( + String filterName, String className) + { + return delegatee.addFilter(filterName, className); + } + + @Override + public javax.servlet.FilterRegistration.Dynamic addFilter( + String filterName, Filter filter) + { + return delegatee.addFilter(filterName, filter); + } + + @Override + public javax.servlet.FilterRegistration.Dynamic addFilter( + String filterName, Class filterClass) + { + return delegatee.addFilter(filterName, filterClass); + } + + @Override + public T createFilter(Class clazz) + throws ServletException + { + return delegatee.createFilter(clazz); + } + + @Override + public FilterRegistration getFilterRegistration(String filterName) + { + return delegatee.getFilterRegistration(filterName); + } + + @Override + public Map getFilterRegistrations() + { + return delegatee.getFilterRegistrations(); + } + + @Override + public SessionCookieConfig getSessionCookieConfig() + { + return delegatee.getSessionCookieConfig(); + } + + @Override + public void setSessionTrackingModes( + Set sessionTrackingModes) + { + delegatee.setSessionTrackingModes(sessionTrackingModes); + } + + @Override + public Set getDefaultSessionTrackingModes() + { + return delegatee.getDefaultSessionTrackingModes(); + } + + @Override + public Set getEffectiveSessionTrackingModes() + { + return delegatee.getEffectiveSessionTrackingModes(); + } + + @Override + public void addListener(String className) + { + delegatee.addListener(className); + } + + @Override + public void addListener(T t) + { + delegatee.addListener(t); + } + + @Override + public void addListener(Class listenerClass) + { + delegatee.addListener(listenerClass); + } + + @Override + public T createListener(Class clazz) + throws ServletException + { + return delegatee.createListener(clazz); + } + + @Override + public JspConfigDescriptor getJspConfigDescriptor() + { + return delegatee.getJspConfigDescriptor(); + } + + @Override + public void declareRoles(String... roleNames) + { + delegatee.declareRoles(roleNames); + } + + @Override + public String getVirtualServerName() + { + return delegatee.getVirtualServerName(); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/RegistrationFailureException.java b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/RegistrationFailureException.java new file mode 100644 index 00000000000..46a60e840fe --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/RegistrationFailureException.java @@ -0,0 +1,59 @@ +/* + * 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.http.base.internal.whiteboard; + +import javax.servlet.ServletException; + +import org.apache.felix.http.base.internal.runtime.WhiteboardServiceInfo; + +@SuppressWarnings("serial") +public class RegistrationFailureException extends ServletException +{ + private final WhiteboardServiceInfo info; + private final int errorCode; + + public RegistrationFailureException(WhiteboardServiceInfo info, int errorCode) + { + super(); + this.info = info; + this.errorCode = errorCode; + } + + public RegistrationFailureException(WhiteboardServiceInfo info, int errorCode, String message) + { + super(message); + this.info = info; + this.errorCode = errorCode; + } + + public RegistrationFailureException(WhiteboardServiceInfo info, int errorCode, Throwable exception) + { + super(exception); + this.info = info; + this.errorCode = errorCode; + } + + public WhiteboardServiceInfo getInfo() + { + return info; + } + + public int getErrorCode() + { + return errorCode; + } +} \ No newline at end of file diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/SharedServletContextImpl.java b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/SharedServletContextImpl.java new file mode 100644 index 00000000000..e497573d272 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/SharedServletContextImpl.java @@ -0,0 +1,491 @@ +/* + * 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.http.base.internal.whiteboard; + +import static org.apache.felix.http.base.internal.util.UriUtils.decodePath; +import static org.apache.felix.http.base.internal.util.UriUtils.removeDotSegments; + +import java.io.InputStream; +import java.net.URL; +import java.util.Collections; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.servlet.Filter; +import javax.servlet.FilterRegistration; +import javax.servlet.RequestDispatcher; +import javax.servlet.Servlet; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextAttributeEvent; +import javax.servlet.ServletContextAttributeListener; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; +import javax.servlet.SessionCookieConfig; +import javax.servlet.SessionTrackingMode; +import javax.servlet.descriptor.JspConfigDescriptor; + +import org.apache.felix.http.base.internal.dispatch.RequestDispatcherImpl; +import org.apache.felix.http.base.internal.dispatch.RequestInfo; +import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.apache.felix.http.base.internal.registry.PathResolution; +import org.apache.felix.http.base.internal.registry.PerContextHandlerRegistry; +import org.apache.felix.http.base.internal.registry.ServletResolution; +import org.apache.felix.http.base.internal.util.UriUtils; + +/** + * This servlet context implementation represents the shared + * part for a servlet context backed by a servlet context helper. + * + * For each using bundle, a {@link PerBundleServletContextImpl} is created. + */ +public class SharedServletContextImpl implements ServletContext +{ + + private final ServletContext context; + private final Map attributes = new ConcurrentHashMap(); + private final String contextPath; + private final String name; + private final PerContextHandlerRegistry registry; + private final ServletContextAttributeListener attributeListener; + private final Map initParameters = new HashMap(); + + public SharedServletContextImpl(final ServletContext webContext, + final String name, + final String path, + final Map initParameters, + final PerContextHandlerRegistry registry) + { + this.context = webContext; + if ( path.equals("/") ) + { + this.contextPath = webContext.getContextPath(); + } + else + { + this.contextPath = webContext.getContextPath() + path; + } + this.name = name; + if ( initParameters != null ) + { + this.initParameters.putAll(initParameters); + } + this.attributeListener = registry.getEventListenerRegistry(); + this.registry = registry; + } + + @Override + public ClassLoader getClassLoader() + { + // is implemented by {@link PerBundleServletContextImpl}. + return null; + } + + /** + * @see javax.servlet.ServletContext#getResource(java.lang.String) + */ + @Override + public URL getResource(String path) + { + // is implemented by {@link PerBundleServletContextImpl}. + return null; + } + + @Override + public String getMimeType(String file) + { + // is implemented by {@link PerBundleServletContextImpl}. + return null; + } + + @Override + public String getRealPath(String path) + { + // is implemented by {@link PerBundleServletContextImpl}. + return null; + } + + @Override + public Set getResourcePaths(final String path) + { + // is implemented by {@link PerBundleServletContextImpl}. + return null; + } + + @Override + public FilterRegistration.Dynamic addFilter(final String filterName, final Class type) + { + throw new UnsupportedOperationException(); + } + + @Override + public FilterRegistration.Dynamic addFilter(final String filterName, final Filter filter) + { + throw new UnsupportedOperationException(); + } + + @Override + public FilterRegistration.Dynamic addFilter(final String filterName, final String className) + { + throw new UnsupportedOperationException(); + } + + @Override + public void addListener(final Class type) + { + throw new UnsupportedOperationException(); + } + + @Override + public void addListener(final String className) + { + throw new UnsupportedOperationException(); + } + + @Override + public void addListener(final T listener) + { + throw new UnsupportedOperationException(); + } + + @Override + public ServletRegistration.Dynamic addServlet(final String servletName, final Class type) + { + throw new UnsupportedOperationException(); + } + + @Override + public ServletRegistration.Dynamic addServlet(final String servletName, final Servlet servlet) + { + throw new UnsupportedOperationException(); + } + + @Override + public ServletRegistration.Dynamic addServlet(final String servletName, final String className) + { + throw new UnsupportedOperationException(); + } + + @Override + public T createFilter(final Class type) throws ServletException + { + throw new UnsupportedOperationException(); + } + + @Override + public T createListener(final Class type) throws ServletException + { + throw new UnsupportedOperationException(); + } + + @Override + public T createServlet(final Class type) throws ServletException + { + throw new UnsupportedOperationException(); + } + + @Override + public void declareRoles(final String... roleNames) + { + throw new UnsupportedOperationException(); + } + + @Override + public String getVirtualServerName() { + return context.getVirtualServerName(); + } + + @Override + public Object getAttribute(final String name) + { + return this.attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() + { + return Collections.enumeration(this.attributes.keySet()); + } + + @Override + public ServletContext getContext(final String uri) + { + return this.context.getContext(uri); + } + + @Override + public String getContextPath() + { + return this.contextPath; + } + + @Override + public Set getDefaultSessionTrackingModes() + { + return this.context.getDefaultSessionTrackingModes(); + } + + @Override + public int getEffectiveMajorVersion() + { + return this.context.getEffectiveMajorVersion(); + } + + @Override + public int getEffectiveMinorVersion() + { + return this.context.getEffectiveMinorVersion(); + } + + @Override + public Set getEffectiveSessionTrackingModes() + { + return this.context.getEffectiveSessionTrackingModes(); + } + + @Override + public FilterRegistration getFilterRegistration(final String filterName) + { + return this.context.getFilterRegistration(filterName); + } + + @Override + public Map getFilterRegistrations() + { + return this.context.getFilterRegistrations(); + } + + @Override + public String getInitParameter(final String name) + { + return this.initParameters.get(name); + } + + @Override + public Enumeration getInitParameterNames() + { + return Collections.enumeration(this.initParameters.keySet()); + } + + @Override + public JspConfigDescriptor getJspConfigDescriptor() + { + return null; + } + + @Override + public int getMajorVersion() + { + return this.context.getMajorVersion(); + } + + @Override + public int getMinorVersion() + { + return this.context.getMinorVersion(); + } + + @Override + public RequestDispatcher getNamedDispatcher(final String name) + { + if (name == null) + { + return null; + } + + final RequestDispatcher dispatcher; + final ServletHandler servletHandler = this.registry.resolveServletByName(name); + if ( servletHandler != null ) + { + final ServletResolution resolution = new ServletResolution(); + resolution.handler = servletHandler; + resolution.handlerRegistry = this.registry; + // TODO - what is the path of a named servlet? + final RequestInfo requestInfo = new RequestInfo("", null, null, null); + dispatcher = new RequestDispatcherImpl(resolution, requestInfo); + } + else + { + dispatcher = null; + } + return dispatcher; + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) + { + // See section 9.1 of Servlet 3.x specification... + if (path == null || (!path.startsWith("/") && !"".equals(path))) + { + return null; + } + + String query = null; + int q = 0; + if ((q = path.indexOf('?')) > 0) + { + query = path.substring(q + 1); + path = path.substring(0, q); + } + // TODO remove path parameters... + final String encodedRequestURI = path == null ? "" : removeDotSegments(path); + final String requestURI = decodePath(encodedRequestURI); + + final RequestDispatcher dispatcher; + final PathResolution pathResolution = this.registry.resolve(requestURI); + if ( pathResolution != null ) + { + final ServletResolution resolution = new ServletResolution(); + resolution.handler = pathResolution.handler; + resolution.handlerRegistry = this.registry; + final RequestInfo requestInfo = new RequestInfo(pathResolution.servletPath, pathResolution.pathInfo, query, + UriUtils.concat(this.contextPath, encodedRequestURI)); + dispatcher = new RequestDispatcherImpl(resolution, requestInfo); + } + else + { + dispatcher = null; + } + return dispatcher; + } + + @Override + public InputStream getResourceAsStream(final String path) + { + // is implemented by {@link PerBundleServletContextImpl}. + return null; + } + + @Override + public String getServerInfo() + { + return this.context.getServerInfo(); + } + + @SuppressWarnings("deprecation") + @Override + public Servlet getServlet(final String name) throws ServletException + { + return this.context.getServlet(name); + } + + @Override + public String getServletContextName() + { + return this.name; + } + + @SuppressWarnings("deprecation") + @Override + public Enumeration getServletNames() + { + return this.context.getServletNames(); + } + + @Override + public ServletRegistration getServletRegistration(final String servletName) + { + return this.context.getServletRegistration(servletName); + } + + @Override + public Map getServletRegistrations() + { + return this.context.getServletRegistrations(); + } + + @SuppressWarnings("deprecation") + @Override + public Enumeration getServlets() + { + return this.context.getServlets(); + } + + @Override + public SessionCookieConfig getSessionCookieConfig() + { + return this.context.getSessionCookieConfig(); + } + + @SuppressWarnings("deprecation") + @Override + public void log(final Exception cause, final String message) + { + SystemLogger.error(message, cause); + } + + @Override + public void log(final String message) + { + SystemLogger.info(message); + } + + @Override + public void log(final String message, final Throwable cause) + { + SystemLogger.error(message, cause); + } + + @Override + public void removeAttribute(final String name) + { + final Object oldValue = this.attributes.remove(name); + + if (oldValue != null) + { + this.attributeListener.attributeRemoved(new ServletContextAttributeEvent(this, name, oldValue)); + } + } + + @Override + public void setAttribute(final String name, final Object value) + { + if (value == null) + { + this.removeAttribute(name); + } + else if (name != null) + { + Object oldValue = this.attributes.put(name, value); + + if (oldValue == null) + { + this.attributeListener.attributeAdded(new ServletContextAttributeEvent(this, name, value)); + } + else + { + this.attributeListener.attributeReplaced(new ServletContextAttributeEvent(this, name, oldValue)); + } + } + } + + @Override + public boolean setInitParameter(final String name, final String value) + { + throw new IllegalStateException(); + } + + @Override + public void setSessionTrackingModes(final Set modes) + { + throw new IllegalStateException(); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/WhiteboardContextHandler.java b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/WhiteboardContextHandler.java new file mode 100644 index 00000000000..25400a260d2 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/WhiteboardContextHandler.java @@ -0,0 +1,202 @@ +/* + * 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.http.base.internal.whiteboard; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletContext; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.registry.HandlerRegistry; +import org.apache.felix.http.base.internal.registry.PerContextHandlerRegistry; +import org.apache.felix.http.base.internal.runtime.ServletContextHelperInfo; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceObjects; +import org.osgi.service.http.context.ServletContextHelper; + +/** + * The whiteboard context handler provides some information and functionality for + * handling servlet context (helpers). + */ +public class WhiteboardContextHandler implements Comparable +{ + /** The info object for the context. */ + private final ServletContextHelperInfo info; + + /** The web context. */ + private final ServletContext webContext; + + /** The http bundle. */ + private final Bundle httpBundle; + + /** A map of all created servlet contexts. Each bundle gets it's own instance. */ + private final Map perBundleContextMap = new HashMap<>(); + + /** The corresponding handler registry. */ + private volatile PerContextHandlerRegistry registry; + + /** The shared part of the servlet context. */ + private volatile ServletContext sharedContext; + + public WhiteboardContextHandler(@NotNull final ServletContextHelperInfo info, + @NotNull final ServletContext webContext, + @NotNull final Bundle httpBundle) + { + this.webContext = webContext; + this.info = info; + this.httpBundle = httpBundle; + } + + public @NotNull BundleContext getBundleContext() + { + return this.httpBundle.getBundleContext(); + } + + public @NotNull ServletContextHelperInfo getContextInfo() + { + return this.info; + } + + @Override + public int compareTo(@NotNull final WhiteboardContextHandler o) + { + return this.info.compareTo(o.getContextInfo()); + } + + /** + * Activate this context. + * @return {@code true} if it succeeded. + */ + public boolean activate(@NotNull final HandlerRegistry registry) + { + this.registry = new PerContextHandlerRegistry(this.info, registry.getConfig()); + this.sharedContext = new SharedServletContextImpl(webContext, + info.getName(), + info.getPath(), + info.getInitParameters(), + this.registry); + final boolean activate = getServletContext(httpBundle) != null; + if ( !activate ) + { + this.registry = null; + this.sharedContext = null; + } + else + { + registry.add(this.registry); + } + return activate; + } + + /** + * Deactivate this context. + */ + public void deactivate(@NotNull final HandlerRegistry registry) + { + registry.remove(this.info); + this.registry = null; + this.sharedContext = null; + this.ungetServletContext(httpBundle); + this.perBundleContextMap.clear(); + } + + public @Nullable ServletContext getSharedContext() + { + return sharedContext; + } + + public @Nullable ExtServletContext getServletContext(@Nullable final Bundle bundle) + { + if ( bundle == null ) + { + return null; + } + final Long key = bundle.getBundleId(); + synchronized ( this.perBundleContextMap ) + { + ContextHolder holder = this.perBundleContextMap.get(key); + if ( holder == null ) + { + final BundleContext ctx = bundle.getBundleContext(); + final ServiceObjects so = (ctx == null ? null : ctx.getServiceObjects(this.info.getServiceReference())); + if ( so != null ) + { + final ServletContextHelper service = so.getService(); + if ( service != null ) + { + holder = new ContextHolder(); + holder.servletContextHelper = service; + holder.servletContext = new PerBundleServletContextImpl(bundle, + this.sharedContext, + service, + this.registry); + this.perBundleContextMap.put(key, holder); + } + } + } + if ( holder != null ) + { + holder.counter++; + + return holder.servletContext; + } + } + return null; + } + + public void ungetServletContext(@NotNull final Bundle bundle) + { + final Long key = bundle.getBundleId(); + synchronized ( this.perBundleContextMap ) + { + ContextHolder holder = this.perBundleContextMap.get(key); + if ( holder != null ) + { + holder.counter--; + if ( holder.counter == 0 ) + { + this.perBundleContextMap.remove(key); + if ( holder.servletContextHelper != null ) + { + final BundleContext ctx = bundle.getBundleContext(); + final ServiceObjects so = (ctx == null ? null : ctx.getServiceObjects(this.info.getServiceReference())); + if ( so != null ) + { + so.ungetService(holder.servletContextHelper); + } + } + } + } + } + } + + public @Nullable PerContextHandlerRegistry getRegistry() + { + return this.registry; + } + + private static final class ContextHolder + { + public long counter; + public ExtServletContext servletContext; + public ServletContextHelper servletContextHelper; + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/WhiteboardManager.java b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/WhiteboardManager.java new file mode 100644 index 00000000000..bfcb2932371 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/WhiteboardManager.java @@ -0,0 +1,1020 @@ +/* + * 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.http.base.internal.whiteboard; + +import static org.osgi.service.http.runtime.dto.DTOConstants.FAILURE_REASON_NO_SERVLET_CONTEXT_MATCHING; +import static org.osgi.service.http.runtime.dto.DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE; +import static org.osgi.service.http.runtime.dto.DTOConstants.FAILURE_REASON_UNKNOWN; +import static org.osgi.service.http.runtime.dto.DTOConstants.FAILURE_REASON_VALIDATION_FAILED; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.servlet.FilterChain; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionEvent; + +import org.apache.felix.http.base.internal.console.HttpServicePlugin; +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.handler.FilterHandler; +import org.apache.felix.http.base.internal.handler.HttpServiceServletHandler; +import org.apache.felix.http.base.internal.handler.HttpSessionWrapper; +import org.apache.felix.http.base.internal.handler.ListenerHandler; +import org.apache.felix.http.base.internal.handler.PreprocessorHandler; +import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.handler.WhiteboardFilterHandler; +import org.apache.felix.http.base.internal.handler.WhiteboardListenerHandler; +import org.apache.felix.http.base.internal.handler.WhiteboardServletHandler; +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.apache.felix.http.base.internal.registry.EventListenerRegistry; +import org.apache.felix.http.base.internal.registry.HandlerRegistry; +import org.apache.felix.http.base.internal.runtime.AbstractInfo; +import org.apache.felix.http.base.internal.runtime.FilterInfo; +import org.apache.felix.http.base.internal.runtime.ListenerInfo; +import org.apache.felix.http.base.internal.runtime.PreprocessorInfo; +import org.apache.felix.http.base.internal.runtime.ResourceInfo; +import org.apache.felix.http.base.internal.runtime.ServletContextHelperInfo; +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.apache.felix.http.base.internal.runtime.WhiteboardServiceInfo; +import org.apache.felix.http.base.internal.runtime.dto.FailedDTOHolder; +import org.apache.felix.http.base.internal.runtime.dto.PreprocessorDTOBuilder; +import org.apache.felix.http.base.internal.runtime.dto.RegistryRuntime; +import org.apache.felix.http.base.internal.runtime.dto.ServletContextDTOBuilder; +import org.apache.felix.http.base.internal.service.HttpServiceFactory; +import org.apache.felix.http.base.internal.service.HttpServiceRuntimeImpl; +import org.apache.felix.http.base.internal.service.ResourceServlet; +import org.apache.felix.http.base.internal.whiteboard.tracker.FilterTracker; +import org.apache.felix.http.base.internal.whiteboard.tracker.ListenersTracker; +import org.apache.felix.http.base.internal.whiteboard.tracker.PreprocessorTracker; +import org.apache.felix.http.base.internal.whiteboard.tracker.ResourceTracker; +import org.apache.felix.http.base.internal.whiteboard.tracker.ServletContextHelperTracker; +import org.apache.felix.http.base.internal.whiteboard.tracker.ServletTracker; +import org.jetbrains.annotations.NotNull; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.http.context.ServletContextHelper; +import org.osgi.service.http.runtime.HttpServiceRuntimeConstants; +import org.osgi.service.http.runtime.dto.DTOConstants; +import org.osgi.service.http.runtime.dto.PreprocessorDTO; +import org.osgi.service.http.runtime.dto.ServletContextDTO; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; +import org.osgi.service.http.whiteboard.Preprocessor; +import org.osgi.util.tracker.ServiceTracker; + +public final class WhiteboardManager +{ + /** The bundle context of the http bundle. */ + private final BundleContext httpBundleContext; + + /** The http service factory. */ + private final HttpServiceFactory httpServiceFactory; + + private final HttpServiceRuntimeImpl serviceRuntime; + + private final List> trackers = new ArrayList<>(); + + private final HttpServicePlugin plugin; + + /** A map containing all servlet context registrations. Mapped by context name */ + private final Map> contextMap = new HashMap<>(); + + /** A map with all servlet/filter registrations, mapped by abstract info. */ + private final Map, List> servicesMap = new HashMap<>(); + + private volatile List preprocessorHandlers = Collections.emptyList(); + + private final HandlerRegistry registry; + + private final FailureStateHandler failureStateHandler = new FailureStateHandler(); + + private volatile ServletContext webContext; + + private volatile ServiceRegistration defaultContextRegistration; + + /** + * Create a new whiteboard http manager + * + * @param bundleContext The bundle context of the http bundle + * @param httpServiceFactory The http service factory + * @param registry The handler registry + */ + public WhiteboardManager(final BundleContext bundleContext, + final HttpServiceFactory httpServiceFactory, + final HandlerRegistry registry) + { + this.httpBundleContext = bundleContext; + this.httpServiceFactory = httpServiceFactory; + this.registry = registry; + this.serviceRuntime = new HttpServiceRuntimeImpl(registry, this, bundleContext); + this.plugin = new HttpServicePlugin(bundleContext, this.serviceRuntime); + } + + /** + * Start the whiteboard manager + * @param containerContext The servlet context + */ + public void start(final ServletContext containerContext, @NotNull final Dictionary httpServiceProps) + { + // runtime service gets the same props for now + this.serviceRuntime.setAllAttributes(httpServiceProps); + + this.serviceRuntime.setAttribute(HttpServiceRuntimeConstants.HTTP_SERVICE_ID, + Collections.singletonList(this.httpServiceFactory.getHttpServiceServiceId())); + this.serviceRuntime.register(this.httpBundleContext); + + this.webContext = containerContext; + + + // add context for http service + final List list = new ArrayList<>(); + final ServletContextHelperInfo info = new ServletContextHelperInfo(Integer.MAX_VALUE, + HttpServiceFactory.HTTP_SERVICE_CONTEXT_SERVICE_ID, + HttpServiceFactory.HTTP_SERVICE_CONTEXT_NAME, "/", null); + list.add(new HttpServiceContextHandler(info, registry.getRegistry(HttpServiceFactory.HTTP_SERVICE_CONTEXT_SERVICE_ID), + httpServiceFactory, webContext, this.httpBundleContext.getBundle())); + this.contextMap.put(HttpServiceFactory.HTTP_SERVICE_CONTEXT_NAME, list); + + // add default context + final Dictionary props = new Hashtable<>(); + props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME, HttpWhiteboardConstants.HTTP_WHITEBOARD_DEFAULT_CONTEXT_NAME); + props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_PATH, "/"); + props.put(Constants.SERVICE_RANKING, Integer.MIN_VALUE); + this.defaultContextRegistration = httpBundleContext.registerService( + ServletContextHelper.class, + new ServiceFactory() + { + + @Override + public ServletContextHelper getService( + final Bundle bundle, + final ServiceRegistration registration) + { + return new ServletContextHelper(bundle) + { + // nothing to override + }; + } + + @Override + public void ungetService( + final Bundle bundle, + final ServiceRegistration registration, + final ServletContextHelper service) + { + // nothing to do + } + }, props); + addTracker(new FilterTracker(this.httpBundleContext, this)); + addTracker(new ListenersTracker(this.httpBundleContext, this)); + addTracker(new PreprocessorTracker(this.httpBundleContext, this)); + addTracker(new ResourceTracker(this.httpBundleContext, this)); + addTracker(new ServletContextHelperTracker(this.httpBundleContext, this)); + addTracker(new ServletTracker(this.httpBundleContext, this)); + + this.plugin.register(); + } + + /** + * Add a tracker and start it + * @param tracker The tracker instance + */ + private void addTracker(ServiceTracker tracker) + { + this.trackers.add(tracker); + tracker.open(); + } + + /** + * Stop the instance + */ + public void stop() + { + this.plugin.unregister(); + for(final ServiceTracker t : this.trackers) + { + t.close(); + } + this.trackers.clear(); + + this.serviceRuntime.unregister(); + + this.preprocessorHandlers = Collections.emptyList(); + this.contextMap.clear(); + this.servicesMap.clear(); + this.failureStateHandler.clear(); + this.registry.reset(); + + if (this.defaultContextRegistration != null) + { + this.defaultContextRegistration.unregister(); + this.defaultContextRegistration = null; + } + this.webContext = null; + } + + public void sessionDestroyed(@NotNull final HttpSession session, final Set contextNames) + { + for(final String contextName : contextNames) + { + final WhiteboardContextHandler handler = this.getContextHandler(contextName); + if ( handler != null ) + { + final ExtServletContext context = handler.getServletContext(this.httpBundleContext.getBundle()); + new HttpSessionWrapper(session, context, this.registry.getConfig(), true).invalidate(); + handler.ungetServletContext(this.httpBundleContext.getBundle()); + } + } + } + + /** + * Handle session id changes + * @param session The session where the id changed + * @param oldSessionId The old session id + * @param contextIds The context ids using that session + */ + public void sessionIdChanged(@NotNull final HttpSessionEvent event, final String oldSessionId, final Set contextNames) + { + for(final String contextName : contextNames) + { + final WhiteboardContextHandler handler = this.getContextHandler(contextName); + if ( handler != null ) + { + handler.getRegistry().getEventListenerRegistry().sessionIdChanged(event, oldSessionId); + } + } + } + + /** + * Activate a servlet context helper. + * + * @param handler The context handler + * @return {@code true} if activation succeeded. + */ + private boolean activate(final WhiteboardContextHandler handler) + { + if ( !handler.activate(this.registry) ) + { + return false; + } + + final List> services = new ArrayList<>(); + for(final Map.Entry, List> entry : this.servicesMap.entrySet()) + { + final WhiteboardServiceInfo info = entry.getKey(); + + if ( info.getContextSelectionFilter().match(handler.getContextInfo().getServiceReference()) ) + { + final int reason = checkForServletRegistrationInHttpServiceContext(handler, info); + if ( reason == -1 ) + { + entry.getValue().add(handler); + if ( entry.getValue().size() == 1 ) + { + this.failureStateHandler.remove(info); + } + if ( info instanceof ListenerInfo && ((ListenerInfo)info).isListenerType(ServletContextListener.class.getName()) ) + { + // servlet context listeners will be registered directly + this.registerWhiteboardService(handler, info); + } + else + { + // registration of other services will be delayed + services.add(info); + } + } + } + } + // notify context listeners first + handler.getRegistry().getEventListenerRegistry().contextInitialized(); + + // register services + for(final WhiteboardServiceInfo info : services) + { + this.registerWhiteboardService(handler, info); + } + + return true; + } + + /** + * Deactivate a servlet context. + * + * @param handler A context handler + */ + private void deactivate(final WhiteboardContextHandler handler) + { + // services except context listeners first + final List> listeners = new ArrayList<>(); + final Iterator, List>> i = this.servicesMap.entrySet().iterator(); + while ( i.hasNext() ) + { + final Map.Entry, List> entry = i.next(); + if ( entry.getValue().remove(handler) ) + { + if ( !this.failureStateHandler.remove(entry.getKey(), handler.getContextInfo().getServiceId()) ) + { + if ( entry.getKey() instanceof ListenerInfo && ((ListenerInfo)entry.getKey()).isListenerType(ServletContextListener.class.getName()) ) + { + listeners.add(entry.getKey()); + } + else + { + this.unregisterWhiteboardService(handler, entry.getKey()); + } + } + if ( entry.getValue().isEmpty() ) + { + this.failureStateHandler.addFailure(entry.getKey(), FAILURE_REASON_NO_SERVLET_CONTEXT_MATCHING); + } + } + } + // context listeners last + handler.getRegistry().getEventListenerRegistry().contextDestroyed(); + for(final WhiteboardServiceInfo info : listeners) + { + this.unregisterWhiteboardService(handler, info); + } + + handler.deactivate(this.registry); + } + + /** + * Add a servlet context helper. + * + * @param info The servlet context helper info + * @return {@code true} if the service matches this http whiteboard service + */ + public boolean addContextHelper(final ServletContextHelperInfo info) + { + // no failure DTO and no logging if not matching + if ( isMatchingService(info) ) + { + if ( info.isValid() ) + { + synchronized ( this.contextMap ) + { + final WhiteboardContextHandler handler = new WhiteboardContextHandler(info, + this.webContext, + this.httpBundleContext.getBundle()); + + // check for activate/deactivate + List handlerList = this.contextMap.get(info.getName()); + if ( handlerList == null ) + { + handlerList = new ArrayList<>(); + } + final boolean activate = handlerList.isEmpty() || handlerList.get(0).compareTo(handler) > 0; + if ( activate ) + { + // try to activate + if ( this.activate(handler) ) + { + handlerList.add(handler); + Collections.sort(handlerList); + this.contextMap.put(info.getName(), handlerList); + + // check for deactivate + if ( handlerList.size() > 1 ) + { + final WhiteboardContextHandler oldHead = handlerList.get(1); + this.deactivate(oldHead); + + this.failureStateHandler.addFailure(oldHead.getContextInfo(), FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE); + } + } + else + { + this.failureStateHandler.addFailure(info, DTOConstants.FAILURE_REASON_SERVICE_NOT_GETTABLE); + } + } + else + { + handlerList.add(handler); + Collections.sort(handlerList); + this.contextMap.put(info.getName(), handlerList); + + this.failureStateHandler.addFailure(info, FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE); + } + } + } + else + { + this.failureStateHandler.addFailure(info, FAILURE_REASON_VALIDATION_FAILED); + } + updateRuntimeChangeCount(); + return true; + } + return false; + } + + /** + * Remove a servlet context helper + * + * @param The servlet context helper info + */ + public void removeContextHelper(final ServletContextHelperInfo info) + { + if ( info.isValid() ) + { + synchronized ( this.contextMap ) + { + final List handlerList = this.contextMap.get(info.getName()); + if ( handlerList != null ) + { + final Iterator i = handlerList.iterator(); + boolean first = true; + boolean activateNext = false; + while ( i.hasNext() ) + { + final WhiteboardContextHandler handler = i.next(); + if ( handler.getContextInfo().equals(info) ) + { + i.remove(); + // check for deactivate + if ( first ) + { + this.deactivate(handler); + activateNext = true; + } + break; + } + first = false; + } + if ( handlerList.isEmpty() ) + { + this.contextMap.remove(info.getName()); + } + else if ( activateNext ) + { + // Try to activate next + boolean done = false; + while ( !handlerList.isEmpty() && !done) + { + final WhiteboardContextHandler newHead = handlerList.get(0); + this.failureStateHandler.removeAll(newHead.getContextInfo()); + + if ( this.activate(newHead) ) + { + done = true; + } + else + { + handlerList.remove(0); + + this.failureStateHandler.addFailure(newHead.getContextInfo(), DTOConstants.FAILURE_REASON_SERVICE_NOT_GETTABLE); + } + } + } + } + } + } + this.failureStateHandler.removeAll(info); + updateRuntimeChangeCount(); + } + + /** + * Find the list of matching contexts for the whiteboard service + */ + private List getMatchingContexts(final WhiteboardServiceInfo info) + { + final List result = new ArrayList<>(); + for(final List handlerList : this.contextMap.values()) + { + final WhiteboardContextHandler h = handlerList.get(0); + // check whether the servlet context helper is visible to the whiteboard bundle + // see chapter 140.2 + boolean visible = h.getContextInfo().getServiceId() < 0; // internal ones are always visible + if ( !visible ) + { + final String filterString = "(" + Constants.SERVICE_ID + "=" + String.valueOf(h.getContextInfo().getServiceId()) + ")"; + try + { + final Collection> col = info.getServiceReference().getBundle().getBundleContext().getServiceReferences(ServletContextHelper.class, filterString); + if ( !col.isEmpty() ) + { + visible = true; + } + } + catch ( final InvalidSyntaxException ise ) + { + // we ignore this and treat it as an invisible service + } + } + if ( visible ) + { + if ( h.getContextInfo().getServiceReference() != null ) + { + if ( info.getContextSelectionFilter().match(h.getContextInfo().getServiceReference()) ) + { + result.add(h); + } + } + else + { + final Map props = new HashMap<>(); + props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME, h.getContextInfo().getName()); + props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_PATH, h.getContextInfo().getPath()); + props.put(HttpWhiteboardConstants.HTTP_SERVICE_CONTEXT_PROPERTY, h.getContextInfo().getName()); + + if ( info.getContextSelectionFilter().matches(props) ) + { + result.add(h); + } + } + } + } + return result; + } + + /** + * Add new whiteboard service to the registry + * + * @param info Whiteboard service info + * @return {@code true} if it matches this http service runtime + */ + public boolean addWhiteboardService(@NotNull final WhiteboardServiceInfo info) + { + // no logging and no DTO if other target service + if ( isMatchingService(info) ) + { + if ( info.isValid() ) + { + if ( info instanceof PreprocessorInfo ) + { + final PreprocessorHandler handler = new PreprocessorHandler(this.httpBundleContext, + this.webContext, ((PreprocessorInfo)info)); + final int result = handler.init(); + if ( result == -1 ) + { + synchronized ( this.preprocessorHandlers ) + { + final List newList = new ArrayList<>(this.preprocessorHandlers); + newList.add(handler); + Collections.sort(newList); + this.preprocessorHandlers = newList; + } + } + else + { + this.failureStateHandler.addFailure(info, FAILURE_REASON_VALIDATION_FAILED); + } + updateRuntimeChangeCount(); + return true; + } + synchronized ( this.contextMap ) + { + final List handlerList = this.getMatchingContexts(info); + this.servicesMap.put(info, handlerList); + if (handlerList.isEmpty()) + { + this.failureStateHandler.addFailure(info, FAILURE_REASON_NO_SERVLET_CONTEXT_MATCHING); + } + else + { + for(final WhiteboardContextHandler h : handlerList) + { + final int result = this.checkForServletRegistrationInHttpServiceContext(h, info); + if ( result == -1) + { + this.registerWhiteboardService(h, info); + if ( info instanceof ListenerInfo && ((ListenerInfo)info).isListenerType(ServletContextListener.class.getName()) ) + { + final ListenerHandler handler = h.getRegistry().getEventListenerRegistry().getServletContextListener((ListenerInfo)info); + if ( handler != null ) + { + final ServletContextListener listener = (ServletContextListener)handler.getListener(); + if ( listener != null ) + { + EventListenerRegistry.contextInitialized(handler.getListenerInfo(), listener, new ServletContextEvent(handler.getContext())); + } + } + } + } + } + } + } + } + else + { + this.failureStateHandler.addFailure(info, FAILURE_REASON_VALIDATION_FAILED); + } + updateRuntimeChangeCount(); + return true; + } + return false; + } + + /** + * Check if a registration for a servlet or resource is tried against the http context + * of the http service + * @param h The handler + * @param info The info + * @return {@code -1} if everything is ok, error code otherwise + */ + private int checkForServletRegistrationInHttpServiceContext(final WhiteboardContextHandler h, + final WhiteboardServiceInfo info) + { + if ( h.getContextInfo().getServiceId() == HttpServiceFactory.HTTP_SERVICE_CONTEXT_SERVICE_ID ) + { + // In order to be compatible with the implementation of the http service 1.0 + // we need still support servlet/resource registrations not using the + // 1.1 HTTP_SERVICE_CONTEXT_PROPERTY property. (contains is not the best check but + // it should do the trick) + if ( info instanceof ResourceInfo && info.getContextSelection().contains(HttpWhiteboardConstants.HTTP_SERVICE_CONTEXT_PROPERTY)) + { + this.failureStateHandler.addFailure(info, HttpServiceFactory.HTTP_SERVICE_CONTEXT_SERVICE_ID, DTOConstants.FAILURE_REASON_VALIDATION_FAILED); + + return DTOConstants.FAILURE_REASON_VALIDATION_FAILED; + } + else if ( info instanceof ServletInfo && info.getContextSelection().contains(HttpWhiteboardConstants.HTTP_SERVICE_CONTEXT_PROPERTY)) + { + final ServletInfo servletInfo = (ServletInfo)info; + final boolean nameIsEmpty = servletInfo.getName() == null || servletInfo.getName().isEmpty(); + final boolean errorPageIsEmpty = servletInfo.getErrorPage() == null || servletInfo.getErrorPage().length == 0; + final boolean patternIsEmpty = servletInfo.getPatterns() == null || servletInfo.getPatterns().length == 0; + if ( !nameIsEmpty || !errorPageIsEmpty ) + { + if ( patternIsEmpty ) + { + // no pattern, so this is valid + return -1; + } + } + + // pattern is invalid, regardless of the other values + this.failureStateHandler.addFailure(info, HttpServiceFactory.HTTP_SERVICE_CONTEXT_SERVICE_ID, DTOConstants.FAILURE_REASON_VALIDATION_FAILED); + + return DTOConstants.FAILURE_REASON_VALIDATION_FAILED; + } + } + + return -1; + } + + /** + * Remove whiteboard service from the registry. + * + * @param info The service id of the whiteboard service + */ + public void removeWhiteboardService(final WhiteboardServiceInfo info ) + { + synchronized ( this.contextMap ) + { + if ( !failureStateHandler.remove(info) ) + { + if ( info instanceof PreprocessorInfo ) + { + synchronized ( this.preprocessorHandlers ) + { + final List newList = new ArrayList<>(this.preprocessorHandlers); + final Iterator iter = newList.iterator(); + while ( iter.hasNext() ) + { + final PreprocessorHandler handler = iter.next(); + if ( handler.getPreprocessorInfo().compareTo((PreprocessorInfo)info) == 0 ) + { + iter.remove(); + this.preprocessorHandlers = newList; + updateRuntimeChangeCount(); + return; + } + } + // not found, nothing to do + } + return; + } + final List handlerList = this.servicesMap.remove(info); + if ( handlerList != null ) + { + for(final WhiteboardContextHandler h : handlerList) + { + if ( !failureStateHandler.remove(info, h.getContextInfo().getServiceId()) ) + { + if ( info instanceof ListenerInfo && ((ListenerInfo)info).isListenerType(ServletContextListener.class.getName()) ) + { + final ListenerHandler handler = h.getRegistry().getEventListenerRegistry().getServletContextListener((ListenerInfo)info); + if ( handler != null ) + { + final ServletContextListener listener = (ServletContextListener) handler.getListener(); + if ( listener != null ) + { + EventListenerRegistry.contextDestroyed(handler.getListenerInfo(), listener, new ServletContextEvent(handler.getContext())); + } + } + } + this.unregisterWhiteboardService(h, info); + } + } + } + } + this.failureStateHandler.removeAll(info); + } + updateRuntimeChangeCount(); + } + + /** + * Register whiteboard service in the http service + * @param handler Context handler + * @param info Whiteboard service info + */ + private void registerWhiteboardService(final WhiteboardContextHandler handler, final WhiteboardServiceInfo info) + { + try + { + int failureCode = -1; + if ( info instanceof ServletInfo ) + { + final ExtServletContext servletContext = handler.getServletContext(info.getServiceReference().getBundle()); + if ( servletContext == null ) + { + failureCode = DTOConstants.FAILURE_REASON_SERVLET_CONTEXT_FAILURE; + } + else + { + final ServletHandler servletHandler = new WhiteboardServletHandler( + handler.getContextInfo().getServiceId(), + servletContext, + (ServletInfo)info, + handler.getBundleContext(), + info.getServiceReference().getBundle(), + this.httpBundleContext.getBundle()); + handler.getRegistry().registerServlet(servletHandler); + } + } + else if ( info instanceof FilterInfo ) + { + final ExtServletContext servletContext = handler.getServletContext(info.getServiceReference().getBundle()); + if ( servletContext == null ) + { + failureCode = DTOConstants.FAILURE_REASON_SERVLET_CONTEXT_FAILURE; + } + else + { + final FilterHandler filterHandler = new WhiteboardFilterHandler( + handler.getContextInfo().getServiceId(), + servletContext, + (FilterInfo)info, + handler.getBundleContext()); + handler.getRegistry().registerFilter(filterHandler); + } + } + else if ( info instanceof ResourceInfo ) + { + final ServletInfo servletInfo = ((ResourceInfo)info).getServletInfo(); + final ExtServletContext servletContext = handler.getServletContext(info.getServiceReference().getBundle()); + if ( servletContext == null ) + { + failureCode = DTOConstants.FAILURE_REASON_SERVLET_CONTEXT_FAILURE; + } + else + { + final ServletHandler servleHandler = new HttpServiceServletHandler( + handler.getContextInfo().getServiceId(), + servletContext, + servletInfo, + new ResourceServlet(servletInfo.getPrefix())); + handler.getRegistry().registerServlet(servleHandler); + } + } + + else if ( info instanceof ListenerInfo ) + { + final ExtServletContext servletContext = handler.getServletContext(info.getServiceReference().getBundle()); + if ( servletContext == null ) + { + failureCode = DTOConstants.FAILURE_REASON_SERVLET_CONTEXT_FAILURE; + } + else + { + final ListenerHandler listenerHandler = new WhiteboardListenerHandler( + handler.getContextInfo().getServiceId(), + servletContext, + (ListenerInfo)info, + handler.getBundleContext()); + handler.getRegistry().registerListeners(listenerHandler); + } + } + else + { + // This should never happen, but we log anyway + SystemLogger.error("Unknown whiteboard service " + info.getServiceReference(), null); + } + if ( failureCode != -1 ) + { + this.failureStateHandler.addFailure(info, handler.getContextInfo().getServiceId(), failureCode); + } + } + catch (final Exception e) + { + this.failureStateHandler.addFailure(info, handler.getContextInfo().getServiceId(), FAILURE_REASON_UNKNOWN, e); + } + } + + /** + * Unregister whiteboard service from the http service + * @param handler Context handler + * @param info Whiteboard service info + */ + private void unregisterWhiteboardService(final WhiteboardContextHandler handler, final WhiteboardServiceInfo info) + { + try + { + if ( info instanceof ServletInfo ) + { + handler.getRegistry().unregisterServlet((ServletInfo)info, true); + handler.ungetServletContext(info.getServiceReference().getBundle()); + } + else if ( info instanceof FilterInfo ) + { + handler.getRegistry().unregisterFilter((FilterInfo)info, true); + handler.ungetServletContext(info.getServiceReference().getBundle()); + } + else if ( info instanceof ResourceInfo ) + { + handler.getRegistry().unregisterServlet(((ResourceInfo)info).getServletInfo(), true); + handler.ungetServletContext(info.getServiceReference().getBundle()); + } + + else if ( info instanceof ListenerInfo ) + { + handler.getRegistry().unregisterListeners((ListenerInfo) info); + handler.ungetServletContext(info.getServiceReference().getBundle()); + } + } + catch (final Exception e) + { + SystemLogger.error("Exception while unregistering whiteboard service " + info.getServiceReference(), e); + } + + } + + /** + * Check whether the service is specifying a target http service runtime + * and if so if that is matching this runtime + */ + private boolean isMatchingService(final AbstractInfo info) + { + final String target = info.getTarget(); + if ( target != null ) + { + try + { + final Filter f = this.httpBundleContext.createFilter(target); + return f.match(this.serviceRuntime.getServiceReference()); + } + catch ( final InvalidSyntaxException ise) + { + // log and ignore service + SystemLogger.error("Invalid target filter expression for " + info.getServiceReference() + " : " + target, ise); + return false; + } + } + return true; + } + + private WhiteboardContextHandler getContextHandler(final String name) + { + synchronized ( this.contextMap ) + { + for(final List handlerList : this.contextMap.values()) + { + final WhiteboardContextHandler h = handlerList.get(0); + if ( h.getContextInfo().getName().equals(name) ) + { + return h; + } + } + } + return null; + } + + public RegistryRuntime getRuntimeInfo() + { + final FailedDTOHolder failedDTOHolder = new FailedDTOHolder(); + + final Collection contextDTOs = new ArrayList<>(); + + // get sort list of context handlers + final List contextHandlerList = new ArrayList<>(); + synchronized ( this.contextMap ) + { + for (final List list : this.contextMap.values()) + { + if ( !list.isEmpty() ) + { + contextHandlerList.add(list.get(0)); + } + } + this.failureStateHandler.getRuntimeInfo(failedDTOHolder); + } + Collections.sort(contextHandlerList); + + for (final WhiteboardContextHandler handler : contextHandlerList) + { + final ServletContextDTO scDTO = ServletContextDTOBuilder.build(handler.getContextInfo(), handler.getSharedContext(), -1); + + if ( registry.getRuntimeInfo(scDTO, failedDTOHolder) ) + { + contextDTOs.add(scDTO); + } + } + + final List preprocessorDTOs = new ArrayList<>(); + final List localHandlers = this.preprocessorHandlers; + for(final PreprocessorHandler handler : localHandlers) + { + preprocessorDTOs.add(PreprocessorDTOBuilder.build(handler.getPreprocessorInfo(), -1)); + } + + return new RegistryRuntime(failedDTOHolder, contextDTOs, preprocessorDTOs); + } + + /** + * Invoke all preprocessors + * + * @param req The request + * @param res The response + * @return {@code true} to continue with dispatching, {@code false} to terminate the request. + * @throws IOException + * @throws ServletException + */ + public void invokePreprocessors(final HttpServletRequest req, + final HttpServletResponse res, + final Preprocessor dispatcher) + throws ServletException, IOException + { + final List localHandlers = this.preprocessorHandlers; + if ( localHandlers.isEmpty() ) + { + // no preprocessors, we can directly execute + dispatcher.doFilter(req, res, null); + } + else + { + final FilterChain chain = new FilterChain() + { + private int index = 0; + + @Override + public void doFilter(final ServletRequest request, final ServletResponse response) + throws IOException, ServletException + { + if ( index == localHandlers.size() ) + { + dispatcher.doFilter(request, response, null); + } + else + { + final PreprocessorHandler handler = localHandlers.get(index); + index++; + handler.handle(request, response, this); + } + } + }; + chain.doFilter(req, res); + } + } + + private void updateRuntimeChangeCount() + { + this.serviceRuntime.updateChangeCount(); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/FilterTracker.java b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/FilterTracker.java new file mode 100644 index 00000000000..3a10f33942f --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/FilterTracker.java @@ -0,0 +1,43 @@ +/* + * 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.http.base.internal.whiteboard.tracker; + +import javax.servlet.Filter; + +import org.apache.felix.http.base.internal.runtime.FilterInfo; +import org.apache.felix.http.base.internal.runtime.WhiteboardServiceInfo; +import org.apache.felix.http.base.internal.whiteboard.WhiteboardManager; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; + +public final class FilterTracker extends WhiteboardServiceTracker +{ + public FilterTracker(final BundleContext context, final WhiteboardManager manager) + { + super(manager, context, String.format("(&(objectClass=%s)(|(%s=*)(%s=*)(%s=*)))", + Filter.class.getName(), + HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_PATTERN, + HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_REGEX, + HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_SERVLET)); + } + + @Override + protected WhiteboardServiceInfo getServiceInfo(final ServiceReference ref) { + return new FilterInfo(ref); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/ListenersTracker.java b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/ListenersTracker.java new file mode 100644 index 00000000000..e758f7bce6d --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/ListenersTracker.java @@ -0,0 +1,66 @@ +/* + * 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.http.base.internal.whiteboard.tracker; + +import java.util.EventListener; + +import javax.servlet.ServletContextAttributeListener; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletRequestAttributeListener; +import javax.servlet.ServletRequestListener; +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionIdListener; +import javax.servlet.http.HttpSessionListener; + +import org.apache.felix.http.base.internal.runtime.ListenerInfo; +import org.apache.felix.http.base.internal.runtime.WhiteboardServiceInfo; +import org.apache.felix.http.base.internal.whiteboard.WhiteboardManager; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; + +public final class ListenersTracker extends WhiteboardServiceTracker +{ + /** + * Create a filter expression for all supported listener. + */ + private static String createListenersFilterExpression() + { + return String.format("(&" + + "(|(objectClass=%s)(objectClass=%s)(objectClass=%s)(objectClass=%s)(objectClass=%s)(objectClass=%s)(objectClass=%s))" + + "(%s=*)(!(%s~=false)))", + HttpSessionAttributeListener.class.getName(), + HttpSessionIdListener.class.getName(), + HttpSessionListener.class.getName(), + ServletContextListener.class.getName(), + ServletContextAttributeListener.class.getName(), + ServletRequestListener.class.getName(), + ServletRequestAttributeListener.class.getName(), + HttpWhiteboardConstants.HTTP_WHITEBOARD_LISTENER, + HttpWhiteboardConstants.HTTP_WHITEBOARD_LISTENER); + } + + public ListenersTracker(final BundleContext context, final WhiteboardManager manager) + { + super(manager, context, createListenersFilterExpression()); + } + + @Override + protected WhiteboardServiceInfo getServiceInfo(final ServiceReference ref) { + return new ListenerInfo(ref); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/PreprocessorTracker.java b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/PreprocessorTracker.java new file mode 100644 index 00000000000..3df51663496 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/PreprocessorTracker.java @@ -0,0 +1,45 @@ +/* + * 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.http.base.internal.whiteboard.tracker; + +import org.apache.felix.http.base.internal.runtime.PreprocessorInfo; +import org.apache.felix.http.base.internal.runtime.WhiteboardServiceInfo; +import org.apache.felix.http.base.internal.whiteboard.WhiteboardManager; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.whiteboard.Preprocessor; + +public final class PreprocessorTracker extends WhiteboardServiceTracker +{ + + /** + * Create a new tracker + * @param bundleContext The bundle context. + * @param contextManager The context manager + */ + public PreprocessorTracker(final BundleContext bundleContext, final WhiteboardManager contextManager) + { + super(contextManager, bundleContext, + String.format("(objectClass=%s)", Preprocessor.class.getName())); + } + + @Override + protected WhiteboardServiceInfo getServiceInfo(final ServiceReference ref) + { + return new PreprocessorInfo(ref); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/ResourceTracker.java b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/ResourceTracker.java new file mode 100644 index 00000000000..e902d06c875 --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/ResourceTracker.java @@ -0,0 +1,39 @@ +/* + * 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.http.base.internal.whiteboard.tracker; + +import org.apache.felix.http.base.internal.runtime.ResourceInfo; +import org.apache.felix.http.base.internal.runtime.WhiteboardServiceInfo; +import org.apache.felix.http.base.internal.whiteboard.WhiteboardManager; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; + +public final class ResourceTracker extends WhiteboardServiceTracker +{ + public ResourceTracker(final BundleContext context, final WhiteboardManager manager) + { + super(manager, context, String.format("(&(%s=*)(%s=*))", + HttpWhiteboardConstants.HTTP_WHITEBOARD_RESOURCE_PATTERN, + HttpWhiteboardConstants.HTTP_WHITEBOARD_RESOURCE_PREFIX)); + } + + @Override + protected WhiteboardServiceInfo getServiceInfo(final ServiceReference ref) { + return new ResourceInfo(ref); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/ServletContextHelperTracker.java b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/ServletContextHelperTracker.java new file mode 100644 index 00000000000..913c46f5f9d --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/ServletContextHelperTracker.java @@ -0,0 +1,106 @@ +/* + * 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.http.base.internal.whiteboard.tracker; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.jetbrains.annotations.NotNull; + +import org.apache.felix.http.base.internal.runtime.ServletContextHelperInfo; +import org.apache.felix.http.base.internal.whiteboard.WhiteboardManager; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.context.ServletContextHelper; +import org.osgi.util.tracker.ServiceTracker; + +/** + * Tracks all {@link ServletContextHelper} services. + */ +public final class ServletContextHelperTracker extends ServiceTracker> +{ + private final WhiteboardManager contextManager; + + /** Map containing all info objects reported from the trackers. */ + private final Map allInfos = new ConcurrentHashMap(); + + private static org.osgi.framework.Filter createFilter(final BundleContext btx) + { + try + { + return btx.createFilter(String.format("(objectClass=%s)", + ServletContextHelper.class.getName())); + } + catch ( final InvalidSyntaxException ise) + { + // we can safely ignore it as the above filter is a constant + } + return null; // we never get here - and if we get an NPE which is fine + } + + public ServletContextHelperTracker(@NotNull final BundleContext context, @NotNull final WhiteboardManager manager) + { + super(context, createFilter(context), null); + this.contextManager = manager; + } + + @Override + public void close() { + super.close(); + this.allInfos.clear(); + } + + @Override + public final ServiceReference addingService(@NotNull final ServiceReference ref) + { + this.added(ref); + return ref; + } + + @Override + public final void modifiedService(@NotNull final ServiceReference ref, @NotNull final ServiceReference service) + { + this.removed(ref); + this.added(ref); + } + + @Override + public final void removedService(@NotNull final ServiceReference ref, @NotNull final ServiceReference service) + { + this.removed(ref); + } + + private void added(@NotNull final ServiceReference ref) + { + final ServletContextHelperInfo info = new ServletContextHelperInfo(ref); + if ( this.contextManager.addContextHelper(info) ) + { + this.allInfos.put((Long)ref.getProperty(Constants.SERVICE_ID), info); + } + } + + private void removed(@NotNull final ServiceReference ref) + { + final ServletContextHelperInfo info = this.allInfos.get(ref.getProperty(Constants.SERVICE_ID)); + if ( info != null ) + { + this.contextManager.removeContextHelper(info); + } + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/ServletTracker.java b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/ServletTracker.java new file mode 100644 index 00000000000..800fa35e83d --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/ServletTracker.java @@ -0,0 +1,43 @@ +/* + * 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.http.base.internal.whiteboard.tracker; + +import javax.servlet.Servlet; + +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.apache.felix.http.base.internal.runtime.WhiteboardServiceInfo; +import org.apache.felix.http.base.internal.whiteboard.WhiteboardManager; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; + +public final class ServletTracker extends WhiteboardServiceTracker +{ + public ServletTracker(final BundleContext context, final WhiteboardManager manager) + { + super(manager, context, String.format("(&(objectClass=%s)(|(%s=*)(%s=*)(%s=*)))", + Servlet.class.getName(), + HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_NAME, + HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, + HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_ERROR_PAGE)); + } + + @Override + protected WhiteboardServiceInfo getServiceInfo(final ServiceReference ref) { + return new ServletInfo(ref); + } +} diff --git a/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/WhiteboardServiceTracker.java b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/WhiteboardServiceTracker.java new file mode 100644 index 00000000000..d673d2312bd --- /dev/null +++ b/http/base/src/main/java/org/apache/felix/http/base/internal/whiteboard/tracker/WhiteboardServiceTracker.java @@ -0,0 +1,125 @@ +/* + * 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.http.base.internal.whiteboard.tracker; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.felix.http.base.internal.runtime.WhiteboardServiceInfo; +import org.apache.felix.http.base.internal.whiteboard.WhiteboardManager; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; + +/** + * Service tracker for all whiteboard services except servlet context helper. + * This tracker does not get/unget the service objects itself, but just forwards the service reference + * by creating an info data object. Each sub class creates a different + * data object. + */ +public abstract class WhiteboardServiceTracker extends ServiceTracker> +{ + /** Map containing all info objects reported from the trackers. */ + private final Map> allInfos = new ConcurrentHashMap>(); + + private static org.osgi.framework.Filter createFilter(final BundleContext btx, final String expr) + { + try + { + return btx.createFilter(expr); + } + catch ( final InvalidSyntaxException ise) + { + // we can safely ignore it as the filter is a constant + } + return null; // we never get here - and if we get an NPE which is fine + } + + /** The manager is called for each added/removed reference. */ + private final WhiteboardManager contextManager; + + /** + * Create a new tracker + * @param contextManager The context manager + * @param bundleContext The bundle context. + * @param filterExpr The filter expression for the services to track + */ + public WhiteboardServiceTracker(final WhiteboardManager contextManager, + final BundleContext bundleContext, final String filterExpr) + { + super(bundleContext, createFilter(bundleContext, filterExpr), null); + this.contextManager = contextManager; + } + + @Override + public void close() { + super.close(); + this.allInfos.clear(); + } + + @Override + public final ServiceReference addingService(final ServiceReference ref) + { + this.added(ref); + return ref; + } + + @Override + public final void modifiedService(final ServiceReference ref, final ServiceReference service) + { + this.modified(ref); + } + + @Override + public final void removedService(final ServiceReference ref, final ServiceReference service) + { + this.removed(ref); + } + + private void modified(final ServiceReference ref) + { + removed(ref); + added(ref); + } + + private void added(final ServiceReference ref) + { + final WhiteboardServiceInfo info = this.getServiceInfo(ref); + if ( this.contextManager.addWhiteboardService(info) ) + { + this.allInfos.put((Long)ref.getProperty(Constants.SERVICE_ID), info); + } + } + + private void removed(final ServiceReference ref) + { + final WhiteboardServiceInfo info = this.allInfos.remove(ref.getProperty(Constants.SERVICE_ID)); + if ( info != null ) + { + this.contextManager.removeWhiteboardService(info); + } + } + + /** + * Implemented by sub classes to create the correct whiteboard service info object. + * @param ref The service reference + * @return A whiteboard service info + */ + protected abstract WhiteboardServiceInfo getServiceInfo(final ServiceReference ref); +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/context/ServletContextImplTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/context/ServletContextImplTest.java new file mode 100644 index 00000000000..78945665c36 --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/context/ServletContextImplTest.java @@ -0,0 +1,857 @@ +/* + * 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.http.base.internal.context; + +import static org.mockito.Mockito.when; + +import java.io.InputStream; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; + +import javax.servlet.Filter; +import javax.servlet.FilterRegistration; +import javax.servlet.RequestDispatcher; +import javax.servlet.Servlet; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextAttributeEvent; +import javax.servlet.ServletContextAttributeListener; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; +import javax.servlet.SessionCookieConfig; +import javax.servlet.SessionTrackingMode; +import javax.servlet.descriptor.JspConfigDescriptor; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.felix.http.base.internal.HttpConfig; +import org.apache.felix.http.base.internal.handler.ListenerHandler; +import org.apache.felix.http.base.internal.registry.EventListenerRegistry; +import org.apache.felix.http.base.internal.registry.HandlerRegistry; +import org.apache.felix.http.base.internal.registry.PerContextHandlerRegistry; +import org.apache.felix.http.base.internal.runtime.ListenerInfo; +import org.apache.felix.http.base.internal.service.HttpServiceFactory; +import org.apache.felix.http.base.internal.service.ServletContextImpl; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.osgi.framework.Bundle; +import org.osgi.service.http.HttpContext; + +public class ServletContextImplTest +{ + private static class AttributeListener implements ServletContextAttributeListener + { + + private int type; + + private String name; + + private Object value; + + @Override + public void attributeAdded(ServletContextAttributeEvent scab) + { + setData(1, scab); + } + + @Override + public void attributeRemoved(ServletContextAttributeEvent scab) + { + setData(2, scab); + } + + @Override + public void attributeReplaced(ServletContextAttributeEvent scab) + { + setData(3, scab); + } + + void checkAdded(String name, Object value) + { + check(1, name, value); + } + + void checkNull() + { + check(0, null, null); + } + + void checkRemoved(String name, Object value) + { + check(2, name, value); + } + + void checkReplaced(String name, Object value) + { + check(3, name, value); + } + + private void check(int type, String name, Object value) + { + try + { + Assert.assertEquals(type, this.type); + Assert.assertEquals(name, this.name); + Assert.assertEquals(value, this.value); + } + finally + { + this.type = 0; + this.name = null; + this.value = null; + } + } + + private void setData(int type, ServletContextAttributeEvent scab) + { + this.type = type; + this.name = scab.getName(); + this.value = scab.getValue(); + } + } + private class MockServletContext implements ServletContext + { + + private Dictionary attributes = new Hashtable<>(); + + @Override + public FilterRegistration.Dynamic addFilter(String name, Class type) + { + return null; + } + + @Override + public FilterRegistration.Dynamic addFilter(String name, Filter filter) + { + return null; + } + + @Override + public FilterRegistration.Dynamic addFilter(String name, String className) + { + return null; + } + + @Override + public void addListener(Class listener) + { + } + + @Override + public void addListener(String className) + { + } + + @Override + public void addListener(T listener) + { + } + + @Override + public ServletRegistration.Dynamic addServlet(String name, Class type) + { + return null; + } + + @Override + public ServletRegistration.Dynamic addServlet(String name, Servlet servlet) + { + return null; + } + + @Override + public ServletRegistration.Dynamic addServlet(String name, String className) + { + return null; + } + + @Override + public T createFilter(Class type) throws ServletException + { + return null; + } + + @Override + public T createListener(Class type) throws ServletException + { + return null; + } + + @Override + public T createServlet(Class type) throws ServletException + { + return null; + } + + @Override + public void declareRoles(String... roleNames) + { + } + + @Override + public String getVirtualServerName() { + return null; + } + + @Override + public Object getAttribute(String name) + { + return attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() + { + return attributes.keys(); + } + + @Override + public ClassLoader getClassLoader() + { + return ServletContextImplTest.class.getClassLoader(); + } + + @Override + public ServletContext getContext(String uripath) + { + return null; + } + + @Override + public String getContextPath() + { + return null; + } + + @Override + public Set getDefaultSessionTrackingModes() + { + return null; + } + + @Override + public int getEffectiveMajorVersion() + { + return 0; + } + + @Override + public int getEffectiveMinorVersion() + { + return 0; + } + + @Override + public Set getEffectiveSessionTrackingModes() + { + return null; + } + + @Override + public FilterRegistration getFilterRegistration(String name) + { + return null; + } + + @Override + public Map getFilterRegistrations() + { + return null; + } + + @Override + public String getInitParameter(String name) + { + return null; + } + + @Override + public Enumeration getInitParameterNames() + { + return Collections.enumeration(Collections.emptyList()); + } + + @Override + public JspConfigDescriptor getJspConfigDescriptor() + { + return null; + } + + @Override + public int getMajorVersion() + { + return 0; + } + + @Override + public String getMimeType(String file) + { + return null; + } + + @Override + public int getMinorVersion() + { + return 0; + } + + @Override + public RequestDispatcher getNamedDispatcher(String name) + { + return null; + } + + @Override + public String getRealPath(String path) + { + return null; + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) + { + return null; + } + + @Override + public URL getResource(String path) + { + return null; + } + + @Override + public InputStream getResourceAsStream(String path) + { + return null; + } + + @Override + public Set getResourcePaths(String path) + { + return null; + } + + @Override + public String getServerInfo() + { + return null; + } + + @Deprecated + @Override + public Servlet getServlet(String name) + { + return null; + } + + @Override + public String getServletContextName() + { + return null; + } + + @SuppressWarnings("deprecation") + @Deprecated + @Override + public Enumeration getServletNames() + { + return Collections.enumeration(Collections.emptyList()); + } + + @Override + public ServletRegistration getServletRegistration(String name) + { + return null; + } + + @Override + public Map getServletRegistrations() + { + return null; + } + + @SuppressWarnings("deprecation") + @Deprecated + @Override + public Enumeration getServlets() + { + return Collections.enumeration(Collections.emptyList()); + } + + @Override + public SessionCookieConfig getSessionCookieConfig() + { + return null; + } + + @SuppressWarnings("deprecation") + @Deprecated + @Override + public void log(Exception exception, String msg) + { + } + + @Override + public void log(String msg) + { + } + + @Override + public void log(String message, Throwable throwable) + { + } + + @Override + public void removeAttribute(String name) + { + attributes.remove(name); + } + + @Override + public void setAttribute(String name, Object object) + { + if (object != null) + { + attributes.put(name, object); + } + else + { + removeAttribute(name); + } + } + + @Override + public boolean setInitParameter(String name, String value) + { + return false; + } + + @Override + public void setSessionTrackingModes(Set modes) + { + } + } + + private Bundle bundle; + private HttpContext httpContext; + private AttributeListener listener; + private ServletContextImpl context; + private PerContextHandlerRegistry contextRegistry; + + @Before + public void setUp() + { + this.bundle = Mockito.mock(Bundle.class); + ServletContext globalContext = new MockServletContext(); + this.httpContext = Mockito.mock(HttpContext.class); + this.listener = new AttributeListener(); + final HandlerRegistry reg = new HandlerRegistry(new HttpConfig()); + reg.init(); + contextRegistry = reg.getRegistry(HttpServiceFactory.HTTP_SERVICE_CONTEXT_SERVICE_ID); + final EventListenerRegistry eventReg = contextRegistry.getEventListenerRegistry(); + final ListenerInfo info = Mockito.mock(ListenerInfo.class); + when(info.getListenerTypes()).thenReturn(new String[] {ServletContextAttributeListener.class.getName()}); + when(info.isListenerType(ServletContextAttributeListener.class.getName())).thenReturn(true); + final ListenerHandler handler = Mockito.mock(ListenerHandler.class); + when(handler.getListenerInfo()).thenReturn(info); + when(handler.getContextServiceId()).thenReturn(HttpServiceFactory.HTTP_SERVICE_CONTEXT_SERVICE_ID); + when(handler.getListener()).thenReturn(listener); + when(handler.init()).thenReturn(-1); + eventReg.addListeners(handler); + this.context = new ServletContextImpl(this.bundle, globalContext, this.httpContext, false, contextRegistry); + } + + @Test + public void testGetAttribute() + { + Assert.assertNull(this.context.getAttribute("key1")); + + this.context.setAttribute("key1", "value1"); + this.listener.checkAdded("key1", "value1"); + Assert.assertEquals("value1", this.context.getAttribute("key1")); + + this.context.removeAttribute("key1"); + this.listener.checkRemoved("key1", "value1"); + Assert.assertNull(this.context.getAttribute("key1")); + + this.context.setAttribute("key1", null); + this.listener.checkNull(); + Assert.assertNull(this.context.getAttribute("key1")); + + this.context.setAttribute("key1", "value1"); + this.listener.checkAdded("key1", "value1"); + Assert.assertEquals("value1", this.context.getAttribute("key1")); + + this.context.setAttribute("key1", "newValue"); + this.listener.checkReplaced("key1", "value1"); + Assert.assertEquals("newValue", this.context.getAttribute("key1")); + } + + @Test + public void testGetAttributeNames() + { + Enumeration e = this.context.getAttributeNames(); + Assert.assertNotNull(e); + Assert.assertFalse(e.hasMoreElements()); + + this.context.setAttribute("key1", "value1"); + this.listener.checkAdded("key1", "value1"); + e = this.context.getAttributeNames(); + Assert.assertNotNull(e); + Assert.assertTrue(e.hasMoreElements()); + Assert.assertEquals("key1", e.nextElement()); + Assert.assertFalse(e.hasMoreElements()); + } + + @Test + public void testGetInitParameter() + { + Assert.assertNull(this.context.getInitParameter("key1")); + } + + @Test + public void testGetInitParameterNames() + { + Enumeration e = this.context.getInitParameterNames(); + Assert.assertNotNull(e); + Assert.assertFalse(e.hasMoreElements()); + } + + @Test + public void testGetMimeType() + { + Mockito.when(this.httpContext.getMimeType("file.xml")).thenReturn("some-other-format"); + Assert.assertEquals("some-other-format", this.context.getMimeType("file.xml")); + Assert.assertEquals("text/plain", this.context.getMimeType("file.txt")); + } + + @Test + public void testGetRealPath() + { + Assert.assertNull(this.context.getRealPath("path")); + } + + @Test + public void testGetResource() throws Exception + { + URL url = getClass().getResource("resource.txt"); + Assert.assertNotNull(url); + + Mockito.when(this.httpContext.getResource("/resource.txt")).thenReturn(url); + Assert.assertNull(this.context.getResource("/notfound.txt")); + Assert.assertEquals(url, this.context.getResource("/resource.txt")); + } + + @Test + public void testGetResourceAsStream() throws Exception + { + URL url = getClass().getResource("resource.txt"); + Assert.assertNotNull(url); + + Mockito.when(this.httpContext.getResource("/resource.txt")).thenReturn(url); + Assert.assertNull(this.context.getResourceAsStream("/notfound.txt")); + Assert.assertNotNull(this.context.getResourceAsStream("/resource.txt")); + } + + @Test + public void testGetResourcePaths() + { + HashSet paths = new HashSet<>(Arrays.asList("/some/path/1", "/some/path/2")); + Mockito.when(this.bundle.getEntryPaths("some/path")).thenReturn(Collections.enumeration(paths)); + + Set set = this.context.getResourcePaths("/some/path"); + Assert.assertNotNull(set); + Assert.assertEquals(2, set.size()); + Assert.assertTrue(set.contains("/some/path/1")); + Assert.assertTrue(set.contains("/some/path/2")); + } + + @Test + public void testGetServlet() throws Exception + { + Assert.assertNull(this.context.getServlet("test")); + } + + @Test + public void testGetServletNames() + { + Enumeration e = this.context.getServletNames(); + Assert.assertNotNull(e); + Assert.assertFalse(e.hasMoreElements()); + } + + @Test + public void testGetServlets() + { + Enumeration e = this.context.getServlets(); + Assert.assertNotNull(e); + Assert.assertFalse(e.hasMoreElements()); + } + + @Test + public void testGetSharedAttribute() + { + ServletContext globalContext = new MockServletContext(); + ServletContext ctx1 = new ServletContextImpl(bundle, globalContext, httpContext, true, + contextRegistry); + ServletContext ctx2 = new ServletContextImpl(bundle, globalContext, httpContext, true, + contextRegistry); + + Assert.assertNull(ctx1.getAttribute("key1")); + Assert.assertNull(ctx2.getAttribute("key1")); + Assert.assertNull(globalContext.getAttribute("key1")); + + // Operations on ctx1 and check results + + ctx1.setAttribute("key1", "value1"); + this.listener.checkAdded("key1", "value1"); + Assert.assertEquals("value1", ctx1.getAttribute("key1")); + Assert.assertEquals("value1", ctx2.getAttribute("key1")); + Assert.assertEquals("value1", globalContext.getAttribute("key1")); + + ctx1.removeAttribute("key1"); + this.listener.checkRemoved("key1", "value1"); + Assert.assertNull(ctx1.getAttribute("key1")); + Assert.assertNull(ctx2.getAttribute("key1")); + Assert.assertNull(globalContext.getAttribute("key1")); + + ctx1.setAttribute("key1", null); + this.listener.checkNull(); + Assert.assertNull(ctx1.getAttribute("key1")); + Assert.assertNull(ctx2.getAttribute("key1")); + Assert.assertNull(globalContext.getAttribute("key1")); + + ctx1.setAttribute("key1", "value1"); + this.listener.checkAdded("key1", "value1"); + Assert.assertEquals("value1", ctx1.getAttribute("key1")); + Assert.assertEquals("value1", ctx2.getAttribute("key1")); + Assert.assertEquals("value1", globalContext.getAttribute("key1")); + + ctx1.setAttribute("key1", "newValue"); + this.listener.checkReplaced("key1", "value1"); + Assert.assertEquals("newValue", ctx1.getAttribute("key1")); + Assert.assertEquals("newValue", ctx2.getAttribute("key1")); + Assert.assertEquals("newValue", globalContext.getAttribute("key1")); + + ctx1.removeAttribute("key1"); + + // Operations on ctx2 and check results + + ctx2.setAttribute("key1", "value1"); + this.listener.checkAdded("key1", "value1"); + Assert.assertEquals("value1", ctx1.getAttribute("key1")); + Assert.assertEquals("value1", ctx2.getAttribute("key1")); + Assert.assertEquals("value1", globalContext.getAttribute("key1")); + + ctx2.removeAttribute("key1"); + this.listener.checkRemoved("key1", "value1"); + Assert.assertNull(ctx1.getAttribute("key1")); + Assert.assertNull(ctx2.getAttribute("key1")); + Assert.assertNull(globalContext.getAttribute("key1")); + + ctx2.setAttribute("key1", null); + this.listener.checkNull(); + Assert.assertNull(ctx1.getAttribute("key1")); + Assert.assertNull(ctx2.getAttribute("key1")); + Assert.assertNull(globalContext.getAttribute("key1")); + + ctx2.setAttribute("key1", "value1"); + this.listener.checkAdded("key1", "value1"); + Assert.assertEquals("value1", ctx1.getAttribute("key1")); + Assert.assertEquals("value1", ctx2.getAttribute("key1")); + Assert.assertEquals("value1", globalContext.getAttribute("key1")); + + ctx2.setAttribute("key1", "newValue"); + this.listener.checkReplaced("key1", "value1"); + Assert.assertEquals("newValue", ctx1.getAttribute("key1")); + Assert.assertEquals("newValue", ctx2.getAttribute("key1")); + Assert.assertEquals("newValue", globalContext.getAttribute("key1")); + + ctx2.removeAttribute("key1"); + + // Operations on globalContext and check results + + globalContext.setAttribute("key1", "value1"); + Assert.assertEquals("value1", ctx1.getAttribute("key1")); + Assert.assertEquals("value1", ctx2.getAttribute("key1")); + Assert.assertEquals("value1", globalContext.getAttribute("key1")); + + globalContext.removeAttribute("key1"); + Assert.assertNull(ctx1.getAttribute("key1")); + Assert.assertNull(ctx2.getAttribute("key1")); + Assert.assertNull(globalContext.getAttribute("key1")); + + globalContext.setAttribute("key1", null); + Assert.assertNull(ctx1.getAttribute("key1")); + Assert.assertNull(ctx2.getAttribute("key1")); + Assert.assertNull(globalContext.getAttribute("key1")); + + globalContext.setAttribute("key1", "value1"); + Assert.assertEquals("value1", ctx1.getAttribute("key1")); + Assert.assertEquals("value1", ctx2.getAttribute("key1")); + Assert.assertEquals("value1", globalContext.getAttribute("key1")); + + globalContext.setAttribute("key1", "newValue"); + Assert.assertEquals("newValue", ctx1.getAttribute("key1")); + Assert.assertEquals("newValue", ctx2.getAttribute("key1")); + Assert.assertEquals("newValue", globalContext.getAttribute("key1")); + + globalContext.removeAttribute("key1"); + } + + @Test + public void testGetSharedAttributeNames() + { + ServletContext globalContext = new MockServletContext(); + ServletContext ctx1 = new ServletContextImpl(bundle, globalContext, httpContext, true, + contextRegistry); + ServletContext ctx2 = new ServletContextImpl(bundle, globalContext, httpContext, true, + contextRegistry); + + Enumeration e = ctx1.getAttributeNames(); + Assert.assertNotNull(e); + Assert.assertFalse(e.hasMoreElements()); + e = ctx2.getAttributeNames(); + Assert.assertNotNull(e); + Assert.assertFalse(e.hasMoreElements()); + e = globalContext.getAttributeNames(); + Assert.assertNotNull(e); + Assert.assertFalse(e.hasMoreElements()); + + ctx1.setAttribute("key1", "value1"); + this.listener.checkAdded("key1", "value1"); + e = ctx1.getAttributeNames(); + Assert.assertNotNull(e); + Assert.assertTrue(e.hasMoreElements()); + Assert.assertEquals("key1", e.nextElement()); + Assert.assertFalse(e.hasMoreElements()); + e = ctx2.getAttributeNames(); + Assert.assertNotNull(e); + Assert.assertTrue(e.hasMoreElements()); + Assert.assertEquals("key1", e.nextElement()); + Assert.assertFalse(e.hasMoreElements()); + e = globalContext.getAttributeNames(); + Assert.assertNotNull(e); + Assert.assertTrue(e.hasMoreElements()); + Assert.assertEquals("key1", e.nextElement()); + Assert.assertFalse(e.hasMoreElements()); + } + + @Test + public void testGetUnsharedAttribute() + { + ServletContext globalContext = new MockServletContext(); + ServletContext ctx1 = new ServletContextImpl(bundle, globalContext, httpContext, false, + contextRegistry); + ServletContext ctx2 = new ServletContextImpl(bundle, globalContext, httpContext, false, + contextRegistry); + + Assert.assertNull(ctx1.getAttribute("key1")); + Assert.assertNull(ctx2.getAttribute("key1")); + Assert.assertNull(globalContext.getAttribute("key1")); + + // Operations on ctx1 and check results + + ctx2.setAttribute("key1", "ctx2_private_value"); + globalContext.setAttribute("key1", "globalContext_private_value"); + ctx1.setAttribute("key1", "value1"); + this.listener.checkAdded("key1", "value1"); + Assert.assertEquals("value1", ctx1.getAttribute("key1")); + Assert.assertEquals("ctx2_private_value", ctx2.getAttribute("key1")); + Assert.assertEquals("globalContext_private_value", globalContext.getAttribute("key1")); + + ctx1.removeAttribute("key1"); + this.listener.checkRemoved("key1", "value1"); + Assert.assertNull(ctx1.getAttribute("key1")); + Assert.assertEquals("ctx2_private_value", ctx2.getAttribute("key1")); + Assert.assertEquals("globalContext_private_value", globalContext.getAttribute("key1")); + + ctx1.setAttribute("key1", null); + this.listener.checkNull(); + Assert.assertNull(ctx1.getAttribute("key1")); + Assert.assertEquals("ctx2_private_value", ctx2.getAttribute("key1")); + Assert.assertEquals("globalContext_private_value", globalContext.getAttribute("key1")); + + ctx1.setAttribute("key1", "value1"); + this.listener.checkAdded("key1", "value1"); + Assert.assertEquals("value1", ctx1.getAttribute("key1")); + Assert.assertEquals("ctx2_private_value", ctx2.getAttribute("key1")); + Assert.assertEquals("globalContext_private_value", globalContext.getAttribute("key1")); + + ctx1.setAttribute("key1", "newValue"); + this.listener.checkReplaced("key1", "value1"); + Assert.assertEquals("newValue", ctx1.getAttribute("key1")); + Assert.assertEquals("ctx2_private_value", ctx2.getAttribute("key1")); + Assert.assertEquals("globalContext_private_value", globalContext.getAttribute("key1")); + } + + @Test + public void testGetUnsharedAttributeNames() + { + ServletContext globalContext = new MockServletContext(); + ServletContext ctx1 = new ServletContextImpl(bundle, globalContext, httpContext, false, + contextRegistry); + ServletContext ctx2 = new ServletContextImpl(bundle, globalContext, httpContext, false, + contextRegistry); + + Enumeration e = ctx1.getAttributeNames(); + Assert.assertNotNull(e); + Assert.assertFalse(e.hasMoreElements()); + e = ctx2.getAttributeNames(); + Assert.assertNotNull(e); + Assert.assertFalse(e.hasMoreElements()); + e = globalContext.getAttributeNames(); + Assert.assertNotNull(e); + Assert.assertFalse(e.hasMoreElements()); + + ctx1.setAttribute("key1", "value1"); + this.listener.checkAdded("key1", "value1"); + e = ctx1.getAttributeNames(); + Assert.assertNotNull(e); + Assert.assertTrue(e.hasMoreElements()); + Assert.assertEquals("key1", e.nextElement()); + Assert.assertFalse(e.hasMoreElements()); + e = ctx2.getAttributeNames(); + Assert.assertNotNull(e); + Assert.assertFalse(e.hasMoreElements()); + e = globalContext.getAttributeNames(); + Assert.assertNotNull(e); + Assert.assertFalse(e.hasMoreElements()); + } + + @Test + public void testHandleSecurity() throws Exception + { + HttpServletRequest req = Mockito.mock(HttpServletRequest.class); + HttpServletResponse res = Mockito.mock(HttpServletResponse.class); + + Mockito.when(this.httpContext.handleSecurity(req, res)).thenReturn(true); + Assert.assertTrue(this.context.handleSecurity(req, res)); + + Mockito.when(this.httpContext.handleSecurity(req, res)).thenReturn(false); + Assert.assertFalse(this.context.handleSecurity(req, res)); + } +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/context/ServletContextManagerTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/context/ServletContextManagerTest.java new file mode 100644 index 00000000000..4e911a94f8e --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/context/ServletContextManagerTest.java @@ -0,0 +1,59 @@ +/* + * 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.http.base.internal.context; + +import javax.servlet.ServletContext; + +import org.apache.felix.http.base.internal.service.ServletContextManager; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.osgi.framework.Bundle; +import org.osgi.service.http.HttpContext; + +public class ServletContextManagerTest +{ + private ServletContextManager manager; + + @Before + public void setUp() + { + Bundle bundle = Mockito.mock(Bundle.class); + ServletContext globalContext = Mockito.mock(ServletContext.class); + this.manager = new ServletContextManager(bundle, globalContext, false, null); + } + + @Test + public void testGetServletContext() + { + HttpContext httpCtx = Mockito.mock(HttpContext.class); + ServletContext result1 = this.manager.getServletContext(httpCtx); + ServletContext result2 = this.manager.getServletContext(httpCtx); + + Assert.assertNotNull(result1); + Assert.assertNotNull(result2); + Assert.assertSame(result1, result2); + + httpCtx = Mockito.mock(HttpContext.class); + result2 = this.manager.getServletContext(httpCtx); + + Assert.assertNotNull(result2); + Assert.assertNotSame(result1, result2); + } + +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/handler/FilterConfigImplTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/handler/FilterConfigImplTest.java new file mode 100644 index 00000000000..f5cc655f4bf --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/handler/FilterConfigImplTest.java @@ -0,0 +1,70 @@ +/* + * 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.http.base.internal.handler; + +import org.junit.Before; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import javax.servlet.ServletContext; +import java.util.HashMap; +import java.util.Enumeration; + +public class FilterConfigImplTest +{ + private ServletContext context; + private FilterConfigImpl config; + + @Before + public void setUp() + { + HashMap params = new HashMap(); + params.put("key1", "value1"); + + this.context = Mockito.mock(ServletContext.class); + this.config = new FilterConfigImpl("myfilter", this.context, params); + } + + @Test + public void testGetFilterName() + { + Assert.assertSame("myfilter", this.config.getFilterName()); + } + + @Test + public void testGetServletContext() + { + Assert.assertSame(this.context, this.config.getServletContext()); + } + + @Test + public void testGetInitParameter() + { + Assert.assertNull(this.config.getInitParameter("key2")); + Assert.assertEquals("value1", this.config.getInitParameter("key1")); + } + + @Test + public void testGetInitParameterNames() + { + Enumeration e = this.config.getInitParameterNames(); + Assert.assertNotNull(e); + Assert.assertTrue(e.hasMoreElements()); + Assert.assertEquals("key1", e.nextElement()); + Assert.assertFalse(e.hasMoreElements()); + } +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/handler/FilterHandlerTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/handler/FilterHandlerTest.java new file mode 100644 index 00000000000..3eefc72f3e5 --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/handler/FilterHandlerTest.java @@ -0,0 +1,268 @@ +/* + * 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.http.base.internal.handler; + +import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static javax.servlet.http.HttpServletResponse.SC_PAYMENT_REQUIRED; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.Map; + +import javax.servlet.DispatcherType; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.dispatch.InvocationChain; +import org.apache.felix.http.base.internal.runtime.FilterInfo; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class FilterHandlerTest +{ + private Filter filter; + + private ExtServletContext context; + + @Before + public void setUp() + { + this.context = Mockito.mock(ExtServletContext.class); + this.filter = mock(Filter.class); + } + + @Test + public void testCompare() + { + FilterHandler h1 = createHandler(0, "a"); + FilterHandler h2 = createHandler(10, "b"); + FilterHandler h3 = createHandler(10, "c"); + + assertTrue(h1.compareTo(h1) == 0); + + assertTrue(h1.compareTo(h2) > 0); + assertTrue(h2.compareTo(h1) < 0); + + // h2 is actually registered first, so should be called first... + assertTrue(h2.compareTo(h3) < 0); + assertTrue(h3.compareTo(h2) > 0); + } + + @Test + public void testDestroy() + { + FilterHandler h1 = createHandler(0, "/a"); + h1.init(); + h1.destroy(); + verify(this.filter).destroy(); + } + + @Test + public void testHandleFound() throws Exception + { + FilterHandler h1 = createHandler(0, "/a"); + HttpServletRequest req = createServletRequest(); + HttpServletResponse res = createServletResponse(); + FilterChain chain = mock(FilterChain.class); + when(this.context.handleSecurity(req, res)).thenReturn(true); + + when(req.getRequestURI()).thenReturn("/a"); + h1.handle(req, res, chain); + + verify(this.filter).doFilter(req, res, chain); + verify(chain, never()).doFilter(req, res); + } + + @Test + public void testHandleFoundContextRoot() throws Exception + { + FilterHandler h1 = createHandler(0, "/"); + HttpServletRequest req = createServletRequest(); + HttpServletResponse res = createServletResponse(); + FilterChain chain = mock(FilterChain.class); + when(this.context.handleSecurity(req, res)).thenReturn(true); + + when(req.getRequestURI()).thenReturn(null); + h1.handle(req, res, chain); + + verify(this.filter).doFilter(req, res, chain); + verify(chain, never()).doFilter(req, res); + } + + /** + * FELIX-3988: only send an error for uncomitted responses with default status codes. + */ + @Test + public void testHandleFoundForbidden() throws Exception + { + FilterHandler h1 = createHandler(0, "/a"); + final ServletHandler sc = mock(ServletHandler.class); + when(sc.getContext()).thenReturn(this.context); + final InvocationChain ic = new InvocationChain(sc, new FilterHandler[] {h1}); + HttpServletRequest req = createServletRequest(); + HttpServletResponse res = createServletResponse(); + + when(req.getRequestURI()).thenReturn("/a"); + // Default behaviour: uncomitted response and default status code... + when(res.isCommitted()).thenReturn(false); + when(res.getStatus()).thenReturn(SC_OK); + + when(this.context.handleSecurity(req, res)).thenReturn(false); + + ic.doFilter(req, res); + + verify(this.filter, never()).doFilter(req, res, ic); + verify(res).sendError(SC_FORBIDDEN); + } + + /** + * FELIX-3988: do not try to write to an already committed response. + */ + @Test + public void testHandleFoundForbiddenCommittedOwnResponse() throws Exception + { + FilterHandler h1 = createHandler(0, "/a"); + final ServletHandler sc = mock(ServletHandler.class); + when(sc.getContext()).thenReturn(this.context); + final InvocationChain ic = new InvocationChain(sc, new FilterHandler[] {h1}); + HttpServletRequest req = createServletRequest(); + HttpServletResponse res = createServletResponse(); + + when(req.getRequestURI()).thenReturn("/a"); + // Simulate an already committed response... + when(res.isCommitted()).thenReturn(true); + when(res.getStatus()).thenReturn(SC_OK); + + when(this.context.handleSecurity(req, res)).thenReturn(false); + + ic.doFilter(req, res); + + verify(this.filter, never()).doFilter(req, res, ic); + // Should not be called from our handler... + verify(res, never()).sendError(SC_FORBIDDEN); + } + + /** + * FELIX-3988: do not overwrite custom set status code. + */ + @Test + public void testHandleFoundForbiddenCustomStatusCode() throws Exception + { + FilterHandler h1 = createHandler(0, "/a"); + final ServletHandler sc = mock(ServletHandler.class); + when(sc.getContext()).thenReturn(this.context); + final InvocationChain ic = new InvocationChain(sc, new FilterHandler[] {h1}); + HttpServletRequest req = createServletRequest(); + HttpServletResponse res = createServletResponse(); + + when(req.getRequestURI()).thenReturn("/a"); + // Simulate an uncommitted response with a non-default status code... + when(res.isCommitted()).thenReturn(false); + when(res.getStatus()).thenReturn(SC_PAYMENT_REQUIRED); + + when(this.context.handleSecurity(req, res)).thenReturn(false); + + ic.doFilter(req, res); + + verify(this.filter, never()).doFilter(req, res, ic); + // Should not be called from our handler... + verify(res, never()).sendError(SC_FORBIDDEN); + } + + @Test + public void testHandleNotFound() throws Exception + { + FilterHandler h1 = createHandler(0, "/a"); + final ServletHandler sc = mock(ServletHandler.class); + when(sc.getContext()).thenReturn(this.context); + final InvocationChain ic = new InvocationChain(sc, new FilterHandler[] {h1}); + HttpServletRequest req = createServletRequest(); + HttpServletResponse res = createServletResponse(); + + when(req.getRequestURI()).thenReturn("/"); + ic.doFilter(req, res); + + verify(this.filter, never()).doFilter(req, res, ic); + } + + @Test + public void testHandleNotFoundContextRoot() throws Exception + { + FilterHandler h1 = createHandler(0, "/a"); + final ServletHandler sc = mock(ServletHandler.class); + when(sc.getContext()).thenReturn(this.context); + final InvocationChain ic = new InvocationChain(sc, new FilterHandler[] {h1}); + HttpServletRequest req = createServletRequest(); + HttpServletResponse res = createServletResponse(); + + when(req.getRequestURI()).thenReturn(null); + ic.doFilter(req, res); + + verify(this.filter, never()).doFilter(req, res, ic); + } + + @Test + public void testInit() throws Exception + { + FilterHandler h1 = createHandler(0, "/a"); + h1.init(); + verify(this.filter).init(any(FilterConfig.class)); + } + + private FilterHandler createHandler(int ranking, String pattern) + { + return createHandler(pattern, ranking, null); + } + + private FilterHandler createHandler(String pattern, int ranking, Map initParams) + { + if ( initParams == null ) + { + initParams = Collections.emptyMap(); + } + final FilterInfo info = new FilterInfo(null, pattern, ranking, initParams); + return new HttpServiceFilterHandler(this.context, info, this.filter); + } + + private HttpServletRequest createServletRequest() + { + return createServletRequest(DispatcherType.REQUEST); + } + + private HttpServletRequest createServletRequest(DispatcherType type) + { + HttpServletRequest result = mock(HttpServletRequest.class); + when(result.getDispatcherType()).thenReturn(type); + return result; + } + + private HttpServletResponse createServletResponse() + { + return mock(HttpServletResponse.class); + } +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/handler/HttpServiceServletHandlerTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/handler/HttpServiceServletHandlerTest.java new file mode 100644 index 00000000000..94003771135 --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/handler/HttpServiceServletHandlerTest.java @@ -0,0 +1,226 @@ +/* + * 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.http.base.internal.handler; + +import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static javax.servlet.http.HttpServletResponse.SC_PAYMENT_REQUIRED; +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.Map; + +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.dispatch.InvocationChain; +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class HttpServiceServletHandlerTest +{ + private Servlet servlet; + + private ExtServletContext context; + + @Before + public void setUp() + { + this.context = Mockito.mock(ExtServletContext.class); + this.servlet = mock(Servlet.class); + } + + @Test + public void testDestroy() + { + ServletHandler h1 = createHandler("/a"); + h1.init(); + h1.destroy(); + verify(this.servlet).destroy(); + } + + @Test + public void testHandleFound() throws Exception + { + ServletHandler h1 = createHandler("/a"); + final InvocationChain ic = new InvocationChain(h1, new FilterHandler[0]); + HttpServletRequest req = mock(HttpServletRequest.class); + HttpServletResponse res = mock(HttpServletResponse.class); + when(this.context.handleSecurity(req, res)).thenReturn(true); + + when(req.getPathInfo()).thenReturn("/a/b"); + ic.doFilter(req, res); + + assertEquals(0, res.getStatus()); + verify(this.servlet).service(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @Test + public void testHandleFoundContextRoot() throws Exception + { + ServletHandler h1 = createHandler("/"); + final InvocationChain ic = new InvocationChain(h1, new FilterHandler[0]); + HttpServletRequest req = mock(HttpServletRequest.class); + HttpServletResponse res = mock(HttpServletResponse.class); + when(this.context.handleSecurity(req, res)).thenReturn(true); + + when(req.getPathInfo()).thenReturn(null); + ic.doFilter(req, res); + + assertEquals(0, res.getStatus()); + verify(this.servlet).service(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + /** + * FELIX-3988: only send an error for uncomitted responses with default status codes. + */ + @Test + public void testHandleFoundForbidden() throws Exception + { + ServletHandler h1 = createHandler("/a"); + final InvocationChain ic = new InvocationChain(h1, new FilterHandler[0]); + HttpServletRequest req = mock(HttpServletRequest.class); + HttpServletResponse res = mock(HttpServletResponse.class); + + when(req.getPathInfo()).thenReturn("/a"); + // Default behaviour: uncomitted response and default status code... + when(res.isCommitted()).thenReturn(false); + when(res.getStatus()).thenReturn(SC_OK); + + when(this.context.handleSecurity(req, res)).thenReturn(false); + + when(req.getPathInfo()).thenReturn("/a/b"); + ic.doFilter(req, res); + + assertEquals(SC_OK, res.getStatus()); + verify(this.servlet, never()).service(req, res); + verify(res).sendError(SC_FORBIDDEN); + } + + /** + * FELIX-3988: do not try to write to an already committed response. + */ + @Test + public void testHandleFoundForbiddenCommittedOwnResponse() throws Exception + { + ServletHandler h1 = createHandler("/a"); + final InvocationChain ic = new InvocationChain(h1, new FilterHandler[0]); + HttpServletRequest req = mock(HttpServletRequest.class); + HttpServletResponse res = mock(HttpServletResponse.class); + + when(req.getPathInfo()).thenReturn("/a"); + // Comitted response with default status code... + when(res.isCommitted()).thenReturn(true); + when(res.getStatus()).thenReturn(SC_OK); + + when(this.context.handleSecurity(req, res)).thenReturn(false); + + when(req.getPathInfo()).thenReturn("/a/b"); + ic.doFilter(req, res); + + assertEquals(SC_OK, res.getStatus()); + verify(this.servlet, never()).service(req, res); + verify(res, never()).sendError(SC_FORBIDDEN); + } + + /** + * FELIX-3988: do not overwrite custom set status code. + */ + @Test + public void testHandleFoundForbiddenCustomStatusCode() throws Exception + { + ServletHandler h1 = createHandler("/a"); + final InvocationChain ic = new InvocationChain(h1, new FilterHandler[0]); + HttpServletRequest req = mock(HttpServletRequest.class); + HttpServletResponse res = mock(HttpServletResponse.class); + + when(req.getPathInfo()).thenReturn("/a"); + // Unomitted response with default status code... + when(res.isCommitted()).thenReturn(false); + when(res.getStatus()).thenReturn(SC_PAYMENT_REQUIRED); + + when(this.context.handleSecurity(req, res)).thenReturn(false); + + when(req.getPathInfo()).thenReturn("/a/b"); + ic.doFilter(req, res); + + assertEquals(SC_PAYMENT_REQUIRED, res.getStatus()); + verify(this.servlet, never()).service(req, res); + verify(res, never()).sendError(SC_FORBIDDEN); + } + + @Test + public void testHandleNotFound() throws Exception + { + ServletHandler h1 = createHandler("/a"); + final InvocationChain ic = new InvocationChain(h1, new FilterHandler[0]); + HttpServletRequest req = mock(HttpServletRequest.class); + HttpServletResponse res = mock(HttpServletResponse.class); + + when(req.getPathInfo()).thenReturn("/"); + ic.doFilter(req, res); + + verify(this.servlet, never()).service(req, res); + } + + @Test + public void testHandleNotFoundContextRoot() throws Exception + { + ServletHandler h1 = createHandler("/a"); + HttpServletRequest req = mock(HttpServletRequest.class); + HttpServletResponse res = mock(HttpServletResponse.class); + when(this.context.handleSecurity(req, res)).thenReturn(true); + + when(req.getRequestURI()).thenReturn(null); + h1.handle(req, res); + + verify(this.servlet).service(req, res); + } + + @Test + public void testInit() throws Exception + { + ServletHandler h1 = createHandler("/a"); + h1.init(); + verify(this.servlet).init(any(ServletConfig.class)); + } + + private ServletHandler createHandler(String alias) + { + return createHandler(alias, null); + } + + private ServletHandler createHandler(String alias, Map map) + { + if ( map == null ) + { + map = Collections.emptyMap(); + } + final ServletInfo info = new ServletInfo(null, alias, map); + return new HttpServiceServletHandler(this.context, info, this.servlet); + } +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/handler/HttpSessionWrapperTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/handler/HttpSessionWrapperTest.java new file mode 100644 index 00000000000..be276bf3c86 --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/handler/HttpSessionWrapperTest.java @@ -0,0 +1,147 @@ +/* + * 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.http.base.internal.handler; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionListener; + +import org.apache.felix.http.base.internal.HttpConfig; +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * Test cases for {@link HttpSessionWrapper}. + */ +public class HttpSessionWrapperTest +{ + + /** + * FELIX-5175 - sessions are incorrectly destroyed / destroyed too soon. + */ + @Test + public void testSessionTimeout() throws Exception + { + Set names; + + String contextName = "default"; + long now = System.currentTimeMillis(); + + HttpSession session = createMockSession(contextName, now, 1); + + names = HttpSessionWrapper.getExpiredSessionContextNames(session); + assertTrue("Session should NOT be destroyed!", names.isEmpty()); + + // Pretend we've accessed this session two seconds ago, which should imply it is timed out... + session = createMockSession(contextName, now - 2000L, 1); + + names = HttpSessionWrapper.getExpiredSessionContextNames(session); + assertFalse("Session should be destroyed!", names.isEmpty()); + assertTrue(names.contains(contextName)); + } + + private HttpSession createMockSession(String sessionName, long lastAccessed, int maxInactive) + { + String attrLastAccessed = String.format("org.apache.felix.http.session.context.lastaccessed.%s", sessionName); + String attrMaxInactive = String.format("org.apache.felix.http.session.context.maxinactive.%s", sessionName); + + HttpSession session = mock(HttpSession.class); + when(session.getAttributeNames()).thenReturn(Collections.enumeration(Arrays.asList(attrLastAccessed))); + when(session.getAttribute(eq(attrLastAccessed))).thenReturn(lastAccessed); + when(session.getAttribute(eq(attrMaxInactive))).thenReturn(maxInactive); + + return session; + } + + /** + * FELIX-5819 : Container session should not be invalidated + */ + @Test + public void testContainerSessionInvalidation() + { + // create container session + final Map attributes = new HashMap<>(); + final HttpSession containerSession = mock(HttpSession.class); + when(containerSession.getAttributeNames()).thenReturn(Collections.enumeration(attributes.keySet())); + when(containerSession.getAttribute(Mockito.anyString())).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return attributes.get(invocation.getArgument(0)); + } + }); + when(containerSession.getAttribute(Mockito.anyString())).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return attributes.get(invocation.getArgument(0)); + } + }); + Mockito.doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + attributes.put((String)invocation.getArgument(0), invocation.getArgument(1)); + return null; + } + }).when(containerSession).setAttribute(Mockito.anyString(), Mockito.any()); + Mockito.doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + attributes.remove(invocation.getArgument(0)); + return null; + } + }).when(containerSession).removeAttribute(Mockito.anyString()); + + final HttpSessionListener listener = mock(HttpSessionListener.class); + + // create context session + final ExtServletContext context = mock(ExtServletContext.class); + when(context.getServletContextName()).thenReturn("default"); + when(context.getHttpSessionListener()).thenReturn(listener); + + final HttpConfig config = new HttpConfig(); + config.setInvalidateContainerSession(false); + final HttpSession contextSession = new HttpSessionWrapper(containerSession, context, config, false); + // invalidate context session and verify that invalidate is not called on the container session + contextSession.invalidate(); + assertTrue(attributes.isEmpty()); + Mockito.verify(containerSession, Mockito.never()).invalidate(); + + config.setInvalidateContainerSession(true); + final HttpSession newSession = new HttpSessionWrapper(containerSession, context, config, false); + // invalidate context session and verify that invalidate is called on the container session + newSession.invalidate(); + assertTrue(attributes.isEmpty()); + Mockito.verify(containerSession).invalidate(); + } + +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/handler/ServletConfigImplTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/handler/ServletConfigImplTest.java new file mode 100644 index 00000000000..f245c74ccef --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/handler/ServletConfigImplTest.java @@ -0,0 +1,70 @@ +/* + * 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.http.base.internal.handler; + +import org.junit.Before; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import javax.servlet.ServletContext; +import java.util.HashMap; +import java.util.Enumeration; + +public class ServletConfigImplTest +{ + private ServletContext context; + private ServletConfigImpl config; + + @Before + public void setUp() + { + HashMap params = new HashMap(); + params.put("key1", "value1"); + + this.context = Mockito.mock(ServletContext.class); + this.config = new ServletConfigImpl("myservlet", this.context, params); + } + + @Test + public void testGetServletName() + { + Assert.assertSame("myservlet", this.config.getServletName()); + } + + @Test + public void testGetServletContext() + { + Assert.assertSame(this.context, this.config.getServletContext()); + } + + @Test + public void testGetInitParameter() + { + Assert.assertNull(this.config.getInitParameter("key2")); + Assert.assertEquals("value1", this.config.getInitParameter("key1")); + } + + @Test + public void testGetInitParameterNames() + { + Enumeration e = this.config.getInitParameterNames(); + Assert.assertNotNull(e); + Assert.assertTrue(e.hasMoreElements()); + Assert.assertEquals("key1", e.nextElement()); + Assert.assertFalse(e.hasMoreElements()); + } +} \ No newline at end of file diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/registry/ErrorPageRegistryTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/registry/ErrorPageRegistryTest.java new file mode 100644 index 00000000000..bb548daedb4 --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/registry/ErrorPageRegistryTest.java @@ -0,0 +1,298 @@ +/* + * 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.http.base.internal.registry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.handler.HttpServiceServletHandler; +import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.apache.felix.http.base.internal.runtime.dto.FailedDTOHolder; +import org.junit.Test; +import org.mockito.Matchers; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.runtime.dto.DTOConstants; +import org.osgi.service.http.runtime.dto.FailedErrorPageDTO; +import org.osgi.service.http.runtime.dto.ServletContextDTO; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; + +public class ErrorPageRegistryTest +{ + private void assertEmpty(final ServletContextDTO dto, final FailedDTOHolder holder) + { + assertNull(dto.servletDTOs); + assertNull(dto.resourceDTOs); + assertNull(dto.errorPageDTOs); + assertTrue(holder.failedErrorPageDTOs.isEmpty()); + } + + private void clear(final ServletContextDTO dto, final FailedDTOHolder holder) + { + dto.servletDTOs = null; + dto.resourceDTOs = null; + dto.errorPageDTOs = null; + holder.failedErrorPageDTOs.clear(); + } + + @Test public void testSingleErrorPage() throws InvalidSyntaxException, ServletException + { + final ErrorPageRegistry reg = new ErrorPageRegistry(); + + final FailedDTOHolder holder = new FailedDTOHolder(); + final ServletContextDTO dto = new ServletContextDTO(); + + // empty reg + reg.getRuntimeInfo(dto, holder.failedErrorPageDTOs); + assertEmpty(dto, holder); + + // register error page + final ServletHandler h1 = createServletHandler(1L, 0, "404", "java.io.IOException"); + reg.addServlet(h1); + + verify(h1.getServlet()).init(Matchers.any(ServletConfig.class)); + + // one entry in reg + clear(dto, holder); + reg.getRuntimeInfo(dto, holder.failedErrorPageDTOs); + assertNull(dto.resourceDTOs); + assertNull(dto.servletDTOs); + assertNotNull(dto.errorPageDTOs); + assertEquals(1, dto.errorPageDTOs.length); + assertEquals(1L, dto.errorPageDTOs[0].serviceId); + assertEquals(1, dto.errorPageDTOs[0].errorCodes.length); + assertEquals(404, dto.errorPageDTOs[0].errorCodes[0]); + assertEquals(1, dto.errorPageDTOs[0].exceptions.length); + assertEquals("java.io.IOException", dto.errorPageDTOs[0].exceptions[0]); + assertTrue(holder.failedErrorPageDTOs.isEmpty()); + + // test error handling + assertNotNull(reg.get(new IOException(), 404)); + assertNotNull(reg.get(new RuntimeException(), 404)); + assertNotNull(reg.get(new IOException(), 500)); + assertNotNull(reg.get(new FileNotFoundException(), 500)); + assertNull(reg.get(new RuntimeException(), 500)); + + // remove servlet + final Servlet s = h1.getServlet(); + reg.removeServlet(h1.getServletInfo(), true); + verify(s).destroy(); + + // empty again + clear(dto, holder); + reg.getRuntimeInfo(dto, holder.failedErrorPageDTOs); + assertEmpty(dto, holder); + } + + @Test public void testSimpleHiding() throws InvalidSyntaxException, ServletException + { + final ErrorPageRegistry reg = new ErrorPageRegistry(); + + final FailedDTOHolder holder = new FailedDTOHolder(); + final ServletContextDTO dto = new ServletContextDTO(); + + // check DTO + reg.getRuntimeInfo(dto, holder.failedErrorPageDTOs); + assertEmpty(dto, holder); + + // register error pages + final ServletHandler h1 = createServletHandler(1L, 0, "404", "java.io.IOException"); + reg.addServlet(h1); + final ServletHandler h2 = createServletHandler(2L, 10, "404", "some.other.Exception"); + reg.addServlet(h2); + + verify(h1.getServlet()).init(Matchers.any(ServletConfig.class)); + verify(h2.getServlet()).init(Matchers.any(ServletConfig.class)); + + // two entries in DTO + clear(dto, holder); + reg.getRuntimeInfo(dto, holder.failedErrorPageDTOs); + + assertNull(dto.resourceDTOs); + assertNull(dto.servletDTOs); + assertNotNull(dto.errorPageDTOs); + assertEquals(2, dto.errorPageDTOs.length); + assertEquals(0, dto.errorPageDTOs[1].errorCodes.length); + assertEquals(1, dto.errorPageDTOs[0].errorCodes.length); + assertEquals(404, dto.errorPageDTOs[0].errorCodes[0]); + assertEquals(1, dto.errorPageDTOs[1].exceptions.length); + assertEquals(1, dto.errorPageDTOs[0].exceptions.length); + assertEquals("java.io.IOException", dto.errorPageDTOs[1].exceptions[0]); + assertEquals("some.other.Exception", dto.errorPageDTOs[0].exceptions[0]); + assertEquals(1, holder.failedErrorPageDTOs.size()); + assertEquals(1L, holder.failedErrorPageDTOs.get(0).serviceId); + assertEquals(1, holder.failedErrorPageDTOs.get(0).errorCodes.length); + assertEquals(404, holder.failedErrorPageDTOs.get(0).errorCodes[0]); + assertEquals(0, holder.failedErrorPageDTOs.get(0).exceptions.length); + assertEquals(DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE, holder.failedErrorPageDTOs.get(0).failureReason); + + // remove second page + final Servlet s2 = h2.getServlet(); + reg.removeServlet(h2.getServletInfo(), true); + verify(s2).destroy(); + + // one entry in DTO + clear(dto, holder); + reg.getRuntimeInfo(dto, holder.failedErrorPageDTOs); + assertNull(dto.resourceDTOs); + assertNull(dto.servletDTOs); + assertNotNull(dto.errorPageDTOs); + assertEquals(1, dto.errorPageDTOs.length); + assertEquals(1, dto.errorPageDTOs[0].errorCodes.length); + assertEquals(404, dto.errorPageDTOs[0].errorCodes[0]); + assertEquals(1, dto.errorPageDTOs[0].exceptions.length); + assertEquals("java.io.IOException", dto.errorPageDTOs[0].exceptions[0]); + assertTrue(holder.failedErrorPageDTOs.isEmpty()); + + // test error handling + assertNotNull(reg.get(new IOException(), 404)); + assertNotNull(reg.get(new RuntimeException(), 404)); + assertNotNull(reg.get(new IOException(), 500)); + assertNotNull(reg.get(new FileNotFoundException(), 500)); + assertNull(reg.get(new RuntimeException(), 500)); + + // remove first page + final Servlet s1 = h1.getServlet(); + reg.removeServlet(h1.getServletInfo(), true); + verify(s1).destroy(); + + // empty again + clear(dto, holder); + reg.getRuntimeInfo(dto, holder.failedErrorPageDTOs); + assertEmpty(dto, holder); + } + + @Test public void testRangeRegistration() throws InvalidSyntaxException + { + final ErrorPageRegistry reg = new ErrorPageRegistry(); + final FailedDTOHolder holder = new FailedDTOHolder(); + final ServletContextDTO dto = new ServletContextDTO(); + + final ServletHandler handler4 = createServletHandler(1L, 0, "4xx"); + final ServletHandler handler5 = createServletHandler(2L, 0, "5xx"); + + reg.addServlet(handler4); + reg.addServlet(handler5); + + // check DTO + reg.getRuntimeInfo(dto, holder.failedErrorPageDTOs); + + assertTrue(holder.failedErrorPageDTOs.isEmpty()); + assertEquals(2, dto.errorPageDTOs.length); + assertEquals(100, dto.errorPageDTOs[0].errorCodes.length); + final Set codes4 = new HashSet(); + for(final long c : dto.errorPageDTOs[0].errorCodes) + { + assertTrue(c >= 400 && c < 500); + codes4.add(c); + } + assertEquals(100, codes4.size()); + assertEquals(100, dto.errorPageDTOs[1].errorCodes.length); + final Set codes5 = new HashSet(); + for(final long c : dto.errorPageDTOs[1].errorCodes) + { + assertTrue(c >= 500 && c < 600); + codes5.add(c); + } + assertEquals(100, codes5.size()); + } + + @Test public void testRangeRegistrationOverlay() throws InvalidSyntaxException + { + final ErrorPageRegistry reg = new ErrorPageRegistry(); + final FailedDTOHolder holder = new FailedDTOHolder(); + final ServletContextDTO dto = new ServletContextDTO(); + + final ServletHandler handler4 = createServletHandler(1L, 0, "4xx"); + final ServletHandler handler = createServletHandler(2L, 10, "404", "403"); + + reg.addServlet(handler4); + reg.addServlet(handler); + + // check DTO + reg.getRuntimeInfo(dto, holder.failedErrorPageDTOs); + + // a 4xx is only registered as failure DTO if overlayed by a 4xx! + // -> no failure in this case + assertEquals(0, holder.failedErrorPageDTOs.size()); + final Set codes4 = new HashSet(); + for(final long c : dto.errorPageDTOs[1].errorCodes) + { + assertTrue(c >= 400 && c < 500); + codes4.add(c); + } + assertEquals(98, codes4.size()); + assertFalse(codes4.contains(404L)); + assertFalse(codes4.contains(403L)); + assertEquals(2, dto.errorPageDTOs[0].errorCodes.length); + final Set codes = new HashSet(); + for(final long c : dto.errorPageDTOs[0].errorCodes) + { + assertTrue(c >= 403 && c < 405); + codes.add(c); + } + assertEquals(2, codes.size()); + } + + private static ServletInfo createServletInfo(final long id, final int ranking, final String... codes) throws InvalidSyntaxException + { + final BundleContext bCtx = mock(BundleContext.class); + when(bCtx.createFilter(Matchers.anyString())).thenReturn(null); + final Bundle bundle = mock(Bundle.class); + when(bundle.getBundleContext()).thenReturn(bCtx); + + @SuppressWarnings("unchecked") + final ServiceReference ref = mock(ServiceReference.class); + when(ref.getBundle()).thenReturn(bundle); + when(ref.getProperty(Constants.SERVICE_ID)).thenReturn(id); + when(ref.getProperty(Constants.SERVICE_RANKING)).thenReturn(ranking); + when(ref.getProperty(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_ERROR_PAGE)).thenReturn(codes); + when(ref.getPropertyKeys()).thenReturn(new String[0]); + final ServletInfo si = new ServletInfo(ref); + + return si; + } + + private static ServletHandler createServletHandler(final long id, final int ranking, final String... codes) throws InvalidSyntaxException + { + final ServletInfo si = createServletInfo(id, ranking, codes); + final ExtServletContext ctx = mock(ExtServletContext.class); + final Servlet servlet = mock(Servlet.class); + + return new HttpServiceServletHandler(ctx, si, servlet); + } +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/registry/EventListenerRegistryTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/registry/EventListenerRegistryTest.java new file mode 100644 index 00000000000..3a00baf9a45 --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/registry/EventListenerRegistryTest.java @@ -0,0 +1,152 @@ +/* + * 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.http.base.internal.registry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import javax.servlet.DispatcherType; +import javax.servlet.Filter; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.handler.FilterHandler; +import org.apache.felix.http.base.internal.handler.HttpServiceFilterHandler; +import org.apache.felix.http.base.internal.runtime.FilterInfo; +import org.apache.felix.http.base.internal.runtime.dto.FailedDTOHolder; +import org.junit.Test; +import org.mockito.Matchers; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.runtime.dto.ServletContextDTO; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; + +public class EventListenerRegistryTest { + + private final FilterRegistry reg = new FilterRegistry(); + + private void assertEmpty(final ServletContextDTO dto, final FailedDTOHolder holder) + { + assertNull(dto.filterDTOs); + assertTrue(holder.failedFilterDTOs.isEmpty()); + } + + private void clear(final ServletContextDTO dto, final FailedDTOHolder holder) + { + dto.filterDTOs = null; + holder.failedFilterDTOs.clear(); + } + + @Test public void testSingleFilter() throws InvalidSyntaxException, ServletException + { + final FailedDTOHolder holder = new FailedDTOHolder(); + final ServletContextDTO dto = new ServletContextDTO(); + + // check DTO + reg.getRuntimeInfo(dto, holder.failedFilterDTOs); + assertEmpty(dto, holder); + + // register filter + final FilterHandler h1 = createFilterHandler(1L, 0, "/foo"); + reg.addFilter(h1); + + verify(h1.getFilter()).init(Matchers.any(FilterConfig.class)); + + // one entry in DTO + clear(dto, holder); + reg.getRuntimeInfo(dto, holder.failedFilterDTOs); + assertTrue(holder.failedFilterDTOs.isEmpty()); + assertNotNull(dto.filterDTOs); + assertEquals(1, dto.filterDTOs.length); + assertEquals(1, dto.filterDTOs[0].patterns.length); + assertEquals("/foo", dto.filterDTOs[0].patterns[0]); + + // remove filter + final Filter f = h1.getFilter(); + reg.removeFilter(h1.getFilterInfo(), true); + verify(f).destroy(); + + // empty again + clear(dto, holder); + reg.getRuntimeInfo(dto, holder.failedFilterDTOs); + assertEmpty(dto, holder); + } + + @Test public void testFilterOrdering() throws InvalidSyntaxException + { + final FilterHandler h1 = createFilterHandler(1L, 20, "/foo"); + reg.addFilter(h1); + final FilterHandler h2 = createFilterHandler(2L, 10, "/foo"); + reg.addFilter(h2); + final FilterHandler h3 = createFilterHandler(3L, 30, "/foo"); + reg.addFilter(h3); + final FilterHandler h4 = createFilterHandler(4L, 0, "/other"); + reg.addFilter(h4); + final FilterHandler h5 = createFilterHandler(5L, 90, "/foo"); + reg.addFilter(h5); + + final FilterHandler[] handlers = reg.getFilterHandlers(null, DispatcherType.REQUEST, "/foo"); + assertEquals(4, handlers.length); + assertEquals(h5.getFilterInfo(), handlers[0].getFilterInfo()); + assertEquals(h3.getFilterInfo(), handlers[1].getFilterInfo()); + assertEquals(h1.getFilterInfo(), handlers[2].getFilterInfo()); + assertEquals(h2.getFilterInfo(), handlers[3].getFilterInfo()); + + // cleanup + reg.removeFilter(h1.getFilterInfo(), true); + reg.removeFilter(h2.getFilterInfo(), true); + reg.removeFilter(h3.getFilterInfo(), true); + reg.removeFilter(h4.getFilterInfo(), true); + reg.removeFilter(h5.getFilterInfo(), true); + } + + private static FilterInfo createFilterInfo(final long id, final int ranking, final String... paths) throws InvalidSyntaxException + { + final BundleContext bCtx = mock(BundleContext.class); + when(bCtx.createFilter(Matchers.anyString())).thenReturn(null); + final Bundle bundle = mock(Bundle.class); + when(bundle.getBundleContext()).thenReturn(bCtx); + + final ServiceReference ref = mock(ServiceReference.class); + when(ref.getBundle()).thenReturn(bundle); + when(ref.getProperty(Constants.SERVICE_ID)).thenReturn(id); + when(ref.getProperty(Constants.SERVICE_RANKING)).thenReturn(ranking); + when(ref.getProperty(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_PATTERN)).thenReturn(paths); + when(ref.getPropertyKeys()).thenReturn(new String[0]); + final FilterInfo si = new FilterInfo(ref); + + return si; + } + + private static FilterHandler createFilterHandler(final long id, final int ranking, final String... paths) throws InvalidSyntaxException + { + final FilterInfo si = createFilterInfo(id, ranking, paths); + final ExtServletContext ctx = mock(ExtServletContext.class); + final Filter filter = mock(Filter.class); + + return new HttpServiceFilterHandler(ctx, si, filter); + } +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/registry/FilterRegistryTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/registry/FilterRegistryTest.java new file mode 100644 index 00000000000..4c23a94408e --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/registry/FilterRegistryTest.java @@ -0,0 +1,127 @@ +/* + * 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.http.base.internal.registry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.EventListener; + +import javax.servlet.ServletContextListener; +import javax.servlet.ServletException; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.handler.ListenerHandler; +import org.apache.felix.http.base.internal.handler.WhiteboardListenerHandler; +import org.apache.felix.http.base.internal.runtime.ListenerInfo; +import org.apache.felix.http.base.internal.runtime.dto.FailedDTOHolder; +import org.junit.Test; +import org.mockito.Matchers; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceObjects; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.runtime.dto.ServletContextDTO; + +public class FilterRegistryTest { + + private void assertEmpty(final ServletContextDTO dto, final FailedDTOHolder holder) + { + assertNull(dto.listenerDTOs); + assertTrue(holder.failedListenerDTOs.isEmpty()); + } + + private void clear(final ServletContextDTO dto, final FailedDTOHolder holder) + { + dto.listenerDTOs = null; + holder.failedListenerDTOs.clear(); + } + + @Test public void testSingleListener() throws InvalidSyntaxException, ServletException + { + final EventListenerRegistry reg = new EventListenerRegistry(); + final FailedDTOHolder holder = new FailedDTOHolder(); + final ServletContextDTO dto = new ServletContextDTO(); + + // check DTO + reg.getRuntimeInfo(dto, holder.failedListenerDTOs); + assertEmpty(dto, holder); + + // register listener + final ListenerHandler h1 = createListenerHandler(1L, 0, ServletContextListener.class); + reg.addListeners(h1); + + // one entry in DTO + clear(dto, holder); + reg.getRuntimeInfo(dto, holder.failedListenerDTOs); + assertTrue(holder.failedListenerDTOs.isEmpty()); + assertNotNull(dto.listenerDTOs); + assertEquals(1, dto.listenerDTOs.length); + assertEquals(1, dto.listenerDTOs[0].types.length); + assertEquals(ServletContextListener.class.getName(), dto.listenerDTOs[0].types[0]); + + // remove listener + reg.removeListeners(h1.getListenerInfo()); + + // empty again + clear(dto, holder); + reg.getRuntimeInfo(dto, holder.failedListenerDTOs); + assertEmpty(dto, holder); + } + + private static ListenerInfo createListenerInfo(final long id, final int ranking, final Class type) throws InvalidSyntaxException + { + final String[] typeNames = new String[1]; + int index = 0; + typeNames[index++] = type.getName(); + + final BundleContext bCtx = mock(BundleContext.class); + when(bCtx.createFilter(Matchers.anyString())).thenReturn(null); + final Bundle bundle = mock(Bundle.class); + when(bundle.getBundleContext()).thenReturn(bCtx); + + final ServiceReference ref = mock(ServiceReference.class); + when(ref.getBundle()).thenReturn(bundle); + when(ref.getProperty(Constants.SERVICE_ID)).thenReturn(id); + when(ref.getProperty(Constants.SERVICE_RANKING)).thenReturn(ranking); + when(ref.getProperty(Constants.OBJECTCLASS)).thenReturn(typeNames); + when(ref.getPropertyKeys()).thenReturn(new String[0]); + + final EventListener listener = mock(type); + final ServiceObjects so = mock(ServiceObjects.class); + when(bCtx.getServiceObjects(ref)).thenReturn(so); + when(so.getService()).thenReturn(listener); + + final ListenerInfo info = new ListenerInfo(ref); + + return info; + } + + private static ListenerHandler createListenerHandler(final long id, final int ranking, final Class type) throws InvalidSyntaxException + { + final ListenerInfo info = createListenerInfo(id, ranking, type); + final ExtServletContext ctx = mock(ExtServletContext.class); + + return new WhiteboardListenerHandler(1L, ctx, info, info.getServiceReference().getBundle().getBundleContext()); + } +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/registry/HandlerRegistryTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/registry/HandlerRegistryTest.java new file mode 100644 index 00000000000..433d80d79a0 --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/registry/HandlerRegistryTest.java @@ -0,0 +1,295 @@ +/* + * 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.http.base.internal.registry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; + +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; + +import org.apache.felix.http.base.internal.HttpConfig; +import org.apache.felix.http.base.internal.handler.HttpServiceServletHandler; +import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.apache.felix.http.base.internal.runtime.dto.FailedDTOHolder; +import org.apache.felix.http.base.internal.service.HttpServiceFactory; +import org.junit.Test; +import org.mockito.Mockito; +import org.osgi.service.http.runtime.dto.ServletContextDTO; +import org.osgi.service.http.runtime.dto.ServletDTO; + + +public class HandlerRegistryTest +{ + private final HandlerRegistry registry = new HandlerRegistry(new HttpConfig()); + + @Test public void testInitialSetup() + { + final FailedDTOHolder holder = new FailedDTOHolder(); + final ServletContextDTO dto = new ServletContextDTO(); + dto.serviceId = HttpServiceFactory.HTTP_SERVICE_CONTEXT_SERVICE_ID; + + assertFalse(registry.getRuntimeInfo(dto, holder)); + + registry.init(); + + assertTrue(registry.getRuntimeInfo(dto, holder)); + + registry.shutdown(); + assertFalse(registry.getRuntimeInfo(dto, holder)); + } + + @Test + public void testAddRemoveServlet() throws Exception + { + registry.init(); + + final FailedDTOHolder holder = new FailedDTOHolder(); + final ServletContextDTO dto = new ServletContextDTO(); + dto.serviceId = HttpServiceFactory.HTTP_SERVICE_CONTEXT_SERVICE_ID; + dto.servletDTOs = new ServletDTO[0]; + + Servlet servlet = Mockito.mock(Servlet.class); + final ServletInfo info = new ServletInfo("foo", "/foo", Collections. emptyMap()); + ServletHandler handler = new HttpServiceServletHandler(null, info, servlet); + + assertTrue(registry.getRuntimeInfo(dto, holder)); + assertEquals("Precondition", 0, dto.servletDTOs.length); + + registry.getRegistry(handler.getContextServiceId()).registerServlet(handler); + Mockito.verify(servlet, Mockito.times(1)).init(Mockito.any(ServletConfig.class)); + assertTrue(registry.getRuntimeInfo(dto, holder)); + assertEquals(1, dto.servletDTOs.length); + assertEquals(info.getServiceId(), dto.servletDTOs[0].serviceId); + + final ServletInfo info2 = new ServletInfo("bar", "/bar", Collections. emptyMap()); + ServletHandler handler2 = new HttpServiceServletHandler(null, info2, Mockito.mock(Servlet.class)); + registry.getRegistry(handler.getContextServiceId()).registerServlet(handler2); + assertTrue(registry.getRuntimeInfo(dto, holder)); + assertEquals(2, dto.servletDTOs.length); + + final ServletInfo info3 = new ServletInfo("zar", "/foo", Collections. emptyMap()); + ServletHandler handler3 = new HttpServiceServletHandler(null,info3, Mockito.mock(Servlet.class)); + registry.getRegistry(handler.getContextServiceId()).registerServlet(handler3); + assertTrue(registry.getRuntimeInfo(dto, holder)); + assertEquals(2, dto.servletDTOs.length); + assertEquals(1, holder.failedServletDTOs.size()); + + registry.shutdown(); + } +/* + @Test + public void testAddServletWhileSameServletAddedDuringInit() throws Exception + { + final HandlerRegistry hr = new HandlerRegistry(); + + Servlet servlet = Mockito.mock(Servlet.class); + final ServletInfo info = new ServletInfo("bar", "/bar", 0, null, servlet, null); + final ServletHandler otherHandler = new ServletHandler(null, null, info, info.getServlet()); + + Mockito.doAnswer(new Answer() + { + boolean registered = false; + @Override + public Void answer(InvocationOnMock invocation) throws Throwable + { + if (!registered) + { + registered = true; + // sneakily register another handler with this servlet before this + // one has finished calling init() + hr.addServlet(null, otherHandler); + } + return null; + } + }).when(servlet).init(Mockito.any(ServletConfig.class)); + + final ServletInfo info2 = new ServletInfo("foo", "/foo", 0, null, servlet, null); + ServletHandler handler = new ServletHandler(null, null, info2, info2.getServlet()); + try + { + hr.addServlet(null, handler); + + // TODO +// fail("Should not have allowed the servlet to be added as it was already " +// + "added before init was finished"); + + } + catch (ServletException ne) + { + // good + } + assertArrayEquals(new ServletHandler[] {otherHandler, handler}, hr.getServlets()); + } + + @Test + public void testAddServletWhileSameAliasAddedDuringInit() throws Exception + { + final HandlerRegistry hr = new HandlerRegistry(); + + Servlet otherServlet = Mockito.mock(Servlet.class); + final ServletInfo info = new ServletInfo("bar", "/foo", 0, null, otherServlet, null); + final ServletHandler otherHandler = new ServletHandler(null, null, info, info.getServlet()); + + Servlet servlet = Mockito.mock(Servlet.class); + Mockito.doAnswer(new Answer() + { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable + { + // sneakily register another servlet before this one has finished calling init() + hr.addServlet(null, otherHandler); + return null; + } + }).when(servlet).init(Mockito.any(ServletConfig.class)); + + final ServletInfo info2 = new ServletInfo("foo", "/foo", 0, null, servlet, null); + ServletHandler handler = new ServletHandler(null, null, info2, info2.getServlet()); + + try + { + hr.addServlet(null, handler); + fail("Should not have allowed the servlet to be added as another one got in there with the same alias"); + } + catch (NamespaceException ne) + { + // good + } + assertArrayEquals(new ServletHandler[] {otherHandler}, hr.getServlets()); + Mockito.verify(servlet, Mockito.times(1)).destroy(); + + assertSame(otherServlet, hr.getServletByAlias("/foo")); + } + + @Test + public void testAddRemoveFilter() throws Exception + { + HandlerRegistry hr = new HandlerRegistry(); + + Filter filter = Mockito.mock(Filter.class); + final FilterInfo info = new FilterInfo("oho", "/aha", 1, null, filter, null); + + FilterHandler handler = new FilterHandler(null, filter, info); + assertEquals("Precondition", 0, hr.getFilters().length); + hr.addFilter(handler); + Mockito.verify(filter, Mockito.times(1)).init(Mockito.any(FilterConfig.class)); + assertEquals(1, hr.getFilters().length); + assertSame(handler, hr.getFilters()[0]); + + final FilterInfo info2 = new FilterInfo("haha", "/hihi", 2, null, filter, null); + FilterHandler handler2 = new FilterHandler(null, filter, info2); + try + { + hr.addFilter(handler2); + fail("Should not have allowed the same filter to be added twice"); + } + catch(ServletException se) + { + // good + } + assertArrayEquals(new FilterHandler[] {handler}, hr.getFilters()); + + Mockito.verify(filter, Mockito.never()).destroy(); + hr.removeFilter(filter, true); + Mockito.verify(filter, Mockito.times(1)).destroy(); + assertEquals(0, hr.getServlets().length); + } + + @Test + public void testAddFilterWhileSameFilterAddedDuringInit() throws Exception + { + final HandlerRegistry hr = new HandlerRegistry(); + + Filter filter = Mockito.mock(Filter.class); + final FilterInfo info = new FilterInfo("two", "/two", 99, null, filter, null); + final FilterHandler otherHandler = new FilterHandler(null, filter, info); + + Mockito.doAnswer(new Answer() + { + boolean registered = false; + @Override + public Void answer(InvocationOnMock invocation) throws Throwable + { + if (!registered) + { + registered = true; + // sneakily register another handler with this filter before this + // one has finished calling init() + hr.addFilter(otherHandler); + } + return null; + } + }).when(filter).init(Mockito.any(FilterConfig.class)); + + final FilterInfo info2 = new FilterInfo("one", "/one", 1, null, filter, null); + FilterHandler handler = new FilterHandler(null, filter, info2); + + try + { + hr.addFilter(handler); + fail("Should not have allowed the filter to be added as it was already " + + "added before init was finished"); + } + catch (ServletException se) + { + // good + } + assertArrayEquals(new FilterHandler[] {otherHandler}, hr.getFilters()); + } + + @Test + public void testRemoveAll() throws Exception + { + HandlerRegistry hr = new HandlerRegistry(); + + Servlet servlet = Mockito.mock(Servlet.class); + final ServletInfo info = new ServletInfo("f", "/f", 0, null, servlet, null); + ServletHandler servletHandler = new ServletHandler(null, null, info, info.getServlet()); + hr.addServlet(null, servletHandler); + Servlet servlet2 = Mockito.mock(Servlet.class); + final ServletInfo info2 = new ServletInfo("ff", "/ff", 0, null, servlet2, null); + ServletHandler servletHandler2 = new ServletHandler(null, null, info2, info2.getServlet()); + hr.addServlet(null, servletHandler2); + Filter filter = Mockito.mock(Filter.class); + final FilterInfo fi = new FilterInfo("f", "/f", 0, null, filter, null); + FilterHandler filterHandler = new FilterHandler(null, filter, fi); + hr.addFilter(filterHandler); + + assertEquals(2, hr.getServlets().length); + assertEquals("Most specific Alias should come first", + "/ff", hr.getServlets()[0].getAlias()); + assertEquals("/f", hr.getServlets()[1].getAlias()); + assertEquals(1, hr.getFilters().length); + assertSame(filter, hr.getFilters()[0].getFilter()); + + Mockito.verify(servlet, Mockito.never()).destroy(); + Mockito.verify(servlet2, Mockito.never()).destroy(); + Mockito.verify(filter, Mockito.never()).destroy(); + hr.removeAll(); + Mockito.verify(servlet, Mockito.times(1)).destroy(); + Mockito.verify(servlet2, Mockito.times(1)).destroy(); + Mockito.verify(filter, Mockito.times(1)).destroy(); + + assertEquals(0, hr.getServlets().length); + assertEquals(0, hr.getFilters().length); + } + */ +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/registry/PathResolverFactoryTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/registry/PathResolverFactoryTest.java new file mode 100644 index 00000000000..46dbe23d756 --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/registry/PathResolverFactoryTest.java @@ -0,0 +1,78 @@ +/* + * 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.http.base.internal.registry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +public class PathResolverFactoryTest { + + private void assertResult(final PathResolver resolver, + final String path, + final String expectedServletPath, + final String expectedPathInfo) + { + final PathResolution pr = resolver.resolve(path); + assertNotNull(pr); + assertEquals(path, pr.requestURI); + assertEquals(expectedServletPath, pr.servletPath); + if ( expectedPathInfo == null ) + { + assertNull(pr.pathInfo); + } + else + { + assertEquals(expectedPathInfo, pr.pathInfo); + } + } + + @Test public void testRootMatching() + { + final PathResolver pr = PathResolverFactory.createPatternMatcher(null, ""); + assertNotNull(pr); + + assertResult(pr, "/", "", "/"); + assertResult(pr, "", "", "/"); + + assertNull(pr.resolve("/foo")); + } + + @Test public void testDefaultMatcher() + { + final PathResolver pr = PathResolverFactory.createPatternMatcher(null, "/"); + assertNotNull(pr); + + assertResult(pr, "/foo/bar", "/foo/bar", null); + assertResult(pr, "/foo", "/foo", null); + } + + @Test public void testPathMatcher() + { + final PathResolver pr = PathResolverFactory.createPatternMatcher(null, "/*"); + assertNotNull(pr); + + assertResult(pr, "/foo", "", "/foo"); + assertResult(pr, "/foo/bar", "", "/foo/bar"); + + assertResult(pr, "/", "", "/"); + + assertResult(pr, "", "", null); + } +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/registry/PerContextHandlerRegistryTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/registry/PerContextHandlerRegistryTest.java new file mode 100644 index 00000000000..27334879018 --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/registry/PerContextHandlerRegistryTest.java @@ -0,0 +1,116 @@ +/* + * 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.http.base.internal.registry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.felix.http.base.internal.HttpConfig; +import org.apache.felix.http.base.internal.runtime.ServletContextHelperInfo; +import org.junit.Test; + +/** + * Test for the ordering of servlet contexts + */ +public class PerContextHandlerRegistryTest +{ + + @Test + public void testPathOrdering() + { + final List list = new ArrayList<>(); + list.add(new PerContextHandlerRegistry(createServletContextHelperInfo("/", 1L, 0), new HttpConfig())); + list.add(new PerContextHandlerRegistry(createServletContextHelperInfo("/foo", 2L, 0), new HttpConfig())); + list.add(new PerContextHandlerRegistry(createServletContextHelperInfo("/", 3L, 0), new HttpConfig())); + list.add(new PerContextHandlerRegistry(createServletContextHelperInfo("/bar", 4L, 0), new HttpConfig())); + + Collections.sort(list); + + assertEquals(2L, list.get(0).getContextServiceId()); + assertEquals(4L, list.get(1).getContextServiceId()); + assertEquals(1L, list.get(2).getContextServiceId()); + assertEquals(3L, list.get(3).getContextServiceId()); + } + + @Test + public void testRankingOrdering() + { + final List list = new ArrayList<>(); + list.add(new PerContextHandlerRegistry(createServletContextHelperInfo("/", 1L, 0), new HttpConfig())); + list.add(new PerContextHandlerRegistry(createServletContextHelperInfo("/", 2L, 0), new HttpConfig())); + list.add(new PerContextHandlerRegistry(createServletContextHelperInfo("/", 3L, -30), new HttpConfig())); + list.add(new PerContextHandlerRegistry(createServletContextHelperInfo("/", 4L, 50), new HttpConfig())); + + Collections.sort(list); + + assertEquals(4L, list.get(0).getContextServiceId()); + assertEquals(1L, list.get(1).getContextServiceId()); + assertEquals(2L, list.get(2).getContextServiceId()); + assertEquals(3L, list.get(3).getContextServiceId()); + } + + @Test + public void testOrderingSymetry() + { + testSymetry("/", "/foo", 1L, 2L, 0, 0); + testSymetry("/", "/", 1L, 2L, 0, 10); + testSymetry("/", "/", 1L, 2L, 0, 0); + testSymetry("/", "/", 1L, -2L, 0, 0); + testSymetry("/", "/", -1L, -2L, 0, 0); + testSymetry("/", "/", 0L, -1L, 0, 0); + testSymetry("/", "/", 0L, 1L, 0, 0); + } + + private void testSymetry(String path, String otherPath, long id, long otherId, int ranking, int otherRanking) + { + PerContextHandlerRegistry handlerRegistry = new PerContextHandlerRegistry(createServletContextHelperInfo(path, id, ranking), new HttpConfig()); + PerContextHandlerRegistry other = new PerContextHandlerRegistry(createServletContextHelperInfo(otherPath, otherId, otherRanking), new HttpConfig()); + + assertEquals(handlerRegistry.compareTo(other), -other.compareTo(handlerRegistry)); + } + + @Test + public void testOrderingTransitivity() + { + testTransitivity("/", "/foo", "/barrr", 1L, 2L, 3L, 0, 0, 0); + testTransitivity("/", "/", "/", 1L, 2L, 3L, 1, 2, 3); + testTransitivity("/", "/", "/", 2L, 1L, 0L, 1, 2, 3); + testTransitivity("/", "/", "/", 2L, 1L, 0L, 0, 0, 0); + testTransitivity("/", "/", "/", 1L, -1L, 0L, 0, 0, 0); + testTransitivity("/", "/", "/", -2L, -1L, 0L, 0, 0, 0); + } + + private void testTransitivity(String highPath, String midPath, String lowPath, long highId, long midId, long lowId, int highRanking, int midRanking, int lowRanking) + { + PerContextHandlerRegistry high = new PerContextHandlerRegistry(createServletContextHelperInfo(highPath, highId, highRanking), new HttpConfig()); + PerContextHandlerRegistry mid = new PerContextHandlerRegistry(createServletContextHelperInfo(midPath, midId, midRanking), new HttpConfig()); + PerContextHandlerRegistry low = new PerContextHandlerRegistry(createServletContextHelperInfo(lowPath, lowId, lowRanking), new HttpConfig()); + + assertTrue(high.compareTo(mid) > 0); + assertTrue(mid.compareTo(low) > 0); + assertTrue(high.compareTo(low) > 0); + } + + private ServletContextHelperInfo createServletContextHelperInfo(final String path, final long serviceId, final int ranking) + { + return new ServletContextHelperInfo(ranking, serviceId, "", path, null); + } +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/registry/ServletRegistryTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/registry/ServletRegistryTest.java new file mode 100644 index 00000000000..3791fe385e3 --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/registry/ServletRegistryTest.java @@ -0,0 +1,269 @@ +/* + * 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.http.base.internal.registry; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; + +import org.apache.felix.http.base.internal.context.ExtServletContext; +import org.apache.felix.http.base.internal.handler.HttpServiceServletHandler; +import org.apache.felix.http.base.internal.handler.ServletHandler; +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.apache.felix.http.base.internal.runtime.dto.FailedDTOHolder; +import org.junit.Test; +import org.mockito.Matchers; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.runtime.dto.FailedServletDTO; +import org.osgi.service.http.runtime.dto.ServletContextDTO; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; + +public class ServletRegistryTest { + + private final ServletRegistry reg = new ServletRegistry(); + + private void assertEmpty(final ServletContextDTO dto, final FailedDTOHolder holder) + { + assertNull(dto.servletDTOs); + assertNull(dto.resourceDTOs); + assertTrue(holder.failedResourceDTOs.isEmpty()); + assertTrue(holder.failedServletDTOs.isEmpty()); + } + + private void clear(final ServletContextDTO dto, final FailedDTOHolder holder) + { + dto.servletDTOs = null; + dto.resourceDTOs = null; + holder.failedResourceDTOs.clear(); + holder.failedServletDTOs.clear(); + } + + @Test public void testSingleServlet() throws InvalidSyntaxException, ServletException + { + final FailedDTOHolder holder = new FailedDTOHolder(); + final ServletContextDTO dto = new ServletContextDTO(); + + // empty reg + reg.getRuntimeInfo(dto, holder.failedServletDTOs, holder.failedResourceDTOs); + assertEmpty(dto, holder); + + // register servlet + final ServletHandler h1 = createServletHandler(1L, 0, "/foo"); + reg.addServlet(h1); + + verify(h1.getServlet()).init(Matchers.any(ServletConfig.class)); + + // one entry in reg + // check DTO + clear(dto, holder); + reg.getRuntimeInfo(dto, holder.failedServletDTOs, holder.failedResourceDTOs); + assertNull(dto.resourceDTOs); + assertTrue(holder.failedResourceDTOs.isEmpty()); + assertTrue(holder.failedServletDTOs.isEmpty()); + assertNotNull(dto.servletDTOs); + assertEquals(1, dto.servletDTOs.length); + assertEquals(1, dto.servletDTOs[0].patterns.length); + assertEquals("/foo", dto.servletDTOs[0].patterns[0]); + + // remove servlet + final Servlet s = h1.getServlet(); + reg.removeServlet(h1.getServletInfo(), true); + verify(s).destroy(); + + // empty again + clear(dto, holder); + reg.getRuntimeInfo(dto, holder.failedServletDTOs, holder.failedResourceDTOs); + assertEmpty(dto, holder); + } + + @Test public void testSimpleHiding() throws InvalidSyntaxException, ServletException + { + final FailedDTOHolder holder = new FailedDTOHolder(); + final ServletContextDTO dto = new ServletContextDTO(); + + // empty reg + reg.getRuntimeInfo(dto, holder.failedServletDTOs, holder.failedResourceDTOs); + assertEmpty(dto, holder); + + // register servlets + final ServletHandler h1 = createServletHandler(1L, 10, "/foo"); + reg.addServlet(h1); + verify(h1.getServlet()).init(Matchers.any(ServletConfig.class)); + + final ServletHandler h2 = createServletHandler(2L, 0, "/foo"); + reg.addServlet(h2); + verify(h2.getServlet(), never()).init(Matchers.any(ServletConfig.class)); + verify(h1.getServlet(), never()).destroy(); + + // two entries in reg + // h1 is active + // h2 is hidden + clear(dto, holder); + reg.getRuntimeInfo(dto, holder.failedServletDTOs, holder.failedResourceDTOs); + assertNull(dto.resourceDTOs); + assertTrue(holder.failedResourceDTOs.isEmpty()); + assertFalse(holder.failedServletDTOs.isEmpty()); + assertNotNull(dto.servletDTOs); + assertEquals(1, dto.servletDTOs.length); + assertEquals(1, dto.servletDTOs[0].patterns.length); + assertEquals("/foo", dto.servletDTOs[0].patterns[0]); + assertEquals(1, dto.servletDTOs[0].serviceId); + assertEquals(1, holder.failedServletDTOs.size()); + final FailedServletDTO failedDTO = holder.failedServletDTOs.iterator().next(); + assertEquals(1, failedDTO.patterns.length); + assertEquals("/foo", failedDTO.patterns[0]); + assertEquals(2, failedDTO.serviceId); + + // remove servlet 1 + final Servlet s1 = h1.getServlet(); + reg.removeServlet(h1.getServletInfo(), true); + verify(s1).destroy(); + verify(h2.getServlet()).init(Matchers.any(ServletConfig.class)); + + // h2 is active + clear(dto, holder); + reg.getRuntimeInfo(dto, holder.failedServletDTOs, holder.failedResourceDTOs); + assertNull(dto.resourceDTOs); + assertTrue(holder.failedResourceDTOs.isEmpty()); + assertTrue(holder.failedServletDTOs.isEmpty()); + assertNotNull(dto.servletDTOs); + assertEquals(1, dto.servletDTOs.length); + assertEquals(1, dto.servletDTOs[0].patterns.length); + assertEquals("/foo", dto.servletDTOs[0].patterns[0]); + assertEquals(2, dto.servletDTOs[0].serviceId); + + // remove servlet 2 + final Servlet s2 = h2.getServlet(); + reg.removeServlet(h2.getServletInfo(), true); + verify(s2).destroy(); + + // empty again + clear(dto, holder); + reg.getRuntimeInfo(dto, holder.failedServletDTOs, holder.failedResourceDTOs); + assertEmpty(dto, holder); + } + + @Test public void testMatcherOrdering() throws InvalidSyntaxException + { + final ServletHandler h1 = createServletHandler(1L, 0, "/foo"); + final ServletHandler h2 = createServletHandler(2L, 0, "/foo/*"); + + final List resolvers = new ArrayList(); + resolvers.add(PathResolverFactory.createPatternMatcher(h1, "/foo")); + resolvers.add(PathResolverFactory.createPatternMatcher(h2, "/foo/*")); + + Collections.sort(resolvers); + assertEquals("/foo", resolvers.get(0).getPattern()); + assertEquals("/foo/*", resolvers.get(1).getPattern()); + } + + @Test public void testServletOrdering() throws InvalidSyntaxException + { + final ServletHandler h1 = createServletHandler(1L, 0, "/foo"); + reg.addServlet(h1); + final ServletHandler h2 = createServletHandler(2L, 0, "/foo/*"); + reg.addServlet(h2); + final ServletHandler h3 = createServletHandler(3L, 0, "/foo/rsrc"); + reg.addServlet(h3); + final ServletHandler h4 = createServletHandler(4L, 0, "/foo/rsrc/*"); + reg.addServlet(h4); + final ServletHandler h5 = createServletHandler(5L, 0, "/other"); + reg.addServlet(h5); + + PathResolution pr = null; + + pr = reg.resolve("/foo"); + assertNotNull(pr); + assertEquals("/foo", pr.patterns[0]); + assertEquals(h1, pr.handler); + + pr = reg.resolve("/fool"); + assertNull(pr); + + pr = reg.resolve("/foo/bar"); + assertEquals("/foo", pr.patterns[0]); + assertEquals(h1, pr.handler); + + pr = reg.resolve("/foo/rsrc"); + assertEquals("/foo/rsrc", pr.patterns[0]); + assertEquals(h3, pr.handler); + + pr = reg.resolve("/foo/rsrc/some"); + assertEquals("/foo/rsrc", pr.patterns[0]); + assertEquals(h3, pr.handler); + + pr = reg.resolve("/other"); + assertEquals("/other", pr.patterns[0]); + assertEquals(h5, pr.handler); + + pr = reg.resolve("/other/bla"); + assertEquals("/other", pr.patterns[0]); + assertEquals(h5, pr.handler); + + // cleanup + reg.removeServlet(h1.getServletInfo(), true); + reg.removeServlet(h2.getServletInfo(), true); + reg.removeServlet(h3.getServletInfo(), true); + reg.removeServlet(h4.getServletInfo(), true); + reg.removeServlet(h5.getServletInfo(), true); + } + + private static ServletInfo createServletInfo(final long id, final int ranking, final String... paths) throws InvalidSyntaxException + { + final BundleContext bCtx = mock(BundleContext.class); + when(bCtx.createFilter(Matchers.anyString())).thenReturn(null); + final Bundle bundle = mock(Bundle.class); + when(bundle.getBundleContext()).thenReturn(bCtx); + + final ServiceReference ref = mock(ServiceReference.class); + when(ref.getBundle()).thenReturn(bundle); + when(ref.getProperty(Constants.SERVICE_ID)).thenReturn(id); + when(ref.getProperty(Constants.SERVICE_RANKING)).thenReturn(ranking); + when(ref.getProperty(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN)).thenReturn(paths); + when(ref.getPropertyKeys()).thenReturn(new String[0]); + final ServletInfo si = new ServletInfo(ref); + + return si; + } + + private static ServletHandler createServletHandler(final long id, final int ranking, final String... paths) throws InvalidSyntaxException + { + final ServletInfo si = createServletInfo(id, ranking, paths); + final ExtServletContext ctx = mock(ExtServletContext.class); + final Servlet servlet = mock(Servlet.class); + + return new HttpServiceServletHandler(ctx, si, servlet); + } +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/runtime/AbstractInfoOrderingTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/runtime/AbstractInfoOrderingTest.java new file mode 100644 index 00000000000..1b9157828ed --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/runtime/AbstractInfoOrderingTest.java @@ -0,0 +1,149 @@ +/* + * 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.http.base.internal.runtime; + +import static java.lang.Integer.signum; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.fail; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class AbstractInfoOrderingTest +{ + @Parameters + public static Collection data() { + return Arrays.asList(new Object[][] { + // Expected value must be non-negative + // negative values are tested by symmetry + + // same service id (note: rank must be identical) + { 0, 0, 0, 1, 1 }, + { 0, 1, 1, 0, 0 }, + { 0, -1, -1, -1, -1 }, + // rank has priority + { 1, 0, 1, 1, 0 }, + { 1, 0, 1, -1, 0 }, + { 1, -1, 0, 1, 0 }, + { 1, -1, 0, -1, 0 }, + // same rank + { 1, 1, 1, 2, 1 }, + { 1, -1, -1, -1, 1 }, + { 1, 0, 0, 2, 1 }, + { 1, 0, 0, 1, 0 }, + { 1, 0, 0, -1, 1 }, + { 1, 0, 0, -1, 1 }, + { 1, 0, 0, -2, 1 }, + { 1, 0, 0, -1, 2 }, + { 1, 0, 0, -1, 1 }, + { 1, 0, 0, -2, -1 } + }); + } + + private final int expected; + private final TestInfo testInfo; + private final TestInfo other; + + public AbstractInfoOrderingTest(int expected, + int testRank, int otherRank, long testId, long otherId) + { + if (expected < 0) + { + throw new IllegalArgumentException("Expected values must be non-negative."); + } + this.expected = expected; + testInfo = new TestInfo(testRank, testId); + other = new TestInfo(otherRank, otherId); + } + + @Test + public void ordering() + { + assertEquals(expected, signum(testInfo.compareTo(other))); + if ( expected != 0 ) + { + final List list = new ArrayList(); + list.add(testInfo); + list.add(other); + Collections.sort(list); + if ( expected == -1 ) + { + assertEquals(testInfo, list.get(0)); + assertEquals(other, list.get(1)); + } + else + { + assertEquals(testInfo, list.get(1)); + assertEquals(other, list.get(0)); + } + } + } + + @Test + public void orderingSymetry() + { + assertTrue(signum(testInfo.compareTo(other)) == -signum(other.compareTo(testInfo))); + } + + @Test + public void orderingTransitivity() + { + assertTrue(testInfo.compareTo(other) >= 0); + + TestInfo three = new TestInfo(0, 0); + + // three falls in between the two other points + if (testInfo.compareTo(three) >= 0 && three.compareTo(other) >= 0) + { + assertTrue(testInfo.compareTo(other) >= 0); + } + // three falls below the two other points + else if (testInfo.compareTo(other) >= 0 && other.compareTo(three) >= 0) + { + assertTrue(testInfo.compareTo(three) >= 0); + } + // three falls above the two other points + else if (three.compareTo(testInfo) >= 0 && testInfo.compareTo(other) >= 0) + { + assertTrue(three.compareTo(other) >= 0); + } + else + { + fail("Since testInfo >= other, one of the above cases must match"); + } + } + + private static class TestInfo extends AbstractInfo + { + public TestInfo(int ranking, long serviceId) + { + super(ranking, serviceId); + } + } +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/runtime/ListenerInfoTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/runtime/ListenerInfoTest.java new file mode 100755 index 00000000000..08faaf370f5 --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/runtime/ListenerInfoTest.java @@ -0,0 +1,75 @@ +/* + * 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.http.base.internal.runtime; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.EventListener; + +import javax.servlet.ServletContextListener; + +import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; + +public class ListenerInfoTest +{ + @Test + public void testEnabled() throws InvalidSyntaxException { + Bundle b = mock(Bundle.class); + BundleContext bc = mock(BundleContext.class); + + ServiceReference ref = mock(ServiceReference.class); + when(ref.getProperty(Constants.SERVICE_ID)).thenReturn(1L); + when(ref.getProperty(Constants.OBJECTCLASS)) + .thenReturn(new String[] { ServletContextListener.class.getName() }); + when(ref.getBundle()).thenReturn(b); + when(b.getBundleContext()).thenReturn(bc); + when(bc.createFilter(ArgumentMatchers.any(String.class))).thenReturn(mock(Filter.class)); + + // string true + when(ref.getProperty(HttpWhiteboardConstants.HTTP_WHITEBOARD_LISTENER)).thenReturn("true"); + assertTrue(new ListenerInfo(ref).isValid()); + + // string false + when(ref.getProperty(HttpWhiteboardConstants.HTTP_WHITEBOARD_LISTENER)).thenReturn("false"); + assertFalse(new ListenerInfo(ref).isValid()); + + // string some text + when(ref.getProperty(HttpWhiteboardConstants.HTTP_WHITEBOARD_LISTENER)).thenReturn("text"); + assertFalse(new ListenerInfo(ref).isValid()); + + // string true + when(ref.getProperty(HttpWhiteboardConstants.HTTP_WHITEBOARD_LISTENER)).thenReturn(Boolean.TRUE); + assertTrue(new ListenerInfo(ref).isValid()); + + // string false + when(ref.getProperty(HttpWhiteboardConstants.HTTP_WHITEBOARD_LISTENER)).thenReturn(Boolean.FALSE); + assertFalse(new ListenerInfo(ref).isValid()); + } +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/runtime/dto/RuntimeDTOBuilderTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/runtime/dto/RuntimeDTOBuilderTest.java new file mode 100644 index 00000000000..ce77e423979 --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/runtime/dto/RuntimeDTOBuilderTest.java @@ -0,0 +1,734 @@ +/* + * 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.http.base.internal.runtime.dto; + + +//@RunWith(MockitoJUnitRunner.class) +public class RuntimeDTOBuilderTest +{ +/* + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private static final Long ID_0 = -ID_COUNTER.incrementAndGet(); + private static final Long ID_A = ID_COUNTER.incrementAndGet(); + private static final Long ID_B = ID_COUNTER.incrementAndGet(); + private static final Long ID_C = ID_COUNTER.incrementAndGet(); + private static final Long ID_D = ID_COUNTER.incrementAndGet(); + + private static final Long ID_LISTENER_1 = ID_COUNTER.incrementAndGet(); + private static final Long ID_LISTENER_2 = ID_COUNTER.incrementAndGet(); + + private static final List CONTEXT_NAMES = Arrays.asList("0", "A", "B"); + + @SuppressWarnings("serial") + private static final Map> CONTEXT_ENTITY_NAMES = new HashMap>() + { + { + put("0", Arrays.asList("1")); + put("A", Arrays.asList("A_1")); + put("B", Arrays.asList("B_1", "B_2")); + } + }; + + @SuppressWarnings("serial") + private static final Map RUNTIME_ATTRIBUTES = new HashMap() + { + { + put("attr_1", "val_1"); + put("attr_2", "val_2"); + } + }; + + @Mock private Bundle bundle; + + @Mock private DTO testDTO; + + @Mock private ExtServletContext context_0; + @Mock private ExtServletContext context_A; + @Mock private ExtServletContext context_B; + @Mock private ExtServletContext context_C; + @Mock private ExtServletContext context_D; + + @Mock private ServiceReference listener_1; + @Mock private ServiceReference listener_2; + + @Mock private ServiceReference resource; + + @Mock private ServiceReference runtimeReference; + + private RegistryRuntime registry; + private Map runtimeAttributes; + + @Before + public void setUp() + { + registry = null; + runtimeAttributes = RUNTIME_ATTRIBUTES; + when(bundle.getBundleId()).thenReturn(47L); + when(runtimeReference.getBundle()).thenReturn(bundle); + when(runtimeReference.getUsingBundles()).thenReturn(null); + when(runtimeReference.getPropertyKeys()).thenReturn(runtimeAttributes.keySet().toArray(new String[5])); + for(final String key : runtimeAttributes.keySet()) + { + when(runtimeReference.getProperty(key)).thenReturn(runtimeAttributes.get(key)); + } + when(runtimeReference.getProperty(Constants.SERVICE_ID)).thenReturn(39L); + } + + public ServletContextHelperRuntime setupContext(ServletContext context, String name, long serviceId) + { + when(context.getServletContextName()).thenReturn(name); + + String path = "/" + name; + when(context.getContextPath()).thenReturn(path); + + List initParameterNames = asList("param_1", "param_2"); + when(context.getInitParameterNames()).thenReturn(Collections.enumeration(initParameterNames)); + when(context.getInitParameter("param_1")).thenReturn("init_val_1"); + when(context.getInitParameter("param_2")).thenReturn("init_val_2"); + + Map initParameters = createInitParameterMap(); + ServletContextHelperInfo contextInfo = createContextInfo(0, serviceId, name, path, initParameters); + + ContextHandler contextHandler = new ContextHandler(contextInfo, context, bundle); + PerContextEventListener eventListener = contextHandler.getListenerRegistry(); + + ServletContext sharedContext = contextHandler.getSharedContext(); + sharedContext.setAttribute("intAttr", 1); + sharedContext.setAttribute("dateAttr", new Date()); + sharedContext.setAttribute("stringAttr", "one"); + sharedContext.setAttribute("dtoAttr", testDTO); + + // TODO + return null; +// return contextHandler; + } + + private Map createInitParameterMap() + { + Map initParameters = new HashMap(); + initParameters.put("param_1", "init_val_1"); + initParameters.put("param_2", "init_val_2"); + return initParameters; + } + + @SuppressWarnings("unchecked") + public Map>> setupListeners() + { + Map>> listenerRuntimes = new HashMap>>(); + + listenerRuntimes.put(ID_0, asList(listener_1, listener_2)); + listenerRuntimes.put(ID_A, Arrays.>asList(listener_1)); + listenerRuntimes.put(ID_B, asList(listener_1, listener_2)); + + when(listener_1.getProperty(Constants.SERVICE_ID)).thenReturn(ID_LISTENER_1); + when(listener_1.getProperty(Constants.OBJECTCLASS)) + .thenReturn(new String[] { "org.test.interface_1" }); + + when(listener_2.getProperty(Constants.SERVICE_ID)).thenReturn(ID_LISTENER_2); + when(listener_2.getProperty(Constants.OBJECTCLASS)) + .thenReturn(new String[] { "org.test.interface_1", "org.test.interface_2" }); + + return listenerRuntimes; + } + + public void setupResource() + { + when(resource.getProperty(HttpWhiteboardConstants.HTTP_WHITEBOARD_RESOURCE_PATTERN)).thenReturn(new String[] { "/" }); + when(resource.getProperty(HttpWhiteboardConstants.HTTP_WHITEBOARD_RESOURCE_PREFIX)).thenReturn("prefix"); + } + + @Test + public void buildRuntimeDTO() + { + ServletContextHelperRuntime contextHelper_0 = setupContext(context_0, "0", ID_0); + ServletContextHelperRuntime contextHelper_A = setupContext(context_A, "A", ID_A); + ServletContextHelperRuntime contextHelper_B = setupContext(context_B, "B", ID_B); + + List servlets = new ArrayList(); + List resources = new ArrayList(); + + servlets.add(createTestServlet("1", context_0, ID_0)); + resources.add(createTestServlet("1", context_0, ID_0)); + List filters_0 = asList(createTestFilter("1", context_0)); + List errorPages_0 = asList(createErrorPage("E_1", context_0, ID_0)); + ContextRuntime contextRuntime_0 = new ContextRuntime(null, errorPages_0, null, null); + + servlets.add(createTestServlet("A_1", context_A, ID_A)); + resources.add(createTestServlet("A_1", context_A, ID_A)); + List filters_A = asList(createTestFilter("A_1", context_A)); + List errorPages_A = asList(createErrorPage("E_A_1", context_A, ID_A)); + ContextRuntime contextRuntime_A = new ContextRuntime(null, errorPages_A, null, null); + + servlets.addAll(asList(createTestServletWithServiceId("B_1", context_B, ID_B), + createTestServletWithServiceId("B_2", context_B, ID_B))); + resources.addAll(asList(createTestServletWithServiceId("B_1", context_B, ID_B), + createTestServletWithServiceId("B_2", context_B, ID_B))); + List filters_B = asList(createTestFilterWithServiceId("B_1", context_B), + createTestFilterWithServiceId("B_2", context_B)); + List errorPages_B = asList(createErrorPageWithServiceId("E_B_1", context_B, ID_B), + createErrorPageWithServiceId("E_B_2", context_B, ID_B)); + ContextRuntime contextRuntime_B = new ContextRuntime(null, errorPages_B, null, null); + + Map>> listenerRuntimes = setupListeners(); + + setupRegistry(asList(contextHelper_0, contextHelper_A, contextHelper_B), + asList(contextRuntime_0, contextRuntime_A, contextRuntime_B), +// new ServletRegistryRuntime(servlets, resources), + listenerRuntimes, + null); + + RuntimeDTO runtimeDTO = new RuntimeDTOBuilder(registry, runtimeReference).build(); + + assertEquals(0, runtimeDTO.failedErrorPageDTOs.length); + assertEquals(0, runtimeDTO.failedFilterDTOs.length); + assertEquals(0, runtimeDTO.failedListenerDTOs.length); + assertEquals(0, runtimeDTO.failedResourceDTOs.length); + assertEquals(0, runtimeDTO.failedServletContextDTOs.length); + assertEquals(0, runtimeDTO.failedServletDTOs.length); + + assertServletContextDTOs(runtimeDTO); + } + + private void assertServletContextDTOs(RuntimeDTO runtimeDTO) + { + SortedSet seenServiceIds = new TreeSet(); + + assertEquals(3, runtimeDTO.servletContextDTOs.length); + + for (ServletContextDTO servletContextDTO : runtimeDTO.servletContextDTOs) + { + String contextName = servletContextDTO.name; + assertTrue(CONTEXT_NAMES.contains(contextName)); + if (contextName.equals("0")) + { + assertTrue(contextName, + servletContextDTO.serviceId < 0); + assertEquals(contextName, + 1, servletContextDTO.servletDTOs.length); + assertEquals(contextName, + 1, servletContextDTO.filterDTOs.length); + assertEquals(contextName, + 1, servletContextDTO.resourceDTOs.length); + assertEquals(contextName, + 1, servletContextDTO.errorPageDTOs.length); + assertEquals(contextName, + 2, servletContextDTO.listenerDTOs.length); + } + else + { + int expectedId = CONTEXT_NAMES.indexOf(contextName) + 1; + assertEquals(contextName, + expectedId, servletContextDTO.serviceId); + + int expectedChildren = CONTEXT_NAMES.indexOf(contextName); + assertEquals(contextName, + expectedChildren, servletContextDTO.servletDTOs.length); + assertEquals(contextName, + expectedChildren, servletContextDTO.filterDTOs.length); + assertEquals(contextName, + expectedChildren, servletContextDTO.resourceDTOs.length); + assertEquals(contextName, + expectedChildren, servletContextDTO.errorPageDTOs.length); + assertEquals(contextName, + expectedChildren, servletContextDTO.listenerDTOs.length); + } + seenServiceIds.add(servletContextDTO.serviceId); + + assertEquals(contextName, + 3, servletContextDTO.attributes.size()); + assertEquals(contextName, + 1, servletContextDTO.attributes.get("intAttr")); + assertEquals(contextName, + "one", servletContextDTO.attributes.get("stringAttr")); + assertEquals(contextName, + testDTO, servletContextDTO.attributes.get("dtoAttr")); + + assertEquals(contextName, + 2, servletContextDTO.initParams.size()); + assertEquals(contextName, + "init_val_1", servletContextDTO.initParams.get("param_1")); + assertEquals(contextName, + "init_val_2", servletContextDTO.initParams.get("param_2")); + + assertEquals(contextName, + "/" + contextName + "/" + contextName, servletContextDTO.contextPath); + + Collection serviceIds = assertServletDTOs(contextName, + servletContextDTO.serviceId, servletContextDTO.servletDTOs); + seenServiceIds.addAll(serviceIds); + + serviceIds = assertFilterDTOs(contextName, + servletContextDTO.serviceId, servletContextDTO.filterDTOs); + seenServiceIds.addAll(serviceIds); + + serviceIds = assertResourceDTOs(contextName, + servletContextDTO.serviceId, servletContextDTO.resourceDTOs); + seenServiceIds.addAll(serviceIds); + + serviceIds = assertErrorPageDTOs(contextName, + servletContextDTO.serviceId, servletContextDTO.errorPageDTOs); + seenServiceIds.addAll(serviceIds); + + serviceIds = assertListenerDTOs(contextName, + servletContextDTO.serviceId, servletContextDTO.listenerDTOs); + seenServiceIds.addAll(serviceIds); + } + assertEquals(12, seenServiceIds.tailSet(0L).size()); + assertEquals(9, seenServiceIds.headSet(0L).size()); + } + + private Collection assertServletDTOs(String contextName, long contextId, ServletDTO[] dtos) { + List serviceIds = new ArrayList(); + for (ServletDTO servletDTO : dtos) + { + String name = servletDTO.name; + assertTrue(CONTEXT_ENTITY_NAMES.get(contextName).contains(name)); + + if (contextId != ID_B) + { + assertTrue(name, + servletDTO.serviceId < 0); + } + else + { + assertTrue(name, + servletDTO.serviceId > 0); + } + serviceIds.add(servletDTO.serviceId); + + assertEquals(name, + contextId, servletDTO.servletContextId); + + assertTrue(name, + servletDTO.asyncSupported); + + assertEquals(name, + 2, servletDTO.initParams.size()); + assertEquals(name, + "valOne_" + name, servletDTO.initParams.get("paramOne_" + name)); + assertEquals(name, + "valTwo_" + name, servletDTO.initParams.get("paramTwo_" + name)); + + assertEquals(name, + 1, servletDTO.patterns.length); + assertEquals(name, + "/" + name, servletDTO.patterns[0]); + + assertEquals(name, + "info_" + name, servletDTO.servletInfo); + } + + return serviceIds; + } + + private Collection assertFilterDTOs(String contextName, long contextId, FilterDTO[] dtos) { + List serviceIds = new ArrayList(); + for (FilterDTO filterDTO : dtos) + { + String name = filterDTO.name; + assertTrue(CONTEXT_ENTITY_NAMES.get(contextName).contains(name)); + + if (contextId != ID_B) + { + assertTrue(name, + filterDTO.serviceId < 0); + } + else + { + assertTrue(name, + filterDTO.serviceId > 0); + } + serviceIds.add(filterDTO.serviceId); + + assertEquals(name, + contextId, filterDTO.servletContextId); + + assertTrue(name, + filterDTO.asyncSupported); + + assertEquals(name, + 2, filterDTO.initParams.size()); + assertEquals(name, + "valOne_" + name, filterDTO.initParams.get("paramOne_" + name)); + assertEquals(name, + "valTwo_" + name, filterDTO.initParams.get("paramTwo_" + name)); + + assertEquals(name, + 1, filterDTO.patterns.length); + assertEquals(name, + "/" + name, filterDTO.patterns[0]); + + assertEquals(name, + 1, filterDTO.regexs.length); + assertEquals(name, + "." + name, filterDTO.regexs[0]); + + assertEquals(name, + 2, filterDTO.dispatcher.length); + assertEquals(name, + "ASYNC", filterDTO.dispatcher[0]); + assertEquals(name, + "REQUEST", filterDTO.dispatcher[1]); + } + + return serviceIds; + } + + private Collection assertResourceDTOs(String contextName, long contextId, ResourceDTO[] dtos) { + List serviceIds = new ArrayList(); + for (ResourceDTO resourceDTO : dtos) + { + if (contextId != ID_B) + { + assertTrue(contextId + " " + contextName, + resourceDTO.serviceId < 0); + } + else + { + assertTrue(contextId + " " + contextName, + resourceDTO.serviceId > 0); + } + serviceIds.add(resourceDTO.serviceId); + + assertEquals(contextId + " " + contextName, + contextId, resourceDTO.servletContextId); + + assertEquals(contextId + " " + contextName, + 1, resourceDTO.patterns.length); + assertTrue(contextId + " " + contextName, + resourceDTO.patterns[0].startsWith("/")); + } + return serviceIds; + } + + private Collection assertErrorPageDTOs(String contextName, long contextId, ErrorPageDTO[] dtos) + { + List serviceIds = new ArrayList(); + for (ErrorPageDTO errorPageDTO : dtos) + { + String name = errorPageDTO.name; + assertTrue(CONTEXT_ENTITY_NAMES.get(contextName).contains(name.substring(2))); + + if (contextId != ID_B) + { + assertTrue(name, + errorPageDTO.serviceId < 0); + } + else + { + assertTrue(name, + errorPageDTO.serviceId > 0); + } + serviceIds.add(errorPageDTO.serviceId); + + assertEquals(name, + contextId, errorPageDTO.servletContextId); + + assertTrue(name, + errorPageDTO.asyncSupported); + + assertEquals(name, + 2, errorPageDTO.initParams.size()); + assertEquals(name, + "valOne_" + name, errorPageDTO.initParams.get("paramOne_" + name)); + assertEquals(name, + "valTwo_" + name, errorPageDTO.initParams.get("paramTwo_" + name)); + + assertEquals(name, + "info_" + name, errorPageDTO.servletInfo); + + assertEquals(name, + 2, errorPageDTO.errorCodes.length); + long[] errorCodes = copyOf(errorPageDTO.errorCodes, 2); + sort(errorCodes); + assertEquals(name, + 400, errorCodes[0]); + assertEquals(name, + 500, errorCodes[1]); + + assertEquals(name, + 2, errorPageDTO.exceptions.length); + String[] exceptions = copyOf(errorPageDTO.exceptions, 2); + sort(exceptions); + assertEquals(name, + "Bad request", exceptions[0]); + assertEquals(name, + "Error", exceptions[1]); + } + + return serviceIds; + } + + private Collection assertListenerDTOs(String contextName, long contextId, ListenerDTO[] dtos) + { + Set serviceIds = new HashSet(); + for (ListenerDTO listenerDTO : dtos) + { + assertEquals(contextId, listenerDTO.servletContextId); + serviceIds.add(listenerDTO.serviceId); + } + + assertEquals(ID_LISTENER_1.longValue(), dtos[0].serviceId); + assertArrayEquals(new String[] { "org.test.interface_1" }, + dtos[0].types); + if (dtos.length > 1) + { + assertEquals(ID_LISTENER_2.longValue(), dtos[1].serviceId); + assertArrayEquals(new String[] { "org.test.interface_1", "org.test.interface_2" }, dtos[1].types); + } + + return serviceIds; + } + + @Test + public void nullValuesInEntities() { + ServletContextHelperRuntime contextHandler = setupContext(context_0, "0", ID_0); + + ServletInfo servletInfo = createServletInfo(0, + ID_COUNTER.incrementAndGet(), + "1", + new String[] { "/*" }, + null, + true, + Collections.emptyMap()); + Servlet servlet = mock(Servlet.class); + ServletHandler servletHandler = new HttpServiceServletHandler(0, context_0, servletInfo, servlet); + when(servlet.getServletInfo()).thenReturn("info_0"); + + FilterInfo filterInfo = createFilterInfo(0, + ID_COUNTER.incrementAndGet(), + "1", + null, + null, + null, + true, + null, + Collections.emptyMap()); + FilterHandler filterHandler = new HttpServiceFilterHandler(0, context_0, filterInfo, mock(Filter.class)); + + ServletInfo resourceInfo = createServletInfo(0, + ID_COUNTER.incrementAndGet(), + "1", + new String[] { "/*" }, + null, + true, + Collections.emptyMap()); + Servlet resource = mock(Servlet.class); + ServletHandler resourceHandler = new HttpServiceServletHandler(ID_0, context_0, resourceInfo, resource); + + ContextRuntime contextRuntime = new ContextRuntime(null, + Collections.emptyList(), + null, null); + setupRegistry(asList(contextHandler), asList(contextRuntime), +// new ServletRegistryRuntime(asList(resourceHandler), asList(servletHandler)), + Collections.>>emptyMap(), + null); + + RuntimeDTO runtimeDTO = new RuntimeDTOBuilder(registry, runtimeReference).build(); + + assertEquals(1, runtimeDTO.servletContextDTOs.length); + assertEquals(1, runtimeDTO.servletContextDTOs[0].servletDTOs.length); + assertEquals(1, runtimeDTO.servletContextDTOs[0].filterDTOs.length); + + assertEquals(emptyMap(), runtimeDTO.servletContextDTOs[0].servletDTOs[0].initParams); + + assertEquals(emptyMap(), runtimeDTO.servletContextDTOs[0].filterDTOs[0].initParams); + assertEquals(0, runtimeDTO.servletContextDTOs[0].filterDTOs[0].patterns.length); + assertEquals(0, runtimeDTO.servletContextDTOs[0].filterDTOs[0].regexs.length); + } + + @Test + public void contextWithNoEntities() { + ServletContextHelperRuntime contextHandler_0 = setupContext(context_0, "0", ID_0); + ServletContextHelperRuntime contextHandler_A = setupContext(context_A, "A", ID_A); + + // TODO + setupRegistry(asList(contextHandler_0, contextHandler_A), +null, // asList(ContextRuntime.empty(ID_0), ContextRuntime.empty(ID_A)), + Collections.>>emptyMap(), + null); + + RuntimeDTO runtimeDTO = new RuntimeDTOBuilder(registry, runtimeReference).build(); + + assertEquals(2, runtimeDTO.servletContextDTOs.length); + assertEquals(0, runtimeDTO.servletContextDTOs[0].servletDTOs.length); + assertEquals(0, runtimeDTO.servletContextDTOs[0].filterDTOs.length); + assertEquals(0, runtimeDTO.servletContextDTOs[1].servletDTOs.length); + assertEquals(0, runtimeDTO.servletContextDTOs[1].filterDTOs.length); + } + + @Test + public void missingPatternInServletThrowsException() + { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("patterns"); + + ServletContextHelperRuntime contextHandler = setupContext(context_0, "0", ID_0); + + ServletInfo servletInfo = createServletInfo(0, + ID_COUNTER.incrementAndGet(), + "1", + null, + null, + true, + Collections.emptyMap()); + Servlet servlet = mock(Servlet.class); + ServletHandler servletHandler = new HttpServiceServletHandler(ID_0, context_0, servletInfo, servlet); + when(servlet.getServletInfo()).thenReturn("info_0"); + + ContextRuntime contextRuntime = new ContextRuntime(Collections.emptyList(), + Collections.emptyList(), + null, null); + setupRegistry(asList(contextHandler), asList(contextRuntime), +// new ServletRegistryRuntime(asList(servletHandler), Collections.emptyList()), + Collections.>>emptyMap(), + null); + + new RuntimeDTOBuilder(registry, runtimeReference).build(); + } + + public FailureRuntime setupFailures() + { + Map, Integer> serviceFailures = new HashMap, Integer>(); + + ServletContextHelperInfo contextInfo = createContextInfo(0, + ID_C, + "context_failure_1", + "/", + createInitParameterMap()); + serviceFailures.put(contextInfo, 1); + + contextInfo = createContextInfo(0, + ID_D, + "context_failure_2", + "/", + createInitParameterMap()); + serviceFailures.put(contextInfo, 2); + + ServletInfo servletInfo = createServletInfo(0, ID_COUNTER.incrementAndGet(), + "servlet_failure_1", + new String[] {"/"}, + null, + false, + createInitParameterMap()); + serviceFailures.put(servletInfo, 3); + + servletInfo = createServletInfo(0, ID_COUNTER.incrementAndGet(), + "servlet_failure_2", + new String[] {"/"}, + null, + false, + createInitParameterMap()); + serviceFailures.put(servletInfo, 4); + + FilterInfo filterInfo = createFilterInfo(0, + ID_COUNTER.incrementAndGet(), + "filter_failure_1", + new String[] {"/"}, + null, + null, + false, + null, + createInitParameterMap()); + serviceFailures.put(filterInfo, 5); + + ServletInfo errorPageInfo = createServletInfo(0, + ID_COUNTER.incrementAndGet(), + "error_failure_1", + null, + new String[] { "405", "TestException" }, + false, + createInitParameterMap()); + serviceFailures.put(errorPageInfo, 6); + + ServletInfo invalidErrorPageInfo = createServletInfo(0, + ID_COUNTER.incrementAndGet(), + "error_failure_2", + new String[] { "/" }, + new String[] { "405", "TestException" }, + false, + createInitParameterMap()); + serviceFailures.put(invalidErrorPageInfo, 7); + + return FailureRuntime.builder().add(serviceFailures).build(); + } + + @Test + public void testFailureDTOs() + { + setupRegistry(Collections.emptyList(), + Collections.emptyList(), + Collections.>>emptyMap(), + setupFailures()); + + RuntimeDTO runtimeDTO = new RuntimeDTOBuilder(registry, runtimeReference).build(); + + assertEquals(0, runtimeDTO.servletContextDTOs.length); + + assertEquals(2, runtimeDTO.failedErrorPageDTOs.length); + assertEquals(1, runtimeDTO.failedFilterDTOs.length); + // ListenerInfo is hard to setup + assertEquals(0, runtimeDTO.failedListenerDTOs.length); + // ResourceInfo is hard to setup + assertEquals(0, runtimeDTO.failedResourceDTOs.length); + assertEquals(2, runtimeDTO.failedServletContextDTOs.length); + assertEquals(2, runtimeDTO.failedServletDTOs.length); + + assertEquals(1, runtimeDTO.failedServletContextDTOs[0].failureReason); + assertEquals(2, runtimeDTO.failedServletContextDTOs[1].failureReason); + assertEquals(3, runtimeDTO.failedServletDTOs[0].failureReason); + assertEquals(4, runtimeDTO.failedServletDTOs[1].failureReason); + assertEquals(5, runtimeDTO.failedFilterDTOs[0].failureReason); + assertEquals(6, runtimeDTO.failedErrorPageDTOs[0].failureReason); + assertEquals(7, runtimeDTO.failedErrorPageDTOs[1].failureReason); + + assertEquals("context_failure_1", runtimeDTO.failedServletContextDTOs[0].name); + assertEquals("context_failure_2", runtimeDTO.failedServletContextDTOs[1].name); + assertEquals("servlet_failure_1", runtimeDTO.failedServletDTOs[0].name); + assertEquals("servlet_failure_2", runtimeDTO.failedServletDTOs[1].name); + assertEquals("filter_failure_1", runtimeDTO.failedFilterDTOs[0].name); + assertEquals("error_failure_1", runtimeDTO.failedErrorPageDTOs[0].name); + assertEquals("error_failure_2", runtimeDTO.failedErrorPageDTOs[1].name); + + assertEquals(ID_C.longValue(), runtimeDTO.failedServletContextDTOs[0].serviceId); + assertEquals(ID_D.longValue(), runtimeDTO.failedServletContextDTOs[1].serviceId); + assertEquals(0, runtimeDTO.failedServletDTOs[0].servletContextId); + assertEquals(0, runtimeDTO.failedServletDTOs[1].servletContextId); + assertEquals(0, runtimeDTO.failedFilterDTOs[0].servletContextId); + assertEquals(0, runtimeDTO.failedErrorPageDTOs[0].servletContextId); + assertEquals(0, runtimeDTO.failedErrorPageDTOs[1].servletContextId); + + assertEquals(0, runtimeDTO.failedServletContextDTOs[0].errorPageDTOs.length); + assertEquals(0, runtimeDTO.failedServletContextDTOs[0].filterDTOs.length); + assertEquals(0, runtimeDTO.failedServletContextDTOs[0].listenerDTOs.length); + assertEquals(0, runtimeDTO.failedServletContextDTOs[0].resourceDTOs.length); + assertEquals(0, runtimeDTO.failedServletContextDTOs[0].servletDTOs.length); + assertEquals(0, runtimeDTO.failedServletContextDTOs[1].errorPageDTOs.length); + assertEquals(0, runtimeDTO.failedServletContextDTOs[1].filterDTOs.length); + assertEquals(0, runtimeDTO.failedServletContextDTOs[1].listenerDTOs.length); + assertEquals(0, runtimeDTO.failedServletContextDTOs[1].resourceDTOs.length); + assertEquals(0, runtimeDTO.failedServletContextDTOs[1].servletDTOs.length); + + assertTrue(runtimeDTO.failedServletContextDTOs[0].attributes.isEmpty()); + assertTrue(runtimeDTO.failedServletContextDTOs[1].attributes.isEmpty()); + } +*/ +} \ No newline at end of file diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/service/HttpServiceFactoryTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/service/HttpServiceFactoryTest.java new file mode 100644 index 00000000000..825350b766d --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/service/HttpServiceFactoryTest.java @@ -0,0 +1,64 @@ +/* + * 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.http.base.internal.service; + +import java.util.Hashtable; + +import javax.servlet.ServletContext; + +import org.apache.felix.http.base.internal.HttpConfig; +import org.apache.felix.http.base.internal.registry.HandlerRegistry; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; +import org.osgi.service.http.HttpService; + +public class HttpServiceFactoryTest { + @Test + public void testGetServiceActiveInActive() throws Exception { + BundleContext bc = Mockito.mock(BundleContext.class); + Mockito.when(bc.createFilter(Mockito.anyString())).then(new Answer() { + @Override + public Filter answer(InvocationOnMock invocation) throws Throwable { + return FrameworkUtil.createFilter((String) invocation.getArguments()[0]); + } + }); + final HandlerRegistry reg = new HandlerRegistry(new HttpConfig()); + reg.init(); + HttpServiceFactory hsf = new HttpServiceFactory(bc, reg); + + Assert.assertNull("Not yet active", + hsf.getService(Mockito.mock(Bundle.class), null)); + + ServletContext sctx = Mockito.mock(ServletContext.class); + hsf.start(sctx, new Hashtable()); + HttpService svc = hsf.getService(Mockito.mock(Bundle.class), null); + Assert.assertNotNull(svc); + + hsf.stop(); + Assert.assertNull("Not active any more", + hsf.getService(Mockito.mock(Bundle.class), null)); + } +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/util/MimeTypesTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/util/MimeTypesTest.java new file mode 100644 index 00000000000..df0fcdb0a52 --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/util/MimeTypesTest.java @@ -0,0 +1,53 @@ +/* + * 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.http.base.internal.util; + +import org.junit.Test; +import org.junit.Assert; + +public class MimeTypesTest +{ + @Test + public void testSingleton() + { + MimeTypes m1 = MimeTypes.get(); + MimeTypes m2 = MimeTypes.get(); + + Assert.assertNotNull(m1); + Assert.assertSame(m1, m2); + + } + + @Test + public void testGetByFile() + { + Assert.assertNull(MimeTypes.get().getByFile(null)); + Assert.assertEquals("text/plain", MimeTypes.get().getByFile("afile.txt")); + Assert.assertEquals("text/xml", MimeTypes.get().getByFile(".xml")); + Assert.assertNull(MimeTypes.get().getByFile("xml")); + Assert.assertNull(MimeTypes.get().getByFile("somefile.notfound")); + } + + @Test + public void testGetByExtension() + { + Assert.assertNull(MimeTypes.get().getByExtension(null)); + Assert.assertEquals("text/plain", MimeTypes.get().getByExtension("txt")); + Assert.assertEquals("text/xml", MimeTypes.get().getByExtension("xml")); + Assert.assertNull(MimeTypes.get().getByExtension("notfound")); + } +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/util/PatternUtilTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/util/PatternUtilTest.java new file mode 100644 index 00000000000..9b8b12c0495 --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/util/PatternUtilTest.java @@ -0,0 +1,58 @@ +/* + * 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.http.base.internal.util; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +/** + * Test cases for {@link PatternUtil}. + */ +public class PatternUtilTest +{ + + @Test + public void testSymbolicName() + { + assertTrue(PatternUtil.isValidSymbolicName("default")); + assertFalse(PatternUtil.isValidSymbolicName("$bad#")); + assertTrue(PatternUtil.isValidSymbolicName("abcdefghijklmnopqrstuvwyz")); + assertTrue(PatternUtil.isValidSymbolicName("ABCDEFGHIJKLMNOPQRSTUVWXYZ")); + assertTrue(PatternUtil.isValidSymbolicName("0123456789-_")); + } + + @Test public void testServletPattern() + { + assertFalse(PatternUtil.isValidPattern(null)); + assertTrue(PatternUtil.isValidPattern("")); + assertTrue(PatternUtil.isValidPattern("*.html")); + assertTrue(PatternUtil.isValidPattern("/")); + assertTrue(PatternUtil.isValidPattern("/test")); + assertTrue(PatternUtil.isValidPattern("/test/*")); + assertTrue(PatternUtil.isValidPattern("/foo/bar")); + assertTrue(PatternUtil.isValidPattern("/foo/bar/*")); + assertFalse(PatternUtil.isValidPattern("/*.html")); + assertFalse(PatternUtil.isValidPattern("/*/foo")); + assertFalse(PatternUtil.isValidPattern("foo")); + assertFalse(PatternUtil.isValidPattern("foo/bla")); + assertFalse(PatternUtil.isValidPattern("/test/")); + } +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/util/UriUtilsTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/util/UriUtilsTest.java new file mode 100644 index 00000000000..41dcca2062b --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/util/UriUtilsTest.java @@ -0,0 +1,139 @@ +/* + * 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.http.base.internal.util; + +import static org.apache.felix.http.base.internal.util.UriUtils.concat; +import static org.apache.felix.http.base.internal.util.UriUtils.decodePath; +import static org.apache.felix.http.base.internal.util.UriUtils.removeDotSegments; +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +/** + * Test cases for {@link UriUtils}. + */ +public class UriUtilsTest +{ + @Test + public void testConcatOk() + { + assertEquals(null, concat(null, null)); + assertEquals("", concat(null, "")); + assertEquals("/", concat(null, "/")); + assertEquals("foo", concat(null, "foo")); + assertEquals("/foo", concat(null, "/foo")); + + assertEquals("", concat("", null)); + assertEquals("/", concat("/", null)); + assertEquals("foo", concat("foo", null)); + assertEquals("/foo", concat("/foo", null)); + + assertEquals("", concat("", "")); + assertEquals("foo", concat("", "foo")); + assertEquals("/", concat("", "/")); + assertEquals("/foo", concat("", "/foo")); + + assertEquals("foo", concat("foo", "")); + assertEquals("/", concat("/", "")); + assertEquals("/foo", concat("/foo", "")); + + assertEquals("foo", concat("foo", "")); + assertEquals("foo/bar", concat("foo", "bar")); + assertEquals("foo/", concat("foo", "/")); + assertEquals("foo/bar", concat("foo", "/bar")); + + assertEquals("/foo", concat("/", "foo")); + assertEquals("/", concat("/", "/")); + assertEquals("/bar", concat("/", "/bar")); + + assertEquals("foo/", concat("foo/", null)); + assertEquals("foo/", concat("foo/", "")); + assertEquals("foo/bar", concat("foo/", "bar")); + assertEquals("foo/", concat("foo/", "/")); + assertEquals("foo/bar", concat("foo/", "/bar")); + + assertEquals("?quu=1", concat("?quu=1", null)); + assertEquals("?quu=1", concat("?quu=1", "")); + assertEquals("foo?quu=1", concat("?quu=1", "foo")); + assertEquals("/?quu=1", concat("?quu=1", "/")); + assertEquals("/foo?quu=1", concat("?quu=1", "/foo")); + + assertEquals("foo?quu=1", concat("foo?quu=1", null)); + assertEquals("foo?quu=1", concat("foo?quu=1", "")); + assertEquals("foo/bar?quu=1", concat("foo?quu=1", "bar")); + assertEquals("foo/?quu=1", concat("foo?quu=1", "/")); + assertEquals("foo/bar?quu=1", concat("foo?quu=1", "/bar")); + + assertEquals("foo/?quu=1", concat("foo/?quu=1", null)); + assertEquals("foo/?quu=1", concat("foo/?quu=1", "")); + assertEquals("foo/bar?quu=1", concat("foo/?quu=1", "bar")); + assertEquals("foo/?quu=1", concat("foo/?quu=1", "/")); + assertEquals("foo/bar?quu=1", concat("foo/?quu=1", "/bar")); + } + + @Test + public void testDecodePathOk() + { + assertEquals(null, decodePath(null)); + assertEquals("foo bar", decodePath("foo%20bar")); + assertEquals("foo%23;,:=b a r", decodePath("foo%2523%3b%2c:%3db%20a%20r")); + assertEquals("f\u00e4\u00e4%23;,:=b a r=", decodePath("f\u00e4\u00e4%2523%3b%2c:%3db%20a%20r%3D")); + assertEquals("f\u0629\u0629%23;,:=b a r", decodePath("f%d8%a9%d8%a9%2523%3b%2c:%3db%20a%20r")); + } + + @Test + public void testRemoveDotSegmentsOk() + { + assertEquals(null, removeDotSegments(null)); + assertEquals("", removeDotSegments("")); + assertEquals("", removeDotSegments(".")); + assertEquals("", removeDotSegments("..")); + assertEquals("/", removeDotSegments("/")); + assertEquals("/", removeDotSegments("/.")); + assertEquals("", removeDotSegments("/..")); + assertEquals("foo", removeDotSegments("./foo")); + assertEquals("/bar/", removeDotSegments("./foo/../bar/")); + assertEquals("foo", removeDotSegments("../foo")); + assertEquals("/", removeDotSegments("/foo/..")); + assertEquals("/foo/", removeDotSegments("/foo/.")); + assertEquals("/foo/bar", removeDotSegments("/foo/./bar")); + assertEquals("/bar", removeDotSegments("/foo/../bar")); + assertEquals("/bar", removeDotSegments("/foo/./../bar")); + assertEquals("/foo/bar", removeDotSegments("/foo/././bar")); + assertEquals("/qux", removeDotSegments("/foo/bar/../../qux")); + assertEquals("/foo/qux/quu", removeDotSegments("/foo/bar/../qux/././quu")); + assertEquals("/bar//", removeDotSegments("/foo/./../bar//")); + assertEquals("/", removeDotSegments("/foo/../bar/..")); + assertEquals("/foo/quu", removeDotSegments("/foo/bar/qux/./../../quu")); + assertEquals("mid/6", removeDotSegments("mid/content=5/../6")); + assertEquals("//bar/qux/file.ext", removeDotSegments("foo/.././/bar/qux/file.ext")); + // weird cases + assertEquals("..foo", removeDotSegments("..foo")); + assertEquals("foo..", removeDotSegments("foo..")); + assertEquals("foo.", removeDotSegments("foo.")); + assertEquals("/.foo", removeDotSegments("/.foo")); + assertEquals("/..foo", removeDotSegments("/..foo")); + + // FELIX-4440 + assertEquals("foo.bar", removeDotSegments("foo.bar")); + assertEquals("/test.jsp", removeDotSegments("/test.jsp")); + assertEquals("http://foo/bar./qux.quu", removeDotSegments("http://foo/bar./qux.quu")); + assertEquals("http://foo/bar.qux/quu", removeDotSegments("http://foo/bar.qux/quu")); + } +} diff --git a/http/base/src/test/java/org/apache/felix/http/base/internal/whiteboard/FailureStateHandlerTest.java b/http/base/src/test/java/org/apache/felix/http/base/internal/whiteboard/FailureStateHandlerTest.java new file mode 100644 index 00000000000..b7789a328b7 --- /dev/null +++ b/http/base/src/test/java/org/apache/felix/http/base/internal/whiteboard/FailureStateHandlerTest.java @@ -0,0 +1,97 @@ +/* + * 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.http.base.internal.whiteboard; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.felix.http.base.internal.runtime.ServletInfo; +import org.apache.felix.http.base.internal.runtime.dto.FailedDTOHolder; +import org.junit.Test; +import org.osgi.service.http.runtime.dto.DTOConstants; +import org.osgi.service.http.runtime.dto.FailedServletDTO; + +public class FailureStateHandlerTest { + + private void assertContainsExactly(final List dtos, final long[] contextIds) + { + assertEquals(dtos.size(), contextIds.length); + final Set set = new HashSet(); + for(final long l : contextIds) + { + set.add(l); + } + for(final FailedServletDTO dto : dtos) + { + assertTrue(set.remove(dto.servletContextId)); + } + } + + @Test public void testAddRemoveNoContext() + { + final ServletInfo info = new ServletInfo("test", "/test", Collections. emptyMap()); + + final FailureStateHandler handler = new FailureStateHandler(); + handler.addFailure(info, DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE); + + final FailedDTOHolder holder = new FailedDTOHolder(); + handler.getRuntimeInfo(holder); + + assertEquals(1, holder.failedServletDTOs.size()); + assertEquals(DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE, holder.failedServletDTOs.get(0).failureReason); + + holder.failedServletDTOs.clear(); + + handler.remove(info); + handler.getRuntimeInfo(holder); + + assertEquals(0, holder.failedServletDTOs.size()); + } + + @Test public void testAddRemoveContext() + { + final ServletInfo info1 = new ServletInfo("test", "/test", Collections. emptyMap()); + final ServletInfo info2 = new ServletInfo("test", "/test", Collections. emptyMap()); + + final FailureStateHandler handler = new FailureStateHandler(); + handler.addFailure(info1, 1L, DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE); + handler.addFailure(info2, 2L, DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE); + + final FailedDTOHolder holder = new FailedDTOHolder(); + handler.getRuntimeInfo(holder); + + assertEquals(2, holder.failedServletDTOs.size()); + assertEquals(DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE, holder.failedServletDTOs.get(0).failureReason); + assertEquals(DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE, holder.failedServletDTOs.get(1).failureReason); + assertContainsExactly(holder.failedServletDTOs, new long[] {1L, 2L}); + + + handler.remove(info1, 1L); + handler.remove(info2, 2L); + + holder.failedServletDTOs.clear(); + handler.getRuntimeInfo(holder); + + assertEquals(0, holder.failedServletDTOs.size()); + } + +} diff --git a/http/base/src/test/resources/org/apache/felix/http/base/internal/context/resource.txt b/http/base/src/test/resources/org/apache/felix/http/base/internal/context/resource.txt new file mode 100644 index 00000000000..bba492d0e33 --- /dev/null +++ b/http/base/src/test/resources/org/apache/felix/http/base/internal/context/resource.txt @@ -0,0 +1,17 @@ +/* + * 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. + */ +Dummy resource... diff --git a/http/bridge/pom.xml b/http/bridge/pom.xml new file mode 100644 index 00000000000..db1167da12f --- /dev/null +++ b/http/bridge/pom.xml @@ -0,0 +1,121 @@ + + + + 4.0.0 + + org.apache.felix + org.apache.felix.http.parent + 12 + ../parent/pom.xml + + + Apache Felix Http Bridge + This is an implementation of the OSGi Http Service and the OSGi Http Whiteboard Specification used when run as a web application + org.apache.felix.http.bridge + 4.0.7-SNAPSHOT + bundle + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/http/bridge + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/http/bridge + http://svn.apache.org/viewvc/felix/trunk/http/bridge/ + + + + + + org.apache.felix + maven-bundle-plugin + true + + + + org.apache.felix.http.bridge.internal.BridgeActivator + + + org.osgi.service.http, + org.osgi.service.http.context, + org.osgi.service.http.runtime, + org.osgi.service.http.runtime.dto, + org.osgi.service.http.whiteboard + + + org.apache.felix.http.base.*, + org.apache.felix.http.bridge.internal.* + + + org.apache.commons.* + + + org.osgi.service.useradmin;resolution:=optional;version="[1.1,2)", + org.osgi.service.log;resolution:=optional;version="[1.3,2)", + * + + + osgi.implementation;osgi.implementation="osgi.http";version:Version="1.1"; + uses:="javax.servlet,javax.servlet.http,org.osgi.service.http.context,org.osgi.service.http.whiteboard", + osgi.service;objectClass:List<String>="org.osgi.service.http.runtime.HttpServiceRuntime"; + uses:="org.osgi.service.http.runtime,org.osgi.service.http.runtime.dto" + + + + + + + + + + javax.servlet + javax.servlet-api + + + org.osgi + osgi.core + + + org.osgi + org.osgi.service.http + 1.2.1 + provided + + + org.osgi + org.osgi.service.http.whiteboard + 1.1.0 + provided + + + commons-fileupload + commons-fileupload + 1.3.3 + + + commons-io + commons-io + 2.6 + + + org.apache.felix + org.apache.felix.http.base + 4.0.7-SNAPSHOT + + + + diff --git a/http/bridge/src/main/appended-resources/META-INF/DEPENDENCIES b/http/bridge/src/main/appended-resources/META-INF/DEPENDENCIES new file mode 100644 index 00000000000..e1fd81f90c0 --- /dev/null +++ b/http/bridge/src/main/appended-resources/META-INF/DEPENDENCIES @@ -0,0 +1,16 @@ +I. Included Third-Party Software + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2015). +Licensed under the Apache License 2.0. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org). +Copyright (c) OSGi Alliance (2000, 2015). +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 diff --git a/http/bridge/src/main/appended-resources/META-INF/NOTICE b/http/bridge/src/main/appended-resources/META-INF/NOTICE new file mode 100644 index 00000000000..975754500f0 --- /dev/null +++ b/http/bridge/src/main/appended-resources/META-INF/NOTICE @@ -0,0 +1,4 @@ +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2015). +Licensed under the Apache License 2.0. \ No newline at end of file diff --git a/http/bridge/src/main/java/org/apache/felix/http/bridge/internal/BridgeActivator.java b/http/bridge/src/main/java/org/apache/felix/http/bridge/internal/BridgeActivator.java new file mode 100644 index 00000000000..a4ed62c491d --- /dev/null +++ b/http/bridge/src/main/java/org/apache/felix/http/bridge/internal/BridgeActivator.java @@ -0,0 +1,108 @@ +/* + * 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.http.bridge.internal; + +import java.io.IOException; +import java.util.EventListener; +import java.util.Hashtable; + +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServlet; + +import org.apache.felix.http.base.internal.AbstractHttpActivator; +import org.apache.felix.http.base.internal.EventDispatcher; +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.osgi.framework.Constants; +import org.osgi.service.http.runtime.HttpServiceRuntimeConstants; + +public final class BridgeActivator extends AbstractHttpActivator +{ + /** Framework property containing the endpoint registration information (optional). */ + private static final String FELIX_HTTP_SERVICE_ENDPOINTS = "org.apache.felix.http.service.endpoints"; + + private static final String VENDOR = "The Apache Software Foundation"; + + private static final String MARKER_PROP = "http.felix.dispatcher"; + + @Override + protected void doStart() throws Exception + { + super.doStart(); + + // check for endpoint registration property + final Hashtable serviceRegProps = new Hashtable(); + if ( getBundleContext().getProperty(FELIX_HTTP_SERVICE_ENDPOINTS) != null ) + { + serviceRegProps.put(HttpServiceRuntimeConstants.HTTP_SERVICE_ENDPOINT, + getBundleContext().getProperty(FELIX_HTTP_SERVICE_ENDPOINTS)); + } + else + { + serviceRegProps.put(HttpServiceRuntimeConstants.HTTP_SERVICE_ENDPOINT, "/"); + } + final Servlet dispatcherServlet = this.getHttpServiceController().createDispatcherServlet(); + final Object servlet = new HttpServlet() + { + private static final long serialVersionUID = -5229577898597483605L; + + @Override + public void destroy() + { + getHttpServiceController().unregister(); + dispatcherServlet.destroy(); + super.destroy(); + } + + @Override + public void init(final ServletConfig config) throws ServletException + { + super.init(config); + dispatcherServlet.init(config); + getHttpServiceController().register(config.getServletContext(), serviceRegProps); + } + + @Override + public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { + dispatcherServlet.service(req, res); + } + }; + + // register dispatcher servlet + Hashtable props = new Hashtable(); + props.put(MARKER_PROP, servlet.getClass().getName()); + props.put(Constants.SERVICE_DESCRIPTION, "Apache Felix Http Dispatcher for bridged request handling"); + props.put(Constants.SERVICE_VENDOR, VENDOR); + getBundleContext().registerService(HttpServlet.class.getName(), servlet, props); + + // Http Session event dispatcher + final EventDispatcher dispatcher = getHttpServiceController(). getEventDispatcher(); + dispatcher.setActive(true); + props = new Hashtable(); + props.put(MARKER_PROP, dispatcher.getClass().getName()); + props.put(Constants.SERVICE_DESCRIPTION, "Apache Felix Http Dispatcher for bridged event handling"); + props.put(Constants.SERVICE_VENDOR, VENDOR); + getBundleContext().registerService(EventListener.class.getName(), dispatcher, props); + + SystemLogger.info("Started bridged http services"); + } +} diff --git a/http/bundle/DEPENDENCIES b/http/bundle/DEPENDENCIES new file mode 100644 index 00000000000..8efdfd4b55f --- /dev/null +++ b/http/bundle/DEPENDENCIES @@ -0,0 +1,31 @@ +Apache Felix HTTP Service Bundle +Copyright 2011 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +This product includes software developed at +Eclipse (http://eclipse.org) +Licensed under the Apache License 2.0. + +This product includes software developed by +the cometd project (http://cometd.org) +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 diff --git a/http/bundle/LICENSE b/http/bundle/LICENSE new file mode 100644 index 00000000000..6b0b1270ff0 --- /dev/null +++ b/http/bundle/LICENSE @@ -0,0 +1,203 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/http/bundle/NOTICE b/http/bundle/NOTICE new file mode 100644 index 00000000000..b94c364403e --- /dev/null +++ b/http/bundle/NOTICE @@ -0,0 +1,19 @@ +Apache Felix Http Service Bundle +Copyright 2011 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developed at +Eclipse (http://eclipse.org) +Licensed under the Apache License 2.0. + +This product includes software developed by +the cometd project (http://cometd.org) +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. diff --git a/http/bundle/pom.xml b/http/bundle/pom.xml new file mode 100644 index 00000000000..ffe1359db85 --- /dev/null +++ b/http/bundle/pom.xml @@ -0,0 +1,137 @@ + + + + 4.0.0 + + org.apache.felix + org.apache.felix.http.parent + 9 + ../parent/pom.xml + + + Apache Felix Http Bundle + org.apache.felix.http.bundle + 3.0.1-SNAPSHOT + jar + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/http/bundle + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/http/bundle + http://svn.apache.org/viewvc/felix/trunk/http/bundle/ + + + + 7 + + + + + + org.apache.felix + maven-bundle-plugin + + + + ${jetty.version} + + + org.apache.felix.http.bundle.internal.CombinedActivator + + + javax.servlet.*;version=${servlet.version};-split-package:=merge-first, + org.osgi.service.http.*;-split-package:=merge-first, + org.cometd.bayeux;version=${cometd.version};-split-package:=merge-first, + org.cometd.bayeux.client;version=${cometd.version};-split-package:=merge-first, + org.cometd.bayeux.server;version=${cometd.version};-split-package:=merge-first, + org.apache.felix.http.api;-split-package:=merge-first, + org.apache.felix.http.cometd;-split-package:=merge-first, + org.eclipse.jetty.*;version=${version;===;${jetty.version}};-split-package:=merge-first + + + org.apache.felix.http.base.*;-split-package:=merge-first, + org.apache.felix.http.bridge.*, + org.apache.felix.http.bundle.*, + org.apache.felix.http.cometd.*, + org.apache.felix.http.jetty.*, + org.apache.felix.http.whiteboard.*, + org.cometd.server.*;-split-package:=merge-first, + org.cometd.common.*;-split-package:=merge-first + + + javax.imageio;resolution:=optional, + javax.sql;resolution:=optional, + org.slf4j.*;resolution:=optional, + org.ietf.jgss;resolution:=optional, + org.mortbay.log;resolution:=optional;version="[6.1,7)", + org.mortbay.util.ajax;resolution:=optional;version="[6.1,7)", + org.osgi.service.useradmin;resolution:=optional, + org.codehaus.jackson.map;resolution:=optional, + org.codehaus.jackson.type;resolution:=optional, + * + + + osgi.implementation;osgi.implementation="osgi.http";version:Version="1.0"; + uses:="javax.servlet,javax.servlet.http,org.osgi.service.http.context,org.osgi.service.http.whiteboard", + osgi.service;objectClass:List<String>="org.osgi.service.http.runtime.HttpServiceRuntime"; + uses:="org.osgi.service.http.runtime,org.osgi.service.http.runtime.dto" + + + + true + + + + + + + + javax.servlet + javax.servlet-api + + + org.osgi + osgi.core + + + org.osgi + osgi.cmpn + + + org.apache.felix + org.apache.felix.http.bridge + 3.0.0 + + + org.apache.felix + org.apache.felix.http.cometd + 3.0.0-SNAPSHOT + + + org.apache.felix + org.apache.felix.http.jetty + 3.1.0 + + + org.apache.felix + org.apache.felix.http.whiteboard + 3.0.0 + + + diff --git a/http/bundle/src/main/java/org/apache/felix/http/bundle/internal/CombinedActivator.java b/http/bundle/src/main/java/org/apache/felix/http/bundle/internal/CombinedActivator.java new file mode 100644 index 00000000000..cdd4e2dbec9 --- /dev/null +++ b/http/bundle/src/main/java/org/apache/felix/http/bundle/internal/CombinedActivator.java @@ -0,0 +1,91 @@ +/* + * 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.http.bundle.internal; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.apache.felix.http.bridge.internal.BridgeActivator; +import org.apache.felix.http.whiteboard.internal.WhiteboardActivator; +import org.apache.felix.http.cometd.internal.CometdActivator; +import org.apache.felix.http.jetty.internal.JettyActivator; + +public final class CombinedActivator + implements BundleActivator +{ + private final static String JETTY_ENABLED_PROP = "org.apache.felix.http.jettyEnabled"; + private final static String WHITEBOARD_ENABLED_PROP = "org.apache.felix.http.whiteboardEnabled"; + private final static String COMETD_ENABLED_PROP = "org.apache.felix.http.cometdEnabled"; + + private BundleActivator jettyActivator; + private BundleActivator bridgeActivator; + private BundleActivator whiteboardActivator; + private BundleActivator cometdActivator; + + public void start(BundleContext context) + throws Exception + { + if ("true".equals(context.getProperty(JETTY_ENABLED_PROP))) { + this.jettyActivator = new JettyActivator(); + } else { + this.bridgeActivator = new BridgeActivator(); + } + + if ("true".equals(context.getProperty(WHITEBOARD_ENABLED_PROP))) { + this.whiteboardActivator = new WhiteboardActivator(); + } + + if ("true".equals(context.getProperty(COMETD_ENABLED_PROP))) { + this.cometdActivator = new CometdActivator(); + } + + if (this.jettyActivator != null) { + this.jettyActivator.start(context); + } + + if (this.bridgeActivator != null) { + this.bridgeActivator.start(context); + } + + if (this.whiteboardActivator != null) { + this.whiteboardActivator.start(context); + } + + if (this.cometdActivator != null) { + this.cometdActivator.start(context); + } + } + + public void stop(BundleContext context) + throws Exception + { + if (this.cometdActivator != null) { + this.cometdActivator.stop(context); + } + + if (this.whiteboardActivator != null) { + this.whiteboardActivator.stop(context); + } + + if (this.jettyActivator != null) { + this.jettyActivator.stop(context); + } + + if (this.bridgeActivator != null) { + this.bridgeActivator.stop(context); + } + } +} diff --git a/http/cometd/DEPENDENCIES b/http/cometd/DEPENDENCIES new file mode 100644 index 00000000000..b5d4162e7f3 --- /dev/null +++ b/http/cometd/DEPENDENCIES @@ -0,0 +1,31 @@ +Apache Felix HTTP Service Cometd +Copyright 2011 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +This product includes software developed at +Eclipse (http://eclipse.org) +Licensed under the Apache License 2.0. + +This product includes software developed by +the cometd project (http://cometd.org) +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 diff --git a/http/cometd/LICENSE b/http/cometd/LICENSE new file mode 100644 index 00000000000..6b0b1270ff0 --- /dev/null +++ b/http/cometd/LICENSE @@ -0,0 +1,203 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/http/cometd/NOTICE b/http/cometd/NOTICE new file mode 100644 index 00000000000..239ff76f0b2 --- /dev/null +++ b/http/cometd/NOTICE @@ -0,0 +1,19 @@ +Apache Felix Http Service Cometd +Copyright 2011 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developed at +Eclipse (http://eclipse.org) +Licensed under the Apache License 2.0. + +This product includes software developed by +the cometd project (http://cometd.org) +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. diff --git a/http/cometd/pom.xml b/http/cometd/pom.xml new file mode 100644 index 00000000000..8b038b317af --- /dev/null +++ b/http/cometd/pom.xml @@ -0,0 +1,192 @@ + + + + 4.0.0 + + org.apache.felix + org.apache.felix.http.parent + 9 + ../parent/pom.xml + + + Apache Felix Http Cometd + org.apache.felix.http.cometd + 3.0.0-SNAPSHOT + jar + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/http/cometd + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/http/cometd + http://svn.apache.org/viewvc/felix/trunk/http/cometd/ + + + + 8 + + + + + + org.apache.felix + maven-bundle-plugin + + + + org.apache.felix.http.cometd.internal.CometdActivator + + + org.apache.felix.http.cometd;version=${project.version}, + org.cometd.bayeux;version=${cometd.version};-split-package:=merge-first, + org.cometd.bayeux.client;version=${cometd.version};-split-package:=merge-first, + org.cometd.bayeux.server;version=${cometd.version};-split-package:=merge-first, + + + org.apache.felix.http.base.*, + org.apache.felix.http.cometd.internal, + org.cometd.server.*;-split-package:=merge-first, + org.cometd.common.*;-split-package:=merge-first + + + cometd-java-server;inline=true, + cometd-java-common;inline=true, + jetty-util;inline=true, + jetty-util-ajax;inline=true, + + + javax.imageio;resolution:=optional, + org.slf4j.*;resolution:=optional, + org.osgi.service.useradmin;resolution:=optional, + org.codehaus.jackson.map;resolution:=optional, + org.codehaus.jackson.type;resolution:=optional, + org.eclipse.jetty.continuation;version="[7.6,9)", + org.eclipse.jetty.jmx;version="[7.6,9)", + * + + + + + + + + + + org.osgi + osgi.core + + + org.osgi + osgi.cmpn + + + javax.servlet + javax.servlet-api + + + org.eclipse.jetty + jetty-util + ${jetty.version} + + + org.eclipse.jetty + jetty-util-ajax + ${jetty.version} + + + org.cometd.java + cometd-java-server + ${cometd.version} + + + org.eclipse.jetty + jetty-continuation + + + org.eclipse.jetty + jetty-jmx + + + + + org.cometd.java + cometd-java-client + ${cometd.version} + + + org.eclipse.jetty + jetty-client + + + org.eclipse.jetty + jetty-io + + + org.eclipse.jetty + jetty-http + + + + + org.cometd.java + cometd-java-common + ${cometd.version} + + + org.cometd.java + bayeux-api + ${cometd.version} + + + ${project.groupId} + org.apache.felix.http.base + 3.0.0 + + + + + org.eclipse.jetty + jetty-client + ${jetty.version} + + + org.eclipse.jetty + jetty-continuation + ${jetty.version} + + + org.eclipse.jetty + jetty-http + ${jetty.version} + + + org.eclipse.jetty + jetty-io + ${jetty.version} + + + org.eclipse.jetty + jetty-jmx + ${jetty.version} + + + + diff --git a/http/cometd/src/main/java/org/apache/felix/http/cometd/CometdService.java b/http/cometd/src/main/java/org/apache/felix/http/cometd/CometdService.java new file mode 100644 index 00000000000..fc5c90b77c6 --- /dev/null +++ b/http/cometd/src/main/java/org/apache/felix/http/cometd/CometdService.java @@ -0,0 +1,27 @@ +/* + * 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.http.cometd; + +import javax.servlet.ServletException; + +import org.cometd.bayeux.server.BayeuxServer; + +public interface CometdService +{ + public BayeuxServer getBayeuxServer() + throws ServletException; +} diff --git a/http/cometd/src/main/java/org/apache/felix/http/cometd/internal/CometdActivator.java b/http/cometd/src/main/java/org/apache/felix/http/cometd/internal/CometdActivator.java new file mode 100644 index 00000000000..a2b9fa8238d --- /dev/null +++ b/http/cometd/src/main/java/org/apache/felix/http/cometd/internal/CometdActivator.java @@ -0,0 +1,36 @@ +/* + * 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.http.cometd.internal; + +import org.apache.felix.http.base.internal.AbstractActivator; + +public final class CometdActivator extends AbstractActivator +{ + private volatile CometdServiceImpl cometd; + + protected void doStart() throws Exception + { + this.cometd = new CometdServiceImpl(getBundleContext()); + this.cometd.start(); + } + + protected void doStop() throws Exception + { + this.cometd.stop(); + this.cometd = null; + } +} diff --git a/http/cometd/src/main/java/org/apache/felix/http/cometd/internal/CometdConfig.java b/http/cometd/src/main/java/org/apache/felix/http/cometd/internal/CometdConfig.java new file mode 100644 index 00000000000..69ebef9d5cd --- /dev/null +++ b/http/cometd/src/main/java/org/apache/felix/http/cometd/internal/CometdConfig.java @@ -0,0 +1,69 @@ +/* + * 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.http.cometd.internal; + +import org.osgi.framework.BundleContext; +import java.util.Dictionary; +import java.util.Properties; + +public final class CometdConfig +{ + /** cometd servlet path */ + private static final String COMETD_PATH = "org.apache.felix.http.cometd.path"; + + /** default cometd servlet path */ + private static final String DEFAULT_COMETD_PATH = "/system/cometd"; + + private final BundleContext context; + private String path; + + public CometdConfig(BundleContext context) + { + this.context = context; + reset(); + } + + public String getPath() + { + return this.path; + } + + public void reset() + { + update(null); + } + + public void update(Dictionary props) + { + if (props == null) { + props = new Properties(); + } + + this.path = getProperty(props, COMETD_PATH, DEFAULT_COMETD_PATH); + } + + private String getProperty(Dictionary props, String name, String defValue) + { + Object value = props.get(name); + if (value == null) + { + value = this.context.getProperty(name); + } + + return value != null ? String.valueOf(value) : defValue; + } +} diff --git a/http/cometd/src/main/java/org/apache/felix/http/cometd/internal/CometdServiceImpl.java b/http/cometd/src/main/java/org/apache/felix/http/cometd/internal/CometdServiceImpl.java new file mode 100644 index 00000000000..7462f41fd78 --- /dev/null +++ b/http/cometd/src/main/java/org/apache/felix/http/cometd/internal/CometdServiceImpl.java @@ -0,0 +1,142 @@ +/* + * 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.http.cometd.internal; + +import java.util.Dictionary; +import java.util.Hashtable; + +import javax.servlet.http.HttpServlet; + +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.apache.felix.http.cometd.CometdService; +import org.cometd.bayeux.server.BayeuxServer; +import org.cometd.server.CometDServlet; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.ManagedService; +import org.osgi.service.http.HttpService; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +public final class CometdServiceImpl + extends HttpServlet + implements ManagedService, ServiceTrackerCustomizer, CometdService +{ + private static final long serialVersionUID = 1L; + private static final String PID = "org.apache.felix.http.cometd"; + + private final CometdConfig config; + private final BundleContext context; + private ServiceRegistration configServiceReg; + private ServiceTracker httpServiceTracker; + private ServiceRegistration cometdServiceReg; + private CometDServlet continuationCometdServlet; + + public CometdServiceImpl(BundleContext context) + { + this.context = context; + this.config = new CometdConfig(this.context); + } + + public void start() + throws Exception + { + Dictionary props = new Hashtable(); + props.put(Constants.SERVICE_PID, PID); + this.configServiceReg = this.context.registerService(ManagedService.class.getName(), this, props); + + this.httpServiceTracker = new ServiceTracker(this.context, HttpService.class.getName(), this); + this.httpServiceTracker.open(); + } + + public void stop() + throws Exception + { + if (this.configServiceReg != null) { + this.configServiceReg.unregister(); + } + + if (this.httpServiceTracker != null) { + this.httpServiceTracker.close(); + } + } + + @Override + public void updated(Dictionary props) + { + this.config.update(props); + if (this.httpServiceTracker != null) { + Object service = this.httpServiceTracker.getService(); + if (service != null) { + this.unregister((HttpService)service); + this.register((HttpService)service); + } + } + } + + @Override + public Object addingService(ServiceReference reference) + { + Object service = this.context.getService(reference); + this.register((HttpService)service); + return service; + } + + @Override + public void modifiedService(ServiceReference reference, Object service) + { + this.unregister((HttpService)service); + this.register((HttpService)service); + } + + @Override + public void removedService(ServiceReference reference, Object service) + { + this.unregister((HttpService)service); + } + + private void register(HttpService httpService) { + if (this.continuationCometdServlet == null) { + this.continuationCometdServlet = new CometDServlet(); + } + try { + Dictionary dictionary = new Hashtable(); + dictionary.put("requestAvailable","true"); + httpService.registerServlet(this.config.getPath(), this.continuationCometdServlet, dictionary, null); + } + catch (Exception e) { + SystemLogger.error("Failed to register ContinuationCometdServlet to " + this.config.getPath(), e); + } + this.cometdServiceReg = + this.context.registerService(CometdService.class.getName(), this, null); + } + + private void unregister(HttpService httpService) { + httpService.unregister(this.config.getPath()); + if (this.cometdServiceReg != null) { + this.cometdServiceReg.unregister(); + this.cometdServiceReg = null; + } + } + + @Override + public BayeuxServer getBayeuxServer() { + return this.continuationCometdServlet.getBayeux(); + } +} diff --git a/http/itest/pom.xml b/http/itest/pom.xml new file mode 100644 index 00000000000..d44842560ff --- /dev/null +++ b/http/itest/pom.xml @@ -0,0 +1,156 @@ + + + + 4.0.0 + + org.apache.felix + org.apache.felix.http.parent + 12 + ../parent/pom.xml + + + Apache Felix Http Integration Tests + org.apache.felix.http.itest + 0.0.3-SNAPSHOT + jar + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/http/itest + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/http/itest + http://svn.apache.org/viewvc/felix/trunk/http/itest/ + + + + 8 + 4.11.0 + 2.5.2 + 1.1.2 + 4.0.9-SNAPSHOT + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${http.servlet.api.version} + ${http.jetty.version} + + + + + + + + org.osgi + osgi.core + + + org.osgi + org.osgi.service.cm + 1.5.0 + provided + + + org.apache.httpcomponents + httpcore-osgi + 4.4.6 + provided + + + org.apache.httpcomponents + httpclient-osgi + 4.5.3 + provided + + + org.apache.geronimo.specs + geronimo-json_1.0_spec + 1.0-alpha-1 + provided + + + org.apache.johnzon + johnzon-core + 1.0.0 + provided + + + org.apache.felix + org.apache.felix.http.servlet-api + ${http.servlet.api.version} + + + org.osgi + org.osgi.service.http + 1.2.1 + provided + + + org.osgi + org.osgi.service.http.whiteboard + 1.1.0 + provided + + + org.apache.felix + org.apache.felix.http.jetty + ${http.jetty.version} + + + org.ops4j.pax.exam + pax-exam-container-forked + ${pax.exam.version} + test + + + org.ops4j.pax.exam + pax-exam-junit4 + ${pax.exam.version} + test + + + org.ops4j.pax.exam + pax-exam-link-mvn + ${pax.exam.version} + test + + + org.objenesis + objenesis + 2.6 + test + + + + org.ops4j.pax.url + pax-url-aether + ${pax.url.aether.version} + test + + + javax.inject + javax.inject + 1 + test + + + org.apache.felix + org.apache.felix.framework + 5.6.4 + test + + + diff --git a/http/itest/src/test/java/org/apache/felix/http/itest/AsyncTest.java b/http/itest/src/test/java/org/apache/felix/http/itest/AsyncTest.java new file mode 100644 index 00000000000..78c874cc8e4 --- /dev/null +++ b/http/itest/src/test/java/org/apache/felix/http/itest/AsyncTest.java @@ -0,0 +1,172 @@ +/* + * 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.http.itest; + +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.servlet.AsyncContext; +import javax.servlet.DispatcherType; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerMethod; + +/** + * @author Felix Project Team + */ +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerMethod.class) +public class AsyncTest extends BaseIntegrationTest +{ + + /** + * Tests that we can use an asynchronous servlet (introduced in Servlet 3.0 spec). + */ + @Test + public void testAsyncServletOk() throws Exception + { + CountDownLatch initLatch = new CountDownLatch(1); + CountDownLatch destroyLatch = new CountDownLatch(1); + + TestServlet servlet = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + final AsyncContext asyncContext = req.startAsync(req, resp); + asyncContext.setTimeout(2000); + asyncContext.start(new Runnable() + { + @Override + public void run() + { + try + { + // Simulate a long running process... + Thread.sleep(1000); + + HttpServletResponse response = (HttpServletResponse) asyncContext.getResponse(); + response.setStatus(SC_OK); + response.getWriter().printf("Hello Async world!"); + + asyncContext.complete(); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + }); + } + }; + + register("/test", servlet); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertContent("Hello Async world!", createURL("/test")); + + unregister("/test"); + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + + assertResponseCode(SC_NOT_FOUND, createURL("/test")); + } + + /** + * Tests that we can use an asynchronous servlet (introduced in Servlet 3.0 spec) using the dispatching functionality. + */ + @Test + public void testAsyncServletWithDispatchOk() throws Exception + { + CountDownLatch initLatch = new CountDownLatch(1); + CountDownLatch destroyLatch = new CountDownLatch(1); + + TestServlet servlet = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(final HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + DispatcherType dispatcherType = req.getDispatcherType(); + if (DispatcherType.REQUEST == dispatcherType) + { + final AsyncContext asyncContext = req.startAsync(req, resp); + asyncContext.start(new Runnable() + { + @Override + public void run() + { + try + { + // Simulate a long running process... + Thread.sleep(1000); + + asyncContext.getRequest().setAttribute("msg", "Hello Async world!"); + asyncContext.dispatch(); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + } + } + }); + } + else if (DispatcherType.ASYNC == dispatcherType) + { + String response = (String) req.getAttribute("msg"); + resp.setStatus(SC_OK); + resp.getWriter().printf(response); + resp.flushBuffer(); + } + } + }; + + register("/test", servlet); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertContent("Hello Async world!", createURL("/test")); + + unregister("/test"); + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + + assertResponseCode(SC_NOT_FOUND, createURL("/test")); + } +} diff --git a/http/itest/src/test/java/org/apache/felix/http/itest/BaseIntegrationTest.java b/http/itest/src/test/java/org/apache/felix/http/itest/BaseIntegrationTest.java new file mode 100644 index 00000000000..49ac38fd089 --- /dev/null +++ b/http/itest/src/test/java/org/apache/felix/http/itest/BaseIntegrationTest.java @@ -0,0 +1,512 @@ +/* + * 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.http.itest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.ops4j.pax.exam.Constants.START_LEVEL_SYSTEM_BUNDLES; +import static org.ops4j.pax.exam.Constants.START_LEVEL_TEST_BUNDLE; +import static org.ops4j.pax.exam.CoreOptions.frameworkStartLevel; +import static org.ops4j.pax.exam.CoreOptions.junitBundles; +import static org.ops4j.pax.exam.CoreOptions.mavenBundle; +import static org.ops4j.pax.exam.CoreOptions.options; +import static org.ops4j.pax.exam.CoreOptions.systemProperty; +import static org.ops4j.pax.exam.CoreOptions.when; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collection; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; +import java.util.Scanner; +import java.util.concurrent.CountDownLatch; + +import javax.inject.Inject; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.After; +import org.junit.Before; +import org.ops4j.pax.exam.Configuration; +import org.ops4j.pax.exam.Option; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedService; +import org.osgi.service.http.HttpContext; +import org.osgi.service.http.HttpService; +import org.osgi.service.http.NamespaceException; +import org.osgi.util.tracker.ServiceTracker; + +/** + * Base class for integration tests. + * + * @author Felix Project Team + */ +public abstract class BaseIntegrationTest +{ + protected static class TestFilter implements Filter + { + private final CountDownLatch m_initLatch; + private final CountDownLatch m_destroyLatch; + + public TestFilter() + { + this(null, null); + } + + public TestFilter(CountDownLatch initLatch, CountDownLatch destroyLatch) + { + m_initLatch = initLatch; + m_destroyLatch = destroyLatch; + } + + @Override + public void destroy() + { + if (m_destroyLatch != null) + { + m_destroyLatch.countDown(); + } + } + + @Override + public final void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException + { + filter((HttpServletRequest) req, (HttpServletResponse) resp, chain); + } + + @Override + public void init(FilterConfig config) throws ServletException + { + if (m_initLatch != null) + { + m_initLatch.countDown(); + } + } + + protected void filter(HttpServletRequest req, HttpServletResponse resp, FilterChain chain) throws IOException, ServletException + { + resp.setStatus(HttpServletResponse.SC_OK); + } + } + + protected static class TestServlet extends HttpServlet + { + private static final long serialVersionUID = 1L; + + private final CountDownLatch m_initLatch; + private final CountDownLatch m_destroyLatch; + + public TestServlet() + { + this(null, null); + } + + public TestServlet(CountDownLatch initLatch, CountDownLatch destroyLatch) + { + m_initLatch = initLatch; + m_destroyLatch = destroyLatch; + } + + @Override + public void destroy() + { + super.destroy(); + if (m_destroyLatch != null) + { + m_destroyLatch.countDown(); + } + } + + @Override + public void init() throws ServletException + { + super.init(); + if (m_initLatch != null) + { + m_initLatch.countDown(); + } + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + resp.setStatus(HttpServletResponse.SC_OK); + } + } + + protected static final int DEFAULT_TIMEOUT = 10000; + + private static final String ORG_APACHE_FELIX_HTTP_JETTY = "org.apache.felix.http.jetty"; + + protected static void assertContent(int expectedRC, String expected, URL url) throws IOException + { + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + + int rc = conn.getResponseCode(); + assertEquals("Unexpected response code,", expectedRC, rc); + + if (rc >= 200 && rc < 500) + { + InputStream is = null; + try + { + is = conn.getInputStream(); + assertEquals(expected, slurpAsString(is)); + } + finally + { + close(is); + conn.disconnect(); + } + } + else + { + InputStream is = null; + try + { + is = conn.getErrorStream(); + assertEquals(expected, slurpAsString(is)); + } + finally + { + close(is); + conn.disconnect(); + } + } + } + + protected static void assertContent(String expected, URL url) throws IOException + { + assertContent(200, expected, url); + } + + protected static void assertResponseCode(int expected, URL url) throws IOException + { + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + try + { + assertEquals(expected, conn.getResponseCode()); + } + finally + { + conn.disconnect(); + } + } + + protected static void close(Closeable resource) + { + if (resource != null) + { + try + { + resource.close(); + } + catch (IOException e) + { + // Ignore... + } + } + } + + protected static Dictionary createDictionary(Object... entries) + { + Dictionary props = new Hashtable<>(); + for (int i = 0; i < entries.length; i += 2) + { + String key = (String) entries[i]; + Object value = entries[i + 1]; + props.put(key, value); + } + return props; + } + + protected static URL createURL(String path) + { + if (path == null) + { + path = ""; + } + while (path.startsWith("/")) + { + path = path.substring(1); + } + int port = Integer.getInteger("org.osgi.service.http.port", 8080); + try + { + return new URL(String.format("http://localhost:%d/%s", port, path)); + } + catch (MalformedURLException e) + { + throw new RuntimeException(e); + } + } + + protected static String slurpAsString(InputStream is) throws IOException + { + // See + Scanner scanner = new Scanner(is, "UTF-8"); + try + { + scanner.useDelimiter("\\A"); + + return scanner.hasNext() ? scanner.next() : null; + } + finally + { + try + { + scanner.close(); + } + catch (Exception e) + { + // Ignore... + } + } + } + + @Inject + protected volatile BundleContext m_context; + + @Configuration + public Option[] config() + { + final String localRepo = System.getProperty("maven.repo.local", ""); + + return options( + when( localRepo.length() > 0 ).useOptions( + systemProperty("org.ops4j.pax.url.mvn.localRepository").value(localRepo) + ), + // CoreOptions.vmOption("-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8787"), + + // scavenge sessions every 10 seconds (10 minutes is default in 9.4.x) + systemProperty("org.eclipse.jetty.servlet.SessionScavengingInterval").value("10"), + mavenBundle("org.slf4j", "slf4j-api", "1.7.5"), + mavenBundle("org.slf4j", "jcl-over-slf4j", "1.7.5"), + mavenBundle("org.slf4j", "log4j-over-slf4j", "1.7.5"), + + mavenBundle("org.apache.sling", "org.apache.sling.commons.log", "4.0.0"), + mavenBundle("org.apache.sling", "org.apache.sling.commons.logservice", "1.0.2"), + + mavenBundle("org.apache.geronimo.specs", "geronimo-json_1.0_spec", "1.0-alpha-1").startLevel(START_LEVEL_SYSTEM_BUNDLES), + mavenBundle("org.apache.johnzon", "johnzon-core", "1.0.0").startLevel(START_LEVEL_SYSTEM_BUNDLES), + + mavenBundle("org.apache.felix", "org.apache.felix.configadmin").version("1.8.14").startLevel(START_LEVEL_SYSTEM_BUNDLES), + mavenBundle("org.apache.felix", "org.apache.felix.http.servlet-api", System.getProperty("http.servlet.api.version")).startLevel(START_LEVEL_SYSTEM_BUNDLES), + mavenBundle("org.apache.felix", ORG_APACHE_FELIX_HTTP_JETTY, System.getProperty("http.jetty.version")).startLevel(START_LEVEL_SYSTEM_BUNDLES), + mavenBundle("org.apache.felix", "org.apache.felix.http.whiteboard", "4.0.0").startLevel(START_LEVEL_SYSTEM_BUNDLES), + + mavenBundle("org.apache.httpcomponents", "httpcore-osgi", "4.4.6").startLevel(START_LEVEL_SYSTEM_BUNDLES), + mavenBundle("org.apache.httpcomponents", "httpclient-osgi", "4.5.3").startLevel(START_LEVEL_SYSTEM_BUNDLES), + mavenBundle("org.mockito", "mockito-all", "1.10.19").startLevel(START_LEVEL_SYSTEM_BUNDLES), + mavenBundle("org.objenesis", "objenesis", "2.6").startLevel(START_LEVEL_SYSTEM_BUNDLES), + + junitBundles(), + frameworkStartLevel(START_LEVEL_TEST_BUNDLE)); + } + + private final Map> trackers = new HashMap<>(); + + @Before + public void setUp() throws Exception + { + assertNotNull("No bundle context?!", m_context); + } + + @After + public void tearDown() throws Exception + { + synchronized ( trackers ) + { + for(final Map.Entry> entry : trackers.entrySet()) + { + entry.getValue().close(); + } + trackers.clear(); + } + Bundle bundle = getHttpJettyBundle(); + // Restart the HTTP-service to clean all registrations... + if (bundle.getState() == Bundle.ACTIVE) + { + bundle.stop(); + bundle.start(); + } + } + + /** + * Waits for a service to become available in certain time interval. + * @param serviceName + * @return + * @throws Exception + */ + protected T awaitService(String serviceName) throws Exception + { + ServiceTracker tracker = null; + tracker = getTracker(serviceName); + return tracker.waitForService(DEFAULT_TIMEOUT); + } + + /** + * Return an array of {@code ServiceReference}s for all services for the + * given serviceName + * @param serviceName + * @return Array of {@code ServiceReference}s or {@code null} if no services + * are being tracked. + */ + protected ServiceReference[] getServiceReferences(String serviceName) + { + ServiceTracker tracker = getTracker(serviceName); + return tracker.getServiceReferences(); + } + + private ServiceTracker getTracker(String serviceName) + { + synchronized ( this.trackers ) + { + ServiceTracker tracker = trackers.get(serviceName); + if ( tracker == null ) + { + tracker = new ServiceTracker(m_context, serviceName, null); + trackers.put(serviceName, tracker); + tracker.open(); + } + return (ServiceTracker) tracker; + } + } + + protected void configureHttpService(Dictionary props) throws Exception + { + final String pid = "org.apache.felix.http"; + + final Collection> serviceRefs = m_context.getServiceReferences(ManagedService.class, String.format("(%s=%s)", Constants.SERVICE_PID, pid)); + assertNotNull("Unable to obtain managed configuration for " + pid, serviceRefs); + assertFalse("Unable to obtain managed configuration for " + pid, serviceRefs.isEmpty()); + + for (final ServiceReference serviceRef : serviceRefs) + { + ManagedService service = m_context.getService(serviceRef); + try + { + service.updated(props); + } + catch (ConfigurationException ex) + { + fail("Invalid configuration provisioned: " + ex.getMessage()); + } + finally + { + m_context.ungetService(serviceRef); + } + } + } + + /** + * @param bsn + * @return + */ + protected Bundle findBundle(String bsn) + { + for (Bundle bundle : m_context.getBundles()) + { + if (bsn.equals(bundle.getSymbolicName())) + { + return bundle; + } + } + return null; + } + + protected Bundle getHttpJettyBundle() + { + Bundle b = findBundle(ORG_APACHE_FELIX_HTTP_JETTY); + assertNotNull("Filestore bundle not found?!", b); + return b; + } + + protected HttpService getHttpService() + { + return getService(HttpService.class.getName()); + } + + /** + * Obtains a service without waiting for it to become available. + * @param serviceName + * @return + */ + protected T getService(final String serviceName) + { + ServiceTracker tracker = null; + synchronized ( this.trackers ) + { + tracker = trackers.get(serviceName); + if ( tracker == null ) + { + tracker = new ServiceTracker(m_context, serviceName, null); + trackers.put(serviceName, tracker); + tracker.open(); + } + } + return (T) tracker.getService(); + } + + protected void register(String alias, Servlet servlet) throws ServletException, NamespaceException + { + register(alias, servlet, null); + } + + protected void register(String alias, Servlet servlet, HttpContext context) throws ServletException, NamespaceException + { + getHttpService().registerServlet(alias, servlet, null, context); + } + + protected void register(String alias, String name) throws ServletException, NamespaceException + { + register(alias, name, null); + } + + protected void register(String alias, String name, HttpContext context) throws ServletException, NamespaceException + { + getHttpService().registerResources(alias, name, context); + } + + + protected void unregister(String alias) throws ServletException, NamespaceException + { + getHttpService().unregister(alias); + } +} diff --git a/http/itest/src/test/java/org/apache/felix/http/itest/ErrorPageTest.java b/http/itest/src/test/java/org/apache/felix/http/itest/ErrorPageTest.java new file mode 100644 index 00000000000..cbdf088549b --- /dev/null +++ b/http/itest/src/test/java/org/apache/felix/http/itest/ErrorPageTest.java @@ -0,0 +1,252 @@ +/* + * 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.http.itest; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertTrue; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_PATH; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_ERROR_PAGE; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_NAME; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.servlet.Servlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerMethod; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.http.context.ServletContextHelper; + +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerMethod.class) +public class ErrorPageTest extends BaseIntegrationTest +{ + private List> registrations = new ArrayList>(); + + private CountDownLatch initLatch; + private CountDownLatch destroyLatch; + + public void setupLatches(int count) + { + initLatch = new CountDownLatch(count); + destroyLatch = new CountDownLatch(count); + } + + public void setupErrorServlet(final Integer errorCode, + final Class exceptionType, + String context) throws Exception + { + Dictionary servletProps = new Hashtable(); + servletProps.put(HTTP_WHITEBOARD_SERVLET_NAME, "servlet"); + servletProps.put(HTTP_WHITEBOARD_SERVLET_PATTERN, asList("/test")); + if (context != null) + { + servletProps.put(HTTP_WHITEBOARD_CONTEXT_SELECT, "(" + HTTP_WHITEBOARD_CONTEXT_NAME + "=" + context + ")"); + } + + TestServlet servletWithErrorCode = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException + { + if (errorCode != null) + { + resp.sendError(errorCode); + } + + if (exceptionType != null) + { + RuntimeException exception; + try + { + exception = exceptionType.newInstance(); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + throw exception; + } + } + }; + + registrations.add(m_context.registerService(Servlet.class.getName(), servletWithErrorCode, servletProps)); + } + + public void setupErrorPage(final Integer errorCode, + final Class exceptionType, + final String name, + String context) throws Exception + { + TestServlet errorPage = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException + { + resp.getWriter().print(name); + resp.flushBuffer(); + } + }; + + List errors = new ArrayList(); + if (errorCode != null) + { + errors.add(errorCode.toString()); + } + if (exceptionType != null) + { + errors.add(exceptionType.getName()); + } + + Dictionary errorPageProps = new Hashtable(); + errorPageProps.put(HTTP_WHITEBOARD_SERVLET_NAME, name); + errorPageProps.put(HTTP_WHITEBOARD_SERVLET_ERROR_PAGE, errors); + if (context != null) + { + errorPageProps.put(HTTP_WHITEBOARD_CONTEXT_SELECT, "(" + HTTP_WHITEBOARD_CONTEXT_NAME + "=" + context + ")"); + } + + registrations.add(m_context.registerService(Servlet.class.getName(), errorPage, errorPageProps)); + } + + private void registerContext(String name, String path) throws InterruptedException + { + Dictionary properties = createDictionary( + HTTP_WHITEBOARD_CONTEXT_NAME, name, + HTTP_WHITEBOARD_CONTEXT_PATH, path); + + ServletContextHelper servletContextHelper = new ServletContextHelper(m_context.getBundle()){ + // test + }; + registrations.add(m_context.registerService(ServletContextHelper.class.getName(), servletContextHelper, properties)); + + Thread.sleep(500); + } + + @After + public void unregisterServices() throws InterruptedException + { + for (ServiceRegistration serviceRegistration : registrations) + { + serviceRegistration.unregister(); + } + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + + Thread.sleep(500); + } + + @Test + public void errorPageForErrorCodeIsSent() throws Exception + { + setupLatches(2); + setupErrorServlet(501, null, null); + setupErrorPage(501, null, "Error page", null); + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertContent(501, "Error page", createURL("/test")); + } + + @Test + public void errorPageForExceptionIsSent() throws Exception + { + setupLatches(2); + setupErrorServlet(null, NullPointerException.class, null); + setupErrorPage(null, NullPointerException.class, "Error page", null); + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertContent(500, "Error page", createURL("/test")); + } + + @Test + public void errorPageForParentExceptionIsSent() throws Exception + { + setupLatches(2); + setupErrorServlet(null, NullPointerException.class, null); + setupErrorPage(null, RuntimeException.class, "Error page", null); + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertContent(500, "Error page", createURL("/test")); + } + + @Test + public void errorPageForExceptionIsPreferedOverErrorCode() throws Exception + { + setupLatches(3); + setupErrorServlet(null, NullPointerException.class, null); + setupErrorPage(500, null, "Error page 2", null); + setupErrorPage(null, NullPointerException.class, "Error page 1", null); + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertContent(500, "Error page 1", createURL("/test")); + } + + @Test + public void errorPageIsHandledPerContext() throws Exception + { + registerContext("context1", "/one"); + registerContext("context2", "/two"); + + setupLatches(3); + setupErrorServlet(501, null, "context1"); + setupErrorPage(501, null, "Error page 1", "context2"); + setupErrorPage(501, null, "Error page 2", "context1"); + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertContent(501, "Error page 2", createURL("/one/test")); + } + + @Test + public void errorPageIsShadowedByHigherRankingPage() throws Exception + { + registerContext("context1", "/one"); + registerContext("context2", "/two"); + + // Shadowed error page is not initialized + setupLatches(2); + setupErrorServlet(501, null, "context1"); + setupErrorPage(501, null, "Error page 1", "context1"); + setupErrorPage(501, null, "Error page 2", "context1"); + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertContent(501, "Error page 1", createURL("/one/test")); + } +} diff --git a/http/itest/src/test/java/org/apache/felix/http/itest/EventListenerTest.java b/http/itest/src/test/java/org/apache/felix/http/itest/EventListenerTest.java new file mode 100644 index 00000000000..d6ac6e8a655 --- /dev/null +++ b/http/itest/src/test/java/org/apache/felix/http/itest/EventListenerTest.java @@ -0,0 +1,518 @@ +/* + * 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.http.itest; + +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.servlet.Servlet; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletException; +import javax.servlet.ServletRequestEvent; +import javax.servlet.ServletRequestListener; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionListener; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerMethod; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; + +/** + * Test cases for all supported event listeners. + * + * @author Felix Project Team + */ +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerMethod.class) +public class EventListenerTest extends BaseIntegrationTest +{ + private Dictionary getListenerProps() + { + final Dictionary props = new Hashtable(); + props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_LISTENER, "true"); + + return props; + } + + private Dictionary getServletProps(final String pattern) + { + final Dictionary props = new Hashtable(); + props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, pattern); + + return props; + } + + /** + * Tests that {@link HttpSessionListener}s are called whenever a session is created or destroyed. + */ + @Test + public void testHttpSessionListenerOldWhiteboardOk() throws Exception + { + final CountDownLatch createdLatch = new CountDownLatch(1); + final CountDownLatch destroyedLatch = new CountDownLatch(1); + + HttpSessionListener listener = new HttpSessionListener() + { + @Override + public void sessionDestroyed(HttpSessionEvent se) + { + destroyedLatch.countDown(); + } + + @Override + public void sessionCreated(HttpSessionEvent se) + { + createdLatch.countDown(); + } + }; + + ServiceRegistration reg = m_context.registerService(HttpSessionListener.class, listener, null); + + register("/session", new TestServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + HttpSession session = req.getSession(); + session.setMaxInactiveInterval(2); + + resp.setStatus(SC_OK); + resp.flushBuffer(); + } + }); + + try + { + assertContent(SC_OK, null, createURL("/session")); + + // Session should been created... + assertTrue(createdLatch.await(50, TimeUnit.SECONDS)); + + assertContent(SC_OK, null, createURL("/session")); + + // Session should timeout automatically... + assertTrue(destroyedLatch.await(50, TimeUnit.SECONDS)); + } + finally + { + reg.unregister(); + } + } + + /** + * Tests that {@link HttpSessionAttributeListener}s are called whenever a session attribute is added, changed or removed. + */ + @Test + public void testHttpSessionAttributeListenerOldWhiteboardOk() throws Exception + { + final CountDownLatch addedLatch = new CountDownLatch(1); + final CountDownLatch removedLatch = new CountDownLatch(1); + final CountDownLatch replacedLatch = new CountDownLatch(1); + + HttpSessionAttributeListener listener = new HttpSessionAttributeListener() + { + @Override + public void attributeAdded(HttpSessionBindingEvent event) + { + addedLatch.countDown(); + } + + @Override + public void attributeRemoved(HttpSessionBindingEvent event) + { + removedLatch.countDown(); + } + + @Override + public void attributeReplaced(HttpSessionBindingEvent event) + { + replacedLatch.countDown(); + } + }; + + ServiceRegistration reg = m_context.registerService(HttpSessionAttributeListener.class, listener, null); + + register("/session", new TestServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + try + { + HttpSession session = req.getSession(); + + session.setAttribute("foo", "bar"); + + assertTrue(addedLatch.await(5, TimeUnit.SECONDS)); + + session.setAttribute("foo", "qux"); + + assertTrue(replacedLatch.await(5, TimeUnit.SECONDS)); + + session.removeAttribute("foo"); + + assertTrue(removedLatch.await(5, TimeUnit.SECONDS)); + + resp.setStatus(SC_OK); + } + catch (AssertionError ae) + { + resp.sendError(SC_INTERNAL_SERVER_ERROR, ae.getMessage()); + throw ae; + } + catch (InterruptedException e) + { + resp.sendError(SC_SERVICE_UNAVAILABLE, e.getMessage()); + } + finally + { + resp.flushBuffer(); + } + } + }); + + try + { + assertContent(SC_OK, null, createURL("/session")); + } + finally + { + reg.unregister(); + } + } + + /** + * Tests that {@link HttpSessionListener}s are called whenever a session is created or destroyed. + */ + @Test + public void testHttpSessionListenerOk() throws Exception + { + final CountDownLatch createdLatch = new CountDownLatch(1); + final CountDownLatch destroyedLatch = new CountDownLatch(1); + + HttpSessionListener listener = new HttpSessionListener() + { + @Override + public void sessionDestroyed(HttpSessionEvent se) + { + destroyedLatch.countDown(); + } + + @Override + public void sessionCreated(HttpSessionEvent se) + { + createdLatch.countDown(); + } + }; + + ServiceRegistration reg = m_context.registerService(HttpSessionListener.class, listener, getListenerProps()); + ServiceRegistration regS = m_context.registerService(Servlet.class, + new TestServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + HttpSession session = req.getSession(); + session.setMaxInactiveInterval(2); + + resp.setStatus(SC_OK); + resp.flushBuffer(); + } + }, getServletProps("/session")); + + try + { + assertContent(SC_OK, null, createURL("/session")); + + // Session should been created... + assertTrue(createdLatch.await(50, TimeUnit.SECONDS)); + + assertContent(SC_OK, null, createURL("/session")); + + // Session should timeout automatically... + assertTrue(destroyedLatch.await(50, TimeUnit.SECONDS)); + } + finally + { + reg.unregister(); + regS.unregister(); + } + } + + /** + * Tests that {@link HttpSessionAttributeListener}s are called whenever a session attribute is added, changed or removed. + */ + @Test + public void testHttpSessionAttributeListenerOk() throws Exception + { + final CountDownLatch addedLatch = new CountDownLatch(1); + final CountDownLatch removedLatch = new CountDownLatch(1); + final CountDownLatch replacedLatch = new CountDownLatch(1); + + HttpSessionAttributeListener listener = new HttpSessionAttributeListener() + { + @Override + public void attributeAdded(HttpSessionBindingEvent event) + { + addedLatch.countDown(); + } + + @Override + public void attributeRemoved(HttpSessionBindingEvent event) + { + removedLatch.countDown(); + } + + @Override + public void attributeReplaced(HttpSessionBindingEvent event) + { + replacedLatch.countDown(); + } + }; + + ServiceRegistration reg = m_context.registerService(HttpSessionAttributeListener.class, listener, getListenerProps()); + + ServiceRegistration regS = m_context.registerService(Servlet.class, + new TestServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + try + { + HttpSession session = req.getSession(); + + session.setAttribute("foo", "bar"); + + assertTrue(addedLatch.await(5, TimeUnit.SECONDS)); + + session.setAttribute("foo", "qux"); + + assertTrue(replacedLatch.await(5, TimeUnit.SECONDS)); + + session.removeAttribute("foo"); + + assertTrue(removedLatch.await(5, TimeUnit.SECONDS)); + + resp.setStatus(SC_OK); + } + catch (AssertionError ae) + { + resp.sendError(SC_INTERNAL_SERVER_ERROR, ae.getMessage()); + throw ae; + } + catch (InterruptedException e) + { + resp.sendError(SC_SERVICE_UNAVAILABLE, e.getMessage()); + } + finally + { + resp.flushBuffer(); + } + } + }, getServletProps("/session")); + + try + { + assertContent(SC_OK, null, createURL("/session")); + } + finally + { + reg.unregister(); + } + } + + /** + * Tests {@link ServletContextListener}s + */ + @Test + public void testServletContextListener() throws Exception + { + final CountDownLatch initLatch = new CountDownLatch(1); + final CountDownLatch destroyLatch = new CountDownLatch(1); + + final ServletContextListener listener = new ServletContextListener() + { + + @Override + public void contextInitialized(final ServletContextEvent sce) + { + initLatch.countDown(); + } + + @Override + public void contextDestroyed(final ServletContextEvent sce) + { + destroyLatch.countDown(); + } + }; + + // register with default context + final ServiceRegistration reg = m_context.registerService(ServletContextListener.class, listener, getListenerProps()); + + try + { + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + } + finally + { + reg.unregister(); + } + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + } + + /** + * Tests {@link ServletRequestListener}s + */ + @Test + public void testServletRequestListener() throws Exception + { + final List list = new ArrayList<>(); + + final ServletRequestListener listener = new ServletRequestListener() + { + + @Override + public void requestDestroyed(ServletRequestEvent arg0) + { + list.add("DESTROY"); + } + + @Override + public void requestInitialized(ServletRequestEvent arg0) + { + list.add("INIT"); + } + }; + + // register with default context + final ServiceRegistration reg1 = m_context.registerService(ServletRequestListener.class, listener, getListenerProps()); + // register proprietary listener + final ServiceRegistration reg2 = m_context.registerService(ServletRequestListener.class, listener, null); + + // register test servlet with default context + ServiceRegistration regS = m_context.registerService(Servlet.class, + new TestServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + resp.setStatus(SC_OK); + resp.flushBuffer(); + } + }, getServletProps("/test")); + + try + { + assertEquals(0, list.size()); + assertContent(SC_OK, null, createURL("/test")); + assertEquals(2, list.size()); + assertEquals("INIT", list.get(0)); + assertEquals("DESTROY", list.get(1)); + } + finally + { + reg1.unregister(); + reg2.unregister(); + regS.unregister(); + } + } + + /** + * Tests {@link ServletRequestListener}s + */ + @Test + public void testServletRequestListenerWithHttpAdmin() throws Exception + { + final List list = new ArrayList<>(); + + final ServletRequestListener listener = new ServletRequestListener() + { + + @Override + public void requestDestroyed(ServletRequestEvent arg0) + { + list.add("DESTROY"); + } + + @Override + public void requestInitialized(ServletRequestEvent arg0) + { + list.add("INIT"); + } + }; + + // register with all contexts + final Dictionary props = new Hashtable(); + props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_LISTENER, "true"); + props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT, "(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=*)"); + final ServiceRegistration reg = m_context.registerService(ServletRequestListener.class, listener, props); + // register proprietary listener + final ServiceRegistration reg2 = m_context.registerService(ServletRequestListener.class, listener, null); + + // register test servlet with http service + getHttpService().registerServlet("/test", new TestServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + resp.setStatus(SC_OK); + resp.flushBuffer(); + } + }, null, null); + + try + { + assertEquals(0, list.size()); + assertContent(SC_OK, null, createURL("/test")); + assertEquals(4, list.size()); + assertEquals("INIT", list.get(0)); + assertEquals("INIT", list.get(1)); + assertEquals("DESTROY", list.get(2)); + assertEquals("DESTROY", list.get(3)); + } + finally + { + reg.unregister(); + reg2.unregister(); + getHttpService().unregister("/test"); + } + } +} diff --git a/http/itest/src/test/java/org/apache/felix/http/itest/HttpJettyConnectorTest.java b/http/itest/src/test/java/org/apache/felix/http/itest/HttpJettyConnectorTest.java new file mode 100644 index 00000000000..8b3b7e2ba5e --- /dev/null +++ b/http/itest/src/test/java/org/apache/felix/http/itest/HttpJettyConnectorTest.java @@ -0,0 +1,85 @@ +/* + * 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.http.itest; + +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.apache.felix.http.jetty.ConnectorFactory; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Server; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerMethod; +import org.osgi.framework.ServiceRegistration; + +/** + * @author Felix Project Team + */ +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerMethod.class) +public class HttpJettyConnectorTest extends BaseIntegrationTest +{ + @Test + public void testRegisterConnectorFactoryOk() throws Exception + { + final CountDownLatch openLatch = new CountDownLatch(1); + final CountDownLatch closeLatch = new CountDownLatch(1); + + ConnectorFactory factory = new ConnectorFactory() + { + @Override + public Connector createConnector(Server server) + { + return new LocalConnector(server) + { + @Override + public void doStart() throws Exception + { + openLatch.countDown(); + super.doStart(); + } + + @Override + public void doStop() throws Exception + { + closeLatch.countDown(); + super.doStop(); + } + + }; + } + }; + + ServiceRegistration reg = m_context.registerService(ConnectorFactory.class.getName(), factory, null); + + // Should be opened automatically when picked up by the Jetty implementation... + assertTrue("Felix HTTP Jetty did not open the Connection or pick up the registered ConnectionFactory", openLatch.await(5, TimeUnit.SECONDS)); + + // Should close our connection... + reg.unregister(); + + assertTrue("Felix HTTP Jetty did not close the Connection", closeLatch.await(5, TimeUnit.SECONDS)); + } +} diff --git a/http/itest/src/test/java/org/apache/felix/http/itest/HttpJettyTest.java b/http/itest/src/test/java/org/apache/felix/http/itest/HttpJettyTest.java new file mode 100644 index 00000000000..899aeb95eb8 --- /dev/null +++ b/http/itest/src/test/java/org/apache/felix/http/itest/HttpJettyTest.java @@ -0,0 +1,302 @@ +/* + * 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.http.itest; + +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.net.ConnectException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerMethod; +import org.osgi.framework.Bundle; +import org.osgi.service.http.HttpContext; + +/** + * @author Felix Project Team + */ +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerMethod.class) +public class HttpJettyTest extends BaseIntegrationTest +{ + + /** + * Tests the starting of Jetty. + */ + @Test + public void test00_StartJettyOk() throws Exception + { + assertTrue(getHttpJettyBundle().getState() == Bundle.ACTIVE); + + assertResponseCode(SC_NOT_FOUND, createURL("/")); + } + + /** + * Tests the starting of Jetty. + */ + @Test + public void test00_StopJettyOk() throws Exception + { + Bundle bundle = getHttpJettyBundle(); + + assertTrue(bundle.getState() == Bundle.ACTIVE); + + CountDownLatch initLatch = new CountDownLatch(1); + CountDownLatch destroyLatch = new CountDownLatch(1); + + TestServlet servlet = new TestServlet(initLatch, destroyLatch); + + register("/test", servlet); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertResponseCode(SC_OK, createURL("/test")); + + bundle.stop(); + + assertFalse(destroyLatch.await(5, TimeUnit.SECONDS)); + + try + { + createURL("/test").openStream(); + fail("Could connect to stopped Jetty instance?!"); + } + catch (ConnectException e) + { + // Ok; expected... + } + + bundle.start(); + + Thread.sleep(500); // Allow Jetty to start (still done asynchronously)... + + assertResponseCode(SC_NOT_FOUND, createURL("/test")); + } + + @Test + public void testCorrectPathInfoInHttpContextOk() throws Exception + { + CountDownLatch initLatch = new CountDownLatch(1); + CountDownLatch destroyLatch = new CountDownLatch(1); + + HttpContext context = new HttpContext() + { + @Override + public String getMimeType(String name) + { + return null; + } + + @Override + public URL getResource(String name) + { + return null; + } + + @Override + public boolean handleSecurity(HttpServletRequest request, HttpServletResponse response) throws IOException + { + try + { + assertEquals("", request.getContextPath()); + assertEquals("/foo", request.getServletPath()); + assertEquals("/bar", request.getPathInfo()); + assertEquals("/foo/bar", request.getRequestURI()); + assertEquals("qux=quu", request.getQueryString()); + return true; + } + catch (Exception e) + { + e.printStackTrace(); + } + return false; + } + }; + + TestServlet servlet = new TestServlet(initLatch, destroyLatch); + + register("/foo", servlet, context); + + URL testURL = createURL("/foo/bar?qux=quu"); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertResponseCode(SC_OK, testURL); + + unregister("/foo"); + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + + assertResponseCode(SC_NOT_FOUND, testURL); + } + + /** + * Tests that we can register a servlet with Jetty and that its lifecycle is correctly controlled. + */ + @Test + public void testRegisterServletLifecycleOk() throws Exception + { + CountDownLatch initLatch = new CountDownLatch(1); + CountDownLatch destroyLatch = new CountDownLatch(1); + + TestServlet servlet = new TestServlet(initLatch, destroyLatch); + + register("/test", servlet); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + unregister("/test"); + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + } + + /** + * Tests that initialization parameters are properly passed. + */ + @Test + public void testInitParametersOk() throws Exception + { + final CountDownLatch initLatch = new CountDownLatch(1); + + Servlet servlet = new HttpServlet() + { + @Override + public void init(ServletConfig config) throws ServletException + { + String value1 = config.getInitParameter("key1"); + String value2 = config.getInitParameter("key2"); + if ("value1".equals(value1) && "value2".equals(value2)) + { + initLatch.countDown(); + } + } + }; + + Dictionary params = new Hashtable(); + params.put("key1", "value1"); + params.put("key2", "value2"); + + getHttpService().registerServlet("/initTest", servlet, params, null); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + unregister("/initTest"); + } + + @Test + public void testUseServletContextOk() throws Exception + { + CountDownLatch initLatch = new CountDownLatch(1); + CountDownLatch destroyLatch = new CountDownLatch(1); + + HttpContext context = new HttpContext() + { + @Override + public String getMimeType(String name) + { + return null; + } + + @Override + public URL getResource(String name) + { + try + { + File f = new File("src/test/resources/resource/" + name); + if (f.exists()) + { + return f.toURI().toURL(); + } + } + catch (MalformedURLException e) + { + fail(); + } + return null; + } + + @Override + public boolean handleSecurity(HttpServletRequest request, HttpServletResponse response) throws IOException + { + return true; + } + }; + + TestServlet servlet = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + public void init(ServletConfig config) throws ServletException + { + ServletContext context = config.getServletContext(); + try + { + assertEquals("", context.getContextPath()); + assertNotNull(context.getResource("test.html")); + assertNotNull(context.getRealPath("test.html")); + } + catch (MalformedURLException e) + { + fail(); + } + + super.init(config); + } + }; + + register("/foo", servlet, context); + + URL testURL = createURL("/foo"); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertResponseCode(SC_OK, testURL); + + unregister("/foo"); + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + + assertResponseCode(SC_NOT_FOUND, testURL); + } +} diff --git a/http/itest/src/test/java/org/apache/felix/http/itest/HttpServiceRuntimeTest.java b/http/itest/src/test/java/org/apache/felix/http/itest/HttpServiceRuntimeTest.java new file mode 100644 index 00000000000..b87a2c5dad8 --- /dev/null +++ b/http/itest/src/test/java/org/apache/felix/http/itest/HttpServiceRuntimeTest.java @@ -0,0 +1,1462 @@ +/* + * 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.http.itest; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.osgi.framework.Constants.SERVICE_RANKING; +import static org.osgi.service.http.runtime.HttpServiceRuntimeConstants.HTTP_SERVICE_ENDPOINT; +import static org.osgi.service.http.runtime.HttpServiceRuntimeConstants.HTTP_SERVICE_ID; +import static org.osgi.service.http.runtime.dto.DTOConstants.FAILURE_REASON_EXCEPTION_ON_INIT; +import static org.osgi.service.http.runtime.dto.DTOConstants.FAILURE_REASON_NO_SERVLET_CONTEXT_MATCHING; +import static org.osgi.service.http.runtime.dto.DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE; +import static org.osgi.service.http.runtime.dto.DTOConstants.FAILURE_REASON_VALIDATION_FAILED; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_PATH; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_NAME; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_PATTERN; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_LISTENER; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_RESOURCE_PATTERN; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_RESOURCE_PREFIX; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_ERROR_PAGE; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_NAME; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_TARGET; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Dictionary; +import java.util.HashSet; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.servlet.Filter; +import javax.servlet.FilterConfig; +import javax.servlet.Servlet; +import javax.servlet.ServletContextAttributeListener; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletException; +import javax.servlet.ServletRequestAttributeListener; +import javax.servlet.ServletRequestListener; +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionListener; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerMethod; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.dto.ServiceReferenceDTO; +import org.osgi.service.http.HttpService; +import org.osgi.service.http.context.ServletContextHelper; +import org.osgi.service.http.runtime.HttpServiceRuntime; +import org.osgi.service.http.runtime.dto.FailedErrorPageDTO; +import org.osgi.service.http.runtime.dto.FailedServletDTO; +import org.osgi.service.http.runtime.dto.RequestInfoDTO; +import org.osgi.service.http.runtime.dto.RuntimeDTO; +import org.osgi.service.http.runtime.dto.ServletContextDTO; +import org.osgi.service.http.runtime.dto.ServletDTO; + +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerMethod.class) +public class HttpServiceRuntimeTest extends BaseIntegrationTest +{ + private static final String HTTP_CONTEXT_NAME = "org.osgi.service.http"; + + Collection> registrations = new ArrayList<>(); + + private static final long DEFAULT_SLEEP = 100; + + @After + public void unregisterServices() throws Exception + { + for (ServiceRegistration serviceRegistration : registrations) + { + try + { + serviceRegistration.unregister(); + } + catch (Exception e) + { + // already unregistered + } + } + registrations.clear(); + + Thread.sleep(100); + } + + private void registerServlet(String name, String path) throws InterruptedException + { + CountDownLatch initLatch = new CountDownLatch(1); + registerServlet(name, path, null, initLatch); + awaitServiceRegistration(initLatch); + } + + private void registerServlet(String name, String path, CountDownLatch initLatch) + { + registerServlet(name, path, null, initLatch); + } + + private void registerServlet(String name, String path, String context, CountDownLatch initLatch) + { + List propertyEntries = Arrays.asList( + HTTP_WHITEBOARD_SERVLET_PATTERN, path, + HTTP_WHITEBOARD_SERVLET_NAME, name, + HTTP_WHITEBOARD_CONTEXT_SELECT, context); + + Dictionary properties = createDictionary(context == null ? + propertyEntries.subList(0, 4).toArray() : propertyEntries.toArray()); + + registrations.add(m_context.registerService(Servlet.class.getName(), new TestServlet(initLatch, null), properties)); + } + + private void registerFilter(String name, String path) throws InterruptedException + { + CountDownLatch initLatch = new CountDownLatch(1); + registerFilter(name, path, initLatch); + awaitServiceRegistration(initLatch); + } + + private void registerFilter(String name, String path, CountDownLatch initLatch) + { + registerFilter(name, path, null, initLatch); + } + + private void registerFilter(String name, String path, String context, CountDownLatch initLatch) + { + List propertyEntries = Arrays.asList( + HTTP_WHITEBOARD_FILTER_PATTERN, path, + HTTP_WHITEBOARD_FILTER_NAME, name, + HTTP_WHITEBOARD_CONTEXT_SELECT, context); + + Dictionary properties = createDictionary(context == null ? + propertyEntries.subList(0, 4).toArray() : propertyEntries.toArray()); + + registrations.add(m_context.registerService(Filter.class.getName(), new TestFilter(initLatch, null), properties)); + } + + private void registerResource(String prefix, String path) throws InterruptedException + { + registerResource(prefix, path, null); + } + + private void registerResource(String prefix, String path, String context) throws InterruptedException + { + List propertyEntries = Arrays.asList( + HTTP_WHITEBOARD_RESOURCE_PATTERN, path, + HTTP_WHITEBOARD_RESOURCE_PREFIX, prefix, + HTTP_WHITEBOARD_CONTEXT_SELECT, context); + + Dictionary properties = createDictionary(context == null ? + propertyEntries.subList(0, 4).toArray() : propertyEntries.toArray()); + + registrations.add(m_context.registerService(TestResource.class.getName(), new TestResource(), properties)); + awaitServiceRegistration(); + } + + private void registerErrorPage(String name, List errors) throws InterruptedException + { + CountDownLatch initLatch = new CountDownLatch(1); + registerErrorPage(name, errors, initLatch); + awaitServiceRegistration(initLatch); + } + + private void registerErrorPage(String name, List errors, CountDownLatch initLatch) + { + registerErrorPage(name, errors, null, initLatch); + } + + private void registerErrorPage(String name, List errors, String context, CountDownLatch initLatch) + { + List propertyEntries = Arrays.asList( + HTTP_WHITEBOARD_SERVLET_ERROR_PAGE, errors, + HTTP_WHITEBOARD_SERVLET_NAME, name, + HTTP_WHITEBOARD_CONTEXT_SELECT, context); + + Dictionary properties = createDictionary(context == null ? + propertyEntries.subList(0, 4).toArray() : propertyEntries.toArray()); + + registrations.add(m_context.registerService(Servlet.class.getName(), new TestServlet(initLatch, null), properties)); + } + + private void registerListener(Class listenerClass, boolean useWithWhiteboard) throws InterruptedException + { + registerListener(listenerClass, useWithWhiteboard, null); + } + + private void registerListener(Class listenerClass, boolean useWithWhiteboard, String context) throws InterruptedException + { + List propertyEntries = Arrays.asList( + HTTP_WHITEBOARD_LISTENER, useWithWhiteboard ? "true" : "false", + HTTP_WHITEBOARD_CONTEXT_SELECT, context); + + Dictionary properties = createDictionary(context == null ? + propertyEntries.subList(0, 2).toArray() : propertyEntries.toArray()); + + registrations.add(m_context.registerService(listenerClass.getName(), mock(listenerClass), properties)); + awaitServiceRegistration(); + } + + private ServiceRegistration registerContext(String name, String path) throws InterruptedException + { + Dictionary properties = createDictionary( + HTTP_WHITEBOARD_CONTEXT_NAME, name, + HTTP_WHITEBOARD_CONTEXT_PATH, path); + + ServiceRegistration contextRegistration = m_context.registerService(ServletContextHelper.class.getName(), mock(ServletContextHelper.class), properties); + registrations.add(contextRegistration); + awaitServiceRegistration(); + return contextRegistration; + } + + @Before + public void awaitServiceRuntime() throws Exception + { + awaitService(HttpServiceRuntime.class.getName()); + } + + @Test + public void httpRuntimeServiceIsAvailableAfterBundleActivation() throws Exception + { + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + ServiceReferenceDTO serviceDTO = runtimeDTO.serviceDTO; + + assertNotNull(serviceDTO); + assertNotNull(serviceDTO.properties); + assertTrue(serviceDTO.properties.containsKey(HTTP_SERVICE_ID)); + assertTrue(serviceDTO.properties.containsKey(HTTP_SERVICE_ENDPOINT)); + + assertTrue(serviceDTO.properties.get(HTTP_SERVICE_ID) instanceof Collection); + final Collection ids = (Collection)serviceDTO.properties.get(HTTP_SERVICE_ID); + assertTrue(ids.size() == 1); + assertTrue(ids.iterator().next() instanceof Long); + assertTrue(0 < (Long)ids.iterator().next()); + + assertEquals(0, runtimeDTO.failedErrorPageDTOs.length); + assertEquals(0, runtimeDTO.failedFilterDTOs.length); + assertEquals(0, runtimeDTO.failedListenerDTOs.length); + assertEquals(0, runtimeDTO.failedResourceDTOs.length); + assertEquals(0, runtimeDTO.failedServletContextDTOs.length); + assertEquals(0, runtimeDTO.failedServletDTOs.length); + + ServletContextDTO defaultContext = assertDefaultContext(runtimeDTO); + + assertEquals(0, defaultContext.attributes.size()); + // TODO The default context should have a negative service Id +// assertTrue(0 > runtimeDTO.servletContextDTOs[0].serviceId); + // TODO Should be "/" ? + assertEquals("", defaultContext.contextPath); + assertEquals(0, defaultContext.initParams.size()); + + assertEquals(0, defaultContext.filterDTOs.length); + assertEquals(0, defaultContext.servletDTOs.length); + assertEquals(0, defaultContext.resourceDTOs.length); + assertEquals(0, defaultContext.errorPageDTOs.length); + assertEquals(0, defaultContext.listenerDTOs.length); + } + + @Test + public void dtosForSuccesfullyRegisteredServlets() throws Exception + { + //register first servlet + registerServlet("testServlet 1", "/servlet_1"); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTOWithFirstSerlvet = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTOWithFirstSerlvet.failedServletDTOs.length); + + ServletContextDTO contextDTO = assertDefaultContext(runtimeDTOWithFirstSerlvet); + assertEquals(1, contextDTO.servletDTOs.length); + assertEquals("testServlet 1", contextDTO.servletDTOs[0].name); + + //register second servlet + registerServlet("testServlet 2", "/servlet_2"); + + RuntimeDTO runtimeDTOWithBothSerlvets = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTOWithBothSerlvets.failedServletDTOs.length); + + contextDTO = assertDefaultContext(runtimeDTOWithBothSerlvets); + assertEquals(2, contextDTO.servletDTOs.length); + final Set names = new HashSet<>(); + names.add(contextDTO.servletDTOs[0].name); + names.add(contextDTO.servletDTOs[1].name); + assertTrue(names.contains("testServlet 1")); + assertTrue(names.contains("testServlet 2")); + } + + @Test + public void dtosForSuccesfullyRegisteredFilters() throws Exception + { + //register first filter + registerFilter("testFilter 1", "/servlet_1"); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTOWithFirstFilter = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTOWithFirstFilter.failedFilterDTOs.length); + + ServletContextDTO contextDTO = assertDefaultContext(runtimeDTOWithFirstFilter); + assertEquals(1, contextDTO.filterDTOs.length); + assertEquals("testFilter 1", contextDTO.filterDTOs[0].name); + + //register second filter + registerFilter("testFilter 2", "/servlet_1"); + + RuntimeDTO runtimeDTOWithBothFilters = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTOWithBothFilters.failedFilterDTOs.length); + + contextDTO = assertDefaultContext(runtimeDTOWithBothFilters); + assertEquals(2, contextDTO.filterDTOs.length); + assertEquals("testFilter 1", contextDTO.filterDTOs[0].name); + assertEquals("testFilter 2", contextDTO.filterDTOs[1].name); + } + + @Test + public void dtosForSuccesfullyRegisteredResources() throws Exception + { + // register first resource service + registerResource("/resources", "/resource_1/*"); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTOWithFirstResource = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTOWithFirstResource.failedResourceDTOs.length); + + ServletContextDTO contextDTO = assertDefaultContext(runtimeDTOWithFirstResource); + assertEquals(1, contextDTO.resourceDTOs.length); + assertEquals("/resources", contextDTO.resourceDTOs[0].prefix); + assertArrayEquals(new String[] { "/resource_1/*" }, contextDTO.resourceDTOs[0].patterns); + + // register second resource service + registerResource("/resources", "/resource_2/*"); + + RuntimeDTO runtimeDTOWithBothResources = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTOWithBothResources.failedResourceDTOs.length); + + contextDTO = assertDefaultContext(runtimeDTOWithBothResources); + assertEquals(2, contextDTO.resourceDTOs.length); + assertEquals("/resources", contextDTO.resourceDTOs[0].prefix); + assertEquals(1, contextDTO.resourceDTOs[0].patterns.length); + assertEquals(1, contextDTO.resourceDTOs[1].patterns.length); + final Set patterns = new HashSet<>(); + patterns.add(contextDTO.resourceDTOs[0].patterns[0]); + patterns.add(contextDTO.resourceDTOs[1].patterns[0]); + assertTrue(patterns.contains("/resource_1/*")); + assertTrue(patterns.contains("/resource_2/*")); + } + + @Test + public void dtosForSuccesfullyRegisteredErrorPages() throws Exception + { + // register first error page + registerErrorPage("error page 1", asList("404", NoSuchElementException.class.getName())); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTOWithFirstErrorPage = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTOWithFirstErrorPage.failedServletDTOs.length); + assertEquals(0, runtimeDTOWithFirstErrorPage.failedErrorPageDTOs.length); + + ServletContextDTO contextDTO = assertDefaultContext(runtimeDTOWithFirstErrorPage); + assertEquals(1, contextDTO.errorPageDTOs.length); + assertEquals("error page 1", contextDTO.errorPageDTOs[0].name); + assertArrayEquals(new String[] { NoSuchElementException.class.getName() }, contextDTO.errorPageDTOs[0].exceptions); + assertArrayEquals(new long[] { 404 }, contextDTO.errorPageDTOs[0].errorCodes); + + // register second error page + registerErrorPage("error page 2", asList("500", ServletException.class.getName())); + + RuntimeDTO runtimeDTOWithBothErrorPages = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTOWithBothErrorPages.failedServletDTOs.length); + assertEquals(0, runtimeDTOWithBothErrorPages.failedErrorPageDTOs.length); + + contextDTO = assertDefaultContext(runtimeDTOWithBothErrorPages); + assertEquals(2, contextDTO.errorPageDTOs.length); + assertEquals("error page 1", contextDTO.errorPageDTOs[0].name); + assertEquals("error page 2", contextDTO.errorPageDTOs[1].name); + assertArrayEquals(new String[] { ServletException.class.getName() }, contextDTO.errorPageDTOs[1].exceptions); + assertArrayEquals(new long[] { 500 }, contextDTO.errorPageDTOs[1].errorCodes); + } + + @Test + public void dtosForSuccesfullyRegisteredErrorPageForClientErrorCodes() throws Exception + { + dtosForSuccesfullyRegisteredErrorPageWithWildcardErrorCode("4xx", 400); + } + + @Test + public void dtosForSuccesfullyRegisteredErrorPageForClientErrorCodesCaseInsensitive() throws Exception + { + dtosForSuccesfullyRegisteredErrorPageWithWildcardErrorCode("4xX", 400); + } + + @Test + public void dtosForSuccesfullyRegisteredErrorPageForServerErrorCodes() throws Exception + { + dtosForSuccesfullyRegisteredErrorPageWithWildcardErrorCode("5xx", 500); + } + + @Test + public void dtosForSuccesfullyRegisteredErrorPageForServerErrorCodesCaseInsensitive() throws Exception + { + dtosForSuccesfullyRegisteredErrorPageWithWildcardErrorCode("5XX", 500); + } + + public void dtosForSuccesfullyRegisteredErrorPageWithWildcardErrorCode(String code, long startCode) throws Exception + { + registerErrorPage("error page 1", asList(code)); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTOWithErrorPage = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTOWithErrorPage.failedServletDTOs.length); + assertEquals(0, runtimeDTOWithErrorPage.failedErrorPageDTOs.length); + + ServletContextDTO contextDTO = assertDefaultContext(runtimeDTOWithErrorPage); + assertEquals(1, contextDTO.errorPageDTOs.length); + assertEquals("error page 1", contextDTO.errorPageDTOs[0].name); + assertContainsAllHundredFrom(startCode, contextDTO.errorPageDTOs[0].errorCodes); + } + + private void assertContainsAllHundredFrom(Long start, long[] errorCodes) + { + assertEquals(100, errorCodes.length); + SortedSet distinctErrorCodes = new TreeSet<>(); + for (Long code : errorCodes) + { + distinctErrorCodes.add(code); + } + assertEquals(100, distinctErrorCodes.size()); + assertEquals(start, distinctErrorCodes.first()); + assertEquals(Long.valueOf(start + 99), distinctErrorCodes.last()); + } + + @Test + public void dtosForSuccesfullyRegisteredListeners() throws Exception + { + // register a servlet context listenere as first listener + registerListener(ServletContextListener.class, true); + awaitService(ServletContextListener.class.getName()); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTOWithFirstListener = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTOWithFirstListener.failedListenerDTOs.length); + + ServletContextDTO contextDTO = assertDefaultContext(runtimeDTOWithFirstListener); + assertEquals(1, contextDTO.listenerDTOs.length); + assertEquals(ServletContextListener.class.getName(), contextDTO.listenerDTOs[0].types[0]); + + // register all other listener types + registerListener(ServletContextAttributeListener.class, true); + registerListener(ServletRequestListener.class, true); + registerListener(ServletRequestAttributeListener.class, true); + registerListener(HttpSessionListener.class, true); + registerListener(HttpSessionAttributeListener.class, true); + + awaitService(ServletContextAttributeListener.class.getName()); + awaitService(ServletRequestListener.class.getName()); + awaitService(ServletRequestAttributeListener.class.getName()); + awaitService(HttpSessionListener.class.getName()); + awaitService(HttpSessionAttributeListener.class.getName()); + + RuntimeDTO runtimeDTOWithAllListeners = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTOWithAllListeners.failedListenerDTOs.length); + + contextDTO = assertDefaultContext(runtimeDTOWithAllListeners); + + assertEquals(6, contextDTO.listenerDTOs.length); + assertEquals(ServletContextListener.class.getName(), contextDTO.listenerDTOs[0].types[0]); + assertEquals(ServletContextAttributeListener.class.getName(), contextDTO.listenerDTOs[1].types[0]); + assertEquals(ServletRequestListener.class.getName(), contextDTO.listenerDTOs[2].types[0]); + assertEquals(ServletRequestAttributeListener.class.getName(), contextDTO.listenerDTOs[3].types[0]); + assertEquals(HttpSessionListener.class.getName(), contextDTO.listenerDTOs[4].types[0]); + assertEquals(HttpSessionAttributeListener.class.getName(), contextDTO.listenerDTOs[5].types[0]); + } + + @Test + public void dtosForSuccesfullyRegisteredContexts() throws Exception + { + // register first additional context + registerContext("contextA", "/contextA"); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTOWithAdditionalContext = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTOWithAdditionalContext.failedServletContextDTOs.length); + assertEquals(3, runtimeDTOWithAdditionalContext.servletContextDTOs.length); + + // default context is last, as it has the lowest service ranking + assertEquals(HTTP_CONTEXT_NAME, runtimeDTOWithAdditionalContext.servletContextDTOs[0].name); + assertEquals("", runtimeDTOWithAdditionalContext.servletContextDTOs[0].contextPath); + assertEquals("contextA", runtimeDTOWithAdditionalContext.servletContextDTOs[1].name); + assertEquals("/contextA", runtimeDTOWithAdditionalContext.servletContextDTOs[1].contextPath); + assertEquals("default", runtimeDTOWithAdditionalContext.servletContextDTOs[2].name); + // TODO should this be "/" ? + assertEquals("", runtimeDTOWithAdditionalContext.servletContextDTOs[2].contextPath); + + // register second additional context + registerContext("contextB", "/contextB"); + + RuntimeDTO runtimeDTOWithAllContexts = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTOWithAllContexts.failedServletContextDTOs.length); + assertEquals(4, runtimeDTOWithAllContexts.servletContextDTOs.length); + + // default context is last, as it has the lowest service ranking + assertEquals(HTTP_CONTEXT_NAME, runtimeDTOWithAdditionalContext.servletContextDTOs[0].name); + assertEquals("", runtimeDTOWithAdditionalContext.servletContextDTOs[0].contextPath); + assertEquals("contextA", runtimeDTOWithAllContexts.servletContextDTOs[1].name); + assertEquals("/contextA", runtimeDTOWithAllContexts.servletContextDTOs[1].contextPath); + assertEquals("contextB", runtimeDTOWithAllContexts.servletContextDTOs[2].name); + assertEquals("/contextB", runtimeDTOWithAllContexts.servletContextDTOs[2].contextPath); + assertEquals("default", runtimeDTOWithAllContexts.servletContextDTOs[3].name); + assertEquals("", runtimeDTOWithAllContexts.servletContextDTOs[3].contextPath); + } + + @Test + public void successfulSetup() throws InterruptedException + { + CountDownLatch initLatch = new CountDownLatch(6); + + registerContext("test-context", "/test-context"); + + registerServlet("default servlet", "/default", initLatch); + registerFilter("default filter", "/default", initLatch); + registerErrorPage("default error page", asList(Exception.class.getName()), initLatch); + registerResource("/", "/default/resource"); + registerListener(ServletRequestListener.class, true); + + registerServlet("context servlet", "/default", "(" + HTTP_WHITEBOARD_CONTEXT_NAME + "=test-context)", initLatch); + registerFilter("context filter", "/default", "(" + HTTP_WHITEBOARD_CONTEXT_NAME + "=test-context)", initLatch); + registerErrorPage("context error page", asList("500"), "(" + HTTP_WHITEBOARD_CONTEXT_NAME + "=test-context)", initLatch); + registerResource("/", "/test-contextd/resource", "(" + HTTP_WHITEBOARD_CONTEXT_NAME + "=test-context)"); + registerListener(ServletRequestListener.class, true, "(" + HTTP_WHITEBOARD_CONTEXT_NAME + "=test-context)"); + + awaitServiceRegistration(initLatch); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTO.failedErrorPageDTOs.length); + assertEquals(0, runtimeDTO.failedFilterDTOs.length); + assertEquals(0, runtimeDTO.failedListenerDTOs.length); + assertEquals(0, runtimeDTO.failedResourceDTOs.length); + assertEquals(0, runtimeDTO.failedServletContextDTOs.length); + assertEquals(0, runtimeDTO.failedServletDTOs.length); + + assertEquals(3, runtimeDTO.servletContextDTOs.length); + assertEquals(HTTP_CONTEXT_NAME, runtimeDTO.servletContextDTOs[0].name); + assertEquals("test-context", runtimeDTO.servletContextDTOs[1].name); + assertEquals("default", runtimeDTO.servletContextDTOs[2].name); + + ServletContextDTO defaultContextDTO = runtimeDTO.servletContextDTOs[2]; + long contextServiceId = defaultContextDTO.serviceId; + + assertEquals(Arrays.toString(defaultContextDTO.servletDTOs), 2, defaultContextDTO.servletDTOs.length); + assertServlet(defaultContextDTO.servletDTOs, "default servlet", contextServiceId); + assertServlet(defaultContextDTO.servletDTOs, "default error page", contextServiceId); + + assertEquals(1, defaultContextDTO.filterDTOs.length); + assertEquals("default filter", defaultContextDTO.filterDTOs[0].name); + assertEquals(contextServiceId, defaultContextDTO.filterDTOs[0].servletContextId); + assertEquals(1, defaultContextDTO.errorPageDTOs.length); + assertEquals(Exception.class.getName(), defaultContextDTO.errorPageDTOs[0].exceptions[0]); + assertEquals(contextServiceId, defaultContextDTO.errorPageDTOs[0].servletContextId); + assertEquals(1, defaultContextDTO.listenerDTOs.length); + assertEquals(ServletRequestListener.class.getName(), defaultContextDTO.listenerDTOs[0].types[0]); + assertEquals(contextServiceId, defaultContextDTO.listenerDTOs[0].servletContextId); + + ServletContextDTO testContextDTO = runtimeDTO.servletContextDTOs[1]; + contextServiceId = testContextDTO.serviceId; + + assertEquals(2, testContextDTO.servletDTOs.length); + assertServlet(testContextDTO.servletDTOs, "context servlet", contextServiceId); + assertServlet(testContextDTO.servletDTOs, "context error page", contextServiceId); + + assertEquals(1, testContextDTO.filterDTOs.length); + assertEquals("context filter", testContextDTO.filterDTOs[0].name); + assertEquals(contextServiceId, testContextDTO.filterDTOs[0].servletContextId); + assertEquals(1, testContextDTO.errorPageDTOs.length); + assertEquals(500L, testContextDTO.errorPageDTOs[0].errorCodes[0]); + assertEquals(contextServiceId, testContextDTO.errorPageDTOs[0].servletContextId); + assertEquals(1, testContextDTO.listenerDTOs.length); + assertEquals(ServletRequestListener.class.getName(), testContextDTO.listenerDTOs[0].types[0]); + assertEquals(contextServiceId, testContextDTO.listenerDTOs[0].servletContextId); + } + + private void assertServlet(final ServletDTO[] servletDTOs, + final String name, + final long contextServiceId) + { + assertNotNull(servletDTOs); + for(final ServletDTO dto : servletDTOs) + { + if ( name.equals(dto.name) && contextServiceId == dto.servletContextId ) + { + return; + } + } + fail("Servlet with name " + name + " and context id " + contextServiceId + " not found in " + Arrays.toString(servletDTOs)); + } + + @Test + public void exceptionInServletInitAppearsAsFailure() throws ServletException, InterruptedException + { + Dictionary properties = createDictionary( + HTTP_WHITEBOARD_SERVLET_PATTERN, "/servlet", + HTTP_WHITEBOARD_SERVLET_NAME, "servlet"); + + CountDownLatch initLatch = new CountDownLatch(1); + + @SuppressWarnings("serial") + Servlet failingServlet = new TestServlet(initLatch, null) { + @Override + public void init() throws ServletException + { + super.init(); + throw new ServletException(); + } + }; + + registrations.add(m_context.registerService(Servlet.class.getName(), failingServlet, properties)); + awaitServiceRegistration(initLatch); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + assertEquals(1, runtimeDTO.failedServletDTOs.length); + assertEquals("servlet", runtimeDTO.failedServletDTOs[0].name); + assertEquals(FAILURE_REASON_EXCEPTION_ON_INIT, runtimeDTO.failedServletDTOs[0].failureReason); + } + + @Test + public void exceptionInServletInitDuringServletRemovalAppearsAsFailure() throws ServletException, InterruptedException + { + Dictionary properties1 = createDictionary( + HTTP_WHITEBOARD_SERVLET_PATTERN, "/servlet1", + HTTP_WHITEBOARD_SERVLET_NAME, "servlet1"); + + final CountDownLatch initLatch1 = new CountDownLatch(1); + + @SuppressWarnings("serial") + Servlet failingServlet1 = new TestServlet(initLatch1, null) { + @Override + public void init() throws ServletException + { + //fail when initialized the second time + if (initLatch1.getCount() == 0) + { + throw new ServletException(); + } + super.init(); + } + }; + + Dictionary properties2 = createDictionary( + HTTP_WHITEBOARD_SERVLET_PATTERN, "/servlet2", + HTTP_WHITEBOARD_SERVLET_NAME, "servlet2"); + + final CountDownLatch initLatch2 = new CountDownLatch(1); + @SuppressWarnings("serial") + Servlet failingServlet2 = new TestServlet(initLatch2, null) { + @Override + public void init() throws ServletException + { + //fail when initialized the second time + if (initLatch2.getCount() == 0) + { + throw new ServletException(); + } + super.init(); + } + }; + + Dictionary propertiesShadowing = createDictionary( + HTTP_WHITEBOARD_SERVLET_PATTERN, asList("/servlet1", "/servlet2"), + HTTP_WHITEBOARD_SERVLET_NAME, "servletShadowing", + SERVICE_RANKING, Integer.MAX_VALUE); + + CountDownLatch initLatchShadowing = new CountDownLatch(1); + Servlet servletShadowing = new TestServlet(initLatchShadowing, null); + + registrations.add(m_context.registerService(Servlet.class.getName(), failingServlet1, properties1)); + registrations.add(m_context.registerService(Servlet.class.getName(), failingServlet2, properties2)); + awaitServiceRegistration(initLatch1); + awaitServiceRegistration(initLatch2); + + ServiceRegistration shadowingRegistration = m_context.registerService(Servlet.class.getName(), servletShadowing, propertiesShadowing); + registrations.add(shadowingRegistration); + awaitServiceRegistration(initLatchShadowing); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + assertEquals(2, runtimeDTO.failedServletDTOs.length); + assertEquals(FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE, runtimeDTO.failedServletDTOs[0].failureReason); + assertEquals(FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE, runtimeDTO.failedServletDTOs[1].failureReason); + + shadowingRegistration.unregister(); + + runtimeDTO = serviceRuntime.getRuntimeDTO(); + assertEquals(2, runtimeDTO.failedServletDTOs.length); + assertEquals(FAILURE_REASON_EXCEPTION_ON_INIT, runtimeDTO.failedServletDTOs[0].failureReason); + assertEquals(FAILURE_REASON_EXCEPTION_ON_INIT, runtimeDTO.failedServletDTOs[1].failureReason); + } + + @Test + public void exceptionInFilterInitAppearsAsFailure() throws ServletException, InterruptedException + { + Dictionary properties = createDictionary( + HTTP_WHITEBOARD_FILTER_PATTERN, "/filter", + HTTP_WHITEBOARD_FILTER_NAME, "filter"); + + CountDownLatch initLatch = new CountDownLatch(1); + + Filter failingFilter = new TestFilter(initLatch, null) { + @Override + public void init(FilterConfig config) throws ServletException + { + super.init(config); + throw new ServletException(); + } + }; + + registrations.add(m_context.registerService(Filter.class.getName(), failingFilter, properties)); + awaitServiceRegistration(initLatch); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + assertEquals(1, runtimeDTO.failedFilterDTOs.length); + assertEquals("filter", runtimeDTO.failedFilterDTOs[0].name); + assertEquals(FAILURE_REASON_EXCEPTION_ON_INIT, runtimeDTO.failedFilterDTOs[0].failureReason); + } + + // As specified in OSGi Compendium Release 6, Chapter 140.1 (TODO : exact version) + @Test + public void hiddenDefaultContextAppearsAsFailure() throws InterruptedException + { + registerContext("default", ""); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + assertEquals(1, runtimeDTO.failedServletContextDTOs.length); + assertEquals("default", runtimeDTO.failedServletContextDTOs[0].name); + assertDefaultContext(runtimeDTO); + } + + // As specified in OSGi Compendium Release 6, Chapter 140.1 + @Test + public void contextHelperWithDuplicateNameAppearsAsFailure() throws InterruptedException + { + ServiceRegistration firstContextReg = registerContext("contextA", "/first"); + registerContext("contextA", "/second"); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(1, runtimeDTO.failedServletContextDTOs.length); + assertEquals("contextA", runtimeDTO.failedServletContextDTOs[0].name); + assertEquals("/second", runtimeDTO.failedServletContextDTOs[0].contextPath); + assertEquals(FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE, runtimeDTO.failedServletContextDTOs[0].failureReason); + + assertEquals(3, runtimeDTO.servletContextDTOs.length); + assertEquals(HTTP_CONTEXT_NAME, runtimeDTO.servletContextDTOs[0].name); + assertEquals("default", runtimeDTO.servletContextDTOs[2].name); + + assertEquals("contextA", runtimeDTO.servletContextDTOs[1].name); + assertEquals("/first", runtimeDTO.servletContextDTOs[1].contextPath); + + firstContextReg.unregister(); + + runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTO.failedServletContextDTOs.length); + + assertEquals(3, runtimeDTO.servletContextDTOs.length); + assertEquals(HTTP_CONTEXT_NAME, runtimeDTO.servletContextDTOs[0].name); + assertEquals("default", runtimeDTO.servletContextDTOs[2].name); + + assertEquals("contextA", runtimeDTO.servletContextDTOs[1].name); + assertEquals("/second", runtimeDTO.servletContextDTOs[1].contextPath); + } + + // As specified in OSGi Compendium Release 6, Chapter 140.1 + @Test + public void missingContextHelperNameAppearsAsFailure() + { + Dictionary properties = createDictionary(HTTP_WHITEBOARD_CONTEXT_PATH, ""); + + registrations.add(m_context.registerService(ServletContextHelper.class.getName(), mock(ServletContextHelper.class), properties)); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(1, runtimeDTO.failedServletContextDTOs.length); + assertEquals(null, runtimeDTO.failedServletContextDTOs[0].name); + assertEquals(FAILURE_REASON_VALIDATION_FAILED, runtimeDTO.failedServletContextDTOs[0].failureReason); + } + + // As specified in OSGi Compendium Release 6, Chapter 140.1 + @Test + public void invalidContextHelperNameAppearsAsFailure() throws InterruptedException + { + registerContext("context A", ""); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(1, runtimeDTO.failedServletContextDTOs.length); + assertEquals("context A", runtimeDTO.failedServletContextDTOs[0].name); + assertEquals(FAILURE_REASON_VALIDATION_FAILED, runtimeDTO.failedServletContextDTOs[0].failureReason); + } + + // As specified in OSGi Compendium Release 6, Chapter 140.1 + @Test + public void invalidContextHelperPathAppearsAsFailure() throws InterruptedException + { + registerContext("contextA", "#"); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(1, runtimeDTO.failedServletContextDTOs.length); + assertEquals("#", runtimeDTO.failedServletContextDTOs[0].contextPath); + assertEquals(FAILURE_REASON_VALIDATION_FAILED, runtimeDTO.failedServletContextDTOs[0].failureReason); + } + + // As specified in OSGi Compendium Release 6, Chapter 140.3 + @Test + public void selectionOfNonExistingContextHelperAppearsAsFailure() throws InterruptedException + { + registerServlet("servlet 1", "/", "(" + HTTP_WHITEBOARD_CONTEXT_NAME + "=contextA)", null); + awaitServiceRegistration(); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(1, runtimeDTO.failedServletDTOs.length); + assertEquals("servlet 1", runtimeDTO.failedServletDTOs[0].name); + assertEquals(FAILURE_REASON_NO_SERVLET_CONTEXT_MATCHING, runtimeDTO.failedServletDTOs[0].failureReason); + + registerContext("contextA", "/contextA"); + + runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTO.failedServletDTOs.length); + assertEquals(3, runtimeDTO.servletContextDTOs.length); + assertEquals("contextA", runtimeDTO.servletContextDTOs[1].name); + assertEquals(1, runtimeDTO.servletContextDTOs[1].servletDTOs.length); + assertEquals("servlet 1", runtimeDTO.servletContextDTOs[1].servletDTOs[0].name); + } + + // As specified in OSGi Compendium Release 6, Chapter 140.3 + @Test + public void differentTargetIsIgnored() throws InterruptedException + { + Dictionary properties = createDictionary( + HTTP_WHITEBOARD_SERVLET_PATTERN, "/servlet", + HTTP_WHITEBOARD_SERVLET_NAME, "servlet", + HTTP_WHITEBOARD_TARGET, "(org.osgi.service.http.port=8282)"); + + registrations.add(m_context.registerService(Servlet.class.getName(), new TestServlet(), properties)); + awaitServiceRegistration(); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTO.failedServletDTOs.length); + + ServletContextDTO defaultContext = assertDefaultContext(runtimeDTO); + assertEquals(0, defaultContext.servletDTOs.length); + } + + // As specified in OSGi Compendium Release 6, Chapter 140.4 + @Test + public void servletWithoutNameGetsFullyQualifiedName() throws InterruptedException + { + Dictionary properties = createDictionary(HTTP_WHITEBOARD_SERVLET_PATTERN, "/servlet"); + + CountDownLatch initLatch = new CountDownLatch(1); + registrations.add(m_context.registerService(Servlet.class.getName(), new TestServlet(initLatch, null), properties)); + awaitServiceRegistration(initLatch); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTO.failedServletDTOs.length); + + ServletContextDTO defaultContext = assertDefaultContext(serviceRuntime.getRuntimeDTO()); + assertEquals(1, defaultContext.servletDTOs.length); + assertEquals(TestServlet.class.getName(), defaultContext.servletDTOs[0].name); + } + + // As specified in OSGi Compendium Release 6, Chapter 140.4.1 + @Test + public void patternAndErrorPageSpecified() throws InterruptedException + { + Dictionary properties = createDictionary( + HTTP_WHITEBOARD_SERVLET_PATTERN, "/servlet", + HTTP_WHITEBOARD_SERVLET_NAME, "servlet", + HTTP_WHITEBOARD_SERVLET_ERROR_PAGE, asList("400")); + + registrations.add(m_context.registerService(Servlet.class.getName(), new TestServlet(), properties)); + awaitServiceRegistration(); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + ServletContextDTO defaultContext = assertDefaultContext(runtimeDTO); + + assertEquals(0, runtimeDTO.failedErrorPageDTOs.length); + assertEquals(0, runtimeDTO.failedServletDTOs.length); + + assertEquals(1, defaultContext.servletDTOs.length); + assertEquals(1, defaultContext.errorPageDTOs.length); + + assertEquals("servlet", defaultContext.servletDTOs[0].name); + assertEquals("servlet", defaultContext.errorPageDTOs[0].name); + + assertArrayEquals(new String[] { "/servlet" }, defaultContext.servletDTOs[0].patterns); + assertArrayEquals(new long[] { 400 }, defaultContext.errorPageDTOs[0].errorCodes); + } + + // As specified in OSGi Compendium Release 6, Chapter 140.4.1 + @Test + public void multipleServletsForSamePatternChoosenByServiceRankingRules() throws InterruptedException + { + registerServlet("servlet 1", "/pathcollision"); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTO.failedServletDTOs.length); + ServletContextDTO defaultContext = assertDefaultContext(runtimeDTO); + assertEquals(1, defaultContext.servletDTOs.length); + + Dictionary properties = createDictionary( + HTTP_WHITEBOARD_SERVLET_PATTERN, "/pathcollision", + HTTP_WHITEBOARD_SERVLET_NAME, "servlet 2", + SERVICE_RANKING, Integer.MAX_VALUE); + + CountDownLatch initLatch = new CountDownLatch(1); + CountDownLatch destroyLatch = new CountDownLatch(1); + TestServlet testServlet = new TestServlet(initLatch, destroyLatch); + ServiceRegistration higherRankingServlet = m_context.registerService(Servlet.class.getName(), testServlet, properties); + registrations.add(higherRankingServlet); + + RuntimeDTO runtimeWithShadowedServlet = serviceRuntime.getRuntimeDTO(); + awaitServiceRegistration(initLatch); + + defaultContext = assertDefaultContext(runtimeWithShadowedServlet); + assertEquals(1, defaultContext.servletDTOs.length); + + assertEquals(1, runtimeWithShadowedServlet.failedServletDTOs.length); + FailedServletDTO failedServletDTO = runtimeWithShadowedServlet.failedServletDTOs[0]; + assertEquals("servlet 1", failedServletDTO.name); + assertEquals(FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE, failedServletDTO.failureReason); + + higherRankingServlet.unregister(); + awaitServiceRegistration(destroyLatch); + + runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTO.failedServletDTOs.length); + defaultContext = assertDefaultContext(runtimeDTO); + assertEquals(1, defaultContext.servletDTOs.length); + assertEquals("servlet 1", defaultContext.servletDTOs[0].name); + } + + // As specified in OSGi Compendium Release 6, Chapter 140.4.1 + @Test + public void multipleErrorPagesForSameErrorCodeChoosenByServiceRankingRules() throws InterruptedException + { + registerErrorPage("error page 1", asList(NullPointerException.class.getName(), "500")); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + + ServletContextDTO defaultContext = assertDefaultContext(runtimeDTO); + + assertEquals(0, runtimeDTO.failedErrorPageDTOs.length); + assertEquals(1, defaultContext.errorPageDTOs.length); + + Dictionary properties = createDictionary( + HTTP_WHITEBOARD_SERVLET_ERROR_PAGE, asList("500", IllegalArgumentException.class.getName()), + HTTP_WHITEBOARD_SERVLET_NAME, "error page 2", + SERVICE_RANKING, Integer.MAX_VALUE); + + CountDownLatch initLatch = new CountDownLatch(1); + CountDownLatch destroyLatch = new CountDownLatch(1); + TestServlet testServlet = new TestServlet(initLatch, destroyLatch); + ServiceRegistration higherRankingServlet = m_context.registerService(Servlet.class.getName(), testServlet, properties); + registrations.add(higherRankingServlet); + awaitServiceRegistration(initLatch); + + RuntimeDTO runtimeWithShadowedErrorPage = serviceRuntime.getRuntimeDTO(); + + defaultContext = assertDefaultContext(runtimeWithShadowedErrorPage); + + assertEquals(2, defaultContext.errorPageDTOs.length); + assertEquals("error page 2", defaultContext.errorPageDTOs[0].name); + assertArrayEquals(new long[] { 500 }, defaultContext.errorPageDTOs[0].errorCodes); + assertArrayEquals(new String[] { IllegalArgumentException.class.getName() }, defaultContext.errorPageDTOs[0].exceptions); + assertEquals("error page 1", defaultContext.errorPageDTOs[1].name); + assertEquals(0, defaultContext.errorPageDTOs[1].errorCodes.length); + assertArrayEquals(new String[] { NullPointerException.class.getName() }, defaultContext.errorPageDTOs[1].exceptions); + + assertEquals(1, runtimeWithShadowedErrorPage.failedErrorPageDTOs.length); + FailedErrorPageDTO failedErrorPageDTO = runtimeWithShadowedErrorPage.failedErrorPageDTOs[0]; + assertEquals("error page 1", failedErrorPageDTO.name); + assertArrayEquals(new long[] { 500 }, failedErrorPageDTO.errorCodes); + assertEquals(0, failedErrorPageDTO.exceptions.length); + assertEquals(FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE, failedErrorPageDTO.failureReason); + + higherRankingServlet.unregister(); + awaitServiceRegistration(destroyLatch); + + runtimeDTO = serviceRuntime.getRuntimeDTO(); + + defaultContext = assertDefaultContext(runtimeDTO); + + assertEquals(0, runtimeDTO.failedErrorPageDTOs.length); + assertEquals(1, defaultContext.errorPageDTOs.length); + assertEquals("error page 1", defaultContext.errorPageDTOs[0].name); + } + + // As specified in OSGi Compendium Release 6, Chapter 140.4 + @Test + public void mulitpleServletsWithSamePatternHttpServiceRegistrationWins() throws Exception + { + registerServlet("servlet 1", "/pathcollision"); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTO.failedServletDTOs.length); + ServletContextDTO defaultContext = assertDefaultContext(runtimeDTO); + assertEquals(1, defaultContext.servletDTOs.length); + + CountDownLatch initLatch = new CountDownLatch(1); + CountDownLatch destroyLatch = new CountDownLatch(1); + TestServlet testServlet = new TestServlet(initLatch, destroyLatch); + register("/pathcollision", testServlet); + + RuntimeDTO runtimeWithShadowedServlet = serviceRuntime.getRuntimeDTO(); + awaitServiceRegistration(initLatch); + + defaultContext = assertDefaultContext(runtimeWithShadowedServlet); + ServletContextDTO httpServiceContext = runtimeWithShadowedServlet.servletContextDTOs[0]; + assertEquals(HTTP_CONTEXT_NAME, httpServiceContext.name); + assertEquals(1, httpServiceContext.servletDTOs.length); + assertArrayEquals(new String[] {"/pathcollision"}, httpServiceContext.servletDTOs[0].patterns); + + assertEquals(1, defaultContext.servletDTOs.length); + ServletDTO servletDTO = defaultContext.servletDTOs[0]; + assertEquals("servlet 1", servletDTO.name); + + // check request info DTO to see which servlet responds + final RequestInfoDTO infoDTO = serviceRuntime.calculateRequestInfoDTO("/pathcollision"); + assertEquals(httpServiceContext.serviceId, infoDTO.servletDTO.servletContextId); + + unregister("/pathcollision"); + awaitServiceRegistration(destroyLatch); + + runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTO.failedServletDTOs.length); + defaultContext = assertDefaultContext(runtimeDTO); + assertEquals(1, defaultContext.servletDTOs.length); + assertEquals("servlet 1", defaultContext.servletDTOs[0].name); + } + + // As specified in OSGi Compendium Release 6, Chapter 140.7 + @Test + public void invalidListenerPopertyValueAppearsAsFailure() throws Exception + { + Dictionary properties = createDictionary(HTTP_WHITEBOARD_LISTENER, "invalid"); + + registrations.add(m_context.registerService(ServletRequestListener.class.getName(), mock(ServletRequestListener.class), properties)); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(1, runtimeDTO.failedListenerDTOs.length); + assertEquals(FAILURE_REASON_VALIDATION_FAILED, runtimeDTO.failedListenerDTOs[0].failureReason); + } + + // As specified in OSGi Compendium Release 6, Chapter 140.8 + @Test + public void contextReplacedWithHigherRankingContext() throws Exception + { + ServiceRegistration firstContext = registerContext("test-context", "/first"); + Long firstContextId = (Long) firstContext.getReference().getProperty(Constants.SERVICE_ID); + + CountDownLatch initLatch = new CountDownLatch(1); + registerServlet("servlet", "/servlet", "(" + HTTP_WHITEBOARD_CONTEXT_NAME + "=test-context)", initLatch); + awaitServiceRegistration(initLatch); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTO.failedServletContextDTOs.length); + assertEquals(3, runtimeDTO.servletContextDTOs.length); + assertEquals(firstContextId.longValue(), runtimeDTO.servletContextDTOs[1].serviceId); + assertEquals("test-context", runtimeDTO.servletContextDTOs[1].name); + assertEquals("/first", runtimeDTO.servletContextDTOs[1].contextPath); + assertEquals("default", runtimeDTO.servletContextDTOs[2].name); + assertEquals(HTTP_CONTEXT_NAME, runtimeDTO.servletContextDTOs[0].name); + + assertEquals(1, runtimeDTO.servletContextDTOs[1].servletDTOs.length); + assertEquals("servlet", runtimeDTO.servletContextDTOs[1].servletDTOs[0].name); + + Dictionary properties = createDictionary( + HTTP_WHITEBOARD_CONTEXT_NAME, "test-context", + HTTP_WHITEBOARD_CONTEXT_PATH, "/second", + SERVICE_RANKING, Integer.MAX_VALUE); + + ServiceRegistration secondContext = m_context.registerService(ServletContextHelper.class.getName(), mock(ServletContextHelper.class), properties); + registrations.add(secondContext); + Long secondContextId = (Long) secondContext.getReference().getProperty(Constants.SERVICE_ID); + + runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(1, runtimeDTO.failedServletContextDTOs.length); + assertEquals(firstContextId.longValue(), runtimeDTO.failedServletContextDTOs[0].serviceId); + assertEquals("test-context", runtimeDTO.failedServletContextDTOs[0].name); + assertEquals("/first", runtimeDTO.failedServletContextDTOs[0].contextPath); + + assertEquals(3, runtimeDTO.servletContextDTOs.length); + + final List names = new ArrayList<>(); + for(final ServletContextDTO dto : runtimeDTO.servletContextDTOs) + { + names.add(dto.name); + } + final int httpContextIndex = names.indexOf(HTTP_CONTEXT_NAME); + final int secondContextIndex = names.indexOf("test-context"); + final int defaultContextIndex = names.indexOf("default"); + assertEquals(secondContextId.longValue(), runtimeDTO.servletContextDTOs[secondContextIndex].serviceId); + assertEquals("test-context", runtimeDTO.servletContextDTOs[secondContextIndex].name); + assertEquals("/second", runtimeDTO.servletContextDTOs[secondContextIndex].contextPath); + assertEquals("default", runtimeDTO.servletContextDTOs[defaultContextIndex].name); + assertEquals(HTTP_CONTEXT_NAME, runtimeDTO.servletContextDTOs[httpContextIndex].name); + + assertEquals(1, runtimeDTO.servletContextDTOs[secondContextIndex].servletDTOs.length); + assertEquals("servlet", runtimeDTO.servletContextDTOs[secondContextIndex].servletDTOs[0].name); + + secondContext.unregister(); + + runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTO.failedServletContextDTOs.length); + assertEquals(3, runtimeDTO.servletContextDTOs.length); + assertEquals(firstContextId.longValue(), runtimeDTO.servletContextDTOs[1].serviceId); + assertEquals("test-context", runtimeDTO.servletContextDTOs[1].name); + assertEquals("/first", runtimeDTO.servletContextDTOs[1].contextPath); + assertEquals("default", runtimeDTO.servletContextDTOs[2].name); + assertEquals(HTTP_CONTEXT_NAME, runtimeDTO.servletContextDTOs[0].name); + + assertEquals(1, runtimeDTO.servletContextDTOs[1].servletDTOs.length); + assertEquals("servlet", runtimeDTO.servletContextDTOs[1].servletDTOs[0].name); + } + + // As specified in OSGi Compendium Release 6, Chapter 140.9 + @Test + public void httpServiceIdIsSet() + { + ServiceReference httpServiceRef = m_context.getServiceReference(HttpService.class.getName()); + ServiceReference httpServiceRuntimeRef = m_context.getServiceReference(HttpServiceRuntime.class.getName()); + + Long expectedId = (Long) httpServiceRef.getProperty(Constants.SERVICE_ID); + Collection col = (Collection)httpServiceRuntimeRef.getProperty(HTTP_SERVICE_ID); + Long actualId = (Long) col.iterator().next(); + + assertEquals(expectedId, actualId); + } + + // As specified in OSGi Compendium Release 6, Chapter 140.9 + @Test + public void serviceRegisteredWithHttpServiceHasNegativeServiceId() throws Exception + { + CountDownLatch initLatch = new CountDownLatch(1); + register("/test", new TestServlet(initLatch, null)); + awaitServiceRegistration(initLatch); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(2, runtimeDTO.servletContextDTOs.length); + assertEquals(1, runtimeDTO.servletContextDTOs[0].servletDTOs.length); + assertTrue(0 > runtimeDTO.servletContextDTOs[0].servletDTOs[0].serviceId); + } + + @Test + public void namedServletIsNotIgnored() throws InterruptedException + { + // Neither pattern nor error page specified + Dictionary properties = createDictionary(HTTP_WHITEBOARD_SERVLET_NAME, "servlet"); + + registrations.add(m_context.registerService(Servlet.class.getName(), new TestServlet(), properties)); + awaitServiceRegistration(); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTO.failedServletContextDTOs.length); + ServletContextDTO defaultContext = assertDefaultContext(runtimeDTO); + assertEquals(1, defaultContext.servletDTOs.length); + assertEquals(0, defaultContext.servletDTOs[0].patterns.length); + assertEquals("servlet", defaultContext.servletDTOs[0].name); + } + + @Test + public void dtosAreIndependentCopies() throws Exception + { + //register first servlet + Dictionary properties = createDictionary( + HTTP_WHITEBOARD_SERVLET_PATTERN, "/test", + HTTP_WHITEBOARD_SERVLET_NAME, "servlet 1", + HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX + "test", "testValue"); + + CountDownLatch initLatch = new CountDownLatch(1); + registrations.add(m_context.registerService(Servlet.class.getName(), new TestServlet(initLatch, null), properties)); + awaitServiceRegistration(initLatch); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTOWithFirstSerlvet = serviceRuntime.getRuntimeDTO(); + + //register second servlet + registerServlet("testServlet 2", "/servlet_2"); + + RuntimeDTO runtimeDTOWithTwoSerlvets = serviceRuntime.getRuntimeDTO(); + + assertNotSame(runtimeDTOWithFirstSerlvet, runtimeDTOWithTwoSerlvets); + + ServletContextDTO defaultContextFirstServlet = assertDefaultContext(runtimeDTOWithFirstSerlvet); + ServletContextDTO defaultContextTwoServlets = assertDefaultContext(runtimeDTOWithTwoSerlvets); + assertNotSame(defaultContextFirstServlet.servletDTOs[0].patterns, + defaultContextTwoServlets.servletDTOs[0].patterns); + + boolean mapsModifiable = true; + try + { + defaultContextTwoServlets.servletDTOs[0].initParams.clear(); + } catch (UnsupportedOperationException e) + { + mapsModifiable = false; + } + + if (mapsModifiable) + { + assertNotSame(defaultContextFirstServlet.servletDTOs[0].initParams, + defaultContextTwoServlets.servletDTOs[0].initParams); + } + } + + @Test + public void requestInfoDTO() throws Exception + { + registerServlet("servlet", "/default"); + registerFilter("filter1", "/default"); + registerFilter("filter2", "/default"); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + ServletContextDTO defaultContext = assertDefaultContext(serviceRuntime.getRuntimeDTO()); + long defaultContextId = defaultContext.serviceId; + + RequestInfoDTO requestInfoDTO = serviceRuntime.calculateRequestInfoDTO("/default"); + assertEquals("/default", requestInfoDTO.path); + assertEquals(defaultContextId, requestInfoDTO.servletContextId); + assertEquals("servlet", requestInfoDTO.servletDTO.name); + assertEquals(2, requestInfoDTO.filterDTOs.length); + assertEquals("filter1", requestInfoDTO.filterDTOs[0].name); + assertEquals("filter2", requestInfoDTO.filterDTOs[1].name); + } + + @Test + public void serviceEndpointPropertyIsSet() + { + // if there is more than one network interface, there might be more than one endpoint! + final String[] endpoint = (String[]) m_context.getServiceReference(HttpServiceRuntime.class).getProperty(HTTP_SERVICE_ENDPOINT); + assertNotNull(endpoint); + assertTrue(Arrays.toString(endpoint), endpoint.length > 0); + assertTrue(endpoint[0].startsWith("http://")); + assertTrue(endpoint[0].endsWith(":8080/")); + } + + /** + * Test for FELIX-5319 + * @throws Exception + */ + @Test + public void testCombinedServletAndResourceRegistration() throws Exception + { + // register single component as Servlet and Resource + final String servletPath = "/hello/sayHello"; + final String servletName = "Hello World"; + final String rsrcPattern = "/hello/static/*"; + final String rsrcPrefix = "/static"; + + CountDownLatch initLatch = new CountDownLatch(1); + List propertyEntries = Arrays.asList( + HTTP_WHITEBOARD_SERVLET_PATTERN, servletPath, + HTTP_WHITEBOARD_SERVLET_NAME, servletName, + HTTP_WHITEBOARD_RESOURCE_PATTERN, rsrcPattern, + HTTP_WHITEBOARD_RESOURCE_PREFIX, rsrcPrefix); + + Dictionary properties = createDictionary(propertyEntries.toArray()); + + registrations.add(m_context.registerService(Servlet.class.getName(), new TestServlet(initLatch, null), properties)); + awaitServiceRegistration(initLatch); + + HttpServiceRuntime serviceRuntime = (HttpServiceRuntime) getService(HttpServiceRuntime.class.getName()); + assertNotNull("HttpServiceRuntime unavailable", serviceRuntime); + + RuntimeDTO runtimeDTO = serviceRuntime.getRuntimeDTO(); + + assertEquals(0, runtimeDTO.failedServletDTOs.length); + assertEquals(0, runtimeDTO.failedResourceDTOs.length); + + // check servlet registration + ServletContextDTO contextDTO = assertDefaultContext(runtimeDTO); + assertEquals(1, contextDTO.servletDTOs.length); + assertEquals(servletName, contextDTO.servletDTOs[0].name); + assertEquals(1, contextDTO.servletDTOs[0].patterns.length); + assertEquals(servletPath, contextDTO.servletDTOs[0].patterns[0]); + + // check resource registration + assertEquals(1, contextDTO.resourceDTOs.length); + assertEquals(1, contextDTO.resourceDTOs[0].patterns.length); + assertEquals(rsrcPattern, contextDTO.resourceDTOs[0].patterns[0]); + assertEquals(rsrcPrefix, contextDTO.resourceDTOs[0].prefix); + } + + private ServletContextDTO assertDefaultContext(RuntimeDTO runtimeDTO) + { + assertTrue(1 < runtimeDTO.servletContextDTOs.length); + assertEquals(HTTP_CONTEXT_NAME, runtimeDTO.servletContextDTOs[0].name); + assertEquals("default", runtimeDTO.servletContextDTOs[1].name); + return runtimeDTO.servletContextDTOs[1]; + } + + private void awaitServiceRegistration() throws InterruptedException + { + // Wait some time until the whiteboard (hopefully) picked up the service + Thread.sleep(DEFAULT_SLEEP); + } + + private void awaitServiceRegistration(CountDownLatch initLatch) throws InterruptedException + { + if (!initLatch.await(5, TimeUnit.SECONDS)) + { + fail("Service was not initialized in time!"); + }; + awaitServiceRegistration(); + } + + public static class TestResource + { + // Tagging class + } +} diff --git a/http/itest/src/test/java/org/apache/felix/http/itest/HttpServiceTest.java b/http/itest/src/test/java/org/apache/felix/http/itest/HttpServiceTest.java new file mode 100644 index 00000000000..e91395c3539 --- /dev/null +++ b/http/itest/src/test/java/org/apache/felix/http/itest/HttpServiceTest.java @@ -0,0 +1,192 @@ +/* + * 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.http.itest; + +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.naming.directory.SearchControls; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerMethod; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.service.http.HttpService; +import org.osgi.service.http.runtime.HttpServiceRuntime; + +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerMethod.class) +public class HttpServiceTest extends BaseIntegrationTest +{ + + private List> registrations = new ArrayList>(); + + private CountDownLatch initLatch; + private CountDownLatch destroyLatch; + + public void setupLatches(int count) + { + initLatch = new CountDownLatch(count); + destroyLatch = new CountDownLatch(count); + } + + public void setupOldWhiteboardFilter(final String pattern) throws Exception + { + Dictionary servletProps = new Hashtable(); + servletProps.put("pattern", pattern); + + final Filter f = new Filter() + { + + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + initLatch.countDown(); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException + { + response.getWriter().print("FILTER-"); + response.flushBuffer(); + chain.doFilter(request, response); + } + + @Override + public void destroy() + { + destroyLatch.countDown(); + } + }; + + registrations.add(m_context.registerService(Filter.class.getName(), f, servletProps)); + } + + @After + public void unregisterServices() throws InterruptedException + { + for (ServiceRegistration serviceRegistration : registrations) + { + serviceRegistration.unregister(); + } + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + + Thread.sleep(500); + } + + @Test + public void testServletAndOldWhiteboardFilter() throws Exception + { + final HttpService service = this.getHttpService(); + service.registerServlet("/tesths", new TestServlet() + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException + { + resp.getWriter().print("helloworld"); + resp.flushBuffer(); + } + }, null, null); + + this.setupLatches(1); + this.setupOldWhiteboardFilter(".*"); + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + final HttpServiceRuntime rt = this.getService(HttpServiceRuntime.class.getName()); + System.out.println(rt.getRuntimeDTO()); + try + { + assertContent("FILTER-helloworld", createURL("/tesths")); + } + finally + { + service.unregister("/tesths"); + } + } + + /** + * Tests the starting of Jetty. + */ + @Test + public void testHttpServiceCapabiltiy() throws Exception + { + setupLatches(0); + + Bundle httpJettyBundle = getHttpJettyBundle(); + + BundleWiring wiring = httpJettyBundle.adapt(BundleWiring.class); + + List capabilities = wiring.getCapabilities("osgi.service"); + + assertFalse(capabilities.isEmpty()); + + boolean found = false; + + for (BundleCapability capability : capabilities) { + @SuppressWarnings("unchecked") + List objectClass = (List) capability.getAttributes().get(Constants.OBJECTCLASS); + + assertNotNull(objectClass); + + if(objectClass.contains(HttpService.class.getName())) { + String uses = capability.getDirectives().get("uses"); + + assertNotNull(uses); + + assertTrue(uses.contains(HttpService.class.getPackage().getName())); + + found = true; + break; + } + } + + assertTrue("Missing HttpService capability", found); + } + +} diff --git a/http/itest/src/test/java/org/apache/felix/http/itest/HttpWhiteboardTargetTest.java b/http/itest/src/test/java/org/apache/felix/http/itest/HttpWhiteboardTargetTest.java new file mode 100644 index 00000000000..a84f98eeeca --- /dev/null +++ b/http/itest/src/test/java/org/apache/felix/http/itest/HttpWhiteboardTargetTest.java @@ -0,0 +1,392 @@ +/* + * 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.http.itest; + +import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.net.URL; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerMethod; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; + +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerMethod.class) +public class HttpWhiteboardTargetTest extends BaseIntegrationTest +{ + + private static final String SERVICE_HTTP_PORT = "org.osgi.service.http.port"; + + /**] + * Test that a servlet with the org.osgi.http.whiteboard.target property not set + * is registered with the whiteboard + */ + @Test + public void testServletNoTargetProperty() throws Exception + { + CountDownLatch initLatch = new CountDownLatch(1); + CountDownLatch destroyLatch = new CountDownLatch(1); + + TestServlet servlet = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException + { + resp.getWriter().print("It works!"); + resp.flushBuffer(); + } + }; + + Dictionary props = new Hashtable(); + props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, "/servletAlias"); + props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_INIT_PARAM_PREFIX + ".myname", "servletName"); + + ServiceRegistration reg = m_context.registerService(Servlet.class.getName(), servlet, props); + + try { + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + URL testURL = createURL("/servletAlias"); + assertContent("It works!", testURL); + } finally { + reg.unregister(); + } + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + } + + /** + * Test that a servlet with the org.osgi.http.whiteboard.target property matching the + * HttpServiceRuntime properties is registered with the whiteboard. + * + * In the current implementation the HttpServiceRuntime properties are the same as the + * HttpService properties. + * + */ + @Test + public void testServletTargetMatchPort() throws Exception + { + CountDownLatch initLatch = new CountDownLatch(1); + CountDownLatch destroyLatch = new CountDownLatch(1); + + TestServlet servlet = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException + { + resp.getWriter().print("matchingServlet"); + resp.flushBuffer(); + } + }; + + Dictionary props = new Hashtable(); + props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, "/servletAlias"); + props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_INIT_PARAM_PREFIX + ".myname", "servletName"); + props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_TARGET, "(" + SERVICE_HTTP_PORT + "=8080" + ")"); + + ServiceRegistration reg = m_context.registerService(Servlet.class.getName(), servlet, props); + + try { + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + URL testURL = createURL("/servletAlias"); + assertContent("matchingServlet", testURL); + } finally { + reg.unregister(); + } + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + } + + /** + * Test that a servlet with the org.osgi.http.whiteboard.target property not matching + * the properties of the HttpServiceRuntime is not registered with the whiteboard. + * + */ + @Test + public void testServletTargetNotMatchPort() throws Exception + { + CountDownLatch initLatch = new CountDownLatch(1); + CountDownLatch destroyLatch = new CountDownLatch(1); + + TestServlet nonMatchingServlet = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException + { + resp.getWriter().print("nonMatchingServlet"); + resp.flushBuffer(); + } + }; + + Dictionary props = new Hashtable(); + props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, "/servletAlias"); + props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_INIT_PARAM_PREFIX + ".myname", "servletName"); + props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_TARGET, "(" + SERVICE_HTTP_PORT + "=8282" + ")"); + + ServiceRegistration reg = m_context.registerService(Servlet.class.getName(), nonMatchingServlet, props); + + try { + // the servlet will not be registered, its init method will not be called, await must return false due to timeout + assertFalse(initLatch.await(5, TimeUnit.SECONDS)); + URL testURL = createURL("/servletAlias"); + assertResponseCode(404, testURL); + } finally { + reg.unregister(); + } + } + + /** + * Test that a filter with no target property set is correctly registered with the whiteboard + * + */ + @Test + public void testFilterNoTargetProperty() throws Exception + { + CountDownLatch initLatch = new CountDownLatch(3); + CountDownLatch destroyLatch = new CountDownLatch(3); + + TestServlet servlet1 = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException + { + resp.getWriter().print("servlet1"); + resp.flushBuffer(); + } + }; + Dictionary props1 = new Hashtable(); + props1.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, "/servlet/1"); + props1.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_INIT_PARAM_PREFIX + ".myname", "servlet1"); + props1.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_TARGET, "(" + SERVICE_HTTP_PORT + "=8080" + ")"); + + TestServlet servlet2 = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException + { + resp.getWriter().print("servlet2"); + resp.flushBuffer(); + } + }; + Dictionary props2 = new Hashtable(); + props2.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, "/servlet/2"); + props2.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_INIT_PARAM_PREFIX + ".myname", "servle2"); + props2.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_TARGET, "(" + SERVICE_HTTP_PORT + "=8080" + ")"); + + TestFilter filter = new TestFilter(initLatch, destroyLatch) + { + @Override + protected void filter(HttpServletRequest req, HttpServletResponse resp, FilterChain chain) throws IOException, ServletException + { + String param = req.getParameter("param"); + if("forbidden".equals(param)) + { + resp.reset(); + resp.sendError(SC_FORBIDDEN); + resp.flushBuffer(); + } + else + { + chain.doFilter(req, resp); + } + } + }; + + Dictionary props = new Hashtable(); + props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_PATTERN, "/servlet/1"); + props.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_INIT_PARAM_PREFIX + ".myname", "servletName"); + + ServiceRegistration reg1 = m_context.registerService(Servlet.class.getName(), servlet1, props1); + ServiceRegistration reg2 = m_context.registerService(Servlet.class.getName(), servlet2, props2); + ServiceRegistration reg = m_context.registerService(Filter.class.getName(), filter, props); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertResponseCode(SC_FORBIDDEN, createURL("/servlet/1?param=forbidden")); + assertContent("servlet1", createURL("/servlet/1?param=any")); + assertContent("servlet1", createURL("/servlet/1")); + + assertResponseCode(SC_OK, createURL("/servlet/2?param=forbidden")); + assertContent("servlet2", createURL("/servlet/2?param=forbidden")); + + reg1.unregister(); + reg2.unregister(); + reg.unregister(); + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testFilterTargetMatchPort() throws Exception + { + CountDownLatch initLatch = new CountDownLatch(2); + CountDownLatch destroyLatch = new CountDownLatch(2); + + TestServlet servlet = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException + { + resp.getWriter().print("servlet"); + resp.flushBuffer(); + } + }; + Dictionary sprops = new Hashtable(); + sprops.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, "/servlet"); + sprops.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_INIT_PARAM_PREFIX + ".myname", "servlet1"); + sprops.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_TARGET, "(" + SERVICE_HTTP_PORT + "=8080" + ")"); + + TestFilter filter = new TestFilter(initLatch, destroyLatch) + { + @Override + protected void filter(HttpServletRequest req, HttpServletResponse resp, FilterChain chain) throws IOException, ServletException + { + String param = req.getParameter("param"); + if("forbidden".equals(param)) + { + resp.reset(); + resp.sendError(SC_FORBIDDEN); + resp.flushBuffer(); + } + else + { + chain.doFilter(req, resp); + } + } + }; + + Dictionary fprops = new Hashtable(); + fprops.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_PATTERN, "/servlet"); + fprops.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_INIT_PARAM_PREFIX + ".myname", "servletName"); + fprops.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_TARGET, "(" + SERVICE_HTTP_PORT + "=8080" + ")"); + + ServiceRegistration sreg = m_context.registerService(Servlet.class.getName(), servlet, sprops); + ServiceRegistration freg = m_context.registerService(Filter.class.getName(), filter, fprops); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertResponseCode(SC_FORBIDDEN, createURL("/servlet?param=forbidden")); + assertContent("servlet", createURL("/servlet?param=any")); + assertContent("servlet", createURL("/servlet")); + + sreg.unregister(); + freg.unregister(); + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testFilterTargetNotMatchPort() throws Exception + { + CountDownLatch servletInitLatch = new CountDownLatch(1); + CountDownLatch servletDestroyLatch = new CountDownLatch(1); + + CountDownLatch filterInitLatch = new CountDownLatch(1); + CountDownLatch filterDestroyLatch = new CountDownLatch(1); + + TestServlet servlet = new TestServlet(servletInitLatch, servletDestroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException + { + resp.getWriter().print("servlet"); + resp.flushBuffer(); + } + }; + Dictionary sprops = new Hashtable(); + sprops.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, "/servlet"); + sprops.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_INIT_PARAM_PREFIX + ".myname", "servlet1"); + sprops.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_TARGET, "(" + SERVICE_HTTP_PORT + "=8080" + ")"); + + TestFilter filter = new TestFilter(filterInitLatch, filterDestroyLatch) + { + @Override + protected void filter(HttpServletRequest req, HttpServletResponse resp, FilterChain chain) throws IOException, ServletException + { + String param = req.getParameter("param"); + if("forbidden".equals(param)) + { + resp.reset(); + resp.sendError(SC_FORBIDDEN); + resp.flushBuffer(); + } + else + { + chain.doFilter(req, resp); + } + } + }; + + Dictionary fprops = new Hashtable(); + fprops.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_PATTERN, "/servlet"); + fprops.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_INIT_PARAM_PREFIX + ".myname", "servletName"); + fprops.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_TARGET, "(" + SERVICE_HTTP_PORT + "=8181" + ")"); + + ServiceRegistration sreg = m_context.registerService(Servlet.class.getName(), servlet, sprops); + ServiceRegistration freg = m_context.registerService(Filter.class.getName(), filter, fprops); + + // servlet is registered + assertTrue(servletInitLatch.await(5, TimeUnit.SECONDS)); + // fitler is not registered, timeout occurs + assertFalse(filterInitLatch.await(5, TimeUnit.SECONDS)); + + assertResponseCode(SC_OK, createURL("/servlet?param=forbidden")); + assertContent("servlet", createURL("/servlet?param=forbidden")); + assertContent("servlet", createURL("/servlet?param=any")); + assertContent("servlet", createURL("/servlet")); + + sreg.unregister(); + freg.unregister(); + + assertTrue(servletDestroyLatch.await(5, TimeUnit.SECONDS)); + assertFalse(filterDestroyLatch.await(5, TimeUnit.SECONDS)); + } +} diff --git a/http/itest/src/test/java/org/apache/felix/http/itest/RequestDispatchTest.java b/http/itest/src/test/java/org/apache/felix/http/itest/RequestDispatchTest.java new file mode 100644 index 00000000000..543dbc0a8aa --- /dev/null +++ b/http/itest/src/test/java/org/apache/felix/http/itest/RequestDispatchTest.java @@ -0,0 +1,516 @@ +/* + * 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.http.itest; + +import static javax.servlet.RequestDispatcher.FORWARD_CONTEXT_PATH; +import static javax.servlet.RequestDispatcher.FORWARD_PATH_INFO; +import static javax.servlet.RequestDispatcher.FORWARD_QUERY_STRING; +import static javax.servlet.RequestDispatcher.FORWARD_REQUEST_URI; +import static javax.servlet.RequestDispatcher.FORWARD_SERVLET_PATH; +import static javax.servlet.RequestDispatcher.INCLUDE_CONTEXT_PATH; +import static javax.servlet.RequestDispatcher.INCLUDE_PATH_INFO; +import static javax.servlet.RequestDispatcher.INCLUDE_QUERY_STRING; +import static javax.servlet.RequestDispatcher.INCLUDE_REQUEST_URI; +import static javax.servlet.RequestDispatcher.INCLUDE_SERVLET_PATH; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerMethod; +import org.osgi.service.http.NamespaceException; + +/** + * @author Felix Project Team + */ +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerMethod.class) +public class RequestDispatchTest extends BaseIntegrationTest +{ + /** + * Tests that we can forward content from other servlets using the {@link RequestDispatcher} service. + */ + @Test + public void testDispatchForwardToAbsoluteURIOk() throws Exception + { + CountDownLatch initLatch = new CountDownLatch(2); + CountDownLatch destroyLatch = new CountDownLatch(2); + + TestServlet forward = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + Object includeContextPath = req.getAttribute(FORWARD_CONTEXT_PATH); + if (includeContextPath != null) + { + assertEquals("", req.getContextPath()); + assertEquals("/forward", req.getServletPath()); + assertEquals(null, req.getPathInfo()); + assertEquals("/forward", req.getRequestURI()); + assertEquals("bar=qux&quu", req.getQueryString()); + + assertEquals("", includeContextPath); + assertEquals("/test", req.getAttribute(FORWARD_SERVLET_PATH)); + assertEquals("/foo", req.getAttribute(FORWARD_PATH_INFO)); + assertEquals("/test/foo", req.getAttribute(FORWARD_REQUEST_URI)); + assertEquals("bar=qux&quu", req.getAttribute(FORWARD_QUERY_STRING)); + } + else + { + assertEquals("", req.getContextPath()); + assertEquals("/forward", req.getServletPath()); + assertEquals("/bar", req.getPathInfo()); + assertEquals("/forward/bar", req.getRequestURI()); + assertEquals("quu=qux", req.getQueryString()); + } + + resp.getWriter().print("FORWARD\n"); + } + }; + + TestServlet servlet = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + assertEquals("", req.getContextPath()); + assertEquals("/test", req.getServletPath()); + assertEquals("/foo", req.getPathInfo()); + assertEquals("/test/foo", req.getRequestURI()); + assertEquals("bar=qux&quu", req.getQueryString()); + + resp.getWriter().print("NOT_SEND\n"); + req.getRequestDispatcher("/forward").forward(req, resp); + resp.getWriter().print("NOT_SEND\n"); + } + }; + + register("/forward", forward); + register("/test", servlet); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertContent("FORWARD\n", createURL("/test/foo?bar=qux&quu")); + assertContent("FORWARD\n", createURL("/forward/bar?quu=qux")); + + unregister("/forward"); + unregister("/test"); + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + } + + /** + * Tests that we can forward content from other servlets using the {@link RequestDispatcher} service. + */ + @Test + public void testDispatchForwardToRelativeURIOk() throws Exception + { + CountDownLatch initLatch = new CountDownLatch(1); + CountDownLatch destroyLatch = new CountDownLatch(1); + + TestServlet servlet = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + Object contextPathAttr = req.getAttribute(FORWARD_CONTEXT_PATH); + if (contextPathAttr != null) + { + assertEquals("", req.getContextPath()); + assertEquals("/test", req.getServletPath()); + assertEquals("/forward", req.getPathInfo()); + assertEquals("/test/forward", req.getRequestURI()); + assertEquals("bar=qux&quu", req.getQueryString()); + + assertEquals("", contextPathAttr); + assertEquals("/test", req.getAttribute(FORWARD_SERVLET_PATH)); + assertEquals("/foo", req.getAttribute(FORWARD_PATH_INFO)); + assertEquals("/test/foo", req.getAttribute(FORWARD_REQUEST_URI)); + assertEquals("bar=qux&quu", req.getAttribute(FORWARD_QUERY_STRING)); + + resp.getWriter().print("FORWARD\n"); + } + else + { + assertEquals("", req.getContextPath()); + assertEquals("/test", req.getServletPath()); + assertEquals("/foo", req.getPathInfo()); + assertEquals("/test/foo", req.getRequestURI()); + assertEquals("bar=qux&quu", req.getQueryString()); + + resp.getWriter().print("NOT_SEND\n"); + + // ServletContext#getRequestDispatcher only takes absolute paths... + RequestDispatcher disp = req.getServletContext().getRequestDispatcher("forward"); + assertNull("ServletContext returned RequestDispatcher for relative path?!", disp); + // Causes a request to ourselves being made (/test/forward)... + disp = req.getRequestDispatcher("forward"); + assertNotNull("ServletRequest returned NO RequestDispatcher for relative path?!", disp); + + disp.forward(req, resp); + resp.getWriter().print("NOT_SEND\n"); + } + } + }; + + register("/test", servlet); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertContent("FORWARD\n", createURL("/test/foo?bar=qux&quu")); + + unregister("/test"); + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + } + + /** + * Tests that we can include content from other servlets using the {@link RequestDispatcher} service. + */ + @Test + public void testDispatchIncludeAbsoluteURIOk() throws Exception + { + CountDownLatch initLatch = new CountDownLatch(2); + CountDownLatch destroyLatch = new CountDownLatch(2); + + TestServlet include = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + Object includeContextPath = req.getAttribute(INCLUDE_CONTEXT_PATH); + if (includeContextPath != null) + { + assertEquals("", req.getContextPath()); + assertEquals("/test", req.getServletPath()); + assertEquals("/foo", req.getPathInfo()); + assertEquals("/test/foo", req.getRequestURI()); + assertEquals("bar=qux&quu", req.getQueryString()); + + assertEquals("", includeContextPath); + assertEquals("/include", req.getAttribute(INCLUDE_SERVLET_PATH)); + assertEquals(null, req.getAttribute(INCLUDE_PATH_INFO)); + assertEquals("/include", req.getAttribute(INCLUDE_REQUEST_URI)); + assertEquals(null, req.getAttribute(INCLUDE_QUERY_STRING)); + } + else + { + assertEquals("", req.getContextPath()); + assertEquals("/include", req.getServletPath()); + assertEquals("/bar", req.getPathInfo()); + assertEquals("/include/bar", req.getRequestURI()); + assertEquals("quu=qux", req.getQueryString()); + } + + resp.getWriter().print("INCLUDE\n"); + } + }; + + TestServlet servlet = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + assertEquals("", req.getContextPath()); + assertEquals("/test", req.getServletPath()); + assertEquals("/foo", req.getPathInfo()); + assertEquals("/test/foo", req.getRequestURI()); + assertEquals("bar=qux&quu", req.getQueryString()); + + resp.getWriter().print("BEFORE\n"); + req.getRequestDispatcher("/include").include(req, resp); + resp.getWriter().print("AFTER\n"); + } + }; + + register("/include", include); + register("/test", servlet); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertContent("BEFORE\nINCLUDE\nAFTER\n", createURL("/test/foo?bar=qux&quu")); + assertContent("INCLUDE\n", createURL("/include/bar?quu=qux")); + + unregister("/include"); + unregister("/test"); + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + } + + /** + * Tests that we can include content from other servlets using the {@link RequestDispatcher} service. + */ + @Test + public void testDispatchIncludeRelativeURIOk() throws Exception + { + CountDownLatch initLatch = new CountDownLatch(1); + CountDownLatch destroyLatch = new CountDownLatch(1); + + TestServlet servlet = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + Object contextPathAttr = req.getAttribute(INCLUDE_CONTEXT_PATH); + if (contextPathAttr != null) + { + assertEquals("", req.getContextPath()); + assertEquals("/foo", req.getPathInfo()); + assertEquals("/test", req.getServletPath()); + assertEquals("/test/foo", req.getRequestURI()); + assertEquals("bar=qux&quu", req.getQueryString()); + + assertEquals("", contextPathAttr); + assertEquals("/test", req.getAttribute(INCLUDE_SERVLET_PATH)); + // assertEquals("/include", req.getAttribute(INCLUDE_PATH_INFO)); + // assertEquals("/test/include", req.getAttribute(INCLUDE_REQUEST_URI)); + assertEquals(null, req.getAttribute(INCLUDE_QUERY_STRING)); + + resp.getWriter().print("INCLUDE\n"); + } + else + { + assertEquals("", req.getContextPath()); + assertEquals("/test", req.getServletPath()); + // assertEquals("/foo", req.getPathInfo()); + // assertEquals("/test/foo", req.getRequestURI()); + assertEquals("bar=qux&quu", req.getQueryString()); + + resp.getWriter().print("BEFORE\n"); + + // ServletContext#getRequestDispatcher only takes absolute paths... + RequestDispatcher disp = req.getServletContext().getRequestDispatcher("include"); + assertNull("ServletContext returned RequestDispatcher for relative path?!", disp); + // Causes a request to ourselves being made (/test/forward)... + disp = req.getRequestDispatcher("include"); + assertNotNull("ServletRequest returned NO RequestDispatcher for relative path?!", disp); + + disp.include(req, resp); + resp.getWriter().print("AFTER\n"); + } + } + }; + + register("/test", servlet); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertContent("BEFORE\nINCLUDE\nAFTER\n", createURL("/test/foo?bar=qux&quu")); + + unregister("/test"); + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + } + + /** + * Tests that we can forward content from other servlets using the {@link RequestDispatcher} service. + */ + @Test + public void testDispatchOnNonRootContextPathOk() throws Exception + { + // Configure HTTP on a different context path... + configureHttpService(createDictionary("org.apache.felix.http.context_path", "/context", "org.osgi.service.http.port", "8080")); + + try + { + // Include two tests in one as to keep tests a little easier to read... + doTestForwardAbsoluteURI(); + doTestIncludeAbsoluteURI(); + } + finally + { + configureHttpService(null); + } + } + + private void doTestForwardAbsoluteURI() throws ServletException, NamespaceException, InterruptedException, IOException + { + CountDownLatch initLatch = new CountDownLatch(2); + CountDownLatch destroyLatch = new CountDownLatch(2); + + TestServlet forward = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + Object includeContextPath = req.getAttribute(FORWARD_CONTEXT_PATH); + if (includeContextPath != null) + { + assertEquals("/context", req.getContextPath()); + assertEquals("/forward", req.getServletPath()); + assertEquals(null, req.getPathInfo()); + assertEquals("/context/forward", req.getRequestURI()); + assertEquals("bar=qux&quu", req.getQueryString()); + + assertEquals("/context", includeContextPath); + assertEquals("/test", req.getAttribute(FORWARD_SERVLET_PATH)); + assertEquals("/foo", req.getAttribute(FORWARD_PATH_INFO)); + assertEquals("/context/test/foo", req.getAttribute(FORWARD_REQUEST_URI)); + assertEquals("bar=qux&quu", req.getAttribute(FORWARD_QUERY_STRING)); + } + else + { + assertEquals("/context", req.getContextPath()); + assertEquals("/forward", req.getServletPath()); + assertEquals("/bar", req.getPathInfo()); + assertEquals("/context/forward/bar", req.getRequestURI()); + assertEquals("quu=qux", req.getQueryString()); + } + + resp.getWriter().print("FORWARD\n"); + } + }; + + TestServlet servlet = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + assertEquals("/context", req.getContextPath()); + assertEquals("/test", req.getServletPath()); + assertEquals("/foo", req.getPathInfo()); + assertEquals("/context/test/foo", req.getRequestURI()); + assertEquals("bar=qux&quu", req.getQueryString()); + + resp.getWriter().print("NOT_SEND\n"); + req.getRequestDispatcher("/forward").forward(req, resp); + resp.getWriter().print("NOT_SEND\n"); + } + }; + + register("/forward", forward); + register("/test", servlet); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertContent("FORWARD\n", createURL("/context/test/foo?bar=qux&quu")); + assertContent("FORWARD\n", createURL("/context/forward/bar?quu=qux")); + + unregister("/forward"); + unregister("/test"); + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + } + + /** + * Tests that we can include content from other servlets using the {@link RequestDispatcher} service. + */ + private void doTestIncludeAbsoluteURI() throws Exception + { + CountDownLatch initLatch = new CountDownLatch(2); + CountDownLatch destroyLatch = new CountDownLatch(2); + + TestServlet include = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + Object includeContextPath = req.getAttribute(INCLUDE_CONTEXT_PATH); + if (includeContextPath != null) + { + assertEquals("/context", req.getContextPath()); + assertEquals("/test", req.getServletPath()); + assertEquals("/foo", req.getPathInfo()); + assertEquals("/context/test/foo", req.getRequestURI()); + assertEquals("bar=qux&quu", req.getQueryString()); + + assertEquals("/context", includeContextPath); + assertEquals("/include", req.getAttribute(INCLUDE_SERVLET_PATH)); + assertEquals(null, req.getAttribute(INCLUDE_PATH_INFO)); + assertEquals("/context/include", req.getAttribute(INCLUDE_REQUEST_URI)); + assertEquals(null, req.getAttribute(INCLUDE_QUERY_STRING)); + } + else + { + assertEquals("/context", req.getContextPath()); + assertEquals("/include", req.getServletPath()); + assertEquals("/bar", req.getPathInfo()); + assertEquals("/context/include/bar", req.getRequestURI()); + assertEquals("quu=qux", req.getQueryString()); + } + + resp.getWriter().print("INCLUDE\n"); + } + }; + + TestServlet servlet = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + assertEquals("/context", req.getContextPath()); + assertEquals("/test", req.getServletPath()); + assertEquals("/foo", req.getPathInfo()); + assertEquals("/context/test/foo", req.getRequestURI()); + assertEquals("bar=qux&quu", req.getQueryString()); + + resp.getWriter().print("BEFORE\n"); + req.getRequestDispatcher("/include").include(req, resp); + resp.getWriter().print("AFTER\n"); + } + }; + + register("/include", include); + register("/test", servlet); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertContent("BEFORE\nINCLUDE\nAFTER\n", createURL("/context/test/foo?bar=qux&quu")); + assertContent("INCLUDE\n", createURL("/context/include/bar?quu=qux")); + + unregister("/include"); + unregister("/test"); + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + } +} diff --git a/http/itest/src/test/java/org/apache/felix/http/itest/ResourceTest.java b/http/itest/src/test/java/org/apache/felix/http/itest/ResourceTest.java new file mode 100644 index 00000000000..0f39254b486 --- /dev/null +++ b/http/itest/src/test/java/org/apache/felix/http/itest/ResourceTest.java @@ -0,0 +1,115 @@ +/* + * 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.http.itest; + +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerMethod; +import org.osgi.service.http.HttpContext; + +/** + * @author Felix Project Team + */ +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerMethod.class) +public class ResourceTest extends BaseIntegrationTest +{ + + @Test + public void testHandleResourceRegistrationOk() throws Exception + { + CountDownLatch initLatch = new CountDownLatch(1); + CountDownLatch destroyLatch = new CountDownLatch(1); + + HttpContext context = new HttpContext() + { + @Override + public boolean handleSecurity(HttpServletRequest request, HttpServletResponse response) throws IOException + { + return true; + } + + @Override + public URL getResource(String name) + { + try + { + File f = new File("src/test/resources/" + name); + if (f.exists()) + { + return f.toURI().toURL(); + } + } + catch (MalformedURLException e) + { + fail(); + } + return null; + } + + @Override + public String getMimeType(String name) + { + return null; + } + }; + + TestServlet servlet = new TestServlet(initLatch, destroyLatch); + + register("/", "/resource", context); + register("/test", servlet, context); + + URL testHtmlURL = createURL("/test.html"); + URL testURL = createURL("/test"); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertResponseCode(SC_OK, testHtmlURL); + assertResponseCode(SC_OK, testURL); + + unregister("/test"); + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + + assertResponseCode(SC_OK, testHtmlURL); + assertResponseCode(SC_OK, testURL); + + unregister("/"); + + assertResponseCode(SC_NOT_FOUND, testHtmlURL); + assertResponseCode(SC_NOT_FOUND, testURL); + } +} diff --git a/http/itest/src/test/java/org/apache/felix/http/itest/ServletPatternTest.java b/http/itest/src/test/java/org/apache/felix/http/itest/ServletPatternTest.java new file mode 100644 index 00000000000..d3617343ddd --- /dev/null +++ b/http/itest/src/test/java/org/apache/felix/http/itest/ServletPatternTest.java @@ -0,0 +1,274 @@ +/* + * 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.http.itest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.osgi.framework.Constants.SERVICE_RANKING; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_PATH; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_RESOURCE_PATTERN; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_RESOURCE_PREFIX; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_NAME; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.servlet.Servlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.felix.http.itest.HttpServiceRuntimeTest.TestResource; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerMethod; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.http.context.ServletContextHelper; + +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerMethod.class) +public class ServletPatternTest extends BaseIntegrationTest +{ + private List> registrations = new ArrayList<>(); + + private CountDownLatch initLatch; + private CountDownLatch destroyLatch; + + public void setupLatches(int count) + { + initLatch = new CountDownLatch(count); + destroyLatch = new CountDownLatch(count); + } + + public void setupServlet(final String name, String[] path, int rank, String context) throws Exception + { + Dictionary servletProps = new Hashtable<>(); + servletProps.put(HTTP_WHITEBOARD_SERVLET_NAME, name); + servletProps.put(HTTP_WHITEBOARD_SERVLET_PATTERN, path); + servletProps.put(SERVICE_RANKING, rank); + if (context != null) + { + servletProps.put(HTTP_WHITEBOARD_CONTEXT_SELECT, "(" + HTTP_WHITEBOARD_CONTEXT_NAME + "=" + context + ")"); + } + + TestServlet servletWithErrorCode = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException + { + resp.getWriter().print(name); + resp.flushBuffer(); + } + }; + + registrations.add(m_context.registerService(Servlet.class.getName(), servletWithErrorCode, servletProps)); + } + + private void setupContext(String name, String path) throws InterruptedException + { + Dictionary properties = createDictionary( + HTTP_WHITEBOARD_CONTEXT_NAME, name, + HTTP_WHITEBOARD_CONTEXT_PATH, path); + + ServletContextHelper servletContextHelper = new ServletContextHelper(m_context.getBundle()){ + // test helper + }; + registrations.add(m_context.registerService(ServletContextHelper.class.getName(), servletContextHelper, properties)); + + Thread.sleep(500); + } + + @After + public void unregisterServices() throws InterruptedException + { + for (ServiceRegistration serviceRegistration : registrations) + { + serviceRegistration.unregister(); + } + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + + Thread.sleep(500); + } + + @Test + public void testHighRankReplaces() throws Exception + { + setupLatches(2); + + setupServlet("lowRankServlet", new String[] { "/foo", "/bar" }, 1, null); + setupServlet("highRankServlet", new String[] { "/foo", "/baz" }, 2, null); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertContent("highRankServlet", createURL("/foo")); + assertContent("lowRankServlet", createURL("/bar")); + assertContent("highRankServlet", createURL("/baz")); + } + + @Test + public void testHttpServiceReplaces() throws Exception + { + setupLatches(2); + + setupContext("contextA", "/test"); + setupServlet("whiteboardServlet", new String[]{ "/foo", "/bar" }, Integer.MAX_VALUE, "contextA"); + + TestServlet httpServiceServlet = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException + { + resp.getWriter().print("httpServiceServlet"); + resp.flushBuffer(); + } + }; + + register("/test/foo", httpServiceServlet); + + try + { + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertContent("whiteboardServlet", createURL("/test/bar")); + assertContent("whiteboardServlet", createURL("/test/bar")); + } + finally + { + unregister("/test/foo"); + } + } + + @Test + public void testSameRankDoesNotReplace() throws Exception + { + setupLatches(2); + + setupServlet("servlet1", new String[]{ "/foo", "/bar" }, 2, null); + setupServlet("servlet2", new String[]{ "/foo", "/baz" }, 2, null); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + assertContent("servlet1", createURL("/foo")); + assertContent("servlet1", createURL("/bar")); + assertContent("servlet2", createURL("/baz")); + } + + @Test + public void testHighRankResourceReplaces() throws Exception + { + setupLatches(1); + + setupServlet("lowRankServlet", new String[]{ "/foo" }, 1, null); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + assertContent("lowRankServlet", createURL("/foo")); + + Dictionary resourceProps = new Hashtable<>(); + String highRankPattern[] = { "/foo" }; + resourceProps.put(HTTP_WHITEBOARD_RESOURCE_PATTERN, highRankPattern); + resourceProps.put(HTTP_WHITEBOARD_RESOURCE_PREFIX, "/resource/test.html"); + resourceProps.put(SERVICE_RANKING, 2); + + registrations.add(m_context.registerService(TestResource.class.getName(), + new TestResource(), resourceProps)); + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + Thread.sleep(500); + + assertContent(getTestHtmlContent(), createURL("/foo")); + } + + private String getTestHtmlContent() throws IOException + { + InputStream resourceAsStream = this.getClass().getResourceAsStream("/resource/test.html"); + return slurpAsString(resourceAsStream); + } + + @Test + public void contextWithLongerPrefixIsChosen() throws Exception + { + setupLatches(2); + + setupContext("contextA", "/a"); + setupContext("contextB", "/a/b"); + + setupServlet("servlet1", new String[]{ "/b/test" }, 1, "contextA"); + + Thread.sleep(500); + assertEquals(1, initLatch.getCount()); + assertContent("servlet1", createURL("/a/b/test")); + + setupServlet("servlet2", new String[]{ "/test" }, 1, "contextB"); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + assertContent("servlet2", createURL("/a/b/test")); + } + + @Test + public void contextWithLongerPrefixIsChosenWithWildcard() throws Exception + { + setupLatches(2); + + setupContext("contextA", "/a"); + setupContext("contextB", "/a/b"); + + setupServlet("servlet1", new String[]{ "/b/test/servlet" }, 1, "contextA"); + + Thread.sleep(500); + assertEquals(1, initLatch.getCount()); + assertContent("servlet1", createURL("/a/b/test/servlet")); + + setupServlet("servlet2", new String[]{ "/test/*" }, 1, "contextB"); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + assertContent("servlet2", createURL("/a/b/test/servlet")); + } + + @Test + public void pathMatchingTest() throws Exception + { + setupLatches(1); + + setupContext("contextA", "/a"); + + setupServlet("servlet1", new String[]{ "/servlet/*" }, 1, "contextA"); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + assertContent("servlet1", createURL("/a/servlet/foo")); + assertContent("servlet1", createURL("/a/servlet")); + } +} diff --git a/http/itest/src/test/java/org/apache/felix/http/itest/SessionHandlingTest.java b/http/itest/src/test/java/org/apache/felix/http/itest/SessionHandlingTest.java new file mode 100644 index 00000000000..02854e94deb --- /dev/null +++ b/http/itest/src/test/java/org/apache/felix/http/itest/SessionHandlingTest.java @@ -0,0 +1,255 @@ +/* + * 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.http.itest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.osgi.framework.Constants.SERVICE_RANKING; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_PATH; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_NAME; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.json.JsonObject; +import javax.servlet.Servlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.apache.http.HttpEntity; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.BasicCookieStore; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.apache.johnzon.core.JsonProviderImpl; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerMethod; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.http.context.ServletContextHelper; + +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerMethod.class) +public class SessionHandlingTest extends BaseIntegrationTest +{ + private List> registrations = new ArrayList<>(); + + private CountDownLatch initLatch; + private CountDownLatch destroyLatch; + + private void setupLatches(int count) + { + initLatch = new CountDownLatch(count); + destroyLatch = new CountDownLatch(count); + } + + private void setupServlet(final String name, String[] path, int rank, final String context) throws Exception + { + Dictionary servletProps = new Hashtable<>(); + servletProps.put(HTTP_WHITEBOARD_SERVLET_NAME, name); + servletProps.put(HTTP_WHITEBOARD_SERVLET_PATTERN, path); + servletProps.put(SERVICE_RANKING, rank); + if (context != null) + { + servletProps.put(HTTP_WHITEBOARD_CONTEXT_SELECT, "(" + HTTP_WHITEBOARD_CONTEXT_NAME + "=" + context + ")"); + } + + Servlet sessionServlet = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException + { + final boolean create = req.getParameter("create") != null; + if ( create ) + { + req.getSession(); + } + final boolean destroy = req.getParameter("destroy") != null; + if ( destroy ) + { + req.getSession().invalidate(); + } + final HttpSession s = req.getSession(false); + if ( s != null ) + { + s.setAttribute("value", context); + } + + final PrintWriter pw = resp.getWriter(); + pw.println("{"); + if ( s == null ) + { + pw.println(" \"session\" : false"); + } + else + { + pw.println(" \"session\" : true,"); + pw.println(" \"sessionId\" : \"" + s.getId() + "\","); + pw.println(" \"value\" : \"" + s.getAttribute("value") + "\","); + pw.println(" \"hashCode\" : \"" + s.hashCode() + "\""); + } + pw.println("}"); + } + }; + + registrations.add(m_context.registerService(Servlet.class.getName(), sessionServlet, servletProps)); + } + + private void setupContext(String name, String path) throws InterruptedException + { + Dictionary properties = createDictionary( + HTTP_WHITEBOARD_CONTEXT_NAME, name, + HTTP_WHITEBOARD_CONTEXT_PATH, path); + + ServletContextHelper servletContextHelper = new ServletContextHelper(m_context.getBundle()){ + // test helper + }; + registrations.add(m_context.registerService(ServletContextHelper.class.getName(), servletContextHelper, properties)); + + Thread.sleep(500); + } + + @After + public void unregisterServices() throws InterruptedException + { + for (ServiceRegistration serviceRegistration : registrations) + { + serviceRegistration.unregister(); + } + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + + Thread.sleep(500); + } + + private JsonObject getJSONResponse(final CloseableHttpClient client, final String path) throws IOException + { + final HttpGet httpGet = new HttpGet(createURL(path).toExternalForm().toString()); + CloseableHttpResponse response1 = client.execute(httpGet); + + try { + HttpEntity entity1 = response1.getEntity(); + final String content = EntityUtils.toString(entity1); + + return new JsonProviderImpl().createReader(new StringReader(content)).readObject(); + } finally { + response1.close(); + } + + } + @Test + public void testSessionAttributes() throws Exception + { + setupContext("test1", "/"); + setupContext("test2", "/"); + + setupLatches(2); + + setupServlet("foo", new String[] { "/foo" }, 1, "test1"); + setupServlet("bar", new String[] { "/bar" }, 2, "test2" ); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + + RequestConfig globalConfig = RequestConfig.custom() + .setCookieSpec(CookieSpecs.BEST_MATCH) + .build(); + final CloseableHttpClient httpclient = HttpClients.custom().setDefaultRequestConfig(globalConfig) + .setDefaultCookieStore(new BasicCookieStore()) + .build(); + + JsonObject json; + + // session should not be available + // check for foo servlet + json = getJSONResponse(httpclient, "/foo"); + assertFalse(json.getBoolean("session")); + + // check for bar servlet + json = getJSONResponse(httpclient, "/bar"); + assertFalse(json.getBoolean("session")); + + // create session for context of servlet foo + // check session and session attribute + json = getJSONResponse(httpclient, "/foo?create=true"); + assertTrue(json.getBoolean("session")); + assertEquals("test1", json.getString("value")); + final String sessionId1 = json.getString("sessionId"); + assertNotNull(sessionId1); + final String hashCode1 = json.getString("hashCode"); + assertNotNull(hashCode1); + + // check session for servlet bar (= no session) + json = getJSONResponse(httpclient, "/bar"); + assertFalse(json.getBoolean("session")); + // another request to servlet foo, still the same + json = getJSONResponse(httpclient, "/foo"); + assertTrue(json.getBoolean("session")); + assertEquals("test1", json.getString("value")); + assertEquals(sessionId1, json.getString("sessionId")); + + // create session for second context + json = getJSONResponse(httpclient, "/bar?create=true"); + assertTrue(json.getBoolean("session")); + assertEquals("test2", json.getString("value")); + final String sessionId2 = json.getString("sessionId"); + assertNotNull(sessionId2); + final String hashCode2 = json.getString("hashCode"); + assertNotNull(hashCode2); + assertFalse(hashCode2.equals(hashCode1)); + + // and context foo is untouched + json = getJSONResponse(httpclient, "/foo"); + assertTrue(json.getBoolean("session")); + assertEquals("test1", json.getString("value")); + assertEquals(sessionId1, json.getString("sessionId")); + + // invalidate session for foo context + json = getJSONResponse(httpclient, "/foo?destroy=true"); + assertFalse(json.getBoolean("session")); + // bar should be untouched + json = getJSONResponse(httpclient, "/bar"); + assertTrue(json.getBoolean("session")); + assertEquals("test2", json.getString("value")); + assertEquals(sessionId2, json.getString("sessionId")); + } +} diff --git a/http/itest/src/test/java/org/apache/felix/http/itest/UploadTest.java b/http/itest/src/test/java/org/apache/felix/http/itest/UploadTest.java new file mode 100644 index 00000000000..301f70b4716 --- /dev/null +++ b/http/itest/src/test/java/org/apache/felix/http/itest/UploadTest.java @@ -0,0 +1,193 @@ +/* + * 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.http.itest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_MULTIPART_ENABLED; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_MULTIPART_MAXFILESIZE; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.Part; + +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.entity.mime.content.StringBody; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerMethod; +import org.osgi.framework.ServiceRegistration; + +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerMethod.class) +public class UploadTest extends BaseIntegrationTest +{ + private static final String PATH = "/post"; + + private List> registrations = new ArrayList>(); + + private CountDownLatch initLatch; + private CountDownLatch destroyLatch; + private CountDownLatch receivedLatch; + + public void setupLatches(int count) + { + initLatch = new CountDownLatch(count); + destroyLatch = new CountDownLatch(count); + receivedLatch = new CountDownLatch(count); + } + + public void setupServlet(final Map contents) throws Exception + { + setupLatches(1); + + Dictionary servletProps = new Hashtable(); + servletProps.put(HTTP_WHITEBOARD_SERVLET_PATTERN, PATH); + servletProps.put(HTTP_WHITEBOARD_SERVLET_MULTIPART_ENABLED, Boolean.TRUE); + servletProps.put(HTTP_WHITEBOARD_SERVLET_MULTIPART_MAXFILESIZE, 1024L); + + TestServlet servletWithErrorCode = new TestServlet(initLatch, destroyLatch) + { + private static final long serialVersionUID = 1L; + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws IOException, ServletException + { + try + { + final Collection parts = req.getParts(); + for(final Part p : parts) + { + contents.put(p.getName(), p.getSize()); + } + resp.setStatus(201); + } + finally + { + receivedLatch.countDown(); + } + + } + }; + + registrations.add(m_context.registerService(Servlet.class.getName(), servletWithErrorCode, servletProps)); + + assertTrue(initLatch.await(5, TimeUnit.SECONDS)); + } + + @After + public void unregisterServices() throws InterruptedException + { + for (ServiceRegistration serviceRegistration : registrations) + { + serviceRegistration.unregister(); + } + + assertTrue(destroyLatch.await(5, TimeUnit.SECONDS)); + } + + private static void postContent(final char c, final long length, final int expectedRT) throws IOException + { + final URL url = createURL(PATH); + final CloseableHttpClient httpclient = HttpClients.createDefault(); + try + { + final HttpPost httppost = new HttpPost(url.toExternalForm()); + + final StringBuilder sb = new StringBuilder(); + for(int i=0;i contents = new HashMap<>(); + setupServlet(contents); + + postContent('a', 500, 201); + assertTrue(receivedLatch.await(5, TimeUnit.SECONDS)); + assertEquals(1, contents.size()); + assertEquals(500L, (long)contents.get("text")); + } + + @Test + public void testMaxFileSize() throws Exception + { + setupLatches(2); + + final Map contents = new HashMap<>(); + setupServlet(contents); + + postContent('b', 2048, 500); + assertTrue(receivedLatch.await(5, TimeUnit.SECONDS)); + assertTrue(contents.isEmpty()); + } +} diff --git a/http/itest/src/test/resources/logback.xml b/http/itest/src/test/resources/logback.xml new file mode 100644 index 00000000000..03dedea3767 --- /dev/null +++ b/http/itest/src/test/resources/logback.xml @@ -0,0 +1,33 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + diff --git a/http/itest/src/test/resources/resource/test.html b/http/itest/src/test/resources/resource/test.html new file mode 100644 index 00000000000..5573d33845a --- /dev/null +++ b/http/itest/src/test/resources/resource/test.html @@ -0,0 +1,19 @@ + +

      TEST

      \ No newline at end of file diff --git a/http/jetty/pom.xml b/http/jetty/pom.xml new file mode 100644 index 00000000000..db18259c35e --- /dev/null +++ b/http/jetty/pom.xml @@ -0,0 +1,235 @@ + + + + 4.0.0 + + org.apache.felix + org.apache.felix.http.parent + 12 + ../parent/pom.xml + + + Apache Felix Http Jetty + This is an implementation of the OSGi Http Service and the OSGi Http Whiteboard Specification + + org.apache.felix.http.jetty + 4.0.9-SNAPSHOT + bundle + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/http/jetty + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/http/jetty + http://svn.apache.org/viewvc/felix/trunk/http/jetty/ + + + + + true + 8 + 9.4.15.v20190215 + + + + + + org.apache.felix + maven-bundle-plugin + true + + + + ${jetty.version} + + + org.apache.felix.http.jetty.internal.JettyActivator + + + org.osgi.service.http, + org.osgi.service.http.context, + org.osgi.service.http.runtime, + org.osgi.service.http.runtime.dto, + org.osgi.service.http.whiteboard, + !org.eclipse.jetty, + !org.eclipse.jetty.util.log.jmx, + !org.eclipse.jetty.version, + org.eclipse.jetty.*, + org.apache.felix.http.jetty + + + org.apache.felix.http.base.*, + org.apache.felix.http.jetty.*, + org.eclipse.jetty, + org.eclipse.jetty.security.authentication, + org.eclipse.jetty.util.log.jmx, + org.eclipse.jetty.version + + + org.apache.commons.* + + + javax.imageio;resolution:=optional, + javax.sql;resolution:=optional, + org.slf4j.*;resolution:=optional, + org.ietf.jgss;resolution:=optional, + org.osgi.service.cm;resolution:=optional;version="[1.3,2)", + org.osgi.service.event;resolution:=optional;version="[1.2,2)", + org.osgi.service.log;resolution:=optional;version="[1.3,2)", + org.osgi.service.metatype;resolution:=optional;version="[1.1,2)", + org.osgi.service.useradmin;resolution:=optional;version="[1.1,2)", + org.osgi.service.http;version="[1.2.1,1.3)", + org.osgi.service.http.context;version="[1.1,1.2)", + org.osgi.service.http.runtime;version="[1.1,1.2)", + org.osgi.service.http.runtime.dto;version="[1.1,1.2)", + * + + + org.osgi.service.cm;version="[1.3,2)", + org.osgi.service.event;version="[1.2,2)", + org.osgi.service.log;version="[1.3,2)", + org.osgi.service.metatype;version="[1.1,2)" + + + osgi.implementation;osgi.implementation="osgi.http";version:Version="1.1"; + uses:="javax.servlet,javax.servlet.http,org.osgi.service.http.context,org.osgi.service.http.whiteboard", + osgi.service;objectClass:List<String>="org.osgi.service.http.runtime.HttpServiceRuntime"; + uses:="org.osgi.service.http.runtime,org.osgi.service.http.runtime.dto", + osgi.service;objectClass:List<String>="org.osgi.service.http.HttpService"; + uses:="org.osgi.service.http" + + + osgi.contract;filter:="(&(osgi.contract=JavaServlet)(version=3.1))" + + + + true + + + + + + + + javax.servlet + javax.servlet-api + + + org.osgi + osgi.core + + + org.osgi + org.osgi.service.cm + 1.5.0 + provided + + + org.osgi + org.osgi.service.event + 1.3.1 + provided + + + org.osgi + org.osgi.service.metatype + 1.3.0 + provided + + + org.osgi + org.osgi.service.useradmin + 1.1.0 + provided + + + org.eclipse.jetty + jetty-servlet + ${jetty.version} + + + org.eclipse.jetty + jetty-server + ${jetty.version} + + + org.eclipse.jetty + jetty-util + ${jetty.version} + + + org.eclipse.jetty + jetty-jmx + ${jetty.version} + + + org.eclipse.jetty + jetty-security + ${jetty.version} + + + org.eclipse.jetty + jetty-webapp + ${jetty.version} + + + org.eclipse.jetty.websocket + websocket-servlet + ${jetty.version} + + + org.eclipse.jetty.websocket + websocket-server + ${jetty.version} + + + org.osgi + org.osgi.service.http + 1.2.1 + provided + + + org.osgi + org.osgi.service.http.whiteboard + 1.1.0 + provided + + + org.apache.felix + org.apache.felix.http.base + 4.0.7-SNAPSHOT + + + commons-fileupload + commons-fileupload + 1.3.3 + + + commons-io + commons-io + 2.6 + + + + org.osgi + org.osgi.service.log + 1.3.0 + test + + + diff --git a/http/jetty/src/main/appended-resources/META-INF/DEPENDENCIES b/http/jetty/src/main/appended-resources/META-INF/DEPENDENCIES new file mode 100644 index 00000000000..33860b759cc --- /dev/null +++ b/http/jetty/src/main/appended-resources/META-INF/DEPENDENCIES @@ -0,0 +1,20 @@ +I. Included Third-Party Software + +This product includes software developed by +Eclipse (http://www.eclipse.org/jetty/) +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2016). +Licensed under the Apache License 2.0. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org). +Copyright (c) OSGi Alliance (2000, 2016). +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 diff --git a/http/jetty/src/main/appended-resources/META-INF/NOTICE b/http/jetty/src/main/appended-resources/META-INF/NOTICE new file mode 100644 index 00000000000..7b2faa7b913 --- /dev/null +++ b/http/jetty/src/main/appended-resources/META-INF/NOTICE @@ -0,0 +1,8 @@ +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2016). +Licensed under the Apache License 2.0. + +This product includes software developed by +Eclipse (http://www.eclipse.org/jetty/) +Licensed under the Apache License 2.0. \ No newline at end of file diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/ConnectorFactory.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/ConnectorFactory.java new file mode 100644 index 00000000000..5de3a848e82 --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/ConnectorFactory.java @@ -0,0 +1,58 @@ +/* + * 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.http.jetty; + +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.osgi.annotation.versioning.ConsumerType; + +/** + * The ConnectorFactory is a service interface which allows + * extensions to inject custom Jetty {@code Connector} instances to add + * to the Jetty server. Example connectors would be a SPDY connector or + * an SSL capable connector with a custom {@code SslContextFactory}. + *

      + * {@code ConnectorFactory} services are responsible for creating the + * {@code Connector} instances and providing base configuration. Global + * configuration such as TCP/IP timeouts or buffer sizes are handled by the + * Jetty server launcher. Likewise the life cycle of the connectors is managed + * by the Jetty server and its launcher. + */ +@ConsumerType +public interface ConnectorFactory +{ + + /** + * Creates new Jetty {@code Connector} instances. + *

      + * The instances must be configured. The Jetty server will additionally + * configure global configuration such as TCP/IP timeouts and buffer + * settings. + *

      + * Connectors returned from this method are not started yet. Callers must + * add them to the Jetty server and start them. + *

      + * If the {@code ConnectorFactory} service is stopped any connectors still + * active in Jetty servers must be stopped and removed from these Jetty + * servers. + * + * @return A configured Jetty {@code Connector} instance. + */ + Connector createConnector(Server server); +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/LoadBalancerCustomizerFactory.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/LoadBalancerCustomizerFactory.java new file mode 100644 index 00000000000..8a7b78d1c44 --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/LoadBalancerCustomizerFactory.java @@ -0,0 +1,47 @@ +/* + * 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.http.jetty; + +import org.eclipse.jetty.server.HttpConfiguration.Customizer; +import org.osgi.annotation.versioning.ConsumerType; + + +/** + * The LoadBalancerCustomizerFactory is a service interface which allows + * extensions to inject custom Jetty {@code Customizer} instances to add + * to the Jetty server in order to handle the Proxy Load Balancer connection. + * + * {@code LoadBalancerCustomizerFactory } services are responsible for creating the + * {@code Customizer} instances and providing base configuration. + * + * @since 2.1 + */ +@ConsumerType +public interface LoadBalancerCustomizerFactory +{ + /** + * Creates new Jetty {@code Customizer} instances. + * + *

      + * @return A configured Jetty {@code Customizer} instance or {@code null} + * if the customizer can't be created. + */ + Customizer createCustomizer(); + +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/ConfigMetaTypeProvider.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/ConfigMetaTypeProvider.java new file mode 100644 index 00000000000..572054bbd7d --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/ConfigMetaTypeProvider.java @@ -0,0 +1,602 @@ +/* + * 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.http.jetty.internal; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.zip.Deflater; + +import org.apache.felix.http.base.internal.HttpConfig; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.server.session.HouseKeeper; +import org.osgi.framework.Bundle; +import org.osgi.service.metatype.AttributeDefinition; +import org.osgi.service.metatype.MetaTypeProvider; +import org.osgi.service.metatype.ObjectClassDefinition; + +class ConfigMetaTypeProvider implements MetaTypeProvider +{ + + private final Bundle bundle; + + public ConfigMetaTypeProvider(final Bundle bundle) + { + this.bundle = bundle; + } + + /** + * @see org.osgi.service.metatype.MetaTypeProvider#getLocales() + */ + @Override + public String[] getLocales() + { + return null; + } + + /** + * @see org.osgi.service.metatype.MetaTypeProvider#getObjectClassDefinition(java.lang.String, java.lang.String) + */ + @Override + public ObjectClassDefinition getObjectClassDefinition( String id, String locale ) + { + if ( !JettyService.PID.equals( id ) ) + { + return null; + } + + final ArrayList adList = new ArrayList<>(); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_HOST, + "Host Name", + "IP Address or Host Name of the interface to which HTTP and HTTPS bind. The default is " + + "\"0.0.0.0\" indicating all interfaces.", + "0.0.0.0", + bundle.getBundleContext().getProperty(JettyConfig.FELIX_HOST))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_HTTP_ENABLE, + "Enable HTTP", + "Whether or not HTTP is enabled. Defaults to true thus HTTP enabled.", + true, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_HTTP_ENABLE))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.HTTP_PORT, + "HTTP Port", + "Port to listen on for HTTP requests. Defaults to 8080.", + 8080, + bundle.getBundleContext().getProperty(JettyConfig.HTTP_PORT))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.HTTP_TIMEOUT, + "Connection Timeout", + "Time limit for reaching an timeout specified in milliseconds. This property applies to both HTTP and HTTP connections. Defaults to 60 seconds.", + 60000, + bundle.getBundleContext().getProperty(JettyConfig.HTTP_TIMEOUT))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_HTTPS_ENABLE, + "Enable HTTPS", + "Whether or not HTTPS is enabled. Defaults to false thus HTTPS disabled.", + false, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_HTTPS_ENABLE))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.HTTPS_PORT, + "HTTPS Port", + "Port to listen on for HTTPS requests. Defaults to 443.", + 443, + bundle.getBundleContext().getProperty(JettyConfig.HTTPS_PORT))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_KEYSTORE, + "Keystore", + "Absolute Path to the Keystore to use for HTTPS. Only used if HTTPS is enabled in which case this property is required.", + null, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_KEYSTORE))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_KEYSTORE_PASSWORD, + "Keystore Password", + "Password to access the Keystore. Only used if HTTPS is enabled.", + null, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_KEYSTORE_PASSWORD))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_KEYSTORE_KEY_PASSWORD, + "Key Password", + "Password to unlock the secret key from the Keystore. Only used if HTTPS is enabled.", + null, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_KEYSTORE_KEY_PASSWORD))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_TRUSTSTORE, + "Truststore", + "Absolute Path to the Truststore to use for HTTPS. Only used if HTTPS is enabled.", + null, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_TRUSTSTORE))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_TRUSTSTORE_PASSWORD, + "Truststore Password", + "Password to access the Truststore. Only used if HTTPS is enabled.", + null, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_TRUSTSTORE_PASSWORD))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_HTTPS_CLIENT_CERT, + "Client Certificate", + "Requirement for the Client to provide a valid certificate. Defaults to none.", + AttributeDefinition.STRING, + new String[] {"none"}, + 0, + new String[] {"No Client Certificate", "Client Certificate Wanted", "Client Certificate Needed"}, + new String[] {"none", "wants", "needs"}, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_HTTPS_CLIENT_CERT))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_HTTP_CONTEXT_PATH, + "Context Path", + "The Servlet Context Path to use for the Http Service. If this property is not configured it " + + "defaults to \"/\". This must be a valid path starting with a slash and not ending with a slash (unless it is the root context).", + "/", + bundle.getBundleContext().getProperty(JettyConfig.FELIX_HTTP_CONTEXT_PATH))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_HTTP_MBEANS, + "Register MBeans", + "Whether or not to use register JMX MBeans from the servlet container (Jetty). If this is " + + "enabled Jetty Request and Connector statistics are also enabled. The default is to not enable JMX.", + false, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_HTTP_MBEANS))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_SESSION_TIMEOUT, + "Session Timeout", + "Default lifetime of an HTTP session specified in a whole number of minutes. If the timeout is 0 or less, sessions will by default never timeout. The default is 0.", + 0, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_SESSION_TIMEOUT))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_THREADPOOL_MAX, + "Thread Pool Max", + "Maximum number of jetty threads. Using the default -1 uses Jetty's default (200).", + -1, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_THREADPOOL_MAX))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_ACCEPTORS, + "Acceptors", + "Number of acceptor threads to use, or -1 for a default value. Acceptors accept new TCP/IP connections. If 0, then the selector threads are used to accept connections.", + -1, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_ACCEPTORS))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_SELECTORS, + "Selectors", + "Number of selector threads, or <=0 for a default value. Selectors notice and schedule established connection that can make IO progress.", + -1, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_SELECTORS))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_HEADER_BUFFER_SIZE, + "Header Buffer Size", + "Size of the buffer for request and response headers. Default is 16KB.", + 16384, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_HEADER_BUFFER_SIZE))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_REQUEST_BUFFER_SIZE, + "Request Buffer Size", + "Size of the buffer for requests not fitting the header buffer. Default is 8KB.", + 8192, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_REQUEST_BUFFER_SIZE))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_RESPONSE_BUFFER_SIZE, + "Response Buffer Size", + "Size of the buffer for responses. Default is 24KB.", + 24576, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_RESPONSE_BUFFER_SIZE))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_MAX_FORM_SIZE, + "Maximum Form Size", + "Size of Body for submitted form content. Default is 200KB.", + 204800, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_MAX_FORM_SIZE))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_HTTP_PATH_EXCLUSIONS, + "Path Exclusions", + "Contains a list of context path prefixes. If a Web Application Bundle is started with a context path matching any " + + "of these prefixes, it will not be deployed in the servlet container.", + AttributeDefinition.STRING, + new String[] {"/system"}, + 2147483647, + null, null, + getStringArray(bundle.getBundleContext().getProperty(JettyConfig.FELIX_HTTP_PATH_EXCLUSIONS)))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_EXCLUDED_SUITES, + "Excluded Cipher Suites", + "List of cipher suites that should be excluded. Default is none.", + AttributeDefinition.STRING, + null, + 2147483647, + null, null, + getStringArray(bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_EXCLUDED_SUITES)))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_INCLUDED_SUITES, + "Included Cipher Suites", + "List of cipher suites that should be included. Default is none.", + AttributeDefinition.STRING, + null, + 2147483647, + null, null, + getStringArray(bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_INCLUDED_SUITES)))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_SEND_SERVER_HEADER, + "Send Server Header", + "If enabled, the server header is sent.", + false, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_SEND_SERVER_HEADER))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_INCLUDED_PROTOCOLS, + "Included Protocols", + "List of SSL protocols to include by default. Protocols may be any supported by the Java " + + "platform such as SSLv2Hello, SSLv3, TLSv1, TLSv1.1, or TLSv1.2. Any listed protocol " + + "not supported is silently ignored. Default is none assuming to use any protocol enabled " + + "and supported on the platform.", + AttributeDefinition.STRING, + null, + 2147483647, + null, null, + getStringArray(bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_INCLUDED_PROTOCOLS)))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_EXCLUDED_PROTOCOLS, + "Excluded Protocols", + "List of SSL protocols to exclude. This property further restricts the enabled protocols by " + + "explicitly disabling. Any protocol listed in both this property and the Included " + + "protocols property is excluded. Default is none such as to accept all protocols enabled " + + "on platform or explicitly listed by the Included protocols property.", + AttributeDefinition.STRING, + null, + 2147483647, + null, null, + getStringArray(bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_EXCLUDED_PROTOCOLS)))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_PROXY_LOAD_BALANCER_CONNECTION_ENABLE, + "Enable Proxy/Load Balancer Connection", + "Whether or not the Proxy/Load Balancer Connection is enabled. Defaults to false thus disabled.", + false, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_PROXY_LOAD_BALANCER_CONNECTION_ENABLE))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_RENEGOTIATION_ALLOWED, + "Renegotiation allowed", + "Whether TLS renegotiation is allowed (true by default)", + false, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_RENEGOTIATION_ALLOWED))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_SESSION_COOKIE_HTTP_ONLY, + "Session Cookie httpOnly", + "Session Cookie httpOnly (true by default)", + true, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_SESSION_COOKIE_HTTP_ONLY))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_SESSION_COOKIE_SECURE, + "Session Cookie secure", + "Session Cookie secure (false by default)", + false, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_SESSION_COOKIE_SECURE))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_SERVLET_SESSION_ID_PATH_PARAMETER_NAME, + "Session Id path parameter", + "Defaults to jsessionid. If set to null or \"none\" no URL rewriting will be done.", + "jsessionid", + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_SERVLET_SESSION_ID_PATH_PARAMETER_NAME))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_SERVLET_CHECK_REMOTE_SESSION_ENCODING, + "Check remote session encoding", + "If true, Jetty will add JSESSIONID parameter even when encoding external urls with calls to encodeURL() (true by default)", + true, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_SERVLET_CHECK_REMOTE_SESSION_ENCODING))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_SERVLET_SESSION_COOKIE_NAME, + "Session Cookie Name", + "Session Cookie Name", + "JSESSIONID", + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_SERVLET_SESSION_COOKIE_NAME))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_SERVLET_SESSION_DOMAIN, + "Session Domain", + "If this property is set, then it is used as the domain for session cookies. If it is not set, then no domain is specified for the session cookie. Default is none.", + null, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_SERVLET_SESSION_DOMAIN))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_SERVLET_SESSION_PATH, + "Session Path", + "If this property is set, then it is used as the path for the session cookie. Default is context path.", + null, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_SERVLET_SESSION_DOMAIN))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_SERVLET_SESSION_MAX_AGE, + "Session Max Age", + "Max age for the session cookie. Default is -1.", + -1, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_SERVLET_SESSION_MAX_AGE))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_SERVLET_SESSION_MAX_AGE, + "Session Scavenging Interval", + "Interval of session scavenging in seconds. Default is " + String.valueOf(HouseKeeper.DEFAULT_PERIOD_MS / 1000), + HouseKeeper.DEFAULT_PERIOD_MS / 1000, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_SESSION_SCAVENGING_INTERVAL))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_HTTP_SERVICE_NAME, + "HTTP Service Name", + "HTTP Service Name used in service filter to target specific HTTP instance. Default is null.", + null, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_HTTP_SERVICE_NAME))); + + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_GZIP_HANDLER_ENABLE, + "Enable GzipHandler", + "Whether the server should use a server-wide gzip handler. Default is false.", + false, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_GZIP_HANDLER_ENABLE))); + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_GZIP_MIN_GZIP_SIZE, + "Gzip Min Size", + String.format("The minimum response size to trigger dynamic compression. Default is %d.", GzipHandler.DEFAULT_MIN_GZIP_SIZE), + GzipHandler.DEFAULT_MIN_GZIP_SIZE, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_GZIP_MIN_GZIP_SIZE))); + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_GZIP_COMPRESSION_LEVEL, + "Gzip Compression Level", + String.format("The compression level to use. Default is %d.", Deflater.DEFAULT_COMPRESSION), + Deflater.DEFAULT_COMPRESSION, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_GZIP_COMPRESSION_LEVEL))); + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_GZIP_INFLATE_BUFFER_SIZE, + "Gzip Inflate Buffer Size", + "The size in bytes of the buffer to inflate compressed request, or <= 0 for no inflation. Default is -1.", + -1, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_GZIP_INFLATE_BUFFER_SIZE))); + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_GZIP_SYNC_FLUSH, + "Gzip Sync Flush", + "True if Deflater#SYNC_FLUSH should be used, else Deflater#NO_FLUSH will be used. Default is false.", + false, + bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_GZIP_SYNC_FLUSH))); + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_GZIP_EXCLUDED_USER_AGENT, + "Gzip Exclude User Agents", + "The regular expressions matching additional user agents to exclude. Default is none.", + AttributeDefinition.STRING, + null, + 2147483647, + null, null, + getStringArray(bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_GZIP_EXCLUDED_USER_AGENT)))); + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_GZIP_INCLUDED_METHODS, + "Gzip Include Methods", + "The additional http methods to include in compression. Default is none.", + AttributeDefinition.STRING, + null, + 2147483647, + null, null, + getStringArray(bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_GZIP_INCLUDED_METHODS)))); + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_GZIP_EXCLUDED_METHODS, + "Gzip Exclude Methods", + "The additional http methods to exclude in compression. Default is none.", + AttributeDefinition.STRING, + null, + 2147483647, + null, null, + getStringArray(bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_GZIP_EXCLUDED_METHODS)))); + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_GZIP_INCLUDED_PATHS, + "Gzip Included Paths", + "The additional path specs to include. Inclusion takes precedence over exclusion. Default is none.", + AttributeDefinition.STRING, + null, + 2147483647, + null, null, + getStringArray(bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_GZIP_INCLUDED_PATHS)))); + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_GZIP_EXCLUDED_PATHS, + "Gzip Excluded Paths", + "The additional path specs to exclude. Inclusion takes precedence over exclusion. Default is none.", + AttributeDefinition.STRING, + null, + 2147483647, + null, null, + getStringArray(bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_GZIP_EXCLUDED_PATHS)))); + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_GZIP_INCLUDED_MIME_TYPES, + "Gzip Included Mime Types", + "The included mime types. Inclusion takes precedence over exclusion. Default is none.", + AttributeDefinition.STRING, + null, + 2147483647, + null, null, + getStringArray(bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_GZIP_INCLUDED_MIME_TYPES)))); + adList.add(new AttributeDefinitionImpl(JettyConfig.FELIX_JETTY_GZIP_EXCLUDED_MIME_TYPES, + "Gzip Excluded Mime Types", + "The excluded mime types. Inclusion takes precedence over exclusion. Default is none.", + AttributeDefinition.STRING, + null, + 2147483647, + null, null, + getStringArray(bundle.getBundleContext().getProperty(JettyConfig.FELIX_JETTY_GZIP_EXCLUDED_MIME_TYPES)))); + adList.add(new AttributeDefinitionImpl(HttpConfig.PROP_INVALIDATE_SESSION, + "Invalidate Container Session", + "If this property is set, the container session is automatically validated.", + HttpConfig.DEFAULT_INVALIDATE_SESSION, + bundle.getBundleContext().getProperty(HttpConfig.PROP_INVALIDATE_SESSION))); + adList.add(new AttributeDefinitionImpl(HttpConfig.PROP_UNIQUE_SESSION_ID, + "Unique Session Id", + "If this property is set, each http context gets a unique session id (derived from the container session).", + HttpConfig.DEFAULT_UNIQUE_SESSION_ID, + bundle.getBundleContext().getProperty(HttpConfig.PROP_UNIQUE_SESSION_ID))); + + return new ObjectClassDefinition() + { + + private final AttributeDefinition[] attrs = adList + .toArray(new AttributeDefinition[adList.size()]); + + @Override + public String getName() + { + return "Apache Felix Jetty Based Http Service"; + } + + @Override + public InputStream getIcon(int arg0) + { + return null; + } + + @Override + public String getID() + { + return JettyService.PID; + } + + @Override + public String getDescription() + { + return "Configuration for the embedded Jetty Servlet Container."; + } + + @Override + public AttributeDefinition[] getAttributeDefinitions(int filter) + { + return (filter == OPTIONAL) ? null : attrs; + } + }; + } + + private String [] getStringArray(final String value) + { + if ( value != null ) + { + return value.trim().split(","); + } + return null; + } + + private static class AttributeDefinitionImpl implements AttributeDefinition + { + + private final String id; + private final String name; + private final String description; + private final int type; + private final String[] defaultValues; + private final int cardinality; + private final String[] optionLabels; + private final String[] optionValues; + + + AttributeDefinitionImpl( final String id, final String name, final String description, final String defaultValue, final String overrideValue ) + { + this( id, name, description, STRING, defaultValue == null ? null : new String[] { defaultValue }, 0, null, null, overrideValue == null ? null : new String[] { overrideValue } ); + } + + AttributeDefinitionImpl( final String id, final String name, final String description, final long defaultValue, final String overrideValue ) + { + this( id, name, description, LONG, new String[] + { String.valueOf(defaultValue) }, 0, null, null, overrideValue == null ? null : new String[] { overrideValue } ); + } + + AttributeDefinitionImpl( final String id, final String name, final String description, final int defaultValue, final String overrideValue ) + { + this( id, name, description, INTEGER, new String[] + { String.valueOf(defaultValue) }, 0, null, null, overrideValue == null ? null : new String[] { overrideValue } ); + } + + AttributeDefinitionImpl( final String id, final String name, final String description, final boolean defaultValue, final String overrideValue ) + { + this( id, name, description, BOOLEAN, new String[] + { String.valueOf(defaultValue) }, 0, null, null, overrideValue == null ? null : new String[] { overrideValue } ); + } + + AttributeDefinitionImpl( final String id, final String name, final String description, final int type, + final String[] defaultValues, final int cardinality, final String[] optionLabels, + final String[] optionValues, + final String overrideValue) + { + this(id, name, description, type, defaultValues, cardinality, optionLabels, optionValues, overrideValue == null ? null : new String[] { overrideValue }); + } + + AttributeDefinitionImpl( final String id, final String name, final String description, final int type, + final String[] defaultValues, final int cardinality, final String[] optionLabels, + final String[] optionValues, + final String[] overrideValues) + { + this.id = id; + this.name = name; + this.description = description; + this.type = type; + if ( overrideValues != null ) + { + this.defaultValues = overrideValues; + } + else + { + this.defaultValues = defaultValues; + } + this.cardinality = cardinality; + this.optionLabels = optionLabels; + this.optionValues = optionValues; + } + + + @Override + public int getCardinality() + { + return cardinality; + } + + + @Override + public String[] getDefaultValue() + { + return defaultValues; + } + + + @Override + public String getDescription() + { + return description; + } + + + @Override + public String getID() + { + return id; + } + + + @Override + public String getName() + { + return name; + } + + + @Override + public String[] getOptionLabels() + { + return optionLabels; + } + + + @Override + public String[] getOptionValues() + { + return optionValues; + } + + + @Override + public int getType() + { + return type; + } + + + @Override + public String validate( String arg0 ) + { + return null; + } + } +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/ConnectorFactoryTracker.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/ConnectorFactoryTracker.java new file mode 100644 index 00000000000..ab869b770f4 --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/ConnectorFactoryTracker.java @@ -0,0 +1,87 @@ +/* + * 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.http.jetty.internal; + +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.apache.felix.http.jetty.ConnectorFactory; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; + +public class ConnectorFactoryTracker extends ServiceTracker +{ + private final Server server; + + public ConnectorFactoryTracker(final BundleContext context, final Server server) + { + super(context, ConnectorFactory.class, null); + this.server = server; + } + + @Override + public void open() + { + if (!this.server.isStarted()) + { + throw new IllegalStateException("Jetty Server must be started before looking for ConnectorFactory services"); + } + + super.open(); + } + + @Override + public Connector addingService(ServiceReference reference) + { + ConnectorFactory factory = (ConnectorFactory) super.addingService(reference); + Connector connector = factory.createConnector(server); + try + { + this.server.addConnector(connector); + connector.start(); + return connector; + } + catch (Exception e) + { + SystemLogger.error("Failed starting connector '" + connector + "' provided by " + reference, e); + } + + // connector failed to start, don't continue tracking + return null; + } + + @Override + public void removedService(ServiceReference reference, Connector service) + { + Connector connector = service; + if (connector.isStarted()) + { + try + { + connector.stop(); + } + catch (Exception e) + { + SystemLogger.info("Failed stopping connector '" + connector + "' provided by " + reference + ": " + e); + } + } + this.server.removeConnector(connector); + } +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/CustomizerWrapper.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/CustomizerWrapper.java new file mode 100644 index 00000000000..0edc793352d --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/CustomizerWrapper.java @@ -0,0 +1,45 @@ +/* + * 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.http.jetty.internal; + +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConfiguration.Customizer; +import org.eclipse.jetty.server.Request; + +public class CustomizerWrapper implements Customizer +{ + + private volatile Customizer customizer; + + public void setCustomizer(final Customizer customizer) + { + this.customizer = customizer; + } + + @Override + public void customize(final Connector connector, + final HttpConfiguration channelConfig, + final Request request) + { + final Customizer local = this.customizer; + if (local!= null ) + { + local.customize(connector, channelConfig, request); + } + } +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java new file mode 100644 index 00000000000..e0cf04a6d44 --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java @@ -0,0 +1,104 @@ +/* + * 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.http.jetty.internal; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.eclipse.jetty.server.AsyncNCSARequestLog; +import org.eclipse.jetty.server.NCSARequestLog; +import org.eclipse.jetty.server.RequestLog; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; + +class FileRequestLog { + + public static final String SVC_PROP_NAME = "name"; + public static final String DEFAULT_NAME = "file"; + public static final String SVC_PROP_FILEPATH = "filepath"; + + private final NCSARequestLog delegate; + private final String logFilePath; + private final String serviceName; + private ServiceRegistration registration = null; + + FileRequestLog(JettyConfig config) { + logFilePath = config.getRequestLogFilePath(); + serviceName = config.getRequestLogFileServiceName() != null ? config.getRequestLogFileServiceName() : DEFAULT_NAME; + if (config.isRequestLogFileAsync()) { + delegate = new AsyncNCSARequestLog(logFilePath, null); + } else { + delegate = new NCSARequestLog(logFilePath); + } + + delegate.setAppend(config.isRequestLogFileAppend()); + delegate.setRetainDays(config.getRequestLogFileRetainDays()); + delegate.setFilenameDateFormat(config.getRequestLogFilenameDateFormat()); + delegate.setExtended(config.isRequestLogFileExtended()); + delegate.setIgnorePaths(config.getRequestLogFileIgnorePaths()); + delegate.setLogCookies(config.isRequestLogFileLogCookies()); + delegate.setLogServer(config.isRequestLogFileLogServer()); + delegate.setLogLatency(config.isRequestLogFileLogLatency()); + if (config.getRequestLogDateFormat() != null) { + delegate.setLogDateFormat(config.getRequestLogDateFormat()); + } + if (config.getRequestLogTimeZone() != null) { + delegate.setLogTimeZone(config.getRequestLogTimeZone()); + } + } + + synchronized void start(BundleContext context) throws IOException, IllegalStateException { + File logFile = new File(logFilePath).getAbsoluteFile(); + File logFileDir = logFile.getParentFile(); + if (logFileDir != null && !logFileDir.isDirectory()) { + SystemLogger.info("Creating directory " + logFileDir.getAbsolutePath()); + Files.createDirectories(logFileDir.toPath(), PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))); + } + + if (registration != null) { + throw new IllegalStateException(getClass().getSimpleName() + " is already started"); + } + try { + delegate.start(); + Dictionary svcProps = new Hashtable<>(); + svcProps.put(SVC_PROP_NAME, serviceName); + svcProps.put(SVC_PROP_FILEPATH, logFilePath); + registration = context.registerService(RequestLog.class, delegate, svcProps); + } catch (Exception e) { + SystemLogger.error("Error starting File Request Log", e); + } + } + + synchronized void stop() { + try { + if (registration != null) { + registration.unregister(); + } + delegate.stop(); + } catch (Exception e) { + SystemLogger.error("Error shutting down File Request Log", e); + } finally { + registration = null; + } + } + +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/ForwardedRequestCustomizerFactory.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/ForwardedRequestCustomizerFactory.java new file mode 100644 index 00000000000..5bfb052014c --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/ForwardedRequestCustomizerFactory.java @@ -0,0 +1,31 @@ +/* + * 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.http.jetty.internal; + +import org.apache.felix.http.jetty.LoadBalancerCustomizerFactory; +import org.eclipse.jetty.server.ForwardedRequestCustomizer; +import org.eclipse.jetty.server.HttpConfiguration.Customizer; + +public class ForwardedRequestCustomizerFactory implements LoadBalancerCustomizerFactory +{ + + @Override + public Customizer createCustomizer() + { + return new ForwardedRequestCustomizer(); + } +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyActivator.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyActivator.java new file mode 100644 index 00000000000..59aa90fd73c --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyActivator.java @@ -0,0 +1,150 @@ +/* + * 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.http.jetty.internal; + +import java.io.Closeable; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.apache.felix.http.base.internal.AbstractHttpActivator; +import org.apache.felix.http.jetty.LoadBalancerCustomizerFactory; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceRegistration; + +public final class JettyActivator extends AbstractHttpActivator +{ + private JettyService jetty; + + private volatile ServiceRegistration metatypeReg; + private volatile ServiceRegistration loadBalancerCustomizerFactoryReg; + private volatile ServiceRegistration jettyServiceFactoryReg; + + private volatile Closeable managedServiceFactory; + + @Override + protected void doStart() throws Exception + { + super.doStart(); + final Dictionary properties = new Hashtable<>(); + properties.put(Constants.SERVICE_DESCRIPTION, "Metatype provider for Jetty Http Service"); + properties.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation"); + properties.put("metatype.pid", JettyService.PID); + + metatypeReg = this.getBundleContext().registerService("org.osgi.service.metatype.MetaTypeProvider", + new ServiceFactory() + { + + @Override + public Object getService(final Bundle bundle, final ServiceRegistration registration) + { + return new ConfigMetaTypeProvider(getBundleContext().getBundle()); + } + + @Override + public void ungetService(Bundle bundle, ServiceRegistration registration, Object service) + { + // nothing to do + } + }, properties); + this.jetty = new JettyService(getBundleContext(), getHttpServiceController()); + this.jetty.start(); + + final Dictionary propertiesCustomizer = new Hashtable<>(); + propertiesCustomizer.put(Constants.SERVICE_DESCRIPTION, "Load Balancer Customizer Factory for Jetty Http Service"); + propertiesCustomizer.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation"); + loadBalancerCustomizerFactoryReg = this.getBundleContext().registerService(LoadBalancerCustomizerFactory.class, + new ServiceFactory() + { + + @Override + public LoadBalancerCustomizerFactory getService(final Bundle bundle, + final ServiceRegistration registration) + { + return new ForwardedRequestCustomizerFactory(); + } + + @Override + public void ungetService(final Bundle bundle, + final ServiceRegistration registration, + final LoadBalancerCustomizerFactory service) + { + // nothing to do + } + }, propertiesCustomizer); + + final Dictionary factoryProps = new Hashtable<>(); + factoryProps.put(Constants.SERVICE_PID, JettyService.PID); + factoryProps.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation"); + factoryProps.put(Constants.SERVICE_DESCRIPTION, "Managed Service Factory for the Jetty Http Service"); + this.jettyServiceFactoryReg = this.getBundleContext().registerService("org.osgi.service.cm.ManagedServiceFactory", + new ServiceFactory() + { + + @Override + public Object getService(final Bundle bundle, + final ServiceRegistration registration) + { + synchronized ( jetty ) + { + if ( managedServiceFactory == null ) + { + managedServiceFactory = new JettyManagedServiceFactory(getBundleContext()); + } + } + return managedServiceFactory; + } + + @Override + public void ungetService(final Bundle bundle, + final ServiceRegistration registration, + final Object service) + { + // do nothing + } + }, factoryProps); + + } + + @Override + protected void doStop() throws Exception + { + if ( this.managedServiceFactory != null ) + { + this.managedServiceFactory.close(); + } + this.jetty.stop(); + if ( metatypeReg != null ) + { + metatypeReg.unregister(); + metatypeReg = null; + } + if ( loadBalancerCustomizerFactoryReg != null ) + { + loadBalancerCustomizerFactoryReg.unregister(); + loadBalancerCustomizerFactoryReg = null; + } + if ( jettyServiceFactoryReg != null ) + { + jettyServiceFactoryReg.unregister(); + jettyServiceFactoryReg = null; + } + + super.doStop(); + } +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java new file mode 100644 index 00000000000..00d301f910c --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java @@ -0,0 +1,909 @@ +/* + * 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.http.jetty.internal; + +import java.io.IOException; +import java.net.ServerSocket; +import java.security.KeyStore; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.zip.Deflater; + +import org.apache.felix.http.base.internal.HttpConfig; +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.osgi.framework.BundleContext; + +public final class JettyConfig +{ + /** Felix specific property to set the interface to listen on. Applies to both HTTP and HTTP */ + public static final String FELIX_HOST = "org.apache.felix.http.host"; + + /** Standard OSGi port property for HTTP service */ + public static final String HTTP_PORT = "org.osgi.service.http.port"; + + /** Standard OSGi port property for HTTPS service */ + public static final String HTTPS_PORT = "org.osgi.service.http.port.secure"; + + /** Felix specific property to set http reaching timeout limit */ + public static final String HTTP_TIMEOUT = "org.apache.felix.http.timeout"; + + /** Felix specific property to override the keystore file location. */ + public static final String FELIX_KEYSTORE = "org.apache.felix.https.keystore"; + private static final String OSCAR_KEYSTORE = "org.ungoverned.osgi.bundle.https.keystore"; + + /** Felix specific property to override the keystore password. */ + public static final String FELIX_KEYSTORE_PASSWORD = "org.apache.felix.https.keystore.password"; + private static final String OSCAR_KEYSTORE_PASSWORD = "org.ungoverned.osgi.bundle.https.password"; + + /** Felix specific property to override the keystore key password. */ + public static final String FELIX_KEYSTORE_KEY_PASSWORD = "org.apache.felix.https.keystore.key.password"; + private static final String OSCAR_KEYSTORE_KEY_PASSWORD = "org.ungoverned.osgi.bundle.https.key.password"; + + /** Felix specific property to override the type of keystore (JKS). */ + public static final String FELIX_KEYSTORE_TYPE = "org.apache.felix.https.keystore.type"; + + /** Felix specific property to control whether to enable HTTPS. */ + public static final String FELIX_HTTPS_ENABLE = "org.apache.felix.https.enable"; + private static final String OSCAR_HTTPS_ENABLE = "org.ungoverned.osgi.bundle.https.enable"; + + /** Felix specific property to control whether to enable HTTP. */ + public static final String FELIX_HTTP_ENABLE = "org.apache.felix.http.enable"; + + /** Felix specific property to override the truststore file location. */ + public static final String FELIX_TRUSTSTORE = "org.apache.felix.https.truststore"; + + /** Felix specific property to override the truststore password. */ + public static final String FELIX_TRUSTSTORE_PASSWORD = "org.apache.felix.https.truststore.password"; + + /** Felix specific property to override the type of truststore (JKS). */ + public static final String FELIX_TRUSTSTORE_TYPE = "org.apache.felix.https.truststore.type"; + + /** Felix specific property to control whether to want or require HTTPS client certificates. Valid values are "none", "wants", "needs". Default is "none". */ + public static final String FELIX_HTTPS_CLIENT_CERT = "org.apache.felix.https.clientcertificate"; + + /** Felix specific property to configure the session timeout in minutes (same session-timout in web.xml). Default is servlet container specific */ + public static final String FELIX_SESSION_TIMEOUT = "org.apache.felix.http.session.timeout"; + + /** Felix specific property to control the maximum size of the jetty thread pool */ + public static final String FELIX_JETTY_THREADPOOL_MAX = "org.apache.felix.http.jetty.threadpool.max"; + + /** Felix specific property to control the number of jetty acceptor threads */ + public static final String FELIX_JETTY_ACCEPTORS = "org.apache.felix.http.jetty.acceptors"; + + /** Felix specific property to control the number of jetty selector threads */ + public static final String FELIX_JETTY_SELECTORS = "org.apache.felix.http.jetty.selectors"; + + /** Felix specific property to configure the request buffer size. Default is 16KB (instead of Jetty's default of 4KB) */ + public static final String FELIX_JETTY_HEADER_BUFFER_SIZE = "org.apache.felix.http.jetty.headerBufferSize"; + + /** Felix specific property to configure the request buffer size. Default is 8KB */ + public static final String FELIX_JETTY_REQUEST_BUFFER_SIZE = "org.apache.felix.http.jetty.requestBufferSize"; + + /** Felix specific property to configure the request buffer size. Default is 24KB */ + public static final String FELIX_JETTY_RESPONSE_BUFFER_SIZE = "org.apache.felix.http.jetty.responseBufferSize"; + + /** Felix specific property to configure the max form size. Default is 200KB */ + public static final String FELIX_JETTY_MAX_FORM_SIZE = "org.apache.felix.http.jetty.maxFormSize"; + + /** Felix specific property to enable Jetty MBeans. Valid values are "true", "false". Default is false */ + public static final String FELIX_HTTP_MBEANS = "org.apache.felix.http.mbeans"; + + /** Felix specific property to set the servlet context path of the Http Service */ + public static final String FELIX_HTTP_CONTEXT_PATH = "org.apache.felix.http.context_path"; + + /** Felix specific property to set the list of path exclusions for Web Application Bundles */ + public static final String FELIX_HTTP_PATH_EXCLUSIONS = "org.apache.felix.http.path_exclusions"; + + /** Felix specific property to configure the excluded cipher suites. @deprecated use {@link #FELIX_JETTY_EXCLUDED_SUITES} instead. */ + @Deprecated + public static final String FELIX_JETTY_EXCLUDED_SUITES_OLD = "org.apache.felix.https.jetty.cipersuites.excluded"; + /** Felix specific property to configure the excluded cipher suites */ + public static final String FELIX_JETTY_EXCLUDED_SUITES = "org.apache.felix.https.jetty.ciphersuites.excluded"; + + /** Felix specific property to configure the included cipher suites. @deprecated use {@link #FELIX_JETTY_INCLUDED_SUITES} instead. */ + @Deprecated + public static final String FELIX_JETTY_INCLUDED_SUITES_OLD = "org.apache.felix.https.jetty.cipersuites.included"; + /** Felix specific property to configure the included cipher suites. */ + public static final String FELIX_JETTY_INCLUDED_SUITES = "org.apache.felix.https.jetty.ciphersuites.included"; + + /** Felix specific property to specify whether a server header should be sent (defaults to true) */ + public static final String FELIX_JETTY_SEND_SERVER_HEADER = "org.apache.felix.http.jetty.sendServerHeader"; + + /** Felix specific property to configure the included protocols */ + public static final String FELIX_JETTY_INCLUDED_PROTOCOLS = "org.apache.felix.https.jetty.protocols.included"; + + /** Felix specific property to configure the excluded protocols */ + public static final String FELIX_JETTY_EXCLUDED_PROTOCOLS = "org.apache.felix.https.jetty.protocols.excluded"; + + /** Felix specific properties to be able to disable renegotiation protocol for TLSv1 */ + public static final String FELIX_JETTY_RENEGOTIATION_ALLOWED = "org.apache.felix.https.jetty.renegotiateAllowed"; + + /** Felix specific property to control whether to enable Proxy/Load Balancer Connection */ + public static final String FELIX_PROXY_LOAD_BALANCER_CONNECTION_ENABLE = "org.apache.felix.proxy.load.balancer.connection.enable"; + + /** Felix specific property to configure the session cookie httpOnly flag */ + public static final String FELIX_JETTY_SESSION_COOKIE_HTTP_ONLY = "org.apache.felix.https.jetty.session.cookie.httpOnly"; + + /** Felix specific property to configure the session cookie secure flag */ + public static final String FELIX_JETTY_SESSION_COOKIE_SECURE = "org.apache.felix.https.jetty.session.cookie.secure"; + + /** Felix specific property to configure session id path parameter*/ + public static final String FELIX_JETTY_SERVLET_SESSION_ID_PATH_PARAMETER_NAME = "org.eclipse.jetty.servlet.SessionIdPathParameterName"; + + /** Felix specific property to configure whether JSESSIONID parameter will be added when encoding external URLs */ + public static final String FELIX_JETTY_SERVLET_CHECK_REMOTE_SESSION_ENCODING = "org.eclipse.jetty.servlet.CheckingRemoteSessionIdEncoding"; + + /** Felix specific property to configure session cookie name */ + public static final String FELIX_JETTY_SERVLET_SESSION_COOKIE_NAME = "org.eclipse.jetty.servlet.SessionCookie"; + + /** Felix specific property to configure session domain */ + public static final String FELIX_JETTY_SERVLET_SESSION_DOMAIN = "org.eclipse.jetty.servlet.SessionDomain"; + + /** Felix specific property to configure session path */ + public static final String FELIX_JETTY_SERVLET_SESSION_PATH = "org.eclipse.jetty.servlet.SessionPath"; + + /** Felix specific property to configure session max age */ + public static final String FELIX_JETTY_SERVLET_SESSION_MAX_AGE = "org.eclipse.jetty.servlet.MaxAge"; + + /** Felix specific property to configure session scavenging interval in Seconds */ + public static final String FELIX_JETTY_SESSION_SCAVENGING_INTERVAL = "org.eclipse.jetty.servlet.SessionScavengingInterval"; + + /** Felix specific property to set HTTP instance name. */ + public static final String FELIX_HTTP_SERVICE_NAME = "org.apache.felix.http.name"; + + /** Felix specific property to configure a filter for RequestLog services */ + public static final String FELIX_HTTP_REQUEST_LOG_FILTER = "org.apache.felix.http.requestlog.filter"; + + /** Felix specific property to enable request logging to the OSGi Log Service */ + public static final String FELIX_HTTP_REQUEST_LOG_OSGI_ENABLE = "org.apache.felix.http.requestlog.osgi.enable"; + + /** Felix specific property to specify the published "name" property of the OSGi Log Service-base Request Log service. Allows server configs to filter on specific log services. */ + public static final String FELIX_HTTP_REQUEST_LOG_OSGI_SERVICE_NAME = "org.apache.felix.http.requestlog.osgi.name"; + + /** Felix specific property to control the level of the log messages generated by the OSGi Log Service-based request log. Values must correspond to the constants defined in the LogService interface, default is 3 "INFO". */ + public static final String FELIX_HTTP_REQUEST_LOG_OSGI_LEVEL = "org.apache.felix.http.requestlog.osgi.level"; + + /** Felix specific property to enable request logging to a file and provide the path to that file. Default is null meaning that the file log is disabled. */ + public static final String FELIX_HTTP_REQUEST_LOG_FILE_PATH = "org.apache.felix.http.requestlog.file.path"; + + /** Felix specific property to specify the published "name" property of the file-based RequestLog service. Allows server configs to filter on specific log services. */ + public static final String FELIX_HTTP_REQUEST_LOG_FILE_SERVICE_NAME = "org.apache.felix.http.requestlog.file.name"; + + /** Felix specific property to enable file request logging to be asynchronous */ + public static final String FELIX_HTTP_REQUEST_LOG_FILE_ASYNC = "org.apache.felix.http.requestlog.file.async"; + + /** Felix specific property to enable request logging to append to the log file rather than overwriting */ + public static final String FELIX_HTTP_REQUEST_LOG_FILE_APPEND = "org.apache.felix.http.requestlog.file.append"; + + /** Felix specific property to specify the number of days the request log file is retained */ + public static final String FELIX_HTTP_REQUEST_LOG_FILE_RETAIN_DAYS = "org.apache.felix.http.requestlog.file.retaindays"; + + /** Felix specific property to specify the date format in request log file names */ + public static final String FELIX_HTTP_REQUEST_LOG_FILE_FILENAME_DATE_FORMAT = "org.apache.felix.http.requestlog.file.dateformat"; + + /** Felix specific property to enable extended request logging to a named file */ + public static final String FELIX_HTTP_REQUEST_LOG_FILE_EXTENDED = "org.apache.felix.http.requestlog.file.extended"; + + /** Felix specific property to ignore matching paths in the request log file */ + public static final String FELIX_HTTP_REQUEST_LOG_FILE_IGNORE_PATHS = "org.apache.felix.http.requestlog.file.ignorepaths"; + + /** Felix specific property to enable request logging cookies in the request log file*/ + public static final String FELIX_HTTP_REQUEST_LOG_FILE_LOG_COOKIES = "org.apache.felix.http.requestlog.file.logcookies"; + + /** Felix specific property to enable request logging the host name in the request log file*/ + public static final String FELIX_HTTP_REQUEST_LOG_FILE_LOG_SERVER = "org.apache.felix.http.requestlog.file.logserver"; + + /** Felix specific property to enable request logging request processing time in the request log file*/ + public static final String FELIX_HTTP_REQUEST_LOG_FILE_LOG_LATENCY = "org.apache.felix.http.requestlog.file.loglatency"; + + /** Felix specific property to specify the date format used for the logging timestamps*/ + public static final String FELIX_HTTP_REQUEST_LOG_FILE_DATE_FORMAT = "org.apache.felix.http.requestlog.file.logdateformat"; + + /** Felix specific property to specify the timezone used for the logging timestamps*/ + public static final String FELIX_HTTP_REQUEST_LOG_FILE_TIMEZONE = "org.apache.felix.http.requestlog.file.timezone"; + + /** Felix specific property to define custom properties for the http runtime service. */ + public static final String FELIX_CUSTOM_HTTP_RUNTIME_PROPERTY_PREFIX = "org.apache.felix.http.runtime.init."; + + /** Felix specific property to specify whether the server should use a server-wide gzip handler (defaults to true) */ + public static final String FELIX_JETTY_GZIP_HANDLER_ENABLE = "org.apache.felix.jetty.gziphandler.enable"; + + /** Felix specific property to specify the minimum response size to trigger dynamic compression */ + public static final String FELIX_JETTY_GZIP_MIN_GZIP_SIZE = "org.apache.felix.jetty.gzip.minGzipSize"; + + /** Felix specific property to specify the compression level to use to initialize {@link Deflater#setLevel(int)} */ + public static final String FELIX_JETTY_GZIP_COMPRESSION_LEVEL = "org.apache.felix.jetty.gzip.compressionLevel"; + + /** Felix specific property to specify the size in bytes of the buffer to inflate compressed request, or 0 for no inflation. */ + public static final String FELIX_JETTY_GZIP_INFLATE_BUFFER_SIZE = "org.apache.felix.jetty.gzip.inflateBufferSize"; + + /** Felix specific property to specify the {@link Deflater} flush mode to use. */ + public static final String FELIX_JETTY_GZIP_SYNC_FLUSH = "org.apache.felix.jetty.gzip.syncFlush"; + + /** Felix specific property to specify the regular expressions matching user agents to exclude */ + public static final String FELIX_JETTY_GZIP_EXCLUDED_USER_AGENT = "org.apache.felix.jetty.gzip.excludedUserAgents"; + + /** Felix specific property to specify the methods to include in compression */ + public static final String FELIX_JETTY_GZIP_INCLUDED_METHODS = "org.apache.felix.jetty.gzip.includedMethods"; + + /** Felix specific property to specify the methods to exclude from compression */ + public static final String FELIX_JETTY_GZIP_EXCLUDED_METHODS = "org.apache.felix.jetty.gzip.excludedMethods"; + + /** Felix specific property to specify the path specs to include. Inclusion takes precedence over exclusion. */ + public static final String FELIX_JETTY_GZIP_INCLUDED_PATHS = "org.apache.felix.jetty.gzip.includedPaths"; + + /** Felix specific property to specify the path specs to exclude. */ + public static final String FELIX_JETTY_GZIP_EXCLUDED_PATHS = "org.apache.felix.jetty.gzip.excludedPaths"; + + /** Felix specific property to specify the included mime types. Inclusion takes precedence over exclusion. */ + public static final String FELIX_JETTY_GZIP_INCLUDED_MIME_TYPES = "org.apache.felix.jetty.gzip.includedMimeTypes"; + + /** Felix specific property to specify the excluded mime types. */ + public static final String FELIX_JETTY_GZIP_EXCLUDED_MIME_TYPES = "org.apache.felix.jetty.gzip.excludedMimeTypes"; + + private static String validateContextPath(String ctxPath) + { + // undefined, empty, or root context path + if (ctxPath == null || ctxPath.length() == 0 || "/".equals(ctxPath)) + { + return "/"; + } + + // ensure leading but no trailing slash + if (!ctxPath.startsWith("/")) + { + ctxPath = "/".concat(ctxPath); + } + while (ctxPath.endsWith("/")) + { + ctxPath = ctxPath.substring(0, ctxPath.length() - 1); + } + + return ctxPath; + } + + private final BundleContext context; + + /** + * Properties from the configuration not matching any of the + * predefined properties. These properties can be accessed from the + * getProperty* methods. + *

      + * This map is indexed by String objects (the property names) and + * the values are just objects as provided by the configuration. + */ + private volatile Dictionary config; + + public JettyConfig(final BundleContext context) + { + this.context = context; + reset(); + } + + /** + * Returns the named generic configuration property from the + * configuration or the bundle context. If neither property is defined + * return the defValue. + */ + public boolean getBooleanProperty(String name, boolean defValue) + { + String value = getProperty(name, null); + if (value != null) + { + return "true".equalsIgnoreCase(value) || "yes".equalsIgnoreCase(value); + } + + return defValue; + } + + public String getClientcert() + { + return getProperty(FELIX_HTTPS_CLIENT_CERT, "none"); + } + + public String getContextPath() + { + return validateContextPath(getProperty(FELIX_HTTP_CONTEXT_PATH, null)); + } + + public String[] getExcludedCipherSuites() + { + return getStringArrayProperty(FELIX_JETTY_EXCLUDED_SUITES, getStringArrayProperty(FELIX_JETTY_EXCLUDED_SUITES_OLD, null)); + } + + public String[] getIncludedProtocols() + { + return getStringArrayProperty(FELIX_JETTY_INCLUDED_PROTOCOLS, null); + } + + public String[] getExcludedProtocols() + { + return getStringArrayProperty(FELIX_JETTY_EXCLUDED_PROTOCOLS, null); + } + + public int getHeaderSize() + { + return getIntProperty(FELIX_JETTY_HEADER_BUFFER_SIZE, 16 * 1024); + } + + public String getHost() + { + return getProperty(FELIX_HOST, null); + } + + public int getHttpPort() + { + return determinePort(String.valueOf(getProperty(HTTP_PORT)), 8080); + } + + public int getHttpsPort() + { + return determinePort(String.valueOf(getProperty(HTTPS_PORT)), 8443); + } + + public int getHttpTimeout() + { + return getIntProperty(HTTP_TIMEOUT, 60000); + } + + public String[] getIncludedCipherSuites() + { + return getStringArrayProperty(FELIX_JETTY_INCLUDED_SUITES, getStringArrayProperty(FELIX_JETTY_INCLUDED_SUITES_OLD, null)); + } + + /** + * Returns the named generic configuration property from the + * configuration or the bundle context. If neither property is defined + * return the defValue. + */ + public int getIntProperty(String name, int defValue) + { + return parseInt(getProperty(name, null), defValue); + } + + /** + * Returns the named generic configuration property from the + * configuration or the bundle context. If neither property is defined + * return the defValue. + */ + public long getLongProperty(String name, long defValue) + { + return parseLong(getProperty(name, null), defValue); + } + + public String getKeyPassword() + { + return getProperty(FELIX_KEYSTORE_KEY_PASSWORD, this.context.getProperty(OSCAR_KEYSTORE_KEY_PASSWORD)); + } + + public String getKeystore() + { + return getProperty(FELIX_KEYSTORE, this.context.getProperty(OSCAR_KEYSTORE)); + } + + public String getKeystoreType() + { + return getProperty(FELIX_KEYSTORE_TYPE, KeyStore.getDefaultType()); + } + + public String getPassword() + { + return getProperty(FELIX_KEYSTORE_PASSWORD, this.context.getProperty(OSCAR_KEYSTORE_PASSWORD)); + } + + public String[] getPathExclusions() + { + return getStringArrayProperty(FELIX_HTTP_PATH_EXCLUSIONS, new String[] { "/system" }); + } + + /** + * Returns the named generic configuration property from the + * configuration or the bundle context. If neither property is defined + * return the defValue. + */ + public String getProperty(String name, String defValue) + { + Object value = getProperty(name); + return value != null ? String.valueOf(value) : defValue; + } + + public int getThreadPoolMax() + { + return getIntProperty(FELIX_JETTY_THREADPOOL_MAX, -1); + } + + public int getAcceptors() + { + return getIntProperty(FELIX_JETTY_ACCEPTORS, -1); + } + + public int getSelectors() + { + return getIntProperty(FELIX_JETTY_SELECTORS, -1); + } + + public int getRequestBufferSize() + { + return getIntProperty(FELIX_JETTY_REQUEST_BUFFER_SIZE, 8 * 1024); + } + + public int getResponseBufferSize() + { + return getIntProperty(FELIX_JETTY_RESPONSE_BUFFER_SIZE, 24 * 1024); + } + + public int getMaxFormSize() + { + return getIntProperty(FELIX_JETTY_MAX_FORM_SIZE, 200 * 1024); + } + + /** + * Returns the configured session timeout in minutes or zero if not + * configured. + */ + public int getSessionTimeout() + { + return getIntProperty(FELIX_SESSION_TIMEOUT, 0); + } + + public String getTrustPassword() + { + return getProperty(FELIX_TRUSTSTORE_PASSWORD, null); + } + + public String getTruststore() + { + String value = getProperty(FELIX_TRUSTSTORE, null); + return value == null || value.trim().length() == 0 ? null : value; + } + + public String getTruststoreType() + { + return getProperty(FELIX_TRUSTSTORE_TYPE, KeyStore.getDefaultType()); + } + + public boolean isRegisterMBeans() + { + return getBooleanProperty(FELIX_HTTP_MBEANS, false); + } + + /** + * Returns true if HTTP is configured to be used ( + * {@link #FELIX_HTTP_ENABLE}) and + * the configured HTTP port ({@link #HTTP_PORT}) is higher than zero. + */ + public boolean isUseHttp() + { + boolean useHttp = getBooleanProperty(FELIX_HTTP_ENABLE, true); + return useHttp && getHttpPort() > 0; + } + + public boolean isSendServerHeader() + { + return getBooleanProperty(FELIX_JETTY_SEND_SERVER_HEADER, false); + } + + /** + * Returns true if HTTPS is configured to be used ( + * {@link #FELIX_HTTPS_ENABLE}) and + * the configured HTTP port ({@link #HTTPS_PORT}) is higher than zero. + */ + public boolean isUseHttps() + { + boolean useHttps = getBooleanProperty(FELIX_HTTPS_ENABLE, getBooleanProperty(OSCAR_HTTPS_ENABLE, false)); + return useHttps && getHttpsPort() > 0; + } + + public boolean isProxyLoadBalancerConnection() + { + return getBooleanProperty(FELIX_PROXY_LOAD_BALANCER_CONNECTION_ENABLE, false); + } + + public boolean isRenegotiationAllowed() { + return getBooleanProperty(FELIX_JETTY_RENEGOTIATION_ALLOWED, false); + } + + public String getHttpServiceName() + { + return (String) getProperty(FELIX_HTTP_SERVICE_NAME); + } + + public String getRequestLogFilter() { + return getProperty(FELIX_HTTP_REQUEST_LOG_FILTER, null); + } + + public boolean isRequestLogOSGiEnabled() { + return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_OSGI_ENABLE, false); + } + + public String getRequestLogOSGiServiceName() { + return (String) getProperty(FELIX_HTTP_REQUEST_LOG_OSGI_SERVICE_NAME); + } + + public int getRequestLogOSGiLevel() { + return getIntProperty(FELIX_HTTP_REQUEST_LOG_OSGI_LEVEL, 3); // 3 == LogService.LOG_INFO + } + + public String getRequestLogFilePath() { + return getProperty(FELIX_HTTP_REQUEST_LOG_FILE_PATH, null); + } + + public String getRequestLogFileServiceName() { + return getProperty(FELIX_HTTP_REQUEST_LOG_FILE_SERVICE_NAME, "file"); + } + + public boolean isRequestLogFileAsync() { + return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_ASYNC, false); + } + + public boolean isRequestLogFileAppend() { + return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_APPEND, true); + } + + public int getRequestLogFileRetainDays() { + return getIntProperty(FELIX_HTTP_REQUEST_LOG_FILE_RETAIN_DAYS, 31); + } + + public String getRequestLogFilenameDateFormat() { + return getProperty(FELIX_HTTP_REQUEST_LOG_FILE_FILENAME_DATE_FORMAT, null); + } + + public boolean isRequestLogFileExtended() { + return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_EXTENDED, false); + } + + public String[] getRequestLogFileIgnorePaths() { + return getStringArrayProperty(FELIX_HTTP_REQUEST_LOG_FILE_IGNORE_PATHS, new String[0]); + } + + public boolean isRequestLogFileLogCookies() { + return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_LOG_COOKIES, false); + } + + public boolean isRequestLogFileLogServer() { + return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_LOG_SERVER, false); + } + + public boolean isRequestLogFileLogLatency() { + return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_LOG_LATENCY, false); + } + + public String getRequestLogDateFormat() { + return getProperty(FELIX_HTTP_REQUEST_LOG_FILE_DATE_FORMAT, null); + } + + public String getRequestLogTimeZone() { + return getProperty(FELIX_HTTP_REQUEST_LOG_FILE_TIMEZONE, null); + } + + + public boolean isGzipHandlerEnabled() { + return getBooleanProperty(FELIX_JETTY_GZIP_HANDLER_ENABLE, false); + } + + public int getGzipMinGzipSize() { + return getIntProperty(FELIX_JETTY_GZIP_MIN_GZIP_SIZE, GzipHandler.DEFAULT_MIN_GZIP_SIZE); + } + + public int getGzipCompressionLevel() { + return getIntProperty(FELIX_JETTY_GZIP_COMPRESSION_LEVEL, Deflater.DEFAULT_COMPRESSION); + } + + public int getGzipInflateBufferSize() { + return getIntProperty(FELIX_JETTY_GZIP_INFLATE_BUFFER_SIZE, -1); + } + + public boolean isGzipSyncFlush() { + return getBooleanProperty(FELIX_JETTY_GZIP_SYNC_FLUSH, false); + } + + public String[] getGzipExcludedUserAgent() { + return getStringArrayProperty(FELIX_JETTY_GZIP_EXCLUDED_USER_AGENT, new String[0]); + } + + public String[] getGzipIncludedMethods() { + return getStringArrayProperty(FELIX_JETTY_GZIP_INCLUDED_METHODS, new String[0]); + } + + public String[] getGzipExcludedMethods() { + return getStringArrayProperty(FELIX_JETTY_GZIP_EXCLUDED_METHODS, new String[0]); + } + + public String[] getGzipIncludedPaths() { + return getStringArrayProperty(FELIX_JETTY_GZIP_INCLUDED_PATHS, new String[0]); + } + + public String[] getGzipExcludedPaths() { + return getStringArrayProperty(FELIX_JETTY_GZIP_EXCLUDED_PATHS, new String[0]); + } + + public String[] getGzipIncludedMimeTypes() { + return getStringArrayProperty(FELIX_JETTY_GZIP_INCLUDED_MIME_TYPES, new String[0]); + } + + public String[] getGzipExcludedMimeTypes() { + return getStringArrayProperty(FELIX_JETTY_GZIP_EXCLUDED_MIME_TYPES, new String[0]); + } + + public void reset() + { + update(null); + } + + public void setServiceProperties(Hashtable props) + { + props.put(HTTP_PORT, Integer.toString(getHttpPort())); + props.put(HTTPS_PORT, Integer.toString(getHttpsPort())); + props.put(FELIX_HTTP_ENABLE, Boolean.toString(isUseHttp())); + props.put(FELIX_HTTPS_ENABLE, Boolean.toString(isUseHttps())); + if (getHttpServiceName() != null) + { + props.put(FELIX_HTTP_SERVICE_NAME, getHttpServiceName()); + } + + props.put(HttpConfig.PROP_INVALIDATE_SESSION, getBooleanProperty(HttpConfig.PROP_INVALIDATE_SESSION, + HttpConfig.DEFAULT_INVALIDATE_SESSION)); + props.put(HttpConfig.PROP_UNIQUE_SESSION_ID, getBooleanProperty(HttpConfig.PROP_UNIQUE_SESSION_ID, + HttpConfig.DEFAULT_UNIQUE_SESSION_ID)); + + addCustomServiceProperties(props); + } + + private void addCustomServiceProperties(final Hashtable props) + { + final Enumeration keys = this.config.keys(); + while(keys.hasMoreElements()) + { + final String key = keys.nextElement(); + if (key.startsWith(FELIX_CUSTOM_HTTP_RUNTIME_PROPERTY_PREFIX)) + { + props.put(key.substring(FELIX_CUSTOM_HTTP_RUNTIME_PROPERTY_PREFIX.length()), this.config.get(key)); + } + } + } + + /** + * Updates this configuration with the given dictionary. + * + * @param props the dictionary with the new configuration values, can be null to reset this configuration to its defaults. + * @return true if the configuration was updated due to a changed value, or false if no change was found. + */ + public boolean update(Dictionary props) + { + if (props == null) + { + props = new Hashtable<>(); + } + + // FELIX-4312 Check whether there's something changed in our configuration... + Dictionary currentConfig = this.config; + if (currentConfig == null || !props.equals(currentConfig)) + { + this.config = props; + + return true; + } + + return false; + } + + private void closeSilently(ServerSocket resource) + { + if (resource != null) + { + try + { + resource.close(); + } + catch (IOException e) + { + // Ignore... + } + } + } + + /** + * Determine the appropriate port to use. portProp is based + * "version range" as described in OSGi Core Spec v4.2 3.2.6. It can use the + * following forms: + *

      + *
      8000 | 8000
      + *
      [8000,9000] | 8000 <= port <= 9000
      + *
      [8000,9000) | 8000 <= port < 9000
      + *
      (8000,9000] | 8000 < port <= 9000
      + *
      (8000,9000) | 8000 < port < 9000
      + *
      [,9000) | 1 < port < 9000
      + *
      [8000,) | 8000 <= port < 65534
      + *
      + * + * @param portProp + * The port property value to parse. + * @return The port determined to be usable. -1 if failed to find a port. + */ + private int determinePort(String portProp, int dflt) + { + // Default cases include null/empty range pattern or pattern == *. + if (portProp == null || "".equals(portProp.trim())) + { + return dflt; + } + + // asking for random port, so let ServerSocket handle it and return the answer + portProp = portProp.trim(); + if ("*".equals(portProp) || "0".equals(portProp)) + { + return getSocketPort(0); + } + else + { + // check that the port property is a version range as described in + // OSGi Core Spec v4.2 3.2.6. + // deviations from the spec are limited to: + // * start, end of interval defaults to 1, 65535, respectively, if missing. + char startsWith = portProp.charAt(0); + char endsWith = portProp.charAt(portProp.length() - 1); + + int minPort = 1; + int maxPort = 65535; + + if (portProp.contains(",") && (startsWith == '[' || startsWith == '(') && (endsWith == ']' || endsWith == ')')) + { + String interval = portProp.substring(1, portProp.length() - 1); + int comma = interval.indexOf(','); + + // check if the comma is first (start port in range is missing) + int start = (comma == 0) ? minPort : parseInt(interval.substring(0, comma), minPort); + // check if the comma is last (end port in range is missing) + int end = (comma == interval.length() - 1) ? maxPort : parseInt(interval.substring(comma + 1), maxPort); + // check for exclusive notation + if (startsWith == '(') + { + start++; + } + if (endsWith == ')') + { + end--; + } + // find a port in the requested range + int port = start - 1; + for (int i = start; port < start && i <= end; i++) + { + port = getSocketPort(i); + } + + return (port < start) ? dflt : port; + } + else + { + // We don't recognize the pattern as special, so try to parse it to an int + return parseInt(portProp, dflt); + } + } + } + + private int getSocketPort(int i) + { + int port = -1; + ServerSocket ss = null; + try + { + ss = new ServerSocket(i); + port = ss.getLocalPort(); + } + catch (IOException e) + { + SystemLogger.debug("Unable to bind to port: " + i); + } + finally + { + closeSilently(ss); + } + return port; + } + + private Object getProperty(final String name) + { + Dictionary conf = this.config; + Object value = (conf != null) ? conf.get(name) : null; + if (value == null) + { + value = this.context.getProperty(name); + } + return value; + } + + /** + * Get the property value as a string array. + * Empty values are filtered out - if the resulting array is empty + * the default value is returned. + */ + private String[] getStringArrayProperty(String name, String[] defValue) + { + Object value = getProperty(name); + if (value instanceof String) + { + final String stringVal = ((String) value).trim(); + if (stringVal.length() > 0) + { + return stringVal.split(","); + } + } + else if (value instanceof String[]) + { + final String[] stringArr = (String[]) value; + final List list = new ArrayList<>(); + for (final String stringVal : stringArr) + { + if (stringVal.trim().length() > 0) + { + list.add(stringVal.trim()); + } + } + if (list.size() > 0) + { + return list.toArray(new String[list.size()]); + } + } + else if (value instanceof Collection) + { + final ArrayList conv = new ArrayList<>(); + for (Iterator vi = ((Collection) value).iterator(); vi.hasNext();) + { + Object object = vi.next(); + if (object != null) + { + conv.add(String.valueOf(object)); + } + } + if (conv.size() > 0) + { + return conv.toArray(new String[conv.size()]); + } + } + return defValue; + } + + private int parseInt(String value, int dflt) + { + try + { + return Integer.parseInt(value); + } + catch (NumberFormatException e) + { + return dflt; + } + } + + private long parseLong(String value, long dflt) + { + try + { + return Long.parseLong(value); + } + catch (NumberFormatException e) + { + return dflt; + } + } +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyLogger.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyLogger.java new file mode 100644 index 00000000000..7227da665b6 --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyLogger.java @@ -0,0 +1,129 @@ +/* + * 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.http.jetty.internal; + +import java.text.MessageFormat; + +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.eclipse.jetty.util.log.Logger; + +public final class JettyLogger implements Logger +{ + private final String name; + private boolean debugEnabled; + + public JettyLogger() + { + this("org.eclipse.jetty.log"); + } + + public JettyLogger(String name) + { + this.name = name; + } + + public Logger getLogger(String name) + { + JettyLogger logger = new JettyLogger(name); + logger.setDebugEnabled(this.debugEnabled); + return logger; + } + + public boolean isDebugEnabled() + { + return this.debugEnabled; + } + + public void setDebugEnabled(boolean enabled) + { + this.debugEnabled = enabled; + } + + public void debug(Throwable throwable) + { + if (this.debugEnabled) + { + SystemLogger.debug(throwable.getMessage()); + } + + } + + public void debug(String msg, Object... args) + { + if (this.debugEnabled) + { + SystemLogger.debug(MessageFormat.format(msg, args)); + } + } + + @Override + public void debug(String msg, long value) + { + debug(msg, value); + } + + public void debug(String msg, Throwable throwable) + { + if (this.debugEnabled) + { + SystemLogger.debug(msg + ": " + throwable.getMessage()); + } + } + + public String getName() + { + return name; + } + + public void ignore(Throwable throwable) + { + + } + + public void info(Throwable throwable) + { + SystemLogger.info(throwable.getMessage()); + } + + public void info(String msg, Object... args) + { + SystemLogger.info(MessageFormat.format(msg, args)); + + } + + public void info(String msg, Throwable throwable) + { + SystemLogger.info(msg + ": " + throwable.getMessage()); + } + + public void warn(Throwable throwable) + { + SystemLogger.warning(null, throwable); + } + + public void warn(String msg, Object... args) + { + SystemLogger.warning(MessageFormat.format(msg, args), null); + + } + + public void warn(String msg, Throwable throwable) + { + SystemLogger.warning(msg, throwable); + } + +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyManagedService.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyManagedService.java new file mode 100644 index 00000000000..9fe84076a5d --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyManagedService.java @@ -0,0 +1,40 @@ +/* + * 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.http.jetty.internal; + +import java.util.Dictionary; + +import org.osgi.service.cm.ManagedService; + +public class JettyManagedService implements ManagedService +{ + + private final JettyService jettyService; + + JettyManagedService(final JettyService jettyService) + { + this.jettyService = jettyService; + } + + @Override + public void updated(Dictionary properties) + { + jettyService.updated(properties); + } +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyManagedServiceFactory.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyManagedServiceFactory.java new file mode 100644 index 00000000000..ec1702c660d --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyManagedServiceFactory.java @@ -0,0 +1,101 @@ +/* + * 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.http.jetty.internal; + +import java.io.Closeable; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.osgi.framework.BundleContext; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedServiceFactory; + +public class JettyManagedServiceFactory implements ManagedServiceFactory, Closeable +{ + private final Map services = new HashMap<>(); + private final BundleContext context; + + JettyManagedServiceFactory(final BundleContext context) + { + this.context = context; + } + + @Override + public synchronized void close() + { + final Set pids = new HashSet<>(services.keySet()); + for (final String pid : pids) + { + deleted(pid); + } + } + + @Override + public String getName() + { + return "Apache Felix Http Jetty"; + } + + @Override + public synchronized void updated(final String pid, final Dictionary properties) throws ConfigurationException + { + JettyServiceStarter jetty = services.get(pid); + + try + { + if (jetty == null) + { + jetty = new JettyServiceStarter(context, properties); + services.put(pid, jetty); + } + else + { + jetty.updated(properties); + } + } + catch (final Exception e) + { + SystemLogger.error("Failed to start Http Jetty pid=" + pid, e); + } + } + + @Override + public synchronized void deleted(String pid) + { + JettyServiceStarter jetty = services.remove(pid); + + if (jetty != null) + { + try + { + jetty.stop(); + } + catch (Exception e) + { + SystemLogger.error("Faiiled to stop Http Jetty pid=" + pid, e); + } + } + + } + +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java new file mode 100644 index 00000000000..8be47d8f647 --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java @@ -0,0 +1,1088 @@ +/* + * 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.http.jetty.internal; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +import javax.servlet.ServletContext; +import javax.servlet.SessionCookieConfig; +import javax.servlet.SessionTrackingMode; + +import org.apache.felix.http.base.internal.HttpServiceController; +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.io.ConnectionStatistics; +import org.eclipse.jetty.security.HashLoginService; +import org.eclipse.jetty.security.UserStore; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.server.handler.ContextHandlerCollection; +import org.eclipse.jetty.server.handler.StatisticsHandler; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.server.session.HouseKeeper; +import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.ThreadPool; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.http.runtime.HttpServiceRuntimeConstants; +import org.osgi.util.tracker.BundleTracker; +import org.osgi.util.tracker.BundleTrackerCustomizer; +import org.osgi.util.tracker.ServiceTracker; +import org.osgi.util.tracker.ServiceTrackerCustomizer; + +public final class JettyService extends AbstractLifeCycle.AbstractLifeCycleListener +{ + /** PID for configuration of the HTTP service. */ + public static final String PID = "org.apache.felix.http"; + + private static final String HEADER_WEB_CONTEXT_PATH = "Web-ContextPath"; + private static final String HEADER_ACTIVATION_POLICY = "Bundle-ActivationPolicy"; + private static final String WEB_SYMBOLIC_NAME = "osgi.web.symbolicname"; + private static final String WEB_VERSION = "osgi.web.version"; + private static final String WEB_CONTEXT_PATH = "osgi.web.contextpath"; + private static final String OSGI_BUNDLE_CONTEXT = "osgi-bundlecontext"; + + private final JettyConfig config; + private final BundleContext context; + private final HttpServiceController controller; + private final Map deployments; + private final ExecutorService executor; + + private volatile ServiceRegistration configServiceReg; + private volatile Server server; + private volatile ContextHandlerCollection parent; + private volatile MBeanServerTracker mbeanServerTracker; + private volatile BundleTracker bundleTracker; + private volatile ServiceTracker eventAdmintTracker; + private volatile ConnectorFactoryTracker connectorTracker; + private volatile RequestLogTracker requestLogTracker; + private volatile LogServiceRequestLog osgiRequestLog; + private volatile FileRequestLog fileRequestLog; + private volatile LoadBalancerCustomizerFactoryTracker loadBalancerCustomizerTracker; + private volatile CustomizerWrapper customizerWrapper; + private boolean registerManagedService = true; + private volatile Object eventAdmin; + private final String jettyVersion; + + public JettyService(final BundleContext context, + final HttpServiceController controller) + { + this.jettyVersion = fixJettyVersion(context); + + this.context = context; + this.config = new JettyConfig(this.context); + this.controller = controller; + this.deployments = new LinkedHashMap<>(); + this.executor = Executors.newSingleThreadExecutor(new ThreadFactory() + { + @Override + public Thread newThread(Runnable runnable) + { + Thread t = new Thread(runnable); + t.setName("Jetty HTTP Service"); + return t; + } + }); + } + + public JettyService(final BundleContext context, + final HttpServiceController controller, + final Dictionary props) + { + this(context, controller); + this.config.update(props); + this.registerManagedService = false; + } + + public void start() throws Exception + { + // FELIX-4422: start Jetty synchronously... + startJetty(); + + if (this.registerManagedService) { + final Dictionary props = new Hashtable<>(); + props.put(Constants.SERVICE_PID, PID); + props.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation"); + props.put(Constants.SERVICE_DESCRIPTION, "Managed Service for the Jetty Http Service"); + this.configServiceReg = this.context.registerService("org.osgi.service.cm.ManagedService", + new ServiceFactory() + { + + @Override + public Object getService(final Bundle bundle, final ServiceRegistration registration) + { + return new JettyManagedService(JettyService.this); + } + + @Override + public void ungetService(Bundle bundle, ServiceRegistration registration, Object service) + { + // nothing to do + } + }, props); + } + + // we use the class name as a String to make the dependency on event admin optional + this.eventAdmintTracker = new ServiceTracker<>(this.context, "org.osgi.service.event.EventAdmin", + new ServiceTrackerCustomizer() + { + @Override + public Object addingService(final ServiceReference reference) + { + final Object service = context.getService(reference); + eventAdmin = service; + return service; + } + + @Override + public void modifiedService(final ServiceReference reference, final Object service) + { + // nothing to do + } + + @Override + public void removedService(final ServiceReference reference, final Object service) + { + eventAdmin = null; + context.ungetService(reference); + } + }); + this.eventAdmintTracker.open(); + + this.bundleTracker = new BundleTracker<>(this.context, Bundle.ACTIVE | Bundle.STARTING, + new BundleTrackerCustomizer() { + + @Override + public Deployment addingBundle(Bundle bundle, BundleEvent event) + { + return detectWebAppBundle(bundle); + } + + @Override + public void modifiedBundle(Bundle bundle, BundleEvent event, Deployment object) + { + detectWebAppBundle(bundle); + } + + private Deployment detectWebAppBundle(Bundle bundle) + { + if (bundle.getState() == Bundle.ACTIVE || (bundle.getState() == Bundle.STARTING && "Lazy".equals(bundle.getHeaders().get(HEADER_ACTIVATION_POLICY)))) + { + + String contextPath = bundle.getHeaders().get(HEADER_WEB_CONTEXT_PATH); + if (contextPath != null) + { + return startWebAppBundle(bundle, contextPath); + } + } + return null; + } + + @Override + public void removedBundle(Bundle bundle, BundleEvent event, Deployment object) + { + String contextPath = bundle.getHeaders().get(HEADER_WEB_CONTEXT_PATH); + if (contextPath == null) + { + return; + } + + Deployment deployment = deployments.remove(contextPath); + if (deployment != null && deployment.getContext() != null) + { + // remove registration, since bundle is already stopping + deployment.setRegistration(null); + undeploy(deployment, deployment.getContext()); + } + } + + }); + this.bundleTracker.open(); + } + + public void stop() throws Exception + { + if (this.configServiceReg != null) + { + this.configServiceReg.unregister(); + this.configServiceReg = null; + } + if (this.bundleTracker != null) + { + this.bundleTracker.close(); + this.bundleTracker = null; + } + if (this.eventAdmintTracker != null) + { + this.eventAdmintTracker.close(); + this.eventAdmintTracker = null; + } + + // FELIX-4422: stop Jetty synchronously... + stopJetty(); + + if (isExecutorServiceAvailable()) + { + this.executor.shutdown(); + // FELIX-4423: make sure to await the termination of the executor... + this.executor.awaitTermination(5, TimeUnit.SECONDS); + } + } + + private Hashtable getServiceProperties() + { + Hashtable props = new Hashtable<>(); + // Add some important configuration properties... + this.config.setServiceProperties(props); + addEndpointProperties(props, null); + + // propagate the new service properties to the actual HTTP service... + return props; + } + + public void updated(final Dictionary props) + { + if (this.config.update(props)) + { + // Something changed in our configuration, restart Jetty... + stopJetty(); + startJetty(); + } + } + + private void startJetty() + { + try + { + initializeJetty(); + } + catch (Exception e) + { + SystemLogger.error("Exception while initializing Jetty.", e); + } + } + + private void stopJetty() + { + if (this.server != null) + { + this.controller.getEventDispatcher().setActive(false); + this.controller.unregister(); + + if (this.fileRequestLog != null) + { + this.fileRequestLog.stop(); + this.fileRequestLog = null; + } + if (this.osgiRequestLog != null) + { + this.osgiRequestLog.unregister(); + this.osgiRequestLog = null; + } + if (this.requestLogTracker != null) + { + this.requestLogTracker.close(); + this.requestLogTracker = null; + } + + if (this.connectorTracker != null) + { + this.connectorTracker.close(); + this.connectorTracker = null; + } + + if (this.loadBalancerCustomizerTracker != null) + { + this.loadBalancerCustomizerTracker.close(); + this.loadBalancerCustomizerTracker = null; + } + + try + { + this.server.stop(); + this.server = null; + SystemLogger.info("Stopped Jetty."); + } + catch (Exception e) + { + SystemLogger.error("Exception while stopping Jetty.", e); + } + + if (this.mbeanServerTracker != null) + { + this.mbeanServerTracker.close(); + this.mbeanServerTracker = null; + } + } + } + + private void initializeJetty() throws Exception + { + if (this.config.isUseHttp() || this.config.isUseHttps()) + { + + final int threadPoolMax = this.config.getThreadPoolMax(); + if (threadPoolMax >= 0) { + this.server = new Server( new QueuedThreadPool(threadPoolMax) ); + } else { + this.server = new Server(); + } + this.server.addLifeCycleListener(this); + + // FELIX-5931 : PropertyUserStore used as default by HashLoginService has changed in 9.4.12.v20180830 + // and fails without a config, therefore using plain UserStore + final HashLoginService loginService = new HashLoginService("OSGi HTTP Service Realm"); + loginService.setUserStore(new UserStore()); + this.server.addBean(loginService); + + this.parent = new ContextHandlerCollection(); + + ServletContextHandler context = new ServletContextHandler(this.parent, + this.config.getContextPath(), + ServletContextHandler.SESSIONS); + + configureSessionManager(context); + this.controller.getEventDispatcher().setActive(true); + context.addEventListener(controller.getEventDispatcher()); + context.getSessionHandler().addEventListener(controller.getEventDispatcher()); + + final ServletHolder holder = new ServletHolder(this.controller.createDispatcherServlet()); + holder.setAsyncSupported(true); + context.addServlet(holder, "/*"); + context.setMaxFormContentSize(this.config.getMaxFormSize()); + + if (this.config.isRegisterMBeans()) + { + this.mbeanServerTracker = new MBeanServerTracker(this.context, this.server); + this.mbeanServerTracker.open(); + context.addBean(new StatisticsHandler()); + } + + this.server.setHandler(this.parent); + + if (this.config.isGzipHandlerEnabled()) + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.setMinGzipSize(this.config.getGzipMinGzipSize()); + gzipHandler.setCompressionLevel(this.config.getGzipCompressionLevel()); + gzipHandler.setInflateBufferSize(this.config.getGzipInflateBufferSize()); + gzipHandler.setSyncFlush(this.config.isGzipSyncFlush()); + gzipHandler.addExcludedAgentPatterns(this.config.getGzipExcludedUserAgent()); + gzipHandler.addIncludedMethods(this.config.getGzipIncludedMethods()); + gzipHandler.addExcludedMethods(this.config.getGzipExcludedMethods()); + gzipHandler.addIncludedPaths(this.config.getGzipIncludedPaths()); + gzipHandler.addExcludedPaths(this.config.getGzipExcludedPaths()); + gzipHandler.addIncludedMimeTypes(this.config.getGzipIncludedMimeTypes()); + gzipHandler.addExcludedMimeTypes(this.config.getGzipExcludedMimeTypes()); + + this.server.insertHandler(gzipHandler); + } + + this.server.start(); + + // session id manager is only available after server is started + context.getSessionHandler().getSessionIdManager().getSessionHouseKeeper().setIntervalSec( + this.config.getLongProperty(JettyConfig.FELIX_JETTY_SESSION_SCAVENGING_INTERVAL, + HouseKeeper.DEFAULT_PERIOD_MS / 1000L)); + + if (this.config.isProxyLoadBalancerConnection()) + { + customizerWrapper = new CustomizerWrapper(); + this.loadBalancerCustomizerTracker = new LoadBalancerCustomizerFactoryTracker(this.context, customizerWrapper); + this.loadBalancerCustomizerTracker.open(); + } + + final StringBuilder message = new StringBuilder("Started Jetty ").append(this.jettyVersion).append(" at port(s)"); + if (this.config.isUseHttp() && initializeHttp()) + { + message.append(" HTTP:").append(this.config.getHttpPort()); + } + + if (this.config.isUseHttps() && initializeHttps()) + { + message.append(" HTTPS:").append(this.config.getHttpsPort()); + } + + this.connectorTracker = new ConnectorFactoryTracker(this.context, this.server); + this.connectorTracker.open(); + + if (this.server.getConnectors() != null && this.server.getConnectors().length > 0) + { + message.append(" on context path ").append(this.config.getContextPath()); + + message.append(" ["); + ThreadPool threadPool = this.server.getThreadPool(); + if (threadPool instanceof ThreadPool.SizedThreadPool) { + ThreadPool.SizedThreadPool sizedThreadPool = (ThreadPool.SizedThreadPool) threadPool; + message.append("minThreads=").append(sizedThreadPool.getMinThreads()).append(","); + message.append("maxThreads=").append(sizedThreadPool.getMaxThreads()).append(","); + } + Connector connector = this.server.getConnectors()[0]; + if (connector instanceof ServerConnector) { + @SuppressWarnings("resource") + ServerConnector serverConnector = (ServerConnector) connector; + message.append("acceptors=").append(serverConnector.getAcceptors()).append(","); + message.append("selectors=").append(serverConnector.getSelectorManager().getSelectorCount()); + } + message.append("]"); + + SystemLogger.info(message.toString()); + this.controller.register(context.getServletContext(), getServiceProperties()); + } + else + { + this.stopJetty(); + SystemLogger.error("Jetty stopped (no connectors available)", null); + } + + try { + this.requestLogTracker = new RequestLogTracker(this.context, this.config.getRequestLogFilter()); + this.requestLogTracker.open(); + this.server.setRequestLog(requestLogTracker); + } catch (InvalidSyntaxException e) { + SystemLogger.error("Invalid filter syntax in request log tracker", e); + } + + if (this.config.isRequestLogOSGiEnabled()) { + this.osgiRequestLog = new LogServiceRequestLog(this.config); + this.osgiRequestLog.register(this.context); + SystemLogger.info("Directing Jetty request logs to the OSGi Log Service"); + } + + if (this.config.getRequestLogFilePath() != null && !this.config.getRequestLogFilePath().isEmpty()) { + this.fileRequestLog = new FileRequestLog(config); + this.fileRequestLog.start(this.context); + SystemLogger.info("Directing Jetty request logs to " + this.config.getRequestLogFilePath()); + } + } + else + { + SystemLogger.warning("Jetty not started (HTTP and HTTPS disabled)", null); + } + } + + private static String fixJettyVersion(final BundleContext ctx) + { + // FELIX-4311: report the real version of Jetty... + final Dictionary headers = ctx.getBundle().getHeaders(); + String version = headers.get("X-Jetty-Version"); + if (version != null) + { + System.setProperty("jetty.version", version); + } + else + { + version = Server.getVersion(); + } + return version; + } + + private boolean initializeHttp() + { + HttpConnectionFactory connFactory = new HttpConnectionFactory(); + configureHttpConnectionFactory(connFactory); + + ServerConnector connector = new ServerConnector( + server, + config.getAcceptors(), + config.getSelectors(), + connFactory + ); + + configureConnector(connector, this.config.getHttpPort()); + + if (this.config.isProxyLoadBalancerConnection()) + { + connFactory.getHttpConfiguration().addCustomizer(customizerWrapper); + } + return startConnector(connector); + } + + private boolean initializeHttps() + { + HttpConnectionFactory connFactory = new HttpConnectionFactory(); + configureHttpConnectionFactory(connFactory); + + SslContextFactory sslContextFactory = new SslContextFactory(); + configureSslContextFactory(sslContextFactory); + + ServerConnector connector = new ServerConnector( + server, + config.getAcceptors(), + config.getSelectors(), + new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.toString()), + connFactory + ); + + HttpConfiguration httpConfiguration = connFactory.getHttpConfiguration(); + httpConfiguration.addCustomizer(new SecureRequestCustomizer()); + + if (this.config.isProxyLoadBalancerConnection()) + { + httpConfiguration.addCustomizer(customizerWrapper); + } + + configureConnector(connector, this.config.getHttpsPort()); + return startConnector(connector); + } + + private void configureSslContextFactory(final SslContextFactory connector) + { + if (this.config.getKeystoreType() != null) + { + connector.setKeyStoreType(this.config.getKeystoreType()); + } + + if (this.config.getKeystore() != null) + { + connector.setKeyStorePath(this.config.getKeystore()); + } + + if (this.config.getPassword() != null) + { + connector.setKeyStorePassword(this.config.getPassword()); + } + + if (this.config.getKeyPassword() != null) + { + connector.setKeyManagerPassword(this.config.getKeyPassword()); + } + + if (this.config.getTruststoreType() != null) + { + connector.setTrustStoreType(this.config.getTruststoreType()); + } + + if (this.config.getTruststore() != null) + { + connector.setTrustStorePath(this.config.getTruststore()); + } + + if (this.config.getTrustPassword() != null) + { + connector.setTrustStorePassword(this.config.getTrustPassword()); + } + + if ("wants".equalsIgnoreCase(this.config.getClientcert())) + { + connector.setWantClientAuth(true); + } + else if ("needs".equalsIgnoreCase(this.config.getClientcert())) + { + connector.setNeedClientAuth(true); + } + + if (this.config.getExcludedCipherSuites() != null) + { + connector.setExcludeCipherSuites(this.config.getExcludedCipherSuites()); + } + + if (this.config.getIncludedCipherSuites() != null) + { + connector.setIncludeCipherSuites(this.config.getIncludedCipherSuites()); + } + + if (this.config.getIncludedProtocols() != null) + { + connector.setIncludeProtocols(this.config.getIncludedProtocols()); + } + + if (this.config.getExcludedProtocols() != null) + { + connector.setExcludeProtocols(this.config.getExcludedProtocols()); + } + + connector.setRenegotiationAllowed(this.config.isRenegotiationAllowed()); + } + + private void configureConnector(final ServerConnector connector, int port) + { + connector.setPort(port); + connector.setHost(this.config.getHost()); + connector.setIdleTimeout(this.config.getHttpTimeout()); + + if (this.config.isRegisterMBeans()) + { + connector.addBean(new ConnectionStatistics()); + } + } + + private void configureHttpConnectionFactory(HttpConnectionFactory connFactory) + { + HttpConfiguration config = connFactory.getHttpConfiguration(); + config.setRequestHeaderSize(this.config.getHeaderSize()); + config.setResponseHeaderSize(this.config.getHeaderSize()); + config.setOutputBufferSize(this.config.getResponseBufferSize()); + + // HTTP/1.1 requires Date header if possible (it is) + config.setSendDateHeader(true); + config.setSendServerVersion(this.config.isSendServerHeader()); + config.setSendXPoweredBy(this.config.isSendServerHeader()); + + connFactory.setInputBufferSize(this.config.getRequestBufferSize()); + } + + private void configureSessionManager(final ServletContextHandler context) throws Exception + { + final SessionHandler sessionHandler = context.getSessionHandler(); + sessionHandler.setMaxInactiveInterval(this.config.getSessionTimeout() * 60); + sessionHandler.setSessionIdPathParameterName(this.config.getProperty(JettyConfig.FELIX_JETTY_SERVLET_SESSION_ID_PATH_PARAMETER_NAME, SessionHandler.__DefaultSessionIdPathParameterName)); + sessionHandler.setCheckingRemoteSessionIdEncoding(this.config.getBooleanProperty(JettyConfig.FELIX_JETTY_SERVLET_CHECK_REMOTE_SESSION_ENCODING, true)); + sessionHandler.setSessionTrackingModes(Collections.singleton(SessionTrackingMode.COOKIE)); + + final SessionCookieConfig cookieConfig = sessionHandler.getSessionCookieConfig(); + cookieConfig.setName(this.config.getProperty(JettyConfig.FELIX_JETTY_SERVLET_SESSION_COOKIE_NAME, SessionHandler.__DefaultSessionCookie)); + cookieConfig.setDomain(this.config.getProperty(JettyConfig.FELIX_JETTY_SERVLET_SESSION_DOMAIN, SessionHandler.__DefaultSessionDomain)); + cookieConfig.setPath(this.config.getProperty(JettyConfig.FELIX_JETTY_SERVLET_SESSION_PATH, context.getContextPath())); + cookieConfig.setMaxAge(this.config.getIntProperty(JettyConfig.FELIX_JETTY_SERVLET_SESSION_MAX_AGE, -1)); + cookieConfig.setHttpOnly(this.config.getBooleanProperty(JettyConfig.FELIX_JETTY_SESSION_COOKIE_HTTP_ONLY, true)); + cookieConfig.setSecure(this.config.getBooleanProperty(JettyConfig.FELIX_JETTY_SESSION_COOKIE_SECURE, false)); + } + + private boolean startConnector(Connector connector) + { + this.server.addConnector(connector); + try + { + connector.start(); + return true; + } + catch (Exception e) + { + this.server.removeConnector(connector); + SystemLogger.error("Failed to start Connector: " + connector, e); + } + + return false; + } + + private String getEndpoint(final Connector listener, final InetAddress ia) + { + if (ia.isLoopbackAddress()) + { + return null; + } + + String address = ia.getHostAddress().trim().toLowerCase(); + if (ia instanceof Inet6Address) + { + // skip link-local + if (address.startsWith("fe80:0:0:0:")) + { + return null; + } + address = "[" + address + "]"; + } + else if (!(ia instanceof Inet4Address)) + { + return null; + } + + return getEndpoint(listener, address); + } + + private ServerConnector getServerConnector(Connector connector) + { + if (connector instanceof ServerConnector) + { + return (ServerConnector) connector; + } + throw new IllegalArgumentException("Connection instance not of type ServerConnector " + connector); + } + + private String getEndpoint(final Connector listener, final String hostname) + { + final StringBuilder sb = new StringBuilder(); + sb.append("http"); + int defaultPort = 80; + //SslConnectionFactory protocol is SSL-HTTP1.0 + if (getServerConnector(listener).getDefaultProtocol().startsWith("SSL")) + { + sb.append('s'); + defaultPort = 443; + } + sb.append("://"); + sb.append(hostname); + if (getServerConnector(listener).getPort() != defaultPort) + { + sb.append(':'); + sb.append(String.valueOf(getServerConnector(listener).getPort())); + } + sb.append(config.getContextPath()); + + return sb.toString(); + } + + private List getEndpoints(final Connector connector, final List interfaces) + { + final List endpoints = new ArrayList<>(); + for (final NetworkInterface ni : interfaces) + { + final Enumeration ias = ni.getInetAddresses(); + while (ias.hasMoreElements()) + { + final InetAddress ia = ias.nextElement(); + final String endpoint = this.getEndpoint(connector, ia); + if (endpoint != null) + { + endpoints.add(endpoint); + } + } + } + return endpoints; + } + + private void addEndpointProperties(final Hashtable props, Object container) + { + final List endpoints = new ArrayList<>(); + + final Connector[] connectors = this.server.getConnectors(); + if (connectors != null) + { + for (int i = 0; i < connectors.length; i++) + { + final Connector connector = connectors[i]; + + if (getServerConnector(connector).getHost() == null || "0.0.0.0".equals(getServerConnector(connector).getHost())) + { + try + { + final List interfaces = new ArrayList<>(); + final List loopBackInterfaces = new ArrayList<>(); + final Enumeration nis = NetworkInterface.getNetworkInterfaces(); + while (nis.hasMoreElements()) + { + final NetworkInterface ni = nis.nextElement(); + if (ni.isLoopback()) + { + loopBackInterfaces.add(ni); + } + else + { + interfaces.add(ni); + } + } + + // only add loop back endpoints to the endpoint property if no other endpoint is available. + if (!interfaces.isEmpty()) + { + endpoints.addAll(getEndpoints(connector, interfaces)); + } + else + { + endpoints.addAll(getEndpoints(connector, loopBackInterfaces)); + } + } + catch (final SocketException se) + { + // we ignore this + } + } + else + { + final String endpoint = this.getEndpoint(connector, getServerConnector(connector).getHost()); + if (endpoint != null) + { + endpoints.add(endpoint); + } + } + } + } + props.put(HttpServiceRuntimeConstants.HTTP_SERVICE_ENDPOINT, + endpoints.toArray(new String[endpoints.size()])); + } + + private Deployment startWebAppBundle(Bundle bundle, String contextPath) + { + postEvent(WebEvent.TOPIC_DEPLOYING, bundle, this.context.getBundle(), null, null, null); + + // check existing deployments + Deployment deployment = this.deployments.get(contextPath); + if (deployment != null) + { + SystemLogger.warning(String.format("Web application bundle %s has context path %s which is already registered", bundle.getSymbolicName(), contextPath), null); + postEvent(WebEvent.TOPIC_FAILED, bundle, this.context.getBundle(), null, contextPath, deployment.getBundle().getBundleId()); + return null; + } + + // check context path belonging to Http Service implementation + if (contextPath.equals("/")) + { + SystemLogger.warning(String.format("Web application bundle %s has context path %s which is reserved", bundle.getSymbolicName(), contextPath), null); + postEvent(WebEvent.TOPIC_FAILED, bundle, this.context.getBundle(), null, contextPath, this.context.getBundle().getBundleId()); + return null; + } + + // check against excluded paths + for (String path : this.config.getPathExclusions()) + { + if (contextPath.startsWith(path)) + { + SystemLogger.warning(String.format("Web application bundle %s has context path %s which clashes with excluded path prefix %s", bundle.getSymbolicName(), contextPath, path), null); + postEvent(WebEvent.TOPIC_FAILED, bundle, this.context.getBundle(), null, path, null); + return null; + } + } + + deployment = new Deployment(contextPath, bundle); + this.deployments.put(contextPath, deployment); + + WebAppBundleContext context = new WebAppBundleContext(contextPath, bundle, this.getClass().getClassLoader()); + deploy(deployment, context); + return deployment; + } + + public void deploy(final Deployment deployment, final WebAppBundleContext context) + { + if (!isExecutorServiceAvailable()) + { + // Shutting down...? + return; + } + + this.executor.submit(new JettyOperation() + { + @Override + protected void doExecute() + { + final Bundle webAppBundle = deployment.getBundle(); + final Bundle extenderBundle = JettyService.this.context.getBundle(); + + try + { + context.getServletContext().setAttribute(OSGI_BUNDLE_CONTEXT, webAppBundle.getBundleContext()); + + JettyService.this.parent.addHandler(context); + context.start(); + + Dictionary props = new Hashtable<>(); + props.put(WEB_SYMBOLIC_NAME, webAppBundle.getSymbolicName()); + props.put(WEB_VERSION, webAppBundle.getVersion()); + props.put(WEB_CONTEXT_PATH, deployment.getContextPath()); + deployment.setRegistration(webAppBundle.getBundleContext().registerService(ServletContext.class, context.getServletContext(), props)); + + postEvent(WebEvent.TOPIC_DEPLOYED, webAppBundle, extenderBundle, null, null, null); + } + catch (Exception e) + { + SystemLogger.error(String.format("Deploying web application bundle %s failed.", webAppBundle.getSymbolicName()), e); + postEvent(WebEvent.TOPIC_FAILED, webAppBundle, extenderBundle, e, null, null); + deployment.setContext(null); + } + } + }); + deployment.setContext(context); + } + + public void undeploy(final Deployment deployment, final WebAppBundleContext context) + { + if (!isExecutorServiceAvailable()) + { + // Already stopped...? + return; + } + + this.executor.submit(new JettyOperation() + { + @Override + protected void doExecute() + { + final Bundle webAppBundle = deployment.getBundle(); + final Bundle extenderBundle = JettyService.this.context.getBundle(); + + try + { + postEvent(WebEvent.TOPIC_UNDEPLOYING, webAppBundle, extenderBundle, null, null, null); + + context.getServletContext().removeAttribute(OSGI_BUNDLE_CONTEXT); + + ServiceRegistration registration = deployment.getRegistration(); + if (registration != null) + { + registration.unregister(); + } + deployment.setRegistration(null); + deployment.setContext(null); + context.stop(); + } + catch (Exception e) + { + SystemLogger.error(String.format("Undeploying web application bundle %s failed.", webAppBundle.getSymbolicName()), e); + } + finally + { + postEvent(WebEvent.TOPIC_UNDEPLOYED, webAppBundle, extenderBundle, null, null, null); + } + } + }); + } + + private void postEvent(final String topic, + final Bundle webAppBundle, + final Bundle extenderBundle, + final Throwable exception, + final String collision, + final Long collisionBundles) + { + final Object ea = this.eventAdmin; + if (ea != null) + { + WebEvent.postEvent(ea, topic, webAppBundle, extenderBundle, exception, collision, collisionBundles); + } + } + + @Override + public void lifeCycleStarted(LifeCycle event) + { + for (Deployment deployment : this.deployments.values()) + { + if (deployment.getContext() == null) + { + postEvent(WebEvent.TOPIC_DEPLOYING, deployment.getBundle(), this.context.getBundle(), null, null, null); + WebAppBundleContext context = new WebAppBundleContext(deployment.getContextPath(), deployment.getBundle(), this.getClass().getClassLoader()); + deploy(deployment, context); + } + } + } + + @Override + public void lifeCycleStopping(LifeCycle event) + { + for (Deployment deployment : this.deployments.values()) + { + if (deployment.getContext() != null) + { + undeploy(deployment, deployment.getContext()); + } + } + } + + /** + * A deployment represents a web application bundle that may or may not be deployed. + */ + static class Deployment + { + private String contextPath; + private Bundle bundle; + private WebAppBundleContext context; + private ServiceRegistration registration; + + public Deployment(String contextPath, Bundle bundle) + { + this.contextPath = contextPath; + this.bundle = bundle; + } + + public Bundle getBundle() + { + return this.bundle; + } + + public String getContextPath() + { + return this.contextPath; + } + + public WebAppBundleContext getContext() + { + return this.context; + } + + public void setContext(WebAppBundleContext context) + { + this.context = context; + } + + public ServiceRegistration getRegistration() + { + return this.registration; + } + + public void setRegistration(ServiceRegistration registration) + { + this.registration = registration; + } + } + + /** + * A Jetty operation is executed with the context class loader set to this class's + * class loader. + */ + abstract static class JettyOperation implements Callable + { + @Override + public Void call() throws Exception + { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + + try + { + Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader()); + doExecute(); + return null; + } + finally + { + Thread.currentThread().setContextClassLoader(cl); + } + } + + protected abstract void doExecute() throws Exception; + } + + /** + * @return true if there is a valid executor service available, false otherwise. + */ + private boolean isExecutorServiceAvailable() + { + return this.executor != null && !this.executor.isShutdown(); + } +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyServiceStarter.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyServiceStarter.java new file mode 100644 index 00000000000..c61a9c282ed --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyServiceStarter.java @@ -0,0 +1,50 @@ +/* + * 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.http.jetty.internal; + +import java.util.Dictionary; + +import org.apache.felix.http.base.internal.HttpServiceController; +import org.osgi.framework.BundleContext; + +public class JettyServiceStarter +{ + + private final HttpServiceController controller; + private final JettyService jetty; + + public JettyServiceStarter(final BundleContext context, final Dictionary properties) + throws Exception + { + this.controller = new HttpServiceController(context); + this.jetty = new JettyService(context, this.controller, properties); + this.jetty.start(); + } + + public void stop() throws Exception + { + this.jetty.stop(); + this.controller.stop(); + } + + public void updated(final Dictionary properties) throws Exception + { + this.jetty.updated(properties); + } +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LoadBalancerCustomizerFactoryTracker.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LoadBalancerCustomizerFactoryTracker.java new file mode 100644 index 00000000000..8ec3a0989c8 --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LoadBalancerCustomizerFactoryTracker.java @@ -0,0 +1,146 @@ +/* + * 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.http.jetty.internal; + +import java.util.SortedSet; +import java.util.TreeSet; + +import org.apache.felix.http.jetty.LoadBalancerCustomizerFactory; +import org.eclipse.jetty.server.HttpConfiguration.Customizer; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; + +public class LoadBalancerCustomizerFactoryTracker extends ServiceTracker> +{ + + private final CustomizerWrapper customizerWrapper; + private final BundleContext bundleContext; + private final SortedSet> set = new TreeSet>(); + + public LoadBalancerCustomizerFactoryTracker(final BundleContext context, final CustomizerWrapper customizerWrapper) + { + super(context, LoadBalancerCustomizerFactory.class.getName(), null); + this.bundleContext = context; + this.customizerWrapper = customizerWrapper; + } + + @Override + public ServiceReference addingService(final ServiceReference reference) + { + final ServiceReference highestReference; + synchronized (set) + { + set.add(reference); + highestReference = set.last(); + } + + // only change if service is higher + if ( highestReference.compareTo(reference) == 0 ) + { + boolean updated = false; + final LoadBalancerCustomizerFactory factory = bundleContext.getService(reference); + if (factory != null) + { + final Customizer customizer = factory.createCustomizer(); + if ( customizer != null ) + { + customizerWrapper.setCustomizer(customizer); + updated = true; + } + else + { + bundleContext.ungetService(reference); + } + } + if ( !updated) + { + // we can't get the service, remove reference + synchronized (set) + { + set.remove(reference); + } + return null; + } + } + return reference; + } + + @Override + public void removedService(final ServiceReference reference, final ServiceReference service) + { + final boolean change; + ServiceReference highestReference = null; + synchronized (set) + { + if (set.isEmpty()) + { + change = false; + } + else + { + change = set.last().compareTo(reference) == 0; + } + set.remove(reference); + highestReference = (set.isEmpty() ? null : set.last()); + } + + if (change) + { + boolean done = false; + + do + { + // update the customizer Wrapper + if ( highestReference == null ) + { + customizerWrapper.setCustomizer(null); + done = true; + } + else + { + final LoadBalancerCustomizerFactory factory = bundleContext.getService(highestReference); + if (factory != null) + { + final Customizer customizer = factory.createCustomizer(); + if ( customizer != null ) + { + customizerWrapper.setCustomizer(customizer); + done = true; + } + else + { + bundleContext.ungetService(highestReference); + } + } + + if ( !done ) + { + synchronized ( set ) + { + set.remove(highestReference); + highestReference = (set.isEmpty() ? null : set.last()); + } + } + } + } while ( !done); + bundleContext.ungetService(reference); + } + } +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java new file mode 100644 index 00000000000..05df9c54805 --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java @@ -0,0 +1,77 @@ +/* + * 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.http.jetty.internal; + +import java.io.IOException; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.eclipse.jetty.server.AbstractNCSARequestLog; +import org.eclipse.jetty.server.RequestLog; +import org.eclipse.jetty.server.RequestLogWriter; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; + +/** + * A RequestLog that logs to the OSGi LogService when present. Not registered by default. + */ +class LogServiceRequestLog extends AbstractNCSARequestLog { + + public static final String SVC_PROP_NAME = "name"; + public static final String DEFAULT_NAME = "osgi"; + public static final String PREFIX = "REQUEST: "; + + private final String serviceName; + + private ServiceRegistration registration; + + LogServiceRequestLog(JettyConfig config) { + super(new RequestLogWriter()); + this.serviceName = config.getRequestLogOSGiServiceName(); + } + + public synchronized void register(BundleContext context) throws IllegalStateException { + if (registration != null) { + throw new IllegalStateException(getClass().getSimpleName() + " already registered"); + } + Dictionary svcProps = new Hashtable<>(); + svcProps.put(SVC_PROP_NAME, serviceName); + this.registration = context.registerService(RequestLog.class, this, svcProps); + } + + public synchronized void unregister() { + try { + if (registration != null) { + registration.unregister(); + } + } finally { + registration = null; + } + } + + @Override + public void write(String s) throws IOException { + SystemLogger.info(PREFIX + s); + } + + @Override + protected boolean isEnabled() { + return true; + } + +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/MBeanServerTracker.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/MBeanServerTracker.java new file mode 100644 index 00000000000..4f1912d90f2 --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/MBeanServerTracker.java @@ -0,0 +1,66 @@ +/* + * 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.http.jetty.internal; + +import javax.management.MBeanServer; + +import org.eclipse.jetty.jmx.MBeanContainer; +import org.eclipse.jetty.server.Server; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; + +public class MBeanServerTracker extends ServiceTracker +{ + + private final Server server; + + public MBeanServerTracker(final BundleContext context, final Server server) + { + super(context, MBeanServer.class, null); + this.server = server; + } + + @Override + public MBeanContainer addingService(ServiceReference reference) + { + MBeanServer server = (MBeanServer) super.addingService(reference); + if ( server != null ) + { + MBeanContainer mBeanContainer = new MBeanContainer(server); + this.server.addEventListener(mBeanContainer); + return mBeanContainer; + } + else + { + super.removedService(reference, null); + } + return null; + } + + @Override + public void removedService(ServiceReference reference, MBeanContainer service) + { + if ( service != null ) + { + this.server.removeEventListener(service); + super.removedService(reference, service); + } + } +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java new file mode 100644 index 00000000000..62f4d90674c --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java @@ -0,0 +1,106 @@ +/* + * 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.http.jetty.internal; + +import org.apache.felix.http.base.internal.logger.SystemLogger; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.RequestLog; +import org.eclipse.jetty.server.Response; +import org.osgi.framework.*; +import org.osgi.util.tracker.ServiceTracker; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * An instance of Jetty's RequestLog that dispatches to registered RequestLog services in the service registry. A filter + * can be provided so that it only dispatches to selected services. + *

      + * Unchecked exceptions from the RequestLog services are caught and logged to the OSGi LogService. to avoid flooding the + * LogService, we will remove a RequestLog service if it breaches a maximum number of errors (see {@link + * RequestLogTracker#MAX_ERROR_COUNT}). Once this happens we will stop dispatching to that service entirely until it is + * unregistered. + */ +class RequestLogTracker extends ServiceTracker implements RequestLog { + + private static final int MAX_ERROR_COUNT = 100; + + private final ConcurrentMap, RequestLog> logSvcs = new ConcurrentHashMap<>(); + private final ConcurrentMap, Integer> naughtyStep = new ConcurrentHashMap<>(); + + RequestLogTracker(BundleContext context, String filter) throws InvalidSyntaxException { + super(context, buildFilter(filter), null); + } + + private static Filter buildFilter(String inputFilter) throws InvalidSyntaxException { + String objectClassFilter = String.format("(%s=%s)", Constants.OBJECTCLASS, RequestLog.class.getName()); + String compositeFilter; + if (inputFilter != null) { + // Parse the input filter just for validation before we insert into ours. + FrameworkUtil.createFilter(inputFilter); + compositeFilter = "(&" + objectClassFilter + inputFilter + ")"; + } else { + compositeFilter = objectClassFilter; + } + return FrameworkUtil.createFilter(compositeFilter); + } + + @Override + public RequestLog addingService(ServiceReference reference) { + RequestLog logSvc = context.getService(reference); + logSvcs.put(reference, logSvc); + return logSvc; + } + + @Override + public void removedService(ServiceReference reference, RequestLog logSvc) { + logSvcs.remove(reference); + naughtyStep.remove(reference); + context.ungetService(reference); + } + + @Override + public void log(Request request, Response response) { + for (Map.Entry, RequestLog> entry : logSvcs.entrySet()) { + try { + entry.getValue().log(request, response); + } catch (Exception e) { + processError(entry.getKey(), e); + } + } + } + + /** + * Process an exception from a RequestLog service instance, and remove the service if it has reached the maximum + * error limit. + */ + private void processError(ServiceReference reference, Exception e) { + SystemLogger.error(reference, String.format("Error dispatching to request log service ID %d from bundle %s:%s", + reference.getProperty(Constants.SERVICE_ID), reference.getBundle().getSymbolicName(), reference.getBundle().getVersion()), e); + + int naughty = naughtyStep.merge(reference, 1, Integer::sum); + if (naughty >= MAX_ERROR_COUNT) { + // We have reached (but not exceeded) the maximum, and the last error has been logged. Remove from the maps + // so we will not invoke the service again. + logSvcs.remove(reference); + naughtyStep.remove(reference); + SystemLogger.error(reference, String.format("RequestLog service ID %d from bundle %s:%s threw too many errors, it will no longer be invoked.", + reference.getProperty(Constants.SERVICE_ID), reference.getBundle().getSymbolicName(), reference.getBundle().getVersion()), null); + } + } +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/WebAppBundleContext.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/WebAppBundleContext.java new file mode 100644 index 00000000000..0c9b4f964da --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/WebAppBundleContext.java @@ -0,0 +1,141 @@ +/* + * 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.http.jetty.internal; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Enumeration; + +import org.eclipse.jetty.util.URIUtil; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.resource.URLResource; +import org.eclipse.jetty.webapp.WebAppContext; +import org.osgi.framework.Bundle; + +class WebAppBundleContext extends WebAppContext +{ + public WebAppBundleContext(String contextPath, final Bundle bundle, final ClassLoader parent) + { + super(null, contextPath.substring(1), contextPath); + + this.setBaseResource(new BundleURLResource(bundle.getEntry("/"))); + this.setClassLoader(new ClassLoader(parent) + { + @Override + protected Class findClass(String s) throws ClassNotFoundException + { + // Don't try to load classes from the bundle when it is not active + if (bundle.getState() == Bundle.ACTIVE) + { + try + { + return bundle.loadClass(s); + } + catch (ClassNotFoundException e) + { + } + } + return super.findClass(s); + } + + @Override + protected URL findResource(String name) + { + // Don't try to load resources from the bundle when it is not active + if (bundle.getState() == Bundle.ACTIVE) + { + URL url = bundle.getResource(name); + if (url != null) + { + return url; + } + } + return super.findResource(name); + } + + @Override + protected Enumeration findResources(String name) throws IOException + { + // Don't try to load resources from the bundle when it is not active + if (bundle.getState() == Bundle.ACTIVE) + { + Enumeration urls = bundle.getResources(name); + if (urls != null) + { + return urls; + } + } + return super.findResources(name); + } + }); + this.setThrowUnavailableOnStartupException(true); + } + + @Override + public Resource newResource(URL url) throws IOException + { + if (url == null) + { + return null; + } + return new BundleURLResource(url); + } + + static class BundleURLResource extends URLResource + { + BundleURLResource(URL url) + { + super(url, null); + } + + @Override + public synchronized void close() + { + if (this._in != null) + { + // Do not close this input stream: it would invalidate + // the associated zipfile's inflater and every future access + // to some bundle entry leads to an NPE with message + // "Inflater has been closed" + this._in = null; + } + super.close(); + } + + @Override + public Resource addPath(String path) throws MalformedURLException + { + if (path == null) + { + return null; + } + path = URIUtil.canonicalPath(path); + + URL url = new URL(URIUtil.addPaths(this._url.toExternalForm(), path)); + return new BundleURLResource(url); + } + + @Override + public File getFile() throws IOException + { + // not available as a file + return null; + } + } +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/WebEvent.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/WebEvent.java new file mode 100644 index 00000000000..f60dd735075 --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/WebEvent.java @@ -0,0 +1,87 @@ +/* + * 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.http.jetty.internal; + +import java.util.Dictionary; +import java.util.Hashtable; + +import org.osgi.framework.Bundle; +import org.osgi.service.event.Event; +import org.osgi.service.event.EventAdmin; +import org.osgi.service.event.EventConstants; + +public abstract class WebEvent +{ + private static final String TOPIC_WEB_EVENT = "org/osgi/service/web"; + public static final String TOPIC_DEPLOYING = TOPIC_WEB_EVENT + "/DEPLOYING"; + public static final String TOPIC_DEPLOYED = TOPIC_WEB_EVENT + "/DEPLOYED"; + public static final String TOPIC_UNDEPLOYING = TOPIC_WEB_EVENT + "/UNDEPLOYING"; + public static final String TOPIC_UNDEPLOYED = TOPIC_WEB_EVENT + "/UNDEPLOYED"; + public static final String TOPIC_FAILED = TOPIC_WEB_EVENT + "/FAILED"; + + private static final String CONTEXT_PATH = "context.path"; + private static final String EXCEPTION = "exception"; + private static final String COLLISION = "collision"; + private static final String COLLISION_BUNDLES = "collision.bundles"; + + private static final String EXTENDER_BUNDLE = "extender." + EventConstants.BUNDLE; + private static final String EXTENDER_BUNDLE_ID = "extender." + EventConstants.BUNDLE_ID; + private static final String EXTENDER_BUNDLE_VERSION = "extender." + EventConstants.BUNDLE_VERSION; + private static final String EXTENDER_BUNDLE_SYMBOLICNAME = "extender." + EventConstants.BUNDLE_SYMBOLICNAME; + + private static final String HEADER_WEB_CONTEXT_PATH = "Web-ContextPath"; + + private static Dictionary createBaseProperties(final Bundle webAppBundle, final Bundle extenderBundle) + { + Dictionary props = new Hashtable<>(); + props.put(EventConstants.BUNDLE_SYMBOLICNAME, webAppBundle.getSymbolicName()); + props.put(EventConstants.BUNDLE_ID, webAppBundle.getBundleId()); + props.put(EventConstants.BUNDLE, webAppBundle); + props.put(EventConstants.BUNDLE_VERSION, webAppBundle.getVersion()); + props.put(CONTEXT_PATH, webAppBundle.getHeaders().get(HEADER_WEB_CONTEXT_PATH)); + props.put(EventConstants.TIMESTAMP, System.currentTimeMillis()); + props.put(EXTENDER_BUNDLE, extenderBundle); + props.put(EXTENDER_BUNDLE_ID, extenderBundle.getBundleId()); + props.put(EXTENDER_BUNDLE_SYMBOLICNAME, extenderBundle.getSymbolicName()); + props.put(EXTENDER_BUNDLE_VERSION, extenderBundle.getVersion()); + return props; + } + + public static void postEvent(final Object eventAdmin, + final String topic, + final Bundle webAppBundle, + final Bundle extenderBundle, + final Throwable exception, + final String collision, + final Long collisionBundles) { + final Dictionary props = createBaseProperties(webAppBundle, extenderBundle); + if (exception != null) + { + props.put(EXCEPTION, exception); + } + if (collision != null) + { + props.put(COLLISION, collision); + } + if (collisionBundles != null) + { + props.put(COLLISION_BUNDLES, collisionBundles); + } + final Event event = new Event(topic, props); + ((EventAdmin)eventAdmin).postEvent(event); + } +} diff --git a/http/jetty/src/main/java/org/apache/felix/http/jetty/package-info.java b/http/jetty/src/main/java/org/apache/felix/http/jetty/package-info.java new file mode 100644 index 00000000000..77d7dfe9f72 --- /dev/null +++ b/http/jetty/src/main/java/org/apache/felix/http/jetty/package-info.java @@ -0,0 +1,22 @@ +/* + * 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. + */ +@Version("2.1") +package org.apache.felix.http.jetty; + +import org.osgi.annotation.versioning.Version; \ No newline at end of file diff --git a/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/JettyConfigTest.java b/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/JettyConfigTest.java new file mode 100644 index 00000000000..e0e7bcc269d --- /dev/null +++ b/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/JettyConfigTest.java @@ -0,0 +1,158 @@ +/* + * 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.http.jetty.internal; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.osgi.framework.BundleContext; + +/** + * Unit test for JettyConfig + */ +public class JettyConfigTest +{ + JettyConfig config; + BundleContext context; + + @Test public void testGetDefaultPort() + { + assertEquals("HTTP port", 8080, this.config.getHttpPort()); + assertEquals("HTTPS port", 8443, this.config.getHttpsPort()); + } + + @Test public void testGetPortInRange() + { + Hashtable props = new Hashtable<>(); + props.put("org.osgi.service.http.port", "[8000,9000]"); + props.put("org.osgi.service.http.port.secure", "[10000,11000)"); + this.config.update(props); + + assertTrue(this.config.getHttpPort() >= 8000 && this.config.getHttpPort() <= 9000); + assertTrue(this.config.getHttpsPort() >= 10000 && this.config.getHttpsPort() < 11000); + + props.put("org.osgi.service.http.port", "(12000,13000]"); + props.put("org.osgi.service.http.port.secure", "(14000,15000)"); + this.config.update(props); + + assertTrue(this.config.getHttpPort() > 12000 && this.config.getHttpPort() <= 13000); + assertTrue(this.config.getHttpsPort() > 14000 && this.config.getHttpsPort() < 15000); + + props.put("org.osgi.service.http.port", "[,9000]"); + props.put("org.osgi.service.http.port.secure", "[9000,)"); + this.config.update(props); + + assertTrue(this.config.getHttpPort() >= 1 && this.config.getHttpPort() <= 9000); + assertTrue(this.config.getHttpsPort() >= 9000 && this.config.getHttpsPort() < 65535); + } + + @Test public void testGetPortInvalidRange() + { + Hashtable props = new Hashtable<>(); + props.put("org.osgi.service.http.port", "+12000,13000*"); + props.put("org.osgi.service.http.port.secure", "%14000,15000"); + this.config.update(props); + + assertEquals(8080, this.config.getHttpPort()); + assertEquals(8443, this.config.getHttpsPort()); + } + + @Test public void testGetSpecificPortOne() throws Exception + { + Hashtable props = new Hashtable<>(); + props.put("org.osgi.service.http.port", "1"); + this.config.update(props); + assertTrue(this.config.getHttpPort() == 1); + } + + @Test public void testGetRandomPort() + { + Hashtable props = new Hashtable<>(); + props.put("org.osgi.service.http.port", "*"); + props.put("org.osgi.service.http.port.secure", "*"); + this.config.update(props); + assertTrue(this.config.getHttpPort() != 8080); + assertTrue(this.config.getHttpsPort() != 443); + } + + @Test public void testGetRandomPortZero() throws Exception + { + Hashtable props = new Hashtable<>(); + props.put("org.osgi.service.http.port", "0"); + this.config.update(props); + assertTrue(this.config.getHttpPort() != 0); + } + + @Test public void testGetSpecificPort() throws Exception + { + int port = 80; + + Hashtable props = new Hashtable<>(); + props.put("org.osgi.service.http.port", port); + props.put("org.osgi.service.http.port.secure", port); + this.config.update(props); + assertTrue(this.config.getHttpPort() == port); + assertTrue(this.config.getHttpsPort() == port); + } + + @Test public void testParseStringArrayProperty() { + Hashtable props = new Hashtable<>(); + props.put("org.apache.felix.https.jetty.ciphersuites.excluded", + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDH_anon_WITH_RC4_128_SHA"); + this.config.update(props); + String[] expecteds = {"TLS_DHE_RSA_WITH_AES_128_CBC_SHA", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", "TLS_ECDH_anon_WITH_RC4_128_SHA"}; + assertArrayEquals(expecteds, this.config.getExcludedCipherSuites()); + } + + @SuppressWarnings("unchecked") + @Test public void testAdditionalCustomProperties() { + Hashtable props = new Hashtable<>(); + props.put(JettyConfig.FELIX_CUSTOM_HTTP_RUNTIME_PROPERTY_PREFIX + "number", 5); + props.put(JettyConfig.FELIX_CUSTOM_HTTP_RUNTIME_PROPERTY_PREFIX + "string", "testString"); + List list = new ArrayList<>(3); + list.add("string1"); + list.add("string2"); + list.add("string3"); + props.put(JettyConfig.FELIX_CUSTOM_HTTP_RUNTIME_PROPERTY_PREFIX + "list", list); + this.config.update(props); + + Hashtable toCheck = new Hashtable<>(); + + this.config.setServiceProperties(toCheck); + + assertEquals(5, toCheck.get("number")); + assertEquals("testString", toCheck.get("string")); + assertTrue(toCheck.get("list") instanceof List); + assertEquals(3, ((List)toCheck.get("list")).size()); + assertEquals("string2", ((List)toCheck.get("list")).get(1)); + } + + @Before + public void setUp() + { + this.context = Mockito.mock(BundleContext.class); + this.config = new JettyConfig(this.context); + } +} diff --git a/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/JettyServiceTest.java b/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/JettyServiceTest.java new file mode 100644 index 00000000000..656a72ce323 --- /dev/null +++ b/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/JettyServiceTest.java @@ -0,0 +1,226 @@ +/* + * 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.http.jetty.internal; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Dictionary; +import java.util.EnumSet; +import java.util.Hashtable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.servlet.DispatcherType; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.apache.felix.http.base.internal.HttpServiceController; +import org.apache.felix.http.jetty.internal.JettyService.Deployment; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Matchers; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.Version; +import org.osgi.service.http.context.ServletContextHelper; +import org.osgi.service.http.runtime.HttpServiceRuntime; + +public class JettyServiceTest +{ + + private static final String OSGI_BUNDLECONTEXT = "osgi-bundlecontext"; + + private JettyService jettyService; + + private BundleContext mockBundleContext; + + private HttpServiceController httpServiceController; + + private Bundle mockBundle; + + @Before + public void setUp() throws Exception + { + //Setup Mocks + mockBundleContext = mock(BundleContext.class); + mockBundle = mock(Bundle.class); + + //Setup Behaviors + when(mockBundleContext.getBundle()).thenReturn(mockBundle); + final org.osgi.framework.Filter f = mock(org.osgi.framework.Filter.class); + when(f.toString()).thenReturn("(prop=*)"); + when(mockBundleContext.createFilter(anyString())).thenReturn(f); + when(mockBundle.getSymbolicName()).thenReturn("main"); + when(mockBundle.getVersion()).thenReturn(new Version("1.0.0")); + when(mockBundle.getHeaders()).thenReturn(new Hashtable()); + final ServiceReference ref = mock(ServiceReference.class); + when(ref.getProperty(Constants.SERVICE_ID)).thenReturn(1L); + final ServiceRegistration reg = mock(ServiceRegistration.class); + when(reg.getReference()).thenReturn(ref); + when(mockBundleContext.registerService((Class)Matchers.isNotNull(), + (ServiceFactory)Matchers.any(ServiceFactory.class), + Matchers.any(Dictionary.class))).thenReturn(reg); + when(mockBundleContext.registerService(Matchers.any(), + Matchers.any(ServiceFactory.class), + Matchers.any(Dictionary.class))).thenReturn(reg); + when(mockBundleContext.registerService((Class)Matchers.isNotNull(), + Matchers.any(HttpServiceRuntime.class), + Matchers.any(Dictionary.class))).thenReturn(reg); + + httpServiceController = new HttpServiceController(mockBundleContext); + jettyService = new JettyService(mockBundleContext, httpServiceController); + + jettyService.start(); + } + + @After + public void tearDown() throws Exception { + jettyService.stop(); + } + + /** + * + * Tests to ensure the osgi-bundlecontext is available for init methods. + * + * @throws MalformedURLException + * @throws InterruptedException + */ + @Test public void testInitBundleContextDeployIT() throws Exception + { + //Setup mocks + Deployment mockDeployment = mock(Deployment.class); + Bundle mockBundle = mock(Bundle.class); + BundleContext mockBundleContext = mock(BundleContext.class); + + //Setup behaviors + when(mockDeployment.getBundle()).thenReturn(mockBundle); + final org.osgi.framework.Filter f = mock(org.osgi.framework.Filter.class); + when(f.toString()).thenReturn("(prop=*)"); + when(mockBundleContext.createFilter(anyString())).thenReturn(f); + when(mockBundle.getBundleContext()).thenReturn(mockBundleContext); + when(mockBundle.getSymbolicName()).thenReturn("test"); + when(mockBundle.getVersion()).thenReturn(new Version("0.0.1")); + + Dictionary headerProperties = new Hashtable(); + headerProperties.put("Web-ContextPath", "test"); + when(mockBundle.getHeaders()).thenReturn(headerProperties); + when(mockDeployment.getContextPath()).thenReturn("test"); + when(mockBundle.getEntry("/")).thenReturn(new URL("http://www.apache.com")); + when(mockBundle.getState()).thenReturn(Bundle.ACTIVE); + + EnumSet dispatcherSet = EnumSet.allOf(DispatcherType.class); + dispatcherSet.add(DispatcherType.REQUEST); + + WebAppBundleContext webAppBundleContext = new WebAppBundleContext("/", mockBundle, this.getClass().getClassLoader()); + + final CountDownLatch testLatch = new CountDownLatch(2); + + //Add a Filter to test whether the osgi-bundlecontext is available at init + webAppBundleContext.addServlet(new ServletHolder(new Servlet() + { + @Override + public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException + { + // Do Nothing + } + + @Override + public void init(ServletConfig config) throws ServletException + { + ServletContext context = config.getServletContext(); + + assertNotNull(context.getAttribute(OSGI_BUNDLECONTEXT)); + + testLatch.countDown(); + } + + @Override + public String getServletInfo() + { + return null; + } + + @Override + public ServletConfig getServletConfig() + { + return null; + } + + @Override + public void destroy() + { + // Do Nothing + } + }), "/test1"); + + webAppBundleContext.addFilter(new FilterHolder(new Filter() + { + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + ServletContext context = filterConfig.getServletContext(); + + assertNotNull(context.getAttribute(OSGI_BUNDLECONTEXT)); + + testLatch.countDown(); + } + + @Override + public void doFilter(ServletRequest arg0, ServletResponse response, FilterChain chain) throws IOException, ServletException + { + // Do Nothing + } + + @Override + public void destroy() + { + // Do Nothing + + } + }), "/test2", dispatcherSet); + + jettyService.deploy(mockDeployment, webAppBundleContext); + + //Pause since service is multi-threaded. + //Fail if takes too long. + if (!testLatch.await(10, TimeUnit.SECONDS)) + { + fail("Test Was not asserted"); + } + } +} \ No newline at end of file diff --git a/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/LoadBalancerCustomizerFactoryTrackerTest.java b/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/LoadBalancerCustomizerFactoryTrackerTest.java new file mode 100644 index 00000000000..8d4cf8c9de0 --- /dev/null +++ b/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/LoadBalancerCustomizerFactoryTrackerTest.java @@ -0,0 +1,236 @@ +/* + * 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.http.jetty.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.felix.http.jetty.LoadBalancerCustomizerFactory; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConfiguration.Customizer; +import org.eclipse.jetty.server.Request; +import org.junit.Test; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; + +public class LoadBalancerCustomizerFactoryTrackerTest +{ + + @Test public void testTrackerOrdering() throws Exception + { + final BundleContext bc = mock(BundleContext.class); + final CustomizerWrapper wrapper = new CustomizerWrapper(); + + final List result = new ArrayList<>(); + + final LoadBalancerCustomizerFactoryTracker tracker = new LoadBalancerCustomizerFactoryTracker(bc, wrapper); + + wrapper.customize(null, null, null); + assertTrue(result.isEmpty()); + + final ServiceReference refA = create(bc, result, 5, "A"); + final ServiceReference refB = create(bc, result, 15, "B"); + final ServiceReference refC = create(bc, result, 25, "C"); + + // just A + tracker.addingService(refA); + + wrapper.customize(null, null, null); + assertEquals(1, result.size()); + assertEquals("A", result.get(0)); + result.clear(); + + // add B, B is highest + tracker.addingService(refB); + + wrapper.customize(null, null, null); + assertEquals(1, result.size()); + assertEquals("B", result.get(0)); + result.clear(); + + // add C, C is highest + tracker.addingService(refC); + + wrapper.customize(null, null, null); + assertEquals(1, result.size()); + assertEquals("C", result.get(0)); + result.clear(); + + // remove B, C is still highest + tracker.removedService(refB, refB); + + wrapper.customize(null, null, null); + assertEquals(1, result.size()); + assertEquals("C", result.get(0)); + result.clear(); + + // remove C, A is highest + tracker.removedService(refC, refC); + + wrapper.customize(null, null, null); + assertEquals(1, result.size()); + assertEquals("A", result.get(0)); + result.clear(); + + // remove A, no customizer + tracker.removedService(refA, refA); + + wrapper.customize(null, null, null); + assertTrue(result.isEmpty()); + } + + @Test public void testTrackerCreateFailures() throws Exception + { + final BundleContext bc = mock(BundleContext.class); + final CustomizerWrapper wrapper = new CustomizerWrapper(); + + final List result = new ArrayList<>(); + + final LoadBalancerCustomizerFactoryTracker tracker = new LoadBalancerCustomizerFactoryTracker(bc, wrapper); + + wrapper.customize(null, null, null); + assertTrue(result.isEmpty()); + + final ServiceReference refA = create(bc, result, 5, "A"); + final ServiceReference refB = create(bc, result, 15, null); + final ServiceReference refC = create(bc, result, 25, "C"); + + // add A, C, B - C is highest + tracker.addingService(refA); + tracker.addingService(refC); + tracker.addingService(refB); + + wrapper.customize(null, null, null); + assertEquals(1, result.size()); + assertEquals("C", result.get(0)); + result.clear(); + + // remove C, B returns null, therefore A is used + tracker.removedService(refC, refC); + + wrapper.customize(null, null, null); + assertEquals(1, result.size()); + assertEquals("A", result.get(0)); + result.clear(); + + // remove A, no wrapper + tracker.removedService(refA, refA); + wrapper.customize(null, null, null); + assertTrue(result.isEmpty()); + + // remove B, no wrapper + tracker.removedService(refB, refB); + wrapper.customize(null, null, null); + assertTrue(result.isEmpty()); + } + + private ServiceReference create(final BundleContext bc, + final List result, + final int ranking, + final String identifier) + { + + final ServiceReference refA = new ServiceReferenceImpl(ranking); + when(bc.getService(refA)).thenReturn(new LoadBalancerCustomizerFactory() + { + + @Override + public Customizer createCustomizer() + { + if ( identifier == null ) + { + return null; + } + return new Customizer() + { + + @Override + public void customize(Connector connector, HttpConfiguration channelConfig, Request request) + { + result.add(identifier); + } + }; + } + }); + + return refA; + } + + private static class ServiceReferenceImpl implements ServiceReference + { + + private final int serviceRanking; + + public ServiceReferenceImpl(final int ranking) + { + this.serviceRanking = ranking; + } + + @Override + public Object getProperty(String key) + { + return null; + } + + @Override + public String[] getPropertyKeys() + { + return null; + } + + @Override + public Bundle getBundle() + { + return null; + } + + @Override + public Bundle[] getUsingBundles() + { + return null; + } + + @Override + public boolean isAssignableTo(Bundle bundle, String className) + { + return false; + } + + @Override + public int compareTo(Object reference) + { + final ServiceReferenceImpl o = (ServiceReferenceImpl)reference; + if ( serviceRanking < o.serviceRanking ) + { + return -1; + } + else if ( serviceRanking > o.serviceRanking ) + { + return 1; + } + return 0; + } + + }; +} diff --git a/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java b/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java new file mode 100644 index 00000000000..4e4b0ff8152 --- /dev/null +++ b/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java @@ -0,0 +1,97 @@ +/* + * 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.http.jetty.internal; + +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.RequestLog; +import org.eclipse.jetty.server.Response; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.osgi.framework.*; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class RequestLogTrackerTest { + + @Mock + BundleContext context; + + @Test + public void testInvokeRequestLog() throws Exception { + RequestLogTracker tracker = new RequestLogTracker(context, null); + + RequestLog mockRequestLog = mock(RequestLog.class); + + ServiceReference mockSvcRef = mock(ServiceReference.class); + when(context.getService(mockSvcRef)).thenReturn(mockRequestLog); + + // These invocations not passed through to the mock because it is not registered yet + for (int i = 0; i < 10; i++) + tracker.log(null, null); + + tracker.addingService(mockSvcRef); + + // These will pass through + for (int i = 0; i < 15; i++) + tracker.log(null, null); + + tracker.removedService(mockSvcRef, mockRequestLog); + + // And these will not. + for (int i = 0; i < 50; i++) + tracker.log(null, null); + + verify(mockRequestLog, times(15)).log(isNull(Request.class), isNull(Response.class)); + } + + @Test + public void testNaughtyService() throws Exception { + RequestLogTracker tracker = new RequestLogTracker(context, null); + + AtomicInteger counter = new AtomicInteger(0); + RequestLog mockRequestLog = new RequestLog() { + @Override + public void log(Request request, Response response) { + counter.addAndGet(1); + throw new RuntimeException("This service always explodes"); + } + }; + ServiceReference mockSvcRef = mock(ServiceReference.class); + Bundle mockBundle = mock(Bundle.class); + when(mockSvcRef.getBundle()).thenReturn(mockBundle); + when(mockBundle.getSymbolicName()).thenReturn("org.example"); + when(mockBundle.getVersion()).thenReturn(new Version("1.0.0")); + when(context.getService(mockSvcRef)).thenReturn(mockRequestLog); + + tracker.addingService(mockSvcRef); + + // Invoke 200 times + for (int i = 0; i < 200; i++) + tracker.log(null, null); + + tracker.removedService(mockSvcRef, mockRequestLog); + + // Invoked 100 times and then removed + assertEquals(100, counter.get()); + } +} diff --git a/http/parent/pom.xml b/http/parent/pom.xml new file mode 100755 index 00000000000..b34187e2bfa --- /dev/null +++ b/http/parent/pom.xml @@ -0,0 +1,144 @@ + + + + 4.0.0 + + + org.apache.felix + felix-parent + 6 + ../../pom/pom.xml + + + Apache Felix Http Parent POM + org.apache.felix.http.parent + 13-SNAPSHOT + pom + + + + 3.1.0 + + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/http/parent + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/http/parent + http://svn.apache.org/viewvc/felix/trunk/http/parent/ + + + + + + + org.apache.felix + maven-bundle-plugin + 3.5.0 + + + bundle + package + + bundle + + + + baseline + + baseline + + + + + + ${project.artifactId} + ${project.version} + + + + + + + + + + + org.jetbrains + annotations + + + org.osgi + osgi.annotation + + + + junit + junit + + + org.mockito + mockito-core + + + + + + + + org.jetbrains + annotations + 16.0.2 + provided + + + org.osgi + osgi.annotation + 6.0.1 + provided + + + + javax.servlet + javax.servlet-api + ${servlet.version} + provided + + + + org.osgi + osgi.core + 6.0.0 + provided + + + + junit + junit + 4.12 + test + + + org.mockito + mockito-core + 2.17.0 + test + + + + diff --git a/http/pom.xml b/http/pom.xml new file mode 100644 index 00000000000..e4ff1a19460 --- /dev/null +++ b/http/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + + org.apache.felix + felix-parent + 5 + ../pom/pom.xml + + + Apache Felix Http Reactor + org.apache.felix + org.apache.felix.http + 7-SNAPSHOT + pom + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/http + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/http + http://svn.apache.org/viewvc/felix/trunk/http/ + + + + servlet-api + base + bridge + jetty + parent + proxy + whiteboard + cometd + bundle + sslfilter + itest + samples/bridge + samples/whiteboard + samples/cometd + + diff --git a/http/proxy/DEPENDENCIES b/http/proxy/DEPENDENCIES new file mode 100644 index 00000000000..7832bf0a83d --- /dev/null +++ b/http/proxy/DEPENDENCIES @@ -0,0 +1,20 @@ +Apache Felix HTTP Service Proxy +Copyright 2011 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +N/A + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 diff --git a/http/proxy/LICENSE b/http/proxy/LICENSE new file mode 100644 index 00000000000..6b0b1270ff0 --- /dev/null +++ b/http/proxy/LICENSE @@ -0,0 +1,203 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/http/proxy/NOTICE b/http/proxy/NOTICE new file mode 100644 index 00000000000..110592201c3 --- /dev/null +++ b/http/proxy/NOTICE @@ -0,0 +1,6 @@ +Apache Felix Http Service Proxy +Copyright 2011 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. diff --git a/http/proxy/pom.xml b/http/proxy/pom.xml new file mode 100644 index 00000000000..1308d70645a --- /dev/null +++ b/http/proxy/pom.xml @@ -0,0 +1,59 @@ + + + + 4.0.0 + + org.apache.felix + org.apache.felix.http.parent + 12 + ../parent/pom.xml + + + Apache Felix Http Proxy + org.apache.felix.http.proxy + 3.0.5-SNAPSHOT + jar + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/http/proxy + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/http/proxy + http://svn.apache.org/viewvc/felix/trunk/http/proxy/ + + + + + + org.apache.felix + maven-bundle-plugin + + + + + + + javax.servlet + javax.servlet-api + + + org.osgi + osgi.core + + + diff --git a/http/proxy/src/main/java/org/apache/felix/http/proxy/DispatcherTracker.java b/http/proxy/src/main/java/org/apache/felix/http/proxy/DispatcherTracker.java new file mode 100644 index 00000000000..82ddba32d5d --- /dev/null +++ b/http/proxy/src/main/java/org/apache/felix/http/proxy/DispatcherTracker.java @@ -0,0 +1,117 @@ +/* + * 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.http.proxy; + +import javax.servlet.ServletConfig; +import javax.servlet.http.HttpServlet; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; + +/** + * @deprecated + */ +@Deprecated +public final class DispatcherTracker + extends ServiceTracker +{ + final static String DEFAULT_FILTER = "(http.felix.dispatcher=*)"; + + private final ServletConfig config; + private HttpServlet dispatcher; + + public DispatcherTracker(BundleContext context, String filter, ServletConfig config) + throws Exception + { + super(context, createFilter(context, filter), null); + this.config = config; + } + + public HttpServlet getDispatcher() + { + return this.dispatcher; + } + + @Override + public Object addingService(ServiceReference ref) + { + Object service = super.addingService(ref); + if (service instanceof HttpServlet) { + setDispatcher((HttpServlet)service); + } + + return service; + } + + @Override + public void removedService(ServiceReference ref, Object service) + { + if (service instanceof HttpServlet) { + setDispatcher(null); + } + + super.removedService(ref, service); + } + + private void log(String message, Throwable cause) + { + this.config.getServletContext().log(message, cause); + } + + private void setDispatcher(HttpServlet dispatcher) + { + destroyDispatcher(); + this.dispatcher = dispatcher; + initDispatcher(); + } + + private void destroyDispatcher() + { + if (this.dispatcher == null) { + return; + } + + this.dispatcher.destroy(); + this.dispatcher = null; + } + + private void initDispatcher() + { + if (this.dispatcher == null) { + return; + } + + try { + this.dispatcher.init(this.config); + } catch (Exception e) { + log("Failed to initialize dispatcher", e); + } + } + + private static Filter createFilter(BundleContext context, String filter) + throws Exception + { + StringBuffer str = new StringBuffer(); + str.append("(&(").append(Constants.OBJECTCLASS).append("="); + str.append(HttpServlet.class.getName()).append(")"); + str.append(filter != null ? filter : DEFAULT_FILTER).append(")"); + return context.createFilter(str.toString()); + } +} diff --git a/http/proxy/src/main/java/org/apache/felix/http/proxy/ProxyListener.java b/http/proxy/src/main/java/org/apache/felix/http/proxy/ProxyListener.java new file mode 100644 index 00000000000..1a0923b2edd --- /dev/null +++ b/http/proxy/src/main/java/org/apache/felix/http/proxy/ProxyListener.java @@ -0,0 +1,195 @@ +/* + * 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.http.proxy; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionIdListener; +import javax.servlet.http.HttpSessionListener; + +import org.apache.felix.http.proxy.impl.EventDispatcherTracker; +import org.apache.felix.http.proxy.impl.ProxyServletContextListener; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; + +/** + * The ProxyListener implements a Servlet API listener for HTTP + * Session related events. These events are provided by the servlet container + * and forwarded to the event dispatcher. + * + * @since 2.1.0 + * @deprecated Use the {@link ProxyServletContextListener} instead. + */ +@Deprecated +public class ProxyListener + implements HttpSessionAttributeListener, + HttpSessionListener, + HttpSessionIdListener, + ServletContextListener +{ + + private volatile ServletContext servletContext; + + private volatile EventDispatcherTracker eventDispatcherTracker; + + // ---------- ServletContextListener + + @Override + public void contextInitialized(final ServletContextEvent sce) + { + this.servletContext = sce.getServletContext(); + } + + @Override + public void contextDestroyed(final ServletContextEvent sce) + { + if (this.eventDispatcherTracker != null) + { + this.eventDispatcherTracker.close(); + this.eventDispatcherTracker = null; + } + this.servletContext = null; + } + + // ---------- HttpSessionListener + + @Override + public void sessionCreated(final HttpSessionEvent se) + { + final HttpSessionListener sessionDispatcher = getSessionDispatcher(); + if (sessionDispatcher != null) + { + sessionDispatcher.sessionCreated(se); + } + } + + @Override + public void sessionDestroyed(final HttpSessionEvent se) + { + final HttpSessionListener sessionDispatcher = getSessionDispatcher(); + if (sessionDispatcher != null) + { + sessionDispatcher.sessionDestroyed(se); + } + } + + // ---------- HttpSessionIdListener + + @Override + public void sessionIdChanged(final HttpSessionEvent event, final String oldSessionId) + { + final HttpSessionIdListener sessionIdDispatcher = getSessionIdDispatcher(); + if (sessionIdDispatcher != null) + { + sessionIdDispatcher.sessionIdChanged(event, oldSessionId); + } + } + + // ---------- HttpSessionAttributeListener + + @Override + public void attributeAdded(final HttpSessionBindingEvent se) + { + final HttpSessionAttributeListener attributeDispatcher = getAttributeDispatcher(); + if (attributeDispatcher != null) + { + attributeDispatcher.attributeAdded(se); + } + } + + @Override + public void attributeRemoved(final HttpSessionBindingEvent se) + { + final HttpSessionAttributeListener attributeDispatcher = getAttributeDispatcher(); + if (attributeDispatcher != null) + { + attributeDispatcher.attributeRemoved(se); + } + } + + @Override + public void attributeReplaced(final HttpSessionBindingEvent se) + { + final HttpSessionAttributeListener attributeDispatcher = getAttributeDispatcher(); + if (attributeDispatcher != null) + { + attributeDispatcher.attributeReplaced(se); + } + } + + // ---------- internal + + private Object getDispatcher() + { + if (this.eventDispatcherTracker == null) + { + // the bundle context may or may not be already provided; + // if not we cannot access the dispatcher yet + Object bundleContextAttr = this.servletContext.getAttribute(BundleContext.class.getName()); + if (!(bundleContextAttr instanceof BundleContext)) + { + return null; + } + + try + { + BundleContext bundleContext = (BundleContext) bundleContextAttr; + this.eventDispatcherTracker = new EventDispatcherTracker(bundleContext); + this.eventDispatcherTracker.open(); + } + catch (InvalidSyntaxException e) + { + // not expected for our simple filter + } + + } + return this.eventDispatcherTracker.getService(); + } + + private HttpSessionListener getSessionDispatcher() + { + if (this.eventDispatcherTracker != null) + { + return this.eventDispatcherTracker.getHttpSessionListener(); + } + return null; + } + + private HttpSessionIdListener getSessionIdDispatcher() + { + if (this.eventDispatcherTracker != null) + { + return this.eventDispatcherTracker.getHttpSessionIdListener(); + } + return null; + } + + private HttpSessionAttributeListener getAttributeDispatcher() + { + if (this.eventDispatcherTracker != null) + { + return this.eventDispatcherTracker.getHttpSessionAttributeListener(); + } + return null; + } +} diff --git a/http/proxy/src/main/java/org/apache/felix/http/proxy/ProxyServlet.java b/http/proxy/src/main/java/org/apache/felix/http/proxy/ProxyServlet.java new file mode 100644 index 00000000000..ebccb737b52 --- /dev/null +++ b/http/proxy/src/main/java/org/apache/felix/http/proxy/ProxyServlet.java @@ -0,0 +1,456 @@ +/* + * 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.http.proxy; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.Map; +import java.util.Set; + +import javax.servlet.Filter; +import javax.servlet.FilterRegistration; +import javax.servlet.RequestDispatcher; +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; +import javax.servlet.ServletRegistration.Dynamic; +import javax.servlet.SessionCookieConfig; +import javax.servlet.SessionTrackingMode; +import javax.servlet.descriptor.JspConfigDescriptor; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; + +import org.osgi.framework.BundleContext; + +public final class ProxyServlet + extends HttpServlet +{ + private volatile DispatcherTracker tracker; + + private volatile boolean initialized = false; + + private volatile ServletContext servletContext; + + @Override + public void init(ServletConfig config) + throws ServletException + { + super.init(config); + } + + private void doInit() + throws Exception + { + final ServletConfig origConfig = getServletConfig(); + ServletConfig config = origConfig; + if ( this.servletContext != null ) { + config = new ServletConfig() { + + @Override + public String getServletName() { + return origConfig.getServletName(); + } + + @Override + public ServletContext getServletContext() { + return servletContext; + } + + @Override + public Enumeration getInitParameterNames() { + return origConfig.getInitParameterNames(); + } + + @Override + public String getInitParameter(String name) { + return origConfig.getInitParameter(name); + } + }; + } + this.tracker = new DispatcherTracker(getBundleContext(), null, config); + this.tracker.open(); + } + + @Override + protected void service(final HttpServletRequest req, final HttpServletResponse res) + throws ServletException, IOException + { + if ( !initialized ) { + synchronized ( this ) { + if (!initialized ) { + if ( ! "".equals(req.getServletPath()) ) { + this.servletContext = new ServletContextWrapper(req.getServletContext(), req.getContextPath() + req.getServletPath()); + } + + try { + doInit(); + } catch (ServletException e) { + throw e; + } catch (Exception e) { + throw new ServletException(e); + } + + this.initialized = true; + } + } + } + + final HttpServlet dispatcher = this.tracker.getDispatcher(); + if (dispatcher != null) { + final HttpServletRequest r = (this.servletContext == null ? req : new BridgeHttpServletRequest(req, this.servletContext)); + dispatcher.service(r, res); + } else { + res.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + } + } + + @Override + public void destroy() + { + if ( this.tracker != null ) { + this.tracker.close(); + this.tracker = null; + } + super.destroy(); + } + + private BundleContext getBundleContext() + throws ServletException + { + Object context = getServletContext().getAttribute(BundleContext.class.getName()); + if (context instanceof BundleContext) + { + return (BundleContext)context; + } + + throw new ServletException("Bundle context attribute [" + BundleContext.class.getName() + + "] not set in servlet context"); + } + + private static final class BridgeHttpServletRequest extends HttpServletRequestWrapper + { + private final ServletContext context; + + public BridgeHttpServletRequest(final HttpServletRequest req, final ServletContext context) + { + super(req); + this.context = context; + } + + @Override + public String getServletPath() + { + return ""; + } + + @Override + public String getContextPath() + { + return this.context.getContextPath(); + } + + @Override + public ServletContext getServletContext() { + return this.context; + } + + + } + + private static final class ServletContextWrapper implements ServletContext + { + private final ServletContext delegatee; + + private final String path; + + public ServletContextWrapper(final ServletContext sc, final String path) + { + this.delegatee = sc; + this.path = path; + } + + @Override + public String getContextPath() { + return path; + } + + @Override + public ServletContext getContext(String uripath) { + return delegatee.getContext(uripath); + } + + @Override + public int getMajorVersion() { + return delegatee.getMajorVersion(); + } + + @Override + public int getMinorVersion() { + return delegatee.getMinorVersion(); + } + + @Override + public int getEffectiveMajorVersion() { + return delegatee.getEffectiveMajorVersion(); + } + + @Override + public int getEffectiveMinorVersion() { + return delegatee.getEffectiveMinorVersion(); + } + + @Override + public String getMimeType(String file) { + return delegatee.getMimeType(file); + } + + @Override + public Set getResourcePaths(String path) { + return delegatee.getResourcePaths(path); + } + + @Override + public URL getResource(String path) throws MalformedURLException { + return delegatee.getResource(path); + } + + @Override + public InputStream getResourceAsStream(String path) { + return delegatee.getResourceAsStream(path); + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + return delegatee.getRequestDispatcher(path); + } + + @Override + public RequestDispatcher getNamedDispatcher(String name) { + return delegatee.getNamedDispatcher(name); + } + + @Override + public Servlet getServlet(String name) throws ServletException { + return delegatee.getServlet(name); + } + + @Override + public Enumeration getServlets() { + return delegatee.getServlets(); + } + + @Override + public Enumeration getServletNames() { + return delegatee.getServletNames(); + } + + @Override + public void log(String msg) { + delegatee.log(msg); + } + + @Override + public void log(Exception exception, String msg) { + delegatee.log(exception, msg); + } + + @Override + public void log(String message, Throwable throwable) { + delegatee.log(message, throwable); + } + + @Override + public String getRealPath(String path) { + return delegatee.getRealPath(path); + } + + @Override + public String getServerInfo() { + return delegatee.getServerInfo(); + } + + @Override + public String getInitParameter(String name) { + return delegatee.getInitParameter(name); + } + + @Override + public Enumeration getInitParameterNames() { + return delegatee.getInitParameterNames(); + } + + @Override + public boolean setInitParameter(String name, String value) { + return delegatee.setInitParameter(name, value); + } + + @Override + public Object getAttribute(String name) { + return delegatee.getAttribute(name); + } + + @Override + public Enumeration getAttributeNames() { + return delegatee.getAttributeNames(); + } + + @Override + public void setAttribute(String name, Object object) { + delegatee.setAttribute(name, object); + } + + @Override + public void removeAttribute(String name) { + delegatee.removeAttribute(name); + } + + @Override + public String getServletContextName() { + return delegatee.getServletContextName(); + } + + @Override + public Dynamic addServlet(String servletName, String className) { + return delegatee.addServlet(servletName, className); + } + + @Override + public Dynamic addServlet(String servletName, Servlet servlet) { + return delegatee.addServlet(servletName, servlet); + } + + @Override + public Dynamic addServlet(String servletName, Class servletClass) { + return delegatee.addServlet(servletName, servletClass); + } + + @Override + public T createServlet(Class clazz) throws ServletException { + return delegatee.createServlet(clazz); + } + + @Override + public ServletRegistration getServletRegistration(String servletName) { + return delegatee.getServletRegistration(servletName); + } + + @Override + public Map getServletRegistrations() { + return delegatee.getServletRegistrations(); + } + + @Override + public javax.servlet.FilterRegistration.Dynamic addFilter(String filterName, String className) { + return delegatee.addFilter(filterName, className); + } + + @Override + public javax.servlet.FilterRegistration.Dynamic addFilter(String filterName, Filter filter) { + return delegatee.addFilter(filterName, filter); + } + + @Override + public javax.servlet.FilterRegistration.Dynamic addFilter(String filterName, + Class filterClass) { + return delegatee.addFilter(filterName, filterClass); + } + + @Override + public T createFilter(Class clazz) throws ServletException { + return delegatee.createFilter(clazz); + } + + @Override + public FilterRegistration getFilterRegistration(String filterName) { + return delegatee.getFilterRegistration(filterName); + } + + @Override + public Map getFilterRegistrations() { + return delegatee.getFilterRegistrations(); + } + + @Override + public SessionCookieConfig getSessionCookieConfig() { + return delegatee.getSessionCookieConfig(); + } + + @Override + public void setSessionTrackingModes(Set sessionTrackingModes) { + delegatee.setSessionTrackingModes(sessionTrackingModes); + } + + @Override + public Set getDefaultSessionTrackingModes() { + return delegatee.getDefaultSessionTrackingModes(); + } + + @Override + public Set getEffectiveSessionTrackingModes() { + return delegatee.getEffectiveSessionTrackingModes(); + } + + @Override + public void addListener(String className) { + delegatee.addListener(className); + } + + @Override + public void addListener(T t) { + delegatee.addListener(t); + } + + @Override + public void addListener(Class listenerClass) { + delegatee.addListener(listenerClass); + } + + @Override + public T createListener(Class clazz) throws ServletException { + return delegatee.createListener(clazz); + } + + @Override + public JspConfigDescriptor getJspConfigDescriptor() { + return delegatee.getJspConfigDescriptor(); + } + + @Override + public ClassLoader getClassLoader() { + return delegatee.getClassLoader(); + } + + @Override + public void declareRoles(String... roleNames) { + delegatee.declareRoles(roleNames); + } + + @Override + public String getVirtualServerName() { + return delegatee.getVirtualServerName(); + } + + } +} diff --git a/http/proxy/src/main/java/org/apache/felix/http/proxy/impl/BridgeServiceTracker.java b/http/proxy/src/main/java/org/apache/felix/http/proxy/impl/BridgeServiceTracker.java new file mode 100644 index 00000000000..36679ff2f20 --- /dev/null +++ b/http/proxy/src/main/java/org/apache/felix/http/proxy/impl/BridgeServiceTracker.java @@ -0,0 +1,81 @@ +/* + * 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.http.proxy.impl; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; + +/** + * @since 3.0.0 + */ +public abstract class BridgeServiceTracker + extends ServiceTracker +{ + private final static String DEFAULT_FILTER = "(http.felix.dispatcher=*)"; + + private volatile T usedService; + + public BridgeServiceTracker(final BundleContext context, final Class objectClass) + throws InvalidSyntaxException + { + super(context, createFilter(context, objectClass), null); + } + + + protected abstract void setService(final T service); + + protected abstract void unsetService(); + + + @Override + public T addingService(final ServiceReference ref) + { + final T service = super.addingService(ref); + if ( usedService == null ) + { + this.usedService = service; + this.setService(service); + } + + return service; + } + + @Override + public void removedService(final ServiceReference ref, final T service) + { + if ( service == usedService ) + { + this.unsetService(); + } + + super.removedService(ref, service); + } + + private static Filter createFilter(final BundleContext context, final Class objectClass) + throws InvalidSyntaxException + { + StringBuffer str = new StringBuffer(); + str.append("(&(").append(Constants.OBJECTCLASS).append("="); + str.append(objectClass.getName()).append(")"); + str.append(DEFAULT_FILTER).append(")"); + return context.createFilter(str.toString()); + } +} diff --git a/http/proxy/src/main/java/org/apache/felix/http/proxy/impl/EventDispatcherTracker.java b/http/proxy/src/main/java/org/apache/felix/http/proxy/impl/EventDispatcherTracker.java new file mode 100644 index 00000000000..9c11700304a --- /dev/null +++ b/http/proxy/src/main/java/org/apache/felix/http/proxy/impl/EventDispatcherTracker.java @@ -0,0 +1,85 @@ +/* + * 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.http.proxy.impl; + +import java.util.EventListener; + +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionIdListener; +import javax.servlet.http.HttpSessionListener; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; + +/** + * @since 3.0.0 + */ +public final class EventDispatcherTracker + extends BridgeServiceTracker +{ + private HttpSessionListener sessionListener; + + private HttpSessionIdListener sessionIdListener; + + private HttpSessionAttributeListener sessionAttributeListener; + + public EventDispatcherTracker(final BundleContext context) + throws InvalidSyntaxException + { + super(context, EventListener.class); + } + + @Override + protected void setService(final EventListener service) + { + if ( service instanceof HttpSessionListener ) + { + this.sessionListener = (HttpSessionListener)service; + } + if ( service instanceof HttpSessionIdListener ) + { + this.sessionIdListener = (HttpSessionIdListener)service; + } + if ( service instanceof HttpSessionAttributeListener ) + { + this.sessionAttributeListener = (HttpSessionAttributeListener)service; + } + } + + public HttpSessionListener getHttpSessionListener() + { + return this.sessionListener; + } + + public HttpSessionIdListener getHttpSessionIdListener() + { + return this.sessionIdListener; + } + + public HttpSessionAttributeListener getHttpSessionAttributeListener() + { + return this.sessionAttributeListener; + } + + @Override + protected void unsetService() + { + sessionListener = null; + sessionIdListener = null; + sessionAttributeListener = null; + } +} diff --git a/http/proxy/src/main/java/org/apache/felix/http/proxy/impl/ProxyServletContextListener.java b/http/proxy/src/main/java/org/apache/felix/http/proxy/impl/ProxyServletContextListener.java new file mode 100644 index 00000000000..e31656ce201 --- /dev/null +++ b/http/proxy/src/main/java/org/apache/felix/http/proxy/impl/ProxyServletContextListener.java @@ -0,0 +1,219 @@ +/* + * 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.http.proxy.impl; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextAttributeEvent; +import javax.servlet.ServletContextAttributeListener; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.annotation.WebListener; +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionIdListener; +import javax.servlet.http.HttpSessionListener; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; + +/** + * The ProxyServletContextListener is a servlet context listener which will setup + * all required listeners for the http service implementation. + * + * @since 3.0.0 + */ +@WebListener +public class ProxyServletContextListener + implements ServletContextListener +{ + + private volatile ServletContext servletContext; + + private volatile EventDispatcherTracker eventDispatcherTracker; + + // ---------- ServletContextListener + + @Override + public void contextInitialized(final ServletContextEvent sce) + { + this.servletContext = sce.getServletContext(); + + // add all required listeners + this.servletContext.addListener(new HttpSessionListener() + { + private HttpSessionListener getHttpSessionListener() + { + final EventDispatcherTracker tracker = eventDispatcherTracker; + if ( tracker != null ) + { + return tracker.getHttpSessionListener(); + } + return null; + } + + @Override + public void sessionCreated(final HttpSessionEvent se) + { + final HttpSessionListener sessionDispatcher = getHttpSessionListener(); + if (sessionDispatcher != null) + { + sessionDispatcher.sessionCreated(se); + } + } + + @Override + public void sessionDestroyed(final HttpSessionEvent se) + { + final HttpSessionListener sessionDispatcher = getHttpSessionListener(); + if (sessionDispatcher != null) + { + sessionDispatcher.sessionDestroyed(se); + } + } + }); + this.servletContext.addListener(new HttpSessionIdListener() + { + private HttpSessionIdListener getHttpSessionIdListener() + { + final EventDispatcherTracker tracker = eventDispatcherTracker; + if ( tracker != null ) + { + return tracker.getHttpSessionIdListener(); + } + return null; + } + + @Override + public void sessionIdChanged(final HttpSessionEvent event, final String oldSessionId) + { + final HttpSessionIdListener sessionIdDispatcher = getHttpSessionIdListener(); + if (sessionIdDispatcher != null) + { + sessionIdDispatcher.sessionIdChanged(event, oldSessionId); + } + } + }); + this.servletContext.addListener(new HttpSessionAttributeListener() + { + private HttpSessionAttributeListener getHttpSessionAttributeListener() + { + final EventDispatcherTracker tracker = eventDispatcherTracker; + if ( tracker != null ) + { + return tracker.getHttpSessionAttributeListener(); + } + return null; + } + + @Override + public void attributeAdded(final HttpSessionBindingEvent se) + { + final HttpSessionAttributeListener attributeDispatcher = getHttpSessionAttributeListener(); + if (attributeDispatcher != null) + { + attributeDispatcher.attributeAdded(se); + } + } + + @Override + public void attributeRemoved(final HttpSessionBindingEvent se) + { + final HttpSessionAttributeListener attributeDispatcher = getHttpSessionAttributeListener(); + if (attributeDispatcher != null) + { + attributeDispatcher.attributeRemoved(se); + } + } + + @Override + public void attributeReplaced(final HttpSessionBindingEvent se) + { + final HttpSessionAttributeListener attributeDispatcher = getHttpSessionAttributeListener(); + if (attributeDispatcher != null) + { + attributeDispatcher.attributeReplaced(se); + } + } + }); + this.servletContext.addListener(new ServletContextAttributeListener() + { + @Override + public void attributeAdded(final ServletContextAttributeEvent event) + { + if ( event.getName().equals(BundleContext.class.getName()) ) + { + startTracker(event.getValue()); + } + } + + @Override + public void attributeRemoved(final ServletContextAttributeEvent event) + { + if ( event.getName().equals(BundleContext.class.getName()) ) + { + stopTracker(); + } + } + + @Override + public void attributeReplaced(final ServletContextAttributeEvent event) + { + if ( event.getName().equals(BundleContext.class.getName()) ) + { + stopTracker(); + startTracker(event.getServletContext().getAttribute(event.getName())); + } + } + }); + } + + private void startTracker(final Object bundleContextAttr) + { + if (bundleContextAttr instanceof BundleContext) + { + try + { + final BundleContext bundleContext = (BundleContext) bundleContextAttr; + this.eventDispatcherTracker = new EventDispatcherTracker(bundleContext); + this.eventDispatcherTracker.open(); + } + catch (final InvalidSyntaxException e) + { + // not expected for our simple filter + } + } + } + + private void stopTracker() + { + if (this.eventDispatcherTracker != null) + { + this.eventDispatcherTracker.close(); + this.eventDispatcherTracker = null; + } + } + + @Override + public void contextDestroyed(final ServletContextEvent sce) + { + this.stopTracker(); + this.servletContext = null; + } +} diff --git a/http/proxy/src/main/java/org/apache/felix/http/proxy/package-info.java b/http/proxy/src/main/java/org/apache/felix/http/proxy/package-info.java new file mode 100644 index 00000000000..fa9b9ecaeeb --- /dev/null +++ b/http/proxy/src/main/java/org/apache/felix/http/proxy/package-info.java @@ -0,0 +1,24 @@ +/* + * 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. + */ + +@Version("3.0.0") +package org.apache.felix.http.proxy; + +import org.osgi.annotation.versioning.Version; + diff --git a/http/samples/bridge/DEPENDENCIES b/http/samples/bridge/DEPENDENCIES new file mode 100644 index 00000000000..c336596efde --- /dev/null +++ b/http/samples/bridge/DEPENDENCIES @@ -0,0 +1,23 @@ +Apache Felix HTTP Service Bridge Sample +Copyright 2011 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 diff --git a/http/samples/bridge/LICENSE b/http/samples/bridge/LICENSE new file mode 100644 index 00000000000..6b0b1270ff0 --- /dev/null +++ b/http/samples/bridge/LICENSE @@ -0,0 +1,203 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/http/samples/bridge/NOTICE b/http/samples/bridge/NOTICE new file mode 100644 index 00000000000..514604df213 --- /dev/null +++ b/http/samples/bridge/NOTICE @@ -0,0 +1,11 @@ +Apache Felix Http Service Bridge Sample +Copyright 2011 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developed at +The OSGi Alliance (http://www.osgi.org/). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. diff --git a/http/samples/bridge/pom.xml b/http/samples/bridge/pom.xml new file mode 100644 index 00000000000..25bb2ebcaa5 --- /dev/null +++ b/http/samples/bridge/pom.xml @@ -0,0 +1,142 @@ + + + + 4.0.0 + + org.apache.felix + org.apache.felix.http.parent + 9 + ../../parent/pom.xml + + + Apache Felix Http Samples - Bridge + org.apache.felix.http.samples.bridge + 2.4.0-SNAPSHOT + war + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/http/samples/bridge + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/http/samples/bridge + http://svn.apache.org/viewvc/felix/trunk/http/samples/bridge/ + + + + + + org.mortbay.jetty + maven-jetty-plugin + 6.1.26 + + / + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-bundles + + copy-dependencies + + + + org.apache.felix.http.bridge,org.apache.felix.http.samples.filter,org.apache.felix.webconsole + + true + + ${project.build.directory}/bundles + + + + + + + org.apache.maven.plugins + maven-war-plugin + 2.5 + + + + + ${project.build.directory}/bundles + + WEB-INF/bundles + + + . + META-INF + + LICENSE + NOTICE + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + + + + + javax.servlet + javax.servlet-api + + + org.osgi + osgi.cmpn + + + org.apache.felix + org.apache.felix.framework + 5.2.0 + + + org.apache.felix + org.apache.felix.http.proxy + 3.0.0 + + + org.apache.felix + org.apache.felix.http.bridge + 3.0.0 + provided + + + org.apache.felix + org.apache.felix.webconsole + 4.2.0 + provided + + + + diff --git a/http/samples/bridge/src/main/java/org/apache/felix/http/samples/bridge/FrameworkService.java b/http/samples/bridge/src/main/java/org/apache/felix/http/samples/bridge/FrameworkService.java new file mode 100644 index 00000000000..cb66857feb5 --- /dev/null +++ b/http/samples/bridge/src/main/java/org/apache/felix/http/samples/bridge/FrameworkService.java @@ -0,0 +1,93 @@ +/* + * 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.http.samples.bridge; + +import org.apache.felix.framework.Felix; +import org.apache.felix.framework.util.FelixConstants; +import javax.servlet.ServletContext; +import java.util.Map; +import java.util.HashMap; +import java.util.Properties; +import java.util.Arrays; + +public final class FrameworkService +{ + private final ServletContext context; + private Felix felix; + + public FrameworkService(ServletContext context) + { + this.context = context; + } + + public void start() + { + try { + doStart(); + } catch (Exception e) { + log("Failed to start framework", e); + } + } + + public void stop() + { + try { + doStop(); + } catch (Exception e) { + log("Error stopping framework", e); + } + } + + private void doStart() + throws Exception + { + Felix tmp = new Felix(createConfig()); + tmp.start(); + this.felix = tmp; + log("OSGi framework started", null); + } + + private void doStop() + throws Exception + { + if (this.felix != null) { + this.felix.stop(); + } + + log("OSGi framework stopped", null); + } + + private Map createConfig() + throws Exception + { + Properties props = new Properties(); + props.load(this.context.getResourceAsStream("/WEB-INF/framework.properties")); + + HashMap map = new HashMap(); + for (Object key : props.keySet()) { + map.put(key.toString(), props.get(key)); + } + + map.put(FelixConstants.SYSTEMBUNDLE_ACTIVATORS_PROP, Arrays.asList(new ProvisionActivator(this.context))); + return map; + } + + private void log(String message, Throwable cause) + { + this.context.log(message, cause); + } +} diff --git a/http/samples/bridge/src/main/java/org/apache/felix/http/samples/bridge/ProvisionActivator.java b/http/samples/bridge/src/main/java/org/apache/felix/http/samples/bridge/ProvisionActivator.java new file mode 100644 index 00000000000..a85cebe2277 --- /dev/null +++ b/http/samples/bridge/src/main/java/org/apache/felix/http/samples/bridge/ProvisionActivator.java @@ -0,0 +1,75 @@ +/* + * 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.http.samples.bridge; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Bundle; +import javax.servlet.ServletContext; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +public final class ProvisionActivator + implements BundleActivator +{ + private final ServletContext servletContext; + + public ProvisionActivator(ServletContext servletContext) + { + this.servletContext = servletContext; + } + + public void start(BundleContext context) + throws Exception + { + servletContext.setAttribute(BundleContext.class.getName(), context); + + ArrayList installed = new ArrayList(); + for (URL url : findBundles()) { + this.servletContext.log("Installing bundle [" + url + "]"); + Bundle bundle = context.installBundle(url.toExternalForm()); + installed.add(bundle); + } + + for (Bundle bundle : installed) { + bundle.start(); + } + } + + public void stop(BundleContext context) + throws Exception + { + } + + private List findBundles() + throws Exception + { + ArrayList list = new ArrayList(); + for (Object o : this.servletContext.getResourcePaths("/WEB-INF/bundles/")) { + String name = (String)o; + if (name.endsWith(".jar")) { + URL url = this.servletContext.getResource(name); + if (url != null) { + list.add(url); + } + } + } + + return list; + } +} diff --git a/http/samples/bridge/src/main/java/org/apache/felix/http/samples/bridge/StartupListener.java b/http/samples/bridge/src/main/java/org/apache/felix/http/samples/bridge/StartupListener.java new file mode 100644 index 00000000000..1b7ad607c13 --- /dev/null +++ b/http/samples/bridge/src/main/java/org/apache/felix/http/samples/bridge/StartupListener.java @@ -0,0 +1,37 @@ +/* + * 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.http.samples.bridge; + +import javax.servlet.ServletContextListener; +import javax.servlet.ServletContextEvent; + +public final class StartupListener + implements ServletContextListener +{ + private FrameworkService service; + + public void contextInitialized(ServletContextEvent event) + { + this.service = new FrameworkService(event.getServletContext()); + this.service.start(); + } + + public void contextDestroyed(ServletContextEvent event) + { + this.service.stop(); + } +} diff --git a/http/samples/bridge/src/main/webapp/WEB-INF/framework.properties b/http/samples/bridge/src/main/webapp/WEB-INF/framework.properties new file mode 100644 index 00000000000..95cfffaa273 --- /dev/null +++ b/http/samples/bridge/src/main/webapp/WEB-INF/framework.properties @@ -0,0 +1,23 @@ +# +# 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. +# + +org.osgi.framework.storage.clean = onFirstInit + +org.osgi.framework.system.packages.extra = \ + javax.servlet;javax.servlet.http;version=2.5 diff --git a/http/samples/bridge/src/main/webapp/WEB-INF/web.xml b/http/samples/bridge/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..86252368f2f --- /dev/null +++ b/http/samples/bridge/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,43 @@ + + + + + + + org.apache.felix.http.samples.bridge.StartupListener + + + + org.apache.felix.http.proxy.ProxyListener + + + + proxy + org.apache.felix.http.proxy.ProxyServlet + 1 + + + + proxy + /* + + + diff --git a/http/samples/cometd/DEPENDENCIES b/http/samples/cometd/DEPENDENCIES new file mode 100644 index 00000000000..292422ee32d --- /dev/null +++ b/http/samples/cometd/DEPENDENCIES @@ -0,0 +1,22 @@ +Apache Felix HTTP Service Cometd Sample +Copyright 2011 The Apache Software Foundation + +This software was developed at the Apache Software Foundation +(http://www.apache.org) and may have dependencies on other +Apache software licensed under Apache License 2.0. + +I. Included Third-Party Software + +This product includes software developed by +the cometd project (http://cometd.org) +Licensed under the Apache License 2.0. + +II. Used Third-Party Software + +This product uses software developed at +The OSGi Alliance (http://www.osgi.org). +Copyright (c) OSGi Alliance (2000, 2009). +Licensed under the Apache License 2.0. + +III. License Summary +- Apache License 2.0 diff --git a/http/samples/cometd/LICENSE b/http/samples/cometd/LICENSE new file mode 100644 index 00000000000..6b0b1270ff0 --- /dev/null +++ b/http/samples/cometd/LICENSE @@ -0,0 +1,203 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/http/samples/cometd/NOTICE b/http/samples/cometd/NOTICE new file mode 100644 index 00000000000..9c09aa8b3df --- /dev/null +++ b/http/samples/cometd/NOTICE @@ -0,0 +1,14 @@ +Apache Felix Http Service Cometd Sample +Copyright 2011 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +Licensed under the Apache License 2.0. + +This product includes software developed by +the cometd project (http://cometd.org) +Licensed under the Apache License 2.0. + +This product includes software developed by +the cometd project (http://cometd.org) +Licensed under the academic free license 1.2 or new BSD license. \ No newline at end of file diff --git a/http/samples/cometd/academic-2.1.txt b/http/samples/cometd/academic-2.1.txt new file mode 100644 index 00000000000..7c5ee34718a --- /dev/null +++ b/http/samples/cometd/academic-2.1.txt @@ -0,0 +1,153 @@ +The Academic Free License, v. 2.1 (for cometd.dojox): +********************************** + +This Academic Free License (the "License") applies to any original work of +authorship (the "Original Work") whose owner (the "Licensor") has placed the +following notice immediately following the copyright notice for the Original +Work: + +Licensed under the Academic Free License version 2.1 + +1) Grant of Copyright License. Licensor hereby grants You a world-wide, +royalty-free, non-exclusive, perpetual, sublicenseable license to do the +following: + +a) to reproduce the Original Work in copies; + +b) to prepare derivative works ("Derivative Works") based upon the Original +Work; + +c) to distribute copies of the Original Work and Derivative Works to the +public; + +d) to perform the Original Work publicly; and + +e) to display the Original Work publicly. + +2) Grant of Patent License. Licensor hereby grants You a world-wide, +royalty-free, non-exclusive, perpetual, sublicenseable license, under patent +claims owned or controlled by the Licensor that are embodied in the Original +Work as furnished by the Licensor, to make, use, sell and offer for sale the +Original Work and Derivative Works. + +3) Grant of Source Code License. The term "Source Code" means the preferred +form of the Original Work for making modifications to it and all available +documentation describing how to modify the Original Work. Licensor hereby +agrees to provide a machine-readable copy of the Source Code of the Original +Work along with each copy of the Original Work that Licensor distributes. +Licensor reserves the right to satisfy this obligation by placing a +machine-readable copy of the Source Code in an information repository +reasonably calculated to permit inexpensive and convenient access by You for as +long as Licensor continues to distribute the Original Work, and by publishing +the address of that information repository in a notice immediately following +the copyright notice that applies to the Original Work. + +4) Exclusions From License Grant. Neither the names of Licensor, nor the names +of any contributors to the Original Work, nor any of their trademarks or +service marks, may be used to endorse or promote products derived from this +Original Work without express prior written permission of the Licensor. Nothing +in this License shall be deemed to grant any rights to trademarks, copyrights, +patents, trade secrets or any other intellectual property of Licensor except as +expressly stated herein. No patent license is granted to make, use, sell or +offer to sell embodiments of any patent claims other than the licensed claims +defined in Section 2. No right is granted to the trademarks of Licensor even if +such marks are included in the Original Work. Nothing in this License shall be +interpreted to prohibit Licensor from licensing under different terms from this +License any Original Work that Licensor otherwise would have a right to +license. + +5) This section intentionally omitted. + +6) Attribution Rights. You must retain, in the Source Code of any Derivative +Works that You create, all copyright, patent or trademark notices from the +Source Code of the Original Work, as well as any notices of licensing and any +descriptive text identified therein as an "Attribution Notice." You must cause +the Source Code for any Derivative Works that You create to carry a prominent +Attribution Notice reasonably calculated to inform recipients that You have +modified the Original Work. + +7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that +the copyright in and to the Original Work and the patent rights granted herein +by Licensor are owned by the Licensor or are sublicensed to You under the terms +of this License with the permission of the contributor(s) of those copyrights +and patent rights. Except as expressly stated in the immediately proceeding +sentence, the Original Work is provided under this License on an "AS IS" BASIS +and WITHOUT WARRANTY, either express or implied, including, without limitation, +the warranties of NON-INFRINGEMENT, MERCHANTABILITY or FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. +This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No +license to Original Work is granted hereunder except under this disclaimer. + +8) Limitation of Liability. Under no circumstances and under no legal theory, +whether in tort (including negligence), contract, or otherwise, shall the +Licensor be liable to any person for any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License +or the use of the Original Work including, without limitation, damages for loss +of goodwill, work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses. This limitation of liability shall not +apply to liability for death or personal injury resulting from Licensor's +negligence to the extent applicable law prohibits such limitation. Some +jurisdictions do not allow the exclusion or limitation of incidental or +consequential damages, so this exclusion and limitation may not apply to You. + +9) Acceptance and Termination. If You distribute copies of the Original Work or +a Derivative Work, You must make a reasonable effort under the circumstances to +obtain the express assent of recipients to the terms of this License. Nothing +else but this License (or another written agreement between Licensor and You) +grants You permission to create Derivative Works based upon the Original Work +or to exercise any of the rights granted in Section 1 herein, and any attempt +to do so except under the terms of this License (or another written agreement +between Licensor and You) is expressly prohibited by U.S. copyright law, the +equivalent laws of other countries, and by international treaty. Therefore, by +exercising any of the rights granted to You in Section 1 herein, You indicate +Your acceptance of this License and all of its terms and conditions. + +10) Termination for Patent Action. This License shall terminate automatically +and You may no longer exercise any of the rights granted to You by this License +as of the date You commence an action, including a cross-claim or counterclaim, +against Licensor or any licensee alleging that the Original Work infringes a +patent. This termination provision shall not apply for an action alleging +patent infringement by combinations of the Original Work with other software or +hardware. + +11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this +License may be brought only in the courts of a jurisdiction wherein the +Licensor resides or in which Licensor conducts its primary business, and under +the laws of that jurisdiction excluding its conflict-of-law provisions. The +application of the United Nations Convention on Contracts for the International +Sale of Goods is expressly excluded. Any use of the Original Work outside the +scope of this License or after its termination shall be subject to the +requirements and penalties of the U.S. Copyright Act, 17 U.S.C. § 101 et +seq., the equivalent laws of other countries, and international treaty. This +section shall survive the termination of this License. + +12) Attorneys Fees. In any action to enforce the terms of this License or +seeking damages relating thereto, the prevailing party shall be entitled to +recover its costs and expenses, including, without limitation, reasonable +attorneys' fees and costs incurred in connection with such action, including +any appeal of such action. This section shall survive the termination of this +License. + +13) Miscellaneous. This License represents the complete agreement concerning +the subject matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent necessary to +make it enforceable. + +14) Definition of "You" in This License. "You" throughout this License, whether +in upper or lower case, means an individual or a legal entity exercising rights +under, and complying with all of the terms of, this License. For legal +entities, "You" includes any entity that controls, is controlled by, or is +under common control with you. For purposes of this definition, "control" means +(i) the power, direct or indirect, to cause the direction or management of such +entity, whether by contract or otherwise, or (ii) ownership of fifty percent +(50%) or more of the outstanding shares, or (iii) beneficial ownership of such +entity. + +15) Right to Use. You may use the Original Work in all ways not otherwise +restricted or conditioned by this License or by law, and Licensor promises not +to interfere with or be responsible for such uses by You. + +This license is Copyright (C) 2003-2004 Lawrence E. Rosen. All rights reserved. +Permission is hereby granted to copy and distribute this license without +modification. This license may not be modified without the express written +permission of its copyright owner. diff --git a/http/samples/cometd/new_bsd.txt b/http/samples/cometd/new_bsd.txt new file mode 100644 index 00000000000..2957312e260 --- /dev/null +++ b/http/samples/cometd/new_bsd.txt @@ -0,0 +1,29 @@ +The "New" BSD License (for cometd.dojox): +********************** + +Copyright (c) 2005-2009, The Dojo Foundation +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the Dojo Foundation nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/http/samples/cometd/pom.xml b/http/samples/cometd/pom.xml new file mode 100644 index 00000000000..6c55152b16d --- /dev/null +++ b/http/samples/cometd/pom.xml @@ -0,0 +1,102 @@ + + + + 4.0.0 + + org.apache.felix + org.apache.felix.http.parent + 9 + ../../parent/pom.xml + + + Apache Felix Http Samples - Cometd + org.apache.felix.http.samples.cometd + 2.4.0-SNAPSHOT + jar + + + scm:svn:http://svn.apache.org/repos/asf/felix/trunk/http/samples/cometd + scm:svn:https://svn.apache.org/repos/asf/felix/trunk/http/samples/cometd + http://svn.apache.org/viewvc/felix/trunk/http/samples/cometd/ + + + + 8 + + + + + + org.apache.felix + maven-bundle-plugin + + + + org.apache.felix.http.samples.cometd.Activator + + + org.apache.felix.http.samples.cometd.* + + + *;resolution:=optional + + + {maven-resources}, {maven-dependencies}, + src-web=src-web + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + + + + + javax.servlet + javax.servlet-api + + + org.osgi + osgi.core + + + org.osgi + osgi.cmpn + + + org.apache.felix + org.apache.felix.http.cometd + 3.0.0-SNAPSHOT + + + + diff --git a/http/samples/cometd/src-web/dojo/dojo.js b/http/samples/cometd/src-web/dojo/dojo.js new file mode 100644 index 00000000000..9f5f7d5e025 --- /dev/null +++ b/http/samples/cometd/src-web/dojo/dojo.js @@ -0,0 +1,15 @@ +/* + Copyright (c) 2004-2011, The Dojo Foundation All Rights Reserved. + Available via Academic Free License >= 2.1 OR the modified BSD license. + see: http://dojotoolkit.org/license for details +*/ + +/* + This is an optimized version of Dojo, built for deployment and not for + development. To get sources and documentation, please visit: + + http://dojotoolkit.org +*/ + +//>>built +(function(_1,_2){var _3=function(){},_4=function(it){for(var p in it){return 0;}return 1;},_5={}.toString,_6=function(it){return _5.call(it)=="[object Function]";},_7=function(it){return _5.call(it)=="[object String]";},_8=function(it){return _5.call(it)=="[object Array]";},_9=function(_a,_b){if(_a){for(var i=0;i<_a.length;){_b(_a[i++]);}}},_c=function(_d,_e){for(var p in _e){_d[p]=_e[p];}return _d;},_f=function(_10,_11){return _c(new Error(_10),{src:"dojoLoader",info:_11});},_12=1,uid=function(){return "_"+_12++;},req=function(_13,_14,_15){return _16(_13,_14,_15,0,req);},_17=this,doc=_17.document,_18=doc&&doc.createElement("DiV"),has=req.has=function(_19){return _6(_1a[_19])?(_1a[_19]=_1a[_19](_17,doc,_18)):_1a[_19];},_1a=has.cache=_2.hasCache;has.add=function(_1b,_1c,now,_1d){(_1a[_1b]===undefined||_1d)&&(_1a[_1b]=_1c);return now&&has(_1b);};false&&has.add("host-node",typeof process=="object"&&/node(\.exe)?$/.test(process.execPath));if(0){require("./_base/configNode.js").config(_2);_2.loaderPatch.nodeRequire=require;}false&&has.add("host-rhino",typeof load=="function"&&(typeof Packages=="function"||typeof Packages=="object"));if(0){for(var _1e=_1.baseUrl||".",arg,_1f=this.arguments,i=0;i<_1f.length;){arg=(_1f[i++]+"").split("=");if(arg[0]=="baseUrl"){_1e=arg[1];break;}}load(_1e+"/_base/configRhino.js");rhinoDojoConfig(_2,_1e,_1f);}for(var p in _1.has){has.add(p,_1.has[p],0,1);}var _20=1,_21=2,_22=3,_23=4,_24=5;if(0){_20="requested";_21="arrived";_22="not-a-module";_23="executing";_24="executed";}var _25=0,_26="sync",xd="xd",_27=[],_28=0,_29=_3,_2a=_3,_2b;if(1){req.isXdUrl=_3;req.initSyncLoader=function(_2c,_2d,_2e){if(!_28){_28=_2c;_29=_2d;_2a=_2e;}return {sync:_26,xd:xd,arrived:_21,nonmodule:_22,executing:_23,executed:_24,syncExecStack:_27,modules:_2f,execQ:_30,getModule:_31,injectModule:_32,setArrived:_33,signal:_34,finishExec:_35,execModule:_36,dojoRequirePlugin:_28,getLegacyMode:function(){return _25;},holdIdle:function(){_74++;},releaseIdle:function(){_37();}};};if(1){var _38=location.protocol,_39=location.host,_3a=!_39;req.isXdUrl=function(url){if(_3a||/^\./.test(url)){return false;}if(/^\/\//.test(url)){return true;}var _3b=url.match(/^([^\/\:]+\:)\/\/([^\/]+)/);return _3b&&(_3b[1]!=_38||_3b[2]!=_39);};true||has.add("dojo-xhr-factory",1);has.add("dojo-force-activex-xhr",1&&!doc.addEventListener&&window.location.protocol=="file:");has.add("native-xhr",typeof XMLHttpRequest!="undefined");if(has("native-xhr")&&!has("dojo-force-activex-xhr")){_2b=function(){return new XMLHttpRequest();};}else{for(var _3c=["Msxml2.XMLHTTP","Microsoft.XMLHTTP","Msxml2.XMLHTTP.4.0"],_3d,i=0;i<3;){try{_3d=_3c[i++];if(new ActiveXObject(_3d)){break;}}catch(e){}}_2b=function(){return new ActiveXObject(_3d);};}req.getXhr=_2b;has.add("dojo-gettext-api",1);req.getText=function(url,_3e,_3f){var xhr=_2b();xhr.open("GET",_40(url),false);xhr.send(null);if(xhr.status==200||(!location.host&&!xhr.status)){if(_3f){_3f(xhr.responseText,_3e);}}else{throw _f("xhrFailed",xhr.status);}return xhr.responseText;};}}else{req.async=1;}var _41=new Function("__text","return eval(__text);");req.eval=function(_42,_43){return _41(_42+"\r\n////@ sourceURL="+_43);};var _44={},_45="error",_34=req.signal=function(_46,_47){var _48=_44[_46];_9(_48&&_48.slice(0),function(_49){_49.apply(null,_8(_47)?_47:[_47]);});},on=req.on=function(_4a,_4b){var _4c=_44[_4a]||(_44[_4a]=[]);_4c.push(_4b);return {remove:function(){for(var i=0;i<_4c.length;i++){if(_4c[i]===_4b){_4c.splice(i,1);return;}}}};};var _4d=[],_4e={},_4f=[],_50={},_51={},_52=[],_2f={},_53="",_54={},_55={},_56={};if(1){var _57=function(_58){for(var p in _55){var _59=p.match(/^url\:(.+)/);if(_59){_54[_5a(_59[1],_58)]=_55[p];}else{if(p!="*noref"){_54[_5b(p,_58).mid]=_55[p];}}}_55={};},_5c=function(map,_5d,_5e){_5d.splice(0,_5d.length);var p,i,_5f,_60=0;for(p in map){_5d.push([p,map[p]]);if(map[p]==_5e){_60=p;}}_5d.sort(function(lhs,rhs){return rhs[0].length-lhs[0].length;});for(i=0;i<_5d.length;){_5f=_5d[i++];_5f[2]=new RegExp("^"+_5f[0].replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g,function(c){return "\\"+c;})+"(/|$)");_5f[3]=_5f[0].length+1;}return _60;},_61=function(_62,_63){var _64=_62.name;if(!_64){_64=_62;_62={name:_64};}_62=_c({main:"main",mapProg:[]},_62);_62.location=(_63||"")+(_62.location?_62.location:_64);_62.reverseName=_5c(_62.packageMap,_62.mapProg,_64);if(!_62.main.indexOf("./")){_62.main=_62.main.substring(2);}_c(_4e,_62.paths);_50[_64]=_62;_51[_64]=_64;},_65=function(_66,_67){for(var p in _66){if(p=="waitSeconds"){req.waitms=(_66[p]||0)*1000;}if(p=="cacheBust"){_53=_66[p]?(_7(_66[p])?_66[p]:(new Date()).getTime()+""):"";}if(p=="baseUrl"||p=="combo"){req[p]=_66[p];}if(1&&p=="async"){var _68=_66[p];req.legacyMode=_25=(_7(_68)&&/sync|legacyAsync/.test(_68)?_68:(!_68?"sync":false));req.async=!_25;}if(_66[p]!==_1a){req.rawConfig[p]=_66[p];p!="has"&&has.add("config-"+p,_66[p],0,_67);}}if(!req.baseUrl){req.baseUrl="./";}if(!/\/$/.test(req.baseUrl)){req.baseUrl+="/";}for(p in _66.has){has.add(p,_66.has[p],0,_67);}_9(_66.packages,_61);for(_1e in _66.packagePaths){_9(_66.packagePaths[_1e],function(_69){_61(_69,_1e+"/");});}_5c(_c(_4e,_66.paths),_4f);_9(_66.aliases,function(_6a){if(_7(_6a[0])){_6a[0]=new RegExp("^"+_6a[0].replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g,function(c){return "\\"+c;})+"$");}_4d.push(_6a);});_5c(_c(_51,_66.packageMap),_52);if(_66.cache){_57();_55=_66.cache;if(_66.cache["*noref"]){_57();}}_34("config",[_66,req.rawConfig]);};if(has("dojo-cdn")||1){for(var _6b,src,_6c,_6d=doc.getElementsByTagName("script"),i=0;i<_6d.length&&!_6c;i++){if((src=_6d[i].getAttribute("src"))&&(_6c=src.match(/(.*)\/?dojo\.js(\W|$)/i))){_1.baseUrl=_6b=_1.baseUrl||_2.baseUrl||_6c[1];src=(_6d[i].getAttribute("data-dojo-config")||_6d[i].getAttribute("djConfig"));if(src){_56=req.eval("({ "+src+" })","data-dojo-config");}if(0){var _6e=_6d[i].getAttribute("data-main");if(_6e){_56.deps=_56.deps||[_6e];}}}}}if(0){try{if(window.parent!=window&&window.parent.require){var doh=window.parent.require("doh");doh&&_c(_56,doh.testConfig);}}catch(e){}}req.rawConfig={};_65(_2,1);_65(_1,1);_65(_56,1);if(has("dojo-cdn")){_50.dojo.location=_6b;_50.dijit.location=_6b+"../dijit/";_50.dojox.location=_6b+"../dojox/";}}else{_4e=_2.paths;_4f=_2.pathsMapProg;_50=_2.packs;_4d=_2.aliases;_51=_2.packageMap;_52=_2.packageMapProg;_2f=_2.modules;_54=_2.cache;_53=_2.cacheBust;req.rawConfig=_2;}if(0){req.combo=req.combo||{add:_3};var _6f=0,_70=[],_71=null;}var _72=function(_73){_74++;_9(_73.deps,_32);if(0&&_6f&&!_71){_71=setTimeout(function(){_6f=0;_71=null;req.combo.done(function(_75,url){var _76=function(){_77(0,_75);_78();};_70.push(_75);_79=_75;req.injectUrl(url,_76,_75);_79=0;},req);},0);}_37();},_16=function(a1,a2,a3,_7a,_7b){var _7c,_7d;if(_7(a1)){_7c=_31(a1,_7a,true);if(_7c&&_7c.executed){return _7c.result;}throw _f("undefinedModule",a1);}if(!_8(a1)){_65(a1);a1=a2;a2=a3;}if(_8(a1)){if(!a1.length){a2&&a2();}else{_7d="require*"+uid();for(var mid,_7e=[],i=0;i_a6){_a7=_6(_a8[1])?mid.replace(_a8[0],_a8[1]):_a8[1];}});if(_a7){return _95(_a7,0,_97,_98,_99,_9a,_9b,_9c);}_a2=_98[mid];if(_a2){return _9c?_7f(_a2.pid,_a2.mid,_a2.pack,_a2.url,_a5):_98[mid];}}_a0=_8c(mid,_9b);if(_a0){url=_a0[1]+mid.substring(_a0[3]-1);}else{if(pid){url=_9d.location+"/"+_9e;}else{if(has("config-tlmSiblingOfDojo")){url="../"+mid;}else{url=mid;}}}if(!(/(^\/)|(\:)/.test(url))){url=_99+url;}url+=".js";return _7f(pid,mid,_9d,_8e(url),_a5);},_5b=function(mid,_aa){return _95(mid,_aa,_50,_2f,req.baseUrl,_52,_4f);},_ab=function(_ac,_ad,_ae){return _ac.normalize?_ac.normalize(_ad,function(mid){return _af(mid,_ae);}):_af(_ad,_ae);},_b0=0,_31=function(mid,_b1,_b2){var _b3,_b4,_b5,_b6;_b3=mid.match(/^(.+?)\!(.*)$/);if(_b3){_b4=_31(_b3[1],_b1,_b2);if(1&&_25==_26&&!_b4.executed){_32(_b4);if(_b4.injected===_21&&!_b4.executed){_74++;_36(_b4);_37();}if(_b4.executed){_b7(_b4);}else{_30.unshift(_b4);}}if(_b4.executed===_24&&!_b4.load){_b7(_b4);}if(_b4.load){_b5=_ab(_b4,_b3[2],_b1);mid=(_b4.mid+"!"+(_b4.dynamic?++_b0+"!":"")+_b5);}else{_b5=_b3[2];mid=_b4.mid+"!"+(++_b0)+"!waitingForPlugin";}_b6={plugin:_b4,mid:mid,req:_81(_b1),prid:_b5};}else{_b6=_5b(mid,_b1);}return _2f[_b6.mid]||(!_b2&&(_2f[_b6.mid]=_b6));},_af=req.toAbsMid=function(mid,_b8){return _5b(mid,_b8).mid;},_5a=req.toUrl=function(_b9,_ba){var _bb=_b9.match(/(.+)(\.[^\/\.]+?)$/),_bc=(_bb&&_bb[1])||_b9,ext=(_bb&&_bb[2])||"",_bd=_5b(_bc,_ba),url=_bd.url;url=typeof _bd.pid=="string"?url.substring(0,url.length-3):url;return _40(url+ext);},_be={injected:_21,executed:_24,def:_22,result:_22},_bf=function(mid){return _2f[mid]=_c({mid:mid},_be);},_c0=_bf("require"),_c1=_bf("exports"),_c2=_bf("module"),_c3=function(_c4,_c5){req.trace("loader-run-factory",[_c4.mid]);var _c6=_c4.def,_c7;1&&_27.unshift(_c4);if(has("config-dojo-loader-catches")){try{_c7=_6(_c6)?_c6.apply(null,_c5):_c6;}catch(e){_34(_45,_c4.result=_f("factoryThrew",[_c4,e]));}}else{_c7=_6(_c6)?_c6.apply(null,_c5):_c6;}_c4.result=_c7===undefined&&_c4.cjs?_c4.cjs.exports:_c7;1&&_27.shift(_c4);},_c8={},_c9=0,_b7=function(_ca){var _cb=_ca.result;_ca.dynamic=_cb.dynamic;_ca.normalize=_cb.normalize;_ca.load=_cb.load;return _ca;},_cc=function(_cd){var map={};_9(_cd.loadQ,function(_ce){var _cf=_ce.mid,_d0=_ab(_cd,_ce.prid,_ce.req.module),mid=_cd.dynamic?_ce.mid.replace(/waitingForPlugin$/,_d0):(_cd.mid+"!"+_d0),_d1=_c(_c({},_ce),{mid:mid,prid:_d0,injected:0});if(!_2f[mid]){_e2(_2f[mid]=_d1);}map[_ce.mid]=_2f[mid];_33(_ce);delete _2f[_ce.mid];});_cd.loadQ=0;var _d2=function(_d3){for(var _d4,_d5=_d3.deps||[],i=0;i<_d5.length;i++){_d4=map[_d5[i].mid];if(_d4){_d5[i]=_d4;}}};for(var p in _2f){_d2(_2f[p]);}_9(_30,_d2);},_35=function(_d6){req.trace("loader-finish-exec",[_d6.mid]);_d6.executed=_24;_d6.defOrder=_c9++;1&&_9(_d6.provides,function(cb){cb();});if(_d6.loadQ){_b7(_d6);_cc(_d6);}for(i=0;i<_30.length;){if(_30[i]===_d6){_30.splice(i,1);}else{i++;}}},_d7=[],_36=function(_d8,_d9){if(_d8.executed===_23){req.trace("loader-circular-dependency",[_d7.concat(mid).join("->")]);return (!_d8.def||_d9)?_c8:(_d8.cjs&&_d8.cjs.exports);}if(!_d8.executed){if(!_d8.def){return _c8;}var mid=_d8.mid,_da=_d8.deps||[],arg,_db,_dc=[],i=0;if(0){_d7.push(mid);req.trace("loader-exec-module",["exec",_d7.length,mid]);}_d8.executed=_23;while(i<_da.length){arg=_da[i++];_db=((arg===_c0)?_81(_d8):((arg===_c1)?_d8.cjs.exports:((arg===_c2)?_d8.cjs:_36(arg,_d9))));if(_db===_c8){_d8.executed=0;req.trace("loader-exec-module",["abort",mid]);0&&_d7.pop();return _c8;}_dc.push(_db);}_c3(_d8,_dc);_35(_d8);}0&&_d7.pop();return _d8.result;},_74=0,_78=function(){if(_74){return;}_74++;_29();for(var _dd,_de,i=0;i<_30.length;){_dd=_c9;_de=_30[i];_36(_de);if(_dd!=_c9){_29();i=0;}else{i++;}}_37();},_37=function(){_74--;if(_8b()){_34("idle",[]);}};if(0){req.undef=function(_df,_e0){var _e1=_31(_df,_e0);_33(_e1);delete _2f[_e1.mid];};}if(1){if(has("dojo-loader-eval-hint-url")===undefined){has.add("dojo-loader-eval-hint-url",1);}var _40=function(url){url+="";return url+(_53?((/\?/.test(url)?"&":"?")+_53):"");},_e2=function(_e3){var _e4=_e3.plugin;if(_e4.executed===_24&&!_e4.load){_b7(_e4);}var _e5=function(def){_e3.result=def;_33(_e3);_35(_e3);_78();};_87(_e3);if(_e4.load){_e4.load(_e3.prid,_e3.req,_e5);}else{if(_e4.loadQ){_e4.loadQ.push(_e3);}else{_30.unshift(_e4);_32(_e4);if(_e4.load){_e4.load(_e3.prid,_e3.req,_e5);}else{_e4.loadQ=[_e3];}}}},_e6=0,_79=0,_e7=0,_e8=function(_e9,_ea){_e7=1;if(has("config-dojo-loader-catches")){try{if(_e9===_e6){_e6.call(null);}else{req.eval(_e9,has("dojo-loader-eval-hint-url")?_ea.url:_ea.mid);}}catch(e){_34(_45,_f("evalModuleThrew",_ea));}}else{if(_e9===_e6){_e6.call(null);}else{req.eval(_e9,has("dojo-loader-eval-hint-url")?_ea.url:_ea.mid);}}_e7=0;},_32=function(_eb){var mid=_eb.mid,url=_eb.url;if(_eb.executed||_eb.injected||_86[mid]||(_eb.url&&((_eb.pack&&_86[_eb.url]===_eb.pack)||_86[_eb.url]==1))){return;}if(0){var _ec=0;if(_eb.plugin&&_eb.plugin.isCombo){req.combo.add(_eb.plugin.mid,_eb.prid,0,req);_ec=1;}else{if(!_eb.plugin){_ec=req.combo.add(0,_eb.mid,_eb.url,req);}}if(_ec){_87(_eb);_6f=1;return;}}if(_eb.plugin){_e2(_eb);return;}_87(_eb);var _ed=function(){_77(_eb);if(_eb.injected!==_21){_33(_eb);_c(_eb,_be);}if(1&&_25){!_27.length&&_78();}else{_78();}};_e6=_54[mid]||_54[_eb.cacheId];if(_e6){req.trace("loader-inject",["cache",_eb.mid,url]);_e8(_e6,_eb);_ed();return;}if(1&&_25){if(_eb.isXd){_25==_26&&(_25=xd);}else{if(_eb.isAmd&&_25!=_26){}else{var _ee=function(_ef){if(_25==_26){_27.unshift(_eb);_e8(_ef,_eb);_27.shift();_77(_eb);if(!_eb.cjs){_33(_eb);_35(_eb);}if(_eb.finish){var _f0=mid+"*finish",_f1=_eb.finish;delete _eb.finish;def(_f0,["dojo",("dojo/require!"+_f1.join(",")).replace(/\./g,"/")],function(_f2){_9(_f1,function(mid){_f2.require(mid);});});_30.unshift(_31(_f0));}_ed();}else{_ef=_2a(_eb,_ef);if(_ef){_e8(_ef,_eb);_ed();}else{_79=_eb;req.injectUrl(_40(url),_ed,_eb);_79=0;}}};req.trace("loader-inject",["xhr",_eb.mid,url,_25!=_26]);if(has("config-dojo-loader-catches")){try{req.getText(url,_25!=_26,_ee);}catch(e){_34(_45,_f("xhrInjectFailed",[_eb,e]));}}else{req.getText(url,_25!=_26,_ee);}return;}}}req.trace("loader-inject",["script",_eb.mid,url]);_79=_eb;req.injectUrl(_40(url),_ed,_eb);_79=0;},_f3=function(_f4,_f5,def){req.trace("loader-define-module",[_f4.mid,_f5]);if(0&&_f4.plugin&&_f4.plugin.isCombo){_f4.result=_6(def)?def():def;_33(_f4);_35(_f4);return _f4;}var mid=_f4.mid;if(_f4.injected===_21){_34(_45,_f("multipleDefine",_f4));return _f4;}_c(_f4,{deps:_f5,def:def,cjs:{id:_f4.mid,uri:_f4.url,exports:(_f4.result={}),setExports:function(_f6){_f4.cjs.exports=_f6;}}});for(var i=0;i<_f5.length;i++){_f5[i]=_31(_f5[i],_f4);}if(1&&_25&&!_86[mid]){_72(_f4);_30.push(_f4);_78();}_33(_f4);if(!_6(def)&&!_f5.length){_f4.result=def;_35(_f4);}return _f4;},_77=function(_f7,_f8){_57(_f7);var _f9=[],_fa,_fb;while(_85.length){_fb=_85.shift();_f8&&(_fb[0]=_f8.shift());_fa=_fb[0]&&_31(_fb[0])||_f7;_f9.push(_f3(_fa,_fb[1],_fb[2]));}_9(_f9,_72);};}var _fc=0,_8a=_3,_fd=_3;if(1){_8a=function(){_fc&&clearTimeout(_fc);_fc=0;},_fd=function(){_8a();req.waitms&&(_fc=setTimeout(function(){_8a();_34(_45,_f("timeout",_86));},req.waitms));};}if(1){has.add("ie-event-behavior",doc.attachEvent&&(typeof opera==="undefined"||opera.toString()!="[object Opera]"));}if(1&&(1||1)){var _fe=function(_ff,_100,_101,_102){if(!has("ie-event-behavior")){_ff.addEventListener(_100,_102,false);return function(){_ff.removeEventListener(_100,_102,false);};}else{_ff.attachEvent(_101,_102);return function(){_ff.detachEvent(_101,_102);};}},_103=_fe(window,"load","onload",function(){req.pageLoaded=1;doc.readyState!="complete"&&(doc.readyState="complete");_103();});if(1){var _104=doc.getElementsByTagName("script")[0],_105=_104.parentNode;req.injectUrl=function(url,_106,_107){_fd();var node=_107.node=doc.createElement("script"),_108=function(e){e=e||window.event;var node=e.target||e.srcElement;if(e.type==="load"||/complete|loaded/.test(node.readyState)){_109();_106&&_106();}},_109=_fe(node,"load","onreadystatechange",_108);node.type="text/javascript";node.charset="utf-8";node.src=url;_105.insertBefore(node,_104);return node;};}}if(1){req.log=function(){try{for(var i=0;i0){_124._delayTimer=setTimeout(_125,de);return _124;}_125();return _124;},_play:function(_126){var _127=this;if(_127._delayTimer){_127._clearTimer();}_127._startTime=new Date().valueOf();if(_127._paused){_127._startTime-=_127.duration*_127._percent;}_127._active=true;_127._paused=false;var _128=_127.curve.getValue(_127._getStep());if(!_127._percent){if(!_127._startRepeatCount){_127._startRepeatCount=_127.repeat;}_127._fire("onBegin",[_128]);}_127._fire("onPlay",[_128]);_127._cycle();return _127;},pause:function(){var _129=this;if(_129._delayTimer){_129._clearTimer();}_129._stopTimer();if(!_129._active){return _129;}_129._paused=true;_129._fire("onPause",[_129.curve.getValue(_129._getStep())]);return _129;},gotoPercent:function(_12a,_12b){var _12c=this;_12c._stopTimer();_12c._active=_12c._paused=true;_12c._percent=_12a;if(_12b){_12c.play();}return _12c;},stop:function(_12d){var _12e=this;if(_12e._delayTimer){_12e._clearTimer();}if(!_12e._timer){return _12e;}_12e._stopTimer();if(_12d){_12e._percent=1;}_12e._fire("onStop",[_12e.curve.getValue(_12e._getStep())]);_12e._active=_12e._paused=false;return _12e;},status:function(){if(this._active){return this._paused?"paused":"playing";}return "stopped";},_cycle:function(){var _12f=this;if(_12f._active){var curr=new Date().valueOf();var step=(curr-_12f._startTime)/(_12f.duration);if(step>=1){step=1;}_12f._percent=step;if(_12f.easing){step=_12f.easing(step);}_12f._fire("onAnimate",[_12f.curve.getValue(step)]);if(_12f._percent<1){_12f._startTimer();}else{_12f._active=false;if(_12f.repeat>0){_12f.repeat--;_12f.play(null,true);}else{if(_12f.repeat==-1){_12f.play(null,true);}else{if(_12f._startRepeatCount){_12f.repeat=_12f._startRepeatCount;_12f._startRepeatCount=0;}}}_12f._percent=0;_12f._fire("onEnd",[_12f.node]);!_12f.repeat&&_12f._stopTimer();}}return _12f;},_clearTimer:function(){clearTimeout(this._delayTimer);delete this._delayTimer;}});var ctr=0,_130=null,_131={run:function(){}};lang.extend(dojo.Animation,{_startTimer:function(){if(!this._timer){this._timer=_11c.connect(_131,"run",this,"_cycle");ctr++;}if(!_130){_130=setInterval(lang.hitch(_131,"run"),this.rate);}},_stopTimer:function(){if(this._timer){_11c.disconnect(this._timer);this._timer=null;ctr--;}if(ctr<=0){clearInterval(_130);_130=null;ctr=0;}}});var _132=has("ie")?function(node){var ns=node.style;if(!ns.width.length&&_11d.get(node,"width")=="auto"){ns.width="auto";}}:function(){};dojo._fade=function(args){args.node=dom.byId(args.node);var _133=_11e({properties:{}},args),_134=(_133.properties.opacity={});_134.start=!("start" in _133)?function(){return +_11d.get(_133.node,"opacity")||0;}:_133.start;_134.end=_133.end;var anim=dojo.animateProperty(_133);_11c.connect(anim,"beforeBegin",lang.partial(_132,_133.node));return anim;};dojo.fadeIn=function(args){return dojo._fade(_11e({end:1},args));};dojo.fadeOut=function(args){return dojo._fade(_11e({end:0},args));};dojo._defaultEasing=function(n){return 0.5+((Math.sin((n+1.5)*Math.PI))/2);};var _135=function(_136){this._properties=_136;for(var p in _136){var prop=_136[p];if(prop.start instanceof _11b){prop.tempColor=new _11b();}}};_135.prototype.getValue=function(r){var ret={};for(var p in this._properties){var prop=this._properties[p],_137=prop.start;if(_137 instanceof _11b){ret[p]=_11b.blendColors(_137,prop.end,r,prop.tempColor).toCss();}else{if(!lang.isArray(_137)){ret[p]=((prop.end-_137)*r)+_137+(p!="opacity"?prop.units||"px":0);}}}return ret;};dojo.animateProperty=function(args){var n=args.node=dom.byId(args.node);if(!args.easing){args.easing=dojo._defaultEasing;}var anim=new dojo.Animation(args);_11c.connect(anim,"beforeBegin",anim,function(){var pm={};for(var p in this.properties){if(p=="width"||p=="height"){this.node.display="block";}var prop=this.properties[p];if(lang.isFunction(prop)){prop=prop(n);}prop=pm[p]=_11e({},(lang.isObject(prop)?prop:{end:prop}));if(lang.isFunction(prop.start)){prop.start=prop.start(n);}if(lang.isFunction(prop.end)){prop.end=prop.end(n);}var _138=(p.toLowerCase().indexOf("color")>=0);function _139(node,p){var v={height:node.offsetHeight,width:node.offsetWidth}[p];if(v!==undefined){return v;}v=_11d.get(node,p);return (p=="opacity")?+v:(_138?v:parseFloat(v));};if(!("end" in prop)){prop.end=_139(n,p);}else{if(!("start" in prop)){prop.start=_139(n,p);}}if(_138){prop.start=new _11b(prop.start);prop.end=new _11b(prop.end);}else{prop.start=(p=="opacity")?+prop.start:parseFloat(prop.start);}}this.curve=new _135(pm);});_11c.connect(anim,"onAnimate",lang.hitch(_11d,"set",anim.node));return anim;};dojo.anim=function(node,_13a,_13b,_13c,_13d,_13e){return dojo.animateProperty({node:node,duration:_13b||dojo.Animation.prototype.duration,properties:_13a,easing:_13c,onEnd:_13d}).play(_13e||0);};return {_Line:dojo._Line,Animation:dojo.Animation,_fade:dojo._fade,fadeIn:dojo.fadeIn,fadeOut:dojo.fadeOut,_defaultEasing:dojo._defaultEasing,animateProperty:dojo.animateProperty,anim:dojo.anim};});},"dojo/dom-form":function(){define("dojo/dom-form",["./_base/lang","./dom","./io-query","./json"],function(lang,dom,ioq,json){function _13f(obj,name,_140){if(_140===null){return;}var val=obj[name];if(typeof val=="string"){obj[name]=[val,_140];}else{if(lang.isArray(val)){val.push(_140);}else{obj[name]=_140;}}};var _141="file|submit|image|reset|button";var form={fieldToObject:function fieldToObject(_142){var ret=null;_142=dom.byId(_142);if(_142){var _143=_142.name,type=(_142.type||"").toLowerCase();if(_143&&type&&!_142.disabled){if(type=="radio"||type=="checkbox"){if(_142.checked){ret=_142.value;}}else{if(_142.multiple){ret=[];var _144=[_142.firstChild];while(_144.length){for(var node=_144.pop();node;node=node.nextSibling){if(node.nodeType==1&&node.tagName.toLowerCase()=="option"){if(node.selected){ret.push(node.value);}}else{if(node.nextSibling){_144.push(node.nextSibling);}if(node.firstChild){_144.push(node.firstChild);}break;}}}}else{ret=_142.value;}}}}return ret;},toObject:function formToObject(_145){var ret={},_146=dom.byId(_145).elements;for(var i=0,l=_146.length;i=0;i--){var node=(_1ee?this._cloneNode(ary[i]):ary[i]);if(ary._runParse&&dojo.parser&&dojo.parser.parse){if(!_1f0){_1f0=_1ef.ownerDocument.createElement("div");}_1f0.appendChild(node);dojo.parser.parse(_1f0);node=_1f0.firstChild;while(_1f0.firstChild){_1f0.removeChild(_1f0.firstChild);}}if(i==_1f1-1){_1de.place(node,_1ef,_1ed);}else{_1ef.parentNode.insertBefore(node,_1ef);}_1ef=node;}},attr:awc(_1e5(_1e0),_1e2),style:awc(_1e5(_1e1),_1e2),addClass:aafe(_1dd.add),removeClass:aafe(_1dd.remove),replaceClass:aafe(_1dd.replace),toggleClass:aafe(_1dd.toggle),empty:aafe(_1de.empty),removeAttr:aafe(_1e0.remove),position:aam(_1df.position),marginBox:aam(_1df.getMarginBox),place:function(_1f2,_1f3){var item=_1db(_1f2)[0];return this.forEach(function(node){_1de.place(node,item,_1f3);});},orphan:function(_1f4){return (_1f4?_1db._filterResult(this,_1f4):this).forEach(_1e3);},adopt:function(_1f5,_1f6){return _1db(_1f5).place(this[0],_1f6)._stash(this);},query:function(_1f7){if(!_1f7){return this;}var ret=new _1e4;this.map(function(node){_1db(_1f7,node).forEach(function(_1f8){if(_1f8!==undefined){ret.push(_1f8);}});});return ret._stash(this);},filter:function(_1f9){var a=arguments,_1fa=this,_1fb=0;if(typeof _1f9=="string"){_1fa=_1db._filterResult(this,a[0]);if(a.length==1){return _1fa._stash(this);}_1fb=1;}return this._wrap(_1dc.filter(_1fa,a[_1fb],a[_1fb+1]),this);},addContent:function(_1fc,_1fd){_1fc=this._normalize(_1fc,this[0]);for(var i=0,node;(node=this[i]);i++){this._place(_1fc,node,_1fd,i>0);}return this;}});return _1e4;});},"dojo/query":function(){define(["./_base/kernel","./has","./dom","./on","./_base/array","./_base/lang","./selector/_loader","./selector/_loader!default"],function(dojo,has,dom,on,_1fe,lang,_1ff,_200){"use strict";has.add("array-extensible",function(){return lang.delegate([],{length:1}).length==1&&!has("bug-for-in-skips-shadowed");});var ap=Array.prototype,aps=ap.slice,apc=ap.concat,_201=_1fe.forEach;var tnl=function(a,_202,_203){var _204=new (_203||this._NodeListCtor||nl)(a);return _202?_204._stash(_202):_204;};var _205=function(f,a,o){a=[0].concat(aps.call(a,0));o=o||dojo.global;return function(node){a[0]=node;return f.apply(o,a);};};var _206=function(f,o){return function(){this.forEach(_205(f,arguments,o));return this;};};var _207=function(f,o){return function(){return this.map(_205(f,arguments,o));};};var _208=function(f,o){return function(){return this.filter(_205(f,arguments,o));};};var _209=function(f,g,o){return function(){var a=arguments,body=_205(f,a,o);if(g.call(o||dojo.global,a)){return this.map(body);}this.forEach(body);return this;};};var _20a=function(_20b){var _20c=this instanceof nl&&has("array-extensible");if(typeof _20b=="number"){_20b=Array(_20b);}var _20d=(_20b&&"length" in _20b)?_20b:arguments;if(_20c||!_20d.sort){var _20e=_20c?this:[],l=_20e.length=_20d.length;for(var i=0;i0;};_21f.filter=_21d.filter||function(_223,_224,root){return _21f(_224,root).filter(function(node){return _1fe.indexOf(_223,node)>-1;});};if(typeof _21d!="function"){var _225=_21d.search;_21d=function(_226,root){return _225(root||document,_226);};}return _21f;};var _219=_21c(_200,_20a);dojo.query=_21c(_200,function(_227){return _20a(_227);});_219.load=function(id,_228,_229,_22a){_1ff.load(id,_228,function(_22b){_229(_21c(_22b,_20a));});};dojo._filterQueryResult=_219._filterResult=function(_22c,_22d,root){return new _20a(_219.filter(_22c,_22d,root));};dojo.NodeList=_219.NodeList=_20a;return _219;});},"dojo/has":function(){define(["require"],function(_22e){var has=_22e.has||function(){};if(!1){var _22f=typeof window!="undefined"&&typeof location!="undefined"&&typeof document!="undefined"&&window.location==location&&window.document==document,_230=this,doc=_22f&&document,_231=doc&&doc.createElement("DiV"),_232={};has=function(name){return typeof _232[name]=="function"?(_232[name]=_232[name](_230,doc,_231)):_232[name];};has.cache=_232;has.add=function(name,test,now,_233){(typeof _232[name]=="undefined"||_233)&&(_232[name]=test);return now&&has(name);};true||has.add("host-browser",_22f);true||has.add("dom",_22f);true||has.add("dojo-dom-ready-api",1);true||has.add("dojo-sniff",1);}if(1){var _234=navigator.userAgent;has.add("dom-addeventlistener",!!document.addEventListener);has.add("touch","ontouchstart" in document);has.add("device-width",screen.availWidth||innerWidth);has.add("agent-ios",!!_234.match(/iPhone|iP[ao]d/));has.add("agent-android",_234.indexOf("android")>1);}has.clearElement=function(_235){_235.innerHTML="";return _235;};has.normalize=function(id,_236){var _237=id.match(/[\?:]|[^:\?]*/g),i=0,get=function(skip){var term=_237[i++];if(term==":"){return 0;}else{if(_237[i++]=="?"){if(!skip&&has(term)){return get();}else{get(true);return get(skip);}}return term||0;}};id=get();return id&&_236(id);};has.load=function(id,_238,_239){if(id){_238([id],_239);}else{_239();}};return has;});},"dojo/_base/loader":function(){define(["./kernel","../has","require","module","./json","./lang","./array"],function(dojo,has,_23a,_23b,json,lang,_23c){if(!1){console.error("cannot load the Dojo v1.x loader with a foreign loader");return 0;}var _23d=function(id){return {src:_23b.id,id:id};},_23e=function(name){return name.replace(/\./g,"/");},_23f=/\/\/>>built/,_240=[],_241=[],_242=function(mid,_243,_244){_240.push(_244);_23c.forEach(mid.split(","),function(mid){var _245=_246(mid,_243.module);_241.push(_245);_247(_245);});_248();},_249,_24a=function(m){if(_249[m.mid]||/loadInit\!/.test(m.mid)){return true;}_249[m.mid]=1;if(m.injected!==_24b&&!m.executed){return false;}for(var deps=m.deps||[],i=0;i=0;--j){_2ad=lin[j].prototype;if(!_2ad.hasOwnProperty("declaredClass")){_2ad.declaredClass="uniqName_"+(_2a4++);}name=_2ad.declaredClass;if(!_2ab.hasOwnProperty(name)){_2ab[name]={count:0,refs:[],cls:lin[j]};++_2ac;}rec=_2ab[name];if(top&&top!==rec){rec.refs.push(top);++top.count;}top=rec;}++top.count;_2aa[0].refs.push(top);}while(_2aa.length){top=_2aa.pop();_2a9.push(top.cls);--_2ac;while(refs=top.refs,refs.length==1){top=refs[0];if(!top||--top.count){top=0;break;}_2a9.push(top.cls);--_2ac;}if(top){for(i=0,l=refs.length;i=0;--i){f=_2c5[i];m=f._meta;f=m?m.ctor:f;if(f){f.apply(this,_2c7?_2c7[i]:a);}}f=this.postscript;if(f){f.apply(this,args);}};};function _2c9(ctor,_2ca){return function(){var a=arguments,t=a,a0=a[0],f;if(!(this instanceof a.callee)){return _2c8(a);}if(_2ca){if(a0){f=a0.preamble;if(f){t=f.apply(this,t)||t;}}f=this.preamble;if(f){f.apply(this,t);}}if(ctor){ctor.apply(this,a);}f=this.postscript;if(f){f.apply(this,a);}};};function _2cb(_2cc){return function(){var a=arguments,i=0,f,m;if(!(this instanceof a.callee)){return _2c8(a);}for(;f=_2cc[i];++i){m=f._meta;f=m?m.ctor:f;if(f){f.apply(this,a);break;}}f=this.postscript;if(f){f.apply(this,a);}};};function _2cd(name,_2ce,_2cf){return function(){var b,m,f,i=0,step=1;if(_2cf){i=_2ce.length-1;step=-1;}for(;b=_2ce[i];i+=step){m=b._meta;f=(m?m.hidden:b.prototype)[name];if(f){f.apply(this,arguments);}}};};function _2d0(ctor){xtor.prototype=ctor.prototype;var t=new xtor;xtor.prototype=null;return t;};function _2c8(args){var ctor=args.callee,t=_2d0(ctor);ctor.apply(t,args);return t;};function _2c3(_2d1,_2d2,_2d3){if(typeof _2d1!="string"){_2d3=_2d2;_2d2=_2d1;_2d1="";}_2d3=_2d3||{};var _2d4,i,t,ctor,name,_2d5,_2d6,_2d7=1,_2d8=_2d2;if(opts.call(_2d2)=="[object Array]"){_2d5=_2a6(_2d2,_2d1);t=_2d5[0];_2d7=_2d5.length-t;_2d2=_2d5[_2d7];}else{_2d5=[0];if(_2d2){if(opts.call(_2d2)=="[object Function]"){t=_2d2._meta;_2d5=_2d5.concat(t?t.bases:_2d2);}else{err("base class is not a callable constructor.",_2d1);}}else{if(_2d2!==null){err("unknown base class. Did you use dojo.require to pull it in?",_2d1);}}}if(_2d2){for(i=_2d7-1;;--i){_2d4=_2d0(_2d2);if(!i){break;}t=_2d5[i];(t._meta?_2b9:mix)(_2d4,t.prototype);ctor=new Function;ctor.superclass=_2d2;ctor.prototype=_2d4;_2d2=_2d4.constructor=ctor;}}else{_2d4={};}_2c3.safeMixin(_2d4,_2d3);t=_2d3.constructor;if(t!==op.constructor){t.nom=_2a5;_2d4.constructor=t;}for(i=_2d7-1;i;--i){t=_2d5[i]._meta;if(t&&t.chains){_2d6=mix(_2d6||{},t.chains);}}if(_2d4["-chains-"]){_2d6=mix(_2d6||{},_2d4["-chains-"]);}t=!_2d6||!_2d6.hasOwnProperty(_2a5);_2d5[0]=ctor=(_2d6&&_2d6.constructor==="manual")?_2cb(_2d5):(_2d5.length==1?_2c9(_2d3.constructor,t):_2c4(_2d5,t));ctor._meta={bases:_2d5,hidden:_2d3,chains:_2d6,parents:_2d8,ctor:_2d3.constructor};ctor.superclass=_2d2&&_2d2.prototype;ctor.extend=_2c1;ctor.prototype=_2d4;_2d4.constructor=ctor;_2d4.getInherited=_2b4;_2d4.isInstanceOf=_2b7;_2d4.inherited=_2b6;_2d4.__inherited=_2ae;if(_2d1){_2d4.declaredClass=_2d1;lang.setObject(_2d1,ctor);}if(_2d6){for(name in _2d6){if(_2d4[name]&&typeof _2d6[name]=="string"&&name!=_2a5){t=_2d4[name]=_2cd(name,_2d5,_2d6[name]==="after");t.nom=name;}}}return ctor;};dojo.safeMixin=_2c3.safeMixin=_2bd;dojo.declare=_2c3;return _2c3;});},"dojo/dom":function(){define(["./_base/sniff","./_base/lang","./_base/window"],function(has,lang,win){try{document.execCommand("BackgroundImageCache",false,true);}catch(e){}var dom={};if(has("ie")){dom.byId=function(id,doc){if(typeof id!="string"){return id;}var _2d9=doc||win.doc,te=id&&_2d9.getElementById(id);if(te&&(te.attributes.id.value==id||te.id==id)){return te;}else{var eles=_2d9.all[id];if(!eles||eles.nodeName){eles=[eles];}var i=0;while((te=eles[i++])){if((te.attributes&&te.attributes.id&&te.attributes.id.value==id)||te.id==id){return te;}}}};}else{dom.byId=function(id,doc){return ((typeof id=="string")?(doc||win.doc).getElementById(id):id)||null;};}dom.isDescendant=function(node,_2da){try{node=dom.byId(node);_2da=dom.byId(_2da);while(node){if(node==_2da){return true;}node=node.parentNode;}}catch(e){}return false;};dom.setSelectable=function(node,_2db){node=dom.byId(node);if(has("mozilla")){node.style.MozUserSelect=_2db?"":"none";}else{if(has("khtml")||has("webkit")){node.style.KhtmlUserSelect=_2db?"auto":"none";}else{if(has("ie")){var v=(node.unselectable=_2db?"":"on"),cs=node.getElementsByTagName("*"),i=0,l=cs.length;for(;i=0){_2e2+=" * ";}else{_2e2+=" ";}var ts=function(s,e){return trim(_2e2.slice(s,e));};var _2e3=[];var _2e4=-1,_2e5=-1,_2e6=-1,_2e7=-1,_2e8=-1,inId=-1,_2e9=-1,lc="",cc="",_2ea;var x=0,ql=_2e2.length,_2eb=null,_2ec=null;var _2ed=function(){if(_2e9>=0){var tv=(_2e9==x)?null:ts(_2e9,x);_2eb[(_2de.indexOf(tv)<0)?"tag":"oper"]=tv;_2e9=-1;}};var _2ee=function(){if(inId>=0){_2eb.id=ts(inId,x).replace(/\\/g,"");inId=-1;}};var _2ef=function(){if(_2e8>=0){_2eb.classes.push(ts(_2e8+1,x).replace(/\\/g,""));_2e8=-1;}};var _2f0=function(){_2ee();_2ed();_2ef();};var _2f1=function(){_2f0();if(_2e7>=0){_2eb.pseudos.push({name:ts(_2e7+1,x)});}_2eb.loops=(_2eb.pseudos.length||_2eb.attrs.length||_2eb.classes.length);_2eb.oquery=_2eb.query=ts(_2ea,x);_2eb.otag=_2eb.tag=(_2eb["oper"])?null:(_2eb.tag||"*");if(_2eb.tag){_2eb.tag=_2eb.tag.toUpperCase();}if(_2e3.length&&(_2e3[_2e3.length-1].oper)){_2eb.infixOper=_2e3.pop();_2eb.query=_2eb.infixOper.query+" "+_2eb.query;}_2e3.push(_2eb);_2eb=null;};for(;lc=cc,cc=_2e2.charAt(x),x=0){if(cc=="]"){if(!_2ec.attr){_2ec.attr=ts(_2e4+1,x);}else{_2ec.matchFor=ts((_2e6||_2e4+1),x);}var cmf=_2ec.matchFor;if(cmf){if((cmf.charAt(0)=="\"")||(cmf.charAt(0)=="'")){_2ec.matchFor=cmf.slice(1,-1);}}_2eb.attrs.push(_2ec);_2ec=null;_2e4=_2e6=-1;}else{if(cc=="="){var _2f2=("|~^$*".indexOf(lc)>=0)?lc:"";_2ec.type=_2f2+cc;_2ec.attr=ts(_2e4+1,x-_2f2.length);_2e6=x+1;}}}else{if(_2e5>=0){if(cc==")"){if(_2e7>=0){_2ec.value=ts(_2e5+1,x);}_2e7=_2e5=-1;}}else{if(cc=="#"){_2f0();inId=x+1;}else{if(cc=="."){_2f0();_2e8=x;}else{if(cc==":"){_2f0();_2e7=x;}else{if(cc=="["){_2f0();_2e4=x;_2ec={};}else{if(cc=="("){if(_2e7>=0){_2ec={name:ts(_2e7+1,x),value:null};_2eb.pseudos.push(_2ec);}_2e5=x;}else{if((cc==" ")&&(lc!=cc)){_2f1();}}}}}}}}}return _2e3;};var _2f3=function(_2f4,_2f5){if(!_2f4){return _2f5;}if(!_2f5){return _2f4;}return function(){return _2f4.apply(window,arguments)&&_2f5.apply(window,arguments);};};var _2f6=function(i,arr){var r=arr||[];if(i){r.push(i);}return r;};var _2f7=function(n){return (1==n.nodeType);};var _2f8="";var _2f9=function(elem,attr){if(!elem){return _2f8;}if(attr=="class"){return elem.className||_2f8;}if(attr=="for"){return elem.htmlFor||_2f8;}if(attr=="style"){return elem.style.cssText||_2f8;}return (_2df?elem.getAttribute(attr):elem.getAttribute(attr,2))||_2f8;};var _2fa={"*=":function(attr,_2fb){return function(elem){return (_2f9(elem,attr).indexOf(_2fb)>=0);};},"^=":function(attr,_2fc){return function(elem){return (_2f9(elem,attr).indexOf(_2fc)==0);};},"$=":function(attr,_2fd){return function(elem){var ea=" "+_2f9(elem,attr);return (ea.lastIndexOf(_2fd)==(ea.length-_2fd.length));};},"~=":function(attr,_2fe){var tval=" "+_2fe+" ";return function(elem){var ea=" "+_2f9(elem,attr)+" ";return (ea.indexOf(tval)>=0);};},"|=":function(attr,_2ff){var _300=_2ff+"-";return function(elem){var ea=_2f9(elem,attr);return ((ea==_2ff)||(ea.indexOf(_300)==0));};},"=":function(attr,_301){return function(elem){return (_2f9(elem,attr)==_301);};}};var _302=(typeof _2dc().firstChild.nextElementSibling=="undefined");var _303=!_302?"nextElementSibling":"nextSibling";var _304=!_302?"previousElementSibling":"previousSibling";var _305=(_302?_2f7:_2e0);var _306=function(node){while(node=node[_304]){if(_305(node)){return false;}}return true;};var _307=function(node){while(node=node[_303]){if(_305(node)){return false;}}return true;};var _308=function(node){var root=node.parentNode;var i=0,tret=root.children||root.childNodes,ci=(node["_i"]||-1),cl=(root["_l"]||-1);if(!tret){return -1;}var l=tret.length;if(cl==l&&ci>=0&&cl>=0){return ci;}root["_l"]=l;ci=-1;for(var te=root["firstElementChild"]||root["firstChild"];te;te=te[_303]){if(_305(te)){te["_i"]=++i;if(node===te){ci=i;}}}return ci;};var _309=function(elem){return !((_308(elem))%2);};var _30a=function(elem){return ((_308(elem))%2);};var _30b={"checked":function(name,_30c){return function(elem){return !!("checked" in elem?elem.checked:elem.selected);};},"first-child":function(){return _306;},"last-child":function(){return _307;},"only-child":function(name,_30d){return function(node){return _306(node)&&_307(node);};},"empty":function(name,_30e){return function(elem){var cn=elem.childNodes;var cnl=elem.childNodes.length;for(var x=cnl-1;x>=0;x--){var nt=cn[x].nodeType;if((nt===1)||(nt==3)){return false;}}return true;};},"contains":function(name,_30f){var cz=_30f.charAt(0);if(cz=="\""||cz=="'"){_30f=_30f.slice(1,-1);}return function(elem){return (elem.innerHTML.indexOf(_30f)>=0);};},"not":function(name,_310){var p=_2e1(_310)[0];var _311={el:1};if(p.tag!="*"){_311.tag=1;}if(!p.classes.length){_311.classes=1;}var ntf=_312(p,_311);return function(elem){return (!ntf(elem));};},"nth-child":function(name,_313){var pi=parseInt;if(_313=="odd"){return _30a;}else{if(_313=="even"){return _309;}}if(_313.indexOf("n")!=-1){var _314=_313.split("n",2);var pred=_314[0]?((_314[0]=="-")?-1:pi(_314[0])):1;var idx=_314[1]?pi(_314[1]):0;var lb=0,ub=-1;if(pred>0){if(idx<0){idx=(idx%pred)&&(pred+(idx%pred));}else{if(idx>0){if(idx>=pred){lb=idx-idx%pred;}idx=idx%pred;}}}else{if(pred<0){pred*=-1;if(idx>0){ub=idx;idx=idx%pred;}}}if(pred>0){return function(elem){var i=_308(elem);return (i>=lb)&&(ub<0||i<=ub)&&((i%pred)==idx);};}else{_313=idx;}}var _315=pi(_313);return function(elem){return (_308(elem)==_315);};}};var _316=(dojo.isIE&&(dojo.isIE<9||dojo.isQuirks))?function(cond){var clc=cond.toLowerCase();if(clc=="class"){cond="className";}return function(elem){return (_2df?elem.getAttribute(cond):elem[cond]||elem[clc]);};}:function(cond){return function(elem){return (elem&&elem.getAttribute&&elem.hasAttribute(cond));};};var _312=function(_317,_318){if(!_317){return _2e0;}_318=_318||{};var ff=null;if(!("el" in _318)){ff=_2f3(ff,_2f7);}if(!("tag" in _318)){if(_317.tag!="*"){ff=_2f3(ff,function(elem){return (elem&&(elem.tagName==_317.getTag()));});}}if(!("classes" in _318)){each(_317.classes,function(_319,idx,arr){var re=new RegExp("(?:^|\\s)"+_319+"(?:\\s|$)");ff=_2f3(ff,function(elem){return re.test(elem.className);});ff.count=idx;});}if(!("pseudos" in _318)){each(_317.pseudos,function(_31a){var pn=_31a.name;if(_30b[pn]){ff=_2f3(ff,_30b[pn](pn,_31a.value));}});}if(!("attrs" in _318)){each(_317.attrs,function(attr){var _31b;var a=attr.attr;if(attr.type&&_2fa[attr.type]){_31b=_2fa[attr.type](a,attr.matchFor);}else{if(a.length){_31b=_316(a);}}if(_31b){ff=_2f3(ff,_31b);}});}if(!("id" in _318)){if(_317.id){ff=_2f3(ff,function(elem){return (!!elem&&(elem.id==_317.id));});}}if(!ff){if(!("default" in _318)){ff=_2e0;}}return ff;};var _31c=function(_31d){return function(node,ret,bag){while(node=node[_303]){if(_302&&(!_2f7(node))){continue;}if((!bag||_31e(node,bag))&&_31d(node)){ret.push(node);}break;}return ret;};};var _31f=function(_320){return function(root,ret,bag){var te=root[_303];while(te){if(_305(te)){if(bag&&!_31e(te,bag)){break;}if(_320(te)){ret.push(te);}}te=te[_303];}return ret;};};var _321=function(_322){_322=_322||_2e0;return function(root,ret,bag){var te,x=0,tret=root.children||root.childNodes;while(te=tret[x++]){if(_305(te)&&(!bag||_31e(te,bag))&&(_322(te,x))){ret.push(te);}}return ret;};};var _323=function(node,root){var pn=node.parentNode;while(pn){if(pn==root){break;}pn=pn.parentNode;}return !!pn;};var _324={};var _325=function(_326){var _327=_324[_326.query];if(_327){return _327;}var io=_326.infixOper;var oper=(io?io.oper:"");var _328=_312(_326,{el:1});var qt=_326.tag;var _329=("*"==qt);var ecs=_2dc()["getElementsByClassName"];if(!oper){if(_326.id){_328=(!_326.loops&&_329)?_2e0:_312(_326,{el:1,id:1});_327=function(root,arr){var te=dom.byId(_326.id,(root.ownerDocument||root));if(!te||!_328(te)){return;}if(9==root.nodeType){return _2f6(te,arr);}else{if(_323(te,root)){return _2f6(te,arr);}}};}else{if(ecs&&/\{\s*\[native code\]\s*\}/.test(String(ecs))&&_326.classes.length&&!_2dd){_328=_312(_326,{el:1,classes:1,id:1});var _32a=_326.classes.join(" ");_327=function(root,arr,bag){var ret=_2f6(0,arr),te,x=0;var tret=root.getElementsByClassName(_32a);while((te=tret[x++])){if(_328(te,root)&&_31e(te,bag)){ret.push(te);}}return ret;};}else{if(!_329&&!_326.loops){_327=function(root,arr,bag){var ret=_2f6(0,arr),te,x=0;var tret=root.getElementsByTagName(_326.getTag());while((te=tret[x++])){if(_31e(te,bag)){ret.push(te);}}return ret;};}else{_328=_312(_326,{el:1,tag:1,id:1});_327=function(root,arr,bag){var ret=_2f6(0,arr),te,x=0;var tret=root.getElementsByTagName(_326.getTag());while((te=tret[x++])){if(_328(te,root)&&_31e(te,bag)){ret.push(te);}}return ret;};}}}}else{var _32b={el:1};if(_329){_32b.tag=1;}_328=_312(_326,_32b);if("+"==oper){_327=_31c(_328);}else{if("~"==oper){_327=_31f(_328);}else{if(">"==oper){_327=_321(_328);}}}}return _324[_326.query]=_327;};var _32c=function(root,_32d){var _32e=_2f6(root),qp,x,te,qpl=_32d.length,bag,ret;for(var i=0;i0){bag={};ret.nozip=true;}var gef=_325(qp);for(var j=0;(te=_32e[j]);j++){gef(te,ret,bag);}if(!ret.length){break;}_32e=ret;}return ret;};var _32f={},_330={};var _331=function(_332){var _333=_2e1(trim(_332));if(_333.length==1){var tef=_325(_333[0]);return function(root){var r=tef(root,[]);if(r){r.nozip=true;}return r;};}return function(root){return _32c(root,_333);};};var nua=navigator.userAgent;var wk="WebKit/";var _334=(dojo.isWebKit&&(nua.indexOf(wk)>0)&&(parseFloat(nua.split(wk)[1])>528));var _335=dojo.isIE?"commentStrip":"nozip";var qsa="querySelectorAll";var _336=(!!_2dc()[qsa]&&(!dojo.isSafari||(dojo.isSafari>3.1)||_334));var _337=/n\+\d|([^ ])?([>~+])([^ =])?/g;var _338=function(_339,pre,ch,post){return ch?(pre?pre+" ":"")+ch+(post?" "+post:""):_339;};var _33a=function(_33b,_33c){_33b=_33b.replace(_337,_338);if(_336){var _33d=_330[_33b];if(_33d&&!_33c){return _33d;}}var _33e=_32f[_33b];if(_33e){return _33e;}var qcz=_33b.charAt(0);var _33f=(-1==_33b.indexOf(" "));if((_33b.indexOf("#")>=0)&&(_33f)){_33c=true;}var _340=(_336&&(!_33c)&&(_2de.indexOf(qcz)==-1)&&(!dojo.isIE||(_33b.indexOf(":")==-1))&&(!(_2dd&&(_33b.indexOf(".")>=0)))&&(_33b.indexOf(":contains")==-1)&&(_33b.indexOf(":checked")==-1)&&(_33b.indexOf("|=")==-1));if(_340){var tq=(_2de.indexOf(_33b.charAt(_33b.length-1))>=0)?(_33b+" *"):_33b;return _330[_33b]=function(root){try{if(!((9==root.nodeType)||_33f)){throw "";}var r=root[qsa](tq);r[_335]=true;return r;}catch(e){return _33a(_33b,true)(root);}};}else{var _341=_33b.split(/\s*,\s*/);return _32f[_33b]=((_341.length<2)?_331(_33b):function(root){var _342=0,ret=[],tp;while((tp=_341[_342++])){ret=ret.concat(_331(tp)(root));}return ret;});}};var _343=0;var _344=dojo.isIE?function(node){if(_2df){return (node.getAttribute("_uid")||node.setAttribute("_uid",++_343)||_343);}else{return node.uniqueID;}}:function(node){return (node._uid||(node._uid=++_343));};var _31e=function(node,bag){if(!bag){return 1;}var id=_344(node);if(!bag[id]){return bag[id]=1;}return 0;};var _345="_zipIdx";var _346=function(arr){if(arr&&arr.nozip){return arr;}var ret=[];if(!arr||!arr.length){return ret;}if(arr[0]){ret.push(arr[0]);}if(arr.length<2){return ret;}_343++;if(dojo.isIE&&_2df){var _347=_343+"";arr[0].setAttribute(_345,_347);for(var x=1,te;te=arr[x];x++){if(arr[x].getAttribute(_345)!=_347){ret.push(te);}te.setAttribute(_345,_347);}}else{if(dojo.isIE&&arr.commentStrip){try{for(var x=1,te;te=arr[x];x++){if(_2f7(te)){ret.push(te);}}}catch(e){}}else{if(arr[0]){arr[0][_345]=_343;}for(var x=1,te;te=arr[x];x++){if(arr[x][_345]!=_343){ret.push(te);}te[_345]=_343;}}}return ret;};var _348=function(_349,root){root=root||_2dc();var od=root.ownerDocument||root.documentElement;_2df=(root.contentType&&root.contentType=="application/xml")||(dojo.isOpera&&(root.doctype||od.toString()=="[object XMLDocument]"))||(!!od)&&(dojo.isIE?od.xml:(root.xmlVersion||od.xmlVersion));var r=_33a(_349)(root);if(r&&r.nozip){return r;}return _346(r);};_348.filter=function(_34a,_34b,root){var _34c=[],_34d=_2e1(_34b),_34e=(_34d.length==1&&!/[^\w#\.]/.test(_34b))?_312(_34d[0]):function(node){return dojo.query(_34b,root).indexOf(node)!=-1;};for(var x=0,te;te=_34a[x];x++){if(_34e(te)){_34c.push(te);}}return _34c;};return _348;});},"dojo/dom-style":function(){define(["./_base/sniff","./dom"],function(has,dom){var _34f,_350={};if(has("webkit")){_34f=function(node){var s;if(node.nodeType==1){var dv=node.ownerDocument.defaultView;s=dv.getComputedStyle(node,null);if(!s&&node.style){node.style.display="";s=dv.getComputedStyle(node,null);}}return s||{};};}else{if(has("ie")&&(has("ie")<9||has("quirks"))){_34f=function(node){return node.nodeType==1?node.currentStyle:{};};}else{_34f=function(node){return node.nodeType==1?node.ownerDocument.defaultView.getComputedStyle(node,null):{};};}}_350.getComputedStyle=_34f;var _351;if(!has("ie")){_351=function(_352,_353){return parseFloat(_353)||0;};}else{_351=function(_354,_355){if(!_355){return 0;}if(_355=="medium"){return 4;}if(_355.slice&&_355.slice(-2)=="px"){return parseFloat(_355);}var s=_354.style,rs=_354.runtimeStyle,cs=_354.currentStyle,_356=s.left,_357=rs.left;rs.left=cs.left;try{s.left=_355;_355=s.pixelLeft;}catch(e){_355=0;}s.left=_356;rs.left=_357;return _355;};}_350.toPixelValue=_351;var astr="DXImageTransform.Microsoft.Alpha";var af=function(n,f){try{return n.filters.item(astr);}catch(e){return f?{}:null;}};var _358=has("ie")<9||(has("ie")&&has("quirks"))?function(node){try{return af(node).Opacity/100;}catch(e){return 1;}}:function(node){return _34f(node).opacity;};var _359=has("ie")<9||(has("ie")&&has("quirks"))?function(node,_35a){var ov=_35a*100,_35b=_35a==1;node.style.zoom=_35b?"":1;if(!af(node)){if(_35b){return _35a;}node.style.filter+=" progid:"+astr+"(Opacity="+ov+")";}else{af(node,1).Opacity=ov;}af(node,1).Enabled=!_35b;if(node.tagName.toLowerCase()=="tr"){for(var td=node.firstChild;td;td=td.nextSibling){if(td.tagName.toLowerCase()=="td"){_359(td,_35a);}}}return _35a;}:function(node,_35c){return node.style.opacity=_35c;};var _35d={left:true,top:true};var _35e=/margin|padding|width|height|max|min|offset/;function _35f(node,type,_360){type=type.toLowerCase();if(has("ie")){if(_360=="auto"){if(type=="height"){return node.offsetHeight;}if(type=="width"){return node.offsetWidth;}}if(type=="fontweight"){switch(_360){case 700:return "bold";case 400:default:return "normal";}}}if(!(type in _35d)){_35d[type]=_35e.test(type);}return _35d[type]?_351(node,_360):_360;};var _361=has("ie")?"styleFloat":"cssFloat",_362={"cssFloat":_361,"styleFloat":_361,"float":_361};_350.get=function getStyle(node,name){var n=dom.byId(node),l=arguments.length,op=(name=="opacity");if(l==2&&op){return _358(n);}name=_362[name]||name;var s=_350.getComputedStyle(n);return (l==1)?s:_35f(n,name,s[name]||n.style[name]);};_350.set=function setStyle(node,name,_363){var n=dom.byId(node),l=arguments.length,op=(name=="opacity");name=_362[name]||name;if(l==3){return op?_359(n,_363):n.style[name]=_363;}for(var x in name){_350.set(node,x,name[x]);}return _350.getComputedStyle(n);};return _350;});},"dojo/dom-geometry":function(){define(["./_base/sniff","./_base/window","./dom","./dom-style"],function(has,win,dom,_364){var geom={};geom.boxModel="content-box";if(has("ie")){geom.boxModel=document.compatMode=="BackCompat"?"border-box":"content-box";}geom.getPadExtents=function getPadExtents(node,_365){node=dom.byId(node);var s=_365||_364.getComputedStyle(node),px=_364.toPixelValue,l=px(node,s.paddingLeft),t=px(node,s.paddingTop),r=px(node,s.paddingRight),b=px(node,s.paddingBottom);return {l:l,t:t,r:r,b:b,w:l+r,h:t+b};};var none="none";geom.getBorderExtents=function getBorderExtents(node,_366){node=dom.byId(node);var px=_364.toPixelValue,s=_366||_364.getComputedStyle(node),l=s.borderLeftStyle!=none?px(node,s.borderLeftWidth):0,t=s.borderTopStyle!=none?px(node,s.borderTopWidth):0,r=s.borderRightStyle!=none?px(node,s.borderRightWidth):0,b=s.borderBottomStyle!=none?px(node,s.borderBottomWidth):0;return {l:l,t:t,r:r,b:b,w:l+r,h:t+b};};geom.getPadBorderExtents=function getPadBorderExtents(node,_367){node=dom.byId(node);var s=_367||_364.getComputedStyle(node),p=geom.getPadExtents(node,s),b=geom.getBorderExtents(node,s);return {l:p.l+b.l,t:p.t+b.t,r:p.r+b.r,b:p.b+b.b,w:p.w+b.w,h:p.h+b.h};};geom.getMarginExtents=function getMarginExtents(node,_368){node=dom.byId(node);var s=_368||_364.getComputedStyle(node),px=_364.toPixelValue,l=px(node,s.marginLeft),t=px(node,s.marginTop),r=px(node,s.marginRight),b=px(node,s.marginBottom);if(has("webkit")&&(s.position!="absolute")){r=l;}return {l:l,t:t,r:r,b:b,w:l+r,h:t+b};};geom.getMarginBox=function getMarginBox(node,_369){node=dom.byId(node);var s=_369||_364.getComputedStyle(node),me=geom.getMarginExtents(node,s),l=node.offsetLeft-me.l,t=node.offsetTop-me.t,p=node.parentNode,px=_364.toPixelValue,pcs;if(has("mozilla")){var sl=parseFloat(s.left),st=parseFloat(s.top);if(!isNaN(sl)&&!isNaN(st)){l=sl,t=st;}else{if(p&&p.style){pcs=_364.getComputedStyle(p);if(pcs.overflow!="visible"){l+=pcs.borderLeftStyle!=none?px(node,pcs.borderLeftWidth):0;t+=pcs.borderTopStyle!=none?px(node,pcs.borderTopWidth):0;}}}}else{if(has("opera")||(has("ie")==8&&!has("quirks"))){if(p){pcs=_364.getComputedStyle(p);l-=pcs.borderLeftStyle!=none?px(node,pcs.borderLeftWidth):0;t-=pcs.borderTopStyle!=none?px(node,pcs.borderTopWidth):0;}}}return {l:l,t:t,w:node.offsetWidth+me.w,h:node.offsetHeight+me.h};};geom.getContentBox=function getContentBox(node,_36a){node=dom.byId(node);var s=_36a||_364.getComputedStyle(node),w=node.clientWidth,h,pe=geom.getPadExtents(node,s),be=geom.getBorderExtents(node,s);if(!w){w=node.offsetWidth;h=node.offsetHeight;}else{h=node.clientHeight;be.w=be.h=0;}if(has("opera")){pe.l+=be.l;pe.t+=be.t;}return {l:pe.l,t:pe.t,w:w-pe.w-be.w,h:h-pe.h-be.h};};function _36b(node,l,t,w,h,u){u=u||"px";var s=node.style;if(!isNaN(l)){s.left=l+u;}if(!isNaN(t)){s.top=t+u;}if(w>=0){s.width=w+u;}if(h>=0){s.height=h+u;}};function _36c(node){return node.tagName.toLowerCase()=="button"||node.tagName.toLowerCase()=="input"&&(node.getAttribute("type")||"").toLowerCase()=="button";};function _36d(node){return geom.boxModel=="border-box"||node.tagName.toLowerCase()=="table"||_36c(node);};geom.setContentSize=function setContentSize(node,box,_36e){node=dom.byId(node);var w=box.w,h=box.h;if(_36d(node)){var pb=geom.getPadBorderExtents(node,_36e);if(w>=0){w+=pb.w;}if(h>=0){h+=pb.h;}}_36b(node,NaN,NaN,w,h);};var _36f={l:0,t:0,w:0,h:0};geom.setMarginBox=function setMarginBox(node,box,_370){node=dom.byId(node);var s=_370||_364.getComputedStyle(node),w=box.w,h=box.h,pb=_36d(node)?_36f:geom.getPadBorderExtents(node,s),mb=geom.getMarginExtents(node,s);if(has("webkit")){if(_36c(node)){var ns=node.style;if(w>=0&&!ns.width){ns.width="4px";}if(h>=0&&!ns.height){ns.height="4px";}}}if(w>=0){w=Math.max(w-pb.w-mb.w,0);}if(h>=0){h=Math.max(h-pb.h-mb.h,0);}_36b(node,box.l,box.t,w,h);};geom.isBodyLtr=function isBodyLtr(){return (win.body().dir||win.doc.documentElement.dir||"ltr").toLowerCase()=="ltr";};geom.docScroll=function docScroll(){var node=win.doc.parentWindow||win.doc.defaultView;return "pageXOffset" in node?{x:node.pageXOffset,y:node.pageYOffset}:(node=has("quirks")?win.body():win.doc.documentElement,{x:geom.fixIeBiDiScrollLeft(node.scrollLeft||0),y:node.scrollTop||0});};geom.getIeDocumentElementOffset=function getIeDocumentElementOffset(){var de=win.doc.documentElement;if(has("ie")<8){var r=de.getBoundingClientRect(),l=r.left,t=r.top;if(has("ie")<7){l+=de.clientLeft;t+=de.clientTop;}return {x:l<0?0:l,y:t<0?0:t};}else{return {x:0,y:0};}};geom.fixIeBiDiScrollLeft=function fixIeBiDiScrollLeft(_371){var ie=has("ie");if(ie&&!geom.isBodyLtr()){var qk=has("quirks"),de=qk?win.body():win.doc.documentElement;if(ie==6&&!qk&&win.global.frameElement&&de.scrollHeight>de.clientHeight){_371+=de.clientLeft;}return (ie<8||qk)?(_371+de.clientWidth-de.scrollWidth):-_371;}return _371;};geom.position=function(node,_372){node=dom.byId(node);var db=win.body(),dh=db.parentNode,ret=node.getBoundingClientRect();ret={x:ret.left,y:ret.top,w:ret.right-ret.left,h:ret.bottom-ret.top};if(has("ie")){var _373=geom.getIeDocumentElementOffset();ret.x-=_373.x+(has("quirks")?db.clientLeft+db.offsetLeft:0);ret.y-=_373.y+(has("quirks")?db.clientTop+db.offsetTop:0);}else{if(has("ff")==3){var cs=_364.getComputedStyle(dh),px=_364.toPixelValue;ret.x-=px(dh,cs.marginLeft)+px(dh,cs.borderLeftWidth);ret.y-=px(dh,cs.marginTop)+px(dh,cs.borderTopWidth);}}if(_372){var _374=geom.docScroll();ret.x+=_374.x;ret.y+=_374.y;}return ret;};geom.getMarginSize=function getMarginSize(node,_375){node=dom.byId(node);var me=geom.getMarginExtents(node,_375||_364.getComputedStyle(node));var size=node.getBoundingClientRect();return {w:(size.right-size.left)+me.w,h:(size.bottom-size.top)+me.h};};geom.normalizeEvent=function(_376){if(!("layerX" in _376)){_376.layerX=_376.offsetX;_376.layerY=_376.offsetY;}if(!has("dom-addeventlistener")){var se=_376.target;var doc=(se&&se.ownerDocument)||document;var _377=has("quirks")?doc.body:doc.documentElement;var _378=geom.getIeDocumentElementOffset();_376.pageX=_376.clientX+geom.fixIeBiDiScrollLeft(_377.scrollLeft||0)-_378.x;_376.pageY=_376.clientY+(_377.scrollTop||0)-_378.y;}};return geom;});},"dojo/dom-prop":function(){define("dojo/dom-prop",["exports","./_base/kernel","./_base/sniff","./_base/lang","./dom","./dom-style","./dom-construct","./_base/connect"],function(_379,dojo,has,lang,dom,_37a,ctr,conn){var _37b={},_37c=0,_37d=dojo._scopeName+"attrid";var _37e={col:1,colgroup:1,table:1,tbody:1,tfoot:1,thead:1,tr:1,title:1};_379.names={"class":"className","for":"htmlFor",tabindex:"tabIndex",readonly:"readOnly",colspan:"colSpan",frameborder:"frameBorder",rowspan:"rowSpan",valuetype:"valueType"};_379.get=function getProp(node,name){node=dom.byId(node);var lc=name.toLowerCase(),_37f=_379.names[lc]||name;return node[_37f];};_379.set=function setProp(node,name,_380){node=dom.byId(node);var l=arguments.length;if(l==2&&typeof name!="string"){for(var x in name){_379.set(node,x,name[x]);}return node;}var lc=name.toLowerCase(),_381=_379.names[lc]||name;if(_381=="style"&&typeof _380!="string"){_37a.style(node,_380);return node;}if(_381=="innerHTML"){if(has("ie")&&node.tagName.toLowerCase() in _37e){ctr.empty(node);node.appendChild(ctr.toDom(_380,node.ownerDocument));}else{node[_381]=_380;}return node;}if(lang.isFunction(_380)){var _382=node[_37d];if(!_382){_382=_37c++;node[_37d]=_382;}if(!_37b[_382]){_37b[_382]={};}var h=_37b[_382][_381];if(h){conn.disconnect(h);}else{try{delete node[_381];}catch(e){}}if(_380){_37b[_382][_381]=conn.connect(node,_381,_380);}else{node[_381]=null;}return node;}node[_381]=_380;return node;};});},"dojo/dom-attr":function(){define(["exports","./_base/sniff","./_base/lang","./dom","./dom-style","./dom-prop"],function(_383,has,lang,dom,_384,prop){var _385={innerHTML:1,className:1,htmlFor:has("ie"),value:1},_386={classname:"class",htmlfor:"for",tabindex:"tabIndex",readonly:"readOnly"};function _387(node,name){var attr=node.getAttributeNode&&node.getAttributeNode(name);return attr&&attr.specified;};_383.has=function hasAttr(node,name){var lc=name.toLowerCase();return _385[prop.names[lc]||name]||_387(dom.byId(node),_386[lc]||name);};_383.get=function getAttr(node,name){node=dom.byId(node);var lc=name.toLowerCase(),_388=prop.names[lc]||name,_389=_385[_388];value=node[_388];if(_389&&typeof value!="undefined"){return value;}if(_388!="href"&&(typeof value=="boolean"||lang.isFunction(value))){return value;}var _38a=_386[lc]||name;return _387(node,_38a)?node.getAttribute(_38a):null;};_383.set=function setAttr(node,name,_38b){node=dom.byId(node);if(arguments.length==2){for(var x in name){_383.set(node,x,name[x]);}return node;}var lc=name.toLowerCase(),_38c=prop.names[lc]||name,_38d=_385[_38c];if(_38c=="style"&&typeof _38b!="string"){_384.set(node,_38b);return node;}if(_38d||typeof _38b=="boolean"||lang.isFunction(_38b)){return prop.set(node,name,_38b);}node.setAttribute(_386[lc]||name,_38b);return node;};_383.remove=function removeAttr(node,name){dom.byId(node).removeAttribute(_386[name.toLowerCase()]||name);};_383.getNodeProp=function getNodeProp(node,name){node=dom.byId(node);var lc=name.toLowerCase(),_38e=prop.names[lc]||name;if((_38e in node)&&_38e!="href"){return node[_38e];}var _38f=_386[lc]||name;return _387(node,_38f)?node.getAttribute(_38f):null;};});},"dojo/dom-construct":function(){define("dojo/dom-construct",["exports","./_base/kernel","./_base/sniff","./_base/window","./dom","./dom-attr","./on"],function(_390,dojo,has,win,dom,attr,on){var _391={option:["select"],tbody:["table"],thead:["table"],tfoot:["table"],tr:["table","tbody"],td:["table","tbody","tr"],th:["table","thead","tr"],legend:["fieldset"],caption:["table"],colgroup:["table"],col:["table","colgroup"],li:["ul"]},_392=/<\s*([\w\:]+)/,_393={},_394=0,_395="__"+dojo._scopeName+"ToDomId";for(var _396 in _391){if(_391.hasOwnProperty(_396)){var tw=_391[_396];tw.pre=_396=="option"?" - - - - - - - - - - - diff --git a/mosgi.jmx.httpconnector/src/main/resources/mx4j/tools/adaptor/http/xsl/error.xsl b/mosgi.jmx.httpconnector/src/main/resources/mx4j/tools/adaptor/http/xsl/error.xsl deleted file mode 100644 index 3bbc4e8939a..00000000000 --- a/mosgi.jmx.httpconnector/src/main/resources/mx4j/tools/adaptor/http/xsl/error.xsl +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - stylesheet.css - text/css - error.title - - - - - - - - - - - - - - - - - - - -
      - - error.title - -
      - - error.httpexception.code - - -
      - - error.httpexception.message - - -
      - - - -
      -
      - diff --git a/mosgi.jmx.httpconnector/src/main/resources/mx4j/tools/adaptor/http/xsl/identity.xsl b/mosgi.jmx.httpconnector/src/main/resources/mx4j/tools/adaptor/http/xsl/identity.xsl deleted file mode 100644 index 4977d0548fb..00000000000 --- a/mosgi.jmx.httpconnector/src/main/resources/mx4j/tools/adaptor/http/xsl/identity.xsl +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - diff --git a/mosgi.jmx.httpconnector/src/main/resources/mx4j/tools/adaptor/http/xsl/mlet.xsl b/mosgi.jmx.httpconnector/src/main/resources/mx4j/tools/adaptor/http/xsl/mlet.xsl deleted file mode 100644 index 5fd78ade08a..00000000000 --- a/mosgi.jmx.httpconnector/src/main/resources/mx4j/tools/adaptor/http/xsl/mlet.xsl +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - stylesheet.css - text/css - mlet.title - - - - - - -

      - - -
      - - - - - - - - - - darkline - clearline - - - - - - - - - mlet.mbean.unregister - - - - - - - - - - - - - - - mlet - - - - - -
      - - mlet.main.title - -
      - - -
      - - - -
      - - diff --git a/mosgi.jmx.httpconnector/src/main/resources/mx4j/tools/adaptor/http/xsl/timer_create.xsl b/mosgi.jmx.httpconnector/src/main/resources/mx4j/tools/adaptor/http/xsl/timer_create.xsl deleted file mode 100644 index b0fe328c910..00000000000 --- a/mosgi.jmx.httpconnector/src/main/resources/mx4j/tools/adaptor/http/xsl/timer_create.xsl +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - stylesheet.css - text/css - timer_create.title - - - - - - - - - - - - - - - - - - - - - timer_create.operation.mbeanview - - - - - -
      - - timer_create.operation.title - - -
      - - - timer_create.operation.success - - - - - timer_create.operation.error - - - -
      -
      -
      - - - - - - - - - timer - - - - - - -
      - diff --git a/mosgi.jmx.httpconnector/src/main/resources/mx4j/tools/adaptor/http/xsl/xalan-ext.xsl b/mosgi.jmx.httpconnector/src/main/resources/mx4j/tools/adaptor/http/xsl/xalan-ext.xsl deleted file mode 100644 index f1e54bad3ab..00000000000 --- a/mosgi.jmx.httpconnector/src/main/resources/mx4j/tools/adaptor/http/xsl/xalan-ext.xsl +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - No encode function available - - - - - diff --git a/mosgi.jmx.registry/pom.xml b/mosgi.jmx.registry/pom.xml deleted file mode 100644 index 0a136b18b8f..00000000000 --- a/mosgi.jmx.registry/pom.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - org.apache.felix - felix - 0.8.0-SNAPSHOT - - 4.0.0 - osgi-bundle - Apache Felix MOSGi JMX registry - org.apache.felix.mosgi.jmx.registry - - - ${pom.groupId} - org.osgi.core - ${pom.version} - provided - - - ${pom.groupId} - org.osgi.compendium - ${pom.version} - provided - - - ${pom.groupId} - org.apache.felix.framework - ${pom.version} - provided - - - - - - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} - true - - - MOSGi JMX rmiregistry - MOSGi JMX rmiregistry - auto-detect - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/ - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}.jar - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}-src.jar - ${pom.artifactId} - - ${pom.artifactId}.mx4j.tools.naming;specification-version="1.0.0" - - - * - - - - - - - diff --git a/mosgi.jmx.registry/src/main/java/org/apache/felix/mosgi/jmx/registry/mx4j/tools/naming/NamingService.java b/mosgi.jmx.registry/src/main/java/org/apache/felix/mosgi/jmx/registry/mx4j/tools/naming/NamingService.java deleted file mode 100644 index d3e985ac847..00000000000 --- a/mosgi.jmx.registry/src/main/java/org/apache/felix/mosgi/jmx/registry/mx4j/tools/naming/NamingService.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) MX4J. - * All rights reserved. - * - * This software is distributed under the terms of the MX4J License version 1.0. - * See the terms of the MX4J License in the documentation provided with this software. - */ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.jmx.registry.mx4j.tools.naming; - -import java.rmi.registry.LocateRegistry; -import java.rmi.registry.Registry; -import java.rmi.server.UnicastRemoteObject; - -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; -import org.osgi.framework.BundleException; -import org.osgi.framework.Constants; -import org.osgi.framework.ServiceReference; -import org.osgi.framework.ServiceRegistration; - -import javax.management.ObjectName; - -import org.osgi.service.log.LogService; - -import org.apache.felix.framework.cache.BundleCache; - -public class NamingService implements BundleActivator,NamingServiceIfc { - private String version=null; - private ObjectName namingServiceName=null; - - private ServiceRegistration sReg=null; - private Registry m_registry=null; - private BundleContext bc=null; - - public void start(BundleContext bc) throws Exception { - this.version=(String)bc.getBundle().getHeaders().get(Constants.BUNDLE_VERSION); - this.bc=bc; - String profile=bc.getProperty(BundleCache.CACHE_PROFILE_PROP); - if (profile==null){ - profile=System.getProperty(BundleCache.CACHE_PROFILE_PROP); - } - String rmiPortS=bc.getProperty("insa.jmxconsole.rmiport."+profile); - int rmiPort=1099; - if (rmiPortS!=null){ - rmiPort=Integer.parseInt(rmiPortS); - } - try { - this.log(LogService.LOG_INFO, "Running rmiregistry on "+rmiPort,null); - Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader()); //!! Absolutely nececary for RMIClassLoading to work - m_registry=LocateRegistry.createRegistry(rmiPort); - //java.rmi.server.RemoteServer.setLog(System.out); - } catch (Exception e) { - this.bc=null; - throw new BundleException("Impossible to start rmiregistry"); - } - sReg=bc.registerService(NamingServiceIfc.class.getName(), this, null); - this.log(LogService.LOG_INFO, "RMI Registry started "+version,null); - } - - public void stop(BundleContext bc) throws Exception { - this.log(LogService.LOG_INFO, "Stopping RMI Registry "+version,null); - UnicastRemoteObject.unexportObject(m_registry, true); - this. m_registry = null; - sReg.unregister(); - this.sReg=null; - this.log(LogService.LOG_INFO, "RMI Stopped "+version,null); - this.bc=null; - } - - private void log(int prio, String message, Throwable t){ - if (this.bc!=null){ - ServiceReference logSR=this.bc.getServiceReference(LogService.class.getName()); - if (logSR!=null){ - ((LogService)this.bc.getService(logSR)).log(prio, message, t); - }else{ - System.out.println("No Log Service"); - } - }else{ - System.out.println(NamingService.class.getName()+": No bundleContext"); - } - } -} diff --git a/mosgi.jmx.registry/src/main/java/org/apache/felix/mosgi/jmx/registry/mx4j/tools/naming/NamingServiceIfc.java b/mosgi.jmx.registry/src/main/java/org/apache/felix/mosgi/jmx/registry/mx4j/tools/naming/NamingServiceIfc.java deleted file mode 100644 index 0edac27d40f..00000000000 --- a/mosgi.jmx.registry/src/main/java/org/apache/felix/mosgi/jmx/registry/mx4j/tools/naming/NamingServiceIfc.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.jmx.registry.mx4j.tools.naming; - -public interface NamingServiceIfc{} diff --git a/mosgi.jmx.registry/src/main/java/org/apache/felix/mosgi/jmx/registry/mx4j/tools/naming/NamingServiceMBean.java b/mosgi.jmx.registry/src/main/java/org/apache/felix/mosgi/jmx/registry/mx4j/tools/naming/NamingServiceMBean.java deleted file mode 100644 index 1603caac3ff..00000000000 --- a/mosgi.jmx.registry/src/main/java/org/apache/felix/mosgi/jmx/registry/mx4j/tools/naming/NamingServiceMBean.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) MX4J. - * All rights reserved. - * - * This software is distributed under the terms of the MX4J License version 1.0. - * See the terms of the MX4J License in the documentation provided with this software.* - * - * - */ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package org.apache.felix.mosgi.jmx.registry.mx4j.tools.naming; - -/** - * Management interface for the NamingService MBean. - * @author Simone Bordet - * @version $Revision: 1.3 $ - */ -public interface NamingServiceMBean -{ - /** - * Sets the port on which rmiregistry listens for incoming connections. - * @see #getPort - */ - public void setPort(int port); - - /** - * Returns the port on which rmiregistry listens for incoming connections - * @see #setPort - */ - public int getPort(); - - /** - * Returns whether this MBean has been started and not yet stopped. - * @see #start - */ - public boolean isRunning(); - -} diff --git a/mosgi.jmx.remotelogger/pom.xml b/mosgi.jmx.remotelogger/pom.xml deleted file mode 100644 index e844f13837c..00000000000 --- a/mosgi.jmx.remotelogger/pom.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - org.apache.felix - felix - 0.8.0-SNAPSHOT - - 4.0.0 - osgi-bundle - Apache Felix MOSGi JMX remotelogger - org.apache.felix.mosgi.jmx.remotelogger - - - ${pom.groupId} - org.osgi.core - ${pom.version} - provided - - - ${pom.groupId} - org.osgi.compendium - ${pom.version} - provided - - - - - - - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} - true - - - MOSGi JMX remote logger - MOSGi JMX remote logger - auto-detect - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/ - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}.jar - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}-src.jar - ${pom.artifactId} - - org.osgi.service.log;specification-version="1.0.0", - org.osgi.framework;specification-version="1.0.0", - javax.management;specification-version="1.0.0" - - - - - - - diff --git a/mosgi.jmx.remotelogger/src/main/java/org/apache/felix/mosgi/jmx/remotelogger/Logger.java b/mosgi.jmx.remotelogger/src/main/java/org/apache/felix/mosgi/jmx/remotelogger/Logger.java deleted file mode 100644 index c5db1282741..00000000000 --- a/mosgi.jmx.remotelogger/src/main/java/org/apache/felix/mosgi/jmx/remotelogger/Logger.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.jmx.remotelogger; - -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; -import org.osgi.framework.ServiceReference; -import org.osgi.framework.BundleException; -import org.osgi.framework.ServiceListener; -import org.osgi.framework.ServiceEvent; -import org.osgi.framework.InvalidSyntaxException; -import org.osgi.framework.Constants; - -import org.osgi.service.log.LogListener; -import org.osgi.service.log.LogReaderService; -import org.osgi.service.log.LogService; -import org.osgi.service.log.LogEntry; - -import javax.management.MBeanServer; -import javax.management.ObjectName; -import javax.management.NotificationBroadcasterSupport; -import javax.management.AttributeChangeNotification; -import javax.management.MalformedObjectNameException; - -import java.io.Serializable; - -public class Logger extends NotificationBroadcasterSupport implements LogListener,BundleActivator,ServiceListener, LoggerMBean, Serializable{ - - private static final String REMOTE_LOGGER_ON_STRING="OSGI:name=Remote Logger"; - - private String version=null; - - private LogReaderService lrs=null; - private BundleContext bc=null; - - private Object logMutex=new Object(); - - private boolean debugLogFlag=true; - private boolean errorLogFlag=true; - private boolean infoLogFlag=true; - private boolean warningLogFlag=true; - - private MBeanServer agent=null; - private ObjectName remoteLoggerON=null; - -//ServiceListener Interface - public void serviceChanged(ServiceEvent serviceevent) { - ServiceReference servicereference= serviceevent.getServiceReference(); - String as[]=(String[])servicereference.getProperty("objectClass"); - switch (serviceevent.getType()) { - case ServiceEvent.REGISTERED : - if (as[0].equals(LogReaderService.class.getName())){ - this.registerLogReaderService(servicereference); - }else if (as[0].equals(MBeanServer.class.getName())){ - this.registerToAgent(servicereference); - } - break; - case ServiceEvent.UNREGISTERING : - if (as[0].equals(LogReaderService.class.getName())){ - this.unRegisterLogReaderService(servicereference); - }else if (as[0].equals(MBeanServer.class.getName())){ - this.unRegisterFromAgent(); - } - break; - } - } - - -//LogListener Interface - public void logged(LogEntry log){ - StringBuffer message=new StringBuffer(); - synchronized (logMutex){ - int lLevel=log.getLevel(); - if(debugLogFlag && lLevel==LogService.LOG_DEBUG){ - message.append("DEBUG : "); - }else if (errorLogFlag && lLevel==LogService.LOG_ERROR){ - message.append("ERROR : "); - }else if(infoLogFlag && lLevel==LogService.LOG_INFO){ - message.append("INFO : "); - }else if(warningLogFlag && lLevel==LogService.LOG_WARNING){ - message.append("WARNING : "); - }else { - message.append("NO LEVEL : "); - } - try{ - message.append(log.getBundle().getBundleId()+" : "); - }catch(NullPointerException e){ - message.append("Unknown source"); - } - message.append(log.getMessage()); - } - System.out.println(message.toString()); - if (this.agent!=null){ - this.sendNotification(new AttributeChangeNotification(this.remoteLoggerON, 0, 0,message.toString(), null, "Log", null, null)); - } - } - -//BundleActivator Interface - public void start(BundleContext bc) throws Exception{ - this.version=(String)bc.getBundle().getHeaders().get(Constants.BUNDLE_VERSION); - this.bc=bc; - this.log(LogService.LOG_INFO, "Remote Logger starting "+version); - try{ - this.remoteLoggerON=new ObjectName(Logger.REMOTE_LOGGER_ON_STRING); - }catch(MalformedObjectNameException e){ - throw new BundleException("Logger.Logger:objectName invalid", e); - } - try{ - bc.addServiceListener(this,"(|(objectClass="+LogReaderService.class.getName()+")"+ - "(objectClass="+MBeanServer.class.getName()+"))"); - }catch(InvalidSyntaxException e){ - throw new BundleException("Logger.Logger:filtre LDAP", e); - } - ServiceReference sr=bc.getServiceReference(LogReaderService.class.getName()); - if (sr!=null){ - this.registerLogReaderService(sr); - } - - ServiceReference sr2=bc.getServiceReference(MBeanServer.class.getName()); - if (sr2!=null){ - this.registerToAgent(sr2); - } - this.log(LogService.LOG_INFO, "Remote Logger started "+version); - - } - - public void stop(BundleContext bc) throws Exception{ - this.log(LogService.LOG_INFO, "Stopping remote Logger "+version); - if (this.lrs==null){ - System.out.println("ERROR : Logger.stop : there is no logger or reader to stop"); - } else { - this.lrs.removeLogListener(this); - this.bc.removeServiceListener(this); - } - if (this.agent!=null){ - this.unRegisterFromAgent(); - } - this.agent=null; - this.lrs=null; - this.log(LogService.LOG_INFO, "Remote Logger stopped"+version); - this.bc=null; - } - -//private methods - private void registerLogReaderService(ServiceReference sr) { - this.lrs=(LogReaderService)this.bc.getService(sr); - this.lrs.addLogListener(this); - } - - private void unRegisterLogReaderService(ServiceReference sr) { - if (sr!=null){ - this.lrs.removeLogListener(this); - this.lrs=null; - } - } - - private void registerToAgent(ServiceReference sr){ - this.agent=(MBeanServer)bc.getService(sr); - try{ - this.agent.registerMBean(this, this.remoteLoggerON); - }catch(Exception e){ - e.printStackTrace(); - } - } - - private void unRegisterFromAgent(){ - try{ - this.agent.unregisterMBean(this.remoteLoggerON); - }catch(Exception e){ - //e.printStackTrace(); - } - } - - private void log (int level, String message){ - ServiceReference lsn=bc.getServiceReference(LogService.class.getName()); - if (lsn!=null){ - LogService ls=(LogService)bc.getService(lsn); - ls.log(LogService.LOG_INFO, message); - }else{ - System.out.println("ERROR : Logger.start : No service "+LogService.class.getName()+" is present"); - } - } - - -} diff --git a/mosgi.jmx.remotelogger/src/main/java/org/apache/felix/mosgi/jmx/remotelogger/LoggerMBean.java b/mosgi.jmx.remotelogger/src/main/java/org/apache/felix/mosgi/jmx/remotelogger/LoggerMBean.java deleted file mode 100644 index 7da4d23d7c8..00000000000 --- a/mosgi.jmx.remotelogger/src/main/java/org/apache/felix/mosgi/jmx/remotelogger/LoggerMBean.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.jmx.remotelogger; - -public interface LoggerMBean{} diff --git a/mosgi.jmx.rmiconnector/pom.xml b/mosgi.jmx.rmiconnector/pom.xml deleted file mode 100644 index f2b2ef9bce8..00000000000 --- a/mosgi.jmx.rmiconnector/pom.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - org.apache.felix - felix - 0.8.0-SNAPSHOT - - 4.0.0 - osgi-bundle - Apache Felix MOSGi JMX rmiconnector - org.apache.felix.mosgi.jmx.rmiconnector - - - ${pom.groupId} - org.osgi.core - ${pom.version} - provided - - - ${pom.groupId} - org.osgi.compendium - ${pom.version} - provided - - - ${pom.groupId} - org.apache.felix.mosgi.jmx.agent - ${pom.version} - provided - - - ${pom.groupId} - org.apache.felix.mosgi.jmx.registry - ${pom.version} - provided - - - ${pom.groupId} - org.apache.felix.framework - ${pom.version} - provided - - - - - - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} - true - - - MOSGi JMX-MX4J RMI Connector - MOSGi JMX-MX4J RMI Connector - auto-detect - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/ - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}.jar - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}-src.jar - ${pom.artifactId} - - org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote.provider.rmi;specification-version="1.0.0" - - - org.osgi.service.log;specification-version="1.0.0", - org.osgi.framework;specification-version="1.0.0", - org.apache.felix.mosgi.jmx.registry.mx4j.tools.naming;specification-version="1.0.0", - org.apache.felix.mosgi.jmx.agent.mx4j.util;specification-version="1.0.0", - javax.management;specification-version="1.0.0", - javax.management.loading;specification-version="1.0.0", - javax.management.remote;specification-version="1.0.0", - javax.management.remote.rmi;specification-version="1.0.0", - javax.naming;specification-version="1.0.0", - javax.security.auth;specification-version="1.0.0" - - - - - - - diff --git a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/AbstractHeartBeat.java b/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/AbstractHeartBeat.java deleted file mode 100644 index ff5edd3704a..00000000000 --- a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/AbstractHeartBeat.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (C) MX4J. - * All rights reserved. - * - * This software is distributed under the terms of the MX4J License version 1.0. - * See the terms of the MX4J License in the documentation provided with this software. - */ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote; - -import java.io.IOException; -import java.util.Map; - -/** - * - * @author Simone Bordet - * @version $Revision: 1.1.1.1 $ - */ -public abstract class AbstractHeartBeat implements HeartBeat, Runnable -{ - private final ConnectionNotificationEmitter emitter; - private long pulsePeriod; - private int maxRetries; - private Thread thread; - private volatile boolean stopped; - - protected AbstractHeartBeat(ConnectionNotificationEmitter emitter, Map environment) - { - this.emitter = emitter; - if (environment != null) - { - try - { - pulsePeriod = ((Long)environment.get(MX4JRemoteConstants.CONNECTION_HEARTBEAT_PERIOD)).longValue(); - } - catch (Exception ignored) - { - } - try - { - maxRetries = ((Integer)environment.get(MX4JRemoteConstants.CONNECTION_HEARTBEAT_RETRIES)).intValue(); - } - catch (Exception ignored) - { - } - } - if (pulsePeriod <= 0) pulsePeriod = 5000; - if (maxRetries <= 0) maxRetries = 3; - } - - protected abstract void pulse() throws IOException; - - public void start() throws IOException - { - thread = new Thread(this, "Connection HeartBeat"); - thread.setDaemon(true); - thread.start(); - } - - public void stop() throws IOException - { - if (stopped) return; - stopped = true; - thread.interrupt(); - } - - public void run() - { - int retries = 0; - while (!stopped && !thread.isInterrupted()) - { - try - { - Thread.sleep(pulsePeriod); - - try - { - pulse(); - retries = 0; - } - catch (IOException x) - { - if (retries++ == maxRetries) - { - // The connection has died - sendConnectionNotificationFailed(); - // And go on - } - } - } - catch (InterruptedException x) - { - Thread.currentThread().interrupt(); - return; - } - } - } - - protected void sendConnectionNotificationFailed() - { - emitter.sendConnectionNotificationFailed(); - } -} diff --git a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/ClientProxy.java b/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/ClientProxy.java deleted file mode 100644 index 08a88875fad..00000000000 --- a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/ClientProxy.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) MX4J. - * All rights reserved. - * - * This software is distributed under the terms of the MX4J License version 1.0. - * See the terms of the MX4J License in the documentation provided with this software. - */ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.InvocationTargetException; - -import javax.management.MBeanServerConnection; - -/** - * @author Simone Bordet - * @version $Revision: 1.1.1.1 $ - */ -public class ClientProxy implements InvocationHandler -{ - private final MBeanServerConnection target; - - protected ClientProxy(MBeanServerConnection target) - { - this.target = target; - } - - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable - { - try - { - return method.invoke(target, args); - } - catch (InvocationTargetException x) - { - throw x.getTargetException(); - } - } -} diff --git a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/ConnectionNotificationEmitter.java b/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/ConnectionNotificationEmitter.java deleted file mode 100644 index ced5560b080..00000000000 --- a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/ConnectionNotificationEmitter.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) MX4J. - * All rights reserved. - * - * This software is distributed under the terms of the MX4J License version 1.0. - * See the terms of the MX4J License in the documentation provided with this software. - */ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote; - -import java.io.IOException; - -import javax.management.NotificationBroadcasterSupport; -import javax.management.remote.JMXConnectionNotification; -import javax.management.remote.JMXConnector; -import javax.management.remote.rmi.RMIConnector; - -/** - * - * @author Simone Bordet - * @version $Revision: 1.1.1.1 $ - */ -public class ConnectionNotificationEmitter extends NotificationBroadcasterSupport -{ - private static long sequenceNumber; - - private JMXConnector connector; - - public ConnectionNotificationEmitter(JMXConnector connector) - { - this.connector = connector; - } - - private long getNextNotificationNumber() - { - synchronized (RMIConnector.class) - { - return sequenceNumber++; - } - } - - private String getConnectionId() - { - try - { - return connector.getConnectionId(); - } - catch (IOException x) - { - return null; - } - } - - public void sendConnectionNotificationOpened() - { - JMXConnectionNotification notification = new JMXConnectionNotification(JMXConnectionNotification.OPENED, connector, getConnectionId(), getNextNotificationNumber(), "Connection opened", null); - sendNotification(notification); - } - - public void sendConnectionNotificationClosed() - { - JMXConnectionNotification notification = new JMXConnectionNotification(JMXConnectionNotification.CLOSED, connector, getConnectionId(), getNextNotificationNumber(), "Connection closed", null); - sendNotification(notification); - } - - public void sendConnectionNotificationFailed() - { - JMXConnectionNotification notification = new JMXConnectionNotification(JMXConnectionNotification.FAILED, connector, getConnectionId(), getNextNotificationNumber(), "Connection failed", null); - sendNotification(notification); - } - - public void sendConnectionNotificationLost(long howMany) - { - JMXConnectionNotification notification = new JMXConnectionNotification(JMXConnectionNotification.NOTIFS_LOST, connector, getConnectionId(), getNextNotificationNumber(), "Some notification (" + howMany + ") was lost", null); - sendNotification(notification); - } -} diff --git a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/HeartBeat.java b/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/HeartBeat.java deleted file mode 100644 index 23f2a224a58..00000000000 --- a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/HeartBeat.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) MX4J. - * All rights reserved. - * - * This software is distributed under the terms of the MX4J License version 1.0. - * See the terms of the MX4J License in the documentation provided with this software. - */ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote; - -import java.io.IOException; - -/** - * - * @author Simone Bordet - * @version $Revision: 1.1.1.1 $ - */ -public interface HeartBeat -{ - public void start() throws IOException; - public void stop() throws IOException; -} diff --git a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/ProviderFactory.java b/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/ProviderFactory.java deleted file mode 100644 index 78cce6baab2..00000000000 --- a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/ProviderFactory.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (C) MX4J. - * All rights reserved. - * - * This software is distributed under the terms of the MX4J License version 1.0. - * See the terms of the MX4J License in the documentation provided with this software. - */ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.util.Map; -import java.util.StringTokenizer; - -import javax.management.remote.JMXConnectorFactory; -import javax.management.remote.JMXConnectorProvider; -import javax.management.remote.JMXConnectorServerFactory; -import javax.management.remote.JMXConnectorServerProvider; -import javax.management.remote.JMXProviderException; -import javax.management.remote.JMXServiceURL; - -import org.osgi.service.log.LogService; - -import org.apache.felix.mosgi.jmx.rmiconnector.RmiConnectorActivator; - -import org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote.MX4JRemoteConstants; -import org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote.ProviderHelper; - -/** - * - * @author Simone Bordet - * @version $Revision: 1.2 $ - */ -public class ProviderFactory extends ProviderHelper -{ - public static JMXConnectorProvider newJMXConnectorProvider(JMXServiceURL url, Map env) throws IOException - { - // Yes, throw NPE if url is null (spec compliant) - String protocol = normalizeProtocol(url.getProtocol()); - String providerPackages = findProviderPackageList(env, JMXConnectorFactory.PROTOCOL_PROVIDER_PACKAGES); - ClassLoader classLoader = findProviderClassLoader(env, JMXConnectorFactory.PROTOCOL_PROVIDER_CLASS_LOADER); - JMXConnectorProvider provider = (JMXConnectorProvider)loadProvider(providerPackages, protocol, MX4JRemoteConstants.CLIENT_PROVIDER_CLASS, classLoader); - return provider; - } - - public static JMXConnectorServerProvider newJMXConnectorServerProvider(JMXServiceURL url, Map env) throws IOException - { - // Yes, throw NPE if url is null (spec compliant) - String protocol = normalizeProtocol(url.getProtocol()); - String providerPackages = findProviderPackageList(env, JMXConnectorServerFactory.PROTOCOL_PROVIDER_PACKAGES); - ClassLoader classLoader = findProviderClassLoader(env, JMXConnectorFactory.PROTOCOL_PROVIDER_CLASS_LOADER); - JMXConnectorServerProvider provider = (JMXConnectorServerProvider)loadProvider(providerPackages, protocol, MX4JRemoteConstants.SERVER_PROVIDER_CLASS, classLoader); - return provider; - } - - private static String findEnvironmentProviderPackageList(Map environment, String key) throws JMXProviderException - { - String providerPackages = null; - if (environment != null) - { - Object pkgs = environment.get(key); - RmiConnectorActivator.log(LogService.LOG_DEBUG, "Provider packages in the environment: " + pkgs, null); - if (pkgs != null && !(pkgs instanceof String)) throw new JMXProviderException("Provider package list must be a string"); - providerPackages = (String)pkgs; - } - return providerPackages; - } - - private static String findProviderPackageList(Map environment, final String providerPkgsKey) throws JMXProviderException - { - // 1. Look in the environment - // 2. Look for system property - // 3. Use implementation's provider - - String providerPackages = findEnvironmentProviderPackageList(environment, providerPkgsKey); - - if (providerPackages == null) - { - providerPackages = findSystemPackageList(providerPkgsKey); - } - - if (providerPackages != null && providerPackages.trim().length() == 0) throw new JMXProviderException("Provider package list cannot be an empty string"); - - if (providerPackages == null) - providerPackages = MX4JRemoteConstants.PROVIDER_PACKAGES; - else - providerPackages += MX4JRemoteConstants.PROVIDER_PACKAGES_SEPARATOR + MX4JRemoteConstants.PROVIDER_PACKAGES; - - RmiConnectorActivator.log(LogService.LOG_DEBUG,"Provider packages list is: " + providerPackages,null); - - return providerPackages; - } - - private static ClassLoader findProviderClassLoader(Map environment, String providerLoaderKey) - { - - ClassLoader classLoader = null; - if (environment != null) - { - Object loader = environment.get(providerLoaderKey); - RmiConnectorActivator.log(LogService.LOG_DEBUG,"Provider classloader in the environment: " + loader, null); - if (loader != null && !(loader instanceof ClassLoader)) throw new IllegalArgumentException("Provider classloader is not a ClassLoader"); - classLoader = (ClassLoader)loader; - } - - if (classLoader == null) - { - //classLoader = Thread.currentThread().getContextClassLoader(); - classLoader = ProviderFactory.class.getClassLoader(); - RmiConnectorActivator.log(LogService.LOG_DEBUG,"Provider classloader (was null) in the environment: " + classLoader, null); - } - - // Add the classloader as required by the spec - environment.put(JMXConnectorFactory.PROTOCOL_PROVIDER_CLASS_LOADER, classLoader); - RmiConnectorActivator.log(LogService.LOG_WARNING,"Provider classloader added to the environment", null); - - return classLoader; - } - - private static Object loadProvider(String packages, String protocol, String className, ClassLoader loader) throws JMXProviderException, MalformedURLException - { - StringTokenizer tokenizer = new StringTokenizer(packages, MX4JRemoteConstants.PROVIDER_PACKAGES_SEPARATOR); - while (tokenizer.hasMoreTokens()) - { - String pkg = tokenizer.nextToken().trim(); - RmiConnectorActivator.log(LogService.LOG_DEBUG,"Provider package: " + pkg, null); - - // The spec states the package cannot be empty - if (pkg.length() == 0) throw new JMXProviderException("Empty package list not allowed: " + packages); - - String providerClassName = constructClassName(pkg, protocol, className); - - Class providerClass = null; - try - { - providerClass = loadClass(providerClassName, loader); - } - catch (ClassNotFoundException x) - { - RmiConnectorActivator.log(LogService.LOG_DEBUG,"Provider class " + providerClassName + " not found, continuing with next package",null); - continue; - } - catch (Exception x) - { - RmiConnectorActivator.log(LogService.LOG_WARNING,"Cannot load provider class " + providerClassName, x); - throw new JMXProviderException("Cannot load provider class " + providerClassName, x); - } - - try - { - return providerClass.newInstance(); - } - catch (Exception x) - { - RmiConnectorActivator.log(LogService.LOG_WARNING,"Cannot instantiate provider class " + providerClassName, x); - throw new JMXProviderException("Cannot instantiate provider class " + providerClassName, x); - } - } - - // Nothing found - RmiConnectorActivator.log(LogService.LOG_DEBUG,"Could not find provider for protocol " + protocol + " in package list '" + packages + "'", null); - throw new MalformedURLException("Could not find provider for protocol " + protocol + " in package list '" + packages + "'"); - } -} diff --git a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/ProviderHelper.java b/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/ProviderHelper.java deleted file mode 100644 index ab27a9668fc..00000000000 --- a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/ProviderHelper.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote; - -import java.security.AccessController; -import java.security.PrivilegedAction; - -import org.osgi.service.log.LogService; - -import org.apache.felix.mosgi.jmx.rmiconnector.RmiConnectorActivator; - -/** - * - * @version $Revision: 1.2 $ - */ -public abstract class ProviderHelper -{ - protected static String normalizeProtocol(String protocol) - { - // Replace special chars as required by the spec - String normalized = protocol.replace('+', '.'); - normalized = normalized.replace('-', '_'); - RmiConnectorActivator.log(LogService.LOG_INFO, "Normalizing protocol: " + protocol + " --> " + normalized, null); - return normalized; - } - - protected static String findSystemPackageList(final String key) - { - String providerPackages = (String)AccessController.doPrivileged(new PrivilegedAction() - { - public Object run() - { - return System.getProperty(key); - } - }); - RmiConnectorActivator.log(LogService.LOG_DEBUG,"Packages in the system property '" + key + "': " + providerPackages,null); - return providerPackages; - } - - protected static Class loadClass(String className, ClassLoader loader) throws ClassNotFoundException - { - RmiConnectorActivator.log(LogService.LOG_DEBUG,"Loading class: " + className +" From "+loader,null); - if (loader == null){ - loader= ProviderHelper.class.getClassLoader(); - RmiConnectorActivator.log(LogService.LOG_DEBUG,"a new loader "+loader,null); - - //Thread.currentThread().getContextClassLoader(); - } - return loader.loadClass(className); - } - - protected static String constructClassName(String packageName, String protocol, String className) - { - return new StringBuffer(packageName).append(".").append(protocol).append(".").append(className).toString(); - } - -} diff --git a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/RemoteNotificationServerHandler.java b/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/RemoteNotificationServerHandler.java deleted file mode 100644 index effff75a116..00000000000 --- a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/RemoteNotificationServerHandler.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) MX4J. - * All rights reserved. - * - * This software is distributed under the terms of the MX4J License version 1.0. - * See the terms of the MX4J License in the documentation provided with this software. - */ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote; - -import javax.management.ObjectName; -import javax.management.NotificationFilter; -import javax.management.NotificationListener; -import javax.management.remote.NotificationResult; - -/** - * @author Simone Bordet - * @version $Revision: 1.1.1.1 $ - */ -public interface RemoteNotificationServerHandler -{ - public Integer generateListenerID(ObjectName name, NotificationFilter filter); - - public NotificationListener getServerNotificationListener(); - - public void addNotificationListener(Integer id, NotificationTuple tuple); - - public void removeNotificationListener(Integer id); - - public NotificationTuple getNotificationListener(Integer id); - - public NotificationResult fetchNotifications(long sequenceNumber, int maxNotifications, long timeout); -} diff --git a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/provider/RMIClientProvider.java b/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/provider/RMIClientProvider.java deleted file mode 100644 index ec2e800ec16..00000000000 --- a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/provider/RMIClientProvider.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) MX4J. - * All rights reserved. - * - * This software is distributed under the terms of the MX4J License version 1.0. - * See the terms of the MX4J License in the documentation provided with this software. - */ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote.provider; - -import java.util.Map; -import java.io.IOException; - -import javax.management.remote.JMXConnectorProvider; -import javax.management.remote.JMXConnector; -import javax.management.remote.JMXServiceURL; -import javax.management.remote.rmi.RMIConnector; - -/** - * @author Simone Bordet - * @version $Revision: 1.1.1.1 $ - */ -public abstract class RMIClientProvider implements JMXConnectorProvider -{ - public JMXConnector newJMXConnector(JMXServiceURL url, Map environment) throws IOException - { - return new RMIConnector(url, environment); - } -} diff --git a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/provider/RMIServerProvider.java b/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/provider/RMIServerProvider.java deleted file mode 100644 index 15d4d8ab7d7..00000000000 --- a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/provider/RMIServerProvider.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) MX4J. - * All rights reserved. - * - * This software is distributed under the terms of the MX4J License version 1.0. - * See the terms of the MX4J License in the documentation provided with this software. - */ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote.provider; - -import java.util.Map; -import java.io.IOException; - -import javax.management.remote.JMXConnectorServerProvider; -import javax.management.remote.JMXConnectorServer; -import javax.management.remote.JMXServiceURL; -import javax.management.remote.rmi.RMIConnectorServer; -import javax.management.MBeanServer; - -/** - * - * @author Simone Bordet - * @version $Revision: 1.1.1.1 $ - */ -public abstract class RMIServerProvider implements JMXConnectorServerProvider -{ - public JMXConnectorServer newJMXConnectorServer(JMXServiceURL url, Map environment, MBeanServer server) throws IOException - { - return new RMIConnectorServer(url, environment, server); - } -} diff --git a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/provider/rmi/ServerProvider.java b/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/provider/rmi/ServerProvider.java deleted file mode 100644 index d1a20cb1e85..00000000000 --- a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/provider/rmi/ServerProvider.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) MX4J. - * All rights reserved. - * - * This software is distributed under the terms of the MX4J License version 1.0. - * See the terms of the MX4J License in the documentation provided with this software. - */ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote.provider.rmi; - -import org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote.provider.RMIServerProvider; - -/** - * @author Simone Bordet - * @version $Revision: 1.1.1.1 $ - */ -public class ServerProvider extends RMIServerProvider -{ -} diff --git a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/rmi/ClientExceptionCatcher.java b/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/rmi/ClientExceptionCatcher.java deleted file mode 100644 index d908b4c713f..00000000000 --- a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/rmi/ClientExceptionCatcher.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) MX4J. - * All rights reserved. - * - * This software is distributed under the terms of the MX4J License version 1.0. - * See the terms of the MX4J License in the documentation provided with this software. - */ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote.rmi; - -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.rmi.NoSuchObjectException; -import java.io.IOException; - -import javax.management.MBeanServerConnection; -import javax.management.remote.JMXServerErrorException; - -import org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote.ClientProxy; - -/** - * @author Simone Bordet - * @version $Revision: 1.1.1.1 $ - */ -public class ClientExceptionCatcher extends ClientProxy -{ - private ClientExceptionCatcher(MBeanServerConnection target) - { - super(target); - } - - public static MBeanServerConnection newInstance(MBeanServerConnection target) - { - ClientExceptionCatcher handler = new ClientExceptionCatcher(target); - return (MBeanServerConnection)Proxy.newProxyInstance(handler.getClass().getClassLoader(), new Class[]{MBeanServerConnection.class}, handler); - } - - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable - { - try - { - return super.invoke(proxy, method, args); - } - catch (NoSuchObjectException x) - { - // The connection has been already closed by the server - throw new IOException("Connection closed by the server"); - } - catch (Exception x) - { - throw x; - } - catch (Error x) - { - throw new JMXServerErrorException("Error thrown during invocation", x); - } - } -} diff --git a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/rmi/ClientUnmarshaller.java b/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/rmi/ClientUnmarshaller.java deleted file mode 100644 index c18ef3ad807..00000000000 --- a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/rmi/ClientUnmarshaller.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) MX4J. - * All rights reserved. - * - * This software is distributed under the terms of the MX4J License version 1.0. - * See the terms of the MX4J License in the documentation provided with this software. - */ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote.rmi; - -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.security.AccessController; -import java.security.PrivilegedAction; - -import javax.management.MBeanServerConnection; - -import org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote.ClientProxy; - -/** - * An MBeanServerConnection proxy that performs the setting of the appropriate context classloader - * to allow classloading of classes sent by the server but not known to the client, in methods like - * {@link MBeanServerConnection#getAttribute}, {@link MBeanServerConnection#invoke} and so on. - * - * @author Simone Bordet - * @version $Revision: 1.1.1.1 $ - */ -public class ClientUnmarshaller extends ClientProxy -{ - private final ClassLoader classLoader; - - private ClientUnmarshaller(MBeanServerConnection target, ClassLoader loader) - { - super(target); - this.classLoader = loader; - } - - public static MBeanServerConnection newInstance(MBeanServerConnection target, ClassLoader loader) - { - ClientUnmarshaller handler = new ClientUnmarshaller(target, loader); - return (MBeanServerConnection)Proxy.newProxyInstance(handler.getClass().getClassLoader(), new Class[]{MBeanServerConnection.class}, handler); - } - - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable - { - if (classLoader == null) - { - return chain(proxy, method, args); - } - else - { - ClassLoader old = Thread.currentThread().getContextClassLoader(); - try - { - setContextClassLoader(classLoader); - return chain(proxy, method, args); - } - finally - { - setContextClassLoader(old); - } - } - } - - private Object chain(Object proxy, Method method, Object[] args) throws Throwable - { - return super.invoke(proxy, method, args); - } - - private void setContextClassLoader(final ClassLoader loader) - { - AccessController.doPrivileged(new PrivilegedAction() - { - public Object run() - { - Thread.currentThread().setContextClassLoader(loader); - return null; - } - }); - } -} diff --git a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/rmi/RMIConnectionProxy.java b/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/rmi/RMIConnectionProxy.java deleted file mode 100644 index a1308767cd9..00000000000 --- a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/rmi/RMIConnectionProxy.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) MX4J. - * All rights reserved. - * - * This software is distributed under the terms of the MX4J License version 1.0. - * See the terms of the MX4J License in the documentation provided with this software. - */ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote.rmi; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.InvocationTargetException; - -import javax.management.remote.rmi.RMIConnection; - -/** - * Base class for RMIConnection dynamic proxies. - * - * @author Simone Bordet - * @version $Revision: 1.1.1.1 $ - */ -public class RMIConnectionProxy implements InvocationHandler -{ - private RMIConnection nested; - - protected RMIConnectionProxy(RMIConnection nested) - { - this.nested = nested; - } - - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable - { - try - { - return method.invoke(nested, args); - } - catch (InvocationTargetException x) - { - throw x.getTargetException(); - } - } -} diff --git a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/rmi/RMIHeartBeat.java b/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/rmi/RMIHeartBeat.java deleted file mode 100644 index 9d539ad958c..00000000000 --- a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/rmi/RMIHeartBeat.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) MX4J. - * All rights reserved. - * - * This software is distributed under the terms of the MX4J License version 1.0. - * See the terms of the MX4J License in the documentation provided with this software. - */ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote.rmi; - -import java.io.IOException; -import java.util.Map; - -import javax.management.remote.rmi.RMIConnection; - -import org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote.AbstractHeartBeat; -import org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote.ConnectionNotificationEmitter; - -/** - * @author Simone Bordet - * @version $Revision: 1.1.1.1 $ - */ -public class RMIHeartBeat extends AbstractHeartBeat -{ - private final RMIConnection connection; - - public RMIHeartBeat(RMIConnection connection, ConnectionNotificationEmitter emitter, Map environment) - { - super(emitter, environment); - this.connection = connection; - } - - protected void pulse() throws IOException - { - connection.getDefaultDomain(null); - } -} diff --git a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/rmi/RMIRemoteNotificationServerHandler.java b/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/rmi/RMIRemoteNotificationServerHandler.java deleted file mode 100644 index 8d68bbcad76..00000000000 --- a/mosgi.jmx.rmiconnector/src/main/java/org/apache/felix/mosgi/jmx/rmiconnector/mx4j/remote/rmi/RMIRemoteNotificationServerHandler.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote.rmi; - -import java.util.Map; -import java.util.ArrayList; - -import javax.management.remote.TargetedNotification; - -import org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote.DefaultRemoteNotificationServerHandler; -import org.apache.felix.mosgi.jmx.rmiconnector.mx4j.remote.MX4JRemoteUtils; - -import org.osgi.service.log.LogService; -import org.apache.felix.mosgi.jmx.rmiconnector.RmiConnectorActivator; - -/** - * - * @version $Revision: 1.1.1.1 $ - */ -class RMIRemoteNotificationServerHandler extends DefaultRemoteNotificationServerHandler -{ - RMIRemoteNotificationServerHandler(Map environment) - { - super(environment); - } - - protected TargetedNotification[] filterNotifications(TargetedNotification[] notifications) - { - ArrayList list = new ArrayList(); - for (int i = 0; i < notifications.length; ++i) - { - TargetedNotification notification = notifications[i]; - if (MX4JRemoteUtils.isTrulySerializable(notification)) - { - list.add(notification); - } - else - { - RmiConnectorActivator.log(LogService.LOG_INFO,"Cannot send notification " + notification + " to the client: it is not serializable", null); - } - } - return (TargetedNotification[])list.toArray(new TargetedNotification[list.size()]); - } -} diff --git a/mosgi.managedelements.bundlesprobes.tab/pom.xml b/mosgi.managedelements.bundlesprobes.tab/pom.xml deleted file mode 100644 index 6f667787ebf..00000000000 --- a/mosgi.managedelements.bundlesprobes.tab/pom.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - org.apache.felix - felix - 0.8.0-SNAPSHOT - - 4.0.0 - osgi-bundle - Apache Felix MOSGi Bundles management tab for the JMX console - org.apache.felix.mosgi.managedelements.bundlesprobes.tab - - - ${pom.groupId} - org.osgi.core - ${pom.version} - provided - - - ${pom.groupId} - org.osgi.compendium - ${pom.version} - provided - - - ${pom.groupId} - org.apache.felix.mosgi.console.ifc - ${pom.version} - provided - - - ${pom.groupId} - org.apache.felix.framework - ${pom.version} - provided - - - - - - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} - true - - - MOSGi Bundles management tab for the JMX console - MOSGi Bundles management tab for the JMX console - auto-detect - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/ - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}.jar - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}-src.jar - ${pom.artifactId} - - ${pom.artifactId};specification-version="1.0.0" - - - javax.management;specification-version="1.0.0", - org.osgi.framework;specification-version="1.0.0", - javax.management.openmbean;specification-version="1.0.0", - javax.swing;specification-version="1.0.0", - javax.swing.table;specification-version="1.0.0", - org.apache.felix.mosgi.console.ifc;specification-version="1.0.0" - - - - - - - diff --git a/mosgi.managedelements.bundlesprobes/pom.xml b/mosgi.managedelements.bundlesprobes/pom.xml deleted file mode 100644 index a4231fb046a..00000000000 --- a/mosgi.managedelements.bundlesprobes/pom.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - org.apache.felix - felix - 0.8.0-SNAPSHOT - - 4.0.0 - osgi-bundle - Apache Felix MOSGi JMX MBean for OSGi bundles management - org.apache.felix.mosgi.managedelements.bundlesprobes - - - ${pom.groupId} - org.osgi.core - ${pom.version} - provided - - - ${pom.groupId} - org.osgi.compendium - ${pom.version} - provided - - - ${pom.groupId} - org.apache.felix.mosgi.console.ifc - ${pom.version} - provided - - - ${pom.groupId} - org.apache.felix.framework - ${pom.version} - provided - - - - - - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} - true - - - MOSGi JMX MBean for OSGi bundles management - MOSGi JMX MBean for OSGi bundles management - auto-detect - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/ - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}.jar - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}-src.jar - ${pom.artifactId} - - ${pom.artifactId};specification-version="1.0.0" - - - org.osgi.framework;specification-version="1.0.0", - org.osgi.service.log;specification-version="1.0.0", - javax.management;specification-version="1.0.0", - org.apache.felix.mosgi.console.ifc;specification-version="1.0.0" - - - - - - - diff --git a/mosgi.managedelements.bundlesprobes/src/main/java/org/apache/felix/mosgi/managedelements/bundlesprobes/BundlesProbes.java b/mosgi.managedelements.bundlesprobes/src/main/java/org/apache/felix/mosgi/managedelements/bundlesprobes/BundlesProbes.java deleted file mode 100644 index 42b17cbceb9..00000000000 --- a/mosgi.managedelements.bundlesprobes/src/main/java/org/apache/felix/mosgi/managedelements/bundlesprobes/BundlesProbes.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.managedelements.bundlesprobes; - -/** - * TODO : Should listen to Agent Service lifecycle - * Need to change ObjectName - * Should listen to serviceLifecycle -**/ - -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; -import java.util.Vector; - -import javax.management.MBeanServer; -import javax.management.MBeanServerFactory; -import javax.management.ObjectName; -import javax.management.NotificationBroadcasterSupport; -import javax.management.AttributeChangeNotification; - -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; -import org.osgi.framework.BundleEvent; -import org.osgi.framework.BundleException; -import org.osgi.framework.BundleListener; -import org.osgi.framework.Constants; -import org.osgi.framework.ServiceEvent; -import org.osgi.framework.ServiceListener; -import org.osgi.framework.ServiceReference; - - -import org.osgi.service.log.LogService; - -public class BundlesProbes extends NotificationBroadcasterSupport implements BundleActivator, BundlesProbesMBean, ServiceListener, BundleListener { - - private String version = null; - private static String tabNameString = "TabUI:name=BundlesProbes"; - - private ObjectName tabName = null; - private MBeanServer server = null; - private BundleContext bc = null; - private ServiceReference sr = null; - - - //////////////////////////////////////////////////////// - // TabIfc (from BundlesProbesMBean) // - //////////////////////////////////////////////////////// - public String getBundleName() { - return this.bc.getProperty("insa.jmxconsole.tab.url.bundlesprobestab"); - } - - - //////////////////////////////////////////////////////// - // BundleActivator // - //////////////////////////////////////////////////////// - public void start(BundleContext context) throws Exception { - this.bc=context; - this.version=(String)bc.getBundle().getHeaders().get(Constants.BUNDLE_VERSION); - this.log(LogService.LOG_INFO, "Starting BundlesProbe MBean " + this.version,null); - this.tabName=new ObjectName(tabNameString); - this.sr = context.getServiceReference(MBeanServer.class.getName()); - if (sr!=null){ - this.connectToAgent(sr); - } - this.log(LogService.LOG_INFO, "BundlesProbes MBean "+this.version+" started", null); - } - - - public void stop(BundleContext context) { - this.log(LogService.LOG_INFO, "Stopping BundlesProbes MBean "+this.version, null); - if (this.server!=null){ - this.disconnectFromAgent(); - } - this.sr=null; - this.log(LogService.LOG_INFO, "BundlesProbes MBean "+this.version+" stopped", null); - this.bc=null; - } - - //////////////////////////////////////////////////////// - // BundlesProbesMBean // - //////////////////////////////////////////////////////// - public Vector bundleList() { - Bundle[] bl = this.bc.getBundles(); - Vector bundleList = new Vector(); - Vector bundle = null; - for (int i = 0; i < bl.length; i++) { - bundle = new Vector(); - String enum1 = (String) bl[i].getHeaders().get(Constants.BUNDLE_NAME); - long id = bl[i].getBundleId(); - bundle.add(new Long(id)); - int state = bl[i].getState(); - switch (state) { - case Bundle.ACTIVE: - bundle.add(new String("ACTIVE")); - break; - case Bundle.INSTALLED: - bundle.add(new String("INSTALLED")); - break; - case Bundle.RESOLVED: - bundle.add(new String("RESOLVED")); - break; - case Bundle.UNINSTALLED: - bundle.add(new String("UNINSTALLED")); - break; - default: - bundle.add(new String("RESOLVED")); - } - bundle.add(enum1); - bundleList.add(bundle); - } - return bundleList; - } - - public void startService(Long [] id) { - try { - bc.getBundle(id[0].longValue()).start(); - } catch (BundleException e) { - e.printStackTrace(); - } - } - - public void stopService(Long [] id) { - try { - bc.getBundle(id[0].longValue()).stop(); - } catch (BundleException e) { - e.printStackTrace(); - } - } - - public void install(String location) { - try { - bc.installBundle(location); - } catch (BundleException e) { - e.printStackTrace(); - } - } - - public void uninstall(Long [] id) { - try { - bc.getBundle(id[0].longValue()).uninstall(); - } catch (BundleException e) { - e.printStackTrace(); - } - } - - public void update(Long [] id) { - try { - bc.getBundle(id[0].longValue()).update(); - } catch (BundleException e) { - e.printStackTrace(); - } - } - - //////////////////////////////////////////////////////// - // ServiceListener // - //////////////////////////////////////////////////////// - public void serviceChanged(ServiceEvent event) { - ServiceReference sr=event.getServiceReference(); - Object service=bc.getService(sr); - if (this.server==null && event.getType()==ServiceEvent.REGISTERED && service instanceof MBeanServer){ - this.connectToAgent(sr); - } - if (this.server!=null){ - if(event.getType()==ServiceEvent.UNREGISTERING && service instanceof MBeanServer){ - this.disconnectFromAgent(); - }else{ - this.sendRemoteNotification(ServiceEvent.class.getName(),sr.getBundle().getBundleId(), event.getType(), (String)sr.getBundle().getHeaders().get(Constants.BUNDLE_NAME)); - } - } - } - - //////////////////////////////////////////////////////// - // BundleListener // - //////////////////////////////////////////////////////// - public void bundleChanged(BundleEvent event) { - if (this.server!=null){ - Bundle b=event.getBundle(); - //SFR this.sendRemoteNotification(BundleEvent.class.getName(), b.getBundleId(), b.getState(), (String)b.getHeaders().get(Constants.BUNDLE_NAME)); -System.out.println("Evenement bundle "+b.getBundleId()+" : "+event.getType()); - this.sendRemoteNotification(BundleEvent.class.getName(), b.getBundleId(), event.getType(), (String)b.getHeaders().get(Constants.BUNDLE_NAME)); - } - } - - private void sendRemoteNotification(String className, long id, int type, String name){ - StringBuffer str = new StringBuffer(className); - str.append(":"); - str.append(id); - str.append(":"); - str.append(type); - str.append(":"); - str.append(name); - super.sendNotification(new AttributeChangeNotification(this.tabName, 0, 0,str.toString(),null, "Bundle", null, null)); - } - - private void connectToAgent(ServiceReference sr){ - this.log(LogService.LOG_INFO, "Registering to agent", null); - try{ - this.server=(MBeanServer)this.bc.getService(sr); - this.server.registerMBean(this, tabName); - this.bc.addServiceListener(this); - this.bc.addBundleListener(this); - }catch (Exception e){ - e.printStackTrace(); - } - this.log(LogService.LOG_INFO, "Registered to agent", null); - } - - private void disconnectFromAgent(){ - this.log(LogService.LOG_INFO, "Unregistering from agent", null); - this.bc.removeServiceListener(this); - this.bc.removeBundleListener(this); - try { - server.unregisterMBean(tabName); - } catch (Exception e) { - e.printStackTrace(); - } - this.server=null; - this.bc.ungetService(this.sr); - this.log(LogService.LOG_INFO, "Unregistered from agent", null); - } - - private void log(int prio, String message, Throwable t){ - if (this.bc!=null){ - ServiceReference logSR=this.bc.getServiceReference(LogService.class.getName()); - if (logSR!=null){ - ((LogService)this.bc.getService(logSR)).log(prio, message, t); - }else{ - System.out.println("No Log Service"); - } - }else{ - System.out.println(this.getClass().getName()+".log: No bundleContext"); - } - } - -} diff --git a/mosgi.managedelements.bundlesprobes/src/main/java/org/apache/felix/mosgi/managedelements/bundlesprobes/BundlesProbesMBean.java b/mosgi.managedelements.bundlesprobes/src/main/java/org/apache/felix/mosgi/managedelements/bundlesprobes/BundlesProbesMBean.java deleted file mode 100644 index 4b3dfbaab88..00000000000 --- a/mosgi.managedelements.bundlesprobes/src/main/java/org/apache/felix/mosgi/managedelements/bundlesprobes/BundlesProbesMBean.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.managedelements.bundlesprobes; -import org.apache.felix.mosgi.console.ifc.TabIfc; -import java.util.Vector; - -public interface BundlesProbesMBean extends TabIfc { - public Vector bundleList(); - - public void startService(Long [] id); - public void stopService(Long [] id); - public void install(String location); - public void uninstall(Long[] id); - public void update(Long [] id); -} diff --git a/mosgi.managedelements.memoryprobe/pom.xml b/mosgi.managedelements.memoryprobe/pom.xml deleted file mode 100644 index dd85ee2425f..00000000000 --- a/mosgi.managedelements.memoryprobe/pom.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - org.apache.felix - felix - 0.8.0-SNAPSHOT - - 4.0.0 - osgi-bundle - Apache Felix MOSGi JMX MBean for obr interaction - org.apache.felix.mosgi.managedelements.memoryprobe - - - ${pom.groupId} - org.osgi.core - ${pom.version} - provided - - - ${pom.groupId} - org.osgi.compendium - ${pom.version} - provided - - - ${pom.groupId} - org.apache.felix.mosgi.console.ifc - ${pom.version} - provided - - - ${pom.groupId} - org.apache.felix.framework - ${pom.version} - provided - - - ${pom.groupId} - org.apache.felix.mosgi.jmx.agent - ${pom.version} - provided - - - - - - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} - true - - - MOSGi JMX MBean for memory supervision - MOSGi JMX MBean for memory supervision - auto-detect - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/ - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}.jar - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}-src.jar - ${pom.artifactId} - - org.osgi.framework;specification-version="1.0.0", - javax.management;specification-version="1.0.0", - org.apache.felix.mosgi.console.ifc;specification-version="1.0.0" - - - - - - - diff --git a/mosgi.managedelements.memoryprobe/src/main/java/org/apache/felix/mosgi/managedelements/memoryprobe/MemoryProbe.java b/mosgi.managedelements.memoryprobe/src/main/java/org/apache/felix/mosgi/managedelements/memoryprobe/MemoryProbe.java deleted file mode 100644 index c9439edaed2..00000000000 --- a/mosgi.managedelements.memoryprobe/src/main/java/org/apache/felix/mosgi/managedelements/memoryprobe/MemoryProbe.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.managedelements.memoryprobe; - -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; -import org.osgi.framework.ServiceRegistration; - -import javax.management.ObjectName; -import javax.management.MBeanServer; - -public class MemoryProbe implements BundleActivator, MemoryProbeMBean { - private static final String tabNameString = "TabUI:name=MemoryProbe"; - private ObjectName tabName=null; - private ServiceRegistration sr=null; - private BundleContext bc=null; - - //////////////////////////////////////////////////////// - // TabIfc (from OsgiProbesMBean) // - //////////////////////////////////////////////////////// - public String getBundleName() { - return this.bc.getProperty("insa.jmxconsole.tab.url.memoryprobetab"); - } - - //////////////////////////////////////////////////////// - // BundleActivator // - //////////////////////////////////////////////////////// - public void start(BundleContext context) throws Exception { - this.bc=context; - this.tabName=new ObjectName(tabNameString); -/* - this.server=(MBeanServer)this.bc.getService(sr); - this.server.registerMBean(this, tabName); -*/ - java.util.Properties p=new java.util.Properties(); - p.put(org.apache.felix.mosgi.jmx.agent.Constants.OBJECTNAME, this.tabNameString); - sr=this.bc.registerService(MemoryProbeMBean.class.getName(), this, p); - } - - public void stop(BundleContext context) throws Exception { - this.tabName=null; -/* - this.server=(MBeanServer)this.bc.getService(sr); - this.server.registerMBean(this, tabName); -*/ - this.sr.unregister(); - this.sr=null; - this.bc=null; - } -} diff --git a/mosgi.managedelements.memoryprobe/src/main/java/org/apache/felix/mosgi/managedelements/memoryprobe/MemoryProbeMBean.java b/mosgi.managedelements.memoryprobe/src/main/java/org/apache/felix/mosgi/managedelements/memoryprobe/MemoryProbeMBean.java deleted file mode 100644 index 8bd8ab6b2ff..00000000000 --- a/mosgi.managedelements.memoryprobe/src/main/java/org/apache/felix/mosgi/managedelements/memoryprobe/MemoryProbeMBean.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.managedelements.memoryprobe; - -import org.apache.felix.mosgi.console.ifc.TabIfc; - -public interface MemoryProbeMBean extends TabIfc { -} - - diff --git a/mosgi.managedelements.obrprobe.tab/pom.xml b/mosgi.managedelements.obrprobe.tab/pom.xml deleted file mode 100644 index 3e3a0957dc9..00000000000 --- a/mosgi.managedelements.obrprobe.tab/pom.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - org.apache.felix - felix - 0.8.0-SNAPSHOT - - 4.0.0 - osgi-bundle - Apache Felix MOSGi obr remote manipulation tab for the JMX console - org.apache.felix.mosgi.managedelements.obrprobe.tab - - - ${pom.groupId} - org.osgi.core - ${pom.version} - provided - - - ${pom.groupId} - org.osgi.compendium - ${pom.version} - provided - - - ${pom.groupId} - org.apache.felix.mosgi.console.ifc - ${pom.version} - provided - - - ${pom.groupId} - org.apache.felix.bundlerepository - ${pom.version} - provided - - - - - - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} - true - - - MOSGi obr remote manipulation tab for the JMX console - MOSGi obr remote manipulation tab for the JMX console - auto-detect - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/ - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}.jar - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}-src.jar - ${pom.artifactId} - - ${pom.artifactId} - - - javax.management, - org.osgi.framework, - javax.swing, - javax.swing.table, - org.osgi.service.obr, - org.apache.felix.mosgi.console.ifc - - - - - - - diff --git a/mosgi.managedelements.obrprobe/pom.xml b/mosgi.managedelements.obrprobe/pom.xml deleted file mode 100644 index 997993387bd..00000000000 --- a/mosgi.managedelements.obrprobe/pom.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - org.apache.felix - felix - 0.8.0-SNAPSHOT - - 4.0.0 - osgi-bundle - Apache Felix MOSGi JMX MBean for obr interaction - org.apache.felix.mosgi.managedelements.obrprobe - - - ${pom.groupId} - org.osgi.core - ${pom.version} - provided - - - ${pom.groupId} - org.osgi.compendium - ${pom.version} - provided - - - ${pom.groupId} - org.apache.felix.mosgi.console.ifc - ${pom.version} - provided - - - ${pom.groupId} - org.apache.felix.framework - ${pom.version} - provided - - - ${pom.groupId} - org.apache.felix.bundlerepository - ${pom.version} - provided - - - - - - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} - true - - - MOSGi JMX MBean for obr interaction - MOSGi JMX MBean for obr interaction - auto-detect - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/ - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}.jar - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}-src.jar - ${pom.artifactId} - - ${pom.artifactId};specification-version="1.0.0" - - - org.osgi.framework;specification-version="1.0.0", - org.osgi.service.log;specification-version="1.0.0", - javax.management;specification-version="1.0.0", - org.osgi.service.obr, - org.apache.felix.mosgi.console.ifc;specification-version="1.0.0" - - - - - - - diff --git a/mosgi.managedelements.obrprobe/src/main/java/org/apache/felix/mosgi/managedelements/obrprobe/ObrProbe.java b/mosgi.managedelements.obrprobe/src/main/java/org/apache/felix/mosgi/managedelements/obrprobe/ObrProbe.java deleted file mode 100644 index c9401085621..00000000000 --- a/mosgi.managedelements.obrprobe/src/main/java/org/apache/felix/mosgi/managedelements/obrprobe/ObrProbe.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.managedelements.obrprobe; - -/** - * TODO : Should listen to Agent Service lifecycle - * Need to change ObjectName - * Should listen to serviceLifecycle -**/ - -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; -import java.util.Vector; - -import javax.management.MBeanServer; -import javax.management.MBeanServerFactory; -import javax.management.ObjectName; -import javax.management.NotificationBroadcasterSupport; -import javax.management.AttributeChangeNotification; - -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; -import org.osgi.framework.BundleEvent; -import org.osgi.framework.BundleException; -import org.osgi.framework.Constants; -import org.osgi.framework.ServiceEvent; -import org.osgi.framework.ServiceReference; - -//import org.apache.felix.bundlerepository.BundleRepository; -import org.osgi.service.obr.RepositoryAdmin; -import org.osgi.service.obr.Resolver; -import org.osgi.service.obr.Resource; -import org.osgi.framework.Version; - -import org.osgi.service.log.LogService; - -public class ObrProbe implements BundleActivator, ObrProbeMBean { - - private String version = null; - private static String tabNameString = "TabUI:name=ObrProbe"; - - private ObjectName tabName = null; - private MBeanServer server = null; - private BundleContext bc = null; - private ServiceReference sr = null; - - - //////////////////////////////////////////////////////// - // TabIfc (from ObrProbeMBean) // - //////////////////////////////////////////////////////// - public String getBundleName() { - return this.bc.getProperty("insa.jmxconsole.tab.url.obrprobetab"); - } - - - //////////////////////////////////////////////////////// - // BundleActivator // - //////////////////////////////////////////////////////// - public void start(BundleContext context) throws Exception { - this.bc=context; - this.version=(String)bc.getBundle().getHeaders().get(Constants.BUNDLE_VERSION); - this.log(LogService.LOG_INFO, "Starting obrProbe MBean " + this.version,null); - this.tabName=new ObjectName(tabNameString); - this.sr = context.getServiceReference(MBeanServer.class.getName()); - if (sr!=null){ - this.connectToAgent(sr); - } - this.log(LogService.LOG_INFO, "ObrProbe MBean "+this.version+" started", null); - } - - - public void stop(BundleContext context) { - this.log(LogService.LOG_INFO, "Stopping obrprobe MBean "+this.version, null); - if (this.server!=null){ - this.disconnectFromAgent(); - } - this.sr=null; - this.log(LogService.LOG_INFO, "obrProbe MBean "+this.version+" stopped", null); - this.bc=null; - } - - //////////////////////////////////////////////////////// - // ObrProbeMBean // - //////////////////////////////////////////////////////// - public void deploy(String location,String version){ - ServiceReference sr=this.bc.getServiceReference(RepositoryAdmin.class.getName()); - - System.out.println("Starting "+location+" "+version); - if (sr!=null){ - RepositoryAdmin brs=(RepositoryAdmin)this.bc.getService(sr); - Resolver resolver=brs.resolver(); - Resource ressource = selectNewestVersion(searchRepository(brs, location, version)); - if (ressource!=null){ - resolver.add(ressource); - } - if ((resolver.getAddedResources() != null) && - (resolver.getAddedResources().length > 0)) { - if (resolver.resolve()) { - try{ - resolver.deploy(true); //Bundles are started - }catch (IllegalStateException ex) { - System.out.println(ex); - } - } - } - }else{ - this.log(LogService.LOG_ERROR, "No BundleRepository Service", null); - } - } - - //////////////////////////////////////////////////////// - // ServiceListener // - //////////////////////////////////////////////////////// - public void serviceChanged(ServiceEvent event) { - ServiceReference sr=event.getServiceReference(); - Object service=bc.getService(sr); - if (this.server==null && event.getType()==ServiceEvent.REGISTERED && service instanceof MBeanServer){ - this.connectToAgent(sr); - } - if (this.server!=null){ - if(event.getType()==ServiceEvent.UNREGISTERING && service instanceof MBeanServer){ - this.disconnectFromAgent(); - } - } - } - - private void connectToAgent(ServiceReference sr){ - this.log(LogService.LOG_INFO, "Registering to agent", null); - try{ - this.server=(MBeanServer)this.bc.getService(sr); - this.server.registerMBean(this, tabName); - }catch (Exception e){ - e.printStackTrace(); - } - this.log(LogService.LOG_INFO, "Registered to agent", null); - } - - private void disconnectFromAgent(){ - this.log(LogService.LOG_INFO, "Unregistering from agent", null); - try { - server.unregisterMBean(tabName); - } catch (Exception e) { - e.printStackTrace(); - } - this.server=null; - this.bc.ungetService(this.sr); - this.log(LogService.LOG_INFO, "Unregistered from agent", null); - } - - private void log(int prio, String message, Throwable t){ - if (this.bc!=null){ - ServiceReference logSR=this.bc.getServiceReference(LogService.class.getName()); - if (logSR!=null){ - ((LogService)this.bc.getService(logSR)).log(prio, message, t); - }else{ - System.out.println("No Log Service"); - } - }else{ - System.out.println(this.getClass().getName()+".log: No bundleContext"); - } - } - - private Resource[] searchRepository(RepositoryAdmin brs, String targetId, String targetVersion) - { - // Try to see if the targetId is a bundle ID. - try - { - Bundle bundle = bc.getBundle(Long.parseLong(targetId)); - targetId = bundle.getSymbolicName(); - } - catch (NumberFormatException ex) - { - // It was not a number, so ignore. - } - - // The targetId may be a bundle name or a bundle symbolic name, - // so create the appropriate LDAP query. - StringBuffer sb = new StringBuffer("(|(presentationname="); - sb.append(targetId); - sb.append(")(symbolicname="); - sb.append(targetId); - sb.append("))"); - if (targetVersion != null) - { - sb.insert(0, "(&"); - sb.append("(version="); - sb.append(targetVersion); - sb.append("))"); - } - return brs.discoverResources(sb.toString()); - } - - private Resource selectNewestVersion(Resource[] resources) - { - int idx = -1; - Version v = null; - for (int i = 0; (resources != null) && (i < resources.length); i++) - { - if (i == 0) - { - idx = 0; - v = resources[i].getVersion(); - } - else - { - Version vtmp = resources[i].getVersion(); - if (vtmp.compareTo(v) > 0) - { - idx = i; - v = vtmp; - } - } - } - - return (idx < 0) ? null : resources[idx]; - } - - -} diff --git a/mosgi.managedelements.obrprobe/src/main/java/org/apache/felix/mosgi/managedelements/obrprobe/ObrProbeMBean.java b/mosgi.managedelements.obrprobe/src/main/java/org/apache/felix/mosgi/managedelements/obrprobe/ObrProbeMBean.java deleted file mode 100644 index 69e6782a7e7..00000000000 --- a/mosgi.managedelements.obrprobe/src/main/java/org/apache/felix/mosgi/managedelements/obrprobe/ObrProbeMBean.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.managedelements.obrprobe; - -/** - * Comments : It impossible to load a standard MBean, since introspection - * does not rely on a classloader -**/ - -import java.util.Vector; -import org.apache.felix.mosgi.console.ifc.TabIfc; - -public interface ObrProbeMBean extends TabIfc { - public void deploy(String location, String version); -} diff --git a/mosgi.managedelements.osgiprobes.tab/pom.xml b/mosgi.managedelements.osgiprobes.tab/pom.xml deleted file mode 100644 index f4e90364476..00000000000 --- a/mosgi.managedelements.osgiprobes.tab/pom.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - org.apache.felix - felix - 0.8.0-SNAPSHOT - - 4.0.0 - osgi-bundle - Apache Felix MOSGi OSGi gateway status tab for the JMX console - org.apache.felix.mosgi.managedelements.osgiprobes.tab - - - ${pom.groupId} - org.osgi.core - ${pom.version} - provided - - - ${pom.groupId} - org.osgi.compendium - ${pom.version} - provided - - - ${pom.groupId} - org.apache.felix.mosgi.console.ifc - ${pom.version} - provided - - - - - - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} - true - - - MOSGi OSGi gateway status tab for the JMX console - MOSGi OSGi gateway status tab for the JMX console - auto-detect - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/ - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}.jar - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}-src.jar - ${pom.artifactId} - - ${pom.artifactId};specification-version="1.0.0" - - - javax.management;specification-version="1.0.0", - org.osgi.framework;specification-version="1.0.0", - javax.swing;specification-version="1.0.0", - javax.swing.table;specification-version="1.0.0", - org.apache.felix.mosgi.console.ifc;specification-version="1.0.0" - - - - - - - diff --git a/mosgi.managedelements.osgiprobes/pom.xml b/mosgi.managedelements.osgiprobes/pom.xml deleted file mode 100644 index e5717b2407b..00000000000 --- a/mosgi.managedelements.osgiprobes/pom.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - org.apache.felix - felix - 0.8.0-SNAPSHOT - - 4.0.0 - osgi-bundle - Apache Felix MOSGi JMX MBean for OSGi gateway status - org.apache.felix.mosgi.managedelements.osgiprobes - - - ${pom.groupId} - org.osgi.core - ${pom.version} - provided - - - ${pom.groupId} - org.osgi.compendium - ${pom.version} - provided - - - ${pom.groupId} - org.apache.felix.mosgi.console.ifc - ${pom.version} - provided - - - ${pom.groupId} - org.apache.felix.framework - ${pom.version} - provided - - - - - - org.apache.felix.plugins - maven-osgi-plugin - ${pom.version} - true - - - MOSGi JMX MBean for OSGi gateway status - MOSGi JMX MBean for OSGi gateway status - auto-detect - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/ - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}.jar - http://oscar-osgi.sf.net/obr2/${pom.artifactId}/${pom.artifactId}-${pom.version}-src.jar - ${pom.artifactId} - - ${pom.artifactId};specification-version="1.0.0" - - - org.osgi.framework;specification-version="1.0.0", - org.osgi.service.log;specification-version="1.0.0", - javax.management;specification-version="1.0.0", - org.apache.felix.mosgi.console.ifc;specification-version="1.0.0" - - - - - - - diff --git a/mosgi.managedelements.osgiprobes/src/main/java/org/apache/felix/mosgi/managedelements/osgiprobes/OsgiProbes.java b/mosgi.managedelements.osgiprobes/src/main/java/org/apache/felix/mosgi/managedelements/osgiprobes/OsgiProbes.java deleted file mode 100644 index edb2ba7e4f5..00000000000 --- a/mosgi.managedelements.osgiprobes/src/main/java/org/apache/felix/mosgi/managedelements/osgiprobes/OsgiProbes.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.managedelements.osgiprobes; - -/** - * TODO : Should listen to Agent Service lifecycle - * Need to change ObjectName - * Should listen to serviceLifecycle -**/ - -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; -import java.util.Vector; - -import javax.management.MBeanServer; -import javax.management.ObjectName; -import javax.management.NotificationBroadcasterSupport; - - -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; -import org.osgi.framework.BundleEvent; -import org.osgi.framework.BundleException; -import org.osgi.framework.Constants; -import org.osgi.framework.ServiceEvent; -import org.osgi.framework.ServiceReference; -import org.osgi.framework.ServiceListener; - -import org.apache.felix.framework.cache.BundleCache; - -import org.osgi.service.log.LogService; - -public class OsgiProbes extends NotificationBroadcasterSupport implements BundleActivator, OsgiProbesMBean, ServiceListener { - - private String version = null; - private static String tabNameString = "TabUI:name=OsgiProbes"; - - private ObjectName tabName = null; - private MBeanServer server = null; - private BundleContext bc = null; - private ServiceReference sr = null; - - - //////////////////////////////////////////////////////// - // TabIfc (from OsgiProbesMBean) // - //////////////////////////////////////////////////////// - public String getBundleName() { - return this.bc.getProperty("insa.jmxconsole.tab.url.osgiprobestab"); - } - - - //////////////////////////////////////////////////////// - // BundleActivator // - //////////////////////////////////////////////////////// - public void start(BundleContext context) throws Exception { - this.bc=context; - this.version=(String)bc.getBundle().getHeaders().get(Constants.BUNDLE_VERSION); - this.log(LogService.LOG_INFO, "Starting OsgiProbe MBean " + this.version,null); - this.tabName=new ObjectName(tabNameString); - this.sr = context.getServiceReference(MBeanServer.class.getName()); - if (sr!=null){ - this.connectToAgent(sr); - } - this.log(LogService.LOG_INFO, "OsgiProbes MBean "+this.version+" started", null); - } - - - public void stop(BundleContext context) { - this.log(LogService.LOG_INFO, "Stopping OsgiProbes MBean "+this.version, null); - if (this.server!=null){ - this.disconnectFromAgent(); - } - this.sr=null; - this.log(LogService.LOG_INFO, "OsgiProbes MBean "+this.version+" stopped", null); - this.bc=null; - } - - //////////////////////////////////////////////////////// - // OsgiProbesMBean // - //////////////////////////////////////////////////////// - public String getFwVersion(){ - return this.bc.getProperty(Constants.FRAMEWORK_VERSION); - } - - public String getFwVendor (){ - return this.bc.getProperty(Constants.FRAMEWORK_VENDOR); - } - - public String getFwLanguage(){ - return this.bc.getProperty(Constants.FRAMEWORK_LANGUAGE); - } - - public String getFwOsName(){ - return this.bc.getProperty(Constants.FRAMEWORK_OS_NAME); - } - - public String getFwOsVersion(){ - return this.bc.getProperty(Constants.FRAMEWORK_OS_VERSION); - } - - public String getFwProcessor(){ - return this.bc.getProperty(Constants.FRAMEWORK_PROCESSOR); - } - - public String getFwExeEnv(){ - return System.getProperty("java.version"); -// return (String) this.bc.getProperty(Constants.FRAMEWORK_EXECUTIONENVIRONMENT); - } - - public String getProfile(){ - return this.bc.getProperty(BundleCache.CACHE_PROFILE_PROP); - } - - //////////////////////////////////////////////////////// - // ServiceListener // - //////////////////////////////////////////////////////// - public void serviceChanged(ServiceEvent event) { - ServiceReference sr=event.getServiceReference(); - Object service=bc.getService(sr); - if (this.server==null && event.getType()==ServiceEvent.REGISTERED && service instanceof MBeanServer){ - this.connectToAgent(sr); - } - if (this.server!=null){ - if(event.getType()==ServiceEvent.UNREGISTERING && service instanceof MBeanServer){ - this.disconnectFromAgent(); - } - } - } - - - private void connectToAgent(ServiceReference sr){ - this.log(LogService.LOG_INFO, "Registering to agent", null); - try{ - this.server=(MBeanServer)this.bc.getService(sr); - this.server.registerMBean(this, tabName); - this.bc.addServiceListener(this); - }catch (Exception e){ - e.printStackTrace(); - } - this.log(LogService.LOG_INFO, "Registered to agent", null); - } - - private void disconnectFromAgent(){ - this.log(LogService.LOG_INFO, "Unregistering from agent", null); - this.bc.removeServiceListener(this); - try { - server.unregisterMBean(tabName); - } catch (Exception e) { - e.printStackTrace(); - } - this.server=null; - this.bc.ungetService(this.sr); - this.log(LogService.LOG_INFO, "Unregistered from agent", null); - } - - private void log(int prio, String message, Throwable t){ - if (this.bc!=null){ - ServiceReference logSR=this.bc.getServiceReference(LogService.class.getName()); - if (logSR!=null){ - ((LogService)this.bc.getService(logSR)).log(prio, message, t); - }else{ - System.out.println("No Log Service"); - } - }else{ - System.out.println(this.getClass().getName()+".log: No bundleContext"); - } - } - -} diff --git a/mosgi.managedelements.osgiprobes/src/main/java/org/apache/felix/mosgi/managedelements/osgiprobes/OsgiProbesMBean.java b/mosgi.managedelements.osgiprobes/src/main/java/org/apache/felix/mosgi/managedelements/osgiprobes/OsgiProbesMBean.java deleted file mode 100644 index c86c3dad0df..00000000000 --- a/mosgi.managedelements.osgiprobes/src/main/java/org/apache/felix/mosgi/managedelements/osgiprobes/OsgiProbesMBean.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package org.apache.felix.mosgi.managedelements.osgiprobes; - -/** - * Comments : It impossible to load a standard MBean, since introspection - * does not rely on a classloader -**/ - -import org.apache.felix.mosgi.console.ifc.TabIfc; - -public interface OsgiProbesMBean extends TabIfc { - public String getProfile(); - public String getFwVersion(); - public String getFwVendor (); - public String getFwLanguage(); - public String getFwOsName(); - public String getFwOsVersion(); - public String getFwProcessor(); - public String getFwExeEnv(); - -} diff --git a/mosgi/console.component/pom.xml b/mosgi/console.component/pom.xml new file mode 100644 index 00000000000..601000616f7 --- /dev/null +++ b/mosgi/console.component/pom.xml @@ -0,0 +1,78 @@ + + + + + org.apache.felix + felix + 1.0.4 + ../../pom/pom.xml + + + 4.0.0 + bundle + Apache Felix MOSGi JMX Console GUI component + org.apache.felix.mosgi.console.component + 0.9.0-SNAPSHOT + + + + ${pom.groupId} + org.osgi.core + 1.0.1 + provided + + + ${pom.groupId} + org.apache.felix.mosgi.console.ifc + 0.9.0-SNAPSHOT + provided + + + mx4j + mx4j-jmx + 3.0.1 + provided + + + + + + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + MOSGi JMX Console main GUI component + MOSGi JMX Console main GUI component + ${pom.artifactId} + org.apache.felix.mosgi.console.component.Activator + The Apache Software Foundation + ${pom.artifactId} + + + + + + + + diff --git a/mosgi/console.component/src/main/java/org/apache/felix/mosgi/console/component/Activator.java b/mosgi/console.component/src/main/java/org/apache/felix/mosgi/console/component/Activator.java new file mode 100644 index 00000000000..68975582ce8 --- /dev/null +++ b/mosgi/console.component/src/main/java/org/apache/felix/mosgi/console/component/Activator.java @@ -0,0 +1,68 @@ +/* + * Copyright 2005 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package org.apache.felix.mosgi.console.component; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.apache.felix.mosgi.console.ifc.CommonPlugin; +import java.lang.String; +import javax.management.ObjectName; +import javax.management.MalformedObjectNameException; + +public class Activator implements BundleActivator { + + public static ObjectName REMOTE_LOGGER_ON = null; + static { + try { + REMOTE_LOGGER_ON = new ObjectName("OSGI:name=Remote Logger"); + } catch (MalformedObjectNameException mone) { + // + } + } + + public void start(BundleContext context) throws Exception{ + String propVal=new String("both"); + String propValue=context.getProperty("mosgi.jmxconsole.remotelogger.componentfilter"); + if (propValue!=null) { + if (propValue.equals("treeonly") | propValue.equals("tableonly") | propValue.equals("both") | propValue.equals("none")) { + propVal=propValue; + }else { + propVal="both"; + } + } + + if (propVal.equals("treeonly") | propVal.equals("both")) { + context.registerService(CommonPlugin.class.getName(), new RemoteLogger_jtree(context), null); + } + if (propVal.equals("tableonly") | propVal.equals("both")) { + context.registerService(CommonPlugin.class.getName(), new RemoteLogger_jtable(), null); + } + } + + public void stop(BundleContext context) { + } + +// m_context.registerService( Plugin.class.getName(), new NodeDetails(), null); +// m_context.registerService( Plugin.class.getName(), new BundleListPanel(), null); +// +// m_context.registerService( CommonPlugin.class.getName(), new OBRPlugin(context), null); +// +// m_context.registerService( Plugin.class.getName(), new MemoryLauncher(context), null); +// m_context.registerService( Plugin.class.getName(), new LinuxDetails(), null); +// + +} diff --git a/mosgi/console.component/src/main/java/org/apache/felix/mosgi/console/component/JtableCellRenderer.java b/mosgi/console.component/src/main/java/org/apache/felix/mosgi/console/component/JtableCellRenderer.java new file mode 100644 index 00000000000..c67958d9f0b --- /dev/null +++ b/mosgi/console.component/src/main/java/org/apache/felix/mosgi/console/component/JtableCellRenderer.java @@ -0,0 +1,65 @@ +/* + * Copyright 2005 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.felix.mosgi.console.component; + +import org.osgi.framework.Bundle; + +import java.util.Hashtable; +import java.awt.Color; +import javax.swing.JTable; +import javax.swing.JLabel; +import javax.swing.table.TableCellRenderer; +import java.awt.Component; + +public class JtableCellRenderer extends JLabel implements TableCellRenderer { + + private Hashtable eventName=new Hashtable(); + + public JtableCellRenderer() { + super(); + eventName.put(new Integer( Bundle.UNINSTALLED ), Color.black ); + eventName.put(new Integer( Bundle.INSTALLED ), Color.red ); + eventName.put(new Integer( Bundle.RESOLVED ), Color.orange ); + eventName.put(new Integer( Bundle.STARTING ), Color.gray ); + eventName.put(new Integer( Bundle.STOPPING ), Color.gray ); + eventName.put(new Integer( Bundle.ACTIVE ), Color.green ); + } + + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + setOpaque(true); + if (column==0){ + Integer state; + try{ + state=new Integer(Integer.parseInt((String) table.getValueAt(row,5))); + }catch (NumberFormatException nfe) { + state=new Integer(-1); + } + if (value!=null) { + if (((String) value).equals(JtreeCellRenderer.UNKNOWN_DATE)) { + setBackground(Color.white); + } else{ + setBackground((Color) eventName.get(state)); + } + } + } + setText((String) value); + + return this; + } + +} diff --git a/mosgi/console.component/src/main/java/org/apache/felix/mosgi/console/component/JtreeCellRenderer.java b/mosgi/console.component/src/main/java/org/apache/felix/mosgi/console/component/JtreeCellRenderer.java new file mode 100644 index 00000000000..453191da597 --- /dev/null +++ b/mosgi/console.component/src/main/java/org/apache/felix/mosgi/console/component/JtreeCellRenderer.java @@ -0,0 +1,168 @@ +/* + * Copyright 2005 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.felix.mosgi.console.component; + +import org.osgi.framework.BundleContext; +import java.awt.Color; +import java.awt.Component; +import java.awt.Font; +import java.awt.Dimension; +import java.awt.Toolkit; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.JTree; +import javax.swing.ImageIcon; +import javax.swing.tree.DefaultTreeCellRenderer; +import java.util.StringTokenizer; +import java.lang.StringBuffer; +import java.util.Hashtable; + +public class JtreeCellRenderer extends DefaultTreeCellRenderer { + + public static final String UNKNOWN_DATE="??/??/??"; + public static final String UNKNOWN_TIME="??:??:??:???"; + public static Hashtable ht_num2string=new Hashtable(); + + private boolean isLeaf=false; + private RemoteLogger_jtree rl_jtree=null; + private static final Font FONT_BIG=new Font("Monospaced",Font.BOLD,14); + private static final Font FONT_SMALL=new Font("Monospaced",Font.PLAIN,10); + + private static Hashtable ht_string2color=new Hashtable(); + private static Hashtable ht_string2icon=new Hashtable(); + private static ImageIcon iiOldLog=null; + private static ImageIcon iiNewLog=null; + private static ImageIcon iiNull=null; + + public JtreeCellRenderer(BundleContext bdlCtx, RemoteLogger_jtree rl_jtree) { + this.rl_jtree=rl_jtree; + + String[] states=new String[] { + "Uninstalled", + "Installed ", + "Resolved ", + "Starting ", + "Stopping ", + "Active " + }; + + Color[] colors=new Color[] { + Color.black, + Color.red, + Color.orange, + Color.gray, + Color.gray, + Color.green + }; + + this.iiOldLog=new ImageIcon(Toolkit.getDefaultToolkit().getImage(bdlCtx.getBundle().getResource("icons/OLDLOG.gif"))); + this.iiNewLog=new ImageIcon(Toolkit.getDefaultToolkit().getImage(bdlCtx.getBundle().getResource("icons/NEWLOG.gif"))); + this.iiNull=new ImageIcon(Toolkit.getDefaultToolkit().getImage(bdlCtx.getBundle().getResource("icons/NULL.gif"))); + + for (int i=0 ; iIP = "+/*IP= Profil=/*/dmtn.getParent().getParent()+" Profil ="+dmtn.getParent()+ + "
      Bundle : "+/*Bundle : Id= : */dmtn+ + "
      Date : "+/* -